Drugi monitor HDMI do Raspberry Pi3 przez interfejs DPI i płytkę FPGA


Na tym filmie widać: płytka Raspberry Pi3, podłączona do niej poprzez złącze GPIO, to płytka FPGA Mars Rover2rpi (Cyclone IV), do której podłączony jest monitor HDMI. Drugi monitor podłącza się za pomocą standardowego złącza HDMI Raspberry Pi3. Wszystko działa razem jak system z dwoma monitorami.

Następnie opowiem Ci, jak to jest realizowane.

Popularna płytka Raspberry Pi3 posiada złącze GPIO, za pomocą którego można podłączyć różne karty rozszerzeń: czujniki, diody LED, sterowniki silników krokowych i wiele więcej. Dokładna funkcja każdego styku złącza zależy od konfiguracji portu. Konfiguracja GPIO ALT2 umożliwia przełączenie złącza w tryb interfejsu DPI, Display Parallel Interface. Istnieją karty rozszerzeń umożliwiające podłączenie monitorów VGA poprzez DPI. Jednak po pierwsze monitory VGA nie są już tak powszechne jak HDMI, a po drugie interfejs cyfrowy jest coraz lepszy od analogowego. Co więcej, przetwornik cyfrowo-analogowy na takich kartach rozszerzeń VGA jest zwykle wykonany w postaci łańcuchów R-2-R i często nie więcej niż 6 bitów na kolor.

W trybie ALT2 piny złącza GPIO mają następujące znaczenie:

Drugi monitor HDMI do Raspberry Pi3 przez interfejs DPI i płytkę FPGA

Tutaj pokolorowałem piny RGB złącza odpowiednio na czerwono, zielono i niebiesko. Inne ważne sygnały to sygnały V-SYNC i H-SYNC, a także CLK. Częstotliwość zegara CLK to częstotliwość, z jaką wartości pikseli są wyprowadzane do złącza; zależy to od wybranego trybu wideo.

Aby podłączyć cyfrowy monitor HDMI, należy przechwycić sygnały DPI interfejsu i przekonwertować je na sygnały HDMI. Można to zrobić na przykład za pomocą jakiejś płytki FPGA. Jak się okazuje, do tych celów nadaje się płyta Mars Rover2rpi. Prawdę mówiąc, główna opcja podłączenia tej płyty za pomocą specjalnego adaptera wygląda następująco:

Drugi monitor HDMI do Raspberry Pi3 przez interfejs DPI i płytkę FPGA

Płytka ta służy do zwiększenia ilości portów GPIO oraz do podłączenia większej ilości urządzeń peryferyjnych do maliny. Jednocześnie 4 sygnały GPIO przy tym połączeniu wykorzystywane są dla sygnałów JTAG, dzięki czemu program z Raspberry może załadować firmware FPGA do FPGA. Z tego powodu to standardowe połączenie mi nie odpowiada, zanikają sygnały 4 DPI. Na szczęście dodatkowe grzebienie na płytce mają rozkład pinów zgodny z Raspberry. Mogę więc obrócić płytkę o 90 stopni i nadal podłączyć ją do mojej maliny:

Drugi monitor HDMI do Raspberry Pi3 przez interfejs DPI i płytkę FPGA

Oczywiście będziesz musiał skorzystać z zewnętrznego programatora JTAG, ale nie stanowi to problemu.

Nadal jest mały problem. Nie każdy pin FPGA może być użyty jako wejście zegara. Istnieje tylko kilka dedykowanych pinów, które można wykorzystać do tych celów. Okazało się więc, że tutaj sygnał GPIO_0 CLK nie dociera do wejścia FPGA, które można wykorzystać jako wejście zegara FPGA. Musiałem więc jeszcze położyć jeden drut na szaliku. Podłączam GPIO_0 i sygnał KEY[1] płytki:

Drugi monitor HDMI do Raspberry Pi3 przez interfejs DPI i płytkę FPGA

Teraz opowiem Wam trochę o projekcie FPGA. Główną trudnością w generowaniu sygnałów HDMI są bardzo wysokie częstotliwości. Jeśli spojrzysz na układ pinów złącza HDMI, zobaczysz, że sygnały RGB są teraz szeregowymi sygnałami różnicowymi:

Drugi monitor HDMI do Raspberry Pi3 przez interfejs DPI i płytkę FPGA

Zastosowanie sygnału różnicowego pozwala zwalczać zakłócenia w trybie wspólnym na linii przesyłowej. W tym przypadku oryginalny ośmiobitowy kod każdego sygnału koloru jest konwertowany na 10-bitowy kod TMDS (sygnalizacja różnicowa o zminimalizowanym przejściu). Jest to specjalna metoda kodowania mająca na celu usunięcie składowej stałej z sygnału i zminimalizowanie przełączania sygnału w linii różnicowej. Ponieważ teraz na jeden bajt koloru trzeba przesłać linią szeregową 10 bitów, okazuje się, że prędkość zegara serializatora musi być 10 razy większa niż prędkość zegara pikseli. Jeśli weźmiemy na przykład tryb wideo 1280x720 60 Hz, wówczas częstotliwość pikseli tego trybu wynosi 74,25 MHz. Serializator powinien mieć częstotliwość 742,5 MHz.

Zwykłe układy FPGA niestety nie są do tego zdolne. Jednak na szczęście dla nas FPGA ma wbudowane piny DDIO. To wnioski, które są już niejako serializatorami 2 do 1. Oznacza to, że mogą wyprowadzać po dwa bity na narastającym i opadającym zboczu częstotliwości zegara. Oznacza to, że w projekcie FPGA można wykorzystać nie 740 MHz, a 370 MHz, ale w FPGA trzeba zastosować elementy wyjściowe DDIO. Teraz 370 MHz jest już częstotliwością całkowicie osiągalną. Niestety tryb 1280x720 to ograniczenie. Wyższej rozdzielczości nie da się osiągnąć w naszym układzie FPGA Cyclone IV zainstalowanym na płycie Mars Rover2rpi.

Tak więc w projekcie częstotliwość piksela wejściowego CLK trafia do PLL, gdzie jest mnożona przez 5. Przy tej częstotliwości bajty R, G, B są konwertowane na pary bitów. To właśnie robi koder TMDS. Kod źródłowy w Verilog HDL wygląda następująco:

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

Następnie pary wyjściowe podawane są na wyjście DDIO, które sekwencyjnie generuje jednobitowy sygnał na zboczach narastających i opadających.

Samo DDIO można opisać następującym kodem Verilog:

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

Ale najprawdopodobniej to nie zadziała w ten sposób. Aby faktycznie włączyć elementy wyjściowe DDIO, musisz użyć megafunkcji Altera ALTDDIO_OUT. Mój projekt wykorzystuje komponent biblioteki ALTDDIO_OUT.

To wszystko może wyglądać na trochę skomplikowane, ale działa.

Możesz wyświetlić cały kod źródłowy napisany w Verilog HDL tutaj na githubie.

Skompilowane oprogramowanie układowe FPGA jest przesyłane do układu EPCS zainstalowanego na płycie Mars Rover2rpi. Zatem po włączeniu zasilania karty FPGA układ FPGA zostanie zainicjowany z pamięci flash i uruchomiony.

Teraz musimy trochę porozmawiać o konfiguracji samej Raspberry.

Eksperymentuję na Raspberry PI OS (32-bitowym) opartym na Debianie Buster, wersja: sierpień 2020,
Data wydania: 2020-08-20, wersja jądra: 5.4.

Musisz zrobić dwie rzeczy:

  • edytować plik config.txt;
  • utwórz konfigurację serwera X do pracy z dwoma monitorami.

Do edycji pliku /boot/config.txt potrzebne są:

  1. wyłącz użycie i2c, i2s, spi;
  2. włącz tryb DPI za pomocą nakładki dtoverlay=dpi24;
  3. skonfiguruj tryb wideo 1280×720 60 Hz, 24 bity na piksel w DPI;
  4. określ wymaganą liczbę buforów ramki 2 (max_framebuffers=2, dopiero wtedy pojawi się drugie urządzenie /dev/fb1)

Pełny tekst pliku config.txt wygląda następująco.

# 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

Następnie musisz utworzyć plik konfiguracyjny dla serwera X, aby mógł używać dwóch monitorów na dwóch buforach ramki /dev/fb0 i /dev/fb1:

Mój plik konfiguracyjny /usr/share/x11/xorg.conf.d/60-dualscreen.conf wygląda następująco

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

Cóż, jeśli nie jest jeszcze zainstalowany, musisz zainstalować Xineramę. Następnie przestrzeń pulpitu zostanie w pełni powiększona do dwóch monitorów, jak pokazano na powyższym filmie demonstracyjnym.

To chyba wszystko. Teraz posiadacze Raspberry Pi3 będą mogli korzystać z dwóch monitorów.

Opis i schemat obwodu płytki Mars Rover2rpi można znaleźć Popatrz tutaj.

Źródło: www.habr.com