Browse Source

projects/nano-pmod-up5k: Initial code import of the nanoDSI pmod demo code

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

+ 36 - 0
projects/nano-pmod-up5k/Makefile

@@ -0,0 +1,36 @@
+# Project config
+PROJ = nano-pmod-up5k
+
+PROJ_DEPS := misc spi_slave
+PROJ_RTL_SRCS := $(addprefix rtl/, \
+	nano_dsi_clk.v \
+	nano_dsi_data.v \
+	pkt_fifo.v \
+	pkt_spi_write.v \
+	sysmgr.v \
+)
+PROJ_TESTBENCHES := \
+	dsi_tb  \
+	pkt_fifo_tb  \
+	pkt_spi_write_tb
+
+PROJ_TOP_SRC := rtl/top.v
+PROJ_TOP_MOD := top
+
+# Target config
+BOARD ?= icebreaker
+DEVICE = up5k
+PACKAGE = sg48
+
+NEXTPNR_ARGS = --freq 55
+
+# 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

BIN
projects/nano-pmod-up5k/data/nyan-long-bgr565.raw


BIN
projects/nano-pmod-up5k/data/nyan-long.data


BIN
projects/nano-pmod-up5k/data/nyan-square-bgr565.raw


BIN
projects/nano-pmod-up5k/data/nyan-square.data


+ 44 - 0
projects/nano-pmod-up5k/data/top-icebreaker.pcf

@@ -0,0 +1,44 @@
+# nano-pmod
+	# PMOD 1A
+set_io clk_lp 46
+set_io clk_hs_p 4
+set_io clk_hs_n 2
+set_io dat_lp 44
+set_io dat_hs_p 47
+set_io dat_hs_n 45
+set_io lcd_reset_n 48
+set_io bl_pwm 3
+
+	# PMOD 1B
+#set_io clk_lp 32
+#set_io clk_hs_p 43
+#set_io clk_hs_n 38
+#set_io dat_lp 28
+#set_io dat_hs_p 34
+#set_io dat_hs_n 31
+#set_io lcd_reset_n 36
+#set_io bl_pwm 42
+
+	# PMOD 2
+#set_io clk_lp 20
+#set_io clk_hs_p 27
+#set_io clk_hs_n 25
+#set_io dat_lp 18
+#set_io dat_hs_p 21
+#set_io dat_hs_n 19
+#set_io lcd_reset_n 23
+#set_io bl_pwm 26
+
+# SPI
+set_io spi_mosi 14
+set_io spi_miso 17
+set_io spi_cs_n 11
+set_io spi_clk 15
+
+# Leds
+set_io rgb[0] 39
+set_io rgb[1] 40
+set_io rgb[2] 41
+
+# Clock
+set_io clk_12m 35

+ 23 - 0
projects/nano-pmod-up5k/data/top-up5kbb.pcf

@@ -0,0 +1,23 @@
+# nano-pmod
+set_io clk_lp 26       # 39A
+set_io clk_hs_p 38     # 50B
+set_io clk_hs_n 42     # 51A
+set_io dat_lp 25       # 36B
+set_io dat_hs_p 36     # 48B
+set_io dat_hs_n 28     # 41A Bodge wire !!!
+set_io lcd_reset_n 27  # 38B
+set_io bl_pwm 32       # 43A
+
+# SPI
+set_io spi_mosi 17
+set_io spi_miso 14
+set_io spi_cs_n 16
+set_io spi_clk 15
+
+# Leds
+set_io rgb[0] 39
+set_io rgb[1] 40
+set_io rgb[2] 41
+
+# Clock
+set_io clk_12m 35

+ 235 - 0
projects/nano-pmod-up5k/rtl/nano_dsi_clk.v

@@ -0,0 +1,235 @@
+/*
+ * nano_dsi_clk.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 nano_dsi_clk (
+	// nano-PMOD - CLK lane
+	output wire clk_lp,
+	output wire clk_hs_p,
+	output wire clk_hs_n,
+
+	// Control interface
+	input  wire hs_req,
+	output wire hs_rdy,
+
+	// Clock/Data sync
+	output wire clk_sync,
+
+	// Config
+	input  wire [7:0] cfg_hs_prep,
+	input  wire [7:0] cfg_hs_zero,
+	input  wire [7:0] cfg_hs_trail,
+
+	// Clock / Reset
+	input  wire clk,
+	input  wire rst
+);
+
+	// Signals
+	// -------
+
+	// IO control
+	reg io_lp;
+
+	reg io_hs_active;
+	reg io_hs_bit;
+
+	// FSM
+	localparam
+		ST_LP11				= 0,
+		ST_LP00				= 1,
+		ST_HS_ZERO			= 2,
+		ST_HS_CLK			= 3,
+		ST_HS_TRAIL			= 4;
+
+	reg  [2:0] fsm_state;
+	reg  [2:0] fsm_state_next;
+
+	// Timer
+	reg  [7:0] timer_val;
+	wire timer_trig;
+
+	// Clocking
+	reg  clk_sync_i;
+
+
+	// IOBs
+	// ----
+
+	// LP bias control
+	SB_IO #(
+		.PIN_TYPE(6'b100100),
+		.PULLUP(1'b0),
+		.NEG_TRIGGER(1'b0),
+		.IO_STANDARD("SB_LVCMOS")
+	) iob_clk_lp_I (
+		.PACKAGE_PIN(clk_lp),
+		.CLOCK_ENABLE(1'b1),
+		.INPUT_CLK(1'b0),
+		.OUTPUT_CLK(clk),
+		.OUTPUT_ENABLE(1'b1),
+		.D_OUT_0(io_lp),
+		.D_OUT_1(1'b0),
+		.D_IN_0(),
+		.D_IN_1()
+	);
+
+	// HS drivers
+	SB_IO #(
+		.PIN_TYPE(6'b110000),
+		.PULLUP(1'b0),
+		.NEG_TRIGGER(1'b0),
+		.IO_STANDARD("SB_LVCMOS")
+	) iob_clk_hs_p_I (
+		.PACKAGE_PIN(clk_hs_p),
+		.CLOCK_ENABLE(1'b1),
+		.INPUT_CLK(1'b0),
+		.OUTPUT_CLK(clk),
+		.OUTPUT_ENABLE(io_hs_active),
+		.D_OUT_0(io_hs_bit),
+		.D_OUT_1(io_hs_bit),
+		.D_IN_0(),
+		.D_IN_1()
+	);
+
+	SB_IO #(
+		.PIN_TYPE(6'b110000),
+		.PULLUP(1'b0),
+		.NEG_TRIGGER(1'b0),
+		.IO_STANDARD("SB_LVCMOS")
+	) iob_clk_hs_n_I (
+		.PACKAGE_PIN(clk_hs_n),
+		.CLOCK_ENABLE(1'b1),
+		.INPUT_CLK(1'b0),
+		.OUTPUT_CLK(clk),
+		.OUTPUT_ENABLE(io_hs_active),
+		.D_OUT_0(~io_hs_bit),
+		.D_OUT_1(~io_hs_bit),
+		.D_IN_0(),
+		.D_IN_1()
+	);
+
+
+	// FSM
+	// ---
+
+	// State register
+	always @(posedge clk or posedge rst)
+		if (rst)
+			fsm_state <= ST_LP11;
+		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_LP11:
+				if (hs_req)
+					fsm_state_next = ST_LP00;
+
+			ST_LP00:
+				if (timer_trig)
+					fsm_state_next = ST_HS_ZERO;
+
+			ST_HS_ZERO:
+				if (timer_trig)
+					fsm_state_next = ST_HS_CLK;
+
+			ST_HS_CLK:
+				if (~hs_req)
+					fsm_state_next = ST_HS_TRAIL;
+
+			ST_HS_TRAIL:
+				if (timer_trig)
+					fsm_state_next = ST_LP11;
+		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_LP00:		timer_val <= cfg_hs_prep;
+				ST_HS_ZERO:		timer_val <= cfg_hs_zero;
+				ST_HS_TRAIL:	timer_val <= cfg_hs_trail;
+			endcase
+		end else begin
+			timer_val  <= timer_val - 1;
+		end
+	end
+
+	assign timer_trig = timer_val[7];
+
+
+	// Clock sync
+	// ----------
+
+	always @(posedge clk or posedge rst)
+		if (rst)
+			clk_sync_i <= 1'b0;
+		else
+			clk_sync_i <= ~clk_sync_i;
+
+	assign clk_sync = clk_sync_i;
+
+
+	// IO control
+	// ----------
+
+	always @(posedge clk)
+	begin
+		io_lp <= (fsm_state == ST_LP11);
+
+		io_hs_active <=
+			(fsm_state == ST_HS_ZERO) ||
+			(fsm_state == ST_HS_CLK) ||
+			(fsm_state == ST_HS_TRAIL);
+
+		io_hs_bit <= clk_sync && (fsm_state == ST_HS_CLK);
+	end
+
+endmodule // nano_dsi_clk

+ 272 - 0
projects/nano-pmod-up5k/rtl/nano_dsi_data.v

@@ -0,0 +1,272 @@
+/*
+ * nano_dsi_data.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 nano_dsi_data (
+	// nano-PMOD - DATA lane
+	output wire data_lp,
+	output wire data_hs_p,
+	output wire data_hs_n,
+
+	// Control/Packet interface
+	input  wire hs_start,
+	input  wire [7:0] hs_data,
+	input  wire hs_last,
+	output wire hs_ack,
+	output wire hs_rdy,
+
+	// Clock/Data sync
+	input  wire clk_sync,
+
+	// Config
+	input  wire [7:0] cfg_hs_prep,
+	input  wire [7:0] cfg_hs_zero,
+	input  wire [7:0] cfg_hs_trail,
+
+	// Clock / Reset
+	input  wire clk,
+	input  wire rst
+);
+
+	// Signals
+	// -------
+
+	// IO control
+	reg io_lp;
+
+	reg io_hs_active;
+	reg io_hs_bit;
+
+	// FSM
+	localparam
+		ST_LP11				= 0,
+		ST_LP00				= 1,
+		ST_HS_ZERO			= 2,
+		ST_HS_SYNC			= 3,
+		ST_HS_DATA			= 4,
+		ST_HS_TRAIL			= 5;
+
+	reg  [2:0] fsm_state;
+	reg  [2:0] fsm_state_next;
+
+	// Timer
+	reg  [7:0] timer_val;
+	wire timer_trig;
+
+	// Shift register
+	reg  [7:0] shift_reg;
+	reg  [3:0] shift_cnt;
+	reg  shift_last;
+
+	reg  hs_bit_final;
+
+
+	// IOBs
+	// ----
+
+	// LP drivers
+	SB_IO #(
+		.PIN_TYPE(6'b100100),
+		.PULLUP(1'b0),
+		.NEG_TRIGGER(1'b0),
+		.IO_STANDARD("SB_LVCMOS")
+	) iob_data_lp_I (
+		.PACKAGE_PIN(data_lp),
+		.CLOCK_ENABLE(1'b1),
+//		.INPUT_CLK(1'b0),
+		.OUTPUT_CLK(clk),
+		.OUTPUT_ENABLE(1'b1),
+		.D_OUT_0(io_lp),
+		.D_OUT_1(1'b0),
+		.D_IN_0(),
+		.D_IN_1()
+	);
+
+	// HS drivers
+	SB_IO #(
+		.PIN_TYPE(6'b100100),
+		.PULLUP(1'b0),
+		.NEG_TRIGGER(1'b0),
+		.IO_STANDARD("SB_LVCMOS")
+	) iob_data_hs_p_I (
+		.PACKAGE_PIN(data_hs_p),
+		.CLOCK_ENABLE(1'b1),
+//		.INPUT_CLK(1'b0),
+		.OUTPUT_CLK(clk),
+		.OUTPUT_ENABLE(io_hs_active),
+		.D_OUT_0(io_hs_bit),
+		.D_OUT_1(1'b0),
+		.D_IN_0(),
+		.D_IN_1()
+	);
+
+	SB_IO #(
+		.PIN_TYPE(6'b100100),
+		.PULLUP(1'b0),
+		.NEG_TRIGGER(1'b0),
+		.IO_STANDARD("SB_LVCMOS")
+	) iob_data_hs_n_I (
+		.PACKAGE_PIN(data_hs_n),
+		.CLOCK_ENABLE(1'b1),
+//		.INPUT_CLK(1'b0),
+		.OUTPUT_CLK(clk),
+		.OUTPUT_ENABLE(io_hs_active),
+		.D_OUT_0(~io_hs_bit),
+		.D_OUT_1(1'b0),
+		.D_IN_0(),
+		.D_IN_1()
+	);
+
+
+	// FSM
+	// ---
+
+	// State register
+	always @(posedge clk or posedge rst)
+		if (rst)
+			fsm_state <= ST_LP11;
+		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_LP11:
+				if (hs_start)
+					fsm_state_next = ST_LP00;
+
+			ST_LP00:
+				if (timer_trig)
+					fsm_state_next = ST_HS_ZERO;
+
+			ST_HS_ZERO:
+				if (timer_trig)
+					fsm_state_next = ST_HS_SYNC;
+
+			ST_HS_SYNC:
+				if (clk_sync)
+					fsm_state_next = ST_HS_DATA;
+
+			ST_HS_DATA:
+				if (shift_cnt[3] && shift_last)
+					fsm_state_next = ST_HS_TRAIL;
+
+			ST_HS_TRAIL:
+				if (timer_trig)
+					fsm_state_next = ST_LP11;
+		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_LP00:			timer_val <= cfg_hs_prep;
+				ST_HS_ZERO:			timer_val <= cfg_hs_zero;
+				ST_HS_TRAIL:		timer_val <= cfg_hs_trail;
+			endcase
+		end else begin
+			timer_val  <= timer_val - 1;
+		end
+	end
+
+	assign timer_trig = timer_val[7];
+
+
+	// Shift register
+	// --------------
+
+	always @(posedge clk)
+		if (fsm_state == ST_HS_SYNC) begin
+			shift_reg  <= 8'hB8;						// SoT
+			shift_last <= 1'b0;
+		end else if (fsm_state == ST_HS_DATA) begin
+			if (shift_cnt[3]) begin
+				shift_reg  <= hs_data;					// Load
+				shift_last <= hs_last;
+			end else begin
+				shift_reg  <= { 1'b0, shift_reg[7:1] };	// Shift LSB out
+				shift_last <= shift_last;
+			end
+		end
+
+	always @(posedge clk)
+		if ((fsm_state != ST_HS_DATA) || shift_cnt[3])
+			shift_cnt <= 4'h1;
+		else
+			shift_cnt <= shift_cnt + 1;
+
+	assign hs_ack = shift_cnt[3];
+	assign hs_rdy = (fsm_state == ST_LP11);
+
+	always @(posedge clk)
+		if (shift_cnt[3] & shift_last)
+			hs_bit_final <= ~shift_reg[0];
+
+
+	// IO control
+	// ----------
+
+	always @(posedge clk)
+	begin
+		io_lp <= (fsm_state == ST_LP11);
+
+		io_hs_active <=
+			(fsm_state == ST_HS_ZERO) ||
+			(fsm_state == ST_HS_SYNC) ||
+			(fsm_state == ST_HS_DATA) ||
+			(fsm_state == ST_HS_TRAIL);
+
+		if (fsm_state == ST_HS_DATA)
+			io_hs_bit <= shift_reg[0];
+		else if (fsm_state == ST_HS_TRAIL)
+			io_hs_bit <= hs_bit_final;
+		else
+			io_hs_bit <= 1'b0;
+	end
+
+endmodule // nano_dsi_data

+ 136 - 0
projects/nano-pmod-up5k/rtl/pkt_fifo.v

@@ -0,0 +1,136 @@
+/*
+ * pkt_fifo.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 pkt_fifo #(
+	parameter integer AWIDTH = 9
+)(
+	input  wire [7:0] wr_data,
+	input  wire wr_last,
+	input  wire wr_ena,
+	output wire full,
+
+	output wire [7:0] rd_data,
+	output wire rd_last,
+	input  wire rd_ena,
+	output wire empty,
+
+	input  wire clk,
+	input  wire rst
+);
+	// Signals
+	reg  [AWIDTH-1:0] ram_waddr;
+	wire [7:0] ram_wdata;
+	wire ram_wen;
+
+	reg  [AWIDTH-1:0] ram_raddr;
+	wire [7:0] ram_rdata;
+	wire ram_ren;
+
+	(* keep="true" *) wire [1:0] ln_mod;
+	wire [AWIDTH:0] ln_mod_ext;
+	reg  [AWIDTH:0] len_nxt;	// Length with next packet
+	reg  [AWIDTH:0] len_cur;	// Length current - 1
+	wire rd_ce;
+
+	wire valid_nxt;
+	reg  valid_out;
+
+	// Storage element
+	ram_sdp #(
+		.AWIDTH(AWIDTH),
+		.DWIDTH(8)
+	) ram_I (
+		.wr_addr(ram_waddr),
+		.wr_data(ram_wdata),
+		.wr_ena(ram_wen),
+		.rd_addr(ram_raddr),
+		.rd_data(ram_rdata),
+		.rd_ena(ram_ren),
+		.clk(clk)
+	);
+
+	// Write pointer
+	always @(posedge clk or posedge rst)
+		if (rst)
+			ram_waddr <= 0;
+		else if (wr_ena)
+			ram_waddr <= ram_waddr + 1;
+
+	// Read pointer
+	always @(posedge clk or posedge rst)
+		if (rst)
+			ram_raddr <= 0;
+		else if (rd_ce)
+			ram_raddr <= ram_raddr + 1;
+
+	// Next Length counter
+	assign ln_mod = { rd_ce & ~wr_ena, rd_ce ^ wr_ena };
+	assign ln_mod_ext = { {(AWIDTH){ln_mod[1]}}, ln_mod[0] };
+
+	always @(posedge clk or posedge rst)
+		if (rst)
+			len_nxt <= 0;
+		else
+			len_nxt <= len_nxt + ln_mod_ext;
+
+	// Length counter (readable length minus 1)
+	always @(posedge clk)
+		if (rst)
+			len_cur <= { (AWIDTH+1){1'b1} };
+		else
+			len_cur <= ((wr_ena & wr_last) ? len_nxt : len_cur) - rd_ce;
+
+	// Write logic
+	assign ram_wdata = wr_data;
+	assign ram_wen   = wr_ena;
+	assign full      = len_nxt[AWIDTH];
+
+	// Read logic
+	assign rd_data = ram_rdata;
+	assign rd_last = ~valid_nxt;
+	assign empty   = ~valid_out;
+
+	assign valid_nxt = ~len_cur[AWIDTH];
+
+	always @(posedge clk or posedge rst)
+		if (rst)
+			valid_out <= 1'b0;
+		else if (rd_ena | ~valid_out)
+			valid_out <= valid_nxt;
+
+	assign rd_ce = valid_nxt & (rd_ena | ~valid_out);
+	assign ram_ren = rd_ce;
+
+endmodule // pkt_fifo

+ 182 - 0
projects/nano-pmod-up5k/rtl/pkt_mux.v

@@ -0,0 +1,182 @@
+/*
+ * pkt_mux.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 pkt_mux #(
+	parameter integer N = 3
+)(
+	// Multiple packet FIFOs interfaces
+	input  wire [8*N-1:0] pkt_data,
+	input  wire [  N-1:0] pkt_last,
+	input  wire [  N-1:0] pkt_valid,
+	output wire [  N-1:0] pkt_ack,
+
+	// HS PHY interface
+	output wire [7:0] hs_data,
+	output wire hs_start,
+	output wire hs_last,
+
+	output wire hs_clk_req,
+	input  wire hs_clk_rdy,
+
+	// Clock / Reset
+	input  wire clk,
+	input  wire rst
+);
+
+	// Signals
+	// -------
+
+	// FSM
+	localparam
+		ST_CLK_OFF	= 0,
+		ST_CLK_BOOT	= 1,
+		ST_CLK_RUN	= 2,
+		ST_STREAM	= 3,
+		ST_EOTP		= 4;
+
+	reg  [2:0] fsm_state;
+	reg  [2:0] fsm_state_next;
+
+	// EoTp
+	reg [7:0] eotp_data;
+	reg [1:0] eotp_cnt;
+	reg eotp_last;
+
+	// HS clock
+
+
+	// FSM
+	// ---
+
+	// State register
+	always @(posedge clk)
+		if (rst)
+			fsm_state <= ST_CLK_OFF;
+		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_CLK_OFF:
+				if ( )
+					fsm_state_next = ST_STREAM;
+
+			ST_CLK_BOOT:
+				if (hs_clk_rdy)
+					fsm_state_next = ST_CLK_RUN;
+
+			ST_CLK_RUN:
+				if (hs_clk_timeout)
+					fsm_state_next = ST_CLK_OFF;
+				else if ( )
+					fsm_state_next = ST_STREAM;
+
+			ST_STREAM:
+				if ( )
+					fsm_state_next = ST_EOTP;
+
+			ST_EOTP:
+				if (hs_ack & eotp_last)
+					fsm_state_next = ST_CLK_RUN;
+		endcase
+	end
+
+
+	// EoTp logic
+	// ----------
+
+	always @(posedge clk)
+		if (fsm_state != ST_EOTP) begin
+			eotp_cnt  <= 2'b00;
+			eotp_last <= 1'b0;
+		end else if (hs_ack) begin
+			eotp_cnt  <= eotp_cnt + 1;
+			eotp_last <= (eotp_cnt == 2'b10);
+		end
+
+	always @(eotp_cnt)
+		case (eotp_cnt)
+			2'b00: eotp_data = 8'h08;
+			2'b01: eotp_data = 8'h0f;
+			2'b10: eotp_data = 8'h0f;
+			2'b11: eotp_data = 8'h01;
+		endcase
+
+
+	// HS clock
+	// --------
+
+	reg [15:0] hs_clk_timer;
+	wire hs_clk_timeout;
+
+	// Request
+	assign hs_clk_req = (fsm_state != ST_CLK_OFF);
+
+	// Time-Out
+	always @(posedge clk)
+		if (fsm_state != ST_CLK_RUN)
+			hs_clk_timer <= 0;
+		else if (~hs_clk_timeout)
+			hs_clk_timer <= hs_clk_timer + 1
+
+	assign hs_clk_timeout <= hs_clk_timer[15];
+
+
+	// Data mux
+	// --------
+
+	// "Any" valid - Is there any channel valid
+
+	// "Any Other" valid - Is there any channel valid other than the current one
+
+
+
+	input  wire [8*N-1:0] pkt_data,
+	input  wire [  N-1:0] pkt_last,
+	input  wire [  N-1:0] pkt_valid,
+	output wire [  N-1:0] pkt_ack,
+
+	// Data mux
+	// --------
+
+
+
+endmodule // pkt_mux

+ 116 - 0
projects/nano-pmod-up5k/rtl/pkt_spi_write.v

@@ -0,0 +1,116 @@
+/*
+ * pkt_spi_write.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 pkt_spi_write #(
+	parameter BASE = 8'h20
+)(
+	// SPI 'simple bus'
+	input  wire [7:0] sb_addr,
+	input  wire [7:0] sb_data,
+	input  wire sb_first,
+	input  wire sb_last,
+	input  wire sb_strobe,
+
+	// Packet FIFO write
+	output reg  [7:0] fifo_data,
+	output reg  fifo_last,
+	output reg  fifo_wren,
+	input  wire fifo_full,
+
+	// Clock / Reset
+	input  wire clk,
+	input  wire rst
+);
+	// Signals
+	reg [7:0] data;
+	reg first;
+	reg last;
+
+	reg [2:0] cnt;
+
+	reg [7:0] data_mux;
+
+	reg hit_ena;
+	reg hit_type;
+	reg hit_ext;
+
+	// Decode 'hits'
+	always @(posedge clk)
+	begin
+		hit_ena  <= sb_strobe & (sb_addr[7:1] == (BASE >> 1));
+		hit_type <= sb_addr[0] & cnt[2] & ~sb_first;
+		hit_ext  <= hit_ena & hit_type;
+	end
+
+	// Register data
+	always @(posedge clk)
+		if (sb_strobe) begin
+			data  <= sb_data;
+			first <= sb_first;
+			last  <= sb_last;
+		end
+
+	// Position counter
+	always @(posedge clk)
+		if (sb_strobe) begin
+			if (sb_first)
+				cnt <= 0;
+			else
+				cnt <= cnt + { 3'b000, ~cnt[2] };
+		end
+
+	// Data Mux
+	always @(*)
+		if (~hit_type)
+			// RAW
+			data_mux = data;
+		else if (~hit_ext)
+			// Ext First byte
+//			data_mux = { data[4:2], data[1:0], data[1:0], data[1] };
+			data_mux = { data[5:3], data[2:0], data[2:1] };
+		else
+			// Ext Second byte
+//			data_mux = { data[7:5], data[7:6], data[4:2] };
+			data_mux = { data[7:6], data[7:6], data[7], data[5:3] };
+
+	// FIFO interface
+	always @(posedge clk)
+	begin
+		fifo_data <= data_mux;
+		fifo_last <= last & (~hit_type | hit_ext);
+		fifo_wren <= hit_ena | hit_ext;
+	end
+
+endmodule // pkt_spi_write

+ 111 - 0
projects/nano-pmod-up5k/rtl/sysmgr.v

@@ -0,0 +1,111 @@
+/*
+ * 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
+`define FREQ_54M
+
+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 #(
+`ifdef FREQ_54M
+			// 54 M
+		.DIVR(4'b0000),
+		.DIVF(7'b1000111),
+		.DIVQ(3'b100),
+		.FILTER_RANGE(3'b001),
+`else
+			// 48 M
+		.DIVR(4'b0000),
+		.DIVF(7'b0111111),
+		.DIVQ(3'b100),
+		.FILTER_RANGE(3'b001),
+`endif
+		.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),
+		.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)
+		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

+ 363 - 0
projects/nano-pmod-up5k/rtl/top.v

@@ -0,0 +1,363 @@
+/*
+ * 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 NO_PLL
+
+module top (
+	// nano-PMOD
+	output wire clk_lp,
+	output wire clk_hs_p,
+	output wire clk_hs_n,
+	output wire dat_lp,
+	output wire dat_hs_p,
+	output wire dat_hs_n,
+
+	output wire lcd_reset_n,
+	output wire bl_pwm,
+
+	// SPI
+	input  wire spi_mosi,
+	output wire spi_miso,
+	input  wire spi_cs_n,
+	input  wire spi_clk,
+
+	// LED
+	output wire [2:0] rgb,
+
+	// Clock
+	input  wire clk_12m
+);
+
+	localparam integer AWIDTH = 10;
+
+	// Signals
+	// -------
+
+	// SPI 'simple-bus'
+	wire [7:0] sb_addr;
+	wire [7:0] sb_data;
+	wire sb_first;
+	wire sb_last;
+	wire sb_stb;
+
+	// SPI Packets
+	wire [7:0] spf_wr_data;
+	wire spf_wr_last;
+	wire spf_wr_ena;
+	wire spf_full;
+	wire [7:0] spf_rd_data;
+	wire spf_rd_last;
+	wire spf_rd_ena;
+	wire spf_empty;
+
+	// MIPI
+	wire [7:0] cfg_dsi_hs_prep;
+	wire [7:0] cfg_dsi_hs_zero;
+	wire [7:0] cfg_dsi_hs_trail;
+
+	wire hs_clk_req;
+	wire hs_clk_rdy;
+	wire hs_clk_sync;
+
+	wire hs_start;
+	wire [7:0] hs_data;
+	wire hs_last;
+	wire hs_ack;
+	wire hs_rdy;
+
+	// LCD Control
+	wire [15:0] cfg_lcd_csr;
+	wire bl_pwm_i;
+
+	// LED debug
+	wire [2:0] rgb_pwm;
+
+	// Clock / Reset logic
+`ifdef NO_PLL
+	reg [7:0] rst_cnt = 8'h00;
+	wire rst_i;
+`endif
+
+	wire clk;
+	wire rst;
+
+
+	// Slave 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),
+		.clk(clk),
+		.rst(rst)
+	);
+
+
+	// Packet handling
+	// ---------------
+
+	// SPI Packet writer
+	pkt_spi_write #(
+		.BASE(8'h20)
+	) write_I (
+		.sb_addr(sb_addr),
+		.sb_data(sb_data),
+		.sb_first(sb_first),
+		.sb_last(sb_last),
+		.sb_strobe(sb_stb),
+		.fifo_data(spf_wr_data),
+		.fifo_last(spf_wr_last),
+		.fifo_wren(spf_wr_ena),
+		.fifo_full(spf_full),
+		.clk(clk),
+		.rst(rst)
+	);
+
+	// SPI packet FIFO
+	pkt_fifo #(
+		.AWIDTH(AWIDTH)
+	) spi_packet_fifo_I (
+		.wr_data(spf_wr_data),
+		.wr_last(spf_wr_last),
+		.wr_ena(spf_wr_ena),
+		.full(spf_full),
+		.rd_data(spf_rd_data),
+		.rd_last(spf_rd_last),
+		.rd_ena(spf_rd_ena),
+		.empty(spf_empty),
+		.clk(clk),
+		.rst(rst)
+	);
+
+	// Packet reader
+	reg reading;
+	assign hs_start   = ~spf_empty & ~reading;
+	assign hs_data    = spf_rd_data;
+	assign hs_last    = spf_rd_last;
+	assign spf_rd_ena = hs_ack;
+
+	always @(posedge clk or posedge rst)
+		if (rst)
+			reading <= 1'b0;
+		else
+			reading <= (reading | hs_start) & ~(hs_last & hs_ack);
+
+
+	// MIPI-DSI
+	// --------
+
+	// Config registers
+	spi_reg #(
+		.ADDR(8'h10),
+		.BYTES(1)
+	) reg_dsi_hs_prep_I (
+		.addr(sb_addr),
+		.data(sb_data),
+		.first(sb_first),
+		.strobe(sb_stb),
+		.rst_val(8'h04),
+		.out_val(cfg_dsi_hs_prep),
+		.out_stb(),
+		.clk(clk),
+		.rst(rst)
+	);
+
+	spi_reg #(
+		.ADDR(8'h11),
+		.BYTES(1)
+	) reg_dsi_hs_zero_I (
+		.addr(sb_addr),
+		.data(sb_data),
+		.first(sb_first),
+		.strobe(sb_stb),
+		.rst_val(8'h04),
+		.out_val(cfg_dsi_hs_zero),
+		.out_stb(),
+		.clk(clk),
+		.rst(rst)
+	);
+
+	spi_reg #(
+		.ADDR(8'h12),
+		.BYTES(1)
+	) reg_dsi_hs_trail_I (
+		.addr(sb_addr),
+		.data(sb_data),
+		.first(sb_first),
+		.strobe(sb_stb),
+		.rst_val(8'h04),
+		.out_val(cfg_dsi_hs_trail),
+		.out_stb(),
+		.clk(clk),
+		.rst(rst)
+	);
+
+	// MIPI DSI cores
+	nano_dsi_clk dsi_clk_I (
+		.clk_lp(clk_lp),
+		.clk_hs_p(clk_hs_p),
+		.clk_hs_n(clk_hs_n),
+		.hs_req(hs_clk_req),
+		.hs_rdy(hs_clk_rdy),
+		.clk_sync(hs_clk_sync),
+		.cfg_hs_prep(cfg_dsi_hs_prep),
+		.cfg_hs_zero(cfg_dsi_hs_zero),
+		.cfg_hs_trail(cfg_dsi_hs_trail),
+		.clk(clk),
+		.rst(rst)
+	);
+
+	nano_dsi_data dsi_data_I (
+		.data_lp(dat_lp),
+		.data_hs_p(dat_hs_p),
+		.data_hs_n(dat_hs_n),
+		.hs_start(hs_start),
+		.hs_data(hs_data),
+		.hs_last(hs_last),
+		.hs_ack(hs_ack),
+		.hs_rdy(hs_rdy),
+		.clk_sync(hs_clk_sync),
+		.cfg_hs_prep(cfg_dsi_hs_prep),
+		.cfg_hs_zero(cfg_dsi_hs_zero),
+		.cfg_hs_trail(cfg_dsi_hs_trail),
+		.clk(clk),
+		.rst(rst)
+	);
+
+
+	// LCD misc
+	// --------
+
+	// Config registers
+	spi_reg #(
+		.ADDR(8'h00),
+		.BYTES(2)
+	) reg_lcd_csr_I (
+		.addr(sb_addr),
+		.data(sb_data),
+		.first(sb_first),
+		.strobe(sb_stb),
+		.rst_val(16'h000f),
+		.out_val(cfg_lcd_csr),
+		.out_stb(),
+		.clk(clk),
+		.rst(rst)
+	);
+
+	// Back Light PWM
+	pwm #(
+		.WIDTH(10)
+	) bl_pwm_I (
+		.pwm(bl_pwm_i),
+		.cfg_val(cfg_lcd_csr[9:0]),
+		.clk(clk),
+		.rst(rst)
+	);
+
+	assign bl_pwm = bl_pwm_i;
+
+	// Reset
+	assign lcd_reset_n = cfg_lcd_csr[15] ? 1'b0 : 1'bz;
+
+	// HS clock enable
+	assign hs_clk_req = cfg_lcd_csr[14];
+
+
+	// LED debug
+	// ---------
+
+	//
+	assign rgb_pwm[0] = bl_pwm_i;
+	assign rgb_pwm[1] = ~spf_empty;
+	assign rgb_pwm[2] = hs_clk_rdy;
+
+	// Driver
+	SB_RGBA_DRV #(
+		.CURRENT_MODE("0b1"),
+		.RGB0_CURRENT("0b000001"),
+		.RGB1_CURRENT("0b000001"),
+		.RGB2_CURRENT("0b000001")
+	) rgb_drv_I (
+		.RGBLEDEN(1'b1),
+		.RGB0PWM(rgb_pwm[0]),
+		.RGB1PWM(rgb_pwm[1]),
+		.RGB2PWM(rgb_pwm[2]),
+		.CURREN(1'b1),
+		.RGB0(rgb[0]),
+		.RGB1(rgb[1]),
+		.RGB2(rgb[2])
+	);
+
+
+	// 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
+

+ 127 - 0
projects/nano-pmod-up5k/sim/dsi_tb.v

@@ -0,0 +1,127 @@
+/*
+ * dsi_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 dsi_tb;
+
+	// Signals
+	reg rst = 1'b1;
+	reg clk = 1'b0;
+
+	// PHY
+	output wire clk_lp;
+	output wire clk_hs_p;
+	output wire clk_hs_n;
+	output wire data_lp;
+	output wire data_hs_p;
+	output wire data_hs_n;
+
+	// Packet interface
+	wire hs_clk_req;
+	wire hs_clk_rdy;
+	wire hs_clk_sync;
+
+	wire hs_start;
+	wire [7:0] hs_data;
+	wire hs_last;
+	wire hs_ack;
+
+	reg [7:0] cnt;
+	reg in_pkt;
+
+	// Setup recording
+	initial begin
+		$dumpfile("dsi_tb.vcd");
+		$dumpvars(0,dsi_tb);
+	end
+
+	// Reset pulse
+	initial begin
+		# 200 rst = 0;
+		# 1000000 $finish;
+	end
+
+	// Clocks
+	always #10 clk = !clk;
+
+	// DUT
+	nano_dsi_clk dsi_clk_I (
+		.clk_lp(clk_lp),
+		.clk_hs_p(clk_hs_p),
+		.clk_hs_n(clk_hs_n),
+		.hs_req(hs_clk_req),
+		.hs_rdy(hs_clk_rdy),
+		.clk_sync(hs_clk_sync),
+		.cfg_hs_prep(8'h10),
+		.cfg_hs_zero(8'h10),
+		.cfg_hs_trail(8'h10),
+		.clk(clk),
+		.rst(rst)
+	);
+	nano_dsi_data dsi_data_I (
+		.data_lp(data_lp),
+		.data_hs_p(data_hs_p),
+		.data_hs_n(data_hs_n),
+		.hs_start(hs_start),
+		.hs_data(hs_data),
+		.hs_last(hs_last),
+		.hs_ack(hs_ack),
+		.clk_sync(hs_clk_sync),
+		.cfg_hs_prep(8'h10),
+		.cfg_hs_zero(8'h10),
+		.cfg_hs_trail(8'h10),
+		.clk(clk),
+		.rst(rst)
+	);
+
+	// Packet generator
+	always @(posedge clk)
+		if (rst)
+			cnt <= 0;
+		else
+			cnt <= cnt + (!in_pkt || hs_ack);
+
+	always @(posedge clk)
+		if (rst)
+			in_pkt <= 1'b0;
+		else
+			in_pkt <= (in_pkt | hs_start) & ~(hs_last & hs_ack);
+
+	assign hs_clk_req = (cnt != 8'h00);
+	assign hs_start   = (cnt == 8'h0f);
+	assign hs_data    = cnt;
+	assign hs_last    = (cnt == 8'h1f);
+
+endmodule // dsi_tb

+ 106 - 0
projects/nano-pmod-up5k/sim/pkt_fifo_tb.v

@@ -0,0 +1,106 @@
+/*
+ * pkt_fifo_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 pkt_fifo_tb;
+
+	// Signals
+	reg rst = 1'b1;
+	reg clk = 1'b0;
+
+	wire [7:0] wr_data;
+	wire wr_last;
+	wire wr_ena;
+	wire full;
+
+	wire [7:0] rd_data;
+	wire rd_last;
+	wire rd_ena;
+	wire empty;
+
+	// Setup recording
+	initial begin
+		$dumpfile("pkt_fifo_tb.vcd");
+		$dumpvars(0,pkt_fifo_tb);
+	end
+
+	// Reset pulse
+	initial begin
+		# 200 rst = 0;
+		# 1000000 $finish;
+	end
+
+	// Clocks
+	always #10 clk = !clk;
+
+	// DUT
+	pkt_fifo #(
+		.AWIDTH(5)
+	) dut_I (
+		.wr_data(wr_data),
+		.wr_last(wr_last),
+		.wr_ena(wr_ena),
+		.full(full),
+		.rd_data(rd_data),
+		.rd_last(rd_last),
+		.rd_ena(rd_ena),
+		.empty(empty),
+		.clk(clk),
+		.rst(rst)
+	);
+
+	// Feed some data
+	assign rd_ena = r & ~empty;
+
+	reg [7:0] cnt;
+	reg r;
+
+	always @(posedge clk)
+		if (rst)
+			r <= 1'b0;
+		else
+			r <= $random & $random;
+
+	always @(posedge clk)
+		if (rst)
+			cnt <= 0;
+		else
+			cnt <= cnt + 1;
+
+	assign wr_data = cnt;
+	assign wr_last = (cnt[2:0] == 3'b111);
+	assign wr_ena  = ~full & (cnt[7:6] == 2'b01);
+
+endmodule // pkt_fifo_tb

+ 102 - 0
projects/nano-pmod-up5k/sim/pkt_spi_write_tb.v

@@ -0,0 +1,102 @@
+/*
+ * pkt_spi_write_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 pkt_spi_write_tb;
+
+	// Signals
+	reg rst = 1'b1;
+	reg clk = 1'b0;
+
+	wire [7:0] sb_addr;
+	wire [7:0] sb_data;
+	wire sb_first;
+	wire sb_last;
+	wire sb_strobe;
+
+	wire [7:0] spf_wr_data;
+	wire spf_wr_last;
+	wire spf_wr_ena;
+	wire spf_full = 1'b0;
+
+	// Setup recording
+	initial begin
+		$dumpfile("pkt_spi_write_tb.vcd");
+		$dumpvars(0,pkt_spi_write_tb);
+	end
+
+	// Reset pulse
+	initial begin
+		# 200 rst = 0;
+		# 1000000 $finish;
+	end
+
+	// Clocks
+	always #10 clk = !clk;
+
+	// DUT
+	pkt_spi_write #(
+		.BASE(8'hA4)
+	) pkt_I (
+		.sb_addr(sb_addr),
+		.sb_data(sb_data),
+		.sb_first(sb_first),
+		.sb_last(sb_last),
+		.sb_strobe(sb_strobe),
+		.fifo_data(spf_wr_data),
+		.fifo_last(spf_wr_last),
+		.fifo_wren(spf_wr_ena),
+		.fifo_full(spf_full),
+		.clk(clk),
+		.rst(rst)
+	);
+
+	// SPI data generation
+	reg [7:0] cnt;
+
+	always @(posedge clk)
+		if (rst)
+			cnt <= 0;
+		else
+			cnt <= cnt + 1;
+
+
+	assign sb_addr   = sb_strobe ? 8'ha5 : 8'hxx;
+	assign sb_data   = sb_strobe ? cnt : 8'hxx;
+	assign sb_first  = sb_strobe ? cnt[7:4] == 4'h0 : 1'bx;
+	assign sb_last   = sb_strobe ? cnt[7:4] == 4'hf : 1'bx;
+	assign sb_strobe = cnt[3:0] == 4'hf;
+
+endmodule // pkt_spi_write_tb

+ 1 - 0
projects/nano-pmod-up5k/sw/control.py

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

+ 339 - 0
projects/nano-pmod-up5k/sw/stream.py

@@ -0,0 +1,339 @@
+#!/usr/bin/env python3
+
+import argparse
+import struct
+import time
+
+from pycrc.algorithms import Crc
+
+import control
+
+
+# ---------------------------------------------------------------------------
+# DSI utilities
+# ---------------------------------------------------------------------------
+
+EOTP = bytearray([ 0x08, 0x0f, 0x0f, 0x01 ])
+
+DSI_CRC = Crc(width=16, poly=0x1021, xor_in=0xffff, xor_out=0x0000, reflect_in=True, reflect_out=True)
+
+
+def parity(x):
+	p = 0
+	while x:
+		p ^= x & 1
+		x >>= 1
+	return p
+
+def dsi_header(*data):
+	cmd = (data[2] << 16) | (data[1] << 8) | data[0]
+	ecc = 0
+	if parity(cmd & 0b111100010010110010110111): ecc |= 0x01;
+	if parity(cmd & 0b111100100101010101011011): ecc |= 0x02;
+	if parity(cmd & 0b011101001001101001101101): ecc |= 0x04;
+	if parity(cmd & 0b101110001110001110001110): ecc |= 0x08;
+	if parity(cmd & 0b110111110000001111110000): ecc |= 0x10;
+	if parity(cmd & 0b111011111111110000000000): ecc |= 0x20;
+	return bytearray(data) + bytearray([ecc])
+
+
+def dsi_crc(payload):
+	crc = DSI_CRC.bit_by_bit(bytes(payload))
+	return bytearray([ crc & 0xff, (crc >> 8) & 0xff ])
+
+
+def dcs_short_write(cmd, val=None):
+	if val is None:
+		return dsi_header(0x05, cmd, 0x00)
+	else:
+		return dsi_header(0x15, cmd, val)
+
+def dcs_long_write(cmd, data):
+	pl = bytearray([ cmd ]) + data
+	l = len(pl)
+	return dsi_header(0x39, l & 0xff, l >> 8) + pl + dsi_crc(pl)
+
+def generic_short_write(cmd, val=None):
+	if val is None:
+		return dsi_header(0x13, cmd, 0x00)
+	else:
+		return dsi_header(0x23, cmd, val)
+
+def generic_long_write(cmd, data):
+	pl = bytearray([ cmd ]) + data
+	l = len(pl)
+	return dsi_header(0x29, l & 0xff, l >> 8) + pl + dsi_crc(pl)
+
+
+
+# ---------------------------------------------------------------------------
+# nanoPMOD Board control
+# ---------------------------------------------------------------------------
+
+class DSIControl(control.BoardControlBase):
+
+	REG_LCD_CTRL = 0x00
+	REG_DSI_HS_PREP = 0x10
+	REG_DSI_HS_ZERO = 0x11
+	REG_DSI_HS_TRAIL = 0x12
+	REG_PKT_WR_DATA_RAW = 0x20
+	REG_PKT_WR_DATA_U8 = 0x21
+
+	TRANSPOSE_NONE   = 0
+	TRANSPOSE_DCS    = 1
+	TRANSPOSE_MANUAL = 2
+
+	def __init__(self, n_col=240, n_page=240, flip_col=False, flip_page=False, transpose=TRANSPOSE_NONE, **kwargs):
+		# Super call
+		super().__init__(**kwargs)
+
+		# Save params
+		self.n_col  = n_col
+		self.n_page = n_page
+		self.flip_col  = flip_col
+		self.flip_page = flip_page
+		self.transpose = transpose
+
+		# Init the LCD
+		self.init()
+
+	def init(self):
+		# Default values
+		self.backlight = 0x100
+
+		# Turn off Back Light / HS clock and assert reset
+		self.reg_w16(self.REG_LCD_CTRL, 0x8000)
+
+		# Wait a bit
+		time.sleep(0.1)
+
+		# Configure backlight and release reset
+		self.reg_w16(self.REG_LCD_CTRL, self.backlight)
+
+		# Configure DSI timings
+		self.reg_w8(self.REG_DSI_HS_PREP,  0x10)
+		self.reg_w8(self.REG_DSI_HS_ZERO,  0x18)
+		self.reg_w8(self.REG_DSI_HS_TRAIL, 0x18)
+
+		# Enable HS clock
+		self.reg_w16(self.REG_LCD_CTRL, 0x4000 | self.backlight)
+
+		# Wait a bit
+		time.sleep(0.1)
+
+		# Send DSI packets
+		self.send_dsi_pkt(
+			dcs_short_write(0x11) +			# Exist sleep
+			EOTP							# EoTp
+		)
+
+		self.send_dsi_pkt(
+			dcs_short_write(0x29) +			# Exist sleep
+			EOTP							# EoTp
+		)
+
+		mode = (
+			(0x80 if self.flip_page else 0) |
+			(0x40 if self.flip_col  else 0) |
+			(0x20 if self.transpose == DSIControl.TRANSPOSE_DCS else 0)
+		)
+
+		self.send_dsi_pkt(
+			dcs_short_write(0x11) +			# Exist sleep
+			dcs_short_write(0x29) +			# Display on
+			dcs_short_write(0x36, mode) +	# Set address mode
+			dcs_short_write(0x3a, 0x55) +	# Set pixel format
+			EOTP							# EoTp
+		)
+
+	def set_backlight(self, backlight):
+		self.backlight = backlight
+		self.reg_w16(self.REG_LCD_CTRL, 0x4000 | self.backlight)
+
+	def send_dsi_pkt(self, data):
+		self.reg_burst(self.REG_PKT_WR_DATA_RAW, data)
+
+	def set_column_address(self, sc, ec):
+		self.send_dsi_pkt(dcs_long_write(0x2a, struct.pack('>HH', sc, ec)))
+
+	def set_page_address(self, sp, ep):
+		self.send_dsi_pkt(dcs_long_write(0x2b, struct.pack('>HH', sp, ep)))
+
+	def _send_frame_normal(self, frame, bpp):
+		# Max packet size
+		mtu = 1024 - 4 - 1 - 2
+		psz = (mtu // (2 * self.n_col)) * (2 * self.n_col)
+		pcnt = (self.n_col * self.n_page * 2 + psz - 1) // psz
+
+		if bpp == 16:
+			for i in range(pcnt):
+				self.send_dsi_pkt(
+					dsi_header(0x39, (psz + 1) & 0xff, (psz + 1) >> 8) +
+					(b'\x2c' if i == 0 else b'\x3c') +
+					frame[i*psz:(i+1)*psz] +
+					b'\x00\x00'
+				)
+
+		else:
+			for i in range(pcnt):
+				self.reg_burst(self.REG_PKT_WR_DATA_U8,
+					dsi_header(0x39, (psz + 1) & 0xff, (psz + 1) >> 8) +
+					(b'\x2c' if i == 0 else b'\x3c') +
+					frame[i*(psz//2):(i+1)*(psz//2)] +
+					b'\x00'
+				)
+
+	def _send_frame_transpose_16b(self, frame):
+		# Packet size for each line
+		mtu = 1024
+		psz  = len(self._line_sel_cmd[0]) + 4 + 1 + self.n_page * 2 + 2
+		ppb  = mtu // psz
+
+		# Scan each line
+		lsz = self.n_page * 2
+		burst = []
+		bpc = 0
+
+		for y in range(self.n_col):
+			burst.append(self._line_sel_cmd[y])
+			burst.append(dsi_header(0x39, (lsz + 1) & 0xff, (lsz + 1) >> 8))
+			burst.append(b'\x2c')
+			burst.append(frame[y*lsz:(y+1)*lsz])
+			burst.append(b'\x00\x00')
+
+			bpc += 1
+			if (bpc == ppb) or (y == (self.n_col-1)):
+				self.send_dsi_pkt(b''.join(burst))
+				bpc = 0
+				burst = []
+
+	def _send_frame_transpose_8b(self, frame):
+		# No choice but to scan each line independently
+		for y in range(self.n_col):
+			# Select line
+			self.send_dsi_pkt(self._line_sel_cmd[y])
+
+			# Send data with special 8bit expand command
+			lsz = self.n_page
+
+			self.reg_burst(self.REG_PKT_WR_DATA_U8,
+				dsi_header(0x39, (lsz*2 + 1) & 0xff, (lsz*2 + 1) >> 8) +
+				b'\x2c' +
+				frame[y*lsz:(y+1)*lsz] +
+				b'\x00'
+			)
+
+	def send_frame(self, frame, bpp=16):
+		# Delegate depending on config
+		if self.transpose == DSIControl.TRANSPOSE_MANUAL:
+			# Init the command tables
+			if not hasattr(self, '_line_sel_cmd'):
+				self._line_sel_cmd = []
+				for y in range(self.n_col):
+					self._line_sel_cmd.append(
+						dcs_long_write(0x2a, struct.pack('>HH', y, y))
+					)
+
+			# In 8 bit mode, we can't combine packets, so do it all the slow way
+			if bpp == 8:
+				self._send_frame_transpose_8b(frame)
+			else:
+				self._send_frame_transpose_16b(frame)
+
+		else:
+			self._send_frame_normal(frame, bpp)
+
+
+# ---------------------------------------------------------------------------
+# Main
+# ---------------------------------------------------------------------------
+
+
+def load_bgr888_as_bgr565(filename):
+	img = open(filename,'rb').read()
+	dat = []
+
+	for i in range(len(img) // 4):
+		b = img[4*i + 0]
+		g = img[4*i + 1]
+		r = img[4*i + 2]
+
+		c  = ((r >> 3) & 0x1f) << 11;
+		c |= ((g >> 2) & 0x3f) <<  5;
+		c |= ((b >> 3) & 0x1f) <<  0;
+
+		dat.append( ((c >> 0) & 0xff) )
+		dat.append( ((c >> 8) & 0xff) )
+
+	return bytearray(dat)
+
+
+def main():
+	# Parse options
+	parser = argparse.ArgumentParser(
+			formatter_class=argparse.ArgumentDefaultsHelpFormatter
+	)
+	g_input   = parser.add_argument_group('input',   'Input options')
+	g_display = parser.add_argument_group('display', 'Display 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_input.add_argument('--bgr8',  help='Input is BGR8 instead of BGR565', action='store_true', default=False)
+
+	g_display.add_argument('--n_col',     type=int, metavar='N', help='Number of columns', default=240)
+	g_display.add_argument('--n_page',    type=int, metavar='N', help='Number of pages',   default=240)
+	g_display.add_argument('--flip_col',  help='Flip column order', action='store_true', default=False)
+	g_display.add_argument('--flip_page', help='Flip page order',   action='store_true', default=False)
+	g_display.add_argument('--transpose', help='Transpose mode', choices=['none', 'dcs', 'manual'], default='none')
+
+	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_col']     = args.n_col
+	kwargs['n_page']    = args.n_page
+	kwargs['flip_col']  = args.flip_col
+	kwargs['flip_page'] = args.flip_page
+	kwargs['transpose'] = {
+		'none'   : DSIControl.TRANSPOSE_NONE,
+		'dcs'    : DSIControl.TRANSPOSE_DCS,
+		'manual' : DSIControl.TRANSPOSE_MANUAL,
+	}[args.transpose]
+
+	ctrl = DSIControl(**kwargs)
+
+	# Streaming loop
+	if args.fps:
+		tpf = 1.0 / args.fps
+		tt  = time.time() + tpf
+
+	fsize = args.n_col * args.n_page * (1 if args.bgr8 else 2)
+
+	while True:
+		# Send one frame
+		data = args.input.read(fsize)
+		if len(data) == fsize:
+			ctrl.send_frame(data, bpp=(8 if args.bgr8 else 16))
+
+		# Loop ?
+		else:
+			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()