add pdh-test

master
kai 2021-08-18 17:41:01 +08:00
parent 323ed54989
commit 6154d953ba
18 changed files with 1298 additions and 16 deletions

View File

@ -83,6 +83,7 @@ git = "https://github.com/quartiq/smoltcp-nal.git"
rev = "0634188"
[features]
default = ["pounder_v1_1"]
nightly = ["cortex-m/inline-asm", "dsp/nightly"]
pounder_v1_1 = [ ]

View File

@ -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)

3
miniconf-mqtt/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
__pycache__/
*.egg
*.egg-info/

9
miniconf-mqtt/README.md Normal file
View File

@ -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.

View File

@ -0,0 +1,4 @@
#!/usr/bin/python3
""" Root Miniconf module file. """
from .miniconf import Miniconf
from .version import __version__

View File

@ -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()

View File

@ -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'])

View File

@ -0,0 +1,5 @@
#!/usr/bin/python3
""" Miniconf version file. """
# The semantic version of Miniconf.
__version__ = '0.1.0'

22
miniconf-mqtt/setup.py Normal file
View File

@ -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'
],
)

28
nix/mqtt-explorer.nix Normal file
View File

@ -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;
};
}

4
nix/rust-toolchain Normal file
View File

@ -0,0 +1,4 @@
[toolchain]
channel = "stable"
components = [ "rust-src" , "llvm-tools-preview" ]
targets = [ "x86_64-unknown-linux-gnu", "thumbv7em-none-eabihf" ]

View File

@ -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()

View File

@ -9,7 +9,94 @@ import logging
import sys
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:
""" A basic timer for measuring elapsed time periods. """
@ -62,7 +149,7 @@ def sequence_delta(previous_sequence, next_sequence):
def main():
""" Main program. """
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')
args = parser.parse_args()
@ -82,7 +169,7 @@ def main():
while True:
# 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():
timer.start()
@ -91,6 +178,9 @@ def main():
last_index = seqnum
good_blocks += 1
if 1000 < good_blocks and good_blocks < 1020 :
print(_data[:8])
# Report the throughput periodically.
if timer.is_triggered():
drate = stream.get_rx_bytes() * 8 / 1e6 / timer.elapsed()

91
shell.nix Normal file
View File

@ -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"
'';
}

556
src/bin/pdh-test.rs Normal file
View File

@ -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();
}
};

View File

@ -105,7 +105,7 @@ impl From<AdcCode> for i16 {
}
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 {
code.0
}

View File

@ -61,7 +61,7 @@ use ad9959::{Channel, DdsConfig, ProfileSerializer};
/// The DDS profile update stream.
pub struct DdsOutput {
_qspi: QspiInterface,
io_update_trigger: HighResTimerE,
pub io_update_trigger: HighResTimerE,
config: DdsConfig,
}
@ -129,7 +129,7 @@ impl DdsOutput {
}
// Trigger the IO_update signal generating timer to asynchronous create the IO_Update pulse.
self.io_update_trigger.trigger();
// self.io_update_trigger.trigger();
}
}

View File

@ -326,6 +326,7 @@ impl DataStream {
self.close();
}
self.remote = remote;
log::info!("set stream remote endpoint to {}", self.remote);
}
/// Process any data for transmission.