From a13397d7d2ef580c8b17a303ff52ab794471e0bc Mon Sep 17 00:00:00 2001 From: Adam Wonak Date: Tue, 1 Jul 2025 17:31:01 -0700 Subject: [PATCH] Add pulse output configuration. --- clock.h | 12 ++- examples/Gravity/Gravity.ino | 31 ++++++ examples/Gravity/app_state.h | 2 + examples/Gravity/display.h | 39 +++++-- examples/Gravity/save_state.cpp | 3 + examples/Gravity/save_state.h | 1 + gravity.cpp | 2 + gravity.h | 3 +- uClock/uClock.h | 180 ++++++++++++++++++++++++++++++++ 9 files changed, 257 insertions(+), 16 deletions(-) create mode 100755 uClock/uClock.h diff --git a/clock.h b/clock.h index 211f2fa..742ea46 100644 --- a/clock.h +++ b/clock.h @@ -39,6 +39,14 @@ class Clock { SOURCE_LAST, }; + enum Pulse { + PULSE_NONE, + PULSE_PPQN_1, + PULSE_PPQN_4, + PULSE_PPQN_24, + PULSE_LAST, + }; + void Init() { NeoSerial.begin(31250); @@ -175,10 +183,6 @@ class Clock { static void sendMIDIClock(uint32_t tick) { NeoSerial.write(MIDI_CLOCK); } - - static void sendPulseOut(uint32_t tick) { - digitalWrite(PULSE_OUT_PIN, !digitalRead(PULSE_OUT_PIN)); - } }; #endif \ No newline at end of file diff --git a/examples/Gravity/Gravity.ino b/examples/Gravity/Gravity.ino index 8a46749..31e3f25 100644 --- a/examples/Gravity/Gravity.ino +++ b/examples/Gravity/Gravity.ino @@ -101,6 +101,33 @@ void HandleIntClockTick(uint32_t tick) { } } + // Pulse Out gate + if (app.selected_pulse != Clock::PULSE_NONE) { + int clock_index; + switch(app.selected_pulse) { + case Clock::PULSE_PPQN_24: + clock_index = 0; + break; + case Clock::PULSE_PPQN_4: + clock_index = 4; + break; + case Clock::PULSE_PPQN_1: + clock_index = 7; + break; + } + + const uint16_t pulse_high_ticks = clock_mod_pulses[clock_index]; + const uint16_t pulse_low_ticks = tick + max((long)(pulse_high_ticks * 0.5), 1L); + + if (tick % pulse_high_ticks == 0) { + gravity.pulse.High(); + } + if (pulse_low_ticks % pulse_high_ticks == 0) { + gravity.pulse.Low(); + } + } + + if (!app.editing_param) { app.refresh_screen |= refresh; } @@ -197,6 +224,10 @@ void editMainParameter(int val) { gravity.clock.SetSource(app.selected_source); break; } + case PARAM_MAIN_PULSE: + byte pulse = static_cast(app.selected_pulse); + updateSelection(pulse, val, Clock::PULSE_LAST); + app.selected_pulse = static_cast(pulse); case PARAM_MAIN_ENCODER_DIR: updateSelection(app.selected_sub_param, val, 2); break; diff --git a/examples/Gravity/app_state.h b/examples/Gravity/app_state.h index 201ac72..062ac3c 100644 --- a/examples/Gravity/app_state.h +++ b/examples/Gravity/app_state.h @@ -16,6 +16,7 @@ struct AppState { byte selected_channel = 0; // 0=tempo, 1-6=output channel byte selected_shuffle = 0; Clock::Source selected_source = Clock::SOURCE_INTERNAL; + Clock::Pulse selected_pulse = Clock::PULSE_PPQN_24; Channel channel[Gravity::OUTPUT_COUNT]; }; @@ -28,6 +29,7 @@ static Channel& GetSelectedChannel() { enum ParamsMainPage { PARAM_MAIN_TEMPO, PARAM_MAIN_SOURCE, + PARAM_MAIN_PULSE, PARAM_MAIN_ENCODER_DIR, PARAM_MAIN_RESET_STATE, PARAM_MAIN_LAST, diff --git a/examples/Gravity/display.h b/examples/Gravity/display.h index c1dc144..ea33442 100644 --- a/examples/Gravity/display.h +++ b/examples/Gravity/display.h @@ -68,16 +68,16 @@ static const unsigned char pause_icon[28] PROGMEM = { 0x38, 0x0E, 0x00, 0x00}; // Constants for screen layout and fonts -constexpr int SCREEN_CENTER_X = 32; -constexpr int MAIN_TEXT_Y = 26; -constexpr int SUB_TEXT_Y = 40; -constexpr int VISIBLE_MENU_ITEMS = 3; -constexpr int MENU_ITEM_HEIGHT = 14; -constexpr int MENU_BOX_PADDING = 4; -constexpr int MENU_BOX_WIDTH = 64; -constexpr int CHANNEL_BOXES_Y = 50; -constexpr int CHANNEL_BOX_WIDTH = 18; -constexpr int CHANNEL_BOX_HEIGHT = 14; +constexpr uint8_t SCREEN_CENTER_X = 32; +constexpr uint8_t MAIN_TEXT_Y = 26; +constexpr uint8_t SUB_TEXT_Y = 40; +constexpr uint8_t VISIBLE_MENU_ITEMS = 3; +constexpr uint8_t MENU_ITEM_HEIGHT = 14; +constexpr uint8_t MENU_BOX_PADDING = 4; +constexpr uint8_t MENU_BOX_WIDTH = 64; +constexpr uint8_t CHANNEL_BOXES_Y = 50; +constexpr uint8_t CHANNEL_BOX_WIDTH = 18; +constexpr uint8_t CHANNEL_BOX_HEIGHT = 14; // Helper function to draw centered text void drawCenteredText(const char* text, int y, const uint8_t* font) { @@ -204,6 +204,23 @@ void DisplayMainPage() { break; } break; + case PARAM_MAIN_PULSE: + mainText = F("OUT"); + switch (app.selected_pulse) { + case Clock::PULSE_NONE: + subText = F("PULSE OFF"); + break; + case Clock::PULSE_PPQN_24: + subText = F("24 PPQN PULSE"); + break; + case Clock::PULSE_PPQN_4: + subText = F("4 PPQN PULSE"); + break; + case Clock::PULSE_PPQN_1: + subText = F("1 PPQN PULSE"); + break; + } + break; case PARAM_MAIN_ENCODER_DIR: mainText = F("DIR"); subText = app.selected_sub_param == 0 ? F("DEFAULT") : F("REVERSED"); @@ -218,7 +235,7 @@ void DisplayMainPage() { drawCenteredText(subText.c_str(), SUB_TEXT_Y, TEXT_FONT); // Draw Main Page menu items - String menu_items[PARAM_MAIN_LAST] = {F("TEMPO"), F("SOURCE"), F("ENCODER DIR"), F("RESET")}; + String menu_items[PARAM_MAIN_LAST] = {F("TEMPO"), F("SOURCE"), F("PULSE OUT"), F("ENCODER DIR"), F("RESET")}; drawMenuItems(menu_items, PARAM_MAIN_LAST); } diff --git a/examples/Gravity/save_state.cpp b/examples/Gravity/save_state.cpp index aca6977..ee374d3 100644 --- a/examples/Gravity/save_state.cpp +++ b/examples/Gravity/save_state.cpp @@ -17,6 +17,7 @@ bool StateManager::initialize(AppState& app) { app.selected_param = load_data.selected_param; app.selected_channel = load_data.selected_channel; app.selected_source = static_cast(load_data.selected_source); + app.selected_pulse = static_cast(load_data.selected_pulse); // Loop through and restore each channel's state. for (int i = 0; i < Gravity::OUTPUT_COUNT; i++) { @@ -54,6 +55,7 @@ void StateManager::reset(AppState& app) { app.selected_param = 0; app.selected_channel = 0; app.selected_source = Clock::SOURCE_INTERNAL; + app.selected_pulse = Clock::PULSE_PPQN_24; for (int i = 0; i < Gravity::OUTPUT_COUNT; i++) { app.channel[i].Init(); @@ -97,6 +99,7 @@ void StateManager::_saveState(const AppState& app) { save_data.selected_param = app.selected_param; save_data.selected_channel = app.selected_channel; save_data.selected_source = static_cast(app.selected_source); + save_data.selected_pulse = static_cast(app.selected_pulse); // Loop through and populate each channel's state for (int i = 0; i < Gravity::OUTPUT_COUNT; i++) { diff --git a/examples/Gravity/save_state.h b/examples/Gravity/save_state.h index d37a6ae..8a2659e 100644 --- a/examples/Gravity/save_state.h +++ b/examples/Gravity/save_state.h @@ -54,6 +54,7 @@ class StateManager { byte selected_param; byte selected_channel; byte selected_source; + byte selected_pulse; ChannelState channel_data[Gravity::OUTPUT_COUNT]; }; diff --git a/gravity.cpp b/gravity.cpp index 658a960..a678c40 100644 --- a/gravity.cpp +++ b/gravity.cpp @@ -52,6 +52,8 @@ void Gravity::initOutputs() { outputs[3].Init(OUT_CH4); outputs[4].Init(OUT_CH5); outputs[5].Init(OUT_CH6); + // Expansion Pulse Output + pulse.Init(PULSE_OUT_PIN); } void Gravity::initDisplay() { // OLED Display configuration. diff --git a/gravity.h b/gravity.h index 1cc424b..5701586 100644 --- a/gravity.h +++ b/gravity.h @@ -32,7 +32,8 @@ class Gravity { U8G2_SSD1306_128X64_NONAME_1_HW_I2C display; // OLED display object. Clock clock; // Clock source wrapper. DigitalOutput outputs[OUTPUT_COUNT]; // An array containing each Output object. - Encoder encoder; // Rotary encoder with button instance + DigitalOutput pulse; // MIDI Expander module pulse output. + Encoder encoder; // Rotary encoder with button instance Button shift_button; Button play_button; AnalogInput cv1; diff --git a/uClock/uClock.h b/uClock/uClock.h new file mode 100755 index 0000000..d8670b0 --- /dev/null +++ b/uClock/uClock.h @@ -0,0 +1,180 @@ +/*! + * @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 setOnSync24(void (*callback)(uint32_t tick)) { + onSync24Callback = 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 (*onSync24Callback)(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; + + uint8_t mod_sync24_counter; + uint16_t mod_sync24_ref; + uint32_t sync24_tick; + + // 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__ */