Zweiter HDMI-Monitor für Raspberry Pi3 über den DPI-Schnittstelle und FPGA-Platine


Dieses Video zeigt: die Raspberry Pi3 Platine, an die über den GPIO-Anschluss die FPGA-Platine Mars Rover 2rpi (Cyclone IV) angeschlossen ist, an die ein HDMI-Monitor angeschlossen ist. Ein zweiter Monitor ist über den HDMI-Anschluss der Raspberry Pi3 verbunden. Alles zusammen funktioniert wie ein System mit zwei Monitoren.

Als Nächstes erkläre ich, wie dies umgesetzt wurde.

Auf der beliebten Raspberry Pi3 Platine 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 Konfiguration der Ports ab. Die GPIO-Konfiguration ALT2 ermöglicht es, den Anschluss in den DPI-Modus des Display Parallel Interface zu wechseln. Es gibt Erweiterungsplatinen, um VGA-Monitore über DPI anzuschließen. Allerdings sind VGA-Monitore erstens nicht mehr so verbreitet wie HDMI-Monitore, und zweitens ist die digitale Schnittstelle zunehmend besser als die analoge. Zumal der DAC auf solchen VGA-Erweiterungsplatinen gewöhnlich als R-2-R-Kette implementiert ist und oft nicht mehr als 6 Bit pro Farbe bietet.

Im ALT2-Modus haben die Pins des GPIO-Anschlusses folgende Bedeutung:

Zweiter HDMI-Monitor für Raspberry Pi3 über den DPI-Schnittstelle und FPGA-Platine

Ich habe hier die RGB-Ausgänge des Anschlusses entsprechend in Rot, Grün und Blau eingefärbt. Wichtige andere Signale sind die Synchronisationssignale V-SYNC und H-SYNC sowie CLK. Die Taktfrequenz 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 DPI-Signale des Interfaces erfasst und in HDMI-Signale umgewandelt werden. Dies kann beispielsweise mit einer FPGA-Karte erfolgen. Es stellte sich heraus, dass die Marsrover2rpi-Karte dafür geeignet ist. Ehrlich gesagt, sieht die Hauptverbindung dieser Karte über einen speziellen Adapter so aus:

Zweiter HDMI-Monitor für Raspberry Pi3 über den DPI-Schnittstelle und FPGA-Platine

Diese Platine dient dazu, die Anzahl der GPIO-Ports zu erhöhen und mehr Peripheriegeräte mit dem Raspberry Pi zu verbinden. Dabei werden 4 GPIO-Signale für die JTAG-Signale verwendet, sodass das Programm auf dem Raspberry die FPGA-Firmware in das FPGA laden kann. Daher ist diese Standardverbindung für mich nicht geeignet, da 4 DPI-Signale verloren gehen. Glücklicherweise haben die zusätzlichen Pins auf der Platine eine für Raspberry kompatible Belegung. Ich kann die Platine also um 90 Grad drehen und sie trotzdem mit meinem Raspberry Pi verbinden.

Zweiter HDMI-Monitor für Raspberry Pi3 über den DPI-Schnittstelle und FPGA-Platine

Natürlich werde ich einen externen JTAG-Programmierer verwenden müssen, aber das stellt kein Problem dar.

Es gibt jedoch ein kleines Problem. Nicht jeder FPGA-Pin kann als Taktfrequenzeingang verwendet werden. Es gibt nur einige dedizierte Pins, die dafür genutzt werden können. So ist es auch hier der Fall, dass das GPIO_0-Signal CLK nicht auf den FPGA-Pin gelangt, der als Taktfrequenzeingang verwendet werden könnte. Daher musste ich doch ein Kabel zur Platine ziehen. Ich verbinde GPIO_0 mit dem SIGNAL KEY[1] auf der Platine:

Zweiter HDMI-Monitor für Raspberry Pi3 über den DPI-Schnittstelle und FPGA-Platine

Jetzt erzähle ich Ihnen ein wenig über das Projekt in FPGA. Die größte Herausforderung bei der Erzeugung von HDMI-Signalen sind die sehr hohen Frequenzen. Wenn man sich die Pinbelegung des HDMI-Anschlusses ansieht, wird deutlich, dass die RGB-Signale nun serielle Differenzialsignale sind:

Zweiter HDMI-Monitor für Raspberry Pi3 über den DPI-Schnittstelle und FPGA-Platine

Die Verwendung von Differenzialsignalen hilft, gemeinsame Modulationsstörungen auf der Übertragungsleitung zu reduzieren. Dabei wird der ursprüngliche achtbitige Code jedes FarbSignals in ein 10-Bit TMDS (Transition-minimized Differential Signaling) umgewandelt. Dies ist ein spezielles Kodierungsverfahren, um die Gleichstromkomponente aus dem Signal zu entfernen und die Signalwechsel in der Differenzialleitung zu minimieren. Da nun für ein Byte Farbe entlang der seriellen Übertragungsleitung 10 Bit übertragen werden müssen, bedeutet dies, dass die Taktfrequenz des Serialisierers zehnmal höher sein muss als die Taktfrequenz der Pixel. Wenn man beispielsweise den Video-Modus 1280x720 bei 60 Hz betrachtet, beträgt die Pixel-Frequenz in diesem Modus 74,25 MHz. Der Serialisierer sollte also 742,5 MHz haben.

Standard-FPGAs sind grundsätzlich dazu leider nicht in der Lage. Glücklicherweise sind jedoch in FPGAs eingebaute DDIO-Ausgänge enthalten. Diese Ausgänge fungieren gewissermaßen als 2 zu 1 Serializer. Das bedeutet, dass sie zwei Bits pro takt zum steigenden und fallenden Flanken ausgeben können. Daher kann im FPGA-Projekt nicht 740 MHz, sondern 370 MHz verwendet werden, wobei die DDIO-Ausgänge im FPGA aktiviert werden müssen. Eine Frequenz von 370 MHz ist damit durchaus erreichbar. Leider ist der Modus 1280×720 das Maximum. Eine höhere Auflösung ist mit unserer auf der Marsrover-2rpi-Platine installierten Cyclone IV FPGA nicht möglich.

Im Projekt wird die Eingangsfrequenz der Pixel CLK auf den PLL geleitet, wo sie um das Fünffache multipliziert wird. Bei dieser Frequenz werden die Bytes R, G, B in Bitpaare umgewandelt. Dies wird durch den TMDS-Encoder realisiert. Der Quellcode in Verilog HDL sieht folgendermaßen aus:

modul hdmi(
	input wire pixclk,		// 74MHz
	input wire clk_TMDS2,	// 370MHz
	input wire hsync,
	input wire vsync,
	input wire aktiv,
	input wire [7:0]rot,
	input wire [7:0]grün,
	input wire [7:0]blau,
	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_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;

wire [4:0] TMDS_blau_l  = {TMDS_blau[9],TMDS_blau[7],TMDS_blau[5],TMDS_blau[3],TMDS_blau[1]};
wire [4:0] TMDS_blau_h  = {TMDS_blau[8],TMDS_blau[6],TMDS_blau[4],TMDS_blau[2],TMDS_blau[0]};
wire [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]};
wire [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]};
wire [4:0] TMDS_rot_l   = {TMDS_rot[9],TMDS_rot[7],TMDS_rot[5],TMDS_rot[3],TMDS_rot[1]};
wire [4:0] TMDS_rot_h   = {TMDS_rot[8],TMDS_rot[6],TMDS_rot[4],TMDS_rot[2],TMDS_rot[0]};

always @(posedge clk_TMDS2)
begin
	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);
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

Dann werden die Ausgangspaare an den DDIO-Ausgang ausgegeben, der nacheinander einsetzenden und fallenden Signale ausgibt.

Der DDIO könnte mit folgendem 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

Das wird jedoch wahrscheinlich nicht funktionieren. Um wirklich die Ausgangselemente des DDIO zu nutzen, muss die Altera-Mega-Funktion ALTDDIO_OUT verwendet werden. In meinem Projekt wird genau dieser Bibliothekskomponenten ALTDDIO_OUT eingesetzt.

Vielleicht sieht das alles etwas kompliziert aus, aber es funktioniert.

Den gesamten Quellcode, der in Verilog HDL geschrieben wurde, können Sie hier auf GitHub einsehen..

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

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

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

Es müssen zwei Dinge erledigt werden:

  • die Datei config.txt bearbeiten;
  • eine X-Server-Konfiguration für den Betrieb mit zwei Monitoren erstellen.

Beim Bearbeiten der Datei /boot/config.txt müssen folgende Schritte beachtet werden:

  1. Deaktivieren Sie die Nutzung von i2c, i2s, spi;
  2. Aktivieren Sie den DPI-Modus mit dem Overlay dtoverlay=dpi24;
  3. Konfigurieren Sie die Videoauflösung auf 1280×720 bei 60 Hz und 24 Bit pro Pixel für DPI;
  4. Geben Sie die benötigte Anzahl an Frame-Buffer an, 2 (max_framebuffers=2, nur dann wird das zweite Gerät /dev/fb1 angezeigt);

Der vollständige Inhalt 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 müssen Sie eine Konfigurationsdatei für den X-Server erstellen, um zwei Monitore mit den beiden Frame-Buffern /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, sollte Xinerama installiert werden. Dann wird der Desktop-Bereich auf zwei Monitore vollständig erweitert, wie im obigen Demovideo gezeigt.

Das sollte es sein. Jetzt können auch Besitzer des Raspberry Pi3 zwei Monitore nutzen.

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

Quelle: habr.com

Zuverlässiges Webhosting mit DDoS-Schutz, VPS- und VDS-Server kaufen 🔥 Zuverlässiges Webhosting mit DDoS-Schutz, VPS- und VDS-Server kaufen | ProHoster