Merge branch 'master' into rj/visibility-cleanup

* master: (34 commits)
  miniconf: update example usage
  apps: spi isrs are spi errors
  pounder/timestamp: docs updatew
  pll: update tests and benches
  pll: merge advance into update (like rpll)
  pll: add advance()
  pounder_timestamper: use input capture prescaler
  Revert "Revert "pounder timestmper: don't use DMA""
  miniconf: add some checks, simplify
  miniconf.py: make retain an option
  refactor flatten_closures
  clippy recursion
  fix a few clippy lints on files that are touched
  Revert "pounder timestmper: don't use DMA"
  pounder timestmper: don't use DMA
  lockin: dma fence
  lockin: port to fast double buffered DMA
  dma: implement overflow checking
  pounder: clippy
  pounder: add comment on channel enum
  ...
master
Robert Jördens 2021-06-04 11:59:54 +02:00
commit 50ea2f360c
23 changed files with 406 additions and 443 deletions

8
Cargo.lock generated
View File

@ -404,8 +404,9 @@ dependencies = [
[[package]] [[package]]
name = "mcp23017" name = "mcp23017"
version = "0.1.1" version = "1.0.0"
source = "git+https://github.com/lucazulian/mcp23017.git?rev=523d71d#523d71dcb11fc0ea4bd9385ef2527ae7a7eee987" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c32fd6627e73f1cfa95c00ddcdcb5a6a6ddbd10b308d08588a502c018b6e12c"
dependencies = [ dependencies = [
"embedded-hal", "embedded-hal",
] ]
@ -775,8 +776,7 @@ dependencies = [
[[package]] [[package]]
name = "stm32h7xx-hal" name = "stm32h7xx-hal"
version = "0.9.0" version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "git+https://github.com/quartiq/stm32h7xx-hal.git?rev=b0b8a93#b0b8a930b2c3bc5fcebc2e905b4c5e13360111a5"
checksum = "67034b80041bc33a48df1c1c435b6ae3bb18c35e42aa7e702ce8363b96793398"
dependencies = [ dependencies = [
"bare-metal 1.0.0", "bare-metal 1.0.0",
"cast", "cast",

View File

@ -45,20 +45,19 @@ ad9959 = { path = "ad9959" }
miniconf = "0.1.0" miniconf = "0.1.0"
shared-bus = {version = "0.2.2", features = ["cortex-m"] } shared-bus = {version = "0.2.2", features = ["cortex-m"] }
serde-json-core = "0.4" serde-json-core = "0.4"
mcp23017 = "1.0"
# rtt-target bump # rtt-target bump
[dependencies.rtt-logger] [dependencies.rtt-logger]
git = "https://github.com/quartiq/rtt-logger.git" git = "https://github.com/quartiq/rtt-logger.git"
rev = "70b0eb5" rev = "70b0eb5"
# rewrite # fast double buffered DMA without poisoning and buffer swapping
[dependencies.mcp23017]
git = "https://github.com/lucazulian/mcp23017.git"
rev = "523d71d"
[dependencies.stm32h7xx-hal] [dependencies.stm32h7xx-hal]
features = ["stm32h743v", "rt", "unproven", "ethernet", "quadspi"] features = ["stm32h743v", "rt", "unproven", "ethernet", "quadspi"]
version = "0.9.0" # version = "0.9.0"
git = "https://github.com/quartiq/stm32h7xx-hal.git"
rev = "b0b8a93"
# link.x section start/end # link.x section start/end
[patch.crates-io.cortex-m-rt] [patch.crates-io.cortex-m-rt]

View File

@ -43,6 +43,7 @@ pub enum Mode {
/// The configuration registers within the AD9959 DDS device. The values of each register are /// The configuration registers within the AD9959 DDS device. The values of each register are
/// equivalent to the address. /// equivalent to the address.
#[allow(clippy::upper_case_acronyms)]
pub enum Register { pub enum Register {
CSR = 0x00, CSR = 0x00,
FR1 = 0x01, FR1 = 0x01,
@ -596,6 +597,22 @@ impl ProfileSerializer {
self.index += value.len() + 1; self.index += value.len() + 1;
} }
fn pad(&mut self) {
// Pad the buffer to 32-bit (4 byte) alignment by adding dummy writes to CSR and LSRR.
match self.index & 3 {
3 => {
// For a level of 3, we have to pad with 5 bytes to align things.
self.add_write(Register::CSR, &[(self.mode as u8) << 1]);
self.add_write(Register::LSRR, &[0, 0]);
}
2 => self.add_write(Register::CSR, &[(self.mode as u8) << 1]),
1 => self.add_write(Register::LSRR, &[0, 0]),
0 => {}
_ => unreachable!(),
}
}
/// Get the serialized profile as a slice of 32-bit words. /// Get the serialized profile as a slice of 32-bit words.
/// ///
/// # Note /// # Note
@ -604,21 +621,8 @@ impl ProfileSerializer {
/// ///
/// # Returns /// # Returns
/// A slice of `u32` words representing the serialized profile. /// A slice of `u32` words representing the serialized profile.
pub fn finalize<'a>(&'a mut self) -> &[u32] { pub fn finalize<'a>(&'a mut self) -> &'a [u32] {
// Pad the buffer to 32-bit alignment by adding dummy writes to CSR and LSRR. self.pad();
let padding = 4 - (self.index % 4);
match padding {
1 => {
// For a pad size of 1, we have to pad with 5 bytes to align things.
self.add_write(Register::CSR, &[(self.mode as u8) << 1]);
self.add_write(Register::LSRR, &[0, 0]);
}
2 => self.add_write(Register::CSR, &[(self.mode as u8) << 1]),
3 => self.add_write(Register::LSRR, &[0, 0]),
4 => {}
_ => unreachable!(),
}
unsafe { unsafe {
core::slice::from_raw_parts::<'a, u32>( core::slice::from_raw_parts::<'a, u32>(
&self.data as *const _ as *const u32, &self.data as *const _ as *const u32,

View File

@ -44,11 +44,11 @@ fn pll_bench() {
let mut dut = PLL::default(); let mut dut = PLL::default();
println!( println!(
"PLL::update(t, 12, 12): {}", "PLL::update(t, 12, 12): {}",
bench_env(0x241, |x| dut.update(*x, 12, 12)) bench_env(Some(0x241), |x| dut.update(*x, 12, 12))
); );
println!( println!(
"PLL::update(t, sf, sp): {}", "PLL::update(t, sf, sp): {}",
bench_env((0x241, 21, 20), |(x, p, q)| dut.update(*x, *p, *q)) bench_env((Some(0x241), 21, 20), |(x, p, q)| dut.update(*x, *p, *q))
); );
} }

View File

@ -77,6 +77,8 @@ pub fn atan2(y: i32, x: i32) -> i32 {
if sign.1 { if sign.1 {
angle = angle.wrapping_neg(); angle = angle.wrapping_neg();
// Negation ends up in slightly faster assembly
// angle = !angle;
} }
angle angle

View File

@ -45,7 +45,7 @@ impl PLL {
/// The signal's phase/frequency is reconstructed relative to the sampling period. /// The signal's phase/frequency is reconstructed relative to the sampling period.
/// ///
/// Args: /// Args:
/// * `x`: New input phase sample. /// * `x`: New input phase sample or None if a sample has been missed.
/// * `shift_frequency`: Frequency error scaling. The frequency gain per update is /// * `shift_frequency`: Frequency error scaling. The frequency gain per update is
/// `1/(1 << shift_frequency)`. /// `1/(1 << shift_frequency)`.
/// * `shift_phase`: Phase error scaling. The phase gain is `1/(1 << shift_phase)` /// * `shift_phase`: Phase error scaling. The phase gain is `1/(1 << shift_phase)`
@ -55,26 +55,31 @@ impl PLL {
/// A tuple of instantaneous phase and frequency (the current phase increment). /// A tuple of instantaneous phase and frequency (the current phase increment).
pub fn update( pub fn update(
&mut self, &mut self,
x: i32, x: Option<i32>,
shift_frequency: u8, shift_frequency: u8,
shift_phase: u8, shift_phase: u8,
) -> (i32, i32) { ) -> (i32, i32) {
debug_assert!((1..=30).contains(&shift_frequency)); debug_assert!((1..=30).contains(&shift_frequency));
debug_assert!((1..=30).contains(&shift_phase)); debug_assert!((1..=30).contains(&shift_phase));
let e = x.wrapping_sub(self.f); let f = if let Some(x) = x {
self.f = self.f.wrapping_add( let e = x.wrapping_sub(self.f);
(1i32 << (shift_frequency - 1)) self.f = self.f.wrapping_add(
.wrapping_add(e) (1i32 << (shift_frequency - 1))
.wrapping_sub(self.x) .wrapping_add(e)
>> shift_frequency, .wrapping_sub(self.x)
); >> shift_frequency,
self.x = x; );
let f = self.f.wrapping_add( self.x = x;
(1i32 << (shift_phase - 1)) self.f.wrapping_add(
.wrapping_add(e) (1i32 << (shift_phase - 1))
.wrapping_sub(self.y) .wrapping_add(e)
>> shift_phase, .wrapping_sub(self.y)
); >> shift_phase,
)
} else {
self.x = self.x.wrapping_add(self.f);
self.f
};
self.y = self.y.wrapping_add(f); self.y = self.y.wrapping_add(f);
(self.y, f) (self.y, f)
} }
@ -86,7 +91,7 @@ mod tests {
#[test] #[test]
fn mini() { fn mini() {
let mut p = PLL::default(); let mut p = PLL::default();
let (y, f) = p.update(0x10000, 8, 4); let (y, f) = p.update(Some(0x10000), 8, 4);
assert_eq!(y, 0x1100); assert_eq!(y, 0x1100);
assert_eq!(f, y); assert_eq!(f, y);
} }
@ -100,7 +105,7 @@ mod tests {
let mut x = 0i32; let mut x = 0i32;
for i in 0..n { for i in 0..n {
x = x.wrapping_add(f0); x = x.wrapping_add(f0);
let (y, f) = p.update(x, shift.0, shift.1); let (y, f) = p.update(Some(x), shift.0, shift.1);
if i > n / 4 { if i > n / 4 {
assert_eq!(f.wrapping_sub(f0).abs() <= 1, true); assert_eq!(f.wrapping_sub(f0).abs() <= 1, true);
} }

View File

@ -87,20 +87,3 @@ pub fn macc_i32(y0: i32, x: &[i32], a: &[i32], shift: u32) -> i32 {
.fold(y0, |y, xa| y + xa); .fold(y0, |y, xa| y + xa);
(y >> shift) as i32 (y >> shift) as i32
} }
/// Combine high and low i32 into a single downscaled i32, saturating the type.
pub fn saturating_scale(lo: i32, hi: i32, shift: u32) -> i32 {
debug_assert!(shift & 31 == shift);
let shift_hi = 31 - shift;
debug_assert!(shift_hi & 31 == shift_hi);
let over = hi >> shift;
if over < -1 {
i32::MIN
} else if over > 0 {
i32::MAX
} else {
(lo >> shift) + (hi << shift_hi)
}
}

View File

@ -16,6 +16,27 @@ pub fn overflowing_sub(y: i32, x: i32) -> (i32, i8) {
(delta, wrap) (delta, wrap)
} }
/// Combine high and low i32 into a single downscaled i32, saturating monotonically.
///
/// Args:
/// `lo`: LSB i32 to scale down by `shift` and range-extend with `hi`
/// `hi`: MSB i32 to scale up and extend `lo` with. Output will be clipped if
/// `hi` exceeds the output i32 range.
/// `shift`: Downscale `lo` by that many bits. Values from 1 to 32 inclusive
/// are valid.
pub fn saturating_scale(lo: i32, hi: i32, shift: u32) -> i32 {
debug_assert!(shift > 0);
debug_assert!(shift <= 32);
let hi_range = -1 << (shift - 1);
if hi <= hi_range {
i32::MIN - hi_range
} else if -hi <= hi_range {
hi_range - i32::MIN
} else {
(lo >> shift) + (hi << (32 - shift))
}
}
/// Overflow unwrapper. /// Overflow unwrapper.
/// ///
/// This is unwrapping as in the phase and overflow unwrapping context, not /// This is unwrapping as in the phase and overflow unwrapping context, not

View File

@ -16,6 +16,7 @@ from gmqtt import Client as MqttClient
LOGGER = logging.getLogger(__name__) LOGGER = logging.getLogger(__name__)
class Miniconf: class Miniconf:
"""An asynchronous API for controlling Miniconf devices using MQTT.""" """An asynchronous API for controlling Miniconf devices using MQTT."""
@ -33,64 +34,64 @@ class Miniconf:
client: A connected MQTT5 client. client: A connected MQTT5 client.
prefix: The MQTT toptic prefix of the device to control. prefix: The MQTT toptic prefix of the device to control.
""" """
self.uuid = uuid.uuid1()
self.request_id = 0 self.request_id = 0
self.client = client self.client = client
self.prefix = prefix self.prefix = prefix
self.inflight = {} self.inflight = {}
self.client.on_message = self._handle_response self.client.on_message = self._handle_response
self.client.subscribe(f'{prefix}/response/{self.uuid.hex}') self.response_topic = f'{prefix}/response/{uuid.uuid1().hex}'
self.client.subscribe(self.response_topic)
def _handle_response(self, _client, _topic, payload, _qos, properties): def _handle_response(self, _client, topic, payload, _qos, properties):
"""Callback function for when messages are received over MQTT. """Callback function for when messages are received over MQTT.
Args: Args:
_client: The MQTT client. _client: The MQTT client.
_topic: The topic that the message was received on. topic: The topic that the message was received on.
payload: The payload of the message. payload: The payload of the message.
_qos: The quality-of-service level of the received packet _qos: The quality-of-service level of the received packet
properties: A dictionary of properties associated with the message. properties: A dictionary of properties associated with the message.
""" """
# Extract corrleation data from the properties if topic == self.response_topic:
correlation_data = json.loads(properties['correlation_data'][0].decode('ascii')) # Extract request_id corrleation data from the properties
request_id = int.from_bytes(
properties['correlation_data'][0], 'big')
# Get the request ID from the correlation data self.inflight[request_id].set_result(json.loads(payload))
request_id = correlation_data['request_id'] del self.inflight[request_id]
else:
LOGGER.warn('Unexpected message on "%s"', topic)
self.inflight[request_id].set_result(json.loads(payload)) async def command(self, path, value, retain=True):
del self.inflight[request_id]
async def command(self, path, value):
"""Write the provided data to the specified path. """Write the provided data to the specified path.
Args: Args:
path: The path to write the message to. path: The path to write the message to.
value: The value to write to the path. value: The value to write to the path.
retain: Retain the MQTT message changing the setting
by the broker.
Returns: Returns:
The response to the command as a dictionary. The response to the command as a dictionary.
""" """
setting_topic = f'{self.prefix}/settings/{path}' topic = f'{self.prefix}/settings/{path}'
response_topic = f'{self.prefix}/response/{self.uuid.hex}'
# Assign a unique identifier to this update request.
request_id = self.request_id
self.request_id += 1
assert request_id not in self.inflight, 'Invalid ID encountered'
correlation_data = json.dumps({
'request_id': request_id,
}).encode('ascii')
value = json.dumps(value)
LOGGER.info('Sending %s to "%s"', value, setting_topic)
fut = asyncio.get_running_loop().create_future() fut = asyncio.get_running_loop().create_future()
self.inflight[request_id] = fut # Assign unique correlation data for response dispatch
self.client.publish(setting_topic, payload=value, qos=0, retain=True, assert self.request_id not in self.inflight
response_topic=response_topic, self.inflight[self.request_id] = fut
correlation_data=correlation_data) 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)
return await fut return await fut
@ -100,16 +101,20 @@ def main():
description='Miniconf command line interface.', description='Miniconf command line interface.',
formatter_class=argparse.RawDescriptionHelpFormatter, formatter_class=argparse.RawDescriptionHelpFormatter,
epilog='''Examples: epilog='''Examples:
%(prog)s dt/sinara/stabilizer afe/0='"G2"' iir_ch/0/0=\ %(prog)s dt/sinara/dual-iir/00-11-22-33-aa-bb iir_ch/0/0=\
'{"y_min": -32767, "y_max": 32767, "y_offset": 0, "ba": [1.0, 0, 0, 0, 0]}' '{"y_min":-32767,"y_max":32767,"y_offset":0,"ba":[1.0,0,0,0,0]}'
%(prog)s dt/sinara/lockin/00-11-22-33-aa-bb afe/0='"G2"'\
''') ''')
parser.add_argument('-v', '--verbose', action='count', default=0, parser.add_argument('-v', '--verbose', action='count', default=0,
help='Increase logging verbosity') help='Increase logging verbosity')
parser.add_argument('--broker', '-b', default='mqtt', type=str, parser.add_argument('--broker', '-b', default='mqtt', type=str,
help='The MQTT broker address') 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, parser.add_argument('prefix', type=str,
help='The MQTT topic prefix of the target') help='The MQTT topic prefix of the target')
parser.add_argument('settings', metavar="KEY=VALUE", nargs='+', parser.add_argument('settings', metavar="PATH=VALUE", nargs='+',
help='JSON encoded values for settings path keys.') help='JSON encoded values for settings path keys.')
args = parser.parse_args() args = parser.parse_args()
@ -122,9 +127,10 @@ def main():
async def configure_settings(): async def configure_settings():
interface = await Miniconf.create(args.prefix, args.broker) interface = await Miniconf.create(args.prefix, args.broker)
for key_value in args.settings: for setting in args.settings:
path, value = key_value.split("=", 1) path, value = setting.split("=", 1)
response = await interface.command(path, json.loads(value)) response = await interface.command(path, json.loads(value),
not args.no_retain)
print(f'{path}: {response}') print(f'{path}: {response}')
if response['code'] != 0: if response['code'] != 0:
return response['code'] return response['code']

View File

@ -2,16 +2,19 @@
#![no_std] #![no_std]
#![no_main] #![no_main]
use core::sync::atomic::{fence, Ordering};
use miniconf::Miniconf;
use serde::Deserialize; use serde::Deserialize;
use dsp::iir; use dsp::iir;
use stabilizer::{ use stabilizer::{
flatten_closures,
hardware::{ hardware::{
hal, setup, Adc0Input, Adc1Input, AdcCode, AfeGain, Dac0Output, hal, setup, Adc0Input, Adc1Input, AdcCode, AfeGain, Dac0Output,
Dac1Output, DacCode, DigitalInput0, DigitalInput1, InputPin, Dac1Output, DacCode, DigitalInput0, DigitalInput1, InputPin,
SystemTimer, AFE0, AFE1, SystemTimer, AFE0, AFE1,
}, },
net::{Miniconf, NetworkState, NetworkUsers, Telemetry, TelemetryBuffer}, net::{NetworkState, NetworkUsers, Telemetry, TelemetryBuffer},
}; };
const SCALE: f32 = i16::MAX as _; const SCALE: f32 = i16::MAX as _;
@ -121,51 +124,63 @@ const APP: () = {
#[task(binds=DMA1_STR4, resources=[adcs, digital_inputs, dacs, iir_state, settings, telemetry], priority=2)] #[task(binds=DMA1_STR4, resources=[adcs, digital_inputs, dacs, iir_state, settings, telemetry], priority=2)]
#[inline(never)] #[inline(never)]
#[link_section = ".itcm.process"] #[link_section = ".itcm.process"]
fn process(c: process::Context) { fn process(mut c: process::Context) {
let adc_samples = [ let process::Resources {
c.resources.adcs.0.acquire_buffer(), adcs: (ref mut adc0, ref mut adc1),
c.resources.adcs.1.acquire_buffer(), dacs: (ref mut dac0, ref mut dac1),
]; ref digital_inputs,
ref settings,
let dac_samples = [ ref mut iir_state,
c.resources.dacs.0.acquire_buffer(), ref mut telemetry,
c.resources.dacs.1.acquire_buffer(), } = c.resources;
];
let digital_inputs = [ let digital_inputs = [
c.resources.digital_inputs.0.is_high().unwrap(), digital_inputs.0.is_high().unwrap(),
c.resources.digital_inputs.1.is_high().unwrap(), digital_inputs.1.is_high().unwrap(),
]; ];
telemetry.digital_inputs = digital_inputs;
let hold = c.resources.settings.force_hold let hold =
|| (digital_inputs[1] && c.resources.settings.allow_hold); settings.force_hold || (digital_inputs[1] && settings.allow_hold);
for channel in 0..adc_samples.len() { flatten_closures!(with_buffer, adc0, adc1, dac0, dac1, {
for sample in 0..adc_samples[0].len() { let adc_samples = [adc0, adc1];
let mut y = f32::from(adc_samples[channel][sample] as i16); let dac_samples = [dac0, dac1];
for i in 0..c.resources.iir_state[channel].len() {
y = c.resources.settings.iir_ch[channel][i].update( // Preserve instruction and data ordering w.r.t. DMA flag access.
&mut c.resources.iir_state[channel][i], fence(Ordering::SeqCst);
y,
hold, for channel in 0..adc_samples.len() {
); adc_samples[channel]
} .iter()
// Note(unsafe): The filter limits ensure that the value is in range. .zip(dac_samples[channel].iter_mut())
// The truncation introduces 1/2 LSB distortion. .map(|(ai, di)| {
let y = unsafe { y.to_int_unchecked::<i16>() }; let x = f32::from(*ai as i16);
// Convert to DAC code let y = settings.iir_ch[channel]
dac_samples[channel][sample] = DacCode::from(y).0; .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() };
// Convert to DAC code
*di = DacCode::from(y).0;
})
.last();
} }
}
// Update telemetry measurements. // Update telemetry measurements.
c.resources.telemetry.adcs = telemetry.adcs =
[AdcCode(adc_samples[0][0]), AdcCode(adc_samples[1][0])]; [AdcCode(adc_samples[0][0]), AdcCode(adc_samples[1][0])];
c.resources.telemetry.dacs = telemetry.dacs =
[DacCode(dac_samples[0][0]), DacCode(dac_samples[1][0])]; [DacCode(dac_samples[0][0]), DacCode(dac_samples[1][0])];
c.resources.telemetry.digital_inputs = digital_inputs; // Preserve instruction and data ordering w.r.t. DMA flag access.
fence(Ordering::SeqCst);
});
} }
#[idle(resources=[network], spawn=[settings_update])] #[idle(resources=[network], spawn=[settings_update])]
@ -223,22 +238,22 @@ const APP: () = {
#[task(binds = SPI2, priority = 3)] #[task(binds = SPI2, priority = 3)]
fn spi2(_: spi2::Context) { fn spi2(_: spi2::Context) {
panic!("ADC0 input overrun"); panic!("ADC0 SPI error");
} }
#[task(binds = SPI3, priority = 3)] #[task(binds = SPI3, priority = 3)]
fn spi3(_: spi3::Context) { fn spi3(_: spi3::Context) {
panic!("ADC1 input overrun"); panic!("ADC1 SPI error");
} }
#[task(binds = SPI4, priority = 3)] #[task(binds = SPI4, priority = 3)]
fn spi4(_: spi4::Context) { fn spi4(_: spi4::Context) {
panic!("DAC0 output error"); panic!("DAC0 SPI error");
} }
#[task(binds = SPI5, priority = 3)] #[task(binds = SPI5, priority = 3)]
fn spi5(_: spi5::Context) { fn spi5(_: spi5::Context) {
panic!("DAC1 output error"); panic!("DAC1 SPI error");
} }
extern "C" { extern "C" {

View File

@ -2,16 +2,19 @@
#![no_std] #![no_std]
#![no_main] #![no_main]
use core::sync::atomic::{fence, Ordering};
use miniconf::Miniconf;
use serde::Deserialize; use serde::Deserialize;
use dsp::{Accu, Complex, ComplexExt, Lockin, RPLL}; use dsp::{Accu, Complex, ComplexExt, Lockin, RPLL};
use stabilizer::{ use stabilizer::{
flatten_closures,
hardware::{ hardware::{
design_parameters, hal, setup, Adc0Input, Adc1Input, AdcCode, AfeGain, design_parameters, hal, setup, Adc0Input, Adc1Input, AdcCode, AfeGain,
Dac0Output, Dac1Output, DacCode, DigitalInput0, DigitalInput1, Dac0Output, Dac1Output, DacCode, DigitalInput0, DigitalInput1,
InputPin, InputStamper, SystemTimer, AFE0, AFE1, InputPin, InputStamper, SystemTimer, AFE0, AFE1,
}, },
net::{Miniconf, NetworkState, NetworkUsers, Telemetry, TelemetryBuffer}, net::{NetworkState, NetworkUsers, Telemetry, TelemetryBuffer},
}; };
// A constant sinusoid to send on the DAC output. // A constant sinusoid to send on the DAC output.
@ -154,26 +157,22 @@ const APP: () = {
#[task(binds=DMA1_STR4, resources=[adcs, dacs, lockin, timestamper, pll, settings, telemetry], priority=2)] #[task(binds=DMA1_STR4, resources=[adcs, dacs, lockin, timestamper, pll, settings, telemetry], priority=2)]
#[inline(never)] #[inline(never)]
#[link_section = ".itcm.process"] #[link_section = ".itcm.process"]
fn process(c: process::Context) { fn process(mut c: process::Context) {
let adc_samples = [ let process::Resources {
c.resources.adcs.0.acquire_buffer(), adcs: (ref mut adc0, ref mut adc1),
c.resources.adcs.1.acquire_buffer(), dacs: (ref mut dac0, ref mut dac1),
]; ref settings,
ref mut telemetry,
let mut dac_samples = [ ref mut lockin,
c.resources.dacs.0.acquire_buffer(), ref mut pll,
c.resources.dacs.1.acquire_buffer(), ref mut timestamper,
]; } = c.resources;
let lockin = c.resources.lockin;
let settings = c.resources.settings;
let (reference_phase, reference_frequency) = match settings.lockin_mode let (reference_phase, reference_frequency) = match settings.lockin_mode
{ {
LockinMode::External => { LockinMode::External => {
let timestamp = let timestamp = timestamper.latest_timestamp().unwrap_or(None); // Ignore data from timer capture overflows.
c.resources.timestamper.latest_timestamp().unwrap_or(None); // Ignore data from timer capture overflows. let (pll_phase, pll_frequency) = pll.update(
let (pll_phase, pll_frequency) = c.resources.pll.update(
timestamp.map(|t| t as i32), timestamp.map(|t| t as i32),
settings.pll_tc[0], settings.pll_tc[0],
settings.pll_tc[1], settings.pll_tc[1],
@ -200,45 +199,55 @@ const APP: () = {
reference_phase.wrapping_mul(settings.lockin_harmonic), reference_phase.wrapping_mul(settings.lockin_harmonic),
); );
let output: Complex<i32> = adc_samples[0] flatten_closures!(with_buffer, adc0, adc1, dac0, dac1, {
.iter() let adc_samples = [adc0, adc1];
// Zip in the LO phase. let mut dac_samples = [dac0, dac1];
.zip(Accu::new(sample_phase, sample_frequency))
// Convert to signed, MSB align the ADC sample, update the Lockin (demodulate, filter)
.map(|(&sample, phase)| {
let s = (sample as i16 as i32) << 16;
lockin.update(s, phase, settings.lockin_tc)
})
// Decimate
.last()
.unwrap()
* 2; // Full scale assuming the 2f component is gone.
// Convert to DAC data. // Preserve instruction and data ordering w.r.t. DMA flag access.
for (channel, samples) in dac_samples.iter_mut().enumerate() { fence(Ordering::SeqCst);
for (i, sample) in samples.iter_mut().enumerate() {
let value = match settings.output_conf[channel] {
Conf::Magnitude => output.abs_sqr() as i32 >> 16,
Conf::Phase => output.arg() >> 16,
Conf::LogPower => (output.log2() << 24) as i32 >> 16,
Conf::ReferenceFrequency => {
reference_frequency as i32 >> 16
}
Conf::InPhase => output.re >> 16,
Conf::Quadrature => output.im >> 16,
Conf::Modulation => DAC_SEQUENCE[i] as i32,
};
*sample = DacCode::from(value as i16).0; let output: Complex<i32> = adc_samples[0]
.iter()
// Zip in the LO phase.
.zip(Accu::new(sample_phase, sample_frequency))
// Convert to signed, MSB align the ADC sample, update the Lockin (demodulate, filter)
.map(|(&sample, phase)| {
let s = (sample as i16 as i32) << 16;
lockin.update(s, phase, settings.lockin_tc)
})
// Decimate
.last()
.unwrap()
* 2; // Full scale assuming the 2f component is gone.
// Convert to DAC data.
for (channel, samples) in dac_samples.iter_mut().enumerate() {
for (i, sample) in samples.iter_mut().enumerate() {
let value = match settings.output_conf[channel] {
Conf::Magnitude => output.abs_sqr() as i32 >> 16,
Conf::Phase => output.arg() >> 16,
Conf::LogPower => (output.log2() << 24) as i32 >> 16,
Conf::ReferenceFrequency => {
reference_frequency as i32 >> 16
}
Conf::InPhase => output.re >> 16,
Conf::Quadrature => output.im >> 16,
Conf::Modulation => DAC_SEQUENCE[i] as i32,
};
*sample = DacCode::from(value as i16).0;
}
} }
} // Update telemetry measurements.
telemetry.adcs =
[AdcCode(adc_samples[0][0]), AdcCode(adc_samples[1][0])];
// Update telemetry measurements. telemetry.dacs =
c.resources.telemetry.adcs = [DacCode(dac_samples[0][0]), DacCode(dac_samples[1][0])];
[AdcCode(adc_samples[0][0]), AdcCode(adc_samples[1][0])];
c.resources.telemetry.dacs = // Preserve instruction and data ordering w.r.t. DMA flag access.
[DacCode(dac_samples[0][0]), DacCode(dac_samples[1][0])]; fence(Ordering::SeqCst);
});
} }
#[idle(resources=[network], spawn=[settings_update])] #[idle(resources=[network], spawn=[settings_update])]
@ -300,22 +309,22 @@ const APP: () = {
#[task(binds = SPI2, priority = 3)] #[task(binds = SPI2, priority = 3)]
fn spi2(_: spi2::Context) { fn spi2(_: spi2::Context) {
panic!("ADC0 input overrun"); panic!("ADC0 SPI error");
} }
#[task(binds = SPI3, priority = 3)] #[task(binds = SPI3, priority = 3)]
fn spi3(_: spi3::Context) { fn spi3(_: spi3::Context) {
panic!("ADC1 input overrun"); panic!("ADC1 SPI error");
} }
#[task(binds = SPI4, priority = 3)] #[task(binds = SPI4, priority = 3)]
fn spi4(_: spi4::Context) { fn spi4(_: spi4::Context) {
panic!("DAC0 output error"); panic!("DAC0 SPI error");
} }
#[task(binds = SPI5, priority = 3)] #[task(binds = SPI5, priority = 3)]
fn spi5(_: spi5::Context) { fn spi5(_: spi5::Context) {
panic!("DAC1 output error"); panic!("DAC1 SPI error");
} }
extern "C" { extern "C" {

View File

@ -29,15 +29,9 @@
///! available. When enough samples have been collected, a transfer-complete interrupt is generated ///! available. When enough samples have been collected, a transfer-complete interrupt is generated
///! and the ADC samples are available for processing. ///! and the ADC samples are available for processing.
///! ///!
///! The SPI peripheral internally has an 8- or 16-byte TX and RX FIFO, which corresponds to a 4- or ///! After a complete transfer of a batch of samples, the inactive buffer is available to the
///! 8-sample buffer for incoming ADC samples. During the handling of the DMA transfer completion, ///! user for processing. The processing must complete before the DMA transfer of the next batch
///! there is a small window where buffers are swapped over where it's possible that a sample could ///! completes.
///! be lost. In order to avoid this, the SPI RX FIFO is effectively used as a "sample overflow"
///! region and can buffer a number of samples until the next DMA transfer is configured. If a DMA
///! transfer is still not set in time, the SPI peripheral will generate an input-overrun interrupt.
///! This interrupt then serves as a means of detecting if samples have been lost, which will occur
///! whenever data processing takes longer than the collection period.
///!
///! ///!
///! ## Starting Data Collection ///! ## Starting Data Collection
///! ///!
@ -68,26 +62,26 @@
///! sample DMA requests, which can be completed by setting e.g. ADC0's comparison to a counter ///! sample DMA requests, which can be completed by setting e.g. ADC0's comparison to a counter
///! value of 0 and ADC1's comparison to a counter value of 1. ///! value of 0 and ADC1's comparison to a counter value of 1.
///! ///!
///! In this implementation, single buffer mode DMA transfers are used because the SPI RX FIFO can ///! In this implementation, double buffer mode DMA transfers are used because the SPI RX FIFOs
///! be used as a means to both detect and buffer ADC samples during the buffer swap-over. Because ///! have finite depth, FIFO access is slower than AXISRAM access, and because the single
///! of this, double-buffered mode does not offer any advantages over single-buffered mode (unless ///! buffer mode DMA disable/enable and buffer update sequence is slow.
///! double-buffered mode offers less overhead due to the DMA disable/enable procedure).
use stm32h7xx_hal as hal; use stm32h7xx_hal as hal;
use super::design_parameters::SAMPLE_BUFFER_SIZE; use super::design_parameters::{SampleBuffer, SAMPLE_BUFFER_SIZE};
use super::timers; use super::timers;
use hal::dma::{ use hal::dma::{
config::Priority, config::Priority,
dma::{DMAReq, DmaConfig}, dma::{DMAReq, DmaConfig},
traits::TargetAddress, traits::TargetAddress,
MemoryToPeripheral, PeripheralToMemory, Transfer, DMAError, MemoryToPeripheral, PeripheralToMemory, Transfer,
}; };
/// A type representing an ADC sample. /// A type representing an ADC sample.
#[derive(Copy, Clone)] #[derive(Copy, Clone)]
pub struct AdcCode(pub u16); pub struct AdcCode(pub u16);
#[allow(clippy::from_over_into)]
impl Into<f32> for AdcCode { impl Into<f32> for AdcCode {
/// Convert raw ADC codes to/from voltage levels. /// Convert raw ADC codes to/from voltage levels.
/// ///
@ -119,8 +113,7 @@ static mut SPI_EOT_CLEAR: [u32; 1] = [0x00];
// processed). Note that the contents of AXI SRAM is uninitialized, so the buffer contents on // processed). Note that the contents of AXI SRAM is uninitialized, so the buffer contents on
// startup are undefined. The dimensions are `ADC_BUF[adc_index][ping_pong_index][sample_index]`. // startup are undefined. The dimensions are `ADC_BUF[adc_index][ping_pong_index][sample_index]`.
#[link_section = ".axisram.buffers"] #[link_section = ".axisram.buffers"]
static mut ADC_BUF: [[[u16; SAMPLE_BUFFER_SIZE]; 2]; 2] = static mut ADC_BUF: [[SampleBuffer; 2]; 2] = [[[0; SAMPLE_BUFFER_SIZE]; 2]; 2];
[[[0; SAMPLE_BUFFER_SIZE]; 2]; 2];
macro_rules! adc_input { macro_rules! adc_input {
($name:ident, $index:literal, $trigger_stream:ident, $data_stream:ident, $clear_stream:ident, ($name:ident, $index:literal, $trigger_stream:ident, $data_stream:ident, $clear_stream:ident,
@ -192,12 +185,11 @@ macro_rules! adc_input {
/// Represents data associated with ADC. /// Represents data associated with ADC.
pub struct $name { pub struct $name {
next_buffer: Option<&'static mut [u16; SAMPLE_BUFFER_SIZE]>,
transfer: Transfer< transfer: Transfer<
hal::dma::dma::$data_stream<hal::stm32::DMA1>, hal::dma::dma::$data_stream<hal::stm32::DMA1>,
hal::spi::Spi<hal::stm32::$spi, hal::spi::Disabled, u16>, hal::spi::Spi<hal::stm32::$spi, hal::spi::Disabled, u16>,
PeripheralToMemory, PeripheralToMemory,
&'static mut [u16; SAMPLE_BUFFER_SIZE], &'static mut SampleBuffer,
hal::dma::DBTransfer, hal::dma::DBTransfer,
>, >,
trigger_transfer: Transfer< trigger_transfer: Transfer<
@ -316,6 +308,7 @@ macro_rules! adc_input {
// data stream is used to trigger a transfer completion interrupt. // data stream is used to trigger a transfer completion interrupt.
let data_config = DmaConfig::default() let data_config = DmaConfig::default()
.memory_increment(true) .memory_increment(true)
.double_buffer(true)
.transfer_complete_interrupt($index == 1) .transfer_complete_interrupt($index == 1)
.priority(Priority::VeryHigh); .priority(Priority::VeryHigh);
@ -333,17 +326,14 @@ macro_rules! adc_input {
Transfer::init( Transfer::init(
data_stream, data_stream,
spi, spi,
// Note(unsafe): The ADC_BUF[$index][0] is "owned" by this peripheral. // Note(unsafe): The ADC_BUF[$index] is "owned" by this peripheral.
// It shall not be used anywhere else in the module. // It shall not be used anywhere else in the module.
unsafe { &mut ADC_BUF[$index][0] }, unsafe { &mut ADC_BUF[$index][0] },
None, unsafe { Some(&mut ADC_BUF[$index][1]) },
data_config, data_config,
); );
Self { Self {
// Note(unsafe): The ADC_BUF[$index][1] is "owned" by this peripheral. It
// shall not be used anywhere else in the module.
next_buffer: unsafe { Some(&mut ADC_BUF[$index][1]) },
transfer: data_transfer, transfer: data_transfer,
trigger_transfer, trigger_transfer,
clear_transfer, clear_transfer,
@ -364,27 +354,17 @@ macro_rules! adc_input {
} }
/// Obtain a buffer filled with ADC samples. /// Wait for the transfer of the currently active buffer to complete,
/// then call a function on the now inactive buffer and acknowledge the
/// transfer complete flag.
/// ///
/// # Returns /// NOTE(unsafe): Memory safety and access ordering is not guaranteed
/// A reference to the underlying buffer that has been filled with ADC samples. /// (see the HAL DMA docs).
pub fn acquire_buffer(&mut self) -> &[u16; SAMPLE_BUFFER_SIZE] { pub fn with_buffer<F, R>(&mut self, f: F) -> Result<R, DMAError>
// Wait for the transfer to fully complete before continuing. Note: If a device where
// hangs up, check that this conditional is passing correctly, as there is no F: FnOnce(&mut SampleBuffer) -> R,
// time-out checks here in the interest of execution speed. {
while !self.transfer.get_transfer_complete_flag() {} unsafe { self.transfer.next_dbm_transfer_with(|buf, _current| f(buf)) }
let next_buffer = self.next_buffer.take().unwrap();
// Start the next transfer.
self.transfer.clear_interrupts();
let (prev_buffer, _, _) =
self.transfer.next_transfer(next_buffer).unwrap();
// .unwrap_none() https://github.com/rust-lang/rust/issues/62633
self.next_buffer.replace(prev_buffer);
self.next_buffer.as_ref().unwrap()
} }
} }
} }

View File

@ -233,11 +233,6 @@ pub fn setup(
let dma_streams = let dma_streams =
hal::dma::dma::StreamsTuple::new(device.DMA1, ccdr.peripheral.DMA1); hal::dma::dma::StreamsTuple::new(device.DMA1, ccdr.peripheral.DMA1);
// Early, before the DMA1 peripherals (#272)
#[cfg(feature = "pounder_v1_1")]
let dma2_streams =
hal::dma::dma::StreamsTuple::new(device.DMA2, ccdr.peripheral.DMA2);
// Configure timer 2 to trigger conversions for the ADC // Configure timer 2 to trigger conversions for the ADC
let mut sampling_timer = { let mut sampling_timer = {
// The timer frequency is manually adjusted below, so the 1KHz setting here is a // The timer frequency is manually adjusted below, so the 1KHz setting here is a
@ -802,7 +797,7 @@ pub fn setup(
let scl = gpiob.pb8.into_alternate_af4().set_open_drain(); let scl = gpiob.pb8.into_alternate_af4().set_open_drain();
let i2c1 = device.I2C1.i2c( let i2c1 = device.I2C1.i2c(
(scl, sda), (scl, sda),
100.khz(), 400.khz(),
ccdr.peripheral.I2C1, ccdr.peripheral.I2C1,
&ccdr.clocks, &ccdr.clocks,
); );
@ -946,7 +941,6 @@ pub fn setup(
pounder::timestamp::Timestamper::new( pounder::timestamp::Timestamper::new(
timestamp_timer, timestamp_timer,
dma2_streams.0,
tim8_channels.ch1, tim8_channels.ch1,
&mut sampling_timer, &mut sampling_timer,
etr_pin, etr_pin,

View File

@ -52,13 +52,13 @@
///! served promptly after the transfer completes. ///! served promptly after the transfer completes.
use stm32h7xx_hal as hal; use stm32h7xx_hal as hal;
use super::design_parameters::SAMPLE_BUFFER_SIZE; use super::design_parameters::{SampleBuffer, SAMPLE_BUFFER_SIZE};
use super::timers; use super::timers;
use hal::dma::{ use hal::dma::{
dma::{DMAReq, DmaConfig}, dma::{DMAReq, DmaConfig},
traits::TargetAddress, traits::TargetAddress,
MemoryToPeripheral, Transfer, DMAError, MemoryToPeripheral, Transfer,
}; };
// The following global buffers are used for the DAC code DMA transfers. Two buffers are used for // The following global buffers are used for the DAC code DMA transfers. Two buffers are used for
@ -66,14 +66,14 @@ use hal::dma::{
// processed). Note that the contents of AXI SRAM is uninitialized, so the buffer contents on // processed). Note that the contents of AXI SRAM is uninitialized, so the buffer contents on
// startup are undefined. The dimensions are `ADC_BUF[adc_index][ping_pong_index][sample_index]`. // startup are undefined. The dimensions are `ADC_BUF[adc_index][ping_pong_index][sample_index]`.
#[link_section = ".axisram.buffers"] #[link_section = ".axisram.buffers"]
static mut DAC_BUF: [[[u16; SAMPLE_BUFFER_SIZE]; 3]; 2] = static mut DAC_BUF: [[SampleBuffer; 2]; 2] = [[[0; SAMPLE_BUFFER_SIZE]; 2]; 2];
[[[0; SAMPLE_BUFFER_SIZE]; 3]; 2];
/// Custom type for referencing DAC output codes. /// Custom type for referencing DAC output codes.
/// The internal integer is the raw code written to the DAC output register. /// The internal integer is the raw code written to the DAC output register.
#[derive(Copy, Clone)] #[derive(Copy, Clone)]
pub struct DacCode(pub u16); pub struct DacCode(pub u16);
#[allow(clippy::from_over_into)]
impl Into<f32> for DacCode { impl Into<f32> for DacCode {
fn into(self) -> f32 { fn into(self) -> f32 {
// The DAC output range in bipolar mode (including the external output op-amp) is +/- 4.096 // The DAC output range in bipolar mode (including the external output op-amp) is +/- 4.096
@ -105,7 +105,7 @@ macro_rules! dac_output {
_channel: timers::tim2::$trigger_channel, _channel: timers::tim2::$trigger_channel,
spi: hal::spi::Spi<hal::stm32::$spi, hal::spi::Disabled, u16>, spi: hal::spi::Spi<hal::stm32::$spi, hal::spi::Disabled, u16>,
) -> Self { ) -> Self {
Self { _channel, spi } Self { spi, _channel }
} }
/// Start the SPI and begin operating in a DMA-driven transfer mode. /// Start the SPI and begin operating in a DMA-driven transfer mode.
@ -137,13 +137,12 @@ macro_rules! dac_output {
/// Represents data associated with DAC. /// Represents data associated with DAC.
pub struct $name { pub struct $name {
next_buffer: Option<&'static mut [u16; SAMPLE_BUFFER_SIZE]>,
// Note: SPI TX functionality may not be used from this structure to ensure safety with DMA. // Note: SPI TX functionality may not be used from this structure to ensure safety with DMA.
transfer: Transfer< transfer: Transfer<
hal::dma::dma::$data_stream<hal::stm32::DMA1>, hal::dma::dma::$data_stream<hal::stm32::DMA1>,
$spi, $spi,
MemoryToPeripheral, MemoryToPeripheral,
&'static mut [u16; SAMPLE_BUFFER_SIZE], &'static mut SampleBuffer,
hal::dma::DBTransfer, hal::dma::DBTransfer,
>, >,
} }
@ -198,33 +197,26 @@ macro_rules! dac_output {
trigger_config, trigger_config,
); );
Self { Self { transfer }
transfer,
// Note(unsafe): This buffer is only used once and provided for the next DMA transfer.
next_buffer: unsafe { Some(&mut DAC_BUF[$index][2]) },
}
} }
pub fn start(&mut self) { pub fn start(&mut self) {
self.transfer.start(|spi| spi.start_dma()); self.transfer.start(|spi| spi.start_dma());
} }
/// Acquire the next output buffer to populate it with DAC codes. /// Wait for the transfer of the currently active buffer to complete,
pub fn acquire_buffer(&mut self) -> &mut [u16; SAMPLE_BUFFER_SIZE] { /// then call a function on the now inactive buffer and acknowledge the
// Note: If a device hangs up, check that this conditional is passing correctly, as /// transfer complete flag.
// there is no time-out checks here in the interest of execution speed. ///
while !self.transfer.get_transfer_complete_flag() {} /// NOTE(unsafe): Memory safety and access ordering is not guaranteed
/// (see the HAL DMA docs).
let next_buffer = self.next_buffer.take().unwrap(); pub fn with_buffer<F, R>(&mut self, f: F) -> Result<R, DMAError>
where
// Start the next transfer. F: FnOnce(&mut SampleBuffer) -> R,
let (prev_buffer, _, _) = {
self.transfer.next_transfer(next_buffer).unwrap(); unsafe {
self.transfer.next_dbm_transfer_with(|buf, _current| f(buf))
// .unwrap_none() https://github.com/rust-lang/rust/issues/62633 }
self.next_buffer.replace(prev_buffer);
self.next_buffer.as_mut().unwrap()
} }
} }
}; };

View File

@ -50,5 +50,7 @@ pub const ADC_SAMPLE_TICKS: u16 = 1 << ADC_SAMPLE_TICKS_LOG2;
pub const SAMPLE_BUFFER_SIZE_LOG2: u8 = 3; pub const SAMPLE_BUFFER_SIZE_LOG2: u8 = 3;
pub const SAMPLE_BUFFER_SIZE: usize = 1 << SAMPLE_BUFFER_SIZE_LOG2; pub const SAMPLE_BUFFER_SIZE: usize = 1 << SAMPLE_BUFFER_SIZE_LOG2;
pub type SampleBuffer = [u16; SAMPLE_BUFFER_SIZE];
// The MQTT broker IPv4 address // The MQTT broker IPv4 address
pub const MQTT_BROKER: [u8; 4] = [10, 34, 16, 10]; pub const MQTT_BROKER: [u8; 4] = [10, 34, 16, 10];

View File

@ -30,16 +30,16 @@ pub trait AttenuatorInterface {
// Read all the channels, modify the channel of interest, and write all the channels back. // Read all the channels, modify the channel of interest, and write all the channels back.
// This ensures the staging register and the output register are always in sync. // This ensures the staging register and the output register are always in sync.
let mut channels = [0_u8; 4]; let mut channels = [0_u8; 4];
self.read_all_attenuators(&mut channels)?; self.transfer_attenuators(&mut channels)?;
// The lowest 2 bits of the 8-bit shift register on the attenuator are ignored. Shift the // The lowest 2 bits of the 8-bit shift register on the attenuator are ignored. Shift the
// attenuator code into the upper 6 bits of the register value. Note that the attenuator // attenuator code into the upper 6 bits of the register value. Note that the attenuator
// treats inputs as active-low, so the code is inverted before writing. // treats inputs as active-low, so the code is inverted before writing.
channels[channel as usize] = (!attenuation_code) << 2; channels[channel as usize] = !(attenuation_code << 2);
self.write_all_attenuators(&channels)?; self.transfer_attenuators(&mut channels)?;
// Finally, latch the output of the updated channel to force it into an active state. // Finally, latch the output of the updated channel to force it into an active state.
self.latch_attenuators(channel)?; self.latch_attenuator(channel)?;
Ok(attenuation_code as f32 / 2.0) Ok(attenuation_code as f32 / 2.0)
} }
@ -57,8 +57,8 @@ pub trait AttenuatorInterface {
// Reading the data always shifts data out of the staging registers, so we perform a // Reading the data always shifts data out of the staging registers, so we perform a
// duplicate write-back to ensure the staging register is always equal to the output // duplicate write-back to ensure the staging register is always equal to the output
// register. // register.
self.read_all_attenuators(&mut channels)?; self.transfer_attenuators(&mut channels)?;
self.write_all_attenuators(&channels)?; self.transfer_attenuators(&mut channels)?;
// The attenuation code is stored in the upper 6 bits of the register, where each LSB // The attenuation code is stored in the upper 6 bits of the register, where each LSB
// represents 0.5 dB. The attenuator stores the code as active-low, so inverting the result // represents 0.5 dB. The attenuator stores the code as active-low, so inverting the result
@ -74,13 +74,10 @@ pub trait AttenuatorInterface {
fn reset_attenuators(&mut self) -> Result<(), Error>; fn reset_attenuators(&mut self) -> Result<(), Error>;
fn latch_attenuators(&mut self, channel: Channel) -> Result<(), Error>; fn latch_attenuator(&mut self, channel: Channel) -> Result<(), Error>;
fn read_all_attenuators(
fn transfer_attenuators(
&mut self, &mut self,
channels: &mut [u8; 4], channels: &mut [u8; 4],
) -> Result<(), Error>; ) -> Result<(), Error>;
fn write_all_attenuators(
&mut self,
channels: &[u8; 4],
) -> Result<(), Error>;
} }

View File

@ -52,9 +52,11 @@
///! compile-time-known register update sequence needed for the application, the serialization ///! compile-time-known register update sequence needed for the application, the serialization
///! process can be done once and then register values can be written into a pre-computed serialized ///! process can be done once and then register values can be written into a pre-computed serialized
///! buffer to avoid the software overhead of much of the serialization process. ///! buffer to avoid the software overhead of much of the serialization process.
use log::warn;
use stm32h7xx_hal as hal;
use super::{hrtimer::HighResTimerE, QspiInterface}; use super::{hrtimer::HighResTimerE, QspiInterface};
use ad9959::{Channel, DdsConfig, ProfileSerializer}; use ad9959::{Channel, DdsConfig, ProfileSerializer};
use stm32h7xx_hal as hal;
/// The DDS profile update stream. /// The DDS profile update stream.
pub struct DdsOutput { pub struct DdsOutput {

View File

@ -15,14 +15,21 @@ pub use dds_output::*;
pub use hrtimer::{Channel as HRTimerChannel, *}; pub use hrtimer::{Channel as HRTimerChannel, *};
pub use rf_power::*; pub use rf_power::*;
const EXT_CLK_SEL_PIN: u8 = 8 + 7; pub enum GpioPin {
#[allow(dead_code)] Led4Green = 0,
const OSC_EN_N_PIN: u8 = 8 + 6; Led5Red = 1,
const ATT_RST_N_PIN: u8 = 8 + 5; Led6Green = 2,
const ATT_LE3_PIN: u8 = 8 + 3; Led7Red = 3,
const ATT_LE2_PIN: u8 = 8 + 2; Led8Green = 4,
const ATT_LE1_PIN: u8 = 8 + 1; Led9Red = 5,
const ATT_LE0_PIN: u8 = 8; AttLe0 = 8,
AttLe1 = 8 + 1,
AttLe2 = 8 + 2,
AttLe3 = 8 + 3,
AttRstN = 8 + 5,
OscEnN = 8 + 6,
ExtClkSel = 8 + 7,
}
#[derive(Debug, Copy, Clone)] #[derive(Debug, Copy, Clone)]
pub enum Error { pub enum Error {
@ -35,13 +42,15 @@ pub enum Error {
Adc, Adc,
} }
/// The numerical value (discriminant) of the Channel enum is the index in the attenuator shift
/// register as well as the attenuator latch enable signal index on the GPIO extender.
#[derive(Debug, Copy, Clone)] #[derive(Debug, Copy, Clone)]
#[allow(dead_code)] #[allow(dead_code)]
pub enum Channel { pub enum Channel {
In0, In0 = 0,
In1, Out0 = 1,
Out0, In1 = 2,
Out1, Out1 = 3,
} }
#[derive(Serialize, Deserialize, Copy, Clone, Debug)] #[derive(Serialize, Deserialize, Copy, Clone, Debug)]
@ -78,14 +87,14 @@ pub struct DdsClockConfig {
pub external_clock: bool, pub external_clock: bool,
} }
impl Into<ad9959::Channel> for Channel { impl From<Channel> for ad9959::Channel {
/// Translate pounder channels to DDS output channels. /// Translate pounder channels to DDS output channels.
fn into(self) -> ad9959::Channel { fn from(other: Channel) -> Self {
match self { match other {
Channel::In0 => ad9959::Channel::Two, Channel::In0 => Self::Two,
Channel::In1 => ad9959::Channel::Four, Channel::In1 => Self::Four,
Channel::Out0 => ad9959::Channel::One, Channel::Out0 => Self::One,
Channel::Out1 => ad9959::Channel::Three, Channel::Out1 => Self::Three,
} }
} }
} }
@ -298,25 +307,20 @@ impl PounderDevices {
adc2_in_p, adc2_in_p,
}; };
// Configure power-on-default state for pounder. All LEDs are on, on-board oscillator // Configure power-on-default state for pounder. All LEDs are off, on-board oscillator
// selected, attenuators out of reset. Note that testing indicates the output state needs to // selected and enabled, attenuators out of reset. Note that testing indicates the
// be set first to properly update the output registers. // output state needs to be set first to properly update the output registers.
devices devices
.mcp23017 .mcp23017
.all_pin_mode(mcp23017::PinMode::OUTPUT) .all_pin_mode(mcp23017::PinMode::OUTPUT)
.map_err(|_| Error::I2c)?; .map_err(|_| Error::I2c)?;
devices devices
.mcp23017 .mcp23017
.write_gpio(mcp23017::Port::GPIOA, 0x3F) .write_gpio(mcp23017::Port::GPIOA, 0x00)
.map_err(|_| Error::I2c)?; .map_err(|_| Error::I2c)?;
devices devices
.mcp23017 .mcp23017
.write_gpio(mcp23017::Port::GPIOB, 1 << 5) .write_gpio(mcp23017::Port::GPIOB, 0x2F)
.map_err(|_| Error::I2c)?;
devices
.mcp23017
.digital_write(EXT_CLK_SEL_PIN, false)
.map_err(|_| Error::I2c)?; .map_err(|_| Error::I2c)?;
Ok(devices) Ok(devices)
@ -327,12 +331,11 @@ impl AttenuatorInterface for PounderDevices {
/// Reset all of the attenuators to a power-on default state. /// Reset all of the attenuators to a power-on default state.
fn reset_attenuators(&mut self) -> Result<(), Error> { fn reset_attenuators(&mut self) -> Result<(), Error> {
self.mcp23017 self.mcp23017
.digital_write(ATT_RST_N_PIN, false) .write_gpio(mcp23017::Port::GPIOB, 0x0f)
.map_err(|_| Error::I2c)?; .map_err(|_| Error::I2c)?;
// TODO: Measure the I2C transaction speed to the RST pin to ensure that the delay is // Duration of one I2C transaction is sufficiently long.
// sufficient. Document the delay here.
self.mcp23017 self.mcp23017
.digital_write(ATT_RST_N_PIN, true) .write_gpio(mcp23017::Port::GPIOB, 0x2f)
.map_err(|_| Error::I2c)?; .map_err(|_| Error::I2c)?;
Ok(()) Ok(())
@ -342,31 +345,24 @@ impl AttenuatorInterface for PounderDevices {
/// ///
/// Args: /// Args:
/// * `channel` - The attenuator channel to latch. /// * `channel` - The attenuator channel to latch.
fn latch_attenuators(&mut self, channel: Channel) -> Result<(), Error> { fn latch_attenuator(&mut self, channel: Channel) -> Result<(), Error> {
let pin = match channel { let pin = channel as u8;
Channel::In0 => ATT_LE0_PIN,
Channel::In1 => ATT_LE2_PIN,
Channel::Out0 => ATT_LE1_PIN,
Channel::Out1 => ATT_LE3_PIN,
};
self.mcp23017 self.mcp23017
.digital_write(pin, true) .write_gpio(mcp23017::Port::GPIOB, 0x2f & !(1 << pin))
.map_err(|_| Error::I2c)?; .map_err(|_| Error::I2c)?;
// TODO: Measure the I2C transaction speed to the RST pin to ensure that the delay is // Duration of one I2C transaction is sufficiently long.
// sufficient. Document the delay here.
self.mcp23017 self.mcp23017
.digital_write(pin, false) .write_gpio(mcp23017::Port::GPIOB, 0x2f)
.map_err(|_| Error::I2c)?; .map_err(|_| Error::I2c)?;
Ok(()) Ok(())
} }
/// Read the raw attenuation codes stored in the attenuator shift registers. /// Read the raw attenuation codes stored in the attenuator shift registers.
/// ///
/// Args: /// Args:
/// * `channels` - A slice to store the channel readings into. /// * `channels` - A 4 byte slice to be shifted into the
fn read_all_attenuators( /// attenuators and to contain the data shifted out.
fn transfer_attenuators(
&mut self, &mut self,
channels: &mut [u8; 4], channels: &mut [u8; 4],
) -> Result<(), Error> { ) -> Result<(), Error> {
@ -376,23 +372,6 @@ impl AttenuatorInterface for PounderDevices {
Ok(()) Ok(())
} }
/// Write the attenuator shift registers.
///
/// Args:
/// * `channels` - The data to write into the attenuators.
fn write_all_attenuators(
&mut self,
channels: &[u8; 4],
) -> Result<(), Error> {
let mut result = [0_u8; 4];
result.clone_from_slice(channels);
self.attenuator_spi
.transfer(&mut result)
.map_err(|_| Error::Spi)?;
Ok(())
}
} }
impl PowerMeasurementInterface for PounderDevices { impl PowerMeasurementInterface for PounderDevices {

View File

@ -1,20 +1,22 @@
use super::{Channel, Error}; use super::{Channel, Error};
/// Provide an interface to measure RF input power in dB. /// Provide an interface to measure RF input power in dBm.
pub trait PowerMeasurementInterface { pub trait PowerMeasurementInterface {
fn sample_converter(&mut self, channel: Channel) -> Result<f32, Error>; fn sample_converter(&mut self, channel: Channel) -> Result<f32, Error>;
/// Measure the power of an input channel in dBm. /// Measure the power of an input channel in dBm.
/// ///
/// Note: This function assumes the input channel is connected to an AD8363 output.
///
/// Args: /// Args:
/// * `channel` - The pounder channel to measure the power of in dBm. /// * `channel` - The pounder input channel to measure the power of.
///
/// Returns:
/// Power in dBm after the digitally controlled attenuator before the amplifier.
fn measure_power(&mut self, channel: Channel) -> Result<f32, Error> { fn measure_power(&mut self, channel: Channel) -> Result<f32, Error> {
let analog_measurement = self.sample_converter(channel)?; let analog_measurement = self.sample_converter(channel)?;
// The AD8363 with VSET connected to VOUT provides an output voltage of 51.7mV / dB at // The AD8363 with VSET connected to VOUT provides an output voltage of 51.7 mV/dB at
// 100MHz. It also indicates a y-intercept of -58dBm. // 100MHz with an intercept of -58 dBm.
Ok(analog_measurement / 0.0517 - 58.0) // It is placed behind a 20 dB tap.
Ok(analog_measurement * (1. / 0.0517) + (-58. + 20.))
} }
} }

View File

@ -13,50 +13,24 @@
///! Once the timer is configured, an input capture is configured to record the timer count ///! Once the timer is configured, an input capture is configured to record the timer count
///! register. The input capture is configured to utilize an internal trigger for the input capture. ///! register. The input capture is configured to utilize an internal trigger for the input capture.
///! The internal trigger is selected such that when a sample is generated on ADC0, the input ///! The internal trigger is selected such that when a sample is generated on ADC0, the input
///! capture is simultaneously triggered. This results in the input capture triggering identically ///! capture is simultaneously triggered. That trigger is prescaled (its rate is divided) by the
///! to when the ADC samples the input. ///! batch size. This results in the input capture triggering identically to when the ADC samples
///! ///! the last sample of the batch. That sample is then available for processing by the user.
///! Once the input capture is properly configured, a DMA transfer is configured to collect all of use crate::hardware::{design_parameters, timers};
///! timestamps. The DMA transfer collects 1 timestamp for each ADC sample collected. In order to use core::convert::TryFrom;
///! avoid potentially losing a timestamp for a sample, the DMA transfer operates in double-buffer
///! mode. As soon as the DMA transfer completes, the hardware automatically swaps over to a second
///! buffer to continue capturing. This alleviates timing sensitivities of the DMA transfer
///! schedule.
use stm32h7xx_hal as hal; use stm32h7xx_hal as hal;
use hal::dma::{dma::DmaConfig, PeripheralToMemory, Transfer};
use crate::hardware::{design_parameters::SAMPLE_BUFFER_SIZE, timers};
// Three buffers are required for double buffered mode - 2 are owned by the DMA stream and 1 is the
// working data provided to the application. These buffers must exist in a DMA-accessible memory
// region. Note that AXISRAM is not initialized on boot, so their initial contents are undefined.
#[link_section = ".axisram.buffers"]
static mut BUF: [[u16; SAMPLE_BUFFER_SIZE]; 3] = [[0; SAMPLE_BUFFER_SIZE]; 3];
/// Software unit to timestamp stabilizer ADC samples using an external pounder reference clock. /// Software unit to timestamp stabilizer ADC samples using an external pounder reference clock.
pub struct Timestamper { pub struct Timestamper {
next_buffer: Option<&'static mut [u16; SAMPLE_BUFFER_SIZE]>,
timer: timers::PounderTimestampTimer, timer: timers::PounderTimestampTimer,
transfer: Transfer< capture_channel: timers::tim8::Channel1InputCapture,
hal::dma::dma::Stream0<hal::stm32::DMA2>,
timers::tim8::Channel1InputCapture,
PeripheralToMemory,
&'static mut [u16; SAMPLE_BUFFER_SIZE],
hal::dma::DBTransfer,
>,
} }
impl Timestamper { impl Timestamper {
/// Construct the pounder sample timestamper. /// Construct the pounder sample timestamper.
/// ///
/// # Note
/// The DMA is immediately configured after instantiation. It will not collect any samples
/// until the sample timer begins to cause input capture triggers.
///
/// # Args /// # Args
/// * `timestamp_timer` - The timer peripheral used for capturing timestamps from. /// * `timestamp_timer` - The timer peripheral used for capturing timestamps from.
/// * `stream` - The DMA stream to use for collecting timestamps.
/// * `capture_channel` - The input capture channel for collecting timestamps. /// * `capture_channel` - The input capture channel for collecting timestamps.
/// * `sampling_timer` - The stabilizer ADC sampling timer. /// * `sampling_timer` - The stabilizer ADC sampling timer.
/// * `_clock_input` - The input pin for the external clock from Pounder. /// * `_clock_input` - The input pin for the external clock from Pounder.
@ -65,18 +39,12 @@ impl Timestamper {
/// The new pounder timestamper in an operational state. /// The new pounder timestamper in an operational state.
pub fn new( pub fn new(
mut timestamp_timer: timers::PounderTimestampTimer, mut timestamp_timer: timers::PounderTimestampTimer,
stream: hal::dma::dma::Stream0<hal::stm32::DMA2>,
capture_channel: timers::tim8::Channel1, capture_channel: timers::tim8::Channel1,
sampling_timer: &mut timers::SamplingTimer, sampling_timer: &mut timers::SamplingTimer,
_clock_input: hal::gpio::gpioa::PA0< _clock_input: hal::gpio::gpioa::PA0<
hal::gpio::Alternate<hal::gpio::AF3>, hal::gpio::Alternate<hal::gpio::AF3>,
>, >,
) -> Self { ) -> Self {
let config = DmaConfig::default()
.memory_increment(true)
.circular_buffer(true)
.double_buffer(true);
// The sampling timer should generate a trigger output when CH1 comparison occurs. // The sampling timer should generate a trigger output when CH1 comparison occurs.
sampling_timer.generate_trigger(timers::TriggerGenerator::ComparePulse); sampling_timer.generate_trigger(timers::TriggerGenerator::ComparePulse);
@ -85,64 +53,39 @@ impl Timestamper {
timestamp_timer.set_trigger_source(timers::TriggerSource::Trigger1); timestamp_timer.set_trigger_source(timers::TriggerSource::Trigger1);
// The capture channel should capture whenever the trigger input occurs. // The capture channel should capture whenever the trigger input occurs.
let input_capture = capture_channel let mut input_capture = capture_channel
.into_input_capture(timers::tim8::CaptureSource1::TRC); .into_input_capture(timers::tim8::CaptureSource1::TRC);
input_capture.listen_dma();
// The data transfer is always a transfer of data from the peripheral to a RAM buffer. // Capture at the batch period.
let data_transfer: Transfer<_, _, PeripheralToMemory, _, _> = input_capture.configure_prescaler(
Transfer::init( timers::Prescaler::try_from(
stream, design_parameters::SAMPLE_BUFFER_SIZE_LOG2,
input_capture, )
// Note(unsafe): BUF[0] and BUF[1] are "owned" by this peripheral. .unwrap(),
// They shall not be used anywhere else in the module. );
unsafe { &mut BUF[0] },
unsafe { Some(&mut BUF[1]) },
config,
);
Self { Self {
timer: timestamp_timer, timer: timestamp_timer,
transfer: data_transfer, capture_channel: input_capture,
// Note(unsafe): BUF[2] is "owned" by this peripheral. It shall not be used anywhere
// else in the module.
next_buffer: unsafe { Some(&mut BUF[2]) },
} }
} }
/// Start the DMA transfer for collecting timestamps. /// Start collecting timestamps.
#[allow(dead_code)]
pub fn start(&mut self) { pub fn start(&mut self) {
self.transfer self.capture_channel.enable();
.start(|capture_channel| capture_channel.enable());
} }
/// Update the period of the underlying timestamp timer. /// Update the period of the underlying timestamp timer.
#[allow(dead_code)]
pub fn update_period(&mut self, period: u16) { pub fn update_period(&mut self, period: u16) {
self.timer.set_period_ticks(period); self.timer.set_period_ticks(period);
} }
/// Obtain a buffer filled with timestamps. /// Obtain a timestamp.
/// ///
/// # Returns /// # Returns
/// A reference to the underlying buffer that has been filled with timestamps. /// A `Result` potentially indicating capture overflow and containing a `Option` of a captured
#[allow(dead_code)] /// timestamp.
pub fn acquire_buffer(&mut self) -> &[u16; SAMPLE_BUFFER_SIZE] { pub fn latest_timestamp(&mut self) -> Result<Option<u16>, Option<u16>> {
// Wait for the transfer to fully complete before continuing. self.capture_channel.latest_capture()
// Note: If a device hangs up, check that this conditional is passing correctly, as there is
// no time-out checks here in the interest of execution speed.
while !self.transfer.get_transfer_complete_flag() {}
let next_buffer = self.next_buffer.take().unwrap();
// Start the next transfer.
let (prev_buffer, _, _) =
self.transfer.next_transfer(next_buffer).unwrap();
self.next_buffer.replace(prev_buffer); // .unwrap_none() https://github.com/rust-lang/rust/issues/62633
self.next_buffer.as_ref().unwrap()
} }
} }

View File

@ -1,5 +1,6 @@
///! The sampling timer is used for managing ADC sampling and external reference timestamping. ///! The sampling timer is used for managing ADC sampling and external reference timestamping.
use super::hal; use super::hal;
use num_enum::TryFromPrimitive;
use hal::stm32::{ use hal::stm32::{
// TIM1 and TIM8 have identical registers. // TIM1 and TIM8 have identical registers.
@ -34,6 +35,8 @@ pub enum TriggerSource {
/// Prescalers for externally-supplied reference clocks. /// Prescalers for externally-supplied reference clocks.
#[allow(dead_code)] #[allow(dead_code)]
#[derive(TryFromPrimitive)]
#[repr(u8)]
pub enum Prescaler { pub enum Prescaler {
Div1 = 0b00, Div1 = 0b00,
Div2 = 0b01, Div2 = 0b01,
@ -353,6 +356,21 @@ macro_rules! timer_channels {
let regs = unsafe { &*<$TY>::ptr() }; let regs = unsafe { &*<$TY>::ptr() };
regs.[< $ccmrx _input >]().modify(|_, w| w.[< ic $index f >]().bits(filter as u8)); regs.[< $ccmrx _input >]().modify(|_, w| w.[< ic $index f >]().bits(filter as u8));
} }
/// Configure the input capture prescaler.
///
/// # Args
/// * `psc` - Prescaler exponent.
#[allow(dead_code)]
pub fn configure_prescaler(&mut self, prescaler: super::Prescaler) {
// Note(unsafe): This channel owns all access to the specific timer channel.
// Only atomic operations on completed on the timer registers.
let regs = unsafe { &*<$TY>::ptr() };
// Note(unsafe): Enum values are all valid.
#[allow(unused_unsafe)]
regs.[< $ccmrx _input >]().modify(|_, w| unsafe {
w.[< ic $index psc >]().bits(prescaler as u8)});
}
} }
// Note(unsafe): This manually implements DMA support for input-capture channels. This // Note(unsafe): This manually implements DMA support for input-capture channels. This

View File

@ -1,8 +1,18 @@
#![no_std] #![no_std]
#![cfg_attr(feature = "nightly", feature(core_intrinsics))] #![cfg_attr(feature = "nightly", feature(core_intrinsics))]
#[macro_use]
extern crate log;
pub mod hardware; pub mod hardware;
pub mod net; pub mod net;
/// Macro to reduce rightward drift when calling the same closure-based API
/// on multiple structs simultaneously, e.g. when accessing DMA buffers.
/// This could be improved a bit using the tuple-based style from `mutex-trait`.
#[macro_export]
macro_rules! flatten_closures {
($fn:ident, $e:ident, $fun:block) => {
$e.$fn(|$e| $fun ).unwrap()
};
($fn:ident, $e:ident, $($es:ident),+, $fun:block) => {
$e.$fn(|$e| flatten_closures!($fn, $($es),*, $fun)).unwrap()
};
}

View File

@ -11,6 +11,7 @@
///! Respones to settings updates are sent without quality-of-service guarantees, so there's no ///! 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. ///! guarantee that the requestee will be informed that settings have been applied.
use heapless::String; use heapless::String;
use log::info;
use super::{MqttMessage, NetworkReference, SettingsResponse, UpdateState}; use super::{MqttMessage, NetworkReference, SettingsResponse, UpdateState};
use crate::hardware::design_parameters::MQTT_BROKER; use crate::hardware::design_parameters::MQTT_BROKER;
@ -102,7 +103,7 @@ where
let path = match topic.strip_prefix(prefix) { let path = match topic.strip_prefix(prefix) {
// For paths, we do not want to include the leading slash. // For paths, we do not want to include the leading slash.
Some(path) => { Some(path) => {
if path.len() > 0 { if !path.is_empty() {
&path[1..] &path[1..]
} else { } else {
path path
@ -116,9 +117,8 @@ where
let message: SettingsResponse = settings let message: SettingsResponse = settings
.string_set(path.split('/').peekable(), message) .string_set(path.split('/').peekable(), message)
.and_then(|_| { .map(|_| {
update = true; update = true;
Ok(())
}) })
.into(); .into();