树莓派 Pico 玩步进电机控制,网上 90% 的教程都还在教 GPIO 翻转加延时挤牙膏:要转几圈就先算好多少个脉冲,然后把引脚拉高、睡几微秒、拉低、再睡几微秒,CPU 全程陪跑。这种做法本身没问题,但一旦你要同时控制三四个轴、跑加减速曲线、再顺手管个编码器或者串口屏,立马就顶不住了。这篇博文要讲的,就是 PIO(可编程 I/O)怎么帮你把脉冲放行交给硬件状态机,彻底改成非阻塞式步进电机控制。文章从 PIO 原理、时序换算、硬件接线一路讲到 C SDK 和 MicroPython 两套代码,最后还有我调驱动时踩过的坑,适合已经能让电机转起来、想更进一步把控制写得像样一点的玩家。如果你之前在 STM32 上用 HAL 库折腾过定时器输出脉冲,你会对这套思路特别有共鸣——本质上都是把节拍交给硬件,但 Pico 的玩法更轻、更自由。
1. 先搞清楚:为什么 PIO 是步进控制的“最优解”
1.1 阻塞式控制的痛点在哪
传统写法其实就一句话:拉高 STEP 引脚,延时,拉低,再延时。单片机执行一个循环就是一个脉冲,速度靠延时长短调节。听起来很简单,实际跑起来问题一堆。
首先是 CPU 被完全占死。假设你要跑 20 kHz 的步率,也就是每秒两万个脉冲,每个脉冲中间只有 50 微秒的空隙。在这 50 微秒里,你既要处理加减速计算,又要响应按键、刷新屏幕、读传感器,任何一个操作稍微超时,脉冲间隔就抖了,电机就开始嗡嗡响,严重时直接丢步。我见过不少人在这一步被迫上 RTOS,结果调度器本身又带来新的抖动源,治标不治本。
其次是多轴控制只能“轮流来”。两个轴就要两套脉冲循环,三个轴就是三套,代码里全是for嵌套和switch,想加一个联动插补简直是噩梦。STM32 生态里解决这个问题常用高级定时器加 DMA,用 DMA 改写 ARR 寄存器来变速,HAL 库写起来一长串不说,理解成本也不低。而 Pico 的 PIO 把定时器、状态判断和脉冲输出揉进了一个独立的小处理器里,一个状态机就能接管一路电机,八个电机互不干扰。
1.2 PIO 状态机到底帮我们做了什么
PIO 是 RP2040 芯片上自带的两组可编程状态机,每组 PIO 有 4 个状态机,每个状态机可以独立跑一段几十条指令的小程序。你可以把它理解成芯片里塞了几个"专门干杂活的小单片机",它们和主 CPU 共享外设总线,但执行指令时不需要主 CPU 插手。
对于步进电机控制,PIO 的用途非常明确:不断地把 STEP 引脚拉高、拉低,形成频率稳定的脉冲串。主 CPU 只需要往 PIO 的 FIFO 队列里写入一个数字,PIO 状态机就会自己把脉冲发出来,发完一个再取下一个。这个数字就是脉冲间隔,值越大速度越慢,值越小速度越快。
好处是什么?硬实时性。PIO 状态机跑在固定的时钟上,指令执行周期精确到 8 纳秒,不受中断、调度器、其他代码的影响。你写一个 20 kHz 的脉冲串,它就能稳稳跑到最后一个脉冲,期间主 CPU 干什么都不干扰它。这活儿要是靠延时函数做,早被系统 tick 搅和得稀碎了。
1.3 哪些场景适合切到 PIO 方案
我也不是劝所有人一上来就上 PIO。如果只是驱动一个小风扇、跑一个固定转速,阻塞式延时完全够用,代码还好写。但如果你符合下面任何一条,PIO 就是正确选择:
- 需要梯形或 S 形加减速,不想在脉冲中间用
sqrt卡时间; - 同一时间要驱动两个以上步进电机,还得各自独立变速;
- 想在电机转动的同时,让主 CPU 去干 UI、通信、传感器采集等别的活;
- 对脉冲精度有要求,比如配合编码器做闭环,不能容忍抖动。
我自己的经验是:当项目里出现"一边转电机一边要处理别的事"这个需求时,再回头写阻塞式代码就是在给自己埋雷。PIO 方案前期多花一小时理解,后面省下的是反复排查脉冲抖动的好几个晚上。
2. PIO 状态机与步进时序基础拆解
2.1 八个状态机、两条流水线和指令集
Pico 上有两个 PIO 外设,PIO0 和 PIO1,每个 PIO 有 4 个状态机,一共 8 路。每个状态机都有一套独立的寄存器:两个通用寄存器 X 和 Y、一个输出移位寄存器 OSR、一个输入移位寄存器 ISR,还有一个程序计数器 PC。程序放在 PIO 的指令存储器里,每个 PIO 有 32 条指令的空间,所有状态机共享。
指令集小得可怜,满打满算九条:pull(从 FIFO 取数据)、push(往 FIFO 写数据)、mov(寄存器间搬数据)、set(置位引脚或寄存器)、jmp(条件跳转)、wait(等待引脚或 IRQ 信号)、out/in(移位输出/输入)、irq(触发中断)、nop。每条指令后面还可以挂一个 0 到 31 的额外延时槽,用来扩展指令执行时间。
这里的关键点是:状态机的时钟直接来自系统主频,默认 125 MHz,一条指令一个周期,也就是 8 纳秒。这个精度远高于任何定时器中断能做到的水平,也是后续时序计算的基础。顺带一提,指令虽然少,但组合起来能干的活非常多,PIO 官方例子里甚至有人用它实现过 DVI 视频输出,控制个步进电机属于降维打击。
2.2 步进脉冲的时序约束
步进电机驱动器(A4988、DRV8825、TMC2208 这些)接收的是“脉冲/方向”信号。每来一个 STEP 脉冲,电机就走一步,方向由 DIR 引脚的电平决定。驱动器对脉冲有几个基本要求:
- STEP 引脚的高电平持续时间和低电平持续时间都要大于驱动器的响应阈值。A4988 手册要求高低电平各不小于 1 微秒左右,DRV8825 略有不同,TMC 系列在微步模式下更宽松一些。实操中我一般保证最小 2 微秒以上,留足余量。
- 改变 DIR 方向时,需要建立时间,也就是先稳定 DIR 电平,再给 STEP 脉冲。建议至少提前 5 微秒切换方向,否则可能丢一个脉冲或者反向错误。
- 脉冲频率不能超过驱动器的最大响应频率。A4988 整步时可以跑到较高频率,但微步细分越大,允许的 STEP 频率上限反而可能受限,这个后面展开。
这些约束加起来,决定了 PIO 程序生成的脉冲宽度必须可控,而且要和驱动器的电气特性匹配。我们不可能直接用 125 MHz 的全速翻转去怼驱动芯片,必须通过延时值把脉冲拉宽到微秒级别。
2.3 从“延时”到“频率”的换算
先给结论:在我的 PIO 程序里,每往 FIFO 写入一个 32 位整数 X,状态机就输出一个完整的 STEP 脉冲,高电平持续 X 个时钟周期左右,低电平也持续 X 个时钟周期左右。
系统时钟 125 MHz,一个时钟周期是 8 纳秒。如果目标步率是 f(单位:步/秒),那么一个脉冲的总周期是 1/f 秒,换算成时钟周期数就是 125000000/f。考虑到程序的固定开销,一个完整脉冲约等于 2X + 7 个时钟周期,所以反推延时值:
X ≈ (125000000 / f − 7) / 2,工程上可以直接用 X ≈ 62500000 / f 估算,误差小于一个脉冲周期,最终以示波器实测为准。
举个例子,目标步率 1 kHz,X 就是 62500 左右,写入后 PIO 大约每毫秒发出一个脉冲。下表是几个常见步率对应的 X 值和 FIFO 耗尽时间(FIFO 加深到 8 级后),方便你心里有个数:
| 目标步率 | 推荐 X 值 | 理论单步周期 | FIFO(8级)耗尽时间 |
|---|---|---|---|
| 100 Hz | 625000 | 10 ms | 80 ms |
| 1 kHz | 62500 | 1 ms | 8 ms |
| 10 kHz | 6250 | 100 µs | 0.8 ms |
| 20 kHz | 3125 | 50 µs | 0.4 ms |
| 50 kHz | 1250 | 20 µs | 0.16 ms |
| 100 kHz | 625 | 10 µs | 80 µs |
注意 X 最大值是 32 位无符号数,上限约 42.9 亿,对应极慢速度;X 最小值给到 1 时,脉冲频率能到十几兆赫兹,远超所有驱动器的承受范围。所以在这个方案里,PIO 从来不会是速度瓶颈,瓶颈永远是驱动器、电机和机械结构。
3. 硬件准备与接线实操
3.1 电机、驱动器与板卡的搭配
硬件选型直接决定后面调试的心情。步进电机我建议从 NEMA 17(42 步进)起步,1.8 度步距角,200 整步一圈,资料最全、扭矩也够用。
驱动器方面,A4988 是入门首选,便宜、矿卡堆里都能翻出几片;DRV8825 电流余量大一点;TMC2208 / TMC2209 静音效果好,适合家用设备。三者在 STEP/DIR 接口上是兼容的,代码不用改,但注意电流调节方式有差异。A4988 靠板上的电位器调 Vref,TMC 系列也有对应调节脚,具体电压换算公式不同,查对应数据手册即可。我的一般建议:把电流上限调到电机额定电流的 1.2 到 1.5 倍以内,宁可小一点也别冒烟。
板卡用标准树莓派 Pico 就行,Pico W 也完全兼容,因为 PIO 外设是一样的。另外提醒一句:千万别拿控制舵机的思路来控步进电机。舵机是靠 50 Hz 左右的 PWM 脉宽控制角度的,步进电机要的是脉冲/方向信号,两者虽然都叫控制,接口逻辑完全是两回事。
3.2 引脚规划与接线表
引脚规划有几个硬性要求:避开 GPIO 0 和 GPIO 1,这两个是 SWD 调试口,接了 STEP 或 DIR 会让调试器失灵;STEP 引脚要能被 PIO 的SET指令控制,这个在代码里配置,任意 GPIO 都行;DIR 和 EN 用普通 GPIO 即可,不需要进 PIO。
我常用的引脚分配是:
| 功能 | Pico GPIO | 备注 |
|---|---|---|
| STEP | GPIO 2 | PIO0 状态机 0,SET 指令输出 |
| DIR | GPIO 3 | 普通 GPIO,切换方向 |
| ENA | GPIO 4 | 普通 GPIO,低电平使能,可省略 |
驱动器接线表如下,不同驱动板端子名称略有差异,但基本逃不出这几个:
| 驱动器端子 | 接法 |
|---|---|
| VMOT / GND | 接外置电机电源(12 V 常见),VMOT 和 GND 之间并一个 100 µF 以上电解电容 |
| VDD / GND(逻辑) | 接 Pico 的 3.3 V 和 GND,与电机电源严格分开 |
| STEP | 接 Pico GPIO 2 |
| DIR | 接 Pico GPIO 3 |
| ENA | 接 Pico GPIO 4(可选,悬空默认使能) |
| 1A 1B 2A 2B | 对应步进电机两组绕组,按驱动器丝印接 |
这里有个容易踩的坑:A4988 的逻辑电源如果接 5 V,它的逻辑高电平阈值可能到 3.5 V 左右,而 Pico 的 GPIO 高电平是 3.3 V,属于“能用但余量不足”。解决方式很简单,把 VDD 接到 Pico 的 3.3 V 上,或者直接选 TMC2208/TMC2209 这类明确支持 3.3 V 逻辑电平的驱动器。我早期在这个细节上吃过亏,表现为电机偶尔丢步、方向乱跳,排查很久才发现是电平兼容问题。
3.3 电源、共地与抗干扰的三条铁律
步进电机这一块,80% 的奇怪问题都是电源和地线没处理好。三条铁律直接背下来:
第一,电机电源和逻辑电源必须分开。电机启动瞬间电流能顶到 1 A 以上,如果和 Pico 共用一个电源,电压跌落会导致芯片复位,表现为电机一转整个板子重启。我见过最离谱的情况是 USB 供电的 Pico 带动 42 电机,一转就黑屏重连,十有八九是电源问题。
第二,所有地线必须共地。驱动器的 VMOT 地、逻辑地、Pico 的地要连到同一个参考点,否则 STEP 信号悬空乱跳。共地不是“碰一下就完事”,建议用较粗的杜邦线或者飞线,把三个地焊在一起。
第三,电机电源两端并联大电容和去耦电容。驱动板手册一般要求最小 100 µF 电解电容,我习惯再并一个 0.1 µF 陶瓷电容,吸收高频噪声。STEP 和 DIR 的线尽量短,最好双绞,不要和电机绕组线扎在一起,否则脉冲线上感应出来的噪声足以让驱动器误触发。
4. 非阻塞式控制核心实现
4.1 PIO 汇编:三行指令搞定一个脉冲
核心程序写在stepper.pio文件里,内容非常短:
.program stepper ; 每从 TX FIFO 取到一个 32 位值 X,就产生一个完整的 STEP 脉冲 ; X 越大速度越慢,高/低电平时间约为 X 个 PIO 时钟周期 .wrap_target pull block ; 阻塞等待 FIFO 数据,空则停在这里(安全停步) mov x, osr ; X = 延时值 set pins, 1 ; STEP = 高 high_loop: jmp x-- high_loop ; 高电平保持 X 个周期,同时 X 递减 mov x, osr ; 重新装载 X set pins, 0 ; STEP = 低 low_loop: jmp x-- low_loop ; 低电平保持 X 个周期 .wrap逐行解释一下。pull block从 TX FIFO 里取一个 32 位整数到 OSR,如果 FIFO 为空,状态机就停在这条指令上不动,STEP 引脚维持上一个电平,非常安全。mov x, osr把延时值搬进 X 寄存器。set pins, 1把 STEP 引脚拉高,然后jmp x-- high_loop不断消耗 X,直到 X 归零,这段时间就是高电平脉宽。mov x, osr重新装载 X,再拉低引脚、再来一轮下降低电平。
整个循环里状态机自己跟自己玩,主 CPU 完全不需要参与引脚翻转。这就是非阻塞的核心:脉冲由 PIO 硬件生成,CPU 只负责往 FIFO 里灌数据。FIFO 空了状态机自动停步,FIFO 里有数据就立刻继续,天然具备暂停和恢复的能力。
这段程序用 pioasm 编译,会在工程里生成stepper.pio.h,提供stepper_program_init之类的辅助函数。MicroPython 用户不用管 pioasm,直接在 Python 里用装饰器写,后面会给出对应版本。
4.2 C SDK 主机端:往 FIFO 里“灌”延时值
C SDK 的初始化函数封装如下,注意顺序别搞反:
% c-sdk { static inline void stepper_program_init(PIO pio, uint sm, uint offset, uint step_pin) { pio_gpio_init(pio, step_pin); pio_sm_config c = stepper_program_get_default_config(offset); sm_config_set_set_pins(&c, step_pin, 1); sm_config_set_fifo_join(&c, PIO_FIFO_JOIN_TX); // TX FIFO 从 4 级加深到 8 级 pio_sm_init(pio, sm, offset, &c); pio_sm_set_consecutive_pindirs(pio, sm, step_pin, 1, true); } %}sm_config_set_fifo_join这行很关键。PIO 的 TX 和 RX FIFO 默认各 4 级,步进控制用不到 RX,把 RX 也并入 TX 后队列变成 8 级,CPU 灌数据的缓冲余量翻倍。pio_sm_set_consecutive_pindirs放在pio_sm_init后面,把它设成输出方向,少了我一开始就踩过这个坑——程序加载了但引脚还是输入,电机纹丝不动。
主流程核心就一个函数:往 FIFO 塞延时值,满了就等一下。
static void push_delay(PIO pio, uint sm, uint32_t delay) { while (pio_sm_is_tx_fifo_full(pio, sm)) { tight_loop_contents(); // 空转,等待 PIO 消费一个值 } pio_sm_put(pio, sm, delay); } static void move_steps(PIO pio, uint sm, int32_t steps, uint32_t delay) { gpio_put(DIR_PIN, steps > 0); // 方向线单独用普通 GPIO 控制 int32_t n = steps > 0 ? steps : -steps; for (int32_t i = 0; i < n; i++) { push_delay(pio, sm, delay); } }注意pio_sm_put是非阻塞的,写之前查一下 FIFO 满没满就行。千万不要在这里用pio_sm_put_blocking,它和pio_sm_put加while(is_full)行为类似,但你在循环里塞进sqrt算延时值时,整个函数的耗时会被拖长,脉冲反而断断续续。先把延时数组算好,再一口气灌进去,才是正确的打开方式。
main 函数里的初始化顺序也很容易记错,正确的做法是:先配 DIR、EN 普通 GPIO,再加载 PIO 程序,最后初始化状态机。
int main(void) { stdio_init_all(); gpio_init(DIR_PIN); gpio_set_dir(DIR_PIN, GPIO_OUT); gpio_put(DIR_PIN, true); gpio_init(EN_PIN); gpio_set_dir(EN_PIN, GPIO_OUT); gpio_put(EN_PIN, false); // 低有效使能 PIO pio = pio0; uint sm = 0; uint offset = pio_add_program(pio, &stepper_program); stepper_program_init(pio, sm, offset, STEP_PIN); while (true) { move_steps(pio, sm, 200, 62500); // 200 步整步一圈,约 1 kHz 步率 sleep_ms(1000); } }这段代码跑起来,电机会以每秒一圈的速度持续转,期间 CPU 实际上大部分时间都空着。你可以在循环里随便加串口打印、按键扫描,都不会影响脉冲的均匀性,这就是非阻塞和阻塞最直观的差别。
4.3 MicroPython 快速实现
原型验证阶段我更喜欢 MicroPython,改起来快,看效果直接。PIO 程序用@asm_pio装饰器写在 Python 代码里,语法和 pioasm 几乎一一对应:
from rp2 import PIO, StateMachine, asm_pio from machine import Pin import time @asm_pio(set_init=PIO.OUT_LOW) def stepper(): pull() mov(x, osr) set(pins, 1) label("high") jmp(x_dec, "high") mov(x, osr) set(pins, 0) label("low") jmp(x_dec, "low") STEP_PIN = Pin(2, Pin.OUT) DIR_PIN = Pin(3, Pin.OUT) EN_PIN = Pin(4, Pin.OUT) EN_PIN.value(0) sm = StateMachine(0, stepper, freq=125_000_000, set_base=STEP_PIN) sm.active(1) def push_delay(sm, delay): while sm.tx_fifo() >= 6: # 留两格余量,避免 CPU 卡在写 FIFO pass sm.put(delay) def move_steps(sm, steps, delay, direction=True): DIR_PIN.value(1 if direction else 0) n = steps if steps > 0 else -steps for _ in range(n): push_delay(sm, delay) move_steps(sm, 200, 62500) time.sleep(1)MicroPython 的sm.put()在 FIFO 满时会阻塞,所以我在前面先查sm.tx_fifo()的存量,留两格余量再写,保证主循环不会因为单次写入卡死。freq=125_000_000要和实际系统时钟一致,如果你的 Pico 跑在 200 MHz 超频状态,这里要相应改成 200_000_000,否则时序全偏。
有一个容易混淆的点:PIO 程序里的pull()默认是阻塞的,但这是状态机的阻塞,不是 CPU 的阻塞。状态机在 FIFO 空的时候停车等待,CPU 该干嘛干嘛,完全符合非阻塞的设计意图。反过来,sm.put是 CPU 侧的写入,它阻塞才说明 FIFO 满了、PIO 来不及消费,这时候才需要优化灌数据的方式。
4.4 加减速曲线的规划与 FIFO 管理
步进电机直接高速起步非常容易丢步,因为转子惯性跟不上换向磁场。解决方式就是加减速曲线:低速起步,逐渐加速到目标速度,结束前再减速停下。最常见的梯形曲线数学上很简单:设加速度为 a(步/秒²),最大速度为 vmax(步/秒),加速段步数 n_acc = vmax² / (2a)。如果总步数小于 2 倍 n_acc,就说明没有匀速段,直接加速到中点后减速。
关键是每走一步的延时值怎么变。加速段第 k 步的瞬时速度近似为 v(k) = sqrt(2·a·k),对应延时值 X(k) ≈ 62500000 / v(k)。减速段对称处理。C 语言里先算一个延时数组,再交给 FIFO:
static uint32_t delay_for_speed(float steps_per_sec) { return (uint32_t)(62500000.0f / steps_per_sec); } static uint32_t make_ramp(uint32_t *buf, int32_t steps, float accel, float vmax) { uint32_t n_acc = (uint32_t)(vmax * vmax / (2.0f * accel)); n_acc = MIN(n_acc, (uint32_t)steps / 2); uint32_t idx = 0; for (uint32_t k = 1; k <= n_acc; k++) { buf[idx++] = delay_for_speed(sqrtf(2.0f * accel * k)); } for (uint32_t k = 0; k < (uint32_t)steps - 2 * n_acc; k++) { buf[idx++] = delay_for_speed(vmax); } for (int32_t k = (int32_t)n_acc; k >= 1; k--) { buf[idx++] = delay_for_speed(sqrtf(2.0f * accel * k)); } return idx; }这就是典型的“先规划、后执行”结构。规划阶段把整个运动的延时数组全部算出来,执行阶段只需要把数组往 FIFO 里搬,CPU 完全不碰数学函数,搬数据的速度远高于 PIO 消费速度。我实测 10 kHz 步率下,8 级 FIFO 耗尽时间只有 0.8 ms,但搬运一个整数只需要几微秒,余量非常充足。
如果运动行程特别长、数组大到内存放不下,就得换思路:在中断服务程序里用pio_sm_put补充 FIFO,或者用 DMA 把内存里的延时表搬到 PIO 的 FIFO。PIO 支持触发 IRQ,每当 FIFO 低于阈值就中断一次,主程序趁机补一批数据。这属于进阶中的进阶,等你们把基础版跑顺了再折腾。
5. 常见问题排查与实战避坑
5.1 电机不动或转一下就停
这个问题排在所有步进电机调试问题之首,排查顺序固定:
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 电机完全不动 | 驱动未使能、STEP/DIR 接反、PIO 未初始化 | 先量 STEP 引脚有没有脉冲,用逻辑分析仪最直接 |
| 电机抖动一下就不动 | 某相绕组没接好、电流太小 | 检查 1A/1B 和 2A/2B 是否成对,调大 Vref |
| 转一下就停,像是没力气 | 加速度过大或起动速度过高 | 把第一步的延时调大一倍,再逐步缩小 |
| 完全没脉冲 | PIO 程序没加载、引脚方向还是输入 | 检查pio_add_program返回值,确认set_consecutive_pindirs调用过 |
有一个非常管用的调试技巧:用pio_sm_get_pc(pio, sm)读状态机的程序计数器。如果 PC 一直停在那条pull指令上,说明 FIFO 空了,状态机在等数据,问题出在主机端;如果 PC 在high_loop附近循环,说明脉冲在正常输出,问题出在驱动或电机侧。这个二分法能帮你快速把问题定位到 PIO 之前还是之后。
5.2 高速丢步与速度上限
高速丢步先别怪 PIO,PIO 的脉冲均匀性绝对是合格的。真正的原因多半在驱动器电流、微步细分和机械负载上。常见几个方向:
电流太小是最普遍的原因。驱动器电流上限调低,高速时力矩急剧下降,电机根本带不动负载。把电流调到电机额定电流附近,丢步现象通常会立刻缓解。
加速度太陡也会丢步。梯形曲线的加速度建议从 5000 到 20000 步/秒²起步去试,NEMA 17 配 12 V 电压一般能应付。如果加速度超过 50000,大多数情况都会直接跟丢。
微步细分和步率上限之间存在平衡。1/16 细分下 20 kHz 步率对应 1.25 kHz 实际转速,看起来不快,但驱动器能否稳定响应看你选的芯片。TMC 系列在微步模式下表现好一些,老款 A4988 在 1/16 细分下跑太高步率容易出现脉冲漏检。
还要注意低速共振。步进电机在某些低频段(通常是 200 Hz 以下)会剧烈共振,表现为电机嘶吼、机身发抖。解决方式要么快速跳过这段速度区间,要么用更细分的方式让脉冲频率提上去,要么干脆给电机加减振垫。
5.3 多轴并发与跨轴同步
一套 PIO 方案最大的优势就是多轴并行。一个状态机管一个电机,Pico 上最多能管 8 路。每个状态机的 FIFO、寄存器都是独立的,互不干扰,CPU 只需要依次往各个 FIFO 灌各自的数据。
但“并行”不等于“同步”。如果做龙门架或者 XY 平台,两个轴必须严格同时起步、同时变速,这时候光靠 FIFO 还不够。pico-sdk 提供了pio_enable_sm_mask_in_sync函数,可以把多个状态机同步启动,确保它们从同一条指令开始执行,这是多轴联动的关键。至于运动过程中每个状态机独立消费 FIFO,只要延时表是对应好的,轴的坐标关系就能保持住。
还有一个资源注意点:每个 PIO 只能放 32 条指令。单轴步进程序不过几条指令,8 个轴共用一套程序就够了,指令空间完全没问题。真正紧张的是 FIFO,每个状态机只有一份,FIFO 加深到 8 级后,每个轴同时只能排队 8 个延时值,长距离运动必须靠 CPU 或 DMA 持续补充。
5.4 调参实测心得
最后分享一个我自己的项目配置,给各位做个参考。一套桌面小雕刻机,42 步进电机驱动三轴,TMC2209 驱动器,1/32 微步细分,Pico 跑 125 MHz,PIO 程序就是我上面这 9 行。三轴各占一个状态机,8 级 FIFO,运行速度上限设到 30000 步/秒,加速度 15000 步/秒²。实测下来脉冲波形非常干净,匀速段相邻脉冲间隔的抖动在示波器上完全看不出来,加减速段也没有出现脉冲丢失。
踩过最大的坑是布线。最初 STEP 线用了一根 30 厘米的杜邦线,跟电机线一起走线,结果电机一转 Pico 偶尔复位。后来把线缩短、双绞、远离功率线,问题消失。另外一个很反直觉的经验是:延时值不要太小。当你把 X 压到几百以下,也就是步率达到 100 kHz 级别时,脉冲宽度只有几微秒,驱动器如果本身响应慢,就会出现“看着有脉冲但电机不动”的怪现象。这种事别纠结代码,直接查驱动器手册的最低脉宽要求。
给新手的忠告就一条:第一次跑通后,先拿示波器或逻辑分析仪看 STEP 引脚波形,确认频率和计算值一致,再谈优化。这个动作能帮你省掉后面所有“不知道是代码问题还是硬件问题”的纠结。