Arduino frameworks
- AltSoftSerial UART with WT61 IMU - UART with Pi on TX0/RX0 (and USB)
This commit is contained in:
@@ -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.
|
||||
|
||||
70
arduino/main/comms.cpp
Normal file
70
arduino/main/comms.cpp
Normal 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
25
arduino/main/comms.h
Normal 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
115
arduino/main/imu.cpp
Normal 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
31
arduino/main/imu.h
Normal 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
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user