Fields — the basic typed values
A field is the smallest typed unit of data in a Waveflow design: a single value with an explicit bit width and interpretation (integer, float, …). Everything larger is built out of fields — structured records (Data Lists) and typed arrays (Data Arrays).
Why bit widths are explicit — the hardware mindset
In ordinary software a number is “just an int”: the language picks 32 or 64 bits and you
never think about it. In hardware, every value has a chosen bit width, because each bit
is physical — it costs flip-flops, wires, logic, power, and timing margin. A counter that
only ever reaches 1000 needs 10 bits, not 32; the other 22 bits are wasted silicon. So
in Waveflow a field’s width is part of its type, declared up front, and it maps directly
to the arbitrary-precision types Vitis HLS uses (ap_int<W> and friends).
The first surprise coming from software is exactly this: you size every number.
The two simple fields
Waveflow has two basic numeric fields. (A third — fixed-point — is important enough in DSP to get its own page; we set it aside here.)
IntField — arbitrary-precision integer
from waveflow.hw.dataschema import IntField
Int16 = IntField.specialize(bitwidth=16, signed=True) # signed 16-bit
UInt8 = IntField.specialize(bitwidth=8, signed=False) # unsigned 8-bit
Flag = IntField.specialize(bitwidth=1, signed=False) # a single bit
bitwidth can be anything from 1 to thousands of bits — you pick exactly what the value
needs.
FloatField — IEEE floating point
from waveflow.hw.dataschema import FloatField
Float32 = FloatField.specialize(bitwidth=32) # IEEE single precision
Float64 = FloatField.specialize(bitwidth=64) # IEEE double precision
BooleanField — a one-bit flag
A boolean flag is common enough to get its own type. BooleanField is a fixed
ap_uint<1> (an IntField subclass), but its .val is a Python bool:
from waveflow.hw.dataschema import BooleanField
enable = BooleanField(True)
enable.val # True (a Python bool)
It accepts bool or 0 / 1 and rejects any other value, so a flag can never
silently hold 2. It packs into exactly one bit (ap_uint<1>), so several flags cost
only a few bits in a record — handy for m_axi control words. Use it directly (no
specialize needed); the equivalent raw form is the one-bit IntField shown above.
Why arbitrary-precision integers matter in hardware
Software integers come in a few fixed sizes (8/16/32/64). Hardware has no such restriction —
you build a register exactly as wide as you need, and narrower is better: less area,
less power, shorter carry chains (so a higher clock speed), and room to pack more parallel
units onto the chip. A 12-bit ADC sample is a 12-bit value; a counter over five states is 3
bits. IntField’s free choice of bitwidth models this directly (it lowers to
ap_int<W> / ap_uint<W>), so your Python model carries the same precision the
hardware will — no accidental 32-bit assumptions creeping in.
Why floating point is “expensive” in hardware
Floating point is wonderfully convenient in software — huge dynamic range, no manual
scaling — but in hardware an FP unit is big and slow. An FP multiply or add consumes
several DSP blocks plus extra logic for exponent handling and normalization, and takes
multiple pipeline cycles; the same operation in integer or fixed-point can be a single DSP
in one cycle. So on a high-throughput datapath, designers usually avoid floating point
and work in integer/fixed-point instead — accepting the burden of choosing scales and
widths in exchange for far less area, power, and latency. FloatField is still useful (for
golden references and non-critical control values), but the performance-critical numeric
work is where fixed-point comes in.
Declaring and using fields
A specialized field is a type; make an instance by calling it with a value, and read or
write the value through .val:
x = Int16(42)
print(x.val) # 42
x.val = -7 # set a new value
f = Float32(1.5)
print(f.val) # 1.5
Fields know how to serialize themselves to the packed bit representation used in simulation and in generated C++ — that machinery is shared with the larger schema types and is covered under Code Generation.
Modeling overflow: wrap vs saturate
What happens when a value doesn’t fit its bit width? Hardware does not raise an exception — it does one of two things, and which one is a design choice you model explicitly:
- Wrap (two’s-complement overflow) — the high bits are simply dropped, so the value “wraps around.” Cheap (it’s just truncation) and the default behavior of plain integer arithmetic.
- Saturate — the value is clamped to the largest/smallest representable, so an overshoot sticks at the maximum instead of wrapping to a negative. Costs a comparator, but is far safer for signals (a saturating adder never flips a loud sample into a quiet one).
Waveflow provides both as vectorized helpers:
from waveflow.utils.fixputils import truncate, saturate
# An 8-bit signed value holds [-128, 127]. What becomes of 200?
truncate(200, wid=8, signed=True) # wrap -> -56
saturate(200, wid=8, signed=True) # clamp -> 127
# They work on numpy arrays too (vectorized — element-wise):
import numpy as np
saturate(np.array([200, -300, 50]), wid=8, signed=True) # -> [127, -128, 50]
These same two behaviors, applied automatically on every assignment, are what the overflow
modes AP_WRAP / AP_SAT select for fixed-point fields.
Next: group fields into records with Data Lists, or into typed arrays with Data Arrays.