SCCB 配置好后,cmos 的 DVP 端口就有數據出來了,怎么設計時序獲取圖像呢?
一、DVP端口
cmos_capture u_cmos_capture ( .clk_24m (clk_24m ), .cmos_pclk (cmos_pclk ), .rst_n (rst_n & sccb_cfg_done ), .cmos_vsync (cmos_vsync ), .cmos_href (cmos_href ), .cmos_data (cmos_data ), .frame_vsync ( ), .frame_hsync ( ), .rgb565_vld ( ), // 640x480 .rgb565_data ( ), .rgb_vld (rgb_vld ), // 480x272 .rgb_data (rgb_data ), .fps_rate (fps_rate ) );
端口的輸出包括:圖像數據,圖像數據使能,fps數值。其中數據和使能又分為原版 640x480 和 裁剪后的 480x272。(如果是上一講配置的ov5640則為1024x768→480x272)
二、去除前10幀
根據數據手冊說的,前10幀圖像數據不穩定,因此一般都是丟掉。代碼設計很簡單,找到 cmos_vsync 的上升沿計數,只計一次,計滿 10 幀后的數據才是有效數據,如下所示:
//========================================================================== //== 打拍,以供后面程序使用 //========================================================================== always @(posedge cmos_pclk or negedge rst_n) begin if(!rst_n) begin cmos_vsync_r1 <= 1'b0; cmos_vsync_r2 <= 1'b0; cmos_href_r1 <= 1'b0; cmos_href_r2 <= 1'b0; end else begin cmos_vsync_r1 <= cmos_vsync; cmos_vsync_r2 <= cmos_vsync_r1; cmos_href_r1 <= cmos_href; cmos_href_r2 <= cmos_href_r1; end end //========================================================================== //== 前10幀圖像數據不穩定,丟棄掉 //========================================================================== //vsync上升沿 //--------------------------------------------------- assign cmos_vsync_pos = (~cmos_vsync_r1 & cmos_vsync); //幀有效信號,去除前10幀 //--------------------------------------------------- always @(posedge cmos_pclk or negedge rst_n) begin if(!rst_n) begin frame_cnt <= 'd0; end else if(cmos_vsync_pos && frame_vld==1'b0) begin frame_cnt <= frame_cnt + 1'b1; end end assign frame_vld = (frame_cnt >= WAIT) ? 1'b1 : 1'b0;
其中,WAIT的數值為 10,frame_vld 為有效數據指示信號,前10幀時為低,之后一直為高。
三、數據拼接
ov7725 的數據手冊中有如下一張圖,說明了配置成 RGB565 時的時序情況。攝像頭的數據為 8bit,是按照 RAW 像素設計的,當我們配置成 RGB565 格式輸出時,需要兩個 8bit 才能表示一個 16bit 的 RGB565 像素,因此需要對輸出的像素進行人為的拼接,典型的時序如下所示:
RGB565的排列順序也是通過寄存器設置的,也有別的排列順序,我這設置的就是如圖的格式。時序設計如下所示:
我們設計一個指示信號 byte_flag,不斷的對原始數據 ov_5640_data 進行拼接,並設計 vld 信號表明輸出有效標志。這個時序圖是我之前學習開源騷客的教學視頻時畫的,和我下面貼出的代碼信號名字有一點點不一樣,但大體是相同的。代碼如下所示:
//========================================================================== //== 兩個原始數據拼成一個RGB565像素 //========================================================================== //字節指示 //--------------------------------------------------- always @(posedge cmos_pclk or negedge rst_n) begin if(!rst_n) begin byte_flag <= 1'b0; end else if(cmos_href) begin byte_flag <= ~byte_flag; end else begin byte_flag <= 1'b0; end end //rgb_data //--------------------------------------------------- always @(posedge cmos_pclk or negedge rst_n) begin if(!rst_n) begin rgb565_data <= 'h0; end else if(byte_flag == 1'b0) begin //first byte rgb565_data <= {cmos_data, rgb565_data[7:0]}; end else if(byte_flag == 1'b1) begin //second byte rgb565_data <= {rgb565_data[15:8], cmos_data}; end end //rgb_vld //--------------------------------------------------- always @(posedge cmos_pclk or negedge rst_n) begin if(!rst_n) begin rgb565_vld <= 1'b0; end else if(frame_vld && byte_flag) begin rgb565_vld <= 1'b1; end else begin rgb565_vld <= 1'b0; end end
四、行場有效信號
經過處理,行場信號需要改變才能和處理后的數據完美對齊,代碼如下所示:
//========================================================================== //== 輸出行場有效信號 //========================================================================== assign frame_vsync = frame_vld ? cmos_vsync_r2 : 1'b0; assign frame_hsync = frame_vld ? cmos_href_r2 : 1'b0;
五、分辨率裁剪
上一講博客中提到,可以通過寄存器的配置改變輸出的圖像分辨率,但寄存器的配置需要查詢 datasheet 再計算得結果,比較麻煩。我們可以通過簡單的計數器,實現分辨率裁剪的效果。以 640x480 裁剪為 480x272 為例,取中間的部分,邊上的舍去,代碼如下所示:
parameter H_START = 12'd79 ; //裁剪后的寬度起始像素 parameter H_STOP = 12'd559 ; //裁剪后的寬度結束像素 parameter V_START = 12'd103 ; //裁剪后的高度起始像素 parameter V_STOP = 12'd375 ; //裁剪后的高度結束像素 //========================================================================== //== 分辨率裁剪:640x480 -> 480x272 //========================================================================== //行計數 //--------------------------------------------------- always @(posedge cmos_pclk or negedge rst_n) begin if(!rst_n) cnt_h <= 'd0; else if(add_cnt_h) begin if(end_cnt_h) cnt_h <= 'd0; else cnt_h <= cnt_h + 1'b1; end end assign add_cnt_h = rgb565_vld; assign end_cnt_h = add_cnt_h && cnt_h== 640-1; //場計數 //--------------------------------------------------- always @(posedge cmos_pclk or negedge rst_n) begin if(!rst_n) cnt_v <= 'd0; else if(add_cnt_v) begin if(end_cnt_v) cnt_v <= 'd0; else cnt_v <= cnt_v + 1'b1; end end assign add_cnt_v = end_cnt_h; assign end_cnt_v = add_cnt_v && cnt_v== 480-1; //裁剪后的數據:適配TFT屏 //--------------------------------------------------- assign rgb_data = rgb565_data; assign rgb_vld = rgb565_vld && (cnt_h >= H_START) && (cnt_h < H_STOP) && (cnt_v >= V_START) && (cnt_v < V_STOP);
這樣得到的 rgb_data 和 rgb_vld 就是裁剪后的數據了,且只是裁剪,數據本身沒有移位,總體時序沒有變化。
六、幀率fps計算
通過巧妙的設計,我們就能夠實時的得到當前 fps 的數值。其思想很簡單,即計算 1s 時間內,來了多少次 cmos_vsync 即可。具體設計思想如下所示:
(1)通過一個確定的時鍾對 cmos_vsync 進行打拍,求得其上升沿;
(2)通過該確定的時鍾再進行 1s 時間的計數,每當計滿 1s 則清0重新計數;
(3)對(1)求得的 cmos_vsync 上升沿進行計數,每當(2)的 1s 計滿時,清 0 重新計數;
(4)對(3)在 1s 計滿時那一刻的 cmos_vsync 上升沿數目寄存並輸出,得到 fps 值。
這樣看文字比較麻煩,好像很難一樣,上代碼吧:
//========================================================================== //== 幀率計算,不能用pclk時鍾,需重新捕捉vsync_pos //========================================================================== //vsync上升沿 //--------------------------------------------------- always @(posedge clk_24m or negedge rst_n) begin if(!rst_n) frame_vsync_r <= 1'b0; else frame_vsync_r <= frame_vsync; end assign frame_vsync_pos = (~frame_vsync_r & frame_vsync); //1s時間 //--------------------------------------------------- always @(posedge clk_24m or negedge rst_n) begin if(!rst_n) cnt_1s <= 'd0; else if(add_cnt_1s) begin if(end_cnt_1s) cnt_1s <= 'd0; else cnt_1s <= cnt_1s + 1'b1; end end assign add_cnt_1s = frame_vld; assign end_cnt_1s = add_cnt_1s && cnt_1s== TIME_1S-1; //1s時間內的vsync次數 //--------------------------------------------------- always @(posedge clk_24m or negedge rst_n) begin if(!rst_n) cnt_fps <= 'd0; else if(end_cnt_1s) begin cnt_fps <= 'd0; end else if(frame_vld && frame_vsync_pos)begin cnt_fps <= cnt_fps + 'd1; end end //實時更新幀率值 //--------------------------------------------------- always @(posedge clk_24m or negedge rst_n) begin if(!rst_n) begin fps_rate <= 'd0; end else if(end_cnt_1s) begin fps_rate <= cnt_fps; end end
注釋中特別提到,不能用 Pclk 來計算,必須是外部引入的確定的時鍾。這點在 CrazyBingo 韓彬的代碼中沒有處理好,算是一個小 bug。最后我們將幀率值 fps_rate 引到端口,輸送給數碼管顯示模塊,就能夠實時的知道當前采集的圖像幀率值了。
至此,攝像頭模塊的部分算是講解完了,講得很粗,大把的貼代碼。一是因為網上關於這方面的資料是在是太多了,講得都比我總結的好,二是這些代碼其實大部分都是我改的別家的,不算原創,也不盈利,所以貼出來。
還沒有結束,下一講我再整理一下攝像頭顯示工程中,攝像頭以外的一些關鍵點。
參考資料:
[1]正點原子FPGA教程
[2]小梅哥《OV5640圖像采集從原理到應用》
[3]開源騷客《SDRAM那些事兒》
[4]韓彬, 於瀟宇, 張雷鳴. FPGA設計技巧與案例開發詳解[M]. 電子工業出版社, 2014.