diff --git a/CHANGES.md b/CHANGES.md index 275ef60b7c..b410d6022b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -36,6 +36,8 @@ * **litex/soc/integration/export** : Added wide CSR field helper support to generated software exports ([cc4a2542b](https://github.com/enjoy-digital/litex/commit/cc4a2542b)). * **litex/soc/cores/hyperbus / integration** : Added native AXI/AXI-Lite HyperRAM support, automatic bus-standard argument selection, and a SoC-level `add_hyperram` helper ([53727e564](https://github.com/enjoy-digital/litex/commit/53727e564), [545985891](https://github.com/enjoy-digital/litex/commit/545985891), [d6a73d241](https://github.com/enjoy-digital/litex/commit/d6a73d241)). * **litex/soc/integration/soc** : Added slave-bus clock-domain crossing support and a SoC reset request hook ([4a8ae302e](https://github.com/enjoy-digital/litex/commit/4a8ae302e), [973351aa8](https://github.com/enjoy-digital/litex/commit/973351aa8)). +* **litex/soc/cores/cpu/cyclonev_hps** : Added Intel Cyclone V HPS (hard dual Cortex-A9) hardcore CPU support with H2F-LW/H2F AXI3 masters, F2H AXI3 slave, F2SDRAM Avalon-MM ports and F2H interrupts through direct `cyclonev_hps_interface_*` atom instantiation (no Qsys), targeting boards where the HPS is pre-booted (e.g. MiSTer/DE10-Nano), with GIC-aware BIOS interrupt support (Cortex-A9 exception vectors + GIC-390 mask/pending/dispatch). +* **litex/soc/interconnect/avalon** : Added `Wishbone2AvalonMM` bridge (Wishbone Slave to Avalon-MM Master, single-beat) with simulation coverage. * **litex/soc/cores/cpu** : Added Loongson GS232 and CDIM MIPS CPU integrations plus Linux toolchain/package support and boot coverage ([e90b7e8af](https://github.com/enjoy-digital/litex/commit/e90b7e8af), [b7ed16b19](https://github.com/enjoy-digital/litex/commit/b7ed16b19), [af19e210a](https://github.com/enjoy-digital/litex/commit/af19e210a), [060c51988](https://github.com/enjoy-digital/litex/commit/060c51988), [6e5982265](https://github.com/enjoy-digital/litex/commit/6e5982265), [4a3f398c2](https://github.com/enjoy-digital/litex/commit/4a3f398c2)). * **litex/sim / soc/cores/cpu/qemu** : Added RISC-V QEMU co-simulation CPU support, AXI co-simulation bridge integration, LiteX simulation wiring, and smoke coverage ([b92b7f0f8](https://github.com/enjoy-digital/litex/commit/b92b7f0f8), [464df90e6](https://github.com/enjoy-digital/litex/commit/464df90e6), [6ff0f1560](https://github.com/enjoy-digital/litex/commit/6ff0f1560), [876ddf83e](https://github.com/enjoy-digital/litex/commit/876ddf83e), [da55d8846](https://github.com/enjoy-digital/litex/commit/da55d8846)). * **litex/soc/software/bios** : Added extensible boot-method registration, documented BIOS configuration flags, reported BIOS SRAM stack margin, and added an option to disable ANSI output ([c612a0194](https://github.com/enjoy-digital/litex/commit/c612a0194), [11a4ddfc0](https://github.com/enjoy-digital/litex/commit/11a4ddfc0), [11f94eaa6](https://github.com/enjoy-digital/litex/commit/11f94eaa6), [b26d1096d](https://github.com/enjoy-digital/litex/commit/b26d1096d)). diff --git a/litex/soc/cores/cpu/cyclonev_hps/__init__.py b/litex/soc/cores/cpu/cyclonev_hps/__init__.py new file mode 100644 index 0000000000..9e702dda95 --- /dev/null +++ b/litex/soc/cores/cpu/cyclonev_hps/__init__.py @@ -0,0 +1 @@ +from litex.soc.cores.cpu.cyclonev_hps.core import CycloneVHPS diff --git a/litex/soc/cores/cpu/cyclonev_hps/boot-helper.c b/litex/soc/cores/cpu/cyclonev_hps/boot-helper.c new file mode 100644 index 0000000000..f16e05298f --- /dev/null +++ b/litex/soc/cores/cpu/cyclonev_hps/boot-helper.c @@ -0,0 +1,5 @@ +void boot_helper(unsigned long r1, unsigned long r2, unsigned long r3, unsigned long addr); + +void boot_helper(unsigned long r1, unsigned long r2, unsigned long r3, unsigned long addr) { + goto *(void*)addr; +} diff --git a/litex/soc/cores/cpu/cyclonev_hps/core.py b/litex/soc/cores/cpu/cyclonev_hps/core.py new file mode 100644 index 0000000000..607987d317 --- /dev/null +++ b/litex/soc/cores/cpu/cyclonev_hps/core.py @@ -0,0 +1,446 @@ +# +# This file is part of LiteX. +# +# Copyright (c) 2026 Florent Kermarrec +# SPDX-License-Identifier: BSD-2-Clause + +from migen import * + +from litex.gen import * + +from litex.soc.interconnect import axi +from litex.soc.interconnect.avalon import AvalonMMInterface + +from litex.soc.cores.cpu import CPU + +# Constants ---------------------------------------------------------------------------------------- + +# H2F/F2H Bridges Port Size Config. +PORT_SIZE_CONFIG = { + 32 : 0b00, + 64 : 0b01, + 128 : 0b10, + None : 0b11, # Unused. +} + +# F2SDRAM Ports (fixed layout, matching the configuration applied by MiSTer's Preloader/U-Boot). +F2SDRAM_PORTS = { + # Data-Width / Word-Address-Width / Rd-Wr FIFOs. + 0 : dict(data_width=128, adr_width=28, fifos=[0, 1]), + 1 : dict(data_width= 64, adr_width=29, fifos=[2]), + 2 : dict(data_width= 64, adr_width=29, fifos=[3]), +} + +# Cyclone V HPS ------------------------------------------------------------------------------------ + +class CycloneVHPS(CPU): + variants = ["standard"] + category = "hardcore" + family = "arm" + name = "cyclonev_hps" + human_name = "Cyclone V HPS (Dual Cortex-A9)" + data_width = 32 + endianness = "little" + reset_address = 0x0100_0000 + reset_address_check = False # HPS boots from its own BootROM/SD-Card; reset_address is only + # used to link the (optional) BIOS in HPS DDR3. + gcc_triple = "arm-none-eabi" + gcc_flags = "-mcpu=cortex-a9 -mfpu=vfpv3 -mfloat-abi=hard -D__cyclonev_hps__" + linker_output_format = "elf32-littlearm" + nop = "nop" + io_regions = {0xff20_0000: 0x0020_0000} # Origin, Length: H2F LW bridge window. + csr_decode = True # AXI address is decoded in AXI2Wishbone, offset needs to be added in Software. + integrated_rom_supported = False + + # Memory Mapping (== ARM physical addresses). + @property + def mem_map(self): + return { + "sram" : 0x0010_0000, # HPS DDR3 in fact (skips first 1MB: ARM vectors/Preloader). + "rom" : 0x0100_0000, # HPS DDR3 in fact (Linker-only region, added by target). + "main_ram" : 0xc000_0000, # Fabric RAM seen by the ARM through the H2F bridge window. + "csr" : 0xff20_0000, # H2F LW bridge window. + } + + def __init__(self, platform, variant, *args, **kwargs): + super().__init__(*args, **kwargs) + self.platform = platform + self.reset = Signal() # Not used (HPS is not reset from the fabric). + self.interrupt = Signal(32) # F2H IRQs 0-31 (GIC IDs 72-103 / Linux GIC_SPI 40-71). + self.periph_buses = [] # Peripheral buses (Connected to main SoC's bus). + self.memory_buses = [] # Memory buses (Connected directly to LiteDRAM). + + self.h2f_lw_master = None # H2F LW AXI3 Master (HPS -> Fabric, CSRs access). + self.h2f_master = None # H2F AXI3 Master (HPS -> Fabric). + self.f2h_slave = None # F2H AXI3 Slave (Fabric -> HPS DDR3/Peripherals). + self.f2sdram_ports = {} # F2SDRAM Avalon-MM Ports (Fabric -> HPS SDRAM Controller). + + self.h2f_rst_n = Signal() # HPS -> Fabric reset. + + # # # + + # H2F LW as Bus Master --------------------------------------------------------------------- + self.pbus = self.add_axi_h2f_lw_master() + self.periph_buses.append(self.pbus) + + # H2F LW AXI3 Master (HPS -> Fabric) ----------------------------------------------------------- + def add_axi_h2f_lw_master(self, clock_domain="sys"): + assert self.h2f_lw_master is None + base = self.mem_map["csr"] + axi_h2f_lw = axi.AXIInterface( + data_width = 32, + address_width = 32, + id_width = 12, + version = "axi3", + clock_domain = clock_domain, + ) + self.h2f_lw_master = axi_h2f_lw + + # Extend the bridge's window-relative addresses to ARM physical addresses, so that the + # LiteX memory map matches the ARM's view (CSRs seen at mem_map["csr"] on both sides). + awaddr = Signal(21) + araddr = Signal(21) + self.comb += [ + axi_h2f_lw.aw.addr.eq(Cat(awaddr, Constant(base >> 21, 11))), + axi_h2f_lw.ar.addr.eq(Cat(araddr, Constant(base >> 21, 11))), + ] + + self.specials += Instance("cyclonev_hps_interface_hps2fpga_light_weight", + # Clk. + i_clk = ClockSignal(clock_domain), + + # AW. + o_awid = axi_h2f_lw.aw.id, + o_awaddr = awaddr, + o_awlen = axi_h2f_lw.aw.len, + o_awsize = axi_h2f_lw.aw.size, + o_awburst = axi_h2f_lw.aw.burst, + o_awlock = axi_h2f_lw.aw.lock, + o_awcache = axi_h2f_lw.aw.cache, + o_awprot = axi_h2f_lw.aw.prot, + o_awvalid = axi_h2f_lw.aw.valid, + i_awready = axi_h2f_lw.aw.ready, + + # W. + o_wid = axi_h2f_lw.w.id, + o_wdata = axi_h2f_lw.w.data, + o_wstrb = axi_h2f_lw.w.strb, + o_wlast = axi_h2f_lw.w.last, + o_wvalid = axi_h2f_lw.w.valid, + i_wready = axi_h2f_lw.w.ready, + + # B. + i_bid = axi_h2f_lw.b.id, + i_bresp = axi_h2f_lw.b.resp, + i_bvalid = axi_h2f_lw.b.valid, + o_bready = axi_h2f_lw.b.ready, + + # AR. + o_arid = axi_h2f_lw.ar.id, + o_araddr = araddr, + o_arlen = axi_h2f_lw.ar.len, + o_arsize = axi_h2f_lw.ar.size, + o_arburst = axi_h2f_lw.ar.burst, + o_arlock = axi_h2f_lw.ar.lock, + o_arcache = axi_h2f_lw.ar.cache, + o_arprot = axi_h2f_lw.ar.prot, + o_arvalid = axi_h2f_lw.ar.valid, + i_arready = axi_h2f_lw.ar.ready, + + # R. + i_rid = axi_h2f_lw.r.id, + i_rdata = axi_h2f_lw.r.data, + i_rresp = axi_h2f_lw.r.resp, + i_rlast = axi_h2f_lw.r.last, + i_rvalid = axi_h2f_lw.r.valid, + o_rready = axi_h2f_lw.r.ready, + ) + return axi_h2f_lw + + # H2F AXI3 Master (HPS -> Fabric) -------------------------------------------------------------- + def add_axi_h2f_master(self, data_width=32, clock_domain="sys"): + assert self.h2f_master is None + assert data_width in [32, 64, 128] + base = self.mem_map["main_ram"] + axi_h2f = axi.AXIInterface( + data_width = data_width, + address_width = 32, + id_width = 12, + version = "axi3", + clock_domain = clock_domain, + ) + self.h2f_master = axi_h2f + + # Extend the bridge's window-relative addresses to ARM physical addresses (window at + # 0xc000_0000, 960MB); bridge data/strb ports are 128-bit, LSB-aligned for 32/64-bit. + awaddr = Signal(30) + araddr = Signal(30) + wdata = Signal(128) + wstrb = Signal(16) + rdata = Signal(128) + self.comb += [ + axi_h2f.aw.addr.eq(Cat(awaddr, Constant(base >> 30, 2))), + axi_h2f.ar.addr.eq(Cat(araddr, Constant(base >> 30, 2))), + axi_h2f.w.data.eq(wdata), + axi_h2f.w.strb.eq(wstrb), + rdata.eq(axi_h2f.r.data), + ] + + self.specials += Instance("cyclonev_hps_interface_hps2fpga", + # Config/Clk. + i_port_size_config = PORT_SIZE_CONFIG[data_width], + i_clk = ClockSignal(clock_domain), + + # AW. + o_awid = axi_h2f.aw.id, + o_awaddr = awaddr, + o_awlen = axi_h2f.aw.len, + o_awsize = axi_h2f.aw.size, + o_awburst = axi_h2f.aw.burst, + o_awlock = axi_h2f.aw.lock, + o_awcache = axi_h2f.aw.cache, + o_awprot = axi_h2f.aw.prot, + o_awvalid = axi_h2f.aw.valid, + i_awready = axi_h2f.aw.ready, + + # W. + o_wid = axi_h2f.w.id, + o_wdata = wdata, + o_wstrb = wstrb, + o_wlast = axi_h2f.w.last, + o_wvalid = axi_h2f.w.valid, + i_wready = axi_h2f.w.ready, + + # B. + i_bid = axi_h2f.b.id, + i_bresp = axi_h2f.b.resp, + i_bvalid = axi_h2f.b.valid, + o_bready = axi_h2f.b.ready, + + # AR. + o_arid = axi_h2f.ar.id, + o_araddr = araddr, + o_arlen = axi_h2f.ar.len, + o_arsize = axi_h2f.ar.size, + o_arburst = axi_h2f.ar.burst, + o_arlock = axi_h2f.ar.lock, + o_arcache = axi_h2f.ar.cache, + o_arprot = axi_h2f.ar.prot, + o_arvalid = axi_h2f.ar.valid, + i_arready = axi_h2f.ar.ready, + + # R. + i_rid = axi_h2f.r.id, + i_rdata = rdata, + i_rresp = axi_h2f.r.resp, + i_rlast = axi_h2f.r.last, + i_rvalid = axi_h2f.r.valid, + o_rready = axi_h2f.r.ready, + ) + return axi_h2f + + # F2H AXI3 Slave (Fabric -> HPS) --------------------------------------------------------------- + def add_axi_f2h_slave(self, data_width=32, clock_domain="sys"): + # Gives Fabric masters access to the full HPS map: addresses < 0x8000_0000 reach HPS DDR3 + # non-coherently (ARM caches must be flushed/bypassed), ACP window at 0x8000_0000+ gives + # L2-coherent access. Returned interface is not connected to the SoC's shared bus (a H2F + # -> F2H loopback would deadlock); connect user DMAs directly to it. + assert self.f2h_slave is None + assert data_width in [32, 64, 128] + axi_f2h = axi.AXIInterface( + data_width = data_width, + address_width = 32, + id_width = 8, + version = "axi3", + clock_domain = clock_domain, + ) + self.f2h_slave = axi_f2h + + # Bridge data/strb ports are 128-bit, LSB-aligned for 32/64-bit. + wdata = Signal(128) + wstrb = Signal(16) + rdata = Signal(128) + self.comb += [ + wdata.eq(axi_f2h.w.data), + wstrb.eq(axi_f2h.w.strb), + axi_f2h.r.data.eq(rdata), + ] + + self.specials += Instance("cyclonev_hps_interface_fpga2hps", + # Config/Clk. + i_port_size_config = PORT_SIZE_CONFIG[data_width], + i_clk = ClockSignal(clock_domain), + + # AW. + i_awid = axi_f2h.aw.id, + i_awaddr = axi_f2h.aw.addr, + i_awlen = axi_f2h.aw.len, + i_awsize = axi_f2h.aw.size, + i_awburst = axi_f2h.aw.burst, + i_awlock = axi_f2h.aw.lock, + i_awcache = axi_f2h.aw.cache, + i_awprot = axi_f2h.aw.prot, + i_awuser = Constant(0b00001, 5), # Shareable (only relevant for ACP window). + i_awvalid = axi_f2h.aw.valid, + o_awready = axi_f2h.aw.ready, + + # W. + i_wid = axi_f2h.w.id, + i_wdata = wdata, + i_wstrb = wstrb, + i_wlast = axi_f2h.w.last, + i_wvalid = axi_f2h.w.valid, + o_wready = axi_f2h.w.ready, + + # B. + o_bid = axi_f2h.b.id, + o_bresp = axi_f2h.b.resp, + o_bvalid = axi_f2h.b.valid, + i_bready = axi_f2h.b.ready, + + # AR. + i_arid = axi_f2h.ar.id, + i_araddr = axi_f2h.ar.addr, + i_arlen = axi_f2h.ar.len, + i_arsize = axi_f2h.ar.size, + i_arburst = axi_f2h.ar.burst, + i_arlock = axi_f2h.ar.lock, + i_arcache = axi_f2h.ar.cache, + i_arprot = axi_f2h.ar.prot, + i_aruser = Constant(0b00001, 5), # Shareable (only relevant for ACP window). + i_arvalid = axi_f2h.ar.valid, + o_arready = axi_f2h.ar.ready, + + # R. + o_rid = axi_f2h.r.id, + o_rdata = rdata, + o_rresp = axi_f2h.r.resp, + o_rlast = axi_f2h.r.last, + o_rvalid = axi_f2h.r.valid, + i_rready = axi_f2h.r.ready, + ) + return axi_f2h + + # F2SDRAM Avalon-MM Port (Fabric -> HPS SDRAM Controller) -------------------------------------- + def add_fpga2sdram_port(self, n=0, clock_domain="sys"): + # Direct port to the HPS SDRAM Controller (higher bandwidth than F2H). The port layout is + # fixed (Port 0: 128-bit, Ports 1/2: 64-bit) since it has to match the configuration + # applied by the Preloader/U-Boot (applycfg), here MiSTer's one. Addresses are HPS DDR3 + # word addresses; accesses are non-coherent with the ARM caches. + assert n in F2SDRAM_PORTS.keys() + assert n not in self.f2sdram_ports.keys() + port = AvalonMMInterface( + data_width = F2SDRAM_PORTS[n]["data_width"], + adr_width = F2SDRAM_PORTS[n]["adr_width"], + ) + self.f2sdram_ports[n] = (port, clock_domain) + return port + + def do_finalize(self): + # Clocks/Resets (No Fabric -> HPS reset requests). + self.specials += Instance("cyclonev_hps_interface_clocks_resets", + i_f2h_cold_rst_req_n = 1, + i_f2h_dbg_rst_req_n = 1, + i_f2h_warm_rst_req_n = 1, + i_f2h_pending_rst_ack = 1, + o_h2f_rst_n = self.h2f_rst_n, + ) + + # Interrupts (F2H IRQs 0-31 used, 32-63 tied to 0). + self.specials += Instance("cyclonev_hps_interface_interrupts", + i_irq = Cat(self.interrupt, Constant(0, 32)), + ) + + # Trace (Disabled). + self.specials += Instance("cyclonev_hps_interface_tpiu_trace", + i_traceclk_ctl = 1, + ) + + # Boot from FPGA (Disabled: bsel_en/csel_en=0, csel/bsel values are then ignored). + self.specials += Instance("cyclonev_hps_interface_boot_from_fpga", + i_boot_from_fpga_ready = 0, + i_boot_from_fpga_on_failure = 0, + i_bsel_en = 0, + i_csel_en = 0, + i_csel = Constant(0b01, 2), + i_bsel = Constant(0b001, 3), + ) + + # Unused H2F/F2H Bridges (explicitly configured as unused). + if self.h2f_master is None: + self.specials += Instance("cyclonev_hps_interface_hps2fpga", + i_port_size_config = PORT_SIZE_CONFIG[None], + ) + if self.f2h_slave is None: + self.specials += Instance("cyclonev_hps_interface_fpga2hps", + i_port_size_config = PORT_SIZE_CONFIG[None], + ) + + # F2SDRAM (only instantiated when at least one port is used). + if len(self.f2sdram_ports): + f2sdram_params = dict( + # Config (fixed, matching MiSTer's: Port 0: 128-bit, Ports 1/2: 64-bit). + i_cfg_port_width = Constant(0b000000010110, 12), + i_cfg_cport_type = Constant(0b000000111111, 12), + i_cfg_axi_mm_select = Constant(0b000000, 6), + i_cfg_rfifo_cport_map = Constant(0b0010000100000000, 16), + i_cfg_wfifo_cport_map = Constant(0b0010000100000000, 16), + i_cfg_cport_rfifo_map = Constant(0b000000000011010000, 18), + i_cfg_cport_wfifo_map = Constant(0b000000000011010000, 18), + + # Rd/Wrack always ready (as MiSTer). + i_rd_ready_0 = 1, + i_rd_ready_1 = 1, + i_rd_ready_2 = 1, + i_rd_ready_3 = 1, + i_wrack_ready_0 = 1, + i_wrack_ready_1 = 1, + i_wrack_ready_2 = 1, + ) + for n, cfg in F2SDRAM_PORTS.items(): + port, clock_domain = self.f2sdram_ports.get(n, (None, "sys")) + clk = ClockSignal(clock_domain) + # Unused Port: only drive Clks, tie-off Cmd. + if port is None: + f2sdram_params.update({ + f"i_cmd_port_clk_{n}" : clk, + f"i_cmd_valid_{n}" : 0, + f"i_cmd_data_{n}" : 0, + }) + for fifo in cfg["fifos"]: + f2sdram_params.update({ + f"i_rd_clk_{fifo}" : clk, + f"i_wr_clk_{fifo}" : clk, + f"i_wr_data_{fifo}" : 0, + }) + continue + # Used Port: Cmd. + cmd_ready = Signal() + self.comb += port.waitrequest.eq(~cmd_ready) + f2sdram_params.update({ + f"i_cmd_port_clk_{n}" : clk, + f"i_cmd_valid_{n}" : port.read | port.write, + f"o_cmd_ready_{n}" : cmd_ready, + f"i_cmd_data_{n}" : Cat( + port.read, # 0: Read. + port.write, # 1: Write. + port.address, # 2+: Word Address. + Constant(0, 32 - cfg["adr_width"]), # :34: Padding. + port.burstcount, # 41:34: Burst Count. + Constant(0, 18)), # 59:42: Padding. + }) + # Used Port: Rd/Wr Data (64-bit per FIFO, 128-bit ports use 2 FIFOs). + for i, fifo in enumerate(cfg["fifos"]): + f2sdram_params.update({ + f"i_rd_clk_{fifo}" : clk, + f"o_rd_data_{fifo}" : port.readdata[64*i:64*(i + 1)], + f"i_wr_clk_{fifo}" : clk, + f"i_wr_data_{fifo}" : Cat( + port.writedata[ 64*i:64*(i + 1)], # 63: 0: Data. + Constant(0, 16), # 79:64: Padding. + port.byteenable[ 8*i: 8*(i + 1)], # 87:80: Byte-Enable. + Constant(0, 2)), # 89:88: Padding. + }) + # Used Port: Rd-Data Valid (from the port's last FIFO). + f2sdram_params.update({f"o_rd_valid_{cfg['fifos'][-1]}" : port.readdatavalid}) + self.specials += Instance("cyclonev_hps_interface_fpga2sdram", **f2sdram_params) diff --git a/litex/soc/cores/cpu/cyclonev_hps/crt0.S b/litex/soc/cores/cpu/cyclonev_hps/crt0.S new file mode 100644 index 0000000000..34422e6b80 --- /dev/null +++ b/litex/soc/cores/cpu/cyclonev_hps/crt0.S @@ -0,0 +1,97 @@ +/* + * This file is part of LiteX. + * + * Copyright (c) 2026 Florent Kermarrec + * SPDX-License-Identifier: BSD-2-Clause + * + * Minimal Cortex-A9 startup for the Cyclone V HPS: the BIOS is loaded/launched from the + * Preloader/U-Boot (ex: go 0x01000000), in Supervisor mode, MMU/Caches disabled (use U-Boot's + * "dcache flush; dcache off; icache off" before "go" if needed). + */ + +.syntax unified +.cpu cortex-a9 +.fpu vfpv3 +.arm + +/* Exception Vectors (VBAR-relative, placed at the start of the image so that the entry point + is the Reset vector; VBAR requires 32-byte alignment). */ +.text +.align 5 +.global _start +_start: + b reset_handler /* Reset. */ + b hang /* Undefined. */ + b hang /* Supervisor Call. */ + b hang /* Prefetch Abort. */ + b hang /* Data Abort. */ + b hang /* Reserved. */ + b irq_handler /* IRQ. */ + b hang /* FIQ. */ + +reset_handler: + /* Disable IRQ/FIQ. */ + cpsid if + + /* Set the Vector Base Address to our vectors (and use VBAR: SCTLR.V=0). */ + ldr r0, =_start + mcr p15, 0, r0, c12, c0, 0 + mrc p15, 0, r0, c1, c0, 0 + bic r0, r0, #(1 << 13) + mcr p15, 0, r0, c1, c0, 0 + + /* Enable VFP/NEON (the BIOS is built hard-float; not guaranteed to be on after U-Boot). */ + mrc p15, 0, r0, c1, c0, 2 + orr r0, r0, #(0xf << 20) + mcr p15, 0, r0, c1, c0, 2 + isb + mov r0, #(1 << 30) + vmsr fpexc, r0 + + /* Setup IRQ mode stack, then switch back to Supervisor mode with the main stack. */ + cps #0x12 + ldr sp, =_irq_stack_top + cps #0x13 + ldr sp, =_fstack + + /* Copy .data from its load address to RAM. */ + ldr r0, =_fdata + ldr r1, =_edata + ldr r2, =_fdata_rom +1: cmp r0, r1 + bhs 2f + ldr r3, [r2], #4 + str r3, [r0], #4 + b 1b +2: + /* Clear .bss. */ + ldr r0, =_fbss + ldr r1, =_ebss + mov r2, #0 +1: cmp r0, r1 + bhs 2f + str r2, [r0], #4 + b 1b +2: + bl litex_startup_init + bl main +hang: + b hang + +/* IRQ trampoline: save the caller-saved registers + SPSR, call isr(), restore and return. */ +irq_handler: + sub lr, lr, #4 + push {r0-r3, r12, lr} + mrs r0, spsr + push {r0, r1} /* r1: keeps the stack 8-byte aligned (AAPCS). */ + bl isr + pop {r0, r1} + msr spsr_cxsf, r0 + ldmfd sp!, {r0-r3, r12, pc}^ /* ^: also restores CPSR from SPSR. */ + +/* IRQ mode stack. */ +.section .bss +.align 3 +_irq_stack: + .space 0x400 +_irq_stack_top: diff --git a/litex/soc/cores/cpu/cyclonev_hps/irq.h b/litex/soc/cores/cpu/cyclonev_hps/irq.h new file mode 100644 index 0000000000..92236491b5 --- /dev/null +++ b/litex/soc/cores/cpu/cyclonev_hps/irq.h @@ -0,0 +1,101 @@ +#ifndef __IRQ_H +#define __IRQ_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +/* Cyclone V HPS MPU GIC-390 (Distributor / CPU Interface). */ +#define GIC_DIST_BASE 0xfffed000L +#define GIC_CPU_BASE 0xfffec100L + +/* Distributor registers. */ +#define GIC_DIST_CTLR 0x000 +#define GIC_DIST_ISENABLER(n) (0x100 + 4*(n)) +#define GIC_DIST_ICENABLER(n) (0x180 + 4*(n)) +#define GIC_DIST_ISPENDR(n) (0x200 + 4*(n)) +#define GIC_DIST_ICPENDR(n) (0x280 + 4*(n)) +#define GIC_DIST_IPRIORITYR(n) (0x400 + 4*(n)) +#define GIC_DIST_ITARGETSR(n) (0x800 + 4*(n)) + +/* CPU Interface registers. */ +#define GIC_CPU_CTLR 0x00 +#define GIC_CPU_PMR 0x04 +#define GIC_CPU_IAR 0x0c +#define GIC_CPU_EOIR 0x10 + +/* F2H IRQs 0-31 are connected to GIC IDs 72-103. */ +#define GIC_F2H_IRQ_OFFSET 72 + +static inline uint32_t gic_dist_read(uint32_t offset) { + return *(volatile uint32_t *)(GIC_DIST_BASE + offset); +} + +static inline void gic_dist_write(uint32_t offset, uint32_t value) { + *(volatile uint32_t *)(GIC_DIST_BASE + offset) = value; +} + +static inline uint32_t gic_cpu_read(uint32_t offset) { + return *(volatile uint32_t *)(GIC_CPU_BASE + offset); +} + +static inline void gic_cpu_write(uint32_t offset, uint32_t value) { + *(volatile uint32_t *)(GIC_CPU_BASE + offset) = value; +} + +static inline unsigned int irq_getie(void) +{ + unsigned int cpsr; + __asm__ volatile("mrs %0, cpsr" : "=r"(cpsr)); + return (cpsr & (1 << 7)) == 0; /* CPSR I-bit: 0 means IRQs enabled. */ +} + +static inline void irq_setie(unsigned int ie) +{ + if (ie) { + int i; + /* Route the F2H IRQs to CPU0 (Target registers hold one byte per interrupt). */ + for (i = GIC_F2H_IRQ_OFFSET/4; i < (GIC_F2H_IRQ_OFFSET + 32)/4; i++) + gic_dist_write(GIC_DIST_ITARGETSR(i), 0x01010101); + /* Allow all priorities and enable the Distributor/CPU Interface. */ + gic_cpu_write(GIC_CPU_PMR, 0xff); + gic_dist_write(GIC_DIST_CTLR, gic_dist_read(GIC_DIST_CTLR) | 1); + gic_cpu_write(GIC_CPU_CTLR, gic_cpu_read(GIC_CPU_CTLR) | 1); + __asm__ volatile("cpsie i" ::: "memory"); + } else { + __asm__ volatile("cpsid i" ::: "memory"); + } +} + +/* Masks map the 32 F2H IRQs: GIC IDs 72-95 are in registers n=2 bits 31:8, IDs 96-103 in + registers n=3 bits 7:0. */ + +static inline unsigned int irq_getmask(void) +{ + return (gic_dist_read(GIC_DIST_ISENABLER(2)) >> 8) | + (gic_dist_read(GIC_DIST_ISENABLER(3)) << 24); +} + +static inline void irq_setmask(unsigned int mask) +{ + /* Set/Clear-Enable registers: written 1s set/clear the enables, 0s are ignored (other GIC + IDs, ex HPS peripherals, are thus not disturbed). */ + gic_dist_write(GIC_DIST_ICENABLER(2), (~mask << 8) & 0xffffff00); + gic_dist_write(GIC_DIST_ICENABLER(3), (~mask >> 24) & 0x000000ff); + gic_dist_write(GIC_DIST_ISENABLER(2), ( mask << 8) & 0xffffff00); + gic_dist_write(GIC_DIST_ISENABLER(3), ( mask >> 24) & 0x000000ff); +} + +static inline unsigned int irq_pending(void) +{ + return (gic_dist_read(GIC_DIST_ISPENDR(2)) >> 8) | + (gic_dist_read(GIC_DIST_ISPENDR(3)) << 24); +} + +#ifdef __cplusplus +} +#endif + +#endif /* __IRQ_H */ diff --git a/litex/soc/cores/cpu/cyclonev_hps/system.h b/litex/soc/cores/cpu/cyclonev_hps/system.h new file mode 100644 index 0000000000..2136164a48 --- /dev/null +++ b/litex/soc/cores/cpu/cyclonev_hps/system.h @@ -0,0 +1,66 @@ +#ifndef __SYSTEM_H +#define __SYSTEM_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +__attribute__((unused)) static void flush_cpu_icache(void){}; + +__attribute__((unused)) static void flush_cpu_dcache(void){}; + +void flush_l2_cache(void); + +void busy_wait(unsigned int ms); +void busy_wait_us(unsigned int us); + +#define CSR_UART_BASE +#define UART_POLLING + +/* HPS UART0 (16550, configured by the Preloader/U-Boot). */ +#define HPS_UART0_BASE 0xffc02000L + +#define HPS_UART_RBR_THR_OFFSET 0x00 +#define HPS_UART_LSR_OFFSET 0x14 +#define HPS_UART_LSR_DR (1 << 0) +#define HPS_UART_LSR_THRE (1 << 5) + +static inline uint32_t hps_uart_read(uint32_t offset) { + return *(volatile uint32_t *)(HPS_UART0_BASE + offset); +} + +static inline void hps_uart_write(uint32_t offset, uint32_t value) { + *(volatile uint32_t *)(HPS_UART0_BASE + offset) = value; +} + +static inline void uart_rxtx_write(char c) { + hps_uart_write(HPS_UART_RBR_THR_OFFSET, (uint32_t) c); +} + +static inline uint8_t uart_rxtx_read(void) { + return hps_uart_read(HPS_UART_RBR_THR_OFFSET); +} + +static inline uint8_t uart_txfull_read(void) { + return !(hps_uart_read(HPS_UART_LSR_OFFSET) & HPS_UART_LSR_THRE); +} + +static inline uint8_t uart_rxempty_read(void) { + return !(hps_uart_read(HPS_UART_LSR_OFFSET) & HPS_UART_LSR_DR); +} + +static inline void uart_ev_pending_write(uint8_t x) {} + +static inline uint8_t uart_ev_pending_read(void) { + return 0; +} + +static inline void uart_ev_enable_write(uint8_t x) {} + +#ifdef __cplusplus +} +#endif + +#endif /* __SYSTEM_H */ diff --git a/litex/soc/interconnect/avalon/__init__.py b/litex/soc/interconnect/avalon/__init__.py index 1dac2b0b6c..1a54a7e387 100644 --- a/litex/soc/interconnect/avalon/__init__.py +++ b/litex/soc/interconnect/avalon/__init__.py @@ -1,6 +1,7 @@ # Avalon MM. from litex.soc.interconnect.avalon.avalon_mm import AvalonMMInterface from litex.soc.interconnect.avalon.avalon_mm_to_wishbone import AvalonMM2Wishbone +from litex.soc.interconnect.avalon.avalon_mm_to_wishbone import Wishbone2AvalonMM # Avalon ST. from litex.soc.interconnect.avalon.avalon_st import Native2AvalonST, AvalonST2Native \ No newline at end of file diff --git a/litex/soc/interconnect/avalon/avalon_mm_to_wishbone.py b/litex/soc/interconnect/avalon/avalon_mm_to_wishbone.py index a7821b4930..98cddc9feb 100644 --- a/litex/soc/interconnect/avalon/avalon_mm_to_wishbone.py +++ b/litex/soc/interconnect/avalon/avalon_mm_to_wishbone.py @@ -12,6 +12,42 @@ from litex.soc.interconnect import wishbone from litex.soc.interconnect.avalon import AvalonMMInterface +# Wishbone -> Avalon MM Bridge --------------------------------------------------------------------- + +class Wishbone2AvalonMM(Module): + """Wishbone Slave to Avalon-MM Master bridge (Classic single-beat accesses).""" + def __init__(self, data_width=32, avalon_address_width=30, avalon_base_address=0x0): + self.w2a_wb = wb = wishbone.Interface(data_width=data_width, adr_width=avalon_address_width, addressing="word") + self.w2a_avl = avl = AvalonMMInterface(data_width=data_width, adr_width=avalon_address_width) + + # # # + + read_done = Signal() # Read command accepted, waiting for Read-Data. + + # Wishbone -> Avalon. + self.comb += [ + # Both sides are word-addressed here (avalon_base_address is a word address). + avl.address.eq(wb.adr - avalon_base_address), + avl.writedata.eq(wb.dat_w), + avl.byteenable.eq(wb.sel), + avl.burstcount.eq(1), + avl.write.eq(wb.cyc & wb.stb & wb.we), + avl.read.eq( wb.cyc & wb.stb & ~wb.we & ~read_done), + ] + + # Avalon -> Wishbone. + self.comb += [ + wb.dat_r.eq(avl.readdata), + wb.ack.eq((avl.write & ~avl.waitrequest) | (read_done & avl.readdatavalid)), + ] + self.sync += [ + If(wb.ack, + read_done.eq(0) + ).Elif(avl.read & ~avl.waitrequest, + read_done.eq(1) + ), + ] + # Avalon MM <--> Wishbone Bridge ------------------------------------------------------------------- class AvalonMM2Wishbone(Module): diff --git a/litex/soc/software/libbase/isr.c b/litex/soc/software/libbase/isr.c index 909d14891a..3797dad9e7 100644 --- a/litex/soc/software/libbase/isr.c +++ b/litex/soc/software/libbase/isr.c @@ -308,6 +308,40 @@ void isr(void) irqs &= irqs - 1; // clear this irq (the first bit set) } } +/****************************************************/ +/* ISR Handling for Cyclone V HPS (GIC-390) CPU. */ +/****************************************************/ +#elif defined(__cyclonev_hps__) + +/* Interrupt Service Routine. */ +void isr(void) +{ + while (1) { + /* Acknowledge (claim) the interrupt. */ + unsigned int id = gic_cpu_read(GIC_CPU_IAR) & 0x3ff; + + /* Spurious interrupt: all pending interrupts handled. */ + if (id >= 1020) + return; + + /* Dispatch F2H IRQs to the interrupt table. */ + if ((id >= GIC_F2H_IRQ_OFFSET) && (id < GIC_F2H_IRQ_OFFSET + 32)) { + unsigned int irq = id - GIC_F2H_IRQ_OFFSET; + if ((irq < CONFIG_CPU_INTERRUPTS) && irq_table[irq].isr) + irq_table[irq].isr(); + else { + irq_setmask(irq_getmask() & ~(1 << irq)); + printf("\n*** disabled spurious irq %d ***\n", irq); + } + } else { + printf("## GIC: Unhandled interrupt: %d\n", id); + } + + /* Signal End Of Interrupt. */ + gic_cpu_write(GIC_CPU_EOIR, id); + } +} + /*******************************************************/ /* Generic ISR Handling for CPUs with Interrupt Table. */ /*******************************************************/ diff --git a/test/soc/test_integration.py b/test/soc/test_integration.py index 100e61874c..d2ca350b32 100644 --- a/test/soc/test_integration.py +++ b/test/soc/test_integration.py @@ -271,6 +271,7 @@ def test_linux_on_litex_rocket_nexys_video_generation(tmp_path): "cortex_m1", # (arm / softcore) -> Proprietary code. "cortex_m3", # (arm / softcore) -> Proprieraty code. "cva5", # (riscv / softcore) -> Verilator misses generated interface header. + "cyclonev_hps", # (arm / hardcore) -> Hardcore. "eos_s3", # (arm / hardcore) -> Hardcore. "gowin_emcu", # (arm / hardcore) -> Hardcore. "rocket", # (riscv / softcore) -> Not enough RAM in CI. diff --git a/test/test_avalon_mm.py b/test/test_avalon_mm.py new file mode 100644 index 0000000000..9101d66917 --- /dev/null +++ b/test/test_avalon_mm.py @@ -0,0 +1,69 @@ +# +# This file is part of LiteX. +# +# Copyright (c) 2026 Florent Kermarrec +# SPDX-License-Identifier: BSD-2-Clause + +import unittest + +from migen import * + +from litex.soc.interconnect.avalon import Wishbone2AvalonMM + +# Avalon-MM RAM Model ------------------------------------------------------------------------------ + +@passive +def avalon_mm_ram_generator(avl, mem, read_latency=2, wait_states=True): + pending = [] + wreq_now = 0 # Wait-Request value driven for the current cycle (reset value). + toggle = 0 + while True: + read = (yield avl.read) + write = (yield avl.write) + adr = (yield avl.address) + dat = (yield avl.writedata) + # Command acceptance (when Wait-Request is deasserted). + if (read or write) and (wreq_now == 0): + if write: + mem[adr] = dat + if read: + pending.append([read_latency, adr]) + # Read-Data return (driven for the next cycle). + pending = [[count - 1, adr] for count, adr in pending] + if pending and (pending[0][0] <= 0): + _, rd_adr = pending.pop(0) + yield avl.readdata.eq(mem.get(rd_adr, 0)) + yield avl.readdatavalid.eq(1) + else: + yield avl.readdatavalid.eq(0) + # Wait-Request (toggling pattern to exercise wait-states). + toggle += 1 + wreq_next = (toggle % 2) if wait_states else 0 + yield avl.waitrequest.eq(wreq_next) + yield + wreq_now = wreq_next + +# TestAvalonMM ------------------------------------------------------------------------------------- + +class TestAvalonMM(unittest.TestCase): + def test_wishbone2avalonmm(self): + def generator(dut, base): + # Writes. + for i in range(8): + yield from dut.w2a_wb.write(base + i, 0x1000_0000 + i) + # Reads. + for i in range(8): + data = (yield from dut.w2a_wb.read(base + i)) + self.assertEqual(data, 0x1000_0000 + i) + + for data_width in [32, 64]: + for base in [0x0000_0000, 0x0100_0000]: + for wait_states in [False, True]: + dut = Wishbone2AvalonMM(data_width=data_width, avalon_base_address=base) + mem = {} + run_simulation(dut, [ + generator(dut, base), + avalon_mm_ram_generator(dut.w2a_avl, mem, wait_states=wait_states), + ]) + # Check the Avalon-side addresses (avalon_base_address subtracted). + self.assertEqual(sorted(mem.keys()), list(range(8)))