Skip to content
Draft
Show file tree
Hide file tree
Changes from 12 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
192 changes: 192 additions & 0 deletions software/glasgow/applet/interface/freq_counter/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
#
# notes:
# - does not play nicely with slow edges or analog signals (e.g: sine wave)
# will produce very inaccurate and inconsistent results
#

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,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The gateware doesn't care about the register width at all, a 1-bit one is as atomic as a 128-bit. The reads are always atomic. The writes are never atomic, they are shifted in in 8-bit chunks. (This is probably a bug.)

)
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