Files
mppt-testbench/code64/Core/Src/debug_protocol.c
grabowski e7a23a3c7e Add LVSolarBuck64 firmware and debug console with uv support
STM32G474RB firmware for solar buck converter with MPPT, CC control,
Vfly compensation, and adaptive deadtime. Includes Textual TUI debug
console for real-time telemetry, parameter tuning, and SQLite logging.

Added pyproject.toml for uv: `cd code64 && uv run debug-console`

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 16:38:23 +07:00

457 lines
16 KiB
C

/*
* debug_protocol.c
*
* Created on: Mar 5, 2026
* Author: janik
*/
#include "debug_protocol.h"
#include "main.h"
#include "cc_controller.h"
#include "mppt.h"
#include <string.h>
#include <stdio.h>
/* ---- External variables from main.c ---- */
extern volatile float vin, iin, vout, iout, vfly, etemp;
extern volatile int16_t last_tmp;
extern volatile int16_t vfly_correction;
extern uint16_t VREF;
extern float vfly_integral;
extern volatile float vfly_avg_debug;
extern float vfly_kp, vfly_ki;
extern uint16_t vfly_clamp;
extern uint16_t vfly_loop_counter_trigger;
extern volatile uint8_t vfly_active;
extern CCController cc;
extern volatile float cc_target;
extern volatile int cc_active;
extern float cc_gain;
extern float CC_MIN_STEP, CC_MAX_STEP;
extern uint16_t cc_loop_counter_trigger;
extern MPPTController mppt;
extern volatile int mppt_active;
extern uint16_t mppt_loop_counter_trigger;
extern float mppt_initial_iref;
extern float mppt_step;
extern float mppt_iref_min, mppt_iref_max;
extern float mppt_dv_threshold;
extern float mppt_deadband;
extern float vin_min_ctrl;
extern uint8_t dt_values[];
/* Raw ADC DMA buffers */
extern uint16_t DMA1BUF1; /* adc4: vbat */
extern uint16_t DMA1BUF2[3]; /* adc1: etemp, vin, iin */
extern uint16_t DMA1BUF3[2]; /* adc2: vfly, iout */
extern uint16_t DMA1BUF4; /* adc5: itemp */
extern COMP_HandleTypeDef hcomp1, hcomp3, hcomp4;
extern FMAC_HandleTypeDef hfmac;
/* ---- CRC8 table (poly 0x07) ---- */
static const uint8_t crc8_table[256] = {
0x00,0x07,0x0E,0x09,0x1C,0x1B,0x12,0x15,0x38,0x3F,0x36,0x31,0x24,0x23,0x2A,0x2D,
0x70,0x77,0x7E,0x79,0x6C,0x6B,0x62,0x65,0x48,0x4F,0x46,0x41,0x54,0x53,0x5A,0x5D,
0xE0,0xE7,0xEE,0xE9,0xFC,0xFB,0xF2,0xF5,0xD8,0xDF,0xD6,0xD1,0xC4,0xC3,0xCA,0xCD,
0x90,0x97,0x9E,0x99,0x8C,0x8B,0x82,0x85,0xA8,0xAF,0xA6,0xA1,0xB4,0xB3,0xBA,0xBD,
0xC7,0xC0,0xC9,0xCE,0xDB,0xDC,0xD5,0xD2,0xFF,0xF8,0xF1,0xF6,0xE3,0xE4,0xED,0xEA,
0xB7,0xB0,0xB9,0xBE,0xAB,0xAC,0xA5,0xA2,0x8F,0x88,0x81,0x86,0x93,0x94,0x9D,0x9A,
0x27,0x20,0x29,0x2E,0x3B,0x3C,0x35,0x32,0x1F,0x18,0x11,0x16,0x03,0x04,0x0D,0x0A,
0x57,0x50,0x59,0x5E,0x4B,0x4C,0x45,0x42,0x6F,0x68,0x61,0x66,0x73,0x74,0x7D,0x7A,
0x89,0x8E,0x87,0x80,0x95,0x92,0x9B,0x9C,0xB1,0xB6,0xBF,0xB8,0xAD,0xAA,0xA3,0xA4,
0xF9,0xFE,0xF7,0xF0,0xE5,0xE2,0xEB,0xEC,0xC1,0xC6,0xCF,0xC8,0xDD,0xDA,0xD3,0xD4,
0x69,0x6E,0x67,0x60,0x75,0x72,0x7B,0x7C,0x51,0x56,0x5F,0x58,0x4D,0x4A,0x43,0x44,
0x19,0x1E,0x17,0x10,0x05,0x02,0x0B,0x0C,0x21,0x26,0x2F,0x28,0x3D,0x3A,0x33,0x34,
0x4E,0x49,0x40,0x47,0x52,0x55,0x5C,0x5B,0x76,0x71,0x78,0x7F,0x6A,0x6D,0x64,0x63,
0x3E,0x39,0x30,0x37,0x22,0x25,0x2C,0x2B,0x06,0x01,0x08,0x0F,0x1A,0x1D,0x14,0x13,
0xAE,0xA9,0xA0,0xA7,0xB2,0xB5,0xBC,0xBB,0x96,0x91,0x98,0x9F,0x8A,0x8D,0x84,0x83,
0xDE,0xD9,0xD0,0xD7,0xC2,0xC5,0xCC,0xCB,0xE6,0xE1,0xE8,0xEF,0xFA,0xFD,0xF4,0xF3,
};
static uint8_t crc8(const uint8_t *data, uint16_t len)
{
uint8_t crc = 0x00;
for (uint16_t i = 0; i < len; i++)
crc = crc8_table[crc ^ data[i]];
return crc;
}
/* ---- Parameter table ---- */
#define PARAM_COUNT 27
static ParamEntry param_table[PARAM_COUNT];
static void param_table_init(void)
{
int i = 0;
/* Vfly params */
param_table[i++] = (ParamEntry){PID_VFLY_KP, PTYPE_FLOAT, &vfly_kp, -10.0f, 10.0f};
param_table[i++] = (ParamEntry){PID_VFLY_KI, PTYPE_FLOAT, &vfly_ki, -10.0f, 10.0f};
param_table[i++] = (ParamEntry){PID_VFLY_CLAMP, PTYPE_UINT16, &vfly_clamp, 0, 10000};
param_table[i++] = (ParamEntry){PID_VFLY_LOOP_COUNTER_TRIGGER, PTYPE_UINT16, &vfly_loop_counter_trigger, 1, 10000};
param_table[i++] = (ParamEntry){PID_VFLY_ACTIVE, PTYPE_UINT8, (void*)&vfly_active, 0, 1};
/* CC params */
param_table[i++] = (ParamEntry){PID_CC_TARGET, PTYPE_FLOAT, (void*)&cc_target, 0, 60000};
param_table[i++] = (ParamEntry){PID_CC_GAIN, PTYPE_FLOAT, &cc_gain, -1.0f, 1.0f};
param_table[i++] = (ParamEntry){PID_CC_MIN_STEP, PTYPE_FLOAT, &CC_MIN_STEP, -1000, 0};
param_table[i++] = (ParamEntry){PID_CC_MAX_STEP, PTYPE_FLOAT, &CC_MAX_STEP, 0, 1000};
param_table[i++] = (ParamEntry){PID_CC_LOOP_COUNTER_TRIGGER, PTYPE_UINT16, &cc_loop_counter_trigger, 1, 10000};
param_table[i++] = (ParamEntry){PID_CC_ACTIVE, PTYPE_INT32, (void*)&cc_active, 0, 1};
param_table[i++] = (ParamEntry){PID_VREF, PTYPE_UINT16, &VREF, 3100, 3700};
/* MPPT params */
param_table[i++] = (ParamEntry){PID_MPPT_STEP, PTYPE_FLOAT, &mppt_step, 0, 10000};
param_table[i++] = (ParamEntry){PID_MPPT_IREF_MIN, PTYPE_FLOAT, &mppt_iref_min, 0, 60000};
param_table[i++] = (ParamEntry){PID_MPPT_IREF_MAX, PTYPE_FLOAT, &mppt_iref_max, 0, 60000};
param_table[i++] = (ParamEntry){PID_MPPT_DV_THRESHOLD, PTYPE_FLOAT, &mppt_dv_threshold, 0, 10000};
param_table[i++] = (ParamEntry){PID_MPPT_LOOP_COUNTER_TRIGGER, PTYPE_UINT16, &mppt_loop_counter_trigger, 1, 10000};
param_table[i++] = (ParamEntry){PID_MPPT_ACTIVE, PTYPE_INT32, (void*)&mppt_active, 0, 1};
param_table[i++] = (ParamEntry){PID_MPPT_INITIAL_IREF, PTYPE_FLOAT, &mppt_initial_iref, 0, 60000};
param_table[i++] = (ParamEntry){PID_MPPT_DEADBAND, PTYPE_FLOAT, &mppt_deadband, 0, 1.0f};
/* Global */
param_table[i++] = (ParamEntry){PID_VIN_MIN_CTRL, PTYPE_FLOAT, &vin_min_ctrl, 0, 90000};
/* Deadtime segments */
param_table[i++] = (ParamEntry){PID_DT_SEG0, PTYPE_UINT8, &dt_values[0], DT_HARD_MIN, 200};
param_table[i++] = (ParamEntry){PID_DT_SEG1, PTYPE_UINT8, &dt_values[1], DT_HARD_MIN, 200};
param_table[i++] = (ParamEntry){PID_DT_SEG2, PTYPE_UINT8, &dt_values[2], DT_HARD_MIN, 200};
param_table[i++] = (ParamEntry){PID_DT_SEG3, PTYPE_UINT8, &dt_values[3], DT_HARD_MIN, 200};
param_table[i++] = (ParamEntry){PID_DT_SEG4, PTYPE_UINT8, &dt_values[4], DT_HARD_MIN, 200};
param_table[i++] = (ParamEntry){PID_DT_SEG5, PTYPE_UINT8, &dt_values[5], DT_HARD_MIN, 200};
}
/* ---- Protocol context ---- */
ProtoCtx proto;
/* ---- Internal helpers ---- */
static void start_rx(void)
{
HAL_UART_Receive_IT(proto.huart, &proto.rx_byte, 1);
}
static void uart_recover(void)
{
/* Clear error flags and reset UART state so TX/RX work again */
__HAL_UART_CLEAR_FLAG(proto.huart, UART_CLEAR_OREF | UART_CLEAR_NEF | UART_CLEAR_PEF | UART_CLEAR_FEF);
proto.huart->gState = HAL_UART_STATE_READY;
proto.huart->RxState = HAL_UART_STATE_READY;
proto.huart->ErrorCode = HAL_UART_ERROR_NONE;
/* Re-arm RX */
start_rx();
}
static void send_frame(uint8_t cmd, const uint8_t *payload, uint8_t len)
{
/* Pick the inactive buffer */
uint8_t buf_idx = proto.tx_active ^ 1;
uint8_t *buf = proto.tx_buf[buf_idx];
uint16_t total = PROTO_HEADER_SIZE + len + 1;
buf[0] = PROTO_SYNC_BYTE;
buf[1] = cmd;
buf[2] = len;
if (len > 0)
memcpy(&buf[3], payload, len);
buf[3 + len] = crc8(buf, 3 + len);
proto.tx_active = buf_idx;
if (HAL_UART_Transmit(proto.huart, buf, total, 5) != HAL_OK)
{
proto.uart_errors++;
uart_recover();
}
}
static ParamEntry* find_param(uint8_t id)
{
for (int i = 0; i < PARAM_COUNT; i++)
{
if (param_table[i].id == id)
return &param_table[i];
}
return NULL;
}
static void apply_param_write(const ParamWritePayload *pw)
{
ParamEntry *p = find_param(pw->param_id);
if (!p) return;
/* Extract value and clamp */
float fval;
switch (p->type)
{
case PTYPE_FLOAT:
fval = pw->value.f;
if (fval < p->min_val) fval = p->min_val;
if (fval > p->max_val) fval = p->max_val;
*(float*)p->ptr = fval;
break;
case PTYPE_UINT16:
fval = (float)pw->value.u16;
if (fval < p->min_val) fval = p->min_val;
if (fval > p->max_val) fval = p->max_val;
*(uint16_t*)p->ptr = (uint16_t)fval;
break;
case PTYPE_UINT8:
fval = (float)pw->value.u8;
if (fval < p->min_val) fval = p->min_val;
if (fval > p->max_val) fval = p->max_val;
*(uint8_t*)p->ptr = (uint8_t)fval;
break;
case PTYPE_INT32:
fval = (float)pw->value.i32;
if (fval < p->min_val) fval = p->min_val;
if (fval > p->max_val) fval = p->max_val;
*(int32_t*)p->ptr = (int32_t)fval;
break;
}
/* Side effects: VREF update -> ADC3 offset + CC output */
if (pw->param_id == PID_VREF)
{
ADC3->OFR1 = (ADC3->OFR1 & ~0xFFFU) | (VREF & 0xFFF);
cc.output_f = (float)VREF;
}
/* Side effects for CC params: sync struct fields */
else if (pw->param_id == PID_CC_GAIN)
cc.gain = cc_gain;
else if (pw->param_id == PID_CC_MIN_STEP)
cc.min_step = CC_MIN_STEP;
else if (pw->param_id == PID_CC_MAX_STEP)
cc.max_step = CC_MAX_STEP;
/* Side effects for MPPT params: sync struct fields */
else if (pw->param_id == PID_MPPT_STEP)
mppt.step = mppt_step;
else if (pw->param_id == PID_MPPT_IREF_MIN)
mppt.iref_min = mppt_iref_min;
else if (pw->param_id == PID_MPPT_IREF_MAX)
mppt.iref_max = mppt_iref_max;
else if (pw->param_id == PID_MPPT_DV_THRESHOLD)
mppt.dv_min = mppt_dv_threshold;
else if (pw->param_id == PID_MPPT_DEADBAND)
mppt.deadband = mppt_deadband;
else if (pw->param_id == PID_MPPT_ACTIVE && mppt_active)
cc_active = 1;
/* Send ACK: echo back the write payload */
send_frame(CMD_PARAM_WRITE_ACK, (const uint8_t*)pw, sizeof(ParamWritePayload));
}
static void send_param_value(const ParamEntry *p)
{
ParamWritePayload pv;
pv.param_id = p->id;
pv.param_type = p->type;
pv._pad[0] = 0; pv._pad[1] = 0;
pv.value.u32 = 0;
switch (p->type)
{
case PTYPE_FLOAT: pv.value.f = *(float*)p->ptr; break;
case PTYPE_UINT16: pv.value.u16 = *(uint16_t*)p->ptr; break;
case PTYPE_UINT8: pv.value.u8 = *(uint8_t*)p->ptr; break;
case PTYPE_INT32: pv.value.i32 = *(int32_t*)p->ptr; break;
}
send_frame(CMD_PARAM_VALUE, (const uint8_t*)&pv, sizeof(ParamWritePayload));
}
static void send_all_params(void)
{
for (int i = 0; i < PARAM_COUNT; i++)
send_param_value(&param_table[i]);
}
static void process_rx_frame(uint8_t cmd, const uint8_t *payload, uint8_t len)
{
switch (cmd)
{
case CMD_PARAM_WRITE:
if (len >= sizeof(ParamWritePayload))
apply_param_write((const ParamWritePayload*)payload);
break;
case CMD_PARAM_READ_ALL:
send_all_params();
break;
case CMD_PING:
send_frame(CMD_PONG, NULL, 0);
break;
default:
break;
}
}
/* ---- Public API ---- */
void Proto_Init(UART_HandleTypeDef *huart)
{
memset(&proto, 0, sizeof(proto));
proto.huart = huart;
proto.rx_state = RX_WAIT_SYNC;
param_table_init();
start_rx();
}
void Proto_SendTelemetry(void)
{
TelemetryPayload t;
t.vin = vin;
t.vout = vout;
t.iin = iin;
t.iout = iout;
t.vfly = vfly;
t.etemp = etemp;
t.last_tmp = last_tmp;
t.VREF = VREF;
t.vfly_correction = vfly_correction;
t._pad0 = 0;
t.vfly_integral = vfly_integral;
t.vfly_avg_debug = vfly_avg_debug;
t.cc_output_f = cc.output_f;
t.mppt_iref = mppt.iref;
t.mppt_last_vin = mppt.last_vin;
t.mppt_last_iin = mppt.last_iin;
t.p_in = vin * (-iin) / 1e6f; /* mV * mA → W */
t.p_out = vout * iout / 1e6f; /* mV * mA → W */
t.seq = proto.seq++;
t._pad1[0] = 0; t._pad1[1] = 0; t._pad1[2] = 0;
send_frame(CMD_TELEMETRY, (const uint8_t*)&t, sizeof(TelemetryPayload));
}
void Proto_SendError(const char *msg)
{
uint8_t len = 0;
while (msg[len] && len < PROTO_MAX_PAYLOAD) len++;
send_frame(CMD_ERROR_MSG, (const uint8_t*)msg, len);
}
void Proto_SendDiagDump(const char *reason)
{
char buf[128];
/* Line 1: reason */
Proto_SendError(reason);
/* Line 2: converted analog values (integer-only — no float printf with nano.specs) */
snprintf(buf, sizeof(buf), "DIAG V:%lu/%lu I:%d/%lu Vfly:%lu T:%d.%u",
(unsigned long)(vin > 0 ? (uint32_t)vin : 0),
(unsigned long)(vout > 0 ? (uint32_t)vout : 0),
(int)iin,
(unsigned long)(iout > 0 ? (uint32_t)iout : 0),
(unsigned long)(vfly > 0 ? (uint32_t)vfly : 0),
(int)etemp, ((unsigned)(etemp > 0 ? etemp : -etemp) % 10));
Proto_SendError(buf);
/* Line 3: raw ADC counts */
snprintf(buf, sizeof(buf), "RAW adc1[%u,%u,%u] adc2[%u,%u] adc4[%u] adc5[%u]",
DMA1BUF2[0], DMA1BUF2[1], DMA1BUF2[2],
DMA1BUF3[0], DMA1BUF3[1],
DMA1BUF1, DMA1BUF4);
Proto_SendError(buf);
/* Line 4: controller state */
snprintf(buf, sizeof(buf), "CTRL VREF:%u last_tmp:%d vfly_corr:%d cc_out:%d mppt_iref:%d",
VREF, (int)last_tmp, (int)vfly_correction,
(int)cc.output_f, (int)mppt.iref);
Proto_SendError(buf);
/* Line 5: HRTIM fault status + comparator outputs */
uint32_t hrtim_isr = HRTIM1->sCommonRegs.ISR;
uint32_t hrtim_oenr = HRTIM1->sCommonRegs.OENR;
uint32_t comp_out = 0;
if (HAL_COMP_GetOutputLevel(&hcomp1) == COMP_OUTPUT_LEVEL_HIGH) comp_out |= 1;
if (HAL_COMP_GetOutputLevel(&hcomp3) == COMP_OUTPUT_LEVEL_HIGH) comp_out |= 2;
if (HAL_COMP_GetOutputLevel(&hcomp4) == COMP_OUTPUT_LEVEL_HIGH) comp_out |= 4;
snprintf(buf, sizeof(buf), "HW HRTIM_ISR:%08lX OENR:%08lX COMP:%lu FMAC_SR:%08lX",
hrtim_isr, hrtim_oenr, comp_out, FMAC->SR);
Proto_SendError(buf);
/* Line 6: HRTIM duty + master compare */
snprintf(buf, sizeof(buf), "HRTIM TE_CMP1:%lu TF_CMP1:%lu MCMP1:%lu",
HRTIM1->sTimerxRegs[HRTIM_TIMERINDEX_TIMER_E].CMP1xR,
HRTIM1->sTimerxRegs[HRTIM_TIMERINDEX_TIMER_F].CMP1xR,
HRTIM1->sMasterRegs.MCMP1R);
Proto_SendError(buf);
/* Line 7: active flags + ADC3 offset */
snprintf(buf, sizeof(buf), "FLAGS cc_act:%d mppt_act:%d vfly_act:%u ADC3_OFR1:%08lX",
cc_active, mppt_active, (unsigned)vfly_active, ADC3->OFR1);
Proto_SendError(buf);
}
/* ---- HAL Callbacks ---- */
void Proto_TxCpltCallback(UART_HandleTypeDef *huart)
{
if (huart == proto.huart)
proto.tx_busy = 0;
}
void Proto_ErrorCallback(UART_HandleTypeDef *huart)
{
if (huart != proto.huart)
return;
proto.uart_errors++;
uart_recover();
}
void Proto_RxCpltCallback(UART_HandleTypeDef *huart)
{
if (huart != proto.huart)
return;
uint8_t b = proto.rx_byte;
switch (proto.rx_state)
{
case RX_WAIT_SYNC:
if (b == PROTO_SYNC_BYTE)
{
proto.rx_buf[0] = b;
proto.rx_state = RX_WAIT_CMD;
}
break;
case RX_WAIT_CMD:
proto.rx_cmd = b;
proto.rx_buf[1] = b;
proto.rx_state = RX_WAIT_LEN;
break;
case RX_WAIT_LEN:
proto.rx_len = b;
proto.rx_buf[2] = b;
proto.rx_idx = 0;
if (b == 0)
proto.rx_state = RX_WAIT_CRC;
else if (b > PROTO_MAX_PAYLOAD)
proto.rx_state = RX_WAIT_SYNC; /* invalid, reset */
else
proto.rx_state = RX_WAIT_PAYLOAD;
break;
case RX_WAIT_PAYLOAD:
proto.rx_buf[PROTO_HEADER_SIZE + proto.rx_idx] = b;
proto.rx_idx++;
if (proto.rx_idx >= proto.rx_len)
proto.rx_state = RX_WAIT_CRC;
break;
case RX_WAIT_CRC:
{
uint8_t expected = crc8(proto.rx_buf, PROTO_HEADER_SIZE + proto.rx_len);
if (b == expected)
process_rx_frame(proto.rx_cmd, &proto.rx_buf[PROTO_HEADER_SIZE], proto.rx_len);
proto.rx_state = RX_WAIT_SYNC;
break;
}
}
/* Re-arm RX */
start_rx();
}