Skip to content
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

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft

Add sdm #2371

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions esp-hal/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Add burst transfer support to DMA buffers (#2336)
- `AnyPin` now implements `From<GpioPin<N>>`. (#2326)
- `Pins::steal()` to unsafely obtain GPIO. (#2335)
- Added `esp_hal::sdm` Sigma-Delta modulation driver (#2370)

### Changed

Expand Down
2 changes: 2 additions & 0 deletions esp-hal/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,8 @@ pub mod rom;
pub mod rsa;
#[cfg(any(lp_clkrst, rtc_cntl))]
pub mod rtc_cntl;
#[cfg(sdm)]
pub mod sdm;
#[cfg(sha)]
pub mod sha;
#[cfg(any(spi0, spi1, spi2, spi3))]
Expand Down
296 changes: 296 additions & 0 deletions esp-hal/src/sdm.rs
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);
}
}
Comment on lines +80 to +84
Copy link
Collaborator

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.

Copy link
Contributor

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.

Copy link
Collaborator

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 statics) and it's somewhat tedious (though I reckon few applications will actually want to release their drivers anyway).

Copy link
Contributor Author

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.


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> {
Copy link
Collaborator

Choose a reason for hiding this comment

The 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 Error?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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>,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using 'd for both the borrow and the ChannelRef will have some awkward consequences around static.

The Channel should just take ownership of the ChannelRef, at least then the Channels can be sent off to other threads.

Copy link
Contributor Author

@katyo katyo Oct 22, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Of course, I trying to redesign that.
But seems taking ownership leads to necessity in explicit drop.

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 {
Copy link
Collaborator

Choose a reason for hiding this comment

The 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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 _
}
1 change: 1 addition & 0 deletions esp-metadata/devices/esp32.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ peripherals = [
"rtc_io",
"sdhost",
"sens",
"sdm",
"sha",
"slc",
"slchost",
Expand Down
1 change: 1 addition & 0 deletions esp-metadata/devices/esp32c3.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ peripherals = [
"rsa",
"rtc_cntl",
"sensitive",
"sdm",
"sha",
"spi0",
"spi1",
Expand Down
1 change: 1 addition & 0 deletions esp-metadata/devices/esp32c6.toml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ peripherals = [
"rmt",
"rng",
"rsa",
"sdm",
"sha",
"slchost",
"soc_etm",
Expand Down
1 change: 1 addition & 0 deletions esp-metadata/devices/esp32h2.toml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ peripherals = [
"rmt",
"rng",
"rsa",
"sdm",
"sha",
"soc_etm",
"spi0",
Expand Down
1 change: 1 addition & 0 deletions esp-metadata/devices/esp32p4.toml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ peripherals = [
# "rmt",
# "rsa",
# "sdhost",
# "sdm",
# "sha",
# "soc_etm",
# "spi0",
Expand Down
1 change: 1 addition & 0 deletions esp-metadata/devices/esp32s2.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ peripherals = [
"rtc_i2c",
"rtc_io",
"sens",
"sdm",
"sha",
"spi0",
"spi1",
Expand Down
1 change: 1 addition & 0 deletions esp-metadata/devices/esp32s3.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ peripherals = [
"rtc_io",
"sens",
"sensitive",
"sdm",
"sha",
"spi0",
"spi1",
Expand Down
Loading
Loading