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

Traits

  • The ToBits trait is used to provide a way to convert Rust standard unsigned types (currently u8, u16, u32, u64, u128) into Bits of different lengths. Note that RustHDL will panic if you attempt to convert an unsigned type into a Bits that is too small to hold the value.

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.
  • Convenience function to construct Bits from an unsigned literal Sometimes, you know you will be working with a value that is smaller than 128 bits (the current maximum sized built-in unsigned integer in Rust). In those cases, the bits function can make construction slightly simpler.
  • 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