Al doilea monitor HDMI către Raspberry Pi3 prin interfața DPI și placa FPGA


Acest videoclip arată: o placă Raspberry Pi3, conectată la aceasta prin conectorul GPIO este o placă FPGA Mars Rover2rpi (Cyclone IV), la care este conectat un monitor HDMI. Al doilea monitor este conectat prin conectorul HDMI standard al Raspberry Pi3. Totul funcționează împreună ca un sistem de monitor dublu.

În continuare, vă voi spune cum este implementat acest lucru.

Populara placă Raspberry Pi3 are un conector GPIO prin care poți conecta diverse plăci de expansiune: senzori, LED-uri, drivere de motoare pas cu pas și multe altele. Funcția exactă a fiecărui pin de pe un conector depinde de configurația portului. Configurația GPIO ALT2 vă permite să comutați conectorul în modul de interfață DPI, Display Parallel Interface. Există plăci de extensie pentru conectarea monitoarelor VGA prin DPI. Totuși, în primul rând, monitoarele VGA nu mai sunt la fel de comune ca HDMI, iar în al doilea rând, interfața digitală este din ce în ce mai bună decât cea analogică. Mai mult, DAC-ul de pe astfel de plăci de expansiune VGA este de obicei realizat sub formă de lanțuri R-2-R și adesea nu mai mult de 6 biți pe culoare.

În modul ALT2, pinii conectorului GPIO au următoarea semnificație:

Al doilea monitor HDMI către Raspberry Pi3 prin interfața DPI și placa FPGA

Aici am colorat pinii RGB ai conectorului în roșu, verde și, respectiv, albastru. Alte semnale importante sunt semnalele V-SYNC și H-SYNC, precum și CLK. Frecvența ceasului CLK este frecvența la care valorile pixelilor sunt transmise conectorului; aceasta depinde de modul video selectat.

Pentru a conecta un monitor digital HDMI, trebuie să captați semnalele DPI ale interfeței și să le convertiți în semnale HDMI. Acest lucru se poate face, de exemplu, folosind un fel de placă FPGA. După cum se dovedește, placa Mars Rover2rpi este potrivită pentru aceste scopuri. De fapt, principala opțiune pentru conectarea acestei plăci printr-un adaptor special arată astfel:

Al doilea monitor HDMI către Raspberry Pi3 prin interfața DPI și placa FPGA

Această placă este folosită pentru a crește numărul de porturi GPIO și pentru a conecta mai multe dispozitive periferice la raspberry. Totodată, pentru semnalele JTAG sunt folosite 4 semnale GPIO cu această conexiune, astfel încât programul de la Raspberry să poată încărca firmware-ul FPGA în FPGA. Din această cauză, această conexiune standard nu mi se potrivește; semnalele 4 DPI scad. Din fericire, pieptenii suplimentari de pe placă au un pinout compatibil cu Raspberry. Așa că pot roti placa la 90 de grade și o pot conecta în continuare la zmeura mea:

Al doilea monitor HDMI către Raspberry Pi3 prin interfața DPI și placa FPGA

Desigur, va trebui să utilizați un programator JTAG extern, dar aceasta nu este o problemă.

Mai este o mică problemă. Nu orice pin FPGA poate fi folosit ca intrare de ceas. Există doar câțiva pini dedicati care pot fi utilizați în aceste scopuri. Așa că aici s-a dovedit că semnalul GPIO_0 CLK nu ajunge la intrarea FPGA, care poate fi folosită ca intrare de ceas FPGA. Așa că mai trebuia să pun un fir pe eșarfă. Conectez GPIO_0 și semnalul KEY[1] al plăcii:

Al doilea monitor HDMI către Raspberry Pi3 prin interfața DPI și placa FPGA

Acum vă voi spune puțin despre proiectul FPGA. Principala dificultate în generarea semnalelor HDMI sunt frecvențele foarte înalte. Dacă vă uitați la pinout-ul conectorului HDMI, puteți vedea că semnalele RGB sunt acum semnale diferențiale seriale:

Al doilea monitor HDMI către Raspberry Pi3 prin interfața DPI și placa FPGA

Utilizarea unui semnal diferențial vă permite să combateți interferența în modul comun pe linia de transmisie. În acest caz, codul original de opt biți al fiecărui semnal de culoare este convertit într-un TMDS de 10 biți (semnalizare diferențială minimizată prin tranziție). Aceasta este o metodă specială de codare pentru a elimina componenta DC din semnal și pentru a minimiza comutarea semnalului într-o linie diferențială. Deoarece acum 10 biți trebuie să fie transmiși pe linia serială pentru un octet de culoare, se dovedește că viteza ceasului serializatorului trebuie să fie de 10 ori mai mare decât viteza ceasului pixelilor. Dacă luăm de exemplu modul video 1280x720 60Hz, atunci frecvența pixelilor acestui mod este de 74,25 MHz. Serializatorul ar trebui să fie de 742,5 MHz.

FPGA-urile obișnuite nu sunt de fapt capabile de acest lucru, din păcate. Cu toate acestea, din fericire pentru noi, FPGA are pini DDIO încorporați. Acestea sunt concluzii care sunt deja, parcă, serializatoare 2-la-1. Adică, pot scoate doi biți secvențial pe marginile ascendentă și descendentă ale frecvenței de ceas. Aceasta înseamnă că într-un proiect FPGA puteți utiliza nu 740 MHz, ci 370 MHz, dar trebuie să utilizați elemente de ieșire DDIO în FPGA. Acum 370 MHz este deja o frecvență complet realizabilă. Din păcate, modul 1280x720 este limita. O rezoluție mai mare nu poate fi obținută în FPGA-ul nostru Cyclone IV instalat pe placa Mars Rover2rpi.

Deci, în proiectare, frecvența pixelului de intrare CLK merge la PLL, unde este înmulțită cu 5. La această frecvență, octeții R, G, B sunt convertiți în perechi de biți. Aceasta este ceea ce face codificatorul TMDS. Codul sursă din Verilog HDL arată astfel:

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

Apoi perechile de ieșire sunt alimentate la ieșirea DDIO, care produce secvenţial un semnal de un bit pe marginile ascendentă și descendentă.

DDIO în sine ar putea fi descris cu următorul cod 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

Dar cel mai probabil nu va funcționa așa. Trebuie să utilizați megafuncția ALTDDIO_OUT a lui Alter pentru a activa efectiv elementele de ieșire DDIO. Proiectul meu folosește componenta de bibliotecă ALTDDIO_OUT.

Toate acestea pot părea puțin complicate, dar funcționează.

Puteți vizualiza tot codul sursă scris în Verilog HDL aici pe github.

Firmware-ul compilat pentru FPGA este flash în cipul EPCS instalat pe placa Mars Rover2rpi. Astfel, atunci când se aplică alimentarea plăcii FPGA, FPGA va fi inițializat din memoria flash și va porni.

Acum trebuie să vorbim puțin despre configurația Raspberry în sine.

Fac experimente pe sistemul de operare Raspberry PI (32 de biți) bazat pe Debian Buster, versiunea: august 2020,
Data lansării: 2020, Versiunea Kernel: 08.

Trebuie să faci două lucruri:

  • editați fișierul config.txt;
  • creați o configurație de server X pentru a funcționa cu două monitoare.

Când editați fișierul /boot/config.txt aveți nevoie de:

  1. dezactivați utilizarea i2c, i2s, spi;
  2. activați modul DPI folosind suprapunerea dtoverlay=dpi24;
  3. configurați modul video 1280×720 60Hz, 24 biți per pixel pe DPI;
  4. specificați numărul necesar de framebuffer-uri 2 (max_framebuffers=2, numai atunci va apărea al doilea dispozitiv /dev/fb1)

Textul complet al fișierului config.txt arată astfel.

# 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

După aceasta, trebuie să creați un fișier de configurare pentru serverul X pentru a utiliza două monitoare pe două framebuffer-uri /dev/fb0 și /dev/fb1:

Fișierul meu de configurare /usr/share/x11/xorg.conf.d/60-dualscreen.conf este așa

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

Ei bine, dacă nu este deja instalat, atunci trebuie să instalați Xinerama. Apoi spațiul desktop va fi extins complet la două monitoare, așa cum se arată în videoclipul demonstrativ de mai sus.

Probabil asta e tot. Acum, proprietarii Raspberry Pi3 vor putea folosi două monitoare.

Puteți găsi descrierea și schema de circuit a plăcii Mars Rover2rpi uite aici.

Sursa: www.habr.com