Segundo monitor HDMI a Raspberry Pi3 mediante interface DPI e placa FPGA


Neste vídeo móstrase: unha placa Raspberry Pi3 á que, a través do conector GPIO, se conecta unha placa FPGA Mars Rover2rpi (Cyclone IV), á que se conecta un monitor HDMI. O segundo monitor está conectado a través do conector HDMI estándar de Raspberry Pi3. Todos xuntos funcionan como un sistema de monitor dual.

A continuación vouvos contar como se implementa.

A popular placa Raspberry Pi3 ten un conector GPIO a través do cal podes conectar varias placas de expansión: sensores, LEDs, controladores de motor paso a paso e moito máis. A función específica de cada pin do conector depende da configuración do porto. A configuración de GPIO ALT2 permítelle cambiar o conector ao modo de interface DPI, Mostrar interface paralela. Existen placas de expansión para conectar monitores VGA mediante DPI. Non obstante, en primeiro lugar, os monitores VGA xa non son tan comúns como o HDMI e, en segundo lugar, a interface dixital está mellorando que a analóxica. Ademais, o DAC destas tarxetas de expansión VGA adoita facerse en forma de cadeas R-2-R e moitas veces non máis de 6 bits por cor.

No modo ALT2, os pinos do conector GPIO teñen o seguinte significado:

Segundo monitor HDMI a Raspberry Pi3 mediante interface DPI e placa FPGA

Aquí coloreei os pinos RGB do conector en vermello, verde e azul respectivamente. Outros sinais importantes son os sinais de sincronización de varrido V-SYNC e H-SYNC, así como CLK. A frecuencia de reloxo CLK é a frecuencia á que se emiten os valores de píxeles ao conector e depende do modo de vídeo seleccionado.

Para conectar un monitor HDMI dixital, cómpre capturar os sinais da interface DPI e convertelos en sinais HDMI. Isto pódese facer, por exemplo, usando calquera placa FPGA. Como se viu, a placa Mars Rover2rpi é adecuada para este fin. En realidade, a opción principal para conectar esta placa a través dun adaptador especial é así:

Segundo monitor HDMI a Raspberry Pi3 mediante interface DPI e placa FPGA

Esta placa úsase para aumentar o número de portos GPIO e para conectar máis periféricos á framboesa. Ao mesmo tempo, utilízanse 4 sinais GPIO con esta conexión para os sinais JTAG, para que o programa da distribución poida cargar o firmware FPGA no FPGA. Debido a isto, unha conexión tan regular non me convén, os sinais de 4 DPI caen. Afortunadamente, os peites adicionais do taboleiro teñen un pinout compatible con Raspberry. Para poder xirar a placa 90 graos e aínda conectala á miña framboesa:

Segundo monitor HDMI a Raspberry Pi3 mediante interface DPI e placa FPGA

Por suposto, terás que usar un programador JTAG externo, pero isto non é un problema.

Aínda hai un pequeno problema. Non todos os pines FPGA poden usarse como entrada de reloxo. Só hai algúns pinos dedicados que se poden usar para este fin. Entón, resultou aquí que o sinal GPIO_0 CLK non chega á entrada FPGA, que se pode usar como entrada de reloxo FPGA. Así que, de todos os xeitos, tiven que botar unha publicación nunha bufanda. Conecto o sinal GPIO_0 e KEY[1] da placa:

Segundo monitor HDMI a Raspberry Pi3 mediante interface DPI e placa FPGA

Agora vouvos falar un pouco do proxecto na FPGA. A principal dificultade na formación de sinais HDMI son as frecuencias moi altas. Mirando o pinout do conector HDMI, podes ver que os sinais RGB son agora sinais diferenciais en serie:

Segundo monitor HDMI a Raspberry Pi3 mediante interface DPI e placa FPGA

O uso dun sinal diferencial permítelle xestionar o ruído de modo común na liña de transmisión. Neste caso, o código orixinal de oito bits de cada sinal de cor convértese nun TMDS de 10 bits (sinalización diferencial minimizada por transición). Este é un método de codificación especial para eliminar o compoñente DC do sinal e minimizar a conmutación de sinal na liña diferencial. Dado que agora hai 10 bits para transmitir por byte de cor a través da liña serie, resulta que a frecuencia de reloxo do serializador debe ser 10 veces maior que a frecuencia de reloxo dos píxeles. Se tomamos por exemplo o modo de vídeo 1280x720 60Hz, entón a frecuencia de píxeles deste modo é de 74,25MHz. O serializador debe ser de 742,5 MHz.

Os FPGA convencionais xeralmente non son capaces de facelo, por desgraza. Non obstante, para a nosa sorte, as FPGA teñen pinos DDIO incorporados. Son conclusións que xa son, por así dicir, serializadores 2 a 1. É dicir, poden emitir dous bits en secuencia ao longo das frecuencias de reloxo ascendente e descendente. Isto significa que no proxecto FPGA podes usar non 740 MHz, senón 370 MHz, pero cómpre usar os elementos de saída DDIO na FPGA. Aquí 370 MHz xa é unha frecuencia bastante alcanzable. Desafortunadamente, o modo 1280×720 é o límite. Non se pode conseguir unha resolución máis alta na nosa FPGA Cyclone IV instalada na placa Rover2rpi.

Así, no proxecto, a frecuencia do píxel de entrada CLK envíase ao PLL, onde se multiplica por 5. Nesta frecuencia, os bytes R, G, B convértense en pares de bits. Isto é o que fai o codificador TMDS. O código fonte en Verilog HDL ten o seguinte aspecto:

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

A continuación, os pares de saída envíanse á saída DDIO, que produce secuencialmente un sinal dun bit en ascenso e descenso.

O propio DDIO podería describirse co código de Verilog así:

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

Pero probablemente non funcione así. Necesitas usar a megafunción ALTDDIO_OUT de Altera para usar realmente os elementos de saída DDIO. No meu proxecto, úsase o compoñente da biblioteca ALTDDIO_OUT.

Todo pode parecer un pouco complicado, pero funciona.

Podes ver o código fonte completo escrito en Verilog HDL aquí mesmo en github.

O firmware compilado para o FPGA está integrado no chip EPCS instalado na placa Mars Rover2rpi. Así, cando se aplica enerxía á placa FPGA, a FPGA inicializarase desde a memoria flash e iniciarase.

Agora cómpre falar un pouco da configuración da propia Raspberry.

Estou facendo experimentos en Raspberry PI OS (32 bits) baseado en Debian Buster, versión: agosto de 2020,
Data de publicación: 2020-08-20, Versión do núcleo: 5.4.

Debes facer dúas cousas:

  • editar o ficheiro config.txt;
  • cree unha configuración de servidor X para traballar con dous monitores.

Ao editar o ficheiro /boot/config.txt, cómpre:

  1. desactivar o uso de i2c, i2s, spi;
  2. habilitar o modo DPI con superposición dtoverlay=dpi24;
  3. establecer o modo de vídeo 1280×720 60Hz, 24 bits por punto por DPI;
  4. especifica o número necesario de framebuffers 2 (max_framebuffers=2, só entón aparecerá o segundo dispositivo /dev/fb1)

O texto completo do ficheiro config.txt ten este aspecto.

# 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

Despois diso, cómpre crear un ficheiro de configuración para que o servidor X use dous monitores en dous framebuffer /dev/fb0 e /dev/fb1:

O meu ficheiro de configuración é /usr/share/x11/xorg.conf.d/60-dualscreen.conf así

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

Ben, se aínda non está instalado, entón cómpre instalar Xinerama. A continuación, o espazo do escritorio estenderase totalmente a dous monitores, como se mostra no vídeo de demostración anterior.

Iso probablemente sexa todo. Agora, os propietarios de Raspberry Pi3 poderán usar dous monitores.

Descrición e diagrama da placa Mars Rover2rpi pode ser mira aquí.

Fonte: www.habr.com