1前言
对于许多FPGA/IC工程师而言,设计实现游刃有余,验证仿真却常成短板——传统验证方法面临两难困局:学习UVM需投入大量时间成本,而纯Verilog自仿又会陷入重复造轮子的低效循环。以通信协议仿真为例,仅报文解析就需要重写整套解析逻辑,相当于用Verilog再实现一次协议栈,耗时费力。
此时,Python的生态优势便锋芒尽显。其丰富的字符串处理库可直接解析报文,配合Cocotb框架,仅需少量Python代码即可构建高效测试平台,将验证工作量压缩70%以上。Cocotb的独特价值正在于此:用Python解放验证生产力,让工程师专注于设计创新而非重复劳动。
本章将深度解析协程的定义,并实战演示如何通过协程构建以太网帧来验证RTL代码。
2协程是什么
协程是 cocotb 中的异步函数,通过 async def 关键字定义,内部使用 await 等待硬件事件(如时钟边沿、定时器、信号变化等)。
所谓的协程就相当于我们RTL中的always块,但是通过 Python 的异步语法来模拟硬件的并行行为。
如下所示,通过async def定义了一个my_test的协程
async def my_test(dut):
await RisingEdge(dut.clk) # 等待时钟上升沿
dut.data.value = 0xFF # 驱动信号
但是,这并不是cocotb测试协程,只需要加上@cocotb.test()装饰器,就会成为cocotb用来验证RTL的协程。
@cocotb.test()
async def my_test(dut):
await RisingEdge(dut.clk) # 等待时钟上升沿
dut.data.value = 0xFF # 驱动信号
3任务是什么
任务是协程的运行实例。一个协程可生成多个任务,多个任务可并行执行。我们知道协程相当于always块,那么任务就相当于always块里面的逻辑。
在我们定义了一个协程之后,相当于RTL实现了:
@cocotb.test()
async def my_test(dut):
alwasys @(posedge clk)begin
end
在我们定义了一个任务,相当于RTL实现了:
@cocotb.test()
async def my_test(dut):
await RisingEdge(dut.clk) # 等待时钟上升沿
dut.data.value = 0xFF # 驱动信号
alwasys @(posedge clk)begin
data <= 8'hFF;
end
需要注意的是,协程需通过任务调度方法(如 start_soon、start)启动后,才能与仿真器交互。例如我们定义了一个test_ethernet_parser协程:
@cocotb.test()
async def test_ethernet_parser(dut):
# 启动时钟(100MHz)
task = cocotb.start_soon(Clock(dut.clk, 10, units="ns").start())
通过start_soon启动了一个100MHz的时钟,相当于在Verilog代码实现了一个initial逻辑块:
initial clk = 0;
always #(10/2) clk = ~clk;
任务控制
我们可以通过await、kill等关键字来控制任务的执行。
await 关键字用于在协程中挂起当前任务,并让出 CPU 控制权给事件循环(Event Loop),使其他协程任务得以执行。其后必须跟随一个可等待对象(如协程、Task 或 Future)。
在verilog中也有wait语句,await可以近似理解为verilog中的wait,虽然两者在本质上是不同的东西,但是对于我们做测试平台,其行为是相似的。
任务也可以被强制终止,通过kill语句来实现,kill() 是管理无限循环任务的必要工具,尤其在需要动态切换配置的测试场景中。
@cocotb.test()
async def test_different_clocks(dut):
# 创建两个不同频率的时钟
clk_1mhz = Clock(dut.clk, 1.0, units='us') # 周期1us (1MHz)
clk_250mhz = Clock(dut.clk, 4.0, units='ns') # 周期4ns (250MHz)
# 启动1MHz时钟生成任务(无限循环)
clk_gen = cocotb.start_soon(clk_1mhz.start())
# 验证1MHz时钟周期
start_time_ns = get_sim_time(units='ns')
await Timer(1, units='ns')
await RisingEdge(dut.clk)
edge_time_ns = get_sim_time(units='ns')
if not isclose(edge_time_ns, start_time_ns + 1000.0):
raise TestFailure("Expected a period of 1 us")
# 终止1MHz时钟任务
clk_gen.kill() # 必须手动终止,否则任务会继续运行!
# 启动250MHz时钟生成任务
clk_gen = cocotb.start_soon(clk_250mhz.start())
# 验证250MHz时钟周期
start_time_ns = get_sim_time(units='ns')
await Timer(1, units='ns')
await RisingEdge(dut.clk)
edge_time_ns = get_sim_time(units='ns')
if not isclose(edge_time_ns, start_time_ns + 4.0):
raise TestFailure("Expected a period of 4 ns")
4异步生成器
在 async def 函数中使用 yield 语句,函数会返回一个 异步生成器对象。异步生成器可以逐步生成值,同时支持在生成过程中挂起(await)其他异步操作。
我们可以直接理解为在协程中使用的for循环,不会阻塞其他任务的执行。
async def ten_samples_of(clk, signal):
for i in range(10):
await RisingEdge(clk) # 等待时钟信号的上升沿(异步挂起)
yield signal.value # 生成当前信号的值到外部循环
等效为:
initial begin
repeat(10) @(posedge clk) a = signal;
end
异步迭代(async for),async for 会驱动异步生成器 ten_samples_of,每次迭代时会自动等待 yield 返回的值。生成器内部 await RisingEdge(clk) 和 yield 的组合,使得每次采样仅在时钟上升沿触发时进行。
@cocotb.test()
async def test_samples_are_even(dut):
async for sample in ten_samples_of(dut.clk, dut.signal):
assert sample % 2 == 0 # 断言采样值为偶数
5实战
在 Python 中有一个专门用于网络数据包处理的库——Scapy。它是一个功能强大的工具,支持生成和解析各种网络协议数据包,常用于端口扫描、网络探测、协议测试等场景。因此,我们可以利用 Scapy 构建以太网帧,来验证我们的网络报文解析模块,从而避免使用 Verilog 堆叠复杂的协议栈。使用 Scapy 十分简单,仅需三行代码即可完成:
# 生成测试以太网帧(Payload为"Hello Cocotb!")
test_payload = b"Hello Cocotb!"
eth_frame = Ether(src="00:11:22:33:44:55", dst="aa:bb:cc:dd:ee:ff") / Raw(test_payload)
frame_bytes = bytes(eth_frame)
融入到cocotb测试协程中之后:
import cocotb
from cocotb.clock import Clock
from cocotb.triggers import RisingEdge, Timer
from scapy.all import Ether, Raw
@cocotb.test()
async def test_ethernet_parser(dut):
# 启动时钟(100MHz)
cocotb.start_soon(Clock(dut.clk, 10, units="ns").start())
# 初始化信号
dut.rx_valid.value = 0
dut.rx_packet.value = 0
await Timer(20, units="ns") # 等待复位完成
# 生成测试以太网帧(Payload为"Hello Cocotb!")
test_payload = b"Hello Cocotb!"
eth_frame = Ether(src="00:11:22:33:44:55", dst="aa:bb:cc:dd:ee:ff") / Raw(test_payload)
frame_bytes = bytes(eth_frame)
# 将帧数据通过接口发送至DUT
for byte in frame_bytes:
dut.rx_valid.value = 1
dut.rx_packet.value = byte
await RisingEdge(dut.clk)
dut.rx_valid.value = 0
# 等待DUT处理完成(假设DUT输出解析后的Payload到信号payload_out)
await RisingEdge(dut.payload_valid) # 假设DUT有payload_valid标志
# 从DUT获取Payload字节流并解析
payload_bytes = bytes([int(dut.payload_out.value) for _ in range(len(test_payload))])
# 使用Scapy解析Payload
parsed_payload = payload_bytes.decode('utf-8')
dut._log.info(f"Parsed Payload: {parsed_payload}")
# 断言Payload正确性
assert parsed_payload == "Hello Cocotb!", "Payload解析错误!"
7写在最后
本文为原创Cocotb技术专栏,欢迎工程师伙伴们留言讨论或交流,共同学习。若想第一时间获取更新,可点击下方「关注」或订阅Cocotb专题。
往期回顾
用Python给Verilog设计自仿(一):Cocotb环境初探
用Python给Verilog设计自仿(二):用D触发器解锁自动化验证的「第一个波形」
用Python给Verilog设计自仿(三):Cocotb高频语法,从此告别SV手写
DeepSeek本地部署最简教程——零成本搭建AI代码编辑器
以太网——网络包解析器设计概述(2)(Packet Parsers)
本文由本账号所属公众号提供,欢迎关注微信公众号 AdirtCoreFpga,获取第一时间更新
:协程调度全解析,仅需三行代码构建以太网帧&spm=1001.2101.3001.5002&articleId=146537691&d=1&t=3&u=348500e19b7a490989515c9246ed1e32)
1036

被折叠的 条评论
为什么被折叠?



