目录
一 项目结构
1.1 设计思路
1.2 设计流程
二 接口设计
2.1 摄像头配置模块
2.2 IIC_master 模块
之后就进行数据采集
2.3 采集数据模块
2.4 灰度转化
2.5 高斯滤波
2.7 二值化
2.8 Sobel边缘检测
2.9 SDRAM乒乓缓存
2.10 VGA显示
三 代码设计
基于 OV5640的 图像边沿检测,采集的图像大小是 1280 * 720 ,采用VGA接口进行显示
项目模块设计:
需要接收摄像头配置完成的信号,当场同步信号拉低后,且行参考信号有效时进行数据的采集。但是摄像头的数据是把16位RGB拆分为高八位和低八位发送的,我们需要通过移位+位拼接的方式把两个8bit数据合并成16bit数据输出,同时为了SDRAM模块更好的识别帧头和帧尾,在图像的第一个像素点以及最后一个像素点的时候分别拉高sop和eop信号,其余像素点拉低。
数据处理,包括图像的灰度转化、高斯滤波、二值化,和Sobel边沿检测等。具体实现后续讲解。
乒乓操作主要⽤于控制数据流,在此项⽬中主要体现为先写SDRAM bank1的数据,同时读SDRAM bank3的数据,当两块bank的数据读写完毕后,切换操作为读bank1的数据,写bank3的数据,这样可以保持数据为完整的⼀帧,使显⽰屏帧与帧之间切换瞬间完成。
显示,利用VGA接口将数据显示到显示屏上。
module cmos_config(
input clk ,
input rst_n ,
//i2c_master
output req ,//请求
output [3:0] cmd ,//命令
output [7:0] dout ,//数据
input done ,//应答
output config_done //配置完成信号
);
摄像头配置模块比较简单,之前设计过利用IIC协议来读写EEPROM,摄像头的控制模块还比EEPROM控制模块简单,只涉及到了向摄像头写入数据,即配置它的功能。
配置流程:
- 主要采用状态机加计数器的方式来设计配置模块;
- 当上电之后计数20ms,之后就可以进行摄像头的配置,有一个配置完成信号,当配置完254个寄存器后,配置信号有效。
- 配置模块主要就是通过IIC_master模块向摄像头里面写入数据,完成配置。
- 发送数据是以任务的方式发请求、命令和数据。
接口模块的作用就只是用来进行发数据或读数据,这里没有用到读数据。
module i2c_master(
input clk ,
input rst_n ,
input req ,
input [3:0] cmd ,
input [7:0] din ,
output [7:0] dout ,
output done ,
output slave_ack ,
output i2c_scl ,
input i2c_sda_i ,
output i2c_sda_o ,
output i2c_sda_oe
);
设计接口模块
- 也是用状态机加计数器来设计接口模块,状态机的设计就是根据IIC协议的读写时序来设计的,具体时序之前的博客有写过。
- 里面很重要的一点就是时钟设计,我用的传输速率是 200k bit/s,因此一个IIC时钟周期 SCL = 50 M / 200 K = 250 次系统时钟周期,需要一个计数器,来记250个系统时钟周期,通过IIC协议收发数据,都是根据SCL来进行的。
SCL设计:
//scl
always @(posedge clk or negedge rst_n)begin
if(~rst_n)begin
scl <= 1'b1;
end
else if(idle2start | idle2write | idle2read)begin//开始发送时,拉低
scl <= 1'b0;
end
else if(add_cnt_scl && cnt_scl == `SCL_HALF-1)begin
scl <= 1'b1;
end
else if(end_cnt_scl && ~stop2idle)begin
scl <= 1'b0;
end
end
数据发送:
//sda_out
always @(posedge clk or negedge rst_n)begin
if(~rst_n)begin
sda_out <= 1'b1;
end
else if(state_c == START)begin //发起始位
if(cnt_scl == `LOW_HLAF)begin //时钟低电平时拉高sda总线
sda_out <= 1'b1;
end
else if(cnt_scl == `HIGH_HALF)begin //时钟高电平时拉低sda总线
sda_out <= 1'b0; //保证从机能检测到起始位
end
end
else if(state_c == WRITE && cnt_scl == `LOW_HLAF)begin //scl低电平时发送数据 并串转换
sda_out <= tx_data[7-cnt_bit];
end
else if(state_c == SACK && cnt_scl == `LOW_HLAF)begin //发应答位
sda_out <= (command&`CMD_STOP)?1'b1:1'b0;
end
else if(state_c == STOP)begin //发停止位
if(cnt_scl == `LOW_HLAF)begin //时钟低电平时拉低sda总线
sda_out <= 1'b0;
end
else if(cnt_scl == `HIGH_HALF)begin //时钟高电平时拉高sda总线
sda_out <= 1'b1; //保证从机能检测到停止位
end
end
end
//sda_out_en 总线输出数据使能
always @(posedge clk or negedge rst_n)begin
if(~rst_n)begin
sda_out_en <= 1'b0;
end
else if(idle2start | idle2write | read2sack | rack2stop)begin
sda_out_en <= 1'b1;
end
else if(idle2read | start2read | write2rack | stop2idle)begin
sda_out_en <= 1'b0;
end
end
具体实现发送就是入上图,这里不多阐述了。
配置的顶层模块接口:
module cmos_top( input clk , //xclk 24M input rst_n , output scl , //scl inout sda , //sda output pwdn , output reset , output cfg_done //配置完成信号 );
之后就进行数据采集
接口设计:
module capture(
input clk ,//像素时钟 摄像头输出的pclk
input rst_n ,
input enable , //采集使能 配置完成
input vsync ,//摄像头场同步信号
input href ,//摄像头行参考信号
input [7:0] din ,//摄像头像素字节
output [15:0] dout ,//像素数据
output dout_sop,//包文头 一帧图像第一个像素点
output dout_eop,//包文尾 一帧图像最后一个像素点
output dout_vld //像素数据有效
);
设计思路:
- 先对场同步信号进行同步打拍,然后检测下降沿
//vsync同步打拍 always @(posedge clk or negedge rst_n)begin if(~rst_n)begin vsync_r <= 2'b00; end else begin vsync_r <= {vsync_r[0],vsync}; end end assign vsync_nedge = vsync_r[1] & ~vsync_r[0]; //检测下降沿
检测到下降沿,且接收到摄像头配置完成信号,采集数据标志拉高,采集完一帧图像,标志拉低。之后进行下一帧图像的采集。
- 采集数据,采集数据标志拉高且行参考信号有效时,进行数据采集,这里进行了拼接。摄像头的数据是把16位RGB拆分为高八位和低八位发送的,我们需要通过移位+位拼接的方式把两个8bit数据合并成16bit数据输出。
//data always @(posedge clk or negedge rst_n)begin if(~rst_n)begin data <= 0; end else begin data <= {data[7:0],din};//左移 //data <= 16'b1101_1010_1111_0111;//16'hdaf7 end end
- SOP、EOP、数据有效信号
//data_sop always @(posedge clk or negedge rst_n)begin if(~rst_n)begin data_sop <= 1'b0; data_eop <= 1'b0; data_vld <= 1'b0; end else begin data_sop <= add_cnt_h && cnt_h == 2-1 && cnt_v == 0; data_eop <= end_cnt_v; data_vld <= add_cnt_h && cnt_h[0] == 1'b1; end end
sop就是一帧图像的第一个像素点,eop是一帧图像的最后一个像素点,数据有效是cnt_h[0] == 1'b1,由于进行了拼接,低位始终是1,因此数据有效。
接口设计:
module rgb2gray(
input clk ,
input rst_n ,
input din_sop ,
input din_eop ,
input din_vld ,
input [15:0] din ,//RGB565
output dout_sop ,
output dout_eop ,
output dout_vld ,
output [7:0] dout //灰度输出
);
原理 :
人眼对RGB颜色的敏感度不同:对绿色最敏感,所以权值最高。对蓝色不敏感,权值最低。在C语言或者Python等高级语言中,权值是
0.299
、0.587
、0.114
,也就是说都是小数,而Verilog不支持小数运算,所以只能先消除小数点来得到乘积,最后再通过移位缩小至近似原来的大小。由于我们摄像头采集的数据时RGB565格式的,需要转化为RGB888格式的图像,进行带补偿的低三位拓展位宽,然后采用加权法进行彩色图片转灰度,用了一个心理学公式。
心理学公式:将三通道的彩色图像转化位单通道的八位输出
Gray = R*0.299 + G*0.587 + B*0.114
- 我采用的是将权值扩大1024倍之后再进行加权求和,最后再右移10位.
Gray = (R*306 + G*601 + B*117) >> 10
设计思路:
- 首先将输入的16位 的彩色图像,转化位24位的图像,即将RGB565 格式转化为 RGB888格式的图像,这里有两种方式的扩展位宽,第一种是采用带补偿的拓展位宽;第二种是采用不带补偿的拓展位宽。
//扩展 RGB565-->RGB888 always @(posedge clk or negedge rst_n)begin if(~rst_n)begin data_r <= 0; data_g <= 0; data_b <= 0; end else if(din_vld)begin data_r <= {din[15:11],din[13:11]}; //带补偿的 r5,r4,r3,r2,r1, r3,r2,r1 data_g <= {din[10:5],din[6:5]} ; //补偿低三位 data_b <= {din[4:0],din[2:0]} ; /* data_r <= {din[15:11],3'd0}; data_g <= {din[10:5],2'd0} ; data_b <= {din[4:0],3'd0} ; */ end end
转化完之后就可以进行加权求和
- 这里用到的是10位精度的扩大,人眼对绿色的敏感度最高,权重为 0.587,换成整数是10位的位宽。
//加权 //第一拍 always @(posedge clk or negedge rst_n)begin if(~rst_n)begin pixel_r <= 0; pixel_g <= 0; pixel_b <= 0; end else if(vld[0])begin pixel_r <= data_r * 306; pixel_g <= data_g * 601; pixel_b <= data_b * 117; end end //第二拍 always @(posedge clk or negedge rst_n)begin if(~rst_n)begin pixel <= 0; end else if(vld[1])begin pixel <= pixel_r + pixel_g + pixel_b; end end assign dout = pixel[10 +:8]; //从第十位开始取数据,取八位。
到这里就已经灰度转化完毕,之后进行高斯滤波。
接口设计:
module gauss_filter(
input clk ,
input rst_n ,
input din_sop ,
input din_eop ,
input din_vld ,
input [7:0] din ,//灰度输入
output dout_sop ,
output dout_eop ,
output dout_vld ,
output [7:0] dout //灰度输出
);
设计思路:
原理: 高斯滤波就是对整幅图像进行加权平均的过程,每一个像素点的值,都由其本身和邻域内的其他像素值经过加权平均后得到。
因为高斯滤波涉及到卷积核对二维图像的一个卷积,所以我们首先需要一个移位寄存器来将串行数据变为并行数据。这里调用一个IP核即可:
wire [7:0] taps0 ; wire [7:0] taps1 ; wire [7:0] taps2 ; //缓存3行数据 gs_line_buf gs_line_buf_inst ( .aclr (~rst_n ), .clken (din_vld ), .clock (clk ), /*input*/ .shiftin (din ), .shiftout ( ), /*output*/ .taps0x (taps0 ), .taps1x (taps1 ), .taps2x (taps2 ) );
卷积:
reg [7:0] line0_0 ; reg [7:0] line0_1 ; reg [7:0] line0_2 ; reg [7:0] line1_0 ; reg [7:0] line1_1 ; reg [7:0] line1_2 ; reg [7:0] line2_0 ; reg [7:0] line2_1 ; reg [7:0] line2_2 ; reg [9:0] sum_0 ;//第0行加权和 reg [9:0] sum_1 ;//第1行加权和 reg [9:0] sum_2 ;//第2行加权和 reg [7:0] sum ;//三行的加权和
- 首先是参数,lineX_Y表示第X第Y行,我们需要创造一个3*3的矩阵寄存器,所以这里共需要9个寄存器,并且需要求的加权和,所以每一行再分配一个sum,最后再分配一个总sum;
/* 高斯滤波系数,加权平均 1 2 1 2 4 2 1 2 1 */ always @(posedge clk or negedge rst_n)begin if(~rst_n)begin line0_0 <= 0;line0_1 <= 0;line0_2 <= 0; line1_0 <= 0;line1_1 <= 0;line1_2 <= 0; line2_0 <= 0;line2_1 <= 0;line2_2 <= 0; end else if(vld[0])begin line0_0 <= taps0;line0_1 <= line0_0;line0_2 <= line0_1; line1_0 <= taps1;line1_1 <= line1_0;line1_2 <= line1_1; line2_0 <= taps2;line2_1 <= line2_0;line2_2 <= line2_1; end end always @(posedge clk or negedge rst_n)begin if(~rst_n)begin sum_0 <= 0; sum_1 <= 0; sum_2 <= 0; end else if(vld[1])begin sum_0 <= {2'd0,line0_0} + {1'd0,line0_1,1'd0} + {2'd0,line0_2}; sum_1 <= {1'd0,line1_0,1'd0} + {line1_1,2'd0} + {1'd0,line1_2,1'd0}; sum_2 <= {2'd0,line2_0} + {1'd0,line2_1,1'd0} + {2'd0,line2_2}; end end always @(posedge clk or negedge rst_n)begin if(~rst_n)begin sum <= 0; end else if(vld[2])begin sum <= (sum_0 + sum_1 + sum_2)>>4; end end
- 第一个Always完成的是9个八位的寄存器对图像数据的存储;
- 第二个Always通过位拼接来实现与卷积核的乘法(直接乘也没问题,只是这样写能显示出老练的技法)与每行的求和;
- 第三个Always就是求总和,这里也和灰度寄存器一样使用了移位运算符,这也是因为Verilog不能进行小数运算,实际运算的卷积核内的参数都是扩大了16倍(2^4)。
克服了边界效应,相对于均值滤波平滑效果更柔和,边缘保留的更好。用一个值相邻像素的加权平均灰度值代替原来的值 。
因为此时图像已经是灰度单通道的(0,255),所以只需要设定一个阈值,当图像数据<它时,就设置为0(黑色),>大于它时就设置为1(白色),根据阈值的不同,最终得到的结果也不一样。
接口设计:
module gray2bin(
input clk ,
input rst_n ,
input din_sop ,
input din_eop ,
input din_vld ,
input [7:0] din ,//灰度输入
output dout_sop ,
output dout_eop ,
output dout_vld ,
output dout //二值输出
);
设计思路:
将经过高斯滤波后的灰度数据输入,进行和设定的 阈值 进行比较,阈值是我们自己设定的,如果大于这个阈值就是 1,即为白色,小于这个阈值为 0 ,即为黑色,
always @(posedge clk or negedge rst_n)begin if(~rst_n)begin binary <= 0 ; binary_sop <= 0 ; binary_eop <= 0 ; binary_vld <= 0 ; end else begin binary <= din>100 ;//二值化阈值可自定义 binary_sop <= din_sop ; binary_eop <= din_eop ; binary_vld <= din_vld ; end end
之后就可以进行Sobel边缘像素点检测。
接口设计:
module sobel(
input clk ,
input rst_n ,
input din ,//输入二值图像
input din_sop ,
input din_eop ,
input din_vld ,
output dout ,
output dout_sop,
output dout_eop,
output dout_vld
);
设计思路:
边缘检测是是特征提取中的一个研究领域,它能边缘检测出数字图像中亮度变化明显的点,减少数据量,并剔除不相 关的信息,最终保留图像重要的结构属性。同时,Sobel 边缘检测通常带有方向性,可以只检测竖直边缘或垂直边缘 或都检测。
Sobel算子提供了水平方向核垂直方向的滤波模板。
assign dout = g >= 3;//阈值假设为3 当某一个像素点的梯度值大于3,认为其是一个边缘点
之后要将输出数据扩展成16位的数据,再存到SDRAM里面。
这里SDRAM接口模块我是直接调用的IP核,写了一个控制模块,用来向SDRAM里卖弄突发写或者突发读,由于涉及到跨时钟域的数据传输,从摄像头采集到的数据写到SDRAM里面是慢时钟域到快时钟域,从SDRAM里面读取数据显示到屏幕上是快时钟域到慢时钟域。因此设计了两个异步FIFO用来缓存数据。
乒乓操作主要⽤于控制数据流,在此项⽬中主要体现为先写SDRAM bank1的数据,同时读SDRAM bank3的数据,当两块bank的数据读写完毕后,切换操作为读bank1的数据,写bank3的数据,这样可以保持数据为完整的⼀帧,使显⽰屏帧与帧之间切换瞬间完成。
保证读取到的数据是一帧的效果,如果一帧数据没有读完,就开始写入会丢帧。
设计思路:
- wfifo设计,慢时钟域写到快时钟域读
数据进行拼接,将eop,sop和 数据拼接到一起,用于判断能不能写入wrfifo wrfifo_inst ( .aclr (~rst_n ), .data (wfifo_data ), .rdclk (clk ), .rdreq (wfifo_rdreq), .wrclk (clk_in ), .wrreq (wfifo_wrreq), .q (wfifo_q ), .rdempty(wfifo_empty), .rdusedw(wfifo_usedw), .wrfull (wfifo_full ) ); assign wfifo_data = {din_eop,din_sop,din}; assign wfifo_wrreq = ~wfifo_full & din_vld & ((~wr_finish_r[1] & din_sop) ||wr_data_flag); assign wfifo_rdreq = state_c == WRITE && ~avs_waitrequest;
- 读写仲裁优先级
/************************读写优先级仲裁*****************************/ //rd_flag ;//读请求标志 always @(posedge clk or negedge rst_n)begin if(!rst_n)begin rd_flag <= 0; end else if(rfifo_usedw <= `RD_LT)begin rd_flag <= 1'b1; end else if(rfifo_usedw > `RD_UT)begin rd_flag <= 1'b0; end end //wr_flag ;//写请求标志 always @(posedge clk or negedge rst_n)begin if(!rst_n)begin wr_flag <= 0; end else if(wfifo_usedw >= `USER_BL)begin wr_flag <= 1'b1; end else begin wr_flag <= 1'b0; end end //flag_sel ;//标记上一次操作 always @(posedge clk or negedge rst_n)begin if(!rst_n)begin flag_sel <= 0; end else if(read2done)begin flag_sel <= 1; end else if(write2done)begin flag_sel <= 0; end end //prior_flag ;//优先级标志 0:写优先级高 1:读优先级高 仲裁读、写的优先级 always @(posedge clk or negedge rst_n)begin if(!rst_n)begin prior_flag <= 0; end else if(wr_flag && (flag_sel || (~flag_sel && ~rd_flag)))begin //突发写优先级高 prior_flag <= 1'b0; end else if(rd_flag && (~flag_sel || (flag_sel && ~wr_flag)))begin //突发读优先级高 prior_flag <= 1'b1; end end /******************************************************************/
读写不能同时进行,只有dq一组数据线
- 地址设计
// wr_addr rd_addr always @(posedge clk or negedge rst_n) begin if (rst_n==0) begin wr_addr <= 0; end else if(add_wr_addr) begin if(end_wr_addr) wr_addr <= 0; else wr_addr <= wr_addr+1 ; end end assign add_wr_addr = (state_c == WRITE) && ~avs_waitrequest; assign end_wr_addr = add_wr_addr && wr_addr == `BURST_MAX-1 ; always @(posedge clk or negedge rst_n) begin if (rst_n==0) begin rd_addr <= 0; end else if(add_rd_addr) begin if(end_rd_addr) rd_addr <= 0; else rd_addr <= rd_addr+1 ; end end assign add_rd_addr = (state_c == READ) && ~avs_waitrequest; assign end_rd_addr = add_rd_addr && rd_addr == `BURST_MAX-1;
地址设计是根据读写状态来改变的,突发写或突发读都是一次突发多少个数据。
乒乓操作,写第一块bank,读第三块bank,这样子能保证读出来的是连续的一帧图像
//change bank always @(posedge clk or negedge rst_n)begin if(~rst_n)begin wr_bank <= 2'b00; rd_bank <= 2'b11; end else if(change_bank)begin wr_bank <= ~wr_bank; rd_bank <= ~rd_bank; end end //wr_finish 一帧数据全部写到SDRAM always @(posedge clk or negedge rst_n)begin if(~rst_n)begin wr_finish <= 1'b0; end else if(~wr_finish & wfifo_q[17])begin //写完 从wrfifo读出eop wr_finish <= 1'b1; end else if(wr_finish && end_rd_addr)begin //读完 wr_finish <= 1'b0; end end //change_bank ;//切换bank always @(posedge clk or negedge rst_n)begin if(~rst_n)begin change_bank <= 1'b0; end else begin change_bank <= wr_finish && end_rd_addr; end end /*********************** wrfifo 写数据 ************************/ //控制像素数据帧 写入 或 丢帧 always @(posedge clk_in or negedge rst_n)begin if(~rst_n)begin wr_data_flag <= 1'b0; end else if(~wr_data_flag & ~wr_finish_r[1] & din_sop)begin//可以向wrfifo写数据 wr_data_flag <= 1'b1; end else if(/*wr_finish_r[1] && din_sop*/wr_data_flag & din_eop)begin//不可以向wrfifo写入数据 wr_data_flag <= 1'b0; end end always @(posedge clk_in or negedge rst_n)begin //把wr_finish从wrfifo的读侧同步到写侧 if(~rst_n)begin wr_finish_r <= 0; end else begin wr_finish_r <= {wr_finish_r[0],wr_finish}; end end /****************************************************************/
- rdfifo设计 ,快时钟写到慢时钟读
rdfifo u_rdfifo( .aclr (~rst_n ), .data (rfifo_data ), .rdclk (clk_out ), .rdreq (rfifo_rdreq), .wrclk (clk ), .wrreq (rfifo_wrreq), .q (rfifo_q ), .rdempty (rfifo_empty), .wrfull (rfifo_full ), .wrusedw (rfifo_usedw) ); assign rfifo_data = avs_rddata; assign rfifo_wrreq = ~rfifo_full & avs_rddata_vld; assign rfifo_rdreq = ~rfifo_empty & rdreq; //rfifo非空且vga显示是行有效和场有效
输出数据:
assign dout = rd_data;
assign dout_vld = rd_data_vld;
assign avm_wrdata = wfifo_q[15:0];
assign avm_write = ~(state_c == WRITE && ~avs_waitrequest);
assign avm_read = ~(state_c == READ && ~avs_waitrequest);
assign avm_addr = (state_c == WRITE)?{wr_bank[1],wr_addr[21:9],wr_bank[0],wr_addr[8:0]}
:((state_c == READ)?{rd_bank[1],rd_addr[21:9],rd_bank[0],rd_addr[8:0]}
:0);
接口设计:
module vga_interface ( //1280*720
input clk ,//75MHz
input rst_n ,
input [15:0] din ,
input din_vld ,
output rdy ,
output [15:0] vga_rgb ,
output vga_hsync,
output vga_vsync
);
设计思路:
- 计数器来设计行有效和场有效区域
- 在行场有效显示区域进行输出数据
//数据输出 always @(posedge clk or negedge rst_n) begin if (!rst_n) begin rgb <= 0; end else if (display_vld) begin rgb <= vga_data ; end else begin rgb <= rgb; end end assign display_vld = (cnt_h_addr > H_SYNC + H_BLACK - 1) && (cnt_h_addr < H_TOTAL - H_FRONT -1) && (add_v_addr > V_SYNC + V_BLACK - 1) && (add_v_addr < V_TOTAL - V_FRONT - 1); assign rgb_r = rgb[15:11]; assign rgb_g = rgb[10:05]; assign rgb_b = rgb[04:00];
用一个fifo来缓存数据
//FIFO例化 vga_buf u_buf( .aclr (~rst_n ), .clock (clk ), .data (din ), .rdreq (rdreq ), .wrreq (wrreq ), .empty (empty ), .full (full ), .q (q_out ), .usedw (usedw ) ); assign wrreq = ~full && din_vld; assign rdreq = ~empty && display_vld;
链接:https://pan.baidu.com/s/1scHW8ilQwdOKbUqN1aCuPg?pwd=gvj0
提取码:gvj0
--来自百度网盘超级会员V1的分享