个人笔记
文章目录
前几天写了一篇关于 I2C 的文章,发现 I2C 其实还挺简单的。(前提是不考虑多主机,10位寻址我也没去研究)
那篇文章的 I2C 代码是我按照 AT24C02 芯片手册里的时序图编写的(同时还参考了许多代码模板),虽然功能实现了,但总觉得自己写的代码可能不够规范。
所以今天我想使用逻辑分析仪(淘宝买的杂牌便宜货😁),对 I2C 波形一探究竟,我将拿我的代码和普中51单片机教程给出的 I2C 范例程序及正点原子STM32教程提供的 I2C 范例程序做一个简单的对比。(对比时序波形)。
【本人水平有限,本文的内容仅供参考,不能保证100%靠谱。】
1. 我写的 C51 I2C 时序
这里只分析时序图,对应 I2C 代码见文章最后一章
总览:
主机先向从机写入数据0x00,然后读取从机返回的数据0x08,从机地址(设备地址+读写位)为0xA0。
起始信号:
起始信号:时钟SCL高电平时,数据SDA由高变低,由于51单片机时钟频率低,空语句延时达到了30us,不过 I2C 周期长短只会影响它的传输速率,并不会导致传输失败。
时钟低电平修改SDA:
在时钟SCL低电平期间,改变SDA状态,下图中,SDA电平变化至时钟上升沿的时间间距为3.25us
,这是由于我代码中发送数据(改变SDA)和拉高时钟电平写在一起,中间没有任何延时代码。(总感觉这中间应该加一个延时代码,其他人的代码这里都加了延时)
传输8位地址数据:
主设备向从设备发送了1个字节的数据,这里的数据是地址数据,时钟SCL高电平时对应的数据SDA即为每位传输的数据,SDA高电平表示1,SDA为低表示0。一共8个周期,传输8位二机制数据。
等待从机应答:
在第9个周期前,需要将SDA拉高(下图SDA并没有出现高电平,说明在我们手动拉高瞬间,从机就把SDA拉低了。?),如果在第9个周期的高电平期间,从机已经将SDA拉低,说明从机发出了应答信号ACK,双方继续通信。
发送1个字节的数据:
和上面的发送地址数据相同,这里继续发送一个字节的数据。可以看到SDA一直是低,说明传输的数据是0x00。
重新起始信号:
由于接下来需要切换成读操作,所以主机需要再发送一个起始信号(又称重新起始信号)。
发送设备地址(读):
和之前的第一个发送地址数据(设备地址+读写位)的时序相同,唯一的区别是上次发的是写地址,这次发送的位读地址,为0xA1(最后1位为1,表示主机读数据)。
读取1个字节的数据:
和写操作时序类似,只是传输方向由主机->从机
变成主机<-从机
。
从机发送非应答信号+主机发送停止信号:
数据读取完成后,从机发出非应答信号(不把SDA拉低),第9个周期过后,主机发送停止信号,结束本次通信。
停止信号:SCL为高电平时,数据SDA由低变高。
2. 普中51提供的 I2C 时序
这里只分析时序图,对应 I2C 代码见文章最后一章
总览
和上文一样的操作,写三个字节(2个地址数据+1个普通数据),读取一个字节。
这份时序中,在修改SDA电平的前后都加了延时,也就是低电平的持续时间是我的两倍,也是高电平时间的两倍。
应答信号的处理方式也和我不同,这份时序的第9个周期的高电平时间不是固定的,而是只要检测到SDA被从机拉低,就算应答信号完成。所以周期信号重要的不是维持时间(但也有最小时间限制),而是上升沿信号。
其他地方和我的时序没太大区别。
3. 正点原子 STM32 的 I2C 时序
这里只分析时序图,对应 I2C 代码见文章最后一章
由于我没接 I2C 从机,所以没有应答信号。这份时序和普中51的很类似,比如低电平时长为高低平的两倍(在中间更改SDA电平),第9个周期采用循环读取应答信号,如果检测到应答信号,就将时钟拉低,否则一直检测,直到超时(循环内有计数值限制循环时间)。
另外,由于这款32位芯片的时钟频率较高,所以一个周期时长只有9.542us,而普中51的时序中周期时长为57.5us。
4. ARM Linux 内核 I2C 时序
内核的 I2C 源码我不太了解,平时只用过内核自带的 I2C 接口函数
总览
LInux 内核的 I2C 时序有个特点,起始信号、结束信号和中间数据传输直接隔得很开,读和写也分开了,但时序规则没变。
发送地址数据
一个周期的时长为4.99us,高电平2us,低电平3us,9个周期长度相同。
读取数据
5. 涉及到的 I2C 代码(软件模拟 I2C)
下面仅给出 I2C 的功能代码和简单的外设读写代码(组成完整 I2C 时序),不涉及主函数和其他无关功能代码
我的
代码仅用来参考 I2C 具体实现过程,不能直接运行
/******************************************************************************
* @ 函数名 : Delay_10us
* @ 功 能 : 10us粗略延时
* @ 参 数 : 延时时间--单位10us
* @ 返回值 : 无
******************************************************************************/
void Delay_10us(unsigned int time)
{
while(time--);
}
/******************************************************************************
* @ 函数名 : I2c_Delay
* @ 功 能 : I2C延时函数
* @ 参 数 : 无
* @ 返回值 : 无
******************************************************************************/
void I2c_Delay()
{
Delay_10us(1);
}
/******************************************************************************
* @ 函数名 : I2c_Start
* @ 功 能 : I2C起始信号
* @ 参 数 : 无
* @ 返回值 : 无
******************************************************************************/
void I2c_Start()
{
sda = 1;
scl = 1;
I2c_Delay(); //起始信号建立时间
sda = 0; //SDA拉低,下降沿
I2c_Delay(); //起始信号保持时间
scl = 0;
}
/******************************************************************************
* @ 函数名 : I2c_Stop
* @ 功 能 : I2C停止信号
* @ 参 数 : 无
* @ 返回值 : 无
******************************************************************************/
void I2c_Stop()
{
scl = 0;
I2c_Delay(); //上一个时钟周期的低电平
sda = 0;
scl = 1;
I2c_Delay(); //停止信号建立时间
sda = 1; //SDA拉高,上升沿
I2c_Delay(); //总线空闲时间保持
}
/******************************************************************************
* @ 函数名 : I2c_Ack
* @ 功 能 : I2C应答信号
* @ 参 数 : 无
* @ 返回值 : 无
******************************************************************************/
void I2c_Ack()
{
scl = 0;
sda = 0; //SDA拉低,发出应答信号
I2c_Delay();
scl = 1;
I2c_Delay();
scl = 0;
}
/******************************************************************************
* @ 函数名 : I2c_No_Ack
* @ 功 能 : I2C非应答信号
* @ 参 数 : 无
* @ 返回值 : 无
******************************************************************************/
void I2c_No_Ack()
{
scl = 0;
sda = 1; //SDA拉高,发出非应答信号
I2c_Delay();
scl = 1;
I2c_Delay();
scl = 0;
}
/******************************************************************************
* @ 函数名 : I2c_Wait_Ack
* @ 功 能 : I2C等待应答信号
* @ 参 数 : 无
* @ 返回值 : 1:接收到应答信号,0:接收到非应答信号
******************************************************************************/
unsigned char I2c_Wait_Ack()
{
unsigned char ack = 0;
sda = 1;
scl = 0;
I2c_Delay();
scl = 1;
I2c_Delay();
if(sda == 0) //检测数据线SDA是否被拉低
ack = 1;
else
ack = 0;
scl = 0;
return ack;
}
/******************************************************************************
* @ 函数名 : I2c_Write_Byte
* @ 功 能 : I2C写字节
* @ 参 数 : dat 要写入的字节数据
* @ 返回值 : 无
******************************************************************************/
void I2c_Write_Byte(unsigned char dat)
{
unsigned char i = 0;
for(i = 0; i < 8; i++) //读取8位
{
scl = 0;
I2c_Delay();
if(dat & 0x80) //发送最高位
sda = 1;
else
sda = 0;
scl = 1;
I2c_Delay();
dat <<= 1; //左移1位
}
scl = 0;
}
/******************************************************************************
* @ 函数名 : I2c_Read_Byte
* @ 功 能 : I2C读字节
* @ 参 数 : 无
* @ 返回值 : 读取的字节数据
******************************************************************************/
unsigned char I2c_Read_Byte()
{
unsigned char dat = 0, i = 0;
for(i = 0; i < 8; i++) //读取8位
{
dat <<= 1; //左移1位
scl = 0;
I2c_Delay();
scl = 1; //SCL高电平
I2c_Delay();
if(sda) //读取SDA状态
dat |= 0x1;
}
scl = 0;
return dat;
}
/******************************************************************************
* @ 函数名 : At24c02_Write
* @ 功 能 : AT24C02写字节
* @ 参 数 :
* addr 要写数据的地址(存储空间)
* dat 要写入的字节数据
* @ 返回值 : 无
******************************************************************************/
void At24c02_Write(unsigned char addr, unsigned char dat)
{
I2c_Start();
I2c_Write_Byte(0xA0); //发送I2C设备地址
I2c_Wait_Ack(); //等待从机响应
I2c_Write_Byte(addr); //发送要写入的内存地址
I2c_Wait_Ack();
I2c_Write_Byte(dat); //写入数据
I2c_Wait_Ack();
I2c_Stop();
Delay_10us(1000); //写周期
}
/******************************************************************************
* @ 函数名 : At24c02_Read
* @ 功 能 : AT24C02读字节
* @ 参 数 : addr 要读数据的地址(存储空间)
* @ 返回值 : 读取的字节数据
******************************************************************************/
unsigned char At24c02_Read(unsigned char addr)
{
unsigned char dat = 0, i = 0;
I2c_Start();
I2c_Write_Byte(0xA0); //发送I2C设备地址
I2c_Wait_Ack(); //等待从机响应
I2c_Write_Byte(addr); //发送要写入的内存地址
I2c_Wait_Ack();
I2c_Start();
I2c_Write_Byte(0xA1); //发送I2C设备地址(读数据)
I2c_Wait_Ack(); //等待从机响应
dat = I2c_Read_Byte(); //读取数据
I2c_Wait_Ack();
I2c_Stop();
return dat;
}
————————————————
版权声明:本文为CSDN博主「小辉_Super」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/weixin_43772810/article/details/122149151
普中科技的
代码仅用来参考 I2C 具体实现过程,不能直接运行
#include"i2c.h"
/*******************************************************************************
* 函数名 : Delay10us()
* 函数功能 : 延时10us
* 输入 : 无
* 输出 : 无
*******************************************************************************/
void Delay10us()
{
unsigned char a,b;
for(b=1;b>0;b--)
for(a=2;a>0;a--);
}
/*******************************************************************************
* 函数名 : I2cStart()
* 函数功能 : 起始信号:在SCL时钟信号在高电平期间SDA信号产生一个下降沿
* 输入 : 无
* 输出 : 无
* 备注 : 起始之后SDA和SCL都为0
*******************************************************************************/
void I2cStart()
{
SDA=1;
Delay10us();
SCL=1;
Delay10us();//建立时间是SDA保持时间>4.7us
SDA=0;
Delay10us();//保持时间是>4us
SCL=0;
Delay10us();
}
/*******************************************************************************
* 函数名 : I2cStop()
* 函数功能 : 终止信号:在SCL时钟信号高电平期间SDA信号产生一个上升沿
* 输入 : 无
* 输出 : 无
* 备注 : 结束之后保持SDA和SCL都为1;表示总线空闲
*******************************************************************************/
void I2cStop()
{
SDA=0;
Delay10us();
SCL=1;
Delay10us();//建立时间大于4.7us
SDA=1;
Delay10us();
}
/*******************************************************************************
* 函数名 : I2cSendByte(unsigned char dat)
* 函数功能 : 通过I2C发送一个字节。在SCL时钟信号高电平期间,保持发送信号SDA保持稳定
* 输入 : num
* 输出 : 0或1。发送成功返回1,发送失败返回0
* 备注 : 发送完一个字节SCL=0,SDA=1
*******************************************************************************/
unsigned char I2cSendByte(unsigned char dat)
{
unsigned char a=0,b=0;//最大255,一个机器周期为1us,最大延时255us。
for(a=0;a<8;a++)//要发送8位,从最高位开始
{
SDA=dat>>7; //起始信号之后SCL=0,所以可以直接改变SDA信号
dat=dat<<1;
Delay10us();
SCL=1;
Delay10us();//建立时间>4.7us
SCL=0;
Delay10us();//时间大于4us
}
SDA=1;
Delay10us();
SCL=1;
while(SDA)//等待应答,也就是等待从设备把SDA拉低
{
b++;
if(b>200) //如果超过2000us没有应答发送失败,或者为非应答,表示接收结束
{
SCL=0;
Delay10us();
return 0;
}
}
SCL=0;
Delay10us();
return 1;
}
/*******************************************************************************
* 函数名 : I2cReadByte()
* 函数功能 : 使用I2c读取一个字节
* 输入 : 无
* 输出 : dat
* 备注 : 接收完一个字节SCL=0,SDA=1.
*******************************************************************************/
unsigned char I2cReadByte()
{
unsigned char a=0,dat=0;
SDA=1; //起始和发送一个字节之后SCL都是0
Delay10us();
for(a=0;a<8;a++)//接收8个字节
{
SCL=1;
Delay10us();
dat<<=1;
dat|=SDA;
Delay10us();
SCL=0;
Delay10us();
}
return dat;
}
/*******************************************************************************
* 函数名 : void At24c02Write(unsigned char addr,unsigned char dat)
* 函数功能 : 往24c02的一个地址写入一个数据
* 输入 : 无
* 输出 : 无
*******************************************************************************/
void At24c02Write(unsigned char addr,unsigned char dat)
{
I2cStart();
I2cSendByte(0xa0);//发送写器件地址
I2cSendByte(addr);//发送要写入内存地址
I2cSendByte(dat); //发送数据
I2cStop();
}
/*******************************************************************************
* 函数名 : unsigned char At24c02Read(unsigned char addr)
* 函数功能 : 读取24c02的一个地址的一个数据
* 输入 : 无
* 输出 : 无
*******************************************************************************/
unsigned char At24c02Read(unsigned char addr)
{
unsigned char num;
I2cStart();
I2cSendByte(0xa0); //发送写器件地址
I2cSendByte(addr); //发送要读取的地址
I2cStart();
I2cSendByte(0xa1); //发送读器件地址
num=I2cReadByte(); //读取数据
I2cStop();
return num;
}
正点原子的
代码仅用来参考 I2C 具体实现过程,不能直接运行
//
//本程序只供学习使用,未经作者许可,不得用于其它任何用途
//ALIENTEK战舰STM32开发板
//IIC驱动 代码
//正点原子@ALIENTEK
//技术论坛:www.openedv.com
//修改日期:2012/9/9
//版本:V1.0
//版权所有,盗版必究。
//Copyright(C) 广州市星翼电子科技有限公司 2009-2019
//All rights reserved
//
//初始化IIC
void IIC_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd( RCC_APB2Periph_GPIOA, ENABLE ); //使能GPIOB时钟
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6|GPIO_Pin_7;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP ; //推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_SetBits(GPIOA,GPIO_Pin_6|GPIO_Pin_7); //PB6,PB7 输出高
}
//产生IIC起始信号
void IIC_Start(void)
{
SDA_OUT(); //sda线输出
IIC_SDA=1;
IIC_SCL=1;
delay_us(4);
IIC_SDA=0;//START:when CLK is high,DATA change form high to low
delay_us(4);
IIC_SCL=0;//钳住I2C总线,准备发送或接收数据
}
//产生IIC停止信号
void IIC_Stop(void)
{
SDA_OUT();//sda线输出
IIC_SCL=0;
IIC_SDA=0;//STOP:when CLK is high DATA change form low to high
delay_us(4);
IIC_SCL=1;
IIC_SDA=1;//发送I2C总线结束信号
delay_us(4);
}
//等待应答信号到来
//返回值:1,接收应答失败
// 0,接收应答成功
u8 IIC_Wait_Ack(void)
{
u8 ucErrTime=0;
SDA_IN(); //SDA设置为输入
IIC_SDA=1;delay_us(1);
IIC_SCL=1;delay_us(1);
while(READ_SDA)
{
ucErrTime++;
if(ucErrTime>250)
{
IIC_Stop();
return 1;
}
}
IIC_SCL=0;//时钟输出0
return 0;
}
//产生ACK应答
void IIC_Ack(void)
{
IIC_SCL=0;
SDA_OUT();
IIC_SDA=0;
delay_us(2);
IIC_SCL=1;
delay_us(2);
IIC_SCL=0;
}
//不产生ACK应答
void IIC_NAck(void)
{
IIC_SCL=0;
SDA_OUT();
IIC_SDA=1;
delay_us(2);
IIC_SCL=1;
delay_us(2);
IIC_SCL=0;
}
//IIC发送一个字节
//返回从机有无应答
//1,有应答
//0,无应答
void IIC_Send_Byte(u8 txd)
{
u8 t;
SDA_OUT();
IIC_SCL=0;//拉低时钟开始数据传输
for(t=0;t<8;t++)
{
//IIC_SDA=(txd&0x80)>>7;
if((txd&0x80)>>7)
IIC_SDA=1;
else
IIC_SDA=0;
txd<<=1;
delay_us(2); //对TEA5767这三个延时都是必须的
IIC_SCL=1;
delay_us(2);
IIC_SCL=0;
delay_us(2);
}
}
//读1个字节,ack=1时,发送ACK,ack=0,发送nACK
u8 IIC_Read_Byte(unsigned char ack)
{
unsigned char i,receive=0;
SDA_IN();//SDA设置为输入
for(i=0;i<8;i++ )
{
IIC_SCL=0;
delay_us(2);
IIC_SCL=1;
receive<<=1;
if(READ_SDA)receive++;
delay_us(1);
}
if (!ack)
IIC_NAck();//发送nACK
else
IIC_Ack(); //发送ACK
return receive;
}
//在AT24CXX指定地址读出一个数据
//ReadAddr:开始读数的地址
//返回值 :读到的数据
u8 AT24CXX_ReadOneByte(u16 ReadAddr)
{
u8 temp=0;
IIC_Start();
if(EE_TYPE>AT24C16)
{
IIC_Send_Byte(0XA0); //发送写命令
IIC_Wait_Ack();
IIC_Send_Byte(ReadAddr>>8);//发送高地址
IIC_Wait_Ack();
}else IIC_Send_Byte(0XA0+((ReadAddr/256)<<1)); //发送器件地址0XA0,写数据
IIC_Wait_Ack();
IIC_Send_Byte(ReadAddr%256); //发送低地址
IIC_Wait_Ack();
IIC_Start();
IIC_Send_Byte(0XA1); //进入接收模式
IIC_Wait_Ack();
temp=IIC_Read_Byte(0);
IIC_Stop();//产生一个停止条件
return temp;
}
//在AT24CXX指定地址写入一个数据
//WriteAddr :写入数据的目的地址
//DataToWrite:要写入的数据
void AT24CXX_WriteOneByte(u16 WriteAddr,u8 DataToWrite)
{
IIC_Start();
if(EE_TYPE>AT24C16)
{
IIC_Send_Byte(0XA0); //发送写命令
IIC_Wait_Ack();
IIC_Send_Byte(WriteAddr>>8);//发送高地址
}else
{
IIC_Send_Byte(0XA0+((WriteAddr/256)<<1)); //发送器件地址0XA0,写数据
}
IIC_Wait_Ack();
IIC_Send_Byte(WriteAddr%256); //发送低地址
IIC_Wait_Ack();
IIC_Send_Byte(DataToWrite); //发送字节
IIC_Wait_Ack();
IIC_Stop();//产生一个停止条件
delay_ms(10);
}