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 如何根据项目需求选型?一张决策流程图

说了这么多,到底怎么选?我画了张决策图,你照着走一遍基本就有答案了。

PWM芯片选型决策流程 开始选型 需要多少路独立PWM? ≤6路 Arduino 够用 >6路 STM32/ESP32 PWM频率 > 10kHz? 是 STM32 优先 否 ESP32 更灵活 需要无线通信? 是 ESP32 不二之选 否 STM32 更稳定 注:频率 > 10kHz 时,Arduino 基本不可用

2.4 我的选型清单:三个典型场景

光讲理论不够,我直接给你三个我做过项目的选型方案,你照着套就行。

  1. 场景一:LED智能照明(12路RGB调光)
    • 选型:ESP32
    • 理由:通道数够(16路),频率可单独配置,自带WiFi做手机控制
    • 注意:分辨率设8位就够,人眼分辨不出更高精度
  2. 场景二:无刷电机FOC驱动(3对互补PWM)
    • 选型:STM32F405
    • 理由:高级定时器硬件互补输出+死区插入,DMA自动更新占空比
    • 注意:死区时间要根据MOS管开关速度算,设小了会烧管子
  3. 场景三:简易舵机控制(2路50Hz PWM)
    • 选型:Arduino Nano
    • 理由:需求简单,开发快,成本低(一块板子十几块钱)
    • 注意:用Servo库,别自己写寄存器,库帮你处理了定时器冲突

最后说一句:选芯片别只看纸面参数。我吃过一次亏——项目选型时觉得ESP32的16路PWM够用,结果实际调试发现,WiFi开启后LEDC的时钟会受RF干扰,PWM频率有微小抖动。虽然不影响LED调光,但要是做精密电机控制就完蛋了。所以,关键项目一定要先拿开发板跑一下实际负载,别光看数据手册。