Skip to content

Commit

Permalink
gateware: update testbenches, squash more amaranth 0.5 warnings (#40)
Browse files Browse the repository at this point in the history
* update all unit testbenches to amaranth 0.5, add to CI
* add utilities for testing wishbone transactions against amaranth-soc peripherals (and i2c example)
* switch from `ast` to `hdl` top-level library where possible to squash amaranth 0.5 warnings
* switch i2c implementation from luna to glasgow implementation to squash Record warnings.
  • Loading branch information
vk2seb authored Sep 24, 2024
2 parents 24102fb + 1870a33 commit a46f35e
Show file tree
Hide file tree
Showing 13 changed files with 620 additions and 263 deletions.
11 changes: 11 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,17 @@ name: build & test
on: [push]

jobs:

unit-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pdm-project/setup-pdm@v4
- run: |
pdm install
pdm test
working-directory: gateware
ubuntu-usb-audio:
runs-on: ubuntu-latest
steps:
Expand Down
100 changes: 66 additions & 34 deletions gateware/src/amaranth_future/fixed.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
# from https://github.com/amaranth-lang/amaranth/pull/1005
# slightly modified to work out-of-tree with Amaranth ~= 0.4

from amaranth.hdl import ast
from amaranth import hdl
from amaranth.utils import bits_for

__all__ = ["Shape", "SQ", "UQ", "Value", "Const"]

class Shape(ast.ShapeCastable):
class Shape(hdl.ShapeCastable):
def __init__(self, i_or_f_width, f_width = None, /, *, signed):
if f_width is None:
self.i_width, self.f_width = 0, i_or_f_width
Expand All @@ -17,7 +17,7 @@ def __init__(self, i_or_f_width, f_width = None, /, *, signed):

@staticmethod
def cast(shape, f_width=0):
if not isinstance(shape, ast.Shape):
if not isinstance(shape, hdl.Shape):
raise TypeError(f"Object {shape!r} cannot be converted to a fixed.Shape")

# i_width is what's left after subtracting f_width and sign bit, but can't be negative.
Expand All @@ -26,7 +26,7 @@ def cast(shape, f_width=0):
return Shape(i_width, f_width, signed = shape.signed)

def as_shape(self):
return ast.Shape(self.signed + self.i_width + self.f_width, self.signed)
return hdl.Shape(self.signed + self.i_width + self.f_width, self.signed)

def __call__(self, target):
return Value(self, target)
Expand All @@ -36,6 +36,11 @@ def const(self, value):
value = 0
return Const(value, self)._target

def from_bits(self, raw):
c = Const(0, self)
c._value = raw
return c

def max(self):
c = Const(0, self)
c._value = c._max_value()
Expand All @@ -60,7 +65,7 @@ def __init__(self, *args):
super().__init__(*args, signed = False)


class Value(ast.ValueCastable):
class Value(hdl.ValueCastable):
def __init__(self, shape, target):
self._shape = shape
self._target = target
Expand All @@ -72,16 +77,16 @@ def cast(value, f_width=0):
def round(self, f_width=0):
# If we're increasing precision, extend with more fractional bits.
if f_width > self.f_width:
return Shape(self.i_width, f_width, signed = self.signed)(ast.Cat(ast.Const(0, f_width - self.f_width), self.sas_value()))
return Shape(self.i_width, f_width, signed = self.signed)(hdl.Cat(hdl.Const(0, f_width - self.f_width), self.raw()))

# If we're reducing precision, truncate bits and add the top truncated bits for rounding.
elif f_width < self.f_width:
return Shape(self.i_width, f_width, signed = self.signed)(self.sas_value()[self.f_width - f_width:] + self.sas_value()[self.f_width - f_width - 1])
return Shape(self.i_width, f_width, signed = self.signed)(self.raw()[self.f_width - f_width:] + self.raw()[self.f_width - f_width - 1])

return self

def truncate(self, f_width=0):
return Shape(self.i_width, f_width, signed = self.signed)(self.sas_value()[self.f_width - f_width:])
return Shape(self.i_width, f_width, signed = self.signed)(self.raw()[self.f_width - f_width:])

@property
def i_width(self):
Expand All @@ -95,13 +100,15 @@ def f_width(self):
def signed(self):
return self._shape.signed

@ast.ValueCastable.lowermethod
def as_value(self):
# FIXME For some reason lib.wiring breaks if we return
# signed values from here, not sure why.
return self._target

def sas_value(self):
def raw(self):
"""
Adding an `s ( )` signedness wrapper in `as_value` when needed
breaks lib.wiring for some reason. In the future, raw() and
`as_value()` should be combined.
"""
if self.signed:
return self._target.as_signed()
return self._target
Expand All @@ -111,8 +118,8 @@ def shape(self):

def eq(self, other):
# Regular values are assigned directly to the underlying value.
if isinstance(other, ast.Value):
return self.sas_value().eq(other)
if isinstance(other, hdl.Value):
return self.raw().eq(other)

# int and float are cast to fixed.Const.
elif isinstance(other, int) or isinstance(other, float):
Expand All @@ -125,11 +132,11 @@ def eq(self, other):
# Match precision.
other = other.round(self.f_width)

return self.sas_value().eq(other.sas_value())
return self.raw().eq(other.raw())

def __mul__(self, other):
# Regular values are cast to fixed.Value
if isinstance(other, ast.Value):
if isinstance(other, hdl.Value):
other = Value.cast(other)

# int are cast to fixed.Const
Expand All @@ -140,14 +147,14 @@ def __mul__(self, other):
elif not isinstance(other, Value):
raise TypeError(f"Object {other!r} cannot be converted to a fixed.Value")

return Value.cast(self.sas_value() * other.sas_value(), self.f_width + other.f_width)
return Value.cast(self.raw() * other.raw(), self.f_width + other.f_width)

def __rmul__(self, other):
return self.__mul__(other)

def __add__(self, other):
# Regular values are cast to fixed.Value
if isinstance(other, ast.Value):
if isinstance(other, hdl.Value):
other = Value.cast(other)

# int are cast to fixed.Const
Expand All @@ -160,14 +167,14 @@ def __add__(self, other):

f_width = max(self.f_width, other.f_width)

return Value.cast(self.round(f_width).sas_value() + other.round(f_width).sas_value(), f_width)
return Value.cast(self.round(f_width).raw() + other.round(f_width).raw(), f_width)

def __radd__(self, other):
return self.__add__(other)

def __sub__(self, other):
# Regular values are cast to fixed.Value
if isinstance(other, ast.Value):
if isinstance(other, hdl.Value):
other = Value.cast(other)

# int are cast to fixed.Const
Expand All @@ -180,7 +187,7 @@ def __sub__(self, other):

f_width = max(self.f_width, other.f_width)

return Value.cast(self.round(f_width).sas_value() - other.round(f_width).sas_value(), f_width)
return Value.cast(self.round(f_width).raw() - other.round(f_width).raw(), f_width)

def __rsub__(self, other):
return -self.__sub__(other)
Expand All @@ -189,37 +196,37 @@ def __pos__(self):
return self

def __neg__(self):
return Value.cast(-self.sas_value(), self.f_width)
return Value.cast(-self.raw(), self.f_width)

def __abs__(self):
return Value.cast(abs(self.sas_value()), self.f_width)
return Value.cast(abs(self.raw()), self.f_width)

def __lshift__(self, other):
if isinstance(other, int):
if other < 0:
raise ValueError("Shift amount cannot be negative")

if other > self.f_width:
return Value.cast(ast.Cat(ast.Const(0, other - self.f_width), self.sas_value()))
return Value.cast(hdl.Cat(hdl.Const(0, other - self.f_width), self.raw()))
else:
return Value.cast(self.sas_value(), self.f_width - other)
return Value.cast(self.raw(), self.f_width - other)

elif not isinstance(other, ast.Value):
elif not isinstance(other, hdl.Value):
raise TypeError("Shift amount must be an integer value")

if other.signed:
raise TypeError("Shift amount must be unsigned")

return Value.cast(self.sas_value() << other, self.f_width)
return Value.cast(self.raw() << other, self.f_width)

def __rshift__(self, other):
if isinstance(other, int):
if other < 0:
raise ValueError("Shift amount cannot be negative")

return Value.cast(self.sas_value(), self.f_width + other)
return Value.cast(self.raw(), self.f_width + other)

elif not isinstance(other, ast.Value):
elif not isinstance(other, hdl.Value):
raise TypeError("Shift amount must be an integer value")

if other.signed:
Expand All @@ -228,14 +235,31 @@ def __rshift__(self, other):
# Extend f_width by maximal shift amount.
f_width = self.f_width + 2**other.width - 1

return Value.cast(self.round(f_width).sas_value() >> other, f_width)
return Value.cast(self.round(f_width).raw() >> other, f_width)

def __lt__(self, other):
return self.__sub__(other).sas_value() < 0
if isinstance(other, hdl.Value):
other = Value.cast(other)
elif isinstance(other, int):
other = Const(other)
elif not isinstance(other, Value):
raise TypeError(f"Object {other!r} cannot be converted to a fixed.Value")
f_width = max(self.f_width, other.f_width)
return self.round(f_width).raw() < other.round(f_width).raw()

def __ge__(self, other):
return ~self.__lt__(other)

def __eq__(self, other):
if isinstance(other, hdl.Value):
other = Value.cast(other)
elif isinstance(other, int):
other = Const(other)
elif not isinstance(other, Value):
raise TypeError(f"Object {other!r} cannot be converted to a fixed.Value")
f_width = max(self.f_width, other.f_width)
return self.round(f_width).raw() == other.round(f_width).raw()

def __repr__(self):
return f"(fixedpoint {'SQ' if self.signed else 'UQ'}{self.i_width}.{self.f_width} {self._target!r})"

Expand All @@ -245,7 +269,11 @@ def __init__(self, value, shape=None):

if isinstance(value, float) or isinstance(value, int):
num, den = value.as_integer_ratio()

elif isinstance(value, Const):
# FIXME: Memory inits seem to construct a fixed.Const with fixed.Const
self._shape = value._shape
self._value = value._value
return
else:
raise TypeError(f"Object {value!r} cannot be converted to a fixed.Const")

Expand Down Expand Up @@ -286,12 +314,16 @@ def _min_value(self):

@property
def _target(self):
return ast.Const(self._value, self._shape.as_shape())
return hdl.Const(self._value, self._shape.as_shape())

def as_integer_ratio(self):
return self._value, 2**self.f_width

def as_float(self):
return self._value / 2**self.f_width
if self._value > self._max_value():
v = self._min_value() + self._value - self._max_value()
else:
v = self._value
return v / 2**self.f_width

# TODO: Operators
4 changes: 2 additions & 2 deletions gateware/src/tiliqua/dsp.py
Original file line number Diff line number Diff line change
Expand Up @@ -365,7 +365,7 @@ def elaborate(self, platform):
if self.continuous:
m.d.comb += rport.addr.eq(x.truncate()+1)
else:
with m.If((x.truncate()).sas_value() ==
with m.If((x.truncate()).raw() ==
2**(self.lut_addr_width-1)-1):
m.d.comb += trunc.eq(1)
m.d.comb += rport.addr.eq(x.truncate())
Expand Down Expand Up @@ -1032,7 +1032,7 @@ def elaborate(self, platform):

# accumulator maintenance
m.d.sync += fifo_r_en_l.eq(fifo.r_en)
m.d.comb += fifo_r_asq.sas_value().eq(fifo.r_data) # raw -> ASQ
m.d.comb += fifo_r_asq.raw().eq(fifo.r_data) # raw -> ASQ
with m.If(self.i.valid & self.i.ready):
with m.If(fifo_r_en_l):
# sample in + out simultaneously (normal case)
Expand Down
2 changes: 1 addition & 1 deletion gateware/src/tiliqua/eurorack_pmod.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ class EurorackPmod(wiring.Component):
eeprom_serial: Out(32)

# Bitwise manual LED overrides. 1 == audio passthrough, 0 == manual set.
led_mode: In(8, reset=0xff)
led_mode: In(8, init=0xff)
# If an LED is in manual, this is signed i8 from -green to +red
led: In(8).array(8)

Expand Down
2 changes: 1 addition & 1 deletion gateware/src/tiliqua/i2c.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from amaranth.lib.wiring import Component, In, Out, flipped, connect
from amaranth.lib.fifo import SyncFIFO
from amaranth_soc import csr, gpio
from luna.gateware.interface.i2c import I2CInitiator
from vendor.i2c import I2CInitiator

class PinSignature(wiring.Signature):
def __init__(self):
Expand Down
8 changes: 4 additions & 4 deletions gateware/src/tiliqua/raster.py
Original file line number Diff line number Diff line change
Expand Up @@ -306,10 +306,10 @@ def elaborate(self, platform) -> Module:
# Fired on every audio sample fs_strobe
with m.If(point_stream.valid):
m.d.sync += [
sample_x.eq((point_stream.payload[0].sas_value()>>self.scale_x) + self.x_offset),
sample_y.eq((point_stream.payload[1].sas_value()>>self.scale_y) + self.y_offset),
sample_p.eq(point_stream.payload[2].sas_value()),
sample_c.eq(point_stream.payload[3].sas_value()),
sample_x.eq((point_stream.payload[0].raw()>>self.scale_x) + self.x_offset),
sample_y.eq((point_stream.payload[1].raw()>>self.scale_y) + self.y_offset),
sample_p.eq(point_stream.payload[2].raw()),
sample_c.eq(point_stream.payload[3].raw()),
sample_intensity.eq(self.intensity),
]
m.next = 'LATCH1'
Expand Down
4 changes: 2 additions & 2 deletions gateware/src/tiliqua/scope.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ def elaborate(self, platform):
m.d.sync += s.intensity.eq(self._intensity.f.intensity.w_data)

with m.If(self._timebase.f.timebase.w_stb):
m.d.sync += self.timebase.sas_value().eq(self._timebase.f.timebase.w_data)
m.d.sync += self.timebase.raw().eq(self._timebase.f.timebase.w_data)

with m.If(self._xscale.f.xscale.w_stb):
for s in self.strokes:
Expand All @@ -210,7 +210,7 @@ def elaborate(self, platform):
m.d.sync += s.scale_y.eq(self._yscale.f.yscale.w_data)

with m.If(self._trigger_lvl.f.trigger_level.w_stb):
m.d.sync += self.trigger_lvl.sas_value().eq(self._trigger_lvl.f.trigger_level.w_data)
m.d.sync += self.trigger_lvl.raw().eq(self._trigger_lvl.f.trigger_level.w_data)

for i, ypos_reg in enumerate(self._ypos):
with m.If(ypos_reg.f.ypos.w_stb):
Expand Down
Loading

0 comments on commit a46f35e

Please sign in to comment.