目录
一、OV7670摄像头简介
OV7670/OV7171 图像传感器,体积小、工作电压低,提供单片VGA摄像头和影像处理器的所有功能。通过SCCB 总线控制,可以输出整帧、子采样、取窗口等方式的各种分辨率8位影响数据。该产品VGA图像最高达到30帧/秒。用户可以完全控制图像质量、数据格式和传输方式。所有图像处理功能过程包括伽玛曲线、白平衡、饱和度、色度等都可以通过SCCB接口编程。OmmiVision 图像传感器应用独有的传感器技术,通过减少或消除光学或电子缺陷如固定图案噪声、托尾、浮散等,提高图像质量,得到清晰的稳定的彩色图像。
其功能框图如图所示:
所用模块如图:
主要引出的信号引脚如下:
SCL:SCCB时钟口
SDA:SCCB数据口
VSYNC:场同步(帧同步)信号
HREF:行同步信号
PCLK:像素时钟
XCLK:系统时钟输入
D0-D7:数据位
RESET:初始化所有寄存器到默认值 0:RESET 模式 1:一般模式
PWDN:POWER DOWN模式选择 0:工作 1:POWER DOWN
其中,对OV7670初始化配置只使用SCL和SDA两个信号线。
二、ov7670 SCCB协议简介
对OV7670初始化使用的是SCCB协议,由数据线SDA和时钟线SCL组成,SCCB协议和I2C协议基本一样,是简化的I2C协议,前面博客中已经讲过I2C协议,需要可以去看看。
SCCB(SeriaI Camera ControlBus)是简化的I2C协议,SIO-l是串行时钟输入线,SIO-O是串行双向数据线,分别相当于I2C协议的SCL和SDA。SCCB的总线时序与I2C基本相同,它的响应信号ACK被称为一个传输单元的第9位,分为Don’t care和NA。Don’t care位由从机产生;NA位由主机产生,由于SCCB不支持多字节的读写,NA位必须为高电平。另外,SCCB没有重复起始的概念,因此在SCCB的读周期中,当主机发送完片内寄存器地址后,必须发送总线停止条件。不然在发送读命令时,从机将不能产生Don’t care响应信号。
其写周期如下:
(1)首先发送设备ID地址:
(2)发送寄存器地址
(3)写寄存器数据
SCCB时序图如图:
这里采用的是100KHz的SCL。
三、OV7670初始化寄存器配置
在OV7670数据手册中只讲了其各种寄存器的列表,没有对具体如何配置进行讲解,可以看一下《OV7670 software application note》这个手册,里边有讲如何配置OV7670寄存器。
OV7670摄像头共201个寄存器,需要配置的有一百六十几个,这个具体看数据手册吧,下边代码中会有配置寄存器的数据。
四、OV7670初始化代码编写
1、初始化过程
(1)上电等待3ms等电平稳定
(2)发送初始化开始标志,通过SCCB协议对寄存器进行配置
(3)配置结束后,发送初始化完成标志,供后边读取图像数据作为开始标志。
2、RTL设计
(1)SCCB发送模块,因为没用用到读的功能,这里只写了SCCB的写数据功能。
// Company :
// Engineer :
// -----------------------------------------------------------------------------
// https://blog.csdn.net/qq_33231534 PHF's CSDN blog
// -----------------------------------------------------------------------------
// Create Date : 2020-09-11 16:53:19
// Revise Data : 2020-09-11 16:53:19
// File Name : SCCB_sender.v
// Target Devices : XC7Z015-CLG485-2
// Tool Versions : Vivado 2019.2
// Revision : V1.1
// Editor : sublime text3, tab size (4)
// Description : SCCB接口协议发送数据,用于OV7670,线性序列机编写代码
module SCCB_sender(
input clk ,//系统时钟50MHz
input rst_n ,//系统复位
input send_en ,//发送使能
input [7:0] addr ,//发送寄存器地址
input [7:0] value ,//发送寄存器地址对应数据
output reg done ,//发送结束标志
output reg state ,//发送状态,忙为0,不忙为1
output reg scl ,//输出时钟线
inout sda //输出数据线
);
reg [7:0] addr_r;
reg [7:0] value_r;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
addr_r <= 8'd0;
value_r <= 8'd0;
end
else if (send_en) begin
addr_r <= addr;
value_r <= value;
end
else begin
addr_r <= addr_r;
value_r <= value_r;
end
end
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
state <= 1;
end
else if (send_en) begin
state <= 0;
end
else if (done) begin
state <= 1;
end
else begin
state <= state;
end
end
localparam SCL_CNT_NUM = 9'd500;//100kHz
reg [8:0] scl_cnt;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
scl_cnt <= 1;
end
else if (state==0) begin
if (scl_cnt==SCL_CNT_NUM-1'b1 || done) begin
scl_cnt <= 0;
end
else begin
scl_cnt <= scl_cnt + 1'b1;
end
end
end
wire scl_high_mid;
wire scl_low_mid ;
assign scl_high_mid = scl_cnt==SCL_CNT_NUM/4-1'b1;
assign scl_low_mid = scl_cnt==SCL_CNT_NUM-SCL_CNT_NUM/4-1'b1;
reg [5:0] scl_mid_cnt;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
scl_mid_cnt <= 0;
end
else if (scl_high_mid || scl_low_mid) begin
scl_mid_cnt <= scl_mid_cnt + 1'b1;
end
else if (done) begin
scl_mid_cnt <= 0;
end
end
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
scl <= 1;
end
else if (state==0) begin
if (scl_cnt<=SCL_CNT_NUM/2-1) begin
scl <= 1;
end
else begin
scl <= 0;
end
end
else begin
scl <= 1;
end
end
reg sda_en;
reg sda_reg;
assign sda = sda_en ? sda_reg : 1'bz;
parameter device_id = 8'b0100_0010;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
sda_reg <= 1;
end
else if (state==0 && (scl_high_mid || scl_low_mid)) begin
case(scl_mid_cnt)
6'd0: sda_reg <= 0;
6'd1: sda_reg <= device_id[7];
6'd3: sda_reg <= device_id[6];
6'd5: sda_reg <= device_id[5];
6'd7: sda_reg <= device_id[4];
6'd9: sda_reg <= device_id[3];
6'd11: sda_reg <= device_id[2];
6'd13: sda_reg <= device_id[1];
6'd15: sda_reg <= device_id[0];
6'd19: sda_reg <= addr_r[7];
6'd21: sda_reg <= addr_r[6];
6'd23: sda_reg <= addr_r[5];
6'd25: sda_reg <= addr_r[4];
6'd27: sda_reg <= addr_r[3];
6'd29: sda_reg <= addr_r[2];
6'd31: sda_reg <= addr_r[1];
6'd33: sda_reg <= addr_r[0];
6'd37: sda_reg <= value_r[7];
6'd39: sda_reg <= value_r[6];
6'd41: sda_reg <= value_r[5];
6'd43: sda_reg <= value_r[4];
6'd45: sda_reg <= value_r[3];
6'd47: sda_reg <= value_r[2];
6'd49: sda_reg <= value_r[1];
6'd51: sda_reg <= value_r[0];
6'd55: sda_reg <= 0;
6'd56: sda_reg <= 1;
default:sda_reg <= sda_reg;
endcase
end
end
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
sda_en <= 0;
end
else if (state==0) begin
if (scl_mid_cnt==6'd18 || scl_mid_cnt==6'd19 || scl_mid_cnt==6'd36 || scl_mid_cnt==6'd37 || scl_mid_cnt==6'd54 || scl_mid_cnt==6'd55) begin
sda_en <= 0;
end
else begin
sda_en <= 1;
end
end
else begin
sda_en <= 0;
end
end
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
done <= 0;
end
else if (scl_mid_cnt==6'd57 && scl_cnt==SCL_CNT_NUM/2-2) begin
done <= 1;
end
else begin
done <= 0;
end
end
endmodule
(2)OV7670寄存器数据配置
// Company :
// Engineer :
// -----------------------------------------------------------------------------
// https://blog.csdn.net/qq_33231534 PHF's CSDN blog
// -----------------------------------------------------------------------------
// Create Date : 2020-09-12 19:09:43
// Revise Data : 2020-09-12 20:06:25
// File Name : ov7670_config.v
// Target Devices : XC7Z015-CLG485-2
// Tool Versions : Vivado 2019.2
// Revision : V1.1
// Editor : sublime text3, tab size (4)
// Description : ov7670配置模块
module ov7670_config(
input clk ,
input rst_n ,
input SCCB_done ,
output reg flag ,
output reg data_vld ,
output wire [7:0] addr ,
output wire [7:0] value
);
reg SCCB_done_r;
reg [7:0] cnt;
reg [15:0] dout;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
SCCB_done_r <= 0;
end
else begin
SCCB_done_r <= SCCB_done;
end
end
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
data_vld <= 0;
end
else if (SCCB_done_r && flag==0) begin
data_vld <= 1;
end
else begin
data_vld <= 0;
end
end
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
cnt <= 7'd0;
end
else if (SCCB_done && flag==0) begin
cnt <= cnt + 1'b1;
end
end
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
flag <= 0;
end
else if (cnt==8'd165) begin
flag <= 1;
end
end
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
dout <= 0;
end
else begin
case(cnt)
8'd1: dout <= 16'h1204;
8'd2: dout <= 16'h40d0;
8'd3: dout <= 16'h3a04;
8'd4: dout <= 16'h3dc8;
8'd5: dout <= 16'h1e31;
8'd6: dout <= 16'h6b00;
8'd7: dout <= 16'h32b6;
8'd8: dout <= 16'h1713;
8'd9: dout <= 16'h1801;
8'd10: dout <= 16'h1902;
8'd11: dout <= 16'h1a7a;
8'd12: dout <= 16'h030a;
8'd13: dout <= 16'h0c00;
8'd14: dout <= 16'h3e10;
8'd15: dout <= 16'h7000;
8'd16: dout <= 16'h7100;
8'd17: dout <= 16'h7211;
8'd18: dout <= 16'h7300;
8'd19: dout <= 16'ha202;
8'd20: dout <= 16'h1180;
8'd21: dout <= 16'h7a20;
8'd22: dout <= 16'h7b1c;
8'd23: dout <= 16'h7c28;
8'd24: dout <= 16'h7d3c;
8'd25: dout <= 16'h7e55;
8'd26: dout <= 16'h7f68;
8'd27: dout <= 16'h8076;
8'd28: dout <= 16'h8180;
8'd29: dout <= 16'h8288;
8'd30: dout <= 16'h838f;
8'd31: dout <= 16'h8496;
8'd32: dout <= 16'h85a3;
8'd33: dout <= 16'h86af;
8'd34: dout <= 16'h87c4;
8'd35: dout <= 16'h88d7;
8'd36: dout <= 16'h89e8;
8'd37: dout <= 16'h13e0;
8'd38: dout <= 16'h0010;
8'd39: dout <= 16'h1000;
8'd40: dout <= 16'h0d00;
8'd41: dout <= 16'h1428;
8'd42: dout <= 16'ha505;
8'd43: dout <= 16'hab07;
8'd44: dout <= 16'h2475;
8'd45: dout <= 16'h2563;
8'd46: dout <= 16'h26a5;
8'd47: dout <= 16'h9f78;
8'd48: dout <= 16'ha068;
8'd49: dout <= 16'ha103;
8'd50: dout <= 16'ha6df;
8'd51: dout <= 16'ha7df;
8'd52: dout <= 16'ha8f0;
8'd53: dout <= 16'ha990;
8'd54: dout <= 16'haa94;
8'd55: dout <= 16'h13ef;
8'd56: dout <= 16'h0e61;
8'd57: dout <= 16'h0f4b;
8'd58: dout <= 16'h1602;
8'd59: dout <= 16'h2102;
8'd60: dout <= 16'h2291;
8'd61: dout <= 16'h2907;
8'd62: dout <= 16'h330b;
8'd63: dout <= 16'h350b;
8'd64: dout <= 16'h371d;
8'd65: dout <= 16'h3871;
8'd66: dout <= 16'h392a;
8'd67: dout <= 16'h3c78;
8'd68: dout <= 16'h4d40;
8'd69: dout <= 16'h4e20;
8'd70: dout <= 16'h6900;
8'd71: dout <= 16'h7419;
8'd72: dout <= 16'h8d4f;
8'd73: dout <= 16'h8e00;
8'd74: dout <= 16'h8f00;
8'd75: dout <= 16'h9000;
8'd76: dout <= 16'h9100;
8'd77: dout <= 16'h9200;
8'd78: dout <= 16'h9600;
8'd79: dout <= 16'h9a80;
8'd80: dout <= 16'hb084;
8'd81: dout <= 16'hb10c;
8'd82: dout <= 16'hb20e;
8'd83: dout <= 16'hb382;
8'd84: dout <= 16'hb80a;
8'd85: dout <= 16'h4314;
8'd86: dout <= 16'h44f0;
8'd87: dout <= 16'h4534;
8'd88: dout <= 16'h4658;
8'd89: dout <= 16'h4728;
8'd90: dout <= 16'h483a;
8'd91: dout <= 16'h5988;
8'd92: dout <= 16'h5a88;
8'd93: dout <= 16'h5b44;
8'd94: dout <= 16'h5c67;
8'd95: dout <= 16'h5d49;
8'd96: dout <= 16'h5e0e;
8'd97: dout <= 16'h6404;
8'd98: dout <= 16'h6520;
8'd99: dout <= 16'h6605;
8'd100: dout <= 16'h9404;
8'd101: dout <= 16'h9508;
8'd102: dout <= 16'h6c0a;
8'd103: dout <= 16'h6d55;
8'd104: dout <= 16'h6e11;
8'd105: dout <= 16'h6f9f;
8'd106: dout <= 16'h6a40;
8'd107: dout <= 16'h0140;
8'd108: dout <= 16'h0240;
8'd109: dout <= 16'h13e7;
8'd110: dout <= 16'h1500;
8'd111: dout <= 16'h4f80;
8'd112: dout <= 16'h5080;
8'd113: dout <= 16'h5100;
8'd114: dout <= 16'h5222;
8'd115: dout <= 16'h535e;
8'd116: dout <= 16'h5480;
8'd117: dout <= 16'h589e;
8'd118: dout <= 16'h4108;
8'd119: dout <= 16'h3f00;
8'd120: dout <= 16'h7505;
8'd121: dout <= 16'h76e1;
8'd122: dout <= 16'h4c00;
8'd123: dout <= 16'h7701;
8'd124: dout <= 16'h4b09;
8'd125: dout <= 16'hc9F0;
8'd126: dout <= 16'h4138;
8'd127: dout <= 16'h5640;
8'd128: dout <= 16'h3411;
8'd129: dout <= 16'h3b02;
8'd130: dout <= 16'ha489;
8'd131: dout <= 16'h9600;
8'd132: dout <= 16'h9730;
8'd133: dout <= 16'h9820;
8'd134: dout <= 16'h9930;
8'd135: dout <= 16'h9a84;
8'd136: dout <= 16'h9b29;
8'd137: dout <= 16'h9c03;
8'd138: dout <= 16'h9d4c;
8'd139: dout <= 16'h9e3f;
8'd140: dout <= 16'h7804;
8'd141: dout <= 16'h7901;
8'd142: dout <= 16'hc8f0;
8'd143: dout <= 16'h790f;
8'd144: dout <= 16'hc800;
8'd145: dout <= 16'h7910;
8'd146: dout <= 16'hc87e;
8'd147: dout <= 16'h790a;
8'd148: dout <= 16'hc880;
8'd149: dout <= 16'h790b;
8'd150: dout <= 16'hc801;
8'd151: dout <= 16'h790c;
8'd152: dout <= 16'hc80f;
8'd153: dout <= 16'h790d;
8'd154: dout <= 16'hc820;
8'd155: dout <= 16'h7909;
8'd156: dout <= 16'hc880;
8'd157: dout <= 16'h7902;
8'd158: dout <= 16'hc8c0;
8'd159: dout <= 16'h7903;
8'd160: dout <= 16'hc840;
8'd161: dout <= 16'h7905;
8'd162: dout <= 16'hc830;
8'd163: dout <= 16'h7926;
8'd164: dout <= 16'h0903;
8'd165: dout <= 16'h3b42;
default:;
endcase
end
end
assign addr = dout[15:8];
assign value = dout[7:0];
endmodule
(3)OV7670初始化顶层模块
// Company :
// Engineer :
// -----------------------------------------------------------------------------
// https://blog.csdn.net/qq_33231534 PHF's CSDN blog
// -----------------------------------------------------------------------------
// Create Date : 2020-09-13 10:18:23
// Revise Data : 2020-09-13 10:18:23
// File Name : ov7670_init.v
// Target Devices : XC7Z015-CLG485-2
// Tool Versions : Vivado 2019.2
// Revision : V1.1
// Editor : sublime text3, tab size (4)
// Description : ov7670初始化操作
module ov7670_init(
input clk ,//50MHz
input rst_n ,
output scl ,
inout sda ,
output init_done
);
localparam device_id = 8'b0100_0010;
wire send_en ;
wire [7:0] addr ;
wire [7:0] value ;
wire done ;
wire state ;
wire flag ;
reg init_en ;
assign init_done = flag&&done;
reg [17:0] cnt_3ms;//上电等待3ms电平稳定后再初始化寄存器
always @(posedge clk or negedge rst_n)begin
if(!rst_n)begin
cnt_3ms <= 0;
end
else if(cnt_3ms<=18'd150000)begin
cnt_3ms <= cnt_3ms + 1'd1;
end
end
always @(posedge clk or negedge rst_n)begin
if(!rst_n)begin
init_en <= 0;
end
else if(cnt_3ms==18'd150000-1)begin
init_en <= 1'b1;
end
else begin
init_en <= 0;
end
end
SCCB_sender #(
.device_id(device_id)
) inst_SCCB_sender (
.clk (clk),
.rst_n (rst_n),
.send_en (send_en),
.addr (addr),
.value (value),
.done (done),
.state (state),
.scl (scl),
.sda (sda)
);
ov7670_config inst_ov7670_config
(
.clk (clk),
.rst_n (rst_n),
.SCCB_done (init_en || done),
.flag (flag),
.data_vld (send_en),
.addr (addr),
.value (value)
);
endmodule
五、仿真测试
测试代码如下:
`timescale 1ns/1ns
module ov7670_init_tb (); /* this is automatically generated */
reg clk;
localparam device_id = 8'b0100_0010;
localparam clk_period = 20;
reg rst_n;
reg init_en;
wire scl;
wire sda;
wire init_done;
pullup(sda);
ov7670_init inst_ov7670_init (
.clk(clk),
.rst_n(rst_n),
.scl(scl),
.sda(sda),
.init_done(init_done)
);
initial clk = 0;
always #(clk_period/2) clk = ~clk;
initial begin
#2;
rst_n = 0;
init_en = 0;
#(clk_period*20);
rst_n = 1;
#(clk_period*10);
init_en = 1;
#clk_period;
init_en = 0;
#(clk_period*15000*168);
$stop;
end
endmodule
仿真图如下:
(1)单个寄存器配置SCCB发送
(2)整体配置如图
通过对OV7670摄像头进行配配置后,在开发板上下板测试,使用quartus在线逻辑分析仪功能读取摄像头输出口数据,查看接收情况。第一次没有对RESET和PWDN信号进行操作,输出的像素时钟PCLK一直拉低,当把RESET拉高,PWDN拉低时,能正常接收到数据。如下图在线逻辑分析仪显示的接收结果:
(1)缩小图
(2)细节图
可以看到数据在行同步信号为高时,正常输出数据,和数据手册中输出形式一样。