feat(firmware): validate configuration and migrate sensor-driven flight APIs
Validate configuration through its owning schemas and keep board declarations separate from shared F722 resource ownership. Bound USB transitions, isolate transport failures and retain restart-only activation. Migrate navigation and flight consumers with the sensor and schema contracts. Require explicit installation data and synchronous output authority; board motor outputs remain inhibited. Validation: assembled-prefix host tests, strict Clippy, platform/USB tests and both ARM release builds. Final-source coverage and scoped Miri/Kani evidence are retained separately from hardware qualification.
This commit is contained in:
1 parent
fb1b8af2d1
commit
f9a61a086f
211 files changed
+45657
-12740
No files matched your search
@@ -8,18 +8,12 @@ version.workspace = true
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
# This is a bare-metal `no_std` platform library with its own panic handler.
|
||||
# Host-verifiable invariants live in `tests/platform_safety_boundary.rs`; a
|
||||
# generated lib-test harness would link `std` and a second panic handler.
|
||||
test = false
|
||||
|
||||
[dependencies]
|
||||
kaptan-err-reportable = { path = "../../libs/kaptan-err-reportable" }
|
||||
kaptan-config = { path = "../../libs/kaptan-config" }
|
||||
kaptan-ports = { path = "../../libs/kaptan-ports" }
|
||||
kaptan-file-mavlink = { path = "../../libs/kaptan-file-mavlink" }
|
||||
kaptan-data-channel = { path = "../../libs/kaptan-data-channel" }
|
||||
kaptan-dma = { path = "../../libs/kaptan-dma" }
|
||||
kaptan-drivers = { path = "../../libs/kaptan-drivers" }
|
||||
kaptan-i2c = { path = "../../libs/kaptan-i2c" }
|
||||
kaptan-log = { path = "../../libs/kaptan-log" }
|
||||
@@ -29,7 +23,8 @@ kaptan-sensor = { path = "../../libs/kaptan-sensor" }
|
||||
kaptan-spi = { path = "../../libs/kaptan-spi" }
|
||||
kaptan-time = { path = "../../libs/kaptan-time" }
|
||||
kaptan-tx = { path = "../../libs/kaptan-tx" }
|
||||
cortex-m = "0.7"
|
||||
cortex-m = { version = "0.7", features = ["critical-section-single-core"] }
|
||||
critical-section = "1.2"
|
||||
cortex-m-rt = "0.7"
|
||||
rtic = { version = "2.3", default-features = false, features = [
|
||||
"thumbv7-backend",
|
||||
@@ -42,3 +37,6 @@ synopsys-usb-otg = { version = "0.4", features = ["cortex-m", "fs"] }
|
||||
usb-device = "0.3"
|
||||
usbd-serial = "0.2"
|
||||
panic-halt = "1"
|
||||
|
||||
[target.'cfg(not(target_os = "none"))'.dev-dependencies]
|
||||
critical-section = { version = "1.2", features = ["std"] }
|
||||
@@ -0,0 +1,32 @@
|
||||
#![cfg_attr(target_os = "none", no_std)]
|
||||
#![cfg_attr(target_os = "none", no_main)]
|
||||
#[cfg(target_os = "none")]
|
||||
mod support;
|
||||
#[cfg(target_os = "none")]
|
||||
mod target {
|
||||
use platform_stm32f7::kaptan_ports::PortCatalog;
|
||||
use platform_stm32f7::*;
|
||||
pub struct Board;
|
||||
impl F7Board for Board {
|
||||
type UserConfig = super::support::UserConfig;
|
||||
type ImmutableConfig = F722ImmutableConfig<Icm42688Spi3>;
|
||||
const FACTORY_USER_CONFIG: Self::UserConfig = super::support::FACTORY;
|
||||
const IMMUTABLE_CONFIG: Self::ImmutableConfig = F722ImmutableConfig {
|
||||
name: "synthetic ICM dual UART",
|
||||
service_hz: 100,
|
||||
hardware: F722Hardware::new(
|
||||
Icm42688Spi3 {
|
||||
mount: ImuMount::Yaw180,
|
||||
},
|
||||
SerialInventory::new(&super::support::Both::DESCRIPTORS),
|
||||
StatusLed::ActiveLowPd2,
|
||||
VerticalSensor::None,
|
||||
MotorPins::Four,
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
#[cfg(target_os = "none")]
|
||||
platform_stm32f7::flight_firmware!(crate::target::Board);
|
||||
#[cfg(not(target_os = "none"))]
|
||||
fn main() {}
|
||||
@@ -0,0 +1,38 @@
|
||||
#![cfg_attr(target_os = "none", no_std)]
|
||||
#![cfg_attr(target_os = "none", no_main)]
|
||||
#[cfg(target_os = "none")]
|
||||
mod support;
|
||||
#[cfg(target_os = "none")]
|
||||
mod target {
|
||||
use platform_stm32f7::kaptan_ports::PortCatalog;
|
||||
use platform_stm32f7::*;
|
||||
pub struct Board;
|
||||
impl F7Board for Board {
|
||||
type UserConfig = super::support::UserConfig;
|
||||
type ImmutableConfig = F722ImmutableConfig<Mpu6000Spi1>;
|
||||
const FACTORY_USER_CONFIG: Self::UserConfig = super::support::FACTORY;
|
||||
const IMMUTABLE_CONFIG: Self::ImmutableConfig = F722ImmutableConfig {
|
||||
name: "synthetic MPU dual UART",
|
||||
service_hz: 100,
|
||||
hardware: F722Hardware::new(
|
||||
Mpu6000Spi1 {
|
||||
mount: ImuMount::BodyAligned,
|
||||
config: Mpu6000Config {
|
||||
accelerometer: AccelerometerRange::PlusMinus4G,
|
||||
gyroscope: GyroscopeRange::PlusMinus500DegreesPerSecond,
|
||||
digital_low_pass_filter: DigitalLowPassFilter::Bandwidth42Hz,
|
||||
sample_rate_divider: 9,
|
||||
},
|
||||
},
|
||||
SerialInventory::new(&super::support::Both::DESCRIPTORS),
|
||||
StatusLed::ActiveHighPa13,
|
||||
VerticalSensor::None,
|
||||
MotorPins::Four,
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
#[cfg(target_os = "none")]
|
||||
platform_stm32f7::flight_firmware!(crate::target::Board);
|
||||
#[cfg(not(target_os = "none"))]
|
||||
fn main() {}
|
||||
@@ -0,0 +1,11 @@
|
||||
//! UART catalog for compile-check examples.
|
||||
use platform_stm32f7::{
|
||||
F722UserConfig, UART1, UART2,
|
||||
kaptan_ports::{PortCatalog, PortDescriptor},
|
||||
};
|
||||
pub struct Both;
|
||||
impl PortCatalog<2> for Both {
|
||||
const DESCRIPTORS: [PortDescriptor; 2] = [UART1, UART2];
|
||||
}
|
||||
pub type UserConfig = F722UserConfig<Both, 2>;
|
||||
pub const FACTORY: UserConfig = UserConfig::FACTORY;
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,6 +1,6 @@
|
||||
//! STM32F722 Pll216 clock-source assembly.
|
||||
|
||||
use super::peripherals::{DwtMicrosecondClock, select_pll216_profile};
|
||||
use super::peripherals::{Tim2Periodic, TimerClock, select_pll216_profile};
|
||||
use kaptan_err_reportable::KaptanError;
|
||||
use stm32f7::stm32f722;
|
||||
|
||||
@@ -21,11 +21,31 @@ pub(crate) enum ClockInitializationError {
|
||||
/// Selects the F722 Pll216 profile and starts its monotonic microsecond clock.
|
||||
pub(crate) fn initialize(
|
||||
core: &mut cortex_m::Peripherals,
|
||||
device: &stm32f722::Peripherals,
|
||||
) -> Result<DwtMicrosecondClock, ClockInitializationError> {
|
||||
select_pll216_profile(&device.RCC, &device.PWR, &device.FLASH)
|
||||
.map_err(|_| ClockInitializationError::Profile)?;
|
||||
rcc: &stm32f722::RCC,
|
||||
pwr: &stm32f722::PWR,
|
||||
flash: &stm32f722::FLASH,
|
||||
tim2: stm32f722::TIM2,
|
||||
dbgmcu: &stm32f722::DBGMCU,
|
||||
) -> Result<(TimerClock, Tim2Periodic), ClockInitializationError> {
|
||||
// Even a clock-profile failure must not leave an inherited TIM2 NVIC IRQ
|
||||
// enabled while RTIC resources are unpublished.
|
||||
cortex_m::peripheral::NVIC::mask(stm32f722::Interrupt::TIM2);
|
||||
core.SCB.clear_sleepdeep();
|
||||
select_pll216_profile(rcc, pwr, flash).map_err(|_| ClockInitializationError::Profile)?;
|
||||
core.SCB.disable_dcache(&mut core.CPUID);
|
||||
DwtMicrosecondClock::start::<CORE_CLOCK_MHZ>(&mut core.DCB, &mut core.DWT)
|
||||
.map_err(|_| ClockInitializationError::Monotonic)
|
||||
|
||||
// Firmware executes through the AXI Flash mapping, outside the ART
|
||||
// accelerator. Clear inherited instruction state before enabling the core
|
||||
// I-cache. The helpers provide the required DSB/ISB barriers.
|
||||
core.SCB.invalidate_icache();
|
||||
core.SCB.enable_icache();
|
||||
|
||||
// DWT remains solely for the existing SRAM flash-poll timeout. That path
|
||||
// runs the CPU continuously at this fixed 216 MHz and never sleeps.
|
||||
core.DCB.enable_trace();
|
||||
cortex_m::peripheral::DWT::unlock();
|
||||
core.DWT.set_cycle_count(0);
|
||||
core.DWT.enable_cycle_counter();
|
||||
|
||||
Tim2Periodic::new(rcc, tim2, dbgmcu).ok_or(ClockInitializationError::Monotonic)
|
||||
}
|
||||
@@ -1,18 +1,107 @@
|
||||
//! Board-owned user configuration support and the generic F722 flash journal.
|
||||
//! F722 user configuration and flash journal.
|
||||
|
||||
use core::{marker::PhantomData, mem};
|
||||
use kaptan_config::{
|
||||
Config, ConfigManager, ConfigModel, ConfigValue, ConfigViolation, DualSlotJournal, DualSlotNor,
|
||||
FieldDescriptor, FieldId, FieldWriteError, JournalError, KcfgDecodeError, KcfgExportPlan,
|
||||
KcfgExporter, KcfgImporter, KcfgResultDetail, ReplaceError, ReplaceOutcome, ValidationRule,
|
||||
kcfg_length,
|
||||
Config, ConfigManager, ConfigModel, ConfigStore, ConfigValue, ConfigViolation, DualSlotJournal,
|
||||
DualSlotNor, FieldDescriptor, FieldId, FieldWriteError, JournalError, KcfgDecodeError,
|
||||
KcfgExportPlan, KcfgExporter, KcfgImporter, KcfgResultDetail, KcfgUpdate, ReplaceError,
|
||||
ReplaceOutcome, StartupError, ValidationRule, kcfg_length,
|
||||
};
|
||||
use kaptan_file_mavlink::{
|
||||
CONTENT_BYTES, Content, FileFailure, FileMetadata, FileProvider, FileStatus,
|
||||
};
|
||||
use stm32f7::stm32f722;
|
||||
|
||||
use super::ports::{disable_ports, project_ports};
|
||||
use crate::{F7ImmutableConfig, LevelFilter};
|
||||
use kaptan_ports::{PortCatalog, PortPlan, PortSettings, Ports, Role};
|
||||
|
||||
#[derive(Clone, Config)]
|
||||
struct MavlinkConfig {
|
||||
#[config(id = 101, key = "mavlink.system_id", unit = "id", range(min = 1))]
|
||||
system_id: u8,
|
||||
#[config(id = 102, key = "mavlink.component_id", unit = "id", range(min = 1))]
|
||||
component_id: u8,
|
||||
#[config(
|
||||
enum,
|
||||
id = 105,
|
||||
key = "mavlink.logging.level_filter",
|
||||
unit = "log_level"
|
||||
)]
|
||||
log_level_filter: LevelFilter,
|
||||
}
|
||||
|
||||
#[derive(Clone, Config)]
|
||||
struct TelemetryConfig {
|
||||
#[config(
|
||||
id = 103,
|
||||
key = "telemetry.imu_hz",
|
||||
unit = "Hz",
|
||||
range(min = 1, max = 100)
|
||||
)]
|
||||
imu_hz: u8,
|
||||
#[config(
|
||||
id = 104,
|
||||
key = "telemetry.heartbeat_hz",
|
||||
unit = "Hz",
|
||||
range(min = 1, max = 10)
|
||||
)]
|
||||
heartbeat_hz: u8,
|
||||
#[config(
|
||||
id = 106,
|
||||
key = "telemetry.range_hz",
|
||||
unit = "Hz",
|
||||
range(min = 1, max = 100)
|
||||
)]
|
||||
range_hz: u8,
|
||||
#[config(
|
||||
id = 107,
|
||||
key = "telemetry.optical_flow_hz",
|
||||
unit = "Hz",
|
||||
range(min = 1, max = 100)
|
||||
)]
|
||||
optical_flow_hz: u8,
|
||||
}
|
||||
|
||||
/// User settings applied at boot.
|
||||
#[derive(Config)]
|
||||
pub struct F722UserConfig<C: PortCatalog<N>, const N: usize> {
|
||||
mavlink: MavlinkConfig,
|
||||
telemetry: TelemetryConfig,
|
||||
ports: Ports<C, N>,
|
||||
}
|
||||
|
||||
impl<C: PortCatalog<N>, const N: usize> Clone for F722UserConfig<C, N> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
mavlink: self.mavlink.clone(),
|
||||
telemetry: self.telemetry.clone(),
|
||||
ports: self.ports,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: PortCatalog<N>, const N: usize> F722UserConfig<C, N> {
|
||||
pub const FACTORY: Self = Self {
|
||||
mavlink: MavlinkConfig {
|
||||
system_id: 1,
|
||||
component_id: 1,
|
||||
log_level_filter: LevelFilter::Info,
|
||||
},
|
||||
telemetry: TelemetryConfig {
|
||||
imu_hz: 20,
|
||||
heartbeat_hz: 2,
|
||||
range_hz: 20,
|
||||
optical_flow_hz: 20,
|
||||
},
|
||||
ports: Ports::new(
|
||||
[PortSettings {
|
||||
role: Role::Disabled,
|
||||
baud: 115_200,
|
||||
}; N],
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
const MAVLINK_SYSTEM_ID: FieldId = FieldId::new(101);
|
||||
const MAVLINK_COMPONENT_ID: FieldId = FieldId::new(102);
|
||||
@@ -22,8 +111,16 @@ const MAVLINK_LOG_LEVEL_FILTER: FieldId = FieldId::new(105);
|
||||
const RANGE_HZ: FieldId = FieldId::new(106);
|
||||
const OPTICAL_FLOW_HZ: FieldId = FieldId::new(107);
|
||||
const USER_CONFIG_PATH: &[u8] = b"/config/user.kcfg";
|
||||
const ACTIVE_CONFIG_PATH: &[u8] = b"/config/active.kcfg";
|
||||
const RESET_CONFIG_PATH: &[u8] = b"/config/reset.kcfg";
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct BootActive<C, const N: usize> {
|
||||
pub(crate) candidate: C,
|
||||
pub(crate) runtime: F722UserRuntime,
|
||||
pub(crate) ports: PortPlan<N>,
|
||||
}
|
||||
|
||||
/// Narrow boot projection consumed only by fixed F722 services.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub(super) struct F722UserRuntime {
|
||||
pub(super) system_id: u8,
|
||||
@@ -35,14 +132,13 @@ pub(super) struct F722UserRuntime {
|
||||
pub(super) heartbeat_hz: u8,
|
||||
}
|
||||
|
||||
/// Generic [`ConfigModel`] adapter for one board-composed aggregate.
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct UserConfigModel<C, I> {
|
||||
pub(crate) struct UserConfigModel<C, I, const N: usize> {
|
||||
candidate: C,
|
||||
_immutable: PhantomData<I>,
|
||||
}
|
||||
|
||||
impl<C, I> UserConfigModel<C, I> {
|
||||
impl<C, I, const N: usize> UserConfigModel<C, I, N> {
|
||||
pub(crate) const fn new(candidate: C) -> Self {
|
||||
Self {
|
||||
candidate,
|
||||
@@ -51,12 +147,12 @@ impl<C, I> UserConfigModel<C, I> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<C, I> ConfigModel for UserConfigModel<C, I>
|
||||
impl<C, I, const N: usize> ConfigModel for UserConfigModel<C, I, N>
|
||||
where
|
||||
C: Clone + Config,
|
||||
I: F7ImmutableConfig,
|
||||
{
|
||||
type Active = C;
|
||||
type Active = BootActive<C, N>;
|
||||
type Context = I;
|
||||
|
||||
const SCHEMA_ID: [u8; 16] = <C as Config>::SCHEMA_ID;
|
||||
@@ -78,7 +174,7 @@ where
|
||||
}
|
||||
|
||||
fn active_field_value(active: &Self::Active, id: FieldId) -> Option<ConfigValue> {
|
||||
active.field_value(id)
|
||||
active.candidate.field_value(id)
|
||||
}
|
||||
|
||||
fn set_field(&mut self, id: FieldId, value: ConfigValue) -> Result<(), FieldWriteError> {
|
||||
@@ -87,8 +183,17 @@ where
|
||||
|
||||
fn validate(&self, context: &Self::Context) -> Result<Self::Active, ConfigViolation> {
|
||||
<C as Config>::validate(&self.candidate)?;
|
||||
let _ = project_runtime(&self.candidate, context)?;
|
||||
Ok(self.candidate.clone())
|
||||
let runtime = project_runtime(&self.candidate, context)?;
|
||||
let ports = project_ports(
|
||||
&self.candidate,
|
||||
context.f722().hardware.serial(),
|
||||
scheduled_wire_bytes(runtime),
|
||||
)?;
|
||||
Ok(BootActive {
|
||||
candidate: self.candidate.clone(),
|
||||
runtime,
|
||||
ports,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,12 +226,25 @@ where
|
||||
let mavlink_log_level_filter = LevelFilter::try_from_wire(level_filter).ok_or(
|
||||
ConfigViolation::field(MAVLINK_LOG_LEVEL_FILTER, ValidationRule::InvalidEnumValue),
|
||||
)?;
|
||||
let range_hz = if immutable.hardware.has_range() {
|
||||
// Full port validation follows with this runtime's publication budget.
|
||||
let mtf01 = immutable
|
||||
.hardware
|
||||
.serial()
|
||||
.descriptors()
|
||||
.iter()
|
||||
.any(|port| {
|
||||
candidate.field_value(port.role_field)
|
||||
== Some(ConfigValue::U8(Role::Mtf01.wire_value()))
|
||||
});
|
||||
if mtf01 && immutable.hardware.has_range() {
|
||||
return Err(ConfigViolation::global(ValidationRule::Inconsistent));
|
||||
}
|
||||
let range_hz = if immutable.hardware.has_range() || mtf01 {
|
||||
Some(required_u8(candidate, RANGE_HZ)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let optical_flow_hz = if immutable.hardware.has_optical_flow() {
|
||||
let optical_flow_hz = if mtf01 {
|
||||
Some(required_u8(candidate, OPTICAL_FLOW_HZ)?)
|
||||
} else {
|
||||
None
|
||||
@@ -153,6 +271,16 @@ where
|
||||
})
|
||||
}
|
||||
|
||||
fn scheduled_wire_bytes(runtime: F722UserRuntime) -> u32 {
|
||||
use kaptan_mavlink::generated::kaptan;
|
||||
// Unsigned MAVLink2 header + CRC; no minimum-payload truncation assumption.
|
||||
u32::from(runtime.imu_hz) * (12 + kaptan::IMU_DATA::ENCODED_LEN as u32)
|
||||
+ u32::from(runtime.heartbeat_hz) * (12 + kaptan::HEARTBEAT_DATA::ENCODED_LEN as u32)
|
||||
+ u32::from(runtime.range_hz.unwrap_or(0)) * (12 + kaptan::RANGE_DATA::ENCODED_LEN as u32)
|
||||
+ u32::from(runtime.optical_flow_hz.unwrap_or(0))
|
||||
* (12 + kaptan::OPTICAL_FLOW_DATA::ENCODED_LEN as u32)
|
||||
}
|
||||
|
||||
fn required_u8<C: Config>(candidate: &C, field: FieldId) -> Result<u8, ConfigViolation> {
|
||||
match candidate.field_value(field) {
|
||||
Some(ConfigValue::U8(value)) => Ok(value),
|
||||
@@ -192,24 +320,36 @@ const FLASH_KEY1: u32 = 0x4567_0123;
|
||||
const FLASH_KEY2: u32 = 0xcdef_89ab;
|
||||
const FLASH_ERROR_FLAGS: u32 = 0x01f2;
|
||||
// The F722 datasheet bounds one x32 128-KiB sector erase at 1.322 seconds.
|
||||
// The already-running 216-MHz DWT counter gives every operation a two-second
|
||||
// deadline without turning an instruction-count guess into a timing contract.
|
||||
// Allow two seconds, measured by DWT at the fixed 216 MHz core frequency.
|
||||
const FLASH_OPERATION_TIMEOUT_CYCLES: u32 = 2 * 216_000_000;
|
||||
|
||||
/// One schema-independent journal shared by every STM32F722 board image.
|
||||
pub(crate) type F722ConfigStore<const MAX_PAYLOAD_BYTES: usize> =
|
||||
DualSlotJournal<F722Nor, MAX_PAYLOAD_BYTES>;
|
||||
|
||||
/// Concrete manager storage selected when the board macro monomorphizes the
|
||||
/// common platform task graph for its aggregate and exact snapshot size.
|
||||
pub(crate) type F722ConfigManager<C, I, const MAX_PAYLOAD_BYTES: usize> =
|
||||
ConfigManager<UserConfigModel<C, I>, F722ConfigStore<MAX_PAYLOAD_BYTES>>;
|
||||
pub(crate) type BootDiagnostic = kaptan_config::RecoveryDisposition;
|
||||
type UserConfigManager<C, I, const N: usize, S> = ConfigManager<UserConfigModel<C, I, N>, S>;
|
||||
|
||||
/// Recovers the generic manager using the platform's explicit USB-only fallback.
|
||||
pub(crate) fn recover_configuration<C, I, const N: usize, S>(
|
||||
factory: UserConfigModel<C, I, N>,
|
||||
context: I,
|
||||
store: S,
|
||||
) -> Result<UserConfigManager<C, I, N, S>, StartupError<S::Error>>
|
||||
where
|
||||
C: Clone + Config,
|
||||
I: F7ImmutableConfig,
|
||||
S: ConfigStore<UserConfigModel<C, I, N>>,
|
||||
{
|
||||
ConfigManager::recover_with(factory, context, store, |factory, context, _| {
|
||||
let mut setup = factory.clone();
|
||||
disable_ports(&mut setup.candidate, context.f722().hardware.serial());
|
||||
Some(setup)
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug, kaptan_err_reportable::KaptanError)]
|
||||
enum PersistConfigurationError {
|
||||
/// Reserved journal sectors cannot hold the configured payload bound.
|
||||
#[kaptan_error(id = 2210)]
|
||||
JournalGeometry,
|
||||
/// The compiled configuration model cannot produce a private snapshot.
|
||||
#[kaptan_error(id = 2202)]
|
||||
ConfigurationModel,
|
||||
@@ -231,6 +371,12 @@ enum PersistConfigurationError {
|
||||
/// A complete journal record failed post-write verification.
|
||||
#[kaptan_error(id = 2209)]
|
||||
JournalVerification,
|
||||
/// Reserved journal sectors cannot hold the configured payload bound.
|
||||
#[kaptan_error(id = 2210)]
|
||||
JournalGeometry,
|
||||
/// Unknown durability requires recovery or explicit factory reset before saving.
|
||||
#[kaptan_error(id = 2211)]
|
||||
DurableStateUnknown,
|
||||
}
|
||||
|
||||
impl From<JournalError<F722FlashError>> for PersistConfigurationError {
|
||||
@@ -250,30 +396,47 @@ impl From<JournalError<F722FlashError>> for PersistConfigurationError {
|
||||
|
||||
enum ConfigFileTransfer<C: ConfigModel> {
|
||||
Idle,
|
||||
Read(KcfgExportPlan),
|
||||
Write(KcfgImporter<C>),
|
||||
Read {
|
||||
plan: KcfgExportPlan,
|
||||
active: bool,
|
||||
},
|
||||
Write {
|
||||
importer: KcfgImporter<C>,
|
||||
reset: bool,
|
||||
},
|
||||
}
|
||||
|
||||
/// Streaming KCFG endpoint over the private power-loss-safe flash journal.
|
||||
///
|
||||
/// The endpoint retains one typed candidate while writing and generates read
|
||||
/// chunks directly from compiled metadata. It never buffers the public file.
|
||||
pub(crate) struct F722ConfigFile<C, I, const MAX_PAYLOAD_BYTES: usize>
|
||||
where
|
||||
/// The endpoint retains requested and expected-base candidates while writing
|
||||
/// and generates read chunks from metadata. It never buffers the public file.
|
||||
/// User writes are guarded full replacements. Reset writes deliberately ignore
|
||||
/// the base, but accept only exact factory values. Active is read-only.
|
||||
pub(crate) struct F722ConfigFile<
|
||||
C,
|
||||
I,
|
||||
const N: usize,
|
||||
const MAX_PAYLOAD_BYTES: usize,
|
||||
S = F722ConfigStore<MAX_PAYLOAD_BYTES>,
|
||||
> where
|
||||
C: Clone + Config,
|
||||
I: F7ImmutableConfig,
|
||||
{
|
||||
manager: F722ConfigManager<C, I, MAX_PAYLOAD_BYTES>,
|
||||
transfer: ConfigFileTransfer<UserConfigModel<C, I>>,
|
||||
manager: UserConfigManager<C, I, N, S>,
|
||||
active_export: UserConfigModel<C, I, N>,
|
||||
transfer: ConfigFileTransfer<UserConfigModel<C, I, N>>,
|
||||
}
|
||||
|
||||
impl<C, I, const MAX_PAYLOAD_BYTES: usize> F722ConfigFile<C, I, MAX_PAYLOAD_BYTES>
|
||||
impl<C, I, const N: usize, const MAX_PAYLOAD_BYTES: usize, S>
|
||||
F722ConfigFile<C, I, N, MAX_PAYLOAD_BYTES, S>
|
||||
where
|
||||
C: Clone + Config,
|
||||
I: F7ImmutableConfig,
|
||||
S: ConfigStore<UserConfigModel<C, I, N>, Error = JournalError<F722FlashError>>,
|
||||
{
|
||||
pub(crate) const fn new(manager: F722ConfigManager<C, I, MAX_PAYLOAD_BYTES>) -> Self {
|
||||
pub(crate) fn new(manager: UserConfigManager<C, I, N, S>) -> Self {
|
||||
Self {
|
||||
active_export: UserConfigModel::new(manager.active().candidate.clone()),
|
||||
manager,
|
||||
transfer: ConfigFileTransfer::Idle,
|
||||
}
|
||||
@@ -284,33 +447,75 @@ where
|
||||
#[kaptan_err_reportable::propagate]
|
||||
fn persist_candidate(
|
||||
&mut self,
|
||||
candidate: UserConfigModel<C, I>,
|
||||
) -> Result<Result<ReplaceOutcome, ConfigViolation>, PersistConfigurationError> {
|
||||
match self.manager.replace(candidate) {
|
||||
update: KcfgUpdate<UserConfigModel<C, I, N>>,
|
||||
reset: bool,
|
||||
) -> Result<Result<ReplaceOutcome, FileFailure>, PersistConfigurationError> {
|
||||
let result = if reset {
|
||||
if !self.manager.is_factory(&update.candidate) {
|
||||
return Ok(Err(kcfg_file_failure(
|
||||
FileStatus::ContentRejected,
|
||||
KcfgResultDetail::failure(),
|
||||
b"reset requires exact factory values",
|
||||
)));
|
||||
}
|
||||
self.manager.reset_to_factory()
|
||||
} else {
|
||||
self.manager
|
||||
.replace_if_current(update.candidate, &update.expected_base)
|
||||
};
|
||||
match result {
|
||||
Ok(outcome) => Ok(Ok(outcome)),
|
||||
Err(ReplaceError::InvalidCandidate(violation)) => Ok(Err(violation)),
|
||||
Err(ReplaceError::InvalidCandidate(violation)) => Ok(Err(kcfg_file_failure(
|
||||
FileStatus::ContentRejected,
|
||||
KcfgResultDetail::violation(violation),
|
||||
b"configuration validation failed",
|
||||
))),
|
||||
Err(ReplaceError::Conflict) => Ok(Err(kcfg_file_failure(
|
||||
FileStatus::RequestConflict,
|
||||
KcfgResultDetail::failure(),
|
||||
b"configuration changed; reload before saving",
|
||||
))),
|
||||
Err(ReplaceError::Store(error)) => Err(error.into()),
|
||||
Err(ReplaceError::DurableStateUnknown) => {
|
||||
Err(PersistConfigurationError::DurableStateUnknown)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn committed(&self) -> Result<&UserConfigModel<C, I, N>, FileFailure> {
|
||||
self.manager.committed().ok_or_else(|| {
|
||||
file_failure(
|
||||
FileStatus::StorageFailure,
|
||||
b"configuration durable state unknown",
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<C, I, const MAX_PAYLOAD_BYTES: usize> FileProvider for F722ConfigFile<C, I, MAX_PAYLOAD_BYTES>
|
||||
impl<C, I, const N: usize, const MAX_PAYLOAD_BYTES: usize, S> FileProvider
|
||||
for F722ConfigFile<C, I, N, MAX_PAYLOAD_BYTES, S>
|
||||
where
|
||||
C: Clone + Config,
|
||||
I: F7ImmutableConfig,
|
||||
S: ConfigStore<UserConfigModel<C, I, N>, Error = JournalError<F722FlashError>>,
|
||||
{
|
||||
fn open_read(
|
||||
&mut self,
|
||||
path: &kaptan_file_mavlink::FilePath,
|
||||
) -> Result<FileMetadata, FileFailure> {
|
||||
if path.as_bytes() != USER_CONFIG_PATH {
|
||||
let active = path.as_bytes() == ACTIVE_CONFIG_PATH;
|
||||
if !active && path.as_bytes() != USER_CONFIG_PATH {
|
||||
return Err(file_failure(FileStatus::NotFound, b"file not found"));
|
||||
}
|
||||
if !matches!(self.transfer, ConfigFileTransfer::Idle) {
|
||||
return Err(file_failure(FileStatus::Busy, b"file transfer busy"));
|
||||
}
|
||||
let exporter = KcfgExporter::new(
|
||||
self.manager.committed(),
|
||||
if active {
|
||||
&self.active_export
|
||||
} else {
|
||||
self.committed()?
|
||||
},
|
||||
self.manager.factory(),
|
||||
self.manager.restart_required(),
|
||||
)
|
||||
@@ -320,7 +525,10 @@ where
|
||||
.map_err(|_| file_failure(FileStatus::TooLarge, b"configuration file too large"))?,
|
||||
checksum_crc32c: exporter.checksum_crc32c(),
|
||||
};
|
||||
self.transfer = ConfigFileTransfer::Read(exporter.plan());
|
||||
self.transfer = ConfigFileTransfer::Read {
|
||||
plan: exporter.plan(),
|
||||
active,
|
||||
};
|
||||
Ok(metadata)
|
||||
}
|
||||
|
||||
@@ -329,13 +537,17 @@ where
|
||||
path: &kaptan_file_mavlink::FilePath,
|
||||
metadata: FileMetadata,
|
||||
) -> Result<(), FileFailure> {
|
||||
if path.as_bytes() != USER_CONFIG_PATH {
|
||||
let reset = path.as_bytes() == RESET_CONFIG_PATH;
|
||||
if !reset && path.as_bytes() != USER_CONFIG_PATH {
|
||||
return Err(file_failure(FileStatus::NotFound, b"file not found"));
|
||||
}
|
||||
if !matches!(self.transfer, ConfigFileTransfer::Idle) {
|
||||
return Err(file_failure(FileStatus::Busy, b"file transfer busy"));
|
||||
}
|
||||
let expected = kcfg_length::<UserConfigModel<C, I>>()
|
||||
if !reset {
|
||||
self.committed()?;
|
||||
}
|
||||
let expected = kcfg_length::<UserConfigModel<C, I, N>>()
|
||||
.map_err(|_| file_failure(FileStatus::Internal, b"cannot import configuration"))?;
|
||||
if usize::try_from(metadata.size) != Ok(expected) {
|
||||
return Err(kcfg_file_failure(
|
||||
@@ -346,19 +558,23 @@ where
|
||||
}
|
||||
let importer = KcfgImporter::new(self.manager.factory())
|
||||
.map_err(|_| file_failure(FileStatus::Internal, b"cannot import configuration"))?;
|
||||
self.transfer = ConfigFileTransfer::Write(importer);
|
||||
self.transfer = ConfigFileTransfer::Write { importer, reset };
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn read(&mut self, position: u32, output: &mut [u8]) -> Result<(), FileFailure> {
|
||||
let ConfigFileTransfer::Read(plan) = &self.transfer else {
|
||||
let ConfigFileTransfer::Read { plan, active } = &self.transfer else {
|
||||
return Err(file_failure(
|
||||
FileStatus::InvalidState,
|
||||
b"file is not open for read",
|
||||
));
|
||||
};
|
||||
let exporter = KcfgExporter::from_plan_unchecked(
|
||||
self.manager.committed(),
|
||||
if *active {
|
||||
&self.active_export
|
||||
} else {
|
||||
self.committed()?
|
||||
},
|
||||
self.manager.factory(),
|
||||
*plan,
|
||||
);
|
||||
@@ -375,7 +591,7 @@ where
|
||||
}
|
||||
|
||||
fn write(&mut self, _: u32, data: &[u8]) -> Result<(), FileFailure> {
|
||||
let ConfigFileTransfer::Write(importer) = &mut self.transfer else {
|
||||
let ConfigFileTransfer::Write { importer, .. } = &mut self.transfer else {
|
||||
return Err(file_failure(
|
||||
FileStatus::InvalidState,
|
||||
b"file is not open for write",
|
||||
@@ -387,25 +603,22 @@ where
|
||||
}
|
||||
|
||||
fn close(&mut self) -> Result<Content, FileFailure> {
|
||||
match mem::replace(&mut self.transfer, ConfigFileTransfer::Idle) {
|
||||
let transfer = mem::replace(&mut self.transfer, ConfigFileTransfer::Idle);
|
||||
match transfer {
|
||||
ConfigFileTransfer::Idle => {
|
||||
Err(file_failure(FileStatus::InvalidState, b"file is not open"))
|
||||
}
|
||||
ConfigFileTransfer::Read(_) => Ok(Content::EMPTY),
|
||||
ConfigFileTransfer::Write(importer) => {
|
||||
let candidate = importer.finish().map_err(kcfg_failure)?;
|
||||
let outcome = self
|
||||
.persist_candidate(candidate)
|
||||
.map_err(|_| {
|
||||
file_failure(FileStatus::StorageFailure, b"configuration storage failed")
|
||||
})?
|
||||
.map_err(|violation| {
|
||||
kcfg_file_failure(
|
||||
FileStatus::ContentRejected,
|
||||
KcfgResultDetail::violation(violation),
|
||||
b"configuration validation failed",
|
||||
)
|
||||
})?;
|
||||
ConfigFileTransfer::Read { active, .. } => {
|
||||
if !active {
|
||||
self.committed()?;
|
||||
}
|
||||
Ok(Content::EMPTY)
|
||||
}
|
||||
ConfigFileTransfer::Write { importer, reset } => {
|
||||
let update = importer.finish().map_err(kcfg_failure)?;
|
||||
let outcome = self.persist_candidate(update, reset).map_err(|_| {
|
||||
file_failure(FileStatus::StorageFailure, b"configuration storage failed")
|
||||
})??;
|
||||
Ok(kcfg_content(
|
||||
KcfgResultDetail::success(outcome.restart_required),
|
||||
b"",
|
||||
|
||||
@@ -1,162 +1,212 @@
|
||||
//! F722 transport and vertical-route adapters for the portable data channel.
|
||||
|
||||
use super::peripherals;
|
||||
use kaptan_dma::TxCompletion;
|
||||
use kaptan_mavlink::{
|
||||
MavlinkFrame, receiver::IntegrityCheckedFrame, transmitter::MavlinkTransmitter,
|
||||
//! Independent per-link scheduling, encoding, framing and bounded transmission.
|
||||
use super::{
|
||||
peripherals::{self, F722UsbMavlink, OwnedUart, ReceiveInterrupt},
|
||||
serial::{ReceiveEvent, RxChunk, SerialDevice, SerialIo},
|
||||
};
|
||||
use crate::SerialInventory;
|
||||
use kaptan_data_channel::{DataChannel, DataChannelRates};
|
||||
use kaptan_mavlink::{
|
||||
MavlinkFrame,
|
||||
receiver::{IntegrityCheckedFrame, IntegrityMavlinkReceiver},
|
||||
};
|
||||
use kaptan_ports::PortId;
|
||||
use kaptan_tx::PendingFrame;
|
||||
use peripherals::F722UsbMavlink;
|
||||
|
||||
// Baud, sensor topology, and mounting remain immutable build-time policy. The
|
||||
// output-inhibited board image may recover endpoint identity and selected
|
||||
// operational rates before this channel is constructed.
|
||||
pub(crate) const UART1_CLOCK_HZ: u32 = 108_000_000;
|
||||
pub(crate) const UART2_CLOCK_HZ: u32 = 54_000_000;
|
||||
pub(crate) const SERVICE_HZ: usize = 100;
|
||||
|
||||
/// Fixed service-timer configuration for the validated Pll216 profile.
|
||||
pub(crate) const SERVICE_TIMER_CONFIG: peripherals::Tim2PeriodicConfig =
|
||||
peripherals::Tim2PeriodicConfig {
|
||||
prescaler: 10_799,
|
||||
auto_reload: 99,
|
||||
};
|
||||
|
||||
/// The two F722 DMA engines behind the portable data-channel transport.
|
||||
enum F722MavlinkTransport {
|
||||
Usart1(MavlinkTransmitter<peripherals::DcacheDisabledDmaMemory, peripherals::Usart1TxDma, 4>),
|
||||
Usart2(MavlinkTransmitter<peripherals::DcacheDisabledDmaMemory, peripherals::Usart2TxDma, 4>),
|
||||
pub(crate) struct Link {
|
||||
pub(crate) channel: DataChannel<SERVICE_HZ>,
|
||||
pub(crate) receiver: IntegrityMavlinkReceiver<2>,
|
||||
}
|
||||
impl Link {
|
||||
pub(crate) fn new(
|
||||
system: u8,
|
||||
component: u8,
|
||||
rates: DataChannelRates,
|
||||
) -> Result<Self, crate::BoardError> {
|
||||
Ok(Self {
|
||||
channel: DataChannel::new(system, component, rates, 100_000)
|
||||
.map_err(|_| crate::BoardError::Initialization)?,
|
||||
receiver: IntegrityMavlinkReceiver::new(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Physical link on which an inbound frame arrived.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub(crate) enum MavlinkIngress {
|
||||
Uart,
|
||||
Usb,
|
||||
impl SerialDevice for OwnedUart {
|
||||
fn receive(&mut self) -> ReceiveEvent {
|
||||
match OwnedUart::receive(self) {
|
||||
ReceiveInterrupt::Byte(byte) => ReceiveEvent::Byte(byte),
|
||||
ReceiveInterrupt::Fault => ReceiveEvent::Fault,
|
||||
ReceiveInterrupt::NotPending => ReceiveEvent::Idle,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Concrete F722 MAVLink link ownership.
|
||||
pub(crate) struct F722MavlinkLink {
|
||||
uart: F722MavlinkTransport,
|
||||
/// Bounded data exchanged between the worker and I/O owners. Decoding,
|
||||
/// scheduling and persistence cannot borrow the physical owners through this API.
|
||||
#[derive(Default)]
|
||||
pub struct IoBatch {
|
||||
pub(crate) serial: RxChunk,
|
||||
pub(crate) usb: Option<IntegrityCheckedFrame>,
|
||||
pub(crate) accepted: bool,
|
||||
pub(crate) pending: bool,
|
||||
}
|
||||
impl<I: SerialDevice> SerialIo<I> {
|
||||
pub fn exchange(&mut self, frame: Option<PendingFrame<MavlinkFrame>>) -> IoBatch {
|
||||
match frame {
|
||||
Some(frame) => IoBatch {
|
||||
accepted: self.send(frame),
|
||||
..IoBatch::default()
|
||||
},
|
||||
None => {
|
||||
let serial = self.take_chunk();
|
||||
IoBatch {
|
||||
pending: serial.pending,
|
||||
serial,
|
||||
..IoBatch::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// One short-lock RTIC resource for the board's declared UARTs. All UART IRQs
|
||||
/// have priority 14. Protocol work stays outside this bounded I/O handoff.
|
||||
pub struct PortIo<const N: usize> {
|
||||
inventory: SerialInventory,
|
||||
pub(crate) ports: [SerialIo<OwnedUart>; N],
|
||||
}
|
||||
impl<const N: usize> PortIo<N> {
|
||||
pub(crate) fn new(inventory: SerialInventory) -> Self {
|
||||
assert_eq!(inventory.len(), N, "validated board port count");
|
||||
Self {
|
||||
inventory,
|
||||
ports: core::array::from_fn(|_| SerialIo::new(None, false)),
|
||||
}
|
||||
}
|
||||
pub fn on_interrupt(&mut self, id: PortId, now: kaptan_time::Timestamp) -> bool {
|
||||
self.inventory
|
||||
.index(id)
|
||||
.is_some_and(|index| self.ports[index].on_interrupt(now))
|
||||
}
|
||||
pub fn exchange(&mut self, id: PortId, frame: Option<PendingFrame<MavlinkFrame>>) -> IoBatch {
|
||||
self.inventory
|
||||
.index(id)
|
||||
.map_or_else(IoBatch::default, |index| self.ports[index].exchange(frame))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct UsbIo {
|
||||
usb: F722UsbMavlink,
|
||||
rx_interrupt: RxFifoInterruptGate,
|
||||
}
|
||||
impl UsbIo {
|
||||
pub(crate) fn new(usb: F722UsbMavlink, rx_interrupt: RxFifoInterruptGate) -> Self {
|
||||
Self { usb, rx_interrupt }
|
||||
}
|
||||
pub fn on_interrupt(&mut self) -> bool {
|
||||
self.usb.with_rx_interrupt_gate(
|
||||
|usb| usb.poll(),
|
||||
|enabled| self.rx_interrupt.set_enabled(enabled),
|
||||
)
|
||||
}
|
||||
pub fn exchange(&mut self, frame: Option<PendingFrame<MavlinkFrame>>) -> IoBatch {
|
||||
exchange_usb(&mut self.usb, frame, |enabled| {
|
||||
self.rx_interrupt.set_enabled(enabled)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl F722MavlinkLink {
|
||||
pub(crate) fn usart1(
|
||||
rcc: &stm32f7::stm32f722::RCC,
|
||||
uart: stm32f7::stm32f722::USART1,
|
||||
baud: u32,
|
||||
usb: F722UsbMavlink,
|
||||
) -> Result<Self, ()> {
|
||||
let uart = peripherals::Usart1Tx::new(rcc, uart, UART1_CLOCK_HZ, baud).map_err(|_| ())?;
|
||||
// SAFETY: platform initialization disables D-cache before this object
|
||||
// is created, and the active MAVLink frame remains queue-owned until
|
||||
// this platform receives a terminal DMA completion notification.
|
||||
let memory = unsafe { peripherals::DcacheDisabledDmaMemory::new() };
|
||||
Ok(Self {
|
||||
uart: F722MavlinkTransport::Usart1(MavlinkTransmitter::new(
|
||||
memory,
|
||||
peripherals::Usart1TxDma::new(rcc, uart),
|
||||
)),
|
||||
usb,
|
||||
})
|
||||
/// Private F722 capability delegated alongside the UsbPeripheral in its factory.
|
||||
/// The USB I/O owner retains it at runtime. No register-block reference is made.
|
||||
pub(crate) struct RxFifoInterruptGate {
|
||||
_private: (),
|
||||
}
|
||||
impl RxFifoInterruptGate {
|
||||
pub(crate) fn new(_global: &stm32f7::stm32f722::OTG_FS_GLOBAL) -> Self {
|
||||
Self { _private: () }
|
||||
}
|
||||
|
||||
pub(crate) fn usart2(
|
||||
rcc: &stm32f7::stm32f722::RCC,
|
||||
uart: stm32f7::stm32f722::USART2,
|
||||
baud: u32,
|
||||
usb: F722UsbMavlink,
|
||||
) -> Result<Self, ()> {
|
||||
let uart = peripherals::Usart2Tx::new(rcc, uart, UART2_CLOCK_HZ, baud).map_err(|_| ())?;
|
||||
// SAFETY: identical DMA-memory invariant to the USART1 route above;
|
||||
// only the fixed platform DMA endpoint differs.
|
||||
let memory = unsafe { peripherals::DcacheDisabledDmaMemory::new() };
|
||||
Ok(Self {
|
||||
uart: F722MavlinkTransport::Usart2(MavlinkTransmitter::new(
|
||||
memory,
|
||||
peripherals::Usart2TxDma::new(rcc, uart),
|
||||
)),
|
||||
usb,
|
||||
})
|
||||
/// Call after bus enable in init, or inside the USB RTIC resource lock.
|
||||
/// Defers RXFLVL only. IN, reset and suspend can still interrupt. EP0 shares
|
||||
/// the RX FIFO and cannot bypass a bulk packet blocked at its head.
|
||||
pub(crate) fn set_enabled(&mut self, enabled: bool) {
|
||||
// RM0431: OTG_FS_GINTMSK is an aligned, 32-bit RW register at +0x18.
|
||||
// Form only its raw address, without borrowing the driver's register block.
|
||||
let gintmsk = (stm32f7::stm32f722::OTG_FS_GLOBAL::ptr() as usize + 0x18) as *mut u32;
|
||||
// SAFETY: the factory consumes the PAC tokens and enables the bus before
|
||||
// use. This single-core device keeps its USB clock on. UsbIo's RTIC lock
|
||||
// excludes every runtime USB caller through this RMW. The pinned 0.4.0
|
||||
// driver writes GINTMSK only in enable(), before this capability is used.
|
||||
unsafe { set_rx_fifo_interrupt_enabled(gintmsk, enabled) };
|
||||
// Complete the device write before unlocking or returning from the IRQ.
|
||||
cortex_m::asm::dsb();
|
||||
}
|
||||
}
|
||||
|
||||
/// Sends publications on the open USB link, or on UART when USB is closed.
|
||||
pub(crate) fn send_active(&mut self, pending: PendingFrame<MavlinkFrame>) -> bool {
|
||||
if self.usb.is_open() {
|
||||
self.usb.send(pending)
|
||||
/// # Safety
|
||||
/// `gintmsk` must permit aligned 32-bit volatile reads/writes, with no concurrent
|
||||
/// writer. The caller must complete the device write before releasing exclusion.
|
||||
unsafe fn set_rx_fifo_interrupt_enabled(gintmsk: *mut u32, enabled: bool) {
|
||||
const RXFLVLM: u32 = 1 << 4;
|
||||
// SAFETY: the caller provides exclusive access to this RW register. GINTMSK
|
||||
// has no read/clear or write-one-to-clear fields. Preserve all other bits.
|
||||
unsafe {
|
||||
let mask = gintmsk.read_volatile();
|
||||
gintmsk.write_volatile(if enabled {
|
||||
mask | RXFLVLM
|
||||
} else {
|
||||
self.send_uart(pending)
|
||||
}
|
||||
}
|
||||
|
||||
/// Sends a protocol reply only on the link that supplied its request.
|
||||
pub(crate) fn send_to(
|
||||
&mut self,
|
||||
ingress: MavlinkIngress,
|
||||
pending: PendingFrame<MavlinkFrame>,
|
||||
) -> bool {
|
||||
match ingress {
|
||||
MavlinkIngress::Uart => self.send_uart(pending),
|
||||
MavlinkIngress::Usb => self.usb.send(pending),
|
||||
}
|
||||
}
|
||||
|
||||
fn send_uart(&mut self, pending: PendingFrame<MavlinkFrame>) -> bool {
|
||||
match &mut self.uart {
|
||||
F722MavlinkTransport::Usart1(transmitter) => transmitter
|
||||
.admit_and_start(pending.class, pending.frame)
|
||||
.admission
|
||||
.is_retained(),
|
||||
F722MavlinkTransport::Usart2(transmitter) => transmitter
|
||||
.admit_and_start(pending.class, pending.frame)
|
||||
.admission
|
||||
.is_retained(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn on_dma_complete(&mut self, completion: TxCompletion) {
|
||||
match &mut self.uart {
|
||||
F722MavlinkTransport::Usart1(transmitter) => {
|
||||
let _ = transmitter.on_dma_complete(completion);
|
||||
}
|
||||
F722MavlinkTransport::Usart2(transmitter) => {
|
||||
let _ = transmitter.on_dma_complete(completion);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Services USB and reports whether the protocol worker has bytes to parse.
|
||||
pub(crate) fn poll_usb(&mut self) -> bool {
|
||||
self.usb.poll()
|
||||
}
|
||||
|
||||
pub(crate) fn take_usb_received(&mut self) -> Option<IntegrityCheckedFrame> {
|
||||
self.usb.take_received()
|
||||
mask & !RXFLVLM
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Board-route status light; it is not part of a board declaration API.
|
||||
pub(crate) fn exchange_usb<B: super::usb::UsbBusHealth>(
|
||||
usb: &mut super::usb::UsbMavlink<'_, B>,
|
||||
frame: Option<PendingFrame<MavlinkFrame>>,
|
||||
set_rx_interrupt_enabled: impl FnOnce(bool),
|
||||
) -> IoBatch {
|
||||
usb.with_rx_interrupt_gate(
|
||||
|usb| match frame {
|
||||
Some(frame) => IoBatch {
|
||||
accepted: usb.send(frame),
|
||||
..IoBatch::default()
|
||||
},
|
||||
None => {
|
||||
usb.poll();
|
||||
// The existing USB adapter consumes at most one 64-byte packet
|
||||
// here, including bounded framing. Message decode/dispatch stays
|
||||
// worker-local. Bus polling has separate hardware wait constraints.
|
||||
let frame = usb.take_received();
|
||||
let pending = usb.poll();
|
||||
IoBatch {
|
||||
usb: frame,
|
||||
pending,
|
||||
..IoBatch::default()
|
||||
}
|
||||
}
|
||||
},
|
||||
set_rx_interrupt_enabled,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) enum F722StatusLed {
|
||||
Kakute(peripherals::ActiveLowLedPd2),
|
||||
Geprc(GeprcStatusLed),
|
||||
Pd2(peripherals::ActiveLowLedPd2),
|
||||
Pa13(GeprcStatusLed),
|
||||
}
|
||||
|
||||
impl F722StatusLed {
|
||||
pub(crate) fn toggle(&mut self) {
|
||||
match self {
|
||||
Self::Kakute(led) => led.toggle(),
|
||||
Self::Geprc(led) => led.toggle(),
|
||||
Self::Pd2(led) => led.toggle(),
|
||||
Self::Pa13(led) => led.toggle(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// GEPRC GEP-F722-HD V2 blue status LED on PA13.
|
||||
pub(crate) struct GeprcStatusLed {
|
||||
port: stm32f7::stm32f722::GPIOA,
|
||||
is_high: bool,
|
||||
}
|
||||
|
||||
impl GeprcStatusLed {
|
||||
pub(crate) fn new(rcc: &stm32f7::stm32f722::RCC, port: stm32f7::stm32f722::GPIOA) -> Self {
|
||||
rcc.ahb1enr().modify(|_, write| write.gpioaen().set_bit());
|
||||
@@ -171,7 +221,6 @@ impl GeprcStatusLed {
|
||||
is_high: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn toggle(&mut self) {
|
||||
self.is_high = !self.is_high;
|
||||
self.port.bsrr().write(|write| {
|
||||
@@ -183,3 +232,7 @@ impl GeprcStatusLed {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../../tests/support/usb_irq.rs"]
|
||||
mod usb_irq;
|
||||
@@ -1,8 +1,6 @@
|
||||
//! The one platform-owned RTIC task graph for all supported F722 profiles.
|
||||
|
||||
/// Defines the F722 application in the platform crate. All interrupt bindings
|
||||
/// are the finite platform-owned superset. Profile activation determines which
|
||||
/// acknowledged route has initialized hardware.
|
||||
/// Defines the F722 application with handlers selected by the board's route.
|
||||
#[doc(hidden)]
|
||||
#[macro_export]
|
||||
macro_rules! __f722_flight_application {
|
||||
@@ -12,10 +10,8 @@ macro_rules! __f722_flight_application {
|
||||
#[allow(unused_imports)]
|
||||
use $crate::__platform_internal::panic as _;
|
||||
|
||||
// RTIC 2.3 expands hardware-vector shims that call unsafe PAC token
|
||||
// constructors inside generated unsafe functions. Those shims are
|
||||
// generated by RTIC, not hand-written platform accesses; all platform
|
||||
// unsafe register operations remain documented at their source.
|
||||
// RTIC 2.3 generates unsafe vector functions that call PAC token
|
||||
// constructors without explicit unsafe blocks.
|
||||
#[allow(unsafe_op_in_unsafe_fn)]
|
||||
#[$crate::__platform_internal::rtic_app::app(
|
||||
device = $crate::__platform_internal::pac::stm32f722,
|
||||
@@ -23,36 +19,34 @@ macro_rules! __f722_flight_application {
|
||||
dispatchers = [TIM6_DAC, TIM7]
|
||||
)]
|
||||
mod kaptan_f722_platform_app {
|
||||
use core::sync::atomic::{AtomicBool, AtomicU8, Ordering};
|
||||
|
||||
use $crate::f722::{
|
||||
F722Imu, F722ImuTransferCompletion, F722InitResources, F722LidarState,
|
||||
F722MavlinkState, F722Mtf01, F722Mtf01Uart, F722ServiceTimer,
|
||||
acknowledge_exti3, acknowledge_exti4,
|
||||
acknowledge_imu_rx_dma1, acknowledge_imu_rx_dma2, acknowledge_imu_spi1_error,
|
||||
acknowledge_imu_spi3_error, acknowledge_imu_tx_dma1, acknowledge_imu_tx_dma2,
|
||||
acknowledge_mavlink_dma1, acknowledge_mavlink_dma2, acknowledge_mavlink_rx_usart1,
|
||||
acknowledge_mavlink_rx_usart2, initialize, on_uart4_interrupt, process_imu,
|
||||
record_mavlink_completion,
|
||||
F722Route, F722Interrupts, F722ImuTransferCompletion, F722InitResources, F722LidarState,
|
||||
F722MavlinkState, F722ServiceTimer, EventNotification,
|
||||
initialize, process_imu,
|
||||
};
|
||||
#[allow(unused_imports)]
|
||||
use $crate::__platform_internal::rtic_app::Mutex;
|
||||
use $crate::kaptan_ports::PortId;
|
||||
|
||||
type BoardUserConfig = <$board as $crate::F7Board>::UserConfig;
|
||||
type BoardImmutableConfig = <$board as $crate::F7Board>::ImmutableConfig;
|
||||
type BoardRoute = <BoardImmutableConfig as $crate::F7ImmutableConfig>::Route;
|
||||
type BoardImu = <BoardRoute as F722Route>::Imu;
|
||||
const PORT_COUNT: usize = <$board as $crate::F7Board>::IMMUTABLE_CONFIG.hardware.serial().len();
|
||||
const CONFIG_PAYLOAD_BYTES: usize = $crate::config_snapshot_length::<
|
||||
BoardUserConfig,
|
||||
>();
|
||||
type BoardInitResources = F722InitResources<
|
||||
BoardUserConfig,
|
||||
BoardImmutableConfig,
|
||||
PORT_COUNT,
|
||||
CONFIG_PAYLOAD_BYTES,
|
||||
>;
|
||||
type BoardMavlinkState = F722MavlinkState<
|
||||
BoardUserConfig,
|
||||
BoardImmutableConfig,
|
||||
CONFIG_PAYLOAD_BYTES,
|
||||
>;
|
||||
type BoardMavlinkState = <BoardInitResources as F722MavlinkState>::Protocol;
|
||||
type BoardPorts = <BoardInitResources as F722MavlinkState>::Ports;
|
||||
type BoardUsb = <BoardInitResources as F722MavlinkState>::Usb;
|
||||
type BoardClock = <BoardInitResources as F722MavlinkState>::Clock;
|
||||
type BoardImuPublisher = <BoardInitResources as F722MavlinkState>::ImuPublisher;
|
||||
|
||||
const IMU_EVENT_DRDY: u8 = 1 << 0;
|
||||
const IMU_EVENT_TRANSFER_COMPLETE: u8 = 1 << 1;
|
||||
@@ -61,41 +55,37 @@ macro_rules! __f722_flight_application {
|
||||
const IMU_EVENT_START: u8 = 1 << 4;
|
||||
const MAVLINK_EVENT_TIMER: u8 = 1 << 0;
|
||||
const MAVLINK_EVENT_RECEIVED: u8 = 1 << 1;
|
||||
const MAVLINK_EVENT_DMA: u8 = 1 << 2;
|
||||
|
||||
// Hardware IRQs and both software workers coalesce event bits.
|
||||
// Release/Acquire pairs make each terminal acknowledgement visible
|
||||
// to the worker that consumes it; the queued bit prevents unbounded
|
||||
// software-task backlog while preserving the latest pending work.
|
||||
static IMU_EVENTS: AtomicU8 = AtomicU8::new(0);
|
||||
static MAVLINK_EVENTS: AtomicU8 = AtomicU8::new(0);
|
||||
static IMU_QUEUED: AtomicBool = AtomicBool::new(false);
|
||||
static MAVLINK_QUEUED: AtomicBool = AtomicBool::new(false);
|
||||
static IMU_EVENTS: EventNotification = EventNotification::new();
|
||||
static MAVLINK_EVENTS: EventNotification = EventNotification::new();
|
||||
|
||||
#[shared]
|
||||
struct Shared {
|
||||
mavlink: BoardMavlinkState,
|
||||
uarts: BoardPorts,
|
||||
usb: BoardUsb,
|
||||
serial_clock: BoardClock,
|
||||
lidar: F722LidarState,
|
||||
}
|
||||
|
||||
#[local]
|
||||
struct Local {
|
||||
imu: F722Imu,
|
||||
mtf01: Option<F722Mtf01>,
|
||||
_mtf01_uart: Option<F722Mtf01Uart>,
|
||||
imu: BoardImu,
|
||||
imu_publisher: BoardImuPublisher,
|
||||
timer: F722ServiceTimer,
|
||||
mavlink: BoardMavlinkState,
|
||||
}
|
||||
|
||||
#[init]
|
||||
fn init(context: init::Context) -> (Shared, Local) {
|
||||
let BoardInitResources {
|
||||
imu,
|
||||
mtf01,
|
||||
mtf01_uart,
|
||||
timer,
|
||||
imu_publisher,
|
||||
mut timer,
|
||||
lidar,
|
||||
mavlink,
|
||||
} = initialize::<BoardUserConfig, BoardImmutableConfig, CONFIG_PAYLOAD_BYTES>(
|
||||
uarts,
|
||||
usb,
|
||||
serial_clock,
|
||||
} = initialize::<BoardUserConfig, BoardImmutableConfig, PORT_COUNT, CONFIG_PAYLOAD_BYTES>(
|
||||
context.core,
|
||||
context.device,
|
||||
<$board as $crate::F7Board>::FACTORY_USER_CONFIG,
|
||||
@@ -103,17 +93,31 @@ macro_rules! __f722_flight_application {
|
||||
)
|
||||
.unwrap_or_else(|_| safe_idle());
|
||||
$crate::__platform_internal::logging::info!("flight controller initialized");
|
||||
imu_worker::spawn().expect("IMU worker starts once");
|
||||
mavlink_worker::spawn().expect("MAVLink worker starts once");
|
||||
schedule_imu(IMU_EVENT_START);
|
||||
// All fallible construction has succeeded. RTIC keeps IRQs
|
||||
// masked until the returned resources have been published.
|
||||
timer.enable_interrupt();
|
||||
// SAFETY: RTIC 2.3 sets PRIMASK before init and releases it only
|
||||
// after publishing Shared and Local. All fallible work is done,
|
||||
// the vector's priority is configured, and TIM2 is initialized.
|
||||
// Restore the NVIC enable masked by clock initialization.
|
||||
unsafe {
|
||||
$crate::__platform_internal::cm::peripheral::NVIC::unmask(
|
||||
$crate::__platform_internal::pac::stm32f722::Interrupt::TIM2,
|
||||
);
|
||||
}
|
||||
(
|
||||
Shared {
|
||||
mavlink,
|
||||
uarts, usb, serial_clock,
|
||||
lidar,
|
||||
},
|
||||
Local {
|
||||
imu,
|
||||
mtf01,
|
||||
_mtf01_uart: mtf01_uart,
|
||||
imu_publisher,
|
||||
timer,
|
||||
mavlink,
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -127,21 +131,21 @@ macro_rules! __f722_flight_application {
|
||||
|
||||
#[task(binds = EXTI4, priority = 15)]
|
||||
fn exti4(_: exti4::Context) {
|
||||
if acknowledge_exti4() {
|
||||
if BoardRoute::acknowledge_exti4() {
|
||||
schedule_imu(IMU_EVENT_DRDY);
|
||||
}
|
||||
}
|
||||
|
||||
#[task(binds = EXTI3, priority = 15)]
|
||||
fn exti3(_: exti3::Context) {
|
||||
if acknowledge_exti3() {
|
||||
if BoardRoute::acknowledge_exti3() {
|
||||
schedule_imu(IMU_EVENT_DRDY);
|
||||
}
|
||||
}
|
||||
|
||||
#[task(binds = DMA2_STREAM0, priority = 14)]
|
||||
fn imu_rx_dma2(_: imu_rx_dma2::Context) {
|
||||
match acknowledge_imu_rx_dma2() {
|
||||
match BoardRoute::acknowledge_imu_rx_dma2() {
|
||||
F722ImuTransferCompletion::Complete => schedule_imu(IMU_EVENT_TRANSFER_COMPLETE),
|
||||
F722ImuTransferCompletion::TransferFault => schedule_imu(IMU_EVENT_TRANSFER_FAULT),
|
||||
F722ImuTransferCompletion::NotPending => {}
|
||||
@@ -150,21 +154,21 @@ macro_rules! __f722_flight_application {
|
||||
|
||||
#[task(binds = DMA2_STREAM3, priority = 14)]
|
||||
fn imu_tx_dma2(_: imu_tx_dma2::Context) {
|
||||
if acknowledge_imu_tx_dma2() == F722ImuTransferCompletion::TransferFault {
|
||||
if BoardRoute::acknowledge_imu_tx_dma2() == F722ImuTransferCompletion::TransferFault {
|
||||
schedule_imu(IMU_EVENT_TRANSFER_FAULT);
|
||||
}
|
||||
}
|
||||
|
||||
#[task(binds = SPI1, priority = 14)]
|
||||
fn imu_spi1_error(_: imu_spi1_error::Context) {
|
||||
if acknowledge_imu_spi1_error() {
|
||||
if BoardRoute::acknowledge_imu_spi1_error() {
|
||||
schedule_imu(IMU_EVENT_TRANSFER_FAULT);
|
||||
}
|
||||
}
|
||||
|
||||
#[task(binds = DMA1_STREAM0, priority = 14)]
|
||||
fn imu_rx_dma1(_: imu_rx_dma1::Context) {
|
||||
match acknowledge_imu_rx_dma1() {
|
||||
match BoardRoute::acknowledge_imu_rx_dma1() {
|
||||
F722ImuTransferCompletion::Complete => schedule_imu(IMU_EVENT_TRANSFER_COMPLETE),
|
||||
F722ImuTransferCompletion::TransferFault => schedule_imu(IMU_EVENT_TRANSFER_FAULT),
|
||||
F722ImuTransferCompletion::NotPending => {}
|
||||
@@ -173,21 +177,40 @@ macro_rules! __f722_flight_application {
|
||||
|
||||
#[task(binds = DMA1_STREAM5, priority = 14)]
|
||||
fn imu_tx_dma1(_: imu_tx_dma1::Context) {
|
||||
if acknowledge_imu_tx_dma1() == F722ImuTransferCompletion::TransferFault {
|
||||
if BoardRoute::acknowledge_imu_tx_dma1() == F722ImuTransferCompletion::TransferFault {
|
||||
schedule_imu(IMU_EVENT_TRANSFER_FAULT);
|
||||
}
|
||||
}
|
||||
|
||||
#[task(binds = SPI3, priority = 14)]
|
||||
fn imu_spi3_error(_: imu_spi3_error::Context) {
|
||||
if acknowledge_imu_spi3_error() {
|
||||
if BoardRoute::acknowledge_imu_spi3_error() {
|
||||
schedule_imu(IMU_EVENT_TRANSFER_FAULT);
|
||||
}
|
||||
}
|
||||
|
||||
#[task(binds = UART4, priority = 14, local = [mtf01])]
|
||||
fn uart4(context: uart4::Context) {
|
||||
on_uart4_interrupt(context.local.mtf01);
|
||||
#[task(binds = USART3, priority = 14, shared = [uarts, serial_clock])]
|
||||
fn usart3(mut context: usart3::Context) {
|
||||
let now = context.shared.serial_clock.lock(|clock| clock.now());
|
||||
if context.shared.uarts.lock(|uarts| uarts.on_interrupt(PortId::new(3), now)) {
|
||||
schedule_mavlink(MAVLINK_EVENT_RECEIVED);
|
||||
}
|
||||
}
|
||||
|
||||
#[task(binds = UART4, priority = 14, shared = [uarts, serial_clock])]
|
||||
fn uart4(mut context: uart4::Context) {
|
||||
let now = context.shared.serial_clock.lock(|clock| clock.now());
|
||||
if context.shared.uarts.lock(|uarts| uarts.on_interrupt(PortId::new(4), now)) {
|
||||
schedule_mavlink(MAVLINK_EVENT_RECEIVED);
|
||||
}
|
||||
}
|
||||
|
||||
#[task(binds = USART6, priority = 14, shared = [uarts, serial_clock])]
|
||||
fn usart6(mut context: usart6::Context) {
|
||||
let now = context.shared.serial_clock.lock(|clock| clock.now());
|
||||
if context.shared.uarts.lock(|uarts| uarts.on_interrupt(PortId::new(6), now)) {
|
||||
schedule_mavlink(MAVLINK_EVENT_RECEIVED);
|
||||
}
|
||||
}
|
||||
|
||||
#[task(binds = I2C1_EV, priority = 11, shared = [lidar])]
|
||||
@@ -206,109 +229,82 @@ macro_rules! __f722_flight_application {
|
||||
.lock(|lidar| lidar.on_i2c_event(true));
|
||||
}
|
||||
|
||||
#[task(binds = DMA2_STREAM7, priority = 10)]
|
||||
fn mavlink_dma2(_: mavlink_dma2::Context) {
|
||||
let completion = acknowledge_mavlink_dma2();
|
||||
if completion != $crate::__platform_internal::dma::TxCompletion::NotPending {
|
||||
record_mavlink_completion(completion);
|
||||
schedule_mavlink(MAVLINK_EVENT_DMA);
|
||||
}
|
||||
}
|
||||
|
||||
#[task(binds = DMA1_STREAM6, priority = 10)]
|
||||
fn mavlink_dma1(_: mavlink_dma1::Context) {
|
||||
let completion = acknowledge_mavlink_dma1();
|
||||
if completion != $crate::__platform_internal::dma::TxCompletion::NotPending {
|
||||
record_mavlink_completion(completion);
|
||||
schedule_mavlink(MAVLINK_EVENT_DMA);
|
||||
}
|
||||
}
|
||||
|
||||
#[task(binds = USART1, priority = 8, shared = [mavlink])]
|
||||
#[task(binds = USART1, priority = 14, shared = [uarts, serial_clock])]
|
||||
fn mavlink_rx_usart1(mut context: mavlink_rx_usart1::Context) {
|
||||
let event = acknowledge_mavlink_rx_usart1();
|
||||
let now = context.shared.serial_clock.lock(|clock| clock.now());
|
||||
if context
|
||||
.shared
|
||||
.mavlink
|
||||
.lock(|mavlink| mavlink.receive_mavlink_interrupt(event))
|
||||
.uarts
|
||||
.lock(|uarts| uarts.on_interrupt(PortId::new(1), now))
|
||||
{
|
||||
schedule_mavlink(MAVLINK_EVENT_RECEIVED);
|
||||
}
|
||||
}
|
||||
|
||||
#[task(binds = USART2, priority = 8, shared = [mavlink])]
|
||||
#[task(binds = USART2, priority = 14, shared = [uarts, serial_clock])]
|
||||
fn mavlink_rx_usart2(mut context: mavlink_rx_usart2::Context) {
|
||||
let event = acknowledge_mavlink_rx_usart2();
|
||||
let now = context.shared.serial_clock.lock(|clock| clock.now());
|
||||
if context
|
||||
.shared
|
||||
.mavlink
|
||||
.lock(|mavlink| mavlink.receive_mavlink_interrupt(event))
|
||||
.uarts
|
||||
.lock(|uarts| uarts.on_interrupt(PortId::new(2), now))
|
||||
{
|
||||
schedule_mavlink(MAVLINK_EVENT_RECEIVED);
|
||||
}
|
||||
}
|
||||
|
||||
#[task(binds = OTG_FS, priority = 8, shared = [mavlink])]
|
||||
#[task(binds = OTG_FS, priority = 8, shared = [usb])]
|
||||
fn mavlink_usb(mut context: mavlink_usb::Context) {
|
||||
// on_interrupt gates only RXFLVLM inside this lock before returning.
|
||||
// Buffered RX must wake its consumer while other USB sources stay enabled.
|
||||
if context
|
||||
.shared
|
||||
.mavlink
|
||||
.lock(F722MavlinkState::on_usb_interrupt)
|
||||
.usb
|
||||
.lock(|usb| usb.on_interrupt())
|
||||
{
|
||||
schedule_mavlink(MAVLINK_EVENT_RECEIVED);
|
||||
}
|
||||
}
|
||||
|
||||
#[task(binds = TIM2, priority = 4, local = [timer], shared = [lidar])]
|
||||
#[task(binds = TIM2, priority = 10, local = [timer], shared = [lidar])]
|
||||
fn tim2(mut context: tim2::Context) {
|
||||
context.local.timer.acknowledge_update();
|
||||
if !context.local.timer.on_interrupt() {
|
||||
return;
|
||||
}
|
||||
let _ = context.shared.lidar.lock(F722LidarState::on_timer);
|
||||
schedule_imu(IMU_EVENT_TIMER);
|
||||
schedule_mavlink(MAVLINK_EVENT_TIMER);
|
||||
}
|
||||
|
||||
#[task(priority = 13, local = [imu])]
|
||||
#[task(priority = 13, local = [imu, imu_publisher])]
|
||||
async fn imu_worker(context: imu_worker::Context) {
|
||||
let events = IMU_EVENTS.swap(0, Ordering::AcqRel);
|
||||
process_imu(context.local.imu, events);
|
||||
IMU_QUEUED.store(false, Ordering::Release);
|
||||
if IMU_EVENTS.load(Ordering::Acquire) != 0
|
||||
&& !IMU_QUEUED.swap(true, Ordering::AcqRel)
|
||||
&& imu_worker::spawn().is_err()
|
||||
{
|
||||
IMU_QUEUED.store(false, Ordering::Release);
|
||||
loop {
|
||||
let events = IMU_EVENTS.wait().await;
|
||||
process_imu(context.local.imu, context.local.imu_publisher, events);
|
||||
}
|
||||
}
|
||||
|
||||
#[task(priority = 9, shared = [mavlink])]
|
||||
#[task(priority = 7, local = [mavlink], shared = [uarts, usb, serial_clock])]
|
||||
async fn mavlink_worker(mut context: mavlink_worker::Context) {
|
||||
let events = MAVLINK_EVENTS.swap(0, Ordering::AcqRel);
|
||||
context
|
||||
.shared
|
||||
.mavlink
|
||||
.lock(|mavlink| mavlink.process_worker_events(events));
|
||||
MAVLINK_QUEUED.store(false, Ordering::Release);
|
||||
if MAVLINK_EVENTS.load(Ordering::Acquire) != 0
|
||||
&& !MAVLINK_QUEUED.swap(true, Ordering::AcqRel)
|
||||
&& mavlink_worker::spawn().is_err()
|
||||
{
|
||||
MAVLINK_QUEUED.store(false, Ordering::Release);
|
||||
loop {
|
||||
let events = MAVLINK_EVENTS.wait().await;
|
||||
let pending = context.local.mavlink.process_worker_events(events,
|
||||
|id, frame| match id {
|
||||
0 => context.shared.usb.lock(|usb| usb.exchange(frame)),
|
||||
_ => context.shared.uarts.lock(|uarts| uarts.exchange(PortId::new(id), frame)),
|
||||
},
|
||||
|| context.shared.serial_clock.lock(|clock| clock.now()),
|
||||
);
|
||||
if pending { schedule_mavlink(MAVLINK_EVENT_RECEIVED); }
|
||||
}
|
||||
}
|
||||
|
||||
fn schedule_imu(event: u8) {
|
||||
IMU_EVENTS.fetch_or(event, Ordering::Release);
|
||||
if !IMU_QUEUED.swap(true, Ordering::AcqRel) && imu_worker::spawn().is_err() {
|
||||
IMU_QUEUED.store(false, Ordering::Release);
|
||||
}
|
||||
IMU_EVENTS.notify(event);
|
||||
}
|
||||
|
||||
fn schedule_mavlink(event: u8) {
|
||||
MAVLINK_EVENTS.fetch_or(event, Ordering::Release);
|
||||
if !MAVLINK_QUEUED.swap(true, Ordering::AcqRel) && mavlink_worker::spawn().is_err()
|
||||
{
|
||||
MAVLINK_QUEUED.store(false, Ordering::Release);
|
||||
}
|
||||
MAVLINK_EVENTS.notify(event);
|
||||
}
|
||||
|
||||
fn safe_idle() -> ! {
|
||||
|
||||
@@ -1,127 +1,110 @@
|
||||
//! Platform-owned interrupt acknowledgement for all supported F722 profiles.
|
||||
//! Static interrupt acknowledgement for the finite F722 route variants.
|
||||
|
||||
use core::sync::atomic::{AtomicU8, Ordering};
|
||||
|
||||
use super::peripherals::{Spi1, Spi3, Usart2Tx, Usart2TxDma};
|
||||
use super::{
|
||||
F722ImuTransferCompletion,
|
||||
peripherals::{ReceiveInterrupt, Usart1Tx, Usart1TxDma},
|
||||
peripherals::{Spi1, Spi3},
|
||||
};
|
||||
use kaptan_dma::TxCompletion;
|
||||
use crate::{Icm42688Spi3, Mpu6000Spi1, sealed};
|
||||
use stm32f7::stm32f722;
|
||||
|
||||
use crate::F722Profile;
|
||||
|
||||
const PROFILE_NONE: u8 = 0;
|
||||
const PROFILE_MPU6000_SPI1_USART1: u8 = 1;
|
||||
const PROFILE_ICM42688_SPI3_USART2: u8 = 2;
|
||||
|
||||
static ACTIVE_PROFILE: AtomicU8 = AtomicU8::new(PROFILE_NONE);
|
||||
|
||||
pub(crate) fn activate_profile(profile: F722Profile) {
|
||||
let encoded = match profile {
|
||||
F722Profile::Mpu6000Spi1Usart1 => PROFILE_MPU6000_SPI1_USART1,
|
||||
F722Profile::Icm42688Spi3Usart2 => PROFILE_ICM42688_SPI3_USART2,
|
||||
};
|
||||
ACTIVE_PROFILE.store(encoded, Ordering::Release);
|
||||
}
|
||||
|
||||
fn active_profile() -> u8 {
|
||||
ACTIVE_PROFILE.load(Ordering::Acquire)
|
||||
}
|
||||
|
||||
pub fn acknowledge_exti4() -> bool {
|
||||
if active_profile() != PROFILE_MPU6000_SPI1_USART1 {
|
||||
return false;
|
||||
/// RTIC binds a literal superset of vectors. Inactive vectors do not touch
|
||||
/// MMIO; active vectors are called only after successful RTIC initialization.
|
||||
/// Keeping this sealed couples the handlers to the reviewed initialization.
|
||||
pub trait F722Interrupts: sealed::Route {
|
||||
fn acknowledge_exti4() -> bool {
|
||||
false
|
||||
}
|
||||
// SAFETY: the selected profile configured EXTI4 once before interrupts.
|
||||
// This ISR writes only the W1C pending bit for PA4 and does not alias the
|
||||
// worker-owned SPI or DMA registers.
|
||||
unsafe {
|
||||
(&*stm32f722::EXTI::ptr())
|
||||
.pr()
|
||||
.write(|write| write.pr4().clear());
|
||||
fn acknowledge_exti3() -> bool {
|
||||
false
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
pub fn acknowledge_exti3() -> bool {
|
||||
if active_profile() != PROFILE_ICM42688_SPI3_USART2 {
|
||||
return false;
|
||||
fn acknowledge_imu_rx_dma2() -> F722ImuTransferCompletion {
|
||||
F722ImuTransferCompletion::NotPending
|
||||
}
|
||||
// SAFETY: the selected profile configured EXTI3 once before interrupts.
|
||||
// This ISR writes only the W1C pending bit for PC3, disjoint from every
|
||||
// peripheral state owned by the deferred IMU worker.
|
||||
unsafe {
|
||||
(&*stm32f722::EXTI::ptr())
|
||||
.pr()
|
||||
.write(|write| write.pr3().clear());
|
||||
fn acknowledge_imu_tx_dma2() -> F722ImuTransferCompletion {
|
||||
F722ImuTransferCompletion::NotPending
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
pub fn acknowledge_imu_rx_dma2() -> F722ImuTransferCompletion {
|
||||
if active_profile() == PROFILE_MPU6000_SPI1_USART1 {
|
||||
return Spi1::acknowledge_rx_interrupt().into();
|
||||
fn acknowledge_imu_spi1_error() -> bool {
|
||||
false
|
||||
}
|
||||
F722ImuTransferCompletion::NotPending
|
||||
}
|
||||
|
||||
pub fn acknowledge_imu_tx_dma2() -> F722ImuTransferCompletion {
|
||||
if active_profile() == PROFILE_MPU6000_SPI1_USART1 {
|
||||
return Spi1::acknowledge_tx_interrupt().into();
|
||||
fn acknowledge_imu_rx_dma1() -> F722ImuTransferCompletion {
|
||||
F722ImuTransferCompletion::NotPending
|
||||
}
|
||||
F722ImuTransferCompletion::NotPending
|
||||
}
|
||||
|
||||
pub fn acknowledge_imu_spi1_error() -> bool {
|
||||
active_profile() == PROFILE_MPU6000_SPI1_USART1 && Spi1::acknowledge_error_interrupt()
|
||||
}
|
||||
|
||||
pub fn acknowledge_imu_rx_dma1() -> F722ImuTransferCompletion {
|
||||
if active_profile() == PROFILE_ICM42688_SPI3_USART2 {
|
||||
return Spi3::acknowledge_rx_interrupt().into();
|
||||
fn acknowledge_imu_tx_dma1() -> F722ImuTransferCompletion {
|
||||
F722ImuTransferCompletion::NotPending
|
||||
}
|
||||
F722ImuTransferCompletion::NotPending
|
||||
}
|
||||
|
||||
pub fn acknowledge_imu_tx_dma1() -> F722ImuTransferCompletion {
|
||||
if active_profile() == PROFILE_ICM42688_SPI3_USART2 {
|
||||
return Spi3::acknowledge_tx_interrupt().into();
|
||||
}
|
||||
F722ImuTransferCompletion::NotPending
|
||||
}
|
||||
|
||||
pub fn acknowledge_imu_spi3_error() -> bool {
|
||||
active_profile() == PROFILE_ICM42688_SPI3_USART2 && Spi3::acknowledge_error_interrupt()
|
||||
}
|
||||
|
||||
pub fn acknowledge_mavlink_dma2() -> TxCompletion {
|
||||
if active_profile() == PROFILE_MPU6000_SPI1_USART1 {
|
||||
Usart1TxDma::acknowledge_interrupt()
|
||||
} else {
|
||||
TxCompletion::NotPending
|
||||
fn acknowledge_imu_spi3_error() -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn acknowledge_mavlink_dma1() -> TxCompletion {
|
||||
if active_profile() == PROFILE_ICM42688_SPI3_USART2 {
|
||||
return Usart2TxDma::acknowledge_interrupt();
|
||||
impl F722Interrupts for Mpu6000Spi1 {
|
||||
fn acknowledge_exti4() -> bool {
|
||||
// SAFETY: RTIC initialization configured EXTI4 before unmasking IRQs.
|
||||
// This ISR writes only PA4's W1C pending bit, disjoint from the worker.
|
||||
unsafe {
|
||||
(&*stm32f722::EXTI::ptr())
|
||||
.pr()
|
||||
.write(|write| write.pr4().clear());
|
||||
}
|
||||
true
|
||||
}
|
||||
TxCompletion::NotPending
|
||||
}
|
||||
|
||||
pub fn acknowledge_mavlink_rx_usart1() -> ReceiveInterrupt {
|
||||
if active_profile() == PROFILE_MPU6000_SPI1_USART1 {
|
||||
Usart1Tx::acknowledge_receive_interrupt()
|
||||
} else {
|
||||
ReceiveInterrupt::NotPending
|
||||
fn acknowledge_imu_rx_dma2() -> F722ImuTransferCompletion {
|
||||
Spi1::acknowledge_rx_interrupt()
|
||||
}
|
||||
|
||||
fn acknowledge_imu_tx_dma2() -> F722ImuTransferCompletion {
|
||||
Spi1::acknowledge_tx_interrupt()
|
||||
}
|
||||
|
||||
fn acknowledge_imu_spi1_error() -> bool {
|
||||
Spi1::acknowledge_error_interrupt()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn acknowledge_mavlink_rx_usart2() -> ReceiveInterrupt {
|
||||
if active_profile() == PROFILE_ICM42688_SPI3_USART2 {
|
||||
return Usart2Tx::acknowledge_receive_interrupt();
|
||||
impl F722Interrupts for Icm42688Spi3 {
|
||||
fn acknowledge_exti3() -> bool {
|
||||
// SAFETY: RTIC initialization configured EXTI3 before unmasking IRQs.
|
||||
// This ISR writes only PC3's W1C pending bit, disjoint from the worker.
|
||||
unsafe {
|
||||
(&*stm32f722::EXTI::ptr())
|
||||
.pr()
|
||||
.write(|write| write.pr3().clear());
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
fn acknowledge_imu_rx_dma1() -> F722ImuTransferCompletion {
|
||||
Spi3::acknowledge_rx_interrupt()
|
||||
}
|
||||
|
||||
fn acknowledge_imu_tx_dma1() -> F722ImuTransferCompletion {
|
||||
Spi3::acknowledge_tx_interrupt()
|
||||
}
|
||||
|
||||
fn acknowledge_imu_spi3_error() -> bool {
|
||||
Spi3::acknowledge_error_interrupt()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn inactive_vectors_are_noops_without_initialized_hardware() {
|
||||
type Mpu = Mpu6000Spi1;
|
||||
type Icm = Icm42688Spi3;
|
||||
let not_pending = F722ImuTransferCompletion::NotPending;
|
||||
|
||||
assert!(!Mpu::acknowledge_exti3());
|
||||
assert!(!Mpu::acknowledge_imu_spi3_error());
|
||||
assert_eq!(Mpu::acknowledge_imu_rx_dma1(), not_pending);
|
||||
assert_eq!(Mpu::acknowledge_imu_tx_dma1(), not_pending);
|
||||
|
||||
assert!(!Icm::acknowledge_exti4());
|
||||
assert!(!Icm::acknowledge_imu_spi1_error());
|
||||
assert_eq!(Icm::acknowledge_imu_rx_dma2(), not_pending);
|
||||
assert_eq!(Icm::acknowledge_imu_tx_dma2(), not_pending);
|
||||
}
|
||||
ReceiveInterrupt::NotPending
|
||||
}
|
||||
@@ -1,9 +1,4 @@
|
||||
//! Finite STM32F722 execution profiles.
|
||||
//!
|
||||
//! This module is the sole owner of STM32F722 register routes and execution
|
||||
//! lifecycle. Its public-in-crate profile names describe useful electrical
|
||||
//! meaning; the RTIC bindings, PAC names, DMA streams and synchronization are
|
||||
//! intentionally contained here and in the macro template.
|
||||
//! STM32F722 initialization and RTIC runtime.
|
||||
|
||||
mod board;
|
||||
mod clock;
|
||||
@@ -12,70 +7,60 @@ mod data_channel;
|
||||
mod firmware;
|
||||
mod interrupts;
|
||||
mod motor_inhibit;
|
||||
mod notification;
|
||||
mod peripherals;
|
||||
pub mod ports;
|
||||
mod routes;
|
||||
mod sensors;
|
||||
mod serial;
|
||||
mod usb;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../../../../boards/geprc-f722-v2/src/board.rs"]
|
||||
mod geprc;
|
||||
#[cfg(test)]
|
||||
#[path = "../../../../boards/kakute-f7-mini-v3/src/board.rs"]
|
||||
mod kakute;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../../tests/support/config_active_state.rs"]
|
||||
mod config_active_state;
|
||||
#[cfg(test)]
|
||||
#[path = "../../tests/support/config_files.rs"]
|
||||
mod config_files;
|
||||
#[cfg(test)]
|
||||
#[path = "../../tests/support/route_variants.rs"]
|
||||
mod route_variants;
|
||||
|
||||
use kaptan_config::Config;
|
||||
|
||||
use crate::{BoardError, F7ImmutableConfig};
|
||||
|
||||
/// Completion classification crossing the RTIC interrupt template.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum F722ImuTransferCompletion {
|
||||
Complete,
|
||||
TransferFault,
|
||||
NotPending,
|
||||
}
|
||||
|
||||
impl From<kaptan_spi::SpiCompletion> for F722ImuTransferCompletion {
|
||||
fn from(value: kaptan_spi::SpiCompletion) -> Self {
|
||||
match value {
|
||||
kaptan_spi::SpiCompletion::Complete => Self::Complete,
|
||||
kaptan_spi::SpiCompletion::TransferFault => Self::TransferFault,
|
||||
kaptan_spi::SpiCompletion::NotPending => Self::NotPending,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub(crate) enum VerticalRoute {
|
||||
None,
|
||||
Mtf01,
|
||||
LidarLiteV3,
|
||||
}
|
||||
pub use kaptan_spi::SpiCompletion as F722ImuTransferCompletion;
|
||||
|
||||
pub use board::{
|
||||
F722InitResources, F722LidarState, F722MavlinkState, F722Mtf01, F722Mtf01Uart,
|
||||
F722ServiceTimer, on_uart4_interrupt, process_imu, record_mavlink_completion,
|
||||
F722InitResources, F722LidarState, F722MavlinkState, F722ServiceTimer, process_imu,
|
||||
};
|
||||
pub use interrupts::{
|
||||
acknowledge_exti3, acknowledge_exti4, acknowledge_imu_rx_dma1, acknowledge_imu_rx_dma2,
|
||||
acknowledge_imu_spi1_error, acknowledge_imu_spi3_error, acknowledge_imu_tx_dma1,
|
||||
acknowledge_imu_tx_dma2, acknowledge_mavlink_dma1, acknowledge_mavlink_dma2,
|
||||
acknowledge_mavlink_rx_usart1, acknowledge_mavlink_rx_usart2,
|
||||
};
|
||||
pub use sensors::F722Imu;
|
||||
pub use config::F722UserConfig;
|
||||
pub use interrupts::F722Interrupts;
|
||||
pub use notification::EventNotification;
|
||||
pub use routes::F722Route;
|
||||
pub use sensors::{Icm42688Imu, ImuEvent, ImuService, Mpu6000Imu};
|
||||
|
||||
/// Materializes a board declaration after RTIC has exclusively taken the PAC
|
||||
/// peripherals. The output-inhibit write is the first hardware action; route
|
||||
/// and configuration checks precede all non-safety hardware initialization.
|
||||
pub fn initialize<C, I, const MAX_PAYLOAD_BYTES: usize>(
|
||||
/// Inhibits motor outputs, validates configuration, then initializes hardware.
|
||||
/// Called after RTIC takes the PAC peripherals.
|
||||
pub fn initialize<C, I, const N: usize, const MAX_PAYLOAD_BYTES: usize>(
|
||||
core: cortex_m::Peripherals,
|
||||
device: stm32f7::stm32f722::Peripherals,
|
||||
factory: C,
|
||||
immutable: I,
|
||||
) -> Result<F722InitResources<C, I, MAX_PAYLOAD_BYTES>, BoardError>
|
||||
) -> Result<F722InitResources<C, I, N, MAX_PAYLOAD_BYTES>, BoardError>
|
||||
where
|
||||
C: Clone + Config,
|
||||
I: F7ImmutableConfig,
|
||||
{
|
||||
// This is deliberately the first hardware access, before configuration
|
||||
// validation, clocks, sensor routes, interrupts, or UART setup. It keeps
|
||||
// the former four motor pins low even for malformed configuration.
|
||||
motor_inhibit::inhibit_pb0_pb1_pb4_pb5(&device.RCC, &device.GPIOB);
|
||||
let hardware = immutable.f722().hardware;
|
||||
board::validate_board_config(&factory, &immutable, hardware)?;
|
||||
board::initialize::<C, I, MAX_PAYLOAD_BYTES>(core, device, factory, immutable, hardware)
|
||||
motor_inhibit::inhibit(&device.RCC, &device.GPIOB, hardware.motors);
|
||||
board::validate_board_config::<C, I, N>(&factory, &immutable)?;
|
||||
board::initialize::<C, I, N, MAX_PAYLOAD_BYTES>(core, device, factory, immutable, hardware)
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
//! Common former-motor-pin interlock for every F722 image.
|
||||
//!
|
||||
//! This is intentionally independent of profile initialization and is called
|
||||
//! as the first PAC access. The GPIO-low interlock is a separate electrical
|
||||
//! boundary from all telemetry initialization.
|
||||
//! Motor GPIO-low interlock, called before all fallible initialization.
|
||||
|
||||
use crate::MotorPins;
|
||||
use stm32f7::stm32f722;
|
||||
|
||||
/// Drives PB0/PB1/PB4/PB5 low before selecting push-pull GPIO output mode.
|
||||
pub(crate) fn inhibit_pb0_pb1_pb4_pb5(rcc: &stm32f722::RCC, port: &stm32f722::GPIOB) {
|
||||
pub(crate) fn inhibit(
|
||||
rcc: &stm32f722::rcc::RegisterBlock,
|
||||
port: &stm32f722::gpiob::RegisterBlock,
|
||||
pins: MotorPins,
|
||||
) {
|
||||
rcc.ahb1enr().modify(|_, write| write.gpioben().set_bit());
|
||||
// RM0431 section 5.2.12 requires two peripheral clock cycles before the
|
||||
// first access. The volatile enable-register read supplies the standard
|
||||
@@ -15,7 +15,7 @@ pub(crate) fn inhibit_pb0_pb1_pb4_pb5(rcc: &stm32f722::RCC, port: &stm32f722::GP
|
||||
// ahead of the clock enable.
|
||||
let _ = rcc.ahb1enr().read().gpioben().bit_is_set();
|
||||
port.bsrr().write(|write| {
|
||||
write
|
||||
let write = write
|
||||
.br0()
|
||||
.set_bit()
|
||||
.br1()
|
||||
@@ -23,10 +23,15 @@ pub(crate) fn inhibit_pb0_pb1_pb4_pb5(rcc: &stm32f722::RCC, port: &stm32f722::GP
|
||||
.br4()
|
||||
.set_bit()
|
||||
.br5()
|
||||
.set_bit()
|
||||
.set_bit();
|
||||
if pins == MotorPins::Six {
|
||||
write.br6().set_bit().br7().set_bit()
|
||||
} else {
|
||||
write
|
||||
}
|
||||
});
|
||||
port.otyper().modify(|_, write| {
|
||||
write
|
||||
let write = write
|
||||
.ot0()
|
||||
.push_pull()
|
||||
.ot1()
|
||||
@@ -34,10 +39,15 @@ pub(crate) fn inhibit_pb0_pb1_pb4_pb5(rcc: &stm32f722::RCC, port: &stm32f722::GP
|
||||
.ot4()
|
||||
.push_pull()
|
||||
.ot5()
|
||||
.push_pull()
|
||||
.push_pull();
|
||||
if pins == MotorPins::Six {
|
||||
write.ot6().push_pull().ot7().push_pull()
|
||||
} else {
|
||||
write
|
||||
}
|
||||
});
|
||||
port.ospeedr().modify(|_, write| {
|
||||
write
|
||||
let write = write
|
||||
.ospeedr0()
|
||||
.low_speed()
|
||||
.ospeedr1()
|
||||
@@ -45,10 +55,15 @@ pub(crate) fn inhibit_pb0_pb1_pb4_pb5(rcc: &stm32f722::RCC, port: &stm32f722::GP
|
||||
.ospeedr4()
|
||||
.low_speed()
|
||||
.ospeedr5()
|
||||
.low_speed()
|
||||
.low_speed();
|
||||
if pins == MotorPins::Six {
|
||||
write.ospeedr6().low_speed().ospeedr7().low_speed()
|
||||
} else {
|
||||
write
|
||||
}
|
||||
});
|
||||
port.pupdr().modify(|_, write| {
|
||||
write
|
||||
let write = write
|
||||
.pupdr0()
|
||||
.floating()
|
||||
.pupdr1()
|
||||
@@ -56,10 +71,15 @@ pub(crate) fn inhibit_pb0_pb1_pb4_pb5(rcc: &stm32f722::RCC, port: &stm32f722::GP
|
||||
.pupdr4()
|
||||
.floating()
|
||||
.pupdr5()
|
||||
.floating()
|
||||
.floating();
|
||||
if pins == MotorPins::Six {
|
||||
write.pupdr6().floating().pupdr7().floating()
|
||||
} else {
|
||||
write
|
||||
}
|
||||
});
|
||||
port.moder().modify(|_, write| {
|
||||
write
|
||||
let write = write
|
||||
.moder0()
|
||||
.output()
|
||||
.moder1()
|
||||
@@ -67,6 +87,51 @@ pub(crate) fn inhibit_pb0_pb1_pb4_pb5(rcc: &stm32f722::RCC, port: &stm32f722::GP
|
||||
.moder4()
|
||||
.output()
|
||||
.moder5()
|
||||
.output()
|
||||
.output();
|
||||
if pins == MotorPins::Six {
|
||||
write.moder6().output().moder7().output()
|
||||
} else {
|
||||
write
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use core::mem::MaybeUninit;
|
||||
|
||||
#[test]
|
||||
fn inhibits_only_board_motor_pins() {
|
||||
for (pins, pin_mask, field_mask, output_mode) in [
|
||||
(MotorPins::Four, 0x0033, 0x0f0f, 0x0505),
|
||||
(MotorPins::Six, 0x00f3, 0xff0f, 0x5505),
|
||||
] {
|
||||
// SAFETY: these PAC blocks contain u32 volatile cells and reserved
|
||||
// bytes. Zero is valid, and this test owns their ordinary RAM storage.
|
||||
let (rcc, port) = unsafe {
|
||||
(
|
||||
MaybeUninit::<stm32f722::rcc::RegisterBlock>::zeroed().assume_init(),
|
||||
MaybeUninit::<stm32f722::gpiob::RegisterBlock>::zeroed().assume_init(),
|
||||
)
|
||||
};
|
||||
// SAFETY: writes initialize only the test's local register storage.
|
||||
unsafe {
|
||||
rcc.ahb1enr().write(|write| write.bits(1));
|
||||
port.moder().write(|write| write.bits(u32::MAX));
|
||||
port.otyper().write(|write| write.bits(0xffff));
|
||||
port.ospeedr().write(|write| write.bits(u32::MAX));
|
||||
port.pupdr().write(|write| write.bits(0xaaaa_aaaa));
|
||||
}
|
||||
inhibit(&rcc, &port, pins);
|
||||
assert_eq!(rcc.ahb1enr().read().bits(), 3);
|
||||
// SAFETY: this reads the local backing cell, not a hardware write-only register.
|
||||
let reset = unsafe { port.bsrr().as_ptr().read_volatile() };
|
||||
assert_eq!(reset, pin_mask << 16);
|
||||
assert_eq!(port.moder().read().bits(), !field_mask | output_mode);
|
||||
assert_eq!(port.otyper().read().bits(), 0xffff & !pin_mask);
|
||||
assert_eq!(port.ospeedr().read().bits(), !field_mask);
|
||||
assert_eq!(port.pupdr().read().bits(), 0xaaaa_aaaa & !field_mask);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
//! A single permanently spawned RTIC worker, with bounded event coalescing.
|
||||
use core::{
|
||||
cell::RefCell,
|
||||
future::poll_fn,
|
||||
task::{Poll, Waker},
|
||||
};
|
||||
use critical_section::Mutex;
|
||||
|
||||
pub struct EventNotification {
|
||||
state: Mutex<RefCell<(u8, Option<Waker>)>>,
|
||||
}
|
||||
impl Default for EventNotification {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
impl EventNotification {
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
state: Mutex::new(RefCell::new((0, None))),
|
||||
}
|
||||
}
|
||||
pub fn notify(&self, events: u8) {
|
||||
let waker = critical_section::with(|cs| {
|
||||
let mut state = self.state.borrow(cs).borrow_mut();
|
||||
state.0 |= events;
|
||||
state.1.take()
|
||||
});
|
||||
if let Some(waker) = waker {
|
||||
waker.wake();
|
||||
}
|
||||
}
|
||||
pub async fn wait(&self) -> u8 {
|
||||
poll_fn(|cx| {
|
||||
critical_section::with(|cs| {
|
||||
let mut state = self.state.borrow(cs).borrow_mut();
|
||||
if state.0 != 0 {
|
||||
let events = core::mem::take(&mut state.0);
|
||||
state.1 = None;
|
||||
Poll::Ready(events)
|
||||
} else {
|
||||
state.1 = Some(cx.waker().clone());
|
||||
Poll::Pending
|
||||
}
|
||||
})
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
extern crate std;
|
||||
use super::*;
|
||||
use core::{future::Future, pin::pin, task::Context};
|
||||
use std::{
|
||||
sync::{
|
||||
Arc,
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
},
|
||||
task::Wake,
|
||||
};
|
||||
struct Counter(AtomicUsize);
|
||||
impl Wake for Counter {
|
||||
fn wake(self: Arc<Self>) {
|
||||
self.0.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn before_poll_during_work_and_after_registration_never_lose_events() {
|
||||
let signal = EventNotification::new();
|
||||
let counter = Arc::new(Counter(AtomicUsize::new(0)));
|
||||
let waker = Waker::from(counter.clone());
|
||||
let mut cx = Context::from_waker(&waker);
|
||||
signal.notify(1);
|
||||
assert_eq!(pin!(signal.wait()).poll(&mut cx), Poll::Ready(1));
|
||||
signal.notify(2);
|
||||
signal.notify(4);
|
||||
assert_eq!(pin!(signal.wait()).poll(&mut cx), Poll::Ready(6));
|
||||
let mut wait = pin!(signal.wait());
|
||||
assert_eq!(wait.as_mut().poll(&mut cx), Poll::Pending);
|
||||
signal.notify(8);
|
||||
signal.notify(16);
|
||||
assert_eq!(counter.0.load(Ordering::Relaxed), 1);
|
||||
assert_eq!(wait.poll(&mut cx), Poll::Ready(24));
|
||||
}
|
||||
}
|
||||
@@ -85,8 +85,8 @@ pub fn select_pll216_profile(
|
||||
return Err(ClockProfileError::OverDriveSwitchNotReady);
|
||||
}
|
||||
flash.acr().modify(|read, write| {
|
||||
// SAFETY: these are the STM32F7-documented latency, instruction-cache and
|
||||
// data-cache fields for 216 MHz; the read-modify-write retains other ACR bits.
|
||||
// SAFETY: these are the F722 latency, prefetch-enable and ART-enable
|
||||
// fields for 216 MHz; the read-modify-write retains other ACR bits.
|
||||
unsafe { write.bits((read.bits() & !0x0f) | 7 | (1 << 8) | (1 << 9)) }
|
||||
});
|
||||
|
||||
@@ -102,6 +102,9 @@ pub fn select_pll216_profile(
|
||||
}) {
|
||||
return Err(ClockProfileError::PllNotReady);
|
||||
}
|
||||
// Make the APB timer multiplier explicit rather than inheriting boot state.
|
||||
// With APB1=/4, TIMPRE=0 selects 2 * PCLK1 = 108 MHz for TIM2.
|
||||
rcc.dckcfgr1().modify(|_, write| write.timpre().clear_bit());
|
||||
rcc.cfgr().modify(|read, write| {
|
||||
// AHB /1, APB1 /4, APB2 /2, SYSCLK = PLL.
|
||||
// SAFETY: this preserves every CFGR field except the documented prescaler and
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
//! D-cache-disabled DMA memory policy for STM32F722 boards.
|
||||
|
||||
use kaptan_dma::{DmaReadableBuffer, DmaTxBufferFactory};
|
||||
///
|
||||
/// DMA transmit memory policy for a platform with D-cache disabled.
|
||||
///
|
||||
/// The platform linker policy must also place transfer frames in SRAM reachable
|
||||
/// by the selected DMA controller. This policy does not support a cached board;
|
||||
/// such a platform requires a distinct policy that performs cache maintenance.
|
||||
pub struct DcacheDisabledDmaMemory {
|
||||
_private: (),
|
||||
}
|
||||
|
||||
impl DcacheDisabledDmaMemory {
|
||||
/// Declares that the selected platform disabled D-cache before DMA setup.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// - D-cache must remain disabled for every CPU access to DMA-owned frames.
|
||||
/// - The linker policy must locate those frames in DMA-visible SRAM.
|
||||
pub const unsafe fn new() -> Self {
|
||||
Self { _private: () }
|
||||
}
|
||||
}
|
||||
|
||||
impl DmaTxBufferFactory for DcacheDisabledDmaMemory {
|
||||
type Error = core::convert::Infallible;
|
||||
|
||||
fn prepare<'a>(&mut self, bytes: &'a [u8]) -> Result<DmaReadableBuffer<'a>, Self::Error> {
|
||||
// SAFETY: `Self` can exist only after the platform proved its D-cache and
|
||||
// linker-memory policy. The runtime retains frame ownership until a
|
||||
// terminal DMA outcome or successful recovery.
|
||||
Ok(unsafe { DmaReadableBuffer::new_unchecked(bytes) })
|
||||
}
|
||||
}
|
||||
@@ -152,10 +152,7 @@ pub fn configure_usart1_rx_pin_pa10(port: &stm32f722::GPIOA) {
|
||||
// SAFETY: AF7 selects USART1 RX on PA10 on STM32F722.
|
||||
unsafe { write.afrh10().bits(0b0111) }
|
||||
});
|
||||
// USART is idle-high. Leaving PA10 floating before its peer drives the
|
||||
// line can manufacture framing and noise errors. The weak internal pull-up
|
||||
// supplies only the electrically correct idle level; it does not mask a
|
||||
// driven low, framing, noise, or overrun fault.
|
||||
// Hold RX idle-high while the peer is disconnected to avoid spurious errors.
|
||||
port.pupdr().modify(|_, write| write.pupdr10().pull_up());
|
||||
}
|
||||
|
||||
@@ -171,7 +168,41 @@ pub fn configure_usart2_pins_pa2_pa3(port: &stm32f722::GPIOA) {
|
||||
port.ospeedr()
|
||||
.modify(|_, write| write.ospeedr2().high_speed());
|
||||
port.pupdr()
|
||||
.modify(|_, write| write.pupdr2().floating().pupdr3().floating());
|
||||
.modify(|_, write| write.pupdr2().floating().pupdr3().pull_up());
|
||||
}
|
||||
|
||||
/// Configures PB10/PB11 for USART3 alternate function 7.
|
||||
pub fn configure_usart3_pins_pb10_pb11(rcc: &stm32f722::RCC, port: &stm32f722::GPIOB) {
|
||||
rcc.ahb1enr().modify(|_, write| write.gpioben().set_bit());
|
||||
let _ = rcc.ahb1enr().read();
|
||||
port.moder()
|
||||
.modify(|_, write| write.moder10().alternate().moder11().alternate());
|
||||
port.afrh().modify(|_, write| {
|
||||
// SAFETY: AF7 selects USART3 TX/RX on PB10/PB11 on STM32F722.
|
||||
unsafe { write.afrh10().bits(7).afrh11().bits(7) }
|
||||
});
|
||||
port.otyper().modify(|_, write| write.ot10().push_pull());
|
||||
port.ospeedr()
|
||||
.modify(|_, write| write.ospeedr10().high_speed());
|
||||
port.pupdr()
|
||||
.modify(|_, write| write.pupdr10().floating().pupdr11().pull_up());
|
||||
}
|
||||
|
||||
/// Configures PC6/PC7 for USART6 alternate function 8.
|
||||
pub fn configure_usart6_pins_pc6_pc7(rcc: &stm32f722::RCC, port: &stm32f722::GPIOC) {
|
||||
rcc.ahb1enr().modify(|_, write| write.gpiocen().set_bit());
|
||||
let _ = rcc.ahb1enr().read();
|
||||
port.moder()
|
||||
.modify(|_, write| write.moder6().alternate().moder7().alternate());
|
||||
port.afrl().modify(|_, write| {
|
||||
// SAFETY: AF8 selects USART6 TX/RX on PC6/PC7 on STM32F722.
|
||||
unsafe { write.afrl6().bits(8).afrl7().bits(8) }
|
||||
});
|
||||
port.otyper().modify(|_, write| write.ot6().push_pull());
|
||||
port.ospeedr()
|
||||
.modify(|_, write| write.ospeedr6().high_speed());
|
||||
port.pupdr()
|
||||
.modify(|_, write| write.pupdr6().floating().pupdr7().pull_up());
|
||||
}
|
||||
|
||||
/// Configures PA11/PA12 for the STM32F722 OTG FS data pair.
|
||||
@@ -203,7 +234,7 @@ pub fn configure_uart4_pins_pa0_pa1(port: &stm32f722::GPIOA) {
|
||||
port.ospeedr()
|
||||
.modify(|_, write| write.ospeedr0().high_speed());
|
||||
port.pupdr()
|
||||
.modify(|_, write| write.pupdr0().floating().pupdr1().floating());
|
||||
.modify(|_, write| write.pupdr0().floating().pupdr1().pull_up());
|
||||
}
|
||||
|
||||
/// Active-low status LED connected to PD2.
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
//! STM32F722 I²C1 interrupt-driven controller.
|
||||
|
||||
use kaptan_err_reportable::KaptanError;
|
||||
use kaptan_i2c::{I2cBus, I2cCompletion};
|
||||
use kaptan_i2c::{CompletionError, I2cBus, I2cCompletion};
|
||||
use kaptan_time::Timestamp;
|
||||
use stm32f7::stm32f722;
|
||||
|
||||
const MAX_BYTES: usize = 255;
|
||||
@@ -20,6 +21,9 @@ const ICR_ERRORS: u32 = (1 << 8) | (1 << 9) | (1 << 10);
|
||||
const ICR_TERMINAL: u32 = ICR_NACKCF | ICR_STOPCF | ICR_ERRORS;
|
||||
const ICR_FAULTS: u32 = ICR_NACKCF | ICR_ERRORS;
|
||||
const CR2_STOP: u32 = 1 << 14;
|
||||
// Disable the controller after 10 ms without STOP. This is a shutdown budget,
|
||||
// not an I²C clock-stretch limit.
|
||||
const STOP_TIMEOUT_US: u64 = 10_000;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, KaptanError)]
|
||||
pub enum I2c1Error {
|
||||
@@ -29,6 +33,8 @@ pub enum I2c1Error {
|
||||
InvalidAddress,
|
||||
#[kaptan_error(id = 11703)]
|
||||
Busy,
|
||||
#[kaptan_error(id = 11704)]
|
||||
Halted,
|
||||
}
|
||||
|
||||
/// Platform-selected I²C1 electrical timing.
|
||||
@@ -54,7 +60,9 @@ enum Direction {
|
||||
enum TransferState {
|
||||
Idle,
|
||||
Active,
|
||||
Stopping,
|
||||
Completed(I2cCompletion),
|
||||
Stopping { since: Timestamp },
|
||||
Halted,
|
||||
}
|
||||
|
||||
fn read_index_after_event(direction: Direction, flags: u32, index: usize, length: usize) -> usize {
|
||||
@@ -79,25 +87,60 @@ impl TransferState {
|
||||
}
|
||||
|
||||
fn after_stop(self) -> Self {
|
||||
if self == Self::Stopping {
|
||||
if matches!(self, Self::Stopping { .. }) {
|
||||
Self::Idle
|
||||
} else {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
fn after_driver_completion(self) -> Self {
|
||||
if self == Self::Active {
|
||||
Self::Idle
|
||||
fn completion(self, notification: I2cCompletion) -> Result<(), CompletionError> {
|
||||
if notification == I2cCompletion::NotPending {
|
||||
return Err(CompletionError::NotPending);
|
||||
}
|
||||
match self {
|
||||
Self::Completed(I2cCompletion::Complete) if notification == I2cCompletion::Complete => {
|
||||
Ok(())
|
||||
}
|
||||
Self::Completed(_) | Self::Stopping { .. } => Err(CompletionError::TransferFault),
|
||||
Self::Halted => Err(CompletionError::StopUnproven),
|
||||
Self::Active => Err(CompletionError::NotPending),
|
||||
Self::Idle => Err(CompletionError::NotActive),
|
||||
}
|
||||
}
|
||||
|
||||
fn expire_stop(&mut self, now: Timestamp) -> Result<(), CompletionError> {
|
||||
if matches!(*self, Self::Stopping { since } if now.as_microseconds().checked_sub(since.as_microseconds()).is_some_and(|elapsed| elapsed >= STOP_TIMEOUT_US))
|
||||
{
|
||||
*self = Self::Halted;
|
||||
Err(CompletionError::StopUnproven)
|
||||
} else {
|
||||
self
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Owns I²C1 while one interrupt-driven transaction is active.
|
||||
/// Only CPU ISR code accesses `buffer`, under the owner's exclusive borrow.
|
||||
/// DMA is never enabled, so moving/dropping this owner cannot free DMA memory.
|
||||
pub struct I2c1 {
|
||||
peripheral: stm32f722::I2C1,
|
||||
controller: Controller<stm32f722::I2C1>,
|
||||
}
|
||||
|
||||
// Register access shared by the PAC driver and test model.
|
||||
trait Registers {
|
||||
fn status(&self) -> u32;
|
||||
fn read_receive(&mut self) -> u8;
|
||||
fn write_transmit(&mut self, byte: u8);
|
||||
fn clear_flags(&mut self, flags: u32);
|
||||
fn control1(&self) -> u32;
|
||||
fn write_control1(&mut self, value: u32);
|
||||
fn control2(&self) -> u32;
|
||||
fn write_control2(&mut self, value: u32);
|
||||
}
|
||||
|
||||
struct Controller<R> {
|
||||
peripheral: R,
|
||||
buffer: [u8; MAX_BYTES],
|
||||
length: usize,
|
||||
index: usize,
|
||||
@@ -122,30 +165,42 @@ impl I2c1 {
|
||||
.cr1()
|
||||
.write(|w| unsafe { w.bits(CR1_INTERRUPTS) });
|
||||
Self {
|
||||
peripheral,
|
||||
buffer: [0; MAX_BYTES],
|
||||
length: 0,
|
||||
index: 0,
|
||||
direction: Direction::Write,
|
||||
state: TransferState::Idle,
|
||||
controller: Controller {
|
||||
peripheral,
|
||||
buffer: [0; MAX_BYTES],
|
||||
length: 0,
|
||||
index: 0,
|
||||
direction: Direction::Write,
|
||||
state: TransferState::Idle,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Advances the event IRQ; returns a terminal result only after STOPF.
|
||||
pub fn acknowledge_event_interrupt(&mut self) -> I2cCompletion {
|
||||
let flags = self.peripheral.isr().read().bits();
|
||||
/// Advances the event IRQ. Success requires STOPF. Faults request STOP and
|
||||
/// retain closed admission until STOP is observed or service halts the route.
|
||||
pub fn acknowledge_event_interrupt(&mut self, now: Timestamp) -> I2cCompletion {
|
||||
self.controller.acknowledge_event_interrupt(now)
|
||||
}
|
||||
|
||||
pub fn acknowledge_error_interrupt(&mut self, now: Timestamp) -> I2cCompletion {
|
||||
self.controller.acknowledge_error_interrupt(now)
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Registers> Controller<R> {
|
||||
fn acknowledge_event_interrupt(&mut self, now: Timestamp) -> I2cCompletion {
|
||||
let flags = self.peripheral.status();
|
||||
if self.state != TransferState::Active {
|
||||
self.clear_inactive_event_flags(flags);
|
||||
self.clear_inactive_event_flags(flags, now);
|
||||
return I2cCompletion::NotPending;
|
||||
}
|
||||
if flags & ISR_ERRORS != 0 {
|
||||
self.abort();
|
||||
self.acknowledge_stop_if_present(flags);
|
||||
return I2cCompletion::TransferFault;
|
||||
}
|
||||
if flags & ISR_NACKF != 0 {
|
||||
self.abort();
|
||||
self.acknowledge_stop_if_present(flags);
|
||||
if flags & (ISR_ERRORS | ISR_NACKF) != 0
|
||||
|| (flags & ISR_RXNE != 0
|
||||
&& (!matches!(self.direction, Direction::Read) || self.index >= self.length))
|
||||
{
|
||||
// RXNE on a write or beyond the read length is unusable data. It
|
||||
// must be consumed and byte IRQs disabled, not left level-pending.
|
||||
self.fail_transfer(flags, now);
|
||||
return I2cCompletion::TransferFault;
|
||||
}
|
||||
// With AUTOEND a final RXNE and STOPF may be visible in one ISR
|
||||
@@ -156,44 +211,62 @@ impl I2c1 {
|
||||
&& flags & ISR_TXIS != 0
|
||||
&& self.index < self.length
|
||||
{
|
||||
// SAFETY: the value is an eight-bit TXDR payload.
|
||||
self.peripheral
|
||||
.txdr()
|
||||
.write(|w| unsafe { w.bits(u32::from(self.buffer[self.index])) });
|
||||
self.peripheral.write_transmit(self.buffer[self.index]);
|
||||
self.index += 1;
|
||||
}
|
||||
let next_read_index =
|
||||
read_index_after_event(self.direction, flags, self.index, self.length);
|
||||
if next_read_index != self.index {
|
||||
self.buffer[self.index] = self.peripheral.rxdr().read().bits() as u8;
|
||||
self.buffer[self.index] = self.peripheral.read_receive();
|
||||
self.index = next_read_index;
|
||||
}
|
||||
if flags & ISR_STOPF != 0 {
|
||||
self.acknowledge_stop();
|
||||
return completion_after_stop(self.index, self.length);
|
||||
let completion = completion_after_stop(self.index, self.length);
|
||||
self.state = TransferState::Completed(completion);
|
||||
return completion;
|
||||
}
|
||||
I2cCompletion::NotPending
|
||||
}
|
||||
|
||||
pub fn acknowledge_error_interrupt(&mut self) -> I2cCompletion {
|
||||
let flags = self.peripheral.isr().read().bits();
|
||||
fn acknowledge_error_interrupt(&mut self, now: Timestamp) -> I2cCompletion {
|
||||
let flags = self.peripheral.status();
|
||||
if flags & ISR_ERRORS == 0 {
|
||||
return I2cCompletion::NotPending;
|
||||
}
|
||||
if self.state == TransferState::Active {
|
||||
self.abort();
|
||||
self.acknowledge_stop_if_present(flags);
|
||||
self.fail_transfer(flags, now);
|
||||
I2cCompletion::TransferFault
|
||||
} else {
|
||||
// A STOP may have already retired the software transaction before
|
||||
// the error IRQ is serviced. Its flags still must be acknowledged
|
||||
// or this high-priority IRQ will continuously retrigger.
|
||||
self.clear_inactive_event_flags(flags);
|
||||
self.clear_inactive_event_flags(flags, now);
|
||||
I2cCompletion::NotPending
|
||||
}
|
||||
}
|
||||
|
||||
fn fail_transfer(&mut self, flags: u32, now: Timestamp) {
|
||||
self.abort(now);
|
||||
// STOPCF does not clear RXNE. Discard RXDR before STOP retirement can
|
||||
// reopen admission, including simultaneous BERR/ARLO/OVR + STOPF + RXNE.
|
||||
self.discard_receive();
|
||||
self.acknowledge_stop_if_present(flags);
|
||||
if flags & ISR_STOPF != 0 {
|
||||
self.state = TransferState::Completed(I2cCompletion::TransferFault);
|
||||
}
|
||||
}
|
||||
|
||||
fn discard_receive(&mut self) {
|
||||
if self.peripheral.status() & ISR_RXNE != 0 {
|
||||
let _ = self.peripheral.read_receive();
|
||||
}
|
||||
}
|
||||
|
||||
fn start(&mut self, address: u8, length: usize, direction: Direction) -> Result<(), I2c1Error> {
|
||||
if self.state == TransferState::Halted {
|
||||
return Err(I2c1Error::Halted);
|
||||
}
|
||||
if length == 0 || length > MAX_BYTES {
|
||||
return Err(I2c1Error::InvalidLength);
|
||||
}
|
||||
@@ -203,14 +276,12 @@ impl I2c1 {
|
||||
if !self.state.accepts_start(self.bus_is_busy()) {
|
||||
return Err(I2c1Error::Busy);
|
||||
}
|
||||
// SAFETY: ICR_TERMINAL contains only documented write-one-to-clear flags.
|
||||
self.peripheral
|
||||
.icr()
|
||||
.write(|w| unsafe { w.bits(ICR_TERMINAL) });
|
||||
self.peripheral.clear_flags(ICR_TERMINAL);
|
||||
self.length = length;
|
||||
self.index = 0;
|
||||
self.direction = direction;
|
||||
self.state = TransferState::Active;
|
||||
self.peripheral.write_control1(CR1_INTERRUPTS);
|
||||
let direction_bit = if matches!(direction, Direction::Read) {
|
||||
1 << 10
|
||||
} else {
|
||||
@@ -221,35 +292,36 @@ impl I2c1 {
|
||||
| ((length as u32) << 16)
|
||||
| (1 << 25)
|
||||
| (1 << 13);
|
||||
// SAFETY: `cr2` contains a validated 7-bit address and documented transfer fields.
|
||||
self.peripheral.cr2().write(|w| unsafe { w.bits(cr2) });
|
||||
self.peripheral.write_control2(cr2);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn abort(&mut self) {
|
||||
fn abort(&mut self, now: Timestamp) {
|
||||
// Keep software ownership closed until STOPF is observed. A requested
|
||||
// STOP may be delayed indefinitely by a held bus, so this state keeps
|
||||
// a new START from being issued before the terminal STOPF arrives.
|
||||
self.state = TransferState::Stopping;
|
||||
// SAFETY: this only requests STOP while preserving the current CR2 fields.
|
||||
self.peripheral
|
||||
.cr2()
|
||||
.modify(|read, write| unsafe { write.bits(read.bits() | CR2_STOP) });
|
||||
// SAFETY: ICR_FAULTS contains only documented fault write-one-to-clear flags.
|
||||
self.peripheral
|
||||
.icr()
|
||||
.write(|w| unsafe { w.bits(ICR_FAULTS) });
|
||||
if matches!(
|
||||
self.state,
|
||||
TransferState::Stopping { .. } | TransferState::Halted
|
||||
) {
|
||||
return;
|
||||
}
|
||||
self.state = TransferState::Stopping { since: now };
|
||||
// Only terminal/error interrupts are useful during STOP. In particular,
|
||||
// TXIS must not continually retrigger while a slave holds the bus.
|
||||
let cr1 = self.peripheral.control1() & !((1 << 1) | (1 << 2));
|
||||
self.peripheral.write_control1(cr1);
|
||||
let cr2 = self.peripheral.control2() | CR2_STOP;
|
||||
self.peripheral.write_control2(cr2);
|
||||
self.peripheral.clear_flags(ICR_FAULTS);
|
||||
}
|
||||
|
||||
fn bus_is_busy(&self) -> bool {
|
||||
self.peripheral.isr().read().bits() & (1 << 15) != 0
|
||||
self.peripheral.status() & (1 << 15) != 0
|
||||
}
|
||||
|
||||
fn acknowledge_stop(&mut self) {
|
||||
// SAFETY: ICR_STOPCF is the documented STOP write-one-to-clear bit.
|
||||
self.peripheral
|
||||
.icr()
|
||||
.write(|w| unsafe { w.bits(ICR_STOPCF) });
|
||||
self.peripheral.clear_flags(ICR_STOPCF);
|
||||
self.state = self.state.after_stop();
|
||||
}
|
||||
|
||||
@@ -259,30 +331,28 @@ impl I2c1 {
|
||||
}
|
||||
}
|
||||
|
||||
fn clear_inactive_event_flags(&mut self, flags: u32) {
|
||||
fn clear_inactive_event_flags(&mut self, flags: u32, now: Timestamp) {
|
||||
// An abort may request STOP after the driver has already released the
|
||||
// transaction. Drain that terminal IRQ instead of leaving I2C1_EV
|
||||
// pending forever. RXNE is normally impossible here, but draining it
|
||||
// keeps a malformed terminal transfer from becoming an IRQ storm.
|
||||
if flags & ISR_RXNE != 0 {
|
||||
let _ = self.peripheral.rxdr().read().bits();
|
||||
}
|
||||
self.discard_receive();
|
||||
if flags & (ISR_NACKF | ISR_ERRORS) != 0 {
|
||||
// SAFETY: ICR_FAULTS contains only documented fault write-one-to-clear flags.
|
||||
self.peripheral
|
||||
.icr()
|
||||
.write(|w| unsafe { w.bits(ICR_FAULTS) });
|
||||
self.peripheral.clear_flags(ICR_FAULTS);
|
||||
}
|
||||
self.acknowledge_stop_if_present(flags);
|
||||
if flags & ISR_TXIS != 0 {
|
||||
self.abort();
|
||||
if flags & ISR_TXIS != 0 && flags & ISR_STOPF == 0 && self.state == TransferState::Idle {
|
||||
self.abort(now);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl I2cBus for I2c1 {
|
||||
impl<R: Registers> I2cBus for Controller<R> {
|
||||
type StartError = I2c1Error;
|
||||
fn start_write(&mut self, address: u8, write: &[u8]) -> Result<(), Self::StartError> {
|
||||
if self.state == TransferState::Halted {
|
||||
return Err(I2c1Error::Halted);
|
||||
}
|
||||
if write.is_empty() || write.len() > MAX_BYTES {
|
||||
return Err(I2c1Error::InvalidLength);
|
||||
}
|
||||
@@ -298,55 +368,331 @@ impl I2cBus for I2c1 {
|
||||
fn start_read(&mut self, address: u8, length: usize) -> Result<(), Self::StartError> {
|
||||
self.start(address, length, Direction::Read)
|
||||
}
|
||||
fn complete_transaction(&mut self, completion: I2cCompletion, read: Option<&mut [u8]>) {
|
||||
if completion == I2cCompletion::Complete
|
||||
&& matches!(self.direction, Direction::Read)
|
||||
&& let Some(read) = read
|
||||
fn complete_transaction(
|
||||
&mut self,
|
||||
completion: I2cCompletion,
|
||||
read: Option<&mut [u8]>,
|
||||
) -> Result<(), CompletionError> {
|
||||
let result = self.state.completion(completion);
|
||||
if !matches!(self.state, TransferState::Completed(_))
|
||||
|| completion == I2cCompletion::NotPending
|
||||
{
|
||||
debug_assert_eq!(read.len(), self.length);
|
||||
return result;
|
||||
}
|
||||
self.state = TransferState::Idle;
|
||||
result?;
|
||||
if matches!(self.direction, Direction::Read) {
|
||||
let read = read
|
||||
.filter(|read| read.len() == self.length)
|
||||
.ok_or(CompletionError::InvalidLength)?;
|
||||
read.copy_from_slice(&self.buffer[..self.length]);
|
||||
}
|
||||
self.state = self.state.after_driver_completion();
|
||||
self.length = 0;
|
||||
self.index = 0;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn cancel_transaction(&mut self) {
|
||||
fn cancel_transaction(&mut self, now: Timestamp) {
|
||||
if self.state == TransferState::Active {
|
||||
self.abort();
|
||||
self.abort(now);
|
||||
} else if matches!(self.state, TransferState::Completed(_)) {
|
||||
self.state = TransferState::Idle;
|
||||
}
|
||||
self.length = 0;
|
||||
self.index = 0;
|
||||
}
|
||||
|
||||
fn service(&mut self, now: Timestamp) -> Result<(), CompletionError> {
|
||||
if matches!(self.state, TransferState::Stopping { .. }) {
|
||||
let flags = self.peripheral.status();
|
||||
self.clear_inactive_event_flags(flags, now);
|
||||
if let Err(error) = self.state.expire_stop(now) {
|
||||
// RM0431 I2C_CR1.PE: disable peripheral and all IRQ sources.
|
||||
// This halts local work but does not prove a physical STOP.
|
||||
self.peripheral.write_control1(0);
|
||||
return Err(error);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl I2cBus for I2c1 {
|
||||
type StartError = I2c1Error;
|
||||
|
||||
fn start_write(&mut self, address: u8, write: &[u8]) -> Result<(), Self::StartError> {
|
||||
self.controller.start_write(address, write)
|
||||
}
|
||||
|
||||
fn start_read(&mut self, address: u8, length: usize) -> Result<(), Self::StartError> {
|
||||
self.controller.start_read(address, length)
|
||||
}
|
||||
|
||||
fn complete_transaction(
|
||||
&mut self,
|
||||
completion: I2cCompletion,
|
||||
read: Option<&mut [u8]>,
|
||||
) -> Result<(), CompletionError> {
|
||||
self.controller.complete_transaction(completion, read)
|
||||
}
|
||||
|
||||
fn cancel_transaction(&mut self, now: Timestamp) {
|
||||
self.controller.cancel_transaction(now);
|
||||
}
|
||||
|
||||
fn service(&mut self, now: Timestamp) -> Result<(), CompletionError> {
|
||||
self.controller.service(now)
|
||||
}
|
||||
}
|
||||
|
||||
impl Registers for stm32f722::I2C1 {
|
||||
fn status(&self) -> u32 {
|
||||
self.isr().read().bits()
|
||||
}
|
||||
|
||||
fn read_receive(&mut self) -> u8 {
|
||||
self.rxdr().read().bits() as u8
|
||||
}
|
||||
|
||||
fn write_transmit(&mut self, byte: u8) {
|
||||
// SAFETY: byte is an eight-bit TXDR payload.
|
||||
self.txdr().write(|w| unsafe { w.bits(u32::from(byte)) });
|
||||
}
|
||||
|
||||
fn clear_flags(&mut self, flags: u32) {
|
||||
// SAFETY: the private controller supplies only ICR W1C flag masks.
|
||||
self.icr().write(|w| unsafe { w.bits(flags) });
|
||||
}
|
||||
|
||||
fn control1(&self) -> u32 {
|
||||
self.cr1().read().bits()
|
||||
}
|
||||
|
||||
fn write_control1(&mut self, value: u32) {
|
||||
// SAFETY: the private controller writes documented CR1 enables or
|
||||
// preserves their current values while clearing TXIE/RXIE.
|
||||
self.cr1().write(|w| unsafe { w.bits(value) });
|
||||
}
|
||||
|
||||
fn control2(&self) -> u32 {
|
||||
self.cr2().read().bits()
|
||||
}
|
||||
|
||||
fn write_control2(&mut self, value: u32) {
|
||||
// SAFETY: the private controller validates transfer fields for START,
|
||||
// or preserves those fields while requesting STOP.
|
||||
self.cr2().write(|w| unsafe { w.bits(value) });
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
Direction, I2cCompletion, ICR_ERRORS, ICR_FAULTS, ICR_NACKCF, ICR_STOPCF, ISR_RXNE,
|
||||
ISR_STOPF, TransferState, completion_after_stop, read_index_after_event,
|
||||
};
|
||||
use super::*;
|
||||
|
||||
fn t(us: u64) -> Timestamp {
|
||||
Timestamp::from_microseconds(us)
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct RegisterModel {
|
||||
flags: u32,
|
||||
cr1: u32,
|
||||
cr2: u32,
|
||||
rxdr: u8,
|
||||
receive_reads: usize,
|
||||
transmitted: [u8; 2],
|
||||
transmit_writes: usize,
|
||||
stop_cleared_with_rxne: bool,
|
||||
}
|
||||
|
||||
impl RegisterModel {
|
||||
fn receive(&mut self, byte: u8) {
|
||||
assert_eq!(self.flags & ISR_RXNE, 0, "unread RXDR would overrun");
|
||||
self.rxdr = byte;
|
||||
self.flags |= ISR_RXNE;
|
||||
}
|
||||
|
||||
fn stop(&mut self) {
|
||||
self.flags = (self.flags & !(1 << 15)) | ISR_STOPF;
|
||||
self.cr2 &= !CR2_STOP;
|
||||
}
|
||||
|
||||
fn interrupt_pending(&self) -> bool {
|
||||
self.cr1 & 1 != 0
|
||||
&& (self.cr1 & self.flags & (ISR_TXIS | ISR_RXNE | ISR_NACKF | ISR_STOPF) != 0
|
||||
|| (self.cr1 & (1 << 7) != 0 && self.flags & ISR_ERRORS != 0))
|
||||
}
|
||||
}
|
||||
|
||||
impl Registers for RegisterModel {
|
||||
fn status(&self) -> u32 {
|
||||
self.flags
|
||||
}
|
||||
|
||||
fn read_receive(&mut self) -> u8 {
|
||||
self.receive_reads += 1;
|
||||
self.flags &= !ISR_RXNE;
|
||||
self.rxdr
|
||||
}
|
||||
|
||||
fn write_transmit(&mut self, byte: u8) {
|
||||
self.transmitted[self.transmit_writes] = byte;
|
||||
self.transmit_writes += 1;
|
||||
self.flags &= !ISR_TXIS;
|
||||
}
|
||||
|
||||
fn clear_flags(&mut self, flags: u32) {
|
||||
if flags & ICR_STOPCF != 0 && self.flags & ISR_RXNE != 0 {
|
||||
self.stop_cleared_with_rxne = true;
|
||||
}
|
||||
// RM0431: ICR W1C bits clear the corresponding terminal/error flags.
|
||||
// Neither STOPCF nor any other ICR write consumes RXDR/RXNE.
|
||||
self.flags &= !(flags & ICR_TERMINAL);
|
||||
}
|
||||
|
||||
fn control1(&self) -> u32 {
|
||||
self.cr1
|
||||
}
|
||||
fn write_control1(&mut self, value: u32) {
|
||||
self.cr1 = value;
|
||||
}
|
||||
fn control2(&self) -> u32 {
|
||||
self.cr2
|
||||
}
|
||||
|
||||
fn write_control2(&mut self, value: u32) {
|
||||
self.cr2 = value & !(1 << 13);
|
||||
if value & (1 << 13) != 0 {
|
||||
self.flags |= 1 << 15;
|
||||
if value & (1 << 10) == 0 {
|
||||
self.flags |= ISR_TXIS;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn software_controller(state: TransferState) -> Controller<RegisterModel> {
|
||||
Controller {
|
||||
peripheral: RegisterModel::default(),
|
||||
buffer: [0xa5; MAX_BYTES],
|
||||
length: 2,
|
||||
index: 1,
|
||||
direction: Direction::Read,
|
||||
state,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_starts_while_a_transaction_or_recovery_is_pending() {
|
||||
assert!(TransferState::Idle.accepts_start(false));
|
||||
assert!(!TransferState::Idle.accepts_start(true));
|
||||
assert!(!TransferState::Active.accepts_start(false));
|
||||
assert!(!TransferState::Stopping.accepts_start(false));
|
||||
assert!(!TransferState::Stopping { since: t(0) }.accepts_start(false));
|
||||
assert!(!TransferState::Halted.accepts_start(false));
|
||||
assert!(!TransferState::Completed(I2cCompletion::Complete).accepts_start(false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn abort_stays_closed_until_stop_and_normal_completion_releases_ownership() {
|
||||
assert_eq!(TransferState::Stopping.after_stop(), TransferState::Idle);
|
||||
fn abort_stays_closed_until_stop_and_expires_by_elapsed_time() {
|
||||
let mut stopping = TransferState::Stopping { since: t(123) };
|
||||
assert_eq!(stopping.after_stop(), TransferState::Idle);
|
||||
assert_eq!(TransferState::Active.after_stop(), TransferState::Active);
|
||||
assert_eq!(stopping.expire_stop(t(123)), Ok(()));
|
||||
assert_eq!(stopping.expire_stop(t(10_122)), Ok(()));
|
||||
assert_eq!(
|
||||
TransferState::Active.after_driver_completion(),
|
||||
TransferState::Idle
|
||||
stopping.expire_stop(t(10_123)),
|
||||
Err(CompletionError::StopUnproven)
|
||||
);
|
||||
assert_eq!(stopping, TransferState::Halted);
|
||||
assert_eq!(stopping.expire_stop(t(u64::MAX)), Ok(()));
|
||||
assert_eq!(stopping, TransferState::Halted);
|
||||
assert_eq!(TransferState::Halted.after_stop(), TransferState::Halted);
|
||||
let mut controller = software_controller(TransferState::Halted);
|
||||
assert_eq!(controller.start_read(98, 2), Err(I2c1Error::Halted));
|
||||
assert_eq!(controller.start_write(98, &[1]), Err(I2c1Error::Halted));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn not_pending_never_changes_active_completed_or_stopping_ownership_or_bytes() {
|
||||
for state in [
|
||||
TransferState::Active,
|
||||
TransferState::Completed(I2cCompletion::Complete),
|
||||
TransferState::Stopping { since: t(123) },
|
||||
TransferState::Halted,
|
||||
] {
|
||||
let mut controller = software_controller(state);
|
||||
let mut destination = [0x55; 2];
|
||||
assert_eq!(
|
||||
controller.complete_transaction(I2cCompletion::NotPending, Some(&mut destination)),
|
||||
Err(CompletionError::NotPending)
|
||||
);
|
||||
assert_eq!(controller.state, state);
|
||||
assert_eq!((controller.length, controller.index), (2, 1));
|
||||
assert_eq!(controller.buffer, [0xa5; MAX_BYTES]);
|
||||
assert_eq!(destination, [0x55; 2]);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delayed_notification_cannot_complete_new_active_transaction() {
|
||||
let mut controller = software_controller(TransferState::Active);
|
||||
let mut destination = [0x55; 2];
|
||||
assert_eq!(
|
||||
TransferState::Stopping.after_driver_completion(),
|
||||
TransferState::Stopping
|
||||
controller.complete_transaction(I2cCompletion::Complete, Some(&mut destination)),
|
||||
Err(CompletionError::NotPending)
|
||||
);
|
||||
assert_eq!(controller.state, TransferState::Active);
|
||||
assert_eq!(destination, [0x55; 2]);
|
||||
controller.state = TransferState::Completed(I2cCompletion::Complete);
|
||||
assert_eq!(
|
||||
controller.complete_transaction(I2cCompletion::Complete, Some(&mut destination)),
|
||||
Ok(())
|
||||
);
|
||||
assert_eq!(destination, [0xa5; 2]);
|
||||
assert_eq!(controller.state, TransferState::Idle);
|
||||
destination.fill(0x55);
|
||||
assert_eq!(
|
||||
controller.complete_transaction(I2cCompletion::Complete, Some(&mut destination)),
|
||||
Err(CompletionError::NotActive)
|
||||
);
|
||||
assert_eq!(destination, [0x55; 2]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fault_length_error_and_unproven_stop_never_copy_out() {
|
||||
for (state, length, error) in [
|
||||
(
|
||||
TransferState::Completed(I2cCompletion::TransferFault),
|
||||
2,
|
||||
CompletionError::TransferFault,
|
||||
),
|
||||
(
|
||||
TransferState::Completed(I2cCompletion::Complete),
|
||||
1,
|
||||
CompletionError::InvalidLength,
|
||||
),
|
||||
(
|
||||
TransferState::Stopping { since: t(100) },
|
||||
2,
|
||||
CompletionError::TransferFault,
|
||||
),
|
||||
(TransferState::Halted, 2, CompletionError::StopUnproven),
|
||||
] {
|
||||
let mut controller = software_controller(state);
|
||||
let mut destination = [0x55; 2];
|
||||
assert_eq!(
|
||||
controller.complete_transaction(
|
||||
I2cCompletion::Complete,
|
||||
Some(&mut destination[..length])
|
||||
),
|
||||
Err(error)
|
||||
);
|
||||
assert_eq!(destination, [0x55; 2]);
|
||||
if matches!(
|
||||
state,
|
||||
TransferState::Stopping { .. } | TransferState::Halted
|
||||
) {
|
||||
assert_eq!(controller.state, state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -362,4 +708,141 @@ mod tests {
|
||||
assert_eq!(index, 2);
|
||||
assert_eq!(completion_after_stop(index, 2), I2cCompletion::Complete);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn both_irq_paths_discard_fault_stop_rxne_before_following_write_or_read() {
|
||||
for error_irq in [false, true] {
|
||||
for fault in [ISR_BERR, ISR_ARLO, ISR_OVR, ISR_NACKF] {
|
||||
if error_irq && fault == ISR_NACKF {
|
||||
continue;
|
||||
}
|
||||
for next_direction in [Direction::Write, Direction::Read] {
|
||||
let mut controller = software_controller(TransferState::Idle);
|
||||
controller.start_read(98, 2).unwrap();
|
||||
controller.peripheral.receive(0xde);
|
||||
controller.peripheral.flags |= fault;
|
||||
controller.peripheral.stop();
|
||||
assert!(controller.peripheral.interrupt_pending());
|
||||
let completion = if error_irq {
|
||||
controller.acknowledge_error_interrupt(t(100))
|
||||
} else {
|
||||
controller.acknowledge_event_interrupt(t(100))
|
||||
};
|
||||
assert_eq!(completion, I2cCompletion::TransferFault);
|
||||
assert_eq!(controller.peripheral.receive_reads, 1);
|
||||
assert!(!controller.peripheral.stop_cleared_with_rxne);
|
||||
assert!(!controller.peripheral.interrupt_pending());
|
||||
assert_eq!(controller.buffer, [0xa5; MAX_BYTES]);
|
||||
assert_eq!(
|
||||
controller.complete_transaction(completion, None),
|
||||
Err(CompletionError::TransferFault)
|
||||
);
|
||||
assert_eq!(controller.state, TransferState::Idle);
|
||||
|
||||
match next_direction {
|
||||
Direction::Write => {
|
||||
controller.start_write(98, &[0x10, 0x20]).unwrap();
|
||||
for _ in 0..2 {
|
||||
controller.peripheral.flags |= ISR_TXIS;
|
||||
assert_eq!(
|
||||
controller.acknowledge_event_interrupt(t(200)),
|
||||
I2cCompletion::NotPending
|
||||
);
|
||||
assert!(!controller.peripheral.interrupt_pending());
|
||||
}
|
||||
controller.peripheral.stop();
|
||||
assert_eq!(
|
||||
controller.acknowledge_event_interrupt(t(300)),
|
||||
I2cCompletion::Complete
|
||||
);
|
||||
controller
|
||||
.complete_transaction(I2cCompletion::Complete, None)
|
||||
.unwrap();
|
||||
assert_eq!(controller.peripheral.transmitted, [0x10, 0x20]);
|
||||
assert_eq!(controller.peripheral.receive_reads, 1);
|
||||
}
|
||||
Direction::Read => {
|
||||
controller.start_read(98, 2).unwrap();
|
||||
assert!(!controller.peripheral.interrupt_pending());
|
||||
assert_eq!(
|
||||
controller.acknowledge_event_interrupt(t(200)),
|
||||
I2cCompletion::NotPending
|
||||
);
|
||||
assert_eq!(
|
||||
controller.index, 0,
|
||||
"no old RXDR byte may enter the new read"
|
||||
);
|
||||
controller.peripheral.receive(0x12);
|
||||
assert_eq!(
|
||||
controller.acknowledge_event_interrupt(t(250)),
|
||||
I2cCompletion::NotPending
|
||||
);
|
||||
controller.peripheral.receive(0x34);
|
||||
controller.peripheral.stop();
|
||||
assert_eq!(
|
||||
controller.acknowledge_event_interrupt(t(300)),
|
||||
I2cCompletion::Complete
|
||||
);
|
||||
let mut read = [0; 2];
|
||||
controller
|
||||
.complete_transaction(I2cCompletion::Complete, Some(&mut read))
|
||||
.unwrap();
|
||||
assert_eq!(read, [0x12, 0x34]);
|
||||
assert_eq!(controller.peripheral.receive_reads, 3);
|
||||
}
|
||||
}
|
||||
assert_eq!(controller.state, TransferState::Idle);
|
||||
assert!(!controller.peripheral.interrupt_pending());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unexpected_rxne_during_write_is_drained_and_cannot_starve_stop_deadline() {
|
||||
let mut controller = software_controller(TransferState::Idle);
|
||||
controller.start_write(98, &[1, 2]).unwrap();
|
||||
controller.peripheral.receive(0xde);
|
||||
assert_eq!(
|
||||
controller.acknowledge_event_interrupt(t(100)),
|
||||
I2cCompletion::TransferFault
|
||||
);
|
||||
assert_eq!(controller.peripheral.receive_reads, 1);
|
||||
assert_eq!(controller.peripheral.transmit_writes, 0);
|
||||
assert_eq!(controller.state, TransferState::Stopping { since: t(100) });
|
||||
assert!(!controller.peripheral.interrupt_pending());
|
||||
assert_eq!(controller.start_read(98, 2), Err(I2c1Error::Busy));
|
||||
controller.service(t(10_099)).unwrap();
|
||||
assert_eq!(
|
||||
controller.service(t(10_100)),
|
||||
Err(CompletionError::StopUnproven)
|
||||
);
|
||||
assert_eq!(controller.peripheral.cr1, 0);
|
||||
assert_eq!(controller.state, TransferState::Halted);
|
||||
controller.peripheral.stop();
|
||||
assert_eq!(
|
||||
controller.acknowledge_event_interrupt(t(10_101)),
|
||||
I2cCompletion::NotPending
|
||||
);
|
||||
assert_eq!(controller.state, TransferState::Halted);
|
||||
assert!(!controller.peripheral.interrupt_pending());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extra_rxne_at_read_end_is_discarded_as_fault_even_with_stop() {
|
||||
let mut controller = software_controller(TransferState::Idle);
|
||||
controller.start_read(98, 1).unwrap();
|
||||
controller.peripheral.receive(0x12);
|
||||
controller.acknowledge_event_interrupt(t(100));
|
||||
controller.peripheral.receive(0xde);
|
||||
controller.peripheral.stop();
|
||||
assert_eq!(
|
||||
controller.acknowledge_event_interrupt(t(200)),
|
||||
I2cCompletion::TransferFault
|
||||
);
|
||||
assert_eq!(controller.buffer[0], 0x12);
|
||||
assert_eq!(controller.peripheral.receive_reads, 2);
|
||||
assert!(!controller.peripheral.stop_cleared_with_rxne);
|
||||
assert!(!controller.peripheral.interrupt_pending());
|
||||
}
|
||||
}
|
||||
@@ -1,35 +1,29 @@
|
||||
//! STM32F722 peripheral implementations selected by supported boards.
|
||||
//!
|
||||
//! This module exports only typed peripheral building blocks. The family
|
||||
//! platform selects pins, clocks, interrupt priorities, and static ownership.
|
||||
//! STM32F722 peripheral drivers.
|
||||
|
||||
mod clock;
|
||||
mod dma_buffer;
|
||||
mod gpio;
|
||||
mod i2c1;
|
||||
mod spi1;
|
||||
mod spi3;
|
||||
mod spi_dma;
|
||||
mod tim2;
|
||||
mod time;
|
||||
mod usart;
|
||||
mod usart1_tx_dma2_stream7;
|
||||
mod usart2_tx_dma1_stream6;
|
||||
mod usb;
|
||||
|
||||
pub use clock::select_pll216_profile;
|
||||
pub use dma_buffer::DcacheDisabledDmaMemory;
|
||||
pub use gpio::{
|
||||
ActiveLowLedPd2, configure_exti3_pin_pc3, configure_exti4_pin_pa4, configure_i2c1_pins_pb8_pb9,
|
||||
configure_spi1_pins_pa5_pa6_pa7, configure_spi3_pins_pc10_pc11_pc12,
|
||||
configure_uart4_pins_pa0_pa1, configure_usart1_rx_pin_pa10, configure_usart1_tx_pin_pa9,
|
||||
configure_usart2_pins_pa2_pa3, configure_usb_fs_pins_pa11_pa12,
|
||||
configure_usart2_pins_pa2_pa3, configure_usart3_pins_pb10_pb11, configure_usart6_pins_pc6_pc7,
|
||||
configure_usb_fs_pins_pa11_pa12,
|
||||
};
|
||||
pub use i2c1::{I2c1, I2c1Config, I2c1Error};
|
||||
pub use spi1::{Spi1, Spi1Error};
|
||||
pub use spi3::{Spi3, Spi3Error};
|
||||
pub use tim2::{Tim2Periodic, Tim2PeriodicConfig};
|
||||
pub use time::DwtMicrosecondClock;
|
||||
pub use usart::{ReceiveInterrupt, Uart4, Usart1Tx, Usart2Tx};
|
||||
pub use usart1_tx_dma2_stream7::Usart1TxDma;
|
||||
pub use usart2_tx_dma1_stream6::Usart2TxDma;
|
||||
pub use tim2::Tim2Periodic;
|
||||
pub use time::TimerClock;
|
||||
pub(crate) use usart::OwnedUart;
|
||||
pub use usart::{ReceiveInterrupt, Uart4, Usart1Tx, Usart2Tx, Usart3, Usart6};
|
||||
pub(crate) use usb::{F722UsbMavlink, usb_mavlink};
|
||||
@@ -1,44 +1,71 @@
|
||||
//! STM32F722 SPI1 full-duplex DMA controller.
|
||||
//!
|
||||
//! SPI1 uses DMA2 Stream0/Channel3 for receive and DMA2 Stream3/Channel3 for
|
||||
//! transmit. The receive terminal event is the transaction boundary because it
|
||||
//! is generated after the last serial frame; transmit completion can occur
|
||||
//! while the final serial clock is still in progress.
|
||||
//! transmit. Receive completion requests finalization; the worker independently
|
||||
//! proves both streams and the bus quiescent before copying out any bytes.
|
||||
|
||||
use core::sync::atomic::{AtomicBool, Ordering};
|
||||
use kaptan_err_reportable::KaptanError;
|
||||
use kaptan_spi::SpiCompletion;
|
||||
use kaptan_spi::{
|
||||
CompletionError, SpiCompletion,
|
||||
dma::{StartError, Storage, Transfer},
|
||||
};
|
||||
use stm32f7::stm32f722;
|
||||
|
||||
const MAX_TRANSFER_LENGTH: usize = u16::MAX as usize;
|
||||
use super::spi_dma;
|
||||
|
||||
const MAX_TRANSFER_LENGTH: usize = 32;
|
||||
|
||||
// The F722 linker places .bss in DMA-accessible SRAM1/2, not DTCM. Platform
|
||||
// clock initialization disables D-cache before constructing any SPI route.
|
||||
// This pair is never freed or reclaimed, even if Spi1 is dropped/forgotten.
|
||||
static DMA_STORAGE: Storage<MAX_TRANSFER_LENGTH> = Storage::new();
|
||||
// Preserve errors even if an ISR clears their flags while finalization runs.
|
||||
static FAULT_SEEN: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
/// Failure while submitting one SPI1 DMA transaction.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, KaptanError)]
|
||||
pub enum Spi1Error {
|
||||
/// The transaction is malformed or exceeds the DMA counter range.
|
||||
/// The transaction is empty or exceeds the route's 32-byte capacity.
|
||||
#[kaptan_error(id = 11501)]
|
||||
InvalidLength,
|
||||
/// A previous DMA transaction still owns SPI1.
|
||||
#[kaptan_error(id = 11502)]
|
||||
Busy,
|
||||
/// Hardware shutdown could not prove quiescence; this route cannot restart.
|
||||
#[kaptan_error(id = 11503)]
|
||||
Quarantined,
|
||||
}
|
||||
|
||||
impl From<StartError> for Spi1Error {
|
||||
fn from(error: StartError) -> Self {
|
||||
match error {
|
||||
StartError::InvalidLength => Self::InvalidLength,
|
||||
StartError::Busy => Self::Busy,
|
||||
StartError::Quarantined => Self::Quarantined,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Owns SPI1 and its fixed full-duplex DMA2 request route.
|
||||
pub struct Spi1 {
|
||||
peripheral: stm32f722::SPI1,
|
||||
controller: stm32f722::DMA2,
|
||||
active: bool,
|
||||
// Transfer is Send through its one-time static owner; moving Spi1 never
|
||||
// moves DMA bytes. ISR helpers touch status only and cannot reclaim it.
|
||||
transfer: Transfer<MAX_TRANSFER_LENGTH>,
|
||||
}
|
||||
|
||||
impl Spi1 {
|
||||
/// Configures SPI1 mode 0 master operation at APB2/128 and reserves DMA2.
|
||||
///
|
||||
/// This keeps the MPU6000 route below 1 MHz with the board's 108 MHz
|
||||
/// APB2 clock while retaining substantial margin for board-level wiring.
|
||||
/// Configures SPI1 mode 0 at APB2/128 (below 1 MHz) and reserves DMA2.
|
||||
pub fn new(
|
||||
rcc: &stm32f722::RCC,
|
||||
peripheral: stm32f722::SPI1,
|
||||
controller: stm32f722::DMA2,
|
||||
) -> Self {
|
||||
// Claim before touching hardware. A second construction fails closed,
|
||||
// including after the original owner was dropped during a transfer.
|
||||
let transfer = DMA_STORAGE.claim().expect("SPI1 storage already claimed");
|
||||
rcc.apb2rstr().modify(|_, write| write.spi1rst().set_bit());
|
||||
rcc.apb2rstr()
|
||||
.modify(|_, write| write.spi1rst().clear_bit());
|
||||
@@ -63,24 +90,26 @@ impl Spi1 {
|
||||
Self {
|
||||
peripheral,
|
||||
controller,
|
||||
active: false,
|
||||
transfer,
|
||||
}
|
||||
}
|
||||
|
||||
/// Starts a full-duplex DMA transaction without polling a peripheral flag.
|
||||
pub fn start_transfer(&mut self, transmit: &[u8], receive: &mut [u8]) -> Result<(), Spi1Error> {
|
||||
if transmit.is_empty()
|
||||
|| transmit.len() != receive.len()
|
||||
|| transmit.len() > MAX_TRANSFER_LENGTH
|
||||
{
|
||||
return Err(Spi1Error::InvalidLength);
|
||||
}
|
||||
if self.active
|
||||
|| self.rx_stream().cr().read().en().is_enabled()
|
||||
/// Checks admission before the electrical route asserts chip select.
|
||||
pub(crate) fn check_start(&self, length: usize) -> Result<(), Spi1Error> {
|
||||
self.transfer.check_start(length)?;
|
||||
if self.rx_stream().cr().read().en().is_enabled()
|
||||
|| self.tx_stream().cr().read().en().is_enabled()
|
||||
|| !spi_dma::bus_finished(&self.peripheral)
|
||||
{
|
||||
return Err(Spi1Error::Busy);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Copies into permanent storage and starts DMA without waiting for flags.
|
||||
pub fn start_transfer(&mut self, transmit: &[u8]) -> Result<(), Spi1Error> {
|
||||
self.check_start(transmit.len())?;
|
||||
let endpoints = self.transfer.begin(transmit)?;
|
||||
|
||||
// Arm the complete DMA receive path before SPI starts generating
|
||||
// clocks. This avoids an RX FIFO overrun between transactions.
|
||||
@@ -90,19 +119,21 @@ impl Spi1 {
|
||||
self.clear_spi_overrun();
|
||||
self.clear_rx_flags();
|
||||
self.clear_tx_flags();
|
||||
self.configure_rx(receive);
|
||||
self.configure_tx(transmit);
|
||||
FAULT_SEEN.store(false, Ordering::Release);
|
||||
self.configure_rx(endpoints.receive, endpoints.length);
|
||||
self.configure_tx(endpoints.transmit, endpoints.length);
|
||||
self.peripheral.cr2().modify(|_, write| {
|
||||
let write = write.rxdmaen().set_bit().txdmaen().set_bit();
|
||||
if transmit.len() & 1 == 0 {
|
||||
write.ldma_rx().even().ldma_tx().even()
|
||||
} else {
|
||||
// With 8-bit frames, F7 SPI needs this for the final
|
||||
// byte of an odd-length DMA transaction. MPU samples are
|
||||
// one command byte plus fourteen data bytes.
|
||||
// Mark the final byte of an odd-length DMA transfer.
|
||||
write.ldma_rx().odd().ldma_tx().odd()
|
||||
}
|
||||
});
|
||||
// begin supplied the compiler Release fence. With D-cache disabled,
|
||||
// DSB commits SRAM writes and peripheral setup before DMA can access it.
|
||||
cortex_m::asm::dsb();
|
||||
self.rx_stream()
|
||||
.cr()
|
||||
.modify(|_, write| write.en().enabled());
|
||||
@@ -112,34 +143,53 @@ impl Spi1 {
|
||||
self.peripheral
|
||||
.cr1()
|
||||
.modify(|_, write| write.spe().set_bit());
|
||||
self.active = true;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Releases controller-side ownership after a DMA terminal event.
|
||||
///
|
||||
/// Both streams are explicitly disabled for every outcome, including an
|
||||
/// SPI peripheral fault. Disabling the SPI DMA requests first prevents a
|
||||
/// new request while the stream hardware drains its enable bit. This
|
||||
/// method deliberately does not wait for that bit: the next submission
|
||||
/// observes it and returns `Busy` until the F7 DMA controller completes
|
||||
/// the documented disable sequence.
|
||||
pub fn complete_transfer(&mut self, _: SpiCompletion) {
|
||||
self.peripheral
|
||||
.cr2()
|
||||
.modify(|_, write| write.rxdmaen().clear_bit().txdmaen().clear_bit());
|
||||
self.peripheral
|
||||
.cr1()
|
||||
.modify(|_, write| write.spe().clear_bit());
|
||||
self.rx_stream()
|
||||
.cr()
|
||||
.modify(|_, write| write.en().clear_bit());
|
||||
self.tx_stream()
|
||||
.cr()
|
||||
.modify(|_, write| write.en().clear_bit());
|
||||
/// Accepts a terminal notification only after bounded hardware shutdown.
|
||||
pub fn complete_transfer(
|
||||
&mut self,
|
||||
completion: SpiCompletion,
|
||||
receive: &mut [u8],
|
||||
) -> Result<(), CompletionError> {
|
||||
// In particular, NotPending must not disable an active transfer.
|
||||
self.transfer.check_completion(completion)?;
|
||||
let outcome = match spi_dma::complete_transfer(
|
||||
&self.peripheral,
|
||||
self.rx_stream(),
|
||||
self.tx_stream(),
|
||||
completion,
|
||||
|| self.has_transfer_error(),
|
||||
) {
|
||||
Ok(outcome) => outcome,
|
||||
Err(error) => {
|
||||
self.transfer.quarantine();
|
||||
return Err(error);
|
||||
}
|
||||
};
|
||||
self.clear_rx_flags();
|
||||
self.clear_tx_flags();
|
||||
self.active = false;
|
||||
// SAFETY: Both EN bits were observed clear, TX FIFO empty and BSY=0
|
||||
// before disabling SPI. DMA requests remain disabled. The helper's DSB and
|
||||
// finish's Acquire compiler fence order DMA writes before copy-out.
|
||||
// Complete additionally requires both counters exhausted and no fault.
|
||||
unsafe { self.transfer.finish(outcome, receive) }
|
||||
}
|
||||
|
||||
fn has_transfer_error(&self) -> bool {
|
||||
let dma = self.controller.lisr().read();
|
||||
let spi = self.peripheral.sr().read();
|
||||
dma.teif0().is_error()
|
||||
|| dma.dmeif0().is_error()
|
||||
|| dma.feif0().is_error()
|
||||
|| dma.teif3().is_error()
|
||||
|| dma.dmeif3().is_error()
|
||||
|| dma.feif3().is_error()
|
||||
|| spi.ovr().is_overrun()
|
||||
|| spi.modf().is_fault()
|
||||
|| spi.fre().is_error()
|
||||
|| spi.crcerr().is_no_match()
|
||||
|| FAULT_SEEN.load(Ordering::Acquire)
|
||||
}
|
||||
|
||||
/// Acknowledges an SPI peripheral overrun interrupt.
|
||||
@@ -153,6 +203,7 @@ impl Spi1 {
|
||||
if !peripheral.sr().read().ovr().is_overrun() {
|
||||
return false;
|
||||
}
|
||||
FAULT_SEEN.store(true, Ordering::Release);
|
||||
let _ = peripheral.dr().read().bits();
|
||||
let _ = peripheral.sr().read().bits();
|
||||
true
|
||||
@@ -166,6 +217,9 @@ impl Spi1 {
|
||||
let status = controller.lisr().read();
|
||||
let fault =
|
||||
status.teif0().is_error() || status.dmeif0().is_error() || status.feif0().is_error();
|
||||
if fault {
|
||||
FAULT_SEEN.store(true, Ordering::Release);
|
||||
}
|
||||
let complete = status.tcif0().is_complete();
|
||||
controller.lifcr().write(|write| {
|
||||
write
|
||||
@@ -197,6 +251,9 @@ impl Spi1 {
|
||||
let status = controller.lisr().read();
|
||||
let fault =
|
||||
status.teif3().is_error() || status.dmeif3().is_error() || status.feif3().is_error();
|
||||
if fault {
|
||||
FAULT_SEEN.store(true, Ordering::Release);
|
||||
}
|
||||
controller.lifcr().write(|write| {
|
||||
write
|
||||
.cfeif3()
|
||||
@@ -217,7 +274,7 @@ impl Spi1 {
|
||||
}
|
||||
}
|
||||
|
||||
fn configure_rx(&self, receive: &mut [u8]) {
|
||||
fn configure_rx(&self, receive: *mut u8, length: usize) {
|
||||
self.rx_stream().cr().write(|write| {
|
||||
// SAFETY: Channel 3 is SPI1_RX on DMA2 Stream0; checked slice
|
||||
// length fits NDTR, and all values map directly to documented bits.
|
||||
@@ -250,18 +307,19 @@ impl Spi1 {
|
||||
self.rx_stream()
|
||||
.ndtr()
|
||||
// SAFETY: `start_transfer` bounds the length to the 16-bit NDTR field.
|
||||
.write(|write| unsafe { write.bits(receive.len() as u32) });
|
||||
.write(|write| unsafe { write.bits(length as u32) });
|
||||
self.rx_stream()
|
||||
.par()
|
||||
// SAFETY: `DR` is the memory-mapped SPI1 DMA peripheral endpoint.
|
||||
.write(|write| unsafe { write.bits(self.peripheral.dr().as_ptr() as u32) });
|
||||
self.rx_stream()
|
||||
.m0ar()
|
||||
// SAFETY: the caller retains `receive` until terminal completion.
|
||||
.write(|write| unsafe { write.bits(receive.as_mut_ptr() as u32) });
|
||||
// SAFETY: begin reserved the exclusively claimed static RX slot.
|
||||
// Its allocation survives owner drop/forget and cannot be reclaimed.
|
||||
.write(|write| unsafe { write.bits(receive as u32) });
|
||||
}
|
||||
|
||||
fn configure_tx(&self, transmit: &[u8]) {
|
||||
fn configure_tx(&self, transmit: *const u8, length: usize) {
|
||||
self.tx_stream().cr().write(|write| {
|
||||
// SAFETY: Channel 3 is SPI1_TX on DMA2 Stream3; checked slice
|
||||
// length fits NDTR, and all values map directly to documented bits.
|
||||
@@ -292,15 +350,16 @@ impl Spi1 {
|
||||
self.tx_stream()
|
||||
.ndtr()
|
||||
// SAFETY: `start_transfer` bounds the length to the 16-bit NDTR field.
|
||||
.write(|write| unsafe { write.bits(transmit.len() as u32) });
|
||||
.write(|write| unsafe { write.bits(length as u32) });
|
||||
self.tx_stream()
|
||||
.par()
|
||||
// SAFETY: `DR` is the memory-mapped SPI1 DMA peripheral endpoint.
|
||||
.write(|write| unsafe { write.bits(self.peripheral.dr().as_ptr() as u32) });
|
||||
self.tx_stream()
|
||||
.m0ar()
|
||||
// SAFETY: the caller retains `transmit` until terminal completion.
|
||||
.write(|write| unsafe { write.bits(transmit.as_ptr() as u32) });
|
||||
// SAFETY: begin copied into and reserved the permanent TX slot;
|
||||
// no pointer to caller or movable driver storage reaches hardware.
|
||||
.write(|write| unsafe { write.bits(transmit as u32) });
|
||||
}
|
||||
|
||||
fn rx_stream(&self) -> &stm32f722::dma2::ST {
|
||||
|
||||
@@ -1,44 +1,71 @@
|
||||
//! STM32F722 SPI3 full-duplex DMA controller.
|
||||
//!
|
||||
//! SPI3 uses DMA1 Stream0/Channel0 for receive and DMA1 Stream5/Channel0 for
|
||||
//! transmit. The receive terminal event is the transaction boundary because it
|
||||
//! is generated after the last serial frame; transmit completion can occur
|
||||
//! while the final serial clock is still in progress.
|
||||
//! transmit. Receive completion requests finalization; the worker independently
|
||||
//! proves both streams and the bus quiescent before copying out any bytes.
|
||||
|
||||
use core::sync::atomic::{AtomicBool, Ordering};
|
||||
use kaptan_err_reportable::KaptanError;
|
||||
use kaptan_spi::SpiCompletion;
|
||||
use kaptan_spi::{
|
||||
CompletionError, SpiCompletion,
|
||||
dma::{StartError, Storage, Transfer},
|
||||
};
|
||||
use stm32f7::stm32f722;
|
||||
|
||||
const MAX_TRANSFER_LENGTH: usize = u16::MAX as usize;
|
||||
use super::spi_dma;
|
||||
|
||||
const MAX_TRANSFER_LENGTH: usize = 32;
|
||||
|
||||
// The F722 linker places .bss in DMA-accessible SRAM1/2, not DTCM. Platform
|
||||
// clock initialization disables D-cache before constructing any SPI route.
|
||||
// This pair is never freed or reclaimed, even if Spi3 is dropped/forgotten.
|
||||
static DMA_STORAGE: Storage<MAX_TRANSFER_LENGTH> = Storage::new();
|
||||
// Preserve errors even if an ISR clears their flags while finalization runs.
|
||||
static FAULT_SEEN: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
/// Failure while submitting one SPI3 DMA transaction.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, KaptanError)]
|
||||
pub enum Spi3Error {
|
||||
/// The transaction is malformed or exceeds the DMA counter range.
|
||||
/// The transaction is empty or exceeds the route's 32-byte capacity.
|
||||
#[kaptan_error(id = 11601)]
|
||||
InvalidLength,
|
||||
/// A previous DMA transaction still owns SPI3.
|
||||
#[kaptan_error(id = 11602)]
|
||||
Busy,
|
||||
/// Hardware shutdown could not prove quiescence; this route cannot restart.
|
||||
#[kaptan_error(id = 11603)]
|
||||
Quarantined,
|
||||
}
|
||||
|
||||
impl From<StartError> for Spi3Error {
|
||||
fn from(error: StartError) -> Self {
|
||||
match error {
|
||||
StartError::InvalidLength => Self::InvalidLength,
|
||||
StartError::Busy => Self::Busy,
|
||||
StartError::Quarantined => Self::Quarantined,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Owns SPI3 and its fixed full-duplex DMA1 request route.
|
||||
pub struct Spi3 {
|
||||
peripheral: stm32f722::SPI3,
|
||||
controller: stm32f722::DMA1,
|
||||
active: bool,
|
||||
// Transfer is Send through its one-time static owner; moving Spi3 never
|
||||
// moves DMA bytes. ISR helpers touch status only and cannot reclaim it.
|
||||
transfer: Transfer<MAX_TRANSFER_LENGTH>,
|
||||
}
|
||||
|
||||
impl Spi3 {
|
||||
/// Configures SPI3 mode 0 master operation at APB1/128 and reserves DMA1.
|
||||
///
|
||||
/// This keeps the ICM-42688-P route below 1 MHz with the board's 54 MHz
|
||||
/// APB1 clock while retaining substantial margin for board-level wiring.
|
||||
/// Configures SPI3 mode 0 at APB1/128 (below 1 MHz) and reserves DMA1.
|
||||
pub fn new(
|
||||
rcc: &stm32f722::RCC,
|
||||
peripheral: stm32f722::SPI3,
|
||||
controller: stm32f722::DMA1,
|
||||
) -> Self {
|
||||
// Claim before touching hardware. A second construction fails closed,
|
||||
// including after the original owner was dropped during a transfer.
|
||||
let transfer = DMA_STORAGE.claim().expect("SPI3 storage already claimed");
|
||||
rcc.apb1rstr().modify(|_, write| write.spi3rst().set_bit());
|
||||
rcc.apb1rstr()
|
||||
.modify(|_, write| write.spi3rst().clear_bit());
|
||||
@@ -63,24 +90,26 @@ impl Spi3 {
|
||||
Self {
|
||||
peripheral,
|
||||
controller,
|
||||
active: false,
|
||||
transfer,
|
||||
}
|
||||
}
|
||||
|
||||
/// Starts a full-duplex DMA transaction without polling a peripheral flag.
|
||||
pub fn start_transfer(&mut self, transmit: &[u8], receive: &mut [u8]) -> Result<(), Spi3Error> {
|
||||
if transmit.is_empty()
|
||||
|| transmit.len() != receive.len()
|
||||
|| transmit.len() > MAX_TRANSFER_LENGTH
|
||||
{
|
||||
return Err(Spi3Error::InvalidLength);
|
||||
}
|
||||
if self.active
|
||||
|| self.rx_stream().cr().read().en().is_enabled()
|
||||
/// Checks admission before the electrical route asserts chip select.
|
||||
pub(crate) fn check_start(&self, length: usize) -> Result<(), Spi3Error> {
|
||||
self.transfer.check_start(length)?;
|
||||
if self.rx_stream().cr().read().en().is_enabled()
|
||||
|| self.tx_stream().cr().read().en().is_enabled()
|
||||
|| !spi_dma::bus_finished(&self.peripheral)
|
||||
{
|
||||
return Err(Spi3Error::Busy);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Copies into permanent storage and starts DMA without waiting for flags.
|
||||
pub fn start_transfer(&mut self, transmit: &[u8]) -> Result<(), Spi3Error> {
|
||||
self.check_start(transmit.len())?;
|
||||
let endpoints = self.transfer.begin(transmit)?;
|
||||
|
||||
// Arm the complete DMA receive path before SPI starts generating
|
||||
// clocks. This avoids an RX FIFO overrun between transactions.
|
||||
@@ -90,19 +119,21 @@ impl Spi3 {
|
||||
self.clear_spi_overrun();
|
||||
self.clear_rx_flags();
|
||||
self.clear_tx_flags();
|
||||
self.configure_rx(receive);
|
||||
self.configure_tx(transmit);
|
||||
FAULT_SEEN.store(false, Ordering::Release);
|
||||
self.configure_rx(endpoints.receive, endpoints.length);
|
||||
self.configure_tx(endpoints.transmit, endpoints.length);
|
||||
self.peripheral.cr2().modify(|_, write| {
|
||||
let write = write.rxdmaen().set_bit().txdmaen().set_bit();
|
||||
if transmit.len() & 1 == 0 {
|
||||
write.ldma_rx().even().ldma_tx().even()
|
||||
} else {
|
||||
// With 8-bit frames, F7 SPI needs this for the final
|
||||
// byte of an odd-length DMA transaction. IMU samples are
|
||||
// one command byte plus fourteen data bytes.
|
||||
// Mark the final byte of an odd-length DMA transfer.
|
||||
write.ldma_rx().odd().ldma_tx().odd()
|
||||
}
|
||||
});
|
||||
// begin supplied the compiler Release fence. With D-cache disabled,
|
||||
// DSB commits SRAM writes and peripheral setup before DMA can access it.
|
||||
cortex_m::asm::dsb();
|
||||
self.rx_stream()
|
||||
.cr()
|
||||
.modify(|_, write| write.en().enabled());
|
||||
@@ -112,34 +143,54 @@ impl Spi3 {
|
||||
self.peripheral
|
||||
.cr1()
|
||||
.modify(|_, write| write.spe().set_bit());
|
||||
self.active = true;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Releases controller-side ownership after a DMA terminal event.
|
||||
///
|
||||
/// Both streams are explicitly disabled for every outcome, including an
|
||||
/// SPI peripheral fault. Disabling the SPI DMA requests first prevents a
|
||||
/// new request while the stream hardware drains its enable bit. This
|
||||
/// method deliberately does not wait for that bit: the next submission
|
||||
/// observes it and returns `Busy` until the F7 DMA controller completes
|
||||
/// the documented disable sequence.
|
||||
pub fn complete_transfer(&mut self, _: SpiCompletion) {
|
||||
self.peripheral
|
||||
.cr2()
|
||||
.modify(|_, write| write.rxdmaen().clear_bit().txdmaen().clear_bit());
|
||||
self.peripheral
|
||||
.cr1()
|
||||
.modify(|_, write| write.spe().clear_bit());
|
||||
self.rx_stream()
|
||||
.cr()
|
||||
.modify(|_, write| write.en().clear_bit());
|
||||
self.tx_stream()
|
||||
.cr()
|
||||
.modify(|_, write| write.en().clear_bit());
|
||||
/// Accepts a terminal notification only after bounded hardware shutdown.
|
||||
pub fn complete_transfer(
|
||||
&mut self,
|
||||
completion: SpiCompletion,
|
||||
receive: &mut [u8],
|
||||
) -> Result<(), CompletionError> {
|
||||
// In particular, NotPending must not disable an active transfer.
|
||||
self.transfer.check_completion(completion)?;
|
||||
let outcome = match spi_dma::complete_transfer(
|
||||
&self.peripheral,
|
||||
self.rx_stream(),
|
||||
self.tx_stream(),
|
||||
completion,
|
||||
|| self.has_transfer_error(),
|
||||
) {
|
||||
Ok(outcome) => outcome,
|
||||
Err(error) => {
|
||||
self.transfer.quarantine();
|
||||
return Err(error);
|
||||
}
|
||||
};
|
||||
self.clear_rx_flags();
|
||||
self.clear_tx_flags();
|
||||
self.active = false;
|
||||
// SAFETY: Both EN bits were observed clear, TX FIFO empty and BSY=0
|
||||
// before disabling SPI. DMA requests remain disabled. The helper's DSB and
|
||||
// finish's Acquire compiler fence order DMA writes before copy-out.
|
||||
// Complete additionally requires both counters exhausted and no fault.
|
||||
unsafe { self.transfer.finish(outcome, receive) }
|
||||
}
|
||||
|
||||
fn has_transfer_error(&self) -> bool {
|
||||
let rx = self.controller.lisr().read();
|
||||
let tx = self.controller.hisr().read();
|
||||
let spi = self.peripheral.sr().read();
|
||||
rx.teif0().is_error()
|
||||
|| rx.dmeif0().is_error()
|
||||
|| rx.feif0().is_error()
|
||||
|| tx.teif5().is_error()
|
||||
|| tx.dmeif5().is_error()
|
||||
|| tx.feif5().is_error()
|
||||
|| spi.ovr().is_overrun()
|
||||
|| spi.modf().is_fault()
|
||||
|| spi.fre().is_error()
|
||||
|| spi.crcerr().is_no_match()
|
||||
|| FAULT_SEEN.load(Ordering::Acquire)
|
||||
}
|
||||
|
||||
/// Acknowledges an SPI peripheral overrun interrupt.
|
||||
@@ -153,6 +204,7 @@ impl Spi3 {
|
||||
if !peripheral.sr().read().ovr().is_overrun() {
|
||||
return false;
|
||||
}
|
||||
FAULT_SEEN.store(true, Ordering::Release);
|
||||
let _ = peripheral.dr().read().bits();
|
||||
let _ = peripheral.sr().read().bits();
|
||||
true
|
||||
@@ -166,6 +218,9 @@ impl Spi3 {
|
||||
let status = controller.lisr().read();
|
||||
let fault =
|
||||
status.teif0().is_error() || status.dmeif0().is_error() || status.feif0().is_error();
|
||||
if fault {
|
||||
FAULT_SEEN.store(true, Ordering::Release);
|
||||
}
|
||||
let complete = status.tcif0().is_complete();
|
||||
controller.lifcr().write(|write| {
|
||||
write
|
||||
@@ -197,6 +252,9 @@ impl Spi3 {
|
||||
let status = controller.hisr().read();
|
||||
let fault =
|
||||
status.teif5().is_error() || status.dmeif5().is_error() || status.feif5().is_error();
|
||||
if fault {
|
||||
FAULT_SEEN.store(true, Ordering::Release);
|
||||
}
|
||||
controller.hifcr().write(|write| {
|
||||
write
|
||||
.cfeif5()
|
||||
@@ -217,7 +275,7 @@ impl Spi3 {
|
||||
}
|
||||
}
|
||||
|
||||
fn configure_rx(&self, receive: &mut [u8]) {
|
||||
fn configure_rx(&self, receive: *mut u8, length: usize) {
|
||||
self.rx_stream().cr().write(|write| {
|
||||
// SAFETY: Channel 0 is SPI3_RX on DMA1 Stream0; checked slice
|
||||
// length fits NDTR, and all values map directly to documented bits.
|
||||
@@ -250,18 +308,19 @@ impl Spi3 {
|
||||
self.rx_stream()
|
||||
.ndtr()
|
||||
// SAFETY: `start_transfer` bounds the length to the 16-bit NDTR field.
|
||||
.write(|write| unsafe { write.bits(receive.len() as u32) });
|
||||
.write(|write| unsafe { write.bits(length as u32) });
|
||||
self.rx_stream()
|
||||
.par()
|
||||
// SAFETY: `DR` is the memory-mapped SPI3 DMA peripheral endpoint.
|
||||
.write(|write| unsafe { write.bits(self.peripheral.dr().as_ptr() as u32) });
|
||||
self.rx_stream()
|
||||
.m0ar()
|
||||
// SAFETY: the caller retains `receive` until terminal completion.
|
||||
.write(|write| unsafe { write.bits(receive.as_mut_ptr() as u32) });
|
||||
// SAFETY: begin reserved the exclusively claimed static RX slot.
|
||||
// Its allocation survives owner drop/forget and cannot be reclaimed.
|
||||
.write(|write| unsafe { write.bits(receive as u32) });
|
||||
}
|
||||
|
||||
fn configure_tx(&self, transmit: &[u8]) {
|
||||
fn configure_tx(&self, transmit: *const u8, length: usize) {
|
||||
self.tx_stream().cr().write(|write| {
|
||||
// SAFETY: Channel 0 is SPI3_TX on DMA1 Stream5; checked slice
|
||||
// length fits NDTR, and all values map directly to documented bits.
|
||||
@@ -292,15 +351,16 @@ impl Spi3 {
|
||||
self.tx_stream()
|
||||
.ndtr()
|
||||
// SAFETY: `start_transfer` bounds the length to the 16-bit NDTR field.
|
||||
.write(|write| unsafe { write.bits(transmit.len() as u32) });
|
||||
.write(|write| unsafe { write.bits(length as u32) });
|
||||
self.tx_stream()
|
||||
.par()
|
||||
// SAFETY: `DR` is the memory-mapped SPI3 DMA peripheral endpoint.
|
||||
.write(|write| unsafe { write.bits(self.peripheral.dr().as_ptr() as u32) });
|
||||
self.tx_stream()
|
||||
.m0ar()
|
||||
// SAFETY: the caller retains `transmit` until terminal completion.
|
||||
.write(|write| unsafe { write.bits(transmit.as_ptr() as u32) });
|
||||
// SAFETY: begin copied into and reserved the permanent TX slot;
|
||||
// no pointer to caller or movable driver storage reaches hardware.
|
||||
.write(|write| unsafe { write.bits(transmit as u32) });
|
||||
}
|
||||
|
||||
fn rx_stream(&self) -> &stm32f722::dma1::ST {
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
use kaptan_spi::{CompletionError, SpiCompletion};
|
||||
use stm32f7::stm32f722::{dma2, spi1};
|
||||
|
||||
const QUIESCE_POLLS: usize = 1024;
|
||||
|
||||
pub(super) fn bus_finished(spi: &spi1::RegisterBlock) -> bool {
|
||||
let status = spi.sr().read();
|
||||
status.ftlvl().is_empty() && status.bsy().is_not_busy()
|
||||
}
|
||||
|
||||
pub(super) fn complete_transfer(
|
||||
spi: &spi1::RegisterBlock,
|
||||
rx: &dma2::ST,
|
||||
tx: &dma2::ST,
|
||||
completion: SpiCompletion,
|
||||
has_fault: impl FnOnce() -> bool,
|
||||
) -> Result<SpiCompletion, CompletionError> {
|
||||
spi.cr2()
|
||||
.modify(|_, write| write.rxdmaen().clear_bit().txdmaen().clear_bit());
|
||||
rx.cr().modify(|_, write| write.en().clear_bit());
|
||||
tx.cr().modify(|_, write| write.en().clear_bit());
|
||||
// Leave SPI enabled while queued frames drain. Writing EN=0 does not
|
||||
// establish that DMA stopped or that the final SPI clock completed.
|
||||
let quiescent = (0..QUIESCE_POLLS).any(|_| {
|
||||
rx.cr().read().en().is_disabled() && tx.cr().read().en().is_disabled() && bus_finished(spi)
|
||||
});
|
||||
spi.cr1().modify(|_, write| write.spe().clear_bit());
|
||||
cortex_m::asm::dsb();
|
||||
if !quiescent {
|
||||
return Err(CompletionError::Quarantined);
|
||||
}
|
||||
let complete = completion == SpiCompletion::Complete
|
||||
&& rx.ndtr().read().bits() == 0
|
||||
&& tx.ndtr().read().bits() == 0
|
||||
&& spi.sr().read().frlvl().is_empty()
|
||||
&& !has_fault();
|
||||
// Cancellation can leave old frames in the four-byte RX FIFO. Check
|
||||
// faults before draining: the DR/SR reads can clear an overrun.
|
||||
for _ in 0..4 {
|
||||
if spi.sr().read().frlvl().is_empty() {
|
||||
break;
|
||||
}
|
||||
let _ = spi.dr().read().bits();
|
||||
}
|
||||
if !spi.sr().read().frlvl().is_empty() {
|
||||
return Err(CompletionError::Quarantined);
|
||||
}
|
||||
Ok(if complete {
|
||||
SpiCompletion::Complete
|
||||
} else {
|
||||
SpiCompletion::TransferFault
|
||||
})
|
||||
}
|
||||
@@ -1,56 +1,123 @@
|
||||
//! STM32F722 TIM2 periodic interrupt configuration.
|
||||
//! One owned TIM2 counter for deadlines and 100 Hz CC1 service interrupts.
|
||||
|
||||
use cortex_m::peripheral::NVIC;
|
||||
use stm32f7::stm32f722;
|
||||
|
||||
/// Precomputed TIM2 periodic-update configuration.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct Tim2PeriodicConfig {
|
||||
/// Timer prescaler register value.
|
||||
pub prescaler: u16,
|
||||
/// Timer auto-reload register value.
|
||||
pub auto_reload: u32,
|
||||
use super::time::{SERVICE_PERIOD_US, SharedTimer, TimerBackend, TimerClock};
|
||||
|
||||
static TIMER: SharedTimer<Tim2Counter> = SharedTimer::new();
|
||||
|
||||
/// Service-IRQ handle for the same counter and extension as every TimerClock.
|
||||
pub struct Tim2Periodic {
|
||||
shared: &'static SharedTimer<Tim2Counter>,
|
||||
}
|
||||
|
||||
/// TIM2 configured to issue periodic update interrupts.
|
||||
pub struct Tim2Periodic {
|
||||
_peripheral: stm32f722::TIM2,
|
||||
pub(super) struct Tim2Counter {
|
||||
peripheral: stm32f722::TIM2,
|
||||
}
|
||||
|
||||
impl Tim2Periodic {
|
||||
/// Enables TIM2 with the supplied platform-derived period configuration.
|
||||
pub fn new(
|
||||
/// Starts the fixed 1 MHz clock, leaving its IRQ disabled until RTIC init
|
||||
/// has assembled every resource. Ownership moves into permanent storage.
|
||||
pub(crate) fn new(
|
||||
rcc: &stm32f722::RCC,
|
||||
peripheral: stm32f722::TIM2,
|
||||
config: Tim2PeriodicConfig,
|
||||
) -> Self {
|
||||
dbgmcu: &stm32f722::DBGMCU,
|
||||
) -> Option<(TimerClock, Self)> {
|
||||
if !TIMER.initialize(|| Tim2Counter::new(rcc, peripheral, dbgmcu)) {
|
||||
return None;
|
||||
}
|
||||
Some((TimerClock { shared: &TIMER }, Self { shared: &TIMER }))
|
||||
}
|
||||
|
||||
/// Called last in successful RTIC init, with PRIMASK set. The init tail
|
||||
/// restores TIM2's NVIC enable before RTIC publishes resources and releases
|
||||
/// PRIMASK. Partial initialization never enables this peripheral interrupt.
|
||||
pub fn enable_interrupt(&mut self) {
|
||||
self.shared.enable_interrupt();
|
||||
}
|
||||
|
||||
/// Returns true only for CC1IF. Compare rearming and epoch extension finish
|
||||
/// before the caller may notify workers or invoke any callbacks.
|
||||
pub fn on_interrupt(&mut self) -> bool {
|
||||
self.shared.on_interrupt()
|
||||
}
|
||||
}
|
||||
|
||||
impl Tim2Counter {
|
||||
fn new(rcc: &stm32f722::RCC, peripheral: stm32f722::TIM2, dbgmcu: &stm32f722::DBGMCU) -> Self {
|
||||
NVIC::mask(stm32f722::Interrupt::TIM2);
|
||||
rcc.apb1enr().modify(|_, write| write.tim2en().set_bit());
|
||||
let _ = rcc.apb1enr().read();
|
||||
rcc.apb1rstr().modify(|_, write| write.tim2rst().set_bit());
|
||||
rcc.apb1rstr()
|
||||
.modify(|_, write| write.tim2rst().clear_bit());
|
||||
rcc.apb1enr().modify(|_, write| write.tim2en().set_bit());
|
||||
peripheral.cr1().modify(|_, write| write.cen().clear_bit());
|
||||
peripheral.psc().write(|write| {
|
||||
// SAFETY: `prescaler` is exactly the 16-bit PSC register width.
|
||||
unsafe { write.psc().bits(config.prescaler) }
|
||||
});
|
||||
peripheral.arr().write(|write| {
|
||||
// SAFETY: TIM2 ARR is a 32-bit register and accepts `auto_reload`.
|
||||
unsafe { write.arr().bits(config.auto_reload) }
|
||||
});
|
||||
rcc.apb1lpenr()
|
||||
.modify(|_, write| write.tim2lpen().set_bit());
|
||||
dbgmcu
|
||||
.apb1_fz()
|
||||
.modify(|_, write| write.dbg_tim2_stop().clear_bit());
|
||||
|
||||
// Reset values select internal clock, continuous edge-aligned upcount,
|
||||
// UIFREMAP=0, frozen output compare, no preload and no GPIO output.
|
||||
peripheral.cr1().reset();
|
||||
peripheral.cr2().reset();
|
||||
peripheral.smcr().reset();
|
||||
peripheral.ccmr1_output().reset();
|
||||
peripheral.ccer().reset();
|
||||
peripheral.dier().reset();
|
||||
// TIMPRE=0, HCLK=216 MHz, APB1=/4: TIM2 input is 108 MHz.
|
||||
peripheral.psc().write(|write| write.psc().set(107));
|
||||
peripheral.arr().write(|write| write.arr().set(u32::MAX));
|
||||
// Latch PSC while stopped, then establish the shared zero epoch and
|
||||
// first compare before publishing handles or enabling any interrupt.
|
||||
peripheral.egr().write(|write| write.ug().set_bit());
|
||||
// TIM2 status flags are write-zero-to-clear. Use the PAC's explicit
|
||||
// W0C operation rather than a read-modify-write after `EGR.UG`.
|
||||
peripheral.sr().write(|write| write.uif().clear());
|
||||
peripheral.dier().modify(|_, write| write.uie().set_bit());
|
||||
peripheral.cnt().write(|write| write.cnt().set(0));
|
||||
peripheral
|
||||
.ccr1()
|
||||
.write(|write| write.ccr().set(SERVICE_PERIOD_US));
|
||||
peripheral.sr().reset();
|
||||
NVIC::unpend(stm32f722::Interrupt::TIM2);
|
||||
peripheral.cr1().modify(|_, write| write.cen().set_bit());
|
||||
Self { peripheral }
|
||||
}
|
||||
}
|
||||
|
||||
impl TimerBackend for Tim2Counter {
|
||||
fn counter(&mut self) -> u32 {
|
||||
self.peripheral.cnt().read().cnt().bits()
|
||||
}
|
||||
|
||||
fn compare_pending(&mut self) -> bool {
|
||||
self.peripheral.sr().read().cc1if().bit_is_set()
|
||||
}
|
||||
|
||||
fn acknowledge_compare(&mut self) {
|
||||
// PAC 0.16 SR::ZERO_TO_MODIFY_FIELDS_BITMAP=0x1e5f makes write()
|
||||
// preserve all other W0C flags. No SR read-modify-write and no later
|
||||
// blanket clear that could erase a newly raised compare flag.
|
||||
self.peripheral.sr().write(|write| write.cc1if().clear());
|
||||
cortex_m::asm::dsb();
|
||||
}
|
||||
|
||||
fn set_compare(&mut self, counter: u32) {
|
||||
self.peripheral
|
||||
.ccr1()
|
||||
.write(|write| write.ccr().set(counter));
|
||||
// Complete the APB write before the shared mechanism verifies CNT.
|
||||
cortex_m::asm::dsb();
|
||||
}
|
||||
|
||||
Self {
|
||||
_peripheral: peripheral,
|
||||
}
|
||||
fn enable_compare_interrupt(&mut self) {
|
||||
// CC1IE only. UIE and every DMA request remain disabled.
|
||||
self.peripheral
|
||||
.dier()
|
||||
.write(|write| write.cc1ie().set_bit());
|
||||
cortex_m::asm::dsb();
|
||||
}
|
||||
|
||||
/// Acknowledges a TIM2 update interrupt.
|
||||
pub fn acknowledge_update(&mut self) {
|
||||
// `UIF` is write-zero-to-clear (W0C).
|
||||
self._peripheral.sr().write(|write| write.uif().clear());
|
||||
fn pend_interrupt(&mut self) {
|
||||
NVIC::pend(stm32f722::Interrupt::TIM2);
|
||||
cortex_m::asm::dsb();
|
||||
}
|
||||
}
|
||||
@@ -1,106 +1,166 @@
|
||||
//! DWT-backed monotonic microsecond clock for STM32F722.
|
||||
//! Shared TIM2 microsecond epoch and compare-service mechanism.
|
||||
|
||||
use cortex_m::peripheral::{DCB, DWT};
|
||||
use kaptan_err_reportable::KaptanError;
|
||||
use core::cell::RefCell;
|
||||
use critical_section::Mutex;
|
||||
use kaptan_time::{MonotonicClock, Timestamp};
|
||||
|
||||
/// A DWT cycle counter extended into monotonic microseconds.
|
||||
use super::tim2::Tim2Counter;
|
||||
|
||||
pub(super) const SERVICE_PERIOD_US: u32 = 10_000;
|
||||
// A missed compare must not turn into a wait for the next 32-bit wrap.
|
||||
const REARM_ATTEMPTS: usize = 2;
|
||||
|
||||
/// A handle to the platform's single TIM2-backed microsecond epoch.
|
||||
///
|
||||
/// `now` must be called at least once during every 32-bit cycle-counter wrap
|
||||
/// interval. At the Kakute reset clock this is approximately 20 seconds; the
|
||||
/// platform periodic service calls it much more frequently.
|
||||
pub struct DwtMicrosecondClock {
|
||||
cycles_per_microsecond: u32,
|
||||
previous_cycles: u32,
|
||||
elapsed_cycles: u64,
|
||||
/// Copies share both the counter and its 64-bit extension. Every CNT read and
|
||||
/// extension runs inside the existing single-core PRIMASK critical section.
|
||||
/// Normal CC1 service observes the counter every 10 ms, even for silent readers.
|
||||
/// All observations combined must be less than 2^32 microseconds apart
|
||||
/// (approximately 71.58 minutes). NMI and fault handlers must not use this clock.
|
||||
///
|
||||
/// Ordinary WFI sleep is supported by the fixed platform clock configuration.
|
||||
/// Deep sleep, counter resets, clock changes and debugger halts spanning a full
|
||||
/// counter wrap are unsupported and require a platform reset. CNT alone cannot
|
||||
/// detect these violations. The manual's architecture chapter describes the
|
||||
/// monotonic-time and service-IRQ contract.
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct TimerClock {
|
||||
pub(super) shared: &'static SharedTimer<Tim2Counter>,
|
||||
}
|
||||
|
||||
/// Invalid fixed clock configuration for the DWT timestamp source.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, KaptanError)]
|
||||
pub enum DwtClockStartError {
|
||||
/// A zero-megahertz core clock cannot produce microsecond timestamps.
|
||||
#[kaptan_error(id = 11101)]
|
||||
ZeroFrequency,
|
||||
}
|
||||
|
||||
impl DwtMicrosecondClock {
|
||||
/// Enables the DWT cycle counter for a fixed integer-MHz core clock.
|
||||
///
|
||||
/// `MEGAHERTZ` must match the platform's core clock. This function is
|
||||
/// called once during platform initialization before interrupts are enabled.
|
||||
pub fn start<const MEGAHERTZ: u32>(
|
||||
dcb: &mut DCB,
|
||||
dwt: &mut DWT,
|
||||
) -> Result<Self, DwtClockStartError> {
|
||||
if MEGAHERTZ == 0 {
|
||||
return Err(DwtClockStartError::ZeroFrequency);
|
||||
}
|
||||
|
||||
dcb.enable_trace();
|
||||
DWT::unlock();
|
||||
dwt.set_cycle_count(0);
|
||||
dwt.enable_cycle_counter();
|
||||
|
||||
Ok(Self {
|
||||
cycles_per_microsecond: MEGAHERTZ,
|
||||
previous_cycles: 0,
|
||||
elapsed_cycles: 0,
|
||||
})
|
||||
}
|
||||
|
||||
/// Captures the current monotonic microsecond timestamp.
|
||||
impl TimerClock {
|
||||
/// Captures a timestamp in the one shared microsecond epoch.
|
||||
pub fn now(&mut self) -> Timestamp {
|
||||
self.advance_to(DWT::cycle_count())
|
||||
}
|
||||
|
||||
/// Creates another independent reader for the same already-running DWT
|
||||
/// counter. Each reader must be serviced often enough to observe wraps.
|
||||
pub const fn fork(&self) -> Self {
|
||||
Self {
|
||||
cycles_per_microsecond: self.cycles_per_microsecond,
|
||||
previous_cycles: self.previous_cycles,
|
||||
elapsed_cycles: self.elapsed_cycles,
|
||||
}
|
||||
}
|
||||
|
||||
fn advance_to(&mut self, current_cycles: u32) -> Timestamp {
|
||||
let elapsed = current_cycles.wrapping_sub(self.previous_cycles);
|
||||
self.previous_cycles = current_cycles;
|
||||
self.elapsed_cycles = self.elapsed_cycles.saturating_add(u64::from(elapsed));
|
||||
|
||||
Timestamp::from_microseconds(self.elapsed_cycles / u64::from(self.cycles_per_microsecond))
|
||||
self.shared.now()
|
||||
}
|
||||
}
|
||||
|
||||
impl MonotonicClock for DwtMicrosecondClock {
|
||||
impl MonotonicClock for TimerClock {
|
||||
fn now(&mut self) -> Timestamp {
|
||||
Self::now(self)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::DwtMicrosecondClock;
|
||||
/// The production PAC owner and injected test counter use this same mechanism.
|
||||
pub(super) trait TimerBackend {
|
||||
fn counter(&mut self) -> u32;
|
||||
fn compare_pending(&mut self) -> bool;
|
||||
fn acknowledge_compare(&mut self);
|
||||
fn set_compare(&mut self, counter: u32);
|
||||
fn enable_compare_interrupt(&mut self);
|
||||
fn pend_interrupt(&mut self);
|
||||
}
|
||||
|
||||
fn clock(previous_cycles: u32) -> DwtMicrosecondClock {
|
||||
DwtMicrosecondClock {
|
||||
cycles_per_microsecond: 16,
|
||||
previous_cycles,
|
||||
elapsed_cycles: 0,
|
||||
pub(super) struct SharedTimer<B> {
|
||||
state: Mutex<RefCell<Option<TimerState<B>>>>,
|
||||
}
|
||||
|
||||
struct TimerState<B> {
|
||||
backend: B,
|
||||
prev_cnt: u32,
|
||||
elapsed_u64: u64,
|
||||
rearm_pending: bool,
|
||||
}
|
||||
|
||||
impl<B: TimerBackend> SharedTimer<B> {
|
||||
pub(super) const fn new() -> Self {
|
||||
Self {
|
||||
state: Mutex::new(RefCell::new(None)),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn converts_cycles_to_microseconds() {
|
||||
let mut clock = clock(0);
|
||||
|
||||
assert_eq!(clock.advance_to(160).as_microseconds(), 10);
|
||||
/// Publishes the owned backend once. A second call does not touch hardware.
|
||||
/// The factory starts CNT at zero with compare interrupts still disabled.
|
||||
pub(super) fn initialize(&self, make_backend: impl FnOnce() -> B) -> bool {
|
||||
critical_section::with(|cs| {
|
||||
let mut slot = self.state.borrow(cs).borrow_mut();
|
||||
if slot.is_some() {
|
||||
return false;
|
||||
}
|
||||
*slot = Some(TimerState {
|
||||
backend: make_backend(),
|
||||
prev_cnt: 0,
|
||||
elapsed_u64: 0,
|
||||
rearm_pending: false,
|
||||
});
|
||||
true
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extends_a_single_cycle_counter_wrap() {
|
||||
let mut clock = clock(u32::MAX - 15);
|
||||
fn with<R>(&self, f: impl FnOnce(&mut TimerState<B>) -> R) -> R {
|
||||
critical_section::with(|cs| {
|
||||
f(self
|
||||
.state
|
||||
.borrow(cs)
|
||||
.borrow_mut()
|
||||
.as_mut()
|
||||
.expect("timer handles are published only after initialization"))
|
||||
})
|
||||
}
|
||||
|
||||
assert_eq!(clock.advance_to(16).as_microseconds(), 2);
|
||||
pub(super) fn now(&self) -> Timestamp {
|
||||
// Read CNT here, after exclusion. Sampling before this lock would allow
|
||||
// an IRQ to advance prev_cnt past the sample and fabricate a full wrap.
|
||||
self.with(|state| Timestamp::from_microseconds(state.sample()))
|
||||
}
|
||||
|
||||
pub(super) fn enable_interrupt(&self) {
|
||||
self.with(|state| {
|
||||
state.backend.acknowledge_compare();
|
||||
state.rearm();
|
||||
state.backend.enable_compare_interrupt();
|
||||
});
|
||||
}
|
||||
|
||||
/// Rearms before returning permission to notify service consumers.
|
||||
pub(super) fn on_interrupt(&self) -> bool {
|
||||
self.with(|state| {
|
||||
let event = state.backend.compare_pending();
|
||||
if event {
|
||||
// Acknowledge only the old CC1IF, before any CCR write. Flags
|
||||
// raised during rearming remain pending for the next IRQ.
|
||||
state.backend.acknowledge_compare();
|
||||
} else if !state.rearm_pending {
|
||||
return false;
|
||||
}
|
||||
state.rearm();
|
||||
// A software recovery pend or stale NVIC pend is not a timer event.
|
||||
event
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<B: TimerBackend> TimerState<B> {
|
||||
fn sample(&mut self) -> u64 {
|
||||
let cnt = self.backend.counter();
|
||||
self.elapsed_u64 += u64::from(cnt.wrapping_sub(self.prev_cnt));
|
||||
self.prev_cnt = cnt;
|
||||
self.elapsed_u64
|
||||
}
|
||||
|
||||
fn rearm(&mut self) {
|
||||
let mut now = self.sample();
|
||||
for _ in 0..REARM_ATTEMPTS {
|
||||
// First strictly future point on the epoch's 100 Hz grid. This
|
||||
// skips any number of missed periods in O(1), without replay.
|
||||
let period = u64::from(SERVICE_PERIOD_US);
|
||||
let next = now + (period - now % period);
|
||||
// CNT and the epoch started at zero. CCR uses the low 32 bits.
|
||||
self.backend.set_compare(next as u32);
|
||||
now = self.sample();
|
||||
if now < next {
|
||||
self.rearm_pending = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
// The target passed during both writes (for example under a debugger).
|
||||
// Retry on a software-pended IRQ even if neither compare raised CC1IF.
|
||||
// Each entry is bounded and recovery alone never notifies consumers.
|
||||
self.rearm_pending = true;
|
||||
self.backend.pend_interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../../../tests/support/timer_clock.rs"]
|
||||
mod tests;
|
||||
@@ -9,7 +9,7 @@ pub enum BaudRateError {
|
||||
/// Zero is not a valid baud rate.
|
||||
#[kaptan_error(id = 11201)]
|
||||
Zero,
|
||||
/// The requested divisor cannot fit USART BRR.
|
||||
/// The requested divisor is outside the USART BRR range 16..=65535.
|
||||
#[kaptan_error(id = 11202)]
|
||||
OutOfRange,
|
||||
}
|
||||
@@ -51,57 +51,18 @@ impl Usart2Tx {
|
||||
) -> Result<Self, BaudRateError> {
|
||||
let brr = calculate_brr(peripheral_clock_hz, baud)?;
|
||||
|
||||
// Do not inherit a bootloader's USART2 kernel-clock selection. The
|
||||
// F722 flight clock profile supplies `peripheral_clock_hz` from PCLK1,
|
||||
// so select PCLK1 before programming BRR.
|
||||
// Select the PCLK1 source used to calculate BRR.
|
||||
rcc.dckcfgr2().modify(|_, write| write.usart2sel().apb1());
|
||||
rcc.apb1rstr()
|
||||
.modify(|_, write| write.usart2rst().set_bit());
|
||||
rcc.apb1rstr()
|
||||
.modify(|_, write| write.usart2rst().clear_bit());
|
||||
rcc.apb1enr().modify(|_, write| write.usart2en().set_bit());
|
||||
peripheral.brr().write(|write| {
|
||||
// SAFETY: `brr` is validated to fit the complete BRR register.
|
||||
unsafe { write.bits(u32::from(brr)) }
|
||||
});
|
||||
peripheral.cr1().write(|write| {
|
||||
write
|
||||
.te()
|
||||
.set_bit()
|
||||
.re()
|
||||
.set_bit()
|
||||
.rxneie()
|
||||
.set_bit()
|
||||
.ue()
|
||||
.set_bit()
|
||||
});
|
||||
peripheral.cr2().write(|write| write.stop().stop1());
|
||||
let _ = rcc.apb1enr().read();
|
||||
configure_usart(&peripheral, brr);
|
||||
|
||||
Ok(Self { peripheral })
|
||||
}
|
||||
|
||||
pub(crate) fn enable_dma_transmit(&self) {
|
||||
self.peripheral.cr3().write(|write| write.dmat().set_bit());
|
||||
}
|
||||
|
||||
pub(crate) fn disable_dma_transmit_from_interrupt() {
|
||||
// SAFETY: the fixed USART2 DMA interrupt route is the sole TX-DMA
|
||||
// owner. Receive interrupts never modify CR3.
|
||||
let peripheral = unsafe { &*stm32f722::USART2::ptr() };
|
||||
peripheral.cr3().modify(|_, write| write.dmat().clear_bit());
|
||||
}
|
||||
|
||||
pub(crate) fn transmit_data_register_address(&self) -> u32 {
|
||||
self.peripheral.tdr().as_ptr() as u32
|
||||
}
|
||||
|
||||
/// Acknowledges one USART2 receive interrupt without accessing TX state.
|
||||
pub fn acknowledge_receive_interrupt() -> ReceiveInterrupt {
|
||||
// SAFETY: the platform binds USART2 exactly once and this function touches
|
||||
// only its receive/error registers, never the TX DMA configuration.
|
||||
let peripheral = unsafe { &*stm32f722::USART2::ptr() };
|
||||
acknowledge_receive_interrupt(peripheral)
|
||||
}
|
||||
}
|
||||
|
||||
impl Usart1Tx {
|
||||
@@ -114,69 +75,51 @@ impl Usart1Tx {
|
||||
) -> Result<Self, BaudRateError> {
|
||||
let brr = calculate_brr(peripheral_clock_hz, baud)?;
|
||||
|
||||
// Do not inherit a bootloader or earlier image's USART1 kernel-clock
|
||||
// selection. The F722 platform passes PCLK2 when calculating BRR, so
|
||||
// select it explicitly before programming the divisor.
|
||||
// Select the PCLK2 source used to calculate BRR.
|
||||
rcc.dckcfgr2().modify(|_, write| write.usart1sel().apb2());
|
||||
rcc.apb2rstr()
|
||||
.modify(|_, write| write.usart1rst().set_bit());
|
||||
rcc.apb2rstr()
|
||||
.modify(|_, write| write.usart1rst().clear_bit());
|
||||
rcc.apb2enr().modify(|_, write| write.usart1en().set_bit());
|
||||
peripheral.brr().write(|write| {
|
||||
// SAFETY: `brr` is validated to fit the complete BRR register.
|
||||
unsafe { write.bits(u32::from(brr)) }
|
||||
});
|
||||
peripheral.cr1().write(|write| {
|
||||
write
|
||||
.te()
|
||||
.set_bit()
|
||||
.re()
|
||||
.set_bit()
|
||||
.rxneie()
|
||||
.set_bit()
|
||||
.ue()
|
||||
.set_bit()
|
||||
});
|
||||
peripheral.cr2().write(|write| write.stop().stop1());
|
||||
let _ = rcc.apb2enr().read();
|
||||
configure_usart(&peripheral, brr);
|
||||
|
||||
Ok(Self { peripheral })
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn enable_dma_transmit(&self) {
|
||||
self.peripheral.cr3().write(|write| write.dmat().set_bit());
|
||||
}
|
||||
/// Owns configured full-duplex USART3 hardware.
|
||||
pub struct Usart3 {
|
||||
peripheral: stm32f722::USART3,
|
||||
}
|
||||
|
||||
pub(crate) fn disable_dma_transmit_from_interrupt() {
|
||||
// SAFETY: the fixed USART1 DMA interrupt route is the sole TX-DMA
|
||||
// owner. Receive interrupts never modify CR3.
|
||||
let peripheral = unsafe { &*stm32f722::USART1::ptr() };
|
||||
peripheral.cr3().modify(|_, write| write.dmat().clear_bit());
|
||||
}
|
||||
|
||||
pub(crate) fn transmit_data_register_address(&self) -> u32 {
|
||||
self.peripheral.tdr().as_ptr() as u32
|
||||
}
|
||||
|
||||
/// Acknowledges one USART1 receive interrupt without accessing TX state.
|
||||
///
|
||||
/// The platform invokes this only from the `USART1` interrupt. USART1 TX DMA
|
||||
/// owns its disjoint data register and DMA stream, so this static route may
|
||||
/// safely acknowledge RX while that transfer is active.
|
||||
pub fn acknowledge_receive_interrupt() -> ReceiveInterrupt {
|
||||
// SAFETY: the platform binds USART1 exactly once and this function touches
|
||||
// only its receive/error registers, never the TX DMA configuration.
|
||||
let peripheral = unsafe { &*stm32f722::USART1::ptr() };
|
||||
acknowledge_receive_interrupt(peripheral)
|
||||
impl Usart3 {
|
||||
/// Enables USART3 with 8N1 framing after platform pin setup.
|
||||
pub fn new(
|
||||
rcc: &stm32f722::RCC,
|
||||
peripheral: stm32f722::USART3,
|
||||
peripheral_clock_hz: u32,
|
||||
baud: u32,
|
||||
) -> Result<Self, BaudRateError> {
|
||||
let brr = calculate_brr(peripheral_clock_hz, baud)?;
|
||||
rcc.dckcfgr2().modify(|_, write| write.usart3sel().apb1());
|
||||
rcc.apb1rstr()
|
||||
.modify(|_, write| write.usart3rst().set_bit());
|
||||
rcc.apb1rstr()
|
||||
.modify(|_, write| write.usart3rst().clear_bit());
|
||||
rcc.apb1enr().modify(|_, write| write.usart3en().set_bit());
|
||||
let _ = rcc.apb1enr().read();
|
||||
configure_usart(&peripheral, brr);
|
||||
Ok(Self { peripheral })
|
||||
}
|
||||
}
|
||||
|
||||
/// Owns configured UART4 hardware.
|
||||
///
|
||||
/// This route is intended for peripherals that continuously stream data, such
|
||||
/// as the MTF-01. It does not enable DMA state.
|
||||
/// Uses interrupt-driven receive and transmit without DMA.
|
||||
pub struct Uart4 {
|
||||
_peripheral: stm32f722::UART4,
|
||||
peripheral: stm32f722::UART4,
|
||||
}
|
||||
|
||||
impl Uart4 {
|
||||
@@ -189,38 +132,103 @@ impl Uart4 {
|
||||
) -> Result<Self, BaudRateError> {
|
||||
let brr = calculate_brr(peripheral_clock_hz, baud)?;
|
||||
|
||||
rcc.dckcfgr2().modify(|_, write| write.uart4sel().apb1());
|
||||
rcc.apb1rstr().modify(|_, write| write.uart4rst().set_bit());
|
||||
rcc.apb1rstr()
|
||||
.modify(|_, write| write.uart4rst().clear_bit());
|
||||
rcc.apb1enr().modify(|_, write| write.uart4en().set_bit());
|
||||
peripheral.brr().write(|write| {
|
||||
// SAFETY: `brr` is validated to fit the complete BRR register.
|
||||
unsafe { write.bits(u32::from(brr)) }
|
||||
});
|
||||
peripheral.cr1().write(|write| {
|
||||
write
|
||||
.te()
|
||||
.set_bit()
|
||||
.re()
|
||||
.set_bit()
|
||||
.rxneie()
|
||||
.set_bit()
|
||||
.ue()
|
||||
.set_bit()
|
||||
});
|
||||
peripheral.cr2().write(|write| write.stop().stop1());
|
||||
let _ = rcc.apb1enr().read();
|
||||
configure_usart(&peripheral, brr);
|
||||
|
||||
Ok(Self {
|
||||
_peripheral: peripheral,
|
||||
})
|
||||
Ok(Self { peripheral })
|
||||
}
|
||||
}
|
||||
|
||||
/// Acknowledges one UART4 receive interrupt.
|
||||
pub fn acknowledge_receive_interrupt() -> ReceiveInterrupt {
|
||||
// SAFETY: the platform binds UART4 exactly once and this function
|
||||
// accesses only its receive and error registers.
|
||||
let peripheral = unsafe { &*stm32f722::UART4::ptr() };
|
||||
acknowledge_receive_interrupt(peripheral)
|
||||
/// Owns configured full-duplex USART6 hardware.
|
||||
pub struct Usart6 {
|
||||
peripheral: stm32f722::USART6,
|
||||
}
|
||||
|
||||
impl Usart6 {
|
||||
/// Enables USART6 with 8N1 framing after platform pin setup.
|
||||
pub fn new(
|
||||
rcc: &stm32f722::RCC,
|
||||
peripheral: stm32f722::USART6,
|
||||
peripheral_clock_hz: u32,
|
||||
baud: u32,
|
||||
) -> Result<Self, BaudRateError> {
|
||||
let brr = calculate_brr(peripheral_clock_hz, baud)?;
|
||||
rcc.dckcfgr2().modify(|_, write| write.usart6sel().apb2());
|
||||
rcc.apb2rstr()
|
||||
.modify(|_, write| write.usart6rst().set_bit());
|
||||
rcc.apb2rstr()
|
||||
.modify(|_, write| write.usart6rst().clear_bit());
|
||||
rcc.apb2enr().modify(|_, write| write.usart6en().set_bit());
|
||||
let _ = rcc.apb2enr().read();
|
||||
configure_usart(&peripheral, brr);
|
||||
Ok(Self { peripheral })
|
||||
}
|
||||
}
|
||||
|
||||
fn configure_usart(peripheral: &stm32f722::usart1::RegisterBlock, brr: u16) {
|
||||
peripheral.brr().write(|write| {
|
||||
// SAFETY: `brr` is validated to fit the complete BRR register.
|
||||
unsafe { write.bits(u32::from(brr)) }
|
||||
});
|
||||
peripheral.cr2().write(|write| write.stop().stop1());
|
||||
peripheral.cr1().write(|write| {
|
||||
write
|
||||
.te()
|
||||
.set_bit()
|
||||
.re()
|
||||
.set_bit()
|
||||
.rxneie()
|
||||
.set_bit()
|
||||
.ue()
|
||||
.set_bit()
|
||||
});
|
||||
}
|
||||
|
||||
/// Exclusive PAC owners for the configurable serial path. No DMA controller,
|
||||
/// static register alias or borrowed transmit buffer crosses this interface.
|
||||
pub(crate) enum OwnedUart {
|
||||
Uart1(Usart1Tx),
|
||||
Uart2(Usart2Tx),
|
||||
Uart3(Usart3),
|
||||
Uart4(Uart4),
|
||||
Uart6(Usart6),
|
||||
}
|
||||
impl OwnedUart {
|
||||
fn registers(&mut self) -> &stm32f722::usart1::RegisterBlock {
|
||||
match self {
|
||||
Self::Uart1(uart) => &uart.peripheral,
|
||||
Self::Uart2(uart) => &uart.peripheral,
|
||||
Self::Uart3(uart) => &uart.peripheral,
|
||||
Self::Uart4(uart) => &uart.peripheral,
|
||||
Self::Uart6(uart) => &uart.peripheral,
|
||||
}
|
||||
}
|
||||
pub(crate) fn receive(&mut self) -> ReceiveInterrupt {
|
||||
acknowledge_receive_interrupt(self.registers())
|
||||
}
|
||||
}
|
||||
impl super::super::serial::UartIo for OwnedUart {
|
||||
fn tx_ready(&mut self) -> bool {
|
||||
self.registers().isr().read().txe().bit_is_set()
|
||||
}
|
||||
fn tx_complete(&mut self) -> bool {
|
||||
self.registers().isr().read().tc().bit_is_set()
|
||||
}
|
||||
fn write_byte(&mut self, byte: u8) {
|
||||
// SAFETY: every u8 fits the configured eight-bit transmit data field.
|
||||
self.registers()
|
||||
.tdr()
|
||||
.write(|write| unsafe { write.tdr().bits(u16::from(byte)) });
|
||||
}
|
||||
fn interrupts(&mut self, ready: bool, complete: bool) {
|
||||
self.registers()
|
||||
.cr1()
|
||||
.modify(|_, write| write.txeie().bit(ready).tcie().bit(complete));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -263,16 +271,16 @@ fn clear_receive_errors(peripheral: &stm32f722::usart1::RegisterBlock) {
|
||||
});
|
||||
}
|
||||
|
||||
const fn calculate_brr(peripheral_clock_hz: u32, baud: u32) -> Result<u16, BaudRateError> {
|
||||
fn calculate_brr(peripheral_clock_hz: u32, baud: u32) -> Result<u16, BaudRateError> {
|
||||
if baud == 0 {
|
||||
return Err(BaudRateError::Zero);
|
||||
}
|
||||
|
||||
let divisor = (peripheral_clock_hz + (baud / 2)) / baud;
|
||||
if divisor == 0 || divisor > u16::MAX as u32 {
|
||||
let divisor = (u64::from(peripheral_clock_hz) + u64::from(baud) / 2) / u64::from(baud);
|
||||
if divisor < 16 {
|
||||
Err(BaudRateError::OutOfRange)
|
||||
} else {
|
||||
Ok(divisor as u16)
|
||||
u16::try_from(divisor).map_err(|_| BaudRateError::OutOfRange)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -289,4 +297,12 @@ mod tests {
|
||||
fn rejects_zero_baud() {
|
||||
assert_eq!(calculate_brr(16_000_000, 0), Err(BaudRateError::Zero));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn brr_rounding_does_not_overflow() {
|
||||
assert_eq!(calculate_brr(u32::MAX, 115_200), Ok(37_283));
|
||||
assert_eq!(calculate_brr(u32::MAX, 1), Err(BaudRateError::OutOfRange));
|
||||
assert_eq!(calculate_brr(15_000, 1_000), Err(BaudRateError::OutOfRange));
|
||||
assert_eq!(calculate_brr(16_000, 1_000), Ok(16));
|
||||
}
|
||||
}
|
||||
@@ -1,222 +0,0 @@
|
||||
//! Fixed STM32F722 DMA route for USART1 transmit.
|
||||
//!
|
||||
//! USART1 TX uses DMA2 stream 7, channel 4 on STM32F722. The route is a type,
|
||||
//! rather than platform-provided integer parameters, so invalid request mappings
|
||||
//! cannot be selected at construction.
|
||||
|
||||
use kaptan_dma::{DmaReadableBuffer, DmaTxEngine, TxCompletion};
|
||||
use kaptan_err_reportable::KaptanError;
|
||||
use stm32f7::stm32f722;
|
||||
|
||||
use super::Usart1Tx;
|
||||
|
||||
// The F7 DMA controller can expose TCIF a few bus cycles before CR.EN reads
|
||||
// low. This bounded IRQ-side settling observation never spins indefinitely;
|
||||
// a later service turn can retry a start if the stream is not yet available.
|
||||
const DISABLE_POLL_LIMIT: u8 = 32;
|
||||
|
||||
/// Owns the fixed STM32F722 USART1 TX DMA route.
|
||||
pub struct Usart1TxDma {
|
||||
usart: Usart1Tx,
|
||||
}
|
||||
|
||||
/// Failure to start one DMA frame transfer.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, KaptanError)]
|
||||
pub enum StartError {
|
||||
/// DMA cannot transfer an empty buffer or one exceeding the NDTR limit.
|
||||
#[kaptan_error(id = 11301)]
|
||||
InvalidBufferLength,
|
||||
/// DMA2 stream 7 still owns an earlier transfer.
|
||||
#[kaptan_error(id = 11302)]
|
||||
Busy,
|
||||
}
|
||||
|
||||
impl Usart1TxDma {
|
||||
/// Enables DMA2 for USART1's fixed transmit route.
|
||||
pub fn new(rcc: &stm32f722::RCC, usart: Usart1Tx) -> Self {
|
||||
rcc.ahb1enr().modify(|_, write| write.dma2en().set_bit());
|
||||
Self { usart }
|
||||
}
|
||||
|
||||
/// Starts one memory-to-USART DMA transfer.
|
||||
///
|
||||
/// # Ownership
|
||||
///
|
||||
/// The caller must retain `buffer`'s backing storage until
|
||||
/// [`Self::acknowledge_interrupt`] returns a terminal result.
|
||||
/// - The platform must bind `DMA2_STREAM7`, call `acknowledge_interrupt` from
|
||||
/// that interrupt, and defer lifecycle handling to its single owner.
|
||||
pub fn start(&mut self, buffer: DmaReadableBuffer<'_>) -> Result<(), StartError> {
|
||||
let bytes = buffer.bytes();
|
||||
if bytes.is_empty() || bytes.len() > u16::MAX as usize {
|
||||
return Err(StartError::InvalidBufferLength);
|
||||
}
|
||||
if self.is_active() {
|
||||
return Err(StartError::Busy);
|
||||
}
|
||||
|
||||
self.clear_interrupt_flags();
|
||||
self.configure_stream(bytes);
|
||||
self.stream().cr().modify(|_, write| write.en().enabled());
|
||||
// AN4031 section 4.3 requires the stream to be enabled before the
|
||||
// peripheral request. Reversing this order can raise a spurious FEIF.
|
||||
self.usart.enable_dma_transmit();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Acknowledges a DMA2 Stream7 terminal IRQ without accessing engine state.
|
||||
pub fn acknowledge_interrupt() -> TxCompletion {
|
||||
// SAFETY: this handler accesses only the fixed DMA2 Stream7 route.
|
||||
let controller = unsafe { &*stm32f722::DMA2::ptr() };
|
||||
let status = controller.hisr().read();
|
||||
let completion = DmaTerminalStatus {
|
||||
transfer_complete: status.tcif7().is_complete(),
|
||||
transfer_error: status.teif7().is_error(),
|
||||
direct_mode_error: status.dmeif7().is_error(),
|
||||
}
|
||||
.classify();
|
||||
if completion == TxCompletion::NotPending {
|
||||
return completion;
|
||||
}
|
||||
|
||||
// A normal-mode STM32 DMA stream clears EN at terminal completion.
|
||||
// Request disable and observe that transition for a small fixed bound;
|
||||
// no unbounded wait is permitted in an IRQ. If the bound expires before
|
||||
// EN becomes visible as low, a later service turn observes `Busy` and
|
||||
// retries without treating this documented hardware latency as a fault.
|
||||
controller
|
||||
.st(7)
|
||||
.cr()
|
||||
.modify(|_, write| write.en().disabled());
|
||||
for _ in 0..DISABLE_POLL_LIMIT {
|
||||
if !controller.st(7).cr().read().en().is_enabled() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Usart1Tx::disable_dma_transmit_from_interrupt();
|
||||
controller.hifcr().write(|write| {
|
||||
write
|
||||
.ctcif7()
|
||||
.clear()
|
||||
.chtif7()
|
||||
.clear()
|
||||
.cteif7()
|
||||
.clear()
|
||||
.cdmeif7()
|
||||
.clear()
|
||||
.cfeif7()
|
||||
.clear()
|
||||
});
|
||||
completion
|
||||
}
|
||||
|
||||
/// Returns whether DMA2 stream 7 owns a transfer.
|
||||
pub fn is_active(&self) -> bool {
|
||||
self.stream().cr().read().en().is_enabled()
|
||||
}
|
||||
|
||||
fn configure_stream(&mut self, buffer: &[u8]) {
|
||||
self.stream()
|
||||
.fcr()
|
||||
.write(|write| write.dmdis().enabled().feie().disabled());
|
||||
self.stream().cr().write(|write| {
|
||||
// SAFETY: Channel 4 and these transfer fields are the documented
|
||||
// USART1 TX mapping for DMA2 stream 7 on STM32F722.
|
||||
unsafe {
|
||||
write
|
||||
.chsel()
|
||||
.bits(4)
|
||||
.dir()
|
||||
.memory_to_peripheral()
|
||||
.pl()
|
||||
.very_high()
|
||||
.minc()
|
||||
.incremented()
|
||||
.pinc()
|
||||
.fixed()
|
||||
.msize()
|
||||
.bits8()
|
||||
.psize()
|
||||
.bits8()
|
||||
.circ()
|
||||
.disabled()
|
||||
.tcie()
|
||||
.enabled()
|
||||
.teie()
|
||||
.enabled()
|
||||
.dmeie()
|
||||
.enabled()
|
||||
}
|
||||
});
|
||||
self.stream().ndtr().write(|write| {
|
||||
// SAFETY: `start` checked the slice length against the NDTR limit.
|
||||
unsafe { write.bits(buffer.len() as u32) }
|
||||
});
|
||||
self.stream().par().write(|write| {
|
||||
// SAFETY: This is the memory-mapped USART1 transmit-data register.
|
||||
unsafe { write.bits(self.usart.transmit_data_register_address()) }
|
||||
});
|
||||
self.stream().m0ar().write(|write| {
|
||||
// SAFETY: The caller of `start` upholds the buffer lifetime contract.
|
||||
unsafe { write.bits(buffer.as_ptr() as u32) }
|
||||
});
|
||||
}
|
||||
|
||||
fn stream(&self) -> &stm32f722::dma2::ST {
|
||||
self.controller().st(7)
|
||||
}
|
||||
|
||||
fn clear_interrupt_flags(&self) {
|
||||
self.controller().hifcr().write(|write| {
|
||||
write
|
||||
.ctcif7()
|
||||
.clear()
|
||||
.chtif7()
|
||||
.clear()
|
||||
.cteif7()
|
||||
.clear()
|
||||
.cdmeif7()
|
||||
.clear()
|
||||
.cfeif7()
|
||||
.clear()
|
||||
});
|
||||
}
|
||||
|
||||
fn controller(&self) -> &stm32f722::dma2::RegisterBlock {
|
||||
// SAFETY: USART1 TX permanently owns this disjoint Stream7 route and
|
||||
// accesses no other DMA2 stream.
|
||||
unsafe { &*stm32f722::DMA2::ptr() }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
struct DmaTerminalStatus {
|
||||
transfer_complete: bool,
|
||||
transfer_error: bool,
|
||||
direct_mode_error: bool,
|
||||
}
|
||||
|
||||
impl DmaTerminalStatus {
|
||||
const fn classify(self) -> TxCompletion {
|
||||
// This route uses direct mode. FEIF can be raised when a peripheral
|
||||
// request arrives before DMA can provide data; it does not terminate
|
||||
// the stream and is cleared with the other status flags. TEIF and
|
||||
// DMEIF remain genuine terminal faults.
|
||||
if self.transfer_error || self.direct_mode_error {
|
||||
TxCompletion::TransferFault
|
||||
} else if self.transfer_complete {
|
||||
TxCompletion::Complete
|
||||
} else {
|
||||
TxCompletion::NotPending
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DmaTxEngine for Usart1TxDma {
|
||||
type StartError = StartError;
|
||||
|
||||
fn start(&mut self, buffer: DmaReadableBuffer<'_>) -> Result<(), Self::StartError> {
|
||||
Self::start(self, buffer)
|
||||
}
|
||||
}
|
||||
@@ -1,211 +0,0 @@
|
||||
//! Fixed STM32F722 DMA route for USART2 transmit.
|
||||
//!
|
||||
//! USART2 TX uses DMA1 Stream6, channel 4 on STM32F722. The route is a type,
|
||||
//! rather than platform-provided integer parameters, so invalid request mappings
|
||||
//! cannot be selected at construction.
|
||||
|
||||
use kaptan_dma::{DmaReadableBuffer, DmaTxEngine, TxCompletion};
|
||||
use kaptan_err_reportable::KaptanError;
|
||||
use stm32f7::stm32f722;
|
||||
|
||||
use super::Usart2Tx;
|
||||
|
||||
// Match the bounded terminal observation used by the USART1 route. A later
|
||||
// service turn handles the documented short interval before EN reads low.
|
||||
const DISABLE_POLL_LIMIT: u8 = 32;
|
||||
|
||||
/// Owns the fixed USART2 TX DMA1 Stream6 route.
|
||||
pub struct Usart2TxDma {
|
||||
usart: Usart2Tx,
|
||||
}
|
||||
|
||||
/// Failure to start one DMA frame transfer on USART2.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, KaptanError)]
|
||||
pub enum Usart2TxDmaStartError {
|
||||
/// DMA cannot transfer an empty buffer or one exceeding the NDTR limit.
|
||||
#[kaptan_error(id = 11401)]
|
||||
InvalidBufferLength,
|
||||
/// DMA1 stream 6 still owns an earlier transfer.
|
||||
#[kaptan_error(id = 11402)]
|
||||
Busy,
|
||||
}
|
||||
|
||||
impl Usart2TxDma {
|
||||
/// Enables DMA1 for USART2's fixed transmit route.
|
||||
pub fn new(rcc: &stm32f722::RCC, usart: Usart2Tx) -> Self {
|
||||
rcc.ahb1enr().modify(|_, write| write.dma1en().set_bit());
|
||||
Self { usart }
|
||||
}
|
||||
|
||||
/// Starts one memory-to-USART DMA transfer.
|
||||
///
|
||||
/// The caller retains the backing storage until
|
||||
/// [`Self::acknowledge_interrupt`] reports a terminal result.
|
||||
pub fn start(&mut self, buffer: DmaReadableBuffer<'_>) -> Result<(), Usart2TxDmaStartError> {
|
||||
let bytes = buffer.bytes();
|
||||
if bytes.is_empty() || bytes.len() > u16::MAX as usize {
|
||||
return Err(Usart2TxDmaStartError::InvalidBufferLength);
|
||||
}
|
||||
if self.is_active() {
|
||||
return Err(Usart2TxDmaStartError::Busy);
|
||||
}
|
||||
|
||||
self.clear_interrupt_flags();
|
||||
self.configure_stream(bytes);
|
||||
self.stream().cr().modify(|_, write| write.en().enabled());
|
||||
// AN4031 section 4.3 requires the stream to be enabled before the
|
||||
// peripheral request. Reversing this order can raise a spurious FEIF.
|
||||
self.usart.enable_dma_transmit();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Acknowledges a DMA1 Stream6 terminal IRQ without accessing engine state.
|
||||
pub fn acknowledge_interrupt() -> TxCompletion {
|
||||
// SAFETY: this handler accesses only the fixed DMA1 Stream6 route.
|
||||
let controller = unsafe { &*stm32f722::DMA1::ptr() };
|
||||
let status = controller.hisr().read();
|
||||
let completion = DmaTerminalStatus {
|
||||
transfer_complete: status.tcif6().is_complete(),
|
||||
transfer_error: status.teif6().is_error(),
|
||||
direct_mode_error: status.dmeif6().is_error(),
|
||||
}
|
||||
.classify();
|
||||
if completion == TxCompletion::NotPending {
|
||||
return completion;
|
||||
}
|
||||
|
||||
controller
|
||||
.st(6)
|
||||
.cr()
|
||||
.modify(|_, write| write.en().disabled());
|
||||
for _ in 0..DISABLE_POLL_LIMIT {
|
||||
if !controller.st(6).cr().read().en().is_enabled() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Usart2Tx::disable_dma_transmit_from_interrupt();
|
||||
controller.hifcr().write(|write| {
|
||||
write
|
||||
.ctcif6()
|
||||
.clear()
|
||||
.chtif6()
|
||||
.clear()
|
||||
.cteif6()
|
||||
.clear()
|
||||
.cdmeif6()
|
||||
.clear()
|
||||
.cfeif6()
|
||||
.clear()
|
||||
});
|
||||
completion
|
||||
}
|
||||
|
||||
/// Returns whether DMA1 Stream6 owns a transfer.
|
||||
pub fn is_active(&self) -> bool {
|
||||
self.stream().cr().read().en().is_enabled()
|
||||
}
|
||||
|
||||
fn configure_stream(&mut self, buffer: &[u8]) {
|
||||
self.stream()
|
||||
.fcr()
|
||||
.write(|write| write.dmdis().enabled().feie().disabled());
|
||||
self.stream().cr().write(|write| {
|
||||
// SAFETY: Channel 4 and these transfer fields are the documented
|
||||
// USART2 TX mapping for DMA1 Stream6 on STM32F722.
|
||||
unsafe {
|
||||
write
|
||||
.chsel()
|
||||
.bits(4)
|
||||
.dir()
|
||||
.memory_to_peripheral()
|
||||
.pl()
|
||||
.very_high()
|
||||
.minc()
|
||||
.incremented()
|
||||
.pinc()
|
||||
.fixed()
|
||||
.msize()
|
||||
.bits8()
|
||||
.psize()
|
||||
.bits8()
|
||||
.circ()
|
||||
.disabled()
|
||||
.tcie()
|
||||
.enabled()
|
||||
.teie()
|
||||
.enabled()
|
||||
.dmeie()
|
||||
.enabled()
|
||||
}
|
||||
});
|
||||
self.stream().ndtr().write(|write| {
|
||||
// SAFETY: `start` checked the slice length against the NDTR limit.
|
||||
unsafe { write.bits(buffer.len() as u32) }
|
||||
});
|
||||
self.stream().par().write(|write| {
|
||||
// SAFETY: This is the memory-mapped USART2 transmit-data register.
|
||||
unsafe { write.bits(self.usart.transmit_data_register_address()) }
|
||||
});
|
||||
self.stream().m0ar().write(|write| {
|
||||
// SAFETY: The caller of `start` upholds the buffer lifetime contract.
|
||||
unsafe { write.bits(buffer.as_ptr() as u32) }
|
||||
});
|
||||
}
|
||||
|
||||
fn stream(&self) -> &stm32f722::dma2::ST {
|
||||
self.controller().st(6)
|
||||
}
|
||||
|
||||
fn clear_interrupt_flags(&self) {
|
||||
self.controller().hifcr().write(|write| {
|
||||
write
|
||||
.ctcif6()
|
||||
.clear()
|
||||
.chtif6()
|
||||
.clear()
|
||||
.cteif6()
|
||||
.clear()
|
||||
.cdmeif6()
|
||||
.clear()
|
||||
.cfeif6()
|
||||
.clear()
|
||||
});
|
||||
}
|
||||
|
||||
fn controller(&self) -> &stm32f722::dma2::RegisterBlock {
|
||||
// SAFETY: SPI3 owns DMA1 initialization; USART2 TX is permanently
|
||||
// assigned to disjoint Stream6 and accesses no other stream.
|
||||
unsafe { &*stm32f722::DMA1::ptr() }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
struct DmaTerminalStatus {
|
||||
transfer_complete: bool,
|
||||
transfer_error: bool,
|
||||
direct_mode_error: bool,
|
||||
}
|
||||
|
||||
impl DmaTerminalStatus {
|
||||
const fn classify(self) -> TxCompletion {
|
||||
// This route uses direct mode. FEIF can be raised when a peripheral
|
||||
// request arrives before DMA can provide data; it does not terminate
|
||||
// the stream and is cleared with the other status flags. TEIF and
|
||||
// DMEIF remain genuine terminal faults.
|
||||
if self.transfer_error || self.direct_mode_error {
|
||||
TxCompletion::TransferFault
|
||||
} else if self.transfer_complete {
|
||||
TxCompletion::Complete
|
||||
} else {
|
||||
TxCompletion::NotPending
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DmaTxEngine for Usart2TxDma {
|
||||
type StartError = Usart2TxDmaStartError;
|
||||
|
||||
fn start(&mut self, buffer: DmaReadableBuffer<'_>) -> Result<(), Self::StartError> {
|
||||
Self::start(self, buffer)
|
||||
}
|
||||
}
|
||||
@@ -8,8 +8,11 @@ use usb_device::{
|
||||
};
|
||||
use usbd_serial::CdcAcmClass;
|
||||
|
||||
use super::time::TimerClock;
|
||||
|
||||
use super::super::{
|
||||
clock::CORE_FREQUENCY_HZ,
|
||||
data_channel::RxFifoInterruptGate,
|
||||
usb::{USB_PACKET_BYTES, UsbMavlink},
|
||||
};
|
||||
|
||||
@@ -29,10 +32,12 @@ pub(crate) struct F722UsbPeripheral {
|
||||
_global: stm32f722::OTG_FS_GLOBAL,
|
||||
_device: stm32f722::OTG_FS_DEVICE,
|
||||
_power_clock: stm32f722::OTG_FS_PWRCLK,
|
||||
clock: TimerClock,
|
||||
}
|
||||
|
||||
// SAFETY: the token is constructed once from PAC-owned peripherals and moved
|
||||
// into one UsbBus. Runtime register access is synchronized by that bus.
|
||||
// into one UsbBus. Its register access, the delegated RXFLVLM gate and the
|
||||
// read-only reset witnesses share the USB I/O owner's RTIC resource lock.
|
||||
unsafe impl Sync for F722UsbPeripheral {}
|
||||
|
||||
// SAFETY: the F722 OTG FS register layout, FIFO depth, and endpoint count are
|
||||
@@ -42,6 +47,9 @@ unsafe impl UsbPeripheral for F722UsbPeripheral {
|
||||
const HIGH_SPEED: bool = false;
|
||||
const FIFO_DEPTH_WORDS: usize = 320;
|
||||
const ENDPOINT_COUNT: usize = 6;
|
||||
// Per-wait fault containment policy, not a qualified hardware latency.
|
||||
const WAIT_TIMEOUT_US: u32 = 5_000;
|
||||
const WAIT_POLL_LIMIT: u32 = CORE_FREQUENCY_HZ / 200;
|
||||
|
||||
fn enable() {
|
||||
cortex_m::interrupt::free(|_| {
|
||||
@@ -59,6 +67,24 @@ unsafe impl UsbPeripheral for F722UsbPeripheral {
|
||||
fn ahb_frequency_hz(&self) -> u32 {
|
||||
CORE_FREQUENCY_HZ
|
||||
}
|
||||
|
||||
fn now_us(&self) -> u64 {
|
||||
let mut clock = self.clock;
|
||||
clock.now().as_microseconds()
|
||||
}
|
||||
|
||||
fn abort(&self) {
|
||||
cortex_m::peripheral::NVIC::mask(stm32f722::Interrupt::OTG_FS);
|
||||
cortex_m::interrupt::free(|_| {
|
||||
// SAFETY: this capability owns OTG FS. Exclusion serializes the RCC
|
||||
// RMW with other peripheral owners. Reset remains asserted, with
|
||||
// the clock on, until the next whole-controller restart.
|
||||
let rcc = unsafe { &*stm32f722::RCC::ptr() };
|
||||
rcc.ahb2rstr().modify(|_, write| write.otgfsrst().set_bit());
|
||||
});
|
||||
cortex_m::asm::dsb();
|
||||
cortex_m::peripheral::NVIC::unpend(stm32f722::Interrupt::OTG_FS);
|
||||
}
|
||||
}
|
||||
|
||||
type F722UsbBus = UsbBus<F722UsbPeripheral>;
|
||||
@@ -87,12 +113,16 @@ pub(crate) fn usb_mavlink(
|
||||
global: stm32f722::OTG_FS_GLOBAL,
|
||||
device: stm32f722::OTG_FS_DEVICE,
|
||||
power_clock: stm32f722::OTG_FS_PWRCLK,
|
||||
clock: TimerClock,
|
||||
product: &'static str,
|
||||
) -> Result<F722UsbMavlink, ()> {
|
||||
) -> Result<(F722UsbMavlink, RxFifoInterruptGate), ()> {
|
||||
// Delegate one narrow capability while consuming the controller's PAC owners.
|
||||
let mut rx_interrupt = RxFifoInterruptGate::new(&global);
|
||||
let peripheral = F722UsbPeripheral {
|
||||
_global: global,
|
||||
_device: device,
|
||||
_power_clock: power_clock,
|
||||
clock,
|
||||
};
|
||||
let endpoint_memory = cortex_m::singleton!(
|
||||
USB_ENDPOINT_MEMORY: [u32; ENDPOINT_MEMORY_WORDS] = [0; ENDPOINT_MEMORY_WORDS]
|
||||
@@ -117,5 +147,10 @@ pub(crate) fn usb_mavlink(
|
||||
.max_packet_size_0(USB_PACKET_BYTES as u8)
|
||||
.map_err(|_| ())?
|
||||
.build();
|
||||
Ok(UsbMavlink::new(device, cdc))
|
||||
// build() calls UsbDevice::new -> allocator.freeze -> UsbBus::enable, which
|
||||
// initializes GINTMSK. Apply the empty application-RX state only afterwards.
|
||||
if !device.bus().is_offline() {
|
||||
rx_interrupt.set_enabled(true);
|
||||
}
|
||||
Ok((UsbMavlink::new(device, cdc), rx_interrupt))
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
//! UART pin routes and configuration validation.
|
||||
use crate::SerialInventory;
|
||||
use kaptan_config::{Config, ConfigValue, ConfigViolation, FieldId, ValidationRule};
|
||||
use kaptan_ports::{PortDescriptor, PortId, PortPlan, PortSettings, Role};
|
||||
|
||||
// Resource IDs: PAx = x, PBx = 16+x, PCx = 32+x; peripheral IDs >= 100.
|
||||
// USB, both supported IMUs and inhibited outputs are disjoint from these routes.
|
||||
pub const UART1: PortDescriptor = PortDescriptor {
|
||||
id: PortId::new(1),
|
||||
role_field: FieldId::new(201),
|
||||
baud_field: FieldId::new(202),
|
||||
role_key: "ports.uart1.role",
|
||||
baud_key: "ports.uart1.baud",
|
||||
supported_roles: Role::CONFIG_ENUM_OPTIONS,
|
||||
allowed_bauds: &[115_200, 230_400, 460_800],
|
||||
resources: &[9, 10, 101],
|
||||
};
|
||||
pub const UART2: PortDescriptor = PortDescriptor {
|
||||
id: PortId::new(2),
|
||||
role_field: FieldId::new(203),
|
||||
baud_field: FieldId::new(204),
|
||||
role_key: "ports.uart2.role",
|
||||
baud_key: "ports.uart2.baud",
|
||||
supported_roles: Role::CONFIG_ENUM_OPTIONS,
|
||||
allowed_bauds: &[115_200, 230_400, 460_800],
|
||||
resources: &[2, 3, 102],
|
||||
};
|
||||
pub const UART3: PortDescriptor = PortDescriptor {
|
||||
id: PortId::new(3),
|
||||
role_field: FieldId::new(205),
|
||||
baud_field: FieldId::new(206),
|
||||
role_key: "ports.uart3.role",
|
||||
baud_key: "ports.uart3.baud",
|
||||
supported_roles: Role::CONFIG_ENUM_OPTIONS,
|
||||
allowed_bauds: &[115_200, 230_400, 460_800],
|
||||
resources: &[26, 27, 103],
|
||||
};
|
||||
pub const UART4: PortDescriptor = PortDescriptor {
|
||||
id: PortId::new(4),
|
||||
role_field: FieldId::new(207),
|
||||
baud_field: FieldId::new(208),
|
||||
role_key: "ports.uart4.role",
|
||||
baud_key: "ports.uart4.baud",
|
||||
supported_roles: Role::CONFIG_ENUM_OPTIONS,
|
||||
allowed_bauds: &[115_200, 230_400, 460_800],
|
||||
resources: &[0, 1, 104],
|
||||
};
|
||||
pub const UART6: PortDescriptor = PortDescriptor {
|
||||
id: PortId::new(6),
|
||||
role_field: FieldId::new(211),
|
||||
baud_field: FieldId::new(212),
|
||||
role_key: "ports.uart6.role",
|
||||
baud_key: "ports.uart6.baud",
|
||||
supported_roles: Role::CONFIG_ENUM_OPTIONS,
|
||||
allowed_bauds: &[115_200, 230_400, 460_800],
|
||||
resources: &[38, 39, 106],
|
||||
};
|
||||
|
||||
const MCU_ROUTES: [PortDescriptor; 5] = [UART1, UART2, UART3, UART4, UART6];
|
||||
const RESERVED_RESOURCES: &[u16] = &[
|
||||
4, 5, 6, 7, 18, 15, 35, 42, 43, 44, 16, 17, 20, 21, 13, 50, 24, 25,
|
||||
];
|
||||
|
||||
pub(crate) fn project_ports<C: Config, const N: usize>(
|
||||
candidate: &C,
|
||||
inventory: SerialInventory,
|
||||
wire_bytes: u32,
|
||||
) -> Result<PortPlan<N>, ConfigViolation> {
|
||||
let descriptors: &[PortDescriptor; N] = inventory
|
||||
.descriptors()
|
||||
.try_into()
|
||||
.map_err(|_| ConfigViolation::global(ValidationRule::Inconsistent))?;
|
||||
// A board selects canonical routes, not replacement pin or capability data.
|
||||
for descriptor in descriptors {
|
||||
if !MCU_ROUTES.contains(descriptor) {
|
||||
return Err(ConfigViolation::field(
|
||||
descriptor.role_field,
|
||||
ValidationRule::UnsupportedCapability,
|
||||
));
|
||||
}
|
||||
for expected in descriptor.fields() {
|
||||
let mut fields = (0..C::FIELD_COUNT)
|
||||
.filter_map(C::descriptor)
|
||||
.filter(|field| field.id == expected.id);
|
||||
if fields.next() != Some(&expected) || fields.next().is_some() {
|
||||
return Err(ConfigViolation::field(
|
||||
expected.id,
|
||||
ValidationRule::Inconsistent,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
// Reject even disabled extra fields. Otherwise the root would advertise
|
||||
// settings that boot never consumes. Other schema children remain independent.
|
||||
for field in (0..C::FIELD_COUNT).filter_map(C::descriptor) {
|
||||
if (field.display_key.starts_with("ports.")
|
||||
|| MCU_ROUTES
|
||||
.iter()
|
||||
.any(|port| field.id == port.role_field || field.id == port.baud_field))
|
||||
&& !descriptors
|
||||
.iter()
|
||||
.any(|port| field.id == port.role_field || field.id == port.baud_field)
|
||||
{
|
||||
return Err(ConfigViolation::field(
|
||||
field.id,
|
||||
ValidationRule::Inconsistent,
|
||||
));
|
||||
}
|
||||
}
|
||||
let mut settings = [PortSettings {
|
||||
role: Role::Disabled,
|
||||
baud: 115_200,
|
||||
}; N];
|
||||
let mut mtf01 = None;
|
||||
for (slot, descriptor) in settings.iter_mut().zip(descriptors) {
|
||||
let Some(ConfigValue::U8(role)) = candidate.field_value(descriptor.role_field) else {
|
||||
return Err(ConfigViolation::field(
|
||||
descriptor.role_field,
|
||||
ValidationRule::Inconsistent,
|
||||
));
|
||||
};
|
||||
slot.role = Role::try_from_wire(role).ok_or(ConfigViolation::field(
|
||||
descriptor.role_field,
|
||||
ValidationRule::InvalidEnumValue,
|
||||
))?;
|
||||
let Some(ConfigValue::U32(baud)) = candidate.field_value(descriptor.baud_field) else {
|
||||
return Err(ConfigViolation::field(
|
||||
descriptor.baud_field,
|
||||
ValidationRule::Inconsistent,
|
||||
));
|
||||
};
|
||||
slot.baud = baud;
|
||||
if slot.role == Role::Mtf01 {
|
||||
// F722 has one range/flow mailbox pair and uses the factory baud.
|
||||
if let Some(previous) = mtf01 {
|
||||
return Err(ConfigViolation::related(
|
||||
descriptor.role_field,
|
||||
previous,
|
||||
ValidationRule::Inconsistent,
|
||||
));
|
||||
}
|
||||
mtf01 = Some(descriptor.role_field);
|
||||
if baud != 115_200 {
|
||||
return Err(ConfigViolation::field(
|
||||
descriptor.baud_field,
|
||||
ValidationRule::UnsupportedCapability,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
PortPlan::validate(descriptors, settings, RESERVED_RESOURCES, wire_bytes, 0)
|
||||
}
|
||||
|
||||
pub(crate) fn disable_ports<C: Config>(candidate: &mut C, inventory: SerialInventory) {
|
||||
for descriptor in inventory.descriptors() {
|
||||
candidate
|
||||
.set_field(descriptor.role_field, ConfigValue::U8(0))
|
||||
.expect("factory port role accepts Disabled");
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,63 @@
|
||||
//! Platform-owned STM32F722 electrical route implementations.
|
||||
|
||||
use super::peripherals::{Spi1, Spi1Error, Spi3, Spi3Error};
|
||||
use kaptan_spi::{SpiCompletion, SpiDevice};
|
||||
use super::{
|
||||
interrupts::F722Interrupts,
|
||||
peripherals::{Spi1, Spi1Error, Spi3, Spi3Error, TimerClock},
|
||||
sensors::ImuService,
|
||||
};
|
||||
use crate::{Icm42688Spi3, Mpu6000Spi1, sealed};
|
||||
use kaptan_spi::{CompletionError, SpiCompletion, SpiDevice};
|
||||
use stm32f7::stm32f722;
|
||||
|
||||
/// Selects matching IMU initialization and interrupt handlers.
|
||||
pub trait F722Route: Copy + sealed::Route + F722Interrupts {
|
||||
type Imu: ImuService;
|
||||
|
||||
fn initialize(self, resources: ImuResources<'_>, clock: TimerClock) -> Self::Imu;
|
||||
}
|
||||
|
||||
/// Peripheral tokens consumed by the selected IMU route.
|
||||
#[doc(hidden)]
|
||||
pub struct ImuResources<'a> {
|
||||
pub(crate) rcc: &'a stm32f722::RCC,
|
||||
pub(crate) gpioa: &'a stm32f722::GPIOA,
|
||||
pub(crate) gpioc: &'a stm32f722::GPIOC,
|
||||
pub(crate) gpiob: stm32f722::GPIOB,
|
||||
pub(crate) syscfg: stm32f722::SYSCFG,
|
||||
pub(crate) exti: stm32f722::EXTI,
|
||||
pub(crate) spi1: stm32f722::SPI1,
|
||||
pub(crate) spi3: stm32f722::SPI3,
|
||||
pub(crate) dma1: stm32f722::DMA1,
|
||||
pub(crate) dma2: stm32f722::DMA2,
|
||||
}
|
||||
|
||||
impl F722Route for Mpu6000Spi1 {
|
||||
type Imu = super::sensors::Mpu6000Imu;
|
||||
fn initialize(self, r: ImuResources<'_>, clock: TimerClock) -> Self::Imu {
|
||||
super::peripherals::configure_spi1_pins_pa5_pa6_pa7(r.rcc, r.gpioa);
|
||||
super::peripherals::configure_exti4_pin_pa4(r.gpioa);
|
||||
super::board::configure_exti4(r.rcc, r.syscfg, r.exti);
|
||||
Self::Imu::new(
|
||||
r.rcc,
|
||||
r.spi1,
|
||||
r.dma2,
|
||||
r.gpiob,
|
||||
clock,
|
||||
self.mount,
|
||||
self.config,
|
||||
)
|
||||
}
|
||||
}
|
||||
impl F722Route for Icm42688Spi3 {
|
||||
type Imu = super::sensors::Icm42688Imu;
|
||||
fn initialize(self, r: ImuResources<'_>, clock: TimerClock) -> Self::Imu {
|
||||
super::peripherals::configure_spi3_pins_pc10_pc11_pc12(r.rcc, r.gpioc);
|
||||
super::peripherals::configure_exti3_pin_pc3(r.gpioc);
|
||||
super::board::configure_exti3(r.rcc, r.syscfg, r.exti);
|
||||
Self::Imu::new(r.rcc, r.spi3, r.dma1, r.gpioa, clock, self.mount)
|
||||
}
|
||||
}
|
||||
|
||||
/// Owns the MPU6000 SPI1 transaction path and its PB2 chip select.
|
||||
pub(crate) struct Mpu6000Spi1Route {
|
||||
spi: Spi1,
|
||||
@@ -25,9 +79,8 @@ impl Mpu6000Spi1Route {
|
||||
.pupdr()
|
||||
.modify(|_, write| write.pupdr2().floating());
|
||||
chip_select.bsrr().write(|write| write.bs2().set_bit());
|
||||
// Drop the full GPIOB owner after initialization. Runtime access is
|
||||
// deliberately limited to PB2's atomic BSRR bits below, leaving no
|
||||
// route-owned PAC token that could reconfigure the former motor pins.
|
||||
// Release GPIOB after setup. Runtime writes only PB2's atomic BSRR
|
||||
// bits and cannot reconfigure motor pins.
|
||||
Self { spi }
|
||||
}
|
||||
|
||||
@@ -54,27 +107,33 @@ impl Mpu6000Spi1Route {
|
||||
impl SpiDevice for Mpu6000Spi1Route {
|
||||
type StartError = Spi1Error;
|
||||
|
||||
fn start_transfer(
|
||||
&mut self,
|
||||
transmit: &[u8],
|
||||
receive: &mut [u8],
|
||||
) -> Result<(), Self::StartError> {
|
||||
fn is_busy(error: &Self::StartError) -> bool {
|
||||
*error == Spi1Error::Busy
|
||||
}
|
||||
|
||||
fn start_transfer(&mut self, transmit: &[u8]) -> Result<(), Self::StartError> {
|
||||
self.spi.check_start(transmit.len())?;
|
||||
self.select();
|
||||
if let Err(error) = self.spi.start_transfer(transmit, receive) {
|
||||
if let Err(error) = self.spi.start_transfer(transmit) {
|
||||
self.deselect();
|
||||
return Err(error);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn complete_transfer(&mut self, completion: SpiCompletion) {
|
||||
self.spi.complete_transfer(completion);
|
||||
fn complete_transfer(
|
||||
&mut self,
|
||||
completion: SpiCompletion,
|
||||
receive: &mut [u8],
|
||||
) -> Result<(), CompletionError> {
|
||||
let result = self.spi.complete_transfer(completion, receive);
|
||||
if matches!(
|
||||
completion,
|
||||
SpiCompletion::Complete | SpiCompletion::TransferFault
|
||||
) {
|
||||
self.deselect();
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,26 +189,32 @@ impl Icm42688Spi3Route {
|
||||
impl SpiDevice for Icm42688Spi3Route {
|
||||
type StartError = Spi3Error;
|
||||
|
||||
fn start_transfer(
|
||||
&mut self,
|
||||
transmit: &[u8],
|
||||
receive: &mut [u8],
|
||||
) -> Result<(), Self::StartError> {
|
||||
fn is_busy(error: &Self::StartError) -> bool {
|
||||
*error == Spi3Error::Busy
|
||||
}
|
||||
|
||||
fn start_transfer(&mut self, transmit: &[u8]) -> Result<(), Self::StartError> {
|
||||
self.spi.check_start(transmit.len())?;
|
||||
self.select();
|
||||
if let Err(error) = self.spi.start_transfer(transmit, receive) {
|
||||
if let Err(error) = self.spi.start_transfer(transmit) {
|
||||
self.deselect();
|
||||
return Err(error);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn complete_transfer(&mut self, completion: SpiCompletion) {
|
||||
self.spi.complete_transfer(completion);
|
||||
fn complete_transfer(
|
||||
&mut self,
|
||||
completion: SpiCompletion,
|
||||
receive: &mut [u8],
|
||||
) -> Result<(), CompletionError> {
|
||||
let result = self.spi.complete_transfer(completion, receive);
|
||||
if matches!(
|
||||
completion,
|
||||
SpiCompletion::Complete | SpiCompletion::TransferFault
|
||||
) {
|
||||
self.deselect();
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
//! Platform-owned F722 sensor-service assembly.
|
||||
|
||||
use super::peripherals::{DwtMicrosecondClock, Spi1, Spi1Error, Spi3, Spi3Error};
|
||||
use super::peripherals::{Spi1, Spi1Error, Spi3, Spi3Error, TimerClock};
|
||||
use kaptan_drivers::{
|
||||
icm42688::{Error as Icm42688Error, Icm42688, Icm42688Service, Icm42688ServiceEvent},
|
||||
mpu6000::{Error as Mpu6000Error, Mpu6000, Mpu6000Config, Mpu6000Service, Mpu6000ServiceEvent},
|
||||
icm42688::{Icm42688, Icm42688Service, Icm42688ServiceEvent},
|
||||
mpu6000::{Mpu6000, Mpu6000Config, Mpu6000Service, Mpu6000ServiceEvent},
|
||||
};
|
||||
use kaptan_err_reportable::{ErrorId, ReportableError};
|
||||
use kaptan_sensor::imu::ImuSnapshot;
|
||||
@@ -16,115 +16,127 @@ use super::routes::{Icm42688Spi3Route, Mpu6000Spi1Route};
|
||||
|
||||
/// A profile-neutral IMU worker result. Device protocol errors are consumed
|
||||
/// here because flight policy only needs the terminal healthy/fault outcome.
|
||||
pub(crate) enum ImuEvent {
|
||||
pub enum ImuEvent {
|
||||
Idle,
|
||||
Initialized,
|
||||
Sample(ImuSnapshot),
|
||||
Fault(ErrorId),
|
||||
}
|
||||
|
||||
/// Opaque owner of the selected F722 IMU service.
|
||||
///
|
||||
/// RTIC places local resource types in generated public metadata, so this
|
||||
/// wrapper is public. The device services and routes remain private to the
|
||||
/// platform, preserving the board-facing abstraction boundary.
|
||||
pub struct F722Imu {
|
||||
inner: F722ImuKind,
|
||||
/// Operations used by the deferred IMU worker, statically dispatched to the
|
||||
/// route's concrete service. No peripheral ownership crosses this interface.
|
||||
pub trait ImuService: Send + 'static {
|
||||
fn begin(&mut self) -> ImuEvent;
|
||||
fn on_periodic_tick(&mut self) -> ImuEvent;
|
||||
fn on_data_ready(&mut self) -> ImuEvent;
|
||||
fn on_transfer_complete(&mut self, completion: SpiCompletion) -> ImuEvent;
|
||||
}
|
||||
|
||||
/// RTIC metadata needs a public owner; its concrete driver and pins stay private.
|
||||
pub struct Mpu6000Imu {
|
||||
inner: Mpu6000Service<TimerClock, Mpu6000Spi1Route>,
|
||||
mount: ImuMount,
|
||||
}
|
||||
|
||||
/// The two concrete IMU services supported by the F722 platform.
|
||||
enum F722ImuKind {
|
||||
Mpu6000(Mpu6000Service<DwtMicrosecondClock, Mpu6000Spi1Route>),
|
||||
Icm42688(Icm42688Service<DwtMicrosecondClock, Icm42688Spi3Route>),
|
||||
}
|
||||
|
||||
impl F722Imu {
|
||||
pub(crate) fn mpu6000(
|
||||
impl Mpu6000Imu {
|
||||
pub(crate) fn new(
|
||||
rcc: &stm32f722::RCC,
|
||||
spi: stm32f722::SPI1,
|
||||
dma: stm32f722::DMA2,
|
||||
chip_select: stm32f722::GPIOB,
|
||||
clock: DwtMicrosecondClock,
|
||||
clock: TimerClock,
|
||||
mount: ImuMount,
|
||||
config: Mpu6000Config,
|
||||
) -> Self {
|
||||
Self {
|
||||
inner: F722ImuKind::Mpu6000(Mpu6000Service::new(
|
||||
inner: Mpu6000Service::new(
|
||||
clock,
|
||||
Mpu6000::new(
|
||||
Mpu6000Spi1Route::new(rcc, Spi1::new(rcc, spi, dma), chip_select),
|
||||
config,
|
||||
),
|
||||
)),
|
||||
),
|
||||
mount,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn icm42688(
|
||||
impl ImuService for Mpu6000Imu {
|
||||
fn begin(&mut self) -> ImuEvent {
|
||||
match self.inner.begin() {
|
||||
Ok(()) => ImuEvent::Idle,
|
||||
Err(error) => ImuEvent::Fault(error.error_id()),
|
||||
}
|
||||
}
|
||||
|
||||
fn on_periodic_tick(&mut self) -> ImuEvent {
|
||||
mpu_event(self.inner.on_periodic_tick(), self.mount)
|
||||
}
|
||||
|
||||
fn on_data_ready(&mut self) -> ImuEvent {
|
||||
mpu_event(self.inner.on_data_ready(), self.mount)
|
||||
}
|
||||
|
||||
fn on_transfer_complete(&mut self, completion: SpiCompletion) -> ImuEvent {
|
||||
mpu_event(self.inner.on_transfer_complete(completion), self.mount)
|
||||
}
|
||||
}
|
||||
|
||||
/// Concrete ICM owner, including its board-declared mounting transform.
|
||||
pub struct Icm42688Imu {
|
||||
inner: Icm42688Service<TimerClock, Icm42688Spi3Route>,
|
||||
mount: ImuMount,
|
||||
}
|
||||
|
||||
impl Icm42688Imu {
|
||||
pub(crate) fn new(
|
||||
rcc: &stm32f722::RCC,
|
||||
spi: stm32f722::SPI3,
|
||||
dma: stm32f722::DMA1,
|
||||
chip_select: &stm32f722::GPIOA,
|
||||
clock: DwtMicrosecondClock,
|
||||
clock: TimerClock,
|
||||
mount: ImuMount,
|
||||
) -> Self {
|
||||
Self {
|
||||
inner: F722ImuKind::Icm42688(Icm42688Service::new(
|
||||
inner: Icm42688Service::new(
|
||||
clock,
|
||||
Icm42688::new(Icm42688Spi3Route::new(
|
||||
rcc,
|
||||
Spi3::new(rcc, spi, dma),
|
||||
chip_select,
|
||||
)),
|
||||
)),
|
||||
),
|
||||
mount,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn begin(&mut self) -> ImuEvent {
|
||||
match &mut self.inner {
|
||||
F722ImuKind::Mpu6000(imu) => match imu.begin() {
|
||||
Ok(()) => ImuEvent::Idle,
|
||||
Err(error) => ImuEvent::Fault(error.error_id()),
|
||||
},
|
||||
F722ImuKind::Icm42688(imu) => match imu.begin() {
|
||||
Ok(()) => ImuEvent::Idle,
|
||||
Err(error) => ImuEvent::Fault(error.error_id()),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn on_periodic_tick(&mut self) -> ImuEvent {
|
||||
match &mut self.inner {
|
||||
F722ImuKind::Mpu6000(imu) => mpu_event(imu.on_periodic_tick()),
|
||||
F722ImuKind::Icm42688(imu) => icm_event(imu.on_periodic_tick(), self.mount),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn on_data_ready(&mut self) -> ImuEvent {
|
||||
match &mut self.inner {
|
||||
F722ImuKind::Mpu6000(imu) => mpu_event(imu.on_data_ready()),
|
||||
F722ImuKind::Icm42688(imu) => icm_event(imu.on_data_ready(), self.mount),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn on_transfer_complete(&mut self, completion: SpiCompletion) -> ImuEvent {
|
||||
match &mut self.inner {
|
||||
F722ImuKind::Mpu6000(imu) => mpu_event(imu.on_transfer_complete(completion)),
|
||||
F722ImuKind::Icm42688(imu) => {
|
||||
icm_event(imu.on_transfer_complete(completion), self.mount)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn mpu_event(event: Mpu6000ServiceEvent<Spi1Error>) -> ImuEvent {
|
||||
impl ImuService for Icm42688Imu {
|
||||
fn begin(&mut self) -> ImuEvent {
|
||||
match self.inner.begin() {
|
||||
Ok(()) => ImuEvent::Idle,
|
||||
Err(error) => ImuEvent::Fault(error.error_id()),
|
||||
}
|
||||
}
|
||||
|
||||
fn on_periodic_tick(&mut self) -> ImuEvent {
|
||||
icm_event(self.inner.on_periodic_tick(), self.mount)
|
||||
}
|
||||
|
||||
fn on_data_ready(&mut self) -> ImuEvent {
|
||||
icm_event(self.inner.on_data_ready(), self.mount)
|
||||
}
|
||||
|
||||
fn on_transfer_complete(&mut self, completion: SpiCompletion) -> ImuEvent {
|
||||
icm_event(self.inner.on_transfer_complete(completion), self.mount)
|
||||
}
|
||||
}
|
||||
|
||||
fn mpu_event(event: Mpu6000ServiceEvent<Spi1Error>, mount: ImuMount) -> ImuEvent {
|
||||
match event {
|
||||
Mpu6000ServiceEvent::Idle => ImuEvent::Idle,
|
||||
Mpu6000ServiceEvent::Initialized => ImuEvent::Initialized,
|
||||
Mpu6000ServiceEvent::Sample(sample) => ImuEvent::Sample(sample),
|
||||
Mpu6000ServiceEvent::Fault(Mpu6000Error::Start(Spi1Error::Busy)) => ImuEvent::Idle,
|
||||
Mpu6000ServiceEvent::Sample(sample) => ImuEvent::Sample(align_mount(sample, mount)),
|
||||
Mpu6000ServiceEvent::Fault(error) => ImuEvent::Fault(error.error_id()),
|
||||
}
|
||||
}
|
||||
@@ -134,15 +146,13 @@ fn icm_event(event: Icm42688ServiceEvent<Spi3Error>, mount: ImuMount) -> ImuEven
|
||||
Icm42688ServiceEvent::Idle => ImuEvent::Idle,
|
||||
Icm42688ServiceEvent::Initialized => ImuEvent::Initialized,
|
||||
Icm42688ServiceEvent::Sample(sample) => ImuEvent::Sample(align_mount(sample, mount)),
|
||||
Icm42688ServiceEvent::Fault(Icm42688Error::Start(Spi3Error::Busy)) => ImuEvent::Idle,
|
||||
Icm42688ServiceEvent::Fault(error) => ImuEvent::Fault(error.error_id()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Applies the board-declared fixed IMU-to-body transform.
|
||||
///
|
||||
/// This is immutable board metadata validated against the selected electrical
|
||||
/// route before initialization. It is not an airframe calibration setting.
|
||||
/// This is immutable board metadata, not an airframe calibration setting.
|
||||
fn align_mount(mut sample: ImuSnapshot, mount: ImuMount) -> ImuSnapshot {
|
||||
match mount {
|
||||
ImuMount::BodyAligned => sample,
|
||||
@@ -157,6 +167,13 @@ fn align_mount(mut sample: ImuSnapshot, mount: ImuMount) -> ImuSnapshot {
|
||||
sample.angular_rate_milliradians_per_second[1].saturating_neg();
|
||||
sample
|
||||
}
|
||||
ImuMount::Yaw270 => {
|
||||
let [x, y, z] = sample.acceleration_milligravity;
|
||||
sample.acceleration_milligravity = [y.saturating_neg(), x, z];
|
||||
let [x, y, z] = sample.angular_rate_milliradians_per_second;
|
||||
sample.angular_rate_milliradians_per_second = [y.saturating_neg(), x, z];
|
||||
sample
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,35 +195,69 @@ mod tests {
|
||||
ImuSnapshot::new(
|
||||
Timestamp::from_microseconds(0),
|
||||
[10, 20, 30],
|
||||
[40, 50, 60],
|
||||
[-34_871, 34_870, -34_871],
|
||||
0,
|
||||
),
|
||||
ImuMount::Yaw180,
|
||||
);
|
||||
assert_eq!(sample.acceleration_milligravity, [-10, -20, 30]);
|
||||
assert_eq!(sample.angular_rate_milliradians_per_second, [-40, -50, 60]);
|
||||
assert_eq!(
|
||||
sample.angular_rate_milliradians_per_second,
|
||||
[34_871, -34_870, -34_871]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transient_spi_busy_is_retry_not_a_typed_imu_fault() {
|
||||
fn mpu_samples_apply_declared_mount_without_changing_timestamp_or_temperature() {
|
||||
let sample = ImuSnapshot::new(
|
||||
Timestamp::from_microseconds(123),
|
||||
[10, 20, 30],
|
||||
[40, 50, 60],
|
||||
25,
|
||||
);
|
||||
for (mount, acceleration, angular_rate) in [
|
||||
(ImuMount::BodyAligned, [10, 20, 30], [40, 50, 60]),
|
||||
(ImuMount::Yaw180, [-10, -20, 30], [-40, -50, 60]),
|
||||
(ImuMount::Yaw270, [-20, 10, 30], [-50, 40, 60]),
|
||||
] {
|
||||
let ImuEvent::Sample(aligned) = mpu_event(Mpu6000ServiceEvent::Sample(sample), mount)
|
||||
else {
|
||||
panic!("sample event must stay a sample");
|
||||
};
|
||||
assert_eq!(
|
||||
aligned,
|
||||
ImuSnapshot::new(
|
||||
Timestamp::from_microseconds(123),
|
||||
acceleration,
|
||||
angular_rate,
|
||||
25,
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn declared_driver_faults_are_never_silently_discarded() {
|
||||
assert!(matches!(
|
||||
mpu_event(Mpu6000ServiceEvent::Fault(Mpu6000Error::Start(
|
||||
Spi1Error::Busy
|
||||
))),
|
||||
ImuEvent::Idle
|
||||
mpu_event(
|
||||
Mpu6000ServiceEvent::Fault(Mpu6000Error::Start(Spi1Error::Busy)),
|
||||
ImuMount::BodyAligned
|
||||
),
|
||||
ImuEvent::Fault(_)
|
||||
));
|
||||
assert!(matches!(
|
||||
icm_event(
|
||||
Icm42688ServiceEvent::Fault(Icm42688Error::Start(Spi3Error::Busy)),
|
||||
ImuMount::BodyAligned,
|
||||
),
|
||||
ImuEvent::Idle
|
||||
ImuEvent::Fault(_)
|
||||
));
|
||||
|
||||
assert!(matches!(
|
||||
mpu_event(Mpu6000ServiceEvent::Fault(Mpu6000Error::Start(
|
||||
Spi1Error::InvalidLength
|
||||
))),
|
||||
mpu_event(
|
||||
Mpu6000ServiceEvent::Fault(Mpu6000Error::Start(Spi1Error::InvalidLength)),
|
||||
ImuMount::BodyAligned
|
||||
),
|
||||
ImuEvent::Fault(_)
|
||||
));
|
||||
assert!(matches!(
|
||||
|
||||
@@ -0,0 +1,241 @@
|
||||
//! Owned bounded IRQ TX: moving/dropping a queue cannot leave a DMA reader.
|
||||
use kaptan_mavlink::MavlinkFrame;
|
||||
use kaptan_time::Timestamp;
|
||||
use kaptan_tx::{PendingFrame, TransmitQueue};
|
||||
|
||||
pub(crate) const RX_CHUNK_BYTES: usize = 16;
|
||||
const RX_CAPACITY: usize = 512;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub(crate) enum ReceiveEvent {
|
||||
Byte(u8),
|
||||
Fault,
|
||||
Idle,
|
||||
}
|
||||
pub(crate) trait SerialDevice: UartIo {
|
||||
fn receive(&mut self) -> ReceiveEvent;
|
||||
}
|
||||
|
||||
/// A short-lock handoff, with the IRQ receipt time of every byte retained.
|
||||
pub struct RxChunk {
|
||||
pub(crate) bytes: [u8; RX_CHUNK_BYTES],
|
||||
pub(crate) timestamps: [Timestamp; RX_CHUNK_BYTES],
|
||||
pub(crate) length: usize,
|
||||
pub(crate) discontinuity: bool,
|
||||
pub(crate) pending: bool,
|
||||
pub(crate) completed: bool,
|
||||
}
|
||||
impl Default for RxChunk {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
bytes: [0; RX_CHUNK_BYTES],
|
||||
timestamps: [Timestamp::default(); RX_CHUNK_BYTES],
|
||||
length: 0,
|
||||
discontinuity: false,
|
||||
pending: false,
|
||||
completed: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// PAC + byte queues only. This owner never decodes protocols or calls storage.
|
||||
/// 512 bytes bound backlog to ~44 ms at 115200 / ~11 ms at 460800 baud.
|
||||
/// Overflow discards the damaged stream and orders a decoder reset before any
|
||||
/// subsequently admitted bytes, rather than splicing across a lost byte.
|
||||
pub(crate) struct SerialIo<I> {
|
||||
device: Option<I>,
|
||||
tx: Option<IrqTransmit>,
|
||||
bytes: [u8; RX_CAPACITY],
|
||||
timestamps: [Timestamp; RX_CAPACITY],
|
||||
head: usize,
|
||||
length: usize,
|
||||
discontinuity: bool,
|
||||
completed: bool,
|
||||
}
|
||||
|
||||
impl<I: SerialDevice> SerialIo<I> {
|
||||
pub(crate) fn new(device: Option<I>, transmit: bool) -> Self {
|
||||
Self {
|
||||
device,
|
||||
tx: transmit.then(IrqTransmit::new),
|
||||
bytes: [0; RX_CAPACITY],
|
||||
timestamps: [Timestamp::default(); RX_CAPACITY],
|
||||
head: 0,
|
||||
length: 0,
|
||||
discontinuity: false,
|
||||
completed: false,
|
||||
}
|
||||
}
|
||||
/// Timestamp is captured by the canonical clock immediately before this
|
||||
/// bounded acknowledgement, not later when the protocol worker drains RX.
|
||||
pub fn on_interrupt(&mut self, now: Timestamp) -> bool {
|
||||
let Some(device) = self.device.as_mut() else {
|
||||
return false;
|
||||
};
|
||||
let event = device.receive();
|
||||
let completed = self.tx.as_mut().is_some_and(|tx| tx.on_interrupt(device));
|
||||
self.completed |= completed;
|
||||
if matches!(event, ReceiveEvent::Fault)
|
||||
|| (matches!(event, ReceiveEvent::Byte(_)) && self.length == RX_CAPACITY)
|
||||
{
|
||||
self.head = 0;
|
||||
self.length = 0;
|
||||
self.discontinuity = true;
|
||||
}
|
||||
if let ReceiveEvent::Byte(byte) = event {
|
||||
let index = (self.head + self.length) % RX_CAPACITY;
|
||||
self.bytes[index] = byte;
|
||||
self.timestamps[index] = now;
|
||||
self.length += 1;
|
||||
}
|
||||
!matches!(event, ReceiveEvent::Idle) || completed
|
||||
}
|
||||
pub(crate) fn take_chunk(&mut self) -> RxChunk {
|
||||
let mut chunk = RxChunk {
|
||||
discontinuity: core::mem::take(&mut self.discontinuity),
|
||||
completed: core::mem::take(&mut self.completed),
|
||||
length: self.length.min(RX_CHUNK_BYTES),
|
||||
..RxChunk::default()
|
||||
};
|
||||
for index in 0..chunk.length {
|
||||
chunk.bytes[index] = self.bytes[self.head];
|
||||
chunk.timestamps[index] = self.timestamps[self.head];
|
||||
self.head = (self.head + 1) % RX_CAPACITY;
|
||||
}
|
||||
self.length -= chunk.length;
|
||||
chunk.pending = self.length != 0;
|
||||
chunk
|
||||
}
|
||||
pub(crate) fn send(&mut self, frame: PendingFrame<MavlinkFrame>) -> bool {
|
||||
match (&mut self.device, &mut self.tx) {
|
||||
(Some(device), Some(tx)) => tx.send(device, frame),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) trait UartIo {
|
||||
fn tx_ready(&mut self) -> bool;
|
||||
fn tx_complete(&mut self) -> bool;
|
||||
fn write_byte(&mut self, byte: u8);
|
||||
fn interrupts(&mut self, ready: bool, complete: bool);
|
||||
}
|
||||
|
||||
pub(crate) struct IrqTransmit {
|
||||
queue: TransmitQueue<MavlinkFrame, 4>,
|
||||
offset: usize,
|
||||
waiting_complete: bool,
|
||||
}
|
||||
impl IrqTransmit {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self {
|
||||
queue: TransmitQueue::new(),
|
||||
offset: 0,
|
||||
waiting_complete: false,
|
||||
}
|
||||
}
|
||||
pub(crate) fn send(&mut self, io: &mut impl UartIo, frame: PendingFrame<MavlinkFrame>) -> bool {
|
||||
let accepted = self.queue.enqueue(frame).is_retained();
|
||||
self.arm(io);
|
||||
accepted
|
||||
}
|
||||
fn arm(&mut self, io: &mut impl UartIo) {
|
||||
let _ = self
|
||||
.queue
|
||||
.start_with(|_| Ok::<_, core::convert::Infallible>(()));
|
||||
io.interrupts(
|
||||
self.queue.is_active() && !self.waiting_complete,
|
||||
self.waiting_complete,
|
||||
);
|
||||
}
|
||||
/// At most one byte copied to TDR and one frame completion per interrupt.
|
||||
pub(crate) fn on_interrupt(&mut self, io: &mut impl UartIo) -> bool {
|
||||
let mut completed = false;
|
||||
if self.waiting_complete && io.tx_complete() {
|
||||
self.queue.complete();
|
||||
self.offset = 0;
|
||||
self.waiting_complete = false;
|
||||
completed = true;
|
||||
}
|
||||
self.arm(io);
|
||||
if !self.waiting_complete
|
||||
&& io.tx_ready()
|
||||
&& let Some(active) = self.queue.active()
|
||||
{
|
||||
let bytes = active.frame.raw_bytes();
|
||||
io.write_byte(bytes[self.offset]);
|
||||
self.offset += 1;
|
||||
self.waiting_complete = self.offset == bytes.len();
|
||||
self.arm(io);
|
||||
}
|
||||
completed
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod receive_tests {
|
||||
use super::*;
|
||||
struct Input(ReceiveEvent);
|
||||
impl UartIo for Input {
|
||||
fn tx_ready(&mut self) -> bool {
|
||||
false
|
||||
}
|
||||
fn tx_complete(&mut self) -> bool {
|
||||
false
|
||||
}
|
||||
fn write_byte(&mut self, _: u8) {
|
||||
panic!("receive-only endpoint");
|
||||
}
|
||||
fn interrupts(&mut self, _: bool, _: bool) {
|
||||
panic!("receive-only endpoint");
|
||||
}
|
||||
}
|
||||
impl SerialDevice for Input {
|
||||
fn receive(&mut self) -> ReceiveEvent {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn overflow_and_hardware_fault_order_reset_before_later_bytes() {
|
||||
let mut io = SerialIo::new(Some(Input(ReceiveEvent::Byte(42))), false);
|
||||
for t in 0..=RX_CAPACITY {
|
||||
assert!(io.on_interrupt(Timestamp::from_microseconds(t as u64)));
|
||||
}
|
||||
let chunk = io.take_chunk();
|
||||
assert!(chunk.discontinuity);
|
||||
assert_eq!(chunk.length, 1);
|
||||
assert_eq!(chunk.timestamps[0].as_microseconds(), RX_CAPACITY as u64);
|
||||
io.device.as_mut().unwrap().0 = ReceiveEvent::Fault;
|
||||
io.on_interrupt(Timestamp::from_microseconds(1000));
|
||||
io.device.as_mut().unwrap().0 = ReceiveEvent::Byte(7);
|
||||
io.on_interrupt(Timestamp::from_microseconds(1001));
|
||||
let chunk = io.take_chunk();
|
||||
assert!(chunk.discontinuity);
|
||||
assert_eq!(&chunk.bytes[..chunk.length], &[7]);
|
||||
assert_eq!(chunk.timestamps[0].as_microseconds(), 1001);
|
||||
}
|
||||
#[test]
|
||||
fn drains_are_bounded_and_disabled_endpoints_never_touch_registers() {
|
||||
let mut io = SerialIo::<Input>::new(None, false);
|
||||
assert!(!io.on_interrupt(Timestamp::default()));
|
||||
assert_eq!(io.take_chunk().length, 0);
|
||||
let mut io = SerialIo::new(Some(Input(ReceiveEvent::Byte(1))), false);
|
||||
for t in 0..35 {
|
||||
io.on_interrupt(Timestamp::from_microseconds(t));
|
||||
}
|
||||
for (length, pending) in [(16, true), (16, true), (3, false)] {
|
||||
let chunk = io.take_chunk();
|
||||
assert_eq!(chunk.length, length);
|
||||
assert_eq!(chunk.pending, pending);
|
||||
assert!(!chunk.completed, "RX must not manufacture a TX completion");
|
||||
}
|
||||
io.device.as_mut().unwrap().0 = ReceiveEvent::Idle;
|
||||
assert!(!io.on_interrupt(Timestamp::default()));
|
||||
let frame = kaptan_mavlink::encoder::MavlinkEncoder::new(1, 1)
|
||||
.encode(&kaptan_mavlink::generated::kaptan::HEARTBEAT_DATA::default());
|
||||
assert!(!io.send(PendingFrame {
|
||||
class: kaptan_tx::FrameClass::Vital,
|
||||
frame
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,21 @@ use usbd_serial::CdcAcmClass;
|
||||
|
||||
pub(crate) const USB_PACKET_BYTES: usize = 64;
|
||||
|
||||
pub(crate) struct UsbMavlink<'a, B: UsbBus> {
|
||||
pub(crate) trait UsbBusHealth: UsbBus {
|
||||
fn is_offline(&self) -> bool;
|
||||
fn disable(&self);
|
||||
}
|
||||
|
||||
impl<P: synopsys_usb_otg::UsbPeripheral> UsbBusHealth for synopsys_usb_otg::UsbBus<P> {
|
||||
fn is_offline(&self) -> bool {
|
||||
self.is_offline()
|
||||
}
|
||||
fn disable(&self) {
|
||||
self.disable();
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct UsbMavlink<'a, B: UsbBusHealth> {
|
||||
device: UsbDevice<'a, B>,
|
||||
cdc: CdcAcmClass<'a, B>,
|
||||
transmit: TransmitQueue<MavlinkFrame, 4>,
|
||||
@@ -26,7 +40,7 @@ pub(crate) struct UsbMavlink<'a, B: UsbBus> {
|
||||
rx_remaining: Range<usize>,
|
||||
}
|
||||
|
||||
impl<'a, B: UsbBus> UsbMavlink<'a, B> {
|
||||
impl<'a, B: UsbBusHealth> UsbMavlink<'a, B> {
|
||||
pub(crate) fn new(device: UsbDevice<'a, B>, cdc: CdcAcmClass<'a, B>) -> Self {
|
||||
Self {
|
||||
device,
|
||||
@@ -40,7 +54,26 @@ impl<'a, B: UsbBus> UsbMavlink<'a, B> {
|
||||
}
|
||||
|
||||
pub(crate) fn is_open(&self) -> bool {
|
||||
self.device.state() == UsbDeviceState::Configured && self.cdc.dtr()
|
||||
!self.device.bus().is_offline()
|
||||
&& self.device.state() == UsbDeviceState::Configured
|
||||
&& self.cdc.dtr()
|
||||
}
|
||||
|
||||
/// Gates RX FIFO interrupts while packet bytes remain unconsumed.
|
||||
/// The caller must hold the USB lock through the operation and gate update.
|
||||
/// A partial MAVLink frame alone must not mask RX.
|
||||
pub(crate) fn with_rx_interrupt_gate<R>(
|
||||
&mut self,
|
||||
operation: impl FnOnce(&mut Self) -> R,
|
||||
set_rx_interrupt_enabled: impl FnOnce(bool),
|
||||
) -> R {
|
||||
let result = operation(self);
|
||||
// A terminal bus fault holds the peripheral in reset. Do not access its
|
||||
// interrupt registers again, including through the delegated RX gate.
|
||||
if !self.device.bus().is_offline() {
|
||||
set_rx_interrupt_enabled(self.rx_remaining.is_empty());
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/// Returns whether the frame was retained, not whether the host received it.
|
||||
@@ -50,34 +83,45 @@ impl<'a, B: UsbBus> UsbMavlink<'a, B> {
|
||||
}
|
||||
let retained = self.transmit.enqueue(pending).is_retained();
|
||||
self.service_transmit();
|
||||
retained
|
||||
retained && !self.device.bus().is_offline()
|
||||
}
|
||||
|
||||
/// Services USB and buffers at most one packet for the protocol worker.
|
||||
pub(crate) fn poll(&mut self) -> bool {
|
||||
if self.device.bus().is_offline() {
|
||||
self.close();
|
||||
return false;
|
||||
}
|
||||
let previous_state = self.device.state();
|
||||
let previous_dtr = self.cdc.dtr();
|
||||
self.device.poll(&mut [&mut self.cdc]);
|
||||
if self.device.bus().is_offline() {
|
||||
self.close();
|
||||
return false;
|
||||
}
|
||||
|
||||
if previous_state != UsbDeviceState::Default
|
||||
&& self.device.state() == UsbDeviceState::Default
|
||||
{
|
||||
// Only bus reset flushes endpoint FIFOs. DTR close, suspend, and
|
||||
// deconfiguration pause TX without invalidating its byte cursor.
|
||||
let bus_reset = previous_state != UsbDeviceState::Default
|
||||
&& self.device.state() == UsbDeviceState::Default;
|
||||
if bus_reset {
|
||||
self.transmit = TransmitQueue::new();
|
||||
self.tx_offset = 0;
|
||||
self.reset_receive();
|
||||
} else if previous_dtr && !self.cdc.dtr() {
|
||||
self.discard_endpoint_receive();
|
||||
}
|
||||
if previous_dtr && !self.cdc.dtr() {
|
||||
self.reset_receive();
|
||||
}
|
||||
|
||||
if self.rx_remaining.is_empty() && self.device.state() == UsbDeviceState::Configured {
|
||||
if !self.device.bus().is_offline()
|
||||
&& self.rx_remaining.is_empty()
|
||||
&& self.device.state() == UsbDeviceState::Configured
|
||||
{
|
||||
// A previous poll may have left a packet in the bus while our
|
||||
// buffer was occupied. Reading must not depend on poll's result.
|
||||
match self.cdc.read_packet(&mut self.rx_packet) {
|
||||
Ok(length) if self.is_open() => self.rx_remaining = 0..length,
|
||||
Ok(_) | Err(UsbError::WouldBlock) => {}
|
||||
Err(error) => panic!("CDC OUT endpoint invariant: {error:?}"),
|
||||
Err(_) => self.close(),
|
||||
}
|
||||
}
|
||||
self.service_transmit();
|
||||
@@ -86,6 +130,10 @@ impl<'a, B: UsbBus> UsbMavlink<'a, B> {
|
||||
|
||||
/// Parses buffered bytes in the worker, without a second receive queue.
|
||||
pub(crate) fn take_received(&mut self) -> Option<IntegrityCheckedFrame> {
|
||||
if self.device.bus().is_offline() {
|
||||
self.close();
|
||||
return None;
|
||||
}
|
||||
for index in self.rx_remaining.by_ref() {
|
||||
if let Some(frame) = self.framer.receive_byte(self.rx_packet[index]) {
|
||||
return Some(frame);
|
||||
@@ -99,6 +147,23 @@ impl<'a, B: UsbBus> UsbMavlink<'a, B> {
|
||||
self.rx_remaining = 0..0;
|
||||
}
|
||||
|
||||
fn close(&mut self) {
|
||||
self.device.bus().disable();
|
||||
self.transmit = TransmitQueue::new();
|
||||
self.tx_offset = 0;
|
||||
self.reset_receive();
|
||||
}
|
||||
|
||||
fn discard_endpoint_receive(&mut self) {
|
||||
// The pinned driver's deconfigure/configure paths flush hardware FIFOs,
|
||||
// but do not clear EndpointBuffer::has_data. Read/discard its single
|
||||
// CDC OUT software packet as cleanup, without admitting it.
|
||||
match self.cdc.read_packet(&mut self.rx_packet) {
|
||||
Ok(_) | Err(UsbError::WouldBlock) => {}
|
||||
Err(_) => self.close(),
|
||||
}
|
||||
}
|
||||
|
||||
fn service_transmit(&mut self) {
|
||||
if !self.is_open() {
|
||||
return;
|
||||
@@ -111,7 +176,9 @@ impl<'a, B: UsbBus> UsbMavlink<'a, B> {
|
||||
let end = (*offset + USB_PACKET_BYTES).min(bytes.len());
|
||||
let packet = &bytes[*offset..end];
|
||||
let written = cdc.write_packet(packet)?;
|
||||
assert_eq!(written, packet.len(), "CDC IN endpoint short write");
|
||||
if written != packet.len() {
|
||||
return Err(UsbError::InvalidState);
|
||||
}
|
||||
*offset = end;
|
||||
// An exact multiple of 64 needs a ZLP: retain offset == len until
|
||||
// the next empty packet is accepted. The endpoint copies bytes,
|
||||
@@ -125,11 +192,17 @@ impl<'a, B: UsbBus> UsbMavlink<'a, B> {
|
||||
};
|
||||
match result {
|
||||
Ok(()) if complete => {
|
||||
self.transmit.complete();
|
||||
self.transmit
|
||||
.complete()
|
||||
.expect("completed active USB frame");
|
||||
self.tx_offset = 0;
|
||||
}
|
||||
Ok(()) | Err(UsbError::WouldBlock) => {}
|
||||
Err(error) => panic!("CDC IN endpoint invariant: {error:?}"),
|
||||
Err(_) => self.close(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../../tests/support/usb_bus.rs"]
|
||||
pub(crate) mod test_bus;
|
||||
@@ -1,20 +1,24 @@
|
||||
//! STM32F7 execution platform for declarative Kaptan boards.
|
||||
//! STM32F7 drivers and RTIC runtime.
|
||||
//!
|
||||
//! A board crate selects the safe route descriptors below and invokes
|
||||
//! [`flight_firmware!`]. The macro specializes one platform-defined RTIC task
|
||||
//! graph for that board's configuration aggregate. RTIC, PAC ownership,
|
||||
//! interrupt acknowledgement, DMA, and atomics remain platform implementation
|
||||
//! details rather than board-package code.
|
||||
//! Board crates supply wiring and configuration to [`flight_firmware!`].
|
||||
|
||||
#![no_std]
|
||||
|
||||
#[cfg(test)]
|
||||
extern crate self as platform_stm32f7;
|
||||
|
||||
use kaptan_config::Config;
|
||||
use kaptan_err_reportable::KaptanError;
|
||||
|
||||
pub use f722::F722UserConfig;
|
||||
pub use f722::ports::{UART1, UART2, UART3, UART4, UART6};
|
||||
pub use kaptan_drivers::mpu6000::{
|
||||
AccelerometerRange, DigitalLowPassFilter, GyroscopeRange, Mpu6000Config,
|
||||
};
|
||||
pub use kaptan_log::LevelFilter;
|
||||
pub use kaptan_ports;
|
||||
|
||||
use f722::F722Route;
|
||||
|
||||
/// Fixed sensor-to-body mounting transform.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
@@ -23,6 +27,8 @@ pub enum ImuMount {
|
||||
BodyAligned,
|
||||
/// Sensor is rotated 180 degrees around the body Z axis.
|
||||
Yaw180,
|
||||
/// Board mounting maps sensor axes (X, Y, Z) to (-Y, X, Z).
|
||||
Yaw270,
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
@@ -32,7 +38,6 @@ pub enum ImuMount {
|
||||
)]
|
||||
pub mod f722;
|
||||
|
||||
/// Computes the exact bounded snapshot storage selected by a board aggregate.
|
||||
#[doc(hidden)]
|
||||
pub const fn config_snapshot_length<C: Config>() -> usize {
|
||||
assert!(
|
||||
@@ -42,14 +47,12 @@ pub const fn config_snapshot_length<C: Config>() -> usize {
|
||||
kaptan_config::scalar_snapshot_length(C::FIELD_COUNT as u16, C::SNAPSHOT_VALUE_BYTES)
|
||||
}
|
||||
|
||||
/// Dependencies named only by the platform's exported RTIC macro expansion.
|
||||
/// Board source never depends on these implementation details directly.
|
||||
/// Dependencies used by the exported RTIC macro.
|
||||
#[doc(hidden)]
|
||||
#[cfg_attr(not(target_os = "none"), allow(unused_imports))]
|
||||
pub mod __platform_internal {
|
||||
pub use cortex_m as cm;
|
||||
pub use cortex_m_rt as cmrt;
|
||||
pub use kaptan_dma as dma;
|
||||
pub use kaptan_log as logging;
|
||||
#[cfg(target_os = "none")]
|
||||
pub use panic_halt as panic;
|
||||
@@ -62,16 +65,10 @@ pub mod __platform_internal {
|
||||
/// Construction or validation failure for a declarative F722 board.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, KaptanError)]
|
||||
pub enum BoardError {
|
||||
/// The immutable vertical route and its electrical parameters disagree.
|
||||
#[kaptan_error(id = 11901)]
|
||||
VerticalSensorCapabilityMismatch,
|
||||
/// The declaration requests a service-timer frequency not implemented by
|
||||
/// the selected F722 platform profile.
|
||||
#[kaptan_error(id = 11902)]
|
||||
UnsupportedServiceFrequency,
|
||||
/// The selected baud cannot carry the deterministic telemetry schedule.
|
||||
#[kaptan_error(id = 11903)]
|
||||
TelemetryLinkOvercommitted,
|
||||
/// The platform could not establish a clock, peripheral, or DMA owner.
|
||||
#[kaptan_error(id = 11904)]
|
||||
Initialization,
|
||||
@@ -80,472 +77,152 @@ pub enum BoardError {
|
||||
Configuration,
|
||||
}
|
||||
|
||||
/// MPU6000 component selector.
|
||||
/// MPU6000 on SPI1 (PA5/PA6/PA7, PB2 chip select, PA4 data-ready).
|
||||
///
|
||||
/// Driver settings must be supplied explicitly:
|
||||
///
|
||||
/// ```compile_fail,E0063
|
||||
/// use platform_stm32f7::{ImuMount, Mpu6000Spi1};
|
||||
/// let _ = Mpu6000Spi1 { mount: ImuMount::BodyAligned };
|
||||
/// ```
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct Mpu6000;
|
||||
/// MPU6000 SPI1 route awaiting a data-ready pin.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct Mpu6000Spi1;
|
||||
/// MPU6000 SPI1/PA4 route awaiting its physical mount.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct Mpu6000Spi1Exti4;
|
||||
/// Mounted MPU6000 route awaiting its driver configuration.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct MountedMpu6000 {
|
||||
mount: ImuMount,
|
||||
}
|
||||
/// Complete MPU6000 declaration.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct ConfiguredMpu6000 {
|
||||
mount: ImuMount,
|
||||
config: Mpu6000Config,
|
||||
pub struct Mpu6000Spi1 {
|
||||
pub mount: ImuMount,
|
||||
pub config: Mpu6000Config,
|
||||
}
|
||||
|
||||
impl Mpu6000 {
|
||||
pub const fn on_spi1() -> Mpu6000Spi1 {
|
||||
Mpu6000Spi1
|
||||
/// ICM-42688-P on SPI3 (PC10/PC11/PC12, PA15 chip select, PC3 data-ready).
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct Icm42688Spi3 {
|
||||
pub mount: ImuMount,
|
||||
}
|
||||
|
||||
/// Optional vertical sensing supported by either electrical route.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum VerticalSensor {
|
||||
None,
|
||||
/// LIDAR-Lite V3 range on I2C1 (PB8/PB9).
|
||||
LidarLiteV3I2c1,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum StatusLed {
|
||||
ActiveLowPd2,
|
||||
ActiveHighPa13,
|
||||
}
|
||||
|
||||
/// Documented motor pins driven low before fallible initialization.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum MotorPins {
|
||||
/// PB0, PB1, PB4 and PB5.
|
||||
Four,
|
||||
/// PB0, PB1, PB4, PB5, PB6 and PB7.
|
||||
Six,
|
||||
}
|
||||
|
||||
/// Board serial ports in schema order. Boot validates each descriptor against
|
||||
/// the supported MCU routes.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct SerialInventory(&'static [kaptan_ports::PortDescriptor]);
|
||||
impl SerialInventory {
|
||||
pub const fn new(descriptors: &'static [kaptan_ports::PortDescriptor]) -> Self {
|
||||
Self(descriptors)
|
||||
}
|
||||
pub const fn len(self) -> usize {
|
||||
self.0.len()
|
||||
}
|
||||
pub const fn is_empty(self) -> bool {
|
||||
self.0.is_empty()
|
||||
}
|
||||
pub const fn descriptors(self) -> &'static [kaptan_ports::PortDescriptor] {
|
||||
self.0
|
||||
}
|
||||
/// Resolves a stable IRQ/wire identity to the board's compact storage slot.
|
||||
pub fn index(self, id: kaptan_ports::PortId) -> Option<usize> {
|
||||
self.0.iter().position(|descriptor| descriptor.id == id)
|
||||
}
|
||||
}
|
||||
|
||||
impl Mpu6000Spi1 {
|
||||
pub const fn with_drdy(self, _: Pa4) -> Mpu6000Spi1Exti4 {
|
||||
Mpu6000Spi1Exti4
|
||||
}
|
||||
/// Fixed board wiring.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct F722Hardware<R: F722Route> {
|
||||
route: R,
|
||||
serial: SerialInventory,
|
||||
led: StatusLed,
|
||||
vertical: VerticalSensor,
|
||||
motors: MotorPins,
|
||||
}
|
||||
|
||||
impl Mpu6000Spi1Exti4 {
|
||||
pub const fn mounted(self, mount: ImuMount) -> MountedMpu6000 {
|
||||
MountedMpu6000 { mount }
|
||||
}
|
||||
}
|
||||
|
||||
impl MountedMpu6000 {
|
||||
pub const fn configured(self, config: Mpu6000Config) -> ConfiguredMpu6000 {
|
||||
ConfiguredMpu6000 {
|
||||
mount: self.mount,
|
||||
config,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ICM-42688-P component selector.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct Icm42688;
|
||||
/// ICM-42688-P SPI3 route awaiting a data-ready pin.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct Icm42688Spi3;
|
||||
/// ICM-42688-P SPI3/PC3 route awaiting its physical mount.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct Icm42688Spi3Exti3;
|
||||
/// Complete ICM-42688-P declaration. The driver currently exposes no tuning
|
||||
/// object, so the manifest does not pretend one exists.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct MountedIcm42688 {
|
||||
mount: ImuMount,
|
||||
}
|
||||
|
||||
impl Icm42688 {
|
||||
pub const fn on_spi3() -> Icm42688Spi3 {
|
||||
Icm42688Spi3
|
||||
}
|
||||
}
|
||||
|
||||
impl Icm42688Spi3 {
|
||||
pub const fn with_drdy(self, _: Pc3) -> Icm42688Spi3Exti3 {
|
||||
Icm42688Spi3Exti3
|
||||
}
|
||||
}
|
||||
|
||||
impl Icm42688Spi3Exti3 {
|
||||
pub const fn mounted(self, mount: ImuMount) -> MountedIcm42688 {
|
||||
MountedIcm42688 { mount }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct Pa4;
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct Pc3;
|
||||
|
||||
/// MAVLink component selector.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct Mavlink;
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct UnconfiguredMavlinkUsart1;
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct UnconfiguredMavlinkUsart2;
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct MavlinkUsart1 {
|
||||
baud: u32,
|
||||
}
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct MavlinkUsart2 {
|
||||
baud: u32,
|
||||
}
|
||||
|
||||
impl Mavlink {
|
||||
pub const fn on_usart1() -> UnconfiguredMavlinkUsart1 {
|
||||
UnconfiguredMavlinkUsart1
|
||||
}
|
||||
|
||||
pub const fn on_usart2() -> UnconfiguredMavlinkUsart2 {
|
||||
UnconfiguredMavlinkUsart2
|
||||
}
|
||||
}
|
||||
|
||||
impl UnconfiguredMavlinkUsart1 {
|
||||
pub const fn baud(self, baud: u32) -> MavlinkUsart1 {
|
||||
MavlinkUsart1 { baud }
|
||||
}
|
||||
}
|
||||
|
||||
impl UnconfiguredMavlinkUsart2 {
|
||||
pub const fn baud(self, baud: u32) -> MavlinkUsart2 {
|
||||
MavlinkUsart2 { baud }
|
||||
}
|
||||
}
|
||||
|
||||
/// Vertical-sensor component selector.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct VerticalSensor;
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct NoVerticalSensor;
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct UnconfiguredMtf01Uart4;
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct Mtf01Uart4 {
|
||||
baud: u32,
|
||||
}
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct LidarLiteV3I2c1;
|
||||
|
||||
impl VerticalSensor {
|
||||
pub const fn none() -> NoVerticalSensor {
|
||||
NoVerticalSensor
|
||||
}
|
||||
|
||||
pub const fn mtf01_on_uart4() -> UnconfiguredMtf01Uart4 {
|
||||
UnconfiguredMtf01Uart4
|
||||
}
|
||||
|
||||
pub const fn lidar_lite_v3_on_i2c1() -> LidarLiteV3I2c1 {
|
||||
LidarLiteV3I2c1
|
||||
}
|
||||
}
|
||||
|
||||
impl UnconfiguredMtf01Uart4 {
|
||||
pub const fn baud(self, baud: u32) -> Mtf01Uart4 {
|
||||
Mtf01Uart4 { baud }
|
||||
}
|
||||
}
|
||||
|
||||
/// Required boot-time output inhibition route.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct Pb0Pb1Pb4Pb5Low;
|
||||
/// Kakute status LED on PD2, active low.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct Pd2ActiveLow;
|
||||
/// GEPRC status LED on PA13, active high.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct Pa13ActiveHigh;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct Missing;
|
||||
|
||||
/// Complete, non-executing hardware manifest stored in [`F722ImmutableConfig`].
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct F722Hardware {
|
||||
profile: F722Profile,
|
||||
vertical: f722::VerticalRoute,
|
||||
imu_mount: ImuMount,
|
||||
mpu6000_config: Option<Mpu6000Config>,
|
||||
mavlink_baud: u32,
|
||||
vertical_sensor_baud: Option<u32>,
|
||||
status_led: F722StatusLedRoute,
|
||||
}
|
||||
|
||||
impl F722Hardware {
|
||||
/// Starts an immutable hardware declaration. Only complete, compatible
|
||||
/// declarations expose [`F722HardwareBuilder::telemetry_only`].
|
||||
#[allow(clippy::new_ret_no_self)]
|
||||
pub const fn new() -> F722HardwareBuilder {
|
||||
F722HardwareBuilder {
|
||||
output: Missing,
|
||||
imu: Missing,
|
||||
mavlink: Missing,
|
||||
vertical: Missing,
|
||||
status_led: Missing,
|
||||
impl<R: F722Route> F722Hardware<R> {
|
||||
pub const fn new(
|
||||
route: R,
|
||||
serial: SerialInventory,
|
||||
led: StatusLed,
|
||||
vertical: VerticalSensor,
|
||||
motors: MotorPins,
|
||||
) -> Self {
|
||||
Self {
|
||||
route,
|
||||
serial,
|
||||
led,
|
||||
vertical,
|
||||
motors,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) const fn profile(self) -> F722Profile {
|
||||
self.profile
|
||||
}
|
||||
|
||||
pub(crate) const fn vertical(self) -> f722::VerticalRoute {
|
||||
pub(crate) const fn vertical(self) -> VerticalSensor {
|
||||
self.vertical
|
||||
}
|
||||
|
||||
pub(crate) const fn imu_mount(self) -> ImuMount {
|
||||
self.imu_mount
|
||||
}
|
||||
|
||||
pub(crate) const fn mpu6000_config(self) -> Option<Mpu6000Config> {
|
||||
self.mpu6000_config
|
||||
}
|
||||
|
||||
pub(crate) const fn mavlink_baud(self) -> u32 {
|
||||
self.mavlink_baud
|
||||
}
|
||||
|
||||
pub(crate) const fn vertical_sensor_baud(self) -> Option<u32> {
|
||||
self.vertical_sensor_baud
|
||||
}
|
||||
|
||||
pub(crate) const fn status_led(self) -> F722StatusLedRoute {
|
||||
self.status_led
|
||||
pub const fn serial(self) -> SerialInventory {
|
||||
self.serial
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub const fn has_range(self) -> bool {
|
||||
matches!(
|
||||
self.vertical,
|
||||
f722::VerticalRoute::Mtf01 | f722::VerticalRoute::LidarLiteV3
|
||||
)
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub const fn has_optical_flow(self) -> bool {
|
||||
matches!(self.vertical, f722::VerticalRoute::Mtf01)
|
||||
}
|
||||
}
|
||||
|
||||
/// Typestate used only while constructing [`F722Hardware`].
|
||||
pub struct F722HardwareBuilder<
|
||||
Output = Missing,
|
||||
Imu = Missing,
|
||||
Link = Missing,
|
||||
Vertical = Missing,
|
||||
Led = Missing,
|
||||
> {
|
||||
output: Output,
|
||||
imu: Imu,
|
||||
mavlink: Link,
|
||||
vertical: Vertical,
|
||||
status_led: Led,
|
||||
}
|
||||
|
||||
impl<I: Copy, M: Copy, V: Copy, L: Copy> F722HardwareBuilder<Missing, I, M, V, L> {
|
||||
pub const fn output_inhibit(
|
||||
self,
|
||||
output: Pb0Pb1Pb4Pb5Low,
|
||||
) -> F722HardwareBuilder<Pb0Pb1Pb4Pb5Low, I, M, V, L> {
|
||||
F722HardwareBuilder {
|
||||
output,
|
||||
imu: self.imu,
|
||||
mavlink: self.mavlink,
|
||||
vertical: self.vertical,
|
||||
status_led: self.status_led,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<O: Copy, M: Copy, V: Copy, L: Copy> F722HardwareBuilder<O, Missing, M, V, L> {
|
||||
pub const fn imu<I: Copy>(self, imu: I) -> F722HardwareBuilder<O, I, M, V, L> {
|
||||
F722HardwareBuilder {
|
||||
output: self.output,
|
||||
imu,
|
||||
mavlink: self.mavlink,
|
||||
vertical: self.vertical,
|
||||
status_led: self.status_led,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<O: Copy, I: Copy, V: Copy, L: Copy> F722HardwareBuilder<O, I, Missing, V, L> {
|
||||
pub const fn mavlink<M: Copy>(self, mavlink: M) -> F722HardwareBuilder<O, I, M, V, L> {
|
||||
F722HardwareBuilder {
|
||||
output: self.output,
|
||||
imu: self.imu,
|
||||
mavlink,
|
||||
vertical: self.vertical,
|
||||
status_led: self.status_led,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<O: Copy, I: Copy, M: Copy, L: Copy> F722HardwareBuilder<O, I, M, Missing, L> {
|
||||
pub const fn vertical_sensor<V: Copy>(self, vertical: V) -> F722HardwareBuilder<O, I, M, V, L> {
|
||||
F722HardwareBuilder {
|
||||
output: self.output,
|
||||
imu: self.imu,
|
||||
mavlink: self.mavlink,
|
||||
vertical,
|
||||
status_led: self.status_led,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<O: Copy, I: Copy, M: Copy, V: Copy> F722HardwareBuilder<O, I, M, V, Missing> {
|
||||
pub const fn status_led<L: Copy>(self, status_led: L) -> F722HardwareBuilder<O, I, M, V, L> {
|
||||
F722HardwareBuilder {
|
||||
output: self.output,
|
||||
imu: self.imu,
|
||||
mavlink: self.mavlink,
|
||||
vertical: self.vertical,
|
||||
status_led,
|
||||
}
|
||||
!matches!(self.vertical, VerticalSensor::None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Board configuration with matching hardware and interrupt routes.
|
||||
///
|
||||
/// ```compile_fail,E0308
|
||||
/// use platform_stm32f7::*;
|
||||
/// fn wrong_route(hardware: F722Hardware<Icm42688Spi3>) {
|
||||
/// let _: F722ImmutableConfig<Mpu6000Spi1> = F722ImmutableConfig {
|
||||
/// name: "mismatch",
|
||||
/// service_hz: 100,
|
||||
/// hardware,
|
||||
/// };
|
||||
/// }
|
||||
/// ```
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub(crate) enum F722Profile {
|
||||
Mpu6000Spi1Usart1,
|
||||
Icm42688Spi3Usart2,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub(crate) enum F722StatusLedRoute {
|
||||
Pd2ActiveLow,
|
||||
Pa13ActiveHigh,
|
||||
}
|
||||
|
||||
macro_rules! finish_mpu6000_hardware {
|
||||
($vertical:ty, $route:expr, no_serial_baud) => {
|
||||
impl
|
||||
F722HardwareBuilder<
|
||||
Pb0Pb1Pb4Pb5Low,
|
||||
ConfiguredMpu6000,
|
||||
MavlinkUsart1,
|
||||
$vertical,
|
||||
Pd2ActiveLow,
|
||||
>
|
||||
{
|
||||
pub const fn telemetry_only(self) -> F722Hardware {
|
||||
F722Hardware {
|
||||
profile: F722Profile::Mpu6000Spi1Usart1,
|
||||
vertical: $route,
|
||||
imu_mount: self.imu.mount,
|
||||
mpu6000_config: Some(self.imu.config),
|
||||
mavlink_baud: self.mavlink.baud,
|
||||
vertical_sensor_baud: None,
|
||||
status_led: F722StatusLedRoute::Pd2ActiveLow,
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
($vertical:ty, $route:expr, serial_baud) => {
|
||||
impl
|
||||
F722HardwareBuilder<
|
||||
Pb0Pb1Pb4Pb5Low,
|
||||
ConfiguredMpu6000,
|
||||
MavlinkUsart1,
|
||||
$vertical,
|
||||
Pd2ActiveLow,
|
||||
>
|
||||
{
|
||||
pub const fn telemetry_only(self) -> F722Hardware {
|
||||
F722Hardware {
|
||||
profile: F722Profile::Mpu6000Spi1Usart1,
|
||||
vertical: $route,
|
||||
imu_mount: self.imu.mount,
|
||||
mpu6000_config: Some(self.imu.config),
|
||||
mavlink_baud: self.mavlink.baud,
|
||||
vertical_sensor_baud: Some(self.vertical.baud),
|
||||
status_led: F722StatusLedRoute::Pd2ActiveLow,
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
finish_mpu6000_hardware!(NoVerticalSensor, f722::VerticalRoute::None, no_serial_baud);
|
||||
finish_mpu6000_hardware!(Mtf01Uart4, f722::VerticalRoute::Mtf01, serial_baud);
|
||||
finish_mpu6000_hardware!(
|
||||
LidarLiteV3I2c1,
|
||||
f722::VerticalRoute::LidarLiteV3,
|
||||
no_serial_baud
|
||||
);
|
||||
|
||||
macro_rules! finish_icm42688_hardware {
|
||||
($vertical:ty, $route:expr, no_serial_baud) => {
|
||||
impl
|
||||
F722HardwareBuilder<
|
||||
Pb0Pb1Pb4Pb5Low,
|
||||
MountedIcm42688,
|
||||
MavlinkUsart2,
|
||||
$vertical,
|
||||
Pa13ActiveHigh,
|
||||
>
|
||||
{
|
||||
pub const fn telemetry_only(self) -> F722Hardware {
|
||||
F722Hardware {
|
||||
profile: F722Profile::Icm42688Spi3Usart2,
|
||||
vertical: $route,
|
||||
imu_mount: self.imu.mount,
|
||||
mpu6000_config: None,
|
||||
mavlink_baud: self.mavlink.baud,
|
||||
vertical_sensor_baud: None,
|
||||
status_led: F722StatusLedRoute::Pa13ActiveHigh,
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
($vertical:ty, $route:expr, serial_baud) => {
|
||||
impl
|
||||
F722HardwareBuilder<
|
||||
Pb0Pb1Pb4Pb5Low,
|
||||
MountedIcm42688,
|
||||
MavlinkUsart2,
|
||||
$vertical,
|
||||
Pa13ActiveHigh,
|
||||
>
|
||||
{
|
||||
pub const fn telemetry_only(self) -> F722Hardware {
|
||||
F722Hardware {
|
||||
profile: F722Profile::Icm42688Spi3Usart2,
|
||||
vertical: $route,
|
||||
imu_mount: self.imu.mount,
|
||||
mpu6000_config: None,
|
||||
mavlink_baud: self.mavlink.baud,
|
||||
vertical_sensor_baud: Some(self.vertical.baud),
|
||||
status_led: F722StatusLedRoute::Pa13ActiveHigh,
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
finish_icm42688_hardware!(NoVerticalSensor, f722::VerticalRoute::None, no_serial_baud);
|
||||
finish_icm42688_hardware!(Mtf01Uart4, f722::VerticalRoute::Mtf01, serial_baud);
|
||||
finish_icm42688_hardware!(
|
||||
LidarLiteV3I2c1,
|
||||
f722::VerticalRoute::LidarLiteV3,
|
||||
no_serial_baud
|
||||
);
|
||||
|
||||
/// Literal immutable root embedded by each F722 board package.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct F722ImmutableConfig {
|
||||
pub struct F722ImmutableConfig<R: F722Route> {
|
||||
pub name: &'static str,
|
||||
pub service_hz: usize,
|
||||
pub hardware: F722Hardware,
|
||||
pub hardware: F722Hardware<R>,
|
||||
}
|
||||
|
||||
mod sealed {
|
||||
pub trait Route {}
|
||||
impl Route for super::Mpu6000Spi1 {}
|
||||
impl Route for super::Icm42688Spi3 {}
|
||||
|
||||
pub trait ImmutableConfig {}
|
||||
impl ImmutableConfig for super::F722ImmutableConfig {}
|
||||
impl<R: super::F722Route> ImmutableConfig for super::F722ImmutableConfig<R> {}
|
||||
}
|
||||
|
||||
/// Sealed associated-type bound preserving the visible two-root board
|
||||
/// contract while requiring the concrete platform manifest above.
|
||||
/// Restricts board declarations to the platform's configuration types.
|
||||
#[doc(hidden)]
|
||||
pub trait F7ImmutableConfig: Copy + sealed::ImmutableConfig {
|
||||
fn f722(&self) -> &F722ImmutableConfig;
|
||||
type Route: F722Route;
|
||||
fn f722(&self) -> &F722ImmutableConfig<Self::Route>;
|
||||
}
|
||||
|
||||
impl F7ImmutableConfig for F722ImmutableConfig {
|
||||
fn f722(&self) -> &F722ImmutableConfig {
|
||||
impl<R: F722Route> F7ImmutableConfig for F722ImmutableConfig<R> {
|
||||
type Route = R;
|
||||
|
||||
fn f722(&self) -> &F722ImmutableConfig<R> {
|
||||
self
|
||||
}
|
||||
}
|
||||
@@ -559,22 +236,13 @@ pub trait F7Board {
|
||||
const IMMUTABLE_CONFIG: Self::ImmutableConfig;
|
||||
}
|
||||
|
||||
/// Installs the one platform-owned RTIC application for a declarative board.
|
||||
/// Installs the F722 RTIC application for a board.
|
||||
///
|
||||
/// Board crates can only select one of the reviewed physical-sensor telemetry
|
||||
/// route descriptors; RTIC ownership stays platform-private.
|
||||
///
|
||||
/// The application binds the finite superset of interrupt roles needed by the
|
||||
/// currently supported F722 profiles. The route declaration selects which
|
||||
/// roles are initialized; inactive bindings acknowledge nothing. Adding an
|
||||
/// interrupt role is therefore a platform-only change, not a board task graph.
|
||||
/// The board's route selects hardware initialization and interrupt handlers.
|
||||
/// Inactive bindings do not access peripherals.
|
||||
#[macro_export]
|
||||
macro_rules! flight_firmware {
|
||||
($board:ty) => {
|
||||
$crate::__f722_flight_application!($board);
|
||||
};
|
||||
}
|
||||
|
||||
/// Link anchor for the platform-owned F722 board-I/O application.
|
||||
#[doc(hidden)]
|
||||
pub fn firmware_anchor() {}
|
||||
@@ -1,415 +1,132 @@
|
||||
//! Source checks only for hardware boundaries that host execution cannot probe.
|
||||
use std::{fs, path::PathBuf};
|
||||
|
||||
use platform_stm32f7::{
|
||||
AccelerometerRange, DigitalLowPassFilter, F722Hardware, F722ImmutableConfig, GyroscopeRange,
|
||||
ImuMount, Mavlink, Mpu6000, Mpu6000Config, Pa4, Pb0Pb1Pb4Pb5Low, Pd2ActiveLow, VerticalSensor,
|
||||
};
|
||||
|
||||
fn platform_source(relative: &str) -> String {
|
||||
let root = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
fs::read_to_string(root.join(relative)).expect("platform source must exist")
|
||||
fn source(relative: &str) -> String {
|
||||
fs::read_to_string(PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(relative)).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn former_motor_pins_are_inhibited_before_all_other_initialization() {
|
||||
let module = platform_source("src/f722/mod.rs");
|
||||
let interlock = module
|
||||
.find("motor_inhibit::inhibit_pb0_pb1_pb4_pb5")
|
||||
.expect("F722 initialization must claim the former motor pins first");
|
||||
let validation = module
|
||||
.find("validate_board_config")
|
||||
.expect("F722 initialization must validate its board configuration");
|
||||
let board_initialization = module
|
||||
.find("board::initialize")
|
||||
.expect("F722 initialization must subsequently claim telemetry I/O");
|
||||
|
||||
assert!(interlock < validation);
|
||||
assert!(validation < board_initialization);
|
||||
|
||||
let interlock = platform_source("src/f722/motor_inhibit.rs");
|
||||
let reset = interlock
|
||||
.find("port.bsrr()")
|
||||
.expect("interlock must reset the former motor-pin output latches");
|
||||
let clock_delay = interlock
|
||||
.find("rcc.ahb1enr().read()")
|
||||
.expect("GPIO access must follow the documented RCC synchronization delay");
|
||||
let output_mode = interlock
|
||||
.find("port.moder()")
|
||||
.expect("interlock must configure the former motor pins as GPIO outputs");
|
||||
fn motor_pins_are_inhibited_before_initialization() {
|
||||
let module = source("src/f722/mod.rs");
|
||||
let interlock = module.find("motor_inhibit::inhibit(").unwrap();
|
||||
let validation = module.find("board::validate_board_config").unwrap();
|
||||
let initialize = module.find("board::initialize").unwrap();
|
||||
assert!(interlock < validation && validation < initialize);
|
||||
let interlock = source("src/f722/motor_inhibit.rs");
|
||||
assert!(
|
||||
clock_delay < reset && reset < output_mode,
|
||||
"output latches must be low before GPIO output mode is selected"
|
||||
interlock.find("rcc.ahb1enr().read()").unwrap() < interlock.find("port.bsrr()").unwrap()
|
||||
);
|
||||
for pin in ["br0", "br1", "br4", "br5"] {
|
||||
assert!(interlock.contains(pin), "interlock must reset PB pin {pin}");
|
||||
}
|
||||
for pin in ["moder0", "moder1", "moder4", "moder5"] {
|
||||
assert!(
|
||||
interlock.contains(pin),
|
||||
"interlock must configure PB pin {pin}"
|
||||
);
|
||||
}
|
||||
for pin in ["ot0", "ot1", "ot4", "ot5"] {
|
||||
assert!(
|
||||
interlock.contains(pin),
|
||||
"interlock must set PB pin {pin} push-pull"
|
||||
);
|
||||
}
|
||||
for pin in ["ospeedr0", "ospeedr1", "ospeedr4", "ospeedr5"] {
|
||||
assert!(
|
||||
interlock.contains(pin),
|
||||
"interlock must set PB pin {pin} low speed"
|
||||
);
|
||||
}
|
||||
assert!(interlock.find("port.bsrr()").unwrap() < interlock.find("port.moder()").unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn immutable_board_policy_is_one_complete_literal_manifest() {
|
||||
const IMMUTABLE: F722ImmutableConfig = F722ImmutableConfig {
|
||||
name: "test",
|
||||
service_hz: 100,
|
||||
hardware: F722Hardware::new()
|
||||
.output_inhibit(Pb0Pb1Pb4Pb5Low)
|
||||
.imu(
|
||||
Mpu6000::on_spi1()
|
||||
.with_drdy(Pa4)
|
||||
.mounted(ImuMount::BodyAligned)
|
||||
.configured(Mpu6000Config {
|
||||
accelerometer: AccelerometerRange::PlusMinus4G,
|
||||
gyroscope: GyroscopeRange::PlusMinus500DegreesPerSecond,
|
||||
digital_low_pass_filter: DigitalLowPassFilter::Bandwidth42Hz,
|
||||
sample_rate_divider: 9,
|
||||
}),
|
||||
)
|
||||
.mavlink(Mavlink::on_usart1().baud(115_200))
|
||||
.vertical_sensor(VerticalSensor::mtf01_on_uart4().baud(115_200))
|
||||
.status_led(Pd2ActiveLow)
|
||||
.telemetry_only(),
|
||||
};
|
||||
|
||||
assert_eq!(IMMUTABLE.name, "test");
|
||||
assert_eq!(IMMUTABLE.service_hz, 100);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn telemetry_platform_has_no_physical_actuator_peripheral_path() {
|
||||
fn board_images_have_no_actuator_route() {
|
||||
let sources = [
|
||||
platform_source("src/lib.rs"),
|
||||
platform_source("src/f722/mod.rs"),
|
||||
platform_source("src/f722/board.rs"),
|
||||
platform_source("src/f722/firmware.rs"),
|
||||
platform_source("src/f722/interrupts.rs"),
|
||||
"src/lib.rs",
|
||||
"src/f722/mod.rs",
|
||||
"src/f722/board.rs",
|
||||
"src/f722/firmware.rs",
|
||||
"src/f722/interrupts.rs",
|
||||
]
|
||||
.map(source)
|
||||
.join("\n");
|
||||
|
||||
for forbidden in [
|
||||
"mod actuator",
|
||||
"MotorOutput",
|
||||
"MotorRoute",
|
||||
"MotorOutputs",
|
||||
"TIM3",
|
||||
"TIM5",
|
||||
"Dshot",
|
||||
"Pwm",
|
||||
"device.TIM3",
|
||||
"device.TIM5",
|
||||
] {
|
||||
assert!(
|
||||
!sources.contains(forbidden),
|
||||
"telemetry platform must not expose actuator path {forbidden}"
|
||||
);
|
||||
assert!(!sources.contains(forbidden), "{forbidden}");
|
||||
}
|
||||
|
||||
let manifest = platform_source("Cargo.toml");
|
||||
assert!(
|
||||
!manifest.contains("kaptan-actuator"),
|
||||
"telemetry platform must not link the physical actuator crate"
|
||||
);
|
||||
assert!(!source("Cargo.toml").contains("kaptan-actuator"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ordinary_f722_image_has_direct_configuration_after_output_inhibition() {
|
||||
let board = platform_source("src/f722/board.rs");
|
||||
let firmware = platform_source("src/f722/firmware.rs");
|
||||
let routes = platform_source("src/f722/routes.rs");
|
||||
let manifest = platform_source("Cargo.toml");
|
||||
|
||||
assert!(
|
||||
!manifest.contains("development-config")
|
||||
&& !manifest.contains("development-authority")
|
||||
&& !manifest.contains("development-unsigned"),
|
||||
"ordinary board images must not hide configuration behind a development feature"
|
||||
);
|
||||
for required in [
|
||||
"config_file: F722ConfigFile",
|
||||
"file_service: FileService",
|
||||
"uart_receiver: IntegrityMavlinkReceiver<1>",
|
||||
"file_lease_ticks: u16",
|
||||
"TRANSFER_REPLAY_LEASE_SECONDS",
|
||||
"let Some(message) = frame.decode()",
|
||||
"wire::decode_request",
|
||||
".dispatch(&mut self.config_file",
|
||||
"send_file_response",
|
||||
"let class = kaptan_mavlink::transmitter::FrameClass::Critical",
|
||||
"control.status != FileStatus::RequestConflict",
|
||||
fn uart_runtime_has_no_dma_or_static_register_aliases() {
|
||||
for path in [
|
||||
"src/f722/serial.rs",
|
||||
"src/f722/peripherals/usart.rs",
|
||||
"src/f722/data_channel.rs",
|
||||
] {
|
||||
assert!(
|
||||
board.contains(required),
|
||||
"ordinary image must retain configuration composition: {required}"
|
||||
);
|
||||
}
|
||||
assert!(!board.contains("DirectConfigAdmission"));
|
||||
assert!(
|
||||
routes.contains("pub(crate) struct Mpu6000Spi1Route {\n spi: Spi1,\n}")
|
||||
&& routes.contains("stm32f722::GPIOB::ptr()")
|
||||
&& routes.contains("write.br2().set_bit()")
|
||||
&& routes.contains("write.bs2().set_bit()"),
|
||||
"the MPU runtime may drive only PB2 atomically, not retain GPIOB ownership"
|
||||
);
|
||||
assert!(!board.contains("development-config"));
|
||||
assert!(
|
||||
!firmware.contains("shared = [mavlink, motors]"),
|
||||
"the MAVLink worker must not share a motor resource"
|
||||
);
|
||||
assert!(
|
||||
firmware.contains("const MAVLINK_EVENT_RECEIVED")
|
||||
&& firmware.contains("mavlink.receive_mavlink_interrupt(event)")
|
||||
&& !firmware.contains("development-config"),
|
||||
"ordinary images must always dispatch received MAVLink frames"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flash_busy_deadline_resets_entirely_from_sram() {
|
||||
let config = platform_source("src/f722/config.rs");
|
||||
assert!(config.contains("link_section = \".data.kaptan_flash_operation\""));
|
||||
assert!(config.contains("fn execute(&self, operation: F722FlashOperation)"));
|
||||
assert!(config.contains("\"mrs {primask}, PRIMASK\""));
|
||||
assert!(config.contains("\"cpsid i\""));
|
||||
assert!(config.contains("\"cpsie i\""));
|
||||
assert!(config.contains("fn reset_after_flash_timeout() -> !"));
|
||||
assert!(config.contains("SYSRESETREQ"));
|
||||
assert!(config.contains("core::arch::asm!(\"dsb sy\""));
|
||||
assert!(!config.contains("BusyTimeout"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn usart_tx_dma_arms_each_stream_before_its_request() {
|
||||
let usart = platform_source("src/f722/peripherals/usart.rs");
|
||||
assert!(usart.contains("disable_dma_transmit_from_interrupt"));
|
||||
|
||||
for (name, path) in [
|
||||
("USART1", "src/f722/peripherals/usart1_tx_dma2_stream7.rs"),
|
||||
("USART2", "src/f722/peripherals/usart2_tx_dma1_stream6.rs"),
|
||||
] {
|
||||
let route = platform_source(path);
|
||||
let constructor_start = route.find("pub fn new").expect("DMA constructor");
|
||||
let transfer_start = route.find("pub fn start").expect("DMA start operation");
|
||||
let acknowledge_start = route
|
||||
.find("pub fn acknowledge_interrupt")
|
||||
.expect("DMA interrupt acknowledgement");
|
||||
|
||||
assert!(
|
||||
!route[constructor_start..transfer_start].contains("enable_dma_transmit"),
|
||||
"{name} must not leave its DMA request armed between transfers"
|
||||
);
|
||||
|
||||
assert!(
|
||||
route.contains("write.dmdis().enabled().feie().disabled()"),
|
||||
"{name} must explicitly select direct mode without FE interrupts"
|
||||
);
|
||||
let start = &route[transfer_start..acknowledge_start];
|
||||
let stream_enable = start
|
||||
.find("write.en().enabled()")
|
||||
.expect("DMA stream must be enabled");
|
||||
let request_enable = start
|
||||
.find("self.usart.enable_dma_transmit()")
|
||||
.expect("USART DMA request must be enabled");
|
||||
assert!(
|
||||
stream_enable < request_enable,
|
||||
"{name} must enable its stream before the peripheral request"
|
||||
);
|
||||
|
||||
let acknowledge = &route[acknowledge_start..];
|
||||
let stream_disable = acknowledge
|
||||
.find("write.en().disabled()")
|
||||
.expect("DMA stream must be disabled");
|
||||
let request_disable = acknowledge
|
||||
.find("disable_dma_transmit_from_interrupt()")
|
||||
.expect("USART DMA request must be disabled");
|
||||
assert!(
|
||||
stream_disable < request_disable,
|
||||
"{name} must disable its stream before the peripheral request"
|
||||
);
|
||||
|
||||
let classifier = acknowledge
|
||||
.find("impl DmaTerminalStatus")
|
||||
.map(|index| &acknowledge[index..])
|
||||
.expect("DMA terminal classifier");
|
||||
assert!(classifier.contains("self.transfer_error || self.direct_mode_error"));
|
||||
assert!(classifier.contains("TxCompletion::Complete"));
|
||||
assert!(
|
||||
!classifier.contains("fifo_error"),
|
||||
"{name} direct-mode FEIF must not manufacture a terminal fault"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn configuration_schema_is_generated_without_board_forwarding_code() {
|
||||
let platform = platform_source("src/lib.rs");
|
||||
let platform_config = platform_source("src/f722/config.rs");
|
||||
let platform_firmware = platform_source("src/f722/firmware.rs");
|
||||
let kakute = platform_source("../../boards/kakute-f7-mini-v3/src/board.rs");
|
||||
let geprc = platform_source("../../boards/geprc-f722-v2/src/board.rs");
|
||||
|
||||
assert!(
|
||||
platform.contains("pub struct F722ImmutableConfig")
|
||||
&& platform.contains("pub hardware: F722Hardware")
|
||||
&& platform.contains("type UserConfig: Clone + Config")
|
||||
&& platform_config.contains("struct UserConfigModel")
|
||||
&& platform_config.contains("fn project_runtime<C, I>")
|
||||
&& platform_config.contains("candidate.field_value(field)"),
|
||||
"the platform must retain exactly one immutable manifest and one generic user adapter"
|
||||
);
|
||||
for semantic_id in [
|
||||
"MAVLINK_SYSTEM_ID",
|
||||
"MAVLINK_COMPONENT_ID",
|
||||
"IMU_HZ",
|
||||
"HEARTBEAT_HZ",
|
||||
"MAVLINK_LOG_LEVEL_FILTER",
|
||||
"RANGE_HZ",
|
||||
"OPTICAL_FLOW_HZ",
|
||||
] {
|
||||
assert!(
|
||||
platform_config.contains(&format!("const {semantic_id}: FieldId")),
|
||||
"missing stable platform-consumed semantic ID {semantic_id}"
|
||||
);
|
||||
}
|
||||
for (name, board) in [("Kakute", kakute), ("GEPRC", geprc)] {
|
||||
assert!(
|
||||
board.contains("type UserConfig = UserConfig;")
|
||||
&& board.contains("type ImmutableConfig = ImmutableConfig;")
|
||||
&& board.contains("const FACTORY_USER_CONFIG: UserConfig = UserConfig {")
|
||||
&& board.contains("const IMMUTABLE_CONFIG: ImmutableConfig = ImmutableConfig {")
|
||||
&& board.contains("hardware: F722Hardware::new()")
|
||||
&& board.contains(".telemetry_only()"),
|
||||
"{name} must visibly declare the two literal roots"
|
||||
);
|
||||
for redundant in [
|
||||
"impl F7ImmutableConfig for",
|
||||
"impl F7UserConfig for",
|
||||
"impl_f722_user_config!",
|
||||
let code = source(path);
|
||||
for forbidden in [
|
||||
"DMA1::ptr",
|
||||
"DMA2::ptr",
|
||||
"USART1::ptr",
|
||||
"USART2::ptr",
|
||||
"USART3::ptr",
|
||||
"UART4::ptr",
|
||||
"USART6::ptr",
|
||||
"DmaReadableBuffer",
|
||||
"MavlinkTransmitter",
|
||||
"dmat()",
|
||||
] {
|
||||
assert!(!board.contains(redundant), "{name} retains {redundant}");
|
||||
assert!(!code.contains(forbidden), "{path}: {forbidden}");
|
||||
}
|
||||
}
|
||||
assert!(
|
||||
platform_firmware.contains("<$board as $crate::F7Board>::UserConfig")
|
||||
&& platform_firmware.contains("<$board as $crate::F7Board>::ImmutableConfig")
|
||||
&& platform_firmware.contains("config_snapshot_length::<"),
|
||||
"the common RTIC graph must specialize storage for the two board roots"
|
||||
);
|
||||
let firmware = source("src/f722/firmware.rs");
|
||||
assert!(!firmware.contains("DMA2_STREAM7"));
|
||||
assert!(!firmware.contains("DMA1_STREAM6"));
|
||||
assert_eq!(firmware.matches("imu_worker::spawn()").count(), 1);
|
||||
assert_eq!(firmware.matches("mavlink_worker::spawn()").count(), 1);
|
||||
}
|
||||
#[test]
|
||||
fn source_keeps_atomic_chip_select_without_gpio_owner() {
|
||||
let routes = source("src/f722/routes.rs");
|
||||
let compact: String = routes.split_whitespace().collect();
|
||||
assert!(compact.contains("pub(crate)structMpu6000Spi1Route{spi:Spi1,}"));
|
||||
for bit in ["br2", "bs2", "br15", "bs15"] {
|
||||
assert!(routes.contains(&format!("write.{bit}().set_bit()")));
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn source_keeps_flash_irq_mask_and_reset() {
|
||||
let config = source("src/f722/config.rs");
|
||||
for required in [
|
||||
"link_section = \".data.kaptan_flash_operation\"",
|
||||
"\"mrs {primask}, PRIMASK\"",
|
||||
"\"cpsid i\"",
|
||||
"\"cpsie i\"",
|
||||
"fn reset_after_flash_timeout() -> !",
|
||||
"SYSRESETREQ",
|
||||
"core::arch::asm!(\"dsb sy\"",
|
||||
] {
|
||||
assert!(config.contains(required), "{required}");
|
||||
}
|
||||
assert!(!config.contains("BusyTimeout"));
|
||||
}
|
||||
#[test]
|
||||
fn normal_images_keep_usb_configuration_and_shared_file_service() {
|
||||
let manifest = source("Cargo.toml");
|
||||
assert!(!manifest.contains("development-config"));
|
||||
let board = source("src/f722/board.rs");
|
||||
assert_eq!(board.matches("FileService::new").count(), 1);
|
||||
assert!(board.contains("usb_mavlink("));
|
||||
assert!(board.contains("IngressId(id)"));
|
||||
assert!(board.contains("result.refresh_lease"));
|
||||
assert!(!board.contains("file_lease_ticks"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn f722_link_budget_reserves_only_deterministic_telemetry() {
|
||||
let board = platform_source("src/f722/board.rs");
|
||||
assert!(board.contains("SCHEDULED_TELEMETRY_BYTES_PER_SECOND"));
|
||||
assert!(board.contains("SERVICE_HZ * SCHEDULED_FRAME_MAXIMUM_BYTES"));
|
||||
assert!(board.contains("self.drain_one_mavlink_diagnostic();"));
|
||||
assert!(board.contains("let mavlink_log_filter = runtime.mavlink_log_level_filter;"));
|
||||
assert!(board.contains("LogSinkQueue::new(mavlink_log_filter)"));
|
||||
assert!(!board.contains("mavlink_logging_enabled"));
|
||||
assert!(!board.contains("LogFilter"));
|
||||
for periodic_diagnostic_policy in [
|
||||
"ERROR_RECORDS_PER_SECOND",
|
||||
"LOG_RECORDS_PER_SECOND",
|
||||
"WORST_CASE_OBSERVABILITY_BYTES_PER_SECOND",
|
||||
"ERROR_FRAME_MAXIMUM_BYTES",
|
||||
"LOG_FRAME_MAXIMUM_BYTES",
|
||||
"budget_ticks",
|
||||
"error_budget",
|
||||
"log_budget",
|
||||
] {
|
||||
assert!(
|
||||
!board.contains(periodic_diagnostic_policy),
|
||||
"diagnostics must not have a periodic policy: {periodic_diagnostic_policy}"
|
||||
);
|
||||
fn source_keeps_protocol_local_and_irq_priorities() {
|
||||
let firmware = source("src/f722/firmware.rs");
|
||||
let shared =
|
||||
&firmware[firmware.find("struct Shared").unwrap()..firmware.find("#[local]").unwrap()];
|
||||
assert!(!shared.contains("mavlink:"));
|
||||
for owner in ["uarts:", "usb:", "serial_clock:"] {
|
||||
assert!(shared.contains(owner));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mavlink_transport_faults_do_not_recurse_or_retain_dead_accounting() {
|
||||
let board = platform_source("src/f722/board.rs");
|
||||
let transport = platform_source("src/f722/data_channel.rs");
|
||||
let portable_channel = platform_source("../../libs/kaptan-data-channel/src/lib.rs");
|
||||
|
||||
assert!(
|
||||
!board.contains("mavlink transmit dma fault"),
|
||||
"the MAVLink sink must not report its own transmit failure through itself"
|
||||
);
|
||||
|
||||
for recursive_path in [
|
||||
"drain_transport_faults",
|
||||
"take_transport_fault",
|
||||
"TransmitMemory",
|
||||
"TransmitEngine",
|
||||
"TransmitTerminal",
|
||||
"pending_faults",
|
||||
] {
|
||||
assert!(
|
||||
!board.contains(recursive_path)
|
||||
&& !transport.contains(recursive_path)
|
||||
&& !portable_channel.contains(recursive_path),
|
||||
"MAVLink transport must not feed {recursive_path} back into itself"
|
||||
);
|
||||
assert!(firmware.contains("context.local.mavlink.process_worker_events"));
|
||||
assert!(!firmware.contains(".lock(|mavlink|"));
|
||||
assert!(firmware.contains("priority = 7, local = [mavlink]"));
|
||||
for vector in ["USART1", "USART2", "USART3", "UART4", "USART6"] {
|
||||
assert!(firmware.contains(&format!("binds = {vector}, priority = 14")));
|
||||
}
|
||||
for dead_counter in [
|
||||
"dma_fault_count",
|
||||
"memory_fault_count",
|
||||
"engine_fault_count",
|
||||
"terminal_fault_count",
|
||||
] {
|
||||
assert!(
|
||||
!transport.contains(dead_counter),
|
||||
"unused transport accounting must stay absent: {dead_counter}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn f722_producers_keep_logs_and_reportable_errors_separate() {
|
||||
let board = platform_source("src/f722/board.rs");
|
||||
let config = platform_source("src/f722/config.rs");
|
||||
let logger = platform_source("../../libs/kaptan-log/src/facade.rs");
|
||||
let errors = platform_source("../../libs/kaptan-err-reportable/src/ingress.rs");
|
||||
|
||||
for macro_name in [
|
||||
"kaptan_log::debug!",
|
||||
"kaptan_log::info!",
|
||||
"kaptan_log::warn!",
|
||||
"kaptan_log::error!",
|
||||
] {
|
||||
assert!(
|
||||
board.contains(macro_name),
|
||||
"f722 does not exercise {macro_name}"
|
||||
);
|
||||
}
|
||||
assert!(board.contains("#[kaptan_err_reportable::propagate]"));
|
||||
assert!(
|
||||
config.contains("enum PersistConfigurationError {")
|
||||
&& config.contains("#[kaptan_error(id = 2210)]")
|
||||
&& config.contains("#[kaptan_error(id = 2209)]")
|
||||
&& config.contains("impl From<JournalError<F722FlashError>>")
|
||||
&& config.contains("fn persist_candidate(")
|
||||
&& config.contains("#[kaptan_err_reportable::propagate]")
|
||||
);
|
||||
assert!(!board.contains("report_error"));
|
||||
assert!(!board.contains("report_static_error"));
|
||||
assert!(logger.contains("GLOBAL_INGRESS_CAPACITY"));
|
||||
assert!(logger.contains("compare_exchange"));
|
||||
assert!(errors.contains("AtomicU8"));
|
||||
assert!(errors.contains("UnsafeCell<MaybeUninit<ErrorRecord>>"));
|
||||
assert!(errors.contains("compare_exchange"));
|
||||
assert!(firmware.contains("binds = OTG_FS, priority = 8, shared = [usb]"));
|
||||
assert!(firmware.contains("binds = TIM2, priority = 10"));
|
||||
assert!(firmware.contains("local = [timer], shared = [lidar]"));
|
||||
let timer_irq = &firmware
|
||||
[firmware.find("fn tim2(").unwrap()..firmware.find("async fn imu_worker").unwrap()];
|
||||
assert!(timer_irq.contains("if !context.local.timer.on_interrupt()"));
|
||||
assert!(timer_irq.find(".on_interrupt()").unwrap() < timer_irq.find(".lidar.lock").unwrap());
|
||||
}
|
||||
+31
-41
@@ -1,15 +1,11 @@
|
||||
#![allow(dead_code)]
|
||||
|
||||
use kaptan_config::{Config, ConfigManager, ConfigModel, ConfigStore, ConfigValue, FieldId};
|
||||
use platform_stm32f7::{
|
||||
F7ImmutableConfig, F722Hardware, F722ImmutableConfig, ImuMount, LevelFilter, Mavlink, Mpu6000,
|
||||
Mpu6000Config, Pa4, Pb0Pb1Pb4Pb5Low, Pd2ActiveLow, VerticalSensor,
|
||||
F722Hardware, ImuMount, LevelFilter, MotorPins, Mpu6000Config, Mpu6000Spi1, SerialInventory,
|
||||
StatusLed, VerticalSensor,
|
||||
};
|
||||
|
||||
// Include the implementation as a child module so this platform-internal test
|
||||
// can exercise the crate-private user-candidate adapter without flash hardware.
|
||||
#[path = "../src/f722/config.rs"]
|
||||
mod target_config;
|
||||
type F722ImmutableConfig = platform_stm32f7::F722ImmutableConfig<Mpu6000Spi1>;
|
||||
use crate::f722::config as target_config;
|
||||
|
||||
use target_config::UserConfigModel;
|
||||
|
||||
@@ -67,23 +63,21 @@ struct TestUserConfig {
|
||||
const IMMUTABLE: F722ImmutableConfig = F722ImmutableConfig {
|
||||
name: "active-candidate-test",
|
||||
service_hz: 100,
|
||||
hardware: F722Hardware::new()
|
||||
.output_inhibit(Pb0Pb1Pb4Pb5Low)
|
||||
.imu(
|
||||
Mpu6000::on_spi1()
|
||||
.with_drdy(Pa4)
|
||||
.mounted(ImuMount::BodyAligned)
|
||||
.configured(Mpu6000Config {
|
||||
accelerometer: platform_stm32f7::AccelerometerRange::PlusMinus4G,
|
||||
gyroscope: platform_stm32f7::GyroscopeRange::PlusMinus500DegreesPerSecond,
|
||||
digital_low_pass_filter: platform_stm32f7::DigitalLowPassFilter::Bandwidth42Hz,
|
||||
sample_rate_divider: 9,
|
||||
}),
|
||||
)
|
||||
.mavlink(Mavlink::on_usart1().baud(115_200))
|
||||
.vertical_sensor(VerticalSensor::none())
|
||||
.status_led(Pd2ActiveLow)
|
||||
.telemetry_only(),
|
||||
hardware: F722Hardware::new(
|
||||
Mpu6000Spi1 {
|
||||
mount: ImuMount::BodyAligned,
|
||||
config: Mpu6000Config {
|
||||
accelerometer: platform_stm32f7::AccelerometerRange::PlusMinus4G,
|
||||
gyroscope: platform_stm32f7::GyroscopeRange::PlusMinus500DegreesPerSecond,
|
||||
digital_low_pass_filter: platform_stm32f7::DigitalLowPassFilter::Bandwidth42Hz,
|
||||
sample_rate_divider: 9,
|
||||
},
|
||||
},
|
||||
SerialInventory::new(&[]),
|
||||
StatusLed::ActiveLowPd2,
|
||||
VerticalSensor::None,
|
||||
MotorPins::Four,
|
||||
),
|
||||
};
|
||||
|
||||
struct OneRecord<C>(Option<C>);
|
||||
@@ -158,13 +152,13 @@ fn mavlink_logging_policy_is_projected_and_validated_independently() {
|
||||
9,
|
||||
"runtime projection must retain narrow board-native scalar widths"
|
||||
);
|
||||
let mut candidate = UserConfigModel::<TestUserConfig, F722ImmutableConfig>::new(factory());
|
||||
let mut candidate = UserConfigModel::<TestUserConfig, F722ImmutableConfig, 0>::new(factory());
|
||||
candidate
|
||||
.set_field(FieldId::new(105), ConfigValue::U8(0))
|
||||
.unwrap();
|
||||
|
||||
let active = candidate.validate(&IMMUTABLE).unwrap();
|
||||
let runtime = target_config::project_runtime(&active, &IMMUTABLE).unwrap();
|
||||
let runtime = target_config::project_runtime(&active.candidate, &IMMUTABLE).unwrap();
|
||||
assert_eq!(runtime.mavlink_log_level_filter, LevelFilter::Off);
|
||||
}
|
||||
|
||||
@@ -188,43 +182,39 @@ fn runtime_projection_rejects_missing_and_wrong_kind_values() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runtime_construction_reads_the_persisted_board_owned_user_root() {
|
||||
let factory = UserConfigModel::<TestUserConfig, F722ImmutableConfig>::new(factory());
|
||||
fn active_projection_retains_the_persisted_board_owned_user_root() {
|
||||
let factory = UserConfigModel::<TestUserConfig, F722ImmutableConfig, 0>::new(factory());
|
||||
let mut persisted = factory.clone();
|
||||
persisted
|
||||
.set_field(FieldId::new(1000), ConfigValue::U8(42))
|
||||
.expect("driver field must accept a valid persisted value");
|
||||
|
||||
let manager = ConfigManager::recover_compatible_or_factory(
|
||||
factory,
|
||||
IMMUTABLE,
|
||||
OneRecord(Some(persisted)),
|
||||
)
|
||||
.expect("synthetic persisted user configuration must recover");
|
||||
let manager = ConfigManager::recover(factory, IMMUTABLE, OneRecord(Some(persisted)))
|
||||
.expect("synthetic persisted user configuration must recover");
|
||||
|
||||
assert_eq!(
|
||||
manager.active().field_value(FieldId::new(1000)),
|
||||
manager.active().candidate.field_value(FieldId::new(1000)),
|
||||
Some(ConfigValue::U8(42)),
|
||||
"runtime construction must receive the validated persisted field"
|
||||
"the active projection must retain the validated persisted field"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn committing_a_user_edit_does_not_mutate_the_boot_active_view() {
|
||||
let factory = UserConfigModel::<TestUserConfig, F722ImmutableConfig>::new(factory());
|
||||
let factory = UserConfigModel::<TestUserConfig, F722ImmutableConfig, 0>::new(factory());
|
||||
let mut manager = ConfigManager::recover(factory, IMMUTABLE, OneRecord(None)).unwrap();
|
||||
let mut replacement = manager.committed().clone();
|
||||
let mut replacement = manager.committed().unwrap().clone();
|
||||
replacement
|
||||
.set_field(FieldId::new(1000), ConfigValue::U8(42))
|
||||
.unwrap();
|
||||
manager.replace(replacement).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
manager.active().field_value(FieldId::new(1000)),
|
||||
manager.active().candidate.field_value(FieldId::new(1000)),
|
||||
Some(ConfigValue::U8(7))
|
||||
);
|
||||
assert_eq!(
|
||||
manager.committed().field_value(FieldId::new(1000)),
|
||||
manager.committed().unwrap().field_value(FieldId::new(1000)),
|
||||
Some(ConfigValue::U8(42))
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,775 @@
|
||||
use kaptan_config::{
|
||||
ConfigManager, ConfigModel, ConfigStore, ConfigValue, DualSlotJournal, DualSlotNor, FieldId,
|
||||
KcfgExporter, KcfgImporter, KcfgResultDetail, ReplaceError, decode_kcfg, encode_edited_kcfg,
|
||||
};
|
||||
use kaptan_file_mavlink::{
|
||||
Content, ControlRequest, ControlResponse, DATA_BYTES, DataRequest, DispatchResult,
|
||||
FileOperation, FileProvider, FileRequest, FileResponse, FileService, FileStatus, IngressId,
|
||||
Principal, Target, TransferPeer, crc32c, wire,
|
||||
};
|
||||
use kaptan_ports::{PortId, Role};
|
||||
use platform_stm32f7::{F7Board, F7ImmutableConfig};
|
||||
extern crate std;
|
||||
use crate::f722::{config as target_config, geprc, kakute};
|
||||
use std::{cell::RefCell, rc::Rc, vec, vec::Vec};
|
||||
use target_config::{
|
||||
BootDiagnostic, F722ConfigFile, F722FlashError, UserConfigModel, recover_configuration,
|
||||
};
|
||||
|
||||
type Board = kakute::KakuteF7MiniV3;
|
||||
type C = <Board as F7Board>::UserConfig;
|
||||
type I = <Board as F7Board>::ImmutableConfig;
|
||||
const PORT_COUNT: usize = Board::IMMUTABLE_CONFIG.hardware.serial().len();
|
||||
type Model = UserConfigModel<C, I, PORT_COUNT>;
|
||||
const N: usize = platform_stm32f7::config_snapshot_length::<C>();
|
||||
type Store = DualSlotJournal<RamNor, N>;
|
||||
type Manager = ConfigManager<Model, Store>;
|
||||
type Provider = F722ConfigFile<C, I, PORT_COUNT, N, Store>;
|
||||
|
||||
struct FlashState {
|
||||
bytes: [[u8; 2048]; 2],
|
||||
operations: usize,
|
||||
bytes_read: usize,
|
||||
fail_after: Option<usize>,
|
||||
fail_after_commit: bool,
|
||||
fail_reads: bool,
|
||||
}
|
||||
impl Default for FlashState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
bytes: [[0xff; 2048]; 2],
|
||||
operations: 0,
|
||||
bytes_read: 0,
|
||||
fail_after: None,
|
||||
fail_after_commit: false,
|
||||
fail_reads: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
#[derive(Clone, Default)]
|
||||
struct RamNor(Rc<RefCell<FlashState>>);
|
||||
impl RamNor {
|
||||
fn operation(state: &mut FlashState) -> Result<(), F722FlashError> {
|
||||
if state.fail_after == Some(state.operations) {
|
||||
return Err(F722FlashError::Controller);
|
||||
}
|
||||
state.operations += 1;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
impl DualSlotNor for RamNor {
|
||||
type Error = F722FlashError;
|
||||
fn slot_size(&self) -> usize {
|
||||
2048
|
||||
}
|
||||
fn read(&mut self, slot: usize, offset: usize, output: &mut [u8]) -> Result<(), Self::Error> {
|
||||
let mut state = self.0.borrow_mut();
|
||||
if state.fail_reads {
|
||||
return Err(F722FlashError::Controller);
|
||||
}
|
||||
state.bytes_read += output.len();
|
||||
output.copy_from_slice(&state.bytes[slot][offset..offset + output.len()]);
|
||||
Ok(())
|
||||
}
|
||||
fn erase_slot(&mut self, slot: usize) -> Result<(), Self::Error> {
|
||||
let mut state = self.0.borrow_mut();
|
||||
Self::operation(&mut state)?;
|
||||
state.bytes[slot].fill(0xff);
|
||||
Ok(())
|
||||
}
|
||||
fn program_word(&mut self, slot: usize, offset: usize, word: u32) -> Result<(), Self::Error> {
|
||||
let mut state = self.0.borrow_mut();
|
||||
Self::operation(&mut state)?;
|
||||
for (byte, value) in state.bytes[slot][offset..offset + 4]
|
||||
.iter_mut()
|
||||
.zip(word.to_le_bytes())
|
||||
{
|
||||
assert_eq!(*byte & value, value);
|
||||
*byte = value;
|
||||
}
|
||||
// Fault after the durable marker, before journal verification.
|
||||
if state.fail_after_commit && word == u32::from_le_bytes(*b"DONE") {
|
||||
return Err(F722FlashError::Verification);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
fn factory() -> Model {
|
||||
Model::new(Board::FACTORY_USER_CONFIG)
|
||||
}
|
||||
fn recover(flash: &RamNor) -> Manager {
|
||||
recover_configuration(
|
||||
factory(),
|
||||
Board::IMMUTABLE_CONFIG,
|
||||
Store::new(flash.clone()),
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
fn artifact(candidate: &Model) -> Vec<u8> {
|
||||
let factory = factory();
|
||||
let export = KcfgExporter::new(candidate, &factory, false).unwrap();
|
||||
let mut bytes = vec![0; export.length()];
|
||||
assert_eq!(export.read(0, &mut bytes).unwrap(), bytes.len());
|
||||
bytes
|
||||
}
|
||||
fn decode(bytes: &[u8]) -> Model {
|
||||
let factory = factory();
|
||||
let mut importer = KcfgImporter::new(&factory).unwrap();
|
||||
for chunk in bytes.chunks(37) {
|
||||
importer.push(&factory, chunk).unwrap();
|
||||
}
|
||||
importer.finish().unwrap().candidate
|
||||
}
|
||||
const TARGET: Target = Target::new(1, 1);
|
||||
const PEER: TransferPeer = TransferPeer {
|
||||
principal: Principal::new(42, 7),
|
||||
ingress: IngressId(0),
|
||||
};
|
||||
fn control(operation: FileOperation, position: u32, path: &[u8], bytes: &[u8]) -> FileRequest {
|
||||
FileRequest::Control(ControlRequest {
|
||||
operation,
|
||||
target: TARGET,
|
||||
position,
|
||||
file_size: bytes.len() as u32,
|
||||
checksum_crc32c: if bytes.is_empty() { 0 } else { crc32c(bytes) },
|
||||
transfer_id: 9,
|
||||
content: Content::try_new(path).unwrap(),
|
||||
})
|
||||
}
|
||||
fn exchange(
|
||||
service: &mut FileService,
|
||||
provider: &mut impl FileProvider,
|
||||
peer: TransferPeer,
|
||||
request: FileRequest,
|
||||
) -> DispatchResult {
|
||||
let request = wire::decode_request(&wire::encode_request(request)).unwrap();
|
||||
let result = service.dispatch(provider, peer, request).unwrap();
|
||||
assert_eq!(
|
||||
wire::decode_response(&wire::encode_response(result.response)).unwrap(),
|
||||
result.response
|
||||
);
|
||||
result
|
||||
}
|
||||
fn accepted(result: DispatchResult) -> Result<ControlResponse, FileStatus> {
|
||||
let FileResponse::Control(response) = result.response else {
|
||||
panic!("control response expected");
|
||||
};
|
||||
if response.status != FileStatus::Ok {
|
||||
return Err(response.status);
|
||||
}
|
||||
assert!(result.refresh_lease);
|
||||
Ok(response)
|
||||
}
|
||||
fn read_result(provider: &mut impl FileProvider, path: &str) -> Result<Vec<u8>, FileStatus> {
|
||||
let mut service = FileService::new(TARGET);
|
||||
let metadata = accepted(exchange(
|
||||
&mut service,
|
||||
provider,
|
||||
PEER,
|
||||
control(FileOperation::OpenRead, 0, path.as_bytes(), &[]),
|
||||
))?;
|
||||
let mut bytes = vec![0; metadata.file_size as usize];
|
||||
for (i, chunk) in bytes.chunks_mut(DATA_BYTES).enumerate() {
|
||||
let result = exchange(
|
||||
&mut service,
|
||||
provider,
|
||||
PEER,
|
||||
control(FileOperation::Read, (i * DATA_BYTES) as u32, &[], &[]),
|
||||
);
|
||||
assert!(result.refresh_lease);
|
||||
let FileResponse::Data(response) = result.response else {
|
||||
panic!("data response expected");
|
||||
};
|
||||
chunk.copy_from_slice(&response.data[..chunk.len()]);
|
||||
}
|
||||
assert_eq!(crc32c(&bytes), metadata.checksum_crc32c);
|
||||
accepted(exchange(
|
||||
&mut service,
|
||||
provider,
|
||||
PEER,
|
||||
control(FileOperation::Close, bytes.len() as u32, &[], &[]),
|
||||
))?;
|
||||
Ok(bytes)
|
||||
}
|
||||
fn read(provider: &mut impl FileProvider, path: &str) -> Vec<u8> {
|
||||
read_result(provider, path).unwrap()
|
||||
}
|
||||
fn write_to(provider: &mut Provider, path: &[u8], candidate: &Model) -> Result<(), FileStatus> {
|
||||
let base = read_result(provider, "/config/user.kcfg")
|
||||
.or_else(|_| read_result(provider, "/config/active.kcfg"))?;
|
||||
upload(provider, path, &edit(&base, candidate)).map(|_| ())
|
||||
}
|
||||
fn edit(base: &[u8], candidate: &Model) -> Vec<u8> {
|
||||
let source = decode_kcfg(base, &mut [0; kaptan_config::KCFG_FIELD_ID_SCRATCH_BYTES]).unwrap();
|
||||
let mut edited = vec![0; base.len()];
|
||||
encode_edited_kcfg(&source, false, &mut edited, |field| {
|
||||
candidate.field_value(field.id()).unwrap()
|
||||
})
|
||||
.unwrap();
|
||||
edited
|
||||
}
|
||||
fn upload(provider: &mut Provider, path: &[u8], bytes: &[u8]) -> Result<Content, FileStatus> {
|
||||
let mut service = FileService::new(TARGET);
|
||||
accepted(exchange(
|
||||
&mut service,
|
||||
provider,
|
||||
PEER,
|
||||
control(FileOperation::OpenWrite, 0, path, bytes),
|
||||
))?;
|
||||
for (i, chunk) in bytes.chunks(DATA_BYTES).enumerate() {
|
||||
let mut data = [0; DATA_BYTES];
|
||||
data[..chunk.len()].copy_from_slice(chunk);
|
||||
let request = FileRequest::Data(DataRequest {
|
||||
target: TARGET,
|
||||
transfer_id: 9,
|
||||
position: (i * DATA_BYTES) as u32,
|
||||
data,
|
||||
});
|
||||
for id in [1, 2, 4] {
|
||||
let foreign = exchange(
|
||||
&mut service,
|
||||
provider,
|
||||
TransferPeer {
|
||||
ingress: IngressId(id),
|
||||
..PEER
|
||||
},
|
||||
request,
|
||||
);
|
||||
assert!(!foreign.refresh_lease);
|
||||
assert_eq!(accepted(foreign), Err(FileStatus::InvalidTransfer));
|
||||
}
|
||||
let result = exchange(&mut service, provider, PEER, request);
|
||||
accepted(result)?;
|
||||
assert_eq!(
|
||||
exchange(&mut service, provider, PEER, request),
|
||||
result,
|
||||
"retry must not append twice"
|
||||
);
|
||||
}
|
||||
let result = exchange(
|
||||
&mut service,
|
||||
provider,
|
||||
PEER,
|
||||
control(FileOperation::Close, bytes.len() as u32, &[], &[]),
|
||||
);
|
||||
if let FileResponse::Control(response) = result.response
|
||||
&& response.status == FileStatus::RequestConflict
|
||||
{
|
||||
let (_, text) = KcfgResultDetail::decode(response.content.as_bytes()).unwrap();
|
||||
assert!(
|
||||
text.contains("reload"),
|
||||
"conflict must carry a readable KCFG prefix"
|
||||
);
|
||||
}
|
||||
accepted(result).map(|response| response.content)
|
||||
}
|
||||
fn write(provider: &mut Provider, candidate: &Model) -> Result<(), FileStatus> {
|
||||
write_to(provider, b"/config/user.kcfg", candidate)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn both_actual_providers_export_only_their_declared_ports() {
|
||||
fn check<B: F7Board, const PORTS: usize, const BYTES: usize>() {
|
||||
let factory = UserConfigModel::<B::UserConfig, B::ImmutableConfig, PORTS>::new(
|
||||
B::FACTORY_USER_CONFIG,
|
||||
);
|
||||
let manager = recover_configuration(
|
||||
factory.clone(),
|
||||
B::IMMUTABLE_CONFIG,
|
||||
DualSlotJournal::<_, BYTES>::new(RamNor::default()),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(manager.active().ports.iter().len(), PORTS);
|
||||
let mut provider = F722ConfigFile::<_, _, PORTS, BYTES, _>::new(manager);
|
||||
for path in ["/config/user.kcfg", "/config/active.kcfg"] {
|
||||
let bytes = read(&mut provider, path);
|
||||
let mut importer = KcfgImporter::new(&factory).unwrap();
|
||||
importer.push(&factory, &bytes).unwrap();
|
||||
let exported = importer.finish().unwrap().candidate;
|
||||
for port in [
|
||||
platform_stm32f7::UART1,
|
||||
platform_stm32f7::UART2,
|
||||
platform_stm32f7::UART3,
|
||||
platform_stm32f7::UART4,
|
||||
platform_stm32f7::UART6,
|
||||
] {
|
||||
let available = B::IMMUTABLE_CONFIG
|
||||
.f722()
|
||||
.hardware
|
||||
.serial()
|
||||
.index(port.id)
|
||||
.is_some();
|
||||
assert_eq!(exported.field_value(port.role_field).is_some(), available);
|
||||
assert_eq!(exported.field_value(port.baud_field).is_some(), available);
|
||||
}
|
||||
}
|
||||
}
|
||||
check::<kakute::KakuteF7MiniV3, PORT_COUNT, N>();
|
||||
check::<
|
||||
geprc::GeprcF722V2,
|
||||
{ geprc::GeprcF722V2::IMMUTABLE_CONFIG.hardware.serial().len() },
|
||||
{
|
||||
platform_stm32f7::config_snapshot_length::<<geprc::GeprcF722V2 as F7Board>::UserConfig>(
|
||||
)
|
||||
},
|
||||
>();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn actual_board_kcfg_provider_round_trips_and_active_is_read_only_restart_only() {
|
||||
let flash = RamNor::default();
|
||||
let mut provider = Provider::new(recover(&flash));
|
||||
let _initial = read(&mut provider, "/config/active.kcfg");
|
||||
let mut next = decode(&read(&mut provider, "/config/user.kcfg"));
|
||||
next.set_field(FieldId::new(201), ConfigValue::U8(2))
|
||||
.unwrap();
|
||||
next.set_field(FieldId::new(207), ConfigValue::U8(1))
|
||||
.unwrap();
|
||||
write(&mut provider, &next).unwrap();
|
||||
let active = decode(&read(&mut provider, "/config/active.kcfg"));
|
||||
let committed = decode(&read(&mut provider, "/config/user.kcfg"));
|
||||
assert_eq!(
|
||||
active.field_value(FieldId::new(201)),
|
||||
Some(ConfigValue::U8(0))
|
||||
);
|
||||
assert_eq!(
|
||||
active.field_value(FieldId::new(207)),
|
||||
Some(ConfigValue::U8(0))
|
||||
);
|
||||
assert_eq!(
|
||||
committed.field_value(FieldId::new(201)),
|
||||
Some(ConfigValue::U8(2))
|
||||
);
|
||||
assert_eq!(
|
||||
committed.field_value(FieldId::new(207)),
|
||||
Some(ConfigValue::U8(1))
|
||||
);
|
||||
assert_eq!(
|
||||
write_to(&mut provider, b"/config/active.kcfg", &next),
|
||||
Err(FileStatus::NotFound)
|
||||
);
|
||||
let reboot = recover(&flash);
|
||||
assert_eq!(
|
||||
reboot.active().ports.get(PortId::new(4)).unwrap().role,
|
||||
Role::Mavlink
|
||||
);
|
||||
assert_eq!(reboot.diagnostic(), BootDiagnostic::Healthy);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn latest_readable_invalid_candidate_is_retained_for_usb_repair_without_enabling_uarts() {
|
||||
let flash = RamNor::default();
|
||||
let mut store = Store::new(flash.clone());
|
||||
store.persist(&factory()).unwrap();
|
||||
let mut invalid = factory();
|
||||
invalid
|
||||
.set_field(FieldId::new(201), ConfigValue::U8(2))
|
||||
.unwrap();
|
||||
invalid
|
||||
.set_field(FieldId::new(207), ConfigValue::U8(2))
|
||||
.unwrap();
|
||||
let error = invalid.validate(&Board::IMMUTABLE_CONFIG).err().unwrap();
|
||||
assert_eq!(error.primary(), Some(FieldId::new(207)));
|
||||
assert_eq!(error.related_field(), Some(FieldId::new(201)));
|
||||
store.persist(&invalid).unwrap();
|
||||
let manager = recover(&flash);
|
||||
assert!(matches!(
|
||||
manager.diagnostic(),
|
||||
BootDiagnostic::InvalidPersisted(_)
|
||||
));
|
||||
assert!(
|
||||
manager
|
||||
.active()
|
||||
.ports
|
||||
.iter()
|
||||
.all(|(_, p)| p.role == Role::Disabled)
|
||||
);
|
||||
assert_eq!(
|
||||
manager.committed().unwrap().field_value(FieldId::new(201)),
|
||||
Some(ConfigValue::U8(2))
|
||||
);
|
||||
let before = flash.0.borrow().operations;
|
||||
let mut provider = Provider::new(manager);
|
||||
assert_eq!(
|
||||
decode(&read(&mut provider, "/config/user.kcfg")).field_value(FieldId::new(201)),
|
||||
Some(ConfigValue::U8(2))
|
||||
);
|
||||
assert_eq!(
|
||||
flash.0.borrow().operations,
|
||||
before,
|
||||
"boot and inspection never rewrite the candidate"
|
||||
);
|
||||
assert_eq!(
|
||||
write(&mut provider, &invalid),
|
||||
Err(FileStatus::ContentRejected)
|
||||
);
|
||||
assert_eq!(flash.0.borrow().operations, before);
|
||||
let mut repaired = factory();
|
||||
repaired
|
||||
.set_field(FieldId::new(201), ConfigValue::U8(1))
|
||||
.unwrap();
|
||||
write(&mut provider, &repaired).unwrap();
|
||||
let active = decode(&read(&mut provider, "/config/active.kcfg"));
|
||||
for id in [201, 207] {
|
||||
assert_eq!(
|
||||
active.field_value(FieldId::new(id)),
|
||||
Some(ConfigValue::U8(0))
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
recover(&flash)
|
||||
.active()
|
||||
.ports
|
||||
.get(PortId::new(1))
|
||||
.unwrap()
|
||||
.role,
|
||||
Role::Mavlink
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_persist_keeps_active_and_invalidates_committed() {
|
||||
let probe = RamNor::default();
|
||||
let mut manager = recover(&probe);
|
||||
let mut next = factory();
|
||||
next.set_field(FieldId::new(201), ConfigValue::U8(1))
|
||||
.unwrap();
|
||||
manager.replace(next.clone()).unwrap();
|
||||
let operations = probe.0.borrow().operations;
|
||||
for fail_after in 0..operations {
|
||||
let flash = RamNor::default();
|
||||
let mut store = Store::new(flash.clone());
|
||||
store.persist(&factory()).unwrap();
|
||||
let mut manager = recover(&flash);
|
||||
{
|
||||
let mut state = flash.0.borrow_mut();
|
||||
state.operations = 0;
|
||||
state.fail_after = Some(fail_after);
|
||||
}
|
||||
assert!(matches!(
|
||||
manager.replace(next.clone()),
|
||||
Err(ReplaceError::Store(_))
|
||||
));
|
||||
assert!(manager.committed().is_none());
|
||||
assert_eq!(
|
||||
manager.active().ports.get(PortId::new(1)).unwrap().role,
|
||||
Role::Disabled
|
||||
);
|
||||
assert!(matches!(
|
||||
manager.replace(factory()),
|
||||
Err(ReplaceError::DurableStateUnknown)
|
||||
));
|
||||
let mut provider = Provider::new(manager);
|
||||
assert_eq!(
|
||||
read_result(&mut provider, "/config/user.kcfg"),
|
||||
Err(FileStatus::StorageFailure)
|
||||
);
|
||||
assert_eq!(
|
||||
decode(&read(&mut provider, "/config/active.kcfg")).field_value(FieldId::new(201)),
|
||||
Some(ConfigValue::U8(0))
|
||||
);
|
||||
flash.0.borrow_mut().fail_after = None;
|
||||
assert_eq!(
|
||||
recover(&flash)
|
||||
.active()
|
||||
.ports
|
||||
.get(PortId::new(1))
|
||||
.unwrap()
|
||||
.role,
|
||||
Role::Disabled
|
||||
);
|
||||
}
|
||||
let flash = RamNor::default();
|
||||
let mut manager = recover(&flash);
|
||||
flash.0.borrow_mut().fail_after_commit = true;
|
||||
assert!(matches!(manager.replace(next), Err(ReplaceError::Store(_))));
|
||||
assert!(manager.committed().is_none());
|
||||
assert_eq!(
|
||||
manager.active().ports.get(PortId::new(1)).unwrap().role,
|
||||
Role::Disabled
|
||||
);
|
||||
flash.0.borrow_mut().fail_after_commit = false;
|
||||
assert_eq!(
|
||||
recover(&flash)
|
||||
.active()
|
||||
.ports
|
||||
.get(PortId::new(1))
|
||||
.unwrap()
|
||||
.role,
|
||||
Role::Mavlink
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_provider_close_blocks_saves_but_keeps_active_export_available() {
|
||||
let flash = RamNor::default();
|
||||
let mut provider = Provider::new(recover(&flash));
|
||||
let mut next = factory();
|
||||
next.set_field(FieldId::new(201), ConfigValue::U8(1))
|
||||
.unwrap();
|
||||
flash.0.borrow_mut().fail_after = Some(2);
|
||||
assert_eq!(write(&mut provider, &next), Err(FileStatus::StorageFailure));
|
||||
flash.0.borrow_mut().fail_after = None;
|
||||
assert_eq!(
|
||||
write(&mut provider, &factory()),
|
||||
Err(FileStatus::StorageFailure)
|
||||
);
|
||||
assert_eq!(
|
||||
decode(&read(&mut provider, "/config/active.kcfg")).field_value(FieldId::new(201)),
|
||||
Some(ConfigValue::U8(0))
|
||||
);
|
||||
write_to(&mut provider, b"/config/reset.kcfg", &factory()).unwrap();
|
||||
write(&mut provider, &next).unwrap();
|
||||
assert_eq!(
|
||||
decode(&read(&mut provider, "/config/active.kcfg")).field_value(FieldId::new(201)),
|
||||
Some(ConfigValue::U8(0))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn storage_read_failure_is_explicit_and_cannot_activate_factory_uart_defaults() {
|
||||
let flash = RamNor::default();
|
||||
flash.0.borrow_mut().fail_reads = true;
|
||||
let mut manager = recover(&flash);
|
||||
assert_eq!(manager.diagnostic(), BootDiagnostic::StorageUnknown);
|
||||
assert!(
|
||||
manager
|
||||
.active()
|
||||
.ports
|
||||
.iter()
|
||||
.all(|(_, p)| p.role == Role::Disabled)
|
||||
);
|
||||
assert!(manager.committed().is_none());
|
||||
assert!(matches!(
|
||||
manager.replace(factory()),
|
||||
Err(ReplaceError::DurableStateUnknown)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn occupied_but_undecodable_storage_is_not_a_healthy_factory_boot() {
|
||||
let flash = RamNor::default();
|
||||
flash.0.borrow_mut().bytes[1][128] = 0;
|
||||
let manager = recover(&flash);
|
||||
assert_eq!(manager.diagnostic(), BootDiagnostic::StorageUnknown);
|
||||
assert!(manager.committed().is_none());
|
||||
assert!(
|
||||
manager
|
||||
.active()
|
||||
.ports
|
||||
.iter()
|
||||
.all(|(_, p)| p.role == Role::Disabled)
|
||||
);
|
||||
assert_eq!(flash.0.borrow().operations, 0);
|
||||
assert_eq!(flash.0.borrow().bytes[1][128], 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn two_downloaded_drafts_conflict_before_noop_and_fresh_noop_skips_flash() {
|
||||
let flash = RamNor::default();
|
||||
let mut provider = Provider::new(recover(&flash));
|
||||
let a = read(&mut provider, "/config/user.kcfg");
|
||||
let b = read(&mut provider, "/config/user.kcfg");
|
||||
let mut next = decode(&b);
|
||||
next.set_field(FieldId::new(201), ConfigValue::U8(1))
|
||||
.unwrap();
|
||||
upload(&mut provider, b"/config/user.kcfg", &edit(&b, &next)).unwrap();
|
||||
let operations = flash.0.borrow().operations;
|
||||
for stale in [a.clone(), edit(&a, &next)] {
|
||||
assert_eq!(
|
||||
upload(&mut provider, b"/config/user.kcfg", &stale),
|
||||
Err(FileStatus::RequestConflict)
|
||||
);
|
||||
assert_eq!(flash.0.borrow().operations, operations);
|
||||
}
|
||||
let fresh = read(&mut provider, "/config/user.kcfg");
|
||||
let result = upload(&mut provider, b"/config/user.kcfg", &fresh).unwrap();
|
||||
assert!(
|
||||
KcfgResultDetail::decode(result.as_bytes())
|
||||
.unwrap()
|
||||
.0
|
||||
.restart_required()
|
||||
);
|
||||
assert_eq!(flash.0.borrow().operations, operations);
|
||||
assert_eq!(
|
||||
decode(&read(&mut provider, "/config/active.kcfg")).field_value(FieldId::new(201)),
|
||||
Some(ConfigValue::U8(0))
|
||||
);
|
||||
assert_eq!(
|
||||
recover(&flash)
|
||||
.active()
|
||||
.ports
|
||||
.get(PortId::new(1))
|
||||
.unwrap()
|
||||
.role,
|
||||
Role::Mavlink
|
||||
);
|
||||
}
|
||||
|
||||
// Use the codec to produce an incompatible snapshot without duplicating its layout.
|
||||
#[derive(Clone)]
|
||||
struct OldSchema(Model);
|
||||
impl ConfigModel for OldSchema {
|
||||
type Active = <Model as ConfigModel>::Active;
|
||||
type Context = I;
|
||||
const SCHEMA_ID: [u8; 16] = *b"OLD-BOARD-SCHEMA";
|
||||
fn descriptor(index: u16) -> Option<&'static kaptan_config::FieldDescriptor> {
|
||||
Model::descriptor(index)
|
||||
}
|
||||
fn field_count() -> u16 {
|
||||
Model::field_count()
|
||||
}
|
||||
fn field_value(&self, id: FieldId) -> Option<ConfigValue> {
|
||||
self.0.field_value(id)
|
||||
}
|
||||
fn active_field_value(active: &Self::Active, id: FieldId) -> Option<ConfigValue> {
|
||||
Model::active_field_value(active, id)
|
||||
}
|
||||
fn set_field(
|
||||
&mut self,
|
||||
id: FieldId,
|
||||
value: ConfigValue,
|
||||
) -> Result<(), kaptan_config::FieldWriteError> {
|
||||
self.0.set_field(id, value)
|
||||
}
|
||||
fn validate(&self, context: &I) -> Result<Self::Active, kaptan_config::ConfigViolation> {
|
||||
self.0.validate(context)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn old_schema_can_only_be_reset_explicitly_to_exact_factory_from_active_artifact() {
|
||||
let flash = RamNor::default();
|
||||
Store::new(flash.clone())
|
||||
.persist(&OldSchema(factory()))
|
||||
.unwrap();
|
||||
let before = flash.0.borrow().operations;
|
||||
assert!(matches!(
|
||||
ConfigManager::recover(
|
||||
factory(),
|
||||
Board::IMMUTABLE_CONFIG,
|
||||
Store::new(flash.clone())
|
||||
),
|
||||
Err(kaptan_config::StartupError::UnreadablePersisted)
|
||||
));
|
||||
let manager = recover(&flash);
|
||||
assert_eq!(manager.diagnostic(), BootDiagnostic::StorageUnknown);
|
||||
let mut provider = Provider::new(manager);
|
||||
assert_eq!(flash.0.borrow().operations, before);
|
||||
assert_eq!(
|
||||
read_result(&mut provider, "/config/user.kcfg"),
|
||||
Err(FileStatus::StorageFailure)
|
||||
);
|
||||
let active = read(&mut provider, "/config/active.kcfg");
|
||||
let mut nonfactory = factory();
|
||||
nonfactory
|
||||
.set_field(FieldId::new(201), ConfigValue::U8(1))
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
upload(
|
||||
&mut provider,
|
||||
b"/config/reset.kcfg",
|
||||
&edit(&active, &nonfactory)
|
||||
),
|
||||
Err(FileStatus::ContentRejected)
|
||||
);
|
||||
assert_eq!(flash.0.borrow().operations, before);
|
||||
let reset = edit(&active, &factory());
|
||||
assert_eq!(
|
||||
upload(&mut provider, b"/config/user.kcfg", &reset),
|
||||
Err(FileStatus::StorageFailure)
|
||||
);
|
||||
let result = upload(&mut provider, b"/config/reset.kcfg", &reset).unwrap();
|
||||
assert!(
|
||||
!KcfgResultDetail::decode(result.as_bytes())
|
||||
.unwrap()
|
||||
.0
|
||||
.restart_required()
|
||||
);
|
||||
assert!(flash.0.borrow().operations > before);
|
||||
assert_eq!(
|
||||
artifact(&decode(&read(&mut provider, "/config/active.kcfg"))),
|
||||
artifact(&decode(&active))
|
||||
);
|
||||
assert_eq!(
|
||||
decode(&read(&mut provider, "/config/user.kcfg")).field_value(FieldId::new(201)),
|
||||
Some(ConfigValue::U8(0))
|
||||
);
|
||||
let reboot = recover(&flash);
|
||||
assert_eq!(reboot.diagnostic(), BootDiagnostic::Healthy);
|
||||
assert_eq!(
|
||||
reboot.active().ports.get(PortId::new(1)).unwrap().role,
|
||||
Role::Disabled
|
||||
);
|
||||
assert!(!reboot.restart_required());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_failures_remain_unknown_and_explicit_retry_can_recover_after_durable_marker() {
|
||||
for after_commit in [false, true] {
|
||||
let flash = RamNor::default();
|
||||
Store::new(flash.clone())
|
||||
.persist(&OldSchema(factory()))
|
||||
.unwrap();
|
||||
let mut provider = Provider::new(recover(&flash));
|
||||
let active = read(&mut provider, "/config/active.kcfg");
|
||||
let reset = edit(&active, &factory());
|
||||
{
|
||||
let mut state = flash.0.borrow_mut();
|
||||
state.fail_after_commit = after_commit;
|
||||
if !after_commit {
|
||||
state.fail_after = Some(state.operations + 2);
|
||||
}
|
||||
}
|
||||
assert_eq!(
|
||||
upload(&mut provider, b"/config/reset.kcfg", &reset),
|
||||
Err(FileStatus::StorageFailure)
|
||||
);
|
||||
assert_eq!(
|
||||
read_result(&mut provider, "/config/user.kcfg"),
|
||||
Err(FileStatus::StorageFailure)
|
||||
);
|
||||
assert_eq!(read(&mut provider, "/config/active.kcfg"), active);
|
||||
{
|
||||
let mut state = flash.0.borrow_mut();
|
||||
state.fail_after_commit = false;
|
||||
state.fail_after = None;
|
||||
}
|
||||
assert_eq!(
|
||||
upload(&mut provider, b"/config/user.kcfg", &reset),
|
||||
Err(FileStatus::StorageFailure)
|
||||
);
|
||||
let result = upload(&mut provider, b"/config/reset.kcfg", &reset).unwrap();
|
||||
assert!(
|
||||
!KcfgResultDetail::decode(result.as_bytes())
|
||||
.unwrap()
|
||||
.0
|
||||
.restart_required()
|
||||
);
|
||||
assert_eq!(
|
||||
artifact(&decode(&read(&mut provider, "/config/active.kcfg"))),
|
||||
artifact(&decode(&active))
|
||||
);
|
||||
assert_eq!(
|
||||
recover(&flash)
|
||||
.active()
|
||||
.ports
|
||||
.get(PortId::new(1))
|
||||
.unwrap()
|
||||
.role,
|
||||
Role::Disabled
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn readable_journal_recovery_does_not_scan_entire_slots() {
|
||||
let flash = RamNor::default();
|
||||
Store::new(flash.clone()).persist(&factory()).unwrap();
|
||||
flash.0.borrow_mut().bytes_read = 0;
|
||||
assert_eq!(recover(&flash).diagnostic(), BootDiagnostic::Healthy);
|
||||
assert!(
|
||||
flash.0.borrow().bytes_read < 2048,
|
||||
"readable records should reuse journal recovery"
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,671 @@
|
||||
//! Exercises the actual protocol worker and physical UART owners. Only elapsed
|
||||
//! time, PAC registers and provider latency are simulated; no protocol is copied.
|
||||
extern crate std;
|
||||
use super::*;
|
||||
use crate::f722::serial::{ReceiveEvent, SerialDevice, SerialIo, UartIo};
|
||||
use crate::f722::{data_channel::exchange_usb, usb};
|
||||
use kaptan_file_mavlink::{
|
||||
Content, ControlRequest, FileFailure, FileMetadata, FileOperation, FilePath, FileRequest,
|
||||
FileStatus,
|
||||
};
|
||||
use kaptan_mavlink::{encoder::MavlinkEncoder, receiver::IntegrityMavlinkFramer};
|
||||
use std::{
|
||||
boxed::Box,
|
||||
cell::{Cell, RefCell},
|
||||
collections::VecDeque,
|
||||
rc::Rc,
|
||||
vec::Vec,
|
||||
};
|
||||
use usb::test_bus as usb_bus;
|
||||
|
||||
const INVENTORY: SerialInventory =
|
||||
SerialInventory::new(&[crate::UART1, crate::UART2, crate::UART4]);
|
||||
|
||||
#[derive(Default)]
|
||||
struct Registers {
|
||||
rx: VecDeque<u8>,
|
||||
tx: Vec<u8>,
|
||||
tx_ready_irq: bool,
|
||||
tx_complete_irq: bool,
|
||||
}
|
||||
struct MockUart(Rc<RefCell<Registers>>);
|
||||
impl UartIo for MockUart {
|
||||
fn tx_ready(&mut self) -> bool {
|
||||
true
|
||||
}
|
||||
fn tx_complete(&mut self) -> bool {
|
||||
true
|
||||
}
|
||||
fn write_byte(&mut self, byte: u8) {
|
||||
self.0.borrow_mut().tx.push(byte);
|
||||
}
|
||||
fn interrupts(&mut self, ready: bool, complete: bool) {
|
||||
let mut registers = self.0.borrow_mut();
|
||||
registers.tx_ready_irq = ready;
|
||||
registers.tx_complete_irq = complete;
|
||||
}
|
||||
}
|
||||
impl SerialDevice for MockUart {
|
||||
fn receive(&mut self) -> ReceiveEvent {
|
||||
self.0
|
||||
.borrow_mut()
|
||||
.rx
|
||||
.pop_front()
|
||||
.map_or(ReceiveEvent::Idle, ReceiveEvent::Byte)
|
||||
}
|
||||
}
|
||||
|
||||
struct Io {
|
||||
uart: [SerialIo<MockUart>; 3],
|
||||
registers: [Rc<RefCell<Registers>>; 3],
|
||||
sent: Vec<(u8, kaptan::MavMessage)>,
|
||||
admitted: [usize; 3],
|
||||
}
|
||||
impl Io {
|
||||
fn new() -> Self {
|
||||
let registers = core::array::from_fn(|_| Rc::new(RefCell::new(Registers::default())));
|
||||
Self {
|
||||
uart: core::array::from_fn(|i| {
|
||||
SerialIo::new(Some(MockUart(registers[i].clone())), i != 2)
|
||||
}),
|
||||
registers,
|
||||
sent: Vec::new(),
|
||||
admitted: [0; 3],
|
||||
}
|
||||
}
|
||||
fn irq(&mut self, now: Timestamp) {
|
||||
for i in 0..3 {
|
||||
if !self.registers[i].borrow().rx.is_empty() {
|
||||
self.admitted[i] += 1;
|
||||
}
|
||||
self.uart[i].on_interrupt(now);
|
||||
}
|
||||
}
|
||||
fn finish_setup_transmit(&mut self, now: Timestamp) {
|
||||
// The owned TX queue has four pending slots plus reserved/active
|
||||
// storage. 2048 ready/complete IRQs cover even six maximum-sized frames.
|
||||
for _ in 0..2048 {
|
||||
let mut pending = false;
|
||||
for i in 0..2 {
|
||||
let enabled = {
|
||||
let registers = self.registers[i].borrow();
|
||||
registers.tx_ready_irq || registers.tx_complete_irq
|
||||
};
|
||||
if enabled {
|
||||
pending = true;
|
||||
self.uart[i].on_interrupt(now);
|
||||
}
|
||||
}
|
||||
if !pending {
|
||||
return;
|
||||
}
|
||||
}
|
||||
panic!("setup UART transmission did not reach quiescence");
|
||||
}
|
||||
fn exchange(&mut self, id: u8, frame: Option<PendingFrame<MavlinkFrame>>) -> IoBatch {
|
||||
if let Some(frame) = &frame {
|
||||
self.sent.push((id, checked(frame.frame).decode().unwrap()));
|
||||
}
|
||||
if id == 0 {
|
||||
return match frame {
|
||||
Some(_) => IoBatch {
|
||||
accepted: true,
|
||||
..IoBatch::default()
|
||||
},
|
||||
None => IoBatch::default(),
|
||||
};
|
||||
}
|
||||
self.uart[INVENTORY.index(PortId::new(id)).unwrap()].exchange(frame)
|
||||
}
|
||||
}
|
||||
|
||||
struct SlowProvider<'a> {
|
||||
work: Option<std::boxed::Box<dyn FnOnce() + 'a>>,
|
||||
opens: usize,
|
||||
}
|
||||
impl FileProvider for SlowProvider<'_> {
|
||||
fn open_read(&mut self, _: &FilePath) -> Result<FileMetadata, FileFailure> {
|
||||
self.opens += 1;
|
||||
if let Some(work) = self.work.take() {
|
||||
work();
|
||||
}
|
||||
Ok(FileMetadata {
|
||||
size: 0,
|
||||
checksum_crc32c: 0,
|
||||
})
|
||||
}
|
||||
fn open_write(&mut self, _: &FilePath, _: FileMetadata) -> Result<(), FileFailure> {
|
||||
unreachable!()
|
||||
}
|
||||
fn read(&mut self, _: u32, _: &mut [u8]) -> Result<(), FileFailure> {
|
||||
unreachable!()
|
||||
}
|
||||
fn write(&mut self, _: u32, _: &[u8]) -> Result<(), FileFailure> {
|
||||
unreachable!()
|
||||
}
|
||||
fn close(&mut self) -> Result<Content, FileFailure> {
|
||||
Ok(Content::EMPTY)
|
||||
}
|
||||
fn abort(&mut self) {}
|
||||
}
|
||||
fn checked(frame: MavlinkFrame) -> IntegrityCheckedFrame {
|
||||
let mut parser = IntegrityMavlinkFramer::new();
|
||||
frame
|
||||
.raw_bytes()
|
||||
.iter()
|
||||
.find_map(|&byte| parser.receive_byte(byte))
|
||||
.unwrap()
|
||||
}
|
||||
fn checked_stream(bytes: &[u8]) -> Vec<IntegrityCheckedFrame> {
|
||||
let mut parser = IntegrityMavlinkFramer::new();
|
||||
let frames: Vec<_> = bytes
|
||||
.iter()
|
||||
.filter_map(|&byte| parser.receive_byte(byte))
|
||||
.collect();
|
||||
assert_eq!(
|
||||
frames
|
||||
.iter()
|
||||
.map(|frame| frame.raw().raw_bytes().len())
|
||||
.sum::<usize>(),
|
||||
bytes.len(),
|
||||
"all transmitted bytes must belong to complete CRC-valid frames"
|
||||
);
|
||||
frames
|
||||
}
|
||||
fn open(system: u8) -> MavlinkFrame {
|
||||
let request = FileRequest::Control(ControlRequest {
|
||||
operation: FileOperation::OpenRead,
|
||||
target: Target::new(1, 1),
|
||||
position: 0,
|
||||
file_size: 0,
|
||||
checksum_crc32c: 0,
|
||||
transfer_id: 9,
|
||||
content: Content::try_new(b"/test").unwrap(),
|
||||
});
|
||||
let kaptan::MavMessage::FILE_CONTROL(data) = wire::encode_request(request) else {
|
||||
unreachable!()
|
||||
};
|
||||
MavlinkEncoder::new(system, 7).encode(&data)
|
||||
}
|
||||
fn mtf() -> [u8; 27] {
|
||||
let mut frame = [0; 27];
|
||||
frame[..6].copy_from_slice(&[0xef, 1, 1, 0x51, 0, 20]);
|
||||
frame[10..14].copy_from_slice(&1234u32.to_le_bytes());
|
||||
frame[16] = 1; // range valid
|
||||
frame[23] = 1; // flow valid
|
||||
frame[26] = frame[..26]
|
||||
.iter()
|
||||
.fold(0u8, |sum, byte| sum.wrapping_add(*byte));
|
||||
frame
|
||||
}
|
||||
fn worker(provider: SlowProvider<'_>) -> ProtocolWorker<SlowProvider<'_>, 3> {
|
||||
let mailboxes = Box::leak(Box::new((
|
||||
LatestImuMailbox::new(),
|
||||
LatestRangeMailbox::new(),
|
||||
LatestOpticalFlowMailbox::new(),
|
||||
)));
|
||||
let (_, imu) = mailboxes.0.split();
|
||||
let (range_publisher, range) = mailboxes.1.split();
|
||||
let (flow_publisher, optical_flow) = mailboxes.2.split();
|
||||
let rates = DataChannelRates {
|
||||
imu_hz: 1,
|
||||
heartbeat_hz: 1,
|
||||
range_hz: Some(98),
|
||||
optical_flow_hz: None,
|
||||
};
|
||||
ProtocolWorker {
|
||||
inventory: INVENTORY,
|
||||
usb: Link::new(1, 1, rates).unwrap(),
|
||||
serial: [
|
||||
Some(SerialEndpoint::Mavlink(Link::new(1, 1, rates).unwrap())),
|
||||
Some(SerialEndpoint::Mavlink(Link::new(1, 1, rates).unwrap())),
|
||||
Some(SerialEndpoint::Mtf01 {
|
||||
service: Mtf01::new(),
|
||||
range: range_publisher,
|
||||
optical_flow: flow_publisher,
|
||||
}),
|
||||
],
|
||||
collector: DataCollector::new(DataSources {
|
||||
imu,
|
||||
range,
|
||||
optical_flow,
|
||||
}),
|
||||
config_file: provider,
|
||||
file_service: FileService::new(Target::new(1, 1)),
|
||||
file_lease: FileLease::default(),
|
||||
mavlink_errors: ErrorSink::new(),
|
||||
mavlink_logs: LogSinkQueue::new(crate::LevelFilter::Off),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compact_worker_routes_by_stable_id_and_never_exchanges_missing_ports() {
|
||||
let ProtocolWorker {
|
||||
usb,
|
||||
serial: [_, uart2, mtf01],
|
||||
collector,
|
||||
config_file,
|
||||
file_service,
|
||||
file_lease,
|
||||
mavlink_errors,
|
||||
mavlink_logs,
|
||||
..
|
||||
} = worker(SlowProvider {
|
||||
work: None,
|
||||
opens: 0,
|
||||
});
|
||||
// Non-numeric order makes any residual platform-wide slot mapping fail.
|
||||
let mut worker = ProtocolWorker {
|
||||
usb,
|
||||
serial: [mtf01, uart2],
|
||||
inventory: SerialInventory::new(&[crate::UART4, crate::UART2]),
|
||||
collector,
|
||||
config_file,
|
||||
file_service,
|
||||
file_lease,
|
||||
mavlink_errors,
|
||||
mavlink_logs,
|
||||
};
|
||||
assert!(worker.link(1).is_none());
|
||||
assert!(worker.link(4).is_none());
|
||||
assert!(worker.link(2).is_some());
|
||||
let mut exchanges = Vec::new();
|
||||
worker.process_worker_events(
|
||||
2,
|
||||
|id, frame| {
|
||||
assert!(frame.is_none());
|
||||
exchanges.push(id);
|
||||
IoBatch::default()
|
||||
},
|
||||
Timestamp::default,
|
||||
);
|
||||
assert_eq!(exchanges, [4, 2, 0]);
|
||||
worker.dispatch_frame(
|
||||
2,
|
||||
checked(open(42)),
|
||||
&mut |id, frame| {
|
||||
assert_eq!(id, 2);
|
||||
assert!(matches!(
|
||||
checked(frame.unwrap().frame).decode(),
|
||||
Some(kaptan::MavMessage::FILE_CONTROL(_))
|
||||
));
|
||||
IoBatch::default()
|
||||
},
|
||||
&mut Timestamp::default,
|
||||
);
|
||||
assert_eq!(worker.config_file.opens, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expensive_dispatch_keeps_uarts_and_usb_tx_serviceable_during_rx_backpressure() {
|
||||
usb_bus::with_link(|host, usb| {
|
||||
let usb = RefCell::new(usb);
|
||||
let io = Rc::new(RefCell::new(Io::new()));
|
||||
let time = Rc::new(Cell::new(0u64));
|
||||
let mut worker = worker(SlowProvider {
|
||||
work: None,
|
||||
opens: 0,
|
||||
});
|
||||
let mut exchange = |id, frame| {
|
||||
let batch = io.borrow_mut().exchange(id, frame);
|
||||
if id == 0 {
|
||||
exchange_usb(&mut usb.borrow_mut(), frame, |enabled| {
|
||||
assert!(
|
||||
usb.try_borrow_mut().is_err(),
|
||||
"gate must finish inside the I/O borrow"
|
||||
);
|
||||
host.set_rx_interrupt_enabled(enabled)
|
||||
})
|
||||
} else {
|
||||
batch
|
||||
}
|
||||
};
|
||||
// Finish an initial sensor report and advance the real schedule to RANGE.
|
||||
io.borrow().registers[2].borrow_mut().rx.extend(mtf());
|
||||
for t in 1..=27 {
|
||||
io.borrow_mut().irq(Timestamp::from_microseconds(t * 100));
|
||||
}
|
||||
time.set(3000);
|
||||
for _ in 0..2 {
|
||||
worker.process_worker_events(1, &mut exchange, || {
|
||||
Timestamp::from_microseconds(time.get())
|
||||
});
|
||||
}
|
||||
assert_eq!(
|
||||
worker
|
||||
.collector
|
||||
.snapshot()
|
||||
.range
|
||||
.unwrap()
|
||||
.timestamp
|
||||
.as_microseconds(),
|
||||
2700
|
||||
);
|
||||
// Warm-up ticks may route process-global errors raised by another test.
|
||||
// Explicitly queue one diagnostic locally so the setup/backlog case is
|
||||
// deterministic without changing or draining the global error ingress.
|
||||
let mut diagnostic = kaptan::ERROR_DATA::DEFAULT;
|
||||
diagnostic.error_id = 65001;
|
||||
let raised_at = b"io_latency_setup:1";
|
||||
diagnostic.raised_at[..raised_at.len()].copy_from_slice(raised_at);
|
||||
for id in [0, 1, 2] {
|
||||
let frame = worker
|
||||
.link(id)
|
||||
.unwrap()
|
||||
.channel
|
||||
.encode_message(FrameClass::Critical, &diagnostic);
|
||||
assert!(exchange(id, Some(frame)).accepted);
|
||||
}
|
||||
// Settle setup traffic on every transport before queuing workload
|
||||
// markers. Vital traffic can overtake a pending critical diagnostic;
|
||||
// without this UART drain, that diagnostic could follow the marker.
|
||||
io.borrow_mut()
|
||||
.finish_setup_transmit(Timestamp::from_microseconds(time.get()));
|
||||
host.collect_packets(&mut usb.borrow_mut());
|
||||
let uart_tx_start: [usize; 2] = core::array::from_fn(|i| {
|
||||
let registers = io.borrow().registers[i].clone();
|
||||
let registers = registers.borrow();
|
||||
let frames = checked_stream(®isters.tx);
|
||||
assert!(frames.iter().any(|frame| matches!(
|
||||
frame.decode(), Some(kaptan::MavMessage::ERROR(data)) if data.error_id == 65001
|
||||
)));
|
||||
registers.tx.len()
|
||||
});
|
||||
io.borrow_mut().sent.clear();
|
||||
let heartbeat = MavlinkEncoder::new(1, 1).encode(&kaptan::HEARTBEAT_DATA::default());
|
||||
for id in [1, 2] {
|
||||
assert!(
|
||||
io.borrow_mut()
|
||||
.exchange(
|
||||
id,
|
||||
Some(PendingFrame {
|
||||
class: FrameClass::Vital,
|
||||
frame: heartbeat,
|
||||
}),
|
||||
)
|
||||
.accepted
|
||||
);
|
||||
}
|
||||
let mut data = kaptan::FILE_DATA_DATA::DEFAULT;
|
||||
data.data[..107].fill(0x5a);
|
||||
let usb_frame = MavlinkEncoder::new(1, 1).encode(&data);
|
||||
assert_eq!(usb_frame.raw_bytes().len(), 128);
|
||||
exchange_usb(
|
||||
&mut usb.borrow_mut(),
|
||||
Some(PendingFrame {
|
||||
class: FrameClass::Vital,
|
||||
frame: usb_frame,
|
||||
}),
|
||||
|enabled| host.set_rx_interrupt_enabled(enabled),
|
||||
);
|
||||
// Real MAVLink bytes on both UARTs and a second MTF report arrive during
|
||||
// expensive provider work. Borrowing I/O here would panic if dispatch still
|
||||
// ran under the caller's I/O lock.
|
||||
let during = io.clone();
|
||||
let clock = time.clone();
|
||||
let physical_usb = &usb;
|
||||
worker.config_file.work = Some(std::boxed::Box::new(move || {
|
||||
for i in 0..2 {
|
||||
during.borrow().registers[i]
|
||||
.borrow_mut()
|
||||
.rx
|
||||
.extend(open(42).raw_bytes());
|
||||
}
|
||||
during.borrow().registers[2].borrow_mut().rx.extend(mtf());
|
||||
let mut transmitted_usb = Vec::new();
|
||||
for t in 1..=3000 {
|
||||
clock.set(3000 + t * 100);
|
||||
during
|
||||
.borrow_mut()
|
||||
.irq(Timestamp::from_microseconds(clock.get()));
|
||||
if t == 1 {
|
||||
host.write_packet(open(42).raw_bytes());
|
||||
}
|
||||
if host.enter_irq() {
|
||||
physical_usb.borrow_mut().with_rx_interrupt_gate(
|
||||
|usb| usb.poll(),
|
||||
|enabled| host.set_rx_interrupt_enabled(enabled),
|
||||
);
|
||||
}
|
||||
if let Some(packet) = host.take_packet() {
|
||||
transmitted_usb.push(packet);
|
||||
}
|
||||
}
|
||||
for (i, start) in uart_tx_start.into_iter().enumerate() {
|
||||
let registers = during.borrow().registers[i].clone();
|
||||
assert_eq!(
|
||||
®isters.borrow().tx[start..],
|
||||
heartbeat.raw_bytes(),
|
||||
"UART {i} must transmit the complete workload marker during provider work"
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
transmitted_usb.concat(),
|
||||
usb_frame.raw_bytes(),
|
||||
"IN completion interrupts submit all remaining bytes during provider work"
|
||||
);
|
||||
assert_eq!(
|
||||
transmitted_usb.iter().map(Vec::len).collect::<Vec<_>>(),
|
||||
[64, 64, 0]
|
||||
);
|
||||
assert!(
|
||||
!host.rx_interrupt_enabled(),
|
||||
"only the RX FIFO interrupt waits for the consumer"
|
||||
);
|
||||
assert!(host.nvic_enabled());
|
||||
assert!(
|
||||
host.events_empty(),
|
||||
"TX completion was serviced without the worker"
|
||||
);
|
||||
}));
|
||||
host.write_packet(open(42).raw_bytes());
|
||||
worker.process_worker_events(1, &mut exchange, || {
|
||||
Timestamp::from_microseconds(time.get())
|
||||
});
|
||||
assert_eq!(time.get(), 303000);
|
||||
// The report that was fresh at worker entry became stale during dispatch.
|
||||
assert!(
|
||||
!io.borrow()
|
||||
.sent
|
||||
.iter()
|
||||
.any(|(_, msg)| matches!(msg, kaptan::MavMessage::RANGE(_)))
|
||||
);
|
||||
for _ in 0..32 {
|
||||
worker.process_worker_events(2, &mut exchange, || {
|
||||
Timestamp::from_microseconds(time.get())
|
||||
});
|
||||
}
|
||||
assert_eq!(
|
||||
worker
|
||||
.collector
|
||||
.snapshot()
|
||||
.range
|
||||
.unwrap()
|
||||
.timestamp
|
||||
.as_microseconds(),
|
||||
5700,
|
||||
"queued MTF bytes retain receipt time, not drain time"
|
||||
);
|
||||
assert_eq!(io.borrow().admitted[2], 54);
|
||||
assert!(
|
||||
host.rx_interrupt_enabled(),
|
||||
"draining RX rearms its FIFO interrupt after provider work"
|
||||
);
|
||||
for id in [0, 1, 2] {
|
||||
assert!(
|
||||
io.borrow().sent.iter().any(|(ingress, msg)| *ingress == id
|
||||
&& matches!(msg, kaptan::MavMessage::FILE_CONTROL(_))),
|
||||
"each link receives its own response"
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
worker.config_file.opens, 1,
|
||||
"USB retry received during provider work is replayed"
|
||||
);
|
||||
assert_eq!(
|
||||
io.borrow()
|
||||
.sent
|
||||
.iter()
|
||||
.filter(|(id, msg)| *id == 0 && matches!(msg, kaptan::MavMessage::FILE_CONTROL(_)))
|
||||
.count(),
|
||||
2
|
||||
);
|
||||
// A long silent interval followed by a new byte uses the SAME canonical time
|
||||
// source (serviced independently by TIM2), not a dormant per-sensor clock.
|
||||
time.set(60_000_000);
|
||||
io.borrow().registers[2].borrow_mut().rx.extend(mtf());
|
||||
for t in 1..=27 {
|
||||
io.borrow_mut()
|
||||
.irq(Timestamp::from_microseconds(time.get() + t));
|
||||
}
|
||||
for _ in 0..2 {
|
||||
worker.process_worker_events(2, &mut exchange, || {
|
||||
Timestamp::from_microseconds(time.get() + 100)
|
||||
});
|
||||
}
|
||||
assert_eq!(
|
||||
worker
|
||||
.collector
|
||||
.snapshot()
|
||||
.range
|
||||
.unwrap()
|
||||
.timestamp
|
||||
.as_microseconds(),
|
||||
60_000_027
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn each_dispatch_rechecks_lease_and_foreign_requests_do_not_renew_it() {
|
||||
let mut worker = worker(SlowProvider {
|
||||
work: None,
|
||||
opens: 0,
|
||||
});
|
||||
let time = Cell::new(0u64);
|
||||
let mut replies = Vec::new();
|
||||
worker.dispatch_frame(
|
||||
0,
|
||||
checked(open(42)),
|
||||
&mut |id, frame| {
|
||||
replies.push((id, checked(frame.unwrap().frame).decode().unwrap()));
|
||||
IoBatch::default()
|
||||
},
|
||||
&mut || Timestamp::from_microseconds(time.get()),
|
||||
);
|
||||
assert_eq!(worker.file_lease.deadline, Some(10_000_000));
|
||||
let mut usb = Some(checked(open(43)));
|
||||
if let Some(SerialEndpoint::Mavlink(link)) = &mut worker.serial[0] {
|
||||
for &byte in open(42).raw_bytes() {
|
||||
link.receiver.receive_byte(byte);
|
||||
}
|
||||
}
|
||||
time.set(9_000_000);
|
||||
worker.process_worker_events(
|
||||
2,
|
||||
|id, frame| {
|
||||
if let Some(frame) = frame {
|
||||
replies.push((id, checked(frame.frame).decode().unwrap()));
|
||||
if id == 0 {
|
||||
time.set(11_000_000);
|
||||
}
|
||||
IoBatch::default()
|
||||
} else if id == 0 {
|
||||
IoBatch {
|
||||
usb: usb.take(),
|
||||
..IoBatch::default()
|
||||
}
|
||||
} else {
|
||||
IoBatch::default()
|
||||
}
|
||||
},
|
||||
|| Timestamp::from_microseconds(time.get()),
|
||||
);
|
||||
let statuses: Vec<_> = replies
|
||||
.iter()
|
||||
.filter_map(|(id, msg)| match msg {
|
||||
kaptan::MavMessage::FILE_CONTROL(data) => Some((*id, data.status as u8)),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(
|
||||
statuses,
|
||||
[
|
||||
(0, FileStatus::Ok as u8),
|
||||
(0, FileStatus::Busy as u8),
|
||||
(1, FileStatus::Ok as u8)
|
||||
]
|
||||
);
|
||||
assert_eq!(worker.config_file.opens, 2);
|
||||
assert_eq!(worker.file_lease.deadline, Some(21_000_000));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn level_triggered_usb_and_continuous_uart_notifications_reach_the_actual_consumer() {
|
||||
usb_bus::with_link(|host, usb| {
|
||||
let mut io = Io::new();
|
||||
let mut worker = worker(SlowProvider {
|
||||
work: None,
|
||||
opens: 0,
|
||||
});
|
||||
for _ in 0..3 {
|
||||
host.write_packet(open(42).raw_bytes());
|
||||
}
|
||||
let uart_frame = MavlinkEncoder::new(43, 7).heartbeat();
|
||||
let mut workers = 0;
|
||||
let mut usb_replies = 0;
|
||||
for tick in 0..128 {
|
||||
// Each decision has a genuine priority-14 byte arrival on both UARTs.
|
||||
// These notifications cannot let a repeated priority-8 USB level
|
||||
// prevent the priority-7 protocol consumer from running.
|
||||
for registers in &io.registers[..2] {
|
||||
registers
|
||||
.borrow_mut()
|
||||
.rx
|
||||
.push_back(uart_frame.raw_bytes()[tick % uart_frame.raw_bytes().len()]);
|
||||
}
|
||||
let now = Timestamp::from_microseconds(tick as u64 * 100);
|
||||
io.irq(now);
|
||||
if host.enter_irq() {
|
||||
usb.with_rx_interrupt_gate(
|
||||
|usb| usb.poll(),
|
||||
|enabled| host.set_rx_interrupt_enabled(enabled),
|
||||
);
|
||||
} else {
|
||||
workers += 1;
|
||||
worker.process_worker_events(
|
||||
2,
|
||||
|id, frame| {
|
||||
if id == 0 {
|
||||
if frame.as_ref().is_some_and(|frame| {
|
||||
matches!(
|
||||
checked(frame.frame).decode(),
|
||||
Some(kaptan::MavMessage::FILE_CONTROL(_))
|
||||
)
|
||||
}) {
|
||||
usb_replies += 1;
|
||||
}
|
||||
exchange_usb(usb, frame, |enabled| {
|
||||
host.set_rx_interrupt_enabled(enabled)
|
||||
})
|
||||
} else {
|
||||
io.exchange(id, frame)
|
||||
}
|
||||
},
|
||||
|| now,
|
||||
);
|
||||
}
|
||||
host.take_packet(); // Host ACK, without a hidden device poll.
|
||||
}
|
||||
assert!(workers > 0);
|
||||
assert_eq!(worker.config_file.opens, 1);
|
||||
assert_eq!(
|
||||
usb_replies, 3,
|
||||
"the consumer decoded the request and replayed both retries"
|
||||
);
|
||||
assert_eq!(io.admitted[..2], [128, 128]);
|
||||
assert_eq!(host.receive_backlog(), (0, false));
|
||||
assert!(host.rx_interrupt_enabled());
|
||||
assert!(host.nvic_enabled());
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
extern crate std;
|
||||
use std::vec::Vec;
|
||||
|
||||
use kaptan_config::{Config, ConfigModel, ConfigValue, FieldId};
|
||||
use kaptan_ports::{PortCatalog, PortDescriptor, PortSettings, Ports, Role};
|
||||
use platform_stm32f7::{
|
||||
f722::{F722Route, Icm42688Imu, ImuService, Mpu6000Imu},
|
||||
*,
|
||||
};
|
||||
|
||||
use crate::f722::{config as target_config, geprc, kakute, ports};
|
||||
|
||||
fn board_route<B, R, Imu>()
|
||||
where
|
||||
B: F7Board,
|
||||
B::ImmutableConfig: F7ImmutableConfig<Route = R>,
|
||||
R: F722Route<Imu = Imu>,
|
||||
Imu: ImuService,
|
||||
{
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn board_defaults_disable_configurable_ports() {
|
||||
board_route::<kakute::KakuteF7MiniV3, Mpu6000Spi1, Mpu6000Imu>();
|
||||
board_route::<geprc::GeprcF722V2, Icm42688Spi3, Icm42688Imu>();
|
||||
fn check<B: F7Board>() {
|
||||
let candidate =
|
||||
target_config::UserConfigModel::<_, B::ImmutableConfig, 5>::new(B::FACTORY_USER_CONFIG);
|
||||
let active = candidate.validate(&B::IMMUTABLE_CONFIG).unwrap();
|
||||
let inventory = B::IMMUTABLE_CONFIG.f722().hardware.serial();
|
||||
assert_eq!(inventory.len(), 5);
|
||||
assert_eq!(
|
||||
active
|
||||
.ports
|
||||
.iter()
|
||||
.map(|(id, _)| id.get())
|
||||
.collect::<Vec<_>>(),
|
||||
[1, 2, 3, 4, 6]
|
||||
);
|
||||
assert!(
|
||||
active
|
||||
.ports
|
||||
.iter()
|
||||
.all(|(_, port)| port.role == Role::Disabled)
|
||||
);
|
||||
let fields: Vec<_> = (0..B::UserConfig::FIELD_COUNT)
|
||||
.map(|index| B::UserConfig::descriptor(index).unwrap())
|
||||
.collect();
|
||||
assert_eq!(fields.len(), 7 + 2 * inventory.len());
|
||||
assert_eq!(
|
||||
fields[..7]
|
||||
.iter()
|
||||
.map(|field| field.id.get())
|
||||
.collect::<Vec<_>>(),
|
||||
[101, 102, 105, 103, 104, 106, 107]
|
||||
);
|
||||
for (port, fields) in inventory
|
||||
.descriptors()
|
||||
.iter()
|
||||
.zip(fields[7..].chunks_exact(2))
|
||||
{
|
||||
assert_eq!(fields, port.fields().iter().collect::<Vec<_>>());
|
||||
assert_eq!(
|
||||
candidate.field_value(port.baud_field),
|
||||
Some(ConfigValue::U32(115_200))
|
||||
);
|
||||
}
|
||||
assert_eq!(inventory.index(kaptan_ports::PortId::new(0)), None);
|
||||
}
|
||||
check::<kakute::KakuteF7MiniV3>();
|
||||
check::<geprc::GeprcF722V2>();
|
||||
}
|
||||
|
||||
struct Both;
|
||||
impl PortCatalog<3> for Both {
|
||||
const DESCRIPTORS: [PortDescriptor; 3] = [UART1, UART2, UART4];
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn independent_imu_serial_and_led_facts_allow_cross_route_compositions() {
|
||||
let mpu = Mpu6000Spi1 {
|
||||
mount: ImuMount::BodyAligned,
|
||||
config: Mpu6000Config {
|
||||
accelerometer: AccelerometerRange::PlusMinus4G,
|
||||
gyroscope: GyroscopeRange::PlusMinus500DegreesPerSecond,
|
||||
digital_low_pass_filter: DigitalLowPassFilter::Bandwidth42Hz,
|
||||
sample_rate_divider: 9,
|
||||
},
|
||||
};
|
||||
let icm = Icm42688Spi3 {
|
||||
mount: ImuMount::Yaw180,
|
||||
};
|
||||
fn check<R: F722Route>(route: R) {
|
||||
for led in [StatusLed::ActiveLowPd2, StatusLed::ActiveHighPa13] {
|
||||
let inventory = SerialInventory::new(&Both::DESCRIPTORS);
|
||||
let hardware =
|
||||
F722Hardware::new(route, inventory, led, VerticalSensor::None, MotorPins::Four);
|
||||
for sensor in [0, 1, 2] {
|
||||
let mut settings = [PortSettings {
|
||||
role: Role::Mavlink,
|
||||
baud: 115_200,
|
||||
}; 3];
|
||||
settings[sensor].role = Role::Mtf01;
|
||||
let root = Ports::<Both, 3>::new(settings);
|
||||
let plan = ports::project_ports::<_, 3>(&root, hardware.serial(), 3400).unwrap();
|
||||
assert_eq!(
|
||||
plan.iter().filter(|(_, p)| p.role == Role::Mavlink).count(),
|
||||
2
|
||||
);
|
||||
assert_eq!(
|
||||
plan.iter().filter(|(_, p)| p.role == Role::Mtf01).count(),
|
||||
1
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
check(mpu);
|
||||
check(icm);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn double_sensor_unsupported_baud_and_missing_inventory_fields_are_rejected() {
|
||||
let inventory = SerialInventory::new(&Both::DESCRIPTORS);
|
||||
let mut root = Ports::<Both, 3>::new(
|
||||
[PortSettings {
|
||||
role: Role::Mtf01,
|
||||
baud: 115_200,
|
||||
}; 3],
|
||||
);
|
||||
let error = ports::project_ports::<_, 3>(&root, inventory, 0).unwrap_err();
|
||||
assert_eq!(error.primary(), Some(UART2.role_field));
|
||||
assert_eq!(error.related_field(), Some(UART1.role_field));
|
||||
root.set_field(FieldId::new(203), ConfigValue::U8(0))
|
||||
.unwrap();
|
||||
root.set_field(FieldId::new(207), ConfigValue::U8(0))
|
||||
.unwrap();
|
||||
assert!(ports::project_ports::<_, 3>(&root, inventory, 0).is_ok());
|
||||
assert!(
|
||||
root.set_field(FieldId::new(202), ConfigValue::U32(12345))
|
||||
.is_err()
|
||||
);
|
||||
root.set_field(FieldId::new(202), ConfigValue::U32(230400))
|
||||
.unwrap();
|
||||
assert!(ports::project_ports::<_, 3>(&root, inventory, 0).is_err());
|
||||
root.set_field(FieldId::new(201), ConfigValue::U8(1))
|
||||
.unwrap();
|
||||
assert!(ports::project_ports::<_, 3>(&root, inventory, 23041).is_err());
|
||||
assert!(
|
||||
ports::project_ports::<_, 3>(&geprc::GeprcF722V2::FACTORY_USER_CONFIG, inventory, 0)
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn schema_inventory_and_storage_count_must_agree_even_when_disabled() {
|
||||
let root = Ports::<Both, 3>::new(
|
||||
[PortSettings {
|
||||
role: Role::Disabled,
|
||||
baud: 115_200,
|
||||
}; 3],
|
||||
);
|
||||
// No silent unused fields, including fields whose current role is Disabled.
|
||||
assert!(ports::project_ports::<_, 1>(&root, SerialInventory::new(&[UART1]), 0).is_err());
|
||||
assert!(ports::project_ports::<_, 0>(&root, SerialInventory::new(&[]), 0).is_err());
|
||||
assert!(
|
||||
ports::project_ports::<_, 2>(&root, SerialInventory::new(&Both::DESCRIPTORS), 0).is_err()
|
||||
);
|
||||
assert!(
|
||||
ports::project_ports::<_, 3>(&root, SerialInventory::new(&[UART1, UART1, UART2]), 0)
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_invalid_port_descriptors() {
|
||||
let root = Ports::<Both, 3>::new(
|
||||
[PortSettings {
|
||||
role: Role::Disabled,
|
||||
baud: 115_200,
|
||||
}; 3],
|
||||
);
|
||||
const DISABLED_ROLES: [(u8, &str); 1] = [Role::CONFIG_ENUM_OPTIONS[0]];
|
||||
const INVALID_INVENTORIES: [[PortDescriptor; 3]; 6] = {
|
||||
let mut rows = [[UART1, UART2, UART4]; 6];
|
||||
rows[0][0].id = kaptan_ports::PortId::new(3);
|
||||
rows[1][0].resources = &[8, 9, 101];
|
||||
rows[2][0].role_field = FieldId::new(209);
|
||||
rows[3][0].role_key = "ports.other.role";
|
||||
rows[4][0].allowed_bauds = &[9600];
|
||||
rows[5][0].supported_roles = &DISABLED_ROLES;
|
||||
rows
|
||||
};
|
||||
for descriptors in &INVALID_INVENTORIES {
|
||||
let inventory = SerialInventory::new(descriptors);
|
||||
assert!(ports::project_ports::<_, 3>(&root, inventory, 0).is_err());
|
||||
}
|
||||
struct WrongMetadata;
|
||||
impl PortCatalog<1> for WrongMetadata {
|
||||
const DESCRIPTORS: [PortDescriptor; 1] = [PortDescriptor {
|
||||
allowed_bauds: &[9600],
|
||||
..UART1
|
||||
}];
|
||||
}
|
||||
let root = Ports::<WrongMetadata, 1>::new([PortSettings {
|
||||
role: Role::Disabled,
|
||||
baud: 9600,
|
||||
}]);
|
||||
assert!(ports::project_ports::<_, 1>(&root, SerialInventory::new(&[UART1]), 0).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stable_ids_follow_catalog_order_and_disabled_ports_keep_their_slots() {
|
||||
struct Reordered;
|
||||
impl PortCatalog<2> for Reordered {
|
||||
const DESCRIPTORS: [PortDescriptor; 2] = [UART4, UART1];
|
||||
}
|
||||
let inventory = SerialInventory::new(&Reordered::DESCRIPTORS);
|
||||
let mut root = Ports::<Reordered, 2>::new(
|
||||
[PortSettings {
|
||||
role: Role::Disabled,
|
||||
baud: 115_200,
|
||||
}; 2],
|
||||
);
|
||||
for role in [Role::Disabled, Role::Mavlink, Role::Mtf01] {
|
||||
root.set_field(UART4.role_field, ConfigValue::U8(role.wire_value()))
|
||||
.unwrap();
|
||||
let plan = ports::project_ports::<_, 2>(&root, inventory, 0).unwrap();
|
||||
assert_eq!(
|
||||
plan.iter().map(|(id, _)| id.get()).collect::<Vec<_>>(),
|
||||
[4, 1]
|
||||
);
|
||||
assert_eq!(plan.get(UART4.id).unwrap().role, role);
|
||||
assert_eq!(inventory.index(UART4.id), Some(0));
|
||||
assert_eq!(inventory.index(UART1.id), Some(1));
|
||||
for id in [0, 2, 3, 5, 255] {
|
||||
assert_eq!(inventory.index(kaptan_ports::PortId::new(id)), None);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,332 @@
|
||||
//! Injected hardware exercises the production shared-state and IRQ mechanism.
|
||||
|
||||
extern crate std;
|
||||
|
||||
use super::{SharedTimer, TimerBackend};
|
||||
use std::{
|
||||
collections::VecDeque,
|
||||
sync::{Arc, Mutex, mpsc},
|
||||
thread,
|
||||
time::Duration,
|
||||
vec::Vec,
|
||||
};
|
||||
|
||||
const CC1IF: u32 = 1 << 1;
|
||||
const UIF: u32 = 1;
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
enum Access {
|
||||
Counter(u32),
|
||||
Ack,
|
||||
Compare(u32),
|
||||
Enable,
|
||||
Pend,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct Registers {
|
||||
cnt: u32,
|
||||
flags: u32,
|
||||
compare: u32,
|
||||
enabled: bool,
|
||||
pended: bool,
|
||||
// Advance while writing CCR. `true` means the new compare took effect
|
||||
// before CNT passed it, so hardware raises a fresh CC1IF.
|
||||
write_delays: VecDeque<(u32, bool)>,
|
||||
accesses: Vec<Access>,
|
||||
}
|
||||
|
||||
struct Counter {
|
||||
registers: Arc<Mutex<Registers>>,
|
||||
read_gate: Option<(mpsc::Sender<()>, mpsc::Receiver<()>)>,
|
||||
}
|
||||
|
||||
impl TimerBackend for Counter {
|
||||
fn counter(&mut self) -> u32 {
|
||||
let cnt = {
|
||||
let mut registers = self.registers.lock().unwrap();
|
||||
let cnt = registers.cnt;
|
||||
registers.accesses.push(Access::Counter(cnt));
|
||||
cnt
|
||||
};
|
||||
if let Some((sampled, resume)) = self.read_gate.take() {
|
||||
sampled.send(()).unwrap();
|
||||
resume.recv().unwrap();
|
||||
}
|
||||
cnt
|
||||
}
|
||||
|
||||
fn compare_pending(&mut self) -> bool {
|
||||
self.registers.lock().unwrap().flags & CC1IF != 0
|
||||
}
|
||||
|
||||
fn acknowledge_compare(&mut self) {
|
||||
let mut registers = self.registers.lock().unwrap();
|
||||
registers.flags &= !CC1IF;
|
||||
registers.accesses.push(Access::Ack);
|
||||
}
|
||||
|
||||
fn set_compare(&mut self, counter: u32) {
|
||||
let mut registers = self.registers.lock().unwrap();
|
||||
registers.compare = counter;
|
||||
registers.accesses.push(Access::Compare(counter));
|
||||
if let Some((advance, raises_flag)) = registers.write_delays.pop_front() {
|
||||
registers.cnt = registers.cnt.wrapping_add(advance);
|
||||
if raises_flag {
|
||||
registers.flags |= CC1IF;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn enable_compare_interrupt(&mut self) {
|
||||
let mut registers = self.registers.lock().unwrap();
|
||||
registers.enabled = true;
|
||||
registers.accesses.push(Access::Enable);
|
||||
}
|
||||
|
||||
fn pend_interrupt(&mut self) {
|
||||
let mut registers = self.registers.lock().unwrap();
|
||||
registers.pended = true;
|
||||
registers.accesses.push(Access::Pend);
|
||||
}
|
||||
}
|
||||
|
||||
fn timer() -> (SharedTimer<Counter>, Arc<Mutex<Registers>>) {
|
||||
let registers = Arc::new(Mutex::new(Registers::default()));
|
||||
let shared = SharedTimer::new();
|
||||
assert!(shared.initialize(|| Counter {
|
||||
registers: registers.clone(),
|
||||
read_gate: None,
|
||||
}));
|
||||
(shared, registers)
|
||||
}
|
||||
|
||||
fn tick(shared: &SharedTimer<Counter>, registers: &Mutex<Registers>, cnt: u32) -> bool {
|
||||
{
|
||||
let mut registers = registers.lock().unwrap();
|
||||
registers.cnt = cnt;
|
||||
registers.flags |= CC1IF;
|
||||
}
|
||||
shared.on_interrupt()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn one_time_publication_keeps_irq_disabled_until_successful_init_finishes() {
|
||||
let (shared, registers) = timer();
|
||||
assert!(!shared.initialize(|| panic!("must not reinitialize hardware")));
|
||||
assert!(!registers.lock().unwrap().enabled);
|
||||
// Other initialization may take time, or fail without ever enabling IRQs.
|
||||
registers.lock().unwrap().cnt = 2_000_000;
|
||||
registers.lock().unwrap().flags = CC1IF | UIF;
|
||||
assert_eq!(shared.now().as_microseconds(), 2_000_000);
|
||||
assert!(!registers.lock().unwrap().enabled);
|
||||
registers.lock().unwrap().accesses.clear();
|
||||
shared.enable_interrupt();
|
||||
let registers = registers.lock().unwrap();
|
||||
assert!(registers.enabled);
|
||||
assert_eq!(registers.flags, UIF);
|
||||
assert_eq!(
|
||||
registers.accesses,
|
||||
[
|
||||
Access::Ack,
|
||||
Access::Counter(2_000_000),
|
||||
Access::Compare(2_010_000),
|
||||
Access::Counter(2_000_000),
|
||||
Access::Enable,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn readers_and_irq_extend_one_epoch_across_rollover() {
|
||||
let (shared, registers) = timer();
|
||||
let reader_a = &shared;
|
||||
let reader_b = &shared;
|
||||
registers.lock().unwrap().cnt = u32::MAX - 5;
|
||||
assert_eq!(reader_a.now().as_microseconds(), 4_294_967_290);
|
||||
assert!(tick(&shared, ®isters, 4));
|
||||
assert_eq!(reader_b.now().as_microseconds(), 4_294_967_300);
|
||||
assert_eq!(registers.lock().unwrap().compare, 2_704);
|
||||
registers.lock().unwrap().cnt = 100;
|
||||
assert_eq!(reader_a.now().as_microseconds(), 4_294_967_396);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn future_compare_can_cross_counter_wrap() {
|
||||
let (shared, registers) = timer();
|
||||
assert!(tick(&shared, ®isters, u32::MAX - 5));
|
||||
let registers = registers.lock().unwrap();
|
||||
assert_eq!(registers.compare, 2_704);
|
||||
assert!(!registers.pended);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn silent_reader_retains_time_across_multiple_irq_serviced_wraps() {
|
||||
let (shared, registers) = timer();
|
||||
let silent_reader = &shared;
|
||||
assert_eq!(silent_reader.now().as_microseconds(), 0);
|
||||
// Deliberately delayed service, still below the single-wrap observation
|
||||
// bound. The silent consumer misses more than two whole counter wraps.
|
||||
for second in 1..=10u64 {
|
||||
assert!(tick(&shared, ®isters, (second * 1_000_000_000) as u32));
|
||||
}
|
||||
assert_eq!(silent_reader.now().as_microseconds(), 10_000_000_000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn on_time_and_two_second_delayed_irq_each_emit_one_event_and_future_compare() {
|
||||
let (shared, registers) = timer();
|
||||
for (cnt, expected) in [(10_000, 20_000), (2_010_000, 2_020_000)] {
|
||||
registers.lock().unwrap().accesses.clear();
|
||||
assert!(tick(&shared, ®isters, cnt));
|
||||
assert_eq!(shared.now().as_microseconds(), u64::from(cnt));
|
||||
// This represents the consumer callback, after on_interrupt returns.
|
||||
assert_eq!(registers.lock().unwrap().compare, expected);
|
||||
assert!(!shared.on_interrupt());
|
||||
assert_eq!(
|
||||
registers
|
||||
.lock()
|
||||
.unwrap()
|
||||
.accesses
|
||||
.iter()
|
||||
.filter(|access| matches!(access, Access::Compare(_)))
|
||||
.count(),
|
||||
1
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stale_nvic_pend_without_cc1if_neither_rearms_nor_notifies() {
|
||||
let (shared, registers) = timer();
|
||||
registers.lock().unwrap().flags = UIF;
|
||||
assert!(!shared.on_interrupt());
|
||||
let registers = registers.lock().unwrap();
|
||||
assert!(registers.accesses.is_empty());
|
||||
assert_eq!(registers.flags, UIF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn target_passed_during_ccr_write_is_retried_before_callbacks() {
|
||||
let (shared, registers) = timer();
|
||||
registers
|
||||
.lock()
|
||||
.unwrap()
|
||||
.write_delays
|
||||
.push_back((10_001, false));
|
||||
assert!(tick(&shared, ®isters, 10_000));
|
||||
assert_eq!(shared.now().as_microseconds(), 20_001);
|
||||
let registers = registers.lock().unwrap();
|
||||
assert_eq!(registers.compare, 30_000);
|
||||
assert!(!registers.pended);
|
||||
assert_eq!(
|
||||
registers.accesses,
|
||||
[
|
||||
Access::Ack,
|
||||
Access::Counter(10_000),
|
||||
Access::Compare(20_000),
|
||||
Access::Counter(20_001),
|
||||
Access::Compare(30_000),
|
||||
Access::Counter(20_001),
|
||||
Access::Counter(20_001),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repeated_missed_writes_pend_bounded_recovery_without_fabricating_an_event() {
|
||||
let (shared, registers) = timer();
|
||||
registers.lock().unwrap().write_delays = [(10_001, false), (10_000, false)].into();
|
||||
assert!(tick(&shared, ®isters, 10_000));
|
||||
{
|
||||
let mut registers = registers.lock().unwrap();
|
||||
assert!(registers.pended);
|
||||
assert_eq!(registers.compare, 30_000);
|
||||
assert_eq!(registers.cnt, 30_001);
|
||||
assert_eq!(registers.flags, 0);
|
||||
assert_eq!(
|
||||
registers
|
||||
.accesses
|
||||
.iter()
|
||||
.filter(|a| matches!(a, Access::Compare(_)))
|
||||
.count(),
|
||||
2
|
||||
);
|
||||
registers.pended = false; // NVIC takes the recovery exception.
|
||||
}
|
||||
assert!(!shared.on_interrupt());
|
||||
assert_eq!(registers.lock().unwrap().compare, 40_000);
|
||||
assert!(!registers.lock().unwrap().pended);
|
||||
assert!(tick(&shared, ®isters, 40_000));
|
||||
assert_eq!(registers.lock().unwrap().compare, 50_000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_compare_flag_during_rearm_and_unrelated_flags_are_preserved() {
|
||||
let (shared, registers) = timer();
|
||||
registers.lock().unwrap().flags = UIF;
|
||||
registers
|
||||
.lock()
|
||||
.unwrap()
|
||||
.write_delays
|
||||
.push_back((10_000, true));
|
||||
assert!(tick(&shared, ®isters, 10_000));
|
||||
assert_eq!(registers.lock().unwrap().flags, UIF | CC1IF);
|
||||
assert_eq!(registers.lock().unwrap().compare, 30_000);
|
||||
assert!(shared.on_interrupt());
|
||||
assert_eq!(registers.lock().unwrap().flags, UIF);
|
||||
assert!(!shared.on_interrupt());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn irq_cannot_sample_between_a_readers_cnt_read_and_epoch_update() {
|
||||
let registers = Arc::new(Mutex::new(Registers::default()));
|
||||
let shared = SharedTimer::new();
|
||||
let (sampled_tx, sampled_rx) = mpsc::channel();
|
||||
let (resume_tx, resume_rx) = mpsc::channel();
|
||||
assert!(shared.initialize(|| Counter {
|
||||
registers: registers.clone(),
|
||||
read_gate: Some((sampled_tx, resume_rx)),
|
||||
}));
|
||||
registers.lock().unwrap().cnt = 100;
|
||||
thread::scope(|scope| {
|
||||
let reader = scope.spawn(|| shared.now());
|
||||
sampled_rx.recv().unwrap();
|
||||
// Hardware continues ticking with PRIMASK set. The IRQ tries to run
|
||||
// while the reader has sampled CNT but has not committed its delta.
|
||||
registers.lock().unwrap().cnt = 150;
|
||||
registers.lock().unwrap().flags = CC1IF;
|
||||
let (attempt_tx, attempt_rx) = mpsc::channel();
|
||||
let (done_tx, done_rx) = mpsc::channel();
|
||||
let shared = &shared;
|
||||
let irq = scope.spawn(move || {
|
||||
attempt_tx.send(()).unwrap();
|
||||
let event = shared.on_interrupt();
|
||||
done_tx.send(()).unwrap();
|
||||
event
|
||||
});
|
||||
attempt_rx.recv().unwrap();
|
||||
let blocked = done_rx.recv_timeout(Duration::from_millis(20)).is_err();
|
||||
// Release before assertions so a failing test cannot strand either thread.
|
||||
resume_tx.send(()).unwrap();
|
||||
assert_eq!(reader.join().unwrap().as_microseconds(), 100);
|
||||
assert!(irq.join().unwrap());
|
||||
assert!(blocked, "IRQ entered while the reader was extending CNT");
|
||||
});
|
||||
assert_eq!(shared.now().as_microseconds(), 150);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn full_wrap_blackout_reset_and_stopped_clock_are_not_detectable_from_cnt() {
|
||||
let (shared, registers) = timer();
|
||||
registers.lock().unwrap().cnt = 100;
|
||||
assert_eq!(shared.now().as_microseconds(), 100);
|
||||
// Stopped/deep-sleep time and a whole unseen wrap both present CNT=100.
|
||||
// No software correction is possible. These modes require a system reset.
|
||||
assert_eq!(shared.now().as_microseconds(), 100);
|
||||
registers.lock().unwrap().cnt = ((1u64 << 32) + 100) as u32;
|
||||
assert_eq!(shared.now().as_microseconds(), 100);
|
||||
// An illicit reset/regression is indistinguishable from a forward wrap.
|
||||
registers.lock().unwrap().cnt = 90;
|
||||
assert_eq!(shared.now().as_microseconds(), 4_294_967_386);
|
||||
}
|
||||
@@ -0,0 +1,407 @@
|
||||
//! Shared packet-level USB hardware fake; runs the real CDC/USB stack.
|
||||
extern crate std;
|
||||
use super::UsbMavlink;
|
||||
use std::{
|
||||
collections::VecDeque,
|
||||
sync::{Arc, Mutex},
|
||||
vec::Vec,
|
||||
};
|
||||
use usb_device::{
|
||||
UsbDirection, UsbError,
|
||||
bus::{PollResult, UsbBus, UsbBusAllocator},
|
||||
device::{UsbDeviceBuilder, UsbVidPid},
|
||||
endpoint::{EndpointAddress, EndpointType},
|
||||
};
|
||||
use usbd_serial::CdcAcmClass;
|
||||
|
||||
const RXFLVL: u32 = 1 << 4;
|
||||
const USBSUSP: u32 = 1 << 11;
|
||||
const USBRST: u32 = 1 << 12;
|
||||
const ENUMDNE: u32 = 1 << 13;
|
||||
const IEPINT: u32 = 1 << 18;
|
||||
const WKUPINT: u32 = 1 << 31;
|
||||
|
||||
struct RxPacket {
|
||||
endpoint: usize,
|
||||
setup: bool,
|
||||
bytes: Vec<u8>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct BusState {
|
||||
offline: bool,
|
||||
fail_poll: bool,
|
||||
read_error: Option<UsbError>,
|
||||
write_error: Option<UsbError>,
|
||||
events: VecDeque<PollResult>,
|
||||
input: [Option<Vec<u8>>; 4],
|
||||
output: [Option<Vec<u8>>; 4],
|
||||
sizes: [[u16; 4]; 2],
|
||||
stalled: [[bool; 4]; 2],
|
||||
bulk_in: usize,
|
||||
bulk_out: usize,
|
||||
// Hardware FIFO is distinct from the driver's one-packet endpoint buffer
|
||||
// (`output`). RXFLVL stays asserted if that buffer prevents popping the FIFO.
|
||||
rx_fifo: VecDeque<RxPacket>,
|
||||
setup: u16,
|
||||
gintmsk: u32,
|
||||
nvic_enabled: bool,
|
||||
nvic_pending: bool,
|
||||
endpoints_configured: bool,
|
||||
}
|
||||
impl BusState {
|
||||
fn raw_status(&self) -> u32 {
|
||||
self.events.iter().fold(
|
||||
if self.rx_fifo.is_empty() { 0 } else { RXFLVL },
|
||||
|status, event| {
|
||||
status
|
||||
| match event {
|
||||
// synopsys-usb-otg returns Reset only for ENUMDNE.
|
||||
PollResult::Reset => ENUMDNE,
|
||||
PollResult::Suspend => USBSUSP,
|
||||
PollResult::Resume => WKUPINT,
|
||||
PollResult::Data { ep_in_complete, .. } if *ep_in_complete != 0 => IEPINT,
|
||||
_ => 0,
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
fn latch_interrupt(&mut self) {
|
||||
self.nvic_pending |= self.raw_status() & self.gintmsk != 0;
|
||||
}
|
||||
}
|
||||
#[derive(Clone, Default)]
|
||||
pub(crate) struct Host(Arc<Mutex<BusState>>);
|
||||
pub(crate) struct PacketBus(Arc<Mutex<BusState>>);
|
||||
impl super::UsbBusHealth for PacketBus {
|
||||
fn is_offline(&self) -> bool {
|
||||
self.0.lock().unwrap().offline
|
||||
}
|
||||
fn disable(&self) {
|
||||
let mut state = self.0.lock().unwrap();
|
||||
state.offline = true;
|
||||
state.nvic_enabled = false;
|
||||
state.nvic_pending = false;
|
||||
}
|
||||
}
|
||||
impl UsbBus for PacketBus {
|
||||
fn alloc_ep(
|
||||
&mut self,
|
||||
direction: UsbDirection,
|
||||
address: Option<EndpointAddress>,
|
||||
kind: EndpointType,
|
||||
size: u16,
|
||||
_: u8,
|
||||
) -> Result<EndpointAddress, UsbError> {
|
||||
let mut state = self.0.lock().unwrap();
|
||||
let sizes = &mut state.sizes[usize::from(direction == UsbDirection::In)];
|
||||
let index = match address {
|
||||
Some(address) => address.index(),
|
||||
None => (1..sizes.len())
|
||||
.find(|index| sizes[*index] == 0)
|
||||
.ok_or(UsbError::EndpointOverflow)?,
|
||||
};
|
||||
if index >= sizes.len() || sizes[index] != 0 {
|
||||
return Err(UsbError::InvalidEndpoint);
|
||||
}
|
||||
sizes[index] = size;
|
||||
if kind == EndpointType::Bulk {
|
||||
match direction {
|
||||
UsbDirection::In => state.bulk_in = index,
|
||||
UsbDirection::Out => state.bulk_out = index,
|
||||
}
|
||||
}
|
||||
Ok(EndpointAddress::from_parts(index, direction))
|
||||
}
|
||||
fn enable(&mut self) {
|
||||
let mut state = self.0.lock().unwrap();
|
||||
state.gintmsk = RXFLVL | USBSUSP | USBRST | ENUMDNE | IEPINT | WKUPINT;
|
||||
state.nvic_enabled = true;
|
||||
}
|
||||
fn reset(&self) {
|
||||
let mut state = self.0.lock().unwrap();
|
||||
state.input.fill(None);
|
||||
// configure_all flushes FIFOs but does not reset the bulk OUT software
|
||||
// EndpointBuffer::has_data. Transport cleanup must discard that packet.
|
||||
let bulk_out = state.bulk_out;
|
||||
let buffered = state.output[bulk_out].take();
|
||||
state.output.fill(None);
|
||||
state.output[bulk_out] = buffered;
|
||||
state.events.clear();
|
||||
state.rx_fifo.clear();
|
||||
state.setup = 0;
|
||||
state.endpoints_configured = true;
|
||||
}
|
||||
fn set_device_address(&self, _: u8) {}
|
||||
fn write(&self, address: EndpointAddress, bytes: &[u8]) -> Result<usize, UsbError> {
|
||||
let mut state = self.0.lock().unwrap();
|
||||
assert!(!state.offline, "write after terminal USB fault");
|
||||
if let Some(error) = state.write_error.take() {
|
||||
return Err(error);
|
||||
}
|
||||
if bytes.len() > usize::from(state.sizes[1][address.index()]) {
|
||||
return Err(UsbError::BufferOverflow);
|
||||
}
|
||||
let packet = &mut state.input[address.index()];
|
||||
if packet.is_some() {
|
||||
return Err(UsbError::WouldBlock);
|
||||
}
|
||||
*packet = Some(bytes.to_vec());
|
||||
Ok(bytes.len())
|
||||
}
|
||||
fn read(&self, address: EndpointAddress, bytes: &mut [u8]) -> Result<usize, UsbError> {
|
||||
let mut state = self.0.lock().unwrap();
|
||||
assert!(!state.offline, "read after terminal USB fault");
|
||||
if let Some(error) = state.read_error.take() {
|
||||
return Err(error);
|
||||
}
|
||||
let packet = state.output[address.index()]
|
||||
.as_ref()
|
||||
.ok_or(UsbError::WouldBlock)?;
|
||||
if packet.len() > bytes.len() {
|
||||
return Err(UsbError::BufferOverflow);
|
||||
}
|
||||
let packet = state.output[address.index()].take().unwrap();
|
||||
state.setup &= !(1 << address.index());
|
||||
bytes[..packet.len()].copy_from_slice(&packet);
|
||||
Ok(packet.len())
|
||||
}
|
||||
fn set_stalled(&self, address: EndpointAddress, stalled: bool) {
|
||||
self.0.lock().unwrap().stalled[usize::from(address.is_in())][address.index()] = stalled;
|
||||
}
|
||||
fn is_stalled(&self, address: EndpointAddress) -> bool {
|
||||
self.0.lock().unwrap().stalled[usize::from(address.is_in())][address.index()]
|
||||
}
|
||||
fn suspend(&self) {}
|
||||
fn resume(&self) {}
|
||||
fn poll(&self) -> PollResult {
|
||||
let mut state = self.0.lock().unwrap();
|
||||
assert!(!state.offline, "poll after terminal USB fault");
|
||||
if state.fail_poll {
|
||||
state.offline = true;
|
||||
state.nvic_enabled = false;
|
||||
state.nvic_pending = false;
|
||||
return PollResult::None;
|
||||
}
|
||||
// Like synopsys-usb-otg 0.4.0, polling reads raw status, not GINTMSK.
|
||||
// Non-RX sources remain serviceable even with a blocked FIFO head.
|
||||
let mut ep_in_complete = 0;
|
||||
if let Some(event) = state.events.pop_front() {
|
||||
match event {
|
||||
PollResult::Data {
|
||||
ep_in_complete: complete,
|
||||
..
|
||||
} => ep_in_complete = complete,
|
||||
_ => return event,
|
||||
}
|
||||
}
|
||||
if let Some(packet) = state.rx_fifo.front()
|
||||
&& state.output[packet.endpoint].is_none()
|
||||
{
|
||||
let packet = state.rx_fifo.pop_front().unwrap();
|
||||
if packet.setup {
|
||||
state.setup |= 1 << packet.endpoint;
|
||||
}
|
||||
state.output[packet.endpoint] = Some(packet.bytes);
|
||||
}
|
||||
let ep_setup = state.setup;
|
||||
let ep_out = state
|
||||
.output
|
||||
.iter()
|
||||
.enumerate()
|
||||
.fold(0, |mask, (index, packet)| {
|
||||
mask | if packet.is_some() { 1 << index } else { 0 }
|
||||
})
|
||||
& !ep_setup;
|
||||
// A still-asserted level can latch before the following gate write.
|
||||
state.latch_interrupt();
|
||||
if ep_out | ep_in_complete | ep_setup != 0 {
|
||||
PollResult::Data {
|
||||
ep_out,
|
||||
ep_in_complete,
|
||||
ep_setup,
|
||||
}
|
||||
} else {
|
||||
PollResult::None
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Host {
|
||||
pub(crate) fn fail_poll(&self) {
|
||||
self.0.lock().unwrap().fail_poll = true;
|
||||
}
|
||||
pub(crate) fn fail_read(&self) {
|
||||
self.0.lock().unwrap().read_error = Some(UsbError::InvalidState);
|
||||
}
|
||||
pub(crate) fn fail_write(&self) {
|
||||
self.0.lock().unwrap().write_error = Some(UsbError::InvalidState);
|
||||
}
|
||||
pub(crate) fn enumeration_done(&self) {
|
||||
let mut state = self.0.lock().unwrap();
|
||||
state.events.push_front(PollResult::Reset);
|
||||
state.latch_interrupt();
|
||||
}
|
||||
|
||||
pub(crate) fn event(&self, event: PollResult) {
|
||||
let mut state = self.0.lock().unwrap();
|
||||
state.events.push_back(event);
|
||||
state.latch_interrupt();
|
||||
}
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn events_empty(&self) -> bool {
|
||||
self.0.lock().unwrap().events.is_empty()
|
||||
}
|
||||
fn control(&self, link: &mut UsbMavlink<'_, PacketBus>, request: [u8; 8]) {
|
||||
self.queue_control(request);
|
||||
link.poll();
|
||||
assert_eq!(self.take_input(0), Some(Vec::new()));
|
||||
link.poll();
|
||||
}
|
||||
pub(crate) fn queue_control(&self, request: [u8; 8]) {
|
||||
let mut state = self.0.lock().unwrap();
|
||||
// EP0 shares the hardware FIFO. Never inject SETUP past a blocked bulk head.
|
||||
state.rx_fifo.push_back(RxPacket {
|
||||
endpoint: 0,
|
||||
setup: true,
|
||||
bytes: request.to_vec(),
|
||||
});
|
||||
state.latch_interrupt();
|
||||
}
|
||||
pub(crate) fn configure(&self, link: &mut UsbMavlink<'_, PacketBus>, configured: bool) {
|
||||
self.control(link, [0, 9, u8::from(configured), 0, 0, 0, 0, 0]);
|
||||
}
|
||||
pub(crate) fn dtr(&self, link: &mut UsbMavlink<'_, PacketBus>, open: bool) {
|
||||
self.control(link, [0x21, 0x22, u8::from(open), 0, 0, 0, 0, 0]);
|
||||
}
|
||||
pub(crate) fn write_packet(&self, bytes: &[u8]) {
|
||||
assert!(
|
||||
self.try_write_packet(bytes),
|
||||
"host must retry when the RX FIFO is full"
|
||||
);
|
||||
}
|
||||
pub(crate) fn try_write_packet(&self, bytes: &[u8]) -> bool {
|
||||
assert!(bytes.len() <= 64);
|
||||
let mut state = self.0.lock().unwrap();
|
||||
// Driver allocation: two 16-word OUT buffers + 30 words. Model packet
|
||||
// data and one receive status word, not cycle-level USB handshakes.
|
||||
let occupied: usize = state
|
||||
.rx_fifo
|
||||
.iter()
|
||||
.map(|packet| packet.bytes.len().div_ceil(4) + 1)
|
||||
.sum();
|
||||
if occupied + bytes.len().div_ceil(4) + 1 > 62 {
|
||||
return false;
|
||||
}
|
||||
let endpoint = state.bulk_out;
|
||||
state.rx_fifo.push_back(RxPacket {
|
||||
endpoint,
|
||||
setup: false,
|
||||
bytes: bytes.to_vec(),
|
||||
});
|
||||
state.latch_interrupt();
|
||||
true
|
||||
}
|
||||
fn take_input(&self, endpoint: usize) -> Option<Vec<u8>> {
|
||||
let mut state = self.0.lock().unwrap();
|
||||
let packet = state.input[endpoint].take()?;
|
||||
state.events.push_back(PollResult::Data {
|
||||
ep_out: 0,
|
||||
ep_in_complete: 1 << endpoint,
|
||||
ep_setup: 0,
|
||||
});
|
||||
state.latch_interrupt();
|
||||
Some(packet)
|
||||
}
|
||||
|
||||
pub(crate) fn set_rx_interrupt_enabled(&self, enabled: bool) {
|
||||
let mut state = self.0.lock().unwrap();
|
||||
state.gintmsk = if enabled {
|
||||
state.gintmsk | RXFLVL
|
||||
} else {
|
||||
state.gintmsk & !RXFLVL
|
||||
};
|
||||
// Masking never acknowledges pending status. Rearm sees the current level.
|
||||
state.latch_interrupt();
|
||||
}
|
||||
|
||||
#[allow(dead_code)] // Also shared with transport-only tests without a scheduler.
|
||||
pub(crate) fn rx_interrupt_enabled(&self) -> bool {
|
||||
self.0.lock().unwrap().gintmsk & RXFLVL != 0
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn nvic_enabled(&self) -> bool {
|
||||
self.0.lock().unwrap().nvic_enabled
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn interrupt_status(&self) -> (u32, u32) {
|
||||
let state = self.0.lock().unwrap();
|
||||
(state.raw_status(), state.gintmsk)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn enter_irq(&self) -> bool {
|
||||
let mut state = self.0.lock().unwrap();
|
||||
state.latch_interrupt();
|
||||
if !state.nvic_enabled || !state.nvic_pending {
|
||||
return false;
|
||||
}
|
||||
state.nvic_pending = false; // Exception entry, not an enable/disable side effect.
|
||||
true
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn receive_backlog(&self) -> (usize, bool) {
|
||||
let state = self.0.lock().unwrap();
|
||||
(state.rx_fifo.len(), state.output[state.bulk_out].is_some())
|
||||
}
|
||||
|
||||
/// Host ACK without polling device software. The device must service TX completion.
|
||||
pub(crate) fn take_packet(&self) -> Option<Vec<u8>> {
|
||||
let state = self.0.lock().unwrap();
|
||||
if !state.endpoints_configured {
|
||||
return None;
|
||||
}
|
||||
let endpoint = state.bulk_in;
|
||||
drop(state);
|
||||
self.take_input(endpoint)
|
||||
}
|
||||
pub(crate) fn collect_packets(&self, link: &mut UsbMavlink<'_, PacketBus>) -> Vec<Vec<u8>> {
|
||||
let mut packets = Vec::new();
|
||||
for _ in 0..16 {
|
||||
let Some(packet) = self.take_packet() else {
|
||||
return packets;
|
||||
};
|
||||
packets.push(packet);
|
||||
link.poll();
|
||||
}
|
||||
panic!("transmission did not finish");
|
||||
}
|
||||
}
|
||||
pub(crate) fn with_link(test: impl FnOnce(&Host, &mut UsbMavlink<'_, PacketBus>)) {
|
||||
let host = Host::default();
|
||||
let allocator = UsbBusAllocator::new(PacketBus(host.0.clone()));
|
||||
let class = CdcAcmClass::new(&allocator, 64);
|
||||
let device = UsbDeviceBuilder::new(&allocator, UsbVidPid(0x1209, 1))
|
||||
.max_packet_size_0(64)
|
||||
.unwrap()
|
||||
.build();
|
||||
let mut link = UsbMavlink::new(device, class);
|
||||
host.enumeration_done();
|
||||
link.poll();
|
||||
host.configure(&mut link, true);
|
||||
host.dtr(&mut link, true);
|
||||
link.with_rx_interrupt_gate(
|
||||
|link| assert!(link.is_open()),
|
||||
|enabled| host.set_rx_interrupt_enabled(enabled),
|
||||
);
|
||||
// Finish initialization's latched exceptions before scheduling test traffic.
|
||||
while host.enter_irq() {
|
||||
link.with_rx_interrupt_gate(
|
||||
|link| link.poll(),
|
||||
|enabled| host.set_rx_interrupt_enabled(enabled),
|
||||
);
|
||||
}
|
||||
test(&host, &mut link);
|
||||
}
|
||||
@@ -0,0 +1,391 @@
|
||||
//! Level-triggered interrupt scheduling around the real USB transport operations.
|
||||
extern crate std;
|
||||
use super::*;
|
||||
use crate::f722::usb;
|
||||
use kaptan_mavlink::{encoder::MavlinkEncoder, generated::kaptan::FILE_DATA_DATA};
|
||||
use kaptan_tx::FrameClass;
|
||||
use std::{vec, vec::Vec};
|
||||
use usb::test_bus as usb_bus;
|
||||
use usb_bus::{Host, PacketBus, with_link};
|
||||
use usb_device::bus::PollResult;
|
||||
|
||||
fn poll_irq(host: &Host, usb: &mut usb::UsbMavlink<'_, PacketBus>) -> bool {
|
||||
usb.with_rx_interrupt_gate(
|
||||
|usb| usb.poll(),
|
||||
|enabled| host.set_rx_interrupt_enabled(enabled),
|
||||
)
|
||||
}
|
||||
fn exchange(
|
||||
host: &Host,
|
||||
usb: &mut usb::UsbMavlink<'_, PacketBus>,
|
||||
frame: Option<MavlinkFrame>,
|
||||
) -> IoBatch {
|
||||
exchange_usb(
|
||||
usb,
|
||||
frame.map(|frame| PendingFrame {
|
||||
class: FrameClass::Critical,
|
||||
frame,
|
||||
}),
|
||||
|enabled| host.set_rx_interrupt_enabled(enabled),
|
||||
)
|
||||
}
|
||||
fn file_frame(length: usize) -> MavlinkFrame {
|
||||
let mut data = FILE_DATA_DATA::DEFAULT;
|
||||
data.data[..length - 21].fill(0x5a);
|
||||
let frame = MavlinkEncoder::new(1, 1).encode(&data);
|
||||
assert_eq!(frame.raw_bytes().len(), length);
|
||||
frame
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn register_rmw_changes_only_rxflvlm_and_preserves_adjacent_registers() {
|
||||
for bit in 0..32 {
|
||||
for initial in [1u32 << bit, !(1u32 << bit)] {
|
||||
let mut registers = [0xa5a5_5a5a, initial, 0x5a5a_a5a5];
|
||||
for enabled in [false, false, true, true, false] {
|
||||
// SAFETY: this aligned RAM word substitutes for the RW register.
|
||||
// No concurrent access exists and RAM needs no device barrier.
|
||||
unsafe { set_rx_fifo_interrupt_enabled(&mut registers[1], enabled) };
|
||||
assert_eq!(registers[1] & !(1 << 4), initial & !(1 << 4));
|
||||
assert_eq!(registers[1] & (1 << 4) != 0, enabled);
|
||||
assert_eq!(registers[0], 0xa5a5_5a5a);
|
||||
assert_eq!(registers[2], 0x5a5a_a5a5);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The asserted, enabled priority-8 vector wins over the priority-7 consumer.
|
||||
fn step(
|
||||
host: &Host,
|
||||
usb: &mut usb::UsbMavlink<'_, PacketBus>,
|
||||
ready: &mut bool,
|
||||
frames: &mut Vec<MavlinkFrame>,
|
||||
) -> bool {
|
||||
if host.enter_irq() {
|
||||
*ready |= poll_irq(host, usb);
|
||||
false
|
||||
} else if core::mem::take(ready) {
|
||||
let batch = exchange(host, usb, None);
|
||||
*ready |= batch.pending;
|
||||
if let Some(frame) = batch.usb {
|
||||
frames.push(*frame.raw());
|
||||
}
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn level_triggered_burst_starves_legacy_consumer_but_gated_consumer_recovers_exact_frames() {
|
||||
for gate in [false, true] {
|
||||
with_link(|host, usb| {
|
||||
let mut encoder = MavlinkEncoder::new(42, 7);
|
||||
let expected = core::array::from_fn::<_, 3, _>(|_| encoder.heartbeat());
|
||||
for frame in expected {
|
||||
host.write_packet(frame.raw_bytes());
|
||||
}
|
||||
let mut ready = false;
|
||||
let mut frames = Vec::new();
|
||||
let mut workers = 0;
|
||||
for _ in 0..32 {
|
||||
if gate {
|
||||
workers += usize::from(step(host, usb, &mut ready, &mut frames));
|
||||
} else if host.enter_irq() {
|
||||
ready |= usb.poll(); // Original handler: no RX source gate.
|
||||
} else if core::mem::take(&mut ready) {
|
||||
workers += 1;
|
||||
let batch = exchange(host, usb, None);
|
||||
ready |= batch.pending;
|
||||
if let Some(frame) = batch.usb {
|
||||
frames.push(*frame.raw());
|
||||
}
|
||||
}
|
||||
}
|
||||
if gate {
|
||||
assert_eq!(frames, expected);
|
||||
assert!(workers > 0);
|
||||
assert_eq!(host.receive_backlog(), (0, false));
|
||||
assert!(host.rx_interrupt_enabled());
|
||||
assert!(host.nvic_enabled());
|
||||
} else {
|
||||
assert_eq!(
|
||||
workers, 0,
|
||||
"level IRQ prevents the only consumer from running"
|
||||
);
|
||||
assert!(frames.is_empty());
|
||||
assert_eq!(
|
||||
host.receive_backlog(),
|
||||
(1, true),
|
||||
"hardware FIFO and driver buffer are both occupied"
|
||||
);
|
||||
assert!(
|
||||
host.enter_irq(),
|
||||
"source is still asserted after repeated polls"
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn split_coalesced_bad_crc_and_zlp_packets_preserve_rx_and_live_tx_boundaries() {
|
||||
with_link(|host, usb| {
|
||||
let incoming = file_frame(130);
|
||||
let mut encoder = MavlinkEncoder::new(42, 7);
|
||||
let first = encoder.heartbeat();
|
||||
let mut corrupt = encoder.heartbeat().raw_bytes().to_vec();
|
||||
*corrupt.last_mut().unwrap() ^= 1;
|
||||
let last = encoder.heartbeat();
|
||||
host.write_packet(&incoming.raw_bytes()[..64]);
|
||||
host.write_packet(&[]); // An OUT ZLP must not mask a partial MAVLink frame.
|
||||
host.write_packet(&incoming.raw_bytes()[64..128]);
|
||||
host.write_packet(
|
||||
&[
|
||||
&incoming.raw_bytes()[128..],
|
||||
first.raw_bytes(),
|
||||
&corrupt,
|
||||
last.raw_bytes(),
|
||||
]
|
||||
.concat(),
|
||||
);
|
||||
|
||||
let outgoing = file_frame(128);
|
||||
let after = encoder.heartbeat();
|
||||
assert!(exchange(host, usb, Some(outgoing)).accepted);
|
||||
assert!(host.enter_irq());
|
||||
assert!(poll_irq(host, usb));
|
||||
assert!(!host.rx_interrupt_enabled());
|
||||
assert!(host.nvic_enabled());
|
||||
let mut packets = vec![host.take_packet().unwrap()];
|
||||
assert!(
|
||||
host.enter_irq(),
|
||||
"IN completion interrupts while RX is masked"
|
||||
);
|
||||
assert!(poll_irq(host, usb));
|
||||
packets.push(host.take_packet().unwrap());
|
||||
assert_eq!(packets.iter().map(Vec::len).collect::<Vec<_>>(), [64, 64]);
|
||||
assert!(host.enter_irq());
|
||||
assert!(poll_irq(host, usb));
|
||||
packets.push(host.take_packet().unwrap());
|
||||
assert_eq!(packets[2], [], "TX ZLP progresses without the RX consumer");
|
||||
|
||||
let sent = exchange(host, usb, Some(after));
|
||||
assert!(sent.accepted);
|
||||
assert!(
|
||||
!sent.pending,
|
||||
"TX admission's IoBatch flag is not the IRQ policy"
|
||||
);
|
||||
assert!(
|
||||
!host.rx_interrupt_enabled(),
|
||||
"TX must preserve the actual unread RX gate"
|
||||
);
|
||||
if let Some(packet) = host.take_packet() {
|
||||
packets.push(packet);
|
||||
}
|
||||
|
||||
let partial = exchange(host, usb, None);
|
||||
assert!(partial.usb.is_none());
|
||||
assert!(!partial.pending);
|
||||
assert!(
|
||||
host.rx_interrupt_enabled(),
|
||||
"the partial framer is waiting for more bytes, not the worker"
|
||||
);
|
||||
|
||||
let mut ready = false;
|
||||
let mut frames = Vec::new();
|
||||
for _ in 0..64 {
|
||||
step(host, usb, &mut ready, &mut frames);
|
||||
if let Some(packet) = host.take_packet() {
|
||||
packets.push(packet);
|
||||
}
|
||||
}
|
||||
assert_eq!(frames, [incoming, first, last]);
|
||||
assert_eq!(
|
||||
packets.iter().map(Vec::len).collect::<Vec<_>>(),
|
||||
[64, 64, 0, 13]
|
||||
);
|
||||
assert_eq!(
|
||||
packets.concat(),
|
||||
[outgoing.raw_bytes(), after.raw_bytes()].concat()
|
||||
);
|
||||
assert!(host.nvic_enabled());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn final_poll_refill_stays_masked_and_rearm_preserves_already_pending_irq() {
|
||||
with_link(|host, usb| {
|
||||
let frame = MavlinkEncoder::new(42, 7).heartbeat();
|
||||
host.write_packet(&frame.raw_bytes()[..8]);
|
||||
assert!(host.enter_irq());
|
||||
usb.with_rx_interrupt_gate(
|
||||
|usb| {
|
||||
assert!(usb.poll());
|
||||
// Latch NVIC pending just before masking the peripheral source.
|
||||
host.write_packet(&frame.raw_bytes()[8..]);
|
||||
},
|
||||
|enabled| host.set_rx_interrupt_enabled(enabled),
|
||||
);
|
||||
let partial = exchange(host, usb, None);
|
||||
assert!(partial.usb.is_none());
|
||||
assert!(partial.pending, "the final poll refilled rx_remaining");
|
||||
assert!(!host.rx_interrupt_enabled());
|
||||
assert!(host.nvic_enabled());
|
||||
let complete = exchange(host, usb, None);
|
||||
assert_eq!(complete.usb.unwrap().raw(), &frame);
|
||||
assert!(!complete.pending);
|
||||
assert_eq!(host.receive_backlog(), (0, false));
|
||||
assert!(
|
||||
host.enter_irq(),
|
||||
"RX rearm preserves latched NVIC pending even after polling consumed the source"
|
||||
);
|
||||
assert!(!poll_irq(host, usb));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arrivals_on_either_side_of_rearm_are_not_lost() {
|
||||
for before in [true, false] {
|
||||
with_link(|host, usb| {
|
||||
let mut encoder = MavlinkEncoder::new(42, 7);
|
||||
let first = encoder.heartbeat();
|
||||
let next = encoder.heartbeat();
|
||||
host.write_packet(first.raw_bytes());
|
||||
assert!(host.enter_irq());
|
||||
assert!(poll_irq(host, usb));
|
||||
let batch = exchange_usb(usb, None, |enabled| {
|
||||
assert!(enabled);
|
||||
if before {
|
||||
host.write_packet(next.raw_bytes());
|
||||
}
|
||||
host.set_rx_interrupt_enabled(enabled);
|
||||
if !before {
|
||||
host.write_packet(next.raw_bytes());
|
||||
}
|
||||
});
|
||||
assert_eq!(batch.usb.unwrap().raw(), &first);
|
||||
assert!(!batch.pending);
|
||||
assert!(host.enter_irq());
|
||||
assert!(poll_irq(host, usb));
|
||||
assert_eq!(exchange(host, usb, None).usb.unwrap().raw(), &next);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn backpressure_keeps_reset_suspend_and_resume_interrupts_serviceable() {
|
||||
for reset in [true, false] {
|
||||
with_link(|host, usb| {
|
||||
let frame = file_frame(130);
|
||||
assert!(exchange(host, usb, Some(frame)).accepted);
|
||||
for _ in 0..3 {
|
||||
host.write_packet(&frame.raw_bytes()[..8]);
|
||||
}
|
||||
assert!(host.enter_irq());
|
||||
assert!(poll_irq(host, usb));
|
||||
assert!(host.enter_irq(), "one already-pending exception is allowed");
|
||||
assert!(poll_irq(host, usb));
|
||||
assert_eq!(host.receive_backlog(), (1, true));
|
||||
assert!(!host.enter_irq());
|
||||
let (_, original_mask) = host.interrupt_status();
|
||||
host.event(if reset {
|
||||
PollResult::Reset
|
||||
} else {
|
||||
PollResult::Suspend
|
||||
});
|
||||
assert!(host.enter_irq());
|
||||
poll_irq(host, usb);
|
||||
assert!(!usb.is_open());
|
||||
assert!(host.events_empty());
|
||||
assert!(host.nvic_enabled());
|
||||
assert_eq!(
|
||||
host.interrupt_status().1 & !(1 << 4),
|
||||
original_mask & !(1 << 4)
|
||||
);
|
||||
if reset {
|
||||
assert!(host.rx_interrupt_enabled());
|
||||
assert_eq!(host.receive_backlog(), (0, false));
|
||||
host.configure(usb, true);
|
||||
host.dtr(usb, true);
|
||||
assert!(
|
||||
host.collect_packets(usb).is_empty(),
|
||||
"reset discarded the old TX queue"
|
||||
);
|
||||
} else {
|
||||
assert!(!host.rx_interrupt_enabled(), "suspend retains unread RX");
|
||||
assert_eq!(host.receive_backlog(), (1, true));
|
||||
host.event(PollResult::Resume);
|
||||
assert!(host.enter_irq());
|
||||
poll_irq(host, usb);
|
||||
assert!(usb.is_open());
|
||||
assert_eq!(host.collect_packets(usb).concat(), frame.raw_bytes());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ep0_setup_cannot_bypass_a_blocked_bulk_fifo_head() {
|
||||
with_link(|host, usb| {
|
||||
let frame = MavlinkEncoder::new(42, 7).heartbeat();
|
||||
assert!(exchange(host, usb, Some(frame)).accepted);
|
||||
for _ in 0..3 {
|
||||
host.write_packet(frame.raw_bytes());
|
||||
}
|
||||
assert!(host.enter_irq());
|
||||
poll_irq(host, usb);
|
||||
assert!(host.enter_irq());
|
||||
poll_irq(host, usb);
|
||||
assert_eq!(host.receive_backlog(), (1, true));
|
||||
assert!(!host.enter_irq());
|
||||
host.queue_control([0x21, 0x22, 0, 0, 0, 0, 0, 0]); // DTR close behind bulk
|
||||
assert_eq!(host.receive_backlog(), (2, true));
|
||||
assert!(
|
||||
!host.enter_irq(),
|
||||
"EP0 arrival also uses the masked RX source"
|
||||
);
|
||||
let (raw, mask) = host.interrupt_status();
|
||||
assert_ne!(raw & (1 << 4), 0);
|
||||
assert_eq!(raw & mask, 0);
|
||||
host.take_packet().unwrap(); // IN completion can interrupt and poll raw RX.
|
||||
assert!(host.enter_irq());
|
||||
poll_irq(host, usb);
|
||||
assert_eq!(host.receive_backlog(), (2, true));
|
||||
assert!(
|
||||
usb.is_open(),
|
||||
"SETUP cannot jump over the blocked FIFO head"
|
||||
);
|
||||
assert_eq!(exchange(host, usb, None).usb.unwrap().raw(), &frame);
|
||||
exchange(host, usb, None);
|
||||
assert!(!usb.is_open(), "consumer progress finally allows DTR close");
|
||||
assert!(host.rx_interrupt_enabled());
|
||||
assert!(host.nvic_enabled());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn full_fifo_backpressures_host_then_consumer_progress_allows_retry() {
|
||||
with_link(|host, usb| {
|
||||
let frame = file_frame(64);
|
||||
for _ in 0..3 {
|
||||
assert!(host.try_write_packet(frame.raw_bytes()));
|
||||
}
|
||||
assert!(!host.try_write_packet(frame.raw_bytes()));
|
||||
let mut ready = false;
|
||||
let mut frames = Vec::new();
|
||||
for _ in 0..32 {
|
||||
step(host, usb, &mut ready, &mut frames);
|
||||
}
|
||||
assert_eq!(frames, [frame; 3]);
|
||||
assert!(host.try_write_packet(frame.raw_bytes()));
|
||||
for _ in 0..16 {
|
||||
step(host, usb, &mut ready, &mut frames);
|
||||
}
|
||||
assert_eq!(frames, [frame; 4]);
|
||||
assert_eq!(host.receive_backlog(), (0, false));
|
||||
assert!(host.rx_interrupt_enabled());
|
||||
assert!(host.nvic_enabled());
|
||||
});
|
||||
}
|
||||
@@ -1,210 +1,146 @@
|
||||
use std::{
|
||||
collections::VecDeque,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
use kaptan_mavlink::{
|
||||
MavlinkFrame,
|
||||
encoder::MavlinkEncoder,
|
||||
generated::kaptan::{FILE_DATA_DATA, MavMessage},
|
||||
};
|
||||
use kaptan_tx::{FrameClass, PendingFrame};
|
||||
use usb_device::{
|
||||
UsbDirection, UsbError,
|
||||
bus::{PollResult, UsbBus, UsbBusAllocator},
|
||||
device::{UsbDeviceBuilder, UsbVidPid},
|
||||
endpoint::{EndpointAddress, EndpointType},
|
||||
};
|
||||
use usbd_serial::CdcAcmClass;
|
||||
use usb_device::bus::PollResult;
|
||||
|
||||
#[path = "../src/f722/usb.rs"]
|
||||
mod usb;
|
||||
use usb::UsbMavlink;
|
||||
use usb::test_bus as usb_bus;
|
||||
use usb_bus::with_link;
|
||||
#[path = "../src/f722/serial.rs"]
|
||||
mod serial;
|
||||
use serial::{IrqTransmit, UartIo};
|
||||
|
||||
// One pending packet per endpoint, with host-controlled completion. Device and
|
||||
// CDC control requests run through the real usb-device/usbd-serial code.
|
||||
#[derive(Default)]
|
||||
struct BusState {
|
||||
events: VecDeque<PollResult>,
|
||||
input: [Option<Vec<u8>>; 4],
|
||||
output: [Option<Vec<u8>>; 4],
|
||||
sizes: [[u16; 4]; 2],
|
||||
stalled: [[bool; 4]; 2],
|
||||
bulk_in: usize,
|
||||
bulk_out: usize,
|
||||
struct MockUart {
|
||||
bytes: Vec<u8>,
|
||||
ready: bool,
|
||||
complete: bool,
|
||||
ready_irq: bool,
|
||||
complete_irq: bool,
|
||||
}
|
||||
impl UartIo for MockUart {
|
||||
fn tx_ready(&mut self) -> bool {
|
||||
self.ready
|
||||
}
|
||||
fn tx_complete(&mut self) -> bool {
|
||||
self.complete
|
||||
}
|
||||
fn write_byte(&mut self, byte: u8) {
|
||||
self.bytes.push(byte);
|
||||
self.complete = false;
|
||||
}
|
||||
fn interrupts(&mut self, ready: bool, complete: bool) {
|
||||
self.ready_irq = ready;
|
||||
self.complete_irq = complete;
|
||||
}
|
||||
}
|
||||
fn flush_uart(tx: &mut IrqTransmit, io: &mut MockUart) {
|
||||
io.ready = true;
|
||||
for _ in 0..2048 {
|
||||
if !io.ready_irq && !io.complete_irq {
|
||||
return;
|
||||
}
|
||||
let previous = io.bytes.len();
|
||||
io.complete = true;
|
||||
tx.on_interrupt(io);
|
||||
assert!(
|
||||
io.bytes.len() <= previous + 1,
|
||||
"IRQ work is bounded to one byte"
|
||||
);
|
||||
}
|
||||
panic!("UART did not drain its bounded queue");
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
struct Host(Arc<Mutex<BusState>>);
|
||||
#[test]
|
||||
fn moving_irq_queue_and_waiting_for_tc_preserves_exact_frame_bytes() {
|
||||
let mut io = MockUart {
|
||||
ready: true,
|
||||
..MockUart::default()
|
||||
};
|
||||
let mut tx = IrqTransmit::new();
|
||||
let mut encoder = MavlinkEncoder::new(1, 1);
|
||||
let frame = encoder.encode(&kaptan_mavlink::generated::kaptan::HEARTBEAT_DATA::default());
|
||||
assert!(tx.send(
|
||||
&mut io,
|
||||
PendingFrame {
|
||||
class: FrameClass::Vital,
|
||||
frame
|
||||
}
|
||||
));
|
||||
assert!(!tx.on_interrupt(&mut io));
|
||||
let mut moved = tx;
|
||||
for _ in 1..frame.raw_bytes().len() {
|
||||
assert!(!moved.on_interrupt(&mut io));
|
||||
}
|
||||
assert_eq!(io.bytes, frame.raw_bytes());
|
||||
assert!(!io.ready_irq && io.complete_irq);
|
||||
assert!(!moved.on_interrupt(&mut io));
|
||||
io.complete = true;
|
||||
assert!(moved.on_interrupt(&mut io));
|
||||
assert!(!io.ready_irq && !io.complete_irq);
|
||||
assert!(!moved.on_interrupt(&mut io));
|
||||
}
|
||||
|
||||
struct PacketBus(Arc<Mutex<BusState>>);
|
||||
|
||||
impl UsbBus for PacketBus {
|
||||
fn alloc_ep(
|
||||
&mut self,
|
||||
direction: UsbDirection,
|
||||
address: Option<EndpointAddress>,
|
||||
kind: EndpointType,
|
||||
size: u16,
|
||||
_: u8,
|
||||
) -> Result<EndpointAddress, UsbError> {
|
||||
let mut state = self.0.lock().unwrap();
|
||||
let sizes = &mut state.sizes[usize::from(direction == UsbDirection::In)];
|
||||
let index = match address {
|
||||
Some(address) => address.index(),
|
||||
None => (1..sizes.len())
|
||||
.find(|index| sizes[*index] == 0)
|
||||
.ok_or(UsbError::EndpointOverflow)?,
|
||||
#[test]
|
||||
fn usb_and_two_uarts_have_independent_sequences_queues_and_completion() {
|
||||
use kaptan_data_channel::{DataChannel, DataChannelRates, TelemetrySamples};
|
||||
use kaptan_time::Timestamp;
|
||||
with_link(|host, usb| {
|
||||
host.configure(usb, true);
|
||||
host.dtr(usb, true);
|
||||
let rates = DataChannelRates {
|
||||
imu_hz: 20,
|
||||
heartbeat_hz: 2,
|
||||
range_hz: None,
|
||||
optical_flow_hz: None,
|
||||
};
|
||||
if index >= sizes.len() || sizes[index] != 0 {
|
||||
return Err(UsbError::InvalidEndpoint);
|
||||
}
|
||||
sizes[index] = size;
|
||||
if kind == EndpointType::Bulk {
|
||||
match direction {
|
||||
UsbDirection::In => state.bulk_in = index,
|
||||
UsbDirection::Out => state.bulk_out = index,
|
||||
let mut channels = core::array::from_fn::<_, 3, _>(|_| {
|
||||
DataChannel::<100>::new(1, 1, rates, 100_000).unwrap()
|
||||
});
|
||||
let mut uart1 = IrqTransmit::new();
|
||||
let mut uart2 = IrqTransmit::new();
|
||||
let mut slow = MockUart::default();
|
||||
let mut fast = MockUart::default();
|
||||
let samples = TelemetrySamples::default();
|
||||
let mut usb_bytes = Vec::new();
|
||||
let mut expected = Vec::new();
|
||||
let mut dropped = false;
|
||||
for tick in 0..400 {
|
||||
let now = Timestamp::from_microseconds(tick * 10_000);
|
||||
let frames = channels
|
||||
.each_mut()
|
||||
.map(|channel| channel.on_service_tick(&samples, now));
|
||||
if let [Some(a), Some(b), Some(c)] = frames {
|
||||
assert_eq!(a.frame.raw_bytes(), b.frame.raw_bytes());
|
||||
assert_eq!(b.frame.raw_bytes(), c.frame.raw_bytes());
|
||||
expected.extend_from_slice(c.frame.raw_bytes());
|
||||
assert!(usb.send(a));
|
||||
dropped |= !uart1.send(&mut slow, b);
|
||||
assert!(uart2.send(&mut fast, c));
|
||||
}
|
||||
flush_uart(&mut uart2, &mut fast);
|
||||
usb.poll();
|
||||
usb_bytes.extend(host.collect_packets(usb).into_iter().flatten());
|
||||
}
|
||||
Ok(EndpointAddress::from_parts(index, direction))
|
||||
}
|
||||
|
||||
fn enable(&mut self) {}
|
||||
|
||||
fn reset(&self) {
|
||||
let mut state = self.0.lock().unwrap();
|
||||
state.input.fill(None);
|
||||
state.output.fill(None);
|
||||
state.events.clear();
|
||||
}
|
||||
|
||||
fn set_device_address(&self, _: u8) {}
|
||||
|
||||
fn write(&self, address: EndpointAddress, bytes: &[u8]) -> Result<usize, UsbError> {
|
||||
let mut state = self.0.lock().unwrap();
|
||||
if bytes.len() > usize::from(state.sizes[1][address.index()]) {
|
||||
return Err(UsbError::BufferOverflow);
|
||||
}
|
||||
let packet = &mut state.input[address.index()];
|
||||
if packet.is_some() {
|
||||
return Err(UsbError::WouldBlock);
|
||||
}
|
||||
*packet = Some(bytes.to_vec());
|
||||
Ok(bytes.len())
|
||||
}
|
||||
|
||||
fn read(&self, address: EndpointAddress, bytes: &mut [u8]) -> Result<usize, UsbError> {
|
||||
let mut state = self.0.lock().unwrap();
|
||||
let packet = state.output[address.index()]
|
||||
.as_ref()
|
||||
.ok_or(UsbError::WouldBlock)?;
|
||||
if packet.len() > bytes.len() {
|
||||
return Err(UsbError::BufferOverflow);
|
||||
}
|
||||
let packet = state.output[address.index()].take().unwrap();
|
||||
bytes[..packet.len()].copy_from_slice(&packet);
|
||||
Ok(packet.len())
|
||||
}
|
||||
|
||||
fn set_stalled(&self, address: EndpointAddress, stalled: bool) {
|
||||
self.0.lock().unwrap().stalled[usize::from(address.is_in())][address.index()] = stalled;
|
||||
}
|
||||
|
||||
fn is_stalled(&self, address: EndpointAddress) -> bool {
|
||||
self.0.lock().unwrap().stalled[usize::from(address.is_in())][address.index()]
|
||||
}
|
||||
|
||||
fn suspend(&self) {}
|
||||
fn resume(&self) {}
|
||||
|
||||
fn poll(&self) -> PollResult {
|
||||
self.0
|
||||
.lock()
|
||||
.unwrap()
|
||||
.events
|
||||
.pop_front()
|
||||
.unwrap_or(PollResult::None)
|
||||
}
|
||||
}
|
||||
|
||||
impl Host {
|
||||
fn event(&self, event: PollResult) {
|
||||
self.0.lock().unwrap().events.push_back(event);
|
||||
}
|
||||
|
||||
fn control(&self, link: &mut UsbMavlink<'_, PacketBus>, request: [u8; 8]) {
|
||||
self.0.lock().unwrap().output[0] = Some(request.to_vec());
|
||||
self.event(PollResult::Data {
|
||||
ep_out: 0,
|
||||
ep_in_complete: 0,
|
||||
ep_setup: 1,
|
||||
});
|
||||
link.poll();
|
||||
assert_eq!(self.take_input(0), Some(Vec::new()));
|
||||
link.poll();
|
||||
}
|
||||
|
||||
fn configure(&self, link: &mut UsbMavlink<'_, PacketBus>, configured: bool) {
|
||||
self.control(link, [0, 9, u8::from(configured), 0, 0, 0, 0, 0]);
|
||||
}
|
||||
|
||||
fn dtr(&self, link: &mut UsbMavlink<'_, PacketBus>, open: bool) {
|
||||
self.control(link, [0x21, 0x22, u8::from(open), 0, 0, 0, 0, 0]);
|
||||
}
|
||||
|
||||
fn write_packet(&self, bytes: &[u8]) {
|
||||
assert!(bytes.len() <= 64);
|
||||
let mut state = self.0.lock().unwrap();
|
||||
let endpoint = state.bulk_out;
|
||||
assert!(state.output[endpoint].is_none());
|
||||
state.output[endpoint] = Some(bytes.to_vec());
|
||||
state.events.push_back(PollResult::Data {
|
||||
ep_out: 1 << endpoint,
|
||||
ep_in_complete: 0,
|
||||
ep_setup: 0,
|
||||
});
|
||||
}
|
||||
|
||||
fn take_input(&self, endpoint: usize) -> Option<Vec<u8>> {
|
||||
let mut state = self.0.lock().unwrap();
|
||||
let packet = state.input[endpoint].take()?;
|
||||
state.events.push_back(PollResult::Data {
|
||||
ep_out: 0,
|
||||
ep_in_complete: 1 << endpoint,
|
||||
ep_setup: 0,
|
||||
});
|
||||
Some(packet)
|
||||
}
|
||||
|
||||
fn collect_packets(&self, link: &mut UsbMavlink<'_, PacketBus>) -> Vec<Vec<u8>> {
|
||||
let endpoint = self.0.lock().unwrap().bulk_in;
|
||||
let mut packets = Vec::new();
|
||||
for _ in 0..16 {
|
||||
let Some(packet) = self.take_input(endpoint) else {
|
||||
return packets;
|
||||
};
|
||||
packets.push(packet);
|
||||
link.poll();
|
||||
}
|
||||
panic!("transmission did not finish");
|
||||
}
|
||||
}
|
||||
|
||||
fn with_link(test: impl FnOnce(&Host, &mut UsbMavlink<'_, PacketBus>)) {
|
||||
let host = Host::default();
|
||||
let allocator = UsbBusAllocator::new(PacketBus(host.0.clone()));
|
||||
let class = CdcAcmClass::new(&allocator, 64);
|
||||
let device = UsbDeviceBuilder::new(&allocator, UsbVidPid(0x1209, 1))
|
||||
.max_packet_size_0(64)
|
||||
.unwrap()
|
||||
.build();
|
||||
let mut link = UsbMavlink::new(device, class);
|
||||
host.event(PollResult::Reset);
|
||||
link.poll();
|
||||
host.configure(&mut link, true);
|
||||
host.dtr(&mut link, true);
|
||||
assert!(link.is_open());
|
||||
test(&host, &mut link);
|
||||
assert!(
|
||||
dropped,
|
||||
"blocked UART must exhaust only its own bounded queue"
|
||||
);
|
||||
assert!(slow.bytes.is_empty());
|
||||
assert_eq!(usb_bytes, expected);
|
||||
assert_eq!(fast.bytes, expected);
|
||||
let fast_before = fast.bytes.clone();
|
||||
flush_uart(&mut uart1, &mut slow);
|
||||
assert!(!slow.bytes.is_empty());
|
||||
assert_eq!(
|
||||
fast.bytes, fast_before,
|
||||
"UART1 completions cannot advance UART2"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
fn pending(frame: MavlinkFrame) -> PendingFrame<MavlinkFrame> {
|
||||
@@ -269,7 +205,7 @@ fn drains_endpoint_backlog_after_its_poll_event_was_consumed() {
|
||||
assert_eq!(link.take_received().unwrap().raw(), &first);
|
||||
assert!(link.take_received().is_none());
|
||||
|
||||
assert!(host.0.lock().unwrap().events.is_empty());
|
||||
assert!(host.events_empty());
|
||||
assert!(link.poll());
|
||||
assert_eq!(link.take_received().unwrap().raw(), &second);
|
||||
});
|
||||
@@ -432,3 +368,30 @@ fn bus_reset_discards_transmit_and_receive_state() {
|
||||
assert_eq!(link.take_received().unwrap().raw(), &next);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bus_fault_closes_link_and_discards_pending_data() {
|
||||
for fail in [
|
||||
usb_bus::Host::fail_poll,
|
||||
usb_bus::Host::fail_read,
|
||||
usb_bus::Host::fail_write,
|
||||
] {
|
||||
with_link(|host, link| {
|
||||
let frame = file_frame(130);
|
||||
assert!(link.send(pending(frame)));
|
||||
host.write_packet(&frame.raw_bytes()[..8]);
|
||||
link.poll();
|
||||
assert!(link.take_received().is_none());
|
||||
fail(host);
|
||||
link.with_rx_interrupt_gate(
|
||||
|link| assert!(!link.poll()),
|
||||
|_| panic!("interrupt register access after terminal fault"),
|
||||
);
|
||||
assert!(!link.is_open());
|
||||
assert!(!link.send(pending(frame)));
|
||||
assert!(link.take_received().is_none());
|
||||
assert!(!link.poll());
|
||||
assert!(!host.nvic_enabled());
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user