七、IIC协议
(一)原理
1.IIC总线
IIC(Inter-Integrated Circuit)是 IIC Bus 简称,中文叫集成电路总线。它是一种串行通信总线,使用多主从(多个主机可以连接多个从机)架构。
IIC使用两根双向信号线进行通信:
一根时钟线SCL,用于通信双方时钟的同步;
一根数据线SDA,用于收发数据。
IIC总线上所有器件的SDA、SCL引脚输出驱动都为 开漏(OD) 结构,并且IIC为同步的半双工通信方式(第五章的第一节有提到过半双工通信模式哦!)
主机(Master): 初始化总线的数据传输并产生允许传输的时钟信号的器件。
从机(slave):任何被寻址的器件。每个器件都有一个唯一的地址(无论是微控制器、LCD驱动器、存储器或键盘接口...),而且都可以作为一个发送器或接收器(由器件的功能决定)。
IIC总线上可以挂很多设备:可以是一个主机和多个从机,还可以是多个主机和多个从机。
(一个主机多个从机)
(多个主机多个从机)
(二)IIC协议内容
1.数据的有效性
SDA 线上的数据必须在时钟的高电平周期保持稳定。数据线的高或低电平状态只有在SCL 线的时钟信号是低电平时才能改变。
2.起始与终止信号
当SCL为高期间:
SDA : 由高到低------->起始信号
SDA:由低到高------->终止信号
起始信号和终止信号都是由主机发送的。上文提到过IIC为同步的半双工通信方式,所以起始信号产生之后,总线就处于被占用的状态;终止信号产生之后,总线就处于空闲状态。
3.发送一个字节(主机发送)
SCL低电平期间,从机将数据位依次放到SDA线上(高位先行),然后拉高SCL,从机将在SCL高电平期间读取数据位,所以SCL高电平期间SDA不允许有数据变化(前面的“数据的有效性”中有提过原因),依次循环上述过程8次即可发送一个字节
图中START:起始信号
MSB:数据位的最高位
LSB:数据位的最低位(图中未标识出)
4.从地址和R/W位
从机地址上文说过就是从机设备的地址号,主机通过从机的地址号能找到对应的从机设备传输数据。
(有些从机地址可以从从机的芯片手册获得)
IIC数据传输遵循下图所示的格式。每次通信开始时,主设备发送一个地址帧来指定与之通信的从设备。这个地址是7位长,后面跟着第八位,第八位是一个数据方向位(R/W)
“0”表示传输(WRITE)
“1”表示数据请求(READ))。
数据传输总是由Master生成的STOP条件(P)终止。然而,如果master仍然希望在总线上通信,它可以生成一个重复的START条件并在没有首先生成STOP条件的情况下寻址另一个从设备。在这样的传输中,各种读/写格式的组合是可能的。
5.接收一个字节(主机接收)
SCL低电平期间,从机将数据位依次放到SDA线上然后拉高SCL,主机将在SCL高电平期间读取数据位(高位在前),所以SCL高电平期间SDA不允许有数据变化,依次循环上述过程8次即可接收一个字节(主机在接收之前,需要释放SDA)
6.发送/接收应答信号
发送应答: 在接收完一个字节之后,主机在下一个时钟发送一位数据,数据0表示应答,数据1表示非应答
接收应答: 在发送完一个字节之后,主机在下一个时钟接收一位数据,判断从机是否应答,数据0表示应答,数据1表示非应答(主机在接收之前,需要释放SDA)。
应答表示 接收器已经成功地接受了该字节
非应答表示 接收器接收该字节没有成功。
(三) IIC数据帧
1.主机发送---从机接收
(1)主机对从机发送一个起始信号
(2)发送七位的从机地址
(3)发送一位的读/写位
(4)等待直到接收到从机设备的ACK信号
(5)主机发送8位数据,每发送8位数据之后就得等待从机设备的ACK应答信号
(6)发完所有数据并且接收到应答信号后,发送结束信号。
2.主机接收---从机发送
主机对向从机读取数据时,方式同发送数据有所不同,要多一次通信过程。
主机需要先向从机发送一次信号,告诉从机”我要读取数据“,然后重开一次通信,等待从机主动返回数据。
3.代码
(1)起始信号
void vI2C_Start_Signal(I2C_HandleTypedef * hIICx) { I2C_SDA_1 (hIICx); //SDA 1 I2C_SCL_1 (hIICx); //SCL 1 Delay_us (I2C_Delay); //延时 Delay_us (I2C_Delay); //延时 I2C_SDA_0 (hIICx); //SDA 0 Delay_us (I2C_Delay); //延时 I2C_SCL_0 (hIICx); //SCL 0 Delay_us (I2C_Delay); //延时 }
(2)结束信号
void vI2C_End_Signal(I2C_HandleTypedef * hIICx) { I2C_SCL_0 (hIICx); //SCL 0 I2C_SDA_0 (hIICx); //SDA 0 Delay_us (I2C_Delay); //延时 I2C_SCL_1 (hIICx); //SCL 1 Delay_us (I2C_Delay); //延时 I2C_SDA_1 (hIICx); //SDA 1 Delay_us (I2C_Delay); //延时 }
(3)发送一字节数据
int nI2C_SendByte(I2C_HandleTypedef * hIICx,uint8_t uSendByte) //数据从高位到低位// { uint8_t i=8; uint8_t byte = uSendByte; int nAck; while(i--) { //准备这次发送的数据 if(byte&0x80) I2C_SDA_1 (hIICx); else I2C_SDA_0 (hIICx); //给一个时钟脉冲 告诉对方来拿数据 Delay_us (I2C_Delay); //延时 I2C_SCL_1 (hIICx); Delay_us (I2C_Delay); //延时 Delay_us (I2C_Delay); //延时 I2C_SCL_0 (hIICx); Delay_us (I2C_Delay); //延时 //准备下一位即将发送的数据 byte<<=1 ; } I2C_SDA_1 (hIICx); //SDA 1 nAck = nI2C_WaitAck(hIICx); return nAck; }
(4)读取一字节数据
void vI2C_ReadByte(I2C_HandleTypedef * hIICx,uint16_t nBytes, uint8_t *dataP) //数据从高位到低位D7 D6 D5 D4 D3 D2 D1 D0(R/W位) { uint8_t i; uint16_t j; uint16_t uEndByteNum = 0x0000; uint8_t uReceiveByte = 0x00; uEndByteNum = nBytes-1; for(j=0;j<nBytes;j++) { uReceiveByte = 0; for(i=0;i<8;i++) { Delay_us (I2C_Delay); //延时 I2C_SCL_1 (hIICx); Delay_us (I2C_Delay); //延时 uReceiveByte = uReceiveByte<<1; if(I2C_SDA_R(hIICx) != 0) { uReceiveByte = uReceiveByte+1; } Delay_us (I2C_Delay); //延时 I2C_SCL_0 (hIICx); Delay_us (I2C_Delay); //延时 } if(j == uEndByteNum) { vI2C_NAck (hIICx); } else { vI2C_Ack (hIICx); } *(dataP+j) = uReceiveByte; } }
(5)应答信号
void vI2C_Ack(I2C_HandleTypedef * hIICx) { I2C_SDA_0 (hIICx); //SDA 0 Delay_us (I2C_Delay); //延时 I2C_SCL_1 (hIICx); //SCL 1 Delay_us (I2C_Delay); //延时 Delay_us (I2C_Delay); //延时 I2C_SCL_0 (hIICx); //SCL 0 Delay_us (I2C_Delay); //延时 I2C_SDA_1 (hIICx); //SDA 1 }
(6)非应答信号
void vI2C_NAck(I2C_HandleTypedef * hIICx) { I2C_SDA_1 (hIICx); //SDA 1 Delay_us (I2C_Delay); //延时 I2C_SCL_1 (hIICx); //SCL 1 Delay_us (I2C_Delay); //延时 Delay_us (I2C_Delay); //延时 I2C_SCL_0 (hIICx); //SCL 0 Delay_us (I2C_Delay); //延时 I2C_SDA_1 (hIICx); //SDA 1 }
(7)等待应答信号
int nI2C_WaitAck(I2C_HandleTypedef * hIICx) //返回为:=1有ACK,=0无ACK { int nAck; Delay_us (I2C_Delay); //延时 I2C_SCL_1 (hIICx); //SCL 1 Delay_us (I2C_Delay); //延时 if(I2C_SDA_R(hIICx) == 0) { nAck = 0; } else { nAck = 1; } Delay_us (I2C_Delay); //延时 I2C_SCL_0 (hIICx); //SCL 0 Delay_us (I2C_Delay); //延时 I2C_SDA_0 (hIICx); //SDA 0 return nAck; }
(8)检查设备地址
int nI2C_Check_Device_Address(I2C_HandleTypedef * hIICx) { int nAck; //=============================================================== // 发送器件地址(+写信号 0) //=============================================================== vI2C_Start_Signal(hIICx); //1. I2C_Start ; 起始信号 nAck = nI2C_SendByte(hIICx ,hIICx->uDevice_Addr)!=0; //2. I2C_Send Device Address(W); 发送(设备地址)告诉总线即将操作的设备 if(nAck != 0){vI2C_End_Signal(hIICx);return -1; } //3. I2C_WaitAck ; 等待响应 vI2C_End_Signal(hIICx); return I2C_OK; }
(四)TIM1650数码管
TM1650是一款国产4位共阴数码管驱动芯片,它还带有矩阵按键扫码功能。它的基本参数如下:
-
工作电压:3~5V
-
数码管驱动模式:8段x4位共阴数码管
-
矩阵按键驱动模式:7x4矩阵按键,不支持组合键
-
通信接口:类IIC,使用了IIC相同的时序,但没有完全遵守IIC的协议,不带从机地址。
1.引脚
SCL:串行通信时钟线
SDA:串行通信数据线
VCC:电源线
GND:接地线
2.通讯时序
TM1650采用2线串行传输协议通讯:
(1)开始信号
保持CLK为“1”电平,DAT从“1”跳“0”,认为是开始信号
结束信号
保持CLK为“1”电平,DAT从“0”跳“1”,认为是结束信号
(2)写“1”
保持DAT为“1”电平,CLK从“0”跳到“1”,再从“1”跳到“0”
写“0”:
保持DAT为“0”电平,CLK从“0”跳到“1”,再从“1”跳到“0”
(3)ACK信号
如果本次通讯正常,芯片在串行通讯的第8个时钟下降沿后,TM1650主动把DAT拉低。直到检测到CLK来 了上升沿,DAT释放为输入状态(对芯片而言)
(4) 一个字节(8位)数据传输格式
数据发送时高位先进。
当CLK是高电平时,DAT上的信号必须保持不变;只有CLK上的时钟信号为低电平时, DAT上的信号才能改变。(之前说的数据有效性)
数据输入的开始条件:
CLK为高电平时,DAT由高变低;
结束条件:
CLK为高时,DAT 由低电平变为高电平。
5、 写显示操作
ADDRESS:显示地址(68H、6AH、6CH、6EH);
DATA:显示数据。
6.完整时序
起始信号>>地址码(1字节)>>ACK应答信号>>数码管段数据(1字节)>>ACK应答信号>>结束信号
command1:系统命令48H;
command2:系统参数设置;(设置功能 | 显示控制 | 开关显示位)
ADDRESS:显示地址(68H、6AH、6CH、6EH);(ps:四个数码管的地址)
DATA:显示数据。
3.Demo代码
(1)7/8段显示方式
static uint8_t s_7number[11] = {0x3F,0x06,0x5B,0x4F,0x66,0x6D,0x7D,0x07,0x7F,0x6F,0x40}; // 7段显示方式0~9,- static uint8_t s_8number[11] = {0xBF,0x86,0xDB,0xCF,0xE6,0xED,0xFD,0x87,0xFF,0xEF,0x40}; // 8段显示方式0~9,-
(2)设备初始化
void vSen_TM1650_Initialize(I2C_HandleTypedef * iicHandle , uint8_t uLight_Degree ,uint8_t uSeg_Num) { //=============================================================== // TM1650初始化:Delay 100 MS //=============================================================== Delay_ms(100); //=============================================================== // TM1650初始化:Command1(0x48) + Command2 //=============================================================== vSen_TM1650_Write_Data( iicHandle , TM1650_COMMAND_1 , uLight_Degree | uSeg_Num | TM1650_DISPLAY_ON); }
(3)发送数据
void vSen_TM1650_Write_Data(I2C_HandleTypedef * iicHandle,uint8_t uAddr,uint8_t uData) { vI2C_Start_Signal(iicHandle); //IIC_Start ; 起始信号 nI2C_SendByte(iicHandle, uAddr); //IIC_Send DataBuffer ; 发送地址 nI2C_SendByte(iicHandle, uData); //IIC_Send DataBuffer ; 发送数据 vI2C_End_Signal(iicHandle); //IIC_End ; 结束信号 }
(4)数码管单个显示
void vSen_TM1650_Set_Single_Display(I2C_HandleTypedef * iicHandle, uint8_t uDigitron_Mem_Addr , uint8_t uValue , uint8_t uDp_Enable , uint8_t Zero_NC_Enable) { uint8_t Display_data = 0x00; if(Zero_NC_Enable && (uValue == 0)){Display_data = 0x00; }//不显示0 else {Display_data = s_7number[uValue]; }//显示0 vSen_TM1650_Write_Data( iicHandle , uDigitron_Mem_Addr , Display_data | uDp_Enable); }
(5)数码管全部显示
void vSen_TM1650_Set_Display(I2C_HandleTypedef * iicHandle, uint8_t *uVar , uint8_t uDp_Pos) { //=============================================================== // 小数点位置 //=============================================================== uint8_t uDp_set[4] = {TM1650_DP_DISENABLE , TM1650_DP_DISENABLE ,TM1650_DP_DISENABLE ,TM1650_DP_DISENABLE}; if(uDp_Pos != 0){uDp_set[uDp_Pos-1] = TM1650_DP_ENABLE;} //=============================================================== // //=============================================================== vSen_TM1650_Set_Single_Display( iicHandle , TM1650_DIGITRON_1_MEM_ADDR , uVar[0] , uDp_set[0] , 0); vSen_TM1650_Set_Single_Display( iicHandle , TM1650_DIGITRON_2_MEM_ADDR , uVar[1] , uDp_set[1] , 0); vSen_TM1650_Set_Single_Display( iicHandle , TM1650_DIGITRON_3_MEM_ADDR , uVar[2] , uDp_set[2] , 0); vSen_TM1650_Set_Single_Display( iicHandle , TM1650_DIGITRON_4_MEM_ADDR , uVar[3] , uDp_set[3] , 0); }
(6)浮点数显示
void vSen_TM1650_Set_Num_Display(I2C_HandleTypedef * iicHandle, float fValue ) { uint8_t uDp_Pos = 0;//是否有小数点 0无 1有 对应第一个数码管 类推······ uint8_t uNum_Buf[4] = {0,0,0,0};//4位数码管数值 uint16_t uTemp = 0; if(fValue >= 0) { if (fValue>1000) { uTemp = (uint16_t)(fValue); uDp_Pos = 0; }//千位数 显示零位小数 else if(fValue>100) { uTemp = (uint16_t)(fValue*10.f); uDp_Pos = 3; }//百位数 显示一位小数 else if(fValue>10 ) { uTemp = (uint16_t)(fValue*100.f); uDp_Pos = 2; }//十位数 显示二位小数 else if(fValue<10 && fValue>0){ uTemp = (uint16_t)(fValue*1000.f);uDp_Pos = 1; }//小数 显示三位小数 & 个位数 显示三位小数 uNum_Buf[0] = (uTemp/1000); uNum_Buf[1] = (uTemp%1000/100); uNum_Buf[2] = (uTemp%100/10); uNum_Buf[3] = (uTemp%10); } else { fValue = -fValue;//数值取反 uNum_Buf[0] =10; //第一个数码管显示负数 if (fValue>1000) { uTemp = (uint16_t)(fValue); uDp_Pos = 0; }//千位数 显示零位小数 else if(fValue>100) { uTemp = (uint16_t)(fValue); uDp_Pos = 0; }//百位数 显示一位小数 else if(fValue>10 ) { uTemp = (uint16_t)(fValue*10.f); uDp_Pos = 3; }//十位数 显示二位小数 else if(fValue<10 && fValue>0){ uTemp = (uint16_t)(fValue*100.f); uDp_Pos = 2; }//小数 显示三位小数 & 个位数 显示三位小数 uNum_Buf[1] = (uTemp%1000/100); uNum_Buf[2] = (uTemp%100/10); uNum_Buf[3] = (uTemp%10); } vSen_TM1650_Set_Display( iicHandle , uNum_Buf , uDp_Pos); }
4.实验现象
VCC------>5V
GND------>GND
SCL------->P34
SDA------->P33
//显示浮点数0.123
void main() { I2C_HandleTypedef hIIC_TM1650;//定义TM1650的iic操作句柄 GPIO_Init(); //STC15W单片机引脚初始化函数 vI2C_Handle_Init(&hIIC_TM1650 , GPIO_Port_3 , GPIO_Pin_4 , GPIO_Port_3 , GPIO_Pin_3, NULL); // P34- SCL P33- SDA vSen_TM1650_Initialize(&hIIC_TM1650 , TM1650_BACKGROUND_LIGHT_02_8 , TM1650_DISPLAY_MODE_8_SEG); while(1) { vSen_TM1650_Set_Num_Display(&hIIC_TM1650 , 0.123f); Delay_ms(1000); } }
5.逻辑分析仪观察波形
可以通过解码器分析从起始信号到结束信号的整个波形(起到检查作用)
屏幕右上角有解码器设置
设置这两条线
就可以看到波形了