SD Cards and CP/M

A project log for 3-Chip Z80 Design

Combining a Z80 retro design with a modern PSoC CPU. 10/22/2019 at 16:290 Comments

I'm not totally sure which source code is running on my card. That's because I grabbed the file out of my FPGA design sources and there's no matching source file name. Funny that I can run code that I don't know what the source is but it wasn't all that hard to run. I ran it until the I/O access hung and I could see that the Z80 was accessing a 2nd ACIA. My PSoC code didn't have a 2nd ACIA and the source code had no access code for a 2nd ACIA. So I know the .asm file I was using didn't match the .HEX file.

Digging deeper into Grant's page I found what I think may be the the original source code - basMon.asm. It also has listing file BASMON.LST. The code looks right. It's 8KB long, it needs two ACIAs.

It should be pretty easy to examine the program that is downloaded by using the Front Panel and see if it matches BASMON.LST.

The PSOC Creator menu options Tool "Debug without programming" lets me restart the code. The compile options are set to include the Front Panel support. After testing the SRAM, the front panel displays the contents of address 0x0 which are 0xF3 which is a DI instruction. Not a surprise since probably all Z80 code starts this way.

Let's see if the Monitor is present in this code. After all the MON in BASMON would imply that it is present. The monitor code starts at address offset 0x00EC.

0294   00EC             ;------------------------------------------------------------------------------
0295   00EC             ; Monitor command loop
0296   00EC             ;------------------------------------------------------------------------------
0297   00EC 21 EC 00    MAIN          LD   HL,MAIN    ; Save entry point for Monitor    
0298   00EF E5                  PUSH HL        ; This is the return address
0299   00F0 CD 22 01    MAIN0        CALL TXCRLF    ; Entry point for Monitor, Normal    
0300   00F3 3E 3E               LD   A,'>'    ; Get a ">"    
0301   00F5 CF                  RST 08H        ; print it

Setting 0xEC on the second row up and pressing the LDADR switch displays the contents of the address which is 0x21. That matches the source code so I think I'm on the right track. The function I am most interested in is CPMLOAD. It looks like this:

0505   01F1             ;------------------------------------------------------------------------------
0506   01F1             ; CP/M load command
0507   01F1             ;------------------------------------------------------------------------------
0508   01F1             CPMLOAD
0509   01F1             
0510   01F1 21 03 02            LD HL,CPMTXT
0511   01F4 CD 1B 01            CALL M_PRINT
0512   01F7 CD 29 01            CALL M_GETCHR
0513   01FA C8                  RET Z    ; Cancel if CTRL-C
0514   01FB E6 5F               AND  $5F ; uppercase
0515   01FD FE 59               CP 'Y'

Checking address 0x1F1 and following address confirms this is very likely the correct listing for the code that is running on the card. Odd since Grant's documentation doesn't mention BASMON.asm at all. I think BASMON is a combination of BASIC.ASM and MONITOR.ASM which Grant does mention. It looks to me like he put the output of that file in with the FPGA VHD files since the .HEX output file is what the FPGA compiler loads.

This gives us a shot at figuring out what is wrong when this has problems - and it will have problems since there's a fair amount of heavy lifting to do getting the SPI to work with an external SD card and CP/M. Given this, we should also be able to correlate the FPGA design used for the SD card interface with Grant's software.

SD Controller in the FPGA

I've got some recent experience with the SD controller that Grant uses in the FPGA. I designed a 32-bit RISC CPU (R32V2020) and got the controller working with my code. The organization of the control registers on that design was:

; SD Card base address is 0x1000
; Register Addresses
;    0x1000    SDDATA        read/write data
;    0x1001    SDSTATUS      read
;    0x1001    SDCONTROL     write
;    0x1002    SDLBA0        write-only
;    0x1003    SDLBA1        write-only
;    0x1004    SDLBA2        write-only (only bits 6:0 are valid)

 Here's some more of the details from that project.

; To read a 512-byte block from the SDHC card:
; Wait until SDSTATUS=0x80 (ensures previous cmd has completed)
; Write SDLBA0, SDLBA1 SDLBA2 to select block index to read from
; Write 0 to SDCONTROL to issue read command
; Loop 512 times:
;     Wait until SDSTATUS=0xE0 (read byte ready, block busy)
;     Read byte from SDDATA

Grant must be doing something similar for this code. Grant's code starts with the label readhst:. and reads the SD_STATUS and SD_DATA locations. It calls setLBAaddr which does three writes to the addresses SD_LBA2..SD_LBA0. These are defined along with the other Z80 I/O addresses as:

0060   0000             SD_DATA        .EQU    088H
0061   0000             SD_CONTROL    .EQU    089H
0062   0000             SD_STATUS    .EQU    089H
0063   0000             SD_LBA0        .EQU    08AH
0064   0000             SD_LBA1        .EQU    08BH
0065   0000             SD_LBA2        .EQU    08CH

There are six separate Z80 I/O address registers for these values.

This seems straightforward enough.

SD vs SDHC Cards

The code above doesn't deal with LBA3 which it would need to if SDHC cards are used. We'll take note of that and figure out if we can do something about that in the future. If I recall correctly the card type can be interrogated and the right code inserted for the right type of card. It might be as simple as just using the lower area of a really big card and setting LBA3 to be 0x00 always.

The code does partly anticipate SDHC cards in the memory storage since it reserves a spot for lba3. Grant initializes the lba3 value to 0x0 (in CPMLOAD2) but the code to talk to the SD card (setLBAaddr) doesn't use it as Grant notes in a comment.

Here's the LBA storage space in Grant's code.

0073   3004 00          lba0        .DB    00h
0074   3005 00          lba1        .DB    00h
0075   3006 00          lba2        .DB    00h
0076   3007 00          lba3        .DB    00h

For the moment we'll just use an SD card that is 2 GB and already has CP/M loaded onto it. This is not quite cheating since Grant details how to make the card and we did that for the Multicomp FPGA build. Ultimately, it would be preferable to use an SDHC card since it's getting increasingly harder to find low capacity SD cards.

The PSoC SD Card Driver

We have enough information now to start to design the PSoC low level software for the SD card interface between the Z80 and the PSoC.  We need six IO space addresses. Three of them are write-only from the Z80 (lba0..2). I'm considering the Compact Flash (CF) start earlier to be a dead end since we now have code that runs and should be able to boot CP/M. This way we won't have to make the CF card code to SD card code.

The PSoC definitions for the SD card are:

    #define USING_SDCARD
    #ifdef USING_SDCARD
        #define SD_DATA                 0x88
        #define SD_CONTROL            0x89
        #define SD_STATUS            0x89
        #define SD_LBA0                0x8A
        #define SD_LBA1                0x8B
        #define SD_LBA2                0x8C

I changed all references to CF into SD and set up the appropriate functions. Here's the function names:

void    SDReadData(void);
void    SDWriteData(void);
void    SDReadStatus(void);
void    SDWriteCommand(void);
void    SDWriteLBA0(void);
void    SDWriteLBA1(void);
void    SDWriteLBA2(void);
void    SdWriteLBA3(void);

The PSoC code compiled without error. This should be all that is needed for the Z80 side access to the SD Card interface. We still need to work out the PSoC to SD Card side in the next log(s).