FPGA实现VGA接口——保存图片至ROM/RAM显示(以及一些坑的梳理)
实验描述:
使用altera芯片FPGA实现VGA接口图像显示,将图片保存在ROM/RAM中,实现静态图片的显示。
这里主要记录自己在调试过程中遇到的一些坑,希望可以让看到的人少走一点弯路,当然,也会附上我的全部代码,供大家参考。
本项目参考了其他博主的文章,再次感谢他们的帮助。
http://dengkanwen.com/70.html
http://t.csdn.cn/7ykBT
过程描述
首先,附上全部各个模块的代码
//VGA彩条显示
//端口定义
module vga_colorbar_top(
input wire sys_clk ,
input wire sys_rst,
output wire vga_hs, // 输出到vga接口的行同步信号
output wire vga_vs, // 输出到vga接口的场同步信号
output wire [15:0] vga_rgb // 输出到vga接口的像素数据
);
//内部信号定义
wire clk_w ;
wire locked_w;
wire [15:0] pixel_data_w;
wire [9:0] pixel_hpos_w;
wire [9:0] pixel_vpos_w;
//wire sys_rst_n;
//assign sys_rst_n = sys_rst && locked_w;
//例化pll
PLL_48MHz_to_25MHz pll_25MHz(
//.areset (sys_rst),//对复位信号取反
.inclk0 (sys_clk),
.c0 (clk_w)
//.locked (locked_w)
);
//vga驱动模块
vga_driver u_vga_driver(
//pll与系统时钟复位输入信号
.clk_25MHz (clk_w),
.rst (sys_rst),
//驱动模块输出信号
.vga_hs (vga_hs),
.vga_vs (vga_vs),
.vga_rgb (vga_rgb),
//显示模块输入像素坐标数据信号
.pixel_hpos (pixel_hpos_w),
.pixel_vpos (pixel_vpos_w),
.pixel_data (pixel_data_w)
);
//vga显示模块
vga_display u_vga_disp(
.clk_25MHz (clk_w),
.rst (sys_rst),
.pixel_hpos (pixel_hpos_w),
.pixel_vpos (pixel_vpos_w),
.pixel_data (pixel_data_w)
);
endmodule
// VGA驱动模块,分辨率640*480@60
module vga_driver(
input wire clk_25MHz, //根据不同分辨率的VGA设定的时钟频率
input wire rst,
input wire [15:0] pixel_data, //RGB--565,即pixel_data[15:11]控制R、pixel_data[10:5]控制G、pixel_data[4:0]控制B
output wire [ 9:0] pixel_hpos, //pixel_data的行坐标
output wire [ 9:0] pixel_vpos, //pixel_data的列坐标
output wire vga_hs, //行同步信号
output wire vga_vs, //列同步信号
output wire [15:0] vga_rgb //输出到VGA接口的颜色数据
);
//内部参量定义,vga时序参数
parameter H_SYNC = 10'd96; // 同步期
parameter H_BACK = 10'd48; // 显示后沿
parameter H_DISP = 10'd640; // 显示区域
parameter H_FRONT = 10'd16; // 显示前沿
parameter H_PRIOD = 10'd800; // 行周期总长度
parameter V_SYNC = 10'd2; // 同步期
parameter V_BACK = 10'd33; // 显示后沿
parameter V_DISP = 10'd480; // 显示区域
parameter V_FRONT = 10'd10; // 显示前沿
parameter V_PRIOD = 10'd525; // 列周期总长度
// 使能信号,用于区分有效数据
wire vga_en;
// 请求信号,用于定位像素带你坐标
wire pixel_data_require;
// VGA行场同步信号计数器
reg [9:0] cnt_h;
reg [9:0] cnt_v;
// VGA行场同步信号生成
assign vga_hs = (cnt_h <= H_SYNC) ? 1'b0 : 1'b1;
assign vga_vs = (cnt_v <= V_SYNC) ? 1'b0 : 1'b1;
// 确定有效区域
assign vga_en = (((cnt_h >= H_SYNC + H_BACK) && (cnt_h < H_SYNC + H_BACK + H_DISP))
&& ((cnt_v >= V_SYNC + V_BACK) &&(cnt_v < V_SYNC + V_BACK + V_DISP)))
? 1'b1 : 1'b0;
// 请求信号比行有效数据先来一个周期,确定有效数据即将到来和即将结束
assign pixel_data_require = (((cnt_h >= H_SYNC + H_BACK - 1'b1) && (cnt_h < H_SYNC + H_BACK + H_DISP - 1'b1)) &&
((cnt_v>= V_SYNC + V_BACK) && (cnt_v < V_SYNC + V_BACK + V_DISP))) ? 1'b1 : 1'b0;
// 确定像素当前坐标
assign pixel_hpos = pixel_data_require ? (cnt_h - (H_SYNC + H_BACK - 1'b1)) : 10'd0;
assign pixel_vpos = pixel_data_require ? (cnt_v - (V_SYNC + V_BACK - 1'b1)) : 10'd0;
// 确定像素数据
assign vga_rgb = vga_en ? pixel_data:16'd0;
// 行列计数
always @ (posedge clk_25MHz or negedge rst) begin
if (!rst) begin
cnt_h <= 10'd0;
end
else begin
if (cnt_h <= H_PRIOD - 1'b1) begin
cnt_h <= cnt_h + 1'b1;
end
else begin
cnt_h <= 10'd0;
end
end
end
always @ (posedge clk_25MHz or negedge rst) begin
if(!rst)begin
cnt_v <= 10'd0;
end
else begin
if (cnt_h == H_PRIOD - 1'b1) begin
if(cnt_v <= V_PRIOD - 1'b1)
cnt_v <= cnt_v + 1'b1;
else begin
cnt_v <= 10'd0;
end
end
end
end
endmodule
// 显示模块
// 保存图像数据,driver模块从这取数据
module vga_display(
input wire clk_25MHz,
input wire rst,
input wire [9:0] pixel_hpos,
input wire [9:0] pixel_vpos,
output reg [15:0] pixel_data
);
parameter H_DISP =10'd640; // 行分辨率
parameter V_DISP =10'd480; // 列分辨率
localparam WHITE = 16'b11111_111111_11111;
localparam BLACK = 16'b00000_000000_00000;
localparam RED = 16'b11111_000000_00000;
localparam GREEN = 16'b00000_111111_00000;
localparam BLUE = 16'b00000_000000_11111;
parameter [9:0] PIC_HPOS = 10'd0; // 图片左上角行坐标
parameter [9:0] PIC_VPOS = 10'd0; // 图片左上角列坐标
parameter [9:0] PIC_WIDTH = 10'd75; // 图片宽度
parameter [9:0] PIC_HEIGHT = 10'd105; // 图片高度
// 判断现在的坐标是不是图像的显示坐标
reg pic_area; // 指示当前坐标是否是图像显示区域
always @ (posedge clk_25MHz or negedge rst) begin
if(!rst)begin
pic_area = 1'b0;
end
else begin
if((pixel_hpos >= PIC_HPOS) && (pixel_hpos < PIC_HPOS + PIC_WIDTH) && (pixel_vpos >= PIC_VPOS) && (pixel_vpos < PIC_VPOS + PIC_HEIGHT))begin
pic_area <= 1'b1;
end
else begin
pic_area <= 1'b0;
end
end
end
// 例化RAM
reg [13:0]wraddr;
reg [12:0]rdaddr;
reg rden;
reg wren;
reg [7:0]wrdata;
wire [15:0]rddata;
RAM RAM1(
.data(wrdata),
.rdaddress(rdaddr),
.rdclock(clk_25MHz),
.rden(rden),
.wraddress(wraddr),
.wrclock(clk_25MHz),
.wren(wren),
.q(rddata)
);
// 确定RAM读使能信号
always@(posedge clk_25MHz or negedge rst)begin
if(!rst)begin
rden <= 1'b0;
end
else begin
rden <= 1'b1;
end
end
// 确定RAM读地址信号
always@(posedge clk_25MHz or negedge rst)begin
if(!rst)begin
rdaddr <= 13'd0;
end
else begin
if(pic_area == 1'b1)begin
rdaddr <=(pixel_hpos - PIC_HPOS) + ((pixel_vpos == 10'd0)?16'd0:((pixel_vpos - 1'b1)* PIC_WIDTH));
end
end
end
// 确定RAM写端口的各信号
always@(*)begin
wraddr <= 14'd0;
wren <= 1'b0;
wrdata <= 8'd0;
end
// 判断当前坐标的像素数据
always @ (posedge clk_25MHz or negedge rst)begin
if(!rst)begin
pixel_data <= 16'd0;
end
else begin
if(pic_area == 1'b0)begin
if (pixel_hpos >= 0 && pixel_hpos <= (H_DISP / 5) * 1)
pixel_data <= WHITE;
else if (pixel_hpos >= (H_DISP / 5) * 1 && pixel_hpos < (H_DISP / 5) * 2)
pixel_data <= BLACK;
else if (pixel_hpos >= (H_DISP / 5) * 2 && pixel_hpos < (H_DISP / 5) * 3)
pixel_data <= RED;
else if (pixel_hpos >=(H_DISP / 5) * 3 && pixel_hpos < (H_DISP / 5) * 4)
pixel_data <= GREEN;
else
pixel_data <= BLUE;
end
else begin
pixel_data <= rddata;
end
end
end
endmodule
testbench代码也附上
`timescale 1ns/1ps
module top_tb ();
reg clk_48MHz;
reg rst;
wire vga_hs; // 输出到vga接口的行同步信号
wire vga_vs; // 输出到vga接口的场同步信号
wire [15:0] vga_rgb; // 输出到vga接口的像素数据
initial begin
clk_48MHz = 1'b0;
forever #10.4 clk_48MHz = ~clk_48MHz;
end
initial begin
rst = 1'b0;
#50 rst = 1'b1;
//#1000 $stop;
end
vga_colorbar_top vga_colorbar_top1(
.sys_clk(clk_48MHz),
.sys_rst(rst),
.vga_hs(vga_hs),
.vga_vs(vga_vs),
.vga_rgb(vga_rgb)
);
endmodule
还剩下生成25MHZ时钟的模块和RAM模块,这里均使用ip核实现,下文会介绍如何使用
实现结果:
这里图片没有正确显示的原因是我的FPGA资源不够,导致RAM的深度不够,不能存储图片的所有数据,下文会详细解释
IP核使用:
双口RAM设置PLL设置(有很多文章详细的介绍了,这里不再赘述)
双口RAM设置:
打开quartus软件,点击tool>MegaWizard Plug-In Manager,在弹出的界面直接点击Next
如下图,选择RAM:2-PORT,再点击三个小点选择文件保存路径,笔者建议再项目文件夹中新建一个名叫IP的文件夹专门存放IP核文件
如下图,注意,选择路径的时候需要在最后写上生成的IP的名字,然后点击Next
如下图操作,点击next
注意,要根据自己板子的资源选择合适的深度,否则资源不够会报错
这里根据自己的需求选择
这里直接点击next
这里我们选择初始化数据,点击Browse选择数据文件,这个数据文件格式可以是.HEX或者.mif,下文会介绍如何使用我们自己的图片生成该文件
这里有一个坑要说明!!
我这里选择.hex文件的路径是C:/Users/Administrator/Desktop/PIC24.hex并没有将该文件保存在我的项目文件夹里,是因为如果保存在项目文件夹里,这里的路径会变得很长,这时候如果使用modelsim仿真,会发现读出的数据全为0000,所以这里的文件路径要尽可能地短,这里我花了好长时间才找到原因!太坑了!
`
后面一路next就行了,这样我们就生成了我们的RAM IP核
IP核生成之后,也可以进行修改,并且前面提到的mif文件路径过长的问题,可以如下图解决
生成hex/mif文件:
选择一副我们的图片,修改分辨率为合适的大小,这里推荐使用windows自带的画图工具
点击上方的重新调整大小,勾选保持纵横比,选择合适的尺寸
这里非常值得注意!
VGA显示图像是RGB565,红色-绿色-蓝色分别分配5bit-6bit-5bit数据,一个像素就是16bit数据我们的RAM每个地址保存8bit数据,深度我们选择的是16384(=2 ^ 14)。我们选择读出时数据是16bit,写入时数据时8bit,所以读出地址深度为8192 = 16384/2 (=2 ^ 13),也就是说我们的RAM最多能保存8192个像素数据,所以我们选择的图像像素不能超过8192上面我放出的我的是实验结果就是因为我选择的图片分辨率为100*140=14000,大于8192,导致显示的图像后半部分重复了图像开头的数据
点击确定后,另存为图像,保存成bmp格式
百度搜索一下这个软件,随便一搜就能搜出来
打开BMP2MIF软件,点击加载,右侧可以看到图像的信息,如下图操作,点击生成
生成的mif或hex文件我们就可以在生成IP核的时候用上了
这里也有一个坑!
前面说过图像像素不能超过8192,所以我一开始是选择的分辨率75 * 100,然而这个软件生成的mif文件我在生成IP核之后,图片显示出错,成了下图的样子,我猜测应该是分辨率太低,软件生成了错误的mif文件,在把图像分辨率调成100*140之后显示就正常了,这里也让我花了好长时间!
ModelSim使用简单介绍:
首先,点击tool>options,在弹出的窗口如下图所示,确认路径是否正确,如果没有路径就需要手动添加(也有可能是因为你没有下载ModelSim,需要额外下载)
注意:ModelSim 的路径是modelsim_ae,而ModelSim-Altera的路径是modelsim_ase,不要弄反了,很容易有人弄反(比如我)
点击assignment>setting,弹出的窗口按如下图操作
设置好了testbench文件之后,点击tool>run simulation tool > RTL simulation,弹出如下界面
点击左边的模块名,右侧显示所有变量名,拖动就可以放进仿真列表里
一些经常用到的功能
结论:
总的来说没什么难点,就是因为不熟悉,导致有一些坑没注意到,调试找原因还是很累人的