一.I2C通信协议简介
1.物理层
I2C物理层有以下特点:
- I2C是支持多设备的总线,支持多个通讯主机、多个通讯从机
- I2C使用两条总线线路,SDA(双向串行数据线)、(SCL)串行时钟线
- 每个连接到总线的设备都有一个独立的地址,主机利用地址进行不同设备的访问
- 多个主机同时使用总线时,为防止数据冲突会通过仲裁的方式决定哪个设备占用总线
- 具有三种传输模式:标准模式(100k/s)、快速模式(400k/s)、高速模式(3.4M/s)
- 总线通过上拉电阻接到电源,当I2C设备空闲时输出高阻态,当所有设备都空闲、都输出高阻态时,由上拉电阻将总线拉到高电平
2.协议层
协议层定义了通讯的起始信号、停止信号、数据有效性、响应、仲裁、时钟同步、地址广播等环节。
(1) I2C基本读写过程
- 起始位产生后,所有从机就开始等待主机接下来广播的从机地址位(SLAVE_ADDRESS,可以是7位或10位)。当选中某个设备地址后其他未被选中的设备将会忽略之后的数据信号。
- 传输方向选择位:该位为0时,主机向从机写数据;该位为1时,主机从从机读数据。
- 应答位:从机会返回一个应答(ACK)或非应答(NACK)信号,只有接收到应答信号后,主机才能继续发送或接收数据。
- 数据位:每次数据传输都以字节(8 位)为单位,每次传输的字节数不受限制 ,每发送完一个数据,都会等待应答信号(ACK)。
- 复合格式:该传输过程有两次起始信号(S)。在第一次传输中,主机通过SLAVE_ADDRESS 找到从设备后,发送从设备内部的寄存器或存储器地址(注意区分它与 SLAVE_ADDRESS
);在第二次的传输中,对该地址进行读或写。第一次通讯是告诉从机读写地址,第二次则是读写的实际内容。
(2)起始位与停止位
起始位:当 SCL 是高电平时 ,SDA 从高电平向低电平切换
停止位:当 SCL 是高电平时 ,SDA 由低电平向高电平切换
起始和停止信号一般由主机产生。
(3)数据有效性
I2C 使用 SDA 信号线来传输数据,使用 SCL信号线进行数据同步。
SDA 数据线在 SCL的每个时钟周期传输一位数据。
当SCL为高电平的时 SDA表示的数据有效;当 SCL为低电平时SDA的数据无效,SDA进行电平切换,为下次数据传输做好准备。
(4)数据传输响应时序
传输时主机产生时钟,在第 9 个时钟时,数据发送端会释放 SDA 的控制权,由数据接收端控制 SDA,若 SDA 为高电平,表示非应答信号(NACK),低电平表示应答信号(ACK)。
3.I2C时序代码实现
//iic.c
/**
* @brief IIC初始化函数
*
* @param void
*
* @return void
*/
void IIC_Init(void)
{
GPIO_InitTypeDef GPIO_Initure;
__HAL_RCC_GPIOC_CLK_ENABLE(); //使能GPIOC时钟
__HAL_RCC_GPIOD_CLK_ENABLE(); //使能GPIOD时钟
/*
SCL - PD6 SDA-PC1
*/
GPIO_Initure.Pin = GPIO_PIN_1;
GPIO_Initure.Mode = GPIO_MODE_OUTPUT_PP; //推挽输出
GPIO_Initure.Pull = GPIO_PULLUP; //上拉
GPIO_Initure.Speed = GPIO_SPEED_FAST; //快速
HAL_GPIO_Init(GPIOC, &GPIO_Initure);
GPIO_Initure.Pin = GPIO_PIN_6;
GPIO_Initure.Mode = GPIO_MODE_OUTPUT_PP; //推挽输出
GPIO_Initure.Pull = GPIO_PULLUP; //上拉
GPIO_Initure.Speed = GPIO_SPEED_FAST; //快速
HAL_GPIO_Init(GPIOD, &GPIO_Initure);
//总线通过上拉电阻接到电源,当I2C设备空闲时会输出高阻态
//当所有I2C设备都空闲,上拉电阻把总线拉成高电平
IIC_SDA(1);
IIC_SCL(1);
}
/**
* @brief 产生IIC起始信号
*
* @param void
*
* @return void
*/
void IIC_Start(void)
{
SDA_OUT(); //sda线输出
IIC_SDA(1);
IIC_SCL(1);
delay_us(500);
IIC_SDA(0);//START:when CLK is high,DATA change from high to low
delay_us(500);
IIC_SCL(0);//钳住I2C总线,准备发送或接收数据
}
/**
* @brief 产生IIC停止信号
*
* @param void
*
* @return void
*/
void IIC_Stop(void)
{
SDA_OUT();//sda线输出
IIC_SDA(0);
IIC_SCL(1);
delay_us(500);
IIC_SDA(1);//STOP:when CLK is high DATA change from low to high
delay_us(500);
IIC_SCL(0);//发送I2C总线结束信号
}
/**
* @brief 等待应答信号到来
*若SDA为高电平,表示非应答信号(NACK),低电平表示应答信号(ACK)。
* @param void
*
* @return u8 1,接收应答失败
* 0,接收应答成功
*/
u8 IIC_Wait_Ack(void)
{
u16 ucErrTime = 0;
SDA_IN(); //SDA设置为输入
IIC_SDA(1);
delay_us(500);
IIC_SCL(1);
delay_us(500);
while(READ_SDA)
{
ucErrTime++;
if(ucErrTime > 1000)//等待1000次从机使SDA线拉低
{
IIC_Stop();
return 1;
}
}
//c语言中,任何函数中遇到return 都会跳出本函数,main函数也不例外,return后面的语句是无法执行的。
IIC_SCL(0);//时钟输出0
return 0;
}
/**
* @brief 产生ACK应答
*
* @param void
*
* @return void
*/
void IIC_Ack(void)
{
IIC_SCL(0);
SDA_OUT();
IIC_SDA(0);
delay_us(500);
IIC_SCL(1);
delay_us(500);
IIC_SCL(0);
}
/**
* @brief 不产生ACK应答
*
* @param void
*
* @return void
*/
void IIC_NAck(void)
{
IIC_SCL(0);
SDA_OUT();
IIC_SDA(1);
delay_us(500);
IIC_SCL(1);
delay_us(500);
IIC_SCL(0);
}
/**
* @brief IIC发送一个字节
*
* @param txd 需要发送的数据
*
* @return void
*/
void IIC_Send_Byte(u8 txd)
{
u8 t;
SDA_OUT();
IIC_SCL(0);//拉低时钟开始数据传输
for(t = 0; t < 8; t++)
{
//由高位到低位依次发送txd,对SDA线拉低、拉高
IIC_SDA((txd & 0x80) >> 7);
txd <<= 1;
IIC_SCL(1);
delay_us(500);
IIC_SCL(0);
delay_us(500);
}
}
/**
* @brief 读1个字节数据
*
* @param ack 1,发送ACK 0,发送nACK
*
* @return u8 返回读取数据
*/
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(500);
IIC_SCL(1);
receive <<= 1;//左移一位 空出来低位接收数据
if(READ_SDA)receive++;
delay_us(500);
}
if(!ack)
IIC_NAck();//发送nACK
else
IIC_Ack(); //发送ACK
return receive;
}
//iic.h
//IO方向设置
#define SDA_IN() {GPIOC->MODER&=~(3<<(1*2));GPIOC->MODER|=0<<(1*2);} //PC1输入模式 2、3位(对应MODER1[1:0])先清零,再赋值00 输入模式
#define SDA_OUT() {GPIOC->MODER&=~(3<<(1*2));GPIOC->MODER|=1<<(1*2);} //PC1输出模式 2、3位(对应MODER1[1:0])先清零,再赋值01 通用输出模式
//IO操作
#define IIC_SCL(n) (n?HAL_GPIO_WritePin(GPIOD,GPIO_PIN_6,GPIO_PIN_SET):HAL_GPIO_WritePin(GPIOD,GPIO_PIN_6,GPIO_PIN_RESET))//SCL
#define IIC_SDA(n) (n?HAL_GPIO_WritePin(GPIOC,GPIO_PIN_1,GPIO_PIN_SET):HAL_GPIO_WritePin(GPIOC,GPIO_PIN_1,GPIO_PIN_RESET))//SDA
#define READ_SDA HAL_GPIO_ReadPin(GPIOC,GPIO_PIN_1)//输入SDA
//IIC所有操作函数
void IIC_Init(void); //初始化IIC的IO口
void IIC_Start(void); //发送IIC开始信号
void IIC_Stop(void); //发送IIC停止信号
void IIC_Send_Byte(u8 txd); //IIC发送一个字节
u8 IIC_Read_Byte(unsigned char ack);//IIC读取一个字节
u8 IIC_Wait_Ack(void); //IIC等待ACK信号
void IIC_Ack(void); //IIC发送ACK信号
void IIC_NAck(void); //IIC不发送ACK信号
void IIC_Write_One_Byte(u8 daddr, u8 addr, u8 data);
u8 IIC_Read_One_Byte(u8 daddr, u8 addr);
#endif