江科大STM32入门:TIM编码器接口——让硬件自动帮你“数脉冲“

⚠️ 本文基于 STM32F103C8T6 + 标准库 V3.5.0,适用于江科大 STM32 入门教程体系。编码器接口属于定时器基础功能,原理跨版本通用,但 HAL 库函数名和配置方式会有差异。
一、前言
做过电机控制或者智能小车的朋友一定遇到过这个问题:怎么测电机的转速?
最原始的办法是接编码器的 A 相到外部中断引脚,每次来一个脉冲进一次中断,在中断里手动计数。但这样有个大问题——脉冲频率一高,CPU 就忙不过来了。编码器转一圈可能产生几百个脉冲,电机转速一上来,中断频率爆炸,主循环都跑不动了。
这时候 STM32 的 编码器接口(Encoder Interface) 就派上用场了。它是定时器内部的一个硬件模块,能自动接收编码器的正交信号,自动控制 CNT 计数器自增或自减,你只需要每隔一段时间去读一下 CNT 的值,就知道编码器转了多少、往哪个方向转的。CPU 完全不用干预!
今天这篇文章,我就把编码器接口的原理、三种工作模式、所有相关库函数、以及完整代码实战给大家讲清楚。
二、编码器接口是什么?
2.1 基本概念
编码器接口是 STM32 定时器内部的一个硬件模块,专门用来接 增量式(正交)编码器。
它的核心能力就三个:
| 能力 | 说明 |
|---|---|
| 自动计数 | 编码器 A、B 相的脉冲直接作为 CNT 的计数时钟,不需要 CPU 干预 |
| 自动辨向 | 根据 A、B 相的相位关系,自动判断正转(CNT++)还是反转(CNT--) |
| 测速测位 | CNT 的值直接表示编码器的位置;定时取 CNT 值并清零,就能算出速度 |
💡 我的理解:你可以把编码器接口想象成一个"自动秘书"。以前你(CPU)要亲自数脉冲(进中断),现在秘书(硬件)帮你数,你每隔一段时间问一下"数到多少了"就行,轻松多了。
2.2 编码器接口"借"了谁的资源?
每个 高级定时器 和 通用定时器 都有 1 个编码器接口。它占用的硬件资源很"克制":
-
引脚:只占用 CH1 和 CH2 两个引脚(对应输入捕获的通道 1 和通道 2)
-
通路:借用了输入捕获前端的 滤波器 和 边沿检测 模块
-
输出:直接控制时基单元的 CNT 计数器(控制计数时钟和计数方向)
CH3 和 CH4 与编码器接口完全无关,后面的预分频器、CCR 寄存器也不参与编码器接口的工作。

图中展示了编码器接口的数据通路:GPIO → 滤波器 → 边沿检测/极性选择 → 产生 TI1FP1 和 TI2FP2 → 编码器接口 → 控制 PSC 和 CNT。编码器接口相当于一个"从模式控制器",根据 A、B 相的边沿和相位关系,决定 CNT 是自增还是自减。
三、正交信号:编码器为什么能辨向?
3.1 什么是正交信号?
增量式编码器有两路输出,叫 A 相 和 B 相。它们输出的是方波,但两路方波之间有一个固定的 90° 相位差。
-
正转时:A 相超前 B 相 90°
-
反转时:A 相滞后 B 相 90°(或者说 B 相超前 A 相 90°)

上图展示了正转和反转时的 A、B 相波形。正转时 A 相上升沿对应 B 相低电平;反转时 A 相上升沿对应 B 相高电平。正是这个"另一相的电平状态"差异,让编码器接口能判断方向。
3.2 编码器接口怎么判断方向?
编码器接口的设计逻辑很巧妙:
把 A 相和 B 相的所有边沿(上升沿 + 下降沿)都作为计数时钟,但向上计数还是向下计数,取决于边沿发生时"另一相"的电平状态。
例如:
-
正转时,A 相产生上升沿,此时 B 相是低电平 → CNT++
-
反转时,A 相产生上升沿,此时 B 相是高电平 → CNT--
因为正转和反转时,同一相边沿对应的"另一相电平"正好是相反的,所以硬件能自动区分方向。
四、三种工作模式详解

编码器接口有三种计数模式,由 TIM_EncoderInterfaceConfig 的第一个参数决定:
| 模式 | 宏定义 | 计数边沿 | 适用场景 |
|---|---|---|---|
| 仅 TI1 计数 | TIM_EncoderMode_TI1 |
只在 A 相(TI1)的边沿计数 | 只需要单倍精度 |
| 仅 TI2 计数 | TIM_EncoderMode_TI2 |
只在 B 相(TI2)的边沿计数 | 只需要单倍精度 |
| TI1 和 TI2 都计数 | TIM_EncoderMode_TI12 |
A 相和 B 相的边沿都计数 | 四倍频,精度最高,最常用 |
💡 我的理解:什么叫"四倍频"?编码器转一圈,A 相和 B 相各输出一圈脉冲(比如各 100 个)。如果只在 A 相计数,一圈计 100 次;如果在 A、B 两相的上升沿和下降沿都计数,一圈就能计 400 次!分辨率直接翻 4 倍,所以叫四倍频。测速测位都用这个模式。
五、配置六步法
编码器接口的配置流程非常固定,记住这六步:
表格
| 步骤 | 操作内容 | 对应代码 |
|---|---|---|
| 第 1 步 | RCC 开启时钟(GPIO + TIM) | RCC_APB1PeriphClockCmd、RCC_APB2PeriphClockCmd |
| 第 2 步 | GPIO 初始化,配置为输入模式 | GPIO_Init |
| 第 3 步 | 配置时基单元(PSC 不分频,ARR 给最大) | TIM_TimeBaseInit |
| 第 4 步 | 配置输入捕获单元(只配滤波器和极性) | TIM_ICInit |
| 第 5 步 | 配置编码器接口模式 | TIM_EncoderInterfaceConfig |
| 第 6 步 | 开启定时器 | TIM_Cmd |
六、所有相关库函数详解
这一节我把编码器接口会用到的 全部库函数 列出来,一个一个讲清楚。
6.1 编码器接口配置函数(核心中的核心)
① 编码器接口模式配置
void TIM_EncoderInterfaceConfig(TIM_TypeDef* TIMx,
uint16_t TIM_EncoderMode,
uint16_t TIM_IC1Polarity,
uint16_t TIM_IC2Polarity);
// 解析:TIMx —— 指定定时器,如 TIM3
// TIM_EncoderMode —— 编码器模式选择
// TIM_EncoderMode_TI1 :仅在 TI1(A相)边沿计数
// TIM_EncoderMode_TI2 :仅在 TI2(B相)边沿计数
// TIM_EncoderMode_TI12 :TI1 和 TI2 边沿都计数(四倍频,最常用)
// TIM_IC1Polarity —— 通道 1(TI1)的极性/边沿选择
// TIM_ICPolarity_Rising :上升沿有效(不反相)
// TIM_ICPolarity_Falling :下降沿有效(反相)
// TIM_IC2Polarity —— 通道 2(TI2)的极性/边沿选择,同上
// 注意:这个函数是编码器接口的"一键配置",调用后硬件自动接管 CNT 的计数
💡 我的理解:这个函数就是编码器接口的"总开关"。一旦调用,定时器就不再走内部时钟了,而是把 TI1FP1 和 TI2FP2 当成外部时钟源,并且根据两相信号的相位关系自动决定 CNT 是加还是减。极性参数一般两个都写
Rising,表示不反相,正常计数。
6.2 时基单元相关函数
② 选择内部时钟源
void TIM_InternalClockConfig(TIM_TypeDef* TIMx);
// 解析:TIMx —— 指定定时器
// 功能:配置定时器使用内部时钟(CK_INT),编码器接口模式下其实用不到内部时钟,
// 但时基单元初始化前显式调用一下是好习惯
③ 时基单元初始化
void TIM_TimeBaseInit(TIM_TypeDef* TIMx, TIM_TimeBaseInitTypeDef* TIM_TimeBaseInitStruct);
// 解析:TIMx —— 指定定时器
// TIM_TimeBaseInitStruct —— 时基结构体指针
TIM_TimeBaseInitTypeDef 结构体成员(编码器接口模式下的特殊配置):
| 成员 | 编码器接口下的配置 | 说明 |
|---|---|---|
TIM_ClockDivision |
TIM_CKD_DIV1 |
时钟不分频 |
TIM_CounterMode |
TIM_CounterMode_Up |
向上计数(编码器接口会覆盖方向) |
TIM_Period |
65536 - 1 |
ARR 给最大,防止计数溢出 |
TIM_Prescaler |
1 - 1(即 0) |
PSC 不分频! 编码器脉冲直接进 CNT |
TIM_RepetitionCounter |
0 |
仅高级定时器有效 |
⚠️ 踩坑预警:编码器接口模式下,PSC 一定要设为 0(1-1)!因为编码器脉冲就是 CNT 的计数时钟,如果再分频,脉冲就被吃了,数出来的值就不对了。ARR 一定要给最大(65535),不然编码器转得快一点就溢出了。
6.3 输入捕获配置函数
④ 输入捕获单元初始化
void TIM_ICInit(TIM_TypeDef* TIMx, TIM_ICInitTypeDef* TIM_ICInitStruct);
// 解析:TIMx —— 指定定时器
// TIM_ICInitStruct —— 输入捕获结构体指针
// 注意:编码器接口只借用输入捕获的"前端"(滤波器和边沿检测),
// 所以只需要配置滤波器和极性,通道选择等参数不影响编码器接口
TIM_ICInitTypeDef 结构体成员(编码器接口模式下):
| 成员 | 常用配置 | 说明 |
|---|---|---|
TIM_Channel |
TIM_Channel_1 / TIM_Channel_2 |
分别配置 CH1 和 CH2 |
TIM_ICFilter |
0xF |
输入滤波器,最大滤波消除抖动 |
TIM_ICPolarity |
TIM_ICPolarity_Rising |
极性选择,实际由 EncoderInterfaceConfig 覆盖 |
TIM_ICPrescaler |
TIM_ICPSC_DIV1 |
不分频 |
TIM_ICSelection |
TIM_ICSelection_DirectTI |
直连模式 |
💡 我的理解:编码器接口模式下,
TIM_ICInit主要目的是配置滤波器。因为编码器信号可能会有机械抖动,滤波器能把毛刺滤掉。极性其实最终由TIM_EncoderInterfaceConfig决定,但这里也写Rising保持一致。
⑤ 给输入捕获结构体赋默认值
void TIM_ICStructInit(TIM_ICInitTypeDef* TIM_ICInitStruct);
// 解析:TIM_ICInitStruct —— 输入捕获结构体指针
// 功能:把结构体所有成员填充为默认值,防止未初始化成员导致奇怪问题
// 建议:定义结构体后先调用这个函数,再修改需要的成员
6.4 计数器读写函数
⑥ 获取当前计数器值
uint16_t TIM_GetCounter(TIM_TypeDef* TIMx);
// 解析:TIMx —— 指定定时器
// 返回值:当前 CNT 计数器的值(0~65535)
// 注意:编码器正转时 CNT 增加,反转时 CNT 减小,但 CNT 本身是 uint16_t
// 如果要判断方向,需要结合前后两次读数的差值符号(用 int16_t 强制转换)
⑦ 设置计数器值
void TIM_SetCounter(TIM_TypeDef* TIMx, uint16_t Counter);
// 解析:TIMx —— 指定定时器
// Counter —— 要写入的计数值
// 功能:直接给 CNT 赋一个值,常用于清零操作(写 0)
💡 我的理解:测速的标准套路是——定时(比如每 100ms)调用
TIM_GetCounter读出 CNT,然后用TIM_SetCounter(TIM3, 0)清零。两次读数的差值就是这段时间内的脉冲数,除以时间就能得到转速。
6.5 定时器开关
⑧ 使能/失能定时器
void TIM_Cmd(TIM_TypeDef* TIMx, FunctionalState NewState);
// 解析:TIMx —— 指定定时器
// NewState —— ENABLE(开启)或 DISABLE(关闭)
// 功能:定时器的总开关,所有配置完成后最后调用
七、完整代码实战:编码器接口测速

下面是基于 TIM3 编码器接口(PA6-A相,PA7-B相) 的完整代码。我把每一行都加上了详细解析,方便大家对照理解。
7.1 编码器接口初始化函数
#include "stm32f10x.h"
/**
* @brief 编码器接口初始化(TIM3,PA6-CH1/A相,PA7-CH2/B相)
* @param 无
* @retval 无
* @note 适用 STM32F103C8T6,标准库 V3.5.0
* 使用四倍频模式(TI1+TI2),最大量程
*/
void Encoder_Init(void)
{
/* ========== 第 1 步:RCC 开启时钟 ========== */
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); // 解析:开启 TIM3 时钟,TIM3 在 APB1 总线上
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // 解析:开启 GPIOA 时钟,PA6、PA7 需要它
/* ========== 第 2 步:GPIO 初始化 ========== */
GPIO_InitTypeDef GPIO_InitStructure; // 解析:定义 GPIO 初始化结构体
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; // 解析:配置上拉输入,防止引脚浮空导致误触发
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7; // 解析:选择 PA6 和 PA7,分别对应 TIM3_CH1 和 TIM3_CH2
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; // 解析:输入模式下此参数不影响,但习惯性写上
GPIO_Init(GPIOA, &GPIO_InitStructure); // 解析:调用初始化函数,将配置写入 PA6 和 PA7
/* ========== 第 3 步:配置时基单元 ========== */
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure; // 解析:定义时基初始化结构体
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1; // 解析:时钟不分频,滤波器采样频率最高
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up; // 解析:向上计数模式,编码器接口会自动覆盖方向
TIM_TimeBaseInitStructure.TIM_Period = 65536 - 1; // 解析:ARR = 65535,最大量程,防止编码器转多了溢出
TIM_TimeBaseInitStructure.TIM_Prescaler = 1 - 1; // 解析:【关键】PSC = 0,编码器脉冲直接作为 CNT 时钟,不分频!
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0; // 解析:重复计数器,仅高级定时器有效,通用定时器写 0
TIM_TimeBaseInit(TIM3, &TIM_TimeBaseInitStructure); // 解析:将时基参数写入 TIM3 寄存器
/* ========== 第 4 步:配置输入捕获单元(滤波器) ========== */
TIM_ICInitTypeDef TIM_ICInitStructure; // 解析:定义输入捕获结构体
TIM_ICStructInit(&TIM_ICInitStructure); // 解析:先给结构体赋默认值,防止未定义成员捣乱
TIM_ICInitStructure.TIM_Channel = TIM_Channel_1; // 解析:配置通道 1(对应 PA6 / A相)
TIM_ICInitStructure.TIM_ICFilter = 0xF; // 解析:输入滤波器设为最大(0xF),滤除机械抖动产生的毛刺
TIM_ICInit(TIM3, &TIM_ICInitStructure); // 解析:将通道 1 配置写入 TIM3
TIM_ICInitStructure.TIM_Channel = TIM_Channel_2; // 解析:配置通道 2(对应 PA7 / B相)
TIM_ICInitStructure.TIM_ICFilter = 0xF; // 解析:同样配置最大滤波
TIM_ICInit(TIM3, &TIM_ICInitStructure); // 解析:将通道 2 配置写入 TIM3
/* ========== 第 5 步:配置编码器接口模式 ========== */
TIM_EncoderInterfaceConfig(TIM3, TIM_EncoderMode_TI12, // 解析:【核心】选择 TI1+TI2 模式,A相和B相的边沿都计数(四倍频)
TIM_ICPolarity_Rising, // 解析:通道 1 极性不反相,上升沿有效
TIM_ICPolarity_Rising); // 解析:通道 2 极性不反相,上升沿有效
/* ========== 第 6 步:开启定时器 ========== */
TIM_Cmd(TIM3, ENABLE); // 解析:启动 TIM3,编码器接口开始工作,硬件自动计数
}
7.2 编码器数值获取函数
/**
* @brief 获取编码器当前计数值并清零
* @param 无
* @retval int16_t 编码器变化量,正数表示正转,负数表示反转
* @note 建议在固定时间间隔(如 10ms/100ms)调用此函数来测速
*/
int16_t Encoder_Get(void)
{
int16_t Temp; // 解析:定义有符号变量,用来保存带方向的结果
Temp = TIM_GetCounter(TIM3); // 解析:读取当前 CNT 值(0~65535)
// 正转时 CNT 增加,反转时 CNT 减小(向下计数)
TIM_SetCounter(TIM3, 0); // 解析:将 CNT 清零,为下一次采样做准备
return Temp; // 解析:返回本次采样到的脉冲数(带符号)
// 注意:uint16_t 转 int16_t,0~32767 为正,32768~65535 为负
}
💡 我的理解:这里返回
int16_t非常关键。因为TIM_GetCounter返回的是uint16_t,但编码器反转时 CNT 是向下减的。如果当前 CNT 值从 0 减到了 65535(借位),直接当无符号数读就是 65535,显然不对。强制转成int16_t后,65535 就变成了 -1,正好表示反转了 1 个脉冲。这个类型转换的细节江科大在视频里强调过。
7.3 主函数调用示例(测速框架)
#include "stm32f10x.h"
#include "OLED.h"
int main(void)
{
OLED_Init(); // 解析:初始化 OLED,用于显示转速
Encoder_Init(); // 解析:初始化编码器接口
int16_t Speed; // 解析:定义速度变量,有符号表示方向
while (1)
{
Delay_ms(100); // 解析:延时 100ms,作为"闸门时间"
Speed = Encoder_Get(); // 解析:获取这 100ms 内编码器的脉冲变化量
// 解析:在 OLED 上显示速度,正数为正转,负数为反转
OLED_ShowSignedNum(1, 1, Speed, 5);
}
}
💡 我的理解:这个
Delay_ms(100)+Encoder_Get()的组合就是最简单的测速法——测频法。每 100ms 读一次 CNT,读出来的值就是这段时间内的脉冲数。
更详细代码:STM32_Project: 基于江科大的STM32的教程编写的程序
八、我的理解
第一次学编码器接口的时候,我最大的困惑是:它到底"借"了输入捕获的哪些资源? 看了江科大的视频和参考手册才搞清楚——编码器接口只借用了输入捕获前端的 滤波器 和 边沿检测,后面的预分频器、CCR 寄存器它一概不用。所以配置输入捕获的时候,你不需要关心通道选择、预分频这些,只要配好滤波器就行。
还有一个让我恍然大悟的点:编码器接口相当于"带有方向控制的外部时钟"。普通的外部时钟模式(External Clock Mode 1)只能让 CNT 单向递增,但编码器接口能根据 A、B 相的相位关系自动决定是递增还是递减。这意味着你不需要软件判断方向,硬件全帮你做了!
关于四倍频,我之前一直以为是软件做的,后来才明白是硬件天然支持的。因为编码器接口在
TI12模式下,A 相的上升沿、下降沿和 B 相的上升沿、下降沿都会触发计数,一圈 100 线的编码器就能计 400 次,分辨率直接翻倍再翻倍。对于需要精确控制的场景(比如 PID 调速),四倍频太重要了。
九、踩坑记录
踩坑点 现象 原因 解决方法 1. PSC 设了分频 编码器转很多圈,CNT 只增加一点点 编码器脉冲被 PSC 分频了,大部分脉冲被"吞"了 PSC 必须设为 0(1-1),编码器脉冲直接进 CNT 2. ARR 设得太小 编码器转快了之后数值突然跳变 CNT 溢出,从 65535 回到 0 ARR 设为 65536 - 1(最大),并定期清零3. 只用了一相计数 测速精度不够,低速时抖动大 用了 TIM_EncoderMode_TI1,只在一相边沿计数改用 TIM_EncoderMode_TI12,四倍频提高分辨率4. GPIO 模式选错 CNT 乱跳,数值不稳定 用了浮空输入,引脚受干扰 改成 上拉输入(IPU) 或 下拉输入(IPD) 5. 返回类型用 uint16_t 反转时显示超大数值(如 65530) 反转时 CNT 向下减,借位后无符号数解读错误 Encoder_Get返回 int16_t,强制转换带符号最惨痛的一次:我把编码器的 A、B 相接反了,结果电机正转时 OLED 显示负数,反转显示正数。排查了半天代码,以为是极性配错了,最后发现是物理线接反了……从那以后,我接编码器之前一定会先确认 A 相和 B 相的定义。
十、总结表格
| 项目 | 说明 |
|---|---|
| 编码器接口本质 | 定时器内部硬件模块,自动接收正交信号并控制 CNT 计数 |
| 占用资源 | 仅占用 CH1(PA6)和 CH2(PA7),借用输入捕获前端滤波器和边沿检测 |
| 辨向原理 | A、B 相 90° 相位差,边沿触发时根据"另一相电平"判断方向 |
| 四倍频 | TIM_EncoderMode_TI12 模式,A、B 相所有边沿都计数,分辨率 ×4 |
| 核心函数 | TIM_EncoderInterfaceConfig(一键配置)、TIM_GetCounter(读数)、TIM_SetCounter(清零) |
| PSC 配置 | 必须设为 0(不分频),否则脉冲被吞 |
| ARR 配置 | 设为 65535(最大量程),防止溢出 |
| 测速方法 | 固定闸门时间读 CNT 并清零,脉冲数 / 时间 = 速度 |
| 返回类型 | 用 int16_t 保存 CNT 值,才能正确表示正反转方向 |
| 适用边界 | 标准库 V3.5.0 + STM32F103C8T6;增量式正交编码器 |
| 注意事项 | 必须共地!PSC 必须为 0!ARR 必须最大!返回必须用有符号数! |
十一、结语
写这篇文章的时候,我把江科大的视频又回顾了一遍,重新梳理了编码器接口在定时器框图中的位置和它的数据通路。编码器接口真的是 STM32 定时器里一个非常贴心的设计——它把"数脉冲"这种简单但频繁的任务完全交给硬件,让 CPU 去干更重要的事情。
从外部中断手动计数,到编码器接口硬件自动计数,这就是用硬件资源换软件资源的典型例子。理解了这一点,你就能明白为什么 STM32 要设计这么多专用硬件模块——它们不是为了炫技,而是为了让你写更少的代码、跑更稳的程序。
希望这篇文章能帮你理清编码器接口的思路。如果你在接编码器或者测速的过程中遇到了其他问题,欢迎在评论区交流,我们一起踩坑、一起进步!
📌 开发环境:Keil5 + STM32F103C8T6最小系统板 + ST-Link
📌 参考资料:江科大STM32入门教程、《STM32F10xxx参考手册》
📌 前置文章:江科大STM32入门:TIM输入捕获之PWMI模式——用一根线同时测频率和占空比
如果本文对你有帮助,欢迎点赞 👍 收藏 ⭐ !有任何问题欢迎在评论区交流,一起学习进步!

更多推荐


所有评论(0)