本文章 来自原创专栏《ESP32教学专栏 (基于ESP-IDF)》,讲解如何使用 ESP-IDF 构建 ESP32 程序,发布文章并会持续为已发布文章添加新内容! 每篇文章都经过了精打细磨!
↓↓↓通过下方对话框进入专栏目录页↓↓↓
CSDN 请求进入目录 _ O x
是否进入ESP32教学导航(基于ESP-IDF)?
确定
I2C是一种通过两条双向IO线:SDA(串行数据线)和SCL(串行时钟线)进行数据通信的一种通信协议
由于 ESP-IDF
对于I2C通信协议的封装较为完善,开发者写程序并不需要完全了解 I2C
通信时序,只需要理解I2C通信的数据帧即可。
Start | Slave Address | R/W | Ack | Slave Address | Ack | Slave Address | Ack | … | Ack | Stop |
---|---|---|---|---|---|---|---|---|---|---|
起始 | 从机地址 | 读/写 | 应答 | 8位数据 | 应答 | 8位数据 | 应答 | … | 应答 | 终止 |
loop
循环执行4、5。直至发送完所有数据接下来我们将离不开I2C数据帧的格式。
ESP-IDF 封装的 I2C
API 中体现了面向对象的思想。它将 I2C 发送的数据帧当作了一个装有数据的容器。具体使用逻辑如下:
I2C 命令
对象。接下来我将用例程来展示在 ESP-IDF 中 使用 I2C API。在此之前,我总结一下使用 I2C 的步骤。
注意:
步骤①和步骤②可以颠倒!
i2c_param_config()
我们需要传递一个 i2c_config_t
结构体指针,该结构体包含了配置i2c主机模式的参数。
int i2c_master_port = 0;
i2c_config_t conf = {
// 选择工作模式,I2C_MODE_MASTER 为本文介绍的主机模式
.mode = I2C_MODE_MASTER,
// 选择SDA管脚的GPIO编号
.sda_io_num = I2C_MASTER_SDA_IO,
// 允许上拉
.sda_pullup_en = GPIO_PULLUP_ENABLE,
// 选择SCL管脚的GPIO编号
.scl_io_num = I2C_MASTER_SCL_IO,
// 允许上拉
.scl_pullup_en = GPIO_PULLUP_ENABLE,
// 选择一个合适的时钟频率(比如100,000)
.master.clk_speed = I2C_MASTER_FREQ_HZ,
/* 选择时钟源头,你可以填入I2C_SCLK_SRC_FLAG_* 来选择合适的时钟源 */
.clk_flags = 0,//此句可以注释掉
};
对于.clk_flags
元素:
名称 | 值 | 含义 |
---|---|---|
I2C_SCLK_SRC_FLAG_FOR_NOMA |
0 | 仅根据所需频率进行自动选择时钟。(不支持特殊功能,如 APB 等) |
I2C_SCLK_SRC_FLAG_AWARE_DFS |
1 | 当 APB 时钟改变时,时钟的波特率不会改变。 |
I2C_SCLK_SRC_FLAG_LIGHT_SLEEP |
2 | 用于轻度睡眠模式 |
之后调用i2c_param_config()
配置 I2C:
i2c_param_config(I2C_NUM_0, &conf);
I2C_NUM_0
或I2C_NUM_1
)方法是调用函数 i2c_driver_install()
函数
i2c_driver_install()
简介:
1、参数(手机端可能得不到完全展示,手机端可以滑动来查看右边一列)
参数名 含义 类型 "i2c_num"
【I2C 端口编号】
写0
或1
,或者I2C_NUM_0
或I2C_NUM_1
)[i2c_port_t]
"mode"
总线工作模式
主机(I2C_MODE_MASTER
)还是从机(I2C_MODE_SLAVE
)[i2c_mode_t]
"slv_rx_buf_len"
主机模式下此参数无效,请填 0
从机接受缓冲区大小,主机不需要。[size_t]
"slv_tx_buf_len"
主机模式下此参数无效,请填 0
从机发送缓冲区大小,主机不需要。[size_t]
"intr_alloc_flags"
如要忽略中断,请填 0
中断分配标志,详见[ESP32终端分配标志宏定义][int]
例如将I2C_NUM_0
驱动配置为主机模式:
i2c_driver_install(0, I2C_MODE_MASTER, 0, 0, 0);
或者
i2c_driver_install(I2C_NUM_0, I2C_MODE_MASTER, 0, 0, 0);
主机模式下,后三个参数全为0
注意:
当使用i2c_driver_install()
建立 I2C 通信,一段时间后不再需要 I2C 通信时,可以通过调用i2c_driver_delete()
来移除驱动程序以释放分配的资源。
ESP-IDF 中的 I2C 通信数据帧是 i2c_cmd_handle_t
对象。你可以把它当作一个容器
。之后向其中添加各种子数据帧(例如起始信号,从机地址,读写位,数据,终止信号等)
首先看一个发送数据的短代码
//创建i2c_cmd_handle_t对象
i2c_cmd_handle_t cmd = i2c_cmd_link_create();
//添加各种子数据帧
i2c_master_start(cmd); //起始信号
i2c_master_write_byte(cmd, 0x78, true); //从机地址及读写位
i2c_master_write(cmd, bytes, datalen, true); //数据位(数组)
i2c_master_stop(cmd); //终止信号
//向I2C_NUM_0 发送这个数据帧,timeout设置为1000毫秒
i2c_master_cmd_begin(0, cmd, 1000 / portTICK_PERIOD_MS);
//删除i2c_cmd_handle_t对象,释放资源
i2c_cmd_link_delete(cmd);
解析:(部分解析在 上文代码注释 中)
i2c_cmd_handle_t
对象中添加一个起始信号i2c_cmd_handle_t
对象中添加一个从机地址0x78
,读写位为0
i2c_master_write_byte()
函数的作用是添加1个byte(8位)数据到cmd_link
中】i2c_master_write()
添加 datalen
个数据位(第三个参数),数据为是bytes
数组,类型为uint8_t[]
,长度要 ⩾ \geqslant ⩾ datalen
i2c_master_write()
函数的作用是添加多个byte数据到cmd_link
中,以数组形式传入,所以这里也可以替换为i2c_master_write_byte()
表示只需要添加一个数据位(8位数据),参考第 5 行】i2c_master_cmd_begin()
来触发 I2C 控制器执行命令链接。一旦开始执行,就不能再修改命令链接。函数
i2c_master_cmd_begin()
简介:
1、返回值
ESP_OK
成功ESP_ERR_INVALID_ARG
参数错误ESP_FAIL
发送错误,从机无应答ESP_ERR_INVALID_STATE
I2C 驱动为安装或非Master模式ESP_ERR_TIMEOUT
总线繁忙,发送超时(TimeOut)
2、参数(手机端可能得不到完全展示,手机端可以滑动来查看右边一列)
参数名 含义 类型 "i2c_num"
【I2C 端口编号】
写0
或1
或者I2C_NUM_0
或I2C_NUM_1
)[i2c_port_t ]
"cmd_handle"
欲执行的 i2c_cmd_handle_t
对象
(I2C command handler对象)[i2c_cmd_handle_t]
"ticks_to_wait"
:最大等待的 ticks
例如:1000 / portTICK_PERIOD_MS
)表示1000毫秒.[TickType_t]
在读取数据时,在上图的步骤 4 中,不是用 i2c_master_write...
,而是用 i2c_master_read_byte()
和/或 i2c_master_read()
填充命令链接。
这两个函数和i2c_master_write_byte()
以及i2c_master_write()
很类似。但是参数有所不同。
如i2c_master_read()
,它的第二个参数data
的含义变为用于接收数据的缓冲区地址(uint8_t数组
指针即可),第三个参数datalen
变为所需要接受数据的长度。第四个参数ack
为主机是否发送应答信号。发送则为I2C_MASTER_ACK
,若每个byte都非应答则为I2C_MASTER_NACK
。若只有最后一个字节(接收到数据大于datalen
之后)才非应答,则为I2C_MASTER_LAST_NACK
再如i2c_master_read_byte()
,第二个参数data
也变成了用于接受数据的缓冲区地址,类型为uint8_t
的变量指针即可。第三个参数ack
和上者一样。
其余操作和 ③ 一致。
为更好的体现 I2C API 的使用,本示例给出的并不是一个完整个ESP-IDF程序,而是一个涉及I2C初始化,主机发送数据等操作的部分程序。因此,这个代码示例程序中不包含"app_main()
"入口函数。
在以下代码示例中:
i2c_init()
是i2c初始化函数i2c_sendData
是i2c向指定7位从机地址发送多个uint8_t
数据的函数i2c_sendByte
是i2c向指定7位从机地址发送一个uint8_t
数据的函数#include
...//省略
#include
#include "driver/i2c.h"
...//省略
#include ...
/****************************************************/
/* 函数声明 声明在命名空间中是为了避免命名冲突*/
namespace MyExample_I2C {
esp_err_t i2c_init();
esp_err_t i2c_sendData(uint8_t slaveAddr, uint8_t *data, std::size_t dataLen, bool ack_en);
esp_err_t i2c_sendByte(uint8_t slaveAddr, uint8_t byte, bool ack_en);
}
/****************************************************/
esp_err_t MyExample_I2C::i2c_init(){
esp_err_t err;
/* 安装驱动程序,并检查是否成功(err == ESP_OK) */
err = i2c_driver_install(I2C_NUM_0, I2C_MODE_MASTER, 0, 0, 0);
if(err != ESP_OK){
return err;
}
/* 配置i2c参数结构体 */
i2c_config_t config;
memset(&config, 0, sizeof config);
config.mode = I2C_MODE_MASTER;
config.scl_io_num = 16;
config.sda_io_num = 18;
config.scl_pullup_en = true;
config.sda_pullup_en = true;
config.master.clk_speed = 100000;
config.clk_flags = 0;
/* 配置i2c参数,接收错误并返回 */
err = i2c_param_config(I2C_NUM_0, &config);
return err;
}
/****************************************************/
//i2c发送1位数据
esp_err_t MyExample_I2C::i2c_sendByte(uint8_t slaveAddr, uint8_t byte, bool ack_en) {
esp_err_t err;
i2c_cmd_handle_t cmd = i2c_cmd_link_create();
i2c_master_start(cmd);
i2c_master_write_byte(cmd, (slaveAddr << 1) | I2C_MASTER_WRITE, ack_en);
i2c_master_write_byte(cmd, byte, ack_en);
i2c_master_stop(cmd);
err = i2c_master_cmd_begin(I2C_NUM_0, cmd, 1000 / portTICK_PERIOD_MS);
i2c_cmd_link_delete(cmd);
return err;
}
/****************************************************/
//i2c发送多位数据
esp_err_t MyExample_I2C::i2c_sendData(uint8_t slaveAddr, uint8_t *data, std::size_t dataLen, bool ack_en) {
esp_err_t err;
i2c_cmd_handle_t cmd = i2c_cmd_link_create();
i2c_master_start(cmd);
i2c_master_write_byte(cmd, (slaveAddr << 1) | I2C_MASTER_WRITE, ack_en);
i2c_master_write(cmd, data, dataLen, ack_en);
i2c_master_stop(cmd);
err = i2c_master_cmd_begin(I2C_NUM_0, cmd, 1000 / portTICK_PERIOD_MS);
i2c_cmd_link_delete(cmd);
return err;
}
如本文 使用步骤——① 配置驱动程序所述,函数 i2c_param_config()
在初始化 I2C 端口的驱动程序配置时,也会将几个 I2C 通信参数设置为 I2C 总线协议规范 规定的默认值。 其他一些相关参数已在 I2C 控制器的寄存器中预先配置。
通过调用下表中提供的专用函数,可以将所有这些参数更改为用户自定义值。请注意,时序值是在 APB 时钟周期中定义。APB 的频率在 I2C_APB_CLK_FREQ 中指定。
要更改的参数 | 函数 |
---|---|
SCL 脉冲周期的高电平和低电平 | i2c_set_period() |
在产生 启动 信号期间使用的 SCL 和 SDA 信号时序 | i2c_set_start_timing() |
在产生 停止 信号期间使用的 SCL 和 SDA 信号时序 | i2c_set_stop_timing() |
从机采样以及主机切换时,SCL 和 SDA 信号之间的时序关系 | i2c_set_data_timing() |
I2C 超时 | i2c_set_timeout() |
优先发送/接收最高有效位 (LSB) 或最低有效位 (MSB),可在 i2c_trans_mode_t 定义的模式中选择 |
i2c_set_data_mode() |
上述每个函数都有一个 _get_
对应项来检查当前设置的值。例如,调用 i2c_get_timeout()
来检查 I2C 超时值。
要检查在驱动程序配置过程中设置的参数默认值,请参考文件 driver/i2c.c
并查找带有后缀 _DEFAULT
的定义。
通过函数 i2c_set_pin()
可以为 SDA 和 SCL 信号选择不同的管脚并改变上拉配置。如果要修改已经输入的值,请使用函数 i2c_param_config()
。
I2C 工作过程会产生多种中断,安装驱动程序时会安装默认中断处理程序。
当然,您可以通过调用函数 i2c_isr_register()
来注册自己的而不是默认的中断处理程序。无论何时,中断服务程序(ISR)都应保持简短!
在运行自己的中断处理程序时,可以参考 ESP32 技术参考手册 > I2C 控制器 (I2C) > 中断 [点击打开PDF链接],以获取有关 I2C 控制器触发的中断描述。
调用函数 i2c_isr_free()
删除中断处理程序。