Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
87 changes: 78 additions & 9 deletions firmware/control.ino
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// -------------------- Pin Definitions --------------------
#define PWM 9
#define PWM2 10
#define HALL_SENSOR A0

// -------------------- I2C Variables --------------------
byte receivedThrottle = 180;
Expand All @@ -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) {
Expand All @@ -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() {
Expand All @@ -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 --------------------
Expand All @@ -81,18 +137,31 @@ void loop() {

// Update throttle based on buttons
updateThrottle();
updateRpm();

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
float speed = (currentRpm * wheelCircumference * 60.0) / 1000.0; // in km/h

// Apply throttle to both motors unless one button disables it
analogWrite(PWM, (receivedRight == 0 ? receivedThrottle : 180));
analogWrite(PWM2, (receivedLeft == 0 ? receivedThrottle : 180));
// 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);
}
Expand Down
80 changes: 50 additions & 30 deletions firmware/display.ino
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
}
Expand Down Expand Up @@ -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);

Expand All @@ -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) {
Expand All @@ -100,6 +123,7 @@ void handleReverseBeep(bool reverseActive) {
}
}

// -------------------- Startup Song --------------------
void play_startup_song() {
// Imperial March melody
int melody[] = {
Expand Down Expand Up @@ -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);
Expand All @@ -158,8 +190,6 @@ void setup() {

pinMode(LED, OUTPUT);
pinMode(LED2, OUTPUT);
pinMode(PWM, OUTPUT);
pinMode(PWM2, OUTPUT);
pinMode(BUZZER, OUTPUT);

digitalWrite(LED, HIGH);
Expand All @@ -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);
Expand Down