IIC
IIC(Inter-Integrated Circuit)总线是一种由NXP(原PHILIPS)公司开发的两线式串行总线,用于连接微控制器及其外围设备。多用于主机和从机在数据量不大且传输距离短的场合下的主从通信,在小数据量场合使用,传输距离短,任意时刻只能有一个主机等特性。I2C总线由数据线SDA和时钟线SCL构成通信线路,既可用于发送数据,也可接收数据,是一种半双工通信协议。总线上的主设备与从设备之间以字节(8位)为单位进行双向的数据传输。
IIC有3种传输模式:
- 标准模式:100K bit/s
- 快速模式:400K bit/s
- 高速模式:3.4M bit/s
IIC 的物理层
IIC一共有只有两个总线: 一条是双向的串行数据线SDA,一条是串行时钟线SCL
- SDA(Serial data)是数据线,D代表Data也就是数据,SendData 也就是用来传输数据的。
- SCL(Serial clock line)是时钟线,C代表 Clock 也就是时钟 也就是控制数据发送的时序的。
所有接到I2C总线设备上的串行数据SDA都接到总线的SDA上,各设备的时钟线SCL接到总线的SCL上。I2C总线上的每个设备都自己一个唯一的地址,来确保不同设备之间访问的准确性。
通常我们为了方便把IIC设备分为主设备和从设备,基本上谁控制时钟线(即控制SCL的电平高低变换)谁就是主设备 。
- IIC主设备功能:主要产生时钟,产生起始信号和停止信号
- IIC从设备功能:可编程的IIC地址检测,停止位检测
- IIC的一个优点是它支持多主控(multimastering), 其中任何一个能够进行发送和接收的设备都可以成为主总线。一个主控能够控制信号的传输和时钟频率。当然,在任何时间点上只能有一个主控。
- 支持不同速率的通讯速度,标准速度(最高速度100kHZ),快速(最高400kHZ)
- SCL和SDA都需要接上拉电阻 (大小由速度和容性负载决定一般在3.3K-10K之间) 保证数据的稳定性,减少干扰。
- IIC是半双工,而不是全双工 ,同一时间只可以单向通信
- 为了避免总线信号的混乱,要求各设备连接到总线的输出端时必须是漏极开路(OD)输出或集电极开路(OC)输出。
IIC 协议层
I2C 总线在传送数据过程中共有三种类型信号, 它们分别是:开始信号、结束信号和应答信号。
- 开始信号:SCL 为高电平时,SDA 由高电平向低电平跳变,开始传送数据。
- 结束信号:SCL 为高电平时,SDA 由低电平向高电平跳变,结束传送数据。
- 应答信号:接收数据的 IC 在接收到 8bit 数据后,向发送数据的 IC 发出特定的低电平脉冲,表示已收到数据。CPU 向受控单元发出一个信号后,等待受控单元发出一个应答信号,CPU 接收到应答信号后,根据实际情况作出是否继续传递信号的判断。若未收到应答信号,由判断为受控单元出现故障。
IIC 时序
1. 空闲状态
I2C总线的SDA和SCL两条信号线同时处于高电平时,规定为总线的空闲状态。此时各个器件的输出级场效应管均处在截止状态,即释放总线,由两条信号线各自的上拉电阻把电平拉高。
在 SCL 为高电平期间,SDA 必须保持稳定。所以 SDA 改变状态最好在 SCL 为低电平的时候改变,如果在高电平改变的话回被认为是一种有效信号(如:起始信号或者结束信号)
2. 起始信号
在SCL保持高电平期间,SDA的电平被拉低,称为I2C总线总线的起始信号,标志着一次数据传输开始。起始信号由主机主动建立的,在建立该信号之前I2C总线必须处于空闲状态。
起始信号,它是需要有一定的保持时间的,在 SDA 从高电平向低电平跳变的时候,两个先必须至少保持 4.7us 的时间,而跳变之后,也要保持 SCL 高电平和 SDA 低电平要至少保持 4us 的时间(从这里我们看出 I2C 总高速率已经决定了) 。
3. 停止信号
在SCL保持高电平期间,SDA被释放,返回高电平,称为I2C总线的停止信号,标志着一次数据传输的终止。停止信号由主机主动建立的,建立该信号之后,I2C总线将返回空闲状态。
SCL保持高电平。SDA由低电平变为高电平。
4. 应答信号
在 I2C 通信中,针对每个数据传输操作,接收端设备需要发送一个应答信号来确认数据是否正确接收。
应答信号有两种类型:主机应答和从机应答。主机应答是由 I2C 总线主机发出的,用于确认从机已经成功地接收到之前发送的数据。而从机应答则是由从机设备发出的,用于确认主机接收到数据并请求继续传输。
在 I2C 通信中,每次发送一个字节时,接收端会通过拉低 SDA 引脚,并保持 SCL 引脚为高电平,来向发送端发送应答(ACK)信号。如果发送端成功接收到应答信号,则继续发送下一个字节;如果没有成功接收到应答信号,则停止发送数据,并发出停止(STOP)信号,通信结束。
一般而言,在主设备向从设备发送数据时,主设备总是发送完一个字节后等待从设备发送应答信号。而在从设备向主设备发送数据时,从设备发送完一个字节后等待主设备发送应答信号。
此外,还有一种特殊情况,即发送端希望终止通信,可以在发送完最后一个字节后不再等待应答信号,而是通过拉低 SDA 引脚,并保持 SCL 引脚为高电平,向接收端发送非应答(NACK)信号,然后发出停止(STOP)信号结束通信。
5. 数据传输
数据传输格式
SDA线上的数据在SCL时钟“高”期间必须是稳定的,只有当SCL线上的时钟信号为低时,数据线上的“高”或“低”状态才可以改变。输出到SDA线上的每个字节必须是8位,数据传送时,先传送最高位(MSB),每一个被传送的字节后面都必须跟随一位应答位(即一帧共有9位)。
当一个字节按数据位从高位到低位的顺序传输完后,紧接着从设备将拉低SDA线,回传给主设备一个应答位ACK, 此时才认为一个字节真正的被传输完成 ,如果一段时间内没有收到从机的应答信号,则自动认为从机已正确接收到数据。
IIC写数据
多数从设备的地址为7位或者10位,一般都用七位。
八位设备地址=7位从机地址+读/写地址
再给地址添加一个方向位位用来表示接下来数据传输的方向,0表示主设备向从设备(write)写数据,1表示主设备向从设备(read)读数据。
在起始信号后必须传送一个从机的地址(7位) 1~7位为7位接收器件地址,第8位为读写位,用“0”表示主机发送数据(W),“1”表示主机接收数据 (R), 第9位为ACK应答位,紧接着的为第一个数据字节,然后是一位应答位,后面继续第2个数据字节。
6. IIC发送数据
- Start: IIC开始信号,表示开始传输。
- DEVICE_ADDRESS:: 从设备地址,就是7位从机地址
- R/W: W(write)为写,R(read)为读
- ACK: 应答信号
- WORD_ADDRESS : 从机中对应的寄存器地址 比方说访问 OLED中的 某个寄存器
- DATA: 发送的数据
- STOP: 停止信号。结束IIC
主机要向从机写数据时:
- 主机首先产生START信号
- 然后紧跟着发送一个从机地址,这个地址共有7位,紧接着的第8位是数据方 向位(R/W),0表示主机发送数据(写),1表示主机接收数据(读)
- 主机发送地址时,总线上的每个从机都将这7位地址码与自己的地址进行比较,若相同,则认为自己正在被主机寻址,根据R/T位将自己确定为发送器和接收器
- 这时候主机等待从机的应答信号(A)
- 当主机收到应答信号时,发送要访问从机的那个地址, 继续等待从机的应答信号
- 当主机收到应答信号时,发送N个字节的数据,继续等待从机的N次应答信号,
- 主机产生停止信号,结束传送过程。
6. IIC读取数据
主机要从从机读数据时:
- 主机首先产生START信号
- 然后紧跟着发送一个从机地址,注意此时该地址的第8位为0,表明是向从机写命令,
- 这时候主机等待从机的应答信号(ACK)
- 当主机收到应答信号时,发送要访问的地址,继续等待从机的应答信号,
- 当主机收到应答信号后,主机要改变通信模式(主机将由发送变为接收,从机将由接收变为发送)所以主机重新发送一个开始start信号,然后紧跟着发送一个从机地址,注意此时该地址的第8位为1,表明将主机设 置成接收模式开始读取数据,
- 这时候主机等待从机的应答信号,当主机收到应答信号时,就可以接收1个字节的数据,当接收完成后,主机发送非应答信号,表示不在接收数据
- 主机进而产生停止信号,结束传送过程。
ESP32-S3 中的 IIC
ESP32-S3 具有两个 I2C 主机接口,称为 I2C0 和 I2C1。
I2C0 接口具有 GPIO 总线复用功能,可使用不同的管脚实现与其他外设的兼容性。其主要特点如下:
- 具有 I2C 从设备模式支持;
- 支持标准、快速和高速模式;
- 可选择不同的时钟频率作为主时钟;
- 可作为主机和从机运行。
使用 IIC 驱动控制 IIC 总线
- 以下部分将指导您完成 I2C 驱动程序配置和工作的基本步骤:
- 配置驱动程序 - 设置初始化参数(如主机模式或从机模式,SDA 和 SCL 使用的 GPIO 管脚,时钟速度等)
- 安装驱动程序- 激活一个 I2C 控制器的驱动,该控制器可为主机也可为从机
- 根据是为主机还是从机配置驱动程序,选择合适的项目
a. 主机模式下通信 - 发起通信(主机模式)
b. 从机模式下通信 - 响应主机消息(从机模式) - 中断处理 - 配置 I2C 中断服务
- 用户自定义配置 - 调整默认的 I2C 通信参数(如时序、位序等)
- 错误处理 - 如何识别和处理驱动程序配置和通信错误
- 删除驱动程序- 在通信结束时释放 I2C 驱动程序所使用的资源
1. 配置驱动程序
建立 I2C 通信第一步是配置驱动程序,这需要设置 i2c_config_t 结构中的几个参数:
- 设置 I2C 工作模式 - 从 i2c_mode_t 中选择主机模式或从机模式
- 设置 通信管脚
- 指定 SDA 和 SCL 信号使用的 GPIO 管脚
- 是否启用 ESP32-S3 的内部上拉电阻
- (仅限主机模式)设置 I2C 时钟速度
- (仅限从机模式)设置以下内容:
- 是否应启用 10 位寻址模式
- 定义 从机地址
- 然后,初始化给定 I2C 端口的配置,请使用端口号和 i2c_config_t 作为函数调用参数来调用 i2c_param_config() 函数。
配置主机:
int i2c_master_port = 0;
i2c_config_t conf = {.mode = I2C_MODE_MASTER, // 设置 I2C 接口为主机模式.sda_io_num = I2C_MASTER_SDA_IO, // 选择连接到 ESP32 的 SDA 引脚号.sda_pullup_en = GPIO_PULLUP_ENABLE, // 启用 SDA 引脚上拉电阻.scl_io_num = I2C_MASTER_SCL_IO, // 选择连接到 ESP32 的 SCL 引脚号.scl_pullup_en = GPIO_PULLUP_ENABLE, // 启用 SCL 引脚上拉电阻.master.clk_speed = I2C_MASTER_FREQ_HZ, // 设置 I2C 主机接口的时钟频率// .clk_flags = 0, // 可选项,可以在这里使用 I2C_SCLK_SRC_FLAG_ 标志来选择 i2c 源时钟.
};
配置从机:
int i2c_slave_port = I2C_SLAVE_NUM;
i2c_config_t conf_slave = {.sda_io_num = I2C_SLAVE_SDA_IO, // 选择连接到 ESP32 的 SDA 引脚号.sda_pullup_en = GPIO_PULLUP_ENABLE, // 启用 SDA 引脚上拉电阻.scl_io_num = I2C_SLAVE_SCL_IO, // 选择连接到 ESP32 的 SCL 引脚号.scl_pullup_en = GPIO_PULLUP_ENABLE, // 启用 SCL 引脚上拉电阻.mode = I2C_MODE_SLAVE, // 设置 I2C 接口为从机模式.slave.addr_10bit_en = 0, // 不使用 10 位地址模式.slave.slave_addr = ESP_SLAVE_ADDR, // 从机设备地址
};
在此阶段,i2c_param_config() 还将其他 I2C 配置参数设置为 I2C 总线协议规范中定义的默认值。
**i2c_param_config()**函数原型如下:
esp_err_t i2c_param_config(i2c_port_t i2c_num, const i2c_config_t* conf);
参数 | 描述 |
---|---|
i2c_num | 表示要配置的 I2C 总线接口号,即 I2C_NUM_0 或 I2C_NUM_1。 |
conf | 指向 i2c_config_t 类型结构体的指针,用于指定 I2C 总线接口的详细配置参数。 |
i2c_config_t 结构体原型如下:
typedef struct{i2c_mode_t mode; /*!< I2C mode */int sda_io_num; /*!< GPIO number for I2C sda signal */int scl_io_num; /*!< GPIO number for I2C scl signal */bool sda_pullup_en; /*!< Internal GPIO pull mode for I2C sda signal*/bool scl_pullup_en; /*!< Internal GPIO pull mode for I2C scl signal*/union {struct {uint32_t clk_speed; /*!< I2C clock frequency for master mode, (no higher than 1MHz for now) */} master; /*!< I2C master config */struct {uint8_t addr_10bit_en; /*!< I2C 10bit address mode enable for slave mode */uint16_t slave_addr; /*!< I2C address for slave mode */uint32_t maximum_speed; /*!< I2C expected clock speed from SCL. */} slave; /*!< I2C slave config */};uint32_t clk_flags; /*!< Bitwise of ``I2C_SCLK_SRC_FLAG_**FOR_DFS**`` for clk source choice*/
} i2c_config_t;
参数 | 描述 |
---|---|
mode | 接口的工作模式,可以设置为主机模式或从设备模式。 |
sda_io_num | 连接到 ESP32 的 SDA 引脚的引脚号。 |
scl_io_num | 连接到 ESP32 的 SCL 引脚的引脚号。 |
sda_pullup_en | 是否启用 SDA 引脚上拉电阻,可以设置为 GPIO_PULLUP_ENABLE 或 GPIO_PULLUP_DISABLE。 |
scl_pullup_en | 是否启用 SCL 引脚上拉电阻,可以设置为 GPIO_PULLUP_ENABLE 或 GPIO_PULLUP_DISABLE。 |
master.clk_speed | I2C 主机接口的时钟频率,单位是 Hz。(主机配置有效) |
slave.addr_10bit_en | 是否启用 10 位地址模式,可以设置为 0 或 1。(从机配置有效) |
slave.slave_addr | 从设备的地址。(从机配置有效) |
slave.maximum_speed | SLC预期的时钟速度。 (从机配置有效) |
2. 配置时钟源
增加了时钟源分配器,用于支持不同的时钟源。时钟分配器将选择一个满足所有频率和能力要求的时钟源(如 i2c_config_t::clk_flags 中的要求)。
当 i2c_config_t::clk_flags 为 0 时,时钟分配器将仅根据所需频率进行选择。如果不需要诸如 APB 之类的特殊功能,则可以将时钟分配器配置为仅根据所需频率选择源时钟。为此,请将 i2c_config_t::clk_flags 设置为 0。有关时钟特性,请参见下表。
时钟名称 | 时钟频率 | SCL最大频率 | 时钟功能 |
---|---|---|---|
XTAL时钟 | 40HMz | 2MHz | – |
RTC时钟 | 20MHz | 1MHz | I2C_SCLK_SRC_FLAG_AWARE_DFS, I2C_SCLK_SRC_FLAG_LIGHT_SLEEP |
对 i2c_config_t::clk_flags 的解释如下:
- I2C_SCLK_SRC_FLAG_AWARE_DFS:当 APB 时钟改变时,时钟的波特率不会改变。
- I2C_SCLK_SRC_FLAG_LIGHT_SLEEP:支持轻度睡眠模式,APB 时钟则不支持。
- ESP32-S3 可能不支持某些标志,请在使用前阅读技术参考手册。
3. 安装驱动程序
配置好 I2C 驱动程序后,使用以下参数调用函数 i2c_driver_install() 安装驱动程序:
- 端口号,从 i2c_port_t 中二选一
- 主机或从机模式,从 i2c_mode_t 中选择
- (仅限从机模式)分配用于在从机模式下发送和接收数据的缓存区大小。I2C 是一个以主机为中心的总线,数据只能根据主机的请求从从机传输到主机。因此,从机通常有一个发送缓存区,供从应用程序写入数据使用。数据保留在发送缓存区中,由主机自行读取。
- 用于分配中断的标志(请参考 esp_hw_support/include/esp_intr_alloc.h 中 ESP_INTR_FLAG_* 值)
i2c_driver_install 函数是 ESP32 I2C 总线驱动程序中的一个重要函数,用于安装 I2C 总线驱动并开启 I2C 总线。该函数的原型为:
esp_err_t i2c_driver_install(i2c_port_t i2c_num, i2c_mode_t mode, size_t slv_rx_buf_len, size_t slv_tx_buf_len, int intr_alloc_flags);
参数 | 描述 |
---|---|
i2c_num | I2C总线的端口号,可以是 I2C_NUM_0 或 I2C_NUM_1。 |
mode | I2C 总线的工作模式,可以选择 I2C_MODE_MASTER 或 I2C_MODE_SLAVE。 |
slv_rx_buf_len | I2C 接收缓冲区长度,单位是字节。 |
slv_tx_buf_len | I2C 发送缓冲区长度,单位是字节。 |
intr_alloc_flags | 中断服务程序分配标志位,可以设置为 0 或 ESP_INTR_FLAG_* 中的一个或多个值。这个参数通常用来控制 I2C 中断服务程序运行时的优先级、占用的 CPU 时间等。 |
返回值 | 该函数的返回值为 esp_err_t 类型,如果函数执行成功,则返回 ESP_OK。否则,返回相应的错误码表明函数执行失败。 |
4. 主机模式下的通讯
安装 I2C 驱动程序后, ESP32-S3 即可与其他 I2C 设备通信。
ESP32-S3 的 I2C 控制器在主机模式下负责与 I2C 从机设备建立通信,并发送命令让从机响应,如进行测量并将结果发给主机。
为优化通信流程,驱动程序提供一个名为 “命令链接” 的容器,该容器应填充一系列命令,然后传递给 I2C 控制器执行。
a. 主机写入数据
下面的示例展示如何为 I2C 主机构建命令链接,从而向从机发送 n 个字节。
下面介绍如何为 “主机写入数据” 设置命令链接及其内部内容:
- 使用 i2c_cmd_link_create() 创建一个命令链接。
然后,将一系列待发送给从机的数据填充命令链接:- 启动位 - i2c_master_start()
- 从机地址 - i2c_master_write_byte() ,提供单字节地址作为调用此函数的实参。
- 数据 - 一个或多个字节的数据作为 i2c_master_write() 的实参。
- 停止位 - i2c_master_stop()
函数 i2c_master_write_byte() 和 i2c_master_write() 都有额外的实参,规定主机是否应确认其有无接受到 ACK 位。
- 通过调用 i2c_master_cmd_begin() 来触发 I2C 控制器执行命令链接。一旦开始执行,就不能再修改命令链接。
- 命令发送后,通过调用 i2c_cmd_link_delete() 释放命令链接使用的资源。
b. 主机读取数据
下面的示例展示如何为 I2C 主机构建命令链接,以便从从机读取 n 个字节。
在读取数据时,在上图的步骤 4 中,不是用 i2c_master_write…,而是用 i2c_master_read_byte() 和 i2c_master_read() 填充命令链接。同样,在步骤 5 中配置最后一次的读取,以便主机不提供 ACK 位。
c. 指示写入或读取数据
发送从机地址后(请参考上图中第 3 步),主机可以写入或从从机读取数据。
主机实际执行的操作信息存储在从机地址的最低有效位中。
因此,为了将数据写入从机,主机发送的命令链接应包含地址 (ESP_SLAVE_ADDR << 1) | I2C_MASTER_WRITE,如下所示:
i2c_master_write_byte(cmd, (ESP_SLAVE_ADDR << 1) | I2C_MASTER_WRITE, ACK_EN);
同理,指示从从机读取数据的命令链接如下所示:
i2c_master_write_byte(cmd, (ESP_SLAVE_ADDR << 1) | I2C_MASTER_READ, ACK_EN);
d. 相关函数
i2c_cmd_link_create 函数用于创建一个 I2C 多条命令链表 i2c_cmd_handle_t。在 I2C 数据传输时,可以通过向该链表中添加多条命令,实现对 I2C 总线上数据传输的控制。原型如下:
i2c_cmd_handle_t i2c_cmd_link_create(void);
该函数没有任何参数输入,它将返回一个 i2c_cmd_handle_t 类型的变量值,即 I2C 命令链表的句柄。这个句柄可以用于后续操作,例如向链表中添加 I2C 命令或在传输完成后释放链表资源等。使用 i2c_cmd_link_create 创建的链表是一个双向链表结构体,可以存储多条 I2C 命令,例如开始信号、停止信号、读或写数据等。创建链表后,可以通过向链表中添加单个命令来定制 I2C 总线通讯中的各种参数,例如地址、数据长度、应答方式等等。使用完 i2c_cmd_handle_t 句柄之后需要调用 i2c_cmd_link_delete 函数来释放链表资源,避免造成内存泄漏。
i2c_cmd_link_delete 函数用于删除 i2c_cmd_link_create 创建的 i2c_cmd_link_t 命令列表对象及内部所有的命令节点,函数原型如下:
void i2c_cmd_link_delete(i2c_cmd_handle_t cmd);
其中,cmd 参数是通过 i2c_cmd_link_create 函数创建的 i2c_cmd_handle_t 句柄,表示待删除的命令链表对象。在 I2C 数据传输过程中,主机会通过 i2c_cmd_link_add 函数向命令链表中逐个添加 I2C 命令节点。当命令链表中的所有命令节点都添加完成后,可以通过 i2c_master_cmd_begin 函数执行命令链表中的命令。执行完命令链表中的命令后,需要释放命令链表对象及其内部的所有命令节点所占用的内存空间,以避免内存泄漏。此时,可以使用 i2c_cmd_link_delete 函数来释放命令链表对象及内部的所有命令节点所占用的内存空间。
i2c_master_start 函数用于向 I2C 总线发送起始信号(Start Bit),开始一次 I2C 数据传输。函数原型如下:
esp_err_t i2c_master_start(i2c_cmd_handle_t cmd);
其中,cmd 参数是通过 i2c_cmd_link_create 函数创建的 i2c_cmd_handle_t 句柄,表示待发送的 I2C 命令链表。在命令链表中添加 I2C 命令后,可以调用 i2c_master_start 函数来启动 I2C 数据传输,发送起始信号。在 I2C 数据传输中,起始信号用于将 I2C 总线上的所有设备从空闲状态转换到忙碌状态,表示当前总线上有主机开始进行数据传输。在发送起始信号后,主机可以通过向从设备发送地址和数据等信息进行数据传输,最终发送停止信号结束本次数据传输。
i2c_master_stop 函数用于向 I2C 总线发送停止信号(Stop Bit),结束一次 I2C 数据传输,函数原型如下:
esp_err_t i2c_master_stop(i2c_cmd_handle_t cmd);
其中,cmd 参数是通过 i2c_cmd_link_create 函数创建的 i2c_cmd_handle_t 句柄,表示当前数据传输所使用的命令链表。在 I2C 数据传输中,停止信号用于将 I2C 总线上的所有设备从忙碌状态转换到空闲状态,表示当前数据传输已经结束。发送停止信号后,主机和从设备都可以开始进行其他操作。
i2c_master_write_byte 函数用于向 I2C 纵向上的从设备下入单个字节数据,函数原型如下:
esp_err_t i2c_master_write_byte(i2c_cmd_handle_t cmd, uint8_t data, bool ack_en);
其中,cmd 参数是通过 i2c_cmd_link_create 函数创建的 i2c_cmd_handle_t 句柄,表示待发送的 I2C 命令链表;data 参数表示待写入的单个字节数据;ack_en 参数表示是否启用应答机制。在 I2C 数据传输过程中,主机可以向从设备写入单个字节数据,并可以选择是否使能应答机制。如果应答机制被禁用,则从设备不会发送应答信号,这时主机必须停止当前数据传输并向从设备发送停止信号。如果启用应答机制,则从设备会在接收到数据后发送应答信号,表示数据已成功接收。在发送完每个字节数据后,主机必须等待从设备发送应答信号,否则本次传输将被视为失败。
i2c_master_write 函数用于向 I2C 总线上的从设备写入多个字节数据,函数原型如下:
esp_err_t i2c_master_write(i2c_cmd_handle_t cmd, uint8_t *data, size_t data_len, bool ack_en);
其中,cmd 参数是通过 i2c_cmd_link_create 函数创建的 i2c_cmd_handle_t 句柄,表示待发送的 I2C 命令链表;data 参数表示待写入的数据缓冲区地址;data_len 表示待写入的数据字节数;ack_en 参数表示是否启用应答机制。在 I2C 数据传输过程中,主机可以向从设备写入多个字节数据,并可以选择是否使能应答机制。如果应答机制被禁用,则从设备不会发送应答信号,这时主机必须停止当前数据传输并向从设备发送停止信号。如果启用应答机制,则从设备会在接收到数据后发送应答信号,表示数据已成功接收。在发送完每个字节数据后,主机必须等待从设备发送应答信号,否则本次传输将被视为失败。
i2c_master_read_byte 函数用于从 I2C 总线上的从设备读取单个字节的数据,函数原型如下:
esp_err_t i2c_master_read_byte(i2c_cmd_handle_t cmd, uint8_t *data, i2c_ack_type_t ack_type);
其中,cmd 参数是通过 i2c_cmd_link_create 函数创建的 i2c_cmd_handle_t 句柄,表示待发送的 I2C 命令链表;data 参数表示存储读取数据的缓冲区地址;ack_type 参数表示应答类型,可以是 I2C_MASTER_ACK 或 I2C_MASTER_NACK。在 I2C 数据传输过程中,主机可以向从设备读取单个字节数据。读取数据时,主机会向从设备发送 ACK 或 NACK 信号作为应答,表示接收数据是否成功。如果应答类型为 ACK,则表示主机成功接收到一字节数据并希望从设备继续发送数据;如果应答类型为 NACK,则表示主机已经成功接收了最后一字节数据并结束本次数据传输。
i2c_master_read 函数用于从 I2C 总线的从设备读取多个字节数据,函数原型如下:
esp_err_t i2c_master_read(i2c_cmd_handle_t cmd, uint8_t *data, size_t data_len, i2c_ack_type_t ack_type);
其中,cmd 参数是通过 i2c_cmd_link_create 函数创建的 i2c_cmd_handle_t 句柄,表示待发送的 I2C 命令链表;data 参数表示存储读取数据的缓冲区地址;data_len 表示期望读取数据的字节数;ack_type 参数表示应答类型,可以是 I2C_MASTER_ACK 或 I2C_MASTER_NACK。在 I2C 数据传输过程中,主机可以向从设备读取多个字节数据。读取数据时,主机会向从设备发送 ACK 或 NACK 信号作为应答,表示接收数据是否成功。如果应答类型为 ACK,则表示主机成功接收到一字节数据并希望从设备继续发送数据;如果应答类型为 NACK,则表示主机已经成功接收了最后一字节数据并结束本次数据传输。
i2c_master_cmd_begin 函数用于执行 I2C 命令链表中的所有命令,函数原型如下:
esp_err_t i2c_master_cmd_begin(i2c_port_t port, i2c_cmd_handle_t cmd, TickType_t ticks_to_wait);
其中,port 参数表示 I2C 总线的端口号;cmd 参数是通过 i2c_cmd_link_create 函数创建的 i2c_cmd_handle_t 句柄,表示待执行的 I2C 命令链表;ticks_to_wait 参数表示等待执行命令的最长时间。在 I2C 数据传输过程中,主机会通过 i2c_cmd_link_add 函数向命令链表中逐个添加 I2C 命令节点。当命令链表中的所有命令节点都添加完成后,可以通过 i2c_master_cmd_begin 函数执行命令链表中的命令。执行命令时,主机会自动处理 I2C 数据总线上的应答信号,并根据应答信号的不同情况进行相应的错误处理。
5. 从机模式下通信
安装 I2C 驱动程序后, ESP32-S3 即可与其他 I2C 设备通信。
API 为从机提供以下功能:
-
i2c_slave_read_buffer()
当主机将数据写入从机时,从机将自动将其存储在接收缓存区中。从机应用程序可自行调用函数 i2c_slave_read_buffer()。如果接收缓存区中没有数据,此函数还具有一个参数用于指定阻塞时间。这将允许从机应用程序在指定的超时设定内等待数据到达缓存区。 -
i2c_slave_write_buffer()
发送缓存区是用于存储从机要以 FIFO 顺序发送给主机的所有数据。在主机请求接收前,这些数据一直存储在发送缓存区。函数 i2c_slave_write_buffer() 有一个参数,用于指定发送缓存区已满时的块时间。这将允许从机应用程序在指定的超时设定内等待发送缓存区中足够的可用空间。
i2c_slave_read_buffer 函数用于从 I2C 从设备接收数据,函数原型如下:
int i2c_slave_read_buffer(i2c_port_t i2c_num, uint8_t *data, size_t max_size, TickType_t ticks_to_wait);
参数 | 描述 |
---|---|
i2c_num | I2C总线的端口号,可以是 I2C_NUM_0 或 I2C_NUM_1。 |
data | 发送数据存储缓冲区地址 |
max_size | 缓冲区的最大容量 |
ticks_to_wait | 等待接收数据的最长时间 |
返回值 | 如果读取超时,则函数将返回 ESP_ERR_TIMEOUT 错误码;如果读取成功,函数将返回 ESP_OK 。 |
i2c_slave_write_buffer汉语用于向 I2C 从设备发送数据,函数原型如下:
int i2c_slave_write_buffer(i2c_port_t i2c_num, const uint8_t *data, int size, TickType_t ticks_to_wait);
参数 | 描述 |
---|---|
i2c_num | I2C总线的端口号,可以是 I2C_NUM_0 或 I2C_NUM_1。 |
data | 接收数据存储缓冲区地址 |
max_size | 缓冲区的最大容量 |
ticks_to_wait | 等待接收数据的最长时间 |
返回值 | 如果发送成功,则函数将返回 ESP_OK;如果发送失败,则函数将返回 ESP_FAIL 。 |
6. 中断处理
安装驱动程序时,默认情况下会安装中断处理程序。但是,您可以通过调用函数 i2c_isr_register() 来注册自己的而不是默认的中断处理程序。在运行自己的中断处理程序时,可以参考 ESP32-S3 技术参考手册 > I2C 控制器 (I2C) > 中断 [PDF],以获取有关 I2C 控制器触发的中断描述。
调用函数 i2c_isr_free() 删除中断处理程序。
7. 用户自定义配置
如本节末尾所述 配置驱动程序,函数 i2c_param_config() 在初始化 I2C 端口的驱动程序配置时,也会将几个 I2C 通信参数设置为 I2C 总线协议规范规定的默认值。其他一些相关参数已在 I2C 控制器的寄存器中预先配置。
通过调用下表中提供的专用函数,可以将所有这些参数更改为用户自定义值。请注意,时序值是在 APB 时钟周期中定义。APB 的频率在 I2C_APB_CLK_FREQ 中指定。
可以通过以下函数修改配置参数:
上述每个函数都有一个 get 对应项来检查当前设置的值。例如,调用 i2c_get_timeout() 来检查 I2C 超时值。
要检查在驱动程序配置过程中设置的参数默认值,请参考文件 driver/i2c.c 并查找带有后缀 _DEFAULT 的定义。
通过函数 i2c_set_pin() 可以为 SDA 和 SCL 信号选择不同的管脚并改变上拉配置。如果要修改已经输入的值,请使用函数 i2c_param_config()。
8. 错误处理
大多数 I2C 驱动程序的函数在成功完成时会返回 ESP_OK ,或在失败时会返回特定的错误代码。实时检查返回的值并进行错误处理是一种好习惯。驱动程序也会打印日志消息,其中包含错误说明,例如检查输入配置的正确性。有关详细信息,请参考文件 driver/i2c.c 并用后缀 _ERR_STR 查找定义。
使用专用中断来捕获通信故障。例如,如果从机将数据发送回主机耗费太长时间,会触发 I2C_TIME_OUT_INT 中断。详细信息请参考 中断处理。
如果出现通信失败,可以分别为发送和接收缓存区调用 i2c_reset_tx_fifo() 和 i2c_reset_rx_fifo() 来重置内部硬件缓存区。
9. 删除驱动程序
当使用 i2c_driver_install() 建立 I2C 通信,一段时间后不再需要 I2C 通信时,可以通过调用 i2c_driver_delete() 来移除驱动程序以释放分配的资源。
由于函数 i2c_driver_delete() 无法保证线程安全性,请在调用该函数移除驱动程序前务必确保所有的线程都已停止使用驱动程序。
函数原型如下:
esp_err_t i2c_driver_delete(i2c_port_t i2c_num)
需要传入一个 I2C 总线的端口号,可以是 I2C_NUM_0 或 I2C_NUM_1。
ESP32 操作 MPU6050
数据手册:https://www.findic.com/doc/browser/JLbnROvL6?doc_id=62833957#locale=zh-CN
中文资料:https://www.renrendoc.com/paper/234359541.html
MPU6050 的初始化顺序
1. 电源寄存器1(0x6B)
因为 MPU6050 上电后会处于低功耗模式,根据数据手册中说明:
ADDR | 寄存器名称 | B7 | B6 | B5 | B4 | B3 | B2 : 0 |
---|---|---|---|---|---|---|---|
0x6B | PWR_MGMT | DEVICE_RESET | SLEEP | CYCLE | - | TEMP_DIS | CLKSEL[2:0] |
含义 | PWR_MGMT | 重置 | 低功耗 | 循环测量 | - | 禁用温度传感器 | 时钟选择位 |
默认值 | PWR_MGMT | 0 | 1 | 1 | 0 | 0 | 0 0 0 |
具体指初始值可能有出入,所以在启动后第一件事就是设置该项,将值设置为0x00,表示不进入休眠模式、开启循环测量、开启温度传感器。 | |||||||
如果给DEVICE_RESET 设置为1,则表示重置所有其他位数据。 | |||||||
时钟选择: |
CLKSEL[2:0] | 时钟源 |
---|---|
000 | 内部 8M RC 晶振 |
001 | PLL,使用 X 轴陀螺作为参考 |
010 | PLL,使用 Y 轴陀螺作为参考 |
011 | PLL,使用 Z 轴陀螺作为参考 |
100 | PLL,使用外部 32.768Khz 作为参考 |
101 | PLL,使用外部 19.2Mhz 作为参考 |
110 | 保留 |
111 | 关闭时钟,保持时序产生电路复位状态 |
默认是使用内部 8M RC 晶振的,精度不高,所以我们一般选择 X/Y/Z 轴陀螺作为参考 的 PLL 作为时钟源,一般设置 CLKSEL=001 即可 |
2. 陀螺仪采样率分频寄存器(0x19)
该寄存器用于设置 MPU6050 的陀螺仪采样频率,计算公式为:
采样频率 = 陀螺仪输出频率 / (1+SMPLRT_DIV)
这里陀螺仪的输出频率,是 1Khz 或者 8Khz,与数字低通滤波器( DLPF)的设置有关, 当 DLPF_CFG=0/7 的时候,频率为 8Khz,其他情况是 1Khz。而且 DLPF 滤波频率一般设置 为采样率的一半。采样率,我们假定设置为 50Hz,那么 SMPLRT_DIV=1000/50-1=19。
3. 配置寄存器(0x1A)
数字低通滤波器( DLPF)的设置位,即: DLPF_CFG[2:0],加速 度计和陀螺仪,都是根据这三个位的配置进行过滤的。 DLPF_CFG 不同配置对应的过滤情 况如表:
这里的加速度传感器,输出速率( Fs)固定是 1Khz,而角速度传感器的输出速率( Fs), 则根据 DLPF_CFG 的配置有所不同。一般我们设置角速度传感器的带宽为其采样率的一半, 如前面所说的,如果设置采样率为 50Hz,那么带宽就应该设置为 25Hz,取近似值 20Hz, 就应该设置 DLPF_CFG=100。
4. 陀螺仪配置寄存器(0x1B)
FS_SEL[1:0]这两个位,用于设置陀螺仪的满量程范围: 0,±250° /S; 1,±500° /S; 2,±1000° /S; 3,±2000° /S;我们一般设置为 3,即±2000° /S,因 为陀螺仪的 ADC 为 16 位分辨率,所以得到灵敏度为: 65536/4000=16.4LSB/(° /S)。
5. 加速度传感器配置寄存器(0x1C)
AFS_SEL[1:0]这两个位,用于设置加速度传感器的满量程范围: 0, ±2g; 1,±4g; 2,±8g; 3,±16g;我们一般设置为 0,即±2g,因为加速度传感器的 ADC 也是 16 位,所以得到灵敏度为: 65536/4=16384LSB/g。
6. FIFO使能寄存器(0x23)
该寄存器用于控制 FIFO 使能,在简单读取传感器数据的时候,可以不用 FIFO,设置 对应位为 0 即可禁止 FIFO,设置为 1,则使能 FIFO。
加速度传感器的 3 个轴,全由 1 个位( ACCEL_FIFO_EN)控制,只要该位置 1,则加速度传感器的三个通道都开启 FIFO。
7. 加速度传感器数据输出寄存器(0x3B ~0x40)
通过读取这6个寄存器,就可以读到加速度传感器 x/y/z 轴的值,比如读 x 轴的数据,可以通过读取 0X3B(高 8 位)和0X3C(低8位)寄存器得到,其他轴以此类推,可以得到三个 int16 类型的数据。
8. 陀螺仪数据输出寄存器(0x43 ~ 0x48)
通过读取这6个寄存器,就可以读到陀螺仪 x/y/z 轴的值,比如 x 轴的数据,可以通过读取 0X43(高 8 位)和 0X44(低 8 位)寄存器得到,其他轴以此类推,可以得到三个 int16 类型的数据。
9. 温度传感器数据输出寄存器(0x41 0x42)
温度传感器的值,可以通过读取 0X41(高 8 位)和 0X42(低 8 位)寄存器得到, 温度换算公式为:
Temperature = 36.53 + regval/340
其中, Temperature 为计算得到的温度值,单位为℃, regval 为从 0X41 和 0X42 读到的温度传感器值
示例代码
#include <stdio.h>
#include "driver/i2c.h"
#include "esp_err.h"
#include "esp_log.h"
#define I2C_MASTER_NUM I2C_NUM_0
#define I2C_MASTER_FREQ_HZ 400000
#define I2C_MASTER_SCL_IO 5
#define I2C_MASTER_SDA_IO 4
#define MPU_ADDR 0x68 // MPU6050 设备地址
#define MPU_CMD_WHO_AM_I 0x75 // MPU6050 设备确认寄存器
#define MPU_CMD_PWR_MGMT_1 0x6B // 电源管理寄存器
#define MPU_CMD_SMPLRT_DIV 0x19 // 陀螺仪采样率分频器寄存器
#define MPU_CMD_CONFIG 0x1A // 数字低通滤波器配置寄存器
#define MPU_CMD_GYRO_CONFIG 0x1B // 陀螺仪配置寄存器
#define MPU_CMD_ACCEL_CONFIG 0x1C // 加速度传感器配置寄存器
#define MPU_CMD_ACCEL_XOUT_H 0x3B // 加速计X轴高字节数据寄存器
static const char *TAG = "MPU6050";
/*** @brief 初始化 I2C 总线*/
void i2c_init()
{// 配置 IIC 总线参数i2c_config_t conf = {.mode = I2C_MODE_MASTER, // 主机方式启动IIC.sda_io_num = I2C_MASTER_SDA_IO, // 设置数据引脚编号.sda_pullup_en = GPIO_PULLUP_ENABLE, // 使能 SDA 上拉电阻.scl_io_num = I2C_MASTER_SCL_IO, // 设置始终引脚编号.scl_pullup_en = GPIO_PULLUP_ENABLE, // 使能 SCL 上拉电阻.master.clk_speed = I2C_MASTER_FREQ_HZ // 设置主频};// 初始化 IIC 总线ESP_ERROR_CHECK(i2c_param_config(I2C_MASTER_NUM, &conf));// 安装 IIC 设备,因为 slv_rx_buf_len 和 slv_tx_buf_len 中主机模式下会忽略,所以不配置,中断标志位也不做分配ESP_ERROR_CHECK(i2c_driver_install(I2C_MASTER_NUM, I2C_MODE_MASTER, 0, 0, 0));ESP_LOGI(TAG, "IIC 初始化完毕!");
}
/*** @brief 初始化 MPU6050*/
void MPU6050_init()
{uint8_t check;/* 创建设备检测命令链,查看 0x75 寄存器返回的数据* 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8* ┌─┬─────────────┬─┬─┬───────────────┬─┬─┬─────────────┬─┬─┬───────────────┬─┬─┐* │S│ DEV-ADDR │W│A│ WHO_AM_I │A│S│ DEV-ADDR │R│A│ RES DATA │N│P│* └─┴─────────────┴─┴─┴───────────────┴─┴─┴─────────────┴─┴─┴───────────────┴─┴─┘* 1 7 1 1 8 1 1 7 1 1 8 1 1*/i2c_cmd_handle_t cmd = i2c_cmd_link_create();i2c_master_start(cmd); // 加入开始信号i2c_master_write_byte(cmd, (MPU_ADDR << 1) | I2C_MASTER_WRITE, true); // 发送地址,以及写指令,命令之后需要带ACKi2c_master_write_byte(cmd, MPU_CMD_WHO_AM_I, true); // 发送 WHO_MI_I 寄存器地址 0x75i2c_master_start(cmd); // 加入开始信号i2c_master_write_byte(cmd, (MPU_ADDR << 1) | I2C_MASTER_READ, true); // 发送地址,以及读指令,命令之后需要带ACKi2c_master_read_byte(cmd, &check, I2C_MASTER_LAST_NACK); // 读取数据i2c_master_stop(cmd); // 加入停止信号i2c_master_cmd_begin(I2C_MASTER_NUM, cmd, pdMS_TO_TICKS(1000)); // 开始发送数据i2c_cmd_link_delete(cmd); // 删除指令if (check != 0x68){ESP_LOGE(TAG, "MPU6050 不在线!( %02X )", check);return;}ESP_LOGI(TAG, "MPU6050 检测到在线,开始初始化...");/* 创建电源管理控制命令链,写入数据* 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8* ┌─┬─────────────┬─┬─┬───────────────┬─┬───────────────┬─┬─┐* │S│ DEV-ADDR │W│A│ PWR_MGMT_1 │A│ SEND DATA │A│P│* └─┴─────────────┴─┴─┴───────────────┴─┴───────────────┴─┴─┘* 1 7 1 1 8 1 8 1 1*/cmd = i2c_cmd_link_create();i2c_master_start(cmd); // 加入开始信号i2c_master_write_byte(cmd, (MPU_ADDR << 1) | I2C_MASTER_WRITE, true); // 以写入方式发送地址i2c_master_write_byte(cmd, MPU_CMD_PWR_MGMT_1, true); // 写入电源管理和复位控制i2c_master_write_byte(cmd, 0x00, true); // 写入寄存器数据i2c_master_stop(cmd); // 加入停止信号i2c_master_cmd_begin(I2C_MASTER_NUM, cmd, pdMS_TO_TICKS(1000)); // 开始发送数据i2c_cmd_link_delete(cmd);// 初始化默认参数(设置陀螺仪采样率分频器)cmd = i2c_cmd_link_create();i2c_master_start(cmd); // 加入开始信号i2c_master_write_byte(cmd, (MPU_ADDR << 1) | I2C_MASTER_WRITE, true); // 以写入方式发送地址i2c_master_write_byte(cmd, MPU_CMD_SMPLRT_DIV, true); // 写入寄存器地址i2c_master_write_byte(cmd, 0x07, true); // 写入寄存器数据 Sample rate = 1kHz/(7+1) = 125Hzi2c_master_stop(cmd); // 加入停止信号i2c_master_cmd_begin(I2C_MASTER_NUM, cmd, pdMS_TO_TICKS(1000)); // 开始发送数据i2c_cmd_link_delete(cmd);// 初始化默认参数(数字低通滤波器配置)cmd = i2c_cmd_link_create();i2c_master_start(cmd); // 加入开始信号i2c_master_write_byte(cmd, (MPU_ADDR << 1) | I2C_MASTER_WRITE, true); // 以写入方式发送地址i2c_master_write_byte(cmd, MPU_CMD_CONFIG, true); // 写入寄存器地址i2c_master_write_byte(cmd, 0x00, true); // 写入寄存器数据 Gyroscope:260Hz 0ms,Accelerometer:256Hz 0.98ms 8Khzi2c_master_stop(cmd); // 加入停止信号i2c_master_cmd_begin(I2C_MASTER_NUM, cmd, pdMS_TO_TICKS(1000)); // 开始发送数据i2c_cmd_link_delete(cmd);// 初始化默认参数(陀螺仪配置)cmd = i2c_cmd_link_create();i2c_master_start(cmd); // 加入开始信号i2c_master_write_byte(cmd, (MPU_ADDR << 1) | I2C_MASTER_WRITE, true); // 以写入方式发送地址i2c_master_write_byte(cmd, MPU_CMD_GYRO_CONFIG, true); // 写入寄存器地址i2c_master_write_byte(cmd, 0x00, true); // 写入寄存器数据 Gyroscope: +/- 250 dpsi2c_master_stop(cmd); // 加入停止信号i2c_master_cmd_begin(I2C_MASTER_NUM, cmd, pdMS_TO_TICKS(1000)); // 开始发送数据i2c_cmd_link_delete(cmd);// 初始化默认参数(加速度传感器配置)cmd = i2c_cmd_link_create();i2c_master_start(cmd); // 加入开始信号i2c_master_write_byte(cmd, (MPU_ADDR << 1) | I2C_MASTER_WRITE, true); // 以写入方式发送地址i2c_master_write_byte(cmd, MPU_CMD_ACCEL_CONFIG, true); // 写入寄存器地址i2c_master_write_byte(cmd, 0x00, true); // 写入寄存器数据 Accelerometer: +/- 2gi2c_master_stop(cmd); // 加入停止信号i2c_master_cmd_begin(I2C_MASTER_NUM, cmd, pdMS_TO_TICKS(1000)); // 开始发送数据i2c_cmd_link_delete(cmd);ESP_LOGI(TAG, "MPU6050 初始化完毕!");
}
/*** @brief 获得 X 轴加速度* @return 返回 X 轴加速度*/
int16_t get_accel_x()
{union{uint8_t bytes[4];int16_t value;} data;i2c_cmd_handle_t cmd = i2c_cmd_link_create();i2c_master_start(cmd); // 加入开始信号i2c_master_write_byte(cmd, (MPU_ADDR << 1) | I2C_MASTER_WRITE, true); // 以写入方式发送地址i2c_master_write_byte(cmd, MPU_CMD_ACCEL_XOUT_H, true); // 写入寄存器地址,这个寄存器是加速度传感器 X轴 的高位地址i2c_master_start(cmd); // 加入开始信号i2c_master_write_byte(cmd, (MPU_ADDR << 1) | I2C_MASTER_READ, true); // 发送地址,以及读指令,命令之后需要带ACKi2c_master_read_byte(cmd, &data.bytes[1], I2C_MASTER_ACK); // 读取高位字节数据,放在后面i2c_master_read_byte(cmd, &data.bytes[0], I2C_MASTER_NACK); // 读取低位字节数据,放在前面i2c_master_stop(cmd); // 加入停止信号i2c_master_cmd_begin(I2C_MASTER_NUM, cmd, pdMS_TO_TICKS(1000)); // 开始发送数据i2c_cmd_link_delete(cmd);return data.value;
}
void app_main(void)
{i2c_init(); // 初始化 IICMPU6050_init(); // 初始化 MPU6050ESP_LOGI(TAG, "准备采集 X 轴加速度数据:");vTaskDelay(100); // 等待一秒钟开始获取 X 轴加速度while (1){printf("%d\n", get_accel_x());vTaskDelay(pdMS_TO_TICKS(100));}vTaskDelete(NULL);
}
代码共享位置:http://192.168.172.17:3000/Mars.CN/ESP-IDF-S2-IIC.git