4、PWM电机调速:LEDC外设原理、PWM频率与占空比设置、电机转速控制、死区时间处理

好,咱们进入第四讲。这一讲要啃的,是智能窗帘电机控制里最核心的硬骨头——PWM调速。

说白了,就是让电机转得快一点、慢一点,甚至精确地停在某个位置。你想想看,如果窗帘只能开和关,那跟普通窗帘有什么区别?智能窗帘的灵魂,就在于能“听话”地停在任意位置。

ESP32 内部有个专门干这活的硬件模块,叫 LEDC。名字虽然叫 LED 控制,但它驱动电机一样好使。我个人习惯,能用硬件 PWM 绝不用软件模拟,省心、精准、不占 CPU。

LEDC 外设原理

LEDC 的本质,是一个计数器加比较器。计数器从0开始,一直数到某个最大值(比如1023),然后归零重新数。比较器里存着你设定的值,当计数器的值小于比较值时,输出高电平;大于时,输出低电平。

这样,就产生了一串方波。方波的周期由计数器的最大值和时钟频率决定,高电平的宽度由比较值决定。

嗯,这里要注意:LEDC 是 ESP32 的硬件模块,不占用 CPU 资源。你设好参数后,它自己就一直在那跑,CPU 可以干别的事。我在项目中遇到过,有人用定时器中断模拟 PWM,结果电机一启动,WiFi 就断连了——就是因为 CPU 被频繁打断,没空处理网络协议栈。

核心要点:LEDC 是硬件 PWM,不是软件模拟。它独立运行,不干扰主程序。

PWM 频率与占空比设置

频率和占空比,是 PWM 的两个基本参数。频率决定了电机转动的“细腻程度”,占空比决定了电机的“力气大小”。

对于直流电机,频率太低,电机会发出“嗡嗡”声,甚至抖动。频率太高,电机的电感会滤掉 PWM 信号,导致电机不转。我一般选 1000Hz 到 5000Hz 之间,具体看电机参数。

占空比,就是高电平占整个周期的比例。50% 占空比,意味着电机得到一半的电压,转速大概是一半。但注意,不是线性的!电机有死区,占空比太低,电机根本转不起来。

设置代码很简单,我直接贴出来:

#include <driver/ledc.h>

#define LEDC_TIMER      LEDC_TIMER_0
#define LEDC_MODE       LEDC_LOW_SPEED_MODE
#define LEDC_CHANNEL    LEDC_CHANNEL_0
#define LEDC_DUTY_RES   LEDC_TIMER_13_BIT  // 13位分辨率,范围0~8191
#define LEDC_FREQUENCY  5000               // 5kHz 频率

void pwm_init(void) {
    ledc_timer_config_t timer_conf = {
        .speed_mode       = LEDC_MODE,
        .timer_num        = LEDC_TIMER,
        .duty_resolution  = LEDC_DUTY_RES,
        .freq_hz          = LEDC_FREQUENCY,
        .clk_cfg          = LEDC_AUTO_CLK
    };
    ledc_timer_config(&timer_conf);

    ledc_channel_config_t channel_conf = {
        .gpio_num   = 18,
        .speed_mode = LEDC_MODE,
        .channel    = LEDC_CHANNEL,
        .timer_sel  = LEDC_TIMER,
        .duty       = 0,
        .hpoint     = 0
    };
    ledc_channel_config(&channel_conf);
}

void set_motor_speed(uint32_t duty) {
    ledc_set_duty(LEDC_MODE, LEDC_CHANNEL, duty);
    ledc_update_duty(LEDC_MODE, LEDC_CHANNEL);
}

你看,13位分辨率,意味着占空比可以从0到8191之间任意设置。5kHz 的频率,电机运行起来很安静。

我的经验:分辨率越高,调速越细腻。但分辨率太高,频率就上不去。ESP32 的 LEDC 支持 1 到 20 位分辨率,我常用 13 位,兼顾精度和频率。

电机转速控制

有了 PWM,控制转速就是改占空比的事。但这里有个坑:电机启动时需要较大的占空比才能克服静摩擦力。我曾经做过一个项目,占空比从0直接跳到10%,电机纹丝不动,我还以为电路坏了。

后来我加了个软启动:占空比从0开始,每10ms增加一点,直到目标值。这样电机就能平稳启动。

代码实现:

void soft_start(uint32_t target_duty, uint32_t step_ms) {
    uint32_t current_duty = 0;
    while (current_duty < target_duty) {
        current_duty += 50;  // 每次增加50
        if (current_duty > target_duty) current_duty = target_duty;
        set_motor_speed(current_duty);
        vTaskDelay(pdMS_TO_TICKS(step_ms));
    }
}

为什么是50?因为我试过,步进太小启动太慢,步进太大又会抖动。50 是我试出来的经验值,你可以根据电机调整。

转速控制还有个关键点:闭环。开环控制下,电机负载变化时转速会波动。窗帘拉起来和放下去,阻力完全不同。我建议加个霍尔传感器或者编码器,实时读取转速,用 PID 算法调整占空比。

警告:不要长时间让电机堵转!占空比很大但电机不转,电流会飙升,可能烧坏驱动芯片。我在代码里加了堵转检测:如果设定转速和实际转速差距超过阈值,立即停止 PWM 输出。

死区时间处理

死区时间,这个词听起来有点吓人,其实很简单。电机从停止到开始转动,需要一定的占空比阈值。低于这个阈值,电机就是不转。这个阈值对应的占空比范围,就是死区。

为什么会这样?因为电机内部的摩擦力、线圈电阻、驱动器的死区电压等因素共同作用。不同电机,死区不一样。同一个电机,在不同温度下,死区也会变化。

我处理死区的方法很简单:

  1. 标定死区:上电后,让占空比从0逐渐增加,记录电机开始转动时的占空比。这个值就是死区下限。
  2. 映射占空比:把用户期望的0~100%速度,映射到死区下限到100%占空比之间。
  3. 反向死区:电机停止时,占空比要降到死区下限以下,否则电机不会停。

代码示例:

#define DEAD_ZONE_LOW  800   // 死区下限,电机开始转的最小占空比
#define DUTY_MAX       8191  // 13位分辨率最大值

uint32_t map_speed_to_duty(uint8_t speed_percent) {
    if (speed_percent == 0) return 0;  // 停止
    uint32_t duty_range = DUTY_MAX - DEAD_ZONE_LOW;
    return DEAD_ZONE_LOW + (duty_range * speed_percent / 100);
}

你看,这样处理后,用户设置50%速度,电机就能真正转到一半的转速,而不是在死区里空转。

避坑指南:我曾经遇到过,电机正转和反转的死区不一样。因为正反转时,电刷和换向器的接触角度不同。所以,正反转要分别标定死区,不能偷懒用一个值。

最后,总结一下这一讲的核心:

  • LEDC 是硬件 PWM,用起来简单,跑起来稳定
  • 频率选 1k~5kHz,分辨率选 13 位,兼顾噪音和精度
  • 软启动和死区处理,是电机控制的必修课
  • 闭环控制才是王道,开环只能应付玩具项目

下一讲,我们会把电机控制跟定时任务结合起来,实现真正的智能窗帘联动。到时候,你会看到 PWM 调速在真实场景中是怎么用的。