
1. 从激光打标机里那块“不听话”的板子说起如果你拆过工业激光打标机、激光焊接机或者激光雷达的扫描头大概率会在振镜电机屁股后面看到一根二十来根线的排线另一端连着一块巴掌大的驱动板。这块板子干的事很专一把上位机发来的“往左偏 3.2 度、往右偏 1.8 度”这种指令翻译成振镜电机能听懂的模拟电流信号。而在这块板子和上位机之间跑的那套通信规矩十有八九就是XY2-100。我第一次接触 XY2-100 是在一个激光打标项目上当时用一块通用 FPGA 板卡去对接某国产振镜上位机发指令过去镜子要么不动要么乱抖。示波器一挂发现时钟和数据相位完全对不上——问题就出在对这套协议时序的理解上。后来把协议啃透、用 Verilog 重写了一遍收发逻辑才算彻底稳住。所以这篇东西不打算写成一份干巴巴的协议手册而是按我实际踩坑的顺序把XY2-100 协议、激光振镜控制原理、以及Verilog/FPGA实现这三块串起来讲清楚。XY2-100本质上是一个面向数字振镜的同步串行通信协议规定了时钟、同步信号、数据三根线的时序关系以及 16 位位置数据的编码方式。它解决的问题很具体让控制器用一套标准接口去驱动不同厂家的振镜而不用每家都写一套私有驱动。适合谁看做激光加工设备、3D 打印扫描头、激光雷达、光学扫描系统的嵌入式工程师和 FPGA 开发者尤其是那些手里有振镜但被时序折磨过的人。下面我会从协议帧结构讲到 Verilog 状态机再到实测中那些文档里不会写的坑。2. XY2-100 协议帧结构三根线里到底塞了什么2.1 时钟、同步、数据三线制的分工XY2-100 是同步串行接口物理层就三根关键信号线CLOCK时钟、SYNC同步/帧同步、DATA数据。有的实现还会带一根使能或方向线但核心就是这三根。理解它的关键是把这三根线想象成一支三人乐队时钟是节拍器同步是每小节的第一拍重音数据是乐手按节拍吐出的音符。时钟频率典型值是2MHz也就是说一个时钟周期 500ns。整个一帧数据由20 个时钟周期组成这 20 个周期里塞进了两个通道X 和 Y各 16 位的位置信息外加一些控制位。注意20 个周期不是随便定的它是协议规定的固定帧长多一个少一个振镜都不认。同步信号 SYNC 的作用是“划重点”它在一帧的起始位置拉高或拉低取决于具体实现约定告诉振镜“新的一帧开始了准备接收”。数据线 DATA 则在时钟的边沿被采样逐位把 20 位数据送出去。这里有个容易搞混的点数据是在时钟的上升沿还是下降沿有效不同厂家文档写法不一实测下来大多数振镜是在时钟上升沿采样数据所以 FPGA 侧应该在下降沿更新数据保证上升沿时数据已经稳定。2.2 20 位帧里 X、Y 和控制位怎么排一帧 20 位具体怎么分配是很多人第一次看协议最懵的地方。常见的分配方式是第 0 位到第 15 位中的一部分给 X 轴另一部分给 Y 轴中间还夹着状态位和控制位。不同资料给的位序略有差异但主流的一种排布是这样的位序号含义说明0控制位/状态位部分实现用作使能或标志1-8X 轴低 8 位X 位置数据的一部分9-16Y 轴低 8 位Y 位置数据的一部分17-19高位/校验/控制补齐 20 位含高位或握手位需要强调的是XY2-100 的位置数据是 16 位精度对应振镜的满量程偏转范围。也就是说0 到 65535 这 65536 个码值线性映射到振镜的机械偏转角度范围比如 ±20 度。这里就引出一个实操中必须算清楚的换算如果你的振镜满量程是 ±20 度那么 1 个码值对应的角度是 40/65536 ≈ 0.00061 度。上位机给你一个“偏转 5 度”的指令你得先算出对应的码值 5/0.00061 ≈ 8197再把这个数拆成高低位塞进帧里。算错这一步镜子要么偏得离谱要么直接撞限位。2.3 为什么是 16 位而不是 12 位或 20 位这个问题我在项目评审时被问过。答案其实很朴素16 位是精度和成本的平衡点。激光打标的分辨率要求通常在微米级对应到振镜角度就是千分之几度12 位4096 级在大幅面扫描时会出现明显的“台阶感”也就是相邻两个码值之间的角度跳变肉眼可见。而 20 位虽然更精细但对 DAC 和电机驱动电路的噪声要求极高低位数据很容易被噪声淹没实际有效精度上不去反而浪费带宽。16 位刚好卡在“够用且稳定”的区间这也是它成为行业事实标准的原因。另外16 位数据在 FPGA 里处理起来特别顺手——正好两个字节移位、拼接、跨时钟域同步都很规整。如果你用 Verilog 写一个reg [15:0]就能装下不需要做复杂的位宽对齐。这种“工程上的顺手”往往比理论上的最优更重要做硬件的都懂。3. 激光振镜为什么要用这套协议而不是 SPI 或 I2C3.1 振镜控制的实时性要求有多苛刻激光振镜不是普通电机它要在毫秒甚至微秒级完成角度调整而且要求极高的重复定位精度。打标一条曲线实际上是振镜在极短时间内走出一连串微小线段每个线段的端点都要精确到位。如果通信协议有哪怕几十微秒的抖动画出来的线就会毛糙、拐角发圆。SPI虽然快但它是主从结构片选、模式配置一堆事而且没有天然的“帧同步”概念多轴同步要靠软件凑。I2C更不行速率低、总线仲裁复杂根本扛不住振镜这种高频刷新。XY2-100 的设计哲学就是“简单粗暴”固定帧长、固定时序、三根线、无需寻址、无需握手控制器想发就发振镜只管收。这种极简结构带来的确定性正是实时控制最需要的。3.2 固定帧长带来的确定性优势固定 20 位帧长意味着每一帧的传输时间是完全可预测的。2MHz 时钟下一帧就是 20 × 500ns 10μs。如果你的控制环要跑 20kHz 刷新率那一帧 10μs 刚好留出余量。这种确定性让上位机可以精确规划运动轨迹的时间轴不会因为协议层的可变延迟导致轨迹失真。对比一下如果用 SPI 传同样 20 位数据虽然时钟可以拉到几十 MHz但加上片选建立时间、软件调度开销实际端到端延迟反而不可控。而 XY2-100 的时序是硬件级的FPGA 里一个状态机就能稳定输出抖动可以压到纳秒级。这就是为什么工业现场宁愿用一根专用排线也不愿意走通用总线。3.3 多轴同步在 XY2-100 里是怎么天然解决的激光扫描经常需要 X、Y 甚至 Z 三轴联动。XY2-100 把 X 和 Y 塞进同一帧意味着两个轴的位置数据是在同一个时钟节拍下发出的天然同步。你不需要像 SPI 那样分别给两个从设备发数据再对齐时间戳硬件层面就保证了 X 和 Y 的更新是同一时刻生效的。这一点在画圆、画螺旋线时特别关键。如果 X 和 Y 更新有哪怕一个时钟周期的偏差圆就会变成椭圆或者出现锯齿。我实测过用软件分别驱动两路 SPI 振镜即使代码里把两次发送挨着写示波器上也能看到几十纳秒的偏斜画出来的圆在放大后能看出接缝。换成 XY2-100 单帧双轴后这个问题直接消失。4. 用 Verilog 实现 XY2-100 发送端状态机怎么搭4.1 时钟分频与 2MHz 节拍生成FPGA 板卡上通常有一个 50MHz 或 100MHz 的晶振要得到 2MHz 的 XY2-100 时钟需要分频。以 50MHz 为例分频系数是 50/2 25也就是每 25 个系统时钟翻转一次 XY2-100 时钟得到 1MHz 的方波不对这里要算清楚要得到 2MHz 的时钟周期是 500ns50MHz 系统时钟周期是 20ns所以每 25 个系统时钟对应一个 2MHz 半周期再算一遍2MHz 周期 500ns半周期 250ns250ns / 20ns 12.5不是整数。所以直接用 50MHz 分频得不到精确的 2MHz。实际工程里有两个做法一是用 48MHz 晶振48/2/12 2MHz整除二是用 50MHz 但接受一点点频偏或者用 PLL 倍频到 100MHz 再分频100/2/25 2MHz整除。我一般推荐用 PLL 把系统时钟倍到 100MHz 或 120MHz再分频得到干净的 2MHz这样时序余量更足。下面是一个基于 100MHz 输入、分频出 2MHz 的 Verilog 片段// 100MHz - 2MHz, 分频系数 50 reg [5:0] div_cnt; reg sclk_r; always (posedge clk_100m or negedge rst_n) begin if (!rst_n) begin div_cnt 6d0; sclk_r 1b0; end else if (div_cnt 6d24) begin div_cnt 6d0; sclk_r ~sclk_r; // 每25个周期翻转得到2MHz end else begin div_cnt div_cnt 1b1; end end注意这里div_cnt 24而不是 49因为我们要的是半周期翻转。25 个 100MHz 周期是 250ns翻转一次得到 500ns 周期正好 2MHz。这种“半周期计数”的写法比“全周期计数再取反”更直观也不容易算错。4.2 20 位数据并串转换的移位逻辑数据发送的核心是一个 20 位移位寄存器在时钟的下降沿把当前最高位推到 DATA 线上然后左移一位。为什么选下降沿更新因为前面说了振镜在上升沿采样下降沿更新能保证数据在上升沿前已经稳定了半个周期建立时间充足。reg [19:0] shift_reg; reg data_out; always (negedge sclk_r or negedge rst_n) begin if (!rst_n) begin shift_reg 20d0; data_out 1b0; end else if (frame_start) begin shift_reg {ctrl_bit, x_data[7:0], y_data[7:0], 3b000}; end else begin data_out shift_reg[19]; shift_reg {shift_reg[18:0], 1b0}; end end这里frame_start是一帧开始的标志由状态机产生。装载数据时把 X、Y 和控制位拼成 20 位然后每个下降沿吐出一位。20 个周期后数据发完状态机回到空闲等待下一帧。这个逻辑很朴素但实测非常稳关键是frame_start的时序要对齐不能和移位时钟打架。4.3 SYNC 信号的拉高时机与帧边界对齐SYNC 是帧同步信号它的拉高时机直接决定振镜能不能正确识别帧头。常见做法是在一帧的第一个时钟周期拉高 SYNC持续一个时钟周期然后拉低。这样振镜看到 SYNC 高电平就知道接下来 20 个时钟是有效数据。但这里有个坑SYNC 的拉高必须和第一个数据位的输出严格对齐。如果 SYNC 早了一个周期振镜会把上一帧的最后一位当成新帧的第一位整个数据错位。我调试时用示波器同时抓 SYNC、CLOCK、DATA 三根线把 SYNC 的上升沿和 DATA 第一位的变化对齐到同一个时钟边沿附近才算稳。Verilog 里可以用一个计数器控制reg sync_r; always (posedge clk_100m or negedge rst_n) begin if (!rst_n) sync_r 1b0; else if (bit_cnt 5d0 sclk_r 1b0) sync_r 1b1; else sync_r 1b0; endbit_cnt是位计数器从 0 数到 19。当它归零且时钟处于低电平时拉高 SYNC保证 SYNC 的上升沿和第一个数据位的更新同步。这种“卡边沿”的写法比单纯用状态机输出更精确因为状态机的输出可能有组合逻辑延迟。5. 接收端与闭环振镜状态怎么读回来5.1 XY2-100 的反馈通道是否存在很多人以为 XY2-100 只是单向发送其实部分振镜支持反馈通道会把当前实际位置或状态位回传给控制器。反馈通常复用同一套时钟但数据线方向反过来或者用额外的反馈线。不过要注意不是所有振镜都支持反馈便宜的模拟振镜往往只有输入没有输出。如果你的应用需要闭环控制比如高精度打标要求实时校正位置就得确认振镜型号是否支持反馈。我遇到过一款国产振镜文档里写了“支持状态回读”实际接线后发现反馈线是开漏输出需要外加上拉电阻才能读到有效电平。这种细节文档里往往一笔带过不实测根本发现不了。5.2 用 FPGA 采样反馈数据的时序要点接收反馈数据的难点在于跨时钟域。振镜的反馈时钟和 FPGA 内部时钟不同源直接采样会有亚稳态风险。稳妥的做法是用两级触发器做同步再用反馈时钟的边沿去采样数据线。// 反馈时钟同步 reg fb_clk_d1, fb_clk_d2; always (posedge clk_100m) begin fb_clk_d1 fb_clk; fb_clk_d2 fb_clk_d1; end // 检测上升沿 wire fb_clk_rise fb_clk_d1 ~fb_clk_d2;检测到反馈时钟上升沿后再去采样反馈数据线把串行数据移位拼成 16 位。这里要注意采样窗口反馈数据在时钟边沿附近可能不稳定最好在上升沿后延迟几个系统时钟再采或者用反馈时钟的下降沿采样。具体延迟多少得看振镜手册的时序图没有统一答案。5.3 位置误差计算与滑动窗口滤波的引入拿到反馈位置后和指令位置相减就得到位置误差。但这个误差信号往往很脏因为振镜电机换向、DAC 噪声、电源纹波都会耦合进来。直接拿原始误差去做 PID系统会抖得厉害。这时候滑动窗口滤波就派上用场了。滑动窗口滤波的本质是“用最近 N 个采样值的平均代替瞬时值”把高频噪声压下去。在 FPGA 里实现可以用一个移位寄存器存最近 N 个误差值每个时钟周期求和再除以 N。N 取 8 或 16 比较常见太大响应会变慢太小滤波效果不明显。我一般先用 8 试看阶跃响应有没有明显滞后再决定要不要加大。reg signed [15:0] err_window [0:7]; reg signed [18:0] err_sum; integer i; always (posedge clk_100m) begin err_window[0] err_new; for (i 1; i 8; i i 1) err_window[i] err_window[i-1]; err_sum err_window[0] err_window[1] ... err_window[7]; end wire signed [15:0] err_filtered err_sum 3; // 除以8注意这里用算术右移实现除以 8对有符号数是正确的。如果用对负数会出问题必须用。这个坑我在早期项目里踩过误差为负时滤波结果完全不对查了半天才发现是移位符号问题。6. 实测中那些文档不会告诉你的坑6.1 时钟极性搞反导致振镜“抽风”最常见的坑就是时钟极性。XY2-100 文档通常写“数据在时钟上升沿有效”但有些振镜实际是在下降沿采样。如果你按上升沿采样去设计发送端数据就会错一位振镜表现为随机抖动或者往一个方向猛冲。判断方法很简单先用一个固定的位置码值比如中点 32768持续发送用示波器看 DATA 线在时钟上升沿和下降沿分别是什么电平。如果上升沿采到的位和预期不符就说明极性反了。改法就是把发送端的更新边沿反过来或者把时钟反相。我现在的习惯是拿到新振镜第一件事就是发中点测试确认极性再往下做。6.2 上电时序与复位竞争振镜驱动板通常需要先上电、再给时钟和数据否则可能进入异常状态。但 FPGA 上电后往往立刻开始输出时钟如果振镜还没准备好就会收到一堆乱码导致电机猛地一抖。解决办法是在 FPGA 里加一个上电延时计数器比如上电后等 100ms 再开始发帧给振镜驱动板足够的初始化时间。另外复位信号也要处理好。如果复位期间时钟还在跑但数据线是随机的同样会干扰振镜。稳妥做法是复位时把 CLOCK、SYNC、DATA 都拉到确定的低电平复位释放后再启动状态机。这个细节在代码里就是几行的事但能避免很多“莫名其妙”的故障。6.3 长排线带来的信号完整性问题振镜和 FPGA 之间往往隔着几十厘米的排线2MHz 时钟虽然不算高频但边沿陡峭时仍然会有反射和串扰。我遇到过一根 50cm 的排线时钟线上出现过冲导致振镜偶尔丢帧。后来在时钟线串联了一个 33Ω 的电阻做阻抗匹配问题就消失了。如果你的排线超过 30cm建议在时钟和数据线上都串小电阻并且尽量用双绞线或者带地线隔离的排线。另外排线远离电机驱动线避免大电流开关噪声耦合进来。这些是硬件层面的经验纯写 Verilog 是解决不了的。6.4 多振镜级联时的同步问题有些设备需要控制多组振镜比如双头打标机。如果每组振镜独立发帧帧与帧之间会有相位差导致两个头打出的图形对不齐。解决办法是用同一个时钟源和同一个帧计数器驱动所有振镜保证所有通道的 SYNC 和 CLOCK 完全同相。在 Verilog 里就是把分频计数器和位计数器做成全局共享每个振镜通道只是独立的数据移位寄存器。这样所有振镜在同一时刻收到同一帧的对应位同步精度可以做到一个时钟周期以内。这个设计思路在单振镜时看不出优势但多振镜时是必须的。7. 从协议到系统一个可复用的 Verilog 模块划分7.1 顶层接口与参数化设计把上面的逻辑整理成一个可复用的模块顶层接口大概是这样输入系统时钟、复位、X/Y 位置数据、发送使能输出 XY2-100 的 CLOCK、SYNC、DATA。参数化方面把分频系数、帧长、数据位宽做成 parameter方便适配不同振镜。module xy2_100_tx #( parameter CLK_FREQ 100_000_000, parameter XY2_CLK 2_000_000, parameter DATA_W 16 )( input wire clk, input wire rst_n, input wire tx_en, input wire [DATA_W-1:0] x_pos, input wire [DATA_W-1:0] y_pos, output reg xy2_clk, output reg xy2_sync, output reg xy2_data );这种参数化设计的好处是换一个晶振频率或者换一种振镜只改 parameter 不用动逻辑。我在多个项目里复用这个模块基本没出过问题。7.2 发送状态机的三段式写法状态机推荐用三段式第一段时序逻辑更新当前状态第二段组合逻辑判断次态第三段时序逻辑输出。这种写法综合出来的电路干净时序容易收敛也方便后期加状态。localparam IDLE 2d0; localparam SEND 2d1; localparam WAIT 2d2; reg [1:0] cur_state, nxt_state; // 第一段状态寄存 always (posedge clk or negedge rst_n) begin if (!rst_n) cur_state IDLE; else cur_state nxt_state; end // 第二段次态逻辑 always (*) begin case (cur_state) IDLE: nxt_state tx_en ? SEND : IDLE; SEND: nxt_state (bit_cnt 5d19) ? WAIT : SEND; WAIT: nxt_state IDLE; default: nxt_state IDLE; endcase end三段式的核心价值是“状态寄存和输出分离”避免组合逻辑毛刺直接跑到输出线上。对 XY2-100 这种时序敏感的协议毛刺可能被振镜误采样所以输出最好也寄存一拍。7.3 仿真验证与板级调试的衔接写完代码先跑仿真用 Verilog testbench 模拟振镜的采样行为检查 20 位数据是否按预期串出。仿真通过后再上板用示波器抓实际波形。仿真和实测的差异往往在时序余量和信号完整性上所以仿真只能验证逻辑不能替代实测。我一般会在 testbench 里加一个“假振镜”模型在时钟上升沿采样 DATA拼成 20 位后和发送值比对。如果一致说明逻辑没问题。上板后再用示波器确认边沿和电平两步都过了才算稳。8. 写在最后的一点个人体会XY2-100 这套协议本身不复杂20 位帧、三根线、2MHz 时钟看文档半小时就能明白。但真正把它用稳靠的是对时序边界的较真和对硬件细节的敏感。我踩过的坑里一半是时钟极性、复位时序这类“低级”问题另一半是排线阻抗、跨时钟域这类“硬件味”很浓的问题。纯写 Verilog 的人容易忽略后者但恰恰是后者决定了系统能不能长期稳定运行。如果你正在做振镜控制的项目我的建议是先用示波器把三根线的时序彻底看明白再动手写代码写完先发中点测试确认极性上板后先低速跑稳定了再提刷新率。这套流程看起来笨但比反复改代码高效得多。后面如果要做多轴联动或者闭环校正滑动窗口滤波和跨时钟域同步这两块值得多花点时间它们是系统精度的天花板。