Search Microcontrollers

Saturday, April 26, 2014

Stellaris Launchpad - NRF24L01 radio - Part 1

I have a couple of digital radio transceivers hanging around my desk since quite some time, so I decided to finally try them out.

These are cheap Nortel NRF24L01 devices, it is common to find them in small breakout modules that communicate via SPI protocol.


Some have an 8 pin connection, some others a 10 pins one, they are just the same in fact since the 10 pins one have 2 VCC and 2 GND pins, normally pins are marked on the silkscreen.

WARNING : The chip works at 3.3V (VCC) which is fine with the Stellaris, but should you use it with an arduino or another 5V MCU make sure you provide a proper VCC. The digital signals are also at 3.3V, but they are 5V tolerant.

There are popular libraries for Arduino / AVR for them and even a Stellaris library for the Energia environment  ( Arduino-like for the Stellaris Launchpad).

I actually prefer to use them in my common environment, plain CCSV5 with driverlib support, so I decided to write my own module.

You can find the datasheet of the module here (Sparkfun site, they sell those little boards too).

I reverse engineered code snippets found on the web, from the various libraries and managed to write my own code.

First off, since we are dealing with a SPI connection we are going to have  a SCLK, MISO , MOSI connection, plus a CE (Chip Enable), a CSN and an IRQ generated by the module itself.
So, it is a sort of " rich" SPI interface.
While the first three (MISO, MOSI and  SCLK) are managed directly by the SSI module of the Cortex M4F, we still need to manage manually the other three signals using 3 GPIO conections, two outputs and 1 input for the IRQ.

I decided to use SPI on port SSI1 (uses pins D.0, D.2, D.3) and the GPIO port E to drive CE, CSN and IRQ -E.1, E.2, E.3- (I saw a library on the web using this setup and found it quite smart, since the connections are all aligned on the launchpad connector).
I stored these values in variables, just to allow for some flexibility in configuration

unsigned long NRF_GPIO = GPIO_PORTE_BASE;
unsigned long NRF_PERIPH_GPIO = SYSCTL_PERIPH_GPIOE;
unsigned char CEPIN    = GPIO_PIN_1;
unsigned char CSNPIN   = GPIO_PIN_2;
unsigned char IRQPIN   = GPIO_PIN_3;

Then I created an init function to enable the needed ports, set up the SPI etc :

void setSPI()
{
  unsigned long dummy = 0;
  SysCtlPeripheralEnable(SYSCTL_PERIPH_SSI1);
  SysCtlPeripheralEnable(SYSCTL_PERIPH_GPIOD); // SPI
  SysCtlPeripheralEnable(NRF_PERIPH_GPIO); // CS, CE, IRQ
  GPIOPinConfigure(GPIO_PD0_SSI1CLK);
  GPIOPinConfigure(GPIO_PD1_SSI1FSS);
  GPIOPinConfigure(GPIO_PD2_SSI1RX);
  GPIOPinConfigure(GPIO_PD3_SSI1TX);
  GPIOPinTypeSSI(GPIO_PORTD_BASE, GPIO_PIN_3 | GPIO_PIN_2 | GPIO_PIN_1 | GPIO_PIN_0);
  SSIConfigSetExpClk(SSI1_BASE, SysCtlClockGet(),
                 SSI_FRF_MOTO_MODE_0,
         SSI_MODE_MASTER, 1000000, 8);
  SSIEnable(SSI1_BASE);
  GPIOPinTypeGPIOOutput(NRF_GPIO, CEPIN| CSNPIN);
  GPIOPinTypeGPIOInput(NRF_GPIO, IRQPIN);
  while(SSIDataGetNonBlocking(SSI1_BASE, &dummy))
   {}
}

The while cycle at the end is there just to remove from the FIFO whatever garbage data might be eventually present.

The NRF24L01 chip is quite versatile, can operate on various channels and can easily manage communication on a network of addressable devices.
To allow this flexibility, the chip is configured with a set of registers, so the first thing we need to implement are the functions to read from and write to these registers, plus the CE / CSN handling.

The process is quite simple :
Each register has a 5 bit address (documented in the data sheet, but since I am lazy I just imported an existing .h file that defined them all -you can find it here-) and if we combine these 5 bits (r rrrr) with 001 in the 3 MSBs (001r rrrr) then we have a 1 byte command that, when delivered via SPI, tells the chip we want to write the register r rrrr.
The following byte is the value we want to store in the specified register.
Reading is similar , but we will add 000 in the 3 MSBs instead, so the command for reading r rrrr is 000r rrrr .
Details about reading values will be discussed later.

However, for the chip to respond we need to properly manage the CE and CSN signals.

The chip is enabled when CE is asserted LOW and when there is no communication from the master, CSN should be HIGH, so this is our initial setting.
Then, before sending data via SPI, CSN must be transitioned to LOW and finally back HIGH once the communication (writes + reads) is terminated.

void setCE(unsigned char val)
{
  if (val>0)
 GPIOPinWrite(NRF_GPIO, CEPIN ,CEPIN); //CE HIGH
  else
GPIOPinWrite(NRF_GPIO, CEPIN ,0); //CE LOW
}

void setCSN(unsigned char val)
{
  if (val>0)
GPIOPinWrite(NRF_GPIO, CSNPIN ,CSNPIN); //CSN HIGH
 else
  GPIOPinWrite(NRF_GPIO, CSNPIN,0); //CSN LOW
}

The two setCxx functions are quite obvious, not particularly elegant I have to admit, but they get the job done.

void sendChar(unsigned char ch)
{
  SSIDataPut(SSI1_BASE, ch);
  while(SSIBusy(SSI1_BASE)) {}
}

sendChar is a helper function that outputs a byte to the SPI port, I added a while loop to ensure data is flushed out before leaving the function, I am not 100% sure we need that, but it helped me when debugging the line with my DSO.

void _writeReg(unsigned char addr, unsigned char val)
{
  setCSN(0);
  sendChar(W_REGISTER | addr);
  sendChar(val);
  setCSN(1);
}

unsigned long _readReg(unsigned char addr)
{
  unsigned long result = 0xff;
  setCSN(0);
  sendChar(R_REGISTER | addr);
  while(SSIDataGetNonBlocking(SSI1_BASE, &result)){}
  sendChar(0xf0); //whatever value, used 0xf0 because 
      // it is easily visible with the oscilloscope
  SSIDataGet(SSI1_BASE, &result);
  setCSN(1);
  return result;
}

_writeReg is quite a straightforward implementation of what I described before (W_REGISTER = 0010 0000) while _readReg is a bit trickier :
First thing notice the while loop used to throw away data right after the read register command.
This is needed (you can check it with an oscilloscope on the RX (MISO) line of the MCU) because as soon as you start sending data on the TX channel (MOSI), the slave device writes garbage data on the RX (typically a 0x0E value I found in my experiments).

After you sent the command, then, you need to send out a dummy byte (it does not matter the value) per each byte you need to read -in this case 1- and finally read the value and bring CSN high.



[You can see in yellow the MOSI line, roughly between the two purple vertical cursors there is the first byte. In red the MISO line is immediately answering with "garbage" data which I suspect is in fact the status of the module.
It is easy to recognize the second dummy byte being 0xF0 and then we get a (correct) response 0x03 on the MISO]

I then found a sequence of "default" values of the registers used to init the module :

void nrfInit()
{
  unsigned long dummy =0;
  _writeReg(CONFIG, 0x00);  // Deep power-down, everything disabled
  _writeReg(EN_AA, 0x03);
  _writeReg(EN_RXADDR, 0x03);
  _writeReg(RF_SETUP, 0x00);
  _writeReg(STATUS, ENRF24_IRQ_MASK);  // Clear all IRQs
  _writeReg(DYNPD, 0x03);
  _writeReg(FEATURE, EN_DPL);  // Dynamic payloads enabled by default
  SysCtlDelay(SysCtlClockGet() / 100 / 3); // grace time, never hurts 
  while(SSIDataGetNonBlocking(SSI1_BASE, &dummy)) {}
}

So, the init overall sequence is :
setSPI();
setCE(0);
setCSN(1);
nrfInit();

I also found that a way to check if the connection with the module is working is to check the value of the SETUP_AW register, which should return 6 MSBs = 0 and the two LSB with 01 (3byte address) ,10 (4 bytes) or 11 (5 bytes) while 00 means illegal number.


int _isAlive()
{
  unsigned long aw;
  aw = _readReg(SETUP_AW);
  return ((aw & 0xFC) == 0x00 && (aw & 0x03) != 0x00);
}

At this point, we can check if the basic communication via SPI works :

#include <inc/hw_memmap.h>
#include <inc/hw_types.h>
#include <driverlib/gpio.h>
#include <driverlib/sysctl.h>
#include <driverlib/uart.h>
#include <inc/hw_timer.h>
#include <driverlib/timer.h>
#include <driverlib/pin_map.h>
#include <utils/uartstdio.c>
#include "driverlib/ssi.h"
#include "nRF24L01.h"

void setClock()
{
   SysCtlClockSet(  SYSCTL_SYSDIV_4 | SYSCTL_USE_PLL |
        SYSCTL_XTAL_16MHZ  | SYSCTL_OSC_MAIN);
}

....

int main(void) {
 setClock();
 // we  use the led to provide feedback
  SysCtlPeripheralEnable(SYSCTL_PERIPH_GPIOF);
  GPIOPinTypeGPIOOutput(GPIO_PORTF_BASE, GPIO_PIN_2 | GPIO_PIN_1 | GPIO_PIN_3);
  GPIOPinWrite(GPIO_PORTF_BASE, GPIO_PIN_2 | GPIO_PIN_1 | GPIO_PIN_3, 0); // LED OFF

 setSPI();
 enableNRF();
 SysCtlDelay(SysCtlClockGet() / 10 / 3);
 nrfInit();
 if (_isAlive()!=0)
  GPIOPinWrite(GPIO_PORTF_BASE, GPIO_PIN_3, GPIO_PIN_3); // GREEN
 else
  GPIOPinWrite(GPIO_PORTF_BASE, GPIO_PIN_3, 0); // OFF
} // main

Ok, so far we implemented the basic communication with the module, now we need to investigate how the radio data transmission happens.

Reading the Nortel datasheet shows some complexity and also allows us to understand that these devices can be operated in quite a few different ways.

Some basic concepts : 
In a network, at a given time, we identify one PTX and one  PRX, these stand for Primary Transmitter and Primary Receiver.
Obviously we are dealing with a Transceiver, so this module can send and receive data.

Table 12 in the Datasheet shows the different states we can set the module


(@Nordic)

For the actual data transmission, a mechanism called "Enhanced ShockBurst"(TM) is used, Nordic defines it as "a packet based data link layer".

What this layer does is to take care of the delivery of payloads (strings of data with 0 to 32 bytes content) from a TX to an RX station.

Enhanced ShockBurst has an automatic handling which automated a few basic actions for us, as an example, after a PTX finished sending out a packed of data, it cnverts it automatically to PRX to be ready to receive an ACK packet.
ACK packets can be also generated automatically if the module is configured to do so.
In fact in the init procedure we have a  _writeReg(EN_AA, 0x03); instruction which enable this Auto ACK feature.
If an ack is not received, then the PTX will retry to send the packet a number  of times, automatically.
To prevent that a packet is read twice (duplicating the data) a PID (Packet Identification) is associated to each packet, a CRC is also used to verify if data is correct.

The packet itself is composed by different sections (Preamble, Address, packet control field, payload and CRC), but it is properly assembled by the device itself using an automatic packet assembly functionality.

Similarly a packet is decoded (disassembled) automatically on the RX side.

An important feature,m on the RX side, is the Multiceiver.
In fact each receiver can use up to 6 data pipes, each one responding on a different address.
There is no magic here, one single channel is used at a time, so you cannot stream different payloads to different pipes at the same time, this functionality is there mainly to allow you to logically separate data streams, for whatever need you might have.

Addresses can be 3, 4 or 5 bytes (can be configured via the SETUP_AW register), so, assuming we are using 5 byte addresses some rules apply: 

1) the lower by is unique across all the pipes
2)  pipes 1 to 5 have the same 4 high bytes

When using the multiceiver feature, the PRX can receive packets from multiple PTXs (on different pipes), it stores on each pipe the "return address" of the TX and uses it to send automated ACK packets.

Specific registers RX_ADDR_Px are used to specify the RX addresses for the 6 pipes, each of these registers is 5 byte wide.

An example of multiceiver configuration (from the nrf24l01 datasheet) is reported here



(@Nordic)

So, our next step will be to write the functions that set up the channel, the TX and RX Addresses and finally that deliver a payload.
While I work on that, you might as well go through the datasheet.

[to be continued]

2 comments:

Unknown said...

Nice work so far! When can we expect the remainder of the library?

Franz said...

Hello Aaraon and thanks for your comment.
You are right! This second part is due since quite some time now!
I have it in draft, but need to complete a few experiments as I seem to have issues with the variable payload.
Unfortunately I am a bit busy lately and have limited spare time to play with these things.
Hopefully this will get better soon!