
简介面向嵌入式开发者的 STM32F103C8T6 正交编码器角度测量工程包覆盖从硬件连接到软件换算的完整闭环适用于电机闭环控制、云台角度反馈、旋转平台定位等需要实时角度采集的场景。资源以 83 个文件封装完整 Keil MDK 工程包含 36 个头文件与 35 个 C 源文件、工程配置、驱动源码及可直接烧录的 HEX 文件压缩包大小仅 328KB目录结构清晰便于二次开发。代码基于标准外设库已适配 2500 线增量式编码器硬件连接明确PB6 接 A 相、PB7 接 B 相、PA1 接 Z 相通过 TIM4 计数配合 Z 相清零实现方向判别与圈数累计角度换算逻辑直观main.c 中 count TIM4-CNT * 360 / 10000 的公式可随编码器线数调整便于实现 360° 整圈映射。ENCODE 文件夹封装了编码器专用驱动并集成系统初始化、延时、串口调试支持方便验证。已有 43 人学习下载适合需要快速搭建角度测量功能的嵌入式开发者参考。 做电机控制、机械臂、转台这类项目最绕不开的就是角度测量。以前总习惯用模拟电位器后来发现精度、线性度、耐久性都跟不上这才认真把手头的正交编码器用了起来。把STM32F103C8T6最小系统板和ABZ三相输出的增量编码器搭配在一起做成一套带360°校准的角度测量工程整个过程不算复杂但细节不少。这篇把我从接线到代码再到校准的完整流程整理出来讲清楚每个环节为什么这么做踩过的坑也一并列出来希望对正在做类似项目的朋友有参考价值。1. 项目概述与整体设计思路1.1 为什么选择正交编码器作为角度传感器做角度测量市面上方案很多电位器、霍尔角度传感器、磁编码器、光电编码器。电位器最便宜但接触式测量存在磨损寿命短温漂明显而且精度受供电电压波动影响很大。磁编码器虽然是非接触的但成本相对高而且对安装位置、磁铁一致性有要求。光电编码器里的增量式正交编码器恰好是成本和性能比较折中的一个选择。正交编码器输出两路相位差90°的方波信号分别是A相和B相。转轴每转过一个角度增量A、B相就会各自输出一个脉冲两者之间的相位关系能直接反映旋转方向。这种测量方式不依赖绝对位置而是通过累计脉冲数来计算相对角度所以又叫增量式编码器。它最核心的优势有三点。第一非接触式测量没有机械磨损寿命长。第二精度稳定只要编码器线数和机械安装没问题读数不会因为环境温度或电压波动而漂移。第三配合STM32的硬件编码器接口可以直接由定时器硬件完成脉冲计数和方向判断几乎不占用CPU资源非常省心。1.2 STM32F103C8T6在这个项目中的角色STM32F103C8T6虽然是一颗老芯片但性价比极高几块钱就能买到而且生态非常成熟。关键的是它的定时器外设自带编码器接口模式这是很多同价位MCU不具备的。这颗芯片有TIM1、TIM2、TIM3、TIM4四个定时器支持编码器模式不同封装下TIM1的引脚映射有差异。编码器模式下定时器硬件会自动检测A、B两相的边沿变化根据相位关系决定计数器是加1还是减1。也就是说你完全不需要在中断里做正交解码主循环只需要定期读取计数器的值然后换算成角度就行。对于做原型验证、小批量设备、教学实验这类场景F103C8T6的64KB Flash和20KB RAM完全够用跑一个编码器读取任务加上简单的显示逻辑、通信逻辑资源绰绰有余。而且现在国产替代芯片非常多直接替换也不影响代码供应链方面的压力小很多。1.3 本项目包含哪些核心功能模块这个工程包的核心功能模块可以拆成四块正交编码器信号采集通过STM32定时器的编码器模式读取A/B相脉冲自动完成方向判断和计数。ABZ三相接口处理A/B相负责增量计数Z相负责零位信号识别用于建立绝对参考零点。360°角度换算将原始脉冲计数转换为0°到360°范围内的角度值支持正反转。360°校准机制包括基于Z相脉冲的硬件归零和基于软件按键的任意位置置零解决上电后初始位置不确定的问题。这四块加在一起就能实现一个完整的、带参考零点的角度测量系统。实际测试中我用一只500线的增量编码器在四倍频模式下角度分辨率能到0.18°对于大多数工业测控和机器人应用来说精度足够。2. 硬件设计与ABZ三相接口接线2.1 硬件清单与选型建议做这个项目需要的硬件不多我列一下我实际使用的清单硬件型号/规格数量说明主控板STM32F103C8T6最小系统板1蓝色Pill板或任意品牌最小系统板均可编码器增量式光电编码器 500线 ABZ输出1常见的是6线或8线输出含ABZ信号和电源联轴器内径按电机轴和编码器轴径选择1建议用弹性联轴器能吸收同心度偏差显示模块OLED 0.96寸 I2C接口1选配调试时很方便电源5V/3.3V适配器或USB供电1大多数编码器是5V供电注意电平匹配手头工具杜邦线、面包板、万用表、示波器或逻辑分析仪若干示波器/逻辑分析仪建议要有排查信号问题很好用编码器选型方面500线是一个非常均衡的选择。线数越高每圈脉冲越多分辨率越高但价格也越贵而且对安装同心度越敏感。对于做原型验证200线到1000线都合适我手里这只500线是之前做电机闭环剩下的一直在用。需要注意的一点是编码器工作电压。很多工业编码器是5V供电输出信号电平也是5V。虽然STM32F103C8T6的大部分引脚是5V容忍的具体要看数据手册的引脚说明但最稳妥的做法是加一个电平转换电路或者在编码器输出和MCU引脚之间串一个1k电阻分压避免长期工作在极限电平下。2.2 ABZ三相接口的接线细节这里详细说明一下接线方式。增量编码器的ABZ三相信号中A相和B相是相位差90°的正交脉冲Z相是每旋转一圈输出一个脉冲的零位信号。下面是我实际用到的接线表编码器信号编码器线色以我手头为例接到STM32引脚说明A相黄色PA6TIM3_CH1正交信号输入B相绿色PA7TIM3_CH2正交信号输入Z相蓝色PB0零位脉冲配外部中断VCC红色5V或3.3V根据编码器工作电压GND黑色GND共地必须接好屏蔽层裸线GND可选电机环境强烈建议接选择PA6/PA7是因为它们刚好是TIM3的通道1和通道2而TIM3是F103上最常用的通用定时器之一编码器模式很好配引脚编号也方便在外面板子上布线。有一个非常关键的接线细节如果编码器是集电极开路Open Collector输出上拉电阻是必不可少的。很多便宜的编码器模块内部已经集成上拉了但如果直接买裸编码器A、B、Z三根信号线都需要外部上拉到3.3V或5V上拉电阻10k左右。不加上拉的话信号会不稳定计数器会出现大量误计数现象就是静止时角度读数乱跳。这个坑我踩过后来用万用表量信号电平才发现是上拉的问题。2.3 定时器外设的选择逻辑与优势F103C8T6有多个定时器但不是每个定时器在所有封装下都能很方便地引出编码器输入引脚。编码器模式下定时器需要同时使用CH1和CH2两个通道所以选择定时器时首先要确认封装上对应的引脚是否方便接入。C8T6是LQFP48封装我整理了一下可用的情况定时器编码器通道引脚优势备注TIM1PA8CH1、PA9CH2高级定时器功能最强但PA8/PA9在这个封装上引出来了也可以使用TIM2PA0CH1、PA1CH2通用定时器PA0/PA1可用但有时被用作其他功能TIM3PA6CH1、PA7CH2通用定时器引脚布线最方便我最终选择TIM4PB6CH1、PB7CH2通用定时器如果PA6/PA7被占用备用我选择TIM3PA6/PA7主要考虑是PA6和PA7在最小系统板上引出的排针位置很规整杜邦线接起来不容易跟旁边的电源、SWD调试引脚互相干扰。而且TIM3是通用定时器配置编码器模式时不需要像TIM1那样额外处理刹车、互补输出这些高级功能逻辑上更简单。另外使用硬件编码器接口而不是外部中断计数最大的优势就是计数不丢脉冲。因为编码器工作在高速旋转时脉冲频率很高如果用外部中断软件计数很容易因为中断处理不及时导致漏计。而硬件编码器接口是定时器内部的逻辑电路直接响应边沿不经过CPU所以不会丢。3. 软件实现编码器读取与角度换算3.1 CubeMX初始化配置整个软件实现是基于STM32CubeMX生成的HAL库工程这样初始化代码可以自动生成不用自己手动写寄存器。手动配也可以但CubeMX能减少很多低级错误我建议新手直接用CubeMX。CubeMX的配置步骤大致如下创建一个新工程选择芯片STM32F103C8Tx。在Pinout视图里把PA6和PA7设置为TIM3的CH1和CH2。点击TIM3在Slave Mode一栏选择Encoder Mode这里有三选项Encoder Mode TI1只在A相边沿计数二倍频。Encoder Mode TI2只在B相边沿计数二倍频。Encoder Mode TI1 and TI2A、B相边沿都计数四倍频。 我选择的是TI1 and TI2也就是四倍频角度分辨率最高。在Parameter Settings里把Prescaler设为0不分频保证编码器信号直接进计数器Counter Period设为0xFFFF16位计数器最大值65535。输入滤波Input Filter建议设置成一个非零值比如0x0A大约滤除43ns以内的毛刺。这个滤波对工业环境下的信号抖动有抑制作用代价是响应的最高频率会降低。500线编码器配合四倍频在2000rpm时脉冲频率约为66.7kHz这个滤波值可以正常工作。编码器模式下counter方向由硬件自动管理向上计数或向下计数取决于旋转方向。把PB0配置为GPIO外部中断EXTI0用于Z相零位信号触发。生成代码后在main函数里启动编码器HAL_TIM_Encoder_Start(htim3, TIM_CHANNEL_ALL); HAL_TIM_Base_Start_IT(htim3); // 如果想用定时器溢出中断可以开启注意编码器模式本身不需要开启溢出中断但如果要做多圈计数就需要处理16位计数器向上溢出和向下溢出的事件。这个后面会专门讲。3.2 核心代码读取编码器计数与方向判断启动编码器之后主循环里读取计数器值非常简单#include main.h extern TIM_HandleTypeDef htim3; // 读取当前编码器原始累计值 int16_t get_encoder_raw_count(void) { return (int16_t)__HAL_TIM_GET_COUNTER(htim3); }这里使用int16_t强转非常关键。因为计数器寄存器是16位取值范围0到65535如果按无符号数读正向计数到65535后回0反向计数到0后回65535这样角度换算会很麻烦。而强转成int16_t后计数器的值范围就变成了-32768到32767从0正向计到32767再继续硬件计数器其实是从32767到32768但int16_t读出来变成-32768从这个角度看数值是连续递增的。这样做的好处是只要单圈脉冲数不超过32767多圈计数和方向判断就变得非常简单直接读int16_t的差值即可判断方向和圈数。判断旋转方向我采用差分方式也就是比较前后两次读取的计数值int16_t last_count 0; int8_t get_rotation_direction(void) { int16_t current_count get_encoder_raw_count(); int16_t delta current_count - last_count; last_count current_count; if (delta 0) return 1; // 正转 if (delta 0) return -1; // 反转 return 0; // 静止 }这段代码在编码器没有转的时候delta为0方向返回0。正常旋转时delta的正负就对应了旋转方向。要注意的是如果编码器转动速度非常快两次读取之间的delta可能会超过单圈脉冲数的一半那就无法判断方向了这种情况说明采样频率太低了需要提高读取频率。3.3 角度换算的公式与溢出处理角度换算的逻辑是整个工程包里最核心的部分。实际生产中编码器通常不是恰好从我们想要的0°位置开始安装的所以需要经过校准。假设已经做过零点校准把零位偏移存在全局变量里角度计算的流程如下// 编码器线数 #define ENCODER_LINES 500 // 四倍频后每圈脉冲数 #define ENCODER_PULSES_PER_REV (ENCODER_LINES * 4) // 零点偏移校准后保存的计数器值 int16_t zero_offset 0; // 获取校准后的单圈角度范围0~359.99° float get_angle_degrees(void) { int16_t raw (int16_t)__HAL_TIM_GET_COUNTER(htim3); int32_t adjusted raw - zero_offset; // 映射到单圈脉冲数范围内 int32_t single_turn adjusted % ENCODER_PULSES_PER_REV; if (single_turn 0) { single_turn ENCODER_PULSES_PER_REV; } // 脉冲数换算成角度 float angle (float)single_turn * 360.0f / (float)ENCODER_PULSES_PER_REV; return angle; }这里面的几个关键点adjusted是原始计数减去零点偏移得到的是相对于校准位置的角度变化量。对ENCODER_PULSES_PER_REV取模是为了把结果限制在单圈范围内防止读到多圈累计值。当adjusted是负数时取模结果也是负数需要加上脉冲数把它修正为正值这样才能得到0°到360°范围的角度。四倍频后500线编码器每圈脉冲数为2000角度分辨率就是360°/2000 0.18°四舍五入0.18°。如果你的编码器是1000线分辨率就是0.09°。还有一个多圈计数的问题。虽然角度测量本身只关心单圈但在某些应用里需要知道总共转了几圈。F103的定时器计数器是16位的四倍频2000脉冲/圈时16位计数器最多能记录约32圈。如果转的圈数更多就得用溢出中断配合软件扩展计数位宽或者改用32位定时器。F103的TIM2和TIM5是32位定时器理论上可以记录约107万圈足以满足绝大多数应用但使用32位定时器在编码器模式下的配置和读取方式与16位一致只是计数器寄存器读取时要用32位变量取模运算用无符号32位整型处理时要额外小心一下。4. ABZ相位与360°校准的具体实现4.1 基于Z相的硬件归零增量编码器有一个天然缺陷它输出的只是相对位置上电那一刻你不知道转轴到底处于什么绝对角度。Z相就是为了解决这个问题而存在的——编码器每旋转一圈Z相会输出一个脉冲代表一个机械参考位置。利用Z相归零的思路很简单当Z相脉冲到来时把计数器清零或者把零点标志记录下来这样就在相对坐标里建立了一个绝对参考点。实际工程中一般会在上电后让电机或转轴低速转一圈等检测到Z相脉冲后就完成了初始定位。我用的方案是把Z相接到PB0配置为外部中断上升沿触发在中断处理里把计数器清零并设置一个标志位表示归零完成volatile uint8_t z_capture_flag 0; void EXTI0_IRQHandler(void) { if (__HAL_GPIO_EXTI_GET_IT(GPIO_PIN_0) ! RESET) { // Z相脉冲到达将计数器清零建立绝对零点 __HAL_TIM_SET_COUNTER(htim3, 0); z_capture_flag 1; // 清除中断标志 __HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_0); } }这个方案适合对零点位置有确定性要求的场景比如转台、机械臂关节。需要注意的是Z相脉冲通常会叠加一些毛刺噪声尤其编码器线缆靠近电机驱动线时更明显。所以要么开启GPIO输入滤波要么在中断里做软件去抖比如连续确认两次高电平否则可能误触发归零。4.2 任意位置软件置零Z相归零虽然可靠但它只能定位到编码器出厂设定的机械零点。实际使用中我们往往希望把某个特定位置定义为0°比如机械臂的起始姿态、转台的初始限位位置。这种情况下就需要软件置零了。软件置零的实现是在系统运行过程中当收到一个校准命令按键触发、串口指令、上位机指令都可以把那一刻的计数器值保存为新的零点偏移// 软件校准把当前位置设为0° void software_calibrate_zero(void) { zero_offset (int16_t)__HAL_TIM_GET_COUNTER(htim3); z_capture_flag 1; // 标记已校准 }这里和Z相归零最大的区别是Z相归零直接把计数器清零而软件置零只是记录偏移量。为什么不在软件置零时直接清零计数器因为在有些应用场景中你需要知道从开机以来总共转了多少圈清掉计数器会丢失累计数据。用偏移量方式角度计算和累计圈数计算互不干扰更灵活。校准完成后可以用串口或OLED把当前角度打印出来确认一下看角度是否稳定在0.0°附近。如果读数有稳定的偏差比如保持在0.5°左右说明安装角度有初始偏差重新校准一次即可。4.3 校准流程的完整设计一套完整的校准流程我建议这样设计上电后系统进入未校准状态此时显示屏或上位机提示需要校准。操作者手动将设备旋转到机械零点或期望的零点位置。按下校准按键或发送校准指令系统执行software_calibrate_zero()。系统读取当前角度并显示确认角度为0.0°。如果使用Z相归零则程序自动启动找Z流程驱动转轴低速旋转等待Z相中断一旦捕获Z相脉冲即完成初始化。这个流程听起来简单但有一个容易忽略的问题校准完成后如果转动方向与编码器安装方向相反角度读数会从0°向359°递减。这会让用户觉得逻辑反了。解决办法有两个一是检查硬件接线把A、B相对调一下旋转方向就反过来二是在软件里做方向取反float get_angle_degrees_reversed(void) { float angle get_angle_degrees(); angle 360.0f - angle; if (angle 360.0f) angle - 360.0f; return angle; }这个函数在angle等于0°时结果是360°再用if修正为0°这样保证了角度范围始终在0到360之间。5. 实际调试过程中的常见问题与排查技巧5.1 计数器乱跳、读数抖动这是我最常遇到的问题。现象是转轴静止但OLED或串口输出的角度值不断在几度甚至几十度范围内乱跳。排查顺序我一般是这样的检查项工具可能原因解决方案信号电平万用表编码器OC输出没有上拉加10k上拉到3.3V或5V供电万用表编码器电源纹波大在编码器电源脚并一个10uF电解电容接线目视/通断测试杜邦线接触不良重新插拔或换线信号毛刺示波器/逻辑分析仪地线环路或串扰检查共地缩短信号线远离电机线单片机输入滤波软件配置滤波强度不够调大TIM3的IC Filter值这里特别要强调滤波参数的选择。TIM3编码器模式下IC1/IC2 Filter的数值越大抗干扰能力越强但能处理的最高输入频率就越低。对于500线编码器四倍频最高转速对应的脉冲频率通常不超过100kHz只要滤波值导致的脉冲最小宽度大于信号周期即可。我实测下来Filter设成0x0A约为系统时钟频率72MHz10个时钟周期的滤波采样窗口大约139ns这个值在一般环境下表现均衡。5.2 角度跳变到0°附近的尖峰另外一个常见现象是转轴匀速旋转时角度显示在接近360°和0°的边界会出现跳变。比如从359.8°直接跳到0.2°、又跳回来。这个问题的根源在于角度换算做了模运算。在边界附近本来累积值有一点微小抖动比如在359.9°和0.1°之间小幅振荡显示出来自然就在358~2°之间来回跳。这是数字测量的正常现象但如果你希望显示更平滑可以做一个小处理判断相邻两次角度之差如果大于某个阈值比如180°就认为跨过了零点做一个平滑过渡处理。不过我要说的是对于大多数应用来说这并不影响真实角度值只是显示层面看着不舒服。我一般会在显示端做一阶低通滤波或者直接忽略这个问题因为真正要拿角度做控制时反馈值本身是正确的。5.3 计数器溢出与多圈累计问题F103C8T6的TIM3是16位定时器四倍频2000脉冲/圈时最多累计约32圈就会从32767跳到-32768。由于代码里用int16_t读取数值是连续变化的角度计算取模后仍正确。但是如果需要统计累计圈数就必须处理溢出。我常用的方案是在计数读取函数里检测溢出标志volatile int32_t overflow_count 0; void TIM3_IRQHandler(void) { if (__HAL_TIM_GET_FLAG(htim3, TIM_FLAG_UPDATE) ! RESET) { if (__HAL_TIM_GET_COUNTER(htim3) 0x8000) { overflow_count--; // 向下溢出 } else { overflow_count; // 向上溢出 } __HAL_TIM_CLEAR_IT(htim3, TIM_IRQn); } }这个方法的原理是计数器向上溢出时会从65535回0向下溢出时从0回65535。读取计数器如果小于32768说明刚发生过向上溢出大于32768说明刚发生过向下溢出。把这个溢出次数累加到软件变量里配合当前计数器值就能还原出一个完整的32位累计数。这个方法在我实际项目中一直用配合TIM3编码器模式和HAL库跑得很稳定。要注意的是溢出中断里读取计数器值需要精准可以在进入中断后立刻读取CNT寄存器避免时序偏差。6. 工程包扩展应用与收尾心得6.1 从单圈角度到多圈位置测量前面写的都是单圈0°到360°的角度测量但很多实际应用需要的是多圈位置信息比如丝杆滑台的绝对位置、云台旋转的总圈数。这个工程包完全可以扩展。如果把get_angle_degrees里的取模运算去掉直接返回累计脉冲数乘以单脉冲角度就是多圈角度值。再配合滚珠丝杆的导程就能把编码器脉冲数换算成直线位移。比如丝杆导程5mm编码器四倍频2000脉冲/圈那么每个脉冲对应的直线位移就是5mm/2000 2.5um。做到这个精度已经远超市面上很多位移传感器了。6.2 与电机控制系统的联动这套角度测量工程包单独用没问题但更常见的场景是配合电机驱动做闭环控制。把编码器接在电机尾轴上F103读取角度值作为位置反馈PID控制器根据目标角度和反馈角度的偏差计算PWM输出驱动电机转动这是典型的编码器位置闭环结构。我这次搭建工程包时特意把编码器读取功能封装成独立模块方便在PID控制工程里直接移植。整个模块只有三个核心函数get_encoder_raw_count()、software_calibrate_zero()和get_angle_degrees()接口清晰不依赖具体的控制逻辑所以移植成本很低。6.3 个人验收心得这套系统我前前后后调了一周多主要是中间处理接线不稳和滤波参数走了不少弯路。最后用串联电位器作为负载配合无刷电机带动编码器做高速旋转测试稳定运行几个小时角度读数在静止时完全稳定在0.00°高速运转时最大误差不超过0.2°这个精度对于500线编码器来说已经接近理论极限了。根据我个人的经验做这类测量系统务必把硬件层面的抗干扰做在前面。如果信号质量不好软件怎么写都补救不了。先把供电、共地、上拉、滤波这些基础做扎实软件层面的问题会少一大半。最后再分享一个小技巧调试阶段一定要用逻辑分析仪抓一下A、B、Z三路信号的波形观察边沿是否符合编码器时序。有一次我调了半天发现编码器静止时偶尔还会跳两个脉冲结果用逻辑分析仪一看是A、B信号在停止位置刚好处于一个边沿附近机械轻微抖动导致多触发了边沿后来把滤波值从0x05调到0x0A就彻底解决了。这种问题用示波器可能一眼扫过去看不出来但逻辑分析仪能直观看到毛刺排查效率高很多。本文还有配套的精品资源点击获取