HDLBits_Verilog向量基础

HDLBits链接


向量

如果把单个信号比作一根电线,那么向量就可以理解为一束电线。就像生活中常见的多芯电缆,向量把多个相关的信号打包在一起处理,让代码更简洁、更易读。

在Verilog中,向量维度的声明放在向量名之前,这一点与C语言不同;但使用向量时,维度信息仍然放在向量名之后。

1
2
wire [99:0] my_vector;      // 声明一个100根电线的向量
assign out = my_vector[10]; // 选择其中的一根

打个比方,声明向量就像给一排房子编号,你可以选择访问整排房子,也可以只访问其中一间。


向量的更多知识

声明向量

向量声明的基本语法是:

1
type [upper:lower] vector_name;

其中,type指定了向量的数据类型,通常为reg或wire类型。如果声明的是输入、输出向量,类型前还需要加上input和output:

1
2
3
4
5
6
wire [7:0] w;         // 8位wire向量
reg [4:1] x; // 4位reg向量
output reg [0:0] y; // 1位reg输出端口(这仍然是一个向量)
input wire [3:-2] z; // 6位wire输入(允许使用负数索引)
output [3:0] a; // 4位输出wire,除非指定,否则默认是wire类型
wire [0:7] b; // 8位wire,其中b[0]是最高位

这里有一个很重要的概念:大小端。向量的大小端决定了最低有效位(LSB)具有较低的索引(如[3:0])还是较高的索引(如[0:3])。一旦用特定的索引规则定义了向量,就必须用相同的方式去使用它!

小贴士: 向量声明时数字的顺序很重要,它决定了向量是以大端存储还是小端存储。

  • 若声明 wire [3:0] w,则 w[0] 为最低位,w[3] 为最高位
  • 若声明为 wire [0:3] w,则结果完全相反
    因此在向量的定义和使用时一定要保持大小端一致!

未定义的中间变量容易出错

使用wire型变量前请先声明,不要图一时之便不定义就直接拿来做中间变量,这样导致的错误会很难发现。

1
2
3
4
5
6
wire [2:0] a, c;   // 两个向量
assign a = 3'b101; // a = 101
assign b = a; // b = 1 隐式创建的wire,只有1位!
assign c = b; // c = 001 <-- 这里就出错了!
my_module i1 (d,e); // d和e如果没有声明,会隐式成为1位
// 如果端口本来应该是向量,这就是个bug

向量名前为维度,向量名后为数目,不写统统视作1。可以这样类比:

  • 维度就像每个房子的大小
  • 数目就像房子的总数目
1
2
reg [7:0] mem [255:0];   // 256个 unpacked 元素,每个都是8位 packed reg向量
reg mem2 [28:0]; // 29个 unpacked 元素,每个都是1位 reg

获取向量元素:部分选择

访问整个向量直接使用向量名即可,例如:assign w = a;。如果左右两边的长度不匹配,则根据情况对向量进行补零或截断。

向量的部分选择操作可以访问向量的一部分:

1
2
3
4
5
6
7
w[3:0]      // 只取w的低4位
x[1] // x的最低位
x[1:1] // ...也是x的最低位
z[-1:-2] // z的最低两位
b[3:0] // 非法!向量部分选择必须与声明的方向一致
b[0:3] // b的高4位
assign w[3:0] = b[0:3]; // 将b的高4位赋值给w的低4位

注意: 取用向量中特定维度的数时,一定要注意与向量的定义一致,注意大小端匹配的问题!


题目:字节序转换

题目描述:

构建一个电路,将一个4字节数的字节顺序调转,常用于数据的大小端转换。

AaaaaaaaBbbbbbbbCcccccccDddddddd => DdddddddCcccccccBbbbbbbbAaaaaaaa

Solution:

1
2
3
4
5
6
7
8
9
10
11
module top_module(
input [31:0] in,
output [31:0] out
);
// 按照字节序反转: 字节3→字节0, 字节2→字节1, 字节1→字节2, 字节0→字节3
assign out[31:24] = in[7:0]; // in的字节0 → out的字节3
assign out[23:16] = in[15:8]; // in的字节1 → out的字节2
assign out[15:8] = in[23:16]; // in的字节2 → out的字节1
assign out[7:0] = in[31:24]; // in的字节3 → out的字节0

endmodule

小贴士: 在向量赋值的左右两端都可以对部分数据进行操作,这让我们可以灵活地处理数据的不同部分。


对向量进行门操作

位操作运算符 vs 逻辑操作运算符

这是另一个容易混淆的地方!让我们搞清楚:

对两个N比特位宽的向量而言:

  • 按位操作: 输出为N比特位宽向量,每一位分别运算
  • 逻辑操作: 将整个向量视为布尔值(true=非零,false=零),产生1比特输出

打个比方:

  • 按位操作就像逐个检查一排开关,每个开关都得出一个结果
  • 逻辑操作就像判断”这一整排开关中是否有打开的”,只给出一个是或否的答案

题目1:向量的按位操作和逻辑操作

题目描述1: 对向量特定部分的数据进行按位操作和逻辑操作。

Solution 1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
module top_module(
input [2:0] a,
input [2:0] b,
output [2:0] out_or_bitwise,
output out_or_logical,
output [5:0] out_not
);
// 逻辑或: 只要a或b非零,结果就是1
assign out_or_logical = a || b;
// 按位或: a和b的每一位分别进行或操作
assign out_or_bitwise = a | b;
// 按位取反: 先放b的反,再放a的反
assign out_not[5:3] = ~b;
assign out_not[2:0] = ~a;
endmodule

题目2:缩位运算符

题目描述2:

对输入向量进行按位的与、或和异或操作。

Solution 2:

1
2
3
4
5
6
7
8
9
10
11
12
13
module top_module(
input [3:0] in,
output out_and,
output out_or,
output out_xor
);
// 缩位与: &in = in[3] & in[2] & in[1] & in[0]
assign out_and = &in;
// 缩位或: |in = in[3] | in[2] | in[1] | in[0]
assign out_or = |in;
// 缩位异或: ^in = in[3] ^ in[2] ^ in[1] ^ in[0]
assign out_xor = ^in;
endmodule

小贴士: 合理利用缩位运算符可以精简代码!按位进行逻辑运算,结果为1比特数。

  • 与缩位运算符: &
  • 或缩位运算符: |
  • 异或缩位运算符: ^
  • 还可以组成复合运算符: ~&, ~|, ~^

向量拼接操作

向量拼接操作就像把几根绳子接在一起。不过,拼接操作需要知道待拼接的各向量的位宽,否则你怎么知道结果的位宽呢?

所以 {1,2,3} 是无效的,因为未指定各向量位宽。但 {3'd1, 3'd2, 3'd3} 就是有效的!

向量的拼接操作在赋值的左右两端均可使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
input  [15:0] in;
output [23:0] out;

// 交换两个字节,左右两边都是16位向量
assign {out[7:0], out[15:8]} = in;

// 同样的效果,但只给out的低16位赋值
assign out[15:0] = {in[7:0], in[15:8]};

// 这就不同了!右边的16位向量会扩展到24位来匹配左边
// 所以out[23:16]会是零
// 在前两个例子中,out[23:16]没有被赋值
assign out = {in[7:0], in[15:8]};

注意:

  1. 位宽合并需要知道各成分的确切位宽,否则结果位宽不定
  2. 位宽合并时在assign两端均可实现,最好左右两端位宽相同,否则未被指定的数据会被置零

题目:向量拼接与分割

题目描述:

给定几个输入向量,将它们拼接在一起,然后分割成几个输出向量。有6个5位输入向量a, b, c, d, e, f,总共30位输入。对于32位的输出,有4个8位的输出向量w、x、y和z。输出应该是输入向量与两个”1”位的串联:

1

Solution:

1
2
3
4
5
6
7
8
9
10
11
module top_module (
input [4:0] a, b, c, d, e, f,
output [7:0] w, x, y, z
);
wire [31:0] temp;

// 先拼接: 6个5位向量 + 2个1位的1 = 32位
assign temp = {a, b, c, d, e, f, 2'b11};
// 再分割: 32位分成4个8位
assign {w, x, y, z} = temp;
endmodule

向量翻转

题目描述:

给定一个8位输入向量[7:0],颠倒它的位顺序。

Solution 1:

1
2
3
4
5
6
7
module top_module(
input [7:0] in,
output [7:0] out
);
// 手动将每一位反过来
assign out = {in[0], in[1], in[2], in[3], in[4], in[5], in[6], in[7]};
endmodule

Solution 2:

1
2
3
4
5
6
7
8
9
10
11
12
module top_module(
input [7:0] in,
output [7:0] out
);
integer i;

always @(*) begin
for(i = 0; i < 8; i = i + 1) begin
out[i] = in[7 - i];
end
end
endmodule

小贴士: 建议使用Solution 2实现,可扩展性强!如果向量变成100位,只需要改一下循环边界就行。


向量复制操作

向量复制操作允许重复一个向量并将它们连接在一起,语法是:

1
{num{vector}}

例如:

1
2
3
{5{1'b1}}           // 5'b11111 (或者 5'd31 或者 5'h1f)
{2{a,b,c}} // 等同于 {a,b,c,a,b,c}
{3'd5, {2{3'd6}}} // 9'b101_110_110,先放101,然后放两个110

小贴士: 复制向量有捷径,外层花括号带重复次数。主要用于符号位的拓展!
例如:

  • 4’b0101 带符号扩展为8bit: 8’b00000101
  • 4’b1101 带符号扩展为8bit: 8’b11111101

题目1:符号位扩展

题目描述: 建立一个电路,将8位数字扩展到32位。这需要将符号位的24个副本(即位[7]复制24次)与8位数字本身连接起来。

Solution:

1
2
3
4
5
6
7
8
module top_module (
input [7:0] in,
output [31:0] out
);
// 复制符号位24次,拼接上原来的8位
assign out = {{24{in[7]}}, in};

endmodule

题目2:多向量复制

题目描述2: 给定5个1位信号(a、b、c、d和e),计算25位输出向量中所有25个成对的1位比较。如果两个被比较的位相等,输出应该是1。

1
2
3
4
5
6
out[24] = ~a ^ a;   // a == a, 所以out[24]总是1
out[23] = ~a ^ b;
out[22] = ~a ^ c;
...
out[ 1] = ~e ^ d;
out[ 0] = ~e ^ e;

2

思路: 该操作可以定位特定signal所在位置,两向量重复数据有规律可循,可用复制的方式来产生,简化代码。

Solution:

1
2
3
4
5
6
7
8
9
10
module top_module (
input a, b, c, d, e,
output [24:0] out
);
// {5{a,b,c,d,e}} 产生: a,b,c,d,e,a,b,c,d,e,a,b,c,d,e,a,b,c,d,e,a,b,c,d,e (25位)
// {{5{a}},{5{b}},{5{c}},{5{d}},{5{e}}} 产生: aaaaabbbbbcccccdddddeeeee (25位)
// 然后按位异或,再取反,相等就是1
assign out = ~{5{a,b,c,d,e}} ^ {{5{a}},{5{b}},{5{c}},{5{d}},{5{e}}};

endmodule

入门者避坑指南

向量操作是Verilog设计中非常基础但也容易出错的部分,让我们看看初学者常踩的坑:


错误1: 向量大小端不匹配

错误表现:

1
2
3
4
5
6
7
module bad_example (
input [3:0] a, // a[3]是最高位,a[0]是最低位
output [0:3] b // b[0]是最高位,b[3]是最低位
);
assign b = a; // 虽然语法没问题,但位的对应关系可能出错!
assign b[0] = a[0]; // 错误!大小端顺序不同,这样赋值会混淆最高/最低位
endmodule

错误原因:

  • [3:0] 表示索引从3降到0,3是最高位
  • [0:3] 表示索引从0升到3,0是最高位
  • 虽然整体赋值时Verilog会自动处理,但单独访问某一位时很容易出错

正确做法:

1
2
3
4
5
6
7
module good_example (
input [3:0] a,
output [3:0] b // 保持一致的大小端
);
assign b = a; // 简单直接
assign b[0] = a[0]; // 清楚明确:最低位赋值给最低位
endmodule

调试技巧:

  • 整个项目中统一使用一种大小端约定(建议用[N:0],低位在右)
  • 如果必须混用,一定要在注释里写清楚每个向量的大小端

错误2: 隐式创建的1位wire

错误表现:

1
2
3
4
5
6
7
8
module bad_example (
input [3:0] a,
output [3:0] y
);
// 错误!b没有声明,会被隐式创建为1位wire
assign b = a + 1'b1;
assign y = b; // y[3:1] 会是0,只有y[0]是有效的!
endmodule

错误原因:
Verilog允许不声明就使用wire,但会默认创建1位的wire。当你把多位向量赋值给它时,只会保留最低位。

正确做法:

1
2
3
4
5
6
7
8
module good_example (
input [3:0] a,
output [3:0] y
);
wire [3:0] b; // 显式声明
assign b = a + 1'b1;
assign y = b;
endmodule

调试技巧:

  • 养成好习惯:所有wire都先声明再使用
  • 开启编译器的警告选项,它会提醒你有隐式声明的wire

错误3: 拼接操作中未指定位宽的常量

错误表现:

1
2
3
4
5
6
7
module bad_example (
input [1:0] a, b,
output [5:0] y
);
// 错误!1和2没有指定位宽
assign y = {1, a, 2, b};
endmodule

错误原因:
拼接操作要求每个元素都有明确的位宽,否则编译器无法确定最终结果的位宽。

正确做法:

1
2
3
4
5
6
7
8
9
module good_example (
input [1:0] a, b,
output [5:0] y
);
// 每个元素都指定位宽
assign y = {2'd1, a, 2'd2, b}; // 2+2+2+2=8? 哦,等一下,让我们重新算...
// 如果要6位,可以这样:
assign y = {1'b1, a, 1'b0, b}; // 1+2+1+2=6位
endmodule

调试技巧:

  • 拼接操作时,在纸上或注释里算好每部分的位宽
  • 确保加起来的总位宽和目标一致

错误4: 部分选择时方向不匹配

错误表现:

1
2
3
4
5
6
7
module bad_example (
input [3:0] a, // 声明是 [3:0],从高到低
output [1:0] y
);
// 错误!部分选择的方向和声明不一致
assign y = a[0:1]; // 应该是 a[1:0]
endmodule

错误原因:
向量声明时是[3:0],表示索引从3降到0,那么部分选择时也必须保持同样的顺序。

正确做法:

1
2
3
4
5
6
7
module good_example (
input [3:0] a,
output [1:0] y
);
// 方向一致: 从高索引到低索引
assign y = a[1:0];
endmodule

调试技巧:

  • 记住:声明时怎么写,使用时就怎么写
  • 如果声明是[upper:lower],使用时也必须是[higher:lower]

错误5: 复制操作的花括号嵌套错误

错误表现:

1
2
3
4
5
6
7
8
9
module bad_example (
input a,
output [3:0] y
);
// 错误!花括号位置不对
assign y = 4{a}; // 应该是 {4{a}}
// 或者这样也不对
assign y = {4, a}; // 这是拼接,不是复制
endmodule

错误原因:
复制操作需要两层花括号:

  • 内层:{a} 表示被复制的内容
  • 外层:{4{...}} 表示复制4次

正确做法:

1
2
3
4
5
6
7
module good_example (
input a,
output [3:0] y
);
// 正确的复制操作
assign y = {4{a}}; // 把a复制4次
endmodule

调试技巧:

  • 复制操作口诀:外层套次数,内层套数据
  • 区分拼接({a,b,c})和复制({3{a}})

本章小结

这一章我们学习了向量操作的相关知识,这些都是设计中非常实用的技巧:

  1. 向量声明: 理解大小端的区别,并在设计中保持一致
  2. 部分选择: 灵活访问向量的任意部分,但要注意方向匹配
  3. 拼接操作: 把多个向量接在一起,每个元素都需要明确位宽
  4. 复制操作: {num{vector}},快速生成重复模式,常用于符号扩展
  5. 缩位运算符: &, |, ^,对向量所有位进行操作,结果为1位

向量就像给信号”打包”,让我们能够用更简洁的代码描述复杂的硬件连接。熟练掌握这些操作,会让你的代码既清晰又高效!