IIC通信
# 硬件基础
之前有写到一点关于 I2C 的内容,请见这里
这里以 RM C 板中磁力计的使用为例。但事实上 I2C 能够配置与读取的传感器种类非常多,例如温度传感器,气压传感器,多路ADC模块等
I2C介绍
I2C是 PHILIPS 公司开发的一种半双工、双向二线制同步串行总线。两线制代表 I2C 只需两根信号线,一根数据线 SDA,另一根是时钟线 SCL
I2C 总线允许挂载多个主设备,但总线时钟同一时刻只能由一个主设备产生,并且要求每个连接到总线上的器件都有唯一的 I2C 地址,从设备可以被主设备寻址
I2C通信具有几类信号:
- 开始信号S:当 SCL 处于高电平时,SDA 从高电平拉低至低电平,代表数据传输的开始
- 结束信号P:当SCL处于高电平时,SDA 从低电平拉高至高电平,代表数据传输结束
- 数据信号:数据信号每次都传输 8 位数据,每一位数据都在一个时钟周期内传递,当 SCL 处于高电平时候,SDA 数据线上的电平需要稳定,当 SCL 处于低电平的时候,SDA 数据线上的电平才允许改变
- 应答信号ACK/NACK:应答信号是主机发送 8bit 数据,从机对主机发送低电平,表示已经接受数据。
# 传输特点
- 在一个 I2C 通讯总线中,可连接多个 I2C 通讯设备,支持多个通讯主机(Master)及多个通讯从机(Slave)
- 一个 I2C 总线只使用两条总线线路,一条双向串行数据线 (SDA),一条串行时钟线 (SCL)。数据线即用来表示数据,时钟线用于数据收发同步
- 每个连接到总线的设备都有一个独立的地址,主机可以利用这个地址进行不同设备之间的访问
- 总线通过上拉电阻接到电源。当 I2C 设备空闲时,会输出高阻态,而当所有设备都空闲,都输出高阻态时,由上拉电阻把总线拉成高电平
- 多个主机同时使用总线时,为了防止数据冲突,会利用仲裁方式决定由哪个设备占用总线
- 具有三种传输模式:标准模式传输速率为 100kbit/s,快速模式为 400kbit/s,高速模下可达 3.4Mbit/s,但目前大多 I2C 设备尚不支持高速模式
# 接线
I2C 设备的接线一般是三根线:SCL
, SDA
与 GND
,但是有的也会有 RESET 引脚需要的接线等等其他接线
# 协议层面
# 信号结构
以主机向从机写数据为例,信号的流程结构如下:
也就是:
- 起始信号
- 从机地址+读写标志位
- 应答
- 数据传输
- 应答
- 数据传输
- 应答
- 结束信号
# 关于从机
找到从机的地址之后,可能还需要与其寄存器通讯,所以还需要找到其目标寄存器的位置
一个比喻:
整个 I2C 通信过程理解成收发快递的过程,设备 I2C 地址理解成学校快递柜的地址,读写位代表寄出和签收快递,寄存器地址则是快递柜上的箱号,而数据便是需要寄出或者签收的快递。整个过程便是如同到学校的快递柜(从机 I2C 地址),对第几号柜箱(寄存器地址),进行寄出或者签收快递(数据)的过程
也就是我们与 I2C 设备通信实际上是与设备中的寄存器的通信,通过设备告诉我们的信息找到对应的寄存器地址;如果要读就从那里读取数据,如果要写就从在那个寄存器中写入数据
# 起始信号与终止信号
- 起始信号 (S):当 SCL 线是高电平时,SDA 线从高电平向低电平切换(Falling edage)
- 终止信号 (P):当 SCL 是高电平时,SDA 线由低电平向高电平切换(Rising edage)
# 地址与数据方向
- 地址帧(MSB)
- I2C 总线上的每个设备都有自己的独立地址,主机发起通讯时,通过 SDA 信号线发送设备地址 (SLAVE_ADDRESS) 来查找从机;寻址的方式并不是定向的,而是将这个信号发送至 I2C 总线中,从机将信号与自身的地址匹配
- I2C 协议规定设备地址可以是 7 位或10 位,实际中 7 位的地址应用比较广泛
- 数据方向(读或写/LSB)
- 数据方向的位在地址后一位(关于如何确定这个地址,请到 Bouns 的部分!)
1
为读模式,0
为写模式
# 数据的有效性
I2C 使用 SDA 信号线来传输数据,同时使用 SCL 信号线进行数据同步。SDA 数据线在 SCL 的每个时钟周期传输一位数据
传输时,SCL 为高电平的时候 SDA 表示的数据有效,即此时的 SDA 为高电平时表示数据 1
,为低电平时表示数据 0
。
当 SCL 为低电平时,SDA 的数据无效,一般在这个时候 SDA 进行电平切换,为下一次表示数据做好准备
# 应答信号
I2C 的数据和地址传输都带响应。响应包括“应答 (ACK)”和“非应答 (NACK)” 两种信号
作为数据接收端时,当设备 (无论主从机) 接收到 I2C 传输的一个字节数据或地址后,若希望对方继续发送数据,则需要向对方发送“应答 (ACK)”信号,发送方会继续发送下一个数据;若接收端希望结束数据传输,则向对方发送“非应答 (NACK)”信号,发送方接收到该信号后会产生一个停止信号,结束信号传输
其中,前面讲到的“地址位+方向位+一个字节数据”刚好到第九个 bit,也就是第九个时钟周期的时间。因此:传输时主机产生时钟,在第 9 个时钟时,数据发送端会释放 SDA 的控制权,由数据接收端控制 SDA,SDA 为高电平表示非应答信号 (NACK),低电平表示应答信号 (ACK)
# CubeMX配置
对于 I2C 的配置也像串口一样,有轮询、中断、DMA三种基本模式。我们可以通过配置、调整代码或者 CubeMX 中的选项进行调整
# 对中断的配置
在 NVIC 中勾选 event interrupt
# DMA 配置
像配置串口一样配置
# 其他设置
其他方面的配置请参考前面的文章
# 代码示例
代码来源:官方例程
# Polling 方式
# 主机模式->发送
/* Timeout is set to 10S */
while(HAL_I2C_Master_Transmit(&hi2c1, (uint16_t)I2C_ADDRESS, (uint8_t*)aTxBuffer, TXBUFFERSIZE, 10000) != HAL_OK)
{
/* Error_Handler() function is called when Timeout error occurs.
When Acknowledge failure occurs (Slave don't acknowledge its address)
Master restarts communication */
if (HAL_I2C_GetError(&hi2c1) != HAL_I2C_ERROR_AF)
{
Error_Handler();
}
}
// 库函数定义如下
/**
* @brief Transmits in master mode an amount of data in blocking mode.
* @param hi2c Pointer to a I2C_HandleTypeDef structure that contains
* the configuration information for the specified I2C.
* @param DevAddress Target device address: The device 7 bits address value
* in datasheet must be shifted to the left before calling the interface
* @param pData Pointer to data buffer
* @param Size Amount of data to be sent
* @param Timeout Timeout duration
* @retval HAL status
*/
HAL_StatusTypeDef HAL_I2C_Master_Transmit (I2C_HandleTypeDef * hi2c,
uint16_t DevAddress,
uint8_t * pData,
uint16_t Size,
uint32_t Timeout)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
此函数中使用了 HAL_I2C_Master_Transmit
库函数
# 主机模式->接收
/* Timeout is set to 10S */
while(HAL_I2C_Master_Receive(&hi2c1, (uint16_t)I2C_ADDRESS, (uint8_t *)aRxBuffer, RXBUFFERSIZE, 10000) != HAL_OK)
{
/* Error_Handler() function is called when Timeout error occurs.
When Acknowledge failure occurs (Slave don't acknowledge it's address)
Master restarts communication */
if (HAL_I2C_GetError(&hi2c1) != HAL_I2C_ERROR_AF)
{
Error_Handler();
}
}
// 库函数定义如下
/**
* @brief Receives in master mode an amount of data in blocking mode.
* @param hi2c Pointer to a I2C_HandleTypeDef structure that contains
* the configuration information for the specified I2C.
* @param DevAddress Target device address: The device 7 bits address value
* in datasheet must be shifted to the left before calling the interface
* @param pData Pointer to data buffer
* @param Size Amount of data to be sent
* @param Timeout Timeout duration
* @retval HAL status
*/
HAL_StatusTypeDef HAL_I2C_Master_Receive(I2C_HandleTypeDef *hi2c,
uint16_t DevAddress,
uint8_t *pData,
uint16_t Size,
uint32_t Timeout)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
此函数中使用了 HAL_I2C_Master_Receive
库函数
# 从机模式->接收
/* Timeout is set to 10S */
if(HAL_I2C_Slave_Receive(&hi2c1, (uint8_t *)aRxBuffer, RXBUFFERSIZE, 10000) != HAL_OK)
{
/* Transfer error in reception process */
Error_Handler();
}
// 库函数定义如下
/**
* @brief Receive in slave mode an amount of data in blocking mode
* @param hi2c Pointer to a I2C_HandleTypeDef structure that contains
* the configuration information for the specified I2C.
* @param pData Pointer to data buffer
* @param Size Amount of data to be sent
* @param Timeout Timeout duration
* @retval HAL status
*/
HAL_StatusTypeDef HAL_I2C_Slave_Receive(I2C_HandleTypeDef *hi2c,
uint8_t *pData,
uint16_t Size,
uint32_t Timeout)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
此函数中使用了 HAL_I2C_Slave_Receive
库函数
# 从机模式->发送
/* Timeout is set to 10S */
if(HAL_I2C_Slave_Transmit(&hi2c1, (uint8_t*)aTxBuffer, TXBUFFERSIZE, 10000)!= HAL_OK)
{
/* Transfer error in transmission process */
Error_Handler();
}
// 库函数定义如下
/**
* @brief Transmits in slave mode an amount of data in blocking mode.
* @param hi2c Pointer to a I2C_HandleTypeDef structure that contains
* the configuration information for the specified I2C.
* @param pData Pointer to data buffer
* @param Size Amount of data to be sent
* @param Timeout Timeout duration
* @retval HAL status
*/
HAL_StatusTypeDef HAL_I2C_Slave_Transmit(I2C_HandleTypeDef *hi2c,
uint8_t *pData,
uint16_t Size,
uint32_t Timeout)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
此函数中使用了 HAL_I2C_Slave_Transmit
库函数
# 错误回调函数
//错误回调函数
/**
* @brief I2C error callbacks.
* @param I2cHandle: I2C handle
* @note This example shows a simple way to report transfer error, and you can
* add your own implementation.
* @retval None
*/
void HAL_I2C_ErrorCallback(I2C_HandleTypeDef *I2cHandle)
{
//
}
2
3
4
5
6
7
8
9
10
11
12
# 中断方式
先上代码:
////主机模式中断发送
/* While the I2C in reception process, user can transmit data through
"aTxBuffer" buffer */
if(HAL_I2C_Master_Transmit_IT(&hi2c1, (uint16_t)I2C_ADDRESS, (uint8_t*)aTxBuffer, TXBUFFERSIZE)!= HAL_OK)
{
/* Error_Handler() function is called in case of error. */
Error_Handler();
}
/*##-3- Wait for the end of the transfer ###################################*/
while (HAL_I2C_GetState(&hi2c1) != HAL_I2C_STATE_READY)
{
}
////主机模式中断接收
if(HAL_I2C_Master_Receive_IT(&hi2c1, (uint16_t)I2C_ADDRESS, (uint8_t *)aRxBuffer, RXBUFFERSIZE) != HAL_OK)
{
/* Error_Handler() function is called in case of error. */
Error_Handler();
}
//主机模式发送回调函数
void HAL_I2C_MasterTxCpltCallback(I2C_HandleTypeDef *I2cHandle)
{
//
}
//主机模式接收回调函数
void HAL_I2C_MasterRxCpltCallback(I2C_HandleTypeDef *I2cHandle)
{
//
}
////从机模式中断接收
if(HAL_I2C_Slave_Receive_IT(&hi2c1, (uint8_t *)aRxBuffer, RXBUFFERSIZE) != HAL_OK)
{
/* Transfer error in reception process */
Error_Handler();
}
/*##-3- Wait for the end of the transfer ###################################*/
while (HAL_I2C_GetState(&hi2c1) != HAL_I2C_STATE_READY)
{
}
////从机模式中断发送
/*##-4- Start the transmission process #####################################*/
/* While the I2C in reception process, user can transmit data through
"aTxBuffer" buffer */
if(HAL_I2C_Slave_Transmit_IT(&hi2c1, (uint8_t*)aTxBuffer, TXBUFFERSIZE)!= HAL_OK)
{
/* Transfer error in transmission process */
Error_Handler();
}
//从机模式发送回调函数
void HAL_I2C_SlaveTxCpltCallback(I2C_HandleTypeDef *I2cHandle)
{
//
}
//从机模式接收回调函数
void HAL_I2C_SlaveRxCpltCallback(I2C_HandleTypeDef *I2cHandle)
{
//
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
简要的分析一下示例代码:
# 库函数部分
相对于 Polling 方式,中断方式的库函数少了最长的等待时间(详细的参数请参考上面或者函数手册),这也是正常的
但是我们需要使能中断。并参考串口中断方式的经验:比如中断函数是否需要反复使能等
# 自封装函数
相对于 Polling 模式,由于发送时长不定,所以需要加上一个死循环来确保数据发送完成
# DMA 模式
//主机模式DMA发送
while(HAL_I2C_Master_Transmit_DMA(&hi2c1, (uint16_t)I2C_ADDRESS, (uint8_t*)aTxBuffer, TXBUFFERSIZE)!= HAL_OK)
{
if (HAL_I2C_GetError(&hi2c1) != HAL_I2C_ERROR_AF)
{
Error_Handler();
}
}
/*##-3- Wait for the end of the transfer ###################################*/
while (HAL_I2C_GetState(&hi2c1) != HAL_I2C_STATE_READY);
//主机模式DMA接收
while(HAL_I2C_Master_Receive_DMA(&hi2c1, (uint16_t)I2C_ADDRESS, (uint8_t *)aRxBuffer, RXBUFFERSIZE) != HAL_OK)
{
if (HAL_I2C_GetError(&hi2c1) != HAL_I2C_ERROR_AF)
{
Error_Handler();
}
}
//从机模式DMA发送
if(HAL_I2C_Slave_Receive_DMA(&hi2c1, (uint8_t *)aRxBuffer, RXBUFFERSIZE) != HAL_OK)
{
Error_Handler();
}
while (HAL_I2C_GetState(&hi2c1) != HAL_I2C_STATE_READY);
//从机模式DMA接收
if(HAL_I2C_Slave_Transmit_DMA(&hi2c1, (uint8_t*)aTxBuffer, TXBUFFERSIZE)!= HAL_OK)
{
/* Transfer error in transmission process */
Error_Handler();
}
while (HAL_I2C_GetState(&hi2c1) != HAL_I2C_STATE_READY);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
分析与上面中断模式相同。但是有一点仍需注意:DMA 也可以触发中断,与普通的中断一一对应中断函数
# 对 I2C 设备的寄存器操作
# 示例代码
uint8_t HALIIC_WriteByteToSlave(uint8_t I2C_Addr,uint8_t reg,uint8_t data)
{
uint8_t *pData;
pData = &data;
return HAL_I2C_Mem_Write(&hi2c1, I2C_Addr, reg, I2C_MEMADD_SIZE_8BIT, pData, 1, 100);
}
uint8_t HALIIC_ReadByteFromSlave(uint8_t I2C_Addr,uint8_t reg,uint8_t *buf)
{
return HAL_I2C_Mem_Read(&hi2c1, I2C_Addr, reg, I2C_MEMADD_SIZE_8BIT, buf, 1, 100);
}
uint8_t HALIIC_ReadMultByteFromSlave(uint8_t dev, uint8_t reg, uint8_t length, uint8_t *data)
{
return HAL_I2C_Mem_Read(&hi2c1, dev, reg, I2C_MEMADD_SIZE_8BIT, data, length, 200);
}
uint8_t HALIIC_WriteMultByteToSlave(uint8_t dev, uint8_t reg, uint8_t length, uint8_t* data)
{
return HAL_I2C_Mem_Write(&hi2c1, dev, reg, I2C_MEMADD_SIZE_8BIT, data, length, 200);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 读取函数
HAL_StatusTypeDef HAL_I2C_Mem_Read(I2C_HandleTypeDef *hi2c,
uint16_t DevAddress,
uint16_t MemAddress,
uint16_t MemAddSize,
uint8_t *pData,
uint16_t Size,
uint32_t Timeout)
2
3
4
5
6
7
# 写入函数
HAL_StatusTypeDef HAL_I2C_Mem_Write(I2C_HandleTypeDef *hi2c,
uint16_t DevAddress,
uint16_t MemAddress,
uint16_t MemAddSize,
uint8_t *pData,
uint16_t Size,
uint32_t Timeout)
2
3
4
5
6
7
# 使用案例
请见这篇文章 (opens new window),显示了对于寄存器的操作修改步骤
# 流程
# Bonus
# 从机地址如何确定
微控制器作为主设备时
每个总线上的设备都可以被唯一的一个地址所寻址,能挂在总线上的设备数量是受到限制的。一个主机可以寻址包括其他主机在内的总线上的所有其他设备。
在发出起始条件后发送从 7 位的设备地址 (MSB)+1 位读写位。7 位从设备的地址可以查阅对应器件的手册的 I2C 部分。比如 max395X 系列中有:
46h 即
1000110b
(某种方法换算的)。这样如果对该设备进行读或写操作的 8 位地址分别为 10001101b 和 10001100b, 即 8Dh 和 8Ch详细信息请参考这篇文章 (opens new window)
微控制器作为从设备时
按照数据手册配置寄存器
# 操作须知
由于 I2C 的通讯对象通常为各种传感器,除了协议规定的两根接线还需要一些外加的连线
且传感器的复位、等等操作可能需要设置不同的操作。所以,多读文档!
# 参考
[1] STM32 HAL 库学习(四):I2C 协议篇:https://blog.csdn.net/la_fe_/article/details/100315073 (opens new window)
[2] RoboMaster 开发板 C 型嵌入式教程文档
[3] I2C 从设备地址 (Slave Address) 的设置与获得:https://blog.csdn.net/sunny92536/article/details/95171020 (opens new window)
[4] IIC/I2C 从地址之 7 位,8 位和 10 位详解:http://www.toomoss.com/news/12-cn.html (opens new window)
[5] STM32CubeIDE HAL 库操作 IIC(二)案例篇(MPU9250):https://blog.csdn.net/u010779035/article/details/104362532 (opens new window)