14FPGA综设之图像边沿检测的sobel算法
阅读原文时间:2023年07月11日阅读:4

连续学习FPGA基础课程接近一个月了,迎来第一个有难度的综合设计,图像的边沿检测算法sobel,用verilog代码实现算法功能。

一设计功能

(一设计要求)

(二系统框图)

根据上面的系统,Verilog代码如下:注意的是,VGA模块的时钟输入有两个,一是50M,二是25M。PLL的IP核的输入时钟连接顶层时钟,产生的输出时钟连接各个功能模块,有两个一是50M,二是25M。50M连接串口接收,sobel_ctrl控制模块。25M连接VGA_ram的vga显示部分和RAM的读地址的时钟,50M连接VGA_ram的RAM的写地址时钟。

module top_sobel(
input s_clk,
input wire rst_n,
input wire rx,

output wire hsync,
output wire vsync,
output wire [7:0] rgb
);

wire clk_25out;
wire clk_50M;
gen_clk25M gen_clk_ins
(// Clock in ports
.CLK(s_clk), // IN
// Clock out ports
.CLK_25MOUT1(clk_25out), // OUT
.CLK_50M(clk_50M)); // OUT

wire uart_flag;
wire [7:0]uart_data;

wire sobel_flag;
wire [7:0]sobel_data;

uart_rx uart_rx_m0(
.sclk(clk_50M),
.rst_n(rst_n),
.rx(rx),
.po_data(uart_data),
.po_flag(uart_flag)
);

sobel_ctrl inst_sobel_ctrl (
.clk (clk_50M),
.rst_n (rst_n),
.pi_flag (uart_flag),
.pi_data (uart_data),
.po_flag (sobel_flag),
.po_sum (sobel_data)
);

vga_ram inst_vga_ram (
.clk (clk_25out),
.clks (clk_50M),
.rst_n (rst_n),
.pi_flag (sobel_flag),
.pi_data (sobel_data),
.hsync (hsync),
.vsync (vsync),
.vga_rgb (rgb)
);

endmodule

二设计思路

在原有基础上,自己动手设计,仿真验证,调试直到成功。

(一)我自己觉得这次的功能和上次的双FIFO流水线很紧密,所以这次,应该是先把双FIFO的逻辑弄懂,即把他们的时序图自己大致画一下,然后我觉得关键是怎么采集三行三列的9个数,我的想法是,比如第零行用FIFO1存储,然后再加三个寄存器,打拍操作。每来一个串口接收标志信号就送FIFO1的数据到如寄存器1中,然后是寄存器2,再是寄存器3。这样不断循环,就可以采集九个数据,最后再按照步骤进行相应的加法和乘法,绝对值操作。

刚开始要循序渐进,可以弄20X20的数据,而且只是实现到DX这一步,等完成了再往下继续加功能。

(二)设计知识点

1.打拍操作:同步复位,没有复位信号

第二点,我的FIFO1和FIFO2数据是在rd_en读使能信号控制下进行寄存器的打拍操作,而对这就个数进行运算则是在add_flag控制下,赋值输出(原因是rd_en  提前了add_flag一个时钟周期)

尤老师,讲为啥有wr_en_pre1和wr_en_pre12.主要是让FIFO的写使能信号和读使能信号同步。

sobel算法实现过程介绍

第三点,仔细看了几遍sobel算法的介绍,明白只需要采集到9个点,然后再按照步骤实现功能即可。

三所遇问题及解决办法

由于这次编写的综合设计模块,众多,而且代码量有上千行,我先暂时不详解每个模块的设计代码,而是我在亲自动手设计图像边沿检测的收获。

问题一:我直接在v3的源码中修改,想直接仿真运行,却发现modelsim报错:显示路径错误

* Error: (vopt-1933) Unable to create temporary directory D:/netclass/firstlevel/net19_double_fifo/double_fifo/work/_tempmsg

# No such file or directory. (errno = ENOENT)

# Error loading design

原因:一是可能路径太长,二是原工程的路径和当前不符合。所以要么把工程直接放在更目录或者自己重新建立一个。

我的解决办法,自己重新建立工程,如IP核等,或者路径更改,可以移除工程在添加进去。

问题二:仿真时找不到文本,提示如下

** Warning: (vsim-7) Failed to open readmem file "./data.txt" in read mode.

# No such file or directory. (errno = ENOENT)    : sim/tb_top_uart.v(45)

#    Time: 0 ps  Iteration: 0  Instance: /tb_top_uart

下面是错误目录下

解决办法:我是问了尤老师才知道应该放在modelsim的目录下即和ISE工程同一目录下

问题三:怎么对86X4的数据进行仿真

答案是并转串。即本来是86X4的矩阵,但我可以先在TXT文档中用344X1的数据替代,直接在仿真中把86改成344即可。

`timescale 1ns / 1ps

module tb_top_uart;

// Inputs

reg sclk;

reg rst_n;

reg rx;

reg [7:0] mem[343:0];

// Outputs

wire tx;

// Instantiate the Unit Under Test (UUT)

top_dfifo uut (

.clk(sclk),

.rst_n(rst_n),

.rx(rx),

.tx(tx)

);

initial begin

// Initialize Inputs

sclk = 0;

rst_n = 0;

rx = 1;

// Wait 100 ns for global reset to finish

#100;

rst_n =1;

// Add stimulus here

end

initial begin

$readmemb("./data.txt",mem);

end

always #10 sclk = ~sclk;

initial begin

#200;

rx_byte();

end

task rx_byte();

integer i;

integer j;

begin

for(j=0;j<344;j=j+1)begin

for (i=0;i<344;i=i+1)begin

rx_bit(mem[i]);

end

end

end

endtask

task rx_bit(input [7:0] data);

integer i;

begin

for(i=0;i<10;i=i+1) begin

case (i)

0:rx =0;

1:rx =data[i-1];

2:rx =data[i-1];

3:rx =data[i-1];

4:rx =data[i-1];

5:rx =data[i-1];

6:rx =data[i-1];

7:rx =data[i-1];

8:rx =data[i-1];

9:rx =1;

endcase

#104160;

end

end

endtask

endmodule

问题三,怎么进行绝对值的计算

方法:先自己网上搜了下,大概是利用原码反码,补码的关系。即最高位为符号位,1表示负数,绝对值为取反加一。0表示正数,绝对值等于本身。

//abs_dx

reg [7:0]abs_dx;

reg [7:0]abs_dy;

always@(posedge clk or negedge rst_n)

if(!rst_n)

abs_dx<=0;

else if(flag_abs & dx[7]==1)

abs_dx<=~dx+1;

else if(flag_abs & dx[7]==0)

abs_dx<=dx;

问题四,怎么写VGA控制程序,在里面调用一个RAM。用来存储198X198个数据,VGA模块负责RAM的读写,让RAM里写入sobel_ctrl模块处理好数据,读出来的数据需要给rgb进行显示。

我的想法是:要做一个新东西,就首先学会联系已学过的东西(基础),既然用RAM读写这198X198数据,那么首先得搞明白RAM。再自己适当修改一下读写逻辑,如数据的读写地址等等就欧克。

我选择的RAM类型为:Simple Dual Port RAM,该ram包含两个地址总线,一个写地址和一个读地址,分别控制两个地址总线可以控制该ram的读和写。还有一个关键信号:wr_en,控制读写逻辑。本RAM位宽为8深度为256的ram.

下面的代码是根据上面的RAM的读写时序和设定的位宽深度设计的:

module ctrl_ram(

input      wire                    clk,

input      wire                    rst_n,

input      wire      [7:0]       pi_data,

output   wire      [7:0]       po_data

);

reg               wr_en;

reg [7:0]       wr_addr;

reg [7:0]       rd_addr;

always @(posedge clk or negedge rst_n) begin

if (rst_n == 1'b0) begin

wr_en <= 1'b1;

end

else if(rd_addr == 'd255) begin

wr_en <= 1'b1;

end

else if (wr_addr == 'd255) begin

wr_en <= 1'b0;

end

end

always @(posedge clk or negedge rst_n) begin

if (rst_n == 1'b0) begin

wr_addr <= 'd0;

end

else if (wr_en == 1'b1) begin

wr_addr <= wr_addr + 1'b1;

end

else begin

wr_addr <= 'd0;

end

end

always @(posedge clk or negedge rst_n) begin

if (rst_n == 1'b0) begin

rd_addr <= 'd0;

end

else if (wr_en == 1'b0) begin

rd_addr <= rd_addr + 1'b1;

end

else begin

rd_addr <= 'd0;

end

end

ram_256x8 ram_256x8_inst (

.clka(clk), // input clka

// .wea(wr_en), // input [0 : 0] wea

.wea(1'b0),

.addra(wr_addr), // input [7 : 0] addra

.dina(pi_data), // input [7 : 0] dina

.clkb(clk), // input clkb

.addrb(rd_addr), // input [7 : 0] addrb

.doutb(po_data) // output [7 : 0] doutb

);

endmodule

而这次的写使能信号是由 sobel_ctrl的pi_flag控制的,还有一个关键点是写地址是50M的系统时钟,而读地址是25M的时钟。所以需要在顶层加一个PLL输出两个时钟,一个是50M,一个是25M,PLL的输入时钟接顶层时钟50M,然后把PLL输出的时钟分别连在串口模块,sobel模块,VGA模块。

关键点:在VGA里调用一个40K的ram存储198X198的数据,怎么设计写使能信号和读写地址值得注意。先将它的代码展示如下

module vga_ram(
input wire clk,
input wire clks,
input wire rst_n,
input wire pi_flag,
input wire [7:0] pi_data,
output reg hsync,
output reg vsync,
output reg [7:0] vga_rgb
);

reg [15:0] addrb,addra;
wire [7:0] doutb;

parameter MAX_value = 16'd39203;

reg [8:0]x; //行移动计数器最大439
reg [8:0]y; //场移动计数器最大279

reg dec_x;//行计数器减一切换标志信号
reg dec_y;//场计数器减一切换标志信号

reg [9:0]cnt_h;
reg [9:0]cnt_v;

parameter h_max =10'd799;
parameter v_max = 10'd524;

//行计数器
always@(posedge clk or negedge rst_n)
if(!rst_n)
cnt_h<=10'd0;
else if(cnt_h==h_max)
cnt_h<=10'd0;
else
cnt_h<=cnt_h+1'b1;
//场计数器
always@(posedge clk or negedge rst_n)
if(!rst_n)
cnt_v<=10'd0;
else if(cnt_v==v_max & cnt_h==h_max)
cnt_v<=10'd0;
else if(cnt_h==h_max)
cnt_v<=cnt_v+1'b1;
//hsync 行同步信号
always@(posedge clk or negedge rst_n)
if(!rst_n)
hsync<=1'b1;
else if(cnt_h==10'd95)
hsync<=1'b0;
else if(cnt_h==h_max)
hsync<=1'b1;

//vsync场同步信号
always@(posedge clk or negedge rst_n)
if(!rst_n)
vsync<=1'b1;
else if(cnt_v=='d1 & cnt_h==h_max)
vsync<=1'b0;
else if(cnt_v==v_max & cnt_h==h_max)
vsync<=1'b1;

parameter T100MS = 23'd2_599_999;
//div counter
reg [22:0]div_cnt;
always@(posedge clk or negedge rst_n)begin
if(rst_n==0)
div_cnt<=23'd0;
else if(div_cnt==T100MS)
div_cnt<=23'd0;
else
div_cnt<=div_cnt+1'b1;
end

//the flag of one_s_flag
reg one_s_flag;
always@(posedge clk or negedge rst_n)begin
if(rst_n==0)
one_s_flag<=1'b0;
else if(div_cnt==(T100MS-1))begin
one_s_flag<=1'b1;
end
else begin
one_s_flag<=1'b0;
end
end
always@(posedge clk or negedge rst_n)
if(!rst_n)begin
x<=9'd0;
dec_x<=1'b0;
end
else begin
case(dec_x)
0:begin
if(x==9'd439)begin
x<=x;
dec_x<=1'b1;
end
else if(one_s_flag) begin
x<=x+1'b1;
dec_x<=1'b0;
end
end
1:begin
if(x==9'd0)begin
x<=x;
dec_x<=1'b0;
end
else if(one_s_flag) begin
x<=x-1'b1;
dec_x<=1'b1;
end
end
default: ;
endcase
end

always@(posedge clk or negedge rst_n)
if(!rst_n)begin
y<=9'd0;
dec_y<=1'b0;
end
else begin
case(dec_y)
0:begin
if(y==9'd279)begin
y<=y;
dec_y<=1'b1;
end
else if(one_s_flag)begin
y<=y+1'b1;
dec_y<=1'b0;
end
end
1:begin
if(y==9'd0)begin
y<=y;
dec_y<=1'b0;
end
else if(one_s_flag)begin
y<=y-1'b1;
dec_y<=1'b1;
end
end
default: ;
endcase
end

always@(posedge clk or negedge rst_n)
if(!rst_n)
vga_rgb<=8'b0; else if(cnt_h>10'd144+x &cnt_h<=10'd343+x & cnt_v>10'd35+y & cnt_v<=10'd234+y) vga_rgb<=doutb; else if(cnt_h>10'd144 &cnt_h<=10'd783 & cnt_v>10'd35 & cnt_v<=10'd194) vga_rgb<=8'b111_000_00; else if(cnt_h>10'd144 &cnt_h<=10'd783 & cnt_v>10'd194 & cnt_v<=10'd354) vga_rgb<=8'b000_111_00; else if(cnt_h>10'd144 &cnt_h<=10'd783 & cnt_v>10'd354 & cnt_v<=10'd514)
vga_rgb<=8'b000_000_11;
else
vga_rgb<=8'b0;

always @(posedge clk or negedge rst_n) begin
if (rst_n == 1'b0) begin
// reset
addrb <= 'd0; end else if(cnt_h >=143+x && cnt_h <=340+x && cnt_v >=35+y && cnt_v <=232+y && addrb=='d39203)begin addrb <= 'd0; end else if (cnt_h >=143+x && cnt_h <=340+x && cnt_v >=35+y && cnt_v <=232+y) begin
addrb <= addrb + 1'b1;
end
end

always @(posedge clks or negedge rst_n) begin
if (rst_n == 1'b0) begin
// reset
addra <= 'd0;
end
else if (pi_flag == 1'b1 && addra == 'd39203) begin
addra <= 'd0;
end
else if (pi_flag == 1'b1) begin
addra <= addra + 1'b1;
end
end

RAM40K ram_inst(
.clka(clks),
.wea(pi_flag),
.addra(addra),
.dina(pi_data),
.clkb(clk),
.addrb(addrb),
.doutb(doutb)
);
endmodule

问题五,怎么产生200X200的数据,即把一个200X200像素的图片转换为200X200的数据阵列?

以前的经验是,直接弄一个txt文本储存200X200的数据,不过是并转串,而且是用在仿真中。

尤老师的经验是,用MATLAB处理,即给一个200X200的像素图片,用MATLAB的相应语句转换产生一个200X200的数据阵列,再复制到友善串口助手发送,之后发现显示器还是黑色的,他推测可能是阈值过大。

我觉得遇到这种完全新的,还是先记录问题,再看哈视频,然后自己动手做。

在图像边沿检测的视频二50分钟,我看到了建完所有的模块。

下面是MATLAB的图片转阵列的代码,第一行是读取图片(直接把图片 粘贴在当前文件夹下,再v3edu修改成图片相应的名字)第二行是转换的图片的阵列大小,这个是根据MATLAB相应的工作路径下显示可以转换的范围,而不是胡乱搞的。

clc;

clear all;

rgbimage=imread('./v3edu.jpg','jpg');%读取rgb图像

grayimage=rgbimage(1:200,1:200,1);%去其中r分量作为传递图像

grayimage=bitshift(grayimage,-5);%右移5位取R分量的高三位

fid=fopen('imagedata.txt','w+');%打开文件返回句柄

fprintf(fid,'%02x ',grayimage');%将图像转置行列对换(默认matlab

imshow(rgbimage);

操作示意图如上

解决办法:即出现多沿触发时,要同一个信号如CLK都是上升沿,或rst_n都为下降沿有效。

问题五。怎么debug?

首先在顶层,看看各个模块的连接有没有错。第二步,检查控制模块sobel_ctrl的各个模块和信号的逻辑有没有错。第三步,逻辑分析仪:在ise14.7中建立了ICON和ILA这两个IP核(弄懂这)

问题六:分成两部分一是没有图像显示,原来时没有管脚约束文件。。

第二个问题是,RAM的数据没有读出来,即移动的方框一直显示黑色,即RAM输出的po_data一直为零。(rgb信号全1为白,全零为黑)

故解决办法,明天好好改哈RAM的读写逻辑。

通过综合器的警告,我发现,在顶层模块中,每个功能模块没有和顶层的时钟信号连接,即只连接的自己模块的,这没有时钟驱动源。我用pll模块产生了两个时钟输出:一个50M和25M,然后对应连接各个模块就欧克勒。

最终结论确实是RAM模块的读写地址时钟不一样,还有就是顶层模块,除了PLL的输入时钟连接系统时钟,其他模块的时钟信号都是连接的PLL的输出时钟,50M 或25M。

最终显示效果如下,在看到图像那一刻很开心,比较自己亲自调了一周程序,还好没放弃:

皮卡丘原图:

经过sobel算法处理后的图片:

知识点:一是怎么采集3X3的9个数,直接用2个FIFO,在每个FIFO用三个寄存器,在标志信号的控制下进行打拍 操作。二是怎么把一个图片转换为一个如200X200的矩阵数列,直接用MATLAB转换即可。

我的收获是:一是做一个新东西,在原有基础上想办法。二是,要拆分设计验证,不能一把搞完所有模块,直接去上板验证。

手机扫一扫

移动阅读更方便

阿里云服务器
腾讯云服务器
七牛云服务器

你可能感兴趣的文章