[Ralph Doncaster], aka Nerdralph, seems to be absolutely driven to see how few resources he can use on a microcontroller to get the job done. In this post on his blog, [Ralph] writes some custom bit-banged SPI code to cut the number of SPI lines necessary to drive an nRF24L01+ radio module from four down to two. That really helps if you’re using a micro with only six free pins, like an ATtiny85.
If you’re going to say, “why don’t you just buy a bigger microcontroller?”, you’re missing the point. This exercise strikes us as optimization for optimization’s sake and a dirty hack, both of which are points in its favor. There are also a couple of techniques here for your mental toolbox. We thought it was interesting enough to look at in depth.
Before we dive in, here’s what you’ll need to know about SPI in three sentences. SPI normally uses four wires for a simple master/slave setup. The master sets a chip-select line (CS) to let the slave know it’s being spoken to, and starts toggling the clock line (SCK). Two more lines are used to carry data from the master to slave (master-out-slave-in or MOSI for short) and from slave to master (MISO). So how does [Ralph] combine these four into two?
To merge the clock and chip-select lines, [Ralph]’s circuit relies on a modification of a trick that he’s previously used. Notice that the CSN line on the radio module is connected to SCK on the microcontroller through a resistor and capacitor to ground: through a lowpass filter. With the values set just right (which will depend on the clock frequency) the value on CSN will be the average voltage on the SCK line, just like when you use a lowpass filter to smooth out “analog” PWM output.
If the clock uses a pulse-width narrow enough to average to a logic low when the clock is running, the CSN line will get pulled to some in-between voltage that’s low enough to be interpreted as logic low when the clock ticks, and the nRF24L01+ will start listening. [Ralph]’s custom SPI code uses a 25% duty cycle to make this work.
When the microcontroller is done sending clock ticks, it will hold the clock line high for a relatively long time, pulling the CSN line high, and letting the nRF24L01+ know that it’s no longer being spoken to.
Normally, both the master and slave set up their next data bit on their respective output lines at the same time, after a low clock transition in this example. Then both parties read their input lines after they see the clock line flip high again.
In [Ralph]’s duplexing scheme, the master sneaks both a read and a write to the single “MOMI” line within the setup period. (Note that we used a symmetric clock in the graphic above, rather than the 25% duty cycle that [Ralph] needs for the chip select hack.)
When the clock transitions low, the nRF24L01+ starts to set its desired output voltage on its MISO line, which is then read by the microcontroller. While still in this setup period, the micro then switches to output mode and establishes its desired voltage on the line before it toggles the clock high again, signalling the nRF24L01+ to read from the line. The master then switches the pin back to input, allowing the nRF24L01+ to control the line, but doesn’t read yet. Finally, the micro drops the clock to repeat the cycle for the next bit.
There are two things [Ralph] needs to get right to make this duplexing work, and the first is the timing. On a low clock transition, the nRF24L01+ sets its output voltage. A little bit of time has to elapse until the microcontroller can reliably read this voltage, and that time will depend on the line’s capacitance, the value of resistor R2, and the strength of the nRF24L01+’s output driver.
Then, after the microcontroller has read the nRF24L01+’s voltage, it will impose its own output voltage on the line and then toggle the clock high again, signalling the nRF24L01+ to read the value. Here, R2 needs to be chosen large enough that it allows the microcontroller to override the nRF24L01+’s own MISO signal.
So picking the resistor R2 is key. It has to be selected small enough that the nRF24L01+ can set up its desired voltage on the shared line before the microcontroller reads it in. But the resistor also has to be large enough that the microcontroller can easily override the nRF24L01+’s own output coming in through MISO. [Ralph] used an oscilloscope to pick the right R2 value, and it works.
But that’s probably also why this type of arrangement isn’t seen much in the wild. Engineers love simple protocols that work reliably across different circuit layouts and with added noise. Here, [Ralph] is using a low-but-not-digital signal level for CSN and then hand-picking a resistor based on line capacitance and the nRF24L01+’s output driver strength, which is not something that Nordic specs for in making the chips. In other words, be prepared to tweak the value of R2 or play with the SPI timings if you try this at home.
In sum, though, this is a sweet hack — an attempt to get the most out of a constrained system by hand-tweaking it beyond the point that any reasonable engineer would. We like that. But by doing so, is [Ralph] living on the edge? Or is the entire system more robust than our inner engineer gives it credit? What are your thoughts, and if you try it out, your experience?
Embed with Elliot is a column in which microcontroller expert [Elliot Williams] explores and explains interesting, clever, unique, strange, and sometimes wrong uses of embedded systems. Help keep this column fresh by sending in tips for the next installment.