最近被问得最多的一个问题STM32的TIM1明明照着别的定时器例程抄了一遍引脚上就是测不到PWM波形是不是芯片坏了每次遇到这种问题我基本都能猜到原因——不是芯片坏了是把TIM1当成普通定时器用了。TIM1和TIM8是高级控制定时器多出来一堆普通定时器没有的控制逻辑少配置一步输出就是静悄悄的。这篇文章我会用标准库把TIM1输出PWM的完整流程拆开讲从时钟树、GPIO复用模式到时基配置再到那个最容易漏掉的主输出使能TIM_CtrlPWMOutputs每一步都解释清楚为什么要这么写最后把我在项目里踩过、以及帮别人排查过的各种坑整理成一篇避坑清单。代码是完整的直接复制就能跑适合刚入门的小白也适合从HAL库转回标准库做电机控制的工程师。1. 为什么要认真对待TIM1和标准库1.1 TIM1不是普通的定时器很多教程讲STM32的PWM输出会拿TIM2、TIM3举例。这些通用定时器也能输出PWM配置起来也确实简单。但如果你只会在通用定时器上输出PWM碰到电机驱动、数字电源这类项目时会很吃力因为这些应用真正需要的不是“一个方波”而是一整套波形安全控制机制那正是TIM1这种高级控制定时器的看家本领。TIM1和TIM8在STM32里单独归为一类叫高级控制定时器Advanced-control timer。它们比普通定时器多出三样核心硬件能力互补输出同一个PWM信号可以同时输出到两个引脚一路正相、一路反相专门用来驱动H桥或者三相桥的上下桥臂。死区插入正反两路信号切换的间隙里自动插入一段两路都为低电平的时间防止上下桥臂瞬间直通烧管子。刹车功能外部故障信号一来硬件立刻封锁PWM输出不需要软件干预响应速度是微秒级的。普通定时器根本没有这些机制。你想用TIM3模拟互补PWM和死区靠GPIO翻转加延时是做不出来的中断延时的抖动足以让功率器件冒烟。所以只要涉及电机功率驱动基本绕不开TIM1/TIM8。1.2 这个场景为什么选标准库而不是HAL库这些年HAL库越来越流行CubeMX点点点就能生成工程确实方便。但在TIM1这种高级定时器的配置场景下我个人还是推荐标准库原因很直接标准库的API基本和寄存器一一对应每个函数配置哪个位、操作哪个寄存器看代码一眼就能明白。比如TIM_CtrlPWMOutputs这个函数操作的就是BDTR寄存器里的MOE主输出使能位。而HAL库里这层逻辑被封装得更靠上很多人出了问题根本不知道往哪查。尤其当你需要精确操作死区时间、刹车极性、锁定级别这些寄存器细节时标准库写起来更痛快——数据手册里说设置DTG位段标准库代码就对应TIM_BDTRInitStructure.TIM_DeadTime一行对一行不容易绕晕。另外一个现实原因是存量代码。手头大部分电机控制项目、竞赛代码、老工程师的参考例程都是标准库写的。现在项目维护、代码移植还是绕不开标准库。用HAL库重新写一遍费时费力不说还容易在移植过程中把时序细节改出错。1.3 TIM1最常见的三大实际应用场景TIM1的PWM输出在日常项目里主要分三类用法电机调速类直流电机用单路PWM配合H桥驱动舵机用50Hz周期PWM控制角度无刷电机用三组互补PWM做六步换相或者FOC。这类应用占TIM1使用场景的大多数。信号调制类驱动无源蜂鸣器发声、LED调光、加热器功率控制。这类场景对占空比精度有一定要求但对死区和刹车不敏感属于TIM1的基础用法。电源变换类同步Buck、Boost、推挽、半桥/全桥等拓扑需要两个互补PWM带死区配合刹车保护TIM1简直就是为这个设计的。把这三种场景归纳一下你会发现它们有一个共同点不只是要“有波形”还要波形可靠、可控制、可保护。这也正是TIM1比普通定时器值钱的地方。2. 标准库配置TIM1 PWM输出一步一步来2.1 时钟树TIM1的72MHz是怎么来的配置PWM频率之前先得搞清楚TIM1的输入时钟是多少。以最常见的STM32F103系列为例系统时钟一般由外部8MHz晶振经过PLL倍频到72MHz。STM32内部有两条外设总线APB1和APB2APB1最高36MHzAPB2最高72MHz。TIM1挂在APB2上。这里有个非常容易理解错的地方APB2预分频器对定时器时钟有自己的一套规则。当APB2分频系数为1即不分频时TIM1的时钟就是APB2的时钟也就是72MHz。当APB2分频系数大于1时TIM1内部会自动把APB2时钟乘以2所以即便APB2被分频到36MHzTIM1最终拿到的时钟依然是72MHz。简单记忆在常见的72MHz主频配置下不管APB2怎么分频TIM1的输入时钟基本就是72MHz。F407略有不同它的APB2最高84MHzTIM1时钟在分频系数大于1时取APB2的两倍也就是168MHz。写代码前先确认目标芯片的定时器时钟后面所有PWM频率计算都基于这个数字。2.2 GPIO复用推挽不是推挽定时器波形通过引脚输出时必须把引脚配置成复用功能模式。对于F103来说TIM1_CH1默认复用引脚是PA8TIM1_CH1N是PB13CH2是PA9CH2N是PB14CH3是PA10CH3N是PB15CH4是PA11。这些默认复用映射在绝大多数F103封装上都是固定的不需要开启重映射。GPIO的模式要选GPIO_Mode_AF_PP也就是复用推挽输出不是普通的GPIO_Mode_Out_PP。这两个模式在标准库里是两个完全不同的配置路径。普通推挽输出模式下引脚状态由ODR寄存器控制定时器外设根本接管不了引脚而复用推挽模式下引脚控制权才会交给片内外设。很多新手把定时器配置得毫无问题但GPIO模式选错引脚始终是死的自然测不到波形。另外别忘了打开GPIO的时钟。代码里用到的引脚如果涉及重映射还要额外开启AFIO时钟。开时钟这件事放在所有配置的最前面和TIM1时钟一起用RCC_APB2PeriphClockCmd打开。典型GPIO配置代码段如下GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_8; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure);2.3 时基和输出通道频率、占空比的计算公式TIM1要输出PWM需要配置两个核心单元时基单元Time Base和输出比较通道OC Channel。时基单元决定了计数器的计数周期配置项包括自动重装载值ARR、预分频系数PSC、计数模式和时钟分割。PWM频率的计算公式是PWM频率 TIM时钟 / (PSC 1) / (ARR 1)占空比 通道比较值CCR / (ARR 1)这里的PSC和ARR都是16位寄存器取值范围0到65535。举个例子如果TIM时钟是72MHz想要输出1kHz的PWM可以设置PSC71、ARR999。计算过程72MHz除以72再除以1000正好1000Hz。如果此时CCR500占空比就是500/1000也就是50%。这里有个细节值得留意ARR999时计数器计数值从0到999一共1000个计数周期。占空比的分子是CCR的值不是CCR加一。用公式时注意别把ARR1和CCR1混在一起不然算出来的占空比会差一个计数脉冲频率高的场景下误差被放大波形看着总是不对劲。时基单元的初始化代码如下TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_TimeBaseStructure.TIM_Period arr; // ARR, 自动重装载值 TIM_TimeBaseStructure.TIM_Prescaler psc; // PSC, 预分频系数 TIM_TimeBaseStructure.TIM_ClockDivision TIM_CKD_DIV1; // 时钟分割, 一般填0 TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; // 向上计数 TIM_TimeBaseInit(TIM1, TIM_TimeBaseStructure);输出比较通道负责把计数器和CCR比较的结果映射成引脚电平。PWM模式1的定义是计数器计数值小于CCR时输出有效电平大于等于CCR时输出无效电平。如果选择PWM模式2逻辑正好反过来。电机控制里绝大多数场景用PWM模式1配合TIM_OCPolarity_High的极性设置直观理解就是“CCR越大高电平时间越长占空比越大”。2.4 主输出使能高级定时器和普通定时器的分水岭TIM1和普通定时器在配置流程上最大的区别就在这里。普通定时器初始化完时基和输出通道调用TIM_Cmd使能定时器引脚上就有PWM了。TIM1不行它还有个叫主输出使能MOE的门控位在BDTR寄存器里。MOE不置1即使计数器在跑、CCR在变四个通道的PWM也全都不会送到引脚上。标准库把这一位封装成了TIM_CtrlPWMOutputs函数TIM_CtrlPWMOutputs(TIM1, ENABLE);这一行是TIM1配置里绝对不能漏的。我排查过的那么多“TIM1没输出”问题里大概有一半就是漏了这一句。剩下的才是GPIO配置、时钟分频等问题。和主输出使能一起提的还有预装载功能。TIM_OC1PreloadConfig和TIM_ARRPreloadConfig分别控制CCR1寄存器和ARR寄存器有没有预装载。使能预装载后修改CCR或ARR不会立刻生效而是等下一个更新事件计数器溢出统一加载。如果不使能预装载运行中修改CCR可能造成PWM波形中出现一个异常宽或异常窄的脉冲在电机控制这种场景里是致命的。2.5 完整可复制代码初始化加动态调占空比把上面这些串起来写一个完整的初始化函数。这个函数传两个参数arr和psc分别对应自动重装载值和预分频系数方便在main里灵活设置频率。void TIM1_PWM_Init(u16 arr, u16 psc) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; // 1. 开启TIM1和GPIOA的时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1 | RCC_APB2Periph_GPIOA, ENABLE); // 2. 配置PA8为复用推挽输出, 用于TIM1_CH1 GPIO_InitStructure.GPIO_Pin GPIO_Pin_8; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); // 3. 配置时基单元 TIM_TimeBaseStructure.TIM_Period arr; TIM_TimeBaseStructure.TIM_Prescaler psc; TIM_TimeBaseStructure.TIM_ClockDivision TIM_CKD_DIV1; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM1, TIM_TimeBaseStructure); // 4. 配置输出比较通道为PWM模式1 TIM_OCInitStructure.TIM_OCMode TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState TIM_OutputState_Enable; TIM_OCInitStructure.TIM_OutputNState TIM_OutputNState_Disable; TIM_OCInitStructure.TIM_Pulse 0; // 初始占空比为0% TIM_OCInitStructure.TIM_OCPolarity TIM_OCPolarity_High; TIM_OCInitStructure.TIM_OCNPolarity TIM_OCNPolarity_High; TIM_OC1Init(TIM1, TIM_OCInitStructure); // 5. 使能预装载, 避免运行中改参数产生毛刺 TIM_OC1PreloadConfig(TIM1, TIM_OCPreload_Enable); TIM_ARRPreloadConfig(TIM1, ENABLE); // 6. 高级定时器特有: 使能主输出 TIM_CtrlPWMOutputs(TIM1, ENABLE); // 7. 启动定时器 TIM_Cmd(TIM1, ENABLE); }main函数里可以这样动态调整占空比。下面这版是个呼吸灯的效果占空比从0逐渐加到最大再降回来实际跑的时候LED亮度会有明显的渐变过程非常适合第一次验证PWM是否正常工作int main(void) { u16 duty 0; s8 dir 1; TIM1_PWM_Init(999, 71); // 初始化TIM1, PWM频率为1kHz while (1) { TIM_SetCompare1(TIM1, duty); duty dir; if (duty 999) dir -1; if (duty 0) dir 1; Delay_ms(3); } }这里有几个点要提醒一下。TIM_SetCompare1函数实际修改的是TIM1-CCR1寄存器参数类型是u16最大值不要超过ARR的值。如果设置的duty大于ARR输出就会变成常高电平波形直接失真别问我怎么知道的。另外占空比到达0时我把方向标志切回来但没让PWM彻底关闭这是因为占空比0和PWM关闭在行为上有一点区别——前者引脚固定低电平后者引脚可能处于高阻态。在驱动电机时这两种状态对电路的影响完全不同实际项目里要根据场景明确区分。3. 避坑实录TIM1 PWM高频翻车现场3.1 没输出先查这四处TIM1配置完成后引脚没波形这是最常见的故障现象。我按排查优先级列一下原因MOE位没置1。这个前面强调过了TIM_CtrlPWMOutputs(TIM1, ENABLE)漏掉的话其他全对也没用。GPIO模式选错。复用推挽AF_PP写成了普通推挽Out_PP定时器信号没法输出到引脚。定时器没启动。TIM_Cmd(TIM1, ENABLE)漏掉计数器根本不跑。输出通道没使能。TIM_OCInitStructure里的TIM_OutputState没置ENABLE通道被关着。这四个问题分别对应BDTR寄存器、GPIO配置、CR1寄存器、CCER寄存器。排查的时候不用凭感觉猜打开调试器直接看这几个寄存器的值一目了然。3.2 频率差一倍或者差很多时钟倍频规则现象明明按公式算好了PSC和ARR示波器测出来的频率却是理论值的两倍或者完全对不上。频率是理论值两倍大概率是APB2分频器规则没吃透。有的工程里SystemInit被改动过或者移植代码时把系统时钟配置换了一套APB2分频系数不是1定时器时钟自动乘2频率自然翻倍。这种问题隐蔽性很强因为从RCC配置代码上看系统时钟依然是72MHz但APB2分频路径已经不同。频率差得离谱还要检查外部晶振频率。很多开发板用的12MHz晶振如果SystemInit里的PLL配置是按8MHz写的实际主频会变成108MHz而不是72MHz全部外设的时钟基准都会偏。遇到这类问题先停一下用示波器测一下MCO引脚输出的系统时钟确认基准频率没问题再往下查定时器。3.3 占空比调了没反应或者波形毛刺多原因一般出在预装载上。没有使能TIM_OC1PreloadConfig时修改CCR1会立即生效。如果修改的时机不凑巧正好赶上计数器跳到CCR附近PWM脉冲宽度就会出现一个突变的毛刺。在示波器上看波形整体占空比好像是变了但某些周期明显异常。还有一种情况是CCR写错通道了。比如初始化用TIM_OC1Init配的CH1main里却用了TIM_SetCompare2去改CH2的CCR。这时CH1的占空比当然不会有任何变化。检查这种问题最快的办法就是在调试器里查看TIM1-CCR1的实际值确认它跟着你的设置走。3.4 CH1N互补通道没有波形TIM1_CN通道表示互补输出通道。很多人想用CH1N输出反向PWM但只配置了CH1相关的参数结果PB13引脚上静悄悄。要输出互补波形必须明确配置三个点TIM_OCInitStructure里把TIM_OutputNState置为EnablePB13这个引脚要初始化成复用推挽输出MOE位同样要置1。缺任何一个CH1N都没波形。另外注意即使只用CH1N不用CH1CH1对应的输出状态也不一定要关掉实际项目中一路正相一路反相的互补输出才是CH1N存在的意义。初始化代码里需要额外加上GPIOB的时钟和PB13的配置再把TIM_OutputNState打开。这样CH1N才有正常的互补PWM输出。3.5 上电瞬间波形乱跳在一些电机控制项目里MCU刚上电那几十毫秒引脚上偶尔会冒出不规则的脉冲直接把功率管打穿。这种现象多半是初始化顺序问题先启动了定时器才去配置输出通道或者CCR的初值是个垃圾值还没被重置就已经输出了一拍异常PWM。标准库推荐的做法是先配置好GPIO、时基、输出通道把输出状态设为Enable同时把TIM_Pulse初始化为0最后再统一使能主输出和定时器。这样从第一拍开始波形就是可控的。如果涉及功率电路更稳妥的办法是硬件上加RC延迟让MCU先稳定运行几百毫秒再让功率级上电。3.6 调试三板斧快速定位PWM问题排查PWM输出我有三个固定套路基本能覆盖90%的情况先用示波器看引脚有没有波形。如果没有把引脚临时配置成普通输入读一下引脚电平是不是悬空或固定电平能快速判断GPIO是否被正确复用。用调试器看寄存器。重点看TIM1-CR1的CEN位是不是1TIM1-BDTR的MOE位是不是1TIM1-CCMR1的OC1M字段是不是110PWM模式1。这三个位置对了配置基本没问题。把参数调到最小。PSC设0ARR设9定时器时钟按72MHz算输出就是7.2MHz的方波。这种频率示波器一抓一个准能快速验证时基和通道链路通不通。等确认链路正常再把ARR和PSC调到目标值。4. 进阶玩法互补输出、死区与刹车4.1 死区时间是怎么算的死区时间通过BDTR寄存器Break and Dead-time register的DTG位段配置。标准库里面对应的是TIM_BDTRInitStructure里的TIM_DeadTime字段。DTG一共8位配置分成几种模式最常用的是DTG[7:5]000的情况此时死区时间等于DTG[6:0]乘以tDTS。tDTS等于定时器时钟周期的整数倍。以72MHz为例tDTS大约是13.89ns。如果TIM_DeadTime填72死区时间就是72乘以13.89ns约等于1微秒。如果DTG的高三位不是000那对应的是更长的死区范围公式要查参考手册的表格实际项目中基本用不到。死区时间怎么选太短会失效太长会增加功耗。驱动IGBT时死区一般取1微秒到3微秒驱动低压MOSFET的半桥驱动芯片像IR2104这类死区取0.5微秒到2微秒比较常见。具体数值还要看栅极电阻、开关速度、寄生电容等因素不是拍脑袋定的。第一次调试时可以把死区设置得稍微保守一点用示波器同时看上下桥的Vgs波形确认没有直通后再逐步减小死区。4.2 配置CH1和CH1N互补输出的完整步骤要配置互补输出需要在前面基础代码上扩展。GPIO方面除了PA8还需要初始化PB13为复用推挽输出。输出通道配置里把TIM_OutputNState置为Enable。补充的GPIO配置如下RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_13; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOB, GPIO_InitStructure);通道配置里增加互补输出使能TIM_OCInitStructure.TIM_OutputState TIM_OutputState_Enable; TIM_OCInitStructure.TIM_OutputNState TIM_OutputNState_Enable; // 使能互补输出然后在初始化过程中调用TIM_BDTRConfig配置死区参数TIM_BDTRInitTypeDef TIM_BDTRInitStructure; TIM_BDTRInitStructure.TIM_OSSIState TIM_OSSIState_Enable; TIM_BDTRInitStructure.TIM_OSSRState TIM_OSSRState_Enable; TIM_BDTRInitStructure.TIM_LOCKLevel TIM_LOCKLevel_OFF; TIM_BDTRInitStructure.TIM_DeadTime 72; // 约1us死区(72MHz时钟) TIM_BDTRInitStructure.TIM_Break TIM_Break_Disable; TIM_BDTRInitStructure.TIM_BreakPolarity TIM_BreakPolarity_High; TIM_BDTRInitStructure.TIM_AutomaticOutput TIM_AutomaticOutput_Enable; TIM_BDTRConfig(TIM1, TIM_BDTRInitStructure);配置顺序上有个讲究BDTR相关的配置应该在主输出使能之前完成。如果把TIM_CtrlPWMOutputs放在TIM_BDTRConfig前面某些寄存器位可能被后续配置覆盖导致保护逻辑没有完全生效。4.3 刹车保护异常时自动封锁波形电机控制里最担心的就是过流。软件上可以通过ADC采样电流超过阈值就让PWM占空比归零但软件路径有延迟中断响应再快也得几十个微秒。TIM1的刹车功能是硬件级的外部故障信号一到BKIN引脚输出立刻被封锁响应速度是纳秒级的不需要软件参与。标准库里配置刹车功能同样在TIM_BDTRInitStructure里。把TIM_Break置为Enable设置刹车信号的极性再配置自动输出功能。刹车触发后输出通道会进入一个由OSSI/OSSR位决定的安全状态通常配置成输出低电平让电机停转。实际项目里常用一个比较器电路检测母线电流过流时比较器输出跳变信号接到TIM1_BKIN引脚。硬件上再配合DMA或中断记录故障状态形成一个完整的保护链路。这就是TIM1区别于普通定时器最有价值的地方没有刹车功能的话功率电路的保护只能靠软件硬扛风险大得多。4.4 实际项目中的参数选择经验带互补输出的TIM1 PWM配置参数选择有一些通用经验可以参考。频率选择上直流有刷电机和BLDC的PWM频率一般取10kHz到20kHz高于人耳可听范围又低于开关损耗爆炸区间。20kHz正好是人耳听觉阈值临界点很多产品为了静音都选这个值但这时开关损耗也会相应增加需要平衡。舵机控制则固定在50Hz周期20毫秒脉宽0.5毫秒到2.5毫秒对应不同角度这是舵机协议本身定义的。占空比范围方面驱动半桥芯片时一般不建议让占空比达到100%。很多自举式驱动芯片的自举电容需要靠下桥开通来充电占空比长时间接近100%下桥一直关断自举电容电压会掉下去上桥驱动就会失效。实际使用中会限制占空比上限在95%左右。这个是典型的实战经验光看芯片手册不容易想到。还有一个容易被忽略的参数是GPIO输出速度。PWM频率越高、边沿越陡对GPIO速度的要求就越高。20kHz以下的PWM用GPIO_Speed_50MHz完全够用但如果你在搞几百kHz的高频电源GPIO速度等级设置不当会影响上升沿质量甚至造成额外的开关损耗。5. 写在最后的一点体会用TIM1输出PWM这件事原理本身并不复杂难的是把每一个配置环节的因果关系理清楚。我刚开始接触高级定时器时也走了不少弯路最头疼的就是“看起来都对了但就是没波形”。后来养成了一个习惯每次配置完新外设先用最简单的参数验证链路再逐步叠加功能。比如先让PA8输出7.2MHz的裸PWM确认没问题后再加死区、加刹车、加互补输出每一步都验证过再往前走。最后分享一个实用小技巧调试PWM和电机控制时可以把占空比、当前PWM频率、故障标志这些变量通过串口打印出来配合上位机做实时曲线。尤其是做PID调参那会儿光靠示波器看波形效率特别低串口数据配合波形图才能快速看到控制环路的动态响应。这也是我后来在所有电机项目里都保留的一个调试接口。希望这篇东西能帮你少踩几个坑早日跑出自己满意的PWM波形。
阅读完成 · 觉得有帮助?