《ESP32+直流电机》第 7 章 LEDC 与 PWM:从波形数学到硬件实现

发布时间:2026/10/18 4:49:24
《ESP32+直流电机》第 7 章 LEDC 与 PWM:从波形数学到硬件实现 本章是全书核心。我们从最底层讲透 PWM①方波的频率/周期/占空比/分辨率的数学定义②占空比如何改变平均电压③LEDC 硬件定时器→计数器→比较器→输出的工作机制④频率公式f clk / (分频 × 2^分辨率)的推导⑤本书最重要的坑fade 服务——它是什么、为什么必须在配置后安装、为什么装错顺序会空指针崩溃。7.1 方波PWM 的原材料PWM脉冲宽度调制输出的是方波在一段时间里高、低交替。电压 3.3V ┤┌──┐ ┌──┐ ┌──┐ ││ │ │ │ │ │ ││ │ │ │ │ │ 0V ─┴┘ └───┘ └───┘ └──► 时间 ├┤ ├───┤ 高宽 低宽 ├───────┤ 周期 T三个基本量量定义单位本例周期 T一个完整波形的时间秒50µs频率 f每秒多少个周期f 1/THz20000 Hz高电平时间 t_on一个周期里高持续多久秒25µsf 1 / T 20000 Hz 1 / 50µs 1µs 1/1000000 秒占空比高的时间占周期的比例占空比 duty t_on / T × 100% 例t_on 25µsT 50µs → duty 50%平均电压为什么 PWM 能调速一个周期内的平均电压V_avg 3.3V × (t_on/T) 0V × (t_off/T) 3.3V × dutyduty 100% → 平均 3.3V全开duty 50% → 平均 1.65V半速duty 0% → 平均 0V全关。电机的速度 ≈ 平均电压近似线性。这就是改变占空比 调速的数学原因。为什么电机不会一格一格抖因为频率高20kHz电机的惯性把它平滑了——平均电压决定转速细节被滤掉。7.2 分辨率占空比能分成多少级占空比是连续的但数字电路只能给离散档位。档位总数由分辨率决定档位数 2^分辨率 10 位 → 1024 档0 ~ 1023 12 位 → 4096 档占空比档位值duty_value / (2^分辨率 - 1)10 位下 duty_value 512 → 约 50%分辨率越高档越细调速越顺但和频率抢资源7.5 推导。7.3 LEDC 硬件结构工厂流水线LEDC 不是软件模拟是芯片里的硬件模块。结构分频器 计数器 比较器 系统时钟80MHz ─► 分频(可调) ─► 节拍时钟 ─► 从0数到上限 ─► 和比较值比大小 │ ┌───────────────────┤ ▼ 输出电路产生沿 │ ▼ 引脚GPIO18具体分工定时器timer设定频率和分辨率决定数到几和多快数通道channel持有比较值就是占空比档位和计数器比大小在上升沿/下降沿翻转输出引脚把波形送出去。两个别踩混的点①图里的80MHz 是 APB 总线时钟LEDC 这类外设的供钟不是 CPU 主频S3 的 CPU 是 240MHz②分频就是把节拍器调慢分频系数设 N节拍就慢 N 倍。S3 的资源表查芯片手册/头文件确认资源数量定时器4 个0~3通道8 个0~7分辨率上限14 位模式只有低速率Low Speed⚠️S3 没有高速模式SOC_LEDC_SUPPORT_HS_MODE不存在。网上大量教程写LEDC_HIGH_SPEED_MODE那是老 ESP32非 S3的。认准S3 只用LEDC_LOW_SPEED_MODE。LEDC 的定时器只管输出方波。想要到点干一件事的通用硬件秒表GPTimer见第 14 章。7.4 计数器与比较器方波是怎么造出来的核心机制理解后代码就活了计数器值 1023 ┤ ┌────┐ ┌────┐ (10位) │ │ │ │ │ 512 ┤────────┐│ │────────┐│ │ │ ││ │ ││ │ 0 ┤────────┘└────┘────────┘└────┘► 时间 └──────────────────────────────► 输出(引脚) 高 ┤ ┌────┐ ┌────┐ 低 ──────┘ └─────┘ └────► 时间计数器从 0 一路数到 1023一个周期然后重新从 0 开始比较值 512计数器 512 时输出高≥ 512 时输出低于是比较值越大 → 高电平越长 → 占空比越大。占空比档位 比较值。我们写ledc_set_duty_and_update(..., duty, hpoint)duty就是比较值hpoint是何时生效7.6 专门讲别当成时间。频率和分辨率的跷跷板数学推导节拍时钟 系统时钟 / 分频 分频 ≥ 1 一个周期要数 2^分辨率 下 频率 f 节拍时钟 / 2^分辨率 80MHz / (分频 × 2^分辨率)分辨率 10 位、不分频f 80M / 1024 ≈ 78.1kHz → 20kHz 完全够分辨率 14 位已经是 S3 的天花板f 80M / 16384 ≈ 4.9kHz →太低电机嗡嗡响。把本书的 20kHz 代回去解分频就是 7.3 图里那个可调分频该拨到几分频 80MHz ÷ (20kHz × 2^10) 80 000 000 ÷ 20 480 000 3.90625 → 整数部分 3小数尾巴 0.90625 232/256 —— 分频寄存器还带 8 位小数尾 3 232/256 正好凑出 3.90625真正生效的频率以回读为准7.7 第 8 步。结论想要高频 → 降低分辨率想要细腻 → 降低频率。鱼和熊掌。7.5 配置代码定时器 通道逐行定时器管频率和分辨率ledc_timer_config_t timer_cfg{};timer_cfg.speed_modeLEDC_LOW_SPEED_MODE;// S3 只有低速timer_cfg.timer_numLEDC_TIMER_0;// 用 0 号定时器timer_cfg.duty_resolutionLEDC_TIMER_10_BIT;// 分辨率 10 位timer_cfg.freq_hz20000;// 频率 20kHztimer_cfg.clk_cfgLEDC_AUTO_CLK;// 时钟自动选ledc_timer_config(timer_cfg);// 写入硬件寄存器通道管占空比和引脚ledc_channel_config_t ch_cfg{};ch_cfg.speed_modeLEDC_LOW_SPEED_MODE;ch_cfg.channelLEDC_CHANNEL_0;ch_cfg.timer_selLEDC_TIMER_0;// 挂到 0 号定时器上ch_cfg.gpio_numGPIO_NUM_18;// 输出到 GPIO18ENAch_cfg.duty0;// 初始比较值 0先停着ch_cfg.hpoint0;// 新占空比在计数器数到该值时生效0 周期开头立即生效7.6 细讲ledc_channel_config(ch_cfg);一个定时器可以被多个通道共享第 12 章双电机不同频率/分辨率才需要不同定时器。7.6 fade 服务必须理解的渐变机制fade 是什么先把两个概念掰开真实的按时长渐变硬件自己每个周期把比较值挪近目标一小步挪到位产生一次渐变结束中断中断 硬件按门铃通知 CPU “有事件来了”必须马上应、快快地应——第 15 章细讲。API 是ledc_set_fade_with_time(...)这里才给时长ledc_fade_start(...)。中断处理和记账推进状态、调用户回调需要一套中断机制——这套机制就是fade 服务用ledc_fade_func_install(0)安装中断里只能干快活的规则见第 15 章。那ledc_set_duty_and_update(..., duty, hpoint)呢高能预警它的第 4 个参数是hpoint不是渐变时长不是微秒、不是毫秒hpoint 的含义LEDC 计数器从 0 数到满量程7.4新占空比在计数器数到 hpoint 的那一刻生效。传 0 下个周期开头立即生效效果就是马上更新既然不变化它凭什么归在 fade 家族因为它走的是同一条多处同时改占空比也不会改乱的硬件更新通道学名叫线程安全并发怎么做到安全是第 13 章的事这里知道有这层保护即可——内部是通道锁 一次性写入 duty/hpoint这条通道依赖 fade 服务的记账结构——所以用它照样必须先装 fade 服务。本书电机库从不用渐变调占空比applyDuty()的 hpoint恒传 0第 8 章逐行看。换向时的保护也不靠占空比渐变而是“先刹车 5ms 再反向”8.7——别把这两件事混在一起。坑 1不装服务就调用 → 明确报错E (749) ledc: Fade service not installed, call ledc_fade_func_install E (769) DCMotor: init(80): 占空比初始化失败原因驱动源码ledc.c核实ledc_set_duty_and_update进门先查fade 服务——中断句柄还是空服务没装就打上面那条日志并返回ESP_FAIL注意不是ESP_ERR_INVALID_STATE。本书motor_control.cpp文件头的注释写的就是这句话“否则驱动直接返回 ESP_FAIL”。修复调用一次ledc_fade_func_install(0)。坑 2装错顺序 → 空指针崩溃本书真踩如果把 install 放在定时器/通道配置之前启动即崩Guru Meditation Error: Core 0 paniced (LoadProhibited) EXCVADDR: 0x00000000 Backtrace: 0x4200942f:... 0x4037c123:... 0x42000056:... Backtrace 有节选每个返回地址后面还跟着一个栈地址这里只保留关键的三个第一次见这四个崩溃黑话先各记住一句Guru Meditation 芯片的崩溃公告公告完默认自动重启见 6.4panic’ed (LoadProhibited) 掉进 panic 应急通道罪名是读取了一个不允许读的内存地址EXCVADDR: 0x00000000 这次碰的非法地址是 0 号——空指针的身份证Backtrace 崩溃瞬间调用链留下的垫脚石地址清单第 11 章教你把它们翻译成代码行号。底层原因对照驱动源码ledc.c addr2line 查出的真相第 11 章讲方法ledc_fade_func_install要注册渐变结束中断注册中断得先从驱动对象手里拿中断寄存器地址。对象住在p_ledc_obj[]里——每个 speed_mode一个的全局驱动对象第一次ledc_timer_config/ledc_channel_config时才由驱动内部创建。S3 只有低速模式定时器/通道还没配置就 install整个数组全是空指针驱动找不到任何一个非空对象就拿越界下标去解引用p_ledc_obj[...]-ledc_hal——访问非法地址 → LoadProhibited 崩溃。崩溃链条 ledc_fade_func_install └─► 扫描 p_ledc_obj[]每个 speed_mode 一个驱动对象此时全空 └─► 一个都没找到 → 拿越界下标解引用 → 野地址 → LoadProhibited修复先配置定时器和通道再装 fade 服务。顺序timer_config → channel_config → fade_func_install → 设占空比fade 服务的全局性fade 服务是全芯片一个的不是每个电机一个装两次 → 报已存在卸载要等最后一个使用者退出本书用实例计数第 8/12 章这就是motor_control.cpp里s_ledc_fade_installed和s_active_instances两个 static 变量的由来。7.7 拼起来真实 init() 按什么顺序走机制都讲完了本书真实的DCMotor::init()就是把这些零件按顺序串起来。这里不放代码完整 263 行的 motor_control.cpp 在第 8 章全文逐字复现、逐行精读只按顺序过一遍每一步在干嘛、为什么在这个位置拒绝重复初始化已 init 过再调用返回ESP_ERR_INVALID_STATE参数校验GPIO_IS_VALID_OUTPUT_GPIO查三个引脚、LEDC_CHANNEL_MAX/LEDC_TIMER_MAX查通道和定时器编号——非法值绝不能写进寄存器分辨率钳位std::clamp限制进 [1, 14]S3 上限网上写 1~15 的是错的超了就打警告日志方向引脚IN1/IN2 配成推挽输出第 4 章定时器ledc_timer_config定频率 20kHz、分辨率 10bit本节开头通道ledc_channel_config挂上定时器、输出到 ENAGPIO18初始 duty0、hpoint07.5 的代码fade 安装ledc_fade_func_install(0)——必须在 5、6 之后7.6 坑 2并且全芯片只装一次7.6 坑 1 实例计数回读实际频率ledc_get_freq把真正生效的值存下来——受时钟分频取整影响它和请求的 20000Hz 通常差一点点停好待机设方向、applyDuty(0)电机停在滑行状态登记实例计数打印一条就绪日志。每一步外面都包着ESP_RETURN_ON_ERROR哪步失败错误码和日志立刻返回给调用方错误处理纪律第 6 章。占空比换算速度 → 比较值真实代码maxDuty()逐字引自 motor_control.hspeedToDuty()逐字引自 motor_control.cpp/// 当前分辨率下的占空比满量程值uint32_tmaxDuty()const{return(1uresolution_)-1u;}uint32_tDCMotor::speedToDuty(intspeed)const{constuint32_tmagnitudestatic_castuint32_t(std::abs(speed));// 0 ~ 100// 纯整数运算并做四舍五入。ESP32-S3 只有单精度 FPUdouble 走软件模拟// 用浮点除法会额外链接软浮点库白白增加几 KB 固件体积。return(magnitude*maxDuty()50u)/100u;}注意maxDuty()移位用的是resolution_——实际生效的分辨率不是cfg_.resolution运行时若调setResolution()重配第 8 章会看到它连占空比重算都要做满量程跟着变换算必须跟上最新值。速度 100 → 1023全开速度 50 → (50×1023 50)/100 512约 50%。7.8 S3 能力速查表防网上教程误导项S3v6.0.1注意定时器4共享需同频率分辨率通道8一个定时器可挂多个通道分辨率上限14 位网上1~15 位是错的高速模式无只用 LOW_SPEED_MODE新 APIledc_new_timerv6.0.1 没有用经典 API权威来源C:\esp\v6.0.1\esp-idf\components\esp_driver_ledc\include\driver\ledc.h和芯片头文件里的SOC_*宏。7.9 踩坑清单完整记录不装 fade →Fade service not installed报错返回值是ESP_FAILinstall 放配置前 → Guru Meditation 空指针崩溃7.6 链条fade 装两次 → 报已存在卸载要等最后实例12.5用 HIGH_SPEED_MODE → S3 不存在该模式分辨率写 15 → 超出 14 位上限。7.9b 动手验证完整可编译的呼吸灯把本章零件拼起来前面两个坑都是别人踩的不服气就亲手拼一遍。下面是一份完整的main 文件不是片段把它整文件替换第 4 章点灯工程的main.cpp即可LED 还是 4.7/4.8 那颗接在GPIO48的灯——GPIO48 是普通 GPIO但 LEDC 的波形经 GPIO 矩阵能送到它4.6 讲过的任意连线。两个故意错开的设计正是本章知识的落地用1 号定时器 1 号通道7.5 电机示例占的是 0 号定时器 0 号通道GPIO18。错开之后呼吸灯和电机可以在同一个工程里各用各的资源互不抢占7.5 框说过一个定时器可被多通道共享但频率/分辨率必须一致——这里我们只要 5kHz不共享也罢。顺序严格按 7.6 的正确姿势timer → channel →最后fade install。#includedriver/gpio.h// GPIO_NUM_48 引脚编号第 4 章#includedriver/ledc.h// LEDC定时器 通道 fade 服务#includeesp_log.h#includefreertos/FreeRTOS.h#includefreertos/task.h#defineBREATH_PINGPIO_NUM_48// 和 4.8 点灯同一颗外接 LED串 330Ωstaticconstchar*TAGbreath;externCvoidapp_main(void){// ① 定时器用 1 号——把 0 号让给 7.5 的电机示例两套实验可共存一工程ledc_timer_config_t timer_cfg{};timer_cfg.speed_modeLEDC_LOW_SPEED_MODE;// S3 只有低速模式7.3timer_cfg.timer_numLEDC_TIMER_1;timer_cfg.duty_resolutionLEDC_TIMER_10_BIT;// 1024 档7.2timer_cfg.freq_hz5000;// 呼吸灯 5kHz 足够公式见 7.4timer_cfg.clk_cfgLEDC_AUTO_CLK;ESP_ERROR_CHECK(ledc_timer_config(timer_cfg));// ② 通道同样用 1 号避开电机示例的 CHANNEL_0ledc_channel_config_t ch_cfg{};ch_cfg.speed_modeLEDC_LOW_SPEED_MODE;ch_cfg.channelLEDC_CHANNEL_1;ch_cfg.timer_selLEDC_TIMER_1;// 挂在①的定时器上ch_cfg.gpio_numBREATH_PIN;ch_cfg.duty0;// 从全黑开始ch_cfg.hpoint0;// 新占空比下个周期开头生效7.6ESP_ERROR_CHECK(ledc_channel_config(ch_cfg));// ③ fade 服务必须在①②之后7.6 坑 2先装会崩ESP_ERROR_CHECK(ledc_fade_func_install(0));ESP_LOGI(TAG,呼吸灯开始duty 范围 0~%d,(110)-1);// ④ 软件斜坡每 5ms 挪 8 档一呼一吸约 0.64 秒intduty0;intdir8;for(;;){dutydir;if(duty1023){duty1023;dir-8;ESP_LOGI(TAG,最亮 duty%d,duty);}if(duty0){duty0;dir8;ESP_LOGI(TAG,最暗 duty%d,duty);}ledc_set_duty_and_update(LEDC_LOW_SPEED_MODE,LEDC_CHANNEL_1,duty,0);vTaskDelay(pdMS_TO_TICKS(5));}}这份代码已在ESP-IDF v6.0.1下用idf.py set-target esp32s3 build对空工程实际编译通过0 警告 0 错误串口现象按代码真实行为给出。【动手框】编译、烧录、看呼吸① 在哪执行第 4 章点灯工程根目录、IDF 终端VS Code 用 ESP-IDF 终端。先确认main/CMakeLists.txt是idf_component_register(SRCS main.cpp INCLUDE_DIRS .REQUIRES esp_driver_gpio esp_driver_ledc)4.9 时你写过esp_driver_gpio现在补上esp_driver_ledc——为什么点名要它3.3 讲过。② 敲什么idf.py build flash monitor③ 预期看到开机日志滚完后出现I (302) breath: 呼吸灯开始duty 范围 0~1023灯从全黑渐亮再渐暗循环呼吸约每 1.28 秒交替出现一行I (…) breath: 最亮 duty1023/I (…) breath: 最暗 duty0。每挪一格 duty 走的就是ledc_set_duty_and_update hpoint0这条必须装 fade 服务的通道7.6——不装第 302 毫秒你就会看到 7.6 坑 1 那条Fade service not installed报错。④ 没看到① 编译报ledc_timer_config_t was not declared→REQUIRES 漏了esp_driver_ledc3.3② 上电就 Guru Meditation并重启6.4会自己爬起来板子没坏→ 对照①②③检查 install是不是排到了配置前面7.6 坑 2③ 日志正常但灯不呼吸 →按 4.8 动手框三查接线LED 方向/电阻虚接/是不是 48 脚。退出 monitor 按 Ctrl]。7.10 想一想 小测验想一想占空比 25%、3.3V平均电压是多少写出公式。为什么分辨率 14 位S3 上限时最大频率会很低用公式推fade 服务装错顺序为什么会崩崩溃机制是什么ledc_set_duty_and_update最后一个参数是什么传 0 表示什么小测验f 1 / T周期 50µs 的频率是A. 20kHz B. 2kHz C. 200kHz10 位分辨率下占空比档位的最大值是A. 255 B. 1023 C. 100S3 的 LEDC 有高速模式吗正确的初始化顺序是A. fade→timer→channel B. timer→channel→fade C. channel→fade→timer7.11 本章总结PWM 方波频率1/周期占空比高的比例平均电压3.3×dutyLEDC 分频→计数→比较→输出的硬件流水线比较值就是占空比档位频率 80MHz / (分频 × 2^分辨率)频率和分辨率互相制约S3 只有低速模式、14 位上限fade 服务先配 timer/channel 再 install全芯片只装一次最后实例才卸载。下一章把 PWM 接到 L298N让电机真正转起来。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询