【自学嵌入式:51单片机】I2C通信
所有自学嵌入式:51单片机系列的操作相关器件的代码都汇总到了:https://gitee.com/qin-ruiqian/Qin51
I2C总线
I2C总线(读作I方C总线),全称Inter-Integrated Circuit,它是由飞利浦公司研发的二线制、多主多从、双向、同步、半双工、带数据应答的串行总线。I2C通信就是利用I2C总线进行数据传输,通用的I2C总线可以使各种设备的通信标准统一,对于厂家来说,使用成熟的方案可以缩短芯片设计周期、提高稳定性,对于应用者来说,使用通用的通信协议可以避免学习各种各样的自定义协议,降低了学习和应用的难度。
两线制
只需要两个引脚进行数据传输
设备间能发送数据,主要是通过SDA这个数据引脚传输,传低电平把SDA拉低,传高电平就是把SDA拉高

用时钟信号SCL引脚控制传输的节拍。
多主多从
多主多从是指在这一个I2C总线上可以有多个主机和多个从机

当然也可以一主一从或者一主多从


主机多为控制芯片(单片机),从机多为显示设备或传感器等

主机是可以发出命令的设备,从机是能接收命令的设备

通信规则:
- 任何时刻,从机只接收一个主机的命令;
- 多主机情况下,从机优先接收最先发送命令的主机的命令;
- 为了区分命令发给谁,每个设备都有自己的地址,即每个设备都有设备ID。
双向
双向是指,主机可以向从机发送数据,也可以接收从机发来的数据,从机可以接收主机发来的数据,也可以向主机发送数据,传输数据的方向是双向的
同步通信

I2C通信通过设备的SCL引脚传递时钟信号,对数据传输进行同步
半双工

数据传送虽然是双向,但是数据线就一根,需要分时传送,即任何时刻只能有一个方向的数据传输
串行通信
传输数据时是一位一位从高位开始从高到底传输,也就是发送一个字节,首先发送的是这个字节对应的最高位二进制,接收一个字节也是同理
I2C通信数据传输流程

一次完整的I2C通信包括:(1)启动信号;(2)发送地址位和读写位(放在一个字节里面发送);(3)接收应答位;(4)发送数据(接收数据);(5)接收(发送)应答位;(6)停止信号
启动信号

每一次I2C通信,先以一个启动信号开始,I2C通信有固定的启动信号,在程序中只要按具体时序图要求发送启动信号。
如上图,也就是SDA下降沿,SCL高电平。
地址位和读写位
靠地址位找到对应的通信设备,也称为寻址,每个从机都有一个固定的器件地址,有7位的,有10位的,下面只介绍7位的地址,每次通信设备的地址要设定准确。

比如olde屏幕,通常是主机对其进行写操作,读写位置0,其他7位记录要通信的设备地址
应答信号

主机在发送为地址位和读写位后,就会等待接收一位数据,根据这个数据判断从机是否正确地应答了,数据0表示接收数据成功,即正确应答,数据1表示未成功接收,即非应答。
主机接收完数据同样要给从机发送应答信号,也是数据0表示接收数据成功,即正确应答,数据1表示未成功接收,即非应答。
发送数据
依次将一个字节8位二进制从高位到低位发送
停止信号

SCL高电平,SDA上升沿,一次I2C通信完成就要以停止信号结束
按读和写两种模式来看I2C通信
按读和写两种不同模式,I2C通信过程也有所不同
写模式:

读模式:

当从机内部也有不同寄存器代表不同功能时,就需要区分,发布的数据是要写入哪个寄存器的,在发送完地址位和读写位并收到应答位后,要先发送内部寄存器的地址,仍然是将地址以一个8位数据发送过去,收到应答后,收到应答后,再继续发送要写入到这个寄存器的数据

I2C电路规范
- 所有I2C设备的SCL连接在一起,SDA连在一起
- 设备的SCL和SDA均要配置成开漏输出模式
- SCL和SDA各添加一个上拉电阻,组织一般为4.7kΩ左右
- 开漏输出和上拉电阻的共同作用实现了“线与”的功能,此设计主要是为了解决多机通信互相干扰的问题。
![image]()
开漏输出,输出高电平的时候相当于引脚断开,CPU想要和其中一个设备通信,最好的状态是其他的设备别影响当前通信的设备,开漏输出相当于把其他设备断开。
![image]()
在外面接两个电阻增强上拉能力,相当于CPU想发送0的时候拉到低电平,想发1的时候松手就不拉了,然后会被外部的电阻自动给拉到高电平
![image]()
SCLK IN是SCLK的检测模式,它的输入阻抗很大,相当于断开,它相当于电压表,检测SCLK点的电压,SCLKN1 OUT是MOS管(电子开关),给低电平导通,给高电平断开,就相当于发0需要手动拉成0,发完0自动缩回1,如下图两个小人通过木杆子通信
![image]()
如果有多个设备,主机通过发送对应设备地址,让对应的通信设备知道要和主机通信了
【注】开漏输出像个 "单方向开关",只能把信号拉低,想输出高电平得靠外部电路帮忙;推挽输出像 "双向开关",自己既能把信号拉高也能拉低,不用麻烦外部电路。
I2C数据帧
发送一帧数据

S是开始信号,P是停止信号
S之后一定要发从机地址+读写位(一共8位一个字节)
然后接收应答信号
最后是一个字节+接收一个应答信号,一直到传输完成
接收一帧数据

S是开始信号,P是停止信号
S之后一定要发从机地址+读写位(一共8位一个字节)
然后发送应答信号
最后是一个字节+发送一个应答信号,一直到接收完成
复合格式(先发送再接收数据帧)

把发数据帧的停止信号去掉,直接拼接到接收数据帧中即可,发送的部分可能是一大堆指令,最后再接收
I2C通信总结

代码实现
(提前说明:江科大版本没有延迟,沈建大这个版本有延迟,这是因为有延迟这个版本按照波特率100K去发送,并且便于移植)

I2C通信的波特率是100K,也就是每秒传送10万个位,取倒数,一位传送实际是10微秒,也就是SCL的时钟周期不能小于10微秒

也就是它的高电平和低电平的总时长不能小于10微秒

所以平均一下就是高电平和低电平的时间都不要小于5微秒
我这个板子的晶振是11.0592Mhz,一个机器周期是正好的1微秒,Delay函数中四个空语句,恰好是4个机器周期,也就是4微秒,在程序中利用这一语句实现传输速率的调整
//I2C通信专用延时程序
void delayI2C()
{
_nop_();
_nop_();
_nop_();
_nop_();
}
发送启动信号

//初始化I2C通信
void initI2C()
{
//先都拉高成高电平
SCL = 1;
SDA = 1;
//下降沿
delayI2C();
SDA = 0;
//保证不会产生额外的时序,SCL最后也拉低
delayI2C();
SCL = 0;
}
【注】REGX52.H并没有包含SCL SDA位,要看对应的通信的模块的引脚和单片机引脚的连接
发送停止信号

SCL先高电平,SDA上升沿
//关闭I2C通信
void closeI2C()
{
//先都拉低
SCL = 0;
SDA = 0;
delayI2C();
SCL = 1; //先拉高SCL
delayI2C();
SDA = 1; //SCL高电平期间,再拉高SDA,这样就产生了一个上升沿
delayI2C();
}
发送数据

发送数据的规则就是,在SCL低电平期间,将要传送的数据位放到SDA线上,然后拉高SCL,从机在SCL高电平期间,读取SDA线上的数据依次循环8次就可以将一个字节的数据传送出去,SCL高电平期间SDA不允许有数据变化。
//I2C写入一个字节,I2C_Byte是待传送的数据
void writeByteI2C(unsigned char I2C_Byte)
{
unsigned char i = 0;
for(i = 0; i<8; i++)
{
//I2C_Byte & 0x80(1000 0000)
//获取最高位,看是1还是0
if(I2C_Byte & 0x80)
{
SDA = 1;
}
else
{
SDA = 0;
}
delayI2C();
SCL = 1;
delayI2C();
SCL = 0;
I2C_Byte <<= 1; //左移,循环读取每一位,让每一位都成为首位
}
}
接收数据
SCL低电平期间,从机将数据位依次放到SDA线上(高位在前),然后拉高SCL,主机将在SCL高电平期间读取数据位,所以SCL高电平期间SDA不允许有数据变化,依次循环上述过程8次,即可接收一个字节(主机在接收之前,需要释放SDA)。
// I2C读一个字节
unsigned char readByteI2C()
{
unsigned char i, Byte = 0x00;
for(i = 0; i<8; i++)
{
SDA = 1; //拉高SDA,释放I2C总线,控制权交给从机
//读总线之前一定要将SDA拉高
SCL = 1; //然后将SCL拉高,准备接收SDA状态
delayI2C();
if(SDA)
{
Byte |= (0x80>>i); //依次循环右移,得到一个字节(0x00分别或每一位得到,如果是低电平,不需要操作,该位就是0)
}
SCL = 0;
}
return Byte;
}
接收应答信号
在发送完一个字节之后,主机在下一个时钟接收一位数据,判断从机是否应答,数据0表示应答,数据1表示非应答(主机在接收之前,需要释放SDA)

//读取I2C总线应答信号
bit readAckI2C()
{
bit Ack_bit; //应答信号
SDA = 1; //拉高SDA,释放I2C
delayI2C();
SCL = 1; // 把SCL拉高,第9个时钟信号
delayI2C();
Ack_bit = SDA; //读取应答信号
delayI2C();
SCL = 0; //拉低SCL,结束接收应答
return Ack_bit;
}
发送应答信号
主机在接收完一个字节后,会给从机发送应答信号,主机成功接收,会在数据线SDA发送0,接收失败会发送1

//I2C发送应答位
void writeAckI2C(bit Ack_bit)
{
//根据应答状态变量来确定SDA是1还是0
if(Ack_bit)
{
SDA = 1;
}
else
{
SDA = 0;
}
//第9个时钟周期
//拉高SCL,再拉低SCL,完成一次发送应答信号
SCL = 1;
SCL = 0;
}




浙公网安备 33010602011771号