GPIO expander in a challenging environment - SX1509
November 17, 2023

In embedded software, lighting LEDs is a simple task that can be a challenge in projects with limited PCB space. The problem is the lack of microcontroller pins. The solution is GPIO expanders, which allow multiple I/O to be handled via serial interfaces like I2C.

The article discusses the challenges with latency in controlling LEDs through expanders and alternatives, such as dedicated LED drivers. An example is the SX1509 chip, which combines LED control and pushbutton operation.

INTRODUCTION
Lighting LEDs in embedded projects is often the first task for developers, which, while seemingly simple, can bring many challenges. Projects seek to maximize the number of peripherals connected to a single microcontroller, minimizing the need for code sharing and optimizing space on the printed circuit board (PCB). However, this configuration can lead to a lack of sufficient pins for all device functions.

The solution is GPIO expander chips, which allow numerous I/Os to be handled via serial interfaces like I2C, needing only two communication lines. However, this method has its challenges, including latency. Direct control of the microcontroller's GPIO pins is fast, which is evident when using logic state analyzers for code profiling. With port expanders, the time required for control is longer due to the need to serialize commands and process communications in memory, which can be a problem in some situations.

WHY?

In our project, we faced the challenge of handling multiple LEDs and several buttons using a port expander. Changing the microcontroller was not an option - we used the same MCU model in different devices, which made the implementation easier. Basic operation of the LEDs, such as turning them on and off, is relatively straightforward, but the task gets complicated when it is required to control the intensity of the light and implement smooth on and off effects.

Pulse-width modulation (PWM) is used to regulate the brightness of LEDs, which requires rapid switching of pin states at precise intervals. Techniques such as bit-banging, where the processor controls the state of a pin every time, are inefficient. Typically, counter circuits with PWM mode are used. However, when using a port expander, bit-banging becomes impossible due to the time-consuming nature of pin state changes, and the use of processor timers is not possible.

Dedicated LED drivers can be an alternative, but in our case this would require additional circuitry to operate the buttons. Fortunately, there are circuits that combine these functions. An example is the SX1509 from Semtech.

SX1509

The SX1509 chip has many features that were useful in our case:

  • Low operating voltage and low current consumption - our device runs on batteries so this feature allows the device to work longer;
  • Built-in LED driver with 256-step PWM and programmable flashing and "breathing" (fade-in/out effect);
  • Hardware debouncing of input pins - avoids additional filter elements on the PCB;
  • Built-in oscillator, so there is no need to generate a separate clock signal for the circuit;
  • Dedicated interrupt line - allows asynchronous reading of button changes instead of continuous polling;

Other interesting features that were not crucial in this case:

  • Pins that tolerate 5V - this allows you to simplify the electronics when dealing with logic at a higher voltage than the rest of the circuit;
  • Independent voltages on the pin banks - so the circuit can be used as a translator of voltage levels;
  • Built-in button array scanning circuitry - in situations where the number of buttons is greater than the number of expander pins;

IMPLEMENTATION

The SX1509 communicates with the processor via the I2C bus, using a standard protocol. The process involves sending the address of the device (SX1509) and the address of the register to which you want to write or from which you want to read data, and then sending or receiving the data.

When developing a universal driver for a port expander, it is crucial to develop an efficient way to write and read the states of GPIO pins. The most basic method involves sending write or read commands to the expander each time, as illustrated by the pseudocode below. This pseudocode is simplified and omits a number of details, such as error handling, avoiding configuring inputs in a different mode, or using an asynchronous API.


typedef uint16_t sx1509_channels_t; void sx1509_write(sx1509_channels_t state) { uint8_t buf[3] = {SX1509_REG_DATA_B, BYTE(1, state), BYTE(0, state)}; sx1509_i2c_write_start(buf, sizeof(buf)); sx1509_i2c_wait(); } sx1509_channels_t sx1509_read(void) { uint8_t tx[1] = {SX1509_REG_DATA_B}; uint8_t rx[2] = {0}; sx1509_i2c_read_start(tx, sizeof(tx), rx, sizeof(rx)); sx1509_i2c_wait(); return (rx[0] << 8) | rx[1]; }

However, it is very easy to optimize our controller by storing a copy of the status of the expander's outputs in the memory of our processor, such as.

static struct { sx1509_channels_t outputs; sx1509_channels_t inputs; } sx1509_state; void sx1509_set_output(sx1509_channels_t state, sx1509_channels_t mask) { state = (sx1509_state.outputs & ~mask) | (state & mask); uint8_t buf[3] = {SX1509_REG_DATA_B, BYTE(1, state), BYTE(0, state)}; sx1509_i2c_write_start(buf, sizeof(buf)); sx1509_i2c_wait(); state.outputs = state; } sx1509_channels_t sx1509_read(void) { return sx1509_state.inputs; } void sx1509_on_interrupt_pin(void) { uint8_t tx[1] = {SX1509_REG_DATA_B}; uint8_t rx[2] = {0}; sx1509_i2c_read_start(tx, sizeof(tx), rx, sizeof(rx)); sx1509_i2c_wait(); sx1509_state.inputs = (rx[0] << 8) | rx[1]; }

In our case, we use the configurability of the SX1509 chip to set the state of the interrupt pin when the state of the inputs changes. In the process of writing, we only update our local copy of the pin state, and when reading, we refer to this stored state. Updating the state of the inputs is done asynchronously, responding to changes in the SX1509 interrupt pin.

With this simple modification, we achieve several benefits:

  • Immediate reading of pin status, as their current state is always stored in memory.
  • There is a masking option in our recording API, which allows you to choose which outputs you want to set.
  • We add a "toggle" function, which allows setting opposite values on the outputs without the need for additional pin state reading via I2C.

While this approach may seem obvious to experienced programmers, it is always worth paying attention to such details, which can significantly facilitate the implementation of our drivers.

SUMMARY

In the embedded world, even tasks as simple as controlling LEDs can lead to numerous challenges and decisions. In our case, the need for a GPIO port expander collided with the need for advanced LED and input handling. With the SX1509 chip, we were able to avoid the need for several separate devices, while relieving the load on the main processor.

However, each case requires individual evaluation. If the priority is to handle pin status quickly, consider using high-frequency SPI-controlled circuits or adjust the design so that microcontroller pins can be used directly. The amount of time required to implement a controller for a given circuit is also an important factor. Developing a universal SX1509 driver with an asynchronous API, with thread safety in mind, can require significant effort.

REFERENCES