refactor(firmware): centralize configuration and diagnostics

This commit is contained in:
Onur Özkan committed 2026-08-22 18:37:18 +03:00
1 parent 7048ed89d0
commit 387fdaad23
140 files changed
+6891 -4541

No files matched your search

+332 -185
View File
@@ -9,24 +9,39 @@ use super::peripherals::{
configure_uart4_pins_pa0_pa1, configure_usart1_rx_pin_pa10, configure_usart1_tx_pin_pa9,
configure_usart2_pins_pa2_pa3,
};
use kaptan_board::{BoardConfig, VerticalSensingKind};
use kaptan_config::Config;
use kaptan_data_channel::{
DataChannel, DataSources, LatestImuMailbox, LatestOpticalFlowMailbox, LatestRangeMailbox,
DataChannel, DataChannelRates, DataSources, LatestImuMailbox, LatestOpticalFlowMailbox,
LatestRangeMailbox,
};
use kaptan_drivers::{
lidar_lite_v3::{AcquisitionMode, LidarLiteV3, LidarLiteV3Service, LidarLiteV3ServiceEvent},
mtf01::Mtf01Service,
};
use kaptan_log::{
DeliveryClass, ErrorReport, EventHub, KaptanError, LogFilter, LogFlags, StaticError,
DeliveryClass, ErrorReport, KaptanError, LogFilter, LogSinkQueue, StaticError,
StaticErrorMailbox, TransportAdmission,
};
use kaptan_log_mavlink::{DiagnosticPayload, encode_diagnostic};
use kaptan_spi::SpiCompletion;
use stm32f7::stm32f722;
use crate::{BoardError, F722BoardRoutes, F722Profile};
use kaptan_config_mavlink::{
ConfigProtocol, Operation, ResponseVisitor, ResultKind, ServiceResponse, Target,
visit_response, wire,
};
use kaptan_mavlink::{
MavlinkVersion,
message::{Message, kaptan},
receiver::IntegrityMavlinkReceiver,
};
use crate::{BoardError, F7ImmutableConfig, F722Hardware, F722Profile, F722StatusLedRoute};
use super::config::{
F722ConfigManager, F722ConfigStore, F722Nor, UserConfigModel, project_runtime,
validate_runtime_values,
};
use super::{
VerticalRoute, clock,
data_channel::{
@@ -38,44 +53,26 @@ use super::{
};
const UART4_CLOCK_HZ: u32 = 54_000_000;
const MTF01_BAUD: u32 = 115_200;
const I2C1_CONFIG: I2c1Config = I2c1Config::from_timing_register(0x2040_4768);
const LIDAR_MODE: AcquisitionMode = AcquisitionMode::WithReceiverBiasCorrection;
const LIDAR_STARTUP_TICKS: u8 = 3;
const ERROR_RECORDS_PER_SECOND: u8 = 10;
const LOG_RECORDS_PER_SECOND: u8 = 5;
const SCHEDULED_FRAMES_PER_SECOND: usize = 62;
const CONFIG_SESSION_LEASE_TICKS: u16 = (SERVICE_HZ * 10) as u16;
const SCHEDULED_FRAME_MAXIMUM_BYTES: usize = 48;
const MAVLINK_V2_UNSIGNED_OVERHEAD_BYTES: usize = 12;
const KAPTAN_ERROR_FRAME_MAXIMUM_BYTES: usize =
kaptan_mavlink::message::kaptan::KAPTAN_ERROR_DATA::ENCODED_LEN
+ MAVLINK_V2_UNSIGNED_OVERHEAD_BYTES;
const KAPTAN_LOG_FRAME_MAXIMUM_BYTES: usize =
kaptan_mavlink::message::kaptan::KAPTAN_LOG_DATA::ENCODED_LEN
+ MAVLINK_V2_UNSIGNED_OVERHEAD_BYTES;
// The static F722 telemetry profile is bounded at 62 scheduled frames/s.
// It permits ten `KAPTAN_ERROR` and five `KAPTAN_LOG` frames/s. Both maxima
// are 163 payload bytes plus 12 unsigned MAVLink V2 framing bytes, so
// 62×48 + 15×175 = 5,601 bytes/s. MAVLink signing is not enabled in this
// telemetry-only image; any future signing profile must re-budget its
// signature overhead before it can enable the endpoint.
pub(crate) const WORST_CASE_OBSERVABILITY_BYTES_PER_SECOND: usize = SCHEDULED_FRAMES_PER_SECOND
* SCHEDULED_FRAME_MAXIMUM_BYTES
+ ERROR_RECORDS_PER_SECOND as usize * KAPTAN_ERROR_FRAME_MAXIMUM_BYTES
+ LOG_RECORDS_PER_SECOND as usize * KAPTAN_LOG_FRAME_MAXIMUM_BYTES;
// User/immutable validation permits at most one scheduled frame per 100 Hz
// service tick. Configuration replies and diagnostics are event-driven and
// use bounded queue admission; they do not invent a periodic link reservation.
pub(crate) const SCHEDULED_TELEMETRY_BYTES_PER_SECOND: usize =
SERVICE_HZ * SCHEDULED_FRAME_MAXIMUM_BYTES;
/// Returns whether the immutable board UART configuration can carry the fixed
/// telemetry and bounded diagnostic schedule at ten UART bits per byte.
pub(crate) const fn link_budget_fits(baud: u32) -> bool {
WORST_CASE_OBSERVABILITY_BYTES_PER_SECOND < baud as usize / 10
/// Returns whether the immutable board UART configuration can carry its
/// deterministic telemetry schedule at ten UART bits per byte.
pub(crate) const fn scheduled_link_budget_fits(baud: u32) -> bool {
SCHEDULED_TELEMETRY_BYTES_PER_SECOND < baud as usize / 10
}
static UART_COMPLETION: AtomicU8 = AtomicU8::new(0);
#[derive(Clone, Copy, Debug, KaptanError)]
enum F722TelemetryError {
Mtf01Uart,
TransmitMemory,
TransmitEngine,
TransmitTerminal,
}
// High-priority/ISR-adjacent producers never encode, lock, allocate, or send
// MAVLink. They retain the first fault text and coalesce later reports for
@@ -99,12 +96,18 @@ pub type F722ServiceTimer = Tim2Periodic;
///
/// Only the MAVLink worker accesses this state, so its priority ceiling is 9
/// rather than the IMU or vertical-sensor IRQ ceiling.
pub struct F722MavlinkState {
pub struct F722MavlinkState<C, I, const MAX_PAYLOAD_BYTES: usize>
where
C: Clone + Config,
I: F7ImmutableConfig,
{
data_channel: DataChannel<'static, F722MavlinkTransmitter, SERVICE_HZ>,
logs: EventHub<2, 4>,
budget_ticks: u8,
error_budget: u8,
log_budget: u8,
configuration: F722ConfigManager<C, I, MAX_PAYLOAD_BYTES>,
config_protocol: ConfigProtocol,
receiver: IntegrityMavlinkReceiver<2>,
config_lease_ticks: u16,
mavlink_logs: LogSinkQueue<1, 1>,
fault_mailbox_cursor: u8,
led: F722StatusLed,
}
@@ -121,81 +124,103 @@ pub struct F722LidarState {
///
/// This is not runtime state indirection: each field is moved exactly once
/// into the single RTIC owner that accesses it.
pub struct F722InitResources {
pub(crate) imu: F722Imu,
pub(crate) mtf01: Option<F722Mtf01>,
pub(crate) mtf01_uart: Option<F722Mtf01Uart>,
pub(crate) timer: F722ServiceTimer,
pub(crate) lidar: F722LidarState,
pub(crate) mavlink: F722MavlinkState,
pub struct F722InitResources<C, I, const MAX_PAYLOAD_BYTES: usize>
where
C: Clone + Config,
I: F7ImmutableConfig,
{
#[doc(hidden)]
pub imu: F722Imu,
#[doc(hidden)]
pub mtf01: Option<F722Mtf01>,
#[doc(hidden)]
pub mtf01_uart: Option<F722Mtf01Uart>,
#[doc(hidden)]
pub timer: F722ServiceTimer,
#[doc(hidden)]
pub lidar: F722LidarState,
#[doc(hidden)]
pub mavlink: F722MavlinkState<C, I, MAX_PAYLOAD_BYTES>,
}
/// Validates the relationship between portable metadata and selected sensor
/// route before platform initialization claims a peripheral.
pub(crate) fn validate_board_routes(
config: BoardConfig,
routes: F722BoardRoutes,
) -> Result<(), BoardError> {
let telemetry = config.telemetry().ok_or(BoardError::Initialization)?;
if telemetry.data_channel.service_hz != SERVICE_HZ {
/// Validates user-controlled runtime values against immutable electrical and
/// platform constraints before initialization claims a non-safety peripheral.
pub(crate) fn validate_board_config<C, I>(
config: &C,
immutable: &I,
hardware: F722Hardware,
) -> Result<(), BoardError>
where
C: Clone + Config,
I: F7ImmutableConfig,
{
<C as kaptan_config::Config>::validate(config).map_err(|_| BoardError::Configuration)?;
validate_runtime_values(config, immutable).map_err(|_| BoardError::Configuration)?;
let immutable = immutable.f722();
if immutable.service_hz != SERVICE_HZ {
return Err(BoardError::UnsupportedServiceFrequency);
}
let expected = match routes.vertical {
VerticalRoute::None => VerticalSensingKind::None,
VerticalRoute::Mtf01 => VerticalSensingKind::RangeAndOpticalFlow,
VerticalRoute::LidarLiteV3 => VerticalSensingKind::Range,
let vertical_baud_is_valid = match (hardware.vertical(), hardware.vertical_sensor_baud()) {
(VerticalRoute::Mtf01, Some(baud)) => baud != 0,
(VerticalRoute::None | VerticalRoute::LidarLiteV3, None) => true,
_ => false,
};
if telemetry.vertical_sensing != expected {
if !vertical_baud_is_valid {
return Err(BoardError::VerticalSensorCapabilityMismatch);
}
if telemetry.imu != routes.imu() {
return Err(BoardError::ImuRouteMismatch);
}
if telemetry.imu_mount != routes.imu_mount() {
return Err(BoardError::ImuMountMismatch);
}
if config.mavlink.port != routes.mavlink_port() {
return Err(BoardError::MavlinkRouteMismatch);
}
if !link_budget_fits(config.mavlink.baud) {
if !scheduled_link_budget_fits(hardware.mavlink_baud()) {
return Err(BoardError::TelemetryLinkOvercommitted);
}
Ok(())
}
pub(crate) fn initialize(
pub(crate) fn initialize<C, I, const MAX_PAYLOAD_BYTES: usize>(
mut core: cortex_m::Peripherals,
device: stm32f722::Peripherals,
config: BoardConfig,
routes: F722BoardRoutes,
) -> Result<F722InitResources, BoardError> {
factory: C,
immutable: I,
hardware: F722Hardware,
) -> Result<F722InitResources<C, I, MAX_PAYLOAD_BYTES>, BoardError>
where
C: Clone + Config,
I: F7ImmutableConfig,
{
let clock = clock::initialize(&mut core, &device).map_err(|_| BoardError::Initialization)?;
// The profile is written once before any interrupt is enabled. ISR helpers
// read it with Acquire ordering and only access the initialized route.
interrupts::activate_profile(routes.profile());
interrupts::activate_profile(hardware.profile());
match routes.profile() {
F722Profile::Mpu6000Spi1Usart1 => {
initialize_mpu6000(device, clock, config, routes.vertical)
}
F722Profile::Icm42688Spi3Usart2 => {
initialize_icm42688(device, clock, config, routes.vertical)
}
match hardware.profile() {
F722Profile::Mpu6000Spi1Usart1 => initialize_mpu6000::<C, I, MAX_PAYLOAD_BYTES>(
device, clock, factory, immutable, hardware,
),
F722Profile::Icm42688Spi3Usart2 => initialize_icm42688::<C, I, MAX_PAYLOAD_BYTES>(
device, clock, factory, immutable, hardware,
),
}
}
fn initialize_mpu6000(
fn initialize_mpu6000<C, I, const MAX_PAYLOAD_BYTES: usize>(
device: stm32f722::Peripherals,
clock: DwtMicrosecondClock,
config: BoardConfig,
vertical: VerticalRoute,
) -> Result<F722InitResources, BoardError> {
factory: C,
immutable: I,
hardware: F722Hardware,
) -> Result<F722InitResources<C, I, MAX_PAYLOAD_BYTES>, BoardError>
where
C: Clone + Config,
I: F7ImmutableConfig,
{
let configuration =
recover_configuration::<C, I, MAX_PAYLOAD_BYTES>(device.FLASH, factory, immutable)?;
let config = configuration.active().clone();
configure_spi1_pins_pa5_pa6_pa7(&device.RCC, &device.GPIOA);
configure_exti4_pin_pa4(&device.GPIOA);
configure_exti4(&device.RCC, device.SYSCFG, device.EXTI);
configure_usart1_tx_pin_pa9(&device.GPIOA);
configure_usart1_rx_pin_pa10(&device.GPIOA);
let vertical = hardware.vertical();
configure_vertical_pins(&device.RCC, &device.GPIOA, &device.GPIOB, vertical);
let (range, mtf01, mtf01_uart) = initialize_vertical(
@@ -205,19 +230,25 @@ fn initialize_mpu6000(
clock.fork(),
clock.fork(),
vertical,
hardware.vertical_sensor_baud(),
)?;
let telemetry = config.telemetry().ok_or(BoardError::Initialization)?;
let imu = F722Imu::mpu6000(
&device.RCC,
device.SPI1,
device.DMA2,
device.GPIOB,
clock.fork(),
telemetry.imu_mount,
hardware.imu_mount(),
hardware
.mpu6000_config()
.ok_or(BoardError::Initialization)?,
);
let transmitter =
F722MavlinkTransmitter::usart1(&device.RCC, device.USART1, config.mavlink.baud)
F722MavlinkTransmitter::usart1(&device.RCC, device.USART1, hardware.mavlink_baud())
.map_err(|_| BoardError::Initialization)?;
if hardware.status_led() != F722StatusLedRoute::Pd2ActiveLow {
return Err(BoardError::Initialization);
}
let led = F722StatusLed::Kakute(super::peripherals::ActiveLowLedPd2::new(
&device.RCC,
device.GPIOD,
@@ -226,6 +257,7 @@ fn initialize_mpu6000(
&device.RCC,
device.TIM2,
config,
immutable,
vertical,
imu,
range,
@@ -233,15 +265,24 @@ fn initialize_mpu6000(
mtf01_uart,
transmitter,
led,
configuration,
)
}
fn initialize_icm42688(
fn initialize_icm42688<C, I, const MAX_PAYLOAD_BYTES: usize>(
device: stm32f722::Peripherals,
clock: DwtMicrosecondClock,
config: BoardConfig,
vertical: VerticalRoute,
) -> Result<F722InitResources, BoardError> {
factory: C,
immutable: I,
hardware: F722Hardware,
) -> Result<F722InitResources<C, I, MAX_PAYLOAD_BYTES>, BoardError>
where
C: Clone + Config,
I: F7ImmutableConfig,
{
let configuration =
recover_configuration::<C, I, MAX_PAYLOAD_BYTES>(device.FLASH, factory, immutable)?;
let config = configuration.active().clone();
configure_spi3_pins_pc10_pc11_pc12(&device.RCC, &device.GPIOC);
configure_exti3_pin_pc3(&device.GPIOC);
configure_exti3(&device.RCC, device.SYSCFG, device.EXTI);
@@ -253,6 +294,7 @@ fn initialize_icm42688(
.ahb1enr()
.modify(|_, write| write.gpioaen().set_bit());
configure_usart2_pins_pa2_pa3(&device.GPIOA);
let vertical = hardware.vertical();
configure_vertical_pins(&device.RCC, &device.GPIOA, &device.GPIOB, vertical);
let (range, mtf01, mtf01_uart) = initialize_vertical(
@@ -262,24 +304,28 @@ fn initialize_icm42688(
clock.fork(),
clock.fork(),
vertical,
hardware.vertical_sensor_baud(),
)?;
let telemetry = config.telemetry().ok_or(BoardError::Initialization)?;
let imu = F722Imu::icm42688(
&device.RCC,
device.SPI3,
device.DMA1,
&device.GPIOA,
clock.fork(),
telemetry.imu_mount,
hardware.imu_mount(),
);
let transmitter =
F722MavlinkTransmitter::usart2(&device.RCC, device.USART2, config.mavlink.baud)
F722MavlinkTransmitter::usart2(&device.RCC, device.USART2, hardware.mavlink_baud())
.map_err(|_| BoardError::Initialization)?;
if hardware.status_led() != F722StatusLedRoute::Pa13ActiveHigh {
return Err(BoardError::Initialization);
}
let led = F722StatusLed::Geprc(GeprcStatusLed::new(&device.RCC, device.GPIOA));
finish(
&device.RCC,
device.TIM2,
config,
immutable,
vertical,
imu,
range,
@@ -287,14 +333,16 @@ fn initialize_icm42688(
mtf01_uart,
transmitter,
led,
configuration,
)
}
#[allow(clippy::too_many_arguments)]
fn finish(
fn finish<C, I, const MAX_PAYLOAD_BYTES: usize>(
rcc: &stm32f722::RCC,
tim2: stm32f722::TIM2,
config: BoardConfig,
config: C,
immutable: I,
vertical: VerticalRoute,
imu: F722Imu,
range: Option<F722Range>,
@@ -302,10 +350,33 @@ fn finish(
mtf01_uart: Option<Uart4>,
transmitter: F722MavlinkTransmitter,
led: F722StatusLed,
) -> Result<F722InitResources, BoardError> {
configuration: F722ConfigManager<C, I, MAX_PAYLOAD_BYTES>,
) -> Result<F722InitResources<C, I, MAX_PAYLOAD_BYTES>, BoardError>
where
C: Clone + Config,
I: F7ImmutableConfig,
{
let runtime = project_runtime(&config, &immutable).map_err(|_| BoardError::Configuration)?;
let immutable = immutable.f722();
let system_id = runtime.system_id;
let component_id = runtime.component_id;
let rates = DataChannelRates {
service_hz: immutable.service_hz,
raw_imu_hz: usize::from(runtime.raw_imu_hz),
range_hz: runtime.range_hz.map(usize::from),
optical_flow_hz: runtime.optical_flow_hz.map(usize::from),
heartbeat_hz: usize::from(runtime.heartbeat_hz),
};
let mavlink_log_filter = if runtime.mavlink_logging_enabled {
LogFilter::up_to(runtime.mavlink_maximum_log_severity)
} else {
LogFilter::disabled()
};
let data_channel = DataChannel::new(
transmitter,
config,
system_id,
component_id,
rates,
vertical_data_config(vertical),
DataSources {
imu: &SAMPLES,
@@ -325,20 +396,35 @@ fn finish(
},
mavlink: F722MavlinkState {
data_channel,
logs: EventHub::new(LogFilter::new(
config
.telemetry()
.ok_or(BoardError::Initialization)?
.maximum_log_level,
)),
budget_ticks: 0,
error_budget: ERROR_RECORDS_PER_SECOND,
log_budget: LOG_RECORDS_PER_SECOND,
configuration,
config_protocol: ConfigProtocol::new(Target::new(system_id, component_id)),
receiver: IntegrityMavlinkReceiver::new(),
config_lease_ticks: 0,
mavlink_logs: LogSinkQueue::new(mavlink_log_filter),
fault_mailbox_cursor: 0,
led,
},
})
}
fn recover_configuration<C, I, const MAX_PAYLOAD_BYTES: usize>(
flash: stm32f722::FLASH,
factory: C,
immutable: I,
) -> Result<F722ConfigManager<C, I, MAX_PAYLOAD_BYTES>, BoardError>
where
C: Clone + Config,
I: F7ImmutableConfig,
{
let candidate = UserConfigModel::<C, I>::new(factory);
let store =
F722ConfigStore::<C, I, MAX_PAYLOAD_BYTES>::new(F722Nor::new(flash), candidate.clone());
F722ConfigManager::<C, I, MAX_PAYLOAD_BYTES>::recover_compatible_or_factory(
candidate, immutable, store,
)
.map_err(|_| BoardError::Configuration)
}
fn configure_vertical_pins(
rcc: &stm32f722::RCC,
gpioa: &stm32f722::GPIOA,
@@ -361,17 +447,21 @@ fn initialize_vertical(
mtf01_clock: DwtMicrosecondClock,
range_clock: DwtMicrosecondClock,
vertical: VerticalRoute,
vertical_sensor_baud: Option<u32>,
) -> Result<VerticalResources, BoardError> {
match vertical {
VerticalRoute::None => Ok((None, None, None)),
VerticalRoute::Mtf01 => Ok((
None,
Some(Mtf01Service::new(mtf01_clock)),
Some(
Uart4::new(rcc, uart4, UART4_CLOCK_HZ, MTF01_BAUD)
.map_err(|_| BoardError::Initialization)?,
),
)),
VerticalRoute::Mtf01 => {
let baud = vertical_sensor_baud.ok_or(BoardError::Initialization)?;
Ok((
None,
Some(Mtf01Service::new(mtf01_clock)),
Some(
Uart4::new(rcc, uart4, UART4_CLOCK_HZ, baud)
.map_err(|_| BoardError::Initialization)?,
),
))
}
VerticalRoute::LidarLiteV3 => Ok((
Some(LidarLiteV3Service::new(
range_clock,
@@ -410,7 +500,7 @@ fn configure_exti3(rcc: &stm32f722::RCC, syscfg: stm32f722::SYSCFG, exti: stm32f
/// Services the MTF-01 UART route. It is a task-local resource, so it needs
/// no RTIC lock and cannot extend another resource's priority ceiling.
pub(crate) fn on_uart4_interrupt(mtf01: &mut Option<F722Mtf01>) {
pub fn on_uart4_interrupt(mtf01: &mut Option<F722Mtf01>) {
let Some(mtf01) = mtf01.as_mut() else {
return;
};
@@ -434,7 +524,7 @@ pub(crate) fn on_uart4_interrupt(mtf01: &mut Option<F722Mtf01>) {
}
impl F722LidarState {
pub(crate) fn on_i2c_event(&mut self, error: bool) {
pub fn on_i2c_event(&mut self, error: bool) {
let Some(range) = self.range.as_mut() else {
return;
};
@@ -448,7 +538,7 @@ impl F722LidarState {
publish_lidar_event(event);
}
pub(crate) fn on_timer(&mut self) {
pub fn on_timer(&mut self) {
if let Some(range) = self.range.as_mut() {
if self.startup_ticks < LIDAR_STARTUP_TICKS {
self.startup_ticks += 1;
@@ -465,7 +555,7 @@ impl F722LidarState {
}
/// Advances the IMU state machine in its dedicated worker-local resource.
pub(crate) fn process_imu(imu: &mut F722Imu, events: u8) {
pub fn process_imu(imu: &mut F722Imu, events: u8) {
const START: u8 = 1 << 4;
const TRANSFER_FAULT: u8 = 1 << 2;
const TRANSFER_COMPLETE: u8 = 1 << 1;
@@ -499,21 +589,31 @@ fn accept_imu_event(event: ImuEvent) {
}
}
impl F722MavlinkState {
impl<C, I, const MAX_PAYLOAD_BYTES: usize> F722MavlinkState<C, I, MAX_PAYLOAD_BYTES>
where
C: Clone + Config,
I: F7ImmutableConfig,
{
/// Handles bounded DMA and timer work for the telemetry image.
pub(crate) fn process_worker_events(&mut self, events: u8) {
pub fn process_worker_events(&mut self, events: u8) {
const TIMER: u8 = 1 << 0;
const RECEIVED: u8 = 1 << 1;
const DMA: u8 = 1 << 2;
if events & RECEIVED != 0 {
self.drain_config_frames();
}
if events & DMA != 0 {
let completion = take_mavlink_completion();
let _ = self.data_channel.on_dma_complete(completion);
self.data_channel.on_dma_complete(completion);
if completion == kaptan_dma::TxCompletion::Complete {
self.led.toggle();
}
}
if events & TIMER != 0 {
self.tick_config_lease();
// The board is initialized and transmitting physical telemetry,
// but has no flight runtime or output authority. `STANDBY` with
// an unarmed base mode communicates that accurately; `UNINIT`
@@ -521,66 +621,81 @@ impl F722MavlinkState {
self.data_channel
.on_service_tick(kaptan_mavlink::message::MavState::MAV_STATE_STANDBY, false);
self.drain_driver_faults();
self.budget_ticks = self.budget_ticks.saturating_add(1);
if self.budget_ticks == SERVICE_HZ as u8 {
self.budget_ticks = 0;
self.error_budget = ERROR_RECORDS_PER_SECOND;
self.log_budget = LOG_RECORDS_PER_SECOND;
self.drain_one_mavlink_log_record();
}
}
pub fn receive_config_interrupt(&mut self, event: ReceiveInterrupt) -> bool {
match event {
ReceiveInterrupt::Byte(byte) => self.receiver.receive_byte(byte),
ReceiveInterrupt::Fault => {
self.receiver.reset();
self.config_lease_ticks = 0;
self.config_protocol.expire(&mut self.configuration);
false
}
self.drain_one_log_record();
ReceiveInterrupt::NotPending => false,
}
}
fn drain_config_frames(&mut self) {
while let Some(frame) = self.receiver.take_received() {
if frame.system_id() == 0 || frame.component_id() == 0 {
continue;
}
let Ok(message) =
kaptan::MavMessage::parse(MavlinkVersion::V2, frame.message_id(), frame.payload())
else {
continue;
};
let Ok(command) = wire::decode_command(&message) else {
continue;
};
let principal =
kaptan_config_mavlink::Principal::new(frame.system_id(), frame.component_id());
let mut sender = ConfigResponseSender {
data_channel: &mut self.data_channel,
};
let mut refresh_lease = false;
let _: Result<(), ()> = self.config_protocol.dispatch_from_composition(
&mut self.configuration,
principal,
command,
|response| {
if let ServiceResponse::Result(reply) = response
&& reply.result != ResultKind::Rejected
{
refresh_lease |= reply.operation != Operation::Query;
}
visit_response(response, &mut sender)
},
);
if refresh_lease {
self.config_lease_ticks = CONFIG_SESSION_LEASE_TICKS;
}
}
}
fn tick_config_lease(&mut self) {
if self.config_lease_ticks == 0 {
return;
}
self.config_lease_ticks -= 1;
if self.config_lease_ticks == 0 {
self.config_protocol.expire(&mut self.configuration);
}
}
fn drain_driver_faults(&mut self) {
self.drain_fault_mailbox(&IMU_FAULTS);
self.drain_fault_mailbox(&LIDAR_FAULTS);
self.drain_fault_mailbox(&MTF01_FAULTS);
self.drain_transport_faults();
let _ = self.mavlink_logs.drain_next_error_mailbox(
[&IMU_FAULTS, &LIDAR_FAULTS, &MTF01_FAULTS],
&mut self.fault_mailbox_cursor,
);
}
fn drain_transport_faults(&mut self) {
while let Some(fault) = self.data_channel.take_transport_fault() {
let message = match fault {
kaptan_mavlink::transmitter::TransmitFaultKind::Memory => {
F722TelemetryError::TransmitMemory.static_error()
}
kaptan_mavlink::transmitter::TransmitFaultKind::Engine => {
F722TelemetryError::TransmitEngine.static_error()
}
kaptan_mavlink::transmitter::TransmitFaultKind::Terminal => {
F722TelemetryError::TransmitTerminal.static_error()
}
};
let _ = self.logs.publish_error_static(LogFlags::NONE, message);
}
}
fn drain_fault_mailbox(&mut self, mailbox: &StaticErrorMailbox) {
let (report, coalesced) = mailbox.take();
let Some(message) = report else {
return;
};
let flags = if coalesced {
LogFlags::COALESCED
} else {
LogFlags::NONE
};
let _ = self.logs.publish_error_static(flags, message);
}
fn drain_one_log_record(&mut self) {
let Some(record) = self.logs.peek() else {
return;
};
let delivery = record.delivery();
if (delivery == DeliveryClass::Critical && self.error_budget == 0)
|| (delivery == DeliveryClass::BestEffort && self.log_budget == 0)
{
return;
}
fn drain_one_mavlink_log_record(&mut self) {
let data_channel = &mut self.data_channel;
let mut accepted = None;
let _ = self.logs.submit_next(|record| {
let _ = self.mavlink_logs.submit_next(|record| {
let class = match record.delivery() {
DeliveryClass::Critical => kaptan_mavlink::transmitter::FrameClass::Critical,
DeliveryClass::BestEffort => kaptan_mavlink::transmitter::FrameClass::BestEffort,
@@ -591,24 +706,56 @@ impl F722MavlinkState {
DiagnosticPayload::Error(payload) => data_channel.send_custom(class, payload),
};
let outcome = if admission.is_retained() {
accepted = Some(record.delivery());
TransportAdmission::Accepted
} else if record.delivery() == DeliveryClass::BestEffort {
TransportAdmission::DropBestEffort
} else {
TransportAdmission::Retry
};
(outcome, ())
});
match accepted {
Some(DeliveryClass::Critical) => {
self.error_budget = self.error_budget.saturating_sub(1);
}
Some(DeliveryClass::BestEffort) => {
self.log_budget = self.log_budget.saturating_sub(1);
}
None => {}
}
}
}
struct ConfigResponseSender<'a> {
data_channel: &'a mut DataChannel<'static, F722MavlinkTransmitter, SERVICE_HZ>,
}
impl ResponseVisitor for ConfigResponseSender<'_> {
type Error = ();
fn info(&mut self, response: &kaptan::KAPTAN_CONFIG_INFO_DATA) -> Result<(), Self::Error> {
self.send(response)
}
fn result(&mut self, response: &kaptan::KAPTAN_CONFIG_RESULT_DATA) -> Result<(), Self::Error> {
self.send(response)
}
fn descriptor(
&mut self,
response: &kaptan::KAPTAN_CONFIG_DESCRIPTOR_DATA,
) -> Result<(), Self::Error> {
self.send(response)
}
fn enum_option(
&mut self,
response: &kaptan::KAPTAN_CONFIG_ENUM_OPTION_DATA,
) -> Result<(), Self::Error> {
self.send(response)
}
fn value(&mut self, response: &kaptan::KAPTAN_CONFIG_VALUE_DATA) -> Result<(), Self::Error> {
self.send(response)
}
}
impl ConfigResponseSender<'_> {
fn send<M: kaptan_mavlink::message::MessageData>(&mut self, response: &M) -> Result<(), ()> {
self.data_channel
.send_custom(kaptan_mavlink::transmitter::FrameClass::Critical, response)
.is_retained()
.then_some(())
.ok_or(())
}
}
@@ -0,0 +1,421 @@
//! Board-owned user configuration support and the generic F722 flash journal.
use core::marker::PhantomData;
use kaptan_config::{
Config, ConfigManager, ConfigModel, ConfigValue, ConfigViolation, DualSlotJournal, DualSlotNor,
FieldDescriptor, FieldId, FieldWriteError, ValidationRule,
};
use stm32f7::stm32f722;
use crate::{F7ImmutableConfig, Severity};
const MAVLINK_SYSTEM_ID: FieldId = FieldId::new(101);
const MAVLINK_COMPONENT_ID: FieldId = FieldId::new(102);
const RAW_IMU_HZ: FieldId = FieldId::new(103);
const HEARTBEAT_HZ: FieldId = FieldId::new(104);
const MAVLINK_MAXIMUM_LOG_SEVERITY: FieldId = FieldId::new(105);
const RANGE_HZ: FieldId = FieldId::new(106);
const OPTICAL_FLOW_HZ: FieldId = FieldId::new(107);
const MAVLINK_LOGGING_ENABLED: FieldId = FieldId::new(108);
/// Narrow boot projection consumed only by fixed F722 services.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(super) struct F722UserRuntime {
pub(super) system_id: u8,
pub(super) component_id: u8,
pub(super) mavlink_logging_enabled: bool,
pub(super) mavlink_maximum_log_severity: Severity,
pub(super) raw_imu_hz: u8,
pub(super) range_hz: Option<u8>,
pub(super) optical_flow_hz: Option<u8>,
pub(super) heartbeat_hz: u8,
}
/// Generic [`ConfigModel`] adapter for one board-composed aggregate.
#[derive(Clone)]
pub(crate) struct UserConfigModel<C, I> {
candidate: C,
_immutable: PhantomData<I>,
}
impl<C, I> UserConfigModel<C, I> {
pub(crate) const fn new(candidate: C) -> Self {
Self {
candidate,
_immutable: PhantomData,
}
}
}
impl<C, I> ConfigModel for UserConfigModel<C, I>
where
C: Clone + Config,
I: F7ImmutableConfig,
{
type Active = C;
type Context = I;
const SCHEMA_ID: [u8; 16] = <C as Config>::SCHEMA_ID;
fn descriptor(index: u16) -> Option<&'static FieldDescriptor> {
C::descriptor(usize::from(index))
}
fn field_count() -> u16 {
assert!(
C::FIELD_COUNT <= u16::MAX as usize,
"configuration schema exceeds wire field-count domain"
);
C::FIELD_COUNT as u16
}
fn field_value(&self, id: FieldId) -> Option<ConfigValue> {
self.candidate.field_value(id)
}
fn active_field_value(active: &Self::Active, id: FieldId) -> Option<ConfigValue> {
active.field_value(id)
}
fn set_field(&mut self, id: FieldId, value: ConfigValue) -> Result<(), FieldWriteError> {
self.candidate.set_field(id, value)
}
fn validate(&self, context: &Self::Context) -> Result<Self::Active, ConfigViolation> {
<C as Config>::validate(&self.candidate)?;
validate_runtime_values(&self.candidate, context)?;
Ok(self.candidate.clone())
}
}
pub(super) fn validate_runtime_values<C, I>(
candidate: &C,
immutable: &I,
) -> Result<(), ConfigViolation>
where
C: Clone + Config,
I: F7ImmutableConfig,
{
project_runtime(candidate, immutable).map(|_| ())
}
pub(super) fn project_runtime<C, I>(
candidate: &C,
immutable: &I,
) -> Result<F722UserRuntime, ConfigViolation>
where
C: Config,
I: F7ImmutableConfig,
{
let immutable = immutable.f722();
let system_id = required_u8(candidate, MAVLINK_SYSTEM_ID)?;
if system_id == 0 {
return Err(ConfigViolation::field(
MAVLINK_SYSTEM_ID,
ValidationRule::Zero,
));
}
let component_id = required_u8(candidate, MAVLINK_COMPONENT_ID)?;
if component_id == 0 {
return Err(ConfigViolation::field(
MAVLINK_COMPONENT_ID,
ValidationRule::Zero,
));
}
let raw_imu_hz = required_u8(candidate, RAW_IMU_HZ)?;
let heartbeat_hz = required_u8(candidate, HEARTBEAT_HZ)?;
let mavlink_logging_enabled = required_bool(candidate, MAVLINK_LOGGING_ENABLED)?;
let severity = required_u8(candidate, MAVLINK_MAXIMUM_LOG_SEVERITY)?;
let mavlink_maximum_log_severity =
Severity::try_from_wire(severity).ok_or(ConfigViolation::field(
MAVLINK_MAXIMUM_LOG_SEVERITY,
ValidationRule::InvalidEnumValue,
))?;
let range_hz = if immutable.hardware.has_range() {
Some(required_u8(candidate, RANGE_HZ)?)
} else {
None
};
let optical_flow_hz = if immutable.hardware.has_optical_flow() {
Some(required_u8(candidate, OPTICAL_FLOW_HZ)?)
} else {
None
};
let service_hz = immutable.service_hz;
let mut scheduled_hz = checked_rate(raw_imu_hz, RAW_IMU_HZ, service_hz)?
.checked_add(checked_rate(heartbeat_hz, HEARTBEAT_HZ, service_hz)?)
.ok_or(ConfigViolation::global(ValidationRule::Inconsistent))?;
scheduled_hz = add_optional_rate(range_hz, RANGE_HZ, service_hz, scheduled_hz)?;
scheduled_hz = add_optional_rate(optical_flow_hz, OPTICAL_FLOW_HZ, service_hz, scheduled_hz)?;
if scheduled_hz > service_hz {
return Err(ConfigViolation::global(ValidationRule::Inconsistent));
}
Ok(F722UserRuntime {
system_id,
component_id,
mavlink_logging_enabled,
mavlink_maximum_log_severity,
raw_imu_hz,
range_hz,
optical_flow_hz,
heartbeat_hz,
})
}
fn required_u8<C: Config>(candidate: &C, field: FieldId) -> Result<u8, ConfigViolation> {
match candidate.field_value(field) {
Some(ConfigValue::U32(value)) => u8::try_from(value)
.map_err(|_| ConfigViolation::field(field, ValidationRule::AboveMaximum)),
Some(_) | None => Err(ConfigViolation::field(field, ValidationRule::Inconsistent)),
}
}
fn required_bool<C: Config>(candidate: &C, field: FieldId) -> Result<bool, ConfigViolation> {
match candidate.field_value(field) {
Some(ConfigValue::Bool(value)) => Ok(value),
Some(_) | None => Err(ConfigViolation::field(field, ValidationRule::Inconsistent)),
}
}
fn checked_rate(rate: u8, field: FieldId, service_hz: usize) -> Result<usize, ConfigViolation> {
let rate = usize::from(rate);
if rate == 0 {
return Err(ConfigViolation::field(field, ValidationRule::Zero));
}
if rate > service_hz {
return Err(ConfigViolation::field(field, ValidationRule::AboveMaximum));
}
Ok(rate)
}
fn add_optional_rate(
rate: Option<u8>,
field: FieldId,
service_hz: usize,
scheduled_hz: usize,
) -> Result<usize, ConfigViolation> {
match rate {
Some(rate) => scheduled_hz
.checked_add(checked_rate(rate, field, service_hz)?)
.ok_or(ConfigViolation::global(ValidationRule::Inconsistent)),
None => Ok(scheduled_hz),
}
}
const F722_LARGE_SECTOR_BASE: usize = 0x0802_0000;
const F722_LARGE_SECTOR_BYTES: usize = 128 * 1024;
const F722_FIRST_LARGE_SECTOR: usize = 5;
const FLASH_KEY1: u32 = 0x4567_0123;
const FLASH_KEY2: u32 = 0xcdef_89ab;
const FLASH_ERROR_FLAGS: u32 = 0x01f2;
const BUSY_POLL_LIMIT: u32 = 1_000_000_000;
/// One schema-independent journal shared by every STM32F722 board image.
pub(crate) type F722ConfigStore<C, I, const MAX_PAYLOAD_BYTES: usize> =
DualSlotJournal<UserConfigModel<C, I>, 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<C, I, MAX_PAYLOAD_BYTES>>;
/// Failure of the STM32F722 embedded-flash controller contract.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum F722FlashError {
Bounds,
BusyTimeout,
Unlock,
Controller,
Verification,
}
/// Sole owner of the F722 flash controller used by the configuration journal.
pub(crate) struct F722Nor {
flash: stm32f722::FLASH,
}
unsafe extern "C" {
static __kaptan_config_slot0_start: u8;
static __kaptan_config_slot1_start: u8;
static __kaptan_config_journal_end: u8;
}
impl F722Nor {
pub(crate) const fn new(flash: stm32f722::FLASH) -> Self {
Self { flash }
}
fn slot_bounds(slot: usize) -> Result<(usize, usize), F722FlashError> {
// The shared F722 linker script defines these absolute symbols and
// asserts both slots are adjacent 128 KiB sectors after FLASH.
let bounds = [
(
core::ptr::addr_of!(__kaptan_config_slot0_start) as usize,
core::ptr::addr_of!(__kaptan_config_slot1_start) as usize,
),
(
core::ptr::addr_of!(__kaptan_config_slot1_start) as usize,
core::ptr::addr_of!(__kaptan_config_journal_end) as usize,
),
];
bounds.get(slot).copied().ok_or(F722FlashError::Bounds)
}
fn address(slot: usize, offset: usize, length: usize) -> Result<usize, F722FlashError> {
let (base, limit) = Self::slot_bounds(slot)?;
let slot_bytes = limit.checked_sub(base).ok_or(F722FlashError::Bounds)?;
let end = offset.checked_add(length).ok_or(F722FlashError::Bounds)?;
if end > slot_bytes {
return Err(F722FlashError::Bounds);
}
base.checked_add(offset).ok_or(F722FlashError::Bounds)
}
fn sector(slot: usize) -> Result<u8, F722FlashError> {
let (base, limit) = Self::slot_bounds(slot)?;
if limit.checked_sub(base) != Some(F722_LARGE_SECTOR_BYTES) {
return Err(F722FlashError::Bounds);
}
let offset = base
.checked_sub(F722_LARGE_SECTOR_BASE)
.ok_or(F722FlashError::Bounds)?;
if !offset.is_multiple_of(F722_LARGE_SECTOR_BYTES) {
return Err(F722FlashError::Bounds);
}
u8::try_from(F722_FIRST_LARGE_SECTOR + offset / F722_LARGE_SECTOR_BYTES)
.map_err(|_| F722FlashError::Bounds)
}
fn wait_ready(&self) -> Result<(), F722FlashError> {
let mut remaining = BUSY_POLL_LIMIT;
while self.flash.sr().read().bsy().is_busy() {
if remaining == 0 {
return Err(F722FlashError::BusyTimeout);
}
remaining -= 1;
core::hint::spin_loop();
}
Ok(())
}
fn unlock(&self) -> Result<(), F722FlashError> {
self.wait_ready()?;
if self.flash.cr().read().lock().is_locked() {
// SAFETY: these are the exact two full-width FPEC unlock keys from
// RM0431, written in the required order while the controller is idle.
self.flash
.keyr()
.write(|write| unsafe { write.key().bits(FLASH_KEY1) });
// SAFETY: this is the second full-width key in the same documented
// idle-controller unlock sequence described above.
self.flash
.keyr()
.write(|write| unsafe { write.key().bits(FLASH_KEY2) });
}
if self.flash.cr().read().lock().is_locked() {
return Err(F722FlashError::Unlock);
}
Ok(())
}
fn prepare(&self) -> Result<(), F722FlashError> {
self.unlock()?;
// SAFETY: all writable FLASH_SR bits in this mask are documented
// write-one-to-clear completion/error flags. Reserved bits remain zero.
self.flash
.sr()
.write(|write| unsafe { write.bits(FLASH_ERROR_FLAGS) });
Ok(())
}
fn finish(&self) -> Result<(), F722FlashError> {
self.wait_ready()?;
let status = self.flash.sr().read();
self.flash.cr().modify(|_, write| {
write.pg().clear_bit();
write.ser().clear_bit();
write.lock().locked()
});
if status.bits() & FLASH_ERROR_FLAGS != 0 {
return Err(F722FlashError::Controller);
}
if status.eop().is_active() {
self.flash.sr().write(|write| write.eop().clear());
}
Ok(())
}
}
impl DualSlotNor for F722Nor {
type Error = F722FlashError;
fn slot_size(&self) -> usize {
Self::slot_bounds(0).map_or(0, |(base, limit)| limit - base)
}
fn read(&mut self, slot: usize, offset: usize, output: &mut [u8]) -> Result<(), Self::Error> {
let address = Self::address(slot, offset, output.len())?;
for (index, byte) in output.iter_mut().enumerate() {
// SAFETY: `address..address + output.len()` was checked to lie in
// one of the two reserved flash slots. D-cache is disabled by F722 clock
// initialization before this store is constructed.
*byte = unsafe { core::ptr::read_volatile((address + index) as *const u8) };
}
Ok(())
}
fn erase_slot(&mut self, slot: usize) -> Result<(), Self::Error> {
let sector = Self::sector(slot)?;
self.prepare()?;
// SAFETY: RM0431 assigns sectors 6 and 7 to the two reserved 128 KiB
// ranges above. No application section is linked into either sector.
self.flash.cr().modify(|_, write| unsafe {
write
.pg()
.clear_bit()
.ser()
.set_bit()
.snb()
.bits(sector)
.psize()
.psize32()
.strt()
.start()
});
// RM0431 permits a DSB between asserting STRT and polling BSY so the
// control-register write cannot be observed out of order.
cortex_m::asm::dsb();
self.finish()?;
let mut probe = [0_u8; 4];
self.read(slot, 0, &mut probe)?;
if probe != [0xff; 4] {
return Err(F722FlashError::Verification);
}
Ok(())
}
fn program_word(&mut self, slot: usize, offset: usize, word: u32) -> Result<(), Self::Error> {
if !offset.is_multiple_of(4) {
return Err(F722FlashError::Bounds);
}
let address = Self::address(slot, offset, 4)?;
self.prepare()?;
self.flash
.cr()
.modify(|_, write| write.ser().clear_bit().psize().psize32().pg().set_bit());
// SAFETY: the checked address is aligned and contained in a reserved
// erased journal sector, and PG with x32 parallelism is active.
unsafe { core::ptr::write_volatile(address as *mut u32, word) };
// RM0431 requires the programming store to complete before BSY is
// polled or the PG/control state is changed.
cortex_m::asm::dsb();
self.finish()?;
// SAFETY: the same checked flash word is readable after BSY clears.
if unsafe { core::ptr::read_volatile(address as *const u32) } != word {
return Err(F722FlashError::Verification);
}
Ok(())
}
}
@@ -1,17 +1,17 @@
//! F722 transport and vertical-route adapters for the portable data channel.
use super::{VerticalRoute, peripherals};
use kaptan_data_channel::{DmaCompletion, FrameAdmission, FrameSink, TxHealth, VerticalDataConfig};
use kaptan_data_channel::{FrameSink, VerticalDataConfig};
use kaptan_dma::TxCompletion;
use kaptan_mavlink::{
MavlinkFrame,
encoder::DistanceSensorConfig,
transmitter::{FrameClass, MavlinkTransmitter, TransmitFaultKind},
transmitter::{EnqueueResult, FrameClass, MavlinkTransmitter},
};
// Board endpoint, baud, sensor topology, and mounting remain immutable
// build-time policy. They are not ground-station-editable in this telemetry
// image: the F722 has no authenticated config ingress or activation runtime.
// 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;
@@ -23,7 +23,7 @@ pub(crate) const SERVICE_TIMER_CONFIG: peripherals::Tim2PeriodicConfig =
auto_reload: 99,
};
/// Maps a selected vertical electrical route to development data-channel metadata.
/// Maps a selected vertical electrical route to configured data-channel metadata.
///
/// The values are telemetry descriptions only; they neither calibrate a flight
/// estimator nor authorize an `ExternalControlOnly` profile.
@@ -62,11 +62,9 @@ enum F722MavlinkTransport {
Usart2(MavlinkTransmitter<peripherals::DcacheDisabledDmaMemory, peripherals::Usart2TxDma, 4>),
}
/// Sole F722 MAVLink DMA owner plus unconfigurable transport-loss accounting.
/// Sole F722 MAVLink DMA owner.
pub(crate) struct F722MavlinkTransmitter {
inner: F722MavlinkTransport,
health: TxHealth,
pending_faults: u8,
}
impl F722MavlinkTransmitter {
@@ -85,8 +83,6 @@ impl F722MavlinkTransmitter {
memory,
peripherals::Usart1TxDma::new(rcc, uart),
)),
health: TxHealth::default(),
pending_faults: 0,
})
}
@@ -104,107 +100,35 @@ impl F722MavlinkTransmitter {
memory,
peripherals::Usart2TxDma::new(rcc, uart),
)),
health: TxHealth::default(),
pending_faults: 0,
})
}
}
impl FrameSink for F722MavlinkTransmitter {
fn send(&mut self, class: FrameClass, frame: MavlinkFrame) -> FrameAdmission {
let (admission, start_fault) = match &mut self.inner {
fn send(&mut self, class: FrameClass, frame: MavlinkFrame) -> EnqueueResult {
match &mut self.inner {
F722MavlinkTransport::Usart1(transmitter) => {
let outcome = transmitter.admit_and_start(class, frame);
(
outcome.admission,
outcome.start.err().map(|fault| fault.kind()),
)
let _ = outcome.start;
outcome.admission
}
F722MavlinkTransport::Usart2(transmitter) => {
let outcome = transmitter.admit_and_start(class, frame);
(
outcome.admission,
outcome.start.err().map(|fault| fault.kind()),
)
let _ = outcome.start;
outcome.admission
}
};
if !matches!(
admission,
kaptan_mavlink::transmitter::EnqueueResult::Enqueued
) {
self.health.admission_loss_count = self.health.admission_loss_count.saturating_add(1);
}
if let Some(fault) = start_fault {
self.record_fault(fault);
}
FrameAdmission::Retained(admission)
}
fn on_dma_complete(&mut self, completion: TxCompletion) -> DmaCompletion {
let start_fault = match &mut self.inner {
F722MavlinkTransport::Usart1(transmitter) => transmitter
.on_dma_complete(completion)
.err()
.map(|error| error.kind()),
F722MavlinkTransport::Usart2(transmitter) => transmitter
.on_dma_complete(completion)
.err()
.map(|error| error.kind()),
};
let terminal = matches!(
completion,
TxCompletion::TransferFault | TxCompletion::Halted
);
if let Some(fault) = start_fault {
self.record_fault(fault);
}
if terminal {
self.record_fault(TransmitFaultKind::Terminal);
}
if start_fault.is_some() || terminal {
DmaCompletion::Fault
} else {
DmaCompletion::Complete
}
}
fn health(&self) -> TxHealth {
self.health
}
fn take_transport_fault(&mut self) -> Option<TransmitFaultKind> {
let (bit, fault) = if self.pending_faults & 1 != 0 {
(1, TransmitFaultKind::Memory)
} else if self.pending_faults & 2 != 0 {
(2, TransmitFaultKind::Engine)
} else if self.pending_faults & 4 != 0 {
(4, TransmitFaultKind::Terminal)
} else {
return None;
};
self.pending_faults &= !bit;
Some(fault)
}
}
impl F722MavlinkTransmitter {
fn record_fault(&mut self, fault: TransmitFaultKind) {
self.health.dma_fault_count = self.health.dma_fault_count.saturating_add(1);
self.pending_faults |= match fault {
TransmitFaultKind::Memory => {
self.health.memory_fault_count = self.health.memory_fault_count.saturating_add(1);
1
fn on_dma_complete(&mut self, completion: TxCompletion) {
match &mut self.inner {
F722MavlinkTransport::Usart1(transmitter) => {
let _ = transmitter.on_dma_complete(completion);
}
TransmitFaultKind::Engine => {
self.health.engine_fault_count = self.health.engine_fault_count.saturating_add(1);
2
F722MavlinkTransport::Usart2(transmitter) => {
let _ = transmitter.on_dma_complete(completion);
}
TransmitFaultKind::Terminal => {
self.health.terminal_fault_count =
self.health.terminal_fault_count.saturating_add(1);
4
}
};
}
}
}
+50 -21
View File
@@ -3,8 +3,10 @@
/// 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.
macro_rules! f722_flight_application {
($routes:path, $config:path) => {
#[doc(hidden)]
#[macro_export]
macro_rules! __f722_flight_application {
($board:ty) => {
#[allow(unused_imports)]
use $crate::__platform_internal::cmrt as _;
#[allow(unused_imports)]
@@ -36,12 +38,29 @@ macro_rules! f722_flight_application {
#[allow(unused_imports)]
use $crate::__platform_internal::rtic_app::Mutex;
type BoardUserConfig = <$board as $crate::F7Board>::UserConfig;
type BoardImmutableConfig = <$board as $crate::F7Board>::ImmutableConfig;
const CONFIG_PAYLOAD_BYTES: usize = $crate::config_snapshot_length::<
BoardUserConfig,
>();
type BoardInitResources = F722InitResources<
BoardUserConfig,
BoardImmutableConfig,
CONFIG_PAYLOAD_BYTES,
>;
type BoardMavlinkState = F722MavlinkState<
BoardUserConfig,
BoardImmutableConfig,
CONFIG_PAYLOAD_BYTES,
>;
const IMU_EVENT_DRDY: u8 = 1 << 0;
const IMU_EVENT_TRANSFER_COMPLETE: u8 = 1 << 1;
const IMU_EVENT_TRANSFER_FAULT: u8 = 1 << 2;
const IMU_EVENT_TIMER: u8 = 1 << 3;
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.
@@ -55,7 +74,7 @@ macro_rules! f722_flight_application {
#[shared]
struct Shared {
mavlink: F722MavlinkState,
mavlink: BoardMavlinkState,
lidar: F722LidarState,
}
@@ -69,16 +88,20 @@ macro_rules! f722_flight_application {
#[init]
fn init(context: init::Context) -> (Shared, Local) {
let routes = $routes().unwrap_or_else(|_| safe_idle());
let F722InitResources {
let BoardInitResources {
imu,
mtf01,
mtf01_uart,
timer,
lidar,
mavlink,
} = initialize(context.core, context.device, $config(), routes)
.unwrap_or_else(|_| safe_idle());
} = initialize::<BoardUserConfig, BoardImmutableConfig, CONFIG_PAYLOAD_BYTES>(
context.core,
context.device,
<$board as $crate::F7Board>::FACTORY_USER_CONFIG,
<$board as $crate::F7Board>::IMMUTABLE_CONFIG,
)
.unwrap_or_else(|_| safe_idle());
schedule_imu(IMU_EVENT_START);
(
Shared {
@@ -200,20 +223,28 @@ macro_rules! f722_flight_application {
}
}
#[task(binds = USART1, priority = 8)]
fn mavlink_rx_usart1(_: mavlink_rx_usart1::Context) {
// `ExternalControlOnly` is rejected and production signing is
// not implemented, so this image has no inbound authority
// path. Acknowledge every RX condition to keep the peripheral
// quiescent without taking any RTIC-shared flight state.
let _ = acknowledge_mavlink_rx_usart1();
#[task(binds = USART1, priority = 8, shared = [mavlink])]
fn mavlink_rx_usart1(mut context: mavlink_rx_usart1::Context) {
let event = acknowledge_mavlink_rx_usart1();
if context
.shared
.mavlink
.lock(|mavlink| mavlink.receive_config_interrupt(event))
{
schedule_mavlink(MAVLINK_EVENT_RECEIVED);
}
}
#[task(binds = USART2, priority = 8)]
fn mavlink_rx_usart2(_: mavlink_rx_usart2::Context) {
// See USART1: inbound bytes are drained but never decoded or
// dispatched by the telemetry-only image.
let _ = acknowledge_mavlink_rx_usart2();
#[task(binds = USART2, priority = 8, shared = [mavlink])]
fn mavlink_rx_usart2(mut context: mavlink_rx_usart2::Context) {
let event = acknowledge_mavlink_rx_usart2();
if context
.shared
.mavlink
.lock(|mavlink| mavlink.receive_config_interrupt(event))
{
schedule_mavlink(MAVLINK_EVENT_RECEIVED);
}
}
#[task(binds = TIM2, priority = 4, local = [timer], shared = [lidar])]
@@ -276,5 +307,3 @@ macro_rules! f722_flight_application {
}
};
}
pub(crate) use f722_flight_application;
+22 -43
View File
@@ -7,6 +7,7 @@
mod board;
mod clock;
mod config;
mod data_channel;
mod firmware;
mod interrupts;
@@ -15,12 +16,13 @@ mod peripherals;
mod routes;
mod sensors;
use crate::{BoardError, F722BoardRoutes};
use kaptan_board::BoardConfig;
use kaptan_config::Config;
use crate::{BoardError, F7ImmutableConfig};
/// Completion classification crossing the RTIC interrupt template.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum F722ImuTransferCompletion {
pub enum F722ImuTransferCompletion {
Complete,
TransferFault,
NotPending,
@@ -43,59 +45,36 @@ pub(crate) enum VerticalRoute {
LidarLiteV3,
}
pub(crate) use board::{
pub use board::{
F722InitResources, F722LidarState, F722MavlinkState, F722Mtf01, F722Mtf01Uart,
F722ServiceTimer, on_uart4_interrupt, process_imu, record_mavlink_completion,
validate_board_routes,
};
pub(crate) use interrupts::{
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(crate) use sensors::F722Imu;
pub use sensors::F722Imu;
/// Materializes a board declaration after RTIC has exclusively taken the PAC
/// peripherals. The route/config check happens before any pin or peripheral
/// is initialized, so a malformed board declaration cannot drive hardware.
pub fn initialize(
/// 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>(
core: cortex_m::Peripherals,
device: stm32f7::stm32f722::Peripherals,
config: BoardConfig,
routes: F722BoardRoutes,
) -> Result<F722InitResources, BoardError> {
factory: C,
immutable: I,
) -> Result<F722InitResources<C, I, 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_motor_pins(&device.RCC, &device.GPIOB);
crate::validate_board_config(config, routes)?;
board::initialize(core, device, config, routes)
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)
}
unsafe extern "Rust" {
static KAPTAN_F722_BOARD_CONFIG: BoardConfig;
fn kaptan_f722_board_routes() -> Result<F722BoardRoutes, BoardError>;
}
fn registered_config() -> BoardConfig {
// SAFETY: `flight_firmware!` emits this immutable symbol exactly once in
// the selected board binary. The platform reads it only during RTIC init,
// before any interrupt or worker can observe configuration state.
unsafe { KAPTAN_F722_BOARD_CONFIG }
}
fn registered_routes() -> Result<F722BoardRoutes, BoardError> {
// SAFETY: `flight_firmware!` emits this function exactly once in the board
// binary. It only constructs a safe descriptor; it cannot touch PAC state
// because the board builder contains no peripherals.
unsafe { kaptan_f722_board_routes() }
}
// This is one source-level task graph. Its compile-time bindings are the
// platform-owned finite interrupt superset, independent of the board crate.
#[cfg(target_os = "none")]
firmware::f722_flight_application!(
crate::f722::registered_routes,
crate::f722::registered_config
);
@@ -7,8 +7,13 @@
use stm32f7::stm32f722;
/// Drives PB0/PB1/PB4/PB5 low before selecting push-pull GPIO output mode.
pub(crate) fn inhibit_motor_pins(rcc: &stm32f722::RCC, port: &stm32f722::GPIOB) {
pub(crate) fn inhibit_pb0_pb1_pb4_pb5(rcc: &stm32f722::RCC, port: &stm32f722::GPIOB) {
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
// synchronization delay and prevents the GPIO write from being reordered
// ahead of the clock enable.
let _ = rcc.ahb1enr().read().gpioben().bit_is_set();
port.bsrr().write(|write| {
write
.br0()
@@ -82,6 +82,13 @@ impl Usart2Tx {
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
}
@@ -138,6 +145,13 @@ impl Usart1Tx {
self.peripheral.cr3().write(|write| write.dmat().set_bit());
}
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
}
@@ -30,11 +30,9 @@ pub enum StartError {
}
impl Usart1TxDma {
/// Enables DMA2 and connects USART1 TX to its fixed request route.
/// 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());
usart.enable_dma_transmit();
Self { usart }
}
@@ -58,6 +56,9 @@ impl Usart1TxDma {
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(())
}
@@ -71,7 +72,6 @@ impl Usart1TxDma {
transfer_complete: status.tcif7().is_complete(),
transfer_error: status.teif7().is_error(),
direct_mode_error: status.dmeif7().is_error(),
fifo_error: status.feif7().is_error(),
}
.classify();
if completion == TxCompletion::NotPending {
@@ -92,6 +92,7 @@ impl Usart1TxDma {
break;
}
}
Usart1Tx::disable_dma_transmit_from_interrupt();
controller.hifcr().write(|write| {
write
.ctcif7()
@@ -114,6 +115,9 @@ impl Usart1TxDma {
}
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.
@@ -189,12 +193,15 @@ struct DmaTerminalStatus {
transfer_complete: bool,
transfer_error: bool,
direct_mode_error: bool,
fifo_error: bool,
}
impl DmaTerminalStatus {
const fn classify(self) -> TxCompletion {
if self.transfer_error || self.direct_mode_error || self.fifo_error {
// 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
@@ -211,21 +218,3 @@ impl DmaTxEngine for Usart1TxDma {
Self::start(self, buffer)
}
}
#[cfg(test)]
mod tests {
use super::DmaTerminalStatus;
use kaptan_dma::TxCompletion;
#[test]
fn terminal_error_has_precedence_over_completion() {
let status = DmaTerminalStatus {
transfer_complete: true,
transfer_error: false,
direct_mode_error: false,
fifo_error: true,
};
assert_eq!(status.classify(), TxCompletion::TransferFault);
}
}
@@ -10,6 +10,10 @@ 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,
@@ -25,10 +29,9 @@ pub enum Usart2TxDmaStartError {
}
impl Usart2TxDma {
/// Enables DMA1 and connects USART2 TX to its fixed request route.
/// 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());
usart.enable_dma_transmit();
Self { usart }
}
@@ -48,6 +51,9 @@ impl Usart2TxDma {
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(())
}
@@ -60,7 +66,6 @@ impl Usart2TxDma {
transfer_complete: status.tcif6().is_complete(),
transfer_error: status.teif6().is_error(),
direct_mode_error: status.dmeif6().is_error(),
fifo_error: status.feif6().is_error(),
}
.classify();
if completion == TxCompletion::NotPending {
@@ -71,6 +76,12 @@ impl Usart2TxDma {
.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()
@@ -93,6 +104,9 @@ impl Usart2TxDma {
}
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.
@@ -168,12 +182,15 @@ struct DmaTerminalStatus {
transfer_complete: bool,
transfer_error: bool,
direct_mode_error: bool,
fifo_error: bool,
}
impl DmaTerminalStatus {
const fn classify(self) -> TxCompletion {
if self.transfer_error || self.direct_mode_error || self.fifo_error {
// 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
@@ -190,20 +207,3 @@ impl DmaTxEngine for Usart2TxDma {
Self::start(self, buffer)
}
}
#[cfg(test)]
mod tests {
use super::DmaTerminalStatus;
use kaptan_dma::TxCompletion;
#[test]
fn terminal_error_has_precedence_over_completion() {
let status = DmaTerminalStatus {
transfer_complete: true,
transfer_error: false,
direct_mode_error: false,
fifo_error: true,
};
assert_eq!(status.classify(), TxCompletion::TransferFault);
}
}
+17 -4
View File
@@ -7,7 +7,6 @@ use stm32f7::stm32f722;
/// Owns the MPU6000 SPI1 transaction path and its PB2 chip select.
pub(crate) struct Mpu6000Spi1Route {
spi: Spi1,
chip_select: stm32f722::GPIOB,
}
impl Mpu6000Spi1Route {
@@ -26,15 +25,29 @@ impl Mpu6000Spi1Route {
.pupdr()
.modify(|_, write| write.pupdr2().floating());
chip_select.bsrr().write(|write| write.bs2().set_bit());
Self { spi, chip_select }
// 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.
Self { spi }
}
fn chip_select_port() -> &'static stm32f722::gpiob::RegisterBlock {
// SAFETY: RTIC initialization configured PB2 once before interrupts
// were enabled. Runtime writes use only PB2's atomic BSRR bits. No
// runtime owner retains GPIOB or performs GPIOB read-modify-write.
unsafe { &*stm32f722::GPIOB::ptr() }
}
fn select(&self) {
self.chip_select.bsrr().write(|write| write.br2().set_bit());
Self::chip_select_port()
.bsrr()
.write(|write| write.br2().set_bit());
}
fn deselect(&self) {
self.chip_select.bsrr().write(|write| write.bs2().set_bit());
Self::chip_select_port()
.bsrr()
.write(|write| write.bs2().set_bit());
}
}
+6 -16
View File
@@ -1,29 +1,18 @@
//! Platform-owned F722 sensor-service assembly.
use super::peripherals::{DwtMicrosecondClock, Spi1, Spi3};
use kaptan_board::ImuMount;
use kaptan_drivers::{
icm42688::{Icm42688, Icm42688Service, Icm42688ServiceEvent},
mpu6000::{
AccelerometerRange, DigitalLowPassFilter, GyroscopeRange, Mpu6000, Mpu6000Config,
Mpu6000Service, Mpu6000ServiceEvent,
},
mpu6000::{Mpu6000, Mpu6000Config, Mpu6000Service, Mpu6000ServiceEvent},
};
use kaptan_log::{ErrorReport, StaticError};
use kaptan_sensor::imu::ImuSnapshot;
use kaptan_spi::SpiCompletion;
use stm32f7::stm32f722;
use super::routes::{Icm42688Spi3Route, Mpu6000Spi1Route};
use crate::ImuMount;
// This is fixed driver setup for the selected board route. It is distinct from
// the immutable board mount and from a future safety-gated calibration flow.
const MPU6000_CONFIG: Mpu6000Config = Mpu6000Config {
accelerometer: AccelerometerRange::PlusMinus4G,
gyroscope: GyroscopeRange::PlusMinus500DegreesPerSecond,
digital_low_pass_filter: DigitalLowPassFilter::Bandwidth42Hz,
sample_rate_divider: 9,
};
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.
@@ -58,13 +47,14 @@ impl F722Imu {
chip_select: stm32f722::GPIOB,
clock: DwtMicrosecondClock,
mount: ImuMount,
config: Mpu6000Config,
) -> Self {
Self {
inner: F722ImuKind::Mpu6000(Mpu6000Service::new(
clock,
Mpu6000::new(
Mpu6000Spi1Route::new(rcc, Spi1::new(rcc, spi, dma), chip_select),
MPU6000_CONFIG,
config,
),
)),
mount,
@@ -171,7 +161,7 @@ fn align_mount(mut sample: ImuSnapshot, mount: ImuMount) -> ImuSnapshot {
#[cfg(test)]
mod tests {
use super::align_mount;
use kaptan_board::ImuMount;
use crate::ImuMount;
use kaptan_sensor::imu::ImuSnapshot;
use kaptan_time::Timestamp;
+397 -229
View File
@@ -1,375 +1,556 @@
//! STM32F7 execution platform for declarative Kaptan boards.
//!
//! A board crate selects the safe route descriptors below and invokes
//! [`flight_firmware!`]. The macro emits only a declarative link-time board
//! registration; the one RTIC application for every supported STM32F722
//! profile is defined and compiled in this crate. Consequently RTIC, PAC
//! ownership, interrupt acknowledgement, DMA and atomics remain platform
//! implementation details.
//! [`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.
#![no_std]
use core::marker::PhantomData;
use kaptan_config::Config;
use kaptan_log::KaptanError;
pub use kaptan_board::BoardConfig;
use kaptan_board::{BoardConfigError, ImuDevice, ImuMount, MavlinkPort};
pub use kaptan_drivers::mpu6000::{
AccelerometerRange, DigitalLowPassFilter, GyroscopeRange, Mpu6000Config,
};
pub use kaptan_log::Severity;
/// Fixed sensor-to-body mounting transform.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ImuMount {
/// Sensor axes already match the vehicle body axes.
BodyAligned,
/// Sensor is rotated 180 degrees around the body Z axis.
Yaw180,
}
#[doc(hidden)]
#[cfg_attr(
not(target_os = "none"),
allow(dead_code, unused_imports, unused_macros)
)]
mod f722;
pub mod f722;
/// Dependencies used only while the platform expands its private RTIC macro.
/// They stay crate-private: board binaries never name them.
/// Computes the exact bounded snapshot storage selected by a board aggregate.
#[doc(hidden)]
pub const fn config_snapshot_length<C: Config>() -> usize {
assert!(
C::FIELD_COUNT <= u16::MAX as usize,
"configuration schema exceeds wire field-count domain"
);
kaptan_config::scalar_snapshot_length(C::FIELD_COUNT as u16)
}
/// Dependencies named only by the platform's exported RTIC macro expansion.
/// Board source never depends on these implementation details directly.
#[doc(hidden)]
#[cfg_attr(not(target_os = "none"), allow(unused_imports))]
pub(crate) mod __platform_internal {
pub(crate) use cortex_m as cm;
pub(crate) use cortex_m_rt as cmrt;
pub(crate) use kaptan_dma as dma;
pub mod __platform_internal {
pub use cortex_m as cm;
pub use cortex_m_rt as cmrt;
pub use kaptan_dma as dma;
#[cfg(target_os = "none")]
pub(crate) use panic_halt as panic;
pub(crate) use rtic as rtic_app;
pub(crate) use stm32f7 as pac;
pub use panic_halt as panic;
pub use rtic as rtic_app;
pub use stm32f7 as pac;
#[cfg(not(target_os = "none"))]
pub(crate) mod panic {}
pub mod panic {}
}
/// Construction or validation failure for a declarative F722 board.
#[derive(Clone, Copy, Debug, Eq, PartialEq, KaptanError)]
pub enum BoardError {
/// Portable board metadata is self-inconsistent.
InvalidConfig(BoardConfigError),
/// Metadata and the selected vertical-sensor route disagree.
/// The immutable vertical route and its electrical parameters disagree.
VerticalSensorCapabilityMismatch,
/// Metadata and the selected immutable IMU route disagree.
ImuRouteMismatch,
/// Metadata and the selected immutable MAVLink route disagree.
MavlinkRouteMismatch,
/// Metadata and the selected immutable IMU mounting transform disagree.
ImuMountMismatch,
/// The declaration requests a service-timer frequency not implemented by
/// the selected F722 platform profile.
UnsupportedServiceFrequency,
/// The selected baud cannot carry the fixed telemetry/log schedule.
/// The selected baud cannot carry the deterministic telemetry schedule.
TelemetryLinkOvercommitted,
/// The platform could not establish a clock, peripheral, or DMA owner.
Initialization,
}
/// Validates immutable board metadata against a selected F722 route graph.
///
/// This is pure validation: it does not claim a pin, clock, interrupt, DMA
/// stream, or peripheral. The runtime invokes it immediately after the motor
/// inhibit boundary and before every other hardware action.
pub fn validate_board_config(
config: BoardConfig,
routes: F722BoardRoutes,
) -> Result<(), BoardError> {
config.validate().map_err(BoardError::InvalidConfig)?;
f722::validate_board_routes(config, routes)
/// The target configuration could not be recovered from its flash journal.
Configuration,
}
/// MPU6000 component selector.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct Mpu6000;
/// The supported MPU6000 SPI1 electrical route before selecting DRDY.
/// MPU6000 SPI1 route awaiting a data-ready pin.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct Mpu6000Spi1;
/// The complete MPU6000 SPI1 / PA4 data-ready route.
/// 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,
}
impl Mpu6000 {
/// Selects the supported SPI1 MPU6000 connection.
pub const fn on_spi1() -> Mpu6000Spi1 {
Mpu6000Spi1
}
}
impl Mpu6000Spi1 {
/// Completes the supported PA4 data-ready connection.
pub const fn with_drdy(self, _: Pa4) -> Mpu6000Spi1Exti4 {
Mpu6000Spi1Exti4
}
}
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;
/// The supported ICM-42688-P SPI3 electrical route before selecting DRDY.
/// ICM-42688-P SPI3 route awaiting a data-ready pin.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct Icm42688Spi3;
/// The complete ICM-42688-P SPI3 / PC3 data-ready route.
/// 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 {
/// Selects the supported SPI3 ICM-42688-P connection.
pub const fn on_spi3() -> Icm42688Spi3 {
Icm42688Spi3
}
}
impl Icm42688Spi3 {
/// Completes the supported PC3 data-ready connection.
pub const fn with_drdy(self, _: Pc3) -> Icm42688Spi3Exti3 {
Icm42688Spi3Exti3
}
}
/// MPU6000 data-ready pin selector.
impl Icm42688Spi3Exti3 {
pub const fn mounted(self, mount: ImuMount) -> MountedIcm42688 {
MountedIcm42688 { mount }
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct Pa4;
/// ICM-42688-P data-ready pin selector.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct Pc3;
/// MAVLink component selector.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct Mavlink;
/// Validated MAVLink route using USART1.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct MavlinkUsart1;
/// Validated MAVLink route using USART2.
pub struct UnconfiguredMavlinkUsart1;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct MavlinkUsart2;
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 {
/// Selects the supported USART1 MAVLink route.
pub const fn on_usart1() -> MavlinkUsart1 {
MavlinkUsart1
pub const fn on_usart1() -> UnconfiguredMavlinkUsart1 {
UnconfiguredMavlinkUsart1
}
/// Selects the supported USART2 MAVLink route.
pub const fn on_usart2() -> MavlinkUsart2 {
MavlinkUsart2
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;
/// No vertical-sensor connection is selected.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct NoVerticalSensor;
/// Validated MTF-01 Microlink route.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct Mtf01Uart4;
/// Validated LIDAR-Lite v3 route.
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 {
/// Declares no vertical-sensor peripheral.
pub const fn none() -> NoVerticalSensor {
NoVerticalSensor
}
/// Selects the supported MTF-01 Microlink connection.
pub const fn mtf01_on_uart4() -> Mtf01Uart4 {
Mtf01Uart4
pub const fn mtf01_on_uart4() -> UnconfiguredMtf01Uart4 {
UnconfiguredMtf01Uart4
}
/// Selects the supported LIDAR-Lite v3 connection.
pub const fn lidar_lite_v3_on_i2c1() -> LidarLiteV3I2c1 {
LidarLiteV3I2c1
}
}
/// Unselected typestate used only while a board declaration is incomplete.
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;
/// Safe typestate builder for a declarative F722 board.
///
/// This type has no peripherals, PAC types, interrupt names, DMA identifiers,
/// atomics, or unsafe operations. The F722 platform has one fixed validated
/// clock tree, so only board-selectable electrical routes participate in
/// typestate. A final `build()` exists only for complete supported route
/// combinations, so omitted or cross-wired essential routes fail to compile at
/// the board declaration.
pub struct F7BoardBuilder<Imu = Missing, MavlinkRoute = Missing, Vertical = Missing> {
_state: PhantomData<(Imu, MavlinkRoute, Vertical)>,
/// 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 F7BoardBuilder {
/// Starts an empty board declaration.
pub const fn new() -> Self {
Self {
_state: PhantomData,
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,
}
}
pub(crate) const fn profile(self) -> F722Profile {
self.profile
}
pub(crate) const fn vertical(self) -> f722::VerticalRoute {
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
}
#[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 Default for F7BoardBuilder {
fn default() -> Self {
Self::new()
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<I, T, V> F7BoardBuilder<I, T, V> {
/// Selects an IMU route descriptor.
///
/// Only a complete, profile-compatible descriptor has a final
/// [`F7BoardBuilder::build`] implementation. Keeping this one selection
/// operation uniform avoids teaching a board author platform profile
/// mechanics merely because the selected IMU model differs.
pub fn imu<Selected>(self, _: Selected) -> F7BoardBuilder<Selected, T, V> {
F7BoardBuilder {
_state: PhantomData,
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,
}
}
}
/// Selects a validated MAVLink route. Only profile-compatible route types
/// have a final [`F7BoardBuilder::build`] implementation.
pub fn mavlink<Selected>(self, _: Selected) -> F7BoardBuilder<I, Selected, V> {
F7BoardBuilder {
_state: PhantomData,
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,
}
}
}
/// Selects a validated vertical-sensor route. Its capability is checked
/// against [`F7Board::CONFIG`] before any peripheral is initialized.
pub fn vertical_sensor<Selected>(self, _: Selected) -> F7BoardBuilder<I, T, Selected> {
F7BoardBuilder {
_state: PhantomData,
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,
}
}
}
/// Finite STM32F722 execution profile selected from safe route descriptors.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum F722Profile {
Mpu6000Spi1Usart1,
Icm42688Spi3Usart2,
}
/// Fully validated, non-executing F722 board declaration.
///
/// It is deliberately only a compact route description. Peripheral tokens
/// are taken later by the platform's RTIC initializer and never reach a board
/// crate.
#[doc(hidden)]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct F722BoardRoutes {
profile: F722Profile,
vertical: f722::VerticalRoute,
pub(crate) enum F722StatusLedRoute {
Pd2ActiveLow,
Pa13ActiveHigh,
}
impl F722BoardRoutes {
#[cfg_attr(not(target_os = "none"), allow(dead_code))]
pub(crate) const fn profile(self) -> F722Profile {
self.profile
}
pub(crate) const fn imu(self) -> ImuDevice {
match self.profile {
F722Profile::Mpu6000Spi1Usart1 => ImuDevice::Mpu6000,
F722Profile::Icm42688Spi3Usart2 => ImuDevice::Icm42688,
}
}
pub(crate) const fn imu_mount(self) -> ImuMount {
match self.profile {
F722Profile::Mpu6000Spi1Usart1 => ImuMount::BodyAligned,
F722Profile::Icm42688Spi3Usart2 => ImuMount::Yaw180,
}
}
pub(crate) const fn mavlink_port(self) -> MavlinkPort {
match self.profile {
F722Profile::Mpu6000Spi1Usart1 => MavlinkPort::Serial1,
F722Profile::Icm42688Spi3Usart2 => MavlinkPort::Serial2,
}
}
}
macro_rules! impl_mpu6000_routes {
($vertical:ty, $route:expr) => {
impl F7BoardBuilder<Mpu6000Spi1Exti4, MavlinkUsart1, $vertical> {
/// Finishes a complete supported MPU6000 F722 declaration.
pub const fn build(self) -> Result<F722BoardRoutes, BoardError> {
Ok(F722BoardRoutes {
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,
}
}
}
};
}
impl_mpu6000_routes!(Mtf01Uart4, f722::VerticalRoute::Mtf01);
impl_mpu6000_routes!(LidarLiteV3I2c1, f722::VerticalRoute::LidarLiteV3);
impl_mpu6000_routes!(NoVerticalSensor, f722::VerticalRoute::None);
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! impl_icm42688_routes {
($vertical:ty, $route:expr) => {
impl F7BoardBuilder<Icm42688Spi3Exti3, MavlinkUsart2, $vertical> {
/// Finishes a complete supported ICM-42688-P F722 declaration.
pub const fn build(self) -> Result<F722BoardRoutes, BoardError> {
Ok(F722BoardRoutes {
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,
}
}
}
};
}
impl_icm42688_routes!(NoVerticalSensor, f722::VerticalRoute::None);
impl_icm42688_routes!(Mtf01Uart4, f722::VerticalRoute::Mtf01);
impl_icm42688_routes!(LidarLiteV3I2c1, f722::VerticalRoute::LidarLiteV3);
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
);
/// Missing and cross-profile route selections are rejected before board I/O is
/// linked, because no matching `build` implementation exists.
///
/// ```compile_fail
/// use platform_stm32f7::{
/// F7BoardBuilder, Mavlink, Mpu6000, Pa4,
/// };
///
/// let _ = F7BoardBuilder::new()
/// .imu(Mpu6000::on_spi1().with_drdy(Pa4))
/// .mavlink(Mavlink::on_usart1())
/// .build(); // vertical route is missing
/// ```
///
/// ```compile_fail
/// use platform_stm32f7::{
/// F7BoardBuilder, Icm42688, Mavlink, Pc3,
/// VerticalSensor,
/// };
///
/// let _ = F7BoardBuilder::new()
/// .imu(Icm42688::on_spi3().with_drdy(Pc3))
/// .mavlink(Mavlink::on_usart1()) // ICM route requires USART2
/// .vertical_sensor(VerticalSensor::none())
/// .build();
/// ```
///
/// Contract implemented by a declarative STM32F722 board crate.
pub trait F7Board {
/// Portable identity, capabilities and outbound data-channel policy.
const CONFIG: BoardConfig;
/// Selects one complete supported F722 route graph.
fn build(platform: F7BoardBuilder) -> Result<F722BoardRoutes, BoardError>;
/// Literal immutable root embedded by each F722 board package.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct F722ImmutableConfig {
pub name: &'static str,
pub service_hz: usize,
pub hardware: F722Hardware,
}
/// Checks portable metadata at compile time.
#[macro_export]
macro_rules! enforce_f7_board_contract {
($board:ty) => {
const _: () = match <$board as $crate::F7Board>::CONFIG.validate() {
Ok(()) => (),
Err(_) => panic!("invalid STM32F7 board configuration"),
};
};
mod sealed {
pub trait ImmutableConfig {}
impl ImmutableConfig for super::F722ImmutableConfig {}
}
/// Sealed associated-type bound preserving the visible two-root board
/// contract while requiring the concrete platform manifest above.
#[doc(hidden)]
pub trait F7ImmutableConfig: Copy + sealed::ImmutableConfig {
fn f722(&self) -> &F722ImmutableConfig;
}
impl F7ImmutableConfig for F722ImmutableConfig {
fn f722(&self) -> &F722ImmutableConfig {
self
}
}
/// Contract implemented by a declarative STM32F722 board crate.
pub trait F7Board {
type UserConfig: Clone + Config;
type ImmutableConfig: F7ImmutableConfig;
const FACTORY_USER_CONFIG: Self::UserConfig;
const IMMUTABLE_CONFIG: Self::ImmutableConfig;
}
/// Installs the one platform-owned RTIC application for a declarative board.
@@ -384,20 +565,7 @@ macro_rules! enforce_f7_board_contract {
#[macro_export]
macro_rules! flight_firmware {
($board:ty) => {
$crate::enforce_f7_board_contract!($board);
// The platform RTIC application calls these symbols during `init`.
// They carry only the declarative board contract; RTIC, PAC, DMA,
// atomics, and unsafe operations remain inside this platform crate.
#[unsafe(no_mangle)]
pub static KAPTAN_F722_BOARD_CONFIG: $crate::BoardConfig =
<$board as $crate::F7Board>::CONFIG;
#[unsafe(no_mangle)]
pub extern "Rust" fn kaptan_f722_board_routes()
-> Result<$crate::F722BoardRoutes, $crate::BoardError> {
<$board as $crate::F7Board>::build($crate::F7BoardBuilder::new())
}
#[used]
static KAPTAN_F722_PLATFORM_FLIGHT: fn() = $crate::firmware_anchor;
$crate::__f722_flight_application!($board);
};
}