RF communication between Arduino Nanos using nRF24L01

In this tutorial I’ll go through a simple example of how to get two Arduino Nano devices to talk to one another.

Materials

You’ll need the following materials. I’ve posted Amazon links just so that you can see the items, but they can be purchased in a variety of locations.

  • Arduino Nano 5V/16 MHz, or equivalent (Amazon)
  • Kuman rRF24L01+PA+LNA, or equivalent (Amazon)

About the nRF24L01+

The nRF24L01+ is an appealing device to work with because it packs a lot of functionality on-chip as opposed to having to do it all in software. There is still a lot of work to be done in code; but it’s a good balance between simplicity and functionality. It’s also inexpensive.

What follows is a lengthy description of the nRF24L01+ device. If you just want to connect up your devices, then you can skip to the device hookup section.

nRF24L01+ theory of operation

There are several libraries for the nRF24L01 in the public domain that seek to simplify interactions with a variety of MCU’s. While they are fine (and we’ll make use of one here) you should understand how the device works so that when you inevitably branch out from the basic demonstration projects, you know how to achieve what you want. Read the nRF24L01 datasheet. I’ll start out here by reviewing it at a high level.

More than likely you are working with a breakout board for this surface-mount device. So you will concern yourself only with the following pins: Vcc, GND, CE, CSN, IRQ, MISO, MOSI, SCK. For the purposes of this example, we won’t be using the interrupt line IRQ so you can leave it disconnected.

The nRFL01 has a relatively simple instruction set for the SPI interface.

Table 8: nRF24L01 SPI instruction set

Reading from nRF24L01 registers

The protocol for addressing the transceiver via the SPI instruction set begins by bringing the CSN line from high to low, thus informing the nRFL01 that an instruction is about to be clocked into it. Next you clock in the command byte for the instruction you which to execute, followed by the data relevant to that instruction. Before we talk about how to configure the instructions, we should glance at the register addresses because we will refer to their addresses in the examples. You will find the memory map of the addresses on pages 22-26 of the nRF24L01 datasheet. For our example, let’s assume we want to read the RF_SETUP register. From page 23 of the nRF24L01 datasheet we see that the memory address is 0x06. To read that register, we bring CSN low, send an R_REGISTER instruction encoded with the register address. From Table 8, we see that the format for this instruction is 0b000aaaaa where the 5 bits aaaaa represent the memory address of the register. In this case we would send 0b00000110 (0x06). For this instruction, we expect a single byte return. To get that return we have to clock in a dummy byte on MOSI. Note that every command also returns the STATUS register.

Writing to nRF24L01 registers

The device would be useless if we couldn’t write to any registers, so we should talk about how to do that. Table 8 shows the W_REGISTER command has the following format: 0b001aaaaa where the 5 lower bits represent the address of the register we want to write to. Let’s say we want to set the data rate to 1 Mps and the RF output power to minimum. First let’s take a look at the format for the RF_SETUP register:

The upper 3 bits are reserved and should be 000. The PLL_LOCK bit is only used in testing. So that leaves us with bits 3:0. Bit 3 RF_DR sets the data rate. Since we want 1 Mbps, that bit gets unset. The next 2 bits set the RF_PWR. Minimum power is 00 for these two bits. The last bit LNA_HCURR sets the low noise amplifier gain. The nRF24L01+ datasheet discusses the LNA gain in more depth. The LNA gain allows the device to reduce the current consumption in receive mode at the expense of some receiver sensitivity. Since we’ll be sufficiently powered, it’s OK to leave that bit unset.

So, the write protocol is to drop CSN and send the W_REGISTER command configured for the address of interest. So, we’ll send 0b00100110 (0x26) followed by 0b00000000 (0x00) to congure it in the way we describe above (1 Mbps, low output power.)

Read receive payload

The next command of interest is the R_RX_PAYLOAD which as the name implies reads a payload of bytes that were received by the device. The command format requires no configuration; it is simply 0b01100001 (0x61.) There is some “choreography” involved in using this command because you must manipulate the CE pin also. How do you know you’ve received a packet? You know a packet has been received when an RX_DR (data ready) interrupt has been triggered. We’ll get to this later but for now, you should know that this interrupt exists as a bit in the STATUS register (yes, the register that we constantly get returned when we send any command.) We can also choose to configure the transceiver to send a hardware interrupt when it receives a package. When a unit is receiving, the CE pin must be high, once you’ve received a package, you have to bring the CE pin low, the send the R_RX_PAYLOAD command as usual. Next, you clock in the same number of dummy bytes as your payload size in order to read out the payload. What happens if you’ve received multiple payloads? The device keeps multiple payloads (3 per pipe) in a first-in, first-out (FIFO) stack. When you’re done receiving, you should clear the RX_DR interrupt and bring the CE pin high again to start receiving.

Write receive payload

To write a payload, we’ll use the W_TX_PAYLOAD. When your device is transmitting, you hold CE low (opposite of read). The “choreography” here is a little different. First we send a W_TX_PAYLOAD (0b10100000 = 0xA0) along with the number of bytes specified by the payload size. Next, we signal a transmit by toggling the CE pin from low to high to low over at least 10 µs. The ТХ_DS interrupt will be set if the packet is sent. Actually, the behaviour is a little more complicated than that. This interrupt actually depends on whether you have auto-acknowledge enabled for the pipe. If you do, then the TX_DS interrupt is only set if you get an ack from the receiver on the pipe. If you are auto-acknowledging on that pipe then, then you also have to look for the MAX_RT flag on the STATUS register to see whether the maximum number of retries has been reached. As with the receiver, there’s also a FIFO transmit stack, so you can stack up to three packets before sending (by toggling CE.)

Flushing the TX and RX stacks

There are two SPI commands that clear the TX and RX FIFO stacks, FLUSH_TX and FLUSH_RX respectively. Neither has has any associated bytes.

NOP command reads STATUS register

There is a NOP (no operation) command that takes no additional bytes and whose only purpose is to read back the STATUS register quickly. It is faster than R_REGISTER because you don’t have to pass the address of the STATUS register.

nRF24L01 registers

Next, I’ll be talking about some of the nRFL01 registers. Since the nRF24L01 datasheet covers everything, we’ll just go over the high points and any gray areas. As always, if you just want to get two Arduinos talking to each other, you can skip to the device hookup section.

Configuration register

The CONFIG register at address 0x00, has a number of useful bits.

CONFIG register

Bits 6,5,4 control how we use the IRQ pin. If we want the RX_DR (packet received) interrupt to show up on the IRQ pin, then we would set the MASK_RX_DR bit. Then it will show up as an active low state on the IRQ pin. Likewise for the MASK_TX_DS interrupt. Remember that the TX_DS flag behaviour depends on whether we’ve enabled auto-acknowledge for the pipe we’re using. The MASK_MAX_RT bit determines whether the MAX_RT state is reflected on the IRQ pin or not. The EN_CRC enables CRC error detection and its default value is 1 (enabled.) You can control power to the transceiver by manipulating the PWR_UP bit. The last bit is the PRIM_RX. If set, your device is a receiver; otherwise it’s a transmitter.

Enable auto-acknowledgment registers

EN_AA register

You can enable or disable auto-acknowledgment on any of the 6 data pipes via this register. For the most fault-tolerant system design, you should enable the auto-acknowledgment on the pipes you are using.

Enable RX addresses

EN_RX_ADDR register

To enable receiving on a given pipe, set its bit in this register.

Set address width register

SETUP_AW

The width of each address across all data pipes, both receive and transmit is set via this register. The width can be configured to be from 3-5 bytes in length and it must be consistent between all devices. Longer is better.

Setup automatic retransmission

SETUP_RETR

In this register you can set up how many times to retry transmission after an initial failure. The number of tries is setup in the lower 4 ARC bits and can therefore range from 0x00 to 0x0F. If automatic retransmission is enabled, the upper three bits specify the delay in microseconds. Each unit from 0-15 increases the delay by 250 µs.

OBSERVE_TX register

OBSERVE_TX

The register is a sort of quality-control register. The upper 4 bits count the number of lost packets and is reset by writing to the the RF_CH register. The lower 4 ARC_CNT bits provide a count of the number of retransmissions. It is reset with each new packet.

Received power detector registers

RPD

The RPD register was previously called the CD (carrier detect) register on the nRF24L01. Only the lower bit is relevant. It triggers to 1 if the received power is above -64 dBm currently receiving, or zero if less than -64 dBm.

Receive address registers

The receive address registers occupy the memory offsets from 0x0A to 0x0F for data pipes 0-5. They are known by the RX_ADDR_Px where x is 0-5. Note that RX_ADDR_P0 is 40 bits wide with a reset value 0xE7E7E7E7E7. RX_ADDR_P is also 40 bits wide but has a reset value of 0xC2C2C2C2C2. The remaining addresses are only a single byte register because they must differ from the base address only by the LSB, the one that is stored.

Transmit address register

Register TX_ADDR occupies memory address 0x10 and is used only on a primary transmitter PTX device. If you want to use auto-ack, this address must be the same as RX_ADDR_P0.

Receive channel payload widths

Receive channel payload width

Each of the six data pipes can have its own payload width. The registers that specify these widths all follow the same format as the RX_PW_P0 register depicted about. The occupy memory slots 0x11 to 0x16. Only the lower 6 bits are used and therefore can express numbers from 1-32.

FIFO status register

FIFO_STATUS register

The FIFO status register at 0x17 reports the status of the FIFO receive and transmit stacks along with related information. The TX_REUSE flag is set when a transmit payload has been reused by pulsing the CE high and using the REUSE_TX_PL command. The rest of the bits relate to the current state (empty or full) of the receive and transmit payload stacks.

Status register

STATUS register

We see a lot of the status register because it gets returned to us, remember, when we clock in a command, whether we ask for it or not. The RX_DR bit is set new data arrives in the receive stack. You can clear this flag by writing a 1 to it. Similarly, the TX_DR bit is set when a packet gets transmitted. If you have enabled AUTO_ACK then this bit gets flipped only when you receive and ACK signal. The MAX_RT flag is set when the maximum number of retransmit retries had been reached. If it gets set, you must reset this flag manually by writing 1 to it. Otherwise you cannot go on transmitting. RX_P_NO these bits reflect the number of the data pipe that has data in the receive FIFO. Finally, the TX_FULL flag is set when the transmit FIFO is full.

nRF24L01+ registers

Some registers are unique to the newer nRF24L01+ device.

DYNPD enable dynamic payload length

The DYNPD register at memory offset 0x1C allows you to enable dynamic payload length on specific data pipes. We will be using fixed payload length in our example.

FEATURE register

The FEATURE register allows you to set features related to dynamic payload length. We’ll leave this to a later discussion since we won’t use this in our example.

Are you finally ready to start connecting everything?

Setting up the devices

Attach the transceiver breakout board to the Arduino Nano in the following fashion:

  • Vcc to 3.3v (not 5v!)
  • GND to ground, shared by Nano
  • CE to D9
  • CSN to D10
  • SCK to D13
  • MOSI to D11
  • MISO to D12

Note that the GettingStarted example code for the RF24 library specifies pin D7 for CE and pin D8 for CSN. Since mine were connected different, you’ll have to modify the GettingStarted example sketch accordingly.

Wire up your Nano to the transceiver breakout board as above. Modify the GettingStarted sketch so that the line RF24 radio(7,8); reads RF24 radio(9,10); instead, corresponding to our wiring differences. Also insure that the transmit power is set at minimum since the antennas are going to inches apart on the breadboard. Just ensure that the line radio.setPALevel(RF24_PA_MIN); is present in the setup() function. Now just load this sketch onto the first Nano. Then do the same with the second Nano. Using the serial monitor you will designate the first Nano as the transmitter by entering T. For the second Nano, it will default to primary receive mode.

If you’ve hooked up everything correctly, you should be seeing the Nano’s pass data back and forth.

References

Using the Raspberry Pi to communicate over the I2C bus using C

I recently wrote about using the excellent bcm2835 library to communicate with peripheral devices over the SPI bus using C. In this post, I’ll talk about using the same library to communicate over the I2C bus. Nothing particularly fancy, but you’ll need to pay careful attention to the datasheet of the device we’re using. TheTSL2561 is a sophisticated little light sensor that has a very high dynamic range and is available on a breakout board from Adafruit. I’m not going to delve into the hookup of this device as you can take a look at the Adafruit tutorial for that. Note that we’re not going to use their library. (Well, I borrowed a bunch of their #define statements for device constants.)

TSL2561 functions

The {% asset_link TSL2561.pdf “TSL2561” %} has two analog-digitial (ADC) channels. Channel 0 responds to broad spectrum visible and IR wavelengths, whereas channel 1 responds to IR only. For most applications, you’ll address channel 0.

TSL2561 I2C interface

The {% asset_link TSL2561.pdf “TSL2561 datasheet” %} is a little confusing because the device family also uses the SMBus and the format differences get lost between the text and the figures. The bottom line with the TSL2561 is that if you want to read a register, you write to the COMMAND register, then read a byte. It’s important to understand how the COMMAND register is configured so that you can read and write to the appropriate registers. Here is the COMMAND register format:

Command register format

Note that the CMD bit (7) must always be set. For ordinary read/write operations, we’ll leave the CLEAR, WORD, and BLOCK bits unset. The remaining 3:0 ADDRESS bits specify the register that we are addressing. The registers are found in Table 2, reproduced below:

TLS2561 registers

Editorial note: don’t be tempted to figure out the bits and encode the command yourself. Always use symbolic references for bit positions. By using symbolic references to bit positions and register addresses you will make your code much more readable. If you configure the COMMAND register as 0x8A, then I have convert the hex to binary and refer back to the datasheet to understand what you’re trying to do. On the other hand, if you configure the command as TSL2561_COMMAND_BIT | TSL2561_REGISTER_ID then I can immediately see you are addressing the ID register.

Sample code

I will go through a working example section by section and provide a github link at the end where you can grab the entire code.

char buf[3];
uint8_t err;

printf("Running ... \n");

if (!bcm2835_init())
{
  printf("bcm2835_init failed. Are you running as root??\n");
  return 1;
}

 if (!bcm2835_i2c_begin())
 {
    printf("bcm2835_i2c_begin failed. Are you running as root??\n");
    return 1;
 }

In our main function, we begin by declaring variables we’ll need later and call two important functions on the bcm2835 library: bcm2835_init() and bcm2835_i2c_begin(). The former sets up our library and from the documentation:

Initialises the library by opening /dev/mem (if you are root) or /dev/gpiomem (if you are not) and getting pointers to the internal memory for BCM 2835 device registers. You must call this (successfully) before calling any other functions in this library (except bcm2835_set_debug). If bcm2835_init() fails by returning 0, calling any other function may result in crashes or other failures. If bcm2835_init() succeeds but you are not running as root, then only gpio operations are permitted, and calling any other functions may result in crashes or other failures.

The latter starts I2C operations by forcing P1-03 (SDA) and P1-05 (SCL) to their alternate function ALT0 thereby enabling them for I2C use. After all I2C operations are done, the program should call bcm2835_i2c_end() to return those pins to their regular functions. Note that for the purposes of this demonstration, I check all of the return codes and printf an informative messages. In a robust application we would want to deal with this in a more fault-tolerant way.

Next we’ll set up some features of the bus:

bcm2835_i2c_setSlaveAddress(TSL2561_ADDR_FLOAT);
bcm2835_i2c_setClockDivider(BCM2835_I2C_CLOCK_DIVIDER_150);

After that, we ready to work with the device. Let’s begin with a simple reading of the ID register. To simplify matters, we’ll create a reusable function readRegister():

uint8_t readRegister(uint8_t reg, uint8_t *fail) {
	uint8_t b[2];
	b[0] = TSL2561_COMMAND_BIT | reg;
	int err = bcm2835_i2c_write(b,1);
	if( err != BCM2835_I2C_REASON_OK ) {
		printf("Unable to write command register %02x\n",err);
		*fail = 1; return 1;
	}
	err = bcm2835_i2c_read(b,1);
	if( err != BCM2835_I2C_REASON_OK ) {
		printf("Unable to read last command response %02x\n",err);
		*fail = 1; return 1;
	}
	*fail = 0;
	return b[0];
}

When we want to read a register, we just need to pass the address of the register and a pointer to a uint8_t in which we’ll return the status (0 for success and 1 for failure.) Why don’t we just return a status? It’s becuase we’re already returning the results of the read. When the caller passes the address of a status variable, we can fill it, and the caller just looks at it afterwards.

In lines 2-3, we are building the COMMAND “register” value to send. Because the datasheet says to set the CMD bit, we do that. Then we logical OR the address into bits 3:0. Then we write the COMMAND register to the device and read a byte. Remember that we’ve already set the hardware address previously.

So calling readRegister() to read the hardware ID will look like:

//	Read the ID register

uint8_t id = readRegister(TSL2561_REGISTER_ID, &err);
if( err == 1) {
	printf("Check ID register failed.\n"); return 1;
}
printf("The ID is %02x.\n",id);

We can do something similar to read another register, such as the TIMING register 0x01h:

//	Read the timing register

uint8_t tr = readRegister(TSL2561_REGISTER_TIMING,&err);
if(err == 1) {
	printf("Check timing register failed.\n");
	return 1;
}
printf("The timing register is %02x.\n",tr);

On my device I get a value of 0x03 which is the default power-up value according to the datasheet.

Now we need to get down to the business of writing to a register. Since we have to explicitly turn on the ADC, we’ll have to write to a control register. A generic writeRegister() should help with this. Again our design uses a pointer to a uint8_t to return the status. We don’t have to do this because a write operation has no useful return, but for API symmetry, I wrote the function the same way.

void writeRegister(uint8_t reg, uint8_t val, uint8_t *fail) {
	uint8_t b[2];
	b[0] = TSL2561_COMMAND_BIT | reg;
	int err = bcm2835_i2c_write(b,1);
	if( err != BCM2835_I2C_REASON_OK ) {
		printf("Unable to write command register %02x\n",err);
		*fail = 1; return;
	}
	b[0] = val;
	err = bcm2835_i2c_write(b,1);
	if( err != BCM2835_I2C_REASON_OK ) {
		printf("Unable to write command register %02x\n",err);
		*fail = 1; return;
	}
	err = bcm2835_i2c_read(b,1);
	if( err != BCM2835_I2C_REASON_OK ) {
		printf("Unable to read following write command register %02x\n",err);
		*fail = 1; return;
	}
	*fail = 0;
	return;
}

Writing to a register is similar to reading except that after addressing the register, we have to send it some data in a subsequent write operation. Following those two operations, we have an obligatory read and move on.

Lines 3-9 address the COMMAND register as we did before. Lines 9-14 write the caller’s specified value to the address specified in the preceding COMMAND call. Then a read that we can disregard and return to the caller.

Turn on the ADC

Turning on the ADC couldn’t be easier; we just need to address the CONTROL register 0x00. The CONTROL register documentation tells us that we simply need to set the POWER bits (1:0) to 0x03 to power up the device or 0x00 to power it down.

Control register

Doing that in code using our generic write function couldn’t be simpler:

writeRegister(TSL2561_REGISTER_TIMING,TSL2561_CONTROL_POWERON, &err );
if( err == 1 ) {
	printf("Unable to power on the TSL2561.\n"); return 1;
}

Take a broad spectrum reading on Channel 0

Now we come to the reason we started working with the device, to take a light measurement. We’re going to focus on the visible + IR channel (Channel 0) but the same principles apply to either channel. We’re just going to do sequential reads from the two channel 0 registers and assemble the result:

uint8_t LSB0 = readRegister(TSL2561_REGISTER_CHAN0_LOW, &err);
if( err == 1 ) {
	printf("Unable to read LSB0\n"); return 1;
}
uint8_t MSB0 = readRegister(TSL2561_REGISTER_CHAN0_HIGH, &err);
if( err == 1 ) {
	printf("Unable to read MSB0\n"); return 1;
}
int lux = (int)(MSB0 << 8) | (int)LSB0;
printf("Light value is %d lux.\n",lux);

There’s a lot more that we could cover, both about the operation of the device and about using I2C on the Raspberry Pi in general, but this should be enough to get you started with luminosity measurement using the TSL2561 or in beginning to code your own I2C interfaces using the BCM2835 library on the Raspberry Pi.

References

Implementing ADC using Raspberry Pi and MCP3008

Several years ago I wrote about adding analog-to-digital capabilities to the Raspberry Pi. At that time, I used an ATtinyx61 series MCU to provide ADC capabilities, communicating with the RPi via an I2C interface. In retrospect it was much more complicated than necessary. What follows is an attempt to re-do that project using an MCP3008, a 10 bit ADC that communicates on the SPI bus.

MCP3008 device

The MCP3008 is an 8-channel 10-bit ADC with an SPI interface^[Datasheet can be found here.]. It has a 4 channel cousin, the MCP3004 that has similar operating characteristics. The device is capable of performing single-ended or differential measurements. For the purposes of this write-up, we’ll only concern ourselves with single-ended measurement. A few pertinent details about the MCP3008:

2018: Experiment No. 1

2018 is my year of experiments (Why? TL;DR: New Year’s resolutions are over-rated and have a high failure rate. Anyone can run an experiment for a month.) My first experiment (No news for a month) is nearly done and I’ll declare it a success.

Background

The round-the-clock sensational news cycle exists in large part to create wealth for the already-too-wealthy. Little of it is actionable, leaving us at the same time both outraged and impotent. Mostly I decided to give up on the news because of Donald Trump, the demented psychopathic moron who managed to get elected president.^[I use these terms very carefully. Many have speculated that he suffers from some form of dementia owing to events where he slurs his words and perseverates. His sociopathic or psychopathic behaviours are well-documented; he is man devoid of empathy. And finally, his lack of reading is well-known. For all I can tell, the man is a functional illiterate. In contrast, his predecessor is a bibliophile and read widely and voraciously throughout his tenure.] Since Trump took office, like others, I’ve found myself cycling repeatedly through the stages of grief. But mostly I’ve been stuck on anger. There’s something about willful ignorance that does that to me.

2018: A year of experiments

New Year’s resolution time is at hand. But not for me; at least not in a traditional sense. I was inspired by David Cain’s experiments. In short, he conducts monthly experiments in self-improvement.

The idea of an experiment is appealing in ways that a resolution is not. A resolution presumes an outcome and relies only on the long application of will to see it through. An experiment on the other hand, makes only a conjecture about the outcome and can be conducted for a shorter period.

Peering into Anki using R

Yet another diversion to keep me from focusing on actually using Anki to learn Russian. I stumbled on the R programming language, a language that focuses on statistical analysis.

Here’s a couple snippets that begin to scratch the surface of what’s possible. Important caveat: I’m an R novice at best. There are probably much better ways of doing some of this…

Counting notes with a particular model type

Here we’ll use R to do what we did previously with Python.

Language word frequencies

Since one of the cornerstones of my approach to learning the Russian language has been to track how many words I’ve learned and their frequencies, I was intrigued by reading the following statistics today:

  • The 15 most frequent words in the language account for 25% of all the words in typical texts.
  • The first 100 words account for 60% of the words appearing in texts.
  • 97% of the words one encounters in a ordinary text will be among the first 4000 most frequent words.

In other words, if you learn the first 4000 words of a language, you’ll be able to understand nearly everything.

Anki database adventures: Counting notes by model type

Continuing my series on accessing the Anki database outside of the Anki application environment, here’s a piece on accessing the note type model. You may wish to start here with the first article on accessing the Anki database. This is geared toward mac OS. (If you’re not on mac OS, then start here instead.)

The note type model

Since notes contain flexible fields in Anki, the model for a note type is in JSON. The best guess definition of the JSON is: