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,258 @@
|
||||
//! DShot framing, timing, and arming policy.
|
||||
|
||||
use crate::ArmingDuration;
|
||||
|
||||
pub const MIN_THROTTLE: u16 = 48;
|
||||
pub const MOTOR_STOP: u16 = 0;
|
||||
pub const FRAME_SYMBOLS: usize = 18;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct Config {
|
||||
rate: Rate,
|
||||
armed_idle_throttle: Throttle,
|
||||
arming_duration: ArmingDuration,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub const fn new(
|
||||
rate: Rate,
|
||||
armed_idle_throttle: Throttle,
|
||||
arming_duration: ArmingDuration,
|
||||
) -> Option<Self> {
|
||||
if rate.frames_for_millis(arming_duration.milliseconds()) == 0 {
|
||||
return None;
|
||||
}
|
||||
Some(Self {
|
||||
rate,
|
||||
armed_idle_throttle,
|
||||
arming_duration,
|
||||
})
|
||||
}
|
||||
|
||||
pub const fn rate(self) -> Rate {
|
||||
self.rate
|
||||
}
|
||||
|
||||
pub const fn armed_idle_throttle(self) -> Throttle {
|
||||
self.armed_idle_throttle
|
||||
}
|
||||
|
||||
pub const fn arming_duration(self) -> ArmingDuration {
|
||||
self.arming_duration
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum Rate {
|
||||
Dshot150,
|
||||
Dshot300,
|
||||
Dshot600,
|
||||
}
|
||||
|
||||
impl Rate {
|
||||
pub const fn hertz(self) -> u32 {
|
||||
match self {
|
||||
Self::Dshot150 => 150_000,
|
||||
Self::Dshot300 => 300_000,
|
||||
Self::Dshot600 => 600_000,
|
||||
}
|
||||
}
|
||||
|
||||
/// Twenty timer ticks per symbol produce exact 7/14 compare values.
|
||||
pub const fn timer_base_hz(self) -> u32 {
|
||||
self.hertz() * 20
|
||||
}
|
||||
|
||||
pub const fn output_rate_hz(self) -> u32 {
|
||||
match self {
|
||||
Self::Dshot150 => 4_000,
|
||||
Self::Dshot300 | Self::Dshot600 => 8_000,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn frames_for_millis(self, milliseconds: u16) -> u16 {
|
||||
((self.output_rate_hz() * milliseconds as u32) / 1_000) as u16
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct Timing {
|
||||
bit_ticks: u16,
|
||||
zero_high_ticks: u16,
|
||||
one_high_ticks: u16,
|
||||
}
|
||||
|
||||
impl Timing {
|
||||
pub const fn from_timer_clock(timer_hz: u32, rate: Rate) -> Option<Self> {
|
||||
let symbol_hz = rate.hertz();
|
||||
let ticks = (timer_hz + symbol_hz / 2) / symbol_hz;
|
||||
if ticks < 3 || ticks > u16::MAX as u32 {
|
||||
return None;
|
||||
}
|
||||
let bit_ticks = ticks as u16;
|
||||
let zero_high_ticks = ((ticks * 7 + 10) / 20) as u16;
|
||||
let one_high_ticks = ((ticks * 14 + 10) / 20) as u16;
|
||||
if zero_high_ticks == 0 || one_high_ticks >= bit_ticks {
|
||||
return None;
|
||||
}
|
||||
Some(Self {
|
||||
bit_ticks,
|
||||
zero_high_ticks,
|
||||
one_high_ticks,
|
||||
})
|
||||
}
|
||||
|
||||
pub const fn bit_ticks(self) -> u16 {
|
||||
self.bit_ticks
|
||||
}
|
||||
pub const fn zero_high_ticks(self) -> u16 {
|
||||
self.zero_high_ticks
|
||||
}
|
||||
pub const fn one_high_ticks(self) -> u16 {
|
||||
self.one_high_ticks
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct Throttle(u16);
|
||||
|
||||
impl Throttle {
|
||||
pub const fn new(value: u16) -> Option<Self> {
|
||||
if value >= MIN_THROTTLE && value <= 2_047 {
|
||||
Some(Self(value))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn get(self) -> u16 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum FrameValue {
|
||||
Stop,
|
||||
Throttle(Throttle),
|
||||
}
|
||||
|
||||
impl FrameValue {
|
||||
pub const fn packet(self) -> u16 {
|
||||
packet(match self {
|
||||
Self::Stop => MOTOR_STOP,
|
||||
Self::Throttle(throttle) => throttle.get(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ArmSequence {
|
||||
armed_throttle: Throttle,
|
||||
zero_frames: u16,
|
||||
remaining: u16,
|
||||
armed: bool,
|
||||
}
|
||||
|
||||
impl ArmSequence {
|
||||
pub const fn new(armed_throttle: Throttle, zero_frames: u16) -> Self {
|
||||
Self {
|
||||
armed_throttle,
|
||||
zero_frames,
|
||||
remaining: 0,
|
||||
armed: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Starts the fixed configured stop-frame sequence. The driver which owns
|
||||
/// this state must acquire external safety authorization before calling
|
||||
/// this method and before every subsequent nonzero output commit.
|
||||
pub fn arm(&mut self) {
|
||||
self.remaining = self.zero_frames;
|
||||
self.armed = true;
|
||||
}
|
||||
|
||||
/// Stops the sequence; the physical output owner must emit an explicit
|
||||
/// stop frame independently of this local state transition.
|
||||
pub fn disarm(&mut self) {
|
||||
self.armed = false;
|
||||
self.remaining = 0;
|
||||
}
|
||||
|
||||
pub const fn is_armed(&self) -> bool {
|
||||
self.armed
|
||||
}
|
||||
|
||||
pub fn next_frame(&mut self) -> Option<FrameValue> {
|
||||
if !self.is_armed() {
|
||||
return None;
|
||||
}
|
||||
if self.remaining != 0 {
|
||||
self.remaining -= 1;
|
||||
return Some(FrameValue::Stop);
|
||||
}
|
||||
Some(FrameValue::Throttle(self.armed_throttle))
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn packet(throttle: u16) -> u16 {
|
||||
let value = throttle << 1;
|
||||
(value << 4) | ((value ^ (value >> 4) ^ (value >> 8)) & 0x0f)
|
||||
}
|
||||
|
||||
pub const fn symbols(packet: u16, zero_high: u16, one_high: u16) -> [u16; FRAME_SYMBOLS] {
|
||||
let mut symbols = [0; FRAME_SYMBOLS];
|
||||
let mut bit = 0;
|
||||
while bit < 16 {
|
||||
symbols[bit] = if packet & (1 << (15 - bit)) != 0 {
|
||||
one_high
|
||||
} else {
|
||||
zero_high
|
||||
};
|
||||
bit += 1;
|
||||
}
|
||||
symbols
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{ArmSequence, Config, FrameValue, Rate, Throttle, Timing, packet, symbols};
|
||||
use crate::ArmingDuration;
|
||||
|
||||
#[test]
|
||||
fn packet_matches_standard_dshot_vectors() {
|
||||
assert_eq!(packet(0), 0x0000);
|
||||
assert_eq!(packet(48), 0x0606);
|
||||
assert_eq!(packet(158), 0x13ce);
|
||||
assert_eq!(packet(300), 0x258f);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arm_sequence_holds_stop_for_the_configured_interval() {
|
||||
let throttle = Throttle::new(300).unwrap();
|
||||
let mut sequence = ArmSequence::new(throttle, 2);
|
||||
sequence.arm();
|
||||
assert_eq!(sequence.next_frame(), Some(FrameValue::Stop));
|
||||
assert_eq!(sequence.next_frame(), Some(FrameValue::Stop));
|
||||
assert_eq!(sequence.next_frame(), Some(FrameValue::Throttle(throttle)));
|
||||
sequence.disarm();
|
||||
assert_eq!(sequence.next_frame(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn symbols_end_with_the_required_low_reset_gap() {
|
||||
assert_eq!(symbols(packet(48), 7, 14)[16..], [0, 0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn standard_rates_have_timer_profiles() {
|
||||
for rate in [Rate::Dshot150, Rate::Dshot300, Rate::Dshot600] {
|
||||
assert!(Timing::from_timer_clock(rate.timer_base_hz(), rate).is_some());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn requires_an_arming_interval_with_frames() {
|
||||
let duration = ArmingDuration::from_millis(2_000).unwrap();
|
||||
let throttle = Throttle::new(300).unwrap();
|
||||
assert!(Config::new(Rate::Dshot600, throttle, duration).is_some());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
//! Protocol-neutral physical-motor protocol reference.
|
||||
//!
|
||||
//! This crate deliberately remains in the workspace for future, separately
|
||||
//! reviewed physical-flight work. No checked-in board platform links it, and
|
||||
//! its types model DShot/PWM framing and
|
||||
//! local arming sequences only; they grant neither peripheral access nor
|
||||
//! flight authority.
|
||||
|
||||
#![no_std]
|
||||
|
||||
pub mod dshot;
|
||||
pub mod pwm;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct ArmingDuration(u16);
|
||||
|
||||
impl ArmingDuration {
|
||||
pub const fn from_millis(milliseconds: u16) -> Option<Self> {
|
||||
if milliseconds != 0 && milliseconds <= 8_000 {
|
||||
Some(Self(milliseconds))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn milliseconds(self) -> u16 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum MotorProtocol {
|
||||
Pwm(pwm::Config),
|
||||
Dshot(dshot::Config),
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
//! Conventional PWM framing and arming policy.
|
||||
|
||||
use crate::ArmingDuration;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct Config {
|
||||
frame_rate: Rate,
|
||||
minimum_pulse: PulseWidth,
|
||||
armed_idle_pulse: PulseWidth,
|
||||
arming_duration: ArmingDuration,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub const fn new(
|
||||
frame_rate: Rate,
|
||||
minimum_pulse: PulseWidth,
|
||||
armed_idle_pulse: PulseWidth,
|
||||
arming_duration: ArmingDuration,
|
||||
) -> Option<Self> {
|
||||
match frame_rate.period_microseconds() {
|
||||
Some(period)
|
||||
if minimum_pulse.microseconds() <= armed_idle_pulse.microseconds()
|
||||
&& armed_idle_pulse.microseconds() <= period =>
|
||||
{
|
||||
Some(Self {
|
||||
frame_rate,
|
||||
minimum_pulse,
|
||||
armed_idle_pulse,
|
||||
arming_duration,
|
||||
})
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn frame_rate(self) -> Rate {
|
||||
self.frame_rate
|
||||
}
|
||||
|
||||
pub const fn minimum_pulse(self) -> PulseWidth {
|
||||
self.minimum_pulse
|
||||
}
|
||||
|
||||
pub const fn armed_idle_pulse(self) -> PulseWidth {
|
||||
self.armed_idle_pulse
|
||||
}
|
||||
|
||||
pub const fn arming_duration(self) -> ArmingDuration {
|
||||
self.arming_duration
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct Rate(u16);
|
||||
|
||||
impl Rate {
|
||||
pub const fn new(hertz: u16) -> Option<Self> {
|
||||
if hertz != 0 && hertz <= 500 {
|
||||
Some(Self(hertz))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn hertz(self) -> u16 {
|
||||
self.0
|
||||
}
|
||||
|
||||
pub const fn period_microseconds(self) -> Option<u16> {
|
||||
if 1_000_000 % self.0 as u32 == 0 {
|
||||
Some((1_000_000 / self.0 as u32) as u16)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn frames_for_millis(self, milliseconds: u16) -> u16 {
|
||||
((self.0 as u32 * milliseconds as u32) / 1_000) as u16
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct PulseWidth(u16);
|
||||
|
||||
impl PulseWidth {
|
||||
pub const fn new(microseconds: u16) -> Option<Self> {
|
||||
if microseconds >= 1_000 && microseconds <= 2_000 {
|
||||
Some(Self(microseconds))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn microseconds(self) -> u16 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum FrameValue {
|
||||
Minimum,
|
||||
ArmedIdle(PulseWidth),
|
||||
}
|
||||
|
||||
pub struct ArmSequence {
|
||||
armed_idle: PulseWidth,
|
||||
minimum_frames: u16,
|
||||
remaining: u16,
|
||||
armed: bool,
|
||||
}
|
||||
|
||||
impl ArmSequence {
|
||||
pub const fn new(armed_idle: PulseWidth, minimum_frames: u16) -> Self {
|
||||
Self {
|
||||
armed_idle,
|
||||
minimum_frames,
|
||||
remaining: 0,
|
||||
armed: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Starts the fixed configured minimum-frame sequence. The driver which
|
||||
/// owns this state must acquire external safety authorization before
|
||||
/// calling this method and before every subsequent non-minimum commit.
|
||||
pub fn arm(&mut self) {
|
||||
self.remaining = self.minimum_frames;
|
||||
self.armed = true;
|
||||
}
|
||||
|
||||
/// Stops the sequence; the physical output owner must drive its explicit
|
||||
/// minimum pulse independently of this local state transition.
|
||||
pub fn disarm(&mut self) {
|
||||
self.armed = false;
|
||||
self.remaining = 0;
|
||||
}
|
||||
|
||||
pub const fn is_armed(&self) -> bool {
|
||||
self.armed
|
||||
}
|
||||
|
||||
pub fn next_frame(&mut self) -> Option<FrameValue> {
|
||||
if !self.is_armed() {
|
||||
return None;
|
||||
}
|
||||
if self.remaining != 0 {
|
||||
self.remaining -= 1;
|
||||
return Some(FrameValue::Minimum);
|
||||
}
|
||||
Some(FrameValue::ArmedIdle(self.armed_idle))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{ArmSequence, Config, FrameValue, PulseWidth, Rate};
|
||||
use crate::ArmingDuration;
|
||||
|
||||
#[test]
|
||||
fn rate_describes_an_exact_400_hertz_frame() {
|
||||
let rate = Rate::new(400).unwrap();
|
||||
assert_eq!(rate.period_microseconds(), Some(2_500));
|
||||
assert_eq!(rate.frames_for_millis(2_000), 800);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sequence_holds_minimum_before_armed_idle() {
|
||||
let pulse = PulseWidth::new(1_100).unwrap();
|
||||
let mut sequence = ArmSequence::new(pulse, 2);
|
||||
sequence.arm();
|
||||
assert_eq!(sequence.next_frame(), Some(FrameValue::Minimum));
|
||||
assert_eq!(sequence.next_frame(), Some(FrameValue::Minimum));
|
||||
assert_eq!(sequence.next_frame(), Some(FrameValue::ArmedIdle(pulse)));
|
||||
sequence.disarm();
|
||||
assert_eq!(sequence.next_frame(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn requires_an_exact_frame_and_ordered_pulses() {
|
||||
let duration = ArmingDuration::from_millis(2_000).unwrap();
|
||||
let rate = Rate::new(400).unwrap();
|
||||
let minimum = PulseWidth::new(1_000).unwrap();
|
||||
let idle = PulseWidth::new(1_100).unwrap();
|
||||
assert!(Config::new(rate, minimum, idle, duration).is_some());
|
||||
assert!(Config::new(Rate::new(333).unwrap(), minimum, idle, duration).is_none());
|
||||
assert!(Config::new(rate, idle, minimum, duration).is_none());
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user