|
- ; 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.
- ;
- ; *** SDC buffers ***
- ;
- ; SD card's lowest common denominator in terms of block size is 512 bytes, so
- ; that's what we deal with. To avoid wastefully reading entire blocks from the
- ; card for one byte read ops, we buffer the last read block. If a GetB or PutB
- ; operation is within that buffer, then no interaction with the SD card is
- ; necessary.
- ;
- ; As soon as a GetB or PutB operation is made that is outside the current
- ; buffer, we load a new block.
- ;
- ; When we PutB, we flag the buffer as "dirty". On the next buffer change (during
- ; an out-of-buffer request or during an explicit "flush" operation), bytes
- ; currently in the buffer will be written to the SD card.
- ;
- ; We hold 2 buffers in memory, each targeting a different sector and with its
- ; own dirty flag. We do that to avoid wasteful block writing in the case where
- ; we read data from a file in the SD card, process it and write the result
- ; right away, in another file on the same card (zasm), on a different sector.
- ;
- ; If we only have one buffer in this scenario, we'll end up loading a new sector
- ; at each GetB/PutB operation and, more importantly, writing a whole block for
- ; a few bytes each time. This will wear the card prematurely (and be very slow).
- ;
- ; With 2 buffers, we solve the problem. Whenever GetB/PutB is called, we first
- ; look if one of the buffer holds our sector. If not, we see if one of the
- ; buffer is clean (not dirty). If yes, we use this one. If both are dirty or
- ; clean, we use any. This way, as long as writing isn't made to random
- ; addresses, we ensure that we don't write wastefully because read operations,
- ; even if random, will always use the one buffer that isn't dirty.
-
- ; *** 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
- .equ SDC_MAXTRIES 8
-
- ; *** Variables ***
- ; This is a pointer to the currently selected buffer. This points to the BUFSEC
- ; part, that is, two bytes before actual content begins.
- .equ SDC_BUFPTR SDC_RAMSTART
- ; Count the number of times we tried a particular read or write operation. When
- ; CRC check fail, we retry. After SDC_MAXTRIES failures, we stop.
- .equ SDC_RETRYCNT SDC_BUFPTR+2
- ; Sector number currently in SDC_BUF1. Little endian like any other z80 word.
- .equ SDC_BUFSEC1 SDC_RETRYCNT+1
- ; Whether the buffer has been written to. 0 means clean. 1 means dirty.
- .equ SDC_BUFDIRTY1 SDC_BUFSEC1+2
- ; The contents of the buffer.
- .equ SDC_BUF1 SDC_BUFDIRTY1+1
- ; CRC bytes for the buffer. They're placed after the contents because that makes
- ; things easier processing-wise. Because the SD card sends them right after the
- ; contents, all we need to do is read SDC_BLKSIZE+2.
- ; IMPORTANT NOTE: This is big endian. The SD card sends the MSB first, so we
- ; keep it in memory this way.
- .equ SDC_CRC1 SDC_BUF1+SDC_BLKSIZE
-
- ; second buffer has the same structure as the first.
- .equ SDC_BUFSEC2 SDC_CRC1+2
- .equ SDC_BUFDIRTY2 SDC_BUFSEC2+2
- .equ SDC_BUF2 SDC_BUFDIRTY2+1
- .equ SDC_CRC2 SDC_BUF2+SDC_BLKSIZE
- .equ SDC_RAMEND SDC_CRC2+2
-
- ; *** 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
-
- ; The opposite of sdcWaitResp: we wait until response if 0xff. After a
- ; successful read or write operation, the card will be busy for a while. We need
- ; to give it time before interacting with it again. Technically, we could
- ; continue processing on our side while the card it busy, and maybe we will one
- ; day, but at the moment, I'm having random write errors if I don't do this
- ; right after a write, so I prefer to stay cautious for now.
- ; This has no error condition and preserves A
- sdcWaitReady:
- push af
- ; for now, we have no timeout for waiting. It means that broken SD
- ; cards can cause infinite loops.
- .loop:
- call sdcIdle
- inc a ; if 0xff, it's going to become zero
- jr nz, .loop ; not zero? still busy. loop
- pop af
- 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)
- ;
- ; Returns R1 response in A.
- ;
- ; This does *not* handle CS. You have to select/deselect the card outside this
- ; routine.
- sdcCmd:
- push bc
-
- ; Wait until ready to receive commands
- push af
- call sdcWaitResp
- pop af
-
- ld c, 0 ; init CRC
- call .crc7
- call sdcSendRecv
- ; Arguments
- ld a, h
- call .crc7
- call sdcSendRecv
- ld a, l
- call .crc7
- call sdcSendRecv
- ld a, d
- call .crc7
- call sdcSendRecv
- ld a, e
- call .crc7
- call sdcSendRecv
- ; send CRC
- ld a, c
- or 0x01 ; ensure stop bit is set
- call sdcSendRecv
-
- ; And now we just have to wait for a valid response...
- call sdcWaitResp
- pop bc
- ret
-
- ; push A into C and compute CRC7 with 0x09 polynomial
- ; Note that the result is "left aligned", that is, that 8th bit to the "right"
- ; is insignificant (will be stop bit).
- .crc7:
- push af
- xor c
- ld b, 8
- .loop:
- sla a
- jr nc, .noCRC
- ; msb was set, apply polynomial
- xor 0x12 ; 0x09 << 1. We apply CRC on high 7 bits
- .noCRC:
- djnz .loop
- ld c, a
- pop af
- ret
-
- ; 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
- call sdcIdle
- ld h, a
- call sdcIdle
- ld l, a
- call sdcIdle
- ld d, a
- call sdcIdle
- 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
- 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
- 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!
- jr .end
-
- .error:
- ld a, 0x01
- .end:
- pop bc
- pop de
- pop hl
- ret
-
- ; Read block index specified in DE and place the contents in buffer pointed to
- ; by (SDC_BUFPTR).
- ; If the operation is a success, updates buffer's sector to the value of DE.
- ; After a block read, check that CRC given by the card matches the content. If
- ; it doesn't, retries up to SDC_MAXTRIES times.
- ; Returns 0 in A if success, non-zero if error.
- sdcReadBlk:
- xor a
- ld (SDC_RETRYCNT), a
-
- push bc
- push hl
-
- out (SDC_PORT_CSLOW), a
- .retry:
- ld hl, 0
- ; DE already has the correct value
- 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+CRC of them to read
- ld bc, SDC_BLKSIZE+2
- ld hl, (SDC_BUFPTR) ; HL --> active buffer's sector
- ; It sounds a bit wrong to set bufsec and dirty flag before we get our
- ; actual data, but at this point, we don't have any error conditions
- ; left, success is guaranteed. To avoid needlesssly INCing hl, let's
- ; set sector and dirty along the way
- ld (hl), e ; sector number LSB
- inc hl
- ld (hl), d ; sector number MSB
- inc hl ; dirty flag
- xor a ; unset
- ld (hl), a
- inc hl ; actual contents
- .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
- ; Success! while the card is busy, let's get busy too: let's check and
- ; see if the CRC matches.
- push de ; <|
- call sdcCRC ; |
- ; before we check the CRC r|esults, let's wait until card is ready
- call sdcWaitReady ; |
- ; check CRC results |
- ld a, (hl) ; |
- cp d ; |
- jr nz, .crcMismatch ; |
- inc hl ; |
- ld a, (hl) ; |
- cp e ; |
- jr nz, .crcMismatch ; |
- pop de ; <|
- ; Everything is fine and dandy!
- xor a ; success
- jr .end
- .crcMismatch:
- ; CRC of the buffer's content doesn't match the CRC reported by the
- ; card. Let's retry.
- pop de ; from the push right before call sdcCRC
- ld a, (SDC_RETRYCNT)
- inc a
- ld (SDC_RETRYCNT), a
- cp SDC_MAXTRIES
- jr nz, .retry ; we jump inside our main stack push. No need
- ; to pop it.
- ; Continue to error condition. Even if we went through many retries,
- ; our stack is still the same as it was at the first call. We can return
- ; normally (but in error condition).
- .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 buffer where (SDC_BUFPTR) points to in sector associated
- ; to it. Unsets the the buffer's dirty flag on success.
- ; Before writing the block, update the buffer's CRC field so that the correct
- ; CRC is sent.
- ; A returns 0 in A on success (with Z set), non-zero (with Z unset) on error.
- sdcWriteBlk:
- push ix
- ld ix, (SDC_BUFPTR) ; HL points to sector LSB
- xor a
- cp (ix+2) ; dirty flag
- pop ix
- ret z ; don't write if dirty flag is zero
-
- push bc
- push de
- push hl
-
- call sdcCRC ; DE -> new CRC. HL -> pointer to buf CRC
- ld (hl), d ; write computed CRC
- inc hl
- ld (hl), e
-
- out (SDC_PORT_CSLOW), a
- ld hl, (SDC_BUFPTR) ; sector LSB
- ld e, (hl) ; sector LSB
- inc hl
- ld d, (hl) ; sector MSB
- ld hl, 0 ; high addr word always zero, DE already set
- 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.
- call sdcIdle
-
- ; data packet token for CMD24
- ld a, 0xfe
- call sdcSendRecv
-
- ; Sending our data token!
- ld bc, SDC_BLKSIZE+2 ; +2 for CRC. (as of now, however, that
- ; CRC isn't properly updated. Because
- ; CMD59 isn't enabled, it doesn't
- ; matter)
- ld hl, (SDC_BUFPTR)
- inc hl ; sector MSB
- inc hl ; dirty flag
- inc hl ; beginning of contents
-
- .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
- ; 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
- ; Success! Now let's unset the dirty flag
- ld hl, (SDC_BUFPTR)
- inc hl ; sector MSB
- inc hl ; dirty flag
- xor a
- ld (hl), a
-
- ; Before returning, wait until card is ready
- call sdcWaitReady
- xor 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 de
- pop bc
- ret
-
- ; Considering the first 15 bits of EHL, select the most appropriate of our two
- ; buffers and, if necessary, sync that buffer with the SD card. If the selected
- ; buffer doesn't have the same sector as what EHL asks, load that buffer from
- ; the SD card.
- ; If the dirty 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:
- push de
- ; Given a 24-bit address in EHL, extracts the 15-bit sector from it and
- ; place it in DE.
- ; We need to shift both E and H right by one bit
- srl e ; sets Carry
- ld d, e
- ld a, h
- rra ; takes Carry
- ld e, a
-
- ; Let's first see if our first buffer has our sector
- ld a, (SDC_BUFSEC1) ; sector LSB
- cp e
- jr nz, .notBuf1
- ld a, (SDC_BUFSEC1+1) ; sector MSB
- cp d
- jr z, .buf1Ok
-
- .notBuf1:
- ; Ok, let's check for buf2 then
- ld a, (SDC_BUFSEC2) ; sector LSB
- cp e
- jr nz, .notBuf2
- ld a, (SDC_BUFSEC2+1) ; sector MSB
- cp d
- jr z, .buf2Ok
-
- .notBuf2:
- ; None of our two buffers have the sector we need, we'll need to load
- ; a new one.
-
- ; We select our buffer depending on which is dirty. If both are on the
- ; same status of dirtiness, we pick any (the first in our case). If one
- ; of them is dirty, we pick the clean one.
- push de ; <|
- ld de, SDC_BUFSEC1 ; |
- ld a, (SDC_BUFDIRTY1) ; |
- or a ; | is buf1 dirty?
- jr z, .ready ; | no? good, that's our buffer
- ; yes? then buf2 is our buffer. ; |
- ld de, SDC_BUFSEC2 ; |
- ; |
- .ready: ; |
- ; At this point, DE points to one o|f our two buffers, the good one.
- ; Let's save it to SDC_BUFPTR |
- ld (SDC_BUFPTR), de ; |
- ; |
- pop de ; <|
-
- ; We have to read a new sector, but first, let's write the current one
- ; if needed.
- call sdcWriteBlk
- jr nz, .end ; error
- ; Let's read our new sector in DE
- call sdcReadBlk
- jr .end
-
- .buf1Ok:
- ld de, SDC_BUFSEC1
- ld (SDC_BUFPTR), de
- ; Z already set
- jr .end
-
- .buf2Ok:
- ld de, SDC_BUFSEC2
- ld (SDC_BUFPTR), de
- ; Z already set
- ; to .end
- .end:
- pop de
- ret
-
- ; Computes the CRC-16, with polynomial 0x1021 of buffer at (SDC_BUFPTR) and
- ; returns its value in DE. Also, make HL point to the first byte of the CRC
- ; associated to (SDC_BUFPTR).
- sdcCRC:
- push af
- push bc
- ld hl, (SDC_BUFPTR)
- inc hl \ inc hl \ inc hl ; HL points to contents
- ld bc, SDC_BLKSIZE
- ld de, 0
- .loop:
- push bc ; <|
- ld b, 8 ; |
- ld a, (hl) ; |
- xor d ; |
- ld d, a ; |
- .inner: ; |
- sla e ; | Sets Carry
- rl d ; | Takes and sets carry
- jr nc, .noCRC ; |
- ; msb was set, apply polyno|mial
- ld a, d ; |
- xor 0x10 ; |
- ld d, a ; |
- ld a, e ; |
- xor 0x21 ; |
- ld e, a ; |
- .noCRC: ; |
- djnz .inner ; |
- pop bc ; <|
-
- cpi ; inc HL, dec BC, sets P/V on BC=0
- jp pe, .loop ; BC is not zero, loop
- ; At this point, HL points to the right place: the first byte of the
- ; recorded CRC.
- pop bc
- pop af
- ret
-
- sdcInitializeCmd:
- call sdcInitialize
- ret nz
- call .setBlkSize
- ret nz
- call .enableCRC
- ret nz
- ; At this point, our buffers are unnitialized. We could have some logic
- ; that determines whether a buffer is initialized in appropriate SDC
- ; routines and act appropriately, but why bother when we could, instead,
- ; just buffer the first two sectors of the card on initialization? This
- ; way, no need for special conditions.
- ; initialize variables
- ld hl, SDC_BUFSEC1
- ld (SDC_BUFPTR), hl
- ld de, 0
- call sdcReadBlk ; read sector 0 in buf1
- ret nz
- ld hl, SDC_BUFSEC2
- ld (SDC_BUFPTR), hl
- inc de
- jp sdcReadBlk ; read sector 1 in buf2, returns
-
- ; Send a command to set block size to SDC_BLKSIZE to the SD card.
- ; Returns zero in A if a success, non-zero otherwise
- .setBlkSize:
- 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
-
- ; Enable CRC checks through CMD59
- .enableCRC:
- push hl
- push de
-
- ld a, 0b01111011 ; CMD59
- ld hl, 0
- ld de, 1 ; 1 means CRC enabled
- call sdcCmdR1
- pop de
- pop hl
- ret
-
- ; Flush the current SDC buffer if dirty
- sdcFlushCmd:
- ld hl, SDC_BUFSEC1
- ld (SDC_BUFPTR), hl
- call sdcWriteBlk
- ret nz
- ld hl, SDC_BUFSEC2
- ld (SDC_BUFPTR), hl
- jp sdcWriteBlk ; returns
-
- ; *** blkdev routines ***
-
- ; Make HL point to its proper place in SDC_BUF.
- ; EHL currently is a 24-bit offset to read in the SD card. E=high byte,
- ; HL=low word. Load the proper sector in memory and make HL point to the
- ; correct data in the memory buffer.
- _sdcPlaceBuf:
- call sdcSync
- ret nz ; error
- ; At this point, we have the proper buffer in place and synced in
- ; (SDC_BUFPTR). Only the 9 low bits of HL are important.
- push de
- ld de, (SDC_BUFPTR)
- inc de ; sector MSB
- inc de ; dirty flag
- inc de ; contents
- ld a, h ; high byte
- and 0x01 ; is first bit set?
- jr z, .read ; first bit reset? we're in the "lowbuf" zone.
- ; DE already points to the right place.
- ; We're in the highbuf zone, let's inc DE by 0x100, which, as it turns
- ; out, is quite easy.
- inc d
- .read:
- ; DE is now placed either on the lower or higher half of the active
- ; buffer and all we need is to increase DE the lower half of HL.
- ld a, l
- call addDE
- ex de, hl
- pop de
- ; Now, HL points exactly at the right byte in the active buffer.
- xor a ; ensure Z
- ret
-
- sdcGetB:
- push hl
- call _sdcPlaceBuf
- jr nz, .end ; NZ already set
-
- ; This is it!
- ld a, (hl)
- cp a ; ensure Z
- .end:
- pop hl
- ret
-
- sdcPutB:
- 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
-
- ; Now, let's set the dirty flag
- ld a, 1
- ld hl, (SDC_BUFPTR)
- inc hl ; sector MSB
- inc hl ; point to dirty flag
- ld (hl), a ; set dirty flag
- xor a ; ensure Z
- jr .end
- .error:
- ; preserve error code
- ex af, af'
- pop af
- ex af, af'
- call unsetZ
- .end:
- pop hl
- ret
|