让GPIO高速开关,把输出扩成亮度档位
普通数字灯只有开与关。PWM让GPIO在固定周期里快速切换,通过高电平所占时间比例改变LED平均导通时间。本篇选5kHz、8位分辨率,再用五项占空比表做可循环的按钮调光器。
PWM适用于这里的普通外接LED;板载可寻址RGB需要颜色数据协议,不能拿同一段LEDC代码驱动。更大负载也不能直接接GPIO,本篇不把LED调光例外推为电机或大功率灯驱动。
先决定频率和分辨率,再处理边界
5kHz对应约200微秒一个周期;8位给出0–255的合法duty数值。频率与可用分辨率受硬件时钟和LEDC能力共同限制,不能同时把两个数随意调到最大。本例参数属于明确的一组配置,成功与否必须看ledcAttach返回值。
按钮依次选{0,64,128,192,255},最后回0。表内值天然限制边界,长按仍由30毫秒消抖与下降沿规则保证只前进一档。人眼对亮度的感知非线性,所以数值约等距不意味着主观看来五档等亮度递增。
本篇外接灯固定为Kingbright WP7113ID(规格书V.14A),使用330Ω、±5%、额定0.25W电阻;如果沿用F03的1kΩ电阻,也可保留,亮度更低,程序不变。更换电阻必须先断电。
参考:[1] Arduino-ESP32 3.3.12:LEDC API[2] Arduino-ESP32 3.3.12:ledcAttach和ledcWrite声明[5] Kingbright WP7113ID规格书 V.14A:极性、10mA条件压降与额定值
接线不变,注意3.x按引脚写入
保持GPIO4→330Ω→普通红LED阳极,阴极→GND;GPIO5经常开按钮接GND,INPUT_PULLUP。使用官方v1.1 N8R8、core3.3.12,下载README提供了完整FQBN与UART日志设置。
3.x接口使用ledcAttach(pin,freq,resolution)配置,再用ledcWrite(pin,duty)按引脚写占空比。旧教程里的ledcSetup与ledcAttachPin属于另一版接口,不要把channel与pin参数混着用。代码每次配置和写入都报告布尔结果,失败时停止调光并使LED输出为低。
| 参数 | 本例 | 需要核对 |
|---|---|---|
| pin | GPIO4 | 与外接LED接线一致 |
| freq | 5000 Hz | 驱动是否接受;仪器可测 |
| resolution | 8 bit | duty必须0–255 |
| button | GPIO5,低电平按下 | 30 ms稳定窗口 |
参考:[1] Arduino-ESP32 3.3.12:LEDC API[2] Arduino-ESP32 3.3.12:ledcAttach和ledcWrite声明[3] 乐鑫:ESP32-S3-DevKitC-1 v1.1,排针与RGB修订[4] Arduino-ESP32 3.3.12:GPIO API与模式
完整调光器:成功返回才继续
setup配置LEDC,随后立即写level0;loop只在稳定按下后增加档位。applyLevel输出level、duty、write返回值及频率/分辨率,方便把API执行和物理灯光分开检查。
一旦写失败,程序释放LEDC并将GPIO4拉低,不在后台悄悄继续假装已经调光。Serial打印出来的frequency是请求参数,不是驱动外部波形的测量值。
// SPDX-License-Identifier: MIT
#include <Arduino.h>
constexpr uint8_t LED_PIN = 4, BUTTON_PIN = 5;
constexpr uint32_t FREQUENCY = 5000;
constexpr uint8_t RESOLUTION = 8;
constexpr uint16_t DUTIES[] = {0, 64, 128, 192, 255};
bool candidate, stable, ready = false;
uint32_t changedAt = 0;
uint8_t level = 0;
bool applyLevel() {
const bool ok = ledcWrite(LED_PIN, DUTIES[level]);
Serial.printf("level=%u duty=%u/255 write=%u frequency=%lu resolution=%u\n",
level, DUTIES[level], ok, (unsigned long)FREQUENCY, RESOLUTION);
return ok;
}
void setup() {
Serial.begin(115200);
pinMode(BUTTON_PIN, INPUT_PULLUP);
candidate = stable = digitalRead(BUTTON_PIN);
changedAt = millis();
ready = ledcAttach(LED_PIN, FREQUENCY, RESOLUTION);
Serial.printf("P04 core=3.3.12 attach=%u\n", ready);
if (ready) ready = applyLevel();
if (!ready) { pinMode(LED_PIN, OUTPUT); digitalWrite(LED_PIN, LOW); }
}
void loop() {
if (!ready) return;
const uint32_t now = millis();
const bool raw = digitalRead(BUTTON_PIN);
if (raw != candidate) { candidate = raw; changedAt = now; }
if (candidate != stable && uint32_t(now - changedAt) >= 30) {
stable = candidate;
if (stable == LOW) {
// Advance to the upper boundary; another press returns to off.
level = (level + 1) % (sizeof(DUTIES) / sizeof(DUTIES[0]));
ready = applyLevel();
if (!ready) { ledcDetach(LED_PIN); pinMode(LED_PIN, OUTPUT); digitalWrite(LED_PIN, LOW); }
}
}
}
参考:[1] Arduino-ESP32 3.3.12:LEDC API[2] Arduino-ESP32 3.3.12:ledcAttach和ledcWrite声明
逐档记录,不把成功返回当成波形测量
在指定环境编译本篇示例并上传,串口115200。先保存core=3.3.12、attach=1以及初始duty=0、write=1的实际日志。以下是应该执行的检查,本站没有实板或示波器结果。
- 按5次,记录64、128、192、255、0的顺序,确认0熄灭、255满亮,长按不连跳。
- 逐档记录是否能区分亮度,同时保留每次write返回值。若肉眼难辨中高档,不据此断言频率错误。
- 有示波器时,探头接GPIO4、地夹接同一GND,在64/128/192档测周期与高电平宽度;0与255是静态端点,不能要求出现完整PWM周期。
- 将频率、分辨率、core、返回值及实际测量分别填写,不以参数日志替代测量。
- 做到这里,你应该看到
- 五档输出和边界检查有记录;物理波形只有仪器测过才能标记为实测。
按接口、资源、接线的顺序排错
找不到ledcSetup等符号时先确认正在编译哪版core;不要为让旧代码编过而同时拼入两套接口。attach=0时核对引脚及其他库对LEDC资源的占用,在只有本例的干净程序上重试。
write=0时先保留日志再RESET,程序已进入安全熄灯路径。若所有档都很亮,断电查LED是否误接3V3或GPIO旁边的电源针;若所有档都灭,先运行P01排除LED与极性问题。恢复原来的5kHz、8bit参数再重编译,不靠随机更换管脚恢复。
资料来源与版本说明
资料与固定版本源码核对;构建、仿真与实板为分开的验证维度。资料核对日期:。资料核对、程序构建与实板测试分别记录,不互相替代。