一 本章简介
本章介绍如何使用 MicroPython 读取筑基学习板上的 EC11 旋转编码器。EC11 是一种常见的增量式旋转编码器,旋转时输出 A/B 两相正交脉冲信号,通过检测脉冲的数量和相位关系,可以判断旋转方向和旋转量。它广泛应用于音量调节、菜单选择、参数设置等人机交互场景。

STM32F407 的定时器内置了硬件编码器模式,可以自动完成 A/B 相信号的解码和计数,无需 CPU 参与,精度高、零丢脉冲。
IMPORTANT
EC11 旋转编码器与直流电机编码器 2 共用定时器通道,通过拨码开关 BIT6 切换。使用 EC11 时,BIT6 必须拨到 OFF 位置(向下,默认状态)。如果 BIT6 被拨到 ON,编码器通道会被路由到直流电机编码器 2,EC11 将无法工作。
1.1 学习目标
| 序号 | 学习目标 | 重要程度 |
|---|---|---|
| 1 | 了解增量式旋转编码器的工作原理(A/B 相正交信号) | ⭐⭐⭐⭐⭐ |
| 2 | 掌握使用 STM32 定时器编码器模式读取 EC11 的方法 | ⭐⭐⭐⭐⭐ |
| 3 | 理解拨码开关 BIT6 对编码器通道的切换作用 | ⭐⭐⭐⭐⭐ |
| 4 | 能够实现旋转计数、方向判断和圈数计算 | ⭐⭐⭐⭐ |
1.2 重点提示
- EC11 的 A 相和 B 相分别连接到 PD12(TIM4_CH1) 和 PD13(TIM4_CH2)。
- 使用前必须确认拨码开关 BIT6 处于 OFF 位置(向下,默认状态)。如果 BIT6 在 ON 位置,PD12/PD13 会被路由到直流电机编码器 2 的通道,EC11 无法工作。
- 筑基学习板板载的 EC11 型号为 EC11L1525601,每相每转输出 15 个脉冲(15 PPR),旋转一圈有 30 个定位点(手感"咔哒"),工作模式为两点一脉冲(每转过 2 个定位点,输出 1 个完整脉冲周期)。STM32 的
ENC_AB模式在 A/B 相的每个边沿都计数(4 倍频),因此实际每转计数为 15 × 4 = 60。但由于每 2 个定位点才产生 1 个脉冲,所以用户每"咔哒"一格,计数器变化 2。 - 定时器编码器模式使用 16 位计数器(0~65535),长时间单方向旋转会溢出。代码中需要做溢出处理(环形计数差值计算)。
- EC11 还带有按下功能(按键 3,PC13),可以在旋转的同时按下确认,非常适合做菜单选择。
1.3 基础概念与术语
- 增量式编码器(Incremental Encoder):输出 A/B 两相脉冲信号,只能反映相对位置变化(增量),不能直接得到绝对位置。
- 正交信号(Quadrature Signal):A 相和 B 相信号之间有 90° 的相位差。正转时 A 相超前 B 相,反转时 B 相超前 A 相,通过相位关系判断方向。
- 编码器模式(Encoder Mode):STM32 定时器的一种特殊工作模式,硬件自动根据 A/B 相信号的边沿进行加减计数,无需 CPU 干预。
- CPR(Counts Per Revolution):每转计数数。筑基学习板的 EC11(EC11L1525601)为 15 PPR,4 倍频后 CPR = 60。
二 硬件说明
2.1 EC11 编码器原理
EC11 内部有一个带缺口的码盘和两个光电/机械传感器(A 相和 B 相)。旋转时,两个传感器依次被触发,输出两路相位差 90° 的方波信号:
- 顺时针旋转:A 相信号超前 B 相 90°,定时器计数递增
- 逆时针旋转:B 相信号超前 A 相 90°,定时器计数递减
STM32 的定时器编码器模式会在 A/B 相的每个边沿(上升沿和下降沿)都进行计数,因此实际分辨率是编码器物理脉冲数的 4 倍(4x 模式)。
2.2 EC11 资源汇总
| 参数 | 说明 |
|---|---|
| A 相引脚 | PD12(TIM4_CH1) |
| B 相引脚 | PD13(TIM4_CH2) |
| 定时器 | TIM4,编码器模式 |
| 复用功能 | AF2(TIM4) |
| 每转计数 | 60(CPR) |
| 拨码开关 | BIT6 = OFF(向下,默认) |
| 按下功能 | PC13(按键 3,高电平有效) |
2.3 拨码开关 BIT6 设置
CAUTION
使用 EC11 旋转编码器时,拨码开关 BIT6 必须在 OFF 位置(向下,默认状态)。如果 BIT6 被拨到 ON,PD12/PD13 会被切换到直流电机编码器 2 的通道,EC11 将完全无响应。
| BIT6 状态 | 效果 |
|---|---|
| OFF(默认,向下) | PD12/PD13 连接到 EC11 旋转编码器 |
| ON(向上) | PD12/PD13 连接到直流电机编码器 2 |
三 软件设计
3.1 基础部分
3.1.1 读取编码器原始计数
先用最简单的方式读取定时器计数器的值,验证硬件是否正常:
# EC11 编码器原始计数读取
from pyb import Pin, Timer
import time
# 配置 PD12/PD13 为 TIM4 的复用功能
Pin('PD12', mode=Pin.AF_PP, alt=Pin.AF2_TIM4)
Pin('PD13', mode=Pin.AF_PP, alt=Pin.AF2_TIM4)
# TIM4 初始化为编码器模式
tim4 = Timer(4, prescaler=0, period=0xFFFF)
tim4.channel(1, Timer.ENC_AB)
print("旋转 EC11 编码器,观察计数变化")
print("顺时针=递增,逆时针=递减")
print("-" * 40)
while True:
count = tim4.counter()
print("计数器: {:5d}".format(count))
time.sleep_ms(100)2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
TIP
如果旋转编码器时计数值没有变化,请检查:
- 拨码开关 BIT6 是否在 OFF 位置(向下)
- 引脚配置是否正确(PD12/PD13,AF2_TIM4)
- 编码器模式是否正确初始化(
Timer.ENC_AB)
3.1.2 带溢出处理的编码器读取
定时器计数器是 16 位的(0~65535),持续单方向旋转会溢出。需要用环形差值算法处理溢出:
# EC11 编码器读取(带溢出处理)
from pyb import Pin, Timer
import time
Pin('PD12', mode=Pin.AF_PP, alt=Pin.AF2_TIM4)
Pin('PD13', mode=Pin.AF_PP, alt=Pin.AF2_TIM4)
tim4 = Timer(4, prescaler=0, period=0xFFFF)
tim4.channel(1, Timer.ENC_AB)
# EC11 每转 60 个计数
EC11_CPR = 60
class EncoderSW:
"""编码器软件层:处理 16 位计数器溢出,累计绝对位置"""
def __init__(self, timer, bits=16, name="ec11"):
self.timer = timer
self.bits = bits
self.name = name
self.mask = (1 << bits) - 1 # 0xFFFF
self.half = (1 << (bits - 1)) # 0x8000
self.last = self.timer.counter() & self.mask
self.acc = 0 # 累计位置
self.delta = 0 # 单次增量
def update(self):
"""更新位置,返回累计值"""
val = self.timer.counter() & self.mask
diff = (val - self.last) & self.mask
# 处理溢出:如果差值超过半量程,说明发生了反向溢出
if diff & self.half:
diff -= (self.mask + 1)
self.delta = diff
self.acc += diff
self.last = val
return self.acc
def read(self):
"""读取累计位置"""
return self.update()
def hw(self):
"""读取硬件计数器原始值"""
return self.timer.counter() & self.mask
def step(self):
"""读取上次 update 的增量"""
return self.delta
def zero(self):
"""清零"""
self.last = self.timer.counter() & self.mask
self.acc = 0
self.delta = 0
ec11 = EncoderSW(tim4, bits=16, name="ec11")
print("EC11 编码器监测")
print("每转计数: {}".format(EC11_CPR))
print("-" * 60)
try:
while True:
pos = ec11.read()
hw = ec11.hw()
delta = ec11.step()
rev = pos / EC11_CPR # 圈数
print("位置={:8d} 硬件={:5d} 增量={:4d} 圈数={:.3f}".format(
pos, hw, delta, rev))
time.sleep_ms(50)
except KeyboardInterrupt:
print("已停止")2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
3.1.3 带滤波处理的编码器读取
细心的朋友可能会发现,在3.1.2章节-带溢出处理的编码器读取中,如果你多转几圈就会发现圈数计数不准,可能转了10圈之后起点和终点就不一样了。
这其实和按键的抖动是一个原理,他们都是机械抖动,机械编码器的内部触点在闭合和断开的瞬间,状态转换的边缘会产生高频的机械抖动。如果直接将这种波形送给微控制器,单片机可能会在一次真实的物理转动中,把波形边缘的抖动误认为是多次快速旋转。那如何处理这个问题呢?
MicroPython 标准的 pyb.Timer API 本身并没有直接提供设置输入捕获滤波器的参数。下面代码中 tim4.channel(1, Timer.ENC_AB) 只能开启硬件的 AB 相正交解码功能,但默认情况下,STM32 内部的引脚滤波值是 0(即不滤波)。
不过,我们可以利用 MicroPython 的 machine.mem32 直接操作底层寄存器,强行开启 TIM4 的硬件滤波功能。大家可以先不用懂原理,关于输入捕获滤波的相关知识,我们会在C语言的教程中继续学习,这里先复制使用看看效果即可。
# EC11 编码器读取(带溢出处理)
from pyb import Pin, Timer
import time
import machine
Pin('PD12', mode=Pin.AF_PP, alt=Pin.AF2_TIM4)
Pin('PD13', mode=Pin.AF_PP, alt=Pin.AF2_TIM4)
tim4 = Timer(4, prescaler=0, period=0xFFFF)
tim4.channel(1, Timer.ENC_AB)
# 直接操作寄存器开启 STM32 硬件滤波
# ==========================================
# TIM4 的基地址是 0x40000800 (STM32F407 参考手册)
# 捕获/比较模式寄存器 1 (TIMx_CCMR1) 的偏移量是 0x18
TIM4_CCMR1 = 0x40000800 + 0x18
# 读取当前的 CCMR1 寄存器值
ccmr1 = machine.mem32[TIM4_CCMR1]
# 清除通道 1 和通道 2 的滤波位: IC1F (bit 4-7) 和 IC2F (bit 12-15)
ccmr1 &= 0xFFFF0F0F
# 设置滤波器值为 0xF (1111)
# 0xF 代表最大硬件滤波:采样频率 f_SAMPLING = f_DTS / 32,且需要连续 8 次采样电平一致才确认跳变
ccmr1 |= 0x0000F0F0
# 将修改后的值写回寄存器
machine.mem32[TIM4_CCMR1] = ccmr1
# ==========================================
# EC11 每转 60 个计数
EC11_CPR = 60
class EncoderSW:
"""编码器软件层:处理 16 位计数器溢出,累计绝对位置"""
def __init__(self, timer, bits=16, name="ec11"):
self.timer = timer
self.bits = bits
self.name = name
self.mask = (1 << bits) - 1 # 0xFFFF
self.half = (1 << (bits - 1)) # 0x8000
self.last = self.timer.counter() & self.mask
self.acc = 0 # 累计位置
self.delta = 0 # 单次增量
def update(self):
"""更新位置,返回累计值"""
val = self.timer.counter() & self.mask
diff = (val - self.last) & self.mask
# 处理溢出:如果差值超过半量程,说明发生了反向溢出
if diff & self.half:
diff -= (self.mask + 1)
self.delta = diff
self.acc += diff
self.last = val
return self.acc
def read(self):
"""读取累计位置"""
return self.update()
def hw(self):
"""读取硬件计数器原始值"""
return self.timer.counter() & self.mask
def step(self):
"""读取上次 update 的增量"""
return self.delta
def zero(self):
"""清零"""
self.last = self.timer.counter() & self.mask
self.acc = 0
self.delta = 0
ec11 = EncoderSW(tim4, bits=16, name="ec11")
print("EC11 编码器监测")
print("每转计数: {}".format(EC11_CPR))
print("-" * 60)
try:
while True:
pos = ec11.read()
hw = ec11.hw()
delta = ec11.step()
rev = pos / EC11_CPR # 圈数
print("位置={:8d} 硬件={:5d} 增量={:4d} 圈数={:.3f}".format(
pos, hw, delta, rev))
time.sleep_ms(50)
except KeyboardInterrupt:
print("已停止")2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
3.2 进阶部分
3.2.1 编码器 + 按键联动
EC11 编码器自带按下功能(PC13),可以实现"旋转选择 + 按下确认"的交互模式:
# EC11 旋转选择 + 按下确认
from pyb import Pin, Timer, LED
import pyb
Pin('PD12', mode=Pin.AF_PP, alt=Pin.AF2_TIM4)
Pin('PD13', mode=Pin.AF_PP, alt=Pin.AF2_TIM4)
tim4 = Timer(4, prescaler=0, period=0xFFFF)
tim4.channel(1, Timer.ENC_AB)
btn_ec11 = Pin('PC13', Pin.IN, Pin.PULL_DOWN) # EC11 按下=高电平
led = LED(1)
# 简化的编码器读取
mask = 0xFFFF
half = 0x8000
last_val = tim4.counter() & mask
position = 0
# 菜单项
menu = ["LED 开", "LED 关", "LED 闪烁", "退出"]
selected = 0
last_press = 0
print("=== EC11 菜单演示 ===")
print("旋转选择,按下确认")
def show_menu():
print()
for i, item in enumerate(menu):
marker = " >> " if i == selected else " "
print("{}{}".format(marker, item))
show_menu()
while True:
# 读取编码器增量
val = tim4.counter() & mask
diff = (val - last_val) & mask
if diff & half:
diff -= (mask + 1)
last_val = val
# 旋转切换菜单项
if diff != 0:
position += diff
# 每 4 个计数切换一项(降低灵敏度)
new_sel = (position // 4) % len(menu)
if new_sel < 0:
new_sel += len(menu)
if new_sel != selected:
selected = new_sel
show_menu()
# 按下确认
if btn_ec11.value() == 1:
now = pyb.millis()
if now - last_press > 300:
last_press = now
action = menu[selected]
print("\n确认: {}".format(action))
if action == "LED 开":
led.on()
elif action == "LED 关":
led.off()
elif action == "LED 闪烁":
for _ in range(5):
led.toggle()
pyb.delay(200)
elif action == "退出":
print("退出菜单")
break
show_menu()
pyb.delay(10)2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
3.2.2 编码器控制 PWM 呼吸灯亮度
用 EC11 旋转编码器实时调节 PB8 LED 的亮度:
# EC11 控制 LED 亮度
from pyb import Pin, Timer
import time
# EC11 编码器
Pin('PD12', mode=Pin.AF_PP, alt=Pin.AF2_TIM4)
Pin('PD13', mode=Pin.AF_PP, alt=Pin.AF2_TIM4)
tim4 = Timer(4, prescaler=0, period=0xFFFF)
tim4.channel(1, Timer.ENC_AB)
# PB8 LED PWM(低电平点亮)
# TIM10_CH1(AF3)
led_pin = Pin('PB8', Pin.AF_PP, af=3) # AF3 = TIM10
tim_led = Timer(10, freq=1000)
ch_led = tim_led.channel(1, Timer.PWM, pin=led_pin)
mask = 0xFFFF
half = 0x8000
last_val = tim4.counter() & mask
brightness = 0 # 0=最亮,100=全灭
ch_led.pulse_width_percent(brightness)
print("旋转 EC11 调节 LED 亮度")
try:
while True:
val = tim4.counter() & mask
diff = (val - last_val) & mask
if diff & half:
diff -= (mask + 1)
last_val = val
if diff != 0:
brightness += diff
brightness = max(0, min(100, brightness))
ch_led.pulse_width_percent(brightness)
# 低电平点亮:0%=最亮,100%=全灭
visual = 100 - brightness
print("亮度: {}%".format(visual))
time.sleep_ms(10)
except KeyboardInterrupt:
ch_led.pulse_width_percent(100)
tim_led.deinit()
print("已停止")2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
四 常见问题
Q: 旋转编码器没有任何反应,计数值不变?
- 检查拨码开关 BIT6:必须在 OFF 位置(向下,默认状态)。这是最常见的原因。
- 确认引脚配置正确:PD12 和 PD13,AF2_TIM4。
- 确认编码器模式初始化正确:
tim4.channel(1, Timer.ENC_AB)。
Q: 旋转方向反了(顺时针递减,逆时针递增)?
这取决于 A/B 相的接线顺序。如果方向反了,最简单的方法是在代码中对增量取反:diff = -diff。
Q: 旋转时计数跳变很大或不稳定?
- EC11 是机械编码器,触点可能有抖动。可以在代码中加入简单的滤波(忽略绝对值过大的增量)或者通过直接修改STM32F407的寄存器来强行开启定时器输入捕获的滤波。
- 检查编码器是否松动或接触不良。
Q: EC11 的按下功能(PC13)和旋转功能可以同时使用吗?
可以。按下功能使用的是独立的 GPIO 引脚(PC13),与编码器的 A/B 相(PD12/PD13)互不干扰。
五 本节参考文档
- MicroPython pyb.Timer 文档:
- MicroPython pyb.Pin 文档:
- 天空星硬件资料(原理图):