-
Notifications
You must be signed in to change notification settings - Fork 244
applet.interface.freq_counter: implement basic frequency counter #218
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
Draft
attie
wants to merge
13
commits into
GlasgowEmbedded:main
Choose a base branch
from
attie:ripple-counter
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
ab96494
platform.ice40: add support for getting an async-safe ripple counter …
attie-argentum e57b398
gateware.ripple: add support for a ripple counter, with external clock
attie-argentum a098c73
support.si_prefix: implement "real" to "scaled with si prefix" number…
attie-argentum 95f2e25
WIP: applet.interface.freq_counter: implement basic frequency counter
attie-argentum 09aec7b
WIP: add --duration argument
attie-argentum 8d8f40e
WIP: move control into FrequencyCounterInterface()
attie-argentum c9cc042
WIP: fixup num_to_si() ... 1 Hz in should give 1 Hz out, not 1000 mHz
attie-argentum de13266
WIP: add precision to output report
attie-argentum 26e8351
WIP: add note about slow edges
attie-argentum 3f69395
WIP: add pull-downs
attie-argentum 1a6bbbf
WIP: add interact() phase, so that interaction can be scripted
attie-argentum 5646fc4
WIP: reduce to 32-bit again... 64-bit registers aren't "atomic"
attie-argentum e0155fd
WIP: add note about runtime vs max freq
attie-argentum File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
202 changes: 202 additions & 0 deletions
202
software/glasgow/applet/interface/freq_counter/__init__.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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)) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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.)