第二个 HDMI 监视器通过 DPI 接口和 FPGA 板连接到 Raspberry Pi3


该视频显示:一个 Raspberry Pi3 板,通过 GPIO 连接器连接到一个 FPGA Mars Rover2rpi (Cyclone IV) 板,一个 HDMI 监视器连接到该板。 第二台显示器通过标准的 Raspberry Pi3 HDMI 连接器连接。 总之,它就像一个双显示器系统。

接下来我会告诉你它是如何实现的。

流行的 Raspberry Pi3 板有一个 GPIO 连接器,您可以通过它连接各种扩展板:传感器、LED、步进电机驱动器等等。 连接器上每个引脚的具体功能取决于端口配置。 GPIO ALT2 配置允许您将连接器切换到 DPI 接口模式,显示并行接口。 有用于通过 DPI 连接 VGA 显示器的扩展板。 然而,首先,VGA 显示器不再像 HDMI 那样普遍,其次,数字接口越来越好于模拟接口。 此外,此类 VGA 扩展卡上的 DAC 通常以 R-2-R 链的形式制作,并且每种颜色通常不超过 6 位。

在ALT2模式下,GPIO连接器的引脚含义如下:

第二个 HDMI 监视器通过 DPI 接口和 FPGA 板连接到 Raspberry Pi3

在这里,我将连接器的 RGB 引脚分别着色为红色、绿色和蓝色。 其他重要信号是 V-SYNC 和 H-SYNC 扫描同步信号,以及 CLK。 CLK 时钟频率是像素值输出到连接器的频率,取决于所选的视频模式。

连接数字HDMI显示器,需要采集DPI接口信号,转换成HDMI信号。 例如,这可以使用任何 FPGA 板来完成。 事实证明,Mars Rover2rpi 板适用于此目的。 事实上,通过特殊适配器连接此板的主要选项如下所示:

第二个 HDMI 监视器通过 DPI 接口和 FPGA 板连接到 Raspberry Pi3

该板用于增加 GPIO 端口的数量并将更多外围设备连接到树莓派。 同时,此连接的4个GPIO信号用于JTAG信号,以便分发的程序可以将FPGA固件加载到FPGA中。 因此,这种常规连接不适合我,4 DPI 信号丢失。 幸运的是,板上的额外梳子具有与 Raspberry 兼容的引出线。 这样我就可以将电路板旋转 90 度并仍然将它连接到我的树莓派上:

第二个 HDMI 监视器通过 DPI 接口和 FPGA 板连接到 Raspberry Pi3

当然,您将不得不使用外部 JTAG 编程器,但这不是问题。

还有一个小问题。 并非每个 FPGA 引脚都可以用作时钟输入。 只有少数专用引脚可用于此目的。 所以这里发现 GPIO_0 CLK 信号没有到达 FPGA 输入,可以用作 FPGA 时钟输入。 所以尽管如此,我还是不得不在围巾上张贴一张。 我连接板子的GPIO_0和KEY[1]信号:

第二个 HDMI 监视器通过 DPI 接口和 FPGA 板连接到 Raspberry Pi3

现在我将告诉您一些有关 FPGA 中的项目的信息。 形成 HDMI 信号的主要困难是非常高的频率。 查看 HDMI 连接器的引脚排列,您可以看到 RGB 信号现在是串行差分信号:

第二个 HDMI 监视器通过 DPI 接口和 FPGA 板连接到 Raspberry Pi3

使用差分信号可以让您处理传输线上的共模噪声。 在这种情况下,每个颜色信号的原始八位代码被转换为 10 位 TMDS(最小化传输差分信号)。 这是一种特殊的编码方法,用于去除信号中的直流分量,并最大限度地减少差分线路中的信号切换。 由于现在有 10 位要通过串行线路传输每个字节的颜色,因此串行器的时钟频率必须比像素的时钟频率高 10 倍。 如果我们以视频模式 1280x720 60Hz 为例,那么该模式的像素频率为 74,25MHz。 串行器应为 742,5 MHz。

不幸的是,传统的 FPGA 通常无法做到这一点。 然而,幸运的是,FPGA 具有内置的 DDIO 引脚。 这些结论已经可以说是 2 对 1 序列化程序。 也就是说,它们可以沿时钟频率的上升沿和下降沿依次输出两位。 这意味着在 FPGA 项目中你不能使用 740 MHz,而是 370 MHz,但你需要使用 FPGA 中的 DDIO 输出元件。 在这里,370 MHz 已经是一个可以实现的频率。 不幸的是,1280×720 模式是极限。 安装在 Rover2rpi 板上的 FPGA Cyclone IV 无法实现更高的分辨率。

因此,在该项目中,输入像素频率 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

但它可能不会那样工作。 您需要使用 Altera 的 ALTDDIO_OUT 宏功能来实际使用 DDIO 输出元件。 在我的项目中,使用了库组件 ALTDDIO_OUT。

这可能看起来有点棘手,但它确实有效。

您可以查看用 Verilog HDL 编写的完整源代码 就在github上.

为 FPGA 编译的固件嵌入到安装在 Mars Rover2rpi 板上的 EPCS 芯片中。 因此,当 FPGA 板上加电时,FPGA 将从闪存初始化并启动。

现在我们需要谈谈 Raspberry 本身的配置。

我正在基于 Debian Buster 的 Raspberry PI OS(32 位)进行实验,版本:2020 年 XNUMX 月,
发布日期:2020-08-20,内核版本:5.4。

需要完成两件事:

  • 编辑 config.txt 文件;
  • 创建一个 X 服务器配置以使用两个监视器。

编辑 /boot/config.txt 文件时,需要:

  1. 禁止使用 i2c、i2s、spi;
  2. 使用覆盖 dtoverlay=dpi24 启用 DPI 模式;
  3. 设置视频模式 1280×720 60Hz,每点每 DPI 24 位;
  4. 指定需要的framebuffer数量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 所有者将能够使用两个显示器。

Mars Rover2rpi板的说明和图可以 看这里.

来源: habr.com