Другий HDMI монітор до Raspberry Pi3 через DPI інтерфейс та FPGA плату


На цьому відео показано: плата Raspberry Pi3, до неї через роз'єм GPIO, підключена FPGA плата Марсоход2rpi (Cyclone IV), до якої підключений HDMI монітор. Другий монітор підключений через штатний роз'єм HDMI Raspberry Pi3. Все разом працює як система з двома моніторами.

Далі розповім, як це реалізовано.

На популярній платі Raspberry Pi3 є роз'єм GPIO, через який можна підключати різні плати розширення: датчики, світлодіоди, драйвера крокових двигунів та багато іншого. Конкретна функція кожного виведення на роз'єм залежить від конфігурації портів. Конфігурація GPIO ALT2 дозволяє переключити роз'єм у режим DPI інтерфейсу, Display Parallel Interface. Існують плати розширення для підключення VGA моніторів через DPI. Однак, по-перше, монітори VGA вже не такі поширені, як HDMI, а по-друге, цифровий інтерфейс все краще аналогового. Тим більше, що ЦАП на подібних платах VGA розширення зазвичай виконаний у вигляді R-2-R ланцюжків і часто не більше 6 біт на колір.

У режимі ALT2 піни роз'єму GPIO мають таке значення:

Другий HDMI монітор до Raspberry Pi3 через DPI інтерфейс та FPGA плату

Я тут розфарбував RGB висновки роз'єму відповідно у червоний, зелений та синій кольори. Інші важливі сигнали - це сигнали синхронізації розгортки V-SYNC і H-SYNC, а також CLK. Тактова частота CLK це частота, з якою значення пікселів видаються на роз'єм, вона залежить від вибраного відеорежиму.

Для підключення цифрового HDMI монітора потрібно захопити сигнали DPI інтерфейсу та перетворити їх на сигнали HDMI. Зробити це можна, наприклад, за допомогою якоїсь FPGA плати. Як виявилося, плата Марсоход2rpi підходить для цих цілей. Правду кажучи, основний варіант підключення цієї плати через спеціальний перехідник виглядає ось так:

Другий HDMI монітор до Raspberry Pi3 через DPI інтерфейс та FPGA плату

Ця плата служить для збільшення числа портів GPIO і для підключення більшої кількості периферійних пристроїв до raspberry. При цьому, 4 сигнали GPIO при такому підключенні використовуються під JTAG сигнали, так, що програма розпберрі може завантажувати FPGA прошивку в ПЛІС. Через це таке штатне підключення мені не підходить, випадають 4 DPI сигнали. На щастя, додаткові гребіночки на платі мають сумісну з Raspberry розпинування. Так що я можу розвернути плату на 90 градусів і все одно підключити її до моєї малинки:

Другий HDMI монітор до Raspberry Pi3 через DPI інтерфейс та FPGA плату

Звичайно, доведеться використовувати зовнішній JTAG програматор, але це не проблема.

Невелика проблема все ж таки є. Не кожен висновок FPGA може використовуватися як вхід тактової частоти. Є лише кілька dedicated pin, які можна використовувати з цією метою. Так і тут вийшло, що GPIO_0 сигнал CLK не потрапляє на введення FPGA, який можна використовувати як вхід тактової частоти ПЛІС. Так що ж довелося кинути один проводок на хустку. Я з'єдную GPIO_0 і сигнал KEY[1] плати:

Другий HDMI монітор до Raspberry Pi3 через DPI інтерфейс та FPGA плату

Тепер розповім трохи про проект у ПЛІС. Основна складність для формування HDMI сигналів це дуже високі частоти. Якщо подивитися на цоколівку роз'єму HDMI, то видно, що RGB сигнали тепер є послідовними диференціальними сигналами:

Другий HDMI монітор до Raspberry Pi3 через DPI інтерфейс та FPGA плату

Використання диференціального сигналу дозволяє боротися із синфазними перешкодами на лінії передачі. При цьому вихідний восьмибітний код кожного сигналу кольору перетворюється на 10-бітний TMDS (Transition-minimized differential signaling). Це спеціальний спосіб кодування для видалення постійної складової сигналу і мінімізації перемикань сигналів в диференціальній лінії. Оскільки на один байт кольору тепер по послідовній лінії передачі потрібно передати 10 біт, виходить, що тактова частота серіалізатора повинна бути в 10 разів вище, ніж тактова частота пікселів. Якщо взяти наприклад відео режим 1280х720 60Гц, то частота пікселів такого режиму 74,25МГц. На серіалізаторі має бути 742,5 МГц.

Звичайні FPGA взагалі на таке, на жаль, не здатні. Однак, на нашу щастя, FPGA є вбудовані висновки DDIO. Це такі висновки, які вже є серіалізаторами 2-к-1. Тобто вони можуть видавати послідовно два біти по фронту та спаду тактової частоти. Значить у проекті FPGA можна використовувати не 740МГц, а 370МГц, але потрібно задіяти вихідні елементи DDIO в ПЛІС. Ось 370МГц вже цілком досяжна частота. На жаль, режим 1280 720 це межа. Більш високого дозволу у нашій FPGA Cyclone IV встановленої на платі Марсоход2rpi не досягти.

Отже, у проекті вхідна частота пікселів CLK надходить на PLL, де множиться на 5. На цій частоті байти R, G, B, перетворюються на пари біт. Це робить TMDS енкодер. Вихідний код на Verilog HDL виглядає так:

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

Потім вихідні пари подаються на DDIO вихід, який послідовно видає однобітний сигнал фронтом і спаду.

Сам DDIO можна було б описати таким Verilog кодом:

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

Але так працювати швидше за все не буде. Потрібно використовувати альтерівську мегафункцію ALTDDIO_OUT, щоб насправді задіяти вихідні елементи DDIO. У проекті використовується саме бібліотечний компонент ALTDDIO_OUT.

Можливо, все це виглядає трохи хитромудро, але працює.

Переглянути весь вихідний код, написаний на Verilog HDL, можна ось тут, на github.

Скомпільована прошивка для FPGA зашивається в чіп EPCS, встановлений на платі Марсоход2rpi. Таким чином, при подачі харчування на плату FPGA, ПЛІС ініціалізуватиметься з флеш пам'яті і стартуватиме.

Тепер потрібно трохи розповісти про конфігурацію самого Raspberry.

Я роблю експерименти на Raspberry PI OS (32 bit), що базується на Debian Buster, Version:August 2020,
Release date:2020-08-20, Kernel version:5.4.

Потрібно зробити дві речі:

  • редагувати файл config.txt;
  • створити конфігурацію сервера X для роботи з двома моніторами.

Під час редагування файлу /boot/config.txt потрібно:

  1. вимкнути використання i2c, i2s, spi;
  2. увімкнути режим DPI за допомогою оверлею dtoverlay = dpi24;
  3. налаштувати відеорежим 1280 720 60Гц, 24 біта на точку на DPI;
  4. вказати необхідну кількість фреймбуфферів 2 (max_framebuffers=2, тільки тоді з'явиться другий пристрій /dev/fb1)

Повний текст файлу config.txt виглядає так.

# 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

Після цього, потрібно створити конфігураційний файл для сервера X для використання двох моніторів на двох фреймбуферах /dev/fb0 і /dev/fb1:

Мій файл конфігурації /usr/share/x11/xorg.conf.d/60-dualscreen.conf такий

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

Ну і якщо ще не встановлена, то потрібно встановити Xinerama. Тоді простір робочого столу буде повноцінно розширено на два монітори, як показано вище на демо ролику.

Ось мабуть і все. Тепер і власники Raspberry Pi3 зможуть користуватися двома моніторами.

Опис та схему плати Марсоход2rpi можна подивитися ось тут.

Джерело: habr.com