diff --git a/doc/connectivity/usb/device_next/api/index.rst b/doc/connectivity/usb/device_next/api/index.rst index 8468dedef3269..41ead6497102d 100644 --- a/doc/connectivity/usb/device_next/api/index.rst +++ b/doc/connectivity/usb/device_next/api/index.rst @@ -12,3 +12,4 @@ New USB device support APIs uac2_device.rst usbd_msc_device.rst usb_midi.rst + usbd_dfu.rst diff --git a/doc/connectivity/usb/device_next/api/usbd_dfu.rst b/doc/connectivity/usb/device_next/api/usbd_dfu.rst new file mode 100644 index 0000000000000..4059fca099f1a --- /dev/null +++ b/doc/connectivity/usb/device_next/api/usbd_dfu.rst @@ -0,0 +1,11 @@ +.. _usbd_dfu: + +USB DFU device API +################## + +USB DFU device specific API defined in :zephyr_file:`include/zephyr/usb/class/usbd_dfu.h`. + +API Reference +************* + +.. doxygengroup:: usbd_dfu diff --git a/include/zephyr/usb/class/usbd_dfu.h b/include/zephyr/usb/class/usbd_dfu.h new file mode 100644 index 0000000000000..ab8207fd5ba7e --- /dev/null +++ b/include/zephyr/usb/class/usbd_dfu.h @@ -0,0 +1,170 @@ +/* + * Copyright (c) 2024 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @file + * @brief USB Device Firmware Upgrade (DFU) public header + * + * Header exposes API for registering DFU images. + */ + +#ifndef ZEPHYR_INCLUDE_USB_CLASS_USBD_DFU_H +#define ZEPHYR_INCLUDE_USB_CLASS_USBD_DFU_H + +#include + +/* DFU Class Subclass */ +#define USB_DFU_SUBCLASS 0x01 + +/* DFU Class runtime Protocol */ +#define USB_DFU_PROTOCOL_RUNTIME 0x01 + +/* DFU Class DFU mode Protocol */ +#define USB_DFU_PROTOCOL_DFU 0x02 + +/* DFU Class Specific Requests */ +#define USB_DFU_REQ_DETACH 0x00 +#define USB_DFU_REQ_DNLOAD 0x01 +#define USB_DFU_REQ_UPLOAD 0x02 +#define USB_DFU_REQ_GETSTATUS 0x03 +#define USB_DFU_REQ_CLRSTATUS 0x04 +#define USB_DFU_REQ_GETSTATE 0x05 +#define USB_DFU_REQ_ABORT 0x06 + +/* Run-Time DFU Functional Descriptor */ +struct usb_dfu_descriptor { + uint8_t bLength; + uint8_t bDescriptorType; + uint8_t bmAttributes; + uint16_t wDetachTimeOut; + uint16_t wTransferSize; + uint16_t bcdDFUVersion; +} __packed; + +/* DFU Functional Descriptor Type */ +#define USB_DESC_DFU_FUNCTIONAL 0x21 + +/* DFU attributes DFU Functional Descriptor */ +#define USB_DFU_ATTR_WILL_DETACH BIT(3) +#define USB_DFU_ATTR_MANIFESTATION_TOLERANT BIT(2) +#define USB_DFU_ATTR_CAN_UPLOAD BIT(1) +#define USB_DFU_ATTR_CAN_DNLOAD BIT(0) + +/* DFU Specification release */ +#define USB_DFU_VERSION 0x0110 + +/* DFU device status */ +enum usb_dfu_status { + ERR_OK = 0x00, + ERR_TARGET = 0x01, + ERR_FILE = 0x02, + ERR_WRITE = 0x03, + ERR_ERASE = 0x04, + ERR_CHECK_ERASED = 0x05, + ERR_PROG = 0x06, + ERR_VERIFY = 0x07, + ERR_ADDRESS = 0x08, + ERR_NOTDONE = 0x09, + ERR_FIRMWARE = 0x0A, + ERR_VENDOR = 0x0B, + ERR_USBR = 0x0C, + ERR_POR = 0x0D, + ERR_UNKNOWN = 0x0E, + ERR_STALLEDPKT = 0x0F, +}; + +/* DFU device states */ +enum usb_dfu_state { + APP_IDLE = 0, + APP_DETACH = 1, + DFU_IDLE = 2, + DFU_DNLOAD_SYNC = 3, + DFU_DNBUSY = 4, + DFU_DNLOAD_IDLE = 5, + DFU_MANIFEST_SYNC = 6, + DFU_MANIFEST = 7, + DFU_MANIFEST_WAIT_RST = 8, + DFU_UPLOAD_IDLE = 9, + DFU_ERROR = 10, + DFU_STATE_MAX = 11, +}; + +struct usbd_dfu_image { + const char *name; + struct usb_if_descriptor *const if_desc; + void *const priv; + struct usbd_desc_node *const sd_nd; + bool (*next_cb)(void *const priv, + const enum usb_dfu_state state, const enum usb_dfu_state next); + int (*read_cb)(void *const priv, + const uint32_t block, const uint16_t size, + uint8_t buf[static CONFIG_USBD_DFU_TRANSFER_SIZE]); + int (*write_cb)(void *const priv, + const uint32_t block, const uint16_t size, + const uint8_t buf[static CONFIG_USBD_DFU_TRANSFER_SIZE]); +}; + +/** + * @brief USB DFU device update API + * @defgroup usbd_dfu USB DFU device update API + * @ingroup usb + * @{ + */ + +/** + * @brief Define USB DFU image + * + * Use this macro to create USB DFU image + * + * The callbacks must be in form: + * + * @code{.c} + * static int read(void *const priv, const uint32_t block, const uint16_t size, + * uint8_t buf[static CONFIG_USBD_DFU_TRANSFER_SIZE]) + * { + * int len; + * + * return len; + * } + * + * static int write(void *const priv, const uint32_t block, const uint16_t size, + * const uint8_t buf[static CONFIG_USBD_DFU_TRANSFER_SIZE]) + * { + * return 0; + * } + * + * static bool next(void *const priv, + * const enum usb_dfu_state state, const enum usb_dfu_state next) + * { + * return true; + * } + * @endcode + * + * @param id Identifier by which the linker sorts registered images + * @param iname Image name as used in interface descriptor + * @param iread Image read callback + * @param iwrite Image write callback + * @param inext Notify/confirm next state + */ +#define USBD_DFU_DEFINE_IMG(id, iname, ipriv, iread, iwrite, inext) \ + static __noinit struct usb_if_descriptor usbd_dfu_iface_##id; \ + \ + USBD_DESC_STRING_DEFINE(usbd_dfu_str_##id, iname, USBD_DUT_STRING_INTERFACE); \ + \ + static const STRUCT_SECTION_ITERABLE(usbd_dfu_image, usbd_dfu_image_##id) = { \ + .name = iname, \ + .if_desc = &usbd_dfu_iface_##id, \ + .priv = ipriv, \ + .sd_nd = &usbd_dfu_str_##id, \ + .read_cb = iread, \ + .write_cb = iwrite, \ + .next_cb = inext, \ + } + +/** + * @} + */ +#endif /* ZEPHYR_INCLUDE_USB_CLASS_USBD_DFU_H */ diff --git a/include/zephyr/usb/usbd.h b/include/zephyr/usb/usbd.h index b588d7087d282..619d926b14e17 100644 --- a/include/zephyr/usb/usbd.h +++ b/include/zephyr/usb/usbd.h @@ -841,14 +841,30 @@ int usbd_register_class(struct usbd_context *uds_ctx, * usbd_register_class for any device, configuration number, or instance, * either usbd_register_class or this function will fail. * + * There may be situations where a particular function should not be + * registered, for example, when using the USB DFU implementation, the DFU mode + * function must be excluded during normal device operation. To do this, the + * device can pass a blocklist in the form shown below as an optional argument. + * If the blocklist is not needed, the argument should be NULL. + * + * @code{.c} + * static const char *const blocklist[] = { + * "dfu_dfu", + * NULL, + * }; + * @endcode + * * @param[in] uds_ctx Pointer to USB device support context * @param[in] speed Configuration speed * @param[in] cfg Configuration value (bConfigurationValue) + * @param[in] blocklist Null pointer terminated array of pointers to string + * literals to be used as a block list * * @return 0 on success, other values on fail. */ int usbd_register_all_classes(struct usbd_context *uds_ctx, - const enum usbd_speed speed, uint8_t cfg); + const enum usbd_speed speed, uint8_t cfg, + const char *const blocklist[]); /** * @brief Unregister an USB class instance diff --git a/include/zephyr/usb/usbd_msg.h b/include/zephyr/usb/usbd_msg.h index b9e99d00c7065..2d81e0ef2f46d 100644 --- a/include/zephyr/usb/usbd_msg.h +++ b/include/zephyr/usb/usbd_msg.h @@ -52,6 +52,10 @@ enum usbd_msg_type { USBD_MSG_CDC_ACM_LINE_CODING, /** CDC ACM Line State update */ USBD_MSG_CDC_ACM_CONTROL_LINE_STATE, + /** USB DFU class detach request */ + USBD_MSG_DFU_APP_DETACH, + /** USB DFU class download completed */ + USBD_MSG_DFU_DOWNLOAD_COMPLETED, /** Maximum number of message types */ USBD_MSG_MAX_NUMBER, }; @@ -70,6 +74,8 @@ static const char *const usbd_msg_type_list[] = { "Stack error", "CDC ACM line coding", "CDC ACM control line state", + "DFU detach request", + "DFU download completed", }; BUILD_ASSERT(ARRAY_SIZE(usbd_msg_type_list) == USBD_MSG_MAX_NUMBER, diff --git a/samples/subsys/usb/common/sample_usbd_init.c b/samples/subsys/usb/common/sample_usbd_init.c index fb5b8e0dc3246..3162fd7dcb463 100644 --- a/samples/subsys/usb/common/sample_usbd_init.c +++ b/samples/subsys/usb/common/sample_usbd_init.c @@ -15,6 +15,12 @@ LOG_MODULE_REGISTER(usbd_sample_config); #define ZEPHYR_PROJECT_USB_VID 0x2fe3 +/* By default, do not register the USB DFU class DFU mode instance. */ +static const char *const blocklist[] = { + "dfu_dfu", + NULL, +}; + /* doc device instantiation start */ /* * Instantiate a context named sample_usbd using the default USB device @@ -124,7 +130,8 @@ struct usbd_context *sample_usbd_setup_device(usbd_msg_cb_t msg_cb) return NULL; } - err = usbd_register_all_classes(&sample_usbd, USBD_SPEED_HS, 1); + err = usbd_register_all_classes(&sample_usbd, USBD_SPEED_HS, 1, + blocklist); if (err) { LOG_ERR("Failed to add register classes"); return NULL; @@ -143,7 +150,7 @@ struct usbd_context *sample_usbd_setup_device(usbd_msg_cb_t msg_cb) /* doc configuration register end */ /* doc functions register start */ - err = usbd_register_all_classes(&sample_usbd, USBD_SPEED_FS, 1); + err = usbd_register_all_classes(&sample_usbd, USBD_SPEED_FS, 1, blocklist); if (err) { LOG_ERR("Failed to add register classes"); return NULL; diff --git a/samples/subsys/usb/dfu-next/CMakeLists.txt b/samples/subsys/usb/dfu-next/CMakeLists.txt new file mode 100644 index 0000000000000..defe46f998fb6 --- /dev/null +++ b/samples/subsys/usb/dfu-next/CMakeLists.txt @@ -0,0 +1,9 @@ +# SPDX-License-Identifier: Apache-2.0 + +cmake_minimum_required(VERSION 3.20.0) +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) +project(usb-dfu) + +include(${ZEPHYR_BASE}/samples/subsys/usb/common/common.cmake) +FILE(GLOB app_sources src/*.c) +target_sources(app PRIVATE ${app_sources}) diff --git a/samples/subsys/usb/dfu-next/Kconfig b/samples/subsys/usb/dfu-next/Kconfig new file mode 100644 index 0000000000000..44a54b057d3ff --- /dev/null +++ b/samples/subsys/usb/dfu-next/Kconfig @@ -0,0 +1,36 @@ +# Copyright (c) 2023 Nordic Semiconductor ASA +# SPDX-License-Identifier: Apache-2.0 + +menu "USB DFU sample options" + +config APP_USB_DFU_USE_FLASH_BACKEND + select FLASH + select FLASH_MAP + select STREAM_FLASH + select IMG_MANAGER + select IMG_ERASE_PROGRESSIVELY + bool "Option to clear the flash area before mounting" + help + Use this to force an existing file system to be created. + +if APP_USB_DFU_USE_FLASH_BACKEND + +config USBD_DFU_FLASH + default y + +config USBD_DFU_FLASH_SLOT0 + default n + +config USBD_DFU_FLASH_SLOT1 + default y + +endif + +endmenu + +# Source common USB sample options used to initialize new experimental USB +# device stack. The scope of these options is limited to USB samples in project +# tree, you cannot use them in your own application. +source "samples/subsys/usb/common/Kconfig.sample_usbd" + +source "Kconfig.zephyr" diff --git a/samples/subsys/usb/dfu-next/README.rst b/samples/subsys/usb/dfu-next/README.rst new file mode 100644 index 0000000000000..7d78fedc07900 --- /dev/null +++ b/samples/subsys/usb/dfu-next/README.rst @@ -0,0 +1,137 @@ +.. zephyr:code-sample:: dfu-next + :name: USB DFU + :relevant-api: usbd_api usbd_dfu + + Implement a basic USB DFU device + +Overview +******** + +This sample application demonstrates the USB DFU implementation using the +new experimental USB device stack. + +Requirements +************ + +This project requires an experimental USB device driver (UDC API) and uses the +:ref:`disk_access_api` and RAM-disk to download/upload the image. + +Building and Running +******************** + +This sample can be built for multiple boards, in this example we will build it +for the reel board: + +.. zephyr-app-commands:: + :zephyr-app: samples/subsys/usb/dfu-next + :board: reel_board + :goals: build flash + :compact: + +`dfu-util`_ tool can be used to download or upload the images. There are two +modes of operation in the USB DFU, runtime and DFU. The example starts in +runtime mode. To switch to DFU mode without uploading or downloading, the +following command can be used: + +.. code-block:: console + + dfu-util --detach + +Use the following command to upload the ``ramdisk0`` image to the host: + +.. code-block:: console + + dfu-util --alt 0 --upload ramdisk0_backup.bin + +Use the following command to download the ``ramdisk0`` image to the device: + +.. code-block:: console + + dfu-util --alt 0 --download ramdisk0_backup.bin + +Building with flash backend enabled +*********************************** + +The USB DFU device support has a built-in flash backend. This backend uses +:ref:`flash_img_api` and :ref:`flash_map_api` to write or read flash image, the +implementation is similar to the one we had in the previous USB DFU device +example. + +To use flash backend set the :kconfig:option:`CONFIG_APP_USB_DFU_USE_FLASH_BACKEND`. +An additional interface will be available in DFU mode to upload/download the +SLOT-1 image. + +It is also possible to try the sample together with the MCUboot bootloader +library. The following example shows how to build MCUboot and this sample with +flash backend and MCUboot support enabled using the :ref:`sysbuild`: + +.. zephyr-app-commands:: + :tool: west + :zephyr-app: samples/subsys/usb/dfu-next + :board: reel_board + :goals: build flash + :west-args: --sysbuild + :gen-args: -DSB_CONFIG_BOOTLOADER_MCUBOOT=y -DCONFIG_APP_USB_DFU_USE_FLASH_BACKEND=y + +Another application image is required to be used as a firmware update and +downloaded to SLOT-1. Build and sign a second application image e.g. +:zephyr:code-sample:`hello_world`, which will be used as an image for the +update. Do not forget to enable the required :kconfig:option:`CONFIG_BOOTLOADER_MCUBOOT` +option (as described in :ref:`mcuboot`). For example: + +.. zephyr-app-commands:: + :app: zephyr/samples/hello_world + :board: reel_board + :gen-args: -DCONFIG_MCUBOOT_SIGNATURE_KEY_FILE=\"bootloader/mcuboot/root-rsa-2048.pem\" -DCONFIG_BOOTLOADER_MCUBOOT=y + :goals: flash + +Use the following command to download new image to the device: + +.. code-block:: console + + dfu-util --alt 1 --download build/zephyr/zephyr.signed.bin + +Reset the SoC. MCUboot boot will swap the images and boot the new application, +showing this output to the console: + +.. code-block:: console + + *** Booting MCUboot v2.1.0-rc1-134-gb9d69dd2a2d6 *** + *** Using Zephyr OS build v3.7.0-4345-ga5d0d8533a41 *** + I: Starting bootloader + I: Primary image: magic=good, swap_type=0x4, copy_done=0x1, image_ok=0x1 + I: Secondary image: magic=good, swap_type=0x2, copy_done=0x3, image_ok=0x3 + I: Boot source: none + I: Image index: 0, Swap type: test + I: Starting swap using move algorithm. + I: Bootloader chainload address offset: 0xc000 + I: Image version: v0.0.0 + I: Jumping to the first image slot + *** Booting Zephyr OS build v3.7.0-4345-ga5d0d8533a41 *** + Hello World! reel_board@1/nrf52840 + + +Reset the SoC again and MCUboot should revert the images and boot +USB DFU sample, showing this output to the console: + +.. code-block:: console + + *** Booting MCUboot v2.1.0-rc1-134-gb9d69dd2a2d6 *** + *** Using Zephyr OS build v3.7.0-4345-ga5d0d8533a41 *** + I: Starting bootloader + I: Primary image: magic=good, swap_type=0x2, copy_done=0x1, image_ok=0x3 + I: Secondary image: magic=unset, swap_type=0x1, copy_done=0x3, image_ok=0x3 + I: Boot source: none + I: Image index: 0, Swap type: revert + I: Starting swap using move algorithm. + I: Secondary image: magic=unset, swap_type=0x1, copy_done=0x3, image_ok=0x3 + I: Bootloader chainload address offset: 0xc000 + I: Image version: v0.0.0 + I: Jumping to the first image slot + *** Booting Zephyr OS build v3.7.0-4345-ga5d0d8533a41 *** + [00:00:00.000,335] main: USBD message: VBUS ready + [00:00:00.000,427] main: USB DFU sample is initialized + + +.. _dfu-util: https://dfu-util.sourceforge.net/ +.. _Using MCUboot with Zephyr: https://docs.mcuboot.com/readme-zephyr diff --git a/samples/subsys/usb/dfu-next/app.overlay b/samples/subsys/usb/dfu-next/app.overlay new file mode 100644 index 0000000000000..ef18db35ccb42 --- /dev/null +++ b/samples/subsys/usb/dfu-next/app.overlay @@ -0,0 +1,14 @@ +/* + * Copyright (c) 2024 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/ { + ramdisk0 { + compatible = "zephyr,ram-disk"; + disk-name = "image0"; + sector-size = <512>; + sector-count = <32>; + }; +}; diff --git a/samples/subsys/usb/dfu-next/prj.conf b/samples/subsys/usb/dfu-next/prj.conf new file mode 100644 index 0000000000000..4ac6e6dd8d2ee --- /dev/null +++ b/samples/subsys/usb/dfu-next/prj.conf @@ -0,0 +1,10 @@ +CONFIG_USB_DEVICE_STACK_NEXT=y +CONFIG_USBD_DFU=y + +CONFIG_LOG=y +CONFIG_USBD_LOG_LEVEL_WRN=y +CONFIG_USBD_DFU_LOG_LEVEL_WRN=y +CONFIG_UDC_DRIVER_LOG_LEVEL_WRN=y +CONFIG_SAMPLE_USBD_PID=0x0005 + +CONFIG_DISK_ACCESS=y diff --git a/samples/subsys/usb/dfu-next/sample.yaml b/samples/subsys/usb/dfu-next/sample.yaml new file mode 100644 index 0000000000000..02b53e25d5362 --- /dev/null +++ b/samples/subsys/usb/dfu-next/sample.yaml @@ -0,0 +1,25 @@ +sample: + name: USB DFU sample +common: + min_ram: 64 + depends_on: + - usbd +tests: + sample.usbd.dfu: + integration_platforms: + - nrf52840dk/nrf52840 + - nrf54h20dk/nrf54h20/cpuapp + - frdm_k64f + - stm32f723e_disco + - nucleo_f413zh + - mimxrt685_evk/mimxrt685s/cm33 + - mimxrt1060_evk/mimxrt1062/qspi + tags: usb + sample.usbd.dfu-flash: + platform_allow: + - nrf52840dk/nrf52840 + - frdm_k64f + min_flash: 1024 + tags: usb + extra_configs: + - CONFIG_APP_USB_DFU_USE_FLASH_BACKEND=y diff --git a/samples/subsys/usb/dfu-next/src/main.c b/samples/subsys/usb/dfu-next/src/main.c new file mode 100644 index 0000000000000..81f8aa6c89c00 --- /dev/null +++ b/samples/subsys/usb/dfu-next/src/main.c @@ -0,0 +1,291 @@ +/* + * Copyright (c) 2024 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include + +#include +#include +#include +#include +#include + +#include +LOG_MODULE_REGISTER(main, LOG_LEVEL_INF); + +USBD_DEVICE_DEFINE(dfu_usbd, + DEVICE_DT_GET(DT_NODELABEL(zephyr_udc0)), + 0x2fe3, 0xffff); + +USBD_DESC_LANG_DEFINE(sample_lang); +USBD_DESC_CONFIG_DEFINE(fs_cfg_desc, "DFU FS Configuration"); +USBD_DESC_CONFIG_DEFINE(hs_cfg_desc, "DFU HS Configuration"); + +static const uint8_t attributes = (IS_ENABLED(CONFIG_SAMPLE_USBD_SELF_POWERED) ? + USB_SCD_SELF_POWERED : 0) | + (IS_ENABLED(CONFIG_SAMPLE_USBD_REMOTE_WAKEUP) ? + USB_SCD_REMOTE_WAKEUP : 0); +/* Full speed configuration */ +USBD_CONFIGURATION_DEFINE(sample_fs_config, + attributes, + CONFIG_SAMPLE_USBD_MAX_POWER, &fs_cfg_desc); + +/* High speed configuration */ +USBD_CONFIGURATION_DEFINE(sample_hs_config, + attributes, + CONFIG_SAMPLE_USBD_MAX_POWER, &hs_cfg_desc); + +static void switch_to_dfu_mode(struct usbd_context *const ctx); + +struct dfu_ramdisk_data { + const char *name; + uint32_t last_block; + uint32_t sector_size; + uint32_t sector_count; + union { + uint32_t uploaded; + uint32_t downloaded; + }; +}; + +static struct dfu_ramdisk_data ramdisk0_data = { + .name = "image0", +}; + +static int init_dfu_ramdisk_data(struct dfu_ramdisk_data *const data) +{ + int err; + + err = disk_access_init(data->name); + if (err) { + return err; + } + + err = disk_access_status(data->name); + if (err) { + return err; + } + + err = disk_access_ioctl(data->name, DISK_IOCTL_GET_SECTOR_COUNT, &data->sector_count); + if (err) { + return err; + } + + err = disk_access_ioctl(data->name, DISK_IOCTL_GET_SECTOR_SIZE, &data->sector_size); + if (err) { + return err; + } + + LOG_INF("disk %s sector count %u sector size %u", + data->name, data->sector_count, data->sector_size); + + return err; +} + +static int ramdisk_read(void *const priv, const uint32_t block, const uint16_t size, + uint8_t buf[static CONFIG_USBD_DFU_TRANSFER_SIZE]) +{ + struct dfu_ramdisk_data *const data = priv; + int err; + + if (size == 0) { + /* There is nothing to upload */ + return 0; + } + + if (block == 0) { + if (init_dfu_ramdisk_data(data)) { + LOG_ERR("Failed to init ramdisk data"); + return -EINVAL; + } + + data->last_block = 0; + data->uploaded = 0; + } else { + if (data->last_block + 1U != block) { + return -EINVAL; + } + + } + + if (block >= data->sector_count) { + /* Nothing to upload */ + return 0; + } + + err = disk_access_read(data->name, buf, block, 1); + if (err) { + LOG_ERR("Failed to read from RAMdisk"); + return err; + } + + data->last_block = block; + data->uploaded += MIN(size, data->sector_size); + LOG_INF("block %u size %u uploaded %u", block, size, data->uploaded); + + return size; +} + +static int ramdisk_write(void *const priv, const uint32_t block, const uint16_t size, + const uint8_t buf[static CONFIG_USBD_DFU_TRANSFER_SIZE]) +{ + struct dfu_ramdisk_data *const data = priv; + int err; + + if (block == 0) { + if (init_dfu_ramdisk_data(data)) { + LOG_ERR("Failed to init ramdisk data"); + return -EINVAL; + } + + data->last_block = 0; + data->downloaded = 0; + } else { + if (data->last_block + 1U != block) { + return -EINVAL; + } + + } + + if (size == 0) { + /* Nothing to write */ + return 0; + } + + err = disk_access_write(data->name, buf, block, 1); + if (err) { + LOG_ERR("Failed to write to RAMdisk"); + return err; + } + + data->last_block = block; + data->downloaded += size; + LOG_INF("block %u size %u downloaded %u", block, size, data->downloaded); + + return 0; +} + +USBD_DFU_DEFINE_IMG(ramdisk0, "ramdisk0", &ramdisk0_data, ramdisk_read, ramdisk_write, NULL); + +static void msg_cb(struct usbd_context *const usbd_ctx, + const struct usbd_msg *const msg) +{ + LOG_INF("USBD message: %s", usbd_msg_type_string(msg->type)); + + if (msg->type == USBD_MSG_CONFIGURATION) { + LOG_INF("\tConfiguration value %d", msg->status); + } + + if (usbd_can_detect_vbus(usbd_ctx)) { + if (msg->type == USBD_MSG_VBUS_READY) { + if (usbd_enable(usbd_ctx)) { + LOG_ERR("Failed to enable device support"); + } + } + + if (msg->type == USBD_MSG_VBUS_REMOVED) { + if (usbd_disable(usbd_ctx)) { + LOG_ERR("Failed to disable device support"); + } + } + } + + if (msg->type == USBD_MSG_DFU_APP_DETACH) { + switch_to_dfu_mode(usbd_ctx); + } + + if (msg->type == USBD_MSG_DFU_DOWNLOAD_COMPLETED) { + if (IS_ENABLED(CONFIG_BOOTLOADER_MCUBOOT) && + IS_ENABLED(CONFIG_APP_USB_DFU_USE_FLASH_BACKEND)) { + boot_request_upgrade(false); + } + } +} + +static void switch_to_dfu_mode(struct usbd_context *const ctx) +{ + int err; + + LOG_INF("Detach USB device"); + usbd_disable(ctx); + usbd_shutdown(ctx); + + err = usbd_add_descriptor(&dfu_usbd, &sample_lang); + if (err) { + LOG_ERR("Failed to initialize language descriptor (%d)", err); + return; + } + + if (usbd_caps_speed(&dfu_usbd) == USBD_SPEED_HS) { + err = usbd_add_configuration(&dfu_usbd, USBD_SPEED_HS, &sample_hs_config); + if (err) { + LOG_ERR("Failed to add High-Speed configuration"); + return; + } + + err = usbd_register_class(&dfu_usbd, "dfu_dfu", USBD_SPEED_HS, 1); + if (err) { + LOG_ERR("Failed to add register classes"); + return; + } + + usbd_device_set_code_triple(&dfu_usbd, USBD_SPEED_HS, 0, 0, 0); + } + + err = usbd_add_configuration(&dfu_usbd, USBD_SPEED_FS, &sample_fs_config); + if (err) { + LOG_ERR("Failed to add Full-Speed configuration"); + return; + } + + err = usbd_register_class(&dfu_usbd, "dfu_dfu", USBD_SPEED_FS, 1); + if (err) { + LOG_ERR("Failed to add register classes"); + return; + } + + usbd_device_set_code_triple(&dfu_usbd, USBD_SPEED_FS, 0, 0, 0); + + err = usbd_init(&dfu_usbd); + if (err) { + LOG_ERR("Failed to initialize USB device support"); + return; + } + + err = usbd_msg_register_cb(&dfu_usbd, msg_cb); + if (err) { + LOG_ERR("Failed to register message callback"); + return; + } + + err = usbd_enable(&dfu_usbd); + if (err) { + LOG_ERR("Failed to enable USB device support"); + } +} + +int main(void) +{ + struct usbd_context *sample_usbd; + int ret; + + sample_usbd = sample_usbd_init_device(msg_cb); + if (sample_usbd == NULL) { + LOG_ERR("Failed to initialize USB device"); + return -ENODEV; + } + + if (!usbd_can_detect_vbus(sample_usbd)) { + ret = usbd_enable(sample_usbd); + if (ret) { + LOG_ERR("Failed to enable device support"); + return ret; + } + } + + LOG_INF("USB DFU sample is initialized"); + + return 0; +} diff --git a/subsys/usb/device_next/CMakeLists.txt b/subsys/usb/device_next/CMakeLists.txt index d216cbe8f11c8..a93d32cd10bc5 100644 --- a/subsys/usb/device_next/CMakeLists.txt +++ b/subsys/usb/device_next/CMakeLists.txt @@ -82,4 +82,19 @@ zephyr_library_sources_ifdef( class/usbd_hid_api.c ) +zephyr_library_sources_ifdef( + CONFIG_USBD_DFU + class/usbd_dfu.c +) + +zephyr_library_sources_ifdef( + CONFIG_USBD_DFU_FLASH + class/usbd_dfu_flash.c +) + +zephyr_linker_sources_ifdef( + CONFIG_USBD_DFU + SECTIONS class/usbd_dfu.ld +) + zephyr_linker_sources(DATA_SECTIONS usbd_data.ld) diff --git a/subsys/usb/device_next/class/Kconfig b/subsys/usb/device_next/class/Kconfig index 4161474497256..fc188653565b3 100644 --- a/subsys/usb/device_next/class/Kconfig +++ b/subsys/usb/device_next/class/Kconfig @@ -11,3 +11,4 @@ rsource "Kconfig.msc" rsource "Kconfig.uac2" rsource "Kconfig.hid" rsource "Kconfig.midi2" +rsource "Kconfig.dfu" diff --git a/subsys/usb/device_next/class/Kconfig.dfu b/subsys/usb/device_next/class/Kconfig.dfu new file mode 100644 index 0000000000000..0ee0f8bad2aa8 --- /dev/null +++ b/subsys/usb/device_next/class/Kconfig.dfu @@ -0,0 +1,76 @@ +# Copyright (c) 2024 Nordic Semiconductor ASA +# SPDX-License-Identifier: Apache-2.0 + +menuconfig USBD_DFU + bool "USB DFU Class support" + help + USB DFU Class support + +if USBD_DFU + +config USBD_DFU_NUMOF_IMAGES + int "Number of possible DFU images" + range 1 256 + default 4 + help + Number of possible DFU images. + +config USBD_DFU_ENABLE_UPLOAD + bool "Allow images to be uploaded to the host" + default y + help + This option sets the bitCanUpload flag in the DFU attributes and + allows images to be uploaded to the host. + +config USBD_DFU_MANIFESTATION_TOLERANT + bool "Device is manifestation tolerant" + default y + help + This option sets the bitManifestationTolerant flag in the DFU + attributes and means that the device can communicate over USB after the + manifestation phase. + +config USBD_DFU_TRANSFER_SIZE + int "Maximum number of bytes the device can accept per transfer" + default 512 + range 64 1024 + help + This option sets the wTransferSize in the DFU functional descriptor. + +config USBD_DFU_POLLTIMEOUT + int "bwPollTimeout value (in ms)" + default 10 + range 0 1000 + help + This option sets the bwPollTimeout field in DFU_GETSTATUS response. + +module = USBD_DFU +module-str = usbd dfu +source "subsys/logging/Kconfig.template.log_config" + +config USBD_DFU_FLASH + depends on FLASH && FLASH_MAP && STREAM_FLASH + depends on IMG_MANAGER && IMG_ERASE_PROGRESSIVELY + bool "Built-in flash backend" + help + Enable the built-in flash backend, which can serve up to two image + slots, which is the common configuration of in-tree boards. + +if USBD_DFU_FLASH + +config USBD_DFU_FLASH_SLOT0 + bool "Flash backend for the slot-0 partition" + default y + help + This option enables download or upload for the slot-0 partition, if + one is defined. + +config USBD_DFU_FLASH_SLOT1 + bool "Flash backend for the slot-1 partition" + help + This option enables download or upload for the slot-1 partition, if + one is defined. + +endif # USBD_DFU_FLASH + +endif diff --git a/subsys/usb/device_next/class/usbd_dfu.c b/subsys/usb/device_next/class/usbd_dfu.c new file mode 100644 index 0000000000000..5317910d3050f --- /dev/null +++ b/subsys/usb/device_next/class/usbd_dfu.c @@ -0,0 +1,794 @@ +/* + * Copyright (c) 2024 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "usbd_msg.h" + +#include +#include +#include + +#include +LOG_MODULE_REGISTER(usbd_dfu, CONFIG_USBD_DFU_LOG_LEVEL); + +/* + * It is very unlikely that anyone would need more than one instance of the DFU + * class. Therefore, we make an exception here and do not support multiple + * instances, which allows us to have a much simpler implementation. + * + * This implementation provides two class instances, one with a single + * interface for the run-time mode, and the other with a number of user-defined + * interfaces for the DFU mode. The DFU mode instance can have up to 256 + * (0...255) image (memory) segments, limited by the + * CONFIG_USBD_DFU_NUMOF_IMAGES and maximum value of bAlternateSetting. + * + * The implementation implicitly sets the bitWillDetach flag and expects the + * user to disable the device with run-time mode and enable a device with DFU + * mode. + */ + +#if defined(CONFIG_USBD_DFU_ENABLE_UPLOAD) +#define ATTR_CAN_UPLOAD USB_DFU_ATTR_CAN_UPLOAD +#else +#define ATTR_CAN_UPLOAD 0 +#endif + +#if defined(CONFIG_USBD_DFU_MANIFESTATION_TOLERANT) +#define ATTR_MANIFESTATION_TOLERANT USB_DFU_ATTR_MANIFESTATION_TOLERANT +#else +#define ATTR_MANIFESTATION_TOLERANT 0 +#endif + +/* DFU Functional Descriptor used for Run-Time und DFU mode */ +static const struct usb_dfu_descriptor dfu_desc = { + .bLength = sizeof(struct usb_dfu_descriptor), + .bDescriptorType = USB_DESC_DFU_FUNCTIONAL, + .bmAttributes = USB_DFU_ATTR_CAN_DNLOAD | + ATTR_CAN_UPLOAD | ATTR_MANIFESTATION_TOLERANT | + USB_DFU_ATTR_WILL_DETACH, + .wDetachTimeOut = 0, + .wTransferSize = sys_cpu_to_le16(CONFIG_USBD_DFU_TRANSFER_SIZE), + .bcdDFUVersion = sys_cpu_to_le16(USB_DFU_VERSION), +}; + +/* Common class data for both run-time and DFU instances. */ +struct usbd_dfu_data { + struct usb_desc_header **const runtime_mode_descs; + struct usb_desc_header **const dfu_mode_descs; + enum usb_dfu_state state; + enum usb_dfu_state next; + enum usb_dfu_status status; + struct k_work_delayable dwork; + struct usbd_context *ctx; + bool dfu_mode; + struct usbd_dfu_image *image; + uint8_t alternate; +}; + +/* Run-Time mode interface descriptor */ +static __noinit struct usb_if_descriptor runtime_if0_desc; + +/* Run-Time mode descriptors. No endpoints, identical for high and full speed. */ +static struct usb_desc_header *runtime_mode_descs[] = { + (struct usb_desc_header *) &runtime_if0_desc, + (struct usb_desc_header *) &dfu_desc, + NULL, +}; + +/* + * DFU mode descriptors with two reserved indices for functional descriptor and + * at least one for NULL. No endpoints, identical for high and full speed. + */ +static struct usb_desc_header *dfu_mode_descs[CONFIG_USBD_DFU_NUMOF_IMAGES + 2]; + +static struct usbd_dfu_data dfu_data = { + .runtime_mode_descs = runtime_mode_descs, + .dfu_mode_descs = dfu_mode_descs, +}; + +static const char *const dfu_state_list[] = { + "APP_IDLE", + "APP_DETACH", + "DFU_IDLE", + "DNLOAD_SYNC", + "DNBUSY", + "DNLOAD_IDLE", + "MANIFEST_SYNC", + "MANIFEST", + "MANIFEST_WAIT_RST", + "UPLOAD_IDLE", + "ERROR", +}; + +static const char *const dfu_req_list[] = { + "DETACH", + "DNLOAD", + "UPLOAD", + "GETSTATUS", + "CLRSTATUS", + "GETSTATE", + "ABORT", +}; + +BUILD_ASSERT(ARRAY_SIZE(dfu_state_list) == DFU_STATE_MAX, + "Number of entries in dfu_state_list is not equal to DFU_STATE_MAX"); + +BUILD_ASSERT(ARRAY_SIZE(dfu_req_list) == USB_DFU_REQ_ABORT + 1, + "Number of entries in dfu_req_list is not equal to USB_DFU_REQ_ABORT + 1"); + +static const char *dfu_state_string(const enum usb_dfu_state state) +{ + if (state >= 0 && state < DFU_STATE_MAX) { + return dfu_state_list[state]; + } + + return "?"; +} + +static const char *dfu_req_string(const enum usb_dfu_state state) +{ + if (state >= 0 && state <= USB_DFU_REQ_ABORT) { + return dfu_req_list[state]; + } + + return "?"; +} + +static void runtime_detach_work(struct k_work *work) +{ + struct k_work_delayable *dwork = k_work_delayable_from_work(work); + struct usbd_dfu_data *data = CONTAINER_OF(dwork, struct usbd_dfu_data, dwork); + + usbd_msg_pub_simple(data->ctx, USBD_MSG_DFU_APP_DETACH, 0); +} + +static void init_if_desc(struct usb_if_descriptor *const desc, + const uint8_t alternate, const uint8_t protocol) +{ + desc->bLength = sizeof(struct usb_if_descriptor); + desc->bDescriptorType = USB_DESC_INTERFACE; + desc->bInterfaceNumber = 0; + desc->bAlternateSetting = alternate; + desc->bNumEndpoints = 0; + desc->bInterfaceClass = USB_BCC_APPLICATION; + desc->bInterfaceSubClass = USB_DFU_SUBCLASS; + desc->bInterfaceProtocol = protocol; + desc->iInterface = 0; +} + +static int usbd_dfu_preinit(void) +{ + struct usb_if_descriptor *if_desc; + int n = 0; + + init_if_desc(&runtime_if0_desc, 0, USB_DFU_PROTOCOL_RUNTIME); + + STRUCT_SECTION_FOREACH(usbd_dfu_image, image) { + if (n >= CONFIG_USBD_DFU_NUMOF_IMAGES) { + LOG_ERR("Cannot register USB DFU image %s", image->name); + return -ENOMEM; + } + + if_desc = image->if_desc; + init_if_desc(if_desc, n, USB_DFU_PROTOCOL_DFU); + dfu_mode_descs[n] = (struct usb_desc_header *)if_desc; + n++; + } + + dfu_mode_descs[n] = (struct usb_desc_header *)&dfu_desc; + + k_work_init_delayable(&dfu_data.dwork, runtime_detach_work); + + return 0; +} + +/* + * Perhaps it makes sense to implement an on_registration class interface + * callback and not use SYS_INIT(). + */ +SYS_INIT(usbd_dfu_preinit, POST_KERNEL, CONFIG_KERNEL_INIT_PRIORITY_DEVICE); + +/* + * This function is used for two purposes, to inform the image backend about + * the next step and in some cases to get feedback if the next step is possible + * from the image perspective. + */ +static inline bool usbd_dfu_image_next(struct usbd_class_data *const c_data, + const enum usb_dfu_state next) +{ + struct usbd_dfu_data *data = usbd_class_get_private(c_data); + struct usbd_dfu_image *const image = data->image; + + if (image->next_cb != NULL) { + return image->next_cb(image->priv, data->state, next); + } + + return true; +} + +static ALWAYS_INLINE void dfu_error(struct usbd_class_data *const c_data, + const enum usb_dfu_state next, + const enum usb_dfu_status status) +{ + struct usbd_dfu_data *data = usbd_class_get_private(c_data); + + data->next = next; + data->status = status; +} + +/* + * Because some states (e.g. APP_IDLE, APP_DETACH) require a stall handshake to + * be sent, but the state does not change to DFU_ERROR, there are some "return + * -ENOTSUP" without state change to indicate a protocol error. + */ + +static int app_idle_next(struct usbd_class_data *const c_data, + const struct usb_setup_packet *const setup) +{ + struct usbd_dfu_data *data = usbd_class_get_private(c_data); + + switch (setup->bRequest) { + case USB_DFU_REQ_DETACH: + data->next = APP_DETACH; + return 0; + case USB_DFU_REQ_GETSTATUS: + __fallthrough; + case USB_DFU_REQ_GETSTATE: + return 0; + default: + return -ENOTSUP; + } +} + +static int app_detach_next(struct usbd_class_data *const c_data, + const struct usb_setup_packet *const setup) +{ + switch (setup->bRequest) { + case USB_DFU_REQ_GETSTATUS: + __fallthrough; + case USB_DFU_REQ_GETSTATE: + return 0; + default: + return -ENOTSUP; + } +} + +static int dfu_idle_next(struct usbd_class_data *const c_data, + const struct usb_setup_packet *const setup) +{ + struct usbd_dfu_data *data = usbd_class_get_private(c_data); + + switch (setup->bRequest) { + case USB_DFU_REQ_DNLOAD: + if (!(dfu_desc.bmAttributes & USB_DFU_ATTR_CAN_DNLOAD)) { + dfu_error(c_data, DFU_ERROR, ERR_STALLEDPKT); + return -ENOTSUP; + } + + if (data->image == NULL || data->image->write_cb == NULL) { + dfu_error(c_data, DFU_ERROR, ERR_VENDOR); + return -ENOTSUP; + } + + if (setup->wLength == 0) { + dfu_error(c_data, DFU_ERROR, ERR_STALLEDPKT); + return -ENOTSUP; + } + + data->next = DFU_DNLOAD_SYNC; + return 0; + case USB_DFU_REQ_UPLOAD: + if (!(dfu_desc.bmAttributes & USB_DFU_ATTR_CAN_UPLOAD)) { + dfu_error(c_data, DFU_ERROR, ERR_STALLEDPKT); + return -ENOTSUP; + } + + if (data->image == NULL || data->image->read_cb == NULL) { + dfu_error(c_data, DFU_ERROR, ERR_VENDOR); + return -ENOTSUP; + } + + if (setup->wLength > sys_le16_to_cpu(dfu_desc.wTransferSize)) { + dfu_error(c_data, DFU_ERROR, ERR_STALLEDPKT); + return -ENOTSUP; + } + + data->next = DFU_UPLOAD_IDLE; + return 0; + case USB_DFU_REQ_ABORT: + __fallthrough; + case USB_DFU_REQ_GETSTATUS: + __fallthrough; + case USB_DFU_REQ_GETSTATE: + return 0; + default: + dfu_error(c_data, DFU_ERROR, ERR_STALLEDPKT); + return -ENOTSUP; + } +} + +static int dfu_dnload_sync_next(struct usbd_class_data *const c_data, + const struct usb_setup_packet *const setup) +{ + struct usbd_dfu_data *data = usbd_class_get_private(c_data); + + switch (setup->bRequest) { + case USB_DFU_REQ_GETSTATUS: + /* Chack if image backend can change DFU_DNLOAD_SYNC -> DFU_DNLOAD_IDLE */ + if (usbd_dfu_image_next(c_data, DFU_DNLOAD_IDLE)) { + data->next = DFU_DNLOAD_IDLE; + } else { + data->next = DFU_DNBUSY; + } + + return 0; + case USB_DFU_REQ_GETSTATE: + return 0; + default: + dfu_error(c_data, DFU_ERROR, ERR_STALLEDPKT); + return -ENOTSUP; + } +} + +static int dfu_dnbusy_next(struct usbd_class_data *const c_data, + const struct usb_setup_packet *const setup) +{ + struct usbd_dfu_data *data = usbd_class_get_private(c_data); + + /* Do not enforce bmPollTimeout (allow GET_STATUS immediately) */ + data->state = DFU_DNLOAD_SYNC; + + return dfu_dnload_sync_next(c_data, setup); +} + +static int dfu_dnload_idle_next(struct usbd_class_data *const c_data, + const struct usb_setup_packet *const setup) +{ + struct usbd_dfu_data *data = usbd_class_get_private(c_data); + + switch (setup->bRequest) { + case USB_DFU_REQ_DNLOAD: + if (setup->wLength == 0) { + data->next = DFU_MANIFEST_SYNC; + } else { + data->next = DFU_DNLOAD_SYNC; + } + + return 0; + case USB_DFU_REQ_ABORT: + data->next = DFU_IDLE; + /* Notify image backend about DFU_DNLOAD_IDLE -> DFU_IDLE change */ + usbd_dfu_image_next(c_data, data->next); + case USB_DFU_REQ_GETSTATUS: + __fallthrough; + case USB_DFU_REQ_GETSTATE: + return 0; + default: + dfu_error(c_data, DFU_ERROR, ERR_STALLEDPKT); + return -ENOTSUP; + } +} + +static int dfu_manifest_sync_next(struct usbd_class_data *const c_data, + const struct usb_setup_packet *const setup) +{ + struct usbd_dfu_data *data = usbd_class_get_private(c_data); + + switch (setup->bRequest) { + case USB_DFU_REQ_GETSTATUS: + if (usbd_dfu_image_next(c_data, DFU_IDLE)) { + data->next = DFU_IDLE; + usbd_msg_pub_simple(data->ctx, USBD_MSG_DFU_DOWNLOAD_COMPLETED, 0); + } else { + data->next = DFU_MANIFEST; + } + case USB_DFU_REQ_GETSTATE: + return 0; + default: + dfu_error(c_data, DFU_ERROR, ERR_STALLEDPKT); + return -ENOTSUP; + } + + return 0; +} + +static int dfu_manifest_next(struct usbd_class_data *const c_data, + const struct usb_setup_packet *const setup) +{ + struct usbd_dfu_data *data = usbd_class_get_private(c_data); + + /* Ignore poll timeout, proceed directly to next state. */ + + if (dfu_desc.bmAttributes & USB_DFU_ATTR_MANIFESTATION_TOLERANT) { + data->state = DFU_MANIFEST_SYNC; + return dfu_manifest_sync_next(c_data, setup); + } + + data->next = DFU_MANIFEST_WAIT_RST; + usbd_dfu_image_next(c_data, DFU_MANIFEST_WAIT_RST); + + return 0; +} + +static int dfu_manifest_wait_rst_next(struct usbd_class_data *const c_data, + const struct usb_setup_packet *const setup) +{ + /* Ignore all requests, wait for system or bus reset */ + + return 0; +} + +static int dfu_upload_idle_next(struct usbd_class_data *const c_data, + const struct usb_setup_packet *const setup) +{ + struct usbd_dfu_data *data = usbd_class_get_private(c_data); + + switch (setup->bRequest) { + case USB_DFU_REQ_UPLOAD: + if (setup->wLength > sys_le16_to_cpu(dfu_desc.wTransferSize)) { + dfu_error(c_data, DFU_ERROR, ERR_STALLEDPKT); + return -ENOTSUP; + } + + data->next = DFU_UPLOAD_IDLE; + return 0; + case USB_DFU_REQ_ABORT: + data->next = DFU_IDLE; + /* Notify image backend about DFU_UPLOAD_IDLE -> DFU_IDLE change */ + usbd_dfu_image_next(c_data, data->next); + case USB_DFU_REQ_GETSTATUS: + __fallthrough; + case USB_DFU_REQ_GETSTATE: + return 0; + default: + dfu_error(c_data, DFU_ERROR, ERR_STALLEDPKT); + return -ENOTSUP; + } +} + +static int dfu_error_next(struct usbd_class_data *const c_data, + const struct usb_setup_packet *const setup) +{ + switch (setup->bRequest) { + case USB_DFU_REQ_GETSTATUS: + __fallthrough; + case USB_DFU_REQ_GETSTATE: + return 0; + case USB_DFU_REQ_CLRSTATUS: + dfu_error(c_data, DFU_IDLE, ERR_OK); + return 0; + default: + return -ENOTSUP; + } +} + +static int (*next_entries[])(struct usbd_class_data *const c_data, + const struct usb_setup_packet *const setup) = { + app_idle_next, + app_detach_next, + dfu_idle_next, + dfu_dnload_sync_next, + dfu_dnbusy_next, + dfu_dnload_idle_next, + dfu_manifest_sync_next, + dfu_manifest_next, + dfu_manifest_wait_rst_next, + dfu_upload_idle_next, + dfu_error_next, +}; + +BUILD_ASSERT(ARRAY_SIZE(next_entries) == DFU_STATE_MAX, + "Number of entries in next_entries is not equal to DFU_STATE_MAX"); + +/* + * Here we only set the next state based on the current state, image state and + * the new request. We do not copy/move any data and we do not update DFU state. + * + * The state change and additional actions are performed in four places, in the + * host/device requests in runtime mode and in the host/device request in DFU + * mode. + */ +static int dfu_set_next_state(struct usbd_class_data *const c_data, + const struct usb_setup_packet *const setup) +{ + struct usbd_dfu_data *data = usbd_class_get_private(c_data); + int err; + + if (setup->RequestType.type != USB_REQTYPE_TYPE_CLASS) { + return -ENOTSUP; + } + + if (setup->bRequest >= ARRAY_SIZE(next_entries)) { + return -ENOTSUP; + } + + data->next = data->state; + err = next_entries[data->state](c_data, setup); + + LOG_DBG("bRequest %s, state %s, next %s, error %d", + dfu_req_string(setup->bRequest), dfu_state_string(data->state), + dfu_state_string(data->next), err); + + return err; +} + +/* Run-Time mode instance implementation, for instance "dfu_runtime" */ + +static int handle_get_status(struct usbd_class_data *const c_data, + const struct usb_setup_packet *const setup, + struct net_buf *const buf) +{ + struct usbd_dfu_data *data = usbd_class_get_private(c_data); + size_t len = MIN(setup->wLength, net_buf_tailroom(buf)); + const size_t getstatus_len = 6; + + if (len != getstatus_len) { + errno = -ENOTSUP; + return 0; + } + + /* + * Add GET_STATUS response consisting of + * bStatus, bwPollTimeout, bStatus, iString (no strings defined) + */ + net_buf_add_u8(buf, data->status); + net_buf_add_le16(buf, CONFIG_USBD_DFU_POLLTIMEOUT); + net_buf_add_u8(buf, 0); + net_buf_add_u8(buf, data->state); + net_buf_add_u8(buf, 0); + + return 0; +} + +static int handle_get_state(struct usbd_class_data *const c_data, + const struct usb_setup_packet *const setup, + struct net_buf *const buf) +{ + struct usbd_dfu_data *data = usbd_class_get_private(c_data); + size_t len = MIN(setup->wLength, net_buf_tailroom(buf)); + const size_t getstate_len = 1; + + if (len != getstate_len) { + errno = -ENOTSUP; + return 0; + } + + net_buf_add_u8(buf, data->state); + + return 0; +} + +static int runtime_mode_control_to_host(struct usbd_class_data *const c_data, + const struct usb_setup_packet *const setup, + struct net_buf *const buf) +{ + struct usbd_dfu_data *data = usbd_class_get_private(c_data); + + errno = dfu_set_next_state(c_data, setup); + + if (errno == 0) { + switch (setup->bRequest) { + case USB_DFU_REQ_GETSTATUS: + errno = handle_get_status(c_data, setup, buf); + break; + case USB_DFU_REQ_GETSTATE: + errno = handle_get_state(c_data, setup, buf); + break; + default: + break; + } + } + + data->state = data->next; + + return 0; +} + +static int runtime_mode_control_to_dev(struct usbd_class_data *const c_data, + const struct usb_setup_packet *const setup, + const struct net_buf *const buf) +{ + struct usbd_dfu_data *data = usbd_class_get_private(c_data); + + errno = dfu_set_next_state(c_data, setup); + + if (errno == 0) { + if (setup->bRequest == USB_DFU_REQ_DETACH) { + k_work_reschedule(&data->dwork, K_MSEC(100)); + } + } + + data->state = data->next; + + return 0; +} + +static void *runtime_mode_get_desc(struct usbd_class_data *const c_data, + const enum usbd_speed speed) +{ + struct usbd_dfu_data *data = usbd_class_get_private(c_data); + + return data->runtime_mode_descs; +} + +static int runtime_mode_init(struct usbd_class_data *const c_data) +{ + struct usbd_dfu_data *data = usbd_class_get_private(c_data); + + LOG_DBG("Init class instance %p", c_data); + data->dfu_mode = false; + data->alternate = 0; + data->state = APP_IDLE; + data->next = APP_IDLE; + data->image = NULL; + data->ctx = usbd_class_get_ctx(c_data); + + return 0; +} + +struct usbd_class_api runtime_mode_api = { + .control_to_host = runtime_mode_control_to_host, + .control_to_dev = runtime_mode_control_to_dev, + .get_desc = runtime_mode_get_desc, + .init = runtime_mode_init, +}; + +USBD_DEFINE_CLASS(dfu_runtime, &runtime_mode_api, &dfu_data, NULL); + +/* DFU mode instance implementation, for instance "dfu_dfu" */ + +static int handle_upload(struct usbd_class_data *const c_data, + const struct usb_setup_packet *const setup, + struct net_buf *const buf) +{ + struct usbd_dfu_data *data = usbd_class_get_private(c_data); + uint16_t size = MIN(setup->wLength, net_buf_tailroom(buf)); + struct usbd_dfu_image *const image = data->image; + int ret; + + ret = image->read_cb(image->priv, setup->wValue, size, buf->data); + if (ret >= 0) { + net_buf_add(buf, ret); + if (ret < sys_le16_to_cpu(dfu_desc.wTransferSize)) { + data->state = DFU_IDLE; + } + } else { + errno = -ENOTSUP; + dfu_error(c_data, DFU_ERROR, ERR_UNKNOWN); + } + + return 0; +} + +static int handle_download(struct usbd_class_data *const c_data, + const struct usb_setup_packet *const setup, + const struct net_buf *const buf) +{ + struct usbd_dfu_data *data = usbd_class_get_private(c_data); + struct usbd_dfu_image *const image = data->image; + uint16_t size = MIN(setup->wLength, buf->len); + int ret; + + ret = image->write_cb(image->priv, setup->wValue, size, buf->data); + if (ret < 0) { + errno = -ENOTSUP; + dfu_error(c_data, DFU_ERROR, ERR_UNKNOWN); + } + + return 0; +} + +static int dfu_mode_control_to_host(struct usbd_class_data *const c_data, + const struct usb_setup_packet *const setup, + struct net_buf *const buf) +{ + struct usbd_dfu_data *data = usbd_class_get_private(c_data); + + errno = dfu_set_next_state(c_data, setup); + + if (errno == 0) { + switch (setup->bRequest) { + case USB_DFU_REQ_GETSTATUS: + errno = handle_get_status(c_data, setup, buf); + break; + case USB_DFU_REQ_GETSTATE: + errno = handle_get_state(c_data, setup, buf); + break; + case USB_DFU_REQ_UPLOAD: + errno = handle_upload(c_data, setup, buf); + break; + default: + break; + } + } + + data->state = data->next; + + return 0; +} + +static int dfu_mode_control_to_dev(struct usbd_class_data *const c_data, + const struct usb_setup_packet *const setup, + const struct net_buf *const buf) +{ + struct usbd_dfu_data *data = usbd_class_get_private(c_data); + + errno = dfu_set_next_state(c_data, setup); + + if (errno == 0) { + if (setup->bRequest == USB_DFU_REQ_DNLOAD) { + handle_download(c_data, setup, buf); + } + } + + data->state = data->next; + + return 0; +} + +static void dfu_mode_update(struct usbd_class_data *const c_data, + const uint8_t iface, const uint8_t alternate) +{ + struct usbd_dfu_data *data = usbd_class_get_private(c_data); + + LOG_DBG("Instance %p, interface %u alternate %u changed", + c_data, iface, alternate); + + data->alternate = alternate; + data->image = NULL; + + STRUCT_SECTION_FOREACH(usbd_dfu_image, image) { + if (image->if_desc->bAlternateSetting == alternate) { + data->image = image; + break; + } + } +} + +static void *dfu_mode_get_desc(struct usbd_class_data *const c_data, + const enum usbd_speed speed) +{ + struct usbd_dfu_data *data = usbd_class_get_private(c_data); + + return data->dfu_mode_descs; +} + +static int dfu_mode_init(struct usbd_class_data *const c_data) +{ + struct usbd_dfu_data *data = usbd_class_get_private(c_data); + struct usbd_context *uds_ctx = usbd_class_get_ctx(c_data); + + LOG_DBG("Init class instance %p", c_data); + data->dfu_mode = true; + data->alternate = 0; + data->image = NULL; + data->state = DFU_IDLE; + data->next = DFU_IDLE; + data->ctx = usbd_class_get_ctx(c_data); + + STRUCT_SECTION_FOREACH(usbd_dfu_image, image) { + if (image->if_desc->bAlternateSetting == data->alternate) { + data->image = image; + } + + if (usbd_add_descriptor(uds_ctx, image->sd_nd)) { + LOG_ERR("Failed to add string descriptor"); + } else { + image->if_desc->iInterface = usbd_str_desc_get_idx(image->sd_nd); + } + } + + return data->image == NULL ? -EINVAL : 0; +} + +struct usbd_class_api dfu_api = { + .control_to_host = dfu_mode_control_to_host, + .control_to_dev = dfu_mode_control_to_dev, + .update = dfu_mode_update, + .get_desc = dfu_mode_get_desc, + .init = dfu_mode_init, +}; + +USBD_DEFINE_CLASS(dfu_dfu, &dfu_api, &dfu_data, NULL); diff --git a/subsys/usb/device_next/class/usbd_dfu.ld b/subsys/usb/device_next/class/usbd_dfu.ld new file mode 100644 index 0000000000000..cff3074e98d2b --- /dev/null +++ b/subsys/usb/device_next/class/usbd_dfu.ld @@ -0,0 +1,3 @@ +#include + +ITERABLE_SECTION_ROM(usbd_dfu_image, Z_LINK_ITERABLE_SUBALIGN) diff --git a/subsys/usb/device_next/class/usbd_dfu_flash.c b/subsys/usb/device_next/class/usbd_dfu_flash.c new file mode 100644 index 0000000000000..fea438bf3b26a --- /dev/null +++ b/subsys/usb/device_next/class/usbd_dfu_flash.c @@ -0,0 +1,155 @@ +/* + * Copyright (c) 2024 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "usbd_msg.h" + +#include +#include +#include +#include +#include +#include + +#include +LOG_MODULE_REGISTER(dfu_flash, CONFIG_USBD_DFU_LOG_LEVEL); + +/* + * This file implements the flash backend for the USB DFU implementation. The + * flash backend can serve up to two image slots, which are typically defined + * for the in-tree boards in the Zephyr project. + */ + +struct usbd_dfu_flash_data { + struct flash_img_context fi_ctx; + uint32_t last_block; + const uint8_t id; + union { + uint32_t uploaded; + uint32_t downloaded; + }; +}; + +static int dfu_flash_read(void *const priv, + const uint32_t block, const uint16_t size, + uint8_t buf[static CONFIG_USBD_DFU_TRANSFER_SIZE]) +{ + struct usbd_dfu_flash_data *const data = priv; + const struct flash_area *fa; + uint32_t to_upload; + int len; + int ret; + + if (size == 0) { + /* There is nothing to upload */ + return 0; + } + + if (block == 0) { + data->last_block = 0; + data->uploaded = 0; + } else { + if (data->last_block + 1U != block) { + return -EINVAL; + } + + } + + ret = flash_area_open(data->id, &fa); + if (ret) { + return ret; + } + + if (block == 0) { + LOG_DBG("Flash area size %u", fa->fa_size); + } + + to_upload = fa->fa_size - data->uploaded; + if (to_upload < size) { + len = to_upload; + } else { + len = size; + } + + ret = flash_area_read(fa, data->uploaded, buf, len); + flash_area_close(fa); + if (ret) { + return ret; + } + + data->last_block = block; + data->uploaded += size; + LOG_DBG("uploaded %u block %u len %u", data->uploaded, block, len); + + return len; +} + +static int dfu_flash_write(void *const priv, + const uint32_t block, const uint16_t size, + const uint8_t buf[static CONFIG_USBD_DFU_TRANSFER_SIZE]) +{ + struct usbd_dfu_flash_data *const data = priv; + const bool flush = size == 0 ? true : false; + int ret; + + if (block == 0) { + if (flash_img_init(&data->fi_ctx)) { + return -EINVAL; + } + + data->last_block = 0; + data->downloaded = 0; + + if (size == 0) { + /* There is nothing to download */ + return 0; + } + } else { + if (data->last_block + 1U != block) { + return -EINVAL; + } + + } + + ret = flash_img_buffered_write(&data->fi_ctx, buf, size, flush); + if (ret) { + return ret; + } + + data->last_block = block; + data->downloaded += size; + LOG_DBG("downloaded %u (%u) block %u size %u", data->downloaded, + flash_img_bytes_written(&data->fi_ctx), block, size); + + return 0; +} + +static bool dfu_flash_next(void *const priv, + const enum usb_dfu_state state, const enum usb_dfu_state next) +{ + if (state == DFU_MANIFEST_SYNC && next == DFU_IDLE) { + LOG_DBG("Download finished"); + } + + return true; +} + +#if FIXED_PARTITION_EXISTS(slot0_partition) && defined(CONFIG_USBD_DFU_FLASH_SLOT0) +static struct usbd_dfu_flash_data slot0_data = { + .id = FIXED_PARTITION_ID(slot0_partition), +}; + +USBD_DFU_DEFINE_IMG(slot0_image, "slot0_image", &slot0_data, + dfu_flash_read, dfu_flash_write, dfu_flash_next); +#endif + +#if FIXED_PARTITION_EXISTS(slot1_partition) && defined(CONFIG_USBD_DFU_FLASH_SLOT1) +static struct usbd_dfu_flash_data slot1_data = { + .id = FIXED_PARTITION_ID(slot1_partition), +}; + +USBD_DFU_DEFINE_IMG(slot1_image, "slot1_image", &slot1_data, + dfu_flash_read, dfu_flash_write, dfu_flash_next); +#endif diff --git a/subsys/usb/device_next/usbd_class.c b/subsys/usb/device_next/usbd_class.c index 465480bd0e3fe..eeb2cfa5a20a3 100644 --- a/subsys/usb/device_next/usbd_class.c +++ b/subsys/usb/device_next/usbd_class.c @@ -340,13 +340,30 @@ int usbd_register_class(struct usbd_context *const uds_ctx, return ret; } +static bool is_blocklisted(const struct usbd_class_node *const c_nd, + const char *const list[]) +{ + for (int i = 0; list[i] != NULL; i++) { + if (strcmp(c_nd->c_data->name, list[i]) == 0) { + return true; + } + } + + return false; +} + int usbd_register_all_classes(struct usbd_context *const uds_ctx, - const enum usbd_speed speed, const uint8_t cfg) + const enum usbd_speed speed, const uint8_t cfg, + const char *const blocklist[]) { int ret; if (speed == USBD_SPEED_HS) { STRUCT_SECTION_FOREACH_ALTERNATE(usbd_class_hs, usbd_class_node, c_nd) { + if (blocklist != NULL && is_blocklisted(c_nd, blocklist)) { + continue; + } + ret = usbd_register_class(uds_ctx, c_nd->c_data->name, speed, cfg); if (ret) { @@ -361,6 +378,10 @@ int usbd_register_all_classes(struct usbd_context *const uds_ctx, if (speed == USBD_SPEED_FS) { STRUCT_SECTION_FOREACH_ALTERNATE(usbd_class_fs, usbd_class_node, c_nd) { + if (blocklist != NULL && is_blocklisted(c_nd, blocklist)) { + continue; + } + ret = usbd_register_class(uds_ctx, c_nd->c_data->name, speed, cfg); if (ret) { diff --git a/tests/subsys/usb/device_next/src/main.c b/tests/subsys/usb/device_next/src/main.c index a0fcb0e83961f..f5fd5e212c9eb 100644 --- a/tests/subsys/usb/device_next/src/main.c +++ b/tests/subsys/usb/device_next/src/main.c @@ -134,7 +134,7 @@ static void *usb_test_enable(void) zassert_equal(err, 0, "Failed to add configuration (%d)"); if (usbd_caps_speed(&test_usbd) == USBD_SPEED_HS) { - err = usbd_register_all_classes(&test_usbd, USBD_SPEED_HS, 1); + err = usbd_register_all_classes(&test_usbd, USBD_SPEED_HS, 1, NULL); zassert_equal(err, 0, "Failed to unregister all instances(%d)"); err = usbd_unregister_all_classes(&test_usbd, USBD_SPEED_HS, 1); @@ -144,7 +144,7 @@ static void *usb_test_enable(void) zassert_equal(err, 0, "Failed to register loopback_0 class (%d)"); } - err = usbd_register_all_classes(&test_usbd, USBD_SPEED_FS, 1); + err = usbd_register_all_classes(&test_usbd, USBD_SPEED_FS, 1, NULL); zassert_equal(err, 0, "Failed to unregister all instances(%d)"); err = usbd_unregister_all_classes(&test_usbd, USBD_SPEED_FS, 1);