用Python给Verilog设计自仿(四):协程调度全解析,仅需三行代码构建以太网帧

Python3.8

Python 是一种高级、解释型、通用的编程语言,以其简洁易读的语法而闻名,适用于广泛的应用,包括Web开发、数据分析、人工智能和自动化脚本

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代码编辑器

FPGA实现DeepSeek加速卡可行性探讨

以太网——网络包解析器设计概述(2)(Packet Parsers)

本文由本账号所属公众号提供,欢迎关注微信公众号 AdirtCoreFpga,获取第一时间更新

您可能感兴趣的与本文相关的镜像

Python3.8

Python3.8

Conda
Python

Python 是一种高级、解释型、通用的编程语言,以其简洁易读的语法而闻名,适用于广泛的应用,包括Web开发、数据分析、人工智能和自动化脚本

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值