3、PWM生成方式:硬件PWM与软件PWM的区别、定时器产生PWM的原理、STM32的PWM配置

说到PWM生成,很多新手第一反应就是——用延时或者循环翻转IO口。嗯,这确实能出波形,但你真的敢在项目里这么用吗?

我个人习惯把PWM生成方式分成两大类:硬件PWM软件PWM。别看它们都能输出方波,背后的差距可不是一星半点。

3.1 硬件PWM vs 软件PWM:到底差在哪?

先说说软件PWM。说白了,就是用CPU的指令去模拟PWM波形。比如你写个while循环,里面delay一下,然后翻转GPIO,再delay一下,再翻转回来。我刚开始学电机控制时就这么干过,结果电机一跑起来,整个系统卡得跟幻灯片似的。

为什么会这样?因为软件PWM有个致命伤——CPU被完全占用了。你想想看,PWM的频率越高,你就要越频繁地去翻转IO口,CPU根本没空干别的事。而且,一旦你插了个中断进去,波形立马变形,电机就开始抖。

硬件PWM就不一样了。它靠的是芯片内部的外设,比如定时器、PWM发生器。你只要配好寄存器,它自己就能干活,CPU完全解放出来。我在项目中遇到过好几次,客户要求电机跑20kHz的PWM,同时还要做CAN通信和电流采样。用软件PWM?想都别想。硬件PWM才是正解。

核心区别一句话总结:

  • 软件PWM:CPU全程参与,精度低,占用高,适合低频、简单场景
  • 硬件PWM:外设独立运行,精度高,零CPU占用,适合电机控制等实时场景

我的建议:但凡你的电机转速超过1000rpm,或者PWM频率超过1kHz,就别考虑软件PWM了。我曾经在一个小项目里偷懒用了软件PWM,结果调试了三天,最后还是老老实实换成了硬件方案。

3.2 定时器产生PWM的原理

硬件PWM的核心,其实就是定时器。你可能会问:定时器不是用来计时的吗?怎么还能产生PWM?

嗯,这里要注意。定时器内部有一个关键模块叫比较器。它的工作原理很简单:

  1. 定时器从0开始向上计数,一直数到某个设定值(ARR,自动重装载值)
  2. 在计数的过程中,会跟另一个设定值(CCR,捕获比较值)做比较
  3. 当计数值小于CCR时,输出高电平;大于等于CCR时,输出低电平(或者反过来)
  4. 计到ARR后,归零重新开始

你看,这不就是一个PWM波形吗?ARR决定了PWM的周期(频率),CCR决定了占空比。

举个例子。假设ARR设为999,CCR设为300。那么在一个周期里,前300个计数周期输出高电平,后699个计数周期输出低电平。占空比就是30%。

关键参数:

  • ARR(Auto-Reload Register):自动重装载值,决定PWM频率
  • CCR(Capture/Compare Register):捕获比较值,决定PWM占空比
  • PSC(Prescaler):预分频器,用于降低定时器时钟频率

我记得有一次调试一个BLDC电机,PWM频率要求20kHz,但定时器时钟是72MHz。如果不分频,ARR就得设成3599。算下来分辨率还行。但如果你要更精细的占空比调节,就得调整PSC和ARR的配合。这个后面我会细讲。

3.3 STM32的PWM配置实战

好了,理论说完了,咱们直接上代码。以STM32F103为例,我用的是TIM2的通道1,输出PWM到PA0引脚。

配置步骤其实就四步:

  1. 使能定时器和GPIO时钟
  2. 配置GPIO为复用推挽输出
  3. 配置定时器时基单元(PSC、ARR)
  4. 配置PWM模式并启动

下面是我常用的配置代码:

// 1. 使能时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);

// 2. 配置GPIO
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;  // 复用推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);

// 3. 配置定时器时基
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_TimeBaseStructure.TIM_Period = 999;           // ARR = 999
TIM_TimeBaseStructure.TIM_Prescaler = 71;          // PSC = 71
TIM_TimeBaseStructure.TIM_ClockDivision = 0;
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);

// 4. 配置PWM模式
TIM_OCInitTypeDef TIM_OCInitStructure;
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;  // PWM模式1
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
TIM_OCInitStructure.TIM_Pulse = 300;                // CCR = 300,占空比30%
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;
TIM_OC1Init(TIM2, &TIM_OCInitStructure);

// 5. 启动定时器
TIM_Cmd(TIM2, ENABLE);

这段代码跑起来,PA0上就会输出一个20kHz、占空比30%的PWM波形。为什么是20kHz?来算一下:

定时器时钟 = 72MHz / (71+1) = 1MHz
PWM频率 = 1MHz / (999+1) = 1kHz

嗯?只有1kHz?不对。我故意设了个低频率的例子。如果你要20kHz,ARR应该设成49(1MHz / 50 = 20kHz)。但ARR太小,分辨率就低了。所以实际项目中,我会调整PSC让定时器时钟更合适。

我曾经踩过的坑:

  • 忘了使能GPIO时钟,结果PWM死活出不来
  • GPIO模式配成了推挽输出而不是复用推挽,波形不对
  • PSC和ARR的数值搞反了,频率完全不对

嗯,这些低级错误,我至少犯过两遍。你调试的时候多留个心眼。

3.4 动态调整占空比

电机控制中,占空比不是固定的。你需要根据控制算法实时调整。怎么做?很简单,直接修改CCR寄存器:

// 动态设置占空比为50%
TIM_SetCompare1(TIM2, 500);

这个函数其实就是往TIM2的CCR1寄存器里写值。你可以在主循环里、中断里、或者PID计算完成后调用它。硬件会自动根据新的CCR值调整PWM波形,完全不需要你操心。

我个人习惯在定时器更新中断里修改占空比,这样可以保证跟PWM周期同步,避免出现半周期突变的情况。当然,如果你的控制频率不高,在主循环里改也行。

一个小技巧:如果你想让电机启动更平滑,可以写一个软启动函数,让占空比从0逐渐增加到目标值。比如每10ms增加5%,这样电机就不会猛地一窜。我在做无人机电调时就用过这个办法,效果很好。

好了,这一章的内容就到这里。硬件PWM和软件PWM的区别、定时器产生PWM的原理、以及STM32的具体配置,我都讲清楚了。下一章我会聊聊PWM的互补输出和死区时间,这对H桥驱动和BLDC控制特别重要。到时候见。