M0AGX / LB9MG

Amateur radio and embedded systems

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)
  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 public domain.