initial commit
This commit is contained in:
470
dc_trackctl.ino
Normal file
470
dc_trackctl.ino
Normal file
@@ -0,0 +1,470 @@
|
||||
/**************************************************************************
|
||||
This is an example for our Monochrome OLEDs based on SSD1306 drivers
|
||||
|
||||
Pick one up today in the adafruit shop!
|
||||
------> http://www.adafruit.com/category/63_98
|
||||
|
||||
This example is for a 128x64 pixel display using I2C to communicate
|
||||
3 pins are required to interface (two I2C and one reset).
|
||||
|
||||
Adafruit invests time and resources providing this open
|
||||
source code, please support Adafruit and open-source
|
||||
hardware by purchasing products from Adafruit!
|
||||
|
||||
Written by Limor Fried/Ladyada for Adafruit Industries,
|
||||
with contributions from the open source community.
|
||||
BSD license, check license.txt for more information
|
||||
All text above, and the splash screen below must be
|
||||
included in any redistribution.
|
||||
**************************************************************************/
|
||||
|
||||
#include <Adafruit_GFX.h>
|
||||
#include <Adafruit_SSD1306.h>
|
||||
#include <SPI.h>
|
||||
#include <Wire.h>
|
||||
#include <EEPROM.h>
|
||||
|
||||
#include "RP2040_PWM.h"
|
||||
|
||||
#define SCREEN_WIDTH 128 // OLED display width, in pixels
|
||||
#define SCREEN_HEIGHT 64 // OLED display height, in pixels
|
||||
|
||||
#define OLED_RESET -1
|
||||
#define SCREEN_ADDRESS 0x3C
|
||||
#define SCL 21
|
||||
#define SDA 20
|
||||
|
||||
#define STAT_LED_A 22
|
||||
#define STAT_LED_B 23
|
||||
|
||||
#define STAT_LED_C 17
|
||||
|
||||
#define ROT_A 8
|
||||
#define ROT_B 9
|
||||
|
||||
#define WAVE_X1 5
|
||||
#define WAVE_Y1 25
|
||||
#define WAVE_X2 123
|
||||
#define WAVE_Y2 48
|
||||
|
||||
#define IN1 26
|
||||
#define IN2 22
|
||||
#define ENA 27
|
||||
|
||||
#define MOT_ENABLE 23
|
||||
#define BTN 7
|
||||
|
||||
#define EEPROM_WRITE_OFFSET 0x0
|
||||
|
||||
bool stat_led_a = 0;
|
||||
bool stat_led_b = 0;
|
||||
bool stat_led_c = 0;
|
||||
|
||||
float pwm_freq = 20000.0;
|
||||
int8_t pwr = 0;
|
||||
uint8_t pwr_norm = 0;
|
||||
float dc_norm = 0;
|
||||
bool fwd = true;
|
||||
|
||||
struct TuningParam {
|
||||
float duty_cycle;
|
||||
float freq;
|
||||
|
||||
bool operator==(TuningParam that) {
|
||||
return this->duty_cycle == that.duty_cycle && this->freq == that.freq;
|
||||
}
|
||||
|
||||
bool operator!=(TuningParam that) {
|
||||
return !(*this == that);
|
||||
}
|
||||
};
|
||||
|
||||
enum DisplayState { RUN, MAP_WAIT, MAP_WRITE };
|
||||
DisplayState state = RUN;
|
||||
|
||||
// display
|
||||
uint32_t refresh_timer_ms = 0;
|
||||
const uint32_t refresh_timeout_ms = 33;
|
||||
|
||||
// rotary encoder polling
|
||||
const uint8_t rot_a_int = digitalPinToInterrupt(ROT_A);
|
||||
const uint8_t rot_b_int = digitalPinToInterrupt(ROT_B);
|
||||
int8_t renc_change = 1;
|
||||
uint32_t poll_timer_ms = 0;
|
||||
const uint32_t poll_timeout_ms = 2;
|
||||
bool poll_wait = false;
|
||||
bool poll_wait_start = false;
|
||||
|
||||
// tuning map
|
||||
TuningParam tuning_map[100];
|
||||
bool tuning_map_set = false;
|
||||
|
||||
// serial read timeout
|
||||
uint32_t serial_timer_ms = 0;
|
||||
const uint32_t serial_timeout_ms = 2000;
|
||||
|
||||
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
|
||||
RP2040_PWM* PWM_Instance;
|
||||
|
||||
void setup() {
|
||||
Wire.setSCL(SCL);
|
||||
Wire.setSDA(SDA);
|
||||
|
||||
pinMode(STAT_LED_A, OUTPUT);
|
||||
pinMode(STAT_LED_B, OUTPUT);
|
||||
pinMode(STAT_LED_C, OUTPUT);
|
||||
|
||||
pinMode(ROT_A, INPUT_PULLUP);
|
||||
pinMode(ROT_B, INPUT_PULLUP);
|
||||
|
||||
pinMode(MOT_ENABLE, INPUT_PULLUP);
|
||||
pinMode(BTN, INPUT_PULLUP);
|
||||
|
||||
pinMode(IN1, OUTPUT);
|
||||
pinMode(IN2, OUTPUT);
|
||||
|
||||
delay(500);
|
||||
|
||||
if (!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
|
||||
digitalWrite(STAT_LED_C, HIGH);
|
||||
}
|
||||
|
||||
display.clearDisplay();
|
||||
display.setTextColor(SSD1306_WHITE);
|
||||
display.setTextSize(2);
|
||||
display.write("initializing...");
|
||||
display.display();
|
||||
|
||||
delay(500);
|
||||
Serial.begin();
|
||||
delay(500);
|
||||
|
||||
if (!digitalRead(BTN)) {
|
||||
if (!Serial) {
|
||||
display.setTextSize(1);
|
||||
display.setCursor(5, 5);
|
||||
display.print("Waiting for serial connection...");
|
||||
display.display();
|
||||
}
|
||||
|
||||
bool pix_on = 1;
|
||||
|
||||
while (!Serial) {
|
||||
delay(100);
|
||||
stat_led_c = !(stat_led_c);
|
||||
digitalWrite(STAT_LED_C, stat_led_c);
|
||||
}
|
||||
|
||||
display.display();
|
||||
}
|
||||
|
||||
Serial.println("start");
|
||||
|
||||
arm_rotary_encoder();
|
||||
|
||||
EEPROM.begin(1000);
|
||||
|
||||
tuning_map_set = load_tuning_map();
|
||||
|
||||
PWM_Instance = new RP2040_PWM(ENA, pwm_freq, dc_norm);
|
||||
}
|
||||
|
||||
void loop() {
|
||||
uint32_t now = millis();
|
||||
|
||||
if (poll_wait_start && !poll_wait) {
|
||||
poll_timer_ms = now;
|
||||
poll_wait = true;
|
||||
poll_wait_start = false;
|
||||
}
|
||||
|
||||
uint32_t last_poll_delta = now - poll_timer_ms;
|
||||
|
||||
if (poll_wait && last_poll_delta >= poll_timeout_ms) {
|
||||
// a sum of 2 means the encoder has settled
|
||||
uint8_t pin_sum = digitalRead(ROT_A) + digitalRead(ROT_B);
|
||||
|
||||
if (pin_sum == 2) {
|
||||
uint8_t vel_mult = 1;
|
||||
|
||||
if (last_poll_delta < poll_timeout_ms * 2) vel_mult = 2;
|
||||
pwr += renc_change * vel_mult;
|
||||
|
||||
// nested because we don't need to check this until ready for next poll
|
||||
poll_wait = false;
|
||||
|
||||
arm_rotary_encoder();
|
||||
}
|
||||
}
|
||||
|
||||
if (Serial.available()) {
|
||||
char cmd = Serial.read();
|
||||
|
||||
if (state == MAP_WAIT) {
|
||||
switch (cmd) {
|
||||
case 'R': {
|
||||
clear_buffer();
|
||||
|
||||
state = RUN;
|
||||
arm_rotary_encoder();
|
||||
|
||||
Serial.println("returning");
|
||||
display.clearDisplay();
|
||||
break;
|
||||
}
|
||||
case 'C': {
|
||||
tuning_map_set = false;
|
||||
Serial.println("disabled tuning map");
|
||||
break;
|
||||
}
|
||||
case 'E': {
|
||||
tuning_map_set = true;
|
||||
Serial.println("enabled tuning map");
|
||||
break;
|
||||
}
|
||||
case 'S': {
|
||||
Serial.println("saving");
|
||||
save_tuning_map();
|
||||
Serial.println("saved");
|
||||
break;
|
||||
}
|
||||
case 'L': {
|
||||
Serial.println("loading");
|
||||
if (load_tuning_map()) {
|
||||
Serial.println("loaded");
|
||||
} else {
|
||||
Serial.println("no tuning map present");
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'D': {
|
||||
size_t idx = Serial.parseInt();
|
||||
if (idx > 99) break;
|
||||
tuning_map[idx].duty_cycle = Serial.parseFloat();
|
||||
tuning_map[idx].freq = Serial.parseFloat();
|
||||
|
||||
display.drawPixel(12 + idx, 30, SSD1306_WHITE);
|
||||
display.display();
|
||||
Serial.println("ok");
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
clear_buffer();
|
||||
Serial.println("invalid command");
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
switch (cmd) {
|
||||
case 'M': {
|
||||
state = MAP_WAIT;
|
||||
|
||||
disarm_rotary_encoder();
|
||||
|
||||
display.clearDisplay();
|
||||
display.setTextSize(1);
|
||||
display.setCursor(5, 5);
|
||||
display.print("receiving tuning map...");
|
||||
display.display();
|
||||
|
||||
Serial.println("ready");
|
||||
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
clear_buffer();
|
||||
Serial.println("invalid command");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch (state) {
|
||||
case RUN:
|
||||
stat_led_c = !(stat_led_c);
|
||||
digitalWrite(STAT_LED_C, stat_led_c);
|
||||
|
||||
if (now - refresh_timer_ms >= refresh_timeout_ms) {
|
||||
display.setTextSize(2);
|
||||
|
||||
if (!tuning_map_set) {
|
||||
PWM_Instance->setPWM(ENA, pwm_freq, 0);
|
||||
display.clearDisplay();
|
||||
display.setTextSize(1);
|
||||
display.setCursor(5, 5);
|
||||
display.print("critical: no tuning map!");
|
||||
display.display();
|
||||
return;
|
||||
}
|
||||
|
||||
fwd = pwr >= 0;
|
||||
pwr_norm = abs(pwr);
|
||||
if (pwr_norm == 0) {
|
||||
dc_norm = 0;
|
||||
pwm_freq = 0;
|
||||
} else {
|
||||
dc_norm = tuning_map[pwr_norm].duty_cycle;
|
||||
pwm_freq = tuning_map[pwr_norm].freq;
|
||||
}
|
||||
|
||||
bool pwm_on = !digitalRead(MOT_ENABLE) && pwr_norm != 0; // since it's input_pullup
|
||||
|
||||
display.clearDisplay();
|
||||
|
||||
if (digitalRead(ROT_A)) display.drawPixel(0, 0, SSD1306_WHITE);
|
||||
|
||||
if (digitalRead(ROT_B)) display.drawPixel(2, 0, SSD1306_WHITE);
|
||||
|
||||
if (digitalRead(BTN)) display.drawPixel(4, 0, SSD1306_WHITE);
|
||||
|
||||
// draw text
|
||||
display.setCursor(2, 2);
|
||||
display.print("pwr");
|
||||
|
||||
if (dc_norm > 0 && pwm_on) {
|
||||
// draw number
|
||||
display.setCursor(48, 2);
|
||||
|
||||
display.print(pwr_norm);
|
||||
display.print('%');
|
||||
|
||||
// draw fwd/rev
|
||||
display.setCursor(90, 2);
|
||||
|
||||
if (fwd)
|
||||
display.print("FWD");
|
||||
else
|
||||
display.print("REV");
|
||||
} else {
|
||||
display.setCursor(72, 2);
|
||||
display.print("OFF");
|
||||
}
|
||||
|
||||
// draw wave
|
||||
uint16_t peak_edge_x = WAVE_X1 + dc_norm;
|
||||
|
||||
display.drawLine(WAVE_X1, WAVE_Y1, peak_edge_x, WAVE_Y1, SSD1306_WHITE);
|
||||
display.drawLine(peak_edge_x, WAVE_Y1, peak_edge_x, WAVE_Y2, SSD1306_WHITE);
|
||||
display.drawLine(peak_edge_x, WAVE_Y2, WAVE_X2, WAVE_Y2, SSD1306_WHITE);
|
||||
|
||||
// draw freq
|
||||
display.setTextSize(1);
|
||||
display.setCursor(5, 50);
|
||||
display.print(pwm_freq);
|
||||
display.print("Hz");
|
||||
|
||||
// draw duty cycle
|
||||
display.setTextSize(1);
|
||||
display.setCursor(75, 50);
|
||||
display.print(dc_norm);
|
||||
display.print("%");
|
||||
|
||||
display.display();
|
||||
|
||||
// drive motor
|
||||
if (pwm_on) {
|
||||
if (fwd) {
|
||||
digitalWrite(IN1, HIGH);
|
||||
digitalWrite(IN2, LOW);
|
||||
} else {
|
||||
digitalWrite(IN1, LOW);
|
||||
digitalWrite(IN2, HIGH);
|
||||
}
|
||||
PWM_Instance->setPWM(ENA, pwm_freq, dc_norm);
|
||||
} else {
|
||||
digitalWrite(IN1, LOW);
|
||||
digitalWrite(IN2, LOW);
|
||||
PWM_Instance->setPWM(ENA, pwm_freq, 0);
|
||||
}
|
||||
|
||||
refresh_timer_ms = now;
|
||||
}
|
||||
|
||||
break;
|
||||
case MAP_WAIT:
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
// cw interrupt
|
||||
void cw_b_isr() {
|
||||
disarm_rotary_encoder();
|
||||
attachInterrupt(rot_a_int, cw_a_isr, FALLING);
|
||||
}
|
||||
|
||||
void cw_a_isr() {
|
||||
detachInterrupt(rot_a_int);
|
||||
if (pwr < 100) renc_change = 1;
|
||||
poll_wait_start = true;
|
||||
}
|
||||
|
||||
// ccw interrupt
|
||||
void ccw_a_isr() {
|
||||
disarm_rotary_encoder();
|
||||
attachInterrupt(rot_b_int, ccw_b_isr, FALLING);
|
||||
}
|
||||
|
||||
void ccw_b_isr() {
|
||||
detachInterrupt(rot_b_int);
|
||||
if (pwr > -100) renc_change = -1;
|
||||
poll_wait_start = true;
|
||||
}
|
||||
|
||||
void clear_buffer() {
|
||||
while (Serial.available()) {
|
||||
Serial.read();
|
||||
}
|
||||
}
|
||||
|
||||
void save_tuning_map() {
|
||||
bool tuning_map_present;
|
||||
EEPROM.get(EEPROM_WRITE_OFFSET, tuning_map_present);
|
||||
|
||||
if (!tuning_map_present) {
|
||||
EEPROM.put(EEPROM_WRITE_OFFSET, true);
|
||||
}
|
||||
|
||||
size_t offset = EEPROM_WRITE_OFFSET + sizeof(bool);
|
||||
for (size_t i = 0; i < 100; i++) {
|
||||
TuningParam param = tuning_map[i];
|
||||
TuningParam existing_param;
|
||||
EEPROM.get(offset, existing_param);
|
||||
|
||||
if (existing_param != param) {
|
||||
EEPROM.put(offset, tuning_map[i]);
|
||||
}
|
||||
|
||||
offset += sizeof(TuningParam);
|
||||
}
|
||||
|
||||
EEPROM.commit();
|
||||
}
|
||||
|
||||
int load_tuning_map() {
|
||||
bool tuning_map_present;
|
||||
EEPROM.get(EEPROM_WRITE_OFFSET, tuning_map_present);
|
||||
|
||||
if (tuning_map_present) {
|
||||
size_t offset = EEPROM_WRITE_OFFSET + sizeof(bool);
|
||||
for (size_t i = 0; i < 100; i++) {
|
||||
TuningParam param;
|
||||
EEPROM.get(offset, param);
|
||||
tuning_map[i] = param;
|
||||
offset += sizeof(TuningParam);
|
||||
}
|
||||
return 1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
void arm_rotary_encoder() {
|
||||
attachInterrupt(rot_b_int, cw_b_isr, FALLING);
|
||||
attachInterrupt(rot_a_int, ccw_a_isr, FALLING);
|
||||
}
|
||||
|
||||
void disarm_rotary_encoder() {
|
||||
detachInterrupt(rot_a_int);
|
||||
detachInterrupt(rot_b_int);
|
||||
}
|
||||
Reference in New Issue
Block a user