Tuesday, June 5, 2012

Why abstraction and open source is valuable even in small embedded programs: An example using the Arduino SPI library

  Since the object oriented "revolution", abstraction and code reuse are the bywords of modern software development even when programming in non-OO languages.  But use of these techniques has lagged in highly embedded development due to perceived inefficiencies compared to C and assembler.   Yet embedded development can be incredibly detail-oriented and time consuming.  It took a bug report discussion  and listserv conversation (you can read the originals by following the links, or just read my summary after the break) between a couple of highly experienced engineers to figure out the best solution for an 8 line chunk of code that on the surface is quite simple.  It just enables the SPI bus.

  This experience shows the power of FOSS and abstraction in microcosm.  First, judging by the discussion, any one of these engineers (including myself) working alone would likely not have figured out the best solution.  This would have meant revisiting the issue some time in the future.  And second, once this time-consuming, detail-oriented problem was solved it never needs to be solved again.  This (added to countless similar cases) creates tremendous efficiencies affecting (judging by hits to this blog) engineers across the entire world.  Neither of these effects would have taken place in classic closed-source development.




The Arduino SPI library is quite stable, but recently I noticed that its simple 8 line initialization routine did not work for certain classes of chips.  This routine essentially proceeds in a fashion typical of most Arduino code, first setting the appropriate IO lines to be "outputs" with pinMode, then initializing them (digitalWrite) and finally turning on the SPI.  But it turns out that the exact order of these 8 lines really matters, and in fact was not correct. 

The original SPI initialization routine looked like this:

void SPIClass::begin() {
// Set direction register for SCK and MOSI pin.
// MISO pin automatically overrides to INPUT.
// When the SS pin is set as OUTPUT, it can be used as
// a general purpose output port (it doesn't influence
// SPI operations).

pinMode(SCK, OUTPUT);
pinMode(MOSI, OUTPUT);
pinMode(SS, OUTPUT);

digitalWrite(SCK, LOW);

digitalWrite(MOSI, LOW);
digitalWrite(SS, HIGH);

// Warning: if the SS pin ever becomes a LOW INPUT then SPI
// automatically switches to Slave, so the data direction of
// the SS pin MUST be kept as OUTPUT.
SPCR |= _BV(MSTR);
SPCR |= _BV(SPE);
}

The issue is that the value of SS is unknown coming into the routine.  So once SS is switched to OUTPUT, it might be LOW which would activate the chip.  This would cause the write of SCK and MOSI to LOW to actually clock in a bit for SPI modes that trigger on a falling edge of the clock signal (remember the various SPI "modes" control exactly when the data is sampled relative to the clock SCK).  But the code works fine for modes that sample on a rising clock signal (the more common mode 0, for example).

The original suggestion was to simply move the digitalWrite(SS) above the other 2 digitalWrite calls.  This would stop an attached chip from listening so monkeying with SCK and MOSI would not matter. 

But this solution only works IF the chip is connected to SS.  However its quite common for there to be only a single SPI "slave" chip on the bus, especially for Arduino newbies.  In this case it is easy (and perfectly valid) to just tie the "slave's" chip select line LOW -- essentially make it always on.  In that case, the proposed fix would not help. 

It turns out the best solution seems to be to move the pinMode SCK and MOSI calls below the initialization of the SPI bus.   Doing it that way transfers control of these lines directly from acting as input to SPI output lines -- they never behave as "normal" output lines which avoids clocking in a bit!

But it turned out there was one more change to make things perfect.  If pinMode(SS) is called first, then there is a possibility that it could be driven low.  So it turns out it is better to first write a 1 to the SS line first, and then set it as an output.  The effect of this is that if SS was configured as input (by some other part of the sketch), it will simply pull-up (if you "write" a 1 to an IO line configured as an "input" you enable an internal pull-up resistor) and then be driven up.  If SS was configured as output it will just drive up.  The effect of both cases is just a single transition to high instead of a driven low and then high line.

So this is the final copy of the SPI initialization code -- which ought to work for chips that use any "mode", any chip select line (or none at all), and any initial state of the lines:
 void SPIClass::begin() {

 // Set SS to high so a connected chip will be "deselected" by default
  digitalWrite(SS, HIGH);

  // When the SS pin is set as OUTPUT, it can be used as
  // a general purpose output port (it doesn't influence
  // SPI operations).
  pinMode(SS, OUTPUT);

  // Warning: if the SS pin ever becomes a LOW INPUT then SPI
  // automatically switches to Slave, so the data direction of
  // the SS pin MUST be kept as OUTPUT.
  SPCR |= _BV(MSTR);
  SPCR |= _BV(SPE);

  // Set direction register for SCK and MOSI pin.
  // MISO pin automatically overrides to INPUT.
  // By doing this AFTER enabling SPI, we avoid accidentally
  // clocking in a single bit since the lines go directly
  // from "input" to SPI control.  
  // http://code.google.com/p/arduino/issues/detail?id=888
  pinMode(SCK, OUTPUT);
  pinMode(MOSI, OUTPUT);
}

No comments:

Post a Comment