Secondo monitor HDMI per Raspberry Pi3 tramite interfaccia DPI e scheda FPGA


Questo video mostra: una scheda Raspberry Pi3, ad essa collegata tramite il connettore GPIO è una scheda FPGA Mars Rover2rpi (Cyclone IV), alla quale è collegato un monitor HDMI. Il secondo monitor è collegato tramite il connettore HDMI standard del Raspberry Pi3. Tutto funziona insieme come un sistema a doppio monitor.

Successivamente ti dirò come viene implementato.

La popolare scheda Raspberry Pi3 dispone di un connettore GPIO tramite il quale è possibile collegare varie schede di espansione: sensori, LED, driver per motori passo-passo e molto altro. La funzione esatta di ciascun pin su un connettore dipende dalla configurazione della porta. La configurazione GPIO ALT2 consente di commutare il connettore sulla modalità di interfaccia DPI, Display Parallel Interface. Sono disponibili schede di espansione per il collegamento di monitor VGA tramite DPI. Tuttavia, in primo luogo, i monitor VGA non sono più così diffusi come quelli HDMI e, in secondo luogo, l'interfaccia digitale è sempre migliore di quella analogica. Inoltre, il DAC su tali schede di espansione VGA è solitamente realizzato sotto forma di catene R-2-R e spesso non supera i 6 bit per colore.

In modalità ALT2, i pin del connettore GPIO hanno il seguente significato:

Secondo monitor HDMI per Raspberry Pi3 tramite interfaccia DPI e scheda FPGA

Qui ho colorato i pin RGB del connettore rispettivamente in rosso, verde e blu. Altri segnali importanti sono i segnali V-SYNC e H-SYNC, nonché CLK. La frequenza del clock CLK è la frequenza con cui i valori dei pixel vengono emessi al connettore; dipende dalla modalità video selezionata.

Per collegare un monitor HDMI digitale, è necessario acquisire i segnali DPI dell'interfaccia e convertirli in segnali HDMI. Questo può essere fatto, ad esempio, utilizzando una sorta di scheda FPGA. A quanto pare, la scheda Mars Rover2rpi è adatta a questi scopi. In verità, l'opzione principale per collegare questa scheda tramite un adattatore speciale è simile alla seguente:

Secondo monitor HDMI per Raspberry Pi3 tramite interfaccia DPI e scheda FPGA

Questa scheda viene utilizzata per aumentare il numero di porte GPIO e per collegare più dispositivi periferici al raspberry. Allo stesso tempo, 4 segnali GPIO con questa connessione vengono utilizzati per i segnali JTAG, in modo che il programma di Raspberry possa caricare il firmware FPGA nell'FPGA. Per questo motivo questa connessione standard non è adatta a me; i segnali 4 DPI cadono. Fortunatamente, i pettini aggiuntivi sulla scheda hanno una piedinatura compatibile con Raspberry. Quindi posso ruotare la scheda di 90 gradi e collegarla comunque al mio lampone:

Secondo monitor HDMI per Raspberry Pi3 tramite interfaccia DPI e scheda FPGA

Naturalmente dovrai utilizzare un programmatore JTAG esterno, ma questo non è un problema.

C'è ancora un piccolo problema. Non tutti i pin FPGA possono essere utilizzati come ingresso di clock. Esistono solo pochi pin dedicati che possono essere utilizzati per questi scopi. Quindi si è scoperto che il segnale GPIO_0 CLK non raggiunge l'ingresso FPGA, che può essere utilizzato come ingresso clock FPGA. Quindi dovevo ancora mettere un filo sulla sciarpa. Collego GPIO_0 e il segnale KEY[1] della scheda:

Secondo monitor HDMI per Raspberry Pi3 tramite interfaccia DPI e scheda FPGA

Ora ti parlerò un po’ del progetto FPGA. La principale difficoltà nella generazione di segnali HDMI sono le frequenze molto elevate. Se guardi la piedinatura del connettore HDMI, puoi vedere che i segnali RGB ora sono segnali differenziali seriali:

Secondo monitor HDMI per Raspberry Pi3 tramite interfaccia DPI e scheda FPGA

L'uso di un segnale differenziale consente di combattere le interferenze di modo comune sulla linea di trasmissione. In questo caso, il codice originale a otto bit di ciascun segnale di colore viene convertito in un TMDS a 10 bit (segnalazione differenziale minimizzata dalla transizione). Si tratta di un metodo di codifica speciale per rimuovere la componente CC dal segnale e ridurre al minimo la commutazione del segnale in una linea differenziale. Poiché un byte di colore ora richiede 10 bit per essere trasferito sulla linea seriale, risulta che la frequenza di clock del serializzatore deve essere 10 volte superiore alla frequenza di clock dei pixel. Se prendiamo ad esempio la modalità video 1280x720 60Hz, la frequenza dei pixel di questa modalità è 74,25 MHz. Il serializzatore dovrebbe essere 742,5 MHz.

Gli FPGA normali, sfortunatamente, non sono in grado di farlo. Tuttavia, fortunatamente per noi, l'FPGA dispone di pin DDIO integrati. Queste sono conclusioni che sono già, per così dire, serializzatori 2 a 1. Cioè, possono emettere due bit in sequenza sui fronti di salita e di discesa della frequenza di clock. Ciò significa che in un progetto FPGA non è possibile utilizzare 740 MHz, ma 370 MHz, ma è necessario utilizzare elementi di uscita DDIO nell'FPGA. Ora 370 MHz è già una frequenza completamente raggiungibile. Sfortunatamente, la modalità 1280x720 è il limite. Una risoluzione più elevata non può essere raggiunta nel nostro FPGA Cyclone IV installato sulla scheda Mars Rover2rpi.

Quindi, nel progetto, la frequenza del pixel di ingresso CLK va al PLL, dove viene moltiplicata per 5. A questa frequenza, i byte R, G, B vengono convertiti in coppie di bit. Questo è ciò che fa il codificatore TMDS. Il codice sorgente in Verilog HDL è simile al seguente:

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

Quindi le coppie di uscite vengono inviate all'uscita DDIO, che produce in sequenza un segnale a un bit sui fronti di salita e di discesa.

Lo stesso DDIO potrebbe essere descritto con il seguente codice 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

Ma molto probabilmente non funzionerà in questo modo. È necessario utilizzare la megafunzione ALTDDIO_OUT di Alter per abilitare effettivamente gli elementi di output DDIO. Il mio progetto utilizza il componente della libreria ALTDDIO_OUT.

Tutto questo può sembrare un po’ complicato, ma funziona.

È possibile visualizzare tutto il codice sorgente scritto in Verilog HDL qui su github.

Il firmware compilato per l'FPGA viene flashato nel chip EPCS installato sulla scheda Mars Rover2rpi. Pertanto, quando viene applicata l'alimentazione alla scheda FPGA, l'FPGA verrà inizializzato dalla memoria flash e avviato.

Ora dobbiamo parlare un po' della configurazione del Raspberry stesso.

Sto facendo esperimenti sul sistema operativo Raspberry PI (32 bit) basato su Debian Buster, versione: agosto 2020,
Data di rilascio:2020/08/20, Versione kernel:5.4.

Due cose devono essere fatte:

  • modificare il file config.txt;
  • creare una configurazione del server X per funzionare con due monitor.

Quando modifichi il file /boot/config.txt hai bisogno di:

  1. disabilitare l'uso di i2c, i2s, spi;
  2. abilita la modalità DPI utilizzando la sovrapposizione dtoverlay=dpi24;
  3. configurare la modalità video 1280×720 60Hz, 24 bit per pixel su DPI;
  4. specifica il numero richiesto di framebuffers 2 (max_framebuffers=2, solo allora apparirà il secondo dispositivo /dev/fb1)

Il testo completo del file config.txt è simile al seguente.

# 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

Dopodiché, devi creare un file di configurazione per il server X per utilizzare due monitor su due framebuffer /dev/fb0 e /dev/fb1:

Il mio file di configurazione /usr/share/x11/xorg.conf.d/60-dualscreen.conf è così

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

Bene, se non è già installato, devi installare Xinerama. Quindi lo spazio sul desktop verrà completamente espanso a due monitor, come mostrato nel video dimostrativo sopra.

Probabilmente è tutto. Ora, i possessori di Raspberry Pi3 potranno utilizzare due monitor.

È possibile trovare la descrizione e lo schema elettrico della scheda Mars Rover2rpi guarda qui.

Fonte: habr.com