bench: Add cocotb-based test bench for MAC test

This commit is contained in:
Markus Koch 2022-04-18 17:06:53 +02:00
parent 7d86f6a262
commit f45ded2ca9
8 changed files with 321 additions and 0 deletions

10
.gitignore vendored
View File

@ -1 +1,11 @@
# VUnit
vunit_out vunit_out
# Cocotb
__pycache__
*.o
cocotb/results.xml
cocotb/sim_build
cocotb/wave.ghw
cocotb/cocotb_top_mac_test

3
.gitmodules vendored Normal file
View File

@ -0,0 +1,3 @@
[submodule "cocotb/cocotb_helpers"]
path = cocotb/cocotb_helpers
url = https://git.notsyncing.net/fpga/cocotb_helpers.git

42
bench/pll0.vhd Normal file
View File

@ -0,0 +1,42 @@
-- -------------------------------------------------------------------------- --
-- TRASHERNET - A Trashy Ethernet Stack for FPGAs --
-- -------------------------------------------------------------------------- --
-- pll0.vhd : Simulates the PLL component present in some demo designs
-- -------------------------------------------------------------------------- --
-- Author : Markus Koch <markus@notsyncing.net>
-- Contributors : None
-- License : Mozilla Public License (MPL) Version 2
-- -------------------------------------------------------------------------- --
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity pll0 is
port(
CLK : in std_logic;
CLKOP : out std_logic;
LOCK : out std_logic
);
end entity pll0;
architecture bench of pll0 is
begin
clock_driver : process
constant period : time := 7.1429 ns; -- 140 MHz configured clock
begin
CLKOP <= '0';
LOCK <= '0';
wait until CLK'event;
wait for 12 ns;
LOCK <= '1';
loop
CLKOP <= '0';
wait for period / 2;
CLKOP <= '1';
wait for period / 2;
end loop;
end process clock_driver;
end architecture bench;

17
cocotb/Makefile Normal file
View File

@ -0,0 +1,17 @@
# defaults
SIM ?= ghdl
TOPLEVEL_LANG ?= vhdl
BASE = $(PWD)/..
COMPILE_ARGS=--std=08
SIM_ARGS ?= --wave=wave.ghw
VHDL_SOURCES_trashernet += $(BASE)/trashernet/*.vhd
VHDL_SOURCES_design += $(BASE)/design/*.vhd
VHDL_SOURCES_design += $(BASE)/bench/pll0.vhd
VHDL_SOURCES += $(BASE)/cocotb/*.vhd
TOPLEVEL = cocotb_top_mac_test
MODULE = cocotb_top_mac_test
include $(shell cocotb-config --makefiles)/Makefile.sim

1
cocotb/cocotb_helpers Submodule

@ -0,0 +1 @@
Subproject commit e2c72ef61d7742fbcb7e048bc928446ed4b56a82

View File

@ -0,0 +1,48 @@
#!/usr/bin/env python
import cocotb
from cocotb.triggers import *
from cocotb.result import *
from cocotb.queue import Queue
from cocotb_helpers import buffers
ETH_HEAD = b'\x55\x55\x55\x55\xD5'
async def timeout(dut):
await Timer(1, units="ms")
assert False, "Timeout"
async def rxprinter(dut, rx):
while True:
frame = await rx.queue.get()
dut._log.info("RX Frame: " + str(frame))
@cocotb.test()
async def my_test(dut):
"""TBD description."""
# Start verification components
eth_tx = buffers.tx_buffer(dut, "cocovc_eth_inst.cocotb_tx_")
await eth_tx.start()
eth_rx = buffers.rx_buffer(dut, "cocovc_eth_inst.cocotb_rx_")
await eth_rx.start()
# Start local monitors
await cocotb.start(rxprinter(dut, eth_rx))
# Start timeout
await cocotb.start(timeout(dut))
# Wait for VHDL part to be ready
await Edge(dut.bench_ready)
# Start test procedure
await Timer(400, units="us") # The device will send a frame at startup
dut._log.info("Send first frame"); # This will trigger a response frame
await eth_tx.send(ETH_HEAD + b'\x00\xff\xff\x11"3\x00\xff\xff\xaa\xbb\xcc\x00\n\n\t\x08\x07\x06\x05\x04\x03\x02\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00!\x10\x90\xe0');
dut._log.info("Send second frame");
await eth_tx.send(ETH_HEAD + b'World');
await Timer(400, units="us") # Wait for response

View File

@ -0,0 +1,77 @@
-- -------------------------------------------------------------------------- --
-- TRASHERNET - A Trashy Ethernet Stack for FPGAs --
-- -------------------------------------------------------------------------- --
-- cocotb_top_mac_test.vhd : Test bench for the MAC test demo.
-- -------------------------------------------------------------------------- --
-- Author : Markus Koch <markus@notsyncing.net>
-- Contributors : None
-- License : Mozilla Public License (MPL) Version 2
-- -------------------------------------------------------------------------- --
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
library design;
use design.all;
entity cocotb_top_mac_test is
end entity cocotb_top_mac_test;
architecture bench of cocotb_top_mac_test is
-- DUT signals
signal clk : std_logic;
signal rst_n : std_logic;
signal rx_p : std_logic;
signal tx_p : std_logic;
signal tx_n : std_logic;
signal led_n : std_logic_vector(7 downto 0);
signal button_n : std_logic_vector(3 downto 0);
signal debug_data : std_logic_vector(7 downto 0);
-- Generic test bench
signal bench_ready : std_logic := '0';
begin
top_mac_test_inst : entity design.top_mac_test
port map(
clk => clk,
rst_n => rst_n,
rx_p => rx_p,
tx_p => tx_p,
tx_n => tx_n,
led_n => led_n,
button_n => button_n,
debug_data => debug_data
);
clock_driver : process
constant period : time := 20 ns;
begin
clk <= '0';
wait for period / 2;
clk <= '1';
wait for period / 2;
end process clock_driver;
rstsim : process is
begin
rst_n <= '0';
wait for 400 ns;
rst_n <= '1';
wait for 100 ns;
wait until rising_edge(clk);
bench_ready <= '1';
wait;
end process rstsim;
cocovc_eth_inst : entity work.cocovc_eth
port map(
rx_p => tx_p,
rx_n => tx_n,
tx_p => rx_p,
tx_n => open
);
button_n <= (others => '1');
end architecture bench;

123
cocotb/cocovc_eth.vhd Normal file
View File

@ -0,0 +1,123 @@
-- -------------------------------------------------------------------------- --
-- TRASHERNET - A Trashy Ethernet Stack for FPGAs --
-- -------------------------------------------------------------------------- --
-- cocovc_eth.vhd : Cocotb-based verification component for 10M Ethernet.
-- Note: This is cheapskated to work with Trashernet. It may not work with
-- every PHY.
-- -------------------------------------------------------------------------- --
-- Author : Markus Koch <markus@notsyncing.net>
-- Contributors : None
-- License : Mozilla Public License (MPL) Version 2
-- -------------------------------------------------------------------------- --
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity cocovc_eth is
port(
rx_p : in std_logic;
rx_n : in std_logic;
tx_p : out std_logic;
tx_n : out std_logic
);
end entity cocovc_eth;
architecture bench of cocovc_eth is
-- Ethernet TX (TB/Python to DUT/VHDL)
signal cocotb_tx_d : std_logic_vector(7 downto 0) := x"00"; -- Data to be transferred to VHDL
signal cocotb_tx_dv : std_logic := '0'; -- '1' when `d` holds valid data that is to be processed by VHDL
signal cocotb_tx_da : std_logic := '0'; -- Toggles when VHDL has latched `d`, Python must advance data or deassert `dv`
-- Ethernet RX (DUT/VHDL to TB/Python)
signal cocotb_rx_d : std_logic_vector(7 downto 0); -- Data to be transferred to Python
signal cocotb_rx_dt : std_logic := '0'; -- Toggles to announce new data (Python has to latch immediately)
signal cocotb_rx_a : std_logic := '0'; -- Block active indicator (rising on block start, falling on block end)
begin
-- Ethernet TX
eth_tx_proc : process is
variable d : std_logic_vector(cocotb_rx_d'range);
begin
tx_p <= '0';
wait until cocotb_tx_dv = '1';
loop
exit when not cocotb_tx_dv;
d := cocotb_tx_d;
cocotb_tx_da <= not cocotb_tx_da;
for j in d'low to d'high loop
tx_p <= not d(j);
wait for 50 ns;
tx_p <= d(j);
wait for 50 ns;
end loop;
end loop;
wait for 16 us; -- Wait IPG
cocotb_tx_da <= not cocotb_tx_da; -- Signal that end-of-block was latched
end process eth_tx_proc;
tx_n <= not tx_p;
-- Ethernet RX
eth_rx_proc : process is
constant SYM_TIME : time := 50 ns;
variable d : std_logic_vector(cocotb_rx_d'range) := x"00";
variable cnt : integer range d'low to d'high;
variable bit : std_logic;
variable ignore : boolean;
variable first : boolean; -- Just a cheap workaround because the weather is too nice to implement it properly this afternoon
procedure latch(value : std_logic) is
begin
d(cnt) := value;
if (cnt = 7) then
cocotb_rx_d <= d;
cocotb_rx_dt <= not cocotb_rx_dt;
cnt := 0;
else
cnt := cnt + 1;
end if;
end procedure latch;
begin
cocotb_rx_a <= '0';
loop
wait on rx_p;
exit when (rx_p'last_value = '0' or rx_p'last_value = '1');
end loop;
cocotb_rx_a <= '1';
wait for 0 ns;
cnt := 0;
bit := '0';
ignore := false;
first := true;
loop
wait on rx_p for SYM_TIME * 1.5;
exit when rx_p'last_event > 2.5 * SYM_TIME;
if rx_p'event then -- short
if not ignore then
latch(bit);
end if;
ignore := not ignore;
else -- long
if (first) then
bit := '1';
cnt := 1;
d(0) := '1';
end if;
bit := not bit;
latch(bit);
ignore := true;
end if;
first := false;
end loop;
end process eth_rx_proc;
end architecture bench;