Implements a speed-dependent PID gain scheduler that interpolates Kp/Ki/Kd across a configurable table of velocity breakpoints, replacing the fixed single-gain PID used previously. Changes: - include/pid_flash.h: add pid_sched_entry_t (16-byte entry), pid_sched_flash_t (128-byte record at 0x0807FF40), pid_flash_load_schedule(), pid_flash_save_all() (atomic single-sector erase for both schedule and single-PID records) - src/pid_flash.c: implement load_schedule and save_all; single erase covers both records at 0x0807FF40 (schedule) and 0x0807FFC0 (single PID) - include/pid_schedule.h: API header -- init, get_gains, apply, set/get table, flash_save, active_band_idx, get_default_table - src/pid_schedule.c: linear interpolation between sorted speed-band entries; integrator reset on band transition; default 3-band table (0/0.3/0.8 m/s) - include/jlink.h: add SCHED_GET (0x0C), SCHED_SET (0x0D), SCHED_SAVE (0x0E) commands; TLM_SCHED (0x85); jlink_tlm_sched_t; JLinkSchedSetBuf; sched_get_req, sched_save_req fields in JLinkState; include pid_flash.h - src/jlink.c: dispatch SCHED_GET/SET/SAVE; implement jlink_send_sched_telemetry, jlink_get_sched_set; add JLinkSchedSetBuf static buffer - test/test_pid_schedule.c: 48 unit tests -- all passing (gcc host build) Flash layout (sector 7): 0x0807FF40 pid_sched_flash_t (128 bytes) -- schedule 0x0807FFC0 pid_flash_t ( 64 bytes) -- single PID (existing) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
175 lines
5.1 KiB
C
175 lines
5.1 KiB
C
#include "pid_schedule.h"
|
|
#include "pid_flash.h"
|
|
#include <string.h>
|
|
#include <math.h> /* fabsf */
|
|
|
|
/* ---- Default 3-band table ---- */
|
|
static const pid_sched_entry_t k_default_table[3] = {
|
|
{ .speed_mps = 0.00f, .kp = 40.0f, .ki = 1.5f, .kd = 1.2f },
|
|
{ .speed_mps = 0.30f, .kp = 35.0f, .ki = 1.0f, .kd = 1.0f },
|
|
{ .speed_mps = 0.80f, .kp = 28.0f, .ki = 0.5f, .kd = 0.8f },
|
|
};
|
|
|
|
/* ---- Active table ---- */
|
|
static pid_sched_entry_t s_bands[PID_SCHED_MAX_BANDS];
|
|
static uint8_t s_num_bands = 0u;
|
|
static uint8_t s_active_band = 0u; /* lower-bracket index of last call */
|
|
static uint8_t s_prev_band = 0xFFu; /* sentinel: forces integrator reset on first apply */
|
|
|
|
/* ---- sort helper (insertion sort — table is small, ≤6 entries) ---- */
|
|
static void sort_bands(void)
|
|
{
|
|
for (uint8_t i = 1u; i < s_num_bands; i++) {
|
|
pid_sched_entry_t key = s_bands[i];
|
|
int8_t j = (int8_t)(i - 1u);
|
|
while (j >= 0 && s_bands[j].speed_mps > key.speed_mps) {
|
|
s_bands[j + 1] = s_bands[j];
|
|
j--;
|
|
}
|
|
s_bands[j + 1] = key;
|
|
}
|
|
}
|
|
|
|
/* ---- pid_schedule_init() ---- */
|
|
void pid_schedule_init(void)
|
|
{
|
|
pid_sched_entry_t tmp[PID_SCHED_MAX_BANDS];
|
|
uint8_t n = 0u;
|
|
|
|
if (pid_flash_load_schedule(tmp, &n)) {
|
|
/* Validate entries minimally */
|
|
bool ok = true;
|
|
for (uint8_t i = 0u; i < n; i++) {
|
|
if (tmp[i].kp < 0.0f || tmp[i].kp > 500.0f ||
|
|
tmp[i].ki < 0.0f || tmp[i].ki > 50.0f ||
|
|
tmp[i].kd < 0.0f || tmp[i].kd > 50.0f ||
|
|
tmp[i].speed_mps < 0.0f) {
|
|
ok = false;
|
|
break;
|
|
}
|
|
}
|
|
if (ok) {
|
|
memcpy(s_bands, tmp, n * sizeof(pid_sched_entry_t));
|
|
s_num_bands = n;
|
|
sort_bands();
|
|
s_active_band = 0u;
|
|
return;
|
|
}
|
|
}
|
|
|
|
/* Fall back to built-in default */
|
|
memcpy(s_bands, k_default_table, sizeof(k_default_table));
|
|
s_num_bands = 3u;
|
|
s_active_band = 0u;
|
|
s_prev_band = 0xFFu;
|
|
}
|
|
|
|
/* ---- pid_schedule_get_gains() ---- */
|
|
void pid_schedule_get_gains(float speed_mps, float *kp, float *ki, float *kd)
|
|
{
|
|
float spd = fabsf(speed_mps);
|
|
|
|
if (s_num_bands == 0u) {
|
|
*kp = k_default_table[0].kp;
|
|
*ki = k_default_table[0].ki;
|
|
*kd = k_default_table[0].kd;
|
|
return;
|
|
}
|
|
|
|
/* Clamp below first entry */
|
|
if (spd <= s_bands[0].speed_mps) {
|
|
s_active_band = 0u;
|
|
*kp = s_bands[0].kp;
|
|
*ki = s_bands[0].ki;
|
|
*kd = s_bands[0].kd;
|
|
return;
|
|
}
|
|
|
|
/* Clamp above last entry */
|
|
uint8_t last = s_num_bands - 1u;
|
|
if (spd >= s_bands[last].speed_mps) {
|
|
s_active_band = last;
|
|
*kp = s_bands[last].kp;
|
|
*ki = s_bands[last].ki;
|
|
*kd = s_bands[last].kd;
|
|
return;
|
|
}
|
|
|
|
/* Find bracket [i-1, i] where bands[i-1].speed <= spd < bands[i].speed */
|
|
uint8_t i = 1u;
|
|
while (i < s_num_bands && s_bands[i].speed_mps <= spd) i++;
|
|
/* Now bands[i-1].speed_mps <= spd < bands[i].speed_mps */
|
|
|
|
s_active_band = (uint8_t)(i - 1u);
|
|
|
|
float dv = s_bands[i].speed_mps - s_bands[i - 1u].speed_mps;
|
|
float t = (dv > 0.0f) ? (spd - s_bands[i - 1u].speed_mps) / dv : 0.0f;
|
|
|
|
*kp = s_bands[i - 1u].kp + t * (s_bands[i].kp - s_bands[i - 1u].kp);
|
|
*ki = s_bands[i - 1u].ki + t * (s_bands[i].ki - s_bands[i - 1u].ki);
|
|
*kd = s_bands[i - 1u].kd + t * (s_bands[i].kd - s_bands[i - 1u].kd);
|
|
}
|
|
|
|
/* ---- pid_schedule_apply() ---- */
|
|
void pid_schedule_apply(balance_t *b, float speed_mps)
|
|
{
|
|
float kp, ki, kd;
|
|
pid_schedule_get_gains(speed_mps, &kp, &ki, &kd);
|
|
|
|
b->kp = kp;
|
|
b->ki = ki;
|
|
b->kd = kd;
|
|
|
|
/* Reset integrator on band transition to prevent windup spike */
|
|
if (s_active_band != s_prev_band) {
|
|
b->integral = 0.0f;
|
|
s_prev_band = s_active_band;
|
|
}
|
|
}
|
|
|
|
/* ---- pid_schedule_set_table() ---- */
|
|
void pid_schedule_set_table(const pid_sched_entry_t *entries, uint8_t n)
|
|
{
|
|
if (n == 0u) n = 1u;
|
|
if (n > PID_SCHED_MAX_BANDS) n = PID_SCHED_MAX_BANDS;
|
|
|
|
memcpy(s_bands, entries, n * sizeof(pid_sched_entry_t));
|
|
s_num_bands = n;
|
|
s_active_band = 0u;
|
|
s_prev_band = 0xFFu;
|
|
sort_bands();
|
|
}
|
|
|
|
/* ---- pid_schedule_get_table() ---- */
|
|
void pid_schedule_get_table(pid_sched_entry_t *out_entries, uint8_t *out_n)
|
|
{
|
|
memcpy(out_entries, s_bands, s_num_bands * sizeof(pid_sched_entry_t));
|
|
*out_n = s_num_bands;
|
|
}
|
|
|
|
/* ---- pid_schedule_get_num_bands() ---- */
|
|
uint8_t pid_schedule_get_num_bands(void)
|
|
{
|
|
return s_num_bands;
|
|
}
|
|
|
|
/* ---- pid_schedule_flash_save() ---- */
|
|
bool pid_schedule_flash_save(float kp_single, float ki_single, float kd_single)
|
|
{
|
|
return pid_flash_save_all(kp_single, ki_single, kd_single,
|
|
s_bands, s_num_bands);
|
|
}
|
|
|
|
/* ---- pid_schedule_active_band_idx() ---- */
|
|
uint8_t pid_schedule_active_band_idx(void)
|
|
{
|
|
return s_active_band;
|
|
}
|
|
|
|
/* ---- pid_schedule_get_default_table() ---- */
|
|
void pid_schedule_get_default_table(pid_sched_entry_t *out_entries, uint8_t *out_n)
|
|
{
|
|
memcpy(out_entries, k_default_table, sizeof(k_default_table));
|
|
*out_n = 3u;
|
|
}
|