UART interrupt tx

Hi , I am trying to implement a Uart tx code for sending 800-1000 bytes data at one time ! I am using the echo_bot sample as reference but nothing is really helping in understanding it.

Could you help me understand basic uart interrupt function .

I am using a nrf 52833 board and ncs 2.1.1

Parents
  • I found the example called "cdc_acm" to be a better example of interrupt-driven UART. The "echo_bot" example kind of mixes interrupt-driven with polling-based UART.

    In general, interrupt-driven UART requires you to attach a callback funtion to your UART device:

    uart_irq_callback_set(dev, interrupt_handler);

    This callback function can handle both receiving and transmitting. Once you register the callback, you need to enable the UART's ability to receive data. Enabling this is what makes the callback function start to fire every time the UART receives data:

    uart_irq_rx_enable(dev);

    So, at this point your callback function should start firing when the UART device receives data. The callback function looks at whether it was triggered due to a transmit or receive "event" and decides what to do based on that information. (At this point it can only be triggered by a receive because we haven't enabled the transmit interrupt like we did for receive in the code above).

    The cdc_acm example also just echoes anything it receives back to transmit. I'll add the code here for what the callback function is doing:

    static void interrupt_handler(const struct device *dev, void *user_data)
    {
    	ARG_UNUSED(user_data);
    
    	while (uart_irq_update(dev) && uart_irq_is_pending(dev)) {
    		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);
    		}
    	}
    }

    So the callback function is handling interrupts that can be caused by transmits or receives. So first, it checks which one caused it. Let's say it was a receive event. It reads up to 64 bytes of data from the UART fifo (the interrupt fires as long as there is data waiting to be received, so you're doing 64 bytes at a time but it just repeats until all 1000 of your bytes have been read):

    recv_len = uart_fifo_read(dev, buffer, len);

    In this example where you're just echoing, the callback function wants to transmit the data it just received. So, it adds that data you just read to a ring buffer and enables the transmit interrupt:

    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);
    }

    Now, the callback function is finished for the first time. Since the TX irq was enabled it fires again. Now, there's no more receive data but there is the TX data that we just put in the ring buffer. The callback function reads all the data possible from the ring buffer and puts that data in the UART fifo which means that data will be transmitted. Once all the data that needs to be sent has been put in the uart fifo, the TX interrupt is disabled again so it doesn't keep firing:

    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);
    }

    So in short, you have a callback function that can be triggered by needing to RX or TX data. You can enable/disable whether an RX or TX can trigger the interrupt with uart_irq_tx_enable(), uart_irq_tx_disable(), uart_irq_rx_enable(), and uart_irq_rx_disable(). Within the callback function, you can read (uart_fifo_read()) and write (uart_fifio_fill()) data to the UART device. If you need to temporarily hold data from the UART device you can use a ring buffer.

Reply
  • I found the example called "cdc_acm" to be a better example of interrupt-driven UART. The "echo_bot" example kind of mixes interrupt-driven with polling-based UART.

    In general, interrupt-driven UART requires you to attach a callback funtion to your UART device:

    uart_irq_callback_set(dev, interrupt_handler);

    This callback function can handle both receiving and transmitting. Once you register the callback, you need to enable the UART's ability to receive data. Enabling this is what makes the callback function start to fire every time the UART receives data:

    uart_irq_rx_enable(dev);

    So, at this point your callback function should start firing when the UART device receives data. The callback function looks at whether it was triggered due to a transmit or receive "event" and decides what to do based on that information. (At this point it can only be triggered by a receive because we haven't enabled the transmit interrupt like we did for receive in the code above).

    The cdc_acm example also just echoes anything it receives back to transmit. I'll add the code here for what the callback function is doing:

    static void interrupt_handler(const struct device *dev, void *user_data)
    {
    	ARG_UNUSED(user_data);
    
    	while (uart_irq_update(dev) && uart_irq_is_pending(dev)) {
    		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);
    		}
    	}
    }

    So the callback function is handling interrupts that can be caused by transmits or receives. So first, it checks which one caused it. Let's say it was a receive event. It reads up to 64 bytes of data from the UART fifo (the interrupt fires as long as there is data waiting to be received, so you're doing 64 bytes at a time but it just repeats until all 1000 of your bytes have been read):

    recv_len = uart_fifo_read(dev, buffer, len);

    In this example where you're just echoing, the callback function wants to transmit the data it just received. So, it adds that data you just read to a ring buffer and enables the transmit interrupt:

    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);
    }

    Now, the callback function is finished for the first time. Since the TX irq was enabled it fires again. Now, there's no more receive data but there is the TX data that we just put in the ring buffer. The callback function reads all the data possible from the ring buffer and puts that data in the UART fifo which means that data will be transmitted. Once all the data that needs to be sent has been put in the uart fifo, the TX interrupt is disabled again so it doesn't keep firing:

    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);
    }

    So in short, you have a callback function that can be triggered by needing to RX or TX data. You can enable/disable whether an RX or TX can trigger the interrupt with uart_irq_tx_enable(), uart_irq_tx_disable(), uart_irq_rx_enable(), and uart_irq_rx_disable(). Within the callback function, you can read (uart_fifo_read()) and write (uart_fifio_fill()) data to the UART device. If you need to temporarily hold data from the UART device you can use a ring buffer.

Children
  • If all you want to be able to do is TX, you wouldn't need to enable the RX interrupt. Say your main function just wants to TX "hello" every second. Every time you want to write "hello" you would add "hello" to the ring buffer with ring_buf_put() and enable the TX interrupt with uart_irq_tx_enable(). This causes the callback function to get triggered and it reads data from the ring buffer and writes it to the UART fifo.

    Main function:

    // Attach your interrupt handler to UART
    uart_irq_callback_set(dev, interrupt_handler);
    
    // Here's the data to be written
    char awesome_data[8] = "Hello!\n\0";
    
    while(1)
    {
    	// Delay for 1 second
    	k_msleep(1000);
    
    	// Add data to ring buffer
    	ring_buf_put(&ringbuf, awesome_data, sizeof(awesome_data));
    
    	// Enable the TX interrupt
    	uart_irq_tx_enable(dev);
    }

    Simpler interrupt handler that only deals with TX:

    static void interrupt_handler(const struct device *dev, void *user_data)
    {
    	ARG_UNUSED(user_data);
    
    	while (uart_irq_update(dev) && uart_irq_is_pending(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);
    		}
    	}
    }

    Here's the whole main file if you want to try it. Just use the cdc acm example and replace the main file with the code below. This example can connect to your computer with the USB port and you can user a terminal like Putty to connect to it and see "hello" printing out.

    /*
     * Copyright (c) 2019 Intel Corporation
     *
     * SPDX-License-Identifier: Apache-2.0
     */
    
    /**
     * @file
     * @brief Sample echo app for CDC ACM class
     *
     * Sample app for USB CDC ACM class driver. The received data is echoed back
     * to the serial port.
     */
    
    #include <stdio.h>
    #include <string.h>
    #include <device.h>
    #include <drivers/uart.h>
    #include <zephyr.h>
    #include <sys/ring_buffer.h>
    
    #include <usb/usb_device.h>
    #include <logging/log.h>
    LOG_MODULE_REGISTER(cdc_acm_echo, 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);
    
    	while (uart_irq_update(dev) && uart_irq_is_pending(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);
    		}
    	}
    }
    
    void setup_usb(const struct device *dev)
    {
    	int ret;
    	uint32_t baudrate, dtr = 0U;
    	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 1 sec for the host to do all settings */
    	k_busy_wait(1000000);
    	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);
    	}
    }
    
    void main(void)
    {
    	const struct device *dev;
    	int ret;
    
    	// Get the USB (UART) device
    	dev = DEVICE_DT_GET_ONE(zephyr_cdc_acm_uart);
    	if (!device_is_ready(dev)) {
    		LOG_ERR("CDC ACM device not ready");
    		return;
    	}
    
    	// Enable the USB port
    	ret = usb_enable(NULL);
    	if (ret != 0) {
    		LOG_ERR("Failed to enable USB");
    		return;
    	}
    
    	// Initialize dat ring buffer
    	ring_buf_init(&ringbuf, sizeof(ring_buffer), ring_buffer);
    
    	// Do a bunch of USB-specific things not related to UART
    	setup_usb(dev);
    
    	// Attach your interrupt handler to UART
    	uart_irq_callback_set(dev, interrupt_handler);
    
    	// Here's the data to be written
    	char awesome_data[8] = "Hello!\n\0";
    
    	while(1)
    	{
    		// Delay for 1 second
    		k_msleep(1000);
    
    		// Add data to ring buffer
    		ring_buf_put(&ringbuf, awesome_data, sizeof(awesome_data));
    
    		// Enable the TX interrupt
    		uart_irq_tx_enable(dev);
    	}
    
    }
    

  • Thank you , .

    I have much better understanding of it now .

  • I would just like to flag this post to Nordic documentation developers as an EXCELLENT example of a fantastic explanation! 

    Thank you so much.

Related