collapseos/kernel/sdc.asm
Virgil Dupras ae028e3a86 blockdev: make implementors "random access"
This huge refactoring remove the Seek and Tell routine from blockdev
implementation requirements and change GetC and PutC's API so that they
take an address to read and write (through HL/DE) at each call.

The "PTR" approach in blockdev implementation was very redundant from
device to device and it made more sense to generalize. It's possible
that future device aren't "random access", but we'll be able to add more
device types later.

Another important change in this commit is that the "blockdev handle" is
now opaque. Previously, consumers of the API would happily call routines
directly from one of the 4 offsets. We can't do that any more. This
makes the API more solid for future improvements.

This change forced me to change a lot of things in fs, but overall,
things are now simpler. No more `FS_PTR`: the "device handle" now holds
the active pointer.

Lots, lots of changes, but it also feels a lot cleaner and solid.
2019-06-04 15:36:20 -04:00

481 lines
11 KiB
NASM

; 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 ***
; 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_RAMSTART
; 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 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 HL, that is, that the current
; buffer in memory corresponds to where HL points to. If it doesn't, loads
; the sector that HL 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:
; HL 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 HL with
; (SDC_BUFSEC). If they're different, we need to load a new block.
push hl
ld a, (SDC_BUFSEC)
ld l, a
ld a, h
srl a
cp l
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, h
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 its proper place in SDC_BUF.
; HL currently is an offset to read in the SD card. Load the proper sector in
; memory and make HL point to the correct data in the memory buffer.
_sdcPlaceBuf:
call sdcSync
ret nz ; error
ld a, h ; high byte
and 0x01 ; is first bit set?
ld a, l ; doesn't change flags
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 A which is the lower
; half of our former HL value.
call addHL
xor a ; ensure Z
ret
sdcGetC:
push hl
call _sdcPlaceBuf
jr nz, .error
; This is it!
ld a, (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
ld a, 1
ld (SDC_BUFDIRTY), a
xor a ; ensure Z
jr .end
.error:
pop af
call unsetZ
.end:
pop hl
ret