2023-12-29_pi-spi-rates.md
2023 12 29
OK, SPI is looking pretty good; actual bit times are identical to what-they-should-be, so I'm not even going to note them down (whereas UART out of the PI has some drift, probably strange BAUD fractional maths).
SPI Waveform Integrity
So, there's just the question of waveform integrity, and (probably) the ability of the RP2040 to keep up, and there's the actually-delineating-and-echoing-etc test.
I'd like to look at some scope traces first, but I should probably do it in context (i.e. actually driving a pin, not free air), so I'll setup my little test rig...
CH1 (yellow) is Chip Select
CH2 (blue) is the Clock
CH3 (magenta) is Data Dout
Mbit/s | Traces |
---|---|
1 | ![]() |
2.5 | ![]() |
5 | ![]() |
10 | ![]() |
15 | ![]() |
20 | ![]() |
25 | ![]() |
50 | ![]() |
So the waveform basically looks like it survives all square-waved up to 5Mbit, then starts rounding off towards 10Mbit, and is probably serviceable between there and 25Mbit, where things are looking quite dicey indeed - as far as my relatively untrained eye can tell. By 100Mbit, all hope is lost.
I suspect that "the move" here will be two-fold: (1) to do decent EE and line these traces with via-punched GND noise capture (to prevent the crosstalk we are seeing), and then to instrument each packet with some kind of CRC.
I want to point out another benefit of the SPI route, which is that (if you look at the trace from 2.5Mbit) there is an inter-packet time of only 6 microseconds, so in our python sketch:
import spidev
bitrate = 50000000
print(f'rate {bitrate/1e6}MBit/s bit period should be {1000000000/bitrate}ns')
spi = spidev.SpiDev()
spi.open(0, 0)
spi.max_speed_hz = bitrate
for i in range(1000000):
spi.xfer([12, 14, 95])
spi.close()
The time between spi.xfer()
s is that few-microseconds of python returning to the top of the loop. If we look at similar code for UART:
class CobsUsbSerial:
def __init__(self, port, baudrate=115200):
self.port = port
self.ser = serial.Serial(port, baudrate=baudrate, timeout=1)
self.buffer = bytearray()
def write(self, data: bytes):
data_enc = cobs.encode(data) + b"\x00"
self.ser.write(data_enc)
def read(self):
byte = self.ser.read(1)
if not byte:
return
if byte == b"\x00":
if len(self.buffer) > 0:
data = cobs.decode(self.buffer)
self.buffer = bytearray()
return data
else:
return
else:
self.buffer += byte
We are normally pulling one byte at a time - if we want to be catching while we are transmitting. I suspect that protocol work can avoid these mechanisms, but the other thing we don't have to deal with in the SPI case is packet framing: the CS line does that for us, we are COBS-less. The complexity ofc is that then we need to develop protocol between top- and bottom- layers to (probably) pack datagrams into fixed size frames (or something something) I digress.
SPI Echo Code
OK, so to see if we can get up to that 10Mbit real-transfer-rate target, I think I will start with an echo test - since SPI is transfer-based anyways (we always send data when we get it), I'll program some arduino to get packets and echo them, and we'll turn the speed up until we start making mistakes.
I have hello-worlded this with the Earle Core, but their SPI implementation is strange - since it's simple enough (I'm starting to see that... that's the pint), I'll just roll my own... listening to CS down / up interrupts to frame packets, stuffing buffers, you know the dealio. It's 730pm here but that's just time to put the embedded hardo hat on...
I'm going a bit mad with this; I can get GPIO interrupts to fire just on a falling edge but not just on a rising edge ... I should see if I can access some lower level masks, or something? But it's genuinely sending events marked as rising edge events on falling and rising edges...