资讯动态

STM32f103编码器程序实战:正交解码、测速避坑与闭环控制

发布时间:2026/10/23 16:47:48 来源:尧图企业网站定制
简介这份STM32F103编码器程序面向嵌入式开发初学者与电机控制方向的工程师聚焦增量式编码器与STM32定时器接口的完整实现可用于电机测速、位置检测等场景的参考与移植。压缩包共58个文件约1.36MB以C语言源码、头文件、Keil工程文件uvproj、uvopt、编译中间文件o、crf、d及hex烧录文件为主另含启动汇编文件与JLink调试配置工程结构完整可直接打开编译。程序围绕TIM模块捕获编码器脉冲展开涵盖GPIO输入配置、定时器预分频与计数模式设置、中断优先级管理及中断服务函数中读取计数值t的流程并涉及信号滤波抗干扰与PID控制思路。已有2030人学习下载适合对照源码理解编码器信号采集与中断处理的完整链路快速搭建自己的电机控制工程。1. 从一次电机测速翻车说起STM32f103 编码器程序到底能干什么前阵子帮一个做云台的朋友调板子他用的就是 STM32f103 接增量式编码器测电机转速。现象很典型串口打出来的速度值要么是 0要么突然跳到几千PID 一上就抖成筛子。他以为是编码器坏了换了三个型号问题照旧。最后定位到根因——定时器编码器模式配置成了「只数一个方向」另一路信号根本没参与计数方向一变计数就乱。这个坑其实在 STM32f103 编码器程序里非常常见也是很多人第一次接触正交解码时最容易翻车的地方。这份 STM32f103 编码器程序核心就是围绕 TIM 定时器的 Encoder Interface 模式把 A/B 两相正交信号解成带方向的计数值再换算成转速、位置供上层做闭环控制。它适合三类人正在做电机测速、云台、平衡车、CNC 进给的学生和工程师想从 GPIO 中断计数升级到硬件正交解码的开发者以及需要一套能直接移植到 F103C8T6 最小系统上的参考代码的人。下面我按「能干什么 → 怎么跑 → 怎么改」的顺序把这份程序拆开讲。2. 架构与目录拆解TIM 编码器模式是怎么把 A/B 相变成方向计数的2.1 整体分层从信号到转速的三段式这份程序的结构不复杂但分层很清楚基本是「硬件抽象 → 编码器驱动 → 应用换算」三层。底层是 STM32f103 的 TIM 外设初始化负责把 GPIO 配成浮空输入或上拉输入再把对应通道映射到定时器的编码器接口中间层是编码器读取与方向判断负责在固定周期里取 CNT 值并做增量计算上层是速度换算和溢出处理把计数值转成 RPM 或 mm/s。我一般会把这三层放在不同文件里避免 main.c 变成一锅粥。常见做法是Project/ ├── Core/ │ ├── Inc/ │ │ ├── encoder.h // 编码器驱动接口 │ │ ├── motor.h // 电机控制接口 │ │ └── main.h │ └── Src/ │ ├── encoder.c // TIM 编码器模式初始化 读取 │ ├── motor.c // PWM 输出 方向控制 │ └── main.c // 主循环、定时采样、串口打印 ├── Drivers/ │ └── STM32F1xx_HAL_Driver/ └── STM32F103C8TX_FLASH.ld这个目录是 STM32CubeMX 生成工程后的典型形态encoder.c 里放的是与 TIM 强相关的代码motor.c 放 PWMmain.c 只做调度。这样分层的好处是换定时器、换引脚只动 encoder.c换控制算法只动 main.c 里的换算部分。2.2 TIM 编码器模式的寄存器级理解STM32f103 的通用定时器TIM2/TIM3/TIM4和高级定时器TIM1/TIM8都支持编码器接口模式。它的本质是把 TIMx_CH1 和 TIMx_CH2 两路输入接到编码器的 A/B 相硬件根据两路信号的边沿顺序自动加一或减一CNT 寄存器就是位置计数。你不需要写任何中断去手动判断方向这是它比 GPIO 中断计数优雅的地方。关键配置项有三个计数模式、输入捕获滤波、极性。计数模式决定在 A/B 的哪些边沿计数常见的是「Encoder Mode TI1 and TI2」也就是四倍频A 和 B 的上升沿下降沿都计数分辨率最高。滤波用来抑制编码器抖动和电机换向噪声值越大滤波越强但会限制最高响应频率。极性则用来纠正 A/B 接反导致的方向反转。// encoder.c —— TIM3 编码器模式初始化四倍频 void Encoder_Init(void) { TIM_Encoder_InitTypeDef sConfig {0}; TIM_MasterConfigTypeDef sMasterConfig {0}; htim3.Instance TIM3; htim3.Init.Prescaler 0; // 编码器模式不需要分频 htim3.Init.CounterMode TIM_COUNTERMODE_UP; htim3.Init.Period 65535; // 16 位满量程靠溢出计数扩展 htim3.Init.ClockDivision TIM_CLOCKDIVISION_DIV1; htim3.Init.AutoReloadPreload TIM_AUTORELOAD_PRELOAD_DISABLE; sConfig.EncoderMode TIM_ENCODERMODE_TI12; // A/B 双边沿都计数 sConfig.IC1Polarity TIM_ICPOLARITY_RISING; sConfig.IC1Selection TIM_ICSELECTION_DIRECTTI; sConfig.IC1Prescaler TIM_ICPSC_DIV1; sConfig.IC1Filter 6; // 滤波抗抖动 sConfig.IC2Polarity TIM_ICPOLARITY_RISING; sConfig.IC2Selection TIM_ICSELECTION_DIRECTTI; sConfig.IC2Prescaler TIM_ICPSC_DIV1; sConfig.IC2Filter 6; HAL_TIM_Encoder_Init(htim3, sConfig); sMasterConfig.MasterOutputTrigger TIM_TRGO_RESET; sMasterConfig.MasterSlaveMode TIM_MASTERSLAVEMODE_DISABLE; HAL_TIMEx_MasterConfigSynchronization(htim3, sMasterConfig); HAL_TIM_Encoder_Start(htim3, TIM_CHANNEL_ALL); // 启动编码器接口 }这段代码里EncoderMode TIM_ENCODERMODE_TI12是核心它让 TIM3 同时对 TI1 和 TI2 的边沿敏感实现四倍频。IC1Filter 6是经验值电机碳刷噪声大时可以调到 8 到 10但如果你编码器线数高、转速快滤波太强会丢脉冲这时候要降下来。Period 65535是因为 F103 的 TIM3 是 16 位计满会溢出所以上层必须处理溢出否则位置会突然跳变。2.3 读取与方向判断CNT 差值的正确算法很多人读编码器就是直接读 CNT然后两次相减。这在低速下没问题但高速或者采样周期长的时候16 位计数器会溢出差值算出来是错的。正确做法是用有符号的 16 位差值让编译器自动处理回绕。// encoder.c —— 读取增量返回带符号的脉冲数 int16_t Encoder_GetDelta(void) { static uint16_t last_cnt 0; uint16_t now_cnt __HAL_TIM_GET_COUNTER(htim3); int16_t delta (int16_t)(now_cnt - last_cnt); // 利用补码回绕 last_cnt now_cnt; return delta; }(int16_t)(now_cnt - last_cnt)这行是关键。假设 last_cnt 是 65530now_cnt 是 5无符号相减得到 11强转成 int16_t 还是 11表示正向走了 11 个计数。反过来 last_cnt 是 5now_cnt 是 65530无符号相减得到 65525强转 int16_t 变成 -11表示反向。这就是利用补码回绕做溢出处理比手动判断溢出标志位简洁得多。注意采样周期不能太长否则一个周期内增量超过 32767 就会误判方向一般 1ms 到 10ms 采样一次足够。3. 跑起来从 CubeMX 配置到串口看到转速的完整步骤3.1 硬件接线与引脚分配先确认你的板子和编码器。F103C8T6 最小系统上TIM3 的 CH1 是 PA6CH2 是 PA7TIM2 的 CH1 是 PA0CH2 是 PA1TIM4 的 CH1 是 PB6CH2 是 PB7。编码器的 A 相接 CH1B 相接 CH2VCC 和 GND 按编码器规格接常见的是 5V 或 3.3V 供电。如果是差分输出的编码器比如伺服电机上的中间要加差分接收芯片转成单端不能直接接。提示接线前先用万用表确认编码器供电电压5V 编码器输出直接进 F103 的 3.3V 引脚长期可能损伤 IO稳妥做法是加电平转换或串 1k 电阻。3.2 CubeMX 关键配置项用 CubeMX 建工程时选好芯片型号后在 Timers 里选一个支持编码器模式的定时器比如 TIM3。Combined Channels 选 Encoder Mode然后 Channel1 和 Channel2 会自动变成编码器输入。Parameter Settings 里Encoder Mode 选 Encoder Mode TI1 and TI2Counter Period 填 65535IC1 Filter 和 IC2 Filter 填 6 到 10。GPIO 那边会自动把 PA6、PA7 配成输入Pull-up/Pull-down 建议选 Pull-up因为很多编码器是开漏输出。时钟树别忘了F103 一般跑 72MHzAPB1 定时器时钟也是 72MHz。串口配一个 USART1PA9/PA10波特率 115200用来打印转速。生成工程时选 HAL 库工具链按你用的 IDE 选。3.3 主循环采样与转速换算生成工程后在 main.c 里加采样逻辑。核心是固定周期读增量换算成转速。假设编码器线数是 500 线四倍频后每转 2000 个计数采样周期 10ms。// main.c —— 10ms 定时采样换算 RPM #define ENCODER_PPR 500 // 编码器线数 #define GEAR_RATIO 1 // 减速比 #define SAMPLE_PERIOD_MS 10 // 采样周期 int32_t total_count 0; float rpm 0.0f; while (1) { int16_t delta Encoder_GetDelta(); total_count delta; // RPM (增量 / 每转计数) / 采样时间(秒) * 60 float counts_per_rev ENCODER_PPR * 4 * GEAR_RATIO; rpm (delta / counts_per_rev) / (SAMPLE_PERIOD_MS / 1000.0f) * 60.0f; printf(delta%d, total%ld, rpm%.2f\r\n, delta, total_count, rpm); HAL_Delay(SAMPLE_PERIOD_MS); }ENCODER_PPR * 4是四倍频后的每转计数GEAR_RATIO是电机到输出轴的减速比直连就填 1。delta / counts_per_rev得到这一周期转了多少圈除以采样时间得到每秒转数乘 60 就是 RPM。total_count是累计位置可以用来做位置闭环。注意HAL_Delay在中断里不能用这里放主循环没问题但如果你的控制周期要求严格建议用定时器中断触发采样避免主循环里其他任务拖慢节奏。烧录后打开串口手动转一下电机轴应该能看到 delta 正负交替、rpm 跟着变。如果方向反了把 A/B 相接线对调或者在 CubeMX 里把 IC1Polarity 和 IC2Polarity 其中一个改成 FALLING。4. 避坑与排查编码器读数不对时先查这五条4.1 读数一直是 0 或者固定值现象串口打出来的 delta 恒为 0或者一直是个固定数不随转动变化。原因通常是编码器没供电、A/B 相接错引脚、或者定时器没启动。先量编码器 VCC 有没有电压再用示波器看 A/B 相有没有方波。如果信号有但 CNT 不动检查HAL_TIM_Encoder_Start有没有调用以及 GPIO 有没有被别的外设占用。F103 的 PA6/PA7 同时也是 SPI1 的引脚如果工程里开了 SPI1 复用会冲突。4.2 方向反了或者正反转都加现象正转时 CNT 增加反转时也增加或者方向跟预期相反。原因一是 A/B 相接反二是编码器模式选成了单通道。解决先对调 A/B 接线看方向是否正常如果正反转都加检查EncoderMode是不是误设成了TIM_ENCODERMODE_TI1或TI2这两个只对单通道边沿计数无法判断方向必须用TI12。4.3 高速时转速跳变或丢脉冲现象低速正常转速一高 rpm 就乱跳甚至出现负值。原因一是采样周期太长一个周期内增量超过 32767补码回绕判断失效二是输入滤波太强高频脉冲被滤掉。解决缩短采样周期到 1ms 到 5ms或者把IC1Filter降到 2 到 4。如果编码器线数很高比如 2500 线四倍频后每转 10000 计数高速时增量很大这时候要么提高采样频率要么换 32 位定时器TIM2/TIM5并扩大 Period。4.4 电机一动读数就抖现象电机不转时读数稳定一上电转动就出现大量随机正负跳变。原因是电机换向噪声和电源纹波耦合到编码器信号线上。解决编码器信号线用屏蔽线并单端接地A/B 相各加 100nF 到 GND 的滤波电容软件上把IC1Filter和IC2Filter调到 8 到 10。如果还抖检查电机和编码器的地是不是共地不共地会引入共模干扰。4.5 累计位置 total_count 越走越偏现象来回转动后 total_count 回不到原点有累积误差。原因是四倍频模式下每个边沿都计数如果编码器信号有毛刺毛刺会被当成有效边沿。解决加强硬件滤波软件上可以在采样时对 delta 做限幅比如单周期增量超过理论最大值就丢弃。另外注意total_count用 int32_t别用 int16_t否则转几圈就溢出了。5. 二次开发把这份程序改成带速度环和位置环的控制底座5.1 从测速到闭环加一个定时器中断做固定周期采样主循环HAL_Delay采样最大的问题是周期不准串口打印、其他任务都会拖慢它。要做闭环第一步是把采样搬到定时器中断里。我一般会再开一个基本定时器比如 TIM6配成 1ms 或 5ms 中断在中断里读编码器增量、算速度、跑 PID。// tim.c —— TIM6 基础定时器1ms 中断 void MX_TIM6_Init(void) { TIM_MasterConfigTypeDef sMasterConfig {0}; htim6.Instance TIM6; htim6.Init.Prescaler 7200 - 1; // 72MHz / 7200 10kHz htim6.Init.CounterMode TIM_COUNTERMODE_UP; htim6.Init.Period 10 - 1; // 10kHz / 10 1kHz即 1ms HAL_TIM_Base_Init(htim6); HAL_TIM_Base_Start_IT(htim6); } // stm32f1xx_it.c —— 中断回调 void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim-Instance TIM6) { int16_t delta Encoder_GetDelta(); g_total_count delta; g_speed_rpm (delta / (ENCODER_PPR * 4.0f)) / 0.001f * 60.0f; // 在这里调用 PID 计算输出 PWM } }Prescaler 7200 - 1把 72MHz 分频到 10kHzPeriod 10 - 1再分频到 1kHz也就是 1ms 一次中断。中断里只做读数和 PID串口打印这种耗时操作放主循环用标志位传递数据。这样速度环的采样周期是硬的PID 参数才好整定。5.2 位置环与多圈计数把 16 位 CNT 扩成 32 位位置F103 的 TIM3 是 16 位单圈计数有限做多圈位置控制必须扩展。上面的g_total_count就是扩展后的 32 位位置。要注意的是如果你在中断里读 CNT而 CNT 在两次中断之间溢出了多次就会丢圈。1ms 中断下只要电机转速不超过「32768 计数/ms」就不会丢换算成 RPM 是极高的值一般应用碰不到。但如果你的采样周期是 10ms高速时就要小心。位置环的改造思路是把g_total_count作为反馈目标位置作为输入误差送 PID输出限幅后给 PWM。这里有个细节位置环和速度环的采样周期最好一致或者位置环用速度环的输出做前馈否则容易振荡。5.3 移植到其他定时器或芯片的注意事项这份程序基于 TIM3换成 TIM2 或 TIM4 时除了改htim3为对应句柄还要注意引脚映射不同。TIM2 是 32 位定时器Period 可以设成 0xFFFFFFFF这样几乎不用考虑溢出做位置计数更省心。如果换到 F4 或 G4 系列HAL 库接口基本一致但时钟树和定时器位宽不同EncoderMode的宏定义可能略有差异移植时对着参考手册确认一下。另外如果你用的是带霍尔传感器的 BLDC编码器模式同样适用但霍尔是三路信号需要换成 Hall 接口模式或者用三个 GPIO 中断做换向这跟正交编码器是两套逻辑别混用。5.4 一个我踩过的坑中断里读 CNT 的原子性最后说个血泪经验。我早期在 1ms 中断里读 CNT同时主循环里也读 CNT 做显示两边都调Encoder_GetDelta结果last_cnt这个静态变量被两个上下文同时改读数偶尔跳变。后来改成只在中断里更新编码器数据主循环只读全局变量问题消失。从那以后我每次把编码器读取放进中断都强制走一遍「谁写谁读」的检查确保last_cnt只有一个写入者。如果你也在做速度环这个习惯能帮你省下不少调试时间。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑