第二讲:电机控制基础与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的单元测试?
我习惯把测试分成三个层次:
- 寄存器级:检查PWM寄存器是否写对了值
- 逻辑级:检查占空比计算、死区补偿是否正确
- 集成级:用示波器或逻辑分析仪抓实际波形
单元测试主要做前两层。下面是我常用的测试用例模板:
// 测试用例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死区问题,如果在产线上才发现,那代价可就大了。