Second HDMI monitor to Raspberry Pi3 via DPI interface and FPGA board


This video shows: a Raspberry Pi3 board, to which, via the GPIO connector, an FPGA Mars Rover2rpi (Cyclone IV) board is connected, to which an HDMI monitor is connected. The second monitor is connected via the standard Raspberry Pi3 HDMI connector. All together it works like a dual monitor system.

Next I will tell you how it is implemented.

The popular Raspberry Pi3 board has a GPIO connector through which you can connect various expansion boards: sensors, LEDs, stepper motor drivers and much more. The specific function of each pin on the connector depends on the port configuration. The GPIO ALT2 configuration allows you to switch the connector to the DPI interface mode, Display Parallel Interface. There are expansion boards for connecting VGA monitors via DPI. However, firstly, VGA monitors are no longer as common as HDMI, and secondly, the digital interface is getting better than analog. Moreover, the DAC on such VGA expansion cards is usually made in the form of R-2-R chains and often no more than 6 bits per color.

In ALT2 mode, the pins of the GPIO connector have the following meaning:

Second HDMI monitor to Raspberry Pi3 via DPI interface and FPGA board

Here I colored the RGB pins of the connector red, green and blue respectively. Other important signals are the V-SYNC and H-SYNC sweep sync signals, as well as CLK. The CLK clock frequency is the frequency at which pixel values ​​are output to the connector and depends on the selected video mode.

To connect a digital HDMI monitor, you need to capture DPI interface signals and convert them to HDMI signals. This can be done, for example, using any FPGA board. As it turned out, the Mars Rover2rpi board is suitable for this purpose. In truth, the main option for connecting this board through a special adapter looks like this:

Second HDMI monitor to Raspberry Pi3 via DPI interface and FPGA board

This board is used to increase the number of GPIO ports and to connect more peripherals to the raspberry. At the same time, 4 GPIO signals with this connection are used for JTAG signals, so that the program from the distribution can load the FPGA firmware into the FPGA. Because of this, such a regular connection does not suit me, 4 DPI signals drop out. Luckily, the extra combs on the board have a Raspberry-compatible pinout. So that I can rotate the board 90 degrees and still connect it to my raspberry:

Second HDMI monitor to Raspberry Pi3 via DPI interface and FPGA board

Of course, you will have to use an external JTAG programmer, but this is not a problem.

There is still a small problem. Not every FPGA pin can be used as a clock input. There are only a few dedicated pins that can be used for this purpose. So it turned out here that the GPIO_0 CLK signal does not get to the FPGA input, which can be used as an FPGA clock input. So all the same, I had to throw one posting on a scarf. I connect GPIO_0 and KEY[1] signal of the board:

Second HDMI monitor to Raspberry Pi3 via DPI interface and FPGA board

Now I'll tell you a little about the project in the FPGA. The main difficulty in the formation of HDMI signals is very high frequencies. Looking at the pinout of the HDMI connector, you can see that the RGB signals are now serial differential signals:

Second HDMI monitor to Raspberry Pi3 via DPI interface and FPGA board

The use of a differential signal allows you to deal with common mode noise on the transmission line. In this case, the original eight-bit code of each color signal is converted into a 10-bit TMDS (Transition-minimized differential signaling). This is a special encoding method to remove the DC component from the signal and minimize signal switching in the differential line. Since there are now 10 bits to transmit per byte of color over the serial line, it turns out that the clock frequency of the serializer must be 10 times higher than the clock frequency of the pixels. If we take for example the video mode 1280x720 60Hz, then the pixel frequency of this mode is 74,25MHz. The serializer should be 742,5 MHz.

Conventional FPGAs are generally not capable of this, unfortunately. However, to our luck, FPGAs have built-in DDIO pins. These are conclusions that are already, as it were, 2-to-1 serializers. That is, they can output two bits in sequence along the rising and falling clock frequencies. This means that in the FPGA project you can use not 740 MHz, but 370 MHz, but you need to use the DDIO output elements in the FPGA. Here 370 MHz is already quite an achievable frequency. Unfortunately, the 1280Γ—720 mode is the limit. Higher resolution cannot be achieved in our FPGA Cyclone IV installed on the Rover2rpi board.

So, in the project, the input pixel frequency CLK is fed to the PLL, where it is multiplied by 5. At this frequency, the R, G, B bytes are converted into bit pairs. This is what the TMDS encoder does. The source code on Verilog HDL looks like this:

module hdmi(
	input wire pixclk,		// 74MHz
	input wire clk_TMDS2,	// 370MHz
	input wire hsync,
	input wire vsync,
	input wire active,
	input wire [7:0]red,
	input wire [7:0]green,
	input wire [7:0]blue,
	output wire TMDS_bh,
	output wire TMDS_bl,
	output wire TMDS_gh,
	output wire TMDS_gl,
	output wire TMDS_rh,
	output wire TMDS_rl
);

wire [9:0] TMDS_red, TMDS_green, TMDS_blue;
TMDS_encoder encode_R(.clk(pixclk), .VD(red  ), .CD({vsync,hsync}), .VDE(active), .TMDS(TMDS_red));
TMDS_encoder encode_G(.clk(pixclk), .VD(green), .CD({vsync,hsync}), .VDE(active), .TMDS(TMDS_green));
TMDS_encoder encode_B(.clk(pixclk), .VD(blue ), .CD({vsync,hsync}), .VDE(active), .TMDS(TMDS_blue));

reg [2:0] TMDS_mod5=0;  // modulus 5 counter
reg [4:0] TMDS_shift_bh=0, TMDS_shift_bl=0;
reg [4:0] TMDS_shift_gh=0, TMDS_shift_gl=0;
reg [4:0] TMDS_shift_rh=0, TMDS_shift_rl=0;

wire [4:0] TMDS_blue_l  = {TMDS_blue[9],TMDS_blue[7],TMDS_blue[5],TMDS_blue[3],TMDS_blue[1]};
wire [4:0] TMDS_blue_h  = {TMDS_blue[8],TMDS_blue[6],TMDS_blue[4],TMDS_blue[2],TMDS_blue[0]};
wire [4:0] TMDS_green_l = {TMDS_green[9],TMDS_green[7],TMDS_green[5],TMDS_green[3],TMDS_green[1]};
wire [4:0] TMDS_green_h = {TMDS_green[8],TMDS_green[6],TMDS_green[4],TMDS_green[2],TMDS_green[0]};
wire [4:0] TMDS_red_l   = {TMDS_red[9],TMDS_red[7],TMDS_red[5],TMDS_red[3],TMDS_red[1]};
wire [4:0] TMDS_red_h   = {TMDS_red[8],TMDS_red[6],TMDS_red[4],TMDS_red[2],TMDS_red[0]};

always @(posedge clk_TMDS2)
begin
	TMDS_shift_bh <= TMDS_mod5[2] ? TMDS_blue_h  : TMDS_shift_bh  [4:1];
	TMDS_shift_bl <= TMDS_mod5[2] ? TMDS_blue_l  : TMDS_shift_bl  [4:1];
	TMDS_shift_gh <= TMDS_mod5[2] ? TMDS_green_h : TMDS_shift_gh  [4:1];
	TMDS_shift_gl <= TMDS_mod5[2] ? TMDS_green_l : TMDS_shift_gl  [4:1];
	TMDS_shift_rh <= TMDS_mod5[2] ? TMDS_red_h   : TMDS_shift_rh  [4:1];
	TMDS_shift_rl <= TMDS_mod5[2] ? TMDS_red_l   : TMDS_shift_rl  [4:1];
	TMDS_mod5 <= (TMDS_mod5[2]) ? 3'd0 : TMDS_mod5+3'd1;
end

assign TMDS_bh = TMDS_shift_bh[0];
assign TMDS_bl = TMDS_shift_bl[0];
assign TMDS_gh = TMDS_shift_gh[0];
assign TMDS_gl = TMDS_shift_gl[0];
assign TMDS_rh = TMDS_shift_rh[0];
assign TMDS_rl = TMDS_shift_rl[0];

endmodule

module TMDS_encoder(
	input clk,
	input [7:0] VD,	// video data (red, green or blue)
	input [1:0] CD,	// control data
	input VDE,  	// video data enable, to choose between CD (when VDE=0) and VD (when VDE=1)
	output reg [9:0] TMDS = 0
);

wire [3:0] Nb1s = VD[0] + VD[1] + VD[2] + VD[3] + VD[4] + VD[5] + VD[6] + VD[7];
wire XNOR = (Nb1s>4'd4) || (Nb1s==4'd4 && VD[0]==1'b0);
wire [8:0] q_m = {~XNOR, q_m[6:0] ^ VD[7:1] ^ {7{XNOR}}, VD[0]};

reg [3:0] balance_acc = 0;
wire [3:0] balance = q_m[0] + q_m[1] + q_m[2] + q_m[3] + q_m[4] + q_m[5] + q_m[6] + q_m[7] - 4'd4;
wire balance_sign_eq = (balance[3] == balance_acc[3]);
wire invert_q_m = (balance==0 || balance_acc==0) ? ~q_m[8] : balance_sign_eq;
wire [3:0] balance_acc_inc = balance - ({q_m[8] ^ ~balance_sign_eq} & ~(balance==0 || balance_acc==0));
wire [3:0] balance_acc_new = invert_q_m ? balance_acc-balance_acc_inc : balance_acc+balance_acc_inc;
wire [9:0] TMDS_data = {invert_q_m, q_m[8], q_m[7:0] ^ {8{invert_q_m}}};
wire [9:0] TMDS_code = CD[1] ? (CD[0] ? 10'b1010101011 : 10'b0101010100) : (CD[0] ? 10'b0010101011 : 10'b1101010100);

always @(posedge clk) TMDS <= VDE ? TMDS_data : TMDS_code;
always @(posedge clk) balance_acc <= VDE ? balance_acc_new : 4'h0;

endmodule

Then the output pairs are fed to the DDIO output, which sequentially produces a one-bit signal on the rise and fall.

DDIO itself could be described with Verilog code like this:

module ddio(
	input wire d0,
	input wire d1,
	input wire clk,
	output wire out
	);

reg r_d0;
reg r_d1;
always @(posedge clk)
begin
	r_d0 <= d0;
	r_d1 <= d1;
end
assign out = clk ? r_d0 : r_d1;
endmodule

But it probably won't work that way. You need to use Altera's ALTDDIO_OUT megafunction to actually use the DDIO output elements. In my project, the library component ALTDDIO_OUT is used.

It may all look a little tricky, but it works.

You can view the entire source code written in Verilog HDL right here on github.

The compiled firmware for the FPGA is embedded into the EPCS chip installed on the Mars Rover2rpi board. Thus, when power is applied to the FPGA board, the FPGA will initialize from flash memory and start.

Now we need to talk a little about the configuration of the Raspberry itself.

I am doing experiments on Raspberry PI OS (32 bit) based on Debian Buster, Version:August 2020,
Release date:2020-08-20, Kernel version:5.4.

There are two things to do:

  • edit the config.txt file;
  • create an X server configuration to work with two monitors.

When editing the /boot/config.txt file, you need to:

  1. disable the use of i2c, i2s, spi;
  2. enable DPI mode with overlay dtoverlay=dpi24;
  3. set video mode 1280Γ—720 60Hz, 24 bits per point per DPI;
  4. specify the required number of framebuffers 2 (max_framebuffers=2, only then will the second device /dev/fb1 appear)

The full text of the config.txt file looks like this.

# For more options and information see
# http://rpf.io/configtxt
# Some settings may impact device functionality. See link above for details

# uncomment if you get no picture on HDMI for a default "safe" mode
#hdmi_safe=1

# uncomment this if your display has a black border of unused pixels visible
# and your display can output without overscan
disable_overscan=1

# uncomment the following to adjust overscan. Use positive numbers if console
# goes off screen, and negative if there is too much border
#overscan_left=16
#overscan_right=16
#overscan_top=16
#overscan_bottom=16

# uncomment to force a console size. By default it will be display's size minus
# overscan.
#framebuffer_width=1280
#framebuffer_height=720

# uncomment if hdmi display is not detected and composite is being output
hdmi_force_hotplug=1

# uncomment to force a specific HDMI mode (this will force VGA)
#hdmi_group=1
#hdmi_mode=1

# uncomment to force a HDMI mode rather than DVI. This can make audio work in
# DMT (computer monitor) modes
#hdmi_drive=2

# uncomment to increase signal to HDMI, if you have interference, blanking, or
# no display
#config_hdmi_boost=4

# uncomment for composite PAL
#sdtv_mode=2

#uncomment to overclock the arm. 700 MHz is the default.
#arm_freq=800

# Uncomment some or all of these to enable the optional hardware interfaces
#dtparam=i2c_arm=on
#dtparam=i2s=on
#dtparam=spi=on

dtparam=i2c_arm=off
dtparam=spi=off
dtparam=i2s=off

dtoverlay=dpi24
overscan_left=0
overscan_right=0
overscan_top=0
overscan_bottom=0
framebuffer_width=1280
framebuffer_height=720
display_default_lcd=0
enable_dpi_lcd=1
dpi_group=2
dpi_mode=87
#dpi_group=1
#dpi_mode=4
dpi_output_format=0x6f027
dpi_timings=1280 1 110 40 220 720 1 5 5 20 0 0 0 60 0 74000000 3

# Uncomment this to enable infrared communication.
#dtoverlay=gpio-ir,gpio_pin=17
#dtoverlay=gpio-ir-tx,gpio_pin=18

# Additional overlays and parameters are documented /boot/overlays/README

# Enable audio (loads snd_bcm2835)
dtparam=audio=on

[pi4]
# Enable DRM VC4 V3D driver on top of the dispmanx display stack
#dtoverlay=vc4-fkms-v3d
max_framebuffers=2

[all]
#dtoverlay=vc4-fkms-v3d
max_framebuffers=2

After that, you need to create a configuration file for the X server to use two monitors on two framebuffer /dev/fb0 and /dev/fb1:

My config file is /usr/share/x11/xorg.conf.d/60-dualscreen.conf like this

Section "Device"
        Identifier      "LCD"
        Driver          "fbturbo"
        Option          "fbdev" "/dev/fb0"
        Option          "ShadowFB" "off"
        Option          "SwapbuffersWait" "true"
EndSection

Section "Device"
        Identifier      "HDMI"
        Driver          "fbturbo"
        Option          "fbdev" "/dev/fb1"
        Option          "ShadowFB" "off"
        Option          "SwapbuffersWait" "true"
EndSection

Section "Monitor"
        Identifier      "LCD-monitor"
        Option          "Primary" "true"
EndSection

Section "Monitor"
        Identifier      "HDMI-monitor"
        Option          "RightOf" "LCD-monitor"
EndSection

Section "Screen"
        Identifier      "screen0"
        Device          "LCD"
        Monitor         "LCD-monitor"
EndSection

Section "Screen"
        Identifier      "screen1"
        Device          "HDMI" 
	Monitor         "HDMI-monitor"
EndSection

Section "ServerLayout"
        Identifier      "default"
        Option          "Xinerama" "on"
        Option          "Clone" "off"
        Screen 0        "screen0"
        Screen 1        "screen1" RightOf "screen0"
EndSection

Well, if not already installed, then you need to install Xinerama. Then the desktop space will be fully extended to two monitors, as shown in the demo video above.

That's probably all. Now, Raspberry Pi3 owners will be able to use two monitors.

Description and diagram of the Mars Rover2rpi board can be look here.

Source: habr.com