找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

2647

积分

0

好友

355

主题
发表于 2 小时前 | 查看: 1| 回复: 0

我们身边的许多电子产品,如汽车、风扇、照明灯和玩具,都离不开PWM(脉冲宽度调制)技术的默默支持。它通过调节脉冲信号的特性,来实现对电机速度、灯光亮度等的精细控制。本文将深入探讨单片机实现PWM输出的几种方式,并以STM32为例,详细讲解硬件PWM的配置步骤与常见问题。

什么是PWM?

PWM的全称是Pulse Width Modulation,即脉冲宽度调制。

网上的解释可能有些抽象,但通过下面这张图,你就能直观地理解PWM——它其实就是由一系列高低电平变化组成的脉冲信号。

脉冲宽度与周期示意图

通过改变这个信号的频率(脉冲周期)和占空比(高电平在一个周期内的比例),就能将其应用在各种各样的场合,这也是嵌入式开发中控制外部设备的常用手段。

PWM波形与占空比关系图

PWM常见输出方式

从本质上讲,产生PWM波就是控制一个IO口以特定的时间周期,交替输出高电平和低电平。实现这一目标的方法有多种,我们可以根据对CPU资源的占用和实现复杂度,将其分为几个级别。

1. 新手级别

while循环中,使用阻塞延时来控制IO口高低电平输出:

while(1)
{
  IO口高电平
  Delay阻塞延时
  IO口低电平
  Delay阻塞延时
}

这里的阻塞延时可以是简单的软件模拟延时,也可以是定时器的阻塞等待。这种方法简单直白,但会完全占用CPU,且延时精度容易受干扰。

2. 入门级别

while循环中,使用非阻塞延时来控制IO口:

while(1)
{
  IO口高电平
  Delay非阻塞延时
  IO口低电平
  Delay非阻塞延时
}

非阻塞延时可以通过检测定时器标志位,或者利用RTOS(实时操作系统)提供的延时函数来实现。这种方法释放了CPU,允许其在延时期间处理其他任务,但程序流程依然需要不断查询状态。

3. 熟悉级别

利用定时器中断来控制IO口高低电平:

配置定时器中断 -> 启动定时器 -> 在中断服务函数中翻转IO口电平...

这种方法将PWM生成的时序完全交给定时器硬件和中断机制,主程序无需干预波形生成,效率更高。

4. 熟练级别

直接使用单片机的硬件PWM外设功能:

配置PWM对应的IO引脚,以及定时器的PWM输出模式 -> 设置频率和占空比 -> 启动定时器,PWM波形自动输出...

这是最理想的方式,配置完成后,硬件会自动输出PWM波,完全无需CPU干预。应用程序可以这样调用:

void AppTask(void *p_arg)
{
  PWM_TIM_Configuration(); // 硬件配置
  PWM_Output(频率, 占空比); // 设定输出参数

  while(1)
  {
    //自己的应用代码
  }
}

几种方式的比较:
前三种方式都需要CPU不同程度地参与PWM波的生成,会占用CPU资源。特别是前两种,不仅占用高,精度也相对较差。第三种中断方式在PWM频率较高时,也会频繁打断CPU,消耗可观的资源。而第四种硬件PWM方式,则是将这项工作完全交给了定时器外设,是高效且精准的首选方案。

硬件输出PWM实例(以STM32F1为例)

下面我们以常见的STM32F103系列单片机为例,详细讲解如何使用硬件定时器输出PWM波形。

首先,定义PWM定时器相关的宏,这决定了定时器的计数时钟基础。

//定时器计数时钟(1M次/秒)
#define PWM_COUNTER_CLOCK         1000000   
//预分频值(与系统时钟、计数值有关)
#define PWM_PRESCALER_VALUE       (SystemCoreClock/PWM_COUNTER_CLOCK - 1)

PWM硬件配置函数

这个函数完成了GPIO、定时器时基和PWM输出通道的初始化。

/**
  * @brief  定时器PWM输出配置
  * @param  无
  * @retval 无
  */
void PWM_TIM_Configuration(void)
{
  GPIO_InitTypeDef        GPIO_InitStructure;
  TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
  TIM_OCInitTypeDef       TIM_OCInitStructure;

  /* 时钟配置 */
  RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
  RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);

  /* 引脚配置:以PA0(TIM2_CH1)为例 */
  GPIO_InitStructure.GPIO_Pin   = GPIO_Pin_0;
  GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
  GPIO_InitStructure.GPIO_Mode  = GPIO_Mode_AF_PP; // 复用推挽输出
  GPIO_Init(GPIOA, &GPIO_InitStructure);

  /* 时基配置 */
  TIM_TimeBaseStructure.TIM_Prescaler = PWM_PRESCALER_VALUE;         //预分频值
  TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;        //向上计数
  TIM_TimeBaseStructure.TIM_Period = 0xFFFF;                         //定时周期(自动重装载值,暂定)
  TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;            //时钟分频
  TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);

  /* PWM模式配置:通道1 */
  TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;                  //PWM模式1
  TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;      //使能输出
  TIM_OCInitStructure.TIM_Pulse = 0;                                 //初始占空比(比较值,暂定)
  TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;          //输出极性:高电平有效
  TIM_OC1Init(TIM2, &TIM_OCInitStructure);                           //初始化通道1
}

PWM输出控制函数

这个函数用于动态调整PWM波的频率和占空比。

/**
* @brief  输出PWM 
* @param  Frequency:频率            
          Dutycycle:占空比(0-100) 
* @retval 无 
*/
void PWM_Output(uint32_t Frequency, uint32_t Dutycycle)
{
  uint32_t tim_period;
  uint32_t tim_pulse;
  tim_period = PWM_COUNTER_CLOCK/Frequency - 1;                      //计算出计数周期(决定输出频率)
  tim_pulse  = (tim_period + 1)*Dutycycle / 100;                     //计算出脉宽值(决定PWM占空比)

  TIM_Cmd(TIM2, DISABLE);                                            //修改参数前先关闭定时器
  TIM_SetCounter(TIM2, 0);                                           //计数清零
  TIM_SetAutoreload(TIM2, tim_period);                               //更改自动重装载值(频率)
  TIM_SetCompare1(TIM2, tim_pulse);                                  //更改比较值(占空比)
  TIM_Cmd(TIM2, ENABLE);                                             //重新使能定时器
}

应用代码示例

在应用程序中,只需初始化并调用输出函数即可。

void AppTask(void *p_arg)
{
  PWM_TIM_Configuration(); // 硬件配置
  PWM_Output(1000, 20);    // 输出1KHz频率,20%占空比的PWM波
  while(1)
  {
    //应用代码
  }
}

使用逻辑分析仪测量上述代码输出的波形,可以看到准确的1KHz、20%占空比方波。

逻辑分析仪测量的PWM波形

说明: 本例使用的是STM32标准外设库,有助于深入理解寄存器和底层原理。如果你想快速实现功能,也可以使用STM32CubeMX工具进行图形化配置,它能自动生成初始化代码。

STM32CubeMX配置PWM界面

硬件PWM配置注意事项

想要更精确地控制PWM并满足复杂应用需求,就需要深入了解其原理。下面列举几个常见的配置要点。

1. 引脚重映射

有些定时器通道的引脚是默认的,有些则需要重映射(Remap)。例如,如果你想使用STM32F1的PB11引脚作为TIM2_CH4输出PWM,就需要查看数据手册的引脚定义表。

STM32引脚定义表示例(PB11对应TIM2_CH4)

从表中可以看到PB11的复用功能包含TIM2_CH4,但通常它可能不是默认映射,此时需要在代码中开启重映射功能:

//使能AFIO时钟(重映射必需)
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);

//将TIM2通道4完全重映射到PB11(具体映射方式需查手册)
GPIO_PinRemapConfig(GPIO_FullRemap_TIM2, ENABLE);

2. 频率与占空比精度

精度受限于定时器的位数。如果使用16位定时器,那么周期值(TIM_Period)和比较值(TIM_Pulse)都不能超过65535。我们的计算函数:

tim_period = PWM_COUNTER_CLOCK/Frequency - 1;
tim_pulse  = (tim_period + 1)*Dutycycle / 100;

需要确保tim_periodtim_pulse的值在0-65535之间。如果想获得极低的频率(如0.01Hz)或极高的占空比精度(0.01%),可以考虑使用32位定时器(如STM32F4系列的TIM2/TIM5),其计数范围大大增加,精度也更高。

3. 更多细节

不同系列的STM32单片机,其硬件PWM的配置寄存器可能略有差异,建议在开发时多参考对应型号的官方参考手册和标准外设库例程。如今大多数现代单片机都集成了硬件PWM功能,这是最推荐的方式。如果你的单片机没有硬件PWM,那么上文提到的“定时器中断”方式是一个不错的软件替代方案。

希望本文对你在云栈社区学习和探索嵌入式技术有所帮助。通过理解从软件模拟到硬件控制的演进,你能更好地根据项目需求选择最合适的PWM实现方式,从而设计出更高效、更可靠的产品。




上一篇:三菱电机出售汽车零部件业务50%股权,富士康深化日本供应链合作
下一篇:Python量化新手必看:3步上手策略开发与QMT实盘指南
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-3-23 07:46 , Processed in 0.672114 second(s), 41 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表