1 Commits

Author SHA1 Message Date
c6ce5b1309 testing firmware on new Nano R4. 2025-09-10 11:54:51 -07:00
9 changed files with 484 additions and 57 deletions

118
examples/test_r4/encoder.h Normal file
View File

@ -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 <RotaryEncoder.h>
#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

View File

@ -0,0 +1,76 @@
#include "peripherials.h"
#include "encoder.h"
#include <U8g2lib.h>
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());
}

View File

@ -12,20 +12,12 @@
#ifndef CLOCK_H #ifndef CLOCK_H
#define CLOCK_H #define CLOCK_H
#include <NeoHWSerial.h>
#include "peripherials.h" #include "peripherials.h"
#include "uClock/uClock.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); typedef void (*ExtCallback)(void);
static ExtCallback extUserCallback = nullptr; static ExtCallback extUserCallback = nullptr;
static void serialEventNoop(uint8_t msg, uint8_t status) {}
class Clock { class Clock {
public: public:
@ -49,19 +41,11 @@ class Clock {
}; };
void Init() { void Init() {
NeoSerial.begin(31250);
// Initialize the clock library // Initialize the clock library
uClock.init(); uClock.init();
uClock.setClockMode(uClock.INTERNAL_CLOCK); uClock.setClockMode(uClock.INTERNAL_CLOCK);
uClock.setOutputPPQN(uClock.PPQN_96); uClock.setOutputPPQN(uClock.PPQN_96);
uClock.setTempo(DEFAULT_TEMPO); uClock.setTempo(DEFAULT_TEMPO);
// MIDI events.
uClock.setOnClockStart(sendMIDIStart);
uClock.setOnClockStop(sendMIDIStop);
uClock.setOnSync24(sendMIDIClock);
uClock.start(); uClock.start();
} }
@ -80,10 +64,6 @@ class Clock {
void SetSource(Source source) { void SetSource(Source source) {
bool was_playing = !IsPaused(); bool was_playing = !IsPaused();
uClock.stop(); 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; source_ = source;
switch (source) { switch (source) {
case SOURCE_INTERNAL: case SOURCE_INTERNAL:
@ -102,9 +82,6 @@ class Clock {
uClock.setInputPPQN(uClock.PPQN_1); uClock.setInputPPQN(uClock.PPQN_1);
break; break;
case SOURCE_EXTERNAL_MIDI: case SOURCE_EXTERNAL_MIDI:
uClock.setClockMode(uClock.EXTERNAL_CLOCK);
uClock.setInputPPQN(uClock.PPQN_24);
NeoSerial.attachInterrupt(onSerialEvent);
break; break;
} }
if (was_playing) { if (was_playing) {
@ -160,37 +137,6 @@ class Clock {
private: private:
Source source_ = SOURCE_INTERNAL; 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 #endif

197
src/clock_midi.h Normal file
View File

@ -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 <NeoHWSerial.h>
#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

View File

@ -1,5 +1,5 @@
/** /**
* @file encoder_dir.h * @file encoder.h
* @author Adam Wonak (https://github.com/awonak) * @author Adam Wonak (https://github.com/awonak)
* @brief Class for interacting with encoders. * @brief Class for interacting with encoders.
* @version 2.0.0 * @version 2.0.0

View File

@ -33,6 +33,7 @@ void Gravity::initInputs() {
cv1.Init(CV1_PIN); cv1.Init(CV1_PIN);
cv2.Init(CV2_PIN); cv2.Init(CV2_PIN);
#if defined(ARDUINO_ARCH_AVR)
// Pin Change Interrupts for Encoder. // Pin Change Interrupts for Encoder.
// Thanks to https://dronebotworkshop.com/interrupts/ // Thanks to https://dronebotworkshop.com/interrupts/
@ -42,6 +43,14 @@ void Gravity::initInputs() {
PCMSK2 |= B00010000; PCMSK2 |= B00010000;
// Select PCINT11 Bit3 (Pin D17/A3) // Select PCINT11 Bit3 (Pin D17/A3)
PCMSK1 |= B00001000; 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() { void Gravity::initOutputs() {
@ -74,6 +83,7 @@ void Gravity::Process() {
} }
} }
#if defined(ARDUINO_ARCH_AVR)
// Pin Change Interrupt on Port D (D4). // Pin Change Interrupt on Port D (D4).
ISR(PCINT2_vect) { ISR(PCINT2_vect) {
Encoder::isr(); Encoder::isr();
@ -82,6 +92,8 @@ ISR(PCINT2_vect) {
ISR(PCINT1_vect) { ISR(PCINT1_vect) {
Encoder::isr(); Encoder::isr();
}; };
#endif
// Global instance // Global instance
Gravity gravity; Gravity gravity;

View File

@ -24,8 +24,8 @@
// Clock and CV Inputs // Clock and CV Inputs
#define EXT_PIN 2 #define EXT_PIN 2
#define CV1_PIN A7 #define CV1_PIN 21 // A7
#define CV2_PIN A6 #define CV2_PIN 20 // A6
#define PULSE_OUT_PIN 3 #define PULSE_OUT_PIN 3
// Button pins // Button pins

View File

@ -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 <Arduino.h>
#include <FspTimer.h>
// 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);
)
}

View File

@ -32,7 +32,13 @@
* DEALINGS IN THE SOFTWARE. * DEALINGS IN THE SOFTWARE.
*/ */
#include "uClock.h" #include "uClock.h"
#if defined(ARDUINO_ARCH_AVR)
#include "platforms/avr.h" #include "platforms/avr.h"
#endif
#if defined(ARDUINO_NANO_R4)
#include "platforms/renesas.h"
#endif
// //
// Platform specific timer setup/control // Platform specific timer setup/control