Added full design created with Claude
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
from amaranth import *
|
||||
from amaranth.sim import Simulator
|
||||
|
||||
|
||||
class SyncFF(Elaboratable):
|
||||
"""Width-N multi-flop synchronizer from `src_domain` to `dst_domain`.
|
||||
|
||||
Use when the source is a level signal that may be stable for multiple destination
|
||||
cycles. Not suitable for single-cycle pulses (use TogglePulseSync instead).
|
||||
"""
|
||||
|
||||
def __init__(self, width=1, src_domain="src", dst_domain="dst"):
|
||||
self.width = width
|
||||
self.src_domain = src_domain
|
||||
self.dst_domain = dst_domain
|
||||
self.src = Signal(self.width)
|
||||
self.dst = Signal(self.width)
|
||||
|
||||
def elaborate(self, platform):
|
||||
m = Module()
|
||||
reg_src = Signal(self.width)
|
||||
ff0 = Signal(self.width)
|
||||
ff1 = Signal(self.width)
|
||||
|
||||
m.d[self.src_domain] += reg_src.eq(self.src)
|
||||
m.d[self.dst_domain] += ff0.eq(reg_src)
|
||||
m.d[self.dst_domain] += ff1.eq(ff0)
|
||||
m.d.comb += self.dst.eq(ff1)
|
||||
|
||||
return m
|
||||
|
||||
|
||||
class TogglePulseSync(Elaboratable):
|
||||
"""Reliable pulse transfer from `src_domain` into `dst_domain`.
|
||||
|
||||
- Source toggles `toggle` whenever an event occurs.
|
||||
- Destination synchronizes the toggle and detects edges.
|
||||
Guarantees ordering and no lost pulses for single-bit events.
|
||||
"""
|
||||
|
||||
def __init__(self, src_domain="src", dst_domain="dst"):
|
||||
self.src_domain = src_domain
|
||||
self.dst_domain = dst_domain
|
||||
self.src_pulse = Signal()
|
||||
self.dst_pulse = Signal()
|
||||
|
||||
def elaborate(self, platform):
|
||||
m = Module()
|
||||
toggle = Signal()
|
||||
sync0 = Signal()
|
||||
sync1 = Signal()
|
||||
prev = Signal()
|
||||
edge = Signal()
|
||||
|
||||
# Source domain: flip the toggle when a pulse arrives
|
||||
m.d[self.src_domain] += If(self.src_pulse, toggle.eq(~toggle))
|
||||
|
||||
# Destination domain: two-flop synchronize the toggle
|
||||
m.d[self.dst_domain] += sync0.eq(toggle)
|
||||
m.d[self.dst_domain] += sync1.eq(sync0)
|
||||
|
||||
# Detect the change in the destination domain
|
||||
m.d[self.dst_domain] += edge.eq(sync1 ^ prev)
|
||||
m.d[self.dst_domain] += prev.eq(sync1)
|
||||
m.d.comb += self.dst_pulse.eq(edge)
|
||||
|
||||
return m
|
||||
|
||||
|
||||
def _sim_toggle_pulse():
|
||||
"""Simple simulation that drives pulses on the source domain and prints detections on the destination domain."""
|
||||
|
||||
top = Module()
|
||||
t = TogglePulseSync(src_domain="src", dst_domain="dst")
|
||||
top.submodules.t = t
|
||||
|
||||
sim = Simulator(top)
|
||||
# Create two asynchronous clocks (periods chosen arbitrarily for the sim)
|
||||
sim.add_clock(1e-6, domain="src")
|
||||
sim.add_clock(1.5e-6, domain="dst")
|
||||
|
||||
def process():
|
||||
# Wait a little, then generate three source pulses at different phases
|
||||
for _ in range(5):
|
||||
yield
|
||||
|
||||
for i in range(3):
|
||||
yield t.src_pulse.eq(1)
|
||||
yield
|
||||
yield t.src_pulse.eq(0)
|
||||
# let the domains run for a few cycles
|
||||
for _ in range(10):
|
||||
dp = (yield t.dst_pulse)
|
||||
if dp:
|
||||
print(f"dst detected pulse at sim tick")
|
||||
yield
|
||||
|
||||
# run a bit longer to observe behavior
|
||||
for _ in range(20):
|
||||
yield
|
||||
|
||||
sim.add_sync_process(process, domain="src")
|
||||
sim.run_until(100e-6)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
_sim_toggle_pulse()
|
||||
@@ -0,0 +1,182 @@
|
||||
from amaranth import *
|
||||
from amaranth.sim import Simulator
|
||||
|
||||
|
||||
def bin_to_gray(x):
|
||||
return x ^ (x >> 1)
|
||||
|
||||
|
||||
def gray_to_bin(g, width):
|
||||
# convert gray to binary iteratively
|
||||
b = 0
|
||||
for i in range(width - 1, -1, -1):
|
||||
if i == width - 1:
|
||||
b |= ((g >> i) & 1) << i
|
||||
else:
|
||||
b |= (((b >> (i + 1)) & 1) ^ ((g >> i) & 1)) << i
|
||||
return b
|
||||
|
||||
|
||||
class AsyncFIFO(Elaboratable):
|
||||
"""Parameterizable gray-pointer dual-clock FIFO.
|
||||
|
||||
- width: data width in bits
|
||||
- depth: must be a power of two
|
||||
- wdomain: write (source) domain name
|
||||
- rdomain: read (destination) domain name
|
||||
"""
|
||||
|
||||
def __init__(self, width=1, depth=16, wdomain="src", rdomain="dst"):
|
||||
assert depth & (depth - 1) == 0
|
||||
self.width = width
|
||||
self.depth = depth
|
||||
self.aw = (depth - 1).bit_length() # address width
|
||||
self.wdomain = wdomain
|
||||
self.rdomain = rdomain
|
||||
|
||||
# write-side interface
|
||||
self.wdata = Signal(width)
|
||||
self.w_en = Signal()
|
||||
self.w_full = Signal()
|
||||
|
||||
# read-side interface
|
||||
self.rdata = Signal(width)
|
||||
self.r_en = Signal()
|
||||
self.r_valid = Signal()
|
||||
self.r_empty = Signal()
|
||||
|
||||
def elaborate(self, platform):
|
||||
m = Module()
|
||||
|
||||
mem = Memory(width=self.width, depth=self.depth)
|
||||
wp = mem.write_port(domain=self.wdomain)
|
||||
rp = mem.read_port(domain=self.rdomain, transparent=False)
|
||||
m.submodules += wp, rp
|
||||
|
||||
# pointers are AW+1 bits (extra MSB for wrap)
|
||||
wbin = Signal(self.aw + 1)
|
||||
wgray = Signal(self.aw + 1)
|
||||
rbin = Signal(self.aw + 1)
|
||||
rgray = Signal(self.aw + 1)
|
||||
|
||||
# synchronized opposing domain gray pointers
|
||||
rgray_sync0 = Signal(self.aw + 1)
|
||||
rgray_sync1 = Signal(self.aw + 1)
|
||||
wgray_sync0 = Signal(self.aw + 1)
|
||||
wgray_sync1 = Signal(self.aw + 1)
|
||||
|
||||
# write domain logic
|
||||
with m.Domain(self.wdomain):
|
||||
waddr = Signal(self.aw)
|
||||
next_wbin = Signal(self.aw + 1)
|
||||
next_wgray = Signal(self.aw + 1)
|
||||
|
||||
# compute next pointer
|
||||
m.d.comb += next_wbin.eq(wbin + self.w_en)
|
||||
m.d.comb += next_wgray.eq(next_wbin ^ (next_wbin >> 1))
|
||||
|
||||
# synchronize rgray into write domain (two flops per bit)
|
||||
m.d.comb += []
|
||||
for i in range(self.aw + 1):
|
||||
m.d[self.wdomain] += rgray_sync0[i].eq(rgray[i])
|
||||
m.d[self.wdomain] += rgray_sync1[i].eq(rgray_sync0[i])
|
||||
|
||||
# full detection: next_wgray equals rgray_sync with top two bits inverted
|
||||
if self.aw >= 1:
|
||||
top = self.aw
|
||||
msb_cmp = Signal()
|
||||
low_eq = Signal()
|
||||
m.d.comb += low_eq.eq(next_wgray[top - 1:0] == rgray_sync1[top - 1:0])
|
||||
m.d.comb += msb_cmp.eq((next_wgray[top] != rgray_sync1[top]) & (next_wgray[top - 1] != rgray_sync1[top - 1]))
|
||||
m.d.comb += self.w_full.eq(low_eq & msb_cmp)
|
||||
else:
|
||||
# depth==2 special case
|
||||
m.d.comb += self.w_full.eq(next_wgray != rgray_sync1)
|
||||
|
||||
# write to memory when enabled & not full
|
||||
with m.If(self.w_en & ~self.w_full):
|
||||
m.d[self.wdomain] += wp.addr.eq(wbin[self.aw - 1:0])
|
||||
m.d[self.wdomain] += wp.data.eq(self.wdata)
|
||||
m.d[self.wdomain] += wp.en.eq(1)
|
||||
m.d[self.wdomain] += wbin.eq(next_wbin)
|
||||
m.d[self.wdomain] += wgray.eq(next_wgray)
|
||||
with m.Else():
|
||||
m.d[self.wdomain] += wp.en.eq(0)
|
||||
|
||||
# read domain logic
|
||||
with m.Domain(self.rdomain):
|
||||
raddr = Signal(self.aw)
|
||||
next_rbin = Signal(self.aw + 1)
|
||||
next_rgray = Signal(self.aw + 1)
|
||||
|
||||
# compute next pointer
|
||||
m.d.comb += next_rbin.eq(rbin + self.r_en)
|
||||
m.d.comb += next_rgray.eq(next_rbin ^ (next_rbin >> 1))
|
||||
|
||||
# synchronize wgray into read domain
|
||||
for i in range(self.aw + 1):
|
||||
m.d[self.rdomain] += wgray_sync0[i].eq(wgray[i])
|
||||
m.d[self.rdomain] += wgray_sync1[i].eq(wgray_sync0[i])
|
||||
|
||||
# empty detection
|
||||
m.d.comb += self.r_empty.eq(rgray == wgray_sync1)
|
||||
|
||||
# read when enabled and not empty
|
||||
with m.If(self.r_en & ~self.r_empty):
|
||||
m.d[self.rdomain] += rp.addr.eq(rbin[self.aw - 1:0])
|
||||
m.d[self.rdomain] += rp.en.eq(1)
|
||||
m.d[self.rdomain] += rbin.eq(next_rbin)
|
||||
m.d[self.rdomain] += rgray.eq(next_rgray)
|
||||
m.d[self.rdomain] += self.r_valid.eq(1)
|
||||
m.d[self.rdomain] += self.rdata.eq(rp.data)
|
||||
with m.Else():
|
||||
m.d[self.rdomain] += rp.en.eq(0)
|
||||
m.d[self.rdomain] += self.r_valid.eq(0)
|
||||
|
||||
return m
|
||||
|
||||
|
||||
def _sim_fifo():
|
||||
top = Module()
|
||||
fifo = AsyncFIFO(width=1, depth=16, wdomain="src", rdomain="dst")
|
||||
top.submodules.fifo = fifo
|
||||
|
||||
sim = Simulator(top)
|
||||
sim.add_clock(1e-6, domain="src")
|
||||
sim.add_clock(1.7e-6, domain="dst")
|
||||
|
||||
def writer():
|
||||
# write a sequence of bits (0..31 repeating pattern)
|
||||
for i in range(32):
|
||||
yield fifo.wdata.eq(i & 1)
|
||||
yield fifo.w_en.eq(1)
|
||||
yield
|
||||
yield fifo.w_en.eq(0)
|
||||
# allow some idle cycles
|
||||
for _ in range((i % 3)):
|
||||
yield
|
||||
|
||||
def reader():
|
||||
seen = []
|
||||
for _ in range(200):
|
||||
# try to consume if not empty
|
||||
empty = (yield fifo.r_empty)
|
||||
if not empty:
|
||||
yield fifo.r_en.eq(1)
|
||||
yield
|
||||
yield fifo.r_en.eq(0)
|
||||
if (yield fifo.r_valid):
|
||||
d = (yield fifo.rdata)
|
||||
seen.append(d)
|
||||
print(f"read: {d}")
|
||||
else:
|
||||
yield
|
||||
print(f"total read: {len(seen)}")
|
||||
|
||||
sim.add_sync_process(writer, domain="src")
|
||||
sim.add_sync_process(reader, domain="dst")
|
||||
sim.run()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
_sim_fifo()
|
||||
@@ -0,0 +1,119 @@
|
||||
"""IceBreaker (iCE40 UP5K) vendor-backed async FIFO example.
|
||||
|
||||
This module uses Amaranth's `Memory` with separate write/read ports in different
|
||||
clock domains. With the icestorm toolchain the memory typically maps to
|
||||
`SB_RAM40_4K` block RAMs. The control (full/empty) is implemented with
|
||||
gray-pointer logic and two-stage synchronization of opposing pointers.
|
||||
|
||||
Notes:
|
||||
- This prefers block RAM for storage (small LUT usage, lower power).
|
||||
- The write/read ports are in independent domains; backend maps ports to
|
||||
dual-port RAM primitives when available.
|
||||
"""
|
||||
|
||||
from amaranth import *
|
||||
|
||||
|
||||
class Ice40AsyncFIFO(Elaboratable):
|
||||
def __init__(self, depth=256, wdomain="src", rdomain="dst"):
|
||||
assert depth & (depth - 1) == 0, "depth must be power of two"
|
||||
self.depth = depth
|
||||
self.aw = (depth - 1).bit_length()
|
||||
self.wdomain = wdomain
|
||||
self.rdomain = rdomain
|
||||
|
||||
# serial (1-bit) interface
|
||||
self.wdata = Signal()
|
||||
self.w_en = Signal()
|
||||
self.w_full = Signal()
|
||||
|
||||
self.rdata = Signal()
|
||||
self.r_en = Signal()
|
||||
self.r_valid = Signal()
|
||||
self.r_empty = Signal()
|
||||
|
||||
def elaborate(self, platform):
|
||||
m = Module()
|
||||
|
||||
# single-bit-wide memory mapped to vendor BRAMs by the backend
|
||||
mem = Memory(width=1, depth=self.depth)
|
||||
wp = mem.write_port(domain=self.wdomain)
|
||||
rp = mem.read_port(domain=self.rdomain, transparent=False)
|
||||
m.submodules += wp, rp
|
||||
|
||||
# pointers (aw+1 bits to include wrap bit)
|
||||
wbin = Signal(self.aw + 1)
|
||||
wgray = Signal(self.aw + 1)
|
||||
rbin = Signal(self.aw + 1)
|
||||
rgray = Signal(self.aw + 1)
|
||||
|
||||
# sync registers for opposing pointers (two-stage)
|
||||
rgray_sync0 = Signal(self.aw + 1)
|
||||
rgray_sync1 = Signal(self.aw + 1)
|
||||
wgray_sync0 = Signal(self.aw + 1)
|
||||
wgray_sync1 = Signal(self.aw + 1)
|
||||
|
||||
# write-side
|
||||
with m.Domain(self.wdomain):
|
||||
next_wbin = Signal(self.aw + 1)
|
||||
next_wgray = Signal(self.aw + 1)
|
||||
m.d.comb += next_wbin.eq(wbin + self.w_en)
|
||||
m.d.comb += next_wgray.eq(next_wbin ^ (next_wbin >> 1))
|
||||
|
||||
# sync read pointer into write domain
|
||||
for i in range(self.aw + 1):
|
||||
m.d[self.wdomain] += rgray_sync0[i].eq(rgray[i])
|
||||
m.d[self.wdomain] += rgray_sync1[i].eq(rgray_sync0[i])
|
||||
|
||||
# full detection (standard gray-pointer trick)
|
||||
top = self.aw
|
||||
low_eq = Signal()
|
||||
msb_cmp = Signal()
|
||||
m.d.comb += low_eq.eq(next_wgray[top - 1:0] == rgray_sync1[top - 1:0])
|
||||
m.d.comb += msb_cmp.eq((next_wgray[top] != rgray_sync1[top]) & (next_wgray[top - 1] != rgray_sync1[top - 1]))
|
||||
m.d.comb += self.w_full.eq(low_eq & msb_cmp)
|
||||
|
||||
# perform write
|
||||
with m.If(self.w_en & ~self.w_full):
|
||||
m.d[self.wdomain] += wp.addr.eq(wbin[self.aw - 1:0])
|
||||
m.d[self.wdomain] += wp.data.eq(self.wdata)
|
||||
m.d[self.wdomain] += wp.en.eq(1)
|
||||
m.d[self.wdomain] += wbin.eq(next_wbin)
|
||||
m.d[self.wdomain] += wgray.eq(next_wgray)
|
||||
with m.Else():
|
||||
m.d[self.wdomain] += wp.en.eq(0)
|
||||
|
||||
# read-side
|
||||
with m.Domain(self.rdomain):
|
||||
next_rbin = Signal(self.aw + 1)
|
||||
next_rgray = Signal(self.aw + 1)
|
||||
m.d.comb += next_rbin.eq(rbin + self.r_en)
|
||||
m.d.comb += next_rgray.eq(next_rbin ^ (next_rbin >> 1))
|
||||
|
||||
# sync write pointer into read domain
|
||||
for i in range(self.aw + 1):
|
||||
m.d[self.rdomain] += wgray_sync0[i].eq(wgray[i])
|
||||
m.d[self.rdomain] += wgray_sync1[i].eq(wgray_sync0[i])
|
||||
|
||||
m.d.comb += self.r_empty.eq(rgray == wgray_sync1)
|
||||
|
||||
with m.If(self.r_en & ~self.r_empty):
|
||||
m.d[self.rdomain] += rp.addr.eq(rbin[self.aw - 1:0])
|
||||
m.d[self.rdomain] += rp.en.eq(1)
|
||||
m.d[self.rdomain] += rbin.eq(next_rbin)
|
||||
m.d[self.rdomain] += rgray.eq(next_rgray)
|
||||
m.d[self.rdomain] += self.r_valid.eq(1)
|
||||
m.d[self.rdomain] += self.rdata.eq(rp.data)
|
||||
with m.Else():
|
||||
m.d[self.rdomain] += rp.en.eq(0)
|
||||
m.d[self.rdomain] += self.r_valid.eq(0)
|
||||
|
||||
return m
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Quick smoke-check: instantiate and print fragment
|
||||
from amaranth.back import verilog
|
||||
|
||||
fifo = Ice40AsyncFIFO(depth=256)
|
||||
print(verilog.convert(fifo, ports=[fifo.wdata, fifo.w_en, fifo.w_full, fifo.rdata, fifo.r_en, fifo.r_valid, fifo.r_empty]))
|
||||
Reference in New Issue
Block a user