refactor: move firmware workspace under firmware
This commit is contained in:
1 parent
153b0bf262
commit
f03fc119a4
222 files changed
+56
-13
No files matched your search
@@ -0,0 +1,36 @@
|
||||
[package]
|
||||
name = "platform-stm32f7"
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
rust-version.workspace = true
|
||||
version.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
# This is a bare-metal `no_std` platform library with its own panic handler.
|
||||
# Host-verifiable invariants live in `tests/platform_safety_boundary.rs`; a
|
||||
# generated lib-test harness would link `std` and a second panic handler.
|
||||
test = false
|
||||
|
||||
[features]
|
||||
default = []
|
||||
|
||||
[dependencies]
|
||||
kaptan-board = { path = "../../libs/kaptan-board" }
|
||||
kaptan-data-channel = { path = "../../libs/kaptan-data-channel" }
|
||||
kaptan-dma = { path = "../../libs/kaptan-dma" }
|
||||
kaptan-drivers = { path = "../../libs/kaptan-drivers" }
|
||||
kaptan-i2c = { path = "../../libs/kaptan-i2c" }
|
||||
kaptan-log = { path = "../../libs/kaptan-log" }
|
||||
kaptan-log-mavlink = { path = "../../libs/kaptan-log-mavlink" }
|
||||
kaptan-mavlink = { path = "../../libs/kaptan-mavlink" }
|
||||
kaptan-sensor = { path = "../../libs/kaptan-sensor" }
|
||||
kaptan-spi = { path = "../../libs/kaptan-spi" }
|
||||
kaptan-time = { path = "../../libs/kaptan-time" }
|
||||
cortex-m = "0.7"
|
||||
cortex-m-rt = "0.7"
|
||||
rtic = { version = "2.3", default-features = false, features = ["thumbv7-backend"] }
|
||||
stm32f7 = { version = "0.16", default-features = false, features = ["rt", "stm32f722"] }
|
||||
panic-halt = "1"
|
||||
@@ -0,0 +1,652 @@
|
||||
//! Physical-sensor telemetry STM32F722 board-I/O state and ownership graph.
|
||||
|
||||
use core::sync::atomic::{AtomicU8, Ordering};
|
||||
|
||||
use super::peripherals::{
|
||||
DwtMicrosecondClock, I2c1, I2c1Config, ReceiveInterrupt, Tim2Periodic, Uart4,
|
||||
configure_exti3_pin_pc3, configure_exti4_pin_pa4, configure_i2c1_pins_pb8_pb9,
|
||||
configure_spi1_pins_pa5_pa6_pa7, configure_spi3_pins_pc10_pc11_pc12,
|
||||
configure_uart4_pins_pa0_pa1, configure_usart1_rx_pin_pa10, configure_usart1_tx_pin_pa9,
|
||||
configure_usart2_pins_pa2_pa3,
|
||||
};
|
||||
use kaptan_board::{BoardConfig, VerticalSensingKind};
|
||||
use kaptan_data_channel::{
|
||||
DataChannel, 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,
|
||||
StaticErrorMailbox, TransportAdmission,
|
||||
};
|
||||
use kaptan_log_mavlink::{DiagnosticPayload, encode_diagnostic};
|
||||
use kaptan_spi::SpiCompletion;
|
||||
use stm32f7::stm32f722;
|
||||
|
||||
use crate::{BoardError, F722BoardRoutes, F722Profile};
|
||||
|
||||
use super::{
|
||||
VerticalRoute, clock,
|
||||
data_channel::{
|
||||
F722MavlinkTransmitter, F722StatusLed, GeprcStatusLed, SERVICE_HZ, SERVICE_TIMER_CONFIG,
|
||||
vertical_data_config,
|
||||
},
|
||||
interrupts,
|
||||
sensors::{F722Imu, ImuEvent},
|
||||
};
|
||||
|
||||
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 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;
|
||||
|
||||
/// 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
|
||||
}
|
||||
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
|
||||
// the low-priority MAVLink worker to publish.
|
||||
static IMU_FAULTS: StaticErrorMailbox = StaticErrorMailbox::new();
|
||||
static LIDAR_FAULTS: StaticErrorMailbox = StaticErrorMailbox::new();
|
||||
static MTF01_FAULTS: StaticErrorMailbox = StaticErrorMailbox::new();
|
||||
static SAMPLES: LatestImuMailbox = LatestImuMailbox::new();
|
||||
static RANGES: LatestRangeMailbox = LatestRangeMailbox::new();
|
||||
static OPTICAL_FLOW: LatestOpticalFlowMailbox = LatestOpticalFlowMailbox::new();
|
||||
|
||||
type F722Range = LidarLiteV3Service<DwtMicrosecondClock, I2c1>;
|
||||
/// Opaque RTIC-local MTF-01 service type; its route is platform-owned.
|
||||
pub type F722Mtf01 = Mtf01Service<DwtMicrosecondClock>;
|
||||
/// Opaque RTIC-local UART4 owner; its construction is platform-owned.
|
||||
pub type F722Mtf01Uart = Uart4;
|
||||
/// Opaque RTIC-local service timer; its construction is platform-owned.
|
||||
pub type F722ServiceTimer = Tim2Periodic;
|
||||
|
||||
/// RTIC-shared MAVLink state.
|
||||
///
|
||||
/// 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 {
|
||||
data_channel: DataChannel<'static, F722MavlinkTransmitter, SERVICE_HZ>,
|
||||
logs: EventHub<2, 4>,
|
||||
budget_ticks: u8,
|
||||
error_budget: u8,
|
||||
log_budget: u8,
|
||||
led: F722StatusLed,
|
||||
}
|
||||
|
||||
/// RTIC-shared LIDAR state.
|
||||
///
|
||||
/// I2C event/error IRQs and the service timer are its only users. MTF-01 is a
|
||||
/// separate UART-only local resource, so it cannot raise this ceiling.
|
||||
pub struct F722LidarState {
|
||||
range: Option<F722Range>,
|
||||
startup_ticks: u8,
|
||||
}
|
||||
|
||||
/// Explicit handoff from platform initialization to the RTIC resource graph.
|
||||
///
|
||||
/// 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,
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
return Err(BoardError::UnsupportedServiceFrequency);
|
||||
}
|
||||
let expected = match routes.vertical {
|
||||
VerticalRoute::None => VerticalSensingKind::None,
|
||||
VerticalRoute::Mtf01 => VerticalSensingKind::RangeAndOpticalFlow,
|
||||
VerticalRoute::LidarLiteV3 => VerticalSensingKind::Range,
|
||||
};
|
||||
if telemetry.vertical_sensing != expected {
|
||||
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) {
|
||||
return Err(BoardError::TelemetryLinkOvercommitted);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn initialize(
|
||||
mut core: cortex_m::Peripherals,
|
||||
device: stm32f722::Peripherals,
|
||||
config: BoardConfig,
|
||||
routes: F722BoardRoutes,
|
||||
) -> Result<F722InitResources, BoardError> {
|
||||
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());
|
||||
|
||||
match routes.profile() {
|
||||
F722Profile::Mpu6000Spi1Usart1 => {
|
||||
initialize_mpu6000(device, clock, config, routes.vertical)
|
||||
}
|
||||
F722Profile::Icm42688Spi3Usart2 => {
|
||||
initialize_icm42688(device, clock, config, routes.vertical)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn initialize_mpu6000(
|
||||
device: stm32f722::Peripherals,
|
||||
clock: DwtMicrosecondClock,
|
||||
config: BoardConfig,
|
||||
vertical: VerticalRoute,
|
||||
) -> Result<F722InitResources, BoardError> {
|
||||
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);
|
||||
configure_vertical_pins(&device.RCC, &device.GPIOA, &device.GPIOB, vertical);
|
||||
|
||||
let (range, mtf01, mtf01_uart) = initialize_vertical(
|
||||
&device.RCC,
|
||||
device.I2C1,
|
||||
device.UART4,
|
||||
clock.fork(),
|
||||
clock.fork(),
|
||||
vertical,
|
||||
)?;
|
||||
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,
|
||||
);
|
||||
let transmitter =
|
||||
F722MavlinkTransmitter::usart1(&device.RCC, device.USART1, config.mavlink.baud)
|
||||
.map_err(|_| BoardError::Initialization)?;
|
||||
let led = F722StatusLed::Kakute(super::peripherals::ActiveLowLedPd2::new(
|
||||
&device.RCC,
|
||||
device.GPIOD,
|
||||
));
|
||||
finish(
|
||||
&device.RCC,
|
||||
device.TIM2,
|
||||
config,
|
||||
vertical,
|
||||
imu,
|
||||
range,
|
||||
mtf01,
|
||||
mtf01_uart,
|
||||
transmitter,
|
||||
led,
|
||||
)
|
||||
}
|
||||
|
||||
fn initialize_icm42688(
|
||||
device: stm32f722::Peripherals,
|
||||
clock: DwtMicrosecondClock,
|
||||
config: BoardConfig,
|
||||
vertical: VerticalRoute,
|
||||
) -> Result<F722InitResources, BoardError> {
|
||||
configure_spi3_pins_pc10_pc11_pc12(&device.RCC, &device.GPIOC);
|
||||
configure_exti3_pin_pc3(&device.GPIOC);
|
||||
configure_exti3(&device.RCC, device.SYSCFG, device.EXTI);
|
||||
// PA2/PA3 writes take effect only after enabling the GPIOA bus clock.
|
||||
// The ICM route enables GPIOA later for chip select, which is too late for
|
||||
// the USART2 alternate-function configuration below.
|
||||
device
|
||||
.RCC
|
||||
.ahb1enr()
|
||||
.modify(|_, write| write.gpioaen().set_bit());
|
||||
configure_usart2_pins_pa2_pa3(&device.GPIOA);
|
||||
configure_vertical_pins(&device.RCC, &device.GPIOA, &device.GPIOB, vertical);
|
||||
|
||||
let (range, mtf01, mtf01_uart) = initialize_vertical(
|
||||
&device.RCC,
|
||||
device.I2C1,
|
||||
device.UART4,
|
||||
clock.fork(),
|
||||
clock.fork(),
|
||||
vertical,
|
||||
)?;
|
||||
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,
|
||||
);
|
||||
let transmitter =
|
||||
F722MavlinkTransmitter::usart2(&device.RCC, device.USART2, config.mavlink.baud)
|
||||
.map_err(|_| BoardError::Initialization)?;
|
||||
let led = F722StatusLed::Geprc(GeprcStatusLed::new(&device.RCC, device.GPIOA));
|
||||
finish(
|
||||
&device.RCC,
|
||||
device.TIM2,
|
||||
config,
|
||||
vertical,
|
||||
imu,
|
||||
range,
|
||||
mtf01,
|
||||
mtf01_uart,
|
||||
transmitter,
|
||||
led,
|
||||
)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn finish(
|
||||
rcc: &stm32f722::RCC,
|
||||
tim2: stm32f722::TIM2,
|
||||
config: BoardConfig,
|
||||
vertical: VerticalRoute,
|
||||
imu: F722Imu,
|
||||
range: Option<F722Range>,
|
||||
mtf01: Option<F722Mtf01>,
|
||||
mtf01_uart: Option<Uart4>,
|
||||
transmitter: F722MavlinkTransmitter,
|
||||
led: F722StatusLed,
|
||||
) -> Result<F722InitResources, BoardError> {
|
||||
let data_channel = DataChannel::new(
|
||||
transmitter,
|
||||
config,
|
||||
vertical_data_config(vertical),
|
||||
DataSources {
|
||||
imu: &SAMPLES,
|
||||
range: &RANGES,
|
||||
optical_flow: &OPTICAL_FLOW,
|
||||
},
|
||||
)
|
||||
.map_err(|_| BoardError::Initialization)?;
|
||||
Ok(F722InitResources {
|
||||
imu,
|
||||
mtf01,
|
||||
mtf01_uart,
|
||||
timer: Tim2Periodic::new(rcc, tim2, SERVICE_TIMER_CONFIG),
|
||||
lidar: F722LidarState {
|
||||
range,
|
||||
startup_ticks: 0,
|
||||
},
|
||||
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,
|
||||
led,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
fn configure_vertical_pins(
|
||||
rcc: &stm32f722::RCC,
|
||||
gpioa: &stm32f722::GPIOA,
|
||||
gpiob: &stm32f722::GPIOB,
|
||||
vertical: VerticalRoute,
|
||||
) {
|
||||
match vertical {
|
||||
VerticalRoute::None => {}
|
||||
VerticalRoute::Mtf01 => configure_uart4_pins_pa0_pa1(gpioa),
|
||||
VerticalRoute::LidarLiteV3 => configure_i2c1_pins_pb8_pb9(rcc, gpiob),
|
||||
}
|
||||
}
|
||||
|
||||
type VerticalResources = (Option<F722Range>, Option<F722Mtf01>, Option<Uart4>);
|
||||
|
||||
fn initialize_vertical(
|
||||
rcc: &stm32f722::RCC,
|
||||
i2c: stm32f722::I2C1,
|
||||
uart4: stm32f722::UART4,
|
||||
mtf01_clock: DwtMicrosecondClock,
|
||||
range_clock: DwtMicrosecondClock,
|
||||
vertical: VerticalRoute,
|
||||
) -> 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::LidarLiteV3 => Ok((
|
||||
Some(LidarLiteV3Service::new(
|
||||
range_clock,
|
||||
LidarLiteV3::new(I2c1::new(rcc, i2c, I2C1_CONFIG)),
|
||||
LIDAR_MODE,
|
||||
)),
|
||||
None,
|
||||
None,
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn configure_exti4(rcc: &stm32f722::RCC, syscfg: stm32f722::SYSCFG, exti: stm32f722::EXTI) {
|
||||
rcc.apb2enr().modify(|_, write| write.syscfgen().set_bit());
|
||||
syscfg.exticr2().modify(|_, write| {
|
||||
// SAFETY: selector zero is GPIOA for EXTI4 on STM32F722.
|
||||
unsafe { write.exti4().bits(0) }
|
||||
});
|
||||
exti.imr().modify(|_, write| write.im4().set_bit());
|
||||
exti.rtsr().modify(|_, write| write.tr4().set_bit());
|
||||
exti.ftsr().modify(|_, write| write.tr4().clear_bit());
|
||||
exti.pr().write(|write| write.pr4().clear());
|
||||
}
|
||||
|
||||
fn configure_exti3(rcc: &stm32f722::RCC, syscfg: stm32f722::SYSCFG, exti: stm32f722::EXTI) {
|
||||
rcc.apb2enr().modify(|_, write| write.syscfgen().set_bit());
|
||||
syscfg.exticr1().modify(|_, write| {
|
||||
// SAFETY: selector two is GPIOC for EXTI3 on STM32F722.
|
||||
unsafe { write.exti3().bits(2) }
|
||||
});
|
||||
exti.imr().modify(|_, write| write.im3().set_bit());
|
||||
exti.rtsr().modify(|_, write| write.tr3().set_bit());
|
||||
exti.ftsr().modify(|_, write| write.tr3().clear_bit());
|
||||
exti.pr().write(|write| write.pr3().clear());
|
||||
}
|
||||
|
||||
/// 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>) {
|
||||
let Some(mtf01) = mtf01.as_mut() else {
|
||||
return;
|
||||
};
|
||||
match Uart4::acknowledge_receive_interrupt() {
|
||||
ReceiveInterrupt::Byte(byte) => {
|
||||
if let Some(report) = mtf01.on_received_byte(byte) {
|
||||
if let Some(range) = report.range_snapshot() {
|
||||
RANGES.publish(range);
|
||||
}
|
||||
if let Some(flow) = report.optical_flow_snapshot() {
|
||||
OPTICAL_FLOW.publish(flow);
|
||||
}
|
||||
}
|
||||
}
|
||||
ReceiveInterrupt::Fault => {
|
||||
mtf01.reset();
|
||||
record_mtf01_fault(F722TelemetryError::Mtf01Uart.static_error());
|
||||
}
|
||||
ReceiveInterrupt::NotPending => {}
|
||||
}
|
||||
}
|
||||
|
||||
impl F722LidarState {
|
||||
pub(crate) fn on_i2c_event(&mut self, error: bool) {
|
||||
let Some(range) = self.range.as_mut() else {
|
||||
return;
|
||||
};
|
||||
let event = range.on_controller_interrupt(|i2c| {
|
||||
if error {
|
||||
i2c.acknowledge_error_interrupt()
|
||||
} else {
|
||||
i2c.acknowledge_event_interrupt()
|
||||
}
|
||||
});
|
||||
publish_lidar_event(event);
|
||||
}
|
||||
|
||||
pub(crate) fn on_timer(&mut self) {
|
||||
if let Some(range) = self.range.as_mut() {
|
||||
if self.startup_ticks < LIDAR_STARTUP_TICKS {
|
||||
self.startup_ticks += 1;
|
||||
if self.startup_ticks == LIDAR_STARTUP_TICKS
|
||||
&& let Err(error) = range.finish_startup()
|
||||
{
|
||||
record_lidar_fault(error.static_error());
|
||||
}
|
||||
} else {
|
||||
publish_lidar_event(range.on_periodic_tick());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Advances the IMU state machine in its dedicated worker-local resource.
|
||||
pub(crate) 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;
|
||||
const TIMER: u8 = 1 << 3;
|
||||
const DRDY: u8 = 1 << 0;
|
||||
|
||||
if events & START != 0 {
|
||||
accept_imu_event(imu.begin());
|
||||
}
|
||||
if events & TRANSFER_FAULT != 0 {
|
||||
accept_imu_event(imu.on_transfer_complete(SpiCompletion::TransferFault));
|
||||
}
|
||||
if events & TRANSFER_COMPLETE != 0 {
|
||||
accept_imu_event(imu.on_transfer_complete(SpiCompletion::Complete));
|
||||
}
|
||||
if events & TIMER != 0 {
|
||||
accept_imu_event(imu.on_periodic_tick());
|
||||
}
|
||||
if events & DRDY != 0 {
|
||||
accept_imu_event(imu.on_data_ready());
|
||||
}
|
||||
}
|
||||
|
||||
fn accept_imu_event(event: ImuEvent) {
|
||||
match event {
|
||||
ImuEvent::Sample(sample) => {
|
||||
SAMPLES.publish(sample);
|
||||
}
|
||||
ImuEvent::Fault(message) => record_imu_fault(message),
|
||||
ImuEvent::Idle | ImuEvent::Initialized => {}
|
||||
}
|
||||
}
|
||||
|
||||
impl F722MavlinkState {
|
||||
/// Handles bounded DMA and timer work for the telemetry image.
|
||||
pub(crate) fn process_worker_events(&mut self, events: u8) {
|
||||
const TIMER: u8 = 1 << 0;
|
||||
const DMA: u8 = 1 << 2;
|
||||
|
||||
if events & DMA != 0 {
|
||||
let completion = take_mavlink_completion();
|
||||
let _ = self.data_channel.on_dma_complete(completion);
|
||||
if completion == kaptan_dma::TxCompletion::Complete {
|
||||
self.led.toggle();
|
||||
}
|
||||
}
|
||||
|
||||
if events & TIMER != 0 {
|
||||
// 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`
|
||||
// would incorrectly claim initialization never completed.
|
||||
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_log_record();
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
let data_channel = &mut self.data_channel;
|
||||
let mut accepted = None;
|
||||
let _ = self.logs.submit_next(|record| {
|
||||
let class = match record.delivery() {
|
||||
DeliveryClass::Critical => kaptan_mavlink::transmitter::FrameClass::Critical,
|
||||
DeliveryClass::BestEffort => kaptan_mavlink::transmitter::FrameClass::BestEffort,
|
||||
};
|
||||
let payload = encode_diagnostic(record);
|
||||
let admission = match &payload {
|
||||
DiagnosticPayload::Log(payload) => data_channel.send_custom(class, payload),
|
||||
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 => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn take_mavlink_completion() -> kaptan_dma::TxCompletion {
|
||||
match UART_COMPLETION.swap(0, Ordering::AcqRel) {
|
||||
1 => kaptan_dma::TxCompletion::Complete,
|
||||
2 => kaptan_dma::TxCompletion::TransferFault,
|
||||
_ => kaptan_dma::TxCompletion::NotPending,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn record_mavlink_completion(completion: kaptan_dma::TxCompletion) {
|
||||
let code = match completion {
|
||||
kaptan_dma::TxCompletion::Complete => 1,
|
||||
kaptan_dma::TxCompletion::TransferFault | kaptan_dma::TxCompletion::Halted => 2,
|
||||
kaptan_dma::TxCompletion::NotPending => 0,
|
||||
};
|
||||
if code != 0 {
|
||||
UART_COMPLETION.store(code, Ordering::Release);
|
||||
}
|
||||
}
|
||||
|
||||
fn publish_lidar_event<E: ErrorReport>(event: LidarLiteV3ServiceEvent<E>) {
|
||||
match event {
|
||||
LidarLiteV3ServiceEvent::Measurement(snapshot) => RANGES.publish(snapshot),
|
||||
LidarLiteV3ServiceEvent::Fault(error) => record_lidar_fault(error.static_error()),
|
||||
LidarLiteV3ServiceEvent::Idle => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn record_imu_fault(message: &'static StaticError) {
|
||||
IMU_FAULTS.record(message);
|
||||
}
|
||||
|
||||
fn record_lidar_fault(message: &'static StaticError) {
|
||||
LIDAR_FAULTS.record(message);
|
||||
}
|
||||
|
||||
fn record_mtf01_fault(message: &'static StaticError) {
|
||||
MTF01_FAULTS.record(message);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
//! STM32F722 Pll216 clock-source assembly.
|
||||
|
||||
use super::peripherals::{DwtMicrosecondClock, select_pll216_profile};
|
||||
use kaptan_log::KaptanError;
|
||||
use stm32f7::stm32f722;
|
||||
|
||||
/// Core frequency in whole megahertz after selecting the PLL216 board profile.
|
||||
const CORE_CLOCK_MHZ: u32 = 216;
|
||||
|
||||
/// A failure while establishing the board's clock tree or monotonic clock.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, KaptanError)]
|
||||
pub(crate) enum ClockInitializationError {
|
||||
Profile,
|
||||
Monotonic,
|
||||
}
|
||||
|
||||
/// Selects the F722 Pll216 profile and starts its monotonic microsecond clock.
|
||||
pub(crate) fn initialize(
|
||||
core: &mut cortex_m::Peripherals,
|
||||
device: &stm32f722::Peripherals,
|
||||
) -> Result<DwtMicrosecondClock, ClockInitializationError> {
|
||||
select_pll216_profile(&device.RCC, &device.PWR, &device.FLASH)
|
||||
.map_err(|_| ClockInitializationError::Profile)?;
|
||||
core.SCB.disable_dcache(&mut core.CPUID);
|
||||
DwtMicrosecondClock::start::<CORE_CLOCK_MHZ>(&mut core.DCB, &mut core.DWT)
|
||||
.map_err(|_| ClockInitializationError::Monotonic)
|
||||
}
|
||||
@@ -0,0 +1,257 @@
|
||||
//! 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_dma::TxCompletion;
|
||||
use kaptan_mavlink::{
|
||||
MavlinkFrame,
|
||||
encoder::DistanceSensorConfig,
|
||||
transmitter::{FrameClass, MavlinkTransmitter, TransmitFaultKind},
|
||||
};
|
||||
|
||||
// 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.
|
||||
pub(crate) const UART1_CLOCK_HZ: u32 = 108_000_000;
|
||||
pub(crate) const UART2_CLOCK_HZ: u32 = 54_000_000;
|
||||
pub(crate) const SERVICE_HZ: usize = 100;
|
||||
|
||||
/// Fixed service-timer configuration for the validated Pll216 profile.
|
||||
pub(crate) const SERVICE_TIMER_CONFIG: peripherals::Tim2PeriodicConfig =
|
||||
peripherals::Tim2PeriodicConfig {
|
||||
prescaler: 10_799,
|
||||
auto_reload: 99,
|
||||
};
|
||||
|
||||
/// Maps a selected vertical electrical route to development data-channel metadata.
|
||||
///
|
||||
/// The values are telemetry descriptions only; they neither calibrate a flight
|
||||
/// estimator nor authorize an `ExternalControlOnly` profile.
|
||||
pub(crate) fn vertical_data_config(route: VerticalRoute) -> VerticalDataConfig {
|
||||
let distance =
|
||||
|minimum_distance_centimetres, maximum_distance_centimetres| DistanceSensorConfig {
|
||||
sensor_id: 0,
|
||||
orientation:
|
||||
kaptan_mavlink::message::MavSensorOrientation::MAV_SENSOR_ROTATION_PITCH_270,
|
||||
minimum_distance_centimetres,
|
||||
maximum_distance_centimetres,
|
||||
};
|
||||
|
||||
match route {
|
||||
VerticalRoute::None => VerticalDataConfig {
|
||||
distance: None,
|
||||
optical_flow_sensor_id: None,
|
||||
maximum_age_ticks: 10,
|
||||
},
|
||||
VerticalRoute::Mtf01 => VerticalDataConfig {
|
||||
distance: Some(distance(2, 800)),
|
||||
optical_flow_sensor_id: Some(0),
|
||||
maximum_age_ticks: 10,
|
||||
},
|
||||
VerticalRoute::LidarLiteV3 => VerticalDataConfig {
|
||||
distance: Some(distance(20, 4_000)),
|
||||
optical_flow_sensor_id: None,
|
||||
maximum_age_ticks: 10,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// The two F722 DMA engines behind the portable data-channel transport.
|
||||
enum F722MavlinkTransport {
|
||||
Usart1(MavlinkTransmitter<peripherals::DcacheDisabledDmaMemory, peripherals::Usart1TxDma, 4>),
|
||||
Usart2(MavlinkTransmitter<peripherals::DcacheDisabledDmaMemory, peripherals::Usart2TxDma, 4>),
|
||||
}
|
||||
|
||||
/// Sole F722 MAVLink DMA owner plus unconfigurable transport-loss accounting.
|
||||
pub(crate) struct F722MavlinkTransmitter {
|
||||
inner: F722MavlinkTransport,
|
||||
health: TxHealth,
|
||||
pending_faults: u8,
|
||||
}
|
||||
|
||||
impl F722MavlinkTransmitter {
|
||||
pub(crate) fn usart1(
|
||||
rcc: &stm32f7::stm32f722::RCC,
|
||||
uart: stm32f7::stm32f722::USART1,
|
||||
baud: u32,
|
||||
) -> Result<Self, ()> {
|
||||
let uart = peripherals::Usart1Tx::new(rcc, uart, UART1_CLOCK_HZ, baud).map_err(|_| ())?;
|
||||
// SAFETY: platform initialization disables D-cache before this object
|
||||
// is created, and the active MAVLink frame remains queue-owned until
|
||||
// this platform receives a terminal DMA completion notification.
|
||||
let memory = unsafe { peripherals::DcacheDisabledDmaMemory::new() };
|
||||
Ok(Self {
|
||||
inner: F722MavlinkTransport::Usart1(MavlinkTransmitter::new(
|
||||
memory,
|
||||
peripherals::Usart1TxDma::new(rcc, uart),
|
||||
)),
|
||||
health: TxHealth::default(),
|
||||
pending_faults: 0,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn usart2(
|
||||
rcc: &stm32f7::stm32f722::RCC,
|
||||
uart: stm32f7::stm32f722::USART2,
|
||||
baud: u32,
|
||||
) -> Result<Self, ()> {
|
||||
let uart = peripherals::Usart2Tx::new(rcc, uart, UART2_CLOCK_HZ, baud).map_err(|_| ())?;
|
||||
// SAFETY: identical DMA-memory invariant to the USART1 route above;
|
||||
// only the fixed platform DMA endpoint differs.
|
||||
let memory = unsafe { peripherals::DcacheDisabledDmaMemory::new() };
|
||||
Ok(Self {
|
||||
inner: F722MavlinkTransport::Usart2(MavlinkTransmitter::new(
|
||||
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 {
|
||||
F722MavlinkTransport::Usart1(transmitter) => {
|
||||
let outcome = transmitter.admit_and_start(class, frame);
|
||||
(
|
||||
outcome.admission,
|
||||
outcome.start.err().map(|fault| fault.kind()),
|
||||
)
|
||||
}
|
||||
F722MavlinkTransport::Usart2(transmitter) => {
|
||||
let outcome = transmitter.admit_and_start(class, frame);
|
||||
(
|
||||
outcome.admission,
|
||||
outcome.start.err().map(|fault| fault.kind()),
|
||||
)
|
||||
}
|
||||
};
|
||||
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
|
||||
}
|
||||
TransmitFaultKind::Engine => {
|
||||
self.health.engine_fault_count = self.health.engine_fault_count.saturating_add(1);
|
||||
2
|
||||
}
|
||||
TransmitFaultKind::Terminal => {
|
||||
self.health.terminal_fault_count =
|
||||
self.health.terminal_fault_count.saturating_add(1);
|
||||
4
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Board-route status light; it is not part of a board declaration API.
|
||||
pub(crate) enum F722StatusLed {
|
||||
Kakute(peripherals::ActiveLowLedPd2),
|
||||
Geprc(GeprcStatusLed),
|
||||
}
|
||||
|
||||
impl F722StatusLed {
|
||||
pub(crate) fn toggle(&mut self) {
|
||||
match self {
|
||||
Self::Kakute(led) => led.toggle(),
|
||||
Self::Geprc(led) => led.toggle(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// GEPRC GEP-F722-HD V2 blue status LED on PA13.
|
||||
pub(crate) struct GeprcStatusLed {
|
||||
port: stm32f7::stm32f722::GPIOA,
|
||||
is_high: bool,
|
||||
}
|
||||
|
||||
impl GeprcStatusLed {
|
||||
pub(crate) fn new(rcc: &stm32f7::stm32f722::RCC, port: stm32f7::stm32f722::GPIOA) -> Self {
|
||||
rcc.ahb1enr().modify(|_, write| write.gpioaen().set_bit());
|
||||
port.moder().modify(|_, write| write.moder13().output());
|
||||
port.otyper().modify(|_, write| write.ot13().push_pull());
|
||||
port.ospeedr()
|
||||
.modify(|_, write| write.ospeedr13().low_speed());
|
||||
port.pupdr().modify(|_, write| write.pupdr13().floating());
|
||||
port.bsrr().write(|write| write.br13().set_bit());
|
||||
Self {
|
||||
port,
|
||||
is_high: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn toggle(&mut self) {
|
||||
self.is_high = !self.is_high;
|
||||
self.port.bsrr().write(|write| {
|
||||
if self.is_high {
|
||||
write.bs13().set_bit()
|
||||
} else {
|
||||
write.br13().set_bit()
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,280 @@
|
||||
//! The one platform-owned RTIC task graph for all supported F722 profiles.
|
||||
|
||||
/// Defines the F722 application in the platform crate. All interrupt bindings
|
||||
/// are the finite platform-owned superset. Profile activation determines which
|
||||
/// acknowledged route has initialized hardware.
|
||||
macro_rules! f722_flight_application {
|
||||
($routes:path, $config:path) => {
|
||||
#[allow(unused_imports)]
|
||||
use $crate::__platform_internal::cmrt as _;
|
||||
#[allow(unused_imports)]
|
||||
use $crate::__platform_internal::panic as _;
|
||||
|
||||
// RTIC 2.3 expands hardware-vector shims that call unsafe PAC token
|
||||
// constructors inside generated unsafe functions. Those shims are
|
||||
// generated by RTIC, not hand-written platform accesses; all platform
|
||||
// unsafe register operations remain documented at their source.
|
||||
#[allow(unsafe_op_in_unsafe_fn)]
|
||||
#[$crate::__platform_internal::rtic_app::app(
|
||||
device = $crate::__platform_internal::pac::stm32f722,
|
||||
peripherals = true,
|
||||
dispatchers = [TIM6_DAC, TIM7]
|
||||
)]
|
||||
mod kaptan_f722_platform_app {
|
||||
use core::sync::atomic::{AtomicBool, AtomicU8, Ordering};
|
||||
|
||||
use $crate::f722::{
|
||||
F722Imu, F722ImuTransferCompletion, F722InitResources, F722LidarState,
|
||||
F722MavlinkState, F722Mtf01, F722Mtf01Uart, F722ServiceTimer,
|
||||
acknowledge_exti3, acknowledge_exti4,
|
||||
acknowledge_imu_rx_dma1, acknowledge_imu_rx_dma2, acknowledge_imu_spi1_error,
|
||||
acknowledge_imu_spi3_error, acknowledge_imu_tx_dma1, acknowledge_imu_tx_dma2,
|
||||
acknowledge_mavlink_dma1, acknowledge_mavlink_dma2, acknowledge_mavlink_rx_usart1,
|
||||
acknowledge_mavlink_rx_usart2, initialize, on_uart4_interrupt, process_imu,
|
||||
record_mavlink_completion,
|
||||
};
|
||||
#[allow(unused_imports)]
|
||||
use $crate::__platform_internal::rtic_app::Mutex;
|
||||
|
||||
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_DMA: u8 = 1 << 2;
|
||||
|
||||
// Hardware IRQs and both software workers coalesce event bits.
|
||||
// Release/Acquire pairs make each terminal acknowledgement visible
|
||||
// to the worker that consumes it; the queued bit prevents unbounded
|
||||
// software-task backlog while preserving the latest pending work.
|
||||
static IMU_EVENTS: AtomicU8 = AtomicU8::new(0);
|
||||
static MAVLINK_EVENTS: AtomicU8 = AtomicU8::new(0);
|
||||
static IMU_QUEUED: AtomicBool = AtomicBool::new(false);
|
||||
static MAVLINK_QUEUED: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
#[shared]
|
||||
struct Shared {
|
||||
mavlink: F722MavlinkState,
|
||||
lidar: F722LidarState,
|
||||
}
|
||||
|
||||
#[local]
|
||||
struct Local {
|
||||
imu: F722Imu,
|
||||
mtf01: Option<F722Mtf01>,
|
||||
_mtf01_uart: Option<F722Mtf01Uart>,
|
||||
timer: F722ServiceTimer,
|
||||
}
|
||||
|
||||
#[init]
|
||||
fn init(context: init::Context) -> (Shared, Local) {
|
||||
let routes = $routes().unwrap_or_else(|_| safe_idle());
|
||||
let F722InitResources {
|
||||
imu,
|
||||
mtf01,
|
||||
mtf01_uart,
|
||||
timer,
|
||||
lidar,
|
||||
mavlink,
|
||||
} = initialize(context.core, context.device, $config(), routes)
|
||||
.unwrap_or_else(|_| safe_idle());
|
||||
schedule_imu(IMU_EVENT_START);
|
||||
(
|
||||
Shared {
|
||||
mavlink,
|
||||
lidar,
|
||||
},
|
||||
Local {
|
||||
imu,
|
||||
mtf01,
|
||||
_mtf01_uart: mtf01_uart,
|
||||
timer,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
#[idle]
|
||||
fn idle(_: idle::Context) -> ! {
|
||||
loop {
|
||||
$crate::__platform_internal::cm::asm::wfi();
|
||||
}
|
||||
}
|
||||
|
||||
#[task(binds = EXTI4, priority = 15)]
|
||||
fn exti4(_: exti4::Context) {
|
||||
if acknowledge_exti4() {
|
||||
schedule_imu(IMU_EVENT_DRDY);
|
||||
}
|
||||
}
|
||||
|
||||
#[task(binds = EXTI3, priority = 15)]
|
||||
fn exti3(_: exti3::Context) {
|
||||
if acknowledge_exti3() {
|
||||
schedule_imu(IMU_EVENT_DRDY);
|
||||
}
|
||||
}
|
||||
|
||||
#[task(binds = DMA2_STREAM0, priority = 14)]
|
||||
fn imu_rx_dma2(_: imu_rx_dma2::Context) {
|
||||
match acknowledge_imu_rx_dma2() {
|
||||
F722ImuTransferCompletion::Complete => schedule_imu(IMU_EVENT_TRANSFER_COMPLETE),
|
||||
F722ImuTransferCompletion::TransferFault => schedule_imu(IMU_EVENT_TRANSFER_FAULT),
|
||||
F722ImuTransferCompletion::NotPending => {}
|
||||
}
|
||||
}
|
||||
|
||||
#[task(binds = DMA2_STREAM3, priority = 14)]
|
||||
fn imu_tx_dma2(_: imu_tx_dma2::Context) {
|
||||
if acknowledge_imu_tx_dma2() == F722ImuTransferCompletion::TransferFault {
|
||||
schedule_imu(IMU_EVENT_TRANSFER_FAULT);
|
||||
}
|
||||
}
|
||||
|
||||
#[task(binds = SPI1, priority = 14)]
|
||||
fn imu_spi1_error(_: imu_spi1_error::Context) {
|
||||
if acknowledge_imu_spi1_error() {
|
||||
schedule_imu(IMU_EVENT_TRANSFER_FAULT);
|
||||
}
|
||||
}
|
||||
|
||||
#[task(binds = DMA1_STREAM0, priority = 14)]
|
||||
fn imu_rx_dma1(_: imu_rx_dma1::Context) {
|
||||
match acknowledge_imu_rx_dma1() {
|
||||
F722ImuTransferCompletion::Complete => schedule_imu(IMU_EVENT_TRANSFER_COMPLETE),
|
||||
F722ImuTransferCompletion::TransferFault => schedule_imu(IMU_EVENT_TRANSFER_FAULT),
|
||||
F722ImuTransferCompletion::NotPending => {}
|
||||
}
|
||||
}
|
||||
|
||||
#[task(binds = DMA1_STREAM5, priority = 14)]
|
||||
fn imu_tx_dma1(_: imu_tx_dma1::Context) {
|
||||
if acknowledge_imu_tx_dma1() == F722ImuTransferCompletion::TransferFault {
|
||||
schedule_imu(IMU_EVENT_TRANSFER_FAULT);
|
||||
}
|
||||
}
|
||||
|
||||
#[task(binds = SPI3, priority = 14)]
|
||||
fn imu_spi3_error(_: imu_spi3_error::Context) {
|
||||
if acknowledge_imu_spi3_error() {
|
||||
schedule_imu(IMU_EVENT_TRANSFER_FAULT);
|
||||
}
|
||||
}
|
||||
|
||||
#[task(binds = UART4, priority = 14, local = [mtf01])]
|
||||
fn uart4(context: uart4::Context) {
|
||||
on_uart4_interrupt(context.local.mtf01);
|
||||
}
|
||||
|
||||
#[task(binds = I2C1_EV, priority = 11, shared = [lidar])]
|
||||
fn i2c1_event(mut context: i2c1_event::Context) {
|
||||
context
|
||||
.shared
|
||||
.lidar
|
||||
.lock(|lidar| lidar.on_i2c_event(false));
|
||||
}
|
||||
|
||||
#[task(binds = I2C1_ER, priority = 11, shared = [lidar])]
|
||||
fn i2c1_error(mut context: i2c1_error::Context) {
|
||||
context
|
||||
.shared
|
||||
.lidar
|
||||
.lock(|lidar| lidar.on_i2c_event(true));
|
||||
}
|
||||
|
||||
#[task(binds = DMA2_STREAM7, priority = 10)]
|
||||
fn mavlink_dma2(_: mavlink_dma2::Context) {
|
||||
let completion = acknowledge_mavlink_dma2();
|
||||
if completion != $crate::__platform_internal::dma::TxCompletion::NotPending {
|
||||
record_mavlink_completion(completion);
|
||||
schedule_mavlink(MAVLINK_EVENT_DMA);
|
||||
}
|
||||
}
|
||||
|
||||
#[task(binds = DMA1_STREAM6, priority = 10)]
|
||||
fn mavlink_dma1(_: mavlink_dma1::Context) {
|
||||
let completion = acknowledge_mavlink_dma1();
|
||||
if completion != $crate::__platform_internal::dma::TxCompletion::NotPending {
|
||||
record_mavlink_completion(completion);
|
||||
schedule_mavlink(MAVLINK_EVENT_DMA);
|
||||
}
|
||||
}
|
||||
|
||||
#[task(binds = USART1, priority = 8)]
|
||||
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 = 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 = TIM2, priority = 4, local = [timer], shared = [lidar])]
|
||||
fn tim2(mut context: tim2::Context) {
|
||||
context.local.timer.acknowledge_update();
|
||||
context.shared.lidar.lock(F722LidarState::on_timer);
|
||||
schedule_imu(IMU_EVENT_TIMER);
|
||||
schedule_mavlink(MAVLINK_EVENT_TIMER);
|
||||
}
|
||||
|
||||
#[task(priority = 13, local = [imu])]
|
||||
async fn imu_worker(context: imu_worker::Context) {
|
||||
let events = IMU_EVENTS.swap(0, Ordering::AcqRel);
|
||||
process_imu(context.local.imu, events);
|
||||
IMU_QUEUED.store(false, Ordering::Release);
|
||||
if IMU_EVENTS.load(Ordering::Acquire) != 0
|
||||
&& !IMU_QUEUED.swap(true, Ordering::AcqRel)
|
||||
&& imu_worker::spawn().is_err()
|
||||
{
|
||||
IMU_QUEUED.store(false, Ordering::Release);
|
||||
}
|
||||
}
|
||||
|
||||
#[task(priority = 9, shared = [mavlink])]
|
||||
async fn mavlink_worker(mut context: mavlink_worker::Context) {
|
||||
let events = MAVLINK_EVENTS.swap(0, Ordering::AcqRel);
|
||||
context
|
||||
.shared
|
||||
.mavlink
|
||||
.lock(|mavlink| mavlink.process_worker_events(events));
|
||||
MAVLINK_QUEUED.store(false, Ordering::Release);
|
||||
if MAVLINK_EVENTS.load(Ordering::Acquire) != 0
|
||||
&& !MAVLINK_QUEUED.swap(true, Ordering::AcqRel)
|
||||
&& mavlink_worker::spawn().is_err()
|
||||
{
|
||||
MAVLINK_QUEUED.store(false, Ordering::Release);
|
||||
}
|
||||
}
|
||||
|
||||
fn schedule_imu(event: u8) {
|
||||
IMU_EVENTS.fetch_or(event, Ordering::Release);
|
||||
if !IMU_QUEUED.swap(true, Ordering::AcqRel) && imu_worker::spawn().is_err() {
|
||||
IMU_QUEUED.store(false, Ordering::Release);
|
||||
}
|
||||
}
|
||||
|
||||
fn schedule_mavlink(event: u8) {
|
||||
MAVLINK_EVENTS.fetch_or(event, Ordering::Release);
|
||||
if !MAVLINK_QUEUED.swap(true, Ordering::AcqRel) && mavlink_worker::spawn().is_err()
|
||||
{
|
||||
MAVLINK_QUEUED.store(false, Ordering::Release);
|
||||
}
|
||||
}
|
||||
|
||||
fn safe_idle() -> ! {
|
||||
loop {
|
||||
$crate::__platform_internal::cm::asm::wfi();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) use f722_flight_application;
|
||||
@@ -0,0 +1,127 @@
|
||||
//! Platform-owned interrupt acknowledgement for all supported F722 profiles.
|
||||
|
||||
use core::sync::atomic::{AtomicU8, Ordering};
|
||||
|
||||
use super::peripherals::{Spi1, Spi3, Usart2Tx, Usart2TxDma};
|
||||
use super::{
|
||||
F722ImuTransferCompletion,
|
||||
peripherals::{ReceiveInterrupt, Usart1Tx, Usart1TxDma},
|
||||
};
|
||||
use kaptan_dma::TxCompletion;
|
||||
use stm32f7::stm32f722;
|
||||
|
||||
use crate::F722Profile;
|
||||
|
||||
const PROFILE_NONE: u8 = 0;
|
||||
const PROFILE_MPU6000_SPI1_USART1: u8 = 1;
|
||||
const PROFILE_ICM42688_SPI3_USART2: u8 = 2;
|
||||
|
||||
static ACTIVE_PROFILE: AtomicU8 = AtomicU8::new(PROFILE_NONE);
|
||||
|
||||
pub(crate) fn activate_profile(profile: F722Profile) {
|
||||
let encoded = match profile {
|
||||
F722Profile::Mpu6000Spi1Usart1 => PROFILE_MPU6000_SPI1_USART1,
|
||||
F722Profile::Icm42688Spi3Usart2 => PROFILE_ICM42688_SPI3_USART2,
|
||||
};
|
||||
ACTIVE_PROFILE.store(encoded, Ordering::Release);
|
||||
}
|
||||
|
||||
fn active_profile() -> u8 {
|
||||
ACTIVE_PROFILE.load(Ordering::Acquire)
|
||||
}
|
||||
|
||||
pub fn acknowledge_exti4() -> bool {
|
||||
if active_profile() != PROFILE_MPU6000_SPI1_USART1 {
|
||||
return false;
|
||||
}
|
||||
// SAFETY: the selected profile configured EXTI4 once before interrupts.
|
||||
// This ISR writes only the W1C pending bit for PA4 and does not alias the
|
||||
// worker-owned SPI or DMA registers.
|
||||
unsafe {
|
||||
(&*stm32f722::EXTI::ptr())
|
||||
.pr()
|
||||
.write(|write| write.pr4().clear());
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
pub fn acknowledge_exti3() -> bool {
|
||||
if active_profile() != PROFILE_ICM42688_SPI3_USART2 {
|
||||
return false;
|
||||
}
|
||||
// SAFETY: the selected profile configured EXTI3 once before interrupts.
|
||||
// This ISR writes only the W1C pending bit for PC3, disjoint from every
|
||||
// peripheral state owned by the deferred IMU worker.
|
||||
unsafe {
|
||||
(&*stm32f722::EXTI::ptr())
|
||||
.pr()
|
||||
.write(|write| write.pr3().clear());
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
pub fn acknowledge_imu_rx_dma2() -> F722ImuTransferCompletion {
|
||||
if active_profile() == PROFILE_MPU6000_SPI1_USART1 {
|
||||
return Spi1::acknowledge_rx_interrupt().into();
|
||||
}
|
||||
F722ImuTransferCompletion::NotPending
|
||||
}
|
||||
|
||||
pub fn acknowledge_imu_tx_dma2() -> F722ImuTransferCompletion {
|
||||
if active_profile() == PROFILE_MPU6000_SPI1_USART1 {
|
||||
return Spi1::acknowledge_tx_interrupt().into();
|
||||
}
|
||||
F722ImuTransferCompletion::NotPending
|
||||
}
|
||||
|
||||
pub fn acknowledge_imu_spi1_error() -> bool {
|
||||
active_profile() == PROFILE_MPU6000_SPI1_USART1 && Spi1::acknowledge_error_interrupt()
|
||||
}
|
||||
|
||||
pub fn acknowledge_imu_rx_dma1() -> F722ImuTransferCompletion {
|
||||
if active_profile() == PROFILE_ICM42688_SPI3_USART2 {
|
||||
return Spi3::acknowledge_rx_interrupt().into();
|
||||
}
|
||||
F722ImuTransferCompletion::NotPending
|
||||
}
|
||||
|
||||
pub fn acknowledge_imu_tx_dma1() -> F722ImuTransferCompletion {
|
||||
if active_profile() == PROFILE_ICM42688_SPI3_USART2 {
|
||||
return Spi3::acknowledge_tx_interrupt().into();
|
||||
}
|
||||
F722ImuTransferCompletion::NotPending
|
||||
}
|
||||
|
||||
pub fn acknowledge_imu_spi3_error() -> bool {
|
||||
active_profile() == PROFILE_ICM42688_SPI3_USART2 && Spi3::acknowledge_error_interrupt()
|
||||
}
|
||||
|
||||
pub fn acknowledge_mavlink_dma2() -> TxCompletion {
|
||||
if active_profile() == PROFILE_MPU6000_SPI1_USART1 {
|
||||
Usart1TxDma::acknowledge_interrupt()
|
||||
} else {
|
||||
TxCompletion::NotPending
|
||||
}
|
||||
}
|
||||
|
||||
pub fn acknowledge_mavlink_dma1() -> TxCompletion {
|
||||
if active_profile() == PROFILE_ICM42688_SPI3_USART2 {
|
||||
return Usart2TxDma::acknowledge_interrupt();
|
||||
}
|
||||
TxCompletion::NotPending
|
||||
}
|
||||
|
||||
pub fn acknowledge_mavlink_rx_usart1() -> ReceiveInterrupt {
|
||||
if active_profile() == PROFILE_MPU6000_SPI1_USART1 {
|
||||
Usart1Tx::acknowledge_receive_interrupt()
|
||||
} else {
|
||||
ReceiveInterrupt::NotPending
|
||||
}
|
||||
}
|
||||
|
||||
pub fn acknowledge_mavlink_rx_usart2() -> ReceiveInterrupt {
|
||||
if active_profile() == PROFILE_ICM42688_SPI3_USART2 {
|
||||
return Usart2Tx::acknowledge_receive_interrupt();
|
||||
}
|
||||
ReceiveInterrupt::NotPending
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
//! Finite STM32F722 execution profiles.
|
||||
//!
|
||||
//! This module is the sole owner of STM32F722 register routes and execution
|
||||
//! lifecycle. Its public-in-crate profile names describe useful electrical
|
||||
//! meaning; the RTIC bindings, PAC names, DMA streams and synchronization are
|
||||
//! intentionally contained here and in the macro template.
|
||||
|
||||
mod board;
|
||||
mod clock;
|
||||
mod data_channel;
|
||||
mod firmware;
|
||||
mod interrupts;
|
||||
mod motor_inhibit;
|
||||
mod peripherals;
|
||||
mod routes;
|
||||
mod sensors;
|
||||
|
||||
use crate::{BoardError, F722BoardRoutes};
|
||||
use kaptan_board::BoardConfig;
|
||||
|
||||
/// Completion classification crossing the RTIC interrupt template.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub(crate) enum F722ImuTransferCompletion {
|
||||
Complete,
|
||||
TransferFault,
|
||||
NotPending,
|
||||
}
|
||||
|
||||
impl From<kaptan_spi::SpiCompletion> for F722ImuTransferCompletion {
|
||||
fn from(value: kaptan_spi::SpiCompletion) -> Self {
|
||||
match value {
|
||||
kaptan_spi::SpiCompletion::Complete => Self::Complete,
|
||||
kaptan_spi::SpiCompletion::TransferFault => Self::TransferFault,
|
||||
kaptan_spi::SpiCompletion::NotPending => Self::NotPending,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub(crate) enum VerticalRoute {
|
||||
None,
|
||||
Mtf01,
|
||||
LidarLiteV3,
|
||||
}
|
||||
|
||||
pub(crate) use board::{
|
||||
F722InitResources, F722LidarState, F722MavlinkState, F722Mtf01, F722Mtf01Uart,
|
||||
F722ServiceTimer, on_uart4_interrupt, process_imu, record_mavlink_completion,
|
||||
validate_board_routes,
|
||||
};
|
||||
pub(crate) 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;
|
||||
|
||||
/// 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(
|
||||
core: cortex_m::Peripherals,
|
||||
device: stm32f7::stm32f722::Peripherals,
|
||||
config: BoardConfig,
|
||||
routes: F722BoardRoutes,
|
||||
) -> Result<F722InitResources, BoardError> {
|
||||
// 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)
|
||||
}
|
||||
|
||||
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
|
||||
);
|
||||
@@ -0,0 +1,67 @@
|
||||
//! Common former-motor-pin interlock for every F722 image.
|
||||
//!
|
||||
//! This is intentionally independent of profile initialization and is called
|
||||
//! as the first PAC access. The GPIO-low interlock is a separate electrical
|
||||
//! boundary from all telemetry initialization.
|
||||
|
||||
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) {
|
||||
rcc.ahb1enr().modify(|_, write| write.gpioben().set_bit());
|
||||
port.bsrr().write(|write| {
|
||||
write
|
||||
.br0()
|
||||
.set_bit()
|
||||
.br1()
|
||||
.set_bit()
|
||||
.br4()
|
||||
.set_bit()
|
||||
.br5()
|
||||
.set_bit()
|
||||
});
|
||||
port.otyper().modify(|_, write| {
|
||||
write
|
||||
.ot0()
|
||||
.push_pull()
|
||||
.ot1()
|
||||
.push_pull()
|
||||
.ot4()
|
||||
.push_pull()
|
||||
.ot5()
|
||||
.push_pull()
|
||||
});
|
||||
port.ospeedr().modify(|_, write| {
|
||||
write
|
||||
.ospeedr0()
|
||||
.low_speed()
|
||||
.ospeedr1()
|
||||
.low_speed()
|
||||
.ospeedr4()
|
||||
.low_speed()
|
||||
.ospeedr5()
|
||||
.low_speed()
|
||||
});
|
||||
port.pupdr().modify(|_, write| {
|
||||
write
|
||||
.pupdr0()
|
||||
.floating()
|
||||
.pupdr1()
|
||||
.floating()
|
||||
.pupdr4()
|
||||
.floating()
|
||||
.pupdr5()
|
||||
.floating()
|
||||
});
|
||||
port.moder().modify(|_, write| {
|
||||
write
|
||||
.moder0()
|
||||
.output()
|
||||
.moder1()
|
||||
.output()
|
||||
.moder4()
|
||||
.output()
|
||||
.moder5()
|
||||
.output()
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
//! Deterministic STM32F722 PLL clock-source selection.
|
||||
|
||||
use kaptan_log::KaptanError;
|
||||
use stm32f7::stm32f722;
|
||||
|
||||
const CLOCK_SWITCH_POLL_LIMIT: u16 = 1_024;
|
||||
|
||||
/// Failure to establish the 216 MHz PLL clock profile.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, KaptanError)]
|
||||
pub enum ClockProfileError {
|
||||
HsiNotReady,
|
||||
SystemClockNotHsi,
|
||||
PllNotStopped,
|
||||
PllNotReady,
|
||||
OverDriveNotReady,
|
||||
OverDriveSwitchNotReady,
|
||||
SystemClockNotPll,
|
||||
}
|
||||
|
||||
/// Selects the F722 platform's HSI PLL profile.
|
||||
///
|
||||
/// SYSCLK/HCLK run at 216 MHz; APB1 is 54 MHz (timers run at 108 MHz), and
|
||||
/// APB2 is 108 MHz. HSI is the PLL source, so no external-crystal assumption
|
||||
/// is introduced.
|
||||
pub fn select_pll216_profile(
|
||||
rcc: &stm32f722::RCC,
|
||||
pwr: &stm32f722::PWR,
|
||||
flash: &stm32f722::FLASH,
|
||||
) -> Result<(), ClockProfileError> {
|
||||
rcc.cr().modify(|_, write| write.hsion().on());
|
||||
if !wait_until(|| rcc.cr().read().hsirdy().is_ready()) {
|
||||
return Err(ClockProfileError::HsiNotReady);
|
||||
}
|
||||
|
||||
// Return to HSI before changing a PLL left configured by an earlier boot stage.
|
||||
rcc.cfgr().modify(|_, write| write.sw().hsi());
|
||||
if !wait_until(|| rcc.cfgr().read().sws().is_hsi()) {
|
||||
return Err(ClockProfileError::SystemClockNotHsi);
|
||||
}
|
||||
rcc.cr().modify(|_, write| write.pllon().off());
|
||||
if !wait_until(|| !rcc.cr().read().pllrdy().is_ready()) {
|
||||
return Err(ClockProfileError::PllNotStopped);
|
||||
}
|
||||
|
||||
// Scale 1, over-drive and seven flash wait states are required at 216 MHz.
|
||||
rcc.apb1enr().modify(|_, write| write.pwren().set_bit());
|
||||
pwr.cr1().modify(|read, write| {
|
||||
// SAFETY: this read-modify-write changes only the documented voltage-scale and
|
||||
// over-drive-enable fields; it preserves every other PWR_CR1 bit owned by boot.
|
||||
unsafe { write.bits((read.bits() & !(0b11 << 14)) | (0b11 << 14) | (1 << 16)) }
|
||||
});
|
||||
if !wait_until(|| pwr.csr1().read().odrdy().bit_is_set()) {
|
||||
return Err(ClockProfileError::OverDriveNotReady);
|
||||
}
|
||||
pwr.cr1().modify(|_, write| write.odswen().set_bit());
|
||||
if !wait_until(|| pwr.csr1().read().odswrdy().bit_is_set()) {
|
||||
return Err(ClockProfileError::OverDriveSwitchNotReady);
|
||||
}
|
||||
flash.acr().modify(|read, write| {
|
||||
// SAFETY: these are the STM32F7-documented latency, instruction-cache and
|
||||
// data-cache fields for 216 MHz; the read-modify-write retains other ACR bits.
|
||||
unsafe { write.bits((read.bits() & !0x0f) | 7 | (1 << 8) | (1 << 9)) }
|
||||
});
|
||||
|
||||
// HSI / 16 * 432 / 2 = 216 MHz; Q=9 supplies a 48 MHz domain.
|
||||
rcc.pllcfgr().write(|write| {
|
||||
// SAFETY: 16, 432, P=/2 (the reset encoding), and Q=9 are the valid PLL
|
||||
// divisors described above; the PLL is disabled before this full-register write.
|
||||
unsafe { write.bits(16 | (432 << 6) | (9 << 24)) }
|
||||
});
|
||||
rcc.cr().modify(|_, write| write.pllon().on());
|
||||
if !wait_until(|| rcc.cr().read().pllrdy().is_ready()) {
|
||||
return Err(ClockProfileError::PllNotReady);
|
||||
}
|
||||
rcc.cfgr().modify(|read, write| {
|
||||
// AHB /1, APB1 /4, APB2 /2, SYSCLK = PLL.
|
||||
// SAFETY: this preserves every CFGR field except the documented prescaler and
|
||||
// system-clock-source fields, which are written with encodings valid for F722.
|
||||
unsafe {
|
||||
write.bits(
|
||||
(read.bits() & !((0x0f << 4) | (0x07 << 10) | (0x07 << 13) | 0x03))
|
||||
| (0b101 << 10)
|
||||
| (0b100 << 13)
|
||||
| 0b10,
|
||||
)
|
||||
}
|
||||
});
|
||||
if !wait_until(|| rcc.cfgr().read().sws().is_pll()) {
|
||||
return Err(ClockProfileError::SystemClockNotPll);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn wait_until(mut ready: impl FnMut() -> bool) -> bool {
|
||||
for _ in 0..CLOCK_SWITCH_POLL_LIMIT {
|
||||
if ready() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
//! D-cache-disabled DMA memory policy for STM32F722 boards.
|
||||
|
||||
use kaptan_dma::{DmaReadableBuffer, DmaTxBufferFactory};
|
||||
///
|
||||
/// DMA transmit memory policy for a platform with D-cache disabled.
|
||||
///
|
||||
/// The platform linker policy must also place transfer frames in SRAM reachable
|
||||
/// by the selected DMA controller. This policy does not support a cached board;
|
||||
/// such a platform requires a distinct policy that performs cache maintenance.
|
||||
pub struct DcacheDisabledDmaMemory {
|
||||
_private: (),
|
||||
}
|
||||
|
||||
impl DcacheDisabledDmaMemory {
|
||||
/// Declares that the selected platform disabled D-cache before DMA setup.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// - D-cache must remain disabled for every CPU access to DMA-owned frames.
|
||||
/// - The linker policy must locate those frames in DMA-visible SRAM.
|
||||
pub const unsafe fn new() -> Self {
|
||||
Self { _private: () }
|
||||
}
|
||||
}
|
||||
|
||||
impl DmaTxBufferFactory for DcacheDisabledDmaMemory {
|
||||
type Error = core::convert::Infallible;
|
||||
|
||||
fn prepare<'a>(&mut self, bytes: &'a [u8]) -> Result<DmaReadableBuffer<'a>, Self::Error> {
|
||||
// SAFETY: `Self` can exist only after the platform proved its D-cache and
|
||||
// linker-memory policy. The runtime retains frame ownership until a
|
||||
// terminal DMA outcome or successful recovery.
|
||||
Ok(unsafe { DmaReadableBuffer::new_unchecked(bytes) })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
//! Named STM32F722 GPIO routes used by supported board wiring.
|
||||
|
||||
use stm32f7::stm32f722;
|
||||
|
||||
/// Configures PA5, PA6, and PA7 for SPI1 alternate function 5.
|
||||
pub fn configure_spi1_pins_pa5_pa6_pa7(rcc: &stm32f722::RCC, port: &stm32f722::GPIOA) {
|
||||
rcc.ahb1enr().modify(|_, write| write.gpioaen().set_bit());
|
||||
port.moder().modify(|_, write| {
|
||||
write
|
||||
.moder5()
|
||||
.alternate()
|
||||
.moder6()
|
||||
.alternate()
|
||||
.moder7()
|
||||
.alternate()
|
||||
});
|
||||
port.afrl().modify(|_, write| {
|
||||
// SAFETY: AF5 selects SPI1 on PA5, PA6, and PA7 on STM32F722.
|
||||
unsafe {
|
||||
write
|
||||
.afrl5()
|
||||
.bits(0b0101)
|
||||
.afrl6()
|
||||
.bits(0b0101)
|
||||
.afrl7()
|
||||
.bits(0b0101)
|
||||
}
|
||||
});
|
||||
port.otyper()
|
||||
.modify(|_, write| write.ot5().push_pull().ot6().push_pull().ot7().push_pull());
|
||||
port.ospeedr().modify(|_, write| {
|
||||
write
|
||||
.ospeedr5()
|
||||
.high_speed()
|
||||
.ospeedr6()
|
||||
.high_speed()
|
||||
.ospeedr7()
|
||||
.high_speed()
|
||||
});
|
||||
port.pupdr().modify(|_, write| {
|
||||
write
|
||||
.pupdr5()
|
||||
.floating()
|
||||
.pupdr6()
|
||||
.floating()
|
||||
.pupdr7()
|
||||
.floating()
|
||||
});
|
||||
}
|
||||
|
||||
/// Configures PC10, PC11, and PC12 for SPI3 alternate function 6.
|
||||
pub fn configure_spi3_pins_pc10_pc11_pc12(rcc: &stm32f722::RCC, port: &stm32f722::GPIOC) {
|
||||
rcc.ahb1enr().modify(|_, write| write.gpiocen().set_bit());
|
||||
port.moder().modify(|_, write| {
|
||||
write
|
||||
.moder10()
|
||||
.alternate()
|
||||
.moder11()
|
||||
.alternate()
|
||||
.moder12()
|
||||
.alternate()
|
||||
});
|
||||
port.afrh().modify(|_, write| {
|
||||
// SAFETY: AF6 selects SPI3 on PC10, PC11, and PC12 on STM32F722.
|
||||
unsafe {
|
||||
write
|
||||
.afrh10()
|
||||
.bits(0b0110)
|
||||
.afrh11()
|
||||
.bits(0b0110)
|
||||
.afrh12()
|
||||
.bits(0b0110)
|
||||
}
|
||||
});
|
||||
port.otyper().modify(|_, write| {
|
||||
write
|
||||
.ot10()
|
||||
.push_pull()
|
||||
.ot11()
|
||||
.push_pull()
|
||||
.ot12()
|
||||
.push_pull()
|
||||
});
|
||||
port.ospeedr().modify(|_, write| {
|
||||
write
|
||||
.ospeedr10()
|
||||
.high_speed()
|
||||
.ospeedr11()
|
||||
.high_speed()
|
||||
.ospeedr12()
|
||||
.high_speed()
|
||||
});
|
||||
port.pupdr().modify(|_, write| {
|
||||
write
|
||||
.pupdr10()
|
||||
.floating()
|
||||
.pupdr11()
|
||||
.floating()
|
||||
.pupdr12()
|
||||
.floating()
|
||||
});
|
||||
}
|
||||
|
||||
/// Configures PB8/PB9 for I²C1 alternate function 4.
|
||||
pub fn configure_i2c1_pins_pb8_pb9(rcc: &stm32f722::RCC, port: &stm32f722::GPIOB) {
|
||||
rcc.ahb1enr().modify(|_, w| w.gpioben().set_bit());
|
||||
port.moder()
|
||||
.modify(|_, w| w.moder8().alternate().moder9().alternate());
|
||||
// SAFETY: AF4 is the documented I2C1 alternate-function selector for PB8/PB9.
|
||||
port.afrh()
|
||||
.modify(|_, w| unsafe { w.afrh8().bits(4).afrh9().bits(4) });
|
||||
port.otyper()
|
||||
.modify(|_, w| w.ot8().open_drain().ot9().open_drain());
|
||||
port.ospeedr()
|
||||
.modify(|_, w| w.ospeedr8().high_speed().ospeedr9().high_speed());
|
||||
port.pupdr()
|
||||
.modify(|_, w| w.pupdr8().floating().pupdr9().floating());
|
||||
}
|
||||
|
||||
/// Configures PA4 as the input pin for an EXTI4 route.
|
||||
///
|
||||
/// A platform route must select this explicitly rather than inheriting the pin mode
|
||||
/// left by a bootloader or an earlier firmware image.
|
||||
pub fn configure_exti4_pin_pa4(port: &stm32f722::GPIOA) {
|
||||
port.moder().modify(|_, write| write.moder4().input());
|
||||
port.pupdr().modify(|_, write| write.pupdr4().floating());
|
||||
}
|
||||
|
||||
/// Configures PC3 as the input pin for an EXTI3 route.
|
||||
pub fn configure_exti3_pin_pc3(port: &stm32f722::GPIOC) {
|
||||
port.moder().modify(|_, write| write.moder3().input());
|
||||
port.pupdr().modify(|_, write| write.pupdr3().floating());
|
||||
}
|
||||
|
||||
/// Configures PA9 for USART1 transmit alternate function 7.
|
||||
pub fn configure_usart1_tx_pin_pa9(port: &stm32f722::GPIOA) {
|
||||
port.moder().modify(|_, write| write.moder9().alternate());
|
||||
port.afrh().modify(|_, write| {
|
||||
// SAFETY: AF7 selects USART1 TX on PA9 on STM32F722.
|
||||
unsafe { write.afrh9().bits(0b0111) }
|
||||
});
|
||||
port.otyper().modify(|_, write| write.ot9().push_pull());
|
||||
port.ospeedr()
|
||||
.modify(|_, write| write.ospeedr9().high_speed());
|
||||
port.pupdr().modify(|_, write| write.pupdr9().floating());
|
||||
}
|
||||
|
||||
/// Configures PA10 for USART1 receive alternate function 7.
|
||||
pub fn configure_usart1_rx_pin_pa10(port: &stm32f722::GPIOA) {
|
||||
port.moder().modify(|_, write| write.moder10().alternate());
|
||||
port.afrh().modify(|_, write| {
|
||||
// SAFETY: AF7 selects USART1 RX on PA10 on STM32F722.
|
||||
unsafe { write.afrh10().bits(0b0111) }
|
||||
});
|
||||
// USART is idle-high. Leaving PA10 floating before its peer drives the
|
||||
// line can manufacture framing and noise errors. The weak internal pull-up
|
||||
// supplies only the electrically correct idle level; it does not mask a
|
||||
// driven low, framing, noise, or overrun fault.
|
||||
port.pupdr().modify(|_, write| write.pupdr10().pull_up());
|
||||
}
|
||||
|
||||
/// Configures PA2/PA3 for USART2 transmit/receive alternate function 7.
|
||||
pub fn configure_usart2_pins_pa2_pa3(port: &stm32f722::GPIOA) {
|
||||
port.moder()
|
||||
.modify(|_, write| write.moder2().alternate().moder3().alternate());
|
||||
port.afrl().modify(|_, write| {
|
||||
// SAFETY: AF7 selects USART2 TX/RX on PA2/PA3 on STM32F722.
|
||||
unsafe { write.afrl2().bits(0b0111).afrl3().bits(0b0111) }
|
||||
});
|
||||
port.otyper().modify(|_, write| write.ot2().push_pull());
|
||||
port.ospeedr()
|
||||
.modify(|_, write| write.ospeedr2().high_speed());
|
||||
port.pupdr()
|
||||
.modify(|_, write| write.pupdr2().floating().pupdr3().floating());
|
||||
}
|
||||
|
||||
/// Configures PA0/PA1 for UART4 alternate function 8.
|
||||
pub fn configure_uart4_pins_pa0_pa1(port: &stm32f722::GPIOA) {
|
||||
port.moder()
|
||||
.modify(|_, write| write.moder0().alternate().moder1().alternate());
|
||||
port.afrl().modify(|_, write| {
|
||||
// SAFETY: AF8 selects UART4 TX/RX on PA0/PA1 on STM32F722.
|
||||
unsafe { write.afrl0().bits(0b1000).afrl1().bits(0b1000) }
|
||||
});
|
||||
port.otyper().modify(|_, write| write.ot0().push_pull());
|
||||
port.ospeedr()
|
||||
.modify(|_, write| write.ospeedr0().high_speed());
|
||||
port.pupdr()
|
||||
.modify(|_, write| write.pupdr0().floating().pupdr1().floating());
|
||||
}
|
||||
|
||||
/// Active-low status LED connected to PD2.
|
||||
pub struct ActiveLowLedPd2 {
|
||||
port: stm32f722::GPIOD,
|
||||
is_on: bool,
|
||||
}
|
||||
|
||||
impl ActiveLowLedPd2 {
|
||||
/// Configures PD2 as an initially-off active-low output.
|
||||
pub fn new(rcc: &stm32f722::RCC, port: stm32f722::GPIOD) -> Self {
|
||||
rcc.ahb1enr().modify(|_, write| write.gpioden().set_bit());
|
||||
port.moder().modify(|_, write| write.moder2().output());
|
||||
port.otyper().modify(|_, write| write.ot2().push_pull());
|
||||
port.ospeedr()
|
||||
.modify(|_, write| write.ospeedr2().low_speed());
|
||||
port.pupdr().modify(|_, write| write.pupdr2().floating());
|
||||
port.bsrr().write(|write| write.bs2().set_bit());
|
||||
|
||||
Self { port, is_on: false }
|
||||
}
|
||||
|
||||
/// Toggles the LED state.
|
||||
pub fn toggle(&mut self) {
|
||||
self.is_on = !self.is_on;
|
||||
if self.is_on {
|
||||
self.port.bsrr().write(|write| write.br2().set_bit());
|
||||
} else {
|
||||
self.port.bsrr().write(|write| write.bs2().set_bit());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,362 @@
|
||||
//! STM32F722 I²C1 interrupt-driven controller.
|
||||
|
||||
use kaptan_i2c::{I2cBus, I2cCompletion};
|
||||
use kaptan_log::KaptanError;
|
||||
use stm32f7::stm32f722;
|
||||
|
||||
const MAX_BYTES: usize = 255;
|
||||
const CR1_INTERRUPTS: u32 = (1 << 0) | (1 << 1) | (1 << 2) | (1 << 4) | (1 << 5) | (1 << 7);
|
||||
const ISR_TXIS: u32 = 1 << 1;
|
||||
const ISR_RXNE: u32 = 1 << 2;
|
||||
const ISR_NACKF: u32 = 1 << 4;
|
||||
const ISR_STOPF: u32 = 1 << 5;
|
||||
const ISR_BERR: u32 = 1 << 8;
|
||||
const ISR_ARLO: u32 = 1 << 9;
|
||||
const ISR_OVR: u32 = 1 << 10;
|
||||
const ISR_ERRORS: u32 = ISR_BERR | ISR_ARLO | ISR_OVR;
|
||||
const ICR_NACKCF: u32 = 1 << 4;
|
||||
const ICR_STOPCF: u32 = 1 << 5;
|
||||
const ICR_ERRORS: u32 = (1 << 8) | (1 << 9) | (1 << 10);
|
||||
const ICR_TERMINAL: u32 = ICR_NACKCF | ICR_STOPCF | ICR_ERRORS;
|
||||
const ICR_FAULTS: u32 = ICR_NACKCF | ICR_ERRORS;
|
||||
const CR2_STOP: u32 = 1 << 14;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, KaptanError)]
|
||||
pub enum I2c1Error {
|
||||
InvalidLength,
|
||||
InvalidAddress,
|
||||
Busy,
|
||||
}
|
||||
|
||||
/// Platform-selected I²C1 electrical timing.
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct I2c1Config {
|
||||
timing: u32,
|
||||
}
|
||||
|
||||
impl I2c1Config {
|
||||
/// Creates a configuration from an STM32 `I2C_TIMINGR` register value.
|
||||
pub const fn from_timing_register(timing: u32) -> Self {
|
||||
Self { timing }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum Direction {
|
||||
Write,
|
||||
Read,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
enum TransferState {
|
||||
Idle,
|
||||
Active,
|
||||
Stopping,
|
||||
}
|
||||
|
||||
fn read_index_after_event(direction: Direction, flags: u32, index: usize, length: usize) -> usize {
|
||||
if matches!(direction, Direction::Read) && flags & ISR_RXNE != 0 && index < length {
|
||||
index + 1
|
||||
} else {
|
||||
index
|
||||
}
|
||||
}
|
||||
|
||||
fn completion_after_stop(index: usize, length: usize) -> I2cCompletion {
|
||||
if index == length {
|
||||
I2cCompletion::Complete
|
||||
} else {
|
||||
I2cCompletion::TransferFault
|
||||
}
|
||||
}
|
||||
|
||||
impl TransferState {
|
||||
fn accepts_start(self, bus_busy: bool) -> bool {
|
||||
self == Self::Idle && !bus_busy
|
||||
}
|
||||
|
||||
fn after_stop(self) -> Self {
|
||||
if self == Self::Stopping {
|
||||
Self::Idle
|
||||
} else {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
fn after_driver_completion(self) -> Self {
|
||||
if self == Self::Active {
|
||||
Self::Idle
|
||||
} else {
|
||||
self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Owns I²C1 while one interrupt-driven transaction is active.
|
||||
pub struct I2c1 {
|
||||
peripheral: stm32f722::I2C1,
|
||||
buffer: [u8; MAX_BYTES],
|
||||
length: usize,
|
||||
index: usize,
|
||||
direction: Direction,
|
||||
state: TransferState,
|
||||
}
|
||||
|
||||
impl I2c1 {
|
||||
/// Configures I²C1 using platform-selected pin electrical timing.
|
||||
pub fn new(rcc: &stm32f722::RCC, peripheral: stm32f722::I2C1, config: I2c1Config) -> Self {
|
||||
rcc.apb1rstr().modify(|_, w| w.i2c1rst().set_bit());
|
||||
rcc.apb1rstr().modify(|_, w| w.i2c1rst().clear_bit());
|
||||
rcc.apb1enr().modify(|_, w| w.i2c1en().set_bit());
|
||||
// SAFETY: zero is the documented reset configuration for CR1.
|
||||
peripheral.cr1().write(|w| unsafe { w.bits(0) });
|
||||
// SAFETY: `timing` is a platform-selected I2C_TIMINGR bit pattern.
|
||||
peripheral
|
||||
.timingr()
|
||||
.write(|w| unsafe { w.bits(config.timing) });
|
||||
// SAFETY: these are only documented CR1 interrupt-enable and PE bits.
|
||||
peripheral
|
||||
.cr1()
|
||||
.write(|w| unsafe { w.bits(CR1_INTERRUPTS) });
|
||||
Self {
|
||||
peripheral,
|
||||
buffer: [0; MAX_BYTES],
|
||||
length: 0,
|
||||
index: 0,
|
||||
direction: Direction::Write,
|
||||
state: TransferState::Idle,
|
||||
}
|
||||
}
|
||||
|
||||
/// Advances the event IRQ; returns a terminal result only after STOPF.
|
||||
pub fn acknowledge_event_interrupt(&mut self) -> I2cCompletion {
|
||||
let flags = self.peripheral.isr().read().bits();
|
||||
if self.state != TransferState::Active {
|
||||
self.clear_inactive_event_flags(flags);
|
||||
return I2cCompletion::NotPending;
|
||||
}
|
||||
if flags & ISR_ERRORS != 0 {
|
||||
self.abort();
|
||||
self.acknowledge_stop_if_present(flags);
|
||||
return I2cCompletion::TransferFault;
|
||||
}
|
||||
if flags & ISR_NACKF != 0 {
|
||||
self.abort();
|
||||
self.acknowledge_stop_if_present(flags);
|
||||
return I2cCompletion::TransferFault;
|
||||
}
|
||||
// With AUTOEND a final RXNE and STOPF may be visible in one ISR
|
||||
// entry. Drain RXDR before retiring the transfer: otherwise RXNE
|
||||
// remains asserted after software advances its state, retriggering
|
||||
// I2C1_EV indefinitely at priority 11.
|
||||
if matches!(self.direction, Direction::Write)
|
||||
&& flags & ISR_TXIS != 0
|
||||
&& self.index < self.length
|
||||
{
|
||||
// SAFETY: the value is an eight-bit TXDR payload.
|
||||
self.peripheral
|
||||
.txdr()
|
||||
.write(|w| unsafe { w.bits(u32::from(self.buffer[self.index])) });
|
||||
self.index += 1;
|
||||
}
|
||||
let next_read_index =
|
||||
read_index_after_event(self.direction, flags, self.index, self.length);
|
||||
if next_read_index != self.index {
|
||||
self.buffer[self.index] = self.peripheral.rxdr().read().bits() as u8;
|
||||
self.index = next_read_index;
|
||||
}
|
||||
if flags & ISR_STOPF != 0 {
|
||||
self.acknowledge_stop();
|
||||
return completion_after_stop(self.index, self.length);
|
||||
}
|
||||
I2cCompletion::NotPending
|
||||
}
|
||||
|
||||
pub fn acknowledge_error_interrupt(&mut self) -> I2cCompletion {
|
||||
let flags = self.peripheral.isr().read().bits();
|
||||
if flags & ISR_ERRORS == 0 {
|
||||
return I2cCompletion::NotPending;
|
||||
}
|
||||
if self.state == TransferState::Active {
|
||||
self.abort();
|
||||
self.acknowledge_stop_if_present(flags);
|
||||
I2cCompletion::TransferFault
|
||||
} else {
|
||||
// A STOP may have already retired the software transaction before
|
||||
// the error IRQ is serviced. Its flags still must be acknowledged
|
||||
// or this high-priority IRQ will continuously retrigger.
|
||||
self.clear_inactive_event_flags(flags);
|
||||
I2cCompletion::NotPending
|
||||
}
|
||||
}
|
||||
|
||||
fn start(&mut self, address: u8, length: usize, direction: Direction) -> Result<(), I2c1Error> {
|
||||
if length == 0 || length > MAX_BYTES {
|
||||
return Err(I2c1Error::InvalidLength);
|
||||
}
|
||||
if address > 0x7f {
|
||||
return Err(I2c1Error::InvalidAddress);
|
||||
}
|
||||
if !self.state.accepts_start(self.bus_is_busy()) {
|
||||
return Err(I2c1Error::Busy);
|
||||
}
|
||||
// SAFETY: ICR_TERMINAL contains only documented write-one-to-clear flags.
|
||||
self.peripheral
|
||||
.icr()
|
||||
.write(|w| unsafe { w.bits(ICR_TERMINAL) });
|
||||
self.length = length;
|
||||
self.index = 0;
|
||||
self.direction = direction;
|
||||
self.state = TransferState::Active;
|
||||
let direction_bit = if matches!(direction, Direction::Read) {
|
||||
1 << 10
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let cr2 = (u32::from(address) << 1)
|
||||
| direction_bit
|
||||
| ((length as u32) << 16)
|
||||
| (1 << 25)
|
||||
| (1 << 13);
|
||||
// SAFETY: `cr2` contains a validated 7-bit address and documented transfer fields.
|
||||
self.peripheral.cr2().write(|w| unsafe { w.bits(cr2) });
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn abort(&mut self) {
|
||||
// Keep software ownership closed until STOPF is observed. A requested
|
||||
// STOP may be delayed indefinitely by a held bus, so this state keeps
|
||||
// a new START from being issued before the terminal STOPF arrives.
|
||||
self.state = TransferState::Stopping;
|
||||
// SAFETY: this only requests STOP while preserving the current CR2 fields.
|
||||
self.peripheral
|
||||
.cr2()
|
||||
.modify(|read, write| unsafe { write.bits(read.bits() | CR2_STOP) });
|
||||
// SAFETY: ICR_FAULTS contains only documented fault write-one-to-clear flags.
|
||||
self.peripheral
|
||||
.icr()
|
||||
.write(|w| unsafe { w.bits(ICR_FAULTS) });
|
||||
}
|
||||
|
||||
fn bus_is_busy(&self) -> bool {
|
||||
self.peripheral.isr().read().bits() & (1 << 15) != 0
|
||||
}
|
||||
|
||||
fn acknowledge_stop(&mut self) {
|
||||
// SAFETY: ICR_STOPCF is the documented STOP write-one-to-clear bit.
|
||||
self.peripheral
|
||||
.icr()
|
||||
.write(|w| unsafe { w.bits(ICR_STOPCF) });
|
||||
self.state = self.state.after_stop();
|
||||
}
|
||||
|
||||
fn acknowledge_stop_if_present(&mut self, flags: u32) {
|
||||
if flags & ISR_STOPF != 0 {
|
||||
self.acknowledge_stop();
|
||||
}
|
||||
}
|
||||
|
||||
fn clear_inactive_event_flags(&mut self, flags: u32) {
|
||||
// An abort may request STOP after the driver has already released the
|
||||
// transaction. Drain that terminal IRQ instead of leaving I2C1_EV
|
||||
// pending forever. RXNE is normally impossible here, but draining it
|
||||
// keeps a malformed terminal transfer from becoming an IRQ storm.
|
||||
if flags & ISR_RXNE != 0 {
|
||||
let _ = self.peripheral.rxdr().read().bits();
|
||||
}
|
||||
if flags & (ISR_NACKF | ISR_ERRORS) != 0 {
|
||||
// SAFETY: ICR_FAULTS contains only documented fault write-one-to-clear flags.
|
||||
self.peripheral
|
||||
.icr()
|
||||
.write(|w| unsafe { w.bits(ICR_FAULTS) });
|
||||
}
|
||||
self.acknowledge_stop_if_present(flags);
|
||||
if flags & ISR_TXIS != 0 {
|
||||
self.abort();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl I2cBus for I2c1 {
|
||||
type StartError = I2c1Error;
|
||||
fn start_write(&mut self, address: u8, write: &[u8]) -> Result<(), Self::StartError> {
|
||||
if write.is_empty() || write.len() > MAX_BYTES {
|
||||
return Err(I2c1Error::InvalidLength);
|
||||
}
|
||||
if !self.state.accepts_start(self.bus_is_busy()) {
|
||||
return Err(I2c1Error::Busy);
|
||||
}
|
||||
// Populate controller-owned storage before `start` makes the
|
||||
// transaction visible to the higher-priority I²C event IRQ.
|
||||
self.buffer[..write.len()].copy_from_slice(write);
|
||||
self.start(address, write.len(), Direction::Write)?;
|
||||
Ok(())
|
||||
}
|
||||
fn start_read(&mut self, address: u8, length: usize) -> Result<(), Self::StartError> {
|
||||
self.start(address, length, Direction::Read)
|
||||
}
|
||||
fn complete_transaction(&mut self, completion: I2cCompletion, read: Option<&mut [u8]>) {
|
||||
if completion == I2cCompletion::Complete
|
||||
&& matches!(self.direction, Direction::Read)
|
||||
&& let Some(read) = read
|
||||
{
|
||||
debug_assert_eq!(read.len(), self.length);
|
||||
read.copy_from_slice(&self.buffer[..self.length]);
|
||||
}
|
||||
self.state = self.state.after_driver_completion();
|
||||
self.length = 0;
|
||||
self.index = 0;
|
||||
}
|
||||
|
||||
fn cancel_transaction(&mut self) {
|
||||
if self.state == TransferState::Active {
|
||||
self.abort();
|
||||
}
|
||||
self.length = 0;
|
||||
self.index = 0;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
Direction, I2cCompletion, ICR_ERRORS, ICR_FAULTS, ICR_NACKCF, ICR_STOPCF, ISR_RXNE,
|
||||
ISR_STOPF, TransferState, completion_after_stop, read_index_after_event,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn rejects_starts_while_a_transaction_or_recovery_is_pending() {
|
||||
assert!(TransferState::Idle.accepts_start(false));
|
||||
assert!(!TransferState::Idle.accepts_start(true));
|
||||
assert!(!TransferState::Active.accepts_start(false));
|
||||
assert!(!TransferState::Stopping.accepts_start(false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn abort_stays_closed_until_stop_and_normal_completion_releases_ownership() {
|
||||
assert_eq!(TransferState::Stopping.after_stop(), TransferState::Idle);
|
||||
assert_eq!(TransferState::Active.after_stop(), TransferState::Active);
|
||||
assert_eq!(
|
||||
TransferState::Active.after_driver_completion(),
|
||||
TransferState::Idle
|
||||
);
|
||||
assert_eq!(
|
||||
TransferState::Stopping.after_driver_completion(),
|
||||
TransferState::Stopping
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fault_cleanup_does_not_acknowledge_stop_before_stop_is_observed() {
|
||||
assert_eq!(ICR_FAULTS, ICR_NACKCF | ICR_ERRORS);
|
||||
assert_eq!(ICR_FAULTS & ICR_STOPCF, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rxne_and_stopf_complete_a_final_read_in_one_event() {
|
||||
let index = read_index_after_event(Direction::Read, ISR_RXNE | ISR_STOPF, 1, 2);
|
||||
|
||||
assert_eq!(index, 2);
|
||||
assert_eq!(completion_after_stop(index, 2), I2cCompletion::Complete);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
//! STM32F722 peripheral implementations selected by supported boards.
|
||||
//!
|
||||
//! This module exports only typed peripheral building blocks. The family
|
||||
//! platform selects pins, clocks, interrupt priorities, and static ownership.
|
||||
|
||||
mod clock;
|
||||
mod dma_buffer;
|
||||
mod gpio;
|
||||
mod i2c1;
|
||||
mod spi1;
|
||||
mod spi3;
|
||||
mod tim2;
|
||||
mod time;
|
||||
mod usart;
|
||||
mod usart1_tx_dma2_stream7;
|
||||
mod usart2_tx_dma1_stream6;
|
||||
|
||||
pub use clock::select_pll216_profile;
|
||||
pub use dma_buffer::DcacheDisabledDmaMemory;
|
||||
pub use gpio::{
|
||||
ActiveLowLedPd2, configure_exti3_pin_pc3, configure_exti4_pin_pa4, configure_i2c1_pins_pb8_pb9,
|
||||
configure_spi1_pins_pa5_pa6_pa7, configure_spi3_pins_pc10_pc11_pc12,
|
||||
configure_uart4_pins_pa0_pa1, configure_usart1_rx_pin_pa10, configure_usart1_tx_pin_pa9,
|
||||
configure_usart2_pins_pa2_pa3,
|
||||
};
|
||||
pub use i2c1::{I2c1, I2c1Config};
|
||||
pub use spi1::{Spi1, Spi1Error};
|
||||
pub use spi3::{Spi3, Spi3Error};
|
||||
pub use tim2::{Tim2Periodic, Tim2PeriodicConfig};
|
||||
pub use time::DwtMicrosecondClock;
|
||||
pub use usart::{ReceiveInterrupt, Uart4, Usart1Tx, Usart2Tx};
|
||||
pub use usart1_tx_dma2_stream7::Usart1TxDma;
|
||||
pub use usart2_tx_dma1_stream6::Usart2TxDma;
|
||||
@@ -0,0 +1,350 @@
|
||||
//! STM32F722 SPI1 full-duplex DMA controller.
|
||||
//!
|
||||
//! SPI1 uses DMA2 Stream0/Channel3 for receive and DMA2 Stream3/Channel3 for
|
||||
//! transmit. The receive terminal event is the transaction boundary because it
|
||||
//! is generated after the last serial frame; transmit completion can occur
|
||||
//! while the final serial clock is still in progress.
|
||||
|
||||
use kaptan_log::KaptanError;
|
||||
use kaptan_spi::SpiCompletion;
|
||||
use stm32f7::stm32f722;
|
||||
|
||||
const MAX_TRANSFER_LENGTH: usize = u16::MAX as usize;
|
||||
|
||||
/// Failure while submitting one SPI1 DMA transaction.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, KaptanError)]
|
||||
pub enum Spi1Error {
|
||||
/// The transaction is malformed or exceeds the DMA counter range.
|
||||
InvalidLength,
|
||||
/// A previous DMA transaction still owns SPI1.
|
||||
Busy,
|
||||
}
|
||||
|
||||
/// Owns SPI1 and its fixed full-duplex DMA2 request route.
|
||||
pub struct Spi1 {
|
||||
peripheral: stm32f722::SPI1,
|
||||
controller: stm32f722::DMA2,
|
||||
active: bool,
|
||||
}
|
||||
|
||||
impl Spi1 {
|
||||
/// Configures SPI1 mode 0 master operation at APB2/128 and reserves DMA2.
|
||||
///
|
||||
/// This keeps the MPU6000 route below 1 MHz with the board's 108 MHz
|
||||
/// APB2 clock while retaining substantial margin for board-level wiring.
|
||||
pub fn new(
|
||||
rcc: &stm32f722::RCC,
|
||||
peripheral: stm32f722::SPI1,
|
||||
controller: stm32f722::DMA2,
|
||||
) -> Self {
|
||||
rcc.apb2rstr().modify(|_, write| write.spi1rst().set_bit());
|
||||
rcc.apb2rstr()
|
||||
.modify(|_, write| write.spi1rst().clear_bit());
|
||||
rcc.apb2enr().modify(|_, write| write.spi1en().set_bit());
|
||||
rcc.ahb1enr().modify(|_, write| write.dma2en().set_bit());
|
||||
|
||||
peripheral.cr1().write(|write| {
|
||||
write
|
||||
.mstr()
|
||||
.master()
|
||||
.br()
|
||||
.div128()
|
||||
.ssm()
|
||||
.set_bit()
|
||||
.ssi()
|
||||
.set_bit()
|
||||
});
|
||||
peripheral
|
||||
.cr2()
|
||||
.write(|write| write.ds().eight_bit().frxth().set_bit().errie().set_bit());
|
||||
|
||||
Self {
|
||||
peripheral,
|
||||
controller,
|
||||
active: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Starts a full-duplex DMA transaction without polling a peripheral flag.
|
||||
pub fn start_transfer(&mut self, transmit: &[u8], receive: &mut [u8]) -> Result<(), Spi1Error> {
|
||||
if transmit.is_empty()
|
||||
|| transmit.len() != receive.len()
|
||||
|| transmit.len() > MAX_TRANSFER_LENGTH
|
||||
{
|
||||
return Err(Spi1Error::InvalidLength);
|
||||
}
|
||||
if self.active
|
||||
|| self.rx_stream().cr().read().en().is_enabled()
|
||||
|| self.tx_stream().cr().read().en().is_enabled()
|
||||
{
|
||||
return Err(Spi1Error::Busy);
|
||||
}
|
||||
|
||||
// Arm the complete DMA receive path before SPI starts generating
|
||||
// clocks. This avoids an RX FIFO overrun between transactions.
|
||||
self.peripheral
|
||||
.cr1()
|
||||
.modify(|_, write| write.spe().clear_bit());
|
||||
self.clear_spi_overrun();
|
||||
self.clear_rx_flags();
|
||||
self.clear_tx_flags();
|
||||
self.configure_rx(receive);
|
||||
self.configure_tx(transmit);
|
||||
self.peripheral.cr2().modify(|_, write| {
|
||||
let write = write.rxdmaen().set_bit().txdmaen().set_bit();
|
||||
if transmit.len() & 1 == 0 {
|
||||
write.ldma_rx().even().ldma_tx().even()
|
||||
} else {
|
||||
// With 8-bit frames, F7 SPI needs this for the final
|
||||
// byte of an odd-length DMA transaction. MPU samples are
|
||||
// one command byte plus fourteen data bytes.
|
||||
write.ldma_rx().odd().ldma_tx().odd()
|
||||
}
|
||||
});
|
||||
self.rx_stream()
|
||||
.cr()
|
||||
.modify(|_, write| write.en().enabled());
|
||||
self.tx_stream()
|
||||
.cr()
|
||||
.modify(|_, write| write.en().enabled());
|
||||
self.peripheral
|
||||
.cr1()
|
||||
.modify(|_, write| write.spe().set_bit());
|
||||
self.active = true;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Releases controller-side ownership after a DMA terminal event.
|
||||
///
|
||||
/// Both streams are explicitly disabled for every outcome, including an
|
||||
/// SPI peripheral fault. Disabling the SPI DMA requests first prevents a
|
||||
/// new request while the stream hardware drains its enable bit. This
|
||||
/// method deliberately does not wait for that bit: the next submission
|
||||
/// observes it and returns `Busy` until the F7 DMA controller completes
|
||||
/// the documented disable sequence.
|
||||
pub fn complete_transfer(&mut self, _: SpiCompletion) {
|
||||
self.peripheral
|
||||
.cr2()
|
||||
.modify(|_, write| write.rxdmaen().clear_bit().txdmaen().clear_bit());
|
||||
self.peripheral
|
||||
.cr1()
|
||||
.modify(|_, write| write.spe().clear_bit());
|
||||
self.rx_stream()
|
||||
.cr()
|
||||
.modify(|_, write| write.en().clear_bit());
|
||||
self.tx_stream()
|
||||
.cr()
|
||||
.modify(|_, write| write.en().clear_bit());
|
||||
self.clear_rx_flags();
|
||||
self.clear_tx_flags();
|
||||
self.active = false;
|
||||
}
|
||||
|
||||
/// Acknowledges an SPI peripheral overrun interrupt.
|
||||
///
|
||||
/// An overrun is not a DMA-stream error, so it must be observed through
|
||||
/// SPI1's own IRQ. The reference manual clears OVR by reading DR then SR.
|
||||
pub fn acknowledge_error_interrupt() -> bool {
|
||||
// SAFETY: SPI1 is initialized once by `Spi1::new`; this ISR only
|
||||
// clears its peripheral OVR condition.
|
||||
let peripheral = unsafe { &*stm32f722::SPI1::ptr() };
|
||||
if !peripheral.sr().read().ovr().is_overrun() {
|
||||
return false;
|
||||
}
|
||||
let _ = peripheral.dr().read().bits();
|
||||
let _ = peripheral.sr().read().bits();
|
||||
true
|
||||
}
|
||||
|
||||
/// Acknowledges the RX terminal IRQ. Invoke only from DMA2 Stream0.
|
||||
pub fn acknowledge_rx_interrupt() -> SpiCompletion {
|
||||
// SAFETY: DMA2 is initialized once by `Spi1::new`; this ISR touches
|
||||
// only Stream0's status and clear registers, never worker state.
|
||||
let controller = unsafe { &*stm32f722::DMA2::ptr() };
|
||||
let status = controller.lisr().read();
|
||||
let fault =
|
||||
status.teif0().is_error() || status.dmeif0().is_error() || status.feif0().is_error();
|
||||
let complete = status.tcif0().is_complete();
|
||||
controller.lifcr().write(|write| {
|
||||
write
|
||||
.cfeif0()
|
||||
.clear()
|
||||
.cdmeif0()
|
||||
.clear()
|
||||
.cteif0()
|
||||
.clear()
|
||||
.chtif0()
|
||||
.clear()
|
||||
.ctcif0()
|
||||
.clear()
|
||||
});
|
||||
if fault {
|
||||
SpiCompletion::TransferFault
|
||||
} else if complete {
|
||||
SpiCompletion::Complete
|
||||
} else {
|
||||
SpiCompletion::NotPending
|
||||
}
|
||||
}
|
||||
|
||||
/// Acknowledges TX errors; TX completion alone is not a transaction event.
|
||||
pub fn acknowledge_tx_interrupt() -> SpiCompletion {
|
||||
// SAFETY: DMA2 Stream3 is initialized once by `Spi1::new`; this ISR
|
||||
// touches only Stream3's status and clear registers.
|
||||
let controller = unsafe { &*stm32f722::DMA2::ptr() };
|
||||
let status = controller.lisr().read();
|
||||
let fault =
|
||||
status.teif3().is_error() || status.dmeif3().is_error() || status.feif3().is_error();
|
||||
controller.lifcr().write(|write| {
|
||||
write
|
||||
.cfeif3()
|
||||
.clear()
|
||||
.cdmeif3()
|
||||
.clear()
|
||||
.cteif3()
|
||||
.clear()
|
||||
.chtif3()
|
||||
.clear()
|
||||
.ctcif3()
|
||||
.clear()
|
||||
});
|
||||
if fault {
|
||||
SpiCompletion::TransferFault
|
||||
} else {
|
||||
SpiCompletion::NotPending
|
||||
}
|
||||
}
|
||||
|
||||
fn configure_rx(&self, receive: &mut [u8]) {
|
||||
self.rx_stream().cr().write(|write| {
|
||||
// SAFETY: Channel 3 is SPI1_RX on DMA2 Stream0; checked slice
|
||||
// length fits NDTR, and all values map directly to documented bits.
|
||||
unsafe {
|
||||
write
|
||||
.chsel()
|
||||
.bits(3)
|
||||
.dir()
|
||||
.peripheral_to_memory()
|
||||
.pl()
|
||||
.very_high()
|
||||
.minc()
|
||||
.incremented()
|
||||
.pinc()
|
||||
.fixed()
|
||||
.msize()
|
||||
.bits8()
|
||||
.psize()
|
||||
.bits8()
|
||||
.circ()
|
||||
.disabled()
|
||||
.tcie()
|
||||
.enabled()
|
||||
.teie()
|
||||
.enabled()
|
||||
.dmeie()
|
||||
.enabled()
|
||||
}
|
||||
});
|
||||
self.rx_stream()
|
||||
.ndtr()
|
||||
// SAFETY: `start_transfer` bounds the length to the 16-bit NDTR field.
|
||||
.write(|write| unsafe { write.bits(receive.len() as u32) });
|
||||
self.rx_stream()
|
||||
.par()
|
||||
// SAFETY: `DR` is the memory-mapped SPI1 DMA peripheral endpoint.
|
||||
.write(|write| unsafe { write.bits(self.peripheral.dr().as_ptr() as u32) });
|
||||
self.rx_stream()
|
||||
.m0ar()
|
||||
// SAFETY: the caller retains `receive` until terminal completion.
|
||||
.write(|write| unsafe { write.bits(receive.as_mut_ptr() as u32) });
|
||||
}
|
||||
|
||||
fn configure_tx(&self, transmit: &[u8]) {
|
||||
self.tx_stream().cr().write(|write| {
|
||||
// SAFETY: Channel 3 is SPI1_TX on DMA2 Stream3; checked slice
|
||||
// length fits NDTR, and all values map directly to documented bits.
|
||||
unsafe {
|
||||
write
|
||||
.chsel()
|
||||
.bits(3)
|
||||
.dir()
|
||||
.memory_to_peripheral()
|
||||
.pl()
|
||||
.very_high()
|
||||
.minc()
|
||||
.incremented()
|
||||
.pinc()
|
||||
.fixed()
|
||||
.msize()
|
||||
.bits8()
|
||||
.psize()
|
||||
.bits8()
|
||||
.circ()
|
||||
.disabled()
|
||||
.teie()
|
||||
.enabled()
|
||||
.dmeie()
|
||||
.enabled()
|
||||
}
|
||||
});
|
||||
self.tx_stream()
|
||||
.ndtr()
|
||||
// SAFETY: `start_transfer` bounds the length to the 16-bit NDTR field.
|
||||
.write(|write| unsafe { write.bits(transmit.len() as u32) });
|
||||
self.tx_stream()
|
||||
.par()
|
||||
// SAFETY: `DR` is the memory-mapped SPI1 DMA peripheral endpoint.
|
||||
.write(|write| unsafe { write.bits(self.peripheral.dr().as_ptr() as u32) });
|
||||
self.tx_stream()
|
||||
.m0ar()
|
||||
// SAFETY: the caller retains `transmit` until terminal completion.
|
||||
.write(|write| unsafe { write.bits(transmit.as_ptr() as u32) });
|
||||
}
|
||||
|
||||
fn rx_stream(&self) -> &stm32f722::dma2::ST {
|
||||
self.controller.st(0)
|
||||
}
|
||||
|
||||
fn tx_stream(&self) -> &stm32f722::dma2::ST {
|
||||
self.controller.st(3)
|
||||
}
|
||||
|
||||
fn clear_rx_flags(&self) {
|
||||
self.controller.lifcr().write(|write| {
|
||||
write
|
||||
.cfeif0()
|
||||
.clear()
|
||||
.cdmeif0()
|
||||
.clear()
|
||||
.cteif0()
|
||||
.clear()
|
||||
.chtif0()
|
||||
.clear()
|
||||
.ctcif0()
|
||||
.clear()
|
||||
});
|
||||
}
|
||||
|
||||
fn clear_tx_flags(&self) {
|
||||
self.controller.lifcr().write(|write| {
|
||||
write
|
||||
.cfeif3()
|
||||
.clear()
|
||||
.cdmeif3()
|
||||
.clear()
|
||||
.cteif3()
|
||||
.clear()
|
||||
.chtif3()
|
||||
.clear()
|
||||
.ctcif3()
|
||||
.clear()
|
||||
});
|
||||
}
|
||||
|
||||
fn clear_spi_overrun(&self) {
|
||||
if self.peripheral.sr().read().ovr().is_overrun() {
|
||||
let _ = self.peripheral.dr().read().bits();
|
||||
let _ = self.peripheral.sr().read().bits();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,350 @@
|
||||
//! STM32F722 SPI3 full-duplex DMA controller.
|
||||
//!
|
||||
//! SPI3 uses DMA1 Stream0/Channel0 for receive and DMA1 Stream5/Channel0 for
|
||||
//! transmit. The receive terminal event is the transaction boundary because it
|
||||
//! is generated after the last serial frame; transmit completion can occur
|
||||
//! while the final serial clock is still in progress.
|
||||
|
||||
use kaptan_log::KaptanError;
|
||||
use kaptan_spi::SpiCompletion;
|
||||
use stm32f7::stm32f722;
|
||||
|
||||
const MAX_TRANSFER_LENGTH: usize = u16::MAX as usize;
|
||||
|
||||
/// Failure while submitting one SPI3 DMA transaction.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, KaptanError)]
|
||||
pub enum Spi3Error {
|
||||
/// The transaction is malformed or exceeds the DMA counter range.
|
||||
InvalidLength,
|
||||
/// A previous DMA transaction still owns SPI3.
|
||||
Busy,
|
||||
}
|
||||
|
||||
/// Owns SPI3 and its fixed full-duplex DMA1 request route.
|
||||
pub struct Spi3 {
|
||||
peripheral: stm32f722::SPI3,
|
||||
controller: stm32f722::DMA1,
|
||||
active: bool,
|
||||
}
|
||||
|
||||
impl Spi3 {
|
||||
/// Configures SPI3 mode 0 master operation at APB1/128 and reserves DMA1.
|
||||
///
|
||||
/// This keeps the ICM-42688-P route below 1 MHz with the board's 54 MHz
|
||||
/// APB1 clock while retaining substantial margin for board-level wiring.
|
||||
pub fn new(
|
||||
rcc: &stm32f722::RCC,
|
||||
peripheral: stm32f722::SPI3,
|
||||
controller: stm32f722::DMA1,
|
||||
) -> Self {
|
||||
rcc.apb1rstr().modify(|_, write| write.spi3rst().set_bit());
|
||||
rcc.apb1rstr()
|
||||
.modify(|_, write| write.spi3rst().clear_bit());
|
||||
rcc.apb1enr().modify(|_, write| write.spi3en().set_bit());
|
||||
rcc.ahb1enr().modify(|_, write| write.dma1en().set_bit());
|
||||
|
||||
peripheral.cr1().write(|write| {
|
||||
write
|
||||
.mstr()
|
||||
.master()
|
||||
.br()
|
||||
.div128()
|
||||
.ssm()
|
||||
.set_bit()
|
||||
.ssi()
|
||||
.set_bit()
|
||||
});
|
||||
peripheral
|
||||
.cr2()
|
||||
.write(|write| write.ds().eight_bit().frxth().set_bit().errie().set_bit());
|
||||
|
||||
Self {
|
||||
peripheral,
|
||||
controller,
|
||||
active: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Starts a full-duplex DMA transaction without polling a peripheral flag.
|
||||
pub fn start_transfer(&mut self, transmit: &[u8], receive: &mut [u8]) -> Result<(), Spi3Error> {
|
||||
if transmit.is_empty()
|
||||
|| transmit.len() != receive.len()
|
||||
|| transmit.len() > MAX_TRANSFER_LENGTH
|
||||
{
|
||||
return Err(Spi3Error::InvalidLength);
|
||||
}
|
||||
if self.active
|
||||
|| self.rx_stream().cr().read().en().is_enabled()
|
||||
|| self.tx_stream().cr().read().en().is_enabled()
|
||||
{
|
||||
return Err(Spi3Error::Busy);
|
||||
}
|
||||
|
||||
// Arm the complete DMA receive path before SPI starts generating
|
||||
// clocks. This avoids an RX FIFO overrun between transactions.
|
||||
self.peripheral
|
||||
.cr1()
|
||||
.modify(|_, write| write.spe().clear_bit());
|
||||
self.clear_spi_overrun();
|
||||
self.clear_rx_flags();
|
||||
self.clear_tx_flags();
|
||||
self.configure_rx(receive);
|
||||
self.configure_tx(transmit);
|
||||
self.peripheral.cr2().modify(|_, write| {
|
||||
let write = write.rxdmaen().set_bit().txdmaen().set_bit();
|
||||
if transmit.len() & 1 == 0 {
|
||||
write.ldma_rx().even().ldma_tx().even()
|
||||
} else {
|
||||
// With 8-bit frames, F7 SPI needs this for the final
|
||||
// byte of an odd-length DMA transaction. IMU samples are
|
||||
// one command byte plus fourteen data bytes.
|
||||
write.ldma_rx().odd().ldma_tx().odd()
|
||||
}
|
||||
});
|
||||
self.rx_stream()
|
||||
.cr()
|
||||
.modify(|_, write| write.en().enabled());
|
||||
self.tx_stream()
|
||||
.cr()
|
||||
.modify(|_, write| write.en().enabled());
|
||||
self.peripheral
|
||||
.cr1()
|
||||
.modify(|_, write| write.spe().set_bit());
|
||||
self.active = true;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Releases controller-side ownership after a DMA terminal event.
|
||||
///
|
||||
/// Both streams are explicitly disabled for every outcome, including an
|
||||
/// SPI peripheral fault. Disabling the SPI DMA requests first prevents a
|
||||
/// new request while the stream hardware drains its enable bit. This
|
||||
/// method deliberately does not wait for that bit: the next submission
|
||||
/// observes it and returns `Busy` until the F7 DMA controller completes
|
||||
/// the documented disable sequence.
|
||||
pub fn complete_transfer(&mut self, _: SpiCompletion) {
|
||||
self.peripheral
|
||||
.cr2()
|
||||
.modify(|_, write| write.rxdmaen().clear_bit().txdmaen().clear_bit());
|
||||
self.peripheral
|
||||
.cr1()
|
||||
.modify(|_, write| write.spe().clear_bit());
|
||||
self.rx_stream()
|
||||
.cr()
|
||||
.modify(|_, write| write.en().clear_bit());
|
||||
self.tx_stream()
|
||||
.cr()
|
||||
.modify(|_, write| write.en().clear_bit());
|
||||
self.clear_rx_flags();
|
||||
self.clear_tx_flags();
|
||||
self.active = false;
|
||||
}
|
||||
|
||||
/// Acknowledges an SPI peripheral overrun interrupt.
|
||||
///
|
||||
/// An overrun is not a DMA-stream error, so it must be observed through
|
||||
/// SPI3's own IRQ. The reference manual clears OVR by reading DR then SR.
|
||||
pub fn acknowledge_error_interrupt() -> bool {
|
||||
// SAFETY: SPI3 is initialized once by `Spi3::new`; this ISR only
|
||||
// clears its peripheral OVR condition.
|
||||
let peripheral = unsafe { &*stm32f722::SPI3::ptr() };
|
||||
if !peripheral.sr().read().ovr().is_overrun() {
|
||||
return false;
|
||||
}
|
||||
let _ = peripheral.dr().read().bits();
|
||||
let _ = peripheral.sr().read().bits();
|
||||
true
|
||||
}
|
||||
|
||||
/// Acknowledges the RX terminal IRQ. Invoke only from DMA1 Stream0.
|
||||
pub fn acknowledge_rx_interrupt() -> SpiCompletion {
|
||||
// SAFETY: DMA1 is initialized once by `Spi3::new`; this ISR touches
|
||||
// only Stream0's status and clear registers, never worker state.
|
||||
let controller = unsafe { &*stm32f722::DMA1::ptr() };
|
||||
let status = controller.lisr().read();
|
||||
let fault =
|
||||
status.teif0().is_error() || status.dmeif0().is_error() || status.feif0().is_error();
|
||||
let complete = status.tcif0().is_complete();
|
||||
controller.lifcr().write(|write| {
|
||||
write
|
||||
.cfeif0()
|
||||
.clear()
|
||||
.cdmeif0()
|
||||
.clear()
|
||||
.cteif0()
|
||||
.clear()
|
||||
.chtif0()
|
||||
.clear()
|
||||
.ctcif0()
|
||||
.clear()
|
||||
});
|
||||
if fault {
|
||||
SpiCompletion::TransferFault
|
||||
} else if complete {
|
||||
SpiCompletion::Complete
|
||||
} else {
|
||||
SpiCompletion::NotPending
|
||||
}
|
||||
}
|
||||
|
||||
/// Acknowledges TX errors; TX completion alone is not a transaction event.
|
||||
pub fn acknowledge_tx_interrupt() -> SpiCompletion {
|
||||
// SAFETY: DMA1 Stream5 is initialized once by `Spi3::new`; this ISR
|
||||
// touches only Stream5's status and clear registers.
|
||||
let controller = unsafe { &*stm32f722::DMA1::ptr() };
|
||||
let status = controller.hisr().read();
|
||||
let fault =
|
||||
status.teif5().is_error() || status.dmeif5().is_error() || status.feif5().is_error();
|
||||
controller.hifcr().write(|write| {
|
||||
write
|
||||
.cfeif5()
|
||||
.clear()
|
||||
.cdmeif5()
|
||||
.clear()
|
||||
.cteif5()
|
||||
.clear()
|
||||
.chtif5()
|
||||
.clear()
|
||||
.ctcif5()
|
||||
.clear()
|
||||
});
|
||||
if fault {
|
||||
SpiCompletion::TransferFault
|
||||
} else {
|
||||
SpiCompletion::NotPending
|
||||
}
|
||||
}
|
||||
|
||||
fn configure_rx(&self, receive: &mut [u8]) {
|
||||
self.rx_stream().cr().write(|write| {
|
||||
// SAFETY: Channel 0 is SPI3_RX on DMA1 Stream0; checked slice
|
||||
// length fits NDTR, and all values map directly to documented bits.
|
||||
unsafe {
|
||||
write
|
||||
.chsel()
|
||||
.bits(0)
|
||||
.dir()
|
||||
.peripheral_to_memory()
|
||||
.pl()
|
||||
.very_high()
|
||||
.minc()
|
||||
.incremented()
|
||||
.pinc()
|
||||
.fixed()
|
||||
.msize()
|
||||
.bits8()
|
||||
.psize()
|
||||
.bits8()
|
||||
.circ()
|
||||
.disabled()
|
||||
.tcie()
|
||||
.enabled()
|
||||
.teie()
|
||||
.enabled()
|
||||
.dmeie()
|
||||
.enabled()
|
||||
}
|
||||
});
|
||||
self.rx_stream()
|
||||
.ndtr()
|
||||
// SAFETY: `start_transfer` bounds the length to the 16-bit NDTR field.
|
||||
.write(|write| unsafe { write.bits(receive.len() as u32) });
|
||||
self.rx_stream()
|
||||
.par()
|
||||
// SAFETY: `DR` is the memory-mapped SPI3 DMA peripheral endpoint.
|
||||
.write(|write| unsafe { write.bits(self.peripheral.dr().as_ptr() as u32) });
|
||||
self.rx_stream()
|
||||
.m0ar()
|
||||
// SAFETY: the caller retains `receive` until terminal completion.
|
||||
.write(|write| unsafe { write.bits(receive.as_mut_ptr() as u32) });
|
||||
}
|
||||
|
||||
fn configure_tx(&self, transmit: &[u8]) {
|
||||
self.tx_stream().cr().write(|write| {
|
||||
// SAFETY: Channel 0 is SPI3_TX on DMA1 Stream5; checked slice
|
||||
// length fits NDTR, and all values map directly to documented bits.
|
||||
unsafe {
|
||||
write
|
||||
.chsel()
|
||||
.bits(0)
|
||||
.dir()
|
||||
.memory_to_peripheral()
|
||||
.pl()
|
||||
.very_high()
|
||||
.minc()
|
||||
.incremented()
|
||||
.pinc()
|
||||
.fixed()
|
||||
.msize()
|
||||
.bits8()
|
||||
.psize()
|
||||
.bits8()
|
||||
.circ()
|
||||
.disabled()
|
||||
.teie()
|
||||
.enabled()
|
||||
.dmeie()
|
||||
.enabled()
|
||||
}
|
||||
});
|
||||
self.tx_stream()
|
||||
.ndtr()
|
||||
// SAFETY: `start_transfer` bounds the length to the 16-bit NDTR field.
|
||||
.write(|write| unsafe { write.bits(transmit.len() as u32) });
|
||||
self.tx_stream()
|
||||
.par()
|
||||
// SAFETY: `DR` is the memory-mapped SPI3 DMA peripheral endpoint.
|
||||
.write(|write| unsafe { write.bits(self.peripheral.dr().as_ptr() as u32) });
|
||||
self.tx_stream()
|
||||
.m0ar()
|
||||
// SAFETY: the caller retains `transmit` until terminal completion.
|
||||
.write(|write| unsafe { write.bits(transmit.as_ptr() as u32) });
|
||||
}
|
||||
|
||||
fn rx_stream(&self) -> &stm32f722::dma1::ST {
|
||||
self.controller.st(0)
|
||||
}
|
||||
|
||||
fn tx_stream(&self) -> &stm32f722::dma1::ST {
|
||||
self.controller.st(5)
|
||||
}
|
||||
|
||||
fn clear_rx_flags(&self) {
|
||||
self.controller.lifcr().write(|write| {
|
||||
write
|
||||
.cfeif0()
|
||||
.clear()
|
||||
.cdmeif0()
|
||||
.clear()
|
||||
.cteif0()
|
||||
.clear()
|
||||
.chtif0()
|
||||
.clear()
|
||||
.ctcif0()
|
||||
.clear()
|
||||
});
|
||||
}
|
||||
|
||||
fn clear_tx_flags(&self) {
|
||||
self.controller.hifcr().write(|write| {
|
||||
write
|
||||
.cfeif5()
|
||||
.clear()
|
||||
.cdmeif5()
|
||||
.clear()
|
||||
.cteif5()
|
||||
.clear()
|
||||
.chtif5()
|
||||
.clear()
|
||||
.ctcif5()
|
||||
.clear()
|
||||
});
|
||||
}
|
||||
|
||||
fn clear_spi_overrun(&self) {
|
||||
if self.peripheral.sr().read().ovr().is_overrun() {
|
||||
let _ = self.peripheral.dr().read().bits();
|
||||
let _ = self.peripheral.sr().read().bits();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
//! STM32F722 TIM2 periodic interrupt configuration.
|
||||
|
||||
use stm32f7::stm32f722;
|
||||
|
||||
/// Precomputed TIM2 periodic-update configuration.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct Tim2PeriodicConfig {
|
||||
/// Timer prescaler register value.
|
||||
pub prescaler: u16,
|
||||
/// Timer auto-reload register value.
|
||||
pub auto_reload: u32,
|
||||
}
|
||||
|
||||
/// TIM2 configured to issue periodic update interrupts.
|
||||
pub struct Tim2Periodic {
|
||||
_peripheral: stm32f722::TIM2,
|
||||
}
|
||||
|
||||
impl Tim2Periodic {
|
||||
/// Enables TIM2 with the supplied platform-derived period configuration.
|
||||
pub fn new(
|
||||
rcc: &stm32f722::RCC,
|
||||
peripheral: stm32f722::TIM2,
|
||||
config: Tim2PeriodicConfig,
|
||||
) -> Self {
|
||||
rcc.apb1rstr().modify(|_, write| write.tim2rst().set_bit());
|
||||
rcc.apb1rstr()
|
||||
.modify(|_, write| write.tim2rst().clear_bit());
|
||||
rcc.apb1enr().modify(|_, write| write.tim2en().set_bit());
|
||||
peripheral.cr1().modify(|_, write| write.cen().clear_bit());
|
||||
peripheral.psc().write(|write| {
|
||||
// SAFETY: `prescaler` is exactly the 16-bit PSC register width.
|
||||
unsafe { write.psc().bits(config.prescaler) }
|
||||
});
|
||||
peripheral.arr().write(|write| {
|
||||
// SAFETY: TIM2 ARR is a 32-bit register and accepts `auto_reload`.
|
||||
unsafe { write.arr().bits(config.auto_reload) }
|
||||
});
|
||||
peripheral.egr().write(|write| write.ug().set_bit());
|
||||
// TIM2 status flags are write-zero-to-clear. Use the PAC's explicit
|
||||
// W0C operation rather than a read-modify-write after `EGR.UG`.
|
||||
peripheral.sr().write(|write| write.uif().clear());
|
||||
peripheral.dier().modify(|_, write| write.uie().set_bit());
|
||||
peripheral.cr1().modify(|_, write| write.cen().set_bit());
|
||||
|
||||
Self {
|
||||
_peripheral: peripheral,
|
||||
}
|
||||
}
|
||||
|
||||
/// Acknowledges a TIM2 update interrupt.
|
||||
pub fn acknowledge_update(&mut self) {
|
||||
// `UIF` is write-zero-to-clear (W0C).
|
||||
self._peripheral.sr().write(|write| write.uif().clear());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
//! DWT-backed monotonic microsecond clock for STM32F722.
|
||||
|
||||
use cortex_m::peripheral::{DCB, DWT};
|
||||
use kaptan_log::KaptanError;
|
||||
use kaptan_time::{MonotonicClock, Timestamp};
|
||||
|
||||
/// A DWT cycle counter extended into monotonic microseconds.
|
||||
///
|
||||
/// `now` must be called at least once during every 32-bit cycle-counter wrap
|
||||
/// interval. At the Kakute reset clock this is approximately 20 seconds; the
|
||||
/// platform periodic service calls it much more frequently.
|
||||
pub struct DwtMicrosecondClock {
|
||||
cycles_per_microsecond: u32,
|
||||
previous_cycles: u32,
|
||||
elapsed_cycles: u64,
|
||||
}
|
||||
|
||||
/// Invalid fixed clock configuration for the DWT timestamp source.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, KaptanError)]
|
||||
pub enum DwtClockStartError {
|
||||
/// A zero-megahertz core clock cannot produce microsecond timestamps.
|
||||
ZeroFrequency,
|
||||
}
|
||||
|
||||
impl DwtMicrosecondClock {
|
||||
/// Enables the DWT cycle counter for a fixed integer-MHz core clock.
|
||||
///
|
||||
/// `MEGAHERTZ` must match the platform's core clock. This function is
|
||||
/// called once during platform initialization before interrupts are enabled.
|
||||
pub fn start<const MEGAHERTZ: u32>(
|
||||
dcb: &mut DCB,
|
||||
dwt: &mut DWT,
|
||||
) -> Result<Self, DwtClockStartError> {
|
||||
if MEGAHERTZ == 0 {
|
||||
return Err(DwtClockStartError::ZeroFrequency);
|
||||
}
|
||||
|
||||
dcb.enable_trace();
|
||||
DWT::unlock();
|
||||
dwt.set_cycle_count(0);
|
||||
dwt.enable_cycle_counter();
|
||||
|
||||
Ok(Self {
|
||||
cycles_per_microsecond: MEGAHERTZ,
|
||||
previous_cycles: 0,
|
||||
elapsed_cycles: 0,
|
||||
})
|
||||
}
|
||||
|
||||
/// Captures the current monotonic microsecond timestamp.
|
||||
pub fn now(&mut self) -> Timestamp {
|
||||
self.advance_to(DWT::cycle_count())
|
||||
}
|
||||
|
||||
/// Creates another independent reader for the same already-running DWT
|
||||
/// counter. Each reader must be serviced often enough to observe wraps.
|
||||
pub const fn fork(&self) -> Self {
|
||||
Self {
|
||||
cycles_per_microsecond: self.cycles_per_microsecond,
|
||||
previous_cycles: self.previous_cycles,
|
||||
elapsed_cycles: self.elapsed_cycles,
|
||||
}
|
||||
}
|
||||
|
||||
fn advance_to(&mut self, current_cycles: u32) -> Timestamp {
|
||||
let elapsed = current_cycles.wrapping_sub(self.previous_cycles);
|
||||
self.previous_cycles = current_cycles;
|
||||
self.elapsed_cycles = self.elapsed_cycles.saturating_add(u64::from(elapsed));
|
||||
|
||||
Timestamp::from_microseconds(self.elapsed_cycles / u64::from(self.cycles_per_microsecond))
|
||||
}
|
||||
}
|
||||
|
||||
impl MonotonicClock for DwtMicrosecondClock {
|
||||
fn now(&mut self) -> Timestamp {
|
||||
Self::now(self)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::DwtMicrosecondClock;
|
||||
|
||||
fn clock(previous_cycles: u32) -> DwtMicrosecondClock {
|
||||
DwtMicrosecondClock {
|
||||
cycles_per_microsecond: 16,
|
||||
previous_cycles,
|
||||
elapsed_cycles: 0,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn converts_cycles_to_microseconds() {
|
||||
let mut clock = clock(0);
|
||||
|
||||
assert_eq!(clock.advance_to(160).as_microseconds(), 10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extends_a_single_cycle_counter_wrap() {
|
||||
let mut clock = clock(u32::MAX - 15);
|
||||
|
||||
assert_eq!(clock.advance_to(16).as_microseconds(), 2);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,276 @@
|
||||
//! STM32F722 USART routes used by supported boards.
|
||||
|
||||
use kaptan_log::KaptanError;
|
||||
use stm32f7::stm32f722;
|
||||
|
||||
/// UART baud-rate configuration error.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, KaptanError)]
|
||||
pub enum BaudRateError {
|
||||
/// Zero is not a valid baud rate.
|
||||
Zero,
|
||||
/// The requested divisor cannot fit USART BRR.
|
||||
OutOfRange,
|
||||
}
|
||||
|
||||
/// Terminal result from acknowledging a USART receive interrupt.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum ReceiveInterrupt {
|
||||
/// One received byte was removed from the UART data register.
|
||||
Byte(u8),
|
||||
/// A serial transport fault invalidated the current receive stream.
|
||||
Fault,
|
||||
/// This USART interrupt did not have a receive event pending.
|
||||
NotPending,
|
||||
}
|
||||
|
||||
/// Owns configured full-duplex USART1 hardware.
|
||||
///
|
||||
/// The family platform owns schematic-selected pin setup. Configure pins
|
||||
/// before constructing this type.
|
||||
pub struct Usart1Tx {
|
||||
peripheral: stm32f722::USART1,
|
||||
}
|
||||
|
||||
/// Owns configured full-duplex USART2 hardware.
|
||||
///
|
||||
/// The family platform owns schematic-selected pin setup. Configure pins
|
||||
/// before constructing this type.
|
||||
pub struct Usart2Tx {
|
||||
peripheral: stm32f722::USART2,
|
||||
}
|
||||
|
||||
impl Usart2Tx {
|
||||
/// Enables USART2 and configures 8N1 transmit and receive operation.
|
||||
pub fn new(
|
||||
rcc: &stm32f722::RCC,
|
||||
peripheral: stm32f722::USART2,
|
||||
peripheral_clock_hz: u32,
|
||||
baud: u32,
|
||||
) -> Result<Self, BaudRateError> {
|
||||
let brr = calculate_brr(peripheral_clock_hz, baud)?;
|
||||
|
||||
// Do not inherit a bootloader's USART2 kernel-clock selection. The
|
||||
// F722 flight clock profile supplies `peripheral_clock_hz` from PCLK1,
|
||||
// so select PCLK1 before programming BRR.
|
||||
rcc.dckcfgr2().modify(|_, write| write.usart2sel().apb1());
|
||||
rcc.apb1rstr()
|
||||
.modify(|_, write| write.usart2rst().set_bit());
|
||||
rcc.apb1rstr()
|
||||
.modify(|_, write| write.usart2rst().clear_bit());
|
||||
rcc.apb1enr().modify(|_, write| write.usart2en().set_bit());
|
||||
peripheral.brr().write(|write| {
|
||||
// SAFETY: `brr` is validated to fit the complete BRR register.
|
||||
unsafe { write.bits(u32::from(brr)) }
|
||||
});
|
||||
peripheral.cr1().write(|write| {
|
||||
write
|
||||
.te()
|
||||
.set_bit()
|
||||
.re()
|
||||
.set_bit()
|
||||
.rxneie()
|
||||
.set_bit()
|
||||
.ue()
|
||||
.set_bit()
|
||||
});
|
||||
peripheral.cr2().write(|write| write.stop().stop1());
|
||||
|
||||
Ok(Self { peripheral })
|
||||
}
|
||||
|
||||
pub(crate) fn enable_dma_transmit(&self) {
|
||||
self.peripheral.cr3().write(|write| write.dmat().set_bit());
|
||||
}
|
||||
|
||||
pub(crate) fn transmit_data_register_address(&self) -> u32 {
|
||||
self.peripheral.tdr().as_ptr() as u32
|
||||
}
|
||||
|
||||
/// Acknowledges one USART2 receive interrupt without accessing TX state.
|
||||
pub fn acknowledge_receive_interrupt() -> ReceiveInterrupt {
|
||||
// SAFETY: the platform binds USART2 exactly once and this function touches
|
||||
// only its receive/error registers, never the TX DMA configuration.
|
||||
let peripheral = unsafe { &*stm32f722::USART2::ptr() };
|
||||
acknowledge_receive_interrupt(peripheral)
|
||||
}
|
||||
}
|
||||
|
||||
impl Usart1Tx {
|
||||
/// Enables USART1 and configures 8N1 transmit and receive operation.
|
||||
pub fn new(
|
||||
rcc: &stm32f722::RCC,
|
||||
peripheral: stm32f722::USART1,
|
||||
peripheral_clock_hz: u32,
|
||||
baud: u32,
|
||||
) -> Result<Self, BaudRateError> {
|
||||
let brr = calculate_brr(peripheral_clock_hz, baud)?;
|
||||
|
||||
// Do not inherit a bootloader or earlier image's USART1 kernel-clock
|
||||
// selection. The F722 platform passes PCLK2 when calculating BRR, so
|
||||
// select it explicitly before programming the divisor.
|
||||
rcc.dckcfgr2().modify(|_, write| write.usart1sel().apb2());
|
||||
rcc.apb2rstr()
|
||||
.modify(|_, write| write.usart1rst().set_bit());
|
||||
rcc.apb2rstr()
|
||||
.modify(|_, write| write.usart1rst().clear_bit());
|
||||
rcc.apb2enr().modify(|_, write| write.usart1en().set_bit());
|
||||
peripheral.brr().write(|write| {
|
||||
// SAFETY: `brr` is validated to fit the complete BRR register.
|
||||
unsafe { write.bits(u32::from(brr)) }
|
||||
});
|
||||
peripheral.cr1().write(|write| {
|
||||
write
|
||||
.te()
|
||||
.set_bit()
|
||||
.re()
|
||||
.set_bit()
|
||||
.rxneie()
|
||||
.set_bit()
|
||||
.ue()
|
||||
.set_bit()
|
||||
});
|
||||
peripheral.cr2().write(|write| write.stop().stop1());
|
||||
|
||||
Ok(Self { peripheral })
|
||||
}
|
||||
|
||||
pub(crate) fn enable_dma_transmit(&self) {
|
||||
self.peripheral.cr3().write(|write| write.dmat().set_bit());
|
||||
}
|
||||
|
||||
pub(crate) fn transmit_data_register_address(&self) -> u32 {
|
||||
self.peripheral.tdr().as_ptr() as u32
|
||||
}
|
||||
|
||||
/// Acknowledges one USART1 receive interrupt without accessing TX state.
|
||||
///
|
||||
/// The platform invokes this only from the `USART1` interrupt. USART1 TX DMA
|
||||
/// owns its disjoint data register and DMA stream, so this static route may
|
||||
/// safely acknowledge RX while that transfer is active.
|
||||
pub fn acknowledge_receive_interrupt() -> ReceiveInterrupt {
|
||||
// SAFETY: the platform binds USART1 exactly once and this function touches
|
||||
// only its receive/error registers, never the TX DMA configuration.
|
||||
let peripheral = unsafe { &*stm32f722::USART1::ptr() };
|
||||
acknowledge_receive_interrupt(peripheral)
|
||||
}
|
||||
}
|
||||
|
||||
/// Owns configured UART4 hardware.
|
||||
///
|
||||
/// This route is intended for peripherals that continuously stream data, such
|
||||
/// as the MTF-01. It does not enable DMA state.
|
||||
pub struct Uart4 {
|
||||
_peripheral: stm32f722::UART4,
|
||||
}
|
||||
|
||||
impl Uart4 {
|
||||
/// Enables UART4 and configures 8N1 transmit and receive operation.
|
||||
pub fn new(
|
||||
rcc: &stm32f722::RCC,
|
||||
peripheral: stm32f722::UART4,
|
||||
peripheral_clock_hz: u32,
|
||||
baud: u32,
|
||||
) -> Result<Self, BaudRateError> {
|
||||
let brr = calculate_brr(peripheral_clock_hz, baud)?;
|
||||
|
||||
rcc.apb1rstr().modify(|_, write| write.uart4rst().set_bit());
|
||||
rcc.apb1rstr()
|
||||
.modify(|_, write| write.uart4rst().clear_bit());
|
||||
rcc.apb1enr().modify(|_, write| write.uart4en().set_bit());
|
||||
peripheral.brr().write(|write| {
|
||||
// SAFETY: `brr` is validated to fit the complete BRR register.
|
||||
unsafe { write.bits(u32::from(brr)) }
|
||||
});
|
||||
peripheral.cr1().write(|write| {
|
||||
write
|
||||
.te()
|
||||
.set_bit()
|
||||
.re()
|
||||
.set_bit()
|
||||
.rxneie()
|
||||
.set_bit()
|
||||
.ue()
|
||||
.set_bit()
|
||||
});
|
||||
peripheral.cr2().write(|write| write.stop().stop1());
|
||||
|
||||
Ok(Self {
|
||||
_peripheral: peripheral,
|
||||
})
|
||||
}
|
||||
|
||||
/// Acknowledges one UART4 receive interrupt.
|
||||
pub fn acknowledge_receive_interrupt() -> ReceiveInterrupt {
|
||||
// SAFETY: the platform binds UART4 exactly once and this function
|
||||
// accesses only its receive and error registers.
|
||||
let peripheral = unsafe { &*stm32f722::UART4::ptr() };
|
||||
acknowledge_receive_interrupt(peripheral)
|
||||
}
|
||||
}
|
||||
|
||||
fn acknowledge_receive_interrupt(
|
||||
peripheral: &stm32f722::usart1::RegisterBlock,
|
||||
) -> ReceiveInterrupt {
|
||||
let status = peripheral.isr().read();
|
||||
let fault = status.pe().is_error()
|
||||
|| status.fe().is_error()
|
||||
|| status.nf().is_noise()
|
||||
|| status.ore().is_overrun();
|
||||
|
||||
if status.rxne().is_data_ready() {
|
||||
let byte = peripheral.rdr().read().rdr().bits() as u8;
|
||||
if fault {
|
||||
clear_receive_errors(peripheral);
|
||||
ReceiveInterrupt::Fault
|
||||
} else {
|
||||
ReceiveInterrupt::Byte(byte)
|
||||
}
|
||||
} else if fault {
|
||||
clear_receive_errors(peripheral);
|
||||
ReceiveInterrupt::Fault
|
||||
} else {
|
||||
ReceiveInterrupt::NotPending
|
||||
}
|
||||
}
|
||||
|
||||
fn clear_receive_errors(peripheral: &stm32f722::usart1::RegisterBlock) {
|
||||
peripheral.icr().write(|write| {
|
||||
write
|
||||
.pecf()
|
||||
.clear()
|
||||
.fecf()
|
||||
.clear()
|
||||
.ncf()
|
||||
.clear()
|
||||
.orecf()
|
||||
.clear()
|
||||
});
|
||||
}
|
||||
|
||||
const fn calculate_brr(peripheral_clock_hz: u32, baud: u32) -> Result<u16, BaudRateError> {
|
||||
if baud == 0 {
|
||||
return Err(BaudRateError::Zero);
|
||||
}
|
||||
|
||||
let divisor = (peripheral_clock_hz + (baud / 2)) / baud;
|
||||
if divisor == 0 || divisor > u16::MAX as u32 {
|
||||
Err(BaudRateError::OutOfRange)
|
||||
} else {
|
||||
Ok(divisor as u16)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{BaudRateError, calculate_brr};
|
||||
|
||||
#[test]
|
||||
fn calculates_reset_clock_brr() {
|
||||
assert_eq!(calculate_brr(16_000_000, 115_200), Ok(139));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_zero_baud() {
|
||||
assert_eq!(calculate_brr(16_000_000, 0), Err(BaudRateError::Zero));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
//! Fixed STM32F722 DMA route for USART1 transmit.
|
||||
//!
|
||||
//! USART1 TX uses DMA2 stream 7, channel 4 on STM32F722. The route is a type,
|
||||
//! rather than platform-provided integer parameters, so invalid request mappings
|
||||
//! cannot be selected at construction.
|
||||
|
||||
use kaptan_dma::{DmaReadableBuffer, DmaTxEngine, TxCompletion};
|
||||
use kaptan_log::KaptanError;
|
||||
use stm32f7::stm32f722;
|
||||
|
||||
use super::Usart1Tx;
|
||||
|
||||
// The F7 DMA controller can expose TCIF a few bus cycles before CR.EN reads
|
||||
// low. This bounded IRQ-side settling observation never spins indefinitely;
|
||||
// a later service turn can retry a start if the stream is not yet available.
|
||||
const DISABLE_POLL_LIMIT: u8 = 32;
|
||||
|
||||
/// Owns the fixed STM32F722 USART1 TX DMA route.
|
||||
pub struct Usart1TxDma {
|
||||
usart: Usart1Tx,
|
||||
}
|
||||
|
||||
/// Failure to start one DMA frame transfer.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, KaptanError)]
|
||||
pub enum StartError {
|
||||
/// DMA cannot transfer an empty buffer or one exceeding the NDTR limit.
|
||||
InvalidBufferLength,
|
||||
/// DMA2 stream 7 still owns an earlier transfer.
|
||||
Busy,
|
||||
}
|
||||
|
||||
impl Usart1TxDma {
|
||||
/// Enables DMA2 and connects USART1 TX to its fixed request route.
|
||||
pub fn new(rcc: &stm32f722::RCC, usart: Usart1Tx) -> Self {
|
||||
rcc.ahb1enr().modify(|_, write| write.dma2en().set_bit());
|
||||
usart.enable_dma_transmit();
|
||||
|
||||
Self { usart }
|
||||
}
|
||||
|
||||
/// Starts one memory-to-USART DMA transfer.
|
||||
///
|
||||
/// # Ownership
|
||||
///
|
||||
/// The caller must retain `buffer`'s backing storage until
|
||||
/// [`Self::acknowledge_interrupt`] returns a terminal result.
|
||||
/// - The platform must bind `DMA2_STREAM7`, call `acknowledge_interrupt` from
|
||||
/// that interrupt, and defer lifecycle handling to its single owner.
|
||||
pub fn start(&mut self, buffer: DmaReadableBuffer<'_>) -> Result<(), StartError> {
|
||||
let bytes = buffer.bytes();
|
||||
if bytes.is_empty() || bytes.len() > u16::MAX as usize {
|
||||
return Err(StartError::InvalidBufferLength);
|
||||
}
|
||||
if self.is_active() {
|
||||
return Err(StartError::Busy);
|
||||
}
|
||||
|
||||
self.clear_interrupt_flags();
|
||||
self.configure_stream(bytes);
|
||||
self.stream().cr().modify(|_, write| write.en().enabled());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Acknowledges a DMA2 Stream7 terminal IRQ without accessing engine state.
|
||||
pub fn acknowledge_interrupt() -> TxCompletion {
|
||||
// SAFETY: this handler accesses only the fixed DMA2 Stream7 route.
|
||||
let controller = unsafe { &*stm32f722::DMA2::ptr() };
|
||||
let status = controller.hisr().read();
|
||||
let completion = DmaTerminalStatus {
|
||||
transfer_complete: status.tcif7().is_complete(),
|
||||
transfer_error: status.teif7().is_error(),
|
||||
direct_mode_error: status.dmeif7().is_error(),
|
||||
fifo_error: status.feif7().is_error(),
|
||||
}
|
||||
.classify();
|
||||
if completion == TxCompletion::NotPending {
|
||||
return completion;
|
||||
}
|
||||
|
||||
// A normal-mode STM32 DMA stream clears EN at terminal completion.
|
||||
// Request disable and observe that transition for a small fixed bound;
|
||||
// no unbounded wait is permitted in an IRQ. If the bound expires before
|
||||
// EN becomes visible as low, a later service turn observes `Busy` and
|
||||
// retries without treating this documented hardware latency as a fault.
|
||||
controller
|
||||
.st(7)
|
||||
.cr()
|
||||
.modify(|_, write| write.en().disabled());
|
||||
for _ in 0..DISABLE_POLL_LIMIT {
|
||||
if !controller.st(7).cr().read().en().is_enabled() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
controller.hifcr().write(|write| {
|
||||
write
|
||||
.ctcif7()
|
||||
.clear()
|
||||
.chtif7()
|
||||
.clear()
|
||||
.cteif7()
|
||||
.clear()
|
||||
.cdmeif7()
|
||||
.clear()
|
||||
.cfeif7()
|
||||
.clear()
|
||||
});
|
||||
completion
|
||||
}
|
||||
|
||||
/// Returns whether DMA2 stream 7 owns a transfer.
|
||||
pub fn is_active(&self) -> bool {
|
||||
self.stream().cr().read().en().is_enabled()
|
||||
}
|
||||
|
||||
fn configure_stream(&mut self, buffer: &[u8]) {
|
||||
self.stream().cr().write(|write| {
|
||||
// SAFETY: Channel 4 and these transfer fields are the documented
|
||||
// USART1 TX mapping for DMA2 stream 7 on STM32F722.
|
||||
unsafe {
|
||||
write
|
||||
.chsel()
|
||||
.bits(4)
|
||||
.dir()
|
||||
.memory_to_peripheral()
|
||||
.pl()
|
||||
.very_high()
|
||||
.minc()
|
||||
.incremented()
|
||||
.pinc()
|
||||
.fixed()
|
||||
.msize()
|
||||
.bits8()
|
||||
.psize()
|
||||
.bits8()
|
||||
.circ()
|
||||
.disabled()
|
||||
.tcie()
|
||||
.enabled()
|
||||
.teie()
|
||||
.enabled()
|
||||
.dmeie()
|
||||
.enabled()
|
||||
}
|
||||
});
|
||||
self.stream().ndtr().write(|write| {
|
||||
// SAFETY: `start` checked the slice length against the NDTR limit.
|
||||
unsafe { write.bits(buffer.len() as u32) }
|
||||
});
|
||||
self.stream().par().write(|write| {
|
||||
// SAFETY: This is the memory-mapped USART1 transmit-data register.
|
||||
unsafe { write.bits(self.usart.transmit_data_register_address()) }
|
||||
});
|
||||
self.stream().m0ar().write(|write| {
|
||||
// SAFETY: The caller of `start` upholds the buffer lifetime contract.
|
||||
unsafe { write.bits(buffer.as_ptr() as u32) }
|
||||
});
|
||||
}
|
||||
|
||||
fn stream(&self) -> &stm32f722::dma2::ST {
|
||||
self.controller().st(7)
|
||||
}
|
||||
|
||||
fn clear_interrupt_flags(&self) {
|
||||
self.controller().hifcr().write(|write| {
|
||||
write
|
||||
.ctcif7()
|
||||
.clear()
|
||||
.chtif7()
|
||||
.clear()
|
||||
.cteif7()
|
||||
.clear()
|
||||
.cdmeif7()
|
||||
.clear()
|
||||
.cfeif7()
|
||||
.clear()
|
||||
});
|
||||
}
|
||||
|
||||
fn controller(&self) -> &stm32f722::dma2::RegisterBlock {
|
||||
// SAFETY: USART1 TX permanently owns this disjoint Stream7 route and
|
||||
// accesses no other DMA2 stream.
|
||||
unsafe { &*stm32f722::DMA2::ptr() }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
struct DmaTerminalStatus {
|
||||
transfer_complete: bool,
|
||||
transfer_error: bool,
|
||||
direct_mode_error: bool,
|
||||
fifo_error: bool,
|
||||
}
|
||||
|
||||
impl DmaTerminalStatus {
|
||||
const fn classify(self) -> TxCompletion {
|
||||
if self.transfer_error || self.direct_mode_error || self.fifo_error {
|
||||
TxCompletion::TransferFault
|
||||
} else if self.transfer_complete {
|
||||
TxCompletion::Complete
|
||||
} else {
|
||||
TxCompletion::NotPending
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DmaTxEngine for Usart1TxDma {
|
||||
type StartError = StartError;
|
||||
|
||||
fn start(&mut self, buffer: DmaReadableBuffer<'_>) -> Result<(), Self::StartError> {
|
||||
Self::start(self, buffer)
|
||||
}
|
||||
}
|
||||
|
||||
#[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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
//! Fixed STM32F722 DMA route for USART2 transmit.
|
||||
//!
|
||||
//! USART2 TX uses DMA1 Stream6, channel 4 on STM32F722. The route is a type,
|
||||
//! rather than platform-provided integer parameters, so invalid request mappings
|
||||
//! cannot be selected at construction.
|
||||
|
||||
use kaptan_dma::{DmaReadableBuffer, DmaTxEngine, TxCompletion};
|
||||
use kaptan_log::KaptanError;
|
||||
use stm32f7::stm32f722;
|
||||
|
||||
use super::Usart2Tx;
|
||||
|
||||
/// Owns the fixed USART2 TX DMA1 Stream6 route.
|
||||
pub struct Usart2TxDma {
|
||||
usart: Usart2Tx,
|
||||
}
|
||||
|
||||
/// Failure to start one DMA frame transfer on USART2.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, KaptanError)]
|
||||
pub enum Usart2TxDmaStartError {
|
||||
/// DMA cannot transfer an empty buffer or one exceeding the NDTR limit.
|
||||
InvalidBufferLength,
|
||||
/// DMA1 stream 6 still owns an earlier transfer.
|
||||
Busy,
|
||||
}
|
||||
|
||||
impl Usart2TxDma {
|
||||
/// Enables DMA1 and connects USART2 TX to its fixed request route.
|
||||
pub fn new(rcc: &stm32f722::RCC, usart: Usart2Tx) -> Self {
|
||||
rcc.ahb1enr().modify(|_, write| write.dma1en().set_bit());
|
||||
usart.enable_dma_transmit();
|
||||
Self { usart }
|
||||
}
|
||||
|
||||
/// Starts one memory-to-USART DMA transfer.
|
||||
///
|
||||
/// The caller retains the backing storage until
|
||||
/// [`Self::acknowledge_interrupt`] reports a terminal result.
|
||||
pub fn start(&mut self, buffer: DmaReadableBuffer<'_>) -> Result<(), Usart2TxDmaStartError> {
|
||||
let bytes = buffer.bytes();
|
||||
if bytes.is_empty() || bytes.len() > u16::MAX as usize {
|
||||
return Err(Usart2TxDmaStartError::InvalidBufferLength);
|
||||
}
|
||||
if self.is_active() {
|
||||
return Err(Usart2TxDmaStartError::Busy);
|
||||
}
|
||||
|
||||
self.clear_interrupt_flags();
|
||||
self.configure_stream(bytes);
|
||||
self.stream().cr().modify(|_, write| write.en().enabled());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Acknowledges a DMA1 Stream6 terminal IRQ without accessing engine state.
|
||||
pub fn acknowledge_interrupt() -> TxCompletion {
|
||||
// SAFETY: this handler accesses only the fixed DMA1 Stream6 route.
|
||||
let controller = unsafe { &*stm32f722::DMA1::ptr() };
|
||||
let status = controller.hisr().read();
|
||||
let completion = DmaTerminalStatus {
|
||||
transfer_complete: status.tcif6().is_complete(),
|
||||
transfer_error: status.teif6().is_error(),
|
||||
direct_mode_error: status.dmeif6().is_error(),
|
||||
fifo_error: status.feif6().is_error(),
|
||||
}
|
||||
.classify();
|
||||
if completion == TxCompletion::NotPending {
|
||||
return completion;
|
||||
}
|
||||
|
||||
controller
|
||||
.st(6)
|
||||
.cr()
|
||||
.modify(|_, write| write.en().disabled());
|
||||
controller.hifcr().write(|write| {
|
||||
write
|
||||
.ctcif6()
|
||||
.clear()
|
||||
.chtif6()
|
||||
.clear()
|
||||
.cteif6()
|
||||
.clear()
|
||||
.cdmeif6()
|
||||
.clear()
|
||||
.cfeif6()
|
||||
.clear()
|
||||
});
|
||||
completion
|
||||
}
|
||||
|
||||
/// Returns whether DMA1 Stream6 owns a transfer.
|
||||
pub fn is_active(&self) -> bool {
|
||||
self.stream().cr().read().en().is_enabled()
|
||||
}
|
||||
|
||||
fn configure_stream(&mut self, buffer: &[u8]) {
|
||||
self.stream().cr().write(|write| {
|
||||
// SAFETY: Channel 4 and these transfer fields are the documented
|
||||
// USART2 TX mapping for DMA1 Stream6 on STM32F722.
|
||||
unsafe {
|
||||
write
|
||||
.chsel()
|
||||
.bits(4)
|
||||
.dir()
|
||||
.memory_to_peripheral()
|
||||
.pl()
|
||||
.very_high()
|
||||
.minc()
|
||||
.incremented()
|
||||
.pinc()
|
||||
.fixed()
|
||||
.msize()
|
||||
.bits8()
|
||||
.psize()
|
||||
.bits8()
|
||||
.circ()
|
||||
.disabled()
|
||||
.tcie()
|
||||
.enabled()
|
||||
.teie()
|
||||
.enabled()
|
||||
.dmeie()
|
||||
.enabled()
|
||||
}
|
||||
});
|
||||
self.stream().ndtr().write(|write| {
|
||||
// SAFETY: `start` checked the slice length against the NDTR limit.
|
||||
unsafe { write.bits(buffer.len() as u32) }
|
||||
});
|
||||
self.stream().par().write(|write| {
|
||||
// SAFETY: This is the memory-mapped USART2 transmit-data register.
|
||||
unsafe { write.bits(self.usart.transmit_data_register_address()) }
|
||||
});
|
||||
self.stream().m0ar().write(|write| {
|
||||
// SAFETY: The caller of `start` upholds the buffer lifetime contract.
|
||||
unsafe { write.bits(buffer.as_ptr() as u32) }
|
||||
});
|
||||
}
|
||||
|
||||
fn stream(&self) -> &stm32f722::dma2::ST {
|
||||
self.controller().st(6)
|
||||
}
|
||||
|
||||
fn clear_interrupt_flags(&self) {
|
||||
self.controller().hifcr().write(|write| {
|
||||
write
|
||||
.ctcif6()
|
||||
.clear()
|
||||
.chtif6()
|
||||
.clear()
|
||||
.cteif6()
|
||||
.clear()
|
||||
.cdmeif6()
|
||||
.clear()
|
||||
.cfeif6()
|
||||
.clear()
|
||||
});
|
||||
}
|
||||
|
||||
fn controller(&self) -> &stm32f722::dma2::RegisterBlock {
|
||||
// SAFETY: SPI3 owns DMA1 initialization; USART2 TX is permanently
|
||||
// assigned to disjoint Stream6 and accesses no other stream.
|
||||
unsafe { &*stm32f722::DMA1::ptr() }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
struct DmaTerminalStatus {
|
||||
transfer_complete: bool,
|
||||
transfer_error: bool,
|
||||
direct_mode_error: bool,
|
||||
fifo_error: bool,
|
||||
}
|
||||
|
||||
impl DmaTerminalStatus {
|
||||
const fn classify(self) -> TxCompletion {
|
||||
if self.transfer_error || self.direct_mode_error || self.fifo_error {
|
||||
TxCompletion::TransferFault
|
||||
} else if self.transfer_complete {
|
||||
TxCompletion::Complete
|
||||
} else {
|
||||
TxCompletion::NotPending
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DmaTxEngine for Usart2TxDma {
|
||||
type StartError = Usart2TxDmaStartError;
|
||||
|
||||
fn start(&mut self, buffer: DmaReadableBuffer<'_>) -> Result<(), Self::StartError> {
|
||||
Self::start(self, buffer)
|
||||
}
|
||||
}
|
||||
|
||||
#[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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
//! Platform-owned STM32F722 electrical route implementations.
|
||||
|
||||
use super::peripherals::{Spi1, Spi1Error, Spi3, Spi3Error};
|
||||
use kaptan_spi::{SpiCompletion, SpiDevice};
|
||||
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 {
|
||||
pub(crate) fn new(rcc: &stm32f722::RCC, spi: Spi1, chip_select: stm32f722::GPIOB) -> Self {
|
||||
rcc.ahb1enr().modify(|_, write| write.gpioben().set_bit());
|
||||
chip_select
|
||||
.moder()
|
||||
.modify(|_, write| write.moder2().output());
|
||||
chip_select
|
||||
.otyper()
|
||||
.modify(|_, write| write.ot2().push_pull());
|
||||
chip_select
|
||||
.ospeedr()
|
||||
.modify(|_, write| write.ospeedr2().low_speed());
|
||||
chip_select
|
||||
.pupdr()
|
||||
.modify(|_, write| write.pupdr2().floating());
|
||||
chip_select.bsrr().write(|write| write.bs2().set_bit());
|
||||
Self { spi, chip_select }
|
||||
}
|
||||
|
||||
fn select(&self) {
|
||||
self.chip_select.bsrr().write(|write| write.br2().set_bit());
|
||||
}
|
||||
|
||||
fn deselect(&self) {
|
||||
self.chip_select.bsrr().write(|write| write.bs2().set_bit());
|
||||
}
|
||||
}
|
||||
|
||||
impl SpiDevice for Mpu6000Spi1Route {
|
||||
type StartError = Spi1Error;
|
||||
|
||||
fn start_transfer(
|
||||
&mut self,
|
||||
transmit: &[u8],
|
||||
receive: &mut [u8],
|
||||
) -> Result<(), Self::StartError> {
|
||||
self.select();
|
||||
if let Err(error) = self.spi.start_transfer(transmit, receive) {
|
||||
self.deselect();
|
||||
return Err(error);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn complete_transfer(&mut self, completion: SpiCompletion) {
|
||||
self.spi.complete_transfer(completion);
|
||||
if matches!(
|
||||
completion,
|
||||
SpiCompletion::Complete | SpiCompletion::TransferFault
|
||||
) {
|
||||
self.deselect();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Owns the ICM-42688-P SPI3 transaction path. PA15 is its chip select.
|
||||
///
|
||||
/// GPIOA itself stays owned by the platform data-channel/LED state. The route
|
||||
/// uses only PA15 BSRR writes, disjoint from USART2 (PA2/PA3) and the blue
|
||||
/// LED (PA13), so no read-modify-write alias exists between these owners.
|
||||
pub(crate) struct Icm42688Spi3Route {
|
||||
spi: Spi3,
|
||||
}
|
||||
|
||||
impl Icm42688Spi3Route {
|
||||
pub(crate) fn new(rcc: &stm32f722::RCC, spi: Spi3, chip_select: &stm32f722::GPIOA) -> Self {
|
||||
rcc.ahb1enr().modify(|_, write| write.gpioaen().set_bit());
|
||||
chip_select
|
||||
.moder()
|
||||
.modify(|_, write| write.moder15().output());
|
||||
chip_select
|
||||
.otyper()
|
||||
.modify(|_, write| write.ot15().push_pull());
|
||||
chip_select
|
||||
.ospeedr()
|
||||
.modify(|_, write| write.ospeedr15().low_speed());
|
||||
chip_select
|
||||
.pupdr()
|
||||
.modify(|_, write| write.pupdr15().floating());
|
||||
chip_select.bsrr().write(|write| write.bs15().set_bit());
|
||||
Self { spi }
|
||||
}
|
||||
|
||||
fn chip_select_port() -> &'static stm32f722::gpioa::RegisterBlock {
|
||||
// SAFETY: RTIC initialization configures GPIOA once before interrupts
|
||||
// are enabled. This route subsequently writes only PA15 through BSRR;
|
||||
// USART2 uses PA2/PA3 and the LED uses PA13 through disjoint BSRR bits.
|
||||
// No task performs a GPIOA read-modify-write on those shared outputs.
|
||||
unsafe { &*stm32f722::GPIOA::ptr() }
|
||||
}
|
||||
|
||||
fn select(&self) {
|
||||
Self::chip_select_port()
|
||||
.bsrr()
|
||||
.write(|write| write.br15().set_bit());
|
||||
}
|
||||
|
||||
fn deselect(&self) {
|
||||
Self::chip_select_port()
|
||||
.bsrr()
|
||||
.write(|write| write.bs15().set_bit());
|
||||
}
|
||||
}
|
||||
|
||||
impl SpiDevice for Icm42688Spi3Route {
|
||||
type StartError = Spi3Error;
|
||||
|
||||
fn start_transfer(
|
||||
&mut self,
|
||||
transmit: &[u8],
|
||||
receive: &mut [u8],
|
||||
) -> Result<(), Self::StartError> {
|
||||
self.select();
|
||||
if let Err(error) = self.spi.start_transfer(transmit, receive) {
|
||||
self.deselect();
|
||||
return Err(error);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn complete_transfer(&mut self, completion: SpiCompletion) {
|
||||
self.spi.complete_transfer(completion);
|
||||
if matches!(
|
||||
completion,
|
||||
SpiCompletion::Complete | SpiCompletion::TransferFault
|
||||
) {
|
||||
self.deselect();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
//! 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,
|
||||
},
|
||||
};
|
||||
use kaptan_log::{ErrorReport, StaticError};
|
||||
use kaptan_sensor::imu::ImuSnapshot;
|
||||
use kaptan_spi::SpiCompletion;
|
||||
use stm32f7::stm32f722;
|
||||
|
||||
use super::routes::{Icm42688Spi3Route, Mpu6000Spi1Route};
|
||||
|
||||
// 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,
|
||||
};
|
||||
|
||||
/// A profile-neutral IMU worker result. Device protocol errors are consumed
|
||||
/// here because flight policy only needs the terminal healthy/fault outcome.
|
||||
pub(crate) enum ImuEvent {
|
||||
Idle,
|
||||
Initialized,
|
||||
Sample(ImuSnapshot),
|
||||
Fault(&'static StaticError),
|
||||
}
|
||||
|
||||
/// Opaque owner of the selected F722 IMU service.
|
||||
///
|
||||
/// RTIC places local resource types in generated public metadata, so this
|
||||
/// wrapper is public. The device services and routes remain private to the
|
||||
/// platform, preserving the board-facing abstraction boundary.
|
||||
pub struct F722Imu {
|
||||
inner: F722ImuKind,
|
||||
mount: ImuMount,
|
||||
}
|
||||
|
||||
/// The two concrete IMU services supported by the F722 platform.
|
||||
enum F722ImuKind {
|
||||
Mpu6000(Mpu6000Service<DwtMicrosecondClock, Mpu6000Spi1Route>),
|
||||
Icm42688(Icm42688Service<DwtMicrosecondClock, Icm42688Spi3Route>),
|
||||
}
|
||||
|
||||
impl F722Imu {
|
||||
pub(crate) fn mpu6000(
|
||||
rcc: &stm32f722::RCC,
|
||||
spi: stm32f722::SPI1,
|
||||
dma: stm32f722::DMA2,
|
||||
chip_select: stm32f722::GPIOB,
|
||||
clock: DwtMicrosecondClock,
|
||||
mount: ImuMount,
|
||||
) -> Self {
|
||||
Self {
|
||||
inner: F722ImuKind::Mpu6000(Mpu6000Service::new(
|
||||
clock,
|
||||
Mpu6000::new(
|
||||
Mpu6000Spi1Route::new(rcc, Spi1::new(rcc, spi, dma), chip_select),
|
||||
MPU6000_CONFIG,
|
||||
),
|
||||
)),
|
||||
mount,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn icm42688(
|
||||
rcc: &stm32f722::RCC,
|
||||
spi: stm32f722::SPI3,
|
||||
dma: stm32f722::DMA1,
|
||||
chip_select: &stm32f722::GPIOA,
|
||||
clock: DwtMicrosecondClock,
|
||||
mount: ImuMount,
|
||||
) -> Self {
|
||||
Self {
|
||||
inner: F722ImuKind::Icm42688(Icm42688Service::new(
|
||||
clock,
|
||||
Icm42688::new(Icm42688Spi3Route::new(
|
||||
rcc,
|
||||
Spi3::new(rcc, spi, dma),
|
||||
chip_select,
|
||||
)),
|
||||
)),
|
||||
mount,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn begin(&mut self) -> ImuEvent {
|
||||
match &mut self.inner {
|
||||
F722ImuKind::Mpu6000(imu) => match imu.begin() {
|
||||
Ok(()) => ImuEvent::Idle,
|
||||
Err(error) => ImuEvent::Fault(error.static_error()),
|
||||
},
|
||||
F722ImuKind::Icm42688(imu) => match imu.begin() {
|
||||
Ok(()) => ImuEvent::Idle,
|
||||
Err(error) => ImuEvent::Fault(error.static_error()),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn on_periodic_tick(&mut self) -> ImuEvent {
|
||||
match &mut self.inner {
|
||||
F722ImuKind::Mpu6000(imu) => mpu_event(imu.on_periodic_tick()),
|
||||
F722ImuKind::Icm42688(imu) => icm_event(imu.on_periodic_tick(), self.mount),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn on_data_ready(&mut self) -> ImuEvent {
|
||||
match &mut self.inner {
|
||||
F722ImuKind::Mpu6000(imu) => mpu_event(imu.on_data_ready()),
|
||||
F722ImuKind::Icm42688(imu) => icm_event(imu.on_data_ready(), self.mount),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn on_transfer_complete(&mut self, completion: SpiCompletion) -> ImuEvent {
|
||||
match &mut self.inner {
|
||||
F722ImuKind::Mpu6000(imu) => mpu_event(imu.on_transfer_complete(completion)),
|
||||
F722ImuKind::Icm42688(imu) => {
|
||||
icm_event(imu.on_transfer_complete(completion), self.mount)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn mpu_event<E: ErrorReport>(event: Mpu6000ServiceEvent<E>) -> ImuEvent {
|
||||
match event {
|
||||
Mpu6000ServiceEvent::Idle => ImuEvent::Idle,
|
||||
Mpu6000ServiceEvent::Initialized => ImuEvent::Initialized,
|
||||
Mpu6000ServiceEvent::Sample(sample) => ImuEvent::Sample(sample),
|
||||
Mpu6000ServiceEvent::Fault(error) => ImuEvent::Fault(error.static_error()),
|
||||
}
|
||||
}
|
||||
|
||||
fn icm_event<E: ErrorReport>(event: Icm42688ServiceEvent<E>, mount: ImuMount) -> ImuEvent {
|
||||
match event {
|
||||
Icm42688ServiceEvent::Idle => ImuEvent::Idle,
|
||||
Icm42688ServiceEvent::Initialized => ImuEvent::Initialized,
|
||||
Icm42688ServiceEvent::Sample(sample) => ImuEvent::Sample(align_mount(sample, mount)),
|
||||
Icm42688ServiceEvent::Fault(error) => ImuEvent::Fault(error.static_error()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Applies the board-declared fixed IMU-to-body transform.
|
||||
///
|
||||
/// This is immutable board metadata validated against the selected electrical
|
||||
/// route before initialization. It is not an airframe calibration setting.
|
||||
fn align_mount(mut sample: ImuSnapshot, mount: ImuMount) -> ImuSnapshot {
|
||||
match mount {
|
||||
ImuMount::BodyAligned => sample,
|
||||
ImuMount::Yaw180 => {
|
||||
sample.acceleration_milligravity[0] =
|
||||
sample.acceleration_milligravity[0].saturating_neg();
|
||||
sample.acceleration_milligravity[1] =
|
||||
sample.acceleration_milligravity[1].saturating_neg();
|
||||
sample.angular_rate_milliradians_per_second[0] =
|
||||
sample.angular_rate_milliradians_per_second[0].saturating_neg();
|
||||
sample.angular_rate_milliradians_per_second[1] =
|
||||
sample.angular_rate_milliradians_per_second[1].saturating_neg();
|
||||
sample
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::align_mount;
|
||||
use kaptan_board::ImuMount;
|
||||
use kaptan_sensor::imu::ImuSnapshot;
|
||||
use kaptan_time::Timestamp;
|
||||
|
||||
#[test]
|
||||
fn geprc_profile_applies_its_documented_yaw_alignment() {
|
||||
let sample = align_mount(
|
||||
ImuSnapshot::new(
|
||||
Timestamp::from_microseconds(0),
|
||||
[10, 20, 30],
|
||||
[40, 50, 60],
|
||||
0,
|
||||
),
|
||||
ImuMount::Yaw180,
|
||||
);
|
||||
assert_eq!(sample.acceleration_milligravity, [-10, -20, 30]);
|
||||
assert_eq!(sample.angular_rate_milliradians_per_second, [-40, -50, 60]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,406 @@
|
||||
//! 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.
|
||||
|
||||
#![no_std]
|
||||
|
||||
use core::marker::PhantomData;
|
||||
use kaptan_log::KaptanError;
|
||||
|
||||
pub use kaptan_board::BoardConfig;
|
||||
use kaptan_board::{BoardConfigError, ImuDevice, ImuMount, MavlinkPort};
|
||||
|
||||
#[cfg_attr(
|
||||
not(target_os = "none"),
|
||||
allow(dead_code, unused_imports, unused_macros)
|
||||
)]
|
||||
mod f722;
|
||||
|
||||
/// Dependencies used only while the platform expands its private RTIC macro.
|
||||
/// They stay crate-private: board binaries never name them.
|
||||
#[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;
|
||||
#[cfg(target_os = "none")]
|
||||
pub(crate) use panic_halt as panic;
|
||||
pub(crate) use rtic as rtic_app;
|
||||
pub(crate) use stm32f7 as pac;
|
||||
#[cfg(not(target_os = "none"))]
|
||||
pub(crate) 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.
|
||||
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.
|
||||
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)
|
||||
}
|
||||
|
||||
/// MPU6000 component selector.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct Mpu6000;
|
||||
|
||||
/// The supported MPU6000 SPI1 electrical route before selecting DRDY.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct Mpu6000Spi1;
|
||||
|
||||
/// The complete MPU6000 SPI1 / PA4 data-ready route.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct Mpu6000Spi1Exti4;
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
/// 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.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct Icm42688Spi3;
|
||||
|
||||
/// The complete ICM-42688-P SPI3 / PC3 data-ready route.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct Icm42688Spi3Exti3;
|
||||
|
||||
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.
|
||||
#[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.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct MavlinkUsart2;
|
||||
|
||||
impl Mavlink {
|
||||
/// Selects the supported USART1 MAVLink route.
|
||||
pub const fn on_usart1() -> MavlinkUsart1 {
|
||||
MavlinkUsart1
|
||||
}
|
||||
|
||||
/// Selects the supported USART2 MAVLink route.
|
||||
pub const fn on_usart2() -> MavlinkUsart2 {
|
||||
MavlinkUsart2
|
||||
}
|
||||
}
|
||||
|
||||
/// 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.
|
||||
#[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
|
||||
}
|
||||
|
||||
/// 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.
|
||||
#[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)>,
|
||||
}
|
||||
|
||||
impl F7BoardBuilder {
|
||||
/// Starts an empty board declaration.
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
_state: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for F7BoardBuilder {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
/// 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,
|
||||
}
|
||||
}
|
||||
|
||||
/// 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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,
|
||||
}
|
||||
|
||||
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 {
|
||||
profile: F722Profile::Mpu6000Spi1Usart1,
|
||||
vertical: $route,
|
||||
})
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl_mpu6000_routes!(Mtf01Uart4, f722::VerticalRoute::Mtf01);
|
||||
impl_mpu6000_routes!(LidarLiteV3I2c1, f722::VerticalRoute::LidarLiteV3);
|
||||
impl_mpu6000_routes!(NoVerticalSensor, f722::VerticalRoute::None);
|
||||
|
||||
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 {
|
||||
profile: F722Profile::Icm42688Spi3Usart2,
|
||||
vertical: $route,
|
||||
})
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl_icm42688_routes!(NoVerticalSensor, f722::VerticalRoute::None);
|
||||
impl_icm42688_routes!(Mtf01Uart4, f722::VerticalRoute::Mtf01);
|
||||
impl_icm42688_routes!(LidarLiteV3I2c1, f722::VerticalRoute::LidarLiteV3);
|
||||
|
||||
/// 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>;
|
||||
}
|
||||
|
||||
/// 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"),
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
/// Installs the one platform-owned RTIC application for a declarative board.
|
||||
///
|
||||
/// Board crates can only select one of the reviewed physical-sensor telemetry
|
||||
/// route descriptors; RTIC ownership stays platform-private.
|
||||
///
|
||||
/// The application binds the finite superset of interrupt roles needed by the
|
||||
/// currently supported F722 profiles. The route declaration selects which
|
||||
/// roles are initialized; inactive bindings acknowledge nothing. Adding an
|
||||
/// interrupt role is therefore a platform-only change, not a board task graph.
|
||||
#[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;
|
||||
};
|
||||
}
|
||||
|
||||
/// Link anchor for the platform-owned F722 board-I/O application.
|
||||
#[doc(hidden)]
|
||||
pub fn firmware_anchor() {}
|
||||
@@ -0,0 +1,253 @@
|
||||
use std::{fs, path::PathBuf};
|
||||
|
||||
use kaptan_board::{
|
||||
BoardConfig, BoardProfile, DataChannelRates, ImuDevice, ImuMount, MavlinkEndpoint, MavlinkPort,
|
||||
TelemetryProfile, VerticalSensingKind,
|
||||
};
|
||||
use kaptan_log::{ErrorAdditionalInformation, ErrorPath, LogFlags, LogRecord, LogText, Severity};
|
||||
use kaptan_log_mavlink::{DiagnosticPayload, encode_diagnostic};
|
||||
use kaptan_mavlink::encoder::MavlinkEncoder;
|
||||
use platform_stm32f7::{
|
||||
BoardError, F7BoardBuilder, Mavlink, Mpu6000, Pa4, VerticalSensor, validate_board_config,
|
||||
};
|
||||
|
||||
fn platform_source(relative: &str) -> String {
|
||||
let root = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
fs::read_to_string(root.join(relative)).expect("platform source must exist")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn former_motor_pins_are_inhibited_before_all_other_initialization() {
|
||||
let module = platform_source("src/f722/mod.rs");
|
||||
let interlock = module
|
||||
.find("motor_inhibit::inhibit_motor_pins")
|
||||
.expect("F722 initialization must claim the former motor pins first");
|
||||
let validation = module
|
||||
.find("validate_board_config")
|
||||
.expect("F722 initialization must validate its board configuration");
|
||||
let board_initialization = module
|
||||
.find("board::initialize")
|
||||
.expect("F722 initialization must subsequently claim telemetry I/O");
|
||||
|
||||
assert!(interlock < validation);
|
||||
assert!(validation < board_initialization);
|
||||
|
||||
let interlock = platform_source("src/f722/motor_inhibit.rs");
|
||||
let reset = interlock
|
||||
.find("port.bsrr()")
|
||||
.expect("interlock must reset the former motor-pin output latches");
|
||||
let output_mode = interlock
|
||||
.find("port.moder()")
|
||||
.expect("interlock must configure the former motor pins as GPIO outputs");
|
||||
assert!(
|
||||
reset < output_mode,
|
||||
"output latches must be low before GPIO output mode is selected"
|
||||
);
|
||||
for pin in ["br0", "br1", "br4", "br5"] {
|
||||
assert!(interlock.contains(pin), "interlock must reset PB pin {pin}");
|
||||
}
|
||||
for pin in ["moder0", "moder1", "moder4", "moder5"] {
|
||||
assert!(
|
||||
interlock.contains(pin),
|
||||
"interlock must configure PB pin {pin}"
|
||||
);
|
||||
}
|
||||
for pin in ["ot0", "ot1", "ot4", "ot5"] {
|
||||
assert!(
|
||||
interlock.contains(pin),
|
||||
"interlock must set PB pin {pin} push-pull"
|
||||
);
|
||||
}
|
||||
for pin in ["ospeedr0", "ospeedr1", "ospeedr4", "ospeedr5"] {
|
||||
assert!(
|
||||
interlock.contains(pin),
|
||||
"interlock must set PB pin {pin} low speed"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn mpu_config() -> BoardConfig {
|
||||
BoardConfig {
|
||||
name: "test",
|
||||
mavlink: MavlinkEndpoint {
|
||||
system_id: 1,
|
||||
component_id: 1,
|
||||
port: MavlinkPort::Serial1,
|
||||
baud: 115_200,
|
||||
},
|
||||
profile: BoardProfile::TelemetryOnly(TelemetryProfile {
|
||||
imu: ImuDevice::Mpu6000,
|
||||
imu_mount: ImuMount::BodyAligned,
|
||||
vertical_sensing: VerticalSensingKind::RangeAndOpticalFlow,
|
||||
data_channel: DataChannelRates {
|
||||
service_hz: 100,
|
||||
raw_imu_hz: 20,
|
||||
range_hz: Some(20),
|
||||
optical_flow_hz: Some(20),
|
||||
heartbeat_hz: 2,
|
||||
},
|
||||
maximum_log_level: Severity::Info,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn immutable_board_policy_must_match_the_executed_route_graph() {
|
||||
let routes = F7BoardBuilder::new()
|
||||
.imu(Mpu6000::on_spi1().with_drdy(Pa4))
|
||||
.mavlink(Mavlink::on_usart1())
|
||||
.vertical_sensor(VerticalSensor::mtf01_on_uart4())
|
||||
.build()
|
||||
.unwrap();
|
||||
assert_eq!(validate_board_config(mpu_config(), routes), Ok(()));
|
||||
|
||||
let mut wrong_imu = mpu_config();
|
||||
let BoardProfile::TelemetryOnly(profile) = &mut wrong_imu.profile else {
|
||||
unreachable!();
|
||||
};
|
||||
profile.imu = ImuDevice::Icm42688;
|
||||
assert_eq!(
|
||||
validate_board_config(wrong_imu, routes),
|
||||
Err(BoardError::ImuRouteMismatch)
|
||||
);
|
||||
|
||||
let mut wrong_mount = mpu_config();
|
||||
let BoardProfile::TelemetryOnly(profile) = &mut wrong_mount.profile else {
|
||||
unreachable!();
|
||||
};
|
||||
profile.imu_mount = ImuMount::Yaw180;
|
||||
assert_eq!(
|
||||
validate_board_config(wrong_mount, routes),
|
||||
Err(BoardError::ImuMountMismatch)
|
||||
);
|
||||
|
||||
let mut wrong_vertical = mpu_config();
|
||||
let BoardProfile::TelemetryOnly(profile) = &mut wrong_vertical.profile else {
|
||||
unreachable!();
|
||||
};
|
||||
profile.vertical_sensing = VerticalSensingKind::Range;
|
||||
profile.data_channel.optical_flow_hz = None;
|
||||
assert_eq!(
|
||||
validate_board_config(wrong_vertical, routes),
|
||||
Err(BoardError::VerticalSensorCapabilityMismatch)
|
||||
);
|
||||
|
||||
let mut wrong_port = mpu_config();
|
||||
wrong_port.mavlink.port = MavlinkPort::Serial2;
|
||||
assert_eq!(
|
||||
validate_board_config(wrong_port, routes),
|
||||
Err(BoardError::MavlinkRouteMismatch)
|
||||
);
|
||||
|
||||
let mut overcommitted_link = mpu_config();
|
||||
overcommitted_link.mavlink.baud = 9_600;
|
||||
assert_eq!(
|
||||
validate_board_config(overcommitted_link, routes),
|
||||
Err(BoardError::TelemetryLinkOvercommitted)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn telemetry_platform_has_no_physical_actuator_peripheral_path() {
|
||||
let sources = [
|
||||
platform_source("src/lib.rs"),
|
||||
platform_source("src/f722/mod.rs"),
|
||||
platform_source("src/f722/board.rs"),
|
||||
platform_source("src/f722/firmware.rs"),
|
||||
platform_source("src/f722/interrupts.rs"),
|
||||
]
|
||||
.join("\n");
|
||||
|
||||
for forbidden in [
|
||||
"mod actuator",
|
||||
"MotorOutput",
|
||||
"MotorRoute",
|
||||
"MotorOutputs",
|
||||
"TIM3",
|
||||
"TIM5",
|
||||
"Dshot",
|
||||
"Pwm",
|
||||
"device.TIM3",
|
||||
"device.TIM5",
|
||||
] {
|
||||
assert!(
|
||||
!sources.contains(forbidden),
|
||||
"telemetry platform must not expose actuator path {forbidden}"
|
||||
);
|
||||
}
|
||||
|
||||
let manifest = platform_source("Cargo.toml");
|
||||
assert!(
|
||||
!manifest.contains("kaptan-actuator"),
|
||||
"telemetry platform must not link the physical actuator crate"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn telemetry_has_no_inbound_command_authority() {
|
||||
let board = platform_source("src/f722/board.rs");
|
||||
let firmware = platform_source("src/f722/firmware.rs");
|
||||
|
||||
assert!(
|
||||
!board.contains("MavlinkReceiver"),
|
||||
"telemetry must not retain an inbound MAVLink dispatch path"
|
||||
);
|
||||
assert!(
|
||||
!board.contains("ArmRequest"),
|
||||
"telemetry must not decode arm commands"
|
||||
);
|
||||
assert!(
|
||||
!board.contains("MAV_RESULT_ACCEPTED"),
|
||||
"telemetry must not accept an arm command"
|
||||
);
|
||||
assert!(
|
||||
!firmware.contains("shared = [mavlink, motors]"),
|
||||
"the MAVLink worker must not share a motor resource"
|
||||
);
|
||||
assert!(
|
||||
!firmware.contains("MAVLINK_EVENT_RECEIVED"),
|
||||
"telemetry must not schedule a worker from an inbound command"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn f722_link_budget_uses_both_maximum_generated_diagnostic_frames() {
|
||||
let text = LogText::from_message(&"x".repeat(160));
|
||||
let log_record = LogRecord::new(Severity::Info, LogFlags::NONE, text);
|
||||
let error_record = LogRecord::error(
|
||||
LogFlags::NONE,
|
||||
ErrorPath::from_message(&"p".repeat(96)),
|
||||
ErrorAdditionalInformation::from_message(&"i".repeat(64)),
|
||||
);
|
||||
let mut encoder = MavlinkEncoder::new(1, 1);
|
||||
let DiagnosticPayload::Log(log_payload) = encode_diagnostic(&log_record) else {
|
||||
panic!("info record must use KAPTAN_LOG");
|
||||
};
|
||||
let log_frame = encoder.custom(&log_payload);
|
||||
let DiagnosticPayload::Error(error_payload) = encode_diagnostic(&error_record) else {
|
||||
panic!("error record must use KAPTAN_ERROR");
|
||||
};
|
||||
let error_frame = encoder.custom(&error_payload);
|
||||
|
||||
assert_eq!(log_frame.raw_bytes().len(), 175);
|
||||
assert_eq!(
|
||||
kaptan_mavlink::message::kaptan::KAPTAN_LOG_DATA::ENCODED_LEN,
|
||||
163
|
||||
);
|
||||
assert_eq!(error_frame.raw_bytes().len(), 175);
|
||||
assert_eq!(
|
||||
kaptan_mavlink::message::kaptan::KAPTAN_ERROR_DATA::ENCODED_LEN,
|
||||
163
|
||||
);
|
||||
assert_eq!(error_payload.path_length, 96);
|
||||
assert_eq!(error_payload.additional_information_length, 64);
|
||||
assert_eq!(error_payload.flags.bits(), LogFlags::NONE.bits());
|
||||
assert_eq!(
|
||||
62 * 48 + 10 * error_frame.raw_bytes().len() + 5 * log_frame.raw_bytes().len(),
|
||||
5_601
|
||||
);
|
||||
let board = platform_source("src/f722/board.rs");
|
||||
assert!(board.contains("KAPTAN_ERROR_FRAME_MAXIMUM_BYTES"));
|
||||
assert!(board.contains("KAPTAN_LOG_FRAME_MAXIMUM_BYTES"));
|
||||
assert!(board.contains("5,601 bytes/s"));
|
||||
}
|
||||
Reference in new issue
Block a user