Zweiter HDMI-Monitor an Raspberry Pi3 über DPI-Schnittstelle und FPGA-Board


Dieses Video zeigt: ein Raspberry Pi3-Board, daran angeschlossen ist über den GPIO-Anschluss ein FPGA-Board Mars Rover2rpi (Cyclone IV), an das ein HDMI-Monitor angeschlossen ist. Der Anschluss des zweiten Monitors erfolgt über den Standard-HDMI-Anschluss des Raspberry Pi3. Alles funktioniert wie ein Dual-Monitor-System zusammen.

Als nächstes erzähle ich Ihnen, wie dies umgesetzt wird.

Das beliebte Raspberry Pi3-Board verfügt über einen GPIO-Anschluss, über den Sie verschiedene Erweiterungskarten anschließen können: Sensoren, LEDs, Schrittmotortreiber und vieles mehr. Die genaue Funktion jedes Pins an einem Stecker hängt von der Portkonfiguration ab. Mit der GPIO ALT2-Konfiguration können Sie den Anschluss in den DPI-Schnittstellenmodus „Parallelschnittstelle anzeigen“ umschalten. Für den Anschluss von VGA-Monitoren per DPI gibt es Erweiterungskarten. Allerdings sind VGA-Monitore erstens nicht mehr so ​​verbreitet wie HDMI und zweitens ist die digitale Schnittstelle immer besser als die analoge. Darüber hinaus ist der DAC auf solchen VGA-Erweiterungskarten normalerweise in Form von R-2-R-Ketten und oft nicht mehr als 6 Bit pro Farbe aufgebaut.

Im ALT2-Modus haben die GPIO-Anschlusspins folgende Bedeutung:

Zweiter HDMI-Monitor an Raspberry Pi3 über DPI-Schnittstelle und FPGA-Board

Hier habe ich die RGB-Pins des Steckers jeweils rot, grün und blau eingefärbt. Weitere wichtige Signale sind die Signale V-SYNC und H-SYNC sowie CLK. Die CLK-Taktfrequenz ist die Frequenz, mit der Pixelwerte an den Anschluss ausgegeben werden; sie hängt vom ausgewählten Videomodus ab.

Um einen digitalen HDMI-Monitor anzuschließen, müssen Sie die DPI-Signale der Schnittstelle erfassen und in HDMI-Signale umwandeln. Dies kann beispielsweise über eine Art FPGA-Board erfolgen. Wie sich herausstellt, ist das Mars Rover2rpi-Board für diese Zwecke geeignet. Tatsächlich sieht die Hauptoption zum Anschluss dieser Platine über einen speziellen Adapter so aus:

Zweiter HDMI-Monitor an Raspberry Pi3 über DPI-Schnittstelle und FPGA-Board

Dieses Board wird verwendet, um die Anzahl der GPIO-Ports zu erhöhen und mehr Peripheriegeräte an den Raspberry anzuschließen. Gleichzeitig werden 4 GPIO-Signale mit dieser Verbindung für JTAG-Signale genutzt, sodass das Programm von Raspberry die FPGA-Firmware in das FPGA laden kann. Aus diesem Grund passt dieser Standardanschluss nicht zu mir; 4 DPI-Signale fallen aus. Glücklicherweise verfügen die zusätzlichen Kämme auf der Platine über eine Raspberry-kompatible Pinbelegung. So kann ich das Board um 90 Grad drehen und es trotzdem mit meinem Raspberry verbinden:

Zweiter HDMI-Monitor an Raspberry Pi3 über DPI-Schnittstelle und FPGA-Board

Natürlich müssen Sie einen externen JTAG-Programmierer verwenden, aber das ist kein Problem.

Es gibt immer noch ein kleines Problem. Nicht jeder FPGA-Pin kann als Takteingang verwendet werden. Es gibt nur wenige dedizierte Pins, die für diese Zwecke verwendet werden können. Hier stellte sich also heraus, dass das GPIO_0 CLK-Signal nicht den FPGA-Eingang erreicht, der als FPGA-Takteingang verwendet werden kann. Also musste ich noch einen Draht am Schal anbringen. Ich verbinde GPIO_0 und das KEY[1]-Signal des Boards:

Zweiter HDMI-Monitor an Raspberry Pi3 über DPI-Schnittstelle und FPGA-Board

Jetzt erzähle ich Ihnen ein wenig über das FPGA-Projekt. Die Hauptschwierigkeit bei der Erzeugung von HDMI-Signalen sind sehr hohe Frequenzen. Wenn Sie sich die Pinbelegung des HDMI-Anschlusses ansehen, können Sie erkennen, dass die RGB-Signale jetzt serielle Differenzsignale sind:

Zweiter HDMI-Monitor an Raspberry Pi3 über DPI-Schnittstelle und FPGA-Board

Durch die Verwendung eines Differenzsignals können Sie Gleichtaktstörungen auf der Übertragungsleitung bekämpfen. In diesem Fall wird der ursprüngliche 10-Bit-Code jedes Farbsignals in ein 10-Bit-TMDS (Transition-Minimized Differential Signaling) umgewandelt. Dies ist eine spezielle Codierungsmethode, um die Gleichstromkomponente aus dem Signal zu entfernen und Signalumschaltungen in einer Differenzleitung zu minimieren. Da nun für ein Byte Farbe 10 Bit über die serielle Leitung übertragen werden müssen, stellt sich heraus, dass die Taktrate des Serialisierers zehnmal höher sein muss als die Pixeltaktrate. Nehmen wir zum Beispiel den Videomodus 1280x720 60Hz, dann beträgt die Pixelfrequenz dieses Modus 74,25 MHz. Der Serializer sollte 742,5 MHz sein.

Normale FPGAs sind dazu leider nicht in der Lage. Glücklicherweise verfügt das FPGA jedoch über integrierte DDIO-Pins. Dies sind Schlussfolgerungen, die sozusagen bereits 2-zu-1-Serialisierer sind. Das heißt, sie können zwei Bits nacheinander an der steigenden und abfallenden Flanke der Taktfrequenz ausgeben. Das bedeutet, dass Sie in einem FPGA-Projekt nicht 740 MHz, sondern 370 MHz verwenden können, aber Sie müssen DDIO-Ausgangselemente im FPGA verwenden. Nun sind 370 MHz bereits eine durchaus erreichbare Frequenz. Leider ist der 1280x720-Modus die Grenze. Eine höhere Auflösung kann in unserem Cyclone IV FPGA, der auf dem Mars Rover2rpi-Board installiert ist, nicht erreicht werden.

Im Design geht also die Eingangspixelfrequenz CLK an die PLL, wo sie mit 5 multipliziert wird. Bei dieser Frequenz werden die R-, G- und B-Bytes in Bitpaare umgewandelt. Dies ist, was der TMDS-Encoder tut. Der Quellcode in Verilog HDL sieht folgendermaßen aus:

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

Anschließend werden die Ausgangspaare dem DDIO-Ausgang zugeführt, der an der steigenden und abfallenden Flanke nacheinander ein Ein-Bit-Signal erzeugt.

DDIO selbst könnte mit dem folgenden Verilog-Code beschrieben werden:

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

Aber so wird es höchstwahrscheinlich nicht funktionieren. Sie müssen die Megafunktion ALTDDIO_OUT von Alter verwenden, um die DDIO-Ausgabeelemente tatsächlich zu aktivieren. Mein Projekt verwendet die Bibliothekskomponente ALTDDIO_OUT.

Das sieht vielleicht alles etwas knifflig aus, aber es funktioniert.

Sie können den gesamten in Verilog HDL geschriebenen Quellcode anzeigen hier auf Github.

Die kompilierte Firmware für das FPGA wird in den EPCS-Chip geflasht, der auf dem Mars Rover2rpi-Board installiert ist. Wenn also die FPGA-Karte mit Strom versorgt wird, wird das FPGA aus dem Flash-Speicher initialisiert und gestartet.

Jetzt müssen wir ein wenig über die Konfiguration des Raspberry selbst sprechen.

Ich mache Experimente mit Raspberry PI OS (32 Bit) basierend auf Debian Buster, Version: August 2020,
Veröffentlichungsdatum: 2020, Kernel-Version: 08.

Zwei Dinge müssen getan werden:

  • Bearbeiten Sie die Datei config.txt.
  • Erstellen Sie eine X-Server-Konfiguration, um mit zwei Monitoren zu arbeiten.

Wenn Sie die Datei /boot/config.txt bearbeiten, benötigen Sie:

  1. Deaktivieren Sie die Verwendung von i2c, i2s, spi;
  2. DPI-Modus mit Overlay aktivieren dtoverlay=dpi24;
  3. Konfigurieren Sie den Videomodus 1280×720 60 Hz, 24 Bit pro Pixel auf DPI;
  4. Geben Sie die erforderliche Anzahl an Framebuffers 2 an (max_framebuffers=2, erst dann erscheint das zweite Gerät /dev/fb1)

Der vollständige Text der Datei config.txt sieht folgendermaßen aus.

# 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

Danach müssen Sie eine Konfigurationsdatei für den X-Server erstellen, um zwei Monitore auf zwei Framebuffer /dev/fb0 und /dev/fb1 zu verwenden:

Meine Konfigurationsdatei /usr/share/x11/xorg.conf.d/60-dualscreen.conf sieht so aus

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

Wenn es noch nicht installiert ist, müssen Sie Xinerama installieren. Anschließend wird die Desktop-Fläche vollständig auf zwei Monitore erweitert, wie im Demo-Video oben gezeigt.

Das ist wahrscheinlich alles. Jetzt können Raspberry Pi3-Besitzer zwei Monitore verwenden.

Beschreibung und Schaltplan der Mars Rover2rpi-Platine finden Sie hier Schau hier.

Source: habr.com