Wiki:知识&笔记 Wiki:知识&笔记
首页
  • 学习笔记

    • 《JavaScript教程》笔记
  • 嵌入式

    • STM32
  • 技术文档
归档
GitHub (opens new window)
首页
  • 学习笔记

    • 《JavaScript教程》笔记
  • 嵌入式

    • STM32
  • 技术文档
归档
GitHub (opens new window)
  • 基础

  • 底层&寄存器

  • HAL库外设

    • HAL库函数
    • HAL结构
    • GPIO各种点亮LED
    • 定时器
    • PWM控制LED颜色
    • 按键外部中断
    • ADC使用
    • 串口与DMA
    • FLASH 读写
    • IIC通信
      • 硬件基础
        • 传输特点
        • 接线
      • 协议层面
        • 信号结构
        • 关于从机
        • 起始信号与终止信号
        • 地址与数据方向
        • 数据的有效性
        • 应答信号
      • CubeMX配置
        • 对中断的配置
        • DMA 配置
        • 其他设置
      • 代码示例
        • Polling 方式
        • 中断方式
        • DMA 模式
        • 对 I2C 设备的寄存器操作
        • 使用案例
      • 流程
      • Bonus
        • 从机地址如何确定
        • 操作须知
      • 参考
    • SPI 通信
    • CAN
  • STM32
  • HAL库外设
2020-10-03
硬件基础
传输特点
接线
协议层面
信号结构
关于从机
起始信号与终止信号
地址与数据方向
数据的有效性
应答信号
CubeMX配置
对中断的配置
DMA 配置
其他设置
代码示例
Polling 方式
中断方式
DMA 模式
对 I2C 设备的寄存器操作
使用案例
流程
Bonus
从机地址如何确定
操作须知
参考

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) 
1
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)
1
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)
1
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)
1
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)
{
  //
}
1
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)
{
    //
}
1
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);
1
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);
}
1
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)
1
2
3
4
5
6
7
image-20201003144747729

# 写入函数

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)
1
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)

编辑 (opens new window)
上次更新: 2020/11/18, 13:11:00
FLASH 读写
SPI 通信

← FLASH 读写 SPI 通信→

Theme by Vdoing | Copyright © 2020-2025 Jack :) | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式