Support multiple concurrent IntervalTimers (Teensy parity)

The previous implementation used a single static SIGALRM handler and one
ITIMER_REAL, so a second IntervalTimer silently clobbered the first
despite the API documenting up to 4 simultaneous timers.

Reimplement with one std::thread per active timer, each invoking its
callback at the requested period via sleep_until. A static slot counter
caps concurrent timers at 4 (matching the Teensy 4.x PIT channels);
begin() returns false once all 4 are in use, and end() frees the slot.
update() retimes the next interval, and a self-end() from within a
callback detaches rather than joining to avoid deadlock.

Because the library now uses std::thread, src/CMakeLists.txt links
Threads::Threads PUBLIC so consumers (e.g. test/blink) link pthread
transitively. Adds a test/bugs regression test covering four concurrent
timers, the max-4 limit, and slot reuse after end().

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FAsvzgsaayj2nbZnnQGAav
This commit is contained in:
Claude
2026-06-24 20:08:25 +00:00
parent f10fa5c08c
commit 9835f5ee2b
5 changed files with 131 additions and 53 deletions
+4 -2
View File
@@ -56,8 +56,10 @@ simply runs the three CMake builds above on every push.
by `initialize_mock_arduino()` (`Arduino.cpp`), which captures the start epoch so timers read
as "since program start". **Sketches must call `initialize_mock_arduino()` at the top of
`main()`** or timing values are absolute epoch values.
- **`IntervalTimer`** (`IntervalTimer.cpp`) is implemented with POSIX `setitimer(ITIMER_REAL)` +
a `SIGALRM` handler — a single static handler, so it effectively supports one active timer.
- **`IntervalTimer`** (`IntervalTimer.cpp`) runs each active timer on its own `std::thread`,
with a static slot counter capped at 4 (matching the Teensy 4.x PIT channels) — `begin()`
returns false once all 4 are in use. Because it uses `std::thread`, `src/CMakeLists.txt` links
`Threads::Threads` as a PUBLIC dependency so consumers link pthread transitively.
- **Concurrency / interrupts.** There is no ISR model. `__disable_irq`/`__enable_irq`
(`Arduino.cpp`) are a `std::mutex` critical section guarded by the global
`arduino_should_exit` flag. `yield()` sleeps and optionally drives the `EventResponder` (via
+6 -1
View File
@@ -46,4 +46,9 @@ if (BUILD_DYNAMIC_LIBRARY)
add_library(teensy_x86_stubs SHARED ${HEADER_FILES} ${SOURCE_FILES})
else ()
add_library(teensy_x86_stubs SHARED STATIC ${HEADER_FILES} ${SOURCE_FILES})
endif ()
endif ()
# IntervalTimer runs each timer on its own std::thread, so consumers must link
# a threads library. Propagate it publicly so anything linking this lib gets it.
find_package(Threads REQUIRED)
target_link_libraries(teensy_x86_stubs PUBLIC Threads::Threads)
+49 -15
View File
@@ -30,24 +30,58 @@
#include "IntervalTimer.h"
std::function<void()> IntervalTimer::handler = NULL;
std::atomic<int> IntervalTimer::s_activeCount{0};
void timer_handler (int signum){
IntervalTimer::callhandler();
//printf("dfsdfsdf\n");
bool IntervalTimer::beginMicros(callback_t funct, uint32_t microseconds)
{
if (funct == nullptr) return false;
if (microseconds == 0 || microseconds > MAX_PERIOD) return false;
// Restart cleanly if this instance was already running (frees its slot).
end();
// Claim one of the limited timer slots, matching the Teensy's 4 PIT
// channels: once all are in use, begin() fails just like on hardware.
int cur = s_activeCount.load();
do {
if (cur >= MAX_TIMERS) return false;
} while (!s_activeCount.compare_exchange_weak(cur, cur + 1));
_func = funct;
_period_us.store(microseconds);
_active.store(true);
_thread = std::thread(&IntervalTimer::run, this);
return true;
}
bool IntervalTimer::beginCycles(callback_t funct, uint32_t cycles)
void IntervalTimer::run()
{
IntervalTimer::handler = funct;
s_action.sa_handler = &timer_handler;
sigaction (SIGALRM, &s_action, NULL);
using namespace std::chrono;
auto next = steady_clock::now();
while (_active.load()) {
// Schedule the next tick before sleeping so update() takes effect on
// the following interval (the in-progress interval completes as set).
next += microseconds(_period_us.load());
std::this_thread::sleep_until(next);
if (!_active.load()) break;
callback_t f = _func;
if (f) f();
}
}
timer.it_value.tv_sec = 0;
timer.it_value.tv_usec = cycles;
timer.it_interval.tv_sec = 0;
timer.it_interval.tv_usec = cycles;
setitimer (ITIMER_REAL, &timer, NULL);
return true;
void IntervalTimer::end()
{
bool was_active = _active.exchange(false);
if (_thread.joinable()) {
if (std::this_thread::get_id() == _thread.get_id()) {
// Called from within the callback itself: can't join our own
// thread, so let it finish and detach.
_thread.detach();
} else {
_thread.join();
}
}
if (was_active) {
s_activeCount.fetch_sub(1);
}
}
+31 -35
View File
@@ -33,28 +33,35 @@
#define __INTERVALTIMER_H__
#include <stddef.h>
#include "Arduino.h"
#include <signal.h>
#include <time.h>
#include <sys/time.h>
#include <functional>
#include <cstring>
#include <cstdint>
#include <atomic>
#include <thread>
#include <chrono>
// IntervalTimer provides access to hardware timers which can run an
// interrupt function a precise timing intervals.
// https://www.pjrc.com/teensy/td_timing_IntervalTimer.html
// Up to 4 IntervalTimers may be in use simultaneously. Many
// libraries use IntervalTimer, so some of these 4 possible
// instances may be in use by libraries.
//
// On the host this is emulated with one std::thread per active timer: the
// callback runs on its own thread at the requested period. Up to MAX_TIMERS
// (4, matching the Teensy 4.x PIT channels) may run at once; begin() returns
// false once they are all in use.
class IntervalTimer {
private:
static const int32_t MAX_PERIOD = UINT32_MAX / (24000000 / 1000000); // need to change to int32_t to avoid warnings
static const uint32_t MAX_PERIOD = UINT32_MAX / (24000000 / 1000000);
static const int MAX_TIMERS = 4; // Teensy 4.x exposes 4 PIT channels
static std::atomic<int> s_activeCount; // number of timers currently running
public:
IntervalTimer() {
memset (&s_action, 0, sizeof (s_action));
}
IntervalTimer() {}
~IntervalTimer() {
end();
}
// IntervalTimer owns a thread and atomics, so it is not copyable/movable.
IntervalTimer(const IntervalTimer&) = delete;
IntervalTimer& operator=(const IntervalTimer&) = delete;
using callback_t = void (*)(void);
// Start the hardware timer and begin calling the function. The
// interval is specified in microseconds, using integer or float
@@ -62,36 +69,27 @@ public:
// all hardware timers are already in use.
template <typename period_t>
bool begin(callback_t funct, period_t period) {
uint32_t cycles = period;
return beginCycles(funct, cycles);
uint32_t microseconds = (uint32_t)period;
return beginMicros(funct, microseconds);
}
// Change the timer's interval. The current interval is completed
// as previously configured, and then the next interval begins with
// with this new setting.
void update(unsigned int microseconds) {
if (microseconds == 0 || microseconds > MAX_PERIOD) return;
timer.it_interval.tv_usec = microseconds;
setitimer (ITIMER_REAL, &timer, NULL);
_period_us.store(microseconds);
}
// Change the timer's interval. The current interval is completed
// as previously configured, and then the next interval begins with
// with this new setting.
template <typename period_t>
void update(period_t period){
uint32_t micros = microsFromPeriod(period);
timer.it_value.tv_sec = 0;
timer.it_value.tv_usec = micros;
timer.it_interval.tv_sec = 0;
timer.it_interval.tv_usec = micros;
setitimer (ITIMER_REAL, &timer, NULL);
}
void update(period_t period) {
update((unsigned int)period);
}
// Stop calling the function. The hardware timer resource becomes available
// for use by other IntervalTimer instances.
void end() {
timer.it_interval.tv_usec = 0;
setitimer (ITIMER_REAL, &timer, NULL);
}
void end();
// Set the interrupt priority level, controlling which other interrupts this
// timer is allowed to interrupt. Lower numbers are higher priority, with 0
// the highest and 255 the lowest. Most other interrupts default to 128. As
@@ -100,16 +98,14 @@ public:
void priority(uint8_t n) {
}
static std::function<void()> handler;
static void callhandler() {
if (handler != nullptr)
handler();
}
private:
struct sigaction s_action;
struct itimerval timer;
bool beginCycles(callback_t funct, uint32_t cycles);
bool beginMicros(callback_t funct, uint32_t microseconds);
void run();
callback_t _func = nullptr;
std::atomic<uint32_t> _period_us{0};
std::atomic<bool> _active{false};
std::thread _thread;
};
#endif //__INTERVALTIMER_H__
+41
View File
@@ -12,6 +12,7 @@
#include <cstdio>
#include <cstring>
#include <string>
#include <atomic>
static int g_failures = 0;
static int g_total = 0;
@@ -106,6 +107,45 @@ static void test_available_count() {
drain();
}
// IntervalTimer: the host emulation must support multiple concurrent timers
// (Teensy 4.x exposes 4 PIT channels), not just a single shared one.
static std::atomic<int> g_tick1{0}, g_tick2{0}, g_tick3{0}, g_tick4{0};
static void cb1() { ++g_tick1; }
static void cb2() { ++g_tick2; }
static void cb3() { ++g_tick3; }
static void cb4() { ++g_tick4; }
static void test_interval_timer_multiple() {
std::printf("IntervalTimer multiple concurrent timers (Teensy parity)\n");
g_tick1 = g_tick2 = g_tick3 = g_tick4 = 0;
IntervalTimer t1, t2, t3, t4, t5;
bool b1 = t1.begin(cb1, 10000); // 10 ms period
bool b2 = t2.begin(cb2, 10000);
bool b3 = t3.begin(cb3, 10000);
bool b4 = t4.begin(cb4, 10000);
bool b5 = t5.begin(cb1, 10000); // 5th must fail: only 4 channels
CHECK(b1 && b2 && b3 && b4, "four IntervalTimers start concurrently");
CHECK(!b5, "a fifth IntervalTimer is refused (max 4, like Teensy)");
delay(120); // ~12 ticks per timer
int s1 = g_tick1.load();
t1.end();
t2.end();
t3.end();
t4.end();
CHECK(g_tick1 > 0 && g_tick2 > 0 && g_tick3 > 0 && g_tick4 > 0,
"all four timer callbacks fired (timers ran in parallel)");
delay(50);
CHECK(g_tick1.load() == s1 || g_tick1.load() == s1 + 1,
"end() stops the timer (no further ticks beyond the in-flight one)");
bool b6 = t5.begin(cb2, 10000);
CHECK(b6, "a timer slot frees up after end()");
t5.end();
}
int main() {
initialize_mock_arduino();
@@ -115,6 +155,7 @@ int main() {
test_readstringuntil_max_cap();
test_write_return_value();
test_available_count();
test_interval_timer_multiple();
std::printf("\n%d/%d checks passed, %d failed\n",
g_total - g_failures, g_total, g_failures);