【esp32s3】GPIO 寄存器 开发解析

发布于:2025-07-24 ⋅ 阅读:(15) ⋅ 点赞:(0)

在这里插入图片描述

从类51 / 类stm32 单片机过渡到 esp,总会有点惯性思维,寄存器在哪?我要怎么操作寄存器,手册怎么没有配置过程的指导。数据手册的寄存器排版还写得那么难看!!!


一、库函数测试

1.1. 代码

  • 创建组件,生成简单的库函数调用例子:
  • 运行后用电表测试,GPIO_NUM_4 有正常的电平转换
#include <stdio.h>
#include "gpio_reg_test.h"

#include "esp_log.h"
#include "driver/gpio.h" // 需要添加依赖 PRIV_REQUIRES driver
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"

static const char *TAG = "gpio_reg_test.c";

// 配置参数
#define BLINK_GPIO GPIO_NUM_4 // 使用GPIO4作为示例(可根据需要修改)
#define BLINK_DELAY_MS (5*1000)    // 闪烁间隔(毫秒)

// 测试 gpio 寄存器 函数
void gpio_reg_test_fun(void)
{
    // GPIO配置结构体
    gpio_config_t io_conf = {
        .pin_bit_mask = (1ULL << BLINK_GPIO),  // 选择GPIO
        .mode = GPIO_MODE_OUTPUT,              // 输出模式
        .pull_up_en = GPIO_PULLUP_DISABLE,     // 不上拉
        .pull_down_en = GPIO_PULLDOWN_DISABLE, // 不下拉
        .intr_type = GPIO_INTR_DISABLE         // 禁用中断
    };

    // 初始化GPIO
    ESP_ERROR_CHECK(gpio_config(&io_conf));

    // 主循环
    while (1)
    {
        // 调用库函数 设置高电平
        ESP_ERROR_CHECK(gpio_set_level(BLINK_GPIO, 1));
        ESP_LOGI(TAG, "GPIO%d set HIGH", BLINK_GPIO);
        vTaskDelay(pdMS_TO_TICKS(BLINK_DELAY_MS));

        // 调用库函数 设置低电平
        ESP_ERROR_CHECK(gpio_set_level(BLINK_GPIO, 0));
        ESP_LOGI(TAG, "GPIO%d set LOW", BLINK_GPIO);
        vTaskDelay(pdMS_TO_TICKS(BLINK_DELAY_MS));
    }
}
  • 教训:函数名字不可以和组件名字相同,会导致 main.c 导入头文件失败,一直找不到文件!!!
// * 确保头文件内容在单个编译单元内仅被展开一次(类似 #ifndef + #define + #endif 组合)
//#pragma one
#ifndef _GPIO_REG_TEST_H_
#define _GPIO_REG_TEST_H_

// 测试 gpio 寄存器 函数
extern void gpio_reg_test_fun(void); // ! 教训:函数名字不可以和组件名字相同,会导致 main.c 导入头文件失败,一直找不到文件!!!

#endif /* _GPIO_REG_TEST_H_ */

1.2. 解析

1.2.1. esp_driver_gpio

  • gpio的初始化过程大体和stm32的类似,使用结构体配置,然后传入结构体参数,然后调用一个设置高低电平的函数。
    // 初始化GPIO
    ESP_ERROR_CHECK(gpio_config(&io_conf));
  • 按住 Ctrl 后鼠标左键点击函数名,跳转到原型查看:可以看到 gpio_config 函数是属于 esp_driver_gpio 组件的内容。
    • 可以看到里面是逐一判断结构体的取值,选择调用进一步不同的函数:
      • 如果是 类stm32 的芯片,这时一般就已经是寄存器操作了,gpio这种简单配置不会嵌套太多层。

在这里插入图片描述

  • 进一步到每一个结构体配置项查看,比如 gpio_input_enable / gpio_input_disable 配置输入开关:
    • 可以看到这些配置的api还是属于 esp_driver_gpio 组件的内容:
    • 它们内部调用了 hal 库的内容,hal 在开发 类stm32 时经常看到。我把它理解为是一个二次打包的库,有点类似逐飞为不同开发板写同一套api的做法,方便调用移植。

在这里插入图片描述

1.2.2. HAL

  • 再进一步,点击进入查看,会看到,跳转到组件 hal 中:
    • 然后这个函数其实是宏定义,链接到了 ll 库,ll 在开发 类stm32 时也经常看到,就是官方给的出厂驱动库,好像叫 dll

在这里插入图片描述

1.2.3. LL

  • 再进一步,点进去查看,看到跳转到了 hal 库的 esp32s3 类别中的 头文件 中:
    • 注意到函数前面加了 __attribute__((always_inline))static inline
    • 这些函数是被定义在头文件中的。

在这里插入图片描述

  • 鼠标悬空看到扩展内容:这条宏定义最后会被解析成寄存器的地址,然后直接赋值。
  • 这就是我们熟悉的寄存器操作了!!!

在这里插入图片描述

1.2.4. SOC

  • 再进一步查看,这个宏定义是位于组件 socesp32s3 的类别下。
  • 这里面定义了对于单片机类型的寄存器起始地址和偏移地址。

在这里插入图片描述

1.2.5. REG 数据手册

  • 打开数据手册查看基础地址,IO MUX0x6000_9000

在这里插入图片描述

  • GPIO 配置寄存器 的偏移地址是 0x0004 , 且这个寄存器的占位大小是 32位 = 4字节 = 0x4 倍数偏移

在这里插入图片描述

  • 其中输入开关控制位 IO_MUX_FUN_IE 处于第9位 1 << 9

在这里插入图片描述

  • 再回过头看看宏定义的扩展内容:完全符合
    • 外设基地址 -> 寄存器偏移地址 -> 赋值 -> 先取值再求或运算 -> 对应位数置1

在这里插入图片描述

  • 在线调试能看到实时结果,卡好断点单步运行,看外设寄存器值变化:
    • 自行查看不同外设不同变化,下面只是一个示意:

在这里插入图片描述

二、寄存器测试

  • 上面已经使用库函数完成了gpio的电平切换,使用上面的方法逐一查看,实际调用的寄存器地址和位数,然后使用寄存器操作实现切换:
static uint32_t * const GPIO_OUT_W1TS_REG = (uint32_t *)(0x60004000 + 0x0008); // GPIO0 ~ 31 输出置位寄存器
static uint32_t * const GPIO_OUT_W1TC_REG = (uint32_t *)(0x60004000 + 0x000C); // GPIO0 ~ 31 输出清零寄存器

/* 省略不需要更改的地方 */

// 调用寄存器 设置高电平
GPIO_OUT_W1TS_REG[0] |= (1 << BLINK_GPIO);
ESP_LOGI(TAG, "GPIO%d set HIGH", BLINK_GPIO);
vTaskDelay(pdMS_TO_TICKS(BLINK_DELAY_MS));

// 调用寄存器 设置低电平
GPIO_OUT_W1TC_REG[0] |= (1 << BLINK_GPIO);
ESP_LOGI(TAG, "GPIO%d set LOW", BLINK_GPIO);
vTaskDelay(pdMS_TO_TICKS(BLINK_DELAY_MS));
  • 实测和库函数调用是一样的效果, 这样操作就没有移植性,不过可以用来测试模拟spi/iic,方便验证模块功能。也是一种思路。

三、API 指南 - 硬件抽象

官方介绍:硬件抽象
以下原封不动截取部分内容:

  • ESP-IDF 提供了一组用于硬件抽象的 API,支持以不同抽象级别控制外设,相比仅使用 ESP-IDF 驱动程序与硬件进行交互,使用更加灵活。ESP-IDF 硬件抽象适用于编写高性能裸机驱动程序,或尝试将 ESP 芯片移植到另一个平台。

3.1. 架构

  • ESP-IDF 的硬件抽象由以下层级各组成,从接近硬件的低层级抽象,到远离硬件的高层级抽象。

    • 低级层 (LL)
    • 硬件抽象层 (HAL)
    • 驱动层 (esp_driver_gpio)
  • LL 层和 HAL 完全包含在 hal 组件中,每一层都依赖于其下方的层级,即驱动层依赖于 HAL 层,HAL 层依赖于 LL 层,LL 层依赖于寄存器头文件。

3.2. LL 层(低级层)

  • LL 层主要目的是将寄存器字段访问抽象为更容易理解的函数。LL 函数本质是将各种输入/输出参数转换为外设寄存器的寄存器字段,并以获取/设置函数的形式呈现。所有必要的位移、掩码、偏移和寄存器字段的字节顺序都应由 LL 函数处理。
  • 所有 LL 函数均定义为 static inline,因此,由于编译器优化而调用这些函数时,开销最小。这些函数不保证由编译器内联,因此在禁用缓存时(例如从 IRAM ISR 上下文调用)调用的任何 LL 函数都应标记为 __attribute__((always_inline))

3.3. HAL(硬件抽象层)

  • HAL 将外设的操作过程建模成一组通用步骤,其中每个步骤都有一个相关联的函数。对于每个步骤,HAL 隐藏(抽象)了外设寄存器的实现细节(即需要设置/读取的寄存器)。通过将外设操作过程建模为一组功能步骤,HAL 可以抽象化(即透明处理)不同目标或芯片版本间的微小硬件实现差异。换句话说,特定外设的 HAL API 在多个目标/芯片版本之间基本保持相同。

  • HAL 函数不应包含任何操作系统原语,如队列、信号量、互斥锁等。所有同步/并发操作应在更高层次(如驱动程序)处理。

3.4. 总结

  • 官方手册里明确规范了不同层的函数名与参数名的习惯定义,方便一眼知道作用,增加可读性。
  • 层层嵌套是为了移植性,编译时自动根据选择芯片切换不同的LL层,HAL及其以上组件是共用的。
  • api的调用需要自行确保不冲突,使用rtos的互斥或信号量等保护。
寄存器读写
原子操作
位域控制
跨芯片兼容接口
安全API
硬件层
ESP32寄存器
LL层
Low-Level
HAL层
Hardware Abstraction
驱动层
esp_driver_xxx
应用层
用户代码

网站公告

今日签到

点亮在社区的每一天
去签到