基于FPGA的图像边缘检测(OV5640)

一、简介

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 高斯滤波算法实现步骤

详细见FPGA丨高斯滤波算法实现

高斯滤波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个:pclkclk_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.摄像头配置模块仿真

2.图像采集模块仿真

3.图像处理模块仿真

3.1 灰度化仿真

3.2 高斯滤波仿真

3.3 二值化仿真

3.4 sobel仿真

4.sdram读写控制模块仿真

相关推荐

  1. 基于边缘检测和HSV图像识别算法

    2024-07-13 10:14:02       17 阅读

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-07-13 10:14:02       67 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-07-13 10:14:02       72 阅读
  3. 在Django里面运行非项目文件

    2024-07-13 10:14:02       58 阅读
  4. Python语言-面向对象

    2024-07-13 10:14:02       69 阅读

热门阅读

  1. 怎么知道服务器100M带宽可以支持多少人访问?

    2024-07-13 10:14:02       27 阅读
  2. [AI 大模型] Meta LLaMA-2

    2024-07-13 10:14:02       28 阅读
  3. Oracle逻辑备份

    2024-07-13 10:14:02       23 阅读
  4. c#视觉应用开发中如何在C#中处理图像噪声?

    2024-07-13 10:14:02       28 阅读
  5. 【ceph】ceph-mon重新选举的情况

    2024-07-13 10:14:02       27 阅读
  6. SpringBoot配置Swagger开启页面访问限制

    2024-07-13 10:14:02       25 阅读
  7. MFC常用数据类型类:CRect

    2024-07-13 10:14:02       26 阅读
  8. noi.openjude1.5 26统计满足条件的4位数个数

    2024-07-13 10:14:02       19 阅读
  9. MYSQL

    MYSQL

    2024-07-13 10:14:02      19 阅读
  10. Vue.js Ajax(axios)

    2024-07-13 10:14:02       20 阅读
  11. 开源项目有哪些机遇与挑战?

    2024-07-13 10:14:02       20 阅读
  12. Spring Boot集成Atomix快速入门Demo

    2024-07-13 10:14:02       24 阅读
  13. Python实现网站IP地址查询

    2024-07-13 10:14:02       20 阅读
  14. parquet-go的CSVWriter

    2024-07-13 10:14:02       27 阅读
  15. 玩转鸿蒙NXET之组件导航与路由跳转二

    2024-07-13 10:14:02       23 阅读
  16. Go语言入门之数组切片

    2024-07-13 10:14:02       28 阅读