【esp32s3】GPIO 寄存器 开发解析
从类
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 // 禁用中断};// 初始化GPIOESP_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
的类似,使用结构体配置,然后传入结构体参数,然后调用一个设置高低电平的函数。
// 初始化GPIOESP_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的做法,方便调用移植。
- 可以看到这些配置的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
- 再进一步查看,这个宏定义是位于组件
soc
的esp32s3
的类别下。 - 这里面定义了对于单片机类型的寄存器起始地址和偏移地址。
1.2.5. REG 数据手册
- 打开数据手册查看基础地址,
IO MUX
是0x6000_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
的互斥或信号量等保护。