直接聊结论:激光打标/雕刻系统的信号链,如果只留第一道分水岭,十有八九会是“STM32 能输出 PWM、能拉 GPIO,但接上振镜驱动器就是带不动”。我在自己从零搭振镜控制的时候就卡在这里,后来把 XY2-100 协议和差分物理层理清楚,问题才真正解开。这篇就把这条信号链完整拆开讲一遍:从 STM32F103 产生时序,到 AM26LS31 转成差分信号,再到振镜驱动器能识别的完整链路,适合正在自研振镜控制板、做低成本打标机实验,或者只是想搞清楚振镜卡内部原理的硬件/嵌入式工程师。
1. 为什么振镜信号链是打标系统的第一道分水岭
先摆一下整个打标系统的骨架。一台最基础的振镜式激光打标机,分成两大部分:一部分负责“光”,包括激光器、扩束镜、聚焦镜;另一部分负责“让光去对的地方”,这就是振镜扫描系统。振镜扫描系统又由振镜电机和振镜驱动器组成。驱动器接受控制板给出的位置指令,把电压/数字信号转换成电机线圈电流,驱动镜片偏转,激光焦点随即在工件上画出图形。
上位机或运动控制软件做的工作是把图形解析成一条条线段,再把线段拆成密集的坐标点。拆出来的坐标点最终要通过某一种接口送给振镜驱动器。老一代模拟振镜用 ±10V 模拟电压,电压越高镜片偏转越大;数字振镜则普遍采用差分串行协议,XY2-100 是出现频率最高的一个。
很多人的第一步是把 STM32F103 和振镜驱动器用杜邦线连起来,向数据脚输出高/低电平,用定时器输出一个 PWM 做时钟,接着发现振镜要么不动,要么乱抖。原因不是代码太烂,而是没有理解两个层次的问题:
- 协议层:振镜驱动器要的不是普通 PWM,而是符合 XY2-100 的“时钟 + 数据 + 同步”串行帧格式。
- 物理层:振镜驱动器的 XY2-100 接口绝大多数是差分形式的 RS-422/RS-485 兼容输入,直接把单片机 GPIO 接上去,共模噪声、地电位差、长线反射全都会压过来。
STM32F103 在这里的角色是“协议产生器”,AM26LS31 是“物理层驱动芯片”,两者配合才能把逻辑信号变成能在几十厘米甚至几米线缆里可靠传输的差分信号。搞明白这条链路,后续不管是换协议还是换主控芯片,思路都是通用的。
2. XY2-100 时序拆解:SYNC、CLOCK、DATA、CHANNEL 到底在传什么
2.1 四个信号的分工
XY2-100 虽然名字里带“100”,但它并不是一个像 I2C 那样死板的标准协议。不同振镜厂商实现有 16 位和 20 位的差别,时钟频率上限也不完全一致。无论位宽怎么变,最核心的四个信号是固定的:
| 信号 | 方向 | 作用 |
|---|---|---|
| CLOCK± | 控制板 → 驱动器 | 位时钟,驱动接收端一位一位采样数据 |
| DATA± | 控制板 → 驱动器 | 串行位置数据,通常是高位先出 |
| SYNC± | 控制板 → 驱动器 | 帧同步,标记一帧位置数据的开始/结束 |
| CHANNEL± | 控制板 → 驱动器 | 通道选择,用于多轴/多驱动器共用总线时分复用 |
这四个信号不是孤立的,必须组合成完整帧来理解。所谓“帧”,可以理解成一次完整的位置更新:一帧数据里只传输一个轴的目标位置(有些实现会把 X、Y 两个轴放入一帧的多个通道里)。帧与帧之间的间隔决定了坐标更新率,也就是打标速度的上限。
2.2 常见的 16 位帧格式
以最常见、也是 STM32 实现最顺手的 16 位位置数据帧为例,一次传输过程是这样的:
- SYNC 置高,通知驱动器“新的一帧要开始了”。
- CLOCK 连续输出 16 个脉冲。
- 在每个 CLOCK 的上升沿(或下降沿,看驱动器手册)之前,DATA 线上摆好当前位。
- 16 个数据位全部移出后,SYNC 拉低,驱动器把收到的 16 位数据锁存到当前位置寄存器。
时序看起来是下面这个样子的简化示意:
SYNC ____▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔____ CLK ____▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁____ DATA ____X D15 D14 D13 ... D1 D0 X____注意这个“锁存”动作很关键。它决定了 XY2-100 不能像串口那样按字节流看,而必须以帧为单位对齐。如果同步极性写反,或者同步宽度不对,驱动器收到的数据位虽然顺序对,但帧边界错位,最终结果就是振镜偏转到一个完全错误的位置,甚至乱抖。
2.3 位宽到底是 16 还是 20
这是新手最容易懵的地方。XY2-100 这个名字的原始定义里,位置字其实是 20 位,其中若干位用作符号和标志,实际位置分辨率可能达到 16~19 位。国内很多数字振镜驱动器,比如常见的配套 JPT、瑞镭、创鑫等激光器方案的振镜驱动板,实际使用时也常按 20 位去接收。
我在项目中采用过一个稳妥策略:代码里封装一帧数据的位数参数,默认 16 位,可以在顶层配置为 20 位。发送 16 位时,数据值按带符号整数处理,范围是 -32768 到 32767,对应镜片机械偏转范围的两端。发送 20 位时,用带符号扩展的方式把 16 位坐标扩展成 20 位:
int32_t value_20bit = (int32_t)x; // x 为 int16_t // 符号扩展后低 20 位有效,范围 -524288~524287这样遇到 20 位驱动器时改一个参数就能适配,不需要重写时序生成函数。至于你的驱动器支持哪种位宽,不要猜,直接看数据手册里的 timing diagram。
2.4 CHANNEL 信号解决的场景
CHANNEL 信号有时被忽略,但在多轴系统里非常有用。振镜系统一般有两轴:X 轴控制水平偏转,Y 轴控制垂直偏转。如果每个轴单独拉一组 CLOCK/DATA 线,主控引脚占用多,线缆也多。CHANNEL 线的做法是让多个驱动器共享同一组 CLOCK 和 DATA,靠 CHANNEL 表示“这一帧是给谁的”。
比如发送 X 轴数据时,CHANNEL 拉低;发送 Y 轴数据时,CHANNEL 拉高。每个驱动器的接收端只在自己对应通道电平下采样。实际应用中,有些驱动器即使没有真正用到 CHANNEL 线,也会要求把这根线接上以避免悬空噪声误触发。所以就算你的系统只有一路振镜,我也建议把 CHANNEL 引脚显式拉高或者拉低,而不是悬空。
3. AM26LS31 在链路中的作用:逻辑电平转差分
3.1 为什么不能拿 GPIO 直接长线传输
STM32F103 的 GPIO 输出的是 3.3V 单端信号。单端信号的意思是:接收端判断高/低电平,靠的是信号线上的电压和地之间的差值。如果控制板和振镜驱动器之间距离一长,或者旁边有激光电源、伺服驱动器这类强干扰源,信号线上的地电位会和控制板的地电位出现偏差,接收端看到的就是“一个等于信号电压的共模噪声叠在信号上”,轻则波形变形,重则完全误码。
差分传输绕开了这个问题。AM26LS31 把一路单端 TTL/CMOS 信号转换成两路完全反相的差分输出 Y 和 Z。接收端不关心 Y 对地的绝对电压是多少,只关心 Y-Z 之间的差值。激光电源干扰过来的共模噪声在两根线上同时出现,接收端一相减就抵消了。
这就好比两个人打电话,单端是“一个人说话,声音大小靠他嗓门大小判断”;差分是“两个人一个说正话一个说反话,听的人只比较两个人的音量差”。哪怕周围环境很吵,只要两个声音同时被干扰,差值依然能保留原始信息。
3.2 AM26LS31 的典型接线
AM26LS31 是一片四路差分线驱动器,封装有 SOP16、DIP16 等,引脚逻辑不复杂。每一路的基本结构是:
输入 A ──► 输出 Y(正相) └─► 输出 Z(反相)片上有两个使能端,G 和 /G,用来统一控制所有输出。5V 供电即可工作,STM32 的 GPIO 输出高电平约 3.3V,已经超过 AM26LS31 作为 TTL 输入的 VIH 阈值(约 2V),可以直接驱动,不需要再电平转换。
一个 AM26LS31 正好有 4 路,对应 XY2-100 一轴的 CLOCK、DATA、SYNC、CHANNEL 四个差分信号。如果要做 X/Y 两轴独立信号线,就需要两片。
| AM26LS31 输入引脚 | STM32 GPIO | 差分输出 | 接双绞线 |
|---|---|---|---|
| 1A | PA4 | 1Y / 1Z | CLOCK± |
| 2A | PA5 | 2Y / 2Z | DATA± |
| 3A | PA6 | 3Y / 3Z | SYNC± |
| 4A | PA7 | 4Y / 4Z | CHANNEL± |
注意 AM26LS31 输出端是推挽结构的差分对,不是开漏,所以不需要上拉电阻。输出端如果不接负载,芯片也依然会输出差分电压。连接振镜驱动器时,通常双绞线到达接收端后,在接收端加一个 120Ω 左右的终端电阻,用来吸收线缆末端的反射能量,避免高速时钟边沿产生振铃。
3.3 接收端要靠什么芯片接回去
振镜驱动器端通常已经集成了差分接收电路,常见芯片是 AM26LS32 或兼容的 RS-422 接收器。AM26LS32 内部有四个接收器,每个接收器把差分输入转成单端 TTL 输出给驱动器内部的 FPGA/DSP。
接收端不是简单地把 Y、Z 两根线剥皮接进去就行,还有两个细节值得注意:
- 终端电阻:距离很短(小于 20cm)时可以不加;如果线长超过 0.5m,终端电阻基本是必须的。直接焊一个 120Ω 电阻在 Y-Z 之间就能明显改善波形。
- 接收端的共模电压范围:RS-422 标准允许接收端的地电位相对发送端有 ±7V 左右偏移。听起来很宽松,但我建议有条件还是让控制板地和驱动器地在供电端共地,不要只靠差分信号硬扛。
3.4 怎么判断物理层是否搭对
在没有振镜驱动器的情况下,也可以用 AM26LS32 接收器加一个示波器来验证发送端。更直接的办法是拿差分探头测 AM26LS31 的 Y、Z 输出端,或者用双通道普通探头,一个接 Y、一个接 Z,示波器用通道 A 减通道 B 的数学功能,直接看差分波形。
如果看到的是幅度约 2V 以上、上升沿清晰、没有明显台阶和振铃的方波,物理层基本是健康的。如果幅度低、有大量毛刺、边沿严重回勾,优先查终端电阻、屏蔽层接地和电源去耦。
4. STM32F103 生成 XY2-100 的两种常用实现
4.1 方案 A:定时器中断 + GPIO 逐位翻转(最容易理解和调试)
这个方法思路最直接:定一个定时器,让它按 XY2-100 的时钟频率产生中断,每次中断里翻转 CLOCK、设置当前位的数据。用 STM32F103 的定时器中断,在 72MHz 主频下做到 2MHz 的时钟翻转频率,完全可行。
代码如下,注意这是简化的核心逻辑:
#define XY_CLK_H() GPIOB->BSRR = GPIO_PIN_0 #define XY_CLK_L() GPIOB->BRR = GPIO_PIN_0 #define XY_SYNC_H() GPIOB->BSRR = GPIO_PIN_1 #define XY_SYNC_L() GPIOB->BRR = GPIO_PIN_1 #define XY_DATA_H() GPIOB->BSRR = GPIO_PIN_2 #define XY_DATA_L() GPIOB->BRR = GPIO_PIN_2 volatile uint16_t xy_data; // 待发送的 16 位位置数据 volatile int xy_bit; // 当前发送到第几位 void TIM3_IRQHandler(void) { if (TIM3->SR & TIM_SR_UIF) { TIM3->SR &= ~TIM_SR_UIF; if (xy_bit >= 0) { // 先放数据,再给时钟上升沿 if (xy_data & (1 << xy_bit)) XY_DATA_H(); else XY_DATA_L(); XY_CLK_H(); XY_CLK_L(); xy_bit--; } else { // 一帧结束,同步拉低,锁存数据 XY_SYNC_L(); // 这里可以置位标志位,通知主循环发下一帧 frame_done = 1; TIM3->CR1 &= ~TIM_CR1_CEN; // 停止定时器 } } } void XY_SendFrame(uint16_t pos) { while (frame_done == 0 && timer_running); xy_data = pos; xy_bit = 15; XY_SYNC_H(); frame_done = 0; TIM3->CR1 |= TIM_CR1_CEN; }这个方案的优点是非常直观,逻辑全在明处,非常适合第一版调通。缺点是两个中断服务函数里干活太多,如果还要同时处理串口、激光开关、插补计算,中断会占掉大量 CPU 时间。实测在 2MHz 时钟下,发送一帧 16 位数据大约 8µs,定时器中断每次执行时间如果控制在 1µs 以内,点率能到比较可用的范围。但如果要把点率推到几十 kHz 以上,还要跑插补算法,这个方案就会捉襟见肘。
4.2 方案 B:DMA + SPI + 定时器(高帧率推荐)
要解放 CPU,可以用 DMA 把预先准备好的“定时器更新事件触发引脚翻转”的数据灌给 GPIO。思路是:
- 把一帧里每一位的状态预先组织成一个 32 位 GPIO 寄存器值数组。
- 定时器以 XY2-100 的位时钟为更新频率触发 DMA。
- DMA 每次传输把这个数组中的一个值写到 GPIO 的 BSRR/BRR 寄存器,完成引脚翻转。
例如 STM32F103 的 GPIOB BSRR 地址 0x40010C10,BRR 地址 0x40010C14。数组里每个元素可以是:
uint32_t frame_bits[16]; // 示例:第 i 位数据为高,CLOCK 低时写 BSRR,CLOCK 高时写 BRR具体如何配置 DMA 通道和定时器内联,每个项目差异较大,这里不贴完整代码,重点是要理解它的核心逻辑:DMA 以固定速率把内存中的位模式“搬运”到引脚,和 XY2-100 要求的时钟时序一一对应。
这个方案的最大优势是 CPU 只负责准备数据,实际信号生成完全由定时器和 DMA 硬件完成,位置更新率可以做得更高,CPU 还有余量去跑直线插补、功率控制、串口交互。我在实际项目中是用 STM32F103 内部定时器触发 DMA,传输完成后触发一次半传输/完全传输中断,在中断里填充下一帧数据,做到了 40kHz 以上的坐标更新率,CPU 占用依然很低。
4.3 为什么有人用 SPI 做却总是对不齐
网上也有人直接用 SPI 的 MOSI 输出数据、SCLK 输出时钟来做。理论上可行,但会踩一个坑:XY2-100 的时钟和数据边沿相对关系,和 SPI 的 CPOL/CPHA 四种模式并不是天然对齐的。SPI 主机在某个时钟沿送出数据,从机在另一个沿采样,这个配合只适合 SPI 从机;XY2-100 接收端要求的是“数据先稳定,然后时钟某个边沿采样”。两者看起来都叫“时钟 + 数据”,但时序约束不一样,直接拿 SPI 模组去拼,经常出现数据与时钟边沿对不齐,驱动器采样到了毛刺。
所以更可控的路径是:把 SPI 当作纯粹的“数据移位输出”,CLOCK 信号则用定时器 PWM 产生,并软件保证两个外设的相位关系。DMA 负责把数据从内存连续送到 SPI 数据寄存器,定时器负责给出一位一位的时钟。这样既绕开了软件中断开销,又不像纯 GPIO 那样每帧都要 CPU 介入。
4.4 数据准备和插补的注意点
时序生成只是信号链的前半段,真正让电机跑得顺畅的还有坐标插补。STM32F103 算力有限,不建议在中等复杂图形上做太高级的样条插补。实用做法是:上位机或 PC 端把图形预处理成稠密点序列,STM32 只做直线插补的切分和下发。
举个例子,一段从 (0, 0) 到 (1000, 0) 的线段,打标速度要求 2000mm/s,点距 0.05mm,那么每秒要输出 40000 个点。STM32 按定时器节拍每 25µs 取一个点,通过插补公式计算当前坐标,填入发送缓冲区。注意 XY2-100 的一帧数据发送本身是异步的,缓冲区一定要用双缓冲或环形队列,否则上一帧还没发完,下一帧已经开始覆盖数据,就会出现振镜一顿一顿的现象。
5. 完整信号链搭建与示波器验证
5.1 一套可落地的接线表
我用 STM32F103C8T6 最小系统板加一片 AM26LS31 模块搭过一版,接线如下,供直接参考:
| 信号 | STM32F103 引脚 | AM26LS31 输入引脚 | 驱动器接线端 |
|---|---|---|---|
| CLOCK | PA4 | 1A | CLOCK± 差分对 |
| DATA | PA5 | 2A | DATA± 差分对 |
| SYNC | PA6 | 3A | SYNC± 差分对 |
| CHANNEL | PA7 | 4A(固定高/低电平) | CHANNEL± 差分对 |
STM32 与 AM26LS31 之间尽量短距离连接,杜邦线长度控制在 10cm 内。AM26LS31 的 5V 供电和 STM32 的 5V(或外部稳压 5V)共地。差分输出到振镜驱动器之间用双绞线,线径 24AWG 左右,屏蔽层如果接,就在控制板端单端接地。
5.2 上电前先做的三项检查
第一,确认所有差分引脚没有接反。Y/Z 或 A/B 标号接反,接收端看到的就是反相波形,驱动器会认为数据位全部倒置。
第二,确认 CHANNEL 引脚不是悬空。没有实际通道切换需求时,把它在代码里固定为高或低,避免驱动器误采样。
第三,确认振镜驱动器接收端的终端电阻。如果驱动器没有内置终端电阻,就在差分输入端并一个 120Ω 电阻。没有它,2MHz 时钟沿的反射会直接导致数据抖动。
5.3 用示波器看哪几个关键波形
不用一上来就接振镜,先把 STM32 和 AM26LS31 这一级调试好。第一个测点是 AM26LS31 的输入端,也就是 STM32 的 GPIO 引脚,确认 SYNC 能周期性地拉高、CLOCK 有连续脉冲、DATA 上有随数据变化的电平。第二个测点是 AM26LS31 的输出端,用差分方式观察 CLOCK±,确认差分幅度正常。第三个测点是接收端,接上长线后观察 DATA± 在 CLOCK 边沿的时刻是否稳定。
如果示波器只有一个普通探头,也可以把探头接地夹子夹在控制板的地上,分别测 Y 和 Z 两根线,再在脑子或纸上做减法。这种方法能看到毛刺,但不如真正的差分探头准确。
5.4 一个简单的验证程序流程
初始化 GPIO 后,先不放插补,直接让程序每 10ms 发送一组固定的递增坐标,比如从 0 开始每次加 100。上电后,用示波器观察 DATA 波形是否只在一帧间隔内变化、SYNC 宽度是否稳定、CLOCK 频率是否符合预期。这一阶段只要逻辑上对,振镜就应该按比例偏转。
如果振镜完全不动,优先怀疑 SYNC 极性。XY2-100 不同驱动器对 SYNC 的有效电平定义不一样,有的高有效,有的低有效,这也是我自己第一次调板子时卡了最久的问题。
6. 实际使用中容易忽略的坑:从信号质量到项目管理
6.1 终端电阻超过 300Ω 等于没接
很多人知道要加终端电阻,但随手找了一个 10kΩ 电位器或 1kΩ 电阻就并上去。对 2MHz 时钟来说,终端电阻要和双绞线特征阻抗匹配才有意义。RS-422 双绞线常见特征阻抗约 100~120Ω,所以终端电阻取 120Ω 最合适,最大不要超过 330Ω。实测用 1kΩ 和 120Ω 对比,时钟上升沿的过冲和振铃差距非常明显。
6.2 地线处理要遵循“一段一段来”
差分传输能抵消共模干扰,但不代表可以不共地。RS-422 接收器的输入共模范围看起来有 ±7V,但那是标准极限,实际驱动器为了抗静电,输入保护电路可能限制得更窄。稳妥做法是:控制板电源、AM26LS31 电源、振镜驱动器电源,全部在同一个配电接入点共地。如果振镜驱动器和激光电源离得远,中间用专门的地线连接,不要只靠差分线内部的地线承担大电流回流。
6.3 位置值范围和符号别搞反
XY2-100 发送的是有符号位置数据。16 位模式下,0x7FFF 和 0x8000 往往对应机械角度的两极,0 是中位。如果直接把加速度、速度转换出来的无符号数填进去,等于告诉振镜“偏到极限”,轻则发出异响,重则撞到机械限位。最好在初始化时先发 0 值,确认镜片回到中位,再递增测试。
6.4 激光控制和位置输出同步,是另一条容易漏的线
打标不只是“振镜动”,还要在合适的时候打开激光。激光器通常需要两路信号:一路 TTL 开关控制激光出光,一路 PWM 模拟信号控制功率。这两路信号与 XY2-100 的位置数据必须同步:如果激光在振镜还在快速移动时点亮,图形就会产生圆角或拖尾。
实际工程中,我会在帧发送完成中断里同时更新激光开关状态。比如插补器算出当前位置后,同时算出这一点的激光功率,在下一帧同步时随位置一起输出。STM32F103 的定时器 PWM 用于功率控制正好,占空比更新通过 DMA 或直接寄存器写入,保证不占用太多 CPU。
6.5 不要把协议文档看成完全一致的真理
XY2-100 只是一个广为流传的名字,不是国际标准。同一片驱动板上,固件版本不同都可能导致 SYNC 极性、位宽、CHANNEL 时序发生变化。我接触过的驱动器里,有的 CLOCK 上升沿采样,有的下降沿采样; SYNC 有的拉高表示启动,有的拉低表示锁存。所以做工程时,第一优先级永远是手里的驱动器数据手册,而不是网上随手搜到的某个框架图。如果手册写得不清楚,直接问厂商要例程和 timing diagram,比盲调快得多。
6.6 调试顺序决定了效率
我的调板顺序一直是:先调试物理层,再调试协议时序,最后才接振镜。物理层没有调好之前,协议层再正确也是浪费时间。每一步用示波器确认一个事实:
- AM26LS31 输入端有正确 TTL 波形。
- AM26LS31 输出端有正确差分波形。
- 接收端差分波形无异常毛刺、抖动。
- 用逻辑分析仪抓 CLOCK/DATA/SYNC,确认帧格式和驱动手册一致。
- 再接振镜负载,小幅坐标偏移测试响应。
这套流程走下来,绝大多数问题都出在第二步到第四步之间,很少真正需要动电机。
回头再看我自己最初踩过的坑,最浪费时间的反而不是芯片选型或代码复杂度,而是没有把协议和物理层拆开去判断。只要把 STM32F103 产生时序、AM26LS31 做差分驱动、接收端按帧锁存这三段各自验证清楚,整个信号链就通了。后续如果再遇到振镜不听话,优先怀疑的也就那几处:SYNC 极性、终端电阻、数据符号范围、CHANNEL 悬空。把这些都处理掉,剩下真正要做难的,才是图形算法和工艺参数本身。