Browse Source

projects/rgb_panel: Import example RGB panel driving using HUB75 core

Signed-off-by: Sylvain Munaut <tnt@246tNt.com>
Sylvain Munaut 6 years ago
parent
commit
8280975da2

+ 54 - 0
projects/_common/control.py

@@ -0,0 +1,54 @@
+#!/usr/bin/env python3
+
+import struct
+
+from pyftdi.spi import SpiController
+
+
+class BoardControlBase(object):
+
+	def __init__(self, addr='ftdi://ftdi:2232h/1', spi_frequency=30e6, spi_cs=None):
+		# SPI link
+		self.spi_frequency = spi_frequency
+		self.spi = SpiController(cs_count=3)
+		self.spi.configure(addr)
+
+		if spi_cs is not None:
+			self.slave = self.spi.get_port(cs=spi_cs, freq=self.spi_frequency, mode=0)
+		else:
+			self.slave = self._spi_probe()
+
+	def _spi_probe(self):
+		for cs in [0, 2]:
+			port = self.spi.get_port(cs=cs, freq=self.spi_frequency, mode=0)
+			r = port.exchange(b'\x00', duplex=True)[0]
+			if r != 0xff:
+				return port
+		raise RunttimeError('Automatic SPI CS probe failed')
+
+	def reg_w16(self, reg, v):
+		self.slave.exchange(struct.pack('>BH', reg, v))
+
+	def reg_w8(self, reg, v):
+		self.slave.exchange(struct.pack('>BB', reg, v))
+
+	def reg_burst(self, reg, data):
+		self.slave.exchange(bytearray([reg]) + data)
+
+	def read_status(self):
+		rv = self.slave.exchange(bytearray(2), duplex=True)
+		return rv[0] | rv[1]
+
+
+def arg_group_setup(group):
+	group.add_argument('--spi-freq',  type=float, help='SPI frequency in MHz', default=30.0)
+	group.add_argument('--spi-cs',    type=int,   help='SPI slave select id (-1 = probe)', default=-1)
+	group.add_argument('--ftdi-addr', type=str,   help='FTDI address', default='ftdi://ftdi:2232h/1')
+
+
+def arg_to_kwargs(args):
+	return {
+		'spi_frequency': args.spi_freq * 1e6,
+		'spi_cs': args.spi_cs if (args.spi_cs >= 0) else None,
+		'addr': args.ftdi_addr,
+	}

+ 41 - 0
projects/rgb_panel/Makefile

@@ -0,0 +1,41 @@
+# Project config
+PROJ = rgb_panel
+
+PROJ_DEPS := hub75 spi_flash spi_slave
+PROJ_RTL_SRCS := $(addprefix rtl/, \
+	pgen.v \
+	sysmgr.v \
+	vgen.v \
+	vstream.v \
+)
+PROJ_TESTBENCHES := \
+	hub75_top_tb
+PROJ_TOP_SRC := rtl/top.v
+PROJ_TOP_MOD := top
+
+# Target config
+BOARD ?= icebreaker
+DEVICE = up5k
+PACKAGE = sg48
+
+NEXTPNR_ARGS = --freq 35
+
+# Include default rules
+include ../../build/project-rules.mk
+
+# Custom rules
+
+	# SPI core selection
+SPI ?= fast
+ifeq ($(SPI),fast)
+YOSYS_READ_ARGS += -DSPI_FAST=1
+endif
+
+	# Custom data program
+$(BUILD_TMP)/$(PROJ)-video.bin: $(BUILD_TMP)/$(PROJ).bin data/nyan_glitch_64x64x16.raw
+	cp $< $@
+	dd if=data/nyan_glitch_64x64x16.raw of=$@ seek=256 bs=1024
+
+data-prog: $(BUILD_TMP)/$(PROJ)-video.bin
+	$(ICEPROG) $<
+

+ 68 - 0
projects/rgb_panel/README.md

@@ -0,0 +1,68 @@
+RGB Panel driver for iCEBreaker board
+=====================================
+
+This is an example usage of the Hub75 driver IP in this repository
+and implements driving RGB panels using the iCEBreaker board along
+with the RGB Panel PMOD.
+
+Default configuration is for a 64x64 panel using 1:32 multiplex.
+Note that some panels have the Red and Blue channels swapped, so
+you might have to adapt this ...
+
+This example has 3 modes of operations explained below. Each
+mode is selected by uncommenting the appropriate `define` at the
+top of the `top.v` file.
+
+
+Pattern mode
+------------
+
+This generates a Red & Blue gradient across the two axises and then some
+moving green lines across. Very simple example of generating data directly
+on the FPGA itself and can also be used as a pretty reliable test that all
+works well.
+
+
+Video play mode
+---------------
+
+In this mode, frames are read from the SPI flash and displayed in sequence.
+
+For this to work, you need some video content to be preloaded into the flash.
+You can use the special `make data-prog` target to load a default nyan cat
+animation.
+
+To load your own animation in flash, checkout the `ADDR_BASE` and `N_FRAMES`
+parameters that tell the module where to look in flash for the image data.
+It needs to be raw frames, pixel format is either in `RGB332` or `RGB565`
+or `RGB888` depending on the `BITDEPTH` you selected. (Default is 16 bits and
+`RGB565`).
+
+
+Video streaming mode
+--------------------
+
+In this mode, video content is streamed from the host PC to the FPGA using
+SPI (through the FT2232H used for programming the FPGA).
+
+A control software `stream.py` is provided in the `sw/` sub-directory.
+It required Python 3.x and [pyftdi](https://github.com/eblot/pyftdi).
+
+And example usage would be :
+
+```
+./stream.py --fps 10 --loop --input ../data/nyan_glitch_64x64x16.raw
+```
+
+See the `--help` for other options available.
+
+To prepare content, you can use ffmpeg :
+
+```
+ffmpeg -i input.mp4 -filter_complex "[0:v]crop=540:540,scale=64:64" -pix_fmt rgb565 -f rawvideo output.raw
+```
+
+Obviously the number for the `crop` filter need to be adjusted for your source
+material to get a square image that selects the best region to show. Also, you
+can do use unix FIFOs to directly pipe content from `ffmpeg` to the `stream.py`
+application without the need for intermediate files.

BIN
projects/rgb_panel/data/nyan_glitch_64x64x16.raw


+ 35 - 0
projects/rgb_panel/data/top-icebreaker.pcf

@@ -0,0 +1,35 @@
+# RGB panel pmod
+set_io -nowarn hub75_addr[4] 28	# B10
+set_io -nowarn hub75_addr[3] 31	# B4
+set_io -nowarn hub75_addr[2] 34	# B3
+set_io -nowarn hub75_addr[1] 38	# B2
+set_io -nowarn hub75_addr[0] 43	# B1
+set_io -nowarn hub75_data[5] 46	# A9
+set_io -nowarn hub75_data[4] 48	# A8
+set_io -nowarn hub75_data[3] 3	# A7
+set_io -nowarn hub75_data[2] 47	# A3
+set_io -nowarn hub75_data[1] 2	# A2
+set_io -nowarn hub75_data[0] 4	# A1
+set_io -nowarn hub75_clk 32	# B9
+set_io -nowarn hub75_le	36	# B8
+set_io -nowarn hub75_blank 42	# B7
+
+# SPI Flash
+set_io -nowarn flash_mosi 14
+set_io -nowarn flash_miso 17
+set_io -nowarn flash_cs_n 16
+set_io -nowarn flash_clk 15
+
+# SPI Slave
+set_io -nowarn slave_mosi 14
+set_io -nowarn slave_miso 17
+set_io -nowarn slave_cs_n 11
+set_io -nowarn slave_clk 15
+
+# PMOD2 buttons
+set_io -nowarn pmod_btn[0] 20
+set_io -nowarn pmod_btn[1] 19
+set_io -nowarn pmod_btn[2] 18
+
+# Clock
+set_io -nowarn clk_12m 35

+ 208 - 0
projects/rgb_panel/rtl/pgen.v

@@ -0,0 +1,208 @@
+/*
+ * pgen.v
+ *
+ * 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.
+ *
+ * vim: ts=4 sw=4
+ */
+
+`default_nettype none
+
+module pgen #(
+	parameter integer N_ROWS   = 64,	// # of rows (must be power of 2!!!)
+	parameter integer N_COLS   = 64,	// # of columns
+	parameter integer BITDEPTH = 24,
+
+	// Auto-set
+	parameter integer LOG_N_ROWS  = $clog2(N_ROWS),
+	parameter integer LOG_N_COLS  = $clog2(N_COLS)
+)(
+	// Frame Buffer write interface
+	output wire [LOG_N_ROWS-1:0] fbw_row_addr,
+	output wire fbw_row_store,
+	input  wire fbw_row_rdy,
+	output wire fbw_row_swap,
+
+	output wire [BITDEPTH-1:0] fbw_data,
+	output wire [LOG_N_COLS-1:0] fbw_col_addr,
+	output wire fbw_wren,
+
+	output wire frame_swap,
+	input  wire frame_rdy,
+
+	// Clock / Reset
+	input  wire clk,
+	input  wire rst
+);
+
+	// Signals
+	// -------
+
+	// FSM
+	localparam
+		ST_WAIT_FRAME	= 0,
+		ST_GEN_ROW		= 1,
+		ST_WRITE_ROW	= 2,
+		ST_WAIT_ROW		= 3;
+
+	reg  [2:0] fsm_state;
+	reg  [2:0] fsm_state_next;
+
+	// Counters
+	reg [11:0] frame;
+	reg [LOG_N_ROWS-1:0] cnt_row;
+	reg [LOG_N_COLS-1:0] cnt_col;
+	reg cnt_row_last;
+	reg cnt_col_last;
+
+	// Output
+	wire [7:0] color [0:2];
+
+
+	// FSM
+	// ---
+
+	// State register
+	always @(posedge clk or posedge rst)
+		if (rst)
+			fsm_state <= ST_WAIT_FRAME;
+		else
+			fsm_state <= fsm_state_next;
+
+	// Next-State logic
+	always @(*)
+	begin
+		// Default is not to move
+		fsm_state_next = fsm_state;
+
+		// Transitions ?
+		case (fsm_state)
+			ST_WAIT_FRAME:
+				if (frame_rdy)
+					fsm_state_next = ST_GEN_ROW;
+
+			ST_GEN_ROW:
+				if (cnt_col_last)
+					fsm_state_next = ST_WRITE_ROW;
+
+			ST_WRITE_ROW:
+				if (fbw_row_rdy)
+					fsm_state_next = cnt_row_last ? ST_WAIT_ROW : ST_GEN_ROW;
+
+			ST_WAIT_ROW:
+				if (fbw_row_rdy)
+					fsm_state_next = ST_WAIT_FRAME;
+		endcase
+	end
+
+
+	// Counters
+	// --------
+
+	// Frame counter
+	always @(posedge clk or posedge rst)
+		if (rst)
+			frame <= 0;
+		else if ((fsm_state == ST_WAIT_ROW) && fbw_row_rdy)
+			frame <= frame + 1;
+
+	// Row counter
+	always @(posedge clk)
+		if (fsm_state == ST_WAIT_FRAME) begin
+			cnt_row <= 0;
+			cnt_row_last <= 1'b0;
+		end else if ((fsm_state == ST_WRITE_ROW) && fbw_row_rdy) begin
+			cnt_row <= cnt_row + 1;
+			cnt_row_last <= cnt_row == ((1 << LOG_N_ROWS) - 2);
+		end
+
+	// Column counter
+	always @(posedge clk)
+		if (fsm_state != ST_GEN_ROW) begin
+			cnt_col <= 0;
+			cnt_col_last <= 0;
+		end else begin
+			cnt_col <= cnt_col + 1;
+			cnt_col_last <= cnt_col == (N_COLS - 2);
+		end
+
+
+	// Front-Buffer write
+	// ------------------
+
+	// Generate R/B channels by taking 8 bits off the row/col counters
+	// (and wrapping to the MSBs if those are shorter than 8 bits
+	genvar i;
+	generate
+		for (i=0; i<8; i=i+1)
+		begin
+			assign color[0][7-i] = cnt_col[LOG_N_COLS-1-(i%LOG_N_COLS)];
+			assign color[2][7-i] = cnt_row[LOG_N_ROWS-1-(i%LOG_N_ROWS)];
+		end
+	endgenerate
+
+	// Moving green lines
+	wire [3:0] c0 = frame[7:4];
+	wire [3:0] c1 = frame[7:4] + 1;
+
+	wire [3:0] a0 = 4'hf - frame[3:0];
+	wire [3:0] a1 = frame[3:0];
+
+	assign color[1] =
+		(((cnt_col[3:0] == c0) || (cnt_row[3:0] == c0)) ? {a0, a0} : 8'h00) +
+		(((cnt_col[3:0] == c1) || (cnt_row[3:0] == c1)) ? {a1, a1} : 8'h00);
+
+	// Write enable and address
+	assign fbw_wren = fsm_state == ST_GEN_ROW;
+	assign fbw_col_addr = cnt_col;
+
+	// Map to color
+	generate
+		if (BITDEPTH == 8)
+			assign fbw_data = { color[0][7:5], color[1][7:5], color[2][7:6] };
+		else if (BITDEPTH == 16)
+			assign fbw_data = { color[0][7:3], color[1][7:2], color[2][7:3] };
+		else if (BITDEPTH == 24)
+			assign fbw_data = { color[0], color[1], color[2] };
+	endgenerate
+
+
+	// Back-Buffer store
+	// -----------------
+
+	assign fbw_row_addr  = cnt_row;
+	assign fbw_row_store = (fsm_state == ST_WRITE_ROW) && fbw_row_rdy;
+	assign fbw_row_swap  = (fsm_state == ST_WRITE_ROW) && fbw_row_rdy;
+
+
+	// Next frame
+	// ----------
+
+	assign frame_swap = (fsm_state == ST_WAIT_ROW) && fbw_row_rdy;
+
+endmodule // pgen

+ 102 - 0
projects/rgb_panel/rtl/sysmgr.v

@@ -0,0 +1,102 @@
+/*
+ * sysmgr.v
+ *
+ * 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.
+ *
+ * vim: ts=4 sw=4
+ */
+
+`default_nettype none
+
+module sysmgr (
+	input  wire clk_in,
+	input  wire rst_in,
+	output wire clk_out,
+	output wire rst_out
+);
+
+	// Signals
+	wire pll_lock;
+	wire pll_reset_n;
+
+	wire clk_i;
+	wire rst_i;
+	reg [3:0] rst_cnt;
+
+	// PLL instance
+`ifdef SIM
+	assign clk_i = clk_in;
+	assign pll_lock = pll_reset_n;
+`else
+	SB_PLL40_PAD #(
+		.DIVR(4'b0000),
+		.DIVF(7'b1001111),
+		.DIVQ(3'b101),
+		.FILTER_RANGE(3'b001),
+		.FEEDBACK_PATH("SIMPLE"),
+		.DELAY_ADJUSTMENT_MODE_FEEDBACK("FIXED"),
+		.FDA_FEEDBACK(4'b0000),
+		.SHIFTREG_DIV_MODE(2'b00),
+		.PLLOUT_SELECT("GENCLK"),
+		.ENABLE_ICEGATE(1'b0)
+	) pll_I (
+		.PACKAGEPIN(clk_in),
+		.PLLOUTCORE(),
+		.PLLOUTGLOBAL(clk_i),
+		.EXTFEEDBACK(1'b0),
+		.DYNAMICDELAY(8'h00),
+		.RESETB(pll_reset_n),
+		.BYPASS(1'b0),
+		.LATCHINPUTVALUE(1'b0),
+		.LOCK(pll_lock),
+		.SDI(1'b0),
+		.SDO(),
+		.SCLK(1'b0)
+	);
+`endif
+
+	assign clk_out = clk_i;
+
+	// PLL reset generation
+	assign pll_reset_n = ~rst_in;
+
+	// Logic reset generation
+	always @(posedge clk_i or negedge pll_lock)
+		if (!pll_lock)
+			rst_cnt <= 4'h8;
+		else if (rst_cnt[3])
+			rst_cnt <= rst_cnt + 1;
+
+	assign rst_i = rst_cnt[3];
+
+	SB_GB rst_gbuf_I (
+		.USER_SIGNAL_TO_GLOBAL_BUFFER(rst_i),
+		.GLOBAL_BUFFER_OUTPUT(rst_out)
+	);
+
+endmodule // sysmgr

+ 317 - 0
projects/rgb_panel/rtl/top.v

@@ -0,0 +1,317 @@
+/*
+ * top.v
+ *
+ * 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.
+ *
+ * vim: ts=4 sw=4
+ */
+
+`default_nettype none
+
+//`define STREAM
+`define PATTERN
+//`define VIDEO
+
+module top (
+	// RGB panel PMOD
+	output wire [4:0] hub75_addr,
+	output wire [5:0] hub75_data,
+	output wire hub75_clk,
+	output wire hub75_le,
+	output wire hub75_blank,
+
+	// SPI Flash interface
+`ifdef VIDEO
+	output wire flash_mosi,
+	input  wire flash_miso,
+	output wire flash_cs_n,
+	output wire flash_clk,
+`endif
+
+	// SPI Slave interface
+`ifdef STREAM
+	input  wire slave_mosi,
+	output wire slave_miso,
+	input  wire slave_cs_n,
+	input  wire slave_clk,
+`endif
+
+	// PMOD2 buttons
+	input  wire [2:0] pmod_btn,
+
+	// Clock
+	input  wire clk_12m
+);
+
+	// Params
+	localparam integer N_BANKS  = 2;
+	localparam integer N_ROWS   = 32;
+	localparam integer N_COLS   = 64;
+	localparam integer N_CHANS  = 3;
+	localparam integer N_PLANES = 10;
+	localparam integer BITDEPTH = 16;
+
+	localparam integer LOG_N_BANKS = $clog2(N_BANKS);
+	localparam integer LOG_N_ROWS  = $clog2(N_ROWS);
+	localparam integer LOG_N_COLS  = $clog2(N_COLS);
+
+
+	// Signals
+	// -------
+
+	// Clock / Reset logic
+`ifdef NO_PLL
+	reg [7:0] rst_cnt = 8'h00;
+	wire rst_i;
+`endif
+
+	wire clk;
+	wire rst;
+
+	// Frame buffer write port
+	wire [LOG_N_BANKS-1:0] fbw_bank_addr;
+	wire [LOG_N_ROWS-1:0]  fbw_row_addr;
+	wire fbw_row_store;
+	wire fbw_row_rdy;
+	wire fbw_row_swap;
+
+	wire [BITDEPTH-1:0] fbw_data;
+	wire [LOG_N_COLS-1:0] fbw_col_addr;
+	wire fbw_wren;
+
+	wire frame_swap;
+	wire frame_rdy;
+
+
+	// Hub75 driver
+	// ------------
+
+	hub75_top #(
+		.N_BANKS(N_BANKS),
+		.N_ROWS(N_ROWS),
+		.N_COLS(N_COLS),
+		.N_CHANS(N_CHANS),
+		.N_PLANES(N_PLANES),
+		.BITDEPTH(BITDEPTH)
+	) hub75_I (
+		.hub75_addr(hub75_addr),
+		.hub75_data(hub75_data),
+		.hub75_clk(hub75_clk),
+		.hub75_le(hub75_le),
+		.hub75_blank(hub75_blank),
+		.fbw_bank_addr(fbw_bank_addr),
+		.fbw_row_addr(fbw_row_addr),
+		.fbw_row_store(fbw_row_store),
+		.fbw_row_rdy(fbw_row_rdy),
+		.fbw_row_swap(fbw_row_swap),
+		.fbw_data(fbw_data),
+		.fbw_col_addr(fbw_col_addr),
+		.fbw_wren(fbw_wren),
+		.frame_swap(frame_swap),
+		.frame_rdy(frame_rdy),
+		.cfg_pre_latch_len(8'h80),
+		.cfg_latch_len(8'h80),
+		.cfg_post_latch_len(8'h80),
+		.cfg_bcm_bit_len(8'h06),
+		.clk(clk),
+		.rst(rst)
+	);
+
+
+	// Host Streaming
+	// --------------
+`ifdef STREAM
+	vstream #(
+		.N_ROWS(N_BANKS * N_ROWS),
+		.N_COLS(N_COLS),
+		.BITDEPTH(BITDEPTH)
+	) stream_I (
+		.spi_mosi(slave_mosi),
+		.spi_miso(slave_miso),
+		.spi_cs_n(slave_cs_n),
+		.spi_clk(slave_clk),
+		.fbw_row_addr({fbw_bank_addr, fbw_row_addr}),
+		.fbw_row_store(fbw_row_store),
+		.fbw_row_rdy(fbw_row_rdy),
+		.fbw_row_swap(fbw_row_swap),
+		.fbw_data(fbw_data),
+		.fbw_col_addr(fbw_col_addr),
+		.fbw_wren(fbw_wren),
+		.frame_swap(frame_swap),
+		.frame_rdy(frame_rdy),
+		.clk(clk),
+		.rst(rst)
+	);
+`endif
+
+
+	// Pattern generator
+	// -----------------
+
+`ifdef PATTERN
+	pgen #(
+		.N_ROWS(N_BANKS * N_ROWS),
+		.N_COLS(N_COLS),
+		.BITDEPTH(BITDEPTH)
+	) pgen_I (
+		.fbw_row_addr({fbw_bank_addr, fbw_row_addr}),
+		.fbw_row_store(fbw_row_store),
+		.fbw_row_rdy(fbw_row_rdy),
+		.fbw_row_swap(fbw_row_swap),
+		.fbw_data(fbw_data),
+		.fbw_col_addr(fbw_col_addr),
+		.fbw_wren(fbw_wren),
+		.frame_swap(frame_swap),
+		.frame_rdy(frame_rdy),
+		.clk(clk),
+		.rst(rst)
+	);
+`endif
+
+
+	// Video generator (from SPI flash)
+	// ---------------
+
+`ifdef VIDEO
+	// Signals
+		// SPI reader interface
+	wire [23:0] sr_addr;
+	wire [15:0] sr_len;
+	wire sr_go;
+	wire sr_rdy;
+
+	wire [7:0] sr_data;
+	wire sr_valid;
+
+		// UI
+	wire btn_up;
+	wire btn_mode;
+	wire btn_down;
+
+	// Main video generator / controller
+	vgen #(
+		.ADDR_BASE(24'h040000),
+		.N_FRAMES(30),
+		.N_ROWS(N_BANKS * N_ROWS),
+		.N_COLS(N_COLS),
+		.BITDEPTH(BITDEPTH)
+	) vgen_I (
+		.sr_addr(sr_addr),
+		.sr_len(sr_len),
+		.sr_go(sr_go),
+		.sr_rdy(sr_rdy),
+		.sr_data(sr_data),
+		.sr_valid(sr_valid),
+		.fbw_row_addr({fbw_bank_addr, fbw_row_addr}),
+		.fbw_row_store(fbw_row_store),
+		.fbw_row_rdy(fbw_row_rdy),
+		.fbw_row_swap(fbw_row_swap),
+		.fbw_data(fbw_data),
+		.fbw_col_addr(fbw_col_addr),
+		.fbw_wren(fbw_wren),
+		.frame_swap(frame_swap),
+		.frame_rdy(frame_rdy),
+		.ui_up(btn_up),
+		.ui_mode(btn_mode),
+		.ui_down(btn_down),
+		.clk(clk),
+		.rst(rst)
+	);
+
+	// SPI reader to fetch frames from flash
+	spi_flash_reader spi_reader_I (
+		.spi_mosi(flash_mosi),
+		.spi_miso(flash_miso),
+		.spi_cs_n(flash_cs_n),
+		.spi_clk(flash_clk),
+		.addr(sr_addr),
+		.len(sr_len),
+		.go(sr_go),
+		.rdy(sr_rdy),
+		.data(sr_data),
+		.valid(sr_valid),
+		.clk(clk),
+		.rst(rst)
+	);
+
+	// UI
+	glitch_filter #( .L(8) ) gf_down_I (
+		.pin_iob_reg(pmod_btn[0]),
+		.cond(1'b1),
+		.rise(btn_down),
+		.clk(clk),
+		.rst(rst)
+	);
+
+	glitch_filter #( .L(8) ) gf_mode_I (
+		.pin_iob_reg(pmod_btn[1]),
+		.cond(1'b1),
+		.rise(btn_mode),
+		.clk(clk),
+		.rst(rst)
+	);
+
+	glitch_filter #( .L(8) ) gf_up_I (
+		.pin_iob_reg(pmod_btn[2]),
+		.cond(1'b1),
+		.rise(btn_up),
+		.clk(clk),
+		.rst(rst)
+	);
+`endif
+
+
+	// Clock / Reset
+	// -------------
+
+`ifdef NO_PLL
+	always @(posedge clk)
+		if (~rst_cnt[7])
+			rst_cnt <= rst_cnt + 1;
+
+	wire rst_i = ~rst_cnt[7];
+
+	SB_GB clk_gbuf_I (
+		.USER_SIGNAL_TO_GLOBAL_BUFFER(clk_12m),
+		.GLOBAL_BUFFER_OUTPUT(clk)
+	);
+
+	SB_GB rst_gbuf_I (
+		.USER_SIGNAL_TO_GLOBAL_BUFFER(rst_i),
+		.GLOBAL_BUFFER_OUTPUT(rst)
+	);
+`else
+	sysmgr sys_mgr_I (
+		.clk_in(clk_12m),
+		.rst_in(1'b0),
+		.clk_out(clk),
+		.rst_out(rst)
+	);
+`endif
+
+endmodule // top

+ 290 - 0
projects/rgb_panel/rtl/vgen.v

@@ -0,0 +1,290 @@
+/*
+ * vgen.v
+ *
+ * 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.
+ *
+ * vim: ts=4 sw=4
+ */
+
+`default_nettype none
+
+module vgen #(
+	parameter ADDR_BASE = 24'h040000,
+	parameter integer N_FRAMES = 30,
+	parameter integer N_ROWS   = 64,	// # of rows (must be power of 2!!!)
+	parameter integer N_COLS   = 64,	// # of columns
+	parameter integer BITDEPTH = 24,
+
+	// Auto-set
+	parameter integer LOG_N_ROWS  = $clog2(N_ROWS),
+	parameter integer LOG_N_COLS  = $clog2(N_COLS)
+)(
+	// SPI reader interface
+	output wire [23:0] sr_addr,
+	output wire [15:0] sr_len,
+	output wire sr_go,
+	input  wire sr_rdy,
+
+	input wire [7:0] sr_data,
+	input wire sr_valid,
+
+	// Frame Buffer write interface
+	output wire [LOG_N_ROWS-1:0] fbw_row_addr,
+	output wire fbw_row_store,
+	input  wire fbw_row_rdy,
+	output wire fbw_row_swap,
+
+	output wire [23:0] fbw_data,
+	output wire [LOG_N_COLS-1:0] fbw_col_addr,
+	output wire fbw_wren,
+
+	output wire frame_swap,
+	input  wire frame_rdy,
+
+	// UI
+	input  wire ui_up,
+	input  wire ui_mode,
+	input  wire ui_down,
+
+	// Clock / Reset
+	input  wire clk,
+	input  wire rst
+);
+
+	localparam integer FW = 23 - LOG_N_ROWS - LOG_N_COLS;
+
+	// Signals
+	// -------
+
+	// FSM
+	localparam
+		ST_FRAME_WAIT	= 0,
+		ST_ROW_SPI_CMD	= 1,
+		ST_ROW_SPI_READ	= 2,
+		ST_ROW_WRITE	= 3,
+		ST_ROW_WAIT		= 4;
+
+	reg  [2:0] fsm_state;
+	reg  [2:0] fsm_state_next;
+
+	// UI
+	reg mode;
+	reg [3:0] cfg_rep;
+	reg [1:0] frame_sel;
+
+	// Counters
+	reg [FW-1:0] cnt_frame;
+	reg cnt_frame_first;
+	reg cnt_frame_last;
+
+	reg [3:0] cnt_rep;
+	reg cnt_rep_last;
+
+	reg [LOG_N_ROWS-1:0] cnt_row;
+	reg cnt_row_last;
+
+	reg [LOG_N_COLS:0] cnt_col;
+
+	// SPI
+	reg [7:0] sr_data_r;
+	wire [15:0] sr_data16;
+
+	// Output
+	wire [7:0] color [0:2];
+
+
+	// FSM
+	// ---
+
+	// State register
+	always @(posedge clk or posedge rst)
+		if (rst)
+			fsm_state <= ST_FRAME_WAIT;
+		else
+			fsm_state <= fsm_state_next;
+
+	// Next-State logic
+	always @(*)
+	begin
+		// Default is not to move
+		fsm_state_next = fsm_state;
+
+		// Transitions ?
+		case (fsm_state)
+			ST_FRAME_WAIT:
+				if (frame_rdy & sr_rdy)
+					fsm_state_next = ST_ROW_SPI_CMD;
+
+			ST_ROW_SPI_CMD:
+				fsm_state_next = ST_ROW_SPI_READ;
+
+			ST_ROW_SPI_READ:
+				if (sr_rdy)
+					fsm_state_next = ST_ROW_WRITE;
+
+			ST_ROW_WRITE:
+				if (fbw_row_rdy)
+					fsm_state_next = cnt_row_last ? ST_ROW_WAIT : ST_ROW_SPI_CMD;
+
+			ST_ROW_WAIT:
+				if (fbw_row_rdy)
+					fsm_state_next = ST_FRAME_WAIT;
+		endcase
+	end
+
+
+	// UI handling
+	// -----------
+
+	// Mode toggle
+	always @(posedge clk or posedge rst)
+		if (rst)
+			mode <= 1'b0;
+		else
+			mode <= mode ^ ui_mode;
+
+	// Repetition counter
+	always @(posedge clk or posedge rst)
+		if (rst)
+			cfg_rep <= 4'h6;
+		else if (~mode) begin
+			if (ui_down & ~&cfg_rep)
+				cfg_rep <= cfg_rep + 1;
+			else if (ui_up & |cfg_rep)
+				cfg_rep <= cfg_rep - 1;
+		end
+
+	// Latch request for prev / next frame
+	always @(posedge clk)
+		if (~mode)
+			frame_sel <= cnt_rep_last ? 2'b10 : 2'b00;
+		else if ((fsm_state == ST_ROW_WAIT) && fbw_row_rdy)
+			frame_sel <= 2'b00;
+		else if (ui_up)
+			frame_sel <= 2'b10;
+		else if (ui_down)
+			frame_sel <= 2'b11;
+
+
+	// Counters
+	// --------
+
+	// Frame counter
+	always @(posedge clk or posedge rst)
+		if (rst)
+			cnt_frame <= 0;
+		else if ((fsm_state == ST_ROW_WAIT) && fbw_row_rdy && frame_sel[1])
+			if (frame_sel[0])
+				cnt_frame <= cnt_frame_last  ? { (FW){1'b0} } : (cnt_frame + 1);
+			else
+				cnt_frame <= cnt_frame_first ? (N_FRAMES - 1) : (cnt_frame - 1);
+
+	always @(posedge clk)
+	begin
+		// Those end up one cycle late vs 'cnt_frame' but that's fine, they
+		// won't be used until a while later
+		cnt_frame_last  <= (cnt_frame == (N_FRAMES - 1));
+		cnt_frame_first <= (cnt_frame == 0);
+	end
+
+	// Repeat counter
+	always @(posedge clk)
+		if ((fsm_state == ST_ROW_WAIT) && fbw_row_rdy) begin
+			cnt_rep <= cnt_rep_last ? 4'h0 : (cnt_rep + 1);
+			cnt_rep_last <= (cnt_rep == cfg_rep);
+		end
+
+	// Row counter
+	always @(posedge clk)
+		if (fsm_state == ST_FRAME_WAIT) begin
+			cnt_row <= 0;
+			cnt_row_last <= 1'b0;
+		end else if ((fsm_state == ST_ROW_WRITE) && fbw_row_rdy) begin
+			cnt_row <= cnt_row + 1;
+			cnt_row_last <= (cnt_row == (1 << LOG_N_ROWS) - 2);
+		end
+
+	// Column counter
+	always @(posedge clk)
+		if (fsm_state != ST_ROW_SPI_READ)
+			cnt_col <= 0;
+		else if (sr_valid)
+			cnt_col <= cnt_col + 1;
+
+
+	// SPI reader
+	// ----------
+
+	// Requests
+	assign sr_addr = { cnt_frame, cnt_row, {(LOG_N_COLS+1){1'b0}} } + ADDR_BASE;
+	assign sr_len = (N_COLS << 1) - 1;
+	assign sr_go = (fsm_state == ST_ROW_SPI_CMD);
+
+	// Data
+	always @(posedge clk)
+		if (sr_valid)
+			sr_data_r <= sr_data;
+	
+	assign sr_data16 = { sr_data, sr_data_r };
+
+
+	// Front-Buffer write
+	// ------------------
+
+	assign fbw_wren = sr_valid & cnt_col[0];
+	assign fbw_col_addr = cnt_col[6:1];
+
+	// Map to color
+	assign color[0] = { sr_data16[15:11], sr_data16[15:13] };
+	assign color[1] = { sr_data16[10: 5], sr_data16[10: 9] };
+	assign color[2] = { sr_data16[ 4: 0], sr_data16[ 4: 2] };
+
+	generate
+		if (BITDEPTH == 8)
+			assign fbw_data = { color[0][7:5], color[1][7:5], color[2][7:6] };
+		else if (BITDEPTH == 16)
+			assign fbw_data = { color[0][7:3], color[1][7:2], color[2][7:3] };
+		else if (BITDEPTH == 24)
+			assign fbw_data = { color[0], color[1], color[2] };
+	endgenerate
+
+
+	// Back-Buffer store
+	// -----------------
+
+	assign fbw_row_addr  = cnt_row;
+	assign fbw_row_store = (fsm_state == ST_ROW_WRITE) && fbw_row_rdy;
+	assign fbw_row_swap  = (fsm_state == ST_ROW_WRITE) && fbw_row_rdy;
+
+
+	// Next frame
+	// ----------
+
+	assign frame_swap = (fsm_state == ST_ROW_WAIT) && fbw_row_rdy;
+
+endmodule // vgen

+ 192 - 0
projects/rgb_panel/rtl/vstream.v

@@ -0,0 +1,192 @@
+/*
+ * vstream.v
+ *
+ * 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.
+ *
+ * vim: ts=4 sw=4
+ */
+
+`default_nettype none
+
+module vstream #(
+	parameter integer N_ROWS   = 64,	// # of rows (must be power of 2!!!)
+	parameter integer N_COLS   = 64,	// # of columns
+	parameter integer BITDEPTH = 24,
+
+	// Auto-set
+	parameter integer LOG_N_ROWS  = $clog2(N_ROWS),
+	parameter integer LOG_N_COLS  = $clog2(N_COLS)
+)(
+	// SPI to the host
+	input  wire spi_mosi,
+	output wire spi_miso,
+	input  wire spi_cs_n,
+	input  wire spi_clk,
+
+	// Frame Buffer write interface
+	output wire [LOG_N_ROWS-1:0] fbw_row_addr,
+	output wire fbw_row_store,
+	input  wire fbw_row_rdy,
+	output wire fbw_row_swap,
+
+	output wire [23:0] fbw_data,
+	output wire [LOG_N_COLS-1:0] fbw_col_addr,
+	output wire fbw_wren,
+
+	output wire frame_swap,
+	input  wire frame_rdy,
+
+	// Clock / Reset
+	input  wire clk,
+	input  wire rst
+);
+
+	localparam integer TW = BITDEPTH / 8;
+
+	// Signals
+	// -------
+
+	// SPI bus
+	wire [7:0] sb_addr;
+	wire [7:0] sb_data;
+	wire sb_first;
+	wire sb_last;
+	wire sb_stb;
+	wire [7:0] sb_out;
+
+	// Front Buffer write
+	reg [TW-1:0] trig;
+	reg [LOG_N_COLS-1:0] cnt_col;
+	reg [7:0] sb_data_r[0:1];
+
+	reg store_swap_pending;
+
+	reg [5:0] err_cnt;
+	wire err;
+
+
+	// SPI interface
+	// -------------
+
+`ifdef SPI_FAST
+	spi_fast spi_I (
+`else
+	spi_simple spi_I (
+`endif
+		.spi_mosi(spi_mosi),
+		.spi_miso(spi_miso),
+		.spi_cs_n(spi_cs_n),
+		.spi_clk(spi_clk),
+		.addr(sb_addr),
+		.data(sb_data),
+		.first(sb_first),
+		.last(sb_last),
+		.strobe(sb_stb),
+		.out(sb_out),
+		.clk(clk),
+		.rst(rst)
+	);
+
+	assign sb_out = { err_cnt, frame_rdy, fbw_row_rdy };
+
+
+	// Front-Buffer write
+	// ------------------
+
+	// "Trigger"
+	always @(posedge clk or posedge rst)
+		if (TW > 1) begin
+			if (rst)
+				trig <= { 1'b1, {(TW-1){1'b0}} };
+			else if (sb_stb)
+				trig <= sb_last ? { 1'b1, {(TW-1){1'b0}} } : { trig[0], trig[TW-1:1] };
+		end else
+			trig <= 1'b1;
+
+	// Column counter
+	always @(posedge clk or posedge rst)
+		if (rst)
+			cnt_col <= 0;
+		else if (sb_stb)
+			cnt_col <= sb_last ? 0 : (cnt_col + trig[0]);
+
+	// Register data for wide writes
+	always @(posedge clk)
+		if (sb_stb) begin
+			sb_data_r[0] <= sb_data;
+			sb_data_r[1] <= sb_data_r[0];
+		end
+
+	// Write commands
+	assign fbw_wren = sb_stb & sb_addr[7] & trig[0];
+	assign fbw_col_addr = cnt_col;
+
+	// Map to color
+	generate
+		if (BITDEPTH == 8)
+			assign fbw_data = sb_data;
+		else if (BITDEPTH == 16)
+			assign fbw_data = { sb_data, sb_data_r[0] };
+		else if (BITDEPTH == 24)
+			assign fbw_data = { sb_data, sb_data_r[0], sb_data_r[1] };
+	endgenerate
+
+
+	// Back-Buffer store
+	// -----------------
+
+	// Direct commands
+	assign fbw_row_addr  = sb_data[LOG_N_ROWS-1:0];
+	assign fbw_row_store = (sb_stb & sb_first & ~sb_addr[7] & sb_addr[0]) | (fbw_row_rdy & store_swap_pending);
+	assign fbw_row_swap  = (sb_stb & sb_first & ~sb_addr[7] & sb_addr[1]) | (fbw_row_rdy & store_swap_pending);
+
+	// Delayed command
+	always @(posedge clk or posedge rst)
+		if (rst)
+			store_swap_pending <= 1'b0;
+		else
+			store_swap_pending <= (store_swap_pending & ~fbw_row_rdy) | (sb_stb & sb_first & ~sb_addr[7] & sb_addr[3]);
+
+	// Error tracking
+	assign err =
+		(~fbw_row_rdy & (fbw_row_store | fbw_row_swap)) |
+		(store_swap_pending & fbw_wren);
+
+	always @(posedge clk or posedge rst)
+		if (rst)
+			err_cnt <= 0;
+		else
+			err_cnt <= err_cnt + err;
+
+
+	// Next frame
+	// ----------
+
+	assign frame_swap = sb_stb & sb_first & ~sb_addr[7] & sb_addr[2];
+
+endmodule // vstream

+ 198 - 0
projects/rgb_panel/sim/hub75_top_tb.v

@@ -0,0 +1,198 @@
+/*
+ * hub75_top_tb.v
+ *
+ * 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.
+ *
+ * vim: ts=4 sw=4
+ */
+
+`default_nettype none
+`timescale 1ns / 100ps
+
+module hub75_top_tb;
+
+	// Params
+	localparam integer N_BANKS  = 2;
+	localparam integer N_ROWS   = 32;
+	localparam integer N_COLS   = 64;
+	localparam integer N_CHANS  = 3;
+	localparam integer N_PLANES = 8;
+	localparam integer BITDEPTH = 24;
+
+	localparam integer LOG_N_BANKS = $clog2(N_BANKS);
+	localparam integer LOG_N_ROWS  = $clog2(N_ROWS);
+	localparam integer LOG_N_COLS  = $clog2(N_COLS);
+
+	// Signals
+	reg rst = 1'b1;
+	reg clk = 1'b0;
+
+	wire [$clog2(N_ROWS)-1:0] hub75_addr;
+	wire [(N_BANKS*N_CHANS)-1:0] hub75_data;
+	wire hub75_clk;
+	wire hub75_le;
+	wire hub75_blank;
+
+	wire [LOG_N_BANKS-1:0] fbw_bank_addr;
+	wire [LOG_N_ROWS-1:0]  fbw_row_addr;
+	wire fbw_row_store;
+	wire fbw_row_rdy;
+	wire fbw_row_swap;
+
+	wire [BITDEPTH-1:0] fbw_data;
+	wire [LOG_N_COLS-1:0] fbw_col_addr;
+	wire fbw_wren;
+
+	wire frame_swap;
+	wire frame_rdy;
+
+	// SPI Reader
+`ifndef PATTERN
+	wire spi_mosi;
+	wire spi_miso;
+	wire spi_cs_n;
+	wire spi_clk;
+
+	wire [23:0] sr_addr;
+	wire [15:0] sr_len;
+	wire sr_go;
+	wire sr_rdy;
+
+	wire [7:0] sr_data;
+	wire sr_valid;
+`endif
+
+	// Setup recording
+	initial begin
+		$dumpfile("hub75_top_tb.vcd");
+		$dumpvars(0,hub75_top_tb);
+	end
+
+	// Reset pulse
+	initial begin
+		# 200 rst = 0;
+		# 10000000 $finish;
+	end
+
+	// Clocks
+	always #33 clk = !clk;	// ~ 30 MHz
+
+	// DUT
+	hub75_top #(
+		.N_BANKS(N_BANKS),
+		.N_ROWS(N_ROWS),
+		.N_COLS(N_COLS),
+		.N_CHANS(N_CHANS),
+		.N_PLANES(N_PLANES),
+		.BITDEPTH(BITDEPTH)
+	) dut_I (
+		.hub75_addr(hub75_addr),
+		.hub75_data(hub75_data),
+		.hub75_clk(hub75_clk),
+		.hub75_le(hub75_le),
+		.hub75_blank(hub75_blank),
+		.fbw_bank_addr(fbw_bank_addr),
+		.fbw_row_addr(fbw_row_addr),
+		.fbw_row_store(fbw_row_store),
+		.fbw_row_rdy(fbw_row_rdy),
+		.fbw_row_swap(fbw_row_swap),
+		.fbw_data(fbw_data),
+		.fbw_col_addr(fbw_col_addr),
+		.fbw_wren(fbw_wren),
+		.frame_swap(frame_swap),
+		.frame_rdy(frame_rdy),
+		.cfg_pre_latch_len(8'h80),
+		.cfg_latch_len(8'h80),
+		.cfg_post_latch_len(8'h80),
+		.cfg_bcm_bit_len(8'h06),
+		.clk(clk),
+		.rst(rst)
+	);
+
+`ifdef PATTERN
+	pgen #(
+		.N_ROWS(N_BANKS * N_ROWS),
+		.N_COLS(N_COLS),
+		.BITDEPTH(BITDEPTH)
+	) pgen_I (
+		.fbw_row_addr({fbw_bank_addr, fbw_row_addr}),
+		.fbw_row_store(fbw_row_store),
+		.fbw_row_rdy(fbw_row_rdy),
+		.fbw_row_swap(fbw_row_swap),
+		.fbw_data(fbw_data),
+		.fbw_col_addr(fbw_col_addr),
+		.fbw_wren(fbw_wren),
+		.frame_swap(frame_swap),
+		.frame_rdy(frame_rdy),
+		.clk(clk),
+		.rst(rst)
+	);
+`else
+	vgen #(
+		.ADDR_BASE(24'h040000),
+		.N_FRAMES(30),
+		.N_ROWS(N_BANKS * N_ROWS),
+		.N_COLS(N_COLS),
+		.BITDEPTH(BITDEPTH)
+	) vgen_I (
+		.sr_addr(sr_addr),
+		.sr_len(sr_len),
+		.sr_go(sr_go),
+		.sr_rdy(sr_rdy),
+		.sr_data(sr_data),
+		.sr_valid(sr_valid),
+		.fbw_row_addr({fbw_bank_addr, fbw_row_addr}),
+		.fbw_row_store(fbw_row_store),
+		.fbw_row_rdy(fbw_row_rdy),
+		.fbw_row_swap(fbw_row_swap),
+		.fbw_data(fbw_data),
+		.fbw_col_addr(fbw_col_addr),
+		.fbw_wren(fbw_wren),
+		.frame_swap(frame_swap),
+		.frame_rdy(frame_rdy),
+		.clk(clk),
+		.rst(rst)
+	);
+
+	spi_flash_reader spi_reader_I (
+		.spi_mosi(spi_mosi),
+		.spi_miso(spi_miso),
+		.spi_cs_n(spi_cs_n),
+		.spi_clk(spi_clk),
+		.addr(sr_addr),
+		.len(sr_len),
+		.go(sr_go),
+		.rdy(sr_rdy),
+		.data(sr_data),
+		.valid(sr_valid),
+		.clk(clk),
+		.rst(rst)
+	);
+`endif
+
+endmodule // hub75_top_tb

+ 1 - 0
projects/rgb_panel/sw/control.py

@@ -0,0 +1 @@
+../../_common/control.py

+ 135 - 0
projects/rgb_panel/sw/stream.py

@@ -0,0 +1,135 @@
+#!/usr/bin/env python3
+
+import argparse
+import time
+
+import control
+
+
+class PanelControl(control.BoardControlBase):
+
+	def __init__(self, n_banks=2, n_rows=32, n_cols=64, colordepth=16, **kwargs):
+		# Super call
+		super().__init__(**kwargs)
+
+		# Save panel description
+		self.n_banks = n_banks
+		self.n_rows  = n_rows
+		self.n_cols  = n_cols
+		self.colordepth = colordepth
+
+		# Pre-create buffers
+		self.line_bytes = n_cols * (colordepth // 8)
+		self.send_buf = bytearray(1 + self.line_bytes)
+		self.send_buf_view = memoryview(self.send_buf)
+
+	def send_line_file(self, fh):
+		self.send_buf_view[0] = 0x80
+		rb = fh.readinto(self.send_buf_view[1:])
+		if rb != self.line_bytes:
+			return False
+		self.slave.exchange(self.send_buf)
+		return True
+
+	def send_line_data(self, data):
+		self.send_buf_view[0] = 0x80
+		self.send_buf_view[1:] = data
+		self.slave.exchange(self.send_buf)
+
+	def send_frame_file(self, fh):
+		# Scan all line
+		for y in range(self.n_banks * self.n_rows):
+			# Send write command to line buffer
+			if not self.send_line_file(fh):
+				return False
+
+			# Swap line buffer & Write it to line y of back frame buffer
+			self.reg_w8(0x03, y)
+
+		# Send frame swap command
+		self.reg_w8(0x04, 0x00)
+
+		# Wait for the frame swap to occur
+		while (self.read_status() & 0x02 == 0):
+			pass
+
+		return True
+
+	def send_frame_data(self, frame):
+		# View on the data
+		frame_view = memoryview(frame)
+
+		# Scan all line
+		for y in range(self.n_banks * self.n_rows):
+			# Send write command to line buffer
+			self.send_line_data(frame_view[y*self.line_bytes:(y+1)*self.line_bytes])
+
+			# Swap line buffer & Write it to line y of back frame buffer
+			self.reg_w8(0x03, y)
+
+		# Send frame swap command
+		self.reg_w8(0x04, 0x00)
+
+		# Wait for the frame swap to occur
+		while (self.read_status() & 0x02 == 0):
+			pass
+
+
+def main():
+	# Parse options
+	parser = argparse.ArgumentParser(
+			formatter_class=argparse.ArgumentDefaultsHelpFormatter
+	)
+	g_input = parser.add_argument_group('input', 'Input options')
+	g_panel = parser.add_argument_group('panel', 'Panel configuation options')
+	g_brd   = parser.add_argument_group('board', 'Board configuration options')
+
+	g_input.add_argument('--input', type=argparse.FileType('rb'), metavar='FILE', help='Input file', required=True)
+	g_input.add_argument('--fps',   type=float, help='Target FPS to regulate to (None=no regulation)')
+	g_input.add_argument('--loop',  help='Play in a loop', action='store_true', default=False)
+
+	g_panel.add_argument('--n_banks',    type=int, metavar='N', help='Number of banks',   default=2)
+	g_panel.add_argument('--n_rows',     type=int, metavar='N', help='Number of rows',    default=32)
+	g_panel.add_argument('--n_cols',     type=int, metavar='N', help='Number of columns', default=64)
+	g_panel.add_argument('--colordepth', type=int, metavar='DEPTH', help='Bit per color',     default=16)
+
+	control.arg_group_setup(g_brd)
+
+	args = parser.parse_args()
+
+	# Build the actual panel control object with those params
+	kwargs = control.arg_to_kwargs(args)
+	kwargs['n_banks']    = args.n_banks
+	kwargs['n_rows']     = args.n_rows
+	kwargs['n_cols']     = args.n_cols
+	kwargs['colordepth'] = args.colordepth
+
+	panel = PanelControl(**kwargs)
+
+	# Streaming loop
+	if args.fps:
+		tpf = 1.0 / args.fps
+		tt  = time.time() + tpf
+
+	while True:
+		# Send one frame
+		rv = panel.send_frame_file(args.input)
+
+		# Loop ?
+		if not rv:
+			if args.loop:
+				args.input.seek(0)
+				continue
+			else:
+				break
+
+		# FPS regulation
+		if args.fps:
+			w = tt - time.time()
+			if w > 0:
+				time.sleep(w)
+			tt += tpf
+
+
+if __name__ == '__main__':
+	main()