Arduino frameworks

- AltSoftSerial UART with WT61 IMU
- UART with Pi on TX0/RX0 (and USB)
This commit is contained in:
Mikkeli Matlock
2026-02-01 11:47:15 +09:00
parent 7a6e69861b
commit 559e62e292
6 changed files with 356 additions and 26 deletions

View File

@@ -11,7 +11,58 @@ Sensor interface running on Arduino Nano, communicating with Pi via UART.
## Current Capabilities ## Current Capabilities
- Battery voltage monitoring (voltage divider on A0) - 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 ## Planned
@@ -19,18 +70,3 @@ Sensor interface running on Arduino Nano, communicating with Pi via UART.
- Engine temperature (thermocouple/NTC) - Engine temperature (thermocouple/NTC)
- Gear position indicator - Gear position indicator
- Turn signal / high beam status - 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.

70
arduino/main/comms.cpp Normal file
View File

@@ -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;
}

25
arduino/main/comms.h Normal file
View File

@@ -0,0 +1,25 @@
#ifndef COMMS_H
#define COMMS_H
#include <Arduino.h>
// 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

115
arduino/main/imu.cpp Normal file
View File

@@ -0,0 +1,115 @@
#include "imu.h"
#include <AltSoftSerial.h>
// 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;
}

31
arduino/main/imu.h Normal file
View File

@@ -0,0 +1,31 @@
#ifndef IMU_H
#define IMU_H
#include <Arduino.h>
// 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

View File

@@ -2,24 +2,77 @@
// Motorcycle telemetry hub // Motorcycle telemetry hub
#include "voltage.h" #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() { void setup() {
Serial.begin(9600);
pinMode(LED_BUILTIN, OUTPUT); pinMode(LED_BUILTIN, OUTPUT);
voltage_init(); voltage_init();
imu_init(); // AltSoftSerial on pins 8(RX)/9(TX)
comms_init(); // Hardware Serial to Pi at 115200
} }
void loop() { void loop() {
// Report battery voltage // Always poll IMU - it's streaming at 20Hz
Serial.print("V_bat: "); imu_update();
Serial.print(voltage_read(), 2);
Serial.println("V");
// Heartbeat blink // Process any commands from Pi
digitalWrite(LED_BUILTIN, HIGH); if (comms_update()) {
delay(50); const char* cmd = comms_get_command();
digitalWrite(LED_BUILTIN, LOW); // 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));
}
} }