Segundo monitor HDMI para Raspberry Pi3 via interface DPI e placa FPGA


Este vídeo mostra: uma placa Raspberry Pi3, à qual, através do conector GPIO, está conectada uma placa FPGA Mars Rover2rpi (Cyclone IV), à qual está conectado um monitor HDMI. O segundo monitor é conectado através do conector padrão Raspberry Pi3 HDMI. Tudo junto funciona como um sistema de monitor duplo.

A seguir, contarei como isso é implementado.

A popular placa Raspberry Pi3 possui um conector GPIO através do qual você pode conectar várias placas de expansão: sensores, LEDs, drivers de motor de passo e muito mais. A função específica de cada pino no conector depende da configuração da porta. A configuração GPIO ALT2 permite alternar o conector para o modo de interface DPI, Display Parallel Interface. Existem placas de expansão para conectar monitores VGA via DPI. No entanto, em primeiro lugar, os monitores VGA não são mais tão comuns quanto os HDMI e, em segundo lugar, a interface digital está ficando melhor que a analógica. Além disso, o DAC nessas placas de expansão VGA geralmente é feito na forma de cadeias R-2-R e geralmente não mais do que 6 bits por cor.

No modo ALT2, os pinos do conector GPIO têm o seguinte significado:

Segundo monitor HDMI para Raspberry Pi3 via interface DPI e placa FPGA

Aqui pintei os pinos RGB do conector de vermelho, verde e azul respectivamente. Outros sinais importantes são os sinais de sincronização de varredura V-SYNC e H-SYNC, bem como CLK. A frequência do clock CLK é a frequência na qual os valores de pixel são enviados para o conector e depende do modo de vídeo selecionado.

Para conectar um monitor HDMI digital, você precisa capturar os sinais da interface DPI e convertê-los em sinais HDMI. Isso pode ser feito, por exemplo, usando qualquer placa FPGA. Como se viu, a placa Mars Rover2rpi é adequada para essa finalidade. Na verdade, a principal opção para conectar esta placa por meio de um adaptador especial é a seguinte:

Segundo monitor HDMI para Raspberry Pi3 via interface DPI e placa FPGA

Esta placa é usada para aumentar o número de portas GPIO e conectar mais periféricos ao raspberry. Ao mesmo tempo, 4 sinais GPIO com esta conexão são usados ​​para sinais JTAG, para que o programa da distribuição possa carregar o firmware FPGA no FPGA. Por causa disso, uma conexão tão regular não combina comigo, os sinais de 4 DPI desaparecem. Felizmente, os pentes extras na placa têm uma pinagem compatível com Raspberry. Para que eu possa girar a placa 90 graus e ainda conectá-la ao meu framboesa:

Segundo monitor HDMI para Raspberry Pi3 via interface DPI e placa FPGA

Claro, você terá que usar um programador JTAG externo, mas isso não é um problema.

Ainda há um pequeno problema. Nem todo pino FPGA pode ser usado como uma entrada de clock. Existem apenas alguns pinos dedicados que podem ser usados ​​para essa finalidade. Acontece que o sinal GPIO_0 CLK não chega à entrada do FPGA, que pode ser usada como uma entrada de clock do FPGA. Mesmo assim, tive que jogar uma postagem em um lenço. Eu conecto GPIO_0 e sinal KEY[1] da placa:

Segundo monitor HDMI para Raspberry Pi3 via interface DPI e placa FPGA

Agora vou contar um pouco sobre o projeto no FPGA. A principal dificuldade na formação de sinais HDMI são as frequências muito altas. Observando a pinagem do conector HDMI, você pode ver que os sinais RGB agora são sinais diferenciais seriais:

Segundo monitor HDMI para Raspberry Pi3 via interface DPI e placa FPGA

O uso de um sinal diferencial permite lidar com o ruído de modo comum na linha de transmissão. Nesse caso, o código original de oito bits de cada sinal de cor é convertido em um TMDS de 10 bits (sinalização diferencial minimizada por transição). Este é um método de codificação especial para remover o componente DC do sinal e minimizar a troca de sinal na linha diferencial. Como agora há 10 bits a serem transmitidos por byte de cor na linha serial, verifica-se que a frequência de clock do serializador deve ser 10 vezes maior que a frequência de clock dos pixels. Se tomarmos, por exemplo, o modo de vídeo 1280x720 60Hz, a frequência de pixel desse modo é 74,25MHz. O serializador deve ser 742,5 MHz.

FPGAs convencionais geralmente não são capazes disso, infelizmente. No entanto, para nossa sorte, os FPGAs possuem pinos DDIO embutidos. Essas são conclusões que já são, por assim dizer, serializadores 2 para 1. Ou seja, eles podem emitir dois bits em sequência ao longo das frequências de clock crescentes e decrescentes. Isso significa que no projeto FPGA você pode usar não 740 MHz, mas 370 MHz, mas precisa usar os elementos de saída DDIO no FPGA. Aqui 370 MHz já é uma frequência bastante alcançável. Infelizmente, o modo 1280×720 é o limite. Uma resolução mais alta não pode ser alcançada em nosso FPGA Cyclone IV instalado na placa Rover2rpi.

Assim, no projeto, a frequência do pixel de entrada CLK é alimentada ao PLL, onde é multiplicada por 5. Nessa frequência, os bytes R, G, B são convertidos em pares de bits. Isso é o que o codificador TMDS faz. O código-fonte no Verilog HDL se parece com isso:

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

Em seguida, os pares de saída são enviados para a saída DDIO, que produz sequencialmente um sinal de um bit em ascensão e queda.

O próprio DDIO pode ser descrito com o 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

Mas provavelmente não funcionará assim. Você precisa usar a megafunção ALTDDIO_OUT da Altera para realmente usar os elementos de saída DDIO. No meu projeto, o componente de biblioteca ALTDDIO_OUT é usado.

Tudo pode parecer um pouco complicado, mas funciona.

Você pode visualizar todo o código-fonte escrito em Verilog HDL aqui mesmo no github.

O firmware compilado para o FPGA está embutido no chip EPCS instalado na placa Mars Rover2rpi. Assim, quando a alimentação é aplicada à placa FPGA, o FPGA inicializará a partir da memória flash e iniciará.

Agora precisamos falar um pouco sobre a configuração do próprio Raspberry.

Estou fazendo experimentos no Raspberry PI OS (32 bits) baseado no Debian Buster, versão: agosto de 2020,
Data de lançamento:2020-08-20, Versão do kernel:5.4.

Duas coisas precisam ser feitas:

  • edite o arquivo config.txt;
  • crie uma configuração de servidor X para trabalhar com dois monitores.

Ao editar o arquivo /boot/config.txt, você precisa:

  1. desativar o uso de i2c, i2s, spi;
  2. ativar o modo DPI com sobreposição dtoverlay=dpi24;
  3. definir modo de vídeo 1280×720 60 Hz, 24 bits por ponto por DPI;
  4. especifique o número necessário de framebuffers 2 (max_framebuffers=2, somente então aparecerá o segundo dispositivo /dev/fb1)

O texto completo do arquivo config.txt tem esta aparência.

# 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

Depois disso, você precisa criar um arquivo de configuração para o servidor X usar dois monitores em dois framebuffer /dev/fb0 e /dev/fb1:

Meu arquivo de configuração é /usr/share/x11/xorg.conf.d/60-dualscreen.conf assim

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

Bem, se ainda não estiver instalado, você precisará instalar o Xinerama. Em seguida, o espaço da área de trabalho será totalmente estendido para dois monitores, conforme mostrado no vídeo de demonstração acima.

Isso é provavelmente tudo. Agora, os proprietários do Raspberry Pi3 poderão usar dois monitores.

A descrição e o diagrama da placa Mars Rover2rpi podem ser Veja aqui.

Fonte: habr.com