
1. 项目缘起与协议定位激光振镜这套东西第一次接触的人往往会被它那两根高速摆动的镜子迷惑以为核心在机械结构或者电机驱动上。实际上真正决定加工精度和响应速度的是控制板卡和振镜驱动器之间那条不起眼的数字链路。XY2-100就是这条链路上最常被提到的一个名字。它不是什么高深莫测的私有协议也不是某个厂商的独家秘方而是一套在工业激光加工领域流通了二十多年的同步串行接口标准。你手里那块FPGA开发板只要时序抠得够准完全可以让它吐出符合XY2-100规范的波形去驱动一台真实的振镜。这个项目的核心目标很明确把XY2-100协议的帧结构、时序关系、校验逻辑吃透然后用Verilog在FPGA上实现一个可综合、可仿真、可上板的发送端。它解决的是“FPGA如何与激光振镜驱动器对话”这个问题。适合谁看如果你正在做激光打标、激光焊接、医疗扫描或者任何需要二维光束偏转控制的嵌入式项目又或者你只是单纯想找一个有真实工业背景的同步串口协议来练手Verilog时序设计那这篇内容就是为你准备的。我不会只丢一段代码出来而是把协议为什么长这样、时序余量怎么算、状态机怎么划分、上板后怎么用示波器抓波形验证一层一层拆开讲。先给一个直观的印象XY2-100本质上是一条三线制的同步串行总线包含一路时钟、一路同步信号、一路数据。每帧20个比特里面塞了X轴位置、Y轴位置、Z轴聚焦或者激光功率之类的控制量外加奇偶校验位。它跑在2MHz左右的时钟频率上帧率大概在20kHz到100kHz之间具体取决于驱动器型号和线缆长度。这个速率放在今天的FPGA眼里简直慢得可爱但正因为慢时序容错空间反而需要仔细拿捏因为工业现场电磁环境复杂振镜驱动器对抖动和毛刺的容忍度并不高。2. 协议帧结构深度拆解2.1 20比特里到底装了些什么XY2-100的一帧固定为20个时钟周期。数据在时钟的上升沿被驱动器采样同步信号用来标记帧的起始和结束。具体比特分配是这样的第0到第9位是X轴位置数据第10到第19位是Y轴位置数据。等等那Z轴和校验呢这里就是第一个容易踩坑的地方。标准XY2-100帧实际上包含三个通道的数据每个通道16位再加上4位校验和状态位总共52位不对那是另一种变体。市面上流通最广的XY2-100帧格式是20位一帧其中X和Y各占10位剩下的是校验和状态。让我把话说得更准确一些。根据公开的接口定义和多家振镜驱动器的数据手册XY2-100的典型帧结构如下时钟频率2MHz帧同步信号在每帧开始时拉高一个时钟周期。数据线在时钟上升沿变化驱动器在下降沿采样或者反过来具体取决于驱动器型号。20个比特中前10位是X轴坐标后10位是Y轴坐标每个坐标都是10位有符号数范围从-512到511。没有独立的Z轴通道Z轴控制通常通过另一个接口或者在同一帧内复用某些比特来实现。奇偶校验位通常位于帧的末尾采用奇校验或偶校验用来检测传输过程中的单比特错误。注意不同厂商对XY2-100的实现存在细微差异尤其是采样沿和校验方式。在动手写Verilog之前务必拿到你手上那台振镜驱动器的接口手册确认它的具体时序要求。盲目照搬网上的代码轻则振镜不动重则驱动器报警甚至损坏。2.2 时序参数与余量计算假设时钟频率为2MHz周期就是500纳秒。同步信号在帧起始处拉高持续一个时钟周期。数据建立时间要求至少50纳秒保持时间至少30纳秒。这些数字看起来宽松但如果你用FPGA的普通IO输出没有做任何约束布局布线后的偏斜可能就吃掉一大半余量。我实测过一块Artix-7的开发板不加时序约束时数据线相对于时钟线的偏斜能到2纳秒以上虽然还在安全范围内但已经让人不太放心了。计算余量的方法很简单驱动器要求的建立时间加上保持时间再加上线缆传输延迟的差值必须小于时钟周期减去FPGA输出偏斜。假设线缆延迟差为1纳秒FPGA偏斜为2纳秒那么可用窗口就是500 - 50 - 30 - 1 - 2 417纳秒。看起来绰绰有余但别忘了振镜驱动器内部的采样电路也有建立保持要求而且工业现场的温度变化会导致线缆延迟漂移。所以我的习惯是把时钟频率降到1MHz甚至500kHz来换取更大的时序余量除非驱动器明确要求2MHz。2.3 校验位的生成逻辑奇偶校验位的生成在Verilog里就是一行异或归约。假设采用偶校验那么校验位等于所有数据位的异或结果。如果数据位中1的个数是奇数校验位就是1使得总1的个数为偶数。接收端重新计算异或如果结果不为0就认为传输出错。这个机制只能检测单比特错误对于双比特错误无能为力。但在XY2-100这种短帧、低速、点对点的场景下单比特校验已经足够。如果你追求更高的可靠性可以在FPGA内部对发送数据做CRC校验然后通过另一个通道传给上位机但那是另一回事了。3. Verilog实现的核心架构3.1 状态机划分与时钟域处理整个发送端可以拆成三个部分一个负责生成帧同步和时钟的时序发生器一个负责把并行数据串行移出的移位寄存器还有一个顶层状态机来协调它们。状态机不需要太复杂四个状态就够了IDLE、SYNC、SHIFT、DONE。IDLE状态下等待发送请求SYNC状态拉高同步信号一个时钟周期SHIFT状态持续20个时钟周期把数据逐位移出DONE状态拉低同步信号并给出一个完成脉冲。时钟域方面FPGA内部通常跑在50MHz或100MHz的主时钟上而XY2-100的时钟是2MHz。最稳妥的做法是用一个计数器对主时钟分频生成一个使能信号而不是直接用分频后的时钟去驱动逻辑。这样做的好处是所有寄存器都在同一个主时钟域下工作避免了跨时钟域带来的亚稳态问题。我见过不少新手直接用分频时钟当always块的敏感信号仿真没问题上板后偶尔抽风十有八九就是时钟树偏斜导致的。// 2MHz使能信号生成假设主时钟50MHz reg [4:0] div_cnt; wire clk_en_2m; always (posedge clk_50m or posedge rst) begin if (rst) div_cnt 5d0; else if (div_cnt 5d24) div_cnt 5d0; else div_cnt div_cnt 1b1; end assign clk_en_2m (div_cnt 5d24);3.2 移位寄存器的实现细节移位寄存器负责把20位并行数据按位输出。这里有一个容易忽略的点XY2-100的数据线在空闲时应该保持高电平还是低电平根据我接触过的几款驱动器空闲时数据线通常被外部上拉电阻拉高FPGA输出应该配置为开漏或者三态在非发送期间释放总线。如果你用的是普通推挽输出那就老老实实输出高电平别让它悬空。移位方向也要确认是先发最高位还是最低位。大多数实现是先发X轴的最高位也就是第9位然后依次降到第0位接着是Y轴的第9位到第0位最后是校验位。reg [19:0] shift_reg; reg [4:0] bit_cnt; always (posedge clk_50m or posedge rst) begin if (rst) begin shift_reg 20d0; bit_cnt 5d0; end else if (clk_en_2m) begin if (state SHIFT) begin shift_reg {shift_reg[18:0], 1b0}; bit_cnt bit_cnt 1b1; end else if (state SYNC) begin shift_reg {x_data[9:0], y_data[9:0]}; bit_cnt 5d0; end end end assign data_out shift_reg[19];3.3 同步信号与数据线的相位关系同步信号和数据线的相位关系是调试时最需要拿示波器盯的地方。理想情况下同步信号拉高后的第一个时钟上升沿数据线的第一位就应该稳定。但实际FPGA输出会有延迟线缆也会引入延迟。我的经验是让数据线比同步信号提前半个时钟周期变化这样驱动器在采样时数据已经稳定了很久。具体做法是在时钟下降沿更新数据上升沿更新同步信号。当然这取决于驱动器是在上升沿还是下降沿采样一定要先确认清楚。4. 上板调试与波形验证4.1 仿真先行但别迷信仿真写Verilog的人都知道要先跑仿真。ModelSim或者Vivado自带的仿真器把测试激励写全覆盖各种边界情况X轴和Y轴数据全0、全1、交替变化、校验位错误等等。仿真波形看起来完美无缺但上板后可能完全是另一回事。我踩过的一个坑是仿真时没有加入IO缓冲区的延迟模型导致数据线和时钟线的相对关系在仿真里是理想的上板后因为IOB延迟不同采样窗口偏移了。解决办法是在综合后做时序仿真或者直接在约束文件里给数据线和时钟线加上输出延迟约束。4.2 示波器抓波形的正确姿势上板后第一件事不是接振镜而是用示波器抓FPGA输出的三根线。探头要用高带宽的至少100MHz接地弹簧要短别用那种长长的鳄鱼夹地线否则你看到的全是振铃。触发方式设为同步信号的上升沿时基打到500纳秒每格应该能看到清晰的20个时钟周期。重点看三个地方同步信号和数据第一位之间的时间差、数据线在时钟沿附近是否干净、帧与帧之间的间隔是否一致。如果发现数据线有台阶或者过冲多半是驱动能力太强或者线缆阻抗不匹配可以在IO上串一个22欧姆或33欧姆的电阻。4.3 接上振镜后的联调步骤确认FPGA输出波形没问题后再把三根线接到振镜驱动器上。先不要给激光器供电只让振镜动起来。发送一组缓慢变化的正弦波数据观察振镜是否跟着摆动。如果振镜不动先检查驱动器的使能信号和电源。如果振镜抖动或者发出异响多半是数据更新率太高或者坐标范围超出了驱动器设定的角度限制。这时候把帧率降下来把坐标范围缩小到±100以内再试。我调试第一台振镜时因为没注意驱动器默认的输入范围是±5V对应±10度而我的数据是10位有符号数直接映射结果振镜直接撞到机械限位发出刺耳的噪音差点把镜子撞坏。提示在接振镜之前务必确认驱动器的输入比例因子和坐标范围。很多驱动器支持通过拨码开关或者软件配置来设定满量程角度先把它设到最小安全第一。5. 常见问题与排查技巧实录5.1 振镜不动或只在一个方向动这是最常见的问题。先确认时钟和同步信号有没有输出用示波器看FPGA引脚上有没有波形。如果有波形但振镜不动检查数据线的顺序是不是反了。XY2-100的X轴和Y轴数据顺序在不同厂商的实现里可能互换。你可以先发送一个只有X轴变化、Y轴固定的数据看振镜往哪个方向动。如果往Y方向动那就把两个通道的数据在Verilog里对调。另外有些驱动器要求帧与帧之间有一个最小间隔如果FPGA连续发送没有间隔驱动器可能来不及处理。在帧之间插入几个空闲时钟周期就能解决。5.2 振镜运动有规律性抖动抖动通常来自数据更新率与振镜机械谐振频率的耦合。振镜的机械结构有一个固有谐振频率通常在几百赫兹到一千赫兹左右。如果你的数据更新率接近这个频率振镜就会共振。解决办法是提高帧率让更新频率远高于谐振频率或者加入滑动窗口滤波让坐标变化更平滑。我在一个打标项目里遇到过这个问题后来在FPGA里加了一个简单的移动平均滤波器对X和Y数据各做4点平均抖动立刻消失了。代价是引入了一点相位延迟但对于打标应用来说完全可以接受。5.3 校验错误导致驱动器报警如果驱动器有校验错误指示先确认校验方式。有的驱动器用奇校验有的用偶校验还有的干脆不用校验位把第20位当作状态位。你可以先用示波器抓一帧数据手动数一下1的个数看看校验位是否符合预期。如果校验方式没错但还是报警检查数据线的噪声。在数据线和时钟线上各加一个33欧姆的串联电阻再在驱动器输入端加一个100皮法到地的小电容通常能滤掉高频毛刺。但电容不能太大否则会吃掉建立时间。5.4 多帧连续发送时的帧同步丢失这个问题通常出现在状态机设计有缺陷的时候。如果状态机在SHIFT状态没有正确计数到20就跳回IDLE或者同步信号的拉高时机不对驱动器就会丢失帧同步。我的做法是在状态机里加一个看门狗计数器如果SHIFT状态超过25个时钟周期还没结束就强制复位到IDLE。另外同步信号的宽度要严格保持一个时钟周期太宽或太窄都可能导致驱动器误判。问题现象可能原因排查方法解决措施振镜完全不动时钟或同步信号未输出示波器测FPGA引脚检查引脚约束和输出使能振镜只往一个方向动X/Y数据顺序颠倒发送单轴变化数据观察在Verilog中对调通道振镜高频抖动更新率接近机械谐振改变帧率观察提高帧率或加滑动平均滤波驱动器校验报警校验方式不匹配手动计算校验位改为奇校验或禁用校验帧同步间歇丢失状态机计数错误仿真检查状态跳转加看门狗强制复位6. 定点数处理与坐标映射6.1 从浮点到10位有符号数的转换上位机通常用浮点数表示毫米或者角度到了FPGA里必须转成定点数。XY2-100的坐标是10位有符号数范围-512到511。假设你的振镜满量程是±10度对应±512个LSB那么每个LSB就是10/512度约等于0.0195度。转换公式很简单定点数 round(浮点数 / 满量程 * 512)。但要注意浮点数转定点数时如果直接截断会引入直流偏置导致振镜零点偏移。我的习惯是加0.5再取整也就是四舍五入。在Verilog里可以用移位和加法来实现不需要调用浮点IP核。// 假设输入是16位有符号定点数范围-32768到32767对应-10度到10度 // 输出是10位有符号数范围-512到511 wire signed [15:0] angle_fixed; wire signed [10:0] temp; wire signed [9:0] xy_out; assign temp angle_fixed 6; // 除以64相当于32768/512 assign xy_out (temp 511) ? 10sd511 : (temp -512) ? -10sd512 : temp[9:0];6.2 滑动窗口滤波在Verilog中的实现滑动窗口滤波是抑制振镜抖动的有效手段。最简单的形式是4点移动平均用一个移位寄存器存最近4个采样值然后求和右移2位。在Verilog里实现起来非常省资源几个加法器就够了。但要注意位宽扩展4个10位数相加需要12位右移2位后截取低10位。如果你想要更好的滤波效果可以用加权平均比如最近的点权重高一些旧的点权重低一些。但加权平均需要乘法器在低端FPGA上可能资源紧张。我的经验是对于大多数打标应用4点或8点简单平均已经足够。reg signed [9:0] win [0:3]; reg [1:0] win_idx; wire signed [11:0] sum; always (posedge clk or posedge rst) begin if (rst) begin win[0] 0; win[1] 0; win[2] 0; win[3] 0; win_idx 0; end else if (new_sample) begin win[win_idx] xy_in; win_idx win_idx 1b1; end end assign sum win[0] win[1] win[2] win[3]; assign xy_filtered sum 2;6.3 坐标旋转与镜像处理有些应用需要把坐标旋转90度或者做镜像。在FPGA里做旋转就是交换X和Y然后对其中一个取反。镜像就是对某个轴取反。这些操作都是组合逻辑一个时钟周期就能完成。但要注意取反后的溢出问题。10位有符号数取反后-512会变成512超出范围。所以取反后要饱和处理把512钳位到511。这个细节在仿真时很容易漏掉上板后如果正好碰到边界值振镜就会跳一下。7. 资源优化与时序约束7.1 在低端FPGA上跑XY2-100XY2-100的逻辑非常简单一个状态机加一个移位寄存器再加几个计数器和加法器总共也就几十个触发器。哪怕是最便宜的CPLD都能跑。但如果你用的是老旧的FPGA比如Spartan-3或者Cyclone II还是要注意资源优化。我的建议是不要用IP核生成分频器直接写计数器分频省去IP核的额外逻辑。移位寄存器用普通的reg数组就行综合器会自动推断成触发器不需要调用SRL16或者移位寄存器IP。校验位生成用异或归约一行代码搞定。7.2 时序约束文件的写法时序约束是保证上板稳定的关键。对于XY2-100这种低速接口不需要做复杂的时序分析但至少要给时钟线和数据线加上输出延迟约束。在Xilinx的XDC文件里可以这样写set_output_delay -clock [get_clocks clk_50m] -max 10.0 [get_ports data_out] set_output_delay -clock [get_clocks clk_50m] -min 2.0 [get_ports data_out] set_output_delay -clock [get_clocks clk_50m] -max 10.0 [get_ports sync_out] set_output_delay -clock [get_clocks clk_50m] -min 2.0 [get_ports sync_out]这些数字不是随便填的要根据驱动器的建立保持时间和线缆延迟来算。如果你不确定可以先填一个保守的值比如max 15nsmin 0ns然后上板测试。如果振镜工作正常再逐步收紧约束观察是否出现偶发错误。7.3 布局布线的注意事项FPGA的布局布线对输出偏斜影响很大。尽量把XY2-100的三根输出线分配到同一个IO Bank而且最好是物理位置相邻的引脚。在Vivado里可以用Pblock约束把相关逻辑锁定到同一个区域。另外输出IO的标准要设置正确通常是3.3V LVCMOS。如果振镜驱动器是5V输入需要加电平转换芯片不能直接把3.3V IO接到5V输入端否则可能损坏FPGA。8. 从XY2-100延伸出去的一些想法XY2-100只是一个起点。如果你已经把它跑通了接下来可以尝试几件事。第一把发送端改成接收端用FPGA去解析振镜驱动器反馈的位置信号实现闭环控制。第二把XY2-100和SPI或者I2C结合起来用FPGA做协议转换让上位机通过USB或者以太网发送指令FPGA再转成XY2-100驱动振镜。第三如果你对图像处理感兴趣可以在FPGA里实现一个简单的矢量图形生成器把直线和圆弧插补成XY2-100的坐标流直接驱动振镜画图。这些扩展方向每一个都够写一篇长文但核心的时序思想和状态机设计方法是相通的。我在实际项目里最深的体会是协议本身并不复杂复杂的是对时序余量的把握和对现场干扰的预估。仿真通过只是及格线上板能稳定跑上几个小时不出错才算真正过关。每次调试振镜我都会在FPGA里留一个计数器记录发送了多少帧以及校验错误发生了多少次。这个计数器通过UART打印出来是我判断链路质量的唯一依据。别小看这个简单的统计它帮我抓出过好几次线缆接触不良和电源纹波过大的问题。