/** * @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" (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 #include #include #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" 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 [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 [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 [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 : 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 [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; } } }