【 1. 概述 】
- IIC ( I2C, Inter-Integrated Circuit :集成电路之间 ),由PHILIPS公司开发用于连接微控制器及其外围设备。
- 特点
两线式 串行同步半双工 通信方式。 - 功能
它是由数据线 SDA 和时钟 SCL 构成的串行总线,可发送和接收数据。
在CPU与被控IC(集成电路)之间、IC与IC之间进行双向传送,高速IIC总线一般可达400kbps以上。 - 具有多主机的IIC总线结构
【 2. 传输特性 】
0. 两总线的特性
开漏输出(外加上拉电阻)
- SCL和SDA硬件结构上属于外加上拉电阻的开漏输出,即写“0”输出0,写“1”输出1(没有外加上拉电阻时写“1”输出高阻,可以通过软件配置上拉电阻)。
- 为什么是要开漏输出呢?前面我们说过IIC总线上可以有多个从设备,那么多个GPIO口可能会连接在同一根线上,存在某个GPIO输出高电平, 另一个GPIO输出低电平的情况。如果使用推挽输出,你会发现这个GPIO的VCC和另一个GPIO的GND接在了一起, 也就是短路了(凉凉了)。如果换成开漏输出呢? VCC和GND多了个电阻, 这样电路就是安全的。所以总线一般会使用开漏输出。如下图展示了总线上为什么不能用推挽输出。
- 为什么要加上拉电阻?这是因为IIC通信需要输出高电平的能力,不然只传输低电平和高阻态(高阻态是不确定的电平状态),谈什么传输信息!
- 由于有的单片机内部能通过软件配置内置上拉电阻,故如果是加上拉电阻的推挽输出GPIO也可以进行IIC通信。
拉高SDA的必要性
- 假设有这么一个情况:设备1想要把SDA线拉高,但是此时设备2已经把SDA线拉低了,那么由于两设备都连接着SDA线,线与:1&0=0,无论设备1的SDA线输出什么,设备2总会把SDA线拉低!因此,在设备1将SDA线拉高前,设备2应先把SDA线拉高。
- 因此对于IIC:只有在拉高释放SDA总线后,才可进行主机/从机对SDA线控制的切换。
1. 空闲状态
- I2C总线总线的SDA和SCL两条信号线同时处于高电平时,规定为总线的空闲状态,此时由两条信号线各自的上拉电阻把电平拉高,各个器件的输出级场效应管均处在截止状态。
2. 起始信号、停止信号
- 起始信号:
当SCL为高电平期间,SDA由高电平到低电平的跳变。 - 停止信号:
当SCL为高电平期间,SDA由低电平到高电平的的跳变。
3. 应答信号
- 应答信号:
发送器每发送一个字节,就在第9个时钟脉冲期间释放数据线,由接收器反馈一个应答信号。 - 有效应答:
应答信号为低电平时,规定为有效应答位(ACK简称应答位),表示接收器已成功地接收了该字节;
对于反馈有效应答位ACK的要求:接收器在第9个时钟脉冲之前的低电平期间将SDA线拉低,并且确保在该时钟SCL的高电平期间为稳定的低电平。 - 无效应答:
应答信号为高电平时,规定为非应答位(NACK),一般表示接收器接收该字节没有成功。
- 接收器是主控器的情况(读):
在主控器收到最后一个字节后,发送一个NACK信号,以通知发送器结束数据发送,并释放SDA线,以便主控接收器发送一个停止信号。
4. 数据的有效性
- IIC总线进行数据传送时,时钟信号为高电平期间,数据线上的数据必须保持稳定。
- 只有在时钟线上的信号为低电平期间,数据线上的电平状态才允许变化。
5. 数据传输
- 在IIC总线上传送的 每一位数据都对应一个时钟脉冲(或同步控制)。即:在SCL串行时钟的配合下,在SDA上逐位地串行传送每一位数据,数据位的传输是边沿触发。
6. 工作过程
主机向从机写入数据
简要过程:
主机发送起始位,这会通知总线上的所有设备传输开始了,接下来主机发送设备地址+读/写信号,与这一地址匹配的从设备将成为从机并与主机进行传输,而其它从设备将会退出此次通信。
主机匹配到从机后,主机发送它所要读取或写入的从机的内部寄存器地址;然后发送数据;数据发送完毕后,发送停止位。
详细过程:
- 主机发送起始位。
- 主机发送从机的地址+读选择位。主机释放SDA总线,等待从机的应答。
如果从机接收主机成功,则进行应答;若没有握手成功或者发送的数据错误时从机不产生应答,此时要求重发或者终止。 - 主机发送想要写入从机内部寄存器的地址。主机释放SDA总线,等待从机的应答(应答过程如上一步)。
- 主机发送数据。
- 主机发送停止位。
PS:从机是EEPROM时,EEPROM 收到停止信号后,进入到一个内部的写入周期,大概需要10ms,此间任何操作都不会被EEPROM响应;(因此以这种方式的两次写入之间要插入一个延时,否则会导致失败)。
主机向从机读取数据
简要过程:
主机先向从机写入其想要读取从机内部寄存器的地址,再从从机中读取数据。
详细过程:
- 主机发送起始位。
- 主机发送从机地址+写选择位。主机释放SDA总线,等待从机应答。
- 主机发送内部寄存器地址。主机释放SDA总线,等待从机应答。
- 主机重新发送起始位,即 restart。
- 主机重新发送从机地址+读选择位。主机释放SDA总线,等待从机应答。
- 主机读取数据。
- 主机接收器在接收到最后一个字节后,主机释放SDA总线并发送NACK信号,以允许主机发出停止位结束传输。
- 主机发送停止位。
【 3. IIC底层驱动程序 】
基于STM32F407ZET6
1. 初始化 IO
//初始化IIC
void IIC_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB, ENABLE);//使能GPIOB时钟
//GPIOB1,B2初始化设置
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2 | GPIO_Pin_1;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT;//普通输出模式
GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;//开漏输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;//100MHz
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;//IIC总线要加上拉,使其能输出高电平。
GPIO_Init(GPIOB, &GPIO_InitStructure);//初始化
IIC_SCL=1;//拉高SCL和SDA,此时为IIC的初始状态
IIC_SDA=1;
}
2. 起始信号
SCL高电平期间,SDA由高电平向低电平的转换。
//产生IIC起始信号
void IIC_Start(void)
{
SDA_OUT(); //SDA线设置 输出模式
IIC_SDA=1; //拉高 SDA
IIC_SCL=1; //拉高 SCL
delay_us(4); //等待上升沿结束
IIC_SDA=0; // 拉低 SDA
delay_us(4); //等待SDA线下降沿结束
IIC_SCL=0; //释放SCL,等待下次指令的时钟信号来临。
}
3. 终止信号
SCL高电平期间,SDA线由低电平到高电平的转换。
//产生IIC停止信号
void IIC_Stop(void)
{
SDA_OUT();//SDA线设置 输出 模式
IIC_SCL=0;//SCL低电平时才允许SDA变化。
IIC_SDA=0;//拉低 SDA
delay_us(4); //等待下降沿结束
IIC_SCL=1; // 拉高 SCL(来了一个时钟信号,表示新的指令开始)
IIC_SDA=1; //拉高 SDA
delay_us(4); //等待上升沿结束
}
4. 发送有效应答
接收器在第9个时钟脉冲之前的低电平期间将SDA线拉低,并且确保在该时钟SCL的高电平期间为稳定的低电平。
//产生ACK应答
void IIC_Ack(void)
{
IIC_SCL=0; //SCL低电平时才允许SDA变化
SDA_OUT();//SDA设置输出模式
IIC_SDA=0;//拉低SDA
delay_us(2);//等待SDA下降沿结束
IIC_SCL=1; //拉高 SCL(来了一个时钟信号,表示新的指令开始)
delay_us(2); //等待SCL上升沿结束
IIC_SCL=0; //释放IIC,等待下个指令的时钟信号来临
}
5. 发送无效应答
接收器在第9个时钟脉冲之前的低电平期间将SDA线拉高,并且确保在该时钟SCL的高电平期间为稳定的低电平。
//不产生ACK应答
void IIC_NAck(void)
{
IIC_SCL=0;//SCL低电平时才允许SDA变化
SDA_OUT();//SDA设置输出模式
IIC_SDA=1;//拉高SDA
delay_us(2);//等待SDA上升沿结束
IIC_SCL=1; //拉高 SCL(来了一个时钟信号,表示新的指令开始)
delay_us(2); //等待SCL上升沿结束
IIC_SCL=0; //释放SCL,等待下个指令的时钟信号
}
6. 等待应答
//等待应答信号到来
//返回值:1,接收应答失败
// 0,接收应答成功
u8 IIC_Wait_Ack(void)
{
u8 ucErrTime=0;
SDA_IN(); //SDA设置为 输入 模式
IIC_SDA=1;delay_us(1); // 完全拉高SDA,读取从机的SDA信号。
IIC_SCL=1;delay_us(1); // 完全拉高SCL
while(READ_SDA) //当读取到的 SDA=1,即无效应答时
{
ucErrTime++;
if(ucErrTime>250)
{
IIC_Stop(); //超过阈值,则停止
return 1;
}
}
IIC_SCL=0;//释放SCL,等待下个指令的时钟信号
return 0;
}
7. 发送字节
//发送一个字节
void IIC_Send_Byte(u8 txd)
{
u8 t;
SDA_OUT(); //SDA设置输出模式
IIC_SCL=0;//拉低时钟开始数据传输
for(t=0;t<8;t++)
{
IIC_SDA=(txd&0x80)>>7; //取txd二进制位的最高位
txd<<=1; //txd二进制位左移,其余位向最高位平移补空位
delay_us(2); //对TEA5767这三个延时都是必须的
IIC_SCL=1; //拉高 SCL(来了一个时钟信号,表示新的指令开始)
delay_us(2); //等待SCL上升沿结束
IIC_SCL=0; // 释放IIC,等待下次指令的时钟信号
delay_us(2);// 等待SCL下降沿结束
}
}
8. 读字节
//读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; //拉低释放 SCL
delay_us(2); //等待 SCL 下降沿结束
IIC_SCL=1; //拉高 SCL(来了一个时钟信号,表示新的指令开始)
receive<<=1; //receive 左移1位(IIC数据传输是从高位向低位传输)
if(READ_SDA) receive++; //若读取的值为1,则receive+1
delay_us(1); //等待传输
}
if (!ack)
IIC_NAck();//发送nACK
else
IIC_Ack(); //发送ACK
return receive; //返回读取的值 receive
}