“This tutorial was extracted from Erich Styger blog http://mcuoneclipse.wordpress.com with his agreement.”
For some time I’m using the Nordic Semiconductor nRF24L01+ transceiver successfully in many projects (see “Tutorial: Ultra Low Cost 2.4 GHz Wireless Transceiver with the FRDM Board“). Since that tutorial things evolved a lot with the introduced RNet Stack. To honor the popularity of the Nordic Semiconductor nRF24L01+, Freescale has put a socket on the FRDM-K64F board. So time to make a new step-by-step tutorial how to use the nRF24L01+ with the FRDM-K64F.
Two FRDM-K64F Boards with nRF24L01+ Transceiver
I apologize that it took so long until I have found the week-end and night hours to write this up: I had the transceivers and boards quickly working after I have received the boards back in April this year. But writing a tutorial takes much more time than what I needed to write the application itself. Probably I’m doing that kind of software+hardware too many times, so it does not sound like a big deal for me. But I have received several requests to help them combining the Freescale FRDM-K64F board with the Nordic Semiconductor nRF24L01+ transceiver, so here we go…..
Outline
In this tutorial, I show the steps how to use the nRF24L01+ transceiver and RNet stack with the FRDM-K64F board.To keep things simple, the board will send periodically a PING message to another board. I’m using in this tutorial Eclipse Kepler with Processor Expert, but any other Eclipse based tool with Processor Expert (e.g. Kinetis Design Studio, IAR or Keil) can be used. Make sure you have loaded my latest Processor Expert components from GitHub, see “Processor Expert Component *.PEupd Files on GitHub“. In this tutorial I assume you have basic Eclipse and Processor Expert knowledge.
Hardware
You need a board (FRDM-K64F), a transceiver (nRF24L01+, you find many sources on the internet, I have mine from here: http://yourduino.com/sunshop2/index.php?l=product_detail&p=188), headers and basic soldering skills . And you need this twice to communicate between two boards.
FRDM-K64F with nRF24L01+ Transceiver and Headers
Solder the header to the board:
Headers soldered
Then put the module on the header:
Check and verify the pin out of the module with the pins on the FRDM-K64F board. The pin mapping for the RF module are on the back of the FRDM-K64F board.
nRF24L01+ Module on Header
Creating Project
In Eclipse, create a new project for the FRDM-K64F for the MK64FN1M0VLL12 device on the board. Make sure you have the project with Processor Expert enabled. With this, I get a basic (empty) project for the FRDM-K64F:
Project for FRDM-K64F
FreeRTOS Component
I’m going to use the RNet stack for my communication, and this stack uses the FreeRTOS queues for message buffering. While it would be possible to use any kind of buffers (ring buffer, etc), I keep using FreeRTOS in this tutorial, as it simplifies things a lot, and I can use it later for a more complex application.
So I’m adding the FreeRTOS Processor Expert component to the project. It will automatically add the Utility component too:
FreeRTOS added
Floating Point Settings in FreeRTOS for K64F
FreeRTOS Heap Size
Compiler Settings
In the project settings, I need to verify that the settings are matching a Cortex-M4 with hardware floating point unit (fpv4-sp-d16):
Floating Point Settings in GNU ARM
CPU Clocks
Next I’m configuring the CPU clocks. I just configure a rather high clock, so I will have a high clock for the SPI used by the nRF24L01+ transceiver. To reach an even higher CPU clock, see “FRDM-K64F at Maximum Speed of 120 MHz“. In this tutorial I’m using the 32 kHz RTC clock as reference. For this I enable the RTC clock in FEI mode:
K64F Clock Settings with RTC
The FLL clock gets configured to 96 MHz (or close to this )
FLL set to 96 MHz
System Clocks Configured
The nRF24L01+ uses an SPI interface to the microcontroller. I can use it two ways:
- Hardware SPI: in this case the microcontroller hardware SPI peripheral is used. This is the preferred way, as the hardware does all the bit shifting and clocking. But in that case the transceiver module needs to be connected to hardware SPI pins on the microcontroller.
- Software SPI: here normal general purpose (GPIO) pins are used. This is called ‘bit banging’, and the software does all the shifting and clocking. This takes a lot of CPU cycles, but the last resort e.g. if no hardware SPI is available.
On the FRDM-K64F board, luckily the pins are connected to the hardware SPI, so I add the SynchroMaster (Hardware SPI) component to the project:
SynchroMaster SPI Component Added
The component shows errors, as it is not configured yet. According to the FRDM-K64F schematics, I need to assign the following pins for the SPI1 channel:
- Interrupts enabled
- MISO: PTD7
- MOSI: PTD6
- Clock: PTD5
- Slave Select pin disabled (the nRF24L01+ driver will do this)
- Clock Speed: the nRF24L01+ allows a maximum clock speed of 8 MHz.
- The Clock edge needs to be set to *falling edge*. This will set the SPI clock and data to ‘data is valid on clock leading/rising edge (CPHA=0).
- The Shift clock idle polarity is set to *Low* (CPOL=0)
- MSB (Most Significant Bit) first (MSB needs to be shifted first)
Hardware SPI Settings for nRF24L01+
Software SPI
You *could* use the Software SPI too (you will need to select it in the RNet component, which comes next). In case you have no hardware SPI, then add the GenericSWSPI component to your project:
GenericSWSPI Component
You need to configure the clock, input (MISO) and output (MOSI) pins to the pins on your board. The picture below shows the component settings:
- Clock edge: Falling edge
- Clock idle polarity: low
- Output pin idle polarity: high
- MSB first set to yes
Software SPI Settings for nRF24L01+
RNet Component
Next I’m adding the RNet Wireless stack component:
RNet Component Added
nRF24L01+ Transceiver Component
I configure the RNet component to use the nRF24L01+ transceiver and add a new component for it in the drop down box of the component:
Configuring RNet Component
The RNet has other settings which we keep by default. Be free to change the settings once you have working version. You can configure the radio channel or the data rate. Both can be changed at runtime with methods provided by the component, so you can implement easily a channel hopping. The node network addresses are 8bit by default, so you can have up to 256 devices on a channel which should be enough for most cases. The address 0xFF (or 0xFFFF for 16bit addresses) are broadcast addresses.The queues are used to buffer incoming or outgoing messages. For this FreeRTOS queues are used: Using more queues will consume more memory. Each element in the queue will hold a full message, which is in our case 32 bytes plus one overhead byte (so 33 bytes). The blocking time specifies how long the task shall wait if the queue is full.Finally, a retry count can be specified. If greater than zero, it will retry to send the message if no acknowlege is received.
SPI Block Transfer
It asks me which SPI bus it shall us, and I can select the SPI component (SM1) I have configured in the previous step:
Using SPI for component
However, the RNet stack requests block send and receive methods on the SPI bus, and this is not provided by default. Looking at the description oft the method it tells me that I need to use buffers in the SPI component:
Block Send and Receive Needed
So I enable the buffers for SPI. The nRF24L01+ has a maximum payload of 32 bytes, so with the overhead I’m using 48 bytes each:
SPI Input and Output Buffers
Now the SPI component is happy again.
CE and CSN Pins
But I need to assign the CE and CSN pins of the nRF24L01 component:
Pins for nRF24L01+
According to the schematics, CE is connected to PTC12 and CSN is connected to PTD4:
CE Configured
CSN Configured
Interrupt Pin
There is one pin missing: the interrupt pin of the nRF24L01+ module! So I enable the interrupt pin in the nRF24L01+ component:
Radio IRQ Pin Enabled
The IRQ pin is routed to PTC18, triggering on falling edge:
Interrupt Pin of nRF24L01+ ConfiguredThis completes the hardware part for the nRF24L01+
LEDs
Because I want to show the transceiver activity with LEDs’, I’m adding three LED components for the RGB LED of the board, configured to PTB22, PTE26 and PTB21:
LED Components Added
Software Files
I’m going to add three source files to my project:
Software Stack Files
- RNet_App.c is my application file.
- RNet_App.h is my interface for the above file.
- RNet_AppConfig.h is my stack application configuration file.
I discuss below the most important parts of each file. For the complete source files, see the project link at the end of the article.
RNet_AppConfig.h
In this header file I can configure the stack settings (I keep the defaults). And I should declare here the content of RAPP_MSG_Type as this will be my messages types. So for this tutorial I’m defining a PING message with numerical ID 0×55:
/** * \file * \brief This is a configuration file for the RNet stack * \author (c) 2014 Erich Styger, http://mcuoneclipse.com/ * \note MIT License (http://opensource.org/licenses/mit-license.html) * * Here the stack can be configured using macros. */ #ifndef __RNET_APP_CONFIG__ #define __RNET_APP_CONFIG__ /*! type ID's for application messages */ typedef enum { RAPP_MSG_TYPE_PING = 0x55, } RAPP_MSG_Type; #endif /* __RNET_APP_CONFIG__ */
RNet_App.h
RNet_App.h is the interface to RNet_App.c. All what I need for now is just to call the Init() method which will initialize everything for me:
/** * \file * \brief This is the interface to the application entry point. * \author (c) 2014 Erich Styger, http://mcuoneclipse.com/ * \note MIT License (http://opensource.org/licenses/mit-license.html) */ #ifndef RNETAPP_H_ #define RNETAPP_H_ /*! \brief Driver initialization */ void RNETA_Init(void); #endif /* RNETAPP_H_ */
RNet_App.c
The functionality of the application and stack is in RNet_App.c.The Init() method initializes the RNet Stack with RSTACK_Init(), assigns a message handler (RAPP_SetMessageHandlerTable(), more about this later) and creates an RTOS task. The RTOS scheduler itself will be started in main().
void RNETA_Init(void) { RSTACK_Init(); /* initialize stack */ if (RAPP_SetMessageHandlerTable(handlerTable)!=ERR_OK) { /* assign application message handler */ for(;;); /* "ERR: failed setting message handler!" */ } if (FRTOS1_xTaskCreate( RNetTask, /* pointer to the task */ "RNet", /* task name for kernel awareness debugging */ configMINIMAL_STACK_SIZE, /* task stack size */ (void*)NULL, /* optional task startup argument */ tskIDLE_PRIORITY, /* initial priority */ (xTaskHandle*)NULL /* optional task handle to create */ ) != pdPASS) { /*lint -e527 */ for(;;){}; /* error! probably out of memory */ /*lint +e527 */ } }
Radio Application Task
In this tutorial, the application runs in a single task: RNetTask(). I suggest to use one task just to deal with the radio transceiver, and add extra tasks for anything your application is doing otherwise. And make sure that the task is processing the state machine frequently enough. How much is ‘frequently engough’ depens on the amount of messages and buffer (queue) sizes. I recommend something of at least 20 Hz or more.
This is my RNetTask:
staticportTASK_FUNCTION(RNetTask, pvParameters) { uint32_t cntr; uint8_t msgCntr; (void)pvParameters; /* not used */ if (RAPP_SetThisNodeAddr(RNWK_ADDR_BROADCAST)!=ERR_OK) { /* set a default address */ for(;;); /* "ERR: Failed setting node address" */ } cntr = 0; /* initialize LED counter */ msgCntr = 0; /* initialize message counter */ appState = RNETA_INITIAL; /* initialize state machine state */ for(;;) { Process(); /* process state machine */ cntr++; if (cntr==100) { /* with an RTOS 10 ms/100 Hz tick rate, this is every second */ LED3_On(); /* blink blue LED for 20 ms */ RAPP_SendPayloadDataBlock(&msgCntr, sizeof(msgCntr), RAPP_MSG_TYPE_PING, RNWK_ADDR_BROADCAST, RPHY_PACKET_FLAGS_NONE); msgCntr++; cntr = 0; FRTOS1_vTaskDelay(20/portTICK_RATE_MS); LED3_Off(); /* blink blue LED */ } FRTOS1_vTaskDelay(10/portTICK_RATE_MS); } /* for */ }
First, it assigns a node address for itself. I assign here the broadcast address as default. Then I initialize the counter variables and the state machine state (appState).In the task loop I call Process(): this processes my state machine (more later). I’m using a counter to send every second a message, and I show this with a 20 ms blue LED blink.Sending date is done with RAPP_SendPayloadDataBlock():
Here I send a ‘PING’ message with an 8bit message counter: 0, 1, 2, 3, 4, 5, and so on.
RAPP_SendPayloadDataBlock(&msgCntr, sizeof(msgCntr), RAPP_MSG_TYPE_PING, RNWK_ADDR_BROADCAST, RPHY_PACKET_FLAGS_NONE);
The first argument is the pointer to the message data (&msgCntr), followed by the size of the buffer (sizeof(msgCntr)). RAPP_MSG_TYPE_PING is the type of the message. RNWK_ADDR_BROADCAST is the destination address (I send it to every node in the network). The last argument are special flags, but I do not need any here, so I use RPHY_PACKET_FLAGS_NONE.
State Machine
The task processes a state machine, which has the following states:
typedef enum { RNETA_INITIAL, /* initialization state */ RNETA_POWERUP, /* powered up the transceiver */ RNETA_TX_RX /* ready to send and receive data */ } RNETA_State;
The corresponding state machine is very simple:
static void Process(void) { for(;;) { switch(appState) { case RNETA_INITIAL: appState = RNETA_POWERUP; continue; case RNETA_POWERUP: RadioPowerUp(); appState = RNETA_TX_RX; break; case RNETA_TX_RX: (void)RADIO_Process(); break; default: break; } /* switch */ break; /* break for loop */ } /* for */ }
From the initial state, it powers up the transceiver. Once the transceiver is powered, it its ready to send and receive data and processes the Radio messages. This is a very basic state machine: I use more complex ones for channel hopping, dynamically powering down the transceiver to save energy and so on. But for this tutorial I want to keep things simple.
Transceveiver Power-Up
If you read the nRF24L01+ data sheet carefully, you will notice that it needs 100 ms after power is applied until it is ready. I have seen some transceivers which needed even 120 or 130 ms, so I decided to wait 150 ms for all. Accessing the transceiver too early will create strange effects (it took me a while to realize that :-( ). To wait the needed time, I use the following routine called from the above state machine:
static void RadioPowerUp(void) { /* need to ensure that we wait at least 100 ms (I use 150 ms here) after power-on of the transceiver */ portTickType xTime; xTime = FRTOS1_xTaskGetTickCount(); if (xTime<(150/portTICK_RATE_MS)) { /* not powered for 100 ms: wait until we can access the radio transceiver */ xTime = (150/portTICK_RATE_MS)-xTime; /* remaining ticks to wait */ FRTOS1_vTaskDelay(xTime); } (void)RADIO_PowerUp(); /* enable the transceiver */ }
As this is called in the context of the RTOS, I can use the current tick counter of the RTOS to find out if 150 ms have been passed. If not, I simply wait for the needed ticks, and then I call RADIO_PowerUp() which will access the transceiver and initialize it from power-up.
Message Handler
So far I can send messages, but how to receive a message? For this I’m using a message handler table which has a NULL pointer at the end, so I can add more handlers as needed before the NULL sentinel:
static const RAPP_MsgHandler handlerTable[] = { RNETA_HandleRxMessage, NULL /* sentinel */ };
The RNet stack is informed about that table with the code we have seen earlier in RNETA_Init():
if (RAPP_SetMessageHandlerTable(handlerTable)!=ERR_OK) { /* assign application message handler */ for(;;); /* "ERR: failed setting message handler!" */ }
Whenever the low-level radio driver receives a message, it is passed up the RNet stack, until it reaches that message handler table. Then each handler can inspect the message and process it.
For our tutorial application, I have implemented the handler as below:
static uint8_t RNETA_HandleRxMessage(RAPP_MSG_Type type, uint8_t size, uint8_t *data, RNWK_ShortAddrType srcAddr, bool *handled, RPHY_PacketDesc *packet) { (void)srcAddr; (void)packet; switch(type) { case RAPP_MSG_TYPE_PING: /* <type><size><data */ *handled = TRUE; /* to be defined: do something with the ping, e.g blink a LED */ LED2_On(); /* green LED blink */ FRTOS1_vTaskDelay(20/portTICK_RATE_MS); LED2_Off(); return ERR_OK; default: break; } /* switch */ return ERR_OK; }
Each handler receives the message type, the message size, the message payload dataand who has sent the message (srcAddr). So all the message parts have been unpacked by the lower stack layers. But if you want to use the full packet, then it is passed as a pointer too. Finally there is a handled parameter: here I tell the caller that I was able to handle the message.In the handler I check for the type (RAPP_MSG_TYPE_PING), and then I’m free to do anything with that message or message data. Here I’m simply blinking the green LED for 20 ms to show that I have received the ping message. To access the message counter I have passed in the payload, I can use the *data pointer.
main()
The last step is to call the initialization from main():
Calling from main()
That should complete all the needed steps. Generate Code (if not already done), compile and build, and this should go without errors.
Running the Application
Running the application on the boards should get you some blinking LED’s:
- Each board sends a PING message every second, indicated with a blue LED blink:
Blue LED for sending message
- Whenever a board receives a PING message, it will blink the green LED:
Green LED for received message
Summary
With this tutorial I have added nRF24L01+ Transceiver support the Freedom FRDM-K64F board. The FRDM-K64F makes it really ease: solder a header, plugin the transceiver and add Processor Expert component to a project and I can communicate with another remote board. So I can build a network of sensor nodes very easily, and connect it to the internet, and so on. While this tutorial uses the FRDM-K64F board, you can easily do the same for any other board: all what you need is to connect the SPI pins and power the module. And of course add the software. Done
This tutorial project is available on GitHub for Eclipse Kepler and the GNU ARM Eclipse.