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



