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"
|
||||
|
||||
[features]
|
||||
default = ["pounder_v1_1"]
|
||||
nightly = ["cortex-m/inline-asm", "dsp/nightly"]
|
||||
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)
|
||||
|
3
miniconf-mqtt/.gitignore
vendored
Normal file
3
miniconf-mqtt/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
__pycache__/
|
||||
*.egg
|
||||
*.egg-info/
|
9
miniconf-mqtt/README.md
Normal file
9
miniconf-mqtt/README.md
Normal 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.
|
4
miniconf-mqtt/miniconf/__init__.py
Normal file
4
miniconf-mqtt/miniconf/__init__.py
Normal file
@ -0,0 +1,4 @@
|
||||
#!/usr/bin/python3
|
||||
""" Root Miniconf module file. """
|
||||
from .miniconf import Miniconf
|
||||
from .version import __version__
|
56
miniconf-mqtt/miniconf/__main__.py
Normal file
56
miniconf-mqtt/miniconf/__main__.py
Normal 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()
|
98
miniconf-mqtt/miniconf/miniconf.py
Normal file
98
miniconf-mqtt/miniconf/miniconf.py
Normal 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'])
|
5
miniconf-mqtt/miniconf/version.py
Normal file
5
miniconf-mqtt/miniconf/version.py
Normal 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
22
miniconf-mqtt/setup.py
Normal 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
28
nix/mqtt-explorer.nix
Normal 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
4
nix/rust-toolchain
Normal file
@ -0,0 +1,4 @@
|
||||
[toolchain]
|
||||
channel = "stable"
|
||||
components = [ "rust-src" , "llvm-tools-preview" ]
|
||||
targets = [ "x86_64-unknown-linux-gnu", "thumbv7em-none-eabihf" ]
|
284
scripts/stabilizer_adc_stream_test.py
Normal file
284
scripts/stabilizer_adc_stream_test.py
Normal 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()
|
@ -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
91
shell.nix
Normal 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
556
src/bin/pdh-test.rs
Normal 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();
|
||||
}
|
||||
};
|
@ -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
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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.
|
||||
|
Loading…
Reference in New Issue
Block a user