STM32L4 I2C driver for FreeRTOS without HAL

I2C remains a popular communication interface between MCUs and all kinds of auxiliary chips like ADCs, digipots and GPIO expanders. I had to make a simple and universal driver for an upcoming STM32L432 project to control Microchip digipots. STM32 I2C peripheral is simple enough to use without the burden of HAL libraries, additionally I needed a custom driver because my application uses FreeRTOS.

This driver supports both sending and receiving data from most common I2C slaves. Very often an I2C slave has its own registers that can be read and written. A typical bus communication pattern for a write is: a start condition, slave address byte (with LSB set to zero indicating a write), target register address followed by data bytes. Often the slave automatically increments its internal register pointer so multiple registers can be written during the same transaction.

Reads require telling the device which register to read from. The typical pattern for a register read is: start condition, slave address byte (with LSB set to zero indicating a write), register address, repeated start condition (or a stop condition and a start condition), slave address byte (with LSB set to one indicating a read) followed by data bytes from the slave. The master switches from the master transmitter to master receiver role after sending the address byte indicating a read.

The only public functions are i2c_transaction() and i2c_init_once(). The main interface is i2c_transaction() which takes care of both writing and reading from a slave device (writes are always handled first). This function is blocking and uses FreeRTOS semaphore to synchronize with the interrupts and to allow usage from multiple RTOS tasks.. Only the beginning of a transaction is handled by this function. Everything else is done in the interrupt handler, so the driver uses 0% CPU time when waiting for the transaction to complete. I don’t see a need for using DMA in this case because all transfers are at most a couple bytes long, so power savings would be very small. There is a configurable timeout to recover if the slave device does not respond. The function returns true when the transaction was successful. The I2C peripheral is enabled and disabled for each transaction for maximum power saving. This code does not configure pin multiplexing. It has to be done separately. Breakpoint instructions are strategically placed to be triggered in the “can never happen” circumstances.

Things that may need customization are:

  • bus speed (TIMINGR register)
  • timeout
  • which I2C peripheral to use (requires changes to pin muxing, I2C1 name and interrupt vector names)
  • interrupt priorities (depending on FreeRTOS configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY setting)

I release this code into the public domain.