RNet: A Simple Open Source Radio Network Stack

I was searching the internet for an open source network stack for my nRF24L01+ transceivers. But these stacks were either too heavy or had a restrictive or not really non-open source license behind it. I was very reluctant to start with something I think already should exist. Two weeks ago I decided that I just do it from scratch, and here I am: I have the basics working 🙂

Two FRDM-KL25Z with nRF24L01+ Transceivers

Two FRDM-KL25Z with nRF24L01+ Transceivers

Requirements

So here is what I defined as my needs:

  1. Open source, with no strings attached.
  2. Support for multiple transceivers. Initially designed for the Nordic Semiconductor nRF24L01+, but it shall work with the Freescale MC1320x too.
  3. Optional command line Shell interface.
  4. Using a simple layered architecture: Radio interface, PHY, MAC, NWK and APP layers.
  5. Optimized for small networks (8bit or 16bit node addresses).
  6. Acknowledge message handling on higher stack levels.
  7. Working with FreeRTOS operating system.
  8. Simple and easy to maintain, at least for myself 🙂

💡 I’m using FreeRTOS in the stack because it was easy and convenient to implement the stack with it: the RTOS provides services like queues and message processing. Clearly, an RTOS is not a must, but makes things easier. Once I find time, I will update the stack so it works in bare metal (no RTOS) mode as well.

Files on GitHub

For sure the stack is not perfect yet, and I will continuously improve it. Here is what is available:

  1. Demo project for the FRDM-KL25Z is available on GitHub.
  2. The network stack files are on GitHub in the RNet folder.

Low Level Radio Transceiver Driver with Message Queues

Currently the Freescale MC1320x and Nordic Semiconductor nRF24L01+ are supported. The drivers are in Radio.c and Radio.h in

  1. RNet\SMAC for the Freescale MC1320x
  2. RNet\nRF24 for the Nordic Semiconductor nRF24L01+

Use one or the other in your application. Each driver implements a local task RadioTask() which processes the transceiver state machine and handles incoming and outgoing messages. The radio task uses the RMSG.c to queue incoming and outgoing messages. FreeRTOS message queues are used to store the data.

The following code shows the radio task for the MC1320x transceiver:

static portTASK_FUNCTION(RadioTask, pvParameters) {
  (void)pvParameters; /* not used */
  /* Initialize Rx/Tx descriptor */
  radioRx.data = &radioRxBuf[0];
  radioRx.dataSize = sizeof(radioRxBuf);
  radioTx.data = &radioTxBuf[0];
  radioTx.dataSize = sizeof(radioTxBuf);
  for(;;) {
    if (RADIO_isOn) { /* radio turned on? */
      RADIO_HandleStateMachine(); /* process state machine */
     (void)RADIO_ProcessTx(&radioTx); /* send outgoing packets (if any) */
     if (RPHY_ProcessRx(&radioRx)==ERR_OK) { /* process incoming packets */
        if (radioRx.flags&RPHY_PACKET_FLAGS_ACK) { /* it was an ack! */
          EVNT_SetEvent(EVNT_RADIO_ACK); /* set event */
        }
      }
    }
    FRTOS1_vTaskDelay(10/portTICK_RATE_MS);
  }
}

RPHY Incoming Messages

RPHY.c and RPHY.h implements the functionality to deal with the physical (raw) data packets. All what it does is to send and receive packets to the message buffer which then are processed by the radio transceiver driver.

RPHY_ProcessRx() gets called to process incoming messages:

uint8_t RPHY_ProcessRx(RPHY_PacketDesc *packet) {
  uint8_t res;

  res = RPHY_GetPayload(packet->data, packet->dataSize); /* get message */
  if (res!=ERR_OK) { /* failed or no message available? */
    return res; /* return error code */
  }
  packet->flags = RPHY_PACKET_FLAGS_NONE; /* initialize packet flags */
  return RMAC_OnPacketRx(packet); /* pass packet up the stack */
}

RMAC and RNWK

RMAC.c and RNWK.c implement the MAC (Media Access) and Network layers. The MAC layer uses a sequence number for each message to track the ACK (acknowledge) of a message. A successful acknowledge message has following conditions:

  1. The message is of type RMAC_MSG_TYPE_ACK
  2. The sequence number matches the previous data message
  3. The destination address of the packet matches the node

Sending the ACK message and checking for it is done in RNWK_OnPacketRx():

uint8_t RNWK_OnPacketRx(RPHY_PacketDesc *packet) {
  RNWK_ShortAddrType addr;
  RMAC_MsgType type;
  uint8_t res;

  addr = RNWK_BUF_GET_DST_ADDR(packet->data);
  if (addr==RNWK_ADDR_BROADCAST || addr==RNWK_GetThisNodeAddr()) { /* it is for me :-) */
    type = RMAC_GetType(packet->data, packet->dataSize); /* get the type of the message */
    if (type==RMAC_MSG_TYPE_ACK && RMAC_IsExpectedACK(packet->data, packet->dataSize)) {
      /* it is an ACK, and the sequence number matches. Mark it with a flag and return, as no need for further processing */
      packet->flags |= RPHY_PACKET_FLAGS_ACK;
      return ERR_OK; /* no need to process the packet further */
    } else if (type==RMAC_MSG_TYPE_DATA) { /* data packet received */
      if (RNWK_AppOnRxCallback!=NULL) { /* do we have a callback? */
        res = RNWK_AppOnRxCallback(packet); /* call upper layer */
        if (res==ERR_OK) { /* all fine, now send acknowledge back */
          addr = RNWK_BUF_GET_SRC_ADDR(packet->data); /* who should receive the ack? */
          RNWK_BUF_SET_DST_ADDR(packet->data, addr); /* destination address is from where we got the data */
          return RMAC_SendACK(packet); /* send ack message back */
        }
      }
    } else {
      return ERR_FAULT; /* wrong message type? */
    }
  }
  return ERR_FAILED;
}

RApp

RApp.c implements the interface to the application. The application passes a table of message parsers to the Rapp.c. The packet application fields are extracted and then passed to the application handler(s):

static uint8_t ParseMessage(RAPP_MSG_Type type, uint8_t size, uint8_t *data, RNWK_ShortAddrType srcAddr) {
  bool handled = FALSE;
  uint8_t res;

  res = IterateTable(type, size, data, srcAddr, &handled, RAPP_MsgHandlerTable); /* iterate through all parser functions in table */
  if (!handled || res!=ERR_OK) { /* no handler has handled the command? */
    res = ERR_FAILED;
  }
  return res;
}

static uint8_t RAPP_OnPacketRx(RPHY_PacketDesc *packet) {
  uint8_t size;
  uint8_t *data;
  RAPP_MSG_Type type;
  RNWK_ShortAddrType srcAddr;

  type = RAPP_BUF_TYPE(packet->data);
  size = RAPP_BUF_SIZE(packet->data);
  data = RAPP_BUF_PAYLOAD_START(packet->data);
  srcAddr = RNWK_BUF_GET_SRC_ADDR(packet->data);
  return ParseMessage(type, size, data, srcAddr);
}

💡 The stack avoids to copy the data between the stack layers for efficiency. For example RAPP_BUF_TYPE() is a macro accessing the physical (raw) data buffer.

Packet Structure

Packet Structure

Application Message Handling

Finally, the application can handle the message. Below is an example where the application is extracting the received data and prints it to the console/Shell:

static uint8_t HandleRxMessage(RAPP_MSG_Type type, uint8_t size, uint8_t *data, RNWK_ShortAddrType srcAddr, bool *handled) {
#if PL_HAS_SHELL
  uint8_t buf[16];
  CLS1_ConstStdIOTypePtr io = CLS1_GetStdio();
#endif
  uint8_t val;

  switch(type) {
    case RAPP_MSG_TYPE_DATA: /* <type><size><data */
      *handled = TRUE;
      val = *data; /* get data value */
#if PL_HAS_SHELL
      CLS1_SendStr((unsigned char*)"Data: ", io->stdOut);
      CLS1_SendNum8u(val, io->stdOut);
      CLS1_SendStr((unsigned char*)" from addr 0x", io->stdOut);
      buf[0] = '\0';
#if RNWK_SHORT_ADDR_SIZE==1
      UTIL1_strcatNum8Hex(buf, sizeof(buf), srcAddr);
#else
      UTIL1_strcatNum16Hex(buf, sizeof(buf), srcAddr);
#endif
      UTIL1_strcat(buf, sizeof(buf), (unsigned char*)"\r\n");
      CLS1_SendStr(buf, io->stdOut);
#endif
      return ERR_OK;
    default:
      break;
  } /* switch */
  return ERR_OK;
}

💡 The stack currently supports both 8bit and 16bit addresses, controlled by the RNWK_SHORT_ADDR_SIZE macro.

Sending Data

Sending data finally is super easy: The code below send a one byte data value to another node:

static uint8_t SendDataByte(uint8_t val) {
  uint8_t buf[RAPP_BUFFER_SIZE]; /* payload data buffer */
  uint8_t res;

  RAPP_BUF_PAYLOAD_START(buf)[0] = val; /* store 1 byte of data */
  res = RAPP_PutPayload(buf, sizeof(buf), sizeof(val), RAPP_MSG_TYPE_DATA, APP_dstAddr); /* send one byte of data */
  if (res!=ERR_OK) {
    Err((unsigned char*)"Failed sending message!\r\n");
  }
  return res;
}

It uses:

  1. A payload buffer (buf[])
  2. A message identifier (RAPP_MSG_TYPE_DATA)
  3. Storing the data in the payload buffer (starting at RAPP_BUF_PAYLOAD_START[])
  4. The destination node address (APP_dstAddr)
  5. and sending it with RAPP_PutPayload()

Example Session

The following is an example session between two boards loaded with the example application on GitHub. The application implements a command line shell. At the beginning, both nodes have default broadcast addresses (0xff):

Two Nodes with Default Broadcast Addresses

Two Nodes with Default Broadcast Addresses

The left node gets the address 0x01 assigned (app saddr 0x01), and the right one 0x02 (app saddr 0x02). And the destination address is the other node (app daddr command):

Assigned Source and Destination Addresses

Assigned Source and Destination Addresses

Now I can send data bytes between the two nodes:

Exchanging Data

Exchanging Data

Of course instead using the shell it is possible to send data directly by the application, e.g. sensor data or commands.

If the target node does not respond, the interface will return an error:

Timeout

Timeout

Summary

It is just a start, but I’m excited how well I can communicate with the RNet stack. There are still many things on my list to do:

  1. Using variable payload for the nRF24L01+ (I have variable payload implemented for the MC1320x).
  2. Network address assignment and using the Freescale ARM UID (Unique Chip Identifier).
  3. Implementation of a generic socket connection (I already have standard I/O redirection implemented).
  4. Routing of messages (now it is a peer-to-peer network).
  5. Removing the need for an RTOS in the low-level driver.
  6. And probably many more ideas 😉

Happy Networking 🙂

47 thoughts on “RNet: A Simple Open Source Radio Network Stack

  1. Hello
    Congratulations on your development, I think I will use it.
    Your prototype operates between two nRF24L01+ cards.
    It remains to develop if multiple cards with the problem of priority access to the bus and avoid collisions.
    One possibility is to use a Master / Slave protocol such as MODBUS (which is 40 years old).
    The multi master protocols are more fashionable but more difficult to implement.
    LONWORKS (LONTALK) is a very good protocol, but can only be used only with the NEURON CHIP designed by ECHELON.
    I have an idea for a similar multi-master protocols (almost the same), but much simpler.
    again thank you.
    Yves Accard

    Like

    • Good day Yves,
      Just a heads up, but you do not need a Echelon Neuron chip to implement LON. At one time that was necessary, but not any longer, although I believe most still do use the chip. Loytec does not use the Neuron Chip and they Support Lon.
      Cheers,
      Sam

      Like

    • Hi Vic,
      yes, looked at that Contiki project, and indeed, this one is very intersting. But I do not need to run IP on my small network, so I do not need Contiki.
      The other thing is: I’m not a fan of a tight integration of a network stack with an OS (like tinyOS does as well): I rather want to have the stack and the RTOS separate.
      But what I have in mind is to run Contiki on a ‘aggregator’ node which then would interface with the internet and include smaller network nodes running an nRF24L01+ transceiver.

      Like

  2. Hi, good work.
    What is your concept about power management in this network? Do you plan to do something that will allow the nodes to sleep during most of the time? I mean timed wake up for receive check? This can be done only on protocol level, so it is up to you to decide about it. It is a mandatory feature for battery operated nodes.

    Like

    • Hi, and thanks!
      I plan to use two concepts:
      a) that the routers need to be always on, while the edge nodes can be in low power mode. The edge nodes would wake up and initiate the data transfer. The advantage is that the edge nodes (e.g. a sensor node) can be sleeping as long as it wants. But the router cannot contact the sensor nodes as communication will not be possible if they are sleeping.
      b) using a beacon mode as e.g. in IEEE802.15.4: there will be a short time frame (configurable, e.g. every minute) where nodes can wake up and start communcition. Nodes would need to sychronize on that beacon because of possible clock drifts.

      I plan to implement first a) (as this is what I need). Will see if I will go to b) as well.

      Like

      • I see additional problem with a) – there will be a collision if two nodes attempt to transmit at the same time. But surely it might be a solution to start with.

        Like

        • There is always the chance for collisions in wireless networks: no way to avoid it, so the stack has to deal with it. Luckily, typically the PHY has a built-in ‘listen and retransmit’. Yes, it is a common problem, but one to deal with.

          Like

  3. Pingback: IEEE802.15.4 for the Zumo Robot | MCU on Eclipse

  4. Pingback: Sumo Robot with Accelerometer Remote Controller | MCU on Eclipse

  5. Pingback: Kinetis Unique Identification Register | MCU on Eclipse

  6. Pingback: Sumo Robots, Sensors and everything else…. | MCU on Eclipse

  7. Good day Erik,
    I am curous what version of CW you used with your RNet stack? I ask, as I downloaded the code from GitHub and when I import the project (FRDM_KL25Z and Freedome_NRF24L01) into CW 10.5 I am getting 8 errors which do not make sense (cannot find cpu.h, etc)… Is it possible I must maintain the same project path that you have used?
    Thanks in advance!
    Cheers,
    Sam

    Like

    • Hi Sam,
      I’m using 10.5. this is a Processor Expert project. The missing Cpu.h tells me that you have not generated the code. With Processor Expert it generates additional sources in the Generated_Code folder inside your project. Right click on the ProcessorExpert.pe file in your project and select ‘Generate Processor Expert Code’. I hope this helps.

      Like

      • Good day Erich,
        Thank you for the prompt response and the info! I thought I did this, but perhaps I did not… my brains/memory is not like it used to be… alas… regardless I will report back with the results.
        Thanks again!
        Cheers,
        Sam

        Like

      • Good day Again Erich,
        Sadly, I am still having issues. It looks as if there is a shared resource between the LED1:DSC_LED and RF1:nR24L01 components. In particular the BitIO_LDD item which has an error stating ” error in inherited Component Settings”. Within the NRF1 components the BitIoLdd1 states “component name conflicts with another source code module name”. I spent some time trying to figure out why this is, but can’t seem to get anywhere, as my familiarity with Eclipse is not overly vast. Any suggestions you can offer?
        Thanks in advance!
        Cheers,
        Sam

        Like

      • Good day Erich,
        I did import both parts and I tried it again and I had the same errors. I then deleted the entire project and then re-imported the project and now those errors disappeared. So, it would appear as if there was something amuck with the component importation and the existing project. Now I can correctly generate the processor expert code (yippee!), however, I get a error that the compiler cannot find the “Rstack.h” file. Upon investigation it looks like the linked resource path is the culprit… I will modify this and I suspect all should be fine. I will report back with the results!
        Cheers,
        Sam

        Like

        • The projects on GitHub are using a shared ‘RNet’ folder with the stack files. Make sure your include paths are pointing to it. The fact that Rstack.h is not found let me think that your include paths have not been set up correctly.

          Like

      • Good day again Erich,
        That was it…I had to modify the project build settings to include the “RNET” Folder and subfolders to the “include” paths. Effectively, my importation target location did not match up with your initial include paths.
        Thanks again!
        Cheers,
        Sam

        Like

        • Hi Sam,
          ah, you found it :-). Once the stack is more complete and stable, I plan to implement it as a Processor Expert component. That way such problems will not happen any more, as the stack sources will be generated with the project sources. Sorry about the troubles!

          Like

      • Good day Erich,
        No apologies are necessary, as the troubles were my own. In fact you are and always have been very gratious with your time to assist and respond to those in need and for all of us, I say thank you ever so much!
        Cheers,
        Sam

        Like

  8. Pingback: RNet: optional ACK, Dynamic Payload and Extended nRF24L0+ Driver | MCU on Eclipse

  9. Pingback: RNet Stack for 8bit MC9S08QE128 Microcontroller and MC13201 Transceiver | MCU on Eclipse

  10. Pingback: Debugging the same Project Multiple Times in Parallel with Eclipse | MCU on Eclipse

  11. Erich, what sort of LOS range are getting with the nRF24L0+? And perhaps you could comment, in general, on signal penetration in urban environment. Thanks, John

    Like

  12. Hello Erich, as my next step i am trying to run the FRDM-KL25Z_RNet. But how do i put the stack into the example? it’s not clear. Other issue is that we have to update the FRTOS calls for the new version.

    Thanks for a great blog!

    Like

  13. Pingback: RNet Stack as Component, nRF24L01+ with Software SPI | MCU on Eclipse

  14. Dear Erich. What about your develop Stack? As I understood, you build the stack is base on only 1 hardware address, both for TX and RX? few days until now, I’m thinking so much about the network use nrf24l01+, but with enhance shockburst, for 6 pipe for one note. we can use to dev app to register node, or release net. what do you think about this?

    Like

    • Not sure if you have seen the follow-up posts on this topic. The stack has been eveloved a bit. But you are correct: all nodes in the network are using the same hardware address. I was looking into the 6-pipes modle Nordic is suggesting for this. But my experience and as well with the feedback from other companies I came to the conclusion that the pipe and sub-node-pipe model is not robust enough. You might have a look at http://maniacbug.wordpress.com/2012/03/30/rf24network/. I had looked at this stack, and that’s probably what you are looking for. However, I decided against it as the license behind was not open enough for me. But it might be ok for you.

      Like

      • Thanks for your quickly response.
        I had read your link before. And try to deep understand code of maniacbug. You can download maniacbug github project to view deeply. Actually, maniacbug just build the network with only 1 hardware address, you can see in the “struct RF24NetworkHeader”. accidentally, 6 child node make confuse with 6 pipe. @@.
        I’m working with 2 pipe ok, pipe 0 and pipe 1. But the others be don’t work. Maybe need to work more.
        Anyway, thanks you so much. If any news, I will accounce you. G9, :)), in my country, now is 1AM. :))

        Like

      • I have something to confirm again.
        YOU’RE RIGHT. The code in the link you share send to 6 pipe – 6 hardware address.
        Base on the address will choose pipe to send.
        I’ve already to send to 6 pipe. And try to build mesh-net with only 1 pipe like that:
        – 1 Node send
        – 1 Node receive
        – The others transmit the package send and ACK.
        But, I have some problem. Because they have the same Hardware address (1 pipe). Therefor, after “Node send” send out the “package send”, the others node transmit the “package send” with the same Hardware address, It make “signal on air” not right, and the “Node receive” and “Node send” can’t get the package.
        It’ happen if amount of “The others” >=3.
        It’s like the situation of 1 Node will receive package of multi node (>=3) at the same time==> It can’t get anything.
        Do you have any suggest?
        Thanks and Best regards.

        Like

        • Hmm, that indeed could be a cause of problems: using the same pipe for many devices will create ACK conflicts.

          I have used the network with many devices and the same pipe, but there was one master, and the nodes were sending/communicating sporadically and random, so I have probably not seen that problem, or got recovered with a re-transmit.

          Like

  15. Looks good!

    There’s a small problem in the components files from 2014-11-16 if you are developing on Linux (where file names are case sensitive).

    When Processor Expert generates files for RNET it creates “Generated_Code/RApp.h”
    “Generated_Code/RNWK.c” however is asking for “RAPP.h”, whereas “Generated_Code/Rapp.c” includes RApp.h”

    Cheers,
    Rene

    Like

      • Thanks!
        That was quick 😉

        I’m yet to work out how to use those PEx sources.
        In the meantime just creating a link RAPP.h -> RApp.h will do the trick as well.

        Regards,
        Rene

        Like

        • The fix was easy 🙂
          Otherwise, you can make the fix in your sources too. In the console view of Eclipse, switch to the one of Processor Expert.
          It should report someting like this (this is for my machine):
          User working directory = C:\ProgramData\Processor Expert\PEXDRV_PE5_3
          This is where the sources are located.
          So for me it is
          C:\ProgramData\Processor Expert\PEXDRV_PE5_3\Drivers\RNet\RNWK.c
          so you could patch that source file directly.

          Just in case.
          Thanks,
          Erich

          Like

  16. Hi Erich,

    looks like a great project! I am just starting with the nrf24 modules and whonder what the status of your rnet stack is.

    Is it actively developed? Sorry I can’t help with it yet. I am not as experienced. But I would like to use it…

    Thanks
    Dominik

    Like

  17. Thanks Erich for giving this link. I can definitely use Message Identifier while sending data from server to device. I wanted to check this code to understand things better but i am getting an error with freeRTOS component RTOSTICKLDD1. Please advice.

    Like

What do you think?

This site uses Akismet to reduce spam. Learn how your comment data is processed.