Added crash-triggered hazard lights, flip confusion mode, and start-up and shutdown sequence.
This commit is contained in:
@@ -0,0 +1,858 @@
|
||||
#include <Button.h>
|
||||
#include <Adafruit_NeoPixel.h>
|
||||
#include <Wire.h>
|
||||
|
||||
// without USB CDC, Serial falls back to USART1 on PA9/PA10, hijacking the mode and right buttons
|
||||
#ifndef USBD_USE_CDC
|
||||
#error "Set Tools > USB support to CDC (generic 'Serial' supersede U(S)ART)"
|
||||
#endif
|
||||
|
||||
/*
|
||||
Records the IMU to internal flash to work out what a crash looks like, download with tools/crash_dump.py
|
||||
|
||||
MODE start/stop recording, starting erases the previous recording, hold 5s to power off
|
||||
LEFT/RIGHT add marker 1/2 to the recording, e.g. "crash now" and "bump now"
|
||||
|
||||
DASH dim blue: empty, green: recording stored (bar shows length), amber: erasing,
|
||||
red: recording (bar shows storage used), blue: downloading, purple blinking: error
|
||||
|
||||
ONBOARD LED fallback for when the dash has no power without the battery
|
||||
off: idle, fast flicker: erasing, slow blink: recording, short flicker: marker added,
|
||||
on: downloading, rapid blink: error
|
||||
|
||||
serial commands: r record, s stop, d dump, i info, l live sensor reading
|
||||
*/
|
||||
|
||||
constexpr int LEDS_TAIL_PIN = PA6;
|
||||
constexpr int LEDS_TAIL_LENGTH = 10;
|
||||
|
||||
constexpr int LEDS_LEFT_PIN = PA5;
|
||||
constexpr int LEDS_RIGHT_PIN = PA7;
|
||||
constexpr int LEDS_HEAD_LENGTH = 39;
|
||||
|
||||
constexpr int LEDS_DASH_PIN = PB0;
|
||||
constexpr int LEDS_DASH_LENGTH = 2;
|
||||
constexpr int LEDS_DASH_BRIGHTNESS = 100;
|
||||
|
||||
constexpr int PIN_ALIVE = PA2;
|
||||
constexpr int PIN_BUCK = PA1;
|
||||
|
||||
constexpr int PIN_PEDAL = PA0;
|
||||
constexpr int PIN_PEDAL_FORWARD = PA3;
|
||||
constexpr int PIN_PEDAL_REVERSE = PA4;
|
||||
|
||||
constexpr int PIN_BUTTON_MODE = PA9;
|
||||
constexpr int PIN_BUTTON_LEFT = PA8;
|
||||
constexpr int PIN_BUTTON_RIGHT = PA10;
|
||||
|
||||
constexpr int PIN_IMU_SCL = PB6;
|
||||
constexpr int PIN_IMU_SDA = PB7;
|
||||
|
||||
constexpr int LED_ON = LOW;
|
||||
constexpr int LED_OFF = HIGH;
|
||||
|
||||
constexpr int BUCK_ON = HIGH;
|
||||
constexpr int BUCK_OFF = LOW;
|
||||
|
||||
constexpr unsigned long SHUTDOWN_HOLD = 5000; // ms
|
||||
constexpr unsigned long IDLE_TIMEOUT = 180000; // ms, not while recording
|
||||
constexpr unsigned long STATUS_INTERVAL = 50; // ms
|
||||
constexpr unsigned long MARKER_FLASH = 150; // ms
|
||||
|
||||
// MPU9250/9255
|
||||
constexpr uint8_t IMU_ADDRESSES[] = {0x68, 0x69}; // AD0 low, AD0 high
|
||||
constexpr uint8_t IMU_REG_SMPLRT_DIV = 0x19;
|
||||
constexpr uint8_t IMU_REG_CONFIG = 0x1A;
|
||||
constexpr uint8_t IMU_REG_GYRO_CONFIG = 0x1B;
|
||||
constexpr uint8_t IMU_REG_ACCEL_CONFIG = 0x1C;
|
||||
constexpr uint8_t IMU_REG_ACCEL_CONFIG2 = 0x1D;
|
||||
constexpr uint8_t IMU_REG_FIFO_EN = 0x23;
|
||||
constexpr uint8_t IMU_REG_INT_PIN_CFG = 0x37;
|
||||
constexpr uint8_t IMU_REG_INT_ENABLE = 0x38;
|
||||
constexpr uint8_t IMU_REG_INT_STATUS = 0x3A;
|
||||
constexpr uint8_t IMU_REG_USER_CTRL = 0x6A;
|
||||
constexpr uint8_t IMU_REG_PWR_MGMT_1 = 0x6B;
|
||||
constexpr uint8_t IMU_REG_FIFO_COUNT_H = 0x72;
|
||||
constexpr uint8_t IMU_REG_FIFO_R_W = 0x74;
|
||||
constexpr uint8_t IMU_REG_WHO_AM_I = 0x75;
|
||||
|
||||
constexpr uint8_t IMU_FIFO_EN_ACCEL_GYRO = 0x78;
|
||||
constexpr uint8_t IMU_USER_CTRL_FIFO_EN = 0x40;
|
||||
constexpr uint8_t IMU_USER_CTRL_FIFO_RST = 0x04;
|
||||
constexpr uint8_t IMU_INT_FIFO_OFLOW = 0x10;
|
||||
constexpr uint8_t IMU_INT_PIN_CFG_BYPASS = 0x02; // exposes the magnetometer on the main bus
|
||||
|
||||
constexpr int SAMPLE_RATE = 500; // Hz
|
||||
constexpr uint8_t IMU_DLPF_184HZ = 0x01; // 1kHz internal rate, used for gyro and accel
|
||||
constexpr uint8_t IMU_ACCEL_16G = 0x18;
|
||||
constexpr uint8_t IMU_GYRO_2000DPS = 0x18;
|
||||
constexpr uint16_t ACCEL_LSB_PER_G = 2048;
|
||||
constexpr uint16_t GYRO_LSB_PER_DPS_X10 = 164;
|
||||
|
||||
constexpr int FIFO_SIZE = 512;
|
||||
constexpr int FIFO_SAMPLE_SIZE = 12; // accel xyz + gyro xyz, big-endian
|
||||
constexpr int FIFO_READ_BATCH = 2; // samples per I2C read, stays within the 32 byte Wire buffer
|
||||
constexpr unsigned long FIFO_INTERVAL = 10; // ms, the fifo holds ~84ms at 500Hz
|
||||
|
||||
// AK8963 magnetometer inside the MPU9250/9255
|
||||
constexpr uint8_t MAG_ADDRESS = 0x0C;
|
||||
constexpr uint8_t MAG_REG_WIA = 0x00;
|
||||
constexpr uint8_t MAG_REG_ST1 = 0x02;
|
||||
constexpr uint8_t MAG_REG_HXL = 0x03;
|
||||
constexpr uint8_t MAG_REG_CNTL1 = 0x0A;
|
||||
constexpr uint8_t MAG_WIA = 0x48;
|
||||
constexpr uint8_t MAG_ST1_DRDY = 0x01;
|
||||
constexpr uint8_t MAG_ST2_HOFL = 0x08;
|
||||
constexpr uint8_t MAG_CNTL1_POWER_DOWN = 0x00;
|
||||
constexpr uint8_t MAG_CNTL1_16BIT_100HZ = 0x16;
|
||||
constexpr unsigned long MAG_INTERVAL = 10; // ms
|
||||
|
||||
// sectors 0-3 (64KB) hold this sketch, sectors 4-7 (448KB) hold the recording
|
||||
constexpr uint32_t STORAGE_START = 0x08010000;
|
||||
constexpr uint32_t STORAGE_END = 0x08080000;
|
||||
constexpr uint32_t STORAGE_SIZE = STORAGE_END - STORAGE_START;
|
||||
constexpr uint32_t STORAGE_SECTOR_FIRST = FLASH_SECTOR_4;
|
||||
constexpr int STORAGE_SECTOR_COUNT = 4;
|
||||
|
||||
constexpr float RECORD_BYTES_PER_SECOND = SAMPLE_RATE * (1 + FIFO_SAMPLE_SIZE) + (1000 / MAG_INTERVAL) * (1 + 6);
|
||||
|
||||
// recording format, a stream of tagged records, keep in sync with tools/crash_dump.py
|
||||
enum RecordTag : uint8_t {
|
||||
TAG_HEADER = 0x01, // 12 bytes: "QLR", version, WHO_AM_I, mag ok, rate, accel LSB/g, gyro LSB/dps * 10 (uint16 LE)
|
||||
TAG_IMU = 0x02, // 12 bytes: raw fifo sample, accel xyz + gyro xyz, int16 BE
|
||||
TAG_MAG = 0x03, // 6 bytes: raw magnetometer xyz, int16 LE, 0.15uT/LSB without sensitivity adjustment
|
||||
TAG_INPUTS = 0x04, // 1 byte: raw pin levels, see INPUT_BIT_*, low is active
|
||||
TAG_MARKER = 0x05, // 1 byte: marker number
|
||||
TAG_OVERFLOW = 0x06, // 0 bytes: fifo overflowed, an unknown number of samples was lost
|
||||
TAG_END = 0xFF, // erased flash
|
||||
};
|
||||
|
||||
constexpr uint8_t RECORD_VERSION = 1;
|
||||
|
||||
constexpr uint8_t INPUT_BIT_PEDAL = 0;
|
||||
constexpr uint8_t INPUT_BIT_FORWARD = 1;
|
||||
constexpr uint8_t INPUT_BIT_REVERSE = 2;
|
||||
constexpr uint8_t INPUT_BIT_MODE = 3;
|
||||
constexpr uint8_t INPUT_BIT_LEFT = 4;
|
||||
constexpr uint8_t INPUT_BIT_RIGHT = 5;
|
||||
|
||||
// end of the program in flash, from the linker script
|
||||
extern "C" uint32_t _sidata, _sdata, _edata;
|
||||
|
||||
Button buttonMode(PIN_BUTTON_MODE);
|
||||
Button buttonLeft(PIN_BUTTON_LEFT);
|
||||
Button buttonRight(PIN_BUTTON_RIGHT);
|
||||
|
||||
Adafruit_NeoPixel ledsTail(LEDS_TAIL_LENGTH, LEDS_TAIL_PIN, NEO_GRBW + NEO_KHZ800);
|
||||
Adafruit_NeoPixel ledsHeadLeft(LEDS_HEAD_LENGTH, LEDS_LEFT_PIN, NEO_GRBW + NEO_KHZ800);
|
||||
Adafruit_NeoPixel ledsHeadRight(LEDS_HEAD_LENGTH, LEDS_RIGHT_PIN, NEO_GRBW + NEO_KHZ800);
|
||||
Adafruit_NeoPixel ledsDash(LEDS_DASH_LENGTH, LEDS_DASH_PIN, NEO_GRBW + NEO_KHZ800);
|
||||
|
||||
const uint32_t COLOR_OFF = ledsDash.Color(0, 0, 0, 0);
|
||||
const uint32_t COLOR_DIM = ledsDash.Color(0, 0, 0, 10);
|
||||
const uint32_t COLOR_WHITE = ledsDash.Color(0, 0, 0, 255);
|
||||
const uint32_t COLOR_EMPTY = ledsDash.Color(0, 0, 40, 0);
|
||||
const uint32_t COLOR_STORED = ledsDash.Color(0, 255, 0, 0);
|
||||
const uint32_t COLOR_ERASING = ledsDash.Color(255, 60, 0, 0);
|
||||
const uint32_t COLOR_RECORDING = ledsDash.Color(255, 0, 0, 0);
|
||||
const uint32_t COLOR_DUMPING = ledsDash.Color(0, 0, 255, 0);
|
||||
const uint32_t COLOR_ERROR = ledsDash.Color(255, 0, 255, 0);
|
||||
|
||||
enum State {
|
||||
STATE_IDLE,
|
||||
STATE_ERASING,
|
||||
STATE_RECORDING,
|
||||
STATE_DUMPING,
|
||||
STATE_ERROR,
|
||||
STATE_OFF,
|
||||
};
|
||||
|
||||
State state = STATE_IDLE;
|
||||
const char* errorReason = "";
|
||||
|
||||
uint8_t imuAddress = 0; // 0 when not found
|
||||
uint8_t imuWhoAmI = 0;
|
||||
bool magFound = false;
|
||||
|
||||
uint32_t storageAddress = STORAGE_START; // next word to program
|
||||
uint32_t storageWord = 0xFFFFFFFF;
|
||||
int storageWordBytes = 0;
|
||||
bool storageFull = false;
|
||||
uint32_t recordedBytes = 0;
|
||||
int sectorsErased = 0;
|
||||
|
||||
unsigned long recordStart = 0;
|
||||
unsigned long lastFifoRead = 0;
|
||||
unsigned long lastMagRead = 0;
|
||||
unsigned long lastStatus = 0;
|
||||
unsigned long lastActivity = 0;
|
||||
unsigned long markerFlashUntil = 0;
|
||||
uint8_t lastInputs = 0;
|
||||
uint32_t overflowCount = 0;
|
||||
|
||||
unsigned long modePressStart = 0;
|
||||
bool modePressed = false;
|
||||
|
||||
// I2C
|
||||
|
||||
void writeRegister(uint8_t address, uint8_t reg, uint8_t value) {
|
||||
Wire.beginTransmission(address);
|
||||
Wire.write(reg);
|
||||
Wire.write(value);
|
||||
Wire.endTransmission();
|
||||
}
|
||||
|
||||
bool readRegisters(uint8_t address, uint8_t reg, uint8_t* buffer, uint8_t length) {
|
||||
Wire.beginTransmission(address);
|
||||
Wire.write(reg);
|
||||
|
||||
if (Wire.endTransmission(false) != 0 || Wire.requestFrom(address, length) != length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = 0; i < length; i++) {
|
||||
buffer[i] = Wire.read();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
uint8_t readRegister(uint8_t address, uint8_t reg) {
|
||||
uint8_t value = 0;
|
||||
readRegisters(address, reg, &value, 1);
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
// IMU
|
||||
|
||||
void connectImu() {
|
||||
for (const uint8_t address : IMU_ADDRESSES) {
|
||||
uint8_t whoAmI = 0;
|
||||
|
||||
if (readRegisters(address, IMU_REG_WHO_AM_I, &whoAmI, 1)) {
|
||||
imuAddress = address;
|
||||
imuWhoAmI = whoAmI;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (imuAddress == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
writeRegister(imuAddress, IMU_REG_PWR_MGMT_1, 0x80); // reset
|
||||
delay(100);
|
||||
writeRegister(imuAddress, IMU_REG_PWR_MGMT_1, 0x01); // gyro PLL clock
|
||||
delay(10);
|
||||
|
||||
writeRegister(imuAddress, IMU_REG_CONFIG, IMU_DLPF_184HZ);
|
||||
writeRegister(imuAddress, IMU_REG_SMPLRT_DIV, 1000 / SAMPLE_RATE - 1);
|
||||
writeRegister(imuAddress, IMU_REG_GYRO_CONFIG, IMU_GYRO_2000DPS);
|
||||
writeRegister(imuAddress, IMU_REG_ACCEL_CONFIG, IMU_ACCEL_16G);
|
||||
writeRegister(imuAddress, IMU_REG_ACCEL_CONFIG2, IMU_DLPF_184HZ);
|
||||
writeRegister(imuAddress, IMU_REG_INT_ENABLE, IMU_INT_FIFO_OFLOW);
|
||||
writeRegister(imuAddress, IMU_REG_INT_PIN_CFG, IMU_INT_PIN_CFG_BYPASS);
|
||||
|
||||
magFound = readRegister(MAG_ADDRESS, MAG_REG_WIA) == MAG_WIA;
|
||||
|
||||
if (magFound) {
|
||||
writeRegister(MAG_ADDRESS, MAG_REG_CNTL1, MAG_CNTL1_POWER_DOWN);
|
||||
delay(10);
|
||||
writeRegister(MAG_ADDRESS, MAG_REG_CNTL1, MAG_CNTL1_16BIT_100HZ);
|
||||
}
|
||||
}
|
||||
|
||||
void startFifo() {
|
||||
writeRegister(imuAddress, IMU_REG_FIFO_EN, 0);
|
||||
writeRegister(imuAddress, IMU_REG_USER_CTRL, IMU_USER_CTRL_FIFO_RST);
|
||||
readRegister(imuAddress, IMU_REG_INT_STATUS); // clears a stale overflow flag
|
||||
writeRegister(imuAddress, IMU_REG_USER_CTRL, IMU_USER_CTRL_FIFO_EN);
|
||||
writeRegister(imuAddress, IMU_REG_FIFO_EN, IMU_FIFO_EN_ACCEL_GYRO);
|
||||
}
|
||||
|
||||
void stopFifo() {
|
||||
writeRegister(imuAddress, IMU_REG_FIFO_EN, 0);
|
||||
writeRegister(imuAddress, IMU_REG_USER_CTRL, 0);
|
||||
}
|
||||
|
||||
// storage
|
||||
|
||||
uint32_t programEnd() {
|
||||
return (uint32_t)&_sidata + ((uint32_t)&_edata - (uint32_t)&_sdata);
|
||||
}
|
||||
|
||||
bool hasHeader() {
|
||||
const uint8_t* storage = (const uint8_t*)STORAGE_START;
|
||||
|
||||
return storage[0] == TAG_HEADER && storage[1] == 'Q' && storage[2] == 'L' && storage[3] == 'R';
|
||||
}
|
||||
|
||||
// recordings end at the first erased tag, the last programmed word marks where to stop downloading
|
||||
uint32_t findRecordedBytes() {
|
||||
if (!hasHeader()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
for (uint32_t address = STORAGE_END - 4; address >= STORAGE_START; address -= 4) {
|
||||
if (*(const uint32_t*)address != 0xFFFFFFFF) {
|
||||
return address + 4 - STORAGE_START;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void flushWord() {
|
||||
if (storageWordBytes == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// unwritten bytes stay 0xFF, which reads as the end tag
|
||||
if (HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, storageAddress, storageWord) != HAL_OK) {
|
||||
storageFull = true;
|
||||
errorReason = "FLASH WRITE FAILED";
|
||||
}
|
||||
|
||||
storageAddress += 4;
|
||||
storageWord = 0xFFFFFFFF;
|
||||
storageWordBytes = 0;
|
||||
}
|
||||
|
||||
void storeByte(uint8_t value) {
|
||||
storageWord &= ~(0xFFu << (storageWordBytes * 8));
|
||||
storageWord |= (uint32_t)value << (storageWordBytes * 8);
|
||||
storageWordBytes++;
|
||||
|
||||
if (storageWordBytes == 4) {
|
||||
flushWord();
|
||||
}
|
||||
}
|
||||
|
||||
// only whole records are stored, so a full recording still parses cleanly
|
||||
void storeRecord(uint8_t tag, const uint8_t* payload, int length) {
|
||||
if (storageFull || storageAddress + storageWordBytes + 1 + length > STORAGE_END) {
|
||||
storageFull = true;
|
||||
return;
|
||||
}
|
||||
|
||||
storeByte(tag);
|
||||
|
||||
for (int i = 0; i < length; i++) {
|
||||
storeByte(payload[i]);
|
||||
}
|
||||
}
|
||||
|
||||
void storeHeader() {
|
||||
const uint8_t header[] = {
|
||||
'Q', 'L', 'R',
|
||||
RECORD_VERSION,
|
||||
imuWhoAmI,
|
||||
magFound,
|
||||
SAMPLE_RATE & 0xFF, SAMPLE_RATE >> 8,
|
||||
ACCEL_LSB_PER_G & 0xFF, ACCEL_LSB_PER_G >> 8,
|
||||
GYRO_LSB_PER_DPS_X10 & 0xFF, GYRO_LSB_PER_DPS_X10 >> 8,
|
||||
};
|
||||
|
||||
storeRecord(TAG_HEADER, header, sizeof(header));
|
||||
}
|
||||
|
||||
bool eraseStorage() {
|
||||
HAL_FLASH_Unlock();
|
||||
__HAL_FLASH_CLEAR_FLAG(FLASH_FLAG_EOP | FLASH_FLAG_OPERR | FLASH_FLAG_WRPERR | FLASH_FLAG_PGAERR | FLASH_FLAG_PGPERR | FLASH_FLAG_PGSERR);
|
||||
|
||||
// one sector at a time so the dash can show progress, each 128KB sector takes about a second
|
||||
for (sectorsErased = 0; sectorsErased < STORAGE_SECTOR_COUNT; sectorsErased++) {
|
||||
renderStatus(millis());
|
||||
|
||||
FLASH_EraseInitTypeDef erase = {};
|
||||
erase.TypeErase = FLASH_TYPEERASE_SECTORS;
|
||||
erase.Sector = STORAGE_SECTOR_FIRST + sectorsErased;
|
||||
erase.NbSectors = 1;
|
||||
erase.VoltageRange = FLASH_VOLTAGE_RANGE_3;
|
||||
|
||||
uint32_t sectorError = 0;
|
||||
|
||||
if (HAL_FLASHEx_Erase(&erase, §orError) != HAL_OK) {
|
||||
HAL_FLASH_Lock();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// recording
|
||||
|
||||
uint8_t readInputs() {
|
||||
return digitalRead(PIN_PEDAL) << INPUT_BIT_PEDAL
|
||||
| digitalRead(PIN_PEDAL_FORWARD) << INPUT_BIT_FORWARD
|
||||
| digitalRead(PIN_PEDAL_REVERSE) << INPUT_BIT_REVERSE
|
||||
| digitalRead(PIN_BUTTON_MODE) << INPUT_BIT_MODE
|
||||
| digitalRead(PIN_BUTTON_LEFT) << INPUT_BIT_LEFT
|
||||
| digitalRead(PIN_BUTTON_RIGHT) << INPUT_BIT_RIGHT;
|
||||
}
|
||||
|
||||
void recordInputs() {
|
||||
const uint8_t inputs = readInputs();
|
||||
|
||||
if (inputs != lastInputs) {
|
||||
lastInputs = inputs;
|
||||
storeRecord(TAG_INPUTS, &inputs, 1);
|
||||
}
|
||||
}
|
||||
|
||||
void recordMarker(uint8_t marker) {
|
||||
storeRecord(TAG_MARKER, &marker, 1);
|
||||
markerFlashUntil = millis() + MARKER_FLASH;
|
||||
|
||||
Serial.print("MARKER ");
|
||||
Serial.println(marker);
|
||||
}
|
||||
|
||||
void readFifo() {
|
||||
const uint8_t status = readRegister(imuAddress, IMU_REG_INT_STATUS);
|
||||
|
||||
uint8_t countBytes[2] = {0, 0};
|
||||
readRegisters(imuAddress, IMU_REG_FIFO_COUNT_H, countBytes, 2);
|
||||
const int count = (countBytes[0] & 0x1F) << 8 | countBytes[1];
|
||||
|
||||
// once full, new samples overwrite the oldest and the stream loses alignment
|
||||
if (status & IMU_INT_FIFO_OFLOW || count > FIFO_SIZE - FIFO_SAMPLE_SIZE) {
|
||||
storeRecord(TAG_OVERFLOW, nullptr, 0);
|
||||
startFifo();
|
||||
overflowCount++;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t buffer[FIFO_SAMPLE_SIZE * FIFO_READ_BATCH];
|
||||
|
||||
for (int samples = count / FIFO_SAMPLE_SIZE; samples > 0; samples -= FIFO_READ_BATCH) {
|
||||
const int batch = min(samples, FIFO_READ_BATCH);
|
||||
|
||||
if (!readRegisters(imuAddress, IMU_REG_FIFO_R_W, buffer, batch * FIFO_SAMPLE_SIZE)) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < batch; i++) {
|
||||
storeRecord(TAG_IMU, buffer + i * FIFO_SAMPLE_SIZE, FIFO_SAMPLE_SIZE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void readMag() {
|
||||
if (!(readRegister(MAG_ADDRESS, MAG_REG_ST1) & MAG_ST1_DRDY)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// reading through ST2 is required to release the next measurement
|
||||
uint8_t data[7];
|
||||
|
||||
if (readRegisters(MAG_ADDRESS, MAG_REG_HXL, data, sizeof(data)) && !(data[6] & MAG_ST2_HOFL)) {
|
||||
storeRecord(TAG_MAG, data, 6);
|
||||
}
|
||||
}
|
||||
|
||||
void printRecordedLength(uint32_t bytes) {
|
||||
Serial.print(bytes);
|
||||
Serial.print(" BYTES, ~");
|
||||
Serial.print(bytes / RECORD_BYTES_PER_SECOND, 1);
|
||||
Serial.println(" S");
|
||||
}
|
||||
|
||||
void startRecording() {
|
||||
if (state != STATE_IDLE) {
|
||||
return;
|
||||
}
|
||||
|
||||
Serial.println("ERASING");
|
||||
state = STATE_ERASING;
|
||||
|
||||
if (!eraseStorage()) {
|
||||
state = STATE_ERROR;
|
||||
errorReason = "FLASH ERASE FAILED";
|
||||
Serial.println(errorReason);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
storageAddress = STORAGE_START;
|
||||
storageWord = 0xFFFFFFFF;
|
||||
storageWordBytes = 0;
|
||||
storageFull = false;
|
||||
recordedBytes = 0;
|
||||
overflowCount = 0;
|
||||
|
||||
storeHeader();
|
||||
|
||||
lastInputs = readInputs();
|
||||
storeRecord(TAG_INPUTS, &lastInputs, 1);
|
||||
|
||||
startFifo();
|
||||
|
||||
recordStart = millis();
|
||||
lastFifoRead = recordStart;
|
||||
lastMagRead = recordStart;
|
||||
state = STATE_RECORDING;
|
||||
|
||||
Serial.println("RECORDING");
|
||||
}
|
||||
|
||||
void stopRecording() {
|
||||
if (state != STATE_RECORDING) {
|
||||
return;
|
||||
}
|
||||
|
||||
stopFifo();
|
||||
flushWord();
|
||||
HAL_FLASH_Lock();
|
||||
|
||||
recordedBytes = storageAddress - STORAGE_START;
|
||||
state = STATE_IDLE;
|
||||
lastActivity = millis();
|
||||
|
||||
Serial.print(storageFull ? "STORAGE FULL, STOPPED, " : "STOPPED, ");
|
||||
printRecordedLength(recordedBytes);
|
||||
|
||||
if (overflowCount > 0) {
|
||||
Serial.print("FIFO OVERFLOWS ");
|
||||
Serial.println(overflowCount);
|
||||
}
|
||||
}
|
||||
|
||||
// serial
|
||||
|
||||
void printInfo() {
|
||||
Serial.print("IMU ");
|
||||
|
||||
if (imuAddress == 0) {
|
||||
Serial.println("NOT FOUND ON PB6/PB7");
|
||||
} else {
|
||||
Serial.print("0x");
|
||||
Serial.print(imuAddress, HEX);
|
||||
Serial.print(", WHO_AM_I 0x");
|
||||
Serial.print(imuWhoAmI, HEX);
|
||||
Serial.println(magFound ? ", MAG OK" : ", MAG NOT FOUND");
|
||||
}
|
||||
|
||||
Serial.print("PROGRAM ENDS 0x");
|
||||
Serial.print(programEnd(), HEX);
|
||||
Serial.print(", STORAGE 0x");
|
||||
Serial.print(STORAGE_START, HEX);
|
||||
Serial.print(" - 0x");
|
||||
Serial.println(STORAGE_END, HEX);
|
||||
|
||||
if (state == STATE_ERROR) {
|
||||
Serial.print("ERROR ");
|
||||
Serial.println(errorReason);
|
||||
}
|
||||
|
||||
Serial.print("RECORDING ");
|
||||
|
||||
if (state == STATE_RECORDING) {
|
||||
Serial.print("IN PROGRESS, ");
|
||||
printRecordedLength(storageAddress - STORAGE_START);
|
||||
} else {
|
||||
printRecordedLength(recordedBytes);
|
||||
}
|
||||
|
||||
Serial.println("INFO END");
|
||||
}
|
||||
|
||||
// reads straight from the data registers instead of the fifo, handy for checking orientation and calibration
|
||||
void printLive() {
|
||||
constexpr uint8_t IMU_REG_ACCEL_XOUT_H = 0x3B;
|
||||
|
||||
Serial.print("CONFIG 0x");
|
||||
Serial.print(readRegister(imuAddress, IMU_REG_CONFIG), HEX);
|
||||
Serial.print(", GYRO_CONFIG 0x");
|
||||
Serial.print(readRegister(imuAddress, IMU_REG_GYRO_CONFIG), HEX);
|
||||
Serial.print(", ACCEL_CONFIG 0x");
|
||||
Serial.print(readRegister(imuAddress, IMU_REG_ACCEL_CONFIG), HEX);
|
||||
Serial.print(", ACCEL_CONFIG2 0x");
|
||||
Serial.println(readRegister(imuAddress, IMU_REG_ACCEL_CONFIG2), HEX);
|
||||
|
||||
uint8_t data[14];
|
||||
|
||||
if (!readRegisters(imuAddress, IMU_REG_ACCEL_XOUT_H, data, sizeof(data))) {
|
||||
Serial.println("LIVE READ FAILED");
|
||||
return;
|
||||
}
|
||||
|
||||
// accel xyz, temperature, gyro xyz
|
||||
const char* names[] = {"AX", "AY", "AZ", "T", "GX", "GY", "GZ"};
|
||||
const float scales[] = {ACCEL_LSB_PER_G, ACCEL_LSB_PER_G, ACCEL_LSB_PER_G, 1, GYRO_LSB_PER_DPS_X10 / 10.0f, GYRO_LSB_PER_DPS_X10 / 10.0f, GYRO_LSB_PER_DPS_X10 / 10.0f};
|
||||
|
||||
for (int i = 0; i < 7; i++) {
|
||||
const int16_t raw = (int16_t)(data[i * 2] << 8 | data[i * 2 + 1]);
|
||||
|
||||
Serial.print(names[i]);
|
||||
Serial.print(" ");
|
||||
Serial.print(raw);
|
||||
|
||||
if (i == 3) {
|
||||
Serial.print(" (");
|
||||
Serial.print(raw / 333.87f + 21, 1); // MPU9250 datasheet
|
||||
Serial.print("C)");
|
||||
} else {
|
||||
Serial.print(" (");
|
||||
Serial.print(raw / scales[i], 3);
|
||||
Serial.print(")");
|
||||
}
|
||||
|
||||
Serial.print(i < 6 ? ", " : "\n");
|
||||
}
|
||||
}
|
||||
|
||||
void dump() {
|
||||
// also works while off or in error, so a recording can be downloaded after the idle timeout
|
||||
if (state == STATE_RECORDING || state == STATE_ERASING || state == STATE_DUMPING) {
|
||||
Serial.println("DUMP UNAVAILABLE");
|
||||
return;
|
||||
}
|
||||
|
||||
const State previousState = state;
|
||||
state = STATE_DUMPING;
|
||||
renderStatus(millis());
|
||||
|
||||
Serial.print("DUMP ");
|
||||
Serial.println(recordedBytes);
|
||||
|
||||
for (uint32_t offset = 0; offset < recordedBytes; offset += 256) {
|
||||
Serial.write((const uint8_t*)(STORAGE_START + offset), min(recordedBytes - offset, (uint32_t)256));
|
||||
}
|
||||
|
||||
Serial.println();
|
||||
Serial.println("DUMP END");
|
||||
|
||||
state = previousState;
|
||||
}
|
||||
|
||||
void handleSerial() {
|
||||
while (Serial.available() > 0) {
|
||||
const char command = Serial.read();
|
||||
lastActivity = millis();
|
||||
|
||||
if (command == 'r') {
|
||||
startRecording();
|
||||
} else if (command == 's') {
|
||||
stopRecording();
|
||||
} else if (command == 'd') {
|
||||
dump();
|
||||
} else if (command == 'i') {
|
||||
printInfo();
|
||||
} else if (command == 'l' && imuAddress != 0) {
|
||||
printLive();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// power
|
||||
|
||||
void shutdown() {
|
||||
// when STLink or USB prevents full power cut, loop keeps running, only shut down once
|
||||
if (state == STATE_OFF) {
|
||||
return;
|
||||
}
|
||||
|
||||
stopRecording();
|
||||
|
||||
Serial.println("SHUTTING DOWN");
|
||||
state = STATE_OFF;
|
||||
renderStatus(millis());
|
||||
|
||||
digitalWrite(PIN_BUCK, BUCK_OFF);
|
||||
delay(100);
|
||||
|
||||
Serial.println("SELF OFF, GOODBYE");
|
||||
digitalWrite(PIN_ALIVE, LOW);
|
||||
}
|
||||
|
||||
// only used when STLink or USB prevents full power cut
|
||||
void restart() {
|
||||
Serial.println("RESTARTING");
|
||||
|
||||
digitalWrite(PIN_BUCK, BUCK_ON);
|
||||
state = imuAddress == 0 ? STATE_ERROR : STATE_IDLE;
|
||||
lastActivity = millis();
|
||||
}
|
||||
|
||||
// status
|
||||
|
||||
void fillBar(uint32_t color, float fraction) {
|
||||
const int length = constrain((int)ceil(fraction * LEDS_DASH_LENGTH), 1, LEDS_DASH_LENGTH);
|
||||
ledsDash.fill(color, 0, length);
|
||||
}
|
||||
|
||||
void renderStatus(unsigned long now) {
|
||||
ledsDash.fill(COLOR_DIM);
|
||||
|
||||
const bool blink = (now / 250) % 2 == 0;
|
||||
|
||||
if (state == STATE_IDLE && recordedBytes == 0) {
|
||||
ledsDash.fill(COLOR_EMPTY);
|
||||
} else if (state == STATE_IDLE) {
|
||||
fillBar(COLOR_STORED, (float)recordedBytes / STORAGE_SIZE);
|
||||
} else if (state == STATE_ERASING) {
|
||||
fillBar(COLOR_ERASING, (float)(sectorsErased + 1) / STORAGE_SECTOR_COUNT);
|
||||
} else if (state == STATE_RECORDING) {
|
||||
fillBar(COLOR_RECORDING, (float)(storageAddress - STORAGE_START) / STORAGE_SIZE);
|
||||
|
||||
if (blink) {
|
||||
ledsDash.setPixelColor(LEDS_DASH_LENGTH - 1, COLOR_RECORDING);
|
||||
}
|
||||
} else if (state == STATE_DUMPING) {
|
||||
ledsDash.fill(COLOR_DUMPING);
|
||||
} else if (state == STATE_ERROR) {
|
||||
ledsDash.fill(blink ? COLOR_ERROR : COLOR_OFF);
|
||||
} else {
|
||||
ledsDash.fill(COLOR_OFF);
|
||||
}
|
||||
|
||||
if (state != STATE_OFF && now < markerFlashUntil) {
|
||||
ledsDash.fill(COLOR_WHITE);
|
||||
}
|
||||
|
||||
ledsDash.show();
|
||||
|
||||
bool onboard = false;
|
||||
|
||||
if (state == STATE_RECORDING && now < markerFlashUntil) {
|
||||
onboard = (now / 25) % 2 == 0;
|
||||
} else if (state == STATE_ERASING) {
|
||||
onboard = (now / 50) % 2 == 0;
|
||||
} else if (state == STATE_RECORDING) {
|
||||
onboard = (now / 500) % 2 == 0;
|
||||
} else if (state == STATE_DUMPING) {
|
||||
onboard = true;
|
||||
} else if (state == STATE_ERROR) {
|
||||
onboard = (now / 100) % 2 == 0;
|
||||
}
|
||||
|
||||
digitalWrite(LED_BUILTIN, onboard ? LED_ON : LED_OFF);
|
||||
}
|
||||
|
||||
void setup() {
|
||||
pinMode(LED_BUILTIN, OUTPUT);
|
||||
pinMode(PIN_ALIVE, OUTPUT);
|
||||
pinMode(PIN_BUCK, OUTPUT);
|
||||
|
||||
pinMode(PIN_PEDAL, INPUT_PULLUP);
|
||||
pinMode(PIN_PEDAL_FORWARD, INPUT_PULLUP);
|
||||
pinMode(PIN_PEDAL_REVERSE, INPUT_PULLUP);
|
||||
|
||||
digitalWrite(PIN_ALIVE, HIGH);
|
||||
digitalWrite(PIN_BUCK, BUCK_ON);
|
||||
|
||||
buttonMode.begin();
|
||||
buttonLeft.begin();
|
||||
buttonRight.begin();
|
||||
|
||||
Serial.begin(9600);
|
||||
|
||||
// the other strips aren't used, make sure they don't keep showing whatever ran before
|
||||
for (Adafruit_NeoPixel* leds : {&ledsTail, &ledsHeadLeft, &ledsHeadRight}) {
|
||||
leds->begin();
|
||||
leds->clear();
|
||||
leds->show();
|
||||
}
|
||||
|
||||
ledsDash.setBrightness(LEDS_DASH_BRIGHTNESS);
|
||||
ledsDash.begin();
|
||||
|
||||
// setSCL/SDA have to be called before begin()
|
||||
Wire.setSCL(PIN_IMU_SCL);
|
||||
Wire.setSDA(PIN_IMU_SDA);
|
||||
Wire.begin();
|
||||
Wire.setClock(400000);
|
||||
|
||||
connectImu();
|
||||
|
||||
recordedBytes = findRecordedBytes();
|
||||
lastActivity = millis();
|
||||
|
||||
if (programEnd() > STORAGE_START) {
|
||||
state = STATE_ERROR;
|
||||
errorReason = "PROGRAM OVERLAPS STORAGE";
|
||||
} else if (imuAddress == 0) {
|
||||
state = STATE_ERROR;
|
||||
errorReason = "IMU NOT FOUND";
|
||||
}
|
||||
}
|
||||
|
||||
void loop() {
|
||||
const unsigned long now = millis();
|
||||
|
||||
if (state == STATE_OFF && (buttonMode.pressed() || buttonLeft.pressed() || buttonRight.pressed())) {
|
||||
restart();
|
||||
} else if (state != STATE_OFF) {
|
||||
if (buttonMode.pressed()) {
|
||||
modePressed = true;
|
||||
modePressStart = now;
|
||||
lastActivity = now;
|
||||
}
|
||||
|
||||
// act on release, so a long press for shutdown doesn't toggle recording first
|
||||
if (buttonMode.released() && modePressed) {
|
||||
modePressed = false;
|
||||
|
||||
if (state == STATE_RECORDING) {
|
||||
stopRecording();
|
||||
} else {
|
||||
startRecording();
|
||||
}
|
||||
}
|
||||
|
||||
if (modePressed && now - modePressStart > SHUTDOWN_HOLD) {
|
||||
modePressed = false;
|
||||
shutdown();
|
||||
}
|
||||
|
||||
if (buttonLeft.pressed()) {
|
||||
lastActivity = now;
|
||||
|
||||
if (state == STATE_RECORDING) {
|
||||
recordMarker(1);
|
||||
}
|
||||
}
|
||||
|
||||
if (buttonRight.pressed()) {
|
||||
lastActivity = now;
|
||||
|
||||
if (state == STATE_RECORDING) {
|
||||
recordMarker(2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleSerial();
|
||||
|
||||
if (state == STATE_RECORDING) {
|
||||
if (now - lastFifoRead >= FIFO_INTERVAL) {
|
||||
lastFifoRead = now;
|
||||
readFifo();
|
||||
}
|
||||
|
||||
if (magFound && now - lastMagRead >= MAG_INTERVAL) {
|
||||
lastMagRead = now;
|
||||
readMag();
|
||||
}
|
||||
|
||||
recordInputs();
|
||||
|
||||
if (storageFull) {
|
||||
stopRecording();
|
||||
}
|
||||
}
|
||||
|
||||
if (now - lastStatus >= STATUS_INTERVAL) {
|
||||
lastStatus = now;
|
||||
renderStatus(now);
|
||||
}
|
||||
|
||||
if (state != STATE_RECORDING && state != STATE_OFF && now - lastActivity > IDLE_TIMEOUT) {
|
||||
shutdown();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
default_fqbn: STMicroelectronics:stm32:GenF4:pnum=BLACKPILL_F411CE,xserial=generic,usb=CDCgen,xusb=FS,opt=osstd,dbg=none,rtlib=nano,upload_method=swdMethod
|
||||
Reference in New Issue
Block a user