diff --git a/software/glasgow/applet/all.py b/software/glasgow/applet/all.py index f8178f8f5..4d40bf522 100644 --- a/software/glasgow/applet/all.py +++ b/software/glasgow/applet/all.py @@ -12,6 +12,7 @@ from .interface.jtag_svf import JTAGSVFApplet from .interface.ps2_host import PS2HostApplet from .interface.sbw_probe import SpyBiWireProbeApplet +from .interface.freq_counter import FrequencyCounterApplet from .memory._24x import Memory24xApplet from .memory._25x import Memory25xApplet diff --git a/software/glasgow/applet/interface/freq_counter/__init__.py b/software/glasgow/applet/interface/freq_counter/__init__.py new file mode 100644 index 000000000..1c4a867c3 --- /dev/null +++ b/software/glasgow/applet/interface/freq_counter/__init__.py @@ -0,0 +1,202 @@ +# +# notes: +# - does not play nicely with slow edges or analog signals (e.g: sine wave) +# will produce very inaccurate and inconsistent results +# - max runtime is ~89 sec, at which the max freq is ~48 MHz +# - given a max freq of ~100 MHz, the max sensible runtime is ~42 sec +# - given the diminishing returns in precision past a few seconds of runtime, long runtimes aren't actually that helpful +# - a 1 sec runtime will give a precision of +/- 1.000 Hz (max freq of ~4.2 GHz) +# - a 2 sec runtime will give a precision of +/- 0.500 Hz (max freq of ~2.1 GHz) +# - a 5 sec runtime will give a precision of +/- 0.200 Hz (max freq of ~858 MHz) +# - a 15 sec runtime will give a precision of +/- 0.066 Hz (max freq of ~286 MHz) +# - a 20 sec runtime will give a precision of +/- 0.050 Hz (max freq of ~214 MHz) +# - a 30 sec runtime will give a precision of +/- 0.033 Hz (max freq of ~140 MHz) +# - given the crossover, a hard limit of 20 seconds has been put on the runtime +# + +import enum +import asyncio +import logging +from nmigen import * + +from ....gateware.pads import * +from ....gateware.ripple import * +from ....support.si_prefix import num_to_si +from ... import * + + +class _Command(enum.IntEnum): + GO = 0x00 + + +class FrequencyCounterSubtarget(Elaboratable): + def __init__(self, pads, clk_count, edge_count, running, out_fifo): + self.pads = pads + self.clk_count = clk_count + self.edge_count = edge_count + self.running = running + self.out_fifo = out_fifo + + def elaborate(self, platform): + m = Module() + + trigger = Signal() + m.d.comb += [ + self.out_fifo.r_en.eq(self.out_fifo.r_rdy), + trigger.eq(self.out_fifo.r_en & (self.out_fifo.r_data == _Command.GO)), + ] + + clk_count = Signal.like(self.clk_count) + with m.If(trigger): + m.d.sync += clk_count.eq(self.clk_count) + with m.Elif(clk_count > 0): + m.d.sync += clk_count.eq(clk_count - 1) + m.d.comb += self.running.eq(1) + + m.submodules.ripple = RippleCounter( + rst=trigger, + clk=self.pads.i_t.i, + clk_en=self.running, + width=32, + ) + m.d.comb += self.edge_count.eq(m.submodules.ripple.count) + + return m + +class FrequencyCounterInterface: + def __init__(self, applet, device, interface): + self.applet = applet + self.device = device + self.lower = interface + + async def configure(self, duration=2.0): + ctr = int(self.applet.sys_clk_freq * duration) + + # this is broken (see comment below) + #await self.device.write_register(self.applet.__reg_clk_count, ctr, width=4) + + await self.applet.set_clk_count(ctr) + + async def start(self): + await self.lower.write([ _Command.GO ]) + await self.lower.flush() + + async def is_running(self): + return await self.applet.get_running() + + async def wait(self): + while await self.is_running(): + await asyncio.sleep(0.1) + + async def get_result(self): + clk_count = await self.applet.get_clk_count() + edge_count = await self.applet.get_edge_count() + + sample_duration = clk_count / self.applet.sys_clk_freq + signal_freq = edge_count / sample_duration + + precision = self.applet.sys_clk_freq / clk_count + + return signal_freq, precision + + async def measure(self, duration=2.0): + await self.configure(duration) + await self.start() + await self.wait() + return await self.get_result() + +class FrequencyCounterApplet(GlasgowApplet, name="freq-counter"): + logger = logging.getLogger(__name__) + help = "frequency counter" + description = """ + Simple frequency counter, based on a ripple counter. + """ + + @classmethod + def add_build_arguments(cls, parser, access): + super().add_build_arguments(parser, access) + + access.add_pin_argument(parser, "i", default=True) + + parser.add_argument( + "--duration", metavar="DURATION", type=float, default=2.0, + help="how long to run for, longer gives higher resolution (default: %(default)s)") + + def build(self, target, args): + self.mux_interface = iface = target.multiplexer.claim_interface(self, args) + + reg_clk_count, self.__reg_clk_count = target.registers.add_rw(32) + reg_edge_count, self.__reg_edge_count = target.registers.add_ro(32) + reg_running, self.__reg_running = target.registers.add_ro(1) + + subtarget = iface.add_subtarget(FrequencyCounterSubtarget( + pads=iface.get_pads(args, pins=("i",)), + clk_count=reg_clk_count, + edge_count=reg_edge_count, + running=reg_running, + out_fifo=iface.get_out_fifo(), + )) + + self.sys_clk_freq = target.sys_clk_freq + + @classmethod + def add_run_arguments(cls, parser, access): + super().add_run_arguments(parser, access) + + async def run(self, device, args): + self.device = device + + iface = await device.demultiplexer.claim_interface(self, self.mux_interface, args, pull_low={args.pin_i}) + freq_ctr = FrequencyCounterInterface(self, device, iface) + + return freq_ctr + + async def interact(self, device, args, freq_ctr): + signal_freq, precision = await freq_ctr.measure(args.duration) + print('signal frequency: {:>7.3f} {:1}Hz'.format( *num_to_si(signal_freq) )) + print('precision: +/- {:>7.3f} {:1}Hz'.format( *num_to_si(precision) )) + + # TODO: for some reason, accessing the registers from the FrequencyCounterInterface + # class will raise an odd / malformed AttributeException... as below. This exception + # isn't raised by GlasgowHardwareDevice.write_register(), but appears to occur on the + # return - wrapping below with a try / except / pass effectively resolves the issue, + # but A) that's disgusting, and B) it still breaks assignment / register_read() calls. + # + # for the moment, I've put proxy functions here, but I'd like to remove them...? + # + # $ glasgow run freq-counter -V 3.3 + # I: g.device.hardware: device already has bitstream ID 171709aadf51812cc9d1e3e54e881a43 + # I: g.cli: running handler for applet 'freq-counter' + # I: g.applet.interface.freq_counter: port(s) A, B voltage set to 3.3 V + # Traceback (most recent call last): + # File "/home/attie/proj_local/glasgow/venv/bin/glasgow", line 11, in + # load_entry_point('glasgow', 'console_scripts', 'glasgow')() + # File "/home/attie/proj_local/glasgow/glasgow/software/glasgow/cli.py", line 857, in main + # exit(loop.run_until_complete(_main())) + # File "/home/attie/.bin/python3.8.2/lib/python3.8/asyncio/base_events.py", line 616, in run_until_complete + # return future.result() + # File "/home/attie/proj_local/glasgow/glasgow/software/glasgow/cli.py", line 650, in _main + # task.result() + # File "/home/attie/proj_local/glasgow/glasgow/software/glasgow/cli.py", line 600, in run_applet + # iface = await applet.run(device, args) + # File "/home/attie/proj_local/glasgow/glasgow/software/glasgow/applet/interface/freq_counter/__init__.py", line 136, in run + # signal_freq = await freq_ctr.measure(args.duration) + # File "/home/attie/proj_local/glasgow/glasgow/software/glasgow/applet/interface/freq_counter/__init__.py", line 85, in measure + # await self.configure(duration) + # File "/home/attie/proj_local/glasgow/glasgow/software/glasgow/applet/interface/freq_counter/__init__.py", line 60, in configure + # await self.device.write_register(self.applet.__reg_clk_count, ctr, width=4) + # AttributeError: 'FrequencyCounterApplet' object has no attribute '_FrequencyCounterInterface__reg_clk_count' + + async def get_clk_count(self): + return await self.device.read_register(self.__reg_clk_count, width=4) + async def set_clk_count(self, value): + await self.device.write_register(self.__reg_clk_count, value, width=4) + + async def get_ctr(self): + return await self.device.read_register(self.__reg_ctr, width=4) + + async def get_edge_count(self): + return await self.device.read_register(self.__reg_edge_count, width=4) + + async def get_running(self): + return bool(await self.device.read_register(self.__reg_running, width=1)) diff --git a/software/glasgow/gateware/ripple.py b/software/glasgow/gateware/ripple.py new file mode 100644 index 000000000..0a7ae36c9 --- /dev/null +++ b/software/glasgow/gateware/ripple.py @@ -0,0 +1,30 @@ +import logging +from nmigen import * + +__all__ = ["RippleCounter"] + +class RippleCounter(Elaboratable): + def __init__(self, clk, clk_en=None, rst=None, width=8, logger=None): + self.logger = logger or logging.getLogger(__name__) + self.clk = clk + self.clk_en = clk_en + self.rst = rst + self.width = width + self.count = Signal(width) + + def elaborate(self, platform): + if not hasattr(platform, "get_ripple_ff_stage"): + raise NotImplementedError("No Ripple Counter support for platform") + + m = Module() + + clk_chain = self.clk + + for i in range(self.width): + d_out = Signal() + clk_en = self.clk_en if i == 0 else None + m.submodules += platform.get_ripple_ff_stage(d_out, clk_chain, clk_en, self.rst) + m.d.comb += self.count[i].eq(d_out) + clk_chain = d_out + + return m diff --git a/software/glasgow/platform/ice40.py b/software/glasgow/platform/ice40.py index 3ea742375..dae192bf0 100644 --- a/software/glasgow/platform/ice40.py +++ b/software/glasgow/platform/ice40.py @@ -116,3 +116,34 @@ def f_out_diff(variant): i_RESETB=~ResetSignal(pll.idomain), i_BYPASS=Const(0), ) + + def get_ripple_ff_stage(self, d_out, clk, clk_en=None, rst=None): + """ + a single stage of a ripple counter + + d_out should be used as the clock for the following stage, and as the data output + """ + if clk_en is None: + clk_en = Const(1) + if rst is None: + rst = Const(0) + + m = Module() + d_in = Signal() + + m.submodules += [ + Instance("SB_LUT4", + p_LUT_INIT=Const(0x00FF, 16), + i_I0=0, i_I1=0, i_I2=0, i_I3=d_out, + o_O=d_in + ), + Instance("SB_DFFNER", + i_D=d_in, + o_Q=d_out, + i_C=clk, + i_E=clk_en, + i_R=rst, + ), + ] + + return m diff --git a/software/glasgow/support/si_prefix.py b/software/glasgow/support/si_prefix.py new file mode 100644 index 000000000..e8b296a20 --- /dev/null +++ b/software/glasgow/support/si_prefix.py @@ -0,0 +1,16 @@ +def num_to_si(num, long_prefix=False): + prefixes = [ + ( 3, 'G', 'Giga' ), + ( 2, 'M', 'Mega' ), + ( 1, 'k', 'Kilo' ), + ( 0, '', '' ), + ( -1, 'm', 'mili' ), + ( -2, 'u', 'micro' ), + ( -3, 'n', 'nano' ), + ] + try: + factor, tshort, tlong = next(filter(lambda x: num >= (1000 ** x[0]), prefixes)) + except StopIteration: + factor, tshort, tlong = prefixes[-1] + prefix = tlong if long_prefix else tshort + return num * (1000 ** -factor), prefix