Adds STM32F7 STOP-mode power management with <10ms wake latency: - power_mgmt.c: state machine (ACTIVE→SLEEP_PENDING→SLEEPING→WAKING), 30s idle timeout (PM_IDLE_TIMEOUT_MS), 3s LED fade before STOP, gate SPI3/I2S3+SPI2+USART6+UART5 on sleep (clock-only, state preserved), EXTI1(PA1/CRSF)+EXTI7(PB7/JLink)+EXTI4(PC4/IMU) wake sources, PLL restore after STOP (PLLM=8/N=216/P=2 → 216MHz), uwTick save/restore - Peripheral gating: I2S3, SPI2(OSD), USART6, UART5 disabled during STOP; SPI1(IMU), UART4(CRSF), USART1(JLink), I2C1 remain active as wake sources - Sleep LED: triangle-wave pulse (2s period) on LED1 during SLEEP_PENDING, software PWM in main loop (1-bit, pm_pwm_phase vs brightness) - IWDG: fed just before WFI; <10ms wake << 50ms WATCHDOG_TIMEOUT_MS - JLink: JLINK_CMD_SLEEP=0x09, JLINK_TLM_POWER=0x81 (11-byte power frame at 1Hz: power_state, est_total_ma, est_audio_ma, est_osd_ma, idle_ms) - main.c: power_mgmt_init(), activity() on CRSF/JLink/armed, tick() when disarmed, sleep_req handler, LED PWM, JLINK_TLM_POWER telemetry - config.h: PM_* constants, PM_CURRENT_*_MA estimates, PM_TLM_HZ - test_power_mgmt.py: 72 tests passing (state machine, LED, gating, current estimates, JLink protocol, wake latency, hardware constants) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
323 lines
10 KiB
C
323 lines
10 KiB
C
#include "jlink.h"
|
||
#include "audio.h"
|
||
#include "config.h"
|
||
#include "stm32f7xx_hal.h"
|
||
#include <string.h>
|
||
|
||
/* ---- DMA circular RX buffer ---- */
|
||
#define JLINK_RX_BUF_LEN 128u /* must be power-of-2 */
|
||
static uint8_t s_rx_buf[JLINK_RX_BUF_LEN];
|
||
static uint32_t s_rx_tail = 0; /* consumer index (byte already processed) */
|
||
|
||
/* ---- HAL handles ---- */
|
||
static UART_HandleTypeDef s_uart;
|
||
static DMA_HandleTypeDef s_dma_rx;
|
||
|
||
/* ---- Volatile state ---- */
|
||
volatile JLinkState jlink_state;
|
||
|
||
/* ---- CRC16-XModem (poly 0x1021, init 0x0000) ---- */
|
||
static uint16_t crc16_xmodem(const uint8_t *data, uint16_t len)
|
||
{
|
||
uint16_t crc = 0x0000u;
|
||
for (uint16_t i = 0; i < len; i++) {
|
||
crc ^= (uint16_t)data[i] << 8;
|
||
for (uint8_t b = 0; b < 8; b++) {
|
||
if (crc & 0x8000u)
|
||
crc = (crc << 1) ^ 0x1021u;
|
||
else
|
||
crc <<= 1;
|
||
}
|
||
}
|
||
return crc;
|
||
}
|
||
|
||
/* ---- jlink_init() ---- */
|
||
void jlink_init(void)
|
||
{
|
||
/* GPIO: PB6=TX AF7 (USART1_TX), PB7=RX AF7 (USART1_RX) */
|
||
__HAL_RCC_GPIOB_CLK_ENABLE();
|
||
GPIO_InitTypeDef gpio = {0};
|
||
gpio.Pin = GPIO_PIN_6 | GPIO_PIN_7;
|
||
gpio.Mode = GPIO_MODE_AF_PP;
|
||
gpio.Pull = GPIO_PULLUP;
|
||
gpio.Speed = GPIO_SPEED_FREQ_HIGH;
|
||
gpio.Alternate = GPIO_AF7_USART1;
|
||
HAL_GPIO_Init(GPIOB, &gpio);
|
||
|
||
/* DMA2 Stream2 Channel4 — USART1_RX circular */
|
||
__HAL_RCC_DMA2_CLK_ENABLE();
|
||
s_dma_rx.Instance = DMA2_Stream2;
|
||
s_dma_rx.Init.Channel = DMA_CHANNEL_4;
|
||
s_dma_rx.Init.Direction = DMA_PERIPH_TO_MEMORY;
|
||
s_dma_rx.Init.PeriphInc = DMA_PINC_DISABLE;
|
||
s_dma_rx.Init.MemInc = DMA_MINC_ENABLE;
|
||
s_dma_rx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
|
||
s_dma_rx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
|
||
s_dma_rx.Init.Mode = DMA_CIRCULAR;
|
||
s_dma_rx.Init.Priority = DMA_PRIORITY_MEDIUM;
|
||
s_dma_rx.Init.FIFOMode = DMA_FIFOMODE_DISABLE;
|
||
HAL_DMA_Init(&s_dma_rx);
|
||
__HAL_LINKDMA(&s_uart, hdmarx, s_dma_rx);
|
||
|
||
/* USART1 at JLINK_BAUD (921600) */
|
||
__HAL_RCC_USART1_CLK_ENABLE();
|
||
s_uart.Instance = USART1;
|
||
s_uart.Init.BaudRate = JLINK_BAUD;
|
||
s_uart.Init.WordLength = UART_WORDLENGTH_8B;
|
||
s_uart.Init.StopBits = UART_STOPBITS_1;
|
||
s_uart.Init.Parity = UART_PARITY_NONE;
|
||
s_uart.Init.Mode = UART_MODE_TX_RX;
|
||
s_uart.Init.HwFlowCtl = UART_HWCONTROL_NONE;
|
||
s_uart.Init.OverSampling = UART_OVERSAMPLING_16;
|
||
HAL_UART_Init(&s_uart);
|
||
|
||
/* Enable USART1 IDLE interrupt for circular buffer draining */
|
||
__HAL_UART_ENABLE_IT(&s_uart, UART_IT_IDLE);
|
||
HAL_NVIC_SetPriority(USART1_IRQn, 6, 0);
|
||
HAL_NVIC_EnableIRQ(USART1_IRQn);
|
||
|
||
/* DMA2_Stream2 IRQ (for error handling) */
|
||
HAL_NVIC_SetPriority(DMA2_Stream2_IRQn, 7, 0);
|
||
HAL_NVIC_EnableIRQ(DMA2_Stream2_IRQn);
|
||
|
||
/* Start circular DMA RX — never stops */
|
||
HAL_UART_Receive_DMA(&s_uart, s_rx_buf, JLINK_RX_BUF_LEN);
|
||
|
||
memset((void *)&jlink_state, 0, sizeof(jlink_state));
|
||
s_rx_tail = 0;
|
||
}
|
||
|
||
/* ---- IRQ handlers ---- */
|
||
void USART1_IRQHandler(void)
|
||
{
|
||
/* Clear IDLE flag by reading SR then DR */
|
||
if (__HAL_UART_GET_FLAG(&s_uart, UART_FLAG_IDLE)) {
|
||
__HAL_UART_CLEAR_IDLEFLAG(&s_uart);
|
||
/* jlink_process() drains the buffer from main loop — no work here */
|
||
}
|
||
HAL_UART_IRQHandler(&s_uart);
|
||
}
|
||
|
||
void DMA2_Stream2_IRQHandler(void)
|
||
{
|
||
HAL_DMA_IRQHandler(&s_dma_rx);
|
||
}
|
||
|
||
/* ---- jlink_is_active() ---- */
|
||
bool jlink_is_active(uint32_t now_ms)
|
||
{
|
||
if (jlink_state.last_rx_ms == 0u) return false;
|
||
return (now_ms - jlink_state.last_rx_ms) < JLINK_HB_TIMEOUT_MS;
|
||
}
|
||
|
||
/* ---- Frame dispatch ---- */
|
||
static void dispatch(const uint8_t *payload, uint8_t cmd, uint8_t plen)
|
||
{
|
||
/* Update heartbeat timestamp on every valid frame */
|
||
jlink_state.last_rx_ms = HAL_GetTick();
|
||
|
||
switch (cmd) {
|
||
|
||
case JLINK_CMD_HEARTBEAT:
|
||
/* Heartbeat only — no payload action needed */
|
||
break;
|
||
|
||
case JLINK_CMD_DRIVE:
|
||
if (plen == 4u) {
|
||
int16_t spd, str;
|
||
memcpy(&spd, payload, 2);
|
||
memcpy(&str, payload + 2, 2);
|
||
/* Clamp to ±1000 */
|
||
if (spd > 1000) spd = 1000;
|
||
if (spd < -1000) spd = -1000;
|
||
if (str > 1000) str = 1000;
|
||
if (str < -1000) str = -1000;
|
||
jlink_state.speed = spd;
|
||
jlink_state.steer = str;
|
||
}
|
||
break;
|
||
|
||
case JLINK_CMD_ARM:
|
||
jlink_state.arm_req = 1u;
|
||
break;
|
||
|
||
case JLINK_CMD_DISARM:
|
||
jlink_state.disarm_req = 1u;
|
||
break;
|
||
|
||
case JLINK_CMD_PID_SET:
|
||
if (plen == 12u) {
|
||
float kp, ki, kd;
|
||
memcpy(&kp, payload, 4);
|
||
memcpy(&ki, payload + 4, 4);
|
||
memcpy(&kd, payload + 8, 4);
|
||
/* Sanity bounds — same as USB CDC PID handler in main.c */
|
||
if (kp >= 0.0f && kp <= 500.0f) jlink_state.pid_kp = kp;
|
||
if (ki >= 0.0f && ki <= 50.0f) jlink_state.pid_ki = ki;
|
||
if (kd >= 0.0f && kd <= 50.0f) jlink_state.pid_kd = kd;
|
||
jlink_state.pid_updated = 1u;
|
||
}
|
||
break;
|
||
|
||
case JLINK_CMD_DFU_ENTER:
|
||
/* Payload-less; main loop checks armed state before calling ota_enter_dfu() */
|
||
jlink_state.dfu_req = 1u;
|
||
break;
|
||
|
||
case JLINK_CMD_ESTOP:
|
||
jlink_state.estop_req = 1u;
|
||
break;
|
||
|
||
case JLINK_CMD_AUDIO:
|
||
/* Payload: int16 PCM samples, little-endian, 1..126 samples (2..252 bytes) */
|
||
if (plen >= 2u && (plen & 1u) == 0u) {
|
||
audio_write_pcm((const int16_t *)payload, plen / 2u);
|
||
}
|
||
break;
|
||
|
||
case JLINK_CMD_SLEEP:
|
||
/* Payload-less; main loop calls power_mgmt_request_sleep() */
|
||
jlink_state.sleep_req = 1u;
|
||
break;
|
||
|
||
default:
|
||
break;
|
||
}
|
||
}
|
||
|
||
/* ---- jlink_process() — call from main loop every tick ---- */
|
||
/*
|
||
* Parser state machine.
|
||
* Frame: [STX][LEN][CMD][PAYLOAD 0..LEN-1][CRC_hi][CRC_lo][ETX]
|
||
* LEN = count of CMD + PAYLOAD bytes (1..253).
|
||
* CRC16-XModem over CMD+PAYLOAD.
|
||
* Maximum payload = 253 - 1 = 252 bytes (LEN field is 1 byte, max 0xFF=255,
|
||
* but we cap at 64 for safety).
|
||
*/
|
||
#define JLINK_MAX_PAYLOAD 252u /* enlarged for AUDIO chunks (126 × int16) */
|
||
|
||
typedef enum {
|
||
PS_WAIT_STX = 0,
|
||
PS_WAIT_LEN,
|
||
PS_WAIT_DATA, /* receiving CMD + PAYLOAD (len bytes total) */
|
||
PS_WAIT_CRC_HI,
|
||
PS_WAIT_CRC_LO,
|
||
PS_WAIT_ETX,
|
||
} ParseState;
|
||
|
||
void jlink_process(void)
|
||
{
|
||
static ParseState s_state = PS_WAIT_STX;
|
||
static uint8_t s_len = 0; /* expected CMD+PAYLOAD length */
|
||
static uint8_t s_count = 0; /* bytes received so far in PS_WAIT_DATA */
|
||
static uint8_t s_frame[JLINK_MAX_PAYLOAD + 1u]; /* [0]=CMD, [1..]=PAYLOAD */
|
||
static uint8_t s_crc_hi = 0;
|
||
|
||
/* Compute how many bytes the DMA has written since last drain */
|
||
uint32_t head = JLINK_RX_BUF_LEN - __HAL_DMA_GET_COUNTER(&s_dma_rx);
|
||
uint32_t bytes = (head - s_rx_tail) & (JLINK_RX_BUF_LEN - 1u);
|
||
|
||
for (uint32_t i = 0; i < bytes; i++) {
|
||
uint8_t b = s_rx_buf[s_rx_tail];
|
||
s_rx_tail = (s_rx_tail + 1u) & (JLINK_RX_BUF_LEN - 1u);
|
||
|
||
switch (s_state) {
|
||
case PS_WAIT_STX:
|
||
if (b == JLINK_STX) s_state = PS_WAIT_LEN;
|
||
break;
|
||
|
||
case PS_WAIT_LEN:
|
||
if (b == 0u || b > JLINK_MAX_PAYLOAD + 1u) {
|
||
/* Invalid length — resync */
|
||
s_state = PS_WAIT_STX;
|
||
} else {
|
||
s_len = b;
|
||
s_count = 0;
|
||
s_state = PS_WAIT_DATA;
|
||
}
|
||
break;
|
||
|
||
case PS_WAIT_DATA:
|
||
s_frame[s_count++] = b;
|
||
if (s_count == s_len) s_state = PS_WAIT_CRC_HI;
|
||
break;
|
||
|
||
case PS_WAIT_CRC_HI:
|
||
s_crc_hi = b;
|
||
s_state = PS_WAIT_CRC_LO;
|
||
break;
|
||
|
||
case PS_WAIT_CRC_LO: {
|
||
uint16_t rx_crc = ((uint16_t)s_crc_hi << 8) | b;
|
||
uint16_t calc_crc = crc16_xmodem(s_frame, s_len);
|
||
if (rx_crc == calc_crc)
|
||
s_state = PS_WAIT_ETX;
|
||
else
|
||
s_state = PS_WAIT_STX; /* CRC mismatch — drop */
|
||
break;
|
||
}
|
||
|
||
case PS_WAIT_ETX:
|
||
if (b == JLINK_ETX) {
|
||
/* Valid frame: s_frame[0]=CMD, s_frame[1..s_len-1]=PAYLOAD */
|
||
dispatch(s_frame + 1, s_frame[0], s_len - 1u);
|
||
}
|
||
/* Either way, go back to idle (resync on bad ETX) */
|
||
s_state = PS_WAIT_STX;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
/* ---- jlink_send_telemetry() ---- */
|
||
void jlink_send_telemetry(const jlink_tlm_status_t *status)
|
||
{
|
||
/*
|
||
* Frame: [STX][LEN][0x80][20 bytes STATUS][CRC_hi][CRC_lo][ETX]
|
||
* LEN = 1 (CMD) + 20 (payload) = 21
|
||
* Total frame length = 1+1+1+20+2+1 = 26 bytes
|
||
* At 921600 baud (10 bits/byte): 26×10/921600 ≈ 0.28ms — safe to block.
|
||
*/
|
||
static uint8_t frame[26];
|
||
const uint8_t plen = (uint8_t)sizeof(jlink_tlm_status_t); /* 20 */
|
||
const uint8_t len = 1u + plen; /* 21 */
|
||
|
||
frame[0] = JLINK_STX;
|
||
frame[1] = len;
|
||
frame[2] = JLINK_TLM_STATUS;
|
||
memcpy(&frame[3], status, plen);
|
||
|
||
uint16_t crc = crc16_xmodem(&frame[2], len); /* over CMD + PAYLOAD */
|
||
frame[3 + plen] = (uint8_t)(crc >> 8);
|
||
frame[3 + plen + 1] = (uint8_t)(crc & 0xFFu);
|
||
frame[3 + plen + 2] = JLINK_ETX;
|
||
|
||
HAL_UART_Transmit(&s_uart, frame, sizeof(frame), 5u);
|
||
}
|
||
|
||
/* ---- jlink_send_power_telemetry() ---- */
|
||
void jlink_send_power_telemetry(const jlink_tlm_power_t *power)
|
||
{
|
||
/*
|
||
* Frame: [STX][LEN][0x81][11 bytes POWER][CRC_hi][CRC_lo][ETX]
|
||
* LEN = 1 (CMD) + 11 (payload) = 12; total = 17 bytes
|
||
* At 921600 baud: 17×10/921600 ≈ 0.18 ms — safe to block.
|
||
*/
|
||
static uint8_t frame[17];
|
||
const uint8_t plen = (uint8_t)sizeof(jlink_tlm_power_t); /* 11 */
|
||
const uint8_t len = 1u + plen; /* 12 */
|
||
|
||
frame[0] = JLINK_STX;
|
||
frame[1] = len;
|
||
frame[2] = JLINK_TLM_POWER;
|
||
memcpy(&frame[3], power, plen);
|
||
|
||
uint16_t crc = crc16_xmodem(&frame[2], len);
|
||
frame[3 + plen] = (uint8_t)(crc >> 8);
|
||
frame[3 + plen + 1] = (uint8_t)(crc & 0xFFu);
|
||
frame[3 + plen + 2] = JLINK_ETX;
|
||
|
||
HAL_UART_Transmit(&s_uart, frame, sizeof(frame), 5u);
|
||
}
|