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

Protect SYSTIMER/TIMG shared registers #2051

Merged
merged 5 commits into from
Sep 2, 2024
Merged
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
88 changes: 88 additions & 0 deletions esp-hal/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -572,6 +572,94 @@ mod critical_section_impl {
}
}

// The state of a re-entrant lock
pub(crate) struct LockState {
#[cfg(multi_core)]
core: portable_atomic::AtomicUsize,
}

impl LockState {
#[cfg(multi_core)]
const UNLOCKED: usize = usize::MAX;

pub const fn new() -> Self {
Self {
#[cfg(multi_core)]
core: portable_atomic::AtomicUsize::new(Self::UNLOCKED),
}
}
}

// This is preferred over critical-section as this allows you to have multiple
// locks active at the same time rather than using the global mutex that is
// critical-section.
#[allow(unused_variables)]
pub(crate) fn lock<T>(state: &LockState, f: impl FnOnce() -> T) -> T {
// In regards to disabling interrupts, we only need to disable
// the interrupts that may be calling this function.

#[cfg(not(multi_core))]
{
// Disabling interrupts is enough on single core chips to ensure mutual
// exclusion.

#[cfg(riscv)]
return riscv::interrupt::free(f);
#[cfg(xtensa)]
return xtensa_lx::interrupt::free(|_| f());
}

#[cfg(multi_core)]
{
use portable_atomic::Ordering;

let current_core = get_core() as usize;

let mut f = f;

loop {
let func = || {
// Use Acquire ordering in success to ensure `f()` "happens after" the lock is
// taken. Use Relaxed ordering in failure as there's no
// synchronisation happening.
if let Err(locked_core) = state.core.compare_exchange(
LockState::UNLOCKED,
current_core,
Ordering::Acquire,
Ordering::Relaxed,
) {
assert_ne!(
locked_core, current_core,
"esp_hal::lock is not re-entrant!"
);

Err(f)
} else {
let result = f();

// Use Release ordering here to ensure `f()` "happens before" this lock is
// released.
state.core.store(LockState::UNLOCKED, Ordering::Release);

Ok(result)
}
};

#[cfg(riscv)]
let result = riscv::interrupt::free(func);
#[cfg(xtensa)]
let result = xtensa_lx::interrupt::free(|_| func());

match result {
Ok(result) => break result,
Err(the_function) => f = the_function,
}

// Consider using core::hint::spin_loop(); Might need SW_INT.
}
}
}

/// Default (unhandled) interrupt handler
pub const DEFAULT_INTERRUPT_HANDLER: interrupt::InterruptHandler = interrupt::InterruptHandler::new(
unsafe { core::mem::transmute::<*const (), extern "C" fn()>(EspDefaultHandler as *const ()) },
Expand Down
53 changes: 34 additions & 19 deletions esp-hal/src/timer/systimer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,13 +80,15 @@ use fugit::{Instant, MicrosDurationU32, MicrosDurationU64};
use super::{Error, Timer as _};
use crate::{
interrupt::{self, InterruptHandler},
lock,
peripheral::Peripheral,
peripherals::{Interrupt, SYSTIMER},
system::{Peripheral as PeripheralEnable, PeripheralClockControl},
Async,
Blocking,
Cpu,
InterruptConfigurable,
LockState,
Mode,
};

Expand Down Expand Up @@ -240,7 +242,7 @@ pub trait Unit {
let systimer = unsafe { &*SYSTIMER::ptr() };
let conf = systimer.conf();

critical_section::with(|_| {
lock(&CONF_LOCK, || {
conf.modify(|_, w| match config {
UnitConfig::Disabled => match self.channel() {
0 => w.timer_unit0_work_en().clear_bit(),
Expand Down Expand Up @@ -418,20 +420,22 @@ pub trait Comparator {
fn set_enable(&self, enable: bool) {
let systimer = unsafe { &*SYSTIMER::ptr() };

critical_section::with(|_| {
lock(&CONF_LOCK, || {
#[cfg(not(esp32s2))]
systimer.conf().modify(|_, w| match self.channel() {
0 => w.target0_work_en().bit(enable),
1 => w.target1_work_en().bit(enable),
2 => w.target2_work_en().bit(enable),
_ => unreachable!(),
});

#[cfg(esp32s2)]
systimer
.target_conf(self.channel() as usize)
.modify(|_r, w| w.work_en().bit(enable));
});

// Note: The ESP32-S2 doesn't require a lock because each
// comparator's enable bit in a different register.
#[cfg(esp32s2)]
systimer
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm still not convinced this is sound, as the implementors are still possible to be Sync. If one context modifies the period, and the other context modifies work_en, one of them will have a chance of restoring the other's old state. You should not avoid locking unless the register has a single bit, or the method takes &mut self.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I'll make the objects not Sync, then use from multiple contexts would be illegal.

Copy link
Contributor

@bugadani bugadani Aug 31, 2024

Choose a reason for hiding this comment

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

That kinda invalidates this whole PR and steers the users toward "slap a Mutex<RefCell> around everything". I would like to not do that, please.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The mutex that is used for the non-S2 chips was added because multiple objects, Unit<0>, Unit<1>, Comparator<0>, etc. which could be sent to different threads/contexts, needed to modify the same register.

The mutex wasn't added to allow sharing of the same object. These objects were never intended to be shared, only sent, for the reasons you described "one of them will have a chance of restoring the other's old state".

I definitely agree with you about Mutex<RefCell> being bad but I don't see why locking helps in the S2 case.
Even if I were to wrap it in a lock. How many LockState objects would be necessary?

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not stubbornly-refusing-to-accept your arguments here (even if I'm not ready to accept them right now), but in the context of this PR it's not technically a correct change :)

.target_conf(self.channel() as usize)
.modify(|_r, w| w.work_en().bit(enable));
}

/// Returns true if the comparator has been enabled. This means
Expand Down Expand Up @@ -964,9 +968,11 @@ where
}

fn enable_interrupt(&self, state: bool) {
unsafe { &*SYSTIMER::PTR }
.int_ena()
.modify(|_, w| w.target(self.comparator.channel()).bit(state));
lock(&INT_ENA_LOCK, || {
unsafe { &*SYSTIMER::PTR }
.int_ena()
.modify(|_, w| w.target(self.comparator.channel()).bit(state));
});
}

fn clear_interrupt(&self) {
Expand Down Expand Up @@ -1004,6 +1010,9 @@ where
}
}

static CONF_LOCK: LockState = LockState::new();
static INT_ENA_LOCK: LockState = LockState::new();

// Async functionality of the system timer.
#[cfg(feature = "async")]
mod asynch {
Expand Down Expand Up @@ -1090,27 +1099,33 @@ mod asynch {

#[handler]
fn target0_handler() {
unsafe { &*crate::peripherals::SYSTIMER::PTR }
.int_ena()
.modify(|_, w| w.target0().clear_bit());
lock(&INT_ENA_LOCK, || {
unsafe { &*crate::peripherals::SYSTIMER::PTR }
.int_ena()
.modify(|_, w| w.target0().clear_bit());
});

WAKERS[0].wake();
}

#[handler]
fn target1_handler() {
unsafe { &*crate::peripherals::SYSTIMER::PTR }
.int_ena()
.modify(|_, w| w.target1().clear_bit());
lock(&INT_ENA_LOCK, || {
unsafe { &*crate::peripherals::SYSTIMER::PTR }
.int_ena()
.modify(|_, w| w.target1().clear_bit());
});

WAKERS[1].wake();
}

#[handler]
fn target2_handler() {
unsafe { &*crate::peripherals::SYSTIMER::PTR }
.int_ena()
.modify(|_, w| w.target2().clear_bit());
lock(&INT_ENA_LOCK, || {
unsafe { &*crate::peripherals::SYSTIMER::PTR }
.int_ena()
.modify(|_, w| w.target2().clear_bit());
});

WAKERS[2].wake();
}
Expand Down
28 changes: 19 additions & 9 deletions esp-hal/src/timer/timg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,16 +77,20 @@ use crate::soc::constants::TIMG_DEFAULT_CLK_SRC;
use crate::{
clock::Clocks,
interrupt::{self, InterruptHandler},
lock,
peripheral::{Peripheral, PeripheralRef},
peripherals::{timg0::RegisterBlock, Interrupt, TIMG0},
private::Sealed,
system::{Peripheral as PeripheralEnable, PeripheralClockControl},
Async,
Blocking,
InterruptConfigurable,
LockState,
Mode,
};

static INT_ENA_LOCK: LockState = LockState::new();

/// A timer group consisting of up to 2 timers (chip dependent) and a watchdog
/// timer.
pub struct TimerGroup<'d, T, DM>
Expand Down Expand Up @@ -481,9 +485,11 @@ where
.config()
.modify(|_, w| w.level_int_en().set_bit());

self.register_block()
.int_ena()
.modify(|_, w| w.t(self.timer_number()).bit(state));
lock(&INT_ENA_LOCK, || {
self.register_block()
.int_ena()
.modify(|_, w| w.t(self.timer_number()).bit(state));
});
}

fn clear_interrupt(&self) {
Expand Down Expand Up @@ -706,15 +712,19 @@ where
.config()
.modify(|_, w| w.level_int_en().set_bit());

self.register_block()
.int_ena()
.modify(|_, w| w.t(T).set_bit());
lock(&INT_ENA_LOCK, || {
self.register_block()
.int_ena()
.modify(|_, w| w.t(T).set_bit());
});
}

fn unlisten(&self) {
self.register_block()
.int_ena()
.modify(|_, w| w.t(T).clear_bit());
lock(&INT_ENA_LOCK, || {
self.register_block()
.int_ena()
.modify(|_, w| w.t(T).clear_bit());
});
}

fn clear_interrupt(&self) {
Expand Down
Loading