SPI Part 2

In this post we will take a look at the basic SPI code and then subsequently at the more complex SD card and FAT file format implementation. Before we get going some definitions. The way my address decoding logic is set up the VIA registers have the following addresses and names that I will use in the code. Should be easy to modify for your system (see VIA documentation for more details)

VIA1_PORTB      = $F100      ; address of port a
VIA1_PORTA      = $F101      ; address of port b
VIA1_DDRB       = $F102      ; data direction register for port a
VIA1_DDRA       = $F103      ; data direction register for port b
VIA1_T1CL       = $F104      ; timer 1 counter low 
VIA1_T1CH       = $F105      ; timer 1 counter high
VIA1_T1LL       = $F106      ; timer 1 latch low
VIA1_T1LH       = $F107      ; timer 1 latch high
VIA1_T2CL       = $F108      ; timer 2 counter low 
VIA1_T2CH       = $F109      ; timer 2 counter high
VIA1_SR         = $F10A      ; shift register 
VIA1_ACR        = $F10B      ; auxiliary control register 
VIA1_PCR        = $F10C      ; peripheral control register 
VIA1_IFR        = $F10D      ; interrupt flag register 
VIA1_IER        = $F10E      ; interrupt enable register
VIA1_ORA        = $F10F      ; same as port A, except "no handshake"

My initialization code disables the shift register interrupt, sets port B to output and sets the T2 clock frequency (for the fastest speed set SPI_CLKN = 0) .

spi.init        lda #%00000100  ; disable shift-register interrupt
                sta VIA1_IER     
        
                lda #$FF        ; set port B to output
                sta VIA1_DDRB
        
                lda #SPI_CLKN   ; set number of counts for T2/clock
                sta VIA1_T2CL

                rts           

You can set an interrupt to trigger when 8 bits have been sent/received, but I have chosen to have the load/save running in the “main thread”. I have certain kernel update routines in IRQ (e.g. updating screen, cock, etc.) that will then interrupt when needed. Hence the SR interrupt is disabled. You could easily chose to do this differently.

Since we are using the shift-register for both MOSI and MISO communication (see my SPI Part 1 post) we need to set up both the SR register and the buffer chip for either output or input. For the SR register we do this by setting bits 2-4 in the VIA auxiliary control register (ACR) to either %001 (shift in under control of T2) or %101 (shift out under control of T2).

The the input line (connected to PB0) or the output line (connected to PB1) on the buffer is enabled by pulling it low (obviously only pull one low and the other high … I know I really should implement this in HW). This will enable or disable the respective gates on the buffer.

Finally pull the SD cards Slave Select low to indicate it is the device we are communicating to (SS1 is connected to PB2). E.g. writing %xxxxxx10 to Port B will disable output (bit 1 = 1) and enable input (bit 0 = 0). And %xx1110xx will set SS1 = 0 to select the SD card and deselect SS2-4 by setting them to 1.

spi.set_input   lda VIA1_ACR
                and #%11100011    ; mask out SR control bits
                ora #%00000100    ; SR in under control of T2 
                sta VIA1_ACR          
     
                lda #%00111010    ; buffer input & SD card CS = 0 
                sta VIA1_PORTB          
                rts 

                
spi.set_output  lda VIA1_ACR
                and #%11100011    ; mask out SR control bits
                ora #%00010100    ; SR in under control of T2
                sta VIA1_ACR

                lda #%00111001    ; buffer input & SD card CS = 0
                sta VIA1_PORTB
                rts

So far all we have done is to set up things and we need the code to actually send a byte over SPI. Note that any reading or writing to the shift register (SR) clears the respective flag in the IFR. Once 8 bits have been sent or received the flag (bit 2 in IFR) is set to signal that data is ready and if the interrupt is enabled it will also trigger an interrupt.

spi.send_byte   sta VIA1_SR      ; send data to SR (also clears SR flag in IFR)
@wait           lda VIA1_IFR     ; check IFR flags
                and #%00000100   ; isolate SR flag  
                beq @wait        ; wait until done sending byte 
                rts
                

spi.get_byte    lda VIA1_IFR     ; check IFR flag
                and #%00000100   ; isolate SR flag  
                beq spi.get_byte ; wait until SR flag is set (when previous shift operation is completed)
                lda VIA1_SR      ; get data (also clears SR flag in IFR)
                rts 

You have to be a little careful as you can only clear the SR flag in IFR by reading or writing to the shift register. For SD card communication you wilI always send bytes before receiving (more about this in the next blog post where we will dive into the actual SD card and FAT format). You will notice that my send_byte function sends first and then waits (until the byte has been sent), whereas the get_byte function waits first and then sends. This works because of this sequence, but if you are not careful you could be caught in a loop waiting for a flag that is never set or cleared.

Ideally you want to do other things while the SR register is sending/receiving, so for sending the above can certainly be optimized. Later you will see in some of my code where I send or receive large quantities of data that I have made these optimizations.

At the maximal T2 clock speed it will take 32 clock cycles to send/receive a byte, which can conveniently be used to e.g. storing the previous byte in memory. If you count the cycles of the code you execute between reads and writes you wouldn’t even have to check the SR flag before triggering the next byte to be sent/received (but if you are not careful you could trigger the shift register before it is ready for the next byte).

These were the basics of sending and receiving bytes over SPI and in my next blog posts I will go over the specifics of communicating with an SD card in SPI mode.

Note 1: As I was writing this blog post I realized the maximum read/write speed is attained by driving the SPI clock signal from PHI2 rather than Timer 2. The speed is then fixed at half the system clock speed and sending/receiving a byte takes 16 clock cycles, rather than 32 or more with T2. To do this you simply set bits 2-4 of ACR to %010 for receiving and %110 for sending. I tested this with my SD card and it works nicely . However if you need variable speeds for SPI communication with other devices (that might not be as fast) you still want to be able to change the SPI clock speed through T2.

Note 2: The IDE I used for coding supports standard 6502 instructions, but not the extended 65C02 instruction set, so there are places where my code could be optimized. This is a project for me to begin at some point. Suggestions for good 65C02 compilers and/or IDE’s very welcome. From my C64 coding I have been using the excellent C64Studio from Georg Rottensteiner.