ARTICLE DETAIL

资讯详情

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

SSI接口协议与FPGA开发:从时序要求到数据解析

SSI接口协议与FPGA开发:从时序要求到数据解析 简介面向FPGA开发者的SSI接口协议实现文档以SSI3为例讲解如何与高精度绝对编码器通信适合有一定Verilog基础、需要掌握串行接口时序设计的读者。资源为单个PDF文件大小184KB已有1279人学习浏览。文档从SSI接口的时序图切入详细说明100kHz至2MHz时钟频率选择、数据发送阶段与更新等待阶段的时间参数并给出完整Verilog代码框架涵盖采样时钟生成、数据打拍防亚稳态处理、16bit数据保存输出等关键逻辑还附有上板验证提示。尤其对完整工作周期需大于37.5微秒、实际取40微秒的原因做了说明能帮助读者理解时序余量与通信可靠性的关系。整体把时序计算、代码实现与验证思路整合在同一文档中适合快速搭建SSI接口的FPGA模块也适合作为排查同步串行通信问题的参考。1. SSI接口协议与FPGA开发从时序要求到数据解析SSISynchronous Serial Interface同步串行接口在工业自动化里非常常见绝对值编码器、角度传感器、拉线位移传感器、扭矩传感器都在用它输出位置数据。它的物理层只有两根线一条时钟线CLK、一条数据线DATA主机提供时钟从机传感器在时钟沿驱动数据。不少工程师看到SSI第一反应是“这和SPI差不多直接拿SPI外设读不就行了吗”但实际动手后就会发现SPI的主机在帧结束时靠CS拉高来复位从机而SSI没有片选线——它靠“时钟停振超过某个阈值”来宣告一帧结束。这个区别决定了直接用MCU的SPI外设往往会在帧边界上漏一拍或者在时钟自由振荡时读到一帧冗余数据。FPGA做SSI主机的价值恰好在这里协议本身不复杂真正麻烦的是任意波特率、任意数据位宽、多传感器级联、以及精确的帧间间隙检测这些用逻辑实现比用处理器中断更可控。对嵌入式工程师来说在一颗FPGA上同时接多路SSI编码器、把解出来的位移数据通过AXI总线交给SoC或者软核CPU处理是一条很实用的路径。这篇就按我平时做FPGA工业接口的流程来讲先把帧时序拆开再写主机状态机然后处理多传感器级联和Gray码解码最后落到示波器和逻辑分析仪的实测方法上。2. SSI接口协议的帧结构拆解与FPGA参数设计2.1 SSI帧结构时钟停振才是帧结束信号先看SSI的空闲态。CLK在无数据传输时保持高电平DATA总线由上拉电阻或传感器内部的驱动器保持高阻或特定电平。主机开始通信时先把CLK拉低这个下降沿是通信起始标志传感器在接下来的时钟上升沿逐位输出数据。数据位从最高位开始每个时钟上升沿对应一位。当最后一位传完后主机停止翻转时钟CLK保持低电平维持一段“单稳态触发时间”文档里常写作monoflop time或frame time然后回到高电平空闲态。从机只有在时钟停振超过这个时间后才会认为一帧结束并在下一次时钟下降沿到来前准备好新数据。所以SSI一帧的时序边界是高电平空闲 → 下降沿启动 → 上升沿逐位移出数据 → 时钟停振在低电平保持足够长 → 回到高电平空闲。帧长度完全由“主机发出了多少个上升沿”决定而过早恢复时钟震荡会把上一帧数据末尾误读成下一帧起始。FPGA做主机时必须自己实现“发完数据后停时钟、数时间、再拉高”这套逻辑这比SPI多一个状态。2.2 波特率与时钟分频先算好SSI_CLK再写计数器SSI的时钟频率不是一个固定值不同厂家的编码器规定不同常见的是125kHz、250kHz、500kHz、1MHz、2MHz。一般来说数据线越长、抗干扰要求越高、传输距离越远时钟越低。我一般先查传感器手册里“CLK frequency”一栏再结合线缆长度决定。FPGA系统时钟通常在50MHz到200MHz无论是用PLL/MMCM还是普通的计数器分频核心是把SSI时钟的周期、占空比、采样点算清楚。如果直接用计数器对系统时钟分频产生SSI_CLK周期精度取决于系统时钟频率这个没问题。关键是有个隐含约束系统时钟需要能整除目标SSI时钟周期否则会出现相邻两个时钟周期长度不一致的现象最典型的表现就是示波器上看SSI_CLK边沿间隔不均匀编码器在这种时钟下输出的数据容易出现偶发跳变。以下是我常用的分频参数设计思路写成Verilog模块module ssi_clk_div #( parameter SYS_CLK_HZ 50_000_000, // FPGA系统时钟 parameter SSI_CLK_HZ 1_000_000 // 目标SSI时钟, 默认1MHz )( input wire clk, // 系统时钟 input wire rst_n, // 复位, 低有效 input wire clk_en, // 时钟输出使能 output wire ssi_clk // 生成的SSI时钟 ); localparam DIV_CNT (SYS_CLK_HZ / SSI_CLK_HZ) / 2 - 1; reg [15:0] cnt; reg clk_reg; always (posedge clk or negedge rst_n) begin if (!rst_n) cnt 16d0; else if (clk_en) begin if (cnt DIV_CNT) cnt 16d0; else cnt cnt 1b1; end else cnt 16d0; end always (posedge clk or negedge rst_n) begin if (!rst_n) clk_reg 1b1; // SSI空闲时CLK为高 else if (clk_en cnt DIV_CNT) clk_reg ~clk_reg; else if (!clk_en) clk_reg 1b1; // 停振时保持高电平 end assign ssi_clk clk_reg; endmodule这个逻辑里有一个细节我把clk_reg的复位值和停振值都设成了1b1。SSI协议要求时钟空闲必须为高这样第一个下降沿才具有“同步起始”的意义。如果复位置成低会在启动瞬间产生一个下降沿传感器可能把它当成数据帧的开始整体错一拍。另外DIV_CNT的算法是先算SSI时钟周期是系统时钟的多少倍再除以2得到半周期计数这样算出来的时钟占空比接近50%。分频计数器的位宽要留够如果系统时钟100MHz、SSI时钟125kHz半周期是400个系统时钟周期计数器位宽至少10位直接写16位更省心。2.3 SSI参数设计表位宽、帧间隙、采样窗口的取值参考FPGA实现SSI前我建议先在文档里把几个参数固定下来写成宏定义或者模块参数。下表是我在项目里常用的一组配置适用于大多数绝对值编码器。参数典型值说明数据位宽13 / 25 / 32单圈13位多圈25位更高精度用32位SSI_CLK频率125kHz ~ 2MHz线长小于3米用1MHz超过10米建议降到500kHz以下空闲时间≥ 20μs编码器手册里的monoflop time停振后需保持低电平这么久帧间隙≥ 25个SSI_CLK周期一帧结束到下一帧开始的等待时间采样点时钟上升沿后 15%~30% 处避开数据线边沿附近的亚稳态区最后一个“采样点”值得展开说。数据线上传感器在上升沿把数据位推出来但上升沿附近是数据切换的过渡区间如果主机也在这个沿采样很容易采到不稳定的电平。常见的正确做法是主机在上升沿之后延迟一点再采样或者在下降沿采样。因为数据在一个时钟周期内保持稳定下降沿采样正好落在数据稳定区域的中后段。这个“延迟多少”可以用系统时钟数出来相当于比上升沿晚几百纳秒能显著降低误码率。想想SPI协议里的CPHA相位选择SSI没有这种配置项所以主机必须自己决定采样相位这是我做SSI时最常遇到的一个坑。3. 用Verilog实现SSI主机状态机与数据移位接收3.1 状态怎么分IDLE、启动、移位、帧等待四个基本态SSI主机的工作流程可以拆成四个状态空闲等触发、拉低时钟启动、逐位发送时钟并采样数据、多等一段时间结束本帧。状态机不用设计得太复杂关键是每个状态里的计数器要独立清晰。设计思路是用一个内部使能信号控制SSI时钟的振荡在移位状态每产生一个上升沿就把DATA采样进移位寄存器同时发送时钟计数加1发完指定的位宽后关闭时钟使能但还要保持时钟低电平一段时间让传感器有时间识别“帧结束”然后再回到空闲。不少初学FPGA的工程师会在最后一步偷懒发完数据直接把时钟拉高从逻辑仿真上看数据没问题但到实测时会发现编码器下一帧的第一位数据偶尔会丢了就是因为没有给帧结束留出单稳态恢复时间。3.2 参数化SSI主机模块代码下面这段状态机是我在实际项目里会用到的完整结构直接作为采集模块的核心module ssi_master #( parameter DATA_WIDTH 25, // 单帧数据位宽 parameter SYS_CLK_DIV 25 // 系统时钟分频比, 控制SSI_CLK频率 )( input wire clk, input wire rst_n, input wire trig_start, // 启动一帧采集 output wire ssi_clk, // 输出到传感器的时钟 input wire ssi_data, // 传感器数据输入 output reg [DATA_WIDTH-1:0] frame_data,// 解析出的数据 output reg frame_valid // 一帧完成标志 ); localparam IDLE 3d0; localparam START 3d1; localparam SHIFT 3d2; localparam FRAME_WAIT 3d3; reg [2:0] state; reg [15:0] clk_cnt; reg [15:0] bit_cnt; reg [15:0] wait_cnt; reg [DATA_WIDTH-1:0] shift_reg; reg clk_en; reg clk_toggle; // 生成SSI时钟空闲高使能时翻转 always (posedge clk or negedge rst_n) begin if (!rst_n) begin clk_toggle 1b1; clk_cnt 16d0; end else if (clk_en) begin if (clk_cnt SYS_CLK_DIV - 1) begin clk_cnt 16d0; clk_toggle ~clk_toggle; end else begin clk_cnt clk_cnt 1b1; end end else begin clk_toggle 1b1; // 时钟停在高电平 clk_cnt 16d0; end end assign ssi_clk clk_toggle; always (posedge clk or negedge rst_n) begin if (!rst_n) begin state IDLE; shift_reg 0; bit_cnt 0; frame_valid 1b0; frame_data 0; wait_cnt 0; clk_en 1b0; end else begin frame_valid 1b0; case (state) IDLE: begin if (trig_start) begin clk_en 1b1; bit_cnt 0; state START; end end START: begin // 等待时钟第一次翻转到低电平, 同步启动 if (clk_toggle 1b0 clk_cnt SYS_CLK_DIV - 1) state SHIFT; end SHIFT: begin // 在时钟上升沿把SSI_DATA打进移位寄存器 if (clk_toggle 1b1 clk_cnt SYS_CLK_DIV - 1) begin shift_reg {shift_reg[DATA_WIDTH-2:0], ssi_data}; bit_cnt bit_cnt 1b1; end if (bit_cnt DATA_WIDTH - 1) state FRAME_WAIT; end FRAME_WAIT: begin clk_en 1b0; // 停振, 时钟回到高电平 if (wait_cnt 8d200) begin // 等200个系统时钟, 满足单稳态时间 frame_data shift_reg; frame_valid 1b1; state IDLE; end else begin wait_cnt wait_cnt 1b1; end end endcase end end endmodule3.3 逻辑说明与参数调优位宽、分频、等待时间怎么配合上面的代码有两个典型的“FPGA风格”处理点。第一个是采样时刻的判断条件我同时检查了clk_toggle 1b1和clk_cnt SYS_CLK_DIV - 1后者意味着半周期计数刚好到终点下一拍时钟就要翻转。在这个时刻采样实际上是采在了数据稳定期的后段不是上升沿的瞬间。第二个是FRAME_WAIT里的wait_cnt这里写的是硬编码200个系统时钟如果系统时钟频率不同这个值要调整。比如系统时钟50MHz200拍对应4μs有些传感器需要的monoflop time是20μs以上那就得加大到1000拍。更好的做法是把等待时间也做成参数用FRAME_GAP_CYCLES传入而不是写死在代码里。实际项目中这个模块的trig_start通常由一个定时器驱动比如每1ms触发一次采集对应1kHz的采样率。如果编码器是多圈的位宽到25位一帧25个时钟周期在1MHz SSI时钟下耗时25μs加上帧等待时间完全可以在1ms内完成一帧时间很宽裕。如果追求更高采样率可以把SSI时钟抬高到2MHz位宽仍为25位帧时间压缩到12.5μs左右配合流水线设计可达每秒上万次采样。这里还要提一个常见的错误用法有人会把clk_en和clk_toggle直接接出去当SSI时钟结果时钟上有毛刺。因为clk_toggle是在系统时钟上升沿更新的它本身就是一个干净的寄存器输出接到IO引脚没有问题。但如果你在组合逻辑里临时改变clk_en时钟输出就可能出现一个极窄的毛刺脉冲这对传感器来说等于多了一个数据时钟整个帧就废了。所以任何时候都不要在状态机里组合式地去改clk_en保存到寄存器再输出。4. 多SSI编码器级联与Gray码解码的FPGA处理方法4.1 级联模式多传感器共用一条时钟线工业设备里经常遇到这种情况一台设备上有好几个绝对值编码器比如机械臂的关节电机、龙门架的两轴位移传感器、圆盘刀库的旋转角度和换刀行程。给每个传感器单独接一组FPGA引脚当然可以但很多SSI编码器支持“级联”模式即把多个传感器串在一条时钟总线上帧长扩展为原来的N倍。级联的原理可以这样理解从机有一个数据输出脚和一个数据输入脚主机把第一个传感器的数据输出连到FPGA同时把自己的数据输入脚接到第二个传感器……串成链。当主机发出足够多的时钟脉冲时整条链上的传感器按顺序把自己的数据一位一位移出来。比如2个25位编码器级联FPGA侧看到的就是一帧50位的数据前25位是第一个传感器后25位是第二个传感器。用前面SSI主机模块实现级联改动非常小只要把参数上的数据位宽改成所有传感器位宽之和然后把最终的shift_reg拆分就行。代码层面的做法是localparam SENSOR_NUM 2; localparam SINGLE_WIDTH 25; localparam TOTAL_WIDTH SINGLE_WIDTH * SENSOR_NUM; // 实例化SSI主机, 位宽设为TOTAL_WIDTH ssi_master #( .DATA_WIDTH(TOTAL_WIDTH), .SYS_CLK_DIV(25) ) u_ssi_master ( .clk(clk), .rst_n(rst_n), .trig_start(trig_start), .ssi_clk(ssi_clk), .ssi_data(ssi_data_chain), .frame_data(frame_raw), .frame_valid(frame_valid) ); // 拆分每个传感器的数据 wire [SINGLE_WIDTH-1:0] sensor0_data frame_raw[TOTAL_WIDTH-1 : SINGLE_WIDTH]; wire [SINGLE_WIDTH-1:0] sensor1_data frame_raw[SINGLE_WIDTH-1 : 0];参数说明里要强调链上的传感器数量不是无限制的每个传感器的数据输出有驱动能力和级联延迟一般编码器手册会给出最大可级联数量常见的是2到4个。如果你接的线缆比较长建议降低SSI时钟频率比如从1MHz降到500kHz给数据在线缆上传播留出余量。这也是为什么FPGA实现的好处突出改一个SYS_CLK_DIV参数就能适配不同的传输环境不用重新画板子。4.2 Gray码转二进制FPGA里的异或链实现大部分绝对值编码器输出的不是二进制原码而是Gray码。Gray码的特性是相邻两个计数都只有一位变化这非常有利于光电编码器在低速转动时避免多位同时翻转带来的毛刺。但Gray码不能直接拿去算角度需要转换成普通的二进制数。Gray码转二进制的规则是最高位保持不变从次高位开始每一位等于前一位转换后的二进制位与当前Gray码位做异或。写成Verilog可以用一个生成器循环function [DATA_WIDTH-1:0] gray_to_bin; input [DATA_WIDTH-1:0] gray; integer i; begin gray_to_bin[DATA_WIDTH-1] gray[DATA_WIDTH-1]; for (i DATA_WIDTH-2; i 0; i i - 1) begin gray_to_bin[i] gray_to_bin[i1] ^ gray[i]; end end endfunction这个函数的逻辑是一个逐位异或链综合后生成一串级联的XOR门从高位到低位逐级向下传播。问题在于如果数据位宽很大比如32位这条组合逻辑链会变得很长在较高系统时钟下可能成为时序瓶颈影响frame_valid之后到数据稳定输出之间的延迟。我一般会在函数外面再加一拍寄存器把转换结果打一拍输出。代价是多一个时钟周期的延迟但换来了干净的时序。对于编码器采样这种对延迟不敏感的应用多一拍完全无所谓。实际使用中解码结果出来以后还要做零位校准。绝对值编码器的零位是机械安装时确定的但FPGA上电后读到的第一个值不一定对应机械原点所以常规处理是在设备初始化时记录一个基准值后续每个采样值都减去这个基准值再做角度换算或者位移换算。这一部分FPGA里就是个减法器如果怕基准值覆盖可以在软核里维护。5. 用示波器和仿真双重验证SSI时序的正确性最后一章分享两个我每次做SSI实现都会做的验证步骤一个是ModelSim仿真波形核对一个是示波器实测时的检查顺序。仿真主要确认FPGA侧的逻辑是否正确实测主要确认和传感器之间是否匹配。5.1 仿真验证重点看时钟停振后的帧间隙写SSI主机的仿真testbench时我会在激励里加一个虚拟编码器模型它对SSI_CLK的上升沿计数每16个时钟输出一个预先定义的测试数据序列。仿真跑起来以后不用去看数据对不对先看两个关键点。第一个是ssi_clk的第一个下降沿之后数据线是否有一段稳定的准备时间第二个是最后一拍时钟之后ssi_clk保持低电平的时间是否满足wait_cnt参数所设定的值然后一次性拉高。如果这个间隙太短传感器在下一帧启动时可能还停留在上一帧的复位过程里。仿真里还可以故意把数据赋成全1或全0这种极端序列最容易暴露移位寄存器首尾错位的问题。比如25位数据最后一位如果没采进去frame_data的最高位实际是上一帧的最低位这种错位在普通随机数据下不容易看出来但全0全1序列下非常明显。数字电路调试里全1全0是性价比最高的测试向量之一做接口调试时务必要跑一遍。5.2 示波器实测检查顺序从空闲电平查到停止位时间示波器是验证SSI协议时序最直接的工具。我建议按下面的顺序过一遍不要上来就去抓数据。先看空闲状态下的CLK和DATA电平CLK必须是高电平不能是悬空或低电平DATA线应该处于释放状态由传感器的驱动器决定。如果CLK空闲时是低检查FPGA引脚配置和上下拉大概率是引脚默认电平设置问题。然后抓一次完整的一帧数据传输观察CLK从高拉低的第一个下降沿确认这个下降沿之前数据线已经稳定没有毛刺。接下来数一下上升沿的个数是否和数据位宽一致。如果编码器是25位一帧应该有25个上升沿多一个或少一个都说明状态机的发送计数有问题直接抓波形就能发现。最后看最后一拍时钟之后的低电平时间用示波器的光标量出这个时间长度和传感器手册上写的monoflop time对照。如果这个时间不够会把下一帧的启动下降沿误判为上一帧的继续最典型的现象就是编码器数据每隔一段时间跳一次大数。5.3 帧间隙调整技巧把阈值做成可调参数调试阶段有一个技巧值得养成习惯把帧等待时间、采样点在顶层模块里做成寄存器可配而不是写死在代码里。这样在测试时不需要每次改代码重新综合布局布线而是通过UART或者JTAG写配置寄存器就能尝试不同的参数组合。比如一个14位编码器工作不正常数据显示偶尔跳变我可能会先降低SSI时钟从1MHz到500kHz看现象是否消失如果消失了基本就是线缆质量问题或采样点太靠近边沿再微调采样点位置。逻辑分析仪在这种场景下比示波器好用它能连续抓几千帧数据统计出跳变概率方便对比不同参数的实际表现。还有一个易踩的坑有些传感器的数据输出在时钟停止后不是立刻回到高阻态而是保持最后一拍的电平一段时间。如果FPGA输入引脚没有配置成弱上拉在时钟不活跃期间数据输入引脚会浮空产生不定电平。解决办法是把SSI数据输入引脚设为内部上拉或者干脆在PCB上加一个10kΩ上拉电阻。这跟I2C的开漏总线不太一样SSI的数据线在传输期间是推挽输出的但停止期间确实存在一段高阻窗口这个细节在解码时不会出错但在做多级联时可能导致链条末端的传感器输入电平不定表现为某一路数据偶发抖动。检查引脚配置和外部上拉是SSI接口排查时的第一项硬件检查。本文还有配套的精品资源点击获取
返回列表