From 21e570a81f6cc6ebea145b6b92343d60c829d8fa Mon Sep 17 00:00:00 2001 From: rileybarshak Date: Fri, 10 Apr 2026 15:27:44 -0400 Subject: [PATCH 1/2] RPM & Speed via hall effect sensor --- README.md | 7 ++-- firmware/control.ino | 79 +++++++++++++++++++++++++++++++++++++++---- firmware/display.ino | 80 +++++++++++++++++++++++++++----------------- 3 files changed, 126 insertions(+), 40 deletions(-) diff --git a/README.md b/README.md index 0d8ddbd..2e60d8b 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,6 @@ The next iteration of Kilobyte introduces many structural and user interface imp - [x] Create new Arduino shield for power and all IO - [ ] Upgrade & enhance firmware - [ ] Show battery voltage and percentage on screen - - [ ] Implement master software shutoff switch - [x] Make UI more user friendly - [x] Add safeguards for when communication to control unit is lost - [x] Implement UI LEDs @@ -53,8 +52,8 @@ The next iteration of Kilobyte introduces many structural and user interface imp - [x] Finalize mounting of all electronics and batteries - [x] Test mounting for strength and neatness - [x] Ensure all electronics are properly fused - - [ ] Neatly route power+communication wiring to display unit - - [ ] Design custom cable harness/mounting atachments + - [x] Neatly route power+communication wiring to display unit + - [x] Design custom cable harness/mounting atachments - [x] Cut a wooden board to mount electronics on - [x] Design battery mounting solution - Cosmetics @@ -70,7 +69,7 @@ The next iteration of Kilobyte introduces many structural and user interface imp - Safety - [ ] Detect human presence on chariot, do not allow operation if not detector - [ ] Add configurable speed limiter - - [ ] Add a hardware battery disconnect + - [x] Add a hardware battery disconnect - [ ] Grip tape / bed liner for chariot - Cosmetics - [ ] clean and clear coat aluminum diff --git a/firmware/control.ino b/firmware/control.ino index eb37afd..decf8be 100644 --- a/firmware/control.ino +++ b/firmware/control.ino @@ -3,6 +3,7 @@ // -------------------- Pin Definitions -------------------- #define PWM 9 #define PWM2 10 +#define HALL_SENSOR A0 // -------------------- I2C Variables -------------------- byte receivedThrottle = 180; @@ -19,6 +20,20 @@ const float accelStep = 4.0; // how much each button press adds const float decayStep = 1.5; // how fast it returns to neutral const float minThrottle = 180; +// -------------------- Hall Effect Sensor -------------------- +const int hallEffectLow = 500; // baseline should stay below this +const int hallEffectHigh = 650; // magnet pass should be above this +const float wheelDiameter = 24.13; // wheel diameter in centimeters (9.5inch = 24.13cm) +const uint8_t magnetsPerRevolution = 1; +const unsigned long rpmTimeout = 2000; // threshold in ms to consider rpm 0 + +volatile uint16_t rpmToSend = 0; + +bool pulseArmed = true; +unsigned long lastPulseMicros = 0; +unsigned long lastPulseMillis = 0; +float currentRpm = 0.0; + // -------------------- I2C Receive ---------------------- void receiveEvent(int numBytes) { if (numBytes >= 3) { @@ -30,6 +45,12 @@ void receiveEvent(int numBytes) { } } +// -------------------- I2C Request -------------------- +void requestEvent() { + uint16_t rpm = rpmToSend; + Wire.write((uint8_t)(rpm & 0xFF)); + Wire.write((uint8_t)(rpm >> 8)); +} // -------------------- Accelerate With Button Press -------------------- void updateThrottle() { @@ -56,14 +77,49 @@ void updateThrottle() { } } +// -------------------- Hall Sensor RPM -------------------- +void updateRpm() { + int hallEffectRaw = analogRead(HALL_SENSOR); + unsigned long nowMicros = micros(); + unsigned long nowMillis = millis(); + + if (pulseArmed && hallEffectRaw >= hallEffectHigh) { + if (lastPulseMicros != 0) { + unsigned long periodMicros = nowMicros - lastPulseMicros; + if (periodMicros > 0) { + currentRpm = 60000000.0 /* 1 minute in microseconds */ / (periodMicros * magnetsPerRevolution); + } + } + + lastPulseMicros = nowMicros; + lastPulseMillis = nowMillis; + pulseArmed = false; + } else if (!pulseArmed && hallEffectRaw <= hallEffectLow) { + pulseArmed = true; + } + + if (lastPulseMillis == 0 || (nowMillis - lastPulseMillis) > rpmTimeout) { + currentRpm = 0.0; + } + + if (currentRpm < 0) currentRpm = 0; // prevent negative RPM + if (currentRpm > 65535) currentRpm = 65535; // prevent overflow + + noInterrupts(); // ensure variable doesn't change in the middle of setting it + rpmToSend = (uint16_t)(currentRpm + 0.5); // round to nearest integer + interrupts(); +} + // -------------------- Setup -------------------- void setup() { Serial.begin(9600); Wire.begin(8); Wire.onReceive(receiveEvent); + Wire.onRequest(requestEvent); pinMode(PWM, OUTPUT); pinMode(PWM2, OUTPUT); + pinMode(HALL_SENSOR, INPUT); } // -------------------- Main Loop -------------------- @@ -81,18 +137,29 @@ void loop() { // Update throttle based on buttons updateThrottle(); + updateRpm(); // Apply throttle to both motors unless one button disables it analogWrite(PWM, (receivedRight == 0 ? receivedThrottle : 180)); analogWrite(PWM2, (receivedLeft == 0 ? receivedThrottle : 180)); + // Speed (km/h) derived from wheel diameter and current RPM, kept here for tuning. + float wheelCircumference = (wheelDiameter / 100.0) * 3.14159265; // in meters + float speed = (currentRpm * wheelCircumference * 60.0) / 1000.0; // in km/h + +// Debugging output + +// Serial.print("Left: "); +// Serial.print(receivedThrottle); +// Serial.print(receivedLeft); +// Serial.print(" Right: "); +// Serial.println(receivedThrottle); +// Serial.print(receivedRight); - Serial.print("Left: "); - Serial.print(receivedThrottle); - Serial.print(receivedLeft); - Serial.print(" Right: "); - Serial.println(receivedThrottle); - Serial.print(receivedRight); +// Serial.print("RPM: "); +// Serial.print((uint16_t)currentRpm); +// Serial.print(" Speed(km/h): "); +// Serial.println(speed, 2); delay(10); } diff --git a/firmware/display.ino b/firmware/display.ino index c453ef9..a3fe3e2 100644 --- a/firmware/display.ino +++ b/firmware/display.ino @@ -11,12 +11,18 @@ #define LEFT 6 #define RIGHT 7 +const uint8_t CONTROL_I2C_ADDRESS = 0x08; + // -------------------- LCD -------------------- LiquidCrystal_I2C lcd(0x27, 20, 4); unsigned long lastLcdUpdate = 0; const unsigned long lcdUpdateInterval = 100; +unsigned long lastRpmPoll = 0; +const unsigned long rpmPollInterval = 100; +uint16_t currentRpm = 0; + // -------------------- Reverse Beeper -------------------- unsigned long lastBeepTime = 0; const unsigned long beepInterval = 1600; @@ -28,6 +34,8 @@ const unsigned long beepDuration = 800; const float smoothFactor = 0.10; float smoothedThrottle = 0; +const float wheelDiameter = 24.13; // wheel diameter in centimeters (9.5inch = 24.13cm) + float smooth(float previous, float current) { return previous + smoothFactor * (current - previous); } @@ -60,6 +68,15 @@ void drawThrottleBar(int throttlePercent, int totalBlocks) { lcd.print("]"); } +void requestRpm() { + Wire.requestFrom((int)CONTROL_I2C_ADDRESS, 2); + if (Wire.available() >= 2) { + uint8_t low = Wire.read(); + uint8_t high = Wire.read(); + currentRpm = (uint16_t)(low | ((uint16_t)high << 8)); + } +} + void update_lcd(bool reverseActive) { int throttlePercent = map(smoothedThrottle, 1023, 0, 0, 100); @@ -70,15 +87,21 @@ void update_lcd(bool reverseActive) { lcd.print("FWD "); drawThrottleBar(throttlePercent, 10); // high-res bar lcd.print(" "); // padding + lcd.print(throttlePercent); + lcd.print("%"); } - lcd.print(throttlePercent); - lcd.print("%"); + float wheelCircumference = (wheelDiameter / 100.0) * 3.14159265; + float speedKmh = (currentRpm * wheelCircumference * 60.0) / 1000.0; // placeholder, will be speedometer lcd.setCursor(0, 3); - drawThrottleBar(throttlePercent, 18); - + lcd.print("RPM:"); + lcd.print(currentRpm); + lcd.print(" SPD:"); + lcd.print(speedKmh, 1); + lcd.print("km/h "); +} // -------------------- Reverse Beep -------------------- void handleReverseBeep(bool reverseActive) { @@ -100,6 +123,7 @@ void handleReverseBeep(bool reverseActive) { } } +// -------------------- Startup Song -------------------- void play_startup_song() { // Imperial March melody int melody[] = { @@ -138,13 +162,21 @@ void play_startup_song() { delay(70); } +// -------------------- I2C Packet -------------------- +bool sendPacket(byte throttle, byte leftBtn, byte rightBtn) { + Wire.beginTransmission(CONTROL_I2C_ADDRESS); + Wire.write(throttle); + Wire.write(leftBtn); + Wire.write(rightBtn); + byte status = Wire.endTransmission(); + return (status == 0); +} // -------------------- Setup -------------------- void setup() { Wire.begin(); - lcd.init(); - lcd.createChar(0, block0); + lcd.createChar(1, block1); lcd.createChar(2, block2); lcd.createChar(3, block3); @@ -158,8 +190,6 @@ void setup() { pinMode(LED, OUTPUT); pinMode(LED2, OUTPUT); - pinMode(PWM, OUTPUT); - pinMode(PWM2, OUTPUT); pinMode(BUZZER, OUTPUT); digitalWrite(LED, HIGH); @@ -174,43 +204,33 @@ void setup() { Serial.begin(9600); } - // -------------------- I2C Packet -------------------- - bool sendPacket(byte throttle, byte leftBtn, byte rightBtn) { - Wire.beginTransmission(8); - Wire.write(throttle); - Wire.write(leftBtn); - Wire.write(rightBtn); - - byte status = Wire.endTransmission(); - return (status == 0); - } - // -------------------- Main Loop -------------------- void loop() { unsigned long now = millis(); smoothedThrottle = smooth(smoothedThrottle, analogRead(THROTTLE)); bool reverseActive = !digitalRead(REVERSE); - int leftPressed = digitalRead(LEFT); - int rightPressed = digitalRead(RIGHT); + byte leftPressed = digitalRead(LEFT); + byte rightPressed = digitalRead(RIGHT); // Throttle mapping // 180 = zero throttle // 255 = full - // 0 = full rever - int driveThrottle = map(smoothedThrottle, 1023, 0, 180, 255); + // 0 = full reverse + int driveThrottle = map((int)smoothedThrottle, 1023, 0, 180, 255); if (reverseActive) { driveThrottle = 255 - driveThrottle; } - // Send packet to control unit - while (!sendPacket(driveThrottle, leftPressed, rightPressed)) { - left = 0; - right = 0; // disable input - lcd.clear(); - lcd.print("COMMUNICATION LOST"); - delay(1000); + if (!sendPacket((byte)driveThrottle, leftPressed, rightPressed)) { + lcd.setCursor(0, 1); + lcd.print("COMMUNICATION LOST "); + } else { + if (now - lastRpmPoll >= rpmPollInterval) { + requestRpm(); + lastRpmPoll = now; + } } handleReverseBeep(reverseActive); From e5bca8657db710979022c630e97e6c0ecec169ee Mon Sep 17 00:00:00 2001 From: rileybarshak Date: Fri, 10 Apr 2026 15:40:07 -0400 Subject: [PATCH 2/2] throttle ramp fix --- firmware/control.ino | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/firmware/control.ino b/firmware/control.ino index decf8be..3340425 100644 --- a/firmware/control.ino +++ b/firmware/control.ino @@ -139,9 +139,11 @@ void loop() { updateThrottle(); updateRpm(); - // Apply throttle to both motors unless one button disables it - analogWrite(PWM, (receivedRight == 0 ? receivedThrottle : 180)); - analogWrite(PWM2, (receivedLeft == 0 ? receivedThrottle : 180)); + uint8_t pwmRight = (uint8_t)constrain((int)(rightThrottle + 0.5f), 0, 255); + uint8_t pwmLeft = (uint8_t)constrain((int)(leftThrottle + 0.5f), 0, 255); + + analogWrite(PWM, pwmRight); + analogWrite(PWM2, pwmLeft); // Speed (km/h) derived from wheel diameter and current RPM, kept here for tuning. float wheelCircumference = (wheelDiameter / 100.0) * 3.14159265; // in meters