【ESP32笔记】LED操作和呼吸灯实现

本节目标:掌握 GPIO 输出操作 → 理解 PWM 脉宽调制 → 用 LEDC 硬件模块实现呼吸灯

一、本节要解决的问题

嵌入式学习的两步传统:第一步点亮 Helloworld(串口打印),第二步点亮 LED。本节做两件事:

  1. 点亮并闪烁一个 LED ------ 最基础的 GPIO 高低电平输出;
  2. 让 LED 亮度渐变(呼吸灯) ------ GPIO 只能输出高/低两种电平,无法输出"一半亮",必须借助 PWM 脉宽调制 和 ESP32 的 LEDC 硬件外设。

二、硬件基础

LED 的驱动电路非常简单:GPIO27 → 限流电阻 → LED → GND。

  • GPIO 输出 高电平 3.3V → 回路中有电流 → LED 点亮;
  • GPIO 输出 低电平 0V → 无电流 → LED 熄灭。

注意 :教程中开发板的 LED 接在 GPIO27 。而当前仓库 ledc/main/ledc.c 里写的是 #define LED_GPIO 15,这是本地改动过的值(该工程同时被切到了 esp32s3 目标)。实际接线以自己板子的原理图为准,esp32开发板资料/2、开发板原理图/ESP32开发板原理图1.1.pdf 是权威依据。

三、知识点

3.1 GPIO 的四种常见工作模式

模式 说明
输出 可以设置 GPIO 的高低电平
输入 可以获取外部输入的高低电平,通常需要配置上拉/下拉电阻
浮空输出 可以设置高低电平,但需要在电路外部加上拉电阻
开漏输入 可以获取外部输入的高低电平,但需要在电路外部加上拉电阻

本节用的是输出模式,并且把上拉、下拉、中断全部关闭。

3.2 PWM 与占空比

如果直接用 GPIO 输出脉冲,灯会一闪一闪。当脉冲切换频率高到约 25Hz 以上时,人眼就分辨不出闪烁了,只会感觉到"灯变暗了"。

暗多少由 占空比(高电平时间占整个 PWM 周期的百分比)决定:

  • 占空比 0% → 全灭;50% → 半亮;100% → 全亮(等同于一直输出高电平);
  • 动态改变占空比,就能看到灯从暗到亮、从亮到暗------这就是呼吸灯。

四、代码分析

4.1 工程结构

复制代码
ledc/
├── CMakeLists.txt
├── main/
│   ├── CMakeLists.txt      # idf_component_register(SRCS "main.c" "ledc.c" REQUIRES driver freertos)
│   ├── main.c              # app_main(),选择运行哪个 demo
│   ├── ledc.c              # 本节全部实现
│   └── ledc.h              # 对外声明两个初始化函数
└── sdkconfig

两个演示函数都声明在 ledc.h 中,app_main() 里注释掉一个、留下一个来切换:

c 复制代码
// 主函数
void app_main(void)
{
	//led_flash_init();     //简单led闪烁
    led_breath_init();      //呼吸灯
}

4.2 闪烁:GPIO 初始化 + 任务

c 复制代码
//定义LED的GPIO口
#define LED_GPIO  15

//LED闪烁初始化
void led_flash_init(void)
{
    gpio_config_t led_gpio_cfg = {
        .pin_bit_mask = (1<<LED_GPIO),          //指定GPIO
        .mode = GPIO_MODE_OUTPUT,               //设置为输出模式
        .pull_up_en = GPIO_PULLUP_DISABLE,      //禁止上拉
        .pull_down_en = GPIO_PULLDOWN_DISABLE,  //禁止下拉
        .intr_type = GPIO_INTR_DISABLE,         //禁止中断
    };
    gpio_config(&led_gpio_cfg);

    xTaskCreatePinnedToCore(led_run_task,"led",2048,NULL,3,NULL,1);
}

流程是:填 gpio_config_t 结构体 → 调用 gpio_config() 把配置写入底层 → 建一个任务循环翻转电平。

c 复制代码
//LED闪烁运行任务
void led_run_task(void* param)
{
    int gpio_level = 0;
    while(1)
    {
        vTaskDelay(pdMS_TO_TICKS(500));
        gpio_set_level(LED_GPIO,gpio_level);
        gpio_level = gpio_level?0:1;
    }
}

gpio_level 在 0/1 之间来回取反,配合 500ms 延时,形成 1Hz 的闪烁。

4.3 呼吸灯:LEDC 初始化

LEDC 需要初始化两个对象,缺一不可:

  • ledc_timer_config_t:配置定时器,决定 PWM 频率与占空比分辨率;
  • ledc_channel_config_t:配置输出通道,并把通道关联到上面那个定时器。
c 复制代码
#define LEDC_TIMER              LEDC_TIMER_0            //定时器0
#define LEDC_MODE               LEDC_LOW_SPEED_MODE     //低速模式
#define LEDC_OUTPUT_IO          (LED_GPIO)              //选择GPIO端口
#define LEDC_CHANNEL            LEDC_CHANNEL_0          //PWM通道
#define LEDC_DUTY_RES           LEDC_TIMER_13_BIT       //分辨率
#define LEDC_DUTY               (4095)                  //最大占空比值,这里是2^13-1
#define LEDC_FREQUENCY          (5000)                  //PWM周期
c 复制代码
//LED呼吸灯初始化
void led_breath_init(void)
{
    //初始化一个定时器
    ledc_timer_config_t ledc_timer = {
        .speed_mode       = LEDC_MODE,      //低速模式
        .timer_num        = LEDC_TIMER,     //定时器ID
        .duty_resolution  = LEDC_DUTY_RES,  //占空比分辨率,这里是13位,2^13-1
        .freq_hz          = LEDC_FREQUENCY,  // PWM频率,这里是5KHZ
        .clk_cfg          = LEDC_AUTO_CLK    // 时钟
    };
    ESP_ERROR_CHECK(ledc_timer_config(&ledc_timer));

    //ledc通道初始化
    ledc_channel_config_t ledc_channel = {
        .speed_mode     = LEDC_MODE,        //低速模式
        .channel        = LEDC_CHANNEL,     //PWM 通道0-7
        .timer_sel      = LEDC_TIMER,       //关联定时器,也就是上面初始化好的那个定时器
        .intr_type      = LEDC_INTR_DISABLE,//不使能中断
        .gpio_num       = LEDC_OUTPUT_IO,   //设置输出PWM方波的GPIO管脚
        .duty           = 0, // 设置默认占空比为0
        .hpoint         = 0
    };
    ESP_ERROR_CHECK(ledc_channel_config(&ledc_channel));
    ...

分辨率与占空比的关系 :13 位分辨率意味着占空比取值是 0 ~ 2¹³−1 = 0 ~ 4095,4095 即 100%。分辨率越高,亮度分级越细腻,但同样频率下占空比可调范围越小。

接下来开启渐变功能并注册回调:

c 复制代码
    //开启硬件PWM
    ledc_fade_func_install(0);

    //创建一个事件组,用于通知任务渐变完成
    s_ledc_ev = xEventGroupCreate();

    //配置LEDC渐变
    ledc_set_fade_with_time(LEDC_MODE,LEDC_CHANNEL,LEDC_DUTY,2000);

    //启动渐变
    ledc_fade_start(LEDC_MODE,LEDC_CHANNEL,LEDC_FADE_NO_WAIT);

    //设置渐变完成回调函数
    ledc_cbs_t cbs = {.fade_cb=ledc_finish_cb,};
    ledc_cb_register(LEDC_MODE,LEDC_CHANNEL,&cbs,NULL);

    xTaskCreatePinnedToCore(ledc_breath_task,"ledc",2048,NULL,3,NULL,1);
}

两个关键函数的区别:

  • ledc_set_fade_with_time(模式, 通道, 目标占空比, 时长ms):设定"在 2000ms 内渐变到 4095";
  • ledc_fade_start(..., LEDC_FADE_NO_WAIT):启动 渐变,NO_WAIT 表示立刻返回不阻塞,所以必须另想办法知道"渐变什么时候结束"------答案就是回调。

4.4 渐变完成通知:中断回调 → 事件组 → 任务

回调函数在中断上下文中执行,不能做耗时操作,因此它只负责"打个标记":

c 复制代码
//用于通知渐变完成
static EventGroupHandle_t   s_ledc_ev = NULL;

//关灯完成事件标志
#define LEDC_OFF_EV  (1<<0)

//开灯完成事件标志
#define LEDC_ON_EV   (1<<1)

//渐变完成回调函数
bool IRAM_ATTR ledc_finish_cb(const ledc_cb_param_t *param, void *user_arg)
{
    BaseType_t xHigherPriorityTaskWoken;
    if(param->duty)
    {
        xEventGroupSetBitsFromISR(s_ledc_ev,LEDC_ON_EV,&xHigherPriorityTaskWoken);
    }
    else
    {
        xEventGroupSetBitsFromISR(s_ledc_ev,LEDC_OFF_EV,&xHigherPriorityTaskWoken);
    }
    return xHigherPriorityTaskWoken;
}

IRAM_ATTR 把函数放进 IRAM,保证中断期间一定能执行到;在中断里操作事件组必须用 xEventGroupSetBitsFromISR(带 FromISR 后缀的版本),并通过 xHigherPriorityTaskWoken 告知内核是否需要任务切换。

任务侧等待这个事件,并反向启动下一次渐变:

c 复制代码
//ledc 渐变任务
void ledc_breath_task(void* param)
{
    EventBits_t ev;
    while(1)
    {
        ev = xEventGroupWaitBits(s_ledc_ev,LEDC_ON_EV|LEDC_OFF_EV,pdTRUE,pdFALSE,pdMS_TO_TICKS(5000));
        if(ev)
        {
            //设置LEDC开灯渐变
            if(ev & LEDC_OFF_EV)
            {
                ledc_set_fade_with_time(LEDC_MODE,LEDC_CHANNEL,LEDC_DUTY,2000);
                ledc_fade_start(LEDC_MODE,LEDC_CHANNEL,LEDC_FADE_NO_WAIT);
            }
            else if(ev & LEDC_ON_EV)    //设置LEDC关灯渐变
            {
                ledc_set_fade_with_time(LEDC_MODE,LEDC_CHANNEL,0,2000);
                ledc_fade_start(LEDC_MODE,LEDC_CHANNEL,LEDC_FADE_NO_WAIT);
            }
            //再次设置回调函数
            ledc_cbs_t cbs = {.fade_cb=ledc_finish_cb,};
            ledc_cb_register(LEDC_MODE,LEDC_CHANNEL,&cbs,NULL);
        }
    }
}

逻辑闭环:关灯渐变结束(占空比=0)→ 触发 OFF 事件 → 任务启动"亮到 4095"的渐变 → 结束后触发 ON 事件 → 任务再启动"暗到 0"的渐变,如此往复,形成呼吸效果。

五、关键 API 速查

API 作用
gpio_config() 按 gpio_config_t 配置引脚模式、上下拉、中断
gpio_set_level(pin, level) 设置引脚输出电平
ledc_timer_config() 配置 LEDC 定时器(频率、占空比分辨率)
ledc_channel_config() 配置 LEDC 通道并关联定时器、绑定 GPIO
ledc_fade_func_install(0) 使能硬件渐变功能(用渐变前必须调用)
ledc_set_fade_with_time() 设定目标占空比与渐变时长
ledc_fade_start() 启动渐变,LEDC_FADE_NO_WAIT 为立即返回
ledc_cb_register() 注册渐变完成回调
xEventGroupCreate() / xEventGroupWaitBits() 事件组的创建与等待(任务上下文)
xEventGroupSetBitsFromISR() 事件组置位(中断上下文专用)

六、编译与现象验证

bash 复制代码
cd esp32-board/ledc
idf.py set-target esp32s3      # 该工程当前目标是 esp32s3
idf.py build
idf.py -p COM<端口> flash monitor
  • 调用 led_flash_init() 时:LED 以 1Hz 闪烁(亮 500ms、灭 500ms);
  • 调用 led_breath_init() 时:LED 在 2 秒内由亮渐暗,再用 2 秒由暗渐亮,循环不止。
相关推荐
PMAOS6 小时前
4G 功能机操作系统选型指南:KaiOS、Mocor、MAUI、PMAOS 的能力边界与验证方法
嵌入式硬件·ai·操作系统
无心水6 小时前
20、大数据架构设计理论与实践 核心考点与学习笔记
大数据·笔记·学习·架构师·系统架构设计师·软考系统架构设计师·架构设计师
mk0156 小时前
单片机同步降压稳压器输出最低1V,关断时损耗3uA
单片机·嵌入式硬件·硬件工程·智能硬件·pcb工艺
snysy7 小时前
WordPress伪静态404故障排查笔记
android·笔记·wordpress
Z21-X7 小时前
云原生运维实战笔记:华为云计算与网络类服务全景解析
运维·笔记·学习·云原生·华为云
半壶清水7 小时前
[软考网规考点笔记]-局域网之冗余网关技术
网络·笔记·智能路由器
LK_077 小时前
【Agent】工具调用的安全边界:从模型输出到程序执行
人工智能·笔记·python·agent
乐橙开放平台7 小时前
跨地域监控难管理?通过 GB28181 把各地海康大华乐橙摄像头注册到云端平台
开发语言·笔记·物联网·自动化·php·音视频·智能家居
动词ing7 小时前
【学习笔记】51单片机 模块化编程
笔记·学习·51单片机
然后,是第八天7 小时前
platformio使用使用pioarduino流程
单片机