So far the following filesystem code has been written:
- MBR parsing reads the offset into the first partition (if there is no MBR then the entire disk is the partition).
- From this offset, the Minix superblock is read in. From this block, the filesystem type can be determined, and the offset into the inode table can be worked out.
- inodes are 32bytes long, so 32 inodes can fit in a 1Kbyte block. Inodes are stored in consective sequence following the superblock. So to read the inode number N, N needs to be divided by 32 to work out the block number to read (with the inode start offset added), and then the remainder used to work out the starting point of the inode within the block. This, of course, can be done with bit shifting and masking.
- inodes contain links (block numbers) to data blocks, for both files and directories. They also contain a 16 bit word with bits for type (file, directory, symlink etc) and permissions. Also an inode contains the file (or directory) size in bytes. Of course the filename is not in the inode, but is instead in a directory entry. A routine has been written to summarise this info, for a given inode.
- Directory entries are 32bytes long, with the first two bytes holding the inode number, and the remaining 30 bytes containing the file or directory name, padded with null characters.
- So to list a directory by inode, the inode needs to be read in, and the datablocks followed. For each directory entry the name can be retrieved. To know if the entry references a file or directory, the inode for the entry can be read in, and the type (directory, file, devnode, etc), file size and permissions can be determined. Without following the inode pointer when listing a directory, only the filename can be known. Linux's ls command works the same way; only if an option like -l is given will inodes be loaded for each file to be listed. This is the "l" monitor command, which takes an inode number for the directory to list. 0001 is the root inode.
- To read the content of a file by inode, a similar process can be followed except that the referenced datablocks are file data and not directory entries. This is the "f" monitor command. It takes a starting memory location and an inode number.
One interesting quirk of reading this data from the CompactFlash is to do with endianess. Since the Motorola 6809 is Big Endian, all words (and longs) that are read from the CF which are things like block numbers or file sizes have to be byte swapped, because the CF is written to be a Linux PC.
The following screenshot shows the monitor being used to mount a partition, read the superblock, list the root directory, and then read a file (me.jpg at inode 2) into RAM. When listing a directory, the inode number, type (with file mode), and file size (in hex) is shown. Not very user friendly, but serviceable. The first 64 bytes of the file, now in RAM, at $4000 is then displayed. You can see it is a JPG file from the first few bytes in the file. Reading a file by filename will be much more useful then reading files by inode number!
While working on the monitor, I finally took the plunge and restructured the code for the ASxxxx assembler. This has a number of advantages, including much friendlier ways of holding static data (including null terminated strings) in the code, and it generally has a more "modern" feel to the assembly syntax, using 0x instead of $ etc. The monitor is now spread across half a dozen files, with a file for serial communications, a file for string handling etc.
At the same time, some of the "reusable" monitor functionality is now exported through a jump table. Jump tables are an age-old technique for publishing APIs (in this case, through a function number) to user-level code through a table of subroutine jumps. Thus, to execute a particular ROM routine (to output a string to the serial port, say), the user-level code meerly jumps to the table position for the routine in question. The table, in term, is a sequence of jump instructions into the real routine. With this technique, if the location in ROM for the real routine has to change because of implementation changes, then only the jump inside the jump table needs to change. The position that the user level program jumps to can remain the same. Thus the "binding" between user code and ROM code can be static and nothing needs to break when the ROM is changed. As long as newly exported routine jump table entries are added to the bottom of the table, and not anywhere in the middle, the routines available to user code can be expanded.
None of this really matters to my little computer; I could easily rebuild user programs when the ROM is changed and just use direct jumps instead of indirect ones. But it is satisfying to do things "properly".
With all this in mind, I have implemented two real programs which are loaded from CompactFlash and executed as user code:
- showtime.bin: performs SPI reads and displays the time, much like the "t" command in the monitor (which has now been removed).
- settime.bin: prompts the user to enter the time and date before performing SPI writes to set the time and date accordingly.
The following screenshot shows these two programs in action. The settime program (at inode 3) is read into location $4000, and the showtime program (at inode 4) into location $4200. Each program is then run:
The new monitor code, as well as the two user programs have been added to github, in case anyone is interested.
Another thing implemented in the last few of weeks is sound. I've had a couple of General Instrument AY-3-8192 chips in the parts drawer for months, and have finally got around to integrating them into my circuit. These chips (and there variants) were amazingly popular and were used in many home micros including the Sinclair Spectrum 128K, Atari ST, and many, many arcade machines. These ICs are a little unusual in that they were designed to integrate seamlessly into GIs slightly strange CPU bus. This bus has multiplexed data and address lines, and instead of Chip Select, Read Write and friends has Bus Direction and two Bus Control lines. Glue logic is needed to convert the 6809 R/W and CS signals into the BDIR and BC lines needed by the sound IC. After playing with several glue logic schemes, I ended up using the same logic as Matt Sarnoff used in this 8bit computer.
In summary my computer now beeps when it starts! I have written some generic sound routines, but unfortunately I'm not musically talented... I might have a go at covering some sheet music later though.
In other hardware news, I am now using the PCB I made up for the core computer. Here is a picture of the top of the board:
The decoupling caps are yet to be soldiered. Seems to work ok without them, but I'll get round to it one day.
And here is a picture of the bottom:
As can be seen there were a couple of small errors:
- The largest and most embarrassing problem is with the rest button. Somehow when making a lovely custom PCB footprint I managed to get the orientation wrong. This required cutting a track and a small jumper wire.
- The crystal was not given enough room. I fixed this by standing the crystal off the board by a few mm, just enough to clear the lead of a nearby resistor.
- I really wish I'd remembered to leave room for the feet, or (better yet) include some holes for some proper mounting posts. Next time....
I have made one other cool improvement which I'm really happy about. I have now ditched the 6850 UART in favor of a 88C681 (PDF) DUART. This is an 8 bit bus compatible version of the 68681, a DUART for the 68000 series of MPUs released by Motorola around 1985. Other then having two UARTs in one, the other big advantage is that it has an integrated baud rate generator. This allows me to divorce the system clock from the baud rate used on the console. My computer is thus now running on a 8Mhz crystal, with a 3.864Mhz crystal now driving just the DUAT. I have taken the opportunity to crank the monitor baud rate up to 115,200 baud in the process. Software wise, interfacing this chip was only moderately harder then interfacing the 6850, with just some extra registers to setup. It is still polled; interrupts are something I would like to look at soon.
Here's a picture of the PCB hooked up to the breadboard:
I jury-rigged a 3.5mm socket out of a small piece of stripboard and a socket which was salvaged from an old MP3 player. Quite pleased with this bodge: the socket is "surface mounted" to the back-side of the strip board. I had to splay the leads slightly to get the (non 0.1") connectors onto the stripboard track, but it seems to work well.
One small problem I'm having is with power. The various IC in the circuit consume more power then can be reliably drawn from a USB hub. While I have always wanted a bench PSU, I think I will instead buy an old PC AT power supply. They are a damn site cheaper, and I'm unlikely to ever need anything other then 5V. A good computer PSU will easily give me 10A at 5V, far more then my little computer will ever need, even with its inefficient NMOS parts.
And here are the two circuits. First the core computer, much as it was in the last post:
And finally the long awaited IO circuit:
Future plans now include:
- Get filename searching working. in the filesystem code.
- More sounds with the AY. Maybe Greensleaves because I like the tune? Or another simple tune.
- Investigate the address decoding oddities.
- Play with interrupts.
- Start designing a proper SBC board incorporating the DUART and IDE interfaces, and possibly sound and the VIA.