refactor(firmware): centralize configuration and diagnostics
This commit is contained in:
1 parent
7048ed89d0
commit
387fdaad23
140 files changed
+6891
-4541
No files matched your search
@@ -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
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user