From 5fd4e567004b9a2663fed7ee9a005b1e59898288 Mon Sep 17 00:00:00 2001 From: Johann Fischer Date: Wed, 25 Sep 2024 18:50:58 +0200 Subject: [PATCH 1/4] usb: device_next: implement blocklist for classes In the usbd_register_all_classes(), we may need to skip some instances as they may have very specific function like USB DFU "DFU mode" which should not be available by default. Signed-off-by: Johann Fischer --- include/zephyr/usb/usbd.h | 18 ++++++++++++++- samples/subsys/usb/common/sample_usbd_init.c | 5 +++-- subsys/usb/device_next/usbd_class.c | 23 +++++++++++++++++++- tests/subsys/usb/device_next/src/main.c | 4 ++-- 4 files changed, 44 insertions(+), 6 deletions(-) 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/samples/subsys/usb/common/sample_usbd_init.c b/samples/subsys/usb/common/sample_usbd_init.c index fb5b8e0dc3246..c71e6e99a1676 100644 --- a/samples/subsys/usb/common/sample_usbd_init.c +++ b/samples/subsys/usb/common/sample_usbd_init.c @@ -124,7 +124,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, + NULL); if (err) { LOG_ERR("Failed to add register classes"); return NULL; @@ -143,7 +144,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, NULL); if (err) { LOG_ERR("Failed to add register classes"); return NULL; 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); From 5e473059e84679e4fa41890bbfdb42eb2b586ec3 Mon Sep 17 00:00:00 2001 From: Johann Fischer Date: Sat, 7 Sep 2024 10:24:14 +0200 Subject: [PATCH 2/4] usb: device_next: implement USB DFU class for the new device support This new implementation is written from scratch and is not tied to the image manager and MCUboot. It allows the user to define their own backend and use a simple macro to instantiate an image. On the USB side this is represented by an interface. The number of possible images is configurable using the Kconfig option, and is a fairly inexpensive approach since it only changes the size of the pointer array. The number of images is only limited by the number of possible interfaces in a configuration. The class implementation does not support multiple instances, as there is no real use for it. However, it does provide two class instances, one for runtime mode and one for DFU mode. The switch from runtime to DFU mode can only be performed by the user application, i.e. the application receives a notification when the host wants to switch to DFU mode, and then the application can disable the runtime configuration and enable the DFU configuration. This implementation does not support switching to the DFU mode by bus reset issued by the host. Signed-off-by: Johann Fischer --- .../usb/device_next/api/index.rst | 1 + .../usb/device_next/api/usbd_dfu.rst | 11 + include/zephyr/usb/class/usbd_dfu.h | 170 ++++ include/zephyr/usb/usbd_msg.h | 6 + subsys/usb/device_next/CMakeLists.txt | 10 + subsys/usb/device_next/class/Kconfig | 1 + subsys/usb/device_next/class/Kconfig.dfu | 51 ++ subsys/usb/device_next/class/usbd_dfu.c | 794 ++++++++++++++++++ subsys/usb/device_next/class/usbd_dfu.ld | 3 + 9 files changed, 1047 insertions(+) create mode 100644 doc/connectivity/usb/device_next/api/usbd_dfu.rst create mode 100644 include/zephyr/usb/class/usbd_dfu.h create mode 100644 subsys/usb/device_next/class/Kconfig.dfu create mode 100644 subsys/usb/device_next/class/usbd_dfu.c create mode 100644 subsys/usb/device_next/class/usbd_dfu.ld 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_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/subsys/usb/device_next/CMakeLists.txt b/subsys/usb/device_next/CMakeLists.txt index d216cbe8f11c8..08d8e6288866e 100644 --- a/subsys/usb/device_next/CMakeLists.txt +++ b/subsys/usb/device_next/CMakeLists.txt @@ -82,4 +82,14 @@ zephyr_library_sources_ifdef( class/usbd_hid_api.c ) +zephyr_library_sources_ifdef( + CONFIG_USBD_DFU + class/usbd_dfu.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..e5b7e078cdaf9 --- /dev/null +++ b/subsys/usb/device_next/class/Kconfig.dfu @@ -0,0 +1,51 @@ +# 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" + +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) From 321e5ab9d873f230b3498718045ce333ea0a054e Mon Sep 17 00:00:00 2001 From: Johann Fischer Date: Fri, 11 Oct 2024 18:27:00 +0200 Subject: [PATCH 3/4] usb: device_next: add USB DFU flash backend Add a simpler flash backend, similar to what we have in the legacy USB DFU implementation. Support slot-0 and slot-1 flash partitions, but allow them to be enabled/disabled via Kconfig options. Signed-off-by: Johann Fischer --- subsys/usb/device_next/CMakeLists.txt | 5 + subsys/usb/device_next/class/Kconfig.dfu | 25 +++ subsys/usb/device_next/class/usbd_dfu_flash.c | 155 ++++++++++++++++++ 3 files changed, 185 insertions(+) create mode 100644 subsys/usb/device_next/class/usbd_dfu_flash.c diff --git a/subsys/usb/device_next/CMakeLists.txt b/subsys/usb/device_next/CMakeLists.txt index 08d8e6288866e..a93d32cd10bc5 100644 --- a/subsys/usb/device_next/CMakeLists.txt +++ b/subsys/usb/device_next/CMakeLists.txt @@ -87,6 +87,11 @@ zephyr_library_sources_ifdef( 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 diff --git a/subsys/usb/device_next/class/Kconfig.dfu b/subsys/usb/device_next/class/Kconfig.dfu index e5b7e078cdaf9..0ee0f8bad2aa8 100644 --- a/subsys/usb/device_next/class/Kconfig.dfu +++ b/subsys/usb/device_next/class/Kconfig.dfu @@ -48,4 +48,29 @@ 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_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 From 67bec6b236355cff82b02b0333766dd26e3cbb93 Mon Sep 17 00:00:00 2001 From: Johann Fischer Date: Wed, 25 Sep 2024 18:49:22 +0200 Subject: [PATCH 4/4] samples: usb: add sample for the new USB DFU implementation The sample defines an image that uses RAMdisk and also provides an option to enable USB DFU flash backend for the "slot-1" flash partition. Assuming the user runs this sample from the slot-0 partition, uploading to flash should work fine for evaluation purposes. If the sample is built with CONFIG_BOOTLOADER_MCUBOOT=y, the sample will mark the image in slot 1 as pending after an image download. Signed-off-by: Johann Fischer --- samples/subsys/usb/common/sample_usbd_init.c | 10 +- samples/subsys/usb/dfu-next/CMakeLists.txt | 9 + samples/subsys/usb/dfu-next/Kconfig | 36 +++ samples/subsys/usb/dfu-next/README.rst | 137 +++++++++ samples/subsys/usb/dfu-next/app.overlay | 14 + samples/subsys/usb/dfu-next/prj.conf | 10 + samples/subsys/usb/dfu-next/sample.yaml | 25 ++ samples/subsys/usb/dfu-next/src/main.c | 291 +++++++++++++++++++ 8 files changed, 530 insertions(+), 2 deletions(-) create mode 100644 samples/subsys/usb/dfu-next/CMakeLists.txt create mode 100644 samples/subsys/usb/dfu-next/Kconfig create mode 100644 samples/subsys/usb/dfu-next/README.rst create mode 100644 samples/subsys/usb/dfu-next/app.overlay create mode 100644 samples/subsys/usb/dfu-next/prj.conf create mode 100644 samples/subsys/usb/dfu-next/sample.yaml create mode 100644 samples/subsys/usb/dfu-next/src/main.c diff --git a/samples/subsys/usb/common/sample_usbd_init.c b/samples/subsys/usb/common/sample_usbd_init.c index c71e6e99a1676..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 @@ -125,7 +131,7 @@ struct usbd_context *sample_usbd_setup_device(usbd_msg_cb_t msg_cb) } err = usbd_register_all_classes(&sample_usbd, USBD_SPEED_HS, 1, - NULL); + blocklist); if (err) { LOG_ERR("Failed to add register classes"); return NULL; @@ -144,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, NULL); + 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; +}