saltylab-firmware/src/pid_flash.c
sl-controls 2b06161cb4 feat: Motor current monitoring and overload protection (Issue #584)
Adds ADC-based motor current sensing with configurable overload threshold,
soft PWM limiting, hard cutoff on sustained overload, and auto-recovery.

Changes:
- include/motor_current.h: MotorCurrentState enum (NORMAL/SOFT_LIMIT/COOLDOWN),
  thresholds (5A hard, 4A soft, 2s overload, 10s cooldown), full API
- src/motor_current.c: reads battery_adc_get_current_ma() each tick (reuses
  existing ADC3 IN13/PC3 DMA sampling); linear PWM scale in soft-limit zone
  (scale256 fixed-point); fault counter + one-tick fault_pending flag for
  main-loop fault log integration; telemetry at MOTOR_CURR_TLM_HZ (5 Hz)
- include/pid_flash.h: add pid_sched_entry_t (16 bytes), pid_sched_flash_t
  (128 bytes at 0x0807FF40), PID_SCHED_MAX_BANDS=6, pid_flash_load_schedule(),
  pid_flash_save_all() — fixes missing types needed by jlink.h (Issue #550)
- src/pid_flash.c: implement flash_write_words() helper, pid_flash_load_schedule(),
  pid_flash_save_all() — single sector-7 erase covers both schedule and PID records
- include/jlink.h: add JLINK_TLM_MOTOR_CURRENT (0x86), jlink_tlm_motor_current_t
  (8 bytes: current_ma, limit_pct, state, fault_count), jlink_send_motor_current_tlm()
- src/jlink.c: implement jlink_send_motor_current_tlm() (14-byte frame)

Motor overload state machine:
  MC_NORMAL     : current_ma < 4000 mA — full PWM authority
  MC_SOFT_LIMIT : 4000-5000 mA — linear reduction (0% at 4A → 100% at 5A)
  MC_COOLDOWN   : >5A sustained 2s → zero output for 10s then NORMAL

Main-loop integration:
  motor_current_tick(now_ms);
  if (motor_current_fault_pending()) fault_log_append(FAULT_MOTOR_OVERCURRENT);
  cmd = motor_current_apply_limit(balance_pid_output());
  motor_current_send_tlm(now_ms);

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-14 12:25:29 -04:00

177 lines
5.2 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include "pid_flash.h"
#include "stm32f7xx_hal.h"
#include <string.h>
bool pid_flash_load(float *kp, float *ki, float *kd)
{
const pid_flash_t *p = (const pid_flash_t *)PID_FLASH_STORE_ADDR;
if (p->magic != PID_FLASH_MAGIC) return false;
/* Basic sanity bounds — same as JLINK_CMD_PID_SET handler */
if (p->kp < 0.0f || p->kp > 500.0f) return false;
if (p->ki < 0.0f || p->ki > 50.0f) return false;
if (p->kd < 0.0f || p->kd > 50.0f) return false;
*kp = p->kp;
*ki = p->ki;
*kd = p->kd;
return true;
}
bool pid_flash_save(float kp, float ki, float kd)
{
HAL_StatusTypeDef rc;
/* Unlock flash */
rc = HAL_FLASH_Unlock();
if (rc != HAL_OK) return false;
/* Erase sector 7 */
FLASH_EraseInitTypeDef erase = {
.TypeErase = FLASH_TYPEERASE_SECTORS,
.Sector = PID_FLASH_SECTOR,
.NbSectors = 1,
.VoltageRange = PID_FLASH_SECTOR_VOLTAGE,
};
uint32_t sector_error = 0;
rc = HAL_FLASHEx_Erase(&erase, &sector_error);
if (rc != HAL_OK || sector_error != 0xFFFFFFFFUL) {
HAL_FLASH_Lock();
return false;
}
/* Build record */
pid_flash_t rec;
memset(&rec, 0xFF, sizeof(rec));
rec.magic = PID_FLASH_MAGIC;
rec.kp = kp;
rec.ki = ki;
rec.kd = kd;
/* Write 64 bytes as 16 × 32-bit words */
const uint32_t *src = (const uint32_t *)&rec;
uint32_t addr = PID_FLASH_STORE_ADDR;
for (uint8_t i = 0; i < sizeof(rec) / 4u; i++) {
rc = HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, addr, src[i]);
if (rc != HAL_OK) {
HAL_FLASH_Lock();
return false;
}
addr += 4u;
}
HAL_FLASH_Lock();
/* Verify readback */
const pid_flash_t *stored = (const pid_flash_t *)PID_FLASH_STORE_ADDR;
return (stored->magic == PID_FLASH_MAGIC &&
stored->kp == kp &&
stored->ki == ki &&
stored->kd == kd);
}
/* ---- Helper: write arbitrary bytes as 32-bit words ---- */
/*
* Writes 'len' bytes from 'src' to flash at 'addr'.
* len must be a multiple of 4. Flash must already be unlocked.
* Returns HAL_OK on success, or first failure status.
*/
static HAL_StatusTypeDef flash_write_words(uint32_t addr,
const void *src,
uint32_t len)
{
const uint32_t *p = (const uint32_t *)src;
HAL_StatusTypeDef rc = HAL_OK;
for (uint32_t i = 0; i < len / 4u; i++) {
rc = HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, addr, p[i]);
if (rc != HAL_OK) return rc;
addr += 4u;
}
return HAL_OK;
}
/* ---- pid_flash_load_schedule() ---- */
bool pid_flash_load_schedule(pid_sched_entry_t *out_entries, uint8_t *out_n)
{
const pid_sched_flash_t *p = (const pid_sched_flash_t *)PID_SCHED_FLASH_ADDR;
if (p->magic != PID_SCHED_MAGIC) return false;
if (p->num_bands == 0u || p->num_bands > PID_SCHED_MAX_BANDS) return false;
*out_n = p->num_bands;
for (uint8_t i = 0; i < p->num_bands; i++) {
out_entries[i] = p->bands[i];
}
return true;
}
/* ---- pid_flash_save_all() ---- */
bool pid_flash_save_all(float kp_single, float ki_single, float kd_single,
const pid_sched_entry_t *entries, uint8_t num_bands)
{
if (num_bands == 0u || num_bands > PID_SCHED_MAX_BANDS) return false;
HAL_StatusTypeDef rc;
rc = HAL_FLASH_Unlock();
if (rc != HAL_OK) return false;
/* Single erase of sector 7 covers both records */
FLASH_EraseInitTypeDef erase = {
.TypeErase = FLASH_TYPEERASE_SECTORS,
.Sector = PID_FLASH_SECTOR,
.NbSectors = 1,
.VoltageRange = PID_FLASH_SECTOR_VOLTAGE,
};
uint32_t sector_error = 0;
rc = HAL_FLASHEx_Erase(&erase, &sector_error);
if (rc != HAL_OK || sector_error != 0xFFFFFFFFUL) {
HAL_FLASH_Lock();
return false;
}
/* Build and write schedule record at PID_SCHED_FLASH_ADDR */
pid_sched_flash_t srec;
memset(&srec, 0xFF, sizeof(srec));
srec.magic = PID_SCHED_MAGIC;
srec.num_bands = num_bands;
srec.flags = 0u;
for (uint8_t i = 0; i < num_bands; i++) {
srec.bands[i] = entries[i];
}
rc = flash_write_words(PID_SCHED_FLASH_ADDR, &srec, sizeof(srec));
if (rc != HAL_OK) {
HAL_FLASH_Lock();
return false;
}
/* Build and write single-PID record at PID_FLASH_STORE_ADDR */
pid_flash_t prec;
memset(&prec, 0xFF, sizeof(prec));
prec.magic = PID_FLASH_MAGIC;
prec.kp = kp_single;
prec.ki = ki_single;
prec.kd = kd_single;
rc = flash_write_words(PID_FLASH_STORE_ADDR, &prec, sizeof(prec));
if (rc != HAL_OK) {
HAL_FLASH_Lock();
return false;
}
HAL_FLASH_Lock();
/* Verify both records */
const pid_sched_flash_t *sv = (const pid_sched_flash_t *)PID_SCHED_FLASH_ADDR;
const pid_flash_t *pv = (const pid_flash_t *)PID_FLASH_STORE_ADDR;
return (sv->magic == PID_SCHED_MAGIC &&
sv->num_bands == num_bands &&
pv->magic == PID_FLASH_MAGIC &&
pv->kp == kp_single &&
pv->ki == ki_single &&
pv->kd == kd_single);
}