2、GPIO基础与LED控制:GPIO寄存器配置、推挽输出与开漏输出、LED闪烁实验
各位同学,欢迎来到第二章。上一章我们把开发环境搭好了,也跑了个点灯程序。但说实话,那个点灯程序你只是复制粘贴,并不知道背后发生了什么。这一章,我们就来扒开GPIO的底裤,看看它到底是怎么工作的。
我个人习惯,学任何外设,第一件事就是看寄存器手册。别看那些封装好的库函数用着爽,一旦遇到奇怪的问题,不懂寄存器你连排查的方向都没有。好,我们开始。
2.1 GPIO寄存器配置——直接操作硬件
ESP32的GPIO,说白了就是一堆寄存器。你往某个地址写个值,引脚的电平就变了。就这么简单。
ESP32的GPIO寄存器主要分几类:
- 输出寄存器:控制引脚输出高电平还是低电平
- 输入寄存器:读取引脚当前的电平状态
- 方向寄存器:设置引脚是输入还是输出
- 功能选择寄存器:把引脚配置成GPIO、UART、SPI等
嗯,这里要注意,ESP32的GPIO不是随便一个引脚都能用的。有些引脚内部有上拉电阻,有些没有。我刚开始做项目时,就踩过这个坑——选了个不能输出的引脚当LED控制脚,折腾了半天。
来看一个最直接的寄存器操作例子:
// 直接操作寄存器点亮LED(假设LED接在GPIO2)
#define GPIO_OUT_REG (*(volatile uint32_t *)0x3FF44004)
#define GPIO_OUT_W1TS_REG (*(volatile uint32_t *)0x3FF44008)
#define GPIO_OUT_W1TC_REG (*(volatile uint32_t *)0x3FF4400C)
#define GPIO_ENABLE_REG (*(volatile uint32_t *)0x3FF44020)
#define GPIO_ENABLE_W1TS_REG (*(volatile uint32_t *)0x3FF44024)
void gpio_init(void) {
// 设置GPIO2为输出模式
GPIO_ENABLE_W1TS_REG = (1 << 2);
}
void led_on(void) {
// 输出高电平
GPIO_OUT_W1TS_REG = (1 << 2);
}
void led_off(void) {
// 输出低电平
GPIO_OUT_W1TC_REG = (1 << 2);
}
你可能会问,为什么不用直接写GPIO_OUT_REG?因为用W1TS(Write 1 to Set)和W1TC(Write 1 to Clear)寄存器,可以只改变某一位,不影响其他引脚的状态。这在多引脚操作时特别有用。
2.2 推挽输出与开漏输出——两种输出模式的区别
很多初学者搞不清推挽和开漏的区别。说白了,就是输出级的结构不同。
| 特性 | 推挽输出 | 开漏输出 |
|---|---|---|
| 输出高电平 | 由内部PMOS管直接驱动 | 需要外部上拉电阻 |
| 输出低电平 | 由内部NMOS管直接驱动 | 由内部NMOS管直接驱动 |
| 驱动能力 | 强,可输出大电流 | 弱,依赖上拉电阻 |
| 典型应用 | 普通LED、蜂鸣器 | I2C总线、多设备共享线 |
推挽输出,就是内部有两个管子,一个推(输出高),一个挽(输出低)。你想想看,这样输出高电平时电流直接从VCC过来,驱动能力很强。我们点LED,用推挽模式就够了。
开漏输出呢?它只有拉低的能力,没有拉高的能力。要输出高电平,必须靠外部上拉电阻。为什么会有人用这么麻烦的模式?因为开漏输出可以实现"线与"功能——多个设备可以共用一个引脚,谁拉低谁就控制总线。I2C总线就是典型的开漏应用。
我在项目中遇到过一个问题:用开漏模式驱动LED,结果LED亮度不够。后来才发现,开漏模式下高电平是靠上拉电阻提供的,电流太小了。换成推挽模式,亮度立马正常。
2.3 LED闪烁实验——从理论到实践
好,理论讲完了,我们来做个LED闪烁实验。这个实验虽然简单,但包含了GPIO配置、延时控制、循环结构等基本要素。
实验要求:LED接在GPIO2,每隔500ms切换一次状态。
先看代码:
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/gpio.h"
#define LED_GPIO GPIO_NUM_2
void app_main(void) {
// 配置GPIO
gpio_config_t io_conf = {
.pin_bit_mask = (1ULL << LED_GPIO),
.mode = GPIO_MODE_OUTPUT,
.pull_up_en = GPIO_PULLUP_DISABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE
};
gpio_config(&io_conf);
while (1) {
gpio_set_level(LED_GPIO, 1); // 点亮
vTaskDelay(500 / portTICK_PERIOD_MS);
gpio_set_level(LED_GPIO, 0); // 熄灭
vTaskDelay(500 / portTICK_PERIOD_MS);
}
}
这段代码里,有几个关键点:
- gpio_config_t结构体:一次性配置引脚的所有属性,比一个个寄存器设置方便多了
- pin_bit_mask:用位掩码指定要配置的引脚,支持同时配置多个引脚
- vTaskDelay:FreeRTOS的延时函数,单位是tick。500ms就是500/portTICK_PERIOD_MS个tick
你可能会问,为什么不用简单的delay循环?因为delay会阻塞CPU,其他任务就没办法运行了。在RTOS环境下,用vTaskDelay会让出CPU给其他任务。
1. 设置引脚掩码(告诉CPU你要操作哪个引脚)
2. 设置模式(输入/输出/开漏等)
3. 调用配置函数(让配置生效)
如果你想让LED闪烁频率更快或更慢,改vTaskDelay的参数就行。比如改成200ms,LED就会闪得更快。
嗯,这里还要提一句,ESP32的GPIO驱动能力是可以配置的。默认是5mA,如果需要更大电流,可以用gpio_set_drive_capability函数调整。不过点个LED,5mA绰绰有余了。
2.4 实验扩展——用按键控制LED
光闪烁太简单了?我们来加点交互。用按键控制LED的亮灭。
#define BUTTON_GPIO GPIO_NUM_0 // 开发板上的BOOT按键
void app_main(void) {
// 配置LED为输出
gpio_config_t led_conf = {
.pin_bit_mask = (1ULL << LED_GPIO),
.mode = GPIO_MODE_OUTPUT
};
gpio_config(&led_conf);
// 配置按键为输入,带上拉
gpio_config_t btn_conf = {
.pin_bit_mask = (1ULL << BUTTON_GPIO),
.mode = GPIO_MODE_INPUT,
.pull_up_en = GPIO_PULLUP_ENABLE
};
gpio_config(&btn_conf);
while (1) {
int level = gpio_get_level(BUTTON_GPIO);
if (level == 0) { // 按键按下(低电平)
gpio_set_level(LED_GPIO, 1);
} else {
gpio_set_level(LED_GPIO, 0);
}
vTaskDelay(10 / portTICK_PERIOD_MS); // 消抖
}
}
这里要注意,按键按下时电平是低还是高,取决于你的电路设计。开发板上的BOOT按键,按下时是低电平,所以代码里判断level==0时点亮LED。
好了,这一章的内容就到这里。GPIO是嵌入式开发的基础,看似简单,但里面的门道不少。推挽和开漏的区别,寄存器操作的细节,都是你以后做项目时绕不开的知识点。
下一章,我们会讲定时器,到时候就能做出更精确的延时和控制了。记得动手把今天的实验做一遍,光看代码是学不会的。
- GPIO寄存器包括输出、输入、方向、功能选择等类型
- 推挽输出驱动能力强,适合LED等负载
- 开漏输出需要上拉电阻,适合总线共享
- LED闪烁实验是GPIO输出的经典入门案例
- 按键输入要注意消抖处理