Module rust_hdl_core::bits
source · Expand description
Module that supports arbitrary width bit vectors The [Bits] type is used to capture values with arbitrarily large (but known) bit length
One significant difference between hardware design and software programming is the need (and indeed ability) to easily manipulate collections of bits that are of various lengths. While Rust has built in types to represent 8, 16, 32, 64, and 128 bits (at the time of this writing, anyway), it is difficult to represent a 5 bit type. Or a 256 bit type. Or indeed any bit length that differs from one of the supported values.
In hardware design, the bit size is nearly always unusual, as bits occupy physical space,
and as a result, as a logic designer, you will intentionally use the smallest number of
bits needed to capture a value. For example, if you are reading a single nibble at a
time from a bus, this is clearly a 4 bit value, and storing it in a u8
is a waste of
space and resources.
To model this behavior in RustHDL, we have the [Bits] type, which attempts to be as close
as possible to a hardware bit vector. The size must be known at compile time, and there is
some internal optimization for short bitvectors being represented efficiently, but ideally
you should be able to think of it as a bit of arbitrary length. Note that the [Bits]
type is Copy
, which is quite important. This means in your RustHDL code, you can freely
copy and assign bitvectors without worrying about the borrow checker or trying to call
clone
in the midst of your HDL.
For the most part, the [Bits] type is meant to act like a u32
or u128
type as far
as your code is concerned. But the emulation of built-in types is not perfect, and
you may struggle with them a bit.
Constructing [Bits]
There are several ways to construct a [Bits] type. It includes an implementation of the Default, trait, so if you need a zero value, you can use that form:
let x: Bits<50> = Default::default();
This will construct a length 50 bit vector that is initialized to all 0
.
You can also convert from literals into bit vectors using the From and Into traits,
provided the literals are of the u64
type.
let x: Bits<50> = 0xBEEF.into();
In some cases, Rust complains about literals, and you may need to provide a suffix:
let x: Bits<50> = 0xDEAD_BEEF_u64.into();
However, in most cases, you can leave literals suffix-free, and Rust will automatically determine the type from the context.
You can construct a larger constant using the bits function. If you have a literal of up to 128 bits, it provides a functional form
let x: Bits<200> = bits(0xDEAD_BEEE); // Works for up to 128 bit constants.
There is also the [ToBits] trait, which is implemented on the basic unsigned integer types. This trait allows you to handily convert from different integer values
let x: Bits<10> = 32_u8.to_bits();
Operations
Only a subset of operations are defined for [Bits]. These are the operations that can be synthesized in hardware without surprises (generally speaking). In Rust, you can operate between [Bits] types and other [Bits] of the same width, or you can use integer literals. Be careful! Especially when manipulating signed quantities. Use the Signed type for those.
Addition
You can perform wrapping addition using the +
operator.
Here are some simple examples of addition. First the version using a literal
let x: Bits<200> = bits(0xDEAD_BEEE);
let y: Bits<200> = x + 1;
assert_eq!(y, bits(0xDEAD_BEEF));
And now a second example that uses two [Bits] values
let x: Bits<40> = bits(0xDEAD_0000);
let y: Bits<40> = bits(0x0000_CAFE);
let z = x + y;
assert_eq!(z, bits(0xDEAD_CAFE));
Note that the addition operator is silently wrapping. In other words the carry bit is discarded silently (again - this is what hardware typically does). So you may find this result surprising:
let x: Bits<40> = bits(0xFF_FFFF_FFFF);
let y = x + 1;
assert_eq!(y, bits(0));
In this case, the addition of 1 caused [x] to wrap to all zeros. This is totally normal, and what one would expect from hardware addition (without a carry). If you need the carry bit, then the solution is to first cast to 1 higher bit, and then add, or alternately, to compute the carry directly.
let x: Bits<40> = bits(0xFF_FFFF_FFFF);
let y = bit_cast::<41, 40>(x) + 1;
assert_eq!(y, bits(0x100_0000_0000));
The order of the arguments does not matter. The bit width of the calculation will be determined by the [Bits] width.
let x : Bits<25> = bits(0xCAFD);
let y = 1 + x;
assert_eq!(y, bits(0xCAFE));
However, you cannot combine two different width [Bits] values in a single expression.
let x: Bits<20> = bits(0x1234);
let y: Bits<21> = bits(0x5123);
let z = x + y; // Won't compile!
Subtraction
Hardware subtraction is defined using 2-s complement representation for negative numbers. This is pretty much a universal standard for representing negative numbers in binary, and has the added advantage that a hardware subtractor can be built from an adder and some basic gates. Subtraction operates much like the [Wrapping] class. Note that overflow and underflow are not detected in RustHDL (nor are they detected in most hardware implementations either).
Here is a simple example with a literal and subtraction that does not cause udnerflow
let x: Bits<40> = bits(0xDEAD_BEF0);
let y = x - 1;
assert_eq!(y, bits(0xDEAD_BEEF));
When values underflow, the representation is still valid as a 2-s complement number. For example,
let x: Bits<16> = bits(0x40);
let y: Bits<16> = bits(0x60);
let z = x - y;
assert_eq!(z, bits(0xFFFF-0x20+1));
Here, we compare the value of z
with 0xFFFF-0x20+1
which is the 2-s complement
representation of -0x20
.
You can also put the literal on the left side of the subtraction expression, as expected. The bitwidth of the computation will be driven by the width of the [Bits] in the expression.
let x = bits::<32>(0xBABE);
let z = 0xB_BABE - x;
assert_eq!(z, bits(0xB_0000));
Bitwise And
You can combine [Bits] using the and operator &
. In general, avoid using the shortcut
logical operator &&
, since this operator is really only defined for logical (scalar) values
of type bool
.
let x: Bits<32> = bits(0xDEAD_BEEF);
let y: Bits<32> = bits(0xFFFF_0000);
let z = x & y;
assert_eq!(z, bits(0xDEAD_0000));
Of course, you can also use a literal value in the and
operation.
let x: Bits<32> = bits(0xDEAD_BEEF);
let z = x & 0x0000_FFFF;
assert_eq!(z, bits(0xBEEF))
and similarly, the literal can appear on the left of the and
expression.
let x: Bits<32> = bits(0xCAFE_BEEF);
let z = 0xFFFF_0000 & x;
assert_eq!(z, bits(0xCAFE_0000));
Just like all other binary operations, you cannot mix widths (unless one of the values is a literal).
let x: Bits<16> = bits(0xFEED_FACE);
let y: Bits<17> = bits(0xABCE);
let z = x & y; // Won't compile!
Bitwise Or
There is also a bitwise-OR operation using the |
symbol. Note that the logical OR
(or shortcut OR) operator ||
is not supported for [Bits], as it is only defined for
scalar boolean values.
let x : Bits<32> = bits(0xBEEF_0000);
let y : Bits<32> = bits(0x0000_CAFE);
let z = x | y;
assert_eq!(z, bits(0xBEEF_CAFE));
You can also use literals
let x : Bits<32> = bits(0xBEEF_0000);
let z = x | 0x0000_CAFE;
assert_eq!(z, bits(0xBEEF_CAFE));
The caveat about mixing [Bits] of different widths still applies.
Bitwise Xor
There is a bitwise-Xor operation using the ^
operator. This will compute the
bitwise exclusive OR of the two values.
let x : Bits<32> = bits(0xCAFE_BABE);
let y : Bits<32> = bits(0xFF00_00FF);
let z = y ^ x;
let w = z ^ y; // XOR applied twice is a null-op
assert_eq!(w, x);
Bitwise comparison
The equality operator ==
can compare two [Bits] for bit-wise equality.
let x: Bits<16> = bits(0x5ea1);
let y: Bits<16> = bits(0xbadb);
assert_eq!(x == y, false)
Again, it is a compile time failure to attempt to compare [Bits] of different widths.
let x: Bits<15> = bits(52);
let y: Bits<16> = bits(32);
let z = x == y; // Won't compile - bit widths must match
You can compare to literals, as they will automatically extended (or truncated) to match the bitwidth of the [Bits] value.
let x : Bits<16> = bits(32);
let z = x == 32;
let y = 32 == x;
assert!(z);
assert!(y);
Unsigned comparison
The [Bits] type only supports unsigned comparisons. If you compare a [Bits] value to a signed integer, it will first convert the signed integer into 2s complement representation and then perform an unsigned comparison. That is most likely not what you want. However, until there is full support for signed integer computations, that is the behavior you get. Hardware signed comparisons require more circuitry and logic than unsigned comparisons, so the rationale is to not inadvertently bloat your hardware designs with sign-aware circuitry when you don’t explicitly invoke it. If you want signed values, use [Signed].
Here are some simple examples.
let x: Bits<16> = bits(52);
let y: Bits<16> = bits(13);
assert!(y < x)
We can also compare with literals, which RustHDL will expand out to match the bit width of the [Bits] being compared to.
let x: Bits<16> = bits(52);
let y = x < 135; // Converts the 135 to a Bits<16> and then compares
assert!(y)
Shift Left
RustHDl supports left shift bit operations using the <<
operator.
Bits that shift off the left end of
the bit vector (most significant bits).
let x: Bits<16> = bits(0xDEAD);
let y = x << 8;
assert_eq!(y, bits(0xAD00));
Shift Right
Right shifting is also supported using the >>
operator.
Bits that shift off the right end of the
the bit vector (least significant bits).
let x: Bits<16> = bits(0xDEAD);
let y = x >> 8;
assert_eq!(y, bits(0x00DE));
Enums
- The Bits type holds a bit array of size [N].
Constants
- LITERAL_BITS is set to the number of bits in the LiteralType. I.e., it is guaranteed that the number of bits in LiteralType is LITERAL_BITS.
Traits
Functions
- Cast from one bit width to another with truncation or zero padding The bit_cast function allows you to convert from one bit width to another. It handles the different widths in the following simplified manner: - if casting to a narrower bit width, the most significant bits are discarded until the new value fits into the specified bits - if casting to a wider bit width, the most significant bits are padded with zeros until the new value occupies the specified bits This may seem a bit counterintuitive, but it fits logical circuitry behavior. Narrowing is usually done by preserving the least significant bits (so that the carry bits are discarded when adding, for example). Widening is also usually done (for unsigned values) by zero extending the most significant bits. The bit_cast operation does both of these operations depending on the arguments.
- Compute the minimum number of bits to represent a container with t items. This is basically
ceil(log2(t))
as a constant (compile time computable) function. You can use it where a const generic (bit width) argument is required.
Type Definitions
- A type alias for a simple bool. You can use them interchangeably.
- The LiteralType is used to set the type for literals that appear in RustHDL expressions. Because of how Rust’s type inference currently works, an expression like