引言
我们在前面学习完了USART,我们知道串口是一对一进行通信的。接下来我们学习的I2C是支持多对多和一对多的,但是我们学习过程中,只考虑一对多。
特点
两线制:一根SCL线用于传输时钟信号,一根SDA用于传输数据信号。
多主机/多从机:支持多对多和一对多,但是我们考虑的,和绝大多数情况都是一对多。
设备地址:一般为七位加一位读写位。
同步串行通信:所有数据都靠SCL时钟来控制。
半双工:同一时刻支持一个方向的传输。
速度模式:标准模式(100kbps),快速模式(400kbps)和快速模式+(1Mbps)
STM32F103ZET6和I2C
首先I2C是MCU上的片上外设,有两个I2C接口,其接线位置如下
和USART相同,在使用I2C的时候,需要复用GPIO接口
这里是对输出进行了复用,且使用开漏模式。但是在前面我们说一个GPIO的引脚,要么是输入,要么输出,为什么这里输入和输出用的是一个引脚呢?这里对输入并不是设置,只是当外部电压变化,内部可以感知到,并没有去配置输入模式之类的。
I2C基本通信原理

这个图中,我们除了SCL和SDA外,还看到了接VDD的两个上拉电阻,这个本质其实和I2C无关,主要是由于设置开漏模式导致的。
这里在输出的时候就是和前面讲GPIO开漏输出是一样的,只是采用复用的方式。主要是对于输入大家可能有些疑问。这里输出后,其实在接收方,可以认为是导线直接连接,类似浮空输入,对输入进行整形,后复用。这里主要是可以感知到这个变化,如果需要就可以直接使用。
时钟与数据
那么时钟与数据是怎么配合协同工作的呢?
这里规定,只有在时钟处于低电平期间,才可以作数据的跳变。在高电平期间会对数据进行读取。为了达到这个要求,也就要求数据的稳定时间要高于时钟高电平稳定时间,低电平。
线与(Wried-AND)
我们刚才只在单个通信的角度看待I2C,现在我们在全局来看I2C

当有任何一个设备拉低SCL/SDA的时候,整条线都会接地。这个和逻辑与或者按位与是有点相似之处的。
线与的好处
时钟同步:比如主设发送数据过快,从设备来不及处理,就可以拉低SCL,来告诉主设备。这时主设备拉高电平也没有作用,知直到从设备释放SCL,才会继续发送。
总线仲裁:当有多台主设备竞争总线,如果自己发送1,由于其他设备发送0,会拉低总线,那么它就知道自己仲裁失败了。
为什么选择开漏
首先为什么推挽模式不可以:其一是因为当主设备输出1,然后从设备拉低电压,会造成短路,还有就是无法实现线与逻辑。而这两个问题正是开漏模式可以解决的。
I2C协议

①为空闲状态,空闲状态默认为高电平。②为开始状态,我们前面说只有在SCL为低电平SDA才可以跳变来改变数据,这里在SCL为高电平时SDA进行了跳变,表示开始。③为结束状态,同样在SCL为高电平的时候,SDA进行了从低向高的跳变,恢复空闲状态。
Master发送从设备地址
在I2C中,一般由主设备产生时钟信号,也由主设备选定从设备。在最开始,会发送从设备的地址,发送后,如果从设备接收,会返回一个ACK,而没有匹配上的从设备不做任何回应。而这个地址一般为七位地址加一位读写位构成。
Master写数据

开始位->发送地址和写->接收方应答->发送数据->接收方应答->...->停止
Master读数据

开始位->发送地址和读->接收方应答->接收方发送数据->主机应答->...->收方发送数据->主机不应答->停止
这里最后一个为什么是NACK,因为在写的时候,主机是知道自己要写多少,写完就结束,但是在读的时候,从机并不知道你要读多少,所以需要主机去进行控制。它在最后一个字节到达后不进行确认,从机就不会在发送数据了。
I2C功能结构图

将其简化,整合到全局
首先,比较器是用来进行地址匹配的。然后控制逻辑电路输出时钟控制信息,数据控制包含移位寄存器和数据寄存器,数据通过APB1来从DR寄存器读取数据。不过这里的发送和USART不同,是MSB其余基本保持不变。
这里和前面还是一样,控制I2C其实就是控制它下面的寄存器。

DR寄存器用于存储数据,在发送的过程中如果TxE为表示不能写入,1表示可以写入。在接收过程中,RxNE为0表示不能读取数据,为1表示可以读取数据。
实战与源码
我们这里使用AT24C02来作从设备,我们要实现的是由USART来控制上位机,当写入的时候,把配置信息存到AT24C02当中。
AT24C02在读取当作一个一维数组来读取即可,但是在写入的时候不能跨页写入,会造成回卷。
我们整体的架构分为硬件,HAL库,板级驱动,控制逻辑,调用逻辑。
我们创建两个组,一个APP用于控制逻辑,BSP_Drivers为板级驱动,HAL库和调用逻辑(main)生成中是带好的。整体逻辑为,HAL库控制寄存器来控制硬件,板级驱动调用HAL库实现各自功能,控制逻辑通过调用板级驱动相关函数实现具体项目功能,最后main函数调用控制逻辑。
源码解析
初始化
这里和USART一样,也是使用了句柄。
cpp
void HAL_I2C_MspInit(I2C_HandleTypeDef* hi2c)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
if(hi2c->Instance==I2C2)
{
/* USER CODE BEGIN I2C2_MspInit 0 */
/* USER CODE END I2C2_MspInit 0 */
__HAL_RCC_GPIOB_CLK_ENABLE();
/**I2C2 GPIO Configuration
PB10 ------> I2C2_SCL
PB11 ------> I2C2_SDA
*/
GPIO_InitStruct.Pin = GPIO_PIN_10|GPIO_PIN_11;
GPIO_InitStruct.Mode = GPIO_MODE_AF_OD;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
/* Peripheral clock enable */
__HAL_RCC_I2C2_CLK_ENABLE();
/* USER CODE BEGIN I2C2_MspInit 1 */
/* USER CODE END I2C2_MspInit 1 */
}
}
首先,开启GPIO时钟,初始化GPIO,随后打开I2C2的时钟。
随后先关闭I2C的使能,进行配置,这里不是时钟配置,这里是设备自己的配置。随后配置寄存器信息,最后打开设备使能。
HAL_I2C_IsDeviceReady
cpp
tmp1 = __HAL_I2C_GET_FLAG(hi2c, I2C_FLAG_ADDR);
tmp2 = __HAL_I2C_GET_FLAG(hi2c, I2C_FLAG_AF);
while ((hi2c->State != HAL_I2C_STATE_TIMEOUT) && (tmp1 == RESET) && (tmp2 == RESET))
{
if (((HAL_GetTick() - tickstart) > Timeout) || (Timeout == 0U))
{
hi2c->State = HAL_I2C_STATE_TIMEOUT;
}
tmp1 = __HAL_I2C_GET_FLAG(hi2c, I2C_FLAG_ADDR);
tmp2 = __HAL_I2C_GET_FLAG(hi2c, I2C_FLAG_AF);
}


这里主要来判断,有没有收到正确的应答。这两位互斥不可能全为1或全为0,ADDR为1表示成功,ADDR为0表示失败,AF则相反。随后循环尝试设定次数。
cpp
if (hi2c->State == HAL_I2C_STATE_READY)
{
/* Wait until BUSY flag is reset */
if (I2C_WaitOnFlagUntilTimeout(hi2c, I2C_FLAG_BUSY, SET, I2C_TIMEOUT_BUSY_FLAG, tickstart) != HAL_OK)
{
return HAL_BUSY;
}
这里是检查设备状态是否就绪。
HAL_I2C_Mem_Write
写操作其实和检查状态类似,只不过后面要在加要写入的数据。
cpp
while (hi2c->XferSize > 0U)
{
/* Wait until TXE flag is set */
if (I2C_WaitOnTXEFlagUntilTimeout(hi2c, Timeout, tickstart) != HAL_OK)
{
if (hi2c->ErrorCode == HAL_I2C_ERROR_AF)
{
/* Generate Stop */
SET_BIT(hi2c->Instance->CR1, I2C_CR1_STOP);
}
return HAL_ERROR;
}
/* Write data to DR */
hi2c->Instance->DR = *hi2c->pBuffPtr;
/* Increment Buffer pointer */
hi2c->pBuffPtr++;
/* Update counter */
hi2c->XferSize--;
hi2c->XferCount--;
if ((__HAL_I2C_GET_FLAG(hi2c, I2C_FLAG_BTF) == SET) && (hi2c->XferSize != 0U))
{
/* Write data to DR */
hi2c->Instance->DR = *hi2c->pBuffPtr;
/* Increment Buffer pointer */
hi2c->pBuffPtr++;
/* Update counter */
hi2c->XferSize--;
hi2c->XferCount--;
}
}
最下面连写入两次,相当于双缓冲区。主要靠TxE判断数据寄存器是否为空。
HAL_I2C_Mem_Read
读这里比较麻烦,做了很多的分类,包括为回复NACK专门设置标志位。本质都是为了实现最后一个回复是NACK。