Deuxième moniteur HDMI vers Raspberry Pi3 via interface DPI et carte FPGA


Cette vidéo montre : une carte Raspberry Pi3, à laquelle, via le connecteur GPIO, une carte FPGA Mars Rover2rpi (Cyclone IV) est connectée, à laquelle un moniteur HDMI est connecté. Le deuxième moniteur est connecté via le connecteur HDMI standard du Raspberry Pi3. Tous ensemble, cela fonctionne comme un système à double moniteur.

Ensuite, je vais vous dire comment il est mis en œuvre.

La populaire carte Raspberry Pi3 dispose d'un connecteur GPIO à travers lequel vous pouvez connecter diverses cartes d'extension : capteurs, LED, pilotes de moteur pas à pas et bien plus encore. La fonction spécifique de chaque broche du connecteur dépend de la configuration du port. La configuration GPIO ALT2 permet de basculer le connecteur en mode d'interface DPI, Display Parallel Interface. Il existe des cartes d'extension pour connecter des moniteurs VGA via DPI. Cependant, premièrement, les moniteurs VGA ne sont plus aussi courants que HDMI, et deuxièmement, l'interface numérique s'améliore par rapport à l'analogique. De plus, le DAC sur ces cartes d'extension VGA est généralement réalisé sous la forme de chaînes R-2-R et souvent pas plus de 6 bits par couleur.

En mode ALT2, les broches du connecteur GPIO ont la signification suivante :

Deuxième moniteur HDMI vers Raspberry Pi3 via interface DPI et carte FPGA

Ici, j'ai coloré les broches RVB du connecteur respectivement en rouge, vert et bleu. D'autres signaux importants sont les signaux de synchronisation de balayage V-SYNC et H-SYNC, ainsi que CLK. La fréquence d'horloge CLK est la fréquence à laquelle les valeurs de pixel sont émises vers le connecteur et dépend du mode vidéo sélectionné.

Pour connecter un moniteur HDMI numérique, vous devez capturer les signaux d'interface DPI et les convertir en signaux HDMI. Cela peut être fait, par exemple, en utilisant n'importe quelle carte FPGA. Il s'est avéré que la carte Mars Rover2rpi convient à cet effet. En vérité, l'option principale pour connecter cette carte via un adaptateur spécial ressemble à ceci :

Deuxième moniteur HDMI vers Raspberry Pi3 via interface DPI et carte FPGA

Cette carte sert à augmenter le nombre de ports GPIO et à connecter plus de périphériques au raspberry. En même temps, 4 signaux GPIO avec cette connexion sont utilisés pour les signaux JTAG, de sorte que le programme de la distribution puisse charger le firmware FPGA dans le FPGA. Pour cette raison, une connexion aussi régulière ne me convient pas, les signaux 4 DPI disparaissent. Heureusement, les peignes supplémentaires sur la carte ont un brochage compatible Raspberry. Pour que je puisse faire pivoter la carte de 90 degrés et toujours la connecter à ma framboise :

Deuxième moniteur HDMI vers Raspberry Pi3 via interface DPI et carte FPGA

Bien sûr, vous devrez utiliser un programmeur JTAG externe, mais ce n'est pas un problème.

Il y a encore un petit problème. Toutes les broches FPGA ne peuvent pas être utilisées comme entrée d'horloge. Seules quelques broches dédiées peuvent être utilisées à cette fin. Il s'est donc avéré ici que le signal GPIO_0 CLK n'arrive pas à l'entrée FPGA, qui peut être utilisée comme entrée d'horloge FPGA. Alors tout de même, j'ai dû jeter un affichage sur un foulard. Je connecte le signal GPIO_0 et KEY[1] de la carte :

Deuxième moniteur HDMI vers Raspberry Pi3 via interface DPI et carte FPGA

Maintenant, je vais vous parler un peu du projet dans le FPGA. La principale difficulté dans la formation des signaux HDMI réside dans les très hautes fréquences. En regardant le brochage du connecteur HDMI, vous pouvez voir que les signaux RVB sont maintenant des signaux différentiels en série :

Deuxième moniteur HDMI vers Raspberry Pi3 via interface DPI et carte FPGA

L'utilisation d'un signal différentiel permet de traiter le bruit de mode commun sur la ligne de transmission. Dans ce cas, le code original à huit bits de chaque signal de couleur est converti en un TMDS à 10 bits (signalisation différentielle minimisée par transition). Il s'agit d'une méthode de codage spéciale pour supprimer la composante continue du signal et minimiser la commutation du signal dans la ligne différentielle. Puisqu'il y a maintenant 10 bits à transmettre par octet de couleur sur la ligne série, il s'avère que la fréquence d'horloge du sérialiseur doit être 10 fois supérieure à la fréquence d'horloge des pixels. Si on prend par exemple le mode vidéo 1280x720 60Hz, alors la fréquence pixel de ce mode est de 74,25MHz. Le sérialiseur doit être de 742,5 MHz.

Les FPGA conventionnels ne sont généralement pas capables de cela, malheureusement. Cependant, pour notre chance, les FPGA ont des broches DDIO intégrées. Ce sont des conclusions qui sont déjà, pour ainsi dire, des sérialiseurs 2 pour 1. C'est-à-dire qu'ils peuvent produire deux bits en séquence le long des fréquences d'horloge montantes et descendantes. Cela signifie que dans le projet FPGA, vous pouvez utiliser non pas 740 MHz, mais 370 MHz, mais vous devez utiliser les éléments de sortie DDIO dans le FPGA. Ici, 370 MHz est déjà une fréquence tout à fait réalisable. Malheureusement, le mode 1280×720 est la limite. Une résolution plus élevée ne peut pas être obtenue dans notre FPGA Cyclone IV installé sur la carte Rover2rpi.

Ainsi, dans le projet, la fréquence de pixel d'entrée CLK est transmise à la PLL, où elle est multipliée par 5. À cette fréquence, les octets R, G, B sont convertis en paires de bits. C'est ce que fait l'encodeur TMDS. Le code source sur Verilog HDL ressemble à ceci :

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

Ensuite, les paires de sorties sont envoyées à la sortie DDIO, qui produit séquentiellement un signal d'un bit à la montée et à la descente.

DDIO lui-même pourrait être décrit avec le code Verilog comme ceci :

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

Mais cela ne fonctionnera probablement pas de cette façon. Vous devez utiliser la mégafonction ALTDDIO_OUT d'Altera pour utiliser réellement les éléments de sortie DDIO. Dans mon projet, le composant de bibliothèque ALTDDIO_OUT est utilisé.

Tout cela peut sembler un peu délicat, mais cela fonctionne.

Vous pouvez afficher l'intégralité du code source écrit en Verilog HDL ici même sur github.

Le firmware compilé pour le FPGA est intégré dans la puce EPCS installée sur la carte Mars Rover2rpi. Ainsi, lorsque l'alimentation est appliquée à la carte FPGA, le FPGA s'initialise à partir de la mémoire flash et démarre.

Maintenant, nous devons parler un peu de la configuration du Raspberry lui-même.

Je fais des expériences sur Raspberry PI OS (32 bits) basé sur Debian Buster, Version : Août 2020,
Date de sortie :2020-08-20, version du noyau :5.4.

Deux choses doivent être faites:

  • éditez le fichier config.txt ;
  • créer une configuration de serveur X pour travailler avec deux moniteurs.

Lors de la modification du fichier /boot/config.txt, vous devez :

  1. désactiver l'utilisation de i2c, i2s, spi ;
  2. activer le mode DPI avec superposition dtoverlay=dpi24 ;
  3. régler le mode vidéo 1280×720 60Hz, 24 bits par point par DPI ;
  4. spécifiez le nombre requis de framebuffers 2 (max_framebuffers=2, alors seulement le deuxième périphérique /dev/fb1 apparaîtra)

Le texte complet du fichier config.txt ressemble à ceci.

# 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

Après cela, vous devez créer un fichier de configuration pour que le serveur X utilise deux moniteurs sur deux framebuffer /dev/fb0 et /dev/fb1 :

Mon fichier de configuration est /usr/share/x11/xorg.conf.d/60-dualscreen.conf comme ceci

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

Eh bien, s'il n'est pas déjà installé, vous devez installer Xinerama. Ensuite, l'espace de bureau sera entièrement étendu à deux moniteurs, comme indiqué dans la vidéo de démonstration ci-dessus.

C'est probablement tout. Désormais, les propriétaires de Raspberry Pi3 pourront utiliser deux moniteurs.

La description et le schéma de la carte Mars Rover2rpi peuvent être vois ici.

Source: habr.com