
1. 振镜控制里那个绕不开的XY2-100到底难在哪如果你拆开过一台工业激光打标机、激光焊接机或者高速激光雕刻设备大概率会在振镜驱动板旁边看到一根排线上面跑的就是XY2-100协议。这个协议本身并不复杂但真正动手用FPGA或者MCU去实现它的时候很多人会卡在几个地方时钟相位怎么对齐、20位数据怎么拼帧、SYNC信号和CLK的时序余量留多少、校验位到底怎么算。我前后做过三四个振镜控制相关的项目从最早用STM32硬扛到后来换成FPGA做并行控制踩过的坑足够写一篇长文了。XY2-100是德国一家做振镜的厂商定义的同步串行协议后来成了行业事实标准。它的核心任务很简单把X轴和Y轴的位置指令以20位分辨率、每帧16个时钟周期的形式同步送到振镜驱动器。但“简单”和“好做”是两回事。振镜对时序非常敏感位置更新率通常要求达到10kHz到100kHz抖动稍微大一点打出来的线条就会毛边焊接的焊缝就会不均匀。这篇文章面向的是已经有一定FPGA或MCU开发基础、正在做振镜控制或者准备做振镜控制的工程师。我会从协议本身的信号层拆起然后分别讲FPGA和MCU两种实现路径的取舍再给出一套经过实际验证的Verilog实现思路和MCU模拟方案最后把调试过程中最容易翻车的几个点单独拎出来说。关键词里的XY2-100、FPGA、MCU、协议解析、振镜控制每一个都会落到具体的代码和波形上不玩虚的。2. XY2-100的信号层拆解别被“同步串行”四个字骗了2.1 三根信号线背后的时序契约XY2-100物理层用三根差分信号对CLOCK、SYNC、DATA。注意是差分不是单端。很多新手第一次画板子的时候直接拿FPGA的3.3V单端IO去接振镜驱动器结果发现根本动不了或者偶尔能动但丢帧严重。振镜驱动器那头通常是RS-422电平差分传输抗干扰能力强而且时钟速率虽然不高典型2MHz但在工业环境里电磁噪声大单端信号走几厘米就可能被干扰得不成样子。协议规定每个时钟周期传输1位数据每帧固定20个时钟周期。等等这里有个容易混淆的地方XY2-100的帧结构是20位数据位加上必要的同步头但实际时钟周期数是20个。我见过有人按16个周期去写状态机结果数据永远对不上。正确的帧格式是这样的前3个时钟周期是控制位包含一个起始位和两个控制位接着16个时钟周期是X轴和Y轴的坐标数据最后1个时钟周期是校验位。加起来正好20个CLK周期。SYNC信号在每帧开始时拉高一个CLK周期用来给驱动器做帧同步。DATA在CLK的上升沿变化驱动器在CLK的下降沿采样。这个相位关系是固定的不能反。我早期用MCU的SPI去模拟的时候就是栽在相位上——SPI的默认模式是上升沿采样跟XY2-100正好相反后来改成下降沿采样才通。2.2 20位数据里藏着的坐标编码逻辑XY2-100的坐标数据是20位有符号数但它的编码方式不是普通的二进制补码。实际传输的20位里低16位是位置数据高4位是状态和控制信息。具体来说X轴和Y轴各占一个16位的字段但它们在20位帧里的排列顺序是交叉的不是简单的X在前Y在后。我拿一个实际例子来说明。假设你要让振镜走到X轴的正中间位置对应的16位数据是0x8000。在XY2-100帧里这个值会被拆成两部分低8位和高8位分别放在不同的时钟周期里。而且X轴和Y轴的数据是交替传输的不是连续传完X再传Y。这种交错排列的设计是为了减少传输延迟——驱动器可以在收到第一个轴的数据后就开始准备不用等整个帧结束。校验位是前19位的奇偶校验。注意是奇校验还是偶校验不同厂商的驱动器可能不一样。我遇到过用偶校验的也遇到过用奇校验的最稳妥的办法是查你手上那台振镜的 datasheet。如果 datasheet 找不到就两种都试一下能正常出光的那个就是对的。2.3 为什么时钟频率不是随便定的XY2-100的典型时钟频率是2MHz但这不是硬性规定。协议允许的时钟范围大概在1MHz到3MHz之间。频率太低帧率上不去振镜响应会迟钝频率太高差分线的信号完整性容易出问题尤其是排线较长的时候。我一般会把时钟定在2MHz这样每帧20个周期就是10微秒帧率100kHz。对于大多数打标和焊接应用100kHz的更新率已经绰绰有余。但如果你做的是高速飞行打标可能需要把时钟提到2.5MHz甚至3MHz这时候就要注意FPGA的IO时序约束和PCB走线了。差分对要等长阻抗控制在100欧姆左右否则眼图会闭合。3. FPGA实现用状态机把20个周期安排得明明白白3.1 为什么我最终选了FPGA而不是MCU最早做振镜控制的时候我用的是STM32F4系列靠SPI加定时器去模拟XY2-100时序。单轴控制还能凑合但一旦要同时控制两轴甚至三轴MCU的中断响应和SPI速率就成了瓶颈。STM32的SPI最高也就几十MHz但问题不在速率在于抖动。MCU的中断延迟受其他任务影响有时候几微秒的抖动振镜那边就能看出来。换成FPGA之后时序完全由硬件逻辑决定抖动可以做到纳秒级。而且FPGA可以并行处理多轴每个轴一个独立的状态机互不干扰。对于需要高同步精度的应用比如三维动态聚焦或者多振镜协同FPGA几乎是唯一的选择。当然FPGA不是没有代价。开发周期比MCU长调试工具链更复杂而且成本更高。如果你的应用对帧率要求不高比如低于10kHzMCU方案完全够用没必要上FPGA。我后面也会讲MCU的实现方案供不同场景参考。3.2 一个经过实测的Verilog状态机框架下面这个状态机是我在多个项目里反复打磨过的核心思路是用一个20位的移位寄存器来拼帧用一个5位计数器来跟踪时钟周期。代码基于Verilog-2001可以在Xilinx和Intel的FPGA上综合。module xy2_100_tx ( input wire clk, // 系统时钟比如50MHz input wire rst_n, input wire [15:0] x_pos, // X轴位置 input wire [15:0] y_pos, // Y轴位置 input wire start, // 启动一帧传输 output reg xy_clk, // XY2-100时钟 output reg xy_sync, // XY2-100同步 output reg xy_data // XY2-100数据 ); // 时钟分频生成2MHz的xy_clk reg [4:0] clk_div; wire xy_clk_en (clk_div 5d24); // 50MHz / 25 2MHz always (posedge clk or negedge rst_n) begin if (!rst_n) clk_div 5d0; else if (clk_div 5d24) clk_div 5d0; else clk_div clk_div 1b1; end // 状态机定义 localparam IDLE 2d0; localparam FRAME 2d1; localparam DONE 2d2; reg [1:0] state; reg [4:0] bit_cnt; reg [19:0] shift_reg; // 拼帧根据XY2-100格式组装20位数据 wire [19:0] frame_data { 1b0, // 起始位 2b00, // 控制位 x_pos[15:8], // X轴高8位 y_pos[15:8], // Y轴高8位 x_pos[7:0], // X轴低8位 y_pos[7:0], // Y轴低8位 1b0 // 校验位占位实际需要计算 }; always (posedge clk or negedge rst_n) begin if (!rst_n) begin state IDLE; bit_cnt 5d0; shift_reg 20d0; xy_clk 1b0; xy_sync 1b0; xy_data 1b0; end else begin case (state) IDLE: begin xy_clk 1b0; xy_sync 1b0; if (start) begin shift_reg frame_data; state FRAME; bit_cnt 5d0; end end FRAME: begin if (xy_clk_en) begin if (bit_cnt 5d0) begin xy_sync 1b1; // 帧起始同步 end else begin xy_sync 1b0; end xy_clk ~xy_clk; if (xy_clk 1b0) begin // 上升沿更新数据 xy_data shift_reg[19]; shift_reg {shift_reg[18:0], 1b0}; end if (bit_cnt 5d19 xy_clk 1b1) begin state DONE; end if (xy_clk 1b1) begin bit_cnt bit_cnt 1b1; end end end DONE: begin xy_clk 1b0; xy_sync 1b0; state IDLE; end endcase end end endmodule这段代码的核心是frame_data的拼接顺序。注意X轴和Y轴的数据是交错放置的先高8位后低8位而且X和Y是交替的。这个顺序如果搞错了振镜会走到完全错误的位置甚至可能因为超出范围而触发保护。校验位的计算我留了占位实际使用的时候需要根据前19位算出奇偶校验值然后填到最低位。校验逻辑很简单就是一个异或树wire parity ^frame_data[19:1]; // 奇校验 // 如果是偶校验用 ~^frame_data[19:1]3.3 时序约束和IO标准的选择FPGA实现XY2-100时序约束是绕不过去的。你需要告诉综合工具xy_clk和xy_data之间的建立保持时间关系。在Xilinx的Vivado里可以用set_output_delay来约束set_output_delay -clock [get_clocks xy_clk] -max 5.0 [get_ports xy_data] set_output_delay -clock [get_clocks xy_clk] -min 1.0 [get_ports xy_data]具体数值要根据振镜驱动器的datasheet来定。一般来说数据在时钟下降沿后至少要保持2ns的稳定时间。IO标准方面如果你的FPGA板子上有RS-422收发器那就直接接差分对。如果没有可以用FPGA的LVDS IO加上外部电阻网络来模拟但我不推荐这么做信号质量很难保证。最稳妥的方案是加一片SN65HVD或者MAX490之类的RS-422收发芯片成本不高但能省掉很多调试时间。4. MCU方案没有差分引脚的时候怎么把XY2-100跑起来4.1 用普通GPIO模拟时序的可行性边界不是所有项目都值得上FPGA。如果你只是做一台低速打标机或者预算有限MCU方案完全可行。但这里有个前提你的MCU得能输出稳定的2MHz时钟而且抖动要控制在可接受范围内。我用STM32F407试过纯GPIO模拟主频168MHz用定时器触发GPIO翻转。实测下来单轴控制帧率可以做到50kHz左右抖动在几十纳秒量级。对于大多数打标应用这个性能足够了。但如果你用中断去驱动抖动会大很多因为中断响应受其他优先级任务影响。所以我的建议是能用DMA就用DMA能用定时器硬件触发就用硬件触发尽量别在中断里做时序敏感的操作。还有一个坑很多MCU的GPIO翻转速度不够快。STM32的GPIO在168MHz下翻转一次大概需要几个纳秒问题不大。但有些低端MCU比如某些51核或者低主频的ARM Cortex-M0GPIO翻转延迟可能达到几十纳秒这时候2MHz的时钟就有点勉强了。选型的时候一定要看datasheet里的GPIO toggle rate。4.2 用SPI外设模拟XY2-100的取巧做法如果你的MCU有SPI外设而且SPI的时钟极性和相位可以配置那就可以用SPI来模拟XY2-100省掉手动翻转GPIO的麻烦。关键是把SPI配置成下降沿采样、空闲时钟为低。以STM32为例SPI_HandleTypeDef hspi; hspi.Instance SPI1; hspi.Init.Mode SPI_MODE_MASTER; hspi.Init.Direction SPI_DIRECTION_1LINE; // 只用MOSI hspi.Init.DataSize SPI_DATASIZE_8BIT; hspi.Init.CLKPolarity SPI_POLARITY_LOW; hspi.Init.CLKPhase SPI_PHASE_2EDGE; // 下降沿采样 hspi.Init.BaudRatePrescaler SPI_BAUDRATEPRESCALER_16; // 根据主频调整 hspi.Init.FirstBit SPI_FIRSTBIT_MSB;然后每帧发3个字节24位其中前20位是有效数据后4位补零。SYNC信号可以用一个GPIO在帧开始时拉高一个时钟周期。注意SPI的NSS信号不能直接当SYNC用因为NSS的时序跟XY2-100的SYNC不一样。你得手动控制一个GPIO来做SYNC。这种做法的好处是时序由SPI硬件保证抖动很小。坏处是SPI的时钟频率可能不是正好2MHz需要根据主频和分频系数去凑。比如STM32F407的APB2时钟是84MHz分频16得到5.25MHz分频32得到2.625MHz都跟2MHz有偏差。振镜驱动器一般能容忍±10%的时钟偏差所以2.625MHz也能用但如果你追求精确还是得用定时器方案。4.3 没有差分引脚时的电平转换电路MCU的GPIO通常是3.3V单端而振镜驱动器要的是RS-422差分。这时候你需要一个电平转换电路。最简单的方案是用一片MAX490或者SN65HVD11把单端信号转成差分。电路很简单MCU的GPIO接收发器的DI引脚收发器的A和B输出接振镜。但这里有个细节RS-422收发器的使能引脚要处理好。如果你只用发送功能把RE和DE接固定电平就行。DE接高电平使能发送RE接高电平禁用接收。有些收发器是DE和RE复用一个引脚那就更简单了。还有一个常见问题MCU的GPIO驱动能力不够导致差分信号的上升沿变缓。这时候可以在GPIO和收发器之间加一个缓冲器比如74HC125或者74LVC1G125。我实测过不加缓冲器的时候2MHz信号的上升沿大概10ns加了之后能压到3ns以内眼图明显更好看。5. 调试现场那些波形上看不出来但实际会翻车的点5.1 第一帧数据总是错的这个问题我遇到过至少三次。现象是上电后第一帧振镜会抖一下或者走到一个莫名其妙的位置从第二帧开始就正常了。原因通常是FPGA或MCU的移位寄存器在上电时初始状态不确定第一帧发出去的数据是随机的。解决办法很简单在状态机里加一个复位后的预加载或者在IDLE状态时就把shift_reg清零。但更隐蔽的一种情况是振镜驱动器在上电后需要一段时间初始化如果你在它还没准备好的时候就发数据它会把第一帧当成配置帧而不是位置帧。这时候需要在软件里加一个延时等驱动器ready之后再开始发帧。5.2 SYNC信号的宽度到底要多少XY2-100协议里SYNC信号是帧起始标志但协议文档没有明确规定SYNC要拉高多少个时钟周期。我见过有的驱动器要求SYNC至少一个完整CLK周期有的要求半个周期就够了。最保险的做法是让SYNC跟第一个CLK周期等宽也就是在bit_cnt为0的时候拉高bit_cnt为1的时候拉低。但这里有个时序陷阱如果你的SYNC和CLK是同时变化的驱动器可能会在CLK的边沿采到SYNC的中间态。所以SYNC的变化应该稍微提前于CLK的边沿。我在FPGA里一般会让SYNC在CLK的下降沿变化这样驱动器在下一个上升沿采样时SYNC已经稳定了。5.3 长排线导致的信号反射工业现场经常需要把振镜驱动器放在离控制板几米远的地方排线一长信号反射就来了。表现是振镜偶尔会丢帧或者打出来的图形有随机偏移。用示波器看波形会发现CLK的上升沿有过冲和振铃。解决这个问题有几个办法第一在差分对的末端加匹配电阻通常是100欧姆第二降低时钟频率给信号更长的建立时间第三用屏蔽双绞线代替普通排线。我一般会先试第一种如果还不行就降频。降频是最简单粗暴但有效的办法从2MHz降到1.5MHz大多数反射问题都能缓解。5.4 校验位算错导致的间歇性故障校验位算错不会导致每帧都出错而是间歇性的。因为如果数据本身变化不大校验位可能碰巧是对的。这种故障最难查因为现象不稳定。我的建议是在FPGA里加一个校验错误计数器每次发送前先算一遍校验如果跟预期不符就报警。在MCU里也一样写一个校验函数发送前调用一次。还有一个容易忽略的点不同厂商对校验位的定义可能不同。有的用奇校验有的用偶校验还有的用固定值。如果你换了一台振镜发现原来的代码突然不好使了先查校验位。6. 从单轴到多轴FPGA并行控制的架构演进6.1 多轴同步的时钟树设计当你需要控制两个以上的振镜时同步就成了核心问题。如果每个轴用独立的时钟分频器由于分频器初始相位不同各轴的CLK之间会有随机偏差。对于需要精确同步的应用比如三维扫描或者多工位焊接这个偏差是不能接受的。我的做法是用一个全局的时钟使能信号所有轴的状态机都基于同一个使能信号工作。这样各轴的CLK边沿是对齐的偏差只取决于FPGA的布线延迟通常在几百皮秒以内。具体实现上用一个全局计数器生成2MHz的使能脉冲然后每个轴的状态机在使能有效时推进。reg [4:0] global_cnt; wire global_en (global_cnt 5d24); always (posedge clk) global_cnt (global_cnt 5d24) ? 5d0 : global_cnt 1b1;然后每个轴的FRAME状态里用global_en代替原来的xy_clk_en。这样所有轴的时序完全一致。6.2 数据缓冲和帧率匹配多轴系统里上位机送过来的位置数据速率可能跟XY2-100的帧率不匹配。比如上位机每100微秒更新一次数据而XY2-100每10微秒发一帧。这时候需要一个FIFO来缓冲数据否则要么丢帧要么重复发送旧数据。我在FPGA里一般会例化一个异步FIFO写端口接上位机接口读端口接XY2-100发送状态机。FIFO的深度不用太大16到32就够了。关键是要处理好空标志和满标志避免读空或者写溢出。如果数据更新率低于帧率那就需要做数据保持。也就是说如果FIFO空了就继续发送上一帧的数据而不是发零。这个逻辑很简单在FIFO读空的时候保持shift_reg的值不变。7. 收尾几个我踩过之后才明白的经验XY2-100这个协议看文档半天就能看懂但真正做稳定没有两三个项目的积累是很难的。我最大的体会是时序余量永远要留够。你以为2MHz很慢但在工业环境里各种干扰和线缆损耗会让它变得很脆弱。我现在的习惯是设计阶段按2.5MHz去约束实际跑2MHz这样即使有意外也有足够的余量。另一个经验是不要迷信示波器上的波形。有时候波形看起来很漂亮但振镜就是偶尔抽风。这时候要去看振镜驱动器的状态指示灯或者用厂商提供的调试软件读错误寄存器。很多问题在模拟层面是看不出来的只有数字层面的错误计数才能告诉你真相。最后如果你正在选型我的建议是单轴低速应用MCU加SPI方案足够多轴高速应用直接上FPGA别犹豫。中间地带的项目可以先用MCU验证逻辑再迁移到FPGA这样开发效率最高。