AT24C02芯片(又叫E2PROM存储器、EEPROM存储器),是一种通过I2C(IIC)协议通信的掉电保存存储器芯片,其内部含有256个8位字节。在介绍这款芯片之前,我们先来粗略了解一下I2C协议。
I2C总线是一种双向二线制的同步串行总线,它只需要两根线即可在连接于总线上的器件之间传送信息(分别为SDA和SCL)。在I2C总线上,可以有若干个从机(如AT24C02芯片),但只能有一个主机(如单片机)。像不同的通信协议一样,I2C协议规定了一些SDA、SCL的行为(如什么时候谁置高、什么时候谁置低,用以代表什么含义),连在I2C总线上的器件则依靠这个规则来传输数据与接收数据。
(想要深入了解I2C协议,可以参考嵌入式硬件入门——EEPROM(AT24C02+I2C协议))
通常,单片机上内置了硬件I2C。开启硬件I2C,并使用相应的HAL库函数,单片机中的硬件I2C就会按照其已经设定好的I2C协议与别的I2C器件进行通信。与硬件I2C相对的,还有软件I2C。软件I2C是指按照I2C协议,自行用两个GPIO端口置高或低模拟SDA和SCL的行为。在比赛方提供的资源数据包——底层驱动代码参考中,有用HAL库函数实现的软件I2C的库,我们就基于这个库来使用AT24C02芯片(E2PROM存储器)进行读写操作。
打开电路原理图(CT117E-M4产品手册),可以看到STM32G431RBT6的24C02芯片被挂到了PB6、PB7上(相当于I2C总线的SDA和SCL):
因此,我们需要在Cube中将PB6和PB7设置为GPIO输出模式。
设置完成后,需要调用比赛方提供的软件I2C库:i2c_hal.c和i2c_hal.h,我们需要用到的函数如下:
/**
* @brief I2C起始信号
* @param None
* @retval None
*/
void I2CStart(void);
/**
* @brief I2C结束信号
* @param None
* @retval None
*/
void I2CStop(void);
/**
* @brief I2C等待确认信号
* @param None
* @retval None
*/
unsigned char I2CWaitAck(void);
/**
* @brief I2C发送一个字节
* @param cSendByte 需要发送的字节
* @retval None
*/
void I2CSendByte(unsigned char cSendByte);
/**
* @brief I2C接收一个字节
* @param None
* @retval 接收到的字节
*/
unsigned char I2CReceiveByte(void);
下面,我们来看看如何利用这些函数来进行E2PROM的读写操作。
打开AT24C02芯片手册(在选手资源数据包——芯片资料中),可以找到向E2PROM写入数据与读取数据(读取内存)的流程图:
对应流程图,我们就可以编写E2PROM的读写操作函数如下:
/* e2prom.c */
#include "e2prom.h"
/* E2PROM写操作,对应Figure 8. Btye Write */
void e2prom_write(unsigned char address, unsigned char info)
{
I2CStart(); //1.I2C起始信号(START)
I2CSendByte(0xa0); //2.发送设备地址与“写”信号(DEVICE ADDRESS+WRITE),将在下文解释
I2CWaitAck(); //3.IC2等待确认信号(ACK)
I2CSendByte(address); //4.发送数据存储地址(WORD ADDRESS)(可以为0~255,对应256个)
I2CWaitAck(); //5.I2C等待确认信号(ACK)
I2CSendByte(info); //6.发送数据(DATA)
I2CWaitAck(); //7.I2C等待确认信号(ACK)
I2CStop(); //8.I2C结束信号(STOP)
}
/* E2PROM读操作,对应Figure 11. Random Read */
unsigned char e2prom_read(unsigned char address)
{
unsigned char val;
I2CStart(); //1.I2C起始信号(START)
I2CSendByte(0xa0); //2.发送设备地址与“写”信号(DEVICE ADDRESS+WR-TE),将在下文解释
I2CWaitAck(); //3.IC2等待确认信号(ACK)
I2CSendByte(address); //4.发送数据存储地址(WORD ADDRESS)(可以为0~255,对应256个)
I2CWaitAck(); //5.I2C等待确认信号(ACK)
I2CStart(); //6.I2C起始信号(START)
I2CSendByte(0xa1); //7.发送设备地址与“读”信号(DEVICE ADDRESS+READ),将在下文解释
I2CWaitAck(); //8.I2C等待确认信号(ACK)
val = I2CReceiveByte(); //9.接收数据(DATA)
I2CWaitAck(); //10.I2C等待确认信号(NO ACK)
I2CStop(); //11.I2C结束信号(STOP)
return (val);
}
/* e2prom.h */
#ifndef __E2PROM_H
#define __E2PROM_H
#include "main.h"
#include "i2c_hal.h"
void e2prom_write(unsigned char address, unsigned char info);
unsigned char e2prom_read(unsigned char address);
#endif /* __E2PROM_H */
下面来解释一下为什么第二步和第七步发送的是0xa0和0xa1。同样在芯片手册中,可以找到AT24C02芯片的地址:
AT24C01/02/04/08/16分别对应1K/2K/4K/8K/16K,A2、A1、A0分别对应电路原理图的E3、E2、E1。在下图中可以看到,E1、E2、E2均接地,为0。最后一位的R(Read)为1,W(Write)为0。因此,在STM32G431RBT6中,AT24C02的地址为1010000_(即0xa_),最后一位视读或写操作为1或0。
通过调用我们自己编写的e2prom的库,就可以使用E2PROM存储器进行简单的8位数据(unsigned char或uint8_t类型)的存储操作了。
附录
i2c_hal.c
/*
程序说明: CT117E-M4嵌入式竞赛板GPIO模拟I2C总线驱动程序
软件环境: MDK-ARM HAL库
硬件环境: CT117E-M4嵌入式竞赛板
日 期: 2020-3-1
*/
#include "i2c_hal.h"
#define DELAY_TIME 20
/**
* @brief SDA线输入模式配置
* @param None
* @retval None
*/
void SDA_Input_Mode()
{
GPIO_InitTypeDef GPIO_InitStructure = {0};
GPIO_InitStructure.Pin = GPIO_PIN_7;
GPIO_InitStructure.Mode = GPIO_MODE_INPUT;
GPIO_InitStructure.Pull = GPIO_PULLUP;
GPIO_InitStructure.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOB, &GPIO_InitStructure);
}
/**
* @brief SDA线输出模式配置
* @param None
* @retval None
*/
void SDA_Output_Mode()
{
GPIO_InitTypeDef GPIO_InitStructure = {0};
GPIO_InitStructure.Pin = GPIO_PIN_7;
GPIO_InitStructure.Mode = GPIO_MODE_OUTPUT_OD;
GPIO_InitStructure.Pull = GPIO_NOPULL;
GPIO_InitStructure.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOB, &GPIO_InitStructure);
}
/**
* @brief SDA线输出一个位
* @param val 输出的数据
* @retval None
*/
void SDA_Output( uint16_t val )
{
if ( val )
{
GPIOB->BSRR |= GPIO_PIN_7;
}
else
{
GPIOB->BRR |= GPIO_PIN_7;
}
}
/**
* @brief SCL线输出一个位
* @param val 输出的数据
* @retval None
*/
void SCL_Output( uint16_t val )
{
if ( val )
{
GPIOB->BSRR |= GPIO_PIN_6;
}
else
{
GPIOB->BRR |= GPIO_PIN_6;
}
}
/**
* @brief SDA输入一位
* @param None
* @retval GPIO读入一位
*/
uint8_t SDA_Input(void)
{
if(HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_7) == GPIO_PIN_SET){
return 1;
}else{
return 0;
}
}
/**
* @brief I2C的短暂延时
* @param None
* @retval None
*/
static void delay1(unsigned int n)
{
uint32_t i;
for ( i = 0; i < n; ++i);
}
/**
* @brief I2C起始信号
* @param None
* @retval None
*/
void I2CStart(void)
{
SDA_Output(1);
delay1(DELAY_TIME);
SCL_Output(1);
delay1(DELAY_TIME);
SDA_Output(0);
delay1(DELAY_TIME);
SCL_Output(0);
delay1(DELAY_TIME);
}
/**
* @brief I2C结束信号
* @param None
* @retval None
*/
void I2CStop(void)
{
SCL_Output(0);
delay1(DELAY_TIME);
SDA_Output(0);
delay1(DELAY_TIME);
SCL_Output(1);
delay1(DELAY_TIME);
SDA_Output(1);
delay1(DELAY_TIME);
}
/**
* @brief I2C等待确认信号
* @param None
* @retval None
*/
unsigned char I2CWaitAck(void)
{
unsigned short cErrTime = 5;
SDA_Input_Mode();
delay1(DELAY_TIME);
SCL_Output(1);
delay1(DELAY_TIME);
while(SDA_Input())
{
cErrTime--;
delay1(DELAY_TIME);
if (0 == cErrTime)
{
SDA_Output_Mode();
I2CStop();
return ERROR;
}
}
SDA_Output_Mode();
SCL_Output(0);
delay1(DELAY_TIME);
return SUCCESS;
}
/**
* @brief I2C发送确认信号
* @param None
* @retval None
*/
void I2CSendAck(void)
{
SDA_Output(0);
delay1(DELAY_TIME);
delay1(DELAY_TIME);
SCL_Output(1);
delay1(DELAY_TIME);
SCL_Output(0);
delay1(DELAY_TIME);
}
/**
* @brief I2C发送非确认信号
* @param None
* @retval None
*/
void I2CSendNotAck(void)
{
SDA_Output(1);
delay1(DELAY_TIME);
delay1(DELAY_TIME);
SCL_Output(1);
delay1(DELAY_TIME);
SCL_Output(0);
delay1(DELAY_TIME);
}
/**
* @brief I2C发送一个字节
* @param cSendByte 需要发送的字节
* @retval None
*/
void I2CSendByte(unsigned char cSendByte)
{
unsigned char i = 8;
while (i--)
{
SCL_Output(0);
delay1(DELAY_TIME);
SDA_Output(cSendByte & 0x80);
delay1(DELAY_TIME);
cSendByte += cSendByte;
delay1(DELAY_TIME);
SCL_Output(1);
delay1(DELAY_TIME);
}
SCL_Output(0);
delay1(DELAY_TIME);
}
/**
* @brief I2C接收一个字节
* @param None
* @retval 接收到的字节
*/
unsigned char I2CReceiveByte(void)
{
unsigned char i = 8;
unsigned char cR_Byte = 0;
SDA_Input_Mode();
while (i--)
{
cR_Byte += cR_Byte;
SCL_Output(0);
delay1(DELAY_TIME);
delay1(DELAY_TIME);
SCL_Output(1);
delay1(DELAY_TIME);
cR_Byte |= SDA_Input();
}
SCL_Output(0);
delay1(DELAY_TIME);
SDA_Output_Mode();
return cR_Byte;
}
//
void I2CInit(void)
{
GPIO_InitTypeDef GPIO_InitStructure = {0};
GPIO_InitStructure.Pin = GPIO_PIN_7 | GPIO_PIN_6;
GPIO_InitStructure.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStructure.Pull = GPIO_PULLUP;
GPIO_InitStructure.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOB, &GPIO_InitStructure);
}
i2c_hal.h
#ifndef __I2C_HAL_H
#define __I2C_HAL_H
#include "stm32g4xx_hal.h"
void I2CStart(void);
void I2CStop(void);
unsigned char I2CWaitAck(void);
void I2CSendAck(void);
void I2CSendNotAck(void);
void I2CSendByte(unsigned char cSendByte);
unsigned char I2CReceiveByte(void);
void I2CInit(void);
#endif