Màn hình HDMI thứ hai tới Raspberry Pi3 thông qua giao diện DPI và bo mạch FPGA


Video này hiển thị: một bo mạch Raspberry Pi3, thông qua đầu nối GPIO, một bo mạch FPGA Mars Rover2rpi (Cyclone IV) được kết nối với một màn hình HDMI được kết nối. Màn hình thứ hai được kết nối thông qua đầu nối HDMI Raspberry Pi3 tiêu chuẩn. Tất cả cùng nhau, nó hoạt động giống như một hệ thống màn hình kép.

Tiếp theo tôi sẽ cho bạn biết nó được thực hiện như thế nào.

Bo mạch Raspberry Pi3 phổ biến có đầu nối GPIO qua đó bạn có thể kết nối nhiều bo mạch mở rộng khác nhau: cảm biến, đèn LED, trình điều khiển động cơ bước, v.v. Chức năng cụ thể của từng chân trên đầu nối phụ thuộc vào cấu hình cổng. Cấu hình GPIO ALT2 cho phép bạn chuyển đầu nối sang chế độ giao diện DPI, Giao diện song song hiển thị. Có bảng mở rộng để kết nối màn hình VGA qua DPI. Tuy nhiên, thứ nhất, màn hình VGA không còn phổ biến như HDMI và thứ hai, giao diện kỹ thuật số đang trở nên tốt hơn so với analog. Hơn nữa, DAC trên các card mở rộng VGA như vậy thường được làm ở dạng chuỗi R-2-R và thường không quá 6 bit cho mỗi màu.

Ở chế độ ALT2, các chân của đầu nối GPIO có ý nghĩa như sau:

Màn hình HDMI thứ hai tới Raspberry Pi3 thông qua giao diện DPI và bo mạch FPGA

Ở đây tôi tô màu các chân RGB của đầu nối lần lượt là đỏ, lục và lam. Các tín hiệu quan trọng khác là tín hiệu đồng bộ quét V-SYNC và H-SYNC, cũng như CLK. Tần số xung nhịp CLK là tần số mà các giá trị pixel được xuất ra đầu nối và phụ thuộc vào chế độ video đã chọn.

Để kết nối màn hình HDMI kỹ thuật số, bạn cần thu tín hiệu giao diện DPI và chuyển đổi chúng thành tín hiệu HDMI. Điều này có thể được thực hiện, ví dụ, bằng cách sử dụng bất kỳ bo mạch FPGA nào. Hóa ra, bảng Mars Rover2rpi phù hợp cho mục đích này. Trên thực tế, tùy chọn chính để kết nối bảng này thông qua bộ điều hợp đặc biệt trông như thế này:

Màn hình HDMI thứ hai tới Raspberry Pi3 thông qua giao diện DPI và bo mạch FPGA

Bo mạch này dùng để tăng số lượng cổng GPIO và kết nối nhiều thiết bị ngoại vi hơn với mâm xôi. Đồng thời, 4 tín hiệu GPIO với kết nối này được sử dụng cho tín hiệu JTAG, để chương trình từ bản phân phối có thể tải phần sụn FPGA vào FPGA. Do đó, kết nối thông thường như vậy không phù hợp với tôi, 4 tín hiệu DPI bị loại bỏ. May mắn thay, các lược bổ sung trên bảng có sơ đồ chân tương thích với Raspberry. Để tôi có thể xoay bảng 90 độ mà vẫn kết nối nó với quả mâm xôi của mình:

Màn hình HDMI thứ hai tới Raspberry Pi3 thông qua giao diện DPI và bo mạch FPGA

Tất nhiên, bạn sẽ phải sử dụng một trình lập trình JTAG bên ngoài, nhưng đây không phải là vấn đề.

Vẫn còn một vấn đề nhỏ. Không phải mọi chân FPGA đều có thể được sử dụng làm đầu vào đồng hồ. Chỉ có một số chân chuyên dụng có thể được sử dụng cho mục đích này. Vì vậy, hóa ra ở đây tín hiệu GPIO_0 CLK không đến được đầu vào FPGA, tín hiệu này có thể được sử dụng làm đầu vào đồng hồ FPGA. Vì vậy, tất cả đều giống nhau, tôi đã phải ném một chiếc khăn quàng cổ. Tôi kết nối tín hiệu GPIO_0 và KEY[1] của bo mạch:

Màn hình HDMI thứ hai tới Raspberry Pi3 thông qua giao diện DPI và bo mạch FPGA

Bây giờ tôi sẽ cho bạn biết một chút về dự án trong FPGA. Khó khăn chính trong việc hình thành tín hiệu HDMI là tần số rất cao. Nhìn vào sơ đồ chân của đầu nối HDMI, bạn có thể thấy rằng tín hiệu RGB hiện là tín hiệu vi sai nối tiếp:

Màn hình HDMI thứ hai tới Raspberry Pi3 thông qua giao diện DPI và bo mạch FPGA

Việc sử dụng tín hiệu vi sai cho phép bạn xử lý nhiễu chế độ chung trên đường truyền. Trong trường hợp này, mã 10-bit ban đầu của mỗi tín hiệu màu được chuyển đổi thành TMDS 10-bit (Tín hiệu vi sai giảm thiểu chuyển tiếp). Đây là một phương pháp mã hóa đặc biệt để loại bỏ thành phần DC ra khỏi tín hiệu và giảm thiểu chuyển đổi tín hiệu trong dòng vi sai. Vì hiện có 10 bit để truyền trên mỗi byte màu qua đường nối tiếp, nên tần số xung nhịp của bộ nối tiếp phải cao hơn 1280 lần so với tần số xung nhịp của pixel. Nếu chúng ta lấy ví dụ chế độ video 720x60 74,25Hz, thì tần số pixel của chế độ này là 742,5 MHz. Bộ nối tiếp phải là XNUMX MHz.

Thật không may, các FPGA thông thường thường không có khả năng này. Tuy nhiên, may mắn thay, FPGA có các chân DDIO tích hợp. Đây là những kết luận đã có, giống như các bộ nối tiếp 2 trên 1. Nghĩa là, chúng có thể xuất ra hai bit theo thứ tự dọc theo tần số xung nhịp tăng và giảm. Điều này có nghĩa là trong dự án FPGA, bạn không thể sử dụng 740 MHz mà là 370 MHz, nhưng bạn cần sử dụng các phần tử đầu ra DDIO trong FPGA. Ở đây 370 MHz đã là một tần số khá khả thi. Thật không may, chế độ 1280 × 720 là giới hạn. Không thể đạt được độ phân giải cao hơn trong FPGA Cyclone IV của chúng tôi được cài đặt trên bo mạch Rover2rpi.

Vì vậy, trong dự án, tần số pixel đầu vào CLK được cung cấp cho PLL, nơi nó được nhân với 5. Ở tần số này, các byte R, G, B được chuyển đổi thành các cặp bit. Đây là chức năng của bộ mã hóa TMDS. Mã nguồn trên Verilog HDL trông như thế này:

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

Sau đó, các cặp đầu ra được đưa đến đầu ra DDIO, đầu ra này tuần tự tạo ra tín hiệu một bit khi tăng và giảm.

Bản thân DDIO có thể được mô tả bằng mã Verilog như sau:

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

Nhưng nó có thể sẽ không hoạt động theo cách đó. Bạn cần sử dụng siêu chức năng ALTDDIO_OUT của Altera để thực sự sử dụng các phần tử đầu ra DDIO. Trong dự án của tôi, thành phần thư viện ALTDDIO_OUT được sử dụng.

Tất cả có thể trông hơi phức tạp, nhưng nó hoạt động.

Bạn có thể xem toàn bộ mã nguồn được viết bằng Verilog HDL ngay tại đây trên github.

Phần sụn đã biên dịch cho FPGA được nhúng vào chip EPCS được cài đặt trên bo mạch Mars Rover2rpi. Do đó, khi cấp nguồn cho bo mạch FPGA, FPGA sẽ khởi tạo từ bộ nhớ flash và khởi động.

Bây giờ chúng ta cần nói một chút về cấu hình của chính Raspberry.

Tôi đang thực hiện thử nghiệm trên Raspberry PI OS (32 bit) dựa trên Debian Buster, Phiên bản:Tháng 2020 năm XNUMX,
Ngày phát hành:2020-08-20, Phiên bản hạt nhân:5.4.

Bạn cần làm hai điều:

  • chỉnh sửa tệp config.txt;
  • tạo cấu hình máy chủ X để hoạt động với hai màn hình.

Khi chỉnh sửa tệp /boot/config.txt, bạn cần:

  1. vô hiệu hóa việc sử dụng i2c, i2s, spi;
  2. bật chế độ DPI với lớp phủ dtoverlay=dpi24;
  3. đặt chế độ video 1280×720 60Hz, 24 bit mỗi điểm trên mỗi DPI;
  4. chỉ định số lượng bộ đệm khung 2 được yêu cầu (max_framebuffers=2, chỉ khi đó thiết bị thứ hai /dev/fb1 mới xuất hiện)

Toàn văn của tệp config.txt trông như thế này.

# 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

Sau đó, bạn cần tạo một tệp cấu hình cho máy chủ X để sử dụng hai màn hình trên hai bộ đệm khung /dev/fb0 và /dev/fb1:

Tệp cấu hình của tôi là /usr/share/x11/xorg.conf.d/60-dualscreen.conf như thế này

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

Chà, nếu chưa cài đặt, thì bạn cần cài đặt Xinerama. Sau đó, không gian màn hình sẽ được mở rộng hoàn toàn cho hai màn hình, như trong video demo ở trên.

Đó có lẽ là tất cả. Giờ đây, chủ sở hữu Raspberry Pi3 sẽ có thể sử dụng hai màn hình.

Mô tả và sơ đồ của bảng Mars Rover2rpi có thể được xem ở đây.

Nguồn: www.habr.com