diff --git a/boot/boot_serial/src/boot_serial.c b/boot/boot_serial/src/boot_serial.c index f99841981c..6be3694236 100644 --- a/boot/boot_serial/src/boot_serial.c +++ b/boot/boot_serial/src/boot_serial.c @@ -161,7 +161,9 @@ BOOT_LOG_MODULE_DECLARE(mcuboot); #define BOOT_DIRECT_UPLOAD_SECONDARY_SLOT_ID_REMAINDER 0 static char in_buf[MCUBOOT_SERIAL_MAX_RECEIVE_SIZE + 1]; +#ifndef MCUBOOT_SERIAL_RAW_PROTOCOL static char dec_buf[MCUBOOT_SERIAL_MAX_RECEIVE_SIZE + 1]; +#endif const struct boot_uart_funcs *boot_uf; static struct nmgr_hdr *bs_hdr; static bool bs_entry; @@ -1374,13 +1376,7 @@ static void boot_serial_output(void) { char *data; - int len, out; - uint16_t crc; - uint16_t totlen; - char pkt_cont[2] = { SHELL_NLIP_DATA_START1, SHELL_NLIP_DATA_START2 }; - char pkt_start[2] = { SHELL_NLIP_PKT_START1, SHELL_NLIP_PKT_START2 }; - char buf[BOOT_SERIAL_OUT_MAX + sizeof(*bs_hdr) + sizeof(crc) + sizeof(totlen)]; - char encoded_buf[BASE64_ENCODE_SIZE(sizeof(buf))]; + int len; data = bs_obuf; len = (uintptr_t)cbor_state->payload_mut - (uintptr_t)bs_obuf; @@ -1390,6 +1386,18 @@ boot_serial_output(void) bs_hdr->nh_len = htons(len); bs_hdr->nh_group = htons(bs_hdr->nh_group); +#ifdef MCUBOOT_SERIAL_RAW_PROTOCOL + boot_uf->write((const char *)bs_hdr, sizeof(*bs_hdr)); + boot_uf->write(data, len); +#else + int out; + uint16_t crc; + uint16_t totlen; + char pkt_cont[2] = { SHELL_NLIP_DATA_START1, SHELL_NLIP_DATA_START2 }; + char pkt_start[2] = { SHELL_NLIP_PKT_START1, SHELL_NLIP_PKT_START2 }; + char buf[BOOT_SERIAL_OUT_MAX + sizeof(*bs_hdr) + sizeof(crc) + sizeof(totlen)]; + char encoded_buf[BASE64_ENCODE_SIZE(sizeof(buf))]; + #ifdef __ZEPHYR__ crc = crc16_itu_t(CRC16_INITIAL_CRC, (uint8_t *)bs_hdr, sizeof(*bs_hdr)); crc = crc16_itu_t(crc, data, len); @@ -1441,10 +1449,12 @@ boot_serial_output(void) boot_uf->write("\n", 1); } +#endif /* MCUBOOT_SERIAL_RAW_PROTOCOL */ BOOT_LOG_DBG("TX"); } +#ifndef MCUBOOT_SERIAL_RAW_PROTOCOL /* * Returns 1 if full packet has been received. */ @@ -1503,6 +1513,44 @@ boot_serial_in_dec(char *in, int inlen, char *out, int *out_off, int maxout) return 1; } +#endif /* !MCUBOOT_SERIAL_RAW_PROTOCOL */ + +#ifdef MCUBOOT_SERIAL_RAW_PROTOCOL +/* Dispatch every complete raw SMP packet at the front of "buf" (length from the + * SMP header, capacity "max_input") via boot_serial_input(), keeping any + * trailing bytes; returns the count of unconsumed bytes left in "buf". */ +static int +boot_serial_input_raw(char *buf, int len, int max_input) +{ + while (len >= (int)sizeof(struct nmgr_hdr)) { + const struct nmgr_hdr *hdr = (const struct nmgr_hdr *)buf; + const int pkt_len = (int)sizeof(*hdr) + ntohs(hdr->nh_len); + + if ((hdr->nh_op != NMGR_OP_READ && hdr->nh_op != NMGR_OP_WRITE) || + pkt_len > max_input) { + /* Not a valid request, or larger than the receive buffer. */ + return 0; + } + + if (len < pkt_len) { + /* More fragments expected. */ + break; + } + + boot_serial_input(buf, pkt_len); + + /* Preserve any bytes that belong to the following packet. */ + if (len > pkt_len) { + memmove(buf, buf + pkt_len, (size_t)(len - pkt_len)); + len -= pkt_len; + } else { + len = 0; + } + } + + return len; +} +#endif /* MCUBOOT_SERIAL_RAW_PROTOCOL */ /* * Task which waits reading console, expecting to get image over @@ -1513,10 +1561,15 @@ boot_serial_read_console(const struct boot_uart_funcs *f,int timeout_in_ms) { int rc; int off; +#ifndef MCUBOOT_SERIAL_RAW_PROTOCOL int dec_off = 0; +#endif int full_line; int max_input; int elapsed_in_ms = 0; +#ifdef MCUBOOT_SERIAL_RAW_PROTOCOL_INPUT_TIMEOUT + uint32_t raw_input_start = 0; +#endif #ifndef MCUBOOT_SERIAL_WAIT_FOR_DFU bool allow_idle = true; @@ -1533,7 +1586,15 @@ boot_serial_read_console(const struct boot_uart_funcs *f,int timeout_in_ms) * from serial console (if single-thread mode is used). */ #ifndef MCUBOOT_SERIAL_WAIT_FOR_DFU - if (allow_idle == true) { + /* + * Stay out of CPU idle while a partial raw packet is buffered, so the + * input-expiration timeout below is enforced promptly. + */ + if (allow_idle == true +#ifdef MCUBOOT_SERIAL_RAW_PROTOCOL_INPUT_TIMEOUT + && off == 0 +#endif + ) { MCUBOOT_CPU_IDLE(); allow_idle = false; } @@ -1550,6 +1611,24 @@ boot_serial_read_console(const struct boot_uart_funcs *f,int timeout_in_ms) goto check_timeout; } off += rc; +#ifdef MCUBOOT_SERIAL_RAW_PROTOCOL + /* + * Raw protocol: bytes are accumulated until one or more full SMP + * packets have been received; a single read may carry several complete + * packets (or a packet plus the start of the next). Dispatch all + * complete packets and keep any trailing bytes for the next read. + */ +#ifdef MCUBOOT_SERIAL_RAW_PROTOCOL_INPUT_TIMEOUT + bool was_idle = (off == rc); +#endif + off = boot_serial_input_raw(in_buf, off, max_input); +#ifdef MCUBOOT_SERIAL_RAW_PROTOCOL_INPUT_TIMEOUT + if (off > 0 && was_idle) { + /* First bytes of a new packet buffered; start its expiry window. */ + raw_input_start = k_uptime_get_32(); + } +#endif +#else if (!full_line) { if (off == max_input) { /* @@ -1573,7 +1652,15 @@ boot_serial_read_console(const struct boot_uart_funcs *f,int timeout_in_ms) boot_serial_input(&dec_buf[2], dec_off - 2); } off = 0; +#endif /* MCUBOOT_SERIAL_RAW_PROTOCOL */ check_timeout: +#ifdef MCUBOOT_SERIAL_RAW_PROTOCOL_INPUT_TIMEOUT + if (off > 0 && (k_uptime_get_32() - raw_input_start) >= + MCUBOOT_SERIAL_RAW_PROTOCOL_INPUT_TIMEOUT_MS) { + /* Stalled partial raw packet expired; discard it. */ + off = 0; + } +#endif /* Subtract elapsed time */ #ifdef MCUBOOT_SERIAL_WAIT_FOR_DFU elapsed_in_ms = (k_uptime_get_32() - start); diff --git a/boot/zephyr/Kconfig.serial_recovery b/boot/zephyr/Kconfig.serial_recovery index 780ba775d5..760b93f759 100644 --- a/boot/zephyr/Kconfig.serial_recovery +++ b/boot/zephyr/Kconfig.serial_recovery @@ -9,8 +9,8 @@ menuconfig MCUBOOT_SERIAL select REBOOT select SERIAL select UART_INTERRUPT_DRIVEN - select BASE64 - select CRC + select BASE64 if !BOOT_SERIAL_RAW_PROTOCOL + select CRC if !BOOT_SERIAL_RAW_PROTOCOL select ZCBOR depends on !BOOT_FIRMWARE_LOADER help @@ -46,6 +46,40 @@ config BOOT_SERIAL_CDC_ACM endchoice +config BOOT_SERIAL_RAW_PROTOCOL + bool "Raw (SMP without framing and encoding) protocol" + help + If y, SMP (mcumgr) commands are exchanged over the serial port as raw SMP + packets instead of using the SMP over console format. This drops the + base64 encoding and the additional console-specific framing (packet + start/continuation markers, length prefix and CRC), which reduces flash + and RAM usage and increases transfer speed, at the cost of requiring a + binary-capable serial link. Packet boundaries are derived from the + length field of the SMP header. The SMP client must be configured + to use a matching raw transport. + +config BOOT_SERIAL_RAW_PROTOCOL_INPUT_TIMEOUT + bool "Raw protocol input expiration" + depends on BOOT_SERIAL_RAW_PROTOCOL + default y + help + Discard a partially received raw SMP packet after a period without new + data, so that a stalled or malformed transfer cannot permanently wedge + serial recovery. Unlike the SMP over console format, the raw protocol has + no CRC or framing to detect a truncated packet, so there is otherwise no + way to resynchronise the input stream. This mirrors Zephyr's + MCUMGR_TRANSPORT_RAW_UART_INPUT_TIMEOUT (zephyrproject-rtos/zephyr#101821) + and is enabled by default because raw recovery is unsafe without it. + +config BOOT_SERIAL_RAW_PROTOCOL_INPUT_TIMEOUT_MS + int "Raw protocol input expiration timeout (ms)" + depends on BOOT_SERIAL_RAW_PROTOCOL_INPUT_TIMEOUT + default 3000 + help + Time in milliseconds after first receiving data for a raw SMP packet, + after which a still-incomplete packet is dropped and the receive buffer + is reset to await a new command. + config MCUBOOT_SERIAL_DIRECT_IMAGE_UPLOAD bool "Allow to select image number for DFU" depends on !SINGLE_APPLICATION_SLOT diff --git a/boot/zephyr/include/mcuboot_config/mcuboot_config.h b/boot/zephyr/include/mcuboot_config/mcuboot_config.h index 766565f3ca..8afec03776 100644 --- a/boot/zephyr/include/mcuboot_config/mcuboot_config.h +++ b/boot/zephyr/include/mcuboot_config/mcuboot_config.h @@ -348,6 +348,16 @@ #define MCUBOOT_SERIAL_IMG_GRP_SLOT_INFO #endif +#ifdef CONFIG_BOOT_SERIAL_RAW_PROTOCOL +#define MCUBOOT_SERIAL_RAW_PROTOCOL +#endif + +#ifdef CONFIG_BOOT_SERIAL_RAW_PROTOCOL_INPUT_TIMEOUT +#define MCUBOOT_SERIAL_RAW_PROTOCOL_INPUT_TIMEOUT +#define MCUBOOT_SERIAL_RAW_PROTOCOL_INPUT_TIMEOUT_MS \ + CONFIG_BOOT_SERIAL_RAW_PROTOCOL_INPUT_TIMEOUT_MS +#endif + #ifdef CONFIG_MCUBOOT_SERIAL #define MCUBOOT_SERIAL_RECOVERY #endif diff --git a/boot/zephyr/sample.yaml b/boot/zephyr/sample.yaml index 5e50a44d8f..781b19e21e 100644 --- a/boot/zephyr/sample.yaml +++ b/boot/zephyr/sample.yaml @@ -47,6 +47,12 @@ tests: integration_platforms: - nrf52840dk/nrf52840 tags: bootloader_mcuboot + sample.bootloader.mcuboot.serial_recovery_raw: + extra_args: EXTRA_CONF_FILE=serial_recovery_raw.conf + platform_allow: nrf52840dk/nrf52840 + integration_platforms: + - nrf52840dk/nrf52840 + tags: bootloader_mcuboot sample.bootloader.mcuboot.serial_recovery_all_options: extra_args: EXTRA_CONF_FILE=serial_recovery.conf extra_configs: diff --git a/boot/zephyr/serial_adapter.c b/boot/zephyr/serial_adapter.c index de2e5c5117..9e731685c7 100644 --- a/boot/zephyr/serial_adapter.c +++ b/boot/zephyr/serial_adapter.c @@ -86,9 +86,44 @@ console_write(const char *str, int cnt) } } +#ifdef CONFIG_BOOT_SERIAL_RAW_PROTOCOL +/* + * Raw protocol read: the data is binary, so it is returned without a NUL + * terminator. A fragment may be larger than the caller's buffer, so retain the + * unconsumed tail for the next call rather than dropping it (the fragment's + * buffer is not recycled by boot_uart_fifo_getline() until it is fully consumed + * here). Returns the number of bytes copied into "str". + */ +static int +console_read_raw(char *str, int str_size, int *newline) +{ + static char *pending; + static int pending_len; + int n; + + if (pending_len == 0) { + pending_len = boot_uart_fifo_getline(&pending); + if (pending == NULL) { + *newline = 0; + return 0; + } + } + + n = MIN(pending_len, str_size); + memcpy(str, pending, n); + pending += n; + pending_len -= n; + *newline = 1; + return n; +} +#endif + int console_read(char *str, int str_size, int *newline) { +#ifdef CONFIG_BOOT_SERIAL_RAW_PROTOCOL + return console_read_raw(str, str_size, newline); +#else char *line; int len; @@ -107,6 +142,7 @@ console_read(char *str, int str_size, int *newline) str[len] = '\0'; *newline = 1; return len + 1; +#endif } int @@ -159,6 +195,16 @@ boot_uart_fifo_callback(const struct device *dev, void *user_data) cmd = CONTAINER_OF(node, struct line_input, node); } +#ifdef CONFIG_BOOT_SERIAL_RAW_PROTOCOL + cmd->line[cur++] = byte; + + if (cur >= CONFIG_BOOT_MAX_LINE_INPUT_LEN) { + cmd->len = cur; + sys_slist_append(&lines_queue, &cmd->node); + cur = 0; + cmd = NULL; + } +#else if (cur < CONFIG_BOOT_MAX_LINE_INPUT_LEN) { cmd->line[cur++] = byte; } @@ -169,7 +215,22 @@ boot_uart_fifo_callback(const struct device *dev, void *user_data) cur = 0; cmd = NULL; } +#endif + } + +#ifdef CONFIG_BOOT_SERIAL_RAW_PROTOCOL + /* + * No line delimiter exists in raw mode, so deliver whatever has been + * received during this interrupt as a fragment; the boot serial reader + * reassembles full packets using the SMP header length field. + */ + if (cmd != NULL && cur > 0) { + cmd->len = cur; + sys_slist_append(&lines_queue, &cmd->node); + cur = 0; + cmd = NULL; } +#endif } static int diff --git a/boot/zephyr/serial_recovery_raw.conf b/boot/zephyr/serial_recovery_raw.conf new file mode 100644 index 0000000000..15136f884e --- /dev/null +++ b/boot/zephyr/serial_recovery_raw.conf @@ -0,0 +1,4 @@ +CONFIG_MCUBOOT_SERIAL=y +CONFIG_BOOT_SERIAL_UART=y +CONFIG_BOOT_SERIAL_RAW_PROTOCOL=y +CONFIG_UART_CONSOLE=n diff --git a/docs/release-notes.d/serial-recovery-raw-protocol.md b/docs/release-notes.d/serial-recovery-raw-protocol.md new file mode 100644 index 0000000000..aace1cbc0d --- /dev/null +++ b/docs/release-notes.d/serial-recovery-raw-protocol.md @@ -0,0 +1,12 @@ +- Serial recovery: added the `BOOT_SERIAL_RAW_PROTOCOL` option, + which exchanges SMP packets over the serial port in raw binary + form instead of the SMP over console encoding (no base64, length + prefix, CRC or console framing). This reduces flash and RAM usage + and increases transfer speed at the cost of requiring a + binary-capable serial link. It mirrors Zephyr's + `CONFIG_UART_MCUMGR_RAW_PROTOCOL` transport. +- Serial recovery: added `BOOT_SERIAL_RAW_PROTOCOL_INPUT_TIMEOUT` + (enabled by default with the raw protocol), which discards a + partially received raw SMP packet after a period without new data + so a stalled transfer cannot wedge recovery. Mirrors Zephyr's + `CONFIG_MCUMGR_TRANSPORT_RAW_UART_INPUT_TIMEOUT`. diff --git a/docs/serial_recovery.md b/docs/serial_recovery.md index ade2419dff..83c7ba2124 100644 --- a/docs/serial_recovery.md +++ b/docs/serial_recovery.md @@ -40,6 +40,33 @@ if the ``MCUBOOT_PERUSER_MGMT_GROUP_ENABLED`` option is enabled. The serial recovery feature can use any serial interface provided by the platform. +## Transport encoding + +By default, SMP packets are exchanged using the *SMP over console* encoding: +each packet is prefixed with a two-byte length, protected with a CRC16, +base64-encoded and split into ``\n``-terminated frames marked with start and +continuation markers. + +When the ``MCUBOOT_SERIAL_RAW_PROTOCOL`` option is enabled (on Zephyr, +``CONFIG_BOOT_SERIAL_RAW_PROTOCOL``), the *raw* encoding is used instead. +The SMP packet (header directly followed by the payload) is transferred as +binary without base64 encoding, length prefix, CRC or console framing. Packet +boundaries are derived from the length field of the SMP header. + +Compared to SMP over console, the raw encoding uses less flash and RAM and +allows faster transfers, at the cost of requiring a binary-capable serial link. +The two encodings are mutually exclusive and selected at build time; the mcumgr +client must be configured to use a matching raw transport (for example a Zephyr +application built with ``CONFIG_UART_MCUMGR_RAW_PROTOCOL``). + +Because the raw encoding has no CRC or framing to detect a truncated packet, a +stalled or malformed transfer would otherwise wedge serial recovery. To prevent +this, ``BOOT_SERIAL_RAW_PROTOCOL_INPUT_TIMEOUT`` (Zephyr: +``CONFIG_BOOT_SERIAL_RAW_PROTOCOL_INPUT_TIMEOUT``) discards a partially received +packet after ``..._INPUT_TIMEOUT_MS`` milliseconds without new data and awaits a +fresh command. It is enabled by default whenever the raw encoding is used, and +mirrors Zephyr's ``CONFIG_MCUMGR_TRANSPORT_RAW_UART_INPUT_TIMEOUT``. + ## Image uploading Uploading an image is targeted to the primary slot by default.