
简介STM32F103编码器程序是一份面向嵌入式开发者的工程源码包聚焦电机控制与位置检测场景适合正在学习STM32定时器编码器模式、中断处理及运动控制的初学者或项目参考者。资源共58个文件压缩包约1.36MB核心内容为Keil工程与C语言源码同时包含编译生成的.o目标文件、.crf浏览信息、.d依赖文件、.hex烧录文件以及启动文件等便于对照工程结构理解编码器采集、计数更新与主循环控制逻辑。源码中涉及的GPIO配置、TIM捕获、中断服务函数编写和速度/位置计算思路均有实际代码可参考可帮助读者快速搭建自己的编码器读取框架。该资源已有2025人学习下载适合希望从示例代码入手、减少底层配置踩坑的STM32开发者。 拿到一块 STM32F103 最小系统板再配一个带编码器的直流减速电机想测出电机转速、判断正反转大多数人第一反应是用外部中断读 A 相脉冲。这个方案不是不能用但如果你后面要跑 PID 闭环、要读精确位置、想省下 CPU 时间去处理别的逻辑那就得换思路了。STM32F103 的定时器自带编码器接口模式硬件正交解码、硬件方向判断、硬件计数堪称增量式编码器的最佳搭档。这篇内容不打算讲那些复制粘贴就能跑的零散代码而是把我从选型、接线、配置到实测排错的完整链路拆开说清楚。适合刚接触编码器测速的初学者也适合被方向抖动、溢出跳变折磨过的老手对照排查。核心就一句话编码器应用 80% 的价值不在代码本身而在懂原理、会算数、能排错。1. 硬件准备与接线先分清你手里的编码器是哪一种1.1 增量式与绝对式编码器的本质区别网上搜编码器会看到一堆名字增量式编码器、绝对式编码器、磁编码器、霍尔编码器、光电编码器。做 STM32F103 入门项目时最常见的是增量式编码器通常和直流减速电机集成在一起电机尾部有个小电路板引出 A、B、Z 三根信号线。增量式编码器的输出是一串方波脉冲。电机转起来后A 相和 B 相分别输出占空比约 50% 的方波两路方波相位相差 90 度。正因为有这个 90 度相位差我们才能判断方向正转时 A 相超前 B 相 90 度反转时 B 相超前 A 相 90 度。Z 相是零位脉冲电机轴每转一圈输出一个脉冲用于归零校准。绝对式编码器就不一样了它直接输出当前角度的二进制数值比如 AS5047P 这类磁编码器通过 SPI 接口直接读出 14 位角度数据MT6701 则通过 I2C 读取。好处是断电不丢位置坏处是数据读取要通信协议配合而且这种东西通常直接装在电机尾部用来测关节角度、云台角度不参与测速。增量式编码器做测速和相对位置绝对式编码器做绝对角度两者应用场景不冲突。STM32F103 入门阶段增量式编码器加定时器编码器模式是最经典的组合。1.2 定时器通道映射与电平匹配细节STM32F103 中基本定时器 TIM6、TIM7 不支持编码器模式通用定时器 TIM2、TIM3、TIM4 和高级定时器 TIM1、TIM8 都支持。每个定时器有两路输入通道对应编码器的 A、B 两相。以 TIM3 为例定时器通道1 引脚通道2 引脚编码器接口TIM1PA8PA9支持TIM2PA0PA1支持TIM3PA6PA7支持TIM4PB6PB7支持接线并不复杂但有几个细节容易翻车。如果编码器模块带板载上拉电阻输出是完整的 0~3.3V 或 0~5V 方波直接接 GPIO 浮空输入即可。如果是开漏输出或者你自己搭的电路A、B 相必须外接上拉电阻否则引脚电平不确定读数会乱跳。默认配置里我习惯把 GPIO 设为浮空输入然后看一眼波形再决定要不要改上拉——没有逻辑分析仪的话可以直接把引脚配置为GPIO_Mode_IPU内部上拉也能救一部分开漏输出的场景。供电电平也要注意。5V 供电的编码器模块输出高电平可能是 5V而 STM32F103 是 5V 容忍引脚但安全起见还是建议串个 1kΩ 电阻限流或者用 3.3V 给编码器供电。很多电机编码器模块标称 3.3~5V直接给 3.3V 就行这样信号电平天然匹配不用做转换。还有一个接线上的小建议A、B 信号线尽量短和电机电源线分开走。电机是强干扰源编码器信号线贴近电机线轻则计数抖动重则直接丢脉冲。我在项目里吃过这个亏后来把编码器线绞合起来远离电机线问题立刻减轻不少。2. 原理剖析为什么编码器模式比外部中断好用2.1 正交解码与四倍频的关系很多人对四倍频这个概念一知半解只会背结论把 A、B 两相的上升沿、下降沿都检测一遍一个完整脉冲周期就变成 4 个计数脉冲。但为什么能这么做因为正交信号的特殊性——A、B 两相相差 90 度在一个完整的电平变化周期里A 和 B 各自有上升沿、下降沿组合起来恰好 4 个边沿事件。可以这样想两个人并排走路左脚和右脚交替迈步有固定顺序。A 相是左脚B 相是右脚正走时先左脚后右脚倒走时先右脚后左脚。你每听到一次某一只脚落地就记一格同时根据上一次是哪只脚、这一次是哪只脚判断是在正走还是倒走。一个完整的左右左循环里你记了 4 格这就是四倍频的物理意义。STM32 定时器编码器模式下可以配置三种计数粒度只在 TI1 边沿计数、只在 TI2 边沿计数、或者 TI1 和 TI2 双沿都计数。第三种就是四倍频分辨率最高。实际项目里几乎都用这种模式因为编码器本身线数不高四倍频之后精度才够用。2.2 计数值方向自动判断的底层逻辑正交解码的另一个强大之处在于计数方向由硬件自动判断。随着 A、B 电平状态变化定时器内部的状态机和计数方向逻辑会同步更新 CNT 寄存器正转时 CNT 递增反转时 CNT 递减整个过程不占用 CPU 任何指令周期。这个过程其实基于一个简单的逻辑A、B 两相的电平状态组合共有 4 种00、01、11、10相邻状态之间切换时如果切换顺序是顺时针00→01→11→10说明正转CNT 加 1如果是逆时针CNT 减 1。硬件内部每一拍都在做这个判断而不是等软件来查。这就是为什么编码器模式不会丢脉冲——只要信号到了引脚硬件就处理了不依赖中断响应时间。2.3 外部中断方案差在哪用外部中断读编码器通常的做法是A 相接外部中断引脚在 A 相上升沿中断里读取 B 相电平高说明正转、低说明反转然后软件让某个变量自增或自减。这个方案最直观但有两个硬伤。第一个是中断频率天花板。假设编码器输出轴每秒转 10 圈编码器 11 线、四倍频后每圈 44 个计数事件每秒 440 次中断这还能扛。但如果电机高速运转每秒钟几千上万次中断主程序就被打断了整个控制循环基本废掉。第二个是中断响应延迟导致的边沿丢失。极端情况下A 相上升沿来了但 CPU 正在处理别的中断等它响应完再来读 B 相也许 B 相早就变了方向判断就反了。定时器编码器模式把数脉冲和辨方向这两件事从 CPU 手里剥离出去CPU 只在需要读数时访问一下 CNT 寄存器即可。打个比方外部中断是你亲自蹲在门口数人进出编码器模式是装了一个自动计数器你只需要定期瞄一眼计数器的数字就行。高转速、高精度场景下方案选择基本没有悬念。3. 代码实现TIM3 编码器模式配置与读取3.1 标准库 V3.5 下的初始化流程STM32F103 标准库 V3.5 至今仍是很多老项目的主力这里直接给出可用的初始化代码。如果你用的是 CubeMX HAL 库配置思路完全一致只是 API 不一样。核心是把 TIM3 的通道 1 和通道 2 配置成编码器接口模式GPIO 复用为 TIM3_CH1 / TIM3_CH2void Encoder_Init_TIM3(void) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_ICInitTypeDef TIM_ICInitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // PA6 - TIM3_CH1, PA7 - TIM3_CH2 浮空输入 GPIO_InitStructure.GPIO_Pin GPIO_Pin_6 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IN_FLOATING; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); // 时基配置预分频1ARR设为最大 TIM_TimeBaseStructure.TIM_Period 0xFFFF; TIM_TimeBaseStructure.TIM_Prescaler 0; TIM_TimeBaseStructure.TIM_ClockDivision TIM_CKD_DIV1; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM3, TIM_TimeBaseStructure); // 输入捕获通道1和2都配置为直接输入、无预分频 TIM_ICInitStructure.TIM_Channel TIM_Channel_1; TIM_ICInitStructure.TIM_ICPolarity TIM_ICPolarity_Rising; TIM_ICInitStructure.TIM_ICSelection TIM_ICSelection_DirectTI; TIM_ICInitStructure.TIM_ICPrescaler TIM_ICPSC_DIV1; TIM_ICInitStructure.TIM_ICFilter 0x0F; // 开启滤波抗抖动 TIM_ICInit(TIM3, TIM_ICInitStructure); TIM_ICInitStructure.TIM_Channel TIM_Channel_2; TIM_ICInitStructure.TIM_ICPolarity TIM_ICPolarity_Rising; TIM_ICInitStructure.TIM_ICSelection TIM_ICSelection_DirectTI; TIM_ICInitStructure.TIM_ICPrescaler TIM_ICPSC_DIV1; TIM_ICInitStructure.TIM_ICFilter 0x0F; TIM_ICInit(TIM3, TIM_ICInitStructure); // 配置编码器模式TI1和TI2都计数双沿采样、四倍频 TIM_EncoderInterfaceConfig(TIM3, TIM_EncoderMode_TI1, TIM_EncoderMode_TI2, TIM_ICPolarity_Rising, TIM_ICPolarity_Rising); TIM_SetCounter(TIM3, 0); TIM_Cmd(TIM3, ENABLE); }这段代码有一个关键点TIM_EncoderInterfaceConfig里的最后两个参数是极性极性配置用的是TIM_ICPolarity_Rising意思是双沿都计数。STM32 的编码器接口配置本身会根据编码器输入信号自动调整计数方式不需要手动处理反相。TIM_ICFilter这个字段值得单独说。它设置输入滤波器的采样频率和采样次数0x0F 表示用较高采样次数过滤毛刺。如果信号质量好可以设置小一点如果信号有抖动这个值能有效减少误计数。我一般从 0x0F 开始调波形干净的项目甚至可以降到 0。3.2 读取与清零的封装函数初始化完成后读取计数值只需要一行代码int16_t Read_Encoder_TIM3(void) { return (int16_t)TIM_GetCounter(TIM3); } void Clear_Encoder_TIM3(void) { TIM_SetCounter(TIM3, 0); }注意这里Read_Encoder_TIM3的返回值是int16_t而不是uint16_t。这是个非常关键的细节。16 位的 TIM3 计数器范围是 0~65535如果读成无符号数反转时从 0 递减到 65535就会突然得到一个很大的正数速度计算直接错乱。而强制转换成有符号的int16_t后65535 在补码表示下就是 -10 递减 1 得到 -1逻辑上完全正确。清零的时机很有讲究。如果你在做速度闭环控制通常是在固定采样周期中断里先读值、再立刻清零这样读到的值就是上一个采样周期内的脉冲增量计算速度最方便。如果你要做位置累加就不能随意清零了应该用另一个全局变量做累加或者直接读差值。4. 速度与位置的计算方法4.1 线数、倍频和减速比的关系拿到一个编码器电机先看三个参数编码器线数PPR、减速比、是否带霍尔/编码器反馈。编码器线数指的是电机电机轴每转一圈A 相输出多少个脉冲。常见的 11 线、13 线、16 线指的就是这个。配合四倍频电机轴每转一圈的计数脉冲数等于线数乘以 4。但电机轴和输出轴往往不是 1:1 的中间隔着减速器。比如减速比 30:1 的电机电机轴转 30 圈输出轴才转 1 圈。所以输出轴转一圈对应的计数脉冲数 线数 × 4 × 减速比。以 11 线编码器、减速比 30 为例输出轴一圈计数 11 × 4 × 30 1320这个数字后面做速度和位置换算都要用到我习惯把它定义成一个宏方便换电机时只改一个参数。4.2 M 法测速公式与采样周期选择M 法测速是最常用的编码器测速方案在固定时间 T 内统计计数脉冲数 N则单位时间脉冲频率是 N/T结合上面的每圈计数就能算出转速。公式展开转速(RPM) N × 60 / (线数 × 4 × 减速比 × T)T 是采样周期单位秒。比如 T 0.01s10ms 采样一次N 220线数 11 线减速比 30转速 220 × 60 / (11 × 4 × 30 × 0.01) 100 RPM如果每次采样后把计数器清零N 就是上一次清零到现在的计数值算起来最直观。速度计算通常放在定时器中断里完成比如我用 TIM2 的 10ms 中断做速度采样中断里读 TIM3 计数、清零、算速度、更新全局变量主循环直接用这个变量。采样周期怎么选周期越短速度响应越快但低速时一个周期内可能只有几个脉冲计算误差放大。周期越长低速测速越稳定但动态响应变差。我的经验是普通电机速度控制用 10ms 起步如果发现过零附近速度波动大就适当加大到 20ms 或者提高编码器线数。实时性要求高的项目另说。4.3 位置累计与归零逻辑如果要做位置控制比如让电机转到指定角度就不能用清零后读数的方式了。位置累计的本质是不断累加每个采样周期内的脉冲增量int32_t position 0; void Position_Update_10ms(void) { int16_t delta Read_Encoder_TIM3(); position delta; Clear_Encoder_TIM3(); }这里用int32_t做位置累加可以容纳 16 位计数器的溢出回绕。因为每次读delta都是int16_t即使一个周期内读数从 32767 跳到 -32768int16_t类型的减法结果依然等于真实增量累加到int32_t上不会出错。位置归零则简单粗暴要么把position清零要么额外做个偏移量。实际项目中碰到限位开关、收到归零命令或者系统上电初始化都会执行这个逻辑。5. 实测踩坑抖动、溢出与方向误判的排查链路5.1 转向抖动与临界位置的误计数编码器测速最典型的故障现象是电机明明匀速转动速度值却偶尔跳一下正负交替好像电机在抽搐。初次遇到这个问题大多数人先怀疑程序逻辑但我建议先看波形。用示波器或逻辑分析仪抓 A、B 相的波形把时间轴拉大重点看信号在跳变沿附近有没有毛刺、回弹。电机机械振动、电刷接触不良有刷电机常见、编码器安装偏心都会造成信号在边界处来回抖动。信号每抖动一次硬件就可能多计一个脉冲。应对手段分两层。硬件层面编码器输出加 RC 滤波A、B 线分别对地接一个 104 电容配合 1kΩ 电阻构成低通滤波同时检查编码器与电机轴的安装紧固度安装偏心是低速抖动的大户。软件层面把TIM_ICFilter调大让输入信号必须连续稳定多个采样周期才被识别为有效跳变。这个值不是越大越好过大会把真实的窄脉冲也滤掉尤其是高速旋转时需要平衡取舍。5.2 16 位计数器溢出引发的速度突变第二个高频问题速度计算偶尔蹦出一个巨大值正负都有像是计算逻辑抽风。本质上这是 16 位计数器溢出没有处理好。举一个典型的错误写法uint16_t last_cnt 0; uint16_t now_cnt TIM_GetCounter(TIM3); int16_t speed now_cnt - last_cnt; // 错误示例如果正转时last_cnt 65530now_cnt 5无符号减法得到 11不对uint16_t的 5 - 65530 会回绕成 11看起来是对的。但如果反转时last_cnt 5now_cnt 65530无符号减法得到 65525一个巨大的正数速度突变就出现了。正确写法是用int16_t读值然后直接做有符号减法int16_t delta (int16_t)TIM_GetCounter(TIM3) - last_cnt; last_cnt (int16_t)TIM_GetCounter(TIM3);这样正转时int16_t的 5 减去 -665530 的补码表示得到 11反转时 -6 减去 5 得到 -11结果完全正确。这种利用补码回绕特性的写法是处理 16 位计数器溢出的标准玩法前提是一个采样周期内的脉冲增量不能超过 32767否则有符号数也救不了。5.3 上电零漂和初始位置不确定还有一个不算 bug 但很影响体验的问题上电瞬间编码器读数不是 0或者轻微晃动电机后读数漂了。原因是 CNT 寄存器上电默认值是随机的或者外部干扰在 GPIO 上产生了误触发。处理办法上电后先执行一次TIM_SetCounter(TIM3, 0)然后延时几百毫秒等电源和编码器供电稳定后再清零一次再开始采样。如果对位置初始值有要求上电后必须让电机找零位碰到限位或 Z 相脉冲才能开始位置控制不能靠清零假装在原点。找零位也可以利用 Z 相脉冲。编码器的 Z 相每圈一个脉冲把它接到一个外部中断引脚在中断里记录当前位置就能实现每圈一次的绝对参考点校准——电机位置漂移后再转一圈就能自校正。这个我后来在云台项目里用了效果相当稳健。5.4 排查链路口诀最后把我排查编码器问题的固定顺序分享出来遇到问题照着走基本都能定位到根因。第一步看接线供电电压对不对、上拉有没有、信号线有没有接反A/B 对调会导致方向反但对调后读数正常说明接线问题第二步看波形毛刺、抖动、电平幅值是否达标第三步看寄存器初始化后读一次 CNT手动慢慢转电机看数值是否连续增减方向是否正确第四步改配置先调滤波再调采样周期不要一上来就改算法第五步才怀疑单片机本身。按这个顺序排查绝大多数编码器问题都能在半小时内找到答案。这套配置方法我用在多款电机驱动项目上从简单的测速到双轮差速小车、云台角度闭环都是同一个套路。最大的体会是编码器读数看似简单但真正折腾人的是细节——一个int16_t和uint16_t的区别一个滤波参数的取舍就可能决定整个系统稳不稳定。建议新项目从标准的四倍频 10ms 采样 int16_t差值方案起步跑通之后再按需优化少走弯路。本文还有配套的精品资源点击获取