Bläddra i källkod

projects/memtest: Import memory tester project

This helps test memory cores. Currently support QSPI PSRAM and the
HyperRAM controller.

And as a demo, this can also output an image from that memory to an
HDMI PMOD.

Signed-off-by: Sylvain Munaut <tnt@246tNt.com>
Sylvain Munaut 4 år sedan
förälder
incheckning
4cb8155dc1

+ 54 - 0
projects/memtest/Makefile

@@ -0,0 +1,54 @@
+# Project config
+PROJ = memtest
+
+PROJ_DEPS := misc ice40
+PROJ_RTL_SRCS := $(addprefix rtl/, \
+	memtest.v \
+	uart2wb.v \
+	sys_mgr.v \
+)
+PROJ_TESTBENCHES := \
+	$(NULL)
+PROJ_TOP_SRC := rtl/top.v
+PROJ_TOP_MOD := top
+
+# Target config
+BOARD ?= icebreaker
+DEVICE = up5k
+PACKAGE = sg48
+
+PIN_DEF = $(BUILD_TMP)/$(PROJ_TOP_MOD).pcf
+NEXTPNR_ARGS = --no-promote-globals --timing-allow-fail --pre-pack data/clocks.py --pre-place $(CORE_ice40_DIR)/sw/serdes-nextpnr-place.py
+
+PCFS = $(abspath data/$(PROJ_TOP_MOD)-$(BOARD).pcf) $(abspath data/$(PROJ_TOP_MOD)-$(BOARD)-$(MEM).pcf)
+
+# Build options
+	# spi / hyperram
+MEM ?= spi
+	# none / 4bpp / 12bpp
+VIDEO ?= none
+
+YOSYS_READ_ARGS += -DMEM_$(MEM)=1 -DVIDEO_$(VIDEO)=1
+
+ifeq ($(MEM),spi)
+	PROJ_DEPS += qspi_master
+endif
+ifeq ($(MEM),hyperram)
+	PROJ_DEPS += hyperram
+endif
+
+ifneq ($(VIDEO),none)
+	PROJ_DEPS += video
+	PROJ_RTL_SRCS += $(addprefix rtl/, \
+		hdmi_buf.v \
+		hdmi_out.v \
+	)
+	PCFS += $(abspath data/$(PROJ_TOP_MOD)-$(BOARD)-hdmi-$(VIDEO).pcf)
+endif
+
+# Include default rules
+include ../../build/project-rules.mk
+
+# Custom rules
+$(PIN_DEF): $(PCFS)
+	cat $^ > $@

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

@@ -0,0 +1,4 @@
+ctx.addClock("clk_1x", 37)
+ctx.addClock("clk_2x", 74)
+ctx.addClock("clk_4x", 139)	# Actually 147
+ctx.addClock("clk_rd", 139)	# Actually 147

BIN
projects/memtest/data/kn-12bpp.data


+ 1 - 0
projects/memtest/data/kn-12bpp.data.pal

@@ -0,0 +1 @@
+9(J(g@*{<&�[?±{_½Š\Ï¢ƒ²²¯à½”ÂÄÀæÆ«ÔÕÑôßËêëè

BIN
projects/memtest/data/kn-4bpp.data


BIN
projects/memtest/data/lena1-4bpp.data


BIN
projects/memtest/data/lena2-4bpp.data


+ 20 - 0
projects/memtest/data/top-icebreaker-hdmi-12bpp.pcf

@@ -0,0 +1,20 @@
+# HDMI 12bpp on PMOD1AB
+set_io --warn-no-port hdmi_data[11] 43	# b[3]
+set_io --warn-no-port hdmi_data[10] 42	# b[2]
+set_io --warn-no-port hdmi_data[9] 36	# b[1]
+set_io --warn-no-port hdmi_data[8] 34	# b[0]
+
+set_io --warn-no-port hdmi_data[7] 47	# g[3]
+set_io --warn-no-port hdmi_data[6] 46	# g[2]
+set_io --warn-no-port hdmi_data[5] 45	# g[1]
+set_io --warn-no-port hdmi_data[4] 44	# g[0]
+
+set_io --warn-no-port hdmi_data[3]  4	# r[3]
+set_io --warn-no-port hdmi_data[2]  3	# r[2]
+set_io --warn-no-port hdmi_data[1]  2	# r[1]
+set_io --warn-no-port hdmi_data[0] 48	# r[0]
+
+set_io --warn-no-port hdmi_hsync 31
+set_io --warn-no-port hdmi_vsync 28
+set_io --warn-no-port hdmi_de 32
+set_io --warn-no-port hdmi_clk 38

+ 9 - 0
projects/memtest/data/top-icebreaker-hdmi-4bpp.pcf

@@ -0,0 +1,9 @@
+# HDMI 4bpp on PMOD2
+set_io --warn-no-port hdmi_data[0] 26 # R
+set_io --warn-no-port hdmi_data[1] 27 # G
+set_io --warn-no-port hdmi_data[2] 23 # B
+set_io --warn-no-port hdmi_data[3] 19 # I
+set_io --warn-no-port hdmi_hsync 21
+set_io --warn-no-port hdmi_vsync 18
+set_io --warn-no-port hdmi_de 20
+set_io --warn-no-port hdmi_clk 25

+ 41 - 0
projects/memtest/data/top-icebreaker-hyperram.pcf

@@ -0,0 +1,41 @@
+# Quad HyperRAM
+set_io --warn-no-port hram_dq[0] 43
+set_io --warn-no-port hram_dq[1] 38
+set_io --warn-no-port hram_dq[2] 34
+set_io --warn-no-port hram_dq[3] 31
+set_io --warn-no-port hram_dq[4] 28
+set_io --warn-no-port hram_dq[5] 32
+set_io --warn-no-port hram_dq[6] 36
+set_io --warn-no-port hram_dq[7] 42
+
+set_io --warn-no-port hram_rwds 44
+
+set_io --warn-no-port hram_ck 47
+
+set_io --warn-no-port hram_rst_n 46
+
+set_io --warn-no-port hram_cs_n[0] 2
+set_io --warn-no-port hram_cs_n[1] 48
+set_io --warn-no-port hram_cs_n[2] 4
+set_io --warn-no-port hram_cs_n[3] 3
+
+# Single HyperRAM
+#set_io --warn-no-port hram_dq[0] 34
+#set_io --warn-no-port hram_dq[1] 36
+#set_io --warn-no-port hram_dq[2] 46
+#set_io --warn-no-port hram_dq[3] 2
+#set_io --warn-no-port hram_dq[4] 48
+#set_io --warn-no-port hram_dq[5] 28
+#set_io --warn-no-port hram_dq[6] 32
+#set_io --warn-no-port hram_dq[7] 38
+#
+#set_io --warn-no-port hram_rwds 47
+#
+#set_io --warn-no-port hram_ck 42
+#
+#set_io --warn-no-port hram_rst_n 45
+#
+#set_io --warn-no-port hram_cs_n[0] 44
+#set_io --warn-no-port hram_cs_n[1] 31
+#set_io --warn-no-port hram_cs_n[2] 4
+#set_io --warn-no-port hram_cs_n[3] 3

+ 8 - 0
projects/memtest/data/top-icebreaker-spi.pcf

@@ -0,0 +1,8 @@
+# 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

+ 70 - 0
projects/memtest/rtl/hdmi_buf.v

@@ -0,0 +1,70 @@
+/*
+ * hdmi_buf.v
+ *
+ * vim: ts=4 sw=4
+ *
+ * Copyright (C) 2020  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 hdmi_buf (
+	// Write port
+	input  wire [ 8:0] waddr,
+	input  wire [31:0] wdata,
+	input  wire        wren,
+
+	// Read port
+	input  wire [ 9:0] raddr,
+	output wire [15:0] rdata,
+
+	// Clock
+	input  wire clk
+);
+
+	genvar i;
+
+	generate
+		for (i=0; i<4; i=i+1)
+			ice40_ebr #(
+				.READ_MODE(2),
+				.WRITE_MODE(1)
+			) ebr_wrap_I (
+				.wr_addr(waddr),
+				.wr_data({wdata[i*4+:4], wdata[16+i*4+:4]}),
+				.wr_mask(8'h00),
+				.wr_ena(wren),
+				.wr_clk(clk),
+				.rd_addr(raddr),
+				.rd_data(rdata[i*4+:4]),
+				.rd_ena(1'b1),
+				.rd_clk(clk)
+			);
+	endgenerate
+
+endmodule

+ 322 - 0
projects/memtest/rtl/hdmi_out.v

@@ -0,0 +1,322 @@
+/*
+ * hdmi_out.v
+ *
+ * vim: ts=4 sw=4
+ *
+ * Copyright (C) 2020  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 hdmi_out #(
+	parameter integer DW = 4,
+)(
+	// HDMI pads
+	output wire [DW-1:0] hdmi_data,
+	output wire          hdmi_hsync,
+	output wire          hdmi_vsync,
+	output wire          hdmi_de,
+	output wire          hdmi_clk,
+
+	// Memory interface
+	output wire [31:0] mi_addr,
+	output wire [ 6:0] mi_len,
+	output wire        mi_rw,
+	output wire        mi_valid,
+	input  wire        mi_ready,
+
+	output wire [31:0] mi_wdata,	// Not used
+	input  wire        mi_wack,		// Not used
+	input  wire        mi_wlast,	// Not used
+
+	input  wire [31:0] mi_rdata,
+	input  wire        mi_rstb,
+	input  wire        mi_rlast,
+
+	// Wishbone interface
+	input  wire [31:0] wb_wdata,
+	output wire [31:0] wb_rdata,
+	input  wire [ 6:0] wb_addr,
+	input  wire        wb_we,
+	input  wire        wb_cyc,
+	output reg         wb_ack,
+
+	// Clocks / Sync / Reset
+	input  wire clk_1x,
+	input  wire clk_4x,
+	input  wire sync_4x,
+	input  wire rst
+);
+
+	genvar i;
+
+
+	// Signals
+	// -------
+
+	// Timing Generator
+	wire vt_hsync;
+	wire vt_vsync;
+	wire vt_de;
+	wire vt_hfirst;
+	wire vt_vfirst;
+	wire vt_vlast;
+
+	wire vt_trig;
+
+	// DMA config
+	reg  [31:0] dma_cfg_base;
+	reg  [ 6:0] dma_cfg_bn_cnt;
+	reg  [ 6:0] dma_cfg_bn_len;
+	reg  [ 6:0] dma_cfg_bl_len;
+	reg  [ 7:0] dma_cfg_bl_inc;
+
+	reg         dma_run;
+
+	// DMA runtime
+	reg  [31:0] dma_addr;
+	reg  [ 7:0] dma_cnt;
+	wire        dma_last;
+	wire        dma_valid;
+
+	// Video Buffer
+	reg         vb_pingpong;
+	reg  [ 7:0] vb_waddr;
+	wire [31:0] vb_wdata;
+	wire        vb_wren;
+	reg  [ 8:0] vb_raddr;
+	wire [15:0] vb_rdata;
+
+	// Palette
+	wire [DW-1:0] pal_wdata;
+	wire [   5:0] pal_waddr;
+	reg           pal_wren;
+
+	// Video Out
+	reg  [   1:0] frame_cnt;
+
+	wire [DW-1:0] vo_data[0:3];
+	wire          vo_hsync;
+	wire          vo_vsync;
+	wire          vo_de;
+
+
+	// Wishbone interface
+	// ------------------
+
+	// Ack
+	always @(posedge clk_1x)
+		wb_ack <= wb_cyc & ~wb_ack;
+
+	// Register Write
+	always @(posedge clk_1x or posedge rst)
+		if (rst) begin
+			dma_run <= 1'b0;
+			dma_cfg_base   <= 0;
+			dma_cfg_bn_cnt <= 0;
+			dma_cfg_bn_len <= 0;
+			dma_cfg_bl_len <= 0;
+			dma_cfg_bl_inc <= 0;
+		end else if (wb_cyc & ~wb_ack & ~wb_addr[6]) begin
+			if (wb_addr[0])
+				dma_cfg_base <= wb_wdata;
+			else begin
+				dma_run        <= wb_wdata[31];
+				dma_cfg_bn_cnt <= wb_wdata[30:24];
+				dma_cfg_bn_len <= wb_wdata[22:16];
+				dma_cfg_bl_len <= wb_wdata[14: 8];
+				dma_cfg_bl_inc <= wb_wdata[ 7: 0];
+			end
+		end
+
+	// Palette write
+	assign pal_wdata = wb_wdata[DW-1:0];
+	assign pal_waddr = wb_addr[5:0];
+
+	always @(posedge clk_1x)
+		pal_wren <= wb_cyc & ~wb_ack & wb_addr[6];
+
+	// No read support
+	assign wb_rdata = 32'h00000000;
+
+
+	// Timing generator
+	// ----------------
+
+		// Standard 1080p60
+	vid_tgen #(
+		.H_WIDTH(12),
+		.V_WIDTH(12),
+		.H_FP     (  88 / 4),
+		.H_SYNC   (  44 / 4),
+		.H_BP     ( 148 / 4),
+		.H_ACTIVE (1920 / 4),
+		.V_FP     (   4),
+		.V_SYNC   (   5),
+		.V_BP     (  36),
+		.V_ACTIVE (1080)
+	) hdmi_tgen_I (
+		.vid_hsync(vt_hsync),
+		.vid_vsync(vt_vsync),
+		.vid_active(vt_de),
+		.vid_h_first(vt_hfirst),
+		.vid_h_last(),
+		.vid_v_first(vt_vfirst),
+		.vid_v_last(vt_vlast),
+		.clk(clk_1x),
+		.rst(rst)
+	);
+
+	assign vt_trig = vt_de & vt_hfirst;
+
+
+	// DMA
+	// ---
+
+	// DMA requests
+	assign dma_last = (dma_cnt[6:0] == 4'h0);
+
+	always @(posedge clk_1x)
+	begin
+		if (~dma_run)
+			dma_cnt <= 8'h00;
+		else if (vt_trig)
+			dma_cnt <= { 1'b1, dma_cfg_bn_cnt };
+		else if (mi_ready & mi_valid)
+			dma_cnt <= dma_cnt - 1;
+	end
+
+	assign dma_valid = dma_cnt[7];
+
+	always @(posedge clk_1x)
+		if (vt_trig & vt_vlast)
+			dma_addr <= dma_cfg_base;
+		else if (mi_ready & mi_valid)
+			dma_addr <= dma_addr + (dma_last ? dma_cfg_bl_inc : dma_cfg_bn_len) + 1;
+
+	// DMA Memory interface
+	assign mi_addr  = dma_addr;
+	assign mi_len   = dma_last ? dma_cfg_bl_len : dma_cfg_bn_len;
+	assign mi_rw    = 1'b1;
+	assign mi_valid = dma_valid;
+
+	assign mi_wdata = 32'hxxxxxxxx;
+
+	// Buffer write path
+	always @(posedge clk_1x)
+		if (vt_trig)
+			vb_waddr <= 8'h00;
+		else
+			vb_waddr <= vb_waddr + mi_rstb;
+
+	assign vb_wdata = mi_rdata;
+	assign vb_wren  = mi_rstb;
+
+
+	// Video Buffer
+	// ------------
+
+	// Ping-Pong
+	always @(posedge clk_1x)
+		if (rst)
+			vb_pingpong <= 1'b0;
+		else
+			vb_pingpong <= vb_pingpong ^ vt_trig;
+
+	// Memory
+	hdmi_buf line_I (
+		.waddr({vb_pingpong, vb_waddr}),
+		.wdata(vb_wdata),
+		.wren (vb_wren),
+		.raddr({~vb_pingpong, vb_raddr}),
+		.rdata(vb_rdata),
+		.clk(clk_1x)
+	);
+
+
+	// Output
+	// ------
+
+	// Frame counter (for temporal dither)
+	always @(posedge clk_1x)
+		if (vt_trig & vt_vfirst)
+			frame_cnt <= frame_cnt + 1;
+
+	// Buffer read
+	always @(posedge clk_1x)
+		if (vt_trig)
+			vb_raddr <= 9'h000;
+		else
+			vb_raddr <= vb_raddr + vt_de;
+
+	// Palette lookup
+	generate
+		for (i=0; i<4; i=i+1)
+			ram_sdp #(
+				.AWIDTH(6),
+				.DWIDTH(DW)
+			) pal_I (
+				.wr_addr(pal_waddr),
+				.wr_data(pal_wdata),
+				.wr_ena(pal_wren),
+				.rd_addr({frame_cnt, vb_rdata[(3-i)*4+:4]}),
+				.rd_data(vo_data[i]),
+				.rd_ena(1'b1),
+				.clk(clk_1x)
+			);
+	endgenerate
+
+	// Control delay
+	delay_bus #(3, 3) dly_vs_I (
+		.d({vt_hsync, vt_vsync, vt_de}),
+		.q({vo_hsync, vo_vsync, vo_de}),
+		.clk(clk_1x)
+	);
+
+	// PHY
+	hdmi_phy_4x #(
+		.DW(DW)
+	) phy_I (
+		.hdmi_data(hdmi_data),
+		.hdmi_hsync(hdmi_hsync),
+		.hdmi_vsync(hdmi_vsync),
+		.hdmi_de(hdmi_de),
+		.hdmi_clk(hdmi_clk),
+		.in_data0(vo_data[0]),
+		.in_data1(vo_data[1]),
+		.in_data2(vo_data[2]),
+		.in_data3(vo_data[3]),
+		.in_hsync(vo_hsync),
+		.in_vsync(vo_vsync),
+		.in_de(vo_de),
+		.clk_1x(clk_1x),
+		.clk_4x(clk_4x),
+		.clk_sync(sync_4x)
+	);
+
+endmodule

+ 234 - 0
projects/memtest/rtl/memtest.v

@@ -0,0 +1,234 @@
+/*
+ * memtest.v
+ *
+ * vim: ts=4 sw=4
+ *
+ * Copyright (C) 2020  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 memtest #(
+	parameter integer ADDR_WIDTH = 32,
+
+	// auto
+	parameter integer AL = ADDR_WIDTH - 1
+)(
+	// Memory interface
+	output wire [AL:0] mi_addr,
+	output wire [ 6:0] mi_len,
+	output wire        mi_rw,
+	output wire        mi_valid,
+	input  wire        mi_ready,
+
+	output wire [31:0] mi_wdata,
+	output wire [ 3:0] mi_wmsk,
+	input  wire        mi_wack,
+
+	input  wire [31:0] mi_rdata,
+	input  wire        mi_rstb,
+
+	// Wishbone interface
+	input  wire [31:0] wb_wdata,
+	output wire [31:0] wb_rdata,
+	input  wire [ 8:0] wb_addr,
+	input  wire        wb_we,
+	input  wire        wb_cyc,
+	output wire        wb_ack,
+
+	// Clock / Reset
+	input  wire clk,
+	input  wire rst
+);
+
+	// Signals
+	// -------
+
+	// Buffers
+	wire [ 7:0] bw_waddr;
+	wire [31:0] bw_wdata;
+	wire        bw_wren;
+
+	reg  [ 7:0] bw_raddr;
+	wire [31:0] bw_rdata;
+	wire        bw_rden;
+
+	reg  [ 7:0] br_waddr;
+	wire [31:0] br_wdata;
+	wire        br_wren;
+
+	wire [ 7:0] br_raddr;
+	wire [31:0] br_rdata;
+	wire        br_rden;
+
+	// Wishbone
+	reg wb_ack_i;
+	reg wb_we_cmd;
+	reg wb_we_addr;
+
+	// Commands
+	reg         cmd_valid;
+	reg         cmd_start;
+	reg         cmd_read;
+	reg  [ 6:0] cmd_len;
+	reg  [AL:0] cmd_addr;
+	reg         cmd_dual;
+
+	// Validate
+	reg val_ok;
+
+
+	// Buffers
+	// -------
+
+	ram_sdp #(
+		.AWIDTH(8),
+		.DWIDTH(32)
+	) buf_wr_I (
+		.wr_addr(bw_waddr),
+		.wr_data(bw_wdata),
+		.wr_ena(bw_wren),
+		.rd_addr(bw_raddr),
+		.rd_data(bw_rdata),
+		.rd_ena(bw_rden),
+		.clk(clk)
+	);
+
+	ram_sdp #(
+		.AWIDTH(8),
+		.DWIDTH(32)
+	) buf_rd_I (
+		.wr_addr(br_waddr),
+		.wr_data(br_wdata),
+		.wr_ena(br_wren),
+		.rd_addr(br_raddr),
+		.rd_data(br_rdata),
+		.rd_ena(br_rden),
+		.clk(clk)
+	);
+
+
+	// Wishbone interface
+	// ------------------
+
+	// Ack
+	always @(posedge clk)
+		wb_ack_i <= wb_cyc & ~wb_ack_i;
+
+	assign wb_ack = wb_ack_i;
+
+	// Read Mux
+	assign wb_rdata = wb_ack_i ?
+		(wb_addr[8] ? br_rdata : { 30'h00000000, val_ok, mi_ready }) :
+		32'h00000000;
+
+	// Buffer accesses
+	assign bw_waddr = wb_addr[7:0];
+	assign bw_wdata = wb_wdata;
+	assign bw_wren  = wb_ack_i & wb_we & wb_addr[8];
+
+	assign br_raddr = wb_addr[7:0];
+	assign br_rden  = 1'b1;
+
+	// Write Strobes
+	always @(posedge clk)
+		if (wb_ack_i) begin
+			wb_we_cmd  <= 1'b0;
+			wb_we_addr <= 1'b0;
+		end else begin
+			wb_we_cmd  <= wb_cyc & wb_we & ~wb_addr[8] & ~wb_addr[0];
+			wb_we_addr <= wb_cyc & wb_we & ~wb_addr[8] &  wb_addr[0];
+		end
+
+	always @(posedge clk)
+		cmd_start <= wb_we_cmd;
+
+	always @(posedge clk)
+		if (rst)
+			cmd_valid <= 1'b0;
+		else
+			cmd_valid <= (cmd_valid & (~mi_ready | cmd_dual)) | cmd_start;
+
+	always @(posedge clk)
+		if (wb_we_cmd)
+			cmd_dual <= wb_wdata[18];
+		else if (mi_ready & mi_valid)
+			cmd_dual <= 1'b0;
+
+	always @(posedge clk)
+		if (wb_we_cmd) begin
+			cmd_read <= wb_wdata[   16];
+			cmd_len  <= wb_wdata[ 6: 0];
+		end
+
+	always @(posedge clk)
+		if (wb_we_addr)
+			cmd_addr <= wb_wdata[ADDR_WIDTH-1:0];
+		else if (mi_ready & mi_valid)
+			cmd_addr <= cmd_addr + cmd_len + 1;
+
+
+	// Memory interface
+	// ----------------
+
+	// Requests
+	assign mi_addr    = cmd_addr;
+	assign mi_len     = cmd_len;
+	assign mi_rw      = cmd_read;
+	assign mi_valid   = cmd_valid;
+
+	// Write data (and read-validate)
+	always @(posedge clk)
+		if (wb_we_cmd)
+			bw_raddr <= wb_wdata[15:8];
+		else
+			bw_raddr <= bw_raddr + bw_rden;
+
+	assign mi_wdata = bw_rdata;
+	assign mi_wmsk  = 4'h0;
+
+	assign bw_rden = (cmd_read ? mi_rstb : mi_wack) | cmd_start;
+
+	// Read data
+	assign br_wdata = mi_rdata;
+	assign br_wren  = mi_rstb;
+
+	always @(posedge clk)
+		if (wb_we_cmd)
+			br_waddr <= wb_wdata[15:8];
+		else
+			br_waddr <= br_waddr + mi_rstb;
+
+	// Data validation
+	always @(posedge clk)
+		if (wb_we_cmd)
+			val_ok <= val_ok | wb_wdata[17];
+		else
+			val_ok <= val_ok & (~mi_rstb | (mi_rdata == bw_rdata));
+
+endmodule

+ 153 - 0
projects/memtest/rtl/sys_mgr.v

@@ -0,0 +1,153 @@
+/*
+ * sys_mgr.v
+ *
+ * vim: ts=4 sw=4
+ *
+ * Copyright (C) 2020  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 sys_mgr (
+	input  wire [3:0] delay,
+	input  wire clk_in,
+	output wire clk_1x,
+	output wire clk_2x,
+	output wire clk_4x,
+	output wire clk_rd,
+	output wire sync_4x,
+	output wire sync_rd,
+	output wire rst
+);
+
+	wire       pll_lock;
+
+	SB_PLL40_2F_PAD #(
+		.FEEDBACK_PATH("SIMPLE"),
+		.DIVR(4'b0000),
+
+	// 48
+//		.DIVF(7'b0111111),
+//		.DIVQ(3'b100),
+
+	// 96
+//		.DIVF(7'b0111111),
+//		.DIVQ(3'b011),
+
+	// 144
+//		.DIVF(7'b0101111),
+//		.DIVQ(3'b010),
+
+	// 147
+		.DIVF(7'b0110000),
+		.DIVQ(3'b010),
+
+	// 200
+//		.DIVF(7'b1000010),
+//		.DIVQ(3'b010),
+
+		.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({delay, 4'h0}),
+		.PLLOUTGLOBALA(clk_rd),
+		.PLLOUTGLOBALB(clk_4x),
+		.RESETB(1'b1),
+		.LOCK(pll_lock)
+	);
+
+	ice40_serdes_crg #(
+		.NO_CLOCK_2X(0)
+	) crg_I (
+		.clk_4x(clk_4x),
+		.pll_lock(pll_lock),
+		.clk_1x(clk_1x),
+		.clk_2x(clk_2x),
+		.rst(rst)
+	);
+
+`ifdef MEM_spi
+	ice40_serdes_sync #(
+		.PHASE(2),
+		.NEG_EDGE(0),
+`ifdef VIDEO_none
+		.GLOBAL_BUF(0),
+		.LOCAL_BUF(0),
+		.BEL_COL("X22"),
+		.BEL_ROW("Y4"),
+`else
+		.GLOBAL_BUF(0),
+		.LOCAL_BUF(1),
+		.BEL_COL("X15")
+`endif
+	) sync_4x_I (
+		.clk_slow(clk_1x),
+		.clk_fast(clk_4x),
+		.rst(rst),
+		.sync(sync_4x)
+	);
+
+	assign sync_rd = 1'b0;
+`endif
+
+`ifdef MEM_hyperram
+	ice40_serdes_sync #(
+		.PHASE(2),
+		.NEG_EDGE(0),
+		.GLOBAL_BUF(0),
+		.LOCAL_BUF(1),
+		.BEL_COL("X12"),
+		.BEL_ROW("Y15")
+	) sync_4x_I (
+		.clk_slow(clk_1x),
+		.clk_fast(clk_4x),
+		.rst(rst),
+		.sync(sync_4x)
+	);
+
+	ice40_serdes_sync #(
+		.PHASE(2),
+		.NEG_EDGE(0),
+		.GLOBAL_BUF(0),
+		.LOCAL_BUF(1),
+		.BEL_COL("X13"),
+		.BEL_ROW("Y15")
+	) sync_rd_I (
+		.clk_slow(clk_1x),
+		.clk_fast(clk_rd),
+		.rst(rst),
+		.sync(sync_rd)
+	);
+`endif
+
+endmodule

+ 488 - 0
projects/memtest/rtl/top.v

@@ -0,0 +1,488 @@
+/*
+ * top.v
+ *
+ * vim: ts=4 sw=4
+ *
+ * Copyright (C) 2020  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 (
+	// SPI
+`ifdef MEM_spi
+	inout  wire [3:0] spi_io,
+	output wire       spi_sck,
+	output wire [1:0] spi_cs_n,
+`endif
+
+	// HyperRAM
+`ifdef MEM_hyperram
+	inout  wire [7:0] hram_dq,
+	inout  wire       hram_rwds,
+	output wire       hram_ck,
+	output wire [3:0] hram_cs_n,
+	output wire       hram_rst_n,
+`endif
+
+	// HDMI pads
+`ifdef VIDEO_4bpp
+	output wire [ 3:0] hdmi_data,
+`endif
+
+`ifdef VIDEO_12bpp
+	output wire [11:0] hdmi_data,
+`endif
+
+`ifndef VIDEO_none
+	output wire hdmi_hsync,
+	output wire hdmi_vsync,
+	output wire hdmi_de,
+	output wire hdmi_clk,
+`endif
+
+	// UART
+	input  wire uart_rx,
+	output wire uart_tx,
+
+	// Clock (12M)
+	input  wire clk_in
+);
+
+	// Signals
+	// -------
+
+	// Control
+	wire [31:0] aux_csr;
+	wire        dma_run;
+
+	// Wishbone interface
+	reg  [31:0] wb_wdata;
+	wire [95:0] wb_rdata;
+	reg  [15:0] wb_addr;
+	reg         wb_we;
+	reg  [ 2:0] wb_cyc;
+	wire [ 2:0] wb_ack;
+
+	// Memory interface
+	wire [31:0] mi_addr;
+	wire [ 6:0] mi_len;
+	wire        mi_rw;
+	wire        mi_valid;
+	wire        mi_ready;
+
+	wire [31:0] mi_wdata;
+	wire        mi_wack;
+	wire        mi_wlast;
+
+	wire [31:0] mi_rdata;
+	wire        mi_rstb;
+	wire        mi_rlast;
+
+	// Memory interface - Memory Tester
+	wire [31:0] mi0_addr;
+	wire [ 6:0] mi0_len;
+	wire        mi0_rw;
+	wire        mi0_valid;
+	wire        mi0_ready;
+
+	wire [31:0] mi0_wdata;
+	wire        mi0_wack;
+	wire        mi0_wlast;
+
+	wire [31:0] mi0_rdata;
+	wire        mi0_rstb;
+	wire        mi0_rlast;
+
+	// Memory interface - Video DMA
+	wire [31:0] mi1_addr;
+	wire [ 6:0] mi1_len;
+	wire        mi1_rw;
+	wire        mi1_valid;
+	wire        mi1_ready;
+
+	wire [31:0] mi1_wdata;
+	wire        mi1_wack;
+	wire        mi1_wlast;
+
+	wire [31:0] mi1_rdata;
+	wire        mi1_rstb;
+	wire        mi1_rlast;
+
+	// Clock / Reset
+	wire [3:0] clk_rd_delay;
+	wire clk_1x;
+	wire clk_2x;
+	wire clk_4x;
+	wire clk_rd;
+	wire sync_4x;
+	wire sync_rd;
+	wire rst;
+
+
+	// Host interface
+	// --------------
+
+	uart2wb #(
+		.WB_N(3)
+	) if_I (
+		.uart_rx(uart_rx),
+		.uart_tx(uart_tx),
+		.uart_div(8'd16),
+		.wb_wdata(wb_wdata),
+		.wb_rdata(wb_rdata),
+		.wb_addr(wb_addr),
+		.wb_we(wb_we),
+		.wb_cyc(wb_cyc),
+		.wb_ack(wb_ack),
+		.aux_csr(aux_csr),
+		.clk(clk_1x),
+		.rst(rst)
+	);
+
+	assign dma_run = aux_csr[0];
+
+
+	// QSPI Controller
+	// ---------------
+
+`ifdef MEM_spi
+	// Config
+	localparam integer PHY_SPEED = 4;
+	localparam integer PL = (4 * PHY_SPEED) - 1;
+	localparam integer CL = PHY_SPEED - 1;
+
+	// Signals
+	wire [PL:0] phy_io_i;
+	wire [PL:0] phy_io_o;
+	wire [ 3:0] phy_io_oe;
+	wire [CL:0] phy_clk_o;
+	wire [ 1:0] phy_cs_o;
+
+	// Controller
+	qspi_master #(
+		.CMD_READ(16'hEBEB),
+		.CMD_WRITE(16'h0202),
+		.DUMMY_CLK(6),
+		.PAUSE_CLK(8),
+		.FIFO_DEPTH(1),
+		.N_CS(2),
+		.PHY_SPEED(PHY_SPEED),
+		.PHY_WIDTH(1),
+		.PHY_DELAY((PHY_SPEED == 1) ? 2 : ((PHY_SPEED == 2) ? 3 : 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[31:30]),
+		.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),
+		.mi_wack(mi_wack),
+		.mi_wlast(mi_wlast),
+		.mi_rdata(mi_rdata),
+		.mi_rstb(mi_rstb),
+		.mi_rlast(mi_rlast),
+		.wb_wdata(wb_wdata),
+		.wb_rdata(wb_rdata[31: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
+	generate
+		if (PHY_SPEED == 1)
+			qspi_phy_ice40_1x #(
+				.N_CS(2),
+				.WITH_CLK(1),
+				.NEG_IN(0)
+			) 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(clk_1x)
+			);
+
+		else if (PHY_SPEED == 2)
+			qspi_phy_ice40_2x #(
+				.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_2x(clk_2x)
+			);
+
+		else if (PHY_SPEED == 4)
+			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)
+			);
+	endgenerate
+
+	assign clk_rd_delay = 4'h0;
+`endif
+
+
+	// HyperRAM Controller
+	// -------------------
+
+`ifdef MEM_hyperram
+	// Signals
+	wire [ 1:0] phy_ck_en;
+
+	wire [ 3:0] phy_rwds_in;
+	wire [ 3:0] phy_rwds_out;
+	wire [ 1:0] phy_rwds_oe;
+
+	wire [31:0] phy_dq_in;
+	wire [31:0] phy_dq_out;
+	wire [ 1:0] phy_dq_oe;
+
+	wire [ 3:0] phy_cs_n;
+	wire        phy_rst_n;
+
+	wire [ 7:0] phy_cfg_wdata;
+	wire [ 7:0] phy_cfg_rdata;
+	wire        phy_cfg_stb;
+
+	// Controller
+	hram_top hram_ctrl_I (
+		.phy_ck_en(phy_ck_en),
+		.phy_rwds_in(phy_rwds_in),
+		.phy_rwds_out(phy_rwds_out),
+		.phy_rwds_oe(phy_rwds_oe),
+		.phy_dq_in(phy_dq_in),
+		.phy_dq_out(phy_dq_out),
+		.phy_dq_oe(phy_dq_oe),
+		.phy_cs_n(phy_cs_n),
+		.phy_rst_n(phy_rst_n),
+		.phy_cfg_wdata(phy_cfg_wdata),
+		.phy_cfg_rdata(phy_cfg_rdata),
+		.phy_cfg_stb(phy_cfg_stb),
+		.mi_addr_cs(mi_addr[31:30]),
+		.mi_addr({1'b0, mi_addr[29:0], 1'b0}),	/* 32b aligned */
+		.mi_len(mi_len),
+		.mi_rw(mi_rw),
+		.mi_linear(1'b0),
+		.mi_valid(mi_valid),
+		.mi_ready(mi_ready),
+		.mi_wdata(mi_wdata),
+		.mi_wmsk(4'h0),
+		.mi_wack(mi_wack),
+		.mi_rdata(mi_rdata),
+		.mi_rstb(mi_rstb),
+		.wb_wdata(wb_wdata),
+		.wb_rdata(wb_rdata[31:0]),
+		.wb_addr(wb_addr[3:0]),
+		.wb_we(wb_we),
+		.wb_cyc(wb_cyc[0]),
+		.wb_ack(wb_ack[0]),
+		.clk(clk_1x),
+		.rst(rst)
+	);
+
+	// PHY
+	hram_phy_ice40 hram_phy_I (
+		.hram_dq(hram_dq),
+		.hram_rwds(hram_rwds),
+		.hram_ck(hram_ck),
+		.hram_cs_n(hram_cs_n),
+		.hram_rst_n(hram_rst_n),
+		.phy_ck_en(phy_ck_en),
+		.phy_rwds_in(phy_rwds_in),
+		.phy_rwds_out(phy_rwds_out),
+		.phy_rwds_oe(phy_rwds_oe),
+		.phy_dq_in(phy_dq_in),
+		.phy_dq_out(phy_dq_out),
+		.phy_dq_oe(phy_dq_oe),
+		.phy_cs_n(phy_cs_n),
+		.phy_rst_n(phy_rst_n),
+		.phy_cfg_wdata(phy_cfg_wdata),
+		.phy_cfg_rdata(phy_cfg_rdata),
+		.phy_cfg_stb(phy_cfg_stb),
+		.clk_rd_delay(clk_rd_delay),
+		.clk_1x(clk_1x),
+		.clk_4x(clk_4x),
+		.clk_rd(clk_rd),
+		.sync_4x(sync_4x),
+		.sync_rd(sync_rd)
+	);
+`endif
+
+
+	// Memory tester
+	// -------------
+
+	memtest #(
+		.ADDR_WIDTH(32)
+	) memtest_I (
+		.mi_addr(mi0_addr),
+		.mi_len(mi0_len),
+		.mi_rw(mi0_rw),
+		.mi_valid(mi0_valid),
+		.mi_ready(mi0_ready),
+		.mi_wdata(mi0_wdata),
+		.mi_wack(mi0_wack),
+		.mi_rdata(mi0_rdata),
+		.mi_rstb(mi0_rstb),
+		.wb_wdata(wb_wdata),
+		.wb_rdata(wb_rdata[63:32]),
+		.wb_addr(wb_addr[8:0]),
+		.wb_we(wb_we),
+		.wb_cyc(wb_cyc[1]),
+		.wb_ack(wb_ack[1]),
+		.clk(clk_1x),
+		.rst(rst)
+	);
+
+
+	// Memory Mux
+	// ----------
+
+	assign mi_addr    = dma_run ? mi1_addr    : mi0_addr;
+	assign mi_len     = dma_run ? mi1_len     : mi0_len;
+	assign mi_rw      = dma_run ? mi1_rw      : mi0_rw;
+	assign mi_valid   = dma_run ? mi1_valid   : mi0_valid;
+	assign mi0_ready  = mi_ready & ~dma_run;
+	assign mi1_ready  = mi_ready &  dma_run;
+
+	assign mi_wdata  = dma_run ? mi1_wdata : mi0_wdata;
+	assign mi0_wack  = mi_wack & ~dma_run;
+	assign mi0_wlast = mi_wlast;
+	assign mi1_wack  = mi_wack &  dma_run;
+	assign mi1_wlast = mi_wlast;
+
+	assign mi0_rdata = mi_rdata;
+	assign mi0_rstb  = mi_rstb & ~dma_run;
+	assign mi0_rlast = mi_rlast;
+	assign mi1_rdata = mi_rdata;
+	assign mi1_rstb  = mi_rstb &  dma_run;
+	assign mi1_rlast = mi_rlast;
+
+
+	// HDMI output
+	// -----------
+
+`ifndef VIDEO_none
+	hdmi_out #(
+`ifdef VIDEO_4bpp
+		.DW(4)
+`endif
+`ifdef VIDEO_12bpp
+		.DW(12)
+`endif
+	) hdmi_I (
+		.hdmi_data(hdmi_data),
+		.hdmi_hsync(hdmi_hsync),
+		.hdmi_vsync(hdmi_vsync),
+		.hdmi_de(hdmi_de),
+		.hdmi_clk(hdmi_clk),
+		.wb_wdata(wb_wdata),
+		.wb_rdata(wb_rdata[95:64]),
+		.wb_addr(wb_addr[6:0]),
+		.wb_we(wb_we),
+		.wb_cyc(wb_cyc[2]),
+		.wb_ack(wb_ack[2]),
+		.mi_addr(mi1_addr),
+		.mi_len(mi1_len),
+		.mi_rw(mi1_rw),
+		.mi_valid(mi1_valid),
+		.mi_ready(mi1_ready),
+		.mi_wdata(mi1_wdata),
+		.mi_wack(mi1_wack),
+		.mi_rdata(mi1_rdata),
+		.mi_rstb(mi1_rstb),
+		.clk_1x(clk_1x),
+		.clk_4x(clk_4x),
+		.sync_4x(sync_4x),
+		.rst(rst)
+	);
+`else
+	// Dummy wishbone
+	assign wb_ack[2]       = wb_cyc[2];
+	assign wb_rdata[95:64] = 32'h00000000;
+
+	// Dummy mem-if
+	assign mi1_addr  = 32'hxxxxxxxx;
+	assign mi1_len   = 7'hxx;
+	assign mi1_rw    = 1'bx;
+	assign mi1_valid = 1'b0;
+	assign mi1_wdata = 32'hxxxxxxxx;
+`endif
+
+
+	// Clock / Reset
+	// -------------
+
+	sys_mgr sys_mgr_I (
+		.delay(clk_rd_delay),
+		.clk_in(clk_in),
+		.clk_1x(clk_1x),
+		.clk_2x(clk_2x),
+		.clk_4x(clk_4x),
+		.clk_rd(clk_rd),
+		.sync_4x(sync_4x),
+		.sync_rd(sync_rd),
+		.rst(rst)
+	);
+
+endmodule

+ 233 - 0
projects/memtest/rtl/uart2wb.v

@@ -0,0 +1,233 @@
+/*
+ * uart2wb.v
+ *
+ * vim: ts=4 sw=4
+ *
+ * Copyright (C) 2020  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 uart2wb #(
+	parameter integer WB_N = 3,
+
+	// auto
+	parameter integer DL = (32*WB_N)-1,
+	parameter integer CL = WB_N-1
+)(
+	// UART
+	input  wire        uart_rx,
+	output wire        uart_tx,
+
+	input  wire [ 7:0] uart_div,
+
+	// Wishbone
+	output reg  [31:0] wb_wdata,
+	input  wire [DL:0] wb_rdata,
+	output reg  [15:0] wb_addr,
+	output reg         wb_we,
+	output reg  [CL:0] wb_cyc,
+	input  wire	[CL:0] wb_ack,
+
+	// Aux-CSR
+	output reg  [31:0] aux_csr,
+
+	// Clock / Reset
+	input  wire clk,
+	input  wire rst
+);
+
+	localparam
+		CMD_SYNC        = 4'h0,
+		CMD_REG_ACCESS  = 4'h1,
+		CMD_DATA_SET    = 4'h2,
+		CMD_DATA_GET    = 4'h3,
+		CMD_AUX_CSR     = 4'h4;
+
+
+	// Signals
+	// -------
+
+	// UART serdes
+	wire [7:0] rx_data;
+	wire rx_stb;
+
+	wire [7:0] tx_data;
+	wire tx_ack;
+	wire tx_valid;
+
+	// Command RX
+	reg  [39:0] rx_reg;
+	reg  [ 2:0] rx_cnt;
+
+	wire [ 3:0] cmd_code;
+	wire [31:0] cmd_data;
+	reg         cmd_stb;
+
+	// Response TX
+	reg  [31:0] tx_reg;
+	reg  [ 2:0] tx_cnt;
+
+	reg  [31:0] resp_data;
+	reg         resp_ld;
+
+	// Wishbone interface
+	reg  [31:0] wb_rdata_i;
+	wire		wb_ack_i;
+
+
+	// Host interface
+	// --------------
+
+	// UART module
+	uart_rx #(
+		.DIV_WIDTH(8),
+		.GLITCH_FILTER(0)
+	) rx_I (
+		.rx(uart_rx),
+		.data(rx_data),
+		.stb(rx_stb),
+		.div(uart_div),
+		.clk(clk),
+		.rst(rst)
+	);
+
+	uart_tx #(
+		.DIV_WIDTH(8)
+	) tx_I (
+		.tx(uart_tx),
+		.data(tx_data),
+		.valid(tx_valid),
+		.ack(tx_ack),
+		.div(uart_div),
+		.clk(clk),
+		.rst(rst)
+	);
+
+	// Command input
+	always @(posedge clk or posedge rst)
+		if (rst)
+			rx_cnt <= 3'd0;
+		else if (rx_stb)
+			rx_cnt <= rx_cnt[2] ? 3'd0 : (rx_cnt + 1);
+
+	always @(posedge clk)
+		if (rx_stb)
+			rx_reg <= { rx_reg[31:0], rx_data };
+
+	assign cmd_code = rx_reg[39:36];
+	assign cmd_data = rx_reg[31: 0];
+
+	always @(posedge clk)
+		cmd_stb <= rx_cnt[2] & rx_stb;
+
+    // Response output
+	always @(posedge clk or posedge rst)
+		if (rst)
+			tx_cnt <= 3'd0;
+		else begin
+			if (resp_ld)
+				tx_cnt <= 3'd4;
+			else if (tx_ack)
+				tx_cnt <= tx_cnt - 1;
+		end
+
+	always @(posedge clk)
+		if (resp_ld)
+			tx_reg <= resp_data;
+		else if (tx_ack)
+			tx_reg <= { tx_reg[23:0], 8'h00 };
+
+	assign tx_data  = tx_reg[31:24];
+	assign tx_valid = |tx_cnt;
+
+	// Commands
+	always @(posedge clk)
+	begin
+		// Defaults
+		resp_ld   <= 1'b0;
+		resp_data <= 40'hxxxxxxxxxx;
+
+		// Commands
+		if (cmd_stb) begin
+			case (cmd_code)
+				CMD_SYNC: begin
+					resp_data <= 432'hcafebabe;
+					resp_ld   <= 1'b1;
+				end
+
+				CMD_REG_ACCESS: begin
+					wb_addr  <=  cmd_data[15:0];
+					wb_we    <= ~cmd_data[20];
+					wb_cyc   <= (1 << cmd_data[19:16]);
+				end
+
+				CMD_DATA_SET: begin
+					wb_wdata <= cmd_data;
+				end
+
+				CMD_DATA_GET: begin
+					resp_ld   <= 1'b1;
+					resp_data <= wb_wdata;
+				end
+
+				CMD_AUX_CSR: begin
+				    aux_csr <= cmd_data;
+				end
+			endcase
+		end
+
+		if (wb_ack_i) begin
+			// Cycle done
+			wb_cyc <= 0;
+
+			// Capture read response
+			if (~wb_we)
+				wb_wdata <= wb_rdata_i;
+		end
+
+		if (rst) begin
+			wb_cyc   <= 0;
+			aux_csr  <= 32'h00000000;
+		end
+	end
+
+	// Wishbone multi-slave handling
+	assign wb_ack_i = |wb_ack;
+
+	always @(*)
+	begin : rdata
+		integer i;
+
+		wb_rdata_i = 32'h00000000;
+
+		for (i=0; i<WB_N; i=i+1)
+			wb_rdata_i = wb_rdata_i | wb_rdata[32*i+:32];
+	end
+
+endmodule

+ 74 - 0
projects/memtest/sw/memtest-hyperram.py

@@ -0,0 +1,74 @@
+#!/usr/bin/python3
+
+import sys
+
+from memtest import WishboneInterface, MemoryTester, HDMIOutput
+from memtest import HyperRAMController
+
+
+# ----------------------------------------------------------------------------
+# Main
+# ----------------------------------------------------------------------------
+
+def RAM_ADDR_CS(cs, addr):
+	return (cs << 30) | addr
+
+
+def main(argv0, port='/dev/ttyUSB1', filename=None):
+	# Connect to board
+	wb = WishboneInterface(port)
+
+	# Devices on the bus
+	hyperram = HyperRAMController(wb, 0x00000)
+	memtest  = MemoryTester(wb, 0x10000)
+	hdmi     = HDMIOutput(wb, 0x20000)
+
+	# Make sure to disable DMA
+	hdmi.disable()
+	wb.aux_csr(0)
+
+	# Initialize HyperRAM core
+	if hyperram.init() is False:
+		print("[!] Init failed")
+		return -1
+
+	hyperram.set_runtime(True)
+
+	# What mode ?
+	if filename is None:
+		# Run memtest
+		for cs in range(4):
+			if not (hyperram.csm & (1 << cs)):
+				continue
+
+			print("[+] Testing CS=%d" % cs)
+			good = memtest.run(RAM_ADDR_CS(cs, 0), 1<<21)
+			if good:
+				print("[.]  All good !")
+			else:
+				print("[!]  Errors found !")
+
+	else:
+		# Load data file
+		print("[+] Uploading image data")
+
+		img = open(filename, 'rb').read()
+		img = bytearray([(a << 4) | b for a, b in zip(img[0::2], img[1::2])])
+		memtest.load_data(RAM_ADDR_CS(3, 0), img)
+
+		# Palette (1:1)
+		print("[+] Uploading palette")
+		for i in range(4*16):
+			hdmi.pal_write(i, i&15)
+
+		# Start DMA
+		print("[+] Starting DMA")
+		wb.aux_csr(1)
+		hdmi.enable(RAM_ADDR_CS(3, 0), 16)
+
+	# Done
+	return 0
+
+
+if __name__ == '__main__':
+	sys.exit(main(*sys.argv) or 0)

+ 114 - 0
projects/memtest/sw/memtest-spi.py

@@ -0,0 +1,114 @@
+#!/usr/bin/python3
+
+import binascii
+import random
+import sys
+
+from memtest import WishboneInterface, MemoryTester, HDMIOutput
+from memtest import QSPIController
+
+
+# ----------------------------------------------------------------------------
+# Main
+# ----------------------------------------------------------------------------
+
+def hexdump(x):
+	return binascii.b2a_hex(x).decode('utf-8')
+
+def RAM_ADDR_CS(cs, addr):
+	return (cs << 30) | addr
+
+
+def main(argv0, port='/dev/ttyUSB1', filename=None):
+	# Connect to board
+	wb = WishboneInterface(port)
+
+	# Devices on the bus
+	flash    = QSPIController(wb, 0x00000, cs=0)
+	psram    = QSPIController(wb, 0x00000, cs=1)
+	memtest  = MemoryTester(wb, 0x10000)
+	hdmi     = HDMIOutput(wb, 0x20000)
+
+	# Make sure to disable DMA
+	hdmi.disable()
+	wb.aux_csr(0)
+
+	# Read chip IDs
+	print("[+] ID read")
+	print(" Flash: " + hexdump(flash.spi_xfer(b'\x9f', rx_len=3)))
+	print(" PSRAM: " + hexdump(psram.spi_xfer(b'\x9f', dummy_len=3, rx_len=8)))
+
+	# Enable PSRAM QPI
+	psram.spi_xfer(b'\x35')
+
+	# Manual page read/write test
+	if True:
+		print("[+] Manual page read/write test")
+
+		# Write a random page
+		data = bytes([random.randint(0,255) for i in range(256)])
+		psram.qpi_xfer(b'\x02\x01\x00\x00', data)
+
+		# Read it back
+		rdata = psram.qpi_xfer(b'\xeb\x01\x00\x00', dummy_len=3, rx_len=256)
+
+		# Results
+		if data != rdata:
+			print("[!] Failed")
+			print(" Orig: " + hexdump(data))
+			print(" Read: " + hexdump(rdata))
+			print(" Diff: " + hexdump(bytes([a^b for a,b in zip(data,rdata) ])))
+		else:
+			print("[.] OK")
+
+	# What mode ?
+	if filename is None:
+		# Run memtest on PSRAM
+		print("[+] Testing PSRAM")
+		good = memtest.run(RAM_ADDR_CS(1, 0), 1<<21)
+		if good:
+			print("[.]  All good !")
+		else:
+			print("[!]  Errors found !")
+
+		# Disable QPI
+		psram.qpi_xfer(b'\xf5')
+
+	else:
+		# Load data file
+		print("[+] Uploading image data")
+
+		img = open(filename, 'rb').read()
+		img = bytearray([(a << 4) | b for a, b in zip(img[0::2], img[1::2])])
+		memtest.load_data(RAM_ADDR_CS(1, 0), img)
+
+		print("[+] Uploading palette")
+		try:
+			# Palette data from file
+			def to_col(d):
+				return (
+					(((d[2] + 0x08) >> 4) << 8) |
+					(((d[1] + 0x08) >> 4) << 4) |
+					(((d[0] + 0x08) >> 4) << 0) |
+					0
+				)
+			with open(filename + '.pal', 'rb') as fh:
+				pal = [to_col(fh.read(3)) for i in range(16)]
+			for i in range(4*16):
+				hdmi.pal_write(i, pal[i&15])
+		except:
+			# 1:1 palette
+			for i in range(4*16):
+				hdmi.pal_write(i, i&15)
+
+		# Start DMA
+		print("[+] Starting DMA")
+		wb.aux_csr(1)
+		hdmi.enable(RAM_ADDR_CS(1, 0), 64)
+
+	# Done
+	return 0
+
+
+if __name__ == '__main__':
+	sys.exit(main(*sys.argv) or 0)

+ 790 - 0
projects/memtest/sw/memtest.py

@@ -0,0 +1,790 @@
+#!/usr/bin/python3
+
+import binascii
+import random
+import serial
+import sys
+
+
+# ----------------------------------------------------------------------------
+# Serial commands
+# ----------------------------------------------------------------------------
+
+class WishboneInterface(object):
+
+	COMMANDS = {
+		'SYNC' : 0,
+		'REG_ACCESS' : 1,
+		'DATA_SET' : 2,
+		'DATA_GET' : 3,
+		'AUX_CSR' : 4,
+	}
+
+	def __init__(self, port):
+		self.ser = ser = serial.Serial()
+		ser.port = port
+		ser.baudrate = 2000000
+		ser.stopbits = 2
+		ser.timeout = 0.1
+		ser.open()
+
+		if not self.sync():
+			raise RuntimeError("Unable to sync")
+
+	def sync(self):
+		for i in range(10):
+			self.ser.write(b'\x00')
+			d = self.ser.read(4)
+			if (len(d) == 4) and (d == b'\xca\xfe\xba\xbe'):
+				return True
+		return False
+
+	def write(self, addr, data):
+		cmd_a = ((self.COMMANDS['DATA_SET']   << 36) | data).to_bytes(5, 'big')
+		cmd_b = ((self.COMMANDS['REG_ACCESS'] << 36) | addr).to_bytes(5, 'big')
+		self.ser.write(cmd_a + cmd_b)
+
+	def read(self, addr):
+		cmd_a = ((self.COMMANDS['REG_ACCESS'] << 36) | (1<<20) | addr).to_bytes(5, 'big')
+		cmd_b = ((self.COMMANDS['DATA_GET']   << 36)).to_bytes(5, 'big')
+		self.ser.write(cmd_a + cmd_b)
+		d = self.ser.read(4)
+		if len(d) != 4:
+			raise RuntimeError('Comm error')
+		return int.from_bytes(d, 'big')
+
+	def aux_csr(self, value):
+		cmd = ((self.COMMANDS['AUX_CSR'] << 36) | value).to_bytes(5, 'big')
+		self.ser.write(cmd)
+
+
+# ----------------------------------------------------------------------------
+# QSPI controller
+# ----------------------------------------------------------------------------
+
+class QSPIController(object):
+
+	CORE_REGS = {
+		'csr': 0,
+		'rf': 3,
+	}
+
+	def __init__(self, intf, base, cs=0):
+		self.intf = intf
+		self.base = base
+		self.cs = cs
+		self._end()
+
+	def _write(self, reg, val):
+		self.intf.write(self.base + self.CORE_REGS.get(reg, reg), val)
+
+	def _read(self, reg):
+		return self.intf.read(self.base + self.CORE_REGS.get(reg, reg))
+
+	def _begin(self):
+		# Request external control
+		self._write('csr', 0x00000004 | (self.cs << 4))
+		self._write('csr', 0x00000002 | (self.cs << 4))
+
+	def _end(self):
+		# Release external control
+		self._write('csr', 0x00000004)
+
+	def spi_xfer(self, tx_data, dummy_len=0, rx_len=0):
+		# Start transaction
+		self._begin()
+
+		# Total length
+		l = len(tx_data) + rx_len + dummy_len
+
+		# Prep buffers
+		tx_data = tx_data + bytes( ((l + 3) & ~3) - len(tx_data) )
+		rx_data = b''
+
+		# Run
+		while l > 0:
+			# Word and command
+			w = int.from_bytes(tx_data[0:4], 'big')
+			c = 0x13 if l >= 4 else (0x10 + l - 1)
+			s = 0 if l >= 4 else 8*(4-l)
+
+			# Issue
+			self._write(c, w);
+			w = self._read('rf')
+
+			# Get RX
+			rx_data = rx_data + ((w << s) & 0xffffffff).to_bytes(4, 'big')
+
+			# Next
+			l = l - 4
+			tx_data = tx_data[4:]
+
+		# End transaction
+		self._end()
+
+		# Return interesting part
+		return rx_data[-rx_len:]
+
+
+	def _qpi_tx(self, data, command=False):
+		while len(data):
+			# Base command
+			cmd = 0x1c if command else 0x18
+
+			# Grab chunk
+			word = data[0:4]
+			data = data[4:]
+
+			cmd |= len(word) - 1
+			word = word + bytes(-len(word) & 3)
+
+			# Transmit
+			self._write(cmd, int.from_bytes(word, 'big'));
+
+	def _qpi_rx(self, l):
+		data = b''
+
+		while l > 0:
+			# Issue read
+			wl = 4 if l >= 4 else l
+			cmd = 0x14 | (wl-1)
+			self._write(cmd, 0)
+			word = self._read('rf')
+
+			# Accumulate
+			data = data + (word & (0xffffffff >> (8*(4-wl)))).to_bytes(wl, 'big')
+
+			# Next
+			l = l - 4
+
+		return data
+
+	def qpi_xfer(self, cmd=b'', payload=b'', dummy_len=0, rx_len=0):
+		# Start transaction
+		self._begin()
+
+		# TX command
+		if cmd:
+			self._qpi_tx(cmd, True)
+
+		# TX payload
+		if payload:
+			self._qpi_tx(payload, False)
+
+		# Dummy
+		if dummy_len:
+			self._qpi_rx(dummy_len)
+
+		# RX payload
+		if rx_len:
+			rv = self._qpi_rx(rx_len)
+		else:
+			rv = None
+
+		# End transaction
+		self._end()
+
+		return rv
+
+
+# ----------------------------------------------------------------------------
+# HyperRAM controller
+# ----------------------------------------------------------------------------
+
+class HyperRAMController(object):
+
+	CORE_REGS = {
+		'csr': 0,
+		'cmd': 1,
+		'wq0': 2,
+		'wq1': 3,
+	}
+
+	CSR_RUN			= (1 << 0)
+	CSR_RESET		= (1 << 1)
+	CSR_IDLE_CFG	= (1 << 2)
+	CSR_IDLE_RUN	= (1 << 3)
+	CSR_CMD_LAT		= lambda self, x: ((x-1) & 15) <<  8
+	CSR_CAP_LAT 	= lambda self, x: ((x-1) & 15) << 12
+	CSR_PHY_DELAY	= lambda self, x: (x & 15) <<  16
+	CSR_PHY_PHASE	= lambda self, x: (x &  3) <<  20
+	CSR_PHY_EDGE    = lambda self, x: (x &  1) <<  22
+
+	CMD_LEN			= lambda self, x: ((x-1) & 15) << 8
+	CMD_LAT			= lambda self, x: ((x-1) & 15) << 4
+	CMD_CS			= lambda self, x: (x &  3) << 2
+	CMD_REG			= (1 << 1)
+	CMD_MEM			= (0 << 1)
+	CMD_READ		= (1 << 0)
+	CMD_WRITE		= (0 << 0)
+
+		# Selected so:
+		#  - each byte is unique
+		#  - ORing  all bytes in a single word == 255
+		#  - ANDing all bytes in a single word == 0
+	CAL_WORDS = [ 0x600dbabe, 0xb16b00b5 ]
+
+		# Register addresses
+	HYPERRAM_REGS = {
+		'id0': 0,
+		'id1': 1,
+		'cr0': 0 | (1 << 11),
+		'cr1': 1 | (1 << 11),
+	}
+
+	def __init__(self, intf, base, latency=3, csm=0xf, burst_len=128):
+		self.intf = intf
+		self.base = base
+
+		self.latency = latency
+		self.csm = csm
+		self.burst_len = burst_len
+
+		# We're always in 2x latency mode, also the location where
+		# latency start and the 1 cycle added by the core because it works
+		# 32 bit at a time means we can remove 2 cycles of the latency
+		self._cmd_latency = (2 * latency - 2) // 2
+
+	def _write(self, reg, val):
+		self.intf.write(self.base + self.CORE_REGS[reg], val)
+
+	def _read(self, reg):
+		return self.intf.read(self.base + self.CORE_REGS[reg])
+
+	def _cr0(self, dpd=False, drive_strength=None, latency=6, fixed_latency=True, hybrid_burst=True, burst_len=32):
+		DRIVE = {
+			None: 0,
+			115: 1,
+			67: 2,
+			46: 3,
+			34: 4,
+			27: 5,
+			22: 6,
+			19: 7,
+		}
+		LATENCY = {
+			3: 14,
+			4: 15,
+			5: 0,
+			6: 1,
+		}
+		BURST_LEN = {
+			128: 0,
+			 64: 1,
+			 32: 3,
+			 16: 2,
+		}
+		return (
+			((dpd ^ 1) << 15) |
+			(DRIVE[drive_strength] << 12) |
+			(0xf << 8) |
+			(LATENCY[latency] << 4) |
+			(fixed_latency << 3) |
+			(hybrid_burst << 2) |
+			(BURST_LEN[burst_len] << 0)
+		)
+
+	def _cr1(self, dri=None):
+		DRI = {
+			None: 2,
+			"1x": 2 ,
+			"1.5x": 3,
+			"2x": 0,
+			"4x": 1,
+		}
+		return DRI[dri]
+
+	def _ca(self, addr, rwn=0, reg=0, linear=0):
+		return (
+			(rwn << 47) |
+			(reg << 46) |
+			((linear | reg) << 45) |
+			((addr >> 3) << 16) |
+			((addr & 7) << 0)
+		)
+
+	def _wait_idle(self):
+		# Wait until it's in IDLE Config mode
+		for i in range(10):
+			if self._read('csr') & self.CSR_IDLE_CFG:
+				break
+		else:
+			raise RuntimeError('HyperRAM controller timeout')
+
+	def _reg_write(self, cs, reg, val):
+		ca = self._ca(self.HYPERRAM_REGS[reg], rwn=0, reg=1)
+
+		self._write('wq1', 0x30)
+		self._write('wq0', ca >> 16)
+		self._write('wq0', ((ca & 0xffff) << 16) | val)
+		self._write('wq0', 0)
+
+		self._write('cmd',
+			self.CMD_CS(cs) |
+			self.CMD_REG |
+			self.CMD_WRITE
+		)
+
+		self._wait_idle()
+
+	def _reg_read(self, cs, reg):
+		ca = self._ca(self.HYPERRAM_REGS[reg], rwn=1, reg=1)
+
+		self._write('wq1', 0x30)
+		self._write('wq0', ca >> 16)
+
+		self._write('wq1', 0x20)
+		self._write('wq0', (ca & 0xffff) << 16)
+
+		self._write('wq1', 0x00)
+		self._write('wq0', 0)
+
+		self._write('cmd',
+			self.CMD_LAT(self._cmd_latency) |
+			self.CMD_CS(cs) |
+			self.CMD_REG |
+			self.CMD_READ
+		)
+
+		self._wait_idle()
+
+		rv = []
+		for i in range(3):
+			w1 = self._read('wq1')
+			w0 = self._read('wq0')
+			rv.append( (w0, w1) )
+
+		return rv[-1][0] >> 16
+
+	def _mem_write(self, cs, addr, val, count=1, mask=0x0):
+		ca = self._ca(addr, rwn=0, reg=0)
+
+		self._write('wq1', 0x30)
+		self._write('wq0', ca >> 16)
+
+		self._write('wq1', 0x20)
+		self._write('wq0', (ca & 0xffff) << 16)
+
+		self._write('wq1', 0x30 | mask)
+		self._write('wq0', val)
+
+		self._write('cmd',
+			self.CMD_LEN(count) |
+			self.CMD_LAT(self._cmd_latency) |
+			self.CMD_CS(cs) |
+			self.CMD_MEM |
+			self.CMD_WRITE
+		)
+
+		self._wait_idle()
+
+	def _mem_read(self, cs, addr, count=3):
+		if count > 3:
+			raise ValueError('Unable to read more than 3 words at a time')
+
+		ca = self._ca(addr, rwn=1, reg=0)
+
+		self._write('wq1', 0x30)
+		self._write('wq0', ca >> 16)
+
+		self._write('wq1', 0x20)
+		self._write('wq0', (ca & 0xffff) << 16)
+
+		self._write('wq1', 0x00)
+		self._write('wq0', 0)
+
+		self._write('cmd',
+			self.CMD_LEN(count) |
+			self.CMD_LAT(self._cmd_latency) |
+			self.CMD_CS(cs) |
+			self.CMD_MEM |
+			self.CMD_READ
+		)
+
+		self._wait_idle()
+
+		rv = []
+		for i in range(3):
+			w1 = self._read('wq1')
+			w0 = self._read('wq0')
+			rv.append( (w0, w1) )
+
+		return rv[-count:]
+
+	def _train_check_edge_delay(self, cs, edge, delay):
+		# Configure for base capture latency and phase
+		self._write('csr',
+			self.CSR_PHY_EDGE(edge) |
+			self.CSR_PHY_PHASE(0) |
+			self.CSR_PHY_DELAY(delay) |
+			self.CSR_CMD_LAT(self._cmd_latency) |
+			self.CSR_CAP_LAT(3)
+		)
+
+		# Find the capture latency and phase
+		data = self._mem_read(cs, 0, count=3)
+
+		for w,a in data:
+			print(f"{bin(a)} {w:08x}")
+
+		for i in range(3):
+			if (data[i][1] & 0xf):
+				break
+		else:
+			return None
+
+		for j in range(4):
+			if data[i][1] & (8 >> j):
+				break
+
+		cap_latency = 3 + i + (j > 0)
+		phase = (4 - j) % 4
+
+		# Re-configure core
+		self._write('csr',
+			self.CSR_PHY_EDGE(edge) |
+			self.CSR_PHY_PHASE(phase) |
+			self.CSR_PHY_DELAY(delay) |
+			self.CSR_CMD_LAT(self._cmd_latency) |
+			self.CSR_CAP_LAT(cap_latency)
+		)
+
+		# Confirm data
+		data = self._mem_read(cs, 0, count=3)
+
+		ref = [
+			(self.CAL_WORDS[0], 0x3a),
+			(self.CAL_WORDS[1], 0x3a),
+			(self.CAL_WORDS[0], 0x3a),
+		]
+
+		if data != ref:
+			return None
+
+		return (cap_latency, phase)
+
+	def _train_consolidate(self, train):
+		# Checks combination valid for all chips
+		rv = {}
+
+		for delay, results in train.items():
+			r = [v for k,v in results.items() if self.csm & (1 << k)]
+			for x in r:
+				if (x is None) or (x != r[0]):
+					print("[.]  delay=%2d -> Invalid" % delay)
+					rv[delay] = None
+					break
+			else:
+				print("[.]  delay=%2d -> cap_latency=%d, phase=%d" % (delay, *r[0]))
+				rv[delay] = r[0]
+
+		return rv
+
+	def _train_group(self, train):
+		groups = []
+
+		c_v = None
+		c_d = []
+		c_first = False
+		c_last = False
+
+		for idx, (delay, result) in enumerate(sorted(train.items())):
+			# First / Last checks
+			is_first = idx == 0
+			is_last  = idx == (len(train) - 1)
+
+			# Continue ?
+			if result and (c_v == result):
+				c_d.append(delay)
+				c_first |= is_first
+				c_last  |= is_last
+
+			# Or not ...
+			else:
+				# Flush current
+				if c_v is not None:
+					groups.append( (c_v, c_d, c_first, c_last) )
+
+				# New item
+				c_v     = result
+				c_d     = [ delay ]
+				c_first = is_first
+				c_last  = is_last
+
+		if c_v is not None:
+			groups.append( (c_v, c_d, c_first, c_last) )
+
+		return groups
+
+	def _train_pick_params(self, best):
+		# Pick delay
+		if best[2] and best[3]:
+			d = (best[1][0] + best[1][-1]) // 2
+		elif best[2]:
+			d = min(best[1])
+		elif best[3]:
+			d = max(best[1])
+		else:
+			d = int(round(sum(best[1]) / len(best[1])))
+
+		# If the group is only a single value 'wide', print warning it might be marginal
+		if len(best[1]) == 1:
+			print("[w] Training results might be marginal. Consider switching capture clock phase by 90 deg")
+
+		# Return delay and params
+		return d, best[0][0], best[0][1]
+
+	def init(self):
+		# Reset HyperRAM and controller
+		self._write('csr', self.CSR_RESET)
+		self._wait_idle()
+		self._write('csr', 0)
+		self._wait_idle()
+
+		# Chip config
+		self.cr0 = self._cr0(latency=self.latency, burst_len=self.burst_len)
+		self.cr1 = self._cr1()
+
+		# DEBUG
+		if False:
+			cs = 0
+
+			for i in range(5):
+				print(hex(self.cr0))
+				self._reg_write(cs, 'cr0', self.cr0)
+				self._mem_write(cs, 0, self.CAL_WORDS[0], count=3)
+				self._mem_write(cs, 2, self.CAL_WORDS[1], count=1)
+
+				self._write('csr',
+					self.CSR_PHY_EDGE(1) |
+					self.CSR_PHY_PHASE(0) |
+					self.CSR_PHY_DELAY(0) |
+					self.CSR_CMD_LAT(self._cmd_latency) |
+					self.CSR_CAP_LAT(3)
+				)
+				print(f"{self._read('csr'):08x}")
+
+				for w,a in self._mem_read(cs, 0, count=3):
+					print(f"{bin(a)} {w:08x}")
+
+			return False
+
+		# Execute configuration and training on all chips
+		edge = 1
+		train = {}
+
+		for cs in range(4):
+			if not self.csm & (1 << cs):
+				continue
+
+			# Debug
+			print("[+] Training CS=%d" % cs)
+
+			# CR write
+			self._reg_write(cs, 'cr0', self.cr0)
+			self._reg_write(cs, 'cr1', self.cr1)
+
+			# Write the calibration words
+			self._mem_write(cs, 0, self.CAL_WORDS[0], count=3)
+			self._mem_write(cs, 2, self.CAL_WORDS[1], count=1)
+
+			# Scan delays
+			any_valid = False
+
+			for delay in [0, 5, 10, 15]:
+				d = self._train_check_edge_delay(cs, edge, delay)
+				print("[.]  delay=%2d -> %s" % (delay, "Failed" if (d is None) else ("cap_latency=%d, phase=%d" % d)))
+				train.setdefault(delay, {})[cs] = d
+				any_valid |= d is not None
+
+			# If nothing valid found, assume chip is missing
+			if not any_valid:
+				print("[w]  No working delay found, assuming chip is missing: disabling it !")
+				self.csm &= ~(1 << cs)
+
+		# Are any chips still enabled ?
+		if not self.csm:
+			print("[!] All chips disabled, somethins is wrong ...")
+			return False
+
+		# Find the best combination
+		print("[+] Compiling training results")
+
+			# Check what works for all chips
+		train = self._train_consolidate(train)
+
+		if not any(train.values()):
+			print("[!] Unable to find single valid combination for all chips :(")
+			return False
+
+			# Group them
+		groups = self._train_group(train)
+
+			# Pick best group
+		best = sorted(groups, key=lambda x: len(x[1]) + 2 * (x[2] + x[3]), reverse=True)[0]
+
+			# Select delay
+		self._delay, self._cap_latency, self._phase = self._train_pick_params(best)
+
+		# Load final configuration
+		print("[+] Core configured for cmd_latency=%d, capture_latency=%d, phase=%d, delay=%d" % (
+			self._cmd_latency, self._cap_latency, self._phase, self._delay
+		))
+
+		self._csr = (
+			self.CSR_PHY_EDGE(edge) |
+			self.CSR_PHY_PHASE(self._phase) |
+			self.CSR_PHY_DELAY(self._delay) |
+			self.CSR_CMD_LAT(self._cmd_latency) |
+			self.CSR_CAP_LAT(self._cap_latency)
+		)
+		self._write('csr', self._csr)
+
+		# Success
+		return True
+
+	def set_runtime(self, runtime):
+		self._write('csr', self._csr | (self.CSR_RUN if runtime else 0))
+
+
+# ----------------------------------------------------------------------------
+# Memory tester
+# ----------------------------------------------------------------------------
+
+class MemoryTester(object):
+
+	CORE_REGS = {
+		'cmd': 0,
+		'addr': 1,
+	}
+
+	CMD_DUAL		= 1 << 18
+	CMD_CHECK_RST	= 1 << 17
+	CMD_READ		= 1 << 16
+	CMD_WRITE		= 0 << 16
+	CMD_BUF_ADDR	= lambda self, addr: addr << 8
+	CMD_LEN			= lambda self, l: (l-1) << 0
+
+	def __init__(self, intf, base):
+		self.intf = intf
+		self.base = base
+
+	def _write(self, reg, val):
+		self.intf.write(self.base + self.CORE_REGS[reg], val)
+
+	def _read(self, reg):
+		return self.intf.read(self.base + self.CORE_REGS[reg])
+
+	def ram_write(self, addr, val):
+		self.intf.write(self.base + 0x100 + addr, val)
+
+	def ram_read(self, addr):
+		return self.intf.read(self.base + 0x100 + addr)
+
+	def cmd_write(self, ram_addr, buf_addr, xfer_len):
+		self._write('addr', ram_addr)
+		self._write('cmd',
+			self.CMD_WRITE |
+			self.CMD_BUF_ADDR(buf_addr) |
+			self.CMD_LEN(xfer_len)
+		)
+
+	def cmd_read(self, ram_addr, buf_addr, xfer_len, check_reset=False, dual=False):
+		self._write('addr', ram_addr)
+		self._write('cmd',
+			(self.CMD_DUAL if dual else 0) |
+			(self.CMD_CHECK_RST if check_reset else 0) |
+			self.CMD_READ |
+			self.CMD_BUF_ADDR(buf_addr) |
+			self.CMD_LEN(xfer_len)
+		)
+
+	def load_data(self, addr, data):
+		for base in range(0, len(data), 128):
+			# Upload chunk to RAM (128 bytes = max burst len)
+			for j in range(0, 128, 4):
+				b = (data[base+j:base+j+4] + b'\x00\x00\x00\x00')[0:4]
+				w = int.from_bytes(b, 'big')
+				self.ram_write(j // 4, w)
+
+			# Issue command to write chunk to RAM
+			self.cmd_write(addr + (base // 4), 0, 32)
+
+	def run(self, base, size):
+		# Check alignement
+		if (base & 31) or (size & 31):
+			raise ValueError('Base Address and Size argument for memory testing must be aligned on 32-words')
+
+		# Load random block of data
+		ref_data = [
+			random.randint(0, (1<<32)-1)
+				for i in range(256)
+		]
+
+		for i in range(256):
+			self.ram_write(i, ref_data[i])
+
+		# Fill memory
+		for addr in range(base, base+size, 32):
+			print(" . Writing block @ %08x\r" % (addr,), end='')
+			self.cmd_write(addr, addr & 0xff, 32)
+
+		# Validate all blocks
+		all_good = True
+
+		for addr in range(base, base+size, 32):
+			blk_first = (addr & 0xfff) == 0x000
+			blk_last  = (addr & 0xfff) == 0xfe0
+
+			print(" . Reading block @ %08x\r" % (addr,), end='')
+			self.cmd_read(addr, addr & 0xff, 32, check_reset=blk_first)
+
+			if blk_last:
+				if not (self._read('cmd') & 2):
+					print(" ! Failed at block %08x" % (addr,))
+					all_good = False
+
+		print("                                    \r", end='')
+
+		return all_good
+
+
+# ----------------------------------------------------------------------------
+# HDMI Output
+# ----------------------------------------------------------------------------
+
+class HDMIOutput(object):
+
+	def __init__(self, intf, base):
+		self.intf = intf
+		self.base = base
+
+	def _write(self, reg, val):
+		self.intf.write(self.base + self.CORE_REGS[reg], val)
+
+	def _read(self, reg):
+		return self.intf.read(self.base + self.CORE_REGS[reg])
+
+	def pal_write(self, addr, val):
+		self.intf.write(self.base + (1<<6) + addr, val)
+
+	def enable(self, fb_addr, burst_len):
+		# Frame Buffer address
+		self.intf.write(self.base + 1, fb_addr)
+
+		# Burst Config
+		bn_cnt = ((1920 // 8) - 1) // burst_len
+		bn_len = burst_len - 1
+		bl_len = (1920 // 8) - (burst_len * bn_cnt) - 1
+		bl_inc = bl_len
+
+		self.intf.write(self.base + 0,
+			(1 << 31) |
+			(bn_cnt << 24) |
+			(bn_len << 16) |
+			(bl_len <<  8) |
+			(bl_inc <<  0)
+		)
+
+	def disable(self):
+		self.intf.write(self.base + 0, 0)