A long time ago, back in 1979 when I was working in California, I went to a series of presentations given by Intel, Motorola, and Zilog, at which overviews of the then-new 8086, 68000, and Z8000 were given. I remember being much impressed by the 68000, but although I've subsequently owned a couple of 68k-based Macs, at no point did I ever do any work in assembler on a 68000 - until recently.
At the end of the summer in 2010, I poked around the web a bit, and was pleased to notice that I could, for a fairly modest sum, obtain a single-board computer with a 68000 and a couple of serial I/O ports. This machine is a Kaycomp II, and it comes with 64k of ROM with a simple monitor, and 256k of RAM. I got mine from Magenta Electronics Ltd (although it appears no longer available from there). A 2-port USB to serial converter for my Mac Mini completed the initial hardware setup. More recently I sourced four 512kbyte memory chips, and so maxed out the memory to 2Mbytes.
I've been using the Mini as a development host, and later I'll describe the software development setup as well as the software itself.
BORG? Oh well, that stands for Board ORGaniser, a style of name copied from earlier work (see this CERN Report, P24 et seq).
This is what the board looks like. I've included a couple of closer pictures so it's easier to see the patches I've added. As supplied, the interrupts from the serial I/O chip and the clock chip are not connected. Using these was a prerequisite for the software I wanted to write, and the patches were added for that purpose. You can click on any of the small images below to see a larger image (which will open in a new window or tab).
The board comes fully assembled, with all chips already socketed. The only changes I've made have been to replaced the supplied RAM as already described, and to add some patches.
For the added memory, there is a small patch to supply A18 to the memory chips. For the 68681, a two-port serial I/O chip, IRQD is connected to IRQ5. For the 68230 clock chip, IRQT is connected to IRQ6. In order to ensure that the CPU doesn't try to use auto-vectors, but rather uses those supplied by the interface chips, VPA has been tied to +5V. Finally, whereas the 68681 was supplied with an IACK signal, this did not seem to be the case for the 68230, so this has been patched from the IC18 socket. That's all the patching I did (not very expertly, I must admit).
I'd also say that it's quite possible that different patching would achieve the same result in a tidier way, but what I've done appears satisfactory for this purpose. I was able to work out what needed patching, and where, by having a little knowledge about how chips work (such as knowing what chip-select means, and what address and data lines are), and by reading the supplied documentation and the Principles of Operation for the various major chips on the board. The PoO documents are available on the web if you poke around a little.
Software for the board is cross-assembled on my Mini, and downloaded from there. The two serial ports on the board are connected to a two-port serial-to-USB device, which in turn is plugged into the Mini and, via its driver, presents two serial ports to applications on the Mini.
Development software consists of a cross-assembler and a linker. The assembler needs various options, so to simplify life I wrote a PHP script to make it easier to use. Most of the options I don't ever alter - just the one that relates to whether to generate a listing or not.
Creating a downloadable file for BORG consists of assembling and then linking, to produce a file in Motorola SREC format (q.v.). This will be downloadable by your terminal emulator program.
The cross-assembler is written in C, and built using Xcode 3.
I found an M68k cross assembler, called vasm. In fact, it supports a variety of CPUs, and supports more than one assembler syntax style. It comes with a makefile, but I ignored that and picked out the components I wanted (M68k CPU and syntax files), put those along with all the other common stuff in a folder, and let Xcode build the module. There were some warnings but none required any action.
The cross-linker is written in C, and built using Xcode 3.
The linker, vlink, is written by the same people who did the assembler. I built that in a similar way to vasm, letting Xcode worry about details like where the compiler is.
The software that comes with the board is in EPROMs. It provides facilities for downloading and running programs, but as it is in ROM, you have to make do with what it provides in terms of TRAPs and interrupt handling. For some interrupts and TRAPs, it allows the user to intercept these by having the interrupt or TRAP vector point into RAM, with those RAM locations initialised to point back to default handlers in ROM. By overwriting these RAM locations, the user's program can take control of those facilities. However, things like illegal instruction and other exceptions are still handled entirely by the ROM Monitor.
I didn't feel like trying to put BORG into ROM myself, because this would have involved writing a lot more handlers, as well as procuring an EPROM eraser, an EPROM programmer, and some EPROMs, all with uncertain results. It might, for example, have been interesting to try to write exception handlers for certain instructions provided on the 68020 and up, such as 32-bit divide. However, as it is, I didn't take that path and so BORG as written is limited in the TRAPs it can use, and starts at location 500000 hex rather than at location zero.
Someone intending to put BORG into ROM would have some work to do to adapt it. In particular, the startup code would need to copy the static Task Control Blocks (TCBs) and Device Control Blocks (DCBs) from ROM into RAM.
What all this means is that as I have written it, BORG has to be downloaded into RAM whenever the board is powered up, or if it is suspected of having been overwritten. You then use ROM command to transfer control to BORG.
BORG is designed as an interrupt driven multitasking system. Major features include:
Everything is driven by the system clock, which interrupts the CPU 25 times a second. If a task asks for a clocked wait, the task's context is saved in its TCB, and the task queue examined to see if another task is able to run. The highest priority task is then resumed, or, if none can run, the CPU executes a STOP instruction (which is externally visible as it causes the DTACK LED on the board to go out).
If the CPU stops, then an interrupt is needed for something more to happen on the system. This could be from the clock, or from an I/O device. As supplied, the board has a two port serial I/O chip. BORG contains a built-in task called KeyTask, which provides commands for the user to examine system operation. For example, via KeyTask it is possible to download a code block, run it as a new task, alter the task's priority, pause the task, or force it to terminate.
In the default setup, there are two instances of KeyTask, one for each port. To release a port for another purpose, either rebuild BORG, or use KeyTask's terminate-task command.
Starting and waiting for I/O is handled for tasks by TRAPs, so a task does not need to concern itself with the internals. Briefly though, when a task initiates an I/O, an IOB is created, with details such as byte count, buffer address, and the DCB address for the device. I/O is then started, and the device marked busy with an I/O. If the same or another task wants to start another I/O, the second IOB will be queued. When the first terminates, the next one will be started.
As things stand, all IOBs are chained into one queue. This is probably sufficient for small amounts of activity, but it would be easy enough to modify BORG to use one queue per device if an application needed to do many small I/Os on a number of devices with good response.
A task can start many I/Os and then wait on one, so double buffering is easy to do. In fact KeyTask does this when downloading code blocks.
The interrupt handler for the serial ports can do either block input, whereby all characters received are passed back to the task which requested the I/O, or keyboard input.
Keyboard input means that the input stream is expected to come from someone typing at a keyboard. So, input is echoed, and if a backspace arrives, then a backspace sequence iof characters is output to erase the last visible character typed and to move the cursor to the left. You can move the cursor around with the left and right arrow keys, and typing while the cursoris not at the end of the line will then insert characters. Forward delete deletes one character to the right, the cursor does not move, and the line of visible text gets shorter. All of this is handled by the interrupt handler, and the task does not have to be concerned with it. Instead, the task will be given the visible line of test when the user presses the return key.
The user can also press CTRL-C to abort input, and the task's I/O will complete with an error code.
A list of commands with a brief description of each may be obtained by typing "help" at the "borg>" prompt. Commands take arguments, and generally if they are omitted they will be prompted for. Commands should be entered in lower case and may be abbreviated to their shortest unambiguous form.
The full list can be found here.
There is no facility within BORG to dynamically add a Device Control Block (DCB); they have to be added to the source code, which would then have to be re-assembled. When BORG starts, one of its startup actions is to look down a simple list of device blocks and add them to the device chain (which means that adding a feature into BORG to do that dynamically would not be hard). Statically adding a device, then, just means adding to that list, as well as incorporating the DCB itself.
Details of how to do this may be found here.
You can add static tasks to BORG in a similar way to adding devices. See here for details of that.
Dynamic tasks can be created at any time. You must have previously loaded a code block, however, that the task will execute. This can be done with keytask's load command. Then the keytask run new command will create a TCB, allocate stack space for the task, and add it to the task list. Information such as the required stack size is contained in the Task Initialisation Block (TIB), as shown in the following example.
; Task Initialisation Block (TIB) offsets ; taskiprty: equ 2 ; Task priority taskistsize: equ 4 ; Stack size taskiname: equ 8 ; Task name ; ; ; flashertask: this Task flashes the dtack light at some rate ; flashertask: bra .l1 ; TIB header ; dc.b 100 ; Flasher task priority dc.b 0 ; Unused dc.l 5 ; Stack size dc.b "FlasherTask " ; Task name dc.l 0 ; No device needed ; .l1: move.l #$12345678,-(sp) ; Real code for task starts here ... ; etc
The code should start with a branch around the TIB, which contains information used by the task initialisation code. In particular note the task priority, stack size (in longwords), and name (up to 12 bytes). The address of a device that the task might do I/O on, may also be provided.
Note that this structure should also be followed for static tasks. A task structured in this way can be run as a task, or as an extension to KeyTask, using the exec command.
Mostly, tasks are going to be re-entrant by default. If you expect to put BORG into ROM, and want to include a static task, then it will need either to keep its local variables on the stack or have to request a memory block to keep them in. If you are going to be managing a number of similar devices, and so want one code block to be used by many tasks, as well as having those tasks report onto a serial device, you may need to modify BORG's initialisation to include a second device list. This could involve adding space to the TCB for a second device pointer.
If you are running BORG in RAM, and have a task which only needs a single instance, then there is no problem with storing some data locally with the code, although it is probably better practice to keep them separate.
Creating such a block, that can be downloaded by KeyTask's load command, is similar to the process for BORG itself. Assemble and then link the file to produce an SREC format file. Then, there is one extra step. The SREC format has no record type that can indicate the size of the block, which would allow KeyTask to pre-allocate a buffer for it. So, since there is no S4 record defined by the SREC format, I have used that record type to contain the block size.
Therefore, after assembling and linking the code block, you must pass the resulting SREC file through a post-processing step. This may most conveniently be done using a PHP script (srecfix) that is included in the BORG download.
BORG uses the 68000 trap instruction for basic system calls such as allocating memory, and for task manipulation functions. Ordinary library functions may be access via a subroutine call or via a trap.
You can read all about that here.
If you want the source code for BORG, plus the srecfix script mentioned above, and a couple of simple example tasks, you can download it here.