VGA时序介绍
VGA时序(下图中Sync Time正脉冲,也有负脉冲的,具体参考显示器的说明)
行时序
H Sync Time:行同步脉冲
H Back Porch:行显示后沿(行消隐)
H Active Video:行视频有效段
H Front Porch:行显示前沿(行前肩)
场时序
V Sync Time:场同步脉冲
V Back Porch:场显示后沿(场消隐)
V Active Video:场视频有效段
V Front Porch:场显示前沿(场前肩)
另一个同步脉冲为负脉冲的例子如下:
其实都是大同小异,因此一行的总输出
H Total=H Sync Time + H Back Porch + H Active Video + H Front Porch
场总输出也同理
V Total=V Sync Time + V Back Porch + V Active Video + V Front Porch
以HD 1920x1080@60 Hz举例
例子来源:Video Timings: VGA, SVGA, 720p, 1080p - Project F
行时序:
Active Pixels 1920
Front Porch 88
Sync Width 44
Back Porch 148
Blanking Total 280
Total Pixels 2200
场时序:
Active Lines 1080
Front Porch 4
Sync Width 5
Back Porch 36
Blanking Total 45
Total Lines 1125
虽然显示区域只有1920x1080,但实际上输出是2200x1125,因为Blanking区域没有数据
对应像素时钟pclk=2200x1125x60=148.5MHz.
IP核源码讲解
打开IP核所在目录的hdl文件夹
最重要的文件为dvp_raw_timing_colorbar.v,其他文件是打包封装IP的时候产生的
该模块是用于生成彩条的,顶层接口可可配置参数如下
module dvp_raw_timing_colorbar
#(
parameter BITS = 8,
parameter BAYER = 0, //0:RGGB 1:GRBG 2:GBRG 3:BGGR
parameter H_FRONT = 50,
parameter H_PULSE = 100,
parameter H_BACK = 50,
parameter H_DISP = 1280,
parameter V_FRONT = 5,
parameter V_PULSE = 10,
parameter V_BACK = 5,
parameter V_DISP = 960,
parameter H_POL = 1'b0, //行同步脉冲极性
parameter V_POL = 1'b1 //场同步脉冲极性
)
(
input xclk,
input reset_n,
output dvp_pclk,
output reg dvp_href,
output reg dvp_hsync,
output reg dvp_vsync,
output [BITS-1:0] dvp_raw
);
BITS是RAW数据位宽,BAYER是RAW数据的格式,可配置为常见的四种格式,H_POL是行同步脉冲的极性,V_POL是场同步脉冲的极性,其他参数即为上一节所讲的VGA时序参数。
接口信号中,xclk是输入的时钟,该时钟是用于生成彩条数据的像素时钟,dvp_pclk是彩条输出像素时钟,dvp_href为行数据有效信号,也就是上述描述的H Active Video,dvp_hsync为行时序信号,dvp_vsync为场时序信号,dvp_raw为输出的RAW数据。
为了生成彩条数据,需要设计行场计数器来定位输出区域,计数器如下:
reg [15:0] pix_cnt; //行计数器
always @ (posedge xclk or negedge reset_n) begin
if (!reset_n)
pix_cnt <= 0;
else if (pix_cnt < H_TOTAL - 1'b1)
pix_cnt <= pix_cnt + 1'b1;
else
pix_cnt <= 0;
end
reg [15:0] line_cnt; //场计数器
always @ (posedge xclk or negedge reset_n) begin
if (!reset_n)
line_cnt <= 0;
else if (pix_cnt == H_TOTAL - 1'b1) begin //每结束一行场+1
if (line_cnt < V_TOTAL - 1'b1)
line_cnt <= line_cnt + 1'b1;
else
line_cnt <= 0;
end
else
line_cnt <= line_cnt;
end
pix_cnt计数每一行的总输出,即H Total,line_cnt计数每一列的总输出,即V Total。
时序控制信号的生成如下:
always @(posedge xclk or negedge reset_n) begin
if (!reset_n) begin
dvp_href <= 0;
dvp_hsync <= ~H_POL;
dvp_vsync <= ~V_POL;
end
else begin
dvp_href <= pix_cnt >= H_FRONT + H_PULSE + H_BACK && line_cnt >= V_FRONT + V_PULSE + V_BACK; //行有效
dvp_hsync <= (pix_cnt >= H_FRONT && pix_cnt < H_FRONT + H_PULSE) ? H_POL : ~H_POL; //行同步脉冲
dvp_vsync <= (line_cnt >= V_FRONT && line_cnt < V_FRONT + V_PULSE) ? V_POL : ~V_POL; //场同步脉冲
end
end
这里的控制VGA时序信号生成是以FRONT开始计数的(也可以按照上述图片一样,从同步脉冲Sync Time开始,然后依次Back Porch、HREF、Front Porch,两种写法产生的时序信号周而复始后是一样的),这里把HREF放在最后比较好描述控制信号的生成,脉冲的极性可以配置。
彩条数据的生成如下所示:
reg [BITS-1:0] raw_data;
always @ (posedge xclk or negedge reset_n) begin
if (!reset_n)
raw_data <= 0;
else if (pix_cnt < H_FRONT + H_PULSE + H_BACK)
raw_data <= 0;
else if (pix_cnt < H_FRONT + H_PULSE + H_BACK + H_DISP * 1 / 8)
raw_data <= color2raw({BITS{1'b0}}, {BITS{1'b0}}, {BITS{1'b0}}, line_cnt[0], pix_cnt[0]);
else if (pix_cnt < H_FRONT + H_PULSE + H_BACK + H_DISP * 2 / 8)
raw_data <= color2raw({BITS{1'b0}}, {BITS{1'b0}}, {BITS{1'b1}}, line_cnt[0], pix_cnt[0]);
else if (pix_cnt < H_FRONT + H_PULSE + H_BACK + H_DISP * 3 / 8)
raw_data <= color2raw({BITS{1'b0}}, {BITS{1'b1}}, {BITS{1'b0}}, line_cnt[0], pix_cnt[0]);
else if (pix_cnt < H_FRONT + H_PULSE + H_BACK + H_DISP * 4 / 8)
raw_data <= color2raw({BITS{1'b0}}, {BITS{1'b1}}, {BITS{1'b1}}, line_cnt[0], pix_cnt[0]);
else if (pix_cnt < H_FRONT + H_PULSE + H_BACK + H_DISP * 5 / 8)
raw_data <= color2raw({BITS{1'b1}}, {BITS{1'b0}}, {BITS{1'b0}}, line_cnt[0], pix_cnt[0]);
else if (pix_cnt < H_FRONT + H_PULSE + H_BACK + H_DISP * 6 / 8)
raw_data <= color2raw({BITS{1'b1}}, {BITS{1'b0}}, {BITS{1'b1}}, line_cnt[0], pix_cnt[0]);
else if (pix_cnt < H_FRONT + H_PULSE + H_BACK + H_DISP * 7 / 8)
raw_data <= color2raw({BITS{1'b1}}, {BITS{1'b1}}, {BITS{1'b0}}, line_cnt[0], pix_cnt[0]);
else if (pix_cnt < H_FRONT + H_PULSE + H_BACK + H_DISP * 8 / 8)
raw_data <= color2raw({BITS{1'b1}}, {BITS{1'b1}}, {BITS{1'b1}}, line_cnt[0], pix_cnt[0]);
else
raw_data <= 0;
end
function [BITS-1:0] color2raw;
input [BITS-1:0] r, g, b;
input odd_line, odd_pix;
begin
case ({BAYER[1:0],odd_line,odd_pix})
{2'd0,2'b00}: color2raw = r;
{2'd0,2'b01}: color2raw = g;
{2'd0,2'b10}: color2raw = g;
{2'd0,2'b11}: color2raw = b;
{2'd1,2'b00}: color2raw = g;
{2'd1,2'b01}: color2raw = r;
{2'd1,2'b10}: color2raw = b;
{2'd1,2'b11}: color2raw = g;
{2'd2,2'b00}: color2raw = g;
{2'd2,2'b01}: color2raw = b;
{2'd2,2'b10}: color2raw = r;
{2'd2,2'b11}: color2raw = g;
{2'd3,2'b00}: color2raw = b;
{2'd3,2'b01}: color2raw = g;
{2'd3,2'b10}: color2raw = g;
{2'd3,2'b11}: color2raw = r;
default: color2raw = 0;
endcase
end
endfunction
代码中,彩条数据只在H HREF的有效期间才生成,这里把实际显示的区域分为8个区域,每个区域配置一种颜色。由于需要输出的是RAW数据,因此通过了一个color2raw的函数模块抽取RGB颜色的RAW数据,RAW数据的抽取通过配置图像BAYER格式和所在行列奇偶性来确定。
以RGGB为例(BAYER被配置为了0),偶数行且偶数(注意这里是以0开始标号)列返回R像素值,偶数行且奇数列(奇数行且偶数列)返回G像素值,奇数行且奇数列返回B像素值。其他BAYER格式也同样如此。
Verilog代码仿真
重新创建一个vivado工程用来仿真该模块(也可以使用Modelsim),如下:
编写仿真文件,这里用到一个dvp_to_file用于将生成的RAW数据保存到文件
tb_top的代码如下:
module tb_top;
localparam OUT_FILE = "E:/ISP/FPGA/ISP_JAY/IP_tb/xil_camif_tb/raw_1280_960_RGGB1.raw";
parameter BAYER = 0;
parameter BITS = 10 ;
parameter PERIOD = 10 ;
reg xclk = 0;
reg rst_n = 0;
wire pclk_o;
wire href_o;
wire hsync_o;
wire vsync_o;
wire [BITS-1:0] rawdata_o;
initial
begin
forever #(PERIOD/2) xclk=~xclk;
end
initial
begin
#(PERIOD*5) rst_n = 1;
end
dvp_raw_timing_colorbar #(
.BITS(BITS),
.BAYER(BAYER)
)
u_raw_colorbar(
.xclk(xclk),
.reset_n(rst_n),
.dvp_pclk(pclk_o),
.dvp_href(href_o),
.dvp_hsync(hsync_o),
.dvp_vsync(vsync_o),
.dvp_raw(rawdata_o)
);
dvp_to_file #(
.FILE(OUT_FILE),
.BITS(BITS)
)
u_FILE_to_DVP(
.pclk(pclk_o),
.rst_n(rst_n),
.href(href_o),
.vsync(vsync_o),
.data(rawdata_o)
);
endmodule
dvp_to_file内容如下:
这里保存RAW时候,不管RAW是0~8位宽还是9~16位宽都统一使用两字节/像素保存
module dvp_to_file
#(
parameter FILE = "out.raw",
parameter BITS = 8
)
(
input pclk,
input rst_n,
input href,
input vsync,
input [BITS-1:0] data
);
integer BYTE;
initial begin
if(BITS > 0 && BITS <= 8)
BYTE = 1;
else if(BITS > 8 && BITS <= 16)
BYTE = 2;
else
BYTE = 3;
end
reg vsync_delay;
always@(posedge pclk or negedge rst_n)begin
if(!rst_n)begin
vsync_delay <= 0;
end
else begin
vsync_delay <= vsync;
end
end
//avoid first vsync from finish
reg enable;
always@(posedge pclk or negedge rst_n)begin
if(!rst_n)begin
enable <= 0;
end
else begin
if(href)begin
enable <= 1;
end
else begin
enable <= enable;
end
end
end
integer fd;
always @(posedge pclk or negedge rst_n) begin
if(!rst_n) begin
fd <= $fopen(FILE, "wb");
end
else begin
if(href) begin
if(BYTE == 1)begin
$fwrite(fd, "%c%c", data[BITS-1:0],{8'b0}); //单字节也按uint16保存,注意字节序
end
else if(BYTE == 2)begin
$fwrite(fd, "%c%c", data[7:0],data[BITS-1:8]); //先写低字节
end
end
if(!vsync_delay && vsync & enable) begin
$fclose(fd);
$finish;
end
end
end
endmodule
在Vivado中跑行为仿真,并打开输出的二进制RAW图像如下所示:
仅添加Demosaic算法插值到RGB后如下:
彩条显示正常
接下来修改源码,使其可以生成24色卡,此种格式还可以用于判断视频是否会垂直滚动,而彩条只能判断视频是否水平滚动。
修改该模块生成彩条处的代码如下:
//色块的行列索引
wire [2:0] block_x;
wire [1:0] block_y;
assign block_x = (pix_cnt - H_FRONT - H_PULSE - H_BACK) * 6 / H_DISP; // 水平块索引
assign block_y = (line_cnt - V_FRONT - V_PULSE - V_BACK) * 4 / V_DISP; // 垂直块索引
always @(posedge xclk or negedge reset_n) begin
if (!reset_n)
raw_data <= 0;
else if (pix_cnt < H_FRONT + H_PULSE + H_BACK || line_cnt < V_FRONT + V_PULSE + V_BACK)
raw_data <= 0; // 行、列前沿和背沿输出0
else begin
// 根据块位置映射到颜色
case ({block_y, block_x}) // 组合索引 4x6
{2'd0,3'd0}: raw_data <= color2raw({8'd115,{(BITS-8){1'b0}}}, {8'd82,{(BITS-8){1'b0}}}, {8'd68,{(BITS-8){1'b0}}}, line_cnt[0], pix_cnt[0]); //dark skin
{2'd0,3'd1}: raw_data <= color2raw({8'd194,{(BITS-8){1'b0}}}, {8'd150,{(BITS-8){1'b0}}}, {8'd130,{(BITS-8){1'b0}}}, line_cnt[0], pix_cnt[0]); //light skin
{2'd0,3'd2}: raw_data <= color2raw({8'd98,{(BITS-8){1'b0}}}, {8'd122,{(BITS-8){1'b0}}}, {8'd157,{(BITS-8){1'b0}}}, line_cnt[0], pix_cnt[0]); //blue sky
{2'd0,3'd3}: raw_data <= color2raw({8'd87,{(BITS-8){1'b0}}}, {8'd108,{(BITS-8){1'b0}}}, {8'd67,{(BITS-8){1'b0}}}, line_cnt[0], pix_cnt[0]); //foliage
{2'd0,3'd4}: raw_data <= color2raw({8'd133,{(BITS-8){1'b0}}}, {8'd128,{(BITS-8){1'b0}}}, {8'd177,{(BITS-8){1'b0}}}, line_cnt[0], pix_cnt[0]); //blue flower
{2'd0,3'd5}: raw_data <= color2raw({8'd103,{(BITS-8){1'b0}}}, {8'd189,{(BITS-8){1'b0}}}, {8'd170,{(BITS-8){1'b0}}}, line_cnt[0], pix_cnt[0]); //bluish green
{2'd1,3'd0}: raw_data <= color2raw({8'd214,{(BITS-8){1'b0}}}, {8'd126,{(BITS-8){1'b0}}}, {8'd44,{(BITS-8){1'b0}}}, line_cnt[0], pix_cnt[0]); //Orange
{2'd1,3'd1}: raw_data <= color2raw({8'd80,{(BITS-8){1'b0}}}, {8'd91,{(BITS-8){1'b0}}}, {8'd166,{(BITS-8){1'b0}}}, line_cnt[0], pix_cnt[0]); //Purplish blue
{2'd1,3'd2}: raw_data <= color2raw({8'd193,{(BITS-8){1'b0}}}, {8'd90,{(BITS-8){1'b0}}}, {8'd99,{(BITS-8){1'b0}}}, line_cnt[0], pix_cnt[0]); //Moderate red
{2'd1,3'd3}: raw_data <= color2raw({8'd94,{(BITS-8){1'b0}}}, {8'd60,{(BITS-8){1'b0}}}, {8'd108,{(BITS-8){1'b0}}}, line_cnt[0], pix_cnt[0]); //Purple
{2'd1,3'd4}: raw_data <= color2raw({8'd157,{(BITS-8){1'b0}}}, {8'd188,{(BITS-8){1'b0}}}, {8'd64,{(BITS-8){1'b0}}}, line_cnt[0], pix_cnt[0]); //Yellow green
{2'd1,3'd5}: raw_data <= color2raw({8'd224,{(BITS-8){1'b0}}}, {8'd163,{(BITS-8){1'b0}}}, {8'd46,{(BITS-8){1'b0}}}, line_cnt[0], pix_cnt[0]); //Orange yellow
{2'd2,3'd0}: raw_data <= color2raw({8'd56,{(BITS-8){1'b0}}}, {8'd61,{(BITS-8){1'b0}}}, {8'd150,{(BITS-8){1'b0}}}, line_cnt[0], pix_cnt[0]); //Blue
{2'd2,3'd1}: raw_data <= color2raw({8'd70,{(BITS-8){1'b0}}}, {8'd148,{(BITS-8){1'b0}}}, {8'd73,{(BITS-8){1'b0}}}, line_cnt[0], pix_cnt[0]); //Green
{2'd2,3'd2}: raw_data <= color2raw({8'd175,{(BITS-8){1'b0}}}, {8'd54,{(BITS-8){1'b0}}}, {8'd60,{(BITS-8){1'b0}}}, line_cnt[0], pix_cnt[0]); //Red
{2'd2,3'd3}: raw_data <= color2raw({8'd231,{(BITS-8){1'b0}}}, {8'd199,{(BITS-8){1'b0}}}, {8'd31,{(BITS-8){1'b0}}}, line_cnt[0], pix_cnt[0]); //Yellow
{2'd2,3'd4}: raw_data <= color2raw({8'd187,{(BITS-8){1'b0}}}, {8'd86,{(BITS-8){1'b0}}}, {8'd149,{(BITS-8){1'b0}}}, line_cnt[0], pix_cnt[0]); //Magenta
{2'd2,3'd5}: raw_data <= color2raw({8'd8,{(BITS-8){1'b0}}}, {8'd133,{(BITS-8){1'b0}}}, {8'd161,{(BITS-8){1'b0}}}, line_cnt[0], pix_cnt[0]); //Cyan
{2'd3,3'd0}: raw_data <= color2raw({8'd243,{(BITS-8){1'b0}}}, {8'd243,{(BITS-8){1'b0}}}, {8'd243,{(BITS-8){1'b0}}}, line_cnt[0], pix_cnt[0]); //White(.05*)
{2'd3,3'd1}: raw_data <= color2raw({8'd200,{(BITS-8){1'b0}}}, {8'd200,{(BITS-8){1'b0}}}, {8'd200,{(BITS-8){1'b0}}}, line_cnt[0], pix_cnt[0]); //Neutral 8(.23*)
{2'd3,3'd2}: raw_data <= color2raw({8'd160,{(BITS-8){1'b0}}}, {8'd160,{(BITS-8){1'b0}}}, {8'd160,{(BITS-8){1'b0}}}, line_cnt[0], pix_cnt[0]); //Neutral 6.5(.44*)
{2'd3,3'd3}: raw_data <= color2raw({8'd122,{(BITS-8){1'b0}}}, {8'd122,{(BITS-8){1'b0}}}, {8'd122,{(BITS-8){1'b0}}}, line_cnt[0], pix_cnt[0]); //Neutral 5(.70*)
{2'd3,3'd4}: raw_data <= color2raw({8'd85,{(BITS-8){1'b0}}}, {8'd85,{(BITS-8){1'b0}}}, {8'd85,{(BITS-8){1'b0}}}, line_cnt[0], pix_cnt[0]); //Neutral 3.5(1.05*)
{2'd3,3'd5}: raw_data <= color2raw({8'd52,{(BITS-8){1'b0}}}, {8'd52,{(BITS-8){1'b0}}}, {8'd52,{(BITS-8){1'b0}}}, line_cnt[0], pix_cnt[0]); //Black
default: raw_data <= 0; // 无效区域
endcase
end
end
24色卡值参考:【转】color checker rgb value-优快云博客
这里假设RAW的位宽至少为8bit,按照4x6个块显示色块。
让后重新仿真得到二进制RAW图后打开如下:
可以正常显示色块,说明功能实现没有问题
IP核打包封装
创建新IP
选择带有AXI总线的IP封装
设置IP的基本信息和保存位置
添加AXI-Lite类型接口
设置AXI总线的数目和类型(Lite、stream、Full),这里需要对IP核内部的寄存器进行读写操作,涉及的数据量较小,选择Lite类型即可,然后配置预留的寄存器个数,这里可以随便设置,因为后续会修改对应的文件,这里设置只是为了生成一个模板
然后选择Edit IP
将会打开如下界面
添加支持的器件
添加IP支持的FPGA器件族
配置参数
添加IP配置界面的参数及其默认值(如果先添加了.v设计文件,里面的parameter参数会自动显示在这里,如果没有就手动添加)
在源文件中添加参数和接口
打开顶层文件xil_camif_v1_1,添加参数和上输入输出接口信息(应该先添加这些参数和接口,再配置上一步的parameter)
实例化的地方也添加相关内容
同理,顶层文件实例化用到的xil_camif_v1_1_S01_AXI文件中也要在相关地方添加parameter配置参数和输入输出接口信息(这里未例举出来)。
Merge Changes以后会出现这些接口信息
封装接口
接下来对输入输出接口进行封装
irq封装:选择interrupt_rtl类型,设置的名称与自定义信号相同时会自动映射
camera信号组的封装:
选择vid_io_rtl类型的slave接口(收数据)映射到摄像头输入信号簇
out_raw的信号组封装:
将vid_io_rtl类型的master接口(发数据)映射到时序输出信号簇
时钟信号的封装:
注意上输入时钟信号设置为slave,输出时钟信号设置为master
另外该开源IP核还增添了out_raw的多路相同输出,可以通过配置参数中的RAW_AUX_NUM来控制,因此还需要添加这几个接口
out_raw_aux1:
这里使用了表达式$RAW_AUX_NUM,这是一个GUI界面可配置的参数,若该参数符合表达式则显示该接口,否则隐藏该接口。
其实输出的信号与out_raw是一样的
完整的封装好IP的接口信号后如下:
封装好设计界面如下:
但是,此时相当于只是设计好了IP的GUI以及可通过AXI-Lite访问内部寄存器的样子,还没有真正实现器功能,还需要把之前几节修改的dvp_raw_timing_colorbar核心文件添加到IP中,并实例化该模块,然后进一步进行封装。
添加设计文件并实例化相关模块
添加设计文件到相应的IP目录中
然后在xil_camif_v1_1_S01_AXI文件中实例化该模块
除此之外,开源IP核中还有一个xdc约束文件也需要添加进来,内容很简单:
#125MHz
create_clock -period 8 -name cam_xclk [get_ports cam_xclk]
create_clock -period 8 -name cam_pclk [get_ports cam_pclk]
对两个输入的时钟信号进行了时序约束,使该IP综合时至少能连接125MHz的输入时钟。
之前我们在IP核设计的时候为该IP核预留了8个AXI-Lite可访问的寄存器(Vivado会自动生成对于这8个寄存器的读写,相当于生成一个模板,按照生成的模板修改即可),接下来需要设计这些寄存器的控制功能。
接下来的修改都是在xil_cam_v1_1_S01_AXI.v文件上进行修改
首先在例化彩条生成模块处添加如下一些代码
1、根据是否使能了colorbar_en来选择时钟,gen_clk生成彩条所使用的时钟,cam_pclk是从摄像头出来的时钟(对齐了视频数据),colorbar_en是一个IP内部寄存器,可以通过AXI-Lite配置(稍后讲到),不管选择输出彩条还是摄像头的视频数据都需要将其对应的控制时序对齐输出。
assign out_pclk = module_reset ? 1'b0 : (colorbar_en ? gen_pclk : cam_pclk); //colorbar使能的时候以gen_pclk输出,否则实时视频以cam_pclk对齐输出
always @ (posedge out_pclk or posedge module_reset) begin
if (module_reset) begin
out_href <= 0;
out_vsync <= 0;
out_raw <= 0;
end
else if (colorbar_en) begin //输出彩条数据和对应的时序
out_href <= gen_href;
out_vsync <= gen_vsync;
out_raw <= gen_db;
end
else begin
out_href <= cam_href; //输出实时视频数据及对应的时序
out_vsync <= cam_vsync;
out_raw <= cam_data;
end
end
2、计数有效显示区域,也就是文章开头所述的Display Area(Boarder=0),~prev_href & out_href表达式是经典的是上升沿信号检测,~out_vsync & prev_vsync是下降沿检测。href的上升沿表示一行的开始,因此列计数器重置,href为高时就将计数器加一;vsync下降沿时新的一帧开始(场同步脉冲为正脉冲),因此行计数器重置,每当一行结束(href下降沿)的时候行计数器加一。(这里要注意控制时序中的极性)
reg prev_href, prev_vsync;
always @ (posedge out_pclk) begin
prev_href <= out_href;
prev_vsync <= out_vsync;
end
reg [15:0] pix_cnt;
always @ (posedge out_pclk or posedge module_reset) begin
if (module_reset)
pix_cnt <= 0;
else if (~prev_href & out_href) //href 信号上升沿时重置计数器,这里只计数有效的列像素
pix_cnt <= 1'b1;
else if (out_href)
pix_cnt <= pix_cnt + 1'b1;
else
pix_cnt <= pix_cnt;
end
reg [15:0] line_cnt;
always @ (posedge out_pclk or posedge module_reset) begin
if (module_reset)
line_cnt <= 0;
else if (~out_vsync & prev_vsync) //vsync 下降沿时重置计数器
line_cnt <= 0;
else if (~out_href & prev_href) //href 信号下降沿时计数器加1
line_cnt <= line_cnt + 1'b1; //这里也是计数有效数据的行
else
line_cnt <= line_cnt;
end
3、视频尺寸和帧计数,vsync上升沿代表一帧结束,由于此时行计数器和列计数器还未重置,因此此时得到的行列计数就是视频的尺寸信息,dvp_width和dvp_height以及dvp_frame_cnt是IP核内部的寄存器,可以通过AXI-Lite访问。
always @ (posedge out_pclk or posedge module_reset) begin
if (module_reset) begin
dvp_width <= 0;
dvp_height <= 0;
dvp_frame_cnt <= 0;
end
else if (~prev_vsync & out_vsync) begin //vsync 上升沿时,一帧的数据结束,可以得到数据显示区域的长宽
dvp_width <= pix_cnt;
dvp_height <= line_cnt;
dvp_frame_cnt <= dvp_frame_cnt + 1'b1;
end
else begin
dvp_width <= dvp_width;
dvp_height <= dvp_height;
dvp_frame_cnt <= dvp_frame_cnt;
end
end
4、接下来修改对于IP核内部寄存器的读写,开始配置的时候Vivado给我们自动生成了8个寄存器,每个都是32位的(实际中IP核源码中只只用了7个可AXI-Lite读写的寄存器),对于其读写如下:
//-- Number of Slave Registers 8
reg [C_S_AXI_DATA_WIDTH-1:0] slv_reg0;
reg [C_S_AXI_DATA_WIDTH-1:0] slv_reg1;
reg [C_S_AXI_DATA_WIDTH-1:0] slv_reg2;
reg [C_S_AXI_DATA_WIDTH-1:0] slv_reg3;
reg [C_S_AXI_DATA_WIDTH-1:0] slv_reg4;
reg [C_S_AXI_DATA_WIDTH-1:0] slv_reg5;
reg [C_S_AXI_DATA_WIDTH-1:0] slv_reg6;
reg [C_S_AXI_DATA_WIDTH-1:0] slv_reg7;
always @( posedge S_AXI_ACLK )
begin
if ( S_AXI_ARESETN == 1'b0 )
begin
slv_reg0 <= 0;
slv_reg1 <= 0;
slv_reg2 <= 0;
slv_reg3 <= 0;
slv_reg4 <= 0;
slv_reg5 <= 0;
slv_reg6 <= 0;
slv_reg7 <= 0;
end
else begin
if (slv_reg_wren)
begin
case ( axi_awaddr[ADDR_LSB+OPT_MEM_ADDR_BITS:ADDR_LSB] )
3'h0:
for ( byte_index = 0; byte_index <= (C_S_AXI_DATA_WIDTH/8)-1; byte_index = byte_index+1 )
if ( S_AXI_WSTRB[byte_index] == 1 ) begin
// Respective byte enables are asserted as per write strobes
// Slave register 0
slv_reg0[(byte_index*8) +: 8] <= S_AXI_WDATA[(byte_index*8) +: 8];
end
3'h1:
for ( byte_index = 0; byte_index <= (C_S_AXI_DATA_WIDTH/8)-1; byte_index = byte_index+1 )
if ( S_AXI_WSTRB[byte_index] == 1 ) begin
// Respective byte enables are asserted as per write strobes
// Slave register 1
slv_reg1[(byte_index*8) +: 8] <= S_AXI_WDATA[(byte_index*8) +: 8];
end
3'h2:
for ( byte_index = 0; byte_index <= (C_S_AXI_DATA_WIDTH/8)-1; byte_index = byte_index+1 )
if ( S_AXI_WSTRB[byte_index] == 1 ) begin
// Respective byte enables are asserted as per write strobes
// Slave register 2
slv_reg2[(byte_index*8) +: 8] <= S_AXI_WDATA[(byte_index*8) +: 8];
end
3'h3:
for ( byte_index = 0; byte_index <= (C_S_AXI_DATA_WIDTH/8)-1; byte_index = byte_index+1 )
if ( S_AXI_WSTRB[byte_index] == 1 ) begin
// Respective byte enables are asserted as per write strobes
// Slave register 3
slv_reg3[(byte_index*8) +: 8] <= S_AXI_WDATA[(byte_index*8) +: 8];
end
3'h4:
for ( byte_index = 0; byte_index <= (C_S_AXI_DATA_WIDTH/8)-1; byte_index = byte_index+1 )
if ( S_AXI_WSTRB[byte_index] == 1 ) begin
// Respective byte enables are asserted as per write strobes
// Slave register 4
slv_reg4[(byte_index*8) +: 8] <= S_AXI_WDATA[(byte_index*8) +: 8];
end
3'h5:
for ( byte_index = 0; byte_index <= (C_S_AXI_DATA_WIDTH/8)-1; byte_index = byte_index+1 )
if ( S_AXI_WSTRB[byte_index] == 1 ) begin
// Respective byte enables are asserted as per write strobes
// Slave register 5
slv_reg5[(byte_index*8) +: 8] <= S_AXI_WDATA[(byte_index*8) +: 8];
end
3'h6:
for ( byte_index = 0; byte_index <= (C_S_AXI_DATA_WIDTH/8)-1; byte_index = byte_index+1 )
if ( S_AXI_WSTRB[byte_index] == 1 ) begin
// Respective byte enables are asserted as per write strobes
// Slave register 6
slv_reg6[(byte_index*8) +: 8] <= S_AXI_WDATA[(byte_index*8) +: 8];
end
3'h7:
for ( byte_index = 0; byte_index <= (C_S_AXI_DATA_WIDTH/8)-1; byte_index = byte_index+1 )
if ( S_AXI_WSTRB[byte_index] == 1 ) begin
// Respective byte enables are asserted as per write strobes
// Slave register 7
slv_reg7[(byte_index*8) +: 8] <= S_AXI_WDATA[(byte_index*8) +: 8];
end
default : begin
slv_reg0 <= slv_reg0;
slv_reg1 <= slv_reg1;
slv_reg2 <= slv_reg2;
slv_reg3 <= slv_reg3;
slv_reg4 <= slv_reg4;
slv_reg5 <= slv_reg5;
slv_reg6 <= slv_reg6;
slv_reg7 <= slv_reg7;
end
endcase
end
end
end
always @(*)
begin
// Address decoding for reading registers
case ( axi_araddr[ADDR_LSB+OPT_MEM_ADDR_BITS:ADDR_LSB] )
3'h0 : reg_data_out <= slv_reg0;
3'h1 : reg_data_out <= slv_reg1;
3'h2 : reg_data_out <= slv_reg2;
3'h3 : reg_data_out <= slv_reg3;
3'h4 : reg_data_out <= slv_reg4;
3'h5 : reg_data_out <= slv_reg5;
3'h6 : reg_data_out <= slv_reg6;
3'h7 : reg_data_out <= slv_reg7;
default : reg_data_out <= 0;
endcase
end
我们将其修改为如下:
详细解释已经写在注释中:)
//总共有7个寄存器,定义每个寄存器的偏移,每个寄存器的位宽为C_S_AXI_DATA_WIDTH (32 bit)
localparam REG_RESET = 0; //复位寄存器,对应module_reset
localparam REG_WIDTH = 1; //视频宽度寄存器,对应dvp_width
localparam REG_HEIGHT = 2; //视频高度寄存器,对应dvp_height
localparam REG_FRAME_CNT = 3; //帧计数寄存器,对应dvp_frame_cnt
localparam REG_COLORBAR_EN = 4; //颜色条使能寄存器,对应colorbar_en
localparam REG_INT_STATUS = 5; //中断状态寄存器,对应{int_frame_done,int_frame_start}
localparam REG_INT_MASK = 6; //中断屏蔽寄存器,对应{int_mask_frame_done,int_mask_frame_start}
//以下就是7个寄存器的定义,但是AXI-Lite的读写接口在zynq7020只支持32位,因此后续读写需要稍微处理一下
reg module_reset;
reg [15:0] dvp_width;
reg [15:0] dvp_height;
reg [31:0] dvp_frame_cnt;
reg colorbar_en;
reg int_frame_start, int_frame_done;
reg int_mask_frame_start, int_mask_frame_done;
// 一帧开始或一帧结束都会生成中断信号(前提没有被屏蔽,这里应该是向MASK中写1屏蔽)
//由于帧开始和帧结束共用一个中断,因此还需要通过中断状态寄存器判断是帧开始中断还是帧结束中断
assign irq = int_frame_start&(~int_mask_frame_start) | int_frame_done&(~int_mask_frame_done);
///对于IP核内部寄存器的写操作
always @( posedge S_AXI_ACLK )
begin
if ( S_AXI_ARESETN == 1'b0 ) //axi-lite复位
begin
module_reset <= 1;
colorbar_en <= 0;
int_frame_start <= 0;
int_frame_done <= 0;
int_mask_frame_start <= 1;
int_mask_frame_done <= 1;
end
else begin
//需要axi-lite写
if (slv_reg_wren)
begin
//在zynq7020中,表达式变为axi_awaddr[4:2]
//因为是以32位宽访问寄存器的,因此地址偏移是4字节对齐的,通过这样访问刚好4字节对齐
//IP核内部只有7个寄存器,2^3=8,因此使用了[4:2],而且地址C_S_AXI_ADDR_WIDTH默认配置的也是5
case ( axi_awaddr[ADDR_LSB+OPT_MEM_ADDR_BITS:ADDR_LSB] )
REG_RESET: module_reset <= S_AXI_WDATA[0]; //将写入该地址的数据最低位写到module_reset寄存器中
REG_WIDTH: ; //该寄存器是仅读的
REG_HEIGHT: ; //该寄存器是仅读的
REG_FRAME_CNT: ; ///该寄存器是仅读的
REG_COLORBAR_EN: colorbar_en <= S_AXI_WDATA[0]; //将写入该地址的数据最低位写到colorbar_en寄存器中
REG_INT_STATUS: {int_frame_done,int_frame_start} <= 2'd0; //写清0(写0写1无所谓,有写操作即可)
REG_INT_MASK: {int_mask_frame_done,int_mask_frame_start} <= S_AXI_WDATA[1:0]; //写入的最低两位表示是否屏蔽对应的中断
default:;
endcase
end
//自动写寄存器
if (frame_start) int_frame_start <= 1'b1;
if (frame_done) int_frame_done <= 1'b1;
end
end
always @(*)
begin
// Address decoding for reading registers
case ( axi_araddr[ADDR_LSB+OPT_MEM_ADDR_BITS:ADDR_LSB] )
REG_RESET: reg_data_out <= {31'd0, module_reset};
REG_WIDTH: reg_data_out <= {16'd0, dvp_width};
REG_HEIGHT: reg_data_out <= {16'd0, dvp_height};
REG_FRAME_CNT: reg_data_out <= dvp_frame_cnt;
REG_COLORBAR_EN: reg_data_out <= {31'd0, colorbar_en};
REG_INT_STATUS: reg_data_out <= {30'd0, int_frame_done, int_frame_start};
REG_INT_MASK: reg_data_out <= {30'd0, int_mask_frame_done, int_mask_frame_start};
default: reg_data_out <= 0;
endcase
end
修改好以后Re-Package IP
即可在一开始选择的文件夹路径下生成IP核的相关文件。
修改驱动文件
除此之外,若需要在vitis中方便的配置IP核寄存器,还要修改drivers文件夹中关于该IP的驱动文件,由于之前配置了自动生成8个寄存器,在xil_camif.h中会自动生成如下寄存器地址偏移
将其修改为上几节中verilog中定义的一致
#define CAMIF_REG_RESET (0 * 4)
#define CAMIF_REG_WIDTH (1 * 4)
#define CAMIF_REG_HEIGHT (2 * 4)
#define CAMIF_REG_FRAME_CNT (3 * 4)
#define CAMIF_REG_COLORBAR_EN (4 * 4)
#define CAMIF_REG_INT_STATUS (5 * 4)
#define CAMIF_REG_INT_MASK (6 * 4)
#define CAMIF_REG_INT_STATUS_BIT_FRAME_START (1<<0)
#define CAMIF_REG_INT_STATUS_BIT_FRAME_DONE (1<<1)
#define CAMIF_REG_INT_MASK_BIT_FRAME_START (1<<0)
#define CAMIF_REG_INT_MASK_BIT_FRAME_DONE (1<<1)
解决自定义IP Make Error问题
到此IP核的修改封装到此结束,以下记录笔者在使用自定义IP时候遇到的问题
(大概自定义IP核都会遇到的问题,不知是不是BUG)
问题参考:【已解决】Vitis加入自定义ip核,出现Makefile error问题 | 鲍温霞的个人站
在vitis中编译平台的时后会一直出现make error的问题,网路上的解决方法是修改vitis几处文件的makefile,但是要从根源上解决参考以下办法
之前在封装好IP后会在IP核的目录下生成驱动文件
将IP核目录下的Makefile文件内容改为如下:
COMPILER=
ARCHIVER=
CP=cp
COMPILER_FLAGS=
EXTRA_COMPILER_FLAGS=
LIB=libxil.a
RELEASEDIR=../../../lib
INCLUDEDIR=../../../include
INCLUDES=-I./. -I${INCLUDEDIR}
INCLUDEFILES=$(wildcard *.h)
LIBSOURCES=$(wildcard *.c *.cpp)
OUTS =*.o
OBJECTS = $(addsuffix .o, $(basename $(wildcard *.c *.cpp)))
ASSEMBLY_OBJECTS = $(addsuffix .o, $(basename $(wildcard *.S)))
libs:
echo "Compiling xil_camif..."
$(COMPILER) $(COMPILER_FLAGS) $(EXTRA_COMPILER_FLAGS) $(INCLUDES) $(LIBSOURCES)
$(ARCHIVER) -r ${RELEASEDIR}/${LIB} ${OBJECTS} ${ASSEMBLY_OBJECTS}
make clean
include:
${CP} $(INCLUDEFILES) $(INCLUDEDIR)
clean:
rm -rf ${OBJECTS} ${ASSEMBLY_OBJECTS}
这样在用到该IP核时,综合生成导出的.xsa文件就会记录这些信息,生成平台工程和BSP时就会导入这些信息,里面的Makefile就不会出错。
然后在vivado中更新IP并重新生成bitstream,最后导出.xsa文件,在Vitis中右键平台工程并选择Update Hardware Specification,选择新导出的.xsa文件,然后重新编译就不会出现make error的问题了。
其实本文讲解的是从零封装打包IP核,作为讲解来记录一个自定义IP核的创建过程,在实际开发中,如果开源的IP核提供了源码,可以直接对导入的IP进行修改(不必从零开始打包封装)
IP的修改也可以直接在IP核的目录下对文件直接进行修改:
上板测试
在Vivado中替换新打包创建的IP核后,重新综合——实现——生成比特流,然后导出.xsa硬件相关文件,如果该ip的名称在BlockDesign中与之前原工程的一致,Vitis也无需修改,编译烧录后实现的效果如下所示,上下边界的方块被xil_vip裁剪了一部分。