From c6ce5b130915d2aa6896700ade8983de21ed95f0 Mon Sep 17 00:00:00 2001 From: Adam Wonak Date: Wed, 10 Sep 2025 11:54:51 -0700 Subject: [PATCH] testing firmware on new Nano R4. --- examples/test_r4/encoder.h | 118 ++++++++++++++++++++ examples/test_r4/test_r4.ino | 76 +++++++++++++ src/clock.h | 54 --------- src/clock_midi.h | 197 +++++++++++++++++++++++++++++++++ src/encoder.h | 2 +- src/libGravity.cpp | 12 ++ src/peripherials.h | 4 +- src/uClock/platforms/renesas.h | 72 ++++++++++++ src/uClock/uClock.cpp | 6 + 9 files changed, 484 insertions(+), 57 deletions(-) create mode 100644 examples/test_r4/encoder.h create mode 100644 examples/test_r4/test_r4.ino create mode 100644 src/clock_midi.h create mode 100644 src/uClock/platforms/renesas.h diff --git a/examples/test_r4/encoder.h b/examples/test_r4/encoder.h new file mode 100644 index 0000000..b1e9bce --- /dev/null +++ b/examples/test_r4/encoder.h @@ -0,0 +1,118 @@ +/** + * @file encoder.h + * @author Adam Wonak (https://github.com/awonak) + * @brief Class for interacting with encoders. + * @version 2.0.0 + * @date 2025-08-17 + * + * @copyright MIT - (c) 2025 - Adam Wonak - adam.wonak@gmail.com + * + */ +#ifndef ENCODER_DIR_H +#define ENCODER_DIR_H + +#include + +#include "button.h" +#include "peripherials.h" + +class Encoder { + protected: + typedef void (*CallbackFunction)(void); + typedef void (*RotateCallbackFunction)(int val); + CallbackFunction on_press; + RotateCallbackFunction on_press_rotate; + RotateCallbackFunction on_rotate; + int change; + + public: + Encoder() : encoder_(ENCODER_PIN1, ENCODER_PIN2, RotaryEncoder::LatchMode::FOUR3), + button_(ENCODER_SW_PIN) { + _instance = this; + } + ~Encoder() {} + + // Set to true if the encoder read direction should be reversed. + void SetReverseDirection(bool reversed) { + reversed_ = reversed; + } + void AttachPressHandler(CallbackFunction f) { + on_press = f; + } + + void AttachRotateHandler(RotateCallbackFunction f) { + on_rotate = f; + } + + void AttachPressRotateHandler(RotateCallbackFunction f) { + on_press_rotate = f; + } + + void Process() { + encoder_.tick(); + // Get encoder position change amount. + int encoder_rotated = _rotate_change() != 0; + bool button_pressed = button_.On(); + button_.Process(); + + // Handle encoder position change and button press. + if (button_pressed && encoder_rotated) { + rotated_while_held_ = true; + if (on_press_rotate != NULL) on_press_rotate(change); + } else if (!button_pressed && encoder_rotated) { + if (on_rotate != NULL) on_rotate(change); + } else if (button_.Change() == Button::CHANGE_RELEASED && !rotated_while_held_) { + if (on_press != NULL) on_press(); + } + + // Reset rotate while held state. + if (button_.Change() == Button::CHANGE_RELEASED && rotated_while_held_) { + rotated_while_held_ = false; + } + } + + static void isr() { + // If the instance has been created, call its tick() method. + if (_instance) { + _instance->encoder_.tick(); + } + } + + private: + static Encoder* _instance; + + int previous_pos_; + bool rotated_while_held_; + bool reversed_ = false; + RotaryEncoder encoder_; + Button button_; + + // Return the number of ticks change since last polled. + int _rotate_change() { + int position = encoder_.getPosition(); + unsigned long ms = encoder_.getMillisBetweenRotations(); + + // Validation (TODO: add debounce check). + if (previous_pos_ == position) { + return 0; + } + + // Update state variables. + change = position - previous_pos_; + previous_pos_ = position; + + // Encoder rotate acceleration. + if (ms < 16) { + change *= 3; + } else if (ms < 32) { + change *= 2; + } + + if (reversed_) { + change = -(change); + } + return change; + } +}; + +#endif \ No newline at end of file diff --git a/examples/test_r4/test_r4.ino b/examples/test_r4/test_r4.ino new file mode 100644 index 0000000..b86de66 --- /dev/null +++ b/examples/test_r4/test_r4.ino @@ -0,0 +1,76 @@ + +#include "peripherials.h" + +#include "encoder.h" + + +#include +U8G2_SSD1306_128X64_NONAME_1_HW_I2C display(U8G2_R2, SCL, SDA, U8X8_PIN_NONE); + + +Encoder encoder; + +const int OUTPUT_COUNT = 6; +int outputs[OUTPUT_COUNT] = { + OUT_CH1, + OUT_CH2, + OUT_CH3, + OUT_CH4, + OUT_CH5, + OUT_CH6, +}; + +volatile int idx = 0; + +// the setup function runs once when you press reset or power the board +void setup() { + // initialize digital pin LED_BUILTIN as an output. + pinMode(LED_BUILTIN, OUTPUT); + for (int i = 0; i < OUTPUT_COUNT; i++) { + pinMode(outputs[i], OUTPUT); + } + + encoder.AttachRotateHandler(rotateEncoder); + encoder.AttachPressHandler(press); + + display.begin(); + +} + +void rotateEncoder(int val) { + idx = (val > 0) + ? constrain(idx + 1, 0 , OUTPUT_COUNT) + : constrain(idx - 1, 0 , OUTPUT_COUNT); +} + +// the loop function runs over and over again forever +void loop() { + encoder.Process(); + UpdateDisplay(); + + digitalWrite(LED_BUILTIN, HIGH); // turn the LED on (HIGH is the voltage level) + digitalWrite(outputs[idx], HIGH); // turn the LED on (HIGH is the voltage level) + delay(500); // wait for a second + digitalWrite(LED_BUILTIN, LOW); // turn the LED off by making the voltage LOW + digitalWrite(outputs[idx], LOW); // turn the LED on (LOW is the voltage level) + delay(500); // wait for a second +} + +void press() { + for (int i = 0; i < OUTPUT_COUNT; i++) { + digitalWrite(LED_BUILTIN, HIGH); // turn the LED on (HIGH is the voltage level) + digitalWrite(outputs[i], HIGH); // turn the LED on (HIGH is the voltage level) + delay(50); // wait for a second + digitalWrite(LED_BUILTIN, LOW); // turn the LED off by making the voltage LOW + digitalWrite(outputs[i], LOW); // turn the LED on (LOW is the voltage level) + delay(50); + } +} + + +void UpdateDisplay() { + display.firstPage(); + do { + display.drawStr(0, 0, "Hello"); + } while (display.nextPage()); +} \ No newline at end of file diff --git a/src/clock.h b/src/clock.h index 886c6b7..9be4e46 100644 --- a/src/clock.h +++ b/src/clock.h @@ -12,20 +12,12 @@ #ifndef CLOCK_H #define CLOCK_H -#include #include "peripherials.h" #include "uClock/uClock.h" -// MIDI clock, start, stop, and continue byte definitions - based on MIDI 1.0 Standards. -#define MIDI_CLOCK 0xF8 -#define MIDI_START 0xFA -#define MIDI_STOP 0xFC -#define MIDI_CONTINUE 0xFB - typedef void (*ExtCallback)(void); static ExtCallback extUserCallback = nullptr; -static void serialEventNoop(uint8_t msg, uint8_t status) {} class Clock { public: @@ -49,19 +41,11 @@ class Clock { }; void Init() { - NeoSerial.begin(31250); - // Initialize the clock library uClock.init(); uClock.setClockMode(uClock.INTERNAL_CLOCK); uClock.setOutputPPQN(uClock.PPQN_96); uClock.setTempo(DEFAULT_TEMPO); - - // MIDI events. - uClock.setOnClockStart(sendMIDIStart); - uClock.setOnClockStop(sendMIDIStop); - uClock.setOnSync24(sendMIDIClock); - uClock.start(); } @@ -80,10 +64,6 @@ class Clock { void SetSource(Source source) { bool was_playing = !IsPaused(); uClock.stop(); - // If we are changing the source from MIDI, disable the serial interrupt handler. - if (source_ == SOURCE_EXTERNAL_MIDI) { - NeoSerial.attachInterrupt(serialEventNoop); - } source_ = source; switch (source) { case SOURCE_INTERNAL: @@ -102,9 +82,6 @@ class Clock { uClock.setInputPPQN(uClock.PPQN_1); break; case SOURCE_EXTERNAL_MIDI: - uClock.setClockMode(uClock.EXTERNAL_CLOCK); - uClock.setInputPPQN(uClock.PPQN_24); - NeoSerial.attachInterrupt(onSerialEvent); break; } if (was_playing) { @@ -160,37 +137,6 @@ class Clock { private: Source source_ = SOURCE_INTERNAL; - static void onSerialEvent(uint8_t msg, uint8_t status) { - // Note: uClock start and stop will echo to MIDI. - switch (msg) { - case MIDI_CLOCK: - if (extUserCallback) { - extUserCallback(); - } - break; - case MIDI_STOP: - uClock.stop(); - sendMIDIStop(); - break; - case MIDI_START: - case MIDI_CONTINUE: - uClock.start(); - sendMIDIStart(); - break; - } - } - - static void sendMIDIStart() { - NeoSerial.write(MIDI_START); - } - - static void sendMIDIStop() { - NeoSerial.write(MIDI_STOP); - } - - static void sendMIDIClock(uint32_t tick) { - NeoSerial.write(MIDI_CLOCK); - } }; #endif \ No newline at end of file diff --git a/src/clock_midi.h b/src/clock_midi.h new file mode 100644 index 0000000..70fa206 --- /dev/null +++ b/src/clock_midi.h @@ -0,0 +1,197 @@ +/** + * @file clock.h + * @author Adam Wonak (https://github.com/awonak) + * @brief Wrapper Class for clock timing functions. + * @version 2.0.0 + * @date 2025-08-17 + * + * @copyright MIT - (c) 2025 - Adam Wonak - adam.wonak@gmail.com + * + */ + +#ifndef CLOCK_H +#define CLOCK_H + + +#include + +#include "peripherials.h" +#include "uClock/uClock.h" + +// MIDI clock, start, stop, and continue byte definitions - based on MIDI 1.0 Standards. +#define MIDI_CLOCK 0xF8 +#define MIDI_START 0xFA +#define MIDI_STOP 0xFC +#define MIDI_CONTINUE 0xFB + +typedef void (*ExtCallback)(void); +static ExtCallback extUserCallback = nullptr; +static void serialEventNoop(uint8_t msg, uint8_t status) {} + +class Clock { + public: + static constexpr int DEFAULT_TEMPO = 120; + + enum Source { + SOURCE_INTERNAL, + SOURCE_EXTERNAL_PPQN_24, + SOURCE_EXTERNAL_PPQN_4, + SOURCE_EXTERNAL_PPQN_1, + SOURCE_EXTERNAL_MIDI, + SOURCE_LAST, + }; + + enum Pulse { + PULSE_NONE, + PULSE_PPQN_24, + PULSE_PPQN_4, + PULSE_PPQN_1, + PULSE_LAST, + }; + + void Init() { + NeoSerial.begin(31250); + + // Initialize the clock library + uClock.init(); + uClock.setClockMode(uClock.INTERNAL_CLOCK); + uClock.setOutputPPQN(uClock.PPQN_96); + uClock.setTempo(DEFAULT_TEMPO); + + // MIDI events. + uClock.setOnClockStart(sendMIDIStart); + uClock.setOnClockStop(sendMIDIStop); + uClock.setOnSync24(sendMIDIClock); + + uClock.start(); + } + + // Handle external clock tick and call user callback when receiving clock trigger (PPQN_4, PPQN_24, or MIDI). + void AttachExtHandler(void (*callback)()) { + extUserCallback = callback; + attachInterrupt(digitalPinToInterrupt(EXT_PIN), callback, RISING); + } + + // Internal PPQN96 callback for all clock timer operations. + void AttachIntHandler(void (*callback)(uint32_t)) { + uClock.setOnOutputPPQN(callback); + } + + // Set the source of the clock mode. + void SetSource(Source source) { + bool was_playing = !IsPaused(); + uClock.stop(); + // If we are changing the source from MIDI, disable the serial interrupt handler. + if (source_ == SOURCE_EXTERNAL_MIDI) { + NeoSerial.attachInterrupt(serialEventNoop); + } + source_ = source; + switch (source) { + case SOURCE_INTERNAL: + uClock.setClockMode(uClock.INTERNAL_CLOCK); + break; + case SOURCE_EXTERNAL_PPQN_24: + uClock.setClockMode(uClock.EXTERNAL_CLOCK); + uClock.setInputPPQN(uClock.PPQN_24); + break; + case SOURCE_EXTERNAL_PPQN_4: + uClock.setClockMode(uClock.EXTERNAL_CLOCK); + uClock.setInputPPQN(uClock.PPQN_4); + break; + case SOURCE_EXTERNAL_PPQN_1: + uClock.setClockMode(uClock.EXTERNAL_CLOCK); + uClock.setInputPPQN(uClock.PPQN_1); + break; + case SOURCE_EXTERNAL_MIDI: + uClock.setClockMode(uClock.EXTERNAL_CLOCK); + uClock.setInputPPQN(uClock.PPQN_24); + NeoSerial.attachInterrupt(onSerialEvent); + break; + } + if (was_playing) { + uClock.start(); + } + } + + // Return true if the current selected source is externl (PPQN_4, PPQN_24, or MIDI). + bool ExternalSource() { + return uClock.getClockMode() == uClock.EXTERNAL_CLOCK; + } + + // Return true if the current selected source is the internal master clock. + bool InternalSource() { + return uClock.getClockMode() == uClock.INTERNAL_CLOCK; + } + + // Returns the current BPM tempo. + int Tempo() { + return uClock.getTempo(); + } + + // Set the clock tempo to a int between 1 and 400. + void SetTempo(int tempo) { + return uClock.setTempo(tempo); + } + + // Record an external clock tick received to process external/internal syncronization. + void Tick() { + uClock.clockMe(); + } + + // Start the internal clock. + void Start() { + uClock.start(); + } + + // Stop internal clock clock. + void Stop() { + uClock.stop(); + } + + // Reset all clock counters to 0. + void Reset() { + uClock.resetCounters(); + } + + // Returns true if the clock is not running. + bool IsPaused() { + return uClock.clock_state == uClock.PAUSED; + } + + private: + Source source_ = SOURCE_INTERNAL; + + static void onSerialEvent(uint8_t msg, uint8_t status) { + // Note: uClock start and stop will echo to MIDI. + switch (msg) { + case MIDI_CLOCK: + if (extUserCallback) { + extUserCallback(); + } + break; + case MIDI_STOP: + uClock.stop(); + sendMIDIStop(); + break; + case MIDI_START: + case MIDI_CONTINUE: + uClock.start(); + sendMIDIStart(); + break; + } + } + + static void sendMIDIStart() { + NeoSerial.write(MIDI_START); + } + + static void sendMIDIStop() { + NeoSerial.write(MIDI_STOP); + } + + static void sendMIDIClock(uint32_t tick) { + NeoSerial.write(MIDI_CLOCK); + } +}; + +#endif \ No newline at end of file diff --git a/src/encoder.h b/src/encoder.h index 89d3ba7..7541347 100644 --- a/src/encoder.h +++ b/src/encoder.h @@ -1,5 +1,5 @@ /** - * @file encoder_dir.h + * @file encoder.h * @author Adam Wonak (https://github.com/awonak) * @brief Class for interacting with encoders. * @version 2.0.0 diff --git a/src/libGravity.cpp b/src/libGravity.cpp index df29191..f2f17a4 100644 --- a/src/libGravity.cpp +++ b/src/libGravity.cpp @@ -33,6 +33,7 @@ void Gravity::initInputs() { cv1.Init(CV1_PIN); cv2.Init(CV2_PIN); +#if defined(ARDUINO_ARCH_AVR) // Pin Change Interrupts for Encoder. // Thanks to https://dronebotworkshop.com/interrupts/ @@ -42,6 +43,14 @@ void Gravity::initInputs() { PCMSK2 |= B00010000; // Select PCINT11 Bit3 (Pin D17/A3) PCMSK1 |= B00001000; +#endif +#if defined(ARDUINO_NANO_R4) + pinMode(ENCODER_PIN1, INPUT_PULLDOWN); + pinMode(ENCODER_PIN2, INPUT_PULLDOWN); + attachInterrupt(digitalPinToInterrupt(ENCODER_PIN1), Encoder::isr, CHANGE); + attachInterrupt(digitalPinToInterrupt(ENCODER_PIN2), Encoder::isr, CHANGE); +#endif + } void Gravity::initOutputs() { @@ -74,6 +83,7 @@ void Gravity::Process() { } } +#if defined(ARDUINO_ARCH_AVR) // Pin Change Interrupt on Port D (D4). ISR(PCINT2_vect) { Encoder::isr(); @@ -82,6 +92,8 @@ ISR(PCINT2_vect) { ISR(PCINT1_vect) { Encoder::isr(); }; +#endif + // Global instance Gravity gravity; diff --git a/src/peripherials.h b/src/peripherials.h index e444909..35ea7ca 100644 --- a/src/peripherials.h +++ b/src/peripherials.h @@ -24,8 +24,8 @@ // Clock and CV Inputs #define EXT_PIN 2 -#define CV1_PIN A7 -#define CV2_PIN A6 +#define CV1_PIN 21 // A7 +#define CV2_PIN 20 // A6 #define PULSE_OUT_PIN 3 // Button pins diff --git a/src/uClock/platforms/renesas.h b/src/uClock/platforms/renesas.h new file mode 100644 index 0000000..43533b6 --- /dev/null +++ b/src/uClock/platforms/renesas.h @@ -0,0 +1,72 @@ +#pragma once + +/** + * @file nano_r4.h + * @author Gemini (Based on the uClock AVR implementation) + * @brief uClock platform support for the Arduino Nano R4 (Renesas RA4M1). + * + * This file implements the timer initialization and control functions + * required by uClock using the FspTimer library, which provides a high-level + * interface to the General PWM Timers (GPT) on the Renesas RA4M1 + * microcontroller. This approach replaces the direct register manipulation + * used for AVR platforms. + */ + +#include +#include + +// ATOMIC macro for defining critical sections where interrupts are disabled. +#define ATOMIC(X) noInterrupts(); X; interrupts(); + +// Forward declaration of the uClock's main handler function. This function +// must be defined in the main uClock library code and will be called by the timer interrupt. +void uClockHandler(); + +// Create an FspTimer instance for uClock. +// We use GPT channel 6, as it is less likely to conflict with the default +// analogWrite() (PWM) functionality on the Nano R4's pins. +FspTimer uClockTimer; + +/** + * @brief Initializes the hardware timer for uClock. + * + * This function configures and starts a hardware timer (GPT6) to fire + * periodically. It attaches the uClockHandler as the interrupt service routine. + * The initial tempo is set to a default of 120 BPM (48 Hz tick rate). + * + * @param init_clock This parameter is unused on this platform but is kept + * for API compatibility with other uClock platforms. + */ +void initTimer(uint32_t init_clock) +{ + ATOMIC( + // Configure the timer to be a periodic interrupt source. + // The frequency/period arguments here are placeholders, as the actual + // period is set precisely with the setPeriod() call below. + uClockTimer.begin(TIMER_MODE_PERIODIC, GPT_TIMER, 6, 1.0f, STANDARD_PWM_FREQ_HZ); + + // Set the timer's period to the provided BPM period in microseconds. + uClockTimer.set_period(init_clock); + + // Start the timer to begin generating ticks. + uClockTimer.start(); + ) +} + +/** + * @brief Sets the timer's interval in microseconds. + * + * This function dynamically updates the timer's period to match the specified + * interval, which effectively changes the clock's tempo. The FspTimer library + * automatically handles the complex low-level prescaler and counter adjustments. + * + * @param us_interval The desired interval between clock ticks in microseconds. + */ +void setTimer(uint32_t us_interval) +{ + // Atomically update the timer's period. The FspTimer library abstracts + // away the manual prescaler math required on AVR platforms. + ATOMIC( + uClockTimer.set_period(us_interval); + ) +} \ No newline at end of file diff --git a/src/uClock/uClock.cpp b/src/uClock/uClock.cpp index 94c432e..557b31a 100755 --- a/src/uClock/uClock.cpp +++ b/src/uClock/uClock.cpp @@ -32,7 +32,13 @@ * DEALINGS IN THE SOFTWARE. */ #include "uClock.h" + +#if defined(ARDUINO_ARCH_AVR) #include "platforms/avr.h" +#endif +#if defined(ARDUINO_NANO_R4) +#include "platforms/renesas.h" +#endif // // Platform specific timer setup/control