packed_struct.rs icon indicating copy to clipboard operation
packed_struct.rs copied to clipboard

Question on LSB0 endianness

Open summivox opened this issue 2 years ago • 3 comments

Let's say I'm trying to interpret a u32 as the following bit-packed struct (bit index: 0=lsb, 31=msb)

bit 31, 30, ..., 15, 16 15, 14, 13, 12 11, 10, 9, 8 7, 6, 5, 4 3, 2, 1, 0
field e d c b a

What I expect: 0x12345678 should unpack into:

  • a == 0x8
  • b == 0x7
  • c == 0x6
  • d == 0x5
  • e == 0x1234

Note that 0x12345678 in little-endian memory is [0x78, 0x56, 0x34, 0x12].

I attempted to implement this as follows:

#!/usr/bin/env rust-script
// cargo-deps: packed_struct="0.10"

use packed_struct::prelude::*;

#[derive(Copy, Clone, Debug, Eq, PartialEq, PackedStruct)]
#[packed_struct(size_bytes="4", bit_numbering="lsb0", endian="lsb")]
pub struct Pack {
    #[packed_field(bits="0..4")]
    pub a: u8,
    #[packed_field(bits="4..8")]
    pub b: u8,
    #[packed_field(bits="8..12")]
    pub c: u8,
    #[packed_field(bits="12..16")]
    pub d: u8,
    #[packed_field(bits="16..32")]
    pub e: u16,
}

pub fn main() {
    {
        let packed = 0x12345678u32;
        let packed_bytes = packed.to_le_bytes();
        let fields = Pack::unpack(&packed_bytes).unwrap();
        println!("{}", fields);
        assert_eq!(packed_bytes, [0x78u8, 0x56u8, 0x34u8, 0x12u8]);
        assert_eq!(fields, Pack { a: 0x8, b: 0x7, c: 0x6, d: 0x5, e: 0x1234 });
    }
    {
        let fields = Pack { a: 0x8, b: 0x7, c: 0x6, d: 0x5, e: 0x1234 };
        let packed_bytes = fields.pack().unwrap();
        let packed = u32::from_le_bytes(packed_bytes);
        println!("{}", fields);
        assert_eq!(packed_bytes, [0x78u8, 0x56u8, 0x34u8, 0x12u8]);
        assert_eq!(packed, 0x12345678u32);
    }
}

It fails, printing:

Pack (4 bytes)

Decimal
[120, 86, 52, 18]

Hex
[0x78, 0x56, 0x34, 0x12]

Binary
[0b01111000, 0b01010110, 0b00110100, 0b00010010]

 a | bits  28:31  | 0b0010             | "2"
 b | bits  24:27  | 0b0001             | "1"
 c | bits  20:23  | 0b0100             | "4"
 d | bits  16:19  | 0b0011             | "3"
 x | bits   0:15  | 0b0111100001010110 | "22136"

thread 'main' panicked at 'assertion failed: `(left == right)`
  left: `Pack { a: 2, b: 1, c: 4, d: 3, x: 22136 }`,
 right: `Pack { a: 8, b: 7, c: 6, d: 5, x: 4660 }`', pack_test.rs:27:9

I specified a to be the least significant 4 bits, i.e. packed[3:0] in Verilog notation. However:

  • The debug printing shows that a is bits 28:31.
  • The actual value of a is packed[27:24].

Also, for sure 0b0111100001010110 != 22136...

Why?

summivox avatar Aug 20 '22 20:08 summivox

A useful hint might be that the struct-level endian parameter only sets the endianness per each field, not for the entire struct. That has to be done manually if needed. This library is really endianness agnostic by only providing arrays as the interface for structs. Also, the formatter only ever prints out the debug fields in MSB0 notation, that's why it looks a bit odd compared to the struct.

So by changing the field-level endiannes to msb, and only taking your first two paragraphs into account, then I believe that the example is consistent. Try to work from there to fit your needs.

use packed_struct::prelude::*;

#[derive(Copy, Clone, Debug, Eq, PartialEq, PackedStruct)]
#[packed_struct(size_bytes="4", bit_numbering="lsb0", endian="msb")]
pub struct Pack {
    #[packed_field(bits="0..4")]
    pub a: u8,
    #[packed_field(bits="4..8")]
    pub b: u8,
    #[packed_field(bits="8..12")]
    pub c: u8,
    #[packed_field(bits="12..16")]
    pub d: u8,
    #[packed_field(bits="16..32")]
    pub e: u16,
}

#[test]
#[cfg(test)]
pub fn issue_92() {
    // packing
    let fields = Pack { a: 0x8, b: 0x7, c: 0x6, d: 0x5, e: 0x1234 };
    println!("{}", fields);

    let packed_bytes = fields.pack().unwrap();
    assert_eq!(packed_bytes, [0x12u8, 0x34u8, 0x56u8, 0x78u8]);

    let packed_int = u32::from_be_bytes(packed_bytes);
    assert_eq!(packed_int, 0x12345678u32);
        
    // unpacking
    let unpacked = Pack::unpack(&packed_bytes).unwrap();
    assert_eq!(unpacked, fields);
}

rudib avatar Aug 22 '22 21:08 rudib

Thanks. Confirmed that endian="msb" together with to_be_bytes gives the correct result. However, if the library is truly endianness agnostic, why does it not work if endian="lsb" is used together with to_le_bytes?

summivox avatar Aug 23 '22 04:08 summivox

Hit a similar problem myself, and in that case, I cannot simply call from_le_bytes on the struct being unpacked, because it's normally nested three levels deep in other packed structs that get read and unpacked or packed and written as a unit. Given that the library is already endian-aware to some extent, would it make sense to add another specifier or extend the functionality of endian to control how the byte array is interpreted?

VyrCossont avatar Sep 01 '22 17:09 VyrCossont