Working with STM32F7 and I2C: Read Mode

In this guide, we shall see how to configure I2C peripheral in STM32F7 to read certain amount of data from memory location. In this guide, we shall use DS3231 RTC to read seconds, minutes and hours.

In this guide, we shall cover the following:

  • What is I2C.
  • Circuit Diagram.
  • Configure I2C pins.
  • Configure I2C peripheral.
  • I2C Read function.
  • Code.
  • Result.

1. What is I2C:

With I2C, data is transferred in messages. Messages are broken up into frames of data. Each message has an address frame that contains the binary address of the slave, and one or more data frames that contain the data being transmitted. The message also includes start and stop conditions, read/write bits, and ACK/NACK bits between each data frame:

Introduction to I2C - Message, Frame, and Bit

Start Condition: The SDA line switches from a high voltage level to a low voltage level before the SCL line switches from high to low.

Stop Condition: The SDA line switches from a low voltage level to a high voltage level after the SCL line switches from low to high.

Address Frame: A 7 or 10 bit sequence unique to each slave that identifies the slave when the master wants to talk to it.

Read/Write Bit: A single bit specifying whether the master is sending data to the slave (low voltage level) or requesting data from it (high voltage level).

ACK/NACK Bit: Each frame in a message is followed by an acknowledge/no-acknowledge bit. If an address frame or data frame was successfully received, an ACK bit is returned to the sender from the receiving device.

For more information and detailed explanations refer to this NXP documentation (here).

2. Circuit Diagram:

Since here we are using STM32F767ZI-Nucleo144, there is SDA and SCL pins in arduino connectors that allow us to connect I2C devices such as OLED display MPU9250 etc.

These pins are PB8 and PB9

STM32F7-Nucleo144DS3231 module
5VVcc
GNDGND
D15 (PB8)SCL
D14 (PB9)SDA
D

3. Configure I2C pins:

From the circuit diagram, the required pins are PB8 and PB9.

Now we need to configure the pins.

We need first to enable clock access to GPIOB:

From block diagram, we can see that GPIOB is connected to AHB1, hence we can enable clock access to GPIOB as following:

/*Enable clock access*/
RCC->AHB1ENR|=RCC_AHB1ENR_GPIOBEN;

Then we need to configure the pins as alternate function:

	/*Set PB8 and PB9 to alternate function*/
	GPIOB->MODER|=GPIO_MODER_MODER8_1|GPIO_MODER_MODER9_1;
	GPIOB->MODER&=~(GPIO_MODER_MODER8_0|GPIO_MODER_MODER9_0);

Then set the pin as open drain mode:

	/*Set output to open drain*/
	GPIOB->OTYPER|=GPIO_OTYPER_OT8|GPIO_OTYPER_OT9;

Then we need configure which alternate function to be used:

From datasheet of stm32f767, table13, the alternate function map, we need AF4:

Hence, we create macro for that:

#define AF4 0x04

Now, we can configure which alternate function:

	GPIOB->AFR[1]|=(AF4<<0)|(AF4<<4);

4. Configure I2C:

In order to configure I2C, we need to enable clock access to I2C1 which is the i2c to be used:

From the block diagram, we can see I2C1 is connected to APB1:

RCC->APB1ENR|=RCC_APB1ENR_I2C1EN;

Then we software reset the I2C:

I2C1->CR1 &=~I2C_CR1_PE;

Then configure timing register (from CubeMX):

#define timing 0x00303D5B
I2C1->TIMINGR=timing;

Then enable I2C:

I2C1->CR1 |=I2C_CR1_PE;

5. I2C read function:

For reading, we create function that takes 4 arguments:

  • Slave address.
  • Memory address.
  • Pointer to hold the data to be read.
  • Number of bytes to be read.
void i2_read(uint8_t slav_add, uint8_t memadd, uint8_t *data, uint8_t length )
	{
	/*Enable I2C*/
	I2C1->CR1 |=I2C_CR1_PE;
	/*Set slave address*/
	I2C1->CR2=(slav_add<<1);
	/*7-bit addressing*/
	I2C1->CR2&=~I2C_CR2_ADD10;
	/*Set number to transfer to 1 for write operation*/
	I2C1->CR2|=(1<<I2C_CR2_NBYTES_Pos);
	/*Set the mode to write mode*/
	I2C1->CR2&=~I2C_CR2_RD_WRN;
	/*Software end*/
	I2C1->CR2&=~I2C_CR2_AUTOEND;
	/*Generate start*/
	I2C1->CR2|=I2C_CR2_START;
	/*Wait until transfer is completed*/
	while(!(I2C1->ISR & I2C_ISR_TC)){
		/*Check if TX buffer is empty*/
		if(I2C1->ISR & I2C_ISR_TXE)
		{
			/*send memory address*/
			I2C1->TXDR = (memadd);
		}
	}
	/*Reset I2C*/
	I2C1->CR1 &=~I2C_CR1_PE;
	I2C1->CR1 |=I2C_CR1_PE;
	/*Set slave address*/
	I2C1->CR2=(slav_add<<1);
	/*Set mode to read operation*/
	I2C1->CR2|=I2C_CR2_RD_WRN;
	/*Set length to the required length*/
	I2C1->CR2|=((length)<<I2C_CR2_NBYTES_Pos);
	/*aut generate stop after transfer completed*/
	I2C1->CR2|=I2C_CR2_AUTOEND;
	/*Generate start*/
	I2C1->CR2|=I2C_CR2_START;
	/*wait until stop is generated*/
	while(!(I2C1->ISR & I2C_ISR_STOPF))
			{
				/*If RX buffer is empty*/
			  if(I2C1->ISR &(I2C_ISR_RXNE))
			  	  {
				  /*read the data and increment the pointer*/
				  *data++=I2C1->RXDR;
				 }
			}
	/*disable the peripheral*/
	I2C1->CR1 &=~I2C_CR1_PE;
	}

6. You may download the code from here:

7. Results:

If you run the code and open terminal application and set baudrate to 115200, you should get the following:

From left to right:

  • Seconds.
  • Minutes.
  • Hours.

Happy coding 🙂

Add Comment

Your email address will not be published. Required fields are marked *