Wireless communications is probably the most complex topic in embedded systems. When you ask a small wireless module to deal with tcp/ip, security, encryption, automatic responses, or ports you are asking for a PhD in embedded wireless. Not only are the protocols expensive as far as power, but the code complexity can make your organs bleed. That's why I need to shed some light on a super hero of the wireless world. What if I were to tell you that you could get wireless communication going and the amount of effort is 50% less and power consumption is up to 90% less. As impossible as it sounds, the solution is out there. Let's look at the module and the code from MikroElektronika that breathes it life.
Let me introduce to you my little friend, the Nordic nRF24L01+. This wireless module has been on the market for several years and offers some things that haven't been seen since. Let's look under the hood and list some specs.
- Upto 2Mbps transmission speeds
- 11.3mA TX at full power
- 12.3mA at 2Mbps RX
- 22uA in standby
- 6 data pipelines for multiple transceivers
- 5V tolerant
- But my favorite - from the Nordic website is: "Ultra low power consumption – months to years of battery lifetime"
Why aren't we using this everywhere! That is a good question and the answer is: It doesn't use the standard wireless you find in your laptop or on your home router. That can be a good or bad thing. The other argument that comes up is, "well, BluetoothLE is about the same energy consumption, why not use that?" and that is a valid argument. What counters that argument is how much the cost of licensing Bluetooth is in a product. How much is it with the Nordic chip? $0.
Looking at the Non-Standard Protocol
The protocol that the nRF24L01+ uses is a proprietary one. Developed to be simple and effective when communicating with another like transceiver. The name of the protocol is called ShockBurst.
A module is put into 1 of 2 modes, Primary Receiver or Primary Transmitter. This mean their "primary" task is for RX or TX. When in RX mode it can listen to up to 6 channels. Meaning you have a limitation of 6 other devices can be communicating with you at one time.
The addressing of the radio is not IP based, so each radio is assigned an address in the following manor:
This means that the address of pipe 0 and pipe1 are the full 4 bytes. Pipes 2 - 5 are a partial copy of pipe1 with the differentiating factor being the last byte.
Initializing the radio requires that you pass the addresses you want to use to the initialization function. Start by declaring a nrf_addr_map_t variable.
#ifdef TRANSMITTER static const nrf_addr_map_t addresses = { {0xB3, 0xB4, 0xB5, 0xB6, 0xF1}, //RX PIPE 0 {0}, //RX PIPE 1 {0}, //RX PIPE 2 {0}, //RX PIPE 3 {0}, //RX PIPE 4 {0}, //RX PIPE 5 {0xB3, 0xB4, 0xB5, 0xB6, 0xF1} //TX PIPE }; #else static const nrf_addr_map_t addresses = { { 0x78, 0x78, 0x78, 0x78, 0x78 }, //RX PIPE 0 { 0xB3, 0xB4, 0xB5, 0xB6, 0xF1 }, //RX PIPE 1 { 0xF2 }, //RX PIPE 2 { 0xF3 }, //RX PIPE 3 { 0xF4 }, //RX PIPE 4 { 0xF5 }, //RX PIPE 5 { 0 } //TX PIPE }; #endif
For a primary transmitter we need to define the pipe0 as the address it will be assigned to and also the TX Pipe will have the same address. Here is the magic secret as to why this is the case. Something you'd have to stare at the datasheet for 10 hours like myself. When a transmitter sends out a packet, the receiver of that packet will automatically switch to a transmitter and the original transmitter turns into a receiver briefly. This is because a acknowledgement packet is sent back to the transmitter. The RX unit needs to transmit it back to an address so it transmits it back to the originator. Smart.
So the limitation here is that you won't be downloading MP3 files from the internet through your router at home with this unit. What you will have is a very energy efficient and inexpensive way for small devices to communicate.
Interrupts and Timers
Like almost all radios on the market you will need an interrupt and timer. The interrupt is used to acknowledge when a packet is received, when transmit is complete and when an error occurs. The timer is used as a timeout system. If you are waiting for a communication that never arrives, you have yourself an infinite loop.
void radio_ISR() iv IVT_INT_EXTI15_10 ics ICS_AUTO { EXTI_PR |= ( 1 << PR10 ); // Clear interrupt flag /**< Max retries interrupt */ if( nrf_is_interrupted( NRF_MAX_RT ) ) lost_packets++; /**< TX data sent interrupt */ if( nrf_is_interrupted( NRF_TX_DS ) ) nrf_acknowledged(); // or user defined data sents /**< RX data received interrupt */ if( nrf_is_interrupted( NRF_RX_DR ) ) // Got some data! }
A convenience function is built into the library called nrf_is_interrupted( ) and accepts a single argument which is what type of interrupt message do you want to check: NRF_TX_DS, NRF_RX_DR, NRF_MAX_RT .
NRF_MAX_RT is triggered, that means the the radio tried the maximum times to send the packet and did not receive confirmation of reception.
NRF_TX_DS is used in 2 cases. When in primary transmitter mode, this will be triggered once the payload has been sent and will also be set when in primary receive mode when the acknowledgement packet was received.
NRF_RX_DR will be triggered in transmit mode as well when the acknowledgment packet was received or when in receive mode and a data packet is ready for reading.
The timer used is triggered every 10ms and needs to call nrf_timer_tick() . This is to keep the radio in check and not find itself in a lost loop.
void timer3_interrupt() iv IVT_INT_TIM3 { TIM3_SR.UIF = 0; nrf_timer_tick(); }
Initialization
In the MikroE library there are three modes that the radio can be initialized to: ShockBurst, Enhanced ShockBurst, Enhanced ShockBurst with Bi-directional Communication. Now don't get too excited about the bi-directional just yet. There are some limitations there. A primary receiver can transmit, but that transmit needs to be short and limited to 32 bytes.
A comparison:
Mode | Auto Ack | Dynamic Payload | Bi-Directional |
ShockBurst | |||
Enhanced | X | ||
Enhanced with Bi-Directional | X | X | X |
So why not use the bi-direction enhanced mode all the time? Someone once said with everything there is a cost. With each new feature you add complexity and power consumption. If you have a simple application that is sending data and it doesn't really matter if once in a while it drops a packet, then ShockBurst is perfect. Very low power and reliable. If you want to take advantage of auto-acknowledgement then Enhanced is for you. Mind you that it comes with a fixed packet size. Meaning that when send a packet, it is always a fixed size. The advantage here is that the packet is smaller and therefore transmission time is less. If you want the full package, then Enhanced with Bi-directional works for you.
To initialize the radio, you'll need to have your SPI bus initialized and from there you have a choice of 3 functions.
int nrf_sb_init( nrf_addr_map_t *address, nrf_operation_mode_t operational_mode ); int nrf_esb_init( nrf_addr_map_t *address, nrf_operation_mode_t operational_mode ); int nrf_esb_bidirection_init( nrf_addr_map_t *address, nrf_operation_mode_t operational_mode );
Initialize the radio in Enhanced mode as well as being a primary transmitter.
nrf_esb_init( &addresses, NRF_PTX );
Transmitting
Inside the nrf24l01.h file are the user configurable settings:
/** Defines how many retransmits that should be performed */ #define NRF_RETRANSMITS 15 /** Defines the payload length the radio should use */ #define NRF_PAYLOAD_LENGTH 4 /** Defines the channel the radio should operate on*/ #define NRF_CHANNEL 40
- Max retries are 15
- Max payload is 32
- Max number of channels 125
uint8_t nrf_send_data( uint8_t *address, uint8_t *data_out , uint8_t count);
Sending data is simple. Under normal situations the *address argument will be 0 , if you wanted to change the address temporarily to send to another radio, the library allows this. The *data_out is a pointer to the data you want to send and count is how many bytes you want to send. The function returns a -1 on error and a 0 on success.
Receiving
uint8_t nrf_recieve_data( nrf_address_t *address, uint8_t *data_in );
When the interrupt NRF_RX_DR is triggered, you can call the nrf_recieve_data function. It accepts a pointer to a new nrf_address_t *address argument. THIS IS NOT THE ADDRESSES THAT YOU INITIALIZED THE RADIO WITH. The purpose of this is an address of where the data is coming from. Finally you will need to provide a pointer to the data_in buffer. This needs to be an array that is equal in size to your max payload size. In the above example the payload length is set to 4. So your array needs to be at least this size.
Extras
The library is comprehensive and gives you access to the full range of power in the radio. Some of the things you can do:
- Turn interrupts on / off
- Change the CRC mode
- Change power output
- Change the data rate
- Change the clock
- Fifo operations / full / empty / flush
Summary
I'm biased and love the radio. Just a few years early of the IoT movement, however it isn't too late. For those MCUs that have low power requirements and can use a simple radio to get their message out, this is a great solution.
Library can be found at:
https://github.com/MikroElektronika/Click_nRF_RF24L01
You can also try our examples from Libstock.