From 559e62e29275c8d858ee6abb2ca43c11c5e150db Mon Sep 17 00:00:00 2001 From: Mikkeli Matlock Date: Sun, 1 Feb 2026 11:47:15 +0900 Subject: [PATCH] Arduino frameworks - AltSoftSerial UART with WT61 IMU - UART with Pi on TX0/RX0 (and USB) --- arduino/README.md | 68 ++++++++++++++++++------ arduino/main/comms.cpp | 70 +++++++++++++++++++++++++ arduino/main/comms.h | 25 +++++++++ arduino/main/imu.cpp | 115 +++++++++++++++++++++++++++++++++++++++++ arduino/main/imu.h | 31 +++++++++++ arduino/main/main.ino | 73 ++++++++++++++++++++++---- 6 files changed, 356 insertions(+), 26 deletions(-) create mode 100644 arduino/main/comms.cpp create mode 100644 arduino/main/comms.h create mode 100644 arduino/main/imu.cpp create mode 100644 arduino/main/imu.h diff --git a/arduino/README.md b/arduino/README.md index d9f4d3f..ed247db 100644 --- a/arduino/README.md +++ b/arduino/README.md @@ -11,7 +11,58 @@ Sensor interface running on Arduino Nano, communicating with Pi via UART. ## Current Capabilities - Battery voltage monitoring (voltage divider on A0) -- Serial output at 9600 baud, 1Hz update rate +- WT61 IMU/gyro via AltSoftSerial (9-axis: accel, gyro, euler angles) +- Duplex UART to Pi at 115200 baud, 10Hz telemetry output +- Simple text-based protocol for easy debugging + +## Dependencies + +Install via Arduino Library Manager: +- **AltSoftSerial** by Paul Stoffregen - for WT61 IMU serial + +## Pin Assignments + +| Pin | Function | +|-----|----------| +| A0 | Battery voltage (via divider) | +| D0 (RX) | Pi UART RX ← Arduino TX | +| D1 (TX) | Pi UART TX → Arduino RX | +| D8 | WT61 IMU RX (AltSoftSerial) | +| D9 | WT61 IMU TX (unused, AltSoftSerial fixed pin) | +| D13 | Status LED (heartbeat) | + +## Hardware + +- **MCU**: Arduino Nano (ATmega328P) +- **Pi Connection**: UART at 115200 baud (TX→RX, RX→TX, common GND) +- **IMU**: WT61 module at 9600 baud, 20Hz output +- **Voltage sensing**: Resistor divider (100k/47k) scaled for 0-20V input + +## Protocol + +Simple text lines, one per sensor reading: +``` +V_bat: 12.45 +Ax: 0.02 +Ay: -0.01 +Az: 1.00 +Gx: 0.50 +Gy: -0.25 +Gz: 0.10 +Roll: 2.35 +Pitch: -1.20 +Yaw: 45.80 +``` + +If IMU data is stale (no valid packets for 200ms): +``` +IMU: STALE +``` + +Commands from Pi are echoed back: +``` +ACK: PING +``` ## Planned @@ -19,18 +70,3 @@ Sensor interface running on Arduino Nano, communicating with Pi via UART. - Engine temperature (thermocouple/NTC) - Gear position indicator - Turn signal / high beam status - -## Hardware - -- **MCU**: Arduino Nano (ATmega328P) -- **Connection**: UART to Pi GPIO (TX→RX, RX→TX, common GND) -- **Voltage sensing**: Resistor divider scaled for 0-20V input range - -## Protocol - -Simple text-based for now: -``` -V_bat: 12.45V -``` - -Future: structured binary or JSON for multiple sensors. diff --git a/arduino/main/comms.cpp b/arduino/main/comms.cpp new file mode 100644 index 0000000..4db3b6c --- /dev/null +++ b/arduino/main/comms.cpp @@ -0,0 +1,70 @@ +#include "comms.h" + +// Pi communication uses hardware Serial (pins 0/1) +// Baud rate - 115200 is reasonable for duplex with Pi +static const long BAUD_RATE = 115200; + +// Command buffer +static const int CMD_BUF_SIZE = 64; +static char cmdBuf[CMD_BUF_SIZE]; +static int cmdIndex = 0; +static bool cmdReady = false; + +// Connection tracking +static unsigned long lastRxTime = 0; + +void comms_init() { + Serial.begin(BAUD_RATE); + cmdIndex = 0; + cmdReady = false; +} + +bool comms_update() { + while (Serial.available()) { + char c = Serial.read(); + lastRxTime = millis(); + + if (c == '\n' || c == '\r') { + if (cmdIndex > 0) { + cmdBuf[cmdIndex] = '\0'; + cmdReady = true; + cmdIndex = 0; + return true; + } + } else if (cmdIndex < CMD_BUF_SIZE - 1) { + cmdBuf[cmdIndex++] = c; + } + // else: overflow, silently drop extra chars + } + return false; +} + +void comms_send(const char* key, float value, int decimals) { + Serial.print(key); + Serial.print(": "); + Serial.println(value, decimals); +} + +void comms_send(const char* key, int value) { + Serial.print(key); + Serial.print(": "); + Serial.println(value); +} + +void comms_send(const char* key, const char* value) { + Serial.print(key); + Serial.print(": "); + Serial.println(value); +} + +const char* comms_get_command() { + if (cmdReady) { + cmdReady = false; + return cmdBuf; + } + return ""; +} + +bool comms_is_connected(unsigned long timeout_ms) { + return (millis() - lastRxTime) < timeout_ms; +} diff --git a/arduino/main/comms.h b/arduino/main/comms.h new file mode 100644 index 0000000..8bcb9e0 --- /dev/null +++ b/arduino/main/comms.h @@ -0,0 +1,25 @@ +#ifndef COMMS_H +#define COMMS_H + +#include + +// Initialize Pi serial communication (call in setup) +void comms_init(); + +// Process incoming commands from Pi - call in loop +// Returns true if a complete command was received +bool comms_update(); + +// Send telemetry line to Pi +void comms_send(const char* key, float value, int decimals = 2); +void comms_send(const char* key, int value); +void comms_send(const char* key, const char* value); + +// Get last received command (empty if none) +// Command buffer is cleared after reading +const char* comms_get_command(); + +// Check if connected (received any data recently) +bool comms_is_connected(unsigned long timeout_ms = 5000); + +#endif diff --git a/arduino/main/imu.cpp b/arduino/main/imu.cpp new file mode 100644 index 0000000..c5585a8 --- /dev/null +++ b/arduino/main/imu.cpp @@ -0,0 +1,115 @@ +#include "imu.h" +#include + +// AltSoftSerial uses fixed pins on ATmega328P: +// RX = Pin 8, TX = Pin 9 (TX not used for WT61) +static AltSoftSerial imuSerial; + +// WT61 packet structure: +// Byte 0: 0x55 (header) +// Byte 1: Packet type (0x51=accel, 0x52=gyro, 0x53=angle) +// Bytes 2-9: Data (4x int16_t, little-endian) +// Byte 10: Checksum (sum of bytes 0-9, lower 8 bits) +static const uint8_t PACKET_HEADER = 0x55; +static const uint8_t PACKET_ACCEL = 0x51; +static const uint8_t PACKET_GYRO = 0x52; +static const uint8_t PACKET_ANGLE = 0x53; +static const int PACKET_SIZE = 11; + +// Receive buffer +static uint8_t rxBuf[PACKET_SIZE]; +static int rxIndex = 0; + +// Latest data +static ImuData currentData = {0}; + +// Scale factors from WT61 datasheet +// Accel: raw / 32768 * 16g +// Gyro: raw / 32768 * 2000 deg/s +// Angle: raw / 32768 * 180 deg +static const float ACCEL_SCALE = 16.0 / 32768.0; +static const float GYRO_SCALE = 2000.0 / 32768.0; +static const float ANGLE_SCALE = 180.0 / 32768.0; + +static int16_t parseI16(uint8_t lo, uint8_t hi) { + return (int16_t)((hi << 8) | lo); +} + +static bool validateChecksum() { + uint8_t sum = 0; + for (int i = 0; i < PACKET_SIZE - 1; i++) { + sum += rxBuf[i]; + } + return sum == rxBuf[PACKET_SIZE - 1]; +} + +static void processPacket() { + if (!validateChecksum()) { + return; // Bad packet, ignore + } + + uint8_t type = rxBuf[1]; + int16_t v0 = parseI16(rxBuf[2], rxBuf[3]); + int16_t v1 = parseI16(rxBuf[4], rxBuf[5]); + int16_t v2 = parseI16(rxBuf[6], rxBuf[7]); + // v3 at bytes 8-9 is temperature, ignored for now + + switch (type) { + case PACKET_ACCEL: + currentData.ax = v0 * ACCEL_SCALE; + currentData.ay = v1 * ACCEL_SCALE; + currentData.az = v2 * ACCEL_SCALE; + break; + case PACKET_GYRO: + currentData.gx = v0 * GYRO_SCALE; + currentData.gy = v1 * GYRO_SCALE; + currentData.gz = v2 * GYRO_SCALE; + break; + case PACKET_ANGLE: + currentData.roll = v0 * ANGLE_SCALE; + currentData.pitch = v1 * ANGLE_SCALE; + currentData.yaw = v2 * ANGLE_SCALE; + currentData.lastUpdate = millis(); + break; + } +} + +void imu_init() { + imuSerial.begin(9600); + rxIndex = 0; + currentData = {0}; +} + +bool imu_update() { + bool gotPacket = false; + + while (imuSerial.available()) { + uint8_t c = imuSerial.read(); + + // State machine: look for header, then collect packet + if (rxIndex == 0) { + if (c == PACKET_HEADER) { + rxBuf[rxIndex++] = c; + } + // else: discard, wait for sync + } else { + rxBuf[rxIndex++] = c; + + if (rxIndex >= PACKET_SIZE) { + processPacket(); + rxIndex = 0; + gotPacket = true; + } + } + } + + return gotPacket; +} + +const ImuData& imu_get_data() { + return currentData; +} + +bool imu_is_fresh(unsigned long timeout_ms) { + return (millis() - currentData.lastUpdate) < timeout_ms; +} diff --git a/arduino/main/imu.h b/arduino/main/imu.h new file mode 100644 index 0000000..fcac228 --- /dev/null +++ b/arduino/main/imu.h @@ -0,0 +1,31 @@ +#ifndef IMU_H +#define IMU_H + +#include + +// WT61 IMU data structure +struct ImuData { + // Acceleration (g) + float ax, ay, az; + // Angular velocity (deg/s) + float gx, gy, gz; + // Euler angles (degrees) + float roll, pitch, yaw; + // Timestamp of last valid packet (millis) + unsigned long lastUpdate; +}; + +// Initialize IMU serial (call in setup) +void imu_init(); + +// Process incoming bytes - call frequently in loop +// Returns true if new complete packet was parsed +bool imu_update(); + +// Get latest IMU data +const ImuData& imu_get_data(); + +// Check if IMU data is fresh (updated within timeout_ms) +bool imu_is_fresh(unsigned long timeout_ms = 200); + +#endif diff --git a/arduino/main/main.ino b/arduino/main/main.ino index f994052..355aece 100644 --- a/arduino/main/main.ino +++ b/arduino/main/main.ino @@ -2,24 +2,77 @@ // Motorcycle telemetry hub #include "voltage.h" +#include "imu.h" +#include "comms.h" + +// Timing +static const unsigned long TELEMETRY_INTERVAL_MS = 100; // 10Hz telemetry +static unsigned long lastTelemetryTime = 0; void setup() { - Serial.begin(9600); pinMode(LED_BUILTIN, OUTPUT); voltage_init(); + imu_init(); // AltSoftSerial on pins 8(RX)/9(TX) + comms_init(); // Hardware Serial to Pi at 115200 } void loop() { - // Report battery voltage - Serial.print("V_bat: "); - Serial.print(voltage_read(), 2); - Serial.println("V"); + // Always poll IMU - it's streaming at 20Hz + imu_update(); - // Heartbeat blink - digitalWrite(LED_BUILTIN, HIGH); - delay(50); - digitalWrite(LED_BUILTIN, LOW); + // Process any commands from Pi + if (comms_update()) { + const char* cmd = comms_get_command(); + // Future: handle commands like "PING", "SET_RATE", etc. + // For now, echo back as acknowledgment + if (cmd[0] != '\0') { + comms_send("ACK", cmd); + } + } - delay(1000); // 1Hz update rate + // Send telemetry at fixed interval + unsigned long now = millis(); + if (now - lastTelemetryTime >= TELEMETRY_INTERVAL_MS) { + lastTelemetryTime = now; + sendTelemetry(); + } + + // Heartbeat - quick blink if IMU fresh, slow blink if stale + static unsigned long lastBlink = 0; + unsigned long blinkInterval = imu_is_fresh() ? 500 : 2000; + if (now - lastBlink >= blinkInterval) { + lastBlink = now; + digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN)); + } +} + +void sendTelemetry() { + // Battery voltage + comms_send("V_bat", voltage_read()); + + // IMU data (only if we have fresh data) + if (imu_is_fresh()) { + const ImuData& imu = imu_get_data(); + + // Acceleration (g) + comms_send("Ax", imu.ax); + comms_send("Ay", imu.ay); + comms_send("Az", imu.az); + + // Angular velocity (deg/s) + comms_send("Gx", imu.gx); + comms_send("Gy", imu.gy); + comms_send("Gz", imu.gz); + + // Euler angles (degrees) + comms_send("Roll", imu.roll); + comms_send("Pitch", imu.pitch); + comms_send("Yaw", imu.yaw); + } else { + comms_send("IMU", "STALE"); + + // flip LED to indicate main cycle + digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN)); + } }