Adding documentation

master
Ryan Summers 2021-05-06 12:33:07 +02:00
parent e9c3e24863
commit 4cbc826f00
9 changed files with 251 additions and 42 deletions

View File

@ -153,7 +153,6 @@ const APP: () = {
}
// Update telemetry measurements.
// TODO: Should we report these as voltages?
c.resources.telemetry.latest_samples =
[adc_samples[0][0] as i16, adc_samples[1][0] as i16];
@ -197,7 +196,7 @@ const APP: () = {
c.resources
.network
.telemetry
.publish(&telemetry.to_telemetry(gains[0], gains[1]));
.publish(&telemetry.finalize(gains[0], gains[1]));
let telemetry_period = c
.resources

View File

@ -243,7 +243,7 @@ const APP: () = {
c.resources
.network
.telemetry
.publish(&telemetry.to_telemetry(gains[0], gains[1]));
.publish(&telemetry.finalize(gains[0], gains[1]));
let telemetry_period = c
.resources

View File

@ -21,13 +21,13 @@ pub struct ProgrammableGainAmplifier<A0, A1> {
}
impl Gain {
/// Get the AFE gain as a multiplying integer.
pub fn to_multiplier(&self) -> u8 {
/// Get the AFE gain as a numerical value.
pub fn as_multiplier (self) -> f32 {
match self {
Gain::G1 => 1,
Gain::G2 => 2,
Gain::G5 => 5,
Gain::G10 => 10,
Gain::G1 => 1.0,
Gain::G2 => 2.0,
Gain::G5 => 5.0,
Gain::G10 => 10.0,
}
}
}

View File

@ -1,11 +1,33 @@
///! System timer used for RTIC scheduling
///!
///! # Design
///! The SystemTimer is an RTIC monotonic timer that can be used for scheduling tasks in RTIC.
///! This timer is used in place of the cycle counter to allow the timer to tick at a slower rate
///! than the CPU clock. This allows for longer scheduling periods with less resolution. This is
///! needed for infrequent (e.g. multiple second) telemetry periods.
///!
///! # Limitations
///! This implementation relies on sufficient timer polling to not miss timer counter overflows. If
///! the timer is not polled often enough, it's possible that an overflow would be missed and time
///! would "halt" for a shore period of time. This could be fixed in the future by instead
///! listening for the overflow interrupt instead of polling the overflow state.
use hal::prelude::*;
use stm32h7xx_hal as hal;
// A global buffer indicating how many times the internal counter has overflowed.
static mut OVERFLOWS: u32 = 0;
/// System timer used for implementing RTIC scheduling.
///
/// # Note
/// The system timer must be initialized before being used.
pub struct SystemTimer {}
impl SystemTimer {
/// Initialize the system timer.
///
/// # Args
/// * `timer` - The hardware timer used for implementing the RTIC monotonic.
pub fn initialize(mut timer: hal::timer::Timer<hal::device::TIM15>) {
timer.pause();
// Have the system timer operate at a tick rate of 10KHz (100uS per tick). With this
@ -16,52 +38,73 @@ impl SystemTimer {
timer.resume();
}
/// Convert a provided number of seconds into timer ticks.
pub fn ticks_from_secs(secs: u32) -> i32 {
(secs * 10_000) as i32
}
}
impl rtic::Monotonic for SystemTimer {
// Instants are stored in 32-bit signed integers. With a 10KHz tick rate, this means an
// instant can store up to ~59 hours of time before overflowing.
type Instant = i32;
fn ratio() -> rtic::Fraction {
rtic::Fraction {
// At 10KHz with a 400MHz CPU clock, the CPU clock runs 40,000 times faster than
// the system timer.
numerator: 40_000,
denominator: 1,
}
}
/// Get the current time instant.
///
/// # Note
/// The time will overflow into -59 hours after the first 59 hours. This time value is intended
/// for use in calculating time delta, and should not be used for timestamping purposes due to
/// roll-over.
fn now() -> i32 {
// Note(unsafe): Multiple interrupt contexts have access to the underlying timer, so care
// is taken when reading and modifying register values.
let regs = unsafe { &*hal::device::TIM15::ptr() };
loop {
// Check for overflows
if regs.sr.read().uif().bit_is_set() {
regs.sr.modify(|_, w| w.uif().clear_bit());
unsafe {
OVERFLOWS += 1;
// Checking for overflows of the current counter must be performed atomically. Any
// other task that is accessing the current time could potentially race for the
// registers. Note that this is only required for writing to global state (e.g. timer
// registers and overflow counter)
cortex_m::interrupt::free(|_cs| {
// Check for overflows and clear the overflow bit atomically. This must be done in
// a critical section to prevent race conditions on the status register.
if regs.sr.read().uif().bit_is_set() {
regs.sr.modify(|_, w| w.uif().clear_bit());
unsafe {
OVERFLOWS += 1;
}
}
}
let current_value = regs.cnt.read().bits();
let current_value = regs.cnt.read().bits();
// If the overflow is still unset, return our latest count, as it indicates we weren't
// pre-empted.
if regs.sr.read().uif().bit_is_clear() {
unsafe {
return (OVERFLOWS * 65535 + current_value) as i32;
// Check that an overflow didn't occur since we just cleared the overflow bit. If
// it did, loop around and retry.
if regs.sr.read().uif().bit_is_clear() {
return (overflows * 65535 + current_value) as i32;
}
}
}
}
/// Reset the timer count.
unsafe fn reset() {
// Note: The timer must be safely configured in `SystemTimer::initialize()`.
let regs = &*hal::device::TIM15::ptr();
OVERFLOWS = 0;
regs.cnt.reset();
}
/// Get a timestamp correlating to zero time.
fn zero() -> i32 {
0
}

View File

@ -1,7 +1,18 @@
use crate::hardware::design_parameters::MQTT_BROKER;
///! Stabilizer Run-time Settings Client
///!
///! # Design
///! Stabilizer allows for settings to be configured at run-time via MQTT using miniconf.
///! Settings are written in serialized JSON form to the settings path associated with the setting.
///!
///! # Limitations
///! The MQTT client logs failures to subscribe to the settings topic, but does not re-attempt to
///connect to it when errors occur.
///!
///! Respones to settings updates are sent without quality-of-service guarantees, so there's no
///! guarantee that the requestee will be informed that settings have been applied.
use heapless::{consts, String};
use crate::hardware::design_parameters::MQTT_BROKER;
use super::{MqttMessage, NetworkReference, SettingsResponse, UpdateState};
/// MQTT settings interface.
@ -144,6 +155,7 @@ where
}
}
/// Get the current settings from miniconf.
pub fn settings(&self) -> &S {
&self.settings
}

View File

@ -1,4 +1,3 @@
use core::fmt::Write;
///! Stabilizer network management module
///!
///! # Design
@ -10,6 +9,8 @@ use heapless::{consts, String};
use miniconf::Miniconf;
use serde::Serialize;
use core::fmt::Write;
mod messages;
mod miniconf_client;
mod shared;
@ -32,6 +33,7 @@ pub enum UpdateState {
Updated,
}
/// A structure of Stabilizer's default network users.
pub struct NetworkUsers<S: Default + Clone + Miniconf, T: Serialize> {
pub miniconf: MiniconfClient<S>,
pub processor: NetworkProcessor,
@ -43,6 +45,17 @@ where
S: Default + Clone + Miniconf,
T: Serialize,
{
/// Construct Stabilizer's default network users.
///
/// # Args
/// * `stack` - The network stack that will be used to share with all network users.
/// * `phy` - The ethernet PHY connecting the network.
/// * `cycle_counter` - The clock used for measuring time in the network.
/// * `app` - The name of the application.
/// * `mac` - The MAC address of the network.
///
/// # Returns
/// A new struct of network users.
pub fn new(
stack: NetworkStack,
phy: EthernetPhy,
@ -81,6 +94,10 @@ where
}
}
/// Update and process all of the network users state.
///
/// # Returns
/// An indication if any of the network users indicated a state change.
pub fn update(&mut self) -> UpdateState {
// Poll for incoming data.
let poll_result = self.processor.update();
@ -95,6 +112,15 @@ where
}
}
/// Get an MQTT client ID for a client.
///
/// # Args
/// * `app` - The name of the application
/// * `client` - The unique tag of the client
/// * `mac` - The MAC address of the device.
///
/// # Returns
/// A client ID that may be used for MQTT client identification.
fn get_client_id(
app: &str,
client: &str,

View File

@ -1,7 +1,12 @@
///! Task to process network hardware.
///!
///! # Design
///! The network processir is a small taks to regularly process incoming data over ethernet, handle
///! the ethernet PHY state, and reset the network as appropriate.
use super::{NetworkReference, UpdateState};
use crate::hardware::{CycleCounter, EthernetPhy};
/// Processor for managing network hardware.
pub struct NetworkProcessor {
stack: NetworkReference,
phy: EthernetPhy,
@ -10,6 +15,15 @@ pub struct NetworkProcessor {
}
impl NetworkProcessor {
/// Construct a new network processor.
///
/// # Args
/// * `stack` - A reference to the shared network stack
/// * `phy` - The ethernet PHY used for the network.
/// * `clock` - The clock used for providing time to the network.
///
/// # Returns
/// The newly constructed processor.
pub fn new(
stack: NetworkReference,
phy: EthernetPhy,
@ -23,6 +37,14 @@ impl NetworkProcessor {
}
}
/// Process and update the state of the network.
///
/// # Note
/// This function should be called regularly before other network tasks to update the state of
/// all relevant network sockets.
///
/// # Returns
/// An update state corresponding with any changes in the underlying network.
pub fn update(&mut self) -> UpdateState {
// Service the network stack to process any inbound and outbound traffic.
let now = self.clock.current_ms();

View File

@ -1,18 +1,50 @@
///! Network Stack Sharing Utilities
///!
///! # Design
///! This module provides a mechanism for sharing a single network stack safely between drivers
///that may or may not execute in multiple contexts. The design copies that of `shared-bus`.
///!
///! Specifically, the network stack is stored in a global static singleton and proxies to the
///! underlying stack are handed out. The proxies provide an identical API for the
///! `embedded_nal::TcpStack` stack trait, so they can be provided direclty to drivers that require
///! a network stack.
///!
///! In order to ensure that pre-emption does not occur while accessing the same network stack from
///! multiple interrupt contexts, the proxy uses an atomic boolean check - if the flag indicates the
///! stack is in use, the proxy will generate a panic. The actual synchronization mechanism (mutex)
///! leverages RTIC resource allocation. All devices that use the underlying network stack must be
///! placed in a single RTIC resource, which will cause RTIC to prevent contention for the
///! underlying network stack.
use minimq::embedded_nal;
use shared_bus::{AtomicCheckMutex, BusMutex};
use crate::hardware::NetworkStack;
/// A manager for a shared network stack.
pub struct NetworkManager {
mutex: AtomicCheckMutex<NetworkStack>,
}
/// A basic proxy that references a shared network stack.
pub struct NetworkStackProxy<'a, S> {
mutex: &'a AtomicCheckMutex<S>,
}
impl<'a, S> NetworkStackProxy<'a, S> {
/// Using the proxy, access the underlying network stack directly.
///
/// # Args
/// * `f` - A closure which will be provided the network stack as an argument.
///
/// # Returns
/// Any value returned by the provided closure
pub fn lock<R, F: FnOnce(&mut S) -> R>(&mut self, f: F) -> R {
self.mutex.lock(|stack| f(stack))
}
}
// A simple forwarding macro taken from the `embedded-nal` to forward the embedded-nal API into the
// proxy structure.
macro_rules! forward {
($func:ident($($v:ident: $IT:ty),*) -> $T:ty) => {
fn $func(&self, $($v: $IT),*) -> $T {
@ -21,6 +53,7 @@ macro_rules! forward {
}
}
// Implement a TCP stack for the proxy if the underlying network stack implements it.
impl<'a, S> embedded_nal::TcpStack for NetworkStackProxy<'a, S>
where
S: embedded_nal::TcpStack,
@ -36,17 +69,22 @@ where
forward! {close(socket: S::TcpSocket) -> Result<(), S::Error>}
}
pub struct NetworkManager {
mutex: AtomicCheckMutex<NetworkStack>,
}
impl NetworkManager {
/// Construct a new manager for a shared network stack
///
/// # Args
/// * `stack` - The network stack that is being shared.
pub fn new(stack: NetworkStack) -> Self {
Self {
mutex: AtomicCheckMutex::create(stack),
}
}
/// Acquire a proxy to the shared network stack.
///
/// # Returns
/// A proxy that can be used in place of the network stack. Note the requirements of
/// concurrency listed in the description of this file for usage.
pub fn acquire_stack<'a>(&'a self) -> NetworkStackProxy<'a, NetworkStack> {
NetworkStackProxy { mutex: &self.mutex }
}

View File

@ -1,19 +1,50 @@
///! Stabilizer Telemetry Capabilities
///!
///! # Design
///! Telemetry is reported regularly using an MQTT client. All telemetry is reported in SI units
///! using standard JSON format.
///!
///! In order to report ADC/DAC codes generated during the DSP routines, a telemetry buffer is
///! employed to track the latest codes. Converting these codes to SI units would result in
///! repetitive and unnecessary calculations within the DSP routine, slowing it down and limiting
///! sampling frequency. Instead, the raw codes are stored and the telemetry is generated as
///! required immediately before transmission. This ensures that any slower computation required
///! for unit conversion can be off-loaded to lower priority tasks.
use heapless::{consts, String, Vec};
use serde::Serialize;
use super::NetworkReference;
use crate::hardware::design_parameters::MQTT_BROKER;
use minimq::QoS;
use crate::hardware::AfeGain;
use super::NetworkReference;
use crate::hardware::{AfeGain, design_parameters::MQTT_BROKER};
/// The telemetry client for reporting telemetry data over MQTT.
pub struct TelemetryClient<T: Serialize> {
mqtt: minimq::MqttClient<minimq::consts::U256, NetworkReference>,
telemetry_topic: String<consts::U128>,
_telemetry: core::marker::PhantomData<T>,
}
/// The telemetry buffer is used for storing sample values during execution.
///
/// # Note
/// These values can be converted to SI units immediately before reporting to save processing time.
/// This allows for the DSP process to continually update the values without incurring significant
/// run-time overhead during conversion to SI units.
#[derive(Copy, Clone)]
pub struct TelemetryBuffer {
/// The latest input sample on ADC0/ADC1.
pub latest_samples: [i16; 2],
/// The latest output code on DAC0/DAC1.
pub latest_outputs: [u16; 2],
/// The latest digital input states during processing.
pub digital_inputs: [bool; 2],
}
/// The telemetry structure is data that is ultimately reported as telemetry over MQTT.
///
/// # Note
/// This structure should be generated on-demand by the buffer when required to minimize conversion
/// overhead.
#[derive(Serialize)]
pub struct Telemetry {
input_levels: [f32; 2],
@ -32,16 +63,37 @@ impl Default for TelemetryBuffer {
}
impl TelemetryBuffer {
pub fn to_telemetry(self, afe0: AfeGain, afe1: AfeGain) -> Telemetry {
/// Convert the telemetry buffer to finalized, SI-unit telemetry for reporting.
///
/// # Args
/// * `afe0` - The current AFE configuration for channel 0.
/// * `afe1` - The current AFE configuration for channel 1.
///
/// # Returns
/// The finalized telemetry structure that can be serialized and reported.
pub fn finalize(self, afe0: AfeGain, afe1: AfeGain) -> Telemetry {
// The input voltage is measured by the ADC across a dynamic scale of +/- 4.096 V with a
// dynamic range across signed integers. Additionally, the signal is fully differential, so
// the differential voltage is measured at the ADC. Thus, the single-ended signal is
// measured at the input is half of the ADC-reported measurement. As a pre-filter, the
// input signal has a fixed gain of 1/5 through a static input active filter. Finally, at
// the very front-end of the signal, there's an analog input multiplier that is
// configurable by the user.
let in0_volts =
(self.latest_samples[0] as f32 / i16::MAX as f32) * 4.096 / 2.0
* 5.0
/ afe0.to_multiplier() as f32;
/ afe0.as_multiplier();
let in1_volts =
(self.latest_samples[1] as f32 / i16::MAX as f32) * 4.096 / 2.0
* 5.0
/ afe1.to_multiplier() as f32;
/ afe1.as_multiplier();
// The output voltage is generated by the DAC with an output range of +/- 4.096 V. This
// signal then passes through a 2.5x gain stage. Note that the DAC operates using unsigned
// integers, and u16::MAX / 2 is considered zero voltage output. Thus, the dynamic range of
// the output stage is +/- 10.24 V. At a DAC code of zero, there is an output of -10.24 V,
// and at a max DAC code, there is an output of 10.24 V.
let out0_volts = (10.24 * 2.0)
* (self.latest_outputs[0] as f32 / (u16::MAX as f32))
- 10.24;
@ -57,13 +109,16 @@ impl TelemetryBuffer {
}
}
pub struct TelemetryClient<T: Serialize> {
mqtt: minimq::MqttClient<minimq::consts::U256, NetworkReference>,
telemetry_topic: String<consts::U128>,
_telemetry: core::marker::PhantomData<T>,
}
impl<T: Serialize> TelemetryClient<T> {
/// Construct a new telemetry client.
///
/// # Args
/// * `stack` - A reference to the (shared) underlying network stack.
/// * `client_id` - The MQTT client ID of the telemetry client.
/// * `prefix` - The device prefix to use for MQTT telemetry reporting.
///
/// # Returns
/// A new telemetry client.
pub fn new(stack: NetworkReference, client_id: &str, prefix: &str) -> Self {
let mqtt =
minimq::MqttClient::new(MQTT_BROKER.into(), client_id, stack)
@ -79,6 +134,14 @@ impl<T: Serialize> TelemetryClient<T> {
}
}
/// Publish telemetry over MQTT
///
/// # Note
/// Telemetry is reported in a "best-effort" fashion. Failure to transmit telemetry will cause
/// it to be silently dropped.
///
/// # Args
/// * `telemetry` - The telemetry to report
pub fn publish(&mut self, telemetry: &T) {
let telemetry: Vec<u8, consts::U256> =
serde_json_core::to_vec(telemetry).unwrap();
@ -87,6 +150,12 @@ impl<T: Serialize> TelemetryClient<T> {
.ok();
}
/// Update the telemetry client
///
/// # Note
/// This function is provided to force the underlying MQTT state machine to process incoming
/// and outgoing messages. Without this, the client will never connect to the broker. This
/// should be called regularly.
pub fn update(&mut self) {
match self.mqtt.poll(|_client, _topic, _message, _properties| {}) {
Err(minimq::Error::Network(