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 输入;
  • 使用数字滤波器 IC1FIC2F
  • 使用输入极性 CC1PCC2P
  • 产生 TI1FP1TI2FP2 送入编码器逻辑。

但普通输入捕获后半段的捕获预分频和 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 选择,输入滤波和极性仍由 CCMR1CCER 配置。

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_CRLCCMR1.IC1F/IC2FCCER.CC1P/CC2P
  2. 模式选择SMCR.SMS
  3. 计数结果CNTARRPSC
  4. 测速闸门 :TIM2 的 PSCARRDIERSR

10. 为什么 uint16_tint16_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_tint16_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,就能用计数值的大小表示速度、用正负号表示方向。

相关推荐
tju新生代魔迷1 小时前
Verilog HDL 学习笔记(十)| 第10章 时序和延迟
笔记·学习·fpga开发
幼儿园蛋小黄1 小时前
嵌入式学习总结:从入门到实战的完整路径
学习
-To be number.wan1 小时前
编译原理第三章:编译器是怎么“认字“的?——词法分析全解析
学习·编辑器
糖糖单片机设计1 小时前
STM32水稻苗大棚环境监测系统
stm32·单片机·嵌入式硬件
zyf1044162 小时前
暑期实践日志 Day47:复盘第七章内容,规整对应成果
学习·计算机网络·剪辑·暑期实践·课题任务
传奇开心果编程2 小时前
【Rust入门知识点学与练】第10课:Option 和 Result(错误处理入门)
开发语言·学习·rust
喵的前端路2 小时前
《EDVAC》学习记录(第二部分)
学习
lee_curry3 小时前
AI Agent 工程师完整学习路线(面向生产级项目与面试)
人工智能·学习·ai·面试·agent
小毛驴8503 小时前
前端开发学习路线可以分为几个层次
学习