Arduino SPI library - what's with the NOPs in the transfer functions? - Page 1
Forum section: electronics / general

author: HwAoRrDk
author_rank: Super Contributor
timestamp: March 28, 2017, 07:21:47 am
content: I have been writing some bare-metal SPI code for the AVR platform (ATmega328P, specifically) and thought I would have a look at how theArduino SPI librarydoes things, in case there's any gotchas or something I missed. I was looking at the code for thetransfer()andtransfer16()functions, and found that they have put in a bunch of NOP instructions in-between copying the byte to the data register and waiting for the transfer to complete, like so:Code:inline static uint8_t transfer(uint8_t data) {SPDR = data;/** The following NOP introduces a small delay that can prevent the wait* loop form iterating when running at the maximum speed. This gives* about 10% more speed, even if it seems counter-intuitive. At lower* speeds it is unnoticed.*/asm volatile("nop");while (!(SPSR & _BV(SPIF))) ; // waitreturn SPDR;}I'm a bit puzzled as to why that's present. The comment preceding the NOP doesn't really seem to explain anything aboutwhyit has the claimed effect. Anyone got any thoughts?

author: bktemp
author_rank: Super Contributor
timestamp: March 28, 2017, 07:31:11 am
content: I haven't looked in detail at the timing but my guess is, it introduces a delay to align the SPI complete wait loop with the actual timing:Reading the SPIF bit, checking this bit and branching back takes maybe 3 cycles. So depending on when this 3 cycle sequence starts, it may miss the end of the SPI transfer by 1 clock cycle and do another loop, wasting 2 clock cycles. Somebody probably did some simulations or measurements and tweaked the number of nops so the 3 clock cycle wait loop just starts at the right time to read the SPIF flag in the first cycle it gets set to avoid any unnecessary delay.This only works for the maximum SPI clock frequency, but for lower speeds it doen't care as much if the wait loop wastes another 1 or 2 cycles because the SPI transfer already takes 32, 64 or more clocks.

author: HwAoRrDk
author_rank: Super Contributor
timestamp: March 28, 2017, 09:02:36 am
content: Ah, that makes sense. Thanks.So with a CPU clock of 16MHz and a maximum SPI clock of 8MHz (divisor of 2) set, it would take 16 cycles to transfer one byte. But then if perhaps it takes 7 cycles for the wait loop, it'd have to iterate 3 times before it knows the transfer is flagged complete, wasting 5 cycles and delaying transmission of subsequent bytes. If the loop is expanded to 8 cycles, that's an exact modulus of 16, so it'd only need to loop twice. Neat!I guess if you're using something like an SD card or Ethernet peripheral over SPI, you'll want the highest transfer rate you can possibly squeeze out, and this must make a noticeable difference.

author: HwAoRrDk
author_rank: Super Contributor
timestamp: March 29, 2017, 07:35:59 am
content: Here's a related question regarding SPI: it was stated within an Atmel app note I read that it's a good idea, when setting up SPI, to make a dummy read to both SPSR and SPDR in order to ensure the SPIF bit in the status register is cleared. So, in my code I did the following:Code:void spi_init(const uint32_t speed, const spiDataOrder_t order, const spiMode_t mode) {uint8_t temp;/* ... etc ... */temp = SPSR;temp = SPDR;}Of course, with this the compiler gives me a "variable 'temp' set but not used" warning. This got me thinking about the possibility that, because I'm not using the value oftemp, the whole thing may be optimised away completely! Am I right to be concerned about that?Can I avoid any such possibility by simply making thetempvariable volatile? Or do I need do nothing at all because SPSR/SPDR are both ultimately volatile themselves?Or is there some way of marking a code block to say to the compiler "don't optimise anything about this"? I know I can of course mark thetempvariable itself with__attribute__((unused)), but that just suppresses the warning as far as I understand.

author: Ian.M
author_rank: Super Contributor
timestamp: March 29, 2017, 07:49:18 am
content: Check what assembly the compiler generates for:Code:SPSR;SPDR;The compiler knows reading a volatile can have side effects so should generate code to do so even though the expression result is immediately discarded.

author: HwAoRrDk
author_rank: Super Contributor
timestamp: March 29, 2017, 09:14:23 am
content: This is what it generates with optimisations disabled (-O0):Code:temp = SPSR;3890:8d e4ldir24, 0x4D; 773892:90 e0ldir25, 0x00; 03894:fc 01movwr30, r243896:80 81ldr24, Z3898:8b 83stdY+3, r24; 0x03temp = SPDR;389a:8e e4ldir24, 0x4E; 78389c:90 e0ldir25, 0x00; 0389e:fc 01movwr30, r2438a0:80 81ldr24, Z38a2:8b 83stdY+3, r24; 0x03And this is what it generates with optimisations enabled (-O1):Code:temp = SPSR;20ae:8d b5inr24, 0x2d; 45temp = SPDR;20b0:8e b5inr24, 0x2e; 46

author: Ian.M
author_rank: Super Contributor
timestamp: March 29, 2017, 09:32:27 am
content: What does my proposal generate? It should be very similar to the second one.

author: HwAoRrDk
author_rank: Super Contributor
timestamp: March 29, 2017, 11:25:10 am
content: Ah, yes, sorry - I misunderstood what you meant. It generates identical code:Code:SPSR;20ae:8d b5inr24, 0x2d; 45SPDR;20b0:8e b5inr24, 0x2e; 46As far as the assembly goes, what's that actually doing? I'm not sure what the 'IN' instruction does. Does that satisfy the demand of reading the two registers?

author: Ian.M
author_rank: Super Contributor
timestamp: March 29, 2017, 11:44:27 am
content: Quote from: HwAoRrDk on March 29, 2017, 11:25:10 amYes. Its using core register r24 as a 'scratch' register and reading the peripheral registers via their I/O space addresses.Check the sections : subsections :-* AVR Memories : I/O Memory* Register Summary* Instruction Set Summary : DATA TRANSFER INSTRUCTIONSin a full AVR MCU datasheet for details.

author: Jeroen3
author_rank: Super Contributor
timestamp: March 29, 2017, 12:02:32 pm
content: Quote from: HwAoRrDk on March 29, 2017, 07:35:59 amYes, do this to avoid "unused" warning:Code: (c)void foo(int unused){(void)unused;}You're right to be concerned. However, all registers are volatile, so the reads are not optimized.See for yourself in the definitions header file.You can usenon-portablecompiler pragma and attributes. See the manual of your specific compiler version.Code: (c)void dontOptimizeThisFunction(void) __attribute__((optimize("-O0")));Or inline asm the code. Or change optimizations per file in the makefile.