Differences
This shows you the differences between two versions of the page.
| Both sides previous revision Previous revision Next revision | Previous revision | ||
| blog:ql_dev1 [2021/12/14 12:07] – [GCC Cross-Compiler] john | blog:ql_dev1 [2021/12/16 11:03] (current) – john | ||
|---|---|---|---|
| Line 1: | Line 1: | ||
| ====== Sinclair QL Development Tools ====== | ====== 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:// | ||
| + | |||
| + | I touch on a few of them, including the one I am using (C68, in it's modern cross-compiler incarnation) below. | ||
| ==== C68 Self-hosted on the QL ==== | ==== C68 Self-hosted on the QL ==== | ||
| Line 17: | Line 22: | ||
| * {{ : | * {{ : | ||
| + | * {{ : | ||
| + | * {{ : | ||
| + | === 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 = / | ||
| + | PATH := ${PREFIX}/ | ||
| + | |||
| + | ################################# | ||
| + | # Locations and name of compilers | ||
| + | # and friends | ||
| + | ################################# | ||
| + | CC = qcc | ||
| + | LD = qld | ||
| + | AS = as68 | ||
| + | CPP = qcpp | ||
| + | |||
| + | ################################# | ||
| + | # Paths to additional headers and | ||
| + | # library files | ||
| + | ################################# | ||
| + | #INCLUDES = -I./ | ||
| + | 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): | ||
| + | $(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 | ||
| + | ld v1.22 QL 68000 SROFF Linker | ||
| + | COMMENT: C68 crt_o v4.24f | ||
| + | COMMENT: C68 libc_a v4.24f | ||
| + | |||
| + | --------------------------- | ||
| + | SECTION | ||
| + | --------------------------- | ||
| + | TEXT | ||
| + | DATA 3ad6 2FA | ||
| + | UDATA | ||
| + | --------------------------- | ||
| + | Program length | ||
| + | Relocation table = 1ef | ||
| + | -------------------- | ||
| + | Memory Usage | ||
| + | Buffer Usage | ||
| + | -------------------- | ||
| + | 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 // | ||
| + | |||
| + | If you hexdump the binary, you will see some trailing data that mirrors that // | ||
| + | |||
| + | < | ||
| + | $ hexdump -C game | ||
| + | . | ||
| + | . | ||
| + | . | ||
| + | 00003fa0 | ||
| + | 00003fb0 | ||
| + | 00003fc0 | ||
| + | 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. | ||
| ==== GCC Cross-Compiler ==== | ==== GCC Cross-Compiler ==== | ||
| Line 29: | Line 170: | ||
| 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 | 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 | ||
| + | |||
| + | ==== Working with QL files & devices ==== | ||
| + | |||
| + | 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** | ||
| + | * https:// | ||
| + | * 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. | ||
| + | |||
| + | * **qltools** | ||
| + | * https:// | ||
| + | * Works with native QL floppy disks (DD 720K, HD 1440K or ED 3.2MB) or disk images to transfer files to/from those devices from Linux. | ||
| + | |||
| + | ==== Other Stuff ==== | ||
| + | |||
| + | 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//: | ||
| + | |||
| + | < | ||
| + | # | ||
| + | |||
| + | #################################### | ||
| + | # | ||
| + | # 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(" | ||
| + | sys.exit(-1) | ||
| + | |||
| + | ql_filename = sys.argv[1] | ||
| + | if (os.path.exists(ql_filename)): | ||
| + | f = open(ql_filename, | ||
| + | filedata = f.read() | ||
| + | offset = filedata.find(b' | ||
| + | |||
| + | if offset: | ||
| + | # First byte of " | ||
| + | f.seek(offset + 5) | ||
| + | dataspace = f.read(4) | ||
| + | |||
| + | # Print out the value of the dataspace field | ||
| + | print(int.from_bytes(dataspace, | ||
| + | |||
| + | f.close() | ||
| + | |||
| + | else: | ||
| + | print(" | ||
| + | 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` | ||
| + | </ | ||
| + | |||