XY2-100协议详解:从差分信号到FPGA/MCU振镜控制方案
手里拿到一台带数字接口的激光振镜翻遍驱动盒上的丝印只看到 CLOCK、SYNC、CHANNEL、DATA 四对差分信号。没接触过的人第一反应是找串口找不到就开始慌。其实这四根线就是振镜界的“普通话”——XY2-100 协议。它没有复杂的握手过程也没有报文头校验本质就是一台 2MHz 的串行移位器主机按固定时序把 20 bit 的帧推给振镜X 轴和 Y 轴轮流接收仅此而已。很多工程师拿到这块的第一反应是不知从哪下手尤其当你的控制端是 MCU 或者 FPGA 时协议本身不复杂但硬件连接、帧时序、双通道协调这些细节每一个都可能让振镜纹丝不动或者画出莫名其妙的线。这篇东西我按自己实际做项目的顺序来写先讲清楚协议帧到底长什么样再讲硬件上怎么接然后分别给出 FPGA 和 MCU 两套能直接跑的方案最后是调试波形和避坑经验。适合正在做激光打标、激光焊接、光通信对准或者单纯想把振镜转起来的工程师参考。1. XY2-100协议到底在传什么先搞懂帧结构再写代码1.1 振镜为什么需要一套数字协议振镜这个东西本质是两个高精度伺服电机分别带动 X 和 Y 反射镜片偏转激光经过镜片反射后投射到工作台面上。早年控制振镜多用模拟量主机输出 ±10V 电压电压高低直接对应镜片摆角。模拟方案便宜但长距离传输容易受干扰零点还会随温度漂移尤其是在激光电源这种强干扰源旁边模拟信号经常让光斑位置飘得没法看。XY2-100 就是在这种背景下被广泛采用的一种数字协议。它用差分信号传输位置数据抗干扰能力强而且主机发给振镜的就是数字量误差只取决于 DAC 分辨率和机械装配精度不会因为线缆长了就衰减。现在市面上常见的 Scanlab、CT、金橙子等品牌数字振镜几乎都兼容或支持 XY2-100 帧格式。1.2 四对差分信号和一个数据帧XY2-100 主机端到振镜驱动器的信号线通常只有四对信号方向作用CLOCK/-主机 → 振镜串行时钟标准频率 2MHzSYNC/-主机 → 振镜帧同步信号同步字CHANNEL/-主机 → 振镜通道选择区分 X 轴和 Y 轴DATA/-主机 → 振镜串行数据20 bit 一帧部分振镜还会有 ENABLE、FAULT、POWER 等辅助信号但控制核心就是上面这四对。协议的精髓在于所有信号都跟着 CLOCK 走CLOCK 一个周期传一位SYNC 负责宣布“我要开始发一帧了”CHANNEL 负责告诉振镜“这帧数据是给 X 还是给 Y”。一帧数据共 20 bit排列顺序如下前 16 bit位置数据二进制补码MSB 在前高位先发后 4 bit控制/状态位常见定义包括 POWER激光使能、MASK遮挡/标记、OVERRIDE速度倍率和保留位位置数据的含义很直观0x0000 对应振镜中心位置0x7FFF 对应正向最大摆角0x8000 对应反向最大摆角。以光学摆角 ±10° 的振镜为例16 bit 分辨率对应每一级约 20°/65536 ≈ 0.0003°足够细腻。1.3 一次完整传输的时序拆分实际发送一帧的过程可以拆成三个阶段同步阶段主机把 SYNC 拉高同时把 CHANNEL 设置为目标通道X 或 Y。SYNC 高电平至少要保持 4 个 CLOCK 周期工程上我习惯放到 810 个留足裕量。数据阶段SYNC 拉低随后在接下来的 20 个 CLOCK 上升沿DATA 线逐位输出。先发 D15MSB最后发控制位的最低位。间隔阶段一帧数据发完后主机可以插入若干 CLOCK 周期的 GAP也可以立刻开始下一帧。振镜端会持续解析收到的位置所以只要持续发帧振镜就持续跟随如果长时间不发帧不同厂家的振镜行为会有差异有的锁在最后位置有的回零具体看手册。这里有两个很多新手容易犯的错。第一是 CLOCK 不能停在一个电平上必须持续翻转不然振镜端采样不到位。第二是 CHANNEL 信号必须在 SYNC 为高期间保持稳定振镜大多是在 SYNC 末端采样 CHANNEL以此决定后续 DATA 归属哪个轴如果 CHANNEL 在 SYNC 期间跳变这一帧很可能会被丢或者发错轴。1.4 刷新率够不够用2MHz 时钟下一帧 20 bit 需要 10us再加上 SYNC 和 GAP按 30 个 CLOCK 周期算一帧约 15us。X、Y 两轴轮询发送一个完整轮询周期约 30us单轴刷新率 33kHz 左右。这个数字远高于振镜机械带宽通常 12kHz完全够用。即使把 SYNC 压缩到 4 个周期也才 24 个 CLOCK单轴 41kHz。所以不用纠结刷新率不够真正要纠结的是帧数据是不是连续、是不是会在传输中被改写。2. 硬件连接的关键细节差分收发器、线缆与终端匹配2.1 先分清电平标准别拿 LVDS 引脚直接怼XY2-100 虽然没在标准文档里把电平规范写成一本书但业界约定俗成用 RS-422 差分电平。也就是说主机输出端需要把 3.3V 或 5V 逻辑信号转换成差分对振镜驱动器端再接差分接收器还原成逻辑电平。有些 FPGA 开发板上有现成的 LVDS 差分对不少人会想当然直接把这些引脚接到振镜上结果要么信号完全无响应要么把引脚烧了。原因很简单LVDS 的共模电压约 1.2V摆幅只有 350mV而 RS-422 接收器的输入共模范围通常可以到 -7V7V两者电气特性根本不匹配。老老实实加 RS-422 收发器是最稳妥的方案。主机侧推荐芯片5V 系统DS26C31、AM26LS313.3V 系统AM26LV31、DS26LV31振镜身侧如果接收芯片坏了需要替换通常用 DS26C32、AM26LS32 这类四路差分接收器。2.2 接线、终端电阻、共地一个都不能少我做的第一版控制板因为图省事直接买了成品差分线缆结果振镜在高速扫描时偶尔抽风。排查到最后发现是终端电阻重复匹配的问题。正确做法主机端差分管脚到振镜接收端每一对差分信号在接收端跨接一个 120Ω 终端电阻用来吸收高频反射。但很多振镜驱动器内部已经集成这个电阻如果外部再加一个等效阻抗变成 60Ω信号幅值反而被拉低。判断方法很简单断电后用万用表量振镜侧差分输入端子的电阻如果已经接近 60Ω内部两个 120Ω 并联或 120Ω说明内部有匹配外部不用再加如果接近开路再在端子上跨接 120Ω。线缆方面用双绞线或者带屏蔽层的多芯电缆每对信号占一对双绞线。屏蔽层单端接地避免形成地环路。距离超过 3 米时尽量让 CLOCK 对和 DATA 对分开一点不要和电源线捆在一起。还有一点容易被忽略RS-422 是差分传输但两端必须共地。真遇到过工厂现场把控制器和振镜驱动分别接在不同的开关电源上两地之间压差几伏长时间运行后接收芯片发烫甚至烧毁。正确做法是信号地线单独连一根或者用带隔离的 RS-422 收发器。2.3 上电顺序与供电隔离振镜驱动器需要 24V 或 ±15V 供电控制板一般用 3.3V/5V 逻辑。上电顺序建议先给驱动器上电稳定 12 秒后再给控制板下发帧数据。反过来如果控制板先跑起来某些驱动器检测到异常信号会直接锁死或报 FAULT。激光器是很大的干扰源尤其 PWM 调制的激光电源开启瞬间噪声会顺着地线窜到控制板。我习惯在控制板电源入口加共模电感信号线的差分芯片和振镜驱动器之间如果距离远还可以串磁珠。这些都是成熟产品上常见的做法。3. FPGA方案状态机、双通道轮询与防抖设计3.1 为什么先讲 FPGAFPGA 做 XY2-100 控制有一个天然优势所有信号由逻辑电路直接产生延迟可以做到纳秒级确定性不会像 MCU 那样被中断、RTOS 调度打断。如果项目里有多路振镜、需要和激光开关严格同步、或者要做实时矫正表插值FPGA 是最合适的载体。3.2 顶层模块结构以 Xilinx 或国产高云/紫光系列 FPGA 为例整个 XY2-100 发送链路可以切成几个模块上位机接口UART、SPI 或以太网接收坐标数据坐标 FIFO异步 FIFO隔离时钟域缓存上位机下发的 XY 坐标2MHz 时钟生成从系统时钟分频或 PLL 生成XY2-100 发送状态机负责产生 SYNC、CHANNEL、DATA 时序输出引脚经过 OBUFDS 或外部 RS-422 芯片驱动差分输出顶层端口大致如下module xy2_100_top #( parameter CLK_FREQ 50_000_000 )( input wire clk, input wire rst_n, input wire uart_rx, output wire clk_out_p, output wire clk_out_n, output wire sync_out_p, output wire sync_out_n, output wire channel_out_p, output wire channel_out_n, output wire data_out_p, output wire data_out_n );注意如果板上已经有 RS-422 差分芯片FPGA 这边直接用单端引脚输出逻辑信号即可如果只有普通 IO要加外部收发器。3.3 核心状态机的设计思路状态机是整个方案的心脏。我的设计包含五个状态IDLE、SYNC_SEND、DATA_SEND、GAP、CH_SWITCH。IDLE检测 FIFO 是否有新坐标同时判断当前该发 X 轴还是 Y 轴SYNC_SEND维持 SYNC 高电平 8 个 CLOCKCHANNEL 稳定在目标通道DATA_SEND20 个 CLOCK 周期逐位移出位置数据和控制位GAP插入 2 个 CLOCK 周期的空闲间隔CH_SWITCH切换通道标记回到 IDLE发送状态机核心代码可以这样写localparam IDLE 3d0; localparam SYNC_SEND 3d1; localparam DATA_SEND 3d2; localparam GAP 3d3; localparam CH_SWITCH 3d4; reg [2:0] state, next_state; reg [5:0] bit_cnt; reg [4:0] sync_cnt; reg [19:0] shift_reg; reg ch_x; // 1: X通道, 0: Y通道 reg data_out_i, sync_out_i, channel_out_i; wire msb shift_reg[19]; always (posedge clk) begin if (!rst_n) begin state IDLE; end else begin state next_state; end end always (*) begin next_state state; case (state) IDLE: if (fifo_empty 1b0) next_state SYNC_SEND; // 注意这里要同时判断轮询到哪个轴 SYNC_SEND: if (sync_cnt 5d9) next_state DATA_SEND; DATA_SEND: if (bit_cnt 6d20) next_state GAP; GAP: if (bit_cnt 6d2) next_state CH_SWITCH; CH_SWITCH: next_state IDLE; endcase endDATA 阶段的移位逻辑我习惯用移位寄存器从高位推送每来一个 CLOCK 上升沿把 shift_reg 左移一位最高位即当前要发送的数据位always (posedge clk) begin if (!rst_n) begin shift_reg 20d0; end else if (state SYNC_SEND sync_cnt 5d9) begin // 在 SYNC 结束前锁存当前通道的位置值 if (ch_x) shift_reg {pos_x[15:0], 4b1000}; else shift_reg {pos_y[15:0], 4b1000}; end else if (state DATA_SEND) begin shift_reg {shift_reg[18:0], 1b0}; end end这里控制位写成了 4b1000含义是 POWER1、MASK0、OVERRIDE0、保留位0具体取决于你的振镜定义。控制位字段务必对照手册确定不要照抄我的数值。3.4 双通道轮询与“传输中途锁存”XY2-100 是半双工总线X 和 Y 必须分时发送。我的做法是维护一个单 bit 的通道选择寄存器 ch_x每一轮完整发送完成后翻转一次。IDLE 状态下如果轮到 X 轴就从 FIFO 读 x 坐标轮到 Y 轴就读 y 坐标。这里最容易被忽略的是坐标更新时机。如果在上位机不停下发坐标的过程中数据总线在任意时刻直接连到移位寄存器那很可能出现这一帧的前半段是旧坐标、后半段是新坐标振镜会画出一道毫无意义的小毛刺。正确做法是移位寄存器在 SYNC_SEND 结束的那个时钟沿锁存坐标值之后 20 位 DATA 全部从这个锁存器输出不允许中途改写。坐标更新只在 IDLE 阶段进行这样能保证每一帧的位置数据都是完整的。3.5 复位与跨时钟域处理FPGA 工程里复位信号的设计直接影响稳定性。直接给状态机用异步复位在复位释放时如果正好靠近 CLOCK 沿就可能出现亚稳态导致状态机跑飞。我一般用“异步复位、同步释放”结构或者干脆全部用同步复位简单可靠。如果项目里有多个时钟域比如 UART 是 115200 波特率的 9.6MHz 时钟而 XY2-100 是独立 2MHz 时钟那上位机坐标必须通过异步 FIFO 打拍不能直接跨时钟采样。生成固化文件这步Xilinx 下是 Generate Bitstream 后写入 QSPI Flash国产软件也都有类似“固化”操作。调硬件时可以先跑 JTAG稳定后转固化流程上没有特别多坑。4. MCU方案定时器PWMDMA硬模拟XY2-1004.1 三种思路选最稳的很多项目不想上 FPGA希望用一颗 STM32 或者 GD32 搞定控制。问题来了MCU 没有 XY2-100 专用外设怎么模拟我见过三种常见实现方案精度CPU占用说明GPIO 延时翻转差高代码简单但抖动大振镜高速时容易毛刺SPI DMA中低用 SCK 做 CLOCK、MOSI 做 DATA但 SYNC/CHANNEL 时序难以精确对齐定时器 PWM DMA 更新 GPIO高极低2MHz PWM 输出 CLOCKDMA 每个周期更新 SYNC/DATA/CHANNEL推荐我直接推荐第三种综合精度和资源占用都最理想。4.2 核心原理时钟由硬件产生数据由 DMA 按节奏投喂思路是这样的定时器的一个通道配置成 PWM 模式输出 2MHz 方波直接作为 CLOCK 信号。同时在定时器的更新事件里触发 DMA 请求DMA 从内存中读取预编排好的数组每个数组元素对应一个 CLOCK 周期里 SYNC、DATA、CHANNEL 三个引脚的电平状态直接写入 GPIO 的 ODR 寄存器。也就是说CLOCK 完全由硬件定时器产生不受代码干扰DMA 负责在和 CLOCK 同步的节拍下更新其他引脚。这样 CPU 只需要负责构造数组、启动 DMA之后整个帧的发送不占 CPU非常干净。以 STM32F103 为例配置一个定时器输出 PWM 频率 2MHzDMA 请求选择更新事件Update Event搬运方向为 Memory-to-Peripheral外设地址写 GPIOB-ODR内存地址指向帧缓冲区数组。4.3 帧缓冲区的构造方法假设控制板引脚下SYNC 接到 GPIOB0CHANNEL 接到 GPIOB1DATA 接到 GPIOB2。数组元素就是这几个 GPIO 引脚的电平组合。一个典型的 XY2-100 帧缓冲区可以这样构造前 8 个元素SYNC1CHANNEL 固定为目标通道DATA 保持 0紧接着 16 个元素SYNC0CHANNEL 保持DATA 依次为位置数据位 D15D0再 4 个元素SYNC0CHANNEL 保持DATA 依次为控制位最后 2 个元素所有信号回到空闲电平代码示意如下#define FRAME_LEN 30 uint16_t frame_buf[FRAME_LEN]; void build_frame(uint16_t ch, int16_t pos, uint16_t status) { uint32_t base 0; if (ch CH_X) base | (1 1); // CHANNEL1 选 X0 选 Y // 注意根据实际引脚映射调整 for (int i 0; i 8; i) { frame_buf[i] base | (1 0); // SYNC 拉高 } for (int i 0; i 16; i) { uint16_t bit (pos (15 - i)) 0x1; frame_buf[8 i] base | (bit 2); } frame_buf[24] base | ((status 0x1) 2); frame_buf[25] base | ((status 0x2) 2); frame_buf[26] base | ((status 0x4) 2); frame_buf[27] base | ((status 0x8) 2); frame_buf[28] base; frame_buf[29] base; }写完数组后启动 DMA 传输一次搬 30 个 uint16_t每来一个定时器更新事件搬一个元素。DMA 传输完成中断里可以准备下一帧数据。整个发送过程里CLOCK 始终由定时器 PWM 输出稳定且无抖动。4.4 DMA 传输完成的时机与双缓冲如果想要连续刷新振镜最好准备两个缓冲区一个被 DMA 当前读取另一个供 CPU 填充新坐标。在 DMA 传输完成中断里交换两个缓冲区。这样就是双缓冲不会出现 CPU 和 DMA 同时操作同一个数组导致半帧新值、半帧旧值的问题。DMA 中断函数里建议只做交换指针的操作不要在里面做复杂的坐标插值否则中断处理时间过长会影响下一帧的启动。坐标生成、插值、矫正表查找这些应该放在主循环或者低优先级任务里完成。4.5 MCU 方案的性能边界STM32F103 在 72MHz 主频下跑这套方案发送 30 个元素也就是 15us 一帧双通道完整轮询 30us单通道刷新率 33kHz和 FPGA 方案几乎没差别。CPU 占用主要体现在构造帧缓冲区、查矫正表、跑插值算法发送过程本身不占 CPU。如果用的是更高主频的 MCU比如 H7 系列还能在中断里做更多事情。5. 联调排错示波器波形判读与三个高频故障5.1 上电前先过一遍检查清单我每次接新振镜上电前都会花两分钟快速核对这几项差分对是否接反CLOCK 是否对 CLOCKSYNC 是否对 SYNC控制板地和振镜驱动地是否共地振镜侧终端电阻是否匹配外部有没有重复并接 120Ω上电顺序振镜驱动器先上电再给控制板信号这些基础项检查花不了多少时间但能省掉后面一大半的返工。5.2 用示波器看波形先看 CLOCK再看 SYNC 和 DATA 对齐联调时最好有差分探头没有的话就用普通探头分别测正端和负端再用示波器的数学通道做 A-B 得到差分波形。至少抓三路信号CLOCK、SYNC、DATA。正确波形应该是这样CLOCK 是一路稳定的 2MHz 方波占空比接近 50%没有明显振铃SYNC 拉高期间至少持续 4 个以上 CLOCK 周期SYNC 拉低后DATA 在每一个 CLOCK 上升沿附近稳定输出数据位如果 DATA 变化时刻和 CLOCK 边沿关系混乱振镜很容易误采样。用示波器测数据建立时间保证 DATA 在 CLOCK 上升沿前至少几十纳秒已经稳定。这也是为什么建议用 DMA 更新 GPIO因为 GPIO 在定时器更新事件里被改写天然比 CLOCK 上升沿提前一个固定时间。5.3 三个高频故障的排查我在实际调试中遇到最多的三类问题现象可能原因排查方向振镜完全不动差分极性反、SYNC 没拉高、CLOCK 没有持续翻转示波器抓 CLOCK、SYNC确认帧在发只动一个轴CHANNEL 选通错误或 X/Y 坐标接到同一个通道检查通道状态交换 CHANNEL 极性试试位置图形镜像/反转位置数据字节序反了或 X/Y 信号接反固定打一条水平线确认哪根轴出了问题5.4 实战中遇到过的两个案例案例一第一次接金橙子驱动图形左右镜像。查了半天发现不是协议问题而是 X 轴差分线接反了导致振镜收到正方向位置时往负方向偏。把差分对交换后正常。案例二客户反映振镜“每隔几秒抖一下”。排查发现是 MCU 在高优先级中断里做了浮点坐标插值导致 DMA 双缓冲切换延迟超过一帧时间偶发丢帧。解决方法是把插值结果预先算好存到缓冲区中断里只做指针切换问题立即消失。6. 从能握手到扫得准矫正表、跳转规划与激光同步6.1 别忽略机械畸变矫正表为什么必须做XY2-100 只是把坐标数据忠实地传给了振镜但激光打出来的图形往往会变形常见的有桶形畸变、梯形畸变、枕形畸变。原因包括反射镜安装角度误差、两轴间距、场镜固有畸变等。简单说你发一个正方形坐标实际打出来可能是一个桶形失真的正方形。解决思路是建立矫正表。先在靶纸上打一个已知间距的网格比如 16×16用相机或人眼读取每个点的实际坐标和理想坐标对比得到一组偏移量。实际控制时对于任意目标坐标在矫正表里查四个邻近网格点做双线性插值得到修正后的驱动坐标。在 FPGA 上可以把矫正表存在 BRAM 或外部 Flash坐标下发的每个点都查表插值算法不复杂但要有流水线设计思路避免成为性能瓶颈。在 MCU 上直接查表插值即可16×16 网格全表才 256 个点双线性插值计算量很小。6.2 跳转速度和标记速度要分开振镜和普通伺服电机一样速度和加速度过大会超调。实际打标任务一般分两类动作移动时不打激光这叫 Jump移动同时打激光这叫 Mark。Jump 追求速度快允许有一点轨迹误差Mark 讲究轨迹精度速度要降下来而且开激光的瞬间不能有抖动画出一个“小尾巴”。控制器端实现思路Jump 和 Mark 分别设置最大速度、加速度参数内部做一个简单的梯形加减速或者 S 形加减速限制相邻帧位置增量。FPGA 方案里可以用累加器限制每帧最大步进MCU 方案里放在定时器中断外的主循环里算好位置值再写入帧缓冲区。6.3 激光开关要和位置同步XY2-100 后 4 个控制位里的 POWER/MASK 就是干这个的。正确顺序是Jump 状态下先移动到达目标点并且振镜稳定后再把 MASK 关掉允许出光并开始 MarkMark 结束要立刻把 MASK 拉起来挡住激光再做下一次 Jump。如果在移动过程中不关激光出来的不是直线而是一条条飞溅的曲线。这里有个经验MASK 变化要比位置变化提前一点点激光器响应有延迟。提前量可以从几十微秒到几百微秒需要在现场边试边调。最后再分享一个我自己的习惯不管用 MCU 还是 FPGA先把 XY2-100 的时序写死成一组固定坐标比如只在中心点和右极限来回切换用逻辑分析仪或示波器把总线波形抓干净、确认无误后再接上位机和矫正表。这样后面出问题至少能确定协议层是对的剩下的锅都在坐标生成和机械装配上。