
简介这是一套基于STM32F103C8T6微控制器的正交编码器角度读取完整工程面向嵌入式开发者与电机控制方向学习者。代码经编译与下载验证可直接在配套硬件上运行。工程采用标准Keil MDK结构完整包含HARDWARE硬件驱动层EXTI外部中断、ENCODE编码器采集、NVIC中断管理、KEY按键、LED指示、SYSTEM系统服务层串口、延时及USER主程序模块目录层级清晰便于阅读与二次开发。资源共80个文件以36个h头文件与35个c源文件为主辅以uvproj工程配置、hex固件及批量清理脚本压缩包约309KB轻量易部署。引脚映射方面PB6、PB7、PA1分别对应编码器A相、B相与Z相ABZ输出已做上拉处理代码预设2500线编码器若更换规格仅需调整定时器周期与角度换算系数两处参数。目前已有1781人学习下载适合正在调试正交解码与角度测量的开发者参考借鉴。 开门见山收到一个命名极其朴素的工程包STM32f103c8t6编码器.zip。解压以后内容也很朴素——一块STM32F103C8T6最小系统板一个增量式编码器一段用定时器编码器模式读取A/B正交信号并换算成转速的代码外加串口打印调试。可别小看这套组合直流电机闭环、小车里程计、云台角度反馈、旋钮人机交互多少项目的地基都是从这套东西长出来的。这篇文章就借这个工程把整条链路拆开看透为什么编码器A相要接定时器CH1、B相接CH2定时器编码器模式内部到底怎么计数M法T法测速怎么选16位计数器回绕怎么处理上电乱跳怎么解。一套流程走完你不仅能跑通这个工程还能顺手把逻辑搬到自己的项目里。适合手里有C8T6最小系统板和增量式编码器、打算做速度闭环或者位置采集的朋友参考。1. 项目拆解C8T6为什么适合做编码器接口1.1 增量式编码器的信号本质增量式编码器输出的是两路相位差90度的方波也就是常说的A/B正交信号。转动方向不同A和B的相位关系也不同正转时A领先B 90度反转时B领先A 90度。每转一圈输出的脉冲数取决于光栅刻线数或磁极对数。比如很常见的13线电机编码器配合1:30减速箱输出轴转一圈就能得到13×30390个脉冲经过定时器4倍频以后是1560个计数。这个数就是后面所有测速和位置计算的基准。我特意强调相位关系是因为很多新手把编码器当成两个独立开关去看用两个外部中断分别统计脉冲数再靠软件判断方向。这种做法在低速、低分辨率下勉强能跑但转速一高、脉冲一密中断频繁到CPU来不及响应丢脉冲、判错方向就全来了。编码器信号本来就该是两路信号联合解码而不是两路独立计数。这也是我推荐直接用定时器编码器模式的根本原因。1.2 定时器编码器模式的硬件优势STM32F103C8T6有TIM1、TIM2、TIM3、TIM4四个支持编码器模式的定时器其中TIM2/3/4是16位通用定时器特别适合干这个活。编码器接口在硬件上做了完整的正交解码内部逻辑实时监测CH1和CH2的边沿与电平组合每当A/B信号变化时计数器自动加一或减一CPU完全不用参与。这意味着即使脉冲频率到几十千赫兹计数也不会丢。把CPU从频繁中断里解放出来这是编码器接口和外部中断方案最本质的区别。C8T6为什么是首选一是便宜几块钱一片48引脚封装通用定时器够用二是生态成熟STM32CubeMX里点几下就能完成配置HAL库接口封装得比较顺手三是48KB Flash跑这类逻辑绰绰有余。很多项目直接从C8T6起步后续换到CBT6或者F103系列其他型号代码几乎不用动。对学习阶段、对产品原型验证这颗芯片的性价比都很难被替代。2. 编码器模式工作原理与计数配置2.1 定时器内部如何判断方向和计数定时器编码器模式本质上用到的是芯片的外部时钟方向选择机制。内部逻辑把A/B两路信号组合成两件事确定计数时钟的触发源确定计数方向。以TI1TI2模式为例A相上升沿触发一次A相下降沿触发一次B相上升沿触发一次B相下降沿触发一次一圈下来就是4倍频。每一时刻具体是加还是减由该时刻另一路信号的电平决定——如果A上升沿时B是低电平说明正转如果A上升沿时B是高电平说明反转。这些判断全部由硬件完成所以响应速度极快这也是编码器接口能扛住高频率脉冲的关键。CubeMX配置界面里有一个Encoder Mode下拉框三个选项对应三种倍频编码器模式计数触发源每圈计数值适用场景TI1仅A相边沿1倍频粗略测向、极低速TI2仅B相边沿1倍频同上TI1 TI2A/B全部边沿4倍频高分辨率、位置测量实际项目里我基本都选TI1TI2。4倍频白白提升分辨率CPU开销却一点不增加这种便宜不占白不占。除非编码器本身线数特别高、计数器容易溢出才会退回到1倍频或者用外部硬件分频。2.2 16位计数器回绕与符号处理TIM2/3/4的计数器是16位的取值范围0到65535。编码器模式下计数器从0向上加超过65535回绕到0反转时向下减低于0回绕到65535。如果你直接读TIM_GetCounter得到的永远是无符号数短时间看好像位置在乱跳其实不是乱跳是回绕了。解决回绕最优雅的办法是把它当有符号数用。读取时先取原始16位值强转成int16_t0xFFFF就变成-10xFFFE变成-2。单位时间内速度测量这么处理足够用——100ms采样间隔内差值范围远小于32767直接算差就能拿到真实增量。但要测电机累计位置16位撑不了太久跑个几千转肯定溢出这时候就需要软件把计数器扩展成32位固定周期比如1ms读取计数器差值累加到一个int32_t变量里。这个思路我在第四部分代码里会完整实现。3. 硬件接线与CubeMX配置实战3.1 引脚分配和接线要点定时器编码器模式必须把编码器A相接定时器的CH1B相接同一个定时器的CH2不能接反也不能分开接两个定时器否则硬件解码逻辑根本不工作。C8T6常用映射如下定时器CH1A相CH2B相TIM2PA0 / PA5 / PA15PA1 / PA3 / PB3TIM3PA6 / PB4PA7 / PB5TIM4PB6PB7这个工程里用的是TIM3A相接PA6B相接PA7两个引脚在最小系统板上挨着飞线很方便。接线时容易踩三个坑。第一编码器如果是开漏输出必须加外部上拉电阻一般4.7k到10k拉到3.3V如果是推挽输出不用上拉直接接。第二编码器供电要干净最好和MCU共地控制电机时编码器线要远离电机电源线否则电机启停瞬间的尖峰很容易干扰A/B信号。第三如果编码器是5V供电输出高电平可能到5VC8T6引脚不允许5V直接灌入这时候要么选3.3V供电的编码器要么加电平转换或者分压电阻。这些细节不处理好后面调试就会出现空转正常、电机一开读数乱飞的典型症状。3.2 CubeMX里怎么配置用STM32CubeMX建工程芯片选STM32F103C8Tx。几个关键配置点如下。时钟走常规流程HSE外部晶振8MHzPLL倍频到72MHzAPB1定时器时钟默认36MHzAPB2定时器时钟72MHz。TIM3挂在APB1上所以编码器接口的工作时钟是36MHz这个值在后面算输入滤波器等级时会用到。TIM3参数页里Clock Source选择Encoder Mode然后按下表填配置项值Encoder ModeEncoder Mode TI1 and TI2Input Filter15对大多数电机编码器够用能滤掉大部分毛刺Prescaler0Counter Period65535Counter ModeCounting Up编码器模式下由硬件自动接管配好之后CubeMX会自动在TIM3的CH1和CH2上分配引脚默认就是PA6和PA7。如果引脚和别的外设冲突手动重映射到PB4/PB5也行。串口打开USART1的TX/RXPA9/PA10用于打印转速。到这里硬件配置就齐了生成代码进入软件编写。3.3 三个容易被忽略的参数编码器接口有几个参数新手很容易忽略。一个是IC1Filter/IC2Filter输入滤波器。它本质是一个数字低通滤波器用定时器时钟对输入信号采样只有连续N个周期都稳定才确认有效能滤除机械抖动和毛刺。但滤波等级太高也会引入延迟对极低速信号甚至可能卡住不计数。经验上电机编码器一般取8到15手拧旋钮这类低频场景可以取0。另一个是Prescaler预分频。编码器模式下一般保持0因为编码器信号已经是外部时钟了再分频就没有意义只有编码器线数极高、需要降低计数频率时才考虑。还有一个是Counter Period计数器周期它直接决定回绕点。常规做法是保持65535配合int16_t强转当有符号数用。如果你只需要单向计数或者固定小范围位置可以把它改小比如1000但边界逻辑要自己算清楚别让差值超出int16_t范围。4. 核心代码与测速算法实现4.1 初始化与读取封装的HAL库代码CubeMX生成初始代码之后编码器其实已经在后台跑了你要做的只有两件事启动编码器计数按周期读取差值。启动代码很简单HAL_TIM_Encoder_Start(htim3, TIM_CHANNEL_ALL);读取和换算我用下面几个函数封装后续在main里直接调用// 读取计数增量带16位回绕处理 int16_t Encoder_GetDelta(void) { uint16_t now __HAL_TIM_GET_COUNTER(htim3); static uint16_t last 0; int16_t delta (int16_t)(now - last); last now; return delta; } // 累计位置32位扩展 int32_t Encoder_GetPosition(void) { static int32_t position 0; position Encoder_GetDelta(); return position; } // 计算电机轴转速单位RPM float Encoder_GetSpeedRPM(uint16_t ppr, float sample_time_s) { int16_t delta Encoder_GetDelta(); float rpm (float)delta / (4.0f * ppr) / sample_time_s * 60.0f; return rpm; }注意Encoder_GetDelta里把差值转成int16_t这个细节。now和last都是uint16_t相减结果如果是负值硬件上会得到一个很大的无符号数再强转成int16_t就恢复了正确的负数。这是16位回绕处理的核心技巧没有这一步反转时你会测出一个莫名其妙的巨大正向速度。4.2 M法、T法、M/T法怎么选测转速的算法按脉冲测量方式划分主流就三种。M法也叫定时计数法固定时间窗口内数有多少个脉冲再做换算。公式就是我在Encoder_GetSpeedRPM里写的那个转速RPM 计数值 / (4 × PPR) / 采样时间 × 60。优点是实现简单中高速下稳定缺点是极低速时一个时间窗口内可能连一个脉冲都没有读数就是0分辨率不够。像电机额定转速几百上千转的项目用M法完全够用。T法也叫测周期法通过测量相邻两个脉冲的时间间隔来算速度。低速时非常灵敏因为脉冲间隔长测时间精度高但高速时脉冲间隔太短测时间精度反而下降。实际工程里T法通常封装成输入捕获中断记录上一个脉冲和当前脉冲的时间戳。M/T法则是把两者结合低速用T法、高速用M法或者在一个测速周期内同时记录脉冲个数和最后一个脉冲的时间戳覆盖很宽的速度范围。代价是代码复杂度和定时器资源占用明显上升。对于C8T6做普通电机闭环我建议先用M法跑通发现低速性能不够再上M/T法别一上来就整花活。4.3 完整测速示例主循环里我习惯固定100ms调用一次测速。这个时间窗口对大多数电机都合适太快了低速下脉冲太少太慢了速度反馈滞后明显闭环会不稳定。用定时器产生100ms中断也行直接在while循环里用HAL_GetTick判断也行简单演示后者uint32_t last_ticks HAL_GetTick(); while (1) { if (HAL_GetTick() - last_ticks 100) { last_ticks HAL_GetTick(); float rpm Encoder_GetSpeedRPM(13, 0.1f); // 13线电机编码器 printf(rpm: %.2f\r\n, rpm); } }特别注意如果编码器装在电机轴上13线就是电机轴每转13个脉冲4倍频后每转52个计数如果编码器在减速箱输出轴上或者电机经过减速器再带动负载PPR要乘上减速比。之前提过的1:30减速箱例子输出轴转速对应PPR就是13×30390。换算单位前一定先搞清楚编码器装在哪一级轴上这是最容易翻车的地方。5. 调试实录常见问题与排查思路5.1 数据跳动、低速丢脉冲怎么办我在这个工程上遇到最烦的问题是电机空转时读数很准一挂上负载串口打印的转速就开始上下跳。排查过程走了几个弯路最后锁定在三个原因。第一个是干扰电机换向或者PWM驱动带来的噪声耦合进编码器线解决办法是编码器线用屏蔽线、远离功率线并且给编码器供电加100nF去耦电容。第二个是输入滤波器等级太低把IC Filter从0调到8以后毛刺基本消失。第三个比较隐蔽当时用的编码器是5V供电信号高电平接近5VC8T6引脚虽然标了容忍5V但边沿上升时间受内部保护二极管影响硬件解码不稳定。后来串了一个1k电阻现象就正常了。遇到数据跳动先别急着改算法按这个顺序排查看供电是否稳定、看信号线是否过长、看滤波器等级、再看共地是否可靠。软件滤波是最后一道防线能掩盖问题但治不了根。5.2 方向判断反了怎么处理如果发现正转时计数器往下减方向判断反了处理办法有三种从硬件到软件把A/B两根线对调在CubeMX里把IC1Polarity和IC2Polarity改成相反极性在代码里把差值取反。我最推荐的是调换IC1/IC2极性不用动硬件逻辑也清晰。但注意极性配置不是简单把上升沿改成下降沿就完事它影响的是解码逻辑对边沿的判定。如果改完方向对了但计数值变成1倍频说明极性配置和编码器的电平触发方式不匹配需要回去核对编码器手册里输出波形是高电平有效还是低电平有效。5.3 上电位置乱跳、没有根位置增量式编码器天生没有绝对零位。上电那一刻计数器从0开始但机械位置可能停在任意角度所以直接显示的位置和实际物理位置之间有一个未知偏移。这不是程序bug是增量编码器的固有特性。工程上有两种标准处理方式一种是在位置控制项目里上电先让电机回零跑到限位开关或者原点信号处再把计数器清零另一种是接受相对位置只在运行过程中做增量统计初始绝对位置用其他传感器磁编码器、电位器等做一次标定。这个工程里走的是第二条路所以串口里看到的初始位置是0不代表编码器实际在0度。理解这一点后面做闭环才不会犯迷糊。5.4 串口打印乱码的经典原因调试时还会碰到一个特别基础但也特别让人抓狂的问题串口打印出现乱码。大部分情况不是编码器问题而是波特率不匹配或者系统时钟被改过而串口初始化没同步更新。比如把外部晶振从8MHz换成12MHzCubeMX重新生成后如果上位机还按原来的波特率打开打印必乱。另外用printf重定向到串口时记得处理fputc并把半主机模式关掉否则printf卡死在调试器上是经常发生的事。6. 工程扩展从测速到闭环控制6.1 速度闭环和位置闭环的基本思路测出转速之后下一步自然是闭环。速度环做法很直接目标转速和实测转速做差经过PID控制器输出PWM占空比给电机驱动。关键在于采样周期要稳定C8T6上通常用1kHz到10kHz的控制频率在定时器中断里读取速度、计算PID、更新占空比。位置环则在速度环外面再套一层目标位置和实测位置的误差先转成目标速度再交给速度环执行。编码器分辨率越高闭环越顺手——这也是为什么我建议编码器模式里开4倍频它直接决定了闭环能分辨的最小位置变化。6.2 显示、通信与上位机联调调试时离不开人机交互。最简单的是串口打印用printf重定向到USART1波特率115200就能在电脑上实时看转速和位置。想更直观可以加一块0.96寸OLED显示转速、位置和目标值I2C接口占两个引脚SSD1306驱动库网上很好找。如果做整车或者多电机项目RS485或者CAN总线会更合适但这块板子没有CAN外设用SPI外挂CAN控制器也能实现看项目需求量力而行。无论走哪条路调试时先拿手慢慢转动编码器观察串口或屏幕数据是否符合预期确认没问题再上电机能帮你省掉一大半排查时间。这套工程做完之后我的体会是编码器看起来简单真正决定项目成败的往往是硬件抗干扰和计数回绕这类细节。把A/B相位关系搞清楚把16位计数器的有符号处理写对后面再去做速度环、位置环心里会踏实很多。如果你手里的编码器线数、减速比或者供电方式和我这个工程不一样核心处理逻辑也不用变把PPR和采样时间替换成自己的参数就行。那包看似普通的zip文件拆到最后其实是一整套电机测速和闭环控制的基础方法论。本文还有配套的精品资源点击获取