一、简介
1.应用范围
边缘主要存在于图像中目标与目标之间,目标与背景之间,区域与区域之间。
边缘检测的目的就是找到图像中亮度变化剧烈的像素点构成的集合,表现出来往往是轮廓。如果图像中边缘能够精确的测量和定位,那么,就意味着实际的物体能够被定位和测量,包括物体的面积,物体的直径,物体的形状等就能被测量。
基于此,边缘检测技术在许多场景下被应用,如:车牌检测与提取,物体识别等。
2.边缘检测背景介绍
数字图像处理是指将图像信号转换成数字信号并利用计算机对其进行处理的过程。图像处理最早出现于20世纪50年代,当时的电了计算机已经发展到一定水平,人们开始利用计算机来处理图形和图像信息。数字图像处理作为一门学科人约形成于20世纪60年代初期。早期的图像处理的目的是改善图像的质量,它以人为对象,以改善人的视觉效果为日的。图像处理中,输入的是质量低的图像,输出的是改善质量后的图像,常用的图像处理方法有图像增强,复原,编码,压缩等。
边缘检测是图像处理和计算机视觉中的基本问题,边缘 检测的目的是标识数字图像中亮度变化明显的点。图像边缘检测大幅度地减少了数据量,并且剔除了可以认为不相关的信息,保留了图像重要的结构属性。
边缘检测的实质是采用某种算法来提取出图像中对象与背景问的交界线。我们将边缘定义为图像中灰度发生急刷变化的区域边界。图像灰度的变化情况可以川图像灰度分布的梯度来反唤,因此我们可以用局部图像微分技术来获得边缘检测算子。经典的边缘检测方法,是通过对原始图像中像素的某小邻域构造边缘检测算子来达到检测边缘这一目的。
3.工程实践
3.1需求分析
3.2 系统模块说明
3.3系统架构
|----------|-------------------------|
| 模块名称 | 模块功能 |
| 摄像头驱动 | 初始化配置ov5640摄像头 |
| 摄像头数据采集 | 采集ov5640摄像头输出的图像数据 |
| 图像处理单元 | 实现图像处理功能--灰度化,二值化,边缘检测等 |
| sdram控制器 | 视频数据缓存 |
| vga显示驱动 | vga显示器驱动时序实现 |
4、摄像头简介
略
二、程序设计
1.摄像头配置
1.1 摄像头配置原理
本次设计使用的摄像头是OV5640,摄像头配置的详细原理,请参考:OV5640手册解读
1.2 程序设计
1.2.1 接口模块程序设计
本次设计中,接口模块采用的是IIC协议。因为IIC向下兼容SCCB协议,只是在写时序时,SCCB第九位传输的是Don't care,而IIC传输的是ACK响应,故IIC接口模块直接拿来使用时,要将写时序的ACK响应废除。
module i2c_intf (
input clk ,
input rst_n ,
input [3:0] cmd ,
input req ,
input [7:0] wr_data ,
output [7:0] dout ,
output done ,
output reg m_scl ,
inout m_sda
);
parameter CMD_START = 4'b0001,
CMD_WITER = 4'b0010,
CMD_READ = 4'b0100,
CMD_STOP = 4'b1000;
parameter SCL_MAX = 50_000_000 / 100_000 ,// 500分频-->100k的时钟频率
SCL_LOW = 125,//低电平的中间时刻,发送 1/4
SCL_HIGHT = 375;//高电平的中间时刻,采样 3/4
//wr_data
reg [7:0] wr_data_r;
reg [7:0] rd_data ;
//cmd寄存
reg [3:0] cmd_r;
//ack响应
reg rx_ack;
//scl计数器
reg [8:0] cnt_scl ;
wire add_cnt_scl ;
wire end_cnt_scl ;
//bit计数器
reg [3:0] cnt_bit ;
wire add_cnt_bit ;
wire end_cnt_bit ;
reg [3:0] bit_max ;//bit最大计数复用
//状态转移条件
reg [3:0] state_c;
reg [3:0] state_n;
wire idle2start ;
wire idle2witer ;
wire idle2read ;
wire start2witer ;
wire witer2rack ;
wire rack2idle ;
wire rack2stop ;
wire read2sack ;
wire sack2idle ;
wire sack2stop ;
wire stop2idle ;
//三态门
reg m_sda_en ; // 设置SDA模式,1位输出,0为输入
reg m_sda_out ; // SDA寄存器
wire m_sda_in;
/**************************************************************
状态机
**************************************************************/
parameter IDLE = 0,
START = 1,
WITER = 2,
RACK = 3,
READ = 4,
SACK = 5,
STOP = 6;
//第一段状态机
always @(posedge clk or negedge rst_n)begin
if(!rst_n)begin
state_c <= IDLE;
end
else begin
state_c <= state_n;
end
end
//第二段状态机
always @(*)begin
case(state_c)
IDLE : if(idle2start)
state_n = START;
else if(idle2witer)
state_n = WITER;
else if(idle2read)
state_n = READ;
else
state_n = state_c;
START : if(start2witer)
state_n = WITER;
else
state_n = state_c;
WITER : if(witer2rack)
state_n = RACK;
else
state_n = state_c;
RACK : if(rack2idle)
state_n = IDLE;
else if(rack2stop)
state_n = STOP;
else
state_n = state_c;
READ : if(read2sack)
state_n = SACK;
else
state_n = state_c;
SACK : if(sack2idle)
state_n = IDLE;
else if(sack2stop)
state_n = STOP;
else
state_n = state_c;
STOP : if(stop2idle)
state_n = IDLE;
else
state_n = state_c;
default : state_n = state_c;
endcase
end
//状态跳转条件
assign idle2start = state_c == IDLE && req && (cmd & CMD_START) ;
assign idle2witer = state_c == IDLE && req && (cmd & CMD_WITER) ;
assign idle2read = state_c == IDLE && req && (cmd & CMD_READ ) ;
assign start2witer = state_c == START && end_cnt_bit ;
assign witer2rack = state_c == WITER && end_cnt_bit ;
assign rack2idle = state_c == RACK && end_cnt_bit && (!(cmd_r & CMD_STOP));
assign rack2stop = state_c == RACK && end_cnt_bit && ((cmd_r & CMD_STOP) /* || rx_ack */);
assign read2sack = state_c == READ && end_cnt_bit ;
assign sack2idle = state_c == SACK && end_cnt_bit && (!(cmd_r & CMD_STOP));
assign sack2stop = state_c == SACK && end_cnt_bit && (cmd_r & CMD_STOP);
assign stop2idle = state_c == STOP && end_cnt_bit ;
/**************************************************************
时序约束
**************************************************************/
//cmd寄存
always@(posedge clk or negedge rst_n)
if(!rst_n)
cmd_r <= 4'b0000;
else if(req)
cmd_r <= cmd;
//接收从机回应的ack
always @(posedge clk or negedge rst_n)begin
if(!rst_n)begin
rx_ack <= 1'b1;
end
else if(state_c == RACK && cnt_scl == SCL_HIGHT)begin
rx_ack <= m_sda_in;
end
end
//写入的数据
always @(posedge clk or negedge rst_n)begin
if(!rst_n)begin
wr_data_r <= 0;
end
else if(req)begin
wr_data_r <= wr_data;
end
end
//接收读取的数据
//rd_data 接收读入的数据
always @(posedge clk or negedge rst_n)begin
if(~rst_n)begin
rd_data <= 0;
end
else if(state_c == READ && cnt_scl == SCL_HIGHT)begin
rd_data[7-cnt_bit] <= m_sda_in; //将接收到的SDA信号串并转换发送到eeprom_rw模块
end
end
/**************************************************************
双向端口m_sda的使用方式
**************************************************************/
assign m_sda_in = m_sda; //高阻态的话,则把总线上的数据赋给m_sda_in
assign m_sda = m_sda_en ? m_sda_out : 1'bz;//使能1则输出,0则高阻态
//m_sda_en
always@(posedge clk or negedge rst_n)
if(!rst_n)
m_sda_en <= 1'b0;
else if(state_c == READ | state_c == RACK)
m_sda_en <= 1'b0;
else
m_sda_en <= 1'b1;
//m_sda_out
always @(posedge clk or negedge rst_n)begin
if(!rst_n)begin
m_sda_out <= 1;
end
else begin
case (state_c)
START : if(cnt_scl == SCL_LOW)
m_sda_out <= 1'b1;
else if(cnt_scl == SCL_HIGHT)
m_sda_out <= 1'b0;
WITER : if(cnt_scl == SCL_LOW)
m_sda_out <= wr_data_r[7-cnt_bit];//MSB
STOP : if(cnt_scl == SCL_LOW)
m_sda_out <= 1'b0;
else if(cnt_scl == SCL_HIGHT)
m_sda_out <= 1'b1;
SACK : if(cnt_scl == SCL_LOW)
m_sda_out <= (cmd & CMD_STOP)?1'b1:1'b0;
default: m_sda_out <= 1'bz;
endcase
end
end
/**************************************************************
系统时钟降频模块
**************************************************************/
//cnt_scl
always @(posedge clk or negedge rst_n)begin
if(!rst_n)begin
cnt_scl <= 0;
end
else if(add_cnt_scl)begin
if(end_cnt_scl)begin
cnt_scl <= 0;
end
else begin
cnt_scl <= cnt_scl + 1;
end
end
else begin
cnt_scl <= cnt_scl;
end
end
assign add_cnt_scl = state_c != IDLE ;
assign end_cnt_scl = add_cnt_scl && cnt_scl == SCL_MAX - 1;
//m_scl
always @(posedge clk or negedge rst_n)begin
if(!rst_n)begin
m_scl <= 1'b1;
end
else if(cnt_scl <= (SCL_MAX>>1))begin
m_scl <= 1'b0;
end
else begin
m_scl <= 1'b1;
end
end
/**************************************************************
bit计数器
**************************************************************/
always @(posedge clk or negedge rst_n)begin
if(!rst_n)begin
cnt_bit <= 0;
end
else if(add_cnt_bit)begin
if(end_cnt_bit)begin
cnt_bit <= 0;
end
else begin
cnt_bit <= cnt_bit + 1'b1;
end
end
else begin
cnt_bit <= cnt_bit;
end
end
assign add_cnt_bit = end_cnt_scl ;
assign end_cnt_bit = add_cnt_bit && cnt_bit == bit_max - 1;
//bit_max
always@(*)
if(state_c == WITER || state_c == READ)
bit_max = 8;
else
bit_max = 1;
/**************************************************************
输出信号
**************************************************************/
assign dout = rd_data;
assign done = rack2idle | sack2idle | stop2idle;
endmodule
1.2.2 OV5640配置模块程序设计
配置流程:
主要采用状态机加计数器的方式来设计配置模块;
当上电之后计数20ms,之后就可以进行摄像头的配置,有一个配置完成信号,当配置完254(测试模式254,实际显示模式252)个寄存器后,配置信号有效。
配置模块主要就是通过IIC_master模块向摄像头里面写入数据,完成配置。
发送数据是以任务的方式发请求、命令和数据
注意: OV5640的寄存器地址是 16 位的,加上数据,sccb_data 的值为 24 位 ,写数据传输时要传输4个字节:1字节写命令+2字节地址+1字节数据。
`include "param.v"
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
);
//定义参数
localparam WAIT = 4'b0001,//上电等待20ms
IDLE = 4'b0010,
WREQ = 4'b0100,//发写请求
WRITE = 4'b1000;//等待一个字节写完
parameter DELAY = 1000_000;//上电延时20ms开始配置
//信号定义
reg [3:0] state_c ;
reg [3:0] state_n ;
reg [19:0] cnt0 ;
wire add_cnt0/* synthesis syn_keep*/ ;
wire end_cnt0/* synthesis syn_keep*/ ;
reg [1:0] cnt1 ;
wire add_cnt1/* synthesis syn_keep*/ ;
wire end_cnt1/* synthesis syn_keep*/ ;
reg config_flag ; //1:表示在配置摄像头 0:表示配置完成
reg [23:0] lut_data ;
reg tran_req ; //发送请求命令和数据
reg [3:0] tran_cmd ;
reg [7:0] tran_dout ;
wire wait2idle ; //状态转移条件
wire idle2wreq ;
wire write2wreq ;
wire write2idle ;
//状态机
always @(posedge clk or negedge rst_n)begin
if(~rst_n)begin
state_c <= WAIT;
end
else begin
state_c <= state_n;
end
end
always @(*)begin
case(state_c)
WAIT :begin
if(wait2idle)
state_n = IDLE;
else
state_n = state_c;
end
IDLE :begin
if(idle2wreq)
state_n = WREQ;
else
state_n = state_c;
end
WREQ :state_n = WRITE;
WRITE :begin
if(write2wreq)
state_n = WREQ;
else if(write2idle)
state_n = IDLE;
else
state_n = state_c;
end
default:state_n = IDLE;
endcase
end
assign wait2idle = state_c == WAIT && end_cnt0;
assign idle2wreq = state_c == IDLE && config_flag;
assign write2wreq = state_c == WRITE && done && ~end_cnt1;
assign write2idle = state_c == WRITE && end_cnt1;
//计数器
always @(posedge clk or negedge rst_n)begin
if(!rst_n)begin
cnt0 <= 0;
end
else if(add_cnt0)begin
if(end_cnt0)
cnt0 <= 0;
else
cnt0 <= cnt0 + 1;
end
end
assign add_cnt0 = state_c == WAIT || state_c == WRITE && end_cnt1;
assign end_cnt0 = add_cnt0 && cnt0 == ((state_c == WAIT)?(DELAY-1):(`REG_NUM-1));
always @(posedge clk or negedge rst_n)begin
if(!rst_n)begin
cnt1 <= 0;
end
else if(add_cnt1)begin
if(end_cnt1)
cnt1 <= 0;
else
cnt1 <= cnt1 + 1;
end
end
assign add_cnt1 = state_c == WRITE && done;
assign end_cnt1 = add_cnt1 && cnt1 == 4-1;
//config_flag
always @(posedge clk or negedge rst_n)begin
if(~rst_n)begin
config_flag <= 1'b1;
end
else if(config_flag & end_cnt0 & state_c != WAIT)begin //所有寄存器配置完,flag拉低
config_flag <= 1'b0;
end
end
//输出寄存器
always @(posedge clk or negedge rst_n)begin
if(~rst_n)begin
tran_req <= 0;
tran_cmd <= 0;
tran_dout <= 0;
end
else if(state_c == WREQ)begin
case(cnt1)
0:begin
tran_req <= 1;
tran_cmd <= {`CMD_START | `CMD_WRITE};
tran_dout <= `WR_ID;
end
1:begin
tran_req <= 1;
tran_cmd <= `CMD_WRITE;
tran_dout <= lut_data[23:16];
end
2:begin
tran_req <= 1;
tran_cmd <= `CMD_WRITE;
tran_dout <= lut_data[15:8];
end
3:begin
tran_req <= 1;
tran_cmd <= {`CMD_STOP | `CMD_WRITE};
tran_dout <= lut_data[7:0];
end
default:tran_req <= 0;
endcase
end
else begin
tran_req <= 0;
tran_cmd <= 0;
tran_dout <= 0;
end
end
//输出
assign config_done = ~config_flag; //配置好为0,否则就是在配置中
assign req = tran_req;
assign cmd = tran_cmd;
assign dout = tran_dout;
//lut_data
always@(*)begin
case(cnt0)
//15fps VGA YUV output
// 24MHz input clock, 84MHz PCLK
0 :lut_data = {24'h3103_11}; // system clock from pad, bit[1]
1 :lut_data = {24'h3008_82}; // software reset, bit[7]
2 :lut_data = {24'h3008_42}; // software power down, bit[6]
3 :lut_data = {24'h3103_03}; // system clock from PLL, bit[1]
4 :lut_data = {24'h3017_ff}; // FREX, Vsync, HREF, PCLK, D[9:6] output enable
5 :lut_data = {24'h3018_ff}; // D[5:0], GPIO[1:0] output enable
6 :lut_data = {24'h3034_1a}; // MIPI 10-bit
7 :lut_data = {24'h3037_13}; // PLL root divider, bit[4], PLL pre-divider, bit[3:0]
8 :lut_data = {24'h3108_01}; // PCLK root divider, bit[5:4], SCLK2x root divider, bit[3:2]
9 :lut_data = {24'h3630_36};//SCLK root divider, bit[1:0]
10 :lut_data = {24'h3631_0e};
11 :lut_data = {24'h3632_e2};
12 :lut_data = {24'h3633_12};
13 :lut_data = {24'h3621_e0};
14 :lut_data = {24'h3704_a0};
15 :lut_data = {24'h3703_5a};
16 :lut_data = {24'h3715_78};
17 :lut_data = {24'h3717_01};
18 :lut_data = {24'h370b_60};
19 :lut_data = {24'h3705_1a};
20 :lut_data = {24'h3905_02};
21 :lut_data = {24'h3906_10};
22 :lut_data = {24'h3901_0a};
23 :lut_data = {24'h3731_12};
24 :lut_data = {24'h3600_08}; // VCM control
25 :lut_data = {24'h3601_33}; // VCM control
26 :lut_data = {24'h302d_60}; // system control
27 :lut_data = {24'h3620_52};
28 :lut_data = {24'h371b_20};
29 :lut_data = {24'h471c_50};
30 :lut_data = {24'h3a13_43}; // pre-gain = 1.047x
31 :lut_data = {24'h3a18_00}; // gain ceiling
32 :lut_data = {24'h3a19_f8}; // gain ceiling = 15.5x
33 :lut_data = {24'h3635_13};
34 :lut_data = {24'h3636_03};
35 :lut_data = {24'h3634_40};
36 :lut_data = {24'h3622_01};
// 50/60Hz detection 50/60Hz 灯光条纹过滤
37 :lut_data = {24'h3c01_34}; // Band auto, bit[7]
38 :lut_data = {24'h3c04_28}; // threshold low sum
39 :lut_data = {24'h3c05_98}; // threshold high sum
40 :lut_data = {24'h3c06_00}; // light meter 1 threshold[15:8]
41 :lut_data = {24'h3c07_08}; // light meter 1 threshold[7:0]
42 :lut_data = {24'h3c08_00}; // light meter 2 threshold[15:8]
43 :lut_data = {24'h3c09_1c}; // light meter 2 threshold[7:0]
44 :lut_data = {24'h3c0a_9c}; // sample number[15:8]
45 :lut_data = {24'h3c0b_40}; // sample number[7:0]
46 :lut_data = {24'h3810_00}; // Timing Hoffset[11:8]
47 :lut_data = {24'h3811_10}; // Timing Hoffset[7:0]
48 :lut_data = {24'h3812_00}; // Timing Voffset[10:8]
49 :lut_data = {24'h3708_64};
50 :lut_data = {24'h4001_02}; // BLC start from line 2
51 :lut_data = {24'h4005_1a}; // BLC always update
52 :lut_data = {24'h3000_00}; // enable blocks
53 :lut_data = {24'h3004_ff}; // enable clocks
54 :lut_data = {24'h300e_58}; //MIPI power down,DVP enable
55 :lut_data = {24'h302e_00};
56 :lut_data = {24'h4300_61}; // RGB,
57 :lut_data = {24'h501f_01}; // ISP RGB
58 :lut_data = {24'h440e_00};
59 :lut_data = {24'h5000_a7}; // Lenc on, raw gamma on, BPC on, WPC on, CIP on
// AEC target 自动曝光控制
60 :lut_data = {24'h3a0f_30}; // stable range in high
61 :lut_data = {24'h3a10_28}; // stable range in low
62 :lut_data = {24'h3a1b_30}; // stable range out high
63 :lut_data = {24'h3a1e_26}; // stable range out low
64 :lut_data = {24'h3a11_60}; // fast zone high
65 :lut_data = {24'h3a1f_14}; // fast zone low
// Lens correction for ? 镜头补偿
66 :lut_data = {24'h5800_23};
67 :lut_data = {24'h5801_14};
68 :lut_data = {24'h5802_0f};
69 :lut_data = {24'h5803_0f};
70 :lut_data = {24'h5804_12};
71 :lut_data = {24'h5805_26};
72 :lut_data = {24'h5806_0c};
73 :lut_data = {24'h5807_08};
74 :lut_data = {24'h5808_05};
75 :lut_data = {24'h5809_05};
76 :lut_data = {24'h580a_08};
77 :lut_data = {24'h580b_0d};
78 :lut_data = {24'h580c_08};
79 :lut_data = {24'h580d_03};
80 :lut_data = {24'h580e_00};
81 :lut_data = {24'h580f_00};
82 :lut_data = {24'h5810_03};
83 :lut_data = {24'h5811_09};
84 :lut_data = {24'h5812_07};
85 :lut_data = {24'h5813_03};
86 :lut_data = {24'h5814_00};
87 :lut_data = {24'h5815_01};
88 :lut_data = {24'h5816_03};
89 :lut_data = {24'h5817_08};
90 :lut_data = {24'h5818_0d};
91 :lut_data = {24'h5819_08};
92 :lut_data = {24'h581a_05};
93 :lut_data = {24'h581b_06};
94 :lut_data = {24'h581c_08};
95 :lut_data = {24'h581d_0e};
96 :lut_data = {24'h581e_29};
97 :lut_data = {24'h581f_17};
98 :lut_data = {24'h5820_11};
99 :lut_data = {24'h5821_11};
100:lut_data = {24'h5822_15};
101:lut_data = {24'h5823_28};
102:lut_data = {24'h5824_46};
103:lut_data = {24'h5825_26};
104:lut_data = {24'h5826_08};
105:lut_data = {24'h5827_26};
106:lut_data = {24'h5828_64};
107:lut_data = {24'h5829_26};
108:lut_data = {24'h582a_24};
109:lut_data = {24'h582b_22};
110:lut_data = {24'h582c_24};
111:lut_data = {24'h582d_24};
112:lut_data = {24'h582e_06};
113:lut_data = {24'h582f_22};
114:lut_data = {24'h5830_40};
115:lut_data = {24'h5831_42};
116:lut_data = {24'h5832_24};
117:lut_data = {24'h5833_26};
118:lut_data = {24'h5834_24};
119:lut_data = {24'h5835_22};
120:lut_data = {24'h5836_22};
121:lut_data = {24'h5837_26};
122:lut_data = {24'h5838_44};
123:lut_data = {24'h5839_24};
124:lut_data = {24'h583a_26};
125:lut_data = {24'h583b_28};
126:lut_data = {24'h583c_42};
127:lut_data = {24'h583d_ce}; // lenc BR offset
// AWB 自动白平衡
128:lut_data = {24'h5180_ff}; // AWB B block
129:lut_data = {24'h5181_f2}; // AWB control
130:lut_data = {24'h5182_00}; // [7:4] max local counter, [3:0] max fast counter
131:lut_data = {24'h5183_14}; // AWB advanced
132:lut_data = {24'h5184_25};
133:lut_data = {24'h5185_24};
134:lut_data = {24'h5186_09};
135:lut_data = {24'h5187_09};
136:lut_data = {24'h5188_09};
137:lut_data = {24'h5189_75};
138:lut_data = {24'h518a_54};
139:lut_data = {24'h518b_e0};
140:lut_data = {24'h518c_b2};
141:lut_data = {24'h518d_42};
142:lut_data = {24'h518e_3d};
143:lut_data = {24'h518f_56};
144:lut_data = {24'h5190_46};
145:lut_data = {24'h5191_f8}; // AWB top limit
146:lut_data = {24'h5192_04}; // AWB bottom limit
147:lut_data = {24'h5193_70}; // red limit
148:lut_data = {24'h5194_f0}; // green limit
149:lut_data = {24'h5195_f0}; // blue limit
150:lut_data = {24'h5196_03}; // AWB control
151:lut_data = {24'h5197_01}; // local limit
152:lut_data = {24'h5198_04};
153:lut_data = {24'h5199_12};
154:lut_data = {24'h519a_04};
155:lut_data = {24'h519b_00};
156:lut_data = {24'h519c_06};
157:lut_data = {24'h519d_82};
158:lut_data = {24'h519e_38}; // AWB control
// Gamma 伽玛曲线
159:lut_data = {24'h5480_01}; //Gamma bias plus on, bit[0]
160:lut_data = {24'h5481_08};
161:lut_data = {24'h5482_14};
162:lut_data = {24'h5483_28};
163:lut_data = {24'h5484_51};
164:lut_data = {24'h5485_65};
165:lut_data = {24'h5486_71};
166:lut_data = {24'h5487_7d};
167:lut_data = {24'h5488_87};
168:lut_data = {24'h5489_91};
169:lut_data = {24'h548a_9a};
170:lut_data = {24'h548b_aa};
171:lut_data = {24'h548c_b8};
172:lut_data = {24'h548d_cd};
173:lut_data = {24'h548e_dd};
174:lut_data = {24'h548f_ea};
175:lut_data = {24'h5490_1d};
// color matrix 色彩矩阵
176:lut_data = {24'h5381_1e}; // CMX1 for Y
177:lut_data = {24'h5382_5b}; // CMX2 for Y
178:lut_data = {24'h5383_08}; // CMX3 for Y
179:lut_data = {24'h5384_0a}; // CMX4 for U
180:lut_data = {24'h5385_7e}; // CMX5 for U
181:lut_data = {24'h5386_88}; // CMX6 for U
182:lut_data = {24'h5387_7c}; // CMX7 for V
183:lut_data = {24'h5388_6c}; // CMX8 for V
184:lut_data = {24'h5389_10}; // CMX9 for V
185:lut_data = {24'h538a_01}; // sign[9]
186:lut_data = {24'h538b_98}; // sign[8:1]
// UV adjust UV 色彩饱和度调整
187:lut_data = {24'h5580_06}; // saturation on, bit[1]
188:lut_data = {24'h5583_40};
189:lut_data = {24'h5584_10};
190:lut_data = {24'h5589_10};
191:lut_data = {24'h558a_00};
192:lut_data = {24'h558b_f8};
193:lut_data = {24'h501d_40}; // enable manual offset of contrast
// CIP 锐化和降噪
194:lut_data = {24'h5300_08}; //CIP sharpen MT threshold 1
195:lut_data = {24'h5301_30}; //CIP sharpen MT threshold 2
196:lut_data = {24'h5302_10}; // CIP sharpen MT offset 1
197:lut_data = {24'h5303_00}; // CIP sharpen MT offset 2
198:lut_data = {24'h5304_08}; // CIP DNS threshold 1
199:lut_data = {24'h5305_30}; // CIP DNS threshold 2
200:lut_data = {24'h5306_08}; // CIP DNS offset 1
201:lut_data = {24'h5307_16}; // CIP DNS offset 2
202:lut_data = {24'h5309_08}; //CIP sharpen TH threshold 1
203:lut_data = {24'h530a_30}; //CIP sharpen TH threshold 2
204:lut_data = {24'h530b_04}; //CIP sharpen TH offset 1
205:lut_data = {24'h530c_06}; //CIP sharpen TH offset 2
206:lut_data = {24'h5025_00};
207:lut_data = {24'h3008_02}; //wake up from standby,bit[6]
// input clock 24Mhz, PCLK 84Mhz
208:lut_data = {24'h3035_21}; // PLL
209:lut_data = {24'h3036_69}; // PLL
210:lut_data = {24'h3c07_07}; // lightmeter 1 threshold[7:0]
211:lut_data = {24'h3820_47}; // flip
212:lut_data = {24'h3821_01}; // no mirror
213:lut_data = {24'h3814_31}; // timing X inc
214:lut_data = {24'h3815_31}; // timing Y inc
215:lut_data = {24'h3800_00}; // HS
216:lut_data = {24'h3801_00}; // HS
217:lut_data = {24'h3802_00}; // VS
218:lut_data = {24'h3803_fa}; // VS
219:lut_data = {24'h3804_0a}; // HW :
220:lut_data = {24'h3805_3f}; // HW :
221:lut_data = {24'h3806_06}; // VH :
222:lut_data = {24'h3807_a9}; // VH :
223:lut_data = {24'h3808_05}; // DVPHO 1280
224:lut_data = {24'h3809_00}; // DVPHO
225:lut_data = {24'h380a_02}; // DVPVO 720
226:lut_data = {24'h380b_d0}; // DVPVO
227:lut_data = {24'h380c_07}; // HTS
228:lut_data = {24'h380d_64}; // HTS
229:lut_data = {24'h380e_02}; // VTS
230:lut_data = {24'h380f_e4}; // VTS
231:lut_data = {24'h3813_04}; // timing V offset
232:lut_data = {24'h3618_00};
233:lut_data = {24'h3612_29};
234:lut_data = {24'h3709_52};
235:lut_data = {24'h370c_03};
236:lut_data = {24'h3a02_02}; // 60Hz max exposure
237:lut_data = {24'h3a03_e0}; // 60Hz max exposure
238:lut_data = {24'h3a14_02}; // 50Hz max exposure
239:lut_data = {24'h3a15_e0}; // 50Hz max exposure
240:lut_data = {24'h4004_02}; // BLC line number
241:lut_data = {24'h3002_1c}; // reset JFIFO, SFIFO, JPG
242:lut_data = {24'h3006_c3}; // disable clock of JPEG2x, JPEG
243:lut_data = {24'h4713_03}; // JPEG mode 3
244:lut_data = {24'h4407_04}; // Quantization scale
245:lut_data = {24'h460b_37};
246:lut_data = {24'h460c_20};
247:lut_data = {24'h4837_16}; // MIPI global timing
248:lut_data = {24'h3824_04}; // PCLK manual divider
249:lut_data = {24'h5001_83}; // SDE on, CMX on, AWB on
250:lut_data = {24'h3503_00}; // AEC/AGC on
251:lut_data = {24'h4740_20}; // VS 1
252:lut_data = {24'h503d_80}; // color bar
253:lut_data = {24'h4741_00}; //
default:lut_data = 0;
endcase
end
endmodule
2.图像数据采集
2.1 图像数据采集模块原理
图像数据采集模块,参考的是OV5640手册中的DVP时序部分
2.2 图像数据采集模块程序设计
1)先对场同步信号进行同步打拍,然后检测下降沿
2)检测到下降沿,且接收到摄像头配置完成信号,采集数据标志拉高(开始采集图像数据),采集完一帧图像,标志拉低。之后进行下一帧图像的采集。
3)改变输出的图像分辨率,两种方法:一是配置寄存器,二是用简单计数器裁剪分辨率。
本次设计采用的是用计数器进行的简单分辨率裁剪。(行、场信号有效时,数据计数;其余无效数据丢弃)。
采集数据:采集数据标志拉高且行参考信号有效时,进行数据采集.
4)数据拼接:摄像头的数据是把16位RGB拆分为高八位和低八位发送的,我们需要通过移位+位拼接的方式把两个8bit数据合并成16bit数据输出。
`include "param.v"
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 //像素数据有效
);
//信号定义
reg [11:0] cnt_h ;
wire add_cnt_h ;
wire end_cnt_h ;
reg [9:0] cnt_v ;
wire add_cnt_v ;
wire end_cnt_v ;
reg [1:0] vsync_r ;//同步打拍
wire vsync_nedge ;//下降沿
reg flag ;//串并转换标志
reg [15:0] data ;
reg data_vld ;
reg data_sop ;
reg data_eop ;
//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]; //检测下降沿
always @(posedge clk or negedge rst_n)begin
if(~rst_n)begin
flag <= 1'b0;
end
else if(enable & vsync_nedge)begin //摄像头配置完成且场同步信号拉低之后开始采集有效数据
flag <= 1'b1;
end
else if(end_cnt_v)begin //一帧数据采集完拉低
flag <= 1'b0;
end
end
//计数器
always @(posedge clk or negedge rst_n) begin
if (rst_n==0) begin
cnt_h <= 0;
end
else if(add_cnt_h) begin
if(end_cnt_h)
cnt_h <= 0;
else
cnt_h <= cnt_h+1 ;
end
end
assign add_cnt_h = flag & href; //摄像头配置完成且场同步信号拉低且行参考信号有效
assign end_cnt_h = add_cnt_h && cnt_h == (`H_AP << 1)-1;
always @(posedge clk or negedge rst_n) begin
if (rst_n==0) begin
cnt_v <= 0;
end
else if(add_cnt_v) begin
if(end_cnt_v)
cnt_v <= 0;
else
cnt_v <= cnt_v+1 ;
end
end
assign add_cnt_v = end_cnt_h;
assign end_cnt_v = add_cnt_v && cnt_v == `V_AP-1 ;
//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
//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
assign dout = data;
assign dout_sop = data_sop;
assign dout_eop = data_eop;
assign dout_vld = data_vld;
endmodule
3.图像灰度转化
3.1 灰度转化算法
对于彩色转灰度,有一个很著名的心理学公式:Gray = R0.299 + G0.587 + B*0.114;
RGB888 转 Ycbcr 算法:
因为FPGA无法进行浮点运算,所以我们采取将整个式子右端先都扩大256倍,然后再右移8位,这样就得到了FPGA擅长的乘法运算和加法运算了。
Y = ((77*R+150*G+29*B)>>8);
Cb = ((-43*R - 85*G + 128*B)>>8) + 128;
Cr = ((128*R - 107*G - 21*B)>>8) + 128;
3.2 灰度化代码设计
module rgb2gary (
input clk ,
input rst_n ,
input [15:0] din , //rgb565
input din_sop ,
input din_eop ,
input din_vld ,
output [7:0] dout , //灰度输出
output dout_sop,
output dout_eop,
output dout_vld
);
reg [7:0] RGB_R;
reg [7:0] RGB_G;
reg [7:0] RGB_B;
reg [15:0] RGB_R_mult;
reg [15:0] RGB_G_mult;
reg [15:0] RGB_B_mult;
reg [16:0] out_gary;
reg [1:0] vld ;
reg [1:0] sop ;
reg [1:0] eop ;
/**************************************************************
RGB564 -> RGB88
**************************************************************/
always@(posedge clk or negedge rst_n)
if(!rst_n)begin
RGB_R <= 8'b0;
RGB_G <= 8'b0;
RGB_B <= 8'b0;
end
else if(din_vld)begin
RGB_R <= {din[15:11],din[13:11]}; //r5-r1,r3-r1;低位补偿3位
RGB_G <= {din[10:5 ],din[ 6:5 ]};
RGB_B <= {din[ 4:0 ],din[ 2:0 ]};
end
/**************************************************************
rgb -> gary
**************************************************************/
always@(posedge clk or negedge rst_n)
if(!rst_n) begin
RGB_R_mult <= 16'b0;
RGB_G_mult <= 16'b0;
RGB_B_mult <= 16'b0;
end
else if(vld[0]) begin
RGB_R_mult <= RGB_R * 76;
RGB_G_mult <= RGB_G * 150;
RGB_B_mult <= RGB_B * 29;
end
//
always@(posedge clk or negedge rst_n)
if(!rst_n)
out_gary <= 17'b0;
else if(vld[1])
out_gary <= RGB_R_mult + RGB_G_mult + RGB_B_mult;
always @(posedge clk or negedge rst_n)begin
if(~rst_n)begin
sop <= 0;
eop <= 0;
vld <= 0;
end
else begin
sop <= {sop[0],din_sop};
eop <= {eop[0],din_eop};
vld <= {vld[0],din_vld};
end
end
//输出
assign dout = out_gary[16:9]; //取平均
assign dout_sop = sop[1];
assign dout_eop = eop[1];
assign dout_vld = vld[1];
endmodule
4.高斯滤波
4.1 高斯滤波原理
高斯滤波是一种线性平滑滤波,适用于消除高斯噪声,广泛应用于图像处理的减噪过程。通俗的讲,高斯滤波就是对整幅图像进行加权平均的过程,每一个像素点的值,都由其本身和邻域内的其他像素值经过加权平均后得到。高斯滤波的具体操作是:用一个模板(或称卷积、掩模)扫描图像中的每一个像素,用模板确定的邻域内像素的加权平均灰度值去替代模板中心像素点的值。
高斯滤波后图像被平滑的程度取决于标准差。它的输出是临域像素的加权平均,同时离中心越近的像素权重越高。因此,相对于均值滤波(mean filter)它的平滑效果更柔和,而且边缘保留的也更好。
高斯滤波被用作为平滑滤波器的本质原因是因为它是一个低通滤波器,而且大部份基于卷积平滑滤波器都是低通滤波器。
GAUSS滤波算法克服了边界效应,因而滤波后的图像较好。
4.2 高斯滤波算法实现步骤
高斯滤波3x3算子:
高斯滤波5x5算子:
用经过shift_ram缓存后的数据,分别乘以高斯滤波算子,在加权求和算出总的高斯滤波输出,如下图:
4.3 高斯滤波代码设计
module gauss_filter (
input clk ,
input rst_n ,
input [7:0] din , //灰度输入
input din_sop ,
input din_eop ,
input din_vld ,
output [7:0] dout , //高斯滤波输出
output dout_sop,
output dout_eop,
output dout_vld
);
wire [7:0] taps0 ;
wire [7:0] taps1 ;
wire [7:0] taps2 ;
//行同步
reg [7:0] row0_0;
reg [7:0] row0_1;
reg [7:0] row0_2;
reg [7:0] row1_0;
reg [7:0] row1_1;
reg [7:0] row1_2;
reg [7:0] row2_0;
reg [7:0] row2_1;
reg [7:0] row2_2;
//
reg [10:0] sum0;
reg [10:0] sum1;
reg [10:0] sum2;
//
reg [7:0] out_gauss;
reg [2:0] vld ;
reg [2:0] sop ;
reg [2:0] eop ;
/**************************************************************
shift_ram(3x3)模块
**************************************************************/
gs_shift_ram gs_shift_ram_inst (
.aclr ( ~rst_n ),
.clken ( din_vld ),
.clock ( clk ),
.shiftin ( din ),
.shiftout ( ),
.taps0x ( taps0 ),
.taps1x ( taps1 ),
.taps2x ( taps2 )
);
/**************************************************************
第一级流水
**************************************************************/
//缓存3行数据
always@(posedge clk or negedge rst_n)
if(!rst_n) begin
row0_0 <= 'd0; row0_1 <= 'd0; row0_2 <= 'd0;
row1_0 <= 'd0; row1_1 <= 'd0; row1_2 <= 'd0;
row2_0 <= 'd0; row2_1 <= 'd0; row2_2 <= 'd0;
end
else if(vld[0]) begin
row0_0 <= taps0; row0_1 <= row0_0; row0_2 <= row0_1;
row1_0 <= taps1; row1_1 <= row1_0; row1_2 <= row1_1;
row2_0 <= taps2; row2_1 <= row2_0; row2_2 <= row2_1;
end
/**************************************************************
第二级流水
**************************************************************/
//对三行数分别进行加权求和
always@(posedge clk or negedge rst_n)
if(!rst_n) begin
sum0 <= 'd0;
sum1 <= 'd0;
sum2 <= 'd0;
end
else if(vld[1]) begin //将每个数分别乘以对应的3x3高斯算子
sum0 <= {2'b0,row0_0} + {1'b0,row0_1,1'b0} + {2'b0,row0_2 };
sum1 <= {1'b0,row1_0,1'b0} + {row1_1,2'b0} + {1'b0,row1_2,1'b0};
sum2 <= {2'b0,row2_0} + {1'b0,row2_1,1'b0} + {2'b0,row2_2 };
end
/**************************************************************
第三级流水
**************************************************************/
//
always@(posedge clk or negedge rst_n)
if(!rst_n)
out_gauss <= 'd0;
else if(vld[2]) begin
out_gauss <= (sum0 + sum1 + sum2) >> 4;
end
always @(posedge clk or negedge rst_n)begin
if(~rst_n)begin
sop <= 'd0;
eop <= 'd0;
vld <= 'd0;
end
else begin
sop <= {sop[1:0],din_sop};
eop <= {eop[1:0],din_eop};
vld <= {vld[1:0],din_vld};
end
end
//输出端口
assign dout = out_gauss;
assign dout_sop = sop[2];
assign dout_eop = eop[2];
assign dout_vld = vld[2];
endmodule
5.图像二值化
5.1 图像二值化原理
对于一个灰度图像来说,如果指定的像素点大于某一个数值,那么该点设置为255;反之则设置为0。这就是图像二值化的由来 。
5.2 二值化代码设计
module binarization (
input clk ,
input rst_n ,
input [7:0] din , //高斯滤波输入
input din_sop ,
input din_eop ,
input din_vld ,
output dout , //图像二值化输出
output dout_sop,
output dout_eop,
output dout_vld
);
reg dout_r ;
reg dout_sop_r;
reg dout_eop_r;
reg dout_vld_r;
always@(posedge clk or negedge rst_n)
if(!rst_n)begin
dout_r <= 'd0;
dout_sop_r <= 'd0;
dout_eop_r <= 'd0;
dout_vld_r <= 'd0;
end
else begin
dout_sop_r <= din_sop;
dout_eop_r <= din_eop;
dout_vld_r <= din_vld;
if(din > 120) //二值化阈值
dout_r <= 1'b1;
else
dout_r <= 1'b0;
end
//输出端口
assign dout = dout_r ;
assign dout_sop = dout_sop_r;
assign dout_eop = dout_eop_r;
assign dout_vld = dout_vld_r;
endmodule
6.sobel边沿检测
6.1 sobel算子简介
关于sobel算子,详细可见:OpenCV(十五)边缘检测1 -- Sobel算子
sobel算子是一个离散的一阶差分算子,广泛应用于边缘检测等领域。算法的应用原理比较简单,可以完成对水平方向和垂直方向的边缘检测。分别用图中的两个卷积模板对图像进行滑动窗口的卷积计算,将卷积模板和图像3*3窗口对应的数据相乘,相乘的结果相加得到
和
,通过
计算的得到G,再通过阈值比较得到二值图像。有时为了提高计算效率,通过
来近似得到G。
运用sobel算子进行边缘检测时,可以直接乘其模板的绝对值,再将相乘结果相减,以便于运算
6.2 sobel算法使用步骤
1.先求x,y方向的梯度dx,dy。
2.然后求出近似梯度
然后开根号,也可以为了分别计算近似为
3.最后提取G的值,来判断该点是不是边缘点,是的话,就将该点的像素复制为255,否则为0;阈值G可以自己随意指定,阈值的设定通常在0-255之间,没有标准值。阈值设定过高,会导致边缘被过滤掉;阈值设定过低,会导致边缘过多的被保留,造成边缘检测的结果混乱。
6.3 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
);
wire taps0 ;
wire taps1 ;
wire taps2 ;
//行同步
reg row0_0;
reg row0_1;
reg row0_2;
reg row1_0;
reg row1_1;
reg row1_2;
reg row2_0;
reg row2_1;
reg row2_2;
reg [2:0] sumx_0;
reg [2:0] sumx_2;
reg [2:0] sumy_0;
reg [2:0] sumy_2;
reg [3:0] x_abs;
reg [3:0] y_abs;
reg [3:0] g ;
reg [3:0] sop ;
reg [3:0] eop ;
reg [3:0] vld ;
/**************************************************************
shift_ram(3x3)模块
**************************************************************/
sobel_shift_ram sobel_shift_ram_inst (
.aclr ( ~rst_n ),
.clken ( din_vld ),
.clock ( clk ),
.shiftin ( din ),
.shiftout ( ),
.taps0x ( taps0 ),
.taps1x ( taps1 ),
.taps2x ( taps2 )
);
//缓存3行数据,第一级流水
always@(posedge clk or negedge rst_n)
if(!rst_n) begin
row0_0 <= 'd0; row0_1 <= 'd0; row0_2 <= 'd0;
row1_0 <= 'd0; row1_1 <= 'd0; row1_2 <= 'd0;
row2_0 <= 'd0; row2_1 <= 'd0; row2_2 <= 'd0;
end
else if(vld[0]) begin
row0_0 <= taps0; row0_1 <= row0_0; row0_2 <= row0_1;
row1_0 <= taps1; row1_1 <= row1_0; row1_2 <= row1_1;
row2_0 <= taps2; row2_1 <= row2_0; row2_2 <= row2_1;
end
//将缓存后的数据乘以sobel算子模板,第二级流水
always@(posedge clk or negedge rst_n)
if(!rst_n) begin
sumx_0 <= 'd0;
sumx_2 <= 'd0;
sumy_0 <= 'd0;
sumy_2 <= 'd0;
end
else if(vld[1]) begin
sumx_0 <= {2'b0,row0_0} + {1'b0,row1_0,1'b0} + {2'b0,row2_0};
sumx_2 <= {2'b0,row0_2} + {1'b0,row1_2,1'b0} + {2'b0,row2_2};
sumy_0 <= {2'b0,row0_0} + {1'b0,row0_1,1'b1} + {2'b0,row0_2};
sumy_2 <= {2'b0,row2_0} + {1'b0,row2_1,1'b1} + {2'b0,row2_2};
end
//计算x、y梯度绝对值,第3级流水
always@(posedge clk or negedge rst_n)
if(!rst_n) begin
x_abs <= 'd0;
y_abs <= 'd0;
end
else if(vld[2]) begin
x_abs <= (sumx_0 > sumx_2) ? (sumx_0 - sumx_2) : (sumx_2 - sumx_0);
y_abs <= (sumy_0 > sumy_2) ? (sumy_0 - sumy_2) : (sumy_2 - sumy_0);
end
//计算最终的g值,第4级流水
always@(posedge clk or negedge rst_n)
if(!rst_n)
g <= 'd0;
else if(vld[3]) begin
g <= x_abs + y_abs;
end
//打拍
always @(posedge clk or negedge rst_n)begin
if(~rst_n)begin
sop <= 0;
eop <= 0;
vld <= 0;
end
else begin
sop <= {sop[2:0],din_sop};
eop <= {eop[2:0],din_eop};
vld <= {vld[2:0],din_vld};
end
end
assign dout = g >= 3;//阈值假设为3 当某一个像素点的梯度值大于3,认为其是一个边缘点
assign dout_sop = sop[3];
assign dout_eop = eop[3];
assign dout_vld = vld[3];
endmodule
7.sdram模块
通过乒乓缓存操作向SDRAM中读写图像数据,接口通过调用IP,主要是SDRAM读写控制逻辑(rw_control),使用两个异步FIFO跨时钟域数据处理,使用读写仲裁机制产生读写传输请求、地址等
为什么要用pp(乒乓)缓存?
如果不采用乒乓缓存,OV5640 帧率 30fps,VGA 帧率 60fps,如果摄像头输入的数据和VGA输出的数据都是连续不断的,那么刚好可以写一帧读两帧。但是一帧图像实际情况是一行行的生成和读取的,所以会出现 VGA 从SDRAM处读的上半帧是新帧,而由于SDRAM缓存的下半帧还没有被 OV5640写完,VGA 从SDRAM处读的下半帧还是旧帧,会出现错帧现象。
采用乒乓缓存机制时,使用两个缓存区,写缓存区 1 时读缓存区 2,写缓存区 2 时读缓存区 1,每个缓存区存储完整的数据帧,读写隔离并且读写交替则不会出现错帧现象。具体乒乓缓存操作如下图:
为什么要读写仲裁?
在FPGA中,当多个操作同时发出请求,容易导致操作冲突,因此我们需要根据相应的优先级来响应哪一个操作,这个过程就叫仲裁。在SDRAM中,初始化完成后,主要的功能就是突发写、突发读和自动刷新。如果同时发起写、读和刷新请求,就会出现操作冲突,从而导致SDRAM工作出错,因此这里就需要引入仲裁机制。为了简化设计,考虑将刷新与读写请求的仲裁分开考虑。由于刷新的优先级一定高于读写,因此,在底层接口中,只对读/写请求与刷新请求进行仲裁,即刷新请求的优先级一定高于读/写请求。在控制逻辑中,对读/写请求进行仲裁,保证底层接口不会同时收到读请求与写请求,从而避免底层接口中出现复杂控制。
7.1 sdram读写控制模块设计思路
7.1.1 整体分析数据从哪儿输入,输出到哪儿去?
(1).跨时钟域数据传输,读写FIFO。
(2).涉及到的时钟信号3个:pclk ,clk_75m (vga),sdram控制器时钟clk_100m。
sdram控制器时钟为什么取100m?
数据吞吐量计算:由于sdram地址总线、命令总线和数据总线是共用的,所以读写操作不能同时进行,要考虑1s钟能否成功接收摄像头传输过来的数据和vga正常显示需要的数据:
|-----------------|---------------------|
| 摄像头1s传输数据量 | 1280*720*30 ≈ 30m |
| vga接口1s需要传输的数据量 | 1280*720*60 ≈ 60m |
7.1.2 如何控制sdram控制器的数据读写策略,能避免数据拥塞?
(1).问题分析:由于摄像头数据输出的像素数据量大且速度较快,vga显示所需要的数据量大且速度较快,若不合理控制读写,则有可能会导致写fifo 中的数据量溢出或者读fifo 中数据读空。
(2).解决方案:动态调整读写操作的优先 -- 根据与缓冲区与读缓冲区中的剩余数据量。动态仲裁读操作和写操作的先后顺序,保证写缓冲区不溢出,读缓冲区不空。
(3).思考:读写速度过快的情况下,无法手动控制读写请求,由控制器内部去产生控制请求。可以做一个读写仲裁机制。
|-----------------|-----------|---------|
| 只读 (满足可读条件) | 读fifo剩余数据可供vga显示 ||
| 只写 (满足可写条件) | 写fifo有多少剩余数据量可向sdram写入 ||
| 读写同时存在?同时满足读写条件 | 上一次操作是读操作 | 这次就是读操作 |
| 读写同时存在?同时满足读写条件 | 上一次操作是写操作 | 这次就是读操作 |
注意:sdram不能同时执行读写操作,所以控制器不能同时给接口模块读写请求。
多久仲裁1次?每完成一次突发读或写操作仲裁1次(突发长度建议512)。
(4).sdram的读写请求怎么产生?利用读写fifo的剩余数据量。
①.写请求:写fifo的usedw足够sdram完成一次突发读时,即wr_usedw > 512,sdram的写请求拉高;反之,拉低。
②.读请求:读fifo的数据余量低于一个下限值(下限值大于突发长度)时,拉高
读请求:读fifo的数据余量高于一个上限值(上限值大于2倍突发长度)时,拉低读请求,能保证低于上限是也能完成一次突发读;保证读fifo中有足够数据量传输到 VGA端.
即,当rd_wrusedw <= 下限值(本次设计为600)时,读请求拉高,开启突发读;当rd_usedw >上限值(本次设计为1500)时,读请求拉低。
7.1.3 如何保证显示器显示的是一帧完整的图像?
通过双bank乒乓缓存实现写入和读出图像帧的完整性 -- 对每个bank的读写都是以完整的数据帧为单位操作,通过sop与eop信号确定数据帧的范围。
注意:代码中SDRAM无法进行同时读写,我们只能在写完且读完一帧数据时去切换存储区域,便于操控选择两个不同的bank进行切换。
乒乓操作主要⽤于控制数据流,在此项⽬中主要体现为先写SDRAM bank1的数据,同时读SDRAM bank3的数据,当两块bank的数据读写完毕后,切换操作为读bank1的数据,写bank3的数据,这样可以保持数据为完整的⼀帧,使显⽰屏帧与帧之间切换瞬间完成。
具体步骤如下:
在第一个缓冲周期,输入数据流写入数据缓冲模块1,写完后进入第二个缓冲周期。
在第二个缓冲周期,输入数据流写入数据缓冲模块2,同时将数据缓冲模块1中的数据读出。
在第三个缓冲周期,输入数据流再次写入数据缓冲模块 1,同时将数据缓冲模块 2 中的数据读出。
7.1.4 丢帧处理.
(1)为什么要丢帧?
乒乓操作中有若出现一帧数据写完但是还没读完的情况,又来一帧新的图像数据,此时就不能再向sdram中写入数据,否则会出现帧错位的情况,此时则需要丢到当前帧,等待读操作完成后,下一次sop的到来。
2.SDRAM模块代码设计
`include "param.v"
module sdram_drive (
input clk , //clk_100m
input clk_in ,
input clk_out ,
input rst_n ,
//image_process
input [15:0] din ,
input din_sop ,
input din_eop ,
input din_vld ,
//vga
input req , //vga读数据请求
output [15:0] dout ,
output dout_vld ,
//avalon_port
output [23:0] addr , //访问sdram的地址
output wr_n , //访问sdram的写使能信号
output [15:0] wr_data , //访问sdram的写数据
output rd_n , //访问sdram的读使能信号
input [15:0] rd_data , //访问sdram的读出数据
input rd_data_vld, //访问sdram的读出数据有效信号
input waitrequest //sdram等待请求信号
);
reg [1:0] state_c ;
reg [1:0] state_n ;
wire idle2write ;
wire idle2read ;
wire read2done ;
wire write2done ;
wire done2idle ;
//avalon_r
reg [15:0] rd_data_r ;
reg rd_data_vld_r ;
reg waitrequest_r ;
//vga_r
reg [15:0] vga_data ;
reg vga_data_vld ;
//cnt_BL
reg [9:0] cnt_bl ;
wire add_cnt_bl ;
wire end_cnt_bl ;
//wraddr
reg [21:0] cnt_wraddr ;
wire add_cnt_wraddr;
wire end_cnt_wraddr;
//rdaddr
reg [21:0] cnt_rdaddr ;
wire add_cnt_rdaddr;
wire end_cnt_rdaddr;
//wrfifo
wire [17:0] wrfifo_din ;
wire [17:0] wrfifo_dout ;
wire wrfifo_wrreq ;
wire wrfifo_rdreq ;
wire wrfifo_rdempty;
wire wrfifo_rdfull ;
wire [10:0] wrfifo_rdusedw;
wire wrfifo_wrempty;
wire wrfifo_wrfull ;
wire [10:0] wrfifo_wrusedw;
reg wr_data_flag ;
//rdfifo
wire [15:0] rdfifo_din ;
wire [15:0] rdfifo_dout ;
wire rdfifo_wrreq ;
wire rdfifo_rdreq ;
wire rdfifo_rdempty;
wire rdfifo_rdfull ;
wire [10:0] rdfifo_rdusedw;
wire rdfifo_wrempty;
wire rdfifo_wrfull ;
wire [10:0] rdfifo_wrusedw;
//读写优先级仲裁标志
reg rd_flag ;
reg wr_flag ;
reg flag_r ;
reg priority_flag ;
//乒乓操作
reg [1:0] wr_bank ;
reg [1:0] rd_bank ;
reg change_bank ;
reg wr_finish ;
//打拍 同步到写侧
reg [1:0] wr_finish_r ;
/**************************************************************
状态机
**************************************************************/
parameter IDLE = 0,
READ = 1,
WRITE = 2,
DONE = 3;
//第一段状态机
always @(posedge clk or negedge rst_n)begin
if(!rst_n)begin
state_c <= IDLE;
end
else begin
state_c <= state_n;
end
end
//第二段状态机
always @(*)begin
case(state_c)
IDLE : if(idle2write)
state_n = WRITE;
else if(idle2read)
state_n = READ;
else
state_n = state_c;
READ : if(read2done)
state_n = DONE;
else
state_n = state_c;
WRITE : if(write2done)
state_n = DONE;
else
state_n = state_c;
DONE : if(done2idle)
state_n = IDLE;
else
state_n = state_c;
default : state_n = state_c;
endcase
end
//状态跳转条件
assign idle2write = state_c == IDLE && (~priority_flag && wrfifo_rdusedw > `BURST_LENTH);
assign idle2read = state_c == IDLE && priority_flag && rdfifo_wrusedw <= `RD_UT;
assign read2done = state_c == READ && end_cnt_rdaddr;
assign write2done = state_c == WRITE && end_cnt_wraddr;
assign done2idle = state_c == DONE && 1'b1;
/**************************************************************
突发长度计数器
**************************************************************/
always@(posedge clk or negedge rst_n)
if(!rst_n)
cnt_bl <= 'd0;
else if(add_cnt_bl) begin
if(end_cnt_bl)
cnt_bl <= 'd0;
else
cnt_bl <= cnt_bl + 1'b1;
end
assign add_cnt_bl = (state_c == READ | state_c == WRITE) && !waitrequest_r;
assign end_cnt_bl = add_cnt_bl && cnt_bl == `BURST_LENTH - 1;
/**************************************************************
读写优先级仲裁
**************************************************************/
//rd_falg
always@(posedge clk or negedge rst_n)
if(!rst_n)
rd_flag <= 1'b0;
else if(rdfifo_wrusedw <= `RD_LT)
rd_flag <= 1'b1;
else if(rdfifo_wrusedw > `RD_UT)
rd_flag <= 1'b0;
else
rd_flag <= rd_flag;
//wr_flag
always@(posedge clk or negedge rst_n)
if(!rst_n)
wr_flag <= 1'b0;
else if(wrfifo_rdusedw > `BURST_LENTH)
wr_flag <= 1'b1;
else
wr_flag <= 1'b0;
//flag_r 判断上一次为读/写操作?flag_r=1,为读操作;flag_r=0,为写操作
always@(posedge clk or negedge rst_n)
if(!rst_n)
flag_r <= 1'b0;
else if(read2done)
flag_r <= 1'b1;
else if(write2done)
flag_r <= 1'b0;
//priority_flag 优先级标志 0:写优先级高; 1:读优先级高
always@(posedge clk or negedge rst_n)
if(!rst_n)
priority_flag <= 1'b0;
else if(wr_flag && (flag_r || (~flag_r && ~rd_flag)))
priority_flag <= 1'b0;
else if(rd_flag && (~flag_r || (flag_r && ~wr_flag)))
priority_flag <= 1'b1;
/**************************************************************
地址计数器
**************************************************************/
//wraddr
always@(posedge clk or negedge rst_n)
if(!rst_n)
cnt_wraddr <= 'd0;
else if(add_cnt_wraddr) begin
if(end_cnt_wraddr)
cnt_wraddr <= 'd0;
else
cnt_wraddr <= cnt_wraddr + `BURST_LENGTH;
end
assign add_cnt_wraddr = state_c == WRITE && !waitrequest_r;
assign end_cnt_wraddr = add_cnt_wraddr && cnt_wraddr == `BURST_MAX - `BURST_LENGTH;
//rdaddr
always@(posedge clk or negedge rst_n)
if(!rst_n)
cnt_rdaddr <= 'd0;
else if(add_cnt_rdaddr) begin
if(end_cnt_rdaddr)
cnt_rdaddr <= 'd0;
else
cnt_rdaddr <= cnt_rdaddr + `BURST_LENGTH;
end
assign add_cnt_rdaddr = state_c == READ && !waitrequest_r;
assign end_cnt_rdaddr = add_cnt_rdaddr && cnt_rdaddr == `BURST_MAX - `BURST_LENGTH;
//wr_bank rd_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
//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 & wrfifo_dout[17])begin //写完 从wrfifo读出eop
wr_finish <= 1'b1;
end
else if(wr_finish && end_cnt_rdaddr)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_cnt_rdaddr;
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_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
/**************************************************************
sdram输入寄存
**************************************************************/
//由于主从机时钟相位不同,所以从机发送来的信号需要同步寄存
always@(posedge clk_out or negedge rst_n)
if(!rst_n) begin
rd_data_r <= 16'b0;
rd_data_vld_r <= 1'b0;
waitrequest_r <= 1'b0;
end
else begin
rd_data_r <= rd_data ;
rd_data_vld_r <= rd_data_vld;
waitrequest_r <= waitrequest;
end
/**************************************************************
vga输出寄存
**************************************************************/
always @(posedge clk_out or negedge rst_n)begin
if(~rst_n)begin
vga_data <= 0;
vga_data_vld <= 1'b0;
end
else begin
vga_data <= rdfifo_dout;
vga_data_vld <= rdfifo_rdreq;
end
end
/**************************************************************
FIFO模块
**************************************************************/
//wrfifo
wrfifo wrfifo_inst (
.aclr ( ~rst_n ),
.data ( wrfifo_din ),
.rdclk ( clk ),
.rdreq ( wrfifo_rdreq ),
.wrclk ( clk_in ),
.wrreq ( wrfifo_wrreq ),
.q ( wrfifo_dout ),
.rdempty ( wrfifo_rdempty ),
.rdfull ( wrfifo_rdfull ),
.rdusedw ( wrfifo_rdusedw ),
.wrempty ( wrfifo_wrempty ),
.wrfull ( wrfifo_wrfull ),
.wrusedw ( wrfifo_wrusedw )
);
assign wrfifo_wrreq = din_vld && ~wrfifo_wrfull && ((wr_finish_r[1] && din_sop) || wr_data_flag);
assign wrfifo_rdreq = ~wrfifo_rdempty && (state_c == WRITE) && !waitrequest_r ;
assign wrfifo_din = {din_eop,din_sop,din};
//rdfifo
rdfifo rdfifo_inst (
.aclr ( ~rst_n ),
.data ( rdfifo_din ),
.rdclk ( clk_out ),
.rdreq ( rdfifo_rdreq ),
.wrclk ( clk ),
.wrreq ( rdfifo_wrreq ),
.q ( rdfifo_dout ),
.rdempty ( rdfifo_rdempty ),
.rdfull ( rdfifo_rdfull ),
.rdusedw ( rdfifo_rdusedw ),
.wrempty ( rdfifo_wrempty ),
.wrfull ( rdfifo_wrfull ),
.wrusedw ( rdfifo_wrusedw )
);
assign rdfifo_wrreq = rd_data_vld_r && !rdfifo_wrfull && !waitrequest_r;
assign rdfifo_rdreq = !rdfifo_rdempty && req;
assign rdfifo_din = rd_data_r;
/**************************************************************
输出端口
**************************************************************/
//avalon
assign wr_data = wrfifo_dout[15:0] ;
assign addr = (state_c == WRITE) ? {wr_bank[1],cnt_wraddr[21:9],wr_bank[0],cnt_wraddr[8:0]} :
((state_c == READ) ? {rd_bank[1],cnt_rdaddr[21:9],rd_bank[0],cnt_rdaddr[8:0]} : 0) ;
assign wr_n = !(state_c == WRITE);
assign rd_n = !(state_c == READ);
//vga
assign dout = vga_data;
assign dout_vld = vga_data_vld;
endmodule
三、仿真测试
1.摄像头配置模块仿真
略