diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index f083acbe..05f3409b 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -19,6 +19,7 @@ target_sources(app PRIVATE src/sm_at_commands.c) target_sources(app PRIVATE src/sm_at_socket.c) target_sources(app PRIVATE src/sm_at_icmp.c) target_sources(app PRIVATE src/sm_at_fota.c) +target_sources(app PRIVATE src/sm_at_dfu.c) target_sources(app PRIVATE src/sm_uart_handler.c) # NORDIC SDK APP END target_sources_ifdef(CONFIG_SM_SMS app PRIVATE src/sm_at_sms.c) diff --git a/app/Kconfig b/app/Kconfig index ec7bf2e3..088efc02 100644 --- a/app/Kconfig +++ b/app/Kconfig @@ -182,6 +182,9 @@ config SM_MQTTC config SM_FULL_FOTA bool "Full modem FOTA support" +config SM_DFU_MODEM_FULL + bool "Full modem DFU support" + config SM_PPP bool "PPP support" diff --git a/app/sample.yaml b/app/sample.yaml index 812b3e06..b04f1692 100644 --- a/app/sample.yaml +++ b/app/sample.yaml @@ -73,6 +73,17 @@ tests: - thingy91x/nrf9151/ns integration_platforms: - nrf9151dk/nrf9151/ns + serial_modem.mfw_full_fota_and_dfu: + sysbuild: true + build_only: true + extra_args: + - EXTRA_CONF_FILE="overlay-full_fota.conf" + extra_configs: + - CONFIG_SM_DFU_MODEM_FULL=y + platform_allow: + - nrf9151dk/nrf9151/ns + integration_platforms: + - nrf9151dk/nrf9151/ns serial_modem.lwm2m_carrier: sysbuild: true build_only: true diff --git a/app/scripts/requirements.txt b/app/scripts/requirements.txt new file mode 100644 index 00000000..78cdb8e6 --- /dev/null +++ b/app/scripts/requirements.txt @@ -0,0 +1,3 @@ +# This file lists all the dependencies required to run the sm_dfu_host.py script. +pyserial +cbor2 diff --git a/app/scripts/sm_dfu_host.py b/app/scripts/sm_dfu_host.py new file mode 100644 index 00000000..1355fe24 --- /dev/null +++ b/app/scripts/sm_dfu_host.py @@ -0,0 +1,527 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2025, Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + +import io +import sys +import time +import argparse +from pathlib import Path + +import serial +import cbor2 + +LINE_END = "\r" + +ser = None + + +def init_serial(port: str, baudrate: int): + """Initialize the global serial device.""" + global ser + ser = serial.Serial(port, baudrate, rtscts=True) + time.sleep(0.1) + ser.reset_input_buffer() + + +def close_serial(): + """Close the global serial device.""" + global ser + if ser: + ser.close() + ser = None + + +def send_command(command: str, *params): + """Send an AT command with optional parameters.""" + if params: + cmd_str = f"{command}={','.join(str(p) for p in params)}{LINE_END}" + else: + cmd_str = f"{command}{LINE_END}" + + ser.write(cmd_str.encode()) + + +def wait_for_response(expected: str, timeout: float) -> bool: + """Wait for expected string in serial output. Returns False on ERROR or timeout.""" + end_time = time.time() + timeout + buffer = "" + + while time.time() < end_time: + if ser.in_waiting: + data = ser.read(ser.in_waiting) + buffer += data.decode(errors="ignore") + if expected in buffer: + return True + if "ERROR" in buffer: + return False + time.sleep(0.01) + + return False + + +def wait_for_device(max_attempts: int = 10, + timeout: float = 3.0, + max_backoff: float = 10.0) -> bool: + """Ping device with AT until OK, using exponential backoff.""" + backoff = 0.5 + for attempt in range(1, max_attempts + 1): + print(f"Pinging device (attempt {attempt}/{max_attempts})...") + send_command("AT") + if wait_for_response("OK", timeout): + print("Device ready") + return True + if attempt < max_attempts: + time.sleep(backoff) + backoff = min(backoff * 2, max_backoff) + return False + + +def wait_for_urc(timeout: float) -> tuple[int | None, str]: + """Wait for #XDFU URC, return (status_code, urc_line).""" + end_time = time.time() + timeout + buffer = "" + + while time.time() < end_time: + if ser.in_waiting: + data = ser.read(ser.in_waiting) + buffer += data.decode(errors="ignore") + + # Look for #XDFU line + if "#XDFU:" in buffer: + for line in buffer.split("\n"): + if "#XDFU:" in line: + urc_line = line.strip() + # Parse "#XDFU:,," + try: + parts = line.split(":")[1].strip().split(",") + if len(parts) >= 3: + return int(parts[2]), urc_line + except (ValueError, IndexError): + pass + time.sleep(0.01) + + return None, "" + + +def send_data(data: bytes): + """Send raw bytes.""" + ser.write(data) + ser.flush() + + +_prev_line_len = 0 + +def print_progress(label: str, current: int, total: int, bytes_sent: int, total_bytes: int, + start_time: float, cmd: str = ""): + """Print single-line progress indicator.""" + global _prev_line_len + percent = (current / total) * 100 if total else 0 + elapsed = max(time.time() - start_time, 0.001) + speed = bytes_sent / elapsed / 1024 + + line = ( + f"{label} [{current:4d}/{total:<4d}] " + f"{percent:5.1f}% {bytes_sent/1024:7.1f} KB / {total_bytes/1024:7.1f} KB {speed:5.0f} KB/s" + ) + if cmd: + line += f" {cmd}" + padding = max(0, _prev_line_len - len(line)) + sys.stdout.write("\r" + line + " " * padding) + sys.stdout.flush() + _prev_line_len = len(line) + + +def parse_chunks_bin(filepath: str, chunk_size: int = 4096) -> list[tuple[int, bytes]]: + """Return list of (offset, data) tuples from a .bin file.""" + chunks = [] + offset = 0 + with open(filepath, "rb") as f: + while chunk := f.read(chunk_size): + chunks.append((offset, chunk)) + offset += len(chunk) + return chunks + + +def parse_chunks_cbor(filepath: str, chunk_size: int = 4096) -> tuple[list, list]: + """Parse .cbor file, return (boot_chunks, fw_chunks) as (addr, data) tuples.""" + with open(filepath, "rb") as f: + raw = f.read() + + # Parse signed CBOR wrapper (contains manifest + Nordic's signature) + wrapper = cbor2.loads(raw) + if hasattr(wrapper, "value"): + wrapper = wrapper.value + payload = wrapper[2] + + # Parse manifest + manifest = cbor2.loads(payload) + segments_cbor = manifest[3] + segments_flat = cbor2.loads(segments_cbor) + + # Find where segment data starts in file + stream = io.BytesIO(raw) + cbor2.CBORDecoder(stream).decode() + blob_offset = stream.tell() + + # Build segment list + segments = [] + data_offset = blob_offset + for i in range(0, len(segments_flat), 2): + addr = segments_flat[i] + length = segments_flat[i + 1] + is_boot = i == 0 + segments.append({ + "addr": addr, + "length": length, + "offset": data_offset, + "is_boot": is_boot, + }) + data_offset += length + + # Split each segment into chunks + boot_chunks = [] + fw_chunks = [] + + for seg in segments: + seg_data = raw[seg["offset"]:seg["offset"] + seg["length"]] + + for i in range(0, seg["length"], chunk_size): + chunk_addr = seg["addr"] + i + chunk_data = seg_data[i:i + chunk_size] + + if seg["is_boot"]: + boot_chunks.append((chunk_addr, chunk_data)) + else: + fw_chunks.append((chunk_addr, chunk_data)) + + return boot_chunks, fw_chunks + + +# DFU types +DFU_TYPE_APP = 0 +DFU_TYPE_DELTA = 1 +DFU_TYPE_FULL = 2 + + +def dfu_init(dfu_type: int, size: int = None) -> bool: + """Initialize DFU. Size required for APP/DELTA. FULL reboots device.""" + if dfu_type == DFU_TYPE_APP: + if size is None: + return False + print(f"AT#XDFUINIT={dfu_type},{size}") + send_command("AT#XDFUINIT", dfu_type, size) + expected, timeout = "OK", 5.0 + elif dfu_type == DFU_TYPE_DELTA: + if size is None: + return False + print(f"AT#XDFUINIT={dfu_type},{size}") + send_command("AT#XDFUINIT", dfu_type, size) + expected, timeout = "OK", 120.0 # Flash erase can take minutes + elif dfu_type == DFU_TYPE_FULL: + print(f"AT#XDFUINIT={dfu_type}") + send_command("AT#XDFUINIT", dfu_type) + expected, timeout = "Bootloader mode ready", 30.0 # Device reboots + else: + return False + + # Wait for expected response or ERROR + end_time = time.time() + timeout + buffer = "" + while time.time() < end_time: + if ser.in_waiting: + data = ser.read(ser.in_waiting) + buffer += data.decode(errors="ignore") + if expected in buffer: + return True + if "ERROR" in buffer: + return False + time.sleep(0.01) + return False + + +def dfu_write(dfu_type: int, addr: int, data: bytes) -> tuple[bool, str]: + """Write firmware chunk. Returns (success, urc_line).""" + send_command("AT#XDFUWRITE", dfu_type, addr, len(data)) + if not wait_for_response("OK", 2.0): + return False, "" + + send_data(data) + + status, urc_line = wait_for_urc(10.0) + return status == 0, urc_line + + +def dfu_apply(dfu_type: int) -> bool: + """Apply firmware update. Waits for URC status.""" + print(f"AT#XDFUAPPLY={dfu_type}") + send_command("AT#XDFUAPPLY", dfu_type) + + status, urc_line = wait_for_urc(10.0) + if urc_line: + print(urc_line) + return status == 0 + + +def do_dfu_app(filepath: str, retries: int = 3) -> bool: + """Perform application DFU.""" + chunks = parse_chunks_bin(filepath) + total_size = sum(len(data) for _, data in chunks) + total_chunks = len(chunks) + + print(f"Application DFU: {total_size:,} bytes in {total_chunks} chunks") + + # Initialize + if not dfu_init(DFU_TYPE_APP, total_size): + print("\nERROR: Init failed") + return False + + # Write chunks + bytes_sent = 0 + start_time = time.time() + + for i, (addr, data) in enumerate(chunks, 1): + success = False + urc = "" + for _ in range(retries): + success, urc = dfu_write(DFU_TYPE_APP, addr, data) + if success: + break + if not success: + print(f"\nERROR: Chunk {i} failed after {retries} retries") + return False + + bytes_sent += len(data) + cmd = f"AT#XDFUWRITE={DFU_TYPE_APP},{addr},{len(data)} -> {urc}" + print_progress("Application", i, total_chunks, bytes_sent, total_size, start_time, cmd) + + print() + + # Apply update + if not dfu_apply(DFU_TYPE_APP): + print("ERROR: Apply failed") + return False + + print("OK: Application DFU complete. Reboot to activate.") + return True + + +def do_dfu_delta(filepath: str, retries: int = 3) -> bool: + """Perform delta modem DFU.""" + chunks = parse_chunks_bin(filepath) + total_size = sum(len(data) for _, data in chunks) + total_chunks = len(chunks) + + print(f"Modem delta DFU: {total_size:,} bytes in {total_chunks} chunks") + + # Initialize + if not dfu_init(DFU_TYPE_DELTA, total_size): + print("\nERROR: Init failed") + return False + + # Write chunks + bytes_sent = 0 + start_time = time.time() + + for i, (addr, data) in enumerate(chunks, 1): + success = False + urc = "" + for _ in range(retries): + success, urc = dfu_write(DFU_TYPE_DELTA, addr, data) + if success: + break + if not success: + print(f"\nERROR: Chunk {i} failed after {retries} retries") + return False + + bytes_sent += len(data) + cmd = f"AT#XDFUWRITE={DFU_TYPE_DELTA},{addr},{len(data)} -> {urc}" + print_progress("Modem delta", i, total_chunks, bytes_sent, total_size, start_time, cmd) + + print() + + # Apply update + if not dfu_apply(DFU_TYPE_DELTA): + print("ERROR: Apply failed") + return False + + print("OK: Modem delta DFU complete. Reset modem to activate.") + return True + + +def do_dfu_full(filepath: str, retries: int = 3) -> bool: + """Perform full modem DFU.""" + boot_chunks, fw_chunks = parse_chunks_cbor(filepath) + boot_size = sum(len(data) for _, data in boot_chunks) + fw_size = sum(len(data) for _, data in fw_chunks) + + print(f"Modem full DFU: Boot={boot_size:,} bytes, FW={fw_size:,} bytes") + + # Initialize + print(f"AT#XDFUINIT={DFU_TYPE_FULL}") + send_command("AT#XDFUINIT", DFU_TYPE_FULL) + + if not wait_for_response("Bootloader mode ready", 30.0): + print("ERROR: Device did not enter bootloader mode") + return False + print("OK: Device in bootloader mode") + + # Phase 1: Write bootloader chunks + print("Phase 1: Bootloader segment") + bytes_sent = 0 + start_time = time.time() + + for i, (addr, data) in enumerate(boot_chunks, 1): + success, urc = dfu_write(DFU_TYPE_FULL, addr, data) + if not success: + print(f"\nERROR: Boot chunk {i} failed") + return False + + bytes_sent += len(data) + cmd = f"AT#XDFUWRITE={DFU_TYPE_FULL},{addr},{len(data)} -> {urc}" + print_progress("BOOT", i, len(boot_chunks), bytes_sent, boot_size, start_time, cmd) + + print() + + # Apply boot (switches to firmware mode) + if not dfu_apply(DFU_TYPE_FULL): + print("ERROR: Boot apply failed") + return False + print("OK: Bootloader committed") + + # Phase 2: Write firmware chunks + # Warning: After the first firmware segment write, the modem will be corrupted if + # the update is not completed successfully. + print("Phase 2: Firmware segments") + print("WARNING: After the first firmware segment write, the modem will be " + "corrupted if the update is not completed successfully.") + bytes_sent = 0 + start_time = time.time() + + for i, (addr, data) in enumerate(fw_chunks, 1): + success = False + urc = "" + for _ in range(retries): + success, urc = dfu_write(DFU_TYPE_FULL, addr, data) + if success: + break + if not success: + print(f"\nERROR: FW chunk {i} failed after {retries} retries") + return False + + bytes_sent += len(data) + cmd = f"AT#XDFUWRITE={DFU_TYPE_FULL},{addr},{len(data)} -> {urc}" + print_progress("FW", i, len(fw_chunks), bytes_sent, fw_size, start_time, cmd) + + print() + + # Apply firmware (triggers reboot) + print(f"AT#XDFUAPPLY={DFU_TYPE_FULL}") + send_command("AT#XDFUAPPLY", DFU_TYPE_FULL) + + # Wait for device to become responsive + if not wait_for_device(max_attempts=10, timeout=5.0, max_backoff=5.0): + print("ERROR: Device did not respond to AT after reboot") + return False + + print("OK: Modem full DFU complete!") + return True + + +def do_ping() -> bool: + """Ping device with AT, return True if OK received.""" + print("AT") + send_command("AT") + if wait_for_response("OK", 3.0): + print("OK") + return True + print("ERROR: No response") + return False + + +def do_reset(): + """Reset device with AT#XRESET.""" + print("AT#XRESET") + send_command("AT#XRESET") + + +def do_modem_reset(): + """Reset modem with AT#XMODEMRESET.""" + print("AT#XMODEMRESET") + send_command("AT#XMODEMRESET") + + +def main() -> int: + parser = argparse.ArgumentParser(description="Serial Modem DFU Host", allow_abbrev=False) + parser.add_argument("--port", required=True, help="Serial port") + parser.add_argument("--baudrate", required=True, type=int, help="Baud rate") + parser.add_argument("--file", help="Firmware file path") + parser.add_argument("--type", choices=["application", "modem-delta", "modem-full"], + help="DFU type") + parser.add_argument("--ping", action="store_true", help="Ping device (AT)") + parser.add_argument("--reset", action="store_true", help="Reset device (AT#XRESET)") + parser.add_argument("--modem-reset", action="store_true", + help="Reset modem (AT#XMODEMRESET)") + args = parser.parse_args() + + # Validate arguments + utility_mode = args.ping or args.reset or getattr(args, 'modem_reset', False) + if utility_mode: + if args.file or args.type: + print("ERROR: --ping/--reset/--modem-reset cannot be combined with --file or --type") + return 1 + utility_count = sum([args.ping, args.reset, getattr(args, 'modem_reset', False)]) + if utility_count > 1: + print("ERROR: --ping, --reset, and --modem-reset cannot be combined") + return 1 + else: + if not args.file or not args.type: + print("ERROR: --file and --type are required for DFU") + return 1 + + print(f"Using: {args.port} @ {args.baudrate} baud") + + try: + init_serial(args.port, args.baudrate) + + if args.ping: + success = do_ping() + elif args.reset: + do_reset() + success = True + elif getattr(args, 'modem_reset', False): + do_modem_reset() + success = True + else: + filepath = Path(args.file).expanduser() + if not filepath.is_file(): + print(f"ERROR: File not found: {filepath}") + return 1 + print(f"File: {filepath}") + print(f"Type: {args.type}") + + if args.type == "application": + success = do_dfu_app(str(filepath)) + elif args.type == "modem-delta": + success = do_dfu_delta(str(filepath)) + elif args.type == "modem-full": + success = do_dfu_full(str(filepath)) + else: + print(f"ERROR: Unknown DFU type: {args.type}") + success = False + except KeyboardInterrupt: + print("\nAborted by user") + success = False + except serial.SerialException as e: + print(f"\nERROR: Serial connection lost: {e}") + success = False + finally: + close_serial() + + return 0 if success else 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/app/src/main.c b/app/src/main.c index ca6c1364..1b1dd18d 100644 --- a/app/src/main.c +++ b/app/src/main.c @@ -11,8 +11,10 @@ #include #include #include +#include #include #include "sm_at_host.h" +#include "sm_at_dfu.h" #include "sm_at_fota.h" #include "sm_settings.h" #include "sm_util.h" @@ -134,6 +136,44 @@ static void check_app_fota_status(void) sm_fota_stage = FOTA_STAGE_COMPLETE; } +static int bootloader_mode_init(void) +{ + int ret; + + ret = nrf_modem_lib_bootloader_init(); + if (ret) { + LOG_ERR("Failed to initialize bootloader mode: %d", ret); + return ret; + } + LOG_INF("Bootloader mode initiated successfully"); + + ret = sm_ctrl_pin_init(); + if (ret) { + LOG_ERR("Failed to init ctrl_pin: %d", ret); + return ret; + } + + k_work_queue_start(&sm_work_q, sm_wq_stack_area, + K_THREAD_STACK_SIZEOF(sm_wq_stack_area), + SM_WQ_PRIORITY, NULL); + + ret = sm_at_host_bootloader_init(); + if (ret) { + LOG_ERR("Failed to init at_host: %d", ret); + return ret; + } + + ret = sm_at_send_str("Bootloader mode ready\r\n"); + if (ret) { + LOG_ERR("Failed to send bootloader mode ready string: %d", ret); + return ret; + } + + sm_bootloader_mode_enabled = true; + + return 0; +} + int lte_auto_connect(void) { int err = 0; @@ -228,6 +268,8 @@ int start_execute(void) int main(void) { + int ret; + const uint32_t rr = nrf_power_resetreas_get(NRF_POWER_NS); nrf_power_resetreas_clear(NRF_POWER_NS, 0x70017); @@ -240,6 +282,21 @@ int main(void) LOG_WRN("Failed to init sm settings"); } + if (sm_bootloader_mode_requested) { + /* Clear bootloader mode flag */ + ret = bootloader_mode_request(false); + if (ret) { + LOG_ERR("Failed to clear bootloader mode flag, starting SM in normal mode"); + } else { + ret = bootloader_mode_init(); + if (ret) { + LOG_ERR("Failed to initialize bootloader mode: %d", ret); + goto exit_reboot; + } + goto exit; + } + } + #if defined(CONFIG_SM_FULL_FOTA) if (sm_modem_full_fota) { sm_finish_modem_full_fota(); @@ -247,7 +304,7 @@ int main(void) } #endif - int ret = nrf_modem_lib_init(); + ret = nrf_modem_lib_init(); if (ret) { LOG_ERR("Modem library init failed, err: %d", ret); @@ -256,6 +313,8 @@ int main(void) } else if (ret == -EIO) { LOG_ERR("Please program full modem firmware with the bootloader or " "external tools"); + (void)bootloader_mode_request(true); + goto exit_reboot; } } @@ -274,4 +333,8 @@ int main(void) LOG_ERR("Failed to start SM (%d). It's not operational!!!", ret); } return ret; + +exit_reboot: + LOG_PANIC(); + sys_reboot(SYS_REBOOT_COLD); } diff --git a/app/src/sm_at_commands.c b/app/src/sm_at_commands.c index 3e396fa3..5b20bdff 100644 --- a/app/src/sm_at_commands.c +++ b/app/src/sm_at_commands.c @@ -150,7 +150,7 @@ static int handle_at_sleep(enum at_parser_cmd_type cmd_type, struct at_parser *p return ret; } -static void final_call(void (*func)(void)) +void final_call(void (*func)(void)) { /* Delegate the final call to a worker so that the "OK" response is properly sent. */ static struct k_work_delayable worker; diff --git a/app/src/sm_at_dfu.c b/app/src/sm_at_dfu.c new file mode 100644 index 00000000..3f236264 --- /dev/null +++ b/app/src/sm_at_dfu.c @@ -0,0 +1,607 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "sm_at_host.h" +#include "sm_at_dfu.h" +#include "sm_settings.h" +#include "sm_uart_handler.h" + +LOG_MODULE_REGISTER(sm_dfu, CONFIG_SM_LOG_LEVEL); + +#define APP_DFU_BUFFER_SIZE 1024 + +enum xdfu_image_type { + DFU_TYPE_APP = 0, + DFU_TYPE_DELTA_MFW = 1, + DFU_TYPE_FULL_MFW = 2, +}; + +enum xdfu_full_mfw_segment_type { + DFU_FULL_MFW_SEGMENT_BOOTLOADER = 0, + DFU_FULL_MFW_SEGMENT_FIRMWARE = 1, +}; + +enum xdfu_operation { + DFU_OPERATION_INITIALIZE = 0, + DFU_OPERATION_DATA_WRITE = 1, + DFU_OPERATION_APPLY_UPDATE = 2, +}; +struct xdfu_app_datamode_context { + size_t addr; + size_t len; +} xdfu_app_datamode_context; + +struct xdfu_delta_mfw_datamode_context { + size_t addr; + size_t len; +} xdfu_delta_mfw_datamode_context; + +struct xdfu_full_mfw_datamode_context { + size_t addr; + size_t len; +} xdfu_full_mfw_datamode_context; + +bool sm_bootloader_mode_requested; +bool sm_bootloader_mode_enabled; + +int full_mfw_dfu_segment_type = DFU_FULL_MFW_SEGMENT_BOOTLOADER; + +static uint8_t app_dfu_buffer[APP_DFU_BUFFER_SIZE] __aligned(4); +static bool app_dfu_buffer_initialized; + +static enum xdfu_image_type xdfu_current_image_type; +static uint32_t xdfu_bytes_written; +static int xdfu_status; + +static void delta_dfu_evt_handler(enum dfu_target_evt_id evt_id) +{ + switch (evt_id) { + case DFU_TARGET_EVT_ERASE_PENDING: + LOG_INF("Delta DFU erase %s", "pending"); + break; + case DFU_TARGET_EVT_TIMEOUT: + LOG_WRN("Delta DFU erase %s", "timeout"); + break; + case DFU_TARGET_EVT_ERASE_DONE: + LOG_INF("Delta DFU erase %s", "done"); + break; + default: + break; + } +} + +int bootloader_mode_request(bool enable) +{ + int err; + + sm_bootloader_mode_requested = enable; + + err = sm_settings_bootloader_mode_save(); + if (err) { + LOG_ERR("Failed to set bootloader mode requested to: %s", + enable ? "enabled" : "disabled"); + return err; + } + + LOG_DBG("Bootloader mode request set to: %s", enable ? "enabled" : "disabled"); + + return 0; +} + +static int set_full_mfw_dfu_segment_type(enum xdfu_full_mfw_segment_type type) +{ + int err; + + full_mfw_dfu_segment_type = type; + + err = sm_settings_full_mfw_dfu_segment_type_save(); + if (err) { + LOG_ERR("Failed to set full MFW DFU segment type to: %d", type); + return err; + } + + LOG_DBG("Full MFW DFU segment type set to: %d", type); + + return 0; +} + +static int xdfu_datamode_callback(uint8_t op, const uint8_t *data, int len, uint8_t flags) +{ + ARG_UNUSED(flags); + + int err; + + switch (op) { + case DATAMODE_SEND: + if (data == NULL || len <= 0) { + LOG_ERR("Chunk data invalid (data=%p len=%d)", (void *)data, len); + return -EINVAL; + } + + switch (xdfu_current_image_type) { + case DFU_TYPE_APP: + err = dfu_target_mcuboot_write((const void *)data, len); + if (err) { + LOG_ERR("Failed to write %s: %d", "app firmware", err); + xdfu_status = err; + } + break; + case DFU_TYPE_DELTA_MFW: + err = dfu_target_modem_delta_write((const void *)data, len); + if (err) { + LOG_ERR("Failed to write %s: %d", "delta modem firmware", err); + xdfu_status = err; + } + break; + case DFU_TYPE_FULL_MFW: + if (full_mfw_dfu_segment_type == DFU_FULL_MFW_SEGMENT_BOOTLOADER) { + err = nrf_modem_bootloader_bl_write((void *)data, len); + if (err) { + LOG_ERR("Failed to write %s: %d", "bootloader segment", err); + xdfu_status = err; + break; + } + } else if (full_mfw_dfu_segment_type == DFU_FULL_MFW_SEGMENT_FIRMWARE) { + err = nrf_modem_bootloader_fw_write( + xdfu_full_mfw_datamode_context.addr, + (void *)data, len); + if (err) { + LOG_ERR("Failed to write %s: %d", "firmware segment", err); + xdfu_status = err; + break; + } + } else { + LOG_ERR("Invalid segment type: %d", full_mfw_dfu_segment_type); + xdfu_status = -EINVAL; + break; + } + break; + default: + LOG_ERR("Invalid image type: %d", xdfu_current_image_type); + return -EINVAL; + } + + if (xdfu_status == 0) { + xdfu_bytes_written += len; + } + + return 0; + + case DATAMODE_EXIT: { + size_t expected_bytes_written; + + switch (xdfu_current_image_type) { + case DFU_TYPE_APP: + expected_bytes_written = xdfu_app_datamode_context.len; + break; + case DFU_TYPE_DELTA_MFW: + expected_bytes_written = xdfu_delta_mfw_datamode_context.len; + break; + case DFU_TYPE_FULL_MFW: + expected_bytes_written = xdfu_full_mfw_datamode_context.len; + break; + default: + LOG_ERR("Invalid image type: %d", xdfu_current_image_type); + xdfu_status = -EINVAL; + return -EINVAL; + } + + if (xdfu_status == 0 && xdfu_bytes_written != expected_bytes_written) { + LOG_WRN("Wrote %u bytes, expected %zu", + xdfu_bytes_written, expected_bytes_written); + xdfu_status = -EIO; + } + + urc_send("#XDFU:%u,%u,%d\r\n", + xdfu_current_image_type, DFU_OPERATION_DATA_WRITE, + xdfu_status ? -1 : 0); + + /* Reset for next chunk. */ + xdfu_bytes_written = 0; + xdfu_status = 0; + + return 0; + } + default: + LOG_WRN("Unexpected datamode op: %u (flags=0x%02x)", op, flags); + return 0; + } +} + +SM_AT_CMD_CUSTOM(xdfu_init, "AT#XDFUINIT", handle_at_xdfu_init); +static int handle_at_xdfu_init(enum at_parser_cmd_type cmd_type, struct at_parser *parser, + uint32_t param_count) +{ + ARG_UNUSED(param_count); + + int err; + uint16_t type; + size_t size; + + switch (cmd_type) { + case AT_PARSER_CMD_TYPE_SET: + err = at_parser_num_get(parser, 1, &type); + if (err) { + LOG_ERR("Failed to get type: %d", err); + return err; + } + + if (sm_bootloader_mode_enabled && type != DFU_TYPE_FULL_MFW) { + LOG_ERR("DFU type %d is not supported in bootloader mode", type); + return -EOPNOTSUPP; + } + + switch (type) { + case DFU_TYPE_APP: + err = at_parser_num_get(parser, 2, &size); + if (err) { + LOG_ERR("Failed to get size: %d", err); + return err; + } + + if (!app_dfu_buffer_initialized) { + err = dfu_target_mcuboot_set_buf(app_dfu_buffer, + APP_DFU_BUFFER_SIZE); + if (err) { + LOG_ERR("Failed to set app firmware buffer: %d", err); + return err; + } + app_dfu_buffer_initialized = true; + } + + err = dfu_target_mcuboot_init(size, 0, NULL); + if (err == -EFAULT) { + /* Already initialized - abort and retry */ + LOG_WRN("MCUBoot DFU already initialized, aborting and retrying"); + (void)dfu_target_mcuboot_done(false); + err = dfu_target_mcuboot_init(size, 0, NULL); + } + if (err) { + LOG_ERR("Failed to initialize MCUBoot DFU target: %d", err); + return err; + } + + LOG_INF("MCUBoot DFU initialized successfully"); + return 0; + case DFU_TYPE_DELTA_MFW: + err = at_parser_num_get(parser, 2, &size); + if (err) { + LOG_ERR("Failed to get size: %d", err); + return err; + } + + err = dfu_target_modem_delta_init(size, 0, delta_dfu_evt_handler); + if (err) { + LOG_ERR("Failed to initialize delta modem firmware: %d", err); + return err; + } + + LOG_INF("Delta modem firmware initialized successfully"); + return 0; + case DFU_TYPE_FULL_MFW: + if (!IS_ENABLED(CONFIG_SM_DFU_MODEM_FULL)) { + LOG_ERR("Full modem DFU is not enabled"); + return -EOPNOTSUPP; + } + + LOG_WRN("WARNING! After the first FW write, the modem will " + "corrupt if the update is not successfully completed."); + err = bootloader_mode_request(true); + if (err) { + LOG_ERR("Failed to enable bootloader mode: %d", err); + return err; + } + + (void)set_full_mfw_dfu_segment_type(DFU_FULL_MFW_SEGMENT_BOOTLOADER); + + LOG_PANIC(); + sys_reboot(SYS_REBOOT_COLD); + default: + LOG_ERR("Invalid target type: %d", type); + return -EINVAL; + } + case AT_PARSER_CMD_TYPE_TEST: +#if defined(CONFIG_SM_DFU_MODEM_FULL) + rsp_send("\r\n#XDFUINIT: (%d,%d,%d),\r\n", + DFU_TYPE_APP, DFU_TYPE_DELTA_MFW, DFU_TYPE_FULL_MFW); +#else + rsp_send("\r\n#XDFUINIT: (%d,%d),\r\n", + DFU_TYPE_APP, DFU_TYPE_DELTA_MFW); +#endif + return 0; + default: + LOG_ERR("Invalid command type: %d", cmd_type); + return -EINVAL; + } +} + +SM_AT_CMD_CUSTOM(xdfu_write, "AT#XDFUWRITE", handle_at_xdfu_write); +static int handle_at_xdfu_write(enum at_parser_cmd_type cmd_type, struct at_parser *parser, + uint32_t param_count) +{ + int err; + uint16_t type; + + switch (cmd_type) { + case AT_PARSER_CMD_TYPE_SET: + err = at_parser_num_get(parser, 1, &type); + if (err) { + LOG_ERR("Failed to get type: %d", err); + return err; + } + + if (sm_bootloader_mode_enabled && type != DFU_TYPE_FULL_MFW) { + LOG_ERR("DFU type %d is not supported in bootloader mode", type); + return -EOPNOTSUPP; + } + + switch (type) { + case DFU_TYPE_APP: + if (param_count != 4) { + LOG_ERR("Invalid number of parameters for data write"); + return -EINVAL; + } + err = at_parser_num_get(parser, 2, &xdfu_app_datamode_context.addr); + if (err) { + LOG_ERR("Failed to get address: %d", err); + return err; + } + err = at_parser_num_get(parser, 3, &xdfu_app_datamode_context.len); + if (err) { + LOG_ERR("Failed to get length: %d", err); + return err; + } + + if (xdfu_app_datamode_context.len == 0) { + LOG_ERR("Length cannot be 0"); + return -EINVAL; + } + + /* Prepare per-chunk accounting for the datamode callback. */ + xdfu_current_image_type = DFU_TYPE_APP; + xdfu_bytes_written = 0; + xdfu_status = 0; + + err = enter_datamode(xdfu_datamode_callback, + xdfu_app_datamode_context.len); + if (err) { + LOG_ERR("Failed to enter data write mode: %d", err); + return err; + } + + return 0; + case DFU_TYPE_DELTA_MFW: + if (param_count != 4) { + LOG_ERR("Invalid number of parameters for data write"); + return -EINVAL; + } + err = at_parser_num_get(parser, 2, &xdfu_delta_mfw_datamode_context.addr); + if (err) { + LOG_ERR("Failed to get address: %d", err); + return err; + } + err = at_parser_num_get(parser, 3, &xdfu_delta_mfw_datamode_context.len); + if (err) { + LOG_ERR("Failed to get length: %d", err); + return err; + } + + if (xdfu_delta_mfw_datamode_context.len == 0) { + LOG_ERR("Length cannot be 0"); + return -EINVAL; + } + + /* Prepare per-chunk accounting for the datamode callback. */ + xdfu_current_image_type = DFU_TYPE_DELTA_MFW; + xdfu_bytes_written = 0; + xdfu_status = 0; + + err = enter_datamode(xdfu_datamode_callback, + xdfu_delta_mfw_datamode_context.len); + if (err) { + LOG_ERR("Failed to enter data write mode: %d", err); + return err; + } + + return 0; + case DFU_TYPE_FULL_MFW: + if (!IS_ENABLED(CONFIG_SM_DFU_MODEM_FULL)) { + LOG_ERR("Full modem DFU is not enabled"); + return -EOPNOTSUPP; + } + + /* + * POINT OF NO RETURN: After the first firmware segment write, + * the modem will be corrupted if the update is not completed. + * Bootloader segment writes can still be rolled back. + */ + if (param_count != 4) { + LOG_ERR("Invalid number of parameters for data write"); + return -EINVAL; + } + + err = at_parser_num_get(parser, 2, &xdfu_full_mfw_datamode_context.addr); + if (err) { + LOG_ERR("Failed to get address: %d", err); + return err; + } + + err = at_parser_num_get(parser, 3, &xdfu_full_mfw_datamode_context.len); + if (err) { + LOG_ERR("Failed to get length: %d", err); + return err; + } + + if (xdfu_full_mfw_datamode_context.len == 0) { + LOG_ERR("Length cannot be 0"); + return -EINVAL; + } + + /* Prepare per-chunk accounting for the datamode callback. */ + xdfu_current_image_type = DFU_TYPE_FULL_MFW; + xdfu_bytes_written = 0; + xdfu_status = 0; + + err = enter_datamode(xdfu_datamode_callback, + xdfu_full_mfw_datamode_context.len); + if (err) { + LOG_ERR("Failed to enter data write mode: %d", err); + return err; + } + + return 0; + default: + LOG_ERR("Invalid target type: %d", type); + return -EINVAL; + } + case AT_PARSER_CMD_TYPE_TEST: +#if defined(CONFIG_SM_DFU_MODEM_FULL) + rsp_send("\r\n#XDFUWRITE: (%d,%d,%d),,\r\n", + DFU_TYPE_APP, DFU_TYPE_DELTA_MFW, DFU_TYPE_FULL_MFW); +#else + rsp_send("\r\n#XDFUWRITE: (%d,%d),,\r\n", + DFU_TYPE_APP, DFU_TYPE_DELTA_MFW); +#endif + return 0; + default: + LOG_ERR("Invalid command type: %d", cmd_type); + return -EINVAL; + } +} + +SM_AT_CMD_CUSTOM(xdfu_apply, "AT#XDFUAPPLY", handle_at_xdfu_apply); +static int handle_at_xdfu_apply(enum at_parser_cmd_type cmd_type, struct at_parser *parser, + uint32_t param_count) +{ + ARG_UNUSED(param_count); + + int err; + uint16_t type; + + switch (cmd_type) { + case AT_PARSER_CMD_TYPE_SET: + err = at_parser_num_get(parser, 1, &type); + if (err) { + LOG_ERR("Failed to get type: %d", err); + return err; + } + + if (sm_bootloader_mode_enabled && type != DFU_TYPE_FULL_MFW) { + LOG_ERR("DFU type %d is not supported in bootloader mode", type); + return -EOPNOTSUPP; + } + + switch (type) { + case DFU_TYPE_APP: + err = dfu_target_mcuboot_done(true); + if (err) { + LOG_ERR("App firmware update failed: %d", err); + } else { + err = dfu_target_mcuboot_schedule_update(0); + if (err) { + LOG_ERR("Failed to schedule app firmware update: %d", err); + } + } + + urc_send("#XDFU:%u,%u,%d\r\n", + DFU_TYPE_APP, DFU_OPERATION_APPLY_UPDATE, err ? -1 : 0); + + LOG_INF("App firmware update scheduled"); + + return 0; + case DFU_TYPE_DELTA_MFW: + err = dfu_target_modem_delta_done(true); + if (err) { + LOG_ERR("Delta modem firmware update failed: %d", err); + } else { + err = dfu_target_modem_delta_schedule_update(0); + if (err) { + LOG_ERR("Failed to schedule delta MFW update: %d", err); + } + } + + urc_send("#XDFU:%u,%u,%d\r\n", + DFU_TYPE_DELTA_MFW, DFU_OPERATION_APPLY_UPDATE, err ? -1 : 0); + + LOG_INF("Delta modem firmware update scheduled"); + + return 0; + case DFU_TYPE_FULL_MFW: + if (!IS_ENABLED(CONFIG_SM_DFU_MODEM_FULL)) { + LOG_ERR("Full modem DFU is not enabled"); + return -EOPNOTSUPP; + } + + err = nrf_modem_bootloader_update(); + if (err) { + LOG_ERR("Failed to update bootloader: %d", err); + } else { + if (full_mfw_dfu_segment_type == + DFU_FULL_MFW_SEGMENT_BOOTLOADER) { + LOG_INF("Bootloader segment update successful"); + LOG_WRN("After first FW write, modem will corrupt " + "if update is not completed"); + (void)set_full_mfw_dfu_segment_type( + DFU_FULL_MFW_SEGMENT_FIRMWARE); + } else if (full_mfw_dfu_segment_type == + DFU_FULL_MFW_SEGMENT_FIRMWARE) { + (void)set_full_mfw_dfu_segment_type( + DFU_FULL_MFW_SEGMENT_BOOTLOADER); + LOG_INF("Firmware update successful, rebooting..."); + LOG_PANIC(); + sys_reboot(SYS_REBOOT_COLD); + } + } + + urc_send("#XDFU:%u,%u,%d\r\n", + DFU_TYPE_FULL_MFW, DFU_OPERATION_APPLY_UPDATE, err ? -1 : 0); + + return 0; + default: + LOG_ERR("Invalid target type: %d", type); + return -EINVAL; + } + case AT_PARSER_CMD_TYPE_TEST: +#if defined(CONFIG_SM_DFU_MODEM_FULL) + rsp_send("\r\n#XDFUAPPLY: (%d,%d,%d)\r\n", + DFU_TYPE_APP, DFU_TYPE_DELTA_MFW, DFU_TYPE_FULL_MFW); +#else + rsp_send("\r\n#XDFUAPPLY: (%d,%d)\r\n", + DFU_TYPE_APP, DFU_TYPE_DELTA_MFW); +#endif + return 0; + default: + LOG_ERR("Invalid command type: %d", cmd_type); + return -EINVAL; + } +} + +int sm_at_handle_xdfu_init(char *buf, size_t len, char *at_cmd) +{ + return sm_at_cb_wrapper(buf, len, at_cmd, handle_at_xdfu_init); +} + +int sm_at_handle_xdfu_write(char *buf, size_t len, char *at_cmd) +{ + return sm_at_cb_wrapper(buf, len, at_cmd, handle_at_xdfu_write); +} + +int sm_at_handle_xdfu_apply(char *buf, size_t len, char *at_cmd) +{ + return sm_at_cb_wrapper(buf, len, at_cmd, handle_at_xdfu_apply); +} diff --git a/app/src/sm_at_dfu.h b/app/src/sm_at_dfu.h new file mode 100644 index 00000000..d58aa7dc --- /dev/null +++ b/app/src/sm_at_dfu.h @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#ifndef SM_AT_DFU_H +#define SM_AT_DFU_H + +/** @file sm_at_dfu.h + * + * @brief Vendor-specific AT command for DFU service. + * @{ + */ + +#include +#include + +/* Whether bootloader mode should be enabled. */ +extern bool sm_bootloader_mode_requested; + +/* Whether bootloader mode is enabled. */ +extern bool sm_bootloader_mode_enabled; + +/* Full MFW DFU segment type. */ +extern int full_mfw_dfu_segment_type; + +/** + * @brief Set bootloader mode to enabled or disabled. + * + * @param enable True to enable bootloader mode, false to disable it. + * + * @return 0 on success, negative error code on failure. + */ +int bootloader_mode_request(bool enable); + +/** + * @brief Handle XDFU INIT AT command. + * + * @param buf Response buffer. + * @param len Response buffer size. + * @param at_cmd AT command. + * + * @return 0 on success, negative error code on failure. + */ +int sm_at_handle_xdfu_init(char *buf, size_t len, char *at_cmd); + +/** + * @brief Handle XDFU WRITE AT command. + * + * @param buf Response buffer. + * @param len Response buffer size. + * @param at_cmd AT command. + * + * @return 0 on success, negative error code on failure. + */ +int sm_at_handle_xdfu_write(char *buf, size_t len, char *at_cmd); + +/** + * @brief Handle XDFU APPLY AT command. + * + * @param buf Response buffer. + * @param len Response buffer size. + * @param at_cmd AT command. + * + * @return 0 on success, negative error code on failure. + */ +int sm_at_handle_xdfu_apply(char *buf, size_t len, char *at_cmd); + +/** @} */ + +#endif /* SM_AT_DFU_H */ diff --git a/app/src/sm_at_host.c b/app/src/sm_at_host.c index 3097a157..0b4e584c 100644 --- a/app/src/sm_at_host.c +++ b/app/src/sm_at_host.c @@ -10,17 +10,26 @@ #include "sm_uart_handler.h" #include "sm_util.h" #include "sm_ctrl_pin.h" +#include "sm_at_dfu.h" #if defined(CONFIG_SM_PPP) #include "sm_ppp.h" #endif #include #include #include +#include #include #include #include #include #include +#include +#include + +/* Added for XRESET command */ +extern void final_call(void (*func)(void)); +extern FUNC_NORETURN void sm_reset(void); + LOG_MODULE_REGISTER(sm_at_host, CONFIG_SM_LOG_LEVEL); #define SM_SYNC_STR "Ready\r\n" @@ -32,6 +41,11 @@ LOG_MODULE_REGISTER(sm_at_host, CONFIG_SM_LOG_LEVEL); #define LF '\n' #define HEXDUMP_LIMIT 16 +#define AT_XDFU_INIT_CMD "AT#XDFUINIT" +#define AT_XDFU_WRITE_CMD "AT#XDFUWRITE" +#define AT_XDFU_APPLY_CMD "AT#XDFUAPPLY" +#define AT_XRESET_CMD "AT#XRESET" + /* Operation mode variables */ enum sm_operation_mode { SM_AT_COMMAND_MODE, /* AT command host or bridge */ @@ -543,6 +557,50 @@ int sm_at_send_str(const char *str) return sm_at_send(str, strlen(str)); } +static void handle_bootloader_at_cmd(uint8_t *buf, size_t buf_size, char *at_cmd) +{ + int err; + + if (strncasecmp(at_cmd, AT_XDFU_INIT_CMD, sizeof(AT_XDFU_INIT_CMD) - 1) == 0) { + err = sm_at_handle_xdfu_init(buf + strlen(CRLF_STR), + buf_size - strlen(CRLF_STR), at_cmd); + if (err) { + LOG_ERR("AT command failed: %d", err); + rsp_send_error(); + } else { + rsp_send_ok(); + } + } else if (strncasecmp(at_cmd, AT_XDFU_WRITE_CMD, + sizeof(AT_XDFU_WRITE_CMD) - 1) == 0) { + err = sm_at_handle_xdfu_write(buf + strlen(CRLF_STR), + buf_size - strlen(CRLF_STR), at_cmd); + if (err) { + LOG_ERR("AT command failed: %d", err); + rsp_send_error(); + } else { + rsp_send_ok(); + } + } else if (strncasecmp(at_cmd, AT_XDFU_APPLY_CMD, + sizeof(AT_XDFU_APPLY_CMD) - 1) == 0) { + err = sm_at_handle_xdfu_apply(buf + strlen(CRLF_STR), + buf_size - strlen(CRLF_STR), + at_cmd); + if (err) { + LOG_ERR("AT command failed: %d", err); + rsp_send_error(); + } else { + rsp_send_ok(); + } + } else if (strncasecmp(at_cmd, AT_XRESET_CMD, sizeof(AT_XRESET_CMD) - 1) == 0) { + LOG_INF("Rebooting device via %s command", AT_XRESET_CMD); + LOG_PANIC(); + final_call(sm_reset); + } else { + LOG_ERR("AT command not supported in bootloader mode: %s", at_cmd); + rsp_send_error(); + } +} + static void cmd_send(uint8_t *buf, size_t cmd_length, size_t buf_size, bool *stop_at_receive) { int err; @@ -569,23 +627,30 @@ static void cmd_send(uint8_t *buf, size_t cmd_length, size_t buf_size, bool *sto return; } - /* Send to modem. Same buffer used for sending and for the response. - * Reserve space for CRLF in response buffer. - */ - err = nrf_modem_at_cmd(buf + strlen(CRLF_STR), buf_size - strlen(CRLF_STR), "%s", at_cmd); - if (err == -SILENT_AT_COMMAND_RET) { - return; - } else if (err == -SILENT_AT_CMUX_COMMAND_RET) { - /* Stop processing AT commands until CMUX pipe is established. */ - *stop_at_receive = true; - return; - } else if (err < 0) { - LOG_ERR("AT command failed: %d", err); - rsp_send_error(); + /* If bootloader mode is enabled, handle custom AT commands. */ + if (sm_bootloader_mode_enabled) { + handle_bootloader_at_cmd(buf, buf_size, at_cmd); return; - } else if (err > 0) { - LOG_ERR("AT command error (%d), type: %d: value: %d", - err, nrf_modem_at_err_type(err), nrf_modem_at_err(err)); + } else { + /* Send to modem. Same buffer used for sending and for the response. + * Reserve space for CRLF in response buffer. + */ + err = nrf_modem_at_cmd(buf + strlen(CRLF_STR), buf_size - strlen(CRLF_STR), + "%s", at_cmd); + if (err == -SILENT_AT_COMMAND_RET) { + return; + } else if (err == -SILENT_AT_CMUX_COMMAND_RET) { + /* Stop processing AT commands until CMUX pipe is established. */ + *stop_at_receive = true; + return; + } else if (err < 0) { + LOG_ERR("AT command failed: %d", err); + rsp_send_error(); + return; + } else if (err > 0) { + LOG_ERR("AT command error (%d), type: %d: value: %d", + err, nrf_modem_at_err_type(err), nrf_modem_at_err(err)); + } } /** Format as TS 27.007 command V1 with verbose response format, @@ -1210,3 +1275,27 @@ void sm_at_host_uninit(void) LOG_DBG("at_host uninit done"); } + +int sm_at_host_bootloader_init(void) +{ + int err; + + ring_buf_init(&urc_ctx.rb, sizeof(urc_ctx.buf), urc_ctx.buf); + k_mutex_init(&urc_ctx.mutex); + + k_mutex_lock(&mutex_mode, K_FOREVER); + sm_datamode_time_limit = 0; + datamode_handler = NULL; + at_mode = SM_AT_COMMAND_MODE; + k_mutex_unlock(&mutex_mode); + + k_work_init(&raw_send_scheduled_work, raw_send_scheduled); + + err = sm_uart_handler_enable(); + if (err) { + return err; + } + + LOG_INF("at_host bootloader init done"); + return 0; +} diff --git a/app/src/sm_at_host.h b/app/src/sm_at_host.h index b046681e..09b1ce9b 100644 --- a/app/src/sm_at_host.h +++ b/app/src/sm_at_host.h @@ -91,6 +91,14 @@ size_t sm_at_receive(const uint8_t *data, size_t len, bool *stop_at_receive); */ int sm_at_host_init(void); +/** + * @brief Initialize AT host for bootloader mode + * + * @retval 0 If the operation was successful. + * Otherwise, a (negative) error code is returned. + */ +int sm_at_host_bootloader_init(void); + /** * @brief Powers the UART down. * diff --git a/app/src/sm_settings.c b/app/src/sm_settings.c index 666fb800..3233aa93 100644 --- a/app/src/sm_settings.c +++ b/app/src/sm_settings.c @@ -9,7 +9,9 @@ #include #include #include +#include #include "sm_at_fota.h" +#include "sm_at_dfu.h" #include "sm_settings.h" LOG_MODULE_REGISTER(sm_settings, CONFIG_SM_LOG_LEVEL); @@ -22,6 +24,18 @@ static int settings_set(const char *name, size_t len, settings_read_cb read_cb, if (read_cb(cb_arg, &sm_modem_full_fota, len) > 0) return 0; } + if (!strcmp(name, "bootloader_mode_requested")) { + if (len != sizeof(sm_bootloader_mode_requested)) + return -EINVAL; + if (read_cb(cb_arg, &sm_bootloader_mode_requested, len) > 0) + return 0; + } + if (!strcmp(name, "full_mfw_dfu_segment_type")) { + if (len != sizeof(full_mfw_dfu_segment_type)) + return -EINVAL; + if (read_cb(cb_arg, &full_mfw_dfu_segment_type, len) > 0) + return 0; + } /* Simply ignore obsolete settings that are not in use anymore. * settings_delete() does not completely remove settings. */ @@ -60,3 +74,15 @@ int sm_settings_fota_save(void) return settings_save_one("sm/modem_full_fota", &sm_modem_full_fota, sizeof(sm_modem_full_fota)); } + +int sm_settings_bootloader_mode_save(void) +{ + return settings_save_one("sm/bootloader_mode_requested", + &sm_bootloader_mode_requested, sizeof(sm_bootloader_mode_requested)); +} + +int sm_settings_full_mfw_dfu_segment_type_save(void) +{ + return settings_save_one("sm/full_mfw_dfu_segment_type", + &full_mfw_dfu_segment_type, sizeof(full_mfw_dfu_segment_type)); +} diff --git a/app/src/sm_settings.h b/app/src/sm_settings.h index cbd74f38..849fa28c 100644 --- a/app/src/sm_settings.h +++ b/app/src/sm_settings.h @@ -28,6 +28,20 @@ int sm_settings_init(void); */ int sm_settings_fota_save(void); +/** + * @brief Saves the bootloader mode settings to NVM. + * + * @retval 0 on success, nonzero otherwise. + */ +int sm_settings_bootloader_mode_save(void); + +/** + * @brief Saves the full MFW DFU segment type settings to NVM. + * + * @retval 0 on success, nonzero otherwise. + */ +int sm_settings_full_mfw_dfu_segment_type_save(void); + /** * @brief Saves the auto-connect settings to NVM. * diff --git a/doc/app/at_commands.rst b/doc/app/at_commands.rst index dd7299c4..991bd715 100644 --- a/doc/app/at_commands.rst +++ b/doc/app/at_commands.rst @@ -36,6 +36,7 @@ The modem specific AT commands are documented in the `nRF91x1 AT Commands Refere at_generic at_cmux + at_dfu at_fota at_gnss at_icmp diff --git a/doc/app/at_dfu.rst b/doc/app/at_dfu.rst new file mode 100644 index 00000000..a9ea49f4 --- /dev/null +++ b/doc/app/at_dfu.rst @@ -0,0 +1,443 @@ +.. _DFU_AT_commands: + +DFU AT commands +*************** + +.. contents:: + :local: + :depth: 2 + +This page describes AT commands related to Device Firmware Update (DFU) operations. +These commands allow you to update the application firmware, delta modem firmware, or full modem firmware through UART. + +.. note:: + + The DFU commands use data mode to receive firmware data. + See the :ref:`sm_data_mode` section for more information about data mode. + +DFU types +========= + +The following DFU image types are supported: + +* ``0`` - Application firmware (MCUboot) +* ``1`` - Delta modem firmware +* ``2`` - Full modem firmware (requires :ref:`CONFIG_SM_DFU_MODEM_FULL `) + +.. caution:: + + Full modem firmware DFU is disabled by default. + If the full modem firmware update fails, the modem will not operate until a successful update is completed. + +DFU initialize #XDFUINIT +======================== + +The ``#XDFUINIT`` command initializes the DFU target for a specific image type. + +Set command +----------- + +The set command initializes the DFU target. + +Syntax +~~~~~~ + +.. code-block:: none + + #XDFUINIT=[,] + +* The ```` parameter is an integer indicating the DFU image type: + + * ``0`` - Application firmware + * ``1`` - Delta modem firmware + * ``2`` - Full modem firmware + +* The ```` parameter is an integer indicating the total firmware image size in bytes. + It is required for application (type ``0``) and delta modem firmware (type ``1``) updates. + +For full modem firmware (type ``2``), the command triggers an immediate reboot into bootloader mode. + +Example +~~~~~~~ + +.. code-block:: none + + AT#XDFUINIT=0,123456 + OK + + AT#XDFUINIT=2 + // device reboots into bootloader mode + Bootloader mode ready + +Read command +------------ + +The read command is not supported. + +Test command +------------ + +The test command tests the existence of the command and provides information about the type of its subparameters. + +Syntax +~~~~~~ + +.. code-block:: none + + #XDFUINIT=? + +Response syntax +~~~~~~~~~~~~~~~ + +.. code-block:: none + + #XDFUINIT: , + +Example +~~~~~~~ + +.. code-block:: none + + AT#XDFUINIT=? + + #XDFUINIT: (0,1,2), + + OK + +DFU write #XDFUWRITE +==================== + +The ``#XDFUWRITE`` command writes firmware data to the DFU target. +The command enters data mode to receive the firmware chunk. + +Set command +----------- + +The set command initiates a firmware data write operation. + +Syntax +~~~~~~ + +.. code-block:: none + + #XDFUWRITE=,, + +* The ```` parameter is an integer indicating the DFU image type (``0``, ``1``, or ``2``). +* The ```` parameter is an integer indicating the address offset for the data. +* The ```` parameter is an integer indicating the length of the data chunk to write. + +After the command returns ``OK``, the |SM| application enters data mode to receive exactly ```` bytes of firmware data. +When the data has been received and written, the |SM| application sends a ``#XDFU`` unsolicited notification with the status. + +Example +~~~~~~~ + +.. code-block:: none + + AT#XDFUWRITE=0,0,4096 + OK + // 4096 bytes of firmware data + #XDFU: 0,1,0 + +Read command +------------ + +The read command is not supported. + +Test command +------------ + +The test command tests the existence of the command and provides information about the type of its subparameters. + +Syntax +~~~~~~ + +.. code-block:: none + + #XDFUWRITE=? + +Response syntax +~~~~~~~~~~~~~~~ + +.. code-block:: none + + #XDFUWRITE: ,, + +Example +~~~~~~~ + +.. code-block:: none + + AT#XDFUWRITE=? + + #XDFUWRITE: (0,1,2),, + + OK + +DFU apply #XDFUAPPLY +==================== + +The ``#XDFUAPPLY`` command finalizes the firmware update and schedules it for activation. + +Set command +----------- + +The set command applies the firmware update. + +Syntax +~~~~~~ + +.. code-block:: none + + #XDFUAPPLY= + +* The ```` parameter is an integer indicating the DFU image type (``0``, ``1``, or ``2``). + +For application (type ``0``) and delta modem firmware (type ``1``), the update is scheduled and will be activated on the next reset, which can be done with the ``AT#XRESET`` command. +For full modem firmware (type ``2``), the command applies the current segment (bootloader or firmware) and triggers a reboot if needed. + +Example +~~~~~~~ + +.. code-block:: none + + AT#XDFUAPPLY=0 + OK + #XDFU: 0,2,0 + + AT#XRESET + OK + Ready + +Read command +------------ + +The read command is not supported. + +Test command +------------ + +The test command tests the existence of the command and provides information about the type of its subparameters. + +Syntax +~~~~~~ + +.. code-block:: none + + #XDFUAPPLY=? + +Response syntax +~~~~~~~~~~~~~~~ + +.. code-block:: none + + #XDFUAPPLY: + +Example +~~~~~~~ + +.. code-block:: none + + AT#XDFUAPPLY=? + + #XDFUAPPLY: (0,1,2) + + OK + +DFU unsolicited notification #XDFU +================================== + +The |SM| application sends the ``#XDFU`` unsolicited notification to report the status of DFU write and apply operations. + +Unsolicited notification +------------------------ + +.. code-block:: none + + #XDFU: ,, + +* The ```` value is an integer indicating the DFU image type (``0``, ``1``, or ``2``). +* The ```` value is an integer indicating the operation: + + * ``1`` - Data write completed + * ``2`` - Apply update completed + +* The ```` value is ``0`` for success or ``-1`` for failure. + +Complete DFU examples +===================== + +The following sections showcase a complete example of application, delta modem, and full modem firmware update. + +Application firmware update +--------------------------- + +The following example shows a complete application firmware update: + +.. code-block:: none + + // Initialize DFU for application firmware (total size 123456 bytes) + AT#XDFUINIT=0,123456 + OK + + // Write first chunk (4096 bytes at offset 0) + AT#XDFUWRITE=0,0,4096 + OK + // 4096 bytes of firmware data + #XDFU: 0,1,0 + + // Write second chunk (4096 bytes at offset 4096) + AT#XDFUWRITE=0,4096,4096 + OK + // 4096 bytes of firmware data + #XDFU: 0,1,0 + + // ... continue writing chunks ... + + // Apply the update + AT#XDFUAPPLY=0 + OK + #XDFU: 0,2,0 + + // Reset to activate the new firmware + AT#XRESET + OK + Ready + +Delta modem firmware update +--------------------------- + +The following example shows a complete delta modem firmware update: + +.. code-block:: none + + // Initialize DFU for delta modem firmware (total size 65536 bytes) + AT#XDFUINIT=1,65536 + OK + + // Write first chunk (4096 bytes at offset 0) + AT#XDFUWRITE=1,0,4096 + OK + // 4096 bytes of firmware data + #XDFU: 1,1,0 + + // Write second chunk (4096 bytes at offset 4096) + AT#XDFUWRITE=1,4096,4096 + OK + // 4096 bytes of firmware data + #XDFU: 1,1,0 + + // ... continue writing chunks ... + + // Apply the update + AT#XDFUAPPLY=1 + OK + #XDFU: 1,2,0 + + // Reset the modem to activate the new firmware + AT#XMODEMRESET + #XMODEMRESET: 0 + OK + +Full modem firmware update +-------------------------- + +The following example shows a complete full modem firmware update. +For full modem firmware update, enable the :ref:`CONFIG_SM_DFU_MODEM_FULL ` Kconfig option. + +.. important:: + + If the update fails, the modem will not function until a successful update is performed. + The bootloader remains intact for retries. + Ensure a reliable connection between the host and serial modem, and stable power supply before starting the update. + +The full modem update consists of two phases: + +#. Bootloader segment - Writes the bootloader data. +#. Firmware segment - Writes the firmware data. + +.. code-block:: none + + // Initialize DFU for full modem firmware + AT#XDFUINIT=2 + // device reboots + Bootloader mode ready + + // Phase 1: Write bootloader segment + AT#XDFUWRITE=2,0,4096 + OK + // 4096 bytes of bootloader data + #XDFU: 2,1,0 + + // ... continue writing bootloader chunks ... + + // Apply bootloader segment (switches to firmware segment mode) + AT#XDFUAPPLY=2 + OK + #XDFU: 2,2,0 + + // Phase 2: Write firmware segments + // Warning: After the first firmware segment write, the modem will be corrupted if the update is not completed successfully. + AT#XDFUWRITE=2,0,4096 + OK + // 4096 bytes of firmware data + #XDFU: 2,1,0 + + // ... continue writing firmware chunks ... + + // Apply firmware segment (triggers reboot) + AT#XDFUAPPLY=2 + OK + #XDFU: 2,2,0 + // device reboots + Ready + +DFU host example +================ + +See ``app/scripts/sm_dfu_host.py`` for an example of how to implement a DFU host. The script demonstrates the DFU AT command flow and can be used as a starting point for custom implementations. + +Install dependencies: + +.. code-block:: bash + + pip install -r app/scripts/requirements.txt + +Usage +----- + +.. code-block:: bash + + python sm_dfu_host.py --port --baudrate --file --type + +Where ```` is one of: ``application``, ``modem-delta``, or ``modem-full``. + +After the DFU transfer completes: + +.. note:: + + Applying application and delta modem firmware can take some time. + +* For application updates, reset the device to activate the new firmware: + + .. code-block:: bash + + python sm_dfu_host.py --port --baudrate --reset + +* For delta modem updates, reset the modem to apply the update: + + .. code-block:: bash + + python sm_dfu_host.py --port --baudrate --modem-reset + +* Full modem updates handle the reset automatically. + +To check if the device is operating in normal mode: + +.. code-block:: bash + + python sm_dfu_host.py --port --baudrate --ping + +.. note:: + + Ping will return an error if the device is in bootloader mode. diff --git a/doc/app/sm_configuration.rst b/doc/app/sm_configuration.rst index 44376de2..15ca6f1e 100644 --- a/doc/app/sm_configuration.rst +++ b/doc/app/sm_configuration.rst @@ -163,6 +163,12 @@ CONFIG_SM_PGPS_INJECT_FIX_DATA - Injects the data obtained when acquiring a fix. In that case, this option should be disabled. The default value is ``y``. +.. _CONFIG_SM_DFU_MODEM_FULL: + +CONFIG_SM_DFU_MODEM_FULL - Enable full modem DFU + This option enables support for full modem firmware updates using the ``#XDFUINIT``, ``#XDFUWRITE``, and ``#XDFUAPPLY`` commands. + See the :ref:`DFU_AT_commands` for more information. + .. _sm_config_files: Configuration files