Finished SSD1351 emulation in FGA.

This commit is contained in:
Bruno Levy
2021-01-04 16:45:49 +01:00
parent d836bad382
commit de21540a32
19 changed files with 322 additions and 45 deletions

View File

@@ -98,7 +98,7 @@ end
assign TMDS_rgb_p[2] = TMDS_shift_red[0];
assign TMDS_rgb_p[1] = TMDS_shift_green[0];
assign TMDS_rgb_p[0] = TMDS_shift_blue[0];
assign TMDS_clock_p = pixclk;
assign TMDS_clock_p = pixclk;
// Note: what's below would not work, _p and _n sides
// require exact synchronization that could not be

View File

@@ -0,0 +1,101 @@
// Started from: https://www.fpga4fun.com/HDMI.html (c) fpga4fun.com & KNJN LLC 2013
// Added comments, adapted to ECP5 / ULX3S, made small changes here and there
//
// - Introduced some ideas from Lawrie's code here: https://github.com/lawrie/ulx3s_examples/blob/master/hdmi/
// See also https://github.com/sylefeb/Silice/tree/master/projects/hdmi_test (also based on Lawrie's code).
// - I'm not using Lawrie's "fake differential" but instead I'm using LVCMOS33D mode for the HDMI pins in ulx3S.lpf,
// that automatically generates the negative signal from the positive one.
// See: https://www.gitmemory.com/issue/YosysHQ/nextpnr/544/751511265
// See also LATTICE ECP5 and ECP5-5G sysI/O Usage Guide / Technical note
// - In Lawrie's "fake differential", there is the ODRX1F trick that makes it possible to operate at half the
// frequency for the bit clock, may be interesting/necessary to use for res higher than 640x480.
// - I've seen also some ECP5 primitives: OLVDS (A->Z,ZN) and OBCO (I->OT,OC)
// that I tried to use with the standard LVCMOS33 mode, without success.
module HDMI_test(
input pclk,
output [3:0] gpdi_dp
// Note: gpdi_dn[3:0] is generated automatically by LVCMOS33D mode in ulx3s.lpf
);
HDMI_gen hdmi_gen(
.pixclk(pclk),
.TMDS_rgb_p(gpdi_dp[2:0]),
.TMDS_clock_p(gpdi_dp[3])
);
endmodule
/*********************************************************************************/
module HDMI_gen(
input pixclk, // 25MHz
output [2:0] TMDS_rgb_p, TMDS_rgb_n, // HDMI pins: RGB
output TMDS_clock_p, TMDS_clock_n // HDMI pins: clock
);
/******** Video generation *******************************************************/
// This part is just like a VGA generator
reg [9:0] CounterX, CounterY;
reg hSync, vSync, DrawArea;
always @(posedge pixclk) DrawArea <= (CounterX<640) && (CounterY<480);
always @(posedge pixclk) CounterX <= (CounterX==799) ? 0 : CounterX+1;
always @(posedge pixclk) if(CounterX==799) CounterY <= (CounterY==524) ? 0 : CounterY+1;
always @(posedge pixclk) hSync <= (CounterX>=656) && (CounterX<752);
always @(posedge pixclk) vSync <= (CounterY>=490) && (CounterY<492);
/******** Draw something *********************************************************/
// Generate 8-bits red,green,blue signals from X and Y coordinates (the "shader")
wire [7:0] W = {8{CounterX[7:0]==CounterY[7:0]}};
wire [7:0] A = {8{CounterX[7:5]==3'h2 && CounterY[7:5]==3'h2}};
reg [7:0] red, green, blue;
always @(posedge pixclk) begin
red <= ({CounterX[5:0] & {6{CounterY[4:3]==~CounterX[4:3]}}, 2'b00} | W) & ~A;
green <= (CounterX[7:0] & {8{CounterY[6]}} | W) & ~A;
blue <= CounterY[7:0] | W | A;
end
/******** RGB TMDS encoding ***************************************************/
// Generate 10-bits TMDS red,green,blue signals. Blue embeds HSync/VSync in its
// control part.
wire [9:0] TMDS_red, TMDS_green, TMDS_blue;
TMDS_encoder encode_R(.clk(pixclk), .VD(red ), .CD(2'b00) , .VDE(DrawArea), .TMDS(TMDS_red));
TMDS_encoder encode_G(.clk(pixclk), .VD(green), .CD(2'b00) , .VDE(DrawArea), .TMDS(TMDS_green));
TMDS_encoder encode_B(.clk(pixclk), .VD(blue ), .CD({vSync,hSync}), .VDE(DrawArea), .TMDS(TMDS_blue));
/******** 125 MHz clock *******************************************************/
// This one needs some FPGA-specific specialized blocks (a PLL).
wire half_clk_TMDS; // The 125 MHz clock used if using DDR mode.
HDMI_clock hdmi_clock(.clk(pixclk), .half_hdmi_clk(half_clk_TMDS));
/******** Shifter *************************************************************/
// Serialize the three 10-bits TMDS red,green,blue signals.
// Another version of the shifter, that shifts and sends two bits per clock,
// using the ODDRX1F block of the ULX3S.
// Counter now counts modulo 5 instead of modulo 10
reg [4:0] TMDS_mod5=1;
wire TMDS_shift_load = TMDS_mod5[4];
always @(posedge half_clk_TMDS) TMDS_mod5 <= {TMDS_mod5[3:0],TMDS_mod5[4]};
// Shifter now shifts two bits at each clock
reg [9:0] TMDS_shift_red=0, TMDS_shift_green=0, TMDS_shift_blue=0;
always @(posedge half_clk_TMDS) begin
TMDS_shift_red <= TMDS_shift_load ? TMDS_red : TMDS_shift_red [9:2];
TMDS_shift_green <= TMDS_shift_load ? TMDS_green : TMDS_shift_green[9:2];
TMDS_shift_blue <= TMDS_shift_load ? TMDS_blue : TMDS_shift_blue [9:2];
end
// DDR serializers: they send D0 at the rising edge and D1 at the falling edge.
ODDRX1F ddr_red (.D0(TMDS_shift_red[0]), .D1(TMDS_shift_red[1]), .Q(TMDS_rgb_p[2]), .SCLK(half_clk_TMDS), .RST(1'b0));
ODDRX1F ddr_green(.D0(TMDS_shift_green[0]), .D1(TMDS_shift_green[1]), .Q(TMDS_rgb_p[1]), .SCLK(half_clk_TMDS), .RST(1'b0));
ODDRX1F ddr_blue (.D0(TMDS_shift_blue[0]), .D1(TMDS_shift_blue[1]), .Q(TMDS_rgb_p[0]), .SCLK(half_clk_TMDS), .RST(1'b0));
// The pixel clock, still the same as before.
assign TMDS_clock_p = pixclk;
// Note (again): gpdi_dn[3:0] is generated automatically by LVCMOS33D mode in ulx3s.lpf
endmodule

View File

@@ -1,4 +1,4 @@
yosys -p "synth_ecp5 -abc9 -top HDMI_test -json HDMI_test.json" HDMI_test.v HDMI_clock.v TMDS_encoder.v
yosys -p "synth_ecp5 -abc9 -top HDMI_test -json HDMI_test.json" HDMI_test_DDR.v HDMI_clock.v TMDS_encoder.v
nextpnr-ecp5 --json HDMI_test.json --lpf ulx3s.lpf --textcfg HDMI_test_out.config --85k --freq 25 --package CABGA381
ecppack --compress --svf-rowsize 100000 --svf HDMI_test.svf HDMI_test_out.config HDMI_test.bit
ujprog HDMI_test.bit

View File

@@ -182,7 +182,7 @@ int main() {
// delay(50); // If GL_clear() is uncommented, uncomment as well
// to reduce flickering.
}
wireframe = !wireframe;
wireframe = !wireframe;
fclose(F);
}
}

View File

@@ -0,0 +1,19 @@
#include <femtorv32.h>
#define SET_MODE 0
#define SET_PALETTE_R 1
#define SET_PALETTE_G 2
#define SET_PALETTE_B 3
#define SET_WWINDOW_X 4
#define SET_WWINDOW_Y 5
void FGA_write_window(uint32_t x1, uint32_t y1, uint32_t x2, uint32_t y2) {
IO_OUT(IO_FGA_CNTL, SET_WWINDOW_X | x1 << 8 | x2 << 20);
IO_OUT(IO_FGA_CNTL, SET_WWINDOW_Y | y1 << 8 | y2 << 20);
}
void FGA_write_data_uint8_uint8(uint8_t hi, uint8_t lo) {
IO_OUT(IO_FGA_DAT, hi);
IO_OUT(IO_FGA_DAT, lo);
}

View File

@@ -12,7 +12,7 @@
#define IO_SDCARD_bit 8
#define IO_BUTTONS_bit 9
#define IO_FGA_CNTL_bit 10
#define IO_FGA_CMD_bit 11
#define IO_FGA_DAT_bit 11
#define IO_HW_CONFIG_RAM_bit 17
#define IO_HW_CONFIG_DEVICES_bit 18
#define IO_HW_CONFIG_CPUINFO_bit 19

View File

@@ -13,7 +13,7 @@
.equ IO_SDCARD_bit, 8
.equ IO_BUTTONS_bit, 9
.equ IO_FGA_CNTL_bit, 10
.equ IO_FGA_CMD_bit, 11
.equ IO_FGA_DAT_bit, 11
.equ IO_HW_CONFIG_RAM_bit, 17
.equ IO_HW_CONFIG_DEVICES_bit, 18
.equ IO_HW_CONFIG_CPUINFO_bit, 19
@@ -33,7 +33,7 @@
.equ IO_SDCARD, 1024
.equ IO_BUTTONS, 2048
.equ IO_FGA_CNTL, 4096
.equ IO_FGA_CMD, 8192
.equ IO_FGA_DAT, 8192
.equ IO_HW_CONFIG_RAM, 524288
.equ IO_HW_CONFIG_DEVICES, 1048576
.equ IO_HW_CONFIG_CPUINFO, 2097152

View File

@@ -13,7 +13,8 @@ OBJECTS=femtorv32.o max7219.o ssd1351.o uart.o \
tty_init.o max7219_text.o \
wait_cycles.o microwait.o milliwait.o \
spi_sd.o cycles.o \
filesystem.o exec.o femto_elf.o
filesystem.o exec.o femto_elf.o \
FGA.o
all: libfemtorv32.a

View File

@@ -168,7 +168,7 @@ void GL_fill_poly(int nb_pts, int* points, uint16_t color) {
GL_line(x1,y1,x2,y2,color);
continue;
}
char* x_buffer = ((clockwise > 0) ^ (y2 > y1)) ? x_left : x_right;
int dx = x2 - x1;
int sx = 1;
@@ -189,7 +189,9 @@ void GL_fill_poly(int nb_pts, int* points, uint16_t color) {
}
if(y1 == y2) {
continue;
x_left[y1] = MIN(x1,x2);
x_right[y1] = MAX(x1,x2);
continue;
}
ex = (dx << 1) - dy;

View File

@@ -120,7 +120,8 @@ void GL_putchar_xy(int x, int y, char c);
#define IO_SPI_FLASH IO_BIT_TO_OFFSET(IO_SPI_FLASH_bit)
#define IO_SDCARD IO_BIT_TO_OFFSET(IO_SDCARD_bit)
#define IO_BUTTONS IO_BIT_TO_OFFSET(IO_BUTTONS_bit)
#define IO_MAPPED_SPI_FLASH IO_BIT_TO_OFFSET(IO_MAPPED_SPI_FLASH_bit)
#define IO_FGA_CNTL IO_BIT_TO_OFFSET(IO_FGA_CNTL_bit)
#define IO_FGA_DAT IO_BIT_TO_OFFSET(IO_FGA_DAT_bit)
#define IO_HW_CONFIG_RAM IO_BIT_TO_OFFSET(IO_HW_CONFIG_RAM_bit)
#define IO_HW_CONFIG_DEVICES IO_BIT_TO_OFFSET(IO_HW_CONFIG_DEVICES_bit)
#define IO_HW_CONFIG_CPUINFO IO_BIT_TO_OFFSET(IO_HW_CONFIG_CPUINFO_bit)
@@ -141,11 +142,13 @@ extern void oled2(uint32_t cmd, uint32_t arg1, uint32_t arg2);
extern void oled3(uint32_t cmd, uint32_t arg1, uint32_t arg2, uint32_t arg3);
/*
* low-level functions to send data to the OLED display
* low-level functions to send data to the OLED display and FGA
* First, call oled_write_window()
* Then send all the pixels using one of the three forms of OLED_WRITE_DATA
*/
#define OLED_WRITE_DATA_UINT8_UINT8(HI,LO) IO_OUT(IO_SSD1351_DAT,(HI)); IO_OUT(IO_SSD1351_DAT,(LO))
#define IO_GFX_DAT (IO_SSD1351_DAT | IO_FGA_DAT)
#define OLED_WRITE_DATA_UINT8_UINT8(HI,LO) IO_OUT(IO_GFX_DAT,(HI)); IO_OUT(IO_GFX_DAT,(LO));
#define OLED_WRITE_DATA_UINT16(RGB) OLED_WRITE_DATA_UINT8_UINT8(RGB>>8, RGB)
#define OLED_WRITE_DATA_RGB(R,G,B) OLED_WRITE_DATA_UINT16(GL_RGB(R,G,B))
@@ -169,6 +172,8 @@ static inline FGA_setpixel(int x, int y, uint8_t R, uint8_t G, uint8_t B) {
((uint16_t*)FGA_BASEMEM)[320*y+x] = GL_RGB(R,G,B);
}
void FGA_emul_write_data_uint8_uint8(uint8_t HI, uint8_t LO);
/* Mapped SPI FLASH */
#define SPI_FLASH_BASE ((void*)(1 << 23))

View File

@@ -106,6 +106,14 @@ oled_write_window:
call oled2
li a0, 0x5c
call oled0
# Set write window in FGA, for SSD1351 emulation.
mv a0,t0
mv a1,t2
mv a2,t1
mv a3,t3
call FGA_write_window
lw ra, 0(sp)
add sp,sp,4
ret

View File

@@ -5,16 +5,16 @@
FIRMWARE_DIR=/home/blevy/Programming/learn-fpga/FemtoRV/FIRMWARE
# Configuration for larger boards (ULX3S, ECP5-EVN)
#ARCH=rv32im
#ABI=ilp32
#OPTIMIZE=-O3
ARCH=rv32im
ABI=ilp32
OPTIMIZE=-O3
# More minimalistic configurations (ICEStick, ICE40)
# note: ICE40up5K could support rv32im but this requires some work
# since yosys does not (yet) infer DSP blocks for ICE40).
ARCH=rv32i
ABI=ilp32
OPTIMIZE=-Os
#ARCH=rv32i
#ABI=ilp32
#OPTIMIZE=-Os
# Optimize for size, on the IceStick we only have 6K of RAM !
# Normally you do not need to modify anything beyond this line.

View File

@@ -135,6 +135,17 @@ RISC-V notes:
can understand.
DebugLog:
- FGA, emulation du SSD1351
a) j'ai oublié de conditionner à io_wstrb
b) quand il y a plusieurs <= dans un bloc, j'ai
tendance à oublier qu'ils ne sont pas séquentiels entre eux, et à faire
par exemple:
x1 <= mem_wdata;
x <= x1;
en voulant dire:
x1 <= mem_wdata;
x <= mem_wdata;
- Everything seemed to be broken after I generated all IO
register IDs from VERILOG sources, in fact I was
reading frequency and "cycles per loop" from

View File

@@ -18,23 +18,20 @@ module FGA(
input wire [31:0] mem_wdata, // data to be written
input wire pixel_clk, // 25 MHz
output wire [3:0] gpdi_dp // HDMI signals, blue, green, red, clock
output wire [3:0] gpdi_dp, // HDMI signals, blue, green, red, clock
// dgpi_dn generated by pins (see ulx3s.lpf)
input wire io_wstrb,
input wire io_rstrb,
input wire sel_cntl, // select control register (R/W)
input wire sel_dat, // select data in (W)
output wire [31:0] rdata // data read from register
);
wire [14:0] vram_word_address = mem_address[16:2];
reg [31:0] VRAM[32767:0];
// write VRAM (interface with processor)
always @(posedge clk) begin
if(sel) begin
if(mem_wmask[0]) VRAM[vram_word_address][ 7:0 ] <= mem_wdata[ 7:0 ];
if(mem_wmask[1]) VRAM[vram_word_address][15:8 ] <= mem_wdata[15:8 ];
if(mem_wmask[2]) VRAM[vram_word_address][23:16] <= mem_wdata[23:16];
if(mem_wmask[3]) VRAM[vram_word_address][31:24] <= mem_wdata[31:24];
end
end
/************************* HDMI signal generation ***************************/
@@ -42,6 +39,13 @@ module FGA(
reg [9:0] X, Y; // current pixel coordinates
reg hSync, vSync; // horizontal and vertical synchronization
reg DrawArea; // asserted if current pixel is in drawing area
reg mem_busy; // asserted if memory transfer is running.
// read control register
assign rdata = (io_rstrb && sel_cntl) ?
{vSync,hSync,DrawArea,mem_busy,4'b0,2'b0,X,2'b0,Y} :
32'b0;
always @(posedge pixel_clk) begin
DrawArea <= (X<640) && (Y<480);
X <= (X==799) ? 0 : X+1;
@@ -118,5 +122,93 @@ module FGA(
assign gpdi_dp[0] = TMDS_shift_B[0];
assign gpdi_dp[3] = pixel_clk;
/*************************************************************************/
// control register - commands
localparam SET_MODE = 7'd0; // TODO
localparam SET_PALETTE_R = 7'd1; // TODO
localparam SET_PALETTE_G = 7'd2; // TODO
localparam SET_PALETTE_B = 7'd3; // TODO
localparam SET_WWINDOW_X = 7'd4;
localparam SET_WWINDOW_Y = 7'd5;
// Emulation of SSD1351 OLED display.
// - write window command, two commands:
// (send 32 bits to IO_FGA_CNTL hardware register)
// CMD=4: SET_WWINDOW_X: X2[11:0] X1[11:0] CMD[7:0]
// CMD=5: SET_WWINDOW_Y: Y2[11:0] Y1[11:0] CMD[7:0]
//
// - write data: send 8 bits to IO_FGA_DAT hardware register
// MSB first, encoding follows SSD1351: RRRRR GGGGG 0 BBBBB
reg[9:0] window_x1, window_x2, window_y1, window_y2, window_x, window_y;
reg[16:0] window_row_start;
reg[16:0] window_byte_address;
always @(posedge clk) begin
if(io_wstrb) begin
if(sel_cntl) begin
case(mem_wdata[7:0])
SET_WWINDOW_X: begin // SET_WWINDOW_X command
window_x1 <= mem_wdata[19:8];
window_x2 <= mem_wdata[31:20];
window_x <= mem_wdata[19:8];
mem_busy <= 1'b1;
end
SET_WWINDOW_Y: begin // SET_WWINDOW_Y command
window_y1 <= mem_wdata[19:8];
window_y2 <= mem_wdata[31:20];
window_y <= mem_wdata[19:8];
mem_busy <= 1'b1;
window_row_start <= (mem_wdata[19:8] * 320 + window_x1) << 1;
window_byte_address <= (mem_wdata[19:8] * 320 + window_x1) << 1;
end
endcase
end else if(sel_dat) begin // one byte of data sent to IO_FGA_DAT
// increment pixel address.
window_byte_address <= window_byte_address + 1;
if(window_byte_address[0]) begin
if(window_x == window_x2) begin
if(window_y == window_y2) begin
mem_busy <= 1'b0;
end else begin
window_y <= window_y+1;
window_x <= window_x1;
window_byte_address <= window_row_start + 640;
window_row_start <= window_row_start + 640;
end
end else begin
window_x <= window_x + 1;
end
end
end // if (sel_dat)
end // if (wstrb)
end // always @ (posedge clk)
// write VRAM (interface with processor)
always @(posedge clk) begin
if(io_wstrb) begin
if(sel_dat && mem_busy) begin // one byte of data sent to IO_FGA_DAT
// write data to VRAM.
case(window_byte_address[1:0])
// Note: bytes are swapped (following SSD1351 norm)
2'b00: VRAM[window_byte_address[16:2]][15:8 ] <= mem_wdata[7:0];
2'b01: VRAM[window_byte_address[16:2]][ 0:7 ] <= mem_wdata[7:0];
2'b10: VRAM[window_byte_address[16:2]][31:24] <= mem_wdata[7:0];
2'b11: VRAM[window_byte_address[16:2]][23:16] <= mem_wdata[7:0];
endcase
end
end else if(sel && !mem_busy) begin
if(mem_wmask[0]) VRAM[vram_word_address][ 7:0 ] <= mem_wdata[ 7:0 ];
if(mem_wmask[1]) VRAM[vram_word_address][15:8 ] <= mem_wdata[15:8 ];
if(mem_wmask[2]) VRAM[vram_word_address][23:16] <= mem_wdata[23:16];
if(mem_wmask[3]) VRAM[vram_word_address][31:24] <= mem_wdata[31:24];
end
end
endmodule

View File

@@ -41,7 +41,7 @@ localparam NRV_DEVICES = 0
| (1 << IO_BUTTONS_bit)
`endif
`ifdef NRV_IO_FGA
| (1 << IO_FGA_CNTL_bit) | (1 << IO_FGA_CMD_bit)
| (1 << IO_FGA_CNTL_bit) | (1 << IO_FGA_DAT_bit)
`endif
;

View File

@@ -11,7 +11,7 @@ localparam IO_SPI_FLASH_bit = 7; // RW write: SPI address (24 bits) rea
localparam IO_SDCARD_bit = 8; // RW write: bit 0: mosi bit 1: clk bit 2: csn read: miso
localparam IO_BUTTONS_bit = 9; // R buttons state
localparam IO_FGA_CNTL_bit = 10; // RW RESERVED
localparam IO_FGA_CMD_bit = 11; // W RESERVED
localparam IO_FGA_DAT_bit = 11; // W RESERVED
// The three constant hardware config registers, using the three last bits of IO address space
localparam IO_HW_CONFIG_RAM_bit = 17; // R total quantity of RAM, in bytes

View File

@@ -374,27 +374,27 @@ module FemtoRV32 #(
// Does 1-cycle ALU ops, or handles jump/branch, or transitions to waitALU, load, store
// If linear execution flow, update instr with lookahead and prepare next lookahead
state[EXECUTE_bit]: begin
nextInstr <= mem_rdata; // Looked-ahead instr.
nextInstr <= mem_rdata; // Looked-ahead instr.
addressReg <= aluAplusB[ADDR_WIDTH-1:0]; // Needed for LOAD,STORE,jump,branch
PC <= PCplus4;
PC <= PCplus4;
(* parallel_case, full_case *)
case (1'b1)
error_latched: state <= ERROR;
isLoad: state <= LOAD;
isStore: begin
state <= STORE;
isStore: begin
state <= STORE;
wdataReg <= STORE_data_aligned_for_MEM;
end
needWaitALU: state <= WAIT_ALU_OR_DATA;
jump_or_take_branch: begin
PC <= aluAplusB[ADDR_WIDTH-1:0];
PC <= aluAplusB[ADDR_WIDTH-1:0];
state <= FETCH_INSTR;
end
default: begin // Linear execution flow, use lookahead, prepare next lookahead
instr <= mem_rdata; // Use looked-ahead instr.
addressReg <= PCplus8; // Look-ahead: PC+8 (PC not updated yet)
state <= FETCH_REGS; // Cool, linear exec flow takes 2 CPIs !
instr <= mem_rdata; // Use looked-ahead instr.
addressReg <= PCplus8; // Look-ahead: PC+8 (PC not updated yet)
state <= FETCH_REGS; // Cool, linear exec flow takes 2 CPIs !
end
endcase
end
@@ -422,7 +422,6 @@ module FemtoRV32 #(
if(!mem_wbusy)
state <= FETCH_REGS;
end
// *********************************************************************
// Used by LOAD and by multi-cycle ALU instr (shifts and RV32M ops), writeback from ALU or memory

View File

@@ -241,6 +241,7 @@ module femtosoc(
/* verilator lint_on WIDTH */
`ifdef NRV_IO_FGA
wire [31:0] FGA_rdata;
FGA graphic_adapter(
.clk(clk),
.sel(mem_address_is_vram),
@@ -248,7 +249,13 @@ module femtosoc(
.mem_address(mem_address[16:0]),
.mem_wdata(mem_wdata),
.pixel_clk(pclk),
.gpdi_dp(gpdi_dp)
.gpdi_dp(gpdi_dp),
.io_rstrb(io_rstrb),
.io_wstrb(io_wstrb),
.sel_cntl(io_word_address[IO_FGA_CNTL_bit]),
.sel_dat(io_word_address[IO_FGA_DAT_bit]),
.rdata(FGA_rdata)
);
`endif

View File

@@ -74,14 +74,46 @@ OK, so it is rather clear, to summarize we have:
[here](https://github.com/BrunoLevy/learn-fpga/blob/master/Basic/ULX3S_hdmi/ulx3s.lpf).
It does exactly what I need:
generate the negative pins from the positive ones. When it is
active, negative pins should not be driven !
active, negative pins should not be driven !
_How I figured out for `LVCMOS33D` ?_ I was very lucky, I blindly
copied `ulx3s.lpf` from Emard's repository [here](https://github.com/emard/ulx3s/blob/master/doc/constraints/ulx3s_v20.lpf)
then tryed my naive design with inverters (that would not have worked anyway BTW),
but I had an assertion failure in NEXPNR: `Assertion failure: pio == "PIOA"`.
Pasting the message in Google redirected me [here](https://github.com/YosysHQ/nextpnr/issues/544),
that explains the story (drive only `gpdi_dp` and remove `gpdi_dn`,
because `LVCMOS33D` implies a pseudo-differential driver that drives both
sides of the pair.
Note: for higher resolution, it is possible to use a specialized ECP5
primitive (`ODDRX1F`) that can shift two bits per clock (then using a
_Coming next_: for higher resolution, it is possible to use a specialized ECP5
primitive (`ODDRX1F`) that can shift and send two bits per clock (then using a
125MHz clock instead of 250MHz), see Lawrie's code
[here](https://github.com/lawrie/ulx3s_examples/blob/master/hdmi/fake_differential.v).
For now I'm not doing that, because it seems that the simpler code
suffices.
Without it, I think that my design can work up to 800x600 (but this will require
changing the frequencies, resolution etc...). For higher resolution,
it will require changing the shifter part as follows, as well as
changing the clocks and resolutions of course (not tested yet):
```
// Counter now counts modulo 5 instead of modulo 10
reg [4:0] TMDS_mod5=1;
wire TMDS_shift_load = TMDS_mod5[4];
always @(posedge clk_TMDS) TMDS_mod5 <= {TMDS_mod5[3:0],TMDS_mod5[4]};
// Shifter now shifts two bits at each clock
reg [9:0] TMDS_shift_red=0, TMDS_shift_green=0, TMDS_shift_blue=0;
always @(posedge clk_TMDS) begin
TMDS_shift_red <= TMDS_shift_load ? TMDS_red : TMDS_shift_red [9:2];
TMDS_shift_green <= TMDS_shift_load ? TMDS_green : TMDS_shift_green[9:2];
TMDS_shift_blue <= TMDS_shift_load ? TMDS_blue : TMDS_shift_blue [9:2];
end
// DDR serializers: they send D0 at the rising edge and D1 at the falling edge
ODDRX1F ddr_red (.D0[TMDS_shift_red[0]), .D1[TMDS_shift_red[1]), .Q(TMDS_rgb_p[2]), .SCLK(), .RST(1'b0));
ODDRX1F ddr_green(.D0[TMDS_shift_green[0]), .D1[TMDS_shift_green[1]), .Q(TMDS_rgb_p[1]), .SCLK(), .RST(1'b0));
ODDRX1F ddr_blue (.D0[TMDS_shift_blue[0]), .D1[TMDS_shift_blue[1]), .Q(TMDS_rgb_p[0]), .SCLK(), .RST(1'b0));
```
[Complete sources for ULX3S](https://github.com/BrunoLevy/learn-fpga/tree/master/Basic/ULX3S_hdmi)