6 May 2008

15. All meshed up - Part 1 - RF comms

I mean, really. Who doesn’t want to create their own wireless, meshed packet delivery network? This stuff is just, like, cool.

This project all began when my elderly but still energetic parents rebuilt their house and decided in a moment of weakness to allow me to design the lighting and control systems. So naturally I had all the light switches installed as X10 controllers and all the lights themselves connected via X10 lamp dimmers. Then a bunch of X10 PIR sensors send movement information to the house controller PC via a third party receiver. Then as you walk through the house the lights come on when you’re in the room and the controller PC also knows which lights are on.

So it goes in theory. In practice, every now and then an X10 lamp dimmer dies which is a total pain – they’re mounted in the wall cavities after all. Sometimes the modules forget their settings, and the X10 protocol doesn’t actually make it all the way through the house, despite adding an expensive module that’s meant to relay and amplify the signals. And with me living in London and them in Australia, it’s all a little difficult when it goes wrong. Still, it’s cool being able to control their lights from the other side of the planet.

My plan was to replace the unreliable X10 controllers and lamp dimmers with RF modules. Of course you could never guarantee that the modules could talk to each other directly, my parents have internal brick walls that really wreak havoc with anything wireless. So the network would need to be resilient to this type of problem.

And as soon as you start creating something like this, you start to see lots of ways it could be used. Temperature and humidity sensors, real time clock information, movement sensors… you name it!

I did look at Zigbee of course, and that’s great in theory, but the most popular modules require a star network with controllers and don’t actually do proper meshing. Besides, I thought it would be fun to design my own.

I started out with the Sparkfun nrf2041a modules and a couple of their Serial Development Node (version 1). These are designed to work together and the SDNs provide translation of signals to RS232, a 16f88 pic on board and spot to plug a 2401a module straight into. Perfect!

The first chunk of work was to get the actual Nordic transceiver nrf2401a chips working and chatting away to each other. Let’s look at that first, and then in the next article, look at how the packet delivery network works.

The PicPack library supports both the nRF2401A and the nRFL01 chips, seamlessly and simultaneously on the same network. We’ll have a look at the 2401a here. Open up the packet_demo project.

Naturally the first thing you need to define the ports and pins used to communicate with the 2401a in your config.h file:

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// pic_rf include
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// For SFE_TDN_V1 board
#define rf_ce_port PORTA
#define rf_ce_pin 6
#define rf_dr1_port PORTA
#define rf_dr1_pin 3
#define rf_cs_port PORTA
#define rf_cs_pin 0
#define rf_data_port PORTA
#define rf_data_pin 2
#define rf_clk1_port PORTA
#define rf_clk1_pin 1
These are the ports and pins used by the SparkFun Terminal Development Node modules. There is, however, a catch. In their ultimate wisdom, the SparkFun guys (bless ‘em) wired the DR1 output from the 2401a to port a, pin 3. The DR1 “Data Ready 1” output goes high when a packet has been received. Now, this doesn’t seem like much of an issues, except that we’d really like to interrupt whatever the pic is doing at the time when a packet arrives. All this polling to find out what’s going on stuff is just silly. So you’ll need to wire the porta pin 3 to port b pin 0. It’s easy, you just need to install a “link” in the alternative connector on the board. You can see from the picture that it's the second and fourth holes on the second row back that need linking.

So, the first thing you need to do, as usual, is run the setup routine that’s part of the library to set the ports and pins to be the right inputs or outputs:

pic_rf_setup();
Then, as usual, you’ll need to actually initialise the hardware itself for use. The trickiest part about using the 2401a is that it pretty much sits there like a dead duck until you get the configuration right, and then it springs into life. Until then, you really have no idea what you’ve got wrong. PicPack provides two routines for configuring your 2401a. We’ll look at the more verbose one first. You need to setup the rf_config struct with all the settings you care about – then pass a pointer to it to the pic_rf_init() routine. Let’s work through the config.

rf_config my_config;
The Nordic transceivers can receive on two (or more) channels at once. We don’t care about this for our purposes, so we set the payload size (or width, in bits) to zero.

rf_config my_config;
For our primary channel, we do care – it has to be the same size as (in this case) our packet size.

my_config.payload_width_ch1 = PKT_PACKET_SIZE * 8;
Here we fill in the address for channel 2, although it’s actually not important want you do or don’t do with these values:

my_config.address_ch2[0] = 0xf0;
my_config.address_ch2[1] = 0xf0;
my_config.address_ch2[2] = 0xf0;
my_config.address_ch2[3] = 0xf0;
my_config.address_ch2[4] = 0xf0;
When it comes to the channel 1 address, we do care. Here’s the default address for Nordic chips, and in this case, we’re using address 0xE7E7E7. We specify two more bytes than we need just for completeness.

my_config.address_ch1[0] = 0b11100111; // addr starts here
my_config.address_ch1[1] = 0b11100111;
my_config.address_ch1[2] = 0b11100111;
my_config.address_ch1[3] = 0b11100111; // only used three but fill
my_config.address_ch1[4] = 0b11100111; // ...for the fun of it
In order to tell the chip we’re using 3 byte addresses, we set the address_width to 24 (8*3).

my_config.address_width = 24; // (6 bits valid)

We want to have a 16 bit CRC check on the packet. This is how the chip tells a real packet from noise in the air. At 1 Mbps you’d be amazed how often noise turns out to give correct CRC values with 8 bit CRCs. 16 bit CRC is much better, but is still not 100%. That’s why the PicPack library has its own check byte as well.

set_bit(my_config.options, OP_LONG_CRC);
Now we enable having a CRC as part of the transmission:

set_bit(my_config.options, OP_ENABLE_CRC);
Turn off the second channel:

clear_bit(my_config.options, OP_ENABLE_CH2);
Shockburst is the name Nordic gives for clocking your packet in to the chip and then sending it out on-air at the correct bit rate in one go.

set_bit(my_config.options, OP_ENABLE_SHOCKBURST);
You can turn this bit off to enable 250kbps rate, which does give slightly longer range. 1 Mbps gives you less chance of packet collisions, which is why I recommend it (as well as giving compatibility with the nrf24L01 chip).

set_bit(my_config.options, OP_ENABLE_1_MBPS);
On the SparkFun boards, they’re wired up with a 16Mhz crystal, which is why we choose that option here:

// (3 bits valid) -> 16Mhz
my_config.crystal = 0b011;
You have two bits (4 levels) of output power:

// (2 bits valid) 11 -> max power!
my_config.output_power = 0b11;
We can choose any of a gazillion channels, and the Nordic chip can hop around then quite quickly. Still, at this stage of the library, we pick one at the start and leave it at that. I thought it would be good to look at a frequency hopping algorithm, but this does get hard when you have a meshed network. Fine when it’s one on one, but when there’s three or more units, how does everyone keep in sync? If you have any ideas, I’d love to hear them. In the mean time:

my_config.channel = 2; // (7 bits valid)
Last but not least, we put the chip into the receive mode:

set_bit(my_config.options, OP_ENABLE_RECEIVE);
And then with a little flourish, call the routine that takes this information and passed it to the 2401a to get it initialised.

pic_rf_init(&my_config);
As you can imagine, this programmatic way of telling the Nordic chip what to do is easy to understand and gives you the ability to change things at run time. You’ll also realise that this does take a chunk of code (and hence flash) to do all this. If all you want to do is set the config right at the start and not change it after that (other than the channel) then you can use the quick method:

pic_rf_quick_init("\x00\xa8\xf0\xf0\xf0\xf0\xf0
\xe7\xe7\xe7\xe7\xe7\x63\x6f\x05",
2, 1);
This does exactly what the previous chunk of code did. The ‘2’ parameter is the channel number, and ‘1’ turns the receiver on. To generate this config string, use the nrf2401a_config.pl script in the tools directory. You don’t need to know anything about perl (other than having it installed somewhere!) – just edit the file and run it to get it to spit out the config string.

So how do you go about sending data across the link? Well, here’s how the packet routines do it:

void pkt_send_packet(rf_packet *packet) {
// +3 for RF address (fixed)
uns8 tx_buffer[PKT_PACKET_SIZE + 3];
uns8 count;
tx_buffer[0] = 0b11100111; // address
tx_buffer[1] = 0b11100111; // address
tx_buffer[2] = 0b11100111; // address
for (count = 0; count < PKT_PACKET_SIZE; count++) {
tx_buffer[count+3] = packet->a[count];
}
pic_rf_transmit(tx_buffer, PKT_PACKET_SIZE + 3);
}
You can see it’s as simple as whacking the destination address on the start. In the case of the packet network, we’re assuming everyone’s on the same address. Of course, you could use these routines to direct your data to a particular node directly using this addressing.

Receiving data is only slightly more complicated. Since we’ve wired up the DR1 line to the port B, pin 0, it means that we can use an interrupt to detect when the nRF2401a wants to tell us something. This is much better than having to poll for data all the time. The 16f88 has two ways of detecting interrupts, but the one we want is when port B, pin 0 goes high. It’s set up like this:

make_input(PORTB, 0);
set_bit(intcon, INTE);
You’ll need to turn interrupts on as well:

turn_global_ints_on();
In your interrupt() routine you can use something like this:

if ( test_bit(intcon, INTF) ) {
pic_rf_receive(rf_rx_buffer, MY_BUFFER_SIZE);
clear_bit( intcon, INTF);
}
Notice how you test for the interrupt flag being set, respond if necessary, and then clear the flag.

That’s all there is to it. Next tutorial we’ll start looking at how to create a packet network on top of the RF communication we’ve set up here.

3 comments:

Jörgen said...

Thanks for your great work looking forrward to read next part:-) Hope you put out some pictures and schematics to. Regards /Jörgen

nuit said...

what i'm interested in: how do you make sure, the signal doesn't get backfired? so it's bouncing around in the mesh?

Joshua said...

For sync'ing frequency hopping systems you've got a couple of options. Obviously the problem is getting the nodes on a common timebase.

The easiest way to do it is to use a star network for just the timing, except you won't own the hub of the star :)

1) A WWVB receiver should work
http://www.sparkfun.com/products/10060


2) There were also smallish, low power modules that I've seen that sync to the 60Hz ether that's usually a PitA for electronics, but here can be an atomic clock-based time reference, but I can't remember where off the top of my head.

If you want to keep it a true mesh, though, you can distribute timing locally, taking advantage of the fact that each node only needs to be sync'd up to its neighbors

Have each node startup in a non-hopping node while it is waiting to join the network

The 'master' (House data sink) node sends a timing packet, say, every 10 seconds to its neighbors. Now the master and each of its neighbors are on the same timebase, and their communications can be frequency hopping. Repeat downstream for each of the nodes.

The exact intervals will depend on your oscillators' relative frequency accuracy and stability, and the slop your protocol allows. If you want to get serious (since oscillators are so cheap) you can buy a bunch of them, then bin them yourself using a scope. Be careful of temperature sensitivity, e.g. for a node in the garage.


Anyhow, personally, I think the 60Hz or WWVB solution is kind of elegant, as you're glomming on to infrastructure that's already there (at least in the US, though a 50Hz receiver for places that have that should be easy enough).

The local timing distribution keeps better faith with the idea of mesh networks that are self-organizing and deployable anywhere.

PS- google 'sensor Mesh network timing distribution' and you should come up with a bunch of stuff. People have been seriously playing around with optimizing this problem for about 10 years.


Happy automating,
--Josh