ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

STM32定时器冲突:编码器接口模式导致PWM输出失败的根因与解决

2026/8/30 5:09:58 拓冰建站 浏览量
STM32定时器冲突:编码器接口模式导致PWM输出失败的根因与解决 最近在调一个基于 STM32F103 的电机位置闭环项目时遇到一个非常典型、但又特别容易让人绕弯的定时器冲突问题我把 TIM2 配置成了编码器接口模式Encoder Interface Mode来读取增量编码器的 A/B 相脉冲同时又想在同一个 TIM2 的 CH3 通道上输出 PWM 信号控制电机驱动。结果编码器数据完全正常位置和方向都能读出来但 TIM2_CH3 上的 PWM 死活出不来——用示波器戳上去不是一条死线就是一些毫无规律的毛刺。这个坑我蹲了大半天来回改配置、换思路最终把根因、原理和几套可行方案都摸清楚了。如果你也遇到“PWM 输出失败”的问题而且这个定时器同时还被编码器计数功能占用那这篇文章应该能帮你少走很多弯路。我会从故障复现、根因分析、解决方案到调试技巧按我实际的排查顺序讲一遍。1. 项目场景与故障复现1.1 我准备实现的功能与硬件连接这个项目是一个直流减速电机的位置闭环控制板电机自带增量编码器我需要同时做两件事用定时器的编码器接口模式读取编码器位置和方向用 PWM 驱动电机的 H 桥或驱动芯片。芯片选的是 STM32F103C8T6硬件连接是这样的功能引脚定时器通道编码器 A 相PA0TIM2_CH1编码器 B 相PA1TIM2_CH2电机驱动 PWMPA2TIM2_CH3编码器是 1000 线的增量式编码器做 4 倍频后每圈能输出 4000 个计数脉冲PWM 我希望跑到 20kHz这样电机运行噪声和电流纹波都比较理想。当时我的想法很朴素TIM2 有 4 个通道CH1/CH2 给编码器用CH3/CH4 不是还空着吗直接在 CH3 上出 PWM 不是顺理成章吗事实证明这个“顺理成章”就是后面所有问题的起点。1.2 我最初的初始化代码我用的是 STM32 标准 HAL 库。初始化顺序是先配置编码器接口再配置 PWM 输出代码如下/* 1. TIM2 编码器接口配置 */ TIM_Encoder_InitTypeDef sEncoderConfig {0}; sEncoderConfig.EncoderMode TIM_ENCODERMODE_TI12; sEncoderConfig.IC1Polarity TIM_ICPOLARITY_RISING; sEncoderConfig.IC1Selection TIM_ICSELECTION_DIRECTTI; sEncoderConfig.IC1Prescaler TIM_ICPSC_DIV1; sEncoderConfig.IC1Filter 0; sEncoderConfig.IC2Polarity TIM_ICPOLARITY_RISING; sEncoderConfig.IC2Selection TIM_ICSELECTION_DIRECTTI; sEncoderConfig.IC2Prescaler TIM_ICPSC_DIV1; sEncoderConfig.IC2Filter 0; htim2.Instance TIM2; htim2.Init.Prescaler 0; htim2.Init.CounterMode TIM_COUNTERMODE_UP; htim2.Init.Period 65535; htim2.Init.ClockDivision TIM_CLOCKDIVISION_DIV1; HAL_TIM_Encoder_Init(htim2, sEncoderConfig); HAL_TIM_Encoder_Start(htim2, TIM_CHANNEL_ALL); /* 2. TIM2 的 CH3 输出 PWM */ htim2.Init.Period 3600 - 1; // 72MHz / 3600 20kHz htim2.Init.Prescaler 0; HAL_TIM_PWM_Init(htim2); HAL_TIM_PWM_Start(htim2, TIM_CHANNEL_3); __HAL_TIM_SET_COMPARE(htim2, TIM_CHANNEL_3, 1800); // 50% 占空比代码从语法和 API 调用角度没有任何问题编译也顺利通过。但不管我怎么设置 CCR3PA2 引脚上就是没有标准的方波。1.3 故障现象与第一轮排查我把现象列了一下大概有三条编码器回读完全正常旋转电机轴CNT 值按照编码器脉冲正常增减方向也能正确判断。TIM2_CH3 引脚输出异常要么保持恒定高/低电平要么输出一些频率极不稳定的波形。单独关闭编码器模式只保留普通向上计数模式CH3 就能输出规规矩矩的 20kHz PWM。第一轮排查时我按常规思路查了一圈检查 GPIO 复用PA2 是 TIM2_CH3 的默认复用功能没有配错检查时钟TIM2 挂在 APB1 上时钟源 72MHz没有问题检查 CCR3 和 ARR比较值、自动重载值都合理把 PWM 单独提出来测试一切正常。最后我用条件编译把编码器初始化屏蔽掉只跑 PWM 部分示波器上立刻出现了标准的 20kHz 方波。再把编码器初始化恢复回去故障又回来了。到这里基本可以断定编码器接口模式的存在就是 PWM 输出失败的直接诱因。2. 根因剖析编码器模式与 PWM 根本不是“各干各的”2.1 定时器的内部架构所有功能共享同一个计数器很多人会下意识地把定时器的“通道”理解成独立的硬件单元——CH1 是编码器模块CH3 是 PWM 模块互不相干。实际情况完全不是这样。STM32 的通用定时器TIM2、TIM3、TIM4、TIM5内部可以简化成几个核心部件一个计数器 CNT所有定时功能共享预分频器 PSC决定 CNT 的计数时钟频率自动重载寄存器 ARR决定计数的上限捕获/比较寄存器 CCRx每个通道各自有一个捕获/比较通道用于输入捕获或输出比较/PWM。输入捕获IC干的事情是把 CNT 当前的值搬到 CCRx输出比较/PWMOC干的事情是比较 CNT 和 CCRx根据比较结果翻转通道输出。不管哪一个功能操作对象都是同一个 CNT。所以你可以把定时器理解成一个“车间里唯一的钟表”编码器模式和 PWM 模式都需要看这个钟但钟表的走时方式只能由一种模式决定。编码器模式想让它“跟着外部脉冲走”PWM 模式想让它“按固定频率自由跑”两者必然冲突。2.2 编码器模式到底改了哪个时钟源编码器接口模式是一种特殊的“外部时钟模式”。它通过设置 TIMx_SMCR 寄存器里的 SMS[2:0] 位来启用。启用后CNT 的时钟源就不再由内部时钟 CK_INT 提供而是改由 TI1CH1 引脚和 TI2CH2 引脚的边沿提供。这里有一个关键点编码器模式有 3 种计数方式计数沿和方向判断逻辑不同编码器模式SMS 值计数沿方向判断模式 1001TI1 的边沿TI2 电平模式 2010TI2 的边沿TI1 电平模式 3011TI1 和 TI2 的双边沿另一个信号的电平我用的就是模式 3也就是在编码器 A、B 相的上升沿和下降沿都计数同时根据另一相的电平判断计数方向。这种模式能实现 4 倍频所以 1000 线编码器能输出 4000 个计数/转。在编码器模式下CNT 的计数节奏完全由外部编码器脉冲决定电机不转CNT 就不动电机转得越快CNT 数得越快电机反转CNT 就反向计数。这个“CNT 的时基由外部决定”的机制是理解整个问题的钥匙。2.3 PWM 在非固定计数时钟下的实际表现为了让问题更清楚先看一下 PWM 正常工作的前提。PWM 输出本质上就是CNT 以固定频率计数从 0 数到 ARR 然后回绕CNT 小于 CCRx 时输出一种电平大于等于 CCRx 时输出另一种电平。这样在一个固定的计数周期内高电平时间和低电平时间之比就是占空比。PWM 频率的计算公式是f_PWM f_CK_PSC / ((PSC 1) * (ARR 1))这个公式成立的前提是 CNT 的计数时钟是稳定的内部时钟。只有时钟稳定ARR 周期对应的时间才是固定的PWM 频率才是稳定的。但在编码器模式下CNT 的时钟变成了编码器脉冲。虽然 ARR、CCR3 这些寄存器还在比较逻辑也还在但 CNT 走到哪儿、什么时候走完全不受你控制编码器静止CNT 停在某个值上不动CCR3 的比较结果也保持不变PA2 引脚就维持在一个固定电平上。你看到的是一次电平不是 PWM。编码器匀速旋转CNT 以外部脉冲节奏计数PA2 确实会周期性翻转但翻转频率 编码器计数频率 / (ARR 1)也就是转速的实时映射和你在代码里设置的 20kHz 没有一毛钱关系。编码器反转CNT 反向计数输出逻辑更加混乱波形在示波器上就像随机毛刺。编码器抖动如果机械上有振动编码器来回抖动CNT 的增减方向不断变化PWM 输出更是完全没法看。用一句通俗的话说PWM 的“节拍器”被换成了外部编码器的转动。节拍频率不由你管波形自然不对。2.4 为什么很多参考手册没有直接写“禁止”我后来翻参考手册发现关于编码器接口模式的章节主要篇幅都花在输入滤波、极性、计数沿和方向判断上并没有用大字警告“此时不能输出 PWM”。这是因为从寄存器层面看硬件并没有禁止你在编码器模式下配置输出比较通道。你完全可以CC3M110、CC3E1把比较输出打开硬件也允许。问题在于输出比较的质量依赖稳定的计数时钟而编码器模式恰恰破坏了这一点。手册在外部门控模式、从模式选择等章节提到过当定时器由外部信号驱动时计数器行为受外部信号控制。编码器模式本质上是这种“外部驱动”的一种特例。很多开发者没把这两章串起来看就会掉进这个坑里。所以这个问题的本质不是“PWM 模块坏了”而是“PWM 没有稳定的时基”。只要 CNT 是由外部脉冲驱动的PWM 就不可能稳定输出预期频率。3. 可行解决方案让 PWM 回到稳定的时基上3.1 最省心的做法换一个定时器输出 PWM在 STM32F103 这类 MCU 上定时器数量通常足够工程使用。我的最终方案就是让 TIM2 专职做编码器计数PWM 挪到 TIM3 上。具体修改如下/* TIM2 只保留编码器配置 */ htim2.Instance TIM2; htim2.Init.Prescaler 0; htim2.Init.CounterMode TIM_COUNTERMODE_UP; htim2.Init.Period 65535; HAL_TIM_Encoder_Init(htim2, sEncoderConfig); HAL_TIM_Encoder_Start(htim2, TIM_CHANNEL_ALL); /* TIM3 单独做 PWM */ htim3.Instance TIM3; htim3.Init.Prescaler 72 - 1; // 72MHz / 72 1MHz htim3.Init.CounterMode TIM_COUNTERMODE_UP; htim3.Init.Period 50 - 1; // 1MHz / 50 20kHz HAL_TIM_PWM_Init(htim3); HAL_TIM_PWM_Start(htim3, TIM_CHANNEL_1); __HAL_TIM_SET_COMPARE(htim3, TIM_CHANNEL_1, 25); // 50% 占空比改完之后编码器还走原来的 PA0/PA1PWM 输出改到 TIM3_CH1 对应的引脚上。在 F103 上TIM3_CH1 默认是 PA6也可以重映射到 PB4。具体用哪个引脚要在画板之前就定好。这个方案的好处非常明显编码器和 PWM 完全解耦编码器怎么转都不影响 PWM 的频率和占空比代码逻辑清晰调试时不用猜“是不是两个功能打架了”后续扩展其他功能比如再增加一路 PWM也更方便。代价是占用了两个定时器。对大多数项目来说这个代价是完全可以接受的。3.2 如果定时器资源非常紧张用输入捕获代替编码器模式有些项目可能真的只有一个定时器可用又必须同时处理编码器和 PWM。这种情况下可以考虑放弃硬件编码器接口模式改用输入捕获或者外部中断的方式读取编码器。思路是这样定时器本身继续配置为普通向上计数模式CNT 由内部时钟驱动把 CH3 配置为 PWM 输出CNT 稳定走锯齿波PWM 正常编码器 A 相接到 CH1 的输入捕获通道每次捕获中断记录 CNT 的差值就可以算出速度方向由另一个 IO 口电平判断。代码大致是这个思路/* 定时器本身做 PWM */ htim2.Init.Period 3600 - 1; htim2.Init.Prescaler 0; HAL_TIM_PWM_Init(htim2); HAL_TIM_PWM_Start(htim2, TIM_CHANNEL_3); /* 同时使能 CH1 输入捕获中断 */ HAL_TIM_IC_Start_IT(htim2, TIM_CHANNEL_1); /* 中断回调里读取并重置捕获值 */ uint32_t last_capture 0; void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) { if (htim-Instance TIM2 htim-Channel HAL_TIM_ACTIVE_CHANNEL_1) { uint32_t current __HAL_TIM_GET_COUNTER(htim2); uint32_t diff current - last_capture; last_capture current; // diff 用于计算转速 } }需要注意这种方案会引入中断开销。如果编码器转速很高捕获中断频率会非常高。实际使用前一定要算一下最坏情况的中断频率最大中断频率 ≈ 编码器分辨率(脉冲/转) × 最大转速(转/秒)比如 1000 线编码器、最高转速 3000 转/分那每秒就是 50 转A 相单边沿频率大约 50kHz。STM32F103 的中断处理能力完全能够应付但如果你在中断里做了大量浮点运算或者复杂逻辑就另当别论了。3.3 需要频率随转速同步试试主从定时器联动有一种少见的场景是真的希望 PWM 频率跟随编码器转速变化比如某些位置触发换相、步进细分、或者需要与角度严格同步的控制。这种场景下可以反过来利用“编码器驱动计数器”这个特性。方法是用主从定时器联动TIM2 保持编码器模式配置为主定时器通过 TRGO 把编码器触发的信号输出TIM3 配置为从定时器选择外部时钟模式接收 TIM2 的 TRGO 作为计数时钟TIM3 的 CH1 输出 PWM它的频率就会跟随编码器转速同步变化。这种用法属于比较高级的定时器联动场景配置复杂度高而且应用需求必须确实需要“频率与速度同步”。如果只是想要一个稳定的 20kHz 固定 PWM不建议用这种方案。这里提出来是为了避免有些读者在特殊项目里走回头路。3.4 选型与引脚规划检查等到代码阶段再发现定时器不够用就已经晚了。根据我的经验在画 PCB 之前就应该做一次外设资源清单至少把下面这些问题回答清楚项目里有几个定时器型号分别是哪个系列编码器需要哪个定时器的 CH1/CH2对应哪些引脚PWM 输出需要哪个定时器的哪个通道对应哪些引脚有没有启用定时器重映射重映射后的引脚会不会和其他外设冲突是否需要高级定时器TIM1/TIM8的互补输出、死区插入等功能把这些整理成一张表格贴在开发文档第一页后面能省掉大量“靠改代码救硬件”的时间。4. 调试实操快速定位和验证这类问题的检查清单4.1 一上来先看寄存器别乱改代码再遇到“PWM 出不来”的情况我的建议是不要再猜了直接用调试器挂上把定时器相关寄存器全部看一遍。重点看这几个寄存器关注位判断结论TIMx_SMCRSMS[2:0]非 0 表示处于从模式/外部时钟模式很可能就是编码器模式TIMx_CCERCC3E为 0 表示输出没使能TIMx_CCMR2CC3M[2:0]应为 110PWM 模式 1或 111PWM 模式 2TIMx_CR1CEN为 0 表示计数器没使能TIMx_CNT当前计数值编码器模式下会随外部脉冲变化普通 PWM 模式下固定锯齿TIMx_CCR3比较值应小于 ARRTIMx_ARR自动重载值决定 PWM 周期我之前排查时发现PWM 配置位的值完全正确CC3E1、CC3M110但SMCR的SMS被编码器初始化函数改成了011一眼就看出问题所在。很多时候代码没错但“模式叠加”本身就是一个隐藏 bug。4.2 用示波器分辨“没有 PWM”和“PWM 频率乱”把示波器探头放在你的 PWM 输出引脚上按照下面几个步骤判断保持编码器静止观察波形。如果是恒定高或恒定低说明计数器没在稳定计数。用手慢慢转动电机轴观察波形。如果出现脉冲但频率明显跟着手速变化基本可以实锤是编码器时钟源导致的问题。如果编码器反转输出波形出现完全无规律的跳变同样说明 CNT 的计数方向在实时影响输出。关掉编码器模式重新使能定时器普通计数模式如果 PWM 立刻恢复正常那问题就彻底定位了。这套方法在实际排障中效率很高。它能够快速区分“输出比较模块坏了”和“计数时基不对”这两种完全不同的原因。前者是硬件或引脚问题后者是定时器模式冲突问题。4.3 常见误区与避坑指南排查过程中我还总结出几个容易踩的误区误区一以为定时器通道是“独立模块”。CH1/CH2 做编码器CH3 做 PWM看起来互不影响实际上所有通道共享同一个计数器。误区二把 PSC 当万能配置工具。在编码器模式下PSC 只能对编码器脉冲做预分频无法把外部脉冲变成你想要的稳定内部时钟。改 PSC 并不能挽救 PWM。误区三调整初始化顺序能解决问题。就算先把 PWM 初始化好再初始化编码器只要编码器模式生效CNT 的时钟源就会被外部信号接管PWM 照样失败。误区四查资料时只搜“PWM 无输出”而忽略了“定时器同时处于编码器模式”这个关键前提。搜索方向错了很难找到对症的内容。如果项目中定时器用的多建议把每个定时器的工作模式、承担功能、使用通道做一张配置表放在代码注释的最前面。后期维护、排查、交接都会省很多力气。4.4 工程上的最佳实践总结经过这件事我现在维护的嵌入式项目基本都会遵循这几条默认规则一个定时器只承担一种主功能。尤其是编码器模式、PWM 这类对时基依赖都比较强的功能尽量不要挤在同一个定时器上。定时器资源在项目需求阶段就要分配好。不要等到 PCB 打样回来、代码写了一半才发现资源冲突。如果 MCU 定时器数量确实不够用优先考虑换型号而不是硬在固件层面“抖机灵”。硬凑的方案往往带来更大的调试成本和维护负担。初始化完成后做一次寄存器自检。很多调试问题不是因为代码逻辑复杂而是因为某个外设被另一个外设的初始化过程无意中改了配置。上电早期打印一下关键寄存器的值能快速确认每个外设处于什么状态。4.5 再补充一个容易被忽略的细节初始化顺序我在最后再补充一个容易被忽略但很实用的细节。如果你不得不在同一个定时器上同时保留“编码器读取”和“PWM 输出”的配置代码那么初始化顺序要特别小心。HAL 的HAL_TIM_Encoder_Init()函数会重写TIMx_CR1、TIMx_SMCR、TIMx_CCMR1和TIMx_CCER等寄存器。如果你先调用HAL_TIM_PWM_Init()再调用HAL_TIM_Encoder_Init()编码器初始化会把 PWM 初始化设置的某些位覆盖掉。反过来如果你先初始化编码器后初始化 PWMPWM 初始化又可能影响编码器相关配置。所以如果真的因为某些特殊原因必须把两个功能放在同一个定时器里建议不要依赖 HAL 的先后顺序应该直接对寄存器做“按位操作”把编码器模式需要的位和 PWM 输出需要的位分别设置好然后仔细检查最终寄存器值。但说实话这种操作难度和风险都偏高我依然建议直接分到两个定时器。我个人在实际操作中的体会是这类问题的排查思路往往比具体代码更重要。不要一上来就怀疑“占空比设错了”“通道配错了”“引脚坏了”先把定时器的时基来源整理清楚再去看现象。只要 CNT 的计数时钟不是你预期的那个源PWM 就永远不可能按你想的方式工作。这个问题最后我用“TIM2 只做编码器TIM3 做 PWM”的方式解决前后不到半小时就验证通过。希望你下次遇到同样的问题时能直接跳到正确解法而不是像我一样在代码里来回折腾大半天。