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_num 为 GPIO 引脚号。该参数在 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 的时钟基准没设置好。


四、读取按键输入
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));
}
}

浙公网安备 33010602011771号