16_TIM编码器接口测速

TIM 编码器接口测速

对应课程:6-8 编码器接口测速

课堂转写:2026-09-04 18_10 记录 编码器测速_原文.docx

整理日期:2026-09-04

芯片与开发方式:STM32F103C8T6、标准外设库

本节边界:讲清正交编码器、TIM 编码器接口、位置计数、定时闸门测速和源码配置;PID、电机驱动及闭环控制只作为后续应用背景。

1. 这节课完成了什么

这节课把上一章"外部中断中手动判断编码器方向并加减变量"的工作,交给了 TIM3 的编码器接口硬件:

  1. 编码器 A、B 两相信号接入 TIM3_CH1 和 TIM3_CH2。
  2. TIM3 根据两相的先后关系自动判断方向。
  3. 每个有效边沿到来时,硬件自动让 CNT 加 1 或减 1。
  4. 直接读取 CNT 可以得到位置增量。
  5. 每隔固定时间读取并清零 CNT,可以得到带正负方向的速度。
  6. 最终用 TIM2 每 1 秒产生一次更新中断,在中断中读取速度,主循环只负责 OLED 显示。

#mermaid-svg-y7uHdyi3DrOn5AW4{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-y7uHdyi3DrOn5AW4 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-y7uHdyi3DrOn5AW4 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-y7uHdyi3DrOn5AW4 .error-icon{fill:#552222;}#mermaid-svg-y7uHdyi3DrOn5AW4 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-y7uHdyi3DrOn5AW4 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-y7uHdyi3DrOn5AW4 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-y7uHdyi3DrOn5AW4 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-y7uHdyi3DrOn5AW4 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-y7uHdyi3DrOn5AW4 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-y7uHdyi3DrOn5AW4 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-y7uHdyi3DrOn5AW4 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-y7uHdyi3DrOn5AW4 .marker.cross{stroke:#333333;}#mermaid-svg-y7uHdyi3DrOn5AW4 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-y7uHdyi3DrOn5AW4 p{margin:0;}#mermaid-svg-y7uHdyi3DrOn5AW4 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-y7uHdyi3DrOn5AW4 .cluster-label text{fill:#333;}#mermaid-svg-y7uHdyi3DrOn5AW4 .cluster-label span{color:#333;}#mermaid-svg-y7uHdyi3DrOn5AW4 .cluster-label span p{background-color:transparent;}#mermaid-svg-y7uHdyi3DrOn5AW4 .label text,#mermaid-svg-y7uHdyi3DrOn5AW4 span{fill:#333;color:#333;}#mermaid-svg-y7uHdyi3DrOn5AW4 .node rect,#mermaid-svg-y7uHdyi3DrOn5AW4 .node circle,#mermaid-svg-y7uHdyi3DrOn5AW4 .node ellipse,#mermaid-svg-y7uHdyi3DrOn5AW4 .node polygon,#mermaid-svg-y7uHdyi3DrOn5AW4 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-y7uHdyi3DrOn5AW4 .rough-node .label text,#mermaid-svg-y7uHdyi3DrOn5AW4 .node .label text,#mermaid-svg-y7uHdyi3DrOn5AW4 .image-shape .label,#mermaid-svg-y7uHdyi3DrOn5AW4 .icon-shape .label{text-anchor:middle;}#mermaid-svg-y7uHdyi3DrOn5AW4 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-y7uHdyi3DrOn5AW4 .rough-node .label,#mermaid-svg-y7uHdyi3DrOn5AW4 .node .label,#mermaid-svg-y7uHdyi3DrOn5AW4 .image-shape .label,#mermaid-svg-y7uHdyi3DrOn5AW4 .icon-shape .label{text-align:center;}#mermaid-svg-y7uHdyi3DrOn5AW4 .node.clickable{cursor:pointer;}#mermaid-svg-y7uHdyi3DrOn5AW4 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-y7uHdyi3DrOn5AW4 .arrowheadPath{fill:#333333;}#mermaid-svg-y7uHdyi3DrOn5AW4 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-y7uHdyi3DrOn5AW4 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-y7uHdyi3DrOn5AW4 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-y7uHdyi3DrOn5AW4 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-y7uHdyi3DrOn5AW4 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-y7uHdyi3DrOn5AW4 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-y7uHdyi3DrOn5AW4 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-y7uHdyi3DrOn5AW4 .cluster text{fill:#333;}#mermaid-svg-y7uHdyi3DrOn5AW4 .cluster span{color:#333;}#mermaid-svg-y7uHdyi3DrOn5AW4 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-y7uHdyi3DrOn5AW4 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-y7uHdyi3DrOn5AW4 rect.text{fill:none;stroke-width:0;}#mermaid-svg-y7uHdyi3DrOn5AW4 .icon-shape,#mermaid-svg-y7uHdyi3DrOn5AW4 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-y7uHdyi3DrOn5AW4 .icon-shape p,#mermaid-svg-y7uHdyi3DrOn5AW4 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-y7uHdyi3DrOn5AW4 .icon-shape .label rect,#mermaid-svg-y7uHdyi3DrOn5AW4 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-y7uHdyi3DrOn5AW4 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-y7uHdyi3DrOn5AW4 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-y7uHdyi3DrOn5AW4 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 编码器旋转
A B 两相输出正交方波
PA6 PA7 输入 TIM3
滤波和极性处理
TIM3 编码器接口判断方向
CNT 自动加一或减一
TIM2 到达固定闸门时间
读取 CNT 到 Speed
将 CNT 清零
OLED 显示有符号速度

课堂顺序:

时间 老师讲解内容
00:00~02:21 展示现象,并比较硬件编码器接口与外部中断计次
02:35~05:24 编码器接口定义、硬件资源和适用场景
05:28~09:09 正交编码器波形、方向判断和抗噪思路
09:09~17:36 TIM 内部结构、三种模式、极性反相及手册示例
17:58~20:43 接线和代码整体规划
20:43~26:07 完成 Encoder_Init()
26:13~29:03 读取 CNT、补码转有符号数、调整方向
29:06~31:10 固定时间读取并清零,使用 TIM2 中断测速

2. 为什么不用 EXTI 中断手动计数

上一节旋转编码器计次的做法是:边沿触发 EXTI,CPU 进入中断,读取另一相信号,再手动执行 ++ 或 --。低速旋钮完全可以这样做,但电机高速旋转时可能每秒产生几千到几万个有效边沿,CPU 会频繁进入中断,只为完成简单加减。

TIM 编码器接口把这件事变成硬件数据通路:边沿、方向判断和 CNT 加减都不需要 CPU 逐次参与。CPU 只需低频地读取结果,因此:

  • 软件中断负担更小;
  • 高速脉冲下不容易因中断处理不及时而漏计;
  • 正反方向直接体现在 CNT 增减中;
  • 代价是独占一个高级或通用定时器,以及它的 CH1、CH2 两个输入通道。

图 1 老师 PPT 第 80 页:编码器接口能根据正交信号自动控制 CNT 自增或自减。

每个高级定时器或通用定时器只有一个编码器接口,基本定时器没有。一个定时器配置成编码器模式后,其 CH1、CH2 和计数器已被占用,不能再同时按普通 PWM 或输入捕获方式随意使用。

3. 正交编码器为什么既能测速度又能判方向

3.1 两相相差 90 度

正交编码器输出 A、B 两路方波,两者相位相差约 90°:

  • 一个方向旋转时,A 相领先 B 相;
  • 反方向旋转时,B 相领先 A 相。

因此:

  • 跳变出现得越频繁,旋转越快;
  • 谁先跳变,表示旋转方向;
  • 停止旋转时没有新边沿,速度为 0。

图 2 老师 PPT 第 81 页:正转和反转时,A、B 相的领先关系相反。

3.2 一个边沿怎样决定加还是减

编码器接口在某一路发生边沿时,同时查看另一路当前的高低电平。边沿种类和另一相电平的组合能唯一判断运动方向。

例如在 PPT 约定的一个正转方向中:

当前边沿 另一相信号 CNT 动作
A 上升沿 B 为低 加 1
B 上升沿 A 为高 加 1
A 下降沿 B 为高 加 1
B 下降沿 A 为低 加 1

反转时,以上组合全部相反,CNT 改为减 1。这里的"正转"只是人为约定;交换 A/B 接线或反相任意一路,正负方向都会反过来。

3.3 为什么一个机械格可能得到 4 个计数

课程使用 TIM_EncoderMode_TI12,A、B 两相的上升沿和下降沿都参与计数。一个完整正交周期包含四次状态变化,所以常见的 x4 解码会得到 4 个计数。

课堂实测旋钮转过一格,CNT 变化约 4,正是因为这一格内 A、B 两相各出现一次上升沿和一次下降沿。

数据手册或编码器商品页写的 PPR、CPR、线数可能采用不同口径。有的表示每相每转周期数,有的已经包含 x4 计数。换算转速前必须先确认厂商定义,最稳妥的方法是让轴实际转一整圈并读取总计数。

4. TIM 编码器接口内部信号怎样走

图 3 老师 PPT 第 82 页:CH1、CH2 借用输入捕获前端,经滤波和极性处理后进入编码器接口。
#mermaid-svg-AzCAuwjEJfdeKO0Y{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-AzCAuwjEJfdeKO0Y .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-AzCAuwjEJfdeKO0Y .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-AzCAuwjEJfdeKO0Y .error-icon{fill:#552222;}#mermaid-svg-AzCAuwjEJfdeKO0Y .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-AzCAuwjEJfdeKO0Y .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-AzCAuwjEJfdeKO0Y .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-AzCAuwjEJfdeKO0Y .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-AzCAuwjEJfdeKO0Y .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-AzCAuwjEJfdeKO0Y .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-AzCAuwjEJfdeKO0Y .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-AzCAuwjEJfdeKO0Y .marker{fill:#333333;stroke:#333333;}#mermaid-svg-AzCAuwjEJfdeKO0Y .marker.cross{stroke:#333333;}#mermaid-svg-AzCAuwjEJfdeKO0Y svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-AzCAuwjEJfdeKO0Y p{margin:0;}#mermaid-svg-AzCAuwjEJfdeKO0Y .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-AzCAuwjEJfdeKO0Y .cluster-label text{fill:#333;}#mermaid-svg-AzCAuwjEJfdeKO0Y .cluster-label span{color:#333;}#mermaid-svg-AzCAuwjEJfdeKO0Y .cluster-label span p{background-color:transparent;}#mermaid-svg-AzCAuwjEJfdeKO0Y .label text,#mermaid-svg-AzCAuwjEJfdeKO0Y span{fill:#333;color:#333;}#mermaid-svg-AzCAuwjEJfdeKO0Y .node rect,#mermaid-svg-AzCAuwjEJfdeKO0Y .node circle,#mermaid-svg-AzCAuwjEJfdeKO0Y .node ellipse,#mermaid-svg-AzCAuwjEJfdeKO0Y .node polygon,#mermaid-svg-AzCAuwjEJfdeKO0Y .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-AzCAuwjEJfdeKO0Y .rough-node .label text,#mermaid-svg-AzCAuwjEJfdeKO0Y .node .label text,#mermaid-svg-AzCAuwjEJfdeKO0Y .image-shape .label,#mermaid-svg-AzCAuwjEJfdeKO0Y .icon-shape .label{text-anchor:middle;}#mermaid-svg-AzCAuwjEJfdeKO0Y .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-AzCAuwjEJfdeKO0Y .rough-node .label,#mermaid-svg-AzCAuwjEJfdeKO0Y .node .label,#mermaid-svg-AzCAuwjEJfdeKO0Y .image-shape .label,#mermaid-svg-AzCAuwjEJfdeKO0Y .icon-shape .label{text-align:center;}#mermaid-svg-AzCAuwjEJfdeKO0Y .node.clickable{cursor:pointer;}#mermaid-svg-AzCAuwjEJfdeKO0Y .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-AzCAuwjEJfdeKO0Y .arrowheadPath{fill:#333333;}#mermaid-svg-AzCAuwjEJfdeKO0Y .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-AzCAuwjEJfdeKO0Y .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-AzCAuwjEJfdeKO0Y .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-AzCAuwjEJfdeKO0Y .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-AzCAuwjEJfdeKO0Y .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-AzCAuwjEJfdeKO0Y .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-AzCAuwjEJfdeKO0Y .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-AzCAuwjEJfdeKO0Y .cluster text{fill:#333;}#mermaid-svg-AzCAuwjEJfdeKO0Y .cluster span{color:#333;}#mermaid-svg-AzCAuwjEJfdeKO0Y div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-AzCAuwjEJfdeKO0Y .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-AzCAuwjEJfdeKO0Y rect.text{fill:none;stroke-width:0;}#mermaid-svg-AzCAuwjEJfdeKO0Y .icon-shape,#mermaid-svg-AzCAuwjEJfdeKO0Y .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-AzCAuwjEJfdeKO0Y .icon-shape p,#mermaid-svg-AzCAuwjEJfdeKO0Y .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-AzCAuwjEJfdeKO0Y .icon-shape .label rect,#mermaid-svg-AzCAuwjEJfdeKO0Y .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-AzCAuwjEJfdeKO0Y .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-AzCAuwjEJfdeKO0Y .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-AzCAuwjEJfdeKO0Y :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 编码器 A 相
编码器 B 相
PA6 TIM3_CH1
PA7 TIM3_CH2
通道 1 数字滤波
通道 2 数字滤波
TI1 极性是否反相
TI2 极性是否反相
TI1FP1
TI2FP2
编码器接口逻辑
产生计数时钟
决定向上或向下计数
TIM3 CNT
ARR 决定回绕范围

编码器模式借用了输入捕获通道的前半段:

  • 使用 CH1、CH2 对应的 GPIO 输入;
  • 使用数字滤波器 IC1F、IC2F;
  • 使用输入极性 CC1P、CC2P;
  • 产生 TI1FP1、TI2FP2 送入编码器逻辑。

但普通输入捕获后半段的捕获预分频和 CCR 锁存并不是本节的核心。编码器接口直接控制 CNT 的计数脉冲和方向。

4.1 哪些时基参数仍然有效

参数 编码器模式中是否关键 原因
TIM_CounterMode 代码要赋值,但运行方向由编码器接口控制 正转/反转会动态控制 CNT 向上或向下
内部 72 MHz 时钟 不作为 CNT 的计数脉冲 CNT 的"时钟"来自 A/B 相有效边沿
PSC 可对编码器计数脉冲分频,课程设为 0 即不分频 每个有效计数事件直接进入 CNT
ARR 有效 决定 CNT 的回绕范围
CNT 有效 保存位置或一个时间窗内的增量
CKD 有效地影响数字滤波采样时钟 和 ICxF 一起决定抗毛刺能力与响应速度

课堂把 ARR 设为 65536-1,使 16 位计数器使用完整范围;PSC 设为 1-1=0,表示不再额外分频。

5. 三种编码器工作模式

图 4 老师 PPT 第 83 页:仅 TI1、仅 TI2、TI1 和 TI2 都计数三种模式。

标准库参数 SMCR.SMS 哪些边沿用于计数 特点
TIM_EncoderMode_TI1 001 只使用 TI1 的两个边沿 分辨率较低,TI2 仍用于判方向
TIM_EncoderMode_TI2 010 只使用 TI2 的两个边沿 分辨率较低,TI1 仍用于判方向
TIM_EncoderMode_TI12 011 TI1、TI2 的两个边沿都计数 x4 解码,分辨率最高,课程采用

即使"仅在 TI1 计数",TI2 也不是完全没用,它仍用于判断 TI1 边沿应该让 CNT 加还是减。

参考手册《STM32F10xxx参考手册(中文)》编码器接口模式见 PDF 第 274~275 页;其中的计数方向表和时序示例对应 PPT 第 83~85 页。编码器模式主要由 SMCR.SMS 选择,输入滤波和极性仍由 CCMR1、CCER 配置。

6. 极性参数为什么不再等于只选某个边沿

在普通输入捕获中,TIM_ICPolarity_Rising/Falling 通常表示选择上升沿或下降沿捕获。但编码器模式必须识别两相的上升沿和下降沿,所以这里的含义变成:

  • TIM_ICPolarity_Rising:输入不反相;
  • TIM_ICPolarity_Falling:输入反相后再交给编码器接口。

图 5 老师 PPT 第 84 页:TI1、TI2 均不反相时的计数方向。

图 6 老师 PPT 第 85 页:只反相 TI1 后,计数方向整体翻转。

改变方向有三种常见办法:

  1. 交换 A、B 两根输入线;
  2. 软件中将 TI1 或 TI2 的一个极性参数改为 Falling;
  3. 读取结果后乘以 -1。

反相任意一路会翻转方向;两路同时反相通常不会改变两相的相对先后关系,因此计数方向保持不变。

6.1 配置覆盖顺序为什么重要

TIM_ICInit() 和 TIM_EncoderInterfaceConfig() 都会写 CCER 中的极性位。课程先初始化输入通道,再配置编码器接口:

c 复制代码
TIM_ICInit(TIM3, &TIM_ICInitStructure);
TIM_EncoderInterfaceConfig(
    TIM3,
    TIM_EncoderMode_TI12,
    TIM_ICPolarity_Rising,
    TIM_ICPolarity_Rising);

后调用的函数覆盖前面的同一位。因此源码注释强调 TIM_EncoderInterfaceConfig() 必须放在输入捕获初始化之后,否则后续 TIM_ICInit() 可能把编码器接口设置的极性重新覆盖。

7. 硬件接线为什么是 PA6 和 PA7

图 7 课程接线图:编码器 A、B 相连接 PA6、PA7,VCC/GND 接电源;OLED 接线沿用前面课程。

STM32F103x8/B 数据手册 PDF 第 18 页给出默认复用功能:

  • PA6 = TIM3_CH1;
  • PA7 = TIM3_CH2。

本节使用 TIM3 编码器接口,所以两相信号必须接入 TIM3 的 CH1 和 CH2;不能随意换成 PB0/PB1,因为 TIM3_CH3/CH4 不属于编码器接口输入。

A、B 接 PA6/PA7 的次序可以互换,但正负方向随之翻转。输入采用上拉,是因为课程旋转编码器模块的空闲状态为高电平;实际模块若已有可靠推挽输出,应根据模块电路和默认电平选择上拉、下拉或浮空输入。

8. Encoder_Init() 的代码配置流程

8.1 纵向流程图

#mermaid-svg-0yPoU0lTeL9BG9YK{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-0yPoU0lTeL9BG9YK .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-0yPoU0lTeL9BG9YK .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-0yPoU0lTeL9BG9YK .error-icon{fill:#552222;}#mermaid-svg-0yPoU0lTeL9BG9YK .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-0yPoU0lTeL9BG9YK .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-0yPoU0lTeL9BG9YK .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-0yPoU0lTeL9BG9YK .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-0yPoU0lTeL9BG9YK .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-0yPoU0lTeL9BG9YK .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-0yPoU0lTeL9BG9YK .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-0yPoU0lTeL9BG9YK .marker{fill:#333333;stroke:#333333;}#mermaid-svg-0yPoU0lTeL9BG9YK .marker.cross{stroke:#333333;}#mermaid-svg-0yPoU0lTeL9BG9YK svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-0yPoU0lTeL9BG9YK p{margin:0;}#mermaid-svg-0yPoU0lTeL9BG9YK .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-0yPoU0lTeL9BG9YK .cluster-label text{fill:#333;}#mermaid-svg-0yPoU0lTeL9BG9YK .cluster-label span{color:#333;}#mermaid-svg-0yPoU0lTeL9BG9YK .cluster-label span p{background-color:transparent;}#mermaid-svg-0yPoU0lTeL9BG9YK .label text,#mermaid-svg-0yPoU0lTeL9BG9YK span{fill:#333;color:#333;}#mermaid-svg-0yPoU0lTeL9BG9YK .node rect,#mermaid-svg-0yPoU0lTeL9BG9YK .node circle,#mermaid-svg-0yPoU0lTeL9BG9YK .node ellipse,#mermaid-svg-0yPoU0lTeL9BG9YK .node polygon,#mermaid-svg-0yPoU0lTeL9BG9YK .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-0yPoU0lTeL9BG9YK .rough-node .label text,#mermaid-svg-0yPoU0lTeL9BG9YK .node .label text,#mermaid-svg-0yPoU0lTeL9BG9YK .image-shape .label,#mermaid-svg-0yPoU0lTeL9BG9YK .icon-shape .label{text-anchor:middle;}#mermaid-svg-0yPoU0lTeL9BG9YK .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-0yPoU0lTeL9BG9YK .rough-node .label,#mermaid-svg-0yPoU0lTeL9BG9YK .node .label,#mermaid-svg-0yPoU0lTeL9BG9YK .image-shape .label,#mermaid-svg-0yPoU0lTeL9BG9YK .icon-shape .label{text-align:center;}#mermaid-svg-0yPoU0lTeL9BG9YK .node.clickable{cursor:pointer;}#mermaid-svg-0yPoU0lTeL9BG9YK .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-0yPoU0lTeL9BG9YK .arrowheadPath{fill:#333333;}#mermaid-svg-0yPoU0lTeL9BG9YK .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-0yPoU0lTeL9BG9YK .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-0yPoU0lTeL9BG9YK .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-0yPoU0lTeL9BG9YK .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-0yPoU0lTeL9BG9YK .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-0yPoU0lTeL9BG9YK .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-0yPoU0lTeL9BG9YK .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-0yPoU0lTeL9BG9YK .cluster text{fill:#333;}#mermaid-svg-0yPoU0lTeL9BG9YK .cluster span{color:#333;}#mermaid-svg-0yPoU0lTeL9BG9YK div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-0yPoU0lTeL9BG9YK .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-0yPoU0lTeL9BG9YK rect.text{fill:none;stroke-width:0;}#mermaid-svg-0yPoU0lTeL9BG9YK .icon-shape,#mermaid-svg-0yPoU0lTeL9BG9YK .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-0yPoU0lTeL9BG9YK .icon-shape p,#mermaid-svg-0yPoU0lTeL9BG9YK .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-0yPoU0lTeL9BG9YK .icon-shape .label rect,#mermaid-svg-0yPoU0lTeL9BG9YK .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-0yPoU0lTeL9BG9YK .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-0yPoU0lTeL9BG9YK .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-0yPoU0lTeL9BG9YK :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 进入 Encoder_Init
1 开启 TIM3 和 GPIOA 时钟
2 PA6 PA7 配置为上拉输入
3 配置 TIM3 时基
ARR 等于 65535
PSC 等于 0
4 初始化输入捕获结构体默认值
5 配 CH1 的数字滤波器
6 复用结构体配 CH2 滤波器
7 选择 TI12 编码器模式
8 设置 TI1 TI2 是否反相
9 启动 TIM3
编码器边沿自动控制 CNT

8.2 完整初始化代码与因果解释

c 复制代码
void Encoder_Init(void)
{
    /* 1. 给 TIM3 和 GPIOA 供时钟 */
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);

    /* 2. 接通 A/B 两相信号入口 */
    GPIO_InitTypeDef GPIO_InitStructure;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &GPIO_InitStructure);

    /* 3. 配置 CNT 范围和编码器脉冲分频 */
    TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
    TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
    TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;
    TIM_TimeBaseInitStructure.TIM_Period = 65536 - 1;
    TIM_TimeBaseInitStructure.TIM_Prescaler = 1 - 1;
    TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;
    TIM_TimeBaseInit(TIM3, &TIM_TimeBaseInitStructure);

    /* 4. CH1、CH2 只使用输入捕获前端的滤波等功能 */
    TIM_ICInitTypeDef TIM_ICInitStructure;
    TIM_ICStructInit(&TIM_ICInitStructure);

    TIM_ICInitStructure.TIM_Channel = TIM_Channel_1;
    TIM_ICInitStructure.TIM_ICFilter = 0xF;
    TIM_ICInit(TIM3, &TIM_ICInitStructure);

    TIM_ICInitStructure.TIM_Channel = TIM_Channel_2;
    TIM_ICInitStructure.TIM_ICFilter = 0xF;
    TIM_ICInit(TIM3, &TIM_ICInitStructure);

    /* 5. 两相四边沿计数;两路都不反相 */
    TIM_EncoderInterfaceConfig(
        TIM3,
        TIM_EncoderMode_TI12,
        TIM_ICPolarity_Rising,
        TIM_ICPolarity_Rising);

    /* 6. 全部配置完成后启动 */
    TIM_Cmd(TIM3, ENABLE);
}

8.3 为什么要先调用 TIM_ICStructInit()

本节对 TIM_ICInitTypeDef 没有逐项完整赋值,只修改了通道和滤波器。若结构体中的其他成员保持栈上的随机值,TIM_ICInit() 可能把不可预测的参数写入寄存器。

c 复制代码
TIM_ICStructInit(&TIM_ICInitStructure);

它先填入标准默认值,再局部修改需要的字段。配置完 CH1 后,同一个结构体可以改通道号后复用于 CH2,因为每次 TIM_ICInit() 都会立即把当前结构体内容写入对应硬件寄存器。

9. 函数和硬件模块对照表

函数 什么时候调用 对应模块或寄存器 本节作用
RCC_APB1PeriphClockCmd() 初始化第一步 RCC APB1ENR 开启 TIM3 或 TIM2 时钟
RCC_APB2PeriphClockCmd() 初始化第一步 RCC APB2ENR 开启 GPIOA 时钟
GPIO_Init() RCC 之后 GPIOA_CRL 将 PA6、PA7 配为上拉输入
TIM_TimeBaseInit() GPIO 后 CR1、PSC、ARR 等 设置 CNT 范围、分频和滤波时钟
TIM_ICStructInit() 定义 IC 结构体后 软件结构体 先赋默认值,防止未赋成员不确定
TIM_ICInit() 编码器接口函数前 CCMR1、CCER 配 CH1/CH2 的输入路径与数字滤波
TIM_EncoderInterfaceConfig() 两个通道初始化后 SMCR.SMS、CCMR1、CCER 选择编码器模式及两路极性
TIM_Cmd() 全部配置完成后 CR1.CEN 启动定时器
TIM_GetCounter() 读取位置或速度窗口结束时 CNT 读取累计的有符号增量原始位型
TIM_SetCounter() 速度读取之后 CNT 清零,开始下一个速度窗口
TIM_ITConfig() 配 TIM2 闸门中断时 DIER.UIE 允许更新中断
TIM_GetITStatus() TIM2 中断入口 SR.UIF、DIER.UIE 判断是否为更新事件
TIM_ClearITPendingBit() 中断处理末尾 SR.UIF 清除中断标志,避免反复进入

关键寄存器可以归为四组:

  1. 输入整形 :GPIOA_CRL、CCMR1.IC1F/IC2F、CCER.CC1P/CC2P。
  2. 模式选择 :SMCR.SMS。
  3. 计数结果 :CNT、ARR、PSC。
  4. 测速闸门 :TIM2 的 PSC、ARR、DIER、SR。

10. 为什么 uint16_t 转 int16_t 能得到负数

TIM3 的 CNT 是 16 位寄存器。设 ARR=65535:

  • 从 0 向上:0、1、2、3......
  • 从 0 向下:0、65535、65534、65533......

硬件寄存器中的 16 位位型不分"有符号"和"无符号"。软件把相同位型解释为 int16_t 时,按二进制补码得到:

CNT 位型 uint16_t 解释 int16_t 解释
0x0001 1 1
0x0000 0 0
0xFFFF 65535 -1
0xFFFE 65534 -2

课程接口因此定义为:

c 复制代码
int16_t Encoder_Get(void)
{
    int16_t Temp;
    Temp = TIM_GetCounter(TIM3);
    TIM_SetCounter(TIM3, 0);
    return Temp;
}

必须先把 CNT 保存到 Temp,再清零;若先清零再读取,返回值永远是 0。

这个技巧要求一个采样窗口内的净计数变化不超出 int16_t 可无歧义表达的范围,实际应控制在约 -32768~32767。高速编码器应缩短采样周期,或使用溢出扩展计数。

11. 从计数值到速度的公式

11.1 课程显示值

若闸门时间为 Tg,在这段时间得到有符号计数增量 ΔN:

vcount=ΔNTg v_{count}=\frac{\Delta N}{T_g} vcount=TgΔN

单位是 count/s。本程序 Tg=1 s,所以:

vcount=ΔN v_{count}=\Delta N vcount=ΔN

OLED 上显示 Speed=120,表示这一秒净增加 120 个编码器计数;Speed=-80 表示反方向每秒 80 个计数。

11.2 换算为转每分钟 RPM

设实际测得编码器轴每转总计数为 Crev:

nrpm=ΔNCrev×60Tg n_{rpm}=\frac{\Delta N}{C_{rev}}\times\frac{60}{T_g} nrpm=CrevΔN×Tg60

例:x4 解码后每转 Crev=2400,100 ms 内读到 ΔN=200:

nrpm=2002400×600.1=50 rpm n_{rpm}=\frac{200}{2400}\times\frac{60}{0.1}=50\text{ rpm} nrpm=2400200×0.160=50 rpm

若读到 -200,则为 -50 rpm,负号表示反方向。

若编码器安装在电机轴或减速器输出轴,还要明确 Crev 指哪根轴;经过减速比后,电机轴和输出轴每转计数不同。

11.3 速度分辨率与采样时间的权衡

一个计数对应的 RPM 分辨率为:

Δnrpm=60CrevTg \Delta n_{rpm}=\frac{60}{C_{rev}T_g} Δnrpm=CrevTg60

  • Tg 长:低速分辨率更好,但显示/控制更新慢,CNT 更容易溢出。
  • Tg 短:更新快、适合闭环控制,但低速时计数太少、数值跳动更明显。

12. 为什么最终使用 TIM2 中断作为测速闸门

课堂先用以下做法验证:

c 复制代码
while (1)
{
    Speed = Encoder_Get();
    Delay_ms(1000);
}

它直观,但 Delay_ms(1000) 会阻塞主循环。如果系统还需要刷新界面、通信或控制电机,主循环每次停 1 秒不可接受。

最终方案让 TIM2 每 1 秒产生一次更新中断:
#mermaid-svg-Os6UQ8MH9S7L9VDT{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Os6UQ8MH9S7L9VDT .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Os6UQ8MH9S7L9VDT .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Os6UQ8MH9S7L9VDT .error-icon{fill:#552222;}#mermaid-svg-Os6UQ8MH9S7L9VDT .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Os6UQ8MH9S7L9VDT .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Os6UQ8MH9S7L9VDT .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Os6UQ8MH9S7L9VDT .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Os6UQ8MH9S7L9VDT .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Os6UQ8MH9S7L9VDT .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Os6UQ8MH9S7L9VDT .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Os6UQ8MH9S7L9VDT .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Os6UQ8MH9S7L9VDT .marker.cross{stroke:#333333;}#mermaid-svg-Os6UQ8MH9S7L9VDT svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Os6UQ8MH9S7L9VDT p{margin:0;}#mermaid-svg-Os6UQ8MH9S7L9VDT .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Os6UQ8MH9S7L9VDT .cluster-label text{fill:#333;}#mermaid-svg-Os6UQ8MH9S7L9VDT .cluster-label span{color:#333;}#mermaid-svg-Os6UQ8MH9S7L9VDT .cluster-label span p{background-color:transparent;}#mermaid-svg-Os6UQ8MH9S7L9VDT .label text,#mermaid-svg-Os6UQ8MH9S7L9VDT span{fill:#333;color:#333;}#mermaid-svg-Os6UQ8MH9S7L9VDT .node rect,#mermaid-svg-Os6UQ8MH9S7L9VDT .node circle,#mermaid-svg-Os6UQ8MH9S7L9VDT .node ellipse,#mermaid-svg-Os6UQ8MH9S7L9VDT .node polygon,#mermaid-svg-Os6UQ8MH9S7L9VDT .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Os6UQ8MH9S7L9VDT .rough-node .label text,#mermaid-svg-Os6UQ8MH9S7L9VDT .node .label text,#mermaid-svg-Os6UQ8MH9S7L9VDT .image-shape .label,#mermaid-svg-Os6UQ8MH9S7L9VDT .icon-shape .label{text-anchor:middle;}#mermaid-svg-Os6UQ8MH9S7L9VDT .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Os6UQ8MH9S7L9VDT .rough-node .label,#mermaid-svg-Os6UQ8MH9S7L9VDT .node .label,#mermaid-svg-Os6UQ8MH9S7L9VDT .image-shape .label,#mermaid-svg-Os6UQ8MH9S7L9VDT .icon-shape .label{text-align:center;}#mermaid-svg-Os6UQ8MH9S7L9VDT .node.clickable{cursor:pointer;}#mermaid-svg-Os6UQ8MH9S7L9VDT .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Os6UQ8MH9S7L9VDT .arrowheadPath{fill:#333333;}#mermaid-svg-Os6UQ8MH9S7L9VDT .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Os6UQ8MH9S7L9VDT .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Os6UQ8MH9S7L9VDT .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Os6UQ8MH9S7L9VDT .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Os6UQ8MH9S7L9VDT .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Os6UQ8MH9S7L9VDT .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Os6UQ8MH9S7L9VDT .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Os6UQ8MH9S7L9VDT .cluster text{fill:#333;}#mermaid-svg-Os6UQ8MH9S7L9VDT .cluster span{color:#333;}#mermaid-svg-Os6UQ8MH9S7L9VDT div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Os6UQ8MH9S7L9VDT .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Os6UQ8MH9S7L9VDT rect.text{fill:none;stroke-width:0;}#mermaid-svg-Os6UQ8MH9S7L9VDT .icon-shape,#mermaid-svg-Os6UQ8MH9S7L9VDT .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Os6UQ8MH9S7L9VDT .icon-shape p,#mermaid-svg-Os6UQ8MH9S7L9VDT .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Os6UQ8MH9S7L9VDT .icon-shape .label rect,#mermaid-svg-Os6UQ8MH9S7L9VDT .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Os6UQ8MH9S7L9VDT .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Os6UQ8MH9S7L9VDT .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Os6UQ8MH9S7L9VDT :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} TIM3 持续接收编码器边沿
CNT 自动累计正负增量
TIM2 计满 1 秒
产生更新中断
进入 TIM2_IRQHandler
检查 TIM_IT_Update
Speed 等于 Encoder_Get
先读取 TIM3 CNT
再将 TIM3 CNT 清零
清除 TIM2 更新中断标志
退出中断
主循环持续显示 Speed

TIM2 的参数:

c 复制代码
TIM_TimeBaseInitStructure.TIM_Period = 10000 - 1;
TIM_TimeBaseInitStructure.TIM_Prescaler = 7200 - 1;

若 TIM2 时钟为 72 MHz:

fupdate=72 000 0007200×10000=1 Hz f_{update}=\frac{72\,000\,000}{7200\times10000}=1\text{ Hz} fupdate=7200×1000072000000=1 Hz

所以更新中断间隔正好是 1 秒。

中断代码:

c 复制代码
void TIM2_IRQHandler(void)
{
    if (TIM_GetITStatus(TIM2, TIM_IT_Update) == SET)
    {
        Speed = Encoder_Get();
        TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
    }
}

主循环只显示全局变量:

c 复制代码
while (1)
{
    OLED_ShowSignedNum(1, 7, Speed, 5);
}

Speed 在中断和主循环之间共享。更规范的写法应声明为:

c 复制代码
volatile int16_t Speed;

volatile 告诉编译器该变量可能在正常流程之外被中断修改,主循环每次都应从内存读取最新值。

13. 完整代码运行关系

#mermaid-svg-zZwYHR5mqYTrChsa{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-zZwYHR5mqYTrChsa .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-zZwYHR5mqYTrChsa .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-zZwYHR5mqYTrChsa .error-icon{fill:#552222;}#mermaid-svg-zZwYHR5mqYTrChsa .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-zZwYHR5mqYTrChsa .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-zZwYHR5mqYTrChsa .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-zZwYHR5mqYTrChsa .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-zZwYHR5mqYTrChsa .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-zZwYHR5mqYTrChsa .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-zZwYHR5mqYTrChsa .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-zZwYHR5mqYTrChsa .marker{fill:#333333;stroke:#333333;}#mermaid-svg-zZwYHR5mqYTrChsa .marker.cross{stroke:#333333;}#mermaid-svg-zZwYHR5mqYTrChsa svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-zZwYHR5mqYTrChsa p{margin:0;}#mermaid-svg-zZwYHR5mqYTrChsa .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-zZwYHR5mqYTrChsa .cluster-label text{fill:#333;}#mermaid-svg-zZwYHR5mqYTrChsa .cluster-label span{color:#333;}#mermaid-svg-zZwYHR5mqYTrChsa .cluster-label span p{background-color:transparent;}#mermaid-svg-zZwYHR5mqYTrChsa .label text,#mermaid-svg-zZwYHR5mqYTrChsa span{fill:#333;color:#333;}#mermaid-svg-zZwYHR5mqYTrChsa .node rect,#mermaid-svg-zZwYHR5mqYTrChsa .node circle,#mermaid-svg-zZwYHR5mqYTrChsa .node ellipse,#mermaid-svg-zZwYHR5mqYTrChsa .node polygon,#mermaid-svg-zZwYHR5mqYTrChsa .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-zZwYHR5mqYTrChsa .rough-node .label text,#mermaid-svg-zZwYHR5mqYTrChsa .node .label text,#mermaid-svg-zZwYHR5mqYTrChsa .image-shape .label,#mermaid-svg-zZwYHR5mqYTrChsa .icon-shape .label{text-anchor:middle;}#mermaid-svg-zZwYHR5mqYTrChsa .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-zZwYHR5mqYTrChsa .rough-node .label,#mermaid-svg-zZwYHR5mqYTrChsa .node .label,#mermaid-svg-zZwYHR5mqYTrChsa .image-shape .label,#mermaid-svg-zZwYHR5mqYTrChsa .icon-shape .label{text-align:center;}#mermaid-svg-zZwYHR5mqYTrChsa .node.clickable{cursor:pointer;}#mermaid-svg-zZwYHR5mqYTrChsa .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-zZwYHR5mqYTrChsa .arrowheadPath{fill:#333333;}#mermaid-svg-zZwYHR5mqYTrChsa .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-zZwYHR5mqYTrChsa .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-zZwYHR5mqYTrChsa .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-zZwYHR5mqYTrChsa .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-zZwYHR5mqYTrChsa .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-zZwYHR5mqYTrChsa .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-zZwYHR5mqYTrChsa .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-zZwYHR5mqYTrChsa .cluster text{fill:#333;}#mermaid-svg-zZwYHR5mqYTrChsa .cluster span{color:#333;}#mermaid-svg-zZwYHR5mqYTrChsa div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-zZwYHR5mqYTrChsa .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-zZwYHR5mqYTrChsa rect.text{fill:none;stroke-width:0;}#mermaid-svg-zZwYHR5mqYTrChsa .icon-shape,#mermaid-svg-zZwYHR5mqYTrChsa .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-zZwYHR5mqYTrChsa .icon-shape p,#mermaid-svg-zZwYHR5mqYTrChsa .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-zZwYHR5mqYTrChsa .icon-shape .label rect,#mermaid-svg-zZwYHR5mqYTrChsa .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-zZwYHR5mqYTrChsa .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-zZwYHR5mqYTrChsa .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-zZwYHR5mqYTrChsa :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} main 开始
OLED_Init
Timer_Init
TIM2 建立 1 秒更新中断
Encoder_Init
TIM3 进入 TI12 编码器模式
主循环刷新 OLED
编码器 A B 边沿到来
TIM3 CNT 硬件加减
TIM2 一秒到期
TIM2_IRQHandler
Encoder_Get
保存 CNT 到 Speed
清零 TIM3 CNT
清除 TIM2 中断标志

要分清两个定时器的职责:

定时器 工作方式 负责什么
TIM3 编码器接口模式 接收 A/B 相,自动更新 CNT
TIM2 内部时钟定时中断 定义测速的固定时间窗口

TIM2 并不读取编码器引脚;TIM3 也不负责"每秒一次"。二者通过软件中的 Encoder_Get() 在 TIM2 中断时刻衔接。

14. 复现代码时的固定思路

#mermaid-svg-m8KkFJuAQaererl8{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-m8KkFJuAQaererl8 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-m8KkFJuAQaererl8 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-m8KkFJuAQaererl8 .error-icon{fill:#552222;}#mermaid-svg-m8KkFJuAQaererl8 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-m8KkFJuAQaererl8 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-m8KkFJuAQaererl8 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-m8KkFJuAQaererl8 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-m8KkFJuAQaererl8 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-m8KkFJuAQaererl8 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-m8KkFJuAQaererl8 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-m8KkFJuAQaererl8 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-m8KkFJuAQaererl8 .marker.cross{stroke:#333333;}#mermaid-svg-m8KkFJuAQaererl8 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-m8KkFJuAQaererl8 p{margin:0;}#mermaid-svg-m8KkFJuAQaererl8 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-m8KkFJuAQaererl8 .cluster-label text{fill:#333;}#mermaid-svg-m8KkFJuAQaererl8 .cluster-label span{color:#333;}#mermaid-svg-m8KkFJuAQaererl8 .cluster-label span p{background-color:transparent;}#mermaid-svg-m8KkFJuAQaererl8 .label text,#mermaid-svg-m8KkFJuAQaererl8 span{fill:#333;color:#333;}#mermaid-svg-m8KkFJuAQaererl8 .node rect,#mermaid-svg-m8KkFJuAQaererl8 .node circle,#mermaid-svg-m8KkFJuAQaererl8 .node ellipse,#mermaid-svg-m8KkFJuAQaererl8 .node polygon,#mermaid-svg-m8KkFJuAQaererl8 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-m8KkFJuAQaererl8 .rough-node .label text,#mermaid-svg-m8KkFJuAQaererl8 .node .label text,#mermaid-svg-m8KkFJuAQaererl8 .image-shape .label,#mermaid-svg-m8KkFJuAQaererl8 .icon-shape .label{text-anchor:middle;}#mermaid-svg-m8KkFJuAQaererl8 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-m8KkFJuAQaererl8 .rough-node .label,#mermaid-svg-m8KkFJuAQaererl8 .node .label,#mermaid-svg-m8KkFJuAQaererl8 .image-shape .label,#mermaid-svg-m8KkFJuAQaererl8 .icon-shape .label{text-align:center;}#mermaid-svg-m8KkFJuAQaererl8 .node.clickable{cursor:pointer;}#mermaid-svg-m8KkFJuAQaererl8 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-m8KkFJuAQaererl8 .arrowheadPath{fill:#333333;}#mermaid-svg-m8KkFJuAQaererl8 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-m8KkFJuAQaererl8 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-m8KkFJuAQaererl8 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-m8KkFJuAQaererl8 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-m8KkFJuAQaererl8 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-m8KkFJuAQaererl8 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-m8KkFJuAQaererl8 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-m8KkFJuAQaererl8 .cluster text{fill:#333;}#mermaid-svg-m8KkFJuAQaererl8 .cluster span{color:#333;}#mermaid-svg-m8KkFJuAQaererl8 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-m8KkFJuAQaererl8 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-m8KkFJuAQaererl8 rect.text{fill:none;stroke-width:0;}#mermaid-svg-m8KkFJuAQaererl8 .icon-shape,#mermaid-svg-m8KkFJuAQaererl8 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-m8KkFJuAQaererl8 .icon-shape p,#mermaid-svg-m8KkFJuAQaererl8 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-m8KkFJuAQaererl8 .icon-shape .label rect,#mermaid-svg-m8KkFJuAQaererl8 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-m8KkFJuAQaererl8 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-m8KkFJuAQaererl8 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-m8KkFJuAQaererl8 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 查编码器电气输出和每转计数
选择带编码器接口的 TIMx
查 CH1 CH2 对应引脚
连接 A B VCC GND
选择上拉 下拉或浮空
估计最高边沿频率
选择不过度的数字滤波
设置 ARR PSC
选择 TI1 TI2 或 TI12 模式
设置两路是否反相
启动并先验证位置计数
确定测速窗口 Tg
定时读取 CNT 后清零
换算 count每秒 或 RPM
验证方向 量程 溢出和低速抖动

实践顺序建议是"先位置,后速度":先持续读 CNT,确认右转增加、左转减少、一圈计数正确;再加入固定闸门和清零。否则方向、计数倍率、滤波、测速公式同时出问题时很难定位。

15. 容易踩坑的地方

15.1 ICFilter=0xF 不一定适合高速电机

机械旋钮抖动大,强数字滤波很有用;高速光电或霍尔编码器脉冲较窄,0xF 可能滤掉合法边沿。应根据参考手册 ICxF 表和编码器最短高低电平时间选取。课程值适合当前实验,不是通用固定值。

15.2 方向反了不代表解码失败

正转定义取决于安装方向、A/B 接线和极性配置。计数稳定但符号相反时,交换 A/B 或只反相一路即可。

15.3 两路必须属于同一定时器的 CH1 和 CH2

PA6、PA7 可以作为 TIM3_CH1/CH2。不能把任意两个 GPIO 填进编码器接口,也不能用 CH3/CH4 替代。

15.4 读取与清零不是严格原子操作

TIM_GetCounter() 和 TIM_SetCounter(0) 之间若恰好来了一个边沿,该计数可能落在边界附近甚至被清掉。入门实验影响很小;高精度测速可在很短临界区内暂停计数/中断,或采用差分读取累计计数而不清零,并妥善处理回绕。

15.5 机械编码器仍可能抖动

正交状态机对部分非法跳变有抵消作用,数字滤波也能抑制短毛刺,但不能保证任何噪声都完全消失。必要时结合 RC/施密特整形、合理布线和软件低通滤波。

15.6 高速时要防止窗口内溢出

使用 int16_t 返回时,一个测速窗口的净计数应控制在有符号 16 位范围内。高速时缩短 TIM2 闸门时间,同时按实际 Tg 修改速度公式。

15.7 净计数不等于总边沿数

窗口内若轴来回摆动,正向计数和反向计数会互相抵消。当前 Speed 表示带方向的净速度,不表示该窗口内总共发生了多少次运动。

16. 转写与表述校正

转写内容 正确术语
编码机接口、编辑接口 编码器接口 Encoder Interface
A项、B项 A 相、B 相
CND、升级、摄像头 CNT 计数器
输入补货 输入捕获
CG1、CG2 CH1、CH2
TIEFP1、TRFP2 TI1FP1、TI2FP2
上证言、下降盐 上升沿、下降沿
自检、质检 自减
电瓶极限 电平极性
ARR 为 6535 ARR 为 65535
UINT16-7、int 16-7 uint16_t、int16_t
测平方、测平板 测频法

课堂中"正交信号可以抗噪声"应理解为:合法正交状态按固定顺序变化,部分单路毛刺可能形成加减抵消,并且输入还有数字滤波;不能理解成正交编码器天然免疫所有干扰。

17. 复习自测

  1. 为什么编码器接口可以看作"带方向选择的外部时钟"?
  2. A、B 两相只有一相时,能测出什么,缺少什么信息?
  3. TIM_EncoderMode_TI1 中为什么 TI2 仍然有作用?
  4. TIM_EncoderMode_TI12 为什么常被称作 x4 解码?
  5. 编码器模式下 Rising/Falling 为什么不能简单理解为只选上升沿/下降沿?
  6. 为什么 TIM_EncoderInterfaceConfig() 要放在 TIM_ICInit() 之后?
  7. 为什么 ARR 设为 65535 后,CNT 的 65535 可被 int16_t 解释为 -1?
  8. Encoder_Get() 为什么必须先读 CNT 再清零?
  9. TIM3 和 TIM2 在最终测速程序中分别负责什么?
  10. 每转 2400 count、采样时间 0.2 s、读到 -400 count,转速是多少 RPM?
  11. 采样时间变短对刷新速度、低速分辨率和溢出风险各有什么影响?
  12. 高速编码器使用 ICFilter=0xF 可能出现什么现象?

18. 参考资料与页码

  • 课堂转写:00:00~17:36 编码器原理与 TIM 结构;17:58~31:10 接线、代码和测速。
  • 教师 PPT《STM32入门教程》:第 80~85 页。
  • 师兄笔记《STM32笔记》:PDF 第 65~68 页,正交编码器、接口结构、工作模式、接线及代码批注。
  • 《STM32F10xxx参考手册(中文)》:PDF 第 274~275 页编码器接口模式;第 284~285 页 SMCR;第 290 页 CCMR1 输入模式;第 292 页 CCER;第 293~294 页 CNT、PSC、ARR。
  • 《STM32F103x8B数据手册(中文)》:PDF 第 18 页 PA6/TIM3_CH1、PA7/TIM3_CH2。
  • 工程源码:STM32Project-有注释版/6-8 编码器接口测速。

19. 一句话总结

TIM 编码器接口用 CH1、CH2 接收 A/B 正交信号,在每个有效边沿根据另一相电平自动控制 CNT 加减;软件再按固定闸门时间读取并清零 CNT,就能用计数值的大小表示速度、用正负号表示方向。

相关推荐
Rabitebla15 小时前
【Linux 底层原理】进程(十):进程等待详解---从僵尸进程到 wait、waitpid 与 status
linux·c++·笔记·学习·visual studio
小lu飞16 小时前
2026小红书小工具怎么制作:纯前端 ZIP 打包、沙箱限制与笔记挂载流程
前端·笔记·ai·ai编程
无心水20 小时前
24、系统架构师论文写作专题 核心考点与学习笔记
笔记·学习·系统架构·架构师·安全架构·软考架构师·软考系统架构设计师
蒸蒸yyyyzwd20 小时前
邓明 Agent 系统面试-意图识别学习笔记
笔记·学习
Zhao-Jichao1 天前
AI Prompting for Everyone 课程笔记 - P21 总结 Conclusion
人工智能·笔记
欢欢李1 天前
【LTE L3 笔记 05】LTE同步信号
笔记·无线通信·物理层·lte·空口协议
La Pulga1 天前
【FreeRTOS源码学习笔记】空闲任务与阻塞延时
笔记·mcu·学习·嵌入式·freertos·实时操作系统
蒸蒸yyyyzwd1 天前
秋招学习笔记 day106
笔记·学习
小李不想当小白1 天前
WDG看门狗(STM32标准库学习笔记)
笔记·stm32·单片机·嵌入式硬件·学习·嵌入式
欢欢李1 天前
【LTE L3 笔记 04】LTE帧结构
笔记·无线通信·物理层·lte·空口协议