Zweiter HDMI-Monitor für Raspberry Pi3 über den DPI-Anschluss und FPGA-Board


In diesem Video sind zu sehen: das Raspberry Pi3-Board, an das über den GPIO-Anschluss die FPGA-Platine Marsrover2rpi (Cyclone IV) angeschlossen ist, an die ein HDMI-Monitor angeschlossen ist. Ein zweiter Monitor ist über den Standard-HDMI-Anschluss des Raspberry Pi3 angeschlossen. Alles zusammen funktioniert wie ein System mit zwei Monitoren.

Ich werde jetzt erklären, wie das umgesetzt wird.

Auf der beliebten Platine Raspberry Pi3 gibt es einen GPIO-Anschluss, über den verschiedene Erweiterungsplatinen angeschlossen werden können: Sensoren, LEDs, Treiber für Schrittmotoren und vieles mehr. Die spezifische Funktion jedes Pins am Anschluss hängt von der Portkonfiguration ab. Die GPIO-ALT2-Konfiguration ermöglicht es, den Anschluss in den DPI-Schnittstellenmodus, den Display Parallel Interface, zu schalten. Es gibt Erweiterungsplatinen, um VGA-Monitore über DPI anzuschließen. Allerdings sind VGA-Monitore erstens nicht mehr so verbreitet wie HDMI und zweitens ist die digitale Schnittstelle immer besser als die analoge. Zumal die DACs auf solchen VGA-Erweiterungsplatinen normalerweise als R-2-R-Ketten ausgeführt sind und oft nicht mehr als 6 Bit pro Farbe bieten.

Im ALT2-Modus haben die Pins des GPIO-Anschlusses folgenden Wert:

Zweiter HDMI-Monitor für Raspberry Pi3 über den DPI-Anschluss und FPGA-Board

Ich habe hier die RGB-Ausgänge des Anschlusses entsprechend in Rot, Grün und Blau eingefärbt. Weitere wichtige Signale sind die Synchronisationssignale V-SYNC und H-SYNC sowie CLK. Die Taktrate von CLK ist die Frequenz, mit der die Pixelwerte an den Anschluss ausgegeben werden, und sie hängt vom gewählten Videomodus ab.

Um einen digitalen HDMI-Monitor anzuschließen, müssen die Signale der DPI-Schnittstelle erfasst und in HDMI-Signale umgewandelt werden. Dies kann zum Beispiel mit einer FPGA-Platine erfolgen. Wie sich herausstellte, eignet sich die Platine Marsrover2rpi für diese Zwecke. Um ehrlich zu sein, sieht die Hauptanschlussmethode für diese Platine über einen speziellen Adapter so aus:

Zweiter HDMI-Monitor für Raspberry Pi3 über den DPI-Anschluss und FPGA-Board

Diese Platine dient zur Erhöhung der Anzahl der GPIO-Ports und zur Verbindung einer größeren Anzahl peripherer Geräte mit dem Raspberry. Dabei werden 4 GPIO-Signale bei dieser Verbindung für JTAG-Signale verwendet, sodass das Programm vom Raspberry die FPGA-Firmware in die FPGA laden kann. Aus diesem Grund ist diese Standardverbindung für mich nicht geeignet, da 4 DPI-Signale inaktiv sind. Glücklicherweise haben die zusätzlichen Pinleisten auf der Platine eine mit dem Raspberry kompatible Belegung. So kann ich die Platine um 90 Grad drehen und sie dennoch mit meinem Raspberry Pi verbinden:

Zweiter HDMI-Monitor für Raspberry Pi3 über den DPI-Anschluss und FPGA-Board

Natürlich werde ich einen externen JTAG-Programmer verwenden müssen, aber das ist kein Problem.

Es gibt dennoch ein kleines Problem. Nicht jeder FPGA-Ausgang kann als Taktfrequenzeingang verwendet werden. Es gibt nur wenige dedizierte Pins, die hierfür verwendet werden können. So kam es hier auch dazu, dass das GPIO_0-Signal CLK nicht auf den FPGA-Eingang gelangt, der als Taktfrequenzeingang für die FPGA genutzt werden könnte. Daher musste ich doch ein Kabel auf die Platine legen. Ich verbinde GPIO_0 mit dem Signal KEY[1] der Platine:

Zweiter HDMI-Monitor für Raspberry Pi3 über den DPI-Anschluss und FPGA-Board

Jetzt erzähle ich ein wenig über das Projekt im FPGA. Die Hauptschwierigkeit bei der Generierung von HDMI-Signalen sind die sehr hohen Frequenzen. Wenn man sich die Belegung des HDMI-Anschlusses ansieht, sieht man, dass die RGB-Signale nun serielleDifferenzsignale sind:

Zweiter HDMI-Monitor für Raspberry Pi3 über den DPI-Anschluss und FPGA-Board

Die Verwendung von Differenzsignalen ermöglicht die Bekämpfung von Gleichtaktstörungen auf der Übertragungsleitung. Dabei wird der ursprüngliche achtbitige Code jedes Farbkanals in 10-Bit TMDS (Transition-minimized differential signaling) umgewandelt. Dies ist eine spezielle Codierungsmethode zur Entfernung der Gleichanteile im Signal und zur Minimierung von Signalwechseln in der Differenzleitung. Da nun 10 Bit für ein Farbbyte über die serielle Übertragungsleitung gesendet werden müssen, ergibt sich, dass die Taktfrequenz des Serialisierers 10-mal höher sein muss als die Taktfrequenz der Pixel. Wenn wir zum Beispiel den Video-Modus 1280x720 60Hz betrachten, hat dieser Modus eine Pixel-Taktfrequenz von 74,25 MHz. Der Serialisierer sollte 742,5 MHz betragen.

Normale FPGAs sind leider nicht in der Lage, so etwas zu leisten. Doch zu unserem Glück gibt es in den FPGAs eingebaute DDIO-Ausgänge. Diese Ausgänge fungieren quasi als 2-zu-1-Serialisierer. Das bedeutet, dass sie bei einer Taktflanke zwei Bits nacheinander ausgeben können. In dem FPGA-Projekt kann also nicht mit 740 MHz, sondern mit 370 MHz gearbeitet werden, wobei die DDIO-Ausgänge im FPGA aktiviert werden müssen. Die 370 MHz sind somit eine gut erreichbare Frequenz. Leider ist der Modus 1280×720 die Grenze. Höhere Auflösungen sind auf unserem FPGA Cyclone IV, der auf der Mars Rover 2rpi-Platine installiert ist, nicht möglich.

Und so kommt in dem Projekt die Eingangsfrequenz der Pixel CLK in den PLL, wo sie um das 5-fache multipliziert wird. Bei dieser Frequenz werden die Bytes R, G, B in Bitpaare umgewandelt. Das erledigt der TMDS-Encoder. Der ursprüngliche Code in Verilog HDL sieht so aus:

Modul hdmi(
	Eingangsleitung pixclk,		// 74MHz
	Eingangsleitung clk_TMDS2,	// 370MHz
	Eingangsleitung hsync,
	Eingangsleitung vsync,
	Eingangsleitung aktiv,
	Eingangsleitung [7:0]rot,
	Eingangsleitung [7:0]grün,
	Eingangsleitung [7:0]blau,
	Ausgangsleitung TMDS_bh,
	Ausgangsleitung TMDS_bl,
	Ausgangsleitung TMDS_gh,
	Ausgangsleitung TMDS_gl,
	Ausgangsleitung TMDS_rh,
	Ausgangsleitung TMDS_rl
);

Leitung [9:0] TMDS_rot, TMDS_grün, TMDS_blau;
TMDS_encoder encode_R(.clk(pixclk), .VD(rot), .CD({vsync,hsync}), .VDE(aktiv), .TMDS(TMDS_rot));
TMDS_encoder encode_G(.clk(pixclk), .VD(grün), .CD({vsync,hsync}), .VDE(aktiv), .TMDS(TMDS_grün));
TMDS_encoder encode_B(.clk(pixclk), .VD(blau), .CD({vsync,hsync}), .VDE(aktiv), .TMDS(TMDS_blau));

reg [2:0] TMDS_mod5=0;  // Modulus 5 Zähler
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;

Leitung [4:0] TMDS_blau_l  = {TMDS_blau[9],TMDS_blau[7],TMDS_blau[5],TMDS_blau[3],TMDS_blau[1]};
Leitung [4:0] TMDS_blau_h  = {TMDS_blau[8],TMDS_blau[6],TMDS_blau[4],TMDS_blau[2],TMDS_blau[0]};
Leitung [4:0] TMDS_grün_l = {TMDS_grün[9],TMDS_grün[7],TMDS_grün[5],TMDS_grün[3],TMDS_grün[1]};
Leitung [4:0] TMDS_grün_h = {TMDS_grün[8],TMDS_grün[6],TMDS_grün[4],TMDS_grün[2],TMDS_grün[0]};
Leitung [4:0] TMDS_rot_l   = {TMDS_rot[9],TMDS_rot[7],TMDS_rot[5],TMDS_rot[3],TMDS_rot[1]};
Leitung [4:0] TMDS_rot_h   = {TMDS_rot[8],TMDS_rot[6],TMDS_rot[4],TMDS_rot[2],TMDS_rot[0]};

immer @(posedge clk_TMDS2)
beginn
	TMDS_shift_bh <= TMDS_mod5[2] ? TMDS_blau_h  : TMDS_shift_bh  [4:1];
	TMDS_shift_bl <= TMDS_mod5[2] ? TMDS_blau_l  : TMDS_shift_bl  [4:1];
	TMDS_shift_gh <= TMDS_mod5[2] ? TMDS_grün_h : TMDS_shift_gh  [4:1];
	TMDS_shift_gl <= TMDS_mod5[2] ? TMDS_grün_l : TMDS_shift_gl  [4:1];
	TMDS_shift_rh <= TMDS_mod5[2] ? TMDS_rot_h   : TMDS_shift_rh  [4:1];
	TMDS_shift_rl <= TMDS_mod5[2] ? TMDS_rot_l   : TMDS_shift_rl  [4:1];
	TMDS_mod5  4'd4) || (Nb1s == 4'd4 && VD[0] == 1'b0);
Leitung [8:0] q_m = {~XNOR, q_m[6:0] ^ VD[7:1] ^ {7{XNOR}}, VD[0]};

reg [3:0] balance_acc = 0;
Leitung [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;
Leitung balance_sign_eq = (balance[3] == balance_acc[3]);
Leitung invert_q_m = (balance == 0 || balance_acc == 0) ? ~q_m[8] : balance_sign_eq;
Leitung [3:0] balance_acc_inc = balance - ({q_m[8] ^ ~balance_sign_eq} & ~(balance == 0 || balance_acc == 0));
Leitung [3:0] balance_acc_new = invert_q_m ? balance_acc - balance_acc_inc : balance_acc + balance_acc_inc;
Leitung [9:0] TMDS_data = {invert_q_m, q_m[8], q_m[7:0] ^ {8{invert_q_m}}};
Leitung [9:0] TMDS_code = CD[1] ? (CD[0] ? 10'b1010101011 : 10'b0101010100) : (CD[0] ? 10'b0010101011 : 10'b1101010100);

immer @(posedge clk) TMDS <= VDE ? TMDS_data : TMDS_code;
immer @(posedge clk) balance_acc <= VDE ? balance_acc_new : 4'h0;

endmodule

Dann werden die Ausgangspaare an den DDIO-Ausgang weitergeleitet, der sukzessive ein Einbitsignal sowohl bei der steigenden als auch bei der fallenden Flanke ausgibt.

Der DDIO könnte mit folgendem Verilog-Code beschrieben werden:

Modul ddio(
	Eingangsleitung d0,
	Eingangsleitung d1,
	Eingangsleitung clk,
	Ausgangsleitung out
	);

reg r_d0;
reg r_d1;
immer @(posedge clk)
beginn
	r_d0 <= d0;
	r_d1 <= d1;
end
assign out = clk ? r_d0 : r_d1;
endmodule

So wird es wahrscheinlich nicht funktionieren. Es braucht die alternative Mega-Funktion ALTDDIO_OUT, um tatsächlich die DDIO-Ausgabeelemente zu aktivieren. In meinem Projekt wird genau das Bibliotheksmodul ALTDDIO_OUT verwendet.

Das mag vielleicht etwas kompliziert aussehen, aber es funktioniert.

Den gesamten Quellcode, der in Verilog HDL geschrieben ist, kann man hier auf GitHub ansehen..

Die kompilierte Firmware für das FPGA wird in den EPCS-Chip geschrieben, der auf der Marsrover2rpi-Platine installiert ist. Somit wird das FPGA beim Einschalten der Platine aus dem Flash-Speicher initialisiert und gestartet.

Jetzt möchte ich etwas über die Konfiguration des Raspberry selbst erzählen.

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

Es müssen zwei Dinge gemacht werden:

  • Die Datei config.txt bearbeiten;
  • Eine Konfiguration des X-Servers für den Betrieb mit zwei Monitoren erstellen.

Beim Bearbeiten der Datei /boot/config.txt sollte man:

  1. Die Nutzung von i2c, i2s, spi deaktivieren;
  2. Den DPI-Modus mit dem Overlay dtoverlay=dpi24 aktivieren;
  3. Den Videomodus 1280×720 60Hz, 24 Bit pro Pixel auf DPI einstellen;
  4. Die benötigte Anzahl an Frame-Puffern auf 2 festlegen (max_framebuffers=2, nur dann erscheint das zweite Gerät /dev/fb1)

Der vollständige Text der Datei config.txt sieht so 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

Anschließend muss eine Konfigurationsdatei für den X-Server erstellt werden, um zwei Monitore mit zwei Frame-Puffern /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

Und falls noch nicht installiert, muss Xinerama aktiviert werden. Dann wird der Desktop-Bereich auf zwei Monitore vollständig erweitert, wie im obigen Demovideo gezeigt.

Das wäre wohl alles. Jetzt können auch die Besitzer eines Raspberry Pi3 zwei Monitore verwenden.

Die Beschreibung und das Diagramm der Platine Marsrover2rpi können hier angesehen werden..

Quelle: habr.com

60GB SSD 8Gb DDR4