资讯详情

STM32旋转编码器数值设定:定时器编码器模式与多级步进切换实现

📅 2026/9/14 4:24:04 | 华诺云谱 👁 阅读
STM32旋转编码器数值设定:定时器编码器模式与多级步进切换实现
简介面向STM32开发者的旋转编码器数值获取代码工程解决旋转编码器在限幅范围内的多档位步进调整问题支持按十位、百位、千位、万位为单位调节数值适用于工业控制参数设置、家电HMI交互等需要精确数值输入的场合。压缩包共1014个文件以C源码597个和头文件287个为主同时包含STM32CubeMX工程.ioc、Keil工程.uvprojx、CMSIS数学库以及HMI固件.bin整体仅8.7MB目录结构清晰便于直接移植参考。已有205人浏览学习。代码实现了编码器正反转判断、上下限检查和多档位步进更新并附带HMI交互相关文件与说明文档可帮助开发者理解从脉冲采集、去抖到数值显示的全链路逻辑快速套用到实际产品按键调节与参数设置中适合需要集成旋转编码器功能的中高级嵌入式工程师。1. 用旋转编码器在 STM32 单片机上做数值设定为什么不是简单读 GPIO用旋转编码器在 STM32 单片机上做数值设定第一直觉是把 A、B 两相接成输入在中断里写状态机。这么做确实能跑但当你要做的是十位、百位、千位、万位这种跨度很大的步进调整时真正的难点反而不是解码而是数值边界和单位切换。一个带上下限的数值设定旋钮旋转一格可能只加 1也可能加 10、100、1000、10000操作者希望快速到达目标值又不想每次越界后重新转回来。这篇直接给出基于 STM32 定时器编码器模式的实现代码包含正交信号读取、上下限钳位、单位切换和快速旋转时不丢步的细节适合做仪器仪表、电源调参、HMI 参数设定这类需要手感的应用。2. STM32 读旋转编码器的两种方式硬件编码器模式和 GPIO 状态机2.1 定时器编码器模式为什么省事STM32 的通用定时器内部自带正交解码器。把旋转编码器的 A、B 两相接到定时器的 CH1、CH2 输入配置为编码器模式后计数器会随着旋转自动加减方向由硬件判断不需要在中断里逐边沿分析相位。这种方案最大的好处是“事件发生时不打扰 CPU”。你把 TIMx-CNT 就当做一个带方向的计数器主循环随时读读到多少就是当前相对位置。旋转速度快、脉冲多的时候CNT 会累加不会因为中断响应不及时而丢步。和 GPIO 中断方案相比这是首选。具体到引脚以常见的 STM32F103 为例TIM2 的 CH1 在 PA0CH2 在 PA1正好能接一个旋转编码器。其他型号的定时器通道映射差异较大用 CubeMX 里的 “Encoder Mode” 直接选就能看到可用的引脚组合。2.2 GPIO 中断状态机与查表法如果定时器资源被占满或者用的不是 STM32而是 51、STC 这类没有正交解码外设的单片机那就只能用 GPIO 中断和状态机。原理是记录上一次 A、B 的电平组合和当前组合通过查表得到旋转方向。A、B 两相按 bit1A、bit0B 编码得到 0~3 四个状态。旋转一圈的状态序列是 0-1-3-2-0 或反向。用上一个状态做高两位、当前状态做低两位合成一个 0~15 的索引查数组就可以返回 1、-1 或 0static const int8_t enc_table[16] { 0, -1, 1, 0, 1, 0, 0, -1, -1, 0, 0, 1, 0, 1, -1, 0 }; int8_t encoder_decode(uint8_t old_state, uint8_t new_state) { return enc_table[(old_state 2) | new_state]; }表格里的 1、-1 只代表一个步进方向。实际调试时如果发现方向反了把表中的正负号整体对调或者把 A、B 两根线交换效果一样。这个表来自经典的 Arduino 旋转编码器库状态覆盖了完整四分之一周期不复位就查表抖动产生的非法跳变多数会返回 0。2.3 硬件接线与滤波旋转编码器虽然有机械触点但输出的是正交信号不是普通的按键电平接法有一点讲究。常见编码器引出 5 个针脚ACLK、BDT、SW按键、电源和地。和 STM32 连接时用内部上拉即可因为大多数模块上的 A、B、SW 都是开漏输出靠外部上拉到 VCC。编码器引脚STM32 引脚说明ACLKTIM2_CH1 / PA0正交信号 A 相BDTTIM2_CH2 / PA1正交信号 B 相SW任意 GPIO如 PB0单位切换按键内部上拉VCC3.3V模块供电GNDGND公共地如果用的模块不是 3.3V 而是 5V 供电要确认 STM32 引脚能容忍 5V否则需要加电平转换或分压电阻。机械编码器的抖动主要在触点闭合瞬间常见做法是在定时器输入捕获的滤波寄存器里配置采样窗口能滤掉大部分毛刺比外接大电容更可控。外接电容不建议超过 100nF太大会让 A、B 相位变形计数器反而会误判。3. 定时器编码器模式下读取数值最小可运行代码3.1 用 CubeMX 配置 TIM2 编码器模式在新版 STM32CubeMX 里选择 TIM2把 Clock Source 设为 Encoder Mode模式选 “Encoder Mode TI1 and TI2”Counter Period 设为 0xFFFF。这样 A 相和 B 相的上升沿和下降沿都会计数四倍频旋转一圈的计数值是编码器脉冲数的 4 倍。输入滤波建议设在 0x0F 左右尤其编码器线比较长或环境有电机干扰时。打开定时器时钟但不需要使能更新中断因为我们只读 CNT不依赖中断。HAL 初始化代码大致是这样TIM_Encoder_InitTypeDef enc {0}; TIM_MasterConfigTypeDef masterCfg {0}; __HAL_RCC_TIM2_CLK_ENABLE(); htim2.Instance TIM2; htim2.Init.Prescaler 0; htim2.Init.CounterMode TIM_COUNTERMODE_UP; htim2.Init.Period 0xFFFF; htim2.Init.ClockDivision TIM_CLOCKDIVISION_DIV1; enc.EncoderMode TIM_ENCODERMODE_TI12; enc.IC1Polarity TIM_ICPOLARITY_RISING; enc.IC2Polarity TIM_ICPOLARITY_RISING; enc.IC1Filter 0x0F; enc.IC2Filter 0x0F; HAL_TIM_Encoder_Init(htim2, enc); masterCfg.MasterOutputTrigger TIM_TRGO_RESET; HAL_TIMEx_MasterConfigSynchronization(htim2, masterCfg);这段代码里的TIM_ENCODERMODE_TI12表示 TI1 和 TI2 双沿计数如果编码器每圈脉冲较少想降低计数分辨率可以改成TIM_ENCODERMODE_TI1只在 A 相边沿计数分辨率变成二分之一。IC1Polarity和IC2Polarity都设为上升沿正好匹配标准正交信号。滤波参数如果设太大会引入几十微秒的延迟对低速手动旋转没影响但高速旋转可能跟不上。3.2 数值更新函数与上下限钳位编码器初始化完成后主循环只需要周期性地读 CNT计算和上次读数的差值然后按当前单位更新数值。下面是一个直接可用的实现包含上下限限制static int32_t g_value 0; static int32_t g_min 0; static int32_t g_max 999999; static int16_t g_last_count 0; static uint8_t g_step_index 1; // 1 十位 static const int32_t g_step_table[5] {1, 10, 100, 1000, 10000}; void encoder_update(void) { int16_t now (int16_t)TIM2-CNT; int16_t diff (int16_t)(now - g_last_count); g_last_count now; if (diff 0) return; int32_t step g_step_table[g_step_index]; int64_t next (int64_t)g_value (int64_t)diff * step; if (next g_max) next g_max; if (next g_min) next g_min; g_value (int32_t)next; }diff是两次读取之间编码器走过的步数。比如快速转了一圈diff可能是 20数值一次就增加20 * step。这里把TIM2-CNT强转成int16_t是故意的目的让计数器回绕时差值仍然正确具体原理在下一节讲。int64_t是防止当前值接近 int32 上限时diff * step溢出。如果工程里的数值范围能保证不超过 2^31也可以直接 int32但我一般保留 64 位中间量。3.3 快速转动时的溢出还原用带符号差值16 位定时器的 CNT 取值范围是 0~65535正转超过 65535 会回绕到 0反转会从 0 绕回 65535。如果按无符号数相减会出现“正转一格得到 -65535”的荒谬结果。解决方法是把 CNT 强转成int16_t。从 65535 读成 -1从 0 读成 0从 65534 读成 -2。两次读数差按带符号处理只要旋转步数不超过 32767差值就是真实物理方向。手动旋转编码器一次采样周期内不可能产生三万多个脉冲所以这个假设成立。这一招比判断溢出标志更简洁也是很多编码器代码的核心技巧。4. 十位、百位、千位、万位步进切换的实现4.1 用 10 的幂表而不是 pow步进单位是十位、百位、千位、万位对应 10^1、10^2、10^3、10^4。最直接的做法是用一个索引指向查表数组而不是在运行时调pow。C 语言里的pow返回浮点数在单片机里既慢又可能产生舍入误差而且浪费代码空间。步进索引单位步进值旋转一格变化1十位10±102百位100±1003千位1000±10004万位10000±10000初始化时g_step_index 1符合标题“从十位开始”。如果以后需要个位把表头和索引 0 加进去即可。切换函数只做索引自增和模回绕void encoder_step_unit_next(void) { g_step_index; if (g_step_index 4) { g_step_index 1; } }这个函数只负责切换单位不改变g_value。也就是说当前值如果是 12345单位从十位切到百位后数值依然是 12345只是下一次旋转一格会变成 12445 而不是 12355。这符合大多数仪表盘设定的习惯调节单位决定增量而不是改变已有值。4.2 单位切换按键与消抖单位切换通常用编码器自带的 SW 按键。这个按键按下后接地读取时上拉输入为 0。机械按键需要消抖我不建议用HAL_Delay阻塞等待因为主循环可能还在做显示和通信阻塞会让编码器读数延时。用非阻塞扫描更可靠#define KEY_PORT GPIOB #define KEY_PIN GPIO_PIN_0 void key_scan_nonblock(uint32_t tick) { static uint8_t key_prev 1; static uint32_t last_stable_tick 0; static uint8_t pending 0; uint8_t key_level (HAL_GPIO_ReadPin(KEY_PORT, KEY_PIN) GPIO_PIN_RESET) ? 0 : 1; if (key_level ! key_prev) { key_prev key_level; last_stable_tick tick; pending 1; } else if (pending (tick - last_stable_tick 20)) { pending 0; if (key_level 0) { encoder_step_unit_next(); } } }tick用HAL_GetTick()传入主循环每秒调用这个函数几十次即可。逻辑是电平变化后开始计时20ms 后如果电平稳定才认为是一次有效动作。这里按键按下和松开都会触发一次稳定检测但只在下降沿调用切换函数所以一次按压只切一个单位。如果想支持长按连续切换可以把 20ms 窗口缩短并在持续按住时逐次递增。4.3 越界时的两种处理策略前面代码用的直接钳位next超过上限就强制等于上限。这个实现最简单但有一个细节容易忽略如果当前值是 95000上限是 100000步进是 10000正转一格next是 105000钳位到 100000。此时反向转一格数值变成 90000而不是 99000。也就是说接近上限后反向旋转的第一格会“跳”得比较多。这是可以接受的因为钳位时多余步数被丢弃了。如果希望保留越界量例如从 95000 正转一格到 100000剩余 5000 留作反向时的余量就需要一个余数累加器。static int32_t g_carry 0; void encoder_update_with_carry(void) { int16_t now (int16_t)TIM2-CNT; int16_t diff (int16_t)(now - g_last_count); g_last_count now; if (diff 0) return; int64_t next (int64_t)g_value (int64_t)diff * g_step_table[g_step_index]; if (next g_max) { g_carry (int32_t)(next - g_max); next g_max; } else if (next g_min) { g_carry (int32_t)(next - g_min); next g_min; } else { g_carry 0; } g_value (int32_t)next; }这里g_carry保存了被钳位掉的部分方向信息也保留了。但注意这只对一次采样内的越界有意义如果连续多次都越界g_carry应该累加而不是覆盖否则逻辑又会乱。实际产品里我大多只用直接钳位因为机械旋钮旋转过界后操作者会自然反方向转过同样的角度丢掉的步数不会被感知到。只有做伺服对位或高精度输入时才需要余数保留。5. 调试与验证方向反、丢步、回绕的判断方法5.1 串口打印把数值和方向暴露出来编码器的问题往往不是代码逻辑而是方向、抖动和计数不连续。调试第一步是串口打印把diff和当前值打出来。在encoder_update里临时加一行printf(diff%d step%ld value%ld\r\n, diff, (long)g_step_table[g_step_index], (long)g_value);旋转编码器一格如果看到diff出现 4 或 -4这是正常的因为双沿四倍频把一格拆成了 4 个边沿如果看到diff在同一个方向总是忽大忽小优先查滤波和接线。方向反了就交换 A、B 输入或者调换状态表里的正负关系。5.2 用逻辑分析仪确认 A/B 相位手头有逻辑分析仪的话把 A、B 分别接两个通道缓慢旋转。标准正交信号顺时针一转A 相上升沿时 B 相是高电平还是低电平直接决定了计数方向。如果看到 A 或 B 上有明显的不规则窄脉冲那是机械抖动STM32 的输入滤波应该能挡住。滤波后仍有毛刺就在编码器电源端并一个 10~100nF 电容到地。快速旋转验证丢步的方法是把编码器从一端转到另一端方向不要变然后用串口观察diff是否出现与旋转方向相反的数值。如果出现就是滤波参数太大或者两次采样间隔太长导致 CNT 的差值被误解释为反向回绕。5.3 回绕边界测试和显示单位联动最后建议做一次边界回绕测试初始化g_min0g_max1000单位设为百位。正转 20 格数值到达 1000 后应该纹丝不动再反转 1 格应该变成 900。这一步验证的是钳位逻辑不是编码器解码。如果接了 OLED 或数码管单位切换时最好把g_step_index显示出来否则操作者很难判断当前步进是十位还是百位。这个显示逻辑不在编码器代码里但它是“按单位步进调整数值”这个功能能否被用户接受的关键。最后再看一眼TIM2-CNT从 65535 回绕到 0 时的打印diff仍然保持 1 或 -1而不是出现 65535 或 -65535就说明整个读取链路已经稳定了。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。