参考文献
[1]VGA时序及其原理(包含不同大小的显示器)
[2]Verilog的系统任务----
r
e
a
d
m
e
m
h
和
readmemh和
readmemh和readmemb
前言
在进行FPGA设计时利用仿真工具进行逻辑仿真是不可避免的一个过程,仿真工具(常见的就是Vivado自带的仿真工具以及Modelsim)倒是大同小异。在波形仿真时,单纯使用initial生成信号是完全不够的,特别是在进行图像仿真的时候,我们总不能用initial生成一张图片。下面将介绍一下图像仿真技巧之一,利用VGA时序将图片输入到图片处理模块当中。
1. VGA时序
VGA总线包含vga_clk, v ga_vs, vga_hs, vga_de, vga_data五种信号。vga_clk即为时钟信号;vga_vs为场同步信号(vertical synchronous signal),代表着一帧信号是否有效;vga_hs为行同步信号(horizontal synchronous signal),代表一行数据是否有效;vga_de为数据有效信号(data enable),代表是否数据有效;vga_data为数据信号(以下为RGB888 24bits)。以下为VGA时序图。需要注意的是行场同步信号既有可能高电平有效也有可能低电平有效,具体看设计要求,下面设计以低电平有效为例。
为了数据的对齐,VGA时序增加一些前沿(Front porch)和后沿(Back porch),不同显示分辨率对应不同的沿参数。具体含义可参考该博客(传送门)。以下以640x480分辨率为例。
2. VGA时序代码
该模块模拟VGA时序输出,模块内部vga_data是累加得到的,具体数据可以另外设置,这也就是后面如何利用VGA时序读取图片的内容。此外该模块使用的分辨率是640x480,可以修改参数改变分辨率。
`timescale 1ns / 1ps
//VGA接口,模拟VGA显示时序的功能
module vga_gen(
input vga_clk,
input rst_n,
output reg vga_hs, //高电平有效
output reg vga_vs, //高电平有效
output reg vga_de,
output reg [7:0] vga_data
);
reg[15:0] h_counter; //行计数器
reg[15:0] v_counter; //场计数器
reg vsenable; //使能vc,场计数器
//parameter define
// 640x480
parameter H_SYNC = 10'd96 , //行同步时钟周期数
H_BACK = 10'd40 , //行时序后沿
H_LEFT = 10'd8 , //行时序左边框
H_VALID = 10'd640 , //行有效数据
H_RIGHT = 10'd8 , //行时序右边框
H_FRONT = 10'd8 , //行时序前沿
H_TOTAL = 10'd800 ; //行扫描周期
parameter V_SYNC = 10'd2 , //场同步
V_BACK = 10'd25 , //场时序后沿
V_TOP = 10'd8 , //场时序上边框
V_VALID = 10'd480, //场有效数据
V_BOTTOM = 10'd8 , //场时序下边框
V_FRONT = 10'd2 , //场时序前沿
V_TOTAL = 10'd525 ; //场扫描周期
//行同步信号计数器
always@ (posedge vga_clk or negedge rst_n)begin
if(!rst_n) begin
h_counter <= 0;
vsenable <= 0;
end
else if(h_counter == H_TOTAL-1) begin
h_counter <= 0;//复位行计数器
vsenable <= 1;//使能场计数器
end
else begin
h_counter <= h_counter+1;
vsenable <= 0;
end
end
//场同步信号计数器
always@(posedge vga_clk or negedge rst_n)begin
if(!rst_n)
v_counter <= 0;
else
if(vsenable == 1)
begin
if(v_counter == V_TOTAL-1)
v_counter <= 0;
else
v_counter <= v_counter+1;
end
end
//产生vsync脉冲
always @(*) begin
if(v_counter <= V_SYNC - 1'b1)
vga_vs = 1;
else
vga_vs = 0;
end
//产生hsync脉冲
always @(*)begin
if(h_counter <= H_SYNC - 1'b1)
vga_hs = 1;
else
vga_hs = 0;
end
//产生de脉冲
always@(*) begin
if((h_counter >= H_SYNC + H_BACK + H_LEFT)
&& (h_counter < H_SYNC + H_BACK + H_LEFT + H_VALID)
&&(v_counter >= V_SYNC + V_BACK + V_TOP)
&& (v_counter < V_SYNC + V_BACK + V_TOP + V_VALID))
vga_de = 1;
else
vga_de = 0;
end
//生成vga_data数据
always @(posedge vga_clk or negedge rst_n)begin
if(!rst_n)
vga_data <= 'd0;
else if(vga_de)
vga_data <= vga_data+'d1;
end
endmodule
3. 利用VGA时序读取图片
为了数据的同步和流水线操作,很多图像处理模块都会存在VGA总线。如果设计存在问题,直接修改代码后直接导板验证无疑效率低下。那比较有效的方法是将需要处理的图片按照VGA时序输入到处理模块,直接可以看模块的功能输出是否正确。下面将完成这一部分的testbench代码的编写。该部分代码涉及到文件的读写以及VGA时序的生成。这部分着重讲解testbench文件的读写。
testbench读文件
testbench可以利用$readmemh()以16进制读取txt文件,或者$readmemb()以二进制读取txt文件。函数用法可参考该博客(传送门)。
reg [23:0] img[0:640*480-1]; //二维数组存储图片数据
initial begin
$readmemh("D:/viavdo_pro/VGA_sim/data_in.txt",img);
end
需要注意的是:在testbench中路径是“/”而不是“\”。
其中需要将图片转换为txt文本数据,可以通过matlab或者是python等工具进行转换即可。以下以python代码为例。
#这是一个图片转文本的python代码
import cv2
img_path = "Lena.jpg" # 400x400
txt_path = "Lena.txt"
img = cv2.imread(img_path) # [BGR....],
img = cv2.resize(img, (640, 480))
# img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # [RGB....]
with open(txt_path, "w") as f:
for i in range(img.shape[0]): # 行
for j in range(img.shape[1]): # 列
# for c in range(img.shape[2]): # 通道
# f.write('%02X' % img[i][j][c]) # 以固定格式写入
f.write('%02X' % (img[i, j, 2])) # 以固定格式写入,低位为R
f.write('%02X' % (img[i, j, 1]))
f.write('%02X' % (img[i, j, 0]))
f.write("\n")
testbench写文件
testbench可以利用$fopen(file_path)和$fwrite()函数将数据写入到txt文件中。
integer file_out;
initial file_out = $fopen("D:/vivado_pro/VGA_sim","w");
always @(posedge clk)begin //这个时钟无所谓
if(de) //图片处理模块输出数据有效信号
$fwrite(file_out,"%d\n",data_out); //data_out为经过处理后的图像数据
end
其中需要将得到的txt文件转换为图片,可以通过matlab或者是python等工具进行转换即可。以下以python代码为例。
# 这是一个文本转图片的python代码
import cv2
import numpy as np
def read_txt(path):
"""
读取txt文件,返回txt_data
"""
with open(path, 'rb') as f:
# 文件在with语句块结束时自动关闭
data = [(int(line.strip(), 16)) for line in f] # strip()用于去除每行末尾的换行符
return data
def list2mat(h,w,c,list_data):#高,宽,通道
img_array = np.zeros((h, w, c), dtype=np.uint8)
for i in range(h):
for j in range(w):
img_array[i, j, 0] = (list_data[i * w + j] & 0x00_00_ff) >> 0 # R
img_array[i, j, 1] = (list_data[i * w + j] & 0x00_ff_00) >> 8 # G
img_array[i, j, 2] = (list_data[i * w + j] & 0xff_00_00) >> 16 # B
return img_array
img_path = "output.jpg" # 400x400
txt_path = "data_out.txt"
IMG_W = 640
IMG_H = 480
IMG_C = 3
data_list = read_txt(txt_path)
img_array = list2mat(IMG_H, IMG_W, IMG_C, data_list) # txt文件里面,低位为R
cv2.imshow('Image', img_array)
cv2.waitKey(0)
#4. 举例说明
操作步骤:
1) python将图片转换成txt文件;
2) testbench读取txt文件存入数组;
3) testbench将处理后的图像数据存入txt文件;
4) python读取txt文件获取处理后的图像。
下面通过一个例子说明testbench如何使用VGA时序读取图片并输入到图像处理模块,并查看最后输出结果。以下的图像处理模块实现的是简单的RGB转BGR功能。
`timescale 1ns / 1ps
module tb();
reg clk;
reg rst_n;
reg vga_vs,vga_hs,vga_de;
reg [23:0] vga_data;
wire vga_clk;
wire vga_vs_r,vga_hs_r,vga_de_r;
wire [23:0] vga_data_r;
wire vs_out,hs_out,de_out;
wire [23:0] data_out;
parameter IMG_W = 640;
parameter IMG_H = 480;
initial begin
clk = 1'b0;rst_n = 1'b1;
#10 rst_n = 1'b0;
#10 rst_n = 1'b1;
end
always #5 clk = ~clk;
//模块例化
vga_gen vga_gen(
.vga_clk (clk ),
.rst_n (rst_n ),
.vga_vs (vga_vs_r ),
.vga_hs (vga_hs_r ),
.vga_de (vga_de_r )
);
assign vga_clk = clk;
RGB2BGR RGB2BGR(
.clk (clk ),
.rst_n (rst_n ),
//input
.vs_i (vga_vs ),
.hs_i (vga_hs ),
.de_i (vga_de ),
.data_i (vga_data ),
//output
.vs_o (vs_out ),
.hs_o (hs_out ),
.de_o (de_out ),
.data_o (data_out )
);
// ---------------读取txt文件-------------
reg [23:0] img[0:IMG_W*IMG_H-1]; //二维数组存储图片数据
reg [19:0] addr;
initial begin
$readmemh("E:vivado_pro/VGA_sim/data_in.txt",img);
end
always @(posedge vga_clk or negedge rst_n)begin
if(!rst_n)begin
addr <= 'd0;vga_data <= 'd0;
end
else if(vga_de_r)begin
vga_data <= img[addr];
addr <= addr + 'd1;
if(addr == IMG_W*IMG_H - 1)
addr <= 'd0;
end
end
//延迟1clk, 与vga_data同步
always @(posedge vga_clk)begin
vga_vs <= vga_vs_r;
vga_hs <= vga_hs_r;
vga_de <= vga_de_r;
end
// ---------------写入txt文件-------------
integer file_out;
reg [19:0] wr_addr;
initial file_out = $fopen("E:/vivado_pro/VGA_sim/data_out.txt","w");
always @(posedge vga_clk or negedge rst_n)begin
if(!rst_n)
wr_addr <= 'd0;
else if(wr_addr == IMG_W*IMG_H)begin
$display("task finish!\n");
$finish;
end
else if(de_out)begin //图片处理模块输出数据有效信号
wr_addr <= wr_addr + 1'b1;
$fwrite(file_out,"%x\n",data_out); //data_out为经过处理后的图像数据
end
end
endmodule
//图像处理模块,RGB图像转BGR模块
module RGB2BGR(
input clk,
input rst_n,
input vs_i,
input hs_i,
input de_i,
input [23:0] data_i,
output reg vs_o,
output reg hs_o,
output reg de_o,
output reg [23:0] data_o
);
always @(posedge clk or negedge rst_n)begin
if(!rst_n)begin
vs_o <= 1'b0;
hs_o <= 1'b0;
de_o <= 1'b0;
data_o <= 'd0;
end
else begin
vs_o <= vs_i;
hs_o <= hs_i;
de_o <= de_i;
data_o <= {data_i[7:0],data_i[15:8],data_i[23:16]};
end
end
endmodule
仿真波形
上图为VGA时序总览图。
上图为输入文本的值。
上图是testbench读取到的像素值,可见和输入文本的数据是相同的。
原图像
处理后图像
对比原图像和处理后的图像可以得到,该模块功能实现结果与预期结果一致,将RGB图像转换为了BGR图像。这里需要注意的是代码里RGB数据的顺序。
总结
本文介绍了verilog如何利用python等软件和波形仿真工具联合仿真,将图像输入到testbench中进行图像处理模块的逻辑验证。本文中的图像处理模块RBG2BGR模块可以替换成任意具有VGA总线的其他功能图像处理模块,很大程度帮助了我进行图像算法开发。当然以上仅仅是一种仿真思路,总线接口可任意,算法任意。