STM32, I2C protocol tutorial with EEPROM AT24C04N
STM32, I2C protocol tutorial with EEPROM AT24C04N

Inter-Integrated circuits is intended for very short distance communication between ICs on a single PCB. I2C is a synchronous serial communication protocol. It is a multi-master multi-slave protocol that need only two wires to transmit data between devices: SDA (Serial Data), SCL (Serial Clock).

In this tutorial STM32f4 nucleo board is interfaced with EEPROM atmel AT24c04n through I2C protocol

The Atmel AT24C04C provides 4,096/8 bits of Serial Electrically Erasable and Programmable Read-Only Memory (EEPROM) organized as 512 words of 8 bits each. it can be accessed via a I2C interface.

SCL is the clock line and SDA is data line for I2C interface with microcontoller.
Device/Page Addresses (A2 and A1) The AT24C04C uses the A2 and A1 inputs for hard wire addressing allowing a total of four 4K devices to be addressed on a single bus system. A0 Pin 1 is a no connect and can be connected to ground
Write Protect (WP): hardware data protection, The Write Protect pin allows normal read/write operations when connected to Ground (GND). When the Write Protect pin is connected to VCC, the write protection feature is enabled

AT24C04 device address

The 4K EEPROM device requires an 8-bit device address word following a start condition to enable the chip for a read or write operation. The device address word consists of a mandatory “1010” (0xA) sequence for the first four Most Significant Bits (MSB). The two device address bits must compare to their corresponding hard-wired input pins. The eighth bit of the device address is the read/write operation select bit. A read operation is initiated if this bit is high and a write operation is initiated if this bit is low.

As seen in the circuit diagram the pins A0, A1, A2 are connected to ground therefore device address of AT24C04 in this tutorial is A0.

Source code and with brief explanation

void i2cEepromInit(void)
{
	 /* Peripheral clock enable */
 __HAL_RCC_I2C1_CLK_ENABLE();

 eepromI2c.Instance = I2C1;
 eepromI2c.Init.ClockSpeed = 100000;
 eepromI2c.Init.DutyCycle = I2C_DUTYCYCLE_2;
 eepromI2c.Init.OwnAddress1 = 0x0;
 eepromI2c.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT;
 eepromI2c.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE;
 eepromI2c.Init.OwnAddress2 = 0;
 eepromI2c.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE;
 eepromI2c.Init.NoStretchMode = I2C_NOSTRETCH_DISABLE;
 HAL_I2C_Init(&eepromI2c);
}

void HAL_I2C_MspInit(I2C_HandleTypeDef* hi2c)
{
  GPIO_InitTypeDef GPIO_InitStruct = {0};
  if(hi2c->Instance==I2C1)
  {
    __HAL_RCC_GPIOB_CLK_ENABLE();
    /**I2C1 GPIO Configuration    
    PB8     ------> I2C1_SCL
    PB9     ------> I2C1_SDA 
    */
    GPIO_InitStruct.Pin = GPIO_PIN_8|GPIO_PIN_9;
    GPIO_InitStruct.Mode = GPIO_MODE_AF_OD;
    GPIO_InitStruct.Pull = GPIO_PULLUP;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
    GPIO_InitStruct.Alternate = GPIO_AF4_I2C1;
    HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
  }
}

In this project EEPROM AT24C04 is connected to I2C 1 of stm32f4 nucleo board. Following are the I2C1 configuration.

  1. Enable clock for I2C1 peripheral.
  2. Configure I2C clock frequency as 100khz.
  3. Select 7 bit device addressing mode.
  4. In STM32F446RE nucleo board, port B pin 8 and pin 9 can be used as I2C1 SCL, SDA.
  5. Enable the clock for port B and configure pin 8 and pin 9 with alternating function for I2C1.
  6. I2C protocol specifies the two lines SDA and SCL needs to be pulled up with some resistance, hence PULLUP is used for both lines while configuring I2C.

Writing data into EEPROM

/*
 * brief: Used to write data array into specified EEPROM memory location
 * param: DevAddress: EEPROM device address
 * param: MemAddress: Memory address location of EEPROM that data needs to be written to.
 * param: pData: base address of data array that needs to be written in EEPROM
 * param: len: Total number of bytes need to write into EEPROM in bytes
 */

HAL_StatusTypeDef eepromWriteIO(uint16_t DevAddress, uint16_t MemAddress, uint8_t *pData, uint16_t len) 
{
  HAL_StatusTypeDef returnValue = HAL_ERROR;
  uint8_t data[18] = {0};
	
	uint16_t completed = 0;
	if(MemAddress + len < 4000)
	{
		while(len >= 16)
		{
			/* We compute the MSB and LSB parts of the memory address */
			data[1] = (uint8_t) ((MemAddress & 0xFF00) >> 8);
			data[0] = (uint8_t) (MemAddress & 0xFF);

			/* And copy the content of the pData array in the temporary buffer */
			memcpy(data+2, pData+completed, ONEPAGE);
			completed += ONEPAGE;
				
			returnValue = HAL_I2C_Master_Transmit(&eepromI2c, DevAddress, data, ONEPAGE+2, HAL_MAX_DELAY);
			if(returnValue != HAL_OK)
				return returnValue;

			len -= ONEPAGE;
			MemAddress += ONEPAGE;
			
			/* We wait until the EEPROM effectively stores data in memory */
			while(HAL_I2C_Master_Transmit(&eepromI2c, DevAddress, 0, 0, HAL_MAX_DELAY) != HAL_OK);
			returnValue = HAL_OK;
		
		}
		if(len > 0)
		{
			data[1] = (uint8_t) ((MemAddress & 0xFF00) >> 8);
			data[0] = (uint8_t) (MemAddress & 0xFF);

			/* And copy the content of the pData array in the temporary buffer */
			memcpy(data+2, pData+completed, len);
			
			returnValue = HAL_I2C_Master_Transmit(&eepromI2c, DevAddress, data, len+2, HAL_MAX_DELAY);
			if(returnValue != HAL_OK)
				return returnValue;
			
			while(HAL_I2C_Master_Transmit(&eepromI2c, DevAddress, 0, 0, HAL_MAX_DELAY) != HAL_OK);
			returnValue = HAL_OK;
		}
	}	
  return returnValue;
}

Function eepromWriteIO() is used to write data to AT24C04N EEPROM. This function is used to write any number of bytes to EEPROM. In AT24C04N memory is divided into pages each of 16 bytes. To write data in EEPROM first memory address where the data has to be stored is sent followed by the data. AT24C04N supports page as well as byte write.

During page write memory address lower four bits are internally incremented following the receipt of each 1 byte data. The higher memory address bits are not incremented thus after writing ONEPAGE (16 bytes) memory address higher bits has to be incremented.

Function eepromWriteIO() can be used to send data array which is more than 16 bytes or less than 16 bytes at desired memory address.

Reading data from EEPROM

/*
 * brief: Used to read data from given EEPROM memory location
 * param: DevAddress: EEPROM device address
 * param: MemAddress: Memory address location of EEPROM where the data needs to be read from.
 * param: pData: Base address of data array where the data read from the EEPROM is stored
 * param: len: Total number of bytes that need to be read from EEPROM in bytes
 */

HAL_StatusTypeDef eepromReadIO(uint16_t DevAddress, uint16_t MemAddress, uint8_t *pData, uint16_t len) 
{
  HAL_StatusTypeDef returnValue;
  uint8_t addr[2];

  /* We compute the MSB and LSB parts of the memory address */
  addr[1] = (uint8_t) ((MemAddress & 0xFF00) >> 8);
  addr[0] = (uint8_t) (MemAddress & 0xFF);

  /* First we send the memory location address where start reading data */
  returnValue = HAL_I2C_Master_Transmit(&eepromI2c, DevAddress, addr, 2, HAL_MAX_DELAY);
  if(returnValue != HAL_OK)
    return returnValue;

  /* Next we can retrieve the data from EEPROM */
	while(HAL_I2C_Master_Receive(&eepromI2c, DevAddress, pData, len, HAL_MAX_DELAY)!= HAL_OK);
  return returnValue;
}

Function eepromReadIO() is used to read the data from the specified address of EEPROM. Any number of bytes can be read from the EEPROM. More than 16 bytes of data or even less than 16 bytes data can be read from the EEPROM from the specified address. Data read from EEPROM is stored in the previously allocated array.

main function

int main(void)
{
  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* Configure the system clock */
  SystemClock_Config();

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_USART2_UART_Init();
  i2cEepromInit();

  uint8_t msg[100];
  uint8_t wmsg[] = "Embedded Diaries\r\n";
  uint8_t rmsg[200];
	
  if(HAL_I2C_IsDeviceReady(&eepromI2c,EEPROMDEVADDR,2,1000) == HAL_OK)
  {
	sprintf((char*)msg,"EEPROM is ready with device address %x\r\n",EEPROMDEVADDR);
	HAL_UART_Transmit(&huart2,msg,sizeof(msg),1000);
  }

  eepromWriteIO(EEPROMDEVADDR, 0x0010, wmsg, sizeof(wmsg)+1);
  eepromReadIO(EEPROMDEVADDR, 0x0010, rmsg, sizeof(wmsg)+1);
  HAL_UART_Transmit(&huart2,(uint8_t*)rmsg,sizeof(rmsg),1000);
  while (1)
  {
  }
}

In main() function after all the hal, gpio, clock, I2C and EEPROM initilization first HAL_I2C_IsDeviceReady() will verify status of EEPROM communication. Then the wmsg character array is used to write the data to EEPROM then rmsg is used to read the data of required bytes from the EEPROM and displayed on the uart console.

Teraterm Output