ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

FPGA实现BISS-C接收模块:双向线控制、超时帧同步与CRC校验

FPGA实现BISS-C接收模块:双向线控制、超时帧同步与CRC校验

简介:这是一份面向FPGA开发者与伺服/编码器接口方向学习者的BiSS-C协议接收模块工程,针对光栅位置数据采集中主从同步串行通信的接收端实现需求。BiSS-C单向模式由主接口控制时序与传输速率,光栅作为从接口回传位置数据,该模块即完成这一接收链路的逻辑设计,并配套仿真代码,可在ModelSim环境下完成功能验证。压缩包共58个文件,约358KB,以Verilog源码、ISE工程文件、仿真脚本与日志、综合报告及网页报告为主,另含少量配置文件与可执行文件,覆盖从工程搭建、综合到仿真的完整流程。目前已有2405人学习下载。读者可从中获得可直接复用的接收模块参考实现、仿真测试平台与波形数据,以及综合与仿真过程中的日志和报告,便于理解BiSS-C时序解析、状态机设计与验证方法,适合具备一定FPGA基础、需要快速搭建接口验证环境的工程师参考。

1. 为什么 BISS-C 的“握手”比你想的更难:从两根线说起

BISS-C 只有两根差分线,却要同时完成位置数据回传和寄存器读写,这件事本身就反直觉。很多开发者第一次接触它时,会以为它跟 SPI 差不多——主机给时钟,从机吐数据,接上就能跑。但真正把 FPGA 接到编码器上电那一刻,才发现事情没那么简单:数据线是双向的,方向切换的时机由主机严格掌控,早一个时钟周期或者晚一个时钟周期,采到的就是错位的数据。更麻烦的是,BISS-C 没有片选信号,帧的边界完全靠“超时”来判定,这意味着接收模块必须自己维护一套状态机来识别帧头、帧尾和空闲间隔。这份 FPGA 实现的 BISS-C 接收模块,核心价值就在于把“双向线方向控制 + 超时帧同步 + CRC 校验”这三件事用纯硬件逻辑串起来,而不是靠软件轮询去凑。搞懂它的底层机制,你才能明白为什么有些实现跑 10MHz 没问题,一上 20MHz 就丢帧——问题往往不在时钟频率本身,而在状态机对超时窗口的容忍度上。

2. BISS-C 协议帧结构:为什么数据线要“先听后说”

2.1 帧的物理层:一对差分线如何承载双向通信

BISS-C 的物理层只有两对差分信号:一对时钟(MA),一对数据(SLO)。时钟永远由主机(FPGA)发出,从机(编码器)不产生时钟。数据线则是双向的,但同一时刻只有一个方向有效。主机发时钟的同时,从机在时钟上升沿把数据推出来;主机需要写寄存器时,也是在时钟沿把数据推给从机。这种“时钟单向、数据双向”的设计,本质上是用时钟来同步双向数据流的相位,避免双方各自计时产生漂移。

常见的实现思路是:在 FPGA 侧用一个三态缓冲器控制数据线方向,方向切换点选在时钟的特定边沿之后。但 BISS-C 规范里并没有明确说“必须在第几个时钟沿切换方向”,它只规定了从机在收到一定数量的时钟后开始驱动数据线。这就给接收模块留了一个设计窗口:你可以在时钟下降沿切换方向,也可以在上升沿切换,但必须保证从机开始驱动之前,FPGA 已经释放了数据线。如果释放晚了,就会出现总线冲突——两边同时驱动,差分电平被拉到一个中间值,接收端采到的就是随机数据。

2.2 帧的时间结构:超时判定为什么比片选更可靠

BISS-C 的一帧由三部分组成:主机发出的时钟串、从机回传的数据位、以及帧与帧之间的空闲间隔。空闲间隔没有时钟,数据线保持高电平(或由从机释放)。接收模块判断一帧结束的依据不是“收到了固定数量的位”,而是“数据线在多少个时钟周期内没有翻转”。这个超时窗口通常设为 10 到 20 个时钟周期,具体值取决于编码器的规格书。

为什么不用片选?因为 BISS-C 的连线只有四根(时钟差分对 + 数据差分对),没有额外的片选线。如果要用片选,就得再加一根线,这会增加连接器的引脚数和线缆成本。用超时来判定帧边界,相当于把“帧同步”这件事从物理层转移到了协议层,代价是接收模块必须自己维护一个超时计数器。这个计数器的位宽和时钟频率直接相关:假设时钟 10MHz,超时窗口 20 个周期,计数器只需要 5 位;但如果时钟降到 1MHz,同样的超时时间就需要 50 个周期,计数器要 6 位。很多实现翻车的地方就在这里——计数器位宽不够,高频时溢出,低频时又等太久。

2.3 数据位的组织:为什么 MSB 先出且没有起始位

BISS-C 的数据帧没有起始位,也没有停止位。从机在收到第一个时钟沿之后,直接把最高位(MSB)推到数据线上,之后每个时钟沿推一位,直到所有位发完。接收模块怎么知道哪一位是第一位?靠的是“帧开始”的判定——当数据线从空闲高电平被拉低(或者从机开始驱动)的那一刻,就是帧的起点。

这种设计的好处是效率高:没有额外的起始/停止位开销,每一帧的每一位都是有效数据。坏处是接收模块必须精确对齐时钟和数据。如果 FPGA 在数据线还没稳定的时候就采样,采到的就是亚稳态。常见的做法是用时钟的下降沿去采样数据线的上升沿,或者用 oversampling 的方式,在时钟周期中间采样。但 BISS-C 的时钟频率可以到 10MHz 以上,oversampling 需要更高的内部时钟,这会增加 FPGA 的资源消耗。所以大多数实现还是选择用时钟边沿直接采样,靠约束时序来保证建立保持时间。

// 数据线方向控制与采样逻辑(简化示意) // 时钟上升沿:从机驱动数据线,FPGA 采样 // 时钟下降沿:FPGA 可以切换方向(如果需要写寄存器) always @(posedge clk_fast) begin if (frame_active) begin // 在时钟上升沿后延迟半个周期采样,避开跳变沿 if (clk_ma_rising_detected) begin data_sample <= slo_data_in; // 采样从机回传的数据 end end end // 方向控制:默认输入,需要写寄存器时切换为输出 assign slo_data_out = (write_mode) ? reg_data : 1'bz; assign slo_data_in = slo_data_p; // 差分接收后的单端信号

这段逻辑的关键在于clk_ma_rising_detected这个信号——它不是简单的时钟上升沿,而是经过同步和边沿检测后的脉冲。因为 MA 时钟来自 FPGA 内部,但 SLO 数据来自外部编码器,两者之间存在传播延迟。如果直接用内部时钟去采样,可能采到数据跳变的瞬间。所以需要把 MA 时钟也引到输入引脚上,用同一个全局时钟去采样 MA 和 SLO,再在数字域里做边沿对齐。参数clk_fast的频率至少是 MA 时钟的 4 倍,才能保证边沿检测的精度。

3. 接收状态机:超时窗口、位计数与 CRC 的三角关系

3.1 状态划分:为什么需要“等待超时”这个独立状态

一个典型的 BISS-C 接收状态机至少包含四个状态:IDLE(空闲)、RECEIVING(接收中)、CRC_CHECK(校验)、DONE(完成)。但很多实现会漏掉一个关键状态:WAIT_TIMEOUT(等待超时)。这个状态的作用是:在接收完所有数据位之后,不立即判定帧结束,而是继续等待数据线保持空闲达到预设的超时周期数,才确认这一帧真正结束。

为什么需要这个状态?因为 BISS-C 允许主机在帧与帧之间插入可变长度的空闲间隔。如果接收模块在收完最后一位就立刻回到 IDLE,那么当下一帧的时钟很快到来时,状态机可能还没来得及复位,导致帧同步丢失。WAIT_TIMEOUT 状态相当于一个“缓冲期”,它让状态机在确认总线空闲之后才复位,避免因时钟抖动或编码器响应延迟导致的误判。

// 状态机核心片段:超时计数与状态转移 localparam IDLE = 2'b00; localparam RECEIVING = 2'b01; localparam WAIT_TIMEOUT = 2'b10; localparam DONE = 2'b11; reg [1:0] state, next_state; reg [4:0] timeout_cnt; // 5位超时计数器,最大31个时钟周期 always @(posedge clk_fast) begin case (state) IDLE: begin if (slo_data_in == 1'b0) begin // 数据线被拉低,帧开始 next_state <= RECEIVING; bit_cnt <= 0; end end RECEIVING: begin if (bit_cnt == TOTAL_BITS - 1) begin next_state <= WAIT_TIMEOUT; timeout_cnt <= 0; end end WAIT_TIMEOUT: begin if (slo_data_in == 1'b1) begin timeout_cnt <= timeout_cnt + 1; if (timeout_cnt == TIMEOUT_MAX) begin next_state <= DONE; end end else begin timeout_cnt <= 0; // 数据线又动了,重新计时 end end DONE: begin next_state <= IDLE; // 一帧处理完毕,回到空闲 end endcase end

这段代码里,TIMEOUT_MAX的取值直接决定了接收模块对时钟频率的适应范围。如果设得太小,高频时可能误判帧结束;设得太大,低频时又会拖慢帧率。常见的做法是根据编码器规格书里的“帧间隔时间”来反推:假设帧间隔最小 1 微秒,内部时钟 50MHz,那么超时计数至少需要 50 个周期,计数器位宽至少 6 位。但很多开源实现为了省资源,只给了 4 位计数器,结果在 10MHz 以上就频繁丢帧。

3.2 位计数与 CRC 的并行处理:为什么校验不能等收完再做

BISS-C 的 CRC 校验通常是 6 位或 8 位,具体取决于帧长度。如果等所有数据位收完之后再逐位计算 CRC,会引入额外的延迟,而且需要额外的存储空间来缓存整帧数据。更高效的做法是在接收每一位的同时,实时更新 CRC 寄存器。这样当最后一位收完时,CRC 结果已经就绪,只需要和帧尾附带的 CRC 值比较即可。

CRC 的实时计算依赖于一个线性反馈移位寄存器(LFSR)。每收到一位数据,LFSR 就根据当前位和多项式做一次移位和异或。BISS-C 常用的多项式是 (x^6 + x + 1),对应的十六进制表示是 0x03。在 Verilog 里可以用一个 6 位寄存器来实现:

// CRC-6 实时计算(多项式 x^6 + x + 1) reg [5:0] crc_reg; wire crc_feedback = crc_reg[5] ^ data_bit; // 最高位与输入位异或 always @(posedge clk_fast) begin if (bit_valid) begin crc_reg <= {crc_reg[4:0], 1'b0} ^ (crc_feedback ? 6'h03 : 6'h00); end end

这里crc_feedback是当前输入位与 CRC 最高位的异或结果,如果为 1,则整个寄存器左移一位后与多项式 0x03 异或;如果为 0,则只左移。这种实现只需要 6 个触发器和几个异或门,资源消耗极小。但要注意:CRC 的初始值通常是 0,但有些编码器会要求初始值为全 1,这需要在复位时设置crc_reg <= 6'h3F。如果初始值搞错,校验永远不过,而且现象很隐蔽——数据看起来是对的,就是 CRC 报错。

3.3 双向切换的时序陷阱:写寄存器时为什么要“提前释放”

BISS-C 的写寄存器操作和读数据操作共用同一对数据线。当主机需要写寄存器时,它会在发送时钟的同时,把数据推到 SLO 线上。但从机什么时候开始驱动 SLO 线?规范里没有明确说“第几个时钟沿”,只说了“在收到一定数量的时钟后”。这意味着主机必须在从机开始驱动之前释放数据线,否则就会冲突。

常见的做法是:在写操作的最后一个时钟下降沿之后,立即把 SLO 线切回输入模式。但这里有一个时序陷阱——如果 FPGA 的内部逻辑延迟导致方向切换信号比时钟沿晚了一个周期,那么从机可能已经开始驱动了,而 FPGA 还在输出。结果就是两边同时驱动,差分电平被拉到中间值,接收端采到错误数据。更糟的是,这种冲突可能不会立即表现为数据错误,而是表现为 CRC 校验随机失败,让人误以为是编码器的问题。

解决方法是:在方向切换信号上增加一个“提前量”。具体来说,在最后一个写时钟的上升沿之后,就用一个寄存器把方向控制信号置为输入,而不是等到下降沿。这样即使有传播延迟,也能保证在从机驱动之前释放总线。代价是最后一个写数据的保持时间会缩短,需要确认从机的建立时间是否满足。如果从机要求数据保持到时钟下降沿之后,那就不能提前释放,只能靠精确的时序约束来保证。

注意:双向总线的方向切换是 BISS-C 接收模块最容易翻车的地方。建议在仿真时用波形观察 SLO 线的方向控制信号和实际数据电平,确认没有同时驱动的区间。如果条件允许,可以在 PCB 上串一个小电阻,用示波器看电流方向。

4. 避坑与排查:BISS-C 接收模块的五个血泪教训

4.1 现象:数据偶尔错一位,CRC 随机失败

原因:采样点太靠近数据跳变沿。BISS-C 的时钟和数据从编码器出来时,存在传播延迟,如果 FPGA 直接用内部时钟的上升沿采样,可能采到数据跳变的瞬间。
解决:把 MA 时钟也引到输入引脚,用同一个全局时钟采样 MA 和 SLO,然后在数字域里检测 MA 的边沿,延迟半个内部时钟周期再采样 SLO。或者用 IDELAY 原语调整采样相位。

4.2 现象:高频时钟下丢帧,低频正常

原因:超时计数器位宽不够。比如用 4 位计数器,最大 15 个周期,在 10MHz 时钟下超时窗口只有 1.5 微秒,而编码器的帧间隔可能达到 2 微秒,导致状态机提前复位。
解决:根据编码器规格书里的最大帧间隔时间,反推所需计数器位宽。公式是:计数器最大值 ≥ 内部时钟频率 × 最大帧间隔时间。留 20% 余量。

4.3 现象:写寄存器后读回数据错乱

原因:方向切换时机不对,总线冲突导致从机内部状态异常。
解决:在写操作的最后一个时钟沿之后,立即释放数据线,并等待至少一个超时周期再发起读操作。如果不确定从机的响应时间,可以在写操作后插入一个固定的空闲帧。

4.4 现象:CRC 校验一直不过,但数据位看起来正确

原因:CRC 初始值或多项式搞错。BISS-C 的 CRC 多项式通常是 (x^6 + x + 1),但有些编码器用 (x^8 + x^5 + x^4 + 1)。初始值可能是 0 或全 1。
解决:用编码器规格书里的测试向量验证 CRC 实现。如果没有规格书,可以用已知正确的帧数据反推多项式和初始值。

4.5 现象:上电后第一帧总是错的

原因:编码器上电后需要一段时间初始化,第一帧可能不完整或 CRC 无效。
解决:在接收模块里增加一个“上电忽略计数器”,上电后丢弃前 N 帧,等编码器稳定后再开始正常接收。N 的取值根据编码器手册里的“上电稳定时间”确定。

5. 设计取舍:资源、频率与可靠性的三角平衡

BISS-C 接收模块的设计本质上是在三个约束之间找平衡:FPGA 资源消耗、支持的最高时钟频率、以及通信可靠性。这三者往往互相矛盾——想要更高的时钟频率,就得用更快的采样逻辑,消耗更多触发器;想要更高的可靠性,就得增加超时窗口和 CRC 校验的冗余,又会拖慢帧率。

一个常见的取舍是:用 oversampling 还是边沿采样。Oversampling 需要内部时钟至少是 MA 时钟的 4 倍,对于 10MHz 的 BISS-C,内部时钟要跑到 40MHz 以上,这对低端 FPGA 来说可能吃不消。但 oversampling 的好处是抗抖动能力强,采样点可以精确控制。边沿采样则相反,资源省,但对时序约束要求高,PCB 走线长度不匹配就容易出错。

另一个取舍是 CRC 的实时计算还是收完再算。实时计算省存储,但逻辑复杂度稍高;收完再算需要缓存整帧数据,但逻辑简单。对于帧长较短的 BISS-C(通常 20 到 40 位),实时计算的优势更明显,因为缓存整帧需要 40 个触发器,而 CRC 寄存器只需要 6 个。

还有一个容易被忽略的取舍是:状态机的编码方式。二进制编码省触发器,但状态译码逻辑复杂;独热码编码浪费触发器,但译码简单,适合高速设计。对于 BISS-C 这种状态数很少(4 到 6 个)的状态机,独热码的额外开销可以接受,而且能跑更高的时钟频率。

我自己的习惯是:在 RTL 里先把超时计数器和 CRC 寄存器位宽留足,宁可多几个触发器,也不要为了省资源而牺牲可靠性。因为 BISS-C 通信一旦出错,排查成本远高于多用的那几个 LUT。从那以后我每次做 BISS-C 接收模块,都会在仿真里强制注入时钟抖动和数据延迟,确认状态机在极端条件下不会误判。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进