blog:ql_dev1

Sinclair QL Development Tools

There are a range of development tools and languages for the QL, including the built-in SuperBASIC. But I am more interested in using the same type of tools that I use for the NEC PC-98, MS-DOS, X68000 and more; a reasonably standards compliant C compiler, assembler and linker.

There are a few C compiler options for the Sinclair QL - which are summarised nicely here: http://www.dilwyn.me.uk/c/index.html

I touch on a few of them, including the one I am using (C68, in it's modern cross-compiler incarnation) below.

Purported to be the main C compiler for Sinclair QL systems. This is the original version intended to run on the QL itself.

A port of C68 to DOS, Windows and Linux systems was made in the late 90's to earl 2000's. Unfortunately it has bit-rotted quite badly and does not run on any current systems. The Git repository above is a reworked, cleaned up version that compiles perfectly on modern 64bit Linux systems.

As suggested, this runs as a cross-compiler; targetting the Sinclair QL QDOS runtime with the convenience of using all of the modern development tools, source code revision of a GNU/Linux system.

I've posted a pre-compiled version of the toolchain, with the original C68 runtime libraries (crt.o, libc.a et-al) and system headers, below.

  • c68_linux64-2021_12_14.tgz - Pre-compiled, with headers and runtime libraries. Unpacks to /opt/toolchains/qdos
  • c68.pdf - C68 User Manual. For the original Sinclair QL hosted version, but most of the compiler and linker flags and options remain the same.
  • c68_documentation_-_dave_walker.pdf - A more in-depth manual about the use of C68, including a basic reference of available calls.

Sample C68 Makefile

You can of course use standard GNU Makefiles alongside C68, here's a basic one that compiles a single binary from two .C source files, as you would normally do with anything of reasonable complexity.

Note that I specifically do not have my C68 toolchain directories in my path, as I have far too many compilers installed to manage them if they were all fighting for priority on the path. I tend to set things up so that I either temporarily add them to the front of the path, as per the makefile below, or have a shell script temporarily set the path for a given session.

It's often easier to keep paths and things like that encapsulated in the Makefile, so you can see that at the top of the example below:

# Sample makefile for C68 projects

#################################
# Temporarily add the cross
# compiler location to the path
#################################
SHELL := /bin/bash
PREFIX = /opt/toolchains/qdos
PATH := ${PREFIX}/bin:$(PATH)

#################################
# Locations and name of compilers 
# and friends
#################################
CC = qcc
LD = qld
AS = as68
CPP = qcpp

#################################
# Paths to additional headers and
# library files
#################################
#INCLUDES = -I./my_includes -I./other_includes
INCLUDES = 
#LIBS	 = -L./my_libs -lmylib
LIBS = 

#################################
# Compiler flags
#################################
ASFLAGS = 
LDFLAGS = -v
CFLAGS = -O

#################################
# What our application is named
#################################
TARGET = game

#################################
# Targets to build/run
#################################
all: $(TARGET)
#dev:
#test:

#################################
# Object files needed to build our 
# main application
#################################
OBJFILES = test.o other.o

#################################
# Main application target build recipe
#################################
$(TARGET):  $(OBJFILES)
	$(LD) $(LDFLAGS) $(OBJFILES) $(LIBS) -o $(TARGET)

###############################
# Clean up
###############################
clean:
	rm -f *.o
	rm -f $(TARGET)

################################
# Each C source is compiled to
# an object file, to be linked
# as a single binary, above.
################################
test.o: test.c
	$(CC) $(CFLAGS) $(INCLUDES) -c test.c
	
other.o: other.c
	$(CC) $(CFLAGS) $(INCLUDES) -c other.c

Running C68

If you use a sample makefile such as the one above, you should get an output such as this:

$ make
qcc -O  -c test.c
qcc -O  -c other.c
qld -v test.o other.o  -o game
ld v1.22  QL 68000 SROFF Linker
COMMENT: C68 crt_o v4.24f
COMMENT: C68 libc_a v4.24f

---------------------------
SECTION      START   LENGTH
---------------------------
TEXT             0     3AD6
DATA          3ad6      2FA
UDATA         3dd0      4EE
---------------------------
Program length   =     42be
Relocation table =      1ef
--------------------
Memory Usage     =       0%
Buffer Usage     =       0%
--------------------
game: dataspace 870 (366)

Link completed

This is mostly easy to understand - two passes of the compiler; one for each source file, then the linker takes the two object files, adds-in crt.o and any functions from the standard library. The output is a little more verbose than the usual compiler though - an important element being the dataspace that the resulting binary will take up in the QL memory.

If you hexdump the binary, you will see some trailing data that mirrors that dataspace number:

$ hexdump -C game
.
.
.
00003fa0  04 04 04 04 04 04 04 04  04 0e 14 0c 04 04 0c 16  |................|
00003fb0  14 26 08 04 04 04 04 48  04 06 60 01 24 38 00 00  |.&.....H..`.$8..|
00003fc0  58 54 63 63 00 00 03 66                           |XTcc...f|
00003fc8

This is referred to as the xtcc field and is used by the qlzip utility when unpacking on the QL native filesystem to restore the needed metadata about the application.

This has the potential to be a great resource for people such as myself who are primarily unix developers. Sadly, this was a project started a long time ago, and as such, is based on a horribly dated version of GCC 2.95.3 - almost 20 years old at the time of writing (2021).

As such, it is a real pain to get working on any reasonably modern version of Linux. The project above has attempted to make it easier by building all of the tools inside an earlier Debian-based docker container, but I simply could not get it to work reliably; the install procedure is a bit of a mess of installing components to bootstrap the build of other components, then overwriting various binaries with the stages of later output. It's all a bit of a mess (and no fault of the guy trying to make it easier!).

It should, in theory, build outside of a docker container, and I got it mostly working, but when invoking the built version of gcc, the linker stage of a test C program would blow up with a signal 6 (ABORT).

It's a shame, as other platforms (Atari ST, for example) that are very similar to the QL, have much more modern versions of the GCC

The QL has it's own filesystem and files have attributes which are not directly equivalent to anything on a modern filesystem - storing the size of the amount of memory needed to load the binary, for example. So there are some tools needed to get anything you are working on, say in a Linux filesystem, into a QL-native format.

  • qlzip
    • Any binaries produced by C68 can be zipped up, have their extended data stored, and then unpacked with the correct data in place using a native QL unzip tool.

I wrote a quick Python script to print out the value of an XTcc field in a binary produced by C68, GCC or similar. This is useful when copying an executable into a QL floppy or hard drive filesystem and you need to set the dataspace parameter:

#!/usr/bin/env python

####################################
#
# Prints out the value of a Sinclar
# QL binary XTcc field, as needed to
# turn it into an executable file
# on the QL itself, or when transferred
# using qltool.
#
# John Snowdon, 2021
#
####################################

import sys
import os

if len(sys.argv) < 2:
	print("ERROR: No filename given")
	sys.exit(-1)

ql_filename = sys.argv[1]
if (os.path.exists(ql_filename)):
	f = open(ql_filename, "rb")
	filedata = f.read()
	offset = filedata.find(b'\x00XTcc')
	
	if offset:
		# First byte of "XTcc" is at 'offset', so jump by 4 bytes
		f.seek(offset + 5)
		dataspace = f.read(4)
		
		# Print out the value of the dataspace field
		print(int.from_bytes(dataspace, byteorder='big'))
	
	f.close()
	
else:
	print("ERROR: Filename does not exist")
	sys.exit(-1)

Just call the script with the name of your C68 executable. For example:

$ xtcc game.bin
868

… the value printed is the size of the dataspace needed. So you would then copy and mark your binary with qltools as follows:

$ xtcc game.bin
868
$ qltools floppy.img -W game.bin
$ qltools floppy.img -x game.bin 868

Or even easier, embed the xtcc call in backticks:

$ qltools floppy.img -W game.bin
$ qltools floppy.img -x game.bin `xtcc game.bin`
  • blog/ql_dev1.txt
  • Last modified: 2021/12/16 11:03
  • by john