diff --git a/.gitignore b/.gitignore index 0ee9421..056b8ea 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,5 @@ __pycache__/ # build outputs build +/overrides.txt +.python-version diff --git a/chipflow_digital_ip/io/__init__.py b/chipflow_digital_ip/io/__init__.py index 8ce595c..4229f79 100644 --- a/chipflow_digital_ip/io/__init__.py +++ b/chipflow_digital_ip/io/__init__.py @@ -3,4 +3,9 @@ from ._i2c import I2CPeripheral from ._spi import SPIPeripheral -__all__ = ['GPIOPeripheral', 'UARTPeripheral', 'I2CPeripheral', 'SPIPeripheral'] +__all__ = [ + 'GPIOPeripheral', + 'UARTPeripheral', + 'I2CPeripheral', + 'SPIPeripheral', +] diff --git a/chipflow_digital_ip/io/_glasgow_iostream.py b/chipflow_digital_ip/io/_glasgow_iostream.py index cd3c4a3..65d69f8 100644 --- a/chipflow_digital_ip/io/_glasgow_iostream.py +++ b/chipflow_digital_ip/io/_glasgow_iostream.py @@ -2,8 +2,12 @@ from amaranth.lib import data, wiring, stream, io from amaranth.lib.wiring import In, Out +from amaranth_types.types import ShapeLike + + __all__ = ["IOStreamer", "PortGroup"] + class PortGroup: """Group of Amaranth library I/O ports. @@ -118,7 +122,7 @@ class IOStreamer(wiring.Component): """ @staticmethod - def o_stream_signature(ioshape, /, *, ratio=1, meta_layout=0): + def o_stream_signature(ioshape, /, *, ratio=1, meta_layout: ShapeLike = 0): return stream.Signature(data.StructLayout({ "port": _map_ioshape("o", ioshape, lambda width: data.StructLayout({ "o": width if ratio == 1 else data.ArrayLayout(width, ratio), @@ -129,7 +133,7 @@ def o_stream_signature(ioshape, /, *, ratio=1, meta_layout=0): })) @staticmethod - def i_stream_signature(ioshape, /, *, ratio=1, meta_layout=0): + def i_stream_signature(ioshape, /, *, ratio=1, meta_layout: ShapeLike = 0): return stream.Signature(data.StructLayout({ "port": _map_ioshape("i", ioshape, lambda width: data.StructLayout({ "i": width if ratio == 1 else data.ArrayLayout(width, ratio), @@ -137,8 +141,8 @@ def i_stream_signature(ioshape, /, *, ratio=1, meta_layout=0): "meta": meta_layout, })) - def __init__(self, ioshape, ports, /, *, ratio=1, init=None, meta_layout=0): - assert isinstance(ioshape, (int, dict)) + def __init__(self, ioshape: dict, ports, /, *, ratio=1, init=None, meta_layout: ShapeLike = 0): + assert isinstance(ioshape, dict) assert ratio in (1, 2) self._ioshape = ioshape @@ -161,7 +165,7 @@ def elaborate(self, platform): buffer_cls, latency = SimulatableDDRBuffer, 3 if isinstance(self._ports, io.PortLike): - m.submodules.buffer = buffer = buffer_cls("io", self._ports) + m.submodules.buffer = buffer = buffer_cls(io.Direction.Bidir, self._ports) if isinstance(self._ports, PortGroup): buffer = {} for name, sub_port in self._ports.__dict__.items(): @@ -231,7 +235,7 @@ def delay(value, name): class IOClocker(wiring.Component): @staticmethod - def i_stream_signature(ioshape, /, *, _ratio=1, meta_layout=0): + def i_stream_signature(ioshape, /, *, _ratio=1, meta_layout: ShapeLike = 0): # Currently the only supported ratio is 1, but this will change in the future for # interfaces like HyperBus. return stream.Signature(data.StructLayout({ @@ -245,10 +249,10 @@ def i_stream_signature(ioshape, /, *, _ratio=1, meta_layout=0): })) @staticmethod - def o_stream_signature(ioshape, /, *, ratio=1, meta_layout=0): + def o_stream_signature(ioshape, /, *, ratio=1, meta_layout: ShapeLike = 0): return IOStreamer.o_stream_signature(ioshape, ratio=ratio, meta_layout=meta_layout) - def __init__(self, ioshape, *, clock, o_ratio=1, meta_layout=0, divisor_width=16): + def __init__(self, ioshape, *, clock, o_ratio=1, meta_layout: ShapeLike = 0, divisor_width=16): assert isinstance(ioshape, dict) assert isinstance(clock, str) assert o_ratio in (1, 2) diff --git a/chipflow_digital_ip/io/_gpio.py b/chipflow_digital_ip/io/_gpio.py index 4fc5994..57417b7 100644 --- a/chipflow_digital_ip/io/_gpio.py +++ b/chipflow_digital_ip/io/_gpio.py @@ -1,4 +1,3 @@ - from amaranth import Module, unsigned from amaranth.lib import wiring from amaranth.lib.wiring import In, Out, flipped, connect diff --git a/chipflow_digital_ip/io/_rfc_uart.py b/chipflow_digital_ip/io/_rfc_uart.py index e08f13e..caa72bc 100644 --- a/chipflow_digital_ip/io/_rfc_uart.py +++ b/chipflow_digital_ip/io/_rfc_uart.py @@ -2,17 +2,26 @@ The Amaranth SoC RFC UART from https://github.com/ChipFlow/chipflow-digital-ip """ +from typing import Generic, TypeVar + from amaranth import * from amaranth.lib import stream, wiring from amaranth.lib.wiring import In, Out, flipped, connect +from amaranth.hdl import ValueCastable + +from amaranth_types.types import ShapeLike from amaranth_soc import csr __all__ = ["RxPhySignature", "TxPhySignature", "RxPeripheral", "TxPeripheral", "Peripheral"] +_T_ValueOrValueCastable = TypeVar("_T_ValueOrValueCastable", bound=Value | ValueCastable, covariant=True) +_T_ShapeLike = TypeVar("_T_ShapeLike", bound=ShapeLike, covariant=True) +_T_Symbol_ShapeLike = TypeVar("_T_Symbol_ShapeLike", bound=ShapeLike, covariant=True) + -class RxPhySignature(wiring.Signature): +class RxPhySignature(wiring.Signature, Generic[_T_ShapeLike, _T_Symbol_ShapeLike]): """Receiver PHY signature. Parameters @@ -38,7 +47,7 @@ class RxPhySignature(wiring.Signature): Receiver error flag. Pulsed for one clock cycle in case of an implementation-specific error (e.g. wrong parity bit). """ - def __init__(self, phy_config_shape, symbol_shape): + def __init__(self, phy_config_shape: _T_ShapeLike, symbol_shape: _T_Symbol_ShapeLike): super().__init__({ "rst": Out(1), "config": Out(phy_config_shape), @@ -48,7 +57,7 @@ def __init__(self, phy_config_shape, symbol_shape): }) -class TxPhySignature(wiring.Signature): +class TxPhySignature(wiring.Signature, Generic[_T_ShapeLike, _T_Symbol_ShapeLike]): """Transmitter PHY signature. Parameters @@ -68,7 +77,7 @@ class TxPhySignature(wiring.Signature): symbols : :py:`Out(stream.Signature(symbol_shape))` Symbol stream. The shape of its payload is given by the `symbol_shape` parameter. """ - def __init__(self, phy_config_shape, symbol_shape): + def __init__(self, phy_config_shape: _T_ShapeLike, symbol_shape: _T_Symbol_ShapeLike): super().__init__({ "rst": Out(1), "config": Out(phy_config_shape), @@ -98,7 +107,7 @@ def elaborate(self, platform): return m -class RxPeripheral(wiring.Component): +class RxPeripheral(wiring.Component, Generic[_T_ShapeLike, _T_ValueOrValueCastable, _T_Symbol_ShapeLike]): class Config(csr.Register, access="rw"): """Peripheral configuration register. @@ -141,7 +150,7 @@ class PhyConfig(csr.Register, access="rw"): phy_config_init : :class:`int` Initial value of the PHY configuration word. """ - def __init__(self, phy_config_shape, phy_config_init): + def __init__(self, phy_config_shape: _T_ShapeLike, phy_config_init: _T_ValueOrValueCastable): super().__init__(csr.Field(_PhyConfigFieldAction, phy_config_shape, init=phy_config_init)) @@ -199,7 +208,7 @@ class Data(csr.Register, access="r"): symbol_shape : :ref:`shape-like ` Shape of a symbol. """ - def __init__(self, symbol_shape): + def __init__(self, symbol_shape: _T_Symbol_ShapeLike): super().__init__(csr.Field(csr.action.R, symbol_shape)) """UART receiver peripheral. @@ -224,8 +233,8 @@ def __init__(self, symbol_shape): phy : :py:`Out(RxPhySignature(phy_config_shape, symbol_shape))` Interface between the peripheral and its PHY. """ - def __init__(self, *, addr_width, data_width, phy_config_shape=unsigned(16), - phy_config_init=0, symbol_shape=unsigned(8)): + def __init__(self, *, addr_width, data_width, phy_config_shape:_T_ShapeLike = unsigned(16), + phy_config_init: _T_ValueOrValueCastable = Value.cast(0), symbol_shape: _T_Symbol_ShapeLike = unsigned(8)): regs = csr.Builder(addr_width=addr_width, data_width=data_width) self._config = regs.add("Config", self.Config()) @@ -298,7 +307,7 @@ def elaborate(self, platform): return m -class TxPeripheral(wiring.Component): +class TxPeripheral(wiring.Component, Generic[_T_ShapeLike, _T_Symbol_ShapeLike, _T_ValueOrValueCastable]): class Config(csr.Register, access="rw"): """Peripheral configuration register. @@ -341,7 +350,7 @@ class PhyConfig(csr.Register, access="rw"): phy_config_init : :class:`int` Initial value of the PHY configuration word. """ - def __init__(self, phy_config_shape, phy_config_init): + def __init__(self, phy_config_shape: _T_ShapeLike, phy_config_init: _T_ValueOrValueCastable): super().__init__(csr.Field(_PhyConfigFieldAction, phy_config_shape, init=phy_config_init)) @@ -391,7 +400,7 @@ class Data(csr.Register, access="w"): symbol_shape : :ref:`shape-like ` Shape of a symbol. """ - def __init__(self, symbol_shape): + def __init__(self, symbol_shape: _T_Symbol_ShapeLike): super().__init__(csr.Field(csr.action.W, symbol_shape)) """UART transmitter peripheral. @@ -416,8 +425,8 @@ def __init__(self, symbol_shape): phy : :py:`Out(TxPhySignature(phy_config_shape, symbol_shape))` Interface between the peripheral and its PHY. """ - def __init__(self, *, addr_width, data_width=8, phy_config_shape=unsigned(16), - phy_config_init=0, symbol_shape=unsigned(8)): + def __init__(self, *, addr_width, data_width=8, phy_config_shape: _T_ShapeLike = unsigned(16), + phy_config_init: _T_ValueOrValueCastable = Value.cast(0), symbol_shape: _T_Symbol_ShapeLike = unsigned(8)): regs = csr.Builder(addr_width=addr_width, data_width=data_width) self._config = regs.add("Config", self.Config()) @@ -487,7 +496,7 @@ def elaborate(self, platform): return m -class Peripheral(wiring.Component): +class Peripheral(wiring.Component, Generic[_T_ShapeLike, _T_Symbol_ShapeLike, _T_ValueOrValueCastable]): """UART transceiver peripheral. This peripheral is composed of two subordinate peripherals. A :class:`RxPeripheral` occupies @@ -522,8 +531,8 @@ class Peripheral(wiring.Component): :exc:`TypeError` If ``addr_width`` is not a positive integer. """ - def __init__(self, *, addr_width, data_width=8, phy_config_shape=unsigned(16), - phy_config_init=0, symbol_shape=unsigned(8)): + def __init__(self, *, addr_width, data_width=8, phy_config_shape: _T_ShapeLike = unsigned(16), + phy_config_init: _T_ValueOrValueCastable = Value.cast(0), symbol_shape: _T_Symbol_ShapeLike = unsigned(8)): if not isinstance(addr_width, int) or addr_width <= 0: raise TypeError(f"Address width must be a positive integer, not {addr_width!r}") diff --git a/chipflow_digital_ip/io/_spi.py b/chipflow_digital_ip/io/_spi.py index 18f51bd..fec8b26 100644 --- a/chipflow_digital_ip/io/_spi.py +++ b/chipflow_digital_ip/io/_spi.py @@ -1,4 +1,4 @@ -from amaranth import Module, Signal, Cat, C, unsigned +from amaranth import * from amaranth.lib import wiring from amaranth.lib.wiring import In, Out, connect, flipped diff --git a/chipflow_digital_ip/io/sv_timer/drivers/wb_timer.h b/chipflow_digital_ip/io/sv_timer/drivers/wb_timer.h new file mode 100644 index 0000000..a15a1cd --- /dev/null +++ b/chipflow_digital_ip/io/sv_timer/drivers/wb_timer.h @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: BSD-2-Clause +// Wishbone Timer Driver Header + +#ifndef WB_TIMER_H +#define WB_TIMER_H + +#include + +// Register offsets +#define WB_TIMER_CTRL 0x00 +#define WB_TIMER_COMPARE 0x04 +#define WB_TIMER_COUNTER 0x08 +#define WB_TIMER_STATUS 0x0C + +// Control register bits +#define WB_TIMER_CTRL_ENABLE (1 << 0) +#define WB_TIMER_CTRL_IRQ_EN (1 << 1) +#define WB_TIMER_CTRL_PRESCALER_SHIFT 16 + +// Status register bits +#define WB_TIMER_STATUS_IRQ_PENDING (1 << 0) +#define WB_TIMER_STATUS_MATCH (1 << 1) + +// Register structure for SoftwareDriverSignature +typedef struct { + volatile uint32_t ctrl; // Control: [31:16] prescaler, [1] irq_en, [0] enable + volatile uint32_t compare; // Compare value for match interrupt + volatile uint32_t counter; // Current counter (read) / Reload value (write) + volatile uint32_t status; // Status: [1] match, [0] irq_pending (write 1 to clear) +} wb_timer_regs_t; + +static inline void wb_timer_init(wb_timer_regs_t *regs, uint16_t prescaler, uint32_t compare) { + regs->compare = compare; + regs->counter = 0; + regs->ctrl = ((uint32_t)prescaler << WB_TIMER_CTRL_PRESCALER_SHIFT) + | WB_TIMER_CTRL_ENABLE + | WB_TIMER_CTRL_IRQ_EN; +} + +static inline void wb_timer_enable(wb_timer_regs_t *regs) { + regs->ctrl |= WB_TIMER_CTRL_ENABLE; +} + +static inline void wb_timer_disable(wb_timer_regs_t *regs) { + regs->ctrl &= ~WB_TIMER_CTRL_ENABLE; +} + +static inline void wb_timer_set_compare(wb_timer_regs_t *regs, uint32_t value) { + regs->compare = value; +} + +static inline uint32_t wb_timer_get_counter(wb_timer_regs_t *regs) { + return regs->counter; +} + +static inline void wb_timer_clear_irq(wb_timer_regs_t *regs) { + regs->status = WB_TIMER_STATUS_IRQ_PENDING | WB_TIMER_STATUS_MATCH; +} + +static inline int wb_timer_irq_pending(wb_timer_regs_t *regs) { + return (regs->status & WB_TIMER_STATUS_IRQ_PENDING) != 0; +} + +#endif // WB_TIMER_H diff --git a/chipflow_digital_ip/io/sv_timer/wb_timer.sv b/chipflow_digital_ip/io/sv_timer/wb_timer.sv new file mode 100644 index 0000000..1b5cfb8 --- /dev/null +++ b/chipflow_digital_ip/io/sv_timer/wb_timer.sv @@ -0,0 +1,181 @@ +// SPDX-License-Identifier: BSD-2-Clause +// Simple Wishbone Timer/Counter in SystemVerilog +// +// A basic 32-bit programmable timer with Wishbone B4 interface. +// Useful for MCU applications requiring periodic interrupts or timing. +// +// Registers: +// 0x00: CTRL - Control register (enable, mode, interrupt enable) +// 0x04: COMPARE - Compare value for timer match +// 0x08: COUNTER - Current counter value (read) / Reload value (write) +// 0x0C: STATUS - Status register (interrupt pending, match flag) + +module wb_timer #( + parameter int WIDTH = 32, + parameter int PRESCALER_WIDTH = 16 +) ( + // Clock and reset + input logic i_clk, + input logic i_rst_n, + + // Wishbone B4 slave interface + input logic i_wb_cyc, + input logic i_wb_stb, + input logic i_wb_we, + input logic [3:0] i_wb_sel, + input logic [3:0] i_wb_adr, // Word address (4 registers) + input logic [31:0] i_wb_dat, + output logic [31:0] o_wb_dat, + output logic o_wb_ack, + + // Interrupt output + output logic o_irq +); + + // Register addresses (word-aligned) + localparam logic [3:0] ADDR_CTRL = 4'h0; + localparam logic [3:0] ADDR_COMPARE = 4'h1; + localparam logic [3:0] ADDR_COUNTER = 4'h2; + localparam logic [3:0] ADDR_STATUS = 4'h3; + + // Control register bits + typedef struct packed { + logic [15:0] prescaler; // [31:16] Prescaler divider value + logic [13:0] reserved; // [15:2] Reserved + logic irq_en; // [1] Interrupt enable + logic enable; // [0] Timer enable + } ctrl_t; + + // Status register bits + typedef struct packed { + logic [29:0] reserved; // [31:2] Reserved + logic match; // [1] Compare match occurred + logic irq_pending; // [0] Interrupt pending + } status_t; + + // Registers + ctrl_t ctrl_reg; + logic [WIDTH-1:0] compare_reg; + logic [WIDTH-1:0] counter_reg; + logic [WIDTH-1:0] reload_reg; + status_t status_reg; + + // Prescaler counter + logic [PRESCALER_WIDTH-1:0] prescaler_cnt; + logic prescaler_tick; + + // Wishbone acknowledge - single cycle response + logic wb_access; + assign wb_access = i_wb_cyc & i_wb_stb; + + always_ff @(posedge i_clk or negedge i_rst_n) begin + if (!i_rst_n) begin + o_wb_ack <= 1'b0; + end else begin + o_wb_ack <= wb_access & ~o_wb_ack; + end + end + + // Prescaler logic + always_ff @(posedge i_clk or negedge i_rst_n) begin + if (!i_rst_n) begin + prescaler_cnt <= '0; + prescaler_tick <= 1'b0; + end else if (ctrl_reg.enable) begin + if (prescaler_cnt >= ctrl_reg.prescaler) begin + prescaler_cnt <= '0; + prescaler_tick <= 1'b1; + end else begin + prescaler_cnt <= prescaler_cnt + 1'b1; + prescaler_tick <= 1'b0; + end + end else begin + prescaler_cnt <= '0; + prescaler_tick <= 1'b0; + end + end + + // Counter logic + logic counter_match; + assign counter_match = (counter_reg == compare_reg); + + always_ff @(posedge i_clk or negedge i_rst_n) begin + if (!i_rst_n) begin + counter_reg <= '0; + end else if (ctrl_reg.enable && prescaler_tick) begin + if (counter_match) begin + counter_reg <= reload_reg; + end else begin + counter_reg <= counter_reg + 1'b1; + end + end + end + + // Status and interrupt logic + always_ff @(posedge i_clk or negedge i_rst_n) begin + if (!i_rst_n) begin + status_reg <= '0; + end else begin + // Set match flag on compare match + if (ctrl_reg.enable && prescaler_tick && counter_match) begin + status_reg.match <= 1'b1; + if (ctrl_reg.irq_en) begin + status_reg.irq_pending <= 1'b1; + end + end + + // Clear flags on status register write + if (wb_access && i_wb_we && (i_wb_adr == ADDR_STATUS)) begin + if (i_wb_sel[0] && i_wb_dat[0]) status_reg.irq_pending <= 1'b0; + if (i_wb_sel[0] && i_wb_dat[1]) status_reg.match <= 1'b0; + end + end + end + + assign o_irq = status_reg.irq_pending; + + // Register write logic + always_ff @(posedge i_clk or negedge i_rst_n) begin + if (!i_rst_n) begin + ctrl_reg <= '0; + compare_reg <= '1; // Default to max value + reload_reg <= '0; + end else if (wb_access && i_wb_we) begin + case (i_wb_adr) + ADDR_CTRL: begin + if (i_wb_sel[0]) ctrl_reg[7:0] <= i_wb_dat[7:0]; + if (i_wb_sel[1]) ctrl_reg[15:8] <= i_wb_dat[15:8]; + if (i_wb_sel[2]) ctrl_reg[23:16] <= i_wb_dat[23:16]; + if (i_wb_sel[3]) ctrl_reg[31:24] <= i_wb_dat[31:24]; + end + ADDR_COMPARE: begin + if (i_wb_sel[0]) compare_reg[7:0] <= i_wb_dat[7:0]; + if (i_wb_sel[1]) compare_reg[15:8] <= i_wb_dat[15:8]; + if (i_wb_sel[2]) compare_reg[23:16] <= i_wb_dat[23:16]; + if (i_wb_sel[3]) compare_reg[31:24] <= i_wb_dat[31:24]; + end + ADDR_COUNTER: begin + // Writing to counter sets the reload value + if (i_wb_sel[0]) reload_reg[7:0] <= i_wb_dat[7:0]; + if (i_wb_sel[1]) reload_reg[15:8] <= i_wb_dat[15:8]; + if (i_wb_sel[2]) reload_reg[23:16] <= i_wb_dat[23:16]; + if (i_wb_sel[3]) reload_reg[31:24] <= i_wb_dat[31:24]; + end + default: ; + endcase + end + end + + // Register read logic + always_comb begin + o_wb_dat = '0; + case (i_wb_adr) + ADDR_CTRL: o_wb_dat = ctrl_reg; + ADDR_COMPARE: o_wb_dat = compare_reg; + ADDR_COUNTER: o_wb_dat = counter_reg; + ADDR_STATUS: o_wb_dat = status_reg; + default: o_wb_dat = '0; + endcase + end + +endmodule diff --git a/chipflow_digital_ip/io/sv_timer/wb_timer.toml b/chipflow_digital_ip/io/sv_timer/wb_timer.toml new file mode 100644 index 0000000..a94df06 --- /dev/null +++ b/chipflow_digital_ip/io/sv_timer/wb_timer.toml @@ -0,0 +1,46 @@ +# Wishbone Timer/Counter Configuration +# A simple 32-bit programmable timer with Wishbone B4 interface + +name = 'wb_timer' + +[files] +path = '.' + +# Generator options: +# 'yosys_slang' - Uses yosys with slang plugin (preferred, works with yowasp-yosys) +# 'systemverilog' - Uses sv2v for conversion (requires sv2v installed) +[generate] +generator = 'yosys_slang' + +[generate.yosys_slang] +top_module = 'wb_timer' + +[clocks] +sys = 'clk' + +[resets] +sys = 'rst_n' + +# Bus interface using auto-inference +# When no explicit 'map' is provided, the wrapper parses the Verilog file +# and matches signal patterns (cyc, stb, ack, etc.) to interface members. +# This works with any naming convention: i_wb_cyc, wb_cyc_i, cyc, etc. +[ports.bus] +interface = 'amaranth_soc.wishbone.Signature' +direction = 'in' + +[ports.bus.params] +addr_width = 4 +data_width = 32 +granularity = 8 + +# Pin interfaces - simple signals need explicit mapping +# (pattern matching can't reliably infer single-bit signals) +[pins.irq] +interface = 'amaranth.lib.wiring.Out(1)' +map = 'o_irq' + +# Software driver configuration for SoftwareDriverSignature +[driver] +regs_struct = 'wb_timer_regs_t' +h_files = ['drivers/wb_timer.h'] diff --git a/chipflow_digital_ip/io/usb_ohci.toml b/chipflow_digital_ip/io/usb_ohci.toml new file mode 100644 index 0000000..65b9ff3 --- /dev/null +++ b/chipflow_digital_ip/io/usb_ohci.toml @@ -0,0 +1,99 @@ +name = 'UsbOhciPeripheral' + +[files] +module = 'pythondata_misc_usb_ohci' + +[generate] +parameters.nusb = 1 +parameters.dma_data_width = 32 +parameters.usb_clk_freq = 48e6 + +generator = 'spinalhdl' + +[generate.spinalhdl] +options = [ '--port-count {nusb}', + '--phy-frequency {usb_clk_freq:.0f}', + '--dma-width {dma_data_width}', + ] +scala_class = 'spinal.lib.com.usb.ohci.UsbOhciWishbone' + +[clocks] +usb = 'i_phy_clk' +sys = 'i_ctrl_clk' + +[resets] +usb = 'i_phy_reset' +sys = 'i_ctrl_reset' + +[ports.wb_ctl] +interface = 'amaranth_soc.wishbone.Interface' + +[ports.wb_ctl.params] +data_width=32 +address_width=32 +addressing='word' + +[ports.wb_ctl.map] +cyc = 'i_io_ctrl_CYC' +stb = 'i_io_ctrl_STB' +ack = 'o_io_ctrl_ACK' +we = 'i_io_ctrl_WE' +adr = 'i_io_ctrl_ADR' +dat.r = 'o_io_ctrl_DAT_MISO' +dat.w = 'i_io_ctrl_DAT_MOSI' +sel = 'i_io_ctrl_SEL' + +[ports.wb_dma] +interface = 'amaranth_soc.wishbone.Interface' + +[ports.wb_dma.params] +data_width=32 +address_width='{dma_data_width}' +addressing='word' + +[ports.wb_dma.map] +cyc = 'o_io_dma_CYC' +stb = 'o_io_dma_STB' +ack = 'i_io_dma_ACK' +we = 'o_io_dma_WE' +adr = 'o_io_dma_ADR' +dat.r = 'i_io_dma_DAT_MISO' +dat.w = 'o_io_dma_DAT_MOSI' +sel = 'o_io_dma_SEL' +err = 'i_io_dma_ERR' +cti = 'o_io_dma_CTI' +bte = 'o_io_dma_BTE' + +[ports.interrupt] +interface = 'amaranth.lib.wiring.Out(1)' +map = 'o_io_interrupt' + +[pins.usb] +interface = 'chipflow_lib.platforms.I2CSignature' + +[pins.usb.vars] +n = 'int' + +[pins.usb.map] +dp.i = 'i_io_usb_{n}_dp_read' +dp.o = 'o_io_usb_{n}_dp_write' +dp.oe = 'o_io_usb_{n}_dp_writeEnable' +dm.i = 'i_io_usb_{n}_dm_read' +dm.o = 'o_io_usb_{n}_dm_write' +dm.oe = 'o_io_usb_{n}_dm_writeEnable' + +[drivers.uboot] +CONFIG_USB_OHCI_NEW = true +CONFIG_SYS_USB_OHCI_REGS_BASE = "{regs_base}" + +[drivers.zephyr] +CONFIG_UHC_NXP_OHCI = true + +[drivers.dtsi] +# reg, interrupts, enabled automatically added +compatible = "nxp,uhc-ohci" +maximum-speed = "full-speed" + +[drivers.raw] +c_files = ['drivers/ohci_generic.c', 'drivers/ohci_hcd.c'] +h_files = ['ohci.h'] diff --git a/docs/index.rst b/docs/index.rst index 3fe829f..f61fc86 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -13,6 +13,7 @@ A curated collection of parameterised and configurable RTL cores implemented or io memory processors + verilog_wrapper Indices and tables ================== diff --git a/docs/verilog_wrapper.rst b/docs/verilog_wrapper.rst new file mode 100644 index 0000000..2985aaf --- /dev/null +++ b/docs/verilog_wrapper.rst @@ -0,0 +1,249 @@ +Verilog/SystemVerilog Integration +================================== + +The VerilogWrapper system allows you to integrate external Verilog and SystemVerilog +modules into Amaranth designs using a TOML-based configuration. It supports: + +- Automatic interface binding for common bus protocols (Wishbone, SPI, UART, etc.) +- SystemVerilog to Verilog conversion via yosys-slang +- SpinalHDL code generation +- CXXRTL compiled simulation for fast testbench execution +- Software driver integration + +Quick Start +----------- + +1. Create a TOML configuration file for your Verilog module +2. Load the wrapper and use it as an Amaranth component +3. Simulate using CXXRTL or synthesize with your Amaranth design + +.. code-block:: python + + from chipflow_digital_ip.io import load_wrapper_from_toml + + # Load and instantiate + wrapper = load_wrapper_from_toml("my_module.toml") + + # Use in your design + m.submodules.my_module = wrapper + m.d.comb += my_bus.connect(wrapper.bus) + +TOML Configuration +------------------ + +The TOML file defines how to wrap your Verilog module. Here's a complete example: + +.. code-block:: toml + + # Module name - must match the Verilog module name + name = 'wb_timer' + + [files] + # Source location (relative path or Python module) + path = '.' + # Or: module = 'mypackage.data' + + [generate] + # Generator: 'yosys_slang', 'systemverilog', 'spinalhdl', or 'verilog' + generator = 'yosys_slang' + + [generate.yosys_slang] + top_module = 'wb_timer' + + [clocks] + # Map Amaranth clock domains to Verilog signals + sys = 'clk' # ClockSignal() -> i_clk + + [resets] + # Map Amaranth reset domains to Verilog signals (active-low) + sys = 'rst_n' # ~ResetSignal() -> i_rst_n + + [ports.bus] + # Bus interface with auto-mapping + interface = 'amaranth_soc.wishbone.Signature' + direction = 'in' + + [ports.bus.params] + addr_width = 4 + data_width = 32 + granularity = 8 + + [pins.irq] + # Simple signal with explicit mapping + interface = 'amaranth.lib.wiring.Out(1)' + map = 'o_irq' + + [driver] + # Optional: Software driver files + regs_struct = 'my_regs_t' + h_files = ['drivers/my_module.h'] + +Configuration Sections +^^^^^^^^^^^^^^^^^^^^^^ + +**[files]** + Specifies where to find the Verilog source files. + + - ``path``: Relative path from the TOML file + - ``module``: Python module containing data files (uses ``data_location`` attribute) + +**[generate]** + Controls how SystemVerilog/SpinalHDL is processed. + + - ``generator``: One of ``'yosys_slang'``, ``'systemverilog'``, ``'spinalhdl'``, ``'verilog'`` + - ``parameters``: Dictionary of parameters for SpinalHDL generation + +**[clocks]** and **[resets]** + Maps Amaranth clock/reset domains to Verilog signal names. + The wrapper adds ``i_`` prefix to clock signals and inverts reset signals + (assuming active-low reset convention). + +**[ports.]** + Defines a bus interface port. + + - ``interface``: Dotted path to Amaranth interface class + - ``direction``: ``'in'`` or ``'out'`` + - ``params``: Interface constructor parameters + - ``map``: Optional explicit signal mapping (auto-mapped if omitted) + +**[pins.]** + Defines a simple signal or pin interface. + + - ``interface``: Interface specification (e.g., ``'amaranth.lib.wiring.Out(1)'``) + - ``map``: Verilog signal name + +**[driver]** + Software driver configuration for SoftwareDriverSignature. + + - ``regs_struct``: C struct name for register access + - ``c_files``, ``h_files``: Driver source/header files + +Auto-Mapping +------------ + +When ``map`` is not specified for a port, the wrapper automatically matches +Verilog signals to interface members using pattern recognition: + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - Interface Member + - Recognized Patterns + * - ``cyc`` + - ``*cyc*``, ``*CYC*`` + * - ``stb`` + - ``*stb*``, ``*STB*`` + * - ``ack`` + - ``*ack*``, ``*ACK*`` + * - ``we`` + - ``*we*``, ``*WE*`` + * - ``adr`` + - ``*adr*``, ``*addr*``, ``*ADR*`` + * - ``dat_w``, ``dat.w`` + - ``*dat*w*``, ``*data*in*``, ``*DAT*MOSI*`` + * - ``dat_r``, ``dat.r`` + - ``*dat*r*``, ``*data*out*``, ``*DAT*MISO*`` + +This allows the wrapper to work with various naming conventions. + +CXXRTL Simulation +----------------- + +The VerilogWrapper integrates with CXXRTL for fast compiled simulation, +allowing you to write Python testbenches that execute real Verilog code. + +.. code-block:: python + + from chipflow_digital_ip.io import load_wrapper_from_toml + + # Load wrapper + wrapper = load_wrapper_from_toml("my_module.toml", generate_dest="build") + + # Build CXXRTL simulator + sim = wrapper.build_simulator("build/sim") + + # Clock cycle helper + def tick(): + sim.set("i_clk", 0) + sim.step() + sim.set("i_clk", 1) + sim.step() + + # Reset + sim.set("i_rst_n", 0) + tick() + tick() + sim.set("i_rst_n", 1) + + # Interact with the design + sim.set("i_wb_cyc", 1) + sim.set("i_wb_stb", 1) + tick() + value = sim.get("o_wb_dat") + + sim.close() + +Signal Names +^^^^^^^^^^^^ + +In CXXRTL simulation, signals are accessed by their Verilog names (not Amaranth paths): + +.. list-table:: + :header-rows: 1 + :widths: 30 30 + + * - Amaranth + - Verilog + * - ``wrapper.bus.cyc`` + - ``"i_wb_cyc"`` + * - ``wrapper.bus.dat_r`` + - ``"o_wb_dat"`` + * - Clock + - ``"i_clk"`` + * - Reset + - ``"i_rst_n"`` + +Use ``wrapper.get_signal_map()`` to get the complete mapping. + +SpinalHDL Integration +--------------------- + +For SpinalHDL-based IP, use the ``spinalhdl`` generator: + +.. code-block:: toml + + [generate] + generator = 'spinalhdl' + parameters.nusb = 1 + parameters.dma_data_width = 32 + + [generate.spinalhdl] + scala_class = 'spinal.lib.com.usb.ohci.UsbOhciWishbone' + options = [ + '--port-count {nusb}', + '--dma-width {dma_data_width}', + ] + +The wrapper will invoke the SpinalHDL generator to produce Verilog before +wrapping the module. + +API Reference +------------- + +.. autofunction:: chipflow_digital_ip.io.load_wrapper_from_toml + +.. autoclass:: chipflow_digital_ip.io.VerilogWrapper + :members: + :special-members: __init__ + +Examples +-------- + +See the ``examples/sv_timer_simulation/`` directory for a complete example +including: + +- SystemVerilog timer IP (``wb_timer.sv``) +- TOML configuration (``wb_timer.toml``) +- Simulation script (``simulate_timer.py``) +- C driver header (``drivers/wb_timer.h``) diff --git a/examples/sv_soc/README.md b/examples/sv_soc/README.md new file mode 100644 index 0000000..c6888f6 --- /dev/null +++ b/examples/sv_soc/README.md @@ -0,0 +1,175 @@ +# SystemVerilog SoC Example + +This example demonstrates integrating SystemVerilog/Verilog components into a +ChipFlow SoC design using `VerilogWrapper`. + +## Overview + +The example creates a minimal SoC with: + +- **Minerva RISC-V CPU** - 32-bit processor +- **QSPI Flash** - Code and data storage +- **SRAM** - 1KB working memory +- **GPIO** - 8-bit LED control +- **UART** - Serial output at 115200 baud +- **SystemVerilog Timer** - `wb_timer` from `chipflow_digital_ip.io.sv_timer` + +The timer peripheral is written in SystemVerilog and integrated using the +TOML-based `VerilogWrapper` configuration system. This example uses the +`wb_timer` that ships with `chipflow-digital-ip` rather than a local copy. + +## Directory Structure + +``` +sv_soc/ +├── chipflow.toml # ChipFlow project configuration +├── design/ +│ ├── design.py # SoC top-level design +│ ├── steps/ +│ │ └── board.py # FPGA board build step +│ ├── software/ +│ │ └── main.c # Firmware demonstrating timer usage +│ └── tests/ +│ └── test_timer_sim.py # CXXRTL simulation test +└── README.md +``` + +## Timer Peripheral + +The `wb_timer` from `chipflow_digital_ip.io.sv_timer` is a 32-bit programmable +timer with: + +- **Wishbone B4 interface** - Standard bus protocol +- **Prescaler** - 16-bit clock divider +- **Compare match** - Interrupt when counter reaches compare value +- **Auto-reload** - Continuous operation mode + +### Registers + +| Offset | Name | Description | +|--------|---------|------------------------------------------------| +| 0x00 | CTRL | Control: [31:16] prescaler, [1] irq_en, [0] en | +| 0x04 | COMPARE | Compare value for match interrupt | +| 0x08 | COUNTER | Current count (read) / Reload value (write) | +| 0x0C | STATUS | Status: [1] match, [0] irq_pending (W1C) | + +## Usage + +### Prerequisites + +- Python 3.12+ +- chipflow-lib with simulation support +- yowasp-yosys with slang plugin (or native yosys+slang) + +### Running the Simulation Test + +```bash +cd examples/sv_soc + +# Run the CXXRTL simulation test +pdm run python design/tests/test_timer_sim.py +``` + +Expected output: + +``` +SystemVerilog Timer CXXRTL Simulation Test +================================================== + +1. Loading timer from: .../chipflow_digital_ip/io/sv_timer/wb_timer.toml + Module: wb_timer + Source files: ['wb_timer.sv'] + +2. Building CXXRTL simulator in: .../build/timer_sim + Simulator ready! + +3. Test: Reset and read initial values + CTRL: 0x00000000 (expected: 0x00000000) + COUNTER: 0x00000000 (expected: 0x00000000) + STATUS: 0x00000000 (expected: 0x00000000) + PASS! + +... + +================================================== +All tests passed! +================================================== +``` + +### Building for FPGA + +```bash +cd examples/sv_soc + +# Build for ULX3S board +chipflow build board +``` + +### Using in Your Own Design + +```python +from pathlib import Path +import chipflow_digital_ip.io +from chipflow_digital_ip.io import load_wrapper_from_toml + +# Get the timer TOML from the package +timer_toml = Path(chipflow_digital_ip.io.__file__).parent / "sv_timer" / "wb_timer.toml" + +# Load the timer +timer = load_wrapper_from_toml( + timer_toml, + generate_dest="build/timer_gen" +) + +# Add to Wishbone decoder +wb_decoder.add(timer.bus, name="timer", addr=TIMER_BASE) + +# Add to module +m.submodules.timer = timer + +# Access IRQ signal +m.d.comb += cpu_irq.eq(timer.irq.o) +``` + +## Key Concepts + +### VerilogWrapper + +`VerilogWrapper` bridges Verilog/SystemVerilog modules into Amaranth designs: + +1. **TOML Configuration** - Describes the module interface +2. **Auto-mapping** - Automatically maps bus signals (cyc, stb, ack, etc.) +3. **Code Generation** - Converts SystemVerilog to Verilog via yosys+slang +4. **Simulation** - Builds CXXRTL simulators for fast testing + +### CXXRTL Simulation + +The `build_simulator()` method compiles the RTL to a shared library: + +```python +# Build simulator +sim = wrapper.build_simulator("build/sim") + +# Control signals +sim.set("i_clk", 1) +sim.step() + +# Read outputs +data = sim.get("o_wb_dat") +``` + +## Memory Map + +| Address | Peripheral | Size | Interface | +|--------------|---------------|--------|-----------| +| 0x00000000 | SPI Flash | 16MB | Wishbone | +| 0x10000000 | SRAM | 1KB | Wishbone | +| 0xA0000000 | Timer (SV) | 64B | Wishbone | +| 0xB0000000 | Flash CSRs | 4KB | CSR | +| 0xB1000000 | GPIO | 4KB | CSR | +| 0xB2000000 | UART | 4KB | CSR | +| 0xB3000000 | SoC ID | 4KB | CSR | + +## License + +BSD-2-Clause diff --git a/examples/sv_soc/chipflow.toml b/examples/sv_soc/chipflow.toml new file mode 100644 index 0000000..3d4c0ab --- /dev/null +++ b/examples/sv_soc/chipflow.toml @@ -0,0 +1,18 @@ +# ChipFlow SoC Example with SystemVerilog Components +# Demonstrates using VerilogWrapper to integrate SV peripherals + +[chipflow] +project_name = "sv-soc-example" + +[chipflow.top] +soc = "design.design:SVTimerSoC" + +[chipflow.steps] +board = "design.steps.board:MyBoardStep" + +[chipflow.silicon] +process = "sky130" +package = "openframe" + +[chipflow.test] +event_reference = "design/tests/events_reference.json" diff --git a/examples/sv_soc/design/__init__.py b/examples/sv_soc/design/__init__.py new file mode 100644 index 0000000..42ed6b4 --- /dev/null +++ b/examples/sv_soc/design/__init__.py @@ -0,0 +1,2 @@ +# SPDX-License-Identifier: BSD-2-Clause +"""SystemVerilog SoC Example Design Package.""" diff --git a/examples/sv_soc/design/design.py b/examples/sv_soc/design/design.py new file mode 100644 index 0000000..5048ece --- /dev/null +++ b/examples/sv_soc/design/design.py @@ -0,0 +1,202 @@ +# SPDX-License-Identifier: BSD-2-Clause +"""SoC design using SystemVerilog timer via VerilogWrapper. + +This example demonstrates how to integrate SystemVerilog/Verilog components +into a ChipFlow SoC design using VerilogWrapper. +""" + +from pathlib import Path + +from amaranth import Module +from amaranth.lib import wiring +from amaranth.lib.wiring import Out, flipped, connect + +from amaranth_soc import csr, wishbone +from amaranth_soc.csr.wishbone import WishboneCSRBridge +from amaranth_soc.wishbone.sram import WishboneSRAM + +from chipflow_digital_ip.base import SoCID +from chipflow_digital_ip.memory import QSPIFlash +from chipflow_digital_ip.io import GPIOPeripheral, UARTPeripheral +from chipflow.rtl import load_wrapper_from_toml +import chipflow_digital_ip.io + +from minerva.core import Minerva + +from chipflow.platform import ( + GPIOSignature, + UARTSignature, + QSPIFlashSignature, + attach_data, + SoftwareBuild, +) + + +__all__ = ["SVTimerSoC"] + +# Use the wb_timer from chipflow_digital_ip package +TIMER_TOML = Path(chipflow_digital_ip.io.__file__).parent / "sv_timer" / "wb_timer.toml" + + +class SVTimerSoC(wiring.Component): + """SoC with SystemVerilog timer peripheral. + + This SoC demonstrates integrating a SystemVerilog peripheral (wb_timer) + alongside native Amaranth peripherals. The timer is loaded via VerilogWrapper + and connected to the Wishbone bus. + + Memory Map: + 0x00000000 - SPI Flash (code/data) + 0x10000000 - SRAM (1KB working memory) + 0xB0000000 - CSR region start + 0xB0000000 - SPI Flash CSRs + 0xB1000000 - GPIO + 0xB2000000 - UART + 0xB3000000 - Timer (SystemVerilog) + 0xB4000000 - SoC ID + """ + + def __init__(self): + super().__init__({ + "flash": Out(QSPIFlashSignature()), + "uart_0": Out(UARTSignature()), + "gpio_0": Out(GPIOSignature(pin_count=8)), + }) + + # Memory regions + self.mem_spiflash_base = 0x00000000 + self.mem_sram_base = 0x10000000 + + # Wishbone peripherals (direct access, not through CSR bridge) + self.wb_timer_base = 0xA0000000 # SystemVerilog timer + + # CSR regions (accessed through CSR bridge) + self.csr_base = 0xB0000000 + self.csr_spiflash_base = 0xB0000000 + self.csr_gpio_base = 0xB1000000 + self.csr_uart_base = 0xB2000000 + self.csr_soc_id_base = 0xB3000000 + + # SRAM size + self.sram_size = 0x400 # 1KB + + # BIOS start (1MB into flash to leave room for bitstream) + self.bios_start = 0x100000 + + # Build directory for generated Verilog + self.build_dir = Path(__file__).parent.parent / "build" + + def elaborate(self, platform): + m = Module() + + # Create Wishbone arbiter and decoder + wb_arbiter = wishbone.Arbiter(addr_width=30, data_width=32, granularity=8) + wb_decoder = wishbone.Decoder(addr_width=30, data_width=32, granularity=8) + csr_decoder = csr.Decoder(addr_width=28, data_width=8) + + m.submodules.wb_arbiter = wb_arbiter + m.submodules.wb_decoder = wb_decoder + m.submodules.csr_decoder = csr_decoder + + connect(m, wb_arbiter.bus, wb_decoder.bus) + + # ======================================== + # CPU - Minerva RISC-V + # ======================================== + cpu = Minerva(reset_address=self.bios_start, with_muldiv=True) + wb_arbiter.add(cpu.ibus) + wb_arbiter.add(cpu.dbus) + m.submodules.cpu = cpu + + # ======================================== + # QSPI Flash - Code and data storage + # ======================================== + spiflash = QSPIFlash(addr_width=24, data_width=32) + wb_decoder.add(spiflash.wb_bus, name="spiflash", addr=self.mem_spiflash_base) + csr_decoder.add(spiflash.csr_bus, name="spiflash", + addr=self.csr_spiflash_base - self.csr_base) + m.submodules.spiflash = spiflash + connect(m, flipped(self.flash), spiflash.pins) + + # ======================================== + # SRAM - Working memory + # ======================================== + sram = WishboneSRAM(size=self.sram_size, data_width=32, granularity=8) + wb_decoder.add(sram.wb_bus, name="sram", addr=self.mem_sram_base) + m.submodules.sram = sram + + # ======================================== + # GPIO - LED control + # ======================================== + gpio_0 = GPIOPeripheral(pin_count=8) + csr_decoder.add(gpio_0.bus, name="gpio_0", + addr=self.csr_gpio_base - self.csr_base) + m.submodules.gpio_0 = gpio_0 + connect(m, flipped(self.gpio_0), gpio_0.pins) + + # ======================================== + # UART - Serial output + # ======================================== + uart_0 = UARTPeripheral(init_divisor=int(25e6 // 115200), addr_width=5) + csr_decoder.add(uart_0.bus, name="uart_0", + addr=self.csr_uart_base - self.csr_base) + m.submodules.uart_0 = uart_0 + connect(m, flipped(self.uart_0), uart_0.pins) + + # ======================================== + # SystemVerilog Timer - Using VerilogWrapper + # ======================================== + # Load the timer from TOML configuration + timer_wrapper = load_wrapper_from_toml( + TIMER_TOML, + generate_dest=self.build_dir / "timer_gen" + ) + + # Add the timer to the Wishbone decoder + # The timer has a 32-bit Wishbone interface + wb_decoder.add(timer_wrapper.bus, name="timer", + addr=self.wb_timer_base) + + # Add the timer module + m.submodules.timer = timer_wrapper + + # Timer IRQ could be connected to CPU interrupt if needed + # For this example, we just expose it for monitoring + + # ======================================== + # SoC ID - Device identification + # ======================================== + soc_id = SoCID(type_id=0xCA7F100F) + csr_decoder.add(soc_id.bus, name="soc_id", + addr=self.csr_soc_id_base - self.csr_base) + m.submodules.soc_id = soc_id + + # ======================================== + # Wishbone-CSR bridge + # ======================================== + wb_to_csr = WishboneCSRBridge(csr_decoder.bus, data_width=32) + wb_decoder.add(wb_to_csr.wb_bus, name="csr", addr=self.csr_base, sparse=False) + m.submodules.wb_to_csr = wb_to_csr + + # ======================================== + # Software build configuration + # ======================================== + sw = SoftwareBuild( + sources=Path(__file__).parent.glob("software/*.c"), + offset=self.bios_start, + ) + + # Attach software data to flash + attach_data(self.flash, m.submodules.spiflash, sw) + + return m + + +if __name__ == "__main__": + # Generate standalone Verilog for inspection + from amaranth.back import verilog + + soc = SVTimerSoC() + with open("build/sv_timer_soc.v", "w") as f: + f.write(verilog.convert(soc, name="sv_timer_soc")) + print("Generated build/sv_timer_soc.v") diff --git a/examples/sv_soc/design/software/.gitignore b/examples/sv_soc/design/software/.gitignore new file mode 100644 index 0000000..05b8c4c --- /dev/null +++ b/examples/sv_soc/design/software/.gitignore @@ -0,0 +1,4 @@ +generated/ +*.o +*.bin +*.elf diff --git a/examples/sv_soc/design/software/main.c b/examples/sv_soc/design/software/main.c new file mode 100644 index 0000000..db806b4 --- /dev/null +++ b/examples/sv_soc/design/software/main.c @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: BSD-2-Clause +// SoC firmware demonstrating SystemVerilog timer usage + +#include +#include "generated/soc.h" + +// TIMER, GPIO_0, UART_0, etc. are defined in generated/soc.h + +void delay_cycles(uint32_t cycles) { + // Simple delay using the timer + wb_timer_disable(TIMER); + TIMER->counter = 0; + TIMER->compare = cycles; + TIMER->ctrl = WB_TIMER_CTRL_ENABLE; // Enable without IRQ + + // Wait for match + while (!(TIMER->status & WB_TIMER_STATUS_MATCH)) + ; + + wb_timer_disable(TIMER); + TIMER->status = WB_TIMER_STATUS_MATCH; // Clear match flag +} + +void blink_leds(uint8_t pattern) { + // Write pattern to GPIO0 (LEDs) + GPIO_0->output = pattern; +} + +void main() { + // Initialize UART + uart_init(UART_0, 25000000 / 115200); + + puts("SystemVerilog Timer SoC Example\r\n"); + puts("================================\r\n\n"); + + // Print SoC ID + puts("SoC type: "); + puthex(SOC_ID->type); + puts("\r\n"); + + // Initialize and test the SystemVerilog timer + puts("\nTimer test:\r\n"); + + // Configure timer with prescaler=0 (no division), compare=1000 + TIMER->compare = 1000; + TIMER->counter = 0; // Sets reload value + TIMER->ctrl = (0 << WB_TIMER_CTRL_PRESCALER_SHIFT) | WB_TIMER_CTRL_ENABLE; + + // Wait for a few timer cycles and read counter + for (int i = 0; i < 5; i++) { + puts("Counter: "); + puthex(TIMER->counter); + puts("\r\n"); + + // Small delay + for (volatile int j = 0; j < 1000; j++) + ; + } + + // Disable timer + wb_timer_disable(TIMER); + puts("\nTimer stopped.\r\n"); + + // LED blinking demo + puts("\nLED blink demo (binary counter):\r\n"); + + uint8_t led_val = 0; + while (1) { + blink_leds(led_val); + + // Use timer for delay + delay_cycles(25000000 / 4); // ~250ms at 25MHz + + led_val++; + + // Print every 16 counts + if ((led_val & 0x0F) == 0) { + puts("LED: "); + puthex(led_val); + puts("\r\n"); + } + } +} diff --git a/examples/sv_soc/design/steps/__init__.py b/examples/sv_soc/design/steps/__init__.py new file mode 100644 index 0000000..70bd39f --- /dev/null +++ b/examples/sv_soc/design/steps/__init__.py @@ -0,0 +1,2 @@ +# SPDX-License-Identifier: BSD-2-Clause +"""ChipFlow build steps.""" diff --git a/examples/sv_soc/design/steps/board.py b/examples/sv_soc/design/steps/board.py new file mode 100644 index 0000000..0db15df --- /dev/null +++ b/examples/sv_soc/design/steps/board.py @@ -0,0 +1,98 @@ +# SPDX-License-Identifier: BSD-2-Clause +"""Board step for FPGA prototyping.""" + +from amaranth import Module, ClockDomain, ClockSignal, ResetSignal, Instance +from amaranth.lib import wiring +from amaranth.lib.cdc import ResetSynchronizer + +from amaranth_boards.ulx3s import ULX3S_85F_Platform + +from chipflow.platform import BoardStep + +from ..design import SVTimerSoC + + +class BoardSocWrapper(wiring.Component): + """Wrapper that connects the SoC to FPGA board peripherals.""" + + def __init__(self): + super().__init__({}) + + def elaborate(self, platform): + m = Module() + + # Instantiate the SoC + m.submodules.soc = soc = SVTimerSoC() + + # Clock domain setup + m.domains += ClockDomain("sync") + m.d.comb += ClockSignal("sync").eq(platform.request("clk25").i) + + # Reset synchronizer from power button + btn_rst = platform.request("button_pwr") + m.submodules.rst_sync = ResetSynchronizer(arst=btn_rst.i, domain="sync") + + # ======================================== + # SPI Flash connection + # ======================================== + flash = platform.request( + "spi_flash", + dir=dict(cs='-', copi='-', cipo='-', wp='-', hold='-') + ) + + # Flash clock requires special primitive on ECP5 + m.submodules.usrmclk = Instance( + "USRMCLK", + i_USRMCLKI=soc.flash.clk.o, + i_USRMCLKTS=ResetSignal(), # Tristate in reset for programmer access + a_keep=1, + ) + + # Flash chip select + m.submodules += Instance( + "OBZ", + o_O=flash.cs.io, + i_I=soc.flash.csn.o, + i_T=ResetSignal(), + ) + + # Flash data pins (QSPI) + data_pins = ["copi", "cipo", "wp", "hold"] + for i in range(4): + m.submodules += Instance( + "BB", + io_B=getattr(flash, data_pins[i]).io, + i_I=soc.flash.d.o[i], + i_T=~soc.flash.d.oe[i], + o_O=soc.flash.d.i[i], + ) + + # ======================================== + # LED connection (from GPIO0) + # ======================================== + for i in range(8): + led = platform.request("led", i) + m.d.comb += led.o.eq(soc.gpio_0.gpio.o[i]) + + # ======================================== + # UART connection + # ======================================== + uart = platform.request("uart") + m.d.comb += [ + uart.tx.o.eq(soc.uart_0.tx.o), + soc.uart_0.rx.i.eq(uart.rx.i), + ] + + return m + + +class MyBoardStep(BoardStep): + """Board build step for ULX3S FPGA board.""" + + def __init__(self, config): + platform = ULX3S_85F_Platform() + super().__init__(config, platform) + + def build(self): + design = BoardSocWrapper() + self.platform.build(design, do_program=False) diff --git a/examples/sv_soc/design/tests/events_reference.json b/examples/sv_soc/design/tests/events_reference.json new file mode 100644 index 0000000..52387b4 --- /dev/null +++ b/examples/sv_soc/design/tests/events_reference.json @@ -0,0 +1,3 @@ +{ + "events": [] +} diff --git a/examples/sv_soc/design/tests/test_timer_sim.py b/examples/sv_soc/design/tests/test_timer_sim.py new file mode 100644 index 0000000..33b489c --- /dev/null +++ b/examples/sv_soc/design/tests/test_timer_sim.py @@ -0,0 +1,182 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: BSD-2-Clause +"""CXXRTL simulation test for the SystemVerilog timer. + +This test demonstrates using VerilogWrapper.build_simulator() to create +a CXXRTL simulator and test the timer peripheral. +""" + +from pathlib import Path + +import chipflow_digital_ip.io +from chipflow.rtl import load_wrapper_from_toml + + +def main(): + """Run timer simulation test.""" + # Use the wb_timer from chipflow_digital_ip package + timer_toml = Path(chipflow_digital_ip.io.__file__).parent / "sv_timer" / "wb_timer.toml" + build_dir = Path(__file__).parent.parent.parent / "build" / "timer_sim" + + print("SystemVerilog Timer CXXRTL Simulation Test") + print("=" * 50) + + # Load the timer wrapper from TOML + print(f"\n1. Loading timer from: {timer_toml}") + wrapper = load_wrapper_from_toml( + timer_toml, + generate_dest=build_dir / "gen" + ) + + print(f" Module: {wrapper.get_top_module()}") + print(f" Source files: {[f.name for f in wrapper.get_source_files()]}") + + # Build the CXXRTL simulator + print(f"\n2. Building CXXRTL simulator in: {build_dir}") + sim = wrapper.build_simulator(build_dir) + print(" Simulator ready!") + + # Helper functions + def reset(): + """Apply reset.""" + sim.set("i_rst_n", 0) + for _ in range(5): + sim.set("i_clk", 0) + sim.step() + sim.set("i_clk", 1) + sim.step() + sim.set("i_rst_n", 1) + + def tick(): + """One clock cycle.""" + sim.set("i_clk", 0) + sim.step() + sim.set("i_clk", 1) + sim.step() + + def wb_write(addr, data): + """Wishbone write transaction.""" + sim.set("i_wb_cyc", 1) + sim.set("i_wb_stb", 1) + sim.set("i_wb_we", 1) + sim.set("i_wb_sel", 0xF) + sim.set("i_wb_adr", addr) + sim.set("i_wb_dat", data) + tick() + while not sim.get("o_wb_ack"): + tick() + sim.set("i_wb_cyc", 0) + sim.set("i_wb_stb", 0) + tick() + + def wb_read(addr): + """Wishbone read transaction.""" + sim.set("i_wb_cyc", 1) + sim.set("i_wb_stb", 1) + sim.set("i_wb_we", 0) + sim.set("i_wb_sel", 0xF) + sim.set("i_wb_adr", addr) + tick() + while not sim.get("o_wb_ack"): + tick() + data = sim.get("o_wb_dat") + sim.set("i_wb_cyc", 0) + sim.set("i_wb_stb", 0) + tick() + return data + + # Register addresses + ADDR_CTRL = 0 + ADDR_COMPARE = 1 + ADDR_COUNTER = 2 + ADDR_STATUS = 3 + + # Test 1: Reset and read initial values + print("\n3. Test: Reset and read initial values") + reset() + + ctrl = wb_read(ADDR_CTRL) + counter = wb_read(ADDR_COUNTER) + status = wb_read(ADDR_STATUS) + + print(f" CTRL: 0x{ctrl:08X} (expected: 0x00000000)") + print(f" COUNTER: 0x{counter:08X} (expected: 0x00000000)") + print(f" STATUS: 0x{status:08X} (expected: 0x00000000)") + + assert ctrl == 0, f"CTRL should be 0 after reset, got {ctrl}" + assert counter == 0, f"COUNTER should be 0 after reset, got {counter}" + assert status == 0, f"STATUS should be 0 after reset, got {status}" + print(" PASS!") + + # Test 2: Configure and start timer + print("\n4. Test: Configure timer (compare=10, prescaler=0, enable)") + wb_write(ADDR_COMPARE, 10) # Compare value + wb_write(ADDR_COUNTER, 0) # Reload value + wb_write(ADDR_CTRL, 0x00000001) # Enable, no prescaler + + # Verify configuration + compare = wb_read(ADDR_COMPARE) + ctrl = wb_read(ADDR_CTRL) + print(f" COMPARE: {compare} (expected: 10)") + print(f" CTRL: 0x{ctrl:08X} (expected: 0x00000001)") + + assert compare == 10 + assert ctrl == 1 + print(" PASS!") + + # Test 3: Let counter run and check it increments + print("\n5. Test: Counter increments") + for _ in range(5): + tick() + + counter = wb_read(ADDR_COUNTER) + print(f" COUNTER after 5 ticks: {counter}") + assert counter > 0, "Counter should have incremented" + print(" PASS!") + + # Test 4: Wait for match and check IRQ + print("\n6. Test: Wait for compare match") + + # Enable IRQ and restart + wb_write(ADDR_CTRL, 0) # Disable + wb_write(ADDR_COUNTER, 0) # Reset reload + wb_write(ADDR_STATUS, 0x3) # Clear any pending flags + wb_write(ADDR_CTRL, 0x00000003) # Enable + IRQ enable + + # Run until match + for i in range(20): + tick() + status = wb_read(ADDR_STATUS) + if status & 0x2: # Match flag + print(f" Match occurred at tick {i}") + break + + irq = sim.get("o_irq") + print(f" STATUS: 0x{status:08X}") + print(f" o_irq: {irq}") + + assert status & 0x2, "Match flag should be set" + assert irq == 1, "IRQ should be asserted" + print(" PASS!") + + # Test 5: Clear IRQ + print("\n7. Test: Clear IRQ by writing to STATUS") + wb_write(ADDR_STATUS, 0x3) # Write 1 to clear + + status = wb_read(ADDR_STATUS) + irq = sim.get("o_irq") + + print(f" STATUS: 0x{status:08X} (expected: 0x00000000)") + print(f" o_irq: {irq} (expected: 0)") + + assert status == 0, "Status should be cleared" + assert irq == 0, "IRQ should be deasserted" + print(" PASS!") + + print("\n" + "=" * 50) + print("All tests passed!") + print("=" * 50) + + +if __name__ == "__main__": + main() diff --git a/examples/sv_timer_simulation/README.md b/examples/sv_timer_simulation/README.md new file mode 100644 index 0000000..8688dfa --- /dev/null +++ b/examples/sv_timer_simulation/README.md @@ -0,0 +1,150 @@ +# SystemVerilog Timer Simulation Example + +This example demonstrates the complete workflow for integrating and simulating +a SystemVerilog IP module using the ChipFlow VerilogWrapper system with CXXRTL +compiled simulation. + +## Overview + +The `wb_timer` is a simple 32-bit programmable timer with a Wishbone B4 bus +interface. This example shows how to: + +1. Define a TOML configuration for the SystemVerilog module +2. Load the wrapper and create an Amaranth component +3. Simulate the design using CXXRTL compiled simulation +4. Write testbenches that verify the hardware behavior + +## Files + +- `wb_timer.sv` - The SystemVerilog timer implementation +- `wb_timer.toml` - TOML configuration for VerilogWrapper +- `simulate_timer.py` - Example simulation script +- `drivers/wb_timer.h` - C driver header for software integration + +## Prerequisites + +```bash +# Install chipflow-digital-ip with simulation support +pip install chipflow-digital-ip + +# yowasp-yosys is included as a dependency and provides SystemVerilog support +``` + +## Quick Start + +```python +from pathlib import Path +from chipflow_digital_ip.io import load_wrapper_from_toml + +# Load the wrapper from TOML configuration +toml_path = Path(__file__).parent / "wb_timer.toml" +wrapper = load_wrapper_from_toml(toml_path, generate_dest=Path("build")) + +# Build CXXRTL simulator +sim = wrapper.build_simulator("build/sim") + +# Reset the design +sim.set("i_rst_n", 0) +sim.set("i_clk", 0) +for _ in range(4): # A few clock cycles in reset + sim.set("i_clk", 0) + sim.step() + sim.set("i_clk", 1) + sim.step() +sim.set("i_rst_n", 1) + +# Now interact with the timer via Wishbone bus +# ... (see simulate_timer.py for complete example) + +sim.close() +``` + +## TOML Configuration Explained + +```toml +# Module name - must match the Verilog module name +name = 'wb_timer' + +[files] +# Source files location (relative to this TOML file) +path = '.' + +[generate] +# Use yosys with slang plugin for SystemVerilog support +generator = 'yosys_slang' + +[generate.yosys_slang] +top_module = 'wb_timer' + +[clocks] +# Map clock domains to Verilog signals +sys = 'clk' # System clock -> i_clk + +[resets] +# Map reset domains to Verilog signals (active-low) +sys = 'rst_n' # System reset -> i_rst_n + +[ports.bus] +# Wishbone bus interface - auto-mapped from signal patterns +interface = 'amaranth_soc.wishbone.Signature' +direction = 'in' + +[ports.bus.params] +addr_width = 4 +data_width = 32 +granularity = 8 + +[pins.irq] +# Simple output signal - requires explicit mapping +interface = 'amaranth.lib.wiring.Out(1)' +map = 'o_irq' + +[driver] +# Software driver configuration +regs_struct = 'wb_timer_regs_t' +h_files = ['drivers/wb_timer.h'] +``` + +## Register Map + +| Address | Name | Description | +|---------|---------|--------------------------------------------------| +| 0x0 | CTRL | [31:16] prescaler, [1] irq_en, [0] enable | +| 0x1 | COMPARE | Compare value for timer match | +| 0x2 | COUNTER | Current counter (read) / Reload value (write) | +| 0x3 | STATUS | [1] match, [0] irq_pending (write 1 to clear) | + +## Running the Example + +```bash +# Run the simulation script +python simulate_timer.py + +# Or run the tests +pytest test_timer.py -v +``` + +## Signal Names in Simulation + +When using `CxxrtlSimulator`, signals are accessed by their Verilog names: + +| Amaranth Path | Verilog Signal | +|----------------|----------------| +| `bus.cyc` | `i_wb_cyc` | +| `bus.stb` | `i_wb_stb` | +| `bus.we` | `i_wb_we` | +| `bus.adr` | `i_wb_adr` | +| `bus.dat_w` | `i_wb_dat` | +| `bus.dat_r` | `o_wb_dat` | +| `bus.ack` | `o_wb_ack` | +| `bus.sel` | `i_wb_sel` | +| `irq` | `o_irq` | +| (clock) | `i_clk` | +| (reset) | `i_rst_n` | + +## Next Steps + +- See the [VerilogWrapper documentation](../../docs/verilog_wrapper.rst) for + complete API reference +- Check `tests/test_wb_timer.py` for comprehensive test examples +- Explore `usb_ohci.toml` for a more complex SpinalHDL-based example diff --git a/examples/sv_timer_simulation/simulate_timer.py b/examples/sv_timer_simulation/simulate_timer.py new file mode 100644 index 0000000..012a328 --- /dev/null +++ b/examples/sv_timer_simulation/simulate_timer.py @@ -0,0 +1,193 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: BSD-2-Clause +"""Example: Simulating a SystemVerilog timer with CXXRTL. + +This script demonstrates the complete workflow for simulating a SystemVerilog +IP module using the ChipFlow VerilogWrapper and CXXRTL compiled simulation. + +The wb_timer is a simple 32-bit programmable timer with: +- Wishbone B4 bus interface +- Configurable prescaler +- Compare match with interrupt generation +- Status register with clear-on-write + +Usage: + python simulate_timer.py +""" + +from pathlib import Path + +from chipflow.rtl import load_wrapper_from_toml + + +# Register addresses (word-addressed) +REG_CTRL = 0x0 +REG_COMPARE = 0x1 +REG_COUNTER = 0x2 +REG_STATUS = 0x3 + +# Control register bits +CTRL_ENABLE = 1 << 0 +CTRL_IRQ_EN = 1 << 1 + + +class WbTimerSimulation: + """Helper class for simulating the wb_timer peripheral.""" + + def __init__(self, sim): + self.sim = sim + + def tick(self): + """Perform one clock cycle.""" + self.sim.set("i_clk", 0) + self.sim.step() + self.sim.set("i_clk", 1) + self.sim.step() + + def reset(self): + """Reset the design.""" + self.sim.set("i_rst_n", 0) + self.sim.set("i_clk", 0) + self.tick() + self.tick() + self.sim.set("i_rst_n", 1) + self.tick() + + def wb_write(self, addr: int, data: int): + """Perform a Wishbone write transaction.""" + self.sim.set("i_wb_cyc", 1) + self.sim.set("i_wb_stb", 1) + self.sim.set("i_wb_we", 1) + self.sim.set("i_wb_adr", addr) + self.sim.set("i_wb_dat", data) + self.sim.set("i_wb_sel", 0xF) + + # Clock until ack + for _ in range(10): + self.tick() + if self.sim.get("o_wb_ack"): + break + + self.sim.set("i_wb_cyc", 0) + self.sim.set("i_wb_stb", 0) + self.sim.set("i_wb_we", 0) + self.tick() + + def wb_read(self, addr: int) -> int: + """Perform a Wishbone read transaction.""" + self.sim.set("i_wb_cyc", 1) + self.sim.set("i_wb_stb", 1) + self.sim.set("i_wb_we", 0) + self.sim.set("i_wb_adr", addr) + self.sim.set("i_wb_sel", 0xF) + + # Clock until ack + for _ in range(10): + self.tick() + if self.sim.get("o_wb_ack"): + break + + data = self.sim.get("o_wb_dat") + + self.sim.set("i_wb_cyc", 0) + self.sim.set("i_wb_stb", 0) + self.tick() + + return data + + +def main(): + print("=" * 60) + print("SystemVerilog Timer CXXRTL Simulation Example") + print("=" * 60) + + # Path to the wb_timer TOML configuration + # In a real project, this would be your own IP's TOML file + toml_path = ( + Path(__file__).parent.parent.parent + / "chipflow_digital_ip" + / "io" + / "sv_timer" + / "wb_timer.toml" + ) + + build_dir = Path("build/example_sim") + build_dir.mkdir(parents=True, exist_ok=True) + + print(f"\n1. Loading wrapper from: {toml_path}") + wrapper = load_wrapper_from_toml(toml_path, generate_dest=build_dir) + print(f" Module name: {wrapper.get_top_module()}") + print(f" Source files: {[f.name for f in wrapper.get_source_files()]}") + + print("\n2. Building CXXRTL simulator...") + sim = wrapper.build_simulator(build_dir) + print(" Simulator built successfully!") + + # List discovered signals + print("\n3. Discovered signals:") + for name, obj in list(sim.signals())[:10]: # First 10 + print(f" - {name} (width={obj.width})") + print(" ...") + + # Create helper + timer = WbTimerSimulation(sim) + + print("\n4. Resetting design...") + timer.reset() + + # Verify reset state + ctrl = timer.wb_read(REG_CTRL) + print(f" CTRL after reset: 0x{ctrl:08X} (expected 0x00000000)") + + print("\n5. Testing register write/read...") + timer.wb_write(REG_COMPARE, 0xDEADBEEF) + compare = timer.wb_read(REG_COMPARE) + print(f" Wrote 0xDEADBEEF to COMPARE, read back: 0x{compare:08X}") + assert compare == 0xDEADBEEF, "Register readback failed!" + + print("\n6. Testing timer counting...") + timer.wb_write(REG_COMPARE, 0xFFFFFFFF) # High compare value + timer.wb_write(REG_CTRL, CTRL_ENABLE) # Enable timer + + # Let it count for some cycles + for _ in range(20): + timer.tick() + + counter = timer.wb_read(REG_COUNTER) + print(f" Counter value after 20 cycles: {counter}") + assert counter > 0, "Counter should have incremented!" + + print("\n7. Testing IRQ generation...") + timer.reset() + timer.wb_write(REG_COMPARE, 5) # Trigger at count 5 + timer.wb_write(REG_CTRL, CTRL_ENABLE | CTRL_IRQ_EN) + + # Wait for IRQ + irq_fired = False + cycles = 0 + for _ in range(50): + timer.tick() + cycles += 1 + if sim.get("o_irq"): + irq_fired = True + break + + print(f" IRQ fired after {cycles} cycles: {irq_fired}") + assert irq_fired, "IRQ should have fired!" + + # Check status + status = timer.wb_read(REG_STATUS) + print(f" STATUS register: 0x{status:08X}") + print(f" - IRQ pending: {bool(status & 0x1)}") + print(f" - Match flag: {bool(status & 0x2)}") + + # Clean up + sim.close() + + print("\n" + "=" * 60) + print("Simulation completed successfully!") + print("=" * 60) + + +if __name__ == "__main__": + main() diff --git a/pyproject.toml b/pyproject.toml index 235a961..6bcf868 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,16 +10,20 @@ authors = [ readme = {file = "README.md", content-type = "text/markdown"} license-files = [ "LICENSE*", - "vendor/*/*/LICEN?E*", ] requires-python = ">=3.12,<3.14" dependencies = [ "amaranth>=0.5,<0.6", - "chipflow-lib @ git+https://github.com/ChipFlow/chipflow-lib.git", + "amaranth-stubs", + "chipflow @ git+https://github.com/ChipFlow/chipflow-lib.git", "amaranth-soc @ git+https://github.com/amaranth-lang/amaranth-soc", "amaranth-stdio @ git+https://github.com/amaranth-lang/amaranth-stdio", "minerva @ git+https://github.com/minerva-cpu/minerva", + "pythondata-misc-usb_ohci @ git+https://github.com/robtaylor/pythondata-misc-usb_ohci@f366de36d0cf04ca9b96614a424ae4feea0851a8", + "pydantic>=2.0", + "tomli>=2.0", + "yowasp-yosys>=0.50", # For SystemVerilog support via built-in yosys-slang ] # Build system configuration @@ -32,6 +36,8 @@ build-backend = "pdm.backend" includes = [ "**/*.py", "**/*.v", + "**/*.sv", + "**/*.toml", "**/*.yaml" ] source-includes = [ @@ -47,11 +53,12 @@ purelib = [ # Development workflow configuration [tool.pyright] -diagnosticMode=false -typeCheckingMode = "off" reportInvalidTypeForm = false reportMissingImports = false reportUnboundVariable = false +reportAttributeAccessIssue = false +reportWildcardImportFromLibrary = false +ignore = [ "tests", "vendor" ] [tool.ruff.lint] select = ['E4', 'E7', 'E9', 'F', 'W291', 'W293'] @@ -75,4 +82,6 @@ dev = [ "pytest>=7.2.0", "pytest-cov>=0.6", "sphinx>=7.0", + "pyright>=1.1.407", + "amaranth-stubs>=0.1.1", ] diff --git a/tests/test_verilog_wrapper.py b/tests/test_verilog_wrapper.py new file mode 100644 index 0000000..8a9d29f --- /dev/null +++ b/tests/test_verilog_wrapper.py @@ -0,0 +1,602 @@ +# amaranth: UnusedElaboratable=no + +# SPDX-License-Identifier: BSD-2-Clause + +import tempfile +import unittest +import warnings +from pathlib import Path + +from amaranth import * +from amaranth.hdl import UnusedElaboratable + +from chipflow import ChipFlowError +from chipflow.rtl.wrapper import ( + DriverConfig, + ExternalWrapConfig, + Files, + Generate, + GenerateSV2V, + Generators, + Port, + RTLWrapper as VerilogWrapper, + _flatten_port_map, + _generate_auto_map, + _infer_auto_map, + _infer_signal_direction, + _INTERFACE_PATTERNS, + _parse_signal_direction, + _parse_verilog_ports, + _resolve_interface_type, + load_wrapper_from_toml, +) + + +class HelperFunctionsTestCase(unittest.TestCase): + def test_parse_signal_direction_input(self): + self.assertEqual(_parse_signal_direction("i_clk"), "i") + self.assertEqual(_parse_signal_direction("i_data_in"), "i") + + def test_parse_signal_direction_output(self): + self.assertEqual(_parse_signal_direction("o_data_out"), "o") + self.assertEqual(_parse_signal_direction("o_valid"), "o") + + def test_parse_signal_direction_default(self): + self.assertEqual(_parse_signal_direction("clk"), "i") + self.assertEqual(_parse_signal_direction("data"), "i") + + def test_flatten_port_map_string(self): + result = _flatten_port_map("i_signal") + self.assertEqual(result, {"": "i_signal"}) + + def test_flatten_port_map_simple_dict(self): + result = _flatten_port_map({"cyc": "i_cyc", "stb": "i_stb"}) + self.assertEqual(result, {"cyc": "i_cyc", "stb": "i_stb"}) + + def test_flatten_port_map_nested_dict(self): + result = _flatten_port_map({ + "dat": {"r": "o_dat_r", "w": "i_dat_w"}, + "cyc": "i_cyc" + }) + self.assertEqual(result, { + "dat.r": "o_dat_r", + "dat.w": "i_dat_w", + "cyc": "i_cyc" + }) + + def test_resolve_interface_type_simple(self): + result = _resolve_interface_type("amaranth.lib.wiring.Out(1)") + self.assertEqual(result, ("Out", 1)) + + result = _resolve_interface_type("amaranth.lib.wiring.In(8)") + self.assertEqual(result, ("In", 8)) + + def test_resolve_interface_type_invalid(self): + with self.assertRaises(ChipFlowError): + _resolve_interface_type("invalid") + + +class FilesConfigTestCase(unittest.TestCase): + def test_files_module_only(self): + files = Files(module="os.path") + self.assertEqual(files.module, "os.path") + self.assertIsNone(files.path) + + def test_files_path_only(self): + files = Files(path=Path("/tmp")) + self.assertIsNone(files.module) + self.assertEqual(files.path, Path("/tmp")) + + def test_files_neither_raises(self): + with self.assertRaises(ValueError): + Files() + + def test_files_both_raises(self): + with self.assertRaises(ValueError): + Files(module="os.path", path=Path("/tmp")) + + +class ExternalWrapConfigTestCase(unittest.TestCase): + def test_minimal_config(self): + config = ExternalWrapConfig( + name="TestModule", + files=Files(path=Path("/tmp")), + ) + self.assertEqual(config.name, "TestModule") + self.assertEqual(config.clocks, {}) + self.assertEqual(config.resets, {}) + self.assertEqual(config.ports, {}) + self.assertEqual(config.pins, {}) + + def test_config_with_ports(self): + config = ExternalWrapConfig( + name="TestModule", + files=Files(path=Path("/tmp")), + ports={ + "interrupt": Port( + interface="amaranth.lib.wiring.Out(1)", + map="o_interrupt" + ) + }, + clocks={"sys": "clk"}, + resets={"sys": "rst_n"}, + ) + self.assertEqual(config.name, "TestModule") + self.assertIn("interrupt", config.ports) + self.assertEqual(config.clocks["sys"], "clk") + self.assertEqual(config.resets["sys"], "rst_n") + + +class VerilogWrapperTestCase(unittest.TestCase): + def setUp(self): + warnings.simplefilter(action="ignore", category=UnusedElaboratable) + + def test_simple_wrapper(self): + config = ExternalWrapConfig( + name="TestModule", + files=Files(path=Path("/tmp")), + ports={ + "interrupt": Port( + interface="amaranth.lib.wiring.Out(1)", + map="o_interrupt" + ) + }, + clocks={"sys": "clk"}, + resets={"sys": "rst_n"}, + ) + + wrapper = VerilogWrapper(config) + self.assertEqual(wrapper._config.name, "TestModule") + # Check that the signature has the interrupt port + self.assertIn("interrupt", wrapper.signature.members) + + def test_elaborate_creates_instance(self): + config = ExternalWrapConfig( + name="TestModule", + files=Files(path=Path("/tmp")), + ports={ + "interrupt": Port( + interface="amaranth.lib.wiring.Out(1)", + map="o_interrupt" + ) + }, + clocks={"sys": "clk"}, + resets={"sys": "rst_n"}, + ) + + wrapper = VerilogWrapper(config) + # Elaborate with no platform (simulation mode) + m = wrapper.elaborate(platform=None) + self.assertIsInstance(m, Module) + # Check that the wrapped submodule exists + self.assertTrue(hasattr(m.submodules, "wrapped")) + + +class LoadWrapperFromTomlTestCase(unittest.TestCase): + def setUp(self): + warnings.simplefilter(action="ignore", category=UnusedElaboratable) + + def test_load_simple_toml(self): + toml_content = """ +name = 'SimpleTest' + +[files] +path = '/tmp' + +[clocks] +sys = 'clk' + +[resets] +sys = 'rst_n' + +[ports.interrupt] +interface = 'amaranth.lib.wiring.Out(1)' +map = 'o_interrupt' +""" + with tempfile.NamedTemporaryFile(mode='w', suffix='.toml', delete=False) as f: + f.write(toml_content) + toml_path = Path(f.name) + + try: + wrapper = load_wrapper_from_toml(toml_path) + self.assertEqual(wrapper._config.name, "SimpleTest") + self.assertIn("interrupt", wrapper.signature.members) + finally: + toml_path.unlink() + + def test_load_invalid_toml_raises(self): + toml_content = """ +# Missing required 'name' field +[files] +path = '/tmp' +""" + with tempfile.NamedTemporaryFile(mode='w', suffix='.toml', delete=False) as f: + f.write(toml_content) + toml_path = Path(f.name) + + try: + with self.assertRaises(ChipFlowError): + load_wrapper_from_toml(toml_path) + finally: + toml_path.unlink() + + +class SystemVerilogConfigTestCase(unittest.TestCase): + def test_generators_enum(self): + self.assertEqual(Generators.VERILOG, "verilog") + self.assertEqual(Generators.SYSTEMVERILOG, "systemverilog") + self.assertEqual(Generators.SPINALHDL, "spinalhdl") + + def test_generate_config_systemverilog(self): + gen = Generate( + generator=Generators.SYSTEMVERILOG, + sv2v=GenerateSV2V( + top_module="wb_timer", + include_dirs=["inc"], + defines={"SIMULATION": "1"} + ) + ) + self.assertEqual(gen.generator, Generators.SYSTEMVERILOG) + self.assertIsNotNone(gen.sv2v) + self.assertEqual(gen.sv2v.top_module, "wb_timer") + self.assertIn("inc", gen.sv2v.include_dirs) + + def test_sv2v_config_defaults(self): + sv2v = GenerateSV2V() + self.assertEqual(sv2v.include_dirs, []) + self.assertEqual(sv2v.defines, {}) + self.assertIsNone(sv2v.top_module) + + def test_config_with_systemverilog_generator(self): + config = ExternalWrapConfig( + name="SVModule", + files=Files(path=Path("/tmp")), + generate=Generate( + generator=Generators.SYSTEMVERILOG, + sv2v=GenerateSV2V(top_module="test") + ), + clocks={"sys": "clk"}, + resets={"sys": "rst_n"}, + ) + self.assertEqual(config.name, "SVModule") + self.assertEqual(config.generate.generator, Generators.SYSTEMVERILOG) + + def test_load_systemverilog_toml(self): + toml_content = """ +name = 'SVTest' + +[files] +path = '/tmp' + +[generate] +generator = 'systemverilog' + +[generate.sv2v] +top_module = 'test_module' +include_dirs = ['inc', 'src'] +defines = { DEBUG = '1', FEATURE_A = '' } + +[clocks] +sys = 'clk' + +[resets] +sys = 'rst_n' + +[ports.irq] +interface = 'amaranth.lib.wiring.Out(1)' +map = 'o_irq' +""" + with tempfile.NamedTemporaryFile(mode='w', suffix='.toml', delete=False) as f: + f.write(toml_content) + toml_path = Path(f.name) + + try: + # This will fail at sv2v stage since no .sv files exist, but config parsing should work + # So we test the config parsing separately + import tomli + with open(toml_path, 'rb') as f: + raw = tomli.load(f) + config = ExternalWrapConfig.model_validate(raw) + self.assertEqual(config.name, "SVTest") + self.assertEqual(config.generate.generator, Generators.SYSTEMVERILOG) + self.assertEqual(config.generate.sv2v.top_module, "test_module") + finally: + toml_path.unlink() + + +class DriverConfigTestCase(unittest.TestCase): + def test_driver_config_defaults(self): + driver = DriverConfig() + self.assertIsNone(driver.regs_struct) + self.assertEqual(driver.c_files, []) + self.assertEqual(driver.h_files, []) + + def test_driver_config_full(self): + driver = DriverConfig( + regs_struct='timer_regs_t', + c_files=['drivers/timer.c'], + h_files=['drivers/timer.h'], + ) + self.assertEqual(driver.regs_struct, 'timer_regs_t') + self.assertEqual(driver.c_files, ['drivers/timer.c']) + self.assertEqual(driver.h_files, ['drivers/timer.h']) + + +class PortDirectionTestCase(unittest.TestCase): + def test_port_direction_explicit(self): + port = Port( + interface='amaranth.lib.wiring.Out(1)', + map='o_signal', + direction='out' + ) + self.assertEqual(port.direction, 'out') + + def test_port_direction_none(self): + port = Port( + interface='amaranth.lib.wiring.Out(1)', + map='o_signal', + ) + self.assertIsNone(port.direction) + + def test_config_with_ports_and_pins(self): + config = ExternalWrapConfig( + name="TestModule", + files=Files(path=Path("/tmp")), + ports={ + "bus": Port( + interface="amaranth.lib.wiring.Out(1)", + map="i_bus", + direction="in" + ) + }, + pins={ + "irq": Port( + interface="amaranth.lib.wiring.Out(1)", + map="o_irq", + direction="out" + ) + }, + driver=DriverConfig( + regs_struct='test_regs_t', + h_files=['drivers/test.h'] + ) + ) + self.assertIn("bus", config.ports) + self.assertIn("irq", config.pins) + self.assertIsNotNone(config.driver) + self.assertEqual(config.driver.regs_struct, 'test_regs_t') + + def test_load_toml_with_driver(self): + toml_content = """ +name = 'DriverTest' + +[files] +path = '/tmp' + +[clocks] +sys = 'clk' + +[resets] +sys = 'rst_n' + +[ports.bus] +interface = 'amaranth.lib.wiring.Out(1)' +map = 'i_bus' +direction = 'in' + +[pins.irq] +interface = 'amaranth.lib.wiring.Out(1)' +map = 'o_irq' + +[driver] +regs_struct = 'my_regs_t' +c_files = ['drivers/my_driver.c'] +h_files = ['drivers/my_driver.h'] +""" + with tempfile.NamedTemporaryFile(mode='w', suffix='.toml', delete=False) as f: + f.write(toml_content) + toml_path = Path(f.name) + + try: + import tomli + with open(toml_path, 'rb') as f: + raw = tomli.load(f) + config = ExternalWrapConfig.model_validate(raw) + self.assertEqual(config.name, "DriverTest") + self.assertIn("bus", config.ports) + self.assertEqual(config.ports["bus"].direction, "in") + self.assertIn("irq", config.pins) + self.assertIsNotNone(config.driver) + self.assertEqual(config.driver.regs_struct, "my_regs_t") + self.assertEqual(config.driver.c_files, ["drivers/my_driver.c"]) + self.assertEqual(config.driver.h_files, ["drivers/my_driver.h"]) + finally: + toml_path.unlink() + + +class AutoMappingTestCase(unittest.TestCase): + def test_interface_patterns_has_known_interfaces(self): + """Verify the interface patterns registry contains expected entries.""" + self.assertIn("amaranth_soc.wishbone.Signature", _INTERFACE_PATTERNS) + self.assertIn("amaranth_soc.csr.Signature", _INTERFACE_PATTERNS) + self.assertIn("chipflow.platform.GPIOSignature", _INTERFACE_PATTERNS) + self.assertIn("chipflow.platform.UARTSignature", _INTERFACE_PATTERNS) + self.assertIn("chipflow.platform.I2CSignature", _INTERFACE_PATTERNS) + self.assertIn("chipflow.platform.SPISignature", _INTERFACE_PATTERNS) + + def test_parse_verilog_ports_ansi_style(self): + """Test parsing ANSI-style Verilog port declarations.""" + verilog = """ +module test_module ( + input logic i_clk, + input logic i_rst_n, + input logic i_wb_cyc, + input logic i_wb_stb, + output logic [31:0] o_wb_dat, + output logic o_wb_ack +); +endmodule +""" + ports = _parse_verilog_ports(verilog, "test_module") + self.assertEqual(ports.get("i_clk"), "input") + self.assertEqual(ports.get("i_wb_cyc"), "input") + self.assertEqual(ports.get("o_wb_dat"), "output") + self.assertEqual(ports.get("o_wb_ack"), "output") + + def test_infer_signal_direction(self): + """Test inferring signal direction from naming conventions.""" + # Prefixes + self.assertEqual(_infer_signal_direction("i_clk"), "i") + self.assertEqual(_infer_signal_direction("o_data"), "o") + self.assertEqual(_infer_signal_direction("in_signal"), "i") + self.assertEqual(_infer_signal_direction("out_signal"), "o") + # Suffixes + self.assertEqual(_infer_signal_direction("clk_i"), "i") + self.assertEqual(_infer_signal_direction("data_o"), "o") + self.assertEqual(_infer_signal_direction("enable_oe"), "o") + # Unknown + self.assertEqual(_infer_signal_direction("signal"), "io") + + def test_infer_auto_map_wishbone(self): + """Test auto-inferring Wishbone mapping from Verilog ports.""" + verilog_ports = { + "i_wb_cyc": "input", + "i_wb_stb": "input", + "i_wb_we": "input", + "i_wb_sel": "input", + "i_wb_adr": "input", + "i_wb_dat": "input", + "o_wb_dat": "output", + "o_wb_ack": "output", + } + result = _infer_auto_map("amaranth_soc.wishbone.Signature", verilog_ports, "in") + self.assertEqual(result.get("cyc"), "i_wb_cyc") + self.assertEqual(result.get("stb"), "i_wb_stb") + self.assertEqual(result.get("we"), "i_wb_we") + self.assertEqual(result.get("ack"), "o_wb_ack") + + def test_infer_auto_map_uart(self): + """Test auto-inferring UART mapping from Verilog ports.""" + verilog_ports = { + "uart_tx": "output", + "uart_rx": "input", + } + result = _infer_auto_map("chipflow.platform.UARTSignature", verilog_ports, "out") + self.assertEqual(result.get("tx.o"), "uart_tx") + self.assertEqual(result.get("rx.i"), "uart_rx") + + def test_generate_auto_map_fallback(self): + """Test fallback prefix-based auto-mapping.""" + result = _generate_auto_map("amaranth_soc.wishbone.Signature", "wb", "in") + self.assertIn("cyc", result) + self.assertIn("stb", result) + self.assertIn("ack", result) + + def test_generate_auto_map_simple_out(self): + """Test auto-mapping for simple Out(1) interface.""" + result = _generate_auto_map("amaranth.lib.wiring.Out(1)", "irq", "out") + self.assertEqual(result[""], "o_irq") + + def test_generate_auto_map_simple_in(self): + """Test auto-mapping for simple In(1) interface.""" + result = _generate_auto_map("amaranth.lib.wiring.In(8)", "data", "in") + self.assertEqual(result[""], "i_data") + + def test_generate_auto_map_unknown_interface(self): + """Test that unknown interfaces raise an error.""" + with self.assertRaises(ChipFlowError) as ctx: + _generate_auto_map("unknown.interface.Type", "prefix", "in") + self.assertIn("No auto-mapping available", str(ctx.exception)) + + def test_port_with_auto_map(self): + """Test Port configuration without explicit map.""" + port = Port( + interface='amaranth_soc.wishbone.Signature', + prefix='wb', + ) + self.assertIsNone(port.map) + self.assertEqual(port.prefix, 'wb') + + def test_port_with_explicit_map_and_prefix(self): + """Test Port configuration with both map and prefix (map takes precedence).""" + port = Port( + interface='amaranth.lib.wiring.Out(1)', + map='o_custom_signal', + prefix='ignored', + ) + self.assertEqual(port.map, 'o_custom_signal') + self.assertEqual(port.prefix, 'ignored') + + def test_config_with_auto_mapped_ports(self): + """Test ExternalWrapConfig with auto-mapped ports.""" + config = ExternalWrapConfig( + name="AutoMapTest", + files=Files(path=Path("/tmp")), + ports={ + "bus": Port( + interface="amaranth_soc.wishbone.Signature", + prefix="wb", + params={"addr_width": 4, "data_width": 32, "granularity": 8} + ) + }, + pins={ + "irq": Port( + interface="amaranth.lib.wiring.Out(1)", + prefix="irq" + ) + }, + clocks={"sys": "clk"}, + resets={"sys": "rst_n"}, + ) + self.assertEqual(config.name, "AutoMapTest") + self.assertIsNone(config.ports["bus"].map) + self.assertEqual(config.ports["bus"].prefix, "wb") + + def test_load_toml_with_auto_map(self): + """Test loading TOML with auto-mapped port.""" + toml_content = """ +name = 'AutoMapTomlTest' + +[files] +path = '/tmp' + +[clocks] +sys = 'clk' + +[resets] +sys = 'rst_n' + +[ports.bus] +interface = 'amaranth_soc.wishbone.Signature' +prefix = 'wb' +direction = 'in' + +[ports.bus.params] +addr_width = 4 +data_width = 32 +granularity = 8 + +[pins.irq] +interface = 'amaranth.lib.wiring.Out(1)' +prefix = 'irq' +""" + with tempfile.NamedTemporaryFile(mode='w', suffix='.toml', delete=False) as f: + f.write(toml_content) + toml_path = Path(f.name) + + try: + import tomli + with open(toml_path, 'rb') as f: + raw = tomli.load(f) + config = ExternalWrapConfig.model_validate(raw) + self.assertEqual(config.name, "AutoMapTomlTest") + self.assertIsNone(config.ports["bus"].map) + self.assertEqual(config.ports["bus"].prefix, "wb") + self.assertIsNone(config.pins["irq"].map) + self.assertEqual(config.pins["irq"].prefix, "irq") + finally: + toml_path.unlink() + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_wb_timer.py b/tests/test_wb_timer.py new file mode 100644 index 0000000..949394b --- /dev/null +++ b/tests/test_wb_timer.py @@ -0,0 +1,316 @@ +# amaranth: UnusedElaboratable=no + +# SPDX-License-Identifier: BSD-2-Clause + +"""Tests for the wb_timer Wishbone timer peripheral. + +This module tests the wb_timer SystemVerilog IP wrapped as an Amaranth component +via the VerilogWrapper system. It demonstrates how external Verilog/SystemVerilog +modules can be integrated and tested within the ChipFlow ecosystem. + +Configuration and signature tests work without additional dependencies. +Simulation tests require yosys with slang plugin (or yowasp-yosys) and use +CXXRTL for fast compiled simulation of the SystemVerilog code. +""" + +import importlib.util +import shutil +import unittest +import warnings +from pathlib import Path + +from amaranth import Module +from amaranth.hdl import UnusedElaboratable + +from chipflow.rtl import load_wrapper_from_toml + + +# Path to the wb_timer TOML configuration +WB_TIMER_TOML = Path(__file__).parent.parent / "chipflow_digital_ip" / "io" / "sv_timer" / "wb_timer.toml" + + +def _has_yosys_slang() -> bool: + """Check if yosys with slang plugin is available.""" + # Try yowasp-yosys first (use find_spec to avoid F401 lint warning) + if importlib.util.find_spec("yowasp_yosys") is not None: + return True + + # Try native yosys with slang + if shutil.which("yosys"): + import subprocess + try: + result = subprocess.run( + ["yosys", "-m", "slang", "-p", "help read_slang"], + capture_output=True, + timeout=10 + ) + return result.returncode == 0 + except (subprocess.TimeoutExpired, FileNotFoundError): + pass + + return False + + +def _has_chipflow_sim() -> bool: + """Check if chipflow.sim module is available for CXXRTL simulation.""" + return importlib.util.find_spec("chipflow.sim") is not None + + +class WbTimerConfigTestCase(unittest.TestCase): + """Test the wb_timer TOML configuration loading.""" + + def setUp(self): + warnings.simplefilter(action="ignore", category=UnusedElaboratable) + + def test_toml_exists(self): + """Verify the wb_timer TOML configuration file exists.""" + self.assertTrue(WB_TIMER_TOML.exists(), f"TOML not found at {WB_TIMER_TOML}") + + def test_load_wrapper_config(self): + """Test that the wb_timer wrapper can be loaded (config parsing only).""" + # This test verifies TOML parsing works + # It will fail at Verilog file loading if sv2v is not installed + # but the config parsing should succeed + import tomli + from chipflow.rtl.wrapper import ExternalWrapConfig + + with open(WB_TIMER_TOML, "rb") as f: + raw_config = tomli.load(f) + + config = ExternalWrapConfig.model_validate(raw_config) + + self.assertEqual(config.name, "wb_timer") + self.assertIn("bus", config.ports) + self.assertIn("irq", config.pins) + self.assertEqual(config.ports["bus"].interface, "amaranth_soc.wishbone.Signature") + self.assertEqual(config.pins["irq"].interface, "amaranth.lib.wiring.Out(1)") + + def test_driver_config(self): + """Test that driver configuration is present.""" + import tomli + from chipflow.rtl.wrapper import ExternalWrapConfig + + with open(WB_TIMER_TOML, "rb") as f: + raw_config = tomli.load(f) + + config = ExternalWrapConfig.model_validate(raw_config) + + self.assertIsNotNone(config.driver) + self.assertEqual(config.driver.regs_struct, "wb_timer_regs_t") + self.assertIn("drivers/wb_timer.h", config.driver.h_files) + + +@unittest.skipUnless(_has_yosys_slang(), "yosys with slang plugin not available") +class WbTimerWrapperTestCase(unittest.TestCase): + """Test the wb_timer wrapper instantiation (requires yosys-slang).""" + + def setUp(self): + warnings.simplefilter(action="ignore", category=UnusedElaboratable) + # Use a local directory instead of /tmp - yowasp-yosys (WASM) can't access /tmp + self._generate_dest = Path("build/test_wb_timer").absolute() + self._generate_dest.mkdir(parents=True, exist_ok=True) + + def tearDown(self): + import shutil as sh + sh.rmtree(self._generate_dest, ignore_errors=True) + + def test_load_wrapper(self): + """Test loading the complete wb_timer wrapper.""" + wrapper = load_wrapper_from_toml(WB_TIMER_TOML, generate_dest=Path(self._generate_dest)) + + self.assertEqual(wrapper._config.name, "wb_timer") + # Check signature has the expected members + self.assertIn("bus", wrapper.signature.members) + self.assertIn("irq", wrapper.signature.members) + + def test_wrapper_elaborate(self): + """Test that the wrapper can be elaborated.""" + wrapper = load_wrapper_from_toml(WB_TIMER_TOML, generate_dest=Path(self._generate_dest)) + + # Elaborate with no platform (simulation mode) + m = wrapper.elaborate(platform=None) + self.assertIsInstance(m, Module) + + +@unittest.skipUnless(_has_yosys_slang(), "yosys with slang plugin not available") +@unittest.skipUnless(_has_chipflow_sim(), "chipflow.sim not available") +class WbTimerCxxrtlSimulationTestCase(unittest.TestCase): + """CXXRTL simulation tests for the wb_timer peripheral. + + These tests verify the timer functionality through Wishbone bus transactions + using CXXRTL compiled simulation. Unlike Amaranth's Python simulator, + CXXRTL actually executes the SystemVerilog code. + + Register map (32-bit registers, word-addressed): + 0x0: CTRL - [31:16] prescaler, [1] irq_en, [0] enable + 0x1: COMPARE - Compare value for timer match + 0x2: COUNTER - Current counter (read) / Reload value (write) + 0x3: STATUS - [1] match, [0] irq_pending + """ + + # Register addresses (word-addressed for 32-bit Wishbone) + REG_CTRL = 0x0 + REG_COMPARE = 0x1 + REG_COUNTER = 0x2 + REG_STATUS = 0x3 + + # Control register bits + CTRL_ENABLE = 1 << 0 + CTRL_IRQ_EN = 1 << 1 + + @classmethod + def setUpClass(cls): + """Build the CXXRTL simulator once for all tests.""" + warnings.simplefilter(action="ignore", category=UnusedElaboratable) + cls._build_dir = Path("build/test_wb_timer_cxxrtl").absolute() + cls._build_dir.mkdir(parents=True, exist_ok=True) + + # Load wrapper and build simulator + cls._wrapper = load_wrapper_from_toml(WB_TIMER_TOML, generate_dest=cls._build_dir) + cls._sim = cls._wrapper.build_simulator(cls._build_dir) + + @classmethod + def tearDownClass(cls): + """Clean up simulator and build artifacts.""" + if hasattr(cls, "_sim"): + cls._sim.close() + shutil.rmtree(cls._build_dir, ignore_errors=True) + + def setUp(self): + """Reset simulator state before each test.""" + self._reset() + + def _tick(self): + """Perform a clock cycle.""" + self._sim.set("i_clk", 0) + self._sim.step() + self._sim.set("i_clk", 1) + self._sim.step() + + def _reset(self): + """Reset the design.""" + self._sim.set("i_rst_n", 0) + self._sim.set("i_clk", 0) + self._tick() + self._tick() + self._sim.set("i_rst_n", 1) + self._tick() + + def _wb_write(self, addr: int, data: int): + """Perform a Wishbone write transaction.""" + self._sim.set("i_wb_cyc", 1) + self._sim.set("i_wb_stb", 1) + self._sim.set("i_wb_we", 1) + self._sim.set("i_wb_adr", addr) + self._sim.set("i_wb_dat", data) + self._sim.set("i_wb_sel", 0xF) + + # Clock until ack + for _ in range(10): + self._tick() + if self._sim.get("o_wb_ack"): + break + + self._sim.set("i_wb_cyc", 0) + self._sim.set("i_wb_stb", 0) + self._sim.set("i_wb_we", 0) + self._tick() + + def _wb_read(self, addr: int) -> int: + """Perform a Wishbone read transaction.""" + self._sim.set("i_wb_cyc", 1) + self._sim.set("i_wb_stb", 1) + self._sim.set("i_wb_we", 0) + self._sim.set("i_wb_adr", addr) + self._sim.set("i_wb_sel", 0xF) + + # Clock until ack + for _ in range(10): + self._tick() + if self._sim.get("o_wb_ack"): + break + + data = self._sim.get("o_wb_dat") + + self._sim.set("i_wb_cyc", 0) + self._sim.set("i_wb_stb", 0) + self._tick() + + return data + + def test_reset(self): + """Test that reset clears state.""" + ctrl = self._wb_read(self.REG_CTRL) + self.assertEqual(ctrl, 0, "CTRL should be 0 after reset") + + def test_register_write_read(self): + """Test register write and readback.""" + # Write to COMPARE register + self._wb_write(self.REG_COMPARE, 0x12345678) + + # Read back + value = self._wb_read(self.REG_COMPARE) + self.assertEqual(value, 0x12345678, "COMPARE should retain written value") + + def test_timer_enable_and_count(self): + """Test that the timer counts when enabled.""" + # Set compare value high so we don't trigger a match + self._wb_write(self.REG_COMPARE, 0xFFFFFFFF) + + # Enable timer with prescaler=0 (count every cycle) + self._wb_write(self.REG_CTRL, self.CTRL_ENABLE) + + # Let it count for a few cycles + for _ in range(20): + self._tick() + + # Read counter value - should be > 0 + count = self._wb_read(self.REG_COUNTER) + self.assertGreater(count, 0, "Counter should have incremented") + + def test_timer_match_and_irq(self): + """Test that compare match sets status and IRQ.""" + # Set compare value to 5 + self._wb_write(self.REG_COMPARE, 5) + + # Enable timer with IRQ enabled + self._wb_write(self.REG_CTRL, self.CTRL_ENABLE | self.CTRL_IRQ_EN) + + # Wait for match (counter should reach 5) + irq_fired = False + for _ in range(50): + self._tick() + if self._sim.get("o_irq"): + irq_fired = True + break + + # Check IRQ is asserted + self.assertTrue(irq_fired, "IRQ should fire on compare match") + + # Check status register + status = self._wb_read(self.REG_STATUS) + self.assertTrue(status & 0x1, "IRQ pending flag should be set") + self.assertTrue(status & 0x2, "Match flag should be set") + + def test_timer_prescaler(self): + """Test that prescaler divides the count rate.""" + # Set compare value high + self._wb_write(self.REG_COMPARE, 0xFFFFFFFF) + + # Enable timer with prescaler=3 (count every 4 cycles) + # Prescaler is in upper 16 bits of CTRL + prescaler = 3 << 16 + self._wb_write(self.REG_CTRL, self.CTRL_ENABLE | prescaler) + + # Run for 20 cycles - should get 20/4 = 5 counts + for _ in range(20): + self._tick() + + count = self._wb_read(self.REG_COUNTER) + # Allow some tolerance for setup cycles + self.assertGreater(count, 0, "Counter should have incremented") + self.assertLessEqual(count, 6, "Counter should be limited by prescaler") + + +if __name__ == "__main__": + unittest.main()