Computing stuff tied to the physical world

Inside the RF12 driver – part 2

In Software on Dec 11, 2011 at 00:01

Yesterday, I described the big picture of the RF12 driver: a public polling-type API with a private interrupt-driven finite state machine (FSM). The FSM turns the RF12 driver into a background activity for the ATmega.

But first, let’s take a step back. This is how you would write code to send out a packet without FSM:

  • set up the RFM12B for transmission
  • feed it the first byte to send (i.e. 0xAA, the start of the preamble)
  • wait for the RFM12B to request more data
  • feed it more bytes, as long as there is more to send
  • then terminate transmit mode and reset the RFM12B mode to idle

Here’s an old sketch which does exactly this, called rf12xmit.pde.

The problem with this code is that it keeps the ATmega occupied – you can’t do anything else while this is running. For sending, that wouldn’t even be such a big deal, but for reception it would be extremely limiting because you’d have to poll the RFM12B all the time to avoid missing packets. Even a simple delay() in your code would be enough to miss that first incoming byte – keep in mind that although packets don’t come in all the time, when they do there is only 160 ┬Ás time to deal with each incoming byte of data.

The solution is to use interrupts. It’s for this same reason that the millis() clock and the Serial handler in the Arduino runtime work with interrupts. You don’t want to constantly check them in your code. With interrupts, you can organize your own code however you want, and check whether a new RFM12B packet was received when you are ready for it. The simplest way is to add a call to rf12_recvDone() in the loop() body, and then simply make sure that the loop will be traversed “reasonably often”.

With interrupts, the RF12 driver is no longer in control of when things happen. It can’t wait for the next event. Instead, it gets activated when there is something to do – and the way it knows what the next step needs to be, is to track the previous step in a private state variable called rxstate.

As mentioned yesterday, the RF12 driver can be doing one of several different things – very roughly summarized as: waiting for reception of data, or waiting to send the next data byte. Let’s examine both in turn:

Waiting for reception: rxstate = TXRECV

The most common state for the RF12 driver is to wait for new data bytes, with the RFM12B in receive mode. Each interrupt at this point will be treated as a new data byte. Recall the packet format:

RF12 packets

The good news, is that the RFM12B will internally take care of the preamble and SYN bytes. There won’t be any interrupts until these two have been correctly received and decoded by the RFM12B itself. So all we have to do is store incoming bytes, figure out whether we have received a full packet (by looking at the length byte in the packet), and verify the checksum at the end.

All of this happens as long as rxstate is set to TXRECV.

The driver doesn’t leave this mode when done, it merely shuts down the RFM12B receiver. Meanwhile, in each call to rf12_recvDone() we check whether the packet is complete. If so, the state is changed to TXIDLE and we return 1 to indicate that a complete packet has been received.

TXIDLE state is a subtle one: the driver could start sending data, but hasn’t yet done so. If the next API call is a call to rf12_recvDone(), then rxstate will be reset to TXRECV and we start waiting for incoming data again.

Sending bytes: rxstate = TXPRE1 .. TXDONE

I’ll skip the logic of how we enter the state TXPRE1 for now, and will just describe what happens next.

In these states, interrupts are used to keep things moving. Whenever an interrupt comes in, the driver decides what to do next, and adjusts the state. There’s a switch statement in rf12_interrupt() which looks like this:

Screen Shot 2011 12 09 at 14 05 42

It’s fairly compact, but the key is the “rxstate++” on the first line: the normal behavior is to do something and move to the next state. So the basic effect of this code is to proceed through each state in sequence:


If you look closely, you’ll see that it corresponds to the packet layout above. With one exception: after TXSYN2, the rxstate variable is set to a negative value. This is a special state where payload data is sent out from the buffer. In this context, the two header bytes are treated as payload, and indeed they “happen” to be placed in the data buffer right in front of the rest of the data, so the driver will send header bytes and payload data in the same way:

Screen Shot 2011 12 09 at 14 11 34

Ignore the details, just note that again there is the “rxstate++” in there to keep advancing the state.

At some point, incrementing these negative states will lead to state 0, which was cunningly defined to be the state TXCRC1. Now the switch statement takes over again, and appends the CRC etc to the outgoing data.

Finally, once state TXDONE has been reached, the interrupt code turns off the RFM12B’s transmit mode, which also means there will be no more interrupts coming in, and bumps the state one last time to TXIDLE.

This concludes today’s story. What you’re looking at is a fairly conventional way to write an interrupt-driven device driver. It may be full of trickery, but the logic is nevertheless quite clean: a public API to start things going and to pick up important state changes (such us having received a complete packet), while interrupts push through the states and “drive” the steps taken at each point.

Each interrupt does a little dance: where was I? oh yes, now do this and call me again when there is new work.

In a previous life I wrote a few Unix (not Linux) kernel drivers – this was for a PDP11 at the time. It’s amazing how the techniques and even the programming language are still the same (actually it’s more powerful now with C++). The difference is the price of the hardware – 10,000 x cheaper and affordable by anyone!

Tommorow, I’ll conclude with some more details about how everything fits together.

  1. The negative state – very clever. I used to work for Symbian, and some of the really optimised code I saw there was similarly clever – at first you swore that couldn’t be right, but after a long think you realised the trick.

Comments are closed.