ESP32C3打造小爱夜灯终极版

自从上次将ESP32C3接入米家,一直有用它做个小夜灯的想法。所以我的目标就是做一个能用小爱同学控制的小夜灯。我最终实现了预期目标,并增加了亮度控制、色调控制、模式控制的功能。本文会详细描述硬件构成及进行完整源码讲解。

在这里插入图片描述

前言

最近测试了ESP32C3接入米家,成功用小爱同学控制板载LED开关。一直在寻找能塞入ESP32C3的小夜灯,或者计划自己画个C3的板子,实现用小爱同学控制真正的灯。

直到我猛地发现,家里有个闲置的语音声控灯(以下简称环形灯),直接用它改装不就好了。它被抛弃的原因是语音识别不准,经常误触,平时说话都会误识别开关灯。

而且用这个灯改还有几个好处:壳子大,能直接塞入ESP32C3核心板;有两种颜色的灯,那我就可以搞出不同颜色,不同模式

这里首先放出最终的演示效果,详见B站:没人要的环形灯接入米家,用小爱同学控制开关灯,调亮度,调模式
在这里插入图片描述
拆开比划了一下,别说,正好可以放入ESP32C3的板子,而且还是单层板。那直接改装做小夜灯好了。
在这里插入图片描述

一、环形灯参数测量

灯正常工作时是4.98V,0.2A,差不多是1W,耗电量不大。
在这里插入图片描述
在这里插入图片描述

LED

在这里插入图片描述
测量了一下这个环型灯,分暖光和白光两种,各8颗,每种颜色的灯单独并联,然后两种颜色的灯的正极经过10Ω(三个电阻并联后的值,R3~R5)与电源正极相连,两组灯的负极各与一个mos相连,所以这个模块是IC通过mos分别控制这两组灯负极的通断,来实现开关。

测量可知,最亮时,灯的压降是2.82V。

这样就清楚了,灯的工作电压是3V左右,灯正极的10Ω串联电阻为限流电阻。

但是注意到限流电阻是3个0402封装,单个62.5mW,三个并联是0.1875W。实测工作时并联电阻压降1.75V,那电流就是1.75/10=0.175A,功率为1.75x0.175=0.31W,这严重超功率了,莫非是厂家故意不小心安排的?一种定期报废的手段???这三个电阻算是这个产品的大雷了。。。

同时注意到这三个限流电阻旁边有个0603空焊盘,也是并联在一起的。

考虑到这个灯是亮度可调,所以应该是PWM控制,所以实际工作时这些限流电阻不是一直有电流的,这应该是他暂时能用的原因。假如我将他接入米家,假如采用IO控制而非PWM控制,那开灯时就是一直导通,限流电阻必坏,真是可恶。。。
在这里插入图片描述
这种情况用三个0805封装的30Ω电阻并联才比较好,然后才能接入米家。

由于我没这个电阻,所以要结合灯的压降2.82V来考虑新的电阻。

假设电源电压5V,不考虑亮度,那限流电阻压降是2.18V,采用三个0402+一个0603则最低要求16.5Ω。

仅仅补焊一个0603电阻不切实际,并联电阻会永远低于10Ω。所以同时要增大0402里面的电阻。

我查了一下我的电阻,0402封装的最小是470Ω,所以要将三个0402里面的电阻换成0402 470Ω,然后补焊一个100Ω 0603电阻即可,此时并联电阻是21Ω,满足需求。

计算一下,5-2.82=2.18V,2.182.18/21=0.23W,0.06253+0.1=0.29W>0.23W,完全没问题。

mos

在这里插入图片描述
mos是杰盛微的2302A,丝印为A2SHB,为nmos,漏源电压20V,漏极电流3A,所以mos的选型是合理的。两组灯的负极各与一个mos漏极相连,mos源极直接接地,两个mos的栅极各有一个100Ω电阻下拉到地。

实测栅极电压最高1.23V,两个电阻日常损耗就是1.23*1.23/50=0.03W,如果采用10k会比较省电。

两个mos栅极直接和IC相连,这里标注一下,Q1控制暖光,栅极接IC第9脚;Q2控制白光,接IC第10脚。

麦克风

在这里插入图片描述
除此了LED,还有个麦克风,麦克风负极接地,正极接IC第13脚,型号未知。

IC

在这里插入图片描述
好了,到主控IC了,这里理一下。丝印BP0A080,sop-16封装,查不到这个型号。推测是改过丝印的。这种芯片归类为音频接口芯片,比较常用的是灵星芯微的PT2399,以及友顺的TEA1062。

回到这款芯片,整理一下引脚情况如下:

引脚功能
1NC
2VCC,接C6到地,接4.7Ω到电源
3接C5到地
4接C1到地
5未知
6未知
7NC
8NC
9Q1栅极(暖)
10Q2栅极(白)
11NC
12NC
13麦克风正极,接电容C2
14接电容C2
15GND
16接电容C3到地

二、改装方案

从上面的测量计算数据可知,首先要改限流电阻和mos下拉电阻,其次是拆掉IC,将IC的9和10脚接入ESP32C3的IO口,将IC的GND接入ESP32C3的GND,灯板5V接入ESP32C3的USB 5V。

限流电阻

将3个限流电阻中的两个换成470Ω 0402(R3、R4、R5中的两个),补焊100Ω 0603(R12楼)。

下拉电阻

mos下拉电阻(R1、R2)换成10K 0402。

拆IC

优先用加热台,怕破坏LED。

连接ESP32C3

在这里插入图片描述
在这里插入图片描述
这里需要寻找支持PWM输出的引脚,从ESP32C3的规格书来看[3],所有的GPIO都支持PWM输出,需要做的是配置LED PWM控制器。

同时LED是nmos控制,高电平开,这样有利于程序里面直接调节亮度,脉宽越大、频率越高就越亮。

于是核心板的9和10号可以做为GPIO,对应灯板上IC位的P9和P10,分别是控制Q1栅极(暖)、Q2栅极(白):

灯板ESP32C3功能EN状态
5V5V5V/
GNDGNDGND/
IC-P9IO9Q1栅极(暖)H
IC-P10IO10Q2栅极(白)H

三、改装过程记录

硬件上的变更为调整限流电阻,去掉旧的主控芯片,引出两路LED控制端,引出供电。

改限流电阻

原以为3个限流电阻是30Ω,才会导致并联电阻为10Ω。实际用热台拆解时才发现,三个电阻已经坏了两个,如下图,引脚到碳膜已经熔断,只有一个10Ω 0402品质较好在一直支撑着。。。

所以他原计划的限流电阻是3.3Ω。
在这里插入图片描述
剩下的那个10Ω 0402虽然万用表测试OK,但是不敢用。于是可以这样,4个100Ω 0603叠在一起并联,那就是0.4W 25Ω,足够了。

限流电阻实际我焊了5个100Ω 0603和一个22Ω 0603,并联电阻为10.5Ω,方式比较抽象,4个叠焊,两侧再各焊一个。
在这里插入图片描述

连接ESP32与灯板

在这里插入图片描述
如第二节改装方案里所述,引出灯板需要连接到ESP32C3上面的线,一共4根:

灯板ESP32C3功能EN状态颜色
5V5V5V/橙色
GNDGNDGND/棕色
IC-P9IO9Q1栅极(暖)H白色
IC-P10IO10Q2栅极(白)H绿色

完事如下图所示:
在这里插入图片描述
在确认线连接没问题且编程反复测试OK后,线焊接处打上热熔胶,防止掉焊盘。
在这里插入图片描述

四、编程调试

程序就用上次小爱控制ESP32C3板载LED的程序魔改,尽可能多的接入点灯科技能接入的接口。

点灯科技小爱接口大全:小米小爱接入

点灯科技函数大全(Arduino库函数):blinker Arduino支持库
在这里插入图片描述

实测效果

实测效果完全符合要求,还超出了预期效果。
在这里插入图片描述
首先通过小爱同学,能实现灯的开关,还能改变灯的亮度,改变灯的色温,以及切换不同的照明模式。同时在点灯科技的app里也可以通过滑块控制灯的亮灭、亮度、色温控制。
在这里插入图片描述
在这里插入图片描述

完整源码

话不多说,上最终调试好的源码,版本1.0,可以说是全篇的精华:

/* *****************************************************************
 *
 * Download latest Blinker library here:
 * https://github.com/blinker-iot/blinker-library/archive/master.zip
 * 
 * 
 * Blinker is a cross-hardware, cross-platform solution for the IoT. 
 * It provides APP, device and server support, 
 * and uses public cloud services for data transmission and storage.
 * It can be used in smart home, data monitoring and other fields 
 * to help users build Internet of Things projects better and faster.
 * 
 * Make sure installed 2.7.4 or later ESP8266/Arduino package,
 * if use ESP8266 with Blinker.
 * https://github.com/esp8266/Arduino/releases
 * 
 * Make sure installed 1.0.5 or later ESP32/Arduino package,
 * if use ESP32 with Blinker.
 * https://github.com/espressif/arduino-esp32/releases
 * 
 * Docs: https://diandeng.tech/doc
 *       
 * 
 * *****************************************************************
 * 
 * Blinker 库下载地址:
 * https://github.com/blinker-iot/blinker-library/archive/master.zip
 * 
 * Blinker 是一套跨硬件、跨平台的物联网解决方案,提供APP端、设备端、
 * 服务器端支持,使用公有云服务进行数据传输存储。可用于智能家居、
 * 数据监测等领域,可以帮助用户更好更快地搭建物联网项目。
 * 
 * 如果使用 ESP8266 接入 Blinker,
 * 请确保安装了 2.7.4 或更新的 ESP8266/Arduino 支持包。
 * https://github.com/esp8266/Arduino/releases
 * 
 * 如果使用 ESP32 接入 Blinker,
 * 请确保安装了 1.0.5 或更新的 ESP32/Arduino 支持包。
 * https://github.com/espressif/arduino-esp32/releases
 * 
 * 文档: https://diandeng.tech/doc
 *       
 * 功能:环形灯接入米家
 * 编写于:2025年3月8日,最终测试通过,by:hpl
 * 本程序的AI协助者:DeepSeek-V3
 * 参考资料:https://diandeng.tech/doc/arduino-support-next#blinkerbegin、https://www.nologo.tech/product/esp32/esp32c3/esp32c3supermini/esp32C3SuperMini.html、
 *        https://blog.csdn.net/weixin_45695902/article/details/125709676、https://blog.csdn.net/weixin_51358957/article/details/137921369、
 *        https://blog.csdn.net/weixin_46364710/article/details/122435587
 * 
 * 
 * *****************************************************************/

#define BLINKER_PRINT Serial  //  打开串口调试
#define BLINKER_WIFI          //  WiFi接入模式
#define BLINKER_MIOT_LIGHT    //  小爱同学设备灯光
// #define BLINKER_ESP_TASK      //  ESP32多任务(会引起不联网,不建议使用)

#include <Blinker.h>               // 包含Blinker库,必须在BLINKER相关定义后包含

// 硬件引脚定义
#define LED_Blue 8 // 板载蓝色LED
#define LED_Warm 9 // 环形灯暖光(低色温)
#define LED_Cold 10  //环形灯白光(高色温)

// PWM通道配置(ESP32专用)
#define PWM_CHANNEL_BLUE 0         // 蓝灯PWM通道
#define PWM_CHANNEL_COLD 1         // 冷光PWM通道
#define PWM_CHANNEL_WARM 2         // 暖光PWM通道
#define PWM_FREQ 5000              // PWM频率
#define PWM_RESOLUTION 8           // PWM分辨率(8位对应0-255)

// 全局变量
float colorW = 0;                                       // 当前亮度(0-100)
int colorT = 0;                                         // 当前色温(0-100)
int cold;                                               //  白光输出值
int warm;                                               //  黄光输出值
bool ledState;                                          // 灯光开关状态
static TaskHandle_t TaskBlink_Handle = NULL;            //  任务句柄
bool wifiInit = false;                                  // 板子初始化状态标志
uint8_t wsMode = 7;                                     // 初始化默认模式,手动模式
String version = "1.0";                                 // 定义版本
String author = "hpl";                                  // 定义作者

// 账号配置
char auth[] = "Your Blinker device Key";
char ssid[] = "WiFi ssid";
char pswd[] = "WiFi pswd";

// 新建组件对象
BlinkerButton Button1("btn-restart");           //  重启ESP32按键组件
BlinkerButton Button2("btn-state");             //  获取状态按键组件
BlinkerSlider Slider1("bar-brightness");        //  亮度滑动条组件
BlinkerSlider Slider2("bar-colortemp");         //  色温滑动条组件

// 函数声明
void TaskBlink(void *pvParameters);
void BlinkLED(int32_t blinktime);

/*----------------------- 回调函数定义 -----------------------*/
// 按下button1按键即会执行该函数
void button1_callback(const String & state)
{
    BLINKER_LOG("get button state: ", state);
    Blinker.print("1s后重启...");
    Blinker.delay(1000);
    ESP.restart();
}

// 按下button2按键即会执行该函数
// 目的是上传当前亮度、色温和模式,同时闪烁板载LED_Blue
void button2_callback(const String & state)
{
    // 创建一个线程(要运行的函数的名称,函数备注,任务栈,任务参数,优先级(0是最低),任务句柄)
    xTaskCreate(TaskBlink, "Task Blink", 2048, (void *) PWM_CHANNEL_BLUE, 0, &TaskBlink_Handle);
    time_t run_time = Blinker.runTime();  // 获取运行时间,单位s
    Blinker.print("版本", version);
    Blinker.print("作者", author);
    Blinker.print("运行时间", run_time, "s");
    Blinker.delay(102); // 两段消息的最小间隔是100ms
    Blinker.print("亮度", colorW, "%");
    Blinker.print("色温", colorT, "%");
    switch(wsMode)
    {
      case 0:
        Blinker.print("模式", "日光模式");
        break;
      case 1:
        Blinker.print("模式", "月光模式");
        break;
      case 2:
        Blinker.print("模式", "彩光模式");
        break;
      case 3:
        Blinker.print("模式", "温馨模式");
        break;
      case 4:
        Blinker.print("模式", "电视模式");
        break;
      case 5:
        Blinker.print("模式", "阅读模式");
        break;
      case 6:
        Blinker.print("模式", "电脑模式");
        break;     
      case 7:
        Blinker.print("模式", "手动模式");
      default:
        break; 
    }
    BlinkLED(1000);
}

// 如果未绑定的组件被触发,则会执行其中内容
void dataRead(const String & data)
{
    BLINKER_LOG("Blinker readString: ", data);
}

//  亮度滑动条回调函数
void Slider1_callback(int32_t value) {

    BLINKER_LOG("亮度设置: ", value);
    colorW = value;
    wsMode = 7; // 手动调节亮度,标记为手动模式
}

//  色温滑动条回调函数
void Slider2_callback(int32_t value2) {

    BLINKER_LOG("色温设置: ", value2);
    colorT = value2;
    wsMode = 7; // 手动调节色温,标记为手动模式
}

// 小爱电源类的操作接口
void miotPowerState(const String & state)
{
    BLINKER_LOG("电源状态:", state);

    // 接收到开灯指令,亮度调到最大
    if (state == BLINKER_CMD_ON) {
        colorW = 100;
        ledState = true;
        BlinkerMIOT.powerState("on");
        BlinkerMIOT.print();
    } // 接收到关灯指令,亮度调为最低
    else if (state == BLINKER_CMD_OFF) {
        colorW = 0;
        ledState = false;
        BlinkerMIOT.powerState("off");
        BlinkerMIOT.print();
    }
    wsMode = 7; // 通过小爱开关灯,标记为手动模式
}

//  小爱亮度控制的回调函数
void miotBright(const String & bright)
{
    colorW = bright.toInt();
	  Slider1.print(colorW);
    BLINKER_LOG("亮度更新: ", colorW);
    BlinkerMIOT.brightness(colorW);
    BlinkerMIOT.print();

    wsMode = 7; // 通过小爱改变亮度,标记为手动模式
}

// 小爱设备状态查询接口回调函数
void miotQuery(int32_t queryCode)
{
    BLINKER_LOG("MIOT查询代码: ", queryCode);
    if (queryCode == 0) {
        BLINKER_LOG("查询亮度 : ", colorW);
			  BLINKER_LOG("查询色温 : ", colorT);
        BlinkerMIOT.brightness(colorW);                             //  反馈灯的亮度
        BlinkerMIOT.colorTemp(map(colorT, 0, 100, 1000, 10000));    //  反馈灯的色温
        BlinkerMIOT.powerState(ledState ? "on" : "off");            //  反馈灯的亮灭
        BlinkerMIOT.print();
    }
}

//  小爱色温回调函数
void miotColoTemp(int32_t colorTemp)
{
    colorT = map(colorTemp, 1000, 10000,  0,  100); // 映射函数,map(value, fromLow, fromHigh, toLow, toHigh)
    BLINKER_LOG("色温转换: ", colorT);
	  Slider2.print(colorT);
    BlinkerMIOT.colorTemp(colorTemp);
    BlinkerMIOT.print();

    wsMode = 7; // 通过小爱改变色温,标记为手动模式
}

// 小爱模式的设置接口回调函数
void miotMode(uint8_t mode)
{
    BLINKER_LOG("need set mode: ", mode);

    if (mode == BLINKER_CMD_MIOT_DAY) {
        // 日光,Your mode function
        colorW = 100;
        colorT = 0;
    }
    else if (mode == BLINKER_CMD_MIOT_NIGHT) {
        // 月光,Your mode function
        colorW = 20;
        colorT = 100;
    }
    else if (mode == BLINKER_CMD_MIOT_COLOR) {
        // 彩光,Your mode function
        colorW = 50;
        colorT = 50;
    }
    else if (mode == BLINKER_CMD_MIOT_WARMTH) {
        // 温馨,Your mode function
        colorW = 50;
        colorT = 0;
    }
    else if (mode == BLINKER_CMD_MIOT_TV) {
        // 电视,Your mode function
        colorW = 30;
        colorT = 70;
    }
    else if (mode == BLINKER_CMD_MIOT_READING) {
        // 阅读,Your mode function
        colorW = 100;
        colorT = 50;
    }
    else if (mode == BLINKER_CMD_MIOT_COMPUTER) {
        // 电脑,Your mode function
        colorW = 30;
        colorT = 50;
    }

    wsMode = mode;

    BlinkerMIOT.mode(mode);
    BlinkerMIOT.print();
}

/*----------------------- 灯光控制函数 -----------------------*/
//  灯光控制函数,分别是亮度,色温(色温越高,蓝光越多)
void LedControl(float brightness, int colorTemp)
{
    // 计算双色灯亮度值(0-255)
    int cold = (brightness * colorTemp * 255) / 10000;  // 控制冷光
    int warm = (brightness * (100 - colorTemp) * 255) / 10000;  // 控制暖光
	
    // 输出PWM信号,ledcWrite不起效果,analogWrite可正常使用
    // ledcWrite(PWM_CHANNEL_COLD, cold);
    // ledcWrite(PWM_CHANNEL_WARM, warm);
    analogWrite(LED_Cold, cold);
    analogWrite(LED_Warm, warm);
}

/*----------------------- 线程执行LED_Blue闪烁函数 -----------------------*/
// 任务函数,在线程中执行,闪烁LED具体实现方式
void TaskBlink(void *pvParameters)
{
  pinMode(LED_Blue, OUTPUT);

  while(1)
  {
    // digitalWrite(LED_Blue, !digitalRead(LED_Blue));
    // fade in from min to max in increments of 5 points:
    for (int fadeValue = 0 ; fadeValue <= 255; fadeValue += 10) {
      // sets the value (range from 0 to 255):
      analogWrite(LED_Blue, fadeValue);
      // ledcWrite(PWM_CHANNEL_BLUE, fadeValue);  // ledcWrite不起效果,analogWrite可正常使用
      // wait for 30 milliseconds to see the dimming effect
      delay(1);
    }

    // fade out from max to min in increments of 5 points:
    for (int fadeValue = 255 ; fadeValue >= 0; fadeValue -= 10) {
      // sets the value (range from 0 to 255):
      analogWrite(LED_Blue, fadeValue);
      // ledcWrite(PWM_CHANNEL_BLUE, fadeValue);
      // wait for 30 milliseconds to see the dimming effect
      delay(1);
    }
    
  }
}

//  闪烁板载LED,blinktime控制闪烁时间,单位ms
void BlinkLED(int32_t blinktime)
{
  vTaskDelay(blinktime / portTICK_PERIOD_MS);                               // 延时
  if (TaskBlink_Handle != NULL)                                        // 如果TaskBlink_Handle不为空就删除任务
  {
      vTaskDelete(TaskBlink_Handle);
  }
  analogWrite(LED_Blue, 0); // 闪烁结束,LED_Blue恢复长亮
}

/*----------------------- 初始化配置 -----------------------*/
void setup()
{
    // 初始化串口
    Serial.begin(115200);
    BLINKER_DEBUG.stream(Serial); //串口打印调试信息
    BLINKER_DEBUG.debugAll();

    // 初始化PWM
    ledcSetup(PWM_CHANNEL_BLUE, PWM_FREQ, PWM_RESOLUTION);
    ledcAttachPin(LED_Blue, PWM_CHANNEL_BLUE);
    ledcSetup(PWM_CHANNEL_COLD, PWM_FREQ, PWM_RESOLUTION);
    ledcAttachPin(LED_Cold, PWM_CHANNEL_COLD);
    ledcSetup(PWM_CHANNEL_WARM, PWM_FREQ, PWM_RESOLUTION);
    ledcAttachPin(LED_Warm, PWM_CHANNEL_WARM);

    // 初始化LED
    pinMode(LED_Blue, OUTPUT);
    pinMode(LED_Cold, OUTPUT);
    pinMode(LED_Warm, OUTPUT);
    digitalWrite(LED_Blue, LOW);
    digitalWrite(LED_Cold, LOW);
    digitalWrite(LED_Warm, LOW);
    
    // 初始化blinker
    Blinker.begin(auth, ssid, pswd);
    // BLINKER_TAST_INIT();  // ESP32多任务,不推荐使用,会有几率卡住无法联网
    Blinker.attachData(dataRead);
    Blinker.setTimezone(8.0);
    
/*----------------------- 绑定回调函数 -----------------------*/
    // 按键1控制,注册回调函数
    Button1.attach(button1_callback);

    // 按键2控制,注册回调函数
    Button2.attach(button2_callback);

    // 小爱电源类的操作接口,注册回调函数
    BlinkerMIOT.attachPowerState(miotPowerState);

    // 小爱亮度的控制接口,注册回调函数
    BlinkerMIOT.attachBrightness(miotBright);

    // 注册亮度滑动条回调函数
	  Slider1.attach(Slider1_callback);

    // 小爱设备查询接口,注册回调函数
    BlinkerMIOT.attachQuery(miotQuery);

    // 注册色温滑动条回调函数
	  Slider2.attach(Slider2_callback);

    // 注册色温回调函数
    BlinkerMIOT.attachColorTemperature(miotColoTemp);

    // 小爱模式的设置接口,注册回调函数
    BlinkerMIOT.attachMode(miotMode);

    wifiInit = true;  // 初始化成功标志

    // 如果初始化失败,则20s后重启(10s后如果成功则继续运行)
    while(!wifiInit)
    { 
      delay(10000);
      if(!wifiInit)
      {
        Blinker.delay(10000);
        ESP.restart();
      }
      break;
    }
}

void loop() {
    Blinker.run();  // 此函数需要频繁调用以保持设备间连接及处理收到的数据, 建议放在 loop() 函数中
    LedControl(colorW,colorT);  // 分别是亮度,色温
    delay(10);	// 10ms比较合适
}

源码讲解

其实我备注的已经足够详细了,有空的时候我还是想把它拆分开讲解一下,方便后期回顾理解。这类教程用8266的比较多,用esp32的也有,但都不够详细,我会弥补这些缺陷,做到尽量完整直观。

代码框架

下面用DeepSeek总结了一下代码框架,使用的是mermaid语法,手动修改后比较符合代码逻辑,也比较直观:

重启
多任务管理
LED控制模块
回调处理模块
初始化阶段
配置引脚/PWM通道
设置WiFi认证/时区
触发事件
持续调用
串口/PWM初始化
Blinker.begin
按钮/滑动条
按钮事件
滑动条事件
小爱指令
LedControl函数
LED闪烁
持续调用
更新颜色参数
模式切换
触发任务
触发任务
重启ESP32
创建/删除任务
TaskBlink任务
使用FreeRTOS API
计算PWM值
控制LED状态
输出到硬件引脚
重启/状态查询
回调处理
亮度/色温调整
电源/模式/查询处理
修改全局变量
引脚模式设置
硬件初始化
组件对象创建
Blinker初始化
回调函数绑定
主循环
Blinker.run

下面我就按照这个框架来展开描述。

1、初始化

一开始是初始化阶段,内容是配置串口、PWM、引脚、网络、加载blinker账户,以及绑定回调函数。

这里主要说一下初始化PWM、引脚,以及绑定回调函数。

初始化PWM

根据ESP32C3手册的说法,PWM使用LEDC来控制,这是ESP32和ESP8266的差异。

在ESP8266里,直接用analogWrite()函数操作对应引脚,就可以按照默认参数输出PWM了,频率分辨率等参数可以先修改好。。

在ESP32中也可以直接用analogWrite()函数操作对应引脚,这和ESP8266类似。但是ESP32还有个ledcWrite()函数,通过 操作预分配的通道来输出PWM。具体是需要先使用ledcSetup()函数配置PWM通道,绑定通道和引脚,然后就可以用ledcWrite()函数操作通道或使用analogWrite()函数操作引脚来输出PWM。

// PWM通道配置(ESP32专用)
#define PWM_CHANNEL_COLD 1         // 配置PWM通道,这里是冷光PWM通道(通道从0开始,我这里只是拿冷光示例,0已经分配给蓝光了)
#define PWM_FREQ 5000              // 配置PWM频率,填数字,单位Hz
#define PWM_RESOLUTION 8           // 配置PWM分辨率(8位对应0-255),填数字就行

// 初始化PWM
    ledcSetup(PWM_CHANNEL_COLD, PWM_FREQ, PWM_RESOLUTION);	//初始化PWM通道、频率、分辨率
    ledcAttachPin(LED_Cold, PWM_CHANNEL_COLD);	//绑定PWM通道和引脚

// 初始化LED
    pinMode(LED_Cold, OUTPUT);	//设置控制LED的IO为输出状态
    digitalWrite(LED_Cold, LOW);	//初始化时关掉LED,这里电平按实际来。初始化时可以使用digitalWrite()函数,此时是数字输出状态。由于通道的状态是动态的,所以不影响。

// 用PWM控制LED
	analogWrite(LED_Cold, cold);	//参数1是冷光LED的引脚,参数2是占空比,范围0到255,越大则脉宽越大则越亮。
	ledcWrite(PWM_CHANNEL_COLD, cold);	//另一种方式,参数1是冷光LED绑定的通道,参数2是占空比。

区别主要是输出PWM时方式不同。ESP8266直接操作引脚,而ESP32可直接操作引脚,也可操作通道来间接操作引脚。此外二者配置PWM参数时,函数也不一样,ESP32不支持ESP8266的配置方式。

我实际使用时,发现掌控不好ledcWrite()函数,所以我仍然用的是analogWrite()函数,效果还是不错的。

初始化引脚

初始化引脚要说明的是,直接填数字,数字就是arduino编译器对应开发板的IO。这里10就是GPIO10的意思。

// 硬件引脚定义
#define LED_Cold 10  //环形灯白光(高色温)
绑定回调函数

绑定回调函数这里是要注册回调函数,每个回调函数都需要注册,规则是由blinker指定的。这些回调函数就是blinker开发的接口,推测是注册了回调函数,blinker.run()函数才会操作对应的回调函数。

这里就必须要参考blinker的开发文档[1][2],详见:https://diandeng.tech/doc/xiaoai

这里用小爱的亮度控制接口举例,官方是这样说的:

当小爱同学向设备发起控制, 设备端需要有**对应控制处理函数**

BlinkerMIOT.attachBrightness()
用户自定义亮度控制的回调函数:

void miotBright(const String & bright)
{
    BLINKER_LOG("need set brightness: ", bright);

    colorW = bright.toInt();

    BLINKER_LOG("now set brightness: ", colorW);
    
    BlinkerMIOT.brightness(colorW);
    BlinkerMIOT.print();
}

务必在回调函数中反馈该控制状态
亮度范围为1-100

注册回调函数:

BlinkerMIOT.attachBrightness(miotBright);

从上面的例子可以看出来,就是两个东西:用户可修改的回调函数对应的处理函数(格式固定)。

所以我这里实际是这样的:

自定义的回调函数:

首先是函数的参数,没变,这里用的&就很有意思,可以实现参数双向传递,代替了return的功能,非常适合这种需要返回参数的函数,可以看见它仍然用的是void,而不是int那些东西。

这里的wsMode则是我自定义的标记,不同数字对应这不同的状态,是为了区分手动模式和预设模式,该参数也会通过按键查询更新到app中。

//  小爱亮度控制的回调函数
void miotBright(const String & bright)
{
    colorW = bright.toInt();
	  Slider1.print(colorW);
    BLINKER_LOG("亮度更新: ", colorW);
    BlinkerMIOT.brightness(colorW);
    BlinkerMIOT.print();

    wsMode = 7; // 通过小爱改变亮度,标记为手动模式
}

注册的回调函数:

这个注册回调函数不需要动,采用blinker开发文档指定的函数就行,参数为配套回调函数的名称。

这个注册的回调函数是为了当小爱同学向设备发起控制时, 设备端需要有****对应控制处理函数[1] ,这类似函数的身份卡,识别到就去执行配套的回调函数,回调函数中会进行数据处理、传递等操作。

// 小爱亮度的控制接口,注册回调函数
    BlinkerMIOT.attachBrightness(miotBright);

2、主循环

接下来便是进入主循环,在循环中主要有两个函数,blinker.run()和LedControl(colorW,colorT),前者用于刷新设备与blinker服务器的连接,以及处理回调事件;后者则是根据传入的全局变量“亮度和色温”来修改LED照明状态。

blinker.run()是blinker库指定的,一定要放到循环中定期运行。细节就要看blinker库文件了。

LedControl()则是单独的LED灯光控制函数,注意其不是blinker的回调函数,是完全自定义的,所以也一定要丢到循环里定期运行。

LED灯光控制函数

这里单独说一下LedControl(colorW,colorT)这个LED灯光控制函数[4][7]。

需要突出介绍的是亮度值转换函数,亮度brightness和色温colorTemp的范围是100到10000,要转换成0到255的PWM占空比。

这里还涉及到色温的控制,色温越低则越黄,色温越高则越蓝(越冷)。

/*----------------------- 灯光控制函数 -----------------------*/
//  灯光控制函数,分别是亮度,色温(色温越高,蓝光越多)
void LedControl(float brightness, int colorTemp)
{
    // 计算双色灯亮度值(0-255)
    int cold = (brightness * colorTemp * 255) / 10000;  // 控制冷光
    int warm = (brightness * (100 - colorTemp) * 255) / 10000;  // 控制暖光
	
    // 输出PWM信号,ledcWrite不起效果,analogWrite可正常使用
    // ledcWrite(PWM_CHANNEL_COLD, cold);
    // ledcWrite(PWM_CHANNEL_WARM, warm);
    analogWrite(LED_Cold, cold);
    analogWrite(LED_Warm, warm);
}

这里还用到了map映射函数:

将色温colorT(0到100)与标准色温colorTemp(1000到10000)进行映射,前者是给blinker用的,后者是给小爱用的。

BlinkerMIOT.colorTemp(map(colorT, 0, 100, 1000, 10000));    //  小爱查询时,反馈灯的色温
colorT = map(colorTemp, 1000, 10000,  0,  100); // 小爱调色温时,映射函数,map(value, fromLow, fromHigh, toLow, toHigh)

关于map()函数范围映射可参考:Arduino中的map函数[7]

map(value, fromLow, fromHigh, toLow, toHigh)
// value:要映射的值。
// fromLow 和 fromHigh:原始范围的最小值和最大值。
// toLow 和 toHigh:目标范围的最小值和最大值。
// 例如,map(colorT, 0, 100, 1000, 10000) 的作用是将 colorT 这个值从 0 到 100 的范围映射到 1000 到 10000 的范围内。
// 这意味着如果 colorT 的值为 0,映射后的值为 100;如果 colorT 的值为 100,映射后的值为 10000;
// 如果 colorT 的值在这个范围内,将按比例线性映射到目标范围内的对应值。
// map()用法参考[7]:https://blog.csdn.net/m0_74381444/article/details/136986918

3、回调事件

回调事件包括按键、滑动条、小爱指令。回调事件处理时会更新亮度、色温、模式的全局变量。

按键

按键事件包括两个按键,一个是状态查询,将上述全局变量的数据汇报给app;另一个则是重启ESP模块。其中状态查询时,会创建一个临时的线程来快速闪烁板载LED指示灯,尽量不影响到其它函数的运行。
在这里插入图片描述
按键这个东西也是blinker的库函数,除了可自定义的回调函数,注册回调函数,还需要先新建组件对象。操作流程和其它回调函数一样,程序运行时通过注册的回调函数信息找到对应的回调函数来执行。

// 新建组件对象
BlinkerButton Button1("btn-restart");           //  重启ESP32按键组件

// 按下button1按键即会执行该函数
void button1_callback(const String & state)
{
    BLINKER_LOG("get button state: ", state);
    Blinker.print("1s后重启...");
    Blinker.delay(1000);
    ESP.restart();
}

// 按键1控制,注册回调函数
    Button1.attach(button1_callback);
滑动条

滑动条会直接修改全局变量。
在这里插入图片描述
滑动条的配置和按键一样,也是三件套,不多说了。

// 新建组件对象
BlinkerSlider Slider1("bar-brightness");        //  亮度滑动条组件,注意我虽然写的是brightness,实际在回调函数中上传的是colorW

//  亮度滑动条回调函数
void Slider1_callback(int32_t value) {

    BLINKER_LOG("亮度设置: ", value);
    colorW = value;
    wsMode = 7; // 手动调节亮度,标记为手动模式
}

// 注册亮度滑动条回调函数
	  Slider1.attach(Slider1_callback);

小爱指令

小爱则是根据需要修改/返回数据。

小爱指令全是回调函数,包括回调函数和注册回调函数两部分,不多说了,具体详见blinker关于小爱的接口文档:https://diandeng.tech/doc/xiaoai

不过这里会挑一些有意思的部分来说说,小爱模式回调函数,在这里可以预设不同的灯光模式,根据blinker开发文档来看,一共有7种,本质上是配置不同的亮度(colorW)和色温值(colorT)。不过需要注意的是,mode的名字是固定的7种格式,不可以乱起。

模式中文描述
DAY日光
NIGHT月光
COLOR彩光
WARMTH温馨
TV电视模式
READING阅读模式
COMPUTER电脑模式

并且每种模式都有固定前缀:BLINKER_CMD_MIOT_

// 小爱模式的设置接口回调函数
void miotMode(uint8_t mode)
{
    BLINKER_LOG("need set mode: ", mode);

    if (mode == BLINKER_CMD_MIOT_DAY) {
        // 日光,Your mode function
        colorW = 100;
        colorT = 0;
    }
    else if (mode == BLINKER_CMD_MIOT_NIGHT) {
        // 月光,Your mode function
        colorW = 20;
        colorT = 100;
    }
    else if (mode == BLINKER_CMD_MIOT_COLOR) {
        // 彩光,Your mode function
        colorW = 50;
        colorT = 50;
    }
    else if (mode == BLINKER_CMD_MIOT_WARMTH) {
        // 温馨,Your mode function
        colorW = 50;
        colorT = 0;
    }
    else if (mode == BLINKER_CMD_MIOT_TV) {
        // 电视,Your mode function
        colorW = 30;
        colorT = 70;
    }
    else if (mode == BLINKER_CMD_MIOT_READING) {
        // 阅读,Your mode function
        colorW = 100;
        colorT = 50;
    }
    else if (mode == BLINKER_CMD_MIOT_COMPUTER) {
        // 电脑,Your mode function
        colorW = 30;
        colorT = 50;
    }

    wsMode = mode;

    BlinkerMIOT.mode(mode);
    BlinkerMIOT.print();
}

上面这个小爱模式回调函数同样有对应的注册回调函数,不多说了,开发文档直接拿来用。

效果像下面按这样,还是相当OK的。
在这里插入图片描述

4、多线程

这个多线程是为板载蓝色LED设计的,一开始是希望它在设备开机后可以一直闪烁,是后面才改成了在手动上传数据时闪烁。

以往LED闪烁的函数是丢在循环中运行,但这里显然不现实,因为我这里是通过延时产生的闪烁。在循环里跑延时会导致程序在执行LED闪烁时无法及时处理其余函数的事件。所以我就想到用多线程单独执行LED闪烁,希望LED一直闪烁,也不影响其余函数的执行。

测试效果不是很好,LED闪烁是没问题,但其余函数的执行还是收到了轻微的影响,没有循环里直接跑那么严重。主要表现为开启多线程LED闪烁时,小爱频繁找不到设备。所以就将LED闪烁放到按键手动上传状态数据那了,上传数据时LED快速闪烁,平时LED长亮。

我仍然保留了多线程的方式,函数主要有三个,涉及创建线程、通过线程运行的函数、结束线程任务[5][6]:

创建线程

xTaskCreate()用来创建一个线程,这里关键参数是函数名TaskBlink和句柄TaskBlink_Handle,从这一刻开始灯就在闪了;

通过线程运行的函数

TaskBlink()用线程来执行LED闪烁,这里函数名就是创建线程时填的函数名TaskBlink,如果在创建线程后面定义,记得声明;

结束线程任务

BlinkLED()则是执行延时结束线程任务,vTaskDelete(TaskBlink_Handle)就是关闭指定的正在运行的线程,参数是函数的句柄。

void button2_callback(const String & state)
{
    // 创建一个线程(要运行的函数的名称,函数备注,任务栈,任务参数,优先级(0是最低),任务句柄)
    xTaskCreate(TaskBlink, "Task Blink", 2048, (void *) PWM_CHANNEL_BLUE, 0, &TaskBlink_Handle);
	...
    ...
    BlinkLED(1000);
}

/*----------------------- 线程执行LED_Blue闪烁函数 -----------------------*/
// 任务函数,在线程中执行,闪烁LED具体实现方式
void TaskBlink(void *pvParameters)
{
  pinMode(LED_Blue, OUTPUT);

  while(1)
  {
    // digitalWrite(LED_Blue, !digitalRead(LED_Blue));
    // fade in from min to max in increments of 5 points:
    for (int fadeValue = 0 ; fadeValue <= 255; fadeValue += 10) {
      // sets the value (range from 0 to 255):
      analogWrite(LED_Blue, fadeValue);
      // ledcWrite(PWM_CHANNEL_BLUE, fadeValue);  // ledcWrite不起效果,analogWrite可正常使用
      // wait for 30 milliseconds to see the dimming effect
      delay(1);
    }

    // fade out from max to min in increments of 5 points:
    for (int fadeValue = 255 ; fadeValue >= 0; fadeValue -= 10) {
      // sets the value (range from 0 to 255):
      analogWrite(LED_Blue, fadeValue);
      // ledcWrite(PWM_CHANNEL_BLUE, fadeValue);
      // wait for 30 milliseconds to see the dimming effect
      delay(1);
    }
    
  }
}

//  闪烁板载LED,blinktime控制闪烁时间,单位ms
void BlinkLED(int32_t blinktime)
{
  vTaskDelay(blinktime / portTICK_PERIOD_MS);                               // 延时
  if (TaskBlink_Handle != NULL)                                        // 如果TaskBlink_Handle不为空就删除任务
  {
      vTaskDelete(TaskBlink_Handle);
  }
  analogWrite(LED_Blue, 0); // 闪烁结束,LED_Blue恢复长亮
}

五、演示视频

这里再提一遍演示视频,详见B站:没人要的环形灯接入米家,用小爱同学控制开关灯,调亮度,调模式
在这里插入图片描述
到此全文结束~

参考资料

[1] 点灯科技. blinker Arduino支持库. https://diandeng.tech/doc/arduino-support-next#blinkerbegin

[2]. 小米小爱接入. https://diandeng.tech/doc/xiaoai

[3]. ESP32C3SuperMini 入门. https://www.nologo.tech/product/esp32/esp32c3/esp32c3supermini/esp32C3SuperMini.html

[4]. esp8266+Blinker+小爱同学控制双色LED. https://blog.csdn.net/weixin_45695902/article/details/125709676

[5]. 12、ESP32 双核. https://blog.csdn.net/weixin_51358957/article/details/137921369

[6]. 01_ESP32_FreeRTOS任务的创建和删除. https://blog.csdn.net/weixin_46364710/article/details/122435587

[7]. Arduino中的map函数. https://blog.csdn.net/m0_74381444/article/details/136986918

### 关于ESP32固件编译缺少头文件的解决方案 在开发基于ESP32的小智AI项目时,如果遇到固件编译过程中无法找到特定头文件的情况,通常是因为以下几个原因造成的: #### 1. 头文件路径未正确配置 确保项目的`CMakeLists.txt`或`platformio.ini`文件中已正确定义了头文件所在的目录。例如,在PlatformIO环境中,可以通过以下方式指定额外的头文件路径[^1]。 ```ini build_flags = -Isrc/include -I../common/include ``` 上述代码片段表示将`src/include``../common/include`作为额外的头文件搜索路径加入到编译选项中。 #### 2. 使用错误的SDK版本 不同版本的ESP-IDF可能不兼容某些功能模块或API定义。确认当前使用的ESP-IDF版本是否支持所需的头文件以及其内部函数声明。可以尝试切换至推荐的稳定版ESP-IDF来解决问题。 下载并安装对应版本的方法如下: - 访问官方文档页面获取具体指导链接。 - 或者通过命令行工具执行更新作。 ```bash git clone --recursive https://github.com/espressif/esp-idf.git cd esp-idf ./install.sh source export.sh ``` 以上脚本会自动拉取依赖库并将环境变量设置好以便后续调用。 #### 3. 需要手动复制缺失文件 有时第三方插件或者自定义组件并未随源码一同发布来,则需从网上查找相应资源然后放置到工程合适位置下再重新构建整个项目结构树形图从而完成加载过程。 比如对于一些特殊外设驱动程序而言,它们往往独立存在于其他仓库里而不是集成进标准框架里面去;此时就需要开发者自行前往相关网站寻找最新发行包之后解压提取必要部分粘贴回去即可正常使用这些特性而无需担心报错现象发生。 --- ### 提供一段简单的测试代码用于验证问题修复情况 下面给了一段基础示例用来检测基本功能是否正常运作: ```c #include "driver/gpio.h" void app_main(void){ gpio_pad_select_gpio(2); gpio_set_direction(2, GPIO_MODE_OUTPUT); while(true){ gpio_set_level(2,1); vTaskDelay(pdMS_TO_TICKS(500)); gpio_set_level(2,0); vTaskDelay(pdMS_TO_TICKS(500)); } } ``` 此段落展示了如何利用GPIO接口控制LED灯闪烁效果,其中包含了必要的包含语句以表明该方法适用于解决因缺乏适当导入而导致的编译失败状况。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

sprlightning

您的鼓励是我最大的创作动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值