H.264视频编解码的FPGA源码分析(一)输入数据分析

概要

本文的源码基于复旦大学的开源芯片—开源H.265/H.264视频编码器项目,本文的工作主要是在梳理源码的同时学习H.264视频编解码的原理及其硬件实现。

输入数据

CSDN的编辑器么有verilog格式…是看不起我HDL了咩!(狗头),所以暂时用C的格式代替吧,感觉好看一点(颜控昂)

读取YUV格式的视频数据,放在pixel_ram

$readmemh(YUV_FILE, pixel_ram);

变量的定义:

reg [31:0]	 					addr_r, cnt;
reg [31:0]   					pixel_ram[1<<25:0];  // 1左移25位

这里可以稍微计算一下,pixel_ram的容量是2^25+1,即4MB
实际上使用的文件的大小:
在这里插入图片描述
寄存器数组pixel_ram中的数据读取:

always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
		rdata_i  <= 'b0;
		rvalid_i <= 1'b0;
		addr_r   <= 'b0;
    end 
    else if (rinc_o && cnt!='d48) begin
		rdata_i  <= {pixel_ram[2*addr_r+0], pixel_ram[2*addr_r+1]};
		rvalid_i <= 1'b1;
		addr_r   <= addr_r+1;
    end
    else begin
		rdata_i  <= 'b0;
		rvalid_i <= 1'b0;
		addr_r   <= addr_r;
    end  
end

rinc_o相当于一个计数使能信号,cnt计数到48就清零一次
每次读取两个地址的数据,也就是64bit,赋值给rdata_i

top u_top     (
				.clk      			( clk      			),
				.rst_n    			( rst_n    			),
				
				.sys_start			( sys_start		    ),      
				.sys_done			( sys_done		    ),		
				.sys_intra_flag		( sys_intra_flag	), 
				.sys_qp				( sys_qp			),         
				.sys_mode			( sys_mode		    ),       
				.sys_x_total		( sys_x_total	    ),    
				.sys_y_total		( sys_y_total	    ),	
				
				.enc_ld_start		( enc_ld_start		),			
				.enc_ld_x           ( enc_ld_x    		),	
				.enc_ld_y           ( enc_ld_y          ),
				
				.rdata_i  			( rdata_i  			),  // 数据输入
				.rvalid_i 			( rvalid_i 			),
				.rinc_o   			( rinc_o   			),
				.wdata_o  			( wdata_o  			),
				.wfull_i  			( wfull_i  			),
				.winc_o	  			( winc_o	 		),
				
				.ext_mb_x_o 		( ext_mb_x 			),         
				.ext_mb_y_o 		( ext_mb_y 			), 
				.ext_start_o		( ext_start			),        
				.ext_done_i 		( ext_done 			),        
				.ext_mode_o 		( ext_mode 			),   
				.ext_wen_i			( ext_wen			),	
				.ext_ren_i		    ( ext_ren           ),
				.ext_addr_i         ( ext_addr          ),
				.ext_data_i         ( ext_data_i        ),
				.ext_data_o         ( ext_data_o        )
);

在综合后的模块连接图中追踪这个输入信号
在这里插入图片描述
可以看到rdata_i是连接到了cur_mb模块的pdata_i即输入,rvalid_i连接到该模块的pvalid_i
在这里插入图片描述

宏块

在H.264进行编码的过程中,每一帧的H图像被分为一个或多个slice(条带)进行编码。每个条带包含多个Macroblock宏块)。宏块是H.264标准中的基本编码单元,其基本结构包含一个1616亮度像素块和两个88色度像素块。每一个宏块会分割成多种不同大小的子块进行预测。帧内预测采用的块大小可能为1616或44;而帧间预测采用的块可能有7种不同的形状:1616,168,816,88,84,48,4*4

assign addr_y = addr_p[4:0];
always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin:cur_luma
	 integer i;
        for(i=0; i<256; i=i+1) begin
             cur_y[i] <= 0;
        end
    end              
	else if(pvalid_i && ~addr_p[5])
		{cur_y[{addr_y,3'b000}],cur_y[{addr_y, 3'b001}],cur_y[{addr_y, 3'b010}],cur_y[{addr_y, 3'b011}],
		 cur_y[{addr_y,3'b100}],cur_y[{addr_y, 3'b101}],cur_y[{addr_y, 3'b110}],cur_y[{addr_y, 3'b111}]}<= pdata_i;
end

数据的分割方法如上
addr_p的计数方法:

always @(posedge clk or negedge rst_n)begin
	if(!rst_n)
		pinc_o <= 1'b0;
	else if((addr_p == 8'd47) && pvalid_i) // read complete 
		pinc_o <= 1'b0;	
	else if(load_start)
		pinc_o <= 1'b1;
end

可以看到同样是计数48次归零
归零之后pinc_0置零,从前面的电路连接图可以看到这个变量作为一个输出变量,连接到top模块的rinc_o,而只有rinc_o为1时,才会从RAM中读取数据
也就是说,pinc_0是一个指示top模块,可以读取下一个像素的指示信号

addr_p计数到48,也就是0x110000,而addr_y取其低5位,也就是0x10000

reg [7:0] cur_y[0:255];

前面定义了cur_y是一个大小为256,每个元素8bit的寄存器数组
复位的时候将其所有值都置零
pvalid_i=1指示输入数据有效,并且addr_p[5]=0的时候,也就是addr_p正在向上计数的时候,此时addr_y同样在向上计数
每次将pdata_i64bit的数据,分配给从{addr_y,3'b000}{addr_y,3'b111}的八个reg,连续赋值,中间没有空的寄存器
注意大小端,应该是寄存器中数值小的分配到了data中的高位的数据

下面看一下cur_y的数据跑到哪里去了

always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin:y_s0
	 integer i;
        for(i=0; i<256; i=i+1) begin
             cur_y_s0[i] <= 0;
             cur_y_s1[i] <= 0;
        end
    end
	else if(mb_switch)begin:y_s1
	integer i;
		for(i=0; i<256; i=i+1)begin
			cur_y_s0[i] <= cur_y[i];
			cur_y_s1[i] <= cur_y_s0[i];
		end
	end
end

做了一个消除亚稳态的处理,打了两拍
追踪数据cur_y_s1

input           mb_switch;		   // start current_mb pipeline
always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin:y_s2
	 integer i;
        for(i=0; i<256; i=i+1) begin
             cur_y_s2[i] <= 0;
        end
    end
	else if(mb_switch&&~intra_flag_i)begin:y_s2_1
	integer i;
		for(i=0; i<256; i=i+1)begin
			cur_y_s2[i] <= cur_y_s1[i];
		end
	end
	else if(mb_switch)begin:y_s2_2
	integer i;
		for(i=0; i<256; i=i+1)begin
			cur_y_s2[i] <= cur_y[i];
		end
	end
end

又打了一拍?迷惑
但注意到判断条件不同
前面的亚稳态处理,是mb_switch=1即开始两级寄存,这里是同时满足mb_switch=1intra_flag_i = 0intra_flag_i这个信号是一个输入信号

input           intra_flag_i;	   // all intra prediction

从电路图上来看
在这里插入图片描述
在这里插入图片描述
也就是说这个信号是从top模块给出来的一个指示信号,在官方给出的测试文件中,在初始化时将其置零,使用时通过task调用

// -------------------------------------------------------
//                   Config Task
// -------------------------------------------------------
task start;
	input intra_flag;
	begin
		if (intra_flag) 
			#100 sys_intra_flag	= 1'b1;
		else 
			#100 sys_intra_flag	= 1'b0; 
	
		     sys_start = 1'b1;        
		#10  sys_start = 1'b0;
	#10  wait(sys_done == 1'b1);                                                               
	end
endtask
if (frame_num%`GOP_LENGTH=='b0)
	start(1); 
else
	start(0);
#500;

由于它连接到了帧内预测模块,暂时记住它是一个跟预测相关的指示变量
也就是说当需要预测的时候,才会将cur_y_s1打入cur_y_s2
但是当intra_flag_i=1的时候,就直接将cur_y打入cur_y_s2中,不经过两级寄存处理
【这里暂时没明白为什么】

那么这个cur_y_s2数据是用来干啥的呢?

genvar j; 
generate
  for(j=0;j<256; j=j+1) begin:j_n 
  	always @( * ) begin
			ime_cur_luma[(j+1)*8-1:j*8] = cur_y_s0[j];
			fme_cur_luma[(j+1)*8-1:j*8] = cur_y_s1[j];
			mc_cur_luma [(j+1)*8-1:j*8] = cur_y_s2[j];   
    end
	end
endgenerate

注意这三个变量ime_cur_lumafme_cur_lumamc_cur_luma都是输出变量,模块连接图如下:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
分别是整像素运动估计分像素运动估计帧内预测
这三个都是H.264中非常重要的三个算法结构,我会在接下来分别重点介绍其原理及实现

这里看一下本模块中其他的信号
读完一帧后输出一个完成信号

always @(posedge clk or negedge rst_n)begin
	if(!rst_n)
		load_done <= 1'b0;
    else if((addr_p == 8'd47) && pvalid_i) // load complete: 16x16x1.5/8=48 cycles 
		load_done <= 1'b1;
	else
		load_done <= 1'b0;
end

pdata_i还有一路流向:

assign addr_uv = addr_y[3:0];
always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin:cur_uv
	 integer i;
        for(i=0; i<64; i=i+1) begin
             cur_u[i] <= 0;
             cur_v[i] <= 0;
        end
    end 
	else if(pvalid_i && addr_p[5])begin
		{cur_u[{addr_uv, 2'b00}],cur_v[{addr_uv, 2'b00}],cur_u[{addr_uv, 2'b01}],cur_v[{addr_uv, 2'b01}],
		 cur_u[{addr_uv, 2'b10}],cur_v[{addr_uv, 2'b10}],cur_u[{addr_uv, 2'b11}],cur_v[{addr_uv, 2'b11}]} <= pdata_i;
	end
end

可以看到,这里和上述的区别在于,addr_uv取的是addr_y的低4bit,而非5bit

reg [7:0] cur_u[0:63];

这是一个容量为64的寄存器数组
只有当addr_p[5]=1也就是addr_p确实计数到48的时候,才会发生一次赋值
同样有消除亚稳态,也就是打两拍的处理

数据cur_u_s2cur_v_s2的去向:

genvar k; 
generate 
  for(k=0;k<64; k=k+1) begin:k_n
  	always @( * ) begin
    	mc_cur_u [(k+1)*8-1:k*8] = cur_u_s2[k];
        mc_cur_v [(k+1)*8-1:k*8] = cur_v_s2[k];   
    end
  end
endgenerate
output [64*8-1 : 0] mc_cur_u;      // output chroma 8x8 for mc and intra 
output [64*8-1 : 0] mc_cur_v;      // output chroma 8x8 for mc and intra

由此可以看到,第一部分的三个输出是作为1616的亮度块输出,而第二部分的两个输出是作为88的色度块输出
模块连接为:
在这里插入图片描述
在这里插入图片描述
宏块的产生过程就是酱紫啦~
呼~明天写帧间预测!加油!

评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值