I provided a summary of the I2C protocol and the basic concept of a simple integrated system based on it in the previous article[3]. The implementation of the system in the microcontroller part is discussed in this article. The objective is to receive the target frequency from the computer through the I2C protocol and blink an LED at that frequency.
Every slave device connected to the I2C bus should have an address through which it can be communicated with. In our case, STM32 microcontroller is a slave and hence will be initialized by the following code.
I2C_HandleTypeDef hi2c1;
hi2c1.Instance = I2C1;
hi2c1.Init.OwnAddress1 = (16 << 1); // The address is stored in the bits [7:1]. So, shifting left once.
hi2c1.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT;
hi2c1.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE;
hi2c1.Init.OwnAddress2 = 0;
hi2c1.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE;
hi2c1.Init.NoStretchMode = I2C_NOSTRETCH_DISABLE;
STM32 is a sophisticated controller with multiple I2C hardware drivers and hence it is imperative that a specific one is clearly defined while initialization, which in our case is I2C1. I have chosen 16 as the address of the device, which is an arbitrary number. It can be any valid number between 8 and 127[2]. The datasheet of STM32[1] provides us with the register map of I2C address which contains the I2C_OAR1, which stands for I2C own address 1, STM32 supports dual addresses for an I2C device[1].
Figure 1: Register of I2C_OAR1 (I2C own address 1)
As you can see, the address of the I2C device can be set using the 10 bits [9:0] of OA1. This makes sense for a 10 bit address. However, we are using 7 bit address scheme. The addressing scheme being 10 bits and 7 bits are determined by the 10th bit, OA1MODE , of the register, I2C_OAR1, where 1 and 0 stands for 10 bits and 7 bits addressing respectively. If 7 bits addressing is used, the address has to be set in bits [7:1]. This is why the address is shifted to the left once as 16<<1, if anybody is wondering. All of these information is defined clearly in the datasheet[1], refer figure 2
Figure 2: Usage of Address bits
Now that the device has been initialized as an I2C slave, the communication has to be defined. According to the objective laid out in the introduction, the flow would be
Receive the target frequency as 8 bits.
Calculate the timer overflow based on the received target frequency
Assign the calculated timer overflow value to the respective register
Figure 3: I2C Communication initiation frames
On a hardware level, an interrupt is triggered when the device realizes that an I2C master is trying to initiate a communication with it. This hardware interrupt is mapped to the HAL function, HAL_I2C_AddrCallback. As discussed in the previous post[3], the address of the slave is followed by a single bit indicating if the master wants to read or write, refer fig 3. If the I2C master tries to send data, a function HAL_I2C_Slave_Sequential_Receive_IT is called, which stores the incoming data in a uint8_t pointer, i2c_rx_buffer. For what it’s worth, it can be a char pointer too.
uint8_t i2c_rx_buffer;
extern void HAL_I2C_AddrCallback(I2C_HandleTypeDef *hi2c, uint8_t TransferDirection, uint16_t AddrMatchCode)
{
if(TransferDirection == I2C_DIRECTION_TRANSMIT) // Master is looking to transmit data
{
HAL_I2C_Slave_Sequential_Receive_IT(hi2c, i2c_rx_buffer, 1, I2C_FIRST_AND_LAST_FRAME);
}
}
A reception callback function is mapped to HAL_I2C_SlaveRxCpltCallback, which gets called when the reception is complete. This function executes the flow defined in the beginning of this section.
extern void HAL_I2C_SlaveRxCpltCallback(I2C_HandleTypeDef *hi2c){
uint32_t received_number = (uint32_t)(i2c_rx_buffer[0]);
target_frequency = received_number;
timer_period = (HAL_RCC_GetHCLKFreq()/(prescaler * target_frequency*2)) - 1;
timer2.Instance->CNT = 0; // reset the value of the counter
timer2.Instance->ARR = timer_period; // writing to the autoreload register.
}
Merging the discussed code with the implementation of the blog post, Blink an LED like a real embedded systems engineer[4], would provide us with a system that receives an 8 bit value from an I2C master and blinks an LED at the frequency of the received 8 bit binary value. For example, if the device receives 00001010, the LED would blink at a frequency of 10 Hz, meaning the LED will turn on and off 10 times a second. All that is left for us to do is write a linux driver to send the target frequency as 8 bits through I2C protocol to the microcontroller, which will be discussed in the next one.
Cheers!
The code discussed in this article can be found in https://github.com/rocheparadox/simple-integrated-system
Medium article - https://medium.com/@rocheinside/get-your-linux-computer-to-communicate-with-a-microcontroller-part-2-microcontroller-6f0f02339688
[2] https://www.ti.com/lit/an/sbaa565/sbaa565.pdf
[4] https://www.linkedin.com/pulse/blink-led-like-real-embedded-systems-engineer-christopher-bisme