To make trying out external code - that is code not stored in the EEPROM - quicker to try out and run then when it is held on the Compact Flash, I have implemented an improvement over the existing method for transferring runnable files (essentially machine code subroutines) between my Linux box and the MAXI09 board. The old method involved transfers with XMODEM. Whilst this method largely worked, it was fiddly because XMODEM transfers need to be initiated on the sending side; the Linux box, which is not very convenient especially if the MAXI09 console is being used.
What I have implemented in its place is a simple file transfer service whereby the name of the file to be transferred is given to the receiver, the MAXI09 board.
This screenshot shows the "getrun" command being used to download and run various binaries:
minicom and immediately try it out, without having to go through the hassle of popping the Compact Flash out of the MAXI09 board, copying the programs across, then putting the Compact Flash back in the MAXI09 board. A big improvement!
The protocol is mind-boggling simple. In this description the client is the MAXI09 board and the server is the Linux box:
- Client sends a command byte 0x01, the "get" operation
- The rest of the sequence is command-specific though in any case only "get" has been implemented
- In the case of 0x01 the filename, with a trailing null byte, is sent next
- The server sends a response code: 0x01 means "data follows", 0x02 means "file not found", 0x03 means bad command, and 0x04 means the file is larger then 64KB
- Assuming the file exists 0x01 is followed by the file size as a 16bit, in big endian order
- Finally the file data itself is sent
- There are no acknowledgements sent from the receiving end as the file is sent; it is essentially sent blind
- Breaking the transfers up into blocks (say 64 bytes) and acknowledging each block transferred
- Introducing hardware flow control
- Disabling task switching - but leaving interrupts enabled - for the duration of the file transfer
Figuring out the cause of the data loss took some time. Initially I suspected the 64 byte FIFO in the UART port in the SC16C654 (PDF) quad UART was overflowing due to interrupt starvation. But after checking the Line Status Register this turned out to not be the case. and it's obvious really: the baud rates used are slow compared to the speed of the CPU and the size of the UART's FIFO.
Instead the problem was an overflow of another FIFO: the circular buffer filled by the ISR and emptied by the sysread call in the UART driver. While other tasks are running, nothing is emptying this buffer so eventually it will overflow (the write pointer moves past the read pointer) causing data loss. The current solution is to insert a forbid at the start of the file transfer and a permit at the end. During the transfer interrupts will be serviced - needed so that the UART's FIFO is still emptied - but only the shell task, where the file transfer code runs, is scheduled. Thus the circular buffer is always emptied fast enough and no data loss occurs. The downside is that no other tasks are running for the duration of the transfer.
The fileserver software itself is based on the old flasher program; the one I use to update the EEPROM on the MAXI09 board. To avoid code duplication, the common routines (serial IO) have been broken out into their own module, which is linked by both the flasher program and the fileserver program. Anyone interested in this code can view it here. The code is not particular pretty, but it works.
While working with external commands, I thought about tackling a little project I've been wanting to complete for a while: running my old Snake game from within MAXI09OS. Not only would the game be started from the Shell (and read either from the Compact Flash or sent over the UART), whilst the game is running other tasks would still be being scheduled. Further, it would be terrific if it were possible to switch between the running game and the virtual consoles running other tasks. Finally, exiting the game should free up all resources and return the user to the virtual console which was used to start the game.
In summary, all this has been achieved. The game code was "ported" from its existing, bare metal, environment to use the facilities from MAXI09OS. One of the things I wanted to avoid was having the game code busy-wait - the previous iteration of the game spends most of its time in a delay loop implemented by decrementing a register until it reaches zero. This has been replaced with the use of a MAXI09OS timer. The joystick driver has also been rewritten to be event-based. Instead of returning the current state of the stick, sysread now returns only changes in position, which are themselves determined by hooking the main system tick. Changes in position generate a signal, which user code can wait on. The main loop for the Snake game thus waits on both timer and joystick events. As before, the game speeds up as the Snake eats the food. This is accomplished by shortening the interval between timer events each time the food is eaten. The interface to the V9558 itself uses the sameAPIs for interacting with the V9958 as the console driver, which happens to be roughly the same mechanism used in the Snake game. For performance reasons it's not really possible to place the V9958 behind a proper driver in the same way as, say, the UART is.
Implementing console switching while the game is running was interesting. This was accomplished by first shuffling the console video memory space around a bit. Previously the console font data and the screen data was spread across the 64KB video RAM memory space. Now the console fonts and screen data is within the first 32KB; the second 32KB is free for other purposes, for example the Snake game's own font and screen data. Actual switching was achieved by extending the console switching routine to adjust all video registers, including the video mode registers. This means that if a graphical mode is active and the user switches to a text-based virtual console, then it will be replaced with text mode. Previously only the screen memory address register were updated to switch the display text to the new virtual console.
Because of possible conflicts between tasks wanting to use non text modes on the V9958, it is only possible to have one non text view active at a time. To claim ownership of the upper half of the video memory for use with alternative video modes, a task registers a video switch callback using the setgraphicsub subroutine. This callback is run, inside the console's ISR, to switch to an alternative video mode when a special "graphical console" function key is pressed, currently F10. In the case of the Snake game, this callback switches the video mode registers to the 32 column tile mode used by the game. When the user presses, say F1, the game switches back to 80 column text mode.
This all works rather well: it is possible to leave Snake running between games (or even during a game, if you are quick) and switch back to text mode to use the Shell, etc. All the while, the system's performance is not too adversely impacted.
I'm pleased enough with this little milestone for MAXI09OS that I've made a brief video. It shows some other things covered later in this blog post:
So, to C compilers.
I've pondered running programs written in something other then Assembly Language for a while now, and I've finally had some success. Whilst there are a number of C compilers that were produced for the 6809 over the years, there seems to be only two which are, more or less, currently being worked on:
- GCC, in the form of some unofficial patches
- CMOC, which is a compiler written specifically for the 6809
- Although they are close to 7 years old, the patches are against GCC 4.3.4, which though somewhat old, supports full C and C++ standards including C99 and C++98 with support for some of the features available in the later C11 and C++03 standard.
- The generated code appears to perform better then CMOC generated code.
- Not to dish CMOC, but it is a fully featured compiler.
- C++ is available as well as C, though I have to used it. The STL is not available.
- It requires the creation of a linker script in order to produce output in a form which can run on a "bare" system like MAXI09OS. Not a big issue.
- It might be my build, but the shipped libgcc contains integer division routines which crash the system. I had to use an alternative implementation and manually link the module containing these routines in, or else the code could not contain expressions with division/modulus.
- Position Independent Code support is patchy. It mostly works, but I had problems getting the compiler to generate sensible code when globals (which were arrays) were involved. I have not spent much time looking at the generated assembly, but it does not look complete.
- There is no C library. Apparently it is possible to use the newlib C library, but I have not had any success getting it to build.
- The 6809 patches are not maintained anymore, it seems.
- Very easy to use: no linker scripts, just point it at the code. The compiler mostly targets at the CoCo, but generating "bare" machine-code files is easy with the Motorola SREC output format, which can be easily converted to a raw binary format with objcopy, a part of binuils.
- Comes with a fairly minimal C library.
- Being actively worked on; the last release (as of this writing) was 26th February. The author happily answered my queries about the software.
- When dumping out the generated assembly files, the code is beautifully annotated with remarks relating to the original C code.
- The inline assembly (required for calls to MAXI09OS routines) is a lot easier to use then GCCs.
- Supports only a subset of C. Some missing features: bitfields (have never liked these anyway), and "const".
- It seems to produce slower code the GCC.
On the subject of the speed of the generated code, my performance measurement was extremely crude. I tested a prime number solver and observed it ran about half the speed of the GCC-build. I have yet to look into why this is the case, but one factor against CMOC is that it passes all function parameters on the stack instead of through registers. I'm unsure if this can account for all the speed difference though.
Because of not being entirely sure which compiler I will end up using for any larger projects, the build system I have implemented for building programs written in C supports both compilers. Getting a build from "the other" compiler is a simple matter of switching a Makefile variable.
A "c" directory in the MAXI09OS repository has been created which contains the relevant files for working on MAXI09OS code written in C. This is broken down into the following files and directories:
Makefile: This is a simple top level Makefile which runs make in the subdirectories.
Makefile.inc: This include file contains shared Makefile commands used by the other subdirectories. The compiler to use (GCC or CMOC) is defined here.
libc/: My very, very trivial C library is here. I have only implemented a few functions: atoi, memcpy, memset, strlen. Also an snprintf implementation has been included, which is based on mini-printf.
libmaxi09os/: This directory contains the interface between programs written in C and the MAXI09OS assembly routines. Only a few MAXI09OS routines are currently exposed: putstrdefio, sysopen, sysread, syswrite and a couple of others. This linkage uses inline assembly: the C function wrapper extracts the parameters passed in (in CMOC's case via the stack) and then uses jsr to call the MAXI09OS routine via the jump table.
libgcc/: Contains the integer division routines required when GCC is the compiler being used.
examples/: Various test programs live here, including the prime number finder.
One of my current concerns lies in the overhead of calling MAXI09OS routines from C code. In a function like sysopen, the steps involved are roughly, assuming CMOC is used:
- The calling code pushes the 3 parameters onto the stack. These parameters are the device name pointer (a word), and the two parameter bytes (eg the UART port number and the Baud rate).
- The C wrapper then loads the correct registers by reading values off the stack. This is done with inline assembly, as is the next step.
- The inline assembly then calls through the MAXI09OS routine via an indirect jsr.
- The return value, if any, is stored into a variable held on the stack.
Since the inline assembly syntax is different between GCC and CMOC, libmaxi09os is pretty much implemented twice, once for each compiler.
Here is the code to the sysopen wrapper, assuming CMOC is being used:
DEVICE m_sysopen(char *name, uint8_t param2, uint8_t param1)
I don't believe anything can really be done about this overhead, short of using inline assembly at the calling point. But if that was done, the whole program might as well be written in assembly.
All told, I'm pretty happy about being able to write user code in C. I'm not sure where I'll use it though, yet.
So far I've written (or converted) a bunch of programs and have ran them up on the board:
- io.bin: A simple a. print a prompt, b. get a string, c. output the string demo.
- gettime.bin: A rewrite of the SPI RTC time-display program.
- prime.bin: Asks for a number, and then prints all the prime numbers between 2 and that number.
- easter.bin: Prints the dates for Easter for the years between 1980 and 2020.
As usual, I have a bunch of ideas for what to work on next, including:
- Tidy up keyboard support: need to add key debouncing, utilities for setting the key repeat parameters, and fix some other little issues.
- Get my Citizen 120D+ 9 pin dot-matrix working. This needs a driver for the VIA parallel port, and some utilities to print files. I really want to do this!
- Write a driver or other routines for the OPL2 sound IC. Not really sure of the best way to approach this though!
- See about porting a more complex C program to MAXI09OS. A text adventure game might be a nice target.
Lots of ideas, and only so much time...