Compare commits
16 Commits
reduce-mem
...
gravity-r4
| Author | SHA1 | Date | |
|---|---|---|---|
| c6ce5b1309 | |||
| acd028846c | |||
| ed625e75fc | |||
| b60dcc0e68 | |||
| 909d589609 | |||
| 330f5e6ceb | |||
| 87dacd869b | |||
| 64f467d6ac | |||
| 84cafe2387 | |||
| 8bb89a5f4b | |||
| 499bc7a643 | |||
| 3f670fa9f7 | |||
| b5029bde88 | |||
| 4bcd618073 | |||
| 6ada2aba30 | |||
| c5965aa1f7 |
31
README.md
31
README.md
@ -1,6 +1,18 @@
|
||||
# Sitka Instruments Gravity Firmware Abstraction
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
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
|
||||
|
||||
@ -17,13 +29,14 @@ 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.
|
||||
* [RotateEncoder](https://github.com/mathertel/RotaryEncoder) [BSD] - Library for reading and interpreting encoder rotation.
|
||||
* [U8g2](https://github.com/olikraus/u8g2/) [MIT] - Graphics helper library.
|
||||
* [NeoHWSerial](https://github.com/SlashDevin/NeoHWSerial) [GPL] - Hardware serial library with attachInterrupt.
|
||||
|
||||
## 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.
|
||||
|
||||
```cpp
|
||||
#include "gravity.h"
|
||||
#include "libGravity.h"
|
||||
|
||||
byte idx = 0;
|
||||
bool reversed = false;
|
||||
@ -75,11 +88,11 @@ void HandlePlayPressed() {
|
||||
}
|
||||
}
|
||||
|
||||
void HandleRotate(Direction dir, int val) {
|
||||
void HandleRotate(int val) {
|
||||
if (selected_param == 0) {
|
||||
gravity.clock.SetTempo(gravity.clock.Tempo() + val);
|
||||
} else if (selected_param == 1) {
|
||||
reversed = (dir == DIRECTION_DECREMENT);
|
||||
reversed = (val < 0);
|
||||
}
|
||||
}
|
||||
|
||||
@ -111,8 +124,16 @@ 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
|
||||
|
||||
```
|
||||
$ arduino-cli compile -v -b arduino:avr:nano ./firmware/Gravity/Gravity.ino -e --output-dir=./build/
|
||||
```
|
||||
```
|
||||
|
||||
@ -17,7 +17,7 @@
|
||||
* TODO: Store the calibration value in EEPROM.
|
||||
*/
|
||||
|
||||
#include "gravity.h"
|
||||
#include "libGravity.h"
|
||||
|
||||
#define TEXT_FONT u8g2_font_profont11_tf
|
||||
#define INDICATOR_FONT u8g2_font_open_iconic_arrow_1x_t
|
||||
@ -43,7 +43,7 @@ void NextCalibrationPoint() {
|
||||
selected_param = (selected_param + 1) % 6;
|
||||
}
|
||||
|
||||
void CalibrateCV(Direction dir, int val) {
|
||||
void CalibrateCV(int val) {
|
||||
AnalogInput* cv = (selected_param > 2) ? &gravity.cv2 : &gravity.cv1;
|
||||
switch (selected_param % 3) {
|
||||
case 0:
|
||||
|
||||
@ -14,7 +14,7 @@
|
||||
*
|
||||
*/
|
||||
|
||||
#include "gravity.h"
|
||||
#include "libGravity.h"
|
||||
|
||||
#define TEXT_FONT u8g2_font_profont11_tf
|
||||
|
||||
@ -39,7 +39,7 @@ void NextCalibrationPoint() {
|
||||
selected_param = (selected_param + 1) % 2;
|
||||
}
|
||||
|
||||
void CalibrateCV(Direction dir, int val) {
|
||||
void CalibrateCV(int val) {
|
||||
// AnalogInput* cv = (selected_param > 2) ? &gravity.cv2 : &gravity.cv1;
|
||||
AnalogInput* cv = &gravity.cv1;
|
||||
switch (selected_param % 2) {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
#include "gravity.h"
|
||||
#include "libGravity.h"
|
||||
|
||||
byte idx = 0;
|
||||
bool reversed = false;
|
||||
@ -33,28 +33,28 @@ void IntClock(uint32_t tick) {
|
||||
if (tick % 12 == 0 && ! freeze) {
|
||||
gravity.outputs[idx].Low();
|
||||
if (reversed) {
|
||||
idx = (idx == 0) ? OUTPUT_COUNT - 1 : idx - 1;
|
||||
idx = (idx == 0) ? Gravity::OUTPUT_COUNT - 1 : idx - 1;
|
||||
} else {
|
||||
idx = (idx + 1) % OUTPUT_COUNT;
|
||||
idx = (idx + 1) % Gravity::OUTPUT_COUNT;
|
||||
}
|
||||
gravity.outputs[idx].High();
|
||||
}
|
||||
}
|
||||
|
||||
void HandlePlayPressed() {
|
||||
gravity.clock.Pause();
|
||||
gravity.clock.Stop();
|
||||
if (gravity.clock.IsPaused()) {
|
||||
for (int i = 0; i < OUTPUT_COUNT; i++) {
|
||||
for (int i = 0; i < Gravity::OUTPUT_COUNT; i++) {
|
||||
gravity.outputs[i].Low();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void HandleRotate(Direction dir, int val) {
|
||||
void HandleRotate(int val) {
|
||||
if (selected_param == 0) {
|
||||
gravity.clock.SetTempo(gravity.clock.Tempo() + val);
|
||||
} else if (selected_param == 1) {
|
||||
reversed = (dir == DIRECTION_DECREMENT);
|
||||
reversed = (val < 0);
|
||||
}
|
||||
}
|
||||
|
||||
@ -80,7 +80,7 @@ void UpdateDisplay() {
|
||||
gravity.display.print("Direction: ");
|
||||
gravity.display.print((reversed) ? "Backward" : "Forward");
|
||||
|
||||
gravity.display.drawChar(0, selected_param * 10, 0x10, 1, 0, 1);
|
||||
gravity.display.drawStr(0, selected_param * 10, "x");
|
||||
|
||||
gravity.display.display();
|
||||
}
|
||||
118
examples/skeleton/skeleton.ino
Normal file
118
examples/skeleton/skeleton.ino
Normal file
@ -0,0 +1,118 @@
|
||||
/**
|
||||
* @file skeleton.ino
|
||||
* @author YOUR_NAME (<url>)
|
||||
* @brief Skeleton app for Sitka Instruments Gravity.
|
||||
* @version vX.Y.Z - MONTH YEAR YOUR_NAME
|
||||
* @date YYYY-MM-DD
|
||||
*
|
||||
* @copyright MIT - (c) 2025 - Adam Wonak - adam.wonak@gmail.com
|
||||
*
|
||||
* Skeleton app for basic structure of a new firmware for Sitka Instruments
|
||||
* Gravity using the libGravity library.
|
||||
*
|
||||
* ENCODER:
|
||||
* Press: change between selecting a parameter and editing the parameter.
|
||||
* Hold & Rotate: change current selected output channel.
|
||||
*
|
||||
* BTN1:
|
||||
* Play/pause - start or stop the internal clock.
|
||||
*
|
||||
* BTN2:
|
||||
* Shift - hold and rotate encoder to change current selected output channel.
|
||||
*
|
||||
* EXT:
|
||||
* External clock input. When Gravity is set to INTERNAL or MIDI clock
|
||||
* source, this input is used to reset clocks.
|
||||
*
|
||||
* CV1:
|
||||
* External analog input used to provide modulation to any channel parameter.
|
||||
*
|
||||
* CV2:
|
||||
* External analog input used to provide modulation to any channel parameter.
|
||||
*
|
||||
*/
|
||||
|
||||
#include <libGravity.h>
|
||||
|
||||
|
||||
|
||||
// Global state for settings and app behavior.
|
||||
struct AppState {
|
||||
int tempo = Clock::DEFAULT_TEMPO;
|
||||
Clock::Source selected_source = Clock::SOURCE_INTERNAL;
|
||||
// Add app specific state variables here.
|
||||
};
|
||||
|
||||
AppState app;
|
||||
|
||||
//
|
||||
// Arduino setup and loop.
|
||||
//
|
||||
|
||||
void setup() {
|
||||
// Start Gravity.
|
||||
gravity.Init();
|
||||
|
||||
// Clock handlers.
|
||||
gravity.clock.AttachIntHandler(HandleIntClockTick);
|
||||
gravity.clock.AttachExtHandler(HandleExtClockTick);
|
||||
|
||||
// Encoder rotate and press handlers.
|
||||
gravity.encoder.AttachPressHandler(HandleEncoderPressed);
|
||||
gravity.encoder.AttachRotateHandler(HandleRotate);
|
||||
gravity.encoder.AttachPressRotateHandler(HandlePressedRotate);
|
||||
|
||||
// Button press handlers.
|
||||
gravity.play_button.AttachPressHandler(HandlePlayPressed);
|
||||
}
|
||||
|
||||
void loop() {
|
||||
// Process change in state of inputs and outputs.
|
||||
gravity.Process();
|
||||
|
||||
// Non-ISR loop behavior.
|
||||
}
|
||||
|
||||
//
|
||||
// Firmware handlers for clocks.
|
||||
//
|
||||
|
||||
void HandleIntClockTick(uint32_t tick) {
|
||||
bool refresh = false;
|
||||
for (int i = 0; i < Gravity::OUTPUT_COUNT; i++) {
|
||||
// Process each output tick handlers.
|
||||
}
|
||||
}
|
||||
|
||||
void HandleExtClockTick() {
|
||||
switch (app.selected_source) {
|
||||
case Clock::SOURCE_INTERNAL:
|
||||
case Clock::SOURCE_EXTERNAL_MIDI:
|
||||
// Use EXT as Reset when not used for clock source.
|
||||
gravity.clock.Reset();
|
||||
break;
|
||||
default:
|
||||
// Register EXT cv clock tick.
|
||||
gravity.clock.Tick();
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// UI handlers for encoder and buttons.
|
||||
//
|
||||
|
||||
void HandlePlayPressed() {
|
||||
}
|
||||
|
||||
void HandleEncoderPressed() {
|
||||
}
|
||||
|
||||
void HandleRotate(int val) {
|
||||
}
|
||||
|
||||
void HandlePressedRotate(int val) {
|
||||
}
|
||||
|
||||
//
|
||||
// Application logic goes here.
|
||||
//
|
||||
118
examples/test_r4/encoder.h
Normal file
118
examples/test_r4/encoder.h
Normal 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
|
||||
76
examples/test_r4/test_r4.ino
Normal file
76
examples/test_r4/test_r4.ino
Normal 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());
|
||||
}
|
||||
@ -2,7 +2,7 @@
|
||||
* @file Gravity.ino
|
||||
* @author Adam Wonak (https://github.com/awonak/)
|
||||
* @brief Alt firmware version of Gravity by Sitka Instruments.
|
||||
* @version v2.0.0 - June 2025 awonak - Full rewrite
|
||||
* @version v2.0.0 - August 2025 awonak - Full rewrite
|
||||
* @version v1.0 - August 2023 Oleksiy H - Initial release
|
||||
* @date 2025-07-04
|
||||
*
|
||||
@ -66,10 +66,6 @@ void setup() {
|
||||
// Start Gravity.
|
||||
gravity.Init();
|
||||
|
||||
// Show bootsplash when initializing firmware.
|
||||
Bootsplash();
|
||||
delay(2000);
|
||||
|
||||
// Initialize the state manager. This will load settings from EEPROM
|
||||
stateManager.initialize(app);
|
||||
InitGravity(app);
|
||||
@ -213,31 +209,42 @@ void HandleEncoderPressed() {
|
||||
// Check if leaving editing mode should apply a selection.
|
||||
if (app.editing_param) {
|
||||
if (app.selected_channel == 0) { // main page
|
||||
// TODO: rewrite as switch
|
||||
if (app.selected_param == PARAM_MAIN_ENCODER_DIR) {
|
||||
app.encoder_reversed = app.selected_sub_param == 1;
|
||||
gravity.encoder.SetReverseDirection(app.encoder_reversed);
|
||||
}
|
||||
if (app.selected_param == PARAM_MAIN_SAVE_DATA) {
|
||||
if (app.selected_sub_param < StateManager::MAX_SAVE_SLOTS) {
|
||||
app.selected_save_slot = app.selected_sub_param;
|
||||
stateManager.saveData(app);
|
||||
}
|
||||
}
|
||||
if (app.selected_param == PARAM_MAIN_LOAD_DATA) {
|
||||
if (app.selected_sub_param < StateManager::MAX_SAVE_SLOTS) {
|
||||
app.selected_save_slot = app.selected_sub_param;
|
||||
stateManager.loadData(app, app.selected_save_slot);
|
||||
InitGravity(app);
|
||||
}
|
||||
}
|
||||
if (app.selected_param == PARAM_MAIN_FACTORY_RESET) {
|
||||
if (app.selected_sub_param == 0) { // Erase
|
||||
// Show bootsplash during slow erase operation.
|
||||
Bootsplash();
|
||||
stateManager.factoryReset(app);
|
||||
InitGravity(app);
|
||||
}
|
||||
switch (app.selected_param) {
|
||||
case PARAM_MAIN_ENCODER_DIR:
|
||||
app.encoder_reversed = app.selected_sub_param == 1;
|
||||
gravity.encoder.SetReverseDirection(app.encoder_reversed);
|
||||
break;
|
||||
case PARAM_MAIN_ROTATE_DISP:
|
||||
app.rotate_display = app.selected_sub_param == 1;
|
||||
gravity.display.setFlipMode(app.rotate_display ? 1 : 0);
|
||||
break;
|
||||
case PARAM_MAIN_SAVE_DATA:
|
||||
if (app.selected_sub_param < StateManager::MAX_SAVE_SLOTS) {
|
||||
app.selected_save_slot = app.selected_sub_param;
|
||||
stateManager.saveData(app);
|
||||
}
|
||||
break;
|
||||
case PARAM_MAIN_LOAD_DATA:
|
||||
if (app.selected_sub_param < StateManager::MAX_SAVE_SLOTS) {
|
||||
app.selected_save_slot = app.selected_sub_param;
|
||||
// Load pattern data into app state.
|
||||
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);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case PARAM_MAIN_FACTORY_RESET:
|
||||
if (app.selected_sub_param == 0) { // Erase
|
||||
stateManager.factoryReset(app);
|
||||
InitGravity(app);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Only mark dirty and reset selected_sub_param when leaving editing mode.
|
||||
@ -313,6 +320,7 @@ void editMainParameter(int val) {
|
||||
}
|
||||
// These changes are applied upon encoder button press.
|
||||
case PARAM_MAIN_ENCODER_DIR:
|
||||
case PARAM_MAIN_ROTATE_DISP:
|
||||
updateSelection(app.selected_sub_param, val, 2);
|
||||
break;
|
||||
case PARAM_MAIN_SAVE_DATA:
|
||||
@ -381,6 +389,7 @@ void InitGravity(AppState& app) {
|
||||
gravity.clock.SetTempo(app.tempo);
|
||||
gravity.clock.SetSource(app.selected_source);
|
||||
gravity.encoder.SetReverseDirection(app.encoder_reversed);
|
||||
gravity.display.setFlipMode(app.rotate_display ? 1 : 0);
|
||||
}
|
||||
|
||||
void ResetOutputs() {
|
||||
|
||||
@ -2,8 +2,8 @@
|
||||
* @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
|
||||
* @version 2.0.0
|
||||
* @date 2025-08-17
|
||||
*
|
||||
* @copyright MIT - (c) 2025 - Adam Wonak - adam.wonak@gmail.com
|
||||
*
|
||||
@ -31,6 +31,7 @@ struct AppState {
|
||||
Clock::Pulse selected_pulse = Clock::PULSE_PPQN_24;
|
||||
bool editing_param = false;
|
||||
bool encoder_reversed = false;
|
||||
bool rotate_display = false;
|
||||
bool refresh_screen = true;
|
||||
};
|
||||
|
||||
|
||||
@ -2,8 +2,8 @@
|
||||
* @file channel.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
|
||||
* @version 2.0.0
|
||||
* @date 2025-08-17
|
||||
*
|
||||
* @copyright MIT - (c) 2025 - Adam Wonak - adam.wonak@gmail.com
|
||||
*
|
||||
@ -86,6 +86,7 @@ class Channel {
|
||||
|
||||
void setClockMod(int index) {
|
||||
base_clock_mod_index = constrain(index, 0, MOD_CHOICE_SIZE - 1);
|
||||
_recalculatePulses();
|
||||
}
|
||||
|
||||
void setProbability(int prob) {
|
||||
@ -94,13 +95,16 @@ class Channel {
|
||||
|
||||
void setDutyCycle(int duty) {
|
||||
base_duty_cycle = constrain(duty, 1, 99);
|
||||
_recalculatePulses();
|
||||
}
|
||||
|
||||
void setOffset(int off) {
|
||||
base_offset = constrain(off, 0, 99);
|
||||
_recalculatePulses();
|
||||
}
|
||||
void setSwing(int val) {
|
||||
base_swing = constrain(val, 50, 95);
|
||||
_recalculatePulses();
|
||||
}
|
||||
|
||||
// Euclidean
|
||||
|
||||
@ -2,8 +2,8 @@
|
||||
* @file display.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
|
||||
* @version 2.0.0
|
||||
* @date 2025-08-17
|
||||
*
|
||||
* @copyright MIT - (c) 2025 - Adam Wonak - adam.wonak@gmail.com
|
||||
*
|
||||
@ -104,6 +104,7 @@ enum ParamsMainPage : uint8_t {
|
||||
PARAM_MAIN_RESET,
|
||||
PARAM_MAIN_PULSE,
|
||||
PARAM_MAIN_ENCODER_DIR,
|
||||
PARAM_MAIN_ROTATE_DISP,
|
||||
PARAM_MAIN_SAVE_DATA,
|
||||
PARAM_MAIN_LOAD_DATA,
|
||||
PARAM_MAIN_FACTORY_RESET,
|
||||
@ -256,9 +257,6 @@ void DisplayMainPage() {
|
||||
case Clock::SOURCE_EXTERNAL_PPQN_4:
|
||||
subText = F("4 PPQN");
|
||||
break;
|
||||
case Clock::SOURCE_EXTERNAL_PPQN_2:
|
||||
subText = F("2 PPQN");
|
||||
break;
|
||||
case Clock::SOURCE_EXTERNAL_PPQN_1:
|
||||
subText = F("1 PPQN");
|
||||
break;
|
||||
@ -316,6 +314,10 @@ void DisplayMainPage() {
|
||||
mainText = F("DIR");
|
||||
subText = app.selected_sub_param == 0 ? F("DEFAULT") : F("REVERSED");
|
||||
break;
|
||||
case PARAM_MAIN_ROTATE_DISP:
|
||||
mainText = F("ROT");
|
||||
subText = app.selected_sub_param == 0 ? F("DEFAULT") : F("FLIPPED");
|
||||
break;
|
||||
case PARAM_MAIN_SAVE_DATA:
|
||||
case PARAM_MAIN_LOAD_DATA:
|
||||
if (app.selected_sub_param == StateManager::MAX_SAVE_SLOTS) {
|
||||
@ -347,7 +349,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("CLK RUN"), F("CLK RESET"), F("PULSE OUT"), F("ENCODER DIR"), F("SAVE"), F("LOAD"), F("ERASE")};
|
||||
String menu_items[PARAM_MAIN_LAST] = {F("TEMPO"), F("SOURCE"), F("CLK RUN"), F("CLK RESET"), F("PULSE OUT"), F("ENCODER DIR"), F("ROTATE DISP"), F("SAVE"), F("LOAD"), F("ERASE")};
|
||||
drawMenuItems(menu_items, PARAM_MAIN_LAST);
|
||||
}
|
||||
|
||||
@ -369,7 +371,7 @@ void DisplayChannelPage() {
|
||||
|
||||
switch (app.selected_param) {
|
||||
case PARAM_CH_MOD: {
|
||||
int mod_value = withCvMod ? ch.getClockModWithMod(cv1, cv2): ch.getClockMod();
|
||||
int mod_value = withCvMod ? ch.getClockModWithMod(cv1, cv2) : ch.getClockMod();
|
||||
if (mod_value > 1) {
|
||||
mainText = F("/");
|
||||
mainText += String(mod_value);
|
||||
@ -497,22 +499,4 @@ void UpdateDisplay() {
|
||||
} while (gravity.display.nextPage());
|
||||
}
|
||||
|
||||
void Bootsplash() {
|
||||
gravity.display.firstPage();
|
||||
do {
|
||||
int textWidth;
|
||||
String loadingText = F("LOADING....");
|
||||
gravity.display.setFont(TEXT_FONT);
|
||||
|
||||
textWidth = gravity.display.getStrWidth(StateManager::SKETCH_NAME);
|
||||
gravity.display.drawStr(16 + (textWidth / 2), 20, StateManager::SKETCH_NAME);
|
||||
|
||||
textWidth = gravity.display.getStrWidth(StateManager::SEMANTIC_VERSION);
|
||||
gravity.display.drawStr(16 + (textWidth / 2), 32, StateManager::SEMANTIC_VERSION);
|
||||
|
||||
textWidth = gravity.display.getStrWidth(loadingText.c_str());
|
||||
gravity.display.drawStr(26 + (textWidth / 2), 44, loadingText.c_str());
|
||||
} while (gravity.display.nextPage());
|
||||
}
|
||||
|
||||
#endif // DISPLAY_H
|
||||
|
||||
@ -2,8 +2,8 @@
|
||||
* @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
|
||||
* @version 2.0.0
|
||||
* @date 2025-08-17
|
||||
*
|
||||
* @copyright MIT - (c) 2025 - Adam Wonak - adam.wonak@gmail.com
|
||||
*
|
||||
|
||||
@ -2,8 +2,8 @@
|
||||
* @file save_state.cpp
|
||||
* @author Adam Wonak (https://github.com/awonak/)
|
||||
* @brief Alt firmware version of Gravity by Sitka Instruments.
|
||||
* @version 2.0.1
|
||||
* @date 2025-07-04
|
||||
* @version 2.0.0
|
||||
* @date 2025-08-17
|
||||
*
|
||||
* @copyright MIT - (c) 2025 - Adam Wonak - adam.wonak@gmail.com
|
||||
*
|
||||
@ -17,7 +17,7 @@
|
||||
|
||||
// Define the constants for the current firmware.
|
||||
const char StateManager::SKETCH_NAME[] = "ALT GRAVITY";
|
||||
const char StateManager::SEMANTIC_VERSION[] = "V2.0.0BETA4"; // NOTE: This should match the version in the library.properties file.
|
||||
const char StateManager::SEMANTIC_VERSION[] = "2.0.0"; // NOTE: This should match the version in the library.properties file.
|
||||
|
||||
// Number of available save slots.
|
||||
const byte StateManager::MAX_SAVE_SLOTS = 10;
|
||||
@ -33,54 +33,66 @@ const int StateManager::EEPROM_DATA_START_ADDR = sizeof(StateManager::Metadata);
|
||||
StateManager::StateManager() : _isDirty(false), _lastChangeTime(0) {}
|
||||
|
||||
bool StateManager::initialize(AppState& app) {
|
||||
noInterrupts();
|
||||
bool success = false;
|
||||
if (_isDataValid()) {
|
||||
// Load global settings.
|
||||
_loadMetadata(app);
|
||||
// Load app data from the transient slot.
|
||||
_loadState(app, TRANSIENT_SLOT);
|
||||
return true;
|
||||
success = true;
|
||||
}
|
||||
// EEPROM does not contain save data for this firmware & version.
|
||||
else {
|
||||
// Erase EEPROM and initialize state. Save default pattern to all save slots.
|
||||
factoryReset(app);
|
||||
return false;
|
||||
}
|
||||
interrupts();
|
||||
return success;
|
||||
}
|
||||
|
||||
bool StateManager::loadData(AppState& app, byte slot_index) {
|
||||
// Check if slot_index is within max range + 1 for transient.
|
||||
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.
|
||||
_loadState(app, slot_index);
|
||||
app.selected_save_slot = slot_index;
|
||||
// Persist this change in the global metadata.
|
||||
_saveMetadata(app);
|
||||
// Persist this change in the global metadata on next update.
|
||||
_isDirty = true;
|
||||
|
||||
interrupts();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Save app state to user specified save slot.
|
||||
void StateManager::saveData(const AppState& app) {
|
||||
noInterrupts();
|
||||
// Check if slot_index is within max range + 1 for transient.
|
||||
if (app.selected_save_slot >= MAX_SAVE_SLOTS + 1) return;
|
||||
|
||||
_saveState(app, app.selected_save_slot);
|
||||
_saveMetadata(app);
|
||||
_isDirty = false;
|
||||
interrupts();
|
||||
}
|
||||
|
||||
// Save transient state if it has changed and enough time has passed since last save.
|
||||
void StateManager::update(const AppState& app) {
|
||||
if (_isDirty && (millis() - _lastChangeTime > SAVE_DELAY_MS)) {
|
||||
noInterrupts();
|
||||
_saveState(app, TRANSIENT_SLOT);
|
||||
_saveMetadata(app);
|
||||
_isDirty = false;
|
||||
interrupts();
|
||||
}
|
||||
}
|
||||
|
||||
void StateManager::reset(AppState& app) {
|
||||
noInterrupts();
|
||||
|
||||
AppState default_app;
|
||||
app.tempo = default_app.tempo;
|
||||
app.selected_param = default_app.selected_param;
|
||||
@ -98,6 +110,7 @@ void StateManager::reset(AppState& app) {
|
||||
_loadMetadata(app);
|
||||
|
||||
_isDirty = false;
|
||||
interrupts();
|
||||
}
|
||||
|
||||
void StateManager::markDirty() {
|
||||
@ -134,7 +147,6 @@ void StateManager::_saveState(const AppState& app, byte slot_index) {
|
||||
// Check if slot_index is within max range + 1 for transient.
|
||||
if (app.selected_save_slot >= MAX_SAVE_SLOTS + 1) return;
|
||||
|
||||
noInterrupts();
|
||||
static EepromData save_data;
|
||||
|
||||
save_data.tempo = app.tempo;
|
||||
@ -165,14 +177,12 @@ void StateManager::_saveState(const AppState& app, byte slot_index) {
|
||||
|
||||
int address = EEPROM_DATA_START_ADDR + (slot_index * sizeof(EepromData));
|
||||
EEPROM.put(address, save_data);
|
||||
interrupts();
|
||||
}
|
||||
|
||||
void StateManager::_loadState(AppState& app, byte slot_index) {
|
||||
// Check if slot_index is within max range + 1 for transient.
|
||||
if (slot_index >= MAX_SAVE_SLOTS + 1) return;
|
||||
|
||||
noInterrupts();
|
||||
static EepromData load_data;
|
||||
int address = EEPROM_DATA_START_ADDR + (slot_index * sizeof(EepromData));
|
||||
EEPROM.get(address, load_data);
|
||||
@ -200,11 +210,9 @@ void StateManager::_loadState(AppState& app, byte slot_index) {
|
||||
ch.setCv1Dest(static_cast<CvDestination>(saved_ch_state.cv1_dest));
|
||||
ch.setCv2Dest(static_cast<CvDestination>(saved_ch_state.cv2_dest));
|
||||
}
|
||||
interrupts();
|
||||
}
|
||||
|
||||
void StateManager::_saveMetadata(const AppState& app) {
|
||||
noInterrupts();
|
||||
Metadata current_meta;
|
||||
strcpy(current_meta.sketch_name, SKETCH_NAME);
|
||||
strcpy(current_meta.version, SEMANTIC_VERSION);
|
||||
@ -212,16 +220,15 @@ void StateManager::_saveMetadata(const AppState& app) {
|
||||
// Global user settings
|
||||
current_meta.selected_save_slot = app.selected_save_slot;
|
||||
current_meta.encoder_reversed = app.encoder_reversed;
|
||||
current_meta.rotate_display = app.rotate_display;
|
||||
|
||||
EEPROM.put(METADATA_START_ADDR, current_meta);
|
||||
interrupts();
|
||||
}
|
||||
|
||||
void StateManager::_loadMetadata(AppState& app) {
|
||||
noInterrupts();
|
||||
Metadata metadata;
|
||||
EEPROM.get(METADATA_START_ADDR, metadata);
|
||||
app.selected_save_slot = metadata.selected_save_slot;
|
||||
app.encoder_reversed = metadata.encoder_reversed;
|
||||
interrupts();
|
||||
app.rotate_display = metadata.rotate_display;
|
||||
}
|
||||
@ -2,8 +2,8 @@
|
||||
* @file save_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
|
||||
* @version 2.0.0
|
||||
* @date 2025-08-17
|
||||
*
|
||||
* @copyright MIT - (c) 2025 - Adam Wonak - adam.wonak@gmail.com
|
||||
*
|
||||
@ -57,6 +57,7 @@ class StateManager {
|
||||
// Additional global/hardware settings
|
||||
byte selected_save_slot;
|
||||
bool encoder_reversed;
|
||||
bool rotate_display;
|
||||
};
|
||||
struct ChannelState {
|
||||
byte base_clock_mod_index;
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
name=libGravity
|
||||
version=2.0.0beta3
|
||||
version=2.0.1
|
||||
author=Adam Wonak
|
||||
maintainer=awonak <github.com/awonak>
|
||||
sentence=Hardware abstraction library for Sitka Instruments Gravity eurorack module
|
||||
@ -7,4 +7,4 @@ category=Other
|
||||
license=MIT
|
||||
url=https://github.com/awonak/libGravity
|
||||
architectures=avr
|
||||
depends=uClock,RotaryEncoder,U8g2
|
||||
depends=uClock,RotaryEncoder,U8g2,NeoHWSerial
|
||||
|
||||
@ -2,8 +2,8 @@
|
||||
* @file analog_input.h
|
||||
* @author Adam Wonak (https://github.com/awonak)
|
||||
* @brief Class for interacting with analog inputs.
|
||||
* @version 0.1
|
||||
* @date 2025-05-23
|
||||
* @version 2.0.0
|
||||
* @date 2025-08-17
|
||||
*
|
||||
* @copyright MIT - (c) 2025 - Adam Wonak - adam.wonak@gmail.com
|
||||
*
|
||||
|
||||
@ -2,8 +2,8 @@
|
||||
* @file button.h
|
||||
* @author Adam Wonak (https://github.com/awonak)
|
||||
* @brief Wrapper class for interacting with trigger / gate inputs.
|
||||
* @version 0.1
|
||||
* @date 2025-04-20
|
||||
* @version 2.0.0
|
||||
* @date 2025-08-17
|
||||
*
|
||||
* @copyright MIT - (c) 2025 - Adam Wonak - adam.wonak@gmail.com
|
||||
*
|
||||
|
||||
67
src/clock.h
67
src/clock.h
@ -2,8 +2,8 @@
|
||||
* @file clock.h
|
||||
* @author Adam Wonak (https://github.com/awonak)
|
||||
* @brief Wrapper Class for clock timing functions.
|
||||
* @version 0.1
|
||||
* @date 2025-05-04
|
||||
* @version 2.0.0
|
||||
* @date 2025-08-17
|
||||
*
|
||||
* @copyright MIT - (c) 2025 - Adam Wonak - adam.wonak@gmail.com
|
||||
*
|
||||
@ -12,20 +12,12 @@
|
||||
#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:
|
||||
@ -35,7 +27,6 @@ class Clock {
|
||||
SOURCE_INTERNAL,
|
||||
SOURCE_EXTERNAL_PPQN_24,
|
||||
SOURCE_EXTERNAL_PPQN_4,
|
||||
SOURCE_EXTERNAL_PPQN_2,
|
||||
SOURCE_EXTERNAL_PPQN_1,
|
||||
SOURCE_EXTERNAL_MIDI,
|
||||
SOURCE_LAST,
|
||||
@ -43,26 +34,18 @@ class Clock {
|
||||
|
||||
enum Pulse {
|
||||
PULSE_NONE,
|
||||
PULSE_PPQN_1,
|
||||
PULSE_PPQN_4,
|
||||
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();
|
||||
}
|
||||
|
||||
@ -81,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:
|
||||
@ -98,18 +77,11 @@ class Clock {
|
||||
uClock.setClockMode(uClock.EXTERNAL_CLOCK);
|
||||
uClock.setInputPPQN(uClock.PPQN_4);
|
||||
break;
|
||||
case SOURCE_EXTERNAL_PPQN_2:
|
||||
uClock.setClockMode(uClock.EXTERNAL_CLOCK);
|
||||
uClock.setInputPPQN(uClock.PPQN_2);
|
||||
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) {
|
||||
@ -165,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
|
||||
197
src/clock_midi.h
Normal file
197
src/clock_midi.h
Normal 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
|
||||
@ -2,8 +2,8 @@
|
||||
* @file digital_output.h
|
||||
* @author Adam Wonak (https://github.com/awonak)
|
||||
* @brief Class for interacting with trigger / gate outputs.
|
||||
* @version 0.1
|
||||
* @date 2025-04-17
|
||||
* @version 2.0.0
|
||||
* @date 2025-08-17
|
||||
*
|
||||
* @copyright MIT - (c) 2025 - Adam Wonak - adam.wonak@gmail.com
|
||||
*
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
/**
|
||||
* @file encoder_dir.h
|
||||
* @file encoder.h
|
||||
* @author Adam Wonak (https://github.com/awonak)
|
||||
* @brief Class for interacting with encoders.
|
||||
* @version 0.1
|
||||
* @date 2025-04-19
|
||||
* @version 2.0.0
|
||||
* @date 2025-08-17
|
||||
*
|
||||
* @copyright MIT - (c) 2025 - Adam Wonak - adam.wonak@gmail.com
|
||||
*
|
||||
|
||||
@ -2,8 +2,8 @@
|
||||
* @file libGravity.cpp
|
||||
* @author Adam Wonak (https://github.com/awonak)
|
||||
* @brief Library for building custom scripts for the Sitka Instruments Gravity module.
|
||||
* @version 0.1
|
||||
* @date 2025-04-19
|
||||
* @version 2.0.0
|
||||
* @date 2025-08-17
|
||||
*
|
||||
* @copyright MIT - (c) 2025 - Adam Wonak - adam.wonak@gmail.com
|
||||
*
|
||||
@ -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;
|
||||
|
||||
@ -2,8 +2,8 @@
|
||||
* @file libGravity.h
|
||||
* @author Adam Wonak (https://github.com/awonak)
|
||||
* @brief Library for building custom scripts for the Sitka Instruments Gravity module.
|
||||
* @version 0.1
|
||||
* @date 2025-04-19
|
||||
* @version 2.0.0
|
||||
* @date 2025-08-17
|
||||
*
|
||||
* @copyright MIT - (c) 2025 - Adam Wonak - adam.wonak@gmail.com
|
||||
*
|
||||
|
||||
@ -2,8 +2,8 @@
|
||||
* @file peripherials.h
|
||||
* @author Adam Wonak (https://github.com/awonak)
|
||||
* @brief Arduino pin definitions for the Sitka Instruments Gravity module.
|
||||
* @version 0.1
|
||||
* @date 2025-04-19
|
||||
* @version 2.0.0
|
||||
* @date 2025-08-17
|
||||
*
|
||||
* @copyright MIT - (c) 2025 - Adam Wonak - adam.wonak@gmail.com
|
||||
*
|
||||
@ -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
|
||||
|
||||
72
src/uClock/platforms/renesas.h
Normal file
72
src/uClock/platforms/renesas.h
Normal 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);
|
||||
)
|
||||
}
|
||||
@ -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
|
||||
|
||||
Reference in New Issue
Block a user