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

PROCESS

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

Check out the VIDEO Below

DOWNLOAD

You can buy me a coffee Sensor by clicking DONATE OR Just click DOWNLOAD to download the code

controllerstech
Subscribe
Notify of
guest
6 Comments
Newest
Oldest Most Voted
Inline Feedbacks
View all comments
Menu