Переглянути джерело

cores/hub75: Import of the HUB75 LED Panel driver IP cores

Signed-off-by: Sylvain Munaut <tnt@246tNt.com>
Sylvain Munaut 6 роки тому
батько
коміт
84d58fd430

+ 3 - 0
cores/hub75/Makefile

@@ -0,0 +1,3 @@
+CORE := hub75
+
+include ../../build/core-rules.mk

+ 22 - 0
cores/hub75/README.md

@@ -0,0 +1,22 @@
+HUB75 LED Panel driver IP core
+==============================
+
+This core allows to drive LED panel chains using the 'classic' HUB75
+protocol. The default top level contains a frame buffer but it's also
+possible to re-use the lower level components to drive a display without
+the need for a full frame buffer and just generate the pixel data
+'just-in-time'.
+
+The LEDS are modulated using Binary Coded Modulation which allow to
+efficiently vary their intensity efficiently.
+[This video](https://www.youtube.com/watch?v=Sq8SxVDO5wE) by Mike Harisson
+explains the concept of BCM very well.
+
+The geometry of the panel and various aspecs are fully configurable through
+parameters given to the cores.
+
+See the doc/ directory for more information about the internals of this
+core.
+
+This core is licensed under the GNU Lesser General Public v3
+(see LICENSE.lgpl3)

+ 25 - 0
cores/hub75/core.mk

@@ -0,0 +1,25 @@
+CORE := hub75
+
+DEPS_hub75 := misc
+
+RTL_SRCS_hub75 := $(addprefix rtl/, \
+	hub75_bcm.v \
+	hub75_blanking.v \
+	hub75_colormap.v \
+	hub75_fb_readout.v \
+	hub75_fb_writein.v \
+	hub75_framebuffer.v \
+	hub75_gamma.v \
+	hub75_linebuffer.v \
+	hub75_scan.v \
+	hub75_shift.v \
+	hub75_top.v \
+)
+
+PREREQ_hub75 := \
+	$(BUILD_TMP)/gamma_table.hex
+
+include $(ROOT)/build/core-magic.mk
+
+$(BUILD_TMP)/gamma_table.hex: $(CORE_hub75_DIR)/sw/mkgamma.py
+	$(CORE_hub75_DIR)/sw/mkgamma.py > $@

+ 111 - 0
cores/hub75/doc/framebuffer.md

@@ -0,0 +1,111 @@
+Frame Buffer
+============
+
+The frame buffer allows to double buffer frames to be displayed on the screen.
+It allows for tear-free image update and also to decouple the input video rate
+from the actual LED panel refresh rate. It also decouples the order in which
+the pixels have to be written from the order that the panel driver needs them
+to control the LEDs.
+
+General flow is as follows :
+
+
+```
+                   ,---------,       ,--------,       ,---------,
+Host / Pixel       |  Write  |       | Frame  |       |  Read   |       Panel
+Generation   ----> | Process | ----> | Buffer | ----> | Process | ----> Driver
+                   '---------'       '--------'       '---------'
+```
+
+
+
+Write Process
+-------------
+
+The write process top level is the `hub75_fb_writein` module.
+
+```            
+            .---------------,
+From        |    Double     |      To
+Pixel  ---> |    Row        | ---> Frame
+Source      |    Buffer     |      Buffer
+            '---------------'
+                    |
+            ,---------------,
+       <--> | Control logic | <-->
+            '---------------'
+```
+
+This module contains a double row buffer.
+
+So the general usage is that the user side interface is free to write an
+entire row of pixels inside one of the row buffer (in any order and at
+any rate).
+
+When done, it can send a store command that will swap the double buffers,
+give it a new row buffer to write the next row, while in the background the
+first buffer is being written to the shared frame buffer.
+
+
+Frame Buffer
+------------
+
+The frame buffer storage element itself is based on the iCE40 SPRAMs blocks.
+Those have some limitations and thus the frame buffer logic has to contain
+logic to make the requires adaptations:
+
+ * They are single-port and so there needs to be arbitration logic between
+   the read and write side to avoid conflicts.
+
+ * Each RAM is a fixed 16 bits and so width adaptation depending on the
+   bits / pixels might be needed.
+
+ * Depending on the required size for the frame buffer, the logic will
+   automatically decide how many SPRAMs to use and how to combine them
+   (in width or depth). The resulting geometry is displayed in the debug
+   output during synthesis / simulation.
+
+All of this is handled internally in the `hub75_framebuffer` module.
+
+
+
+Read Process
+------------
+
+The read process top level is the `hub75_fb_readout`.
+
+```
+            ,--------------------------------------,
+            | Color Mapping                        |      .--------,
+From        |     ,-----------,     ,--------,     |      | Double |      To
+Frame  ---> | --> |   Bit     | --> | Gamma  |     | ---> | Row    | ---> Panel
+Buffer      |     | Expansion |     | Lookup | --> |      | Buffer |
+            |     '-----------'     '--------'     |      '--------'
+            '----------|----------------|----------'          |
+                       |                |                     |
+            ,------------------------------------------------------,
+       <--> |                   Control logic                      | <-->
+            '------------------------------------------------------'
+```
+
+This module also contains a double row buffer like the write process and the
+flow is very similar but in reverse.
+
+The 'user side' (which in this case is the core of the LED driver to pilot the
+LEDs) can request a row to be loaded from the shared frame buffer memory in
+the background. And when it's loaded, it can 'swap' the front/back buffer to
+actually read the data and it can also issue the command to load the next row.
+
+Another specificity is that this buffer loads the same row for all banks at
+a time since they will need to be send to the panels in parallel and so it
+actually buffers `N_BANKS` rows and not just one.
+
+Finally this is also during the read out that the final color mapping is done.
+This color mapping step is what converts from the `BITDEPTH` bits that have
+been provided to the user to the actual value that will be used for the BCM
+modulation of the RGB leds.
+
+This step can be modified to the user to suit their need (for instance, doing
+a palette lookup), but by default it does a bit expansion to 24 bits RGB
+(assuming a RGB332 or RGB556 or RGB888 depending on `BITDEPTH`) and then
+performs a gamma correction using a built-in LUT.

+ 7 - 0
cores/hub75/doc/hub75.json

@@ -0,0 +1,7 @@
+{signal: [
+  {name: 'addr',   wave: '4..........5........................3.', data: ['N-1', 'N', 'N+1'] },
+  {name: 'blank',  wave: '10.......1..0.1....0...1..0.......1..0' },
+  {name: 'le',     wave: '0.........10.....10.....10.........10.' },
+  {name: 'clk',    wave: 'l.p...l......p...l..p...l..p...l......p', phase: 0.5 },  
+  {name: 'data',   wave: 'x5555x......5555x..5555x..3333x......3' },
+]}

BIN
cores/hub75/doc/hub75.png


+ 100 - 0
cores/hub75/doc/overview.md

@@ -0,0 +1,100 @@
+HUB75 driver overview
+=====================
+
+Principle of operation
+----------------------
+
+An example wave form is show below :
+
+![](hub75.png)
+
+Note this is just an illustration of BCM modulation and control for these
+panels and is not a cycle accurate representation of the signals generated
+by this driver.
+
+General principle is :
+
+* Shift in the data for the new row/plane to be displayed
+* When the current row/plane display is done (i.e. shown for the right
+   amount of time):
+     * Assert the blanking signal
+     * Wait for the data shift to be done if required
+     * Use the Latch signal to transfer data from the shift register to
+       the output register.
+* De-assert the blanking signal for the required amount of time for this
+  BCM plane
+* Go to the next plane and or next row
+
+This driver supports to have a minimum bit length for the BCM to be shorter
+than the time it takes to shift in the data and it will drive the blanking
+signal accordingly. The compromise in this case is the light efficiency is
+slightly reduced in favor of an increased refresh rate.
+
+A tool to compute the timings you can achieve is provided in the `sw/`
+subdirectory. And the length of the minimum BCM plane is controlled by
+the `cfg_bcm_bit_len` signal of `hub75_top`.
+
+
+Modules
+-------
+
+A quick list of the modules and what they do :
+
+* Frame buffer logic : Described in more details [here](framebuffer.md)
+    * `hub75_fb_readout`
+    * `hub75_fb_writein`
+    * `hub75_framebuffer`
+    * `hub75_linebuffer`
+
+* Color Mapping : Converts from `BITDEPTH` wide words to actual values used in BCM for each color
+    * `hub75_colormap`
+    * `hub75_gamma`
+
+* Low-level BCM driver
+    * `hub75_bcm`
+    * `hub75_blanking`
+    * `hub75_scan`
+    * `hub75_shift`
+
+* Top level from driver with frame buffer
+    * `hub75_top`
+
+
+Main high-level driver
+----------------------
+
+The module `hub75_top` provides an example on how to use the low level blocks
+to make a full driver that uses a frame buffer and exposes a convenient
+interface to write data and request frame swap.
+
+It is fully configurable for various geometry of panels using the parameters :
+
+* `N_BANKS`: How many rows are shifted/displayed in parallel
+* `N_ROWS`: The number of rows in each bank (i.e. the number of multiplexed rows)
+* `N_COLS`: The total number of columns in the chain of panels
+* `N_CHANS`: How many channels, in general this is 3 for RGB panels
+* `N_PLANES`: How many bitplanes are modulated using BCM
+* `BITDEPTH`: The width of each pixel color when fed in and store in the frame buffer
+
+
+Low-level BCM driver
+--------------------
+
+The low level modules can be used independently of the frame buffer if you
+need to drive panels without a frame buffer. The way to use and connect them
+is illustrated in the `hub75_top` module.
+
+You will then need to be able to provide the pixel values on demand. You
+can also just use a row buffer to make this easier (since the low level driver
+needs random access in rows without delays, it's easier to at least have
+a row buffer).
+
+* `hub75_scan`: This is sort of the high level control and coordinates the
+                scanning of all the rows in the panel.
+* `hub75_bcm`: This controls the BCM modulation for each row following the
+               commands from the `scan` module. It handles shifting the data,
+	       controling the blanking and properly enabling the row driver.
+* `hub75_blanking`: This is the submodule that specifically handled the blanking
+                    signal control for each BCM plane.
+* `hub75_shift`: This is the submodule that specifically handles the data
+                 shifting.

+ 235 - 0
cores/hub75/rtl/hub75_bcm.v

@@ -0,0 +1,235 @@
+/*
+ * hub75_bcm.v
+ *
+ * Copyright (C) 2019  Sylvain Munaut <tnt@246tNt.com>
+ * All rights reserved.
+ *
+ * LGPL v3+, see LICENSE.lgpl3
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * vim: ts=4 sw=4
+ */
+
+`default_nettype none
+
+module hub75_bcm #(
+	parameter integer N_ROWS   = 32,
+	parameter integer N_PLANES = 8,
+
+	// Auto-set
+	parameter integer LOG_N_ROWS  = $clog2(N_ROWS)
+)(
+	// Hub75 interface
+	output wire [LOG_N_ROWS-1:0] hub75_addr,
+	output wire hub75_le,
+
+	// Shifter interface
+	output wire [N_PLANES-1:0] shift_plane,
+	output wire shift_go,
+	input  wire shift_rdy,
+
+	// Blanking interface
+	output wire [N_PLANES-1:0] blank_plane,
+	output wire blank_go,
+	input  wire blank_rdy,
+
+	// Control
+	input  wire [LOG_N_ROWS-1:0] ctrl_row,
+	input  wire ctrl_go,
+	output wire ctrl_rdy,
+
+	// Config
+	input  wire [7:0] cfg_pre_latch_len,
+	input  wire [7:0] cfg_latch_len,
+	input  wire [7:0] cfg_post_latch_len,
+
+	// Clock / Reset
+	input  wire clk,
+	input  wire rst
+);
+
+	genvar i;
+
+	// Signals
+	// -------
+
+	// FSM
+	localparam
+		ST_IDLE				= 0,
+		ST_SHIFT			= 1,
+		ST_WAIT_TO_LATCH	= 2,
+		ST_PRE_LATCH		= 3,
+		ST_DO_LATCH			= 4,
+		ST_POST_LATCH		= 5,
+		ST_ISSUE_BLANK		= 6;
+
+	reg  [2:0] fsm_state;
+	reg  [2:0] fsm_state_next;
+
+	reg  [7:0] timer_val;
+	wire timer_trig;
+
+	reg  [N_PLANES-1:0] plane;
+	wire plane_last;
+
+	reg  [LOG_N_ROWS-1:0] addr;
+	reg  [LOG_N_ROWS-1:0] addr_out;
+	wire addr_ce;
+	wire le;
+
+
+	// FSM
+	// ---
+
+	// State register
+	always @(posedge clk or posedge rst)
+		if (rst)
+			fsm_state <= ST_IDLE;
+		else
+			fsm_state <= fsm_state_next;
+
+	// Next-State logic
+	always @(*)
+	begin
+		// Default is to not move
+		fsm_state_next = fsm_state;
+
+		// Transitions ?
+		case (fsm_state)
+			ST_IDLE:
+				if (ctrl_go)
+					fsm_state_next = ST_SHIFT;
+
+			ST_SHIFT:
+				fsm_state_next = ST_WAIT_TO_LATCH;
+
+			ST_WAIT_TO_LATCH:
+				if (shift_rdy & blank_rdy)
+					fsm_state_next = ST_PRE_LATCH;
+
+			ST_PRE_LATCH:
+				if (timer_trig)
+					fsm_state_next = ST_DO_LATCH;
+
+			ST_DO_LATCH:
+				if (timer_trig)
+					fsm_state_next = ST_POST_LATCH;
+
+			ST_POST_LATCH:
+				if (timer_trig)
+					fsm_state_next = ST_ISSUE_BLANK;
+
+			ST_ISSUE_BLANK:
+				fsm_state_next = plane_last ? ST_IDLE : ST_SHIFT;
+		endcase
+	end
+
+
+	// Timer
+	// -----
+
+	always @(posedge clk)
+	begin
+		if (fsm_state != fsm_state_next) begin
+			// Default is to trigger all the time
+			timer_val <= 8'h80;
+
+			// Preload for next state
+			case (fsm_state_next)
+				ST_PRE_LATCH:	timer_val <= cfg_pre_latch_len;
+				ST_DO_LATCH:	timer_val <= cfg_latch_len;
+				ST_POST_LATCH:	timer_val <= cfg_post_latch_len;
+			endcase
+		end else begin
+			timer_val <= timer_val - 1;
+		end
+	end
+
+	assign timer_trig = timer_val[7];
+
+
+	// Plane counter
+	// -------------
+
+	always @(posedge clk)
+		if (fsm_state == ST_IDLE)
+			plane <= { {(N_PLANES-1){1'b0}}, 1'b1 };
+		else if (fsm_state == ST_ISSUE_BLANK)
+			plane <= { plane[N_PLANES-2:0], 1'b0 };
+
+	assign plane_last = plane[N_PLANES-1];
+
+
+	// External Control
+	// ----------------
+
+	// Shifter
+	assign shift_plane = plane;
+	assign shift_go = (fsm_state == ST_SHIFT);
+
+	// Blanking
+	assign blank_plane = plane;
+	assign blank_go = (fsm_state == ST_ISSUE_BLANK);
+
+	// Address
+	always @(posedge clk)
+		if (ctrl_go)
+			addr <= ctrl_row;
+
+	always @(posedge clk)
+		if (fsm_state == ST_DO_LATCH)
+			addr_out <= addr;
+
+	// Latch
+	assign le = (fsm_state == ST_DO_LATCH);
+
+	// Ready ?
+	assign ctrl_rdy = (fsm_state == ST_IDLE);
+
+
+	// IOBs
+	// ----
+
+	// Address
+	generate
+		for (i=0; i<LOG_N_ROWS; i=i+1)
+			SB_IO #(
+				.PIN_TYPE(6'b010100),
+				.PULLUP(1'b0),
+				.NEG_TRIGGER(1'b0),
+				.IO_STANDARD("SB_LVCMOS")
+			) iob_addr_I (
+				.PACKAGE_PIN(hub75_addr[i]),
+				.CLOCK_ENABLE(1'b1),
+				.OUTPUT_CLK(clk),
+				.D_OUT_0(addr_out[i])
+			);
+	endgenerate
+
+	// Latch
+	SB_IO #(
+		.PIN_TYPE(6'b010100),
+		.PULLUP(1'b0),
+		.NEG_TRIGGER(1'b0),
+		.IO_STANDARD("SB_LVCMOS")
+	) iob_le_I (
+		.PACKAGE_PIN(hub75_le),
+		.CLOCK_ENABLE(1'b1),
+		.OUTPUT_CLK(clk),
+		.D_OUT_0(le)
+	);
+
+endmodule // hub75_bcm

+ 101 - 0
cores/hub75/rtl/hub75_blanking.v

@@ -0,0 +1,101 @@
+/*
+ * hub75_blanking.v
+ *
+ * Copyright (C) 2019  Sylvain Munaut <tnt@246tNt.com>
+ * All rights reserved.
+ *
+ * LGPL v3+, see LICENSE.lgpl3
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * vim: ts=4 sw=4
+ */
+
+`default_nettype none
+
+module hub75_blanking #(
+	parameter integer N_PLANES = 8
+)(
+	// Hub75 interface
+	output wire hub75_blank,
+
+	// Control
+	input  wire [N_PLANES-1:0] ctrl_plane,
+	input  wire ctrl_go,
+	output wire ctrl_rdy,
+
+	// Config
+	input  wire [7:0] cfg_bcm_bit_len,
+
+	// Clock / Reset
+	input  wire clk,
+	input  wire rst
+);
+
+	// Signals
+	// -------
+
+	wire active;
+	wire plane_cnt_ce;
+	reg [N_PLANES:0] plane_cnt;
+	reg [7:0] bit_cnt;
+	wire bit_cnt_trig;
+
+
+	// Control
+	// -------
+
+	// Active
+	assign active = plane_cnt[N_PLANES];
+
+	// Plane length counter
+	always @(posedge clk or posedge rst)
+		if (rst)
+			plane_cnt <= 0;
+		else if (plane_cnt_ce)
+			plane_cnt <= (ctrl_go ? { 1'b1, ctrl_plane } : plane_cnt) - 1;
+
+	assign plane_cnt_ce = (bit_cnt_trig & active) | ctrl_go;
+
+	// Base len bit counter
+	always @(posedge clk)
+		if (~active | bit_cnt_trig)
+			bit_cnt <= cfg_bcm_bit_len;
+		else
+			bit_cnt <= bit_cnt - 1;
+
+	assign bit_cnt_trig = bit_cnt[7];
+
+	// Ready
+	assign ctrl_rdy = ~active;
+
+
+	// IOBs
+	// ----
+
+	// Blanking
+	SB_IO #(
+		.PIN_TYPE(6'b010100),
+		.PULLUP(1'b0),
+		.NEG_TRIGGER(1'b0),
+		.IO_STANDARD("SB_LVCMOS")
+	) iob_blank_I (
+		.PACKAGE_PIN(hub75_blank),
+		.CLOCK_ENABLE(1'b1),
+		.OUTPUT_CLK(clk),
+		.D_OUT_0(~active)
+	);
+
+endmodule // hub75_blanking

+ 144 - 0
cores/hub75/rtl/hub75_colormap.v

@@ -0,0 +1,144 @@
+/*
+ * hub75_colormap.v
+ *
+ * Copyright (C) 2019  Sylvain Munaut <tnt@246tNt.com>
+ * All rights reserved.
+ *
+ * LGPL v3+, see LICENSE.lgpl3
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * vim: ts=4 sw=4
+ */
+
+`default_nettype none
+
+module hub75_colormap #(
+	parameter integer N_CHANS  = 3,
+	parameter integer N_PLANES = 8,
+	parameter integer BITDEPTH = 24,
+	parameter integer USER_WIDTH = 1
+)(
+	// Input pixel
+	input  wire [BITDEPTH-1:0] in_data,
+	input  wire [USER_WIDTH-1:0] in_user,
+	input  wire in_valid,
+	output reg  in_ready,
+
+	// Output pixel
+	output wire [(N_CHANS*N_PLANES)-1:0] out_data,
+	output wire [USER_WIDTH-1:0] out_user,
+	output reg  out_valid,
+
+	// Clock / Reset
+	input  wire clk,
+	input  wire rst
+);
+	// Signals
+	// -------
+
+	wire [7:0] c0;
+	wire [7:0] c1;
+	wire [7:0] c2;
+	reg  [7:0] cmux;
+
+	reg  [1:0] cnt;
+
+	wire [N_PLANES-1:0] do;
+	reg  [N_PLANES-1:0] do_r[0:1];
+
+
+	// Control
+	// -------
+
+		// Little note: Technically we need only 3 cycles to lookup the
+		// 3 colors. But to avoid having some registers to pipeline the 'user'
+		// data, we take 4 cycles. It doesn't matter anyway since we'll have
+		// to wait to pipe the data to the LCD anyway, so 'wasting' a cycle
+		// here has no consequence.
+
+	// Cycle counter
+	always @(posedge clk)
+		cnt <= in_valid ? (cnt + 1) : 2'b00;
+
+
+	// Input stage
+	// -----------
+
+	// Map color channels
+	generate
+		if (BITDEPTH == 24) begin
+			assign c2 = in_data[23:16];
+			assign c1 = in_data[15: 8];
+			assign c0 = in_data[ 7: 0];
+		end else if (BITDEPTH == 16) begin
+			assign c2 = { in_data[15:11], in_data[15:13] };
+			assign c1 = { in_data[10: 5], in_data[10: 9] };
+			assign c0 = { in_data[ 4: 0], in_data[ 4: 2] };
+		end else if (BITDEPTH == 8) begin
+			assign c2 = { {2{in_data[7:5]}}, in_data[7:6] };
+			assign c1 = { {2{in_data[4:2]}}, in_data[4:3] };
+			assign c0 = { {4{in_data[1:0]}} };
+		end
+	endgenerate
+
+	// Mux
+	always @(*)
+		case (cnt)
+			2'b00: cmux = c0;
+			2'b01: cmux = c1;
+			2'b10: cmux = c2;
+			default: cmux = 8'hx;
+		endcase
+
+	// When are we ready
+	always @(posedge clk)
+		in_ready <= (cnt == 2'b10);
+
+
+	// Gamma LUT
+	// ---------
+
+	hub75_gamma #(
+		.IW(8),
+		.OW(N_PLANES)
+	) gamma_lut_I (
+		.in(cmux),
+		.out(do),
+		.enable(1'b1),
+		.clk(clk)
+	);
+
+
+	// Output stage
+	// ------------
+
+	// Data
+	always @(posedge clk)
+	begin
+		do_r[1] <= do;
+		do_r[0] <= do_r[1];
+	end
+
+	assign out_data = { do, do_r[1], do_r[0] };
+
+	// User infos
+	assign out_user = in_user;
+
+	// Valid signal
+	always @(posedge clk)
+		out_valid <= (cnt == 2'b10);
+
+endmodule // hub75_colormap

+ 288 - 0
cores/hub75/rtl/hub75_fb_readout.v

@@ -0,0 +1,288 @@
+/*
+ * hub75_fb_readout.v
+ *
+ * Copyright (C) 2019  Sylvain Munaut <tnt@246tNt.com>
+ * All rights reserved.
+ *
+ * LGPL v3+, see LICENSE.lgpl3
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * vim: ts=4 sw=4
+ */
+
+`default_nettype none
+
+module hub75_fb_readout #(
+	parameter integer N_BANKS  = 2,
+	parameter integer N_ROWS   = 32,
+	parameter integer N_COLS   = 64,
+	parameter integer N_CHANS  = 3,
+	parameter integer N_PLANES = 8,
+	parameter integer BITDEPTH = 24,
+	parameter integer FB_AW    = 13,
+	parameter integer FB_DW    = 16,
+	parameter integer FB_DC    = 2,
+
+	// Auto-set
+	parameter integer LOG_N_BANKS = $clog2(N_BANKS),
+	parameter integer LOG_N_ROWS  = $clog2(N_ROWS),
+	parameter integer LOG_N_COLS  = $clog2(N_COLS)
+)(
+	// Read interface - Preload
+	input  wire [LOG_N_ROWS-1:0] rd_row_addr,
+	input  wire rd_row_load,
+	output wire rd_row_rdy,
+	input  wire rd_row_swap,
+
+	// Read interface - Access
+	output wire [(N_BANKS * N_CHANS * N_PLANES)-1:0] rd_data,
+	input  wire [LOG_N_COLS-1:0] rd_col_addr,
+	input  wire rd_en,
+
+	// Read Out - Control
+	output wire ctrl_req,
+	input  wire ctrl_gnt,
+	output reg  ctrl_rel,
+
+	// Read Out - Frame Buffer Access
+	output wire [FB_AW-1:0] fb_addr,
+	input  wire [FB_DW-1:0] fb_data,
+
+	// Clock / Reset
+	input  wire clk,
+	input  wire rst
+);
+
+	// Counter = [ col_addr : bank_addr : dc_idx ]
+	localparam integer CS1 = $clog2(FB_DC);
+	localparam integer CS2 = CS1 + LOG_N_BANKS;
+	localparam integer CW  = CS2 + LOG_N_COLS;
+
+
+	// Signals
+	// -------
+
+	// Read-out process
+	reg  rop_buf;
+
+	reg  rop_pending;
+	reg  rop_running;
+	reg  rop_ready;
+
+	reg [LOG_N_ROWS-1:0] rop_row_addr;
+
+	reg [CW-1:0] rop_cnt;
+	reg rop_last;
+
+	wire rop_move;
+	wire rop_done;
+
+	// Frame buffer access
+	wire fb_rden;
+
+	reg  fb_rden_r;
+	reg  [FB_DW-1:0] fb_data_save;
+	wire [FB_DW-1:0] fb_data_mux;
+	reg  [(FB_DC*FB_DW)-1:0] fb_data_ext;
+
+	// Color Mapper
+	reg  [CW-CS1-1:0] cm_in_user_addr_pre;
+	reg  cm_in_user_last_pre;
+	reg  cm_in_valid_pre;
+
+	wire [BITDEPTH-1:0] cm_in_data;
+	reg  [CW-CS1-1:0] cm_in_user_addr;
+	reg  cm_in_user_last;
+	reg  cm_in_valid;
+	wire cm_in_ready;
+
+	wire [(N_CHANS*N_PLANES)-1:0] cm_out_data;
+	wire [CW-CS1-1:0] cm_out_user_addr;
+	wire cm_out_user_last;
+	wire cm_out_valid;
+
+	// Line buffer access
+	wire [(N_BANKS * N_CHANS * N_PLANES)-1:0] rolb_wr_data;
+	wire [N_BANKS-1:0] rolb_wr_mask;
+	wire [LOG_N_COLS-1:0] rolb_wr_addr;
+	wire rolb_wr_ena;
+
+
+	// Control
+	// -------
+
+	// Buffer swap
+	always @(posedge clk or posedge rst)
+		if (rst)
+			rop_buf <= 1'b0;
+		else
+			rop_buf <= rop_buf ^ rd_row_swap;
+
+	// Track status and requests
+	always @(posedge clk or posedge rst)
+		if (rst) begin
+			rop_pending <= 1'b0;
+			rop_running <= 1'b0;
+			rop_ready   <= 1'b0;
+		end else begin
+			rop_pending <= (rop_pending & ~ctrl_gnt) |  rd_row_load;
+			rop_running <= (rop_running & ~rop_done) |  ctrl_gnt;
+			rop_ready   <= (rop_ready   |  rop_done) & ~rd_row_load;
+		end
+
+	// Arbiter interface
+	assign ctrl_req = rop_pending;
+
+	always @(posedge clk)
+		ctrl_rel <= cm_out_valid & cm_out_user_last;
+
+	// Read interface
+	assign rd_row_rdy = rop_ready;
+
+	// Latch row address
+	always @(posedge clk)
+		if (rd_row_load)
+			rop_row_addr <= rd_row_addr;
+
+	// Counter
+	always @(posedge clk or negedge rop_running)
+		if (~rop_running) begin
+			rop_cnt  <= 0;
+			rop_last <= 1'b0;
+		end else if (rop_move) begin
+			rop_cnt  <= rop_cnt + 1;
+			rop_last <= rop_cnt == ((N_COLS << CS2) - 2);
+		end
+
+	assign rop_done = rop_last & rop_move;
+
+	// Move pipeline ahead
+	assign rop_move = ~cm_in_valid | cm_in_ready;
+
+
+	// Line buffer
+	// -----------
+
+	hub75_linebuffer #(
+		.N_WORDS(N_BANKS),
+		.WORD_WIDTH(N_CHANS * N_PLANES),
+		.ADDR_WIDTH(1 + LOG_N_COLS)
+	) readout_buf_I (
+		.wr_addr({~rop_buf, rolb_wr_addr}),
+		.wr_data(rolb_wr_data),
+		.wr_mask(rolb_wr_mask),
+		.wr_ena(rolb_wr_ena),
+		.rd_addr({rop_buf, rd_col_addr}),
+		.rd_data(rd_data),
+		.rd_ena(rd_en),
+		.clk(clk)
+	);
+
+
+	// Frame buffer -> Color mapper
+	// ----------------------------
+
+	// Frame buffer read
+	assign fb_addr = { rop_row_addr, rop_cnt };
+	assign fb_rden = rop_move;
+
+	// Simulate a 'READ ENABLE' on the frame buffer by saving the previous
+	// data and muxing
+	always @(posedge clk)
+		fb_rden_r <= fb_rden;
+
+	always @(posedge clk)
+		if (fb_rden_r)
+			fb_data_save <= fb_data;
+
+	assign fb_data_mux = fb_rden_r ? fb_data : fb_data_save;
+
+	// Shift register of frame buffer words to reconstruct and entire
+	// 'BITDEPTH' worth of bits.
+	always @(posedge clk)
+		if (rop_move)
+			if (FB_DC > 1)
+				fb_data_ext <= { fb_data_mux, fb_data_ext[(FB_DC*FB_DW)-1:FB_DW] };
+			else
+				fb_data_ext <= { fb_data_mux };
+
+	// Map to the color mapper input
+	assign cm_in_data = fb_data_ext[BITDEPTH-1:0];
+
+	always @(posedge clk or posedge rst)
+		if (rst) begin
+			cm_in_valid_pre <= 1'b0;
+			cm_in_valid <= 1'b0;
+		end else if (rop_move) begin
+			if (CS1 > 0)
+				cm_in_valid_pre <= rop_running & &rop_cnt[CS1-1:0];
+			else
+				cm_in_valid_pre <= rop_running;
+
+			cm_in_valid <= cm_in_valid_pre;
+		end
+
+	always @(posedge clk)
+		if (rop_move) begin
+			// This is synced with the RAM output
+			cm_in_user_addr_pre <= rop_cnt[CW-1:CS1];
+			cm_in_user_last_pre <= rop_last;
+
+			// This is synced with the fb_data_ext signal
+			cm_in_user_addr <= cm_in_user_addr_pre;
+			cm_in_user_last <= cm_in_user_last_pre;
+		end
+
+
+	// Color mapping core
+	// ------------------
+
+	hub75_colormap #(
+		.N_CHANS(N_CHANS),
+		.N_PLANES(N_PLANES),
+		.BITDEPTH(BITDEPTH),
+		.USER_WIDTH(CW-CS1+1)
+	) cm_I (
+		.in_data(cm_in_data),
+		.in_user({cm_in_user_addr, cm_in_user_last}),
+		.in_valid(cm_in_valid),
+		.in_ready(cm_in_ready),
+		.out_data(cm_out_data),
+		.out_user({cm_out_user_addr, cm_out_user_last}),
+		.out_valid(cm_out_valid),
+		.clk(clk),
+		.rst(rst)
+	);
+
+
+	// Color mapper -> Line buffer
+	// ---------------------------
+
+	genvar i;
+
+	assign rolb_wr_data = { (N_BANKS){cm_out_data} };
+	assign rolb_wr_addr = cm_out_user_addr[CW-CS1-1:CS2-CS1];
+	assign rolb_wr_ena  = cm_out_valid;
+
+	generate
+		if (N_BANKS > 1)
+			for (i=0; i<N_BANKS; i=i+1)
+				assign rolb_wr_mask[i] = (cm_out_user_addr[CS2-CS1-1:0] == i);
+		else
+			assign rolb_wr_mask = 1'b1;
+	endgenerate
+
+endmodule // hub75_fb_readout

+ 201 - 0
cores/hub75/rtl/hub75_fb_writein.v

@@ -0,0 +1,201 @@
+/*
+ * hub75_fb_writein.v
+ *
+ * Copyright (C) 2019  Sylvain Munaut <tnt@246tNt.com>
+ * All rights reserved.
+ *
+ * LGPL v3+, see LICENSE.lgpl3
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * vim: ts=4 sw=4
+ */
+
+`default_nettype none
+
+module hub75_fb_writein #(
+	parameter integer N_BANKS  = 2,
+	parameter integer N_ROWS   = 32,
+	parameter integer N_COLS   = 64,
+	parameter integer BITDEPTH = 24,
+	parameter integer FB_AW    = 13,
+	parameter integer FB_DW    = 16,
+	parameter integer FB_DC    = 2,
+
+	// Auto-set
+	parameter integer LOG_N_BANKS = $clog2(N_BANKS),
+	parameter integer LOG_N_ROWS  = $clog2(N_ROWS),
+	parameter integer LOG_N_COLS  = $clog2(N_COLS)
+)(
+	// Write interface - Row store/swap
+	input  wire [LOG_N_BANKS-1:0] wr_bank_addr,
+	input  wire [LOG_N_ROWS-1:0]  wr_row_addr,
+	input  wire wr_row_store,
+	output wire wr_row_rdy,
+	input  wire wr_row_swap,
+
+	// Write interface - Access
+	input  wire [BITDEPTH-1:0] wr_data,
+	input  wire [LOG_N_COLS-1:0] wr_col_addr,
+	input  wire wr_en,
+
+	// Write In - Control
+	output wire ctrl_req,
+	input  wire ctrl_gnt,
+	output reg  ctrl_rel,
+
+	// Write In - Frame Buffer Access
+	output wire [FB_AW-1:0] fb_addr,
+	output wire [FB_DW-1:0] fb_data,
+	output wire fb_wren,
+
+	// Clock / Reset
+	input  wire clk,
+	input  wire rst
+);
+
+	// Counter = [ col_addr : dc_idx ]
+	localparam integer CS = $clog2(FB_DC);
+	localparam integer CW = LOG_N_COLS + CS;
+
+
+	// Signals
+	// -------
+
+	// Write-in process
+	reg  wip_buf;
+
+	reg  wip_pending;
+	reg  wip_running;
+	reg  wip_ready;
+
+	reg  [LOG_N_BANKS-1:0] wip_bank_addr;
+	reg  [LOG_N_ROWS-1:0]  wip_row_addr;
+
+	reg  [CW-1:0] wip_cnt;
+	reg  wip_last;
+
+	// Line buffer access
+	wire [LOG_N_COLS-1:0] wilb_col_addr;
+	wire [BITDEPTH-1:0] wilb_data;
+	wire wilb_rden;
+
+	wire [FB_DW*FB_DC-1:0] wilb_data_ext;
+
+	// Frame buffer access
+	reg  [FB_AW-1:0] fb_addr_i;
+	reg  fb_wren_i;
+
+
+	// Control
+	// -------
+
+	// Buffer swap
+	always @(posedge clk or posedge rst)
+		if (rst)
+			wip_buf <= 1'b0;
+		else
+			wip_buf <= wip_buf ^ wr_row_swap;
+
+	// Track status and requests
+	always @(posedge clk or posedge rst)
+		if (rst) begin
+			wip_pending <= 1'b0;
+			wip_running <= 1'b0;
+			wip_ready   <= 1'b1;
+		end else begin
+			wip_pending <= (wip_pending & ~ctrl_gnt) |  wr_row_store;
+			wip_running <= (wip_running & ~wip_last) |  ctrl_gnt;
+			wip_ready   <= (wip_ready   |  wip_last) & ~wr_row_store;
+		end
+
+	// Arbiter interface
+	assign ctrl_req = wip_pending;
+
+	always @(posedge clk)
+		ctrl_rel <= wip_last;
+
+	// Write interface
+	assign wr_row_rdy = wip_ready;
+
+	// Latch bank/row address
+	always @(posedge clk)
+		if (wr_row_store) begin
+			wip_bank_addr <= wr_bank_addr;
+			wip_row_addr  <= wr_row_addr;
+		end
+
+	// Counter
+	always @(posedge clk)
+		if (~wip_running) begin
+			wip_cnt  <= 0;
+			wip_last <= 1'b0;
+		end else begin
+			wip_cnt  <= wip_cnt + 1;
+			wip_last <= wip_cnt == ((N_COLS << CS) - 2);
+		end
+
+
+	// Line buffer
+	// -----------
+
+	hub75_linebuffer #(
+		.N_WORDS(1),
+		.WORD_WIDTH(BITDEPTH),
+		.ADDR_WIDTH(1 + LOG_N_COLS)
+	) writein_buf_I (
+		.wr_addr({~wip_buf, wr_col_addr}),
+		.wr_data(wr_data),
+		.wr_mask(1'b1),
+		.wr_ena(wr_en),
+		.rd_addr({wip_buf, wilb_col_addr}),
+		.rd_data(wilb_data),
+		.rd_ena(wilb_rden),
+		.clk(clk)
+	);
+
+
+	// Line buffer -> Frame buffer
+	// ---------------------------
+
+	// Line buffer read
+	assign wilb_col_addr = wip_cnt[CW-1:CS];
+	assign wilb_rden = wip_running;
+
+	// Route data from frame buffer to line buffer
+		// Extend it to a multiple of the frame buffer data width
+	assign wilb_data_ext = { {(FB_DW*FB_DC-BITDEPTH){1'b0}}, wilb_data };
+
+		// Mux
+	generate
+		if (CS > 0)
+			assign fb_data = wilb_data_ext[FB_DW*fb_addr_i[CS-1:0]+:FB_DW];
+		else
+			assign fb_data = wilb_data_ext;
+	endgenerate
+
+	// Sync FB command with the read data from line buffer (1 cycle delay)
+	always @(posedge clk)
+	begin
+		fb_addr_i[FB_AW-1:CS] <= { wip_row_addr, wip_cnt[CW-1:CS], wip_bank_addr };
+		if (CS > 0)
+			fb_addr_i[CS-1:0] <= wip_cnt[CS-1:0];
+		fb_wren_i <= wip_running;
+	end
+
+	assign fb_addr = fb_addr_i;
+	assign fb_wren = fb_wren_i;
+
+endmodule // hub75_fb_writein

+ 370 - 0
cores/hub75/rtl/hub75_framebuffer.v

@@ -0,0 +1,370 @@
+/*
+ * hub75_framebuffer.v
+ *
+ * Copyright (C) 2019  Sylvain Munaut <tnt@246tNt.com>
+ * All rights reserved.
+ *
+ * LGPL v3+, see LICENSE.lgpl3
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * vim: ts=4 sw=4
+ */
+
+`default_nettype none
+
+module hub75_framebuffer #(
+	parameter integer N_BANKS  = 2,
+	parameter integer N_ROWS   = 32,
+	parameter integer N_COLS   = 64,
+	parameter integer N_CHANS  = 3,
+	parameter integer N_PLANES = 8,
+	parameter integer BITDEPTH = 24,
+
+	// Auto-set
+	parameter integer LOG_N_BANKS = $clog2(N_BANKS),
+	parameter integer LOG_N_ROWS  = $clog2(N_ROWS),
+	parameter integer LOG_N_COLS  = $clog2(N_COLS)
+)(
+	// Write interface - Row store/swap
+	input  wire [LOG_N_BANKS-1:0] wr_bank_addr,
+	input  wire [LOG_N_ROWS-1:0]  wr_row_addr,
+	input  wire wr_row_store,
+	output wire wr_row_rdy,
+	input  wire wr_row_swap,
+
+	// Write interface - Access
+	input  wire [BITDEPTH-1:0] wr_data,
+	input  wire [LOG_N_COLS-1:0] wr_col_addr,
+	input  wire wr_en,
+
+	// Read interface - Preload
+	input  wire [LOG_N_ROWS-1:0] rd_row_addr,
+	input  wire rd_row_load,
+	output wire rd_row_rdy,
+	input  wire rd_row_swap,
+
+	// Read interface - Access
+	output wire [(N_BANKS * N_CHANS * N_PLANES)-1:0] rd_data,
+	input  wire [LOG_N_COLS-1:0] rd_col_addr,
+	input  wire rd_en,
+
+	// Frame swap request
+	input  wire frame_swap,
+
+	// Clock / Reset
+	input  wire clk,
+	input  wire rst
+);
+	// Internal params
+	// ---------------
+		// This tries to come up with the best memory layout and sets up
+		// a bunch of constants appropriately
+		//
+		// Address seen from access PoV ( fb_addr signal ) :
+		//            1 : Buffer Select
+		//  LOG_N_BANKS : Bank selection
+		//  LOG_N_ROWS  : Row address
+		//  LOG_N_COLS  : Column address
+		//  LOG_FB_DC   : Index of the word that compose the full color data
+		//                (when framebuffer width is thinner than BITDEPTH)
+		//  -------------
+		//  FB_AW       : (total number of bit in that address)
+		//
+		// To map this to the memory address ( mem_addr signal ):
+		//  * Add PAD_BITS at the MSBs (just in case we use less than 1 SPRAM)
+		//  * Drop OMUX_BITS + IMUX_BITS at the LSBs
+		//    - IMUX_BITS used for muxing down the 16 bit wide bus down to
+		//      FB_DW if needed
+		//    - OMUX_BITS used to mux between the SPRAMs used in // to
+		//      increase the total memory depth
+
+	`define MIN(_a, _b) ((_a) < (_b) ? (_a) : (_b))
+	`define MAX(_a, _b) ((_a) > (_b) ? (_a) : (_b))
+
+	// Round bitdepth to a power of 2 with minimum of 4
+	localparam integer LOG_BITDEPTH = (BITDEPTH > 4) ? $clog2(BITDEPTH) : 2;
+
+	// Number of SPRAM needed for frame buffer
+	localparam integer LOG_SPRAM_COUNT = `MAX(0, (1 + LOG_N_BANKS + LOG_N_ROWS + LOG_N_COLS + LOG_BITDEPTH) - 18);
+	localparam integer SPRAM_COUNT = 1 << LOG_SPRAM_COUNT;
+
+	// Width of the framebuffer access bus
+	localparam integer FB_DW = `MIN((16 * SPRAM_COUNT), (1 << LOG_BITDEPTH));
+
+	// Number of SPRAM used in 'width-mode'
+	localparam integer LOG_SPRAM_WIDE = $clog2(`MAX(FB_DW,16)) - 4;
+	localparam integer SPRAM_WIDE = 1 << LOG_SPRAM_WIDE;
+
+	// Number of SPRAM used in 'depth-mode'
+	localparam integer LOG_SPRAM_DEEP = LOG_SPRAM_COUNT - LOG_SPRAM_WIDE;
+	localparam integer SPRAM_DEEP = 1 << LOG_SPRAM_DEEP;
+
+	// Number of framebuffer words for each pixel
+	localparam integer LOG_FB_DC = LOG_BITDEPTH - $clog2(FB_DW);
+	localparam integer FB_DC = 1 << LOG_FB_DC;
+
+	// Framebuffer final address width
+	localparam integer FB_AW = 1 + LOG_N_BANKS + LOG_N_ROWS + LOG_N_COLS + LOG_FB_DC;
+
+	// Zero-bits to MSB pad SPRAM address (if using less than 1 SPRAM)
+	localparam integer PAD_BITS = `MAX(0, 18 - (1 + LOG_N_BANKS + LOG_N_ROWS + LOG_N_COLS + LOG_BITDEPTH));
+
+	// Number of bits used for muxing inside the wide memory bus down to FB_DW
+	localparam integer IMUX_BITS = $clog2(`MAX(1, 16 / FB_DW));
+
+	// Number of bits used for muxing between the SPRAM used in // to increase depth
+	localparam integer OMUX_BITS = LOG_SPRAM_DEEP;
+
+	initial begin
+		$display("Hub75 Frame Buffer config :");
+		$display(" - SPRAM_COUNT : %d", SPRAM_COUNT);
+		$display(" - SPRAM_WIDE  : %d", SPRAM_WIDE);
+		$display(" - SPRAM_DEEP  : %d", SPRAM_DEEP);
+		$display(" - FB_AW       : %d", FB_AW);
+		$display(" - FB_DW       : %d", FB_DW);
+		$display(" - FB_DC       : %d", FB_DC);
+		$display(" - PAD_BITS    : %d", PAD_BITS);
+		$display(" - IMUX_BITS   : %d", IMUX_BITS);
+		$display(" - OMUX_BITS   : %d", OMUX_BITS);
+	end
+
+
+	// Signals
+	// -------
+
+	// Arbitration logic
+	reg  arb_busy;
+	reg  arb_prio;
+
+	// Write-in control
+	wire wi_req;
+	reg  wi_gnt;
+	wire wi_rel;
+
+	// Read-out control
+	wire ro_req;
+	reg  ro_gnt;
+	wire ro_rel;
+
+	// Raw signals from the storage cells
+	wire [16*SPRAM_WIDE-1:0] mem_di;
+	wire [16*SPRAM_WIDE-1:0] mem_do [0:SPRAM_DEEP-1];
+	wire [13:0] mem_addr;
+	wire [ 3:0] mem_mask;
+	wire mem_wren [0:SPRAM_DEEP-1];
+
+	wire [16*SPRAM_WIDE-1:0] mem_do_mux;
+
+
+	// Frame buffer access
+	wire [FB_DW-1:0] fb_di;
+	wire [FB_DW-1:0] fb_do;
+	wire [FB_AW-1:0] fb_addr;
+	wire fb_wren;
+
+	reg  [FB_AW-1:0] fb_addr_r;
+	reg  fb_pingpong;
+
+	// Write-in frame buffer access
+	wire [FB_AW-2:0] wifb_addr;
+	wire [FB_DW-1:0] wifb_data;
+	wire wifb_wren;
+
+	// Read-out frame-buffer access
+	wire [FB_AW-2:0] rofb_addr;
+	wire [FB_DW-1:0] rofb_data;
+
+
+	// Control
+	// -------
+
+	// Arbitration logic
+	always @(posedge clk or posedge rst)
+	begin
+		if (rst) begin
+			arb_prio <= 1'b0;
+			arb_busy <= 1'b0;
+			wi_gnt   <= 1'b0;
+			ro_gnt   <= 1'b0;
+		end else begin
+			arb_busy <= (arb_busy | wi_req | ro_req) & ~(wi_rel | ro_rel);
+			arb_prio <= (wi_gnt | ro_gnt) ? ro_gnt  : arb_prio;
+			wi_gnt   <= ~arb_busy & wi_req & (~ro_req |  arb_prio);
+			ro_gnt   <= ~arb_busy & ro_req & (~wi_req | ~arb_prio);
+		end
+	end
+
+	// Double-Buffer
+	always @(posedge clk or posedge rst)
+		if (rst)
+			fb_pingpong <= 1'b0;
+		else
+			fb_pingpong <= fb_pingpong ^ frame_swap;
+
+	// Shared access
+		// We assume users as well behaved and just use wren for mux control
+	assign fb_di = wifb_data;
+	assign rofb_data = fb_do;
+	assign fb_addr = wifb_wren ? { ~fb_pingpong, wifb_addr } : { fb_pingpong, rofb_addr };
+	assign fb_wren = wifb_wren;
+
+
+	// Storage
+	// -------
+
+	genvar i, j;
+
+	// Generate memory elements
+	generate
+		for (i=0; i<SPRAM_DEEP; i=i+1)
+		begin
+			for (j=0; j<SPRAM_WIDE; j=j+1)
+			begin
+
+				SB_SPRAM256KA mem_I (
+					.DATAIN(mem_di[16*j+15:16*j]),
+					.ADDRESS(mem_addr),
+					.MASKWREN(mem_mask),
+					.WREN(mem_wren[i]),
+					.CHIPSELECT(1'b1),
+					.CLOCK(clk),
+					.STANDBY(1'b0),
+					.SLEEP(1'b0),
+					.POWEROFF(1'b1),
+					.DATAOUT(mem_do[i][16*j+15:16*j])
+				);
+
+			end
+		end
+	endgenerate
+
+	// Register address to have it available for muxing
+	always @(posedge clk)
+		fb_addr_r <= fb_addr;
+
+	// Map fb_addr -> mem_addr
+	assign mem_addr = { {(PAD_BITS){1'b0}}, fb_addr[FB_AW-1:OMUX_BITS+IMUX_BITS] };
+
+	// Output muxing
+	generate
+		// Mux across the SPRAM used in parallel for depth (if needed)
+		if (OMUX_BITS > 0)
+			assign mem_do_mux = mem_do[fb_addr_r[OMUX_BITS+IMUX_BITS-1:IMUX_BITS]];
+		else
+			assign mem_do_mux = mem_do[0];
+
+		// Mux down to FB_DW (if needed)
+		if (IMUX_BITS > 0)
+			assign fb_do = mem_do_mux[FB_DW*fb_addr_r[IMUX_BITS-1:0]+:FB_DW];
+		else
+			assign fb_do = mem_do_mux;
+	endgenerate
+
+	// Map fb_di -> mem_di
+	generate
+		for (i=0; i<(1<<IMUX_BITS); i=i+1)
+			assign mem_di[FB_DW*i+:FB_DW] = fb_di;
+	endgenerate
+
+	// Input masking / write-enables
+	generate
+		// Write Enable
+		if (OMUX_BITS > 0)
+			for (i=0; i<SPRAM_DEEP; i=i+1)
+				assign mem_wren[i] = fb_wren & (fb_addr[IMUX_BITS+:OMUX_BITS] == i);
+		else
+			assign mem_wren[0] = fb_wren;
+
+		// Mask nibbles (if needed)
+		if (IMUX_BITS == 2)
+			assign mem_mask = {
+				fb_addr[1:0] == 2'b11,
+				fb_addr[1:0] == 2'b10,
+				fb_addr[1:0] == 2'b01,
+				fb_addr[1:0] == 2'b00
+			};
+		else if (IMUX_BITS == 1)
+			assign mem_mask = {
+				 fb_addr[0],  fb_addr[0],
+				~fb_addr[0], ~fb_addr[0]
+			};
+		else
+			assign mem_mask = 4'hf;
+	endgenerate
+
+
+	// Write-in
+	// --------
+
+	hub75_fb_writein #(
+		.N_BANKS(N_BANKS),
+		.N_ROWS(N_ROWS),
+		.N_COLS(N_COLS),
+		.BITDEPTH(BITDEPTH),
+		.FB_AW(FB_AW-1),
+		.FB_DW(FB_DW),
+		.FB_DC(FB_DC)
+	) writein_I (
+		.wr_bank_addr(wr_bank_addr),
+		.wr_row_addr(wr_row_addr),
+		.wr_row_store(wr_row_store),
+		.wr_row_rdy(wr_row_rdy),
+		.wr_row_swap(wr_row_swap),
+		.wr_data(wr_data),
+		.wr_col_addr(wr_col_addr),
+		.wr_en(wr_en),
+		.ctrl_req(wi_req),
+		.ctrl_gnt(wi_gnt),
+		.ctrl_rel(wi_rel),
+		.fb_addr(wifb_addr),
+		.fb_data(wifb_data),
+		.fb_wren(wifb_wren),
+		.clk(clk),
+		.rst(rst)
+	);
+
+
+	// Read-out
+	// --------
+
+	hub75_fb_readout #(
+		.N_BANKS(N_BANKS),
+		.N_ROWS(N_ROWS),
+		.N_COLS(N_COLS),
+		.N_CHANS(N_CHANS),
+		.N_PLANES(N_PLANES),
+		.BITDEPTH(BITDEPTH),
+		.FB_AW(FB_AW-1),
+		.FB_DW(FB_DW),
+		.FB_DC(FB_DC)
+	) readout_I (
+		.rd_row_addr(rd_row_addr),
+		.rd_row_load(rd_row_load),
+		.rd_row_rdy(rd_row_rdy),
+		.rd_row_swap(rd_row_swap),
+		.rd_data(rd_data),
+		.rd_col_addr(rd_col_addr),
+		.rd_en(rd_en),
+		.ctrl_req(ro_req),
+		.ctrl_gnt(ro_gnt),
+		.ctrl_rel(ro_rel),
+		.fb_addr(rofb_addr),
+		.fb_data(rofb_data),
+		.clk(clk),
+		.rst(rst)
+	);
+
+endmodule // hub75_framebuffer

+ 59 - 0
cores/hub75/rtl/hub75_gamma.v

@@ -0,0 +1,59 @@
+/*
+ * hub75_gamma.v
+ *
+ * Copyright (C) 2019  Sylvain Munaut <tnt@246tNt.com>
+ * All rights reserved.
+ *
+ * LGPL v3+, see LICENSE.lgpl3
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * vim: ts=4 sw=4
+ */
+
+`default_nettype none
+
+module hub75_gamma #(
+	parameter IW = 8,
+	parameter OW = 10
+)(
+	input  wire [IW-1:0] in,
+	output wire [OW-1:0] out,
+	input  wire enable,
+	input  wire clk
+);
+	reg  [15:0] gamma_rom [0:255];
+	wire [ 7:0] rd_addr;
+	reg  [15:0] rd_data;
+
+	initial
+		$readmemh("gamma_table.hex", gamma_rom);
+
+	always @(posedge clk)
+	begin
+		// Read
+		if (enable)
+			rd_data <= gamma_rom[rd_addr];
+	end
+
+	genvar i;
+	generate
+		for (i=0; i<8; i=i+1)
+			assign rd_addr[7-i] = in[IW-1-(i%IW)];
+	endgenerate
+
+	assign out = rd_data[15:16-OW];
+
+endmodule // hub75_gamma

+ 66 - 0
cores/hub75/rtl/hub75_linebuffer.v

@@ -0,0 +1,66 @@
+/*
+ * hub75_linebuffer.v
+ *
+ * Copyright (C) 2019  Sylvain Munaut <tnt@246tNt.com>
+ * All rights reserved.
+ *
+ * LGPL v3+, see LICENSE.lgpl3
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * vim: ts=4 sw=4
+ */
+
+`default_nettype none
+
+module hub75_linebuffer #(
+	parameter N_WORDS = 1,
+	parameter WORD_WIDTH = 24,
+	parameter ADDR_WIDTH = 6
+)(
+	input  wire [ADDR_WIDTH-1:0] wr_addr,
+	input  wire [(N_WORDS*WORD_WIDTH)-1:0] wr_data,
+	input  wire [N_WORDS-1:0] wr_mask,
+	input  wire wr_ena,
+
+	input  wire [ADDR_WIDTH-1:0] rd_addr,
+	output reg  [(N_WORDS*WORD_WIDTH)-1:0] rd_data,
+	input  wire rd_ena,
+
+	input  wire clk
+);
+	integer i;
+	reg [(N_WORDS*WORD_WIDTH)-1:0] ram [(1<<ADDR_WIDTH)-1:0];
+
+`ifdef SIM
+	initial
+		for (i=0; i<(1<<ADDR_WIDTH); i=i+1)
+			ram[i] = 0;
+`endif
+
+	always @(posedge clk)
+	begin
+		// Read
+		if (rd_ena)
+			rd_data <= ram[rd_addr];
+
+		// Write
+		if (wr_ena)
+			for (i=0; i<N_WORDS; i=i+1)
+				if (wr_mask[i])
+					ram[wr_addr][((i+1)*WORD_WIDTH)-1 -: WORD_WIDTH] <= wr_data[((i+1)*WORD_WIDTH)-1 -: WORD_WIDTH];
+	end
+
+endmodule // hub75_linebuffer

+ 135 - 0
cores/hub75/rtl/hub75_scan.v

@@ -0,0 +1,135 @@
+/*
+ * hub75_scan.v
+ *
+ * Copyright (C) 2019  Sylvain Munaut <tnt@246tNt.com>
+ * All rights reserved.
+ *
+ * LGPL v3+, see LICENSE.lgpl3
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * vim: ts=4 sw=4
+ */
+
+`default_nettype none
+
+module hub75_scan #(
+	parameter integer N_ROWS   = 32,
+
+	// Auto-set
+	parameter integer LOG_N_ROWS  = $clog2(N_ROWS)
+)(
+	// BCM interface
+	output wire [LOG_N_ROWS-1:0] bcm_row,
+	output wire bcm_go,
+	input  wire bcm_rdy,
+
+	// Frame buffer read interface
+	output wire [LOG_N_ROWS-1:0] fb_row_addr,
+	output wire fb_row_load,	// Back-buffer load request
+	input  wire fb_row_rdy,		// Back-buffer loaded
+	output wire fb_row_swap,	// Buffer swap
+
+	// Control
+	input  wire ctrl_go,
+	output wire ctrl_rdy,
+
+	// Clock / Reset
+	input  wire clk,
+	input  wire rst
+);
+
+	// Signals
+	// -------
+
+	// FSM
+	localparam
+		ST_IDLE		= 0,	// Idle
+		ST_LOAD		= 1,	// Load back-buffer with next-row
+		ST_WAIT		= 2,	// Wait for back-buffer & BCM to be ready
+		ST_PAINT	= 3;	// Swap buffer, issue BCM paint, go to next row
+
+	reg [1:0] fsm_state;
+	reg [1:0] fsm_state_next;
+
+	// Row counter
+	reg [LOG_N_ROWS-1:0] row;
+	reg row_last;
+
+
+	// FSM
+	// ---
+
+	// State register
+	always @(posedge clk or posedge rst)
+		if (rst)
+			fsm_state <= ST_IDLE;
+		else
+			fsm_state <= fsm_state_next;
+
+	// Next-State logic
+	always @(*)
+	begin
+		// Default is to not move
+		fsm_state_next = fsm_state;
+
+		// Transitions ?
+		case (fsm_state)
+			ST_IDLE:
+				if (ctrl_go)
+					fsm_state_next = ST_LOAD;
+
+			ST_LOAD:
+				fsm_state_next = ST_WAIT;
+
+			ST_WAIT:
+				if (bcm_rdy & fb_row_rdy)
+					fsm_state_next = ST_PAINT;
+
+			ST_PAINT:
+				fsm_state_next = row_last ? ST_IDLE : ST_LOAD;
+		endcase
+	end
+
+
+	// Row counter
+	// -----------
+
+	always @(posedge clk)
+		if (fsm_state == ST_IDLE) begin
+			row <= 0;
+			row_last <= 1'b0;
+		end else if (fsm_state == ST_PAINT) begin
+			row <= row + 1;
+			row_last <= (row == {{(LOG_N_ROWS-1){1'b1}}, 1'b0});
+		end
+
+
+	// External interfaces
+	// -------------------
+
+	// BCM
+	assign bcm_row = row;
+	assign bcm_go  = (fsm_state == ST_PAINT);
+
+	// Frame Buffer pre loader
+	assign fb_row_addr = row;
+	assign fb_row_load = (fsm_state == ST_LOAD);
+	assign fb_row_swap = (fsm_state == ST_PAINT);
+
+	// Ready signal
+	assign ctrl_rdy = (fsm_state == ST_IDLE);
+
+endmodule // hub75_scan

+ 154 - 0
cores/hub75/rtl/hub75_shift.v

@@ -0,0 +1,154 @@
+/*
+ * hub75_shift.v
+ *
+ * Copyright (C) 2019  Sylvain Munaut <tnt@246tNt.com>
+ * All rights reserved.
+ *
+ * LGPL v3+, see LICENSE.lgpl3
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * vim: ts=4 sw=4
+ */
+
+`default_nettype none
+
+module hub75_shift #(
+	parameter integer N_BANKS  = 2,
+	parameter integer N_COLS   = 64,
+	parameter integer N_CHANS  = 3,
+	parameter integer N_PLANES = 8,
+
+	// Auto-set
+	parameter integer LOG_N_COLS  = $clog2(N_COLS)
+)(
+	// Hub75 interface
+	output wire [(N_BANKS*N_CHANS)-1:0] hub75_data,
+	output wire hub75_clk,
+
+	// RAM interface
+	input  wire [(N_BANKS*N_CHANS*N_PLANES)-1:0] ram_data,
+	output wire [LOG_N_COLS-1:0] ram_col_addr,
+	output wire ram_rden,
+
+	// Control
+	input  wire [N_PLANES-1:0] ctrl_plane,
+	input  wire ctrl_go,
+	output wire ctrl_rdy,
+
+	// Clock / Reset
+	input  wire clk,
+	input  wire rst
+);
+
+	genvar i;
+
+	// Signals
+	// -------
+
+	reg active_0;
+	reg active_1;
+	reg active_2;
+	reg active_3;
+	reg [LOG_N_COLS:0] cnt_0;
+	reg cnt_last_0;
+
+	wire [(N_BANKS*N_CHANS)-1:0] ram_data_bit;
+	reg  [(N_BANKS*N_CHANS)-1:0] data_2;
+
+
+	// Control logic
+	// -------------
+
+	// Active / Valid flag
+	always @(posedge clk or posedge rst)
+		if (rst) begin
+			active_0 <= 1'b0;
+			active_1 <= 1'b0;
+			active_2 <= 1'b0;
+			active_3 <= 1'b0;
+		end else begin
+			active_0 <= (active_0 & ~cnt_last_0) | ctrl_go;
+			active_1 <= active_0;
+			active_2 <= active_1;
+			active_3 <= active_2;
+		end
+
+	// Counter
+	always @(posedge clk)
+		if (ctrl_go) begin
+			cnt_0 <= 0;
+			cnt_last_0 <= 1'b0;
+		end else if (active_0) begin
+			cnt_0 <= cnt_0 + 1;
+			cnt_last_0 <= (cnt_0 == (N_COLS - 2));
+		end
+
+	// Ready ?
+	assign ctrl_rdy = ~active_0;
+
+
+	// Data path
+	// ---------
+
+	// RAM access
+	assign ram_rden = active_0;
+	assign ram_col_addr = cnt_0[LOG_N_COLS-1:0];
+
+	// Data plane mux
+	generate
+		for (i=0; i<(N_BANKS*N_CHANS); i=i+1)
+			assign ram_data_bit[i] = |(ram_data[((i+1)*N_PLANES)-1:i*N_PLANES] & ctrl_plane);
+	endgenerate
+
+	// Mux register
+	always @(posedge clk)
+		data_2 <= ram_data_bit;
+
+
+	// IOBs
+	// ----
+
+	// Data lines
+	generate
+		for (i=0; i<(N_BANKS*N_CHANS); i=i+1)
+			SB_IO #(
+				.PIN_TYPE(6'b010100),
+				.PULLUP(1'b0),
+				.NEG_TRIGGER(1'b0),
+				.IO_STANDARD("SB_LVCMOS")
+			) iob_data_I (
+				.PACKAGE_PIN(hub75_data[i]),
+				.CLOCK_ENABLE(1'b1),
+				.OUTPUT_CLK(clk),
+				.D_OUT_0(data_2[i])
+			);
+	endgenerate
+
+	// Clock DDR register
+    SB_IO #(
+        .PIN_TYPE(6'b010000),
+        .PULLUP(1'b0),
+        .NEG_TRIGGER(1'b0),
+        .IO_STANDARD("SB_LVCMOS")
+    ) iob_clk_I (
+        .PACKAGE_PIN(hub75_clk),
+        .CLOCK_ENABLE(1'b1),
+        .OUTPUT_CLK(clk),
+        .D_OUT_0(1'b0),
+        .D_OUT_1(active_3)	// Falling edge, so need one more delay so it's not too early !
+    );
+
+endmodule // hub75_shift

+ 230 - 0
cores/hub75/rtl/hub75_top.v

@@ -0,0 +1,230 @@
+/*
+ * hub75_top.v
+ *
+ * Copyright (C) 2019  Sylvain Munaut <tnt@246tNt.com>
+ * All rights reserved.
+ *
+ * LGPL v3+, see LICENSE.lgpl3
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * vim: ts=4 sw=4
+ */
+
+`default_nettype none
+
+module hub75_top #(
+	parameter integer N_BANKS  = 2,		// # of parallel readout rows
+	parameter integer N_ROWS   = 32,	// # of rows (must be power of 2!!!)
+	parameter integer N_COLS   = 64,	// # of columns
+	parameter integer N_CHANS  = 3,		// # of data channel
+	parameter integer N_PLANES = 8,		// # bitplanes
+	parameter integer BITDEPTH = 24,	// # bits per color
+
+	// Auto-set
+	parameter integer LOG_N_BANKS = $clog2(N_BANKS),
+	parameter integer LOG_N_ROWS  = $clog2(N_ROWS),
+	parameter integer LOG_N_COLS  = $clog2(N_COLS)
+)(
+	// Hub75 interface
+	output wire [LOG_N_ROWS-1:0] hub75_addr,
+	output wire [(N_BANKS*N_CHANS)-1:0] hub75_data,
+	output wire hub75_clk,
+	output wire hub75_le,
+	output wire hub75_blank,
+
+	// Frame Buffer write interface
+		// Row store/swap
+	input  wire [LOG_N_BANKS-1:0] fbw_bank_addr,
+	input  wire [LOG_N_ROWS-1:0]  fbw_row_addr,
+	input  wire fbw_row_store,
+	output wire fbw_row_rdy,
+	input  wire fbw_row_swap,
+
+		// Line buffer access
+	input  wire [BITDEPTH-1:0] fbw_data,
+	input  wire [LOG_N_COLS-1:0] fbw_col_addr,
+	input  wire fbw_wren,
+
+		// Frame buffer swap
+	input  wire frame_swap,
+	output wire frame_rdy,
+
+	// Config
+	input  wire [7:0] cfg_pre_latch_len,
+	input  wire [7:0] cfg_latch_len,
+	input  wire [7:0] cfg_post_latch_len,
+	input  wire [7:0] cfg_bcm_bit_len,
+
+	// Clock / Reset
+	input  wire clk,
+	input  wire rst
+);
+
+	// Signals
+	// -------
+
+	// Frame swap logic
+	reg  frame_swap_pending;
+	wire frame_swap_fb;
+
+	// Frame Buffer access
+		// Read - Back Buffer loading
+	wire [LOG_N_ROWS-1:0] fbr_row_addr;
+	wire fbr_row_load;
+	wire fbr_row_rdy;
+	wire fbr_row_swap;
+
+		// Read - Front Buffer access
+	wire [(N_BANKS*N_CHANS*N_PLANES)-1:0] fbr_data;
+	wire [LOG_N_COLS-1:0] fbr_col_addr;
+	wire fbr_rden;
+
+	// Scanning
+	wire scan_go;
+	wire scan_rdy;
+
+	// Binary Code Modulator
+	wire [LOG_N_ROWS-1:0] bcm_row;
+	wire bcm_go;
+	wire bcm_rdy;
+
+	// Shifter
+	wire [N_PLANES-1:0] shift_plane;
+	wire shift_go;
+	wire shift_rdy;
+
+	// Blanking control
+	wire [N_PLANES-1:0] blank_plane;
+	wire blank_go;
+	wire blank_rdy;
+
+
+	// Sub-blocks
+	// ----------
+
+	// Synchronized frame swap logic
+	always @(posedge clk or posedge rst)
+		if (rst)
+			frame_swap_pending <= 1'b0;
+		else
+			frame_swap_pending <= (frame_swap_pending & ~scan_rdy) | frame_swap;
+
+	assign frame_rdy = ~frame_swap_pending;
+	assign scan_go = scan_rdy & ~frame_swap_pending;
+	assign frame_swap_fb = frame_swap_pending & scan_rdy;
+
+
+	// Frame Buffer
+	hub75_framebuffer #(
+		.N_BANKS(N_BANKS),
+		.N_ROWS(N_ROWS),
+		.N_COLS(N_COLS),
+		.N_CHANS(N_CHANS),
+		.N_PLANES(N_PLANES),
+		.BITDEPTH(BITDEPTH)
+	) fb_I (
+		.wr_bank_addr(fbw_bank_addr),
+		.wr_row_addr(fbw_row_addr),
+		.wr_row_store(fbw_row_store),
+		.wr_row_rdy(fbw_row_rdy),
+		.wr_row_swap(fbw_row_swap),
+		.wr_data(fbw_data),
+		.wr_col_addr(fbw_col_addr),
+		.wr_en(fbw_wren),
+		.rd_row_addr(fbr_row_addr),
+		.rd_row_load(fbr_row_load),
+		.rd_row_rdy(fbr_row_rdy),
+		.rd_row_swap(fbr_row_swap),
+		.rd_data(fbr_data),
+		.rd_col_addr(fbr_col_addr),
+		.rd_en(fbr_rden),
+		.frame_swap(frame_swap_fb),
+		.clk(clk),
+		.rst(rst)
+	);
+
+	// Scan
+	hub75_scan #(
+		.N_ROWS(N_ROWS)
+	) scan_I (
+		.bcm_row(bcm_row),
+		.bcm_go(bcm_go),
+		.bcm_rdy(bcm_rdy),
+		.fb_row_addr(fbr_row_addr),
+		.fb_row_load(fbr_row_load),
+		.fb_row_rdy(fbr_row_rdy),
+		.fb_row_swap(fbr_row_swap),
+		.ctrl_go(scan_go),
+		.ctrl_rdy(scan_rdy),
+		.clk(clk),
+		.rst(rst)
+	);
+
+	// Binary Code Modulator control
+	hub75_bcm #(
+		.N_PLANES(N_PLANES)
+	) bcm_I (
+		.hub75_addr(hub75_addr),
+		.hub75_le(hub75_le),
+		.shift_plane(shift_plane),
+		.shift_go(shift_go),
+		.shift_rdy(shift_rdy),
+		.blank_plane(blank_plane),
+		.blank_go(blank_go),
+		.blank_rdy(blank_rdy),
+		.ctrl_row(bcm_row),
+		.ctrl_go(bcm_go),
+		.ctrl_rdy(bcm_rdy),
+		.cfg_pre_latch_len(cfg_pre_latch_len),
+		.cfg_latch_len(cfg_latch_len),
+		.cfg_post_latch_len(cfg_post_latch_len),
+		.clk(clk),
+		.rst(rst)
+	);
+
+	// Shifter
+	hub75_shift #(
+		.N_BANKS(N_BANKS),
+		.N_COLS(N_COLS),
+		.N_CHANS(N_CHANS),
+		.N_PLANES(N_PLANES)
+	) shift_I (
+		.hub75_data(hub75_data),
+		.hub75_clk(hub75_clk),
+		.ram_data(fbr_data),
+		.ram_col_addr(fbr_col_addr),
+		.ram_rden(fbr_rden),
+		.ctrl_plane(shift_plane),
+		.ctrl_go(shift_go),
+		.ctrl_rdy(shift_rdy),
+		.clk(clk),
+		.rst(rst)
+	);
+
+	// Blanking control
+	hub75_blanking #(
+		.N_PLANES(N_PLANES)
+	) blank_I (
+		.hub75_blank(hub75_blank),
+		.ctrl_plane(blank_plane),
+		.ctrl_go(blank_go),
+		.ctrl_rdy(blank_rdy),
+		.cfg_bcm_bit_len(cfg_bcm_bit_len),
+		.clk(clk),
+		.rst(rst)
+	);
+
+endmodule // hub75_top

+ 89 - 0
cores/hub75/sw/hub75_timing.py

@@ -0,0 +1,89 @@
+#!/usr/bin/env python3
+
+import argparse
+
+OVERHEAD = 5	# Guesstimated
+
+class PanelConfig(object):
+
+
+	def __init__(self, **kwargs):
+		params = [
+			'freq',				# Clock frequency in Hz
+			'n_banks',			# Number of banks
+			'n_rows',			# Number of rows
+			'n_cols',			# Number of columns
+			'n_planes',			# Number of bitplanes in BCM modulation
+			'bcm_lsb_len',		# Duration of the LSB of BCM modulation (in clk cycles)
+		]
+
+		for x in params:
+			setattr(self, x, kwargs.pop(x))
+
+		self._sim()
+
+	def _sim(self):
+		# Init
+		cyc_tot = 0
+		cyc_on  = 0
+
+		# Scan all place
+		for plane in range(self.n_planes):
+			# Length of the plane in clock cycle
+			len_show = self.bcm_lsb_len << plane
+
+			# Length required to do data shift for the next plane
+			len_shift = self.n_cols
+
+			# Length of this cycle is the max
+			len_plane = max(len_show, len_shift) + OVERHEAD
+
+			# Accumulate
+			cyc_tot += len_plane
+			cyc_on  += len_show
+
+		# Compute results
+		self._light_efficiency = 1.0 * cyc_on / cyc_tot
+		self._refresh_rate = self.freq / (self.n_rows * cyc_tot)
+
+	@property
+	def light_efficiency(self):
+		return self._light_efficiency
+
+	@property
+	def refresh_rate(self):
+		return self._refresh_rate
+
+
+def main():
+	# Parse options
+	parser = argparse.ArgumentParser()
+	parser.add_argument('--freq',		type=float, help='Clock frequency in Hz', default=30e6)
+	parser.add_argument('--n_banks',	type=int, required=True, metavar='N', help='Number of banks')
+	parser.add_argument('--n_rows',		type=int, required=True, metavar='N', help='Number of rows')
+	parser.add_argument('--n_cols',		type=int, required=True, metavar='N', help='Number of columns')
+	parser.add_argument('--n_planes',	type=int, required=True, metavar='N', help='Number of bitplanes in BCM modulation')
+	parser.add_argument('--bcm_min_len',type=int, metavar='CYCLES', help='Min duration of the LSB of BCM modulation (in clk cycles, default=1)', default=1)
+	parser.add_argument('--bcm_max_len',type=int, metavar='CYCLES', help='Max duration of the LSB of BCM modulation (in clk cycles, default=20)', default=20)
+	args = parser.parse_args()
+
+	# Scan various bcm_lsb_len
+	print("bcm_lsb_len\tlight_efficiency\trefresh_rate")
+	for i in range(args.bcm_min_len, args.bcm_max_len+1):
+		pc = PanelConfig(
+			freq     = args.freq,
+			n_banks  = args.n_banks,
+			n_rows   = args.n_rows,
+			n_cols   = args.n_cols,
+			n_planes = args.n_planes,
+			bcm_lsb_len = i,
+		)
+		print("%2d\t\t%4.1f\t\t\t%5.1f" % (
+			i,
+			pc.light_efficiency * 100.0,
+			pc.refresh_rate
+		))
+
+
+if __name__ == '__main__':
+	main()

+ 15 - 0
cores/hub75/sw/mkgamma.py

@@ -0,0 +1,15 @@
+#!/usr/bin/env python3
+
+import math
+
+GAMMA     =  2.0
+WIDTH_IN  =  8
+WIDTH_OUT = 16
+
+
+for iv in range(1 << WIDTH_IN):
+	ov = 1.0 * iv / ((1 << WIDTH_IN) - 1)
+	ov = math.pow(ov, GAMMA)
+	ov = ov * ((1 << WIDTH_OUT) - 1)
+	ov = round(ov)
+	print("%04x" % ov)