Pico + TMC2209 静音步进电机:从踩坑到跑顺的完整记录(小白向)
我手边正好有 树莓派 Pico 和一块 42 步进电机,就决定先用 TMC2209 SilentStepStick + 树莓派 Pico 搭面包板做快速验证,看看能不能让步进电机先转起来。
没想到在 Pico 这一步就踩了一个非常经典的坑——DIR 和 STEP 引脚接反了。电机不会报错、不会冒烟,就是静止不转,排查起来特别容易被带偏。
这篇文章尽量写得详细一点,让刚接触步进电机的朋友也能跟着走一遍,少走弯路。
一、前言:为什么用 Pico 做快速验证
步进电机调试涉及电源、驱动、电机相序、控制信号好几个环节。如果一上来就写复杂的底层代码,代码量大、调试周期长,出问题的时候很难判断是硬件问题还是软件问题。
用 Pico + MicroPython 的好处是:
代码短:十几行就能让步进电机转起来。
好调试:Thonny 里直接跑,错了马上改。
先排除硬件问题:确认电机、驱动、接线都没问题之后,再往上加功能就心里有底。
所以这次我选择先用 Pico 把电机跑起来,确认整套硬件链路没问题之后,再考虑上更复杂的平台或功能。
二、硬件清单与接线
2.1 器件清单
器件型号/说明作用
| 步进驱动 | TMC2209 SilentStepStick | 基于 TMC2209-LA,支持 StealthChop2、StallGuard4、CoolStep |
| 主控 | 树莓派 Pico | RP2040,MicroPython 支持好,适合快速验证 |
| 电机 | 42 步进电机 | 1.8°/步,相电流 1.7 A,相电阻 2.2 Ω,带 1000 线编码器 |
| 电源 | 数控电源 20 V / 2 A | 给 TMC2209 的 VM 供电 |
| 其他 | 面包板、杜邦线 | 临时接线 |
我这块 42 步进电机的铭牌参数如下,后面调 Vref 时可以参考:

2.2 TMC2209 SilentStepStick 简介
TMC2209 是 Trinamic(现属于 ADI)推出的一款静音步进电机驱动芯片。SilentStepStick 是基于这颗芯片做的小模块,尺寸大概只有 0.6" × 0.8",焊上排针就能插面包板。
主要参数:
参数数值
| 供电电压 VM | 4.75 V ~ 28 V |
| 持续相电流 | 1.4 A RMS |
| 峰值电流 | 约 2.5 A |
| 控制接口 | STEP/DIR,或单线 UART |
| 关键特性 | StealthChop2、SpreadCycle、StallGuard4、CoolStep |
本次验证先用最简单的 STEP/DIR 独立模式,不通过 UART 配置,后面再用 UART 调电流和静音参数。
2.3 认识 TMC2209 的引脚
我这块模块的丝印在背面,芯片在另一面。下面这张表是从芯片面朝上俯视的引脚位置,和直接看背面丝印是左右镜像关系:
左侧引脚(自上而下)名称右侧引脚(自上而下)名称
| 9 | DIR | 1 | GND |
| 10 | STEP | 2 | VIO |
| 11 | PDN/UART | 3 | M1B |
| 12 | UART | 4 | M1A |
| 13 | SPREAD | 5 | M2A |
| 14 | MS2 | 6 | M2B |
| 15 | MS1 | 7 | GND |
| 16 | EN | 8 | VM |
底部两个引脚(从左到右):18 DIAG、17 INDEX。
本次验证用到的引脚:
DIR/STEP/EN:接 Pico 的 GPIO,控制方向和步进。
VIO:接 Pico 的 3.3 V,作为逻辑电平参考。
GND:和 Pico、电源负极共地。
VM:接外部 20 V 电源正。
M1A/M1B/M2A/M2B:接电机两相。
MS1/MS2/PDN/SPREAD:接高/低电平,配置工作模式。
2.4 电源、共地与电流设置
供电
TMC2209 需要两路供电:
VM:电机电源,我用正点原子的数控电源 P100 供电,20 V 左右。TMC2209 可接受供电电压在 4.75 V ~ 28 V 范围内。
VIO:逻辑电源,接 Pico 的 3.3 V。
特别注意:VM 和 VIO 千万不要接反或短路。 VM 是高压大电流,VIO 是低压逻辑,接反轻则驱动损坏,重则烧 Pico。
共地
Pico 的 GND、TMC2209 的 GND、20 V 电源的负极,三者的地必须接到一起。否则 STEP/DIR 信号没有共同参考地,TMC2209 识别不了 Pico 发来的脉冲。
电流限制
TMC2209 SilentStepStick 上有一个小电位器,用来设置输出电流。量电位器金属片对 GND 的电压就是 Vref。电流估算:
I_rms ≈ Vref / 1.4
我这块电机标称相电流 1.7 A,但快速验证时不需要跑满。我先把 Vref 调到 1.14 V,对应约 0.8 A,空载完全够用。后面带负载再慢慢往上调,但建议不要超过 2.0 V(对应约 1.4 A RMS),否则要加散热片。
2.5 详细接线步骤
Pico 到 TMC2209Pico 引脚TMC2209 引脚说明
| GP2 | DIR | 方向控制 |
| GP3 | STEP | 步进脉冲 |
| GP4 | EN | 使能,低电平有效 |
| 3V3(OUT) | VIO | 逻辑电平 3.3 V |
| GND | GND | 共地 |
电机电源电源TMC2209 引脚说明
| 20 V 正 | VM | 电机供电 |
| 20 V 负 | GND | 与 Pico 共地 |
步进电机到 TMC2209
四线步进电机有两组线圈,先用万用表通断档找到同一组:
万用表响了 / 阻值很小(约 2.2 Ω)的两根线是一组。
另外两根是另一组。
然后接到 TMC2209:
电机绕组TMC2209 引脚
| A+ | M1A |
| A- | M1B |
| B+ | M2A |
| B- | M2B |
如果后面发现电机方向反了,交换其中一相的两根线即可。
模式引脚配置TMC2209 引脚接法说明
| PDN/UART | GND | 正常工作,启用静止自动降流 |
| UART | 悬空 | 本次用独立模式,不通过 UART 配置 |
| SPREAD | GND | StealthChop 静音模式 |
| MS1 | 3.3 V | 1/16 微步 |
| MS2 | 3.3 V | 1/16 微步 |
这样配置后,TMC2209 工作在 1/16 微步、StealthChop、STEP/DIR 控制 状态。
实际接线如下图所示(Pico GP2/GP3/GP4 分别接 DIR/STEP/EN,VIO 接 3.3 V,VM 接 20 V,电机四根线按 A+/A-/B+/B- 接 M1A/M1B/M2A/M2B):

三、软件环境与代码说明
3.1 给 Pico 刷 MicroPython
下载 Pico 对应的 MicroPython UF2 文件。
按住 Pico 的 BOOTSEL 键,插 USB。
电脑会弹出一个 U 盘,把 UF2 文件拖进去。
Pico 会自动重启,MicroPython 就刷好了。
3.2 安装 Thonny
Thonny 是一个对新手很友好的 Python IDE,内置 MicroPython 支持:
安装 Thonny。
右下角选择解释器为 MicroPython (Raspberry Pi Pico)。
选择对应的 COM 口。
打开脚本,点击运行即可。
3.3 代码文件结构
我把代码分成了三个文件,放在 motor/ 目录下:
motor/ ├── motor_controller.py # 电机控制类 ├── bootsel_button.py # BOOTSEL 按键多击检测 └── test_motor.py # 测试入口
3.4 motor_controller.py
这个文件封装了 StepperMotor 类,提供使能、设置方向、设置速度、停止、加减速等基础功能。
from machine import Pin, PWM import time DEFAULT_DIR_PIN = 2 DEFAULT_STEP_PIN = 3 DEFAULT_EN_PIN = 4 class StepperMotor: def __init__(self, dir_pin=DEFAULT_DIR_PIN, step_pin=DEFAULT_STEP_PIN, en_pin=DEFAULT_EN_PIN, microstep=16): self._dir_pin = Pin(dir_pin, Pin.OUT) self._step_pwm = PWM(Pin(step_pin)) self._en_pin = Pin(en_pin, Pin.OUT) self._microstep = microstep self._steps_per_rev = 200 * microstep self._current_speed = 0 self._current_direction = True self._enabled = False self.disable() @property def is_running(self): return self._current_speed > 0 @property def current_direction(self): return self._current_direction def enable(self): if self._enabled: return self._en_pin.value(0) self._enabled = True time.sleep_ms(50) def disable(self): self.stop() time.sleep_ms(50) self._en_pin.value(1) self._enabled = False def set_direction(self, clockwise=True): self.enable() self._dir_pin.value(1 if clockwise else 0) self._current_direction = clockwise time.sleep_ms(10) def set_speed(self, freq_hz): freq_hz = int(freq_hz) if freq_hz <= 0: self.stop() return self.enable() self._step_pwm.freq(freq_hz) self._step_pwm.duty_u16(32768) self._current_speed = freq_hz def stop(self): self._step_pwm.duty_u16(0) self._current_speed = 0 def run(self, freq_hz, clockwise=True): if self.is_running and self._current_direction != clockwise: self.stop() time.sleep_ms(100) self.set_direction(clockwise) self.set_speed(freq_hz) def ramp_to_speed(self, target_speed, ramp_step=40, ramp_delay_ms=20): target_speed = int(target_speed) if target_speed <= 0: while self._current_speed > 0: self._current_speed -= ramp_step if self._current_speed < 0: self._current_speed = 0 self.set_speed(self._current_speed) time.sleep_ms(ramp_delay_ms) return if self._current_speed == 0: self._current_speed = ramp_step self.set_speed(self._current_speed) time.sleep_ms(ramp_delay_ms) while self._current_speed != target_speed: if self._current_speed < target_speed: self._current_speed += ramp_step if self._current_speed > target_speed: self._current_speed = target_speed else: self._current_speed -= ramp_step if self._current_speed < target_speed: self._current_speed = target_speed self.set_speed(self._current_speed) time.sleep_ms(ramp_delay_ms)
3.5 bootsel_button.py
这个文件负责检测 Pico 的 BOOTSEL 按键点击次数。我用的 MicroPython 固件没有 machine.bootsel(),所以代码里优先使用 rp2.bootsel_button(),找不到再回退到 machine.bootsel(),两个都没有就用普通 GPIO 按键。
import time from machine import Pin try: from rp2 import bootsel_button as DEFAULT_READ_FUNC except (ImportError, AttributeError): try: from machine import bootsel as DEFAULT_READ_FUNC except AttributeError: DEFAULT_READ_FUNC = None _DEBOUNCE_MS = 30 _CLICK_TIMEOUT_MS = 300 _LONG_PRESS_MS = 800 def make_pin_reader(pin_id, active_low=True): pin = Pin(pin_id, Pin.IN, Pin.PULL_UP) def reader(): value = pin.value() return not value if active_low else value return reader class BootselButton: def __init__(self, read_func=None, debounce_ms=30, click_timeout_ms=300, long_press_ms=800): read_func = read_func or DEFAULT_READ_FUNC if read_func is None: raise RuntimeError( "machine.bootsel() 不可用。请使用 make_pin_reader(pin_id) " "传入一个 GPIO 按键读取函数,或刷入支持 bootsel 的固件。" ) self._read = read_func self._debounce_ms = debounce_ms self._click_timeout_ms = click_timeout_ms self._long_press_ms = long_press_ms self._last_raw = False self._stable_pressed = False self._last_change_time = 0 self._press_start_time = 0 self._click_count = 0 self._last_release_time = 0 def _read_stable(self, now): raw = bool(self._read()) if raw != self._last_raw: self._last_raw = raw self._last_change_time = now return None if time.ticks_diff(now, self._last_change_time) < self._debounce_ms: return None return raw def _check_timeout(self, now): if self._click_count == 0: return 0 if time.ticks_diff(now, self._last_release_time) <= self._click_timeout_ms: return 0 count = self._click_count self._click_count = 0 return count def update(self): now = time.ticks_ms() pressed = self._read_stable(now) if pressed is None: return self._check_timeout(now) if pressed and not self._stable_pressed: self._stable_pressed = True self._press_start_time = now elif not pressed and self._stable_pressed: self._stable_pressed = False duration = time.ticks_diff(now, self._press_start_time) if duration < self._long_press_ms: self._click_count += 1 self._last_release_time = now return self._check_timeout(now)
3.6 test_motor.py
这是测试入口,把电机和按键组合起来:
import time
from motor_controller import StepperMotor
from bootsel_button import BootselButton
SLOW_SPEED = 320
FAST_SPEED = 1600
class MotorState:
def __init__(self):
self.gear = 0
self.speeds = [SLOW_SPEED, FAST_SPEED]
def handle_clicks(clicks, motor, state):
if clicks == 1:
print("单击:停止")
motor.stop()
return
if clicks == 2:
print("双击:正转")
motor.run(state.speeds[state.gear], clockwise=True)
return
if clicks == 3:
print("三击:反转")
motor.run(state.speeds[state.gear], clockwise=False)
return
if clicks == 4:
state.gear = 1 - state.gear
level = "快" if state.gear else "慢"
print(f"四击:切换到 {level} 速")
if motor.is_running:
motor.run(state.speeds[state.gear], motor.current_direction)
return
print(f"忽略 {clicks} 击")
def main():
motor = StepperMotor()
try:
button = BootselButton()
print("BOOTSEL 按键控制已启动")
except RuntimeError:
from bootsel_button import make_pin_reader
button = BootselButton(read_func=make_pin_reader(15))
print("GP15 按键控制已启动(BOOTSEL 不可用)")
state = MotorState()
print("单击=停止 | 双击=正转 | 三击=反转 | 四击=调速")
while True:
clicks = button.update()
if clicks > 0:
handle_clicks(clicks, motor, state)
time.sleep_ms(10)
if __name__ == "__main__":
main()按键逻辑:
点击次数动作
| 单击 | 停止 |
| 双击 | 正转 |
| 三击 | 反转 |
| 四击 | 切换速度(慢档 320 Hz / 快档 1600 Hz) |
四、上电验证与踩坑实录
4.1 上电后先检查什么
第一次上电不要急着跑复杂脚本,按下面顺序验证:
检查电源
VM 对 GND:约 18.9 V。
VIO 对 GND:约 3.3 V。
Pico GND 和 TMC2209 GND、电源负极之间是通的。
检查 EN 状态
EN 低电平有效。如果 EN 悬空或被拉高,电机不会锁轴。上电后 GP4 应该输出低电平。
检查电机是否锁轴
用手转电机轴,如果转不动或明显有阻力,说明驱动已经使能、线圈有电流。如果还能轻松转动,重点查 EN、VIO、共地。
跑最简步进脚本
先用一个每秒一步的慢速脚本,确认电机有“嗒嗒”声:
from machine import Pin import utime step = Pin(3, Pin.OUT) # STEP 接 GP3 direction = Pin(2, Pin.OUT) # DIR 接 GP2 enable = Pin(4, Pin.OUT) # EN 接 GP4 enable.value(0) direction.value(1) while True: step.value(1) utime.sleep_ms(500) step.value(0) utime.sleep_ms(500)
能听到“嗒嗒”声、轴在慢慢转,说明基本链路通了。
4.2 踩坑实录:DIR 和 STEP 接反了
现象
我最初写的 MicroPython 脚本里:
STEP_PIN = 2 DIR_PIN = 3
也就是说,Pico 的 GP2 接到了 TMC2209 的 STEP 引脚,GP3 接到了 DIR 引脚。而当时参考的 CircuitPython 脚本是 DIR = GP2、STEP = GP3。
两者正好反了。
表现出来的症状非常隐蔽:
电机上电后被锁住,说明 EN、VIO、VM 都正常。
用万用表 Hz 档测 TMC2209 的 STEP 引脚,能看到脉冲频率(其实是 DIR 在跳)。
量两相线圈电压,M1A-M1B 和 M2A-M2B 都一动不动。
电机没有正常步进的“嗒嗒”声,只偶尔因为方向信号变化抖一下。
电流始终只有 0.095 A 左右,维持锁轴状态。
容易误判的方向
因为这种症状和“电流太小”“相序接反”“驱动损坏”都很像,我先后试了:
交换电机一相的两根线 —— 没用。
把 Vref 从 1.14 V 往上调 —— 没用。
把 SPREAD 接到 3.3 V 换 SpreadCycle —— 没用。
怀疑 TMC2209 模块坏了 —— 准备换模块。
关键转折
死马当活马医,我找了一个 CircuitPython 的测试脚本跑了一下,电机居然正常转了。
对比两个脚本的引脚定义后才发现,是 DIR 和 STEP 反了。Pico 把方向信号发到了 TMC2209 的 STEP 引脚,把步进脉冲发到了 DIR 引脚。TMC2209 收到方向信号后只会保持在一个微步位置,自然不会换相。
修正
把 MicroPython 代码改成和 CircuitPython 一致:
DIR_PIN = 2 STEP_PIN = 3
也就是:
PicoTMC2209
| GP2 | DIR |
| GP3 | STEP |
再跑脚本,电机立刻正常转动。
4.3 其他常见坑
共地没接好
Pico、TMC2209、电源三者的 GND 必须连在一起。如果共地不好,STEP/DIR 信号没有参考地,TMC2209 识别不了。
电机绕组没找对
四线步进电机有两组线圈,一定要用万用表通断档确认哪两根是同一组。如果分组错了,电机只会震动。
Vref 太低
如果电机锁轴有力但启动不了,试试把 Vref 调到 1.2 V ~ 1.5 V。Vref 太低时,电机扭矩不够。
VM 电压不够或电流限制
TMC2209 的 VM 范围是 4.75 V ~ 28 V。我用 20 V,数控电源限流 2 A,实际工作电流只有 0.1 A ~ 0.3 A,完全够用。
MS1/MS2 悬空
如果 MS1/MS2 不接,TMC2209 会工作在默认模式,细分可能不是 1/16。建议明确把它们接到 3.3 V 或 GND。
4.4 亲身经历:Vref 过高把芯片烧出鼓包
一开始我怀疑是电流不够,把 Vref 调得很高,结果 TMC2209 发热严重,主控芯片表面出现了两个小凸起(如下图)。后来把 Vref 降下来,模块还能继续用,但这也说明电流不是越大越好。建议先用低电流让电机转起来,再根据需要慢慢往上调,并且一定要做好散热。

如果你也遇到“只锁不转”的情况,可以按这个顺序排查:
现象可能原因排查方法
| 轴能轻松转动 | EN 没使能 / VIO 没电 / 没共地 | 量 EN、VIO、GND |
| 轴锁死但不转,无声音 | DIR/STEP 接反 | 对脚本和实际接线 |
| 轴锁死,嗡嗡响 | 一相接反 / 相序错 | 万用表通断档确认绕组 |
| 轴锁死,有顿挫但不走 | 电流太小 / 负载太大 | 调大 Vref,空载测试 |
| 能走但方向反 | 电机相序反 | 交换一相两根线 |
五、BOOTSEL 按键控制与实测效果
5.1 为什么用 BOOTSEL 按键
Pico 上只有一个 BOOTSEL 按键,没有专门的用户按键。这个按键平时用来进 USB 下载模式,但在 MicroPython 里可以通过 rp2.bootsel_button() 读取状态。
我的代码里做了兼容:
优先用 rp2.bootsel_button()
找不到再用 machine.bootsel()
两个都没有则自动降级到 GP15 外接按键
按键操作要点:
按键按下的间隔不要超过 300 ms,否则会被识别成两次独立点击。
不要长按超过 800 ms,会被过滤掉,防止误进入 BOOTSEL 模式。
如果你外接按键,一端接 GP15,另一端接 GND。
5.2 多击检测与按键映射
bootsel_button.py 里实现了去抖、连击超时、长按过滤,最终把 BOOTSEL 按键映射成 1~4 次点击:
点击次数动作
| 单击 | 停止 |
| 双击 | 正转 |
| 三击 | 反转 |
| 四击 | 切换速度(慢档 320 Hz / 快档 1600 Hz) |
5.3 测试结果
我把文件拷到 Pico 上,依次验证了以下操作,全部正常:
正转
正转加速
反转
反转减速
停止
正转 → 反转切换
停止
正转 → 正转加速 → 反转 → 停止
Thonny Shell 里的输出类似下图,可以看到每次 BOOTSEL 按键触发的动作:

实际测试运转效果如下:
六、总结与后续可以做的事
6.1 这次最大的教训
脚本里的引脚注释和实际接线必须一致,尤其是 DIR 和 STEP 这种名字长得像、功能完全不同的信号。
DIR/STEP 反接后,电机不会报错、不会冒烟,就是 quietly 不转,排查起来特别容易被带到电流、相序、驱动损坏这些方向上去。如果你也遇到“只锁不转、没声音、线圈电压不变”的情况,先别急着换模块,把脚本和实际接线对一遍。
另外,先用 Pico 做快速验证是个非常正确的决定。用 MicroPython 十几行就能定位问题,不用在底层代码里绕来绕去。
6.2 后面还可以继续做的事
Pico 上跑通之后,还有很多可以玩的方向:
UART 在线配置 TMC2209:调整 RMS 电流、微步细分、静音参数。
加上滑轨:把旋转运动转成直线运动,做往复控制。
尝试 StallGuard4:无传感器归位,省掉限位开关。
封装成更通用的库:把电机控制类做得更完善,方便以后复用。
我要赚赏金
