An Overview of the Linux UART Subsystem

Introduction

  1. The development of UART has matured significantly, and it is generally sufficient to directly port the 8250 driver. For example, the UART driver for Rockchip directly uses “snps,dw-apb-uart”, which corresponds to <span>Linux-4.9.88/drivers/tty/serial/8250/8250_dw.c</span> driver.
    uart1: serial@ff190000 {
        compatible = "rockchip,rk3368-uart", "snps,dw-apb-uart";
        reg = <0x0 0xff190000 0x0 0x100>;
        clock-frequency = <24000000>;
        clocks = <&cru SCLK_UART1>, <&cru PCLK_UART1>;
        clock-names = "baudclk", "apb_pclk";
        interrupts = <GIC_SPI 56 IRQ_TYPE_LEVEL_HIGH>;
        reg-shift = <2>;
        reg-io-width = <4>;
        status = "disabled";
    };
  1. However, if you are an engineer at a chip manufacturer and your boss requires you to write a UART driver from scratch for customized adaptation, you will definitely need to understand the entire UART framework.

UART Subsystem

Hierarchical Structure

  1. The UART hierarchical structure is as follows:
  • Application Layer: The interface between user-space applications and kernel-space in the serial subsystem, where applications typically perform operations through standard C library functions (such as open, read, write), e.g., minicom.
  • Character Device Layer: The standard framework provided by the Linux kernel, generally not requiring adjustments. It passes user-space serial read/write requests to the kernel-space tty layer, registered and managed through the interfaces provided by the character device layer.
  • TTY Layer: Responsible for managing the registration, configuration, and data transmission of all TTY devices. The TTY layer provides an abstract interface, allowing different types of terminal devices (serial ports, LCDs, USB) to be managed in a unified manner.
    • TTY Core Layer: Implemented by the Linux kernel, it provides a unified interface to user space, handling operation requests from user space and interacting with TTY line discipline or TTY drivers based on these requests.
    • TTY Line Discipline: Implemented by the Linux kernel, responsible for secondary processing and formatting of data. Line discipline can implement specific protocols or data processing logic, such as line buffering, echo control, signal handling, etc.
    • TTY Driver Layer: Typically implemented by the standard framework provided by the Linux kernel, but the specific hardware driver part may be provided by the chip manufacturer. It is responsible for interacting with specific hardware devices, handling character device read/write, control commands, signal handling, etc. Chip manufacturers may provide specific drivers for different hardware devices, which need to be implemented based on specific hardware characteristics.
  • UART Layer: Implemented by the chip manufacturer. It provides the low-level driver interface for serial devices, responsible for the initialization, configuration, and data transmission of the UART controller. By implementing data structures such as uart_driver, uart_port, and uart_ops, it provides a unified UART API interface to the upper layers.
  • Hardware Layer: The actual physical hardware, including the UART controller chip and related circuit design.
An Overview of the Linux UART Subsystem

TTY Layer

  1. The TTY layer driver is generally written by kernel developers and does not require adjustments. Therefore, I will not introduce it here.
  2. However, if you still wish to understand the content of the TTY layer, you can read the WeChat public account: 【Driver】Serial Driver Analysis (Part 2) – TTY Core

UART Driver Layer

  1. In many cases, simply porting the 8250 driver can successfully adapt the UART driver. However, if you read the 8250 driver code, you will find it quite painful and dizzying.
  2. If you wish to learn how to write a UART driver layer, you can read <span>Linux-4.9.88/drivers/tty/serial/imx.c</span> and combine it with the simplest UART driver framework code I provide below.
  3. Content to be implemented by the chip manufacturer:
  • uart_driver structure: one.
    • Function: Describes a UART driver, defining the basic information and operation interface of the driver.
    • Main functions: Register the UART driver to the kernel, manage multiple UART ports; provide device information, such as driver name, device number range, etc.; encapsulate the interface with the TTY subsystem, allowing UART devices to be managed as TTY devices.
    • Typical fields: owner, pointing to the module owner; driver_name, the name of the driver; dev_name, device name prefix; nr, the number of supported ports.
struct uart_driver {
    struct module *owner; // Points to the module that owns this uart_driver, usually THIS_MODULE
    const char *driver_name; // Name of the serial port driver, device file name is usually based on this name
    const char *dev_name; // Name of the serial port device
    int major; // Major device number, used to identify device type
    int minor; // Minor device number, used to identify specific device instance
    int nr; // Maximum number of serial ports supported by this uart_driver
    struct console *cons; // Points to the corresponding console structure, not NULL if supporting serial console

    /*
     * The following members are private, low-level drivers should not modify these members;
     * They should be initialized to NULL
     */
    struct uart_state *state; // Pointer to UART state, managing serial port state information
    struct tty_driver *tty_driver; // Pointer to TTY driver, managing TTY layer interface
};
  • uart_port structure: determined by the number of serial ports (uart_driver.nr), one or more.
    • Function: Describes a specific UART port. It contains configuration information and status related to the hardware.
    • Main functions: Save the hardware register address, clock frequency, pin configuration, etc. of the UART port; maintain the current status of the port, such as open, close, configure, etc.; provide interrupt number and interrupt handler.
    • Typical fields: iotype, I/O type (memory-mapped or I/O port); mapbase, memory-mapped base address; irq, interrupt number; uartclk, UART clock frequency.
  • uart_ops serial port operation set: There may be different operation methods for multiple serial ports in the chip, so there may be one or multiple.
    • Function: Defines a set of operation functions used to implement specific operations of the UART port, such as initialization, sending, and receiving data.
    • Main functions: Provide operation interfaces for the UART port, including initialization, configuration, data transmission, etc.; through these operation functions, the UART driver can abstract and adapt to different hardware platforms.
    • Typical fields: tx_empty, checks if the send buffer is empty; set_mctrl, sets the modem control line; get_mctrl, gets the status of the modem control line; start_tx, starts sending data; stop_tx, stops sending data.
  • console structure: Used to abstract and manage console devices, optional.
struct console {
    char name[16]; // Name of the console device, length of 16 characters, used to identify different console devices.
    
    void (*write)(struct console *, const char *, unsigned); 
    // Function pointer, points to a function that outputs characters to the console device.
    // Parameters include a pointer to struct console, the character array to be output, and the number of characters.
    
    int (*read)(struct console *, char *, unsigned); 
    // Function pointer, points to a function that reads characters from the console device.
    // Parameters include a pointer to struct console, a buffer for storing read characters, and the number of characters to read.
    
    struct tty_driver *(*device)(struct console *, int *);
    // Function pointer, points to a function that returns a pointer to the tty_driver structure associated with the console.
    // Parameters include a pointer to struct console and an integer pointer for storing the device index.
    
    void (*unblank)(void); 
    // Function pointer, points to a function that cancels the screen saver or blank state of the console device.
    
    int (*setup)(struct console *, char *);
    // Function pointer, points to a function that initializes and configures the console device.
    // Parameters include a pointer to struct console and a character array for passing configuration options.
    
    int (*match)(struct console *, char *name, int idx, char *options);
    // Function pointer, points to a function that matches the console device.
    // Parameters include a pointer to struct console, device name, device index, and options string.
    
    short flags; 
    // Short integer variable for storing the status and attribute flags of the console.
    
    short index; 
    // Short integer variable for storing the index of the console device, distinguishing multiple devices of the same type.
    
    int cflag; 
    // Integer variable for storing the configuration flags of the console device.
    
    void *data; 
    // Pointer to private data related to the console device.
    
    struct console *next;
    // Pointer to the next struct console structure, used to link multiple console devices together, forming a linked list.
};

Simple Introduction to the 8250 Driver

  1. The UART layer contains two files: 8250_core.c and 8250_dw.c. Among them, 8250_core.c provides the basic operation interface with the UART hardware. 8250_dw.c completes support for the DesignWare controller. 8250_dw.c relies on the general functions provided by 8250_core.c to implement support for specific hardware.
  2. Due to the mature technology, wide compatibility, and stable performance of the 8250/16550 series serial port chips, many modern serial port chips are still based on the design of the 8250/16550 series.
  3. For the above reasons, 8250_core.c is the general part provided by the kernel, while 8250_dw.c is the implementation for specific hardware. The chip manufacturer is mainly responsible for making necessary configurations and extensions based on the existing driver. For example, we enter 8250_dw.c and find the conditional compilation of the macro CONFIG_ARCH_ROCKCHIP, which is used to indicate whether it is for Rockchip chips, thus completing specific functions.
  4. Although the 8250 driver is painful to look at, let me briefly introduce it. First, we look at the following module parameters in <span>kernel/drivers/tty/serial/8250/8250_core.c</span>. The manufacturer needs to pass parameters to complete the registration of <span>uart_driver</span> and <span>uart_ops</span>.
/* 
 * Define a kernel module parameter named share_irqs, of type unsigned integer (uint).
 * Parameter permission is 0644, indicating that this parameter can be read and modified by users.
 * 'other' indicates that the parameter type is hardware-related other properties (non-standard type).
 * Function: Allows configuration of whether to share IRQs (interrupt requests) with other non-8250/16x50 devices. Note that this sharing may be unsafe in some cases.
 */
module_param_hw(share_irqs, uint, other, 0644);
/* 
 * Describe the share_irqs parameter. It explains its function of whether to allow sharing IRQs with other non-8250/16x50 devices during device initialization.
 */
MODULE_PARM_DESC(share_irqs, "Share IRQs with other non-8250/16x50 devices (unsafe)");

/* 
 * Define a kernel module parameter named nr_uarts, of type unsigned integer (uint), permission 0644.
 * Function: Specifies the maximum number of UARTs supported.
 */
module_param(nr_uarts, uint, 0644);
/* 
 * Describe the nr_uarts parameter. It explains the maximum number of UARTs supported, ranging from 1 to CONFIG_SERIAL_8250_NR_UARTS.
 */
MODULE_PARM_DESC(nr_uarts, "Maximum number of UARTs supported. (1-" __MODULE_STRING(CONFIG_SERIAL_8250_NR_UARTS) ")");

/* 
 * Define a kernel module parameter named skip_txen_test, of type unsigned integer (uint), permission 0644.
 * Function: Specifies whether to skip the TXEN (transmit enable) error check during initialization.
 */
module_param(skip_txen_test, uint, 0644);
/* 
 * Describe the skip_txen_test parameter. It explains its function of whether to skip the TXEN error check during initialization.
 */
MODULE_PARM_DESC(skip_txen_test, "Skip checking for the TXEN bug at init time");

#ifdef CONFIG_SERIAL_8250_RSA
/* 
 * Define a kernel module parameter array named probe_rsa, of type unsigned long (ulong), bound to I/O ports.
 * Permission is 0444, read-only. Combined with the parameter probe_rsa_count to record the size of the array.
 * Function: Used to probe the I/O ports used by the RSA card.
 */
module_param_hw_array(probe_rsa, ulong, ioport, &probe_rsa_count, 0444);
/* 
 * Describe the probe_rsa parameter. It explains its function of probing the I/O ports used by the RSA (cryptographic accelerator) during system initialization.
 */
MODULE_PARM_DESC(probe_rsa, "Probe I/O ports for RSA");
#endif
  1. In the device tree, add relevant node description information, which will eventually match the kernel/drivers/tty/serial/8250/8250_dw.c driver. This driver parses the information in the device tree and then completes the registration of uart_port.
        uart1: serial@fe650000 {
                compatible = "rockchip,rk3568-uart", "snps,dw-apb-uart";
                reg = <0x0 0xfe650000 0x0 0x100>;
                interrupts = <GIC_SPI 117 IRQ_TYPE_LEVEL_HIGH>;
                clocks = <&cru SCLK_UART1>, <&cru PCLK_UART1>;
                clock-names = "baudclk", "apb_pclk";
                reg-shift = <2>;
                reg-io-width = <4>;
                dmas = <&dmac0 2>, <&dmac0 3>;
                pinctrl-names = "default";
                pinctrl-0 = <&uart1m1_xfer>; /*&uart1m0_xfer*/
                status = "disabled";
        };

        uart2: serial@fe660000 {
                compatible = "rockchip,rk3568-uart", "snps,dw-apb-uart";
                reg = <0x0 0xfe660000 0x0 0x100>;
                interrupts = <GIC_SPI 118 IRQ_TYPE_LEVEL_HIGH>;
                clocks = <&cru SCLK_UART2>, <&cru PCLK_UART2>;
                clock-names = "baudclk", "apb_pclk";
                reg-shift = <2>;
                reg-io-width = <4>;
                dmas = <&dmac0 4>, <&dmac0 5>;
                pinctrl-names = "default";
                pinctrl-0 = <&uart2m0_xfer>;
                status = "disabled";
        };

Minimal UART Driver

#include <linux/module.h>
#include <linux/serial_core.h>
#include <linux/tty.h>

#define MY_UART1_BASE 0x1234      // Base address of the first UART
#define MY_UART1_IRQ 36           // Interrupt number of the first UART
#define MY_UART2_BASE 0x5678      // Base address of the second UART
#define MY_UART2_IRQ 37           // Interrupt number of the second UART
#define MY_UART_CLOCK 48000000    // UART clock frequency
#define MY_UART_FIFO_SIZE 64      // FIFO size
#define MY_UART_REGISTER_SHIFT 2  // Register offset

static const char driver_name[] = "simple_uart";
static const char tty_dev_name[] = "ttySU";

static struct uart_ops simple_uart_ops = {
    .startup       = my_uart_startup,
    .shutdown      = my_uart_shutdown,
    .set_termios   = my_uart_set_termios,
    // ... other necessary operations ...
};

// Define the first UART port
static struct uart_port simple_port1 = {
    .iotype    = UPIO_MEM,
    .mapbase   = MY_UART1_BASE,
    .irq       = MY_UART1_IRQ,
    .uartclk   = MY_UART_CLOCK,
    .fifosize  = MY_UART_FIFO_SIZE,
    .regshift  = MY_UART_REGISTER_SHIFT,
    .flags     = UPF_BOOT_AUTOCONF,
    .ops       = &simple_uart_ops,
};

// Define the second UART port
static struct uart_port simple_port2 = {
    .iotype    = UPIO_MEM,
    .mapbase   = MY_UART2_BASE,
    .irq       = MY_UART2_IRQ,
    .uartclk   = MY_UART_CLOCK,
    .fifosize  = MY_UART_FIFO_SIZE,
    .regshift  = MY_UART_REGISTER_SHIFT,
    .flags     = UPF_BOOT_AUTOCONF,
    .ops       = &simple_uart_ops,
};

static struct uart_driver simple_uart_driver = {
    .owner = THIS_MODULE,
    .driver_name = driver_name,
    .dev_name = tty_dev_name,
    .major = TTY_MAJOR,
    .minor = 0,
    .nr = 2, // Set the number of ports to 2
    .cons = NULL,
};

static int __init simple_uart_init(void)
{
    int ret;

    // Register the serial port driver
    ret = uart_register_driver(&simple_uart_driver);
    if (ret)
        return ret;

    // Add the first UART port
    ret = uart_add_one_port(&simple_uart_driver, &simple_port1);
    if (ret)
        goto err_unregister_driver;

    // Add the second UART port
    ret = uart_add_one_port(&simple_uart_driver, &simple_port2);
    if (ret)
        goto err_remove_first_port;

    return ret;

err_remove_first_port:
    uart_remove_one_port(&simple_uart_driver, &simple_port1);
err_unregister_driver:
    uart_unregister_driver(&simple_uart_driver);
    return ret;
}

static void __exit simple_uart_exit(void)
{
    uart_remove_one_port(&simple_uart_driver, &simple_port2);
    uart_remove_one_port(&simple_uart_driver, &simple_port1);
    uart_unregister_driver(&simple_uart_driver);
}

module_init(simple_uart_init);
module_exit(simple_uart_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("Simple UART Driver");

Application Program

  1. For most people, UART development is done at the application layer. Here, I will provide a relatively general UART application development program.
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <termios.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <assert.h>
#include <getopt.h>  /* getopt_long() */
#include <errno.h>
#include <sys/time.h>
#include <sys/mman.h>
#include <sys/ioctl.h>

static void errno_exit(const char *s)
{
        fprintf(stderr, "%s error %d, %s\n", s, errno, strerror(errno));
        exit(EXIT_FAILURE);
}

/**
 * \brief Open serial port device
 * \param[in] p_path: device path
 * \retval Returns file descriptor on success, -1 on failure
 */
int uart_open(const char *p_path)
{
    /* O_NOCTTY: Prevents the operating system from making the opened file the controlling terminal of the process; if this flag is not specified, any input will affect the user's process. */
    /* O_NONBLOCK: Makes I/O non-blocking; if read is called and no data is received, it immediately returns -1 and sets errno to EAGAIN. */
    /* O_NDELAY: Same as O_NONBLOCK */
    return open(p_path, O_RDWR | O_NOCTTY);
}

/**
 * \brief Test function, prints the values of each member of struct termios
 */
#if 0
static void __print_termios(struct termios *p_termios)
{
    printf("c_iflag = %#010x\n", p_termios->c_iflag);
    printf("c_oflag = %#010x\n", p_termios->c_oflag);
    printf("c_cflag = %#010x\n", p_termios->c_cflag);
    printf("c_lflag = %#010x\n\n", p_termios->c_lflag);
}
#endif 
/**
 * \brief Set serial port attributes
 *
 * \param[in] fd: file descriptor of the opened serial port device
 * \param[in] baudrate: baud rate
 *            #{0, 50, 75, 110, 134, 150, 200, 300, 600, 1200, 1800,
 *              2400, 4800, 9600, 19200, 38400, 57600, 115200, 230400}
 * \param[in] bits: data bits
 *            #{5, 6, 7, 8}
 * \param[in] parity: parity
 *            #'n'/'N': no parity
 *            #'o'/'O': odd parity
 *            #'e','E': even parity
 * \param[in] stop: stop bits
 *            #1: 1 stop bit
 *            #2: 2 stop bits
 * \param[in] flow: flow control
 *            #'n'/'N': no flow control
 *            #'h'/'H': use hardware flow control
 *            #'s'/'S': use software flow control
 *
 * \retval Returns 0 on success, -1 on failure, and prints the reason for failure
 *
 * \note Although the baud rate setting supports so many values, it does not mean that the values in the input table can definitely be set successfully.
 */
int uart_set(int fd, int baudrate, int bits, char parity, int stop, char flow)
{
    struct termios termios_uart;
    int ret = 0;
    speed_t uart_speed = 0;

    /* Get serial port attributes */
    memset(&termios_uart, 0, sizeof(termios_uart));
    ret = tcgetattr(fd, &termios_uart);
    if (ret == -1) {
        printf("tcgetattr failed\n");
        return -1;
    }

    //__print_termios(&termios_uart);

    /* Set baud rate */
    switch (baudrate) {
        case 0:      uart_speed = B0;      break;
        case 50:     uart_speed = B50;     break;
        case 75:     uart_speed = B75;     break;
        case 110:    uart_speed = B110;    break;
        case 134:    uart_speed = B134;    break;
        case 150:    uart_speed = B150;    break;
        case 200:    uart_speed = B200;    break;
        case 300:    uart_speed = B300;    break;
        case 600:    uart_speed = B600;    break;
        case 1200:   uart_speed = B1200;   break;
        case 1800:   uart_speed = B1800;   break;
        case 2400:   uart_speed = B2400;   break;
        case 4800:   uart_speed = B4800;   break;
        case 9600:   uart_speed = B9600;   break;
        case 19200:  uart_speed = B19200;  break;
        case 38400:  uart_speed = B38400;  break;
        case 57600:  uart_speed = B57600;  break;
        case 115200: uart_speed = B115200; break;
        case 230400: uart_speed = B230400; break;
        case 460800: uart_speed = B460800; break;
        case 500000: uart_speed = B500000; break;
        case 921600: uart_speed = B921600; break;
        default: printf("Baud rate not supported\n"); return -1;
    }
    cfsetspeed(&termios_uart, uart_speed);

    /* Set data bits */
    switch (bits) {
        case 5:     /* Data bits 5 */
            termios_uart.c_cflag &= ~CSIZE;
            termios_uart.c_cflag |= CS5;
        break;

        case 6:     /* Data bits 6 */
            termios_uart.c_cflag &= ~CSIZE;
            termios_uart.c_cflag |= CS6;
        break;

        case 7:     /* Data bits 7 */
            termios_uart.c_cflag &= ~CSIZE;
            termios_uart.c_cflag |= CS7;
        break;

        case 8:     /* Data bits 8 */
            termios_uart.c_cflag &= ~CSIZE;
            termios_uart.c_cflag |= CS8;
        break;

        default:
            printf("Data bits not supported\n");
            return -1;
    }

    /* Set parity bits */
    switch (parity) {
        case 'n':   /* No parity */
        case 'N':
            termios_uart.c_cflag &= ~PARENB;
            termios_uart.c_iflag &= ~INPCK;        /* Disable input parity check */
        break;

        case 'o':   /* Odd parity */
        case 'O':
            termios_uart.c_cflag |= PARENB;
            termios_uart.c_cflag |= PARODD;
            termios_uart.c_iflag |= INPCK;        /* Enable input parity check */
            termios_uart.c_iflag |= ISTRIP;        /* Remove the eighth bit (parity bit) */
        break;

        case 'e':   /* Even parity */
        case 'E':
            termios_uart.c_cflag |= PARENB;
            termios_uart.c_cflag &= ~PARODD;
            termios_uart.c_iflag |= INPCK;        /* Enable input parity check */
            termios_uart.c_iflag |= ISTRIP;        /* Remove the eighth bit (parity bit) */
        break;

        default:
            printf("Parity not supported\n");
            return -1;
    }

    /* Set stop bits */
    switch (stop) {
        case 1: termios_uart.c_cflag &= ~CSTOPB; break; /* 1 stop bit */
        case 2: termios_uart.c_cflag |= CSTOPB;  break; /* 2 stop bits */
        default: printf("Stop bits not supported\n");
    }

    /* Set flow control */
    switch (flow) {
        case 'n':
        case 'N':   /* No flow control */
            termios_uart.c_cflag &= ~CRTSCTS;
            termios_uart.c_iflag &= ~(IXON | IXOFF | IXANY);
        break;

        case 'h':
        case 'H':   /* Hardware flow control */
            termios_uart.c_cflag |= CRTSCTS;
            termios_uart.c_iflag &= ~(IXON | IXOFF | IXANY);
        break;

        case 's':
        case 'S':   /* Software flow control */
            termios_uart.c_cflag &= ~CRTSCTS;
            termios_uart.c_iflag |= (IXON | IXOFF | IXANY);
        break;

        default:
            printf("Flow control parameter error\n");
            return -1;
    }

    /* Other settings */
    termios_uart.c_cflag |= CLOCAL;    /* Ignore modem control lines */
    termios_uart.c_cflag |= CREAD;    /* Enable reception */

    /* Disable implementation-defined output processing, meaning that some special data will be processed specially; if disabled, it will output raw data */
    termios_uart.c_oflag &= ~OPOST;

    /**
     *  Set local mode bits to raw mode
     *  ICANON: Canonical input mode; if set, special characters like backspace will have actual effects
     *  ECHO: Echoes input characters back to the terminal device
     *  ECHOE: If ICANON is also set, receiving the ERASE character will erase one character from the displayed characters
     *         In simple terms, when the backspace key is received, the displayed content will delete one character back
     *  ISIG: Makes signals generated by the terminal take effect. (For example, pressing ctrl+c can exit the program)
     */
    termios_uart.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG);

    /**
     * Set wait time and minimum received characters
     * These two values are only meaningful in blocking mode, meaning that O_NONBLOCK cannot be passed when opening.
     * If after c_cc[VTIME] this long time has passed and there is data in the buffer, but it has not reached c_cc[VMIN] characters, read will also return. However, if c_cc[VMIN] characters are in the buffer, read will return regardless of whether the wait time has reached c_cc[VTIME], but the return value may be larger than c_cc[VMIN]. If c_cc[VMIN] is set to 0, then when c_cc[VTIME] time has passed, read will also return, returning 0. If both c_cc[VTIME] and c_cc[VMIN] are set to 0, the program will run similarly to setting O_NONBLOCK; the difference is that if O_NONBLOCK is set, read returns -1 when there is no data, while if O_NONBLOCK is not set, read returns 0 when there is no data.
     */
    termios_uart.c_cc[VTIME] = 1;   /* Set wait time, unit 1/10 second */
    termios_uart.c_cc[VMIN]  = 1;    /* Minimum read one character */

    tcflush(fd, TCIFLUSH);          /* Clear read buffer */

    /* Write configuration */
    ret = tcsetattr(fd, TCSANOW, &termios_uart);
    if (ret == -1) {
        printf("tcsetattr failed\n");
    }

    return ret;
}

char* dev_name = "/dev/ttyS1";  // Modify this parameter for different boards
int baudrate = 115200;
int flow_ctrl_flag = 0;
int show_hex_flag = 1;

static void usage(FILE *fp, int argc, const char **argv)
{
    fprintf(fp,
             "Usage: %s [options]\n"
             "default device name: /dev/ttyUSB0\n"
             "default baud rate: 115200\n"
             "default show: hex format\n"
             "default flow ctrl: no\n"
             "Options:\n"
             "-d | --device        uart device name [%%s]\n"
             "-h | --help          Print this message\n"
             "-b | --baudrate      set baudrate %%d\n"
             "-s | --string        format string show\n"
             "-f | --flow_ctrl     add hardware flow ctrl\n"
             "",
             argv[0], dev_name, baudrate);
}

static const struct option long_options[] = {
        { "help",      no_argument,           NULL, 'h' },
        { "device",    required_argument,     NULL, 'd' },
        { "baudrate",  required_argument,     NULL, 'b' },
        { "string",    no_argument,           NULL, 's' },
        { "flow_ctrl", no_argument,           NULL, 'f' },
        { 0, 0, 0, 0 }
};

static const char short_options[] = "hd:b:sf";
// Parameter parsing
void option_par(int argc, char const** argv)
{
    for (;;) {
            int idx;
            int c;

            c = getopt_long(argc, (char * const*)argv,
                            short_options, long_options, &idx);

            if (-1 == c)
                    break;

            switch (c) {
            case 0: /* getopt_long() flag */
                    break;

            case 'd':
                    dev_name = optarg;
                    break;

            case 'h':
                    usage(stdout, argc, argv);
                    exit(EXIT_SUCCESS);

            case 's':
                    show_hex_flag = 0;
                    printf("format string\r\n");
                    break;

            case 'f':
                    flow_ctrl_flag = 1;
                    printf("add hardware flow ctrl\r\n");
                    break;

            case 'b':
                    errno = 0;
                    baudrate = strtol(optarg, NULL, 0);
                    printf("set baud rate is : %d\r\n", baudrate);
                    if (errno)
                            errno_exit(optarg);
                    break;

            default:
                    usage(stderr, argc, argv);
                    exit(EXIT_FAILURE);
            }
    }
}

int main(int argc, const char *argv[])
{
    int fd = 0;
    int ret = 0;
    char buf[4096] = { 0 };
    int len = 0;
    int i = 0;

    option_par(argc, argv);

    /* Open serial port device */
    fd = uart_open(dev_name);
    if (fd < 0) {
        printf("open %s failed\n", dev_name);
        return 0;
    }

    /**
     * Configure serial port:
     * Baud rate: baudrate
     * Data bits: 8
     * Parity  : no parity
     * Stop bits: 1
     * Flow ctrl:
     */
    ret = uart_set(fd, baudrate, 8, 'n', 1, flow_ctrl_flag ? 'h' : 'n');
    if (ret == -1) {
        return 0;
    }
        
        printf("%s baudrate=%d, flow_ctrl_flag=%d\n", dev_name, baudrate, flow_ctrl_flag);

    while (1) {
        memset(buf, 0, sizeof(buf));
        len = read(fd, buf, sizeof(buf));
        if (len > 0) {
            printf("len: %d\n", len);
            printf("data: ");
            if (show_hex_flag) {
                for (i = 0; i < len; i++) {
                    printf("%02x ", buf[i] & 0xff);
                }
                printf("\r\n");
            } else {
                printf("%s\r\n", buf);
            }
        }
        }
        close(fd);
        return 0;
}

Debugging

Driver Debugging

  1. Enter <span>kernel/include/linux/tty_flip.h</span> to find the <span>tty_insert_flip_char</span> function, where <span>port->buf</span> contains the raw data packets actually received by the hardware.
  2. Enter <span>kernel/drivers/tty/serial/serial_core.c</span> to find the <span>uart_write</span> function, where the buf parameter is the data sent from the application layer.
  3. Use the following proc files for debugging.
# Check interrupt counts to determine if the physical device is transmitting data
cat /proc/interrupts
# List information about all TTY drivers in the system, including driver names, versions, etc.
cat /proc/tty/drivers
# View the status and statistics of TTY devices
cd /proc/tty/driver/

Changing Serial Port Names

  1. In some scenarios, we may want to fix the device node name, as the device node name to be debugged may change due to different loading times.
  2. To facilitate debugging, we can fix the serial port name.
  • We can first check the vendor ID and product ID of the corresponding device.
    • Insert the USB to serial device and use the lsusb command to view the basic information of the device, including the vendor ID and product ID.
    • Use the dmesg command to view the system log and obtain detailed information about the device, such as device path and serial number.
  • Create a udev rules file:
    • Create a new rules file in the /etc/udev/rules.d/ directory, for example, 99-usb-serial.rules.
    • Add a rule in the file specifying the vendor ID, product ID, and serial number of the device, and assign a fixed node name to it. An example is as follows:
SUBSYSTEM=="tty", ATTRS{idVendor}=="1234", ATTRS{idProduct}=="5678", ATTRS{serial}=="0001", NAME="ttyUSB10"
  • Reload the udev rules:
sudo udevadm control --reload-rules
sudo udevadm trigger
  • Verify the configuration:
    • Disconnect and reconnect the USB to serial device.
    • Use the ls -l /dev/ttyUSB10 command to check if a new device node has been created and verify that it points to the correct device file.

References

  1. [Driver] Serial Driver Analysis (Part 3) – Serial Driver

Leave a Comment