mothercup/Boot/src/boot_cli.c
evan.liu 3d6567819a CLI 新增 pinset 管脚高低电平命令 (Boot V1.00.07 / APP V1.00.31)
- pinset <pxN> [0|1]: 设置 GPIO 输出高低 / 读当前电平 (pa0-6/pb0-13)
- Boot/src/boot_cli.c + mcm-ddc-ble/src/cli_core.c 各加一份同名命令
- 对 SWD(PA4/PA5) / 控制台(PB6/PB7) 重配前打印 warning
- Boot V1.00.06 -> V1.00.07; APP V1.00.30 -> V1.00.31 (0x0001001F)
- docs/开发日志.md: 新增 §59
2026-09-12 09:58:55 +08:00

789 lines
22 KiB
C

/**
* @file boot_cli.c
* @brief Bootloader download-mode CLI: bare-metal polled line editor
* (echo, backspace, TAB command-name completion, UP/DOWN history)
* + command table, and the UART OTA binary frame mode.
*
* Wire-compatible with the application's UART CLI so that
* tools/ble_ota_update.py works unchanged against the bootloader:
* - prompt "caiic->" (the script's PROMPT marker)
* - "devinfo" prints "cur bank: APP<n>" (the boot's OTA target is the
* OTHER bank, exactly like the app's "write the inactive bank" rule)
* - "ota" prints the "[ota] binary mode ON" go-ahead marker, then the
* stream carries 0xCA OTA frames (engine: shared app_ota.c)
*
* No libc printf: cli_printf() below is a minimal formatter (%% %c %s %d
* %u %x %X, width, '-'/'0' pads, 'l' ignored - all integers are 32-bit).
*/
#include "cli_core.h" /* bootloader shim (Boot/src) */
#include "boot_usart.h"
#include "boot_version.h"
#include "app_bootset.h"
#include "app_ota.h"
#include "dfu_layout.h"
#include "boot_crc32.h"
#include "n32wb03x.h"
#include <string.h>
#include <stddef.h>
#include <stdarg.h>
#define CLI_TX_BUF_SIZE 96
#define CLI_LINE_SIZE 80
#define CLI_MAX_ARGS 8
#define OTA_IDLE_EXIT_MS 3000u /* binary mode fallback exit */
#define CLI_KEY_TAB 0x09 /* TAB: command-name completion */
#define CLI_KEY_ESC 0x1B /* ESC: arrow-key escape sequence start */
#define CLI_HISTORY_SIZE 8 /* number of remembered command lines */
/* ------------------------------------------------------------------ */
/* Output (cli_core.h shim implementation) */
/* ------------------------------------------------------------------ */
void cli_write(const char* str)
{
boot_usart_write((const uint8_t*)str, (uint16_t)strlen(str));
}
/** Format v into out (NUL-terminated), least-significant first reversed. */
static void u32_to_str(uint32_t v, char* out, uint8_t base, int neg)
{
char tmp[11]; /* 10 digits + sign */
uint8_t n = 0;
do
{
uint32_t d = v % base;
tmp[n++] = (char)((d < 10u) ? ('0' + d) : ('A' + d - 10u));
v /= base;
} while (v != 0u);
if (neg)
{
tmp[n++] = '-';
}
while (n != 0u)
{
*out++ = tmp[--n];
}
*out = '\0';
}
static char* str_pad(char* out, const char* s, int width, uint8_t left, char pad)
{
int len = (int)strlen(s);
if (!left)
{
while (width-- > len)
{
*out++ = pad;
}
}
while (*s != '\0')
{
*out++ = *s++;
}
while (width-- > len)
{
*out++ = ' ';
}
return out;
}
void cli_printf(const char* fmt, ...)
{
static char s_txBuf[CLI_TX_BUF_SIZE];
char* out = s_txBuf;
va_list args;
va_start(args, fmt);
while (*fmt != '\0' && out < s_txBuf + CLI_TX_BUF_SIZE - 12)
{
char c = *fmt++;
if (c != '%')
{
*out++ = c;
continue;
}
{
uint8_t left = 0;
char pad = ' ';
int width = 0;
char num[12];
char spec;
if (*fmt == '-')
{
left = 1;
fmt++;
}
if (*fmt == '0')
{
pad = '0';
fmt++;
}
while (*fmt >= '0' && *fmt <= '9')
{
width = width * 10 + (*fmt++ - '0');
}
if (*fmt == 'l')
{
fmt++; /* 32-bit already: %lu == %u here */
}
spec = *fmt;
if (spec != '\0')
{
fmt++;
}
switch (spec)
{
case 's':
out = str_pad(out, va_arg(args, const char*), width, left, ' ');
break;
case 'c':
*out++ = (char)va_arg(args, int);
break;
case 'd':
{
long v = va_arg(args, long);
u32_to_str((uint32_t)((v < 0) ? -v : v), num, 10, v < 0);
out = str_pad(out, num, width, left, pad);
break;
}
case 'u':
u32_to_str(va_arg(args, unsigned long), num, 10, 0);
out = str_pad(out, num, width, left, pad);
break;
case 'x':
case 'X':
u32_to_str(va_arg(args, unsigned long), num, 16, 0);
out = str_pad(out, num, width, left, pad);
break;
case '%':
*out++ = '%';
break;
default:
*out++ = '%';
if (spec != '\0')
{
*out++ = spec;
}
break;
}
}
}
va_end(args);
*out = '\0';
cli_write(s_txBuf);
}
/* ------------------------------------------------------------------ */
/* Commands */
/* ------------------------------------------------------------------ */
typedef void (*CliCmdHandler_t)(int argc, char* argv[]);
typedef struct
{
const char* name;
const char* help;
CliCmdHandler_t handler;
} CliCmd_t;
static uint8_t s_otaMode; /* 1 = OTA binary frame mode */
static void CmdHelp(int argc, char* argv[]);
static void CmdVersion(int argc, char* argv[])
{
(void)argc;
(void)argv;
cli_printf("bootloader version: %s (download mode)\r\n", BOOT_FW_VERSION);
cli_printf("SystemCoreClock = %lu Hz\r\n", (unsigned long)SystemCoreClock);
}
/**
* @brief "devinfo": report the bank the NEXT OTA would replace, using the
* same "cur bank: APP<n>" line the application prints - the PC
* updater picks the payload for the other bank, which is exactly
* the boot's OTA target rule (inactive bank; APP1 when the
* bootsetting record is invalid).
*/
static void CmdDevInfo(int argc, char* argv[])
{
const caiic_bootsetting_t* bs = (const caiic_bootsetting_t*)CAIIC_BOOTSETTING_ADDR;
uint32_t active;
uint8_t bs_ok;
(void)argc;
(void)argv;
bs_ok = (bs->magic == CAIIC_BOOT_MAGIC &&
bs->crc32 == caiic_crc32((const uint8_t*)bs,
(uint32_t)offsetof(caiic_bootsetting_t, crc32))) ? 1 : 0;
active = bs->active_bank;
cli_printf("bootloader: %s, download mode\r\n", BOOT_FW_VERSION);
if (!bs_ok)
{
cli_write("bootsetting: INVALID - OTA targets APP1\r\n");
active = CAIIC_ACTIVE_BANK2; /* report the bank NOT targeted */
}
cli_printf("cur bank: APP%lu\r\n", (unsigned long)active);
cli_printf("uptime: %lu s\r\n", (unsigned long)(boot_millis() / 1000u));
}
/**
* @brief "ota": enter the OTA binary frame mode (0xCA frames on the raw
* stream, framed RSP on TX). The marker line is the host's go-ahead.
* Exits: OTA_ABORT frame, 3 s idle, or reset after OTA_END.
*/
static void CmdOta(int argc, char* argv[])
{
(void)argc;
(void)argv;
cli_write("[ota] binary mode ON - send 0xCA OTA frames now\r\n"
"(ble_protocol.md section 6.6; OTA_ABORT or 3s idle exits)\r\n");
s_otaMode = 1;
}
static void CmdReset(int argc, char* argv[])
{
(void)argc;
(void)argv;
cli_write("resetting...\r\n");
boot_delay_ms(200u);
NVIC_SystemReset();
}
static void CmdUartInfo(int argc, char* argv[])
{
(void)argc;
(void)argv;
cli_write("usart1: TX=PB6 RX=PB7 (AF4)\r\n");
cli_write("baud: 460800, 8 data bits, no parity, 1 stop bit\r\n");
cli_write("flow control: none; tx: polled; rx: DMA ring (3KB, polled)\r\n");
}
/**
* @brief Parse a "pxN" pin name (e.g. "pb0", "PA3", "PB13") into a GPIO port
* and pin mask. @return 1 on success, 0 on bad name/out-of-range pin.
*/
static int pinset_parse(const char* name, GPIO_Module** port, uint16_t* mask)
{
const char* p;
int pin = 0;
if (name[0] != 'p' && name[0] != 'P')
{
return 0;
}
if (name[1] == 'a' || name[1] == 'A')
{
*port = GPIOA;
}
else if (name[1] == 'b' || name[1] == 'B')
{
*port = GPIOB;
}
else
{
return 0;
}
p = &name[2];
if (*p < '0' || *p > '9')
{
return 0;
}
while (*p >= '0' && *p <= '9')
{
pin = pin * 10 + (*p - '0');
if (pin > 15)
{
return 0;
}
p++;
}
if (*p != '\0')
{
return 0;
}
if (*port == GPIOA && pin > 6) /* PA7 not bonded on this chip */
{
return 0;
}
if (*port == GPIOB && pin > 13) /* PB14/PB15 not bonded */
{
return 0;
}
*mask = (uint16_t)(1u << pin);
return 1;
}
/**
* @brief "pinset <pxN> [0|1]": set a GPIO output high/low, or read its level.
* e.g. "pinset pb0 1", "pinset pa0 0", "pinset pb0" (read level).
*/
static void CmdPinSet(int argc, char* argv[])
{
GPIO_Module* port = NULL;
uint16_t mask = 0;
GPIO_InitType s;
if (argc < 2)
{
cli_write("usage: pinset <pxN> [0|1] (e.g. pinset pb0 1)\r\n");
return;
}
if (!pinset_parse(argv[1], &port, &mask))
{
cli_printf("bad pin: %s (use pa0..pa6 / pb0..pb13)\r\n", argv[1]);
return;
}
if (port == GPIOA)
{
RCC_EnableAPB2PeriphClk(RCC_APB2_PERIPH_GPIOA, ENABLE);
}
else
{
RCC_EnableAPB2PeriphClk(RCC_APB2_PERIPH_GPIOB, ENABLE);
}
if (argc >= 3)
{
/* reserved-pin guard: warn before reconfiguring SWD / console pins */
if ((port == GPIOA && (mask & (GPIO_PIN_4 | GPIO_PIN_5)) != 0u) ||
(port == GPIOB && (mask & (GPIO_PIN_6 | GPIO_PIN_7)) != 0u))
{
cli_write("warning: SWD/console pin - reconfigure may break the link\r\n");
}
GPIO_InitStruct(&s);
s.Pin = mask;
s.GPIO_Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitPeripheral(port, &s);
if (argv[2][0] == '1' && argv[2][1] == '\0')
{
GPIO_SetBits(port, mask);
cli_printf("%s = 1\r\n", argv[1]);
}
else if (argv[2][0] == '0' && argv[2][1] == '\0')
{
GPIO_ResetBits(port, mask);
cli_printf("%s = 0\r\n", argv[1]);
}
else
{
cli_write("usage: pinset <pxN> [0|1] (0=low, 1=high)\r\n");
}
}
else
{
/* read: the input data register reflects the actual pin level */
cli_printf("%s = %u\r\n", argv[1],
(unsigned)(GPIO_ReadInputDataBit(port, mask) ? 1u : 0u));
}
}
static const CliCmd_t s_cmds[] = {
{"help", "help [cmd]: list commands / show one command's help", CmdHelp},
{"version", "show bootloader version", CmdVersion},
{"devinfo", "show boot info + cur bank (OTA writes the OTHER bank)", CmdDevInfo},
{"bsdump", "bsdump: show bootsetting record + crc check", AppBootset_CmdBsdump},
{"bsset", "bsset <field> <val>: write bootsetting field (careful!)", AppBootset_CmdBsset},
{"appsw", "appsw [1|2]: switch boot bank (crc verified) + reboot", AppBootset_CmdAppswitch},
{"ota", "ota: UART enters OTA binary frame mode (see help ota)", CmdOta},
{"reset", "reset: system reset (reboot)", CmdReset},
{"pinset", "pinset <pxN> [0|1]: set/read GPIO level (pa0-6/pb0-13)", CmdPinSet},
{"uartinfo", "uartinfo: show USART1 pins/baud/format", CmdUartInfo},
};
#define CLI_CMD_COUNT (sizeof(s_cmds) / sizeof(s_cmds[0]))
static void CmdHelp(int argc, char* argv[])
{
uint32_t i;
if (argc >= 2)
{
for (i = 0; i < CLI_CMD_COUNT; i++)
{
if (strcmp(argv[1], s_cmds[i].name) == 0)
{
cli_printf(" %-8s %s\r\n", s_cmds[i].name, s_cmds[i].help);
return;
}
}
cli_printf("unknown command: %s (try 'help')\r\n", argv[1]);
return;
}
cli_write("mode: BOOT download\r\n");
cli_write("commands:\r\n");
for (i = 0; i < CLI_CMD_COUNT; i++)
{
cli_printf(" %-8s %s\r\n", s_cmds[i].name, s_cmds[i].help);
}
cli_write("keys: TAB = complete, UP/DOWN = history\r\n");
}
/* ------------------------------------------------------------------ */
/* Line parsing / dispatch */
/* ------------------------------------------------------------------ */
static int cli_exec_line(char* line)
{
char* argv[CLI_MAX_ARGS];
int argc = 0;
char* p = line;
uint32_t i;
while (*p != '\0' && argc < CLI_MAX_ARGS)
{
while (*p == ' ' || *p == '\t')
{
p++;
}
if (*p == '\0')
{
break;
}
argv[argc++] = p;
while (*p != '\0' && *p != ' ' && *p != '\t')
{
p++;
}
if (*p != '\0')
{
*p++ = '\0';
}
}
if (argc == 0)
{
return 0;
}
for (i = 0; i < CLI_CMD_COUNT; i++)
{
if (strcmp(argv[0], s_cmds[i].name) == 0)
{
s_cmds[i].handler(argc, argv);
return 0;
}
}
cli_printf("unknown command: %s (try 'help')\r\n", argv[0]);
return 1;
}
/* ------------------------------------------------------------------ */
/* Download-mode superloop */
/* ------------------------------------------------------------------ */
#define BOOT_CLI_PROMPT "caiic->" /* wire-compatible with the app CLI */
/**
* @brief Redraw the input line: erase `shownLen` chars, print prompt + line.
* @return new shown length (in the boot CLI this always equals strlen(line)).
*/
static uint16_t boot_cli_redraw(const char* line, uint16_t shownLen)
{
uint16_t i;
uint16_t len = (uint16_t)strlen(line);
cli_write("\r" BOOT_CLI_PROMPT);
for (i = 0; i < shownLen; i++)
{
cli_write(" ");
}
cli_write("\r" BOOT_CLI_PROMPT);
if (len > 0u)
{
boot_usart_write((const uint8_t*)line, len);
}
return len;
}
/**
* @brief TAB completion on the first token of the line (command name only).
* Single match: complete it and append a space.
* Multiple matches: extend to the common prefix, or list candidates.
* Ported from the application CLI (mcm-ddc-ble/src/app_cli.c).
*/
static uint16_t boot_cli_tab_complete(char* line, uint16_t len)
{
char common[CLI_LINE_SIZE];
uint16_t commonLen = 0;
int matchCount = 0;
uint32_t i;
/* Only complete the first token (no space typed yet) */
if (memchr(line, ' ', len) != NULL)
{
return len;
}
line[len] = '\0';
for (i = 0; i < CLI_CMD_COUNT; i++)
{
const char* name = s_cmds[i].name;
if (strncmp(name, line, len) == 0)
{
if (matchCount == 0)
{
strcpy(common, name);
commonLen = (uint16_t)strlen(common);
}
else
{
/* shrink common to the shared prefix */
while (commonLen > len && strncmp(common, name, commonLen) != 0)
{
commonLen--;
}
common[commonLen] = '\0';
}
matchCount++;
}
}
if (matchCount == 0)
{
return len;
}
if (matchCount == 1)
{
/* complete the command name + one space */
while (len < CLI_LINE_SIZE - 1u && len < commonLen)
{
boot_usart_write((const uint8_t*)&common[len], 1);
line[len] = common[len];
len++;
}
if (len < CLI_LINE_SIZE - 1u)
{
line[len] = ' ';
len++;
cli_write(" ");
}
return len;
}
if (commonLen > len)
{
/* extend to the common prefix */
while (len < CLI_LINE_SIZE - 1u && len < commonLen)
{
boot_usart_write((const uint8_t*)&common[len], 1);
line[len] = common[len];
len++;
}
}
else
{
/* ambiguous: list the candidates and redraw */
cli_write("\r\n");
for (i = 0; i < CLI_CMD_COUNT; i++)
{
const char* name = s_cmds[i].name;
if (strncmp(name, line, len) == 0)
{
cli_write(name);
cli_write(" ");
}
}
cli_write("\r\n");
boot_cli_redraw(line, len);
}
return len;
}
/* ------------------------------------------------------------------ */
/* Command history (ring buffer) */
/* ------------------------------------------------------------------ */
static char s_history[CLI_HISTORY_SIZE][CLI_LINE_SIZE];
static uint8_t s_histCount = 0; /* number of valid entries */
static uint8_t s_histHead = 0; /* next write position */
/**
* @brief Store a command line in the history ring (skips empty lines and
* repeats of the most recent entry).
*/
static void cli_history_add(const char* line)
{
if (line[0] == '\0')
{
return;
}
if (s_histCount > 0u)
{
uint8_t last = (uint8_t)((s_histHead + CLI_HISTORY_SIZE - 1u) % CLI_HISTORY_SIZE);
if (strcmp(s_history[last], line) == 0)
{
return;
}
}
strcpy(s_history[s_histHead], line);
s_histHead = (uint8_t)((s_histHead + 1u) % CLI_HISTORY_SIZE);
if (s_histCount < CLI_HISTORY_SIZE)
{
s_histCount++;
}
}
/**
* @brief Get a history entry; nav = 0 is the newest entry.
*/
static const char* cli_history_get(int nav)
{
int idx = ((int)s_histHead - 1 - nav + CLI_HISTORY_SIZE) % CLI_HISTORY_SIZE;
return s_history[idx];
}
/**
* @brief Download-mode main loop. Never returns (leaves via reset only).
*/
void boot_cli_run(void)
{
static char s_line[CLI_LINE_SIZE];
uint16_t len = 0;
uint16_t shownLen = 0; /* chars currently displayed after the prompt */
uint8_t escState = 0; /* 0=normal, 1=got ESC, 2=got ESC '[' */
int histNav = -1; /* -1 = editing, 0.. = history entry (0=newest) */
uint32_t otaLastRx = 0;
cli_write("\r\nCLI ready (bootloader download mode). Type 'help' for commands.\r\n"
BOOT_CLI_PROMPT);
for (;;)
{
uint8_t ch;
/* deferred post-OTA_END reset (waits until the RSP drained) */
app_ota_reset_poll();
/* OTA binary-mode fallback exit after 3 s of silence */
if (s_otaMode && (boot_millis() - otaLastRx) > OTA_IDLE_EXIT_MS)
{
s_otaMode = 0;
app_ota_chan_idle();
cli_write("\r\n[ota] binary mode OFF (idle)\r\n" BOOT_CLI_PROMPT);
len = 0;
shownLen = 0;
}
if (boot_usart_read(&ch) == 0)
{
continue;
}
if (s_otaMode)
{
otaLastRx = boot_millis();
if (app_ota_chan_rx_uart(ch) != 0)
{
s_otaMode = 0;
cli_write("\r\n[ota] binary mode OFF (abort)\r\n" BOOT_CLI_PROMPT);
len = 0;
shownLen = 0;
}
continue;
}
/* Escape sequences (arrow keys: ESC '[' 'A'/'B') */
if (escState == 1)
{
escState = (ch == '[') ? 2 : 0;
continue;
}
if (escState == 2)
{
escState = 0;
if (ch == 'A') /* UP: older history entry */
{
if (histNav + 1 < (int)s_histCount)
{
histNav++;
strcpy(s_line, cli_history_get(histNav));
shownLen = boot_cli_redraw(s_line, shownLen);
len = (uint16_t)strlen(s_line);
}
}
else if (ch == 'B') /* DOWN: newer entry / back to empty line */
{
if (histNav >= 0)
{
histNav--;
if (histNav >= 0)
{
strcpy(s_line, cli_history_get(histNav));
}
else
{
s_line[0] = '\0';
}
shownLen = boot_cli_redraw(s_line, shownLen);
len = (uint16_t)strlen(s_line);
}
}
/* other escape sequences are ignored */
continue;
}
switch (ch)
{
case CLI_KEY_ESC:
escState = 1;
break;
case '\r':
case '\n':
cli_write("\r\n");
s_line[len] = '\0';
cli_history_add(s_line); /* copy before dispatch tokenizes in place */
(void)cli_exec_line(s_line);
len = 0;
shownLen = 0;
histNav = -1;
cli_write(BOOT_CLI_PROMPT);
break;
case '\b':
case 0x7F:
if (len != 0u)
{
len--;
cli_write("\b \b");
shownLen = len;
}
break;
case CLI_KEY_TAB:
len = boot_cli_tab_complete(s_line, len);
shownLen = len;
break;
default:
if (ch >= 0x20u && ch < 0x7Fu && len < CLI_LINE_SIZE - 1u)
{
s_line[len++] = (char)ch;
boot_usart_write(&ch, 1); /* echo */
shownLen = len;
}
break;
}
}
}