This post continues after Keyboard from scratch. We are on the same set of hardware as before. Starting point in code is commit bd6d3eeaad.

You may have noticed, that a main loop that simple sleeps until we get an interrupt isn’t very “interrupt based execution”. So this time, we’ll take a look into getting at least the setup step of our mcp23017 interrupt based. Simply because it’s the easiest starting point for now.

Bunch of Plumbing

Since I2C only allows for a singular device to communicate with the controller at a given time, we have to do time based multiplexing. OTOH this allows us to just pass through any event of the I2C hardware to a device specific handler relatively unfiltered.

To provide such a device specific handler, we need to know which devices we have, and which handler they use. Also allow them to have device specific storage.

The struct i2c_device can be used to chain configuration sequences on startup and later hand interrupt events (currently event interrupt only) to our device. Since every device needs a bus address, we can store it in here as well. It’s only 7 or 10 bit in practice, but the API takes a uint32_t and we aren’t terribly memory constrained.

struct i2c_device {
    void (*interrupt)(uint32_t bus, struct i2c_device *device);
    void (*configure)(uint32_t bus, struct i2c_device *device);
    uint32_t address;
};

this structure will then be instantiated for specific devices by having it inside a larger container and calculating the base address of the container (containing device specific data) from there.

For the MCP23017 my current struct is still pretty simple, we only store an enum of configuration state and it got buffer/state storage prepared to add interrupt based read interaction later on.

struct mcp23017_state {
    struct i2c_device device;

    uint8_t configured;
    uint8_t read16;
    uint8_t read8;
};

On the I2C bus level code, we don’t have to do much. Just add the IRQ handler and add some space where we store the bus-level tracking which device we should currently send events to.

void I2C0_EV_IRQHandler(void) __attribute__((used));

struct i2c_state {
    uint32_t device;

    struct i2c_device *devices[1];
} state = { 0 };

void I2C0_EV_IRQHandler(void) {
    if (state.device < sizeof(state.devices) / sizeof(state.devices[0])) {
        state.devices[state.device]->interrupt(I2C0, state.devices[state.device]);
}

Changes in main Function

There isn’t really much we have to change in the main function. State for interrupt state machine needs to be set up.

+    mcp23017_init_state(&mcp_state1, i2c_mcp23017_low);

And we need to add the device to the interrupt based handling.

+    //i2c_set_device(0, &mcp_state1.device);

This specific commit has it as comment, because the code didn’t work. But imagine this was in there, and the normal configure line commented out instead.

There’s also a new sleep in here delay_1ms(500);. This specific method to wait is only temporary to debug things. But it’s reuqired to wait until setup is done (asynchronously) before we continue to actually read from the mcp23017.

The State Machine

The actual meaty part here is the state machine in mcp23017 specific code, so let’s take a quick look what’s going on here.

static void handle_configure_int(uint32_t bus, struct mcp23017_state *state) is the main entry point for this state machine. Contrary to most iterative code it won’t block and return once it’s done, but needs to be called for every step of the machine. When it’s done, it will call i2c_device_configure_done(bus, &state->device) to report back. This can be used to synchronize on it. E.g. to configure the next device, or let main continue.

Our state machine is pretty simple. It only really got 3 inputs:

  • Address sent
  • Start sequence sent
  • Transmit Buffer Empty (TBE)

State is tracked via enum CONFIG_STEPS. The first two inputs will always go back to the current state, while TBE always advances it by 1.

Once we reached mcp_23017_config_done we tell the I2C bus-level code and exepct to no longer be called.

Kicking off the State Machine

To kick everything off, we first need to enable the relevant interrupts.

        i2c_interrupt_enable(I2C0, I2C_INT_BUF);
        i2c_interrupt_enable(I2C0, I2C_INT_EV);
        eclic_irq_enable(I2C0_EV_IRQn, 1, 1);

Initialize the state to our starting state, and then kick off the machinery. Kick off is simply i2c_start_on_bus(bus);. Done, we are running and will be triggered by hardware \o/.

Running a couple Steps

So now that we kicked of the hardware, we’ll get the first interrupt. Which is going to be of the “Start Sequence Sent” kind. In response, we just send the Address and exit the handler. Again, we’ll get a new event. This time it’s going to be “Address Sent” kind. So we clear the corresponding interrupt flag. After this flag is cleared (well, before. But more on that later) we’ll get another event with TBE.

When the transmit buffer is empty, we can submit a new byte to send out. Due to (IMO implementation detail) the shift buffer being seperate from our I2C_DATA register, which triggers TBE, we’ll get this twice in very rappid succession, then we’ll have to wait for the first byte to be transmitted before we get more interrupts and submit more bytes.

After we sent a sequence, we’ll send the Stop sequence on our bus. Sadly there’s no interrupt once that’s done, so we stay in the interrupt handler context until we can request the hardware to send our start sequence. Now we are back to the begging, just further along our state machine.

Ending the Machine

At the end, we turn of the interrupts again, since the current main code is not interrupt based, but just uses it to end the sleep.

        eclic_irq_disable(I2C0_EV_IRQn);
        i2c_interrupt_disable(I2C0, I2C_INT_BUF);
        i2c_interrupt_disable(I2C0, I2C_INT_EV);

So, in the feedback handler of our setup sequence, we do just that. Turn of the interrupt on both the I2C and the eclic level. Done

Fixing the Code a Bit

If you’ve looked ahead in the log, you will probably have seen, that I needed another quick commit to fix up the code and let it actually run.

The issue was simply that the hardware already sets the TBE interrupt flag when we only want to handle the ADDSEND one. And while the ADDSEND bit is still set, writing to the register apparently does nothing. I’m not sure why, but switching around the order a bit fixed things up, and we are golden.

The series continues in Adding more Bits.