Anden HDMI-skærm til Raspberry Pi3 via DPI-interface og FPGA-kort


Denne video viser: et Raspberry Pi3-kort, forbundet til det via GPIO-stikket, er et FPGA-kort Mars Rover2rpi (Cyclone IV), som en HDMI-skærm er tilsluttet. Den anden skærm er tilsluttet via standard HDMI-stikket på Raspberry Pi3. Alt fungerer sammen som et dobbelt skærmsystem.

Dernæst vil jeg fortælle dig, hvordan dette implementeres.

Det populære Raspberry Pi3-kort har et GPIO-stik, hvorigennem du kan tilslutte forskellige udvidelseskort: sensorer, LED'er, stepmotordrivere og meget mere. Den nøjagtige funktion af hvert ben på et stik afhænger af portkonfigurationen. GPIO ALT2-konfigurationen giver dig mulighed for at skifte stikket til DPI-interfacetilstand, Display Parallel Interface. Der findes udvidelseskort til tilslutning af VGA-skærme via DPI. Men for det første er VGA-skærme ikke længere så almindelige som HDMI, og for det andet er det digitale interface stadig bedre end det analoge. Desuden er DAC'en på sådanne VGA-udvidelseskort normalt lavet i form af R-2-R-kæder og ofte ikke mere end 6 bits pr. farve.

I ALT2-tilstand har GPIO-stikbenene følgende betydning:

Anden HDMI-skærm til Raspberry Pi3 via DPI-interface og FPGA-kort

Her har jeg farvet stikkets RGB pins henholdsvis røde, grønne og blå. Andre vigtige signaler er V-SYNC og H-SYNC signaler samt CLK. CLK-clockfrekvensen er den frekvens, hvormed pixelværdier udsendes til stikket; det afhænger af den valgte videotilstand.

For at tilslutte en digital HDMI-skærm skal du fange DPI-signalerne fra grænsefladen og konvertere dem til HDMI-signaler. Dette kan for eksempel gøres ved at bruge en form for FPGA-kort. Som det viser sig, er Mars Rover2rpi-brættet velegnet til disse formål. I sandhed ser hovedmuligheden for at forbinde dette kort gennem en speciel adapter sådan ud:

Anden HDMI-skærm til Raspberry Pi3 via DPI-interface og FPGA-kort

Dette kort bruges til at øge antallet af GPIO-porte og til at forbinde flere perifere enheder til hindbæret. Samtidig bruges 4 GPIO-signaler med denne forbindelse til JTAG-signaler, så programmet fra Raspberry kan indlæse FPGA-firmwaren i FPGA'en. På grund af dette passer denne standardforbindelse ikke til mig; 4 DPI-signaler falder ud. Heldigvis har de ekstra kamme på brættet en Raspberry-kompatibel pinout. Så jeg kan rotere brættet 90 grader og stadig forbinde det til min hindbær:

Anden HDMI-skærm til Raspberry Pi3 via DPI-interface og FPGA-kort

Selvfølgelig skal du bruge en ekstern JTAG-programmør, men det er ikke et problem.

Der er stadig et lille problem. Ikke alle FPGA-pinde kan bruges som urindgang. Der er kun få dedikerede stifter, der kan bruges til disse formål. Så det viste sig her, at GPIO_0 CLK-signalet ikke når FPGA-indgangen, som kan bruges som FPGA-clock-indgang. Så jeg skulle stadig sætte en ledning på tørklædet. Jeg forbinder GPIO_0 og kortets KEY[1]-signal:

Anden HDMI-skærm til Raspberry Pi3 via DPI-interface og FPGA-kort

Nu vil jeg fortælle dig lidt om FPGA-projektet. Den største vanskelighed ved at generere HDMI-signaler er meget høje frekvenser. Hvis du ser på HDMI-stikkets pinout, kan du se, at RGB-signalerne nu er serielle differentielle signaler:

Anden HDMI-skærm til Raspberry Pi3 via DPI-interface og FPGA-kort

Brugen af ​​et differentielt signal giver dig mulighed for at bekæmpe common mode interferens på transmissionslinjen. I dette tilfælde konverteres den oprindelige otte-bit kode for hvert farvesignal til en 10-bit TMDS (Transition-minimized differential signaling). Dette er en speciel kodningsmetode til at fjerne DC-komponenten fra signalet og minimere signalomskiftning i en differentiallinje. Da 10 bits nu skal transmitteres over den serielle linje for en byte farve, viser det sig, at serializerens clockhastighed skal være 10 gange højere end pixel clockhastigheden. Hvis vi for eksempel tager videotilstanden 1280x720 60Hz, så er pixelfrekvensen for denne tilstand 74,25 MHz. Serializeren skal være 742,5 MHz.

Almindelige FPGA'er er desværre ikke i stand til dette. Men heldigvis for os har FPGA'en indbyggede DDIO-ben. Det er konklusioner, der allerede så at sige er 2-til-1 serializers. Det vil sige, at de kan udsende to bits sekventielt på de stigende og faldende kanter af klokfrekvensen. Det betyder, at du i et FPGA-projekt ikke kan bruge 740 MHz, men 370 MHz, men du skal bruge DDIO output-elementer i FPGA'en. Nu er 370 MHz allerede en fuldstændig opnåelig frekvens. Desværre er 1280x720-tilstand grænsen. En højere opløsning kan ikke opnås i vores Cyclone IV FPGA installeret på Mars Rover2rpi-kortet.

Så i designet går inputpixelfrekvensen CLK til PLL'en, hvor den ganges med 5. Ved denne frekvens konverteres R, G, B bytes til bitpar. Dette er, hvad TMDS-koderen gør. Kildekoden i Verilog HDL ser sådan ud:

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

Derefter føres udgangsparrene til DDIO-udgangen, som sekventielt producerer et en-bit-signal på de stigende og faldende flanker.

DDIO selv kunne beskrives med følgende Verilog-kode:

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

Men det vil højst sandsynligt ikke fungere sådan. Du skal bruge Alters megafunktion ALTDDIO_OUT for faktisk at aktivere DDIO-outputelementerne. Mit projekt bruger ALTDDIO_OUT bibliotekskomponenten.

Det hele ser måske lidt vanskeligt ud, men det virker.

Du kan se al kildekoden skrevet i Verilog HDL her på github.

Den kompilerede firmware til FPGA'en flashes ind i EPCS-chippen, der er installeret på Mars Rover2rpi-kortet. Når der tilføres strøm til FPGA-kortet, vil FPGA'en blive initialiseret fra flashhukommelsen og starte.

Nu skal vi tale lidt om konfigurationen af ​​selve Raspberry.

Jeg laver eksperimenter på Raspberry PI OS (32 bit) baseret på Debian Buster, Version: August 2020,
Udgivelsesdato:2020-08-20, Kernelversion:5.4.

Du skal gøre to ting:

  • rediger filen config.txt;
  • oprette en X-serverkonfiguration til at fungere med to skærme.

Når du redigerer filen /boot/config.txt, skal du bruge:

  1. deaktiver brugen af ​​i2c, i2s, spi;
  2. aktiver DPI-tilstand ved hjælp af overlay dtoverlay=dpi24;
  3. konfigurer videotilstand 1280×720 60Hz, 24 bits pr. pixel på DPI;
  4. angiv det nødvendige antal framebuffers 2 (max_framebuffers=2, først da vil den anden enhed /dev/fb1 dukke op)

Den fulde tekst af config.txt-filen ser sådan ud.

# 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

Herefter skal du oprette en konfigurationsfil til X-serveren for at bruge to skærme på to framebuffere /dev/fb0 og /dev/fb1:

Min konfigurationsfil /usr/share/x11/xorg.conf.d/60-dualscreen.conf er sådan her

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

Nå, hvis det ikke allerede er installeret, så skal du installere Xinerama. Så bliver skrivebordspladsen fuldt udvidet til to skærme, som vist i demovideoen ovenfor.

Det er nok alt. Nu vil Raspberry Pi3-ejere kunne bruge to skærme.

Beskrivelse og kredsløbsdiagram af Mars Rover2rpi-kortet kan findes se her.

Kilde: www.habr.com