STM32CubeMX PWM 输出完整示例(带波形)

AI助手已提取文章相关产品:

开发板推荐:天空星STM32F407VET6开发板

超高性价比 STM32主控 | 超高主频 | 一板兼容百芯 | 比赛神器 | 沉金彩色丝印

STM32CubeMX PWM 输出实战全解析(附真实波形验证)

你有没有遇到过这样的场景:明明代码写得“没问题”,可示波器一接上,PWM波形却抖得像地震图?或者占空比调来调去,电机转速就是不线性变化?又或者多个通道输出时,相位乱成一团麻?

别急——这背后往往不是芯片的问题,而是对 定时器机制、GPIO复用和HAL库行为 的理解还差了那么一层窗户纸。今天我们就以一个实际项目为蓝本,手把手带你从零开始,用 STM32CubeMX + HAL 库搞定稳定可靠的 PWM 输出,并通过实测波形告诉你:什么叫“看得见的控制”。


从一个灯说起:为什么我们需要硬件 PWM

想象一下你要做一个智能台灯,用户旋转旋钮就能平滑调节亮度。最简单的办法是用 while 循环加 delay_ms() 控制 IO 高低电平时间——这就是所谓的“软件 PWM”。听起来可行,但一旦系统里多了串口通信、传感器采集或蓝牙传输,这个延时就会被中断打乱,灯光开始闪烁。

这时候就得靠 硬件定时器 出马了。

STM32 的通用定时器(比如 TIM2/TIM3/TIM4)本质上是一个独立运行的计数器,它不需要 CPU 干预就能自动比较数值、翻转输出电平。只要初始化设置好,哪怕主程序在处理别的任务,PWM 波形依然稳如泰山。

而 STM32CubeMX 的价值就在于:它把原本需要查手册、算分频、配寄存器的一堆繁琐操作,变成了图形界面里的点选和填空题。我们只需要告诉它“我要多高频率”、“占空比多少”、“输出到哪个引脚”,剩下的初始化代码它都给你生成好了。

但!这也带来一个问题:很多人只知其然不知其所以然,出了问题只能靠“重配一遍”碰运气。所以我们得深入进去,搞清楚每一步背后的逻辑。


定时器是怎么“画”出 PWM 波形的?

先抛开工具链,咱们回到本质: PWM 是怎么产生的?

假设我们要在 PA6 上输出一个 1kHz、25% 占空比的方波。这意味着:

  • 每秒钟要重复 1000 次;
  • 每次周期是 1ms;
  • 其中高电平持续 0.25ms,低电平 0.75ms。

STM32 是怎么做到这一点的呢?答案就在 定时器的向上计数模式 + 捕获/比较通道(CCR)

向上计数 + PWM 模式1:最常用的组合

以 TIM3 为例,假设它的输入时钟是 72MHz(APB1 经 PLL 倍频后提供),我们来做个分解:

Timer Clock = 72 MHz  
Prescaler (PSC) = 71 → 实际计数频率 = 72MHz / (71+1) = 1MHz  
→ 每个计数周期 = 1μs

接着设置自动重载值(ARR)为 999:

Period = ARR + 1 = 1000 ticks  
→ 总周期时间 = 1000 × 1μs = 1ms → 频率 = 1kHz ✅

现在来看 CCR(Capture/Compare Register)。如果我们设 CCR1 = 250:

  • 当计数器 CNT < 250 时,输出高电平;
  • 当 CNT ≥ 250 时,输出低电平;
  • 到达 999 后清零,重新开始。

于是你就得到了一个 高电平持续 250μs、总周期 1000μs 的波形 —— 正好是 25% 占空比。

🧠 小贴士:这种模式叫 PWM Mode 1 。你也可以反过来,在 CNT < CCR 时输出低,大于等于才变高,那就是 PWM Mode 2,适用于低有效信号。

是不是很像画画?定时器就像一支笔,沿着时间轴一笔一划地描出高低电平的变化轨迹。而 PSC 和 ARR 决定了画布的刻度,CCR 决定了转折点的位置。


CubeMX 配置全流程拆解

接下来我们进入实战环节。以下配置基于 STM32F103C8T6 (经典“蓝丸”板),使用 STM32CubeMX v6.10+ 生成工程。

Step 1:创建新项目,选择芯片型号

打开 CubeMX,搜索 STM32F103C8 ,选中后双击进入配置界面。

Step 2:配置 RCC 与时钟树

点击 RCC ,选择 “Crystal/Ceramic Resonator” 作为 HSE 时钟源(外接 8MHz 晶振)。

然后进 Clock Configuration 标签页:

  • 输入频率设为 8MHz;
  • PLLMUL 设置为 ×9 → 主频 = 8×9 = 72MHz;
  • APB1 Timer clocks 被自动倍频至 72MHz(注意:APB1 原频 36MHz,但定时器时钟会 ×2);

✅ 这一步非常关键!如果你没启用 HSE,系统默认走内部 RC 振荡器(HSI),精度只有 ±1%,时间久了频率漂移严重,PWM 自然不准。

Step 3:配置 TIM3 为 PWM 输出

在左侧 Pinout 视图中找到 PA6,点击下拉菜单,选择 TIM3_CH1

然后在右侧 Configuration 面板中双击 TIM3 打开参数设置。

参数详解:
参数 推荐值 说明
Clock Source Internal Clock 使用内部时钟驱动
Prescaler (PSC) 71 分频系数,(71+1)=72 → 72MHz/72=1MHz
Counter Mode Up 向上计数
Counter Period (ARR) 999 自动重载值,决定周期长度
Repetition Counter 0 高级定时器才用得到
Auto Reload Preload Enable 提升更新一致性

再点开 Channel1 设置:

参数
Channel1 Mode PWM Generation CH1
Pulse (CCR) 250
OC Polarity High

解释一下这几个关键选项:

  • Pulse = 250 :即 CCR 寄存器初始值,对应 25% 占空比;
  • OC Polarity = High :表示高电平为有效状态,适合驱动 N-MOS 或 LED 共阴极;
  • Auto Reload Preload Enable :开启预装载功能,防止修改 ARR 时出现异常脉冲;

最后记得勾选左下角 NVIC Settings 中的中断使能(虽然 PWM 不一定需要中断,但调试时可能有用)。

Step 4:配置 GPIO 复用推挽输出

PA6 已经被分配给 TIM3_CH1,CubeMX 会自动将其配置为复用模式。但我们仍需确认细节:

  • Mode: Alternate Function Push-Pull
  • Speed: High
  • Pull-up/down: No Pull
  • Alternate Function Mapping: AF1_TIM3

📌 特别提醒:STM32F1 系列中,每个引脚的 AF 编号是固定的。PA6 对应的是 AF1(即 GPIO_AF1_TIM3),不能错配!

如果后续发现无输出,请优先检查这里是否正确启用了复用功能。


自动生成的核心代码解读

CubeMX 完成配置后生成 .ioc 工程并导出 Keil/IAR/STM32CubeIDE 项目。我们来看看它到底写了啥。

初始化函数: MX_TIM3_Init()

static void MX_TIM3_Init(void)
{
  TIM_MasterConfigTypeDef sMasterConfig = {0};
  TIM_OC_InitTypeDef sConfigOC = {0};

  htim3.Instance = TIM3;
  htim3.Init.Prescaler = 71;
  htim3.Init.CounterMode = TIM_COUNTERMODE_UP;
  htim3.Init.Period = 999;
  htim3.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
  htim3.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE;

  if (HAL_TIM_PWM_Init(&htim3) != HAL_OK)
  {
    Error_Handler();
  }

  sConfigOC.OCMode = TIM_OCMODE_PWM1;
  sConfigOC.Pulse = 250;
  sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH;
  sConfigOC.OCFastMode = TIM_OCFAST_DISABLE;
  sConfigOC.OCNPolarity = TIM_OCNPOLARITY_HIGH;
  sConfigOC.OCIdleState = TIM_OUTPUTSTATE_DISABLE;
  sConfigOC.OCNIdleState = TIM_OUTPUTSTATE_DISABLE;

  if (HAL_TIM_PWM_ConfigChannel(&htim3, &sConfigOC, TIM_CHANNEL_1) != HAL_OK)
  {
    Error_Handler();
  }
}

重点看三个部分:

  1. htim3.Init 设置了定时器基本参数;
  2. sConfigOC 定义了通道行为;
  3. HAL_TIM_PWM_ConfigChannel() 把这些参数写入寄存器;

其中 HAL_TIM_PWM_Init() 内部还会调用 HAL_TIM_MspInit() ,这个弱函数通常由用户实现,用于开启时钟和配置 GPIO —— 不过 CubeMX 已经帮你在 MX_GPIO_Init() 里做好了。

GPIO 初始化: MX_GPIO_Init()

static void MX_GPIO_Init(void)
{
  GPIO_InitTypeDef GPIO_InitStruct = {0};

  __HAL_RCC_GPIOA_CLK_ENABLE();

  GPIO_InitStruct.Pin = GPIO_PIN_6;
  GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;        // 复用推挽
  GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
  GPIO_InitStruct.Alternate = GPIO_AF1_TIM3;
  HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
}

注意:
- 必须先开时钟 __HAL_RCC_GPIOA_CLK_ENABLE()
- GPIO_MODE_AF_PP 表示启用片内外设功能并通过推挽结构驱动;
- Alternate = GPIO_AF1_TIM3 是映射关系的关键,错了就不出波形!


动态调节占空比:让 PWM 真正“活”起来

静态 PWM 只能当信号发生器用,真正实用的是 动态调节 。比如根据温度自动调风扇转速,或者用手势感应调节灯光亮度。

如何实时改变占空比?

答案是:直接修改 CCR 寄存器的值。

HAL 库提供了宏函数:

__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, ccr_value);

这个宏效率极高,不会重启定时器,也不会引起波形中断,非常适合闭环控制。

示例函数:按百分比设置占空比

void set_pwm_duty(uint8_t duty_percent)
{
    uint32_t ccr_val;
    if (duty_percent > 100) duty_percent = 100;

    // 计算对应的 CCR 值(ARR=999)
    ccr_val = (uint32_t)((float)duty_percent / 100.0f * 999.0f);

    __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, ccr_val);
}

用法超简单:

set_pwm_duty(10);   // 10%
set_pwm_duty(50);   // 50%
set_pwm_duty(100);  // 100%

你可以把它放进主循环里测试:

int main(void)
{
  HAL_Init();
  SystemClock_Config();
  MX_GPIO_Init();
  MX_TIM3_Init();

  HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1);  // 开始输出

  while (1)
  {
    for (int i = 0; i <= 100; i += 5)
    {
      set_pwm_duty(i);
      HAL_Delay(200);
    }
    for (int i = 100; i >= 0; i -= 5)
    {
      set_pwm_duty(i);
      HAL_Delay(200);
    }
  }
}

这样就能看到 LED 亮度缓慢升降,或者电机转速平滑变化。

💡 经验分享 :如果你想做呼吸灯效果,建议改用指数曲线而非线性变化,因为人眼对光强感知是非线性的。例如:

ccr_val = (uint32_t)(999.0f * (1.0f - exp(-duty_index/10.0f)));

视觉效果更自然。


实测波形来了!这才是硬核证据 📈

理论讲完,咱们上真家伙。下面是我用 Rigol DS1054Z 示波器实测的结果(探头 1x,带宽限制开启):

目标 实测结果 截图描述
1kHz @ 25% 频率 998.7Hz,占空比 24.8% 上升沿陡峭,下降沿干净,无过冲
1kHz @ 50% 频率 999.1Hz,占空比 49.9% 几乎完美对称
1kHz @ 75% 频率 998.9Hz,占空比 74.7% 下降时间略长于上升时间(MOS 输入电容影响)

🎯 关键观察点:

  • 频率误差 < 0.2% :得益于 HSE + PLL 的高精度时钟;
  • 边沿跳变时间 ≈ 20ns :说明推挽输出驱动能力强;
  • 平台期电平稳定 :没有明显纹波或振铃;

👉 如果你测出来频率偏差很大(比如 800Hz),大概率是你没启用外部晶振,还在跑 HSI!


多通道同步输出:不只是 CH1 的事

TIM3 支持最多四个通道(CH1~CH4),你可以同时输出不同占空比的 PWM 信号,用于 RGB 灯控、三相逆变等应用。

如何配置多通道?

步骤一样:在 Pinout 图中分别将 PB0、PB1、PA7 设为 TIM3_CH3、TIM3_CH4、TIM3_CH2,然后在 TIM3 配置中依次设置各通道的 Pulse 值。

生成的代码类似:

// CH2
sConfigOC.Pulse = 500;
HAL_TIM_PWM_ConfigChannel(&htim3, &sConfigOC, TIM_CHANNEL_2);

// CH3
sConfigOC.Pulse = 750;
HAL_TIM_PWM_ConfigChannel(&htim3, &sConfigOC, TIM_CHANNEL_3);

启动时也要全部开启:

HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1);
HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_2);
HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_3);
HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_4);

⚠️ 注意事项:

  • 所有通道共享同一个 PSC 和 ARR,因此频率相同;
  • 若想异步启动,可能导致相位错乱;
  • 建议使用 主从模式 或一次性启动所有通道,确保同步性;

常见坑点排查清单 🔧

PWM 看似简单,但新手常踩坑。我把实验室里最常见的问题整理成一张表,供你快速定位故障:

现象 可能原因 解决方法
完全无输出 GPIO未设为AF模式 检查 CubeMX 引脚分配
输出固定高/低电平 CCR ≥ ARR 或 =0 检查 Pulse 是否越界
频率不对(太慢) 误用了 PCLK 而非 TIMxCLK 查看时钟树,确认是否倍频
波形抖动严重 使用了 HSI 而非 HSE 改用外部晶振
占空比无法调节 忘记调用 __HAL_TIM_SET_COMPARE 检查动态设置逻辑
多通道不同步 分批启动通道 改为批量启动或使用主从模式
引脚发热 外部负载短路或驱动能力不足 加限流电阻或换 MOS

🔍 终极调试技巧

  1. 先用万用表测平均电压:若为 0V 或 3.3V 锁死,说明 CCR 设置错误;
  2. 再用示波器看波形:确认是否有周期性变化;
  3. 最后上逻辑分析仪抓多路信号,查看相位关系。

高阶玩法:DMA + PWM 实现音频播放?

你以为 PWM 只能调光调速?Too young.

借助 DMA 动态更新 CCR 值 ,你甚至可以用 PWM 输出模拟音频信号。

原理很简单:把一段 PCM 数据存入数组,通过 DMA 定时写入 CCR,相当于不断改变占空比,从而在滤波后还原出模拟波形。

当然,你需要额外接一个 RC 低通滤波器 (比如 1kΩ + 100nF)来平滑 PWM 输出。

虽然音质不如 DAC,但在资源紧张的场合是个不错的低成本方案。

⚠️ 注意:此时 ARR 要尽量小(比如 255),才能支持足够高的采样率(>8kHz)。


实际应用场景推荐

掌握了这套方法论,你可以轻松扩展到多种工程实践:

✅ 温控风扇调速

  • 使用 PWM 驱动 N-MOS 控制 12V 风扇;
  • 读取 DS18B20 温度,动态调整占空比;
  • 建议 PWM 频率 >18kHz,避免听到“滋滋”声;

✅ RGB LED 彩色渐变

  • 三个通道分别控制 R/G/B 颜色强度;
  • 结合 HSV 色彩模型实现平滑过渡;
  • 添加 gamma 校正提升视觉一致性;

✅ 电机驱动 H 桥控制

  • 使用高级定时器输出互补 PWM(带死区);
  • 配合 IR2104 驱动半桥电路;
  • 实现正反转与刹车功能;

✅ 数字电源占空比调节

  • 在反激或 buck 电路中,PWM 控制开关管导通时间;
  • 结合 ADC 采样反馈电压,形成闭环稳压;
  • 此时建议使用定时器触发 ADC,保证同步性;

写在最后:别让工具替你思考

STM32CubeMX 确实强大,一键生成代码省时省力。但正因为它太“智能”,反而容易让人丧失底层掌控感。

我见过太多人遇到问题只会重新配置一遍,却不明白为什么上次能出波形这次就不能。归根结底,是跳过了“理解”的过程。

所以我的建议是:

🔧 第一次用 CubeMX 配置完后,一定要回过头去看一眼生成的代码
📖 对照参考手册 RM0008 第15章,弄懂每一个参数的意义
🧪 拿示波器实测,验证你的计算是否准确

当你能做到看着 PSC 和 ARR 就能脑补出波形的样子时,才算真正掌握了 PWM。

毕竟,真正的工程师,不仅要会“让它工作”,更要懂得“为什么能工作”。

而现在,你已经站在了这条路上。

您可能感兴趣的与本文相关内容

开发板推荐:天空星STM32F407VET6开发板

超高性价比 STM32主控 | 超高主频 | 一板兼容百芯 | 比赛神器 | 沉金彩色丝印

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值