Другі 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 біт), заснаваны на 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