Building an MRBus Node with Arduino

Note: This example sketch depends upon the MRBus Library for Arduino. Be sure to read the documentation for the library - particularly for installation instructions and compatible hardware.

The first step is to get the latest sketch code. The latest example code can always be checked out using Subversion from our version control archive - svn://mrbus.org/mrbus/trunk/mrb-ard/src/sketch (As an example, to check this out you'd type svn checkout svn://mrbus.org/mrbus/trunk/mrb-ard/src/sketch mrbus-sketch )

If you don't want to mess with checking it out from version control, you can also download the sketch here. (Last updated 28 Jan 2013) I don't promise it's the most current version, but it should work.

We'll walk through this section by section down below, but let me first show you the code for the whole sketch (or at least as it existed on 28 Jan 2013). This is a fully working node that will transmit status packets and respond to command packets, pings, EEPROM reads, and EEPROM writes. It'll make a pretty good starting point for creating your own node.

#include <mrbus-arduino.h>
#include <EEPROM.h>
MRBus mrbus;

unsigned long lastUpdateTime, currentTime, lastTransmitTime;
unsigned long updateInterval = 2000;

void setup()
{
  uint8_t address = EEPROM.read(MRBUS_EE_DEVICE_ADDR);
  mrbus.begin();
  if (0xFF == address)
  {
    EEPROM.write(MRBUS_EE_DEVICE_ADDR, 0x07);
    address = EEPROM.read(MRBUS_EE_DEVICE_ADDR);
  }

  // Read the update interval out of EEPROM
  // By MRBus convention, it's stored in tenths of seconds.  The Arduino counter operates in milliseconds
  updateInterval = (((unsigned int)EEPROM.read(MRBUS_EE_DEVICE_UPDATE_H))<<8) + EEPROM.read(MRBUS_EE_DEVICE_UPDATE_L);
  updateInterval *= 100;

  if (updateInterval > 2000)
    updateInterval = 2000;

  mrbus.setNodeAddress(address);
  lastUpdateTime = currentTime = millis();
  pinMode(13, OUTPUT);
}

void pktActions(MRBusPacket& mrbPkt)
{
  // Is it broadcast or for us?  Otherwise ignore
  if (0xFF != mrbPkt.pkt[MRBUS_PKT_DEST] && mrbus.getNodeAddress() != mrbPkt.pkt[MRBUS_PKT_DEST])
    return;

  if('A' == mrbPkt.pkt[MRBUS_PKT_TYPE])
  {
    // Ping
    MRBusPacket replyPkt;
    replyPkt.pkt[MRBUS_PKT_DEST] = mrbPkt.pkt[MRBUS_PKT_SRC];
    replyPkt.pkt[MRBUS_PKT_SRC] = mrbus.getNodeAddress();
    replyPkt.pkt[MRBUS_PKT_TYPE] = 'a';
    replyPkt.pkt[MRBUS_PKT_LEN] = 6;
    mrbus.queueTransmitPacket(replyPkt);
  }
  else if ('C' == mrbPkt.pkt[MRBUS_PKT_TYPE])
  {
    // Command Packet
    if (0 == mrbPkt.pkt[6])
      digitalWrite(13, LOW);
    else
      digitalWrite(13, HIGH);

    MRBusPacket replyPkt;
    replyPkt.pkt[MRBUS_PKT_DEST] = mrbPkt.pkt[MRBUS_PKT_SRC];
    replyPkt.pkt[MRBUS_PKT_SRC] = mrbus.getNodeAddress();
    replyPkt.pkt[MRBUS_PKT_TYPE] = 'c';
    replyPkt.pkt[MRBUS_PKT_LEN] = 7;
    replyPkt.pkt[6] = mrbPkt.pkt[6];
    mrbus.queueTransmitPacket(replyPkt);
  }
  else if ('R' == mrbPkt.pkt[MRBUS_PKT_TYPE] && mrbPkt.pkt[MRBUS_PKT_LEN] >= 7)
  {
    // EEPROM Read
    MRBusPacket replyPkt;
    replyPkt.pkt[MRBUS_PKT_DEST] = mrbPkt.pkt[MRBUS_PKT_SRC];
    replyPkt.pkt[MRBUS_PKT_SRC] = mrbus.getNodeAddress();
    replyPkt.pkt[MRBUS_PKT_TYPE] = 'r';
    replyPkt.pkt[MRBUS_PKT_LEN] = 8;
    replyPkt.pkt[6] = mrbPkt.pkt[6];
    replyPkt.pkt[7] = EEPROM.read(mrbPkt.pkt[6]);
    mrbus.queueTransmitPacket(replyPkt);
  }
  else if ('W' == mrbPkt.pkt[MRBUS_PKT_TYPE] && mrbPkt.pkt[MRBUS_PKT_LEN] >= 8)
  {
    // EEPROM Write
    EEPROM.write(mrbPkt.pkt[6], mrbPkt.pkt[7]);

    MRBusPacket replyPkt;
    replyPkt.pkt[MRBUS_PKT_DEST] = mrbPkt.pkt[MRBUS_PKT_SRC];
    replyPkt.pkt[MRBUS_PKT_SRC] = mrbus.getNodeAddress();
    replyPkt.pkt[MRBUS_PKT_TYPE] = 'w';
    replyPkt.pkt[MRBUS_PKT_LEN] = 8;
    replyPkt.pkt[6] = mrbPkt.pkt[6];
    replyPkt.pkt[7] = EEPROM.read(mrbPkt.pkt[6]);
    mrbus.queueTransmitPacket(replyPkt);
  }

}

void loop()
{
  currentTime = millis();
  if (currentTime - lastUpdateTime >= updateInterval)
  {
    // More than 2 seconds have elapsed since we sent a status packet  
    MRBusPacket statusPkt;
    statusPkt.pkt[MRBUS_PKT_DEST] = 0xFF;
    statusPkt.pkt[MRBUS_PKT_SRC] = mrbus.getNodeAddress();
    statusPkt.pkt[MRBUS_PKT_TYPE] = 'S';
    statusPkt.pkt[MRBUS_PKT_LEN] = 7;
    statusPkt.pkt[6] = 0;
    statusPkt.pkt[6] |= (digitalRead(13))?0x01:0;
    mrbus.queueTransmitPacket(statusPkt);
    lastUpdateTime = currentTime;
  }

  // If we have packets, try parsing them
  if (mrbus.hasRxPackets())
  {
    MRBusPacket mrbPkt;
    mrbus.getReceivedPacket(mrbPkt);
    pktActions(mrbPkt);
  }

  // If there are packets to transmit and it's been more than 20mS since our last transmission attempt, try again
  if (mrbus.hasTxPackets() && ((lastTransmitTime - currentTime) > 20))
  {
     lastTransmitTime = currentTime;
     mrbus.transmit();
  }
}
Walking Through the Example

Section 1: Includes and Global Variables

#include <mrbus-arduino.h>
#include <EEPROM.h>
MRBus mrbus;

unsigned long lastUpdateTime, currentTime, lastTransmitTime;
unsigned long updateInterval = 2000;

The first part - #include <mrbus-arduino.h> tells the Arduino to include the appropriate definitions for the MRBus class and interface. If you were starting from scratch, you'd get this line included when you selected "Sketch -> Import Library... MRBus"

The next line - #include <EEPROM.h> - is included because we want to make use of the AVR's onboard EEPROM memory to store MRBus configuration values.

Next, the program sets up the global variables - those things that need to be accessed from lots of places in our program. Notice that this where we create our MRBus class instance, which we'll call mrbus. Because it's global, we can call it from anywhere in our program without worrying about variable scoping.

The other pieces - lastUpdateTime, currentTime, lastTransmitTime, updateInterval - are used to determine when to send status packets. They essentially hold times. Note that we'll initialize updateInterval to 2000 (meaning 2000 milliseconds, or 2 seconds) when the program starts up.

Section 2: The Arduino setup() Function

void setup()
{
  uint8_t address;

  mrbus.begin();

  address = EEPROM.read(MRBUS_EE_DEVICE_ADDR);
  if (0xFF == address)
  {
    EEPROM.write(MRBUS_EE_DEVICE_ADDR, 0x07);
    address = EEPROM.read(MRBUS_EE_DEVICE_ADDR);
  }

  mrbus.setNodeAddress(address);

  // Read the update interval out of EEPROM
  // By MRBus convention, it's stored in tenths of seconds.  The Arduino counter operates in milliseconds
  updateInterval = (((unsigned int)EEPROM.read(MRBUS_EE_DEVICE_UPDATE_H))<<8) + EEPROM.read(MRBUS_EE_DEVICE_UPDATE_L);
  updateInterval *= 100;

  if (updateInterval > 2000)
    updateInterval = 2000;

  lastUpdateTime = currentTime = millis();
  pinMode(13, OUTPUT);
}

The setup() function is called by the Arduino library once when the sketch starts running. This is where you should be doing all of your one-time setup to get the node initialized the way you want it.

The key thing we do in this section for MRBus is call MRBus::begin() to initialize the MRBus object. We also need to configure a few key things before we can start using it.

The first thing we need to configure is give our new node an address. Each device on an MRBus segment that transmits will need a unique 8-bit address between 0x01 and 0xFE. (0x00 is reserved as "ignore" and 0xFF is reserved for broadcast by the specification.) Since we want to be able to change the address when we set up a node - imagine having dozens of the same node running the same sketch, each with its own configuration - but we want it to stay the same every time the node starts, we'll store that in an EEPROM slot. The MRBus specification says that the address should be in configuration value 0, so for simplicity we'll just make it the same EEPROM address. Therefore, we can use MRBus configuration value defines MRBUS_EE_DEVICE_ADDR (which is 0x0000) and the EEPROM library to get the value out of non-volatile memory.

The part that starts with if (0xFF == address) ... is a sanity check. It makes sure that the value we're getting out of EEPROM isn't the broadcast address. Given that the contents of EEPROM are unpredictable before they're set by the user, we might get any value. If it is equal to 0xFF, we change the address to a nice safe value of 0x07, write it to EEPROM, and re-read it.

Once we've got a valid address, we'll set that into the MRBus object with MRBus::setNodeAddress().

We're going to do something similar with the update interval. MRBus uses a model where data sources (such as our demo node) blast out a status packet every so often to reconfirm our current state to the rest of the bus. (Many nodes implement an algorithm where status packets are also sent when critical changes happen, speeding up the process of informing other nodes that something changed.) Other nodes can choose to listen to these status packets and act upon them.

''Example: Let's say that on one side of your model railroad layout, you have a removable span to allow operators to get inside a large loop of track. Obviously, when the span isn't closed and locked, you don't want trains entering that section. There's a node that monitors switches at the ends of the span and broadcasts out a status packet with one data byte, where one of the bits means "draw span closed". You have another node that has a relay hooked to it that controls track power on both sides of the open span, and you have a third node that has an LED on the dispatching board marked "draw span closed".

The drawspan sensor node would broadcast the current state of the span every so often, and the two other nodes would be programmed to listen to status packets from that sensor node and act on the "draw span closed" bit. One would energize the track relay, and one would light an indicator. The sensor "producer" node doesn't know who is consuming the data, just that it should transmit it every so often or upon change. The consuming nodes don't have to interact with the producer, and if they miss a packet, another is coming along shortly.

This is why the concept of update intervals is important - some nodes need to transmit very infrequently, whereas others will need to broadcast often to assure information is communicated in a timely manner.''

By convention, these are stored in configuration values 2 and 3 (with 2 being the higher order bits), and are specified in units of 0.1 second increments. So a value of 20 would be a 2 second update interval. The default Arduino time functions have a resolution in milliseconds, however, so we just multiply the updateInterval value by 100. Then there's the usual sanity logic, which says if our update interval is greater than 2 seconds (2000 ms), just set it to 2 seconds.

In order to make updateInterval work, we're also going to need the current time and the last time we sent an update. The line of lastUpdateTime = currentTime = millis(); sets these both to the current number of milliseconds elapsed since the node started up. That way we know they're initialized to something sane.

That's it! At this point, we're done with the things you'll probably want for MRBus, and you can put in any other node initialization code that you need. In this case, since we're going to use the LED that comes on most Arduino boards attached to digital I/O 13, we'll set DIO 13 to an output.

Section 3: The Arduino Main Loop

void loop()
{
  // Check to see if it's time to send a status packet
  currentTime = millis();
  if (currentTime - lastUpdateTime >= updateInterval)
  {
    // More than 2 seconds have elapsed since we sent a status packet  
    MRBusPacket statusPkt;
    statusPkt.pkt[MRBUS_PKT_DEST] = 0xFF;
    statusPkt.pkt[MRBUS_PKT_SRC] = mrbus.getNodeAddress();
    statusPkt.pkt[MRBUS_PKT_TYPE] = 'S';
    statusPkt.pkt[MRBUS_PKT_LEN] = 7;
    statusPkt.pkt[6] = 0;
    statusPkt.pkt[6] |= (digitalRead(13))?0x01:0;
    mrbus.queueTransmitPacket(statusPkt);
    lastUpdateTime = currentTime;
  }

Every Arduino sketch has a loop() function. This is the one that will run until the end of time (or at least until rebooted or powered down), and thus the one that has to continuously look for input, transform it somehow, and create output.

At this point, I should point out that you don't need to sit around waiting for MRBus packets to come in. The MRBus class? has buffers that hold up to four incoming and outgoing packets while your code is off doing other things. This will become important later.

The first thing our loop does is check the time. If the current time minus the last time we updated is greater than updateInterval, then it's time to create a status packet.

To create this packet, we start with a MRBusPacket structure. Into that, at a minimum, we need to set the packet destination, type, and length. Most useful packets will also have some actual data bytes in them as well. Here's the structure of an MRBus packet (each set of braces is a byte, numbered left to right starting at byte 0):

[DEST][SRC][LEN][CRC16_H][CRC16_L][TYPE][DATA (bytes as needed)]

  • [DEST] is the address to which you want this packet to go. Almost all status packets go to everybody (0xFF, the broadcast address that everybody listens to)
  • [SRC] will be filled in when the packet is added to the transmit, but it's good practice to just fill it in.
  • [LEN] is something between 6 (the minimum MRBus packet length, basically everything from DEST through TYPE) and 20 (the maximum MRBus packet length - the header plus 14 bytes of data).
  • [CRC16_H] and [CRC16_L] are two bytes of checksumming information that help assure the packet arrived uncorrupted. You don't need to fill these in - the library will do it for you when the packet is transmitted.
  • [TYPE] tells the receiver what kind of packet this is. This isn't hard and fixed, but there are some conventions to follow. For example, status packets carry a type of 'S'.
  • [DATA (bytes as needed)] is whatever data you wish to send in your packet. In this example, we're just going to send a single byte, where the lowest bit means "Arduino LED is on".

Once we've put the proper packet values in the proper places, we call MRBus::queueTransmitPacket()? to place this newly completed packet into our queue of packets to transmit.

// If we have packets, try parsing them
  if (mrbus.hasRxPackets())
  {
    MRBusPacket mrbPkt;
    mrbus.getReceivedPacket(mrbPkt);
    pktActions(mrbPkt);
  }

This part of the loop works to process any packets we may have received off the bus. These received packets get tested for correctness (correct length & correct checksum) in the background as they're received, so anything sitting in the receive queue is valid.

MRBus::hasRxPackets()? is a quick function that tells us if we have packets in the receive queue that need to be processed. (It actually returns a number of them - zero, being a logical false in C, will cause the if statement to be skipped. If you wanted to completely clean out your receive queue, you could always use a while loop here.) Again, we create an MRBusPacket structure to hold our packet, and then call MRBus::getReceivedPacket()? to pull that received packet off the queue and put it in our new data structure.

While we could have put all the packet handling code inline here, we'll make a function called pktActions() instead. That keeps our main loop clean, so it's easy to read and maintain. Suffice to say its job is to take incoming packets, filter them down to the ones our node is interested in, and act on them.

// If there are packets to transmit and it's been more than 20mS since our last transmission attempt, try again
  if (mrbus.hasTxPackets() && ((lastTransmitTime - currentTime) > 20))
  {
     lastTransmitTime = currentTime;
     mrbus.transmit();
  }
}

The final part of our main loop handles actually transmitting packets we need to send. Remember that call to MRBus::queueTransmitPacket()? we made back when we constructed the status packet? Well, it just placed that packet in a queue inside the MRBus object. Here's where we actually put it on the wire.

The function MRBus::hasTxPackets()? is similar to the receive equivalent - it tests if there are packets in our transmit queue that need to go out. If there aren't, it skips over the whole process. Otherwise, we also test if it's been 20 milliseconds since we sent something - ((lastTransmitTime - currentTime) > 20). It's not strictly necessary, but it provides a safeguard against one node flooding the bus with packets and silencing everyone else.

If we make it through that, we try transmitting with the MRBus::transmit()? function. This will go off and do bus arbitration and - hopefully - eventually put our data packet on the bus for everybody else to hear. That said, MRBus::transmit()? can fail if somebody else is using the bus at the same time. No harm done - the packet will stay in the transmit queue, we'll wait a while thanks to the 20ms delay, and we'll try again on the next go round.

Section 4: The Packet Handler

void pktActions(MRBusPacket& mrbPkt)
{
  // Is it broadcast or for us?  Otherwise ignore
  if (0xFF != mrbPkt.pkt[MRBUS_PKT_DEST] && mrbus.getNodeAddress() != mrbPkt.pkt[MRBUS_PKT_DEST])
    return;

Remember that chunk of stuff in the middle we skipped over when talking about handling the receive queue in section 3 - the pktActions() function? Well, here it is. The first thing we need to check is whether the message is addressed to us, either directly by our address, or via the broadcast address - 0xFF - that everybody should listen to. If it isn't one of those two things, the return dumps us out of the function and we're done with this packet.

So at this point, it's a packet that we may be interested in...

if('A' == mrbPkt.pkt[MRBUS_PKT_TYPE])
  {
    // Ping - blah blah blah
  }
  else if ('C' == mrbPkt.pkt[MRBUS_PKT_TYPE])
  {
    // Command - blah blah blah
  else if ('R' == mrbPkt.pkt[MRBUS_PKT_TYPE] && mrbPkt.pkt[MRBUS_PKT_LEN] >= 7)
  {
    // EEPROM Read - blah blah blah
  }
  else if ('W' == mrbPkt.pkt[MRBUS_PKT_TYPE] && mrbPkt.pkt[MRBUS_PKT_LEN] >= 8)
  {
    // EEPROM Write - blah blah blah
  }

One of the common things for a node to do is respond to the common packet types. All nodes should at the very least respond to a ping (packet type 'A'). Our sample node here will recognize four packet types - ping ('A'), command ('C'), read eeprom ('R') and write eeprom ('W'). I've omitted all the actual code in the middle for clarity. The read/write commands also have some sanity testing logic on them to make sure we received enough bytes to actually perform the operation. Obviously if we're going to read or write eeprom, we'll need at least an address and possibly a value to write into it.

Let's look at the implementation of the ping ('A') command. Pings are just a way to tell if a node is on the network - they can be issued and the node should transmit a ping response if it's alive.

if('A' == mrbPkt.pkt[MRBUS_PKT_TYPE])
  {
    // Ping
    MRBusPacket replyPkt;
    replyPkt.pkt[MRBUS_PKT_DEST] = mrbPkt.pkt[MRBUS_PKT_SRC];
    replyPkt.pkt[MRBUS_PKT_SRC] = mrbus.getNodeAddress();
    replyPkt.pkt[MRBUS_PKT_TYPE] = 'a';
    replyPkt.pkt[MRBUS_PKT_LEN] = 6;
    mrbus.queueTransmitPacket(replyPkt);
  }

This code tells us if we get a ping, construct a ping response ('a', sometimes called a "pong", as in ping-pong) in an MRBusPacket structure and put it in our transmit queue for later sending. Note that we take the source address off the incoming ping (in the mrbPkt structure) and use it as the destination in the replyPkt structure. That way the ping response goes back to whatever node sent it.

Now on to something slightly more complicated - the command packet handler. Most nodes are probably either going to take inputs and put them on the bus, or take state from the bus and drive outputs with it. Sometimes this output will just be by watching a bit/byte in a regular status transmission broadcast, and sometimes it will be an explicit command sent to a node. For this node, I'm going to implement the latter - have a command packet consisting of a TYPE of 'C' (by convention) and a single data byte that tells the node to do something. If that data byte is zero, it will shut off the LED on DIO 13. If the byte is anything else, it will turn that same LED on. It also responds with a command acknowledge packet ('c') to indicate that it got and processed the command.

else if ('C' == mrbPkt.pkt[MRBUS_PKT_TYPE])
  {
    // Command Packet
    if (0 == mrbPkt.pkt[6])
      digitalWrite(13, LOW);
    else
      digitalWrite(13, HIGH);

    MRBusPacket replyPkt;
    replyPkt.pkt[MRBUS_PKT_DEST] = mrbPkt.pkt[MRBUS_PKT_SRC];
    replyPkt.pkt[MRBUS_PKT_SRC] = mrbus.getNodeAddress();
    replyPkt.pkt[MRBUS_PKT_TYPE] = 'c';
    replyPkt.pkt[MRBUS_PKT_LEN] = 7;
    replyPkt.pkt[6] = mrbPkt.pkt[6];
    mrbus.queueTransmitPacket(replyPkt);
  }

I'm not going to run through the configuration value read/write commands ('R' and 'W') but they work very similarly.

There you go - all the pieces you need for a fully-functional MRBus node, written as a sketch for an Arduino!

Note: I've been working on MRBus nodes (or their predecessor, RDB nodes) in one form or another for almost 15 years now. In doing this so long, sometimes I overlook details that are not clear to a newcomer. Questions, comments and improvements are always welcome - email me at maverick@drgw.net. Thanks!

License

This sketch is public domain - it's just an example, do whatever you want with it.

The MRBus Library for Arduino is free software, licensed under the GNU General Public License v3.

Copyright 2012 by the MRBus Group.
Licensed under a Creative Commons Attribution-ShareAlike 3.0 License.
Questions? Comments? Please email us at support@iascaled.com

Last modified on February 19, 2012, at 01:36 PM