1个不稳定版本

0.1.0 2023年9月9日

#10 in #二进制运算符

MIT 许可证

68KB
1.5K SLoC

该库提供了两个对硬件设计工作非常重要的类型。Bits类型是一个具有可变位数的有符号整数类型。SignedBits类型是一个具有可变位数的有符号整数类型。这两个类型都是为了模仿在硬件设计中通常合成的固定宽度二进制表示的整数的行为。

硬件设计和软件编程之间一个显著的区别是需要(实际上也是能够)轻松地操作各种长度的位集合。尽管Rust内置了表示8、16、32、64和128位(截至撰写本文时)的类型,但要表示5位类型很难。或者256位类型。或者任何不是2的幂次或大于128位的位长度。

[Bits]和[SignedBits]类型被设计来填补这个空白。它们在它们表示的位数上是通用的,并且可以用来表示从1到128位的任何位数。Bits类型是无符号整数类型,而SignedBits类型是有符号整数类型。这两个类型都实现了整数类型的标准Rust特性,包括Add、Sub、BitAnd、BitOr、BitXor、Shl、Shr、Not、Eq、Ord、PartialEq、PartialOrd、Display、LowerHex、UpperHex和Binary。
SignedBits类型还实现了Neg。请注意,在所有情况下,这些类型都实现了2的补码环绕算术,正如你在硬件设计中会发现的那样。它们在溢出或下溢时不会崩溃,而是简单地环绕。这是最符合真实硬件设计的行为。你当然可以在设计中实现溢出和下溢检测,但这不是默认行为。

这两种类型也可以[复制],这使得它们的使用就像内置整数类型一样简单。以下是一些一般性的建议。如果你之前没有这样做过,硬件对位向量的操作可能会感觉不符合直觉。[Bits]和[SignedBits]类型被设计成模拟硬件设计的行为,因此它们可能不会按你预期的行为运行。如果你不熟悉2的补码算术,在使用这些类型之前你应该先了解一下。

构建[Bits]

构建[Bits]值有几种方式。最简单的是使用[From]特质,并从整数字面量转换。例如

use rhdl_bits::Bits;
let bits: Bits<8> = 0b1101_1010.into();

这将对任何在[Bits]类型范围内的整数字面量都有效。如果字面量超出了[Bits]类型的范围,Rust将会panic。

你还可以从[u128]值构建一个[Bits]值

let bits: Bits<8> = 0b1101_1010_u128.into();

注意,[Bits]类型只支持最多128位的值。可以通过数据结构(数组、结构体、枚举、元组)轻松构建更大的位向量。但是,默认情况下不支持对这些进行算术操作。你需要为这些类型提供自己的算术实现。这是[Bits]类型的一个限制,但在实际应用中这个限制不太可能成为问题。实际的硬件限制可能意味着对非常长的位向量进行算术运算可能会非常慢。

构建[SignedBits]

[SignedBits]类型可以像[Bits]类型一样构建。唯一的区别是[SignedBits]类型可以从[i128]值构建

let bits: SignedBits<8> = 0b0101_1010_i128.into();

同样,你可以从有符号字面量构建[SignedBits]

let bits: SignedBits<8> = (-42).into();

注意括号! 由于运算顺序,取反运算的优先级低于 .into()。因此,如果你省略了括号,你将得到Rust关于无法决定整数字面量必须假设的类型的问题的警告。这是不幸的,但不可避免。

操作

为[Bits]和[SignedBits]定义的操作仅是一小部分。这些是在硬件上不会出现意外(通常情况下)的操作。在Rust中,你可以在相同宽度的[Bits]类型之间或与其他[Bits]类型进行操作,或者使用整数字面量,这些字面量将被转换为适当宽度的[Bits]类型。例如

let bits: Bits<8> = 0b1101_1010.into();
let result = bits & 0b1111_0000;
assert_eq!(result, 0b1101_0000);

注意,如果结果是直接与整数字面量进行比较。

你还可以操作不同宽度的[Bits]类型,但你需要首先将它们转换为相同的宽度。例如

let bits: Bits<8> = 0b1101_1010.into();
let nibble: Bits<4> = 0b1111.into();
let result = bits.slice(4) & nibble;
assert_eq!(result, 0b1101);

这里,slice运算符将提取bits值的最高4位,然后可以使用&运算符进行操作。请注意,slice运算符是关于切片宽度的泛型,因此你可以从[Bits]值中提取任意数量的位。如果你请求的位数多于[Bits]值的位数,则额外的位数将被初始化为0。

let bits: Bits<8> = 0b1101_1010.into();
let word: Bits<16> = bits.slice(0);
assert_eq!(word, 0b0000_0000_1101_1010);

你还可以对[SignedBits]进行slice操作。但是,在这种情况下,额外的位数将进行符号扩展,而不是零扩展。最终结果是[Bits]类型,而不是[SignedBits]类型。例如

let bits: SignedBits<8> = (-42).into();
let word: Bits<16> = bits.slice(0);
assert_eq!(word, 0xFF_D6);
  • 在[SignedBits]值上使用slice运算符时要小心*。如果你将[SignedBits]值切片到更小的尺寸,符号位将会丢失。例如
let bits: SignedBits<8> = (-42).into();
let nibble: Bits<4> = bits.slice(0);
assert_eq!(nibble, 6);

为了详细说明这个例子,-42在8位中的表示是1101_0110。如果你将其切片到4位,你会得到0110,即6。切片过程中丢失了符号位。

位宽度和二进制运算符

所有的二进制运算符遵循相同的规则

  • 两个操作数必须具有相同的宽度。
  • 两个操作数必须具有相同的类型(例如,[SignedBits]或[Bits])。
  • 操作数之一可能是一个字面量,在这种情况下,在应用运算符之前,它将被转换为适当的类型。

这些规则完全由 Rust 类型系统强制执行。因此,遵循这些规则并没有什么特别之处,你不会感到不习惯。例如,以下代码将无法编译:

let x: Bits<20> = 0x1234.into();
let y: Bits<21> = 0x5123.into();
let z = x + y; // This will fail to compile.

加法

支持对 [Bits] 和 [SignedBits] 类型进行加法。你可以将两个 [Bits] 值相加,或者将一个 [Bits] 值与整数字面量相加。例如:

let x: Bits<32> = 0xDEAD_BEEE.into();
let y: Bits<32> = x + 1;
assert_eq!(y, 0xDEAD_BEEF);

参数的顺序并不重要

let x: Bits<32> = 0xDEAD_BEEE.into();
let y: Bits<32> = 1 + x;
assert_eq!(y, 0xDEAD_BEEF);

或者使用两个 [Bits] 值

let x: Bits<32> = 0xDEAD_0000.into();
let y: Bits<32> = 0xBEEF.into();
let z: Bits<32> = x + y;
assert_eq!(z, 0xDEAD_BEEF);

AddAssign 特性也针对 [Bits] 和 [SignedBits] 实现,因此你也可以使用 += 运算符

let mut x: Bits<32> = 0xDEAD_0000.into();
x += 0xBEEF;
assert_eq!(x, 0xDEAD_BEEF);

请注意,加法操作是2的补码环绕加法。这是对硬件设计最有用的行为。如果你想检测溢出,你需要自己实现。

let mut x: Bits<8> = 0b1111_1111.into();
x += 1;
assert_eq!(x, 0);

在这种情况下,加1导致 x 环绕到全零。这是完全正常的,也是硬件加法(没有进位)所期望的。如果你 需要 进位位,解决方案是首先将其转换为更高的位,然后相加,或者直接计算进位。

let x: Bits<40> = (0xFF_FFFF_FFFF).into();
let y: Bits<41> = x.slice(0) + 1;
assert_eq!(y, 0x100_0000_0000);

减法

硬件减法使用2的补码算术定义。这是在硬件中表示负数和减法的通用标准。对于 [Bits] 和 [SignedBits],实现了 Sub 特性,并且其行为与 Rust 中内置整数上的 Wrapping 特性相似。请注意,在 RHDL 中(在硬件中也是如此)不检测溢出和下溢。如果你想在这些情况下采取行动,你需要显式检查溢出或下溢条件。

let x: Bits<8> = 0b0000_0001.into();
let y: Bits<8> = 0b0000_0010.into();
let z: Bits<8> = x - y; // 1 - 2 = -1
assert_eq!(z, 0b1111_1111);

请注意,在这种情况下,我们从1中减去2,结果是-1。然而,-1在2的补码中是 0xFF,这被存储在 z 中作为无符号值255。这与你在标准 Rust 中使用 Wrapping 算术时使用 u8 时的行为相同

let x : u8 = 1;
let y : u8 = 2;
let z = u8::wrapping_sub(x, y);
assert_eq!(z, 0b1111_1111);

我不想过多地强调这一点,但环绕算术和2的补码表示可能会让不熟悉硬件算术实现的人感到惊讶。

对于 [SignedBits],结果是相同的,但被正确解释

let x: SignedBits<8> = 0b0000_0001.into();
let y: SignedBits<8> = 0b0000_0010.into();
let z: SignedBits<8> = x - y; // 1 - 2 = -1
assert_eq!(z, -1);

SubAssign 特性为 [Bits] 和 [SignedBits] 都实现了,因此你也可以使用 -= 运算符

let mut x: Bits<8> = 0b0000_0001.into();
x -= 1;
assert_eq!(x, 0);

位逻辑运算符

标准 Rust 中的所有四个逻辑运算符都支持 [Bits] 和 [SignedBits]。它们按位操作,并使用标准 Rust 特性实现。为了完整性,支持的位运算符列表如下:

  • OrOrAssign 对应于 ||=
  • AndAndAssign 对应于 &&=
  • XorXorAssign 对应于 ^^=
  • Not 对应于 ! 其他更奇特的二进制运算符(如 Xnor 或 Nand)不受支持。如果你需要这些,你需要用这些更基本的运算符来实现它们。

以下是一个二进制运算符的示例

let x: Bits<8> = 0b1101_1010.into();
let y: Bits<8> = 0b1111_0000.into();
let z: Bits<8> = x | y;
assert_eq!(z, 0b1111_1010);
let z: Bits<8> = x & y;
assert_eq!(z, 0b1101_0000);
let z: Bits<8> = x ^ y;
assert_eq!(z, 0b0010_1010);
let z: Bits<8> = !x;
assert_eq!(z, 0b0010_0101);

请注意,您还可以将这些运算符应用于[有符号位]。结果的意义由您自行解释。位运算符仅简单地操作位,而不关心值的符号。这对Rust和内建类型也适用。

let x: i8 = -0b0101_1010;
let y: i8 = -0b0111_0000;
let z = x ^ y; // This will be positive
assert_eq!(z, 54);

位移

位移是一个相对复杂的话题,因为它涉及一些额外的细节。

  • 在左位移下,[位]和[有符号位]的行为是相同的。
  • 在右位移下,[位]和[有符号位]的行为是不同的。
  • 对[位]进行右位移将在值的“左侧”(最高位)插入0
  • 对[有符号位]进行右位移将在值的“左侧”复制最高位。

这些差异的总体影响是,左位移(在一定范围内)将保留值的符号,直到所有位都从值中移出。右位移将保留值的符号。如果您想对[有符号位]值进行右位移而不在最高位插入0,则首先将其转换为[位]。

对于[位]和[有符号位],实现了ShlShlAssign特性,以及ShrShrAssign特性。请注意,与其它运算符不同,位移运算符允许您使用不同的位数来指定位移量。这是因为,在硬件设计中,位移量通常由动态控制(使用称为桶形移位器的电路)进行控制。用于编码位移量的位数将与寄存器中位数的二进制对数相关。例如,如果您有一个32位寄存器,您将需要5位来编码位移量。如果您有一个64位寄存器,您将需要6位来编码位移量。依此类推。

为了模拟这一点,位移运算符在值被移位的位数以及控制位移的值的位数上是泛型的。例如

let x: Bits<8> = 0b1101_1010.into();
let y: Bits<3> = 0b101.into();
let z: Bits<8> = x >> y;
assert_eq!(z, 0b0000_0110);

您也可以使用整数字面量来控制位移量

let x: Bits<8> = 0b1101_1010.into();
let z: Bits<8> = x >> 3;
assert_eq!(z, 0b0001_1011);

在[位]/[有符号位]的位移运算符与Rust内建整数的包装运算符之间有一个关键的区别。如果位移的位数超过值中的位数,Rust将不执行任何操作。例如

let x: u8 = 0b1101_1010;
let y = u8::wrapping_shl(x,10);
assert_ne!(y, 0b1101_1010); // Note that this is _not_ zero - the result is not even clearly defined.

对于[位]和[有符号位]来说,情况并非如此。如果您位移的位数超过值中的位数,结果将简单地是零(除非您正在右位移一个[有符号位]值,在这种情况下,它将收敛到0或-1,具体取决于符号位)。这是一个特殊情况,尚不清楚“正确”的行为应该是什么。但是,RHDL中实现了这种行为。

let x: Bits<8> = 0b1101_1010.into();
let z: Bits<8> = x >> 10;
assert_eq!(z, 0);

比较运算符

标准Rust比较运算符适用于[位]和[有符号位]。这些运算符是

  • PartialEqEq对应于==!=
  • OrdPartialOrd对应于<><=>=

请注意,比较运算符对于[有符号位]使用带符号算术,对于[位]使用无符号算术。这与您在硬件设计中看到的行为相同。例如,对于[位]

let x: Bits<8> = 0b1111_1111.into();
let y: Bits<8> = 0b0000_0000.into();
assert!(x > y);

另一方面,对于[有符号位]

let x: SignedBits<8> = (-0b0000_0001).into();
let y: SignedBits<8> = 0b0000_0000.into();
assert!(x < y);
assert_eq!(x.as_unsigned(), 0b1111_1111);

依赖项

~335–790KB
~18K SLoC