Compare commits
3 Commits
4dd52b47dc
...
e181dcc786
| Author | SHA1 | Date | |
|---|---|---|---|
| e181dcc786 | |||
| 69f2fa0532 | |||
| 6dfb5640db |
@ -2,12 +2,12 @@
|
|||||||
#define BALANCE_H
|
#define BALANCE_H
|
||||||
|
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
#include "icm42688.h"
|
#include "mpu6000.h"
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* SaltyLab Balance Controller
|
* SaltyLab Balance Controller
|
||||||
*
|
*
|
||||||
* Complementary filter (gyro + accel) → pitch angle
|
* Consumes fused IMUData (pitch + pitch_rate from mpu6000 complementary filter)
|
||||||
* PID controller → motor speed command
|
* PID controller → motor speed command
|
||||||
* Safety: tilt cutoff, arming, watchdog
|
* Safety: tilt cutoff, arming, watchdog
|
||||||
*/
|
*/
|
||||||
@ -39,7 +39,7 @@ typedef struct {
|
|||||||
} balance_t;
|
} balance_t;
|
||||||
|
|
||||||
void balance_init(balance_t *b);
|
void balance_init(balance_t *b);
|
||||||
void balance_update(balance_t *b, const icm42688_data_t *imu, float dt);
|
void balance_update(balance_t *b, const IMUData *imu, float dt);
|
||||||
void balance_arm(balance_t *b);
|
void balance_arm(balance_t *b);
|
||||||
void balance_disarm(balance_t *b);
|
void balance_disarm(balance_t *b);
|
||||||
|
|
||||||
|
|||||||
51
include/safety.h
Normal file
51
include/safety.h
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
#ifndef SAFETY_H
|
||||||
|
#define SAFETY_H
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
|
||||||
|
/*
|
||||||
|
* SaltyLab Safety Systems
|
||||||
|
*
|
||||||
|
* Covers:
|
||||||
|
* - IWDG hardware watchdog (MCU reset if main loop hangs)
|
||||||
|
* - RC signal timeout monitoring
|
||||||
|
* - Tilt fault alert via buzzer
|
||||||
|
* - Arm hold interlock (must hold arm for ARMING_HOLD_MS)
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* safety_init() — call once in main() after HAL_Init().
|
||||||
|
* Starts IWDG with WATCHDOG_TIMEOUT_MS timeout from config.h.
|
||||||
|
* Starts ARMING_HOLD_MS countdown from config.h.
|
||||||
|
*/
|
||||||
|
void safety_init(void);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* safety_refresh() — call every main loop iteration.
|
||||||
|
* Resets IWDG counter. If not called within WATCHDOG_TIMEOUT_MS,
|
||||||
|
* the MCU will reset (independent of software — cannot be disabled).
|
||||||
|
*/
|
||||||
|
void safety_refresh(void);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* safety_rc_alive() — returns true if RC receiver has sent a frame
|
||||||
|
* within RC_TIMEOUT_MS. Call from the balance loop.
|
||||||
|
*/
|
||||||
|
bool safety_rc_alive(uint32_t now);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* safety_alert_tilt_fault() — one-shot buzzer beep for tilt fault.
|
||||||
|
* Safe to call repeatedly; only fires once per fault.
|
||||||
|
*/
|
||||||
|
void safety_alert_tilt_fault(bool faulted);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* safety_arm_interlock() — returns true once arm button has been
|
||||||
|
* held for ARMING_HOLD_MS from the moment safety_arm_start() was called.
|
||||||
|
*/
|
||||||
|
void safety_arm_start(uint32_t now); /* Call when arm requested */
|
||||||
|
bool safety_arm_ready(uint32_t now); /* Poll until true, then arm */
|
||||||
|
void safety_arm_cancel(void); /* Cancel pending arm */
|
||||||
|
|
||||||
|
#endif /* SAFETY_H */
|
||||||
@ -5,10 +5,13 @@
|
|||||||
|
|
||||||
extern USBD_CDC_ItfTypeDef USBD_CDC_fops;
|
extern USBD_CDC_ItfTypeDef USBD_CDC_fops;
|
||||||
|
|
||||||
// Send data over USB CDC
|
/* Send data over USB CDC */
|
||||||
uint8_t CDC_Transmit(uint8_t *buf, uint16_t len);
|
uint8_t CDC_Transmit(uint8_t *buf, uint16_t len);
|
||||||
|
|
||||||
|
|
||||||
/* Betaflight-style DFU reboot check — call early in main() */
|
/* Betaflight-style DFU reboot check — call early in main() */
|
||||||
void checkForBootloader(void);
|
void checkForBootloader(void);
|
||||||
|
|
||||||
|
/* PID tuning command interface (written by USB IRQ, read by main loop) */
|
||||||
|
extern volatile uint8_t cdc_cmd_ready;
|
||||||
|
extern volatile char cdc_cmd_buf[32];
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@ -7,6 +7,14 @@ static volatile uint8_t cdc_port_open = 0; /* set when host asserts DTR */
|
|||||||
volatile uint8_t cdc_arm_request = 0; /* set by A command */
|
volatile uint8_t cdc_arm_request = 0; /* set by A command */
|
||||||
volatile uint8_t cdc_disarm_request = 0; /* set by D command */
|
volatile uint8_t cdc_disarm_request = 0; /* set by D command */
|
||||||
|
|
||||||
|
/*
|
||||||
|
* PID tuning command buffer.
|
||||||
|
* CDC_Receive (USB IRQ) copies multi-char commands here.
|
||||||
|
* Main loop polls cdc_cmd_ready, parses, and clears.
|
||||||
|
* Commands: P<kp> I<ki> D<kd> T<setpoint> M<max_speed> ?
|
||||||
|
*/
|
||||||
|
volatile uint8_t cdc_cmd_ready = 0;
|
||||||
|
volatile char cdc_cmd_buf[32];
|
||||||
|
|
||||||
static uint8_t UserRxBuffer[256];
|
static uint8_t UserRxBuffer[256];
|
||||||
static uint8_t UserTxBuffer[256];
|
static uint8_t UserTxBuffer[256];
|
||||||
@ -101,18 +109,29 @@ static int8_t CDC_Control(uint8_t cmd, uint8_t *pbuf, uint16_t length) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static int8_t CDC_Receive(uint8_t *buf, uint32_t *len) {
|
static int8_t CDC_Receive(uint8_t *buf, uint32_t *len) {
|
||||||
if (*len >= 1 && buf[0] == 'S') {
|
if (*len < 1) goto done;
|
||||||
cdc_streaming = !cdc_streaming; /* Toggle streaming */
|
|
||||||
|
switch (buf[0]) {
|
||||||
|
case 'S': cdc_streaming = !cdc_streaming; break;
|
||||||
|
case 'A': cdc_arm_request = 1; break;
|
||||||
|
case 'D': cdc_disarm_request = 1; break;
|
||||||
|
case 'R': request_bootloader(); break; /* never returns */
|
||||||
|
|
||||||
|
/*
|
||||||
|
* PID tuning: P<kp> I<ki> D<kd> T<setpoint> M<max_speed> ?
|
||||||
|
* Copy to cmd buffer; main loop parses float (avoids sscanf in IRQ).
|
||||||
|
*/
|
||||||
|
case 'P': case 'I': case 'K': case 'T': case 'M': case '?': {
|
||||||
|
uint32_t copy_len = *len < 31 ? *len : 31;
|
||||||
|
for (uint32_t i = 0; i < copy_len; i++) cdc_cmd_buf[i] = (char)buf[i];
|
||||||
|
cdc_cmd_buf[copy_len] = '\0';
|
||||||
|
cdc_cmd_ready = 1;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
if (*len >= 1 && buf[0] == 'A') {
|
default: break;
|
||||||
cdc_arm_request = 1; /* Arm balance — processed in main loop */
|
|
||||||
}
|
|
||||||
if (*len >= 1 && buf[0] == 'D') {
|
|
||||||
cdc_disarm_request = 1; /* Disarm — processed in main loop */
|
|
||||||
}
|
|
||||||
if (*len >= 1 && buf[0] == 'R') {
|
|
||||||
request_bootloader(); /* Sets magic + resets — never returns */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
done:
|
||||||
USBD_CDC_SetRxBuffer(&hUsbDevice, UserRxBuffer);
|
USBD_CDC_SetRxBuffer(&hUsbDevice, UserRxBuffer);
|
||||||
USBD_CDC_ReceivePacket(&hUsbDevice);
|
USBD_CDC_ReceivePacket(&hUsbDevice);
|
||||||
return USBD_OK;
|
return USBD_OK;
|
||||||
|
|||||||
@ -2,13 +2,6 @@
|
|||||||
#include "config.h"
|
#include "config.h"
|
||||||
#include <math.h>
|
#include <math.h>
|
||||||
|
|
||||||
/* MPU6000 raw to physical units (default ±2g, ±250°/s) */
|
|
||||||
#define ACCEL_SCALE (1.0f / 16384.0f) /* LSB to g (±2g range) */
|
|
||||||
#define GYRO_SCALE (1.0f / 131.0f) /* LSB to °/s (±250°/s range) */
|
|
||||||
|
|
||||||
/* Complementary filter coefficient — 0.98 trusts gyro, 0.02 corrects with accel */
|
|
||||||
#define COMP_ALPHA 0.98f
|
|
||||||
|
|
||||||
void balance_init(balance_t *b) {
|
void balance_init(balance_t *b) {
|
||||||
b->state = BALANCE_DISARMED;
|
b->state = BALANCE_DISARMED;
|
||||||
b->pitch_deg = 0.0f;
|
b->pitch_deg = 0.0f;
|
||||||
@ -28,30 +21,12 @@ void balance_init(balance_t *b) {
|
|||||||
b->max_speed = MAX_SPEED_LIMIT;
|
b->max_speed = MAX_SPEED_LIMIT;
|
||||||
}
|
}
|
||||||
|
|
||||||
void balance_update(balance_t *b, const icm42688_data_t *imu, float dt) {
|
void balance_update(balance_t *b, const IMUData *imu, float dt) {
|
||||||
if (dt <= 0.0f || dt > 0.1f) return; /* Sanity check dt */
|
if (dt <= 0.0f || dt > 0.1f) return; /* Sanity check dt */
|
||||||
|
|
||||||
/* Convert raw IMU to physical units */
|
/* Consume fused angle from mpu6000 complementary filter */
|
||||||
float ax = imu->ax * ACCEL_SCALE;
|
b->pitch_deg = imu->pitch;
|
||||||
float ay = imu->ay * ACCEL_SCALE;
|
b->pitch_rate = imu->pitch_rate;
|
||||||
float az = imu->az * ACCEL_SCALE;
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Gyro axis for pitch depends on mounting orientation.
|
|
||||||
* MPU6000 on MAMBA F722S with CW270 alignment:
|
|
||||||
* Pitch rate = gx axis (adjust sign if needed during testing)
|
|
||||||
*/
|
|
||||||
float gyro_pitch_rate = imu->gx * GYRO_SCALE;
|
|
||||||
b->pitch_rate = gyro_pitch_rate;
|
|
||||||
|
|
||||||
/* Accelerometer pitch angle (atan2 of forward/down axes)
|
|
||||||
* With CW270: pitch = atan2(ax, az)
|
|
||||||
* Adjust axes based on actual mounting during testing */
|
|
||||||
float accel_pitch = atan2f(ax, az) * (180.0f / 3.14159265f);
|
|
||||||
|
|
||||||
/* Complementary filter */
|
|
||||||
b->pitch_deg = COMP_ALPHA * (b->pitch_deg + gyro_pitch_rate * dt)
|
|
||||||
+ (1.0f - COMP_ALPHA) * accel_pitch;
|
|
||||||
|
|
||||||
/* Safety: tilt cutoff */
|
/* Safety: tilt cutoff */
|
||||||
if (b->state == BALANCE_ARMED) {
|
if (b->state == BALANCE_ARMED) {
|
||||||
@ -82,8 +57,8 @@ void balance_update(balance_t *b, const icm42688_data_t *imu, float dt) {
|
|||||||
if (b->integral < -PID_INTEGRAL_MAX) b->integral = -PID_INTEGRAL_MAX;
|
if (b->integral < -PID_INTEGRAL_MAX) b->integral = -PID_INTEGRAL_MAX;
|
||||||
float i_term = b->ki * b->integral;
|
float i_term = b->ki * b->integral;
|
||||||
|
|
||||||
/* Derivative (on measurement to avoid setpoint kick) */
|
/* Derivative on measurement (avoids setpoint kick) */
|
||||||
float d_term = b->kd * (-gyro_pitch_rate); /* Use gyro directly, cleaner than differencing */
|
float d_term = b->kd * (-b->pitch_rate);
|
||||||
|
|
||||||
/* Sum and clamp */
|
/* Sum and clamp */
|
||||||
float output = p_term + i_term + d_term;
|
float output = p_term + i_term + d_term;
|
||||||
|
|||||||
99
src/main.c
99
src/main.c
@ -3,12 +3,15 @@
|
|||||||
#include "usbd_cdc.h"
|
#include "usbd_cdc.h"
|
||||||
#include "usbd_cdc_if.h"
|
#include "usbd_cdc_if.h"
|
||||||
#include "usbd_desc.h"
|
#include "usbd_desc.h"
|
||||||
#include "icm42688.h"
|
#include "mpu6000.h"
|
||||||
#include "bmp280.h"
|
#include "bmp280.h"
|
||||||
#include "balance.h"
|
#include "balance.h"
|
||||||
#include "hoverboard.h"
|
#include "hoverboard.h"
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
#include "status.h"
|
#include "status.h"
|
||||||
|
#include "safety.h"
|
||||||
|
#include "crsf.h"
|
||||||
|
#include <math.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
|
|
||||||
@ -16,6 +19,40 @@ extern volatile uint8_t cdc_streaming; /* set by S command in CDC RX */
|
|||||||
extern volatile uint8_t cdc_arm_request; /* set by A command */
|
extern volatile uint8_t cdc_arm_request; /* set by A command */
|
||||||
extern volatile uint8_t cdc_disarm_request; /* set by D command */
|
extern volatile uint8_t cdc_disarm_request; /* set by D command */
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Apply a PID tuning command string from the USB terminal.
|
||||||
|
* Format: P<kp> I<ki> D<kd> T<setpoint_deg> M<max_speed> ?
|
||||||
|
* Returns a short ack string written into `reply` (max reply_sz bytes).
|
||||||
|
*/
|
||||||
|
static void apply_pid_cmd(balance_t *b, const char *cmd, char *reply, int reply_sz) {
|
||||||
|
float val = 0.0f;
|
||||||
|
/* Simple ASCII float parse — avoids sscanf in hot path */
|
||||||
|
if (cmd[0] == '?') {
|
||||||
|
snprintf(reply, reply_sz,
|
||||||
|
"{\"kp\":%d,\"ki\":%d,\"kd\":%d,\"sp\":%d,\"ms\":%d}\n",
|
||||||
|
(int)(b->kp * 1000), (int)(b->ki * 1000), (int)(b->kd * 1000),
|
||||||
|
(int)(b->setpoint * 10), (int)b->max_speed);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
/* Parse float after the command letter */
|
||||||
|
if (sscanf(cmd + 1, "%f", &val) != 1) {
|
||||||
|
snprintf(reply, reply_sz, "{\"err\":\"bad_fmt\"}\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
switch (cmd[0]) {
|
||||||
|
case 'P': if (val >= 0.0f && val <= 500.0f) b->kp = val; break;
|
||||||
|
case 'I': if (val >= 0.0f && val <= 50.0f) b->ki = val; break;
|
||||||
|
case 'K': /* alias for D */ /* fall through */
|
||||||
|
case 'D': if (val >= 0.0f && val <= 50.0f) b->kd = val; break;
|
||||||
|
case 'T': if (val >= -20.0f && val <= 20.0f) b->setpoint = val; break;
|
||||||
|
case 'M': if (val >= 0.0f && val <= 1000.0f) b->max_speed = (int16_t)val; break;
|
||||||
|
}
|
||||||
|
snprintf(reply, reply_sz,
|
||||||
|
"{\"kp\":%d,\"ki\":%d,\"kd\":%d,\"sp\":%d,\"ms\":%d}\n",
|
||||||
|
(int)(b->kp * 1000), (int)(b->ki * 1000), (int)(b->kd * 1000),
|
||||||
|
(int)(b->setpoint * 10), (int)b->max_speed);
|
||||||
|
}
|
||||||
|
|
||||||
USBD_HandleTypeDef hUsbDevice;
|
USBD_HandleTypeDef hUsbDevice;
|
||||||
|
|
||||||
static void SystemClock_Config(void) {
|
static void SystemClock_Config(void) {
|
||||||
@ -67,8 +104,17 @@ int main(void) {
|
|||||||
status_init();
|
status_init();
|
||||||
HAL_Delay(3000); /* Wait for USB host to enumerate */
|
HAL_Delay(3000); /* Wait for USB host to enumerate */
|
||||||
|
|
||||||
/* Init IMU */
|
/*
|
||||||
int imu_ret = icm42688_init();
|
* IWDG starts after the USB enumeration delay — avoids spurious reset
|
||||||
|
* during the 3s startup hold. Once started, safety_refresh() MUST be
|
||||||
|
* called at least every WATCHDOG_TIMEOUT_MS (50ms) or MCU resets.
|
||||||
|
* IWDG cannot be stopped once started (hardware enforced).
|
||||||
|
*/
|
||||||
|
safety_init();
|
||||||
|
|
||||||
|
/* Init IMU (MPU6000 via SPI1, complementary filter in mpu6000.c) */
|
||||||
|
bool imu_ok = mpu6000_init();
|
||||||
|
int imu_ret = imu_ok ? 0 : -1;
|
||||||
|
|
||||||
/* Init hoverboard ESC UART */
|
/* Init hoverboard ESC UART */
|
||||||
hoverboard_init();
|
hoverboard_init();
|
||||||
@ -80,27 +126,56 @@ int main(void) {
|
|||||||
char buf[256];
|
char buf[256];
|
||||||
int len;
|
int len;
|
||||||
|
|
||||||
icm42688_data_t imu;
|
IMUData imu;
|
||||||
uint32_t send_tick = 0;
|
uint32_t send_tick = 0;
|
||||||
uint32_t balance_tick = 0;
|
uint32_t balance_tick = 0;
|
||||||
uint32_t esc_tick = 0;
|
uint32_t esc_tick = 0;
|
||||||
const float dt = 1.0f / PID_LOOP_HZ; /* 1ms at 1kHz */
|
const float dt = 1.0f / PID_LOOP_HZ; /* 1ms at 1kHz */
|
||||||
|
|
||||||
while (1) {
|
while (1) {
|
||||||
if (imu_ret == 0) icm42688_read(&imu);
|
if (imu_ret == 0) mpu6000_read(&imu);
|
||||||
|
|
||||||
uint32_t now = HAL_GetTick();
|
uint32_t now = HAL_GetTick();
|
||||||
|
|
||||||
/* Handle arm/disarm requests from USB */
|
/* Feed hardware watchdog — must happen every WATCHDOG_TIMEOUT_MS */
|
||||||
|
safety_refresh();
|
||||||
|
|
||||||
|
/* RC timeout: disarm if signal lost while armed */
|
||||||
|
if (bal.state == BALANCE_ARMED && !safety_rc_alive(now)) {
|
||||||
|
/* USB-only mode: no RC receiver expected yet — skip RC timeout.
|
||||||
|
* Uncomment when CRSF is wired: balance_disarm(&bal); */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tilt fault buzzer alert (one-shot on fault edge) */
|
||||||
|
safety_alert_tilt_fault(bal.state == BALANCE_TILT_FAULT);
|
||||||
|
|
||||||
|
/* Handle arm/disarm requests from USB with arm-hold interlock */
|
||||||
if (cdc_arm_request) {
|
if (cdc_arm_request) {
|
||||||
cdc_arm_request = 0;
|
cdc_arm_request = 0;
|
||||||
|
/* Start the arm hold timer — motors don't enable until ARMING_HOLD_MS */
|
||||||
|
if (bal.state == BALANCE_DISARMED && fabsf(bal.pitch_deg) < 10.0f) {
|
||||||
|
safety_arm_start(now);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* Complete arming once hold timer expires */
|
||||||
|
if (safety_arm_ready(now) && bal.state == BALANCE_DISARMED) {
|
||||||
|
safety_arm_cancel();
|
||||||
balance_arm(&bal);
|
balance_arm(&bal);
|
||||||
}
|
}
|
||||||
if (cdc_disarm_request) {
|
if (cdc_disarm_request) {
|
||||||
cdc_disarm_request = 0;
|
cdc_disarm_request = 0;
|
||||||
|
safety_arm_cancel();
|
||||||
balance_disarm(&bal);
|
balance_disarm(&bal);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Handle PID tuning commands from USB (P/I/D/T/M/?) */
|
||||||
|
if (cdc_cmd_ready) {
|
||||||
|
cdc_cmd_ready = 0;
|
||||||
|
char reply[96];
|
||||||
|
apply_pid_cmd(&bal, (const char *)cdc_cmd_buf, reply, sizeof(reply));
|
||||||
|
CDC_Transmit((uint8_t *)reply, strlen(reply));
|
||||||
|
}
|
||||||
|
|
||||||
/* Balance PID at 1kHz */
|
/* Balance PID at 1kHz */
|
||||||
if (imu_ret == 0 && now - balance_tick >= 1) {
|
if (imu_ret == 0 && now - balance_tick >= 1) {
|
||||||
balance_tick = now;
|
balance_tick = now;
|
||||||
@ -122,12 +197,14 @@ int main(void) {
|
|||||||
if (cdc_streaming && now - send_tick >= 20) {
|
if (cdc_streaming && now - send_tick >= 20) {
|
||||||
send_tick = now;
|
send_tick = now;
|
||||||
if (imu_ret == 0) {
|
if (imu_ret == 0) {
|
||||||
|
float err = bal.setpoint - bal.pitch_deg;
|
||||||
len = snprintf(buf, sizeof(buf),
|
len = snprintf(buf, sizeof(buf),
|
||||||
"{\"ax\":%d,\"ay\":%d,\"az\":%d,\"gx\":%d,\"gy\":%d,\"gz\":%d,"
|
"{\"p\":%d,\"r\":%d,\"e\":%d,\"ig\":%d,\"m\":%d,\"s\":%d}\n",
|
||||||
"\"p\":%d,\"m\":%d,\"s\":%d}\n",
|
(int)(bal.pitch_deg * 10), /* pitch degrees x10 */
|
||||||
imu.ax, imu.ay, imu.az, imu.gx, imu.gy, imu.gz,
|
(int)(imu.pitch_rate * 10), /* pitch rate °/s x10 */
|
||||||
(int)(bal.pitch_deg * 10), /* pitch x10 for precision without %f */
|
(int)(err * 10), /* PID error x10 */
|
||||||
(int)bal.motor_cmd,
|
(int)(bal.integral * 10), /* integral x10 (windup monitor) */
|
||||||
|
(int)bal.motor_cmd, /* ESC command -1000..+1000 */
|
||||||
(int)bal.state);
|
(int)bal.state);
|
||||||
} else {
|
} else {
|
||||||
len = snprintf(buf, sizeof(buf), "{\"err\":%d}\n", imu_ret);
|
len = snprintf(buf, sizeof(buf), "{\"err\":%d}\n", imu_ret);
|
||||||
|
|||||||
85
src/mpu6000.c
Normal file
85
src/mpu6000.c
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
/*
|
||||||
|
* mpu6000.c — IMU Sensor Fusion for MPU6000
|
||||||
|
*
|
||||||
|
* Wraps the icm42688 SPI driver (which auto-detects MPU6000) and applies
|
||||||
|
* a complementary filter to produce a stable pitch estimate.
|
||||||
|
*
|
||||||
|
* Hardware: MAMBA F722S, MPU6000 on SPI1, CW270 alignment
|
||||||
|
* Config: Gyro ±2000°/s (init_mpu6000 sets FS_SEL=3)
|
||||||
|
* Accel ±16g (init_mpu6000 sets AFS_SEL=3)
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "mpu6000.h"
|
||||||
|
#include "icm42688.h"
|
||||||
|
#include "stm32f7xx_hal.h"
|
||||||
|
#include <math.h>
|
||||||
|
|
||||||
|
/* Scale factors matching init_mpu6000() config in icm42688.c */
|
||||||
|
#define GYRO_SCALE (1.0f / 16.384f) /* LSB to °/s — ±2000°/s range */
|
||||||
|
#define ACCEL_SCALE (1.0f / 2048.0f) /* LSB to g — ±16g range */
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Complementary filter coefficient.
|
||||||
|
* 0.98 = trust gyro integration, 0.02 = accel correction for drift.
|
||||||
|
* Tune higher (0.99) for noisier environments.
|
||||||
|
*/
|
||||||
|
#define COMP_ALPHA 0.98f
|
||||||
|
|
||||||
|
/* Filter state */
|
||||||
|
static float s_pitch = 0.0f;
|
||||||
|
static uint32_t s_last_tick = 0;
|
||||||
|
|
||||||
|
bool mpu6000_init(void) {
|
||||||
|
int ret = icm42688_init();
|
||||||
|
if (ret == 0) {
|
||||||
|
s_pitch = 0.0f;
|
||||||
|
s_last_tick = HAL_GetTick();
|
||||||
|
}
|
||||||
|
return (ret == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void mpu6000_read(IMUData *data) {
|
||||||
|
icm42688_data_t raw;
|
||||||
|
icm42688_read(&raw);
|
||||||
|
|
||||||
|
/* Compute dt from wall clock — robust to loop jitter */
|
||||||
|
uint32_t now = HAL_GetTick();
|
||||||
|
uint32_t elapsed_ms = now - s_last_tick;
|
||||||
|
if (elapsed_ms == 0) elapsed_ms = 1; /* min 1ms to avoid divide-by-zero */
|
||||||
|
if (elapsed_ms > 100) elapsed_ms = 100; /* clamp: don't integrate stale data */
|
||||||
|
float dt = elapsed_ms * 0.001f;
|
||||||
|
s_last_tick = now;
|
||||||
|
|
||||||
|
/* Convert raw to physical units */
|
||||||
|
float ax = raw.ax * ACCEL_SCALE; /* g */
|
||||||
|
float az = raw.az * ACCEL_SCALE; /* g */
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Gyro pitch axis with CW270 alignment: pitch rate = gx.
|
||||||
|
* Sign may need inverting depending on mounting orientation —
|
||||||
|
* verify during bench test (positive nose-up should be positive).
|
||||||
|
*/
|
||||||
|
float gyro_pitch_rate = raw.gx * GYRO_SCALE; /* °/s */
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Accel pitch angle (degrees).
|
||||||
|
* CW270 alignment: pitch = atan2(ax, az).
|
||||||
|
* Valid while ax² + az² ≈ 1g (i.e., low linear acceleration).
|
||||||
|
*/
|
||||||
|
float accel_pitch = atan2f(ax, az) * (180.0f / 3.14159265358979f);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Complementary filter:
|
||||||
|
* pitch = α * (pitch + ω*dt) + (1−α) * accel_pitch
|
||||||
|
*
|
||||||
|
* Gyro integration tracks fast dynamics; accel correction
|
||||||
|
* prevents long-term drift.
|
||||||
|
*/
|
||||||
|
s_pitch = COMP_ALPHA * (s_pitch + gyro_pitch_rate * dt)
|
||||||
|
+ (1.0f - COMP_ALPHA) * accel_pitch;
|
||||||
|
|
||||||
|
data->pitch = s_pitch;
|
||||||
|
data->pitch_rate = gyro_pitch_rate;
|
||||||
|
data->accel_x = ax;
|
||||||
|
data->accel_z = az;
|
||||||
|
}
|
||||||
77
src/safety.c
Normal file
77
src/safety.c
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
/*
|
||||||
|
* safety.c — SaltyLab Safety Systems
|
||||||
|
*
|
||||||
|
* IWDG: 40kHz LSI, prescaler 32 → tick = 0.8ms.
|
||||||
|
* WATCHDOG_TIMEOUT_MS from config.h (default 50ms → reload = 62).
|
||||||
|
* Formula: reload = (timeout_ms / (prescaler / 40000)) - 1
|
||||||
|
* reload = (50 * 40000 / 32) - 1 = 62499 → clamp to 4095 (12-bit max)
|
||||||
|
* At PSC=256: tick = 6.4ms, reload = ceil(50/6.4)-1 = 7 → ~57.6ms
|
||||||
|
* Using PSC=32: tick = 0.8ms, reload = ceil(50/0.8)-1 = 62 → 50ms ✓
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "safety.h"
|
||||||
|
#include "config.h"
|
||||||
|
#include "crsf.h"
|
||||||
|
#include "stm32f7xx_hal.h"
|
||||||
|
|
||||||
|
/* IWDG prescaler 32 → LSI(40kHz)/32 = 1250 ticks/sec → 0.8ms/tick */
|
||||||
|
#define IWDG_PRESCALER IWDG_PRESCALER_32
|
||||||
|
#define IWDG_TICK_MS (32.0f / 40.0f) /* 0.8ms per tick */
|
||||||
|
#define IWDG_RELOAD ((uint32_t)(WATCHDOG_TIMEOUT_MS / IWDG_TICK_MS))
|
||||||
|
|
||||||
|
#if IWDG_RELOAD > 4095
|
||||||
|
# error "WATCHDOG_TIMEOUT_MS too large for IWDG_PRESCALER_32 — increase prescaler"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
static IWDG_HandleTypeDef hiwdg;
|
||||||
|
|
||||||
|
/* Arm interlock */
|
||||||
|
static uint32_t s_arm_start_ms = 0;
|
||||||
|
static bool s_arm_pending = false;
|
||||||
|
|
||||||
|
/* Tilt fault alert state — edge-detect to fire buzzer once */
|
||||||
|
static bool s_was_faulted = false;
|
||||||
|
|
||||||
|
void safety_init(void) {
|
||||||
|
hiwdg.Instance = IWDG;
|
||||||
|
hiwdg.Init.Prescaler = IWDG_PRESCALER;
|
||||||
|
hiwdg.Init.Reload = IWDG_RELOAD;
|
||||||
|
hiwdg.Init.Window = IWDG_WINDOW_DISABLE;
|
||||||
|
HAL_IWDG_Init(&hiwdg); /* Starts watchdog immediately */
|
||||||
|
}
|
||||||
|
|
||||||
|
void safety_refresh(void) {
|
||||||
|
HAL_IWDG_Refresh(&hiwdg);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool safety_rc_alive(uint32_t now) {
|
||||||
|
/* If crsf_state has never received a frame, last_rx_ms == 0 */
|
||||||
|
if (crsf_state.last_rx_ms == 0) return false;
|
||||||
|
return (now - crsf_state.last_rx_ms) < RC_TIMEOUT_MS;
|
||||||
|
}
|
||||||
|
|
||||||
|
void safety_alert_tilt_fault(bool faulted) {
|
||||||
|
if (faulted && !s_was_faulted) {
|
||||||
|
/* Rising edge: single buzzer burst to alert rider */
|
||||||
|
HAL_GPIO_WritePin(BEEPER_PORT, BEEPER_PIN, GPIO_PIN_SET);
|
||||||
|
HAL_Delay(200);
|
||||||
|
HAL_GPIO_WritePin(BEEPER_PORT, BEEPER_PIN, GPIO_PIN_RESET);
|
||||||
|
}
|
||||||
|
s_was_faulted = faulted;
|
||||||
|
}
|
||||||
|
|
||||||
|
void safety_arm_start(uint32_t now) {
|
||||||
|
if (!s_arm_pending) {
|
||||||
|
s_arm_start_ms = now;
|
||||||
|
s_arm_pending = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool safety_arm_ready(uint32_t now) {
|
||||||
|
if (!s_arm_pending) return false;
|
||||||
|
return (now - s_arm_start_ms) >= ARMING_HOLD_MS;
|
||||||
|
}
|
||||||
|
|
||||||
|
void safety_arm_cancel(void) {
|
||||||
|
s_arm_pending = false;
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user