"""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]))