This is the first of 3 posts about the RF12 library which drives the RFM12B wireless modules on the JeeNode, etc.
The RF12 driver is a small but reasonably complex bit of software. The reason for this is that it has some stringent time constraints, which really require it to be driven through interrupts.
This is due to the fact that the packet data rate is set fairly high to keep the transmitter and receiver occupied as briefly as possible. Data rate, bandwidth, and wireless range are inter-related and based on trade-offs. Based on some experimentation long ago, I decided to use 49.2 kBaud as data rate and 134 KHz bandwidth setting. This means that the receiver will get one data byte per 162 µs, and that the transmitter must be fed a new new byte at that same rate.
With an ATmega running at 16 MHz, interrupt processing takes about 35 µs, i.e. roughly 20% of the time. It works down to 4 MHz in fact, with processor utilization nearing 100%.
Even with the ATtiny, which has limited SPI hardware support, it looks like 4 MHz is about the lower limit.
So how does one go about in creating an interrupt-driven driver?
The first thing to note is that interrupts are tricky. They can lead to hard-to-find bugs, which are not easy to reproduce and which happen only when you’re not looking – because interrupts won’t happen exactly the same way each time. And what’s worse: they mess with the way compiled code works, requiring the use of the “volatile” datatype to prevent compiler optimizations from caching too much. Just as with threads – a similar minefield – you need to prepare against all problems in advance and deal with weird things called “race conditions”.
The way the RF12 driver works, is that it creates a barrier between the high-level interface (the user callable API), and the lower-level interrupt code. The public calls can be used without having to think about the RFM12B’s interrupts. This means that as far as the public API is concerned, interrupt handling can be completely ignored:
Calling RF12 driver functions from inside other interrupt code is not a good idea. In fact, performing callbacks from inside interrupt code is not a good idea in general (for several reasons) – not just in the RF12 driver.
So the way the RF12 driver is used, is that you ask it to do things, and check to find out its current state. All the time-critical work will happen inside interrupt code, but once a packet has been fully received, for example, you can take as much time as you want before picking up that result and acting on it.
The central RF12 driver check is the call:
if (rf12_recvDone()) ...
From the caller’s perspective, its task is to find out whether a new packet has been received since the last call. From the RF12’s perspective, however, its task is to keep going and track which state the driver is currently in.
The RF12 driver can be in one of several states at any point in time – these are, very roughly: idling, busy receiving a packet, packet pending in buffer, or busy transmitting. None of these can happen at the same time.
These states are implemented as a Finite State Machine (FSM). What this means, is that there is a (private) variable called “rxstate
“, which stores the current state as an integer code. The possible states are defined as a (private) enum in rf12.cpp
(but it can also have a negative value, this will be described later).
Note that rxstate is defined as a “volatile” 8-bit int. This is essential for all data which can be changed inside interrupt code. It prevents the compiler from applying certain optimizations. Without it, strange things happen!
So the “big picture” view of the RF12 driver is as follows:
- the public API does not know about interrupts and is not time-critical
- the interrupt code is only used inside the driver, for time-critical activities
- the RF12 driver is always in one of several well-defined “states”, as stored in
rxstate
- the rf12_recvDone() call keeps the driver going w.r.t. non time-critical tasks
- hardware interrupts keep the driver going for everything that is time-critical
- “keeping things going” is another way of saying: adjusting the
rxstate
variable
In a way, the RF12 driver can be considered as a custom single-purpose background task. It’ll use interrupts to steal some time from the running sketch, whenever there is a need to do things quickly. This is similar to the milliseconds timer in the Arduino runtime library, which uses a hardware timer interrupt to keep track of elapsed time, regardless of what the sketch may be doing. Another example is the serial port driver.
Interrupts add some overhead (entering and exiting interrupt code is fairly tedious on a RISC architecture such as the ATmega), but they also make it possible to “hide” all sorts of urgent work in the background.
In the next post, I’ll describe the RF12 driver states in full detail.
PS. This is weblog post number 900 ! (with 3000 comments, wow)
Very interesting post! – looking forward to the rest of the series.
It’s great to have more documentation on the libraries so please do keep blogging about them – RF12 and EtherCard in particular!
Good idea to describe the RF12-driver in detail. I hope differences between normal transmission and easy transmission will dicussed too
Ooh – good idea. Not in the current series. Right now, there are only some older weblog posts about that: look for “easy” in the chronological index.
For reference, the easySend functions are here – https://jeelabs.org/2009/12/02/easy-transmissions/ – and this post from a year ago – https://jeelabs.org/2010/12/11/rf12-acknowledgements/ – is also potentially useful for anyone looking into it! I know I got tripped up a bit by the “easy” functions – I found I got on better with the real ones!