add pdh-test
This commit is contained in:
parent
323ed54989
commit
6154d953ba
|
@ -83,6 +83,7 @@ git = "https://github.com/quartiq/smoltcp-nal.git"
|
||||||
rev = "0634188"
|
rev = "0634188"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
|
default = ["pounder_v1_1"]
|
||||||
nightly = ["cortex-m/inline-asm", "dsp/nightly"]
|
nightly = ["cortex-m/inline-asm", "dsp/nightly"]
|
||||||
pounder_v1_1 = [ ]
|
pounder_v1_1 = [ ]
|
||||||
|
|
||||||
|
|
50
README.md
50
README.md
|
@ -1,16 +1,46 @@
|
||||||
[![QUARTIQ Matrix Chat](https://img.shields.io/matrix/quartiq:matrix.org)](https://matrix.to/#/#quartiq:matrix.org)
|
|
||||||
[![Continuous Integration](https://github.com/quartiq/stabilizer/actions/workflows/ci.yml/badge.svg)](https://github.com/quartiq/stabilizer/actions/workflows/ci.yml)
|
|
||||||
[![Stabilizer HITL [Nightly]](https://github.com/quartiq/hitl/actions/workflows/stabilizer-nightly.yml/badge.svg)](https://github.com/quartiq/hitl/actions/workflows/stabilizer-nightly.yml)
|
|
||||||
|
|
||||||
# Stabilizer Firmware
|
# Pounder_test
|
||||||
|
|
||||||
## Applications
|
This Pounder quick noise floor test folder is forked from [quartiq_stabilizer](https://github.com/quartiq/stabilizer/tree/323ed54989992b613d65a0bb9711728ac24cf22a). Check out quartiq/stabilizer github page for more details.
|
||||||
|
|
||||||
|
For this quick test, I simply hard code the Pounder DDS setting in the init section in src/bin/pdh-test.rs , currently the DDS setting in this pdh-test has no MQTT control support yet. The stabilizer DAC is disabled and set to 0 here for simplicity. Change anything ask you like.
|
||||||
|
|
||||||
|
|
||||||
|
## How to use this
|
||||||
|
|
||||||
|
This nix shell cotains rust, mqtt-explorer ... and python packages of gmqtt, miniconf-mqtt and some other packages. Simply clone this repo and run this to set up the environment. I only tested it with nixos-21.05 channel.
|
||||||
|
```
|
||||||
|
nix-shell shell.nix
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
Connect stabilizer JTAG connector to a ST-link V2 (or other debuggers) and then connect the debugger to your PC. Run the pdh-test.rs by calling this in the terminal: (Cargo.toml default features contain pounder_v1_1 and the broker IP is set to be 192.168.1.139 , change it if necessary)
|
||||||
|
```
|
||||||
|
BROKER="192.168.1.139" cargo run --release --bin pdh-test
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
Open another terminal and start mosquitto broker with this:
|
||||||
|
```
|
||||||
|
mosquitto -c mosquitto.conf
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
Open another terminal and change the stabilizer setting via MQTT. For example this will change the gain of the stabilizer ADC_ch0 to 10 (change the broker IP and topic path's mac address to your own one) and you should see the update log in the cargo run terminal:
|
||||||
|
```
|
||||||
|
python -m miniconf --broker 192.168.1.139 dt/sinara/pdh-test/04-91-62-d9-f5-e5 afe/0='"G10"'
|
||||||
|
```
|
||||||
|
|
||||||
|
Another example is to start the stabilizer UDP datastream, which is currently streaming data of (adc_0, adc_1, dac_0, dac_1). You can change the UDP listener IP and port. To turn it off, just set the target ip to 0.0.0.0 and port to 0.
|
||||||
|
```
|
||||||
|
python -m miniconf --broker 192.168.1.139 dt/sinara/pdh-test/04-91-62-d9-f5-e5 stream_target='{"ip":[192,168,1,139], "port":1883}'
|
||||||
|
```
|
||||||
|
|
||||||
|
To see and plot the stabilizer ADC_ch0 data, run this. Change the port and the python file if you want to plot data of other adc or dac channel.
|
||||||
|
```
|
||||||
|
python scripts/stabilizer_adc_stream_test.py --port 1883
|
||||||
|
```
|
||||||
|
|
||||||
Check out the [Documentation](https://quartiq.de/stabilizer) for more information on usage,
|
|
||||||
configuration, and development.
|
|
||||||
|
|
||||||
## Hardware
|
|
||||||
|
|
||||||
[![Stabilizer](https://github.com/sinara-hw/Stabilizer/wiki/Stabilizer_v1.0_top_small.jpg)](https://github.com/sinara-hw/Stabilizer)
|
|
||||||
|
|
||||||
[![Pounder](https://user-images.githubusercontent.com/1338946/125936814-3664aa2d-a530-4c85-9393-999a7173424e.png)](https://github.com/sinara-hw/Pounder/wiki)
|
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
__pycache__/
|
||||||
|
*.egg
|
||||||
|
*.egg-info/
|
|
@ -0,0 +1,9 @@
|
||||||
|
# Miniconf Python Utility
|
||||||
|
|
||||||
|
This directory contains a Python package for interacting with Miniconf utilities.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
Run `pip install .` from this directory to install the `miniconf` package.
|
||||||
|
|
||||||
|
Alternatively, run `python -m pip install
|
||||||
|
git+https://github.com/quartiq/miniconf#subdirectory=miniconf-py` to avoid cloning locally.
|
|
@ -0,0 +1,4 @@
|
||||||
|
#!/usr/bin/python3
|
||||||
|
""" Root Miniconf module file. """
|
||||||
|
from .miniconf import Miniconf
|
||||||
|
from .version import __version__
|
|
@ -0,0 +1,56 @@
|
||||||
|
#!/usr/bin/python3
|
||||||
|
"""
|
||||||
|
Author: Ryan Summers, Robert Jördens
|
||||||
|
|
||||||
|
Description: Command-line utility to program run-time settings utilize Miniconf.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import argparse
|
||||||
|
import logging
|
||||||
|
import json
|
||||||
|
|
||||||
|
from .miniconf import Miniconf
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
""" Main program entry point. """
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description='Miniconf command line interface.',
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||||
|
epilog='''Examples:
|
||||||
|
%(prog)s dt/sinara/dual-iir/00-11-22-33-aa-bb stream_target=\
|
||||||
|
'{"ip": [192, 168, 0, 1], "port": 1000}'
|
||||||
|
''')
|
||||||
|
parser.add_argument('-v', '--verbose', action='count', default=0,
|
||||||
|
help='Increase logging verbosity')
|
||||||
|
parser.add_argument('--broker', '-b', default='mqtt', type=str,
|
||||||
|
help='The MQTT broker address')
|
||||||
|
parser.add_argument('--no-retain', '-n', default=False,
|
||||||
|
action='store_true',
|
||||||
|
help='Do not retain the affected settings')
|
||||||
|
parser.add_argument('prefix', type=str,
|
||||||
|
help='The MQTT topic prefix of the target')
|
||||||
|
parser.add_argument('settings', metavar="PATH=VALUE", nargs='+',
|
||||||
|
help='JSON encoded values for settings path keys.')
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
logging.basicConfig(
|
||||||
|
format='%(asctime)s [%(levelname)s] %(name)s: %(message)s',
|
||||||
|
level=logging.WARN - 10*args.verbose)
|
||||||
|
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
|
||||||
|
async def configure_settings():
|
||||||
|
interface = await Miniconf.create(args.prefix, args.broker)
|
||||||
|
for setting in args.settings:
|
||||||
|
path, value = setting.split("=", 1)
|
||||||
|
await interface.command(path, json.loads(value), not args.no_retain)
|
||||||
|
print(f'{path}: OK')
|
||||||
|
|
||||||
|
loop.run_until_complete(configure_settings())
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
|
@ -0,0 +1,98 @@
|
||||||
|
#!/usr/bin/python
|
||||||
|
"""
|
||||||
|
Author: Vertigo Designs, Ryan Summers
|
||||||
|
Robert Jördens
|
||||||
|
|
||||||
|
Description: Provides an API for controlling Miniconf devices over MQTT.
|
||||||
|
"""
|
||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from gmqtt import Client as MqttClient
|
||||||
|
|
||||||
|
LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class MiniconfException(Exception):
|
||||||
|
""" Generic exceptions generated by Miniconf. """
|
||||||
|
|
||||||
|
|
||||||
|
class Miniconf:
|
||||||
|
"""An asynchronous API for controlling Miniconf devices using MQTT."""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def create(cls, prefix, broker):
|
||||||
|
"""Create a connection to the broker and a Miniconf device using it."""
|
||||||
|
client = MqttClient(client_id='')
|
||||||
|
await client.connect(broker)
|
||||||
|
return cls(client, prefix)
|
||||||
|
|
||||||
|
def __init__(self, client, prefix):
|
||||||
|
"""Constructor.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
client: A connected MQTT5 client.
|
||||||
|
prefix: The MQTT toptic prefix of the device to control.
|
||||||
|
"""
|
||||||
|
self.request_id = 0
|
||||||
|
self.client = client
|
||||||
|
self.prefix = prefix
|
||||||
|
self.inflight = {}
|
||||||
|
self.client.on_message = self._handle_response
|
||||||
|
self.response_topic = f'{prefix}/response/{uuid.uuid1().hex}'
|
||||||
|
self.client.subscribe(self.response_topic)
|
||||||
|
|
||||||
|
def _handle_response(self, _client, topic, payload, _qos, properties):
|
||||||
|
"""Callback function for when messages are received over MQTT.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
_client: The MQTT client.
|
||||||
|
topic: The topic that the message was received on.
|
||||||
|
payload: The payload of the message.
|
||||||
|
_qos: The quality-of-service level of the received packet
|
||||||
|
properties: A dictionary of properties associated with the message.
|
||||||
|
"""
|
||||||
|
if topic == self.response_topic:
|
||||||
|
# Extract request_id corrleation data from the properties
|
||||||
|
request_id = int.from_bytes(
|
||||||
|
properties['correlation_data'][0], 'big')
|
||||||
|
|
||||||
|
self.inflight[request_id].set_result(json.loads(payload))
|
||||||
|
del self.inflight[request_id]
|
||||||
|
else:
|
||||||
|
LOGGER.warning('Unexpected message on "%s"', topic)
|
||||||
|
|
||||||
|
async def command(self, path, value, retain=True):
|
||||||
|
"""Write the provided data to the specified path.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
path: The path to write the message to.
|
||||||
|
value: The value to write to the path.
|
||||||
|
retain: Retain the MQTT message changing the setting
|
||||||
|
by the broker.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The response to the command as a dictionary.
|
||||||
|
"""
|
||||||
|
topic = f'{self.prefix}/settings/{path}'
|
||||||
|
|
||||||
|
fut = asyncio.get_running_loop().create_future()
|
||||||
|
|
||||||
|
# Assign unique correlation data for response dispatch
|
||||||
|
assert self.request_id not in self.inflight
|
||||||
|
self.inflight[self.request_id] = fut
|
||||||
|
correlation_data = self.request_id.to_bytes(4, 'big')
|
||||||
|
self.request_id += 1
|
||||||
|
|
||||||
|
payload = json.dumps(value)
|
||||||
|
LOGGER.info('Sending "%s" to "%s"', value, topic)
|
||||||
|
|
||||||
|
self.client.publish(
|
||||||
|
topic, payload=payload, qos=0, retain=retain,
|
||||||
|
response_topic=self.response_topic,
|
||||||
|
correlation_data=correlation_data)
|
||||||
|
|
||||||
|
result = await fut
|
||||||
|
if result['code'] != 0:
|
||||||
|
raise MiniconfException(result['msg'])
|
|
@ -0,0 +1,5 @@
|
||||||
|
#!/usr/bin/python3
|
||||||
|
""" Miniconf version file. """
|
||||||
|
|
||||||
|
# The semantic version of Miniconf.
|
||||||
|
__version__ = '0.1.0'
|
|
@ -0,0 +1,22 @@
|
||||||
|
#!/usr/bin/python3
|
||||||
|
"""
|
||||||
|
Author: Vertigo Designs, Ryan Summers
|
||||||
|
|
||||||
|
Description: Setup file for Miniconf packaging.
|
||||||
|
"""
|
||||||
|
from setuptools import setup, find_packages
|
||||||
|
|
||||||
|
# Load the version string from the version file.
|
||||||
|
with open('miniconf/version.py') as version_file:
|
||||||
|
exec(version_file.read())
|
||||||
|
|
||||||
|
setup(name='miniconf-mqtt',
|
||||||
|
version=__version__,
|
||||||
|
author='Ryan Summers, Robert Jördens',
|
||||||
|
description='Utilities for configuring Miniconf-configurable devices',
|
||||||
|
url='https://github.com/quartiq/miniconf',
|
||||||
|
packages=find_packages(),
|
||||||
|
install_requires=[
|
||||||
|
'gmqtt'
|
||||||
|
],
|
||||||
|
)
|
|
@ -0,0 +1,28 @@
|
||||||
|
{ stdenv, lib, fetchurl, appimageTools, electron_8, makeWrapper }:
|
||||||
|
|
||||||
|
stdenv.mkDerivation rec {
|
||||||
|
pname = "MQTT-Explorer";
|
||||||
|
version = "0.4.0-beta1";
|
||||||
|
src = appimageTools.extract {
|
||||||
|
name = pname;
|
||||||
|
src = fetchurl {
|
||||||
|
url = "https://github.com/thomasnordquist/${pname}/releases/download/0.0.0-${version}/${pname}-${version}.AppImage";
|
||||||
|
sha256 = "0x9ava13hn1nkk2kllh5ldi4b3hgmgwahk08sq48yljilgda4ppn";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
buildInputs = [ makeWrapper ];
|
||||||
|
installPhase = ''
|
||||||
|
install -m 444 -D resources/app.asar $out/libexec/app.asar
|
||||||
|
install -m 444 -D mqtt-explorer.png $out/share/icons/mqtt-explorer.png
|
||||||
|
install -m 444 -D mqtt-explorer.desktop $out/share/applications/mqtt-explorer.desktop
|
||||||
|
makeWrapper ${electron_8}/bin/electron $out/bin/mqtt-explorer --add-flags $out/libexec/app.asar
|
||||||
|
'';
|
||||||
|
meta = with lib; {
|
||||||
|
description = "A comprehensive and easy-to-use MQTT Client";
|
||||||
|
homepage = "https://mqtt-explorer.com/";
|
||||||
|
# license = # TODO: make licenses.cc-by-nd-40
|
||||||
|
# { free = false; fullName = "Creative Commons Attribution-No Derivative Works v4.00"; shortName = "cc-by-nd-40"; spdxId = "CC-BY-ND-4.0"; url = "https://spdx.org/licenses/CC-BY-ND-4.0.html"; };
|
||||||
|
# maintainers = [ maintainers.yorickvp ];
|
||||||
|
inherit (electron_8.meta) platforms;
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
[toolchain]
|
||||||
|
channel = "stable"
|
||||||
|
components = [ "rust-src" , "llvm-tools-preview" ]
|
||||||
|
targets = [ "x86_64-unknown-linux-gnu", "thumbv7em-none-eabihf" ]
|
|
@ -0,0 +1,284 @@
|
||||||
|
#!/usr/bin/python3
|
||||||
|
"""
|
||||||
|
Author: Ryan Summers
|
||||||
|
|
||||||
|
Description: quick test pounder_mixer_output => stabilizer ADC
|
||||||
|
|
||||||
|
ADC range is +- 4 V and 16 bit resolution
|
||||||
|
|
||||||
|
adc_volts_per_lsb is 0.00031250002
|
||||||
|
|
||||||
|
ADC gain can be 1,2,5,10
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
import argparse
|
||||||
|
import logging
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import numpy as np
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
|
||||||
|
|
||||||
|
#from stabilizer.stream import StabilizerStream
|
||||||
|
|
||||||
|
import socket
|
||||||
|
import struct
|
||||||
|
|
||||||
|
class StabilizerStream:
|
||||||
|
""" Provides access to Stabilizer's livestreamed data. """
|
||||||
|
|
||||||
|
# The magic header half-word at the start of each packet.
|
||||||
|
MAGIC_HEADER = 0x057B
|
||||||
|
|
||||||
|
# The struct format of the header.
|
||||||
|
HEADER_FORMAT = '<HBBI'
|
||||||
|
|
||||||
|
# All supported formats by this reception script.
|
||||||
|
#
|
||||||
|
# The items in this dict are functions that will be provided the sample batch size and will
|
||||||
|
# return the struct deserialization code to unpack a single batch.
|
||||||
|
FORMAT = {
|
||||||
|
1: lambda batch_size: f'<{batch_size}H{batch_size}H{batch_size}H{batch_size}H'
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, port, timeout=None):
|
||||||
|
""" Initialize the stream.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
port: The UDP port to receive the stream from.
|
||||||
|
timeout: The timeout to set on the UDP socket.
|
||||||
|
"""
|
||||||
|
self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||||
|
self.socket.bind(("", port))
|
||||||
|
self.total_bytes = 0
|
||||||
|
|
||||||
|
if timeout is not None:
|
||||||
|
self.socket.settimeout(timeout)
|
||||||
|
|
||||||
|
|
||||||
|
def clear(self, duration=1):
|
||||||
|
""" Clear the socket RX buffer by reading all available data.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
duration: The maximum duration in seconds to read data for.
|
||||||
|
"""
|
||||||
|
time.sleep(duration)
|
||||||
|
|
||||||
|
try:
|
||||||
|
while self.socket.recv(4096):
|
||||||
|
pass
|
||||||
|
except socket.timeout:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def get_rx_bytes(self):
|
||||||
|
""" Get the number of bytes read from the stream. """
|
||||||
|
return self.total_bytes
|
||||||
|
|
||||||
|
|
||||||
|
def read_frame(self):
|
||||||
|
""" Read a single frame from the stream.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Yields the (seqnum, data) of the batches available in the frame.
|
||||||
|
"""
|
||||||
|
buf = self.socket.recv(4096)
|
||||||
|
self.total_bytes += len(buf)
|
||||||
|
|
||||||
|
# Attempt to parse a block from the buffer.
|
||||||
|
if len(buf) < struct.calcsize(self.HEADER_FORMAT):
|
||||||
|
return
|
||||||
|
|
||||||
|
# Parse out the packet header
|
||||||
|
magic, format_id, batch_size, sequence_number = struct.unpack_from(self.HEADER_FORMAT, buf)
|
||||||
|
buf = buf[struct.calcsize(self.HEADER_FORMAT):]
|
||||||
|
|
||||||
|
if magic != self.MAGIC_HEADER:
|
||||||
|
logging.warning('Encountered bad magic header: %s', hex(magic))
|
||||||
|
return
|
||||||
|
|
||||||
|
frame_format = self.FORMAT[format_id](batch_size)
|
||||||
|
|
||||||
|
batch_count = int(len(buf) / struct.calcsize(frame_format))
|
||||||
|
|
||||||
|
for offset in range(batch_count):
|
||||||
|
data = struct.unpack_from(frame_format, buf)
|
||||||
|
buf = buf[struct.calcsize(frame_format):]
|
||||||
|
yield (sequence_number + offset, data)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class Timer:
|
||||||
|
""" A basic timer for measuring elapsed time periods. """
|
||||||
|
|
||||||
|
def __init__(self, period=1.0):
|
||||||
|
""" Create the timer with the provided period. """
|
||||||
|
self.start_time = time.time()
|
||||||
|
self.trigger_time = self.start_time + period
|
||||||
|
self.period = period
|
||||||
|
self.started = False
|
||||||
|
|
||||||
|
|
||||||
|
def is_triggered(self):
|
||||||
|
""" Check if the timer period has elapsed. """
|
||||||
|
now = time.time()
|
||||||
|
return now >= self.trigger_time
|
||||||
|
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
""" Start the timer. """
|
||||||
|
self.start_time = time.time()
|
||||||
|
self.started = True
|
||||||
|
|
||||||
|
|
||||||
|
def is_started(self):
|
||||||
|
""" Check if the timer has started. """
|
||||||
|
return self.started
|
||||||
|
|
||||||
|
|
||||||
|
def arm(self):
|
||||||
|
""" Arm the timer trigger. """
|
||||||
|
self.trigger_time = time.time() + self.period
|
||||||
|
|
||||||
|
|
||||||
|
def elapsed(self):
|
||||||
|
""" Get the elapsed time since the timer was started. """
|
||||||
|
now = time.time()
|
||||||
|
return now - self.start_time
|
||||||
|
|
||||||
|
|
||||||
|
def sequence_delta(previous_sequence, next_sequence):
|
||||||
|
""" Check the number of items between two sequence numbers. """
|
||||||
|
if previous_sequence is None:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
delta = next_sequence - (previous_sequence + 1)
|
||||||
|
return delta & 0xFFFFFFFF
|
||||||
|
|
||||||
|
|
||||||
|
def adc_code_to_volt (raw_adc_code_data):
|
||||||
|
adc_volts_per_lsb = np.float32 ( (5.0 / 2.0) * 4.096 / (2 ** 15) )
|
||||||
|
# +- 4 V with 16 bit resolution equivalent to 4V with 15 bit resolution
|
||||||
|
# op-amp has gain 1/5 and then divide into two differential inputs
|
||||||
|
return np.float32( np.int16(raw_adc_code_data) ) * adc_volts_per_lsb
|
||||||
|
|
||||||
|
def flatten(t):
|
||||||
|
return [item for sublist in t for item in sublist]
|
||||||
|
|
||||||
|
def my_plot (data):
|
||||||
|
fig, (ax1, ax2, ax3) = plt.subplots(3)
|
||||||
|
|
||||||
|
ax1.plot( 1000*data )
|
||||||
|
max_v = 1000*max(data)
|
||||||
|
min_v = 1000*min(data)
|
||||||
|
ax1.axhline(y=0.0, color='r', linestyle='-', linewidth=0.3)
|
||||||
|
ax1.axhline(y=np.mean(1000*data), color='g', linestyle='-', linewidth=0.3)
|
||||||
|
|
||||||
|
ax1.set_ylabel('stabilizer_adc_0 / mV')
|
||||||
|
ax1.set_xlabel('time / 1.28 us')
|
||||||
|
ax1.set_title(f' Stabilizer ADC gain=10 with max V = {max_v:.3f} mV and min V = {min_v:.3f} mV')
|
||||||
|
|
||||||
|
data = np.array(data)
|
||||||
|
|
||||||
|
# import csv
|
||||||
|
# with open('data4.csv', 'w') as f:
|
||||||
|
# writer = csv.writer(f)
|
||||||
|
# writer.writerow(data)
|
||||||
|
|
||||||
|
N = data.shape[0]
|
||||||
|
timestep = 1.28e-6
|
||||||
|
f = np.fft.rfftfreq(N,timestep)
|
||||||
|
f = f/1000 # convert to kHz
|
||||||
|
|
||||||
|
Vxx = (1./N)*np.fft.rfft(data)
|
||||||
|
Vxx = np.abs(Vxx)
|
||||||
|
|
||||||
|
V_max = max(Vxx)
|
||||||
|
max_freq = f[ np.argmax(Vxx) ]
|
||||||
|
|
||||||
|
ax2.plot( f , Vxx )
|
||||||
|
ax2.set_ylabel('FFT Amplitude by (1./N)*np.fft.rfft(stabilizer_adc_0)')
|
||||||
|
ax2.set_xlabel('Frequency / kHz')
|
||||||
|
ax2.set_title(f'FFT of {N} datapoint and max is {V_max:.5f} at {max_freq:.3f} kHz')
|
||||||
|
ax2.set_xlim([-10, 400])
|
||||||
|
# ax2.set_ylim([0, 0.02])
|
||||||
|
|
||||||
|
Pxx = 20*np.log10(Vxx)
|
||||||
|
ax3.plot( f , Pxx )
|
||||||
|
ax3.set_ylabel('20*np.log10(Amplitude)')
|
||||||
|
ax3.set_xlabel('Frequency / kHz')
|
||||||
|
ax3.set_title(f'FFT of {N} datapoint')
|
||||||
|
ax3.set_xlim([-10, 400])
|
||||||
|
plt.subplots_adjust(hspace = 0.5)
|
||||||
|
plt.show()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
""" Main program. """
|
||||||
|
parser = argparse.ArgumentParser(description='Measure Stabilizer livestream quality')
|
||||||
|
parser.add_argument('--port', type=int, default=1883,
|
||||||
|
help='The port that stabilizer is streaming to')
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.INFO,
|
||||||
|
format='%(asctime)s.%(msecs)03d %(levelname)-8s %(message)s')
|
||||||
|
|
||||||
|
last_index = None
|
||||||
|
|
||||||
|
drop_count = 0
|
||||||
|
good_blocks = 0
|
||||||
|
total_bytes = 0
|
||||||
|
|
||||||
|
timer = Timer()
|
||||||
|
|
||||||
|
stream = StabilizerStream(args.port)
|
||||||
|
|
||||||
|
adc_0_data = []
|
||||||
|
|
||||||
|
while True:
|
||||||
|
# Receive any data over UDP and parse it.
|
||||||
|
for (seqnum, _data) in stream.read_frame():
|
||||||
|
if not timer.is_started():
|
||||||
|
timer.start()
|
||||||
|
|
||||||
|
# Handle any dropped packets.
|
||||||
|
drop_count += sequence_delta(last_index, seqnum)
|
||||||
|
last_index = seqnum
|
||||||
|
good_blocks += 1
|
||||||
|
|
||||||
|
# if drop_count> 0 :
|
||||||
|
# print("UDP packet drop warning")
|
||||||
|
# break
|
||||||
|
|
||||||
|
if 1000 < good_blocks and good_blocks < 15050 : # 16080
|
||||||
|
# print(_data[:8])
|
||||||
|
adc_0_data.append( _data[:8] ) # adc_ch0
|
||||||
|
# adc_0_data.append( _data[8:16] ) # adc_ch1
|
||||||
|
|
||||||
|
if good_blocks == 15051 :
|
||||||
|
print(_data)
|
||||||
|
print(f"drop_count is {drop_count}")
|
||||||
|
my_plot( adc_code_to_volt(flatten(adc_0_data)) )
|
||||||
|
break
|
||||||
|
|
||||||
|
# # Report the throughput periodically.
|
||||||
|
# if timer.is_triggered():
|
||||||
|
# drate = stream.get_rx_bytes() * 8 / 1e6 / timer.elapsed()
|
||||||
|
|
||||||
|
# print(f'''
|
||||||
|
# Data Rate: {drate:.3f} Mbps
|
||||||
|
# Received Blocks: {good_blocks}
|
||||||
|
# Dropped blocks: {drop_count}
|
||||||
|
|
||||||
|
# Metadata: {total_bytes / 1e6:.3f} MB in {timer.elapsed():.2f} s
|
||||||
|
# ----
|
||||||
|
# ''')
|
||||||
|
# sys.stdout.flush()
|
||||||
|
# timer.arm()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
|
@ -9,7 +9,94 @@ import logging
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from stabilizer.stream import StabilizerStream
|
#from stabilizer.stream import StabilizerStream
|
||||||
|
|
||||||
|
import socket
|
||||||
|
import struct
|
||||||
|
|
||||||
|
class StabilizerStream:
|
||||||
|
""" Provides access to Stabilizer's livestreamed data. """
|
||||||
|
|
||||||
|
# The magic header half-word at the start of each packet.
|
||||||
|
MAGIC_HEADER = 0x057B
|
||||||
|
|
||||||
|
# The struct format of the header.
|
||||||
|
HEADER_FORMAT = '<HBBI'
|
||||||
|
|
||||||
|
# All supported formats by this reception script.
|
||||||
|
#
|
||||||
|
# The items in this dict are functions that will be provided the sample batch size and will
|
||||||
|
# return the struct deserialization code to unpack a single batch.
|
||||||
|
FORMAT = {
|
||||||
|
1: lambda batch_size: f'<{batch_size}H{batch_size}H{batch_size}H{batch_size}H'
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, port, timeout=None):
|
||||||
|
""" Initialize the stream.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
port: The UDP port to receive the stream from.
|
||||||
|
timeout: The timeout to set on the UDP socket.
|
||||||
|
"""
|
||||||
|
self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||||
|
self.socket.bind(("", port))
|
||||||
|
self.total_bytes = 0
|
||||||
|
|
||||||
|
if timeout is not None:
|
||||||
|
self.socket.settimeout(timeout)
|
||||||
|
|
||||||
|
|
||||||
|
def clear(self, duration=1):
|
||||||
|
""" Clear the socket RX buffer by reading all available data.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
duration: The maximum duration in seconds to read data for.
|
||||||
|
"""
|
||||||
|
time.sleep(duration)
|
||||||
|
|
||||||
|
try:
|
||||||
|
while self.socket.recv(4096):
|
||||||
|
pass
|
||||||
|
except socket.timeout:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def get_rx_bytes(self):
|
||||||
|
""" Get the number of bytes read from the stream. """
|
||||||
|
return self.total_bytes
|
||||||
|
|
||||||
|
|
||||||
|
def read_frame(self):
|
||||||
|
""" Read a single frame from the stream.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Yields the (seqnum, data) of the batches available in the frame.
|
||||||
|
"""
|
||||||
|
buf = self.socket.recv(4096)
|
||||||
|
self.total_bytes += len(buf)
|
||||||
|
|
||||||
|
# Attempt to parse a block from the buffer.
|
||||||
|
if len(buf) < struct.calcsize(self.HEADER_FORMAT):
|
||||||
|
return
|
||||||
|
|
||||||
|
# Parse out the packet header
|
||||||
|
magic, format_id, batch_size, sequence_number = struct.unpack_from(self.HEADER_FORMAT, buf)
|
||||||
|
buf = buf[struct.calcsize(self.HEADER_FORMAT):]
|
||||||
|
|
||||||
|
if magic != self.MAGIC_HEADER:
|
||||||
|
logging.warning('Encountered bad magic header: %s', hex(magic))
|
||||||
|
return
|
||||||
|
|
||||||
|
frame_format = self.FORMAT[format_id](batch_size)
|
||||||
|
|
||||||
|
batch_count = int(len(buf) / struct.calcsize(frame_format))
|
||||||
|
|
||||||
|
for offset in range(batch_count):
|
||||||
|
data = struct.unpack_from(frame_format, buf)
|
||||||
|
buf = buf[struct.calcsize(frame_format):]
|
||||||
|
yield (sequence_number + offset, data)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class Timer:
|
class Timer:
|
||||||
""" A basic timer for measuring elapsed time periods. """
|
""" A basic timer for measuring elapsed time periods. """
|
||||||
|
@ -62,7 +149,7 @@ def sequence_delta(previous_sequence, next_sequence):
|
||||||
def main():
|
def main():
|
||||||
""" Main program. """
|
""" Main program. """
|
||||||
parser = argparse.ArgumentParser(description='Measure Stabilizer livestream quality')
|
parser = argparse.ArgumentParser(description='Measure Stabilizer livestream quality')
|
||||||
parser.add_argument('--port', type=int, default=2000,
|
parser.add_argument('--port', type=int, default=1883,
|
||||||
help='The port that stabilizer is streaming to')
|
help='The port that stabilizer is streaming to')
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
@ -82,7 +169,7 @@ def main():
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
# Receive any data over UDP and parse it.
|
# Receive any data over UDP and parse it.
|
||||||
for (seqnum, _) in stream.read_frame():
|
for (seqnum, _data) in stream.read_frame():
|
||||||
if not timer.is_started():
|
if not timer.is_started():
|
||||||
timer.start()
|
timer.start()
|
||||||
|
|
||||||
|
@ -91,6 +178,9 @@ def main():
|
||||||
last_index = seqnum
|
last_index = seqnum
|
||||||
good_blocks += 1
|
good_blocks += 1
|
||||||
|
|
||||||
|
if 1000 < good_blocks and good_blocks < 1020 :
|
||||||
|
print(_data[:8])
|
||||||
|
|
||||||
# Report the throughput periodically.
|
# Report the throughput periodically.
|
||||||
if timer.is_triggered():
|
if timer.is_triggered():
|
||||||
drate = stream.get_rx_bytes() * 8 / 1e6 / timer.elapsed()
|
drate = stream.get_rx_bytes() * 8 / 1e6 / timer.elapsed()
|
||||||
|
|
|
@ -0,0 +1,91 @@
|
||||||
|
let
|
||||||
|
|
||||||
|
rust_overlay = import (builtins.fetchTarball
|
||||||
|
"https://github.com/oxalica/rust-overlay/archive/master.tar.gz");
|
||||||
|
|
||||||
|
pkgs = import <nixpkgs> { overlays = [ rust_overlay ]; };
|
||||||
|
|
||||||
|
rust_channel = pkgs.rust-bin.fromRustupToolchainFile ./nix/rust-toolchain;
|
||||||
|
|
||||||
|
mqtt-explorer = pkgs.callPackage ./nix/mqtt-explorer.nix {} ;
|
||||||
|
|
||||||
|
gmqtt = pkgs.python3Packages.buildPythonPackage rec {
|
||||||
|
name = "gmqtt";
|
||||||
|
version = "v0.6.10";
|
||||||
|
src = pkgs.fetchFromGitHub {
|
||||||
|
owner = "wialon" ;
|
||||||
|
repo = "gmqtt";
|
||||||
|
rev = "50bc08300d858c6e28d4975a989be578954bf7bd" ;
|
||||||
|
sha256 = "1f51x3vkpkqa20mdqqakyk9ms7l5p3i4masrfhc6n7cr45qfr5fz";
|
||||||
|
};
|
||||||
|
|
||||||
|
buildInputs = [
|
||||||
|
(pkgs.python3.withPackages(ps: [
|
||||||
|
ps.codecov
|
||||||
|
ps.pytest-asyncio
|
||||||
|
ps.pytest
|
||||||
|
ps.pytestcov
|
||||||
|
ps.six
|
||||||
|
ps.uvloop
|
||||||
|
]))
|
||||||
|
];
|
||||||
|
|
||||||
|
propagatedBuildInputs = with pkgs.python3Packages; [ setproctitle ];
|
||||||
|
|
||||||
|
meta = {
|
||||||
|
homepage = "https://github.com/wialon/gmqtt";
|
||||||
|
description = "gmqtt";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
miniconf = pkgs.python3Packages.buildPythonPackage rec {
|
||||||
|
name = "miniconf";
|
||||||
|
src = ./miniconf-mqtt;
|
||||||
|
propagatedBuildInputs = with pkgs.python3Packages; [ gmqtt setuptools ];
|
||||||
|
};
|
||||||
|
|
||||||
|
stabilizer = pkgs.python3Packages.buildPythonPackage rec {
|
||||||
|
name = "stabilizer" ;
|
||||||
|
src = ./py;
|
||||||
|
propagatedBuildInputs = with pkgs.python3Packages; [ setuptools ];
|
||||||
|
};
|
||||||
|
|
||||||
|
in
|
||||||
|
pkgs.mkShell {
|
||||||
|
buildInputs = [
|
||||||
|
(pkgs.python3.withPackages(ps: [
|
||||||
|
# List desired Python packages here.
|
||||||
|
ps.numpy
|
||||||
|
ps.setuptools
|
||||||
|
ps.matplotlib
|
||||||
|
gmqtt
|
||||||
|
stabilizer
|
||||||
|
miniconf
|
||||||
|
|
||||||
|
]))
|
||||||
|
|
||||||
|
# List desired non-Python packages here
|
||||||
|
# dfu ... for usb flashing
|
||||||
|
pkgs.libtool
|
||||||
|
rust_channel
|
||||||
|
pkgs.pkg-config
|
||||||
|
pkgs.libusb1
|
||||||
|
pkgs.dfu-util
|
||||||
|
pkgs.gcc-arm-embedded-8
|
||||||
|
pkgs.stlink
|
||||||
|
pkgs.mosquitto
|
||||||
|
pkgs.gdb
|
||||||
|
pkgs.automake
|
||||||
|
pkgs.autoconf
|
||||||
|
pkgs.texinfo
|
||||||
|
pkgs.libftdi
|
||||||
|
mqtt-explorer
|
||||||
|
|
||||||
|
];
|
||||||
|
|
||||||
|
shellHook = ''
|
||||||
|
export PATH="/home/$USER/.cargo/bin:$PATH"
|
||||||
|
'';
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,556 @@
|
||||||
|
// # pdh_test
|
||||||
|
// hard coded DDS for quick testing mixer IF output noise floor
|
||||||
|
|
||||||
|
//! The Dual IIR application exposes two configurable channels. Stabilizer samples input at a fixed
|
||||||
|
//! rate, digitally filters the data, and then generates filtered output signals on the respective
|
||||||
|
//! channel outputs.
|
||||||
|
//!
|
||||||
|
//! ## Features
|
||||||
|
//! * Two indpenendent channels
|
||||||
|
//! * up to 800 kHz rate, timed sampling
|
||||||
|
//! * Run-time filter configuration
|
||||||
|
//! * Input/Output data streaming
|
||||||
|
//! * Down to 2 µs latency
|
||||||
|
//! * f32 IIR math
|
||||||
|
//! * Generic biquad (second order) IIR filter
|
||||||
|
//! * Anti-windup
|
||||||
|
//! * Derivative kick avoidance
|
||||||
|
//!
|
||||||
|
//! ## Settings
|
||||||
|
//! Refer to the [Settings] structure for documentation of run-time configurable settings for this
|
||||||
|
//! application.
|
||||||
|
//!
|
||||||
|
//! ## Telemetry
|
||||||
|
//! Refer to [Telemetry] for information about telemetry reported by this application.
|
||||||
|
//!
|
||||||
|
//! ## Livestreaming
|
||||||
|
//! This application streams raw ADC and DAC data over UDP. Refer to
|
||||||
|
//! [stabilizer::net::data_stream](../stabilizer/net/data_stream/index.html) for more information.
|
||||||
|
#![deny(warnings)]
|
||||||
|
#![no_std]
|
||||||
|
#![no_main]
|
||||||
|
|
||||||
|
use core::sync::atomic::{fence, Ordering};
|
||||||
|
|
||||||
|
use mutex_trait::prelude::*;
|
||||||
|
|
||||||
|
use dsp::iir;
|
||||||
|
use stabilizer::{
|
||||||
|
hardware::{
|
||||||
|
self,
|
||||||
|
adc::{Adc0Input, Adc1Input, AdcCode},
|
||||||
|
afe::Gain,
|
||||||
|
dac::{Dac0Output, Dac1Output, DacCode},
|
||||||
|
embedded_hal::digital::v2::InputPin,
|
||||||
|
hal,
|
||||||
|
signal_generator::{self, SignalGenerator},
|
||||||
|
system_timer::SystemTimer,
|
||||||
|
DigitalInput0, DigitalInput1, AFE0, AFE1,
|
||||||
|
pounder::{
|
||||||
|
attenuators::AttenuatorInterface,
|
||||||
|
rf_power::PowerMeasurementInterface,
|
||||||
|
Channel,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
net::{
|
||||||
|
data_stream::{FrameGenerator, StreamFormat, StreamTarget},
|
||||||
|
miniconf::Miniconf,
|
||||||
|
serde::Deserialize,
|
||||||
|
telemetry::{Telemetry, TelemetryBuffer},
|
||||||
|
NetworkState, NetworkUsers,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const SCALE: f32 = i16::MAX as _;
|
||||||
|
|
||||||
|
// The number of cascaded IIR biquads per channel. Select 1 or 2!
|
||||||
|
const IIR_CASCADE_LENGTH: usize = 1;
|
||||||
|
|
||||||
|
// The number of samples in each batch process
|
||||||
|
const BATCH_SIZE: usize = 8;
|
||||||
|
|
||||||
|
// The logarithm of the number of 100MHz timer ticks between each sample. With a value of 2^7 =
|
||||||
|
// 128, there is 1.28uS per sample, corresponding to a sampling frequency of 781.25 KHz.
|
||||||
|
const SAMPLE_TICKS_LOG2: u8 = 7;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Deserialize, Miniconf)]
|
||||||
|
pub struct Settings {
|
||||||
|
/// Configure the Analog Front End (AFE) gain.
|
||||||
|
///
|
||||||
|
/// # Path
|
||||||
|
/// `afe/<n>`
|
||||||
|
///
|
||||||
|
/// * <n> specifies which channel to configure. <n> := [0, 1]
|
||||||
|
///
|
||||||
|
/// # Value
|
||||||
|
/// Any of the variants of [Gain] enclosed in double quotes.
|
||||||
|
afe: [Gain; 2],
|
||||||
|
|
||||||
|
/// Configure the IIR filter parameters.
|
||||||
|
///
|
||||||
|
/// # Path
|
||||||
|
/// `iir_ch/<n>/<m>`
|
||||||
|
///
|
||||||
|
/// * <n> specifies which channel to configure. <n> := [0, 1]
|
||||||
|
/// * <m> specifies which cascade to configure. <m> := [0, 1], depending on [IIR_CASCADE_LENGTH]
|
||||||
|
///
|
||||||
|
/// # Value
|
||||||
|
/// See [iir::IIR#miniconf]
|
||||||
|
iir_ch: [[iir::IIR; IIR_CASCADE_LENGTH]; 2],
|
||||||
|
|
||||||
|
/// Specified true if DI1 should be used as a "hold" input.
|
||||||
|
///
|
||||||
|
/// # Path
|
||||||
|
/// `allow_hold`
|
||||||
|
///
|
||||||
|
/// # Value
|
||||||
|
/// "true" or "false"
|
||||||
|
allow_hold: bool,
|
||||||
|
|
||||||
|
/// Specified true if "hold" should be forced regardless of DI1 state and hold allowance.
|
||||||
|
///
|
||||||
|
/// # Path
|
||||||
|
/// `force_hold`
|
||||||
|
///
|
||||||
|
/// # Value
|
||||||
|
/// "true" or "false"
|
||||||
|
force_hold: bool,
|
||||||
|
|
||||||
|
/// Specifies the telemetry output period in seconds.
|
||||||
|
///
|
||||||
|
/// # Path
|
||||||
|
/// `telemetry_period`
|
||||||
|
///
|
||||||
|
/// # Value
|
||||||
|
/// Any non-zero value less than 65536.
|
||||||
|
telemetry_period: u16,
|
||||||
|
|
||||||
|
/// Specifies the target for data livestreaming.
|
||||||
|
///
|
||||||
|
/// # Path
|
||||||
|
/// `stream_target`
|
||||||
|
///
|
||||||
|
/// # Value
|
||||||
|
/// See [StreamTarget#miniconf]
|
||||||
|
stream_target: StreamTarget,
|
||||||
|
|
||||||
|
/// Specifies the config for signal generators to add on to DAC0/DAC1 outputs.
|
||||||
|
///
|
||||||
|
/// # Path
|
||||||
|
/// `signal_generator/<n>`
|
||||||
|
///
|
||||||
|
/// * <n> specifies which channel to configure. <n> := [0, 1]
|
||||||
|
///
|
||||||
|
/// # Value
|
||||||
|
/// See [signal_generator::BasicConfig#miniconf]
|
||||||
|
signal_generator: [signal_generator::BasicConfig; 2],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Settings {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
// Analog frontend programmable gain amplifier gains (G1, G2, G5, G10)
|
||||||
|
afe: [Gain::G1, Gain::G1],
|
||||||
|
// IIR filter tap gains are an array `[b0, b1, b2, a1, a2]` such that the
|
||||||
|
// new output is computed as `y0 = a1*y1 + a2*y2 + b0*x0 + b1*x1 + b2*x2`.
|
||||||
|
// The array is `iir_state[channel-index][cascade-index][coeff-index]`.
|
||||||
|
// The IIR coefficients can be mapped to other transfer function
|
||||||
|
// representations, for example as described in https://arxiv.org/abs/1508.06319
|
||||||
|
iir_ch: [[iir::IIR::new(1., -SCALE, SCALE); IIR_CASCADE_LENGTH]; 2],
|
||||||
|
// Permit the DI1 digital input to suppress filter output updates.
|
||||||
|
allow_hold: false,
|
||||||
|
// Force suppress filter output updates.
|
||||||
|
force_hold: false,
|
||||||
|
// The default telemetry period in seconds.
|
||||||
|
telemetry_period: 5, // 10 before
|
||||||
|
|
||||||
|
signal_generator: [signal_generator::BasicConfig::default(); 2],
|
||||||
|
|
||||||
|
stream_target: StreamTarget::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rtic::app(device = stabilizer::hardware::hal::stm32, peripherals = true, monotonic = stabilizer::hardware::system_timer::SystemTimer)]
|
||||||
|
const APP: () = {
|
||||||
|
struct Resources {
|
||||||
|
afes: (AFE0, AFE1),
|
||||||
|
digital_inputs: (DigitalInput0, DigitalInput1),
|
||||||
|
adcs: (Adc0Input, Adc1Input),
|
||||||
|
dacs: (Dac0Output, Dac1Output),
|
||||||
|
network: NetworkUsers<Settings, Telemetry>,
|
||||||
|
generator: FrameGenerator,
|
||||||
|
signal_generator: [SignalGenerator; 2],
|
||||||
|
|
||||||
|
settings: Settings,
|
||||||
|
telemetry: TelemetryBuffer,
|
||||||
|
|
||||||
|
#[init([[[0.; 5]; IIR_CASCADE_LENGTH]; 2])]
|
||||||
|
iir_state: [[iir::Vec5; IIR_CASCADE_LENGTH]; 2],
|
||||||
|
}
|
||||||
|
|
||||||
|
#[init(spawn=[telemetry, settings_update, ethernet_link])]
|
||||||
|
fn init(c: init::Context) -> init::LateResources {
|
||||||
|
// Configure the microcontroller
|
||||||
|
let (mut stabilizer, _pounder) = hardware::setup::setup(
|
||||||
|
c.core,
|
||||||
|
c.device,
|
||||||
|
BATCH_SIZE,
|
||||||
|
1 << SAMPLE_TICKS_LOG2,
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut network = NetworkUsers::new(
|
||||||
|
stabilizer.net.stack,
|
||||||
|
stabilizer.net.phy,
|
||||||
|
stabilizer.cycle_counter,
|
||||||
|
env!("CARGO_BIN_NAME"),
|
||||||
|
stabilizer.net.mac_address,
|
||||||
|
option_env!("BROKER")
|
||||||
|
.unwrap_or("192.168.1.139")
|
||||||
|
.parse()
|
||||||
|
.unwrap(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let generator = network
|
||||||
|
.configure_streaming(StreamFormat::AdcDacData, BATCH_SIZE as u8);
|
||||||
|
|
||||||
|
// Spawn a settings update for default settings.
|
||||||
|
c.spawn.settings_update().unwrap();
|
||||||
|
c.spawn.telemetry().unwrap();
|
||||||
|
|
||||||
|
// Spawn the ethernet link period check task.
|
||||||
|
c.spawn.ethernet_link().unwrap();
|
||||||
|
|
||||||
|
|
||||||
|
// unwrap the _pounder Option type
|
||||||
|
let mut my_pounder = _pounder.expect("none pounder error") ;
|
||||||
|
|
||||||
|
// Reset attenuators, assumed to use internal clock here
|
||||||
|
my_pounder.pounder.reset_attenuators().unwrap();
|
||||||
|
log::info!("Reset all attenuators");
|
||||||
|
|
||||||
|
// Hard code setting the DDS (channel , ftw , pow , acr)
|
||||||
|
// RF
|
||||||
|
// ftw is 32 bit , 0.12 Hz per 1 ftw
|
||||||
|
let my_channel_1 = [ad9959::Channel::from(Channel::Out0)] ;
|
||||||
|
let frequency_1 = 103e6 ; // 4.8e6 ; // 5e6 ;
|
||||||
|
let frequency_tuning_word_1 = ( (frequency_1 as f32 / 500e6) * 1u64.wrapping_shl(32) as f32 ) as u32 ;
|
||||||
|
log::info!("ftw_1 {}", frequency_tuning_word_1);
|
||||||
|
|
||||||
|
// cpow is 14 bit
|
||||||
|
// this one shift the phase earlier by 90 degree
|
||||||
|
|
||||||
|
// adjust the phase until mixer output center at 0 V or the absolute DC amplitude is 0
|
||||||
|
|
||||||
|
// for 5e6 Hz
|
||||||
|
// let phase_dds1: u16 = (0x3FFF as u16) / 4 + 286 - 2 + 5 ;
|
||||||
|
|
||||||
|
// for 50e6 Hz
|
||||||
|
// let phase_dds1: u16 = 400 - 20 as u16 ;
|
||||||
|
|
||||||
|
// for 51.51 MHz
|
||||||
|
// let phase_dds1: u16 = 450 + 60 + 6 as u16 ;
|
||||||
|
|
||||||
|
// for 100e6 Hz + 3 MHz
|
||||||
|
let phase_dds1: u16 = (0x3FFF as u16) / 4 + 613 + 15 - 8 +121 ; // 608
|
||||||
|
|
||||||
|
// for 150e6 Hz
|
||||||
|
// let phase_dds1: u16 = 800 - 5 as u16 ;
|
||||||
|
|
||||||
|
|
||||||
|
// acr is 10 bit, 0x1 is to enable amplitude scaling
|
||||||
|
// 0x03ff = 1023 => 9 dBm
|
||||||
|
// 0x1100 = 256 => -3 dBm
|
||||||
|
// 0x10b7 = 183 => -6 dBm (around)
|
||||||
|
// 0x1039 = 57 => -16 dBm
|
||||||
|
// 0x1011 = 17 => -26 dBm
|
||||||
|
let amp_acr_1 = 0x1039 as u32 ; //-16 dBm
|
||||||
|
|
||||||
|
// RF : Pounder_Out_0 => SMA => Pounder_In_0
|
||||||
|
{
|
||||||
|
my_pounder.dds_output.builder()
|
||||||
|
.update_channels( & my_channel_1, Some(frequency_tuning_word_1), Some(phase_dds1), Some(amp_acr_1) )
|
||||||
|
.write_profile() ;
|
||||||
|
}
|
||||||
|
|
||||||
|
// LO
|
||||||
|
let my_channel_2 = [ad9959::Channel::from(Channel::In0)] ;
|
||||||
|
let _frequency_2 = 103e6 ; // 5e6 ; // 15.1e6 ;
|
||||||
|
let _frequency_tuning_word_2 = ( (_frequency_2 as f32 / 500e6) * 1u64.wrapping_shl(32) as f32 ) as u32 ;
|
||||||
|
log::info!("ftw_2 {}", _frequency_tuning_word_2);
|
||||||
|
let amp_acr_2 = 0x1100 as u32 ; // -3 dbm
|
||||||
|
{
|
||||||
|
my_pounder.dds_output.builder()
|
||||||
|
.update_channels( & my_channel_2, Some(_frequency_tuning_word_2), Some(0 as u16), Some(amp_acr_2) )
|
||||||
|
.write_profile() ;
|
||||||
|
}
|
||||||
|
|
||||||
|
// move the io_update_trigger out to make the output synchronized
|
||||||
|
my_pounder.dds_output.io_update_trigger.trigger();
|
||||||
|
|
||||||
|
log::info!("Finish DDS update");
|
||||||
|
|
||||||
|
// set attenuation to 0.0 dB (f32)
|
||||||
|
my_pounder.pounder.set_attenuation(Channel::In0 , 10.0).unwrap();
|
||||||
|
my_pounder.pounder.set_attenuation(Channel::Out0, 0.0).unwrap();
|
||||||
|
// my_pounder.pounder.set_attenuation(Channel::In1, 10.0).unwrap();
|
||||||
|
// my_pounder.pounder.set_attenuation(Channel::Out1, 0.0).unwrap();
|
||||||
|
|
||||||
|
log::info!("Finish setting attenuation");
|
||||||
|
|
||||||
|
for _i in 0..10 {
|
||||||
|
log::info!("In0 RF power is {} dBm", my_pounder.pounder.measure_power(Channel::In0).unwrap() );
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Enable ADC/DAC events
|
||||||
|
stabilizer.adcs.0.start();
|
||||||
|
stabilizer.adcs.1.start();
|
||||||
|
stabilizer.dacs.0.start();
|
||||||
|
stabilizer.dacs.1.start();
|
||||||
|
|
||||||
|
// Start sampling ADCs.
|
||||||
|
stabilizer.adc_dac_timer.start();
|
||||||
|
|
||||||
|
let settings = Settings::default();
|
||||||
|
|
||||||
|
init::LateResources {
|
||||||
|
afes: stabilizer.afes,
|
||||||
|
adcs: stabilizer.adcs,
|
||||||
|
dacs: stabilizer.dacs,
|
||||||
|
generator,
|
||||||
|
network,
|
||||||
|
digital_inputs: stabilizer.digital_inputs,
|
||||||
|
telemetry: TelemetryBuffer::default(),
|
||||||
|
settings,
|
||||||
|
signal_generator: [
|
||||||
|
SignalGenerator::new(
|
||||||
|
settings.signal_generator[0]
|
||||||
|
.try_into_config(SAMPLE_TICKS_LOG2)
|
||||||
|
.unwrap(),
|
||||||
|
),
|
||||||
|
SignalGenerator::new(
|
||||||
|
settings.signal_generator[1]
|
||||||
|
.try_into_config(SAMPLE_TICKS_LOG2)
|
||||||
|
.unwrap(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Main DSP processing routine for Stabilizer.
|
||||||
|
///
|
||||||
|
/// # Note
|
||||||
|
/// Processing time for the DSP application code is bounded by the following constraints:
|
||||||
|
///
|
||||||
|
/// DSP application code starts after the ADC has generated a batch of samples and must be
|
||||||
|
/// completed by the time the next batch of ADC samples has been acquired (plus the FIFO buffer
|
||||||
|
/// time). If this constraint is not met, firmware will panic due to an ADC input overrun.
|
||||||
|
///
|
||||||
|
/// The DSP application code must also fill out the next DAC output buffer in time such that the
|
||||||
|
/// DAC can switch to it when it has completed the current buffer. If this constraint is not met
|
||||||
|
/// it's possible that old DAC codes will be generated on the output and the output samples will
|
||||||
|
/// be delayed by 1 batch.
|
||||||
|
///
|
||||||
|
/// Because the ADC and DAC operate at the same rate, these two constraints actually implement
|
||||||
|
/// the same time bounds, meeting one also means the other is also met.
|
||||||
|
#[task(binds=DMA1_STR4, resources=[adcs, digital_inputs, dacs, iir_state, settings, signal_generator, telemetry, generator], priority=2)]
|
||||||
|
#[inline(never)]
|
||||||
|
#[link_section = ".itcm.process"]
|
||||||
|
fn process(mut c: process::Context) {
|
||||||
|
let process::Resources {
|
||||||
|
adcs: (ref mut adc0, ref mut adc1),
|
||||||
|
dacs: (ref mut dac0, ref mut dac1),
|
||||||
|
ref digital_inputs,
|
||||||
|
ref settings,
|
||||||
|
ref mut iir_state,
|
||||||
|
ref mut telemetry,
|
||||||
|
ref mut generator,
|
||||||
|
ref mut signal_generator,
|
||||||
|
} = c.resources;
|
||||||
|
|
||||||
|
let digital_inputs = [
|
||||||
|
digital_inputs.0.is_high().unwrap(),
|
||||||
|
digital_inputs.1.is_high().unwrap(),
|
||||||
|
];
|
||||||
|
telemetry.digital_inputs = digital_inputs;
|
||||||
|
|
||||||
|
let _hold =
|
||||||
|
settings.force_hold || (digital_inputs[1] && settings.allow_hold);
|
||||||
|
|
||||||
|
(adc0, adc1, dac0, dac1).lock(|adc0, adc1, dac0, dac1| {
|
||||||
|
let adc_samples = [adc0, adc1];
|
||||||
|
let dac_samples = [dac0, dac1];
|
||||||
|
|
||||||
|
// Preserve instruction and data ordering w.r.t. DMA flag access.
|
||||||
|
fence(Ordering::SeqCst);
|
||||||
|
|
||||||
|
for channel in 0..adc_samples.len() {
|
||||||
|
adc_samples[channel]
|
||||||
|
.iter()
|
||||||
|
.zip(dac_samples[channel].iter_mut())
|
||||||
|
.zip(&mut signal_generator[channel])
|
||||||
|
.map(|((ai, di), signal)| {
|
||||||
|
let _x = f32::from(*ai as i16);
|
||||||
|
let _iir_placeholder = & iir_state ;
|
||||||
|
let y = 0.0 as f32;
|
||||||
|
|
||||||
|
/*
|
||||||
|
let y = settings.iir_ch[channel]
|
||||||
|
.iter()
|
||||||
|
.zip(iir_state[channel].iter_mut())
|
||||||
|
.fold(x, |yi, (ch, state)| {
|
||||||
|
ch.update(state, yi, hold)
|
||||||
|
});
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Note(unsafe): The filter limits must ensure that the value is in range.
|
||||||
|
// The truncation introduces 1/2 LSB distortion.
|
||||||
|
let y: i16 = unsafe { y.to_int_unchecked() };
|
||||||
|
|
||||||
|
let y = y.saturating_add(signal);
|
||||||
|
|
||||||
|
// Convert to DAC code
|
||||||
|
*di = DacCode::from(y).0;
|
||||||
|
})
|
||||||
|
.last();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stream the data.
|
||||||
|
const N: usize = BATCH_SIZE * core::mem::size_of::<u16>();
|
||||||
|
generator.add::<_, { N * 4 }>(|buf| {
|
||||||
|
for (data, buf) in adc_samples
|
||||||
|
.iter()
|
||||||
|
.chain(dac_samples.iter())
|
||||||
|
.zip(buf.chunks_exact_mut(N))
|
||||||
|
{
|
||||||
|
let data = unsafe {
|
||||||
|
core::slice::from_raw_parts(
|
||||||
|
data.as_ptr() as *const u8,
|
||||||
|
N,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
buf.copy_from_slice(data)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update telemetry measurements.
|
||||||
|
telemetry.adcs =
|
||||||
|
[AdcCode(adc_samples[0][0]), AdcCode(adc_samples[1][0])];
|
||||||
|
|
||||||
|
telemetry.dacs =
|
||||||
|
[DacCode(dac_samples[0][0]), DacCode(dac_samples[1][0])];
|
||||||
|
|
||||||
|
// Preserve instruction and data ordering w.r.t. DMA flag access.
|
||||||
|
fence(Ordering::SeqCst);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[idle(resources=[network], spawn=[settings_update])]
|
||||||
|
fn idle(mut c: idle::Context) -> ! {
|
||||||
|
loop {
|
||||||
|
match c.resources.network.lock(|net| net.update()) {
|
||||||
|
NetworkState::SettingsChanged => {
|
||||||
|
c.spawn.settings_update().unwrap()
|
||||||
|
}
|
||||||
|
NetworkState::Updated => {}
|
||||||
|
NetworkState::NoChange => cortex_m::asm::wfi(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[task(priority = 1, resources=[network, afes, settings, signal_generator])]
|
||||||
|
fn settings_update(mut c: settings_update::Context) {
|
||||||
|
// Update the IIR channels.
|
||||||
|
let settings = c.resources.network.miniconf.settings();
|
||||||
|
c.resources.settings.lock(|current| *current = *settings);
|
||||||
|
|
||||||
|
// Update AFEs
|
||||||
|
c.resources.afes.0.set_gain(settings.afe[0]);
|
||||||
|
c.resources.afes.1.set_gain(settings.afe[1]);
|
||||||
|
|
||||||
|
// Update the signal generators
|
||||||
|
for (i, &config) in settings.signal_generator.iter().enumerate() {
|
||||||
|
match config.try_into_config(SAMPLE_TICKS_LOG2) {
|
||||||
|
Ok(config) => {
|
||||||
|
c.resources
|
||||||
|
.signal_generator
|
||||||
|
.lock(|generator| generator[i].update_waveform(config));
|
||||||
|
}
|
||||||
|
Err(err) => log::error!(
|
||||||
|
"Failed to update signal generation on DAC{}: {:?}",
|
||||||
|
i,
|
||||||
|
err
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let target = settings.stream_target.into();
|
||||||
|
c.resources.network.direct_stream(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[task(priority = 1, resources=[network, settings, telemetry], schedule=[telemetry])]
|
||||||
|
fn telemetry(mut c: telemetry::Context) {
|
||||||
|
let telemetry: TelemetryBuffer =
|
||||||
|
c.resources.telemetry.lock(|telemetry| *telemetry);
|
||||||
|
|
||||||
|
let (gains, telemetry_period) = c
|
||||||
|
.resources
|
||||||
|
.settings
|
||||||
|
.lock(|settings| (settings.afe, settings.telemetry_period));
|
||||||
|
|
||||||
|
c.resources
|
||||||
|
.network
|
||||||
|
.telemetry
|
||||||
|
.publish(&telemetry.finalize(gains[0], gains[1]));
|
||||||
|
|
||||||
|
// Schedule the telemetry task in the future.
|
||||||
|
c.schedule
|
||||||
|
.telemetry(
|
||||||
|
c.scheduled
|
||||||
|
+ SystemTimer::ticks_from_secs(telemetry_period as u32),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[task(priority = 1, resources=[network], schedule=[ethernet_link])]
|
||||||
|
fn ethernet_link(c: ethernet_link::Context) {
|
||||||
|
c.resources.network.processor.handle_link();
|
||||||
|
c.schedule
|
||||||
|
.ethernet_link(c.scheduled + SystemTimer::ticks_from_secs(1))
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[task(binds = ETH, priority = 1)]
|
||||||
|
fn eth(_: eth::Context) {
|
||||||
|
unsafe { hal::ethernet::interrupt_handler() }
|
||||||
|
}
|
||||||
|
|
||||||
|
#[task(binds = SPI2, priority = 3)]
|
||||||
|
fn spi2(_: spi2::Context) {
|
||||||
|
panic!("ADC0 SPI error");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[task(binds = SPI3, priority = 3)]
|
||||||
|
fn spi3(_: spi3::Context) {
|
||||||
|
panic!("ADC1 SPI error");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[task(binds = SPI4, priority = 3)]
|
||||||
|
fn spi4(_: spi4::Context) {
|
||||||
|
panic!("DAC0 SPI error");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[task(binds = SPI5, priority = 3)]
|
||||||
|
fn spi5(_: spi5::Context) {
|
||||||
|
panic!("DAC1 SPI error");
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
// hw interrupt handlers for RTIC to use for scheduling tasks
|
||||||
|
// one per priority
|
||||||
|
fn DCMI();
|
||||||
|
fn JPEG();
|
||||||
|
fn SDMMC();
|
||||||
|
}
|
||||||
|
};
|
|
@ -105,7 +105,7 @@ impl From<AdcCode> for i16 {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<AdcCode> for u16 {
|
impl From<AdcCode> for u16 {
|
||||||
/// Get an ADC-frmatted binary value from the code.
|
/// Get an ADC-formatted binary value from the code.
|
||||||
fn from(code: AdcCode) -> u16 {
|
fn from(code: AdcCode) -> u16 {
|
||||||
code.0
|
code.0
|
||||||
}
|
}
|
||||||
|
|
|
@ -61,7 +61,7 @@ use ad9959::{Channel, DdsConfig, ProfileSerializer};
|
||||||
/// The DDS profile update stream.
|
/// The DDS profile update stream.
|
||||||
pub struct DdsOutput {
|
pub struct DdsOutput {
|
||||||
_qspi: QspiInterface,
|
_qspi: QspiInterface,
|
||||||
io_update_trigger: HighResTimerE,
|
pub io_update_trigger: HighResTimerE,
|
||||||
config: DdsConfig,
|
config: DdsConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -129,7 +129,7 @@ impl DdsOutput {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Trigger the IO_update signal generating timer to asynchronous create the IO_Update pulse.
|
// Trigger the IO_update signal generating timer to asynchronous create the IO_Update pulse.
|
||||||
self.io_update_trigger.trigger();
|
// self.io_update_trigger.trigger();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -326,6 +326,7 @@ impl DataStream {
|
||||||
self.close();
|
self.close();
|
||||||
}
|
}
|
||||||
self.remote = remote;
|
self.remote = remote;
|
||||||
|
log::info!("set stream remote endpoint to {}", self.remote);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Process any data for transmission.
|
/// Process any data for transmission.
|
||||||
|
|
Loading…
Reference in New Issue