Block pin heqder J4 SPI games
Posted: Tue Oct 24, 2023 1:22 am
As verified elsewhere in the CX16 General Chat, the boards have the J4 pin header promised in the documentation. It is a straight line pin header:
[1] CA1
[2] CA2
[3] PB0
[4] PB1
[5] PB3
[6] CB2
This is enough to support the "4-wire" SPI interface: /Select, SCLK, MISO, MOSI, plus an extra output to send an alert to the SPI bus, and an extra input to receive an alert. If a chip supports both I2C and SPI, you might just use I2C for convenience, since the Kernel routine for I2C communication is already in place. However, if it is an SPI device, the I2C management overheads of accessing an SPI device through an I2C to SPI bridge chip means that using a direct bit banged SPI interface would be substantially more efficient. If you were aiming at a "slotless" TTL UART solution, the MAX3100 offers a cheap solution with 8byte hardware FIFO buffers, and if accesses over a bit-banged SPI interface on the CX16 certainly ought to support 19,200baud for byte-oriented serial UART communications, and perhaps 38,400baud.
J4 does not include power and ground, though, so if you need power and ground, you need them from another source. The most immediately available power tap would be the SNES 3 & 4 pin header, which gives VCC on Pin2 and GND on Pin6. A daughterboard that plugged into both of those (which are on opposite sides of the VIA#1 chip) could tap the power and pass the power/ground plus the other SNES 3 & 4 pins onto a pin header on the board.
The system pins on Port B that are used by the system are used for the IEC port, so if you do not load interrupt driven code that uses the IEC port, and you do not use the IEC port yourself in the middle of an SPI transceive operation, you can assume that the state of any outputs in PB3-PB7 when you call an SPI transfer routines are going to remain the same as the SPI transfer routine executes. This would not be true of PortA, since I2C access to the Keyboard/Mouse MCU and the operations on the SNES ports performed with the PortA GPIO are performed during interrupt servicing, so the state of PortA could indeed change during the processing of an SPI transfer routine.
After running through and clock counting a variety of possible bit banged SPI routines, it turns out that:
SPI_Init sets up the desired SPI mode by passing the clock polarity in A. Technically the clock polarity is either 0 or 1, but the routine takes any nonzero value as a request for a clock polarity of 1. The transfer routine implements data latching on the rising edge, so phase is the same as polarity. That is, if polarity is low, rising edge is leading, so latching occurs on the leading edge, which is called phase 0. If polarity is high, the rising edge is trailing, so latching occurs on the trailing edge, which is called phase 1. Therefore CPOL of 0 sets Mode0, and CPOL of 1 sets Mode 3. If you need Mode1 or Mode2, you can run the SCLK line through an inverter before running it into the device, but Mode0 and Mode3 handles the large majority of SPI ICs I have seen.
The Select API has three routines: SPI_Select turns on the select status, SPI_DeSelect turns off the Select status, and SPI_Reselect turns the status off then on (for example, for the MAX3100 UART, the selection is made at the start of each pair of bytes which makes up a MAC3100 operation). The Select and Reselect routine should be passed a Device number of #1, for upward compatibility with the 7-device SPI bus that can be implemented by adding a 74x299 universal serial shift register.
My shorthand for the SPI transfer routine is "TX", because SPI works as a transceiver -- sending an output byte at the same that it is receives an input byte.
The SPI bit bang routine first works out the byte for outputting a 1 or a 0 bit, putting them in X and Y. They are both created with the Clock bit set to 0. The output byte is shifted left to test the value and select between the 1 output or 0 output legs of the loop. The correct contents for PortB are transferred from register X or Y to register A, stored in PortB, then clock bit raised high, and stored again to give the rising edge of the clock bit. Then Port B is loaded into A, and the input bit is shifted into the carry flag.
By rotating the input bit into the zero page location containing the output value, the storage of the input bit also retrieves the next output bit. The loop is counted down (in zero page, since all count/index registers are in use), and if not finished, the correct leg of the loop is called for the next bit.
[1] CA1
[2] CA2
[3] PB0
[4] PB1
[5] PB3
[6] CB2
This is enough to support the "4-wire" SPI interface: /Select, SCLK, MISO, MOSI, plus an extra output to send an alert to the SPI bus, and an extra input to receive an alert. If a chip supports both I2C and SPI, you might just use I2C for convenience, since the Kernel routine for I2C communication is already in place. However, if it is an SPI device, the I2C management overheads of accessing an SPI device through an I2C to SPI bridge chip means that using a direct bit banged SPI interface would be substantially more efficient. If you were aiming at a "slotless" TTL UART solution, the MAX3100 offers a cheap solution with 8byte hardware FIFO buffers, and if accesses over a bit-banged SPI interface on the CX16 certainly ought to support 19,200baud for byte-oriented serial UART communications, and perhaps 38,400baud.
J4 does not include power and ground, though, so if you need power and ground, you need them from another source. The most immediately available power tap would be the SNES 3 & 4 pin header, which gives VCC on Pin2 and GND on Pin6. A daughterboard that plugged into both of those (which are on opposite sides of the VIA#1 chip) could tap the power and pass the power/ground plus the other SNES 3 & 4 pins onto a pin header on the board.
The system pins on Port B that are used by the system are used for the IEC port, so if you do not load interrupt driven code that uses the IEC port, and you do not use the IEC port yourself in the middle of an SPI transceive operation, you can assume that the state of any outputs in PB3-PB7 when you call an SPI transfer routines are going to remain the same as the SPI transfer routine executes. This would not be true of PortA, since I2C access to the Keyboard/Mouse MCU and the operations on the SNES ports performed with the PortA GPIO are performed during interrupt servicing, so the state of PortA could indeed change during the processing of an SPI transfer routine.
After running through and clock counting a variety of possible bit banged SPI routines, it turns out that:
- It is easy to support both Mode0 and Mode3 SPI clocks, for support of a wider range of devices
- The method of supporting Mode0 means the SPI clock is agnostic about which pin it is on
- Therefore the SPI Input (or 'MISO" for Master-In, Servant-Out) should be in PB0, for fastest access to the input bit.
- SPI Output (or "MOSI" for Master-Out, Servant-In), and SPI SCLK should be on GPIO, but with this routine is it arbitrary which is which, so I put the MISO and MOSI pins together, which is the convention on most micro-controllers.
- Either PA2 or PB2 may be used for /Select. I specify PB2 simply to place the optional auxiliary signal lines /AlertOut and /AlertIn next to each other.
Code: Select all
; PORTB:
; PB0 =: MISO (J4_Pin3)
; PB1 := MOSI (J4_Pin4)
; PB2 =: SCLK (J4_Pin5)
; CB2 =: /SELECT (J4_Pin6)
VIA1 = $9F00
VIA1_PB = VIA1
VIA1_DDRB = VIA1+2
VIA1_PCR = VIA1+$0C
; In the PCR, %110xxxxx is PB2 Low Output, %111xxxxx is Hi Output
; So the initialization sets all three high, /Select toggles bit5
PCR_SPI_SEL = %00100000
; SPI_CLK is in PB2
SPI_CLK = %00000100
; MOSI is in PB1, "TXA|STA" agnostic about position
MOSI0_MASK = %11111001 ; AND mask, done first
MOSI1_MASK = %00000010 ; ORA mask, done second
; Use API register 16 as ZP Temp
SPI_VALUE = $20
LP_COUNT = $21
; Mode 0 or 3 setting is done with the CPOL byte,
; which is a TRB mask, #SPI_CLK to reset the clock bit or #0 to do nothing.
CPOL: !byte 0
SPI_Init sets up the desired SPI mode by passing the clock polarity in A. Technically the clock polarity is either 0 or 1, but the routine takes any nonzero value as a request for a clock polarity of 1. The transfer routine implements data latching on the rising edge, so phase is the same as polarity. That is, if polarity is low, rising edge is leading, so latching occurs on the leading edge, which is called phase 0. If polarity is high, the rising edge is trailing, so latching occurs on the trailing edge, which is called phase 1. Therefore CPOL of 0 sets Mode0, and CPOL of 1 sets Mode 3. If you need Mode1 or Mode2, you can run the SCLK line through an inverter before running it into the device, but Mode0 and Mode3 handles the large majority of SPI ICs I have seen.
The Select API has three routines: SPI_Select turns on the select status, SPI_DeSelect turns off the Select status, and SPI_Reselect turns the status off then on (for example, for the MAX3100 UART, the selection is made at the start of each pair of bytes which makes up a MAC3100 operation). The Select and Reselect routine should be passed a Device number of #1, for upward compatibility with the 7-device SPI bus that can be implemented by adding a 74x299 universal serial shift register.
Code: Select all
SPI_INIT:
; Pass polarity in A, 0 for Mode0, 1 for Mode3
; Any non-zero value gives a polarity of 1
STZ CPOL
CMP #0
BNE +
LDA #SPI_CLK
STA CPOL
+ LDA VIA1_DDRB
AND #%11111000 ; Preserve DDPB3-7
ORA #%00000110 ; PB1,2 outputs, PB0 input
STA VIA1_DDRB
LDA #%11100000 ; CB1 mask for TSB
TSB VIA1_PCR ; b5-b7 = %111 => /SELECT = H
LDA #SPI_CLK
TSB VIA1_PB ; Set clock to Mode3
LDA CPOL
TRB VIA1_PB ; Reset clock if Mode0
RTS
SPI_DESELECT:
LDA #PCR_SPI_SEL
TSB VIA1_PCR ; low bit of PCR.CB2 = 1
RTS
;
SPI_RESELECT: ; Pass 1 in A
LDA #PCR_SPI_SEL
TSB VIA1_PCR ; low bit of PCR.CB2 = 1
; fall through to select
SPI_SELECT: ; Pass 1 in A
LDA #PCR_SPI_SEL
TRB VIA1_PCR ; low bit of PCR.CB2 = 0
RTS
My shorthand for the SPI transfer routine is "TX", because SPI works as a transceiver -- sending an output byte at the same that it is receives an input byte.
The SPI bit bang routine first works out the byte for outputting a 1 or a 0 bit, putting them in X and Y. They are both created with the Clock bit set to 0. The output byte is shifted left to test the value and select between the 1 output or 0 output legs of the loop. The correct contents for PortB are transferred from register X or Y to register A, stored in PortB, then clock bit raised high, and stored again to give the rising edge of the clock bit. Then Port B is loaded into A, and the input bit is shifted into the carry flag.
By rotating the input bit into the zero page location containing the output value, the storage of the input bit also retrieves the next output bit. The loop is counted down (in zero page, since all count/index registers are in use), and if not finished, the correct leg of the loop is called for the next bit.
Code: Select all
SPI_TX:
LDY #8
STY LP_COUNT
ASL ; Get first MOSI bit
STA SPI_VALUE ; Store rest of MOSI for later
LDA VIA1_PB
AND #MOSI0_MASK
TAX
ORA #MOSI1_MASK
TAY
BCS LOOP1
LOOP0:
TXA
STA VIA1_PB ; Store MOSI bit & drop clock
ORA #SCLK1_MASK
STA VIA1_PB ; Raise clock
LDA VIA1_PB
- ROR
ROL SPI_VALUE ; Store input & retrieve output
DEC LP_COUNT
BEQ +
BCC LOOP0
LOOP1: TYA
STA VIA1_FB ; Store MOSI bit & drop clock
ORA #SCLK1_MASK
STA VIA1_PB ; Raise clock
LDA VIA1_PB
ROR
ROL SPI_VALUE ; Store input & retrieve output
DEC LP_COUNT
BEQ +
BCC LOOP1
BCS LOOP0
+ LDA CPOL
TRB VIA1_PB
LDA SPI_VALUE
RTS