【I2C通讯协议】I2C入门认知篇
一、I2C协议简介
1.I2C概述
IIC全称Inter-Integrated Circuit (集成电路总线) 是由PHILIPS公司在80年代开发的两线式串行总线,用于连接微控制器及其外围设备。IIC属于半双工(同一时间只可以单向通信)同步通信方式。
多设备之间的通讯如果使用串口通讯,则每个设备要有足够的独立串口收发组,而采用I2C就能极大简化设备硬件要求和电路。
I2C一般采用一主多从,其中任何能够进行发送和接收的设备都可以成为主总线。一个主控能够控制信号的传输和时钟频率。当然,在任何时间点上只能有一个主控。
串口通讯:
I2C通讯:
2.I2C构成
- IIC串行总线一般有两根信号线,一根是双向的数据线SDA,另一根是时钟线SCL,其时钟信号是由主控器件产生。
- 所有接到IIC总线设备上的串行数据SDA都接到总线的SDA上,各设备的时钟线SCL接到总线 的SCL上。
- 对于并联在一条总线上的每个IC都有唯一的地址。
3.IIC协议规则
IIC总线在传输数据的过程中一共有三种类型信号,分别为:开始信号、结束信号和应答信号。
起始位,停止位,数据位,速度 这些信号中,起始信号是必需的,结束信号和应答信号
3.1 空闲状态(初始状态)
因为IIC的 SCL 和SDA 都需要接上拉电阻,保证空闲状态的稳定性
所以IIC总线在空闲状态下SCL 和SDA都保持高电平
3.2 起始信号
SCL保持高电平,SDA由高电平变为低电平后,延时(>4.7us),SCL变为低电平, 比如发送设备唯一地址
3.3 终止信号(停止信号)
SCL保持高电平,SDA由低电平变为高电平
3.4 应答信号(逻辑“0” “1”)
每当主机向从机发送完一个字节的数据,主机总是需要等待从机给出一个应答信号,以确认从机是否成功接收到了数据。
- 应答信号:主机SCL拉高,读取从机SDA的电平,为低电平表示产生应答。
- 应答信号为低电平时,规定为有效应答位(ACK,简称应答位),表示接收器已经成功地接收了该字节;
- 应答信号为高电平时,规定为非应答位(NACK),一般表示接收器接收该字节没有成功。
例如:
- 每发送一个字节(8个bit), 在一个字节传输的8个时钟后的第九个时钟期间,接收器接收数据后必须回一个ACK应答信号给发送器,这样才能进行数据传输。
- 应答出现在每一次主机完成8个数据位传输后紧跟着的时钟周期,低电平0表示应答,1表示非应答。
3.5 数据有效性
IIC信号在数据传输过程中,当SCL=1高电平时,数据线SDA必须保持稳定状态,不允许有电平跳变,只有在时钟线上的信号为低电平期间,数据线上的高电平或低电平状态才允许变化。
SCL=1时 数据线SDA的任何电平变换会看做是总线的起始信号或者停止信号。
也就是在IIC传输数据的过程中,SCL时钟线会频繁的转换电平,以保证数据的传输
3.6 总线数据发送时序图
-
写数据帧
-
读数据帧
-
读 写 数据帧
4. I2C扩展:IIC为什么用开漏输出和上拉电阻?
4.1 开漏输出的作用
作用1:防止短路
I2C协议支持多个主设备与多个从设备在一条总线上,如果不用开漏输出,而用推挽输出,会出现主设备之间短路的情况。所以总线一般会使用开漏输出。
- 在一些情况下(比如总线), 多个GPIO口可能会连接在同一根线上, 存在某个GPIO输出高电平, 另一个GPIO输出低电平的情况. 如果使用推挽输出, 你会发现这个GPIO的VCC和另一个GPIO的GND接在了一起, 也就是短路了(凉凉了).
- 如果换成开漏输出呢? VCC和GND多了个电阻, 这样电路就是安全的.所以总线一般会使用开漏输出
作用2:线与
- 开漏输出还能实现 线与 (自行百度), 减少一个与门, 简化电路
4.2 上拉电阻的作用
接上拉电阻是因为I2C通信需要输出高电平的能力。一般开漏输出无法输出高电平,如果在漏极接上拉电阻,则可以进行电平转换。
I2C由两条总线SDA和SCL组成。连接到总线的器件的输出级必须是漏极开路,都通过上拉电阻连接到电源,这样才能够实现“线与”功能。当总线空闲时,这两条线路都是高电平。
注:上拉电阻选取
上拉电阻阻值怎么确定?
- 一般IO端口的驱动能力在2mA~4mA量级。
阻值不能过小:
- 功耗问题。如果上拉阻值过小,VDD灌入端口的电流将较大,功耗会很大,导致端口输出的低电平值增大(I2C协议规定,端口输出低电平的最高允许值为0.4V)。故通常上拉电阻应选取不低于1K的电阻(当VDD=3V时,灌入电流不超过3mA)。
阻值不能过大:
- 速度问题。它取决于上拉电阻和线上电容形成的RC延时,RC延时越大,波形越偏离方波趋向于正弦波,数据读写正确的概率就越低,所以上拉电阻不能过大。
I2C总线上的负载电容不能超过400pF。当I2C总线上器件逐渐增多时,总线负载电容也相应增加。当总的负载电容大于400pF时,就不能可靠的工作。这也是I2C的局限性。
(建议上拉电阻可选用1.5K,2.2K,4.7K)
4.3 I2C总线基本操作
-
IIC只有两根线(SCL和SDA), 怎么判断哪个主设备占用总线(当然是先来后到了).
-
假设主设备A需要启动IIC, 他需要在SCL高电平时, 将SDA由高电平转换为低电平作为启动信号. 主设备A在把SDA拉高后, 它需要再检查一下SDA的电平.
-
SDA是高电平, 说明主设备A可以占用总线, 然后主设备A将SDA拉低, 开始通信.
-
SDA是低电平, 说明有人已经捷足先登了, 主设备A不能占用总线, 结束通信.
-
为什么? 因为线与. 如果主设备A拉高SDA时, 已经有其他主设备将SDA拉低了. 由于 1 & 0 = 0 那么主设备A在检查SDA电平时, 会发现不是高电平, 而是低电平. 说明其他主设备抢占总线的时间比它早, 主设备A只能放弃占用总线. 如果是高电平, 则可以占用.
二、实现软件I2C(直接上手)
1.软硬件I2C概述
I2C有两种实现方式,一种是硬件I2C,另一种是软件I2C
- 软件I2C一般是用GPIO引脚,用代码控制管脚状态以模拟I2C通讯波形。
- 硬件I2C对应芯片上的I2C外设,有相应I2C驱动电路,其所使用的I2C管脚也是专用,直接调用内部寄存器进行配置通讯波形(类似有封装好的波形通讯函数可直接调用)。
硬件I2C的效率要远高于软件的,而软件I2C由于不受管脚限制,接口比较灵活
- 软件I2C 是通过GPIO,软件模拟寄存器的工作方式,而硬件(固件)I2C是直接调用内部寄存器进行配置。
- 如果要从具体硬件上来看,可以去看下芯片手册。因为固件I2C的端口是固定的,所以会有所区别
2.代码实现I2C时序
总的来说,软件I2C就是要编写代码封装“实现该通讯波形时序”的函数(根据时序控制GPIO引脚的电平变化),方便后续调用
2.1 起始信号函数
scl、sda为单片机上两个不同的gpio引脚
//起始信号
void I2C_Start()
{
scl = 1;
sda = 1;
delay5us();
sda = 0;
delay5us();
scl = 0;
}
2.2 终止信号函数
//停止信号
void I2C_Stop()
{
scl = 1;
sda = 0;
delay5us();
sda = 1;
delay5us();
sda = 0;
}
2.3 应答信号函数(获取从机应答信号:“0”或“1”)
//获取应答信号,逻辑“0”或“1”
char I2C_ACK()
{
char flag; //应答标志
scl = 0; //先拉低SCL,使得SDA数据可以发生改变
sda = 1; //就在时钟脉冲9期间释放数据线
delay5us();
scl = 1;
delay5us();
flag = sda; //获取从机返回的应答信号“0”或“1”
delay5us();
scl = 0;
delay5us();
return flag;
}
2.4 发送字节数据函数(当scl=1时,sda状态不变,则认为sda为传输的数据)
数据传输格式
-
SDA线上的数据在SCL时钟“高”期间必须是稳定的,只有当SCL线上的时钟信号为低时,数据线上的“高”或“低”状态才可以改变
-
输出到SDA线上的每个字节必须是8位,数据传送时,先传送最高位(MSB),每一个被传送的字节后面都必须跟随一位应答位(即一帧共有9位)
-
当一个字节按数据位从高位到低位的顺序传输完后,紧接着从设备将拉低SDA线,回传给主设备一个应答位ACK, 此时才认为一个字节真正的被传输完成 ,如果一段时间内没有收到从机的应答信号,则自动认为从机已正确接收到数据
// 发送字节数据函数
void I2C_Send_Byte(char dataSend)
{
int i;
for(i=0; i<8; i++){
scl = 0; //scl拉低,让sda做好数据准备
//1000 0000 获取dataSend的最高位,给sda
sda = dataSend & 0x80;
delay5us(); //发送数据建立时间
scl = 1; //scl拉高开始发送
delay5us(); //数据发送时间
scl = 0; //发送完毕拉低
delay5us();
dataSend = dataSend << 1; //数据左移
}
}
3.I2C实战(暂不作要求,后续可单独学习)
3.1 硬件准备
- 0.96寸 SSD1306 OLED
- STC89C52开发板(这里以C51/52为例)
3.2 OLED模块介绍
该模块是IIC接口,引脚分别为VCC,GND,SCL,SDA
该模块的写入时序,如下图:
可结合后面的代码注释进行理解(在此不作过多介绍)
写命令/数据的代码:
/*
1. start()
2. 写入 b0111 1000 0x78
3. ACK
4. cotrol byte: (0)(0)000000 写入命令 (0)(1)000000写入数据
5. ACK
6. 写入指令/数据
7. ACK
8. STOP
*/
//写命令
void Oled_Write_Cmd(char dataCmd)
{
// 1. start()
I2C_Start();
// 2. 写入从机地址 b0111 1000 0x78
I2C_Send_Byte(0x78);
// 3. ACK
I2C_ACK();
// 4. cotrol byte: (0)(0)000000 写入命令 (0)(1)000000写入数据
I2C_Send_Byte(0x00);
// 5. ACK
I2C_ACK();
//6. 写入指令/数据
I2C_Send_Byte(dataCmd);
//7. ACK
I2C_ACK();
//8. STOP
I2C_Stop();
}
//写数据
void Oled_Write_Data(char dataData)
{
// 1. start()
I2C_Start();
// 2. 写入从机地址 b0111 1000 0x78
I2C_Send_Byte(0x78);
// 3. ACK
I2C_ACK();
// 4. cotrol byte: (0)(0)000000 写入命令 (0)(1)000000写入数据
I2C_Send_Byte(0x40);
// 5. ACK
I2C_ACK();
///6. 写入指令/数据
I2C_Send_Byte(dataData);
//7. ACK
I2C_ACK();
//8. STOP
I2C_Stop();
}
OLDE模块的初始化和清屏函数代码:
//初始化
void Oled_Init(void){
Oled_Write_Cmd(0xAE);//--display off
Oled_Write_Cmd(0x00);//---set low column address
Oled_Write_Cmd(0x10);//---set high column address
Oled_Write_Cmd(0x40);//--set start line address
Oled_Write_Cmd(0xB0);//--set page address
Oled_Write_Cmd(0x81); // contract control
Oled_Write_Cmd(0xFF);//--128
Oled_Write_Cmd(0xA1);//set segment remap
Oled_Write_Cmd(0xA6);//--normal / reverse
Oled_Write_Cmd(0xA8);//--set multiplex ratio(1 to 64)
Oled_Write_Cmd(0x3F);//--1/32 duty
Oled_Write_Cmd(0xC8);//Com scan direction
Oled_Write_Cmd(0xD3);//-set display offset
Oled_Write_Cmd(0x00);//
Oled_Write_Cmd(0xD5);//set osc division
Oled_Write_Cmd(0x80);//
Oled_Write_Cmd(0xD8);//set area color mode off
Oled_Write_Cmd(0x05);//
Oled_Write_Cmd(0xD9);//Set Pre-Charge Period
Oled_Write_Cmd(0xF1);//
Oled_Write_Cmd(0xDA);//set com pin configuartion
Oled_Write_Cmd(0x12);//
Oled_Write_Cmd(0xDB);//set Vcomh
Oled_Write_Cmd(0x30);//
Oled_Write_Cmd(0x8D);//set charge pump enable
Oled_Write_Cmd(0x14);//
Oled_Write_Cmd(0xAF);//--turn on oled panel
}
//清屏
void Oled_Clear()
{
unsigned char i,j; //-128 --- 127
for(i=0;i<8;i++){
Oled_Write_Cmd(0xB0 + i);//page0--page7
//每个page从0列
Oled_Write_Cmd(0x00);
Oled_Write_Cmd(0x10);
//0到127列,依次写入0,每写入数据,列地址自动偏移
for(j = 0;j<128;j++){
Oled_Write_Data(0);
}
}
}
3.3 使用STC89C52单片机通过模拟I2C让OLED显示一个字符A
部分使用规则需查看模块使用手,如OLED初始化,寻址等
STC89C52 | 0.96寸OLED |
---|---|
P2.2 | SCL |
P2.3 | SDA |
5V | VCC |
GND | GND |
源码:
#include "reg52.h"
#include "intrins.h"
sbit scl = P2^2;
sbit sda = P2^3;
void IIC_Start(){
scl = 0; //防止雪花
sda = 1;
scl = 1;
_nop_(); //约等于5us
sda = 0;
_nop_();
}
void IIC_Stop(){
scl = 0; //防止雪花
sda = 0;
scl = 1;
_nop_();
sda = 1;
_nop_();
}
char IIC_ACK(){
char flag;
sda = 1; //就在时钟脉冲9期间释放数据线
_nop_();
scl = 1;
_nop_();
flag = sda;
_nop_();
scl = 0;
_nop_();
return flag;
}
void IIC_Send_Byte(char dataSend){
int i;
for(i = 0;i<8;i++){
scl = 0; //scl拉低,让sda做好数据准备,scl低电平时才方便发生数据的翻转
sda = dataSend & 0x80; //1000 0000获得dataSend的最高位,给sda
_nop_(); //发送数据建立时间
scl = 1; //scl拉高开始发送
_nop_(); //数据发送时间
scl = 0; //发送完毕拉低
_nop_();
dataSend = dataSend << 1; //左移
}
}
void Oled_Write_Cmd(char dataCmd){
//1.start
IIC_Start();
//2.写入从机地址 b0111 1000 0x78
IIC_Send_Byte(0x78);
//3.ACK
IIC_ACK();
//4.cotrol byte:(0)(0)000000 写入命令 (0)(1)000000 写入数据
IIC_Send_Byte(0x00);
//5.ACK
IIC_ACK();
//6.写入指令/数据
IIC_Send_Byte(dataCmd);
//7.ACK
IIC_ACK();
//8.Stop
IIC_Stop();
}
void Oled_Write_Data(char dataData){
//1.start
IIC_Start();
//2.写入从机地址 b0111 1000 0x78
IIC_Send_Byte(0x78);
//3.ACK
IIC_ACK();
//4.cotrol byte:(0)(0)000000 写入命令 (0)(1)000000 写入数据
IIC_Send_Byte(0x40);
//5.ACK
IIC_ACK();
//6.写入指令/数据
IIC_Send_Byte(dataData);
//7.ACK
IIC_ACK();
//8.Stop
IIC_Stop();
}
void Oled_Init(void){
Oled_Write_Cmd(0xAE);//--display off
Oled_Write_Cmd(0x00);//---set low column address
Oled_Write_Cmd(0x10);//---set high column address
Oled_Write_Cmd(0x40);//--set start line address
Oled_Write_Cmd(0xB0);//--set page address
Oled_Write_Cmd(0x81); // contract control
Oled_Write_Cmd(0xFF);//--128
Oled_Write_Cmd(0xA1);//set segment remap
Oled_Write_Cmd(0xA6);//--normal / reverse
Oled_Write_Cmd(0xA8);//--set multiplex ratio(1 to 64)
Oled_Write_Cmd(0x3F);//--1/32 duty
Oled_Write_Cmd(0xC8);//Com scan direction
Oled_Write_Cmd(0xD3);//-set display offset
Oled_Write_Cmd(0x00);//
Oled_Write_Cmd(0xD5);//set osc division
Oled_Write_Cmd(0x80);//
Oled_Write_Cmd(0xD8);//set area color mode off
Oled_Write_Cmd(0x05);//
Oled_Write_Cmd(0xD9);//Set Pre-Charge Period
Oled_Write_Cmd(0xF1);//
Oled_Write_Cmd(0xDA);//set com pin configuartion
Oled_Write_Cmd(0x12);//
Oled_Write_Cmd(0xDB);//set Vcomh
Oled_Write_Cmd(0x30);//
Oled_Write_Cmd(0x8D);//set charge pump enable
Oled_Write_Cmd(0x14);//
Oled_Write_Cmd(0xAF);//--turn on oled panel
}
void Oled_Clear(){
int i,j;
for(i=0;i<8;i++){
Oled_Write_Cmd(0xB0 + i); //page0 - page7
//每个page从0列
Oled_Write_Cmd(0x00);
Oled_Write_Cmd(0x10);
//从0到127列,依次写入0,每写入数据,列地址自动偏移
for(j=0;j<128;j++){
Oled_Write_Data(0);
}
}
}
/*-- 文字: A --*/
/*-- 宋体12; 此字体下对应的点阵为:宽x高=8x16 --*/
//需要用到2个page块 1个page 8位高
char A1[8]={0x00,0x00,0xC0,0x38,0xE0,0x00,0x00,0x00};
char A2[8]={0x20,0x3C,0x23,0x02,0x02,0x27,0x38,0x20};
void main()
{
int i;
//1.OLED初始化
Oled_Init();
//2.选择一个位置
//2.1 确认页寻址模式
Oled_Write_Cmd(0x20);
Oled_Write_Cmd(0x02);
Oled_Clear();
//2.2 选择page 页码数 1011 0xxx
Oled_Write_Cmd(0xB0); //1011 0000
Oled_Write_Cmd(0x00);
Oled_Write_Cmd(0x10);
for(i=0;i<8;i++){
Oled_Write_Data(A1[i]);
}
Oled_Write_Cmd(0xB1); //1011 0000
Oled_Write_Cmd(0x00);
Oled_Write_Cmd(0x10);
for(i=0;i<8;i++){
Oled_Write_Data(A2[i]);
}
//不让程序退出
while(1);
}
效果展示: