Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
198 changes: 198 additions & 0 deletions text/0072-stdlib-lfsr.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
- Start Date: 2024-08-04
- RFC PR: [amaranth-lang/rfcs#72](https://github.com/amaranth-lang/rfcs/pull/72)
- Amaranth Issue: [amaranth-lang/amaranth#0000](https://github.com/amaranth-lang/amaranth/issues/0000)

# LFSR generator

## Summary
[summary]: #summary

Add a linear feedback shift register (LFSR) generator to the Amaranth standard
library, with pre-defined generators for standard PRBS sequences.

## Motivation
[motivation]: #motivation

Many digital designs require the generation of pseudo-random bit sequences,
for purposes such as link error detection, data whitening, generating training
signals, or low-overhead generation of random noise signals. One common and
efficient way to generate maximum-length pseudo-random bit sequences is a
linear feedback shift register (LFSR).

Because an LFSR is fully characterised by its generating polynomial (or,
equivalently, by the set of feedback taps), it is straightforward to support
a wide range of use cases in an optimal implementation that can be used by
most Amaranth users. In particular, the well-known PRBS sequences can be
provided as pre-defined LFSRs.

See the [Wikipedia page on LFSRs] for more background and use cases.

[Wikipedia page on LFSRs]: https://en.wikipedia.org/wiki/Linear-feedback_shift_register

## Guide-level explanation
[guide-level-explanation]: #guide-level-explanation

The Amaranth standard library includes a generator for a linear feedback shift
register (LFSR) module, which can be used to compute many common bit sequences.

An LFSR is characterised by two properties, stored in the `lfsr.Algorithm`
class:

* `width`: the bit-width of the feedback register
* `polynominal`: the polynomial that defines which bits are used for feedback,
excluding an implicit leading 1 for the "x^n" term
* `reverse`: whether to generate the time-reversed output sequence, equivalent
to mirroring the polynominal

Common PRBS sequences are generated using LFSRs and are provided as pre-defined
`lfsr.Algorithm` objects in `lfsr.catalog`, including PRBS7, PRBS9, PRBS11,
PRBS13, PRBS15, PRBS20, PRBS23, and PRBS31.

Settings specific to a particular instantiation of an LFSR are contained in the
`lfsr.Parameters` class, which is constructed by calling an `lfsr.Algorithm`
instance and providing the additional settings:

* `output_width`: by default 1, but may be increased to generate multiple
output bits in parallel per clock cycle
* `structure`: either `lfsr.Structure.Galois` (the default) or
`lfsr.Structure.Fibbonacci` to select the implementation technique; both
generate the same sequence but with a different time offset and sequence of
internal states, and one or the other may be preferable on some architectures

The `lfsr.Parameters` class can generate output bit sequences in software using
its `compute()` method, which returns a generator.

`lfsr.Structure` is an `Enum` with two variants, `Galois` and `Fibbonacci`.

To generate a hardware LFSR module, either call `create()` on `lfsr.Parameters`
or manually construct an `lfsr.Processor`:

```python
from amaranth.lib import lfsr
algo = lfsr.Algorithm(width=7, polynomial=0x20)
params = algo(output_width=4)
m.submodules.prbs7 = lfsr.Processor(params)
m.submodules.prbs9 = lfsr.catalog.PRBS9().create()
```

The LFSR module has an output stream that generates a new output on every cycle
where `ready` is asserted. The module can be reset to its initial state using
a `ResetInserter`. The internal state is available as the `state` output.
Whenever the module is reset, its state is loaded from the `state_init` input,
which defaults to `1`.

## Reference-level explanation
[reference-level-explanation]: #reference-level-explanation

The proposed new library items are:

* The `lfsr.Algorithm` class to hold the generic parameters, which are all passed
to the constructor by name:
* `width`: positive integer width of LFSR state register
* `polynominal`: integer constant indicating which feedback taps to use,
excluding implicit 1 in the most-significant bit position
* `reverse`: default-false boolean indicating whether the output sequence
should be time-reversed, which can be achieved by mirroring the
polynomial before logic generation
* `lfsr.Algorithm` implements `__call__()` which is used to create `lfsr.Parameters` instances
* The `lfsr.Parameters` class which holds an `lfsr.Algorithm` alongside
implementation-specific settings, provided as named arguments to the
`__call__()` method on `lfsr.Algorithm`:
* `output_width`: positive integer number of output bits to generate per clock cycle, default 1
* `structure`: either `"galois"` or `"fibonacci"` (default), which type of
implementation to generate
* `lfsr.Parameters` has the following methods:
* `compute()` to compute output words in software, returning an infinite iterator
* `create()` to generate an `lfsr.Processor`
* `algorithm()` returns the `lfsr.Algorithm` used to create this instance
* The `lfsr.Processor` class which is a `lib.wiring.Component` and implements
the hardware generator, with the following signature:
* `Out(stream.Signature(parameters.output_width, always_valid=True))`
* `state: Out(algorithm.width)`
* `state_init: In(algorithm.width)`
* An `lfsr.catalog` module which contains instances of `lfsr.Algorithm`
corresponding to commonly used PRBS sequences and the 64b66b and 128b130b
training sequences.

Further technical detail will be added as the draft implementation is developed.

## Drawbacks
[drawbacks]: #drawbacks

* LFSR generation could exist outside of the Amaranth stdlib; implementing it
here may discourage experimentation in alternative interfaces.
* Some hardware or design patterns may still require other implementation
techniques, although all options I'm aware of are covered in the proposed
design or the unresolved questions and future possibilities below.

## Rationale and alternatives
[rationale-and-alternatives]: #rationale-and-alternatives

This design implements LFSRs (and thus PRBS sequence generation) in a standard
way that should be useful to almost all users of LFSRs. There is a fairly small
design space for the actual hardware implementation and so it is unlikely an
alternative design would be significantly more efficient.

The following alternatives were considered but rejected during review of this RFC:

* We could simplify the implementation and API by only supporting Galois or
only Fibonacci implementations, with the downside of potentially less
efficient implementations on some platforms.
I haven't researched how serious an impact this would be.
* We could not support multi-bit generation, but this is a useful feature for
many applications.
* The split between Algorithm and Parameters mirrors that in `lib.crc`,
but with fewer parameters we could consider merging them or making the
Parameters settings be part of the Processor creation.
* We could support internally-inverted LFSRs using XNOR operations, which allow
the all-0 state to be valid and so make initialisation easier, but decided
that this can be left to synthesis tools.

## Prior art
[prior-art]: #prior-art

LFSR generation is well established, some references are:

* Wikipedia articles:
* https://en.wikipedia.org/wiki/Linear-feedback_shift_register
* https://en.wikipedia.org/wiki/Pseudorandom_binary_sequence
* "Efficient Shift Registers, LFSR Counters, and Long Pseudo- Random Sequence Generators", application note by AMD:
* https://docs.amd.com/v/u/en-US/xapp052
* CCITT O.151 and O.152 define some PRBS sequences

Implementations in Verilog and VHDL are widespread, including:

* https://github.com/alexforencich/verilog-lfsr/blob/master/rtl/lfsr.v
* https://opencores.org/projects/lfsrcountergenerator

The Glasgow project includes an implementation in Amaranth:

* https://github.com/GlasgowEmbedded/glasgow/blob/main/software/glasgow/gateware/lfsr.py
* Uses `degree` instead of `width`, and a list of tap positions instead of
`polynominal`, and `reset` instead of `init`
* Also uses `EnableInserter` and `ResetInserter`
* Does not support generation of multi-bit outputs, but the entire internal
state might be used instead
* Only supports Fibonacci implementations

## Unresolved questions
[unresolved-questions]: #unresolved-questions

Before this RFC is ready to merge, we should resolve:

* It might be useful to add logic for LFSR matching/error detection, which is
a small addition and often useful
* Parallel generation of bits might change the internal state representation,
so we may need to only guarantee it matching expected values with single-bit
outputs.

The specific details of the parallel LFSR generation will be resolved during
implementation - there are a couple of possible ways to implement this, either
a similar matrix-based approach to the CRC module or by a virtual extension of
the shift register length.

## Future possibilities
[future-possibilities]: #future-possibilities

Some unresolved questions above may become a future possibility instead.
Beyond those, I don't foresee other future extensions.