<charlottia>
Hi all! I've been DIY learning HDL/Amaranth and just kinda using primitives as I come across them, and then having gaps in my understanding reveal themselves to me later. ^_^ I've just hit one that was a fun surprise and would love a quick "yep you got there in the end!" or otherwise.
<charlottia>
I've been happily using `SyncFIFO` without thinking too hard about it to let me shuffle data back and forth between components, so far only ever with `depth=1`. Just now I used one with `depth=2` in a simulation test and it broke a bunch of my assumptions! At this point I've read the implementation more carefully, and tried to understand the `fwft` parameter more clearly.
<charlottia>
Until now, I've been doing this when I want to write to a FIFO:
<charlottia>
...(truncated)
<charlottia>
* Hi all! I've been DIY learning HDL/Amaranth and just kinda using primitives as I come across them, and then having gaps in my understanding reveal themselves to me later. ^\_^ I've just hit one that was a fun surprise and would love a quick "yep you got there in the end!" or otherwise.
<charlottia>
I've been happily using `SyncFIFO` without thinking too hard about it to let me shuffle data back and forth between components, so far only ever with `depth=1`. Just now I used one with `depth=2` in a simulation test and it broke a bunch of my assumptions! At this point I've read the implementation more carefully, and tried to understand the `fwft` parameter more clearly.
<charlottia>
(XXX: splitting into multiple messages to traverse bridges)
<charlottia>
Until now, I've been doing this when I want to write to a FIFO:
<charlottia>
1. ensure/wait for `w_rdy`;
<charlottia>
3. unset `w_en` in the next.
<charlottia>
...(truncated)
<charlottia>
2. set `w_data` and `w_en` in one cycle;
<charlottia>
Agh, okay, the bridges truncate mercilessly.
<charlottia>
On the other side:
<charlottia>
1. wait for `r_rdy`;
<charlottia>
2. set `r_en` in one cycle;
<charlottia>
3. unset `r_en` in the next, and use `r_data`.
<charlottia>
This has worked fine, but it fell apart when using one with depth=2, and I realise it's probably due to first-word fallthrough. Per the docs, given fwft=True, if r_rdy rises (i.e. the queue becomes non-empty), r_data already contains that first word. What was surprising to me was that, given such a FIFO that's been written to twice (i.e. level==2), strobing r_en once will then put the second word into r_data, because
<charlottia>
the first was already available before that.
<charlottia>
The bit where I got super confused was that strobing r_en a second time then puts the first word back into r_data. This caused a very funny hour or so where I was wondering how I was possibly reversing the flow of time. I now realise this is because SyncFIFO internally uses its memory like a ring buffer, and so the read port address just wraps around, pointing at where the next write will go.
<charlottia>
All of this was transparent (pun intentional?) to me when using solely SyncFIFO(depth=1)s with the default FWFT setting. I note SyncFIFOBuffered's docstring mentions the aforementioned's incompatibility with FPGA BRAM -- I assume I don't care for these to be used with BRAM anyway, given they're tiny.
<charlottia>
tl;dr `SyncFIFO(fwft=True)` means `r_rdy` implies `r_data` is already valid, and strobing `r_en` will "discard", though for `depth=1` there will be no change in `r_data` when doing so.
<charlottia>
Is this more-or-less right?
<whitequark>
FL4SHK: re View.like: no, you're supposed to use Signal.like, but it doesn't currently work due to a bug
<whitequark>
adamgreig: re crc.catalog: I feel like it's not necessary to have crc.catalog be automatically present in crc, but if you want to, you can import it at the *end* of the crc module
<jfng[m]>
charlotte: yep, SyncFIFO internally uses a ring buffer, indexed by counters that wrap around `depth`.
<jfng[m]>
when `r_en` and `r_rdy` are high, the consumer counter is incremented; if `depth=1`, then the counter is always 0 (`(0 + 1) % 1`), so `r_data` won't change unless a write happens on the other side
<charlottia>
jfng[m]: > <@jfng:matrix.org> charlotte: yep, SyncFIFO internally uses a ring buffer, indexed by counters that wrap around `depth`.
<charlottia>
> when `r_en` and `r_rdy` are high, the consumer counter is incremented; if `depth=1`, then the counter is always 0 (`(0 + 1) % 1`), so `r_data` won't change unless a write happens on the other side
<whitequark>
especially if some part of the design is unclear or is doubtful, I'd like to hear that
<Psentee>
@whitequark: would you be open to PRs that add some type annotations? Not necessarily complete signatures checking value widths or signal directions (it's not haskell or typescript), but some extra hints that would make work with pyright/mypy a bit more comfortable. Minimum Python version was recently raised to 3.7, which removes the main blocker.
<whitequark>
can these be put into separate files somehow?
<Psentee>
They can even be put into a separate package, but keeping that in sync with Python source would be a nightmare. I can check if there are any tools that would make maintenance of separate stubs feasible.
<whitequark>
I don't object to them being in the same package, but them being in the same files turn a "I need to think about it" into a "I can just merge this right now"
<whitequark>
them *not being in the same files
<whitequark>
re: "Not necessarily complete signatures checking value widths or signal directions (it's not haskell or typescript)" I want that! this is why I've been stalling on typing for a while
<Psentee>
Theoretically you might be able to make it work with type annotations, but Python's typing tries to avoid new syntax and as a result it's very verbose and needs a lot of boilerplate for anything complex. I can try to add stubs, looks like putting .pyi files next to .py should do the trick.
<whitequark>
yes, that's why I don't like annotations that much
<Psentee>
Side note, Struct seems fundamentally incompatible with typing because it puts something that's not type in annotations and it confuses at least Pyright
<Psentee>
But I think adding a __class_getitem__ on Value that adds
<Psentee>
*that returns an annotated type might make it possible to write a struct like `field: Value[signed(23)]` - longer, but works with typecheckers
<d1b2>
<Olivier Galibert> whitequark: I really, really need examples to wrap my head around the design
<Chips4MakersakaS>
Psentee: Regarding annotations that are not python annotations; there has been some discussion about that in [RFCS:!11](https://github.com/amaranth-lang/rfcs/pull/11)
<Chips4MakersakaS>
s/python/type/
<Chips4MakersakaS>
I think it was also discussed in IRC meeting of 3 weeks ago; should be in IRC log.
<Psentee>
I'll continue that discussion in the RFC thread, thanks for pointing me there
<Psentee>
whitequark: I like to have things in nix, that's my pet project and doesn't have to make sense:) and maybe eventually I'll send PR for nixpkgs to include -unstable version
<whitequark>
ah, nix!
<whitequark>
yeah makes sense
<Psentee>
(also, one or both of yosys/nextpnr use apicula which is a moving target, and I think it's called during build)
<whitequark>
oh yeah, you'd have to get test pypi builds to get apicula
Guest9 has joined #amaranth-lang
<Psentee>
Anyway, README is out of date and it's still building yosys/nextpnr but does that in nix. It's a personal playground, not a proper project, it might evolve into something more useful someday
Guest9 has quit [Client Quit]
<FL4SHK>
whitequark: I see
<tpw_rules>
Psentee: nix <3
<crzwdjk>
For the interfaces RFC I too would like to see a worked example, but otherwise I think the basic ideas are good. Maybe you can call it amaranth.lib.port instead of amaranth.lib.module? But that's just bikeshedding.
<crzwdjk>
Specifically the example I would like to see is how to easily create a pair of complementary interfaces.
<crzwdjk>
"It may be useful to be able to define elaboratables with a signature specified with type annotations" is definitely true but already this RFC is going to be a huge improvement over wiring stuff up manually in an unstructured way (and screwing it up, which is an apparently bottomless source of errors for me)
<Sarayan>
I'd love to see something that would be usual, like a given-size wishbone and connecting it
<crzwdjk>
Or even smaller and simpler, like connecting a FIFO to a UART
<whitequark>
yeah, this will be written
d1b23 has joined #amaranth-lang
tpw-rules has joined #amaranth-lang
tpw_rules has quit [*.net *.split]
d1b2 has quit [*.net *.split]
tpw-rules is now known as tpw_rules
d1b23 is now known as d1b2
<d1b2>
<zyp> is there a ~signature equivalent to ~member?
<whitequark>
nope; do you think you'll need it?
<whitequark>
I'm not even sure it's that useful on Member
<d1b2>
<zyp> how would you make input and output interfaces from a common signature?
<whitequark>
oh, right. my initial plan involved every Elaboratable defining those with class annotations with wishbone : In[wishbone.Signature] or something
<whitequark>
but that part got postponed
<whitequark>
so now you do need a way to do that
<d1b2>
<zyp> also, motivation point 3 mentions «pairs of identical, complementary endpoints»; how would you connect those? can you call connect() on parts of an interface?
<d1b2>
<zyp> let's for example's sake say you've got two modules that both got an UART, could you do something like connect(m, a.uart.tx, b.uart.rx); connect(m, a.uart.rx, b.uart.tx)?
<whitequark>
UART seems like a particularly bad example because that's just two signals
<whitequark>
and if you consider all the other DB-9 signals, you can't easily reverse it (like... a null modem cable leaves RI open)
<whitequark>
there's no advantage over m.d.comb += b.uart.rx.eq(a.uart.tx), a.uart.rx.eq(b.uart.tx), is there?
<d1b2>
<zyp> the UART part isn't what's important, just the first symmetric interface that came to mind, let's say rx and tx are formed from the same signature with any number of signals
<whitequark>
so a lot of the reasoning behind the interface library is that interfaces are typically asymmetric!
<d1b2>
<zyp> I could suggest RGMII instead if that helps, but you could probably still argue that the MAC and PHY ends aren't entirely identical 🙂
<whitequark>
yes, exactly
<whitequark>
the library is designed to help you connect MAC to PHY
<whitequark>
where the same signature would be used in both
<whitequark>
it won't help you connect MAC to MAC very much
<whitequark>
it doesn't know that rx is "the inverse" of tx
<whitequark>
you could potentially build that in, but... you have to handle 'null modem cables' case by case anyway, inevitably the MAC will have some register interface or something (it's a thing for both SGMII and PCIe!) that you'll need to stub out
<d1b2>
<zyp> well, my question in any case is «can you use connect() on parts of an interface?»
<whitequark>
oh!
<d1b2>
<zyp> because that seems useful to me
<whitequark>
you can call connect on sub-interfaces since there's nothing special about them
<whitequark>
you can't call connect on individual ports since those are just signals and they have no direction
<whitequark>
re the 'null modem' case, if it is really that important, we can write and ship gasket that has two PHY-side interfaces on it and handles all the control logic that needs to be stubbed out
<d1b2>
<zyp> right, so aggregate members of an interface will also be interface instances?
<whitequark>
correct
<d1b2>
<zyp> good, that's then also the obvious solution to real world cases of the problem above (but not for UARTs, since uart.tx and uart.rx would be directionless signals)
<d1b2>
<zyp> how would you handle the case where you've got an instance of a module with an interface that you just want to propagate through? i.e. connect out to out and in to in?
<whitequark>
this creates and immediately throws out a bunch of objects, but other than that is harmless
<d1b2>
<zyp> with .eq()? does that handle both directions?
<whitequark>
no, with =
<d1b2>
<zyp> in other words just reference the interface in the child module? that seems inconvenient in that you can't «create» the outer interface before you've instanced the child
<whitequark>
then you can say self.bus = self.child.bus in the parent
<d1b2>
<zyp> fair enough
<d1b2>
<zyp> how about when you want to conditionally connect them? 🙂
<whitequark>
what would a specific case be?
<d1b2>
<zyp> e.g. multiplexing two external interfaces to one internal
<whitequark>
I get what you want to see, but I feel like a motivating example should be a thing someone actually wants to do
<whitequark>
all sorts of things are theoretically useful and I've tried to support such use cases as much as I can (lib.data is full of them), but here I think the cost is higher
<whitequark>
we could have a .eq() on interfaces that does this (with extensive checking and not like with Record)
<d1b2>
<zyp> thinking about this, an interface has two sides; an outside and an inside, and that gives three ways to connect them
<whitequark>
yeah
<whitequark>
Signature is an outside view
<d1b2>
<zyp> the RFC covers the basic case of looking at both from the outside perspective, where you're connecting opposites
<d1b2>
<zyp> and then I've mentioned outside/inside, connecting like flows to forward the interfaces
<whitequark>
(I'll be afk)
<d1b2>
<zyp> and then there's also an inside/inside perspective, where you'd be connecting opposite flows, but in the opposite direction of the outside perspective
<d1b2>
<zyp> I think maybe the cleanest way to solve this would be for an Interface to have a inside method (name TBD), that would give you a view of the same signals with all the directions flipped
<d1b2>
<zyp> that way the example above would become connect(m, self.bus.inside, m.child.bus)
<d1b2>
<zyp> as for motivating examples, I think streams will provide plenty
<d1b2>
<zyp> consider e.g. litex' stream.Pipeline, it's fairly common for a module to do something like Pipeline(self.sink, a, b, c, self.source) which is effectively connect(m, self.sink, a.sink)… connect(m, c.source, self.source)