
激光振镜控制入门XY2-100协议Verilog实现全解析附代码如果你在调试一台激光打标机或者激光焊接机应该见过那两块高速摆动的镜片——X轴和Y轴振镜。它们配合着把激光束反射到工作台的不同位置每分钟可以跳转上万次。而这套动作背后FPGA把坐标数据打包成XY2-100协议帧通过差分信号送到振镜驱动器驱动器再驱动电机带动镜片偏转。这套链路里XY2-100协议是最常见、也是绕不开的一道坎。这篇文章就从电路时序讲到Verilog实现把我实际调通的一版发送器完整拆开适合刚开始接触振镜控制或者写过SPI、UART但没碰过XY2-100的朋友。看完之后你至少能照着代码写出一个可以跑仿真的发送模块并且知道上板调试时应该抓哪些波形。先说清楚一点XY2-100本质上是振镜驱动器都认的一套串行数字接口。它的帧结构不复杂真正麻烦的是时序细节和不同驱动器之间的头部字段差异。我写这篇文章时会把“常见配置”和“必须查手册的配置”分开讲避免你拿着我的代码去接一个完全不兼容的驱动器然后怀疑人生。别问我是怎么知道的问就是我上过当。1. 项目概述与设计思路拆解1.1 振镜控制链路到底在做什么激光振镜控制的通链路通常是这样的上位机软件算好打标轨迹把每一条线段离散成一系列坐标点然后通过以太网、USB、或者PCIe把坐标数据丢给控制板。控制板上的FPGA拿到坐标后先做插补和坐标变换再把X轴、Y轴对应的角度值转换成XY2-100协议帧最后通过RS-422差分芯片输出到振镜驱动器。振镜驱动器内部是一个高精度的位置闭环系统它接收数字命令和振镜电机上的位置反馈做比较经过电流环、速度环、位置环的调节让振镜偏转到指定角度。所以我们在FPGA侧最关心的是“怎么把角度数据准确、准时地送进驱动器的嘴巴里”至于驱动器内部怎么跑PID、怎么换向一般不需要我们管。XY2-100协议就是进这张嘴巴的那块“饭票格式”。数据是串行的、同步的驱动器靠CLOCK做时钟靠SYNC识别帧头然后在每一个CLOCK沿上把DATA串行采样进去。这样一条数据线加一条时钟线就能把16位甚至更高精度的角度值传过去。1.2 为什么不用模拟量或者SPI很多刚接触振镜的人会问以前模拟振镜不是挺多吗为什么现在都用数字接口了确实早期振镜控制常用±10V模拟电压角度和电压成正比。但模拟量在工业现场容易被电机、开关电源干扰线缆长了还会衰减再加上DAC芯片的温度漂移和校准问题定位精度很难往上走。SPI的问题则在于它是单端信号抗干扰能力弱而且振镜驱动器并没有一个统一的SPI标准各家接法五花八门。XY2-100则几乎是所有中高端振镜驱动器标配的数字接口。它用差分信号传输CLOCK、SYNC、DATA共模抑制能力强线缆长度可以做到几米甚至十几米再加上协议本身是标准的换一家驱动器只需要微调参数甚至完全不用改代码。这些优势决定了它在这个行业里绕不开。当然有些更高速的振镜系统会采用Sercos III或者EtherCAT之类的工业总线那是另一个方向。如果你只是想快速驱动一个现成的振镜摆头XY2-100就是最直接、成本最低的路径。1.3 技术方案与设计目标我的设计目标很明确在FPGA上用纯逻辑实现一个XY2-100主设备发送器占用资源尽量少时序尽量干净且能够参数化调整比特率、帧长度和SYNC长度。模块划分上我把它拆成了四块位时钟分频器负责从系统时钟产生bit时钟状态机负责整体帧流程并转串逻辑负责把头部和数据拼成20位帧并逐位输出最后在顶层把CLOCK、SYNC、DATA引到引脚外面接一颗RS-422差分芯片就上板了。整个发送器不算接收回读功能的话几十个LUT加几十个触发器就能放下对FPGA资源非常友好。2. XY2-100协议核心细节解析2.1 CLOCK/SYNC/DATA三根线的协作关系XY2-100的物理层是差分信号通常在板子上看到的是CLOCK/-、SYNC/-、DATA/-三对差分线。FPGA的普通GPIO引脚是单端信号所以中间要加一颗RS-422收发器比如TI的AM26LV31E做发送端AM26LV32E做接收端。协议逻辑本身不关心差分还是不差分它只处理单端的0和1所以Verilog代码里看到的只是三根普通线xclk、xsync、xdata。时序上CLOCK是连续方波SYNC拉高几个周期表示一帧数据要开始了DATA在SYNC结束之后紧跟着每一位数据。接收端在CLOCK的上升沿采样DATA所以我的代码里把数据更新点放在CLOCK低电平期间这样上升沿到来时数据线已经稳定了半个位周期以上建立时间比较充裕。这里有一个容易被忽略的坑有些驱动器的头部字段里包含了校验位或者通道号SYNC拉高期间应该输出一个确定电平而不是高阻。我的做法是空闲时把xdata保持为低电平SYNC拉高时也保持低电平确认过手上几款驱动器的兼容性没问题。2.2 帧格式20位还是16位头部字段怎么办XY2-100最常见的帧组织方式是20位前4位是控制/格式位后16位是有效数据。这4位在不同厂家的含义不同有的表示通道选择有的表示范围选择有的只是保留位。以我手头使用的振镜驱动器为例它要求前4位的第一位固定为0第二位是通道选择0为X轴1为Y轴后两位固定为0。所以你如果只针对某一种驱动器写代码完全可以硬编码一个默认头部。但更稳妥的做法是把头部做成一个4位输入端口由上层逻辑在发X轴帧和Y轴帧时分别赋值。这样即使换了驱动器只需要改上层的配置而不用动底层时序模块。有些振镜驱动器也支持无头部、只发16位数据的模式那就把FRAME_BITS参数改成16同时把头部数据从帧里去掉。我的代码里用参数控制了总帧位数改起来很方便。这一点很关键不要假设世界上所有振镜驱动器的帧格式都一模一样必须把“协议公共部分”和“厂家差异部分”隔离。2.3 比特率与时钟分频怎么算XY2-100协议里没有强制规定比特率常见的是2Mbps、5Mbps、8Mbps。比特率越高一帧数据发得越快振镜能响应的刷新率就越高但同时也对FPGA时序和驱动器的接收能力要求更高。我一开始做样机时用的是20MHz系统时钟配合2Mbps一帧20位数据只要10微秒对于大部分打标应用已经够用。Verilog里面做分频的本质就是一个计数器。系统时钟频率除以目标比特率得到的是每个数据位占多少个系统时钟周期也就是分频系数。以20MHz、2Mbps为例分频系数是10。代码里的DIV_RATIO参数就填10。注意如果系统时钟不是比特率的整数倍会有累积相位误差串行时钟边缘会抖动所以尽量选能整除的时钟频率。我在实际设计中会让xclk在每半个bit周期翻转一次也就是用div_cnt和DIV_RATIO/2比较来产生一个占空比50%的方波。这样CLOCK看起来就是标准的连续时钟驱动器的锁相电路也比较容易跟踪。2.4 电气接口与阻抗匹配FPGA引脚输出的是单端LVCMOS电平振镜驱动器则是RS-422差分输入。中间必须接RS-422驱动器芯片常见的AM26LV31E或者DS26LV31。芯片输入端接FPGA引脚输出端接DB9或者航空插头的差分对线缆末端一般要接120Ω终端电阻。很多初学者在这里会犯一个错误直接用杜邦线把FPGA引脚接到驱动器上然后发现数据完全不对。原因就是电平不匹配甚至可能烧引脚。另外差分线要尽量短最好用双绞线并且收发两端的GND要共地。如果你用笔记本USB供电的FPGA开发板去接一台工业振镜驱动器共地问题会非常明显经常出现随机跳数。所以板子上除了信号线一定要把GND也连过去。3. Verilog发送器实现从模块到完整代码3.1 顶层模块需要哪些引脚我设计的xy2_100_tx模块输入输出很简单clk是系统时钟rst_n是异步复位start是一个单周期脉冲表示收到一帧数据要开始发送data_in是16位位置数据header是4位头部数据输出端xclk、xsync、xdata就是协议的三个单端信号busy是高电平忙指示。start信号可以由上一级插补模块产生也可以由一个定时器触发。在FPGA引脚规划上xclk、xsync、xdata三根线可以直接约束到普通IO然后从FPGA出来接RS-422驱动器。我建议把这三根信号在顶层各加一级寄存器打拍避免组合逻辑直接驱动引脚造成毛刺。当然我下面的代码里已经用寄存器输出了不用担心这个问题。3.2 位时钟分频和bit_tick的产生分频部分的核心就是一个div_cnt计数器。每次计数到DIV_RATIO-1时产生一个系统时钟周期宽度的bit_tick脉冲。这个bit_tick就像是整个发送器的灵魂所有状态跳转和数据移位都在它到来时进行。xclk的翻转则更简单div_cnt小于DIV_RATIO/2时输出高否则输出低这样CLOCK就是连续方波。// 位时钟分频产生bit_tick同时生成50%占空比的xclk always (posedge clk or negedge rst_n) begin if (!rst_n) begin div_cnt 16d0; bit_tick 1b0; end else if (div_cnt DIV_RATIO - 1) begin div_cnt 16d0; bit_tick 1b1; // 一个完整bit周期结束产生脉冲 end else begin div_cnt div_cnt 1b1; bit_tick 1b0; end end always (posedge clk or negedge rst_n) begin if (!rst_n) xclk 1b0; else if (div_cnt (DIV_RATIO 1)) xclk 1b1; else xclk 1b0; end这里有个细节想提醒一下xclk连续翻转意味着即使在IDLE状态驱动器也能看到时钟。这对大多数驱动器来说是正常的因为协议本身就是异步连续时钟SYNC同步帧头。如果你的驱动器手册说空闲时CLOCK可以停止那也可以在状态机里加一个xclk_en但这会让代码复杂一些我一般不做。3.3 状态机设计IDLE、SYNC_HOLD、SEND_BITS状态机我用三段式。状态定义三个IDLE等待start脉冲SYNC_HOLD保持SYNC高电平若干个bit周期SEND_BITS逐位输出20位数据。这样一个简单的状态机verilog写法对初学者来说也好理解。localparam IDLE 2d0; localparam SYNC_HOLD 2d1; localparam SEND_BITS 2d2; reg [1:0] state, next_state; reg [2:0] sync_cnt; reg [4:0] bit_index; reg [FRAME_BITS-1:0] frame_reg;SYNC_HOLD里需要一个计数器sync_cnt。它在state等于SYNC_HOLD并且bit_tick为高时累加累计到SYNC_CLKS-1时下一个bit_tick就跳到SEND_BITS。SEND_BITS里则用bit_index从FRAME_BITS-1往下数到0每来一个bit_tick发送并递减一位减到0时下一拍回到IDLE。always (posedge clk or negedge rst_n) begin if (!rst_n) state IDLE; else state next_state; end always (*) begin next_state state; case (state) IDLE: if (start) next_state SYNC_HOLD; SYNC_HOLD: if (bit_tick (sync_cnt SYNC_CLKS - 1)) next_state SEND_BITS; SEND_BITS: if (bit_tick (bit_index 0)) next_state IDLE; default: next_state IDLE; endcase end这里要注意组合逻辑里的next_state判断用的是bit_tick和sync_cnt、bit_index这些寄存器值。这没问题因为它们都是同步更新后的稳定值。实际综合出来可能会多几个LUT但完全可接受。3.4 帧数据拼接与并转串输出帧数据的拼接我放在start到来时做。如果FRAME_BITS是20就把{header, data_in}赋给frame_reg如果FRAME_BITS是16就直接把data_in赋给frame_reg。发送时从最高位开始依次把frame_reg[bit_index]送到xdata寄存器上。// 帧数据更新start到来时锁存当前数据 always (posedge clk or negedge rst_n) begin if (!rst_n) begin frame_reg {FRAME_BITS{1b0}}; bit_index 5d0; end else if (start) begin if (FRAME_BITS 20) frame_reg {header, data_in}; // 4位头部 16位数据 else frame_reg {{(FRAME_BITS-DATA_WIDTH){1b0}}, data_in}; bit_index FRAME_BITS - 1; end else if (state SEND_BITS bit_tick) begin if (bit_index 0) bit_index 5d0; else bit_index bit_index - 1b1; end end // 数据输出在CLOCK低电平期间更新避免与上升沿冲突 always (posedge clk or negedge rst_n) begin if (!rst_n) xdata 1b0; else if (state SEND_BITS bit_tick) xdata frame_reg[bit_index]; else if (state SYNC_HOLD) xdata 1b0; end // 同步输出 always (posedge clk or negedge rst_n) begin if (!rst_n) xsync 1b0; else if (state SYNC_HOLD) xsync 1b1; else xsync 1b0; end // 忙信号 always (posedge clk or negedge rst_n) begin if (!rst_n) busy 1b0; else if (start) busy 1b1; else if (state SEND_BITS bit_tick (bit_index 0)) busy 1b0; end数据更新点选择在bit_tick也就是div_cnt等于DIV_RATIO-1时此刻xclk处于低电平的下半段。随后xclk变成高电平驱动器在上升沿采样建立时间大概有半个bit周期以上。2Mbps的bit周期是500ns半个周期250ns足够绝大多数驱动器采样了。3.5 完整发送器代码把上面这些片段拼在一起就是完整的xy2_100_tx模块。我把它整理成了可直接编译的版本参数都用localparam或parameter定义好了仿真或者上板前只需要根据实际系统时钟和比特率修改DIV_RATIO。////////////////////////////////////////////////////////////////////////////// // xy2_100_tx.v // XY2-100协议发送器支持20位帧4位头部16位数据或16位帧配置 // 系统时钟分频产生bit时钟SYNC拉高4个bit周期后逐位输出 ////////////////////////////////////////////////////////////////////////////// module xy2_100_tx #( parameter DIV_RATIO 10, // 系统时钟频率 / 比特率 parameter DATA_WIDTH 16, // 有效数据位宽 parameter FRAME_BITS 20, // 总帧位数设为16则不带头部 parameter SYNC_CLKS 4 // SYNC保持的bit周期数 )( input wire clk, // 系统时钟 input wire rst_n, // 异步复位低有效 input wire start, // 启动发送单周期脉冲 input wire [DATA_WIDTH-1:0] data_in, // 16位位置/角度数据 input wire [3:0] header, // 4位控制头部无头部时可接0 output reg xclk, // 协议时钟输出 output reg xsync, // 协议同步输出 output reg xdata, // 协议数据输出 output reg busy // 忙标志高有效 ); localparam IDLE 2d0; localparam SYNC_HOLD 2d1; localparam SEND_BITS 2d2; reg [15:0] div_cnt; reg bit_tick; reg [1:0] state, next_state; reg [2:0] sync_cnt; reg [4:0] bit_index; reg [FRAME_BITS-1:0] frame_reg; // 1. 分频产生bit_tick和xclk always (posedge clk or negedge rst_n) begin if (!rst_n) begin div_cnt 16d0; bit_tick 1b0; end else if (div_cnt DIV_RATIO - 1) begin div_cnt 16d0; bit_tick 1b1; end else begin div_cnt div_cnt 1b1; bit_tick 1b0; end end always (posedge clk or negedge rst_n) begin if (!rst_n) xclk 1b0; else if (div_cnt (DIV_RATIO 1)) xclk 1b1; else xclk 1b0; end // 2. 状态寄存器 always (posedge clk or negedge rst_n) begin if (!rst_n) state IDLE; else state next_state; end // 3. 次态逻辑 always (*) begin next_state state; case (state) IDLE: if (start) next_state SYNC_HOLD; SYNC_HOLD: if (bit_tick (sync_cnt SYNC_CLKS - 1)) next_state SEND_BITS; SEND_BITS: if (bit_tick (bit_index 0)) next_state IDLE; default: next_state IDLE; endcase end // 4. SYNC周期计数 always (posedge clk or negedge rst_n) begin if (!rst_n) sync_cnt 3d0; else if (state SYNC_HOLD bit_tick) sync_cnt sync_cnt 1b1; else sync_cnt 3d0; end // 5. 帧数据锁存与位索引 always (posedge clk or negedge rst_n) begin if (!rst_n) begin frame_reg {FRAME_BITS{1b0}}; bit_index 5d0; end else if (start) begin if (FRAME_BITS 20) frame_reg {header, data_in}; else if (FRAME_BITS DATA_WIDTH) frame_reg data_in; else frame_reg data_in; // 其他情况自行调整 bit_index FRAME_BITS - 1; end else if (state SEND_BITS bit_tick) begin if (bit_index 0) bit_index bit_index; else bit_index bit_index - 1b1; end end // 6. 数据输出 always (posedge clk or negedge rst_n) begin if (!rst_n) xdata 1b0; else if (state SYNC_HOLD) xdata 1b0; else if (state SEND_BITS bit_tick) xdata frame_reg[bit_index]; end // 7. SYNC输出 always (posedge clk or negedge rst_n) begin if (!rst_n) xsync 1b0; else xsync (state SYNC_HOLD) ? 1b1 : 1b0; end // 8. busy输出 always (posedge clk or negedge rst_n) begin if (!rst_n) busy 1b0; else if (start) busy 1b1; else if (state SEND_BITS bit_tick (bit_index 0)) busy 1b0; end endmodule这段代码里有一个细节SEND_BITS状态下bit_index等于0时我让它保持0而不是减成负数避免下一拍出现下溢。实际最后一位发送完成后下一拍就回到IDLE了所以bit_index具体是多少影响不大但这样写更干净。3.6 仿真测试平台搭建仿真代码我习惯直接用VCD波形验证时序不用太复杂的UVM环境。下面这个testbench会给出一帧数据然后等待busy拉低再结束仿真。如果波形里看到SYNC先拉高4个bit周期随后xdata按高位到低位依次输出基本就对了。timescale 1ns/1ps module tb_xy2_100_tx; reg clk 0; reg rst_n 0; reg start 0; reg [15:0] data_in 16h3A5C; reg [3:0] header 4b0000; wire xclk, xsync, xdata, busy; always #25 clk ~clk; // 20MHz时钟 initial begin repeat (10) (posedge clk); rst_n 1; repeat (5) (posedge clk); start 1; (posedge clk); start 0; wait (busy 1b0); $display(Frame done.); #1000; $finish; end xy2_100_tx #( .DIV_RATIO (10), .DATA_WIDTH(16), .FRAME_BITS(20), .SYNC_CLKS (4) ) u_dut ( .clk (clk), .rst_n (rst_n), .start (start), .data_in (data_in), .header (header), .xclk (xclk), .xsync (xsync), .xdata (xdata), .busy (busy) ); initial begin $dumpfile(xy2_100_tx.vcd); $dumpvars(0, tb_xy2_100_tx); end endmodule仿真跑完以后重点观察两个点一是xclk是不是50%占空比的连续方波二是xsync拉高的起点是不是和xclk低电平段末尾对齐。如果start是在任意时刻来的SYNC拉高会等到下一个bit_tick所以xsync上升沿和xclk上升沿之间可能会有最多一个bit周期的延时这是正常的驱动器不会因此误判。真正不能接受的是SYNC在CLOCK高电平中间跳变那时候帧头会乱。4. 上板调试与常见问题排查4.1 用示波器和逻辑分析仪抓哪些信号如果你有逻辑分析仪抓xclk、xsync、xdata三根线就够了。把采样率设成至少10倍于比特率然后触发条件设为xsync上升沿这样可以稳定捕获一帧完整波形。如果你手里只有示波器那至少用两根探头一根抓xsync一根抓xdata在xsync拉高后的第二个bit处开始观察数据位是否和预期一致。上板测试时我建议先在FPGA内部写一个简单的测试源比如固定发0x3A5C然后在示波器上数20位数据核对每一位的电平。确认无误后再接振镜驱动器。这一步看起来很笨但能省掉后面联调时的大量排查时间。毕竟如果你连固定数据都发不对就谈不上动态跟踪了。还有一个常用的验证方法是自环把xclk、xsync、xdata从FPGA引脚引出来通过RS-422芯片变成差分信号再接回另一颗RS-422接收芯片变成单端回读进FPGA的一个接收模块。这样可以在没有真实驱动器的情况下验证协议时序和板级传输质量。自环能通过基本就能排除FPGA逻辑和电平转换芯片的问题。4.2 常见问题速查表我把实际调试中碰到过的问题整理成了一张表基本上新手上板遇到的坑都在里面。现象可能原因排查与解决办法振镜完全不动差分芯片没供电或焊接错误SYNC长度不够数据帧头部字段不匹配用示波器确认差分信号是否正常确认SYNC至少保持4个bit周期认真读驱动器手册用默认头部0试数据随机跳变偶尔动作共地不良CLOCK抖动太大数据建立时间不足检查GND连接线缆用双绞线确认系统时钟频率稳定把数据更新点挪到CLOCK低电平段固定数据但实际角度不对位序发反了头部字段含义理解错误驱动增益设置不对检查MSB/LSB顺序常见协议是最高位先发对比驱动器软件里的调试界面确认每一帧的16位数据含义第一帧总是丢后面正常上电后启动时序不对start信号和xclk不同步上电后等待几个bit周期再发start或者干脆让start沿bit_tick对齐长时间运行偶尔卡死FIFO溢出/欠压复位不稳定外部电磁干扰检查上层数据链路增加看门狗模块在FPGA输入侧加滤波电容这里特别想强调一下MSB和LSB的坑。不同驱动器对位序的定义可能不同绝大多数XY2-100实现是最高位先发但有个别低成本驱动器偏偏反着来。我的模块里是从frame_reg[bit_index]的最高位开始发如果你的驱动器手册说先发最低位只需要改成从0开始递增即可。4.3 资源占用与后续优化这个发送器在Artix-7上综合下来资源消耗大概不到40个LUT和40个FF非常小巧。整个振镜控制系统的资源大头通常不在协议而在坐标变换、插补和FIFO缓存上。所以我建议协议模块尽量做得轻量不要在里面堆功能。如果你需要同时支持多个振镜轴最简单的办法是例化多个xy2_100_tx每个轴一个。它们的C LOCK可以共用同一个分频时钟但SYNC和DATA各自独立。只要保证所有channel的帧头和帧周期对齐就能做到多轴同步。实际做双轴振镜时我习惯把X轴和Y轴的上层数据先写入同一个FIFO然后由同一个发送调度器轮流发X帧和Y帧这样两个轴的命令刷新时刻非常接近激光轨迹的转角处不会出现明显错位。更进一步的优化方向是加入接收模块用来回读振镜驱动器的状态和位置反馈。接收模块本质上是另一个状态机xclk连续运行在CLOCK上升沿采样DATA通过SYNC定位帧头再把16位数据并出来存入一个异步FIFO供上层处理。这个扩展我会放在后面讲平滑滤波时一起说。5. 进阶扩展让振镜动得平滑一点5.1 为什么位置命令不能太“暴力”直接把上位机给的坐标点一股脑塞给振镜会有一个隐患振镜是带质量的机械部件有固有谐振频率。如果你给它的角速度指令变化太陡相当于一个阶跃激励振镜会产生过冲和振铃轻则轨迹边缘有毛刺重则听到“嗡嗡”的电感啸叫长期下来机械结构还会疲劳。所以真正量产的控制板里都会在XY2-100发送前做一级数字化平滑。平滑滤波的常见手段有两类一类是滑动窗口均值滤波一类是一阶IIR低通。滑动窗口的优点是线性相位群延迟固定适合对轨迹时间一致性要求高的场景IIR的低通效果更好但相位特性是非线性的延迟随频率变化。我做的第一个版本用的是滑动窗口因为它实现简单参数也直观。5.2 滑动窗口均值滤波的Verilog设计思路滑动窗口均值滤波的原理是维护一个长度为N的队列每来一个新数据把它加进总和同时把队列里最老的数据减掉最后输出总和除以N。如果N是2的幂除法可以直接用右移实现非常省资源。// 滑动窗口均值滤波示例窗口长度N8数据位宽W16 module sliding_avg #( parameter N 8, parameter W 16 )( input wire clk, input wire rst_n, input wire [W-1:0] din, input wire din_valid, output reg [W-1:0] dout ); reg [W-1:0] window [0:N-1]; reg [$clog2(N)-1:0] ptr; reg [W$clog2(N)-1:0] sum; wire [W-1:0] old_data window[ptr]; always (posedge clk or negedge rst_n) begin if (!rst_n) begin ptr 0; sum 0; dout 0; end else if (din_valid) begin sum sum - old_data din; window[ptr] din; ptr ptr 1b1; dout (sum - old_data din) $clog2(N); end end endmodule这段代码的思路很清楚用ptr指针指向即将被覆盖的最老数据更新时先减旧值再加新值dout直接输出滤波后的结果。注意sum的位宽要比W多出log2(N)位否则累加会溢出。窗口长度N我一般取8或16再大群延迟就有点明显了振镜会显得“肉”。在使用时把上位机坐标先经过这个滤波模块再把输出接到xy2_100_tx的data_in上。这样每次给振镜驱动器的位置命令都是平滑变化的振镜跟随起来会稳定很多。不过要注意滤波会给信号带来额外延迟闭环控制系统要记得把这一拍考虑进去。5.3 更进一步插补、坐标变换与三角函数滑动窗口只是最基础的一层。实际振镜扫描中上位机给的常常是线段、圆弧甚至任意矢量图形FPGA要在中间做插补把一条线段离散成密集的坐标点。这部分如果用纯Verilog写常用的方法是DDA直线插补用累加器判断每一步X、Y方向该不该走。圆弧插补则复杂一些一般会引入CORDIC算法计算正弦余弦也就是你搜索verilog arctan时可能看到的东西。我在做下一个版本时计划加入一个小型插补引擎输入线段起点、终点和速度参数输出一系列中间点然后经过滑动窗口滤波再交给XY2-100发送器。这样做的好处是把打标画图的大部分工作下沉到FPGA上位机只需下发少量图形指令大幅降低通信带宽和延迟。但这是一条漫长的路需要从基础的定时器、计数器verilog开始一点点搭。最后分享一个我踩过多次的体会XY2-100协议本身一点也不难难的是把“协议时序”和“上层数据流”正确对接。很多人在仿真里看到xclk、xsync、xdata都对一接驱动器就出问题原因往往出在头部字段没配好或者系统时钟域没有做同步处理。所以同一个项目里状态机verilog、计数器verilog、异步FIFO、跨时钟域处理这些基本功最后都会用得上。先把这个发送器跑通再往上层加滤波和插补你会发现自己已经能独立调通一套完整的振镜控制链路了。