FEATURED · 精选文章

STM32 HAL库定时器PWM配置详解:从原理到实战应用

发布时间 / 2026/8/5 5:28:19
来源 / 创域科博编辑部
栏目 / 资讯中心
STM32 HAL库定时器PWM配置详解:从原理到实战应用 1. 项目概述为什么是HAL库与定时器PWM如果你正在用STM32做项目无论是驱动一个舵机、控制LED亮度还是调节电机转速PWM脉冲宽度调制输出几乎是一个绕不开的功能。而STM32的定时器正是产生PWM的“心脏”。过去很多工程师习惯于标准库Standard Peripheral Library但如今ST官方主推且在新系列芯片上唯一全面支持的就是HAL库Hardware Abstraction Layer。这个转变让不少朋友感到头疼HAL库代码看起来更“臃肿”初始化流程更长文档似乎也不如标准库直观。但当你真正上手后会发现HAL库在跨芯片移植、代码结构清晰度和CubeMX工具链支持上有着不可替代的优势。这个内容就是为你拆解如何用STM32的HAL库配置定时器输出PWM。我不会只给你一堆代码让你复制而是会带你走一遍从原理到配置、从代码到调试的完整流程。你会明白每一个配置选项背后的意义知道为什么这里要填这个数那里要选那个模式。最终你能独立地让任何一个STM32定时器的通道输出你想要的频率和占空比的PWM波。无论你是刚接触HAL库的新手还是从标准库转过来想系统梳理一遍的老手这篇内容都能给你提供可直接“抄作业”的步骤和避坑指南。2. 核心原理与硬件基础扫盲在动手写代码之前我们必须先统一“语言”。理解下面几个核心概念能让你在配置时心里有底出错时也知道该往哪里排查。2.1 PWM到底是什么一个生活化的比喻你可以把PWM想象成一个高速开关的水龙头。我们的目标不是调节水流的压力那是模拟电压而是控制这个水龙头在固定周期内打开的时间和关闭的时间比例。周期Period水龙头“开-关-开”这样一个完整循环的时间。对应PWM波的频率Frequency频率 1 / 周期。脉冲宽度Pulse在一个周期内水龙头打开的时间。占空比Duty Cycle脉冲宽度与周期的比值通常用百分比表示。占空比50%就意味着一个周期内一半时间打开一半时间关闭。如果这个水龙头连接的是一个LED灯高速的“开-关”切换由于视觉暂留我们看到的不是闪烁而是亮度的变化。占空比越大灯就越亮。这就是PWM调光的基本原理。对于电机占空比控制平均电压从而控制转速。2.2 STM32定时器不止是“计时器”STM32的定时器功能异常强大远不止简单的延时。对于PWM生成我们主要用到它的“比较输出”功能。以最常见的通用定时器如TIM2, TIM3, TIM4为例其核心是一个计数器CNT它可以从0开始向上计数也可以向上向下计数。同时我们会配置一个自动重装载寄存器ARR和一个捕获/比较寄存器CCRxx为通道号1-4。PWM生成的工作原理以向上计数为例计数器CNT从0开始每个时钟周期加1。我们将目标周期值写入ARR寄存器。比如ARR999那么计数器会从0数到999然后溢出归零重新开始形成一个周期。我们将目标脉冲宽度值写入CCRx寄存器。比如CCR1300。在计数器计数过程中硬件会持续比较CNT和CCRx的值。当CNT CCRx时对应的输出通道如CH1被设置为有效电平可以是高也可以是低由配置决定。当CNT CCRx时输出通道被设置为无效电平。当CNT计数到ARR值999并溢出时输出电平再次翻转开始下一个周期。这样我们就得到了一个周期由ARR1决定高电平时间由CCRx决定的PWM波。占空比 CCRx / (ARR 1)。这里“1”是因为计数器从0开始计数。2.3 HAL库的角色从寄存器到函数接口如果没有库我们需要直接操作TIMx-ARR、TIMx-CCR1这类寄存器。HAL库把这些底层操作封装成了一个个函数比如HAL_TIM_PWM_Start()、__HAL_TIM_SET_COMPARE()。这样做的好处是可读性增强HAL_TIM_PWM_Init()比直接写一堆十六进制数到寄存器更容易理解。可移植性增强更换不同系列的STM32芯片如F1到F4初始化流程和函数名基本一致只需调整底层时钟配置。与CubeMX无缝对接CubeMX图形化工具生成的代码就是基于HAL库的这极大地简化了外设初始化。当然代价是代码效率稍低且有时为了通用性代码显得繁琐。但对于大多数应用这点开销完全可以接受。注意理解“ARR1”这个点非常重要。很多新手配置完发现频率不对问题往往出在这里。在CubeMX或代码中设置的“Period”值实际上就是ARR的值。因此实际PWM周期 (ARR 1) * 计数器时钟周期。3. 实战配置从CubeMX到代码生成理论说再多不如动手做一遍。我们以STM32F103C8T6蓝色药丸板的TIM3的通道1PA6引脚为例生成一个1kHz频率、50%占空比的PWM信号。3.1 CubeMX图形化配置详解引脚与定时器选择在CubeMX的引脚图界面找到PA6或你的目标引脚。它的复用功能Alternate Functions里找到TIM3_CH1点击选择。引脚颜色会变绿表示已配置。定时器基础参数配置左侧切换到Timers-TIM3。Clock Source时钟源选择Internal Clock内部时钟。这是最基本的定时器由内部APB总线时钟驱动。Parameter Settings参数设置Prescaler预分频器PSC这是定时器时钟的第一级分频。定时器实际计数时钟CK_CNT TIMx_CLK / (PSC 1)。我们的系统时钟SYSCLK是72MHzAPB1总线时钟TIM3挂载在此是72MHz。如果我们想让计数器每1us计数一次可以设置PSC71。因为72MHz / (711) 1MHz即每个计数周期1us。Counter Mode计数模式选择Up向上计数。最常用的模式。Counter Period计数周期ARR这是我们公式里的ARR。要生成1kHz的PWM周期是1ms 1000us。由于我们上一步让计数器1us计一次数所以ARR应该设置为999因为从0到999是1000个数。记住这里填的是999不是1000。Internal Clock Division内部时钟分频保持为No Division。auto-reload preload自动重装载预装载建议使能Enable。这会让对ARR的修改在下一个更新事件时才生效防止在计数中途修改ARR导致产生畸形的PWM周期。PWM生成配置下方切换到PWM Generation CH1。Mode模式选择PWM mode 1。在这个模式下当CNTCCR时通道输出有效电平由下面极性决定。Pulse脉冲宽度CCR1占空比 Pulse / (Period 1)。我们要50%占空比Period是999所以Pulse填499或500有细微差别后面解释。Fast Mode快速模式禁用。通常用于需要极快电平翻转的场景一般用不上。CH Polarity通道极性选择High。这意味着“有效电平”是高电平。即CNTCCR时输出高。如果你希望默认空闲状态为高电平PWM脉冲是低电平脉冲则选择Low。生成代码在Project Manager标签页设置好项目名称、路径、IDE如MDK-ARM V5。在Code Generator里选择“为每个外设生成独立的.c/.h文件”这样代码结构更清晰。点击GENERATE CODE。3.2 生成代码结构解析与用户代码填充CubeMX会生成完整的HAL库初始化代码。我们需要关注以下几个文件中的函数tim.c包含了MX_TIM3_Init()函数。这个函数完成了我们刚才在CubeMX中的所有配置包括时基结构体TIM_HandleTypeDef htim3的初始化以及PWM通道的配置通过HAL_TIM_PWM_ConfigChannel()。main.cmain()函数里会调用MX_TIM3_Init()。我们需要在定时器初始化后启动PWM输出。在/* USER CODE BEGIN 2 */和/* USER CODE END 2 */之间添加HAL_TIM_PWM_Start(htim3, TIM_CHANNEL_1);这个函数启动了TIM3通道1的PWM输出。此时PA6引脚上应该就能用示波器看到PWM波了。动态调节占空比 初始化时的占空比是固定的。我们通常需要在程序运行中动态改变它。HAL库提供了宏和函数// 方法一使用宏直接操作寄存器效率高 __HAL_TIM_SET_COMPARE(htim3, TIM_CHANNEL_1, 新的CCR值); // 例如设置占空比为75%__HAL_TIM_SET_COMPARE(htim3, TIM_CHANNEL_1, 749); // (9991)*0.75 750, CCR749 // 方法二使用函数可包含一些状态检查 TIM_OC_InitTypeDef sConfigOC {0}; sConfigOC.OCMode TIM_OCMODE_PWM1; sConfigOC.Pulse 新的CCR值; // 设置新的脉冲值 sConfigOC.OCPolarity TIM_OCPOLARITY_HIGH; sConfigOC.OCFastMode TIM_OCFAST_DISABLE; HAL_TIM_PWM_ConfigChannel(htim3, sConfigOC, TIM_CHANNEL_1); HAL_TIM_PWM_Start(htim3, TIM_CHANNEL_1); // 如果需要重新启动通常在运行中频繁修改占空比推荐使用__HAL_TIM_SET_COMPARE宏因为它几乎不消耗时间。实操心得CubeMX中Pulse的值和代码中__HAL_TIM_SET_COMPARE的值指的都是CCRx寄存器值。这个值必须小于等于ARR值。如果你设置了一个大于ARR的值实际效果会和等于ARR一样即常有效电平HAL库或硬件并不会报错但PWM波形会异常。4. 频率与占空比计算搞懂每一个数字这是最容易出错的部分。我们建立一个清晰的公式链。已知条件SYSCLK系统主频例如72MHz。APBx_PrescalerAPB总线分频系数。在时钟树配置中确定。例如APB1不分频则TIMx_CLK SYSCLK 72MHz。注意如果APB分频系数不为1STM32的定时器时钟会有一个x2的补偿机制这里不展开CubeMX会帮你算好最终的TIMx_CLK。PSC定时器预分频寄存器值0-65535。ARR定时器自动重装载寄存器值0-65535。计算公式定时器计数时钟频率F_cnt TIMx_CLK / (PSC 1)定时器计数周期T_cnt 1 / F_cntPWM波形周期T_pwm (ARR 1) * T_cnt (ARR 1) / F_cntPWM波形频率F_pwm 1 / T_pwm F_cnt / (ARR 1)占空比Duty (CCRx 1) / (ARR 1)注意这里是1为什么占空比公式里是CCRx1这是因为在PWM模式1下输出在CNT小于CCRx时有效。当CCRx0时CNT0不小于0所以有效时间为0占空比为0。当CCRxARR时CNT从0到ARR-1都小于ARR有效时间为ARR个计数周期占空比为ARR/(ARR1)并非100%。要得到100%占空比通常需要设置CCRx大于ARR但硬件会将其限制为ARR。因此精确的占空比控制尤其是在ARR值较小时需要仔细校准。对于大多数应用当ARR足够大比如几百上千时Duty ≈ CCRx / ARR的误差可以忽略不计。我们的实例计算 目标F_pwm 1kHz Duty 50% TIMx_CLK 72MHz。步骤1选择ARR。为了方便计算先定ARR999则 (ARR1)1000。步骤2计算所需的F_cnt。F_cnt F_pwm * (ARR 1) 1kHz * 1000 1MHz。步骤3计算PSC。PSC TIMx_CLK / F_cnt - 1 72MHz / 1MHz - 1 71。步骤4计算CCR1。Duty (CCR1 1) / (ARR 1)CCR1 Duty * (ARR 1) - 1 0.5 * 1000 - 1 499。所以配置为PSC71 ARR999 CCR1499。与我们在CubeMX中的设置完全一致。5. 进阶话题与实用技巧掌握了基础配置后下面这些技巧能让你的PWM应用更上一层楼。5.1 多通道与互补输出一个定时器可以同时产生多个通道的PWM如TIM3有CH1, CH2, CH3, CH4。在CubeMX中使能多个通道即可代码中使用HAL_TIM_PWM_Start(htim3, TIM_CHANNEL_1 | TIM_CHANNEL_2)来启动多个通道。它们的频率由ARR决定一定相同但占空比由各自的CCRx决定可以独立设置。对于电机驱动等需要互补PWM一对反相的信号的场景需要使用定时器的互补输出功能这通常涉及高级控制定时器如TIM1, TIM8并可以配置死区时间Dead Time防止上下桥臂同时导通而短路。配置更为复杂需要在CubeMX中选择PWM Generation CHxN并配置Dead Time参数。5.2 使用DMA自动更新占空比如果需要产生非常复杂或高速变化的PWM序列如生成特定波形频繁调用__HAL_TIM_SET_COMPARE会占用大量CPU时间。此时可以结合DMA。将一组预计算好的CCR值存放在数组中然后配置DMA让定时器的CCRx寄存器与这个数组建立联系。每次定时器更新事件ARR溢出时DMA自动将数组中的下一个值搬运到CCRx寄存器无需CPU干预。这在LED点阵扫描、音频合成等场景非常有用。5.3 测量PWM频率与占空比除了输出定时器还可以用来输入捕获测量外部PWM信号的频率和占空比。这需要用到定时器的另一个强大功能。通常需要两个输入捕获通道或一个通道两次捕获分别捕获上升沿和下降沿的时间点通过计算差值得到高电平时间和周期。HAL库提供了HAL_TIM_IC_Start()和相关的回调函数配合中断可以方便实现。5.4 硬件问题排查清单当你用示波器查看引脚却没有波形时可以按以下清单排查问题现象可能原因排查方法完全无信号1. 定时器未启动。2. 引脚复用功能未正确映射。3. 引脚被其他外设占用。4. 时钟未使能。1. 检查是否调用了HAL_TIM_PWM_Start()。2. 核对CubeMX引脚配置图确认AFAlternate Function选择正确。3. 检查代码中是否有其他地方如初始化GPIO为输入覆盖了该引脚配置。4. 确认__HAL_RCC_TIMx_CLK_ENABLE()被调用CubeMX生成的代码已包含。有信号但频率不对1. PSC或ARR计算错误。2. 定时器时钟源频率不对。1. 根据第4章的公式重新计算PSC和ARR。2. 检查系统时钟树配置确认APB总线时钟和定时器时钟频率是否正确。占空比不对或不可调1. CCRx值计算错误或设置超出范围。2. PWM极性配置错误。3. 代码中修改CCRx的语句未生效。1. 用示波器测量反推实际占空比核对CCRx值。2. 检查CubeMX中CH Polarity设置尝试切换High/Low看现象是否反相。3. 确保修改CCRx的代码确实被执行如通过调试器查看变量值。PWM波形毛刺多1. 电源噪声。2. 负载电路如电机反电动势干扰。3. 探头接地不良。1. 为MCU和负载电路增加滤波电容。2. 在电机驱动线路上并联续流二极管、使用光耦隔离。3. 使用示波器探头接地弹簧而非长接地线。6. 从理论到产品一个完整的调光台灯案例让我们用一个简单的智能台灯项目来串联所有知识。功能通过一个旋钮电位器ADC读取来无级调节LED灯的亮度PWM控制。硬件连接STM32F103C8T6LED灯串联一个限流电阻连接到PA6TIM3_CH1。电位器中间抽头连接到PA0ADC1_IN0两端接3.3V和GND。软件设计CubeMX配置TIM3配置如前所述生成1kHz PWM。ADC1配置PA0为单端输入启用连续转换模式使用DMA搬运数据。生成代码。关键代码// main.c 用户代码区 /* USER CODE BEGIN PV */ volatile uint16_t adc_value 0; // DMA搬运的ADC值 /* USER CODE END PV */ int main(void) { // ... HAL初始化外设初始化由CubeMX生成 HAL_TIM_PWM_Start(htim3, TIM_CHANNEL_1); // 启动PWM HAL_ADC_Start_DMA(hadc1, (uint32_t*)adc_value, 1); // 启动ADC DMA while (1) { // ADC值范围是0-409512位分辨率 // 我们需要映射到CCR值范围0-999因为ARR999 // 注意占空比公式是 (CCR1)/(ARR1)为了线性控制我们使用CCR adc_value * (ARR1) / 4096 uint32_t new_ccr adc_value * 1000 / 4096; // 计算得到0-999之间的值 // 防止计算溢出并做边界保护 if(new_ccr 999) new_ccr 999; __HAL_TIM_SET_COMPARE(htim3, TIM_CHANNEL_1, new_ccr); HAL_Delay(10); // 简单的延时避免更新过于频繁 } }优化与思考线性化人眼对光强的感知是非线性的韦伯-费希纳定律。直接线性映射ADC值到PWM占空比可能会觉得低亮度区域变化太剧烈高亮度区域变化不明显。可以在程序中做一个伽马校正表将ADC值通过查表映射到一个非线性的CCR值上使亮度变化更符合人眼感知。消抖与滤波电位器读数可能会有抖动。可以在ADC采样后增加软件滤波如滑动平均滤波使亮度变化更平滑。效率在主循环中不断更新PWM是可行的。但对于更复杂的系统可以在ADC转换完成回调函数中更新PWM实现事件驱动。通过这个案例你将PWM输出、ADC采集、DMA传输这几个STM32的常用功能结合了起来完成了一个具备实用价值的小项目。这比单纯的点亮一个LED更能巩固你对HAL库定时器PWM的理解。最后关于HAL库“臃肿”的争论我的体会是在资源不是极端紧张的中低端项目上HAL库的开发效率和可维护性优势远远大于其带来的微小性能损失。尤其是配合CubeMX能快速搭建项目框架避免在底层寄存器配置上耗费过多时间。当你熟悉了HAL库的套路后完全可以针对关键的热点路径混合使用LL库Low-Layer甚至直接寄存器操作来优化性能这才是使用ST官方生态最有效率的方式。
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻