本文已参与「新人创作礼」活动,一起开启掘金创作之路。
记录I2C(I2代表I的平方)学习相关笔记。
一、I2C 协议简介
I2C 通讯协议(Inter-Integrated Circuit)是由 Phiilps 公司开发的,由于它引脚少,硬件实现简单,可扩展性强,不需要 USART、CAN 等通讯协议的外部收发设备,现在被广泛地使用在系统内多个集成电路(IC)间的通讯。 I2C和USART类似分为物理层和协议层
二、I2C协议物理层
1)物理层就是一个支持设备的总线,“总线”指多个设备共用的信号线。在一个 I2C 通讯总线中,可连接多个 I2C 通讯设备,支持多个通讯主机及多个通讯从机。当然也有数量限制,但是一般不会超出总线,因为一般不会在一个总线上挂载百多个外设。 2)从图上可以看到,I2C有两条总线一条双向串行数据线(SDA) ,一条串行时钟线(SCL)。数据线即用来表示数据,时钟线用于数据收发同步。 3)主机想要访问外设,是通过外设地址来访问,每个外设都有独立的地址。 4)总线通过上拉电阻接到电源,当I2C设备空闲时候,会输出高阻态(相当于断路),需要的外设挂载到总线上,这样就不会产生访问冲突。当所有外设都空闲的时候,都输出高阻态时,由上拉电阻把总线拉成高电平。 5)多个主机同时使用总线时,为了防止数据冲突,会利用仲裁方式决定由哪个设备占用总线。(类似优先级) 6)具有三种传输模式:标准模式传输速率为 100kbit/s ,快速模式为 400kbit/s ,高速模式下可达 3.4Mbit/s,但目前大多 I 2 C 设备尚不支持高速模式。 7)连接到相同总线的 IC 数量受到总线的最大电容 400pF 限制 。
三、I2C协议层
I2C 的协议定义了通讯的起始和停止信号、数据有效性、响应、仲裁、时钟同步和地址广播等环节。(与USART类似) 下面框图讲解: 阴影部分代表主机主动的,空白代表从机(外设)主动的。
3.1 I2C协议的写过程
写过程就是主机向外设写入东西的过程,这里的0就是标志位。一般我们数据都是以字节来算,是8位,但是I2C总线上外设地址是7位,最后1位 用0来代表写、用1来代表读。
这里解释第4步: 第二步主机在总线上发起广播,我要找到对应的地址,总线的外设就自己检查自己的地址,是否和主机的一样,如果一样,那就回复主机,你要找的是我。
3.2 I2C协议的读过程
3.2 I2C协议的复合读写过程
四、通讯的起始和停止信号
前文中提到的起始(S)和停止(P)信号是两种特殊的状态,见图 24-5。当 SCL 线是高电平时 SDA 线从高电平向低电平切换,这个情况表示通讯的起始。当 SCL 是高电平时 SDA线由低电平向高电平切换,表示通讯的停止。起始和停止信号一般由主机产生。
五、数据有效性
I2C使用 SDA信号线来传输数据,使用 SCL信号线进行数据同步。见图 24-6。SDA数据线在 SCL的每个时钟周期传输一位数据。传输时,SCL为高电平的时候 SDA表示的数据有效,即此时的SDA为高电平时表示数据“1”,为低电平时表示数据“0”。当SCL为低电平时,SDA的数据无效,一般在这个时候 SDA进行电平切换,为下一次表示数据做好准备。
也就是说在SCL为低电平的时候,SDA的数据可能进行交换。
每次数据传输都以字节为单位,每次传输的字节数不受限制。
六、地址及数据方向
I2C 协议规定设备地址可以是 7 位或 10 位,实际中 7 位的地址应用比较广泛。紧跟设备地址的一个数据位用来表示数据传输方向,它是数据方向位(R/W——),第 8位或第 11位。数据方向位为“1”时表示主机由从机读数据,该位为“0”时表示主机向从机写数据。这就是刚才说的读或则写的标志位。
七、响应
数据传输一定要有对应的响应,响应包括“应答(ACK)”和“非应答(NACK)”两种信号。(我还行,我不行了)作为数据接收端时,当设备(无论主从机)接收到 I2C传输的一个字节数据或地址后,若希望对方继续发送数据,则需要向对方发送“应答(ACK)”信号,发送方会继续发送下一个数据;若接收端希望结束数据传输,则向对方发送“非应答(NACK)”信号,发送方接收到该信号后会产生一个停止信号,结束信号传输。
传输时主机产生时钟,在第 9 个时钟时,数据发送端会释放 SDA 的控制权,由数据接收端控制 SDA,若 SDA 为高电平,表示非应答信号(NACK),低电平表示应答信号(ACK)。
八、STM32的 I2C架构图详解
STM32 的 I2C 外设可用作通讯的主机及从机,支持 100Kbit/s 和 400Kbit/s 的速率,支持 7 位、10 位设备地址,支持 DMA 数据传输,并具有数据校验功能。
8.1 通讯引脚
I 2 C 的所有硬件架构都是根据图中左侧 SCL 线和 SDA 线展开的(其中的 SMBA 线用于SMBUS的警告信号,I2C通讯没有使用)。STM32芯片有多个 I2C外设,它们的 I2C通讯信号引出到不同的 GPIO 引脚上,使用时必须配置到这些指定的引脚。
引脚 | I2C1 | I2C2 |
---|---|---|
SCL | PB5 / PB8(重映射) | PB10 |
SDA | PB6 / PB9重映射) | PB11 |
8.2 时钟控制逻辑
SCL线的时钟信号,由 I 2 C 接口根据时钟控制寄存器(CCR)控制,控制的参数主要为时钟频率。配置 I2C的 CCR 寄存器可修改通讯速率相关的参数
1)可选择 I2C 通讯的“标准/快速”模式,这两个模式分别 I2C 对应 100/400Kbit/s 的通讯速率。
2)在快速模式下可选择 SCL 时钟的占空比,可选 Tlow/Thigh=2 或 Tlow/Thigh=16/9模式,我们知道 I2C 协议在 SCL 高电平时对 SDA 信号采样,SCL 低电平时 SDA准备下一个数据,修改 SCL 的高低电平比会影响数据采样,但其实这两个模式的比例差别并不大,若不是要求非常严格,这里随便选就可以了。
3)CCR 寄存器中还有一个 12 位的配置因子 CCR,它与 I2C 外设的输入时钟源共同作用,产生 SCL 时钟,STM32 的 I2C 外设都挂载在 APB1 总线上,使用 APB1 的时钟源 PCLK1,SCL信号线的输出时钟公式如下:
8.3 数据控制逻辑
I2C 的 SDA 信号主要连接到数据移位寄存器上,数据移位寄存器的数据来源及目标是数据寄存器(DR)、地址寄存器(OAR)、PEC 寄存器以及 SDA 数据线。当向外发送数据的时候,数据移位寄存器以“数据寄存器”为数据源,把数据一位一位地通过 SDA 信号线发送出去;当从外部接收数据的时候,数据移位寄存器把 SDA 信号线采样到的数据一位一位地存储到“数据寄存器”中。
若使能了数据校验,接收到的数据会经过 PCE 计算器运算,运算结果存储在“PEC 寄存器”中。当 STM32 的 I2C 工作在从机模式的时候,接收到设备地址信号时,数据移位寄存器会把接收到的地址与 STM32 的自身的“I2C 地址寄存器”的值作比较,以便响应主机的寻址。STM32 的自身 I2C 地址可通过修改“自身地址寄存器”修改,支持同时使用两个 I2C设备地址,两个地址分别存储在 OAR1和 OAR2中。
数据寄存器就像一个仓库,发送的数据从这里开始,接收的数据在这里存放。
九、通讯过程
9.1 主发送器
蓝色的框子就像流程,红色的框子就像流程对应的时间,需要这个事件来检测上一步是否完成。
流程说明: 1)控制产生起始信号(S),当发生起始信号后,它产生事件“EV5”,并会对 SR1 寄存器的“SB”位置 1,表示起始信号已经发送; 2)紧接着发送设备地址并等待应答信号,若有从机应答,则产生事件“EV6”及“EV8”,这时 SR1 寄存器的“ADDR”位及“TXE”位被置 1,ADDR 为 1表示地址已经发送,TXE 为 1表示数据寄存器为空; 3)以上步骤正常执行并对 ADDR 位清零后,我们往 I2C 的“数据寄存器 DR”写入要发送的数据,这时TXE位会被重置0,表示数据寄存器非空,I2C外设通过SDA信号线一位位把数据发送出去后,又会产生“EV8”事件,即 TXE 位被置 1,重复这个过程,就可以发送多个字节数据了; 4)当我们发送数据完成后,控制 I2C 设备产生一个停止信号(P),这个时候会产生EV8_2 事件,SR1 的 TXE位及 BTF位都被置 1,表示通讯结束。
9.2 主接收器
流程说明:
- 同主发送流程,起始信号(S)是由主机端产生的,控制发生起始信号后,它产生事件“EV5”,并会对 SR1寄存器的“SB”位置 1,表示起始信号已经发送;
- 紧接着发送设备地址并等待应答信号,若有从机应答,则产生事件“EV6”这时SR1 寄存器的“ADDR”位被置 1,表示地址已经发送。
- 从机端接收到地址后,开始向主机端发送数据。当主机接收到这些数据后,会产生“EV7”事件,SR1寄存器的 RXNE被置 1,表示接收数据寄存器非空,我们读取该寄存器后,可对数据寄存器清空,以便接收下一次数据。此时我们可以控制I2C 发送应答信号(ACK)或非应答信号(NACK),若应答,则重复以上步骤接收数据,若非应答,则停止传输; (4) 发送非应答信号后,产生停止信号(P),结束传输。
十、编程实战
10.1 I2C结构体详解
typedef struct {
uint32_t I2C_ClockSpeed; /*!< 设置 SCL 时钟频率,此值要低于 400000*/
uint16_t I2C_Mode; /*!< 指定工作模式,可选 I2C 模式及 SMBUS 模式 */
uint16_t I2C_DutyCycle; /*指定时钟占空比,可选 low/high = 2:1 及 16:9 模式*/
uint16_t I2C_OwnAddress1; /*!< 指定自身的 I2C 设备地址 */
uint16_t I2C_Ack; /*!< 使能或关闭响应(一般都要使能) */
uint16_t I2C_AcknowledgedAddress; /*!< 指定地址的长度,可为 7 位及 10 位 */
} I2C_InitTypeDef;
10.2 编程要点
- 配置通讯使用的目标引脚为开漏模式;
- 使能 I2C外设的时钟;
- 配置 I2C外设的模式、地址、速率等参数并使能 I2C外设;
- 编写基本 I2C按字节收发的函数;
- 编写读写 EEPROM 存储内容的函数;
- 编写测试程序,对读写数据进行校验。
10.3 根据结构体编程
根据霸道的原理图可以看到这样一个电路图,
GPIOB6和GPIOB7代表输入,我们需要打开对应的时钟。I2C我们也要打开对应的时钟。
对应APB1和APB2的时钟。 然后就是初始化CPIO口,配置I2C模式。
//配置GPIO口
static void I2C_GPIO_Config(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
/* 使能与 I2C 有关的时钟 */
EEPROM_I2C_APBxClock_FUN ( EEPROM_I2C_CLK, ENABLE );
EEPROM_I2C_GPIO_APBxClock_FUN ( EEPROM_I2C_GPIO_CLK, ENABLE );
/* I2C_SCL、I2C_SDA*/
GPIO_InitStructure.GPIO_Pin = EEPROM_I2C_SCL_PIN;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD; // 开漏输出
GPIO_Init(EEPROM_I2C_SCL_PORT, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = EEPROM_I2C_SDA_PIN;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD; // 开漏输出
GPIO_Init(EEPROM_I2C_SDA_PORT, &GPIO_InitStructure);
}
//配置I2C
static void I2C_Mode_Configu(void)
{
I2C_InitTypeDef I2C_InitStructure;
/* I2C 配置 */
I2C_InitStructure.I2C_Mode = I2C_Mode_I2C;
/* 高电平数据稳定,低电平数据变化 SCL 时钟线的占空比 */
I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2;
I2C_InitStructure.I2C_OwnAddress1 =I2Cx_OWN_ADDRESS7;
I2C_InitStructure.I2C_Ack = I2C_Ack_Enable ;
/* I2C的寻址模式 */
I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;
/* 通信速率 */
I2C_InitStructure.I2C_ClockSpeed = I2C_Speed;
/* I2C 初始化 */
I2C_Init(EEPROM_I2Cx, &I2C_InitStructure);
/* 使能 I2C */
I2C_Cmd(EEPROM_I2Cx, ENABLE);
}
这里有一些宏定义
#define EEPROM_I2Cx I2C1
#define EEPROM_I2C_APBxClock_FUN RCC_APB1PeriphClockCmd
#define EEPROM_I2C_CLK RCC_APB1Periph_I2C1
#define EEPROM_I2C_GPIO_APBxClock_FUN RCC_APB2PeriphClockCmd
#define EEPROM_I2C_GPIO_CLK RCC_APB2Periph_GPIOB
#define EEPROM_I2C_SCL_PORT GPIOB
#define EEPROM_I2C_SCL_PIN GPIO_Pin_6
#define EEPROM_I2C_SDA_PORT GPIOB
#define EEPROM_I2C_SDA_PIN GPIO_Pin_7
接下来就是向外设传输数据,传输数据需要找到这个外设的地址。 我们先传输一个简单的字节。 所以:
//传输地址和传输的数据
void EEPROM_Byte_Write(uint8_t addr,uint8_t data)
{
//产生起始信号
I2C_GenerateSTART(EEPROM_I2Cx, ENABLE);
/* 设置超时等待时间 */
I2CTimeout = I2CT_FLAG_TIMEOUT;
/* 判断这个起始信号是否发送完成,并进行超时处理*/
/* 检测 EV5 事件并清除标志*/
while(!I2C_CheckEvent(EEPROM_I2Cx, I2C_EVENT_MASTER_MODE_SELECT))
{
if((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(0);
}
/* 设置超时等待时间 */
I2CTimeout = I2CT_FLAG_TIMEOUT;
I2CTimeout = I2CT_FLAG_TIMEOUT;
/* 发送 EEPROM 设备地址 */
I2C_Send7bitAddress(EEPROM_I2Cx, EEPROM_ADDRESS, I2C_Direction_Transmitter);
/* 检测 EV6 事件并清除标志*/
while(!I2C_CheckEvent(EEPROM_I2Cx, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED))
{
if((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(1);
}
/* 发送要写入的 EEPROM 内部地址(即 EEPROM 内部存储器的地址) */
I2C_SendData(EEPROM_I2Cx, WriteAddr);
I2CTimeout = I2CT_FLAG_TIMEOUT;
/* 检测 EV8 事件并清除标志 */
while(!I2C_CheckEvent(EEPROM_I2Cx, I2C_EVENT_MASTER_BYTE_TRANSMITTED))
{
if((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(2);
}
/*检测 EV8 事件并清除标志*/
I2C_SendData(EEPROM_I2Cx, *pBuffer);
I2CTimeout = I2CT_FLAG_TIMEOUT;
/* 检测 EV8 事件并清除标志*/
while(!I2C_CheckEvent(EEPROM_I2Cx, I2C_EVENT_MASTER_BYTE_TRANSMITTED))
{
if((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(3);
}
/* 发送停止信号 */
I2C_GenerateSTOP(EEPROM_I2Cx, ENABLE);
return 1;
}
}
剩下的代码就不写了,因为我自己没写,只是按照视频分析。详细可以去看stm32IIC第5-7节。