【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 秒由暗渐亮,循环不止。
相关推荐
夏虫不可与之言冰1 小时前
使用Flash Download Tool给ESP32烧录固件
c语言·c++·windows·单片机·物联网·iot
zjxtxdy1 小时前
STM32hal库开发-GPIO
stm32·单片机·嵌入式硬件
zbyyd1 小时前
Linux字符设备驱动:LED驱动实战开发
linux·单片机·arm·驱动
碧海银沙音频科技研究院5 小时前
炬芯ATS3089X手表的HFP通话不自动路由到设备BT的问题解决
网络·人工智能·嵌入式硬件·算法
youcans_10 小时前
【STM32C5教程】(3)GPIO 按键输入实验
stm32·单片机·嵌入式硬件
俊基科技11 小时前
回音消不掉、底噪压不住、主板还对不上口?——A-59P 多功能语音处理模组技术解析
嵌入式硬件·回音消除·ai降噪模块
滕州市燕猫虎计算机科技工作室个体工商户11 小时前
《Rust程序设计》学习笔记八:结构体
笔记·学习·rust·rust程序设计
北京盛世宏博12 小时前
通信异常深度排查:以太网温湿度设备 TCP 粘包、UDP 丢包根治方案
单片机·网络协议·tcp/ip·udp·以太网·传感器·温湿度
志尊宝13 小时前
Vue3 零基础每日笔记(082):文件上传实战——el-upload 校验、自定义上传、头像预览
前端·javascript·vue.js·笔记·#javascript·#vue.js