; sdc ; ; Manages the initialization of a SD card and implement a block device to read ; and write from/to it, in SPI mode. ; ; Note that SPI can't really be used directly from the z80, so this part ; assumes that you have a device that handles SPI communication on behalf of ; the z80. This device is assumed to work in a particular way. See the ; "rc2014/sdcard" recipe for details. ; ; That device has 3 ports. One write-only port to make CS high, one to make CS ; low (data sent is irrelevant), and one read/write port to send and receive ; bytes with the card through the SPI protocol. The device acts as a SPI master ; and writing to that port initiates a byte exchange. Data from the slave is ; then placed on a buffer that can be read by reading the same port. ; ; It's through that kind of device that this code below is supposed to work. ; *** Defines *** ; SDC_PORT_CSHIGH: Port number to make CS high ; SDC_PORT_CSLOW: Port number to make CS low ; SDC_PORT_SPI: Port number to send/receive SPI data ; *** Consts *** .equ SDC_BLKSIZE 512 ; *** Variables *** ; Where the block dev current points to. This is a byte index. Higher 7 bits ; indicate a sector number, lower 9 bits are an offset in the current SDC_BUF. .equ SDC_PTR SDC_RAMSTART ; Whenever we read a sector, we read a whole block at once and we store it ; in memory. That's where it goes. .equ SDC_BUF SDC_PTR+2 ; Sector number currently in SDC_BUF. 0xff, it's initial value, means "no ; sector. .equ SDC_BUFSEC SDC_BUF+SDC_BLKSIZE ; Whether the buffer has been written to. 0 means clean. 1 means dirty. .equ SDC_BUFDIRTY SDC_BUFSEC+1 .equ SDC_RAMEND SDC_BUFDIRTY+1 ; *** Code *** ; Wake the SD card up. After power up, a SD card has to receive at least 74 ; dummy clocks with CS and DI high. We send 80. sdcWakeUp: out (SDC_PORT_CSHIGH), a ld b, 10 ; 10 * 8 == 80 ld a, 0xff .loop: out (SDC_PORT_SPI), a nop djnz .loop ret ; Initiate SPI exchange with the SD card. A is the data to send. Received data ; is placed in A. sdcSendRecv: out (SDC_PORT_SPI), a nop nop in a, (SDC_PORT_SPI) nop nop ret sdcIdle: ld a, 0xff jp sdcSendRecv ; sdcSendRecv 0xff until the response is something else than 0xff for a maximum ; of 20 times. Returns 0xff if no response. sdcWaitResp: push bc ld b, 20 .loop: call sdcIdle inc a ; if 0xff, it's going to become zero jr nz, .end ; not zero? good, that's our command djnz .loop .end: ; whether we had a success or failure, we return the result. ; But first, let's bring it back to its original value. dec a pop bc ret ; Sends a command to the SD card, along with arguments and specified CRC fields. ; (CRC is only needed in initial commands though). ; A: Command to send ; H: Arg 1 (MSB) ; L: Arg 2 ; D: Arg 3 ; E: Arg 4 (LSB) ; C: CRC ; ; Returns R1 response in A. ; ; This does *not* handle CS. You have to select/deselect the card outside this ; routine. sdcCmd: ; Wait until ready to receive commands push af call sdcWaitResp pop af call sdcSendRecv ; Arguments ld a, h call sdcSendRecv ld a, l call sdcSendRecv ld a, d call sdcSendRecv ld a, e call sdcSendRecv ; send CRC ld a, c call sdcSendRecv ; And now we just have to wait for a valid response... jp sdcWaitResp ; return ; Send a command that expects a R1 response, handling CS. sdcCmdR1: out (SDC_PORT_CSLOW), a call sdcCmd out (SDC_PORT_CSHIGH), a ret ; Send a command that expects a R7 response, handling CS. A R7 is a R1 followed ; by 4 bytes. Those 4 bytes are returned in HL/DE in the same order as in ; sdcCmd. sdcCmdR7: out (SDC_PORT_CSLOW), a call sdcCmd ; We have our R1 response in A. Let's try reading the next 4 bytes in ; case we have a R3. push af ld a, 0xff call sdcSendRecv ld h, a ld a, 0xff call sdcSendRecv ld l, a ld a, 0xff call sdcSendRecv ld d, a ld a, 0xff call sdcSendRecv ld e, a pop af out (SDC_PORT_CSHIGH), a ret ; Initialize a SD card. This should be called at least 1ms after the powering ; up of the card. Sets result code in A. Zero means success, non-zero means ; error. sdcInitialize: push hl push de push bc call sdcWakeUp ; Call CMD0 and expect a 0x01 response (card idle) ; This should be called multiple times. We're actually expected to. ; Let's call this for a maximum of 10 times. ld b, 10 .loop1: ld a, 0b01000000 ; CMD0 ld hl, 0 ld de, 0 ld c, 0x95 call sdcCmdR1 cp 0x01 jp z, .cmd0ok djnz .loop1 ; Nothing? error jr .error .cmd0ok: ; Then comes the CMD8. We send it with a 0x01aa argument and expect ; a 0x01aa argument back, along with a 0x01 R1 response. ld a, 0b01001000 ; CMD8 ld hl, 0 ld de, 0x01aa ld c, 0x87 call sdcCmdR7 cp 0x01 jr nz, .error xor a cp h ; H is zero jr nz, .error cp l ; L is zero jr nz, .error ld a, d cp 0x01 jp nz, .error ld a, e cp 0xaa jr nz, .error ; Now we need to repeatedly run CMD55+CMD41 (0x40000000) until we ; the card goes out of idle mode, that is, when it stops sending us ; 0x01 response and send us 0x00 instead. Any other response means that ; initialization failed. .loop2: ld a, 0b01110111 ; CMD55 ld hl, 0 ld de, 0 call sdcCmdR1 cp 0x01 jr nz, .error ld a, 0b01101001 ; CMD41 (0x40000000) ld hl, 0x4000 ld de, 0x0000 call sdcCmdR1 cp 0x01 jr z, .loop2 or a ; cp 0 jr nz, .error ; Success! out of idle mode! ; initialize variables ld hl, 0 ld (SDC_PTR), hl ld a, 0xff ld (SDC_BUFSEC), a xor a ld (SDC_BUFDIRTY), a jr .end .error: ld a, 0x01 .end: pop bc pop de pop hl ret ; Send a command to set block size to SDC_BLKSIZE to the SD card. ; Returns zero in A if a success, non-zero otherwise sdcSetBlkSize: push hl push de ld a, 0b01010000 ; CMD16 ld hl, 0 ld de, SDC_BLKSIZE call sdcCmdR1 ; Since we're out of idle mode, we expect a 0 response ; We need no further processing: A is already the correct value. pop de pop hl ret ; Read block index specified in A and place the contents in (SDC_BUF). ; Doesn't check CRC. If the operation is a success, updates (SDC_BUFSEC) to the ; value of A. ; Returns 0 in A if success, non-zero if error. sdcReadBlk: push bc push hl out (SDC_PORT_CSLOW), a ld hl, 0 ; read single block at addr A ld d, 0 ld e, a ; E isn't touched in the rest of the routine ; and holds onto our original A ld a, 0b01010001 ; CMD17 call sdcCmd or a ; cp 0 jr nz, .error ; Command sent, no error, now let's wait for our data response. ld b, 20 .loop1: call sdcWaitResp ; 0xfe is the expected data token for CMD17 cp 0xfe jr z, .loop1end cp 0xff jr nz, .error djnz .loop1 jr .error ; timeout. error out .loop1end: ; We received our data token! ; Data packets follow immediately, we have 512 of them to read ld bc, SDC_BLKSIZE ld hl, SDC_BUF .loop2: call sdcIdle ld (hl), a cpi ; a trick to inc HL and dec BC at the same time. ; P/V indicates whether BC reached 0 jp pe, .loop2 ; BC is not zero, loop ; Read our 2 CRC bytes call sdcIdle call sdcIdle ; success! Let's recall our orginal A arg and put it in SDC_BUFSEC ld a, e ld (SDC_BUFSEC), a xor a ld (SDC_BUFDIRTY), a jr .end .error: ; try to preserve error code or a ; cp 0 jr nz, .end ; already non-zero inc a ; zero, adjust .end: out (SDC_PORT_CSHIGH), a pop hl pop bc ret ; Write the contents of (SDC_BUF) in sector number (SDC_BUFSEC). Unsets the ; (SDC_BUFDIRTY) flag on success. ; A returns 0 in A on success (with Z set), non-zero (with Z unset) on error. sdcWriteBlk: ld a, (SDC_BUFDIRTY) or a ; cp 0 ret z ; return success, but do nothing. push bc push hl out (SDC_PORT_CSLOW), a ld a, (SDC_BUFSEC) ld hl, 0 ; write single block at addr A ld d, 0 ld e, a ld a, 0b01011000 ; CMD24 call sdcCmd or a ; cp 0 jr nz, .error ; Before sending the data packet, we need to send at least one empty ; byte. ld a, 0xff call sdcSendRecv ; data packet token for CMD24 ld a, 0xfe call sdcSendRecv ; Sending our data token! ld bc, SDC_BLKSIZE ld hl, SDC_BUF .loop: ld a, (hl) call sdcSendRecv cpi ; a trick to inc HL and dec BC at the same time. ; P/V indicates whether BC reached 0 jp pe, .loop ; BC is not zero, loop ; Send our 2 CRC bytes. They can be anything call sdcIdle call sdcIdle ; Let's see what response we have call sdcWaitResp and 0b00011111 ; We ignore the first 3 bits of the response. cp 0b00000101 ; A valid response is "010" in bits 3:1 flanked ; by 0 on its left and 1 on its right. jr nz, .error ; good! Now, we need to let the card process this data. It will return ; 0xff when it's not busy any more. call sdcWaitResp xor a ld (SDC_BUFDIRTY), a jr .end .error: ; try to preserve error code or a ; cp 0 jr nz, .end ; already non-zero inc a ; zero, adjust .end: out (SDC_PORT_CSHIGH), a pop hl pop bc ret ; Ensures that (SDC_BUFSEC) is in sync with (SDC_PTR), that is, that the current ; buffer in memory corresponds to where SDC_PTR points to. If it doesn't, loads ; the sector that (SDC_PTR) points to in (SDC_BUF) and update (SDC_BUFSEC). ; If the (SDC_BUFDIRTY) flag is set, we write the content of the in-memory ; buffer to the SD card before we read a new sector. ; Returns Z on success, not-Z on error (with the error code from either ; sdcReadBlk or sdcWriteBlk) sdcSync: ; SDC_PTR points to the character we're supposed to read or right now, ; but we first have to check whether we need to load a new sector in ; memory. To do this, we compare the high 7 bits of (SDC_PTR) with ; (SDC_BUFSEC). If they're different, we need to load a new block. push hl ld a, (SDC_BUFSEC) ld h, a ld a, (SDC_PTR+1) ; high byte has bufsec in its high 7 bits srl a cp h pop hl ret z ; equal? nothing to do ; We have to read a new sector, but first, let's write the current one ; if needed. call sdcWriteBlk ret nz ; error ; Let's read our new sector ld a, (SDC_PTR+1) srl a jp sdcReadBlk ; returns ; *** shell cmds *** sdcInitializeCmd: .db "sdci", 0, 0, 0 call sdcInitialize jp sdcSetBlkSize ; returns ; Flush the current SDC buffer if dirty sdcFlushCmd: .db "sdcf", 0, 0, 0 jp sdcWriteBlk ; returns ; *** blkdev routines *** ; Make HL point to (SDC_PTR) in current buffer _sdcPlaceBuf: call sdcSync ret nz ; error ld a, (SDC_PTR+1) ; high byte and 0x01 ; is first bit set? jr nz, .highbuf ; first bit set? we're in the "highbuf" zone. ; lowbuf zone ; Read byte from memory at proper offset in lowbuf (first 0x100 bytes) ld hl, SDC_BUF jr .read .highbuf: ; Read byte from memory at proper offset in highbuf (0x100-0x1ff) ld hl, SDC_BUF+0x100 .read: ; HL is now placed either on the lower or higher half of SDC_BUF and ; all we need is to increase HL by the number in SDC_PTR's LSB (little ; endian, remember). ld a, (SDC_PTR) ; LSB call addHL ; returns xor a ; ensure Z ret sdcGetC: push hl call _sdcPlaceBuf jr nz, .error ; This is it! ld a, (hl) ; before we return A, we need to increase (SDC_PTR) ld hl, (SDC_PTR) inc hl ld (SDC_PTR), hl cp a ; ensure Z jr .end .error: call unsetZ .end: pop hl ret sdcPutC: push hl push af ; let's remember the char we put, _sdcPlaceBuf ; destroys A. call _sdcPlaceBuf jr nz, .error ; HL points to our dest. Recall A and write pop af ld (hl), a ; we need to increase (SDC_PTR) ld hl, (SDC_PTR) inc hl ld (SDC_PTR), hl ld a, 1 ld (SDC_BUFDIRTY), a xor a ; ensure Z jr .end .error: pop af call unsetZ .end: pop hl ret sdcSeek: ld (SDC_PTR), hl ret sdcTell: ld hl, (SDC_PTR) ret