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)

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)

i2c.c #include <FreeRTOS.h> #include <stm32l432xx.h> #define TRANSACTION_TIMEOUT_ms 100 static SemaphoreHandle_t _task_mutex; //this one is used to block calls from different tasks static SemaphoreHandle_t _semaphore; //this one is used to synchronize task context with ISR static volatile bool _failure_flag; static volatile const uint8_t *_tx_data_ptr; static volatile uint8_t *_rx_data_ptr; static volatile uint8_t _tx_data_length; static volatile uint8_t _rx_data_length; static uint8_t _address; void i2c_init_once(void){ static StaticSemaphore_t mutex_buffer; _task_mutex = xSemaphoreCreateMutexStatic(&mutex_buffer); static StaticSemaphore_t semaphore_buffer; _semaphore = xSemaphoreCreateBinaryStatic(&semaphore_buffer); } static void i2c_acquire(void){ xSemaphoreTake(_task_mutex, portMAX_DELAY); } static void i2c_release(void){ xSemaphoreGive(_task_mutex); } static void i2c_init(void){ taskENTER_CRITICAL(); RCC->APB1ENR1 |= RCC_APB1ENR1_I2C1EN_Msk; //enable clock to the I2C peripheral taskEXIT_CRITICAL(); I2C1->CR1 = 0; //disable everything I2C1->CR2 = 0; //relaxed timing, adjust for your bus frequency I2C1->TIMINGR = (0x03 << I2C_TIMINGR_PRESC_Pos) | (0xC7 << I2C_TIMINGR_SCLL_Pos) | (0xC3 << I2C_TIMINGR_SCLH_Pos) | (0x02 << I2C_TIMINGR_SDADEL_Pos) | (0x04 << I2C_TIMINGR_SCLDEL_Pos) ; I2C1->CR1 = I2C_CR1_ERRIE_Msk/*enable error interrupts*/ | //I2C_CR1_TCIE_Msk/*enable transmit complete interrupt*/ | I2C_CR1_RXIE_Msk/*enable receive complete interrupt*/ | I2C_CR1_NACKIE_Msk/*enable NACK interrupt*/ | I2C_CR1_PE_Msk /*enable peripheral*/; NVIC_EnableIRQ(I2C1_EV_IRQn); NVIC_SetPriority(I2C1_EV_IRQn, 6); //very low priority NVIC_EnableIRQ(I2C1_ER_IRQn); NVIC_SetPriority(I2C1_ER_IRQn, 6); //very low priority } static void i2c_deinit(void){ I2C1->CR1 = 0; //disable everything NVIC_DisableIRQ(I2C1_EV_IRQn); NVIC_DisableIRQ(I2C1_ER_IRQn); taskENTER_CRITICAL(); RCC->APB1ENR1 &= ~RCC_APB1ENR1_I2C1EN_Msk; //disable clock to the I2C peripheral taskEXIT_CRITICAL(); } bool i2c_transaction(uint8_t address, uint8_t tx_length, const uint8_t *tx_data, uint8_t rx_length, uint8_t *rx_data){ i2c_acquire(); i2c_init(); xSemaphoreGive(_semaphore); _failure_flag = false; _address = address << 1; if (tx_length){ _tx_data_length = tx_length; _tx_data_ptr = tx_data; } else { _tx_data_length = 0; _tx_data_ptr = NULL; } if (rx_length){ _rx_data_length = rx_length; _rx_data_ptr = rx_data; } else { _rx_data_length = 0; _rx_data_ptr = NULL; } I2C1->ICR = 0xFFFFFFFF; //clear all interrupt flags //Writes are always done first, reception is then done by the interrupt handler if (_tx_data_length){ I2C1->CR2 = (_tx_data_length << I2C_CR2_NBYTES_Pos) | /*I2C_CR2_RD_WRN_Msk*/ /*this is a write operation*/ (_address << I2C_CR2_SADD_Pos); //no autoend - ISR will decide to send a repeated start or stop if (xSemaphoreTake(_semaphore, pdMS_TO_TICKS(TRANSACTION_TIMEOUT_ms)) == pdFALSE){ //can't happen - the semaphore should be available at this point __BKPT(1); i2c_release(); return false; } I2C1->CR1 |= I2C_CR1_TXIE_Msk; //enable transmitter buffer empty interrupt I2C1->CR2 |= I2C_CR2_START_Msk /*send start condition*/; } else { //receive-only transaction I2C1->CR2 = (_rx_data_length << I2C_CR2_NBYTES_Pos) | I2C_CR2_RD_WRN_Msk /*read operation*/ | (_address << I2C_CR2_SADD_Pos) | I2C_CR2_AUTOEND_Msk; if (xSemaphoreTake(_semaphore, pdMS_TO_TICKS(TRANSACTION_TIMEOUT_ms)) == pdFALSE){ //can't happen - the semaphore should be available at this point __BKPT(1); i2c_release(); return false; } I2C1->CR2 |= I2C_CR2_START_Msk /*send start condition*/; } //rest of the transaction is done in the interrupt handler //wait until the ISR is done and unlocks the semaphore if (xSemaphoreTake(_semaphore, pdMS_TO_TICKS(TRANSACTION_TIMEOUT_ms))){ i2c_deinit(); i2c_release(); if (_failure_flag){ return false; } } else { //the ISR failed to complete everything in time i2c_deinit(); i2c_release(); return false; } i2c_release(); return true; } void I2C1_EV_IRQHandler(void){ //Flag checking order is important. if (I2C1->ISR & I2C_ISR_TXIS_Msk){ //can send more data I2C1->TXDR = *_tx_data_ptr; _tx_data_length--; _tx_data_ptr++; if (_tx_data_length == 0){ I2C1->CR1 &= ~I2C_CR1_TXIE_Msk; //transmitter buffer empty interrupt will no longer be needed I2C1->CR1 |= I2C_CR1_TCIE_Msk; //enable transmission complete interrupt } return; } if (I2C1->ISR & I2C_ISR_TC_Msk){ //last outgoing byte has been sent out if (_rx_data_length){ //start receiving I2C1->CR2 = (_rx_data_length << I2C_CR2_NBYTES_Pos) | I2C_CR2_RD_WRN_Msk /*this is a read operation*/ | (_address << I2C_CR2_SADD_Pos) | I2C_CR2_AUTOEND_Msk; I2C1->CR2 |= I2C_CR2_START_Msk /*send repeated start condition*/; } else { //end of transmission - send stop I2C1->CR1 &= ~I2C_CR1_TCIE_Msk; //disable transmission complete interrupt I2C1->CR1 |= I2C_CR1_STOPIE_Msk; I2C1->CR2 |= I2C_CR2_STOP_Msk; //send stop condition } return; } if (I2C1->ISR & I2C_ISR_STOPF_Msk){ //stop condition has been sent - this means end of the transaction I2C1->ICR = I2C_ICR_STOPCF_Msk; //clear flag I2C1->CR1 &= ~I2C_CR1_STOPIE_Msk; } if (I2C1->ISR & I2C_ISR_NACKF_Msk){ //NACK has been received I2C1->ICR = I2C_ICR_NACKCF_Msk; //clear flag _failure_flag = true; } if (I2C1->ISR & I2C_ISR_RXNE_Msk){ if (_rx_data_ptr){ *_rx_data_ptr = I2C1->RXDR; _rx_data_ptr++; _rx_data_length--; if (_rx_data_length == 0){ I2C1->CR1 &= ~I2C_CR1_RXIE_Msk; //disable reception complete interrupt I2C1->CR1 |= I2C_CR1_STOPIE_Msk; I2C1->CR2 |= I2C_CR2_STOP_Msk; //send stop condition } } return; //stop condition is automatically delivered by the hardware } //unblock the task BaseType_t xHigherPriorityTaskWoken; xSemaphoreGiveFromISR(_semaphore, &xHigherPriorityTaskWoken); portYIELD_FROM_ISR(xHigherPriorityTaskWoken); } void I2C1_ER_IRQHandler(void){ _failure_flag = true; BaseType_t xHigherPriorityTaskWoken; xSemaphoreGiveFromISR(_semaphore, &xHigherPriorityTaskWoken); portYIELD_FROM_ISR(xHigherPriorityTaskWoken); __BKPT(1); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 #include <FreeRTOS.h> #include <stm32l432xx.h> #define TRANSACTION_TIMEOUT_ms 100 static SemaphoreHandle_t _task_mutex ; //this one is used to block calls from different tasks static SemaphoreHandle_t _semaphore ; //this one is used to synchronize task context with ISR static volatile bool _failure_flag ; static volatile const uint8_t * _tx_data_ptr ; static volatile uint8_t * _rx_data_ptr ; static volatile uint8_t _tx_data_length ; static volatile uint8_t _rx_data_length ; static uint8_t _address ; void i2c_init_once ( void ) { static StaticSemaphore_t mutex_buffer ; _task_mutex = xSemaphoreCreateMutexStatic ( & mutex_buffer ) ; static StaticSemaphore_t semaphore_buffer ; _semaphore = xSemaphoreCreateBinaryStatic ( & semaphore_buffer ) ; } static void i2c_acquire ( void ) { xSemaphoreTake ( _task_mutex , portMAX_DELAY ) ; } static void i2c_release ( void ) { xSemaphoreGive ( _task_mutex ) ; } static void i2c_init ( void ) { taskENTER_CRITICAL ( ) ; RCC -> APB1ENR1 |= RCC_APB1ENR1_I2C1EN_Msk ; //enable clock to the I2C peripheral taskEXIT_CRITICAL ( ) ; I2C1 -> CR1 = 0 ; //disable everything I2C1 -> CR2 = 0 ; //relaxed timing, adjust for your bus frequency I2C1 -> TIMINGR = ( 0x03 << I2C_TIMINGR_PRESC_Pos ) | ( 0xC7 << I2C_TIMINGR_SCLL_Pos ) | ( 0xC3 << I2C_TIMINGR_SCLH_Pos ) | ( 0x02 << I2C_TIMINGR_SDADEL_Pos ) | ( 0x04 << I2C_TIMINGR_SCLDEL_Pos ) ; I2C1 -> CR1 = I2C_CR1_ERRIE_Msk /*enable error interrupts*/ | //I2C_CR1_TCIE_Msk/*enable transmit complete interrupt*/ | I2C_CR1_RXIE_Msk /*enable receive complete interrupt*/ | I2C_CR1_NACKIE_Msk /*enable NACK interrupt*/ | I2C_CR1_PE_Msk /*enable peripheral*/ ; NVIC_EnableIRQ ( I2C1_EV_IRQn ) ; NVIC_SetPriority ( I2C1_EV_IRQn , 6 ) ; //very low priority NVIC_EnableIRQ ( I2C1_ER_IRQn ) ; NVIC_SetPriority ( I2C1_ER_IRQn , 6 ) ; //very low priority } static void i2c_deinit ( void ) { I2C1 -> CR1 = 0 ; //disable everything NVIC_DisableIRQ ( I2C1_EV_IRQn ) ; NVIC_DisableIRQ ( I2C1_ER_IRQn ) ; taskENTER_CRITICAL ( ) ; RCC -> APB1ENR1 &= ~ RCC_APB1ENR1_I2C1EN_Msk ; //disable clock to the I2C peripheral taskEXIT_CRITICAL ( ) ; } bool i2c_transaction ( uint8_t address , uint8_t tx_length , const uint8_t * tx_data , uint8_t rx_length , uint8_t * rx_data ) { i2c_acquire ( ) ; i2c_init ( ) ; xSemaphoreGive ( _semaphore ) ; _failure_flag = false ; _address = address << 1 ; if ( tx_length ) { _tx_data_length = tx_length ; _tx_data_ptr = tx_data ; } else { _tx_data_length = 0 ; _tx_data_ptr = NULL ; } if ( rx_length ) { _rx_data_length = rx_length ; _rx_data_ptr = rx_data ; } else { _rx_data_length = 0 ; _rx_data_ptr = NULL ; } I2C1 -> ICR = 0xFFFFFFFF ; //clear all interrupt flags //Writes are always done first, reception is then done by the interrupt handler if ( _tx_data_length ) { I2C1 -> CR2 = ( _tx_data_length << I2C_CR2_NBYTES_Pos ) | /*I2C_CR2_RD_WRN_Msk*/ /*this is a write operation*/ ( _address << I2C_CR2_SADD_Pos ) ; //no autoend - ISR will decide to send a repeated start or stop if ( xSemaphoreTake ( _semaphore , pdMS_TO_TICKS ( TRANSACTION_TIMEOUT_ms ) ) == pdFALSE ) { //can't happen - the semaphore should be available at this point __BKPT ( 1 ) ; i2c_release ( ) ; return false ; } I2C1 -> CR1 |= I2C_CR1_TXIE_Msk ; //enable transmitter buffer empty interrupt I2C1 -> CR2 |= I2C_CR2_START_Msk /*send start condition*/ ; } else { //receive-only transaction I2C1 -> CR2 = ( _rx_data_length << I2C_CR2_NBYTES_Pos ) | I2C_CR2_RD_WRN_Msk /*read operation*/ | ( _address << I2C_CR2_SADD_Pos ) | I2C_CR2_AUTOEND_Msk ; if ( xSemaphoreTake ( _semaphore , pdMS_TO_TICKS ( TRANSACTION_TIMEOUT_ms ) ) == pdFALSE ) { //can't happen - the semaphore should be available at this point __BKPT ( 1 ) ; i2c_release ( ) ; return false ; } I2C1 -> CR2 |= I2C_CR2_START_Msk /*send start condition*/ ; } //rest of the transaction is done in the interrupt handler //wait until the ISR is done and unlocks the semaphore if ( xSemaphoreTake ( _semaphore , pdMS_TO_TICKS ( TRANSACTION_TIMEOUT_ms ) ) ) { i2c_deinit ( ) ; i2c_release ( ) ; if ( _failure_flag ) { return false ; } } else { //the ISR failed to complete everything in time i2c_deinit ( ) ; i2c_release ( ) ; return false ; } i2c_release ( ) ; return true ; } void I2C1_EV_IRQHandler ( void ) { //Flag checking order is important. if ( I2C1 -> ISR & I2C_ISR_TXIS_Msk ) { //can send more data I2C1 -> TXDR = * _tx_data_ptr ; _tx_data_length -- ; _tx_data_ptr ++ ; if ( _tx_data_length == 0 ) { I2C1 -> CR1 &= ~ I2C_CR1_TXIE_Msk ; //transmitter buffer empty interrupt will no longer be needed I2C1 -> CR1 |= I2C_CR1_TCIE_Msk ; //enable transmission complete interrupt } return ; } if ( I2C1 -> ISR & I2C_ISR_TC_Msk ) { //last outgoing byte has been sent out if ( _rx_data_length ) { //start receiving I2C1 -> CR2 = ( _rx_data_length << I2C_CR2_NBYTES_Pos ) | I2C_CR2_RD_WRN_Msk /*this is a read operation*/ | ( _address << I2C_CR2_SADD_Pos ) | I2C_CR2_AUTOEND_Msk ; I2C1 -> CR2 |= I2C_CR2_START_Msk /*send repeated start condition*/ ; } else { //end of transmission - send stop I2C1 -> CR1 &= ~ I2C_CR1_TCIE_Msk ; //disable transmission complete interrupt I2C1 -> CR1 |= I2C_CR1_STOPIE_Msk ; I2C1 -> CR2 |= I2C_CR2_STOP_Msk ; //send stop condition } return ; } if ( I2C1 -> ISR & I2C_ISR_STOPF_Msk ) { //stop condition has been sent - this means end of the transaction I2C1 -> ICR = I2C_ICR_STOPCF_Msk ; //clear flag I2C1 -> CR1 &= ~ I2C_CR1_STOPIE_Msk ; } if ( I2C1 -> ISR & I2C_ISR_NACKF_Msk ) { //NACK has been received I2C1 -> ICR = I2C_ICR_NACKCF_Msk ; //clear flag _failure_flag = true ; } if ( I2C1 -> ISR & I2C_ISR_RXNE_Msk ) { if ( _rx_data_ptr ) { * _rx_data_ptr = I2C1 -> RXDR ; _rx_data_ptr ++ ; _rx_data_length -- ; if ( _rx_data_length == 0 ) { I2C1 -> CR1 &= ~ I2C_CR1_RXIE_Msk ; //disable reception complete interrupt I2C1 -> CR1 |= I2C_CR1_STOPIE_Msk ; I2C1 -> CR2 |= I2C_CR2_STOP_Msk ; //send stop condition } } return ; //stop condition is automatically delivered by the hardware } //unblock the task BaseType_t xHigherPriorityTaskWoken ; xSemaphoreGiveFromISR ( _semaphore , & xHigherPriorityTaskWoken ) ; portYIELD_FROM_ISR ( xHigherPriorityTaskWoken ) ; } void I2C1_ER_IRQHandler ( void ) { _failure_flag = true ; BaseType_t xHigherPriorityTaskWoken ; xSemaphoreGiveFromISR ( _semaphore , & xHigherPriorityTaskWoken ) ; portYIELD_FROM_ISR ( xHigherPriorityTaskWoken ) ; __BKPT ( 1 ) ; }

I release this code into the public domain.