Segundo monitor HDMI a Raspberry Pi3 a través de interfaz DPI y placa FPGA


En este vídeo se muestra: una placa Raspberry Pi3, a la que, a través del conector GPIO, se le conecta una placa FPGA Mars Rover2rpi (Cyclone IV), a la que se conecta un monitor HDMI. El segundo monitor se conecta a través del conector estándar Raspberry Pi3 HDMI. En conjunto, funciona como un sistema de doble monitor.

A continuación te cuento cómo se implementa.

La popular placa Raspberry Pi3 tiene un conector GPIO a través del cual puedes conectar varias placas de expansión: sensores, LED, controladores de motores paso a paso y mucho más. La función específica de cada pin en el conector depende de la configuración del puerto. La configuración GPIO ALT2 le permite cambiar el conector al modo de interfaz DPI, Display Parallel Interface. Hay placas de expansión para conectar monitores VGA a través de DPI. Sin embargo, en primer lugar, los monitores VGA ya no son tan comunes como los HDMI y, en segundo lugar, la interfaz digital es cada vez mejor que la analógica. Además, el DAC en tales tarjetas de expansión VGA generalmente se fabrica en forma de cadenas R-2-R y, a menudo, no más de 6 bits por color.

En modo ALT2, los pines del conector GPIO tienen el siguiente significado:

Segundo monitor HDMI a Raspberry Pi3 a través de interfaz DPI y placa FPGA

Aquí coloreé los pines RGB del conector rojo, verde y azul respectivamente. Otras señales importantes son las señales de sincronización de barrido V-SYNC y H-SYNC, así como CLK. La frecuencia de reloj CLK es la frecuencia a la que los valores de píxeles se envían al conector y depende del modo de video seleccionado.

Para conectar un monitor HDMI digital, debe capturar señales de interfaz DPI y convertirlas en señales HDMI. Esto se puede hacer, por ejemplo, usando cualquier placa FPGA. Al final resultó que, la placa Mars Rover2rpi es adecuada para este propósito. En verdad, la opción principal para conectar esta placa a través de un adaptador especial se ve así:

Segundo monitor HDMI a Raspberry Pi3 a través de interfaz DPI y placa FPGA

Esta placa se utiliza para aumentar la cantidad de puertos GPIO y conectar más periféricos a la frambuesa. Al mismo tiempo, se utilizan 4 señales GPIO con esta conexión para señales JTAG, de modo que el programa de la distribución pueda cargar el firmware de la FPGA en la FPGA. Debido a esto, una conexión tan regular no me conviene, las señales de 4 DPI se caen. Afortunadamente, los peines adicionales en el tablero tienen un pinout compatible con Raspberry. Para que pueda rotar el tablero 90 grados y aun así conectarlo a mi frambuesa:

Segundo monitor HDMI a Raspberry Pi3 a través de interfaz DPI y placa FPGA

Por supuesto, tendrás que usar un programador JTAG externo, pero esto no es un problema.

Todavía hay un pequeño problema. No todos los pines FPGA se pueden usar como entrada de reloj. Solo hay unos pocos pines dedicados que se pueden usar para este propósito. Entonces resultó que aquí la señal GPIO_0 CLK no llega a la entrada FPGA, que se puede usar como una entrada de reloj FPGA. De todos modos, tuve que tirar una publicación en una bufanda. Conecto la señal GPIO_0 y KEY[1] de la placa:

Segundo monitor HDMI a Raspberry Pi3 a través de interfaz DPI y placa FPGA

Ahora les cuento un poco sobre el proyecto en la FPGA. La principal dificultad en la formación de señales HDMI son las frecuencias muy altas. Mirando el pinout del conector HDMI, puede ver que las señales RGB ahora son señales diferenciales en serie:

Segundo monitor HDMI a Raspberry Pi3 a través de interfaz DPI y placa FPGA

El uso de una señal diferencial le permite lidiar con el ruido de modo común en la línea de transmisión. En este caso, el código original de ocho bits de cada señal de color se convierte en un TMDS (señalización diferencial minimizada por transición) de 10 bits. Este es un método de codificación especial para eliminar el componente de CC de la señal y minimizar la conmutación de señal en la línea diferencial. Dado que ahora hay 10 bits para transmitir por byte de color a través de la línea serial, resulta que la frecuencia de reloj del serializador debe ser 10 veces mayor que la frecuencia de reloj de los píxeles. Si tomamos por ejemplo el modo de video 1280x720 60Hz, entonces la frecuencia de píxeles de este modo es 74,25MHz. El serializador debe ser de 742,5 MHz.

Desafortunadamente, los FPGA convencionales generalmente no son capaces de hacer esto. Sin embargo, para nuestra suerte, los FPGA tienen pines DDIO integrados. Estas son conclusiones que ya son, por así decirlo, serializadores 2 a 1. Es decir, pueden generar dos bits en secuencia a lo largo de las frecuencias de reloj ascendentes y descendentes. Esto significa que en el proyecto FPGA no puede usar 740 MHz, sino 370 MHz, pero necesita usar los elementos de salida DDIO en el FPGA. Aquí 370 MHz ya es una frecuencia bastante alcanzable. Desafortunadamente, el modo 1280×720 es el límite. No se puede lograr una resolución más alta en nuestro FPGA Cyclone IV instalado en la placa Rover2rpi.

Entonces, en el proyecto, la frecuencia de píxel de entrada CLK se alimenta al PLL, donde se multiplica por 5. En esta frecuencia, los bytes R, G, B se convierten en pares de bits. Esto es lo que hace el codificador TMDS. El código fuente en Verilog HDL se ve así:

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

Luego, los pares de salida se alimentan a la salida DDIO, que produce secuencialmente una señal de un bit en subida y bajada.

DDIO en sí podría describirse con un código Verilog como este:

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 no funcionará de esa manera. Debe usar la megafunción ALTDDIO_OUT de Altera para usar realmente los elementos de salida DDIO. En mi proyecto, se usa el componente de biblioteca ALTDDIO_OUT.

Todo puede parecer un poco complicado, pero funciona.

Puede ver el código fuente completo escrito en Verilog HDL aquí mismo en github.

El firmware compilado para la FPGA está integrado en el chip EPCS instalado en la placa Mars Rover2rpi. Por lo tanto, cuando se aplica energía a la placa FPGA, la FPGA se inicializará desde la memoria flash y se iniciará.

Ahora tenemos que hablar un poco sobre la configuración de la propia Raspberry.

Estoy haciendo experimentos en Raspberry PI OS (32 bit) basado en Debian Buster, Versión: agosto de 2020,
Fecha de lanzamiento:2020-08-20, Versión del kernel:5.4.

Hay que hacer dos cosas:

  • edite el archivo config.txt;
  • cree una configuración de servidor X para trabajar con dos monitores.

Al editar el archivo /boot/config.txt, debe:

  1. deshabilitar el uso de i2c, i2s, spi;
  2. habilitar el modo DPI con superposición dtoverlay=dpi24;
  3. establezca el modo de video 1280 × 720 60 Hz, 24 bits por punto por DPI;
  4. especifique el número requerido de framebuffers 2 (max_framebuffers=2, solo entonces aparecerá el segundo dispositivo /dev/fb1)

El texto completo del archivo config.txt tiene 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

Después de eso, debe crear un archivo de configuración para que el servidor X use dos monitores en dos framebuffer /dev/fb0 y /dev/fb1:

Mi archivo de configuración es /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

Bueno, si aún no está instalado, entonces necesita instalar Xinerama. Luego, el espacio del escritorio se extenderá por completo a dos monitores, como se muestra en el video de demostración anterior.

Eso es probablemente todo. Ahora, los propietarios de Raspberry Pi3 podrán usar dos monitores.

La descripción y el diagrama de la placa Mars Rover2rpi se pueden mira aquí.

Fuente: habr.com