Skip to main content

pixracerpro/
board.rs

1// ******************************************************************************
2// * File     : boards/pixracerpro/src/board.rs
3// * Date     : June 28, 2026
4// ******************************************************************************
5// *
6// * Copyright (c) 2023, AeroVironment, Inc.
7// * All rights reserved.
8// *
9// * Redistribution and use in source and binary forms, with or without
10// * modification, are permitted provided that the following conditions are met:
11// *
12// * 1.Redistributions of source code must retain the above copyright notice, this
13// * list of conditions and the following disclaimer.
14// *
15// * 2.Redistributions in binary form must reproduce the above copyright notice,
16// * this list of conditions and the following disclaimer in the documentation
17// * and/or other materials provided with the distribution.
18// *
19// * 3.Neither the name of the copyright holder nor the names of its
20// * contributors may be used to endorse or promote products derived from
21// * this software without specific prior written permission.
22// *
23// * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
24// * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
25// * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
26// * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
27// * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
28// * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
29// * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
30// * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
31// * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
32// * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
33// *
34// ******************************************************************************
35
36use veloxity_core::board::BoardIo;
37use veloxity_core::errors;
38use veloxity_core::math::FlightFloat;
39use veloxity_core::packets::ParamPacket;
40use veloxity_core::params::Params;
41use veloxity_core::sensors::SensorBus;
42
43use embassy_time::{Delay, Duration, Instant};
44use stm_32::cortex_m::prelude::_embedded_hal_blocking_delay_DelayMs;
45#[cfg(not(feature = "scope-timing-pins"))]
46use stm_32::cortex_m::prelude::_embedded_hal_blocking_delay_DelayUs;
47use stm_32::peripherals;
48use stm_32::peripherals::pwm::PixRacerProServoMonstrosity;
49use stm_32::*;
50
51#[cfg(feature = "runtime-diagnostics")]
52use core::sync::atomic::{AtomicU32, Ordering};
53
54include!("../../../platforms/stm_32/stm32h7x3_common.rs");
55
56const PARAM_STORAGE_TIMEOUT: Duration = Duration::from_secs(2);
57
58#[cfg(feature = "runtime-diagnostics")]
59#[unsafe(no_mangle)]
60pub static VELOXITY_DIAG_IMU_CONSUME: AtomicU32 = AtomicU32::new(0);
61#[cfg(feature = "runtime-diagnostics")]
62#[unsafe(no_mangle)]
63pub static VELOXITY_DIAG_IMU_CONSUME_ERROR: AtomicU32 = AtomicU32::new(0);
64#[cfg(feature = "runtime-diagnostics")]
65#[unsafe(no_mangle)]
66pub static VELOXITY_DIAG_IMU_CONSUME_AGE_SUM_US: AtomicU32 = AtomicU32::new(0);
67#[cfg(feature = "runtime-diagnostics")]
68#[unsafe(no_mangle)]
69pub static VELOXITY_DIAG_IMU_CONSUME_AGE_MAX_US: AtomicU32 = AtomicU32::new(0);
70
71#[cfg(feature = "runtime-diagnostics")]
72mod runtime_sensor_diagnostics {
73    use core::sync::atomic::{AtomicU32, Ordering};
74
75    use veloxity_core::{errors::SensorError, math::FlightFloat, sensors::SensorBus};
76
77    macro_rules! consume_counters {
78        ($count:ident, $errors:ident, $age_sum:ident, $age_max:ident) => {
79            #[unsafe(no_mangle)]
80            pub static $count: AtomicU32 = AtomicU32::new(0);
81            #[unsafe(no_mangle)]
82            pub static $errors: AtomicU32 = AtomicU32::new(0);
83            #[unsafe(no_mangle)]
84            pub static $age_sum: AtomicU32 = AtomicU32::new(0);
85            #[unsafe(no_mangle)]
86            pub static $age_max: AtomicU32 = AtomicU32::new(0);
87        };
88    }
89
90    consume_counters!(
91        VELOXITY_DIAG_MAG_CONSUME,
92        VELOXITY_DIAG_MAG_CONSUME_ERROR,
93        VELOXITY_DIAG_MAG_CONSUME_AGE_SUM_US,
94        VELOXITY_DIAG_MAG_CONSUME_AGE_MAX_US
95    );
96    consume_counters!(
97        VELOXITY_DIAG_BARO_CONSUME,
98        VELOXITY_DIAG_BARO_CONSUME_ERROR,
99        VELOXITY_DIAG_BARO_CONSUME_AGE_SUM_US,
100        VELOXITY_DIAG_BARO_CONSUME_AGE_MAX_US
101    );
102    consume_counters!(
103        VELOXITY_DIAG_PITOT_CONSUME,
104        VELOXITY_DIAG_PITOT_CONSUME_ERROR,
105        VELOXITY_DIAG_PITOT_CONSUME_AGE_SUM_US,
106        VELOXITY_DIAG_PITOT_CONSUME_AGE_MAX_US
107    );
108    consume_counters!(
109        VELOXITY_DIAG_RANGE_CONSUME,
110        VELOXITY_DIAG_RANGE_CONSUME_ERROR,
111        VELOXITY_DIAG_RANGE_CONSUME_AGE_SUM_US,
112        VELOXITY_DIAG_RANGE_CONSUME_AGE_MAX_US
113    );
114    consume_counters!(
115        VELOXITY_DIAG_GNSS_CONSUME,
116        VELOXITY_DIAG_GNSS_CONSUME_ERROR,
117        VELOXITY_DIAG_GNSS_CONSUME_AGE_SUM_US,
118        VELOXITY_DIAG_GNSS_CONSUME_AGE_MAX_US
119    );
120    consume_counters!(
121        VELOXITY_DIAG_BATTERY_CONSUME,
122        VELOXITY_DIAG_BATTERY_CONSUME_ERROR,
123        VELOXITY_DIAG_BATTERY_CONSUME_AGE_SUM_US,
124        VELOXITY_DIAG_BATTERY_CONSUME_AGE_MAX_US
125    );
126    consume_counters!(
127        VELOXITY_DIAG_RC_CONSUME,
128        VELOXITY_DIAG_RC_CONSUME_ERROR,
129        VELOXITY_DIAG_RC_CONSUME_AGE_SUM_US,
130        VELOXITY_DIAG_RC_CONSUME_AGE_MAX_US
131    );
132
133    pub fn record_bus<R: FlightFloat>(sensors: &SensorBus<R>, now_us: u64) {
134        macro_rules! record {
135            ($field:ident, $count:ident, $errors:ident, $age_sum:ident, $age_max:ident) => {
136                if let Some(result) = &sensors.$field {
137                    $count.fetch_add(1, Ordering::Relaxed);
138                    match result {
139                        Ok(packet) => {
140                            let age = now_us
141                                .saturating_sub(packet.header.timestamp)
142                                .min(u32::MAX as u64) as u32;
143                            $age_sum.fetch_add(age, Ordering::Relaxed);
144                            $age_max.fetch_max(age, Ordering::Relaxed);
145                        }
146                        Err(SensorError::GenericSensorError(_)) => {
147                            $errors.fetch_add(1, Ordering::Relaxed);
148                        }
149                    }
150                }
151            };
152        }
153        record!(
154            mag,
155            VELOXITY_DIAG_MAG_CONSUME,
156            VELOXITY_DIAG_MAG_CONSUME_ERROR,
157            VELOXITY_DIAG_MAG_CONSUME_AGE_SUM_US,
158            VELOXITY_DIAG_MAG_CONSUME_AGE_MAX_US
159        );
160        record!(
161            baro,
162            VELOXITY_DIAG_BARO_CONSUME,
163            VELOXITY_DIAG_BARO_CONSUME_ERROR,
164            VELOXITY_DIAG_BARO_CONSUME_AGE_SUM_US,
165            VELOXITY_DIAG_BARO_CONSUME_AGE_MAX_US
166        );
167        record!(
168            pitot,
169            VELOXITY_DIAG_PITOT_CONSUME,
170            VELOXITY_DIAG_PITOT_CONSUME_ERROR,
171            VELOXITY_DIAG_PITOT_CONSUME_AGE_SUM_US,
172            VELOXITY_DIAG_PITOT_CONSUME_AGE_MAX_US
173        );
174        record!(
175            range,
176            VELOXITY_DIAG_RANGE_CONSUME,
177            VELOXITY_DIAG_RANGE_CONSUME_ERROR,
178            VELOXITY_DIAG_RANGE_CONSUME_AGE_SUM_US,
179            VELOXITY_DIAG_RANGE_CONSUME_AGE_MAX_US
180        );
181        record!(
182            gnss,
183            VELOXITY_DIAG_GNSS_CONSUME,
184            VELOXITY_DIAG_GNSS_CONSUME_ERROR,
185            VELOXITY_DIAG_GNSS_CONSUME_AGE_SUM_US,
186            VELOXITY_DIAG_GNSS_CONSUME_AGE_MAX_US
187        );
188        record!(
189            battery,
190            VELOXITY_DIAG_BATTERY_CONSUME,
191            VELOXITY_DIAG_BATTERY_CONSUME_ERROR,
192            VELOXITY_DIAG_BATTERY_CONSUME_AGE_SUM_US,
193            VELOXITY_DIAG_BATTERY_CONSUME_AGE_MAX_US
194        );
195        record!(
196            rc,
197            VELOXITY_DIAG_RC_CONSUME,
198            VELOXITY_DIAG_RC_CONSUME_ERROR,
199            VELOXITY_DIAG_RC_CONSUME_AGE_SUM_US,
200            VELOXITY_DIAG_RC_CONSUME_AGE_MAX_US
201        );
202    }
203}
204
205fn wait_for_sd_response(request_id: u64) -> Option<ParamPacket> {
206    let start = Instant::now();
207    while start.elapsed() < PARAM_STORAGE_TIMEOUT {
208        match peripherals::sd_card::SD_READ_SIGNAL.try_take() {
209            Some(Ok(packet)) if packet.header.timestamp == request_id => return Some(packet),
210            Some(Ok(_)) => {}
211            Some(Err(_)) => return None,
212            None => {}
213        }
214        core::hint::spin_loop();
215    }
216    None
217}
218
219#[cfg(feature = "sensor-poll-diagnostics")]
220mod sensor_poll_diagnostics {
221    use core::sync::atomic::{AtomicU32, Ordering};
222
223    use veloxity_core::{errors::SensorError, math::FlightFloat};
224
225    #[unsafe(no_mangle)]
226    pub static VELOXITY_PIXRACER_DIAG_IMU: AtomicU32 = AtomicU32::new(0);
227    #[unsafe(no_mangle)]
228    pub static VELOXITY_PIXRACER_DIAG_IMU_ERR: AtomicU32 = AtomicU32::new(0);
229    #[unsafe(no_mangle)]
230    pub static VELOXITY_PIXRACER_DIAG_MAG: AtomicU32 = AtomicU32::new(0);
231    #[unsafe(no_mangle)]
232    pub static VELOXITY_PIXRACER_DIAG_MAG_ERR: AtomicU32 = AtomicU32::new(0);
233    #[unsafe(no_mangle)]
234    pub static VELOXITY_PIXRACER_DIAG_BARO: AtomicU32 = AtomicU32::new(0);
235    #[unsafe(no_mangle)]
236    pub static VELOXITY_PIXRACER_DIAG_BARO_ERR: AtomicU32 = AtomicU32::new(0);
237    #[unsafe(no_mangle)]
238    pub static VELOXITY_PIXRACER_DIAG_PITOT: AtomicU32 = AtomicU32::new(0);
239    #[unsafe(no_mangle)]
240    pub static VELOXITY_PIXRACER_DIAG_PITOT_ERR: AtomicU32 = AtomicU32::new(0);
241    #[unsafe(no_mangle)]
242    pub static VELOXITY_PIXRACER_DIAG_RANGE: AtomicU32 = AtomicU32::new(0);
243    #[unsafe(no_mangle)]
244    pub static VELOXITY_PIXRACER_DIAG_RANGE_ERR: AtomicU32 = AtomicU32::new(0);
245    #[unsafe(no_mangle)]
246    pub static VELOXITY_PIXRACER_DIAG_GNSS: AtomicU32 = AtomicU32::new(0);
247    #[unsafe(no_mangle)]
248    pub static VELOXITY_PIXRACER_DIAG_GNSS_ERR: AtomicU32 = AtomicU32::new(0);
249    #[unsafe(no_mangle)]
250    pub static VELOXITY_PIXRACER_DIAG_RC: AtomicU32 = AtomicU32::new(0);
251    #[unsafe(no_mangle)]
252    pub static VELOXITY_PIXRACER_DIAG_RC_ERR: AtomicU32 = AtomicU32::new(0);
253
254    pub fn record_bus<R: FlightFloat>(sensors: &veloxity_core::sensors::SensorBus<R>) {
255        record(
256            &sensors.imu,
257            &VELOXITY_PIXRACER_DIAG_IMU,
258            &VELOXITY_PIXRACER_DIAG_IMU_ERR,
259        );
260        record(
261            &sensors.mag,
262            &VELOXITY_PIXRACER_DIAG_MAG,
263            &VELOXITY_PIXRACER_DIAG_MAG_ERR,
264        );
265        record(
266            &sensors.baro,
267            &VELOXITY_PIXRACER_DIAG_BARO,
268            &VELOXITY_PIXRACER_DIAG_BARO_ERR,
269        );
270        record(
271            &sensors.pitot,
272            &VELOXITY_PIXRACER_DIAG_PITOT,
273            &VELOXITY_PIXRACER_DIAG_PITOT_ERR,
274        );
275        record(
276            &sensors.range,
277            &VELOXITY_PIXRACER_DIAG_RANGE,
278            &VELOXITY_PIXRACER_DIAG_RANGE_ERR,
279        );
280        record(
281            &sensors.gnss,
282            &VELOXITY_PIXRACER_DIAG_GNSS,
283            &VELOXITY_PIXRACER_DIAG_GNSS_ERR,
284        );
285        record(
286            &sensors.rc,
287            &VELOXITY_PIXRACER_DIAG_RC,
288            &VELOXITY_PIXRACER_DIAG_RC_ERR,
289        );
290    }
291
292    fn record<T>(sample: &Option<Result<T, SensorError>>, count: &AtomicU32, errors: &AtomicU32) {
293        if let Some(result) = sample {
294            count.fetch_add(1, Ordering::Relaxed);
295            if result.is_err() {
296                errors.fetch_add(1, Ordering::Relaxed);
297            }
298        }
299    }
300}
301
302fn spawn_task<S: Send>(
303    spawner: &embassy_executor::SendSpawner,
304    token: Result<embassy_executor::SpawnToken<S>, embassy_executor::SpawnError>,
305) {
306    spawner.spawn(token.expect("failed to allocate Embassy task"));
307}
308
309pub struct Board {
310    _probe: [Output<'static>; 3],
311    pub start_time: embassy_time::Instant,
312    test_pin_1: Output<'static>,
313    test_pin_2: Output<'static>,
314    pending_reset_to_bootloader: Option<bool>,
315    sd_request_id: u64,
316    #[cfg(feature = "sensor-poll-diagnostics")]
317    last_sbus_diag_ms: u32,
318    #[cfg(feature = "sensor-poll-diagnostics")]
319    sbus_rc_drains: u32,
320    #[cfg(feature = "sensor-poll-diagnostics")]
321    sbus_last_rc_status: u32,
322    #[cfg(feature = "sensor-poll-diagnostics")]
323    sbus_last_rc_lol: bool,
324}
325
326impl BoardIo for Board {
327    fn set_test_pin_1(&mut self, high: bool) {
328        if high {
329            self.test_pin_1.set_high();
330        } else {
331            self.test_pin_1.set_low();
332        }
333    }
334
335    fn set_test_pin_2(&mut self, high: bool) {
336        if high {
337            self.test_pin_2.set_high();
338        } else {
339            self.test_pin_2.set_low();
340        }
341    }
342
343    fn update_sensor_bus<R: FlightFloat>(&mut self, sensors: &mut SensorBus<R>) {
344        sensors.clear();
345        sensors.imu = peripherals::bmi08x::IMU_SIGNAL
346            .try_take()
347            .map(|result| result.map(|packet| packet.cast()));
348        sensors.mag = peripherals::ist8308::MAG_SIGNAL.try_take();
349        sensors.baro = peripherals::dps310::BARO_SIGNAL.try_take();
350        sensors.pitot = peripherals::ms4525::PITOT_SIGNAL.try_take();
351        sensors.range = peripherals::llv3hp::RANGE_SIGNAL.try_take();
352        sensors.gnss = peripherals::ublox::GNSS_SIGNAL.try_take();
353        sensors.battery = crate::battery::BATTERY_SIGNAL.try_take();
354        sensors.rc = peripherals::sbus::RC_CHANNEL.try_receive().ok();
355        #[cfg(feature = "runtime-diagnostics")]
356        runtime_sensor_diagnostics::record_bus(sensors, self.clock_micros());
357        #[cfg(feature = "sensor-poll-diagnostics")]
358        self.record_sbus_rc_drain(&sensors.rc);
359        #[cfg(feature = "sensor-poll-diagnostics")]
360        sensor_poll_diagnostics::record_bus(sensors);
361        #[cfg(feature = "sensor-poll-diagnostics")]
362        self.log_sbus_diagnostics_if_due();
363        #[cfg(not(feature = "scope-timing-pins"))]
364        if sensors.imu.is_some() {
365            let mut delay = Delay;
366            self.set_test_pin_1(true);
367            delay.delay_us(1u32);
368            self.set_test_pin_1(false);
369        }
370    }
371
372    fn imu_pending(&self) -> bool {
373        peripherals::bmi08x::IMU_SIGNAL.signaled()
374    }
375
376    fn update_imu_sensor<R: FlightFloat>(&mut self, sensors: &mut SensorBus<R>) {
377        sensors.clear();
378        sensors.imu = peripherals::bmi08x::IMU_SIGNAL
379            .try_take()
380            .map(|result| result.map(|packet| packet.cast()));
381        #[cfg(feature = "runtime-diagnostics")]
382        if sensors.imu.is_some() {
383            VELOXITY_DIAG_IMU_CONSUME.fetch_add(1, Ordering::Relaxed);
384            match &sensors.imu {
385                Some(Ok(packet)) => {
386                    let age = self
387                        .clock_micros()
388                        .saturating_sub(packet.header.timestamp)
389                        .min(u32::MAX as u64) as u32;
390                    VELOXITY_DIAG_IMU_CONSUME_AGE_SUM_US.fetch_add(age, Ordering::Relaxed);
391                    VELOXITY_DIAG_IMU_CONSUME_AGE_MAX_US.fetch_max(age, Ordering::Relaxed);
392                }
393                Some(Err(_)) => {
394                    VELOXITY_DIAG_IMU_CONSUME_ERROR.fetch_add(1, Ordering::Relaxed);
395                }
396                None => {}
397            }
398        }
399        #[cfg(feature = "sensor-poll-diagnostics")]
400        sensor_poll_diagnostics::record_bus(sensors);
401        #[cfg(not(feature = "scope-timing-pins"))]
402        if sensors.imu.is_some() {
403            let mut delay = Delay;
404            self.set_test_pin_1(true);
405            delay.delay_us(1u32);
406            self.set_test_pin_1(false);
407        }
408    }
409
410    fn update_service_sensor_bus<R: FlightFloat>(&mut self, sensors: &mut SensorBus<R>) {
411        sensors.clear();
412        sensors.mag = peripherals::ist8308::MAG_SIGNAL.try_take();
413        sensors.baro = peripherals::dps310::BARO_SIGNAL.try_take();
414        sensors.pitot = peripherals::ms4525::PITOT_SIGNAL.try_take();
415        sensors.range = peripherals::llv3hp::RANGE_SIGNAL.try_take();
416        sensors.gnss = peripherals::ublox::GNSS_SIGNAL.try_take();
417        sensors.battery = crate::battery::BATTERY_SIGNAL.try_take();
418        sensors.rc = peripherals::sbus::RC_CHANNEL.try_receive().ok();
419        #[cfg(feature = "runtime-diagnostics")]
420        runtime_sensor_diagnostics::record_bus(sensors, self.clock_micros());
421        #[cfg(feature = "sensor-poll-diagnostics")]
422        self.record_sbus_rc_drain(&sensors.rc);
423        #[cfg(feature = "sensor-poll-diagnostics")]
424        sensor_poll_diagnostics::record_bus(sensors);
425        #[cfg(feature = "sensor-poll-diagnostics")]
426        self.log_sbus_diagnostics_if_due();
427    }
428
429    fn serial_rx_read(&mut self, buf: &mut [u8]) -> Option<Result<usize, errors::TelemError>> {
430        #[cfg(feature = "usb-vcp-serial")]
431        match peripherals::vcp::VCP_RX.try_read(buf) {
432            Ok(n) => {
433                return Some(Ok(n));
434            }
435            Err(embassy_sync::pipe::TryReadError::Empty) => {
436                // This is NORMAL. Do not log an error.
437                // Return Ok(0) to indicate "no bytes right now" without erroring.
438                return Some(Ok(0));
439            }
440        }
441
442        #[cfg(not(feature = "usb-vcp-serial"))]
443        match peripherals::telem::TELEM_RX.try_read(buf) {
444            Ok(n) => {
445                return Some(Ok(n));
446            }
447            Err(embassy_sync::pipe::TryReadError::Empty) => {
448                // This is NORMAL. Do not log an error.
449                // Return Ok(0) to indicate "no bytes right now" without erroring.
450                return Some(Ok(0));
451            }
452        }
453    }
454
455    fn configure_battery_monitor(&mut self, voltage_multiplier: f32, current_multiplier: f32) {
456        crate::battery::configure_multipliers(voltage_multiplier, current_multiplier);
457    }
458    fn serial_tx_write(&mut self, bytes: &[u8]) -> Option<Result<usize, errors::TelemError>> {
459        #[cfg(feature = "usb-vcp-serial")]
460        {
461            #[cfg(feature = "runtime-diagnostics")]
462            let message_id = (bytes.len() > 5 && bytes[0] == 0xFE).then_some(bytes[5]);
463            #[cfg(feature = "runtime-diagnostics")]
464            peripherals::vcp::record_frame_attempt(message_id);
465            let mut n = 0;
466            let len = bytes.len();
467            loop {
468                match peripherals::vcp::VCP_TX.try_write(&bytes[n..len]) {
469                    Ok(wrote) => {
470                        if wrote == (len - n) {
471                            break;
472                        } else {
473                            n += wrote;
474                        }
475                    }
476                    Err(_) => {
477                        #[cfg(feature = "runtime-diagnostics")]
478                        peripherals::vcp::record_frame_rejected(message_id, n != 0);
479                        return Some(Err(errors::TelemError::GenericTelemError(
480                            "Error Writing USB VCP Packet!",
481                        )));
482                    }
483                }
484            }
485            #[cfg(feature = "runtime-diagnostics")]
486            peripherals::vcp::record_frame_enqueued(message_id);
487            return Some(Ok(len));
488        }
489
490        #[cfg(not(feature = "usb-vcp-serial"))]
491        {
492            let mut n = 0;
493            let len = bytes.len();
494            loop {
495                match peripherals::telem::TELEM_TX.try_write(&bytes[n..len]) {
496                    Ok(wrote) => {
497                        if wrote == (len - n) {
498                            break;
499                        } else {
500                            n += wrote;
501                        }
502                    }
503                    Err(_) => {
504                        return Some(Err(errors::TelemError::GenericTelemError(
505                            "Error Writing Telem Packet!",
506                        )));
507                    }
508                }
509            }
510            Some(Ok(len))
511        }
512    }
513
514    fn clock_millis(&self) -> u32 {
515        self.start_time.elapsed().as_millis() as u32
516    }
517
518    fn clock_micros(&self) -> u64 {
519        self.start_time.elapsed().as_micros() as u64
520    }
521
522    fn read_params(&mut self, params: &mut Params) -> bool {
523        let request_id = self.next_sd_request_id();
524        peripherals::sd_card::SD_READ_SIGNAL.reset();
525        peripherals::sd_card::SD_READ_REQUEST_SIGNAL.signal(request_id);
526
527        let Some(packet) = wait_for_sd_response(request_id) else {
528            return false;
529        };
530        let Some(stored) = peripherals::sd_card::decode_params(&packet) else {
531            return false;
532        };
533
534        *params = stored;
535        true
536    }
537
538    fn write_params(&mut self, params: &Params) -> bool {
539        let Some(mut packet) = peripherals::sd_card::encode_params(params) else {
540            return false;
541        };
542        let request_id = self.next_sd_request_id();
543        packet.header.timestamp = request_id;
544
545        peripherals::sd_card::SD_READ_SIGNAL.reset();
546        peripherals::sd_card::SD_WRITE_SIGNAL.signal(Ok(packet));
547
548        matches!(
549            wait_for_sd_response(request_id),
550            Some(packet) if packet.header.status == 1
551        )
552    }
553
554    fn reboot(&mut self) -> bool {
555        self.pending_reset_to_bootloader = Some(false);
556        true
557    }
558
559    fn reboot_to_bootloader(&mut self) -> bool {
560        self.pending_reset_to_bootloader = Some(true);
561        true
562    }
563
564    fn run_deferred_board_actions(&mut self) {
565        if self.pending_reset_to_bootloader.take().is_some() {
566            let mut delay = Delay;
567            delay.delay_ms(20u32);
568            stm_32::cortex_m::peripheral::SCB::sys_reset();
569        }
570    }
571}
572
573impl Board {
574    fn next_sd_request_id(&mut self) -> u64 {
575        self.sd_request_id = self.sd_request_id.wrapping_add(1);
576        self.sd_request_id
577    }
578
579    pub fn new() -> (Board, PixRacerProServoMonstrosity) {
580        let p: EMBASSY_Peripherals = embassy_stm32::init(clock_config(24));
581
582        let start_time = embassy_time::Instant::now();
583
584        let battery_monitor = crate::battery::PixracerBatteryMonitor::new(
585            p.ADC1, p.ADC3, p.PA2, p.PA3, p.DMA2_CH0, p.DMA2_CH1,
586        );
587
588        // SPI1 (ROSflight uses for internal ICM, unused here)
589        let mut spi1_config: embassy_stm32::spi::Config = spi::Config::default();
590        spi1_config.frequency = mhz(16); // Phil recommends not running over 4 Mbps
591        spi1_config.mode = spi::MODE_3;
592        spi1_config.bit_order = spi::BitOrder::MsbFirst;
593        spi1_config.miso_pull = embassy_stm32::gpio::Pull::Up;
594        let spi1 = spi::Spi::new(
595            p.SPI1,
596            p.PA5,
597            p.PA7,
598            p.PA6,
599            p.DMA1_CH0,
600            p.DMA1_CH1,
601            BoardIrqs,
602            spi1_config,
603        );
604        let spi1_bus = Mutex::new(spi1);
605        let _spi1_bus = SPI1_BUS.init(spi1_bus);
606
607        // SPI2 (internal DPS310)
608        let mut spi2_config: embassy_stm32::spi::Config = spi::Config::default();
609        spi2_config.frequency = mhz(16); // Phil recommends not running over 4 Mbps
610        spi2_config.mode = spi::MODE_3;
611        spi2_config.bit_order = spi::BitOrder::MsbFirst;
612        spi2_config.miso_pull = embassy_stm32::gpio::Pull::Up;
613        let spi2 = spi::Spi::new(
614            p.SPI2,
615            p.PB10, // Sck
616            p.PB15, // Mosi
617            p.PB14, // Miso
618            p.DMA1_CH2,
619            p.DMA1_CH3,
620            BoardIrqs,
621            spi2_config,
622        );
623        let spi2_bus = Mutex::new(spi2);
624        let spi2_bus = SPI2_BUS.init(spi2_bus);
625
626        // DPS310 Baro (Internal)
627        let nss2 = Output::new(p.PD7, Level::High, Speed::Low);
628        // these pins are generalized for the IC
629        let drdy2 = ExtiInput::new(p.PD15, p.EXTI15, Pull::Down, BoardIrqs);
630        let dps_dev = SpiDevice::new(spi2_bus, nss2);
631        let dps_sensor = peripherals::dps310::Dps310Sensor {
632            dev: dps_dev,
633            drdy: drdy2,
634            three_wire: false,
635        };
636
637        // I2C1 Bus (ist8308 Mag and MS4525 Pitot on external)
638        let mut i2c_config = i2c::Config::default();
639        i2c_config.scl_pullup = true;
640        i2c_config.sda_pullup = true;
641        i2c_config.frequency = Hertz(100_000);
642        let i2c1 = i2c::I2c::new(
643            p.I2C1, p.PB8, p.PB9, p.DMA2_CH2, p.DMA2_CH3, BoardIrqs, i2c_config,
644        );
645        let i2c1_bus = Mutex::new(i2c1);
646        let i2c1_bus = I2C1_BUS.init(i2c1_bus);
647
648        // IST8308 Magnetometer (External)
649        let ist8303_sensor = peripherals::ist8308::Ist8308Sensor {
650            dev: I2cDevice::new(i2c1_bus),
651        };
652
653        // MS4525 Pitot (External)
654        let ms4525_sensor = peripherals::ms4525::Ms4525Sensor {
655            dev: I2cDevice::new(i2c1_bus),
656        };
657
658        let llv3hp_sensor = peripherals::llv3hp::Llv3hpSensor {
659            dev: I2cDevice::new(i2c1_bus),
660        };
661
662        // Companion Computer UART - Austin's documentation references uart3 instead of 2 for companion computer
663        let mut uart3config = usart::Config::default();
664        uart3config.rx_pull = Pull::Up;
665        uart3config.baudrate = 921600;
666        let uart3 = Uart::new(
667            p.USART3,
668            p.PD9,
669            p.PD8,
670            p.DMA2_CH4,
671            p.DMA2_CH5,
672            BoardIrqs,
673            uart3config,
674        )
675        .unwrap();
676        let (uart3_tx, uart3_rx) = uart3.split();
677
678        let telem3_rx = peripherals::telem::TelemRx {
679            uart_rx: uart3_rx,
680            byte_processor: stm_32::peripherals::telem::BasicProcessor {},
681        };
682
683        let telem3_tx = peripherals::telem::TelemTx { uart_tx: uart3_tx };
684
685        // VCP
686        static EP_BUF_CELL: StaticCell<[u8; 256]> = StaticCell::new();
687        let mut config = embassy_stm32::usb::Config::default();
688        config.vbus_detection = true;
689        let driver = Driver::new_fs(
690            p.USB_OTG_FS,
691            Irqs,
692            p.PA12,
693            p.PA11,
694            EP_BUF_CELL.init([0u8; 256]),
695            config,
696        );
697        let vcp = peripherals::vcp::Vcp {
698            driver,
699            byte_processor: stm_32::peripherals::vcp::BasicProcessor {},
700        };
701
702        // USART4 (external GPS)
703        let mut uart4config = usart::Config::default();
704        uart4config.baudrate = 9600u32;
705        uart4config.rx_pull = Pull::Up;
706        let uart4 = Uart::new(
707            p.UART4,
708            p.PA1,
709            p.PA0,
710            p.DMA2_CH6,
711            p.DMA2_CH7,
712            BoardIrqs,
713            uart4config,
714        )
715        .unwrap();
716
717        // UBlox NEO-M9N GNSS (External)
718        let ublox_sensor = peripherals::ublox::UbloxSensor {
719            uart: uart4,
720            protocol: peripherals::ublox::Protocol::M8,
721            baudrate: peripherals::ublox::Bitrate::Baud230400,
722            nav_period_ms: 100u16,
723        };
724        let drdy_pps = ExtiInput::new(p.PG12, p.EXTI12, Pull::Down, BoardIrqs);
725        let pps_sensor = peripherals::pps::PpsSensor { pps: drdy_pps };
726
727        // S.Bus USART6
728        // Sbus only uses Rx.
729        let mut uart6config = usart::Config::default();
730        uart6config.baudrate = 100000u32;
731        uart6config.parity = usart::Parity::ParityEven;
732        uart6config.stop_bits = usart::StopBits::STOP2;
733        uart6config.invert_rx = true;
734        uart6config.invert_tx = true;
735        uart6config.data_bits = usart::DataBits::DataBits8;
736
737        let usart6 = Uart::new(
738            p.USART6,
739            p.PC7,
740            p.PC6,
741            p.DMA1_CH4,
742            p.DMA1_CH5,
743            BoardIrqs,
744            uart6config,
745        )
746        .unwrap();
747        let (_uart6_tx, uart6_rx) = usart6.split();
748        let sbus_rx = peripherals::sbus::SbusRC { uart: uart6_rx };
749
750        // uSD SDMMC1
751        let sdmmc1 = sdmmc::Sdmmc::new_4bit(
752            p.SDMMC1,
753            BoardIrqs,
754            p.PC12,
755            p.PD2,
756            p.PC8,
757            p.PC9,
758            p.PC10,
759            p.PC11,
760            Default::default(),
761        );
762
763        // SPI5 (Internal BMI085)
764        let mut spi5_config: embassy_stm32::spi::Config = spi::Config::default();
765        spi5_config.frequency = mhz(2); // Phil recommends not running over 4 Mbps
766        spi5_config.mode = spi::MODE_3;
767        spi5_config.bit_order = spi::BitOrder::MsbFirst;
768        spi5_config.miso_pull = embassy_stm32::gpio::Pull::Up;
769        let spi5 = spi::Spi::new(
770            p.SPI5,
771            p.PF7,      // sck
772            p.PF9,      // mosi
773            p.PF8,      // miso
774            p.DMA1_CH6, // tx_dma
775            p.DMA1_CH7, // rx_dma
776            BoardIrqs,
777            spi5_config,
778        );
779        let spi5_ = Mutex::new(spi5);
780        let spi5_bus = SPI5_BUS.init(spi5_);
781
782        // BMI085 (Internal)
783        let nss_bmi08x_a = Output::new(p.PF6, Level::High, Speed::Low); // Accel
784        let drdy_bmi08x_a = ExtiInput::new(p.PF1, p.EXTI1, Pull::Down, BoardIrqs); // Accel
785        let nss_bmi08x_g = Output::new(p.PF10, Level::High, Speed::Low); // Gyro
786        let drdy_bmi08x_g = ExtiInput::new(p.PF3, p.EXTI3, Pull::Down, BoardIrqs); // Gyro
787        let bmi08x_dev_a = SpiDevice::new(spi5_bus, nss_bmi08x_a);
788        let bmi08x_dev_g = SpiDevice::new(spi5_bus, nss_bmi08x_g);
789        let jumper: Output<'static> = Output::new(p.PF2, Level::High, Speed::Low); // Bridge pin
790        let bmi08x_sensor = peripherals::bmi08x::Bmi08xSensor {
791            dev_a: bmi08x_dev_a,
792            dev_g: bmi08x_dev_g,
793            drdy_a: drdy_bmi08x_a,
794            drdy_g: drdy_bmi08x_g,
795            jumper: jumper,
796            range_a: peripherals::bmi08x::AccelRange::Bmi085(
797                peripherals::bmi08x::AccelRange085::Max16g,
798            ),
799            range_g: peripherals::bmi08x::GyroRange::Max500dps,
800            sample_rate: peripherals::bmi08x::SampleRate::Odr400Hz,
801            // The Pixracer Pro IMU samples already use the board's physical
802            // axis directions. Aircraft mounting is handled separately by
803            // IMU_ROLL, IMU_PITCH, and IMU_YAW.
804            board_axis_signs: [1.0, 1.0, 1.0],
805        };
806
807        // Detect GPIO input.
808        let usd_detect = embassy_stm32::gpio::Input::new(p.PG3, Pull::None); // PG3 is not connected
809        let usd_card = peripherals::sd_card::SdCard {
810            sdmmc: sdmmc1,
811            detect: usd_detect,
812        };
813
814        // P1 Priority Task for Rx Telemetry
815        interrupt::SAI1.set_priority(Priority::P0);
816        let spawner1 = P1_EXECUTOR.start(interrupt::SAI1);
817        spawn_task(&spawner1, peripherals::bmi08x::task(bmi08x_sensor));
818
819        // P2 Priority Task for Gyros
820        interrupt::SAI2.set_priority(Priority::P2);
821        let spawner2 = P2_EXECUTOR.start(interrupt::SAI2);
822
823        // P2 VCP Task (Telemetry alternate)
824        spawn_task(&spawner2, peripherals::vcp::task(vcp));
825        spawn_task(&spawner2, peripherals::telem::task_rx(telem3_rx));
826
827        // P3 Priority Task for Polled Peripherals
828        interrupt::SAI3.set_priority(Priority::P3);
829        let spawner3 = P3_EXECUTOR.start(interrupt::SAI3);
830        spawn_task(&spawner3, peripherals::ist8308::task(ist8303_sensor));
831        spawn_task(&spawner3, peripherals::ms4525::task(ms4525_sensor));
832        spawn_task(&spawner3, peripherals::dps310::task(dps_sensor));
833        spawn_task(&spawner3, peripherals::ublox::task(ublox_sensor));
834        spawn_task(&spawner3, peripherals::pps::task(pps_sensor));
835        spawn_task(&spawner3, peripherals::sbus::task(sbus_rx));
836        spawn_task(&spawner3, peripherals::llv3hp::task(llv3hp_sensor));
837        spawn_task(&spawner3, crate::battery::task(battery_monitor));
838
839        // P4 Priority for Tx Telemetry
840        interrupt::SAI4.set_priority(Priority::P4);
841        let spawner4 = P4_EXECUTOR.start(interrupt::SAI4);
842        spawn_task(&spawner4, peripherals::telem::task_tx(telem3_tx));
843        spawn_task(&spawner4, peripherals::sd_card::task(usd_card));
844
845        // Match the ROSflight C logical-to-physical PWM mapping from the
846        // Pixracer Pro BoardConfig.h: PWM1-4 use TIM1 CH4-CH1, PWM5-6 use
847        // TIM4 CH2-CH3, and PWM7-8 use TIM8 CH1-CH2.
848        let tim1_ch1_pin = PwmPin::<_, embassy_stm32::timer::Ch1>::new(p.PE9, OutputType::PushPull);
849        let tim1_ch2_pin =
850            PwmPin::<_, embassy_stm32::timer::Ch2>::new(p.PE11, OutputType::PushPull);
851        let tim1_ch3_pin =
852            PwmPin::<_, embassy_stm32::timer::Ch3>::new(p.PE13, OutputType::PushPull);
853        let tim1_ch4_pin =
854            PwmPin::<_, embassy_stm32::timer::Ch4>::new(p.PE14, OutputType::PushPull);
855        let tim4_ch2_pin =
856            PwmPin::<_, embassy_stm32::timer::Ch2>::new(p.PD13, OutputType::PushPull);
857        let tim4_ch3_pin =
858            PwmPin::<_, embassy_stm32::timer::Ch3>::new(p.PD14, OutputType::PushPull);
859        let tim8_ch1_pin = PwmPin::<_, embassy_stm32::timer::Ch1>::new(p.PI5, OutputType::PushPull);
860        let tim8_ch2_pin = PwmPin::<_, embassy_stm32::timer::Ch2>::new(p.PI6, OutputType::PushPull);
861
862        let timer1 = SimplePwm::new(
863            p.TIM1,
864            Some(tim1_ch1_pin),
865            Some(tim1_ch2_pin),
866            Some(tim1_ch3_pin),
867            Some(tim1_ch4_pin),
868            Hertz::hz(400),
869            Default::default(),
870        );
871        let timer4 = SimplePwm::new(
872            p.TIM4,
873            None,
874            Some(tim4_ch2_pin),
875            Some(tim4_ch3_pin),
876            None,
877            Hertz::hz(400),
878            Default::default(),
879        );
880        let timer8 = SimplePwm::new(
881            p.TIM8,
882            Some(tim8_ch1_pin),
883            Some(tim8_ch2_pin),
884            None,
885            None,
886            Hertz::hz(400),
887            Default::default(),
888        );
889
890        let timer1 = peripherals::pwm::TimerEnum::TIM1(timer1);
891        let timer4 = peripherals::pwm::TimerEnum::TIM4(timer4);
892        let timer8 = peripherals::pwm::TimerEnum::TIM8(timer8);
893
894        let timers: [peripherals::pwm::TimerEnum; 3] = [timer1, timer4, timer8];
895
896        let servos = peripherals::pwm::PixRacerProServoMonstrosity::with_timer_kinds_and_dma(
897            timers,
898            [
899                (0, peripherals::pwm::TimerChannel::Ch4), // PWM1: TIM1 CH4, PE14
900                (0, peripherals::pwm::TimerChannel::Ch3), // PWM2: TIM1 CH3, PE13
901                (0, peripherals::pwm::TimerChannel::Ch2), // PWM3: TIM1 CH2, PE11
902                (0, peripherals::pwm::TimerChannel::Ch1), // PWM4: TIM1 CH1, PE9
903                (1, peripherals::pwm::TimerChannel::Ch2), // PWM5: TIM4 CH2, PD13
904                (1, peripherals::pwm::TimerChannel::Ch3), // PWM6: TIM4 CH3, PD14
905                (2, peripherals::pwm::TimerChannel::Ch1), // PWM7: TIM8 CH1, PI5
906                (2, peripherals::pwm::TimerChannel::Ch2), // PWM8: TIM8 CH2, PI6
907            ],
908            [
909                peripherals::pwm::PwmTimerBlockKind::StandardOnly,
910                peripherals::pwm::PwmTimerBlockKind::StandardOnly,
911                peripherals::pwm::PwmTimerBlockKind::StandardOnly,
912            ],
913            [None, None, None],
914        );
915
916        // Test PWM pins
917        #[cfg_attr(feature = "scope-timing-pins", allow(unused_mut))]
918        let mut test_pin_1 = Output::new(p.PD11, Level::Low, Speed::VeryHigh);
919        #[cfg(not(feature = "scope-timing-pins"))]
920        test_pin_1.set_high();
921        #[cfg_attr(feature = "scope-timing-pins", allow(unused_mut))]
922        let mut test_pin_2 = Output::new(p.PD12, Level::Low, Speed::VeryHigh);
923        #[cfg(not(feature = "scope-timing-pins"))]
924        test_pin_2.set_high();
925
926        // Setup Probe GPIO's
927        let probe = [
928            Output::new(p.PG13, Level::Low, Speed::Low),
929            Output::new(p.PG9, Level::Low, Speed::Low),
930            Output::new(p.PG14, Level::Low, Speed::Low),
931            // Output::new(p.PG0, Level::Low, Speed::Low), // unknown
932        ];
933        (
934            Board {
935                _probe: probe,
936                start_time,
937                test_pin_1,
938                test_pin_2,
939                pending_reset_to_bootloader: None,
940                sd_request_id: 0,
941                #[cfg(feature = "sensor-poll-diagnostics")]
942                last_sbus_diag_ms: 0,
943                #[cfg(feature = "sensor-poll-diagnostics")]
944                sbus_rc_drains: 0,
945                #[cfg(feature = "sensor-poll-diagnostics")]
946                sbus_last_rc_status: 0,
947                #[cfg(feature = "sensor-poll-diagnostics")]
948                sbus_last_rc_lol: false,
949            },
950            servos,
951        )
952    }
953
954    #[cfg(feature = "sensor-poll-diagnostics")]
955    fn record_sbus_rc_drain(
956        &mut self,
957        rc: &Option<Result<veloxity_core::packets::RcPacket, errors::SensorError>>,
958    ) {
959        if let Some(result) = rc {
960            self.sbus_rc_drains = self.sbus_rc_drains.wrapping_add(1);
961            if let Ok(packet) = result {
962                self.sbus_last_rc_status = packet.header.status as u32;
963                self.sbus_last_rc_lol = packet.lol;
964            }
965        }
966    }
967
968    #[cfg(feature = "sensor-poll-diagnostics")]
969    fn log_sbus_diagnostics_if_due(&mut self) {
970        let now_ms = self.clock_millis();
971        if now_ms.wrapping_sub(self.last_sbus_diag_ms) < 1_000 {
972            return;
973        }
974        self.last_sbus_diag_ms = now_ms;
975
976        let sbus = peripherals::sbus::diagnostics();
977        veloxity_core::log_info!(
978            "SBUS rx{} e{} sz{} n{} v{}",
979            sbus.read_ok,
980            sbus.read_err,
981            sbus.last_read_size,
982            sbus.size_25,
983            sbus.valid_frame
984        );
985        veloxity_core::log_info!(
986            "SBUS bh{} bf{} sig{} to{} dr{}",
987            sbus.bad_header,
988            sbus.bad_footer,
989            sbus.signal,
990            sbus.timeout,
991            self.sbus_rc_drains
992        );
993        veloxity_core::log_info!(
994            "SBUS st{} rst{} lol{}",
995            sbus.last_status,
996            self.sbus_last_rc_status,
997            self.sbus_last_rc_lol as u8
998        );
999    }
1000}