Cannot get Async UART0 to work with USB CDC

Hello,

I first got the USB CDC Zephyr sample with interrupt driven UART to work just fine. 

But because I want to to be able to TX outside of interrupts I tried to convert that sample to use Async UART while using USB CDC.  I have looked at all tickets that seem related to this but nothing works, maybe because I'm using a  newer SDK version, which is v2.4.2.

When I call uart_callback_set I get -ENOSYS.   I can see all callbacks for the API are not set which is why I get this.  (I print the pointers and they are 0)

from C:\ncs\v2.4.2\zephyr\include\zephyr\drivers\uart.h

static inline int uart_callback_set(const struct device *dev,
				    uart_callback_t callback,
				    void *user_data)
{
#ifdef CONFIG_UART_ASYNC_API
	const struct uart_driver_api *api =
			(const struct uart_driver_api *)dev->api;

	if (api->callback_set == NULL) {
		return -ENOSYS;
	}

	return api->callback_set(dev, callback, user_data);
#else
	ARG_UNUSED(dev);
	ARG_UNUSED(callback);
	ARG_UNUSED(user_data);
	return -ENOTSUP;
#endif
}

I have tried all kings of CONFIG options (from other tickets) and nothing seems to work.  I can see the ASYNC code is enabled in the file C:\ncs\v2.4.2\zephyr\drivers\serial\uart_nrfx_uarte.c.  I can not see the linkage with the calling of DEVICE_DT_GET_ONE(zephyr_cdc_acm_uart) and driver API callbacks being set correctly. 

Last configs I tried are:

CONFIG_LOG=y

CONFIG_MAIN_STACK_SIZE=1536


CONFIG_STDOUT_CONSOLE=y

CONFIG_USB_DEVICE_STACK=y
CONFIG_USB_DEVICE_PRODUCT="Zephyr CDC ACM sample"
CONFIG_USB_DEVICE_SN="0123456789ABCDEG"
CONFIG_USB_DEVICE_MANUFACTURER="Zephyr USBD CDC ACM with MS"
CONFIG_USB_DEVICE_PID=0x0002
CONFIG_USB_DEVICE_VID=0x2fe3
CONFIG_USB_DRIVER_LOG_LEVEL_ERR=y
CONFIG_USB_DEVICE_LOG_LEVEL_ERR=y
CONFIG_USB_DEVICE_INITIALIZE_AT_BOOT=n
CONFIG_USB_CDC_ACM=y
CONFIG_SERIAL=y

CONFIG_UART_ASYNC_API=y
CONFIG_UART_INTERRUPT_DRIVEN=y
CONFIG_UART_0_ASYNC=y
CONFIG_UART_0_INTERRUPT_DRIVEN=n
CONFIG_NRFX_UARTE0=y

CONFIG_UART_LINE_CTRL=y

&zephyr_udc0 {
	cdc_acm_uart0 {
		compatible = "zephyr,cdc-acm-uart";
	};
};

My Code

#include <zephyr/kernel.h>
#include <zephyr/device.h>


#include <zephyr/usb/usb_device.h>
#include <zephyr/usb/usbd.h>

#include <zephyr/drivers/uart.h>
#include <zephyr/sys/ring_buffer.h>

#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(main, LOG_LEVEL_INF);

#define RING_BUF_SIZE 1024
uint8_t ring_buffer[RING_BUF_SIZE];
struct ring_buf ringbuf;

// static void interrupt_handler(const struct device *dev, void *user_data)
// {
// 	ARG_UNUSED(user_data);

// 	LOG_INF("11");

// 	while (uart_irq_update(dev) && uart_irq_is_pending(dev)) {

// 		LOG_INF("222");

// 		if (uart_irq_rx_ready(dev)) {
// 			int recv_len, rb_len;
// 			uint8_t buffer[64];
// 			size_t len = MIN(ring_buf_space_get(&ringbuf), sizeof(buffer));

// 			recv_len = uart_fifo_read(dev, buffer, len);
// 			if (recv_len < 0) {
// 				LOG_ERR("Failed to read UART FIFO");
// 				recv_len = 0;
// 			};

// 			rb_len = ring_buf_put(&ringbuf, buffer, recv_len);
// 			if (rb_len < recv_len) {
// 				LOG_ERR("Drop %u bytes", recv_len - rb_len);
// 			}

// 			LOG_DBG("tty fifo -> ringbuf %d bytes", rb_len);
// 			if (rb_len) {
// 				uart_irq_tx_enable(dev);
// 			}
// 		}

// 		if (uart_irq_tx_ready(dev)) {
// 			uint8_t buffer[64];
// 			int rb_len, send_len;

// 			rb_len = ring_buf_get(&ringbuf, buffer, sizeof(buffer));
// 			if (!rb_len) {
// 				LOG_DBG("Ring buffer empty, disable TX IRQ");
// 				uart_irq_tx_disable(dev);
// 				continue;
// 			}

// 			send_len = uart_fifo_fill(dev, buffer, rb_len);
// 			if (send_len < rb_len) {
// 				LOG_ERR("Drop %d bytes", rb_len - send_len);
// 			}

// 			LOG_DBG("ringbuf -> tty fifo %d bytes", send_len);
// 		}
// 	}
// }

static uint8_t rx_buf[64] = {0}; //A buffer to store incoming UART data 

static void async_interrupt_cb(const struct device *dev, struct uart_event *evt, void *user_data)
{
	switch (evt->type) {
	
	case UART_TX_DONE:
		// do something
		break;

	case UART_TX_ABORTED:
		// do something
		break;
		
	case UART_RX_RDY:
		LOG_INF("UART_RX_RDY");
		break;

	case UART_RX_BUF_REQUEST:
		LOG_INF("UART_RX_BUF_REQUEST");
		break;

	case UART_RX_BUF_RELEASED:
		LOG_INF("UART_RX_BUF_RELEASED");
		break;
		
	case UART_RX_DISABLED:
		// do something
		break;

	case UART_RX_STOPPED:
		// do something
		break;
		
	default:
		break;
	}
}

int main(void) {

	const struct device *dev;
	uint32_t baudrate, dtr = 0U;
	int ret;

	//
	// Setup USB
	//

	dev = DEVICE_DT_GET_ONE(zephyr_cdc_acm_uart);
	if (!device_is_ready(dev)) {
		LOG_ERR("CDC ACM device not ready");
		return 0;
	}

	ret = usb_enable(NULL);

	LOG_INF("Configured.......");

	if (ret != 0) {
		LOG_ERR("Failed to enable USB");
		return 0;
	}

	ring_buf_init(&ringbuf, sizeof(ring_buffer), ring_buffer);

	LOG_INF("Wait for DTR");

	while (true) {
		uart_line_ctrl_get(dev, UART_LINE_CTRL_DTR, &dtr);
		if (dtr) {
			break;
		} else {
			/* Give CPU resources to low priority threads. */
			k_sleep(K_MSEC(100));
		}
	}

	LOG_INF("DTR set");

		/* They are optional, we use them to test the interrupt endpoint */
	ret = uart_line_ctrl_set(dev, UART_LINE_CTRL_DCD, 1);
	if (ret) {
		LOG_WRN("Failed to set DCD, ret code %d", ret);
	}

	ret = uart_line_ctrl_set(dev, UART_LINE_CTRL_DSR, 1);
	if (ret) {
		LOG_WRN("Failed to set DSR, ret code %d", ret);
	}

	/* Wait 100ms for the host to do all settings */
	k_msleep(100);

	ret = uart_line_ctrl_get(dev, UART_LINE_CTRL_BAUD_RATE, &baudrate);
	if (ret) {
		LOG_WRN("Failed to get baudrate, ret code %d", ret);
	} else {
		LOG_INF("Baudrate detected: %d", baudrate);
	}


	//uart_irq_callback_set(dev, interrupt_handler);
	ret = uart_callback_set(dev, async_interrupt_cb, NULL);
	if (ret != 0) { // :TODO:
		LOG_ERR("Failed to register UART UART cb: %i", ret);
		return 0;
	}

	/* Enable rx interrupts */
	//uart_irq_rx_enable(dev);

	ret = uart_rx_enable(dev, rx_buf, sizeof(rx_buf), 100);
	if (ret != 0) { // :TODO:
		LOG_ERR("Failed to enable UART RX");
		return 0;
	}

	LOG_INF("Starting Observer Demo\n");

	LOG_ERR("Exiting %s thread", __func__);
	return 0;
}

When I connect with pyserial I always get the error NOSYS, 88.  You will note I can make some sort of connection to get the baudrate.  (below)

*** Booting Zephyr OS build v3.3.99-ncs1-1 ***
[00:00:00.416,503] <inf> main: Configured.......
[00:00:00.416,534] <inf> main: Wait for DTR
[00:00:00.420,562] <inf> usb_cdc_acm: Device suspended
[00:00:00.518,188] <inf> usb_cdc_acm: Device resumed
[00:00:00.869,995] <inf> usb_cdc_acm: Device configured
[00:00:04.319,244] <inf> main: DTR set
[00:00:04.433,929] <inf> main: Baudrate detected: 115200
[00:00:04.433,959] <err> main: Failed to register UART UART cb: -88

After spending hours on this I don't know what else to try.  Any help with this would be very much appreciated.

Async UART0 (with EasyDMA) is supposed to work with USB CDC, they are compatible?

Parents
  • The later, use Async API for USB CDC.  I need to use the uart in async mode going over USB CDC.

    I did see that ticket but didn’t get any hints.

    It seems like there is a disconnect on setup between the uart and usb cdc stacks.  I can see all the individual uart api callbacks are just not set, I.e.

    if (api->callback_set == NULL) {

         return -ENOSYS;
    }

    In uart_callback_set.  So it is easy to see what is not working, but just cannot figure out why.

    Thanks

  • To follow up with more, I did see from that ticket:

    "The CDC ACM driver does not support the ASYNC API, and will return ENOTSUP if you try to enable the callback"

    From the code:

    from C:\ncs\v2.4.2\zephyr\include\zephyr\drivers\uart.h
    
    static inline int uart_callback_set(const struct device *dev,
    				    uart_callback_t callback,
    				    void *user_data)
    {
    #ifdef CONFIG_UART_ASYNC_API
    	const struct uart_driver_api *api =
    			(const struct uart_driver_api *)dev->api;
    
    	if (api->callback_set == NULL) {
    		return -ENOSYS;
    	}
    
    	return api->callback_set(dev, callback, user_data);
    #else
    	ARG_UNUSED(dev);
    	ARG_UNUSED(callback);
    	ARG_UNUSED(user_data);
    	return -ENOTSUP;
    #endif
    }

    There is specific logic for CONFIG_UART_ASYNC_API use in uart_callback_set.  And this is why I get the ENOSYS error and not the ENOTSUP error as in that ticket (he was likely not specifying CONFIG_UART_ASYNC_API).

    But I guess fundamentally does the CDC ACM driver support the ASYNC API? This could theoretically cause something farther up the stack to cause api->callback_set == NULL too be TRUE.

    Thanks

  • I am looking into this, but have not been able to find any reason for it yet.
    I will continue to look into this next week

  • Dean said:
    But I guess fundamentally does the CDC ACM driver support the ASYNC API?

    I was not able to find any official docs saying this, which is what I hoped.

    But one of our experts on the field says CDC ACM does not support the Async API in zephyr, so from what I can find that is just how it is.

  • I haven't found anything in the documentation, that specifies that the asynchronous API for USB CDC is not implemented. As developing 3 different implementations just to find out, which API is working correctly is a lot of work, I would either expect all APIs to just work as documented, or at least a list of APIs that are supposed to work. Is the polling API working?

  • I found a place where it says that cdc_acm only supports CONFIG_UART_INTERRUPT_DRIVEN, and it gives a precompiler error as well. https://github.com/nrfconnect/sdk-zephyr/blob/main/subsys/usb/device/class/cdc_acm.c#L52.

    For the rest of your questions Torsten:
    While we in Nordic Semiconductor ASA frequently contribute to the Zephyr RTOS, we do not make all of it.
    What you complain about would be a lack or documentation philosophy in Zephyr RTOS I think, so I suggest that if you want to feedback this, making an issue for Zephyr RTOS is the best way to be heard by any zephyr developers.

  • Hi Sigurd,

    thank you for your fast response! :-)

    # Temporary workaround for https://devzone.nordicsemi.com/f/nordic-q-a/107445/colon-in-soc_cpu_idle-h-leads-to-failing-build-with-arm-none-eabi-gcc-version-12-2
    CONFIG_SOC_NRF53_ANOMALY_168_WORKAROUND=n
    
    # Enable support for C++
    CONFIG_CPP=y
    CONFIG_REQUIRES_FULL_LIBCPP=y
    CONFIG_NEWLIB_LIBC=y
    CONFIG_STD_CPP20=y
    
    CONFIG_STDOUT_CONSOLE=y
    CONFIG_LOG=y
    
    # USB
    CONFIG_USB_DEVICE_STACK=y
    CONFIG_USB_DEVICE_PRODUCT="Lovelace Pod"
    CONFIG_USB_DEVICE_PID=0x0001
    CONFIG_USB_DRIVER_LOG_LEVEL_ERR=y
    CONFIG_USB_DEVICE_LOG_LEVEL_ERR=y
    
    # UART
    CONFIG_SERIAL=y
    CONFIG_UART_ASYNC_API=y
    CONFIG_UART_INTERRUPT_DRIVEN=n
    CONFIG_UART_LINE_CTRL=y
    CONFIG_USB_DEVICE_INITIALIZE_AT_BOOT=n
    
    # MQTT
    CONFIG_MQTT_SN_LIB=y
    CONFIG_NETWORKING=y

    That's the configuration I'm currently working with. It compiles and links fine and then just returns `-ENOSYS`.

    Sure, Nodic is not the only contributor to Zephyr, but if the Zephyr documentation states, that there are 3 different APIs for UARTs, I would expect either support for the 3 APIs or I would expect that the documentation states, that there are some exceptions and how to find out which implementations do not support the documented features.

    In the end, when I have the task to develop firmware for the nordic hardware, I have not much choices, but to use Zephyr (and in deed, I have to use the Nordic branch of Zephyr). And in order to find out, how to do so, I have to rely on the documentation.

Reply
  • Hi Sigurd,

    thank you for your fast response! :-)

    # Temporary workaround for https://devzone.nordicsemi.com/f/nordic-q-a/107445/colon-in-soc_cpu_idle-h-leads-to-failing-build-with-arm-none-eabi-gcc-version-12-2
    CONFIG_SOC_NRF53_ANOMALY_168_WORKAROUND=n
    
    # Enable support for C++
    CONFIG_CPP=y
    CONFIG_REQUIRES_FULL_LIBCPP=y
    CONFIG_NEWLIB_LIBC=y
    CONFIG_STD_CPP20=y
    
    CONFIG_STDOUT_CONSOLE=y
    CONFIG_LOG=y
    
    # USB
    CONFIG_USB_DEVICE_STACK=y
    CONFIG_USB_DEVICE_PRODUCT="Lovelace Pod"
    CONFIG_USB_DEVICE_PID=0x0001
    CONFIG_USB_DRIVER_LOG_LEVEL_ERR=y
    CONFIG_USB_DEVICE_LOG_LEVEL_ERR=y
    
    # UART
    CONFIG_SERIAL=y
    CONFIG_UART_ASYNC_API=y
    CONFIG_UART_INTERRUPT_DRIVEN=n
    CONFIG_UART_LINE_CTRL=y
    CONFIG_USB_DEVICE_INITIALIZE_AT_BOOT=n
    
    # MQTT
    CONFIG_MQTT_SN_LIB=y
    CONFIG_NETWORKING=y

    That's the configuration I'm currently working with. It compiles and links fine and then just returns `-ENOSYS`.

    Sure, Nodic is not the only contributor to Zephyr, but if the Zephyr documentation states, that there are 3 different APIs for UARTs, I would expect either support for the 3 APIs or I would expect that the documentation states, that there are some exceptions and how to find out which implementations do not support the documented features.

    In the end, when I have the task to develop firmware for the nordic hardware, I have not much choices, but to use Zephyr (and in deed, I have to use the Nordic branch of Zephyr). And in order to find out, how to do so, I have to rely on the documentation.

Children
  • I've looked at the proprocessor output of the compilation of `cdc_acm.c` to figure out, why the compile time check does not work for my example. It turns out, that despite the prj.config given above, the generated autoconf.h contains this line:

    #define CONFIG_UART_INTERRUPT_DRIVEN 1
    

  • CONFIG_USB_CDC_ACM selects CONFIG_UART_INTERRUPT_DRIVEN.
    Do you by chance have this warning in your build log?

    warning: UART_INTERRUPT_DRIVEN (defined at boards/shields/sparkfun_sara_r4/Kconfig.defconfig:24,
    boards/shields/wnc_m14a2a/Kconfig.defconfig:17, soc/arm/quicklogic_eos_s3/Kconfig.defconfig:17,
    drivers/serial/Kconfig:72) was assigned the value 'n' but got the value 'y'. See
    http://docs.zephyrproject.org/latest/kconfig.html#CONFIG_UART_INTERRUPT_DRIVEN and/or look up
    UART_INTERRUPT_DRIVEN in the menuconfig/guiconfig interface. The Application Development Primer,
    Setting Configuration Values, and Kconfig - Tips and Best Practices sections of the manual might be
    helpful too.
    

    Torsten Robitzki said:
    Sure, Nodic is not the only contributor to Zephyr, but if the Zephyr documentation states, that there are 3 different APIs for UARTs, I would expect either support for the 3 APIs or I would expect that the documentation states, that there are some exceptions and how to find out which implementations do not support the documented features.

    For whats it's worth, I agree.

    Torsten Robitzki said:
    In the end, when I have the task to develop firmware for the nordic hardware, I have not much choices, but to use Zephyr (and in deed, I have to use the Nordic branch of Zephyr). And in order to find out, how to do so, I have to rely on the documentation.

    That is true. I can talk to our documentation team and see if we can get a disclaimer into the CDC ACM documentation of Zephyr.

  • Yes, I see the KConfig warning. Would love to see an error when setting `CONFIG_UART_ASYNC_API=y`. But maybe the problem is that "normal" UART and CDC are handled by the very same configuration parameters, while they are most likely handled by very different parts of the software.

  • I am so glad I found this as it has been bugging me a lot.

    Earlier (2 years ago!) I had issues with CDC_ACM and discovered most of the time was spent copying data to the ring-buffer. At this time I was not able to get the async api to work so instead I moved the copying of data out of my interrupt handler and let a separate thread add data to the fifo whenever there was room for more, while at the same time never filling it completely (as I also ran into issues doing this).
    I was able to get massive transfer speed improvements and do 900+ kbyte/s but posted an issue as I was seeing data corruption. I suspected this to be caused by the access to the fifo outside of ISR but lately I have become less sure. I find the corruption to be highly host dependent and on some systems it seems absent all together. It may still be an interaction between the host and device however, causing the issues.

    If Nordic had added support for the async api I am hopeful they would have discovered something relating to the fifo and possibly solved multiple issues. I do see there is a device_next folder in zephyr/subsys/usb for a more flexible device descriptor setup but is this using a better approach to buffers and data?
    usbd_cdc_acm.c does not appear to do things the same way as the old cdc_acm.c ?

  • The Zephyr cdc_acm example contains some "shortcoming", that might result in what you are observing.

    In the IRQ, data to be transmitted is read into a local buffer. That buffer is then send via USB. If not all data could be send, the data is simply dropped. This situation can happen in real world applications when the receiver can not keep up with the transmitted data. And that situation is usually well handled with flow control, so there is no reason to handle this as an unavoidable error.

    My solution to this, was to write my own ring buffer, that allows me to peek into the buffer, try to transmit it and to only remove that data from the buffer, that I was able to pass successfully to the USB stack.

    I have searched quite some time for such a buffer in Zephyr, but couldn't find anything suitable.

Related