一、OV5640输出图像处理
前面说过,选择 RGB565 格式输出,2 个RAW像素合成 1 个 RGB565 像素,但摄像头出来的 RGB565 像素不是单个的,而仍然1个RGB565像素用 2 个字节表示。因此,需要我们手动将两个字节拼接成一个RGB565像素。
时序设计如下:
用 ov5640_href 来生成 byte_flag,再用 byte_flag 来拼接像素和生成 vld 信号。
此外,在 OV5640 数据手册没有写明一个事情,但在 OV7725 数据手册中提到过,摄像头出来的前 10 帧数据不稳定,需要丢弃,因此在上述的时序上,还得计数前 10 帧图像数据,并丢弃它。
1 module ov5640_data 2 //========================< 端口 >========================================== 3 ( 4 //system -------------------------------------------- 5 input wire rst_n , 6 //OV5640 -------------------------------------------- 7 input wire ov5640_pclk , 8 input wire ov5640_href , 9 input wire ov5640_vsync , 10 input wire [ 7:0] ov5640_data , 11 //User ---------------------------------------------- 12 output reg [15:0] rgb_data , 13 output reg rgb_vld 14 ); 15 //========================< 信号 >========================================== 16 reg byte_flag ; // 0: first byte, 1: second byte 17 wire ov5640_vsync_pos ; 18 reg ov5640_vsync_r ; 19 reg [ 3:0] frame_cnt ; 20 wire frame_vaild ; 21 22 //========================================================================== 23 //== RGB564信号产生,两个byte合成一个rgb像素 24 //========================================================================== 25 //字节指示 26 //--------------------------------------------------- 27 always @(posedge ov5640_pclk or negedge rst_n) begin 28 if(!rst_n) begin 29 byte_flag <= 1'b0; 30 end 31 else if(ov5640_href) begin 32 byte_flag <= ~byte_flag; 33 end 34 else begin 35 byte_flag <= 1'b0; 36 end 37 end 38 39 //rgb_data 40 //--------------------------------------------------- 41 always @(posedge ov5640_pclk or negedge rst_n) begin 42 if(!rst_n) begin 43 rgb_data <= 'h0; 44 end 45 else if(byte_flag == 1'b0) begin //first byte 46 rgb_data <= {ov5640_data, rgb_data[7:0]}; 47 end 48 else if(byte_flag == 1'b1) begin //second byte 49 rgb_data <= {rgb_data[15:8], ov5640_data}; 50 end 51 end 52 53 //========================================================================== 54 //== 丢弃前10帧图像 55 //========================================================================== 56 //ov_5640 vsync 上升沿检测 57 //--------------------------------------------------- 58 always @(posedge ov5640_pclk) begin 59 ov5640_vsync_r <= ov5640_vsync; 60 end 61 62 assign ov5640_vsync_pos = ov5640_vsync & ~ov5640_vsync_r; 63 64 //帧有效信号,去除前10帧 65 //--------------------------------------------------- 66 always @(posedge ov5640_pclk or negedge rst_n) begin 67 if(!rst_n) begin 68 frame_cnt <= 'd0; 69 end 70 else if(frame_vaild==1'b0 && ov5640_vsync_pos) begin 71 frame_cnt <= frame_cnt + 1'b1; 72 end 73 end 74 75 assign frame_vaild = (frame_cnt >= 'd10) ? 1'b1 : 1'b0; 76 77 //========================================================================== 78 //== rgb_vld 79 //========================================================================== 80 always @(posedge ov5640_pclk or negedge rst_n) begin 81 if(!rst_n) begin 82 rgb_vld <= 1'b0; 83 end 84 else if(frame_vaild && byte_flag) begin 85 rgb_vld <= 1'b1; 86 end 87 else begin 88 rgb_vld <= 1'b0; 89 end 90 end 91 92 93 94 95 endmodule
二、SDRAM缓存处理
先假设 OV5640 和 VGA 直接相连,中间不采用任何缓存器件,那会出现什么情况呢?
OV5640的帧率为 30fps, VGA 的帧率为 60fps,且时钟也完全不一样。假设分辨率是 3x2 = 6,数据为:1,2,3,4,5,6。OV5640 帧率小,生成一个像素的时间假设是2秒,而 VGA 帧率大,需要一个像素的时间假设是 1s。在 6s 的时间里,VGA就请求了一帧数据,可这个时间里 OV5640 只生成了1,2,3共三个像素,VGA那边就出现了空数据的情况。在 12s 的时间里,VGA 请求了两帧数据,可这个时间里 OV5640 只生成了一帧数据,怎么来说都是错的。上述假设是简化了时间点,实际情况是 OV5640 生成像素和 VGA 请求像素的时间不一致,导致VGA总是要不到一帧图像正确的数据,也要不全一帧图像正确的数据。因此,OV5640 和 VGA 之间需要加入一个缓存器件,用于缓存一帧的数据,这样 VGA 每次要一帧数据都可以被满足。加入缓存器件的根本原因就是两端时钟不一致,导致读写速率不一致。
那采用什么缓存器件呢?FIFO 和 RAM 是我们最常用的,但是这里一帧的图像数据往往是 640x480、1280x768、1920x1280,每个像素是 16bit,数据量大,FPGA的片内资源无法满足。因此需要引入外部缓存器件,如 SDRAM、DDR2、DDR3等。
三、乒乓操作
1、无乒乓操作
上面的那种情况显然考虑不周到,除非一帧图像是瞬间产生完,OV5640帧率30fps,VGA帧率60fps,仿佛刚好写一帧可以读两帧。但是,一帧图像实际情况是一行行的生成和读取的,所以会出现 VGA 从SDRAM处读的上半帧是新帧,由于而SDRAM缓存的下半帧还没有被 OV5640 写完,VGA 从SDRAM处读的下半帧还是老帧!示意图如下所示,红线为写,黑线为读。
将上述 5 帧图像生成的时间点编号为 t1、t2、t3、t4、t5。在 t1、t3、t5 时刻图像都是残缺帧(新老帧参半),在 t2、t4 时刻图像才是完整的一帧,而 VGA 那边可不管,每个时间点都会要一帧图像,这就是错帧现象。而解决错帧现象的方法则是乒乓操作。
2、乒乓操作
乒乓操作,使用两个缓存区,写缓存区 1 时读缓存区 2,写缓存区 2 时读缓存区 1,读写交替。示意图如下,红线为写,黑线为读。
由示意图可以看出,乒乓操作中,每次读的(黑线)都是完整的帧,每一帧读 2 次,这样便没有出现读残缺帧的现象,解决了错帧问题。
3、乒乓操作设计
在上述设计中,由于写读速率为1:2,所以写一帧读两帧即可,但是很多时候写读速率的比例是其他数值,那怎么办呢?
(1)写端在缓存区1写完一帧数据就切换到缓存区2写下一帧,写完后又切换回缓存区1写再下一帧,如此反复。
(2)读端在缓存区2读完一帧:
①如果写端仍然在缓存区1,则读端不切换缓存区,而是继续在缓存区2重读一帧。
②如果写端离开了缓存区1,即切换到缓存区2了,说明写端已经在缓存区1写完了,则读端可以切换到缓存区1。这样就能保证读端每次读的都是完整的帧,就算有一小段时间是读写端都在同一个缓存区,但是由于写慢读快,写是不会追上读的,读还是能读完旧帧,之后旧帧才被新帧覆盖。
写慢读快这样设计就行,那如果写快读慢呢?其实也是一样的,无论读写谁快谁慢,都是快端照顾慢端,即慢端一帧结束就离开自身缓存区到另一缓存区。快端则一帧结束后,先检测慢端是否离开自身缓存区,是则切换过去,否则不切换过去,重新在本缓存区再工作一帧。示意图如下所示:
4、SDRAM乒乓操作
用两片SDRAM来进行乒乓操作有些太浪费了,可以直接利用SDRAM里面的bank进行乒乓操作即可。虽然读写只有一根总线,但是SDRAM时钟高,读写足够快,可以满足乒乓操作的需求。
参考资料:[1]开源骚客《SDRAM那些事儿》
[2]威三学院FPGA教程