文章目录
前言
本节实验任务是使用开发板上的两个光纤接口实现视频图像的传输,视频图像由 OV2640 摄像头模块采集数据, 再通过开发板上的其中两路光模块进行视频信号的光纤发送和接收,然后在 HDMI 显示器上显示出来。
提示:任何文章不要过度深思!万事万物都经不起审视,因为世上没有同样的成长环境,也没有同样的认知水平,更「没有适用于所有人的解决方案」 ;不要急着评判文章列出的观点,只需代入其中,适度审视一番自己即可,能「跳脱出来从外人的角度看看现在的自己处在什么样的阶段」才不为俗人 。怎么想、怎么做,全在乎自己「不断实践中寻找适合自己的大道」
一、整体模块设计
对于上图中各个模块的功能介绍如下:
(1)camera_init 模块:摄像头初始化模块,完成对 OV2640 众多模式设置寄存器的写入操作(camera_init 摄像头初始化模块);
(2)DVP_Capture 模块:实现每两个数据拼接为一个 16 位的数据(DVP 接口时序逻辑设计);
(3)光口传输顶层模块:该模块将摄像头采集到的数据通过光口完成数据的发送和接收,该模块主要包含四部分:一个是 sfp_encoder模块主要负责将上游传递过来的 16 位数据转换成 32 位数据;一个是sfp_data_align 模 块 主 要 是 将 接 收 到 的 数 据 进 行 对 齐 ; 一 个 是sfp_decode 模块主要负责将接收到的 32 位数据转换为 16 位数据,再将解码后的数据送入下游;还有一个是 gt_aurora_exdes 模块(用于将sfp_encoder模块输入进行8b_10b编码输出给sfp_data_align 模 块,同时控制光口),主模块主要作用就是例化了 GTP IP;
(4)fifo_axi4_adapter 模块:该模块主要包含三部分:一个是写fifo 主要负责将上游外设传递过来的数据写进 DDR3 控制器,一个读fifo 主要负责人将 DDR3 控制器的内部数据读出到下游,一个是读写fifo 与 DDR3 存储器数据交互控制模块(参考fifo_mig_axi_fifo);
(5)dvi_encoder 模块:该模块就是将输出的图像数据与时序转换为符合 HDMI 接口发送时序的编码格式(基于 FPGA 的 HDMI/DVI 显示);
(6)disp_driver 模块:该模块就是将 tft 屏显示驱动控制,对缓存在 DDR3 中的图像数据进行显示(VGA成像原理与简单实现);
【于是本次设计主要是解决】
光口传输顶层模块(sfp_8b10b_top):光口传输顶层模块负责将摄像头采集的数据先转化为 32bit 数据,然后按照 8b10b 的通信编码格式将数据发送出去,然后将光口接收到的数据按照 8b10b 的通信解码格式将数据解析出来,最后将 32bit 数据转化为 16bit 数据写入到 ddr3 中。
二、光口传输顶层模块设计
(1)光口编码模块(sfp_encoder):光口编码模块负责将摄像头输入进来的 16bit 数据转化为 32bit 数据,再按照 8b10b 的编码格式将数据发送到 GTP 顶层模块。
(2)GTP 顶层模块(gt_aurora_exdes):GTP 顶层模块一方面负责与用户(FPGA)进行数据交互,另一方面还产生控制光口传输的各种时序,并实现对光口的通信操作。用户可以把该 IP 看作是 FPGA 与光口交流信息的桥梁,当然这个“桥梁”的实现代码不需要用户自己写,Xilinx 官方已帮大家写好,并封装成了一个 IP 供用户调用,用户只需要根据实际应用配置该 IP 即可。
(3)光口字对齐模块(sfp_data_align):光口字对齐模块是将接收回来不对齐的数据转化为对齐的数据,然后再输出到光口解码模块。
(4)光口解码模块(sfp_decode):光口解码模块将对齐后的数据按照 8b10b 的解码格式转化为 32bit 的像素数据,再通过 fifo 将 32bit 的数据转化为 16bit 的数据写入到 ddr3 中。
光口传输顶层模块下面有 4 个子模块,顶层模块是对子模块的例化。
module sfp_8b10b_top(
//光口接口
input Q0_CLK1_GTREFCLK_PAD_N_IN, //差分参考时钟
input Q0_CLK1_GTREFCLK_PAD_P_IN,
input [1:0] RXN_IN, //差分接收数据
input [1:0] RXP_IN,
output [1:0] TXN_OUT, //差分发送数据
output [1:0] TXP_OUT,
output [1:0] tx_disable, //tx端发送禁止使能
//用户接口
input drp_clk, //光口输入时钟
input clk_in, //输入时钟
input rst_n, //输入复位
input vs_in, //输入场信号
input data_valid_in, //输入数据有效信号
input [15:0] data_in, //输入数据
output vs_out, //输出场信号
output data_valid_out, //输出数据有效信号
output [15:0] data_out //输出数据
);
//parameter define
parameter vs_data0 = 32'h550001bc ;
parameter vs_data1 = 32'h550002bc ;
parameter data_begin0 = 32'h550003bc ;
parameter data_begin1 = 32'h550004bc ;
parameter data_end0 = 32'h550005bc ;
parameter data_end1 = 32'h550006bc ;
parameter unuse_data = 32'h550007bc ;
//wire define
wire tx0_user_clk;
wire tx0_rst_n;
wire [3:0] tx0_gt_txcharisk;
wire [31:0] tx0_gt_txdata;
wire rx1_user_clk;
wire rx1_rst_n;
wire [3:0] rx1_gt_rxcharisk;
wire [31:0] rx1_gt_rxdata;
wire [3:0] rx1_charisk_align;
wire [31:0] rx1_data_align;
sfp_encode#(
.vs_data0 (vs_data0 ),
.vs_data1 (vs_data1 ),
.data_begin0(data_begin0),
.data_begin1(data_begin1),
.data_end0 (data_end0 ),
.data_end1 (data_end1 ),
.unuse_data (unuse_data )
)
sfp_encoder(
.clk (clk_in ), //输入时钟
.rst_n (rst_n ), //输入复位
.vs_in (vs_in ), //输入场信号
.code_valid (data_valid_in ), //输入数据有效信号
.code_data (data_in ), //输入数据
.tx_code_clk (tx0_user_clk ), //光口tx端时钟
. tx_code_rst_n(tx0_rst_n ), //光口tx端复位
.tx_charrisk (tx0_gt_txcharisk), //光口tx端K码发送信号
.tx_data (tx0_gt_txdata ) //光口tx端发送数据
);
//光口控制及8b_10
gt_aurora_exdes u_gt_aurora_exdes
(
.Q0_CLK1_GTREFCLK_PAD_N_IN (Q0_CLK1_GTREFCLK_PAD_N_IN),
.Q0_CLK1_GTREFCLK_PAD_P_IN (Q0_CLK1_GTREFCLK_PAD_P_IN),
.RXN_IN (RXN_IN ),
.RXP_IN (RXP_IN ),
.TXN_OUT (TXN_OUT ),
.TXP_OUT (TXP_OUT ),
.tx_disable (tx_disable ),
.drp_clk (drp_clk ),
.rx0_user_clk (rx0_user_clk ),
.rx0_rst_n (rx0_rst_n ),
.tx0_user_clk (tx0_user_clk ),
.tx0_rst_n (tx0_rst_n ),
.rx0_gt_rxdata ( ),
.rx0_gt_rxcharisk ( ),
.tx0_gt_txdata (tx0_gt_txdata ),
.tx0_gt_txcharisk (tx0_gt_txcharisk ),
.rx1_user_clk (rx1_user_clk ),
.rx1_rst_n (rx1_rst_n ),
.tx1_user_clk (tx1_user_clk ),
.tx1_rst_n (tx1_rst_n ),
.rx1_gt_rxdata (rx1_gt_rxdata ),
.rx1_gt_rxcharisk (rx1_gt_rxcharisk ),
.tx1_gt_txdata (32'd0 ),
.tx1_gt_txcharisk (4'd0 )
);
//光口字对齐模块
data_align sfp_data_align(
.clk (rx1_user_clk ), //rx端时钟
.rst_n (rx1_rst_n ), //rx端复位
.rx_data (rx1_gt_rxdata ), //rx端K码未校正接收信号
.rx_charrisk (rx1_gt_rxcharisk ), //rx端未校正发送数据
.rx_data_align (rx1_data_align ), //rx端K码校正后接收信号
.rx_charrisk_align (rx1_charisk_align) //rx端校正后发送数据
);
ila_8 ila_data_align (
.clk(rx1_user_clk), // input wire clk
.probe0(rx1_charisk_align), // input wire [3:0] probe0
.probe1(rx1_data_align) // input wire [31:0] probe1
);
//光口解码模块
sfp_decode#(
.vs_data0 (vs_data0 ),
.vs_data1 (vs_data1 ),
.data_begin0 (data_begin0 ),
.data_begin1 (data_begin1 ),
.data_end0 (data_end0 ),
.data_end1 (data_end1 ),
.unuse_data (unuse_data )
)
sfp_decoder(
.clk (clk_in ),
.rst_n (rst_n ),
.vs_out (vs_out ),
.valid_out (data_valid_out ),
.data_out (data_out ),
.rx_code_clk (rx1_user_clk ),
.rx_code_rst_n (rx1_rst_n ),
.rx_code_charrisk (rx1_charisk_align),
.rx_code (rx1_data_align )
);
endmodule
代码//parameter define 是对编解码时所需的一些特定字符进行定义。因为不加这些特定字符,那么当接收端接收到数据时就不知道什么时候是有效数据,什么时候是无效数据,也不知道什么时候恢复场信号,所以添加这些特定字符是很有必要的。
数据流向为sfp_encode →gt_aurora_exdes(TX0 – RX1)→data_align→sfp_decode
三、 光口编码模块设计
1、光口编码模块设计
光口编码模块负责将摄像头输入进来的 16bit 数据转化为 32bit 数据,再按照 8b10b 的编码格式将数据发送到 GTP 顶层模块。
状态图的设计思想是当场信号的上升沿到来时,GTP 会发送帧同步信号。然后再判断这个 FIFO
中的数据量,如果 FIFO 内的数据还没有一行的视频数据,GTP 则发送无用的数据,当 FIFO 内已经有一行视频的数据时,GTP 会先发数据开始信号的特殊字符,然后再把这一行视频的数据通过 GTP 发送出去。一行数据发送完成时 GTP 会发送数据发送结束信号的特殊字符。最后再重新判断 FIFO 内的数据量,FIFO 数据量达到一行视频数据时,接着发送第二行视频图像。(注:状态图中使用的名称与实际代码名称可能不符,但不影响理解)
首先对光口编码(sfp_encode)模块进行讲解,由于 GTP IP 的外部接口是32 位,我们在光口编码模块中将摄像头采集的 16 位位宽的数据先经过异步 fifo转为 32 位位宽的数据,再将 fifo 输出的数据按照 8b/10b 的方式进行编码。首先,使用独热码对各个状态进行定义。
parameter tx_unuse_data = 8'b0000_0001,
tx_vs_data0 = 8'b0000_0010,
tx_vs_data1 = 8'b0000_0100,
tx_data_begin0 = 8'b0000_1000,
tx_data_begin1 = 8'b0001_0000,
tx_send_data = 8'b0010_0000,
tx_data_end0 = 8'b0100_0000,
tx_data_end1 = 8'b1000_0000;
参数化定义特殊字符,我们使用的是 K28.5,对应的十六进制为 0xbc,而且 0xbc 放在数据的最低字节,其余的高 24 位自定义即可。我们对特殊字符进行参数化定义,而且特殊字符的最低字节必须为 0xbc。
parameter vs_data0 = 32'h550001bc,
parameter vs_data1 = 32'h550002bc,
parameter data_begin0 = 32'h550003bc,
parameter data_begin1 = 32'h550004bc,
parameter data_end0 = 32'h550005bc,
parameter data_end1 = 32'h550006bc,
parameter unuse_data = 32'h550007bc
下面我们接着对光口编码(sfp_encoder)模块中发送端的状态转移进行讲解。首先产生一个数据发送的标志信号,当 fifo 中读计数大于本次实验设置的阈值 390 时(80016位 = 40032位,选取390防止读写速度不一致,造成数据读空)就将数据发送的标志信号拉高;当 fifo 为空或者场信号 vs_pose 到来时就将数据发送的标志信号拉低。
//-----------------------------------------------------
//产生一个数据发送的标志信号,当 fifo 中读计数大于本次实验设置的阈值时就将数据发送的标志信号拉高
//当 fifo 为空或者场信号 vs_pose 到来时就将数据发送的标志信号拉低。
always@(posedge tx_code_clk or negedge tx_code_rst_n)
if(!tx_code_rst_n)
data_begin<=1'b0;
else if(empty||vs_pose)
data_begin<=1'b0;
else if(rd_data_count >= fifo_threshold_value)
data_begin<=1'b1;
else
data_begin<=data_begin;
//-----------------------------------------------------
状态机在进入 tx_unuse_data 状态时,在该状态里判断 fifo 中数据的数量,如果 fifo 中数据的数量还没有达到定值 255 时 TX 端就发送无用的数据;如果fifo 中的数据到达定值 255 时,TX 端就开始发送特殊字符。当检测到场信号vs_pose 时就跳转到 tx_vs_data0 状态; tx_data_begin0 状态,tx_data_begin1状态,tx_data_end0 以及 tx_data_end1 都是 TX 端发送 k 码以及发送无用数据,只有在 tx_send_data 发送有用数据。代码中还有一个 tx_charrisk 信号,该信号位宽为 4 位,该信号的每个 bit 都依次对应着 tx_data 中的每个 byte,用 tx_charrisk信号的高电平来表示 K 码位于 tx_data 中的哪一个字节。
//-----------------------------------------------------
//产生 tx 端 K 码发送信号和发送数据
always@(posedge tx_code_clk or negedge tx_code_rst_n)
if(!tx_code_rst_n)begin
tx_charrisk <= 4'd0;
tx_data <= 32'd0;
state <= tx_unuse_data;
rd_en <=1'b0;
data_cnt<=8'd0;
end
else begin
if(vs_pose) begin
tx_charrisk <= 4'd0;
tx_data <= 32'h55000102;
state <= tx_vs_data0;
data_cnt<=8'd0;
end
else if(data_begin_flag==2'b10)begin
tx_charrisk <= 4'd0;
tx_data <= 32'h55000102;
state <= tx_data_begin0;
data_cnt <= 8'd0;
end
else begin
case(state)
tx_unuse_data :begin
rd_en<=1'b0;
data_cnt<=data_cnt+1'b1;
if(data_cnt==8'd255)begin
tx_charrisk <= 4'b0001;
tx_data <= unuse_data; //unuse_data=32'h550007bc
state <=tx_unuse_data;
end
else begin
tx_charrisk <= 4'b0000;
tx_data <= 32'h55000102;
state <=tx_unuse_data;
end
end
tx_vs_data0 :begin
tx_charrisk <= 4'b0001;
tx_data <= vs_data0; //vs_data0=32'h550001bc
state <=tx_vs_data1;
rd_en<=1'b0;
end
tx_vs_data1 :begin
tx_charrisk <= 4'b0001;
tx_data <= vs_data1; //vs_data0=32'h550002bc
state <=tx_unuse_data;
end
tx_data_begin0:begin
tx_charrisk <= 4'b0001;
tx_data <= data_begin0;//data_begin0=32'h550003bc
state <= tx_data_begin1;
rd_en<=1'b1;
end
tx_data_begin1:begin
tx_charrisk <= 4'b0001;
tx_data <= data_begin1;//data_begin0=32'h550004bc
state <= tx_send_data;
rd_en<=1'b1;
end
tx_send_data:begin
if(almost_empty==1'b0) begin
tx_charrisk <= 4'b0000;
tx_data <= dout;
state <= tx_send_data;
rd_en<=1'b1;
end
else if(almost_empty==1'b1&&empty==1'b0)begin
tx_charrisk <= 4'b0000;
tx_data <= dout;
state <= tx_send_data;
rd_en<=1'b1; //0
end
else begin
tx_charrisk <= 4'b0000;
tx_data <= dout;
state <= tx_data_end0;
rd_en<=1'b0;
end
end
tx_data_end0:begin
tx_charrisk <= 4'b0001;
tx_data <= data_end0; //data_end0=32'h550005bc
state <= tx_data_end1;
rd_en <=1'b0;
end
tx_data_end1:begin
tx_charrisk <= 4'b0001;
tx_data <= data_end1; //data_end0=32'h550006bc
state <= tx_unuse_data ;
rd_en <=1'b0;
end
default:begin
data_cnt <=data_cnt +1'b1;
if(data_cnt==8'd255)begin
tx_charrisk <= 4'b0001;
tx_data <= unuse_data;
state <=tx_unuse_data;
end
else begin
tx_charrisk <= 4'b0000;
tx_data <= 32'h55000102;
state <=tx_unuse_data;
end
end
endcase
end
end
当场信号到来时,对 fifo 进行复位,至于 fifo 的复位信号为什么要持续那么长时间,这是为了保证 fifo 完全复位成功。
//产生 fifo 复位信号
always@(posedge tx_code_clk or negedge tx_code_rst_n)
if(!tx_code_rst_n)begin
fifo_cnt <= 8'd0;
fifo_rst <= 1'b1;
end
else if(vs_pose)begin
fifo_cnt <= 8'd0;
fifo_rst <= 1'b1;
end
else if(fifo_cnt >=100