Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
9f0f79f
believe to be working prototype of 'strict external clock' mode
doctea Aug 1, 2025
1dde239
moved 'allowTick' check into a class method, setStrictExternalMode(tr…
doctea Aug 1, 2025
405f5fa
move int_clock_tick+ext_clock_tick back into private
doctea Aug 1, 2025
2c37651
Basically works -- but external USB devices are slipping out of time...
doctea Aug 4, 2025
e178623
think fixed 'external midi devices slip out of time' problem?
doctea Aug 4, 2025
47bd015
spacing
doctea Aug 4, 2025
917c103
abs->labs for RP2040 compilability
doctea Aug 4, 2025
638a025
Merge branch 'feature-strict-external-regress' into last-known-good-m…
doctea Aug 17, 2025
ce704cf
Merge branch 'develop' into feature-strict-external-develop
doctea Sep 1, 2025
32d2753
add UCLOCK_HAS_STRICT_EXTERNAL_MODE define so projects don't need cha…
doctea Sep 1, 2025
f18d1ee
Merge branch 'feature-strict-external-regress' into feature-strict-ex…
doctea Sep 1, 2025
84f0f64
fix missing ; in get***OverflowCounter() functions
doctea Sep 1, 2025
e2fbae1
Merge pull request #60 from midilab/develop
midilab Nov 8, 2025
2376848
make MINIMUM_SYNC_COUNTER more easily configurable
doctea Nov 8, 2025
8d4cb27
Merge branch 'main' into feature-strict-external-develop
doctea Nov 8, 2025
6cf39cc
add breakchanges for v2.3+
midilab Nov 10, 2025
1860f4a
Merge branch 'main' into feature-strict-external-develop
doctea Nov 24, 2025
17bb8ec
RP2350 (Pico 2) support
doctea Feb 11, 2026
1063787
Merge branch 'main' into feature-strict-external-develop
doctea Feb 11, 2026
d06ff1a
FIX: timer initialization on RP2040/RP2350 -- BPM will now be accurate
doctea Apr 15, 2026
c7de253
(fix typo in previous commit)
doctea Apr 15, 2026
7d2d9fa
Merge remote-tracking branch 'origin/main' into feature-strict-extern…
doctea Apr 15, 2026
63f7234
use static_cast
doctea Apr 16, 2026
6482c32
Merge remote-tracking branch 'origin/main' into feature-strict-extern…
doctea Apr 16, 2026
d99f175
perhaps better at syncing
doctea Apr 17, 2026
44ed9de
attempt to stick rigidly to external clock while still syncing; does …
doctea Apr 17, 2026
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
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ The absence of real-time features necessary for creating professional-level embe
- **AVR**: ATmega168/328, ATmega16u4/32u4, ATmega2560
- **ARM**: Teensy (all versions), STM32XX, Seeed Studio XIAO M0
- **ESP32**: All ESP32 family boards
- **RP2040**: Raspberry Pi Pico and compatible boards
- **RP2040/RP2350**: Raspberry Pico, Pico 2, and compatible boards

## Why uClock?

Expand Down Expand Up @@ -534,16 +534,19 @@ void loop() {

⚠️ **Note**: Software timer mode provides less accurate timing than hardware interrupts.

## Migration Guide (v1.x → v2.0)
## Migration Guide (v1.x → v2.3)

### Breaking Changes

| Old API (v1.x) | New API (v2.0+) |
| Old API (v1.x) | New API (v2.3+) |
|----------------|-----------------|
| `setClock96PPQNOutput()` | `setOnOutputPPQN()` |
| `setClock16PPQNOutput()` | `setOnStep()` |
| `setOnClockStartOutput()` | `setOnClockStart()` |
| `setOnClockStopOutput()` | `setOnClockStop()` |
| `setOnSync24()` | `setOnSync(uClock.PPQN_24, onSync24)` |
| `setOnSync48()` | `setOnSync(uClock.PPQN_48, onSync48)` |
| `setOnSyncXX()` | `setOnSync(uClock.PPQN_XX, onSyncXX)` |

### Resolution Changes

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ void onClockStop() {
}

void setup() {
#if defined(ARDUINO_ARCH_MBED) && defined(ARDUINO_ARCH_RP2040)
#if defined(ARDUINO_ARCH_MBED) && (defined(ARDUINO_ARCH_RP2040) || defined(ARDUINO_ARCH_RP2350))
// Manual begin() is required on core without built-in support for TinyUSB such as mbed rp2040
TinyUSB_Device_Init(0);
#endif
Expand Down
2 changes: 1 addition & 1 deletion library.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "uClock",
"version": "2.3.0",
"description": "A Library to implement BPM clock tick calls using hardware interruption. Supported and tested on AVR boards(ATmega168/328, ATmega16u4/32u4 and ATmega2560) and ARM boards(Teensy, STM32XX, ESP32, Raspberry Pico, Seedstudio XIAO M0 and RP2040)",
"description": "A Library to implement BPM clock tick calls using hardware interruption. Supported and tested on AVR boards(ATmega168/328, ATmega16u4/32u4 and ATmega2560) and ARM boards(Teensy, STM32XX, ESP32, Raspberry Pico, Seedstudio XIAO M0 and RP2040/RP2350)",
"keywords": "bpm, clock, timing, tick, music, generator",
"repository": {
"type": "git",
Expand Down
4 changes: 2 additions & 2 deletions library.properties
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ version=2.3.0
author=Romulo Silva <contact@midilab.co>
maintainer=Romulo Silva <contact@midilab.co>
sentence=BPM clock generator for Arduino platform.
paragraph=A Library to implement BPM clock tick calls using hardware interruption. Supported and tested on AVR boards(ATmega168/328, ATmega16u4/32u4 and ATmega2560) and ARM boards(Teensy, STM32XX, ESP32, Raspberry Pico, Seedstudio XIAO M0 and RP2040)
paragraph=A Library to implement BPM clock tick calls using hardware interruption. Supported and tested on AVR boards(ATmega168/328, ATmega16u4/32u4 and ATmega2560) and ARM boards(Teensy, STM32XX, ESP32, Raspberry Pico, Seedstudio XIAO M0 and RP2040/RP2350)
category=Timing
url=https://github.com/midilab/uClock
architectures=avr,arm,samd,stm32,esp32,rp2040
architectures=avr,arm,samd,stm32,esp32,rp2040,rp2350
includes=uClock.h
8 changes: 3 additions & 5 deletions src/platforms/rp2040.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,10 @@ bool handlerISR(repeating_timer *timer)

void initTimer(uint32_t init_clock) {
// set up RPi interrupt timer
// todo: actually should be -init_clock so that timer is set to start init_clock us after last tick, instead of init_clock us after finished processing last tick!
add_repeating_timer_us(init_clock, &handlerISR, NULL, &timer);
add_repeating_timer_us(-static_cast<int32_t>(init_clock), &handlerISR, NULL, &timer);
}

void setTimer(uint32_t us_interval) {
cancel_repeating_timer(&timer);
// todo: actually should be -us_interval so that timer is set to start init_clock us after last tick, instead of init_clock us after finished processing last tick!
add_repeating_timer_us(us_interval, &handlerISR, NULL, &timer);
}
add_repeating_timer_us(-static_cast<int32_t>(us_interval), &handlerISR, NULL, &timer);
}
117 changes: 94 additions & 23 deletions src/uClock.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,9 @@
#define UCLOCK_PLATFORM_FOUND
#endif
//
// RP2040 (Raspberry Pico) family
// RP2040 and RP2350 (Raspberry Pico and Pico 2) family
//
#if defined(ARDUINO_ARCH_RP2040)
#if defined(ARDUINO_ARCH_RP2040) || defined(ARDUINO_ARCH_RP2350)
#include "platforms/rp2040.h"
#define UCLOCK_PLATFORM_FOUND
#endif
Expand Down Expand Up @@ -165,10 +165,23 @@ void uClockClass::handleInternalClock()
if (clock_state <= STARTING) // STOPED=0, PAUSED=1, STARTING=2, SYNCING=3, STARTED=4
return;

// Watchdog: stop musical clock if no external pulse received for 1 second
if (clock_mode == EXTERNAL_CLOCK && clock_state == STARTED && ext_clock_us > 0) {
if (clock_diff(ext_clock_us, micros()) > 1000000UL) {
clock_state = PAUSED;
return;
}
}

// tick phase lock and external tempo match for EXTERNAL_CLOCK mode
if (clock_mode == EXTERNAL_CLOCK) {
// Tick Phase-lock
if (labs(int_clock_tick - ext_clock_tick) > 1) {

// check for strict external mode -- don't progress if external clock hasn't caught up with internal clock
if (!uClock.allowTick())
return;

// only update tick at a full quarter or phase_lock_quarters * a quarter
// how many quarters to count until we phase-lock?
if ((ext_clock_tick * mod_clock_ref) % (output_ppqn*phase_lock_quarters) == 0) {
Expand All @@ -189,24 +202,34 @@ void uClockClass::handleInternalClock()
}
}

// any external interval avaliable to start sync timer?
if (ext_interval > 0) {
counter = ext_interval;
sync_interval = clock_diff(ext_clock_us, micros());

// phase-multiplier interval
if (int_clock_tick <= ext_clock_tick) {
counter -= (sync_interval * PHASE_FACTOR) >> 8;
} else {
if (counter > sync_interval) {
counter += ((counter - sync_interval) * PHASE_FACTOR) >> 8;
// use buffer average for stable tempo estimation; raw ext_interval can be corrupted by USB bursts
{
uint32_t avg_interval = 0;
uint8_t valid = 0;
for (uint8_t i = 0; i < ext_interval_buffer_size; i++) {
if (ext_interval_buffer[i] > 0) {
avg_interval += ext_interval_buffer[i];
valid++;
}
}
if (valid > 0) {
counter = avg_interval / valid;
sync_interval = clock_diff(ext_clock_us, micros());

// phase-multiplier interval
if (int_clock_tick <= ext_clock_tick) {
counter -= (sync_interval * PHASE_FACTOR) >> 8;
} else {
if (counter > sync_interval) {
counter += ((counter - sync_interval) * PHASE_FACTOR) >> 8;
}
}

external_tempo = constrainBpm(freqToBpm(counter));
if (external_tempo != tempo) {
tempo = external_tempo;
uClockSetTimerTempo(tempo);
external_tempo = constrainBpm(freqToBpm(counter));
if (external_tempo != tempo) {
tempo = external_tempo;
uClockSetTimerTempo(tempo);
}
}
}
}
Expand Down Expand Up @@ -268,15 +291,46 @@ void uClockClass::handleExternalClock()
switch (clock_state) {
case STARTING:
clock_state = SYNCING;
start_sync_counter = 4;
start_sync_counter = MINIMUM_SYNC_COUNTER;
break;
case SYNCING:
if (--start_sync_counter == 0)
// Accumulate valid intervals during SYNCING so the PLL buffer has real
// data by the time we reach STARTED.
if (ext_interval >= (60000000UL / input_ppqn / MAX_BPM)) {
ext_interval_buffer[ext_interval_idx] = ext_interval;
if (++ext_interval_idx >= ext_interval_buffer_size)
ext_interval_idx = 0;
}
if (--start_sync_counter == 0) {
// Force-align all internal counters to ext_clock_tick, which is
// always the canonical song position. The existing phase-lock only
// snaps on beat boundaries, which means without this we can start
// up to a full beat out of alignment.
tick = ext_clock_tick * mod_clock_ref;
int_clock_tick = ext_clock_tick;
mod_clock_counter = 0;
for (uint8_t track = 0; track < track_slots_size; track++) {
tracks[track].step_counter = tick / mod_step_ref;
tracks[track].mod_step_counter = 0;
}
for (uint8_t i = 0; i < sync_callback_size; i++) {
if (sync_callbacks[i].callback) {
sync_callbacks[i].tick = tick / sync_callbacks[i].sync_ref;
sync_callbacks[i].mod_counter = 0;
}
}
// Prime the timer to the correct BPM immediately.
if (ext_interval >= (60000000UL / input_ppqn / MAX_BPM)) {
tempo = constrainBpm(freqToBpm(ext_interval));
uClockSetTimerTempo(tempo);
}
clock_state = STARTED;
}
break;
default:
// accumulate interval incomming ticks data for getTempo() smooth reads on slave clock_mode
if (ext_interval > 0) {
// accumulate interval incoming ticks data for getTempo() smooth reads on slave clock_mode
// reject intervals shorter than the minimum valid period at MAX_BPM (filters USB burst packets)
if (ext_interval >= (60000000UL / input_ppqn / MAX_BPM)) {
ext_interval_buffer[ext_interval_idx] = ext_interval;
if(++ext_interval_idx >= ext_interval_buffer_size)
ext_interval_idx = 0;
Expand All @@ -293,6 +347,23 @@ void uClockClass::clockMe()
ATOMIC(handleExternalClock())
}

void uClockClass::setStrictExternalMode(bool strict)
{
strict_external_mode = strict;
}
bool uClockClass::isStrictExternalMode()
{
return strict_external_mode;
}
bool uClockClass::allowTick()
{
if (getClockMode()==ClockMode::EXTERNAL_CLOCK && isStrictExternalMode())
// in strict mode and external, so only allow internal clock to tick if external clock has already been received
return ext_clock_tick > int_clock_tick;
// in internal clock mode or non-strict external clock mode, always allow internal clock to tick
return true;
}

void uClockClass::start()
{
ATOMIC(resetCounters())
Expand Down Expand Up @@ -629,8 +700,8 @@ void uClockClass::resetCounters()
}

// external bpm read buffer
//for (uint8_t i=0; i < ext_interval_buffer_size; i++)
// ext_interval_buffer[i] = 0;
for (uint8_t i=0; i < ext_interval_buffer_size; i++)
ext_interval_buffer[i] = 0;
}

void uClockClass::tap()
Expand Down
14 changes: 13 additions & 1 deletion src/uClock.h
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@
#include <Arduino.h>
#include <inttypes.h>

#define UCLOCK_HAS_STRICT_EXTERNAL_MODE

namespace umodular { namespace clock {

// Shuffle templates are specific for each PPQN output resolution
Expand All @@ -50,6 +52,10 @@ namespace umodular { namespace clock {
#define SECS_PER_HOUR (3600UL)
#define SECS_PER_DAY (SECS_PER_HOUR * 24L)

#ifndef MINIMUM_SYNC_COUNTER
#define MINIMUM_SYNC_COUNTER 4
#endif

class uClockClass {

public:
Expand Down Expand Up @@ -164,10 +170,15 @@ class uClockClass {
// for software timer implementation(fallback for no board support)
void run();

// external timming control
// external timing control
void setClockMode(ClockMode tempo_mode);
ClockMode getClockMode();
void clockMe();

// strict external clock mode functions
bool allowTick();
void setStrictExternalMode(bool strict);
bool isStrictExternalMode();
void setPhaseLockQuartersCount(uint8_t count);
// for smooth slave tempo calculate display you should raise the
// buffer_size of ext_interval_buffer in between 64 to 128. 254 max size.
Expand Down Expand Up @@ -251,6 +262,7 @@ class uClockClass {
volatile float tempo = 120.0;
volatile ClockMode clock_mode = INTERNAL_CLOCK;
uint32_t start_timer = 0;
bool strict_external_mode = false;

// output and internal counters, ticks and references
volatile uint32_t tick = 0;
Expand Down