Skip to content

xilinx: use FDPE instances to implement get_async_ff_sync() #1091

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged

Conversation

daniestevez
Copy link
Contributor

This closes #721 by implementing get_async_ff_sync() using FDPE primitives to obtain exactly the netlist that we want. This consits of a chain of N FPDEs (by default N = 2) with all their PRE pins connected to the reset for a positive edge reset or to the ~reset for a negative edge reset. The D pin of the first FDPE in the chain is connected to GND.

To make timing analysis work correctly, two new attributes are introduced: amaranth.vivado.false_path_pre and
amaranth.vivado.max_delay_pre. These work similarly to amaranth.vivado.false_path and amaranth.vivado.max_delay, but affect only the PRE pin, which is what is needed for this synchronizer. The TCL has been modified to generate constraints using these attributes, and there are comments explaining how to use the attributes directly in an XDC file in case the user wants to manage their XDC file manually instead of using the TCL.


I have tested this with the following example.

#!/usr/bin/env python3

from amaranth import *
import amaranth.cli
from amaranth.lib.cdc import ResetSynchronizer
from amaranth.vendor import XilinxPlatform


class Test(Elaboratable):
    def __init__(self):
        self.rst_in = Signal()
        self.toggle = Signal()
        self.toggle2 = Signal()
        self.rstdomain = ClockDomain()
        self.sync2 = ClockDomain()

    def elaborate(self, platform):
        m = Module()
        m.domains += [self.rstdomain, self.sync2]
        rst_in_q = Signal(reset=1)
        m.submodules.sync_rst = ResetSynchronizer(rst_in_q)
        m.submodules.sync_rst_maxdel = ResetSynchronizer(
                rst_in_q, domain="sync2", max_input_delay=3e-9)
        m.d.rstdomain += rst_in_q.eq(self.rst_in)
        m.d.sync += self.toggle.eq(~self.toggle)
        m.d.sync2 += self.toggle2.eq(~self.toggle2)
        return m


class MyPlatform(XilinxPlatform):
    device = "xc7z010"
    package = "clg400"
    speed = "1"
    resources = []
    connectors = []


if __name__ == '__main__':
    top = Test()
    platform = MyPlatform()
    amaranth.cli.main(
        top, platform=platform,
        ports=[top.rst_in, top.toggle, top.toggle2,
               top.rstdomain.clk, top.rstdomain.rst,
               top.sync2.clk, top.sync2.rst])

These are the constraints I'm using

create_clock -period 5 -name clk [get_ports clk]
create_clock -period 5 -name sync2_clk [get_ports sync2_clk]
create_clock -period 5.01 -name rstdomain_clk [get_ports rstdomain_clk]
set_false_path -to [get_cells -hier -filter {amaranth.vivado.false_path == "TRUE"}]

set_false_path -to [ \
  get_pins -of [get_cells -hier -filter {amaranth.vivado.false_path_pre == "TRUE"}] \
    -filter { REF_PIN_NAME == PRE } ]

set_max_delay -to [ \
  get_pins -of [get_cells -hier -filter {amaranth.vivado.max_delay_pre == "3.0"}] \
    -filter { REF_PIN_NAME == PRE } ] \
  3.0

The implementation schematic is as follows. Paths that are timed normally are highlighted in blue. Paths timed of a 3 ns max delay are highlighted in red. The paths that are not highlighted are not timed.

implementation_results

This looks correct to me. I haven't found detailed information about the timing characteristics of the FDPE, but my thinking is that if PRE is asserted then the output of stage1 will go high in a short time regardless of what is happening with the clock. If PRE is deasserted, the worst that can happen is that this happens close to a clock edge. In this case I think that the output of stage0 might go metastable, but the output of stage1 should be a stable 1, because when this happens, stage1 has a stable 1 at its input. At the next clock edge, hopefully the metastability of stage0 should have resolved to a stable 0 or a stable 1 (and if not, use a longer chain). If it has resolved to a 0, then the output of stage1 will pick the 0 in this clock cycle. If it has resolved to a 1, then stage0 will pick the 0 in this clock cycle, and stage1 will pick it in the next clock cycle.

Besides testing this example with the constraints shown above, I have deleted the set_false_path and set_max_delay constraints and checked that running the TCL commands that I have added generates the correct constraints.

A question to make sure I understand what async_edge="neg" means: my interpretation is that when the input is 0, then the output should be 1 immediately. When the input is 1, then the output should be 0 at the next clock edge. Is this right?

By the way, now that I have given some more thought to this reset synchronizer, I wonder how useful it is in a typical Xilinx design. Flip-flops are normally synchronous reset, so if the clock is off, they won't reset even if the reset is asserted (I guess that the main use case of AsyncFFSynchronizer instead of FFSynchronizer is to be able to assert the output even when there is no clock).

Copy link

codecov bot commented Feb 8, 2024

Codecov Report

All modified and coverable lines are covered by tests ✅

Comparison is base (9e75962) 86.98% compared to head (b051216) 86.98%.

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #1091   +/-   ##
=======================================
  Coverage   86.98%   86.98%           
=======================================
  Files          41       41           
  Lines        7159     7159           
  Branches     1706     1706           
=======================================
  Hits         6227     6227           
+ Misses        769      768    -1     
- Partials      163      164    +1     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@whitequark
Copy link
Member

(I guess that the main use case of AsyncFFSynchronizer instead of FFSynchronizer is to be able to assert the output even when there is no clock).

Yes. It's only useful in a situation where a clock domain has async reset.

This closes amaranth-lang#721 by implementing get_async_ff_sync() using FDPE
primitives to obtain exactly the netlist that we want. This consits
of a chain of N FPDEs (by default N = 2) with all their PRE pins
connected to the reset for a positive edge reset or to the ~reset
for a negative edge reset. The D pin of the first FDPE in the chain
is connected to GND.

To make timing analysis work correctly, two new attributes are
introduced: amaranth.vivado.false_path_pre and
amaranth.vivado.max_delay_pre. These work similarly to
amaranth.vivado.false_path and amaranth.vivado.max_delay, but affect
only the PRE pin, which is what is needed for this synchronizer.
The TCL has been modified to generate constraints using these
attributes, and there are comments explaining how to use the attributes
directly in an XDC file in case the user wants to manage their XDC
file manually instead of using the TCL.
@daniestevez daniestevez force-pushed the xilinx-reset-synchronizer branch from 5aab6d7 to b051216 Compare February 8, 2024 11:25
Copy link
Member

@whitequark whitequark left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fantastic, thank you!

@whitequark whitequark enabled auto-merge February 8, 2024 11:30
@whitequark whitequark added this pull request to the merge queue Feb 8, 2024
Merged via the queue into amaranth-lang:main with commit d8f70be Feb 8, 2024
@whitequark whitequark added this to the 0.5 milestone Feb 8, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

ResetSynchronizer does not work properly with Vivado
2 participants