4、信号生成:使用Arduino生成50Hz PWM信号,使用STM32生成PWM信号
好,咱们进入实战环节。前面讲了PWM的原理和电机驱动的底层逻辑,现在该动手了。这一章,我带你用两种最常见的平台——Arduino和STM32,把PWM信号实实在在地生成出来。
为什么选这两个?说白了,Arduino适合快速验证想法,STM32才是工业级产品的标配。我个人的习惯是:原型阶段用Arduino跑通逻辑,量产阶段再移植到STM32上优化性能。你想想看,这样既能快速迭代,又能保证最终产品的可靠性。
4.1 用Arduino生成50Hz PWM信号
50Hz这个频率,在无人机领域太常见了。标准的航模舵机、部分电调,都认这个频率。它的周期是20ms,也就是每20ms产生一个脉冲。
Arduino的analogWrite()函数默认频率是490Hz或980Hz(取决于引脚),这显然不符合我们的需求。所以,得用底层寄存器操作来调整。
核心思路:直接操作定时器寄存器,改变PWM的频率和占空比。
4.1.1 代码实现:Arduino Uno (ATmega328P)
以Arduino Uno为例,它有三个定时器。Timer0用于delay()和millis(),我们不动它。Timer1是16位定时器,最适合生成50Hz信号。
// Arduino Uno - 生成50Hz PWM信号 (Pin 9, 10)
// 使用Timer1,16位模式
void setup() {
// 设置引脚9和10为输出
pinMode(9, OUTPUT);
pinMode(10, OUTPUT);
// 关闭全局中断(短暂操作)
cli();
// 设置Timer1为Fast PWM模式,ICR1作为TOP值
// WGM13:0 = 1110 (模式14)
TCCR1A = 0;
TCCR1B = 0;
TCCR1A |= (1 << WGM11); // 模式14的高位
TCCR1B |= (1 << WGM13) | (1 << WGM12); // 模式14的低位
// 设置预分频器为256
// CS12:0 = 100 (256分频)
TCCR1B |= (1 << CS12);
// 设置TOP值 (ICR1) 来实现50Hz
// 公式: 频率 = 16MHz / (预分频 * (ICR1 + 1))
// 50Hz = 16,000,000 / (256 * (ICR1 + 1))
// ICR1 + 1 = 16,000,000 / (256 * 50) = 1250
// ICR1 = 1249
ICR1 = 1249;
// 设置占空比 (OCR1A 和 OCR1B)
// 占空比 = OCR1x / (ICR1 + 1)
// 例如: 1.5ms脉宽 (中立点) -> 占空比 = 1.5ms / 20ms = 7.5%
// OCR1A = 1250 * 0.075 = 93.75 -> 取整94
OCR1A = 94; // 约1.5ms脉宽 (Pin 9)
OCR1B = 94; // 约1.5ms脉宽 (Pin 10)
// 设置OC1A和OC1B为非反相PWM输出
TCCR1A |= (1 << COM1A1); // 清除OC1A在比较匹配时置位
TCCR1A |= (1 << COM1B1); // 清除OC1B在比较匹配时置位
// 重新开启全局中断
sei();
}
void loop() {
// 主循环可以干别的事
// 比如动态调整占空比
// 这里留空,保持信号稳定输出
}
我的经验:第一次调这个代码时,我忘了关全局中断,结果millis()函数直接罢工了。后来养成习惯:操作定时器寄存器前,先cli(),完事再sei()。嗯,这算是个小坑,你记住就行。
4.1.2 验证信号
代码烧进去后,用示波器看Pin 9或Pin 10。你应该能看到:
- 频率:50Hz(周期20ms)
- 脉宽:约1.5ms(占空比7.5%)
- 幅值:5V(Arduino逻辑电平)
如果你没有示波器,用逻辑分析仪也行。我早期做项目时,就靠一个几十块钱的逻辑分析仪调通了所有PWM信号。
4.2 用STM32生成PWM信号
STM32就灵活多了。它的定时器功能强大,可以轻松生成任意频率和占空比的PWM。而且,STM32的PWM分辨率更高,适合对电机控制精度要求高的场景。
我个人偏爱STM32的HAL库,虽然有人说它臃肿,但胜在可读性好,移植方便。当然,如果你追求极致性能,直接操作寄存器也行。这里我用HAL库演示,因为更贴近实际项目开发。
4.2.1 硬件准备
以STM32F103C8T6(蓝色药丸板)为例,我们用TIM2的通道1(PA0)输出PWM。
| 引脚 | 定时器通道 | 功能 |
|---|---|---|
| PA0 | TIM2_CH1 | PWM输出 |
| GND | - | 共地 |
4.2.2 代码实现:STM32 HAL库
我们用STM32CubeMX配置好工程,然后添加如下代码:
// main.c - STM32F103 PWM生成 (50Hz)
// 使用TIM2,通道1 (PA0)
#include "main.h"
TIM_HandleTypeDef htim2;
// 定时器初始化函数
void MX_TIM2_Init(void) {
TIM_OC_InitTypeDef sConfigOC = {0};
htim2.Instance = TIM2;
htim2.Init.Prescaler = 7199; // 72MHz / (7199+1) = 10kHz
htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
htim2.Init.Period = 199; // 10kHz / (199+1) = 50Hz
htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
htim2.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE;
HAL_TIM_PWM_Init(&htim2);
sConfigOC.OCMode = TIM_OCMODE_PWM1;
sConfigOC.Pulse = 15; // 占空比 = (15+1) / (199+1) = 8% -> 约1.6ms脉宽
sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH;
sConfigOC.OCFastMode = TIM_OCFAST_DISABLE;
HAL_TIM_PWM_ConfigChannel(&htim2, &sConfigOC, TIM_CHANNEL_1);
}
// 启动PWM输出
void Start_PWM_Generation(void) {
HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1);
}
int main(void) {
HAL_Init();
SystemClock_Config(); // 配置系统时钟为72MHz
MX_GPIO_Init();
MX_TIM2_Init();
Start_PWM_Generation();
while (1) {
// 主循环可以动态调整占空比
// 例如:__HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, new_pulse_value);
}
}
注意:预分频器和自动重装载值的计算一定要仔细。我曾经在项目里把预分频算错了一位,结果电机转得跟抽风似的,查了半天才发现是频率不对。公式再强调一遍:
PWM频率 = 定时器时钟 / ((预分频器+1) * (自动重装载值+1))
4.2.3 动态调整占空比
实际飞行中,我们需要根据遥控器输入或飞控算法实时调整电机转速。这时候,动态修改占空比就很重要了。
// 动态调整占空比的函数
void Set_PWM_DutyCycle(uint16_t pulse_value) {
// pulse_value范围:0 到 Period (199)
// 对应脉宽:0% 到 100%
__HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, pulse_value);
}
// 使用示例:在loop中根据某个变量调整
uint16_t target_pulse = 100; // 50%占空比
Set_PWM_DutyCycle(target_pulse);
4.3 Arduino vs STM32:怎么选?
我把两个平台的对比整理了一下,方便你根据项目阶段选择:
| 对比项 | Arduino | STM32 |
|---|---|---|
| 开发速度 | 快,几分钟就能跑起来 | 慢,需要配置CubeMX和HAL库 |
| PWM分辨率 | 8位(0-255) | 16位(0-65535),可更高 |
| 频率灵活性 | 受限于定时器,调整麻烦 | 任意频率,寄存器随便配 |
| 多通道支持 | 有限(Uno只有6路PWM) | 丰富(F103有十几个定时器通道) |
| 适用场景 | 原型验证、教学演示 | 量产产品、高性能飞控 |
我的建议:如果你刚开始学无人机,先用Arduino把PWM的概念玩透。等你理解了占空比、频率、死区这些概念后,再切换到STM32做真正的飞控开发。别一上来就啃STM32,容易劝退。
4.4 避坑指南
最后,分享几个我踩过的坑:
- 电平不匹配:Arduino输出5V,STM32输出3.3V。如果直接驱动5V的电调,STM32需要加电平转换。我曾经偷懒没加,结果电调不识别信号,折腾了两天。
- 共地问题:PWM信号的地必须和电机驱动板的地连在一起。不共地,信号就是浮空的,电机乱转。
- 启动瞬间:STM32上电时,GPIO默认是浮空输入。如果此时电机驱动已经上电,可能会收到一个不确定的PWM信号,导致电机猛转一下。解决办法:在初始化代码中,先把PWM引脚拉低,再配置定时器。
好了,这一章的内容就到这。你可以在Arduino上跑通50Hz信号,再移植到STM32上试试。下一章,我们会把这些PWM信号真正接到电机驱动板上,让电机转起来。