ESP-IDF 存储控制器 HAL 层解析:esp_hal_sd 组件中的 SDMMC 主机与 SDIO 从机硬件抽象
【免费下载链接】esp-idfEspressif IoT Development Framework. Official development framework for Espressif SoCs.项目地址: https://gitcode.com/GitHub_Trending/es/esp-idf
esp_hal_sd是 ESP-IDF 中面向 SD/MMC 与 SDIO 外设的硬件抽象层(Hardware Abstraction Layer)组件,为SDMMC Host Controller(SD 主机控制器)与SDIO Slave Controller(SDIO 从机控制器)提供统一的操作封装。本文以 components/esp_hal_sd/README.md 为骨架,结合组件源码深入讲解其 HAL 分层设计、两类控制器的核心抽象模型与典型调用流程,帮助开发者在阅读 IDF 外设驱动或自研底层驱动时快速建立对该 HAL 的整体认知。
⚠️稳定性警告(原文档明确声明):该 HAL 组件当前仍处于重度开发阶段,官方不保证各版本间的接口稳定性与向后兼容性。普通应用开发应使用其上层的 IDF 外设驱动(如
sdmmc、sdio_slave驱动)而非直接调用 HAL 接口;只有希望自行实现驱动的高级开发者才建议直接使用本层函数。
组件定位:为谁服务,服务什么
esp_hal_sd组件的核心职责是为 ESP-IDF 支持的所有芯片目标(target)提供两种外设的硬件抽象:
- SDMMC Host Controller:负责与 SD 卡、eMMC、SDIO 设备(如 WiFi/蓝牙模块)通信的主机端控制器;
- SDIO Slave Controller:让 ESP 芯片自身以 SDIO 从机身份接入外部主机(如 Linux 主机、另一颗 MCU),实现主机对从机的寄存器访问、中断通知与批量数据传输。
在 ESP-IDF 的分层体系中,本组件处于外设驱动(Driver)之下、寄存器层(soc)之上的中间位置,其产出被esp_driver_sdmmc、sdmmc、sdio_slave等上层驱动消费。
HAL 双层架构:HAL(上层)与 Low-Level(底层)
原文档指出,广义上的 HAL 层由两个子层构成,这也是 ESP-IDF 近几代 HAL 重构后的通用组织方式:
- HAL(上层):定义操作外设所需的步骤与数据,例如初始化流程、参数设置、上下文(context)结构等。它面向"如何操作这个外设"这一高层语义。
- Low-Level(底层,
_ll后缀):位于soc组件寄存器文件之上的翻译层,只负责把"通用概念"翻译为具体的寄存器配置,例如把"使能 DMA"翻译为对某个状态寄存器写入特定位。
这种分层带来的直接收益是:上层 HAL 可以做到跨芯片尽量通用,而芯片差异被收敛在_ll层;新增芯片支持时主要工作量落在 LL 层。在esp_hal_sd中,底层 LL 实现按芯片分目录存放,例如 esp32/include/hal/sdmmc_ll.h、esp32p4/include/hal/sdmmc_ll.h。
SDMMC Host HAL:上下文、初始化与电源管理
上下文结构
HAL 以"上下文 + 显式初始化"的方式组织状态。sdmmc_hal.h 定义了最小化的上下文结构:
typedef struct sdmmc_dev_t *sdmmc_soc_handle_t; typedef struct { sdmmc_soc_handle_t dev; // SDMMC SOC layer handle (即寄存器基地址) } sdmmc_hal_context_t;上下文只保存一个dev句柄(即寄存器基地址),其余状态由上层驱动自行管理——这是典型的轻量 HAL 设计:HAL 不复制驱动的全部状态,只封装"拿到硬件、释放硬件"的步骤。
初始化 / 反初始化
sdmmc_hal.c 给出了两个入口函数的实际行为:
void sdmmc_hal_init(sdmmc_hal_context_t *hal) { hal->dev = SDMMC_LL_GET_HW(0); sdmmc_ll_mem_power_by_pmu(hal->dev); sdmmc_ll_mem_set_low_power_mode(hal->dev, SDMMC_LL_MEM_LP_MODE_SHUT_DOWN); } void sdmmc_hal_deinit(sdmmc_hal_context_t *hal) { sdmmc_ll_mem_force_low_power(hal->dev); hal->dev = NULL; }可以看到sdmmc_hal_init除了取硬件句柄外,还额外完成了电源管理(PMU)配置:先通过sdmmc_ll_mem_power_by_pmu让 SDMMC 内存由 PMU 供电,再设置低功耗模式下内存进入关闭状态;sdmmc_hal_deinit则强制关闭 SDMMC 电源域并清空句柄。这三个 LL 函数定义在 esp32/include/hal/sdmmc_ll.h(第 173–194 行附近)与 esp32p4/include/hal/sdmmc_ll.h 等目标文件中,印证了"上层 HAL 定义步骤、底层 LL 翻译为寄存器操作"的分工。
共享类型定义
sd_types.h 提供了 SDMMC/SDIO 主机侧共用的基础类型,上层驱动配置参数时经常直接引用:
- 总线宽度
sd_bus_width_t:SD_BUS_WIDTH_1_BIT(1)、SD_BUS_WIDTH_4_BIT(4)、SD_BUS_WIDTH_8_BIT(8); - 工作模式
sd_mode_t:SD_MODE_NORMAL(普通模式)、SD_MODE_UHS1(UHS-I 模式); - 采样模式
sd_sampling_mode_t:SD_SAMPLING_MODE_SDR(1)(单数据率)、SD_SAMPLING_MODE_DDR(双数据率); - 时钟延时相位
sdmmc_delay_phase_t:SDMMC_DELAY_PHASE_0~SDMMC_DELAY_PHASE_7及SDMMC_DELAY_PHASE_AUTO(仅 UHS-I 模式有效)。该参数仅在主机工作于SDMMC_FREQ_HIGHSPEED、SDMMC_FREQ_52M、SDR50、DDR50、SDR104 等高速档位时生效,驱动会打印实际延时(单位皮秒 ps); - 时钟延时线
sdmmc_delay_line_t:SDMMC_DELAY_LINE_0~SDMMC_DELAY_LINE_7及SDMMC_DELAY_LINE_AUTO。
此外该头文件还给出两个实用宏:SDMMC_DATA_SIG_NUM(数据信号数,取自SOC_SDMMC_DATA_WIDTH_MAX,未定义时为 0)与SDMMC_DMA_ALIGNMENT(DMA 对齐要求为 4 字节)。
槽位与引脚信息
主机侧还通过 sdmmc_periph.h 暴露外设的静态描述数据:
sdmmc_slot_info_t:每个槽位的最大总线宽度(1/4/8)以及 Card Detect、Write Protect、Card Interrupt 信号在 GPIO Matrix 中的编号;sdmmc_slot_gpio_num[]/sdmmc_slot_gpio_sig[]:按槽位给出 CLK/CMD/D0–D7 等 SD 总线信号的 GPIO 引脚号(SOC_SDMMC_USE_IOMUX时)或 GPIO Matrix 信号号(SOC_SDMMC_USE_GPIO_MATRIX时)。
具体数值由各芯片目录下的sdmmc_periph.c提供(如 esp32/sdmmc_periph.c、esp32s3/sdmmc_periph.c),上层驱动据此完成引脚复用配置。
SDIO Slave HAL:三大独立子系统的抽象
SDIO 从机是esp_hal_sd中内容最丰富的部分。原文档指出,从机外设硬件上由三部分独立子系统组成,HAL 也按此组织——这一设计描述完整记录在 sdio_slave_hal.h 的架构注释中:
- 寄存器与中断(Register / Interrupt):共享寄存器对从机内部总线直接可读写,主机则通过 CMD52/53 按地址访问;主机写命令中断从机时置位中断寄存器相应位,从机需要中断主机时则写寄存器触发 DAT1 线上的中断信号。
- 发送 FIFO:外设提供计数器寄存器,主机据此判断从机是否就绪,并通过 CMD53 读写 FIFO。HAL 在初始化时重置计数器;若主机侧重启(或丢失计数),应通知从机再次重置。
- 接收 FIFO:结构同发送侧,方向相反。
免拷贝的关键设计:描述符队列
为避免每次传输都在 FIFO 与内存缓冲间拷贝数据,HAL 内部维护一个描述符队列(以链表实现):内存缓冲的描述符被排队后,HAL 即取得该缓冲的所有权,直到对应的"完成"函数成功返回。ISR 会被多次调用以迭代已排队的描述符,并在缓冲释放时通知上层。发送侧描述符sdio_slave_hal_send_desc_t在 DMA 描述符基础上追加了pkt_len(累积长度)与arg(用户参数)两个字段;接收侧sdio_slave_hal_recv_desc_t则直接复用 LL 层描述符。
HAL 上下文
sdio_slave_hal.h 中sdio_slave_context_t是使用 HAL 的核心结构,其中需要手动配置的成员包括:
sending_mode:发送模式,取值见 sdio_slave_types.h 中的sdio_slave_sending_mode_t——SDIO_SLAVE_SEND_STREAM(流模式,尽量合并为一个包)或SDIO_SLAVE_SEND_PACKET(包模式,逐个发送);timing:时序模式sdio_slave_timing_t,默认推荐SDIO_SLAVE_TIMING_PSEND_PSAMPLE(HS 模式默认值,DS 模式通常也无问题);no_highspeed:位标志,置 1 关闭高速支持;send_queue_size:发送队列中可排队缓冲的最大数量;recv_buffer_size:每个接收缓冲的大小(需主机与从机预先协商一致);send_desc_queue:保存排队描述符的环形缓冲,需先初始化好data与size。
其余成员(发送状态机、in-flight 链表头尾、接收 STAILQ 等)为内部状态,使用时无需触碰。发送描述符占用的空间大小由宏SDIO_SLAVE_SEND_DESC_SIZE给出。
接收侧典型调用流程
原文档给出接收的完整操作步骤(实现位于 sdio_slave_hal.c):
sdio_slave_hal_recv_start启动接收 DMA(若已有缓冲则优先从它们开始接收);sdio_slave_hal_recv_init_desc将缓冲地址与描述符关联,HAL 会写入协商好的recv_buffer_size;sdio_slave_hal_load_buf装载接收缓冲——DMA 启动后描述符进入链表,接收缓冲计数增加,硬件自动按链表把数据写入各缓冲;- (可选,启用中断时必需)
sdio_slave_hal_recv_done检查并清除接收中断位; sdio_slave_hal_recv_has_next_item检查是否存在已完成接收的缓冲;- 对每次成功返回,调用一次
sdio_slave_hal_recv_unload_desc取回描述符; - (可选,DMA 停止时)
sdio_slave_hal_recv_reset_counter仅重置计数器到"已装载未使用"的缓冲数; - (可选,DMA 停止时)递归调用
sdio_slave_hal_recv_flush_one_buffer丢弃一个(或多个)缓冲中的数据并重新装载; - (反初始化时可选,DMA 停止时)递归
sdio_slave_hal_recv_unload_desc无条件取回所有缓冲。
发送侧典型调用流程
发送驱动未使用重启动特性,流程为:
sdio_slave_hal_send_start启动发送 DMA(已有数据则立即待发);sdio_slave_hal_send_queue把数据排入发送队列(启用中断时 ISR 被触发);- (启用中断时必需)清除软件调用逻辑使用的中断位;
sdio_slave_hal_send_new_packet_if_exist检查并发送新包(ESP_OK表示 DMA 开始发送新包,ESP_ERR_NOT_FOUND表示无包可发,ESP_ERR_INVALID_STATE表示仍有包在传输中);sdio_slave_hal_send_eof_happened检查上一包是否发送完成(同时清除该事件中断位);- 递归调用
sdio_slave_hal_send_get_next_finished_arg取回已完成缓冲的arg参数; - (反初始化时可选)递归
sdio_slave_hal_send_flush_next_buffer取回全部已排队缓冲; - (可选)
sdio_slave_hal_send_reset_counter把计数器重置到"已装载未发送"的缓冲数。
文档特别提醒:执行第 2 步与第 6 步时应使用计数器配对,确保队列空间充足;且该 HAL 并非线程安全,调用方需用自旋锁保证sdio_slave_hal_send_queue等函数不被并发调用。此外,DMA 在传输期间已装载的描述符不可修改,因此发送侧使用一个环形缓冲保存描述符、让硬件看到的始终是线性链表,传输结束后由 ISR 修复链表环结构(见 sdio_slave_hal.c 中 Send 章节的设计注释)。
主机交互(Host 部分)
从机与外部主机之间的控制面交互由以下函数承担:
sdio_slave_hal_hostint_set_ena/sdio_slave_hal_hostint_get_ena:使能/查询发往主机的各类中断(主机可同时修改相同寄存器,建议仅在初始化阶段使用);sdio_slave_hal_hostint_send/sdio_slave_hal_hostint_clear:触发通用目的中断或取消发往主机的所有中断,这两个函数以并发安全方式实现,从机可安全调用;sdio_slave_hal_slvint_fetch_clear:读取并清除主机发往从机的通用目的中断;sdio_slave_hal_host_get_reg/sdio_slave_hal_host_set_reg:读写主机与从机共享的通用寄存器(位置 0–63,但 24–27 除外,非并发安全,避免两侧同时写同一寄存器);sdio_slave_hal_get_intr_status_reg:获取中断状态寄存器地址。
中断位掩码定义在 sdio_slave_types.h:SDIO_SLAVE_HOSTINT_BIT0~BIT7为 8 个通用目的中断位,SDIO_SLAVE_HOSTINT_SEND_NEW_PACKET(BIT23)表示有新包可发。
构建与裁剪:按芯片能力条件编译
从 CMakeLists.txt 可以确认该组件的编译策略:
- 组件不支持 POSIX/Linux 模拟器(
target为linux时直接返回); CONFIG_SOC_SDMMC_HOST_SUPPORTED使能时编译sdmmc_hal.c与对应芯片的sdmmc_periph.c;CONFIG_SOC_SDIO_SLAVE_SUPPORTED使能时编译sdio_slave_hal.c与对应芯片的sdio_slave_periph.c;- 组件
REQUIRES soc hal,头文件同时暴露公共include与按芯片追加的${target}/include目录。
因此,是否包含本组件的 SDMMC/SDIO 从机实现,取决于目标芯片的 SOC 能力宏(如SOC_SDMMC_HOST_SUPPORTED、SOC_SDIO_SLAVE_SUPPORTED),这解释了目录结构中为何不同芯片只保留自己支持的 LL 头文件:例如 esp32p4、esp32s3、esp32s31 只含 SDMMC 主机侧,而 esp32c5、esp32c6 等只含 SDIO 从机侧,esp32则两者兼备。
使用建议与注意事项
- 接口稳定性:原文档反复强调本 HAL 不保证跨版本稳定性与向后兼容性,直接依赖本层接口的代码在升级 ESP-IDF 时可能需要同步修改。
- 使用边界:本组件头文件顶部声明 "The hal is not public api, don't use in application code",即 HAL 并非公共 API,不应在应用代码中直接使用;它主要为 IDF 外设驱动提供抽象,高级开发者自研驱动时可参考其调用模式(如
sdio_slave_hal.c中的 ISR 处理与环形描述符队列设计)。 - 参数一致性:SDIO 从机场景中,
recv_buffer_size等参数必须与主机侧预先协商一致;FIFO 计数器在主机重启后会失效,需要从机重新复位(HAL 初始化时会重置计数器)。 - 并发与状态机约束:发送队列函数非线程安全需加锁;
reset_counter、flush系列函数只能在 DMA 停止时调用;反初始化时取回缓冲同样必须在 DMA 停止后进行。
如需深入,可继续阅读组件公共头文件 sdio_slave_hal.h(含完整架构注释与逐函数说明)、sdmmc_hal.h 与 sd_types.h,以及各芯片目录下的_ll底层实现。
【免费下载链接】esp-idfEspressif IoT Development Framework. Official development framework for Espressif SoCs.项目地址: https://gitcode.com/GitHub_Trending/es/esp-idf
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考