平台:STM32ZET6(核心板)+ST-LINK/V2+SD卡+USB串口线+外部EEPROM(不需要上拉电阻)
工程介绍:主要文件在USER组中,bsp_i2c_ee.c,bsp_i2c_ee.h,bsp_eeprom.c,bsp_eeprom.h和main.c,其中bsp_i2c_ee.c中主要时基本的模拟I2C时序,而bsp_eeprom.c中主要利用前一个文件中定义的基本操作,进行EEPROM的读写操作。其他类似I2C时序的协议,均可以保留bsp_i2c_ee.c的基础上添加新的内容。本文有些内容借鉴了其他网友的总结,在此表示感谢。
1.硬件部分:电路连接较为简单,笔者在淘宝上买的24C02N主要有四根线,两根电源线,一根SCL和一根SDA。这里我们把SCL和SDA连接到B端口的6和7引脚。如上图所示,如果需要改变引脚设置,只需要更改宏即可。
MCU和EEPROM连接好之后就像下图所示,MCU作为主机,EEPROM作为从机,从机地址不可以重复,由于STM32ZET6(核心板)上提供了上拉输入功能,我们可以很方便的将EEPROM模块的SCL和SDA直接连接到PB6和PB7上。
2.软件部分:
关键在于 I2C时序的模拟,主要模拟的是起始信号,终止信号,应答信号,非应答信号,等待接收应答信号,发送一个字节,读取一个字节。
这些工作分别由以下函数模拟产生。
int I2C_Start(void);
void I2C_Stop(void);
void I2C_Ack();
void I2C_NoAck();
uint8_t I2C_GetAck(void);
void I2C_SendByte(uint8_t Data);
uint8_t I2C_ReadByte(uint8_t ack);
2.1 起始信号和终止信号
如图所示,当SCL(SCLK)为高电平,SDA(SDI)从高电平到低电平跳变,作为起始信号。反映在程序上,如下:
int I2C_Start(void)
{
I2C_SDA_OUT(); //配置SDA为推挽输出
SDA_H;
SCL_H;//高电平有效
I2C_delay();//延时
//查看此时SDA是否就绪(高电平)
if(!SDA_read)
{
printf("\r\nSDA线为低电平,总线忙,退出\r\n");
return DISABLE;//SDA总线忙,退出
}
//制造一个下降沿,下降沿是开始的标志
SDA_L;
I2C_delay();
//查看此时SDA已经变为低电平
if(SDA_read)
{
printf("\r\nSDA线为高电平,总线出错,退出\r\n");
return DISABLE;//SDA总线忙,退出
}
SCL_L;
return ENABLE;
}
当SCL(SCLK)为高电平,SDA(SDI)从低电平到高电平跳变,作为终止信号。反映在程序上,如下:
void I2C_Stop(void)
{
I2C_SDA_OUT(); //配置SDA为推挽输出
SCL_L;
//制造一个上升沿,上升沿是结束的标志
SDA_L;
SCL_H;//高电平有效
I2C_delay();//延时
SDA_H;
I2C_delay();
}
2.2 应答和非应答信号
//主机的应答信号,主机把第九位置高,从机将其拉低表示应答
static void I2C_Ack()
{
SCL_L;
I2C_SDA_OUT(); //配置SDA为推挽输出
SDA_L;//置低
I2C_delay(); //注意延时时间应该大于4微秒,其他位置也是如此
SCL_H;
I2C_delay();
SCL_L;
}
//主机的非应答信号,从机把第九位置高,主机将其拉低表示非应答
static void I2C_NoAck()
{
SCL_L;
I2C_SDA_OUT(); //配置SDA为推挽输出
I2C_delay();
SDA_H;//置高
I2C_delay();
SCL_H;
I2C_delay();
SCL_L;
}
//注意延时时间应该大于4微秒,其他位置也是如此
SCL_H;
I2C_delay();
SCL_L;
}
//主机的非应答信号,从机把第九位置高,主机将其拉低表示非应答
static void I2C_NoAck()
{
SCL_L;
I2C_SDA_OUT(); //配置SDA为推挽输出
I2C_delay();
SDA_H;//置高
I2C_delay();
SCL_H;
I2C_delay();
SCL_L;
}
2.3 应答位的接收
uint8_t I2C_GetAck(void)
{
uint8_t time = 0;
I2C_SDA_IN(); //配置SDA为上拉输入
SDA_H;
I2C_delay();
SCL_H;
I2C_delay();
while(SDA_read)//从机未应答,若应答,会拉低第九位
{
time++;
if(time > 250)
{
//不应答时不可以发出终止信号,否则,复合读写模式下不可以进行第二阶段
//SCCB_Stop();
SCL_L;
return DISABLE;
}
}
SCL_L;
return ENABLE;
}
2.4 读一个字节和写一个字节
//I2C写一个字节
void I2C_SendByte(uint8_t Data)
{
uint8_t cnt;
I2C_SDA_OUT(); //配置SDA为推挽输出
for(cnt=0; cnt<8; cnt++)
{
SCL_L; //SCL低(SCL低时,变化SDA)
I2C_delay();
if(Data & 0x80)
{
SDA_H; //SDA高,从最低位开始写起
}
else
{
SDA_L; //SDA低
}
Data <<= 1;
SCL_H; //SCL高(发送数据)
I2C_delay();
}
SCL_L; //SCL低(等待应答信号)
I2C_delay();
}
//I2C读取一个字节
uint8_t I2C_ReadByte(uint8_t ack)
{
uint8_t cnt;
uint8_t data;
I2C_SDA_IN(); //配置SDA为上拉输入
for(cnt=0; cnt<8; cnt++)
{
SCL_L; //SCL低
I2C_delay();
SCL_H; //SCL高(读取数据)
data <<= 1;
if(SDA_read)
{
data |= 0x01; //SDA高(数据有效)
}
I2C_delay();
}
//发送应答信号,为低代表应答,高代表非应答
if(ack == 1)
{
I2C_NoAck();
}
else
{
I2C_Ack();
}
return data; //返回数据
}
2.5 GPIO的初始化,以及SDA的输入输出模式的重新配置
前面我们注意到,当在应答位的接收,以及读取一个字节的信息时,SDA需要被设置为输入模式,读取从机(EEPROM)发送来的数据。
//GPIO配置函数
void I2C_GPIO_Configuration(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
GPIO_InitStructure.GPIO_Pin = PIN_I2C_SCL;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //推挽输出
GPIO_Init(PORT_I2C_SCL, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = PIN_I2C_SDA;
GPIO_Init(PORT_I2C_SDA, &GPIO_InitStructure);
}
//重新设置SDA为上拉输入模式
void I2C_SDA_IN()
{
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Pin = PIN_I2C_SDA;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; //上拉输入,使得板外部不需要接上拉电阻
GPIO_Init(PORT_I2C_SDA, &GPIO_InitStructure);
}
//重新设置SDA为推挽输出模式
void I2C_SDA_OUT()
{
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Pin = PIN_I2C_SDA;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; //IO口速度为50MHz
GPIO_Init(PORT_I2C_SDA, &GPIO_InitStructure);
}
//I2C初始化
void I2C_Initializes(void)
{
I2C_GPIO_Configuration();
SCL_H; //置位状态
SDA_H;
}
2.6 与EEPROM相关的宏定义
#define EEPROM_DEV_ADDR 0xA0 //地址(设备地址)
#define EEPROM_WR 0x00 //写
#define EEPROM_RD 0x01 //读
2.7 调用之前定义好的I2C时序模拟函数,完成EEPROM的读写一个字节操作。(借鉴于他人)
//写入一个字节
int EEPROM_WriteByte(uint16_t Addr, uint8_t Data)
{
/* 1.开始 */
I2C_Start();
/* 2.设备地址/写 */
I2C_SendByte(EEPROM_DEV_ADDR | EEPROM_WR);
//读取应答位
if(!I2C_GetAck())
{
printf("\r\n发送设备地址时非应答!\r\n");
I2C_Stop();
return DISABLE;
}
/* 3.数据地址 */
#if (8 == EEPROM_WORD_ADDR_SIZE)
I2C_SendByte((uint8_t)(Addr&0x00FF)); //数据地址(8位)
#else
I2C_SendByte((uint8_t)(Addr>>8)); //数据地址(16位)
I2C_SendByte((uint8_t)(Addr&0x00FF));
#endif
I2C_GetAck();//不需要判断应答位的状况
/* 4.写一字节数据 */
I2C_SendByte(Data);
/* 5.停止 */
I2C_Stop();
}
//读取一个字节
int EEPROM_ReadByte(uint16_t Addr, uint8_t *Data)
{
/* 1.开始 */
I2C_Start();
/* 2.设备地址/写 */
I2C_SendByte(EEPROM_DEV_ADDR | EEPROM_WR);
//读取应答位
if(!I2C_GetAck())
{
printf("\r\n读一串数据的两相写阶段非应答!\r\n");
I2C_Stop();
return DISABLE;
}
/* 3.数据地址 */
#if (8 == EEPROM_WORD_ADDR_SIZE)
I2C_SendByte((uint8_t)(Addr&0x00FF)); //数据地址(8位)
#else
I2C_SendByte((uint8_t)(Addr>>8)); //数据地址(16位)
I2C_SendByte((uint8_t)(Addr&0x00FF));
#endif
/* 4.重新开始 */
I2C_Start();
/* 5.设备地址/读 */
I2C_SendByte(EEPROM_DEV_ADDR | EEPROM_RD);
//读取应答位
if(!I2C_GetAck())
{
printf("\r\n读一串数据的两相读阶段非应答!\r\n");
I2C_Stop();
return DISABLE;
}
/* 6.读一字节数据 */
*Data = I2C_ReadByte(I2C_NOACK); //只读取1字节(产生非应答)
/* 7.停止 */
I2C_Stop();
}
2.8 重复调用读写一个字节函数,实现同时读写多个字节的数据。
//写入多个字节
void EEPROM_WriteNByte(uint16_t Addr, uint8_t *pData, uint16_t Length)
{
uint16_t i;
//每写一个字节,调用一次EEPROM_WriteByte
for(i=0;i
2.9 测试读写功能
#define EEPROM_BUF_LEN 64 //测试BUF长度
void System_Initializes(void)
{
//定时器配置
SysTick_Init();
//串口配置
USART_Config();
//I2C配置
I2C_Initializes();
}
int main(void)
{
uint8_t cnt;
uint8_t line = 0;
uint8_t w_buf[EEPROM_BUF_LEN];
uint8_t r_buf[EEPROM_BUF_LEN];
System_Initializes();
/* 填充缓冲区 */
for(cnt=0; cnt= 4)
{
printf("\r\n");
line = 0;
}
}
//0地址连续写EEPROM_BUF_LEN节数据
EEPROM_WriteNByte(0, w_buf, EEPROM_BUF_LEN);
Delay_us(100000);
//0地址连续读EEPROM_BUF_LEN节数据,并打印
EEPROM_ReadNByte(0, r_buf, EEPROM_BUF_LEN);
//打印读取的内容
for(cnt=0; cnt= 4)
{
printf("\r\n");
line = 0;
}
}
}