Computing stuff tied to the physical world

From RFM69 to RasPi via I2C

On the Raspberry Pi side, we’re going to use the following setup as a quick prototype:

DSC 4908

This is an LPC810, connected with 2 I/O pins to the I2C bus (and 2 more pins to power), and the other 4 I/O pins connected to the RFM69CW radio module:

  • pin 1 = GPIO0_5 = RFM69 MISO
  • pin 2 = GPIO0_4 = I2C SCL
  • pin 3 = GPIO0_3 = RFM69 SCLK
  • pin 4 = GPIO0_2 = RFM69 NSS
  • pin 5 = GPIO0_1 = RFM69 MOSI
  • pin 6 = ground
  • pin 7 = +3.3V
  • pin 8 = GPIO0_0 = I2C SDA

Two more wires connect the radio to the same power pins, and then of course there’s that 83mm-or-so quarter-wave antenna wire, sticking straight up in the image above.

The hardware is actually the easy part in this case. Let’s now turn to the software we’re going to have to implement:

  • as minimum, an interface for the Raspberry Pi to pull received packets from the µC
  • preferably an extensible design, so that we can add more features later, such as setting up the radio, and also send outgoing packets

The LPC810 will have two tasks: 1) listen and respond to incoming I2C requests, i.e. acting as an I2C slave, and 2) collect incoming packets from the radio, buffering them until they can be sent over I2C. Again, since it’s a slave device, it has no control over when this is.

The RF69 driver has already been presented, so the major pieces we still need to write are the I2C slave logic, and some sort of buffering. Let’s start with the buffering part first:

class PacketBuffer {
    static const int limit = 500;
    uint8_t buf [limit+4+66]; // slack for last packet
    uint16_t rpos, wpos;
public:
    PacketBuffer () : rpos (0), wpos (0) {}
    void append (const void* ptr, int len) {}
    int peek () const { return rpos != wpos ? buf[rpos] : -1; }
    const void* next () const { return buf + rpos + 1; }
    void remove () { rpos += peek() + 1; if (rpos >= limit) rpos = 0; }
};

PacketBuffer pb;

This is a densely-coded example of how to implement a circular buffer in object-oriented C++. Let’s go through this code in detail, just to get to grips with it:

  • the packet buffer should be able to hold at least 500 bytes of data
  • there is extra slack at the end, due to the way “append” is implemented
  • it has a read and a write position, both as index into that buffer
  • the constructor initialises these two positions to zero
  • there’s an “append” method, to add a packet as a specified number of bytes
  • the “peek” method returns the size of the next packet we can pull out
  • the “next” method returns a pointer to the data we can pull out (or zero)
  • the “remove” method will drop the extracted data, advancing to the next
  • the very last line declares an actual packet buffer, called “pb”

Apart from the “append” method, which is more complex, all the code is done.

Here’s how it can be used inside the main loop:

struct {
    uint8_t when, rssi;
    uint16_t afc;
    uint8_t buf [66];
} entry;

int n = rf.receive(entry.buf, sizeof entry.buf);
if (n > 0) {
    entry.when = 0; // not used yet
    entry.rssi = rf.rssi;
    entry.afc = rf.afc;
    pb.append(&entry, 4 + n); // also adds: when, rssi, and afc
}

Note how we’ve encapsulated all the decisions about how to deal with packet buffering in a class designed specifically for that purpose, so the main code can be really simple and tidy.

The other part of the design is about acting as I2C slave and responding to requests coming in from the I2C bus, and somehow making it all work together. There are two alternatives here: polling the I2C hardware or setting up the I2C slave to use interrupts. The latter makes it easier to service incoming requests at any time, but interrupts are also more complicated to debug and get right. There are lots of tricky race-conditions to avoid.

So let’s go for the polled approach, and come up with a class to clarify the request logic:

class RequestHandler {
    uint32_t i2cBuf [24];
    I2C_HANDLE_T* ih;
public:
    RequestHandler (uint8_t address) {}
    bool wantsData () const { return false; }
    void replyWith (const void* ptr, int len) {}
};

RequestHandler rh (0x70);

Again, some additional notes:

  • the i2cBuf and ih data members are needed by the LPC810’s ROM driver
  • the constructor sets things up as I2C slave with the specified address
  • the wantsData member needs to implement our main polling code
  • the replyWith member provides the next packet to send, if we have any
  • the last line declares a request handler, called “rh”, listening to I2C address 0x70
  • all the methods are empty stubs, i.e. this code compiles, but it does nothing

Note that we have not implemented this code yet, we’ve merely designed an API, but this is nevertheless sufficient to write down the entire main loop of our program:

while (true) {
    int n = rf.receive(entry.buf, sizeof entry.buf);
    if (n > 0) {
        // ... same as before
    }
    if (rh.wantsData()) {
        rh.replyWith(pb.next(), pb.peek());
        pb.remove();
    }
}

Not coincidentally, all the calls we’ve defined so far are being used. It’s always a good idea to start implementing only what you need right away. We can always add features later.

An important – perhaps not so obvious – property of this design is that the PacketBuffer and RequestHandler classes are independent of each other: neither one knows about the other. They are “decoupled”, with all interaction taking place in the main loop. This makes things easier to test, and simplifies re-use if we ever need some of this functionality again.

As you can see, the code so far does indeed compile and the RAM use seems to be ok:

$ arm-none-eabi-size firmware.elf
   text    data     bss     dec     hex filename
   1228       8     692    1928     788 firmware.elf

But it won’t do anything useful: the I2C slave has not been set up yet, and all the received packets are being thrown away by the packet buffer’s dummy “append” method.

The complete implementation is available on GitHub, but the main point here is to show how you can go about designing software which is not quite trivial, without having to think about all the details right away. Even though we’ve ignored lots of low-level stuff, we have been writing real code which will become part of the real system. This approach allows us think about the bigger picture first, instead of coming up with all sorts of working snippets which in the end are not doing what we were after. This is called top-down design.

To be continued…

[Back to article index]