diff --git a/esp-hal-common/Cargo.toml b/esp-hal-common/Cargo.toml index 4c275c22c7d..91968564e47 100644 --- a/esp-hal-common/Cargo.toml +++ b/esp-hal-common/Cargo.toml @@ -54,7 +54,7 @@ esp32 = { version = "0.16.0", features = ["critical-section"], optional = true esp32c2 = { version = "0.5.1", features = ["critical-section"], optional = true } esp32c3 = { version = "0.8.1", features = ["critical-section"], optional = true } esp32s2 = { version = "0.6.0", features = ["critical-section"], optional = true } -esp32s3 = { version = "0.9.0", features = ["critical-section"], optional = true } +esp32s3 = { version = "0.10.0", features = ["critical-section"], optional = true } [features] esp32 = ["esp32/rt" , "procmacros/xtensa", "xtensa-lx-rt/esp32", "xtensa-lx/esp32", "critical-section/restore-state-u32", "lock_api"] diff --git a/esp-hal-common/src/clock.rs b/esp-hal-common/src/clock.rs index 9031e092fd6..6049990ffed 100644 --- a/esp-hal-common/src/clock.rs +++ b/esp-hal-common/src/clock.rs @@ -113,6 +113,10 @@ pub struct Clocks { pub apb_clock: HertzU32, pub xtal_clock: HertzU32, pub i2c_clock: HertzU32, + #[cfg(esp32)] + pub pwm_clock: HertzU32, + #[cfg(esp32s3)] + pub crypto_pwm_clock: HertzU32, // TODO chip specific additional ones as needed } @@ -129,6 +133,10 @@ impl Clocks { apb_clock: raw_clocks.apb_clock, xtal_clock: raw_clocks.xtal_clock, i2c_clock: raw_clocks.i2c_clock, + #[cfg(esp32)] + pwm_clock: raw_clocks.pwm_clock, + #[cfg(esp32s3)] + crypto_pwm_clock: raw_clocks.crypto_pwm_clock, } } } @@ -139,6 +147,10 @@ pub struct RawClocks { pub apb_clock: HertzU32, pub xtal_clock: HertzU32, pub i2c_clock: HertzU32, + #[cfg(esp32)] + pub pwm_clock: HertzU32, + #[cfg(esp32s3)] + pub crypto_pwm_clock: HertzU32, // TODO chip specific additional ones as needed } @@ -172,6 +184,7 @@ impl ClockControl { apb_clock: HertzU32::MHz(80), xtal_clock: HertzU32::MHz(40), i2c_clock: HertzU32::MHz(80), + pwm_clock: HertzU32::MHz(160), }, } } @@ -200,6 +213,10 @@ impl ClockControl { apb_clock: HertzU32::MHz(80), xtal_clock: HertzU32::MHz(40), i2c_clock: HertzU32::MHz(40), + // The docs are unclear here. pwm_clock seems to be tied to clocks.apb_clock + // while simultaneously being fixed at 160 MHz. + // Testing showed 160 MHz to be correct for current clock configurations. + pwm_clock: HertzU32::MHz(160), }, } } @@ -344,6 +361,7 @@ impl ClockControl { apb_clock: HertzU32::MHz(80), xtal_clock: HertzU32::MHz(40), i2c_clock: HertzU32::MHz(40), + crypto_pwm_clock: HertzU32::MHz(160), }, } } @@ -360,6 +378,7 @@ impl ClockControl { apb_clock: HertzU32::MHz(80), xtal_clock: HertzU32::MHz(40), i2c_clock: HertzU32::MHz(40), + crypto_pwm_clock: HertzU32::MHz(160), }, } } diff --git a/esp-hal-common/src/lib.rs b/esp-hal-common/src/lib.rs index aedc9493879..40354fabc16 100644 --- a/esp-hal-common/src/lib.rs +++ b/esp-hal-common/src/lib.rs @@ -62,6 +62,8 @@ pub mod i2c; pub mod i2s; pub mod ledc; +#[cfg(any(esp32, esp32s3))] +pub mod mcpwm; #[cfg(usb_otg)] pub mod otg_fs; pub mod prelude; diff --git a/esp-hal-common/src/mcpwm/mod.rs b/esp-hal-common/src/mcpwm/mod.rs new file mode 100644 index 00000000000..03f379f4229 --- /dev/null +++ b/esp-hal-common/src/mcpwm/mod.rs @@ -0,0 +1,288 @@ +//! MCPWM (Motor Control Pulse Width Modulator) peripheral +//! +//! # Peripheral capabilities: +//! * PWM Timers 0, 1 and 2 +//! * Every PWM timer has a dedicated 8-bit clock prescaler. +//! * The 16-bit counter in the PWM timer can work in count-up mode, +//! count-down mode or count-up-down mode. +//! * A hardware sync or software sync can trigger a reload on the PWM timer +//! with a phase register (Not yet implemented) +//! * PWM Operators 0, 1 and 2 +//! * Every PWM operator has two PWM outputs: PWMxA and PWMxB. They can work +//! independently, in symmetric and asymmetric configuration. +//! * Software, asynchronously override control of PWM signals. +//! * Configurable dead-time on rising and falling edges; each set up +//! independently. (Not yet implemented) +//! * All events can trigger CPU interrupts. (Not yet implemented) +//! * Modulating of PWM output by high-frequency carrier signals, useful +//! when gate drivers are insulated with a transformer. (Not yet +//! implemented) +//! * Period, time stamps and important control registers have shadow +//! registers with flexible updating methods. +//! * Fault Detection Module (Not yet implemented) +//! * Capture Module (Not yet implemented) +//! +//! # Example +//! Uses timer0 and operator0 of the MCPWM0 peripheral to output a 50% duty +//! signal at 20 kHz. The signal will be output to the pin assigned to `pin`. +//! +//! ``` +//! # use esp_hal_common::{mcpwm, prelude::*}; +//! use mcpwm::{operator::PwmPinConfig, timer::PwmWorkingMode, PeripheralClockConfig, MCPWM}; +//! +//! // initialize peripheral +//! let clock_cfg = PeripheralClockConfig::with_frequency(&clocks, 40u32.MHz()).unwrap(); +//! let mut mcpwm = MCPWM::new( +//! peripherals.PWM0, +//! clock_cfg, +//! &mut system.peripheral_clock_control, +//! ); +//! +//! // connect operator0 to timer0 +//! mcpwm.operator0.set_timer(&mcpwm.timer0); +//! // connect operator0 to pin +//! let mut pwm_pin = mcpwm +//! .operator0 +//! .with_pin_a(pin, PwmPinConfig::UP_ACTIVE_HIGH); +//! +//! // start timer with timestamp values in the range of 0..=99 and a frequency of 20 kHz +//! let timer_clock_cfg = clock_cfg +//! .timer_clock_with_frequency(99, PwmWorkingMode::Increase, 20u32.kHz()) +//! .unwrap(); +//! mcpwm.timer0.start(timer_clock_cfg); +//! +//! // pin will be high 50% of the time +//! pwm_pin.set_timestamp(50); +//! ``` + +#![deny(missing_docs)] + +use core::{marker::PhantomData, ops::Deref}; + +use fugit::HertzU32; +use operator::Operator; +use timer::Timer; + +use crate::{ + clock::Clocks, + system::{Peripheral, PeripheralClockControl}, + types::OutputSignal, +}; + +/// MCPWM operators +pub mod operator; +/// MCPWM timers +pub mod timer; + +/// The MCPWM peripheral +#[non_exhaustive] +pub struct MCPWM { + /// Timer0 + pub timer0: Timer<0, PWM>, + /// Timer1 + pub timer1: Timer<1, PWM>, + /// Timer2 + pub timer2: Timer<2, PWM>, + /// Operator0 + pub operator0: Operator<0, PWM>, + /// Operator1 + pub operator1: Operator<1, PWM>, + /// Operator2 + pub operator2: Operator<2, PWM>, +} + +impl MCPWM { + /// `pwm_clk = clocks.crypto_pwm_clock / (prescaler + 1)` + // clocks.crypto_pwm_clock normally is 160 MHz + pub fn new( + peripheral: PWM, + peripheral_clock: PeripheralClockConfig, + system: &mut PeripheralClockControl, + ) -> Self { + let _ = peripheral; + + PWM::enable(system); + + // set prescaler + peripheral + .clk_cfg + .write(|w| w.clk_prescale().variant(peripheral_clock.prescaler)); + // enable clock + peripheral.clk.write(|w| w.en().set_bit()); + + MCPWM { + timer0: Timer::new(), + timer1: Timer::new(), + timer2: Timer::new(), + operator0: Operator::new(), + operator1: Operator::new(), + operator2: Operator::new(), + } + } +} + +/// Clock configuration of the MCPWM peripheral +#[derive(Copy, Clone)] +pub struct PeripheralClockConfig<'a> { + frequency: HertzU32, + prescaler: u8, + phantom: PhantomData<&'a Clocks>, +} + +impl<'a> PeripheralClockConfig<'a> { + /// Get a clock configuration with the given prescaler. + /// + /// With standard system clock configurations the input clock to the MCPWM + /// peripheral is `160 MHz`. + /// + /// The peripheral clock frequency is calculated as: + /// `peripheral_clock = input_clock / (prescaler + 1)` + pub fn with_prescaler(clocks: &'a Clocks, prescaler: u8) -> Self { + #[cfg(esp32)] + let source_clock = clocks.pwm_clock; + #[cfg(esp32s3)] + let source_clock = clocks.crypto_pwm_clock; + + PeripheralClockConfig { + frequency: source_clock / (prescaler as u32 + 1), + prescaler, + phantom: PhantomData, + } + } + + /// Get a clock configuration with the given frequency. + /// + /// ### Note: + /// This will try to select an appropriate prescaler for the + /// [`PeripheralClockConfig::with_prescaler`] method. + /// If the calculated prescaler is not in the range `0..u8::MAX` + /// [`FrequencyError`] will be returned. + /// + /// With standard system clock configurations the input clock to the MCPWM + /// peripheral is `160 MHz`. + /// + /// Only divisors of the input clock (`160 Mhz / 1`, `160 Mhz / 2`, ..., + /// `160 Mhz / 256`) are representable exactly. Other target frequencies + /// will be rounded up to the next divisor. + pub fn with_frequency( + clocks: &'a Clocks, + target_freq: HertzU32, + ) -> Result { + #[cfg(esp32)] + let source_clock = clocks.pwm_clock; + #[cfg(esp32s3)] + let source_clock = clocks.crypto_pwm_clock; + + if target_freq.raw() == 0 || target_freq > source_clock { + return Err(FrequencyError); + } + let prescaler = source_clock / target_freq - 1; + if prescaler > u8::MAX as u32 { + return Err(FrequencyError); + } + Ok(Self::with_prescaler(clocks, prescaler as u8)) + } + + /// Get the peripheral clock frequency. + /// + /// ### Note: + /// The actual value is rounded down to the nearest `u32` value + pub fn frequency(&self) -> HertzU32 { + self.frequency + } + + /// Get a timer clock configuration with the given prescaler. + /// + /// The resulting timer frequency depends of the chosen + /// [`timer::PwmWorkingMode`]. + /// + /// #### `PwmWorkingMode::Increase` or `PwmWorkingMode::Decrease` + /// `timer_frequency = peripheral_clock / (prescaler + 1) / (period + 1)` + /// #### `PwmWorkingMode::UpDown` + /// `timer_frequency = peripheral_clock / (prescaler + 1) / (2 * period)` + pub fn timer_clock_with_prescaler( + &self, + period: u16, + mode: timer::PwmWorkingMode, + prescaler: u8, + ) -> timer::TimerClockConfig<'a> { + timer::TimerClockConfig::with_prescaler(self, period, mode, prescaler) + } + + /// Get a timer clock configuration with the given frequency. + /// + /// ### Note: + /// This will try to select an appropriate prescaler for the timer. + /// If the calculated prescaler is not in the range `0..u8::MAX` + /// [`FrequencyError`] will be returned. + /// + /// See [`PeripheralClockConfig::timer_clock_with_prescaler`] for how the + /// frequency is calculated. + pub fn timer_clock_with_frequency( + &self, + period: u16, + mode: timer::PwmWorkingMode, + target_freq: HertzU32, + ) -> Result, FrequencyError> { + timer::TimerClockConfig::with_frequency(self, period, mode, target_freq) + } +} + +/// Target frequency could not be set. +/// Check how the frequency is calculated in the corresponding method docs. +#[derive(Copy, Clone, Debug)] +pub struct FrequencyError; + +/// A MCPWM peripheral +pub unsafe trait PwmPeripheral: Deref { + /// Enable peripheral + fn enable(system: &mut PeripheralClockControl); + /// Get a pointer to the peripheral RegisterBlock + fn block() -> *const crate::pac::pwm0::RegisterBlock; + /// Get operator GPIO mux output signal + fn output_signal() -> OutputSignal; +} + +unsafe impl PwmPeripheral for crate::pac::PWM0 { + fn enable(system: &mut PeripheralClockControl) { + system.enable(Peripheral::Mcpwm0) + } + + fn block() -> *const crate::pac::pwm0::RegisterBlock { + Self::ptr() + } + + fn output_signal() -> OutputSignal { + match (OP, IS_A) { + (0, true) => OutputSignal::PWM0_0A, + (1, true) => OutputSignal::PWM0_1A, + (2, true) => OutputSignal::PWM0_1A, + (0, false) => OutputSignal::PWM0_0B, + (1, false) => OutputSignal::PWM0_1B, + (2, false) => OutputSignal::PWM0_1B, + _ => unreachable!(), + } + } +} + +unsafe impl PwmPeripheral for crate::pac::PWM1 { + fn enable(system: &mut PeripheralClockControl) { + system.enable(Peripheral::Mcpwm1) + } + + fn block() -> *const crate::pac::pwm0::RegisterBlock { + Self::ptr() + } + + fn output_signal() -> OutputSignal { + match (OP, IS_A) { + (0, true) => OutputSignal::PWM1_0A, + (1, true) => OutputSignal::PWM1_1A, + (2, true) => OutputSignal::PWM1_1A, + (0, false) => OutputSignal::PWM1_0B, + (1, false) => OutputSignal::PWM1_1B, + (2, false) => OutputSignal::PWM1_1B, + _ => unreachable!(), + } + } +} diff --git a/esp-hal-common/src/mcpwm/operator.rs b/esp-hal-common/src/mcpwm/operator.rs new file mode 100644 index 00000000000..6ac5bcdd8fe --- /dev/null +++ b/esp-hal-common/src/mcpwm/operator.rs @@ -0,0 +1,380 @@ +use core::marker::PhantomData; + +use crate::{ + mcpwm::{timer::Timer, PwmPeripheral}, + OutputPin, +}; + +/// A MCPWM operator +/// +/// The PWM Operator submodule has the following functions: +/// * Generates a PWM signal pair, based on timing references obtained from the +/// corresponding PWM timer. +/// * Each signal out of the PWM signal pair includes a specific pattern of dead +/// time. (Not yet implemented) +/// * Superimposes a carrier on the PWM signal, if configured to do so. (Not yet +/// implemented) +/// * Handles response under fault conditions. (Not yet implemented) +pub struct Operator { + phantom: PhantomData, +} + +impl Operator { + pub(super) fn new() -> Self { + // Side note: + // It would have been nice to deselect any timer reference on peripheral + // initialization. + // However experimentation (ESP32-S3) showed that writing `3` to timersel + // will not disable the timer reference but instead act as though `2` was + // written. + Operator { + phantom: PhantomData, + } + } + + /// Select a [`Timer`] to be the timing reference for this operator + /// + /// ### Note: + /// By default TIMER0 is used + pub fn set_timer(&mut self, timer: &Timer) { + let _ = timer; + // SAFETY: + // We only write to our OPERATORx_TIMERSEL register + let block = unsafe { &*PWM::block() }; + block.operator_timersel.modify(|_, w| match OP { + 0 => w.operator0_timersel().variant(TIM), + 1 => w.operator1_timersel().variant(TIM), + 2 => w.operator2_timersel().variant(TIM), + _ => { + unreachable!() + } + }); + } + + /// Use the A output with the given pin and configuration + pub fn with_pin_a( + self, + pin: Pin, + config: PwmPinConfig, + ) -> PwmPin { + PwmPin::new(pin, config) + } + + /// Use the B output with the given pin and configuration + pub fn with_pin_b( + self, + pin: Pin, + config: PwmPinConfig, + ) -> PwmPin { + PwmPin::new(pin, config) + } + + /// Use both the A and the B output with the given pins and configurations + pub fn with_pins( + self, + pin_a: PinA, + config_a: PwmPinConfig, + pin_b: PinB, + config_b: PwmPinConfig, + ) -> (PwmPin, PwmPin) { + (PwmPin::new(pin_a, config_a), PwmPin::new(pin_b, config_b)) + } +} + +/// Configuration describing how the operator generates a signal on a connected +/// pin +pub struct PwmPinConfig { + actions: PwmActions, + update_method: PwmUpdateMethod, +} + +impl PwmPinConfig { + /// A configuration using [`PwmActions::UP_ACTIVE_HIGH`] and + /// [`PwmUpdateMethod::SYNC_ON_ZERO`] + pub const UP_ACTIVE_HIGH: Self = + Self::new(PwmActions::UP_ACTIVE_HIGH, PwmUpdateMethod::SYNC_ON_ZERO); + /// A configuration using [`PwmActions::UP_DOWN_ACTIVE_HIGH`] and + /// [`PwmUpdateMethod::SYNC_ON_ZERO`] + pub const UP_DOWN_ACTIVE_HIGH: Self = Self::new( + PwmActions::UP_DOWN_ACTIVE_HIGH, + PwmUpdateMethod::SYNC_ON_ZERO, + ); + + /// Get a configuration using the given `PwmActions` and `PwmUpdateMethod` + pub const fn new(actions: PwmActions, update_method: PwmUpdateMethod) -> Self { + PwmPinConfig { + actions, + update_method, + } + } +} + +/// A pin driven by an MCPWM operator +pub struct PwmPin { + _pin: Pin, + phantom: PhantomData, +} + +impl + PwmPin +{ + fn new(mut pin: Pin, config: PwmPinConfig) -> Self { + let output_signal = PWM::output_signal::(); + pin.enable_output(true) + .connect_peripheral_to_output(output_signal); + let mut pin = PwmPin { + _pin: pin, + phantom: PhantomData, + }; + pin.set_actions(config.actions); + pin.set_update_method(config.update_method); + pin + } + + /// Configure what actions should be taken on timing events + pub fn set_actions(&mut self, value: PwmActions) { + // SAFETY: + // We only write to our GENx_x register + let block = unsafe { &*PWM::block() }; + + let bits = value.0; + + // SAFETY: + // `bits` is a valid bit pattern + unsafe { + match (OP, IS_A) { + (0, true) => block.gen0_a.write(|w| w.bits(bits)), + (1, true) => block.gen1_a.write(|w| w.bits(bits)), + (2, true) => block.gen2_a.write(|w| w.bits(bits)), + (0, false) => block.gen0_b.write(|w| w.bits(bits)), + (1, false) => block.gen1_b.write(|w| w.bits(bits)), + (2, false) => block.gen2_b.write(|w| w.bits(bits)), + _ => unreachable!(), + } + } + } + + /// Set how a new timestamp syncs with the timer + #[cfg(esp32)] + pub fn set_update_method(&mut self, update_method: PwmUpdateMethod) { + // SAFETY: + // We only write to our GENx_x_UPMETHOD register + let block = unsafe { &*PWM::block() }; + let bits = update_method.0; + match (OP, IS_A) { + (0, true) => block + .gen0_stmp_cfg + .modify(|_, w| w.gen0_a_upmethod().variant(bits)), + (1, true) => block + .gen1_stmp_cfg + .modify(|_, w| w.gen1_a_upmethod().variant(bits)), + (2, true) => block + .gen2_stmp_cfg + .modify(|_, w| w.gen2_a_upmethod().variant(bits)), + (0, false) => block + .gen0_stmp_cfg + .modify(|_, w| w.gen0_b_upmethod().variant(bits)), + (1, false) => block + .gen1_stmp_cfg + .modify(|_, w| w.gen1_b_upmethod().variant(bits)), + (2, false) => block + .gen2_stmp_cfg + .modify(|_, w| w.gen2_b_upmethod().variant(bits)), + _ => { + unreachable!() + } + } + } + + /// Set how a new timestamp syncs with the timer + #[cfg(esp32s3)] + pub fn set_update_method(&mut self, update_method: PwmUpdateMethod) { + // SAFETY: + // We only write to our GENx_x_UPMETHOD register + let block = unsafe { &*PWM::block() }; + let bits = update_method.0; + match (OP, IS_A) { + (0, true) => block + .cmpr0_cfg + .modify(|_, w| w.cmpr0_a_upmethod().variant(bits)), + (1, true) => block + .cmpr1_cfg + .modify(|_, w| w.cmpr1_a_upmethod().variant(bits)), + (2, true) => block + .cmpr2_cfg + .modify(|_, w| w.cmpr2_a_upmethod().variant(bits)), + (0, false) => block + .cmpr0_cfg + .modify(|_, w| w.cmpr0_b_upmethod().variant(bits)), + (1, false) => block + .cmpr1_cfg + .modify(|_, w| w.cmpr1_b_upmethod().variant(bits)), + (2, false) => block + .cmpr2_cfg + .modify(|_, w| w.cmpr2_b_upmethod().variant(bits)), + _ => { + unreachable!() + } + } + } + + /// Set how a new timestamp syncs with the timer. + /// The written value will take effect according to the set + /// [`PwmUpdateMethod`]. + #[cfg(esp32)] + pub fn set_timestamp(&mut self, value: u16) { + // SAFETY: + // We only write to our GENx_TSTMP_x register + let block = unsafe { &*PWM::block() }; + match (OP, IS_A) { + (0, true) => block.gen0_tstmp_a.write(|w| w.gen0_a().variant(value)), + (1, true) => block.gen1_tstmp_a.write(|w| w.gen1_a().variant(value)), + (2, true) => block.gen2_tstmp_a.write(|w| w.gen2_a().variant(value)), + (0, false) => block.gen0_tstmp_b.write(|w| w.gen0_b().variant(value)), + (1, false) => block.gen1_tstmp_b.write(|w| w.gen1_b().variant(value)), + (2, false) => block.gen2_tstmp_b.write(|w| w.gen2_b().variant(value)), + _ => { + unreachable!() + } + } + } + + /// Write a new timestamp. + /// The written value will take effect according to the set + /// [`PwmUpdateMethod`]. + #[cfg(esp32s3)] + pub fn set_timestamp(&mut self, value: u16) { + // SAFETY: + // We only write to our GENx_TSTMP_x register + let block = unsafe { &*PWM::block() }; + match (OP, IS_A) { + (0, true) => block.cmpr0_value0.write(|w| w.cmpr0_a().variant(value)), + (1, true) => block.cmpr1_value0.write(|w| w.cmpr1_a().variant(value)), + (2, true) => block.cmpr2_value0.write(|w| w.cmpr2_a().variant(value)), + (0, false) => block.cmpr0_value1.write(|w| w.cmpr0_b().variant(value)), + (1, false) => block.cmpr1_value1.write(|w| w.cmpr1_b().variant(value)), + (2, false) => block.cmpr2_value1.write(|w| w.cmpr2_b().variant(value)), + _ => { + unreachable!() + } + } + } +} + +/// An action the operator applies to an output +#[non_exhaustive] +#[repr(u32)] +pub enum UpdateAction { + /// Clear the output by setting it to a low level. + SetLow = 1, + /// Set the to a high level. + SetHigh = 2, + /// Change the current output level to the opposite value. + /// If it is currently pulled high, pull it low, or vice versa. + Toggle = 3, +} + +/// Settings for what actions should be taken on timing events +/// +/// ### Note: +/// The hardware supports using a timestamp A event to trigger an action on +/// output B or vice versa. For clearer ownership semantics this HAL does not +/// support such configurations. +pub struct PwmActions(u32); + +impl PwmActions { + /// Using this setting together with a timer configured with + /// [`PwmWorkingMode::Increase`](super::timer::PwmWorkingMode::Increase) + /// will set the output high for a duration proportional to the set + /// timestamp. + pub const UP_ACTIVE_HIGH: Self = Self::empty() + .on_up_counting_timer_equals_zero(UpdateAction::SetHigh) + .on_up_counting_timer_equals_timestamp(UpdateAction::SetLow); + + /// Using this setting together with a timer configured with + /// [`PwmWorkingMode::UpDown`](super::timer::PwmWorkingMode::UpDown) will + /// set the output high for a duration proportional to the set + /// timestamp. + pub const UP_DOWN_ACTIVE_HIGH: Self = Self::empty() + .on_down_counting_timer_equals_timestamp(UpdateAction::SetHigh) + .on_up_counting_timer_equals_timestamp(UpdateAction::SetLow); + + /// `PwmActions` with no `UpdateAction`s set + pub const fn empty() -> Self { + PwmActions(0) + } + + /// Choose an `UpdateAction` for an `UTEZ` event + pub const fn on_up_counting_timer_equals_zero(self, action: UpdateAction) -> Self { + self.with_value_at_offset(action as u32, 0) + } + + /// Choose an `UpdateAction` for an `UTEP` event + pub const fn on_up_counting_timer_equals_period(self, action: UpdateAction) -> Self { + self.with_value_at_offset(action as u32, 2) + } + + /// Choose an `UpdateAction` for an `UTEA`/`UTEB` event + pub const fn on_up_counting_timer_equals_timestamp(self, action: UpdateAction) -> Self { + match IS_A { + true => self.with_value_at_offset(action as u32, 4), + false => self.with_value_at_offset(action as u32, 6), + } + } + + /// Choose an `UpdateAction` for an `DTEZ` event + pub const fn on_down_counting_timer_equals_zero(self, action: UpdateAction) -> Self { + self.with_value_at_offset(action as u32, 12) + } + + /// Choose an `UpdateAction` for an `DTEP` event + pub const fn on_down_counting_timer_equals_period(self, action: UpdateAction) -> Self { + self.with_value_at_offset(action as u32, 14) + } + + /// Choose an `UpdateAction` for an `DTEA`/`DTEB` event + pub const fn on_down_counting_timer_equals_timestamp(self, action: UpdateAction) -> Self { + match IS_A { + true => self.with_value_at_offset(action as u32, 16), + false => self.with_value_at_offset(action as u32, 18), + } + } + + const fn with_value_at_offset(self, value: u32, offset: u32) -> Self { + let mask = !(0b11 << offset); + let value = (self.0 & mask) | (value << offset); + PwmActions(value) + } +} + +/// Settings for when [`PwmPin::set_timestamp`] takes effect +/// +/// Multiple syncing triggers can be set. +pub struct PwmUpdateMethod(u8); + +impl PwmUpdateMethod { + /// New timestamp will be applied immediately + pub const SYNC_IMMEDIATLY: Self = Self::empty(); + /// New timestamp will be applied when timer is equal to zero + pub const SYNC_ON_ZERO: Self = Self::empty().sync_on_timer_equals_zero(); + /// New timestamp will be applied when timer is equal to period + pub const SYNC_ON_PERIOD: Self = Self::empty().sync_on_timer_equals_period(); + + /// `PwmUpdateMethod` with no sync triggers. + /// Corresponds to syncing immediately + pub const fn empty() -> Self { + PwmUpdateMethod(0) + } + + /// Enable syncing new timestamp values when timer is equal to zero + pub const fn sync_on_timer_equals_zero(mut self) -> Self { + self.0 |= 0b0001; + self + } + + /// Enable syncing new timestamp values when timer is equal to period + pub const fn sync_on_timer_equals_period(mut self) -> Self { + self.0 |= 0b0010; + self + } +} diff --git a/esp-hal-common/src/mcpwm/timer.rs b/esp-hal-common/src/mcpwm/timer.rs new file mode 100644 index 00000000000..adbc71c880c --- /dev/null +++ b/esp-hal-common/src/mcpwm/timer.rs @@ -0,0 +1,296 @@ +use core::marker::PhantomData; + +use fugit::HertzU32; + +use crate::{ + clock::Clocks, + mcpwm::{FrequencyError, PeripheralClockConfig, PwmPeripheral}, +}; + +/// A MCPWM timer +/// +/// Every timer of a particular [`MCPWM`](super::MCPWM) peripheral can be used +/// as a timing reference for every +/// [`Operator`](super::operator::Operator) of that peripheral +pub struct Timer { + pub(super) phantom: PhantomData, +} + +impl Timer { + pub(super) fn new() -> Self { + Timer { + phantom: PhantomData, + } + } + + /// Apply the given timer configuration. + /// + /// ### Note: + /// The prescalar and period configuration will be applied immediately and + /// before setting the [`PwmWorkingMode`]. + /// If the timer is already running you might want to call [`Timer::stop`] + /// and/or [`Timer::set_counter`] first + /// (if the new period is larger than the current counter value this will + /// cause weird behavior). + /// + /// The hardware supports writing these settings in sync with certain timer + /// events but this HAL does not expose these for now. + pub fn start(&mut self, timer_config: TimerClockConfig) { + // write prescaler and period with immediate update method + self.cfg0().write(|w| { + w.timer0_prescale() + .variant(timer_config.prescaler) + .timer0_period() + .variant(timer_config.period) + .timer0_period_upmethod() + .variant(0) + }); + + // set timer to continuously run and set the timer working mode + self.cfg1().write(|w| { + w.timer0_start() + .variant(2) + .timer0_mod() + .variant(timer_config.mode as u8) + }); + } + + /// Stop the timer in its current state + pub fn stop(&mut self) { + // freeze the timer + self.cfg1().write(|w| w.timer0_mod().variant(0)); + } + + /// Set the timer counter to the provided value + pub fn set_counter(&mut self, phase: u16, direction: CounterDirection) { + // SAFETY: + // We only write to our TIMERx_SYNC register + let block = unsafe { &*PWM::block() }; + + match TIM { + 0 => { + let sw = block.timer0_sync.read().sw().bit_is_set(); + block.timer0_sync.write(|w| { + w.timer0_phase_direction() + .variant(direction as u8 != 0) + .timer0_phase() + .variant(phase) + .sw() + .variant(!sw) + }); + } + 1 => { + let sw = block.timer1_sync.read().sw().bit_is_set(); + block.timer1_sync.write(|w| { + w.timer1_phase_direction() + .variant(direction as u8 != 0) + .timer1_phase() + .variant(phase) + .sw() + .variant(!sw) + }); + } + 2 => { + let sw = block.timer2_sync.read().sw().bit_is_set(); + block.timer2_sync.write(|w| { + w.timer2_phase_direction() + .variant(direction as u8 != 0) + .timer2_phase() + .variant(phase) + .sw() + .variant(!sw) + }); + } + _ => { + unreachable!() + } + } + } + + /// Read the counter value and counter direction of the timer + pub fn status(&self) -> (u16, CounterDirection) { + // SAFETY: + // We only read from our TIMERx_STATUS register + let block = unsafe { &*PWM::block() }; + + match TIM { + 0 => { + let reg = block.timer0_status.read(); + ( + reg.timer0_value().bits(), + reg.timer0_direction().bit_is_set().into(), + ) + } + 1 => { + let reg = block.timer1_status.read(); + ( + reg.timer1_value().bits(), + reg.timer1_direction().bit_is_set().into(), + ) + } + 2 => { + let reg = block.timer2_status.read(); + ( + reg.timer2_value().bits(), + reg.timer2_direction().bit_is_set().into(), + ) + } + _ => { + unreachable!() + } + } + } + + fn cfg0(&mut self) -> &crate::pac::pwm0::TIMER0_CFG0 { + // SAFETY: + // We only grant access to our CFG0 register with the lifetime of &mut self + let block = unsafe { &*PWM::block() }; + + // SAFETY: + // The CFG0 registers are identical for all timers so we can pretend they're + // TIMER0_CFG0 + match TIM { + 0 => &block.timer0_cfg0, + 1 => unsafe { &*(&block.timer1_cfg0 as *const _ as *const _) }, + 2 => unsafe { &*(&block.timer2_cfg0 as *const _ as *const _) }, + _ => { + unreachable!() + } + } + } + fn cfg1(&mut self) -> &crate::pac::pwm0::TIMER0_CFG1 { + // SAFETY: + // We only grant access to our CFG1 register with the lifetime of &mut self + let block = unsafe { &*PWM::block() }; + + // SAFETY: + // The CFG1 registers are identical for all timers so we can pretend they're + // TIMER0_CFG1 + match TIM { + 0 => &block.timer0_cfg1, + 1 => unsafe { &*(&block.timer1_cfg1 as *const _ as *const _) }, + 2 => unsafe { &*(&block.timer2_cfg1 as *const _ as *const _) }, + _ => { + unreachable!() + } + } + } +} + +/// Clock configuration of a MCPWM timer +/// +/// Use [`PeripheralClockConfig::timer_clock_with_prescaler`](super::PeripheralClockConfig::timer_clock_with_prescaler) or +/// [`PeripheralClockConfig::timer_clock_with_frequency`](super::PeripheralClockConfig::timer_clock_with_frequency) to it. +#[derive(Copy, Clone)] +pub struct TimerClockConfig<'a> { + frequency: HertzU32, + period: u16, + prescaler: u8, + mode: PwmWorkingMode, + phantom: PhantomData<&'a Clocks>, +} + +impl<'a> TimerClockConfig<'a> { + pub(super) fn with_prescaler( + clock: &PeripheralClockConfig<'a>, + period: u16, + mode: PwmWorkingMode, + prescaler: u8, + ) -> Self { + let cycle_period = match mode { + PwmWorkingMode::Increase | PwmWorkingMode::Decrease => period as u32 + 1, + // The reference manual seems to provide an incorrect formula for UpDown + PwmWorkingMode::UpDown => period as u32 * 2, + }; + let frequency = clock.frequency / (prescaler as u32 + 1) / cycle_period; + + TimerClockConfig { + frequency, + prescaler, + period, + mode, + phantom: PhantomData, + } + } + + pub(super) fn with_frequency( + clock: &PeripheralClockConfig<'a>, + period: u16, + mode: PwmWorkingMode, + target_freq: HertzU32, + ) -> Result { + let cycle_period = match mode { + PwmWorkingMode::Increase | PwmWorkingMode::Decrease => period as u32 + 1, + // The reference manual seems to provide an incorrect formula for UpDown + PwmWorkingMode::UpDown => period as u32 * 2, + }; + let target_timer_frequency = target_freq + .raw() + .checked_mul(cycle_period) + .ok_or(FrequencyError)?; + if target_timer_frequency == 0 || target_freq > clock.frequency { + return Err(FrequencyError); + } + let prescaler = clock.frequency.raw() / target_timer_frequency - 1; + if prescaler > u8::MAX as u32 { + return Err(FrequencyError); + } + let frequency = clock.frequency / (prescaler + 1) / cycle_period; + + Ok(TimerClockConfig { + frequency, + prescaler: prescaler as u8, + period, + mode, + phantom: PhantomData, + }) + } + + /// Get the timer clock frequency. + /// + /// ### Note: + /// The actual value is rounded down to the nearest `u32` value + pub fn frequency(&self) -> HertzU32 { + self.frequency + } +} + +/// PWM working mode +#[derive(Copy, Clone)] +#[repr(u8)] +pub enum PwmWorkingMode { + /// In this mode, the PWM timer increments from zero until reaching the + /// value configured in the period field. Once done, the PWM timer + /// returns to zero and starts increasing again. PWM period is equal to the + /// value of the period field + 1. + Increase = 1, + /// The PWM timer decrements to zero, starting from the value configured in + /// the period field. After reaching zero, it is set back to the period + /// value. Then it starts to decrement again. In this case, the PWM period + /// is also equal to the value of period field + 1. + Decrease = 2, + /// This is a combination of the two modes mentioned above. The PWM timer + /// starts increasing from zero until the period value is reached. Then, + /// the timer decreases back to zero. This pattern is then repeated. The + /// PWM period is the result of the value of the period field × 2. + UpDown = 3, +} + +/// The direction the timer counter is changing +#[derive(Debug)] +#[repr(u8)] +pub enum CounterDirection { + /// The timer counter is increasing + Increasing = 0, + /// The timer counter is decreasing + Decreasing = 1, +} + +impl From for CounterDirection { + fn from(bit: bool) -> Self { + match bit { + false => CounterDirection::Increasing, + true => CounterDirection::Decreasing, + } + } +} diff --git a/esp32-hal/examples/mcpwm.rs b/esp32-hal/examples/mcpwm.rs new file mode 100644 index 00000000000..1bc4caf1857 --- /dev/null +++ b/esp32-hal/examples/mcpwm.rs @@ -0,0 +1,64 @@ +//! Uses timer0 and operator0 of the MCPWM0 peripheral to output a 50% duty signal at 20 kHz. +//! +//! The signal will be output to the pin assigned to `pin`. (GPIO4) + +#![no_std] +#![no_main] + +use esp32_hal::{ + clock::ClockControl, + gpio::IO, + mcpwm::{ + {MCPWM, PeripheralClockConfig}, + operator::PwmPinConfig, + timer::PwmWorkingMode, + }, + pac::Peripherals, + prelude::*, + timer::TimerGroup, + Rtc, +}; +use esp_backtrace as _; +use xtensa_lx_rt::entry; + +#[entry] +fn main() -> ! { + let peripherals = Peripherals::take().unwrap(); + let mut system = peripherals.DPORT.split(); + let clocks = ClockControl::boot_defaults(system.clock_control).freeze(); + + let timer_group0 = TimerGroup::new(peripherals.TIMG0, &clocks); + let mut wdt = timer_group0.wdt; + let mut rtc = Rtc::new(peripherals.RTC_CNTL); + + // Disable watchdog timer + wdt.disable(); + rtc.rwdt.disable(); + + let io = IO::new(peripherals.GPIO, peripherals.IO_MUX); + let pin = io.pins.gpio4; + + // initialize peripheral + let clock_cfg = PeripheralClockConfig::with_frequency(&clocks, 40u32.MHz()).unwrap(); + let mut mcpwm = MCPWM::new( + peripherals.PWM0, + clock_cfg, + &mut system.peripheral_clock_control, + ); + + // connect operator0 to timer0 + mcpwm.operator0.set_timer(&mcpwm.timer0); + // connect operator0 to pin + let mut pwm_pin = mcpwm + .operator0 + .with_pin_a(pin, PwmPinConfig::UP_ACTIVE_HIGH); + + // start timer with timestamp values in the range of 0..=99 and a frequency of 20 kHz + let timer_clock_cfg = clock_cfg.timer_clock_with_frequency(99, PwmWorkingMode::Increase, 20u32.kHz()).unwrap(); + mcpwm.timer0.start(timer_clock_cfg); + + // pin will be high 50% of the time + pwm_pin.set_timestamp(50); + + loop {} +} diff --git a/esp32-hal/src/lib.rs b/esp32-hal/src/lib.rs index 70be3d5496a..d521b4f099f 100644 --- a/esp32-hal/src/lib.rs +++ b/esp32-hal/src/lib.rs @@ -14,6 +14,7 @@ pub use esp_hal_common::{ interrupt, ledc, macros, + mcpwm, pac, prelude, pulse_control, diff --git a/esp32s3-hal/examples/mcpwm.rs b/esp32s3-hal/examples/mcpwm.rs new file mode 100644 index 00000000000..43e62c6b5c1 --- /dev/null +++ b/esp32s3-hal/examples/mcpwm.rs @@ -0,0 +1,64 @@ +//! Uses timer0 and operator0 of the MCPWM0 peripheral to output a 50% duty signal at 20 kHz. +//! +//! The signal will be output to the pin assigned to `pin`. (GPIO4) + +#![no_std] +#![no_main] + +use esp32s3_hal::{ + clock::ClockControl, + gpio::IO, + mcpwm::{ + {MCPWM, PeripheralClockConfig}, + operator::PwmPinConfig, + timer::PwmWorkingMode, + }, + pac::Peripherals, + prelude::*, + timer::TimerGroup, + Rtc, +}; +use esp_backtrace as _; +use xtensa_lx_rt::entry; + +#[entry] +fn main() -> ! { + let peripherals = Peripherals::take().unwrap(); + let mut system = peripherals.SYSTEM.split(); + let clocks = ClockControl::boot_defaults(system.clock_control).freeze(); + + let timer_group0 = TimerGroup::new(peripherals.TIMG0, &clocks); + let mut wdt = timer_group0.wdt; + let mut rtc = Rtc::new(peripherals.RTC_CNTL); + + // Disable watchdog timer + wdt.disable(); + rtc.rwdt.disable(); + + let io = IO::new(peripherals.GPIO, peripherals.IO_MUX); + let pin = io.pins.gpio4; + + // initialize peripheral + let clock_cfg = PeripheralClockConfig::with_frequency(&clocks, 40u32.MHz()).unwrap(); + let mut mcpwm = MCPWM::new( + peripherals.PWM0, + clock_cfg, + &mut system.peripheral_clock_control, + ); + + // connect operator0 to timer0 + mcpwm.operator0.set_timer(&mcpwm.timer0); + // connect operator0 to pin + let mut pwm_pin = mcpwm + .operator0 + .with_pin_a(pin, PwmPinConfig::UP_ACTIVE_HIGH); + + // start timer with timestamp values in the range of 0..=99 and a frequency of 20 kHz + let timer_clock_cfg = clock_cfg.timer_clock_with_frequency(99, PwmWorkingMode::Increase, 20u32.kHz()).unwrap(); + mcpwm.timer0.start(timer_clock_cfg); + + // pin will be high 50% of the time + pwm_pin.set_timestamp(50); + + loop {} +} diff --git a/esp32s3-hal/src/lib.rs b/esp32s3-hal/src/lib.rs index fd161de7632..ecfb3a71028 100644 --- a/esp32s3-hal/src/lib.rs +++ b/esp32s3-hal/src/lib.rs @@ -15,6 +15,7 @@ pub use esp_hal_common::{ interrupt, ledc, macros, + mcpwm, otg_fs, pac, prelude,