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信号真正接到电机驱动板上,让电机转起来。