I2C protocol, STM32 programming with RTC module DS3231.

The I2C (Inter-Integrated Circuit) protocol allows devices to communicate with each other over a single data line, with one device acting as the Master and other devices acting as Slaves. In this guide, we will learn how to set up an I2C device to operate as a Slave and send data to the Master.

We will use two popular microcontrollers, ESP32 and Arduino, as examples, but the general principles can be applied to other microcontrollers with necessary modifications.

I2C Slave Data Transmission Guide to Master

1. Hardware Configuration

  • Master: Controls and requests data from Slave devices.
  • Slave: Provides data when requested.

This is the 6th tutorial in the STM32 I2C Slave tutorial series. This tutorial will cover how the Slave responds to a Read request from the master and sends the required number of bytes.

The I2C REGISTRS defined in the previous tutorial will act as memories. The Master will request data from these registers, and the Slave will respond by sending the corresponding data.

uint8_t I2C_REGISTERS[10] = {0,0,0,0,0,0,0,0,0,0};

The master can write a byte to a single register or multiple bytes starting from a specific register.
We will continue this tutorial from where we left off in PART 5 of this series. Most of the code will remain the same as in PART 5 and we will just add Data Transfer to it.
i2cslave 1

How to Set Up CubeMX 

Above shows the configuration for I2C1
– The mode is set to standard mode with a clock rate of 100000 Hz
– Clock No Stretch mode is disabled, which means Clock stretching is enabled.
 – The length of the Slave address is 7 bits and the address for the device is set to 0x12 (7 bits)
– I2C STM32 is capable of acting as 2 different Slave devices with 2 different addresses, but it is disabled and there is only 1 Slave device.
– We will learn more about Clock stretching and common calling address detection in upcoming tutorials.
i2cslave 1

We also need to enable Event Interrupts and Error Interrupts in the NVIC Tab

The pinout is shown below.
Pin PB6 is the SCL (Clock) pin and should be connected to the master’s SCL pin. Pin PB7 is the SDA (Data) pin and should be connected to the master’s SDA. If you are connecting 2 similar MCUs, you can connect the same pins together. For example, PB6 -> PB6 and PB7 -> PB7.

Some information about the SOURCE CODE

We have created separate files to write source code for I2C Slave. We will modify these files again.
The i2c_slave.c file is located in the src folder and the i2c_slave.h file is located in the inc folder. The image is shown below.
The main functionality remains the same. We put the I2C in Listen mode.

HAL_I2C_EnableListen_IT(&hi2c1);

callback code
The changes will be made in the slave source file. They are as follows

uint8_t bytesTransd = 0;
uint8_t txcount = 0;
uint8_t startPosition = 0;

void HAL_I2C_AddrCallback(I2C_HandleTypeDef *hi2c, uint8_t TransferDirection, uint16_t AddrMatchCode)
{
if (TransferDirection == I2C_DIRECTION_TRANSMIT) // if the master wants to transmit the data
{
RxData[0] = 0; // reset the RxData[0] to clear any residue address from previous call
rxcount =0;
HAL_I2C_Slave_Seq_Receive_IT(hi2c, RxData+rxcount, 1, I2C_FIRST_FRAME);
}

The Address Callback is called when the address sent by the master matches the slave address.
Here we will check whether the Master wants to write data or read data, using the TransferDirection variable.
If the Master wants to write (Transmit) data, we will start receiving data.
RxData[0] contains the location of the START REGISTER, where the master wants to read/write from. So we will reset this register to clear any old data left over due to errors.
The variable rxcount holds the position of the buffer, so we will reset it to 0. This will cause new data to start storing from the beginning of the RxData buffer.
Slave receives only 1 byte in interrupt mode, and Option is set to I2C_FIRST_FRAME.
This first byte will be the Start Register address that the master wants to read/write from.
The FIRST FRAME option allows to manage a sequence with a start condition, and is usually used when the slave receives the latest byte.
After the slave successfully receives 1 byte of data, the Rx complete callback is called.
A read request from master is made as follows:
START -> SLAVE ADDRESS (Write) -> REGISTER ADDRESS -> START -> SLAVE ADDRESS (Read) -> Receive data
When the master writes the Register address, it is stored in RxData[0]. When the master sends START followed by the Slave Address, the Address Callback is called again. This time the transmission direction will be set to receive and the else condition in the callback will be executed.

else
{
txcount = 0;
startPosition = RxData[0];
RxData[0] = 0; // Reset the start register as we have already copied it
HAL_I2C_Slave_Seq_Transmit_IT(hi2c, I2C_REGISTERS+startPosition+txcount, 1, I2C_FIRST_FRAME);
}
}

If the data transfer direction is RECEIVE, the child device needs to transmit data to the master device.
The txcount variable keeps track of the number of bytes transferred, so we’ll reset it to 0.
Extract the Register Address from the RxData buffer, and store it into the startposition variable.
Reset RxData[0] since we have occupied the Start Address.
Now the child device will Transmit only 1 byte in interrupt mode, and the Option is set to I2C_FIRST_FRAME
The FIRST FRAME option allows a chain to be managed with a start condition, and is typically used when the child device is transmitting the latest byte.
After 1 byte is transmitted, the Transmit Callback is called.

Callback

TxComplete Callback
void HAL_I2C_SlaveTxCpltCallback(I2C_HandleTypeDef *hi2c)
{
	txcount++;
	HAL_I2C_Slave_Seq_Transmit_IT(hi2c, I2C_REGISTERS+startPosition+txcount, 1, I2C_NEXT_FRAME);
}
Here we will increment the txcount variable to indicate that a new byte has been transmitted. This will also update the location of the data in the I2C REGISTRS.
Then transmit another byte with the NEXT FRAME option.
I2C_NEXT_FRAME implies that the slave is Transmitting this byte and is also ready to transmit the next byte.
Since the slave has no way of knowing how many bytes the master has requested, this callback will continue to run until the master sends a NACK response. This causes the Transmit callback to still be called even after the last byte of data has been transmitted. As a result, the txcount variable has a value 1 higher than the number of bytes of data that have actually been transmitted.
Since the master sends a NACK response while the slave is preparing to transmit the next byte, an AF (ACK Failure) error will be triggered in the slave. This will callback the error.
Here we will increment the txcount variable to indicate that a new byte has been transmitted. This will also update the location of the data in the I2C REGISTERS.
Then transmit another byte with the NEXT FRAME option.
I2C_NEXT_FRAME implies that the slave is Transmitting this byte and is also ready to transmit the next byte.
Since the slave does not know how many bytes the master has requested, this callback will continue to run until the master sends a NACK response. This causes the Transmit callback to be called even after the last byte of data has been transmitted. Therefore, the txcount variable has a value 1 higher than the number of bytes of data that have actually been transmitted.
i2cslave 3
Since the master sends a NACK response while the slave is preparing to transmit the next byte, an AF (ACK Failure) error will be triggered in the slave. This will invoke the error callback.
Callback Error
There are usually 2 types of errors triggered when the master receives data.
Bus Error (BERR) is triggered when the slave detects a misplaced start or stop condition.
Acknowledgement Failure (AF) error is triggered when the slave receives a NACK response during sending or receiving data.
We will first get the error code to check what error was generated.
error callback begins
void HAL_I2C_ErrorCallback(I2C_HandleTypeDef *hi2c)
{
	uint32_t errorcode = HAL_I2C_GetError(hi2c); Bây giờ chúng ta sẽ xử lý từng lỗi. Hãy bắt đầu với Lỗi không thể xác nhận. if (errorcode == 4) // AF error { if (txcount == 0) // error is while slave is receiving { bytesRrecvd = rxcount-1; // the first byte is the register address rxcount = 0; // Reset the rxcount for the next operation process_data(); } else // error while slave is transmitting { bytesTransd = txcount-1; // the txcount is 1 higher than the actual data transmitted txcount = 0; // Reset the txcount for the next operation } }
If you remember from the previous tutorial, AF ERROR is also triggered during data reception. This happens when the master sends a stop condition while the slave is expecting a data byte.
So we have to do a check in the error callback function to make sure that the error is triggered during the data receiving or transmitting.
Checking can be done by checking the txcount value.
If txcount is 0, it means that the slave never transmitted any data. And hence the error must have occurred during the receive.
If txcount is not 0, it means that the slave has transmitted some data. And an error must have occurred during the transmission.
If AF Error occurs during data receiving, we will call the data handling function, Just like we did in the previous tutorial.
Before doing that, we’ll update the number of bytes received and reset rxcount, to prepare for the new data.
And if an AF Error occurs during transmission, we will simply update the number of bytes transmitted.
I mentioned above that the txcount value is 1 higher than the actual number of bytes transmitted, so we will decrement it by 1.
The HAL handles bus errors in the error handler function itself. The BERR part of the error handler is shown below.
/* I2C Bus error interrupt occurred —————————————-*/ if ((I2C_CHECK_FLAG(sr1itflags, I2C_FLAG_BERR) != RESET) && (I2C_CHECK_IT_SOURCE(itsources, I2C_IT_ERR) != RESET)) { error |= HAL_I2C_ERROR_BERR; /* Clear BERR flag */ __HAL_I2C_CLEAR_FLAG(hi2c, I2C_FLAG_BERR); /* Workaround: Start cannot be generated after a misplaced Stop */ SET_BIT(hi2c->Instance->CR1, I2C_CR1_SWRST); }
Here, the HAL clears the BERR flag and performs a software reset for I2C by setting the SWRST bit in the CR1 register. Below you can see the details about the SWRST bit.
As mentioned in the reference, we must ensure the I2C lines are released and the bus is free before resetting this bit.
To not complicate things too much, I simply re-initialize the peripheral in case of a BERR error. The code is shown below.
/* BERR Error commonly occurs during the Direction switch * Here we the software reset bit is set by the HAL error handler * Before resetting this bit, we make sure the I2C lines are released and the bus is free * I am simply reinitializing the I2C to do so */ else if ( errorcode == 1 ) // BERR Error { HAL_I2C_DeInit ( hi2c ) ; HAL_I2C_Init ( hi2c ) ; memset ( RxData , ‘\0’ , RxSIZE ) ; // reset the Rx buffer rxcount = 0 ; // reset the count }

Result

Below is an image showing the commands sent by the master and the response sent by the slave.
The RED box indicates that the master writes 10 bytes of data starting from register 0. The data is then stored in the I2C_REGISTERS array in the slave.
The BLUE box indicates that the master requests 4 bytes of data starting from register 5.
The slave sends data stored in 4 registers, starting from register 5.
The data is then received by the master.

i2cslave6 1

5. Check and Verify

  • Hardware Connection: Ensure correct connection of SDA and SCL pins between the Master and Slave, and that all devices share the same power source (GND).
  • Load Code and Run: Upload the source code to the respective microcontrollers, then check the data transmitted and received through the Serial Monitor.

Conclusion

  • By using the provided source code and instructions, you can set up an I2C Slave device to easily send data to the Master. This process involves hardware configuration, programming of the Slave and Master devices, and result verification. If you encounter any issues or have further questions, please let me know!

Leave a Reply

Click to scroll the page