STM32 I2C Configuration using Registers
This is another tutorial in register based series for STM32, and today we will see how to work with the I2C.
I will cover both transmission and reception using the I2C and ofcourse the configuration will remain common in both the processes. So let’s start by configuring the microcontroller for the I2C
Configuration
Below are the steps shown for configuring the I2C in STM32F4
/**** STEPS FOLLOWED ************
1. Enable the I2C CLOCK and GPIO CLOCK
2. Configure the I2C PINs for ALternate Functions
a) Select Alternate Function in MODER Register
b) Select Open Drain Output
c) Select High SPEED for the PINs
d) Select Pull-up for both the Pins
e) Configure the Alternate Function in AFR Register
3. Reset the I2C
4. Program the peripheral input clock in I2C_CR2 Register in order to generate correct timings
5. Configure the clock control registers
6. Configure the rise time register
7. Program the I2C_CR1 register to enable the peripheral
*/
Let’s cover them one by one
1. Enable the I2C and GPIO CLOCKS
I2C CLOCK can be enabled in the RCC_APB1ENR Register
As you can see above, the I2C1 Enable Bit is 21st bit of the RCC_APB1ENR Register. And in order to enable the clock, we need to write a ‘1’ in this position
RCC->APB1ENR |= (1<<21); // enable I2C CLOCK
Similarly we will enable the GPIO clock also. I2C1 is connected to pins PB8 and PB9 and therefore we will enable the GPIOB clock in RCC_AHB1ENR Register
GPIOB Enable Bit is 1st bit of RCC_AHB1ENR Register
RCC->AHB1ENR |= (1<<1); // Enable GPIOB CLOCK
2. Configure the Pins for I2C
configuring the pins is divided to various steps
Select Alternate Function in MODER Register
The MODER Register can be used to configure the pins in different modes
As I am using Pins PB8 and PB9 for the I2C (Alternate functions), I need to set the Bits (17:16) = 1:0 for PB8 and Bits (19:18) = 1:0 for PB9.
GPIOB->MODER |= (2<<16) | (2<<18); // Bits (17:16)= 1:0 --> Alternate Function for Pin PB8; Bits (19:18)= 1:0 --> Alternate Function for Pin PB9
Select Open Drain Output
The OTYPER Register can be used to select the output type
To select the Pin as the output drain we need to write a ‘1’ in the 8th and 9th bits (PB8, PB9)
GPIOB->OTYPER |= (1<<8) | (1<<9); // Bit8=1, Bit9=1 output open drain
Select High SPEED for the PINs
The speed selection can be done using OSPEEDR Register
We will select the High Speed for both of our pins here Bits (17:16)= 1:1 (for PB8) and Bits (19:18)= 1:1 for PB9
GPIOB->OSPEEDR |= (3<<16) | (3<<18); // Bits (17:16)= 1:1 --> High Speed for PIN PB8; Bits (19:18)= 1:1 --> High Speed for PIN PB9
Select Pull-up for both the Pins
It’s better to use external pull up registers while using I2C, but just for the sake of this tutorial I am using internal pull-up resistors
The Internal Pull up registers can be controlled by using PUDPR Register
Inorder to enable the Pull-Up resistors, we need to write Bits (17:16)= 0:1 for PB8, and Bits (19:18)= 0:1 for PB9
GPIOB->PUPDR |= (1<<16) | (1<<18); // Bits (17:16)= 0:1 --> Pull up for PIN PB8; Bits (19:18)= 0:1 --> pull up for PIN PB9
Configure the Alternate Function in AFR Register
We have already set the pins in the alternate functions mode, but we haven’t defined what those functions should be. This can be done by modifying the AFR Registers
AFR Register is divided into 2 sections i.e AFRH (Pins 8 to 15) and AFRL (Pins 0 to 7)
Since I am using Pins PB8 and PB9, I will be modifying AFRH
As you can see above, AF4 is the function corresponding to the I2C1. Also AFRH is shown below
Inorder to set the function AF4, we need to write Bits (3:2:1:0) = 0:1:0:0 for PB8, and Bits (7:6:5:4) = 0:1:0:0 for PB9
GPIOB->AFR[1] |= (4<<0) | (4<<4); // Bits (3:2:1:0) = 0:1:0:0 --> AF4 for pin PB8; Bits (7:6:5:4) = 0:1:0:0 --> AF4 for pin PB9
This completes the Configuration of the Pins for the I2C. Now we will configure the rest of the I2C using I2C Registers
3. Reset the I2C
In order to reset the I2C, we need to modify the I2C Control Register 1 (I2C_CR1)
As you can see above the 15th bit of this register is the SWRST (Software reset). We will write a ‘1’ to this position to reset the I2C, and again write a ‘0’ to this position to pull the I2C from the reset
I2C1->CR1 |= (1<<15); // reset the I2C
I2C1->CR1 &= ~(1<<15); // Normal operation
4. Set the I2C clock
Now we need to set the 1000 KHz clock for the I2C and the formula for the same in the reference manual is shown below
Here the values of Tr(scl) and Tw(sclh) are provided in the datasheet as you can see below
TPCLK1 is the Time Period for the Peripheral clock. We need to set the value of the peripheral clock in the I2C Control Register 2 (I2C_CR2). The Clock setup from the first tutorial is shown below
As you can see the APB1 Peripheral Clock is at 45 MHz, and that’s the value we are going to input in the I2C_CR2 Register
// Program the peripheral input clock in I2C_CR2 Register in order to generate correct timings
I2C1->CR2 |= (45<<0); // PCLK1 FREQUENCY in MHz
Using all the above values in the mentioned formula, gives us the value for the Clock Control Register (CCR)
Now we feed this value to the CCR Register
// Configure the clock control registers
I2C1->CCR = 225<<0; // check calculation in PDF
After CCR, we will program the TRISE Register (I2C_TRISE). The formula to calculate TRISE value is shown below
// Configure the rise time register
I2C1->TRISE = 46; // check PDF again
This completes the Setup for the I2C Clock. Now we will enable the I2C
4. Enable the I2C
The I2C Peripheral can be enabled in the Control Register 1 (I2C_CR1). The Register is shown below
PE is the Peripheral Enable bit, and to enable or disable the I2C, we need to modify this bit
// Program the I2C_CR1 register to enable the peripheral
I2C1->CR1 |= (1<<0); // Enable I2C
This completes the configuration for the I2C. Now we will cover all the I2C functions that we are going to use, in order to write or read the data from any I2C device
I2C Functions
We are going to create different functions to interact with the I2C device. Let’s see them
I2C START
I2C_Start will be used to start the I2C Communication. Following are the steps required to start the I2C
/**** STEPS FOLLOWED ************
1. Enable the ACK
2. Send the START condition
3. Wait for the SB ( Bit 0 in SR1) to set. This indicates that the start condition is generated
*/
First of all we will enable the ACK bit and send the start condition. These both can be done by modifying the Control Register 1 (I2C_CR1)
I2C1->CR1 |= (1<<10); // Enable the ACK
I2C1->CR1 |= (1<<8); // Generate START
This will generate the START condition on the I2C bus
I2C WRITE
I2C_Write can be used to write the data to the slave device. The following is the procedure to perform the I2C Write
/**** STEPS FOLLOWED ************
1. Wait for the TXE (bit 7 in SR1) to set. This indicates that the DR is empty
2. Send the DATA to the DR Register
3. Wait for the BTF (bit 2 in SR1) to set. This indicates the end of LAST DATA transmission
*/
while (!(I2C1->SR1 & (1<<7))); // wait for TXE bit to set
I2C1->DR = data;
while (!(I2C1->SR1 & (1<<2))); // wait for BTF bit to set
- Here we first wait for the TXE (bit 7 in SR1) to set. This bit indicates that the DR (Data Register) is Empty
- Then we copy the Data into the DR
- And finally we wait for the BTF (Bit 2 in SR1) to set. This will indicate that the Byte transfer has finished
This completes the write process
Send Address
Generally Sending Address can be handled by the write function, but ST have slightly different checks for the Address alone. This is why I have created a separate function to deal with the Address part.
/**** STEPS FOLLOWED ************
1. Send the Slave Address to the DR Register
2. Wait for the ADDR (bit 1 in SR1) to set. This indicates the end of address transmission
3. clear the ADDR by reading the SR1 and SR2
*/
I2C1->DR = Address; // send the address
while (!(I2C1->SR1 & (1<<1))); // wait for ADDR bit to set
uint8_t temp = I2C1->SR1 | I2C1->SR2; // read SR1 and SR2 to clear the ADDR bit
- Here we first send the Address of the slave Device by copying it into the DR (Data Register)
- Then we wait for the ADDR (Bit 1 in SR1) to set. This will indicate that the Address Transmission is finished
- Now we will clear the ADDR bit by performing a dummy read in Status Registers SR1 and SR2
This will complete the Address Transmission. You can Read the ACK flag after this step to confirm whether the slave device has sent the acknowledgement or not
I2C READ
I2C_Read is probably the most complicated part. It is used to read the data from the device. Let’s see the detail about the steps
/**** STEPS FOLLOWED ************
1. If only 1 BYTE needs to be Read
a) Write the slave Address, and wait for the ADDR bit (bit 1 in SR1) to be set
b) the Acknowledge disable is made during EV6 (before ADDR flag is cleared) and the STOP condition generation is made after EV6
c) Wait for the RXNE (Receive Buffer not Empty) bit to set
d) Read the data from the DR
2. If Multiple BYTES needs to be read
a) Write the slave Address, and wait for the ADDR bit (bit 1 in SR1) to be set
b) Clear the ADDR bit by reading the SR1 and SR2 Registers
c) Wait for the RXNE (Receive buffer not empty) bit to set
d) Read the data from the DR
e) Generate the Acknowlegment by settint the ACK (bit 10 in SR1)
f) To generate the nonacknowledge pulse after the last received data byte, the ACK bit must be cleared just after reading the
second last data byte (after second last RxNE event)
g) In order to generate the Stop/Restart condition, software must set the STOP/START bit
after reading the second last data byte (after the second last RxNE event)
*/
int remaining = size;
/**** STEP 1 ****/
if (size == 1)
{
/**** STEP 1-a ****/
I2C1->DR = Address; // send the address
while (!(I2C1->SR1 & (1<<1))); // wait for ADDR bit to set
/**** STEP 1-b ****/
I2C1->CR1 &= ~(1<<10); // clear the ACK bit
uint8_t temp = I2C1->SR1 | I2C1->SR2; // read SR1 and SR2 to clear the ADDR bit.... EV6 condition
I2C1->CR1 |= (1<<9); // Stop I2C
/**** STEP 1-c ****/
while (!(I2C1->SR1 & (1<<6))); // wait for RxNE to set
/**** STEP 1-d ****/
buffer[size-remaining] = I2C1->DR; // Read the data from the DATA REGISTER
}
/**** STEP 2 ****/
else
{
/**** STEP 2-a ****/
I2C1->DR = Address; // send the address
while (!(I2C1->SR1 & (1<<1))); // wait for ADDR bit to set
/**** STEP 2-b ****/
uint8_t temp = I2C1->SR1 | I2C1->SR2; // read SR1 and SR2 to clear the ADDR bit
while (remaining>2)
{
/**** STEP 2-c ****/
while (!(I2C1->SR1 & (1<<6))); // wait for RxNE to set
/**** STEP 2-d ****/
buffer[size-remaining] = I2C1->DR; // copy the data into the buffer
/**** STEP 2-e ****/
I2C1->CR1 |= 1<<10; // Set the ACK bit to Acknowledge the data received
remaining--;
}
// Read the SECOND LAST BYTE
while (!(I2C1->SR1 & (1<<6))); // wait for RxNE to set
buffer[size-remaining] = I2C1->DR;
/**** STEP 2-f ****/
I2C1->CR1 &= ~(1<<10); // clear the ACK bit
/**** STEP 2-g ****/
I2C1->CR1 |= (1<<9); // Stop I2C
remaining--;
// Read the Last BYTE
while (!(I2C1->SR1 & (1<<6))); // wait for RxNE to set
buffer[size-remaining] = I2C1->DR; // copy the data into the buffer
}
Before we discuss this in more details, there are few common things in reading the data here
- After sending the read address, we always wait for the ADDR (Bit 1 in SR1) to set. This indicates that the address has been transmitted
- Before Reading the data from the DR (Data Register), we always wait for the RXNE (Bit 6 in SR1) to set. This indicates that the Receive buffer is not empty, and it is ready to be read.
This Read function is divided into 2 different parts. If you want to read a single byte, or multiple bytes. The reason for this is as shown below
If we are going to receive only a single byte, we need to send the ACK Disable before clearing the Address flag, and the STOP condition after disabling the flag. This can be seen in the code below
/**** STEP 1-b ****/
I2C1->CR1 &= ~(1<<10); // clear the ACK bit
uint8_t temp = I2C1->SR1 | I2C1->SR2; // read SR1 and SR2 to clear the ADDR bit.... EV6 condition
I2C1->CR1 |= (1<<9); // Stop I2C
Things are different for multiple byte reception. Here we need to send the ACK Disable and STOP conditions after receiving the second last data byte. This can be seen below
while (remaining>2)
{
/**** STEP 2-c ****/
while (!(I2C1->SR1 & (1<<6))); // wait for RxNE to set
/**** STEP 2-d ****/
buffer[size-remaining] = I2C1->DR; // copy the data into the buffer
/**** STEP 2-e ****/
I2C1->CR1 |= 1<<10; // Set the ACK bit to Acknowledge the data received
remaining--;
}
// Read the SECOND LAST BYTE
while (!(I2C1->SR1 & (1<<6))); // wait for RxNE to set
buffer[size-remaining] = I2C1->DR;
/**** STEP 2-f ****/
I2C1->CR1 &= ~(1<<10); // clear the ACK bit
/**** STEP 2-g ****/
I2C1->CR1 |= (1<<9); // Stop I2C
remaining--;
// Read the Last BYTE
while (!(I2C1->SR1 & (1<<6))); // wait for RxNE to set
buffer[size-remaining] = I2C1->DR; // copy the data into the buffer
- Till the remaining bytes are more than 2, we will perform the simple reception. Where we read the data from the DR and send an ACK after reading this data
- But when we receive the second last data byte, we will send the ACK Disable and STOP, to indicate that we want to end the reception after the next data byte
- Now we will read the last data byte and the I2C will automatically stop after that
This completes the I2C related functions, Now let’s see the Process of writing and Receiving data
The method
Here we will discuss how can we use the functions that we have created above. I am going to create 2 separate functions for writing and reading the data from the device
WRITE
The process of writing the data to any I2C Device is mentioned below
/**** STEPS FOLLOWED ************
1. START the I2C
2. Send the ADDRESS of the Device
3. Send the ADDRESS of the Register, where you want to write the data to
4. Send the DATA
5. STOP the I2C
*/
void MPU_Write (uint8_t Address, uint8_t Reg, uint8_t Data)
{
I2C_Start ();
I2C_Address (Address);
I2C_Write (Reg);
I2C_Write (Data);
I2C_Stop ();
}
Here MPU_Write is some write function i have created to write the data to the device.
READ
The reading process is also similar, but with few extra steps
/**** STEPS FOLLOWED ************
1. START the I2C
2. Send the ADDRESS of the Device
3. Send the ADDRESS of the Register, where you want to READ the data from
4. Send the RESTART condition
5. Send the Address (READ) of the device
6. Read the data
7. STOP the I2C
*/
void MPU_Read (uint8_t Address, uint8_t Reg, uint8_t *buffer, uint8_t size)
{
I2C_Start ();
I2C_Address (Address);
I2C_Write (Reg);
I2C_Start (); // repeated start
I2C_Read (Address+0x01, buffer, size);
I2C_Stop ();
}
Here MPU_Read is the function to read the data from the device. Note that the Slave Address is (Address+0x01) during the Read function. Basically we need to set the R/W bit (Bit 0) HIGH during the Read operation. This is common for all the devices that you will use for the I2C.
The main function
void MPU6050_Init (void)
{
uint8_t check;
uint8_t Data;
// check device ID WHO_AM_I
MPU_Read (MPU6050_ADDR,WHO_AM_I_REG, &check, 1);
if (check == 104) // 0x68 will be returned by the sensor if everything goes well
{
// power management register 0X6B we should write all 0's to wake the sensor up
Data = 0;
MPU_Write (MPU6050_ADDR, PWR_MGMT_1_REG, Data);
// Set DATA RATE of 1KHz by writing SMPLRT_DIV register
Data = 0x07;
MPU_Write(MPU6050_ADDR, SMPLRT_DIV_REG, Data);
// Set accelerometer configuration in ACCEL_CONFIG Register
// XA_ST=0,YA_ST=0,ZA_ST=0, FS_SEL=0 -> ? 2g
Data = 0x00;
MPU_Write(MPU6050_ADDR, ACCEL_CONFIG_REG, Data);
// Set Gyroscopic configuration in GYRO_CONFIG Register
// XG_ST=0,YG_ST=0,ZG_ST=0, FS_SEL=0 -> ? 250 ?/s
Data = 0x00;
MPU_Write(MPU6050_ADDR, GYRO_CONFIG_REG, Data);
}
}
void MPU6050_Read_Accel (void)
{
uint8_t Rx_data[6];
// Read 6 BYTES of data starting from ACCEL_XOUT_H register
MPU_Read (MPU6050_ADDR, ACCEL_XOUT_H_REG, Rx_data, 6);
Accel_X_RAW = (int16_t)(Rx_data[0] << 8 | Rx_data [1]);
Accel_Y_RAW = (int16_t)(Rx_data[2] << 8 | Rx_data [3]);
Accel_Z_RAW = (int16_t)(Rx_data[4] << 8 | Rx_data [5]);
/*** convert the RAW values into acceleration in 'g'
we have to divide according to the Full scale value set in FS_SEL
I have configured FS_SEL = 0. So I am dividing by 16384.0
for more details check ACCEL_CONFIG Register ****/
Ax = Accel_X_RAW/16384.0;
Ay = Accel_Y_RAW/16384.0;
Az = Accel_Z_RAW/16384.0;
}
int main ()
{
SysClockConfig ();
TIM6Config ();
I2C_Config ();
MPU6050_Init ();
while (1)
{
MPU6050_Read_Accel ();
Delay_ms (1000);
}
}
Result
You cam see the values of Ax, Ay and Az are being received from the Device. To understand this better, check the video below.