Code Generation

Once an Architecture has been derived, the final step is to generate synthesizable HDL that describes it, along with a testbench that can be used to verify the implementation through simulation.

Recreating the Architecture

We start by reproducing the architecture derived in the previous tutorial.

from scipy.signal import iirfilter

from b_asic.architecture import Architecture, Memory, ProcessingElement
from b_asic.schedule import Schedule
from b_asic.sfg_generators.wave_digital_filters import lattice_wdf
from b_asic.special_operations import Input, Output
from b_asic.wdf import lattice_coeffs_from_tf
from b_asic.wdf_operations import SymmetricTwoportAdaptor
from b_asic.quantization import quantize

b, a = iirfilter(N=7, Wn=0.3, rp=0.1, rs=60, btype="low", ftype="ellip")
adaptor_coeffs = lattice_coeffs_from_tf(a)
quantized_coeffs = [quantize(c, fractional_bits=14) for c in adaptor_coeffs]

wdf_sfg = lattice_wdf(quantized_coeffs, only_adaptors=True)
wdf_sfg.set_latency_of_type(SymmetricTwoportAdaptor, 4)
wdf_sfg.set_execution_time_of_type(SymmetricTwoportAdaptor, 1)

schedule = Schedule(wdf_sfg, cyclic=True)
schedule.move_operation('out0', 9)
schedule.move_operation('sym2p3', 4)
schedule.move_operation('sym2p3', 5)
schedule.move_operation('sym2p4', 1)
schedule.move_operation('sym2p6', 4)
schedule.move_operation('sym2p4', 7)
schedule.move_operation('sym2p1', 1)
schedule.set_schedule_time(12)
schedule.move_operation('sym2p1', 1)
schedule.move_operation('sym2p6', 1)
schedule.move_operation('sym2p5', 2)
schedule.move_operation('sym2p2', 2)
schedule.move_operation('sym2p7', 1)
schedule.move_operation('sym2p6', -1)
schedule.move_operation('sym2p4', -2)
schedule.move_operation('sym2p5', -1)
schedule.move_operation('sym2p4', -1)
schedule.move_operation('sym2p3', -4)
schedule.move_operation('sym2p6', -1)
schedule.move_operation('out0', -4)
schedule.move_operation('sym2p1', 1)
schedule.move_operation('sym2p2', 1)
schedule.move_operation('sym2p6', 1)
schedule.move_y_location('sym2p3', 9, True)
schedule.move_operation('out0', 1)
schedule.move_operation('sym2p3', 1)
schedule.move_operation('sym2p4', 1)
schedule.move_operation('sym2p4', -1)
schedule.move_operation('sym2p6', -3)
schedule.move_operation('sym2p6', 3)
schedule.move_operation('sym2p6', -2)
schedule.move_y_location('sym2p6', 3, True)
schedule.move_operation('sym2p6', 2)
schedule.move_y_location('sym2p6', 5, True)

variables = schedule.get_memory_variables()
direct, variables = variables.split_on_length()
variable_groups = variables.split_on_ports(write_ports=1, read_ports=1, total_ports=2)
mem0 = Memory(variable_groups[0], assign=True, entity_name="m0")
mem1 = Memory(variable_groups[1], assign=True, entity_name="m1")

ops = schedule.get_operations()
adaptor = ProcessingElement(ops.get_by_type(SymmetricTwoportAdaptor), entity_name="a")
input_pe = ProcessingElement(ops.get_by_type(Input), entity_name="x")
output_pe = ProcessingElement(ops.get_by_type(Output), entity_name="y")

arch = Architecture(
    processing_elements=[adaptor, input_pe, output_pe],
    memories=[mem0, mem1],
    direct_interconnects=direct,
    entity_name="wdf",
)

Generating VHDL

HDL is generated by a code printer. Currently, VHDL is supported, which is done through VhdlPrinter. The printer needs to know which DataType to use for the generated arithmetic. Here, we use 10 fractional bits, and two integer bits. Furthermore, we use magnitude truncation for quantization, and saturation for overflow handling.

from b_asic.code_printer import VhdlPrinter
from b_asic.data_type import DataType
from b_asic.quantization import QuantizationMode, OverflowMode

dt = DataType(
    wl=(2, 10),
    quantization_mode=QuantizationMode.MAGNITUDE_TRUNCATION,
    overflow_mode=OverflowMode.SATURATION,
)
printer = VhdlPrinter(dt)

Generating the VHDL files describing the architecture is then as simple as

printer.print(arch, path="generated")

This writes one file per processing element, I/O unit, and memory, along with a top-level file describing arch itself. get_compile_order() returns the resulting file names in the order in which they can be compiled.

['m0.vhdl', 'm1.vhdl', 'a.vhdl', 'x.vhdl', 'y.vhdl', 'wdf.vhdl']

Let’s have a look at the generated code, by reading back the file that was written.

from pathlib import Path

Top-level file, containing all units found below, top-level muxes, and the schedule counter.

print(Path("generated", "wdf.vhdl").read_text())
--
-- This code was automatically generated by the B-ASIC toolbox.
-- Code generation timestamp: (2026-06-25 08:19:29.392463)
-- B-ASIC version: 0.1.0.dev983+unknown.gc02664293
-- URL: https://github.com/b-asic-eda/b-asic
--

library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

entity wdf is
    port (
        clk : in std_logic;
        rst : in std_logic;
        en : in std_logic;
        x_0_in : in std_logic_vector(11 downto 0);
        y_0_out : out std_logic_vector(11 downto 0)
    );
end entity wdf;

architecture rtl of wdf is
    -- Component declaration
    component a
        port (
            clk : in std_logic;
            en : in std_logic;
            schedule_cnt : in unsigned(3 downto 0);
            p_0_in : in signed(11 downto 0);
            p_1_in : in signed(11 downto 0);
            p_0_out : out signed(11 downto 0);
            p_1_out : out signed(11 downto 0)
        );
    end component;

    component x
        port (
            clk : in std_logic;
            en : in std_logic;
            schedule_cnt : in unsigned(3 downto 0);
            p_0_in : in std_logic_vector(11 downto 0);
            p_0_out : out signed(11 downto 0)
        );
    end component;

    component y
        port (
            clk : in std_logic;
            en : in std_logic;
            schedule_cnt : in unsigned(3 downto 0);
            p_0_in : in signed(11 downto 0);
            p_0_out : out std_logic_vector(11 downto 0)
        );
    end component;

    component m0
        port (
            clk : in std_logic;
            en : in std_logic;
            schedule_cnt : in unsigned(3 downto 0);
            p_0_in : in signed(11 downto 0);
            p_0_out : out signed(11 downto 0)
        );
    end component;

    component m1
        port (
            clk : in std_logic;
            en : in std_logic;
            schedule_cnt : in unsigned(3 downto 0);
            p_0_in : in signed(11 downto 0);
            p_0_out : out signed(11 downto 0)
        );
    end component;

    -- a signals
    signal a_0_in : signed(11 downto 0);
    signal a_1_in : signed(11 downto 0);
    signal a_0_out : signed(11 downto 0);
    signal a_1_out : signed(11 downto 0);

    -- x signals
    signal x_0_out : signed(11 downto 0);

    -- y signals
    signal y_0_in : signed(11 downto 0);

    -- m0 signals
    signal m0_0_in : signed(11 downto 0);
    signal m0_0_out : signed(11 downto 0);

    -- m1 signals
    signal m1_0_in : signed(11 downto 0);
    signal m1_0_out : signed(11 downto 0);

    signal schedule_cnt : unsigned(3 downto 0) := (others => '0');

    -- Multiplexer control signals
    signal a_0_sel : std_logic_vector(1 downto 0);
    signal a_1_sel : std_logic_vector(1 downto 0);
    signal m0_0_sel : std_logic_vector(1 downto 0);
    signal m1_0_sel : std_logic_vector(0 downto 0);

begin
    -- Component instantiation
    a_inst: a
        port map (
            clk => clk,
            en => en,
            schedule_cnt => schedule_cnt,
            p_0_in => a_0_in,
            p_1_in => a_1_in,
            p_0_out => a_0_out,
            p_1_out => a_1_out
        );

    x_inst: x
        port map (
            clk => clk,
            en => en,
            schedule_cnt => schedule_cnt,
            p_0_in => x_0_in,
            p_0_out => x_0_out
        );

    y_inst: y
        port map (
            clk => clk,
            en => en,
            schedule_cnt => schedule_cnt,
            p_0_in => y_0_in,
            p_0_out => y_0_out
        );

    m0_inst: m0
        port map (
            clk => clk,
            en => en,
            schedule_cnt => schedule_cnt,
            p_0_in => m0_0_in,
            p_0_out => m0_0_out
        );

    m1_inst: m1
        port map (
            clk => clk,
            en => en,
            schedule_cnt => schedule_cnt,
            p_0_in => m1_0_in,
            p_0_out => m1_0_out
        );

    -- Schedule counter
    schedule_cnt_proc: process(clk)
    begin
        if rising_edge(clk) then
            if rst = '1' then
                schedule_cnt <= (others => '0');
            elsif en = '1' then
                if schedule_cnt = 11 then
                    schedule_cnt <= (others => '0');
                else
                    schedule_cnt <= schedule_cnt + 1;
                end if;
            end if;
        end if;
    end process schedule_cnt_proc;

    -- Multiplexer control signal generation
    with schedule_cnt select
        a_0_sel <=
            "00" when "0000",
            "01" when "0001",
            "10" when "0010",
            "01" when "0011",
            "10" when "0100",
            "10" when "0101",
            "01" when "0111",
            "11" when "1001",
            "--" when others;

    with schedule_cnt select
        a_1_sel <=
            "00" when "0000",
            "00" when "0001",
            "01" when "0010",
            "00" when "0011",
            "01" when "0100",
            "10" when "0101",
            "10" when "0111",
            "00" when "1001",
            "--" when others;

    with schedule_cnt select
        m0_0_sel <=
            "00" when "0000",
            "01" when "0001",
            "01" when "0100",
            "01" when "0101",
            "01" when "0111",
            "10" when "1000",
            "10" when "1011",
            "--" when others;

    with schedule_cnt select
        m1_0_sel <=
            "0" when "0001",
            "0" when "0100",
            "1" when "1000",
            "1" when "1001",
            "1" when "1011",
            "-" when others;

    -- Interconnect
    with a_0_sel select
        a_0_in <=
            x_0_out when "00",
            m1_0_out when "01",
            m0_0_out when "10",
            a_0_out when "11",
            (others => '-') when others;

    with a_1_sel select
        a_1_in <=
            m0_0_out when "00",
            m1_0_out when "01",
            a_0_out when "10",
            (others => '-') when others;

    y_0_in <= a_0_out;

    with m0_0_sel select
        m0_0_in <=
            x_0_out when "00",
            a_1_out when "01",
            a_0_out when "10",
            (others => '-') when others;

    with m1_0_sel select
        m1_0_in <=
            a_0_out when "0",
            a_1_out when "1",
            (others => '-') when others;

end architecture rtl;

Memories

print(Path("generated", "m0.vhdl").read_text())
print(Path("generated", "m1.vhdl").read_text())
--
-- This code was automatically generated by the B-ASIC toolbox.
-- Code generation timestamp: (2026-06-25 08:19:29.391860)
-- B-ASIC version: 0.1.0.dev983+unknown.gc02664293
-- URL: https://github.com/b-asic-eda/b-asic
--

library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

entity m0 is
    port (
        clk : in std_logic;
        en : in std_logic;
        schedule_cnt : in unsigned(3 downto 0);
        p_0_in : in signed(11 downto 0);
        p_0_out : out signed(11 downto 0)
    );
end entity m0;

architecture rtl of m0 is

    -- HDL memory description
    type mem_type is array(0 to 4) of signed(11 downto 0);
    signal memory : mem_type := (others => (others => '0'));

    -- Memory address generation
    signal read_port_0 : signed(11 downto 0);
    signal read_adr_0 : unsigned(2 downto 0);
    signal write_port_0 : signed(11 downto 0);
    signal write_adr_0 : unsigned(2 downto 0);
    signal write_en_0 : std_logic;

    -- Address generation multiplexing signals
    signal write_adr_0_0_0 : unsigned(2 downto 0);
    signal write_en_0_0_0 : std_logic;
    signal read_adr_0_0_0 : unsigned(2 downto 0);

    -- Type conversion for interface
    signal p_0_in_internal : signed(11 downto 0) := (others => '0');
    signal p_0_out_internal : signed(11 downto 0) := (others => '0');
    signal forward_ctrl : std_logic;

begin


    -- Type conversions
    p_0_in_internal <= p_0_in;
    p_0_out <= p_0_out_internal;

    -- Memory
    mem_0_proc: process(clk)
    begin
        if rising_edge(clk) then
            if en = '1' then
                if write_en_0 = '1' then
                    memory(to_integer(write_adr_0)) <= write_port_0;
                end if;
            end if;
        end if;
    end process mem_0_proc;
    read_port_0 <= memory(to_integer(read_adr_0));
    read_adr_0 <= read_adr_0_0_0;
    write_adr_0 <= write_adr_0_0_0;
    write_en_0 <= write_en_0_0_0;
    write_port_0 <= p_0_in_internal;

    -- Input and output assignments
    output_reg_proc: process(clk)
    begin
        if rising_edge(clk) then
            if forward_ctrl = '1' then
                p_0_out_internal <= p_0_in_internal;
            else
                p_0_out_internal <= read_port_0;
            end if;
        end if;
    end process output_reg_proc;

    with schedule_cnt select
        forward_ctrl <=
        '0' when "0000",
        '0' when "0001",
        '0' when "0100",
        '0' when "0101",
        '0' when "0111",
        '1' when "1000",
        '0' when "1011",
        '-' when others;

    --
    -- Memory write address generation
    --
    mem_write_address_proc: process(schedule_cnt)
    begin
            case schedule_cnt is
                -- MemoryVariable(0, <b_asic.port.OutputPort object at 0x7fc9dc3cabc0>, {<b_asic.port.InputPort object at 0x7fc9dc125ae0>: 5}, 'in0.0')
                when "0000" =>
                    write_adr_0_0_0 <= to_unsigned(0, write_adr_0_0_0'length);
                    write_en_0_0_0 <= '1';
                -- MemoryVariable(1, <b_asic.port.OutputPort object at 0x7fc9dc127480>, {<b_asic.port.InputPort object at 0x7fc9dc06ee40>: 3}, 'sym2p4.1')
                when "0001" =>
                    write_adr_0_0_0 <= to_unsigned(1, write_adr_0_0_0'length);
                    write_en_0_0_0 <= '1';
                -- MemoryVariable(4, <b_asic.port.OutputPort object at 0x7fc9dc3ca7b0>, {<b_asic.port.InputPort object at 0x7fc9dc3c93b0>: 8}, 'sym2p0.1')
                when "0100" =>
                    write_adr_0_0_0 <= to_unsigned(1, write_adr_0_0_0'length);
                    write_en_0_0_0 <= '1';
                -- MemoryVariable(5, <b_asic.port.OutputPort object at 0x7fc9dc06fac0>, {<b_asic.port.InputPort object at 0x7fc9dc06fbb0>: 8}, 'sym2p7.1')
                when "0101" =>
                    write_adr_0_0_0 <= to_unsigned(2, write_adr_0_0_0'length);
                    write_en_0_0_0 <= '1';
                -- MemoryVariable(7, <b_asic.port.OutputPort object at 0x7fc9dc1e0730>, {<b_asic.port.InputPort object at 0x7fc9dc3c8910>: 8}, 'sym2p2.1')
                when "0111" =>
                    write_adr_0_0_0 <= to_unsigned(3, write_adr_0_0_0'length);
                    write_en_0_0_0 <= '1';
                -- MemoryVariable(8, <b_asic.port.OutputPort object at 0x7fc9dc06c5f0>, {<b_asic.port.InputPort object at 0x7fc9dc1e3660>: 1}, 'sym2p6.0')
                when "1000" =>
                    write_adr_0_0_0 <= to_unsigned(0, write_adr_0_0_0'length);
                    write_en_0_0_0 <= '1';
                -- MemoryVariable(11, <b_asic.port.OutputPort object at 0x7fc9dc3ca120>, {<b_asic.port.InputPort object at 0x7fc9dc1e1ef0>: 3}, 'sym2p1.0')
                when "1011" =>
                    write_adr_0_0_0 <= to_unsigned(4, write_adr_0_0_0'length);
                    write_en_0_0_0 <= '1';
                when others =>
                    write_adr_0_0_0 <= (others => '-');
                    write_en_0_0_0 <= '0';
            end case;
    end process mem_write_address_proc;


    --
    -- Memory read address generation
    --
    mem_read_address_proc: process(schedule_cnt)
    begin
            case schedule_cnt is
                -- MemoryVariable(4, <b_asic.port.OutputPort object at 0x7fc9dc3ca7b0>, {<b_asic.port.InputPort object at 0x7fc9dc3c93b0>: 8}, 'sym2p0.1')
                when "1011" =>
                    read_adr_0_0_0 <= to_unsigned(1, read_adr_0_0_0'length);
                -- MemoryVariable(5, <b_asic.port.OutputPort object at 0x7fc9dc06fac0>, {<b_asic.port.InputPort object at 0x7fc9dc06fbb0>: 8}, 'sym2p7.1')
                when "0000" =>
                    read_adr_0_0_0 <= to_unsigned(2, read_adr_0_0_0'length);
                -- MemoryVariable(11, <b_asic.port.OutputPort object at 0x7fc9dc3ca120>, {<b_asic.port.InputPort object at 0x7fc9dc1e1ef0>: 3}, 'sym2p1.0')
                when "0001" =>
                    read_adr_0_0_0 <= to_unsigned(4, read_adr_0_0_0'length);
                -- MemoryVariable(7, <b_asic.port.OutputPort object at 0x7fc9dc1e0730>, {<b_asic.port.InputPort object at 0x7fc9dc3c8910>: 8}, 'sym2p2.1')
                when "0010" =>
                    read_adr_0_0_0 <= to_unsigned(3, read_adr_0_0_0'length);
                -- MemoryVariable(1, <b_asic.port.OutputPort object at 0x7fc9dc127480>, {<b_asic.port.InputPort object at 0x7fc9dc06ee40>: 3}, 'sym2p4.1')
                when "0011" =>
                    read_adr_0_0_0 <= to_unsigned(1, read_adr_0_0_0'length);
                -- MemoryVariable(0, <b_asic.port.OutputPort object at 0x7fc9dc3cabc0>, {<b_asic.port.InputPort object at 0x7fc9dc125ae0>: 5}, 'in0.0')
                when "0100" =>
                    read_adr_0_0_0 <= to_unsigned(0, read_adr_0_0_0'length);
                -- MemoryVariable(8, <b_asic.port.OutputPort object at 0x7fc9dc06c5f0>, {<b_asic.port.InputPort object at 0x7fc9dc1e3660>: 1}, 'sym2p6.0')
                when "1000" =>
                    read_adr_0_0_0 <= to_unsigned(0, read_adr_0_0_0'length);
                when others =>
                    read_adr_0_0_0 <= to_unsigned(0, read_adr_0_0_0'length);
            end case;
    end process mem_read_address_proc;

end architecture rtl;


--
-- This code was automatically generated by the B-ASIC toolbox.
-- Code generation timestamp: (2026-06-25 08:19:29.392193)
-- B-ASIC version: 0.1.0.dev983+unknown.gc02664293
-- URL: https://github.com/b-asic-eda/b-asic
--

library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

entity m1 is
    port (
        clk : in std_logic;
        en : in std_logic;
        schedule_cnt : in unsigned(3 downto 0);
        p_0_in : in signed(11 downto 0);
        p_0_out : out signed(11 downto 0)
    );
end entity m1;

architecture rtl of m1 is

    -- HDL memory description
    type mem_type is array(0 to 2) of signed(11 downto 0);
    signal memory : mem_type := (others => (others => '0'));

    -- Memory address generation
    signal read_port_0 : signed(11 downto 0);
    signal read_adr_0 : unsigned(1 downto 0);
    signal write_port_0 : signed(11 downto 0);
    signal write_adr_0 : unsigned(1 downto 0);
    signal write_en_0 : std_logic;

    -- Address generation multiplexing signals
    signal write_adr_0_0_0 : unsigned(1 downto 0);
    signal write_en_0_0_0 : std_logic;
    signal read_adr_0_0_0 : unsigned(1 downto 0);

    -- Type conversion for interface
    signal p_0_in_internal : signed(11 downto 0) := (others => '0');
    signal p_0_out_internal : signed(11 downto 0) := (others => '0');
    signal forward_ctrl : std_logic;

begin


    -- Type conversions
    p_0_in_internal <= p_0_in;
    p_0_out <= p_0_out_internal;

    -- Memory
    mem_0_proc: process(clk)
    begin
        if rising_edge(clk) then
            if en = '1' then
                if write_en_0 = '1' then
                    memory(to_integer(write_adr_0)) <= write_port_0;
                end if;
            end if;
        end if;
    end process mem_0_proc;
    read_port_0 <= memory(to_integer(read_adr_0));
    read_adr_0 <= read_adr_0_0_0;
    write_adr_0 <= write_adr_0_0_0;
    write_en_0 <= write_en_0_0_0;
    write_port_0 <= p_0_in_internal;

    -- Input and output assignments
    output_reg_proc: process(clk)
    begin
        if rising_edge(clk) then
            if forward_ctrl = '1' then
                p_0_out_internal <= p_0_in_internal;
            else
                p_0_out_internal <= read_port_0;
            end if;
        end if;
    end process output_reg_proc;

    with schedule_cnt select
        forward_ctrl <=
        '1' when "0001",
        '0' when "0100",
        '0' when "1000",
        '0' when "1001",
        '0' when "1011",
        '-' when others;

    --
    -- Memory write address generation
    --
    mem_write_address_proc: process(schedule_cnt)
    begin
            case schedule_cnt is
                -- MemoryVariable(1, <b_asic.port.OutputPort object at 0x7fc9dc1277a0>, {<b_asic.port.InputPort object at 0x7fc9dc1e0d70>: 1}, 'sym2p4.0')
                when "0001" =>
                    write_adr_0_0_0 <= to_unsigned(0, write_adr_0_0_0'length);
                    write_en_0_0_0 <= '1';
                -- MemoryVariable(4, <b_asic.port.OutputPort object at 0x7fc9dc3cb0c0>, {<b_asic.port.InputPort object at 0x7fc9dc3c8d20>: 3}, 'sym2p0.0')
                when "0100" =>
                    write_adr_0_0_0 <= to_unsigned(0, write_adr_0_0_0'length);
                    write_en_0_0_0 <= '1';
                -- MemoryVariable(8, <b_asic.port.OutputPort object at 0x7fc9dc06e7b0>, {<b_asic.port.InputPort object at 0x7fc9dc06eb70>: 8}, 'sym2p6.1')
                when "1000" =>
                    write_adr_0_0_0 <= to_unsigned(1, write_adr_0_0_0'length);
                    write_en_0_0_0 <= '1';
                -- MemoryVariable(9, <b_asic.port.OutputPort object at 0x7fc9dc1240f0>, {<b_asic.port.InputPort object at 0x7fc9dc06f8e0>: 4}, 'sym2p5.1')
                when "1001" =>
                    write_adr_0_0_0 <= to_unsigned(0, write_adr_0_0_0'length);
                    write_en_0_0_0 <= '1';
                -- MemoryVariable(11, <b_asic.port.OutputPort object at 0x7fc9dc3c8370>, {<b_asic.port.InputPort object at 0x7fc9dc3ca030>: 4}, 'sym2p1.1')
                when "1011" =>
                    write_adr_0_0_0 <= to_unsigned(2, write_adr_0_0_0'length);
                    write_en_0_0_0 <= '1';
                when others =>
                    write_adr_0_0_0 <= (others => '-');
                    write_en_0_0_0 <= '0';
            end case;
    end process mem_write_address_proc;


    --
    -- Memory read address generation
    --
    mem_read_address_proc: process(schedule_cnt)
    begin
            case schedule_cnt is
                -- MemoryVariable(9, <b_asic.port.OutputPort object at 0x7fc9dc1240f0>, {<b_asic.port.InputPort object at 0x7fc9dc06f8e0>: 4}, 'sym2p5.1')
                when "0000" =>
                    read_adr_0_0_0 <= to_unsigned(0, read_adr_0_0_0'length);
                -- MemoryVariable(1, <b_asic.port.OutputPort object at 0x7fc9dc1277a0>, {<b_asic.port.InputPort object at 0x7fc9dc1e0d70>: 1}, 'sym2p4.0')
                when "0001" =>
                    read_adr_0_0_0 <= to_unsigned(0, read_adr_0_0_0'length);
                -- MemoryVariable(11, <b_asic.port.OutputPort object at 0x7fc9dc3c8370>, {<b_asic.port.InputPort object at 0x7fc9dc3ca030>: 4}, 'sym2p1.1')
                when "0010" =>
                    read_adr_0_0_0 <= to_unsigned(2, read_adr_0_0_0'length);
                -- MemoryVariable(8, <b_asic.port.OutputPort object at 0x7fc9dc06e7b0>, {<b_asic.port.InputPort object at 0x7fc9dc06eb70>: 8}, 'sym2p6.1')
                when "0011" =>
                    read_adr_0_0_0 <= to_unsigned(1, read_adr_0_0_0'length);
                -- MemoryVariable(4, <b_asic.port.OutputPort object at 0x7fc9dc3cb0c0>, {<b_asic.port.InputPort object at 0x7fc9dc3c8d20>: 3}, 'sym2p0.0')
                when "0110" =>
                    read_adr_0_0_0 <= to_unsigned(0, read_adr_0_0_0'length);
                when others =>
                    read_adr_0_0_0 <= to_unsigned(0, read_adr_0_0_0'length);
            end case;
    end process mem_read_address_proc;

end architecture rtl;

Adaptor PE

print(Path("generated", "a.vhdl").read_text())
--
-- This code was automatically generated by the B-ASIC toolbox.
-- Code generation timestamp: (2026-06-25 08:19:29.390962)
-- B-ASIC version: 0.1.0.dev983+unknown.gc02664293
-- URL: https://github.com/b-asic-eda/b-asic
--

library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

entity a is
    port (
        clk : in std_logic;
        en : in std_logic;
        schedule_cnt : in unsigned(3 downto 0);
        p_0_in : in signed(11 downto 0);
        p_1_in : in signed(11 downto 0);
        p_0_out : out signed(11 downto 0);
        p_1_out : out signed(11 downto 0)
    );
end entity a;

architecture rtl of a is
    signal res_overflow_0_reg_0 : signed(11 downto 0) := (others => '0');
    signal res_overflow_1_reg_0 : signed(11 downto 0) := (others => '0');
    signal p_0_in_reg_0 : signed(11 downto 0) := (others => '0');
    signal p_1_in_reg_0 : signed(11 downto 0) := (others => '0');
    signal op_0 : signed(11 downto 0);
    signal op_1 : signed(11 downto 0);
    signal value : signed(14 downto 0) := (others => '0');
    constant WL_VALUE_INT : integer := 1;
    signal u0 : signed(12 downto 0);
    signal mul_res : signed(26 downto 0);
    signal res_arith_0_comb : signed(27 downto 0);
    signal res_arith_1_comb : signed(27 downto 0);
    signal res_arith_0 : signed(27 downto 0) := (others => '0');
    signal res_arith_1 : signed(27 downto 0) := (others => '0');
    signal mul_res_p : signed(26 downto 0) := (others => '0');
    signal op_0_p : signed(11 downto 0) := (others => '0');
    signal op_1_p : signed(11 downto 0) := (others => '0');
    signal res_quant_0 : signed(13 downto 0);
    signal res_overflow_0 : signed(11 downto 0);
    signal pos_overflow_0, neg_overflow_0 : std_logic;
    signal res_quant_1 : signed(13 downto 0);
    signal res_overflow_1 : signed(11 downto 0);
    signal pos_overflow_1, neg_overflow_1 : std_logic;

begin
    process(clk)
    begin
        if rising_edge(clk) then
            if en = '1' then
                res_overflow_0_reg_0 <= res_overflow_0;
                res_overflow_1_reg_0 <= res_overflow_1;
                p_0_in_reg_0 <= p_0_in;
                p_1_in_reg_0 <= p_1_in;
            end if;
        end if;
    end process;
    op_0 <= p_0_in_reg_0;
    op_1 <= p_1_in_reg_0;
    with schedule_cnt select
        value <=
            b"101111111101110" when "1010",
            b"010010001011111" when "0010",
            b"011000111110011" when "0101",
            b"100010011101000" when "0110",
            b"110000000000000" when "0011",
            b"010011011011101" when "0001",
            b"010100010101100" when "0100",
            b"101000010110110" when "1000",
            (others => '-') when others;

    p_0_out <= res_overflow_0_reg_0;
    p_1_out <= res_overflow_1_reg_0;
    u0 <= resize(op_1, 13) - resize(op_0, 13);
    mul_res <= resize(u0 * value, mul_res'length);
    process(clk)
    begin
        if rising_edge(clk) then
            if en = '1' then
                mul_res_p <= mul_res;
                op_0_p <= op_0;
                op_1_p <= op_1;
            end if;
        end if;
    end process;
    res_arith_1_comb <= (resize(op_0_p, op_0_p'length + 2) & "00000000000000") + resize(mul_res_p, res_arith_0'length);
    res_arith_0_comb <= (resize(op_1_p, op_1_p'length + 2) & "00000000000000") + resize(mul_res_p, res_arith_1'length);
    process(clk)
    begin
        if rising_edge(clk) then
            if en = '1' then
                res_arith_0 <= res_arith_0_comb;
                res_arith_1 <= res_arith_1_comb;
            end if;
        end if;
    end process;
    res_quant_0 <= res_arith_0(27 downto 14) + (to_signed(0, 13) & res_arith_0(27));
    pos_overflow_0 <= '1' when res_quant_0(13 downto 12) /= (1 downto 0 => res_quant_0(11)) and res_quant_0(13) = '0' else '0';
    neg_overflow_0 <= '1' when res_quant_0(13 downto 12) /= (1 downto 0 => res_quant_0(11)) and res_quant_0(13) = '1' else '0';
    res_overflow_0 <= to_signed(2047, 12) when pos_overflow_0 = '1' else to_signed(-2048, 12) when neg_overflow_0 = '1' else res_quant_0(11 downto 0);
    res_quant_1 <= res_arith_1(27 downto 14) + (to_signed(0, 13) & res_arith_1(27));
    pos_overflow_1 <= '1' when res_quant_1(13 downto 12) /= (1 downto 0 => res_quant_1(11)) and res_quant_1(13) = '0' else '0';
    neg_overflow_1 <= '1' when res_quant_1(13 downto 12) /= (1 downto 0 => res_quant_1(11)) and res_quant_1(13) = '1' else '0';
    res_overflow_1 <= to_signed(2047, 12) when pos_overflow_1 = '1' else to_signed(-2048, 12) when neg_overflow_1 = '1' else res_quant_1(11 downto 0);

end architecture rtl;

I/O units

print(Path("generated", "x.vhdl").read_text())
print(Path("generated", "y.vhdl").read_text())
--
-- This code was automatically generated by the B-ASIC toolbox.
-- Code generation timestamp: (2026-06-25 08:19:29.391510)
-- B-ASIC version: 0.1.0.dev983+unknown.gc02664293
-- URL: https://github.com/b-asic-eda/b-asic
--

library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

entity x is
    port (
        clk : in std_logic;
        en : in std_logic;
        schedule_cnt : in unsigned(3 downto 0);
        p_0_in : in std_logic_vector(11 downto 0);
        p_0_out : out signed(11 downto 0)
    );
end entity x;

architecture rtl of x is
    signal res_arith_0 : signed(11 downto 0);
    signal res_quant_0 : signed(11 downto 0);
    signal res_overflow_0 : signed(11 downto 0);

begin
    p_0_out <= res_overflow_0;
    res_arith_0 <= resize(signed(p_0_in), 12);
    res_quant_0 <= res_arith_0(11 downto 0);
    res_overflow_0 <= res_quant_0(11 downto 0);

end architecture rtl;


--
-- This code was automatically generated by the B-ASIC toolbox.
-- Code generation timestamp: (2026-06-25 08:19:29.391703)
-- B-ASIC version: 0.1.0.dev983+unknown.gc02664293
-- URL: https://github.com/b-asic-eda/b-asic
--

library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

entity y is
    port (
        clk : in std_logic;
        en : in std_logic;
        schedule_cnt : in unsigned(3 downto 0);
        p_0_in : in signed(11 downto 0);
        p_0_out : out std_logic_vector(11 downto 0)
    );
end entity y;

architecture rtl of y is
    signal op_0 : signed(11 downto 0);
    signal res_arith_0 : signed(11 downto 0);
    signal res_quant_0 : signed(11 downto 0);
    signal res_overflow_0 : signed(11 downto 0);

begin
    op_0 <= p_0_in;
    res_arith_0 <= op_0;
    p_0_out <= std_logic_vector(res_overflow_0);
    res_quant_0 <= res_arith_0(11 downto 0);
    res_overflow_0 <= res_quant_0(11 downto 0);

end architecture rtl;

A number of keyword arguments can be passed to print() to alter the generated code, e.g., to add registers on I/O ports, pipeline mux control signals for the top-level, or forcing a certain register-placement for PEs.

Generating a Testbench

To verify that the generated VHDL behaves as expected, a testbench can be generated from a finite-wordlength Simulation of the SFG, using the same DataType as for the code generation. Here, we drive the filter with a uniform input sequence for a few repetitions of the schedule period. Note that we use sfg here, which is the recreated SFG after scheduling. This is because, the scheduling pipelined/retimed the algorithm.

from b_asic.signal_generator import Uniform
from b_asic.simulation import Simulation

sim = Simulation(schedule.sfg, [Uniform()], dt)
sim.run_for(10 * schedule.schedule_time)

The simulation results are then handed to a CocotbPrinter, which generates a cocotb testbench that drives the architecture and asserts that its outputs match the simulated values. If a VHDL testbench is preferred, VhdlTbPrinter is also available.

from b_asic.tb_printer import CocotbPrinter

cocotb_printer = CocotbPrinter(sim.results)
cocotb_printer.print(arch, schedule, path="generated", simulator = "ghdl", waves=True)

This writes a tb.py file containing the testbench. Let’s have a look at the generated testbench.

print(Path("generated", "tb.py").read_text())
"""cocotb testbench generated by B-ASIC."""

import csv
import sys
from contextlib import nullcontext
from pathlib import Path

import cocotb
import pytest
from cocotb.clock import Clock
from cocotb.triggers import FallingEdge
from cocotb_tools.runner import get_runner


def test_start():
    runner = get_runner(SIMULATOR)
    runner.build(sources=SOURCES, hdl_toplevel=ENTITY_NAME)
    runner.test(hdl_toplevel=ENTITY_NAME, test_module="tb", waves=WAVES, gui=GUI)


SOURCES = [Path("m0.vhdl"), Path("m1.vhdl"), Path("a.vhdl"), Path("x.vhdl"), Path("y.vhdl"), Path("wdf.vhdl")]
SIMULATOR = "ghdl"
WAVES = True
GUI = False
CSV = False
ASSERTS = True
ENABLE_PIN = True
ENTITY_NAME = "wdf"
SEQUENCE = {   0: {'x_0_in': 309},
    6: {'y_0_out': 0},
    12: {'x_0_in': 3440},
    18: {'y_0_out': 2},
    24: {'x_0_in': 3514},
    30: {'y_0_out': 4},
    36: {'x_0_in': 3206},
    42: {'y_0_out': 0},
    48: {'x_0_in': 3956},
    54: {'y_0_out': 4069},
    60: {'x_0_in': 615},
    66: {'y_0_out': 3991},
    72: {'x_0_in': 3286},
    78: {'y_0_out': 3890},
    84: {'x_0_in': 496},
    90: {'y_0_out': 3683},
    96: {'x_0_in': 3905},
    102: {'y_0_out': 3646},
    108: {'x_0_in': 3153},
    114: {'y_0_out': 3711},
    120: {'x_0_in': 3081},
    126: {'y_0_out': 3846},
    132: {'x_0_in': 467},
    138: {'y_0_out': 3974},
    144: {'x_0_in': 3906},
    150: {'y_0_out': 4048},
    156: {'x_0_in': 607},
    162: {'y_0_out': 3939},
    168: {'x_0_in': 843},
    174: {'y_0_out': 3859},
    180: {'x_0_in': 3770},
    186: {'y_0_out': 3769},
    192: {'x_0_in': 244},
    198: {'y_0_out': 3761},
    204: {'x_0_in': 11},
    210: {'y_0_out': 4048},
    216: {'x_0_in': 3089},
    222: {'y_0_out': 137},
    228: {'x_0_in': 3103},
    234: {'y_0_out': 300},
    240: {'x_0_in': 794},
    246: {'y_0_out': 348},
    252: {'x_0_in': 809},
    258: {'y_0_out': 174},
    264: {'x_0_in': 199},
    270: {'y_0_out': 3967},
    276: {'x_0_in': 546},
    282: {'y_0_out': 3847},
    288: {'x_0_in': 265},
    294: {'y_0_out': 3825},
    300: {'x_0_in': 3685},
    306: {'y_0_out': 3977},
    312: {'x_0_in': 3830},
    318: {'y_0_out': 155},
    324: {'x_0_in': 804},
    330: {'y_0_out': 392},
    336: {'x_0_in': 3926},
    342: {'y_0_out': 512},
    348: {'x_0_in': 3636},
    354: {'y_0_out': 318},
    360: {'x_0_in': 3331},
    366: {'y_0_out': 181},
    372: {'x_0_in': 723},
    378: {'y_0_out': 30},
    384: {'x_0_in': 3764},
    390: {'y_0_out': 4000},
    396: {'x_0_in': 4039},
    402: {'y_0_out': 3954},
    408: {'x_0_in': 3164},
    414: {'y_0_out': 3937},
    420: {'x_0_in': 730},
    426: {'y_0_out': 3934},
    432: {'x_0_in': 280},
    438: {'y_0_out': 3937},
    444: {'x_0_in': 3508},
    450: {'y_0_out': 3945},
    456: {'x_0_in': 765},
    462: {'y_0_out': 3961},
    468: {'x_0_in': 3751},
    474: {'y_0_out': 3995},
    480: {'x_0_in': 612},
    486: {'y_0_out': 4051},
    492: {'x_0_in': 969},
    498: {'y_0_out': 29},
    504: {'x_0_in': 206},
    510: {'y_0_out': 114},
    516: {'x_0_in': 3332},
    522: {'y_0_out': 199},
    528: {'x_0_in': 3241},
    534: {'y_0_out': 263},
    540: {'x_0_in': 378},
    546: {'y_0_out': 329},
    552: {'x_0_in': 42},
    558: {'y_0_out': 278},
    564: {'x_0_in': 850},
    570: {'y_0_out': 102},
    576: {'x_0_in': 3961},
    582: {'y_0_out': 4070},
    588: {'x_0_in': 3280},
    594: {'y_0_out': 3957},
    600: {'x_0_in': 3968},
    606: {'y_0_out': 3898},
    612: {'x_0_in': 235},
    618: {'y_0_out': 3996},
    624: {'x_0_in': 520},
    630: {'y_0_out': 4091},
    636: {'x_0_in': 4030},
    642: {'y_0_out': 63},
    648: {'x_0_in': 445},
    654: {'y_0_out': 70},
    660: {'x_0_in': 3506},
    666: {'y_0_out': 4089},
    672: {'x_0_in': 3428},
    678: {'y_0_out': 4047},
    684: {'x_0_in': 658},
    690: {'y_0_out': 4059},
    696: {'x_0_in': 3512},
    702: {'y_0_out': 42},
    708: {'x_0_in': 3088},
    714: {'y_0_out': 54},
    720: {'x_0_in': 484},
    726: {'y_0_out': 31},
    732: {'x_0_in': 486},
    738: {'y_0_out': 4006},
    744: {'x_0_in': 3887},
    750: {'y_0_out': 3853},
    756: {'x_0_in': 953},
    762: {'y_0_out': 3755},
    768: {'x_0_in': 3243},
    774: {'y_0_out': 3783},
    780: {'x_0_in': 3795},
    786: {'y_0_out': 3929},
    792: {'x_0_in': 3570},
    798: {'y_0_out': 70},
    804: {'x_0_in': 722},
    810: {'y_0_out': 178},
    816: {'x_0_in': 3405},
    822: {'y_0_out': 199},
    828: {'x_0_in': 3209},
    834: {'y_0_out': 47},
    840: {'x_0_in': 3093},
    846: {'y_0_out': 4002},
    852: {'x_0_in': 4017},
    858: {'y_0_out': 3851},
    864: {'x_0_in': 3361},
    870: {'y_0_out': 3719},
    876: {'x_0_in': 3134},
    882: {'y_0_out': 3677},
    888: {'x_0_in': 3680},
    894: {'y_0_out': 3603},
    900: {'x_0_in': 768},
    906: {'y_0_out': 3504},
    912: {'x_0_in': 385},
    918: {'y_0_out': 3428},
    924: {'x_0_in': 3305},
    930: {'y_0_out': 3510},
    936: {'x_0_in': 543},
    942: {'y_0_out': 3569},
    948: {'x_0_in': 536},
    954: {'y_0_out': 3859},
    960: {'x_0_in': 105},
    966: {'y_0_out': 4016},
    972: {'x_0_in': 3529},
    978: {'y_0_out': 62},
    984: {'x_0_in': 974},
    990: {'y_0_out': 173},
    996: {'x_0_in': 71},
    1002: {'y_0_out': 230},
    1008: {'x_0_in': 3270},
    1014: {'y_0_out': 230},
    1020: {'x_0_in': 3180},
    1026: {'y_0_out': 189},
    1032: {'x_0_in': 3396},
    1038: {'y_0_out': 124},
    1044: {'x_0_in': 861},
    1050: {'y_0_out': 22},
    1056: {'x_0_in': 3425},
    1062: {'y_0_out': 4050},
    1068: {'x_0_in': 660},
    1074: {'y_0_out': 3786},
    1080: {'x_0_in': 79},
    1086: {'y_0_out': 3747},
    1092: {'x_0_in': 4069},
    1098: {'y_0_out': 3757},
    1104: {'x_0_in': 287},
    1110: {'y_0_out': 3835},
    1116: {'x_0_in': 616},
    1122: {'y_0_out': 3986},
    1128: {'x_0_in': 3784},
    1134: {'y_0_out': 79},
    1140: {'x_0_in': 3284},
    1146: {'y_0_out': 243},
    1152: {'x_0_in': 117},
    1158: {'y_0_out': 319},
    1164: {'x_0_in': 3690},
    1170: {'y_0_out': 281},
    1176: {'x_0_in': 3843},
    1182: {'y_0_out': 142},
    1188: {'x_0_in': 3292},
    1194: {'y_0_out': 4052},
    1200: {'x_0_in': 811},
    1206: {'y_0_out': 3872},
    1212: {'x_0_in': 212},
    1218: {'y_0_out': 3750},
    1224: {'x_0_in': 101},
    1230: {'y_0_out': 3710},
    1236: {'x_0_in': 3995},
    1242: {'y_0_out': 3759},
    1248: {'x_0_in': 544},
    1254: {'y_0_out': 3885},
    1260: {'x_0_in': 714},
    1266: {'y_0_out': 4057},
    1272: {'x_0_in': 971},
    1278: {'y_0_out': 142},
    1284: {'x_0_in': 3857},
    1290: {'y_0_out': 296},
    1296: {'x_0_in': 1010},
    1302: {'y_0_out': 407},
    1308: {'x_0_in': 3689},
    1314: {'y_0_out': 416},
    1320: {'x_0_in': 3256},
    1326: {'y_0_out': 443},
    1332: {'x_0_in': 3223},
    1338: {'y_0_out': 466},
    1344: {'x_0_in': 3470},
    1350: {'y_0_out': 422},
    1356: {'x_0_in': 3223},
    1362: {'y_0_out': 307},
    1368: {'x_0_in': 739},
    1374: {'y_0_out': 4070},
    1380: {'x_0_in': 877},
    1386: {'y_0_out': 3733},
    1392: {'x_0_in': 3124},
    1398: {'y_0_out': 3456},
    1404: {'x_0_in': 3397},
    1410: {'y_0_out': 3539},
    1416: {'x_0_in': 3703},
    1422: {'y_0_out': 3814},
    1428: {'x_0_in': 3625},
    1434: {'y_0_out': 3873}}


@cocotb.test()
async def test_one(dut):
    clk = Clock(dut.clk, 2, unit="ns")
    cocotb.start_soon(clk.start())

    max_cycle = max(SEQUENCE.keys())

    dut.rst.value = 1
    if ENABLE_PIN:
        dut.en.value = 0
        await FallingEdge(dut.clk)
    dut.rst.value = 0
    if ENABLE_PIN:
        dut.en.value = 1

    # Context manager that does nothing if CSV is False
    csv_context = Path("waveform.csv").open("w", newline="") if CSV else nullcontext()  # noqa: SIM115

    with csv_context as f:
        writer = csv.writer(f) if CSV else None
        if CSV:
            writer.writerow(["port_name", "cycle", "value"])

        for cycle in range(max_cycle + 1):
            if cycle in SEQUENCE:
                step = SEQUENCE[cycle]
                # Drive inputs and check outputs based on the sequence map
                for signal_name, value in step.items():
                    if CSV:
                        hw_val = getattr(dut, signal_name).value
                    if signal_name.endswith(("_in", "_in_re", "_in_im")):
                        writer.writerow([signal_name, cycle, value]) if CSV else None
                        getattr(dut, signal_name).value = value
                    else:
                        hw_val = getattr(dut, signal_name).value
                        if CSV and hw_val.is_resolvable:
                            writer.writerow([signal_name, cycle, int(hw_val)])
                        if hw_val.is_resolvable:
                            if ASSERTS:
                                assert hw_val == value, (
                                    f"Cycle {cycle}: Expected {signal_name} to be {value}, "
                                    f"but got {int(hw_val)}"
                                )
                                cocotb.log.info(
                                    f"Cycle {cycle}: {signal_name} = {int(hw_val)} OK"
                                )
                        else:
                            # Skip assertion if value is 'x' (unknown/undefined)
                            cocotb.log.warning(
                                f"Cycle {cycle}: Skipping check for {signal_name} - "
                                f"value is unresolvable (x/z/u)"
                            )

            await FallingEdge(dut.clk)


if __name__ == "__main__":
    # forward command-line pytest options (e.g. -k, -q, -s) to allow running this file directly
    sys.exit(pytest.main([__file__, *sys.argv[1:]]))

Assuming that GHDL is correctly set up, verification should be as simple as running

python tb.py

in the generated directory, which should run the testbench and report that all assertions passed. A waveform file is also generated, which can be rendered using a waveform viewer, e.g., Surfer.

Generating a VHDL LS Configuration

To get clickable references, completion, and other IDE features for the generated VHDL code, e.g., through the VHDL by VHDL-LS VS Code extension, a vhdl_ls.toml configuration file for the VHDL Language Server can be generated with print_vhdl_ls_toml().

printer.print_vhdl_ls_toml(arch, path="generated")

Let’s have a look at the generated file.

print(Path("generated", "vhdl_ls.toml").read_text())
standard = "1993"

[libraries]
lib.files = [
    'm0.vhdl',
    'm1.vhdl',
    'a.vhdl',
    'x.vhdl',
    'y.vhdl',
    'wdf.vhdl',
]

Conclusion

Congratualations, you have now successfully designed a wave digital filter, derived a time-multiplexed architecture for it, and generated synthesizable VHDL code, along with a testbench for it. The generated code can now be synthesized and implemented on an FPGA or ASIC.

Next Steps

Take a look at Examples to see B-ASIC used in more complex situations, play around with a different schedules and assignments for the current algorithm, or try building your own architecture for a different algorithm. If you encounter any issues, or have suggestions for improvements, please open an issue on GitHub.

Total running time of the script: (0 minutes 0.083 seconds)

Gallery generated by Sphinx-Gallery