第二讲:电机控制基础与PWM测试

好,咱们进入第二讲。这一讲要解决一个核心问题——怎么让电机听话地转起来。

你想想看,窗帘电机本质上就是个直流电机。让它转不难,难的是让它转得稳、转得准、转得可控。这里面的关键,就是PWM。

PWM原理回顾——说白了就是“开关的艺术”

PWM,脉冲宽度调制。名字听着高大上,其实原理特别简单:

你有一个开关,每秒开关几千次。开的时间长,负载得到的平均电压就高;开的时间短,平均电压就低。就这么回事。

我习惯用“水龙头”来类比:

  • PWM频率 = 你开关水龙头的速度
  • 占空比 = 每次打开的时间比例
  • 平均电压 = 流出来的水量

举个例子,5V供电,50%占空比,电机得到的平均电压就是2.5V。100%占空比就是全速,0%就是停。

关键参数:

  • 频率:窗帘电机一般用1kHz-20kHz。太低会听到“滋滋”声,太高MOS管损耗大。我个人习惯用10kHz,兼顾噪音和效率。
  • 占空比:0%~100%,对应电机从停止到全速。
  • 分辨率:比如8位PWM,就是0~255共256级。够用吗?说实话,窗帘控制8位足够了。

避坑指南:我曾经在一个项目里把PWM频率设成了50Hz,结果电机没转,倒是听到喇叭一样的蜂鸣声。后来才发现,50Hz是人耳可听范围,电机线圈在“唱歌”。嗯,从此我记住了——PWM频率至少要在人耳听觉范围以上。

电机驱动芯片DRV8833——小身材大能量

单片机IO口直接驱动电机?别想了。IO口电流也就几毫安,电机启动瞬间可能几百毫安。所以需要驱动芯片。

DRV8833是我在窗帘项目里常用的芯片。为什么选它?

  • 双H桥,一路控制电机正反转,另一路可以控制刹车或第二路电机
  • 工作电压2.7V~10.8V,窗帘电机常用的5V或12V都支持
  • 每通道最大1.5A持续电流,窗帘电机绰绰有余
  • 内置过流保护、过热保护——省心

控制逻辑其实就一张真值表:

IN1 IN2 电机状态
0 0 刹车(滑行停止)
0 1 反转
1 0 正转
1 1 刹车(主动制动)

注意看,两个输入都是1时是主动制动,相当于把电机两端短路,利用反电动势快速停止。这个在窗帘到位停止时特别有用。

重要提醒:千万不要让IN1和IN2同时为1的同时还输入PWM!我见过有人这么干,结果芯片瞬间过流保护。正确的做法是:正转时IN1给PWM,IN2给0;反转时反过来。

PWM占空比与速度——不是简单的线性关系

很多人以为:50%占空比 = 50%速度。错!

为什么?因为电机有启动电压。你给个10%的占空比,电机可能根本不转。这叫“死区”。

我实测过一款窗帘电机,数据如下:

占空比 实际转速(RPM) 备注
0% 0 停止
10% 0 死区,不转
20% 12 刚启动,抖动
40% 35 平稳低速
60% 58 中速
80% 80 高速
100% 100 全速

看到了吗?从20%到100%,转速从12到100,近似线性但起点不是0。所以写代码时,我习惯做一个“死区补偿”:

// 实际占空比 = 目标占空比 * (100% - 死区) + 死区
// 假设死区为15%
uint8_t calc_duty(uint8_t target_percent) {
    if (target_percent == 0) return 0;
    if (target_percent < 15) target_percent = 15;  // 跳过死区
    return (uint8_t)((float)target_percent * 0.85f + 15.0f);
}

个人经验:死区值不是固定的,跟电机负载、电压都有关系。我建议在单元测试里专门写一个“死区标定”用例,自动找出当前电机的死区阈值。

编写PWM输出单元测试用例

好,理论讲完了,咱们来点实际的。怎么写PWM的单元测试?

我习惯把测试分成三个层次:

  1. 寄存器级:检查PWM寄存器是否写对了值
  2. 逻辑级:检查占空比计算、死区补偿是否正确
  3. 集成级:用示波器或逻辑分析仪抓实际波形

单元测试主要做前两层。下面是我常用的测试用例模板:

// 测试用例1:验证PWM初始化
void test_pwm_init(void) {
    pwm_init(10000);  // 10kHz
    TEST_ASSERT_EQUAL(10000, pwm_get_frequency());
    TEST_ASSERT_EQUAL(0, pwm_get_duty());  // 初始占空比为0
}

// 测试用例2:验证占空比设置范围
void test_pwm_duty_range(void) {
    // 边界测试
    TEST_ASSERT_TRUE(pwm_set_duty(0));    // 0% 应该成功
    TEST_ASSERT_TRUE(pwm_set_duty(100));  // 100% 应该成功
    TEST_ASSERT_FALSE(pwm_set_duty(101)); // 超出范围应该失败
    TEST_ASSERT_FALSE(pwm_set_duty(255)); // 非法值
}

// 测试用例3:验证死区补偿
void test_dead_zone_compensation(void) {
    // 假设死区为15%
    pwm_set_dead_zone(15);
    
    TEST_ASSERT_EQUAL(0, calc_duty(0));    // 0% -> 0%
    TEST_ASSERT_EQUAL(15, calc_duty(10));  // 10% -> 15%(跳过死区)
    TEST_ASSERT_EQUAL(15, calc_duty(15));  // 15% -> 15%
    TEST_ASSERT_EQUAL(57, calc_duty(50));  // 50% -> 57%(线性映射)
    TEST_ASSERT_EQUAL(100, calc_duty(100));// 100% -> 100%
}

// 测试用例4:验证正反转控制
void test_motor_direction(void) {
    motor_set_direction(MOTOR_FORWARD);
    TEST_ASSERT_EQUAL(1, GPIO_ReadPin(IN1_PIN));
    TEST_ASSERT_EQUAL(0, GPIO_ReadPin(IN2_PIN));
    
    motor_set_direction(MOTOR_REVERSE);
    TEST_ASSERT_EQUAL(0, GPIO_ReadPin(IN1_PIN));
    TEST_ASSERT_EQUAL(1, GPIO_ReadPin(IN2_PIN));
    
    motor_set_direction(MOTOR_STOP);
    TEST_ASSERT_EQUAL(0, GPIO_ReadPin(IN1_PIN));
    TEST_ASSERT_EQUAL(0, GPIO_ReadPin(IN2_PIN));
}

// 测试用例5:验证PWM频率稳定性
void test_pwm_frequency_stability(void) {
    pwm_init(10000);
    uint32_t start = get_system_ticks();
    for (int i = 0; i < 1000; i++) {
        pwm_set_duty(50);
    }
    uint32_t elapsed = get_system_ticks() - start;
    // 1000次设置应该在10ms内完成
    TEST_ASSERT_TRUE(elapsed < 10);
}

测试要点总结:

  • 边界值测试:0%、100%、超出范围的值
  • 死区补偿逻辑:确保低占空比被正确映射
  • 方向控制:IN1/IN2组合是否正确
  • 性能测试:频繁设置占空比会不会卡顿

我曾经踩过的坑:有一次测试PWM,寄存器值明明写对了,但电机就是不动。查了半天,发现是GPIO复用功能没配置——PWM输出引脚被当成了普通IO。所以测试用例里一定要加一条:验证GPIO模式是否为复用功能。

嗯,这一讲的内容就到这里。PWM看似简单,但细节不少。下一讲我们会聊电机的位置控制——怎么让窗帘停在你想停的位置。那才是真正考验算法的地方。

记住:测试不是找茬,是帮自己省时间。你想想看,一个PWM死区问题,如果在产线上才发现,那代价可就大了。