Browse Source

projects/riscv_doom: Initial project import

Signed-off-by: Sylvain Munaut <tnt@246tNt.com>
Sylvain Munaut 4 years ago
parent
commit
452cddf2a2

+ 40 - 0
projects/riscv_doom/Makefile

@@ -0,0 +1,40 @@
+# Project config
+PROJ = riscv_doom
+
+PROJ_DEPS := no2usb no2misc no2ice40 qspi_master mem_cache video
+PROJ_RTL_SRCS := $(addprefix rtl/, \
+	vid_top.v \
+	vid_palette.v \
+	vid_framebuf.v \
+	soc_bram.v \
+	sysmgr.v \
+	VexRiscv.v \
+)
+PROJ_SIM_SRCS := $(addprefix sim/, \
+	spiflash.v \
+)
+PROJ_SIM_SRCS += rtl/top.v
+PROJ_TESTBENCHES := \
+	top_tb
+PROJ_PREREQ = \
+	$(BUILD_TMP)/boot.hex
+PROJ_TOP_SRC := rtl/top.v
+PROJ_TOP_MOD := top
+
+# Target config
+BOARD ?= icebreaker
+DEVICE = up5k
+PACKAGE = sg48
+
+YOSYS_SYNTH_ARGS = -dffe_min_ce_use 4 -abc9 -device u -dsp
+NEXTPNR_ARGS = --pre-pack data/clocks.py --pre-place $(CORE_no2ice40_DIR)/sw/serdes-nextpnr-place.py --seed 4
+
+# Include default rules
+include ../../build/project-rules.mk
+
+# Custom rules
+fw_boot/boot.hex:
+	make -C fw_boot boot.hex
+
+$(BUILD_TMP)/boot.hex: fw_boot/boot.hex
+	cp $< $@

+ 2 - 0
projects/riscv_doom/README.md

@@ -0,0 +1,2 @@
+RISC-V Doom for iCE40UP5k
+=========================

+ 4 - 0
projects/riscv_doom/data/clocks.py

@@ -0,0 +1,4 @@
+BASE = 25.175
+
+ctx.addClock("clk_1x", 0.925 * BASE)	# Allow overclock ...
+ctx.addClock("clk_4x", 4     * BASE)

+ 41 - 0
projects/riscv_doom/data/top-icebreaker.pcf

@@ -0,0 +1,41 @@
+# SPI
+set_io --warn-no-port             spi_sck   15
+set_io --warn-no-port -pullup no  spi_io[0] 14
+set_io --warn-no-port -pullup no  spi_io[1] 17
+set_io --warn-no-port -pullup no  spi_io[2] 12
+set_io --warn-no-port -pullup no  spi_io[3] 13
+set_io --warn-no-port -pullup yes spi_cs_n[0] 16
+set_io --warn-no-port -pullup yes spi_cs_n[1] 37
+
+# HDMI 12 bit pmod on 1A/1B
+set_io -nowarn hdmi_clk 38
+set_io -nowarn hdmi_de 32
+set_io -nowarn hdmi_hsync 31
+set_io -nowarn hdmi_vsync 28
+set_io -nowarn hdmi_b[3] 43
+set_io -nowarn hdmi_b[2] 42
+set_io -nowarn hdmi_b[1] 36
+set_io -nowarn hdmi_b[0] 34
+set_io -nowarn hdmi_g[3] 47
+set_io -nowarn hdmi_g[2] 46
+set_io -nowarn hdmi_g[1] 45
+set_io -nowarn hdmi_g[0] 44
+set_io -nowarn hdmi_r[3] 4
+set_io -nowarn hdmi_r[2] 3
+set_io -nowarn hdmi_r[1] 2
+set_io -nowarn hdmi_r[0] 48
+
+# UART
+set_io -nowarn uart_rx 6
+set_io -nowarn uart_tx 9
+
+# Clock
+set_io -nowarn clk_in 35
+
+# Button
+set_io -nowarn btn 10
+
+# Leds
+set_io -nowarn rgb[0] 39
+set_io -nowarn rgb[1] 40
+set_io -nowarn rgb[2] 41

+ 20 - 0
projects/riscv_doom/fw_boot/Makefile

@@ -0,0 +1,20 @@
+CROSS ?= riscv-none-embed-
+
+CC = $(CROSS)gcc
+OBJCOPY = $(CROSS)objcopy
+
+CFLAGS=-Wall -Os -march=rv32i -mabi=ilp32 -ffreestanding -flto -nostartfiles -fomit-frame-pointer -Wl,--gc-section --specs=nano.specs
+
+boot.elf: lnk-boot.lds boot.S
+	$(CC) $(CFLAGS) -Wl,-Bstatic,-T,lnk-boot.lds,--strip-debug -DFLASH_APP_ADDR=0x00100000 -o $@ boot.S
+
+%.hex: %.bin
+	./bin2hex.py $< $@
+
+%.bin: %.elf
+	$(OBJCOPY) -O binary $< $@
+
+clean:
+	rm -f *.bin *.hex *.elf *.o
+
+.PHONY: clean

+ 1 - 0
projects/riscv_doom/fw_boot/bin2hex.py

@@ -0,0 +1 @@
+../../riscv_usb/fw/bin2hex.py

+ 169 - 0
projects/riscv_doom/fw_boot/boot.S

@@ -0,0 +1,169 @@
+/*
+ * boot.S
+ *
+ * Boot code
+ *
+ * Copyright (C) 2020 Sylvain Munaut <tnt@246tNt.com>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef FLASH_APP_ADDR
+#define FLASH_APP_ADDR 0x00100000
+#endif
+
+#define BOOT_DEBUG
+
+	.section .text.start
+	.global _start
+_start:
+
+#ifdef BOOT_DEBUG
+	// Set UART divisor
+	li	a0, 0x82000004
+	li	a1, 23
+	sw	a1, 0(a0)
+#endif
+
+	// Delay boot
+	li	t0, 0x01000000
+1:
+	addi	t0, t0, -1
+	bne	t0, zero, 1b
+
+	// SPI init
+	jal	spi_init
+
+	// Setup reboot code
+	li	t0, 0x0002006f
+	sw	t0, 0(zero)
+
+	// Jump to main code in flash
+	li	ra, (0x40000000 + FLASH_APP_ADDR)
+	ret
+
+	.equ    SPI_BASE, 0x80000000
+	.equ    SPI_CSR,  4 * 0x00
+	.equ	SPI_RF,   4 * 0x03
+
+
+spi_init:
+	// Save return address
+	// -------------------
+
+	mv	t6, ra
+
+
+	// Flash QSPI enable
+	// -----------------
+
+	li	t5, SPI_BASE
+
+	// Request external control
+	li	t0, 0x00000004
+	sw	t0, SPI_CSR(t5)
+
+	li	t0, 0x00000002
+	sw	t0, SPI_CSR(t5)
+
+	// Enable QSPI (0x38)
+	li	t0, 0x38000000
+	sw	t0, 0x40(t5)
+
+	// Read and discard response
+	lw	t0, SPI_RF(t5)
+
+	// Release external control
+	li	t0, 0x00000004
+	sw	t0, SPI_CSR(t5)
+
+
+	// Flash QSPI config
+	// -----------------
+
+	// Request external control
+	li	t0, 0x00000004
+	sw	t0, SPI_CSR(t5)
+
+	li	t0, 0x00000002
+	sw	t0, SPI_CSR(t5)
+
+	// Set QSPI parameters (dummy=6, wrap=64b)
+	li	t0, 0xc0230000
+	sw	t0, 0x74(t5)
+
+	// Release external control
+	li	t0, 0x00000004
+	sw	t0, SPI_CSR(t5)
+
+
+	// PSRAM init
+	// ----------
+
+	// Request external control
+	li	t0, 0x00000004
+	sw	t0, SPI_CSR(t5)
+
+	li	t0, 0x00000012
+	sw	t0, SPI_CSR(t5)
+
+	// Enable QSPI (0x35)
+	li	t0, 0x35000000
+	sw	t0, 0x40(t5)
+
+	// Read and discard response
+	lw	t0, SPI_RF(t5)
+
+	// Release external control
+	li	t0, 0x00000004
+	sw	t0, SPI_CSR(t5)
+
+
+	// Return
+	// ------
+
+	mv	ra, t6
+	ret
+
+
+#ifdef BOOT_DEBUG
+// Agument in a0
+// Clobbers a0, t0-t3
+print_hex:
+	li	t0, 0x82000000
+	li	t1, 8
+	la	t2, hexchar
+
+1:
+	srli	t3, a0, 28
+	add	t3, t3, t2
+	lb	t3, 0(t3)
+	sw	t3, 0(t0)
+
+	slli	a0, a0, 4
+
+	addi	t1, t1, -1
+	bne	zero, t1, 1b
+
+print_nl:
+	li	t0, 0x82000000
+	li	a0, '\r'
+	sw	a0, 0(t0)
+	li	a0, '\n'
+	sw	a0, 0(t0)
+
+	ret
+
+hexchar:
+	.ascii	"0123456789abcdef"
+#endif

+ 14 - 0
projects/riscv_doom/fw_boot/lnk-boot.lds

@@ -0,0 +1,14 @@
+MEMORY
+{
+    ROM (rx)    : ORIGIN = 0x00000000, LENGTH = 0x0400
+}
+ENTRY(_start)
+SECTIONS {
+    .text :
+    {
+        . = ALIGN(4);
+        *(.text.start)
+        *(.text)
+        *(.text*)
+    } >ROM
+}

File diff suppressed because it is too large
+ 4904 - 0
projects/riscv_doom/rtl/VexRiscv.v


+ 38 - 0
projects/riscv_doom/rtl/soc_bram.v

@@ -0,0 +1,38 @@
+/*
+ * soc_bram.v
+ *
+ * vim: ts=4 sw=4
+ *
+ * Copyright (C) 2020  Sylvain Munaut <tnt@246tNt.com>
+ * SPDX-License-Identifier: CERN-OHL-P-2.0
+ */
+
+`default_nettype none
+
+module soc_bram #(
+	parameter integer AW = 8,
+	parameter INIT_FILE = ""
+)(
+	input  wire [AW-1:0] addr,
+	output reg    [31:0] rdata,
+	input  wire   [31:0] wdata,
+	input  wire   [ 3:0] wmsk,
+	input  wire          we,
+	input  wire          clk
+);
+
+	reg [31:0] mem [0:(1<<AW)-1];
+
+	initial
+		if (INIT_FILE != "")
+			$readmemh(INIT_FILE, mem);
+
+	always @(posedge clk) begin
+		rdata <= mem[addr];
+		if (we & ~wmsk[0]) mem[addr][ 7: 0] <= wdata[ 7: 0];
+		if (we & ~wmsk[1]) mem[addr][15: 8] <= wdata[15: 8];
+		if (we & ~wmsk[2]) mem[addr][23:16] <= wdata[23:16];
+		if (we & ~wmsk[3]) mem[addr][31:24] <= wdata[31:24];
+	end
+
+endmodule // soc_bram

+ 65 - 0
projects/riscv_doom/rtl/sysmgr.v

@@ -0,0 +1,65 @@
+/*
+ * sysmgr.v
+ *
+ * vim: ts=4 sw=4
+ *
+ * Copyright (C) 2021  Sylvain Munaut <tnt@246tNt.com>
+ * SPDX-License-Identifier: CERN-OHL-P-2.0
+ */
+
+`default_nettype none
+
+module sysmgr (
+	input  wire clk_in,
+	output wire clk_1x,
+	output wire clk_4x,
+	output wire sync_4x,
+	output wire rst
+);
+
+	wire pll_lock;
+
+	SB_PLL40_2F_PAD #(
+		.FEEDBACK_PATH("SIMPLE"),
+		.DIVR(4'b0000),
+		.DIVF(7'b1000010),
+		.DIVQ(3'b011),
+		.FILTER_RANGE(3'b001),
+		.DELAY_ADJUSTMENT_MODE_RELATIVE("DYNAMIC"),
+		.FDA_RELATIVE(15),
+		.SHIFTREG_DIV_MODE(0),
+		.PLLOUT_SELECT_PORTA("GENCLK"),
+		.PLLOUT_SELECT_PORTB("GENCLK")
+	) pll_I (
+		.PACKAGEPIN    (clk_in),
+		.DYNAMICDELAY  (8'h0),
+		.PLLOUTGLOBALA (),
+		.PLLOUTGLOBALB (clk_4x),
+		.RESETB        (1'b1),
+		.LOCK          (pll_lock)
+	);
+
+	ice40_serdes_crg #(
+		.NO_CLOCK_2X(1)
+	) crg_I (
+		.clk_4x   (clk_4x),
+		.pll_lock (pll_lock),
+		.clk_1x   (clk_1x),
+		.clk_2x   (),
+		.rst      (rst)
+	);
+
+	ice40_serdes_sync #(
+		.PHASE      (2),
+		.NEG_EDGE   (0),
+		.GLOBAL_BUF (0),
+		.BEL_COL    ("X20"),
+		.BEL_ROW    ("Y4")
+	) sync_96m_I (
+		.clk_slow (clk_1x),
+		.clk_fast (clk_4x),
+		.rst      (rst),
+		.sync     (sync_4x)
+	);
+
+endmodule

+ 469 - 0
projects/riscv_doom/rtl/top.v

@@ -0,0 +1,469 @@
+/*
+ * top.v
+ *
+ * vim: ts=4 sw=4
+ *
+ * Copyright (C) 2021  Sylvain Munaut <tnt@246tNt.com>
+ * SPDX-License-Identifier: CERN-OHL-P-2.0
+ */
+
+`default_nettype none
+
+module top (
+	// SPI
+	inout  wire [3:0] spi_io,
+	inout  wire       spi_sck,
+	inout  wire [1:0] spi_cs_n,
+
+	// Video output
+	output wire [3:0] hdmi_r,
+	output wire [3:0] hdmi_g,
+	output wire [3:0] hdmi_b,
+	output wire       hdmi_hsync,
+	output wire       hdmi_vsync,
+	output wire       hdmi_de,
+	output wire       hdmi_clk,
+
+	// Debug UART
+	input  wire uart_rx,
+	output wire uart_tx,
+
+	// Button
+	input  wire btn,
+
+	// LED
+	output wire [2:0] rgb,
+
+	// Clock
+	input  wire clk_in
+);
+
+	localparam integer WB_N  =  4;
+
+	localparam integer WB_DW = 32;
+	localparam integer WB_AW = 22;
+	localparam integer WB_RW = WB_DW * WB_N;
+	localparam integer WB_MW = WB_DW / 8;
+
+	genvar i;
+
+
+	// Signals
+	// -------
+
+	// Vex Misc
+	wire [31:0] vex_externalResetVector;
+	wire        vex_timerInterrupt;
+	wire        vex_softwareInterrupt;
+	wire [31:0] vex_externalInterruptArray;
+
+	// Vex busses
+	wire        i_axi_ar_valid;
+	wire        i_axi_ar_ready;
+	wire [31:0] i_axi_ar_payload_addr;
+	wire [ 7:0] i_axi_ar_payload_len;
+	wire [ 1:0] i_axi_ar_payload_burst;
+	wire [ 3:0] i_axi_ar_payload_cache;
+	wire [ 2:0] i_axi_ar_payload_prot;
+	wire        i_axi_r_valid;
+	wire        i_axi_r_ready;
+	wire [31:0] i_axi_r_payload_data;
+	wire [ 1:0] i_axi_r_payload_resp;
+	wire        i_axi_r_payload_last;
+
+	wire        d_wb_cyc;
+	wire        d_wb_stb;
+	wire        d_wb_ack;
+	wire        d_wb_we;
+	wire [29:0] d_wb_adr;
+	wire [31:0] d_wb_dat_miso;
+	wire [31:0] d_wb_dat_mosi;
+	wire [ 3:0] d_wb_sel;
+	wire        d_wb_err;
+	wire [ 1:0] d_wb_bte;
+	wire [ 2:0] d_wb_cti;
+
+	// RAM
+	wire [27:0] ram_addr;
+	wire [31:0] ram_rdata;
+	wire [31:0] ram_wdata;
+	wire [ 3:0] ram_wmsk;
+	wire        ram_we;
+
+	// Cache Request / Response interface
+	wire [27:0] cache_req_addr_pre;
+	wire        cache_req_valid;
+	wire        cache_req_write;
+	wire [31:0] cache_req_wdata;
+	wire [ 3:0] cache_req_wmsk;
+
+	wire        cache_resp_ack;
+	wire        cache_resp_nak;
+	wire [31:0] cache_resp_rdata;
+
+	// Memory interface
+	wire [23:0] mi_addr;
+	wire [ 6:0] mi_len;
+	wire        mi_rw;
+	wire        mi_linear;
+	wire        mi_valid;
+	wire        mi_ready;
+
+	wire [31:0] mi_wdata;
+	wire [ 3:0] mi_wmsk;
+	wire        mi_wack;
+	wire        mi_wlast;
+
+	wire [31:0] mi_rdata;
+	wire        mi_rstb;
+	wire        mi_rlast;
+
+	// QSPI PHY signals
+	wire [15:0] phy_io_i;
+	wire [15:0] phy_io_o;
+	wire [ 3:0] phy_io_oe;
+	wire [ 3:0] phy_clk_o;
+	wire [ 1:0] phy_cs_o;
+
+	// Wishbone
+	wire [WB_AW-1:0] wb_addr;
+	wire [WB_DW-1:0] wb_rdata [0:WB_N-1];
+	wire [WB_RW-1:0] wb_rdata_flat;
+	wire [WB_DW-1:0] wb_wdata;
+	wire [WB_MW-1:0] wb_wmsk;
+	wire             wb_we;
+	wire [WB_N -1:0] wb_cyc;
+	wire [WB_N -1:0] wb_ack;
+
+	// Clock / Reset logic
+	wire clk_1x;
+	wire clk_4x;
+	wire sync_4x;
+	wire rst;
+
+
+	// SoC
+	// ---
+
+	// CPU
+	VexRiscv cpu_I (
+		.externalResetVector      (vex_externalResetVector),
+		.timerInterrupt           (vex_timerInterrupt),
+		.softwareInterrupt        (vex_softwareInterrupt),
+		.externalInterruptArray   (vex_externalInterruptArray),
+		.iBusAXI_ar_valid         (i_axi_ar_valid),
+		.iBusAXI_ar_ready         (i_axi_ar_ready),
+		.iBusAXI_ar_payload_addr  (i_axi_ar_payload_addr),
+		.iBusAXI_ar_payload_len   (i_axi_ar_payload_len),
+		.iBusAXI_ar_payload_burst (i_axi_ar_payload_burst),
+		.iBusAXI_ar_payload_cache (i_axi_ar_payload_cache),
+		.iBusAXI_ar_payload_prot  (i_axi_ar_payload_prot),
+		.iBusAXI_r_valid          (i_axi_r_valid),
+		.iBusAXI_r_ready          (i_axi_r_ready),
+		.iBusAXI_r_payload_data   (i_axi_r_payload_data),
+		.iBusAXI_r_payload_resp   (i_axi_r_payload_resp),
+		.iBusAXI_r_payload_last   (i_axi_r_payload_last),
+		.dBusWishbone_CYC         (d_wb_cyc),
+		.dBusWishbone_STB         (d_wb_stb),
+		.dBusWishbone_ACK         (d_wb_ack),
+		.dBusWishbone_WE          (d_wb_we),
+		.dBusWishbone_ADR         (d_wb_adr),
+		.dBusWishbone_DAT_MISO    (d_wb_dat_miso),
+		.dBusWishbone_DAT_MOSI    (d_wb_dat_mosi),
+		.dBusWishbone_SEL         (d_wb_sel),
+		.dBusWishbone_ERR         (d_wb_err),
+		.dBusWishbone_BTE         (d_wb_bte),
+		.dBusWishbone_CTI         (d_wb_cti),
+		.clk                      (clk_1x),
+		.reset                    (rst)
+	);
+
+	// CPU interrupt wiring
+	assign vex_externalResetVector    = 32'h00000000;
+	assign vex_timerInterrupt         = 1'b0;
+	assign vex_softwareInterrupt      = 1'b0;
+	assign vex_externalInterruptArray = 32'h00000000;
+
+	// Cache bus interface / bridge
+	mc_bus_vex #(
+		.WB_N(WB_N)
+	) cache_bus_I (
+		.i_axi_ar_valid         (i_axi_ar_valid),
+		.i_axi_ar_ready         (i_axi_ar_ready),
+		.i_axi_ar_payload_addr  (i_axi_ar_payload_addr),
+		.i_axi_ar_payload_len   (i_axi_ar_payload_len),
+		.i_axi_ar_payload_burst (i_axi_ar_payload_burst),
+		.i_axi_ar_payload_cache (i_axi_ar_payload_cache),
+		.i_axi_ar_payload_prot  (i_axi_ar_payload_prot),
+		.i_axi_r_valid          (i_axi_r_valid),
+		.i_axi_r_ready          (i_axi_r_ready),
+		.i_axi_r_payload_data   (i_axi_r_payload_data),
+		.i_axi_r_payload_resp   (i_axi_r_payload_resp),
+		.i_axi_r_payload_last   (i_axi_r_payload_last),
+		.d_wb_cyc               (d_wb_cyc),
+		.d_wb_stb               (d_wb_stb),
+		.d_wb_ack               (d_wb_ack),
+		.d_wb_we                (d_wb_we),
+		.d_wb_adr               (d_wb_adr),
+		.d_wb_dat_miso          (d_wb_dat_miso),
+		.d_wb_dat_mosi          (d_wb_dat_mosi),
+		.d_wb_sel               (d_wb_sel),
+		.d_wb_err               (d_wb_err),
+		.d_wb_bte               (d_wb_bte),
+		.d_wb_cti               (d_wb_cti),
+		.wb_addr                (wb_addr),
+		.wb_wdata               (wb_wdata),
+		.wb_wmsk                (wb_wmsk),
+		.wb_rdata               (wb_rdata_flat),
+		.wb_cyc                 (wb_cyc),
+		.wb_we                  (wb_we),
+		.wb_ack                 (wb_ack),
+		.ram_addr               (ram_addr),
+		.ram_wdata              (ram_wdata),
+		.ram_wmsk               (ram_wmsk),
+		.ram_rdata              (ram_rdata),
+		.ram_we                 (ram_we),
+		.req_addr_pre           (cache_req_addr_pre),
+		.req_valid              (cache_req_valid),
+		.req_write              (cache_req_write),
+		.req_wdata              (cache_req_wdata),
+		.req_wmsk               (cache_req_wmsk),
+		.resp_ack               (cache_resp_ack),
+		.resp_nak               (cache_resp_nak),
+		.resp_rdata             (cache_resp_rdata),
+		.clk                    (clk_1x),
+		.rst                    (rst)
+	);
+
+	for (i=0; i<WB_N; i=i+1)
+		assign wb_rdata_flat[i*WB_DW+:WB_DW] = wb_rdata[i];
+
+	// Boot memory
+	soc_bram #(
+		.AW(8),
+		.INIT_FILE("boot.hex")
+	) bram_I (
+		.addr  (ram_addr[7:0]),
+		.rdata (ram_rdata),
+		.wdata (ram_wdata),
+		.wmsk  (ram_wmsk),
+		.we    (ram_we),
+		.clk   (clk_1x)
+	);
+
+	// Cache
+	mc_core #(
+		.N_WAYS(4),
+		.ADDR_WIDTH(24),
+		.CACHE_LINE(32),
+		.CACHE_SIZE(64)
+	) cache_I (
+		.req_addr_pre (cache_req_addr_pre[23:0]),
+		.req_valid    (cache_req_valid),
+		.req_write    (cache_req_write),
+		.req_wdata    (cache_req_wdata),
+		.req_wmsk     (cache_req_wmsk),
+		.resp_ack     (cache_resp_ack),
+		.resp_nak     (cache_resp_nak),
+		.resp_rdata   (cache_resp_rdata),
+		.mi_addr      (mi_addr),
+		.mi_len       (mi_len),
+		.mi_rw        (mi_rw),
+		.mi_valid     (mi_valid),
+		.mi_ready     (mi_ready),
+		.mi_wdata     (mi_wdata),
+		.mi_wack      (mi_wack),
+		.mi_wlast     (mi_wlast),
+		.mi_rdata     (mi_rdata),
+		.mi_rstb      (mi_rstb),
+		.mi_rlast     (mi_rlast),
+		.clk          (clk_1x),
+		.rst          (rst)
+	);
+
+
+	// QSPI
+	// ----
+
+	// Simulation
+`ifdef SIM
+	mem_sim #(
+		.INIT_FILE("firmware.hex"),
+		.AW(20)
+	) qspi_sim (
+		.mi_addr  ({mi_addr[22], mi_addr[18:0]}),
+		.mi_len   (mi_len),
+		.mi_rw    (mi_rw),
+		.mi_valid (mi_valid),
+		.mi_ready (mi_ready),
+		.mi_wdata (mi_wdata),
+		.mi_wack  (mi_wack),
+		.mi_wlast (mi_wlast),
+		.mi_rdata (mi_rdata),
+		.mi_rstb  (mi_rstb),
+		.mi_rlast (mi_rlast),
+		.clk      (clk_1x),
+		.rst      (rst)
+	);
+
+	assign wb_ack[0] = wb_cyc[0];
+`else
+	// Controller
+	qspi_master #(
+		.CMD_READ   (16'hEB0B),
+		.CMD_WRITE  (16'h0202),
+		.DUMMY_CLK  (6),
+		.PAUSE_CLK  (8),
+		.FIFO_DEPTH (1),
+		.N_CS       (2),
+		.PHY_SPEED  (4),
+		.PHY_WIDTH  (1),
+		.PHY_DELAY  (4)
+	) memctrl_I (
+		.phy_io_i   (phy_io_i),
+		.phy_io_o   (phy_io_o),
+		.phy_io_oe  (phy_io_oe),
+		.phy_clk_o  (phy_clk_o),
+		.phy_cs_o   (phy_cs_o),
+		.mi_addr_cs (mi_addr[23:22]),
+		.mi_addr    ({mi_addr[21:0], 2'b00 }),	/* 32 bits aligned */
+		.mi_len     (mi_len),
+		.mi_rw      (mi_rw),
+		.mi_valid   (mi_valid),
+		.mi_ready   (mi_ready),
+		.mi_wdata   ({mi_wdata[7:0], mi_wdata[15:8], mi_wdata[23:16], mi_wdata[31:24]}),
+		.mi_wack    (mi_wack),
+		.mi_wlast   (mi_wlast),
+		.mi_rdata   ({mi_rdata[7:0], mi_rdata[15:8], mi_rdata[23:16], mi_rdata[31:24]}),
+		.mi_rstb    (mi_rstb),
+		.mi_rlast   (mi_rlast),
+		.wb_wdata   (wb_wdata),
+		.wb_rdata   (wb_rdata[0]),
+		.wb_addr    (wb_addr[4:0]),
+		.wb_we      (wb_we),
+		.wb_cyc     (wb_cyc[0]),
+		.wb_ack     (wb_ack[0]),
+		.clk        (clk_1x),
+		.rst        (rst)
+	);
+
+	// PHY
+	qspi_phy_ice40_4x #(
+		.N_CS(2),
+		.WITH_CLK(1)
+	) phy_I (
+		.pad_io    (spi_io),
+		.pad_clk   (spi_sck),
+		.pad_cs_n  (spi_cs_n),
+		.phy_io_i  (phy_io_i),
+		.phy_io_o  (phy_io_o),
+		.phy_io_oe (phy_io_oe),
+		.phy_clk_o (phy_clk_o),
+		.phy_cs_o  (phy_cs_o),
+		.clk_1x    (clk_1x),
+		.clk_4x    (clk_4x),
+		.clk_sync  (sync_4x)
+	);
+`endif
+
+
+	// Video [1]
+	// -----
+
+	vid_top vid_I (
+		.hdmi_r     (hdmi_r),
+		.hdmi_g     (hdmi_g),
+		.hdmi_b     (hdmi_b),
+		.hdmi_hsync (hdmi_hsync),
+		.hdmi_vsync (hdmi_vsync),
+		.hdmi_de    (hdmi_de),
+		.hdmi_clk   (hdmi_clk),
+		.wb_addr    (wb_addr[15:0]),
+		.wb_rdata   (wb_rdata[1]),
+		.wb_wdata   (wb_wdata),
+		.wb_wmsk    (wb_wmsk),
+		.wb_we      (wb_we),
+		.wb_cyc     (wb_cyc[1]),
+		.wb_ack     (wb_ack[1]),
+		.clk        (clk_1x),
+		.rst        (rst)
+	);
+
+
+	// UART [2]
+	// ----
+
+	uart_wb #(
+		.DIV_WIDTH(12),
+		.DW(WB_DW)
+	) uart_I (
+		.uart_tx  (uart_tx),
+		.uart_rx  (uart_rx),
+		.wb_addr  (wb_addr[1:0]),
+		.wb_rdata (wb_rdata[2]),
+		.wb_we    (wb_we),
+		.wb_wdata (wb_wdata),
+		.wb_cyc   (wb_cyc[2]),
+		.wb_ack   (wb_ack[2]),
+		.clk      (clk_1x),
+		.rst      (rst)
+	);
+
+
+	// LEDs [3]
+	// ----
+
+	ice40_rgb_wb #(
+		.CURRENT_MODE("0b1"),
+		.RGB0_CURRENT("0b000001"),
+		.RGB1_CURRENT("0b000001"),
+		.RGB2_CURRENT("0b000001")
+	) rgb_I (
+		.pad_rgb    (rgb),
+		.wb_addr    (wb_addr[4:0]),
+		.wb_rdata   (wb_rdata[3]),
+		.wb_wdata   (wb_wdata),
+		.wb_we      (wb_we),
+		.wb_cyc     (wb_cyc[3]),
+		.wb_ack     (wb_ack[3]),
+		.clk        (clk_1x),
+		.rst        (rst)
+	);
+
+
+	// Clock / Reset
+	// -------------
+
+`ifdef SIM
+	reg       rst_s = 1'b1;
+	reg       clk_4x_s = 1'b0;
+	reg       clk_1x_s = 1'b0;
+	reg [1:0] clk_sync_cnt = 2'b00;
+
+	always  #5 clk_4x_s <= !clk_4x_s;
+	always #20 clk_1x_s <= !clk_1x_s;
+
+	initial
+		#200 rst_s = 0;
+
+	always @(posedge clk_4x_s)
+		if (rst)
+			clk_sync_cnt <= 2'b00;
+		else
+			clk_sync_cnt <= clk_sync_cnt + 1;
+
+	assign clk_4x  = clk_4x_s;
+	assign clk_1x  = clk_1x_s;
+	assign sync_4x = (clk_sync_cnt == 2'b10);
+	assign rst     = rst_s;
+`else
+	sysmgr sys_mgr_I (
+		.clk_in  (clk_in),
+		.clk_1x  (clk_1x),
+		.clk_4x  (clk_4x),
+		.sync_4x (sync_4x),
+		.rst     (rst)
+	);
+`endif
+
+endmodule // top

+ 78 - 0
projects/riscv_doom/rtl/vid_framebuf.v

@@ -0,0 +1,78 @@
+/*
+ * vid_framebuf.v
+ *
+ * Video framebuffer memory
+ *
+ * vim: ts=4 sw=4
+ *
+ * Copyright (C) 2021  Sylvain Munaut <tnt@246tNt.com>
+ * SPDX-License-Identifier: CERN-OHL-P-2.0
+ */
+
+`default_nettype none
+
+module vid_framebuf (
+	// Video Read port
+	input  wire [13:0] v_addr_0,
+	output wire [31:0] v_data_1,
+	input  wire        v_re_0,
+
+	// Aux R/W port
+	input  wire [13:0] a_addr_0,
+	output wire [31:0] a_rdata_1,
+	input  wire [31:0] a_wdata_0,
+	input  wire [ 3:0] a_wmsk_0,
+	input  wire        a_we_0,
+	output wire        a_rdy_0,
+
+	// Clock
+	input  wire clk
+);
+
+	// Signals
+	// -------
+
+	wire [13:0] ram_addr;
+	wire [31:0] ram_rdata;
+	wire [31:0] ram_wdata;
+	wire [ 7:0] ram_mask_n;
+	wire        ram_we;
+
+
+	// Memory
+	// ------
+
+	SB_SPRAM256KA spram_I[1:0] (
+		.DATAIN     (ram_wdata),
+		.ADDRESS    (ram_addr),
+		.MASKWREN   (ram_mask_n),
+		.WREN       (ram_we),
+		.CHIPSELECT (1'b1),
+		.CLOCK      (clk),
+		.STANDBY    (1'b0),
+		.SLEEP      (1'b0),
+		.POWEROFF   (1'b1),
+		.DATAOUT    (ram_rdata)
+	);
+
+
+	// Muxing
+	// ------
+
+	assign ram_addr = v_re_0 ? v_addr_0 : a_addr_0;
+
+	assign ram_wdata = a_wdata_0;
+	assign ram_mask_n  = {
+		~a_wmsk_0[3], ~a_wmsk_0[3],
+		~a_wmsk_0[2], ~a_wmsk_0[2],
+		~a_wmsk_0[1], ~a_wmsk_0[1],
+		~a_wmsk_0[0], ~a_wmsk_0[0]
+	};
+	assign ram_we = a_we_0 & ~v_re_0;
+
+	assign a_rdata_1 = ram_rdata;
+	assign v_data_1  = ram_rdata;
+
+	assign a_rdy_0 = ~v_re_0;
+
+endmodule // vid_framebuf

+ 45 - 0
projects/riscv_doom/rtl/vid_palette.v

@@ -0,0 +1,45 @@
+/*
+ * vid_palette.v
+ *
+ * Video palette memory
+ *
+ * vim: ts=4 sw=4
+ *
+ * Copyright (C) 2021  Sylvain Munaut <tnt@246tNt.com>
+ * SPDX-License-Identifier: CERN-OHL-P-2.0
+ */
+
+`default_nettype none
+
+module vid_palette (
+	// Write port
+	input  wire [ 7:0] w_addr_0,
+	input  wire [15:0] w_data_0,
+	input  wire        w_ena_0,
+
+	// Read port
+	input  wire [ 7:0] r_addr_0,
+	output wire [15:0] r_data_1,
+
+	// Clock
+	input wire clk
+);
+
+	SB_RAM40_4K #(
+		.WRITE_MODE(0),
+		.READ_MODE(0)
+	) ebr_I (
+		.RDATA (r_data_1),
+		.RADDR ({3'b000, r_addr_0}),
+		.RCLK  (clk),
+		.RCLKE (1'b1),
+		.RE    (1'b1),
+		.WDATA (w_data_0),
+		.WADDR ({3'b000, w_addr_0}),
+		.MASK  (16'h0000),
+		.WCLK  (clk),
+		.WCLKE (w_ena_0),
+		.WE    (1'b1)
+	);
+
+endmodule // vid_palette

+ 304 - 0
projects/riscv_doom/rtl/vid_top.v

@@ -0,0 +1,304 @@
+/*
+ * vid_top.v
+ *
+ * Top-level for the video module
+ *
+ * vim: ts=4 sw=4
+ *
+ * Copyright (C) 2021  Sylvain Munaut <tnt@246tNt.com>
+ * SPDX-License-Identifier: CERN-OHL-P-2.0
+ */
+
+`default_nettype none
+
+/* Use 640x480 60 Hz video and not original 640x400 70 Hz mode */
+`define COMPAT_MODE
+
+module vid_top (
+	// Video output
+	output wire [3:0] hdmi_r,
+	output wire [3:0] hdmi_g,
+	output wire [3:0] hdmi_b,
+	output wire       hdmi_hsync,
+	output wire       hdmi_vsync,
+	output wire       hdmi_de,
+	output wire       hdmi_clk,
+
+	// Wishbone
+	input  wire [15:0] wb_addr,
+	output reg  [31:0] wb_rdata,
+	input  wire [31:0] wb_wdata,
+	input  wire [ 3:0] wb_wmsk,
+	input  wire        wb_we,
+	input  wire        wb_cyc,
+	output reg         wb_ack,
+
+	// Clock / Reset
+	input  wire        clk,
+	input  wire        rst
+);
+
+	// Signals
+	// -------
+
+	// Frame Buffer
+	wire [13:0] fb_v_addr_0;
+	wire [31:0] fb_v_data_1;
+	wire        fb_v_re_0;
+	wire [13:0] fb_a_addr_0;
+	wire [31:0] fb_a_rdata_1;
+	wire [31:0] fb_a_wdata_0;
+	wire [ 3:0] fb_a_wmsk_0;
+	wire        fb_a_we_0;
+	wire        fb_a_rdy_0;
+
+	// Palette
+	wire [ 7:0] pal_w_addr;
+	wire [15:0] pal_w_data;
+	wire        pal_w_ena;
+
+	wire [ 7:0] pal_r_addr_0;
+	wire [15:0] pal_r_data_1;
+
+	// Timing gen
+	wire        tg_hsync_0;
+	wire        tg_vsync_0;
+	wire        tg_active_0;
+	wire        tg_h_first_0;
+	wire        tg_h_last_0;
+	wire        tg_v_first_0;
+	wire        tg_v_last_0;
+
+	// Video status
+	reg  [15:0] vs_frame_cnt;
+	reg         vs_in_vbl;
+
+	// Pixel pipeline
+	reg         pp_active_1;
+	reg         pp_ydbl_1;
+	reg         pp_xdbl_1;
+	reg  [15:0] pp_addr_base_1;
+	reg  [15:0] pp_addr_cur_1;
+
+	reg         pp_data_load_2;
+	reg  [31:0] pp_data_3;
+
+	wire [11:0] pp_data_4;
+	wire        pp_hsync_4;
+	wire        pp_vsync_4;
+	wire        pp_de_4;
+
+
+
+	// Frame Buffer
+	// ------------
+
+	vid_framebuf fb_I (
+		.v_addr_0  (fb_v_addr_0),
+		.v_data_1  (fb_v_data_1),
+		.v_re_0    (fb_v_re_0),
+		.a_addr_0  (fb_a_addr_0),
+		.a_rdata_1 (fb_a_rdata_1),
+		.a_wdata_0 (fb_a_wdata_0),
+		.a_wmsk_0  (fb_a_wmsk_0),
+		.a_we_0    (fb_a_we_0),
+		.a_rdy_0   (fb_a_rdy_0),
+		.clk       (clk)
+	);
+
+
+	// Palette
+	// -------
+
+	vid_palette pal_I (
+		.w_addr_0 (pal_w_addr),
+		.w_data_0 (pal_w_data),
+		.w_ena_0  (pal_w_ena),
+		.r_addr_0 (pal_r_addr_0),
+		.r_data_1 (pal_r_data_1),
+		.clk      (clk)
+	);
+
+
+	// Timing Generator
+	// ----------------
+
+	vid_tgen #(
+`ifndef COMPAT_MODE
+		.H_WIDTH  (  10 ),
+		.H_FP     (  16 ),
+		.H_SYNC   (  96 ),
+		.H_BP     (  48 ),
+		.H_ACTIVE ( 640 ),
+		.V_WIDTH  (   9 ),
+		.V_FP     (  12 ),
+		.V_SYNC   (   2 ),
+		.V_BP     (  35 ),
+		.V_ACTIVE ( 400 )
+`else
+		.H_WIDTH  (  10 ),
+		.H_FP     (  16 ),
+		.H_SYNC   (  96 ),
+		.H_BP     (  48 ),
+		.H_ACTIVE ( 640 ),
+		.V_WIDTH  (   9 ),
+		.V_FP     (  10 ),
+		.V_SYNC   (   2 ),
+		.V_BP     (  33 ),
+		.V_ACTIVE ( 480 )
+`endif
+	) tgen_I (
+		.vid_hsync   (tg_hsync_0),
+		.vid_vsync   (tg_vsync_0),
+		.vid_active  (tg_active_0),
+		.vid_h_first (tg_h_first_0),
+		.vid_h_last  (tg_h_last_0),
+		.vid_v_first (tg_v_first_0),
+		.vid_v_last  (tg_v_last_0),
+		.clk         (clk),
+		.rst         (rst)
+	);
+
+
+	// Video Status and counter
+	// ------------------------
+
+	always @(posedge clk)
+		vs_in_vbl <= (vs_in_vbl & ~tg_v_first_0) | (tg_v_last_0 & tg_h_last_0);
+
+	always @(posedge clk)
+		if (rst)
+			vs_frame_cnt <= 0;
+		else
+			vs_frame_cnt <= vs_frame_cnt + (tg_v_last_0 & tg_h_last_0);
+
+
+	// Video Pipeline
+	// --------------
+
+	// Pixel fetch
+`ifndef COMPAT_MODE
+		// Counter control in 640x400 -> Double pixels
+	always @(posedge clk) begin
+		pp_active_1 <= tg_active_0;
+		pp_ydbl_1   <= (pp_ydbl_1 ^ tg_h_first_0) |  tg_v_first_0;
+		pp_xdbl_1   <= (pp_xdbl_1 ^ 1'b1        ) & ~tg_h_first_0;
+	end
+`else
+		// Counter control in 640x480 -> Double X, Double-or-Triple Y
+	reg [3:0] pp_yscale_state;
+
+	always @(posedge clk)
+		if (tg_h_first_0) begin
+			if (tg_v_first_0) begin
+				pp_yscale_state <= 4'h0;
+				pp_ydbl_1       <= 1'b0;
+			end else begin
+				case (pp_yscale_state)
+					4'h0:    { pp_ydbl_1, pp_yscale_state } <= { 1'b1, 4'h1 };
+					4'h1:    { pp_ydbl_1, pp_yscale_state } <= { 1'b0, 4'h2 };
+					4'h2:    { pp_ydbl_1, pp_yscale_state } <= { 1'b1, 4'h3 };
+					4'h3:    { pp_ydbl_1, pp_yscale_state } <= { 1'b0, 4'h4 };
+					4'h4:    { pp_ydbl_1, pp_yscale_state } <= { 1'b0, 4'h5 };
+					4'h5:    { pp_ydbl_1, pp_yscale_state } <= { 1'b1, 4'h6 };
+					4'h6:    { pp_ydbl_1, pp_yscale_state } <= { 1'b0, 4'h7 };
+					4'h7:    { pp_ydbl_1, pp_yscale_state } <= { 1'b1, 4'h8 };
+					4'h8:    { pp_ydbl_1, pp_yscale_state } <= { 1'b0, 4'h9 };
+					4'h9:    { pp_ydbl_1, pp_yscale_state } <= { 1'b0, 4'ha };
+					4'ha:    { pp_ydbl_1, pp_yscale_state } <= { 1'b1, 4'hb };
+					4'hb:    { pp_ydbl_1, pp_yscale_state } <= { 1'b0, 4'h0 };
+					default: { pp_ydbl_1, pp_yscale_state } <= { 1'b0, 4'h0 };
+				endcase;
+			end
+		end
+
+	always @(posedge clk) begin
+		pp_active_1 <= tg_active_0;
+		pp_xdbl_1   <= (pp_xdbl_1 ^ 1'b1) & ~tg_h_first_0;
+	end
+`endif
+
+		// Counters
+	always @(posedge clk)
+		if (tg_h_first_0) begin
+			if (tg_v_first_0)
+				pp_addr_base_1 <= 0;
+			else
+				pp_addr_base_1 <= pp_addr_base_1 + (pp_ydbl_1 ? 16'd320 : 16'd0);
+		end
+
+	always @(posedge clk)
+		if (tg_h_first_0)
+			pp_addr_cur_1 <= tg_v_first_0 ? 16'd0 : pp_addr_base_1;
+		else
+			pp_addr_cur_1 <= pp_addr_cur_1 + pp_xdbl_1;
+
+		// Frame Buffer
+	assign fb_v_addr_0 = pp_addr_cur_1[15:2];
+	assign fb_v_re_0   = pp_active_1 & (pp_addr_cur_1[1:0] == 2'b00) & ~pp_xdbl_1;
+
+		// Shift Reg
+	always @(posedge clk)
+		pp_data_load_2 <= fb_v_re_0;
+
+	always @(posedge clk)
+		if (pp_xdbl_1)
+			pp_data_3 <= pp_data_load_2 ? fb_v_data_1 : { 8'h00, pp_data_3[31:8] };
+
+	// Palette fetch
+	assign pal_r_addr_0 = pp_data_3[7:0];
+	assign pp_data_4 = {
+		pal_r_data_1[15:12],	// R[15:11]
+		pal_r_data_1[10: 7],	// G[10: 5]
+		pal_r_data_1[4:1]		// B[ 4: 0]
+	};
+
+	// Sync signals
+	delay_bit #(4) dly_hsync ( ~tg_hsync_0,  pp_hsync_4, clk );
+	delay_bit #(4) dly_vsync ( ~tg_vsync_0,  pp_vsync_4, clk );
+	delay_bit #(4) dly_de    (  tg_active_0, pp_de_4,    clk );
+
+	// Output buffers
+	hdmi_phy_1x #(
+		.DW(12)
+	) phy_I (
+		.hdmi_data  ({hdmi_r, hdmi_g, hdmi_b}),
+		.hdmi_hsync (hdmi_hsync),
+		.hdmi_vsync (hdmi_vsync),
+		.hdmi_de    (hdmi_de),
+		.hdmi_clk   (hdmi_clk),
+		.in_data    (pp_data_4),
+		.in_hsync   (pp_hsync_4),
+		.in_vsync   (pp_vsync_4),
+		.in_de      (pp_de_4),
+		.clk        (clk)
+	);
+
+
+	// Bus Interface
+	// -------------
+
+	// Ack
+	always @(posedge clk)
+		wb_ack <= wb_cyc & ~wb_ack & (~wb_addr[15] | fb_a_rdy_0);
+
+	// Read Mux
+	always @(*)
+	begin
+		wb_rdata = 32'h00000000;
+		if (wb_ack)
+			wb_rdata = wb_addr[15] ? fb_a_rdata_1 : { 15'h0000, vs_in_vbl, vs_frame_cnt };
+	end
+
+	// Frame Buffer write
+	assign fb_a_addr_0  = wb_addr[13:0];
+	assign fb_a_wdata_0 = wb_wdata;
+	assign fb_a_wmsk_0  = wb_wmsk;
+	assign fb_a_we_0    = wb_cyc & wb_we & ~wb_ack & wb_addr[15];
+
+	// Palette write
+	assign pal_w_addr = wb_addr[7:0];
+	assign pal_w_data = { wb_wdata[23:19], wb_wdata[15:10], wb_wdata[7:3] };
+	assign pal_w_ena  = wb_cyc & wb_we & ~wb_ack & (wb_addr[15:14] == 2'b01);
+
+endmodule // vid_top

+ 1 - 0
projects/riscv_doom/sim/spiflash.v

@@ -0,0 +1 @@
+../../riscv_usb/sim/spiflash.v

+ 89 - 0
projects/riscv_doom/sim/top_tb.v

@@ -0,0 +1,89 @@
+/*
+ * top_tb.v
+ *
+ * vim: ts=4 sw=4
+ *
+ * Copyright (C) 2019  Sylvain Munaut <tnt@246tNt.com>
+ * All rights reserved.
+ *
+ * BSD 3-clause, see LICENSE.bsd
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ *       notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ *       notice, this list of conditions and the following disclaimer in the
+ *       documentation and/or other materials provided with the distribution.
+ *     * Neither the name of the <organization> nor the
+ *       names of its contributors may be used to endorse or promote products
+ *       derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+`default_nettype none
+
+module top_tb;
+
+	// Signals
+	// -------
+
+	wire [3:0] spi_io;
+	wire       spi_sck;
+	wire [1:0] spi_cs_n;
+
+	wire uart_rx;
+	wire uart_tx;
+
+
+	// Setup recording
+	// ---------------
+
+	initial begin
+		$dumpfile("top_tb.vcd");
+		$dumpvars(0,top_tb);
+		# 2000000 $finish;
+	end
+
+
+	// DUT
+	// ---
+
+	top dut_I (
+		.spi_io   (spi_io),
+		.spi_sck  (spi_sck),
+		.spi_cs_n (spi_cs_n),
+		.uart_rx  (uart_rx),
+		.uart_tx  (uart_tx),
+		.btn      (1'b1),
+		.rgb      (),
+		.clk_in   (1'b0)
+	);
+
+
+	// Support
+	// -------
+
+	pullup(uart_tx);
+	pullup(uart_rx);
+
+	spiflash flash_I (
+		.csb(spi_cs_n[0]),
+		.clk(spi_sck),
+		.io0(spi_io[0]),
+		.io1(spi_io[1]),
+		.io2(spi_io[2]),
+		.io3(spi_io[3])
+	);
+
+endmodule // top_tb

+ 564 - 0
projects/riscv_doom/sw/doom_ctrl.py

@@ -0,0 +1,564 @@
+#!/usr/bin/env python3
+#
+# DOOM control application
+#
+# Copyright (C) 2021 Sylvain Munaut
+# SPDX-License-Identifier: MIT
+#
+
+import base64
+import pygame
+import serial
+import struct
+import sys
+
+from io import BytesIO
+
+# Add local WASD binding ...
+if True:
+	# Tweaked ...
+	KEYBIND = {
+		pygame.K_w:	pygame.K_UP,		# Forward
+		pygame.K_a:	pygame.K_COMMA,		# Strafe left
+		pygame.K_s: pygame.K_DOWN,		# Back
+		pygame.K_d: pygame.K_PERIOD,	# Strafe right
+		pygame.K_e: pygame.K_SPACE,		# Use
+	}
+	DISABLE_MOUSE_Y = True
+else:
+	# Original controls
+	KEYBIND = {}
+	DISABLE_MOUSE_Y = False
+
+
+def map_key(k):
+
+	KEYVAL = {
+		'KEY_LEFTARROW':	 0,
+		'KEY_RIGHTARROW':	 1,
+		'KEY_DOWNARROW':	 2,
+		'KEY_UPARROW':		 3,
+		'KEY_RSHIFT':		 4,
+		'KEY_RCTRL':		 5,
+		'KEY_RALT':			 6,
+		'KEY_ESCAPE':		 7,
+		'KEY_ENTER':		 8,
+		'KEY_TAB':			 9,
+		'KEY_BACKSPACE':	10,
+		'KEY_PAUSE':		11,
+		'KEY_EQUALS':		12,
+		'KEY_MINUS':		13,
+		'KEY_F1':			14,
+		'KEY_F2':			15,
+		'KEY_F3':			16,
+		'KEY_F4':			17,
+		'KEY_F5':			18,
+		'KEY_F6':			19,
+		'KEY_F7':			20,
+		'KEY_F8':			21,
+		'KEY_F9':			22,
+		'KEY_F10':			23,
+		'KEY_F11':			24,
+		'KEY_F12':			25,
+	}
+
+	KEYMAP = {
+		pygame.K_LEFT:		'KEY_LEFTARROW',
+		pygame.K_RIGHT:		'KEY_RIGHTARROW',
+		pygame.K_DOWN:		'KEY_DOWNARROW',
+		pygame.K_UP:		'KEY_UPARROW',
+		pygame.K_RSHIFT:	'KEY_RSHIFT',
+		pygame.K_LSHIFT:	'KEY_RSHIFT',
+		pygame.K_RCTRL:		'KEY_RCTRL',
+		pygame.K_LCTRL:		'KEY_RCTRL',
+		pygame.K_RALT:		'KEY_RALT',
+		pygame.K_LALT:		'KEY_RALT',
+		pygame.K_ESCAPE:	'KEY_ESCAPE',
+		pygame.K_RETURN:	'KEY_ENTER',
+		pygame.K_TAB:		'KEY_TAB',
+		pygame.K_BACKSPACE:	'KEY_BACKSPACE',
+		pygame.K_PAUSE:		'KEY_PAUSE',
+		pygame.K_EQUALS:	'KEY_EQUALS',
+		pygame.K_MINUS:		'KEY_MINUS',
+		pygame.K_F1:		'KEY_F1',
+		pygame.K_F2:		'KEY_F2',
+		pygame.K_F3:		'KEY_F3',
+		pygame.K_F4:		'KEY_F4',
+		pygame.K_F5:		'KEY_F5',
+		pygame.K_F6:		'KEY_F6',
+		pygame.K_F7:		'KEY_F7',
+		pygame.K_F8:		'KEY_F8',
+		pygame.K_F9:		'KEY_F9',
+		pygame.K_F10:		'KEY_F10',
+		pygame.K_F11:		'KEY_F11',
+		pygame.K_F12:		'KEY_F12',
+	}
+
+	# Default is no mapping
+	rc = None
+
+	# Check for local keybindings
+	if k in KEYBIND:
+		k = KEYBIND[k]
+
+	# Check for spcial mapping
+	if k in KEYMAP:
+		rc = KEYVAL[KEYMAP[k]]
+
+	# Map upper case to lowercase
+	elif ord('A') <= k <= ord('Z'):
+		rc = k - ord('A') + ord('a')
+
+	# Default is to map 1:1 for anything in printable ascii range
+	elif 32 <= k <= 127:
+		rc = k
+
+	return rc
+
+
+def main(argv0, dev='/dev/ttyUSB1'):
+
+	# Init PyGame
+	pygame.init()
+
+	# Load logo
+	logo = pygame.image.load(
+		BytesIO(base64.decodebytes(LOGO_DATA)),
+		'logo.jpg'
+	)
+
+	# Init screen
+	screen = pygame.display.set_mode([logo.get_width(), logo.get_height()])
+	pygame.display.set_caption("Doom controller")
+
+	pygame.time.set_timer(pygame.USEREVENT, 50)
+
+	# Open port
+	port = serial.Serial(dev, baudrate=1000000)
+
+	# Main loop
+	running = True
+	held = set()
+
+	mouse_active = False
+	mdx = 0
+	mdy = 0
+
+	while running:
+		# Process event
+		pygame.event.pump()
+		event = pygame.event.wait()
+
+		if event.type == pygame.QUIT:
+			running = False
+
+		elif event.type == pygame.VIDEORESIZE:
+			screen.blit(pygame.transform.scale(logo, event.dict['size']), (0, 0))
+			pygame.display.update()
+
+		elif event.type == pygame.VIDEOEXPOSE:
+			screen.blit(pygame.transform.scale(logo, screen.get_size()), (0, 0))
+			pygame.display.update()
+
+		elif event.type == pygame.KEYDOWN:
+			kv = map_key(event.key)
+			if (kv is not None) and (kv not in held):
+				held.add(kv)
+				cmd = (kv | 0x80).to_bytes(1, 'little')
+				port.write(cmd)
+
+		elif event.type == pygame.KEYUP:
+			if mouse_active and (event.key in (pygame.K_LMETA, pygame.K_RMETA)):
+				# Release mouse
+				pygame.mouse.set_visible(True)
+				pygame.event.set_grab(False)
+
+				# Release all pressed buttons
+				for kv in [28, 29, 30]:
+					if kv in held:
+						held.remove(kv)
+						cmd = kv.to_bytes(1, 'little')
+						port.write(cmd)
+
+				# Not active
+				mouse_active = False
+
+			kv = map_key(event.key)
+			if (kv is not None) and (kv in held):
+				held.remove(kv)
+				cmd = kv.to_bytes(1, 'little')
+				port.write(cmd)
+
+		elif event.type == pygame.MOUSEBUTTONDOWN:
+			if mouse_active:
+				# We have the mouse, send events
+				kv = (28 + event.button - 1) if (1 <= event.button <= 3) else None
+				if (kv is not None) and (kv not in held):
+					held.add(kv)
+					cmd = (kv | 0x80).to_bytes(1, 'little')
+					port.write(cmd)
+
+			elif event.button == 1:
+				# If button 1, grab mouse
+				pygame.mouse.set_visible(False)
+				pygame.event.set_grab(True)
+
+				# Active
+				mouse_active = True
+
+		elif event.type == pygame.MOUSEBUTTONUP:
+			if mouse_active:
+				# We have the mouse, send events
+				kv = (28 + event.button - 1) if (1 <= event.button <= 3) else None
+				if (kv is not None) and (kv in held):
+					held.remove(kv)
+					cmd = kv.to_bytes(1, 'little')
+					port.write(cmd)
+
+		elif event.type == pygame.MOUSEMOTION:
+			if mouse_active:
+				# We accumulate mouse events to not overwhelm
+				# target ...
+				mdx += event.rel[0]
+				mdy += event.rel[1]
+
+		elif event.type == pygame.USEREVENT:
+			# Hack
+			if DISABLE_MOUSE_Y:
+				mdy = 0
+
+			# Send pending mouse events
+			if (mdx != 0) or (mdy != 0):
+
+				# Send what we can
+				x = max(min(mdx, 127), -128)
+				y = max(min(mdy, 127), -128)
+
+				mdx -= x
+				mdy -= y
+
+				# Send it
+				port.write(struct.pack('bbb', 31, x, y))
+
+
+LOGO_DATA = b"""
+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
+"""
+
+if __name__ == '__main__':
+	sys.exit(main(*sys.argv))