2. 硬件平台选型:常用MCU的PWM外设对比
做PWM输出,第一步就是选芯片。这事儿看着简单,但选错了后面全是坑。我这些年用过的MCU少说也有十几种,STM32、ESP32、Arduino这三家是最常见的。今天咱们就掰开揉碎聊聊,它们各自的PWM外设到底差在哪儿。
核心观点:没有最好的芯片,只有最合适的。选型的关键是搞清楚你的PWM需求——频率、分辨率、通道数、实时性,这四个维度卡死了,芯片自然就浮出水面了。
2.1 三款主流MCU的PWM外设速览
先给个总览表格,方便你快速对比。我习惯把关键参数列出来,一眼就能看出差距。
| 特性 | STM32 (以F103为例) | ESP32 | Arduino (ATmega328P) |
|---|---|---|---|
| PWM分辨率 | 16位 (0~65535) | 8~16位可配置 | 8位 (0~255) |
| 独立通道数 | 最多30路 (高级定时器) | 16路 (LEDC控制器) | 6路 (3个定时器各2路) |
| 最高频率 | 72MHz (主频直接分频) | 40MHz (APB时钟) | 8MHz (系统时钟分频) |
| 互补输出 | 支持 (带死区插入) | 不支持 (需软件模拟) | 不支持 |
| DMA支持 | 全系列支持 | 部分支持 (I2S/DAC) | 不支持 |
| 开发难度 | 中等 (寄存器/HAL库) | 较低 (Arduino框架) | 低 (直接写寄存器) |
你看,光分辨率一项,Arduino的8位和STM32的16位就差了两个数量级。什么意思?说白了,Arduino只能输出256级占空比,而STM32能输出65536级。做呼吸灯没问题,但你要是调一个高精度伺服电机,256级根本不够用。
2.2 深入拆解:每个芯片的脾气秉性
2.2.1 STM32:PWM外设的瑞士军刀
STM32的定时器系统是我用过最灵活的。它把定时器分成了基本、通用、高级三类。高级定时器(TIM1/TIM8)支持互补输出和死区插入,做电机控制、半桥驱动简直是绝配。
我记得有个项目要做无刷电机FOC控制,需要三对互补PWM,每对之间还要有精确的死区时间。当时用STM32F405的高级定时器,直接硬件生成,死区时间设个几百纳秒,稳得很。要是换别的芯片,光软件模拟死区就能让你调到头秃。
我的习惯:做多路PWM输出时,优先用STM32的DMA+定时器更新事件。把占空比数据放在一个数组里,DMA自动搬运到CCR寄存器,CPU全程不用管。这样哪怕输出32路PWM,CPU占用率也几乎为零。
代码示例:STM32 HAL库配置PWM输出(以TIM2通道1为例)
// 初始化定时器PWM
TIM_HandleTypeDef htim2;
TIM_OC_InitTypeDef sConfigOC = {0};
htim2.Instance = TIM2;
htim2.Init.Prescaler = 71; // 72MHz / (71+1) = 1MHz
htim2.Init.Period = 999; // PWM频率 = 1MHz / (999+1) = 1kHz
htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
HAL_TIM_PWM_Init(&htim2);
sConfigOC.OCMode = TIM_OCMODE_PWM1;
sConfigOC.Pulse = 500; // 占空比50%
sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH;
HAL_TIM_PWM_ConfigChannel(&htim2, &sConfigOC, TIM_CHANNEL_1);
HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1);
嗯,这里要注意:预分频器和自动重装载值的计算一定要确认好。我见过有人把Prescaler设成0,结果PWM频率直接飙到72MHz,示波器一看全是毛刺。
2.2.2 ESP32:WiFi加持的PWM多面手
ESP32的PWM靠的是LEDC控制器,名字叫LEDC,但别被它骗了——它不光能调LED,还能做电机控制、舵机驱动。最大优势是通道数多,16路独立PWM,而且频率和分辨率可以每个通道单独设置。
我曾经做过一个智能灯控项目,需要同时控制12路RGB LED,每路还要不同的频率(有些灯珠对频率敏感)。ESP32的LEDC正好派上用场,每个通道独立配置,互不干扰。要是用STM32,虽然也能做,但定时器资源分配起来就麻烦多了。
避坑指南:ESP32的LEDC虽然灵活,但有个坑——频率和分辨率是互相制约的。比如你要20kHz的PWM,分辨率最高只能到10位(1024级)。为什么?因为LEDC的时钟源是80MHz,分频后要同时满足频率和分辨率,公式是:分辨率位数 = log2(80MHz / 频率)。你想想看,频率越高,分辨率必然下降。
代码示例:ESP32 Arduino框架配置PWM
// LEDC配置
const int freq = 5000; // 5kHz
const int resolution = 8; // 8位分辨率
const int channel = 0; // 通道0
const int pin = 2; // GPIO2
ledcSetup(channel, freq, resolution);
ledcAttachPin(pin, channel);
ledcWrite(channel, 128); // 50%占空比
这段代码看着简单,但背后有个细节:ledcSetup函数会自动计算分频系数。如果你设的频率和分辨率组合不合理,它会自动降低分辨率来保频率。所以我的建议是:先确定你需要的频率,再看这个频率下能拿到多少位分辨率,够用就行。
2.2.3 Arduino (ATmega328P):简单粗暴的入门之选
Arduino的PWM,说白了就是三个定时器(Timer0、Timer1、Timer2)各分出两路。Timer0还被系统用做delay()和millis(),所以改它的频率会影响系统时钟。我刚开始玩Arduino时不知道这个,把Timer0的预分频改了,结果delay(1000)变成了delay(8000),排查了半天才发现。
Arduino的PWM频率是固定的:约490Hz(5、6、9、10脚)和约980Hz(3、11脚)。这个频率做LED调光没问题,但做电机控制就太低了——电机会发出刺耳的啸叫声。为什么?因为人耳能听到20Hz~20kHz,490Hz刚好在可听范围内。
我的经验:如果非要用Arduino做电机PWM,可以修改定时器的分频寄存器来提升频率。比如把Timer1的预分频从64改成8,PWM频率就能从490Hz升到3.9kHz。代价是分辨率从8位降到6位。值不值得?看你的电机能不能忍那个噪音。
代码示例:修改Arduino PWM频率(以D9脚为例,Timer1)
// 将D9脚的PWM频率从490Hz提升到3.9kHz
// 原理:修改Timer1的预分频器
void setup() {
pinMode(9, OUTPUT);
// 直接操作寄存器
TCCR1B = (TCCR1B & 0b11111000) | 0x02; // 预分频设为8
// 默认是0x03(预分频64),改成0x02(预分频8)
}
void loop() {
analogWrite(9, 128); // 50%占空比
}
嗯,这里要提醒一句:改了Timer1的频率,会影响所有用到Timer1的库(比如Servo库)。所以改之前想清楚,别让其他功能跟着遭殃。
2.3 如何根据项目需求选型?一张决策流程图
说了这么多,到底怎么选?我画了张决策图,你照着走一遍基本就有答案了。
2.4 我的选型清单:三个典型场景
光讲理论不够,我直接给你三个我做过项目的选型方案,你照着套就行。
- 场景一:LED智能照明(12路RGB调光)
- 选型:ESP32
- 理由:通道数够(16路),频率可单独配置,自带WiFi做手机控制
- 注意:分辨率设8位就够,人眼分辨不出更高精度
- 场景二:无刷电机FOC驱动(3对互补PWM)
- 选型:STM32F405
- 理由:高级定时器硬件互补输出+死区插入,DMA自动更新占空比
- 注意:死区时间要根据MOS管开关速度算,设小了会烧管子
- 场景三:简易舵机控制(2路50Hz PWM)
- 选型:Arduino Nano
- 理由:需求简单,开发快,成本低(一块板子十几块钱)
- 注意:用Servo库,别自己写寄存器,库帮你处理了定时器冲突
最后说一句:选芯片别只看纸面参数。我吃过一次亏——项目选型时觉得ESP32的16路PWM够用,结果实际调试发现,WiFi开启后LEDC的时钟会受RF干扰,PWM频率有微小抖动。虽然不影响LED调光,但要是做精密电机控制就完蛋了。所以,关键项目一定要先拿开发板跑一下实际负载,别光看数据手册。