02. 通用GPIO

一、GPIO简介

  GPIO 是负责控制或采集外部器件信息的外设,主要负责输入输出功能。ESP32-S3 芯片具有 45 个物理 GPIO 管脚。每个管脚都可用作一个通用输入输出,或连接一个内部外设信号。ESP-IDF 提供了丰富的 GPIO 操作函数,开发者可以在 esp-idf-v5.3.2\components\driver\gpio 路径下找到相关的 gpio.c 和 gpio.h 文件。

这 45 个物理 GPIO 管脚的编号为:0 ~ 21、26 ~ 48。这些管脚既可作为输入又可作为输出管脚。

二、GPIO常用函数

  ESP-IDF 提供了一套 API 来配置 GPIO。要使用此功能,我们需要在 CMakeLists.txt 文件中导入 esp_timer 依赖库,然后还需要导入必要的头文件:

# 注册组件到构建系统的函数
idf_component_register(
    # 依赖库的路径
    REQUIRES driver
)
#include "driver/gpio.h"

2.1、配置GPIO函数

  我们可以使用 gpio_config() 函数 配置 GPIO 的模式、上下拉等功能,其函数原型如下所示:

/**
 * @brief 设置GPIO配置
 * 
 * @param pGPIOConfig GPIO配置结构体的指针
 * @return esp_err_t ESP_OK配置成功,其它配置失败
 */
esp_err_t gpio_config(const gpio_config_t *pGPIOConfig);

  形参 pGPIOConfig 为 GPIO 配置结构体指针,它的主要成员如下:

typedef struct 
{
    uint64_t pin_bit_mask;              // 配置引脚位
    gpio_mode_t mode;                   // 设置引脚模式
    gpio_pullup_t pull_up_en;           // 设置上拉
    gpio_pulldown_t pull_down_en;       // 设置下拉
    gpio_int_type_t intr_type;          // 中断配置
} gpio_config_t;

  成员 pin_bit_mask 用来 设置的引脚位,我们可以填写的参数格式如下:(1 << x),其中 x 为 ESP32 S3 中可用 GPIO。比如我们使用 IO1 引脚,则可以写为:(1ull << GPIO_NUM_1)

  成员 mode 用来 设置引脚模式,它的可选值如下:

typedef enum
{
    GPIO_MODE_DISABLE = GPIO_MODE_DEF_DISABLE,                                                          // 失能输入输出模式
    GPIO_MODE_INPUT = GPIO_MODE_DEF_INPUT,                                                              // 输入模式
    GPIO_MODE_OUTPUT = GPIO_MODE_DEF_OUTPUT,                                                            // 输出模式
    GPIO_MODE_OUTPUT_OD = ((GPIO_MODE_DEF_OUTPUT) | (GPIO_MODE_DEF_OD)),                                // 输出开漏输出模式
    GPIO_MODE_INPUT_OUTPUT_OD = ((GPIO_MODE_DEF_INPUT) | (GPIO_MODE_DEF_OUTPUT) | (GPIO_MODE_DEF_OD)),  // 输入输出开漏模式
    GPIO_MODE_INPUT_OUTPUT = ((GPIO_MODE_DEF_INPUT) | (GPIO_MODE_DEF_OUTPUT)),                          // 输入输出模式
} gpio_mode_t;

  成员 pull_up_en 用来 配置上拉,它的可选值如下:

typedef enum 
{
    GPIO_PULLUP_DISABLE = 0x0,      // 失能上拉
    GPIO_PULLUP_ENABLE = 0x1,       // 使能上拉
} gpio_pullup_t;

  成员 pull_down_en 用来 配置下拉,它的可选值如下:

typedef enum 
{
    GPIO_PULLDOWN_DISABLE = 0x0,    // 失能下拉
    GPIO_PULLDOWN_ENABLE = 0x1,     // 使能下拉
} gpio_pulldown_t;

  成员 intr_type 用来 配置中断,它的可选值如下:

typedef enum 
{
    GPIO_INTR_DISABLE = 0,      // 失能中断
    GPIO_INTR_POSEDGE = 1,      // 上升沿
    GPIO_INTR_NEGEDGE = 2,      // 下降沿
    GPIO_INTR_ANYEDGE = 3,      // 上升沿和下降沿
    GPIO_INTR_LOW_LEVEL = 4,    // 输入低电平触发
    GPIO_INTR_HIGH_LEVEL = 5,   // 输入高电平触发
    GPIO_INTR_MAX,
} gpio_int_type_t;

  该函数返回 ESP_OK 表示 配置成功,返回 ESP_FAIL 表示 配置失败

#define ESP_OK          0       // 配置成功
#define ESP_FAIL        -1      // 配置失败

2.2、设置引脚输出电平函数

  在我们配置好 GPIO 后,还可以 gpio_set_level() 函数 设置引脚的输出电平,它的函数声明如下:

/**
 * @brief 设置GPIO电平
 * 
 * @param gpio_num GPIO引脚编号
 * @param level GPIO的引脚电平
 * @return esp_err_t ESP_OK设置成功,其它设置失败
 */
esp_err_t gpio_set_level(gpio_num_t gpio_num, uint32_t level);

  形参 gpio_numGPIO 引脚号。该参数在 gpio_types.h 文件中枚举 gpio_num_t 有定义。

typedef enum 
{
    GPIO_NUM_NC = -1,   // Use to signal not connected to S/W
    GPIO_NUM_0 = 0,     // GPIO0, input and output
    GPIO_NUM_1 = 1,     // GPIO1, input and output
    GPIO_NUM_2 = 2,     // GPIO2, input and output
    GPIO_NUM_3 = 3,     // GPIO3, input and output
    GPIO_NUM_4 = 4,     // GPIO4, input and output
    GPIO_NUM_5 = 5,     // GPIO5, input and output
    GPIO_NUM_6 = 6,     // GPIO6, input and output
    GPIO_NUM_7 = 7,     // GPIO7, input and output
    GPIO_NUM_8 = 8,     // GPIO8, input and output
    GPIO_NUM_9 = 9,     // GPIO9, input and output
    GPIO_NUM_10 = 10,   // GPIO10, input and output
    GPIO_NUM_11 = 11,   // GPIO11, input and output
    GPIO_NUM_12 = 12,   // GPIO12, input and output
    GPIO_NUM_13 = 13,   // GPIO13, input and output
    GPIO_NUM_14 = 14,   // GPIO14, input and output
    GPIO_NUM_15 = 15,   // GPIO15, input and output
    GPIO_NUM_16 = 16,   // GPIO16, input and output
    GPIO_NUM_17 = 17,   // GPIO17, input and output
    GPIO_NUM_18 = 18,   // GPIO18, input and output
    GPIO_NUM_19 = 19,   // GPIO19, input and output
    GPIO_NUM_20 = 20,   // GPIO20, input and output
    GPIO_NUM_21 = 21,   // GPIO21, input and output
    GPIO_NUM_26 = 26,   // GPIO26, input and output
    GPIO_NUM_27 = 27,   // GPIO27, input and output
    GPIO_NUM_28 = 28,   // GPIO28, input and output
    GPIO_NUM_29 = 29,   // GPIO29, input and output
    GPIO_NUM_30 = 30,   // GPIO30, input and output
    GPIO_NUM_31 = 31,   // GPIO31, input and output
    GPIO_NUM_32 = 32,   // GPIO32, input and output
    GPIO_NUM_33 = 33,   // GPIO33, input and output
    GPIO_NUM_34 = 34,   // GPIO34, input and output
    GPIO_NUM_35 = 35,   // GPIO35, input and output
    GPIO_NUM_36 = 36,   // GPIO36, input and output
    GPIO_NUM_37 = 37,   // GPIO37, input and output
    GPIO_NUM_38 = 38,   // GPIO38, input and output
    GPIO_NUM_39 = 39,   // GPIO39, input and output
    GPIO_NUM_40 = 40,   // GPIO40, input and output
    GPIO_NUM_41 = 41,   // GPIO41, input and output
    GPIO_NUM_42 = 42,   // GPIO42, input and output
    GPIO_NUM_43 = 43,   // GPIO43, input and output
    GPIO_NUM_44 = 44,   // GPIO44, input and output
    GPIO_NUM_45 = 45,   // GPIO45, input and output
    GPIO_NUM_46 = 46,   // GPIO46, input and output
    GPIO_NUM_47 = 47,   // GPIO47, input and output
    GPIO_NUM_48 = 48,   // GPIO48, input and output
    GPIO_NUM_MAX,
} gpio_num_t;

2.3、获取引脚电平函数

  我们可以使用 gpio_get_level() 函数用于 获取某个管脚的电平,该函数原型如下所示:

/**
 * @brief 获取GPIO电平
 * 
 * @param gpio_num GPIO引脚编号
 * @return int GPIO的引脚电平
 */
int gpio_get_level(gpio_num_t gpio_num);

如果要使用 gpio_get_level() 读取引脚的电平,则需要将引脚配置为 输入模式输入输出模式,否则该函数始终返回 0。

2.4、设置GPIO的方向

  我们可以使用 gpio_set_direction() 函数 设置 GPIO 的方向,其函数原型如下:

/**
 * @brief 配置GPIO方向
 * 
 * @param gpio_num GPIO引脚编号
 * @param mode GPIO模式
 * @return esp_err_t ESP_OK设置成功,其它设置失败
 */
esp_err_t gpio_set_direction(gpio_num_t gpio_num, gpio_mode_t mode);

2.5、设置GPIO的上下拉

  我们使用 gpio_set_pull_mode() 函数 设置 GPIO 的上下拉,其函数原型如下:

/**
 * @brief GPIO设置上下拉
 * 
 * @param gpio_num GPIO引脚编号
 * @param pull 上拉/下拉
 * @return esp_err_t SP_OK设置成功,其它设置失败
 */
esp_err_t gpio_set_pull_mode(gpio_num_t gpio_num, gpio_pull_mode_t pull);

三、点亮LED

3.1、LED简介

  LED,即发光二极管,其发光原理基于半导体的特性。在半导体中,有两类重要的载流子:电子空穴电子,主要存在于 N 型半导体中;而 空穴,则主要存在于 P 型半导体中。当 N 型半导体与 P型半导体材料接触时,它们的交界处会形成一个特殊的层结。当对这个层结施加适当的电压时,层结中的空穴与电子会发生重组,并释放出能量。这些能量会以光子的形式被释放出来,从而产生可见光。

  LED 驱动是指通过稳定的电源为 LED 提供适宜的电流和电压,确保其正常发光。LED 驱动方式主要有 恒流恒压 两种。其中,恒流驱动 因其能 限定电流 而备受青睐。由于 LED 灯对电流变化极为敏感,一旦电流超过其额定值,可能导致损坏。因此,恒流驱动通过确保电流的稳定性,进而保障 LED 的安全运行。

  恒流驱动 LED 主要有两种方式:灌入电流接法输出电流接法

  灌入电流接法 指的是 LED 的供电电流是由外部提供电流,将电流灌入我们的 MCU;风险是当外部电源出现变化时,会导致 MCU 的引脚烧坏。其接法如下图所示:

灌入电流接法

  输出电流接法 指的是由 MCU 提供电压电流,将电流输出给 LED;如果使用 MCU 的 GPIO 直接驱动 LED,则驱动能力较弱,可能无法提供足够的电流驱动 LED。其接法如下图所示:

输出电流接法

3.2、实验例程

  这里,我们在 ESP-IDF 版的工程例程中新建一个【components】 文件夹,该文件夹主要用于存放第三方驱动库和开发者编写的驱动库(ESP-IDF 框架要求的)。然后,我们在【components】文件夹下新建一个 【device】文件夹,用来存储设备的驱动程序,然后再该文件夹中新增了一个 【LED】 文件夹,用于存放 led.c 和 led.h 这两个文件。

  其中,led.h 文件负责声明 LED 相关的函数和变量,它的内容如下:

#ifndef __LED_H__
#define __LED_H__

#include "driver/gpio.h"

void led_init(gpio_num_t gpio_num);
void led_set_status(gpio_num_t gpio_num, uint8_t status);
void led_toggle_status(gpio_num_t gpio_num);

#endif // !__LED_H__

  led.c 文件则实现了 LED 的驱动代码,它的内容如下:

#include "led.h"

/**
 * @brief LED初始化
 * 
 * @param gpio_num GPIO引脚
 */
void led_init(gpio_num_t gpio_num)
{
    gpio_config_t gpio_config_struct = {0};

    gpio_config_struct.pin_bit_mask = 1ULL << gpio_num;             // 设置引脚
    gpio_config_struct.intr_type = GPIO_INTR_DISABLE;               // 不使用中断
    gpio_config_struct.mode = GPIO_MODE_INPUT_OUTPUT;               // 输入输出模式
    gpio_config_struct.pull_down_en = GPIO_PULLDOWN_DISABLE;        // 不使用下拉
    gpio_config_struct.pull_up_en = GPIO_PULLUP_DISABLE;            // 不使用上拉
    gpio_config(&gpio_config_struct);                               // 配置GPIO
}

/**
 * @brief LED设置电平
 * 
 * @param gpio_num GPIO引脚
 * @param status 引脚电平
 */
void led_set_status(gpio_num_t gpio_num, uint8_t status)
{
    gpio_set_level(gpio_num, status);                               // 设置引脚电平
}

/**
 * @brief LED翻转电平
 * 
 * @param gpio_num GPIO引脚
 */
void led_toggle_status(gpio_num_t gpio_num)
{
    // 如果引脚未配置为输入模式或输入输出模式,gpio_get_level()函数始终返回0
    gpio_set_level(gpio_num, !gpio_get_level(gpio_num));
}

  我们还需要在 【device】 文件夹中新建一个 CMakeLists.txt 文件,用来管理 C 程序。它的内容如下:

# 源文件路径
set(src_dirs
    led
)

# 头文件路径
set(include_dirs
    led
)

# 设置依赖库
set(requires
    driver
)

# 注册组件到构建系统的函数
idf_component_register(
    # 源文件路径
    SRC_DIRS ${src_dirs}

    # 自定义头文件的路径
    INCLUDE_DIRS ${include_dirs}
    # 依赖库的路径
    REQUIRES ${requires}
)

# 设置特定组件编译选项的函数
# -ffast-math: 允许编译器进行某些可能减少数学运算精度的优化,以提高性能。
# -O3: 这是一个优化级别选项,指示编译器尽可能地进行高级优化以生成更高效的代码。
# -Wno-error=format: 这将编译器关于格式字符串不匹配的警告从错误降级为警告。
# -Wno-format: 这将完全禁用关于格式字符串的警告。
component_compile_options(-ffast-math -O3 -Wno-error=format=-Wno-format)

  修改【main】目录下的 main.c 文件。

#include "freertos/FreeRTOS.h"

#include "led.h"

// app_main()函数是ESP32的入口函数,它是FreRTOS的一个任务,任务优先级是1
// main()函数是C语言入口函数,它会在编译过程中插入到二进制文件中的
void app_main(void)
{   
    led_init(GPIO_NUM_1);

    while (1)
    {
        led_toggle_status(GPIO_NUM_1);
        // 将一个任务延迟给定的滴答数,IDF中提供pdMS_TO_TICKS可以将指定的ms转换为对应的tick数
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

  这里,我们使用 vTaskDelay() 延迟,使灯闪烁以 1s 的频率闪烁。如果灯闪烁的频率不对,那可能是单片机的时钟频率或 FreeRTOS 的时钟基准没设置好。

配置CPU主频

配置FreeRTOS的时钟节拍

四、读取按键输入

4.1、按键简介

  独立按键的原理主要依赖于机械触点和电气触点之间的相互作用。在未被按下时,触点保持分离状态,电路处于断开状态。然而,当用户按下按键时,在弹簧和导电片的共同作用下,触点会闭合,从而使电路连通。此时,微控制器能够检测到按键触发的信号,进而执行相应的操作。

  机械按键在闭合与分开的过程中,由于机械振动(类似于弹簧效应)的存在,可能导致开关状态在短时间内频繁切换,这种现象被称为 按键抖动。下图是独立按键抖动波形图。

独立按键抖动波形图

  图中的按下抖动和释放抖动的时间一般为 5 ~ 10ms,如果在抖动阶段采样,其不稳定状态可能出现一次按键动作被认为是多次按下的情况。为了避免抖动可能带来的误操作,我们要做的措施就是给按键消抖(即采样稳定闭合阶段)。

  为了消除这种抖动,我们通常采用软件消抖和硬件消抖两种主要方法:软件消抖硬件消抖

  • 软件消抖:主要是通过编程的方法,设定一个延迟或计时器,确保在一定的时间内只读取一次按键状态,避免抖动对程序的影响。
  • 硬件消抖:在按键电路中加入元器件如电阻、电容组成的 RC 低通滤波器,对按键信号进行平滑处理,降低抖动的影响。

4.2、实验例程

  我们在【components】文件夹下的【device】文件夹中新增了一个 【key】 文件夹,用于存放 key.c 和 key.h 这两个文件。

#ifndef __KEY_H__
#define __KEY_H__

#include "driver/gpio.h"

#include "freertos/FreeRTOS.h"

void key_init(gpio_num_t gpio_num, uint8_t press_level);
bool key_scan(gpio_num_t gpio_num, uint8_t press_level);

#endif // !__KEY_H__
#include "key.h"

/**
 * @brief 按键初始化函数
 * 
 * @param gpio_num GPIO 引脚编号
 * @param press_level 按键按下时的电压电平
 */
void key_init(gpio_num_t gpio_num, uint8_t press_level)
{
    gpio_config_t gpio_config_struct = {0};

    gpio_config_struct.pin_bit_mask = 1ULL << gpio_num;             // 设置引脚
    gpio_config_struct.intr_type = GPIO_INTR_DISABLE;               // 不使用中断
    gpio_config_struct.mode = GPIO_MODE_INPUT;                      // 输入模式
    if (press_level) 
    {
        gpio_config_struct.pull_down_en = GPIO_PULLDOWN_ENABLE;     // 使用下拉
        gpio_config_struct.pull_up_en = GPIO_PULLUP_DISABLE;        // 不使用上拉
    } 
    else 
    {
        gpio_config_struct.pull_up_en = GPIO_PULLUP_ENABLE;         // 使用上拉
        gpio_config_struct.pull_down_en = GPIO_PULLDOWN_DISABLE;    // 不使用下拉
    }
    gpio_config(&gpio_config_struct);                               // 配置GPIO
}

/**
 * @brief 按键扫描函数
 * 
 * @param gpio_num GPIO 引脚编号
 * @param press_level 按键按下时的电压电平
 * @return true 按键按下
 * @return false 按键未按下
 */
bool key_scan(gpio_num_t gpio_num, uint8_t press_level)
{
    if (gpio_get_level(gpio_num) == press_level)
    {
        // 将一个任务延迟给定的滴答数,IDF中提供pdMS_TO_TICKS可以将指定的ms转换为对应的tick数
        vTaskDelay(pdMS_TO_TICKS(10));                              // 延迟消抖
        if (gpio_get_level(gpio_num) == press_level)                // 再次检测
        {
            return true;
        }
    }
  
    return false;
}

  然后,我们修改【components】文件夹下的【device】文件夹下 CMakeLists.txt 文件。

# 源文件路径
set(src_dirs
    led
    key
)

# 头文件路径
set(include_dirs
    led
    key
)

# 设置依赖库
set(requires
    driver
)

# 注册组件到构建系统的函数
idf_component_register(
    # 源文件路径
    SRC_DIRS ${src_dirs}
    # 自定义头文件的路径
    INCLUDE_DIRS ${include_dirs}
    # 依赖库的路径
    REQUIRES ${requires}
)

# 设置特定组件编译选项的函数
# -ffast-math: 允许编译器进行某些可能减少数学运算精度的优化,以提高性能。
# -O3: 这是一个优化级别选项,指示编译器尽可能地进行高级优化以生成更高效的代码。
# -Wno-error=format: 这将编译器关于格式字符串不匹配的警告从错误降级为警告。
# -Wno-format: 这将完全禁用关于格式字符串的警告。
component_compile_options(-ffast-math -O3 -Wno-error=format=-Wno-format)

  接着,我们修改【main】文件夹下的 main.c 文件。

#include "freertos/FreeRTOS.h"

#include "led.h"
#include "key.h"

// app_main()函数是ESP32的入口函数,它是FreRTOS的一个任务,任务优先级是1
// main()函数是C语言入口函数,它会在编译过程中插入到二进制文件中的
void app_main(void)
{   
    led_init(GPIO_NUM_1);
    key_init(GPIO_NUM_2, 0);

    while (1)
    {
        if (key_scan(GPIO_NUM_2, 0))
        {
            led_toggle_status(GPIO_NUM_1);
        }

        // 将一个任务延迟给定的滴答数,IDF中提供pdMS_TO_TICKS可以将指定的ms转换为对应的tick数  
        vTaskDelay(pdMS_TO_TICKS(10));
    }
}
posted @ 2025-03-06 21:27  星光映梦  阅读(473)  评论(0)    收藏  举报