概要
本文的源码基于复旦大学的开源芯片—开源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_i
64bit的数据,分配给从{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=1
和intra_flag_i = 0
,intra_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_luma
、fme_cur_luma
、mc_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_s2
和cur_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的色度块输出
模块连接为:
宏块的产生过程就是酱紫啦~
呼~明天写帧间预测!加油!