ESP32——基于idf框架开发I2C设备
文章目录
- 一、编译烧写I2C闪灯示例
- 二、I2C通信协议原理介绍
- 2.1 硬件连接
- 2.2 传输数据类比
- 2.3 IIC传输数据的格式
- 2.3.1 写操作
- 2.3.2 读操作
- 2.3.3 I2C信号
- 2.3.4 协议细节
- 2.3.5 仲裁机制
- 1.起始条件(Start)阶段的仲裁
- 2.地址 / 数据传输阶段的仲裁
- 三、ESP32-C3 I2C控制器
- 四、板载I2C引脚连接分析说明
- 4.1 查看底板
- 4.2 查看模组
- 五、ESP-IDF I2C开发
- 5.1 ESP-IDF I2C框架分析
- 5.1.1 驱动程序的功能
- 5.1.2 使用驱动程序
- 5.1.3 配置驱动程序
- 5.1.4 安装驱动程序
- 5.1.5 主机模式下通信
- 1.主机写入数据
- 2.主机读取数据
- 3.指示写入或读取数据
- 5.1.4 从机模式下通信
- 5.1.5 中断处理
- 5.2 常用API函数
- 5.3 如何支持一个I2C设备
- 5.3.1 初始化i2c 结构体配置
- 5.3.2 安装i2c驱动
- 5.3.3 i2c主机发送数据
- 5.3.4 i2c主机接受数据
- 5.3.5 扩展
- 1.设备地址数据格式
- 2.使用I2C示例检测设备ID
- 六、使用I2C驱动OLED屏幕显示字符
- 6.1 OLED模块分析
- 6.2 初始化OLED模块显示
- 6.3 清理屏幕
- 6.4 使OLED显示字符
- 6.5 主函数调用显示字体
- 6.6 使用封装好的I2C函数
- 6.7 移植STM32 OLED驱动
一、编译烧写I2C闪灯示例
- 设置 idf.py 开发环境:
book@100ask:~/esp/ssd1306-oled$ get_idf
- 设置目标芯片型号:
book@100ask:~/esp/ssd1306-oled$ idf.py set-target esp32c3
- 编译命令:
book@100ask:~/esp/ssd1306-oled$ idf.py build
- 烧写:
book@100ask:~/esp/ssd1306-oled$ idf.py -p /dev/ttyUSB0 flash
- 监控oled输出
book@100ask:~/esp/ssd1306-oled$ idf.py -p /dev/ttyUSB0 monitor
二、I2C通信协议原理介绍
2.1 硬件连接
I2C在硬件上的接法如下所示,主控芯片引出两条线SCL,SDA线,在一条I2C总线上可以接很多I2C设备,我们还会放一个上拉电阻(只要有任意一个或多个设备输出了低电平,总线就处于低电平,只有所有设备都输出高电平,总线才处于高电平)。
2.2 传输数据类比
怎么通过I2C传输数据,我们需要把数据从主设备发送到从设备上去,也需要把数据从从设备传送到主设备上去,数据涉及到双向传输。
举个例子:
体育老师:可以把球发给学生,也可以把球从学生中接过来。
-
发球:
- 老师:开始了(start)
- 老师:A!我要发球给你!(地址/方向)
- 学生A:到!(回应)
- 老师把球发出去(传输)
- A收到球之后,应该告诉老师一声(回应)
- 老师:结束(停止)
-
接球:
- 老师:开始了(start)
- 老师:B!把球发给我!(地址/方向)
- 学生B:到!
- B把球发给老师(传输)
- 老师收到球之后,给B说一声,表示收到球了(回应)
- 老师:结束(停止)
我们就使用这个简单的例子,来解释一下IIC的传输协议:
- 老师说开始了,表示开始信号(start)
- 老师提醒某个学生要发球,表示发送地址和方向(address/read/write)
- 同学听到自己的名字了要回应(ACK)
- 老师发球/接球,表示数据的传输
- 收到球要回应:回应信号(ACK)
- 老师说结束,表示IIC传输结束§
2.3 IIC传输数据的格式
2.3.1 写操作
流程如下:
- 主芯片要发出一个start信号
- 然后发出一个设备地址(用来确定是往哪一个芯片写数据),方向(读/写,0表示写,1表示读)
- 从设备回应(用来确定这个设备是否存在),然后就可以传输数据
- 主设备发送一个字节数据给从设备,并等待回应
- 每传输一字节数据,接收方要有一个回应信号(确定数据是否接受完成),然后再传输下一个数据。
- 数据发送完之后,主芯片就会发送一个停止信号。
下图:白色背景表示"主→从",灰色背景表示"从→主"
2.3.2 读操作
流程如下:
- 主芯片要发出一个start信号
- 然后发出一个设备地址(用来确定是往哪一个芯片写数据),方向(读/写,0表示写,1表示读)
- 从设备回应(用来确定这个设备是否存在),然后就可以传输数据
- 从设备发送一个字节数据给主设备,并等待回应
- 每传输一字节数据,接收方要有一个回应信号(确定数据是否接受完成),然后再传输下一个数据。
- 数据发送完之后,主芯片就会发送一个停止信号。
下图:白色背景表示"主→从",灰色背景表示"从→主"
2.3.3 I2C信号
I2C协议中数据传输的单位是字节,也就是8位。但是要用到9个时钟:前面8个时钟用来传输8数据,第9个时钟用来传输回应信号。传输时,先传输最高位(MSB)。
- 开始信号(S):SCL为高电平时,SDA山高电平向低电平跳变,开始传送数据。
- 结束信号(P):SCL为高电平时,SDA由低电平向高电平跳变,结束传送数据。
- 响应信号(ACK):接收器在接收到8位数据后,在第9个时钟周期,拉低SDA
- SDA上传输的数据必须在SCL为高电平期间保持稳定,SDA上的数据只能在SCL为低电平期间变化
I2C协议信号如下:
2.3.4 协议细节
-
如何在SDA上实现双向传输?
主芯片通过一根SDA线既可以把数据发给从设备,也可以从SDA上读取数据,连接SDA线的引脚里面必然有两个引脚(发送引脚/接受引脚)。 -
主、从设备都可以通过SDA发送数据,肯定不能同时发送数据,怎么错开时间?
在9个时钟里:
前8个时钟由主设备发送数据的话,第9个时钟就由从设备发送数据;
前8个时钟由从设备发送数据的话,第9个时钟就由主设备发送数据。 -
双方设备中,某个设备发送数据时,另一方怎样才能不影响SDA上的数据?
设备的SDA中有一个三极管,使用开极/开漏电路(三极管是开极,CMOS管是开漏,作用一样),如下图:
真值表如下:
从真值表和电路图我们可以知道:
- 当某一个芯片不想影响SDA线时,那就不驱动这个三极管
- 想让SDA输出高电平,双方都不驱动三极管(SDA通过上拉电阻变为高电平)
- 想让SDA输出低电平,就驱动三极管
从下面的例子可以看看数据是怎么传的(实现双向传输)。
举例:主设备发送(8bit)给从设备
-
前8个clk,由主设备决定数据
- 从设备不要影响SDA,从设备不驱动三极管
- 主设备决定数据,主设备要发送1时不驱动三极管,要发送0时驱动三极管(拉低SDA)
-
第9个clk,由从设备决定数据
- 主设备不驱动三极管
- 从设备决定数据,要发出回应信号的话,就驱动三极管让SDA变为0
- 从这里也可以知道ACK信号是低电平
从上面的例子,就可以知道怎样在一条线上实现双向传输,这就是SDA上要使用上拉电阻的原因。
为何SCL也要使用上拉电阻?
在第9个时钟之后,如果有某一方需要更多的时间来处理数据,它可以一直驱动三极管把SCL拉低。
当SCL为低电平时候,大家都不应该使用IIC总线,只有当SCL从低电平变为高电平的时候,IIC总线才能被使用。
当它就绪后,就可以不再驱动三极管,这是上拉电阻把SCL变为高电平,其他设备就可以继续使用I2C总线了。
对于IIC协议它只能规定怎么传输数据,数据是什么含义由从设备决定。
2.3.5 仲裁机制
- 当多个设备同时驱动 SDA 线时,只要有一个设备输出低电平(0),总线就会被拉低。
- 只有所有设备都输出高电平(1)时,总线才会呈现高电平。
- 仲裁过程:主设备在发送数据的同时监听 SDA 线的实际状态,若发现实际状态与自己发送的状态不一致,则判定仲裁失败,主动退出总线控制。
1.起始条件(Start)阶段的仲裁
当多个主设备同时发送起始条件(SCL 高电平时 SDA 由高变低)时:
- 先完成 SDA 下降沿的主设备获得仲裁胜利;
- 后发送起始条件的主设备会检测到 SDA 线已被拉低,从而放弃仲裁。
2.地址 / 数据传输阶段的仲裁
主设备发送地址或数据时,会持续比较自己发送的电平与总线上实际的电平:
- 若一致:继续发送后续数据;
- 若不一致(如自己发送 1,但总线被其他设备拉低为 0):判定仲裁失败,停止发送并转为从设备模式。
三、ESP32-C3 I2C控制器
ESP32-C3 系列有一个I2C总线接口,根据用户的配置,总线接口可以用作I2C主机或从机模式。I2C接口支持:
- 标准模式(100Kbit/s)
- 快速模式(400Kbit/s)
- 速度最高可达800Kbit/s,但受制于SCL和SDA上拉强度
- 7位寻址模式和10位寻址模式
- 双寻址模式
- 7位广播地址
用户可以配置指令寄存器来控制I2C接口,从而实现更多灵活的应用。
四、板载I2C引脚连接分析说明
4.1 查看底板
可以看到我们引出了一路 I2C接口 用来接OLED模块,其中引脚信息分别对应如下:
4.2 查看模组
同时根据核心板原理图可以通过查找对比 搜索查找到该引脚最后连接到了CPU的那些引脚。
五、ESP-IDF I2C开发
5.1 ESP-IDF I2C框架分析
5.1.1 驱动程序的功能
I2C 驱动程序管理在 I2C 总线上设备的通信,该驱动程序具备以下功能:
- 在主机模式下读写字节
- 支持从机模式
- 读取并写入寄存器,然后由主机读取/写入
5.1.2 使用驱动程序
以下部分将指导完成 I2C 驱动程序配置和工作的基本步骤:
- 配置驱动程序 - 设置初始化参数(如主机模式或从机模式,SDA 和 SCL 使用的 GPIO 管脚,时钟速度等)
- 安装驱动程序- 激活一个 I2C 控制器的驱动,该控制器可为主机也可为从机
- 根据是为主机还是从机配置驱动程序,选择合适的项目
- 主机模式下通信 - 发起通信(主机模式)
- 从机模式下通信 - 响应主机消息(从机模式)
- 中断处理 - 配置 I2C 中断服务
- 用户自定义配置 - 调整默认的 I2C 通信参数(如时序、位序等)
- 错误处理 - 如何识别和处理驱动程序配置和通信错误
- 删除驱动程序- 在通信结束时释放 I2C 驱动程序所使用的资源
5.1.3 配置驱动程序
建立 I2C 通信第一步是配置驱动程序,这需要设置 i2c_config_t
结构中的几个参数:
- 设置 I2C 工作模式 —— 从
i2c_mode_t
中选择主机模式或从机模式 - 设置 通信管脚
- 指定 SDA 和 SCL 信号使用的 GPIO 管脚
- 是否启用 ESP32-C3 的内部上拉电阻
- (仅限主机模式)设置 I2C 时钟速度
- (仅限从机模式)设置以下内容:
- 是否应启用 10 位寻址模式
- 定义 从机地址
然后,初始化给定 I2C 端口的配置,请使用端口号和 i2c_config_t
作为函数调用参数来调用 i2c_param_config()
函数。
配置示例(主机):
// I2C总线端口号(0或1,取决于硬件连接)
int i2c_master_port = 0;// 定义I2C配置结构体并初始化
i2c_config_t conf = {.mode = I2C_MODE_MASTER, // 设置为I2C主机模式// SDA数据线配置.sda_io_num = I2C_MASTER_SDA_IO, // SDA引脚号(需根据实际硬件修改).sda_pullup_en = GPIO_PULLUP_ENABLE, // 启用SDA内部上拉电阻// SCL时钟线配置.scl_io_num = I2C_MASTER_SCL_IO, // SCL引脚号(需根据实际硬件修改).scl_pullup_en = GPIO_PULLUP_ENABLE, // 启用SCL内部上拉电阻// 主机时钟配置.master.clk_speed = I2C_MASTER_FREQ_HZ, // I2C时钟频率(Hz)// 常见值:100000(100kHz)、400000(400kHz)// 可选配置项:时钟源标志(默认使用APB时钟)// .clk_flags = 0, /*!< Optional, you can use I2C_SCLK_SRC_FLAG_* flags to choose i2c source clock here. */
};
配置示例(从机):
// I2C从机端口号(通常为0或1,取决于硬件连接)
int i2c_slave_port = I2C_SLAVE_NUM;// 定义I2C从机配置结构体并初始化
i2c_config_t conf_slave = {// SDA数据线配置.sda_io_num = I2C_SLAVE_SDA_IO, // SDA引脚号(需根据实际硬件修改).sda_pullup_en = GPIO_PULLUP_ENABLE, // 启用SDA内部上拉电阻// SCL时钟线配置.scl_io_num = I2C_SLAVE_SCL_IO, // SCL引脚号(需根据实际硬件修改).scl_pullup_en = GPIO_PULLUP_ENABLE, // 启用SCL内部上拉电阻// 工作模式配置.mode = I2C_MODE_SLAVE, // 设置为I2C从机模式(被动响应)// 从机地址配置.slave.addr_10bit_en = 0, // 禁用10位地址模式(0=7位地址,1=10位地址).slave.slave_addr = ESP_SLAVE_ADDR, // 从机7位地址(0x01-0x77之间的唯一值)
};
在此阶段,i2c_param_config()
还将其他 I2C 配置参数设置为 I2C 总线协议规范中定义的默认值。有关默认值及修改默认值的详细信息,请参考 用户自定义配置。
5.1.4 安装驱动程序
配置好 I2C 驱动程序后,使用以下参数调用函数 i2c_driver_install()
安装驱动程序:
- 端口号,从
i2c_port_t
中二选一 - 主机或从机模式,从
i2c_mode_t
中选择 - (仅限从机模式)分配用于在从机模式下发送和接收数据的缓存区大小。I2C 是一个以主机为中心的总线,数据只能根据主机的请求从从机传输到主机。因此,从机通常有一个发送缓存区,供从应用程序写入数据使用。数据保留在发送缓存区中,由主机自行读取。
- 用于分配中断的标志(请参考 ESP_INTR_FLAG_* values in esp_hw_support/include/esp_intr_alloc.h)
5.1.5 主机模式下通信
安装 I2C 驱动程序后, ESP32 即可与其他 I2C 设备通信。
ESP32 的 I2C 控制器在主机模式下负责与 I2C 从机设备建立通信,并发送命令让从机响应,如进行测量并将结果发给主机。
为优化通信流程,驱动程序提供一个名为 “命令链接” 的容器,该容器应填充一系列命令,然后传递给 I2C 控制器执行。
1.主机写入数据
下面的示例展示如何为 I2C 主机构建命令链接,从而向从机发送 n 个字节。
下面介绍如何为 “主机写入数据” 设置命令链接及其内部内容:
- 使用
i2c_cmd_link_create()
创建一个命令链接。- 启动位 ——
i2c_master_start()
- 从机地址 ——
i2c_master_write_byte()
。提供单字节地址作为调用此函数的实参。 - 数据 —— 一个或多个字节的数据作为
i2c_master_write()
的实参。 - 停止位 ——
i2c_master_stop()
- 启动位 ——
- 通过调用
i2c_master_cmd_begin()
来触发 I2C 控制器执行命令链接。一旦开始执行,就不能再修改命令链接。 - 命令发送后,通过调用
i2c_cmd_link_delete()
释放命令链接使用的资源。
以下面函数为例:
2.主机读取数据
下面的示例展示如何为 I2C 主机构建命令链接,以便从从机读取 n 个字节。
在读取数据时,在上图的步骤 4 中,不是用 i2c_master_write
… ,而是用 i2c_master_read_byte()
和/或 i2c_master_read()
填充命令链接。同样,在步骤 5 中配置最后一次的读取,以便主机不提供 ACK 位。
以下面函数为例:
上面主机发送的停止信号为NACK。
3.指示写入或读取数据
发送从机地址后(请参考上图中第 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);
区别:第二个参数不一样,写数据为 I2C_MASTER_WRITE
,读取数据为 I2C_MASTER_READ
。
5.1.4 从机模式下通信
安装 I2C 驱动程序后, ESP32 即可与其他 I2C 设备通信。
API 为从机提供以下功能:
i2c_slave_read_buffer()
- 当主机将数据写入从机时,从机将自动将其存储在接收缓存区中。从机应用程序可自行调用函数
i2c_slave_read_buffer()
。如果接收缓存区中没有数据,此函数还具有一个参数用于指定阻塞时间。这将允许从机应用程序在指定的超时设定内等待数据到达缓存区。
- 当主机将数据写入从机时,从机将自动将其存储在接收缓存区中。从机应用程序可自行调用函数
i2c_slave_write_buffer()
- 发送缓存区是用于存储从机要以 FIFO 顺序发送给主机的所有数据。在主机请求接收前,这些数据一直存储在发送缓存区。函数
i2c_slave_write_buffer()
有一个参数,用于指定发送缓存区已满时的块时间。这将允许从机应用程序在指定的超时设定内等待发送缓存区中足够的可用空间。
- 发送缓存区是用于存储从机要以 FIFO 顺序发送给主机的所有数据。在主机请求接收前,这些数据一直存储在发送缓存区。函数
在 peripherals/i2c
中可找到介绍如何使用这些功能的代码示例。
5.1.5 中断处理
安装驱动程序时,默认情况下会安装中断处理程序。但是,您可以通过调用函数 i2c_isr_register()
来注册自己的而不是默认的中断处理程序。在运行自己的中断处理程序时,可以参考 ESP32 技术参考手册 > I2C 控制器 (I2C) > 中断 [PDF],以获取有关 I2C 控制器触发的中断描述。
调用函数 i2c_isr_free()
删除中断处理程序。
5.2 常用API函数
i2c_cmd_link_create :创建和初始化 I2C 命令链接
i2c_cmd_handle_t i2c_cmd_link_create(void);
i2c_cmd_link_delete :释放/删除 i2c命令链接
void i2c_cmd_link_delete(i2c_cmd_handle_t cmd_handle);
i2c_master_start :用于 I2C 主机生成启动信号的 队列命令
esp_err_t i2c_master_start(i2c_cmd_handle_t cmd_handle);
i2c_master_stop :用于I2C主机停止发送队列命令
esp_err_t i2c_master_stop(i2c_cmd_handle_t cmd_handle);
i2c_master_cmd_begin :用于开始i2c主机发送队列数据命令
/*** @brief I2C主机发送已排队的命令序列* 此函数将触发发送所有已排队的I2C命令。* * 任务将被阻塞,直到所有命令发送完成。I2C API不是线程安全的,若需在不同任务中使用同一I2C端口,* 需自行处理多线程同步问题。* * @note此函数仅适用于I2C主机模式** @param i2c_num I2C端口号* @param cmd_handle I2C命令链表句柄* @param ticks_to_wait 最大等待 ticks 数(超时时间)** @return* - ESP_OK 成功* - ESP_ERR_INVALID_ARG 参数错误* - ESP_FAIL 发送命令错误,从机未响应ACK* - ESP_ERR_INVALID_STATE I2C驱动未安装或未处于主机模式* - ESP_ERR_TIMEOUT 操作超时(总线繁忙)*/
esp_err_t i2c_master_cmd_begin(i2c_port_t i2c_num, i2c_cmd_handle_t cmd_handle, TickType_t ticks_to_wait);
i2c_master_write :I2C主机将缓冲区数据写入I2C总线内
/*** @brief 向I2C命令链表中添加主机写数据命令(将缓冲区数据写入I2C总线)* @note* 此函数仅适用于I2C主机模式需要调用i2c_master_cmd_begin()来实际发送所有已排队的命令** @param cmd_handle I2C命令链表句柄* @param data 待发送的数据缓冲区指针* @note* 若启用了PSRAM且中断标志设置为`ESP_INTR_FLAG_IRAM`,请确保使用内部RAM分配的内存* @param data_len 数据长度(字节数)* @param ack_en 主机是否启用ACK校验(true=检查从机应答,false=忽略应答)** @return* - ESP_OK 成功* - ESP_ERR_INVALID_ARG 参数错误*/
esp_err_t i2c_master_write(i2c_cmd_handle_t cmd_handle, const uint8_t *data,
size_t data_len, bool ack_en);
i2c_master_write_byte :I2C 主机向 I2C 总线写入一个字节数据
/*** @brief 向I2C命令链表中添加主机写单字节命令* @note* 此函数仅适用于I2C主机模式* 需要调用i2c_master_cmd_begin()来实际发送所有已排队的命令** @param cmd_handle I2C命令链表句柄(通过i2c_cmd_link_create()创建)* @param data 待发送的单字节数据* @param ack_en 是否启用ACK校验* - true: 发送后检查从机应答(标准模式)* - false: 忽略应答(用于特殊协议或测试)** @return* - ESP_OK 成功添加到命令队列* - ESP_ERR_INVALID_ARG 参数错误(如cmd_handle无效)*/
esp_err_t i2c_master_write_byte(i2c_cmd_handle_t cmd_handle, uint8_t data, bool ack_en);
i2c_driver_install :安装I2C设备驱动
/*** @brief 安装I2C驱动,初始化指定I2C控制器** @param i2c_num I2C端口号(ESP32支持I2C_NUM_0和I2C_NUM_1)* @param mode I2C工作模式:* - I2C_MODE_MASTER:主机模式(主动发起通信)* - I2C_MODE_SLAVE:从机模式(被动响应通信)* @param slv_rx_buf_len 从机模式接收缓冲区大小(字节)* @note* 仅从机模式使用此参数,主机模式下驱动会忽略此值* @param slv_tx_buf_len 从机模式发送缓冲区大小(字节)* @note* 仅从机模式使用此参数,主机模式下驱动会忽略此值* @param intr_alloc_flags 中断分配标志(一个或多个ESP_INTR_FLAG_*值的按位或)* @note* 主机模式下,若可能禁用缓存(如写入Flash)且从设备对时序敏感,* 建议使用`ESP_INTR_FLAG_IRAM`。此时,I2C读写函数需使用内部RAM分配的内存,* 因为缓存禁用时中断处理函数无法访问PSRAM(若启用)** @return* - ESP_OK 成功* - ESP_ERR_INVALID_ARG 参数错误(如端口号或模式无效)* - ESP_FAIL 驱动安装失败(如端口已被占用)*/
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_param_config :I2C设备参数配置初始化
/*** @brief 配置I2C控制器的参数** @param i2c_num I2C端口号(ESP32支持I2C_NUM_0和I2C_NUM_1)* @param i2c_conf 指向I2C配置结构体的指针,包含以下参数:* - mode:工作模式(I2C_MODE_MASTER或I2C_MODE_SLAVE)* - sda_io_num:SDA数据线对应的GPIO引脚号* - sda_pullup_en:是否启用SDA内部上拉电阻* - scl_io_num:SCL时钟线对应的GPIO引脚号* - scl_pullup_en:是否启用SCL内部上拉电阻* - master.clk_speed:主机模式时钟频率(Hz)* - slave.slave_addr:从机模式设备地址(7位)* - slave.addr_10bit_en:是否启用10位地址模式** @return* - ESP_OK 配置成功* - ESP_ERR_INVALID_ARG 参数错误(如端口号无效或配置结构体为空)*/
esp_err_t i2c_param_config(i2c_port_t i2c_num, const i2c_config_t *i2c_conf);
5.3 如何支持一个I2C设备
5.3.1 初始化i2c 结构体配置
如果使用I2C,必须要先初始化 i2c_param_config()
; 这个函数里面有两个参数,分别是我们使用的I2C设备端口号
,另一个是 I2c_config_t结构体
,里面包含了i2c设备的参数信息,下面单独说明说一下。
- .mode 指的是模式,I2C一共有两种模式
I2C_MODE_MASTER
主机模式和I2C_SLAVE_SDA_IO
从机模式。 - .sda_io_num SDA数据
引脚编号
。 - .sda_pullup_en
SDA拉高使能
。 - .scl_io_num SCL 时钟
引脚编号
。 - .scl_pullup_en
SCL拉高使能
。 - .master.clk_speed 主机时钟传输速率,
默认是100khz
可以根据模块要求进行配置。
int i2c_master_port = I2C_MASTER_NUM;i2c_config_t conf = {.mode = I2C_MODE_MASTER,.sda_io_num = I2C_MASTER_SDA_IO,.sda_pullup_en = GPIO_PULLUP_ENABLE,.scl_io_num = I2C_MASTER_SCL_IO,.scl_pullup_en = GPIO_PULLUP_ENABLE,.master.clk_speed = I2C_MASTER_FREQ_HZ,// .clk_flags = 0, /*!< Optional, you can use I2C_SCLK_SRC_FLAG_* flags to choose i2c source clock here. */
};
i2c_param_config(i2c_master_port, &conf);
5.3.2 安装i2c驱动
在配置了使用的I2C总线设备参数后,就可以使用 i2c_driver_install
来启用它。下面我们使用的是i2c 主机模式,如果您使用的是i2c 从机模式,则需要配置一下 buffer size
。
i2c_driver_install(i2c_master_port, conf.mode, I2C_MASTER_RX_BUF_DISABLE, I2C_MASTER_TX_BUF_DISABLE, 0);
5.3.3 i2c主机发送数据
* _________________________________________________________________
* | start | slave_addr + wr_bit + ack | write 1 byte + ack | stop |
* --------|---------------------------|---------------------|------|i2c_cmd_handle_t cmd = i2c_cmd_link_create();i2c_master_start(cmd);i2c_master_write_byte(cmd, BH1750_SENSOR_ADDR << 1 | WRITE_BIT, ACK_CHECK_EN);i2c_master_write_byte(cmd, BH1750_CMD_START, ACK_CHECK_EN);i2c_master_stop(cmd);i2c_master_cmd_begin(i2c_num, cmd, 1000 / portTICK_RATE_MS);i2c_cmd_link_delete(cmd);
5.3.4 i2c主机接受数据
*
______________________________________________________________________________________
* | start | slave_addr + rd_bit + ack | read 1 byte + ack | read 1 byte + nack | stop |
* --------|---------------------------|--------------------|--------------------|------|cmd = i2c_cmd_link_create();i2c_master_start(cmd);i2c_master_write_byte(cmd, BH1750_SENSOR_ADDR << 1 | READ_BIT,
ACK_CHECK_EN);i2c_master_read_byte(cmd, data_h, ACK_VAL);i2c_master_read_byte(cmd, data_l, NACK_VAL);i2c_master_stop(cmd);i2c_master_cmd_begin(i2c_num, cmd, 1000 / portTICK_RATE_MS);i2c_cmd_link_delete(cmd);
5.3.5 扩展
1.设备地址数据格式
从地址通常是7位地址位加上1位标志位,其中标志位是用来进行操作读或者写。对于程序而言,通常读操作标志位设置为 1 写操作时标志位设置0 ,他们会被定义为宏,比如写操作 WRITE_BIT 读操作为 READ_BIT 。如果我们想从地址0x38处的设备进行读取数据那么我们传输的I2C地址是 (0x38 << 1 | READ_BIT)如果我们想往设备地址为0x38写入数据,那么我们传输的I2C地址为 (0x38 << 1 | WRITE_BIT )
2.使用I2C示例检测设备ID
book@100ask:~/esp$ cp esp-idf/examples/peripherals/i2c/i2c_tools/ . -rf
book@100ask:~/esp/i2c_tools$ get_idf
book@100ask:~/esp/i2c_tools$ idf.py set-target esp32c3
book@100ask:~/esp/i2c_tools$ idf.py build
book@100ask:~/esp/i2c_tools$ idf.py -p /dev/ttyUSB0 flash
book@100ask:~/esp/i2c_tools$ idf.py -p /dev/ttyUSB0 monitor
在该虚拟终端下输入help:
我们只有一个I2C端口,所以默认为0;频率为100K~400K,我们使用100K即可;sda端口号为3;scl端口号为9:
i2c-tools> i2cconfig --port=0 --freq=100 --sda=3 --scl=9
输入 i2cdetect
查看i2c设备:
i2c-tools> i2cdetect
若将我们的i2c设备拔掉,则不会有i2c设备出现。
六、使用I2C驱动OLED屏幕显示字符
6.1 OLED模块分析
查看芯片特性
获取设备地址
前7位是设备地址,最后1位是读写位。假设主机是要写入数据,那么b0就为0。SA0引脚接低电平,所以默认为0。所以对应的数据就是0111 1000(0x78)。那么我们在写入该数据时,由于数据规定地址需要左移1位,所以我们会直接将地址定义成 #define OLED_I2C_ADDRESS 0x3C
,这样子左移1位后得到的就是0x78。
查看如何传输数据
这里在发送从机地址后,需要发送控制位和数据位,C0设置为0则表示后面跟的仅是一个数据位的数据;D/C需要根据实际设置,当我们设置为0时表示发送的是命令,设置为1时表示发送数据到显存上(即显示在屏幕)。则此时表示为0100 0000(0x40),
发送完红色框内的数据后,后面就是在屏幕上想要显示内容(文本)。
上面函数中,发送控制位后后面跟的不仅是数据,所以C0为1,发送的是命令所以D/C为0,那么就表示为1000 0000(0x80),后面发的0xB0是去设置页的起始地址已经模式,接下来发的0x40是去设置从哪一行开始清数据,最后的128表面要清理8页的数据(2^8)。
查看初始化操作流程
查看命令与寄存器地址说明
6.2 初始化OLED模块显示
void ssd1306_init() {esp_err_t espRc;i2c_cmd_handle_t cmd = i2c_cmd_link_create();i2c_master_start(cmd);i2c_master_write_byte(cmd, (OLED_I2C_ADDRESS << 1) | I2C_MASTER_WRITE, true);i2c_master_write_byte(cmd,0x00, true); //set lower column address --- 设置起始列地址的低四位,00h~0Fhi2c_master_write_byte(cmd,0x10, true); //set higher column address --- 设置起始列地址的高四位,10h~17hi2c_master_write_byte(cmd,0x40, true); //set display start line --- 设置起始行寄存器,04h~7Fhi2c_master_write_byte(cmd,0xB0, true); //set page address --- 设置页地址,B0h~B7hi2c_master_write_byte(cmd,0x81, true); //set contrast control --- 设置对比度i2c_master_write_byte(cmd,0x7F, true); //Set Display Start Line --- 设置显示起始线i2c_master_write_byte(cmd,0xA1, true); //set segment remap --- 设置SEG重映射,A0h/A1hi2c_master_write_byte(cmd,0xA6, true); //normal display --- 即“1”点亮像素点 A6h --- “1”点亮像素点 A7h --- “0”点亮像素点i2c_master_write_byte(cmd,0xA8, true); //set multiplex ratio --- 设置多路复用率i2c_master_write_byte(cmd, SSD1306_LCDHEIGHT - 1, true);i2c_master_write_byte(cmd,0x3F, true); //duty = 1/64,00h~3Fh i2c_master_write_byte(cmd,0xC8, true); //com scan direction --- Com口扫描方向,C0h/C8h i2c_master_write_byte(cmd,0xD3, true); //set display offset --- 设置显示抵消i2c_master_write_byte(cmd,0x00, true); //set display offset --- 设置显示抵消 i2c_master_write_byte(cmd,0xD5, true); //set osc division --- 设置时钟分配和振荡频率i2c_master_write_byte(cmd,0x80, true); //set osc division --- 设置时钟分配和振荡频率i2c_master_write_byte(cmd,0xD9, true); //set pre-charge period --- 设置预充电周期i2c_master_write_byte(cmd,0x22, true); //Set the page start and end address of the target display location by command 22h --- 设置目标显示位置的页面开始和结束地址i2c_master_write_byte(cmd,0xDA, true); //set com pin configuration --- 设置COM口引脚配置i2c_master_write_byte(cmd,0x12, true); //set com pin configuration --- 设置COM口引脚配置i2c_master_write_byte(cmd,0xDB, true); //set vcomh --- 设置COM电压等级i2c_master_write_byte(cmd,0x20, true); //0x00 —— 0.65*Vcc 0x10 —— 0.71*Vcc 0x20 —— 0.77*Vcc 0x30 —— 0.83*Vcci2c_master_write_byte(cmd,0x8D, true); //set charge pump enable --- 设置电荷泵使能i2c_master_write_byte(cmd,0x14, true); //set charge pump enable --- 设置电荷泵使能i2c_master_write_byte(cmd,0xAF, true); //display on --- 打开显示i2c_master_stop(cmd);espRc = i2c_master_cmd_begin(I2C_NUM_0, cmd, 10/portTICK_PERIOD_MS);if (espRc == ESP_OK) {ESP_LOGI(tag, "OLED configured successfully");} else {ESP_LOGE(tag, "OLED configuration failed. code: 0x%.2X", espRc);}i2c_cmd_link_delete(cmd);
}
6.3 清理屏幕
void task_ssd1306_display_clear() {i2c_cmd_handle_t cmd;uint8_t zero[128] = {0};for (uint8_t i = 0; i < 8; i++) {cmd = i2c_cmd_link_create();i2c_master_start(cmd);i2c_master_write_byte(cmd, (OLED_I2C_ADDRESS << 1) | I2C_MASTER_WRITE, true);i2c_master_write_byte(cmd, 0x80, true); //CONTROL BYTE CMD SINGLE i2c_master_write_byte(cmd, 0xB0 | i, true); //Set Page Start Address for Page Addressing Modei2c_master_write_byte(cmd, 0x40, true); //Set Display Start Linei2c_master_write(cmd, zero, 128, true);i2c_master_stop(cmd);i2c_master_cmd_begin(I2C_NUM_0, cmd, 10/portTICK_PERIOD_MS);i2c_cmd_link_delete(cmd);}vTaskDelete(NULL);
}
6.4 使OLED显示字符
void task_ssd1306_display_text(void *arg_text) {char *text = (char*)arg_text;uint8_t text_len = strlen(text);i2c_cmd_handle_t cmd;uint8_t cur_page = 0;cmd = i2c_cmd_link_create();i2c_master_start(cmd);i2c_master_write_byte(cmd, (OLED_I2C_ADDRESS << 1) | I2C_MASTER_WRITE, true);i2c_master_write_byte(cmd, OLED_CONTROL_BYTE_CMD_STREAM, true);i2c_master_write_byte(cmd, 0x00, true); // Set Lower Column Start Address for Page Addressing Modei2c_master_write_byte(cmd, 0x10, true); //Set Higher Column Start Address for Page Addressing Modei2c_master_write_byte(cmd, 0xB0 | cur_page, true); // reset pagei2c_master_stop(cmd);i2c_master_cmd_begin(I2C_NUM_0, cmd, 10/portTICK_PERIOD_MS);i2c_cmd_link_delete(cmd);for (uint8_t i = 0; i < text_len; i++) {if (text[i] == '\n') {cmd = i2c_cmd_link_create();i2c_master_start(cmd);i2c_master_write_byte(cmd, (OLED_I2C_ADDRESS << 1) | I2C_MASTER_WRITE, true);i2c_master_write_byte(cmd, 0x00, true); //Control byte CMD STREAMi2c_master_write_byte(cmd, 0x00, true); //Set Lower Column Start Address for Page Addressing Modei2c_master_write_byte(cmd, 0x10, true);//Set Higher Column Start Address for Page Addressing Modei2c_master_write_byte(cmd, 0xB0 | ++cur_page, true); // Set Page Start Address for Page Addressing Modei2c_master_stop(cmd);i2c_master_cmd_begin(I2C_NUM_0, cmd, 10/portTICK_PERIOD_MS);i2c_cmd_link_delete(cmd);} else {cmd = i2c_cmd_link_create();i2c_master_start(cmd);i2c_master_write_byte(cmd, (OLED_I2C_ADDRESS << 1) | I2C_MASTER_WRITE, true);i2c_master_write_byte(cmd, 0x40, true); //CONTROL BYTE DATA STREAMi2c_master_write(cmd, font8x8_basic_tr[(uint8_t)text[i]], 8, true);i2c_master_stop(cmd);i2c_master_cmd_begin(I2C_NUM_0, cmd, 10/portTICK_PERIOD_MS);i2c_cmd_link_delete(cmd);}}vTaskDelete(NULL);
}
6.5 主函数调用显示字体
void app_main(void)
{i2c_master_init();ssd1306_init();xTaskCreate(&task_ssd1306_display_pattern, "ssd1306_display_pattern", 2048, NULL, 6, NULL);xTaskCreate(&task_ssd1306_display_clear, "ssd1306_display_clear", 2048, NULL, 6, NULL);vTaskDelay(100/portTICK_PERIOD_MS);xTaskCreate(&task_ssd1306_display_text, "ssd1306_display_text", 2048,(void *)"Hello world!\nLjunG 666!", 6, NULL);xTaskCreate(&task_ssd1306_contrast, "ssid1306_contrast", 2048, NULL, 6, NULL);xTaskCreate(&task_ssd1306_scroll, "ssid1306_scroll", 2048, NULL, 6, NULL);
}
6.6 使用封装好的I2C函数
在6.2的初始化函数中,每次都是需要使用start和一系列的发送命令,最后加一个stop完成发送,这样的操作过于繁琐,我们可以去查看是否有I2C已经封装好的函数,能够直接发送数据到对应地址,我们可以直接将发送的地址或者数据全部存到一个bufff中,调用下面函数即可:
i2c_master_write_to_device(i2c_port_t i2c_num, uint8_t device_address, const uint8_t* write_buffer, size_t write_sizeTickpyte_t tick_to_wait);
void ssd1306_init() {esp_err_t espRc;uint8_t buf[] = {0x00, //set lower column address --- 设置起始列地址的低四位,00h~0Fh0x10, //set higher column address --- 设置起始列地址的高四位,10h~17h0x40, //set display start line --- 设置起始行寄存器,04h~7Fh0xB0, //set page address --- 设置页地址,B0h~B7h0x81, //set contrast control --- 设置对比度0x7F, //Set Display Start Line --- 设置显示起始线0xA1, //set segment remap --- 设置SEG重映射,A0h/A1h0xA6, //normal display --- 即“1”点亮像素点 A6h --- “1”点亮像素点 A7h --- “0”点亮像素点0xA8, //set multiplex ratio --- 设置多路复用率SSD1306_LCDHEIGHT - 1,0x3F, //duty = 1/64,00h~3Fh 0xC8, //com scan direction --- Com口扫描方向,C0h/C8h 0xD3, //set display offset --- 设置显示抵消0x00, //set display offset --- 设置显示抵消 0xD5, //set osc division --- 设置时钟分配和振荡频率0x80, //set osc division --- 设置时钟分配和振荡频率0xD9, //set pre-charge period --- 设置预充电周期0x22, //Set the page start and end address of the target display location by command 22h --- 设置目标显示位置的页面开始和结束地址0xDA, //set com pin configuration --- 设置COM口引脚配置0x12, //set com pin configuration --- 设置COM口引脚配置0xDB, //set vcomh --- 设置COM电压等级0x20, //0x00 —— 0.65*Vcc 0x10 —— 0.71*Vcc 0x20 —— 0.77*Vcc 0x30 —— 0.83*Vcc0x8D, //set charge pump enable --- 设置电荷泵使能0x14, //set charge pump enable --- 设置电荷泵使能0xAF, //display on --- 打开显示};espRc = i2c_master_write_to_device(I2C_NUM_0, OLED_I2C_ADDRESS, buf, sizeof(buf), 1000);if (espRc == ESP_OK) {ESP_LOGI(tag, "OLED configured successfully");} else {ESP_LOGE(tag, "OLED configuration failed. code: 0x%.2X", espRc);}
}
6.7 移植STM32 OLED驱动
在共享文件夹中复制粘贴上面的代码包,并修改权限:
sudo chmod 777 ssd1306-oled-stm32-my -R
找到我们之前的STM32 OLED驱动代码,将driver_oled.c和driver_oled.h放入到main文件夹下:
修改文件权限:
原函数如下:
修改如下:
主函数中修改成我们重新写的函数:
这里的矩阵使用的是1288,我们修改成原来的驱动代码(12816):
修改CMake文件:
最后编译烧录到开发板,开发板显示:LjunG666。