SPI Controller
13.1 Overview
The SPI Controller allows the host CPU to communicate with an external TF (microSD) card using hardware SPI transfers.
For maximum compatibility with TF cards, the interface employs SPI transfer modes with CPOL=0 and CPHA=0."
It is designed for simplicity, suitable for small block data exchange, and removes the need for precise timing on the CPU side.
Like other controllers, the SPI Controller can be mapped into the device bank, starting at address 0xA0 on the I/O port. Its device index is 1.
⚠️ The controller is currently only connected to the TF card interface, and not accessible via external SPI pins.
13.2 Chip Select
The SPI controller allows the host CPU to control the Chip Select (CS) line independently of any active SPI transaction. This makes it possible to keep the CS line active across multiple transfers, for example, to start a transaction, process the received data, and then send another command without releasing CS.
It is also possible to initiate a transaction with no chip select active, which is required during the initialization sequence of all TF cards.
Available Chip Select Lines
The hardware currently supports two CS lines:
0— Reserved1— External TF card
Controlling the CS Line
Chip Select control is managed through the CTRL register:
CS_SEL— Selects the CS line to control (0or1).CS_START— Pulls the selected CS line low (active).CS_END— Releases the selected CS line high (inactive).
If both CS_START and CS_END are 0, the state of the CS line remains unchanged.
If both are set to 1, the CS line will be activated.
Example: Enabling TF Card CS (Z80)
ld a, (1 << CS_SEL) | (1 << CS_START)
out (0xa0 + CTRL), a
Example: Disabling TF Card CS (Z80)
ld a, (1 << CS_SEL) | (1 << CS_END)
out (0xa0 + CTRL), a
13.3 Internal Buffers
Internally, the controller uses two 8-byte RAM blocks:
- OUT RAM: holds up to 8 bytes to transmit
- IN RAM: stores the 8 bytes received during transmission
Both these RAMs can be accessed via the 8 registers, starting from RAM_FROM to RAM_TO. When writing to these registers, the data will be stored in the internal OUT_RAM, when reading from these registers, the date from the IN RAM will be ouputted.
The number of valid bytes in these RAMs can be written to RAM_LEN before starting a transfer. Please note that each transfer always starts from RAM offset 0.
For example on a Z80, populating the OUT RAM with data cane be done as follows:
; Assumption: SPI controller is mapped at 0xa0
ld a, 0x12
out (0xa0 + RAM_FROM + 0), a
ld a, 0x34
out (0xa0 + RAM_FROM + 1), a
ld a, 0x56
out (0xa0 + RAM_FROM + 2), a
ld a, 0x78
out (0xa0 + RAM_FROM + 3), a
; Set the RAM size to 4 since we wrote 4 bytes
ld a, 4
out (0xa0 + RAM_LEN), a
; 4 bytes are now read to be sent on the bus: 0x12, 0x34, 0x56 and 0x78
13.4 Pseudo-FIFO access
For the sack of optimization (mainly using outi or otir instruction on the Z80), it is possible to access the RAM as pseudo-FIFOs. The SPI controller keeps two indexes, one for the OUT RAM and one for IN RAM.
By writing data to register RAM_FIFO, the OUT RAM will be populated at its index position, the latter is then incremented. Same goes for the IN RAM when the same register is read.
It is possible to reset both indexes by writing the bit REST_FIFO of register RAM_LEN.
For example on a Z80 target, if we want to populate the OUT RAM with the same bytes as the example above, we can do:
; Assumption: SPI controller is mapped at 0xa0
populate_spi:
; Prepare the bytes to write and the size
ld hl, data
ld b, data_end - data ; Size of the data array
; Reset the pseudo-INFO index, take advantage of this instruction to also set
; the final number of bytes we will write to the RAM.
ld a, 0x80
or b ; A is now 0x84
ld (0xa0 + RAM_LEN), a
; Use `otir` to populate the SPI OUT RAM
ld c, 0xa0 + RAM_FIFO
otir
ret
data: db 0x12, 0x34, 0x56, 0x78
data_end:
Similarly, after a transfer, it is possible to read all the bytes received into an array:
; Assumption: SPI controller is mapped at 0xa0
read_spi:
ld hl, data
ld b, data_end - data
; Reset the pseudo-INFO index, take advantage of this instruction to also set
; the final number of bytes to read from RAM. It must be the same as the write size.
ld a, 0x80
or b ; A is now 0x84
ld (0xa0 + RAM_LEN), a
; Use `inir` to populate the read the data from the SPI IN RAM
ld c, 0xa0 + RAM_FIFO
inir
ret
data: ds 4 ; Allocate 4 bytes in RAM to store the data since we sent 4 bytes
data_end:
13.5 Clock Frequency
The SPI controller is driven by the system’s main 50 MHz clock.
To generate the SPI bus clock, this base frequency is divided down using the CLK_DIV register.
The CLK_DIV value can range from 1 to 255, and the resulting SPI bus frequency is calculated as:
If the CLK_DIV register is set to 0, the hardware automatically forces it to 1 to avoid a division-by-zero condition.
Example Frequencies
CLK_DIV |
SPI Frequency |
|---|---|
| 1 | 25 MHz |
| 2 | 12.5 MHz |
| 5 | 5 MHz |
| 10 | 2.5 MHz |
| 25 | 1 MHz |
| 50 | 500 kHz |
| 125 | 200 kHz |
| 255 | ~98 kHz |
13.6 Transfer Process
Overall, to perform a transfer, the steps are:
-
Set the SPI Bus freqency (Optional)
- Choose the desired bus frequency by setting the
CLK_DIVregister.
- Choose the desired bus frequency by setting the
-
Fill the Output Buffer
-
Start the Transfer
- Set the
STARTbit in theCTRLregister. It is possible to specify, in the same byte, whether a CS line shall be enabled. (CS_STARTandCS_SELfields) - The SPI controller immediately begins shifting the 8 bytes out, while simultaneously receiving 8 bytes into the input buffer.
- Set the
-
Wait for Completion
- Poll the
IDLEflag in theSTATUSregister. - Once set, the transfer is complete.
- Poll the
-
Read the Input Buffer
- Read the 8 received bytes from the IN RAM,either using the
RAM_FIFOor using theRAM_FROM-RAM_TOregisters.
- Read the 8 received bytes from the IN RAM,either using the
-
Deassert the CS line (Optional)
- Deassert the previously enabled CS line by setting the
CS_ENDandCS_SELfields in theCTRLregister
- Deassert the previously enabled CS line by setting the
⚠️ Always wait for the
IDLEflag to be set before starting a new transfer. Starting a transfer while another is in progress will result in undefined behavior.
13.7 Registers Summary
| Name | Description | Base-relative address | Access |
| CTRL0 | Reserved or future use | 0x00 | |
| CTRL | Control register for starting and managing SPI transactions | 0x01 | varies |
| CLK_DIV | Clock divider for SPI transactions | 0x02 | RW |
| RAM_LEN | Controls the RAM length and FIFO reset | 0x03 | varies |
| RAM_FIFO | FIFO data register | 0x07 | RW |
| RAM_FROM | Start of memory-mapped data for SPI transactions | 0x08 | RW |
| RAM_TO | End of memory-mapped data for SPI transactions | 0x0F | RW |
13.8 Registers
| 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
| 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
| 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
| 0 | 0 | 0 | 0 | 1 | 0 | 1 | 0 |
| 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
| 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
| 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
| 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
| 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |