在FPGA开发中,经常可能会涉及到位宽截取;比如一个信号定义一个信号A[15:0],在实际使用的时候有时候只需要截取高8位,那么就是A[15:8],或者截取低8位A[7:0]。这是一种最常见的使用场景,对于一个入门级的FPGA工程师都没什么问题。这篇文章主要介绍几种与位宽操作实用但又稍微冷门的几个场景。
拼接的意思是将几个短位宽的数据拼接成更大位宽的数据。比如:A = 4'h5 B = 4'hA ; 将A与 B 拼接在一起就是{A,B} = 8'h5A。这是最常见的拼接操作,但是如果我们需要将A和B重复拼接32次呢?如果重复敲32个{A,B}是不是显得有点蠢。Verilog也考虑到这一场景,给出了一个复制拼接的用法,具体如下:
C = {32{A,B}} ;
等效为
C= {{A,B},{A,B},......,{A,B}} //共32次
两层拼接符,内层拼接符外面的数字代表复制的次数;将{A,B}作为一个整体,重复拼接32次;
如果将32定义为一个parameter,这样代码更直观、更利于维护。需注意的是:这里的复制的次数一定得是常量,不可以是变量reg / wire ;
parameter copy_num = 8'd32 ;
assign C = {copy_num{A,B}};
移位拼接是一种作为移位寄存器 经常使用方法;
假设需求如下,要从输入的串行序列中寻找固定的编码段4‘b1011,那么就需要将串行的数据转并行,最直接的实现方法可能是:
always@(posedge clk)begin
A[0] <= Din ;
A[1] <= A[0] ;
A[2] <= A[1] ;
A[3] <= A[2] ;
end
这样写自然也没什么问题,做边沿检测或者跨时钟域处理,只需要缓存个2~3clk的时候我也倾向直接这么用,
但是当这个移位寄存器的位宽太大的时候,比如超过10 或者更大,这么写就显得代码臃肿,这时候有没有更好的选择呢?
当然,移位拼接就是更好的选择:
向左移位
always@(posedge clk)begin
A[3:0] <= {A[2:0],Din};
end
向右移位
always@(posedge clk)begin
A[3:0] <= {Din,A[3:1]};
end
按照IEEE Verilog的标准,向量的位宽截取分为 按位截取(bit-select) 和 按块截取(part-select)
按位截取可能是大多数人更常用的一种方式,这里不多赘述;
A[15:0]
截取高8位:
B = A[15:8]
这里重点说一下Part-Select这种模式,如上图所示:
也就是说,part-select这种模式有两个关键表达式:base_expr 和 width_expr ; 一个关键的操作符 +: 或 -:
base_expr简单来说就是基地址,width_expr是宽度;通过“+:” 或 “-: ” 来控制方向;
其中,基地址可以是变量,但宽度一定是正整数常量
由于part-select的这种特征,对于长位宽信号,拆分为多个小位宽信号就非常友好,比如:
// part-select实现
reg [255:0] A;
//现需要将A拆分成 16 个 16bit的信号,我们可以这样做
reg [15:0] B[15:0] ;
integer i;
for(i=0;i<=15;i<=i+1)begin
assign B[i] = A[i*16 +: 16] ;
end
如果是传统的方法,那将会是这样:
// part-select实现
reg [255:0] A;
//现需要将A拆分成 16 个 16bit的信号,我们可以这样做
reg [15:0] B[15:0] ;
assign B[0] = A[15:0] ;
assign B[1] = A[31:16] ;
assign B[2] = A[47:32] ;
....
assign B[15] = A[255:240] ;
两种方法一对比,从代码的书写量、可读性、可维护性各方面,高下立判。
而且两种方法实现的电路是一模一样,并不会说用这种“投机取巧”的方法,就会增加电路实现的负担。