Teensy Z80 – Part 3 – File System, SD Card, VRAM?

This is the third part of a series of posts detailing steps required to get a simple Z80 based computer running, facilitated by a Teensy microcontroller. It’s a bit of fun, fuzing old and new hobbyist technologies. See Part 1 and Part 2, if you’ve missed them.

Now we have the base Z80 working, interrupts and a display connected which can be manipulated in a console/terminal fashion using the Z80 I/O ports. The next step? File storage!

The obvious choice for file storage here is an SD card. It uses ~3v3 logic, which is what we are running everything with, and also uses the SPI bus, which we already have set up for our LCD screen. We’d need another pin on the Teensy for the SD chip select, and also another for the MISO line reading data from the SD – the LCD only ever used MOSI for input.

schematic_tft_sdA peek of what is covered in this post:

Interfacing for SD cards

Now, as I keep stressing, this is just a little fun exercise. So to make things (a lot) easier, the Teensy will actually handle all of the FAT file system behind the scenes work. I’m sure I could get it all ported, or find a Z80 FAT16 implementation already, but I like the pace this project is moving at – so we will cheat more!

I exposed 3 I/O ports to the Z80. I could probably combine them, but for now, I will stick with three:

  1. Opening/closing files
  2. Read/writing files
  3. Performing ‘nextfile’ operations on directories

I won’t go into too much detail on the Teensy side of things. The code is all available on my github project page if you want to look. I Teensy code uses SdFat, which means I need to license the Teensy Z80 code as GPL (for those who don’t know, my stance on GPL is “ugh“, but will abide by its demands).

I will, however, detail the I/O ports, commands and the data structures used to communicate the operations. The major file system functions – open, read/write, next – are implemented in such a way that you place required information in a section of memory, give that memory address to a port, and then tell it to execute the operation. So, for the ‘Open’ command, we set aside an area of memory with the following structure:

  openfile_cmd_data {
 0:    uint8_t error;    // operation writes
 1:    uint32_t size;    // operation writes
 5:    uint8_t type;     // operation writes
 6:    uint8_t flags;    // operation reads
 7:    char    name[13]; // operation reads, 8.3, null-terminated
  }

For open, we provide the name and flags in the structure before initiating the open command. Flags are whether we are opening for reading, writing, appending, etc. The open command itself will write the error, size and type fields. Error and size are self explanatory, the type field is for extra information, such as if this file is actually a directory.

Performing this in the Z80 assembly looks like the following:

      ; definitions of ports, commands, flags required
PORT_FILESYS_OPEN_CLOSE equ 11
FILESYS_OPEN_OPENFILE   equ 5
FILESYS_OPEN_SETMEMPTR  equ 6
OPEN_READ               equ 0

...

      ; area of memory for openfile_cmd_data
filesys_readme_open_read:
  defb 0ffh, 0,0,0,0,  0,  OPEN_READ,  'README.TXT',0,0,0

...
loadreadme:
  ld a, FILESYS_OPEN_SETMEMPTR       ; the 'Set Memory Pointer' command
  out (PORT_FILESYS_OPEN_CLOSE), a   ; tell the port we are giving it memory
  ld de, filesys_readme_open_read
  ld a, e
  out (PORT_FILESYS_OPEN_CLOSE), a   ; give the port 8 bits of address
  ld a, d
  out (PORT_FILESYS_OPEN_CLOSE), a   ; give the port the other 8 bits
  ld a, FILESYS_OPEN_OPENFILE
  out (PORT_FILESYS_OPEN_CLOSE), a   ; initiate the OPENFILE command
                                     ; - operation is immediate.
  ld a, (de)                         ; load the error byte
  or a
  jr nz, Lfile_fail                  ; if non-zero jump to fail handler

...

I’ve left out the previous step to this, which is actually opening (and closing) the root directory. It’s implemented as a special case of open where “/” is the filename. But you can see the code is very straightforward.

We can now use the information written at filesys_readme_open_read to help read the file contents. The file size is written to the 4 bytes after the error byte, so we cam read that in as to see how much memory we need to read the whole file into memory.

...
  ; assume < 256b file for now
  ld de, filesys_readme_open_read + 1
  ld a, (de)
...

It we read the first byte, as the system is little-endian we will read the file size if the file is less than 256 bytes. We’ll assume README.TXT is (well, we know it is) and so for simplicity will just read this once.

With this information, we can set some memory aside to store the file contents. We could use the stack by removing the bytes from there, or in my case, just have a fixed address in memory where program RAM can begin. We now need to fill out a memory read/write request structure:

   read_write_command {
 0:    uint8_t error_code;      // writes
 1:    uint8_t op_type;         // reads, CMD_READ(0) or CMD_WRITE(1)
 2:    uint16_t file_offset;    // reads, ignored if OPEN_APPEND
 4:    uint16_t block_size;     // reads,
 6:    uint16_t mem_buffer_ptr; // reads,
   }

The file_offset is where you’d typically seek() to before a read. The block size is the size of the read/write request, and the mem_buffer_ptr should point to a further area of memory that is at least of size block_size for the file system to write into (or read from given a write instruction).

filesys_read_request:
  ; error_code, op_type, file_offset_lo,file_offset_hi,block_size_lo,block_size_hi
  defb 0ffh, 0,  0,0,  0,0
  defw scratch_mem

scratch_mem:
  dc 128,0 ; 128 bytes of zero for scratch memory

After opening the file, we can insert the size of the file into the block_size_lo part of the data, to read the whole file starting from offset 0.

  ; assume < 256b file for now
  ld de, filesys_readme_open_read + 1
  ld a, (de)

  ld hl, filesys_read_request+4
  ld (hl), a

Now we use the same style of port i/o to provide the read/write port with the location of this structure in memory, and fire off the EXEC command to perform the operation.

  ld a, FILESYS_RW_SETCMDMEMPTR
  out (PORT_FILESYS_READ_WRITE), a   ; give the i/o port the command ptr
  ld de, filesys_read_request
  ld a, e
  out (PORT_FILESYS_READ_WRITE), a   ; give the i/o port the command ptr
  ld a, d
  out (PORT_FILESYS_READ_WRITE), a   ; give the i/o port the command ptr
  ld a, FILESYS_RW_EXEC
  out (PORT_FILESYS_READ_WRITE), a   ; execute the command
                                     ; - operation is instant
  ld a, (de)                         ; load the error byte
  or a
  jr nz, Lfile_fail                  ; if non-zero jump to fail 

        ; scratch_mem now contains file content, ascii text
  ld de, scratch_mem
  call print_string                  ; print that content to the screen
  call newline

  ld a, FILESYS_CLOSE_FILE
  out (PORT_FILESYS_OPEN_CLOSE), a   ; close README.txt

This setup allows for quite a decent amount of functionality. I can read files much larger than the available RAM (16KB as I write this) due to providing the seek location in the file as a file_offset in the read command. Writing acts exactly the same, except that scratch_mem would contain what I wanted to write to the file, and the file would be opened for writing, and the command op_type set CMD_WRITE.

setupDirectory Traversal

I skipped the fact that before opening README.TXT I had to open the root directory. In this system, directories are simply files. You need to be in the correct working directory to open a file, the directory does not form part of the open request. In this way, internally a directory tree can be kept on the Teensy. I’ve not implemented this fully yet, as for now, a flat filesystem really is enough for me.

The NEXT command allows the discovery of files in a directory, like most other file systems. Unlike the other file operations, this operation has no arguments, and simply operates on the current open file – if it’s a directory!

  getnext_output {
 0:    uint8_t  error;
 1:    uint32_t filesize;
 5:    uint8_t  flags;
 6:    char     name[13]; // null-terminated
  }

For this request, since this operation only writes memory, we can use the scratch_mem location from earlier, and initiate the GETNEXT command. We initiate GETNEXT operations until the error byte becomes non-zero.

  ld a, FILESYS_NEXT_SETMEMPTR
  out (PORT_FILESYS_NEXT), a    ; set the memory area to scratch_mem
  ld de, scratch_mem
  ld a, e
  out (PORT_FILESYS_NEXT), a
  ld a, d
  out (PORT_FILESYS_NEXT), a
  ld hl, scratch_mem
  ld de, scratch_mem + 6        ; scratch_mem+6 is char name[13]
getnextfile:
  call newline                  ; new line on the console
  ld a, FILESYS_NEXT_GETNEXT
  out (PORT_FILESYS_NEXT), a    ; initiate the GETNET command
  ld a, (hl)
  or a
  jr nz, nomorefiles            ; non-zero error? no more files.
  call print_string             ; prints string at de (scratch_mem+6)
  jr getnextfile
nomorefiles:
  ld a, FILESYS_CLOSE_FILE      ; close the directory file
  out (PORT_FILESYS_OPEN_CLOSE), a

dir_listing

That really is all there is to it! At present, only one file can be open at any one time. This is quite limiting, but for now, it will do. I can always add some sort of file descriptor system later.

Lets display an image!

The filesystem test I wrote lists the root directory, reads README.TXT, writes ‘0123456’ to TEST.TXT, and then reads a file speccy.565. Speccy.565 is an image file!

This was done fairly quickly, for some visual eye candy. speccy.565 is a tiny 48, 16-bit colour image of a ZX Spectrum keyboard. It’s in the 565 format, in that there are 5 bits for red, 6 bits for green, and 5 bits for blue. The file has no header, and the teensy is currently hard coded to have a 48x48x16bit ‘vram’. All I have working at the moment is a port for setting the start of this vram in memory, aligned to 256 bytes so we only need to set the top 8 bits of the address. I also have a port which ignores what is on the dataBus, it just initiates a draw of the vram to the LCD screen. It’s pretty primitive just now, but I hope to add more display and colour modes – as at the moment this 64×64 image in vram takes up a whopping 25% of the total RAM available to me!

The code looks like this:

  ld a, VRAM_HIGH_BEGIN
  out (PORT_VRAM_BUFFER_LOC), a 

  ... read speccy.565 into ram at VRAM_BEGIN ...

  ld a, 0
  out (PORT_VRAM_DRAW), a           ; Draw VRAM to screen

On the teensy, all we need is:

if (portAddress == PORT_VRAM_BUFFER_LOC)
{
  ioVramBuffer = ((unsigned short)dataBus) << 8U;
}
else if (portAddress == PORT_VRAM_DRAW)
{
  uint16_t* vram = (uint16_t*)&Z80_RAM[ioVramBuffer];

  // 48 X 48 test
  for (int y = 0; y < 48; y++)
  {
    for (int x = 0; x < 48; x++)
    {
      tft.drawPixel(VRAM_START_X+x, VRAM_START_Y+y, vram[y*48+x]);
    }
  }

}

The result:

read_image_test
This video is showing the real time execution of this test program on the Z80. It’s running probably around the 50KHz mark – I’m now beginning to think about an asynchronous clock – but that’s for yet another post 🙂

Wrapping up

That’s it for this post. We have a decent amount of filesystem functionality available, and I’ll be using that significantly in the future posts! I hope you’ve been enjoying this Teensy Z80 project. If you have, let me know on twitter @domipheus!

Part 4 is now available!

Comments are closed.