Saturday, May 23, 2015

nRF24l01 control with 2 MCU pins using time-division duplexed SPI

Doing more with pin-limited MCUs seems to be a popular challenge, as my post nrf24l01+ control with 3 ATtiny85 pins is by far the most popular on my blog.  A couple months ago I had an idea of how to multiplex the MOSI and MISO pins, and got around to working on it over the past couple weeks.  The result is that I was able to control a nRF24l01+ module using just two pins on an ATtiny13a.  I also simplified my design for multiplexing the SCK and CSN lines so it uses just a resistor and capacitor.  Here's the circuit:

Starting with the top of the circuit, MOMI represents the AVR pin used for both input and output.  The circuit is simply a direct connection to the slave's MOSI (data in) pin, and a resistor to the MISO.  Since this is not a standard SPI configuration, I've written some bit-bang SPI code that works with the multiplexing circuit.  To read the data, the MOMI pin is simply set to input.  Before bringing SCK high, MOMI is set to output and the pin is set high or low according to the current data bit.  The 4.7k resistor keeps the slave from shorting out the output from the AVR if the AVR outputs high, or vice-verse.

Looking at the SCK/CSN multiplexing part of the circuit, I've removed the diode that was in the original version.  The purpose of the diode was to discharge the capacitor during the low portion of the SCK clock cycles, so the voltage on the CSN pin wouldn't move up in accordance with the typical 50% duty cycle of the SPI clock.  My bit-bang duplex SPI code is written so the clock duty cycle is less than 25%, keeping CSN from going high while data is being transmitted.  The values for C1 and R1 are not critical and are just based on what was within reach when I built the circuit; in fact I'd recommend lower values.  470Ohms * .22uF gives an RC time constant of 103uS, meaning SCK needs to be held low for >103uS for C1 to discharge enough for CSN to go low.  Something like a 220Ohm resistor and .1uF capacitor would reduce the delay required for CSN to go low to around 25uS.

The R2 is far more important.  The first value I tried was 1.5K, and after fixing a couple minor software bugs, it seemed to be working OK.  When I looked at the signals on my scope, I saw a problem:

The yellow trace shows the voltage level detected on the MOMI pin at the AVR.  Each successive high bit was a bit lower voltage, so after more than a few bytes of data, all the bits would likely be read as zero.  I suspect this has something to do with the internal capacitance of the output drivers on the nRF module, as well as it's somewhat weak drive strength, documented in the datasheet at table 13.  A 4.7K resistor seems to be optimal, though anything from 3.3K to 6.8K should work.


Here is the AVR code for the time-division duplexed SPI:
uint8_t spi_byte(uint8_t dataout)
    uint8_t datain, bits = 8;

        datain <<= 1;
        if(SPI_PIN & (1<<SPI_MOMI)) datain++;

        sbi (SPI_DDR, SPI_MOMI);        // output mode
        if (dataout & 0x80) sbi (SPI_PORT, SPI_MOMI);
        SPI_PIN = (1<<SPI_SCK);
        cbi (SPI_DDR, SPI_MOMI);        // input mode
        SPI_PIN = (1<<SPI_SCK);         // toggle SCK

        cbi (SPI_PORT, SPI_MOMI);
        dataout <<= 1;


    return datain;

I also wrote unidirectional spi_in and spi_out functions that work with the multiplexed MOSI/MISO.  Besides being faster than spi_byte, these functions work with the SE8R01 modules that have inconsistent drive strength on their MISO line.

The functions are in halfduplexspi.h, and I also wrote spitest.c, which will print the value of registers 15 through 0.  Here's a screen capture of the output from spitest.c:


  1. This is seriously cool. I just have to try in myself when I get back to my lab in my home in a few weeks.

  2. another great post!

    Can you tell me how much memory was taken up (or left over) on the t13a + nrf code? Did you skip using any rf libs? I could use the extra free pin this offers.

    I'm tempted to try to redesign my t85+nrf24 (3KB) BLE beacon for the t13a, since they're so cheap ($4 @ 10).


    1. I didn't use an rf library for this. For the coin-cell powered nodes with a t88 that I built last year I modified the mirf library.

      My plan is to make a lightweight rf lib that can use my bitbang spi routines, but being the perfectionist that I am, it may take a couple weeks before I'm finished.

      Watch out with the cheap nRF modules - all the cheap ones I've seen are actually SE8R01 modules, which won't work for bitbanged BLE. The real nRF modules (or at least good clones) are around $7/10.

    2. thx for the warning about the SE8R01, I had some good luck with them talking to each other, but didn't even try to bitbang BLE on them.

      The smd nrf24l01+ ($2) from ebay are working well with BLE, at least while testing in the same room.

  3. hi can you make this proyect on PIC from microchip? i use a PIC16f876a

    1. I'm sure someone could, but not me. I don't use any PICs.

  4. Hi. First, thanks for your great work.
    I'm trying to make a autonomous sensor for humidity control (trinket 5v + DHT11 + Relay + nRF24l01). My goal is to send to a receiver (arduino uno r3 or raspberry pi) the status of the relay and the current conditions, and if it's possible, send override commands to activate/deactivate the relay or the autonomous part.
    Every available pin counts, but I had problems trying to implement your version because I really don't see where you connect to the receiving end and how can I test the spitest program. Hope you can explain it or at least, some useful link to understand what you do.
    Thanks in advance and sorry for any mistake in my writing.

    1. I don't understand what you are asking. The spi_byte code is bi-directional - it does transmit and receive. Though if you're using a uno r3 on the receiving side, you can just wire it up normally and use regular SPI.
      p.s. instead of a uno r3 I'd suggest a nano clone as they are much cheaper. The ones on aliexpress using the CH340G serial chip are <$2 shipped.

    2. I was trying to ask if you can show a quick implementation of your halfduplexspi with a Mirf or NRF24 library because I'm stuck on my limited electronic knowledge and I don't know what to do next.

      My progress on the humidity control (trinket 5v + DHT11 + Relay + nRF24l01):
      - I made a clone Uno R3 show temp/humidity to Serial at 2sec intervals.
      - I made a Trinket 5V switch ON-OFF a lamp connected to a relay while pressing two buttons simultaneosly.
      - I made two UNO R3 (1 official, 1 clone) talk to each other using two nrf24l01+ with the NRF24 library and works fine using a 1uF short on clone's VCC-GND wireless module. One receiving, one transmitting, works fine.
      - Tried:
      - (Trinket 5V with the above circuit) + (red led and R1K suggested in your previous post) + (your bbuart-halfduplexspi) + (modified spitest.c using Arduino IDE sintax) = no reception.
      - Additional 1uF between wireless module VCC-GND = no result.
      - Merging the Mirf library using your halfduplexspi.h = no errors, but not working at all.

      I have a background in programming, but this low level instructions strike me like a boxing glove in the eye and don't know where to go now.

      Hope you can help me or drive me in the right direction. Best regards and thanks for answering.

    3. Hi Angelo,
      If you were using avr-gcc I'd be able to offer some help, however if you're stuck on using the Arduino IDE and core, you're on your own. Although other people have (poorly) hacked my 3-pin code into the Arduino rf24 library, I'm not aware of anyone doing the same with my half-duplex SPI code. I'm sure I'm not the first to point out how much of a kludge with poor programming practices the Arduino core is.

      If you are serious about AVR programming I suggest you abandon Arduino and use the Atmel AVR toolchain (avr-gcc & avr-libc)

      If you can't give up your Arduino addiction, then I think you'll have the most luck with the RF24 library, as that one already has a crude port of my 3-pin nRF control.

  5. Hello, first off really nice work i used your nrf24l01 3 pin hack to get it running on a esp 01 module still running after half a year.
    I'm have added this new implementation to my minimal arduino code and its working but on my accelerated test it stops sending after 20-30k messages (140.. messages per sek) using 220Ohm resistor and .1uF works a bit better but still stops around 30k.
    i have tried different delays on the CSN timing,
    35 us for the discharge and 20 for the charge seems to give the best results.

    Have you done any more experimenting with this or maybe have any ideas abut better timings??

    1. So the 3-pin hack works on an ESP-01 but the 2-pin trick doesn't?
      My first guess would be the ESP module, as I have found them very noisy. Looking at the Vcc pin on the NOR flash chip I could see over 100mV of noise. I add a 0.1uF cap on RST to reduce the chance of spurious hangs/resets.
      I don't know the ESPs as well as I do the AVRs, but I'd also be concerned about interrupts. Any interrupt during the SPI transfer while the SCK pin is high could lead to CSN going high. If you don't already do it, I'd disable interrupts during the SPI xfer.
      A nrf to wifi gateway is an idea I've had in mind for a while but still haven't got around to doing it. Did you make a custom protocol to translate the nRF packets? My idea was to use SL/IP, since it is pretty simple to implement on a MCU and the ESP8266 SDK supposedly already supports it.

  6. It's really cool, I'm now working with ATtiny85s more and see how precious is every pin :-)
    I'm thinking regarding 433/315 rf transmitters is it possible to use them with only one pin for sending and receiving. When it transmits it turns off receiving part, and in this case even one antenna will be enough.

  7. This comment has been removed by the author.

  8. Hi there,
    Since many years a did various projects with php, js, python, bash/linux... and more recently I began to work with MCUs by using the arduino bootloader. I am aware that it is not much appreciated in the AVR community, but for now turning to C and byte shifts feels like a big step forward. Nevertheless, I got some attiny85 which I would like to use as a an introduction to avr coding. I would like to start by making a very basic remote control for my computer, using a joystick and a nrf24l01 module. A nano clone feels like overkill for that purpose, so I am very enthousiast about your 2-wire hack. However, I am used to the rf24 radiohead library for arduino, which send and receive C string on defined channels.. Using that library I made a 'bridge' receiver that transferts all RF strings to UART (to Python), and I was wondering if I could still use this for the receiver, along with your code for the sender unit. I am well aware that I gotta do my homeworks on this, but I was wondering if you could give me a hand with a simple code exemple of how to send a string by using your modified spi library. And thank you very much for sharing :)

    1. Not up to giving you a tutorial, but you might get some ideas from looking at more of my AVR code.

    2. Not up to giving you a tutorial, but you might get some ideas from looking at more of my AVR code.