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


W tym filmie pokazano: płytę Raspberry Pi3, do której poprzez złącze GPIO podłączona jest płytka FPGA Mars Rover2rpi (Cyclone IV), a do niej podłączony jest monitor HDMI. Drugi monitor jest podłączony przez standardowe złącze HDMI Raspberry Pi3. Całość działa jako system z dwoma monitorami.

Teraz opowiem, jak to zostało zrealizowane.

Na popularnej płycie Raspberry Pi3 znajduje się złącze GPIO, przez które można podłączać różne płytki rozszerzeń: czujniki, diody LED, sterowniki silników krokowych i wiele innych. Konkretna funkcja każdego wyjścia na złączu zależy od konfiguracji portów. Konfiguracja GPIO ALT2 pozwala przełączyć złącze w tryb interfejsu DPI, czyli Display Parallel Interface. Istnieją płytki rozszerzeń do podłączenia monitorów VGA przez DPI. Jednak po pierwsze, monitory VGA nie są już tak powszechne jak HDMI, a po drugie, interfejs cyfrowy jest coraz lepszy od analogowego. Zwłaszcza, że DAC na takich płytkach rozszerzeń zwykle wykonany jest 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łytę FPGA

Tutaj pomalowałem wyjścia RGB złącza odpowiednio na czerwono, zielono i niebiesko. Inne ważne sygnały to sygnały synchronizacji odchylania V-SYNC i H-SYNC, a także CLK. Częstotliwość taktowania CLK to częstotliwość, z jaką wartości pikseli są wydawane na złącze, zależy ona od wybranego trybu wideo.

Aby podłączyć cyfrowy monitor HDMI, należy przechwycić sygnały interfejsu DPI i przekształcić je w sygnały HDMI. Można to zrobić, na przykład, za pomocą jakiejkolwiek płytki FPGA. Okazało się, że płytka Mars Rover2rpi nadaje się do tych celów. Szczerze mówiąc, podstawowa opcja podłączenia tej płytki przez specjalny adapter wygląda tak:

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

Ta płytka służy do zwiększenia liczby portów GPIO i podłączenia większej liczby urządzeń peryferyjnych do Raspberry. Przy tym 4 sygnały GPIO w takim podłączeniu są wykorzystywane jako sygnały JTAG, dzięki czemu program z Raspberry może wczytać firmware FPGA do PLIS. Z tego powodu to standardowe podłączenie mi nie odpowiada, brakuje 4 sygnałów DPI. Na szczęście dodatkowe gniazda na płytce mają zgodność z pinami Raspberry. Można więc obrócić płytkę o 90 stopni i wciąż połączyć ją z moim Raspberry Pi:

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

Oczywiście, będę musiał użyć zewnętrznego programatora JTAG, ale to nie problem.

Jest jednak mały problem. Nie każdy wyjście FPGA może być używane jako wejście sygnału zegarowego. Jest tylko kilka dedykowanych pinów, które można wykorzystać do tych celów. Tak więc w tym przypadku sygnał CLK z GPIO_0 nie trafia do wejścia FPGA, które można wykorzystać jako wejście sygnału zegarowego. W związku z tym trzeba było podłączyć jeden przewód do płytki. Łączę GPIO_0 z sygnałem KEY[1] na płytce:

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

Teraz opowiem nieco o projekcie w FPGA. Główną trudnością przy generowaniu sygnałów HDMI są bardzo wysokie częstotliwości. Jeśli spojrzeć na pinowanie złącza HDMI, to widać, że sygnały RGB stały się teraz szeregowo różnicowymi sygnałami:

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

Użycie sygnału różnicowego pozwala walczyć z zakłóceniami wspólnymi na linii transmisyjnej. W tym przypadku oryginalny ośmiobitowy kod każdego sygnału koloru przekształca się w 10-bitowy TMDS (Transition-minimized differential signaling). To specjalny sposób kodowania, który pozwala usunąć składową stałą z sygnału oraz zminimalizować przełączenia sygnałów w linii różnicowej. Ponieważ do przesłania jednego bajtu koloru przez szeregową linię transmisyjną trzeba przekazać 10 bitów, oznacza to, że częstotliwość zegarowa serializatora musi być 10 razy wyższa niż częstotliwość pikseli. Weźmy na przykład tryb wideo 1280x720 przy 60 Hz, w którym częstotliwość pikseli wynosi 74,25 MHz. Na serializatorze musi być 742,5 MHz.

Zwykłe FPGA niestety nie są w stanie tego osiągnąć. Na nasze szczęście, w FPGA znajdują się wbudowane wyjścia DDIO. To są wyjścia, które działają jakby jako 2-do-1 serializatory. Oznacza to, że mogą wydawać szeregowo dwa bity na zboczu i opadzie sygnału zegarowego. Tak więc w projekcie FPGA można używać nie 740 MHz, ale 370 MHz, ale trzeba aktywować wyjściowe elementy DDIO w FPGA. A więc 370 MHz to już całkiem osiągalna częstotliwość. Niestety, tryb 1280×720 to maksymalna rozdzielczość. Wyższej rozdzielczości w naszej FPGA Cyclone IV na płytce Mars Rover 2rpi nie osiągniemy.

W projekcie częstotliwość wejściowa pikseli CLK trafia do PLL, gdzie jest mnożona przez 5. Przy tej częstotliwości bajty R, G, B są przekształcane w pary bitów. To robi enkoder TMDS. Orkiestracja kodu w Verilog HDL wygląda następująco:

moduł 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;  // licznik modulus 5
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 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 wyjściowe pary są podawane na wyjście DDIO, które sekwencyjnie wysyła sygnał jednoukładowy na narastającym i opadającym zboczu.

Sam DDIO można było opisać takim kodem Verilog:

moduł 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

Jednak w ten sposób najprawdopodobniej nie zadziała. Należy użyć funkcji ALTDDIO_OUT, aby rzeczywiście aktywować wyjściowe elementy DDIO. W moim projekcie używany jest komponent biblioteczny ALTDDIO_OUT.

Może to wyglądać nieco skomplikowanie, ale działa.

Cały kod źródłowy napisany w Verilog HDL można zobaczyć tutaj, na githubie.

Skontrowana wersja oprogramowania dla FPGA jest zapisywana w chipie EPCS zamontowanym na płycie Mars Rover2rpi. W ten sposób, po podłączeniu zasilania do płyty FPGA, układ FPGA będzie inicjowany z pamięci flash i zacznie działać.

Teraz muszę trochę opowiedzieć o konfiguracji samego Raspberry.

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

Musisz wykonać dwie rzeczy:

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

Podczas edytowania pliku /boot/config.txt należy:

  1. wyłączyć używanie i2c, i2s, spi;
  2. włączyć tryb DPI za pomocą overlay dtoverlay=dpi24;
  3. ustawić tryb wideo 1280×720 60Hz, 24 bity na punkt na DPI;
  4. określić wymaganą liczbę buforów ramki na 2 (max_framebuffers=2, tylko wtedy pojawi się drugie urządzenie /dev/fb1)

Pełny tekst pliku config.txt wygląda tak.

# 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 należy utworzyć plik konfiguracyjny dla serwera X do pracy z dwoma monitorami 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 tak

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

A jeśli jeszcze nie jest zainstalowane, to należy zainstalować Xinerama. Wtedy przestrzeń robocza zostanie w pełni rozszerzona na dwa monitory, jak pokazano na powyższym nagraniu demo.

To chyba wszystko. Teraz właściciele Raspberry Pi3 również mogą korzystać z dwóch monitorów.

Można zobaczyć opis i schemat płytki Mars Rover2rpi tutaj.

Źródło: habr.com

Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS 🔥 Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS | ProHoster