1 Commits

Author SHA1 Message Date
af3cfe9614 initial commit of new GridSeq firmware 2025-08-13 07:06:53 -07:00
31 changed files with 549 additions and 602 deletions

View File

@ -1,18 +1,6 @@
# Sitka Instruments Gravity Firmware Abstraction # Sitka Instruments Gravity Firmware Abstraction
This library helps make writing firmware for the [Sitka Instruments Gravity](https://sitkainstruments.com/gravity/) eurorack module easier by abstracting away the initialization and peripheral interactions. Now your firmware code can just focus on the logic and behavior of the app, and keep the low level code neatly tucked away in this library. This library helps make writing firmware easier by abstracting away the initialization and peripheral interactions. Now your firmware code can just focus on the logic and behavior of the app, and keep the low level code neatly tucked away in this library.
The latest releases of all Sitka Instruments Gravity firmware builds can be found on the [Updater](https://sitkainstruments.com/gravity/updater/) page. You can use this page to flash the latest build directly to the Arduino Nano on the back of your module.
## Project Code Layout
* [`src/`](src/) - **libGravity**: This is the hardware abstraction library used to simplify the creation of new Gravity module firmware by providing common reusable wrappers around the module peripherials like [DigitalOutput](src/digital_output.h#L18) providing methods like [`Update(uint8_t state)`](src/digital_output.h#L45) which allow you to set that output channel voltage high or low, and common module behavior like [Clock](src/clock.h#L30) which provides handlers like [`AttachExtHandler(callback)`](src/clock.h#L69) which takes a callback function to handle external clock tick behavior when receiving clock trigger.
* [`firmware/Gravity`](firmware/Gravity/) - **Alt Gravity**: This is the implementation of the default 6-channel trigger/gate clock modulation firmware. This is a full rewrite of the original firmware designed to use `libGravity` with a focus on open source friendlines.
* `firmware/GridSeq` - **GridSeq**: Comming Soon.
* [`examples/skeleton`](examples/skeleton/skeleton.ino) - **Skeleton**: This is the bare bones scaffloding for a `libGravity` firmware app.
## Installation ## Installation
@ -29,14 +17,13 @@ Common directory locations:
* [uClock](https://github.com/midilab/uClock) [MIT] - (Included with this repo) 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. * [RotateEncoder](https://github.com/mathertel/RotaryEncoder) [BSD] - Library for reading and interpreting encoder rotation.
* [U8g2](https://github.com/olikraus/u8g2/) [MIT] - Graphics helper library. * [U8g2](https://github.com/olikraus/u8g2/) [MIT] - Graphics helper library.
* [NeoHWSerial](https://github.com/SlashDevin/NeoHWSerial) [GPL] - Hardware serial library with attachInterrupt.
## Example ## Example
Here's a trivial example showing some of the ways to interact with the library. This script rotates the active clock channel according to the set tempo. The encoder can change the temo or rotation direction. The play/pause button will toggle the clock activity on or off. The shift button will freeze the clock from advancing the channel rotation. Here's a trivial example showing some of the ways to interact with the library. This script rotates the active clock channel according to the set tempo. The encoder can change the temo or rotation direction. The play/pause button will toggle the clock activity on or off. The shift button will freeze the clock from advancing the channel rotation.
```cpp ```cpp
#include "libGravity.h" #include "gravity.h"
byte idx = 0; byte idx = 0;
bool reversed = false; bool reversed = false;
@ -88,11 +75,11 @@ void HandlePlayPressed() {
} }
} }
void HandleRotate(int val) { void HandleRotate(Direction dir, int val) {
if (selected_param == 0) { if (selected_param == 0) {
gravity.clock.SetTempo(gravity.clock.Tempo() + val); gravity.clock.SetTempo(gravity.clock.Tempo() + val);
} else if (selected_param == 1) { } else if (selected_param == 1) {
reversed = (val < 0); reversed = (dir == DIRECTION_DECREMENT);
} }
} }
@ -124,14 +111,6 @@ void UpdateDisplay() {
} }
``` ```
**Building New Firmware Using libGravity**
When starting a new firmware sketch you can use the [skeleton](examples/skeleton/skeleton.ino) app as a place to start.
**Building New Firmware from scratch**
If you do not want to use the libGravity hardware abstraction library and want to roll your own vanilla firmware, take a look at the [peripherials.h](src/peripherials.h) file for the pinout definitions used by the module.
### Build for release ### Build for release
``` ```

View File

@ -17,7 +17,7 @@
* TODO: Store the calibration value in EEPROM. * TODO: Store the calibration value in EEPROM.
*/ */
#include "libGravity.h" #include "gravity.h"
#define TEXT_FONT u8g2_font_profont11_tf #define TEXT_FONT u8g2_font_profont11_tf
#define INDICATOR_FONT u8g2_font_open_iconic_arrow_1x_t #define INDICATOR_FONT u8g2_font_open_iconic_arrow_1x_t
@ -43,7 +43,7 @@ void NextCalibrationPoint() {
selected_param = (selected_param + 1) % 6; selected_param = (selected_param + 1) % 6;
} }
void CalibrateCV(int val) { void CalibrateCV(Direction dir, int val) {
AnalogInput* cv = (selected_param > 2) ? &gravity.cv2 : &gravity.cv1; AnalogInput* cv = (selected_param > 2) ? &gravity.cv2 : &gravity.cv1;
switch (selected_param % 3) { switch (selected_param % 3) {
case 0: case 0:

View File

@ -14,7 +14,7 @@
* *
*/ */
#include "libGravity.h" #include "gravity.h"
#define TEXT_FONT u8g2_font_profont11_tf #define TEXT_FONT u8g2_font_profont11_tf
@ -39,7 +39,7 @@ void NextCalibrationPoint() {
selected_param = (selected_param + 1) % 2; selected_param = (selected_param + 1) % 2;
} }
void CalibrateCV(int val) { void CalibrateCV(Direction dir, int val) {
// AnalogInput* cv = (selected_param > 2) ? &gravity.cv2 : &gravity.cv1; // AnalogInput* cv = (selected_param > 2) ? &gravity.cv2 : &gravity.cv1;
AnalogInput* cv = &gravity.cv1; AnalogInput* cv = &gravity.cv1;
switch (selected_param % 2) { switch (selected_param % 2) {

View File

@ -1,4 +1,4 @@
#include "libGravity.h" #include "gravity.h"
byte idx = 0; byte idx = 0;
bool reversed = false; bool reversed = false;
@ -33,28 +33,28 @@ void IntClock(uint32_t tick) {
if (tick % 12 == 0 && ! freeze) { if (tick % 12 == 0 && ! freeze) {
gravity.outputs[idx].Low(); gravity.outputs[idx].Low();
if (reversed) { if (reversed) {
idx = (idx == 0) ? Gravity::OUTPUT_COUNT - 1 : idx - 1; idx = (idx == 0) ? OUTPUT_COUNT - 1 : idx - 1;
} else { } else {
idx = (idx + 1) % Gravity::OUTPUT_COUNT; idx = (idx + 1) % OUTPUT_COUNT;
} }
gravity.outputs[idx].High(); gravity.outputs[idx].High();
} }
} }
void HandlePlayPressed() { void HandlePlayPressed() {
gravity.clock.Stop(); gravity.clock.Pause();
if (gravity.clock.IsPaused()) { if (gravity.clock.IsPaused()) {
for (int i = 0; i < Gravity::OUTPUT_COUNT; i++) { for (int i = 0; i < OUTPUT_COUNT; i++) {
gravity.outputs[i].Low(); gravity.outputs[i].Low();
} }
} }
} }
void HandleRotate(int val) { void HandleRotate(Direction dir, int val) {
if (selected_param == 0) { if (selected_param == 0) {
gravity.clock.SetTempo(gravity.clock.Tempo() + val); gravity.clock.SetTempo(gravity.clock.Tempo() + val);
} else if (selected_param == 1) { } else if (selected_param == 1) {
reversed = (val < 0); reversed = (dir == DIRECTION_DECREMENT);
} }
} }
@ -80,7 +80,7 @@ void UpdateDisplay() {
gravity.display.print("Direction: "); gravity.display.print("Direction: ");
gravity.display.print((reversed) ? "Backward" : "Forward"); gravity.display.print((reversed) ? "Backward" : "Forward");
gravity.display.drawStr(0, selected_param * 10, "x"); gravity.display.drawChar(0, selected_param * 10, 0x10, 1, 0, 1);
gravity.display.display(); gravity.display.display();
} }

View File

@ -1,118 +0,0 @@
/**
* @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

@ -1,76 +0,0 @@
#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

@ -2,7 +2,7 @@
* @file Gravity.ino * @file Gravity.ino
* @author Adam Wonak (https://github.com/awonak/) * @author Adam Wonak (https://github.com/awonak/)
* @brief Alt firmware version of Gravity by Sitka Instruments. * @brief Alt firmware version of Gravity by Sitka Instruments.
* @version v2.0.0 - August 2025 awonak - Full rewrite * @version v2.0.0 - June 2025 awonak - Full rewrite
* @version v1.0 - August 2023 Oleksiy H - Initial release * @version v1.0 - August 2023 Oleksiy H - Initial release
* @date 2025-07-04 * @date 2025-07-04
* *
@ -227,17 +227,9 @@ void HandleEncoderPressed() {
case PARAM_MAIN_LOAD_DATA: case PARAM_MAIN_LOAD_DATA:
if (app.selected_sub_param < StateManager::MAX_SAVE_SLOTS) { if (app.selected_sub_param < StateManager::MAX_SAVE_SLOTS) {
app.selected_save_slot = app.selected_sub_param; app.selected_save_slot = app.selected_sub_param;
// Load pattern data into app state.
stateManager.loadData(app, app.selected_save_slot); stateManager.loadData(app, app.selected_save_slot);
// Load global performance settings if they have changed.
if (gravity.clock.Tempo() != app.tempo) {
gravity.clock.SetTempo(app.tempo);
}
// Load global settings only clock is not active.
if (gravity.clock.IsPaused()) {
InitGravity(app); InitGravity(app);
} }
}
break; break;
case PARAM_MAIN_FACTORY_RESET: case PARAM_MAIN_FACTORY_RESET:
if (app.selected_sub_param == 0) { // Erase if (app.selected_sub_param == 0) { // Erase

View File

@ -2,8 +2,8 @@
* @file app_state.h * @file app_state.h
* @author Adam Wonak (https://github.com/awonak/) * @author Adam Wonak (https://github.com/awonak/)
* @brief Alt firmware version of Gravity by Sitka Instruments. * @brief Alt firmware version of Gravity by Sitka Instruments.
* @version 2.0.0 * @version 2.0.1
* @date 2025-08-17 * @date 2025-07-04
* *
* @copyright MIT - (c) 2025 - Adam Wonak - adam.wonak@gmail.com * @copyright MIT - (c) 2025 - Adam Wonak - adam.wonak@gmail.com
* *

View File

@ -2,8 +2,8 @@
* @file channel.h * @file channel.h
* @author Adam Wonak (https://github.com/awonak/) * @author Adam Wonak (https://github.com/awonak/)
* @brief Alt firmware version of Gravity by Sitka Instruments. * @brief Alt firmware version of Gravity by Sitka Instruments.
* @version 2.0.0 * @version 2.0.1
* @date 2025-08-17 * @date 2025-07-04
* *
* @copyright MIT - (c) 2025 - Adam Wonak - adam.wonak@gmail.com * @copyright MIT - (c) 2025 - Adam Wonak - adam.wonak@gmail.com
* *

View File

@ -2,8 +2,8 @@
* @file display.h * @file display.h
* @author Adam Wonak (https://github.com/awonak/) * @author Adam Wonak (https://github.com/awonak/)
* @brief Alt firmware version of Gravity by Sitka Instruments. * @brief Alt firmware version of Gravity by Sitka Instruments.
* @version 2.0.0 * @version 2.0.1
* @date 2025-08-17 * @date 2025-07-04
* *
* @copyright MIT - (c) 2025 - Adam Wonak - adam.wonak@gmail.com * @copyright MIT - (c) 2025 - Adam Wonak - adam.wonak@gmail.com
* *

View File

@ -2,8 +2,8 @@
* @file euclidean.h * @file euclidean.h
* @author Adam Wonak (https://github.com/awonak/) * @author Adam Wonak (https://github.com/awonak/)
* @brief Alt firmware version of Gravity by Sitka Instruments. * @brief Alt firmware version of Gravity by Sitka Instruments.
* @version 2.0.0 * @version 2.0.1
* @date 2025-08-17 * @date 2025-07-04
* *
* @copyright MIT - (c) 2025 - Adam Wonak - adam.wonak@gmail.com * @copyright MIT - (c) 2025 - Adam Wonak - adam.wonak@gmail.com
* *

View File

@ -2,8 +2,8 @@
* @file save_state.cpp * @file save_state.cpp
* @author Adam Wonak (https://github.com/awonak/) * @author Adam Wonak (https://github.com/awonak/)
* @brief Alt firmware version of Gravity by Sitka Instruments. * @brief Alt firmware version of Gravity by Sitka Instruments.
* @version 2.0.0 * @version 2.0.1
* @date 2025-08-17 * @date 2025-07-04
* *
* @copyright MIT - (c) 2025 - Adam Wonak - adam.wonak@gmail.com * @copyright MIT - (c) 2025 - Adam Wonak - adam.wonak@gmail.com
* *
@ -33,66 +33,54 @@ const int StateManager::EEPROM_DATA_START_ADDR = sizeof(StateManager::Metadata);
StateManager::StateManager() : _isDirty(false), _lastChangeTime(0) {} StateManager::StateManager() : _isDirty(false), _lastChangeTime(0) {}
bool StateManager::initialize(AppState& app) { bool StateManager::initialize(AppState& app) {
noInterrupts();
bool success = false;
if (_isDataValid()) { if (_isDataValid()) {
// Load global settings. // Load global settings.
_loadMetadata(app); _loadMetadata(app);
// Load app data from the transient slot. // Load app data from the transient slot.
_loadState(app, TRANSIENT_SLOT); _loadState(app, TRANSIENT_SLOT);
success = true; return true;
} }
// EEPROM does not contain save data for this firmware & version. // EEPROM does not contain save data for this firmware & version.
else { else {
// Erase EEPROM and initialize state. Save default pattern to all save slots. // Erase EEPROM and initialize state. Save default pattern to all save slots.
factoryReset(app); factoryReset(app);
return false;
} }
interrupts();
return success;
} }
bool StateManager::loadData(AppState& app, byte slot_index) { bool StateManager::loadData(AppState& app, byte slot_index) {
// Check if slot_index is within max range + 1 for transient. // Check if slot_index is within max range + 1 for transient.
if (slot_index >= MAX_SAVE_SLOTS + 1) return false; if (slot_index >= MAX_SAVE_SLOTS + 1) return false;
noInterrupts();
// Load the state data from the specified EEPROM slot and update the app state save slot. // Load the state data from the specified EEPROM slot and update the app state save slot.
_loadState(app, slot_index); _loadState(app, slot_index);
app.selected_save_slot = slot_index; app.selected_save_slot = slot_index;
// Persist this change in the global metadata on next update. // Persist this change in the global metadata.
_isDirty = true; _saveMetadata(app);
interrupts();
return true; return true;
} }
// Save app state to user specified save slot. // Save app state to user specified save slot.
void StateManager::saveData(const AppState& app) { void StateManager::saveData(const AppState& app) {
noInterrupts();
// Check if slot_index is within max range + 1 for transient. // Check if slot_index is within max range + 1 for transient.
if (app.selected_save_slot >= MAX_SAVE_SLOTS + 1) return; if (app.selected_save_slot >= MAX_SAVE_SLOTS + 1) return;
_saveState(app, app.selected_save_slot); _saveState(app, app.selected_save_slot);
_saveMetadata(app); _saveMetadata(app);
_isDirty = false; _isDirty = false;
interrupts();
} }
// Save transient state if it has changed and enough time has passed since last save. // Save transient state if it has changed and enough time has passed since last save.
void StateManager::update(const AppState& app) { void StateManager::update(const AppState& app) {
if (_isDirty && (millis() - _lastChangeTime > SAVE_DELAY_MS)) { if (_isDirty && (millis() - _lastChangeTime > SAVE_DELAY_MS)) {
noInterrupts();
_saveState(app, TRANSIENT_SLOT); _saveState(app, TRANSIENT_SLOT);
_saveMetadata(app); _saveMetadata(app);
_isDirty = false; _isDirty = false;
interrupts();
} }
} }
void StateManager::reset(AppState& app) { void StateManager::reset(AppState& app) {
noInterrupts();
AppState default_app; AppState default_app;
app.tempo = default_app.tempo; app.tempo = default_app.tempo;
app.selected_param = default_app.selected_param; app.selected_param = default_app.selected_param;
@ -110,7 +98,6 @@ void StateManager::reset(AppState& app) {
_loadMetadata(app); _loadMetadata(app);
_isDirty = false; _isDirty = false;
interrupts();
} }
void StateManager::markDirty() { void StateManager::markDirty() {
@ -147,6 +134,7 @@ void StateManager::_saveState(const AppState& app, byte slot_index) {
// Check if slot_index is within max range + 1 for transient. // Check if slot_index is within max range + 1 for transient.
if (app.selected_save_slot >= MAX_SAVE_SLOTS + 1) return; if (app.selected_save_slot >= MAX_SAVE_SLOTS + 1) return;
noInterrupts();
static EepromData save_data; static EepromData save_data;
save_data.tempo = app.tempo; save_data.tempo = app.tempo;
@ -177,12 +165,14 @@ void StateManager::_saveState(const AppState& app, byte slot_index) {
int address = EEPROM_DATA_START_ADDR + (slot_index * sizeof(EepromData)); int address = EEPROM_DATA_START_ADDR + (slot_index * sizeof(EepromData));
EEPROM.put(address, save_data); EEPROM.put(address, save_data);
interrupts();
} }
void StateManager::_loadState(AppState& app, byte slot_index) { void StateManager::_loadState(AppState& app, byte slot_index) {
// Check if slot_index is within max range + 1 for transient. // Check if slot_index is within max range + 1 for transient.
if (slot_index >= MAX_SAVE_SLOTS + 1) return; if (slot_index >= MAX_SAVE_SLOTS + 1) return;
noInterrupts();
static EepromData load_data; static EepromData load_data;
int address = EEPROM_DATA_START_ADDR + (slot_index * sizeof(EepromData)); int address = EEPROM_DATA_START_ADDR + (slot_index * sizeof(EepromData));
EEPROM.get(address, load_data); EEPROM.get(address, load_data);
@ -210,9 +200,11 @@ void StateManager::_loadState(AppState& app, byte slot_index) {
ch.setCv1Dest(static_cast<CvDestination>(saved_ch_state.cv1_dest)); ch.setCv1Dest(static_cast<CvDestination>(saved_ch_state.cv1_dest));
ch.setCv2Dest(static_cast<CvDestination>(saved_ch_state.cv2_dest)); ch.setCv2Dest(static_cast<CvDestination>(saved_ch_state.cv2_dest));
} }
interrupts();
} }
void StateManager::_saveMetadata(const AppState& app) { void StateManager::_saveMetadata(const AppState& app) {
noInterrupts();
Metadata current_meta; Metadata current_meta;
strcpy(current_meta.sketch_name, SKETCH_NAME); strcpy(current_meta.sketch_name, SKETCH_NAME);
strcpy(current_meta.version, SEMANTIC_VERSION); strcpy(current_meta.version, SEMANTIC_VERSION);
@ -223,12 +215,14 @@ void StateManager::_saveMetadata(const AppState& app) {
current_meta.rotate_display = app.rotate_display; current_meta.rotate_display = app.rotate_display;
EEPROM.put(METADATA_START_ADDR, current_meta); EEPROM.put(METADATA_START_ADDR, current_meta);
interrupts();
} }
void StateManager::_loadMetadata(AppState& app) { void StateManager::_loadMetadata(AppState& app) {
noInterrupts();
Metadata metadata; Metadata metadata;
EEPROM.get(METADATA_START_ADDR, metadata); EEPROM.get(METADATA_START_ADDR, metadata);
app.selected_save_slot = metadata.selected_save_slot; app.selected_save_slot = metadata.selected_save_slot;
app.encoder_reversed = metadata.encoder_reversed; app.encoder_reversed = metadata.encoder_reversed;
app.rotate_display = metadata.rotate_display; interrupts();
} }

View File

@ -2,8 +2,8 @@
* @file save_state.h * @file save_state.h
* @author Adam Wonak (https://github.com/awonak/) * @author Adam Wonak (https://github.com/awonak/)
* @brief Alt firmware version of Gravity by Sitka Instruments. * @brief Alt firmware version of Gravity by Sitka Instruments.
* @version 2.0.0 * @version 2.0.1
* @date 2025-08-17 * @date 2025-07-04
* *
* @copyright MIT - (c) 2025 - Adam Wonak - adam.wonak@gmail.com * @copyright MIT - (c) 2025 - Adam Wonak - adam.wonak@gmail.com
* *
@ -52,8 +52,8 @@ class StateManager {
// This struct holds the data that identifies the firmware version. // This struct holds the data that identifies the firmware version.
struct Metadata { struct Metadata {
char sketch_name[16]; char sketch_name[12];
char version[16]; char version[5];
// Additional global/hardware settings // Additional global/hardware settings
byte selected_save_slot; byte selected_save_slot;
bool encoder_reversed; bool encoder_reversed;

View File

@ -1,14 +1,37 @@
/** /**
* @file skeleton.ino * @file GridSeq.ino
* @author YOUR_NAME (<url>) * @author Adam Wonak (https://github.com/awonak/)
* @brief Skeleton app for Sitka Instruments Gravity. * @brief Grid based step sequencer firmware for Gravity by Sitka Instruments.
* @version vX.Y.Z - MONTH YEAR YOUR_NAME * @version v1.0.0 - August 2025 awonak
* @date YYYY-MM-DD * @date 2025-08-12
* *
* @copyright MIT - (c) 2025 - Adam Wonak - adam.wonak@gmail.com * @copyright MIT - (c) 2025 - Adam Wonak - adam.wonak@gmail.com
* *
* Skeleton app for basic structure of a new firmware for Sitka Instruments * Grid based step sequencer with lots of dynamic features.
* Gravity using the libGravity library. *
* Pattern:
* - length
* - clock division
* - probability
* - fill density
* - direction (fwd, rev, pend, rand)
* - mode:
* - step equencer
* - euclidean rhythm
* - pattern (grids like presets)
*
* Step:
* - gate / trigger
* - duty / duration
* - probability
* - ratchet / retrig
*
* Global:
* - internal / external / midi
* - run / reset
* - mute
* - save / load banks
* - 6 channel / 3 channel accent
* *
* ENCODER: * ENCODER:
* Press: change between selecting a parameter and editing the parameter. * Press: change between selecting a parameter and editing the parameter.
@ -34,14 +57,9 @@
#include <libGravity.h> #include <libGravity.h>
#include "app_state.h"
#include "channel.h"
// Global state for settings and app behavior. #include "display.h"
struct AppState {
int tempo = Clock::DEFAULT_TEMPO;
Clock::Source selected_source = Clock::SOURCE_INTERNAL;
// Add app specific state variables here.
};
AppState app; AppState app;
@ -70,7 +88,12 @@ void loop() {
// Process change in state of inputs and outputs. // Process change in state of inputs and outputs.
gravity.Process(); gravity.Process();
// Non-ISR loop behavior. // Check if cv run or reset is active and read cv.
CheckRunReset(gravity.cv1, gravity.cv2);
if (app.refresh_screen) {
UpdateDisplay();
}
} }
// //
@ -80,7 +103,7 @@ void loop() {
void HandleIntClockTick(uint32_t tick) { void HandleIntClockTick(uint32_t tick) {
bool refresh = false; bool refresh = false;
for (int i = 0; i < Gravity::OUTPUT_COUNT; i++) { for (int i = 0; i < Gravity::OUTPUT_COUNT; i++) {
// Process each output tick handlers. app.channel[i].processClockTick(tick, gravity.outputs[i]);
} }
} }
@ -89,12 +112,35 @@ void HandleExtClockTick() {
case Clock::SOURCE_INTERNAL: case Clock::SOURCE_INTERNAL:
case Clock::SOURCE_EXTERNAL_MIDI: case Clock::SOURCE_EXTERNAL_MIDI:
// Use EXT as Reset when not used for clock source. // Use EXT as Reset when not used for clock source.
ResetOutputs();
gravity.clock.Reset(); gravity.clock.Reset();
break; break;
default: default:
// Register EXT cv clock tick. // Register EXT cv clock tick.
gravity.clock.Tick(); gravity.clock.Tick();
} }
app.refresh_screen = true;
}
void CheckRunReset(AnalogInput& cv1, AnalogInput& cv2) {
// Clock Run
if (app.cv_run == 1 || app.cv_run == 2) {
const int val = (app.cv_run == 1) ? cv1.Read() : cv2.Read();
if (val > AnalogInput::GATE_THRESHOLD && gravity.clock.IsPaused()) {
gravity.clock.Start();
app.refresh_screen = true;
} else if (val < AnalogInput::GATE_THRESHOLD && !gravity.clock.IsPaused()) {
gravity.clock.Stop();
ResetOutputs();
app.refresh_screen = true;
}
}
// Clock Reset
if ((app.cv_reset == 1 && cv1.IsRisingEdge(AnalogInput::GATE_THRESHOLD)) ||
(app.cv_reset == 2 && cv2.IsRisingEdge(AnalogInput::GATE_THRESHOLD))) {
gravity.clock.Reset();
}
} }
// //
@ -113,6 +159,9 @@ void HandleRotate(int val) {
void HandlePressedRotate(int val) { void HandlePressedRotate(int val) {
} }
// // TODO: move to libGravity
// Application logic goes here. void ResetOutputs() {
// for (int i = 0; i < Gravity::OUTPUT_COUNT; i++) {
gravity.outputs[i].Low();
}
}

View File

@ -0,0 +1,38 @@
/**
* @file app_state.h
* @author Adam Wonak (https://github.com/awonak/)
* @brief Alt firmware version of Gravity by Sitka Instruments.
* @version 2.0.1
* @date 2025-07-04
*
* @copyright MIT - (c) 2025 - Adam Wonak - adam.wonak@gmail.com
*
*/
#ifndef APP_STATE_H
#define APP_STATE_H
#include <libGravity.h>
#include "channel.h"
// Global state for settings and app behavior.
struct AppState {
int tempo = Clock::DEFAULT_TEMPO;
Clock::Source selected_source = Clock::SOURCE_INTERNAL;
Channel channel[Gravity::OUTPUT_COUNT];
byte selected_param = 0;
byte selected_channel = 0; // 0=tempo, 1-6=output channel
byte cv_run = 0;
byte cv_reset = 0;
bool editing_param = false;
bool refresh_screen = true;
};
extern AppState app;
static Channel& GetSelectedChannel() {
return app.channel[app.selected_channel - 1];
}
#endif // APP_STATE_H

129
firmware/GridSeq/channel.h Normal file
View File

@ -0,0 +1,129 @@
/**
* @file channel.h
* @author Adam Wonak (https://github.com/awonak/)
* @brief Grid Sequencer.
* @version 1.0.0
* @date 2025-08-12
*
* @copyright MIT - (c) 2025 - Adam Wonak - adam.wonak@gmail.com
*
*/
#ifndef CHANNEL_H
#define CHANNEL_H
#include <Arduino.h>
#include <libGravity.h>
#include "euclidean.h"
// Enums for CV Mod destination
enum CvDestination : uint8_t {
CV_DEST_NONE,
CV_DEST_MODE,
CV_DEST_LENGTH,
CV_DEST_DIV,
CV_DEST_PROB,
CV_DEST_DENSITY,
CV_DEST_LAST,
};
// Enums for GridSeq modes
enum Mode : uint8_t {
MODE_SEQ,
MODE_EUCLIDEAN,
MODE_PATTERN,
MODE_LAST,
};
class Channel {
public:
Channel() {
Init();
}
void Init() {
base_probability = 100;
cv1_dest = CV_DEST_NONE;
cv2_dest = CV_DEST_NONE;
}
bool isCvModActive() const { return cv1_dest != CV_DEST_NONE || cv2_dest != CV_DEST_NONE; }
// Setters (Set the BASE value)
void setProbability(int prob) {
base_probability = constrain(prob, 0, 100);
}
void setCv1Dest(CvDestination dest) {
cv1_dest = dest;
}
void setCv2Dest(CvDestination dest) {
cv2_dest = dest;
}
CvDestination getCv1Dest() const { return cv1_dest; }
CvDestination getCv2Dest() const { return cv2_dest; }
// Getters (Get the BASE value for editing or cv modded value for display)
int getProbability() const { return base_probability; }
// Getters that calculate the value with CV modulation applied.
int getProbabilityWithMod(int cv1_val, int cv2_val) {
int prob_mod = _calculateMod(CV_DEST_PROB, cv1_val, cv2_val, -50, 50);
return constrain(base_probability + prob_mod, 0, 100);
}
void toggleMute() { mute = !mute; }
/**
* @brief Processes a clock tick and determines if the output should be high or low.
* Note: this method is called from an ISR and must be kept as simple as possible.
* @param tick The current clock tick count.
* @param output The output object to be modified.
*/
void processClockTick(uint32_t tick, DigitalOutput& output) {
// Mute check
if (mute) {
output.Low();
return;
}
int cv1 = gravity.cv1.Read();
int cv2 = gravity.cv2.Read();
int cvmod_probability = getProbabilityWithMod(cv1, cv2);
// Duty cycle high check logic
if (!output.On()) {
// Step check
bool hit = cvmod_probability >= random(0, 100);
if (hit) {
output.Trigger();
}
}
}
private:
int _calculateMod(CvDestination dest, int cv1_val, int cv2_val, int min_range, int max_range) {
int mod1 = (cv1_dest == dest) ? map(cv1_val, -512, 512, min_range, max_range) : 0;
int mod2 = (cv2_dest == dest) ? map(cv2_val, -512, 512, min_range, max_range) : 0;
return mod1 + mod2;
}
// User-settable base values.
byte base_probability;
// CV mod configuration
CvDestination cv1_dest;
CvDestination cv2_dest;
// Mute channel flag
bool mute;
uint16_t _duty_pulses;
};
#endif // CHANNEL_H

View File

@ -0,0 +1,93 @@
/**
* @file display.h
* @author Adam Wonak (https://github.com/awonak/)
* @brief Alt firmware version of Gravity by Sitka Instruments.
* @version 1.0.0
* @date 2025-07-04
*
* @copyright MIT - (c) 2025 - Adam Wonak - adam.wonak@gmail.com
*
*/
#ifndef DISPLAY_H
#define DISPLAY_H
#include <Arduino.h>
#include "app_state.h"
//
// UI Display functions for drawing the UI to the OLED display.
//
/*
* Font: velvetscreen.bdf 9pt
* https://stncrn.github.io/u8g2-unifont-helper/
* "%/0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
*/
const uint8_t TEXT_FONT[437] U8G2_FONT_SECTION("velvetscreen") PROGMEM =
"\64\0\2\2\3\3\2\3\4\5\5\0\0\5\0\5\0\0\221\0\0\1\230 \4\200\134%\11\255tT"
"R\271RI(\6\252\334T\31)\7\252\134bJ\12+\7\233\345\322J\0,\5\221T\4-\5\213"
"f\6.\5\211T\2/\6\244\354c\33\60\10\254\354T\64\223\2\61\7\353\354\222\254\6\62\11\254l"
"\66J*\217\0\63\11\254l\66J\32\215\4\64\10\254l\242\34\272\0\65\11\254l\206\336h$\0\66"
"\11\254\354T^\61)\0\67\10\254lF\216u\4\70\11\254\354TL*&\5\71\11\254\354TL;"
")\0:\6\231UR\0A\10\254\354T\34S\6B\11\254lV\34)\216\4C\11\254\354T\324\61"
")\0D\10\254lV\64G\2E\10\254l\206\36z\4F\10\254l\206^\71\3G\11\254\354TN"
"\63)\0H\10\254l\242\34S\6I\6\251T\206\0J\10\254\354k\231\24\0K\11\254l\242J\62"
"\225\1L\7\254lr{\4M\11\255t\362ZI\353\0N\11\255t\362TI\356\0O\10\254\354T"
"\64\223\2P\11\254lV\34)g\0Q\10\254\354T\264b\12R\10\254lV\34\251\31S\11\254\354"
"FF\32\215\4T\7\253dVl\1U\10\254l\242\63)\0V\11\255t\262Ne\312\21W\12\255"
"t\262J*\251.\0X\11\254l\242L*\312\0Y\12\255tr\252\63\312(\2Z\7\253df*"
"\7p\10\255\364V\266\323\2q\7\255\364\216\257\5r\10\253d\242\32*\2t\6\255t\376#w\11"
"\255\364V\245FN\13x\6\233dR\7\0\0\0\4\377\377\0";
/*
* Font: STK-L.bdf 36pt
* https://stncrn.github.io/u8g2-unifont-helper/
* "%/0123456789ABCDEFILNORSTUVXx"
*/
const uint8_t LARGE_FONT[766] U8G2_FONT_SECTION("stk-l") PROGMEM =
"\35\0\4\4\4\5\3\1\6\20\30\0\0\27\0\0\0\1\77\0\0\2\341%'\17;\226\261\245FL"
"\64B\214\30\22\223\220)Bj\10Q\232\214\42R\206\310\210\21d\304\30\32a\254\304\270!\0/\14"
"\272\272\275\311H\321g\343\306\1\60\37|\373\35CJT\20:fW\207\320\210\60\42\304\204\30D\247"
"\214\331\354\20\11%\212\314\0\61\24z\275\245a\244\12\231\71\63b\214\220q\363\377(E\6\62\33|"
"\373\35ShT\20:fl\344\14\211\231\301\306T\71\202#g\371\340\201\1\63\34|\373\35ShT"
"\20:fl\344@r\264\263\222\344,\215\35\42\241\6\225\31\0\64 |\373-!\203\206\214!\62\204"
"\314\220A#\10\215\30\65b\324\210Q\306\354\354\1\213\225\363\1\65\32|\373\15\25[\214\234/\10)"
"Y\61j\350\310Y\32;DB\15*\63\0\66\33}\33\236SiV\14;gt^\230Y\302\202\324"
"\71\273;EbM\252\63\0\67\23|\373\205\25\17R\316\207\344\350p\312\201#\347\35\0\70 |\373"
"\35ShT\20:f\331!\22D\310 :\205\206\10\11B\307\354\354\20\11\65\250\314\0\71\32|\373"
"\35ShT\20:fg\207H,Q\223r\276\30DB\15*\63\0A\26}\33\246r\247\322P\62"
"j\310\250\21\343\354\335\203\357\354w\3B$}\33\206Dj\226\214\42\61l\304\260\21\303F\14\33\61"
"\212\304\222MF\221\30v\316\236=\10\301b\11\0C\27}\33\236Si\226\20Bft\376O\211\215"
" Db\215\42$\0D\33}\33\206Dj\226\214\32\62l\304\260\21\343\354\177vl\304(\22K\324"
"$\2E\22|\373\205\17R\316KD\30\215\234_>x`\0F\20|\373\205\17R\316\227i\262\31"
"\71\377\22\0I\7s\333\204\77HL\15{\333\205\201\363\377\77|\360`\0N$}\33\6\201\346\314"
"\35;\206\12U\242D&\306\230\30cd\210\221!fF\230\31a(+\314\256\63\67\0O\26}\33"
"\236Si\226\214\32\61\316\376\277\33\61j\310\232Tg\0R\61\216;\6Ek\230\14#\61n\304\270"
"\21\343F\214\33\61n\304\60\22\243\210\60Q\224j\310\260\61\243\306\20\232\325\230QD\206\221\30\67b"
"\334\301\1S\42\216;\236c\211\226\220\42\61n\304\270\21c\307R\232,[\262\203\307\216\65h\16\25"
"\21&\253\320\0T\15}\33\206\17R\15\235\377\377\25\0U\21|\373\205a\366\377\237\215\30\64D\15"
"*\63\0V\26\177\371\205\221\366\377\313\21\343\206\220\42C\25\11r'\313\16\3X)~;\206\201\6"
"\217\221\30\66\204\20\31\42\244\206\14Cg\320$Q\222\6\315!\33\62\212\10\31BD\206\215 v\320"
"\302\1x\24\312\272\205A\206\216\220@c\212\224\31$S\14\262h\0\0\0\0\4\377\377\0";
#define play_icon_width 14
#define play_icon_height 14
static const unsigned char play_icon[28] PROGMEM = {
0x00, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x7C, 0x00, 0xFC, 0x00, 0xFC, 0x03,
0xFC, 0x0F, 0xFC, 0x0F, 0xFC, 0x03, 0xFC, 0x00, 0x7C, 0x00, 0x3C, 0x00,
0x00, 0x00, 0x00, 0x00};
static const unsigned char pause_icon[28] PROGMEM = {
0x00, 0x00, 0x00, 0x00, 0x38, 0x0E, 0x38, 0x0E, 0x38, 0x0E, 0x38, 0x0E,
0x38, 0x0E, 0x38, 0x0E, 0x38, 0x0E, 0x38, 0x0E, 0x38, 0x0E, 0x38, 0x0E,
0x38, 0x0E, 0x00, 0x00};
void UpdateDisplay() {
app.refresh_screen = false;
gravity.display.firstPage();
do {
} while (gravity.display.nextPage());
}
#endif // DISPLAY_H

View File

@ -0,0 +1,100 @@
/**
* @file euclidean.h
* @author Adam Wonak (https://github.com/awonak/)
* @brief Alt firmware version of Gravity by Sitka Instruments.
* @version 2.0.1
* @date 2025-07-04
*
* @copyright MIT - (c) 2025 - Adam Wonak - adam.wonak@gmail.com
*
*/
#ifndef EUCLIDEAN_H
#define EUCLIDEAN_H
#define MAX_PATTERN_LEN 32
struct EuclideanState {
uint8_t steps;
uint8_t hits;
uint8_t offset;
uint8_t padding;
};
const EuclideanState DEFAULT_PATTERN = {1, 1};
class Euclidean {
public:
Euclidean() {}
~Euclidean() {}
enum Step : uint8_t {
REST,
HIT,
};
void Init(EuclideanState state) {
steps_ = constrain(state.steps, 1, MAX_PATTERN_LEN);
hits_ = constrain(state.hits, 1, steps_);
updatePattern();
}
EuclideanState GetState() const { return {steps_, hits_}; }
Step GetCurrentStep(byte i) {
if (i >= MAX_PATTERN_LEN) return REST;
return (pattern_bitmap_ & (1UL << i)) ? HIT : REST;
}
void SetSteps(int steps) {
steps_ = constrain(steps, 1, MAX_PATTERN_LEN);
hits_ = min(hits_, steps_);
updatePattern();
}
void SetHits(int hits) {
hits_ = constrain(hits, 1, steps_);
updatePattern();
}
void Reset() { step_index_ = 0; }
uint8_t GetSteps() const { return steps_; }
uint8_t GetHits() const { return hits_; }
uint8_t GetStepIndex() const { return step_index_; }
Step NextStep() {
if (steps_ == 0) return REST;
Step value = GetCurrentStep(step_index_);
step_index_ = (step_index_ < steps_ - 1) ? step_index_ + 1 : 0;
return value;
}
private:
uint8_t steps_ = 0;
uint8_t hits_ = 0;
volatile uint8_t step_index_ = 0;
uint32_t pattern_bitmap_ = 0;
// Update the euclidean rhythm pattern using bitmap
void updatePattern() {
pattern_bitmap_ = 0; // Clear the bitmap
if (steps_ == 0) return;
byte bucket = 0;
// Set the first bit (index 0) if it's a HIT
pattern_bitmap_ |= (1UL << 0);
for (int i = 1; i < steps_; i++) {
bucket += hits_;
if (bucket >= steps_) {
bucket -= steps_;
pattern_bitmap_ |= (1UL << i);
}
}
}
};
#endif

0
firmware/GridSeq/step.h Normal file
View File

View File

@ -1,5 +1,5 @@
name=libGravity name=libGravity
version=2.0.1 version=2.0.0
author=Adam Wonak author=Adam Wonak
maintainer=awonak <github.com/awonak> maintainer=awonak <github.com/awonak>
sentence=Hardware abstraction library for Sitka Instruments Gravity eurorack module sentence=Hardware abstraction library for Sitka Instruments Gravity eurorack module
@ -7,4 +7,4 @@ category=Other
license=MIT license=MIT
url=https://github.com/awonak/libGravity url=https://github.com/awonak/libGravity
architectures=avr architectures=avr
depends=uClock,RotaryEncoder,U8g2,NeoHWSerial depends=uClock,RotaryEncoder,U8g2

View File

@ -2,8 +2,8 @@
* @file analog_input.h * @file analog_input.h
* @author Adam Wonak (https://github.com/awonak) * @author Adam Wonak (https://github.com/awonak)
* @brief Class for interacting with analog inputs. * @brief Class for interacting with analog inputs.
* @version 2.0.0 * @version 0.1
* @date 2025-08-17 * @date 2025-05-23
* *
* @copyright MIT - (c) 2025 - Adam Wonak - adam.wonak@gmail.com * @copyright MIT - (c) 2025 - Adam Wonak - adam.wonak@gmail.com
* *

View File

@ -2,8 +2,8 @@
* @file button.h * @file button.h
* @author Adam Wonak (https://github.com/awonak) * @author Adam Wonak (https://github.com/awonak)
* @brief Wrapper class for interacting with trigger / gate inputs. * @brief Wrapper class for interacting with trigger / gate inputs.
* @version 2.0.0 * @version 0.1
* @date 2025-08-17 * @date 2025-04-20
* *
* @copyright MIT - (c) 2025 - Adam Wonak - adam.wonak@gmail.com * @copyright MIT - (c) 2025 - Adam Wonak - adam.wonak@gmail.com
* *

View File

@ -2,8 +2,8 @@
* @file clock.h * @file clock.h
* @author Adam Wonak (https://github.com/awonak) * @author Adam Wonak (https://github.com/awonak)
* @brief Wrapper Class for clock timing functions. * @brief Wrapper Class for clock timing functions.
* @version 2.0.0 * @version 0.1
* @date 2025-08-17 * @date 2025-05-04
* *
* @copyright MIT - (c) 2025 - Adam Wonak - adam.wonak@gmail.com * @copyright MIT - (c) 2025 - Adam Wonak - adam.wonak@gmail.com
* *
@ -12,12 +12,20 @@
#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:
@ -41,11 +49,19 @@ 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();
} }
@ -64,6 +80,10 @@ 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:
@ -82,6 +102,9 @@ 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) {
@ -137,6 +160,37 @@ 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

View File

@ -1,197 +0,0 @@
/**
* @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

@ -2,8 +2,8 @@
* @file digital_output.h * @file digital_output.h
* @author Adam Wonak (https://github.com/awonak) * @author Adam Wonak (https://github.com/awonak)
* @brief Class for interacting with trigger / gate outputs. * @brief Class for interacting with trigger / gate outputs.
* @version 2.0.0 * @version 0.1
* @date 2025-08-17 * @date 2025-04-17
* *
* @copyright MIT - (c) 2025 - Adam Wonak - adam.wonak@gmail.com * @copyright MIT - (c) 2025 - Adam Wonak - adam.wonak@gmail.com
* *

View File

@ -1,9 +1,9 @@
/** /**
* @file encoder.h * @file encoder_dir.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 0.1
* @date 2025-08-17 * @date 2025-04-19
* *
* @copyright MIT - (c) 2025 - Adam Wonak - adam.wonak@gmail.com * @copyright MIT - (c) 2025 - Adam Wonak - adam.wonak@gmail.com
* *

View File

@ -2,8 +2,8 @@
* @file libGravity.cpp * @file libGravity.cpp
* @author Adam Wonak (https://github.com/awonak) * @author Adam Wonak (https://github.com/awonak)
* @brief Library for building custom scripts for the Sitka Instruments Gravity module. * @brief Library for building custom scripts for the Sitka Instruments Gravity module.
* @version 2.0.0 * @version 0.1
* @date 2025-08-17 * @date 2025-04-19
* *
* @copyright MIT - (c) 2025 - Adam Wonak - adam.wonak@gmail.com * @copyright MIT - (c) 2025 - Adam Wonak - adam.wonak@gmail.com
* *
@ -33,7 +33,6 @@ 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/
@ -43,14 +42,6 @@ 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() {
@ -83,7 +74,6 @@ 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();
@ -92,8 +82,6 @@ ISR(PCINT2_vect) {
ISR(PCINT1_vect) { ISR(PCINT1_vect) {
Encoder::isr(); Encoder::isr();
}; };
#endif
// Global instance // Global instance
Gravity gravity; Gravity gravity;

View File

@ -2,8 +2,8 @@
* @file libGravity.h * @file libGravity.h
* @author Adam Wonak (https://github.com/awonak) * @author Adam Wonak (https://github.com/awonak)
* @brief Library for building custom scripts for the Sitka Instruments Gravity module. * @brief Library for building custom scripts for the Sitka Instruments Gravity module.
* @version 2.0.0 * @version 0.1
* @date 2025-08-17 * @date 2025-04-19
* *
* @copyright MIT - (c) 2025 - Adam Wonak - adam.wonak@gmail.com * @copyright MIT - (c) 2025 - Adam Wonak - adam.wonak@gmail.com
* *

View File

@ -2,8 +2,8 @@
* @file peripherials.h * @file peripherials.h
* @author Adam Wonak (https://github.com/awonak) * @author Adam Wonak (https://github.com/awonak)
* @brief Arduino pin definitions for the Sitka Instruments Gravity module. * @brief Arduino pin definitions for the Sitka Instruments Gravity module.
* @version 2.0.0 * @version 0.1
* @date 2025-08-17 * @date 2025-04-19
* *
* @copyright MIT - (c) 2025 - Adam Wonak - adam.wonak@gmail.com * @copyright MIT - (c) 2025 - Adam Wonak - adam.wonak@gmail.com
* *
@ -24,8 +24,8 @@
// Clock and CV Inputs // Clock and CV Inputs
#define EXT_PIN 2 #define EXT_PIN 2
#define CV1_PIN 21 // A7 #define CV1_PIN A7
#define CV2_PIN 20 // A6 #define CV2_PIN A6
#define PULSE_OUT_PIN 3 #define PULSE_OUT_PIN 3
// Button pins // Button pins

View File

@ -1,72 +0,0 @@
#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,13 +32,7 @@
* 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