[plum] Tutorial: Bit Field Structure Members

The BitFields data store type provides a mechanism to embed, or nest a collection of bit fields within a single structure member. This tutorial demonstrates an alternative that avoids nesting, using the bitfield_member() function to define structure members as bit fields (members that hold sequences of bits).

The examples shown on this page require the following setup:

>>> from enum import IntEnum
>>> from plum.bigendian import uint8
>>> from plum.enum import EnumX
>>> from plum.structure import Structure, bitfield_member, member
>>> from plum.utilities import pack, unpack
>>>
>>> class Register(IntEnum):
...     PC = 0
...     SP = 1
...     R0 = 2
...

Basic Usage

The member() function accepts a fmt that provides the transform between a structure member value and bytes. The func:bitfield_member function instead accepts size, lsb, and typ arguments to define a transform between a structure member value and a sequence of bits (typ argument defaults to a Python int):

>>> class Sample(Structure):
...    nibble1: int = bitfield_member(size=4, lsb=4)
...    nibble2: int = bitfield_member(size=4, lsb=0)
...    byte: int = member(fmt=uint8)
...
>>> pack(Sample(nibble1=1, nibble2=2, byte=3)).hex()
'1203'

In applications such as the example above where there is no gap between the bit fields, let the lsb parameter default to None. Then the bit field member automatically calculates the position based on the position and size of the bit fields that follow. You may also let the typ parameter default. If a type annotation exists, the bit field member uses it, otherwise it defaults to int:

>>> class Sample(Structure):
...    nibble1: int = bitfield_member(size=4)
...    nibble2: int = bitfield_member(size=4)
...    byte: int = member(fmt=uint8)
...
>>> pack(Sample(nibble1=1, nibble2=2, byte=3)).hex()
'1203'

In the example above, the bitfield order defaulted to be from most significant to the least. To reverse, set fieldorder="least_to_most":

>>> class Sample(Structure, fieldorder="least_to_most"):
...    nibble1: int = bitfield_member(size=4)
...    nibble2: int = bitfield_member(size=4)
...    byte: int = member(fmt=uint8)
...
>>> pack(Sample(nibble1=1, nibble2=2, byte=3)).hex()
'2103'

For typ, you may use any callable that accepts an integer and produces something capable of being converted to an integer (ie. int() can be called with it as an argument). Examples include a simple integer enumeration type, an integer flag enumeration type, or even a custom subclass of BitFields. The following examples shows use of a simple enumeration type:

>>> class Sample(Structure):
...    reg1: Register = bitfield_member(lsb=0, size=4, typ=Register)
...    reg2: int = bitfield_member(lsb=4, size=4, typ=EnumX(Register, strict=False))
...    byte: int = member(fmt=uint8)
...
>>> sample = unpack(Sample, b'\x21\x03')
>>> sample.dump()
+--------+--------+-------------+----------+--------------------+
| Offset | Access | Value       | Bytes    | Format             |
+--------+--------+-------------+----------+--------------------+
|        |        |             |          | Sample (Structure) |
| 0      |        | 33          | 21       |                    |
|  [0:4] | reg1   | Register.SP | ....0001 | Register           |
|  [4:8] | reg2   | Register.R0 | 0010.... | Register (IntEnum) |
| 1      | byte   | 3           | 03       | uint8              |
+--------+--------+-------------+----------+--------------------+
>>>
>>> # non-strict enum transform allows integers not in enumeration to come through
>>> sample = unpack(Sample, b'\x31\x00')
>>> sample.reg2
3

For bit fields that store a signed integer, set signed=True:

>>> class Sample(Structure):
...    nibble1: int = bitfield_member(lsb=0, size=4, signed=True)
...    nibble2: int = bitfield_member(lsb=4, size=4)
...    byte: int = member(fmt=uint8)
...
>>> pack(Sample(nibble1=-1, nibble2=0, byte=3)).hex()
'0f03'

The bitfield_member() function accepts the following keyword arguments that have the same behaviors as those in a normal member() function:

default:default value to use in initializer when one not passed
ignore:ignore member when comparing against another structure instance
readonly:block setting member attribute
compute:automatically compute based on another member value (use in combination with sized_member(), dimmed_member(), etc.)

Reserving Bits

The bitfield_member() function nbytes parameter controls the number of bytes the bit field (and all bit fields that immediately follow that don’t set the nbytes explicitly) occupy. When left unspecified, the number of bytes becomes just large enough to accommodate the bit fields. Within the first bit field member defiinition, specify nbytes explicitly to reserve extra bits without defining an additional member for them:

>>> class Sample(Structure):
...    nibble1: int = bitfield_member(lsb=0, size=4, nbytes=2)
...    nibble2: int = bitfield_member(lsb=4, size=4)
...    byte: int = member(fmt=uint8)
...
>>> pack(Sample(nibble1=1, nibble2=2, byte=3)).hex()
'210003'

When leaving lsb unspecified, setting nbytes a second time has the net effect of resetting the automatically computed position to honor the first nbytes. The following example demonstrates two equivalent structures, one explicitly specifying lsb, one not.

>>> class Sample1(Structure):
...    nibble1: int = bitfield_member(size=4, nbytes=2)
...    nibble2: int = bitfield_member(size=4, nbytes=2)
...    byte: int = member(fmt=uint8)
...
>>> class Sample2(Structure):
...    nibble1: int = bitfield_member(lsb=0, size=4, nbytes=4)
...    nibble2: int = bitfield_member(lsb=16, size=4)
...    byte: int = member(fmt=uint8)
...
>>> sample1 = Sample1(nibble1=1, nibble2=2, byte=3)
>>> sample2 = Sample2(nibble1=1, nibble2=2, byte=3)
>>> pack(sample1) == pack(sample2)
True

Tip

The Sample2 has slightly better performance.

Byte Order

Structure members defined with the member() function follow the byte order of the format specified with the fmt argument. Since the bitfield_member() function does not accept a format, the structure controls the byte order of the bit fields. By default, structures use little endian. The Structure metaclass accepts a byteorder keyword argument to change the byteorder to big endian.

>>> class BigSample(Structure, byteorder="big"):
...    nibble1: int = bitfield_member(lsb=0, size=4, nbytes=2)
...    nibble2: int = bitfield_member(lsb=4, size=4)
...    byte: int = member(fmt=uint8)
...
>>> pack(BigSample(nibble1=1, nibble2=2, byte=3)).hex()
'002103'
>>> class LittleSample(Structure):
...    nibble1: int = bitfield_member(lsb=0, size=4, nbytes=2)
...    nibble2: int = bitfield_member(lsb=4, size=4)
...    byte: int = member(fmt=uint8)
...
>>> pack(LittleSample(nibble1=1, nibble2=2, byte=3)).hex()
'210003'