diff --git a/src/bin/dual-iir.rs b/src/bin/dual-iir.rs index a698924..8c617cc 100644 --- a/src/bin/dual-iir.rs +++ b/src/bin/dual-iir.rs @@ -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 diff --git a/src/bin/lockin.rs b/src/bin/lockin.rs index b2c6d65..f4a009f 100644 --- a/src/bin/lockin.rs +++ b/src/bin/lockin.rs @@ -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 diff --git a/src/hardware/afe.rs b/src/hardware/afe.rs index 836a3d0..ec633a7 100644 --- a/src/hardware/afe.rs +++ b/src/hardware/afe.rs @@ -21,13 +21,13 @@ pub struct ProgrammableGainAmplifier { } 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, } } } diff --git a/src/hardware/system_timer.rs b/src/hardware/system_timer.rs index 0d243e3..34011a1 100644 --- a/src/hardware/system_timer.rs +++ b/src/hardware/system_timer.rs @@ -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) { 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 } diff --git a/src/net/miniconf_client.rs b/src/net/miniconf_client.rs index e7fec73..cc802c6 100644 --- a/src/net/miniconf_client.rs +++ b/src/net/miniconf_client.rs @@ -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 } diff --git a/src/net/mod.rs b/src/net/mod.rs index 3a5ea66..4b5b0d3 100644 --- a/src/net/mod.rs +++ b/src/net/mod.rs @@ -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 { pub miniconf: MiniconfClient, 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, diff --git a/src/net/network_processor.rs b/src/net/network_processor.rs index d2ec9ab..a64d6e7 100644 --- a/src/net/network_processor.rs +++ b/src/net/network_processor.rs @@ -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(); diff --git a/src/net/shared.rs b/src/net/shared.rs index e0be260..488f4d6 100644 --- a/src/net/shared.rs +++ b/src/net/shared.rs @@ -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, +} + +/// A basic proxy that references a shared network stack. pub struct NetworkStackProxy<'a, S> { mutex: &'a AtomicCheckMutex, } 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>(&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, -} - 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 } } diff --git a/src/net/telemetry.rs b/src/net/telemetry.rs index 37c705b..b727e23 100644 --- a/src/net/telemetry.rs +++ b/src/net/telemetry.rs @@ -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 { + mqtt: minimq::MqttClient, + telemetry_topic: String, + _telemetry: core::marker::PhantomData, +} + +/// 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 { - mqtt: minimq::MqttClient, - telemetry_topic: String, - _telemetry: core::marker::PhantomData, -} - impl TelemetryClient { + /// 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 TelemetryClient { } } + /// 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 = serde_json_core::to_vec(telemetry).unwrap(); @@ -87,6 +150,12 @@ impl TelemetryClient { .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(