На этом видео показаны: плата 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 имеют следующее значение:
Я здесь раскрасил RGB выводы разъема соответственно в красный, зеленый и синий цвета. Другие важные сигналы это сигналы синхронизации развертки V-SYNC и H-SYNC, а так же CLK. Тактовая частота CLK это частота, с которой значения пикcелей выдаются на разъем, она зависит от выбранного видеорежима.
Для подключения цифрового HDMI монитора нужно захватить сигналы DPI интерфейса и преобразовать их в сигналы HDMI. Сделать это можно, например, с помощью какой либо FPGA платы. Как оказалось, плата Марсоход2rpi подходит для этих целей. По правде говоря, основной вариант подключения этой платы через специальный переходник выглядит вот так:
Эта плата служит для увеличения числа GPIO портов и для подключение большего числа периферийных устройств к raspberry. При этом, 4 сигнала GPIO при таком подключении используются под JTAG сигналы, так, что программа из распберри может загружать FPGA прошивку в ПЛИС. Из-за этого такое штатное подключение мне не подходит, выпадают 4 DPI сигнала. По счастью, дополнительные гребеночки на плате имеют совместимую с Raspberry распиновку. Так, что я могу развернуть плату на 90 градусов и все равно подключить ее к моей малинке:
Конечно, придется использовать внешний JTAG программатор, но это не проблема.
Небольшая проблема все же есть. Не каждый вывод FPGA может использоваться, как вход тактовой частоты. Есть только несколько dedicated pin, которые можно использовать для этих целей. Так и здесь получилось, что GPIO_0 сигнал CLK не попадает на ввод FPGA, который возможно использовать как вход тактовой частоты ПЛИС. Так что все таки пришлось кинуть один проводок на платку. Я соединяю GPIO_0 и сигнал KEY[1] платы:
Теперь расскажу немного про проект в ПЛИС. Основная сложность при формировании HDMI сигналов это очень высокие частоты. Если посмотреть на цоколевку разъема HDMI, то видно, что сигналы RGB теперь являются последовательными дифференциальными сигналами:
Использование дифференциального сигнала позволяет бороться с синфазными помехами на линии передачи. При этом, исходный восьмибитный код каждого сигнала цвета преобразуется в 10-ти битный TMDS (Transition-minimized differential signaling). Это специальный способ кодирования для удаления постоянной составляющей из сигнала и минимизации переключений сигналов в дифференциальной линии. Поскольку на один байт цвета теперь по последовательной линии передачи нужно передать 10 бит, то получается, что тактовая частота сериализатора должна быть в 10 раз выше, чем тактовая частота пикселей. Если взять к примеру видео режим 1280х720 60Гц, то частота пикселей у такого режима 74,25МГц. На сериализаторе должно быть 742,5МГц.
Обычные FPGA вообще-то на такое, к сожалению, не способны. Однако, по нашему счастью, в FPGA имеются встроенные выводы DDIO. Это такие выводы, которые уже как бы являются сериализаторами 2-к-1. То есть они могут выдавать последовательно два бита по фронту и спаду тактовой частоты. Значит в проекте FPGA можно использовать не 740МГц, а 370МГц, но нужно задейcтвовать выходные элементы DDIO в ПЛИС. Вот 370МГц уже вполне достижимая частота. К сожалению, режим 1280x720 это предел. Более высокого разрешения в нашей 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) based on Debian Buster, Version:August 2020,
Release date:2020-08-20, Kernel version:5.4.
Нужно сделать две вещи:
- отредактировать файл config.txt;
- создать конфигурацию X сервера для работы с двумя мониторами.
При редактировании файла /boot/config.txt нужно:
- выключить использование i2c, i2s, spi;
- включить режим DPI с помощью оверлея dtoverlay=dpi24;
- настроить видеорежим 1280x720 60Гц, 24 бита на точку на DPI;
- указать необходимое количество фреймбуфферов 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 можно посмотреть вот здесь.
drWhy
Отличная работа.
Возможно, кому-нибудь пригодится: если достаточно оттенков серого, к примеру для информационных табло, скорее всего можно подключить три монитора к одному выходу hdmi без дополнительного синхронизатора TMDS. По крайней мере на dvi срабатывал трюк с параллельным подключением входов всех трёх каналов TMDS монитора к одному выходному каналу видеокарты + канала синхронизации.
nckma Автор
Я вот кстати теперь думаю не попробовать ли мне таким образом Pi4? Не получу ли я три экрана от одной платы RPI4? Может на выходных займусь…
kilya123
Ну и что там с Pi4? Получилось?