diff --git a/README.md b/README.md index e6a7fce..99e9ab5 100755 --- a/README.md +++ b/README.md @@ -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? @@ -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 diff --git a/examples/RP2040UsbUartMasterClock/RP2040UsbUartMasterClock.ino b/examples/RP2040UsbUartMasterClock/RP2040UsbUartMasterClock.ino index cf5b6eb..7faf971 100644 --- a/examples/RP2040UsbUartMasterClock/RP2040UsbUartMasterClock.ino +++ b/examples/RP2040UsbUartMasterClock/RP2040UsbUartMasterClock.ino @@ -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 diff --git a/library.json b/library.json index ffe6d19..a0ec300 100644 --- a/library.json +++ b/library.json @@ -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", diff --git a/library.properties b/library.properties index 86a95fc..32aa442 100755 --- a/library.properties +++ b/library.properties @@ -3,8 +3,8 @@ version=2.3.0 author=Romulo Silva maintainer=Romulo Silva 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 diff --git a/src/platforms/rp2040.h b/src/platforms/rp2040.h index eb918c2..e804f33 100644 --- a/src/platforms/rp2040.h +++ b/src/platforms/rp2040.h @@ -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(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); -} \ No newline at end of file + add_repeating_timer_us(-static_cast(us_interval), &handlerISR, NULL, &timer); +} diff --git a/src/uClock.cpp b/src/uClock.cpp index 73a3cf3..564797b 100755 --- a/src/uClock.cpp +++ b/src/uClock.cpp @@ -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 @@ -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) { @@ -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); + } } } } @@ -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; @@ -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()) @@ -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() diff --git a/src/uClock.h b/src/uClock.h index eeb4d9f..6640ee1 100755 --- a/src/uClock.h +++ b/src/uClock.h @@ -32,6 +32,8 @@ #include #include +#define UCLOCK_HAS_STRICT_EXTERNAL_MODE + namespace umodular { namespace clock { // Shuffle templates are specific for each PPQN output resolution @@ -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: @@ -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. @@ -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;