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


Este vídeo mostra: a placa Raspberry Pi 3, à qual está conectada uma placa FPGA Mars Rover 2rpi (Cyclone IV) através do conector GPIO, e um monitor HDMI. Um segundo monitor está conectado através da porta HDMI padrão da Raspberry Pi 3. Tudo funciona como um sistema de dois monitores.

A seguir, vou explicar como isso foi realizado.

Na popular placa Raspberry Pi 3, existe um conector GPIO, através do qual é possível conectar várias placas de expansão: sensores, LEDs, drivers de motores de passo e muito mais. A função específica de cada pino no conector depende da configuração das portas. A configuração GPIO ALT2 permite alternar o conector para o modo DPI da interface, Display Parallel Interface. Há placas de expansão para conectar monitores VGA através do DPI. No entanto, em primeiro lugar, os monitores VGA já não são tão comuns quanto os HDMI e, em segundo lugar, a interface digital é sempre melhor do que a analógica. Além disso, o DAC em placas de expansão VGA costuma ser implementado em forma de cadeias R-2-R e frequentemente não supera 6 bits por cor.

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

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

Aqui, pintei os pinos RGB do conector nas cores vermelha, verde e azul, respectivamente. Outros sinais importantes são os sinais de sincronização de varredura V-SYNC e H-SYNC, além do CLK. A frequência do CLK é a frequência com a qual os valores dos pixels são enviados para o conector, e depende do modo de vídeo escolhido.

Para conectar um monitor HDMI digital, é preciso capturar os sinais da interface DPI e convertê-los para sinais HDMI. Isso pode ser feito, por exemplo, com o uso de alguma placa FPGA. A placa Mars Rover 2rpi se mostrou adequada para esses fins. Na verdade, a principal forma de conectar essa placa através de um adaptador especial é assim:

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

Esta placa serve para aumentar o número de portas GPIO e para conectar um maior número de dispositivos periféricos à Raspberry. Nesse caso, 4 sinais GPIO são usados para sinais JTAG, de modo que o programa da Raspberry pode carregar o firmware FPGA na FPGA. Devido a isso, essa conexão padrão não me serve, 4 sinais DPI ficam fora de uso. Por sorte, os pinos adicionais na placa têm uma pinagem compatível com a Raspberry. Assim, posso girar a placa em 90 graus e ainda conectá-la à minha Raspberry Pi:

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

Claro, terei que usar um programador JTAG externo, mas isso não é um problema.

Ainda há um pequeno problema. Nem todas as saídas FPGA podem ser usadas como entrada de clock. Existem apenas alguns pinos dedicados que podem ser utilizados para esses fins. Assim, aconteceu que o sinal GPIO_0 CLK não chega à entrada FPGA, que pode ser utilizada como entrada de clock da FPGA. Então, tive que colocar um fio na placa. Estou conectando GPIO_0 e o sinal KEY[1] da placa:

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

Agora vou falar um pouco sobre o projeto em FPGA. A dificuldade principal na geração de sinais HDMI são as frequências muito altas. Se olharmos para a pinagem do conector HDMI, podemos ver que os sinais RGB agora são sinais diferenciais sequenciais:

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

O uso de sinal diferencial ajuda a combater as interferências em modo comum na linha de transmissão. Neste caso, o código original de oito bits de cada sinal de cor é convertido em 10 bits TMDS (Transição-minimizada sinalização diferencial). Esta é uma forma especial de codificação para remover a componente contínua do sinal e minimizar as mudanças de sinal na linha diferencial. Como para cada byte de cor agora precisamos transmitir 10 bits por linha de transmissão sequencial, a frequência de clock do serializador deve ser 10 vezes maior que a frequência dos pixels. Por exemplo, no modo de vídeo 1280x720 a 60Hz, a frequência dos pixels desse modo é 74,25MHz. Portanto, o serializador deve operar a 742,5MHz.

FPGA comuns, na verdade, não são capazes disso, infelizmente. No entanto, felizmente, as FPGAs têm saídas DDIO integradas. Essas saídas funcionam como serializadores 2 para 1. Ou seja, elas podem emitir dois bits sequencialmente na borda de subida e descida do clock. Isso significa que no projeto FPGA podemos usar uma frequência de 370MHz em vez de 740MHz, mas precisamos ativar os elementos de saída DDIO na FPGA. Assim, 370MHz é uma frequência completamente alcançável. Infelizmente, o modo 1280×720 é o limite. Resoluções mais altas na nossa FPGA Cyclone IV instalada na placa Mars Rover2rpi não podem ser alcançadas.

Assim, no projeto, a frequência de entrada dos pixels CLK vai para o PLL, onde é multiplicada por 5. A essa frequência, os bytes R, G, B são convertidos em pares de bits. Isso é feito pelo codificador TMDS. O código original em Verilog HDL é assim:

módulo hdmi(
	entrada wire pixclk,		// 74MHz
	entrada wire clk_TMDS2,	// 370MHz
	entrada wire hsync,
	entrada wire vsync,
	entrada wire ativo,
	entrada wire [7:0]vermelho,
	entrada wire [7:0]verde,
	entrada wire [7:0]azul,
	saída wire TMDS_bh,
	saída wire TMDS_bl,
	saída wire TMDS_gh,
	saída wire TMDS_gl,
	saída wire TMDS_rh,
	saída wire TMDS_rl
);

wire [9:0] TMDS_vermelho, TMDS_verde, TMDS_azul;
TMDS_encoder encode_R(.clk(pixclk), .VD(vermelho), .CD({vsync,hsync}), .VDE(ativo), .TMDS(TMDS_vermelho));
TMDS_encoder encode_G(.clk(pixclk), .VD(verde), .CD({vsync,hsync}), .VDE(ativo), .TMDS(TMDS_verde));
TMDS_encoder encode_B(.clk(pixclk), .VD(azul), .CD({vsync,hsync}), .VDE(ativo), .TMDS(TMDS_vermelho));

reg [2:0] TMDS_mod5=0;  // contador de módulo 5
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_azul_l  = {TMDS_azul[9],TMDS_azul[7],TMDS_azul[5],TMDS_azul[3],TMDS_azul[1]};
wire [4:0] TMDS_azul_h  = {TMDS_azul[8],TMDS_azul[6],TMDS_azul[4],TMDS_azul[2],TMDS_azul[0]};
wire [4:0] TMDS_verde_l = {TMDS_verde[9],TMDS_verde[7],TMDS_verde[5],TMDS_verde[3],TMDS_verde[1]};
wire [4:0] TMDS_verde_h = {TMDS_verde[8],TMDS_verde[6],TMDS_verde[4],TMDS_verde[2],TMDS_verde[0]};
wire [4:0] TMDS_vermelho_l   = {TMDS_vermelho[9],TMDS_vermelho[7],TMDS_vermelho[5],TMDS_vermelho[3],TMDS_vermelho[1]};
wire [4:0] TMDS_vermelho_h   = {TMDS_vermelho[8],TMDS_vermelho[6],TMDS_vermelho[4],TMDS_vermelho[2],TMDS_vermelho[0]};

always @(posedge clk_TMDS2)
begin
	TMDS_shift_bh <= TMDS_mod5[2] ? TMDS_azul_h  : TMDS_shift_bh  [4:1];
	TMDS_shift_bl <= TMDS_mod5[2] ? TMDS_azul_l  : TMDS_shift_bl  [4:1];
	TMDS_shift_gh <= TMDS_mod5[2] ? TMDS_verde_h : TMDS_shift_gh  [4:1];
	TMDS_shift_gl <= TMDS_mod5[2] ? TMDS_verde_l : TMDS_shift_gl  [4:1];
	TMDS_shift_rh <= TMDS_mod5[2] ? TMDS_vermelho_h   : TMDS_shift_rh  [4:1];
	TMDS_shift_rl <= TMDS_mod5[2] ? TMDS_vermelho_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

Em seguida, os pares de saída são enviados para a saída DDIO, que emite um sinal de um bit de forma sequencial na borda de subida e descida.

O próprio DDIO poderia ser descrito com o seguinte código Verilog:

módulo ddio(
	entrada wire d0,
	entrada wire d1,
	entrada wire clk,
	saída 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 isso não funcionará. É necessário usar a mega função da Alter para realmente ativar os elementos de saída do DDIO. Meu projeto usa justamente o componente de biblioteca ALTDDIO_OUT.

Talvez isso tudo pareça um pouco complicado, mas funciona.

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

O firmware compilado para o FPGA é gravado no chip EPCS instalado na placa Mars Rover 2rpi. Assim, ao ligar a placa FPGA, o FPGA será inicializado a partir da memória flash e começará.

Agora, é necessário falar um pouco sobre a configuração do Raspberry.

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

É preciso fazer duas coisas:

  • editar o arquivo config.txt;
  • criar a configuração do servidor X para funcionar com dois monitores.

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

  1. desativar o uso de i2c, i2s, spi;
  2. ativar o modo DPI usando o overlay dtoverlay=dpi24;
  3. configurar o modo de vídeo 1280×720 60Hz, 24 bits por pixel no DPI;
  4. indicar 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 é o seguinte.

# 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 para usar dois monitores em dois framebuffers /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

E, se ainda não estiver instalado, você deve instalar o Xinerama. Então, o espaço de trabalho será completamente estendido em dois monitores, como mostrado no vídeo de demonstração acima.

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

Você pode ver a descrição e o esquema da placa Mars Rover2rpi aqui.

Fonte: habr.com

Compre hospedagem confiável para sites com proteção contra DDoS, servidores VPS VDS 🔥 Compre hospedagem confiável para sites com proteção contra DDoS, servidores VPS VDS | ProHoster