平台: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;
}
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<Length;i++) { //写入数据 EEPROM_WriteByte(Addr, *pData); Addr++; pData++; //延时 Delay_us(10000); } } //读取多个字节 void EEPROM_ReadNByte(uint16_t Addr, uint8_t *pData, uint16_t Length) { uint16_t i; //每写一个字节,调用一次EEPROM_ReadByte for(i=0;i<Length;i++) { //写入数据 EEPROM_ReadByte(Addr, pData); Addr++; pData++; //延时 Delay_us(10000); } }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<EEPROM_BUF_LEN; cnt++) { w_buf[cnt] = cnt; } printf("************************I2C协议读写EEPROM实验*********************\r\n"); //打印读取的内容 for(cnt=0; cnt<EEPROM_BUF_LEN; cnt++) { printf("w_buf[%d] = %d\t", cnt, w_buf[cnt]); line++; if(line >= 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<EEPROM_BUF_LEN; cnt++) { printf("r_buf[%d] = %d\t", cnt, r_buf[cnt]); line++; if(line >= 4) { printf("\r\n"); line = 0; } } }