【esp32s3】6 - 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         // 禁用中断
    };

    // 初始化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
应用层
用户代码
### 关于ESP32-S3与TCA9554驱动的使用 在ESP-IDF框架下,针对ESP32-S3开发板集成并使用TCA9554 I/O扩展芯片时,可以遵循以下方式实现其功能。TCA9554是一种8位通用I/O端口扩展器,通过I²C接口连接到微控制器。 #### 配置I²C总线 为了使能TCA9554设备通信,需先初始化I²C总线。以下是基于ESP-IDF的一个简单示例代码: ```c #include "driver/i2c.h" #define SDA_PIN GPIO_NUM_21 // 定义SDA引脚 #define SCL_PIN GPIO_NUM_22 // 定义SCL引脚 #define I2C_MASTER_FREQ_HZ 100000 // 设置频率为100kHz void i2c_master_init() { int i2c_master_port = I2C_NUM_0; i2c_config_t conf = { .mode = I2C_MODE_MASTER, .sda_io_num = SDA_PIN, .scl_io_num = SCL_PIN, .sda_pullup_en = GPIO_PULLUP_ENABLE, // 启用内部上拉电阻 .scl_pullup_en = GPIO_PULLUP_ENABLE, .master.clk_speed = I2C_MASTER_FREQ_HZ, }; i2c_param_config(i2c_master_port, &conf); // 参数配置 i2c_driver_install(i2c_master_port, conf.mode, 0, 0, 0); } ``` 以上代码片段用于设置I²C主机模式下的基本参数[^2]。 #### 初始化TCA9554设备 完成I²C总线初始化后,可以通过向特定寄存器写入数据来控制TCA9554的状态。该器件支持两个主要寄存器:输入/输出方向寄存器(Port Configuration Register)和数据寄存器(Input/Output Port Register)。下面是一个简单的函数用来操作这些寄存器: ```c #define TCA9554_ADDR 0x20 // 假设地址为0x20 // 函数声明 esp_err_t tca9554_write_reg(uint8_t reg_addr, uint8_t data); // 实现部分 esp_err_t tca9554_write_reg(uint8_t reg_addr, uint8_t data) { esp_err_t ret; i2c_cmd_handle_t cmd = i2c_cmd_link_create(); i2c_master_start(cmd); i2c_master_write_byte(cmd, (TCA9554_ADDR << 1) | WRITE_BIT, ACK_CHECK_EN); i2c_master_write_byte(cmd, reg_addr, ACK_CHECK_EN); i2c_master_write_byte(cmd, data, ACK_CHECK_EN); i2c_master_stop(cmd); ret = i2c_master_cmd_begin(I2C_NUM_0, cmd, pdMS_TO_TICKS(10)); i2c_cmd_link_delete(cmd); return ret; } // 使用实例 tca9554_write_reg(0x00, 0xFF); // 将所有管脚配置为输入 tca9554_write_reg(0x01, 0x00); // 清除输出状态 ``` 上述代码展示了如何利用`i2c_master_write_byte()`命令序列化构建发送缓冲区,并最终执行传输过程[^3]。 #### 注意事项 当实际部署项目时,请注意确认所使用的硬件版本以及对应的I²C地址可能有所不同;另外还需要考虑电源电压匹配等问题以确保稳定工作环境。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值