ARTICLE DETAIL

建站实战干货

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

STM32定时器Encoder模式驱动EC11编码器:从原理到代码实现

2026/9/24 13:27:25 拓冰建站 浏览量
STM32定时器Encoder模式驱动EC11编码器:从原理到代码实现 1. 轮询EC11编码器的问题在哪代码又乱又准度差实话跟各位说玩STM32有一段时间的人基本都跟EC11编码器打过交道。这玩意儿在音量旋钮、频率调节、菜单选择、电机调速面板上到处都是可真正能把它用得顺手的人却不多。我见过太多人在写EC11驱动的时候用的是最原始的轮询方式在while(1)主循环里反复读两个GPIO引脚的电平然后手动判断相位关系、手动消抖、手动计数。这种方式不是不能用而是非常别扭。首先是CPU被白白占用主循环每次都要去扫那两个引脚哪怕用户根本没转旋钮代码也在那里空转。更难受的是逻辑容易出问题EC11在转动的时候A相和B相会输出一系列电平变化用轮询去判断正反转你得维护一套状态机什么CW、CCW还要处理各种中间态。稍微没处理好要么是转一下跳好几个数要么是该加的时候减了该减的时候加了。而且如果主循环里还有其他耗时操作比如刷屏、处理通信协议轮询的实时性就会变得很差快速转动时漏脉冲是家常便饭。我最早用EC11做菜单旋钮的时候就踩过这个坑。那时候用的是软件消抖加轮询代码里塞了一堆标志位和case分支改一个逻辑要牵动好几个地方调了整整两天才勉强能用。后来换了思路去翻参考手册发现STM32的定时器外设居然自带Encoder模式专门处理这类正交编码器信号。花了一下午改完代码量直接砍掉一半以上而且转动的检测精度和实时性好了一截。这个方案的核心逻辑其实不复杂EC11转起来产生的A、B两相信号本来就是一对正交信号而STM32定时器的Encoder模式就是为这类信号设计的。由硬件自动捕捉相位变化、自动判断旋转方向、自动增减计数器的值CPU完全不用管只需要在需要的时候去读一下计数器的值即可。这篇文章就把这个方案从原理到落地完整写一遍包括定时器Encoder模式的底层机制、与EC11的特性匹配点、完整的配置代码、以及我在实际项目中踩过的坑和解决过的边界问题。阅读本文你不需要多深的基础只要能看懂GPIO配置、知道定时器大概是个什么东西就能照着我给的步骤把它跑起来。2. EC11编码器的信号特性为什么它天生适合定时器Encoder模式2.1 EC11输出的到底是什么信号EC11编码器内部是一个机械结构旋钮转动时内部的金属刷片会依次接触不同的触点A、B两个引脚就分别输出两路方波信号。因为机械结构本身的性质这两路方波的相位差固定是90°这种信号组合在专业上叫正交编码信号。这里得先搞清楚一个关键点EC11跟光电编码器不一样它的输出是机械触点的通断所以信号上升沿和下降沿不是瞬间完成的会有毛刺和抖动。这也是很多新手疑惑为什么EC11转一下会跳好几个数的直接原因。为了把抖动问题说透我用一个具体例子。假设你以正常速度拧旋钮某个瞬间A相从低电平跳到高电平这个跳变过程里由于金属触点的弹性碰撞电平会来回跳几次每次可能持续几十微秒到几百微秒。如果你的程序在主循环里高速轮询刚好扫到了这些毛刺就会把一次机械转动误判成好几次脉冲这也就是轮询方案精度差的根源。2.2 正转和反转在信号上的本质区别正交编码信号的核心特征是当旋钮正转时A相的电平变化会领先B相90°反转时反过来B相领先A相90°。这句话听着抽象实际看波形就非常直观。正转时A相先产生一个上升沿紧接着B相才产生上升沿反转时B相先上升A相随后才上升。也就是说通过观察一个通道跳变时另一个通道的电平是高还是低就能判断旋转方向。这其实跟两个人在门口依次进出的状态很像。A相和B相分别代表两个人的位置变化谁先迈步、谁后迈步就决定了整个队伍是往里走还是往外走。2.3 EC11转一下到底发几个脉冲很多刚接触EC11的人都会问一个问题拧一格编码器到底输出多少个脉冲这个问题的答案跟编码器的型号有关市面上常见的EC11有12脉冲/圈、18脉冲/圈、20脉冲/圈、24脉冲/圈等不同规格。这个脉冲数是编码器一整圈输出的脉冲总量对应的作用是你的代码读到的计数器每增加一定数值说明旋钮转过了确定的机械角度。用24脉冲/圈的编码器举例拧完整整一圈计数器总共增加24步。但要注意的是EC11通常是带定位设计的你每次拧一格感觉到咔嗒一下这一格可能对应1个脉冲也可能对应2个脉冲取决于内部结构设计。我实测下来带定位的EC11多数是一格对应1个脉冲也就是一整圈有24个定位档位刚好对应24个脉冲。知道了这个参数你就可以在代码里做精确换算比如旋钮每转一格对应音量增减1dB还是0.5dB完全由你决定的映射关系来定。3. STM32定时器Encoder模式的底层机制硬件为什么能自动干活3.1 定时器外设里的正交解码器逻辑STM32的定时器特别是通用定时器TIM2、TIM3、TIM4、TIM5这些内部有一个专门处理正交编码信号的功能模块。它的作用可以理解成把A、B两相输入信号通过两个输入捕获通道TI1和TI2送入一个硬件状态机这个状态机会根据两路信号的相位关系自动决定计数器的方向递增还是递减。这个设计的精妙之处在于状态机是纯硬件逻辑不像软件那样需要采样和执行指令。每一个边沿到来硬件都会实时响应并更新计数器和方向标志。所以无论你拧旋钮的速度多快只要信号频率在硬件能响应的范围内就不会丢步。关键的一点是方向判断的鲁棒性非常好。在低速的时候A、B两相的电平变化是规整的状态机很好判断在快速猛拧的时候哪怕你只捕捉到了部分边沿状态机也能根据电平的组合关系恢复正确的旋转方向。3.2 计数倍频1倍频、2倍频和4倍频的区别STM32的Encoder模式提供一个非常实用的配置项计数倍频。你可以选择只在A相或B相的单个边沿计数1倍频也可以选择在两个通道的各个边沿都计数4倍频。这里的倍频关系是个让很多人绕晕的点我用表格来说清楚。倍频配置计数器变化规律适用场景1倍频仅在A相上升沿计数当你想只关心最简单的正反转判断时2倍频在A相和B相的上升沿都计数需要更高分辨率但能容忍一定噪声时4倍频在A相和B相的所有边沿都计数追求最大分辨率适合对位置精度要求高的场合要注意的是4倍频模式虽然精度最高对外部噪声也更敏感。EC11这种机械编码器的信号带有抖动4倍频可能会把一次物理接触的微小抖动放大成好几个脉冲。所以我个人在EC11项目里一般用2倍频平衡了分辨率和稳定性。如果你的项目用的是光电编码器、信号很干净那果断用4倍频就好。用24脉冲/圈的EC11举例1倍频时一圈只有24次计数2倍频时一圈最多48次计数4倍频时一圈最多96次计数。分辨率越高你在代码里能做的细分控制就越精细。3.3 计数器的溢出、回绕与清零策略定时器计数器本身是一个16位或32位的寄存器16位模式下计数范围是0到65535超出上限会回绕到0低于0会回绕到最大值。这在编码器场景下其实影响不大因为EC11不像电机编码器那样需要记录多圈累计绝对位置。你通常只需要读当前值、和上一次读的值做差、然后把计数器复位清零或者手工把差值累加到自定义变量里。我习惯的做法是这样的在主循环或定时器中断里周期性读取计数器的值计算差值然后将计数器清零。这样好处很多。操作方式优点缺点计数器不清零只记录绝对值代码简单多圈旋转会溢出需要额外判断读值后清零每次读到的就是增量逻辑清晰如果两次读取间隔内有大量转动会有少量丢失实际项目影响可忽略不清零程序里做相对差运算兼容大量转动不丢数据代码多几行需要处理16位回绕边界我在实际项目里多数使用读值后清零的方式简单直接而且由于EC11的机械特性人是不可能在几毫秒内转出成千上万步的读取间隔内的丢失完全无感。4. 完整代码实现与关键配置直接复制就能跑通了4.1 硬件连接规划先说硬件连接。EC11编码器一般有五个引脚分别是A相、B相、公共端C、以及两个用于按键功能的引脚按键做开关用通常按下接地。不同厂家的引脚定义有差异但A、B、C三个基本引脚是固定的。我在STM32F103C8T6最小系统板上用的连接方式是EC11的A相接到PA0对应TIM2的CH1输入通道。EC11的B相接到PA1对应TIM2的CH2输入通道。EC11的公共端C接地。如果编码器带按键按键一端接PA2另一端接地。PA0和PA1是TIM2的通道1和通道2这是STM32F103系列上最经典的编码器引脚组合之一。别的引脚不是不能用关键是要查一下定时器的通道映射表确保你用的引脚确实对应目标定时器的输入通道。补充一点EC11编码器模块上如果有上拉电阻那STM32内部可以配置为浮空输入如果没有最好在STM32初始化时把引脚配置成上拉输入避免悬空导致的不定电平。我手头的模块自带上拉电阻所以用的浮空输入也能稳定运行但很多裸编码器没有上拉那就得注意了。4.2 GPIO与定时器初始化代码注释这里用标准外设库写一份完整的初始化配置基于STM32F103系列。当然HAL库的思路是一样的只是API名称不同。void EC11_Encoder_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_ICInitTypeDef TIM_ICInitStructure; // 开启GPIOA和TIM2的时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); // PA0、PA1作为编码器输入配置为浮空输入 GPIO_InitStructure.GPIO_Pin GPIO_Pin_0 | GPIO_Pin_1; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IN_FLOATING; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); // TIM2时基配置预分频设为0 TIM_TimeBaseStructure.TIM_Period 0xFFFF; // 16位计数器最大值 TIM_TimeBaseStructure.TIM_Prescaler 0; // 不分频直接使用72MHz时钟 TIM_TimeBaseStructure.TIM_ClockDivision TIM_CKD_DIV1; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, TIM_TimeBaseStructure); // 编码器接口配置 TIM_EncoderInterfaceConfig(TIM2, TIM_EncoderMode_TI12, TIM_ICPolarity_Rising, TIM_ICPolarity_Rising); // 输入捕获配置 TIM_ICInitStructure.TIM_ICFilter 10; // 输入滤波抑制机械抖动 TIM_ICInitStructure.TIM_ICPrescaler TIM_ICPSC_DIV1; TIM_ICInitStructure.TIM_ICSelection TIM_ICSelection_DirectTI; TIM_ICInitStructure.TIM_ICPolarity TIM_ICPolarity_Rising; TIM_ICInit(TIM2, TIM_ICInitStructure); TIM_Cmd(TIM2, ENABLE); }这段代码的核心就俩函数TIM_EncoderInterfaceConfig设置编码器接口模式TIM_ICInit设置输入滤波。其余的时基初始化是为计数器提供一个可工作的框架。4.3 滤镜参数的选择与实测效果TIM_ICFilter这个参数非常关键它对应定时器内部的输入滤波器本质是一个数字低通滤波器可以滤除信号线上的窄脉冲毛刺。这个滤波器的原理是只有输入信号保持稳定N个采样时钟周期后内部看到的电平才会改变高频毛刺会被直接忽略。滤波器参数的取值范围是0到15数值越大滤波越强但也会让信号的有效边沿延迟更多。我实测的结果是参数设为0完全不滤波抖动明显快速转动偶尔会跳数。参数设为4到6抖动基本消除但反应依然灵敏。参数设为10到15非常稳定但快速转动时会有轻微的延迟感。实际项目里我通常把滤波值设在8到10之间既保证了稳定性又不会让旋钮变得迟钝。这个值不是随便选的它跟定时器的输入时钟频率有关系比如72MHz主频下滤波采样周期大约是几十纳秒一级具体计算方式在参考手册里写得很清楚但实际调试时直接换几个值测试手感是最快的。4.4 读取编码器状态与按键复用编码器初始化和滤波到位之后主程序里的读取逻辑就变得极其简单了。int16_t EC11_GetCounter(void) { int16_t val; val (int16_t)TIM_GetCounter(TIM2); TIM_SetCounter(TIM2, 0); // 读完立即清零 return val; }这个函数每次调用返回的是自上次读取以来的增量正数代表正转负数代表反转零代表没转。因为你读到的值可能是负数所以变量类型要使用有符号的int16_t。这里有个细节值得展开。你可能会好奇为什么一定要读完立刻清零原因在于EC11用于人机交互的场景我们一般只关心相对变化量而不关心绝对位置。每次拿到增量更新全局变量然后清零下次读到的依然是一个增量这样逻辑上不会累计误差。如果你硬是不清零还得自己处理计数器溢出时的回绕逻辑何必呢EC11编码器带按键的结构在市面上很常见转钮加按键的组合可以做菜单的左右调节确认功能。按键检测很简单按键引脚在内部被上拉按下时变低用一个10ms左右的软件延时或者定时器扫描消抖就行。这里不需要占用编码器模式的两个通道所以直接复用普通的GPIO输入检测即可。4.5 主循环里怎么用读到的值转到实际应用场景你可以把增量值直接当成全局变量的变化量来用。这里给出一个音量调节的简化示例。int16_t volume 10; // 初始音量 int16_t inc; void EC11_Process(void) { inc EC11_GetCounter(); if (inc ! 0) { volume inc; if (volume 0) volume 0; if (volume 100) volume 100; printf(Volume %d\n, volume); } }这段代码不再需要一个复杂的按键状态机也不需要在主循环里反复读引脚你只需要周期性地调用EC11_Process比如放在1ms或者10ms的定时器中断里中断的频率越高旋钮的响应越即时。如果放在10ms的中断里快速旋转时依然能准确捕获步数因为硬件的计数器不会丢脉冲只是你读取的频率决定了你多久能发现一次变化。5. 我把代码从轮询改成Encoder模式后的前后对比5.1 轮询方案原本的代码有多麻烦为了让你直观感受为什么说代码量减半我简单把之前轮询版的代码结构列一下。那时候要维护一个状态标志大概思路是static uint8_t last_state 0; void EC11_Scan(void) { uint8_t a GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0); uint8_t b GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_1); uint8_t now (a 1) | b; // 组合当前AB相电平 // 根据上次状态和当前状态的组合判断旋转方向 // 这个判断要覆盖全部状态跳变组合写起来很长 // 还需要消抖延时遇到抖动会产生误判断 ... last_state now; }这还只是最简单的判断逻辑实际项目里还得加上消抖的延时、防止重复触发用的标志位、以及和操作界面模块联动的逻辑。总之光这一个旋钮的驱动写了上百行代码而且调试的时候非常痛苦因为你不知道某个跳变是真实转动还是抖动。5.2 用了Encoder模式之后的变化现在的代码量有多少呢初始化函数大约30行读取函数6行应用层逻辑另算。整体下来不到原来的一半。当你把定时器Encoder模式的代码写出来之后剩下的主循环逻辑可以变得非常干净旋钮相关的工作几乎就一个流程轮询读值、处理增量。更重要的改变是实时性。轮询模式下CPU必须保证在两次扫描之间不能有太长的停顿否则会错过信号的某个边沿。而在Encoder模式下定时器硬件是全自动处理的无论你的主循环在干什么只要定时器在跑脉冲就不会丢。这在做菜单系统的时候特别重要因为界面刷新和逻辑处理经常会占用较长的CPU时间。5.3 从资源占用的角度看两个方案的差距用不用中断资源占用差别也很明显。轮询方案如果放在主循环里会占用循环周期的大量时间放在定时器中断里做高频扫描又会频繁打断主流程的执行。Encoder模式完全不需要为编码器专门开一个高频中断你只需要在低频的系统节拍里比如10ms访问一次计数器即可。这对于依赖RTOS或者复杂状态机的项目尤其有价值。当多个任务需要共享CPU时你不想因为一个旋钮的高频扫描需求影响了其他的任务调度。6. 踩坑实录这些边界问题项目里遇到过才懂处理6.1 初始化之后计数器不在零位初始化定时器之后计数器默认值为0但如果你用的是编码器接口模式一旦信号线上有毛刺或者上电瞬间的电平跳变计数器可能立刻从0变成正值或负值。你可能在开机的那一刹那什么都没做就会看到旋转值莫名其妙地发生了跳变。解决方式很简单在系统启动完成、功能逻辑开始运行之前手动调用一次TIM_SetCounter(TIM2, 0)把计数器强制清零。这一步看似多余实际却非常重要。即便没有启动毛刺我也建议在每次开启编码器功能之后都做一次清零以消除不确定状态。6.2 编码器模块带了额外的滤波电容导致信号边沿变缓有些EC11模块为了方便接线会在A、B引脚上加上拉电阻并且并联几纳法的滤波电容。这个电容会让信号的上升沿和下降沿变得不那么陡峭好的一面是抗干扰变强了坏的一面是如果你的引脚配置成高速输入模式反而可能因为边沿缓导致多次触发。遇到这种情况优先调整TIM_ICFilter的值增加滤波级数而不是去改GPIO速度。我之前一个项目里换了另外一个厂家的EC11模块以后快速旋转出现了极其严重的跳数排查了一下午没找到原因最后发现是新模块的PCB上多焊了一个100nF的大电容直接把信号的边沿弄缓了。改用更高的滤波参数后问题消失。这事给我的经验是遇到异常跳数先看波形别急着怀疑代码。6.3 快速旋转时偶尔反向走一下如果你发现旋钮快速转动时偶尔会出现一次反向跳变多半不是逻辑判断的问题而是信号毛刺或者滤波不够。EC11内部机械触点在快速转动时会产生短暂的悬浮状态这个状态下引脚电平是不定的如果没有滤波电路和数字滤波参数配合硬件状态机可能会误判一次方向。这种情况的排查顺序建议是先用示波器看A、B两相的波形确认毛刺出现在哪个位置然后尝试加大TIM_ICFilter的数值如果还是不行考虑在引脚对地加一个小电容比如10nF到100nF作为硬件层面的滤波。我一般在电路设计阶段就预留这个电容的位置以便调试时按需焊接。6.4 两个定时器通道的极性配置错了正反转反了有一种特别容易踩的坑就是A、B相信号的极性和你预期的输入捕获极性不匹配导致正转读数反而变成负数。这种问题一般不是硬件接错而是软件配置里的TIM_ICPolarity设置。有些应用里你希望正转时计数器增加却发现现实中正转时计数器在减。这时候不用去交换A、B引脚直接把输入捕获极性反转就好。你也可以在应用层做取反处理但直接从配置层修正更干净。如果你用HAL库看到的是__HAL_TIM_SET_CAPTUREPOLARITY之类的宏效果是一样的。6.5 读值清零后丢失中间增量的处理方案前面提过我推荐读值清零的方式但有些场景下这种方式有水桶效应。比如你在一个1ms的中断里去读计数器清零之前机械旋钮转得飞快在极短的时间内转出了好几圈计数器一次增量可能超过65535吗有可能特别是你用了4倍频模式并且是一颗多脉冲每圈的高分辨率编码器。当然了EC11的时间尺度上人的手速远远不够在1ms里转出超过65535步所以这个问题在EC11上几乎不存在。但如果你以后做光电编码器、磁盘位置环这类高速变化的场景建议改用读值不立即清零、在应用层做两次值的差分计算的方式才能保证不丢数据。7. 从EC11出发这套方案还能怎么扩展稳定跑通EC11之后你能把这套思路扩展到很多地方。第一个直接的扩展是双编码器甚至多编码器。只要STM32还有空闲的定时器你就可以用同样的套路为每个编码器分配一个定时器互不干涉。比如一个用于音量调节一个用于菜单滚动一个用于参数微调代码结构完全一致。我在一个信号发生器项目里同时挂了三个EC11一个调频率、一个调幅度、一个调占空比整个界面交互立刻丰富起来。第二个扩展方向是替代普通旋转开关或电位器。电位器的缺点是寿命有限、精度有限而EC11配合定时器Encoder模式精度取决于你的编码器分辨率寿命远高于电位器。很多功放、电源的数字控制面板已经大量采用这类方案了。第三个方向是结合其他外设做自动化交互。比如编码器配合OLED显示屏做实时参数调节旋钮转动的每个档位都对应屏幕上参数的变化用户操作反馈非常直观。编码器配合步进电机做手动微调用在3D打印机或者数控平台上可以实现精确的位置补偿。还有一个大家比较关心的扩展是低功耗场景。EC11的Encoder模式在读取时CPU可以处于睡眠状态只有定时器和GPIO在监听信号一旦有转动事件完全可以通过外部中断或者定时器触发唤醒MCU。这比轮询方案在功耗上优势明显因为轮询必须让CPU定期醒来干活而Encoder方案可以让CPU一直睡到底。8. 一些测试数据和参数推荐为了让你少走调试弯路我把自己用过的几个参数组合按场景整理成表。场景滤波参数倍频模式读取周期实测结果菜单选择/音量调节82倍频10ms稳定无跳数手感顺滑需要精细位置调节44倍频1ms精度高偶尔会有小跳变强电磁干扰环境121倍频5ms稳定性优先响应略迟电池供电低功耗项目102倍频事件触发功耗低转动响应可靠这些参数不是绝对的不同厂家EC11的输出质量差异较大所以真正上项目之前建议花半天时间测一下你手头编码器的手感和跳数情况然后微调滤波值。另外如果你用的是其他型号的STM32比如G0、L4、H7系列编码器模式的基本逻辑一致只是引脚映射、时钟树配置、库函数名称略有差别对照参考手册的复用功能表即可。换芯片时最容易出错的是把定时器通道引脚搞错建议查表确认后再画板或接线。我也看到很多人有疑问到底是用标准外设库还是HAL库来做这个功能。我的建议如果你是初学者用标准库确实更容易理解底层原理因为代码更透明如果你做的是工程化项目、需要频繁换芯片平台HAL库的可移植性更好。功能层面没有本质区别核心逻辑都是配置定时器的Encoder模式所以不必在这个问题上纠结太久。9. 顺手把按键抖动和交互手感一起优化了EC11编码器往往自带按键旋钮按下就是一次确认操作。这个按键用普通GPIO检测的话消抖处理跟独立按键完全一致。很多人会在消抖上花不少时间这里分享一个自己用着很顺手的方案。按键检测放在定时器中断里一般用1ms到5ms的扫描周期即可检测到电平变化后延时几十毫秒再次确认电平是否稳定如果稳定就认为是有效按键事件否则丢弃。这样可以最大程度避免机械抖动带来的虚假触发。手感方面有两个额外的小技巧。第一在旋转值变化后可以触发短时间的提示反馈。比如播放一个短暂的蜂鸣器嘀声或者让屏幕闪烁一下用户会立刻意识到自己的操作被系统识别了。第二如果旋钮转动很快可以对增量做非线性映射比如在参数调节时快速转动超过某阈值就自动粗调慢速转动则细调。这种做法在工程上叫自适应步进体验比恒定步进好很多。我在很多项目的调试中总结了一个规律编码器本身是个非常成熟的器件它的行为特性基本不会变真正决定交互品质的往往是滤波参数、读取频率和步进映射这三者的配合。建议你在开发的时候把这几个参数尽量设计成可调项方便现场调试时临时修改。10. 关于编码器数值溢出和符号处理的进一步说明在实际使用中很多人会在计数器的数值类型上翻车。我这里再强调一次读取定时器计数值时一定要把返回值和变量都声明成有符号16位类型。原因是当你读到的计数器最高位是1时如果声明的类型是无符号程序会把它解释成一个很大的正数而实际上它是一个负数。比如你用uint16_t类型读取计数器值为0xFFF6程序会认为旋转增量是65526但这个值在编码器场景里正常情况下不可能出现显然是符号处理出了问题。换成int16_t类型0xFFF6便会被正确解释为-10代表反方向转了10步。代码里最容易忽略符号问题的地方是在编译优化选项开启的时候某些隐式类型转换可能会产生警告或者错误行为。建议所有编码器相关变量都显式声明为有符号类型不要在函数返回值上过度依赖编译器隐式转换。另外一个值得注意的点是不同STM32系列的定时器计数器位宽不一样。F1系列的定时器大部分是16位F4系列有些定时器是16位有些是32位。如果你用的是32位定时器读取增量时要用int32_t类型或者依然截取低16位使用但要确保你不会在短时间内累计超过16位的增量。对EC11来说16位已经完全充裕但了解这一点能帮你避免在别的项目上踩坑。11. 选型建议什么情况用轮询什么情况用Encoder模式客观地说我不是说轮询方案一无是处。如果你的项目里编码器只是偶尔用一下转动的频率极低而且对实时性没有任何要求那么用轮询省一个定时器也是可以的。比如某些设备只在设置界面才检测旋钮平时旋钮完全闲置这种场景下轮询的缺点不会被放大。但你的项目如果满足下面任何一条我都建议直接上Encoder模式旋钮的转动是高频操作比如音量旋钮、频率旋钮。系统主循环里有较多耗时任务无法保证高频扫描。需要精确的方向判断和步数统计不想在软件状态机上花时间。代码里已经有多个定时器任务在跑想尽可能减少CPU占用。以后可能扩展到多编码器或更多设备交互。我见过有些同学觉得定时器资源很宝贵不想为编码器占用一个完整的定时器。但实际上在大多数MCU方案里TIM2到TIM5这些通用定时器并不总会被全部用满而编码器带来的交互价值远远超过这个资源消耗。如果真到了定时器完全不够用的地步那我可以负责任地说轮询方案也只是权宜之计你更应该从系统架构层面考虑选型比如换一颗更多定时器的芯片。回到本文标题的那句话代码量减半不是我随口说的。从我的实际经验来看代码量的缩减只是最表面的收获更深层的收获是可靠性的提升和心智负担的降低。写代码这件事有时候不是功能越多越高级而是能用更少的代码稳定实现同样的功能才真正体现你对底层的理解程度。定时器Encoder模式就是一个典型的让硬件干它该干的活的例子把复杂的状态判断交给硬件把精力留给你真正需要关注的业务逻辑上。我做过多年的嵌入式开发每次把这种原来以为只能靠软件硬扛的问题找到硬件级解决方案时都会觉得当初多翻几页参考手册比什么都值。EC11只是其中一个很典型的案例类似的配置还有定时器的霍尔传感器模式、正交解码器接口、捕获比较通道的PWM输入模式都属于这类硬件已经为你准备好了就等你用起来的功能。