Tang Nano 4K摄像头驱动例程与原理讲解 解决显示问题
介绍
Tang Nano 4K是基于高云半导体的小蜜蜂系列 GW1NSR-LV4C 设计的简约型开发板。开发板设计小巧精致,将芯片的所有资源都引出,板载Type-C、USB-JTAG、DVP、HDMI座子及其电路等,并把所有IO资源引出,方便开发者拓展使用,非常适用于小型数字逻辑的设计和实验。
原理图分析
由于我们要做的是从摄像头读取数据,然后显示到HDMI上,所以重点关注相关部分原理图
首先是摄像头部分,看得出来是一个非常标准的Ref设计拉过来的,意味着通用的OV2640等Demo是可以直接移植使用的。
然后是HDMI部分,电源部分居然额外加了磁珠。
Demo分析
从官方https://github.com/sipeed/TangNano-4K-example 把工程Git了下来,这里安装Git和Gowin的部分省略。
首先我们需要打开这个摄像头的工程,由于实际测试发现直接使用Demo会无法在显示屏上显示图像。
分析有以下两个原因:
1、烧录后,不掉电复位只是对FPGA进行复位,导致复位不完全。
2、时序问题,或者烧录失败。
直接编译布局布线是通过了,并且显示是绿色的,分析发现资源也是够用的
但是仔细一看,存在部分时序是不满足的(PS,我并没有在哪里找到多种工况)
尤其是这个像素时钟不满足,还是会导致HDMI和摄像头相关逻辑出问题。仔细一看FalsePath,差额还比较大,考虑到资源使用情况不大,应当是这些时钟驱动不动了。
那么怎么解决呢?一般来说,如果需求确定的情况下,无法几种解决方法:1.氪金战士,更换性能更强,工艺更高的器件。2.删除不必要的逻辑。3.打拍。4.回家吃饭
这里我们选择2,把那个测试输出部分删了就行,咱们是要摄像头显示到屏幕的。
我把没用的部分删除了,然后添加了部分注释,帮助读者理解
module video_top ( input I_clk, // 输入时钟信号 27MHz input I_rst_n, // 输入复位信号 output [1:0] O_led, // 输出LED信号 inout SDA, // SCCB接口数据线 inout SCL, // SCCB接口时钟线 input VSYNC, // 输入垂直同步信号 input HREF, // 输入水平参考信号 input [9:0] PIXDATA, // 输入像素数据 input PIXCLK, // 输入像素时钟 output XCLK, // 输出时钟信号 output [0:0] O_hpram_ck, // 输出Hyperram时钟 output [0:0] O_hpram_ck_n, // 输出Hyperram时钟取反 output [0:0] O_hpram_cs_n, // 输出Hyperram片选信号取反 output [0:0] O_hpram_reset_n, // 输出Hyperram复位信号取反 inout [7:0] IO_hpram_dq, // 输入/输出Hyperram数据线 inout [0:0] IO_hpram_rwds, // 输入/输出Hyperram读写使能信号 output O_tmds_clk_p, // 输出TMDS时钟正极性信号 output O_tmds_clk_n, // 输出TMDS时钟负极性信号 output [2:0] O_tmds_data_p, // 输出TMDS数据正极性信号({r,g,b}) output [2:0] O_tmds_data_n, // 输出TMDS数据负极性信号({r,g,b}) input key // 输入按键信号 ); //================================================== reg [31:0] run_cnt; // 运行计数器 wire running; // 运行标志位 //-------------------------- wire tp0_vs_in; // TP0垂直同步输入 wire tp0_hs_in; // TP0水平同步输入 wire tp0_de_in; // TP0数据使能输入 //-------------------------- reg [ 9:0] pixdata_d1; // 上一个像素数据 reg hcnt; // 水平计数器 wire [15:0] cam_data; // 摄像头数据 //------------------------- // 帧缓冲器输入 wire ch0_vfb_clk_in; // 通道0帧缓冲器时钟输入 wire ch0_vfb_vs_in; // 通道0帧缓冲器垂直同步输入 wire ch0_vfb_de_in; // 通道0帧缓冲器数据使能输入 wire [15:0] ch0_vfb_data_in; // 通道0帧缓冲器数据输入 //------------------- // 同步码 wire syn_off0_re; // 偏移同步码读使能信号 wire syn_off0_vs; // 偏移同步码垂直同步信号 wire syn_off0_hs; // 偏移同步码水平同步信号 wire off0_syn_de; // 偏移同步码数据使能信号 wire [15:0] off0_syn_data; // 偏移同步码数据 //------------------------------------- // Hyperram wire dma_clk; // DMA时钟信号 wire memory_clk; // 存储器时钟信号 wire mem_pll_lock; // 存储器PLL锁定信号 //------------------------------------------------- // 存储器接口 wire cmd; // 存储器命令信号 wire cmd_en; // 存储器命令使能信号 wire [21:0] addr; // 存储器地址 wire [31:0] wr_data; // 存储器写数据 wire [ 3:0] data_mask; // 存储器数据掩码 wire rd_data_valid; // 存储器读数据有效信号 wire [31:0] rd_data; // 存储器读数据 wire init_calib; // 初始化校准信号 //------------------------------------------ // RGB数据 wire rgb_vs; // RGB垂直同步信号 wire rgb_hs; // RGB水平同步信号 wire rgb_de; // RGB数据使能信号 wire [23:0] rgb_data; // RGB数据 //------------------------------------ // HDMI TX wire serial_clk; // 串行时钟信号 wire pll_lock; // PLL锁定信号 wire hdmi_rst_n; // HDMI复位信号 wire pix_clk; // 像素时钟信号 wire clk_12M; // 12MHz时钟信号 //=================================================== // LED测试 always @(posedge I_clk or negedge sys_resetn) begin if (!sys_resetn) run_cnt <= 32'd0; else if (run_cnt >= 32'd27_000_000) run_cnt <= 32'd0; else run_cnt <= run_cnt + 1'b1; end assign running = (run_cnt < 32'd13_500_000) ? 1'b1 : 1'b0; // 如果运行计数小于一半,设置运行标志位 assign O_led[0] = running; // LED0显示运行状态 assign O_led[1] = ~init_calib; // LED1显示初始化校准状态 assign XCLK = clk_12M; // 设置输出时钟信号 // 相机复位 Reset_Sync u_Reset_Sync ( .resetn(sys_resetn), .ext_reset(I_rst_n & pll_lock), .clk(I_clk) ); //============================================================================== OV2640_Controller u_OV2640_Controller ( .clk (clk_12M), // 24Mhz时钟信号 .resend (1'b0), // 复位信号 .config_finished(), // 配置完成标志 .sioc (SCL), // SCCB接口 - 时钟信号 .siod (SDA), // SCCB接口 - 数据信号 .reset (), // OV7670复位信号 .pwdn () // OV7670电源控制信号 ); always @(posedge PIXCLK or negedge sys_resetn) begin if (!sys_resetn) pixdata_d1 <= 10'd0; else pixdata_d1 <= PIXDATA; end always @(posedge PIXCLK or negedge sys_resetn) begin if (!sys_resetn) hcnt <= 1'd0; else if (HREF) hcnt <= ~hcnt; else hcnt <= 1'd0; end // assign cam_data = {pixdata_d1[9:5],pixdata_d1[4:2],PIXDATA[9:7],PIXDATA[6:2]}; //RGB565 // assign cam_data = {PIXDATA[9:5],PIXDATA[4:2],pixdata_d1[9:7],pixdata_d1[6:2]}; //RGB565 assign cam_data = {PIXDATA[9:5], PIXDATA[9:4], PIXDATA[9:5]}; // RAW10 //============================================== // 数据宽度16位 assign ch0_vfb_clk_in = key_flag ? I_clk : PIXCLK; assign ch0_vfb_vs_in = key_flag ? ~tp0_vs_in : VSYNC; // 取反 assign ch0_vfb_de_in = key_flag ? tp0_de_in : HREF; // hcnt; assign ch0_vfb_data_in = cam_data; // RGB565 key_flag key_flag_inst ( .clk(I_clk), .rst_n(I_rst_n), .key(key), .key_flag(key_flag) ); //===================================================== // SRAM 控制模块 Video_Frame_Buffer_Top Video_Frame_Buffer_Top_inst ( .I_rst_n (init_calib), // 复位信号 .I_dma_clk (dma_clk), // 存储器时钟信号 .I_wr_halt (1'd0), // 写入暂停,0表示不暂停 .I_rd_halt (1'd0), // 读取暂停,0表示不暂停 // 视频数据输入 .I_vin0_clk (ch0_vfb_clk_in), .I_vin0_vs_n (ch0_vfb_vs_in), .I_vin0_de (ch0_vfb_de_in), .I_vin0_data (ch0_vfb_data_in), .O_vin0_fifo_full (), // 视频数据输出 .I_vout0_clk (pix_clk), .I_vout0_vs_n (~syn_off0_vs), .I_vout0_de (syn_off0_re), .O_vout0_den (off0_syn_de), .O_vout0_data (off0_syn_data), .O_vout0_fifo_empty(), // DDR写请求 .O_cmd (cmd), .O_cmd_en (cmd_en), .O_addr (addr), //[ADDR_WIDTH-1:0] .O_wr_data (wr_data), //[DATA_WIDTH-1:0] .O_data_mask (data_mask), .I_rd_data_valid (rd_data_valid), .I_rd_data (rd_data), //[DATA_WIDTH-1:0] .I_init_calib (init_calib) ); //================================================ // HyperRAM ip GW_PLLVR GW_PLLVR_inst ( .clkout(memory_clk), // 输出clkout .lock (mem_pll_lock), // 输出锁定信号 .clkin (I_clk) // 输入时钟信号 ); HyperRAM_Memory_Interface_Top HyperRAM_Memory_Interface_Top_inst ( .clk (I_clk), .memory_clk (memory_clk), .pll_lock (mem_pll_lock), .rst_n (sys_resetn), // 复位信号 .O_hpram_ck (O_hpram_ck), .O_hpram_ck_n (O_hpram_ck_n), .IO_hpram_rwds (IO_hpram_rwds), .IO_hpram_dq (IO_hpram_dq), .O_hpram_reset_n(O_hpram_reset_n), .O_hpram_cs_n (O_hpram_cs_n), .wr_data (wr_data), .rd_data (rd_data), .rd_data_valid (rd_data_valid), .addr (addr), .cmd (cmd), .cmd_en (cmd_en), .clk_out (dma_clk), .data_mask (data_mask), .init_calib (init_calib) ); //================================================ wire out_de; syn_gen syn_gen_inst ( .I_pxl_clk (pix_clk), //40MHz //65MHz //74.25MHz .I_rst_n (hdmi_rst_n), //800x600 //1024x768 //1280x720 .I_h_total (16'd1650), // 16'd1056 // 16'd1344 // 16'd1650 .I_h_sync (16'd40), // 16'd128 // 16'd136 // 16'd40 .I_h_bporch(16'd220), // 16'd88 // 16'd160 // 16'd220 .I_h_res (16'd1280), // 16'd800 // 16'd1024 // 16'd1280 .I_v_total (16'd750), // 16'd628 // 16'd806 // 16'd750 .I_v_sync (16'd5), // 16'd4 // 16'd6 // 16'd5 .I_v_bporch(16'd20), // 16'd23 // 16'd29 // 16'd20 .I_v_res (16'd720), // 16'd600 // 16'd768 // 16'd720 .I_rd_hres (16'd640), .I_rd_vres (16'd480), .I_hs_pol (1'b1), //HS polarity , 0:负极性,1:正极性 .I_vs_pol (1'b1), //VS polarity , 0:负极性,1:正极性 .O_rden (syn_off0_re), .O_de (out_de), .O_hs (syn_off0_hs), .O_vs (syn_off0_vs) ); localparam N = 2; //delay N clocks reg [N-1:0] Pout_hs_dn; reg [N-1:0] Pout_vs_dn; reg [N-1:0] Pout_de_dn; always @(posedge pix_clk or negedge hdmi_rst_n) begin if (!hdmi_rst_n) begin Pout_hs_dn <= {N{1'b1}}; Pout_vs_dn <= {N{1'b1}}; Pout_de_dn <= {N{1'b0}}; end else begin Pout_hs_dn <= {Pout_hs_dn[N-2:0], syn_off0_hs}; Pout_vs_dn <= {Pout_vs_dn[N-2:0], syn_off0_vs}; Pout_de_dn <= {Pout_de_dn[N-2:0], out_de}; end end //============================================================================== //TMDS TX assign rgb_data = off0_syn_de ? {off0_syn_data[15:11],3'd0,off0_syn_data[10:5],2'd0,off0_syn_data[4:0],3'd0} : 24'h0000ff;//{r,g,b} assign rgb_vs = Pout_vs_dn[N-1]; //syn_off0_vs; assign rgb_hs = Pout_hs_dn[N-1]; //syn_off0_hs; assign rgb_de = Pout_de_dn[N-1]; //off0_syn_de; TMDS_PLLVR TMDS_PLLVR_inst ( .clkin (I_clk) //input clk , .clkout (serial_clk) //output clk , .clkoutd(clk_12M) //output clkoutd , .lock (pll_lock) //output lock ); assign hdmi_rst_n = sys_resetn & pll_lock; CLKDIV u_clkdiv ( .RESETN(hdmi_rst_n), .HCLKIN(serial_clk) //clk x5 , .CLKOUT(pix_clk) //clk x1 , .CALIB(1'b1) ); defparam u_clkdiv.DIV_MODE = "5"; DVI_TX_Top DVI_TX_Top_inst ( .I_rst_n (hdmi_rst_n), //asynchronous reset, low active .I_serial_clk (serial_clk), .I_rgb_clk (pix_clk), //pixel clock .I_rgb_vs (rgb_vs), .I_rgb_hs (rgb_hs), .I_rgb_de (rgb_de), .I_rgb_r (rgb_data[23:16]), .I_rgb_g (rgb_data[15:8]), .I_rgb_b (rgb_data[7:0]), .O_tmds_clk_p (O_tmds_clk_p), .O_tmds_clk_n (O_tmds_clk_n), .O_tmds_data_p(O_tmds_data_p), //{r,g,b} .O_tmds_data_n(O_tmds_data_n) ); endmodule module Reset_Sync ( input clk, input ext_reset, output resetn ); reg [3:0] reset_cnt = 0; always @(posedge clk or negedge ext_reset) begin if (~ext_reset) reset_cnt <= 4'b0; else reset_cnt <= reset_cnt + !resetn; end assign resetn = &reset_cnt; endmodule module key_flag #( parameter clk_frequency = 27_000_000, parameter io_num = 1 ) ( input clk, // Clock in input rst_n, input key, output key_flag ); parameter count_ms = clk_frequency / 1000; parameter count_20ms = count_ms * 20 - 1; parameter count_500ms = count_ms * 500 - 1; reg [($clog2(count_20ms)-1)+10:0] count_20ms_reg; reg [$clog2(count_500ms)-1:0] count_500ms_reg; // key flag reg key_input = 1'd1; always @(posedge clk) begin key_input <= ~key; end reg key_flag; always @(posedge clk or negedge rst_n) begin if (!rst_n) count_20ms_reg <= 'd0; else if (key_input) count_20ms_reg <= count_20ms_reg + 'd1; else if (count_20ms_reg >= count_20ms) begin key_flag = ~key_flag; count_20ms_reg <= 'd0; ; end else count_20ms_reg <= 'd0; ; end endmodule
然后就是约束文件也得修改,参考高云官方的设计时序约束手册,说真的,连示例都没有,搞起来真费劲,易用性和兼容性有待提高。
我写了多年的新思和赛灵思系列产品的约束,如PT和Vivado这些,写高云这个动不动就莫名其妙的报错。。。确实让人头疼和莫名其妙。
修改之后重新编译,资源使用也少了,时序也够用了↓
最后烧录上板测试,通过!接下来我们将在这块开发板上介绍图像处理相关应用。
为了方便大家进行测试,可以直接使用本压缩包的工程进行烧录:camera_hdmi_mdf.zip