-
Notifications
You must be signed in to change notification settings - Fork 214
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add sdm #2371
base: main
Are you sure you want to change the base?
Add sdm #2371
Changes from all commits
d4e571c
89a3b1a
8a6260a
fa0e2a0
4c6c403
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,296 @@ | ||
//! # Sigma-Delta modulation (SDM) | ||
//! | ||
//! ## Overview | ||
//! | ||
//! Almost all ESP SoCs has a second-order sigma-delta modulator, which | ||
//! can generate independent PDM pulses to multiple channels. Please refer | ||
//! to the TRM to check how many hardware channels are available. | ||
//! | ||
//! Delta-sigma modulation converts an analog voltage signal into a pulse | ||
//! frequency, or pulse density, which can be understood as pulse-density | ||
//! modulation (PDM) (refer to [Delta-sigma modulation on Wikipedia](https://en.wikipedia.org/wiki/Delta-sigma_modulation)). | ||
//! | ||
//! Typically, a Sigma-Delta modulated channel can be used in scenarios like: | ||
//! | ||
//! - LED dimming | ||
//! - Simple DAC (8-bit), with the help of an active RC low-pass filter | ||
//! - Class D amplifier, with the help of a half-bridge or full-bridge circuit | ||
//! plus an LC low-pass filter | ||
//! | ||
//! ## Configuration | ||
//! | ||
//! After creating [`Sdm`] instance you should connect individual channels to | ||
//! GPIO outputs. Also you need set modulation frequency. | ||
//! | ||
//! ## Usage | ||
//! | ||
//! Connected channels accepts pulse density in range -128..127. | ||
|
||
use core::marker::PhantomData; | ||
|
||
use fugit::HertzU32; | ||
|
||
use crate::{ | ||
clock::Clocks, | ||
gpio::{OutputPin, OutputSignal}, | ||
peripheral::{Peripheral, PeripheralRef}, | ||
peripherals::GPIO_SD, | ||
private, | ||
}; | ||
|
||
/// Sigma-Delta modulation peripheral driver. | ||
pub struct Sdm<'d> { | ||
/// Channel 0 | ||
pub channel0: ChannelRef<'d, 0>, | ||
|
||
/// Channel 1 | ||
pub channel1: ChannelRef<'d, 1>, | ||
|
||
/// Channel 2 | ||
pub channel2: ChannelRef<'d, 2>, | ||
|
||
/// Channel 3 | ||
pub channel3: ChannelRef<'d, 3>, | ||
|
||
#[cfg(any(esp32, esp32s2, esp32s3))] | ||
/// Channel 4 | ||
pub channel4: ChannelRef<'d, 4>, | ||
|
||
#[cfg(any(esp32, esp32s2, esp32s3))] | ||
/// Channel 5 | ||
pub channel5: ChannelRef<'d, 5>, | ||
|
||
#[cfg(any(esp32, esp32s2, esp32s3))] | ||
/// Channel 6 | ||
pub channel6: ChannelRef<'d, 6>, | ||
|
||
#[cfg(any(esp32, esp32s2, esp32s3))] | ||
/// Channel 7 | ||
pub channel7: ChannelRef<'d, 7>, | ||
} | ||
|
||
/// Channel errors | ||
#[derive(Debug, Clone, Copy, PartialEq)] | ||
#[cfg_attr(feature = "defmt", derive(defmt::Format))] | ||
pub enum Error { | ||
/// Prescale out of range | ||
PrescaleRange, | ||
} | ||
|
||
impl<'d> Drop for Sdm<'d> { | ||
fn drop(&mut self) { | ||
GPIO_SD::enable_clock(false); | ||
} | ||
} | ||
|
||
impl<'d> Sdm<'d> { | ||
/// Initialize driver using a given SD instance. | ||
pub fn new(_sd: impl crate::peripheral::Peripheral<P = GPIO_SD> + 'd) -> Self { | ||
GPIO_SD::enable_clock(true); | ||
|
||
Self { | ||
channel0: ChannelRef::new(), | ||
channel1: ChannelRef::new(), | ||
channel2: ChannelRef::new(), | ||
channel3: ChannelRef::new(), | ||
|
||
#[cfg(any(esp32, esp32s2, esp32s3))] | ||
channel4: ChannelRef::new(), | ||
#[cfg(any(esp32, esp32s2, esp32s3))] | ||
channel5: ChannelRef::new(), | ||
#[cfg(any(esp32, esp32s2, esp32s3))] | ||
channel6: ChannelRef::new(), | ||
#[cfg(any(esp32, esp32s2, esp32s3))] | ||
channel7: ChannelRef::new(), | ||
} | ||
} | ||
} | ||
|
||
/// Sigma-Delta modulation channel reference. | ||
pub struct ChannelRef<'d, const N: u8> { | ||
_phantom: PhantomData<&'d ()>, | ||
} | ||
|
||
impl<'d, const N: u8> ChannelRef<'d, N> { | ||
fn new() -> Self { | ||
Self { | ||
_phantom: PhantomData, | ||
} | ||
} | ||
|
||
/// Configure and connect sigma-delta channel to output | ||
pub fn connect<O: OutputPin>( | ||
&'d self, | ||
output: impl Peripheral<P = O> + 'd, | ||
frequency: HertzU32, | ||
) -> Result<Channel<'d, N, O>, Error> { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looks like this can't fail or is there something planned for the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This may fail when prescale which calculated from frequency is out of range. |
||
crate::into_ref!(output); | ||
|
||
let signal = CHANNELS[N as usize]; | ||
|
||
output.connect_peripheral_to_output(signal, private::Internal); | ||
|
||
let channel = Channel { _ref: self, output }; | ||
|
||
channel.set_frequency(frequency)?; | ||
|
||
Ok(channel) | ||
} | ||
} | ||
|
||
/// Sigma-Delta modulation channel handle. | ||
pub struct Channel<'d, const N: u8, O: OutputPin> { | ||
_ref: &'d ChannelRef<'d, N>, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using The There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Of course, I trying to redesign that. |
||
output: PeripheralRef<'d, O>, | ||
} | ||
|
||
impl<'d, const N: u8, O: OutputPin> Drop for Channel<'d, N, O> { | ||
fn drop(&mut self) { | ||
let signal = CHANNELS[N as usize]; | ||
self.output | ||
.disconnect_from_peripheral_output(signal, private::Internal); | ||
} | ||
} | ||
|
||
impl<'d, const N: u8, O: OutputPin> Channel<'d, N, O> { | ||
/// Set raw pulse density | ||
/// | ||
/// Sigma-delta quantized density of one channel, the value ranges from -128 | ||
/// to 127, recommended range is -90 ~ 90. The waveform is more like a | ||
/// random one in this range. | ||
pub fn set_pulse_density(&self, density: i8) { | ||
GPIO_SD::set_pulse_density(N, density); | ||
} | ||
|
||
/// Set duty cycle | ||
pub fn set_duty(&self, duty: u8) { | ||
let density = duty as i16 - 128; | ||
self.set_pulse_density(density as i8) | ||
} | ||
|
||
/// Set raw prescale | ||
/// | ||
/// The divider of source clock, ranges from 1 to 256 | ||
pub fn set_prescale(&self, prescale: u16) -> Result<(), Error> { | ||
if (1..=256).contains(&prescale) { | ||
GPIO_SD::set_prescale(N, prescale); | ||
Ok(()) | ||
} else { | ||
Err(Error::PrescaleRange) | ||
} | ||
} | ||
|
||
/// Set prescale using frequency | ||
pub fn set_frequency(&self, frequency: HertzU32) -> Result<(), Error> { | ||
let clocks = Clocks::get(); | ||
let clock_frequency = clocks.apb_clock.to_Hz(); | ||
let frequency = frequency.to_Hz(); | ||
|
||
let prescale = prescale_from_frequency(clock_frequency, frequency); | ||
|
||
self.set_prescale(prescale) | ||
} | ||
} | ||
|
||
mod ehal1 { | ||
use embedded_hal::pwm::{Error as PwmError, ErrorKind, ErrorType, SetDutyCycle}; | ||
|
||
use super::{Channel, Error, OutputPin}; | ||
|
||
impl PwmError for Error { | ||
fn kind(&self) -> ErrorKind { | ||
ErrorKind::Other | ||
} | ||
} | ||
|
||
impl<'d, const N: u8, O: OutputPin> ErrorType for Channel<'d, N, O> { | ||
type Error = Error; | ||
} | ||
|
||
impl<'d, const N: u8, O: OutputPin> SetDutyCycle for Channel<'d, N, O> { | ||
fn max_duty_cycle(&self) -> u16 { | ||
255 | ||
} | ||
|
||
fn set_duty_cycle(&mut self, mut duty: u16) -> Result<(), Self::Error> { | ||
let max = self.max_duty_cycle(); | ||
duty = if duty > max { max } else { duty }; | ||
self.set_duty(duty as u8); | ||
Ok(()) | ||
} | ||
} | ||
} | ||
|
||
#[cfg(any(esp32, esp32s2, esp32s3))] | ||
const CHANNELS: [OutputSignal; 8] = [ | ||
OutputSignal::GPIO_SD0, | ||
OutputSignal::GPIO_SD1, | ||
OutputSignal::GPIO_SD2, | ||
OutputSignal::GPIO_SD3, | ||
OutputSignal::GPIO_SD4, | ||
OutputSignal::GPIO_SD5, | ||
OutputSignal::GPIO_SD6, | ||
OutputSignal::GPIO_SD7, | ||
]; | ||
|
||
#[cfg(any(esp32c3, esp32c6, esp32h2))] | ||
const CHANNELS: [OutputSignal; 4] = [ | ||
OutputSignal::GPIO_SD0, | ||
OutputSignal::GPIO_SD1, | ||
OutputSignal::GPIO_SD2, | ||
OutputSignal::GPIO_SD3, | ||
]; | ||
|
||
#[doc(hidden)] | ||
pub trait RegisterAccess { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This trait isn't really being used, the methods should just be inlined. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok |
||
/// Enable/disable sigma/delta clock | ||
fn enable_clock(en: bool); | ||
|
||
/// Set channel pulse density | ||
fn set_pulse_density(ch: u8, density: i8); | ||
|
||
/// Set channel clock pre-scale | ||
fn set_prescale(ch: u8, prescale: u16); | ||
} | ||
|
||
impl RegisterAccess for GPIO_SD { | ||
fn enable_clock(_en: bool) { | ||
// The clk enable register does not exist on ESP32. | ||
#[cfg(not(esp32))] | ||
{ | ||
let sd = unsafe { &*Self::PTR }; | ||
|
||
sd.sigmadelta_misc() | ||
.modify(|_, w| w.function_clk_en().bit(_en)); | ||
} | ||
} | ||
|
||
fn set_pulse_density(ch: u8, density: i8) { | ||
let sd = unsafe { &*Self::PTR }; | ||
|
||
sd.sigmadelta(ch as _) | ||
.modify(|_, w| unsafe { w.in_().bits(density as _) }); | ||
} | ||
|
||
fn set_prescale(ch: u8, prescale: u16) { | ||
let sd = unsafe { &*Self::PTR }; | ||
|
||
sd.sigmadelta(ch as _) | ||
.modify(|_, w| unsafe { w.prescale().bits((prescale - 1) as _) }); | ||
} | ||
} | ||
|
||
fn prescale_from_frequency(clk_freq: u32, req_freq: u32) -> u16 { | ||
let pre = clk_freq / req_freq; | ||
let err = clk_freq % req_freq; | ||
|
||
// Do the normal rounding and error >= (src/n + src/(n+1)) / 2, | ||
// then carry the bit | ||
let pre = if err >= clk_freq / (2 * pre * (pre + 1)) { | ||
pre + 1 | ||
} else { | ||
pre | ||
}; | ||
|
||
pre as _ | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -33,6 +33,7 @@ peripherals = [ | |
"rtc_io", | ||
"sdhost", | ||
"sens", | ||
"sdm", | ||
"sha", | ||
"slc", | ||
"slchost", | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -26,6 +26,7 @@ peripherals = [ | |
"rsa", | ||
"rtc_cntl", | ||
"sensitive", | ||
"sdm", | ||
"sha", | ||
"spi0", | ||
"spi1", | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -50,6 +50,7 @@ peripherals = [ | |
"rmt", | ||
"rng", | ||
"rsa", | ||
"sdm", | ||
"sha", | ||
"slchost", | ||
"soc_etm", | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -44,6 +44,7 @@ peripherals = [ | |
"rmt", | ||
"rng", | ||
"rsa", | ||
"sdm", | ||
"sha", | ||
"soc_etm", | ||
"spi0", | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -71,6 +71,7 @@ peripherals = [ | |
# "rmt", | ||
# "rsa", | ||
# "sdhost", | ||
# "sdm", | ||
# "sha", | ||
# "soc_etm", | ||
# "spi0", | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -29,6 +29,7 @@ peripherals = [ | |
"rtc_i2c", | ||
"rtc_io", | ||
"sens", | ||
"sdm", | ||
"sha", | ||
"spi0", | ||
"spi1", | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -37,6 +37,7 @@ peripherals = [ | |
"rtc_io", | ||
"sens", | ||
"sensitive", | ||
"sdm", | ||
"sha", | ||
"spi0", | ||
"spi1", | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, having a
Drop
implementation here means that the channels can't be sent off to other threads/tasks/cores/interrupts.I'm wondering if there are any downsides of leaving it out.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, the channels themselves should track how many are still enabled (with an AtomicUsize for example) and disable the clocks if all are gone. The "collection" peripheral shouldn't be Drop because the usual intent is to move out of it. We are putting work into establishing these patterns in this dev cycle, along with implementing Drop for peripherals so that they clean up after themselves. I'm not sure it's worth doing similar work in parallel until we arrive at something we like.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Personally I'd like an explicit drop. The user can recreate the "collection" peripheral from the parts that were split, and then call some
release()
method to safely get the peripheral back (which would disable clocks, reset, etc.).Only two issues are it doesn't quite play nicely with the PeripheralRef pattern (but it works better for
static
s) and it's somewhat tedious (though I reckon few applications will actually want to release their drivers anyway).There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Dominaezzz When I tried to share LEDC channel with interrupt handler I cannot do that. Seems current design lacks support for such usage scenarios.
@bugadani I think that ability to deinit periphery is matter especially for applications which needs power management.
@Dominaezzz In my opinion explicit drop via API looks like a low level thing and we should avoid it as much as possible.