ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

FPGA实现XY2-100振镜控制协议:Verilog时序设计与调试指南

FPGA实现XY2-100振镜控制协议:Verilog时序设计与调试指南 前一阵帮客户调一套振镜打标控制系统发现很多新手把 XY2-100 协议当成普通串口协议去写 Verilog结果数据怎么都对不上波形一抓一大把毛刺。说实话XY2-100 这个协议本身不算复杂核心难点在于它的时序要求、帧结构定义以及和振镜驱动板之间的电气配合。这篇就把激光振镜控制里最常用到的 XY2-100 协议完整拆开讲一遍从物理层、帧结构到 Verilog 状态机实现再到仿真和上板调试的坑最后附上可以直接用的代码和 Testbench 片段。适合想入门振镜控制、或者要做 FPGA 协议开发的工程师参考单片机党也能从中理解为什么这类场景需要硬实时逻辑。1. 先搞明白激光振镜控制到底在控制什么1.1 振镜不是“镜面偏转”那么简单激光振镜系统一般由上位机、运动控制卡或控制软件、XY 振镜驱动板、振镜电机、场镜和激光器组成。上位机负责轨迹规划把坐标数据发给控制卡控制卡再把坐标转换成模拟电压或者数字协议信号驱动 XY 振镜电机偏转使激光光斑在加工平面上快速定位和扫描。振镜电机本质上是一种高精度的检流计电机转子上装有反射镜片内部有位置反馈传感器电容式或光电式。驱动板内部是一个伺服闭环它根据接收到的目标位置指令结合当前位置反馈输出电流驱动电机转动。XY2-100 协议做的就是“控制卡 - 驱动板”这一段位置指令传输。协议的核心是把 16 位带符号的位置数据以串行差分方式发送给振镜驱动板驱动板解码后立刻执行。这个协议在激光打标、雕刻、焊接、切割设备里非常常见。它替代了早年用 DAC 模拟电压控制振镜的方式抗干扰能力强布线简单而且可以同时传输多个通道的数据。如果你正在做激光振镜相关的控制系统大概率绕不开它。1.2 XY2-100 协议的物理层和帧结构XY2-100 协议基于 RS-422 差分电气标准不是普通的单端串口也不是 LVDS。接口上至少包含四组主要信号信号名方向作用CLOCKMaster - Slave2MHz 串行时钟数据位随时钟逐位发送SYNCMaster - Slave同步信号用于标记一帧数据的起始CHANNELMaster - Slave通道选择区分当前发送的是 X 轴还是 Y 轴XDATAMaster - SlaveX/Y/Z 通道共用数据线具体看板卡STATUS/ERRORSlave - Master可选回读振镜状态从电气特性来说这四组信号都是差分对一般用 AM26LS31 或者 DS26C31 这类 RS-422 驱动芯片发送接收端用 AM26LS32 或类似芯片。传输线用双绞屏蔽线接收端并在线上加 120Ω 终端电阻大家在实际布线时别把这个和 LVDS 混为一谈。数据帧结构是 XY2-100 最容易让人翻车的地方。最常见的 20 位帧格式如下第 19 位bit19同步头固定为 1标志帧开始第 18 位bit18同步头固定为 1第 17 位到第 2 位bit17 到 bit216 位位置数据MSB 先发第 1 位bit1状态位普通发送时写 0第 0 位bit0错误标志位普通发送时写 0。也就是说一个完整的 20 位字是“2 bit 同步头 16 bit 数据 2 bit 尾部标记”。这和我们常写的 UART 帧结构不一样它没有起始位和停止位的概念完全靠 SYNC 信号来对齐一帧数据。如果 SYNC 时序没对齐驱动板解码时就会整体偏移一位位置数据直接废掉。CHANNEL 信号决定当前 20 位数据属于哪个轴。很多驱动板上 X 通道和 Y 通道是同一个数据线的差分对靠 CHANNEL 来切换。Z 轴动态聚焦在多数板卡上要么复用 X 通道要么通过扩展协议实现具体要以板卡规格书为准。1.3 为什么用 FPGA/Verilog 而不是单片机很多初学者第一反应是这不就是个串行协议吗我用 STM32 的 SPI 或者 GPIO 模拟也能发。确实能发但实际工程中会碰到两个问题。第一个问题是抖动。XY2-100 的 20 位数据和 SYNC、CHANNEL 之间必须是严格同步的关系任何一位的延时抖动都会导致振镜位置输出毛躁。单片机在主循环里发送一旦被中断打断发送时序就会产生几十到几百纳秒的抖动。振镜伺服系统对位置指令的稳定性很敏感指令抖动会直接体现在扫描图形的边缘毛刺上。第二个问题是多通道并行。XYZ 三轴数据需要按固定顺序连续发送还要保证帧与帧之间没有间隙。如果把发送逻辑放在定时器中断里主程序要保证数据更新和帧发送互不干扰代码复杂度会上来可维护性也不好。FPGA 用状态机和计数器天然解决这两个问题时钟确定性高所有信号在同一系统时钟域下打拍输出延迟可控还能很容易地扩展回读状态、滤波、速度前瞻等功能。这也是我在这个方案里选择 Verilog 实现而不是单片机的根本原因。2. 整体架构设计先画好“数据怎么流出去”2.1 顶层模块划分写 Verilog 之前先把模块图画出来。虽然不能用流程图但我们可以用文字描述整个数据通路。顶层设计分三块协议发送模块TX把上位机送来的 X/Y/Z 坐标按 20 位帧格式串行发出去协议接收模块RX接收驱动板返回的 STATUS/ERROR 信息留作状态监控时钟与复位管理系统时钟统一输入内部用计数器产生 2MHz 协议时钟使能。模块间的接口信号大致如下信号方向说明clk输入系统时钟比如 50MHzrst_n输入异步复位低有效start输入启动发送脉冲每帧/每通道一次x_data / y_data / z_data输入上位机写入的坐标数据xy2_clock输出输出给驱动板的 2MHz 差分时钟xy2_sync输出输出给驱动板的同步信号xy2_channel输出输出给驱动板的通道选择xy2_data输出串行数据输出rx_status / rx_error输出接收回读状态busy输出发送忙标志可用于上位机握手2.2 为什么用“时钟使能”而不是直接分频XY2-100 要求 2MHz 协议时钟。如果直接在系统时钟 50MHz 下做一个 2MHz 分频时钟然后用这个分频时钟去驱动协议逻辑会面临两个问题。第一分频时钟是一个由组合逻辑翻转出来的时钟它在 FPGA 内部走的一般不是专用时钟网络时序收敛和约束都麻烦。第二协议逻辑一旦挂在这个手动分频时钟上后续要加滤波器、FIFO、状态机跨时钟域对接马上就会遇到潜在的亚稳态问题。更稳妥的做法是系统时钟统一使用 50MHz内部通过计数器产生一个clk_en使能脉冲每 25 个系统时钟周期产生一个高电平脉冲。这个脉冲等效于 2MHz 的采样节拍所有协议状态机都在这个节拍上工作而物理的xy2_clock则在这个节拍下翻转输出给驱动板。这样做的好处非常明显。整个工程只有一个主时钟域时序约束简单状态机的每一个跳变都由系统时钟驱动可综合、可仿真。实际项目中我看到很多人把协议时钟直接当内部时钟用结果上板后偶尔出现数据错位排查起来非常痛苦。用时钟使能是 FPGA 设计里很基础也很重要的思路后面所有代码都建立在它上面。2.3 状态机设计从空闲到一帧结束XY2-100 的发送逻辑可以用一个简单的状态机描述。我的做法是分成四个状态IDLE等待 start 脉冲HEAD发送 2 个同步头位0 和 1 都是 1DATA发送 16 位数据MSB 先发TAIL发送 2 个尾部标志位0、0。如果开启了 Z 轴发送完一帧后自动切换到下一个通道继续发送下一帧直到三个通道都发完回到 IDLE。内部用一个 bit_cnt 计数器范围 0~19配合组合逻辑选择当前要输出的数据位。同步信号 SYNC 则在 bit_cnt 等于 0 时拉高其余时间拉低这样驱动板能清晰识别每帧的起点。这一版状态机设计的关键在于所有跳转都发生在clk_en有效时避免乱打拍子。实际写代码时状态机的输出必须用寄存器寄存不能直接拿 wire 出去否则亚稳态和毛刺很难查。3. Verilog 代码实现可落地的 XY2-100 发送与接收3.1 参数化发送器XY2-100 Master 模块下面是一份完整的发送器代码可以直接在 Vivado 或 Quartus 里综合。参数CLK_DIV用来适配不同的系统时钟默认 25 对应 2MHz 协议时钟。module xy2_100_tx #( parameter DATA_WIDTH 16, parameter CLK_DIV 25, parameter AXIS_NUM 3 )( input wire clk, input wire rst_n, input wire start, input wire [DATA_WIDTH-1:0] x_data, input wire [DATA_WIDTH-1:0] y_data, input wire [DATA_WIDTH-1:0] z_data, output reg xy2_clock, output reg xy2_sync, output reg xy2_channel, output reg xy2_data, output reg busy ); localparam IDLE 2d0; localparam HEAD 2d1; localparam DATA 2d2; localparam TAIL 2d3; reg [7:0] div_cnt; wire clk_en; reg [4:0] bit_cnt; reg [1:0] state; reg [2:0] axis_cnt; reg [DATA_WIDTH-1:0] x_buf; reg [DATA_WIDTH-1:0] y_buf; reg [DATA_WIDTH-1:0] z_buf; reg [DATA_WIDTH-1:0] data_cur; assign clk_en (div_cnt (CLK_DIV - 1)); // 产生协议时钟使能 always (posedge clk or negedge rst_n) begin if (!rst_n) div_cnt 0; else if (clk_en) div_cnt 0; else div_cnt div_cnt 1; end // 协议时钟翻转 always (posedge clk or negedge rst_n) begin if (!rst_n) xy2_clock 1b0; else if (clk_en) xy2_clock ~xy2_clock; end // 状态机主体 always (posedge clk or negedge rst_n) begin if (!rst_n) begin state IDLE; bit_cnt 0; axis_cnt 0; xy2_sync 1b0; xy2_channel 1b0; xy2_data 1b0; busy 1b0; end else if (clk_en) begin case (state) IDLE: begin if (start) begin x_buf x_data; y_buf y_data; z_buf z_data; axis_cnt 0; busy 1b1; state HEAD; bit_cnt 0; end end HEAD: begin xy2_data 1b1; // 同步头固定为 1 xy2_sync (bit_cnt 0) ? 1b1 : 1b0; if (bit_cnt 1) state DATA; bit_cnt bit_cnt 1; end DATA: begin case (axis_cnt) 3d0: data_cur x_buf; 3d1: data_cur y_buf; default: data_cur z_buf; endcase // MSB 先发bit_cnt 为 2 时输出 data[15] xy2_data data_cur[DATA_WIDTH-1 - (bit_cnt - 2)]; xy2_sync 1b0; if (bit_cnt 17) state TAIL; bit_cnt bit_cnt 1; end TAIL: begin xy2_data 1b0; // 状态位、错误位固定为 0 xy2_sync 1b0; if (bit_cnt 19) begin if (axis_cnt AXIS_NUM - 1) begin state IDLE; busy 1b0; end else begin axis_cnt axis_cnt 1; state HEAD; end bit_cnt 0; end else begin bit_cnt bit_cnt 1; end end endcase end end // 通道选择X 为 0Y 为 1Z 复用 X 通道实际看驱动板规格 always * begin case (axis_cnt) 3d0: xy2_channel 1b0; 3d1: xy2_channel 1b1; default: xy2_channel 1b0; endcase end endmodule这里有几个细节值得单独说明。第一HEAD 和 DATA 阶段都要在赋值之后再更新 bit_cnt这样数据在协议时钟上升沿刚好稳定驱动板采样不虚。第二DATA 阶段直接用了组合逻辑来选择data_cur的某一位但真正的xy2_data是寄存器输出所以不存在毛刺。第三TAIL 阶段最后的跳转根据 axis_cnt 判断是否发完所有轴这样单轴、双轴、三轴模式都能复用。3.2 接收器和顶层整合发送器只是半路振镜驱动板一般会返回 STATUS 和 ERROR 信号尤其在一些高端振镜板卡上回读位置、温度、报警状态是有用的。接收器逻辑比发送器简单核心是检测 SYNC 脉冲然后按 20 位节拍采样数据线。module xy2_100_rx #( parameter DATA_WIDTH 16 )( input wire clk, input wire rst_n, input wire rx_sync, input wire rx_data, output reg [DATA_WIDTH-1:0] rx_frame, output reg rx_valid, output reg rx_error ); reg [4:0] bit_cnt; reg [19:0] shift_reg; reg detecting; always (posedge clk or negedge rst_n) begin if (!rst_n) begin bit_cnt 0; shift_reg 0; detecting 0; rx_valid 0; rx_error 0; end else begin if (rx_sync) begin detecting 1b1; bit_cnt 0; end if (detecting) begin shift_reg {shift_reg[18:0], rx_data}; if (bit_cnt 19) begin detecting 1b0; // 检查同步头如果前两位不是 11则认为帧异常 rx_error ~(shift_reg[19] shift_reg[18]); rx_frame shift_reg[17:2]; rx_valid 1b1; end else begin bit_cnt bit_cnt 1; end end end end endmodule顶层模块就是把 TX 和 RX 例化在一起输入输出管脚对接。实际工程里TX 的数据来源可能是上位机写入的寄存器也可能是内部轨迹插补模块输出的坐标这些都可以通过总线接到顶层。3.3 信号时序和代码关键点解释写代码容易写对时序才是重点。为了方便理解我把一帧 20 位的输出时序列出来bit_cnt状态xy2_data 输出xy2_sync0HEAD111HEAD102DATAdata[15]03DATAdata[14]0...DATA...017DATAdata[0]018TAIL0019TAIL00协议时钟每 25 个系统时钟翻转一次所以一帧 20 位大约需要 20 个协议时钟周期也就是 10μs。如果 X、Y、Z 三轴连续发送一个完整刷新周期大约 30μs对应刷新率超过 30kHz远高于振镜本身的伺服带宽实际使用完全够用。还有一点必须强调。DATA 阶段里我用了一组 case 语句来更新data_cur这个赋值在每个协议节拍都会执行一次其实只要在进入 DATA 状态之前锁存好当前轴的数据就行。从代码逻辑上看放在 DATA 状态下每次也对只是会在前几个时钟周期造成data_cur的短暂变化。为了更干净我建议把data_cur的锁存放到 HEAD 状态结束前确保数据在 DATA 阶段全程稳定。4. 仿真与上板调试实录4.1 写 Testbench 时最容易漏的 3 件事Verilog 写完了先别急着上板。我见过太多人一边下板一边抓头发其实问题在仿真阶段就能暴露。Testbench 里最容易漏三件事。第一系统时钟和复位信号必须初始化完整。比如 50MHz 系统时钟周期 20ns用always #10 clk ~clk;生成复位保持至少 100ns。很多人只给一个时钟周期复位导致状态机还没完全进入确定状态就开始工作仿真波形一团乱。第二start 脉冲宽度要控制好。如果发送器正在忙上位机再次拉高 start此时状态机在 IDLE 阶段已经判断过 start不会重复锁存数据。但测试时如果 start 持续拉高状态机会一直从 IDLE 跳到 HEAD导致看起来发了很多帧实际坐标根本没更新。正确做法是用短脉冲或者让 start 和 busy 信号做互锁。第三观察内部信号不要只盯着输出管脚。仿真时多抓 state、bit_cnt、axis_cnt、clk_en 这几个内部信号一眼就能看出状态机是不是按预期跑。输出管脚上看到的数据线如果和预期坐标对不上多半是内部计数偏移了。下面是一段简单的 Testbench 模板module tb_xy2_100_tx(); reg clk 0; reg rst_n 0; reg start 0; reg [15:0] x_data 16h8000; // 模拟零点 reg [15:0] y_data 16h8000; reg [15:0] z_data 16h8000; wire xy2_clock, xy2_sync, xy2_channel, xy2_data, busy; xy2_100_tx #( .DATA_WIDTH(16), .CLK_DIV(25), .AXIS_NUM(3) ) u_tx ( .clk(clk), .rst_n(rst_n), .start(start), .x_data(x_data), .y_data(y_data), .z_data(z_data), .xy2_clock(xy2_clock), .xy2_sync(xy2_sync), .xy2_channel(xy2_channel), .xy2_data(xy2_data), .busy(busy) ); always #10 clk ~clk; initial begin #100; rst_n 1; #100; start 1; #20; start 0; #30000; $finish; end endmodule跑完仿真重点看 xy2_data 在 HEAD 阶段是不是两个高电平DATA 阶段的第一位是不是 x_data[15]TAIL 阶段是不是两个低电平。确认无误后再上板。4.2 实际接线和差分芯片选型上板调试要解决电气问题。XY2-100 是 RS-422 差分信号发送端一般选 AM26LS31接收端选 AM26LS32。这类芯片很常见价格便宜四路差分刚好覆盖 CLOCK、SYNC、CHANNEL、DATA 四组信号。接线方面每组差分对用一对双绞线屏蔽层单端接地。终端电阻放在接收端按 120Ω 匹配别在发送端并电阻否则会吃掉信号幅度长时间传输还会发热。很多人一上来用普通杜邦线飞线测试波形全是振铃扫描图形就会莫名抖动。老老实实用双绞屏蔽线。供电也要注意。AM26LS31 的工作电压一般是 5VFPGA 开发板如果只有 3.3V 接口需要加电平转换或者选 3.3V 兼容的 RS-422 驱动芯片比如 DS26C31 有些型号支持 3.3V。直接拿 3.3V GPIO 怼振镜差分输入多半不可靠。4.3 上板调试示波器波形判读实际抓波形时重点关注三件事。第一CLOCK 频率是不是稳定在 2MHz 左右。如果频率偏低或抖动先查分频参数是否匹配系统时钟。第二SYNC 脉冲是不是出现在每一帧的第一个协议时钟周期。如果 SYNC 和 HEAD 没有对齐驱动板解出的数据就会整体移位。第三用 CHANNEL 信号作为示波器触发源观察 DATA 线上前两位是不是连续高电平中间 16 位是不是按坐标数据变化。把 x_data 改成 16h8000 时DATA 阶段前几位应该是 1、0、0、0…… 如果看到第一位是 0说明发送器帧头用错了状态赶紧回仿真查状态机的跳变条件。5. 常见问题排查与避坑技巧5.1 图形失真/抖动/无光/错乱分别怎么查先把现象和可能原因整理成一张表方便排查现象可能原因排查方法扫描图形整体偏移/镜像数据编码错误XY2-100 一般用偏移码而非补码检查 0x8000 是否对应模拟零点图形边角抖动、飞点差分线质量差、终端电阻缺失、时钟抖动换双绞屏蔽线加 120Ω 电阻示波器看波形无光/振镜不动帧头同步头错误、CHANNEL 接反、供电异常先用仿真确认时序再量差分信号数据偶发错乱SYNC 信号未对齐、状态机跳转过早观察内部 bit_cnt 和 xy2_sync 的相对位置图形有额外回画现象发送器在 IDLE 又检测到 start重复发送旧数据上位机用 busy 做握手start 脉冲宽度严格限制5.2 我踩过的几个坑第一个坑是数据编码。很多文章不会强调 XY2-100 的 16 位数据是偏移二进制码0x8000 对应模拟量零点0x7FFF 对应负最大0x8001 等高于 0x8000 的是正方向。如果直接把带符号整数送出去扫描图形会整体偏到一边或者反转。正确做法是上位机先做一次偏移转换或者在 FPGA 内部把符号数加 0x8000。第二个坑是同步头和 SYNC 信号的区别。SYNC 是物理线上的一根独立同步线它每帧开始时输出一个脉冲。同步头是数据帧内部约定的两位固定高电平。两者都要正确驱动板才能锁定帧边界。有的驱动板要求 SYNC 在整个帧期间保持高有的只要求在起始位置有脉冲一定要看规格书。第三个坑是上电时序。FPGA 刚上电时内部状态机未初始化xy2_data 和 xy2_sync 的电平不稳定。如果这时候直接发坐标振镜可能会乱飞。我的习惯是加一个软件复位或者在驱动板使能之前至少等待几十毫秒确保输出稳定后再发送。第四个坑是关于 Z 轴复用的。市面上很多振镜驱动板虽然标称支持三个通道但 XY2-100 标准里 CHANNEL 信号只有一个没办法同时表达 X/Y/Z 三个通道。Z 轴要么复用 X 通道要么依赖板卡自己的扩展协议要么通过额外的模拟接口。如果你在项目里踩了 Z 轴数据不响应的坑大概率是板卡对 Z 通道的映射方式和你的代码不一致。6. 最后再分享一点实战体会这个方案我从最早的单片机 SPI 模拟到后来彻底改到 FPGA Verilog前后迭代了三版。最大的体会是XY2-100 协议本身很简单真正决定项目成败的是时序细节和电气设计。一开始我图省事用普通杜邦线飞线结果波形一片毛刺振镜在低增益下都会抖后来换成双绞屏蔽线加终端电阻同样的代码扫描直线立刻稳了。这说明协议栈再对物理层不行上层全部白搭。如果你也是刚开始做振镜控制我的建议是分三步走先花一天时间把协议帧结构和 SYNC/CHANNEL 的时序关系彻底吃透再用 Verilog 写完发送器后先在仿真里把每帧 20 位掰开揉碎看一遍最后再接真实振镜驱动板用示波器验证波形。不要一上来就想着连振镜跑图形那样出问题你根本分不清是发送器错了、驱动板配置错了还是电气连接错了。代码层面我最终用的发送器比上面给出的版本多了一层 FIFO用来承接上位机连续写入的坐标流避免每一帧之间产生间隙。FPGA 的优势就在这里同样的状态机逻辑加个 FIFO、加个滑动窗口滤波、加个速度前瞻模块都不用动主干整个系统就进阶了。后面如果你做到动态聚焦、多振镜协同或者更高速的控制再回头来看这套 XY2-100 的 Verilog 实现会发现它像地基一样支撑着上层所有功能值得多花点时间把它写稳。
返回列表