ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

ESP-IDF esp_hal_i2c 实践:仅用 HAL 函数编写裸机 I2C 主机驱动(hal_i2c 示例全解析)

ESP-IDF esp_hal_i2c 实践:仅用 HAL 函数编写裸机 I2C 主机驱动(hal_i2c 示例全解析) ESP-IDF esp_hal_i2c 实践仅用 HAL 函数编写裸机 I2C 主机驱动hal_i2c 示例全解析【免费下载链接】esp-idfEspressif IoT Development Framework. Official development framework for Espressif SoCs.项目地址: https://gitcode.com/GitHub_Trending/es/esp-idf本文以 ESP-IDF 的 hal_i2c 测试工程 为主体讲解如何不依赖 RTOS、中断与动态内存分配只使用esp_hal_i2c的 HAL 函数与底层 LL 寄存器接口从零写一个最简 I2C 主机Master驱动。读完后你能掌握该工程的接线与配置方法、构建烧录步骤以及 hal_i2c.c 中初始化、命令 FIFO 编排、轮询等待等核心实现原理从而能够在 bootloader 等裸机场景或第三方系统中复用硬件 I2C 外设。支持的目标芯片ESP32ESP32-C2ESP32-C3ESP32-C5ESP32-C6ESP32-C61ESP32-H2ESP32-H21ESP32-H4ESP32-P4ESP32-S2ESP32-S3ESP32-S311. 这个示例解决什么问题hal_i2c 工程 README 明确了两点定位这是一个纯 HAL I2C 测试它不能与esp_driver_i2c中的新版 I2C 驱动同时工作它演示了仅依赖I2C、GPIO 和 XTAL三种硬件特性的最简 I2C 主机驱动写法因此可以在无中断、无内存分配的裸机环境bare metal中运行。它面向两类典型场景为裸机应用编写 I2C 驱动例如 bootloader 阶段访问 I2C 器件在任意第三方系统或应用中移植 ESP 硬件 I2C 组件即直接使用 IDF HAL 函数而非整套 ESP-IDF 驱动框架。README 同时说明了一个边界受限于示例可用的源码量本工程聚焦如何使用 HAL 函数而不是提供一套完美 API实际应用中你可以根据自己的需要自行组织更好的接口。与 esp_hal_i2c 组件的关系esp_hal_i2c 组件 README 指出HAL 层分两个子层HAL上层定义操作外设所需的步骤与数据初始化、start/stop 等Low-Level底层是soc组件寄存器文件之上的翻译层把寄存器配置封装成通用概念。组件还提供esp_err_t风格的 HAL APIi2c_hal.c/i2c_hal_iram.c可用于辅助实现自己的驱动——这正是本工程hal_i2c组件所做的示范。注意官方提示该 HAL 组件仍处于积极开发中接口不保证跨版本稳定与向后兼容。2. 工程结构工程位于 components/esp_hal_i2c/test_apps/hal_i2c/目录组织如下components/esp_hal_i2c/test_apps/hal_i2c/ ├── CMakeLists.txt # 项目级 CMake含 G1 组件依赖检查 ├── main/ │ ├── CMakeLists.txt # 注册 hal_i2c_main.c依赖 hal_i2c / esp_hal_i2c │ ├── Kconfig.projbuild # SCL/SDA 引脚与频率的 menuconfig 配置 │ └── hal_i2c_main.c # app_main初始化 写 写读回验 └── components/hal_i2c/ ├── CMakeLists.txt # 依赖 esp_hal_i2c / esp_hal_clock ├── hal_i2c.h # hal_i2c_config 与 3 个 API 声明 └── hal_i2c.c # 纯 HAL/LL 实现的 I2C 主机驱动其中 main/CMakeLists.txt 显示主程序依赖hal_i2c esp_hal_i2ccomponents/hal_i2c/CMakeLists.txt 显示驱动组件依赖esp_hal_i2c esp_hal_clock时钟频率查询。项目顶层 CMakeLists.txt 还启用了 G1 组件依赖检查check_dependencies.py提示当前工程遵循严格的组件分层依赖规则。3. 硬件准备与引脚配置3.1 硬件要求与接线运行本工程需要一块 ESP32 / ESP32-S / ESP32-C / ESP32-P / ESP32-H 系列开发板I2C 总线上接一个从设备示例按 EEPROM 处理。条件允许时建议用逻辑分析仪或示波器观察波形。README 给出的默认引脚分配SDASCLESP I2C MasterI2C_MASTER_SDAI2C_MASTER_SCLDEVICE1SDASCLDEVICEnSDASCL这些默认值可在menuconfig中修改对应配置项定义在 main/Kconfig.projbuild配置项含义默认值I2C_MASTER_SCLI2C Master 时钟线 GPIO 号4I2C_MASTER_SDAI2C Master 数据线 GPIO 号5I2C_MASTER_FREQUENCY主机 SCL 频率Hz400000Fast Modehal_i2c_main.c 通过CONFIG_I2C_MASTER_SCL/CONFIG_I2C_MASTER_SDA/CONFIG_I2C_MASTER_FREQUENCY这三个宏读取上面的配置。关于上拉电阻存在两处说法使用时请注意区分工程 README 称无需外部上拉因为驱动会启用内部上拉电阻而 hal_i2c.c 的源码注释指出SDA/SCL 必须配置为开漏且线上必须有上拉内部上拉非常弱强烈建议使用外部上拉电阻。因此对信号完整性要求高的实际项目按源码注释加上外部上拉更稳妥。4. 构建、烧录与预期输出4.1 构建与烧录idf.py -p PORT flash monitor其中PORT替换为实际串口设备。退出串口监视器输入Ctrl-]。完整的 ESP-IDF 配置与构建流程可参考官方 Getting Started GuideREADME 中的跳转链接此处不展开。4.2 预期输出I (300) hal_i2c_main: HAL I2C initialized successfully I (320) hal_i2c_main: HAL I2C write-read successfully4.3 主程序做了什么hal_i2c_main.c 的app_main()流程#define SCL_IO_PIN CONFIG_I2C_MASTER_SCL #define SDA_IO_PIN CONFIG_I2C_MASTER_SDA #define MASTER_FREQUENCY CONFIG_I2C_MASTER_FREQUENCY #define TIMEOUT_MS (20) static const uint8_t slave_address 0x50; // 7 位从机地址示例按 EEPROM void app_main(void) { hal_i2c_config i2c_config { .freq MASTER_FREQUENCY, .i2c_port I2C_NUM_0, .scl_pin SCL_IO_PIN, .sda_pin SDA_IO_PIN, }; ESP_ERROR_CHECK(hal_i2c_init(i2c_config)); // 以 EEPROM 为从设备向地址 0x0000 写入 0x33 和 0x44然后读回 uint8_t tx_data[4] {0x00, 0x00, 0x33, 0x44}; ESP_ERROR_CHECK(hal_i2c_write(I2C_NUM_0, slave_address, tx_data, 4, TIMEOUT_MS)); uint8_t rx_data[2]; ESP_ERROR_CHECK(hal_i2c_write_read(I2C_NUM_0, slave_address, tx_data, 2, rx_data, 2, TIMEOUT_MS)); ESP_LOGI(TAG, Read back value 1 is %x, value two is %x\n, rx_data[0], rx_data[1]); }即初始化 I2C0 → 写操作2 字节器件地址 2 字节数据→ 写读操作重复 START 后从 0x0000 读回 2 字节验证全部走阻塞轮询timeout_ms固定为 20 ms。5. 核心 API 与驱动实现剖析5.1 hal_i2c_config 与四个 APIhal_i2c.h 定义了配置结构体与 3 个操作函数typedef struct { int scl_pin; /*! SCL PIN, -1 means not change the current pin */ int sda_pin; /*! SDA PIN, -1 means not change the current pin */ uint32_t freq; /*! SCL frequency */ i2c_port_t i2c_port; /*! i2c port */ } hal_i2c_config; esp_err_t hal_i2c_init(hal_i2c_config *cfg); esp_err_t hal_i2c_write(i2c_port_t port_num, uint16_t addr, const uint8_t *txdata, uint32_t txlength, uint32_t timeout_ms); esp_err_t hal_i2c_read(i2c_port_t port_num, uint16_t addr, uint8_t *rxdata, uint32_t rxlength, uint32_t timeout_ms); esp_err_t hal_i2c_write_read(i2c_port_t port_num, uint16_t addr, const uint8_t *txdata, uint32_t txlength, uint8_t *rxdata, uint32_t rxlength, uint32_t timeout_ms);引脚传-1表示保持当前引脚不变返回值统一为ESP_OK/ESP_ERR_INVALID_STATE端口未初始化或不存在/ESP_ERR_INVALID_ARG/ESP_ERR_TIMEOUT单次命令超时。驱动内部维护一个静态上下文数组覆盖目标芯片上的全部 I2C 端口SOC_I2C_NUM 1时含 I2C1见 hal_i2c.c。5.2 hal_i2c_init时钟、GPIO 与寄存器配置hal_i2c_init() 完成四件事时钟使能与寄存器复位PERIPH_RCC_ATOMIC()保护下执行i2c_ll_enable_bus_clock()与i2c_ll_reset_register()再i2c_ll_enable_controller_clock()开启控制器时钟GPIO 引脚配置SDA 与 SCL 对称处理见 L83-L104gpio_ll_set_level(…, 1)先拉高因为 I2C 在 1→0 跳变时有效gpio_ll_od_enable()置为开漏输出I2C 硬性要求gpio_ll_pullup_en()gpio_ll_pulldown_dis()使能内部上拉、禁止下拉使用外部上拉时此项可忽略esp_rom_gpio_connect_out_signal()/esp_rom_gpio_connect_in_signal()把 GPIO 引脚与i2c_periph_signal[port].sda_out_sig / sda_in_sigSCL 同理互连。注释特别说明作为主机时SDA 和 SCL都需要out/in 双向信号连接控制器模式配置i2c_ll_set_mode(dev, I2C_BUS_MODE_MASTER)设为主机模式、i2c_ll_enable_pins_open_drain()开漏、i2c_ll_set_data_mode()收发均 MSB firstI2C 标准要求、复位 TX/RX FIFO并且i2c_ll_disable_intr_mask()关闭全部中断——这是无中断裸机驱动的关键总线时钟计算注释明确init clock, always use xtal in hal driver即始终使用XTAL作为 I2C 时钟源i2c_ll_set_source_clk(dev, SOC_MOD_CLK_XTAL)。XTAL 频率的获取做了芯片差异兼容支持时钟树SOC_CLK_TREE_SUPPORTED的芯片用clk_hal_xtal_get_freq_mhz()否则退回clk_ll_xtal_get_freq_mhz()。随后i2c_ll_master_cal_bus_clk(xtal_freq * MHZ, freq, clk_cal)根据 XTAL 频率与目标freq计算分频参数i2c_ll_master_set_bus_timing()写入时序寄存器最后i2c_ll_update(dev)生效。这解释了 README 所说的仅依赖 I2C、GPIO 和 XTAL初始化路径不触碰系统时钟切换、电源管理或 RTOS 服务。5.3 命令 FIFO 编排WRITE / READ / STOP / ENDESP32 系列 I2C 控制器是基于命令列表的硬件状态机软件把若干命令单元RESTART / WRITE / READ / STOP / END写入命令寄存器 FIFO控制器依次执行。i2c_format_cmd() 组装每条命令的五个字段字段含义op_code操作码各目标芯片的具体宏定义见{target}/hal/i2c_ll.h中的I2C_LL_CMD_RESTART/WRITE/READ/STOP/ENDack_valREAD 时控制器发出的 ACK 位RSTART/STOP/END/WRITE 时忽略ack_expWRITE 时期望从机发出的 ACK 位ack_en是否校验从机 ACKbyte_num该命令传输的字节数以 hal_i2c_write() 为例一次写入的命令序列是若总线忙i2c_ll_is_bus_busy()先i2c_ll_master_fsm_rst()复位状态机并复位 TX/RX FIFOI2C_LL_CMD_RESTART发出 START地址字节(addr 0xFF) 1 | 0读/写位为 0写入 TX FIFO跟一条I2C_LL_CMD_WRITEbyte_num1数据分段循环每段最多I2C_LL_GET(FIFO_LEN) - 1字节为命令槽留出 FIFO 空间数据写入 TX FIFO 后跟一条WRITE命令再跟一条END命令——表示先执行已入队的命令后续命令等软件追加然后i2c_ll_update()i2c_ll_start_trans()启动传输并i2c_wait_done()等待本批命令完成全部数据发完后追加I2C_LL_CMD_STOP启动并等待完成返回ESP_OK。hal_i2c_write_read() 在此基础上演示了重复 STARTRepeated START先完成写阶段地址写位 数据STOP 前用 END 收尾然后重新入队RESTART 地址字节读位置 1 WRITE进入读阶段。读阶段按 FIFO 容量分段并处理了 I2C 协议的 ACK/NACK 细节最后一段只有 1 字节时直接READ(ack_valNACK, 1 字节)STOP即对最后一字节回 NACK最后一段大于 1 字节时拆成READ(ack_valACK, n-1 字节)READ(ack_valNACK, 1 字节)STOP非最后一段则READ(ack_valACK, n 字节)END读完本段后从 RX FIFO 取数并继续下一批。独立的 hal_i2c_read() 逻辑与上述读阶段相同只是起始命令为 START 读地址。5.4 无中断的超时等待机制裸机轮询的核心是 i2c_wait_done()while (i2c_ll_master_is_cmd_done(dev, cmd_idx) 0) { RECORD_TIME_ELAPSED(wait_time); if (time_get_us_by_ccount(wait_time) timeout_us) { return ESP_ERR_TIMEOUT; } }它用esp_cpu_get_cycle_count()读取 CPU 计数器把经过的 cycle 数按CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ换算成微秒time_get_us_by_ccount()超过timeout_ms主程序传 20 ms即返回ESP_ERR_TIMEOUT。整个驱动不注册任何中断、不分配任何内存全部是static/全局上下文加调用者提供的缓冲这正是它能跑在 bootloader 等裸机环境的原因。5.5 与 esp_driver_i2c 的互斥检查hal_i2c.c 末尾 用__attribute__((constructor))注册了一个启动时检查函数extern __attribute__((weak)) esp_err_t i2c_acquire_bus_handle(int port_num, void *i2c_new_bus, int mode); if ((void *)i2c_acquire_bus_handle ! NULL) { abort(); }新版 I2C 驱动esp_driver_i2c中i2c_acquire_bus_handle有实际实现若其被链接进固件则该符号非空程序直接abort()。这与 README 中本测试是纯 HAL I2C 测试不能与esp_driver_i2c驱动配合工作的声明一一对应——两套驱动会争抢同一硬件外设工程选择用链接期符号探测在启动时快速失败。6. 局限与使用建议不保证 API 稳定esp_hal_i2c组件尚在积极开发本示例的hal_i2c_init/write/read/write_read只是演示性封装跨 IDF 版本使用时需以当前仓库源码为准仅主机模式驱动固定I2C_BUS_MODE_MASTER未实现从机功能未启用仲裁检测i2c_ll_enable_arbitration(dev, false)写 ACK 未强制校验示例中 WRITE 命令的ack_en传的是NOT_CHECK_ACK_VALUE对从机无应答NACK的总线错误不会显式报错实际项目中可参考 LL 层的ack_exp/ack_en字段自行加强校验时钟源固定 XTAL总线频率计算基于 XTAL 实测值与 README 所述依赖 I2C、GPIO、XTAL一致在 XTAL 配置特殊的平台上如部分 26 MHz 晶振变体需注意CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ与 XTAL 频率宏的正确性上拉电阻README 与源码注释对内部上拉的可用性表述有出入见第 3.1 节高可靠性设计建议加外部上拉适用边界该驱动适合 bootloader 启动流程、极简固件、或需要把 ESP I2C 移植进第三方 RTOS/裸机框架的场景在完整 ESP-IDF 应用中常规功能开发仍应优先使用 esp_driver_i2c 提供的i2c_master_*接口本示例的价值在于展示其底层命令 FIFO 轮询的硬件工作方式。7. 参考资料仓库内路径示例说明components/esp_hal_i2c/test_apps/hal_i2c/README.mdHAL I2C 组件说明components/esp_hal_i2c/README.md驱动实现components/esp_hal_i2c/test_apps/hal_i2c/components/hal_i2c/hal_i2c.c、hal_i2c.h主程序与配置main/hal_i2c_main.c、main/Kconfig.projbuild官方 I2C 驱动对比参考components/esp_driver_i2c/i2c_master.c【免费下载链接】esp-idfEspressif IoT Development Framework. Official development framework for Espressif SoCs.项目地址: https://gitcode.com/GitHub_Trending/es/esp-idf创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表