From 032aef79ea3c9828b5e58b466e6aab27350a1f9d Mon Sep 17 00:00:00 2001 From: Adam Wonak Date: Mon, 30 Jun 2025 12:16:18 -0700 Subject: [PATCH] vendorize uClock - include optimization changes. --- README.md | 5 +- clock.h | 6 +- uClock/platforms/avr.h | 71 ++++++++ uClock/uClock.cpp | 398 +++++++++++++++++++++++++++++++++++++++++ uClock/uClock.h | 171 ++++++++++++++++++ 5 files changed, 645 insertions(+), 6 deletions(-) create mode 100644 uClock/platforms/avr.h create mode 100755 uClock/uClock.cpp create mode 100755 uClock/uClock.h diff --git a/README.md b/README.md index 58d0e58..f244487 100644 --- a/README.md +++ b/README.md @@ -14,10 +14,9 @@ Common directory locations: ## Required Third-party Libraries -* [uClock](https://github.com/midilab/uClock) [MIT] - Handle clock tempo, external clock input, and internal clock timer handler. +* [uClock](https://github.com/midilab/uClock) [MIT] - (Included with this repo) Handle clock tempo, external clock input, and internal clock timer handler. * [RotateEncoder](https://github.com/mathertel/RotaryEncoder) [BSD] - Library for reading and interpreting encoder rotation. -* [Adafruit_GFX](https://github.com/adafruit/Adafruit-GFX-Library) [BSD] - Graphics helper library. -* [Adafruit_SSD1306](https://github.com/adafruit/Adafruit_SSD1306) [BSD] - Library for interacting with the SSD1306 OLED display. +* [U8g2](https://github.com/olikraus/u8g2/) [MIT] - Graphics helper library. ## Example diff --git a/clock.h b/clock.h index 19ed250..5402828 100644 --- a/clock.h +++ b/clock.h @@ -54,8 +54,8 @@ class Clock { // MIDI events. uClock.setOnClockStart(sendMIDIStart); uClock.setOnClockStop(sendMIDIStop); - uClock.setOnSync24(sendMIDIClock); - uClock.setOnSync48(sendPulseOut); + // uClock.setOnSync24(sendMIDIClock); + // uClock.setOnSync48(sendPulseOut); uClock.start(); } @@ -75,7 +75,7 @@ class Clock { void SetSource(Source source) { bool was_playing = !IsPaused(); uClock.stop(); - // If source is currently MIDI, disable the serial interrupt handler. + // If we are changing the source from MIDI, disable the serial interrupt handler. if (source_ == SOURCE_EXTERNAL_MIDI) { NeoSerial.attachInterrupt(serialEventNoop); } diff --git a/uClock/platforms/avr.h b/uClock/platforms/avr.h new file mode 100644 index 0000000..9931dac --- /dev/null +++ b/uClock/platforms/avr.h @@ -0,0 +1,71 @@ +#include + +#define ATOMIC(X) noInterrupts(); X; interrupts(); + +// want a different avr clock support? +// TODO: we should do this using macro guards for avrs different clocks freqeuncy setup at compile time +#define AVR_CLOCK_FREQ 16000000 + +// forward declaration of uClockHandler +void uClockHandler(); + +// AVR ISR Entrypoint +ISR(TIMER1_COMPA_vect) +{ + uClockHandler(); +} + +void initTimer(uint32_t init_clock) +{ + ATOMIC( + // 16bits Timer1 init + // begin at 120bpm (48.0007680122882 Hz) + TCCR1A = 0; // set entire TCCR1A register to 0 + TCCR1B = 0; // same for TCCR1B + TCNT1 = 0; // initialize counter value to 0 + // set compare match register for 48.0007680122882 Hz increments + OCR1A = 41665; // = 16000000 / (8 * 48.0007680122882) - 1 (must be <65536) + // turn on CTC mode + TCCR1B |= (1 << WGM12); + // Set CS12, CS11 and CS10 bits for 8 prescaler + TCCR1B |= (0 << CS12) | (1 << CS11) | (0 << CS10); + // enable timer compare interrupt + TIMSK1 |= (1 << OCIE1A); + ) +} + +void setTimer(uint32_t us_interval) +{ + float tick_hertz_interval = 1/((float)us_interval/1000000); + + uint32_t ocr; + uint8_t tccr = 0; + + // 16bits avr timer setup + if ((ocr = AVR_CLOCK_FREQ / ( tick_hertz_interval * 1 )) < 65535) { + // Set CS12, CS11 and CS10 bits for 1 prescaler + tccr |= (0 << CS12) | (0 << CS11) | (1 << CS10); + } else if ((ocr = AVR_CLOCK_FREQ / ( tick_hertz_interval * 8 )) < 65535) { + // Set CS12, CS11 and CS10 bits for 8 prescaler + tccr |= (0 << CS12) | (1 << CS11) | (0 << CS10); + } else if ((ocr = AVR_CLOCK_FREQ / ( tick_hertz_interval * 64 )) < 65535) { + // Set CS12, CS11 and CS10 bits for 64 prescaler + tccr |= (0 << CS12) | (1 << CS11) | (1 << CS10); + } else if ((ocr = AVR_CLOCK_FREQ / ( tick_hertz_interval * 256 )) < 65535) { + // Set CS12, CS11 and CS10 bits for 256 prescaler + tccr |= (1 << CS12) | (0 << CS11) | (0 << CS10); + } else if ((ocr = AVR_CLOCK_FREQ / ( tick_hertz_interval * 1024 )) < 65535) { + // Set CS12, CS11 and CS10 bits for 1024 prescaler + tccr |= (1 << CS12) | (0 << CS11) | (1 << CS10); + } else { + // tempo not achiavable + return; + } + + ATOMIC( + TCCR1B = 0; + OCR1A = ocr-1; + TCCR1B |= (1 << WGM12); + TCCR1B |= tccr; + ) +} \ No newline at end of file diff --git a/uClock/uClock.cpp b/uClock/uClock.cpp new file mode 100755 index 0000000..9991004 --- /dev/null +++ b/uClock/uClock.cpp @@ -0,0 +1,398 @@ +/*! + * @file uClock.cpp + * Project BPM clock generator for Arduino + * @brief 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(RPI2040, Teensy, Seedstudio XIAO M0 and ESP32) + * @version 2.2.1 + * @author Romulo Silva + * @date 10/06/2017 + * @license MIT - (c) 2024 - Romulo Silva - contact@midilab.co + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ +#include "uClock.h" +#include "platforms/avr.h" + +// +// Platform specific timer setup/control +// +// initTimer(uint32_t us_interval) and setTimer(uint32_t us_interval) +// are called from architecture specific module included at the +// header of this file +void uclockInitTimer() +{ + // begin at 120bpm + initTimer(uClock.bpmToMicroSeconds(120.00)); +} + +void setTimerTempo(float bpm) +{ + setTimer(uClock.bpmToMicroSeconds(bpm)); +} + +namespace umodular { namespace clock { + +static inline uint32_t phase_mult(uint32_t val) +{ + return (val * PHASE_FACTOR) >> 8; +} + +static inline uint32_t clock_diff(uint32_t old_clock, uint32_t new_clock) +{ + if (new_clock >= old_clock) { + return new_clock - old_clock; + } else { + return new_clock + (4294967295 - old_clock); + } +} + +uClockClass::uClockClass() +{ + tempo = 120; + start_timer = 0; + last_interval = 0; + sync_interval = 0; + clock_state = PAUSED; + clock_mode = INTERNAL_CLOCK; + resetCounters(); + + onOutputPPQNCallback = nullptr; + onClockStartCallback = nullptr; + onClockStopCallback = nullptr; + // initialize reference data + calculateReferencedata(); +} + +void uClockClass::init() +{ + if (ext_interval_buffer == nullptr) + setExtIntervalBuffer(1); + + uclockInitTimer(); + // first interval calculus + setTempo(tempo); +} + +uint32_t uClockClass::bpmToMicroSeconds(float bpm) +{ + return (60000000.0f / (float)output_ppqn / bpm); +} + +void uClockClass::calculateReferencedata() +{ + mod_clock_ref = output_ppqn / input_ppqn; +} + +void uClockClass::setOutputPPQN(PPQNResolution resolution) +{ + // dont allow PPQN lower than PPQN_4 for output clock (to avoid problems with mod_step_ref) + if (resolution < PPQN_4) + return; + + ATOMIC( + output_ppqn = resolution; + calculateReferencedata(); + ) +} + +void uClockClass::setInputPPQN(PPQNResolution resolution) +{ + ATOMIC( + input_ppqn = resolution; + calculateReferencedata(); + ) +} + +void uClockClass::start() +{ + resetCounters(); + start_timer = millis(); + + if (onClockStartCallback) { + onClockStartCallback(); + } + + if (clock_mode == INTERNAL_CLOCK) { + clock_state = STARTED; + } else { + clock_state = STARTING; + } +} + +void uClockClass::stop() +{ + clock_state = PAUSED; + start_timer = 0; + resetCounters(); + if (onClockStopCallback) { + onClockStopCallback(); + } +} + +void uClockClass::pause() +{ + if (clock_mode == INTERNAL_CLOCK) { + if (clock_state == PAUSED) { + start(); + } else { + stop(); + } + } +} + +void uClockClass::setTempo(float bpm) +{ + if (clock_mode == EXTERNAL_CLOCK) { + return; + } + + if (bpm < MIN_BPM || bpm > MAX_BPM) { + return; + } + + ATOMIC( + tempo = bpm + ) + + setTimerTempo(bpm); +} + +float uClockClass::getTempo() +{ + if (clock_mode == EXTERNAL_CLOCK) { + uint32_t acc = 0; + // wait the buffer to get full + if (ext_interval_buffer[ext_interval_buffer_size-1] == 0) { + return tempo; + } + for (uint8_t i=0; i < ext_interval_buffer_size; i++) { + acc += ext_interval_buffer[i]; + } + if (acc != 0) { + return constrainBpm(freqToBpm(acc / ext_interval_buffer_size)); + } + } + return tempo; +} + +// for software timer implementation(fallback for no board support) +void uClockClass::run() +{ +#if !defined(UCLOCK_PLATFORM_FOUND) + // call software timer implementation of software + softwareTimerHandler(micros()); +#endif +} + +float inline uClockClass::freqToBpm(uint32_t freq) +{ + return 60000000.0f / (float)(freq * input_ppqn); +} + +float inline uClockClass::constrainBpm(float bpm) +{ + return (bpm < MIN_BPM) ? MIN_BPM : ( bpm > MAX_BPM ? MAX_BPM : bpm ); +} + +void uClockClass::setClockMode(ClockMode tempo_mode) +{ + clock_mode = tempo_mode; +} + +uClockClass::ClockMode uClockClass::getClockMode() +{ + return clock_mode; +} + +void uClockClass::clockMe() +{ + if (clock_mode == EXTERNAL_CLOCK) { + ATOMIC( + handleExternalClock() + ) + } +} + +void uClockClass::setExtIntervalBuffer(uint8_t buffer_size) +{ + if (ext_interval_buffer != nullptr) + return; + + // alloc once and forever policy + ext_interval_buffer_size = buffer_size; + ext_interval_buffer = (uint32_t*) malloc( sizeof(uint32_t) * ext_interval_buffer_size ); +} + +void uClockClass::resetCounters() +{ + tick = 0; + int_clock_tick = 0; + mod_clock_counter = 0; + ext_clock_tick = 0; + ext_clock_us = 0; + ext_interval_idx = 0; + + for (uint8_t i=0; i < ext_interval_buffer_size; i++) { + ext_interval_buffer[i] = 0; + } +} + +void uClockClass::handleExternalClock() +{ + switch (clock_state) { + case PAUSED: + break; + + case STARTING: + clock_state = STARTED; + ext_clock_us = micros(); + break; + + case STARTED: + uint32_t now_clock_us = micros(); + last_interval = clock_diff(ext_clock_us, now_clock_us); + ext_clock_us = now_clock_us; + + // external clock tick me! + ext_clock_tick++; + + // accumulate interval incomming ticks data for getTempo() smooth reads on slave clock_mode + if(++ext_interval_idx >= ext_interval_buffer_size) { + ext_interval_idx = 0; + } + ext_interval_buffer[ext_interval_idx] = last_interval; + + if (ext_clock_tick == 1) { + ext_interval = last_interval; + } else { + ext_interval = (((uint32_t)ext_interval * (uint32_t)PLL_X) + (uint32_t)(256 - PLL_X) * (uint32_t)last_interval) >> 8; + } + break; + } +} + +void uClockClass::handleTimerInt() +{ + // track main input clock counter + if (mod_clock_counter == mod_clock_ref) + mod_clock_counter = 0; + + // process sync signals first please... + if (mod_clock_counter == 0) { + + if (clock_mode == EXTERNAL_CLOCK) { + // sync tick position with external tick clock + if ((int_clock_tick < ext_clock_tick) || (int_clock_tick > (ext_clock_tick + 1))) { + int_clock_tick = ext_clock_tick; + tick = int_clock_tick * mod_clock_ref; + mod_clock_counter = tick % mod_clock_ref; + } + + uint32_t counter = ext_interval; + uint32_t now_clock_us = micros(); + sync_interval = clock_diff(ext_clock_us, now_clock_us); + + if (int_clock_tick <= ext_clock_tick) { + counter -= phase_mult(sync_interval); + } else { + if (counter > sync_interval) { + counter += phase_mult(counter - sync_interval); + } + } + + // update internal clock timer frequency + float bpm = constrainBpm(freqToBpm(counter)); + if (bpm != tempo) { + tempo = bpm; + setTimerTempo(bpm); + } + } + + // internal clock tick me! + ++int_clock_tick; + } + ++mod_clock_counter; + + // main PPQNCallback + if (onOutputPPQNCallback) { + onOutputPPQNCallback(tick); + ++tick; + } +} + +// elapsed time support +uint8_t uClockClass::getNumberOfSeconds(uint32_t time) +{ + if ( time == 0 ) { + return time; + } + return ((_millis - time) / 1000) % SECS_PER_MIN; +} + +uint8_t uClockClass::getNumberOfMinutes(uint32_t time) +{ + if ( time == 0 ) { + return time; + } + return (((_millis - time) / 1000) / SECS_PER_MIN) % SECS_PER_MIN; +} + +uint8_t uClockClass::getNumberOfHours(uint32_t time) +{ + if ( time == 0 ) { + return time; + } + return (((_millis - time) / 1000) % SECS_PER_DAY) / SECS_PER_HOUR; +} + +uint8_t uClockClass::getNumberOfDays(uint32_t time) +{ + if ( time == 0 ) { + return time; + } + return ((_millis - time) / 1000) / SECS_PER_DAY; +} + +uint32_t uClockClass::getNowTimer() +{ + return _millis; +} + +uint32_t uClockClass::getPlayTime() +{ + return start_timer; +} + +} } // end namespace umodular::clock + +umodular::clock::uClockClass uClock; + +volatile uint32_t _millis = 0; + +// +// TIMER HANDLER +// +void uClockHandler() +{ + // global timer counter + _millis = millis(); + + if (uClock.clock_state == uClock.STARTED) { + uClock.handleTimerInt(); + } +} diff --git a/uClock/uClock.h b/uClock/uClock.h new file mode 100755 index 0000000..6f9a453 --- /dev/null +++ b/uClock/uClock.h @@ -0,0 +1,171 @@ +/*! + * @file uClock.h + * Project BPM clock generator for Arduino + * @brief 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(RPI2040, Teensy, Seedstudio XIAO M0 and ESP32) + * @version 2.2.1 + * @author Romulo Silva + * @date 10/06/2017 + * @license MIT - (c) 2024 - Romulo Silva - contact@midilab.co + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef __U_CLOCK_H__ +#define __U_CLOCK_H__ + +#include +#include + +namespace umodular { namespace clock { + +#define MIN_BPM 1 +#define MAX_BPM 400 + +#define PHASE_FACTOR 16 +#define PLL_X 220 + +#define SECS_PER_MIN (60UL) +#define SECS_PER_HOUR (3600UL) +#define SECS_PER_DAY (SECS_PER_HOUR * 24L) + +class uClockClass { + + public: + enum ClockMode { + INTERNAL_CLOCK = 0, + EXTERNAL_CLOCK + }; + + enum ClockState { + PAUSED = 0, + STARTING, + STARTED + }; + + enum PPQNResolution { + PPQN_1 = 1, + PPQN_2 = 2, + PPQN_4 = 4, + PPQN_8 = 8, + PPQN_12 = 12, + PPQN_24 = 24, + PPQN_48 = 48, + PPQN_96 = 96, + PPQN_384 = 384, + PPQN_480 = 480, + PPQN_960 = 960 + }; + + ClockState clock_state; + + uClockClass(); + + void setOnOutputPPQN(void (*callback)(uint32_t tick)) { + onOutputPPQNCallback = callback; + } + + void setOnClockStart(void (*callback)()) { + onClockStartCallback = callback; + } + + void setOnClockStop(void (*callback)()) { + onClockStopCallback = callback; + } + + void init(); + void setOutputPPQN(PPQNResolution resolution); + void setInputPPQN(PPQNResolution resolution); + + void handleTimerInt(); + void handleExternalClock(); + void resetCounters(); + + // external class control + void start(); + void stop(); + void pause(); + void setTempo(float bpm); + float getTempo(); + + // for software timer implementation(fallback for no board support) + void run(); + + // external timming control + void setClockMode(ClockMode tempo_mode); + ClockMode getClockMode(); + void clockMe(); + // for smooth slave tempo calculate display you should raise the + // buffer_size of ext_interval_buffer in between 64 to 128. 254 max size. + // note: this doesn't impact on sync time, only display time getTempo() + // if you dont want to use it, it is default set it to 1 for memory save + void setExtIntervalBuffer(uint8_t buffer_size); + + // elapsed time support + uint8_t getNumberOfSeconds(uint32_t time); + uint8_t getNumberOfMinutes(uint32_t time); + uint8_t getNumberOfHours(uint32_t time); + uint8_t getNumberOfDays(uint32_t time); + uint32_t getNowTimer(); + uint32_t getPlayTime(); + + uint32_t bpmToMicroSeconds(float bpm); + + private: + float inline freqToBpm(uint32_t freq); + float inline constrainBpm(float bpm); + void calculateReferencedata(); + + void (*onOutputPPQNCallback)(uint32_t tick); + void (*onClockStartCallback)(); + void (*onClockStopCallback)(); + + // clock input/output control + PPQNResolution output_ppqn = PPQN_96; + PPQNResolution input_ppqn = PPQN_24; + // output and internal counters, ticks and references + uint32_t tick; + uint32_t int_clock_tick; + uint8_t mod_clock_counter; + uint16_t mod_clock_ref; + + // external clock control + volatile uint32_t ext_clock_us; + volatile uint32_t ext_clock_tick; + volatile uint32_t ext_interval; + uint32_t last_interval; + uint32_t sync_interval; + + float tempo; + uint32_t start_timer; + ClockMode clock_mode; + + volatile uint32_t * ext_interval_buffer = nullptr; + uint8_t ext_interval_buffer_size; + uint16_t ext_interval_idx; +}; + +} } // end namespace umodular::clock + +extern umodular::clock::uClockClass uClock; + +extern "C" { + extern volatile uint32_t _millis; +} + +#endif /* __U_CLOCK_H__ */