概要
本文主要记录个人在学习I2C协议的一些个人见解,且基于I2C协议实现STM32读写EEPROM的数据
声明:因个人能力有限,本文仅是个人的学习记录笔记,有错误之处还望指出
I2C特征
- 支持设备总线,即可以多个设备共用信号线。在一个I2C通讯总线中可以连接多个I2C通信设备,支持多个通讯主机及多个通过从机
- 使用两条总线线路,一条双向串行数据线(SDA),一条串行时钟线(SCL)。SCL用于数据收发的同步
- 每个连接到总线的设备都有一个独立的地址,通过这个地址来访问不同的设备
- 总线通过上拉电阻接到电源,当I2C设备空闲时候,会输出高阻态,当所有设备都空闲,都输出高阻态,由上拉电阻把总线拉成高电平
- 多个主机使用总线时,会利用仲裁的方式来决定那个设备占用总线
I2C的基本读写过程
主机写数据到从机
主机由从机中读数据
- 在主机和从机通讯时,S表示从主机的I2C接口产生的传输起始信号,这个时候所有连接到I2C总线上的从机都会收到该信号。
- 为了实现操作正确的从机,故通过主机广播的从机地址(SLAVE ADDRESS),在I2C总线上,每个设备的地址都是唯一的,故从机被选中。(从机地址可以是7位或10位)
- 在成功找到从机后,要传输方向的选择位,该位为0时,表示主机向从机写数据,该位为1时,表示主机向从机读数据(0写1读)
- 在从机接收到匹配的地址后,主机或从机会返回一个应答(ACK)或非应答(NACK)信号,只有接收到应答信号后才可以继续发送或接收数据。
写数据
配置方向传输位为“写数据”方向(0),广播完地址,接收到应答信号后,主机正式开始向从机写数据(DATA),数据包大小为8位,主机每次发送完一个字节的数据,都要等待从机的应答信号(ACK),重复这个过程,可以向从机传输N个数据,最后当传输结束,主机向从机发送一个停止传输信号,表示不再传数据。
读数据
配置方向传输位为“读数据”方向(1),广播完地址,接收到应答信号后,主机正式开始向从机写数据(DATA),数据包大小为8位,主机每次发送完一个字节的数据,都要等待从机的应答信号(ACK),重复这个过程,可以向从机传输N个数据。当主机希望停止接收数据时候,从机可以返回一个非应答信号(NACK),则从机自动停止数据传输
读和写数据
除了基本读写,I2C通讯更加常用的是复合模式,该传输过程有两次起始信号(S)。一般在第一次传输中,主机通过SLAVE_ADDRESS寻找到从机设备后,发送一段数据,这段数据是表示从设备内部的存储器或存储器地址(注意区分它与 SLAVE_ADDRESS 的区别);在第二次传输中才是对改地址内容的读写。
简言之第一次传输读写地址,第二次是读写的具体数据
I2C的起始状态
- 开始
SCL为高电平,SDA由高电平向低电平转换
/* SCL为高电平,出现一个下跳沿 */
void I2C_Start(void){
SDA_HIGH();
SCL_HIGH();
delay();
SDA_LOW();
delay();
SCL_LOW():
delay();
}
- 结束
SCL是高电平,SDA由低电平向高电平转换
/* SCL为高电平,出现一个上跳沿 */
void I2C_Stop(void){
SDA_LOW();
SCL_HIGH();
delay();
SDA_HIGH();
}
3. 数据的有效性
SDA数据线在SCL的每个时钟周期传输一位数据(8个字节),SCL为高电平的时候SDA表示的数据有效,此时SDA为高电平表示数据 “1”,低电平表示数据 “0”;当SCL为低电平时,SDA数据无效,一般在这个时候SDA进行电平切换,为下一次数据做准备。
传输数据
- 字节格式
发送到 SDA 线上的每个字节必须为8位,每次传输可以发送的字节数量不受限制,但是每个字节后必须跟一个响应位
- 响应
数据传输必须带响应,相应的响应时钟由主机产生。响应包括“应答”(ACK)和"非应答"信号(NACK)。在第九个时钟,发送器释放SDA线(高)SDA高电平表示非应答信号,SDA低电平表示应答信号,SCL线进行高低电平的转换,会产生一个时钟信号。
EEORPM
本次实验采用的EEPROM芯片(AT24C02),将A0/A1/A2均接地,故设备地址的7位地址为0x50(1010000),读地址:0xA1(10100001),写地址:0xA0(10100000);I2C采用软件模拟的方式实现。
编程流程
- 配置外设引脚
- 编写模拟I2C时序的控制函数
- 编写基本I2C读写函数
- 编写读写EEPROM此处内容的函数
- 编写测试函数
代码实现
软件模拟I2C
void I2C_Delay(void){
uint8_t i;
for(i=10;i>0;i--);
}
/* I2C开始信号 */
void EEPROM_I2C_Start(void){
//SCL为高电平,SDA出现下降沿
SDA_HIGH();
SCL_HIGH();
I2C_Delay();
SDA_LOW();
I2C_Delay();
SCL_LOW();
I2C_Delay();
}
/* I2C结束信号 */
void EEPROM_I2C_Stop(void){
//SCL为高电平,SDA出现上升沿
SDA_LOW();
SCL_HIGH();
I2C_Delay();
SDA_HIGH();
I2C_Delay();
}
/* I2C应答信号 */
void EEPROM_I2C_ACK(void){
SDA_LOW(); //应答信号
I2C_Delay();
SCL_HIGH();//CPU产生一个时钟
I2C_Delay();
SCL_LOW();//CPU产生一个时钟
I2C_Delay();
SDA_HIGH(); //CPU释放SDA线
}
/* I2C非应答信号 */
void EEPROM_I2C_NACK(void){
SDA_HIGH(); //非应答信号
I2C_Delay();
SCL_HIGH();//CPU产生一个时钟
I2C_Delay();
SCL_LOW();//CPU产生一个时钟
I2C_Delay();
}
/* I2C等待应答 */
uint8_t EEPROM_I2C_WaitAck(void){
//返回0表示正确应答,返回1表示无器件响应
uint8_t re;
SDA_HIGH(); //CPU释放SDA总线
I2C_Delay();
SCL_HIGH();//CPU产生一个时钟
I2C_Delay();
//CPU此时读取SDA口状态(SDA低电平返回应答,高电平返回非应答)
if(EEPROM_I2C_SDA_Read())
re = 1;
else
re = 0;
SCL_LOW();
I2C_Delay();
return re;
}
/* CPU向I2C总线发送8bit数据 */
void I2C_SendByte(uint8_t Byte){
uint8_t i;
//先发送字节的高位bit7
for(i = 0; i < 8 ;i++){
if(Byte & 0x80) //判断最高位逻辑值
SDA_HIGH();
else
SDA_LOW();
I2C_Delay();
SCL_HIGH();//CPU产生一个时钟
I2C_Delay();
SCL_LOW();
if( i == 7)
SDA_HIGH(); //最后一个数据传输完毕,释放总线
Byte <<= 1; //左移一个bit,便于下一次循环发送下一位数据
I2C_Delay();
}
}
/* CPU从I2C总线读取8bit数据 */
uint8_t I2C_ReadByte(void){
uint8_t i,value;
value = 0;
//读到第一个bit为数据的 bit7
for(i = 0; i < 8 ;i++ ){
value <<= 1; //串行读
SCL_HIGH(); //产生一个时钟
I2C_Delay();
if(EEPROM_I2C_SDA_Read())
value++;
SCL_LOW();
I2C_Delay();
}
return value;
}
I2C读写EEPROM
//检查I2C设备
uint8_t ee_CheckDevice(uint8_t _Address){
uint8_t ucAck;
//发送启动信号
I2c_Start();
//发送设备地址+读写控制位
I2c_SendByte(_Address|EEPROM_I2C_WR);
ucAck=i2c_WaitAck();
I2c_Stop(); /* 发送停止信号 */
i2c_NAck(); /*若输入的是读地址,需要产生非应答信号*/
return ucAck;
}
/*
* 功能:等待EEPROM到准备状态,在写入数据后,必须调用本函数
* 写入操作时,使用 I2C把数据传输到EEPROM后,
* EEPROM会向内部空间写入数据需要一定的时间,
* 当EEPEOM写入完成后,会对I2C设备寻址有响应
*
* 调用本函数可等待至EEPROM内部时序写入完毕
*/
u8 ee_WaitStandby(void){
u32 wait_count=0;
while(ee_CheckDevice(EEPROM_DEV_ADDR))
{
//若检测超过次数,退出循环(避免死循环)
if(wait_count ++ >0xFFFF)
//等待超时
return 1;
}
return 0;
}
/*
* 功能:向串行EEPROM指定地址写入若干数据,采用页操作可以提高写入效率
*
* 形参: _usAddress:起始地址
* _usSize:数据长度,单位为字节
* _pWriteBuf:存放读到的数据的缓冲区指针
*/
u8 ee_WriteBytes(u8 *_pWriteBuf,u16 _usAddress,u16 _usSize){
u16 i,m,usAddr;
/*
写串行EEPROM不像读操作一样可以连续读取很多字节,每次操作都只能写在同一个page
对应24xx02芯片,page size =8;
简单的处理方式为:按照字节写操作模式,每写一个字节,都发送地址
提高效率采用 page write
*/
usAddr = _usAddress;
for(i=0;i<_usSize;i++){
//当发送第一个字节或者是页面首地址时,需要重新发送启动信号和地址
if((i==0)||(usAddr&(EEPROM_PAGE_SIZE-1))==0){
//第0步:发送停止信号,结束上一页的通讯,准备下一次通讯
I2c_Stop();
/* 通过检查器件应答的方式,判断内部写操作是否完成,一般小于10ms
CLK频率为KHz时,查询次数为30次左右
原理与ee_WaitStandby()函数,但是该函数检查完成后会产生停止信号,
不适用于此处
*/
for(m=0;m < 1000; m++){
//第一步:发送I2C总线信号
I2c_Start();
//第二步:发起控制字节,高7bit是地址,bit0为读写控制位,0:写,1:读
I2c_SendByte(EEPROM_DEV_ADDR|EEPROM_I2C_WR);
//第三步:发送一个时钟,判断器件是否正确应答
if(i2c_WaitAck() == 0)
break;
}
if(m == 1000)
goto cmd_fail;//EEPROM器件写超时
//第四步:发送字节地址,24c02只有256个字节,因此1个字节就够了,如果是其他型号,\
要连发多个地址
I2c_SendByte((u8)usAddr);
//第五步:等待ACK
if(i2c_WaitAck() != 0)
{
goto cmd_fail;//EEPROM器件无应答
}
}
//第六步:开始写入数据
I2c_SendByte(_pWriteBuf[i]);
//第七步:发送ACK
if(i2c_WaitAck() != 0)
goto cmd_fail;//EEPROM器件无应答
usAddr++; //地址增1
}
//命令执行成功,发送I2C总线停止信号
I2c_Stop();
//等待最后一次 EEPROM内部写入完成
if(ee_WaitStandby() == 1)
goto cmd_fail;
return 1;
cmd_fail: /* 命令执行失败后,切记发送停止信号,避免影响I2C总线上其他设备*/
I2c_Stop();
return 0;
}
/*
* 功能:从串行EEPROM指定地址读取若干数据
*
* 形参: _usAddress:起始地址
* _usSize:数据长度,单位为字节
* _pReadBuf:存放读到的数据的缓冲区指针
*/
u8 ee_ReadBytes(u8 *_pReadBuf,u16 _usAddress,u16 _usSize){
u16 i;
//第一步:发起I2C总线启动信号
I2c_Start();
//第二步:发起控制字节,高七位是地址,最后一位是读写位 ,0:写,1:读
I2c_SendByte(EEPROM_DEV_ADDR|EEPROM_I2C_WR);//写方向,写地址
//第三步:等待ACK
if(i2c_WaitAck() != 0)
goto cmd_fail; //EEPROM器件无应答
//第四步:发送字节地址,24c02只有256个字节,因此一个字节就够了
I2c_SendByte((u8)_usAddress);
//第五步:等待ACK
if(i2c_WaitAck() != 0)
goto cmd_fail; //EEPROM器件无应答
//第六步:重新启动I2C总线
//前面的目的是未来向EEPROM传送地址,下面开始读取数据
I2c_Start();
//第七步:发送控制字节
I2c_SendByte(EEPROM_DEV_ADDR|EEPROM_I2C_RD);
//第八步:发送ACK
if(i2c_WaitAck() != 0)
goto cmd_fail; //EEPROM器件无应答
//第九步:循环读取数据
for(i = 0; i < _usSize; i++){
_pReadBuf[i]=i2c_ReadByte();//读一个字节
//每次读完一个字节后,需要发送ACK,最后一个字节不需要发送ACK,发送NACK
if(i != _usSize -1)
i2c_Ack();
else
i2c_NAck();
}
//发送停止信号
I2c_Stop();
return 1; //执行成功
cmd_fail: //命令执行失败,切记发送停止信号,避免影响I2C总线上的其他设备
//I2C总线停止信号
I2c_Stop();
return 0;
}
EEPROM读写测试
//EPROM读写测试
u8 ee_test(void){
u16 i;
u8 writebuf[EEPROM_SIZE];
u8 readbuf[EEPROM_SIZE];
if(ee_CheckDevice(EEPROM_DEV_ADDR)==1){
printf("没有检测到串行EEPROM\r\n");
return 0;
}
//填充缓冲区
for(i = 0;i < EEPROM_SIZE; i++)
writebuf[i]=i;
//写
if(ee_WriteBytes(writebuf,0,EEPROM_SIZE) == 0){
printf("写EEPROM出错\r\n");
return 0;
}else
printf("写EEPROM成功\r\n");
//读
if(ee_ReadBytes(readbuf,0,EEPROM_SIZE) == 0){
printf("读EEPROM出错\r\n");
return 0;
}else
printf("读EEPROM成功\r\n");
//输出读到的数据(不一致)
printf("readbuf:\n");
for(i=0;i<EEPROM_SIZE ; i++){
if(readbuf[i] != writebuf[i]){
printf("0x%02X ", readbuf[i]);
printf("错误:EEPROM读出和写入的数据不一致\r\n");
return 0;
}
if((i % 16)== 0&&(i != 0))
printf("\n");
printf(" %02X", readbuf[i]);
}
printf("\nEEPROM 读写测试成功\r\n");
return 1;
}
硬件配置
SDA–>PB7
SCL–>PB6
结果图
成功将1~128的数据写入EEPROM,并且成功读取
而塞过 2021-2-14
版权声明:本文为CSDN博主「而塞过」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/weixin_45742100/article/details/122906373
暂无评论