diff --git a/Cargo.toml b/Cargo.toml index 8cd1ce9..1aa76ae 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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 = [ ] diff --git a/README.md b/README.md index 75249db..1f3230d 100644 --- a/README.md +++ b/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) diff --git a/miniconf-mqtt/.gitignore b/miniconf-mqtt/.gitignore new file mode 100644 index 0000000..ab7ceb0 --- /dev/null +++ b/miniconf-mqtt/.gitignore @@ -0,0 +1,3 @@ +__pycache__/ +*.egg +*.egg-info/ diff --git a/miniconf-mqtt/README.md b/miniconf-mqtt/README.md new file mode 100644 index 0000000..bfbb627 --- /dev/null +++ b/miniconf-mqtt/README.md @@ -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. diff --git a/miniconf-mqtt/miniconf/__init__.py b/miniconf-mqtt/miniconf/__init__.py new file mode 100644 index 0000000..143dff4 --- /dev/null +++ b/miniconf-mqtt/miniconf/__init__.py @@ -0,0 +1,4 @@ +#!/usr/bin/python3 +""" Root Miniconf module file. """ +from .miniconf import Miniconf +from .version import __version__ diff --git a/miniconf-mqtt/miniconf/__main__.py b/miniconf-mqtt/miniconf/__main__.py new file mode 100644 index 0000000..a428fe0 --- /dev/null +++ b/miniconf-mqtt/miniconf/__main__.py @@ -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() diff --git a/miniconf-mqtt/miniconf/miniconf.py b/miniconf-mqtt/miniconf/miniconf.py new file mode 100644 index 0000000..82b6550 --- /dev/null +++ b/miniconf-mqtt/miniconf/miniconf.py @@ -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']) diff --git a/miniconf-mqtt/miniconf/version.py b/miniconf-mqtt/miniconf/version.py new file mode 100644 index 0000000..2598984 --- /dev/null +++ b/miniconf-mqtt/miniconf/version.py @@ -0,0 +1,5 @@ +#!/usr/bin/python3 +""" Miniconf version file. """ + +# The semantic version of Miniconf. +__version__ = '0.1.0' diff --git a/miniconf-mqtt/setup.py b/miniconf-mqtt/setup.py new file mode 100644 index 0000000..cf10921 --- /dev/null +++ b/miniconf-mqtt/setup.py @@ -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' + ], +) diff --git a/nix/mqtt-explorer.nix b/nix/mqtt-explorer.nix new file mode 100644 index 0000000..0e0a48f --- /dev/null +++ b/nix/mqtt-explorer.nix @@ -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; + }; +} \ No newline at end of file diff --git a/nix/rust-toolchain b/nix/rust-toolchain new file mode 100644 index 0000000..05cf75e --- /dev/null +++ b/nix/rust-toolchain @@ -0,0 +1,4 @@ +[toolchain] +channel = "stable" +components = [ "rust-src" , "llvm-tools-preview" ] +targets = [ "x86_64-unknown-linux-gnu", "thumbv7em-none-eabihf" ] \ No newline at end of file diff --git a/scripts/stabilizer_adc_stream_test.py b/scripts/stabilizer_adc_stream_test.py new file mode 100644 index 0000000..5625e3f --- /dev/null +++ b/scripts/stabilizer_adc_stream_test.py @@ -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 = '= 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() diff --git a/scripts/stream_throughput.py b/scripts/stream_throughput.py index 1abe500..4d66ac3 100644 --- a/scripts/stream_throughput.py +++ b/scripts/stream_throughput.py @@ -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 = ' { 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" + ''; + + } + diff --git a/src/bin/pdh-test.rs b/src/bin/pdh-test.rs new file mode 100644 index 0000000..a4aa66d --- /dev/null +++ b/src/bin/pdh-test.rs @@ -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/` + /// + /// * specifies which channel to configure. := [0, 1] + /// + /// # Value + /// Any of the variants of [Gain] enclosed in double quotes. + afe: [Gain; 2], + + /// Configure the IIR filter parameters. + /// + /// # Path + /// `iir_ch//` + /// + /// * specifies which channel to configure. := [0, 1] + /// * specifies which cascade to configure. := [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/` + /// + /// * specifies which channel to configure. := [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, + 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::(); + 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(); + } +}; diff --git a/src/hardware/adc.rs b/src/hardware/adc.rs index 4da4100..7f62ba4 100644 --- a/src/hardware/adc.rs +++ b/src/hardware/adc.rs @@ -105,7 +105,7 @@ impl From for i16 { } impl From 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 } diff --git a/src/hardware/pounder/dds_output.rs b/src/hardware/pounder/dds_output.rs index b67837e..d2d7476 100644 --- a/src/hardware/pounder/dds_output.rs +++ b/src/hardware/pounder/dds_output.rs @@ -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(); } } diff --git a/src/net/data_stream.rs b/src/net/data_stream.rs index dbedac0..d83a525 100644 --- a/src/net/data_stream.rs +++ b/src/net/data_stream.rs @@ -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.