FPGA实现XY2-100激光振镜控制:Verilog发送器设计与调试指南
发布时间:2026/9/25 2:45:06来源:尧图网络
玩过激光打标机的朋友都知道振镜控制是整个系统里最吃时序的一环。最近我把手头一台老机器的模拟量驱动板换成了XY2-100数字接口用一块入门级FPGA把上位机的X/Y坐标实时打包成差分串行帧整个过程踩了不少坑。这篇就把协议格式、Verilog发送器实现、仿真和上板调试完整梳理一遍给初次接触激光振镜控制的人一个能直接抄作业的参考。先说结论XY2-100不是高速协议位时钟通常只有2MHz帧结构也不复杂但它对信号完整性、帧边界和校验位的正确性要求很高。很多第一次写的同学在仿真里看波形没问题一接真实振镜就出现单轴乱转、位置漂移、偶发跳点问题大多不在算法而在协议时序细节和物理层处理。下面我按自己从“看数据手册”到“振镜正常工作”的完整链路来讲。1. 激光振镜控制为什么绕不开XY2-1001.1 模拟量控制的瓶颈与数字接口的出现振镜的核心是驱动一个高响应速度的电机带动反射镜快速偏转。传统的振镜驱动板多采用模拟量控制上位机输出一个±10V或±5V的模拟电压驱动板内部把电压映射成角度。模拟量控制的好处是简单但坏处也很明显线缆长了电压会跌、外界干扰会叠在信号上、不同驱动板的零点不一致而且整个链路没有校验手段一旦丢了一个点光斑位置就错了不会报错。数字接口正是为了解决这些问题出现的。XY2-100协议把位置数据变成一串串行比特流用差分信号传输天然抗共模干扰接收端通过同步信号严格对齐帧边界还能用校验位判断数据是否损坏。对激光打标、切割、雕刻这类要求光斑位置精确、重复性好的场景来说XY2-100几乎是事实标准。1.2 一图看懂XY2-100的四对差分线XY2-100的物理层是四对RS-422差分信号线每对线在接收端会用差分接收器还原成单端逻辑电平。最常见的接口定义如下信号差分对作用说明XY2_CLOCKCLOCK / CLOCK−位时钟常用2MHz方波数据按位移动XY2_SYNCSYNC / SYNC−帧同步每个数据帧开始时拉高一个时钟周期XY2_CHANNELCHANNEL / CHANNEL−通道选择低电平发送X轴数据高电平发送Y轴数据XY2_DATADATA / DATA−串行数据携带坐标和校验信息MSB先发这里有个新手最容易犯的错误看着“差分”两个字就直接把FPGA的LVDS引脚接到振镜驱动板上。实际操作时如果FPGA的bank供电、输出标准、终端电阻没有配对波形一样会乱。后面第5节我会单独讲物理层的处理。1.3 不是所有振镜都叫“XY2-100”市面上振镜驱动板对数字协议的实现并不完全统一。有的用3位头码100加16位数据加1位校验有的用4位头码加16位数据有的甚至把校验位放在数据前面。我下面的Verilog代码按最常见的“3位头码 16位数据 1位偶校验、每帧20bit”来写代码里把头码和校验方式都做成参数可调。你拿到板子后第一件事不是翻教程而是找驱动板手册里的时序图确认头码和校验位定义再决定要不要改代码。这一步能省掉后续一整天的排查时间。2. 20位帧拆解头码、位置数据、校验位是怎么排布的2.1 帧内位序与典型波形XY2-100每一帧是20bit按顺序发送的内容是3bit头码常见为100用于接收端识别“这是一帧数据的开始”。16bit位置数据采用二进制补码范围-32768到32767。1bit偶校验位由16bit数据逐位异或得到。发送顺序是MSB先发也就是说先发头码的最高位再发数据的最高位最后发校验位。配合SYNC信号描述典型时序可以这样理解SYNC在帧的第一个位时间内为高之后19个位时间都为低CHANNEL在整个帧期间保持为当前轴的通道电平DATA在每一个CLOCK的下降沿后切换内容接收端在CLOCK上升沿采样。以发送X轴坐标0x1234为例假设头码是100那么这一整帧在DATA线上的序列是头码: 1 0 0 数据: 0 0 0 1 0 0 1 0 0 0 1 1 0 1 0 0 校验: 10x1234的二进制是0001 0010 0011 0100统计1的数量共5个偶校验位需要补成偶数个1所以校验位为1。这个例子最适合写仿真testbench验证因为结果一眼能看明白。2.2 CHANNEL信号如何决定X/Y通道XY2-100在同一根DATA线上既传X轴数据也传Y轴数据靠CHANNEL区分。常见约定是CHANNEL 0当前帧是X轴位置CHANNEL 1当前帧是Y轴位置。所以一个完整的双轴控制周期是先发一帧X再发一帧Y两帧交替重复。有的驱动板约定反一下CHANNEL高电平表示X、低电平表示Y代码里只要把cur_data的mux逻辑对调即可。因为FPGA代码是硬件描述改这一处比改硬件快得多也是Verilog实现相比软件方案最大的优势。2.3 为什么只做奇偶校验而不上CRC很多做通信的人第一次看到XY2-100会觉得奇怪位置数据这么重要怎么只有1位偶校验CRC呢其实这是有意为之。振镜控制系统对延迟极其敏感一帧数据只有10us左右如果接收端每帧都要算CRC、缓存、确认控制延迟会明显变大。偶校验虽然不能纠错但能发现奇数个比特翻转对串行链路来说已经覆盖了大部分位翻转场景。真出现连续多位错误说明信号完整性已经坏了应该优先查线缆和地线而不是依赖传输层纠错。2.4 与4位头码协议的兼容处理如果你拿到的驱动板手册写的是“4bit header 16bit data”不要慌数据结构很容易改。我们的帧总长还是20bit时3位头码和4位头码的区别本质是头码长度不一样。有的板子是4位头码1000、16位数据没有校验位正好也是20bit。这种情况下把发送状态机里的头码位从3位改成4位去掉校验位就能兼容。我在代码里把FRAME_BITS、HEADER都做成参数就是为了应对这种差异。真正要严格对齐的是“哪个沿采样、SYNC高电平持续几个时钟、头码具体值”这三个信息以驱动板手册为准。3. Verilog发送器代码逐段拆解3.1 顶层信号定义与参数设计我习惯把协议参数写到模块头部这样换一块不同协议的驱动板时不需要重写逻辑。下面这个模块名就叫xy2_100_tx它接受系统时钟和复位接收上位机送来的16位X/Y坐标输出XY2-100所需的四根单端信号。如果你在FPGA工程里需要真差分输出再在外面接OBUFDS或ALT_OUTBUF_DIFF这个模块内部保持单端逻辑仿真也方便。module xy2_100_tx #( parameter SYS_CLK_HZ 50_000_000, // 系统时钟频率 parameter FRAME_BITS 20, // 一帧总位数 parameter HEADER 3b100 // 头码按驱动板手册调整 )( input wire clk, input wire rst_n, // 上位机坐标输入 input wire [15:0] x_position, input wire [15:0] y_position, input wire load_x, // 拉高一拍载入X新值 input wire load_y, // 拉高一拍载入Y新值 // XY2-100单端输出 output wire xy2_clock, output reg xy2_sync, output reg xy2_channel, output reg xy2_data );这里有个设计选择点为什么把坐标输入设计成“先写入影子寄存器、再由发送状态机取走”因为上位机更新坐标的频率可能与2MHz发送时钟不同步如果直接在发送过程中改变cur_data当前帧后半段可能混入新数据接收端就会收到一个撕裂的位置值。影子寄存器保证每一帧使用的坐标是一个完整快照。3.2 低抖动2MHz时钟产生的正确姿势XY2-100的位时钟是2MHz最简单的方法是用系统时钟分频产生一个clk_2m。很多教程直接在一个独立always块里翻转clk_2m然后用这个分频时钟去触发其他逻辑。这样做能跑通但会带来两个问题一是分频时钟在FPGA内部不是全局时钟网络扇出过多会产生路径延迟二是综合工具对多时钟域约束处理更麻烦。入门阶段为了让代码直观我仍然用了一个分频寄存器但明确告诉你工程化改造时最好把所有逻辑都放在系统时钟域用2MHz时钟使能信号代替真正的分频时钟。也就是说每25个系统时钟周期拉高一次clk_en所有状态机只在clk_en1时推进。下面的代码为了保持可读性先使用分频时钟标题里的“入门”也定位在这里如果你要做产品务必改成时钟使能方式。// 分频产生2MHz位时钟 localparam DIV_CNT (SYS_CLK_HZ / 2_000_000) / 2; reg [7:0] div_cnt; reg clk_2m; always (posedge clk or negedge rst_n) begin if (!rst_n) begin div_cnt 0; clk_2m 1b0; end else if (div_cnt DIV_CNT - 1) begin div_cnt 0; clk_2m ~clk_2m; end else begin div_cnt div_cnt 1; end end assign xy2_clock clk_2m;DIV_CNT的计算要特别注意50MHz时钟分频到2MHz周期从20ns变为500ns半个周期是250ns即12.5个50MHz时钟周期。所以DIV_CNT应该是12还是13严格说2MHz不是从50MHz整数分频得到的真实频率会略有偏差。这时候两个选择一是把系统时钟改成48MHz便于整数分频二是在振镜驱动板允许的频率范围内接受微小偏差。很多驱动板的时钟频率范围是1MHz到2MHz稍微偏一点不影响工作但如果你做的是要求严格的产品应该选一个能被2MHz整除的系统时钟比如40MHz、60MHz、100MHz。3.3 发送状态机计数器就是最简单的状态机XY2-100发送器不需要复杂FSM一个0到19的位计数器就能表达所有状态。计数器为0时说明正在发送新一帧的第一个位计数器为19时说明当前是校验位发送完下一秒就要把计数器清零并切换通道。为了让接收端在CLOCK上升沿采到稳定的数据我选择在clk_2m的下降沿更新xy2_data、xy2_sync、xy2_channel。这样数据变化发生在下降沿到上升沿时已经稳定了半个时钟周期接收端的建立时间裕量非常充裕。reg [4:0] bit_cnt; reg chan_sel; // 0X轴, 1Y轴 reg [15:0] x_shadow; reg [15:0] y_shadow; wire [15:0] cur_data chan_sel ? y_shadow : x_shadow; wire parity ^cur_data; // 偶校验 wire frame_first (bit_cnt 0); always (posedge clk or negedge rst_n) begin if (!rst_n) begin x_shadow 16d0; y_shadow 16d0; end else begin if (load_x) x_shadow x_position; if (load_y) y_shadow y_position; end end always (negedge clk_2m or negedge rst_n) begin if (!rst_n) begin bit_cnt 0; chan_sel 0; xy2_sync 1b0; xy2_channel 1b0; xy2_data 1b0; end else begin xy2_channel chan_sel; xy2_sync frame_first ? 1b1 : 1b0; case (bit_cnt) 3d0: xy2_data HEADER[2]; 3d1: xy2_data HEADER[1]; 3d2: xy2_data HEADER[0]; 4d3: xy2_data cur_data[15]; 4d4: xy2_data cur_data[14]; 4d5: xy2_data cur_data[13]; 4d6: xy2_data cur_data[12]; 4d7: xy2_data cur_data[11]; 4d8: xy2_data cur_data[10]; 4d9: xy2_data cur_data[9]; 4d10: xy2_data cur_data[8]; 4d11: xy2_data cur_data[7]; 4d12: xy2_data cur_data[6]; 4d13: xy2_data cur_data[5]; 4d14: xy2_data cur_data[4]; 4d15: xy2_data cur_data[3]; 4d16: xy2_data cur_data[2]; 4d17: xy2_data cur_data[1]; 4d18: xy2_data cur_data[0]; 4d19: xy2_data parity; default: xy2_data 1b0; endcase if (bit_cnt FRAME_BITS - 1) begin bit_cnt 0; chan_sel ~chan_sel; end else begin bit_cnt bit_cnt 1; end end end3.4 X/Y数据载入与校验位计算上面的代码里load_x和load_y脉冲相当于坐标的“写使能”。实际系统中上位机可能通过UART、USB还是AXI总线送来坐标但最终都要转成这两个信号。我的经验是在收到新坐标后不要急着改cur_data只要在当前帧发送完成之前把影子寄存器更新掉就行下一帧自然会取用新值。这样即使load_x脉冲发生在帧中间也不会造成一帧数据被撕裂。校验位用^cur_data一步就能得到16位数据的偶校验。如果驱动板要求奇校验改成~^cur_data即可。这里有个容易忽略的细节校验位的计算对象必须是本帧正在发送的那个轴的坐标。因为X帧和Y帧交替发送chan_sel决定cur_data所以在发送校验位的那一拍cur_data仍然保持当前通道的坐标计算结果是正确的。如果用了一个提前打拍的data_reg而没有跟随chan_sel校验位就会算错接收端会一直报错。3.5 完整代码与差分输出连接把上面几段拼起来就是完整的xy2_100_tx模块。为了让差分输出适配FPGA板卡我通常会在顶层再封装一层以Xilinx为例// Xilinx 差分输出示例 OBUFDS #(.IOSTANDARD(LVDS_25)) obufds_clk ( .I (xy2_clock), .O (xy2_clk_p), .OB (xy2_clk_n) ); OBUFDS #(.IOSTANDARD(LVDS_25)) obufds_sync ( .I (xy2_sync), .O (xy2_sync_p), .OB (xy2_sync_n) ); OBUFDS #(.IOSTANDARD(LVDS_25)) obufds_chan ( .I (xy2_channel), .O (xy2_chan_p), .OB (xy2_chan_n) ); OBUFDS #(.IOSTANDARD(LVDS_25)) obufds_data ( .I (xy2_data), .O (xy2_data_p), .OB (xy2_data_n) );如果使用的是Intel/Altera芯片可以用ALT_OUTBUF_DIFF原语。如果你手头只有单端转差分芯片也可以把FPGA的普通IO接到芯片输入端由芯片输出差分对到振镜驱动板。但无论哪种方案都要保证差分阻抗匹配否则长线上反射会直接导致校验错。4. 仿真与上板调试怎样确认协议真的工作4.1 写一个最小testbench对时序自检代码写完先别急着上板仿真成本低能发现问题。testbench的核心任务是给一个已知坐标然后观察xy2_data序列是否与预期逐位一致。下面这段testbench用50MHz时钟复位后依次载入X0x1234和Y0x5678然后跑完两帧。timescale 1ns / 1ps module tb_xy2_100_tx(); reg clk; reg rst_n; reg [15:0] x_position; reg [15:0] y_position; reg load_x; reg load_y; wire xy2_clock; wire xy2_sync; wire xy2_channel; wire xy2_data; xy2_100_tx #( .SYS_CLK_HZ (50_000_000), .FRAME_BITS (20), .HEADER (3b100) ) dut ( .clk (clk), .rst_n (rst_n), .x_position (x_position), .y_position (y_position), .load_x (load_x), .load_y (load_y), .xy2_clock (xy2_clock), .xy2_sync (xy2_sync), .xy2_channel(xy2_channel), .xy2_data (xy2_data) ); initial begin clk 0; forever #10 clk ~clk; // 50MHz end initial begin rst_n 0; x_position 16h1234; y_position 16h5678; load_x 0; load_y 0; #500; rst_n 1; #100; load_x 1; #20; load_x 0; // 跑完若干个2M时钟周期 #50000; $finish; end endmodule仿真时重点检查三个点xy2_sync是否每20个xy2_clock周期产生一次高电平脉冲xy2_channel是否在每帧结束时翻转在xy2_clock上升沿上采样xy2_data读出来是否等于100 数据位 校验位。如果这三个点都对再上板调试就会非常有底气。4.2 用ILA/逻辑分析仪抓数据的判断点上板之后第一件事不是接振镜而是用ILA抓FPGA内部信号。把xy2_clock、xy2_sync、xy2_channel、xy2_data都加进ILA触发条件设置为xy2_sync 1。抓到的波形应该是一个很规整的重复序列。我倾向于在逻辑分析仪里直接检查一个已知坐标。比如上位机固定发送X0x0001那么你看到的DATA序列应该是100 0000000000000001 1。注意数据部分的16位是0000 0000 0000 0001因为1的个数是1个偶校验位为1所以末尾是1。这个固定序列非常好抓如果看起来顺序不对或者少了一位优先检查bit_cnt初值、头码位序和case语句里的索引。4.3 板级常见异常现象和对应原因如果你没有ILA只能用示波器看差分引脚下面这份排查表是我实际踩坑总结的。现象可能原因处理办法振镜完全不动FPGA没有输出时钟或SYNC不翻转检查分频参数、复位信号振镜只朝一个方向偏转X/Y坐标符号或channel极性接反交换chan_sel的mux映射或用软件反转坐标位置缓慢漂移坐标被频繁更新且载入信号一直有效确认load_x是单周期脉冲而不是持续高电平偶发跳点校验位错误或数据线被干扰查看校验函数、检查差分终端电阻上电瞬间振镜猛甩复位释放前输出坐标不是零FPGA复位时把X/Y坐标强制为0振镜供电顺序控制5. 我踩过的坑时钟沿、数据锁存和地线干扰5.1 输出沿与采样沿的真相XY2-100的调试里最容易迷惑的就是“到底哪个沿发送哪个沿采样”。我一开始按普通SPI的习惯在2MHz时钟上升沿更新数据结果接收端同样在上升沿采样建立时间几乎没有有时候能用、有时候不能用换成温度高一点的机箱就跳点。后来我统一改成“数据在下降沿变化接收端在上升沿采样”。对发送端来说只要保证数据变化点离上升沿足够远即可。很多驱动板内部就是在这两个沿之间采样数据所以这个时序是安全的。如果你已经用现成的IP核可能在内部把数据锁存在PLL相位后的时钟沿上效果也类似。但自己写协议时务必明确这个沿关系并在仿真testbench里用时钟上升沿采样来验证。5.2 每帧最后一个bit之后要不要留空闲XY2-100的帧是连续发送的理论上第20位发送完下一帧的头码立即开始不需要在帧和帧之间插入空闲时间。有的驱动板为了兼容低速上位机允许在SYNC前多留几个时钟的低电平但这不是协议强制要求。我建议初学者先按“背靠背”连续帧实现因为这样时序模型最简单只要保证bit_cnt在19后清零成0下一拍自然进入下一帧。如果你在连续帧之间加了等待反而容易把SYNC的位置搞错。那为什么有的工程里会在帧间插入空隙因为有些上位机坐标更新很慢没必要以50kHz每轴的速率刷新插入空闲可以降低平均数据率。但这种“慢速模式”的波形判断不如连续模式直观入门阶段不建议。5.3 差分对与终端电阻实战建议物理层是最容易被忽略也是最容易翻车的环节。RS-422差分信号虽然抗干扰能力强但前提是接收端有正确的终端匹配。振镜驱动板内部通常已经接了100Ω左右的终端电阻但你的FPGA板卡到驱动板的连线如果是杜邦线、跳线帽那种就别指望信号质量有多好了。我后来换成了双绞屏蔽线并把每对差分线做成等长处理跳点问题明显减少。还有一个细节是共地。XY2-100是差分协议理论上不要求两端严格共地但实际设备中振镜电机启动瞬间会在地线上拉出很大的噪声如果不共地或地线过细差分接收器仍然可能被差模转共模的噪声打穿。最直接的做法是FPGA板卡和振镜驱动板使用同一个电源系统地并且在靠近驱动板一端并一个0.1uF瓷片电容和10uF电解电容。5.4 数据更新时机的工程建议最后分享一个我在实际项目中用的技巧不要让坐标更新脉冲来得太“随意”。如果你的上位机通过UART接收坐标每一帧坐标到达后就直接拉load_x可能会出现连续两三个坐标在同一个发送帧周期内到达导致其中一个坐标被覆盖。正确做法是维护一个“最新坐标有效”标志发送器每帧开始前检查一次如果标志有效就载入新坐标同时清标志。这样即使上位机以不确定的速度发坐标发送链路始终以固定帧率工作。这其实也解释了为什么很多人觉得Verilog写通信逻辑比写C复杂C程序里数据就是变量但硬件里“数据在哪里、什么时候被采样”是必须显式设计的。XY2-100看似简单恰恰是练习这种“硬件直觉”的好题目。如果你现在正要调自己的第一块振镜驱动板我的建议是先仿真过基准波形再接驱动板先固定坐标测试再做扫描运动先解决信号完整性和校验问题再优化上位机刷新率。这套流程我用了很多次每一次都能在半小时内定位到是逻辑问题还是物理问题。剩下的事情就是慢慢体会“下沿改数、上沿采数”这个简单规则带来的确定性——这一点是振镜控制最迷人的地方。
网站建设高端定制企业官网