Fastest possible SPI slave on a MCU? - Page 1
Forum section: electronics / general

author: L1L1
author_rank: Regular Contributor
timestamp: September 06, 2020, 01:37:31 pm
content: Hello all!Question: what's thefastest speedyou have managed to implement for anSPI slaveon a typical microcontroller (e.g. ARM Cortex M0) ?To be more specific, I'm looking at cases where the SPI master and slave communicate with messages of the typical format[Command byte][data byte 1][data byte 2]...[data byte N], where N varies.I can't get past 3 or 4 Mbits/s on STM32F0 running at 48Mhz, above which I get synchronisation issues, and I'm looking for ideas or code examples to reach higher performance.L1L1EDIT: To be clear I'm asking about implementing code for aSPI SLAVEnot a SPI master. I'm talking about implementing the code of an SPI slave on the microcontroller.

author: Siwastaja
author_rank: Super Contributor
timestamp: September 06, 2020, 01:54:21 pm
content: Use DMA.Fastest rate is usually clearly documented in the peripheral documentation, and may be half of the frequency of the peripheral bus clock, for example.Utilize the packing modes and FIFOs of the DMA, if available, so that if your SPI word is 8 bits, you don't generate 8 bit bus operations, but combine each 4 bytes to a single 32-bit bus operation. Especially important if the CPU has to go through the same bus for RAM or ROM.I think I have gone to somewhere around 50 Mbit/s.If every communication requires some reaction + processing, instead of just flooding data, then writing this code efficiently is a big thing. With DMA, you could double buffer, i.e., collect the next transaction with DMA to memory while processing the previous transaction (in ISR triggered by DMA completion, for example). In tight spots, use your imagination to play tricks; for example, you could interrupt on DMA half transfer already and start processing the data, so that DMA writes more data to the memory while your code processes the first bytes.

author: T3sl4co1l
author_rank: Super Contributor
timestamp: September 06, 2020, 02:16:34 pm
content: Well heck, I've just got a XMEGA here that's running an SD card at 16MHz (and as it happens, that's the maximum rate, half the 32MHz peripheral clock). (Though the overall bandwidth for the operation is around 250kB/s. The operation being, loading a (potentially non-contiguous) file, from the file system, into a display graphics buffer. I suspect most of the overhead is the file system, with copying through internal buffers being the next runner-up. But I just put it together the other day so I haven't had a close look at it yet.)Yeah, >= 50MHz SPI (even QSPI) is normal for ARMs. RTFDS.Some can even boot natively from external Flash in this way! Poke around the manufacturer's configuration / code generation tools, and study their examples.Tim

author: L1L1
author_rank: Regular Contributor
timestamp: September 06, 2020, 02:35:39 pm
content: My question is about implementing a SLAVE not a master. Of course, if you are writing code for an SPI master talking to an SDCARD, you can get pretty fast performance with DMA and stuff.But consider the opposite: how would you emulate an SDCARD with a micro acting as a SPI slave?Quote from: T3sl4co1l on September 06, 2020, 02:16:34 pm

author: TK
author_rank: Super Contributor
timestamp: September 06, 2020, 02:39:33 pm
content: Have you tried with STM32CubeMX or STM32CubeIDE? It will give you the speed constraints based on the main clock for the SPI slave. It shouldn't matter if it is master or slave, you can use the same techniques like DMA, events / interrupts...

author: L1L1
author_rank: Regular Contributor
timestamp: September 06, 2020, 02:52:38 pm
content: I'd love to try DMA but how would that help in a case where you have SPI commands like [Command byte][data 1][data 2]...[data N]?More concretely, say for example you have a SPI message  which means "Read" (opcode 0x01) from address 0x20, 4 bytes (here marked as 0x00 by the master):As a slave I get the opcode (0x01) and the address (0x20), and then I start sending the 4 requested bytes. Even with DMA, I have a very short time to set up the DMA channel correctly once I have received the opcode and address (half an SPI clock cycle).Quote from: Siwastaja on September 06, 2020, 01:54:21 pm

author: L1L1
author_rank: Regular Contributor
timestamp: September 06, 2020, 02:55:43 pm
content: I'm working with direct register access. The problem is not setting the hardware for maxium processing speed. The issue is on the software side as a slave: how do you find the time to process the commands sent by the master, and send a response without skipping a byte in the request.Quote from: TK on September 06, 2020, 02:39:33 pm

author: snarkysparky
author_rank: Frequent Contributor
timestamp: September 06, 2020, 05:51:58 pm
content: Read the SPI into a buffer in the hardware interrupt for the SPI peripheral. Outside the interrupt process the SPI commands as quickly as you can. The interrupt will ensure you don't miss any bytes.

author: Siwastaja
author_rank: Super Contributor
timestamp: September 06, 2020, 06:00:55 pm
content: Quote from: L1L1 on September 06, 2020, 02:52:38 pmIt usually helps because without DMA, you would be going into the interrupt handler (say, with some 20-ish-cycle overhead) for each byte, then updating state and doing housekeeping inefficiently for each byte.Consider DMA, where you have zero CPU intervention, until you have the whole packet to process; further, you can acquire the next packet while processing the previous one, in parallel, with no extra CPU penalty because the DMA is working in background.Processing many data bytes at once may be faster, for example if you have 32-bit values there load them in one instruction and do math directly, instead of constructing from single bytes.Details depend on the actual protocol and use cases, but most likely, DMA does help; I'm assuming your packets are longer than just one or two bytes.If you have one command byte and one-two data byte(s), or something super simple like that, DMA may be worse than interrupt-based.Some SPI peripherals, for example the one I used in STM32H7, has an internal FIFO which can supply you more than one byte at once; if you whole message fits in one read/write operation, then obviously DMA is only a hindrance; access the SPI FIFO instead.

author: T3sl4co1l
author_rank: Super Contributor
timestamp: September 06, 2020, 06:11:43 pm
content: Duh, it does say slaveSpeaking of SD, the response comes some cycles after the command byte; many (some? most?) SD cards have MCUs of some sort inside them, and it stands to reason they're doing much the same internally. Seems like a good starting point.At the very best, you can have buffered data ready to go, and receiving the command byte triggers an interrupt, which selects the buffer used to feed the subsequent frames. This will require at least a modest delay between command byte end and data byte start, or a dummy byte (or several). Without a bus WAIT signal, or clock stretching (like I2C sometimes can do), the dummy byte would seem to be the better option.Or if it doesn't need to respond to the command byte at all, well, you could potentially set up the buffer, ready to go, and have no latency.But if you need to do work to build those buffers, you'll need even more time to spare.Perhaps the bytes can be computed on-demand, coincident with access, but that can be complicated to design (e.g., a monolithic C or even asm driver that interleaves memory and port access), and won't be applicable in all cases. (An SD card can stream data from its memory core, but a block cipher can't start transmitting until it's finished computing the first block.)I suppose an SD card does a bit of both, probably with a lot of helper hardware as well. There's a delay which may be enough to process block remapping and buffer a few bytes; then it can stream from there, and perform incremental preparations along the way.There's also the callback, where you receive a command, then some time later, assert INT, and data are read out. Obviously this is much slower. It could be pipelined, but then there has to be some way to figure out which packet came from which request; among other pipeline management operations. That will take some overhead, so needs to be justified. (Probably burst-mode devices would be good at this, spinning HDDs for example?)(That's kind of a funny idea, heh. There wasn't really a point in history where SPI was in common use, and where HDDs were still small enough to be worth interfacing through it. Note that classic FDD and HDD interfaces are bit-serial, but they're timed by spindle speed, not externally clocked, so an SPI controller for them would be... rather involved. And these days, even if you make an SPI-SATA bridge for instance, there isn't much that you'd be able to access through it, that wouldn't also fit entirely inside the HDC cache. That is, you could spendwhole secondssubmitting random read or write commands, before the cache has been completely flushed.)And then there's read-only and write-only devices:- An SPI DAC doesn't need to respond instantly, or compute anything besides perhaps a register index, which is all very easy to do in hardware (if harder to emulate in software, I suppose). Latency should be... some time after the end of the frame (potentially including zero time). Often a LATCH signal is provided, so you can get consistent, synchronous timing.- An LCD display controller simply addresses control registers, or queues pixels into its frame buffer. Latency is... milliseconds, you won't know anything's changed until the next screen refresh! It can take its sweet time making updates and maintaining internal state, as long as it has enough internal and external bandwidth to do what it needs to (e.g. full screen redraw per frame?).- A sampling ADC does need to respond instantly, but stores/accesses very little internal state; most use SCK as ADCCLK, literally cranking out bits in real time. It would be hard to emulate with an MCU, but fairly easy to build even with discrete gates.- A lot of S-D ADCs apparently use MCUs internally. Sampling is a gradual affair (the sample rate is low, say 1~1kS/s); probably they can simply clock out the current running tally (sum of high bits over the sample period) on the SPI channel, pretty much on-demand. That is, there isn't much internal state to request, it's always ready to go, not dependent on the input command (at least, not immediately; it will take some milliseconds or whatever for configuration changes to be acquired).That feels fairly comprehensive, at least as a tour of things that I've seen. Network protocols fit in here: packet based, pipelined request-response and such. Any types I don't know about? So, take your pick.Tim

author: L1L1
author_rank: Regular Contributor
timestamp: September 06, 2020, 06:42:53 pm
content: This is not how SPI works usually. The master expects to see a response on the MISO line, synchronously with the bytes clocked on the MOSI line. If you process the SPI command outside the interrupt you won't be able to respond with the same SPI transaction.Quote from: snarkysparky on September 06, 2020, 05:51:58 pm

author: L1L1
author_rank: Regular Contributor
timestamp: September 06, 2020, 08:29:50 pm
content: Quote from: Siwastaja on September 06, 2020, 06:00:55 pmI tried DMA in the past, but it didn't help much. I found that the devil is in the details. Consider the following 6-byte SPI transaction (hopefully this is rendered in monospace font):SCLK --<//////////////////////>--MOSI --<A0><A1><A2><A3><A4><A5>--MISO --<B0><B1><B2><B3><B4><B5>--_ _SS \________________________/Now assume that we are in the situation I was describing previously where:- A0 is the command indicator (e.g. 0x01) which means "read at address"- A1 is the address (e.g. 0x20) to which the "read at address" command applies.- A2, A3, A4, A5 are 4 bytes set to a random value by the master, designed to be filled by the slave with 4 bytes of data read at address 0x20.In the best case, the slave will get an event when SS goes low and can get ready to read the first byte A0 (the command). It can then issue B1 on MISO (which is not important here). It is preceded by B0 which is usually garbage (or more typically a copy of whatever was previously in the TX buffer in a previous transaction).Next the slave can wait for A1 (the address). It should then immediately clock out B2, which corresponds to the first byte at indicated address (e.g. the byte at address 0x20).Next the slave can wait for A2. It should then immediately clock out B3, which corresponds to the byte at the indicated address + 1 (e.g. the byte at address 0x21).Next the slave can wait for A3. It should then immediately clock out B4, which corresponds to the byte at the indicated address + 2 (e.g. the byte at address 0x22).Next the slave can wait for A4. It should then immediately clock out B5, which corresponds to the byte at the indicated address + 3 (e.g. the byte at address 0x23).Next the slave can wait for A5. It should then immediately clock out a hypothetical B6, which corresponds to the byte at the indicated address + 4 (e.g. the byte at address 0x24). This byte will never be transmitted because the SPI transaction terminates.I can see how it's possible to use DMA when starting to clock out B2. This means however that the DMA address must be configured and enabled in a very short time, corresponding to roughly half a SPI clock cycle, between the end of the last bit of A1 and the beginning of A2/B2. I've done some experiments in the past with DMA in this configuration and the performance was poor.An improved solution is to allow the master to send a "dummy" byte between A1 and A2. This gives plenty of time for the DMA setup. The limiting factor is then the processing of A0 and A1, which remain "off DMA". In my experience, this still limits the speed to a few megabits.As a side note, as far as I remember, as a slave, it's painful to deal with DMA transactions of unknown lengths on the STM32F0 as in the example above where the master determines the number of bytes transferred by ending the transaction. You basically need to configure your DMA for the maximum possible length and then cancel the DMA transaction when the SPI transaction ends, which requires a SPI reset to do properly. A solution is to require the master to specify a "length" in addition to the address described in the example above, but you still need to deal with mis-behaving masters.

author: Siwastaja
author_rank: Super Contributor
timestamp: September 06, 2020, 08:39:48 pm
content: You may already know this, but note that DMA configuration sequences as documented by the manufacturer often contains extra garbage, like this: "write to DMA config register. Write to address register. Write number of data. Write the enable bit in the config register." Manufacturer libraries are even slower. You may want to experiment what you can set beforehand, what stays between cycles, and what you truly need to write. It may be just 1-2 register writes, in your example the memory address register and the enable. Note that if the enable bit is in the same register as the config, doing REG |= REG_EN causes read-modify-write, but doing:#define REG_STANDARD_CONFIG whateverinit: REG = REG_STANDARD_CONFIG;enable: REG = REG_STANDARD_CONFIG | REG_EN;results in only the write operation, saving time and bus operations.You can also consider mixing non-dma and dma usage.But as you say, the devil is in the details and you seem to have a good grasp of the problem already. Being creative and spending time at the problem while listening to the hints of other, even if not directly applicable, is the key.Quote from: L1L1 on September 06, 2020, 08:29:50 pmYes, been through this, for truly nCS-delimited variable-length slave operation, you need to reset the SPI peripheral through the freaking RCC reset registers; this was on F7. On H7, they had partially fixed it, so that turning SPI peripheral enable off and on is enough, still a nuisance and shouldn't be that way. It's sad there STILL isn't any meaningful nCS hardware management mode even when it's been there on paper for over a decade.

author: NorthGuy
author_rank: Super Contributor
timestamp: September 06, 2020, 08:46:30 pm
content: Quote from: L1L1 on September 06, 2020, 02:52:38 pmLooks like you need to make your decision on what to send after you have received the address byte (0x20), but before the master starts clocking the next byte. Most likely, DMA cannot make the decision for you, so you need CPU.If master doesn't have delays between bytes (as it may, and you need to look at the datasheet for this), this is only 1 clock cycle time you're looking at. If your SPI clock is 3 MHz, and 48 MHz is your CPU speed, you have at most 16 instructions to get into ISR, parse the byte, make the decision and write the byte back. Doesn't sound possible for the ARM processor. Therefore, I think the master does insert at least some delay between sending the address and reading the bytes. If you can configure this delay, you can speed up the SPI clock a lot. If not, keep working on shortening your code from the ISR entry to the point where the decision on what to send is made.

author: PCB.Wiz
author_rank: Super Contributor
timestamp: September 06, 2020, 10:52:06 pm
content: Quote from: L1L1 on September 06, 2020, 06:42:53 pmDo you have any control over the master code ?If not, you may be stuck where you are already - if the slave does not know what it needs to do, until already into the burst, and the master has no means to pause, then any SW is going to limit reactions.The only way to get a fixed-master link faster, would be to code a FPGA which can make the needed faster mid-message decisions.If you do have control over master side, then things get much simpler - you can split into command and data messages, and raise/pause CSN between those, and the SW manages the commands and DMA could manage the burst.You could even have a shared CSN drive that gives simple handshake - eg master asserts CSN for command send, then releases and polls until slave pulls CSN low, saying it has DMA primed based on last command.Slave releases CSN low when it sees any SPI clocks.

author: ejeffrey
author_rank: Super Contributor
timestamp: September 06, 2020, 11:58:30 pm
content: Quote from: Siwastaja on September 06, 2020, 06:00:55 pmThat isn't how it works for a standard SPI read request. You only have half a clock cycle to turn around between receiving the address and asserting the first bit of data. SPI flash actually solves this with a fast read command that inserts wait cycles between the address and data phases and you must use the fast write command above a certain clock frequency. But if you don't get to define the protocol you aren't going to be able to operate at high speed on a typical MCU.

author: Siwastaja
author_rank: Super Contributor
timestamp: September 07, 2020, 08:13:14 am
content: Yes, if the command byte defines the very next result byte, and you have control over this protocol, change it. It can be dummy byte(s) to give some time; it can be separate short command transaction, then delay, then read-the-result transaction, or it can even be "give the result of the previous operation while listening for the next command".If this scheme is set in stone, it's going to be slow, and is expected to be slow by the designer of the protocol.You could trig the interrupt from the falling NSS so that you would already be in the handler, skipping the ISR latency (was it 20 cycles for M0? Definitely more than the 12 cycles of higher end ARM MCUs), then just busy-wait for SPI completion for the few remaining cycles of SPI operation. This would allow you to read SPI command byte, parse it, write to the DMA memory address register, and enable DMA quicker than if you first have to wait for the ISR latency. It's still going to take some 10 cycles, I guess.

author: L1L1
author_rank: Regular Contributor
timestamp: September 07, 2020, 10:43:49 am
content: Thank you all for your suggestions and ideas!I can now reach about 14Mbits/s (without even using DMA), which is close to the maximum the chip I use will allow in slave mode (18Mbits/s according to the datasheet).The solution was to:- add a single "dummy" byte (a wait state) in read transactions in the protocol (write transactions don't need one),- aggressively fill the transmit buffer as soon as possible,- inline everything possible (replacing a couple of function calls with linlines or marcros).So to take my previous example, the SPI transaction to read 4 bytes of memory at address 0x20 would now be, where:- 0x01 is the opcode for "Read"- 0x20 is the address to read from- 0x00 is an extra dummy byte, which gives the slave enough time to resspond.- 0x00, 0x00, 0x00, 0x00, which are 4 bytes marked as 0x00 by the master, which will carry the result of reading 4 bytes at address 0x20 on the MISO line.After reading Siwastaja's suggestion to look the packing modes and fifos, I modified the SPI to work 16bits at a time. It ultimately failed, but I was able to identify some spots where optimizations could be applied and that helped a lot.

author: Jeroen3
author_rank: Super Contributor
timestamp: September 07, 2020, 01:07:45 pm
content: Impressive. I think, at 14 MHz SPI you have only 27 instructions per byte. You're losing at least 16 for the isr jump, which should mean you're doing your ISR in only 11 instructions!Perhaps you can speed things up a bit more by only interrupting at the opcode, then go polling waiting for the address, winning the 16 cycles inherent interrupt latency.Basically you're writing the ISR in assembler now, right?Of course, anything else your chip is doing will have a problem when you're doing spi now...

author: TK
author_rank: Super Contributor
timestamp: September 07, 2020, 01:44:15 pm
content: If it is the only think the slave SPI MCU is doing, you could do it with a polling loop without interrupt.

author: L1L1
author_rank: Regular Contributor
timestamp: September 07, 2020, 02:17:07 pm
content: Quote from: Jeroen3 on September 07, 2020, 01:07:45 pmI'm working in C but I'm not doing an interrupt for each byte. I've been experimenting with three approaches:1) Doing no interrupt at all. Process everything in the main loop: just wait for SS to go down and work on the request with interrupts dissabled, re-enable them when SS goes up.2) Doing an interrupt on the SS signal and processing all the data in the interrupt.3) Doing an interrupt on the SS signal, processing the first few bytes in the interrupt and offloading the rest to DMA.Both approaches (1) and (2) enable to reach 14Mbits/s.Approach (1) is the easiest, but it might not fit in all applications.Approach (2) is a bad idea in most cases, but it was interesting to check how it performs.With approach (3) the maximum speed seems to be 12Mbits/s, because the code offloading things to DMA creates a small "bottleneck". It seems like an interesting compromise for application where the master uses a fast SPI clock.

author: IDEngineer
author_rank: Super Contributor
timestamp: September 13, 2020, 07:03:56 pm
content: A bit of a late comment, but....This is actually a problem even for some commercially available IC's that have an SPI interface. As inferred earlier in this thread, in a traditional SPI exchange the slave has less than one bit time to decode the command byte and prepare the return data to be clocked out (unless there's a dummy byte to give the slave extra time). At higher clock rates it becomes basically impossible for the slave firmware to execute fast enough.Obviously the best way to handle a synchronous scheme like SPI is with dedicated hardware. A firmware layer above it then doesn't have to adhere to the strict timing requirements. A quick glance at typical SPI hardware peripherals in MCU's reveals they can run up to 16MHz. That's a bit time of 62.5nS. You're not going to get very many instruction cycles in 62.5nS, which is why most little SPI parts (like memory parts, etc.) have dedicated hardware to do things like translate the leading byte(s) into an address so they can start shifting in/out the data on the very next clock.I've run into some interesting hardware vs. firmware implementations in SPI parts, particularly things like accelerometers and IMU's that offer "advanced features" like low-pass filters. If you use just the basic features, they work great. But as you start enabling more features - especially things like digital low-pass filters which are likely implemented in firmware - there's a point where the data just goes bad.When I first encountered this I spent a bunch of time characterizing it, to the point that on the specific device I had a chart showing the approximate processing burden of each "feature". If the sum of each enabled feature's burden was less than the apparent "budget" for processing, the part could have the data ready by the time the first data bit had to be shifted out. But cross that line, and you still got bits - they were just bogus. Importantly, there was NO WARNING when you crossed the threshold. The part failed silently, just emitted useless data. I scoured the datasheet but found nothing which hinted that some configurations, combined with sufficiently high data rates, could yield bad behavior. (Separately, after endless days of frustration, my personal rule is to simply capture raw data off such sensors and do ALL post-processing myself. It's just more reliable.)My interpretation of these results is that the SPI interface itself is implemented in hardware, while the "advanced features" are implemented in firmware. (Technically they could be hardware too, but other things in the spec sheet strongly implied they were firmware based.) The hardware is synchronous and blindly shifts out whatever data is available when the time comes. If the firmware can't keep up, sorry for you.Anyway, my point is that high data rates in general start forcing hardware implementations (either dedicated hardware, or FPGA's as suggested by another respondent herein). Instruction cycle times just can't keep up. 16MHz frankly isn't all that fast in terms of serial data streams yet it's already beyond most processing cores if you must make decisions in one bit time or less.

author: PCB.Wiz
author_rank: Super Contributor
timestamp: September 13, 2020, 10:35:00 pm
content: Quote from: IDEngineer on September 13, 2020, 07:03:56 pmOuch !Another approach, if you have the luxury of control of both ends of the link, is to do what the OP eventually decided, and insert dummy byte(s), very similar to FLASH memory schemes.A SPI+firmware system could emit dummy 0x00's until it had valid data, then emit 0xff, and then actual valid data.The host just sends dummy poll bytes and checks for a 0xff incoming.

author: ejeffrey
author_rank: Super Contributor
timestamp: September 13, 2020, 11:47:09 pm
content: It's best to have a fixed number of dummy bytes so the total transaction size can be known in advance.

author: mac.6
author_rank: Regular Contributor
timestamp: September 21, 2020, 11:03:23 am
content: I used to interface a 3G modem with a slave SPI interface up to 100MHz clock. It was tricky to do as you want to rely as much as possible on DMA.So if you have a fast DMA and can control both side of the interface it should be easy.Stick to fixed 32bits multiple frame so you can always have a waiting DMA transaction, freeing your slave CPU as much as possible.Since SPI is bi-directionnal you can use the upstream path to do flow control mechanism (either hard on/off or leaking bucket).