commit 1ecf83fcc7dbcdf65ab0d37ef4a264296ec563ea Author: Marcel Peterkau Date: Fri Dec 13 13:49:38 2024 +0100 first draft diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..89cc49c --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.pio +.vscode/.browse.c_cpp.db* +.vscode/c_cpp_properties.json +.vscode/launch.json +.vscode/ipch diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..080e70d --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,10 @@ +{ + // See http://go.microsoft.com/fwlink/?LinkId=827846 + // for the documentation about the extensions.json format + "recommendations": [ + "platformio.platformio-ide" + ], + "unwantedRecommendations": [ + "ms-vscode.cpptools-extension-pack" + ] +} diff --git a/fuses.py b/fuses.py new file mode 100644 index 0000000..b3faa30 --- /dev/null +++ b/fuses.py @@ -0,0 +1,20 @@ +from os.path import join +Import("env") + +conf_path = join(env.PioPlatform().get_package_dir("tool-avrdude"), "avrdude.conf") + +env.Replace( + FUSEUPLOADER="avrdude", + FUSEFLAGS=[ + "-C", conf_path, + "-p", "atmega328p", + "-Pusb", + "-c", "jtag3isp" + ], + FUSECOMMAND="$FUSEUPLOADER $FUSEFLAGS -U lfuse:w:0xE6:m -U hfuse:w:0xDA:m -U efuse:w:0x05:m" +) + +def set_fuses(source, target, env): + env.Execute(env.VerboseAction("$FUSECOMMAND", "Setting Fuses...")) + +env.AddPreAction("upload", set_fuses) diff --git a/include/README b/include/README new file mode 100644 index 0000000..194dcd4 --- /dev/null +++ b/include/README @@ -0,0 +1,39 @@ + +This directory is intended for project header files. + +A header file is a file containing C declarations and macro definitions +to be shared between several project source files. You request the use of a +header file in your project source file (C, C++, etc) located in `src` folder +by including it, with the C preprocessing directive `#include'. + +```src/main.c + +#include "header.h" + +int main (void) +{ + ... +} +``` + +Including a header file produces the same results as copying the header file +into each source file that needs it. Such copying would be time-consuming +and error-prone. With a header file, the related declarations appear +in only one place. If they need to be changed, they can be changed in one +place, and programs that include the header file will automatically use the +new version when next recompiled. The header file eliminates the labor of +finding and changing all the copies as well as the risk that a failure to +find one copy will result in inconsistencies within a program. + +In C, the usual convention is to give header files names that end with `.h'. +It is most portable to use only letters, digits, dashes, and underscores in +header file names, and at most one dot. + +Read more about using header files in official GCC documentation: + +* Include Syntax +* Include Operation +* Once-Only Headers +* Computed Includes + +https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html diff --git a/lib/README b/lib/README new file mode 100644 index 0000000..2593a33 --- /dev/null +++ b/lib/README @@ -0,0 +1,46 @@ + +This directory is intended for project specific (private) libraries. +PlatformIO will compile them to static libraries and link into executable file. + +The source code of each library should be placed in an own separate directory +("lib/your_library_name/[here are source files]"). + +For example, see a structure of the following two libraries `Foo` and `Bar`: + +|--lib +| | +| |--Bar +| | |--docs +| | |--examples +| | |--src +| | |- Bar.c +| | |- Bar.h +| | |- library.json (optional, custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html +| | +| |--Foo +| | |- Foo.c +| | |- Foo.h +| | +| |- README --> THIS FILE +| +|- platformio.ini +|--src + |- main.c + +and a contents of `src/main.c`: +``` +#include +#include + +int main (void) +{ + ... +} + +``` + +PlatformIO Library Dependency Finder will find automatically dependent +libraries scanning project source files. + +More information about PlatformIO Library Dependency Finder +- https://docs.platformio.org/page/librarymanager/ldf.html diff --git a/platformio.ini b/platformio.ini new file mode 100644 index 0000000..65275dd --- /dev/null +++ b/platformio.ini @@ -0,0 +1,30 @@ +; PlatformIO Project Configuration File +; +; Build options: build flags, source filter +; Upload options: custom upload port, speed and extra flags +; Library options: dependencies, extra library storages +; Advanced options: extra scripting +; +; Please visit documentation for the other options and examples +; https://docs.platformio.org/page/projectconf.html + +[env:nanoatmega328new] +platform = atmelavr +board = nanoatmega328new +framework = arduino + +;upload_protocol = custom +;upload_flags = +; -C +; ${platformio.packages_dir}/tool-avrdude/avrdude.conf +; -p +; atmega328p +; -Pusb +; -c +; jtag3isp +;upload_command = avrdude $UPLOAD_FLAGS -U flash:w:$SOURCE:i + +;extra_scripts = fuses.py + +lib_deps = + adafruit/Adafruit SSD1306 @ ^2.5.13 \ No newline at end of file diff --git a/src/MotorController.cpp b/src/MotorController.cpp new file mode 100644 index 0000000..d92e67b --- /dev/null +++ b/src/MotorController.cpp @@ -0,0 +1,59 @@ +// MotorController.cpp +#include "MotorController.h" + +const MotorConfig defaultConfig = { + .minPWM = 0, + .maxPWM = 255, + .inverted = false, + .responseSpeed = 10, + .lookupTable = {0, 28, 57, 85, 114, 142, 171, 199, 228, 255} +}; + +MotorController::MotorController(int motorPin) + : motorPWMPin(motorPin), currentSpeed(0) { + loadDefaults(); + pinMode(motorPWMPin, OUTPUT); + stopMotor(); +} + +void MotorController::loadDefaults() { + config = defaultConfig; +} + +void MotorController::setTargetSpeed(int targetSpeedPercent) { + targetSpeedPercent = constrain(targetSpeedPercent, 0, 100); + int tableIndex = map(targetSpeedPercent, 0, 100, 0, MotorConfig::lookupTableSize - 1); + uint8_t targetPWM = config.lookupTable[tableIndex]; + uint8_t adjustedPWM = applyDynamicResponse(targetPWM); + setPWM(adjustedPWM); + currentSpeed = targetSpeedPercent; +} + +int MotorController::getSpeed() const { + return currentSpeed; +} + +void MotorController::stopMotor() { + setTargetSpeed(0); +} + +MotorConfig MotorController::getConfig() const { + return config; +} + +void MotorController::setConfig(const MotorConfig &newConfig) { + config = newConfig; +} + +void MotorController::setPWM(uint8_t pwmValue) { + uint8_t constrainedPWM = constrain(pwmValue, config.minPWM, config.maxPWM); + analogWrite(motorPWMPin, config.inverted ? 255 - constrainedPWM : constrainedPWM); +} + +int MotorController::applyDynamicResponse(int targetValue) { + static int smoothedValue = 0; + int delta = targetValue - smoothedValue; + int step = delta / config.responseSpeed; + smoothedValue += step; + return smoothedValue; +} diff --git a/src/MotorController.h b/src/MotorController.h new file mode 100644 index 0000000..085bdc8 --- /dev/null +++ b/src/MotorController.h @@ -0,0 +1,39 @@ +// MotorController.h +#ifndef MOTORCONTROLLER_H +#define MOTORCONTROLLER_H + +#include + +// Motor configuration structure +struct MotorConfig { + uint8_t minPWM; + uint8_t maxPWM; + bool inverted; + int responseSpeed; + static const int lookupTableSize = 10; + uint8_t lookupTable[lookupTableSize]; +}; + +class MotorController { +private: + int motorPWMPin; + int currentSpeed; + MotorConfig config; + + int applyDynamicResponse(int targetValue); + void setPWM(uint8_t pwmValue); + +public: + MotorController(int motorPin); + + void stopMotor(); + void setTargetSpeed(int targetSpeedPercent); + int getSpeed() const; + + MotorConfig getConfig() const; + void setConfig(const MotorConfig &newConfig); + void loadDefaults(); + +}; + +#endif // MOTORCONTROLLER_H diff --git a/src/PedalController.cpp b/src/PedalController.cpp new file mode 100644 index 0000000..498e7ad --- /dev/null +++ b/src/PedalController.cpp @@ -0,0 +1,156 @@ +// PedalController.cpp +#include "PedalController.h" + +const int minMin = 50; // Minimum allowed raw value for calibration +const int maxMin = 200; // Maximum allowed raw value for calibration +const int minMax = 800; // Minimum allowed maximum value for calibration +const int maxMax = 1000; // Maximum allowed raw value for calibration + +const PedalConfig defaultConfig = { + .minRaw = 0, + .maxRaw = 1023, + .calibrated = false + }; + +PedalController::PedalController(int pin) + : pedalPin(pin) +{ + loadDefaults(); + pinMode(pedalPin, INPUT); +} + +void PedalController::loadDefaults() +{ + config = defaultConfig; +} + +int PedalController::applySmoothing(int rawValue) +{ + filteredValue = (filteredValue * (config.filterStrength - 1) + rawValue) / config.filterStrength; + return filteredValue; +} + +CalibrationState PedalController::autoCalibrate(bool reset) +{ + static int step = 0; + static unsigned long lastChangeTime = 0; + static int previousValue = 0; + static CalibrationState errorState = IDLE; + + if (reset) + { + step = 0; + config.minRaw = 1023; + config.maxRaw = 0; + lastChangeTime = millis(); + config.calibrated = false; + errorState = IDLE; + Serial.println("Calibration reset."); + return IDLE; + } + + unsigned long currentTime = millis(); + int rawValue = applySmoothing(analogRead(pedalPin)); + + switch (step) + { + case 0: // Initial state: wait for pedal release + if (rawValue == previousValue && currentTime - lastChangeTime > 2000) + { + if (rawValue >= minMin && rawValue <= maxMin) + { + config.minRaw = rawValue; + step++; + lastChangeTime = currentTime; + errorState = RUNNING; + } + else + { + errorState = ERROR; + } + } + else if (rawValue != previousValue) + { + lastChangeTime = currentTime; + } + break; + + case 1: // Wait for pedal full press + if (rawValue == previousValue && currentTime - lastChangeTime > 2000) + { + if (rawValue >= minMax && rawValue <= maxMax) + { + config.maxRaw = rawValue; + step++; + lastChangeTime = currentTime; + errorState = RUNNING; + } + else + { + errorState = ERROR; + } + } + else if (rawValue != previousValue) + { + lastChangeTime = currentTime; + } + break; + + case 2: // Validate calibration + if (config.maxRaw > config.minRaw + 100) + { // Ensure valid range + step++; + config.calibrated = true; + errorState = SUCCESS; + } + else + { + errorState = ERROR; + } + break; + + default: + errorState = IDLE; + step = 0; + break; + } + + previousValue = rawValue; + return errorState; +} + +int PedalController::getPedal() +{ + if (!config.calibrated || autoCalibrate(false) != IDLE) + { + return 0; + } + + int rawValue = applySmoothing(analogRead(pedalPin)); + if (rawValue < config.minRaw || rawValue > config.maxRaw) + { + return 0; // Out of bounds + } + + return map(rawValue, config.minRaw, config.maxRaw, 0, 100); +} + +CalibrationState PedalController::getStatus() +{ + return autoCalibrate(false); +} + +int PedalController::getRawValue() +{ + return analogRead(pedalPin); +} + +PedalConfig PedalController::getConfig() +{ + return config; +} + +void PedalController::setConfig(const PedalConfig &newConfig) +{ + config = newConfig; +} diff --git a/src/PedalController.h b/src/PedalController.h new file mode 100644 index 0000000..c6bd7e7 --- /dev/null +++ b/src/PedalController.h @@ -0,0 +1,46 @@ +// PedalController.h +#ifndef PEDALCONTROLLER_H +#define PEDALCONTROLLER_H + +#include + +// Calibration state enumeration +enum CalibrationState { + IDLE, + RUNNING, + SUCCESS, + ERROR +}; + +// Pedal configuration structure +struct PedalConfig { + int minRaw; + int maxRaw; + bool calibrated; + int filterStrength; +}; + +class PedalController { +private: + int pedalPin; + int filteredValue; + PedalConfig config; + + int applySmoothing(int rawValue); + int getRawValue(); + +public: + PedalController(int pin); + + CalibrationState autoCalibrate(bool reset); + CalibrationState getStatus(); + + int getPedal(); + + PedalConfig getConfig(); + void setConfig(const PedalConfig &newConfig); + void loadDefaults(); + +}; + +#endif // PEDALCONTROLLER_H diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..577cec9 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,254 @@ +#include +#include +#include +#include +#include "MotorController.h" +#include "PedalController.h" + +// OLED display setup +#define OLED_RESET -1 +#define SCREEN_WIDTH 128 +#define SCREEN_HEIGHT 64 +Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET); + +// Pin definitions +const int hallSensor = 2; // Hall sensor input +const int encoderPinA = 3; // Encoder pin A +const int encoderPinB = 4; // Encoder pin B +const int encoderButton = 5; // Encoder button +const int motorPin = 9; // Motor PWM pin +const int pedalPin = A0; // Pedal input pin + +// Global objects +MotorController motor(motorPin); // Motor control object +PedalController pedal(pedalPin); // Pedal input object + +// Combined settings structure +struct Settings { + MotorConfig motorConfig; + PedalConfig pedalConfig; + uint8_t checksum; +}; + +Settings settings; +bool autoCalibrating = false; +bool serialControlEnabled = false; +CalibrationState lastCalibrationState = IDLE; + +// Function prototypes +void loadSettings(); +void saveSettings(); +uint8_t calculateChecksum(const Settings &s); +void handleEncoder(); +void handleEncoderButton(); +void updateDisplay(); +void handleSerialInput(); +void configurePWMFrequency(); +void stopMotorAtNeedleUp(); +void printHelp(); + +void setup() { + // Initialize pins + pinMode(hallSensor, INPUT); + pinMode(encoderPinA, INPUT_PULLUP); + pinMode(encoderPinB, INPUT_PULLUP); + pinMode(encoderButton, INPUT_PULLUP); + + // Configure PWM frequency + configurePWMFrequency(); + + // Attach interrupts for encoder + attachInterrupt(digitalPinToInterrupt(encoderPinA), handleEncoder, CHANGE); + attachInterrupt(digitalPinToInterrupt(encoderPinB), handleEncoder, CHANGE); + attachInterrupt(digitalPinToInterrupt(encoderButton), handleEncoderButton, FALLING); + + // Initialize serial and load settings + Serial.begin(9600); + loadSettings(); + + // Initialize OLED display + // if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { + // Serial.println(F("SSD1306 allocation failed")); + // } + // display.clearDisplay(); + // display.display(); + + printHelp(); +} + +void loop() { + // Handle serial input for debugging and manual control + handleSerialInput(); + + if (autoCalibrating) { + CalibrationState currentState = pedal.autoCalibrate(false); + if (currentState != lastCalibrationState) { + lastCalibrationState = currentState; + switch (currentState) { + case RUNNING: + Serial.println("Calibration running..."); + break; + case SUCCESS: + Serial.println("Auto-calibration completed."); + break; + case ERROR: + Serial.println("Auto-calibration failed."); + break; + default: + break; + } + } + motor.setTargetSpeed(0); // Ensure motor stays off during calibration + } else { + int pedalValue = serialControlEnabled ? 0 : pedal.getPedal(); + motor.setTargetSpeed(pedalValue); + } + + // Update display + // updateDisplay(); + + delay(10); // Short delay for stability +} + +void stopMotorAtNeedleUp() { + motor.stopMotor(); + delay(100); // Stabilize +} + +void loadSettings() { + EEPROM.get(0, settings); + + // Validate checksum + if (calculateChecksum(settings) != settings.checksum) { + Serial.println("Invalid settings checksum, loading defaults."); + motor.loadDefaults(); + pedal.autoCalibrate(true); + + settings.motorConfig = motor.getConfig(); + settings.pedalConfig = pedal.getConfig(); + settings.checksum = calculateChecksum(settings); + + saveSettings(); + } else { + motor.setConfig(settings.motorConfig); + pedal.setConfig(settings.pedalConfig); + Serial.println("Settings loaded from EEPROM."); + } +} + +void saveSettings() { + settings.motorConfig = motor.getConfig(); + settings.pedalConfig = pedal.getConfig(); + settings.checksum = calculateChecksum(settings); + + EEPROM.put(0, settings); + Serial.println("Settings saved to EEPROM."); +} + +uint8_t calculateChecksum(const Settings &s) { + uint8_t sum = 0; + const uint8_t *data = reinterpret_cast(&s); + for (size_t i = 0; i < sizeof(Settings) - 1; i++) { + sum ^= data[i]; + } + return sum; +} + +void handleEncoder() { + static uint8_t lastState = 0; + uint8_t currentState = (digitalRead(encoderPinA) << 1) | digitalRead(encoderPinB); + if ((lastState == 0b00 && currentState == 0b01) || + (lastState == 0b01 && currentState == 0b11) || + (lastState == 0b11 && currentState == 0b10) || + (lastState == 0b10 && currentState == 0b00)) { + // Increment + } else if ((lastState == 0b00 && currentState == 0b10) || + (lastState == 0b10 && currentState == 0b11) || + (lastState == 0b11 && currentState == 0b01) || + (lastState == 0b01 && currentState == 0b00)) { + // Decrement + } + lastState = currentState; +} + +void handleEncoderButton() { + // Handle encoder button press +} + +void updateDisplay() { + display.clearDisplay(); + display.setTextSize(1); + display.setTextColor(SSD1306_WHITE); + + display.setCursor(0, 0); + display.print("Pedal: "); + display.println(pedal.getPedal()); + + display.display(); +} + +void handleSerialInput() { + if (Serial.available()) { + char input = Serial.read(); + switch (input) { + case 'c': + Serial.println("Starting auto-calibration..."); + pedal.autoCalibrate(true); + autoCalibrating = true; + lastCalibrationState = IDLE; // Reset state tracking + break; + case 'x': + Serial.println("Stopping auto-calibration or serial control."); + autoCalibrating = false; + serialControlEnabled = false; + motor.stopMotor(); + break; + case 'r': + Serial.println("Loading defaults."); + motor.loadDefaults(); + pedal.loadDefaults(); + break; + case 's': + Serial.println("Saving settings to EEPROM."); + saveSettings(); + break; + case 'l': + Serial.println("Loading settings from EEPROM."); + loadSettings(); + break; + case 'h': + printHelp(); + break; + case '+': + serialControlEnabled = true; + motor.setTargetSpeed(motor.getSpeed() + 1); + Serial.print("Increased Speed to: "); + Serial.println(motor.getSpeed()); + break; + case '-': + serialControlEnabled = true; + motor.setTargetSpeed(motor.getSpeed() - 1); + Serial.print("Decreased Speed to: "); + Serial.println(motor.getSpeed()); + break; + } + Serial.println(input); + } +} + +void printHelp() { + Serial.println("Available commands:"); + Serial.println("c - Start auto-calibration"); + Serial.println("x - Stop auto-calibration or serial control"); + Serial.println("r - Load defaults"); + Serial.println("s - Save settings to EEPROM"); + Serial.println("l - Load settings from EEPROM"); + Serial.println("h - Show this help"); + Serial.println("+ - Increment PWM"); + Serial.println("- - Decrement PWM"); +} + +void configurePWMFrequency() { + // Configure Timer1 for higher PWM frequency (e.g., ~8 kHz) + TCCR1B = (TCCR1B & 0b11111000) | 0x01; // Set prescaler to 1 +} diff --git a/test/README b/test/README new file mode 100644 index 0000000..9b1e87b --- /dev/null +++ b/test/README @@ -0,0 +1,11 @@ + +This directory is intended for PlatformIO Test Runner and project tests. + +Unit Testing is a software testing method by which individual units of +source code, sets of one or more MCU program modules together with associated +control data, usage procedures, and operating procedures, are tested to +determine whether they are fit for use. Unit testing finds problems early +in the development cycle. + +More information about PlatformIO Unit Testing: +- https://docs.platformio.org/en/latest/advanced/unit-testing/index.html