Skip to content
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

applet.interface.freq_counter: implement basic frequency counter #218

Draft
wants to merge 13 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions software/glasgow/applet/all.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
202 changes: 202 additions & 0 deletions software/glasgow/applet/interface/freq_counter/__init__.py
Original file line number Diff line number Diff line change
@@ -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 <module>
# 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))
30 changes: 30 additions & 0 deletions software/glasgow/gateware/ripple.py
Original file line number Diff line number Diff line change
@@ -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
31 changes: 31 additions & 0 deletions software/glasgow/platform/ice40.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
16 changes: 16 additions & 0 deletions software/glasgow/support/si_prefix.py
Original file line number Diff line number Diff line change
@@ -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