☰
UART BFM设计与实现:从比特到字节的验证环境构建
2026/10/6 4:38:50 网站建设 项目流程

1. 为什么要在验证环境里造一个UART BFM

做数字验证的人迟早会碰到一个尴尬局面:DUT里明明有UART接口,但手头没有对端的串口设备,或者有设备却没法精确控制每一个比特的时序。拿真实串口助手去测,波特率一高就丢字节,想构造一个奇偶校验错误得靠运气,想复现一个帧错误更是难上加难。这时候BFM(Bus Functional Model)就是那个"把协议装进管道"的东西——它不实现完整的UART控制器,只负责在事务层和信号层之间做翻译,让testbench能用"发一个字节"这种人类语言去驱动tx线上的串行比特流。

UART BFM的核心价值在于把串行时序从验证工程师的脑子里搬到代码里。一个字节在线上是怎么走的?起始位拉低一个比特时间,接着8个数据位从LSB开始依次送出,然后是可选的校验位,最后停止位拉高。这些细节如果每次写testbench都要手动算时间、手动翻转信号,那验证效率会低到令人发指。BFM把这些封装成任务(task),你只需要调用send_byte(8'hA5),剩下的比特级操作它全包了。

我见过不少项目组用最原始的方式做UART验证:写一个always块,用计数器分频产生波特率时钟,然后在状态机里硬编码发送逻辑。这种做法在只有一个测试用例时还能凑合,一旦要跑回归、要随机化、要覆盖各种错误场景,代码就会膨胀成一团乱麻。BFM的思路是把协议层和测试层解耦,测试层只管"发什么",BFM管"怎么发"。这个解耦带来的好处在后期做覆盖率驱动验证时会体现得淋漓尽致。

还有一点容易被忽略:UART BFM不只是发送端的事。一个完整的BFM应该同时具备发送和接收能力,而且接收端要能独立于发送端工作。为什么?因为很多UART DUT的环回测试需要同时验证收发两条路径,如果BFM的收发共用一个状态机,就会出现"发的时候不能收"的尴尬。所以从架构设计的第一天起,就要把TX BFM和RX BFM当作两个独立的模块来对待,它们之间唯一的耦合是共享波特率配置。

2. UART BFM的架构拆解与接口定义

2.1 事务层与信号层的边界划分

BFM的本质是一个翻译器,它的一端是事务(transaction),另一端是引脚(pin)。事务层关心的是"发一个字节0xA5,无校验,1个停止位",信号层关心的是"tx线在第3个比特时间应该是什么电平"。这两层之间的边界划在哪里,直接决定了BFM的灵活性和可复用性。

我的做法是把边界划在字节级。也就是说,BFM对外暴露的API是send_byte、receive_byte、send_break这类字节级操作,而不是send_bit。原因很简单:UART协议的最小语义单元就是帧(frame),一个帧包含起始位、数据位、校验位、停止位,单独操作比特位没有实际意义。如果把边界划在比特级,测试层就要自己组装帧,那BFM就退化成了一个简单的移位寄存器,失去了封装的价值。

但字节级边界也有代价:某些特殊场景需要发送不完整的帧,比如故意在停止位之前拉低信号来构造帧错误。这种需求可以通过在BFM上开一个"原始模式"接口来满足,让测试层在必要时能绕过帧封装直接控制信号。这个口子不能开太大,否则BFM的封装就形同虚设。

2.2 参数化配置:波特率、数据位、校验、停止位

一个可复用的UART BFM必须支持参数化配置。这些参数包括:

参数典型取值说明
波特率9600, 115200, 1M决定每个比特的持续时间
数据位5, 6, 7, 8通常为8
校验模式无、奇、偶、固定0、固定1影响校验位的计算
停止位1, 1.5, 2影响帧尾的高电平持续时间
空闲电平高UART标准规定空闲为高

这些参数在BFM初始化时配置,运行过程中一般不变。但有一种情况需要动态切换:某些协议在握手阶段用9600波特率,握手成功后切到115200。这时候BFM需要支持运行时重配置,而且重配置的时机要精确控制——必须在当前帧发送完成之后、下一帧开始之前切换,否则会产生半个帧的垃圾数据。

波特率的实现方式有两种:一种是直接用#delay做比特级延时,另一种是生成一个波特率使能信号(baud tick),所有时序逻辑都基于这个使能信号。前者写起来简单,但在SystemVerilog里大量使用#delay会导致仿真性能下降,而且不利于做时序检查。后者更接近真实硬件的设计思路,也方便在波形上观察。我倾向于用baud tick方案,具体做法是:用一个计数器对系统时钟计数,计到CLK_FREQ/BAUD_RATE时产生一个单周期脉冲,这个脉冲就是比特时间的基准。

2.3 TX与RX路径的独立设计

TX路径的核心是一个并串转换器。当send_byte被调用时,BFM把字节锁存到内部寄存器,然后按照帧格式依次在tx线上输出起始位、数据位、校验位、停止位。每个比特的持续时间由baud tick控制。TX路径还需要一个"忙"信号,告诉测试层当前帧是否发送完毕,避免在发送过程中被新的事务打断。

RX路径的核心是一个串并转换器,但它比TX多了一个关键步骤:起始位检测。RX BFM必须持续监视rx线,当检测到从高到低的跳变时,认为起始位到来,然后在比特时间的中间点采样数据位。为什么要在中间点采样?因为起始位的边沿可能带有抖动,如果在边沿附近采样,容易采到错误的值。在比特时间的中点采样,此时信号最稳定,抗抖动能力最强。

RX路径还需要一个超时机制。如果起始位检测到了,但后续的数据位没有按时到来(比如对端设备故障),RX BFM不能无限等待,应该在一个帧时间内没有完成接收就报超时错误。这个超时时间通常设为(数据位+校验位+停止位+1) * 比特时间 * 1.5,留出50%的余量。

3. 从比特到字节:TX BFM的发送逻辑实现

3.1 帧格式的精确构造

UART帧的构造看起来简单,但有几个细节容易出错。第一个细节是数据位的顺序。UART协议规定数据位从LSB开始发送,也就是说如果要发送0xA5(二进制10100101),线上依次出现的是1, 0, 1, 0, 0, 1, 0, 1。很多新手会习惯性地从MSB开始发,结果对端收到的字节是位反转的。

第二个细节是校验位的计算时机。校验位是对数据位的奇偶校验,计算时必须基于原始数据字节,而不是移位后的值。在实现时,我通常先把校验位算好存到一个临时变量里,然后在发送完数据位之后直接输出这个变量,避免在移位过程中重复计算。

第三个细节是停止位的持续时间。标准UART的停止位是1个比特时间的高电平,但有些设备支持1.5或2个停止位。在BFM实现时,停止位的处理不能简单地"拉高一个比特时间",因为如果下一帧紧接着到来,停止位和下一帧的起始位之间需要有一个明确的边界。我的做法是在停止位之后强制插入至少半个比特时间的空闲,确保接收端能正确识别帧边界。

下面是一个TX BFM核心逻辑的伪代码示例:

task send_byte(input byte data); // 等待当前帧发送完成 wait(tx_busy == 0); tx_busy = 1; // 起始位 tx_line = 0; repeat(baud_ticks_per_bit) @(posedge clk); // 数据位,LSB first for (int i = 0; i < data_bits; i++) begin tx_line = data[i]; repeat(baud_ticks_per_bit) @(posedge clk); end // 校验位 if (parity_mode != NONE) begin tx_line = calc_parity(data); repeat(baud_ticks_per_bit) @(posedge clk); end // 停止位 tx_line = 1; repeat(stop_bits * baud_ticks_per_bit) @(posedge clk); tx_busy = 0; endtask

这段代码里有一个关键点:baud_ticks_per_bit的计算。如果系统时钟是50MHz,波特率是115200,那么每个比特持续的时间是50_000_000 / 115200 ≈ 434个时钟周期。这个除法结果通常不是整数,直接取整会累积误差。对于8个数据位加起始位和停止位共10个比特,434的取整误差在10个比特后会累积到10个时钟周期,相当于0.023个比特时间,这个误差在可接受范围内。但如果波特率更高,比如1Mbps,每个比特只有50个时钟周期,取整误差的影响就相对更大了。这时候需要用分数分频或者累加器方案来减小误差。

3.2 波特率精度对采样点的影响

波特率误差是UART通信中最隐蔽的问题之一。假设发送端和接收端的波特率都有±2%的误差,最坏情况下两者的偏差达到4%。UART接收端在第10个比特(停止位)采样时,采样点已经偏离了理想位置4% × 10 = 40%的比特时间。如果采样点原本在比特中点(50%位置),偏移40%后就到了90%的位置,非常接近比特边界,稍有抖动就会采错。

这就是为什么UART协议通常要求波特率误差控制在±2%以内。在BFM实现时,如果系统时钟和波特率不是整数倍关系,必须评估取整误差是否在可接受范围内。一个实用的经验法则是:累积误差不超过比特时间的5%。对于10比特的帧,这意味着每个比特的误差不超过0.5%。如果系统时钟是100MHz,波特率是115200,每个比特约868个时钟周期,取整误差最多1个周期,10个比特累积10个周期,占比10/868 ≈ 1.15%,在可接受范围内。

如果误差超标,有两种解决方案:一是提高系统时钟频率,让每个比特对应的时钟周期数更多,取整误差的相对影响更小;二是采用分数分频,用累加器在多个比特之间分配余数。第二种方案更优雅,但实现复杂度更高,需要根据项目实际情况权衡。

3.3 发送完成标志与背压处理

TX BFM必须提供一个明确的"发送完成"标志,否则测试层无法知道什么时候可以发下一帧。这个标志的实现方式有两种:一种是tx_busy信号,高电平表示正在发送;另一种是tx_done脉冲,在停止位结束的那个时钟周期产生一个单周期脉冲。

我倾向于同时提供这两个信号。tx_busy用于流控,测试层在调用send_byte之前检查它;tx_done用于事件通知,测试层可以用@(posedge tx_done)来等待发送完成。这两个信号配合使用,可以覆盖绝大多数测试场景。

背压处理是另一个容易被忽略的点。如果测试层在tx_busy为高时强行调用send_byte,BFM应该怎么处理?直接丢弃?阻塞等待?还是报错?我的做法是阻塞等待,在send_byte的开头加一个wait(tx_busy == 0)。这样测试层不需要关心BFM的内部状态,调用send_byte就一定能发出去,只是可能需要等一会儿。这种设计对测试层最友好,代价是如果测试层不小心在循环里连续调用send_byte,仿真时间会拉长,但不会出错。

4. RX BFM的采样策略与错误检测

4.1 起始位检测与中点采样

RX BFM的核心挑战是在正确的时间点采样。UART没有独立的时钟线,接收端必须从起始位的下降沿开始,自己推算每个数据位的采样时刻。这个过程叫位同步。

标准的做法是:检测到rx线从高到低的跳变后,等待半个比特时间,确认这确实是起始位(而不是噪声毛刺),然后在每个比特时间的中间点采样。为什么是中间点?因为起始位的下降沿可能不是理想的垂直跳变,而是有一定斜率的,在边沿附近采样容易采到不确定的值。在比特中点采样,此时信号已经稳定,采样结果最可靠。

实现时,我用一个计数器来跟踪比特时间。检测到起始位后,计数器从0开始计数,计到baud_ticks_per_bit/2时采样起始位(确认),然后每计到baud_ticks_per_bit时采样一个数据位。采样点的计算公式是:

采样时刻 = 起始位边沿 + (n + 0.5) * 比特时间

其中n是比特序号,起始位为0,第一个数据位为1,依此类推。

这里有一个细节:如果波特率不是整数分频,baud_ticks_per_bit/2可能不是整数。这时候需要做舍入,而且舍入误差会累积。对于接收端来说,误差累积比发送端更致命,因为接收端没有参考时钟,完全靠自己的计数器推算采样点。如果累积误差导致采样点偏移超过半个比特时间,就会采到相邻比特的值,产生错误。

4.2 校验错误、帧错误与溢出错误的判定

一个健壮的RX BFM应该能检测三类错误:

校验错误(Parity Error):接收完数据位后,BFM计算数据位的奇偶校验值,与接收到的校验位比较。如果不一致,置位parity_error标志。这个标志应该在帧接收完成后保持至少一个时钟周期,供测试层采样。

帧错误(Framing Error):在停止位应该为高电平时,如果采样到低电平,说明帧格式错误。帧错误通常意味着发送端和接收端的波特率不匹配,或者线路受到了干扰。BFM检测到帧错误后,应该立即终止当前帧的接收,回到空闲状态等待下一个起始位。

溢出错误(Overrun Error):如果接收完一个字节后,测试层还没有读取上一个字节,新的字节又到了,就会产生溢出。BFM需要维护一个接收缓冲区(通常深度为1或2),当缓冲区满时,新到的数据会覆盖旧数据,同时置位overrun_error。这个错误在高速通信时特别常见,测试层必须及时读取数据。

这三类错误的检测逻辑应该在RX BFM内部完成,测试层只需要读取错误标志即可。错误标志的清除通常由测试层显式操作,或者在读取数据时自动清除,具体取决于协议约定。

4.3 接收超时与空闲线检测

RX BFM不能无限等待。如果起始位检测到了,但后续的数据位迟迟不来,BFM应该在一个合理的时间后报超时错误。超时时间的设定需要权衡:太短会误报,太长会拖慢仿真。我的经验值是(数据位 + 校验位 + 停止位 + 1) * 比特时间 * 1.5,也就是正常帧时间的1.5倍。

空闲线检测是另一个有用的功能。当rx线连续保持高电平超过一个帧时间时,BFM可以产生一个idle标志,告诉测试层线路空闲。这个标志在检测帧间隔、判断通信结束时很有用。实现上,用一个计数器对rx线的高电平持续时间计数,超过阈值就置位idle。

下面是一个RX BFM采样逻辑的伪代码:

always @(posedge clk) begin case (rx_state) IDLE: begin if (rx_line == 0) begin rx_state <= START_CHECK; bit_counter <= 0; end end START_CHECK: begin bit_counter <= bit_counter + 1; if (bit_counter == HALF_BIT) begin if (rx_line == 0) begin rx_state <= DATA_SAMPLE; bit_counter <= 0; sample_index <= 0; end else begin rx_state <= IDLE; // 毛刺,忽略 end end end DATA_SAMPLE: begin bit_counter <= bit_counter + 1; if (bit_counter == BIT_TICKS) begin rx_data[sample_index] <= rx_line; sample_index <= sample_index + 1; bit_counter <= 0; if (sample_index == DATA_BITS - 1) rx_state <= PARITY_CHECK; end end // ... 校验位和停止位处理 endcase end

这段代码的关键在于HALF_BIT和BIT_TICKS的精确计算。如果这两个值有误差,采样点就会偏移。在实际项目中,我通常会把这两个值做成参数,方便在不同波特率下调整。

5. 把BFM接入testbench:连接、配置与激励生成

5.1 虚拟接口与时钟域处理

在SystemVerilog的UVM或传统testbench中,BFM通常以interface的形式存在。interface封装了tx、rx、baud_tick等信号,以及send_byte、receive_byte等任务。测试层通过virtual interface访问BFM,这样可以在不修改BFM代码的情况下替换不同的物理接口。

时钟域处理是一个容易踩坑的地方。如果BFM的时钟和DUT的时钟是同一个时钟,那很简单,直接用即可。但如果DUT的UART接口有自己的时钟域(比如经过了一个异步FIFO),BFM就需要做跨时钟域处理。最简单的做法是在BFM和DUT之间插入一个同步器,但这会引入额外的延迟,影响时序精度。更优雅的做法是让BFM直接工作在DUT的时钟域,通过配置接口传入时钟频率和波特率,BFM内部自己生成baud tick。

5.2 测试场景的激励构造

有了BFM之后,测试场景的构造就变得非常直观。以下是几个典型的测试场景:

基本收发测试:发送一串随机字节,接收端环回,比较发送和接收的数据是否一致。这个测试验证基本的收发功能。

边界条件测试:发送0x00和0xFF,这两个值在数据位上分别是全0和全1,容易暴露移位方向的错误。

错误注入测试:故意发送错误的校验位,验证DUT是否能正确检测到校验错误。或者故意在停止位拉低,验证帧错误检测。

背靠背测试:连续发送多个字节,中间不留空闲时间,验证DUT的接收缓冲区是否能正确处理连续帧。

波特率容错测试:让BFM的波特率与DUT的波特率有微小偏差(比如±1%),验证DUT的采样点是否足够鲁棒。

这些场景用BFM实现起来都很简单,基本就是几行send_byte和receive_byte的调用。如果没有BFM,每个场景都需要手写时序逻辑,工作量会大很多。

5.3 覆盖率收集与断言嵌入

BFM不仅是激励生成器,也是覆盖率收集和断言检查的天然载体。在BFM内部,可以很方便地收集以下覆盖率:

  • 数据值覆盖:是否发送了所有256个可能的字节值
  • 校验模式覆盖:是否测试了无校验、奇校验、偶校验
  • 停止位覆盖:是否测试了1位和2位停止位
  • 错误类型覆盖:是否触发了校验错误、帧错误、溢出错误

断言方面,可以在BFM的发送和接收路径上嵌入协议检查。比如:起始位必须为低,停止位必须为高,数据位在采样点必须稳定。这些断言在仿真过程中实时检查,一旦违反立即报错,比事后分析波形高效得多。

一个实用的技巧是把覆盖率收集和断言检查做成可配置的。在回归测试的早期阶段,打开所有断言,快速暴露问题;在后期,关闭部分断言以提升仿真速度,只保留关键检查。

6. 实测中容易踩的坑与调试技巧

6.1 波特率计算中的取整陷阱

前面提到过波特率取整误差的问题,这里展开说一个具体的坑。假设系统时钟是48MHz,波特率是115200,每个比特的时钟周期数是48_000_000 / 115200 = 416.67。如果直接取整为416,实际波特率变成48_000_000 / 416 = 115384.6,误差0.16%。这个误差看起来很小,但10个比特累积后,采样点偏移了1.6%的比特时间。如果接收端的采样点原本在50%位置,偏移后到了51.6%,仍然安全。

但如果系统时钟是25MHz,波特率是115200,每个比特是25_000_000 / 115200 = 217.01。取整为217,实际波特率是25_000_000 / 217 = 115207.4,误差0.006%,几乎可以忽略。但如果取整为218,误差就变成-0.45%,10个比特累积4.5%,采样点偏移到54.5%,开始接近危险区域。

这个坑的教训是:取整方向要一致,而且要做误差分析。我通常会在BFM初始化时打印实际波特率和目标波特率的偏差,如果超过0.5%就发出警告。

6.2 起始位毛刺导致的误触发

在实际硬件中,rx线在空闲时应该是稳定的高电平,但如果有干扰,可能会出现短暂的负脉冲。如果RX BFM在检测到下降沿后立即开始接收,就会把毛刺当成起始位,接收一帧垃圾数据。

解决方案是在检测到下降沿后,等待半个比特时间再确认。如果半个比特时间后rx线仍然是低电平,才认为是真正的起始位;如果已经恢复高电平,就判定为毛刺,回到空闲状态。这个"半比特确认"机制在大多数UART接收器中都有实现,BFM也必须加上。

6.3 仿真中的竞争冒险与采样时机

在Verilog/SystemVerilog仿真中,信号的更新和采样存在竞争冒险。如果BFM在时钟上升沿更新tx线,而DUT也在同一个上升沿采样tx线,就可能出现采样到旧值的情况。这是仿真语义的问题,不是设计错误,但会导致仿真结果与硬件不一致。

避免这个坑的方法是使用非阻塞赋值(<=)更新信号,并且在时钟边沿之后采样。在BFM中,我通常用@(posedge clk)等待时钟边沿,然后用阻塞赋值更新信号,确保信号在下一个时钟边沿之前稳定。如果DUT在时钟边沿采样,它看到的是BFM在上一个边沿之后更新的值,不会产生竞争。

另一个技巧是在BFM和DUT之间插入一个小的延时(比如#1),让信号更新和采样在时间上错开。这个延时在仿真中不会影响功能,但能消除竞争冒险。不过这个技巧要慎用,因为如果延时太大,可能会掩盖真实的时序问题。

6.4 用波形反推BFM行为的调试方法

当BFM的行为不符合预期时,最有效的调试方法是看波形。我通常会在波形中重点观察以下几个信号:

  • baud_tick:确认波特率是否正确
  • tx_busy/rx_busy:确认BFM的状态机是否按预期跳转
  • tx_line/rx_line:确认比特级的波形是否符合UART协议
  • 内部状态机的状态编码:确认状态跳转是否正确

一个实用的技巧是在BFM内部加一个"调试模式",当调试模式打开时,BFM会在每个比特时间打印当前状态和信号值。这样即使不看波形,也能从日志中追踪BFM的行为。调试模式在回归测试时关闭,避免日志过多影响性能。

7. 从BFM到VIP:可复用性的进阶思考

7.1 参数化与可配置设计

一个BFM如果只能用于一个项目,那它的价值是有限的。要提升可复用性,必须做参数化设计。以下参数应该在BFM初始化时配置:

  • 系统时钟频率
  • 波特率
  • 数据位宽(5-8)
  • 校验模式
  • 停止位数量
  • 采样点位置(默认50%,可调)
  • 超时时间

这些参数通过一个配置类或配置接口传入,BFM内部根据这些参数计算所有时序。这样同一个BFM代码可以用于不同的项目,只需要改配置即可。

7.2 与UVM寄存器模型的集成

在UVM环境中,UART BFM通常需要与寄存器模型(RAL)配合工作。寄存器模型负责配置DUT的波特率、数据位等参数,BFM负责实际的收发。两者之间需要保持同步:当寄存器模型修改了波特率配置后,BFM也需要相应更新。

实现这种同步有两种方式:一种是在寄存器模型的update回调中通知BFM更新配置;另一种是BFM定期读取DUT的配置寄存器,自动同步。第一种方式更精确,但需要修改寄存器模型;第二种方式更简单,但可能有延迟。我倾向于第一种方式,因为UART的配置切换通常需要精确控制时机。

7.3 性能优化:减少仿真时间开销

BFM在仿真中会被频繁调用,如果实现不当,会成为仿真性能的瓶颈。以下是一些优化技巧:

  • 避免在BFM内部使用#delay,改用时钟边沿和计数器
  • 减少不必要的断言和覆盖率收集,在回归后期关闭非关键检查
  • 使用automatic变量而不是static变量,避免仿真器的额外开销
  • 对于大批量数据传输,考虑使用DMA风格的批量接口,减少任务调用次数

这些优化在小型测试中效果不明显,但在大规模回归中能显著缩短仿真时间。我做过一个对比:优化前一个UART回归需要4小时,优化后降到2.5小时,效果还是很明显的。

7.4 从BFM到完整VIP的演进路径

BFM是VIP(Verification IP)的雏形。一个完整的UART VIP除了BFM的基本功能外,还应该包括:

  • 协议检查器(Protocol Checker):自动检查UART协议的合规性
  • 覆盖率模型(Coverage Model):预定义的覆盖率组和交叉覆盖
  • 序列库(Sequence Library):常用的测试序列,如随机收发、错误注入
  • 寄存器抽象层(RAL):与DUT寄存器的标准接口

从BFM演进到VIP的过程是渐进的。一开始只需要基本的收发功能,随着项目复杂度提升,逐步添加检查、覆盖、序列。不要一开始就追求大而全,那样会拖慢开发进度。我的经验是:先用BFM跑通基本测试,然后在实际使用中根据需求逐步扩展,最终形成一个适合项目需求的VIP。

UART BFM的设计和实现,说到底是一个"把协议细节封装起来,让测试层专注于场景"的工程问题。它不追求功能上的大而全,而是追求接口的清晰和行为的可预测。一个设计良好的BFM,应该让测试工程师在写测试用例时感觉不到它的存在——就像呼吸一样自然,你不需要想着怎么呼吸,但它一直在那里工作。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询