Skip to content

Understanding the QMK firmware for the IO Expander

Pierre Chevalier edited this page Apr 26, 2021 · 2 revisions

Code that just works

- What's better than bad code?
- Documented bad code :)

The QMK firmware for the IO/Expander can feel a little bit magical. It was written by taking inspiration from some other similar projects and tweaked to the point of just working. For this reason, it inherited some performance tweaks that make it hard to read and hard to adapt to a different situation.

It may be a good idea to refactor this code at some point, to be more generic. Ideally, most of the code in matrix.c wouldn't be needed and instead, the existing generic code under quantum would be used: https://github.com/qmk/qmk_firmware/blob/master/quantum/matrix.c

Until we get there, here is my best shot at unraveling the hard to parse code to help other keyboard designers get their own code to the "just works" state too.

Understanding what we want to do at a high level

At a high level, we want to use the row pins to send a signal by changing the voltage on one row pin (write), then read the column pins to detect which pins register the new voltage, or in other words, which switches were depressed, connecting this row to those columns. On the Ferris, the row pins are GPB0, GPB1, GPB2 and GPB3. The col pins are GPA0, GPA1, GPA2, GPA3 and GPA4.

Configuring the address pins

To allow for multiple (up to 8) micro-controllers to co-exist on the same i2c bus, the chip has 3 address pins that can be connected either to GND or to VCC. In firmware, this is configured at this line: https://github.com/qmk/qmk_firmware/blob/2f47bafd6a9c648daa4cfc11d5b1f15e32be3fc8/keyboards/ferris/0_2/matrix.c#L44. See the comment above for a little explanation: The three last bits of that number represent A2, A1 and A0 respectively.

If all your address pins are connected to GND, you can use the same line. If not, you'll need to adjust.

Initialization

At initialization, we tell the chip which pins you would like to use for reading and which pins you would like to use for writing. There are different situations depending on the pull-up, no pull-up, internal pull-up that I don't exactly understand. If your hardware is significantly different from mine in terms of pull up situation, I think this page would be a good one to read first: https://tasmota.github.io/docs/MCP230xx/.

The following explanations are assuming my comments are correct. These come from a mix of my reading previous comments in existing firmware and trying to trust it, then experimenting until the code worked for me and writing down what I thought was happening. There could be errors, so please take with a grain of salt, but this is what I think we're saying.

https://github.com/qmk/qmk_firmware/blob/2f47bafd6a9c648daa4cfc11d5b1f15e32be3fc8/keyboards/ferris/0_2/matrix.c#L84

We are preparing to send 3 ints to the MCP chip. The first one: IODIRA means: listen, I'll tell you how I plan to use each pin on GPIOA and GPIOB. The second one represents the 8 pins of GPIOA, one bit per pin. The rightmost bit represents pin 0. The third one represents the 8 pins on GPIOB, one bit per pin. The rightmost bit represents pin 0.

A pin that we plan to use for writing (or driving in other words) should get a 0. A pin that we want to use for reading or that we don't plan to use should get a 1.

In our case, we are saying: Pins 0-4 on GPIOB are meant to be used for writing. All other pins will be used for reading or won't be used.

https://github.com/qmk/qmk_firmware/blob/2f47bafd6a9c648daa4cfc11d5b1f15e32be3fc8/keyboards/ferris/0_2/matrix.c#L95 We are preparing to send 3 ints to the MCP chip. The first one: GPPUA means: listen, I'll tell you which pins should have pull-up enabled, and which shouldn't. The second one represents the 8 pins of GPIOA, one bit per pin. The rightmost bit represents pin 0. The third one represents the 8 pins on GPIOB, one bit per pin. The rightmost bit represents pin 0.

From my understanding, pins used for writing should have pull up disabled so get a zero; other pins should have pull-up enabled, so get a 1.

Writing to a row (in select_row)

For now, we'll only focus on the part that's relevant to the IO/Expander. The code for the MCU is a less generic version of what's defined in quantum/matrix.c.

https://github.com/qmk/qmk_firmware/blob/2f47bafd6a9c648daa4cfc11d5b1f15e32be3fc8/keyboards/ferris/0_2/matrix.c#L251

Here, what we do is a bit magical; but hopefully, the comment can guide us towards an understanding of what's happening. We want to select the row with index: row. That means, we want to ask the chip to send a high voltage pulse to the pin that matches this row. To do this, we send two ints. The first one selects which pin we want to write to. In our case, only GPIOB as that's where all the rows are. The second one indicates which pin we want to write to. A 0 represents a write and a 1 represents an ignored pin. We need to construct a byte where only the bit for the row is a 0. As our pins are contiguous, we can afford to do a bit of bit magic and save ourselves a few if statements. A more general version of this would look like this:

if row == 0 {
    port = MCP23017_GPIOA;
    row_byte = 0b11011111;
} else if row == 1 {
    port = MCP23017_GPIOB;
    row_byte = 0b11111101;
}

where the port and row_byte would effectively describe the pinout of the IO/Expander. In this example, row 0 would be pin A5 and row 1 would be pin B1.

Reading the columns that were depressed, in read_cols

https://github.com/qmk/qmk_firmware/blob/2f47bafd6a9c648daa4cfc11d5b1f15e32be3fc8/keyboards/ferris/0_2/matrix.c#L196

We select the GPIO port we want to read. In our case, all columns are on GPIOA, so we select that.

https://github.com/qmk/qmk_firmware/blob/2f47bafd6a9c648daa4cfc11d5b1f15e32be3fc8/keyboards/ferris/0_2/matrix.c#L204

We ask the IO/Expander to populate one byte of data called data on which each bit represent the state of one of the pins on the selected port. Here, receiving 11101111 would mean that the switch intersecting row A4 and the last col sent was depressed, so we got the 0 that was sent by the last send_cols.

The return value of this function expects a 1 per depressed pin and 0 everything else, hence the bitwise or operation here: https://github.com/qmk/qmk_firmware/blob/2f47bafd6a9c648daa4cfc11d5b1f15e32be3fc8/keyboards/ferris/0_2/matrix.c#L205

This line is simple because the columns are sequentially ordered from pin 0 up in our case. For a more general use, one would need to map the pin to its column here, which is more or less what's being done in the more general code on the MCU: Read the content of this if branch for inspiration on how to do that: https://github.com/qmk/qmk_firmware/blob/2f47bafd6a9c648daa4cfc11d5b1f15e32be3fc8/keyboards/ferris/0_2/matrix.c#L176