We use a very practical I2C library on several families of MCUs. It gives access to the basic phases of an I2C exchange, which then makes it possible to perform higher level functions. So we decided to start from this base to create an easy-to-use I2C library for STM32H7xx.
This library can also be used with STM32L4+ which have the same I2C interface.
The API specification
We’ll focus here on the master API, although the slave API also exists. The functions of this API can be described as follows in pseudo code:
i2cConfigure (interface, speed, & hdl) i2cMasterStart (hdl, slaveAddress, direction) i2cMasterXfer (hdl, buffer, size) i2cMasterStop (hdl)
The implementation uses a few fundamental principles:
- The use of interrupts.
- The i2cMasterStart() and i2cMasterXfer() functions which take the most time are not blocking.
- Non-blocking functions use a signal at the end to wake up the task which started them and which is put sleeping in order to free the CPU for other tasks.
- The independence of the functions allows them to be combined to carry out sophisticated exchanges.
Example of using the API
Here is a simple example of a protocol to implement:
- Each message consists of a header and a body.
- The header contains the size of the body.
Why ? If the master is to receive a message from the slave, he must know its size, since he is the master of the clock and therefore of the number of bytes to be received.
The pseudo code of this reception function, omitting the error handling:
i2cConfigure (interface, speed, & hdl) i2cMasterStart (hdl, slaveAddress, I2C_RX) wait () i2cMasterXfer (hdl, & header, sizeof (header)) wait () i2cMasterXfer (hdl, pBuffer, header.size) wait () i2cMasterStop (hdl)
We wanted to port this simple API to the STM32H7xx MPU family. It turned out that this porting was problematic.
Note that in the desired API, the phases of the I2C protocol are separate and independent from each other: START – XFER – STOP.
It turns out that the I2C interface of the STM32H7 has automatic mechanisms that do not allow these phases to be generated independently of each other. For example, during a byte transfer phase, you must indicate BEFORE performing the phase which will be used AFTER. In fact, the I2C protocol allows after a transfer to carry out either another transfer, either a Stop or a ReStart.
This limitation of the interface is due to the fact that the master mode of the interface is designed for block transfer, and it is necessary to specify before the transfer the interrupt which will be generated at the end:
- TRC (Transfer Complete Reload), which only allows you to start another transfer.
- TC (Transfer Complete), which only allows to continue with a Stop or a ReStart.
The interest of these two separate interruptions is not clear, but the disadvantages are!
Before transferring a block, you must know whether it is the last one or not. This is not provided for in the API used. It was therefore necessary to introduce an additional parameter in the i2cMasterXfer() function to provide it with this information, which therefore becomes:
i2cMasterXfer (hdl, buffer, size, nextOrLast)
The nextOrLast parameter takes one of two values:
I2C_NEXT for all transfers except the last before Stop or ReStart.
I2C_LAST for the last transfer before Stop or ReStart.
In the I2C interface of the STM32H7 family, the Start phase is not separated from the transfer phase. When the master receives a message, the first interruption corresponds to:
NACK The slave did not respond to the Start condition.
TXIS The first byte to be transmitted can be supplied (if transmission). In this direction (transmission by the master), there is no problem since the 1st byte does not have to be provided during the Start phase.
RXNE The first byte has been received and is available (if reception). Therefore the transfer must be configured at the same time as the Start condition. But in our API the reception buffer and the next / last information are only available in the i2cMasterXfer () function.
There is therefore a problem in receiving a message. The solution is to configure the transfer of 1 byte during the Start phase, which will generate a TRC interrupt. Then during the transfer phase this byte will be read, the rest of the message will be received with size-1, and the appropriate TC or TRC interrupt will be generated.
However if the message size is only 1 byte, you must specify during the Start that the end of the transfer generates the TC interrupt and not TCR. In this case, you must therefore inform the i2cMasterStart() function that the message is only one byte.
It was therefore necessary to introduce an additional parameter in the i2cMasterStart() function to provide it with this information. The function prototype becomes:
i2cMasterStart (hdl, slaveAddress, directionAndLast)
The directionAndLast parameter includes a combination of flags:
I2C_RX or I2C_TX To indicate the direction of the transfer.
I2C_1LAST If the message contains only one byte. Useful only if the transfer is a reception by the master.
The I2C interface of the STM32H7xx family cannot manage the timings of the I2C protocol itself. Therefore it depends on the value of the TIMING register which must be initialized by the application.
The reference manual is confused about this, and it recommends using STM32CubeMX to calculate this value. The LL or HAL libraries provided by STMicroelectronics do not provide a function to calculate this value. So it is not possible to configure the speed of an I2C bus dynamically.
To overcome this problem a timing calculation function is proposed, but it does not give the same values as STM32CubeMX.
It was therefore necessary to introduce an additional parameter in the i2cConfigure() function to provide it with the timing to be used. It therefore becomes:
i2cConfigure (interface, speed, timing, & hdl)
The port is effective and the API is fully functional.
However, to adapt the API to the I2C interface of the STM32H7xx family, it was necessary to modify the specification of practically all the functions, which makes it less intuitive.
The functions of this API can be described as follows in pseudo code:
i2cConfigure (interface, speed, timing, & hdl) i2cMasterStart (hdl, slaveAddress, directionAndLast) i2cMasterXfer (hdl, buffer, size, nextOrLast) i2cMasterStop (hdl)
The real API is:
// Configuration API const i2cDesc_t * i2cGetDesc (uint32_t i2cId) ; uint32_t i2cTiming (const i2cDesc_t * pI2cDesc, uint32_t speed, uint32_t maxEdge) ; i2cStatus_t i2cConfigure (const i2cDesc_t * pI2cDesc, uint32_t timing, uint32_t signal, i2cHandle_t * pHdl) ; void i2cDeactivate (i2cHandle_t hdl) ; void i2cReset (i2cHandle_t hdl) ; i2cStatus_t i2cResult (i2cHandle_t hdl, uint32_t * pSize) ; // Master API i2cStatus_t i2cMasterStart (i2cHandle_t hdl, uint32_t slaveAddress, i2cFlags_t direction) ; i2cStatus_t i2cMasterStop (i2cHandle_t hdl) ; i2cStatus_t i2cMasterXfer (i2cHandle_t hdl, uint8_t * pBuffer, uint32_t numByte, i2cFlags_t nextOrLast) ; i2cStatus_t i2cMasterProbeDevice (i2cHandle_t hdl, uint32_t slaveAddress) ; // SLAVE API i2cStatus_t i2cSlaveInit (i2cHandle_t hdl, uint32_t ownAddress1) ; i2cStatus_t i2cSlaveStart (i2cHandle_t hdl, uint32_t flags) ; i2cStatus_t i2cSlaveXfer (i2cHandle_t hdl, uint8_t * pBuffer, uint32_t numByte, i2cFlags_t nextOrLast) ;
The I2C interface of the STM32F1 and F4 families is particularly difficult to use because of timing problems (it is necessary to inhibit interruptions during certain operations) and the special cases of the last 3 characters of each message.
The hope was that newer families would implement a more advanced interface. This is the case with the implementation of a counter for transfers by DMA. However, the use of interruption is still full of “features” which make complex the porting of existing applications from the STM32 families and others.
STM should really involve more software engineers in the design of the hardware peripherals. This would provide peripherals that are easier to use, and with the features that are commonly found in MCU of other providers.
You can find the i2cbasic library for STM32H7xx in this Eclipse project.