How to disable USB CDC-ACM during running

Hello,

I am currently developing a program that uses USB CDC-ACM for USB serial communication.

The USB serial communication is only used when the device is powered via USB, and it should be disabled when powered by battery (i.e., when nothing is connected to the USB connector). I want to achieve this power-saving behavior by disabling USB CDC-ACM during runtime, rather than excluding it from the build configuration (similar to CONFIG_SERIAL=n).

Here’s what I’ve tried so far:

1.I defined the USB CDC-ACM device using
 const struct device *uartdev = DEVICE_DT_GET_ONE(zephyr_cdc_acm_uart);


2.I attempted to turn off the power for uartdev using
int ret = pm_device_action_run(uartdev, PM_DEVICE_ACTION_TURN_OFF);

Results:

    The return value of pm_device_action_run was -88 (Function not implemented), indicating failure. The uartdev->pm_device is NULL, so this result is correct. However, why is uartdev->pm_device NULL?

    How can I ensure that uartdev->pm_device includes the appropriate structure at build time?

    If there are any other methods to disable the power of usb_cdc_acm during program execution rather than at build time, please let me know.

    Additional information:

    • Nordic Connect SDK (NCS) version: 2.4.2
    • SoC: nRF52833

    Best regards,

    Parents
    • Hi,

      The PM_DEVICE_ACTION_TURN_OFF action is not implemented for USB CDC-ACM. Instead, you should use PM_DEVICE_ACTION_SUSPEND.

      Best regards,
      Marte

    • Thank you for your response. However, it was not quite what I was looking for.

      Even if I change the argument to PM_DEVICE_ACTION_SUSPEND, since uartdev does not hold a reference to struct pm_device, pm_device_action_run will still return early due to the NULL check.

      How should I define the USB CDC-ACM device so that I can execute pm_device_action_run(uartdev, PM_DEVICE_ACTION_SUSPEND);

      I forgot to mention that the USB CDC-ACM is based on the Zephyr sample program USB CDC-ACM (https://docs.zephyrproject.org/latest/samples/subsys/usb/cdc_acm/README.html), and I am using "zephyr,cdc-acm-uart".

      Best regards,

      Kentarou

    • Hi Kentarou,

      Device Power Management is not implemented for USB CDC-ACM, which is why the pm_device is NULL and why using pm_device_action_run() does not work. For this to work, you will have to implement support for PM device in the driver.

      For this, you need to create a function to handle the different actions, e.g. cdc_acm_pm_action(). Then you need to define the device PM resources with PM_DEVICE_DT_INST_DEFINE and provide the device PM resources to DEVICE_DT_INST_DEFINE.

      Here you can see how I modified zephyr/subsys/usb/device/class/cdc_acm.c to get this to work:

      diff --git a/subsys/usb/device/class/cdc_acm.c b/subsys/usb/device/class/cdc_acm.c
      index fedccf5fdc0..586c7e083c8 100644
      --- a/subsys/usb/device/class/cdc_acm.c
      +++ b/subsys/usb/device/class/cdc_acm.c
      @@ -48,6 +48,7 @@
       #include <zephyr/usb/usb_device.h>
       #include <usb_descriptor.h>
       #include <usb_work_q.h>
      +#include <zephyr/pm/device.h>
       
       #ifndef CONFIG_UART_INTERRUPT_DRIVEN
       #error "CONFIG_UART_INTERRUPT_DRIVEN must be set for CDC ACM driver"
      @@ -1071,6 +1072,30 @@ static const struct uart_driver_api cdc_acm_driver_api = {
       #endif /* CONFIG_UART_USE_RUNTIME_CONFIGURE */
       };
       
      +
      +#ifdef CONFIG_PM_DEVICE
      +static int cdc_acm_pm_action(const struct device *dev,
      +                              enum pm_device_action action)
      +{
      +       int ret;
      +
      +       switch (action) {
      +       case PM_DEVICE_ACTION_RESUME:
      +               // Implement resume here
      +               LOG_INF("PM_DEVICE_ACTION_RESUME");
      +               break;
      +       case PM_DEVICE_ACTION_SUSPEND:
      +               // Implement suspend here
      +               LOG_INF("PM_DEVICE_ACTION_SUSPEND");
      +               break;
      +       default:
      +               return -ENOTSUP;
      +       }
      +
      +       return 0;
      +}
      +#endif /* CONFIG_PM_DEVICE */
      +
       #define INITIALIZER_IAD                                                        \
              .iad_cdc = {                                                    \
                      .bLength = sizeof(struct usb_association_descriptor),   \
      @@ -1215,7 +1240,9 @@ static const struct uart_driver_api cdc_acm_driver_api = {
                           " is not assigned to a USB device controller");    \
              CDC_ACM_CFG_AND_DATA_DEFINE(idx)                                \
                                                                              \
      -       DEVICE_DT_INST_DEFINE(idx, cdc_acm_init, NULL,                  \
      +       PM_DEVICE_DT_INST_DEFINE(idx, cdc_acm_pm_action); \
      +                                                                                                               \
      +       DEVICE_DT_INST_DEFINE(idx, cdc_acm_init, PM_DEVICE_DT_INST_GET(idx),                    \
                      &cdc_acm_dev_data_##idx, &cdc_acm_config_##idx,         \
                      PRE_KERNEL_1, CONFIG_SERIAL_INIT_PRIORITY,              \
                      &cdc_acm_driver_api);
      

      Please note that this code has not been thoroughly tested or qualified and should be considered provided “as-is”. I only tested to verify that it entered cdc_acm_pm_action().

      Best regards,
      Marte

    • Hello, Marte.

      I apologize for the delayed response.

      After some trial and error, we succeeded in reducing power consumption by referencing the struct device from uart0 instead of zephyr_cdc_acm_uart. Below is an excerpt from the code:

          const struct device *uart = DEVICE_DT_GET(DT_NODELABEL(uart0));
          ret = pm_device_action_run(uart, PM_DEVICE_ACTION_SUSPEND);

      uart0 is defined in the .dts file as follows:

      &uart0 {
          compatible = "nordic,nrf-uarte";
          status = "okay";
          current-speed = <115200>;
          pinctrl-0 = <&uart0_default>;
          pinctrl-1 = <&uart0_sleep>;
          pinctrl-names = "default", "sleep";
      };

      In the zephyr/subsys/usb/device/class/cdc_acm.c file you mentioned, I found a macro check with #if DT_NODE_HAS_COMPAT(DT_CHOSEN(zephyr_console), zephyr_cdc_acm_uart). This led me to believe that uart0, which can be considered the actual zephyr_console, is related to zephyr_cdc_acm_uart, so I tried the code above.

      Is this the correct approach?

    • Hi,

      Yes, referencing from uart0 is the correct approach in this case.

      Best regards,
      Marte

    Reply Children
    No Data
    Related