【STM32F407学习笔记】模拟IIC协议
通信总线在嵌入式系统有着极其重要的地位,它是单片机与单片机通信,单片机与外围设备通信的一个协议,如果没有这些协议,我们就不能保证数据的有效性。我们常用的总线有USART、IIC、SPI、CAN等。
涉及外设:GPIO、MPU9250
1. IIC协议简介
IIC通信协议(Inter-Integrated Circuit)是由飞利浦公司开发的,由于它的引脚少,硬件实现简单,可扩展性强,不需要USART、CAN等通信协议的外部收发设备,被广泛地应用在系统内多个集成电路(IC)间的通信。下面我们分别对IIC协议的物理层及协议层进行讲解。
1.1 物理层
物理层特点:
1)支持多设备的总线。在一个IIC通信总线中,可连接多个IIC通信设备,支持多个通信主机及多个通信从机。
2)一个IIC总线只使用两条总线线路,一条双向串行数据线(SDA),一条串行时钟线(SCL)。数据线用于传输数据,时钟线用于数据收发同步。
3)每个连接到总线的设备都有一个独立的地址。主机可以利用这个地址进行不同设备之间的访问。
4)总线通过上拉电阻连接到电源。当IIC设备空闲时,会输出高阻态,而当所有设备都空闲,都输出高阻态时,有上拉电阻将总线变为高电平。
5)多个主机同时使用总线时,为了防止数据冲突,会利用仲裁方式决定哪个设备占用总线。
6)具有三种传输模式:标准模式传输速率为100kbit/s,快速模式为400kbit/s,高速模式可达3.4Mbit/s,但目前大部分I2C设备尚不支持高速模式。
7)连接到相同总线的IC数量收到总线最大电容400pF限制。
1.2 协议层
IIC的协议定义了通讯的起始信号、停止信号、数据有效性、仲裁、时钟同步和地址广播等环节。
1.2.1 I2C基本读写流程
I2C的通信流程可以分为三部分,如途中所示:
其中灰色代表:数据从主机到从机,白色表示:数据从从机到主机,S:开始信号,SLAVE ADDRESS:从机地址,R/W:传输方向选择位(1读0写),A/Ā:应答(ACK)/非应答(NACK)信号,P:停止信号。这三个序列分别代表:主机写数据到从机、主机读取从机数据、复合通信模式。
起始信号(S):表示由主机的I2C接口产生的传输,这时连接到I2C总线上的所有从机都会接收到这个信号。
从机地址(SLAVE ADDRESS):起始信号产生后,所有从机就开始等待主机紧接着广播下来的从机地址(SLAVE ADDRESS)。在IIC总线上每个设备的地址都是唯一的,当主机广播的地址与某个从机地址相同时,这个设备就被选中了,没有被选中的设备将忽略后续的数据信号。
传输方向选择位(R/W):在地址位之后是传输方向选择位,该位为0时,表示后面的数据传输方向是有主机写数据到从机、该位为1时,即主机读取从机数据。从机地址+传输方向位一般是8位。
应答信号(A):第一种:从机接收到匹配的地址,从机返回应答信号(ACK)表示找到对应地址的从机设备;第二种,当从机被寻址,主机写数据到从机时:要求从机每接收到一个字节都需要给主机一个应答(ACK),主机只有接收到应答信号后,才能继续发送数据。主机读从机数据时:主机每接收一个字节都需要给从机一个应答(ACK),从机只有接收到应答之后,才能继续发送数据。
非应答信号(NACK):主机读取从机数据时:主机接收完最后一个字节,必须产生一个非应答信号(NACK),用来通知从机不要再发送数据了,这样从机也就释放了SDA。主机写入从机数据时:主机发送到最后一个字节时,从机给主机返回非应答信号(NACK),用来通知主机不用发送数据了,这样释放从机SDA。
停止信号(P):实验数据传输完成时,主机会给出一个停止信号,用于结束此次通信。
最常用的是复合通信模式,如图中所示的流程为:主机发出起始信号S,并产生从机地址,传输方向选择为写,从机产生一个应答信号ACK,主机写入一个字节的数据,从机产生一个ACK应答信号;再给一个起始信号,传输方向选择为读,从机产生一个ACK应答信号,并输出数据给主机,从机产生NACK,最后主机产生停止信号P。
总结:I2C通信中,SDA和SCL都是由主机控制的,从机只能将SDA线拉低而已,对于SCL线,从机是没有任何能力去控制的。
1.2.2 I2C通信各信号分解
- 起始和停止信号
如图中所示,当IIC是空闲时SCL和SDA都是高电平。当SCL是高电平时,SDA由高电平向低电平切换,这个情况表示通信的起始;当SCL时高电平时,SDA由低电平向高电平切换,这个情况表示通信的结束。起始停止信号由主机产生。 - 地址与数据方向
I2C总线上的每个设备都有自己的独立地址,主机发起通信时,通过SDA信号线发送从机地址来查找从机。IIC协议规定设备地址可以是7位或10位,实际中7位应用较为广泛。紧跟从机地址的一个数据位用于表示数据的传输方向,它是数据方向位(R/W),第8位或第11位。数据方向位为1,表示主机读取从机数据;数据方向位为0,表示主机向从机写入数据。 - 数据有效性
IIC使用SDA信号线来传输数据,使用SCL信号线进行数据同步。SDA在SCL的每个时钟周期传输一位数据。传输时:SCL为高电平SDA表示的数据有效,即此时的SDA高电平表示数据1,低电平表示数据0。当SCL为低电平时,SDA的数据无效,一般在这时候(SCL低电平时)进行SDA电平切换,为下一位数据做好准备。 - 应答与非应答
I2C的数据和地址传输都需要带响应。响应包括“应答ACK”和“非应答信号NACK”两种。作为数据接收端,当设备(无论主从机)接收到I2C传输的一个字节数据或地址后,若希望对方继续发送数据,则需要向对方发送“应答信号(ACK)”,发送方会继续发送下一个数据;若接收方希望结束数据传输,则向对方发送“非应答信号(NACK)”,发送方接收到该信号后会产生一个停止信号,结束信号传输。
2. GPIO模拟IIC时序
STM32F4自带IIC硬件接口,通信速度能达到400kHz。但是其缺点是通用性不好,不同芯片配置不同,引脚固定。所以我们选择使用IO口模拟IIC,它具有通用性强,移植方便,可用任意IO口模拟等特点。
2.1 GPIO模式初始化
IIC一共两条总线SDA与SCL,所以只需要两个IO口,其中SDA为数据线,主机从机都可以控制,既需要输出也需要输入,所以模拟时序时需要有IO输入与输出模式切换。SCL为时钟线,仅由主机控制,所以只需配置成输出模式。
因为在IIC总线上,有上拉电阻对电平进行拉高,在空闲时总线上是高电平,因此只需要配置输出为开漏模式(只能输出低电平)
#define RCC_IIC_SCL RCC_AHB1Periph_GPIOB // 端口时钟
#define IIC_SCL_PORT GPIOB // 端口
#define IIC_SCL GPIO_Pin_6 // 引脚
#define RCC_IIC_SDA RCC_AHB1Periph_GPIOB // 端口时钟
#define IIC_SDA_PORT GPIOB // 端口
#define IIC_SDA GPIO_Pin_7 // 引脚
// IO操作函数
#define IIC_SCL_H IIC_SCL_PORT->BSRR |= IIC_SCL // SCL置1
#define IIC_SCL_L IIC_SCL_PORT->BSRR |= (uint32_t)IIC_SCL << 16 // SCL置0
#define IIC_SDA_H IIC_SDA_PORT->BSRR |= IIC_SDA // SDA置1
#define IIC_SDA_L IIC_SDA_PORT->BSRR |= (uint32_t)IIC_SDA << 16 // SDA置0
#define READ_SDA (IIC_SDA_PORT->IDR & IIC_SDA) ? 1 : 0 // 读取SDA输入(IIC_SDA_PORT->IDR & IIC_SDA表示IIC_SDA口的IIC_SDA引脚)
void IIC_init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
// GPIO时钟
RCC_AHB1PeriphClockCmd(RCC_IIC_SCL, ENABLE);
RCC_AHB1PeriphClockCmd(RCC_IIC_SDA, ENABLE);
// GPIO初始化
GPIO_InitStructure.GPIO_Pin = IIC_SCL;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT; // 输出模式
GPIO_InitStructure.GPIO_OType = GPIO_OType_OD; // 开漏输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz; // 速度
GPIO_Init(IIC_SCL_PORT, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = IIC_SDA;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT; // 输出模式
GPIO_InitStructure.GPIO_OType = GPIO_OType_OD; // 开漏输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz; // 速度
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP; // 仅对输入时有效
GPIO_Init(IIC_SDA_PORT, &GPIO_InitStructure);
IIC_SCL_H;
IIC_SDA_H;
}
2.2 模拟IIC时序
-
起始信号
void IIC_Start(void) { // 输出 SDA_OUT(); IIC_SDA_H; IIC_SCL_H; delay_us(4); IIC_SDA_L;// 拉低SDA delay_us(4); IIC_SCL_L;// 钳住IIC总线,准备发送或接收数据 }
-
停止信号
void IIC_Stop(void) { SDA_OUT(); IIC_SCL_L; IIC_SDA_L; delay_us(4); IIC_SCL_H; IIC_SDA_H; delay_us(4); }
-
产生ACK应答
void IIC_ACK(void) { IIC_SCL_L; SDA_OUT(); IIC_SDA_L; delay_us(1); IIC_SCL_H; delay_us(2); IIC_SCL_L; }
-
产生NACK应答
void IIC_NACK(void) { IIC_SCL_L; SDA_OUT(); IIC_SDA_H; delay_us(1); IIC_SCL_H; delay_us(1); IIC_SCL_L; }
-
等待应答信号
uint8_t IIC_waitACK(void) { uint8_t ucErrTime=0; SDA_IN(); IIC_SDA_H; delay_us(1); IIC_SCL_H; delay_us(1); while(READ_SDA) { ucErrTime++; if(ucErrTime>250) { IIC_Stop(); return 1; } } IIC_SCL_L; return 0; }
3. 读取MPU6050 WHO AM I 寄存器
MPU6050中有一个WHO AM I寄存器,能够帮我们快速验证模拟IIC的实现是否正确,寄存器的位图如下:
这个寄存器用于验证设备的标识。WHO AM I的内容是一个8位的设备ID,寄存器的地址是0x75,寄存器的默认值是0x68。MPU6050在AD0引脚接GND(或为空)时,读取用的I2C地址为:0x68。下面实现通过IIC读取从机一个字节数据的函数:
/// @brief 读取指定设备,指定寄存器的一个值
/// @param I2C_Addr 目标设备地址
/// @param reg 寄存器地址
/// @param buf 读取数据存放地址
/// @return 1失败 0成功
uint8_t IIC_ReadByteFromSlave(uint8_t I2C_Addr, uint8_t reg, uint8_t *buf)
{
IIC_Start();
IIC_SendByte((I2C_Addr << 1) | 0); // 写入模式 发送从机地址:七位地址,左移一位并与上0,表示写
if (IIC_waitACK()) // 等待从机应答
{
// 非应答
IIC_Stop();
return 1;
}
IIC_SendByte(reg); // 发送寄存器地址
IIC_waitACK();
IIC_Start();
IIC_SendByte((I2C_Addr << 1) | 1); // 接收模式
IIC_waitACK();
*buf = IIC_ReadByte(0); // 仅读取一个字节
IIC_Stop();
return 0;
}
然后调用函数即可读取寄存器的一个字节内容:
uint8_t ID;
IIC_ReadByteFromSlave(0x68, 0x75, &ID);