一 本章简介
本章介绍如何使用 MicroPython 读取直流电机上的增量式编码器,实现电机转速和位置的实时测量。筑基学习板提供了 2 路编码器接口,分别对应两路直流电机,使用 STM32 定时器的硬件编码器模式进行高精度脉冲计数。
编码器测速是闭环电机控制(PID 调速)的基础——只有知道电机当前的实际转速,才能与目标转速进行比较并调整 PWM 输出,实现精确的速度控制。
IMPORTANT
电机编码器 2 与 EC11 旋转编码器共用定时器通道(TIM4,PD12/PD13),通过拨码开关 BIT6 切换。使用电机编码器 2 时,BIT6 必须拨到 ON 位置(向上)。如果 BIT6 处于 OFF(默认状态),PD12/PD13 会被路由到 EC11 旋转编码器,电机编码器 2 将无法工作。
电机编码器 1(TIM3,PB4/PC7)不受拨码开关影响,可以直接使用。
1.1 学习目标
| 序号 | 学习目标 | 重要程度 |
|---|---|---|
| 1 | 掌握使用定时器编码器模式读取直流电机编码器的方法 | ⭐⭐⭐⭐⭐ |
| 2 | 理解两路编码器的引脚分配和拨码开关配置 | ⭐⭐⭐⭐⭐ |
| 3 | 能够计算电机的实时转速(RPM) | ⭐⭐⭐⭐⭐ |
| 4 | 理解编码器计数器溢出处理的原理 | ⭐⭐⭐⭐ |
| 5 | 能够同时读取双电机编码器并显示数据 | ⭐⭐⭐⭐ |
1.2 重点提示
- 电机编码器 1 使用 TIM3(PB4/PC7),不受拨码开关影响,可以直接使用。
- 电机编码器 2 使用 TIM4(PD12/PD13),与 EC11 旋转编码器共用通道。使用电机编码器 2 时,拨码开关 BIT6 必须拨到 ON 位置(向上)。
- 定时器编码器模式使用 16 位计数器(0~65535),电机高速旋转时计数器会快速变化并溢出,代码中必须做环形差值处理。
- 转速计算公式:RPM = (增量计数 / 编码器每转脉冲数) / 采样时间(秒) × 60。
- 编码器测速需要电机实际转动才能看到数据变化。请确保已通过 DC 头接入外部电源,并参考驱动直流电机章节的方法让电机转起来。
- 与 EC11 旋转编码器类似,直流电机编码器同样可能存在信号抖动问题。本章进阶部分会介绍如何通过直接操作 STM32 寄存器开启硬件输入滤波来提高计数精度。
1.3 基础概念与术语
- 增量式编码器(Incremental Encoder):安装在电机轴上,随电机旋转输出 A/B 两相正交脉冲信号,用于测量转速和位置。
- PPR(Pulses Per Revolution):编码器每转输出的脉冲数,是编码器的物理分辨率参数。不同电机的编码器 PPR 不同,需要查阅电机规格书。
- CPR(Counts Per Revolution):定时器每转的计数数。在 4x 编码器模式下,CPR = PPR × 4。
- RPM(Revolutions Per Minute):每分钟转数,衡量电机转速的常用单位。
- 编码器模式(Encoder Mode):STM32 定时器的一种特殊工作模式,硬件自动根据 A/B 相信号的边沿进行加减计数,无需 CPU 干预。与 EC11 旋转编码器章节使用的是同一种硬件机制。
二 硬件说明
2.1 编码器接口分配
筑基学习板提供了 2 路直流电机编码器接口:
| 参数 | 电机编码器 1 | 电机编码器 2 |
|---|---|---|
| A 相引脚 | PB4(TIM3_CH1) | PD12(TIM4_CH1) |
| B 相引脚 | PC7(TIM3_CH2) | PD13(TIM4_CH2) |
| 定时器 | TIM3 | TIM4 |
| 复用功能 | AF2(TIM3) | AF2(TIM4) |
| 拨码开关 | 无需设置 | BIT6 = ON(向上) |
| 对外接口 | 4P 3.81mm 可插拔接口 | 4P 3.81mm 可插拔接口 |
2.2 拨码开关 BIT6 设置
CAUTION
使用电机编码器 2 时,拨码开关 BIT6 必须拨到 ON 位置(向上)。BIT6 默认是 OFF(EC11 旋转编码器),如果不切换,电机编码器 2 无法工作。
电机编码器 1(TIM3,PB4/PC7)不受 BIT6 影响,始终可用。
| BIT6 状态 | 效果 |
|---|---|
| OFF(默认,向下) | PD12/PD13 连接到 EC11 旋转编码器 |
| ON(向上) | PD12/PD13 连接到直流电机编码器 2 |
2.3 编码器接线说明
直流电机编码器通常有 4 根线:VCC(5V 或 3.3V)【我们筑基学习板子的编码器接口只推荐使用3.3V的】、GND、A 相、B 相。将编码器的 A/B 相信号线插入筑基学习板对应的编码器接口即可。
TIP
如果你不确定电机编码器的 PPR(每转脉冲数),可以先运行基础代码,手动缓慢转动电机轴一整圈,观察计数器变化量,即可得到 CPR 值。PPR = CPR / 4。
2.4 供电说明
CAUTION
编码器测速需要电机实际转动。电机驱动必须通过 DC 头或接线端子接入外部直流电源(8V~24V),TYPE-C 的 5V 无法驱动电机。
如果你还没有驱动过电机,请先参考驱动直流电机章节完成电机驱动的基本操作。
三 软件设计
3.1 基础部分
3.1.1 读取单路编码器(电机 1)
先读取电机编码器 1 的原始计数,验证硬件连接:
# 电机编码器 1 原始计数读取(TIM3,PB4/PC7)
from pyb import Pin, Timer
import time
# 配置编码器引脚
Pin('PB4', mode=Pin.AF_PP, alt=Pin.AF2_TIM3)
Pin('PC7', mode=Pin.AF_PP, alt=Pin.AF2_TIM3)
# TIM3 初始化为编码器模式
tim3 = Timer(3, prescaler=0, period=0xFFFF)
tim3.channel(1, Timer.ENC_AB)
print("电机编码器 1 监测(TIM3: PB4/PC7)")
print("转动电机轴,观察计数变化")
print("-" * 40)
while True:
count = tim3.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
如果转动电机轴时计数值没有变化,请检查:
- 编码器接线是否正确(PB4=A 相,PC7=B 相)
- 编码器供电是否正常(部分编码器需要 5V 供电)
- 引脚复用功能是否正确(AF2_TIM3)
3.1.2 带溢出处理的编码器读取
定时器计数器是 16 位的(0~65535),电机持续旋转时计数器会溢出。需要用环形差值算法处理溢出,这与 EC11 旋转编码器章节中的处理方式完全一致:
# 电机编码器 1 读取(带溢出处理)
from pyb import Pin, Timer
import time
Pin('PB4', mode=Pin.AF_PP, alt=Pin.AF2_TIM3)
Pin('PC7', mode=Pin.AF_PP, alt=Pin.AF2_TIM3)
tim3 = Timer(3, prescaler=0, period=0xFFFF)
tim3.channel(1, Timer.ENC_AB)
class EncoderSW:
"""编码器软件层:处理 16 位计数器溢出,累计绝对位置"""
def __init__(self, timer, bits=16, name="encoder"):
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
enc1 = EncoderSW(tim3, bits=16, name="motor1")
print("电机编码器 1 监测(带溢出处理)")
print("-" * 60)
try:
while True:
pos = enc1.read()
hw = enc1.hw()
delta = enc1.step()
print("位置={:8d} 硬件={:5d} 增量={:4d}".format(pos, hw, delta))
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
3.1.3 双路编码器同时读取
同时读取两路电机编码器:
WARNING
运行此代码前,请确认拨码开关 BIT6 已拨到 ON(向上),否则电机编码器 2 无法工作。
# 双直流电机编码器读取
from pyb import Pin, Timer
import time
# 电机编码器 1: TIM3 (PB4/PC7)
Pin('PB4', mode=Pin.AF_PP, alt=Pin.AF2_TIM3)
Pin('PC7', mode=Pin.AF_PP, alt=Pin.AF2_TIM3)
# 电机编码器 2: TIM4 (PD12/PD13) 注意:BIT6 必须 ON!
Pin('PD12', mode=Pin.AF_PP, alt=Pin.AF2_TIM4)
Pin('PD13', mode=Pin.AF_PP, alt=Pin.AF2_TIM4)
tim3 = Timer(3, prescaler=0, period=0xFFFF)
tim4 = Timer(4, prescaler=0, period=0xFFFF)
tim3.channel(1, Timer.ENC_AB)
tim4.channel(1, Timer.ENC_AB)
class EncoderSW:
"""编码器软件层:处理 16 位计数器溢出,累计绝对位置"""
def __init__(self, timer, bits=16, name="encoder"):
self.timer = timer
self.bits = bits
self.name = name
self.mask = (1 << bits) - 1
self.half = (1 << (bits - 1))
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 = 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):
return self.delta
def zero(self):
self.last = self.timer.counter() & self.mask
self.acc = 0
self.delta = 0
motor1_enc = EncoderSW(tim3, bits=16, name="motor1")
motor2_enc = EncoderSW(tim4, bits=16, name="motor2")
print("双电机编码器监测")
print("注意:BIT6 必须拨到 ON!")
print("-" * 80)
try:
while True:
pos1 = motor1_enc.read()
hw1 = motor1_enc.hw()
d1 = motor1_enc.step()
pos2 = motor2_enc.read()
hw2 = motor2_enc.hw()
d2 = motor2_enc.step()
print("M1 pos={:8d} hw={:5d} d={:4d} || M2 pos={:8d} hw={:5d} d={:4d}".format(
pos1, hw1, d1, pos2, hw2, d2))
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
3.2 进阶部分
3.2.1 计算电机转速(RPM)
通过定时采样编码器增量,计算电机的实时转速:
# 电机转速计算(RPM)
from pyb import Pin, Timer
import time
Pin('PB4', mode=Pin.AF_PP, alt=Pin.AF2_TIM3)
Pin('PC7', mode=Pin.AF_PP, alt=Pin.AF2_TIM3)
tim3 = Timer(3, prescaler=0, period=0xFFFF)
tim3.channel(1, Timer.ENC_AB)
mask = 0xFFFF
half = 0x8000
last_val = tim3.counter() & mask
# ===== 请根据你的电机编码器参数修改 =====
ENCODER_PPR = 13 # 编码器每转脉冲数(查阅电机规格书)
# 如果你的电机带减速箱,还需要乘以减速比:
GEAR_RATIO = 20 # 减速比(例如 20:1)
ENCODER_CPR = ENCODER_PPR * 4 * GEAR_RATIO # = 1040
# 如果不确定,先运行 3.1.1 的基础代码,手动转输出轴一圈看计数变化量
# =========================================
SAMPLE_MS = 100 # 采样周期(ms)
print("电机 1 转速监测")
print("编码器 CPR = {}".format(ENCODER_CPR))
print("-" * 50)
try:
while True:
time.sleep_ms(SAMPLE_MS)
val = tim3.counter() & mask
diff = (val - last_val) & mask
if diff & half:
diff -= (mask + 1)
last_val = val
# RPM = (增量 / CPR) / (采样时间/60秒)
# 化简: RPM = 增量 × 60000 / (CPR × 采样时间ms)
rpm = (diff * 60000) / (ENCODER_CPR * SAMPLE_MS)
print("增量={:6d} 转速={:8.1f} RPM".format(diff, rpm))
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
TIP
如何确定你的 ENCODER_CPR?
很多带减速箱的直流电机,编码器安装在电机轴(高速端)而非输出轴(低速端)。此时从输出轴转一圈测到的 CPR = PPR × 4 × 减速比。
例如:编码器 PPR=13,减速比 20:1,则输出轴每转计数 = 13 × 4 × 20 = 1040。
如果不确定,最简单的方法是运行 3.1.1 节的基础代码,手动缓慢转动输出轴一整圈,记录计数器的变化量,那就是你的 CPR 值。
3.2.2 带硬件滤波的编码器读取
与 EC11 旋转编码器章节中提到的问题类似,直流电机编码器的信号也可能存在抖动,尤其是在低速运转或电机换向的瞬间。MicroPython 的 pyb.Timer API 没有直接提供设置输入捕获滤波器的参数,但我们可以通过 machine.mem32 直接操作 STM32 寄存器来开启硬件滤波。
下面以电机编码器 1(TIM3)为例,开启硬件输入滤波:
# 电机编码器 1 读取(带硬件滤波)
from pyb import Pin, Timer
import time
import machine
Pin('PB4', mode=Pin.AF_PP, alt=Pin.AF2_TIM3)
Pin('PC7', mode=Pin.AF_PP, alt=Pin.AF2_TIM3)
tim3 = Timer(3, prescaler=0, period=0xFFFF)
tim3.channel(1, Timer.ENC_AB)
# 直接操作寄存器开启 STM32 硬件滤波
# ==========================================
# TIM3 的基地址是 0x40000400 (STM32F407 参考手册)
# 捕获/比较模式寄存器 1 (TIMx_CCMR1) 的偏移量是 0x18
TIM3_CCMR1 = 0x40000400 + 0x18
# 读取当前的 CCMR1 寄存器值
ccmr1 = machine.mem32[TIM3_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[TIM3_CCMR1] = ccmr1
# ==========================================
# 如果同时使用电机编码器 2(TIM4),也需要对 TIM4 做同样的操作:
# TIM4_CCMR1 = 0x40000800 + 0x18
# ccmr1_t4 = machine.mem32[TIM4_CCMR1]
# ccmr1_t4 &= 0xFFFF0F0F
# ccmr1_t4 |= 0x0000F0F0
# machine.mem32[TIM4_CCMR1] = ccmr1_t4
mask = 0xFFFF
half = 0x8000
last_val = tim3.counter() & mask
position = 0
# ===== 请根据你的电机编码器参数修改 =====
ENCODER_PPR = 13 # 编码器每转脉冲数(查阅电机规格书)
# 如果你的电机带减速箱,还需要乘以减速比:
GEAR_RATIO = 20 # 减速比(例如 20:1)
ENCODER_CPR = ENCODER_PPR * 4 * GEAR_RATIO # = 1040
# 如果不确定,先运行 3.1.1 的基础代码,手动转输出轴一圈看计数变化量
# =========================================
print("电机编码器 1 监测(硬件滤波已开启)")
print("-" * 50)
try:
while True:
time.sleep_ms(SAMPLE_MS)
val = tim3.counter() & mask
diff = (val - last_val) & mask
if diff & half:
diff -= (mask + 1)
last_val = val
position += diff
rpm = (diff * 60000) / (ENCODER_CPR * SAMPLE_MS)
print("位置={:8d} 增量={:6d} 转速={:8.1f} RPM".format(position, diff, rpm))
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
NOTE
关于输入捕获滤波的详细原理(CCMR1 寄存器的 ICxF 位域含义、不同滤波等级的采样频率和采样次数),我们会在 C 语言的教程中深入讲解。这里先复制使用,体验一下开启滤波前后计数稳定性的差异即可。
3.2.3 电机驱动 + 编码器测速联动
同时驱动电机和读取编码器,实现"给速度 → 测速度"的完整闭环数据链路。这是后续 PID 闭环控制的基础:
# 电机驱动 + 编码器测速联动
from pyb import Pin, Timer
import time
# ===== 电机 1 驱动(TIM9,PE5/PE6)=====
pin_in1 = Pin('PE5', Pin.AF_PP, af=Pin.AF3_TIM9)
pin_in2 = Pin('PE6', Pin.AF_PP, af=Pin.AF3_TIM9)
tim_motor = Timer(9, freq=20000)
ch_in1 = tim_motor.channel(1, Timer.PWM, pin=pin_in1)
ch_in2 = tim_motor.channel(2, Timer.PWM, pin=pin_in2)
ch_in1.pulse_width_percent(0)
ch_in2.pulse_width_percent(0)
# ===== 电机 1 编码器(TIM3,PB4/PC7)=====
Pin('PB4', mode=Pin.AF_PP, alt=Pin.AF2_TIM3)
Pin('PC7', mode=Pin.AF_PP, alt=Pin.AF2_TIM3)
tim_enc = Timer(3, prescaler=0, period=0xFFFF)
tim_enc.channel(1, Timer.ENC_AB)
mask = 0xFFFF
half = 0x8000
last_val = tim_enc.counter() & mask
# ===== 请根据实际电机修改 =====
ENCODER_PPR = 13
GEAR_RATIO = 20
ENCODER_CPR = ENCODER_PPR * 4 * GEAR_RATIO # = 1040
SAMPLE_MS = 100
def motor_forward(speed):
ch_in1.pulse_width_percent(speed)
ch_in2.pulse_width_percent(0)
def motor_backward(speed):
ch_in1.pulse_width_percent(0)
ch_in2.pulse_width_percent(speed)
def motor_stop():
ch_in1.pulse_width_percent(0)
ch_in2.pulse_width_percent(0)
def read_rpm():
global last_val
val = tim_enc.counter() & mask
diff = (val - last_val) & mask
if diff & half:
diff -= (mask + 1)
last_val = val
return (diff * 60000) / (ENCODER_CPR * SAMPLE_MS)
print("电机驱动 + 编码器测速联动")
print("编码器 CPR = {} (PPR={} x 4 x 减速比{})".format(ENCODER_CPR, ENCODER_PPR, GEAR_RATIO))
print("=" * 50)
try:
# 不同占空比下测量实际转速
for pwm_duty in [20, 40, 60, 80, 100]:
print("\nPWM 占空比: {}%".format(pwm_duty))
motor_forward(pwm_duty)
# 等待电机稳定
time.sleep_ms(500)
last_val = tim_enc.counter() & mask # 重置基准
# 采样 5 次取平均
rpm_sum = 0
for _ in range(5):
time.sleep_ms(SAMPLE_MS)
rpm = read_rpm()
rpm_sum += rpm
print(" 实测转速: {:.1f} RPM".format(rpm))
avg_rpm = rpm_sum / 5
print(" 平均转速: {:.1f} RPM".format(avg_rpm))
motor_stop()
print("\n测试完成")
except KeyboardInterrupt:
motor_stop()
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
3.2.4 双电机编码器测速类封装
将编码器测速功能封装为类,方便在更复杂的项目(如 PID 调速、循迹小车)中复用:
# 编码器测速类封装
from pyb import Pin, Timer
import time
class MotorEncoder:
"""
电机编码器测速类
自动处理计数器溢出,提供位置和转速读取
"""
def __init__(self, timer, cpr, sample_ms=100, bits=16, name="motor"):
self.timer = timer
self.cpr = cpr
self.sample_ms = sample_ms
self.name = name
self.mask = (1 << bits) - 1
self.half = (1 << (bits - 1))
self.last = self.timer.counter() & self.mask
self.position = 0
self.delta = 0
self.rpm = 0.0
def update(self):
"""更新位置和转速,需要以 sample_ms 的间隔定时调用"""
val = self.timer.counter() & self.mask
diff = (val - self.last) & self.mask
if diff & self.half:
diff -= (self.mask + 1)
self.delta = diff
self.position += diff
self.last = val
# 计算 RPM
self.rpm = (diff * 60000.0) / (self.cpr * self.sample_ms)
return self.rpm
def get_position(self):
return self.position
def get_rpm(self):
return self.rpm
def get_revolutions(self):
return self.position / self.cpr
def zero(self):
self.last = self.timer.counter() & self.mask
self.position = 0
self.delta = 0
self.rpm = 0.0
# ===== 初始化 =====
# 电机编码器 1: TIM3 (PB4/PC7)
Pin('PB4', mode=Pin.AF_PP, alt=Pin.AF2_TIM3)
Pin('PC7', mode=Pin.AF_PP, alt=Pin.AF2_TIM3)
tim3 = Timer(3, prescaler=0, period=0xFFFF)
tim3.channel(1, Timer.ENC_AB)
# 电机编码器 2: TIM4 (PD12/PD13) BIT6=ON!
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)
# ===== 请根据实际电机修改 CPR =====
ENCODER_PPR = 13
GEAR_RATIO = 20
ENCODER_CPR = ENCODER_PPR * 4 * GEAR_RATIO # = 1040
SAMPLE_MS = 100
enc1 = MotorEncoder(tim3, cpr=ENCODER_CPR, sample_ms=SAMPLE_MS, name="M1")
enc2 = MotorEncoder(tim4, cpr=ENCODER_CPR, sample_ms=SAMPLE_MS, name="M2")
print("双电机编码器测速")
print("编码器 CPR = {} (PPR={} x 4 x 减速比{})".format(ENCODER_CPR, ENCODER_PPR, GEAR_RATIO))
print("注意:BIT6 必须拨到 ON!")
print("-" * 70)
try:
while True:
time.sleep_ms(SAMPLE_MS)
rpm1 = enc1.update()
rpm2 = enc2.update()
pos1 = enc1.get_position()
pos2 = enc2.get_position()
print("M1: {:7.1f}RPM pos={:8d} | M2: {:7.1f}RPM pos={:8d}".format(
rpm1, pos1, rpm2, pos2))
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
3.2.5 按键调速 + 双电机编码器实时测速
这个综合示例将前面学到的知识串联起来:用两个板载按键(PA0 加速、PE8 减速)实时调节双电机的 PWM 占空比,同时通过编码器读取两路电机的实际转速并打印输出。你可以直观地观察到 PWM 占空比 与 实际转速 之间的对应关系。
WARNING
运行此代码前,请确认:
- 已通过 DC 头或接线端子接入外部直流电源(8V~24V)
- 拨码开关 BIT5 拨到 ON(向上)——启用电机 2 驱动
- 拨码开关 BIT6 拨到 ON(向上)——启用电机编码器 2
# 按键调速 + 双电机编码器实时测速
# PA0 = 加速,PE8 = 减速
# BIT5 = ON(电机2驱动),BIT6 = ON(电机编码器2)
from pyb import Pin, Timer
import pyb
import time
# ===== 电机 1 驱动(TIM9,PE5/PE6)=====
m1_in1 = Pin('PE5', Pin.AF_PP, af=Pin.AF3_TIM9)
m1_in2 = Pin('PE6', Pin.AF_PP, af=Pin.AF3_TIM9)
tim9 = Timer(9, freq=20000)
m1_ch1 = tim9.channel(1, Timer.PWM, pin=m1_in1)
m1_ch2 = tim9.channel(2, Timer.PWM, pin=m1_in2)
# ===== 电机 2 驱动(TIM12,PB14/PB15)BIT5=ON! =====
m2_in1 = Pin('PB14', Pin.AF_PP, af=Pin.AF9_TIM12)
m2_in2 = Pin('PB15', Pin.AF_PP, af=Pin.AF9_TIM12)
tim12 = Timer(12, freq=20000)
m2_ch1 = tim12.channel(1, Timer.PWM, pin=m2_in1)
m2_ch2 = tim12.channel(2, Timer.PWM, pin=m2_in2)
# ===== 电机编码器 1(TIM3,PB4/PC7)=====
Pin('PB4', mode=Pin.AF_PP, alt=Pin.AF2_TIM3)
Pin('PC7', mode=Pin.AF_PP, alt=Pin.AF2_TIM3)
tim3 = Timer(3, prescaler=0, period=0xFFFF)
tim3.channel(1, Timer.ENC_AB)
# ===== 电机编码器 2(TIM4,PD12/PD13)BIT6=ON! =====
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_up = Pin('PA0', Pin.IN, Pin.PULL_DOWN) # 加速
btn_down = Pin('PE8', Pin.IN, Pin.PULL_DOWN) # 减速
# ===== 编码器读取类 =====
class MotorEncoder:
def __init__(self, timer, cpr, sample_ms, bits=16):
self.timer = timer
self.cpr = cpr
self.sample_ms = sample_ms
self.mask = (1 << bits) - 1
self.half = (1 << (bits - 1))
self.last = self.timer.counter() & self.mask
self.rpm = 0.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.last = val
self.rpm = (diff * 60000.0) / (self.cpr * self.sample_ms)
return self.rpm
# ===== 请根据实际电机修改 =====
ENCODER_PPR = 13
GEAR_RATIO = 20
ENCODER_CPR = ENCODER_PPR * 4 * GEAR_RATIO # = 1040
SAMPLE_MS = 100
enc1 = MotorEncoder(tim3, ENCODER_CPR, SAMPLE_MS)
enc2 = MotorEncoder(tim4, ENCODER_CPR, SAMPLE_MS)
# ===== 电机控制 =====
duty = 0 # 当前占空比 0~100
STEP = 10 # 每次按键调整的步进值
last_press = 0 # 按键消抖时间戳
def set_motors(speed):
"""双电机同时正转"""
m1_ch1.pulse_width_percent(speed)
m1_ch2.pulse_width_percent(0)
m2_ch1.pulse_width_percent(speed)
m2_ch2.pulse_width_percent(0)
def stop_motors():
m1_ch1.pulse_width_percent(0)
m1_ch2.pulse_width_percent(0)
m2_ch1.pulse_width_percent(0)
m2_ch2.pulse_width_percent(0)
stop_motors()
print("=== 按键调速 + 双电机编码器测速 ===")
print("PA0=加速 PE8=减速 步进={}%".format(STEP))
print("BIT5=ON(电机2) BIT6=ON(编码器2)")
print("-" * 55)
try:
while True:
# 按键检测(带消抖)
now = pyb.millis()
if now - last_press > 200:
if btn_up.value() == 1:
duty = min(100, duty + STEP)
set_motors(duty)
last_press = now
print(">>> PWM 占空比: {}%".format(duty))
elif btn_down.value() == 1:
duty = max(0, duty - STEP)
set_motors(duty)
last_press = now
print(">>> PWM 占空比: {}%".format(duty))
# 定时采样编码器
time.sleep_ms(SAMPLE_MS)
rpm1 = enc1.update()
rpm2 = enc2.update()
print("PWM={:3d}% M1={:7.1f}RPM M2={:7.1f}RPM".format(duty, rpm1, rpm2))
except KeyboardInterrupt:
stop_motors()
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
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
TIP
这个示例运行后,你可以观察到几个有趣的现象:
- 两个电机即使给相同的 PWM 占空比,实际转速也不会完全一样——这是电机个体差异和负载差异导致的,也正是为什么需要 PID 闭环控制。
- 从 0% 开始加速时,电机不会立刻转动,甚至到了10%的占空比也不会转动,需要达到一定的占空比(死区)才能克服静摩擦力启动。
- 减速到 0% 后电机会靠惯性继续转动一小段时间(滑行),编码器仍然能读到转速数据。
四 常见问题
Q: 电机编码器 2 没有数据,计数值始终为 0?
- 检查拨码开关 BIT6:必须拨到 ON 位置(向上)。这是最常见的原因。
- 确认编码器已正确插入 4P 可插拔接口,A/B 相线序正确。
- 确认电机实际在转动(需要外部电源驱动电机)。
Q: 电机编码器 1 也没有数据?
电机编码器 1 不受拨码开关影响,如果没有数据:
- 检查编码器接线是否正确(PB4=A 相,PC7=B 相)。
- 确认编码器供电正常(部分编码器需要 5V 供电)。
- 手动转动电机轴,看计数器是否变化。
Q: 转速值不准确或跳动很大?
- 确认
ENCODER_CPR参数与你的电机编码器匹配。不同电机的编码器 PPR 和减速比不同,CPR = PPR × 4 × 减速比。如果不确定,手动转输出轴一圈测量计数变化量。 - 增大采样周期(
SAMPLE_MS)可以减小测量噪声,但会降低响应速度。 - 可以对多次采样取平均值来平滑数据。
- 尝试开启硬件输入滤波(参考 3.2.2 节),减少信号抖动对计数的干扰。
Q: 转速方向反了?
交换编码器的 A/B 相接线,或在代码中对增量取反。
Q: 可以同时使用 EC11 和电机编码器 2 吗?
不可以。它们共用 TIM4(PD12/PD13),通过 BIT6 二选一。电机编码器 1(TIM3)不受影响,始终可用。
五 本节参考文档
- MicroPython pyb.Timer 文档:
- MicroPython pyb.Pin 文档:
- 天空星硬件资料(原理图):