Close

rp2040 SPI slave woes (aka meh, should have used a 555...)

A project log for vector-06c mini

A 50% size replica of my favourite 8-bit computer

svofskisvofski 11/20/2023 at 10:491 Comment

TL;DR

Now for the ranty log...

So the idea was that the mini keyboard uses a Pi Pico as a kind of universal interface adapter, something that you can plug in into a PC and have a little keyboard for your v06x emulator and simultaneously be able to attach it to v06x-mini board via SPI interface and use it with the esp32 hardware emulator. The board layout actually even allows it to be plugged in directly into the original steamy DIP-packagey Vector-06c -- however in this case PiPico should not be soldered down as it's not 5V tolerant and scanning the keyboard would most likely damage it. So it's amazing and should be easy, right?

Oh so wrong...

SPI keyboard matrix scanning

rp2040 SPI peripheral is fundamentally broken in every possible regard, at least when you're trying to implement a slave device. On top of it there's a very raw driver in the SDK which introduces another layer of brokedity. After a day or so trying to figure out why I can't receive 2 bytes, I found out that in order to be able to transfer more than one byte in one CS assertion, you absolutely must use SPI mode 3.

That's not really a problem, why not use mode 3 indeed. After a while though it turns out that in mode 3 somehow the bit position tends to get garbled and it results in a bit-shifted message. I can't figure out a way to consistently deliver it. The only solution that I can find is to automatically reset SPI peripheral on the PiPico if the message is not right. And it seems to work.

So the protocol at this point would be something like this. When a program on the host v06x encounters an OUT instruction that selects columns, it retransmits this selection to the PiPico. E.g. "0xe5 0xfe" to select column 0. Later IN instruction sends a request "0xe6 0x00" and receives rows in return. For some other technical reasons I had to add a third byte to be able to reliably get the data back.

So all of this is minor technical details, something you always deal with when putting relatively unknown pieces together. What matters is that albeit with more complexity and slightly less pretty than initially imagined, stuff works.

SD card

Enter the second peripheral.

So there's also an SD card, which is a must have. There are more free pins on ESP32, but on this board it is connected to the same bus, using all the same wires as the external SPI peripheral, except for the CS pin obviously. That was a known from the beginning, and it should never be a problem. SPI slaves hi-Z their I/O when CS is not asserted and have no impact on the bus whatsoever.

I'm really glad that the Espressif SDK has SD/MMC card and FAT components conveniently standardized. Huge kudos to them for doing that. So I have put together my first little test that would just list files on the card and print them in the console. However, the card would not mount. After verifying all pin connections and some frustrating searching for similar errors, I remembered about the keyboard. Some long troubleshooting hours later I realised that SD card works when I disconnect MISO pin from the keyboard, and only then. No matter what I would do, MISO line connected to PiPico with initialised SPI peripheral (completely regardless of mode) would mean total and complete bus sabotage. Beautiful.

I have long suspected that eventually I would have to use a custom SPI peripheral built using PIO. So this seemed like a good moment to try. But after some research, and much later than I should have, I found that the only example doing that that I could find needs consecutive pins in an arrangement not compatible with the standard SPI peripheral pins. Which I very much need because the board is routed with the standard SPI in mind. So PIO stuff goes out of the window...

My second (third, thwentieth?) idea was to manually control GPIO function, something that the SPI peripheral should do by itself but doesn't. Long story short, I have an interrupt handler that connects MISO to SPI peripheral when CS goes low and disconnects when CS goes back up. This is ridiculously stupid, but it's the only solution that works.

Unfortunately, this switchover in software takes time and even using exclusive ISR handler on GPIO transition I lose several clocks. This is where the super-resourceful ESP32 came to the rescue. Their SPI peripheral apparently was designed by actual people with actual brain working on it, so it has a ton of little tweaks that you don't immediately notice at first -- that is until you really need them and begin reaching for the straws. So there is one such spi_device_interface_config_t.cs_ena_pretrans, which allows you to specify an extra delay between pulling CS low and start of transmission. And it comes to rescue. By adding several clocks here I can compensate for the switchover delay. And magically my keyboard works again, sharing the same bus with SD card.

Then comes the question of being able to correctly transmit actual data in just a single 16-bit transaction (because in mode 0 you can't have more than one word, remember). But it's a relatively minor thing, I just had to abandon using API calls from the SDK and use the peripheral directly. Before a transaction, I prime the transmit buffer with the current state of modifier bits and rows so that the host gets the full status in every transaction.

Should have used a 555....

I don't know if choosing some shift registers over a rp2040 would make things significantly easier. I'm sure it would be very easy for some, I personally would manage to screw up something in this configuration as well. Besides, the keyboard is not just a 8x8 matrix, it's also 5 extra bits for modifier and reset keys and a LED, so that would add even more complexity to the discrete logic solution. I guess in the end my choice of microcontroller is as good as any.

Discussions

maciel310 wrote 03/05/2024 at 21:43 point

> needs consecutive pins in an arrangement not compatible with the standard SPI peripheral pins

Hey, came across this post when I was researching manually implementing SPI on the Pico myself, and I just wanted to let you know that I was able to come up with a workaround to this issue. I hit a similar roadblock in my project, and the solution was to use absolute GPIO pin numbering for the `wait` command, instead of relative PINS numbering. That let me move the `in_base` to the MOSI pin instead of the CLK pin, which then lets you use the IN instruction as expected.

So this:

    wait 0 pin 1
    wait 1 pin 1
    in pins, 1

becomes this:

    wait 0 gpio 2
    wait 1 gpio 2
    in pins, 1

This of course limits your flexibility in dynamically reassigning the PIO program to different pins, however I think that's generally not going to be an issue for most people and it's a reasonable trade-off IMO.

Definitely agreed that it would be nice to have a bit more flexibility in the instruction set for the PIO, though it does seem really powerful if you can figure out the "right" way to use it.

  Are you sure? yes | no