1. UART
    1. Baud rate
    2. Serial Communication Protocol
      1. Data Stream Structure
      2. Timing Waveform Analysis
    3. Types of Serial Communication Interfaces
      1. RS232 interface
        1. DB9 pin description
        2. Level Characteristics
      2. RS485 Interface
        1. Features and Advantages
        2. Level characteristics
        3. SIT3485E chip
          1. Self-transceiver 485 circuit
  2. Serial Subsystem Framework
    1. Configuring the serial driver
    2. Analysis of uart_driver Registration Process
      1. UART Related Low-Level Structures
        1. struct uart_driver
        2. struct uart_port
        3. struct uart_state
        4. struct uart_ops
      2. Analysis of uart_driver registration
        1. serial8250_init()
        2. Initialize the 8250 serial port
          1. serial8250_isa_init_ports()
          2. serial8250_init_port()
        3. Register UART driver
          1. uart_register_driver()
          2. tty_register_dirver()
        4. Allocate a platform device structure and register it
        5. Register serial port
          1. serial8250_register_ports()
        6. Register the platform_driver
    3. Port registration process analysis
      1. dw_probe()
      2. serial8250_register_8250_port()
      3. serial8250_find_match_or_unused()
      4. tty_port_register_device_attr_serdev()
      5. tty_register_device_attr()
      6. tty_cdev_add()
  3. Serial port programming
    1. Serial port device node
    2. struct termios structure
      1. Input mode
      2. Output mode
      3. Control mode
        1. Baud Rate bitmask
        2. Data Bits Bitmask
        3. Stop Bits Bitmask
        4. Other Control Flags
      4. Local mode
      5. Special control characters
    3. Common Serial Port Control Functions
      1. tcgetattr()
      2. tcsetattr()
      3. cfgetispeed() and cfgetospeed()
      4. cfsetispeed() and cfsetospeed()
      5. tcflush() and tcflow()
        1. tcflush()
        2. tcflow()
    4. Serial Port Operation Flow
      1. Set the Baud Rate of the Serial Port
      2. Setting the Data Bit Size
      3. Set the parity bit.
        1. Odd parity enable
        2. Even parity enable
        3. No parity
      4. Set stop bits
      5. Example
  4. GPS module programming
    1. Introduction to GPS data frames
    2. Example
      1. gps.h
      2. gps.c
      3. uart.c
Cover image for Linux UART

Linux UART


Timeline

Timeline

2025-12-31

init

This article introduces the basics of Linux UART serial communication, discussing in detail the fundamental concepts of serial ports, the difference and conversion relationship between baud rate and bit rate, the data frame structure and timing waveforms of serial communication protocols, and summarizes common interface level types such as TTL and RS232 along with their conversion requirements.

Linux Driver Notes

Table of ContentsLinks
1. Linux Driver Framework
2. Linux Driver Loading Logic
3. Character Device Basics
4. Concurrency and Competition
5. Advanced Character Device Progression
6. Interrupts
7. Platform Bus
8. Device Tree
9. Device Model
10. Hotplug
11. pinctrl Subsystem
12. gpio Subsystem
13. Input Subsystem
14. Single Bus
15. I2C
16. SPI
17. UART
18. PWM
19. RTC
20. Watchdog
21. CAN
22. Network Device
23. ADC
24. IIO
25. USB
26. LCD

UART

Serial Port, also known as serial communication interface, often calledCOM interface, is a type ofasynchronous full-duplex interfacefor data communication between a computer and external devices (such as serial communication devices). It usesserial transmissionmethod, which transmits data one bit at a time.

Specifically, typical serial communication requires only three wires: ground (GND), transmit (TX), and receive (RX). As shown in the diagram below, transmission and reception each use one line,No clock line

Connection between CPU and serial port device
Connection between CPU and serial port device

Baud rate

Baud rate is a key parameter in serial communication, referring to the number of bits or symbol rate transmitted per second.

In a digital channel, a pulse signal is a symbol, as shown in the figure below. The symbol rate indicates how many symbols or pulse signals can be sent in one second.

Baud rate
Baud rate

Both communicating parties must set the same baud rate to ensure correct data transmission. Common standard baud rates such as 9600, 115200, etc., usually meet most application requirements.

However, in specific cases, a non-standard baud rate may need to be set, and it must be ensured that all communication devices can support and correctly configure that baud rate.

For baud rates higher than 1.5 Mbps, it may be necessary to achieve them through fractional or integer division of the clock.

If the required baud rate cannot be achieved through division, it may be necessary to adjust the PLL (Phase-Locked Loop) settings. Adjusting the PLL carries certain risks as it may affect other modules of the device.

Bit raterefers tothe number of bits transmitted per unit time, usually expressed in bps (bits per second), with the unit bit/s. In comparison,baud ratethenthe number of symbols or pulse signals transmitted per second. The relationship between the two can be expressed by the formulabit rate = baud rate * log2(M), where M represents the amount of information carried by each symbol.

A symbol is essentially a pulse signal, which may carry 1 bit, 2 bits, or more bits of data, depending on the specific implementation of the communication system.In a binary system, the bit rate equals the baud rate because each symbol carries exactly 1 bit of information

For example: if the baud rate of a serial port is 9600, how many bytes can be transmitted per second in a binary system?

One byte equals 8 bits, i.e., 8 high and low level changes, because in a binary system, the bit rate equals the baud rate. Therefore, the number of bytes that can be transmitted per second is 9600/8 = 1200 bytes.

Serial Communication Protocol

Data Stream Structure

In serial communication, besides paying attention to the baud rate, the structure of the data stream is also crucial.

Each frame of data includes 11 bits:

  • 1 start bit
  • 8 data bits
  • 1 parity bit
  • 1 stop bit

Serial data stream structure
Serial data stream structure

  • Start bit: Indicates the start of data transmission. The idle state on the data line is 1, and pulling it low from high level (idle state) indicates the start of data transmission.

  • Data bits: Refers to the number of data bits per byte, typically 7 or 8 bits.

  • Parity bit: Used to verify the accuracy of transmitted data. Its types include odd parity, even parity, space parity, and mark parity.

  • Odd parity: When the number of 1s in the actual data is even, the parity bit is 1; otherwise, the parity bit is 0.

  • Even parity: When the number of 1s in the actual data is even, the parity bit is 0; otherwise, the parity bit is 1.

  • Space parity: The parity bit is always 0; if it is 1, it indicates an error.

  • 1 Mark Parity: The parity bit is always 1; if it is 0, it indicates an error.

  • Stop Bit: Refers to the number of bits sent after each data byte transmission, typically 1 or 2 bits.

Timing Waveform Analysis

Logic Analyzer Waveform
Logic Analyzer Waveform

Whenthe data line is at a high level when idle, and duringdata transmission, it is pulled low. The first pulse in the diagram corresponds to the start bit. Following it are 8 data bits, transmitted in order of least significant bit (LSB) first.

For example, the data bits “00110001” convert to hexadecimal 0x31, which is 49 in decimal. In ASCII, 49 corresponds to the character ‘1’, so the transmitted data is 1. After data transmission is complete, the bus is pulled high.

Types of Serial Communication Interfaces

UART only specifies the timing of transmission and reception, i.e., “send start bit first, then data bits, parity bit, and finally stop bit”. It only defines high and low levels, but does not specify what voltage the high level or low level corresponds to.

Common interface levels for serial ports includeTTLRS232,RS485,RS422, andEach interface typically requires a corresponding level-shifting chip. DirectlyWhen directly using the serial port interface from the processor, it is usually TTL level

However, different models or suppliers of processors may have level differences, meaning that in some cases devices cannot be directly connected. Therefore, proper level shifting must be performed to ensure normal communication. A general comparison of serial port interface levels is shown in the table below:

Level StandardLogic Level DefinitionTypical High/Low Level VoltageSignal TypeMaximum Transmission DistanceMain Features
TTL0 = Low Level
1 = High Level
Low Level: 0 V High Level: 1.8 V / 2.5 V / 3.3 V / 5 V (depending on supply)Single-Ended SignalSeveral meters (typically ≤ 2–5 m)Short-distance communication within or between boards, directly compatible with MCUs, sensors, etc.; weak anti-interference capability, not suitable for long distances
RS-2320 = Positive voltage(+3 V ~ +15 V)
1 = Negative voltage(-3 V ~ -15 V)
Logic 0 (space): +3 V ~ +15 V; Logic 1 (mark): -3 V ~ -15 V; commonly ±12 VSingle-ended (but uses positive and negative voltages)Approximately 15–50 meters (lower rate allows longer distance)Supports point-to-point communication, stronger anti-interference than TTL, requires level conversion chips (e.g., MAX232), gradually being replaced by USB, etc.
RS-422Differential signal:
1 = A > B(+2 V ~ +6 V)
0 = A < B(-2 V ~ -6 V)
Differential voltage: ±2 V ~ ±6 VDifferential signal(Full-duplex, 4-wire)Maximum approximately1200 meters(@ 100 kbps)Supports full-duplex communication, strong common-mode interference rejection, suitable for industrial environments, typically 1 transmitter and multiple receivers
RS-485Differential signal:
1 = A > B(+1.5 V ~ +5 V)
0 = A < B(-1.5 V ~ -5 V)
Differential voltage: ≥ ±1.5 V (typical ±2 V ~ ±5 V)Differential signal(Half-duplex or full-duplex, 2 or 4 wires)Maximum approximately1200 meters(@ 100 kbps) (Theoretically up to 1219 meters)Supports multi-point communication (up to 32~256 nodes), widely used in industrial buses (e.g., Modbus), strong anti-interference, suitable for long-distance, noisy environments

RS232 interface

RS232(Recommended Standard 232)The protocol is a serial communication standard established by the Electronic Industries Association in 1970. This standardunifies the connector and pin definitions for serial communication, as shown in the figure below, and clearly specifies the voltage level standards for each connector pin

DB9 male and female connectors
DB9 male and female connectors

DB9 pin description

DB9 is a common serial connector, typically used for RS-232 serial communication and other serial communication applications. It contains 9 pins, each with a specific function. Below is the pin description of the DB9 connector:

  • Pin 1 - DCD (Data Carrier Detect): Data Carrier Detect. Indicates whether the remote device is ready for communication.
  • Pin 2 - RXD (Receive Data): Receive Data. Receives data stream from the remote device.
  • Pin 3 - TXD (Transmit Data): Transmit Data. Sends data stream to the remote device.
  • Pin 4 - DTR (Data Terminal Ready): Data Terminal Ready. Indicates that the data terminal equipment (e.g., computer) is ready for communication.
  • Pin 5 - GND (Ground): Ground. Electrical ground, used for circuit reference and shielding.
  • Pin 6 - DSR (Data Set Ready): Data Set Ready. Indicates that the remote device is ready to receive and send data.
  • Pin 7 - RTS (Request to Send): Request to Send. The sender uses this signal to request starting data transmission.
  • Pin 8 - CTS (Clear to Send): Clear to Send. The receiver uses this signal to indicate readiness to receive data.
  • Pin 9 - RI (Ring Indicator): Ring Indicator. Indicates that the remote device has sent a ring signal.

RS232 is essentially also a serial port protocol, identical to the serial port protocol. However, it specifies the physical interface and level characteristics of the serial port, so it differs at the hardware level, but there is no difference in software programming and serial communication implementation.

Level Characteristics

  • The electrical signal at the RS232 receive-transmit end isrelative to the common ground(GND)a voltage signal

    • In the RS232 standard,A voltage difference between +3V and +15V is defined as logic “0”, while a voltage difference between**-3V and -15V represents logic “1”**. The voltage difference between -3V and +3V is undefined.
    • Typically, in practical applications, the absolute value of the voltage difference is expected to be between 5V and 15V to ensure reliable signal transmission.
    • When transmitting data, the driver at the sending end outputs a positive voltage signal of +5V to +15V to represent logic “0”, and a negative voltage signal of -5V to -15V to represent logic “1”.
    • When receiving data, any electrical signal greater than 3V is considered a valid signal.
  • The RS232 interface has relatively high current capability during transmission and reception, allowing it to handle relatively large current loads, which makes it suitable for long-distance communication and connecting external devices.

  • RS232 signals have high anti-interference capability, enabling stable operation in industrial environments and being less susceptible to electromagnetic interference.

  • RS232 includes the following signal lines:

    • Transmit line (Tx)
    • Receive line (Rx)
    • Ground line
    • Data Terminal Ready (DTR)
    • Data Set Ready (DSR)
    • Request to Send (RTS)
    • Clear to Send (CTS)
    • Data Set Ready (DSR)
  • RS232 supports various baud rates, typically from low to high speeds, up to hundreds of kilobits per second (kbps).

On the iTOP-RK3568 development board, the debug serial port uses the MAX3232 chip to convert TTL levels to RS-232 levels. The schematic diagram of the debug serial port is shown below.

Debug UART2
Debug UART2

RS485 Interface

The RS485 standard was jointly developed by the Telecommunications Industry Association (TIA) and the Electronic Industries Alliance (EIA). The main purpose of this standard is toaddress the need for long-distance communication(up to 1200 meters)and provide excellent anti-interference performance

Features and Advantages

  • Long-distance communication capability: RS485 can achieve a communication distance of up to 1200 meters under ideal conditions, making it suitable for scenarios requiring long-distance data transmission.
  • Strong anti-interference capability: Due to the use of differential signal transmission, RS485 can effectively resist electromagnetic interference (EMI) and radio frequency interference (RFI), ensuring the stability and reliability of data transmission.
  • Multi-station capability: RS485 supports communication between multiple devices (up to 32) on the same bus, with each device able to independently send and receive data, enabling flexible network configuration.
  • Wide application: RS485 is widely used in industrial automation control systems, building automation, smart home systems, power system monitoring, and other fields, meeting the requirements for long-distance, high-speed, and reliable communication.

Level characteristics

The level characteristics of RS485 are as follows:

  • Differential signal transmission: RS485 uses differential signals for data transmission, meaning the data signal is represented by the voltage difference between two signal lines (typically labeled A and B). This differential signal transmission method gives RS485 good anti-interference capability and long-distance transmission capability.
  • Voltage range
    • Logic ‘1’ is represented by a voltage difference of +(2~6)V between the two lines
    • Logic ‘0’ is represented by a voltage difference of -(2~6)V between the two lines
    • Interface signal level ratio RS232 is reduced, making it less likely to damage the interface circuit chip, andthis level is compatible with TTL Level compatibility, which can be conveniently connected to TTL circuits.
  • Current capability: The RS485 transmitter has strong driving capability, enabling it to drive long communication lines and multiple receivers. The receiver can handle larger input currents to ensure reliable signal reception.
  • Electrical characteristics: RS485 supportsmultiple devices(up to 32 a/an (general measure word))communicating on the same bus. It supports communication distances up to 1200 meters, and can even be longer under certain conditions.

The iTOP-RK3568 development board uses theSIT3485E chipto convert TTL levels to 485 levels. The schematic diagram is shown below:

Convert TTL levels to 485 levels
Convert TTL levels to 485 levels

It can be seen that the RS485 interface of the RK3568 development board is actually serial port 7 converted through the SIT3485E chip.

SIT3485E chip

SIT3485E Chip Features
SIT3485E Chip Features

The SIT3485E is a wide power supply range of 3.0V~5.5V, with bus port ESD level exceeding 15KV HBM and bus withstand voltage range reaching ±15V,half-duplexlow power consumptionan RS-485 transceiver whose functions fully meet the requirements of the TIA/EIA-485 standard

The SIT3485E includesa driveranda receiver, both of which can beindependently enabled and disabled. When both are disabled, the driver and receiver both output a high-impedance state.

The SIT3485E has a 1/8 load, allowing up to 256 SIT3485E transceivers to be connected in parallel on the same communication bus. It can achieve error-free data transmission up to 12Mbps.

The SIT3485E operates over a supply voltage range of 3.0~5.5 V and features fail-safe, current limiting protection, overvoltage protection, and other functions.

Pin Configuration Diagram
Pin Configuration Diagram

Pin NumberPin NamePin Function
1ROReceiver Output. When /RE is low, if A-B ≥ -10mV, RO outputs high; if A-B ≤ -200mV, RO outputs low.
2/REReceiver Output Enable Control. When /RE is low, receiver output is enabled and RO is active; when /RE is high, receiver output is disabled and RO is high impedance; when /RE is high and DE is low, the device enters low-power shutdown mode.
3DEDriver Output Enable Control. When DE is high, driver output is active; when DE is low, output is high impedance; when /RE is high and DE is low, the device enters low-power shutdown mode.
4DIDriver Input. When DE is high, a low level on DI makes driver non-inverting output A low and inverting output B high; a high level on DI makes non-inverting output high and inverting output low.
5GNDGround
6AReceiver Non-inverting Input and Driver Non-inverting Output
7BReceiver Inverting Input and Driver Inverting Output
8VCCPower Supply

From the pin definition diagram, it can be seen that:

  • The RO pin is the receiver output, connected to UART7 in the schematic diagram_RX_M1 pin.
  • The DI pin is the driver input pin, connected to UART7 in the schematic diagram_TX_M1 pin.
  • The A pin is the receiver non-inverting input and driver non-inverting output.
  • The B pin is the receiver inverting input and driver inverting output. A and B form the RS485 differential pair.
  • REThe /RE pin is the receiver output enable control pin.
    • When /RE is low, the receiver output is enabled, and RO output is active.
    • When /RE is high, the receiver output is disabled, and RO is in high-impedance state.
  • The DE pin is the driver output enable control pin. When DE is high, the driver output is active; when DE is low, the output is in high-impedance state.
  • REWhen /RE is high and DE is low, the device enters low-power mode.

In simple terms:

  • REWhen /RE is low, RO output is active. Otherwise, RO is in high-impedance state, meaning inactive. RO is connected to UART7_RX_M1,UART7_RX_M1 is the serial port's receive pin. Therefore, when /RE is low, the serial port can receive data; otherwise, the serial port cannot receive data.
  • When DE is high, DI is active, and DI is connected to UART7._TX_M1,UART7_RX_M1 is the serial port transmit pin. Therefore, when DE is high, the low level on DI causes the driver’s non-inverting output A to be low and the inverting output B to be high, allowing the serial port to send data; otherwise, the serial port cannot send data.

REis exactly opposite to the active level of DE., and because RS-485 ishalf-duplex, it cannot transmit and receive simultaneously. Therefore,REand DE definitely cannot be enabled at the same time,that is,REthe levels of and DE must be the same.. In this way,REwhen is enabled, it is equivalent to DE being disabled, so these two pins are connected together.

This makes it very clear: if GPIO0_C6 outputs a low level,REand DE are low,REwhen is low, the serial port can receive data; when DE is low, the serial port cannot send data.

If GPIO0_C6 outputs a high level,REWhen both and DE are high,REWhen is high, the serial port cannot receive data; when DE is high, the serial port can send data.

That is, we need to use GPIO0_The high or low level of pin C6 controls whether RS485 is transmitting or receiving. Therefore, we need to write a driver to achieve this. The source code provided by Xunwei has a default driver configured to control GPIO0_C6。

Self-transceiver 485 circuit

In addition to software control, automatic switching can also be achieved through hardware to realize the automatic transceiver function of RS485. There is a certain time delay in software-controlled transmission and reception. To reduce this delay, the baseboard schematic of the Xunwei development board has been optimized to be compatible with hardware-implemented automatic transceiver. The specific modification method is to remove R295 and solder all components marked as DNP in the figure below.

UART7_M1 To RS485
UART7_M1 To RS485

  • When UART_TX_When M1 is high, the base of Q17 is also high, causing Q17 to conduct,REand DE are low, so the serial port is in receive data mode
  • When UART_TX_When M1 is low, the base of Q17 is low, causing Q17 to cut off,REand DE are high, the serial port is in transmit data mode. Since UART_TX_M1 is high when idle, indicating the serial port is in receive data state; when UART_TX_M1 is pulled low, the serial port is in transmit data state.

Since UART_TX_M1 high level indicates receive mode, so when sending a 1, it is also high level. This would keep the chip in receive mode, preventing the 1 from being sent out, right?

When the transceiver chip is in receive mode, both pins A and B are in a high-impedance state.

High-impedance state means a very large resistance, almost equivalent to an open circuit.At this point, A is pulled high by a pull-up resistor, and B is pulled low by a pull-down resistor.

Therefore,**A high and B low represent a logic 1 in communication.**Through this ‘receive mode’, we cleverly send out a ‘1’.

This optimization eliminates the need for an RS485 transceiver control IO, allowing RS485 to be used entirely as a serial port, making driver development easier.

Serial Subsystem Framework

The serial subsystem framework is a modular framework in the Linux kernel specifically designed for handling serial port devices. The framework diagram is shown below.

Serial Subsystem Framework
Serial Subsystem Framework

  • Application Layer: Located at the topmost layer, it is the interface between user-space applications and kernel space in the serial subsystem. The application layer includes user-space serial port applications, such as the serial communication tool minicom.
  • Character Device Layer: Located below the application layer, it is responsible for passing serial port read/write requests from user space to the tty_core layer in kernel space. The character device layer treats the serial port device as a special character device and operates through the character device interface.
  • tty_core layer: Located below the character device layer, it is the core module in the Linux kernel for managing serial port devices. It handles basic functions of serial port devices, such as data transmission, control, and buffer management. The tty_core layer is independent of specific serial hardware and serves as a generic processing layer for serial devices.
  • uart_core layer: Located below the tty_core layer, it provides the low-level driver interface for serial port devices and is responsible for communicating with specific serial hardware. The uart_core layer is responsible for controlling low-level operations such as serial data transmission and reception, interrupt handling, and clock management.
  • Hardware layer: Located at the bottom layer, it is the part of the serial subsystem that is specific to the hardware. The hardware layer includes the driver for the serial hardware, communicates with the specific serial controller, and implements low-level control and operation of the hardware.

Configuring the serial driver

In the SDK kernel source code provided by Rockchip, the serial driver uses the 8250 generic serial driver. The following are the main driver files:

  • drivers/tty/serial/8250/8250_core.c8250 serial driver core
  • drivers/tty/serial/8250/8250_dw.cSynopsis DesignWare 8250 serial driver
  • drivers/tty/serial/8250/8250_dma.c8250 serial DMA driver
  • drivers/tty/serial/8250/8250_port.c8250 serial port operations
  • drivers/tty/serial/8250/8250_early.c8250 serial early console driver

Select the driver in make menuconfig

1
2
Device Driver / Character devices / Serial drivers
[*] Console on 8250/16550 and compatible serial port

In the device tree of the SDK source code provided by Xunwei, UART 9 is enabled by default. Openarch/arm64/boot/dts/rockchip/rk3568.dtsithe device tree file. The device tree node for the UART 9 controller is as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
uart9: serial@fe6d0000 {
compatible = "rockchip,rk3568-uart", "snps,dw-apb-uart";
reg = <0x0 0xfe6d0000 0x0 0x100>;
interrupts = <GIC_SPI 125 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&cru SCLK_UART9>, <&cru PCLK_UART9>;
clock-names = "baudclk", "apb_pclk";
reg-shift = <2>;
reg-io-width = <4>;
dmas = <&dmac0 18>, <&dmac0 19>;
pinctrl-names = "default";
pinctrl-0 = <&uart9m0_xfer>;
status = "disabled";
};
  • compatible: Specifies the compatibility string of the device, indicating that this UART device is compatible withrockchip,rk3568-uartandsnps,dw-apb-uarttwo types of UART controllers. This helps the device tree bind the corresponding driver.
  • reg: Specifies the address and size of the UART device. 0xfe6d0000 is the base address of the UART device, and 0x100 indicates the size of the address space.
  • interrupts: Specifies the interrupt information of the UART device, including the interrupt type and interrupt number.
  • clocks: Specifies the clock sources used by the UART device, including the baud rate clock and the APB clock.
  • clock-names: Specifies the name of the clock source, used to match the specific configuration of the clock source.
  • reg-shift: Indicates the number of bits for the address offset, i.e., whether the offset of each register is in bytes or words.
  • reg-io-width: Indicates the access width for device addresses and data. Here, 4 means a width of 4 bytes.
  • dmas: Specifies the DMA controller and DMA channel number used by the UART device for DMA data transfer operations.
  • dma-names: Optional values are
    • txEnable TX DMA
    • rxEnable RX DMA
    • !txDisable TX DMA
    • !rxDisable RX DMA
  • pinctrl-namesandpinctrl-0: Used for pin control of serial port devices to configure and manage pin settings. Optional parameters are as follows:
    • &uart9m0_xferConfigure TX and RX pins as iomux group 0
    • &uart9m1_xferConfigure TX and RX pins as iomux group 1
    • &uart9m0_ctsnand&uart1m0_rtsnConfigure hardware auto flow control CTS and RTS pins as iomux group 0
    • &uart9m1_ctsnand&uart1m1_rtsnConfigure hardware auto flow control CTS and RTS pins as iomux group 1
  • status: Indicates the status of the serial port device. Here, “disabled” means the device is currently disabled. If set to “okay”, it means the device is enabled.

Analysis of uart_driver Registration Process

The relationship between UART related low-level structures is as follows

Relationship between UART-related underlying structures
Relationship between UART-related underlying structures

struct uart_driver

uart_driverThe structure represents the UART driver,uart_driverdefined in theinclude/linux/serial_core.hfile, with the following content:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct uart_driver {
struct module *owner;// Module owner
const char *driver_name;// Driver name
const char *dev_name;// Device name
int major;// Major device number assigned to the device
int minor;// Minor device number assigned to the device
int nr;// Unique identifier of the device
struct console *cons;// Pointer to the console

/*
* these are private; the low level driver should not
* touch these; they should be initialised to NULL
*/
struct uart_state *state;// Pointer to the UART driver state
struct tty_driver *tty_driver;// Pointer to the TTY driver
};

struct uart_driverencapsulatestty_driver, so that the underlying UART driver does not need to care abouttty_driver

struct uart_port

uart_port is an abstraction for a serial port, defined inkernel/include/linux/serial_core.h, which internally contains auart_statemember variable of type, as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
struct uart_port {
spinlock_t lock; /* port lock */
unsigned long iobase; /* in/out[bwl] *//* IO port base address (physical) */
unsigned char __iomem *membase; /* read/write[bwl] *//* IO memory base address (virtual) */
unsigned int (*serial_in)(struct uart_port *, int);
void (*serial_out)(struct uart_port *, int, int);
void (*set_termios)(struct uart_port *,
struct ktermios *new,
struct ktermios *old);
void (*set_ldisc)(struct uart_port *,
struct ktermios *);
unsigned int (*get_mctrl)(struct uart_port *);
void (*set_mctrl)(struct uart_port *, unsigned int);
unsigned int (*get_divisor)(struct uart_port *,
unsigned int baud,
unsigned int *frac);
void (*set_divisor)(struct uart_port *,
unsigned int baud,
unsigned int quot,
unsigned int quot_frac);
int (*startup)(struct uart_port *port);
void (*shutdown)(struct uart_port *port);
void (*throttle)(struct uart_port *port);
void (*unthrottle)(struct uart_port *port);
int (*handle_irq)(struct uart_port *);
void (*pm)(struct uart_port *, unsigned int state,
unsigned int old);
void (*handle_break)(struct uart_port *);
int (*rs485_config)(struct uart_port *,
struct serial_rs485 *rs485);
int (*iso7816_config)(struct uart_port *,
struct serial_iso7816 *iso7816);
unsigned int irq; /* irq number */
unsigned long irqflags; /* irq flags */
unsigned int uartclk; /* base uart clock */
unsigned int fifosize; /* tx fifo size */
unsigned char x_char; /* xon/xoff char */
unsigned char regshift; /* reg offset shift */
unsigned char iotype; /* io access style */
unsigned char quirks; /* internal quirks */

#define UPIO_PORT (SERIAL_IO_PORT) /* 8b I/O port access */
#define UPIO_HUB6 (SERIAL_IO_HUB6) /* Hub6 ISA card */
#define UPIO_MEM (SERIAL_IO_MEM) /* driver-specific */
#define UPIO_MEM32 (SERIAL_IO_MEM32) /* 32b little endian */
#define UPIO_AU (SERIAL_IO_AU) /* Au1x00 and RT288x type IO */
#define UPIO_TSI (SERIAL_IO_TSI) /* Tsi108/109 type IO */
#define UPIO_MEM32BE (SERIAL_IO_MEM32BE) /* 32b big endian */
#define UPIO_MEM16 (SERIAL_IO_MEM16) /* 16b little endian */

/* quirks must be updated while holding port mutex */
#define UPQ_NO_TXEN_TEST BIT(0)

unsigned int read_status_mask; /* driver specific */
unsigned int ignore_status_mask; /* driver specific */
struct uart_state *state; /* pointer to parent state */
struct uart_icount icount; /* statistics */

struct console *cons; /* struct console, if any */
/* flags must be updated while holding port mutex */
upf_t flags;

/*
* These flags must be equivalent to the flags defined in
* include/uapi/linux/tty_flags.h which are the userspace definitions
* assigned from the serial_struct flags in uart_set_info()
* [for bit definitions in the UPF_CHANGE_MASK]
*
* Bits [0..UPF_LAST_USER] are userspace defined/visible/changeable
* The remaining bits are serial-core specific and not modifiable by
* userspace.
*/
#define UPF_FOURPORT ((__force upf_t) ASYNC_FOURPORT /* 1 */ )
#define UPF_SAK ((__force upf_t) ASYNC_SAK /* 2 */ )
#define UPF_SPD_HI ((__force upf_t) ASYNC_SPD_HI /* 4 */ )
#define UPF_SPD_VHI ((__force upf_t) ASYNC_SPD_VHI /* 5 */ )
#define UPF_SPD_CUST ((__force upf_t) ASYNC_SPD_CUST /* 0x0030 */ )
#define UPF_SPD_WARP ((__force upf_t) ASYNC_SPD_WARP /* 0x1010 */ )
#define UPF_SPD_MASK ((__force upf_t) ASYNC_SPD_MASK /* 0x1030 */ )
#define UPF_SKIP_TEST ((__force upf_t) ASYNC_SKIP_TEST /* 6 */ )
#define UPF_AUTO_IRQ ((__force upf_t) ASYNC_AUTO_IRQ /* 7 */ )
#define UPF_HARDPPS_CD ((__force upf_t) ASYNC_HARDPPS_CD /* 11 */ )
#define UPF_SPD_SHI ((__force upf_t) ASYNC_SPD_SHI /* 12 */ )
#define UPF_LOW_LATENCY ((__force upf_t) ASYNC_LOW_LATENCY /* 13 */ )
#define UPF_BUGGY_UART ((__force upf_t) ASYNC_BUGGY_UART /* 14 */ )
#define UPF_MAGIC_MULTIPLIER ((__force upf_t) ASYNC_MAGIC_MULTIPLIER /* 16 */ )

#define UPF_NO_THRE_TEST ((__force upf_t) (1 << 19))
/* Port has hardware-assisted h/w flow control */
#define UPF_AUTO_CTS ((__force upf_t) (1 << 20))
#define UPF_AUTO_RTS ((__force upf_t) (1 << 21))
#define UPF_HARD_FLOW ((__force upf_t) (UPF_AUTO_CTS | UPF_AUTO_RTS))
/* Port has hardware-assisted s/w flow control */
#define UPF_SOFT_FLOW ((__force upf_t) (1 << 22))
#define UPF_CONS_FLOW ((__force upf_t) (1 << 23))
#define UPF_SHARE_IRQ ((__force upf_t) (1 << 24))
#define UPF_EXAR_EFR ((__force upf_t) (1 << 25))
#define UPF_BUG_THRE ((__force upf_t) (1 << 26))
/* The exact UART type is known and should not be probed. */
#define UPF_FIXED_TYPE ((__force upf_t) (1 << 27))
#define UPF_BOOT_AUTOCONF ((__force upf_t) (1 << 28))
#define UPF_FIXED_PORT ((__force upf_t) (1 << 29))
#define UPF_DEAD ((__force upf_t) (1 << 30))
#define UPF_IOREMAP ((__force upf_t) (1 << 31))

#define __UPF_CHANGE_MASK 0x17fff
#define UPF_CHANGE_MASK ((__force upf_t) __UPF_CHANGE_MASK)
#define UPF_USR_MASK ((__force upf_t) (UPF_SPD_MASK|UPF_LOW_LATENCY))

#if __UPF_CHANGE_MASK > ASYNC_FLAGS
#error Change mask not equivalent to userspace-visible bit defines
#endif

/*
* Must hold termios_rwsem, port mutex and port lock to change;
* can hold any one lock to read.
*/
upstat_t status;

#define UPSTAT_CTS_ENABLE ((__force upstat_t) (1 << 0))
#define UPSTAT_DCD_ENABLE ((__force upstat_t) (1 << 1))
#define UPSTAT_AUTORTS ((__force upstat_t) (1 << 2))
#define UPSTAT_AUTOCTS ((__force upstat_t) (1 << 3))
#define UPSTAT_AUTOXOFF ((__force upstat_t) (1 << 4))
#define UPSTAT_SYNC_FIFO ((__force upstat_t) (1 << 5))

int hw_stopped; /* sw-assisted CTS flow state */
unsigned int mctrl; /* current modem ctrl settings */
unsigned int timeout; /* character-based timeout */
unsigned int type; /* port type */
const struct uart_ops *ops;
unsigned int custom_divisor;
unsigned int line; /* port index */
unsigned int minor;
resource_size_t mapbase; /* for ioremap */
resource_size_t mapsize;
struct device *dev; /* parent device */

unsigned long sysrq; /* sysrq timeout */
unsigned int sysrq_ch; /* char for sysrq */
unsigned char has_sysrq;
unsigned char sysrq_seq; /* index in sysrq_toggle_seq */

unsigned char hub6; /* this should be in the 8250 driver */
unsigned char suspended;
unsigned char console_reinit;
const char *name; /* port name */
struct attribute_group *attr_group; /* port specific attributes */
const struct attribute_group **tty_groups; /* all attributes (serial core use only) */
struct serial_rs485 rs485;
struct gpio_desc *rs485_term_gpio; /* enable RS485 bus termination */
struct serial_iso7816 iso7816;
void *private_data; /* generic platform data pointer */
};

struct uart_state

struct uart_stateis a structure, defined ininclude/linux/serial_core.h, which internally contains atty_portmember variable of type, typically used to represent the status information of the UART driver. Through theuart_driverstate member pointer in the structure, data related to the UART device status can be accessed and manipulated.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/*
* This is the state information which is persistent across opens.
*/
struct uart_state {
struct tty_port port;

enum uart_pm_state pm_state;
struct circ_buf xmit;

atomic_t refcount;
wait_queue_head_t remove_wait;
struct uart_port *uart_port;
};

struct uart_ops

The structureuart_opscontains a series of function pointers, which define the interface for operating the UART port. Each function pointer corresponds to a specific operation, such as sending data, setting control signals, starting or stopping transmission, etc.

Through theuart_opsstructure, upper-layer applications or drivers can call these function pointers to operate the UART port, achieving data transmission and control operations.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
/*
* This structure describes all the operations that can be done on the
* physical hardware. See Documentation/driver-api/serial/driver.rst for details.
*/
struct uart_ops {
unsigned int (*tx_empty)(struct uart_port *);// Function pointer to check if the transmit buffer is empty
void (*set_mctrl)(struct uart_port *, unsigned int mctrl);// Function pointer to set modem control signals
unsigned int (*get_mctrl)(struct uart_port *);// Function pointer for obtaining modem control signals
void (*stop_tx)(struct uart_port *);// Function pointer for stopping transmission
void (*start_tx)(struct uart_port *);// Function pointer for starting transmission
void (*throttle)(struct uart_port *);// Function pointer for flow control
void (*unthrottle)(struct uart_port *);// Function pointer for releasing flow control
void (*send_xchar)(struct uart_port *, char ch);// Function pointer for sending special characters
void (*stop_rx)(struct uart_port *);// Function pointer for stopping reception
void (*enable_ms)(struct uart_port *);// Function pointer for enabling RTS/CTS (hardware flow control)
void (*break_ctl)(struct uart_port *, int ctl);// Function pointer for controlling BREAK signal transmission
int (*startup)(struct uart_port *);// Function pointer for starting
void (*shutdown)(struct uart_port *);// Function pointer for closing
void (*flush_buffer)(struct uart_port *);// Function pointer for flushing buffer
void (*set_termios)(struct uart_port *, struct ktermios *new,
struct ktermios *old);// Function pointer for setting terminal information
void (*set_ldisc)(struct uart_port *, struct ktermios *);// Function pointer for setting line discipline
void (*pm)(struct uart_port *, unsigned int state,
unsigned int oldstate);// Function pointer for power management

/*
* Return a string describing the type of the port
*/
const char *(*type)(struct uart_port *);// Function pointer to get port type

/*
* Release IO and memory resources used by the port.
* This includes iounmap if necessary.
*/
void (*release_port)(struct uart_port *);// Function pointer to release port

/*
* Request IO and memory resources used by the port.
* This includes iomapping the port if necessary.
*/
int (*request_port)(struct uart_port *);// Function pointer to request port
void (*config_port)(struct uart_port *, int);// Function pointer to configure port
int (*verify_port)(struct uart_port *, struct serial_struct *);// Function pointer to verify port
int (*ioctl)(struct uart_port *, unsigned int, unsigned long);// Function pointer for control operation
#ifdef CONFIG_CONSOLE_POLL
int (*poll_init)(struct uart_port *);// Function pointer for polling initialization
void (*poll_put_char)(struct uart_port *, unsigned char);// Function pointer for polling character transmission
int (*poll_get_char)(struct uart_port *);// Function pointer for polling character reception
#endif
};

Analysis of uart_driver registration

drivers/tty/serial/8250/8250_core.cThe functions in this file are mainly related to the core functionality of the 8250 series UART driver. This file implements the core operations of the 8250 serial communication device, including initialization, configuration, interrupt handling, data transmission, and other functions.

serial8250_init()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
static int __init serial8250_init(void)
{
int ret;

if (nr_uarts == 0)// Check if any UART port is defined; if not, return -ENODEV error
return -ENODEV;

// Initialize 8250/16550 serial port
serial8250_isa_init_ports();

// Print serial driver information, including port count and IRQ sharing status
pr_info("Serial: 8250/16550 driver, %d ports, IRQ sharing %sabled\n",
nr_uarts, share_irqs ? "en" : "dis");

#ifdef CONFIG_SPARC
// If it is SPARC architecture, register the minor device number of the serial device
ret = sunserial_register_minors(&serial8250_reg, UART_NR);
#else
// Otherwise, register the UART driver on the current platform
serial8250_reg.nr = UART_NR;
ret = uart_register_driver(&serial8250_reg);
#endif
if (ret)
goto out;

ret = serial8250_pnp_init();// Initialize the PNP device (if present)
if (ret)
goto unreg_uart_drv;

// Allocate the ISA platform device structure and register it
serial8250_isa_devs = platform_device_alloc("serial8250",
PLAT8250_DEV_LEGACY);
if (!serial8250_isa_devs) {
ret = -ENOMEM;
goto unreg_pnp;
}

// Add the platform device to the system
ret = platform_device_add(serial8250_isa_devs);
if (ret)
goto put_dev;
// Register the serial port
serial8250_register_ports(&serial8250_reg, &serial8250_isa_devs->dev);
// Register the platform driver
ret = platform_driver_register(&serial8250_isa_driver);
if (ret == 0)
goto out;
// If registration fails, remove the added ISA platform device
platform_device_del(serial8250_isa_devs);
put_dev:
platform_device_put(serial8250_isa_devs);
unreg_pnp:
serial8250_pnp_exit();// Release the PNP device on exit
unreg_uart_drv:
#ifdef CONFIG_SPARC
sunserial_unregister_minors(&serial8250_reg, UART_NR);// If it is a SPARC architecture, unregister the minor device number of the serial port
#else
uart_unregister_driver(&serial8250_reg);// Otherwise, unregister the UART driver on the current platform
#endif
out:
return ret;// Return the initialization result
}

Initialize the 8250 serial port

serial8250_isa_init_ports()

serial8250_init(void)The function is mainly responsible for initializing and registering the 8250/16550 serial device driver at system startup, where the function on line 10serial8250_isa_init_ports()initializes the 8250 serial port, and the function is as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
static void __init serial8250_isa_init_ports(void)
{
struct uart_8250_port *up;// UART 8250 port structure pointer
static int first = 1;// Static variable to mark whether it is the first initialization
int i, irqflag = 0;// Loop variable and IRQ flag are initialized to 0

if (!first)// If it is not the first initialization, return directly to avoid repeated initialization
return;
first = 0;// Mark as not the first initialization

// If the number of UART ports defined by the system is greater than the maximum supported by hardware, limit it to the maximum supported number
if (nr_uarts > UART_NR)
nr_uarts = UART_NR;

// Traverse all defined UART ports and initialize each port
for (i = 0; i < nr_uarts; i++) {
struct uart_8250_port *up = &serial8250_ports[i];// Get the pointer to the i-th UART port structure
struct uart_port *port = &up->port;// Get the generic UART port structure of the port

port->line = i;// Set the logical line number of the UART port
serial8250_init_port(up);// Initialize the UART port

// If base_ops is not yet set, use the current port's operation functions as the base operation functions
if (!base_ops)
base_ops = port->ops;
port->ops = &univ8250_port_ops;// Set the UART port's operation functions to univ8250_port_ops

// Initialize the timer
timer_setup(&up->timer, serial8250_timeout, 0);

// Set the UART port's driver operation functions to univ8250_driver_ops
up->ops = &univ8250_driver_ops;

/*
* ALPHA_KLUDGE_MCR needs to be killed.
*/
up->mcr_mask = ~ALPHA_KLUDGE_MCR;// Set the MCR mask bit of the UART port to mask ALPHA_KLUDGE_MCR
up->mcr_force = ALPHA_KLUDGE_MCR;// Set the MCR force bit of the UART port to ALPHA_KLUDGE_MCR
serial8250_set_defaults(up);// Set default parameters for the UART port
}

/* chain base port ops to support Remote Supervisor Adapter */
univ8250_port_ops = *base_ops;// Chain basic port operations to support Remote Supervisor Adapter (RSA)
univ8250_rsa_support(&univ8250_port_ops);

// If the shared IRQ flag is set, set the IRQ flag bit
if (share_irqs)
irqflag = IRQF_SHARED;

// Iterate through the old serial port array and initialize the corresponding UART ports
for (i = 0, up = serial8250_ports;
i < ARRAY_SIZE(old_serial_port) && i < nr_uarts;
i++, up++) {
struct uart_port *port = &up->port;
// Set parameters for the UART port such as IO base address, IRQ, IRQ flags, clock frequency, flags, hub6, etc.
port->iobase = old_serial_port[i].port;
port->irq = irq_canonicalize(old_serial_port[i].irq);// Normalize IRQ
port->irqflags = 0;
port->uartclk = old_serial_port[i].baud_base * 16;// Set UART clock frequency
port->flags = old_serial_port[i].flags;// Set UART port flags
port->hub6 = 0;
port->membase = old_serial_port[i].iomem_base;
port->iotype = old_serial_port[i].io_type;
port->regshift = old_serial_port[i].iomem_reg_shift;

port->irqflags |= irqflag;// Set IRQ flag bit
// If an ISA configuration function is defined, call the configuration function for additional configuration
if (serial8250_isa_config != NULL)
serial8250_isa_config(i, &up->port, &up->capabilities);
}
}
serial8250_init_port()

In the code above, line 21 executedserial8250_init_portfunction:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/*
* Initialize UART 8250 port function。
*/
void serial8250_init_port(struct uart_8250_port *up)
{
struct uart_port *port = &up->port;// Get UART 8250 port structure

spin_lock_init(&port->lock);// Initialize port lock
port->pm = NULL;
port->ops = &serial8250_pops;// Set port operation function to serial8250_pops
port->has_sysrq = IS_ENABLED(CONFIG_SERIAL_8250_CONSOLE);

up->cur_iotype = 0xFF;// Set current port IO type to 0xFF
}
EXPORT_SYMBOL_GPL(serial8250_init_port);

serial8250_popsAs follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
static const struct uart_ops serial8250_pops = {
.tx_empty = serial8250_tx_empty,// Whether the transmission buffer is empty
.set_mctrl = serial8250_set_mctrl, // Set control signals
.get_mctrl = serial8250_get_mctrl, // Get control signals
.stop_tx = serial8250_stop_tx,// Stop sending
.start_tx = serial8250_start_tx,// Start sending
.throttle = serial8250_throttle,// Throttle
.unthrottle = serial8250_unthrottle,// Unthrottle
.stop_rx = serial8250_stop_rx,// Stop receiving
.enable_ms = serial8250_enable_ms, // Enable RTS/CTS
.break_ctl = serial8250_break_ctl,// Control sending BREAK signal
.startup = serial8250_startup,// Start
.shutdown = serial8250_shutdown, // Close
.flush_buffer = serial8250_flush_buffer,
.set_termios = serial8250_set_termios,
.set_ldisc = serial8250_set_ldisc,// Set terminal parameters
.pm = serial8250_pm, // Power management
.type = serial8250_type, // Return port type string
.release_port = serial8250_release_port,// Release port resources
.request_port = serial8250_request_port,// Request port resources
.config_port = serial8250_config_port,// Configure port
.verify_port = serial8250_verify_port,// Verify port
#ifdef CONFIG_CONSOLE_POLL
.poll_get_char = serial8250_get_poll_char,// Get character (for polling)
.poll_put_char = serial8250_put_poll_char,// Send character (for polling)
#endif
};

Register UART driver

serial8250_init(void)The function is mainly responsible for initializing and registering the 8250/16550 serial port device driver during system startup, where theuart_register_driver(&serial8250_reg), through theuart_register_driverfunction registers this uart_driver with the system, the function prototype is as follows

1
int uart_register_driver(struct uart_driver *uart)

The meanings of the function parameters and return value are as follows:

  • uart: The uart_driver to be registered.
  • Return value: 0 for success; negative value for failure.

When unregistering the driver, it is also necessary to unregister the previously registereduart_driver, which requires the use ofuart_unregister_driverThe function prototype is as follows:

1
void uart_unregister_driver(struct uart_driver *uart)

The meanings of the function parameters and return value are as follows:

  • uart: The uart_driver to be unregistered.
  • Return value: None

serial8250_init(void)In the function,uart_register_driverthe parameter passed to the function isserial8250_reg

1
2
3
4
5
6
7
8
static struct uart_driver serial8250_reg = {
.owner = THIS_MODULE,// the owner of the module
.driver_name = "serial",// the driver name
.dev_name = "ttyS",// Device Name
.major = TTY_MAJOR,// Major Device Number
.minor = 64,// Minor Device Number
.cons = SERIAL8250_CONSOLE,// Console
};
uart_register_driver()

uart_register_driverThe function content is as follows, defined in the source codedrivers/tty/serial/serial_core.c.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
/**
* uart_register_driver - register a driver with the uart core layer
* @drv: low level driver structure
*
* Register a uart driver with the core driver. We in turn register
* with the tty layer, and initialise the core driver per-port state.
*
* We have a proc file in /proc/tty/driver which is named after the
* normal driver.
*
* drv->port should be NULL, and the per-port structures should be
* registered using uart_add_one_port after this call has succeeded.
*/
int uart_register_driver(struct uart_driver *drv)
{
struct tty_driver *normal;
int i, retval = -ENOMEM;

BUG_ON(drv->state);// Check if the driver status is already occupied

/*
* Maybe we should be using a slab cache for this, especially if
* we have a large number of ports to handle.
*/
drv->state = kcalloc(drv->nr, sizeof(struct uart_state), GFP_KERNEL);// Allocate memory space for UART status
if (!drv->state)
goto out;

// Allocate tty driver
normal = alloc_tty_driver(drv->nr);
if (!normal)
goto out_kfree;

drv->tty_driver = normal;

// Set tty driver attributes
normal->driver_name = drv->driver_name;
normal->name = drv->dev_name;
normal->major = drv->major;
normal->minor_start = drv->minor;
normal->type = TTY_DRIVER_TYPE_SERIAL;
normal->subtype = SERIAL_TYPE_NORMAL;
normal->init_termios = tty_std_termios;
normal->init_termios.c_cflag = B9600 | CS8 | CREAD | HUPCL | CLOCAL;
normal->init_termios.c_ispeed = normal->init_termios.c_ospeed = 9600;
normal->flags = TTY_DRIVER_REAL_RAW | TTY_DRIVER_DYNAMIC_DEV;
normal->driver_state = drv;
tty_set_operations(normal, &uart_ops);

/*
* Initialise the UART state(s).
*/
for (i = 0; i < drv->nr; i++) {// Traverse each UART status and initialize the corresponding tty port
struct uart_state *state = drv->state + i;
struct tty_port *port = &state->port;

tty_port_init(port);
port->ops = &uart_port_ops;
}

retval = tty_register_driver(normal);// Register tty driver
if (retval >= 0)
return retval;

// When registration fails, destroy the initialized tty ports and free memory
for (i = 0; i < drv->nr; i++)
tty_port_destroy(&drv->state[i].port);
put_tty_driver(normal);
out_kfree:
kfree(drv->state);
out:
return retval;
}

uart_register_driverLine 48 of the function sets the tty driver’s operation functions, which define operations related to specific devices, such as read, write, control, etc.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
static const struct tty_operations uart_ops = {
.install = uart_install,// Install tty device
.open = uart_open,// Open tty device
.close = uart_close,// Close tty device
.write = uart_write,// Write data to tty device
.put_char = uart_put_char,// Write character to tty device output buffer
.flush_chars = uart_flush_chars,// Flush tty device output buffer
.write_room = uart_write_room,// Get remaining space size of tty device output buffer
.chars_in_buffer= uart_chars_in_buffer, // Get number of characters in tty device input buffer
.flush_buffer = uart_flush_buffer,// Flush tty device input buffer
.ioctl = uart_ioctl,// Control tty device operation
.throttle = uart_throttle,// Control tty device flow control state
.unthrottle = uart_unthrottle,// Control tty device flow control state
.send_xchar = uart_send_xchar,// Send special character to tty device
.set_termios = uart_set_termios,// Set terminal parameters of tty device
.set_ldisc = uart_set_ldisc,// Set line discipline of tty device
.stop = uart_stop,// Stop tty device
.start = uart_start,// Start the tty device
.hangup = uart_hangup,// Close the connection of the tty device
.break_ctl = uart_break_ctl,// Control the transmission interrupt of the tty device
.wait_until_sent= uart_wait_until_sent,// Wait for the tty device to finish sending all data
#ifdef CONFIG_PROC_FS
.proc_show = uart_proc_show,// Display UART-related proc file system information
#endif
.tiocmget = uart_tiocmget,// Get the modem status of the tty device
.tiocmset = uart_tiocmset,// Set the modem status of the tty device
.set_serial = uart_set_info_user,
.get_serial = uart_get_info_user,
.get_icount = uart_get_icount,// Get the count information of the tty device
#ifdef CONFIG_CONSOLE_POLL
.poll_init = uart_poll_init,// Initialize the polling mode of the tty device
.poll_get_char = uart_poll_get_char,// Get a character from the tty device (polling mode)
.poll_put_char = uart_poll_put_char,// Write a character to the tty device (polling mode)
#endif
};

uart_register_driverCode call at line 61 in the functiontty_register_driverThe function is as shown below, defined indrivers/tty/tty_io.c

tty_register_dirver()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
/*
* Called by a tty driver to register itself.
*/
/*
* When tty Called when the driver invokes this function to register itself。
*/
int tty_register_driver(struct tty_driver *driver)
{
int error;
int i;
dev_t dev;
struct device *d;

if (!driver->major) {// If the driver does not specify a major device number, allocate one
error = alloc_chrdev_region(&dev, driver->minor_start,
driver->num, driver->name);
if (!error) {
driver->major = MAJOR(dev);
driver->minor_start = MINOR(dev);
}
} else {
dev = MKDEV(driver->major, driver->minor_start);
error = register_chrdev_region(dev, driver->num, driver->name);
}
if (error < 0)
goto err;
// If the driver flag is TTY_DRIVER_DYNAMIC_ALLOC, then dynamically add the tty character device
if (driver->flags & TTY_DRIVER_DYNAMIC_ALLOC) {
error = tty_cdev_add(driver, dev, 0, driver->num);
if (error)
goto err_unreg_char;
}

mutex_lock(&tty_mutex);
// Add the driver to the tty driver list
list_add(&driver->tty_drivers, &tty_drivers);
mutex_unlock(&tty_mutex);

// If the driver flag is not set to TTY_DRIVER_DYNAMIC_DEV, then register the tty device
if (!(driver->flags & TTY_DRIVER_DYNAMIC_DEV)) {
for (i = 0; i < driver->num; i++) {
d = tty_register_device(driver, i, NULL);
if (IS_ERR(d)) {
error = PTR_ERR(d);
goto err_unreg_devs;
}
}
}
// Register the TTY driver to the proc file system
proc_tty_register_driver(driver);
driver->flags |= TTY_DRIVER_INSTALLED;
return 0;

err_unreg_devs:
// Unregister the registered tty device
for (i--; i >= 0; i--)
tty_unregister_device(driver, i);

mutex_lock(&tty_mutex);
// Remove the driver from the tty driver list
list_del(&driver->tty_drivers);
mutex_unlock(&tty_mutex);

err_unreg_char:
unregister_chrdev_region(dev, driver->num);// Unregister the registered character device
err:
return error;
}
EXPORT_SYMBOL(tty_register_driver);

Allocate a platform device structure and register it

serial8250_init(void)The function is mainly responsible for initializing and registering the 8250/16550 serial port device driver during system startup:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
...
// Allocate an ISA platform device structure and register it
serial8250_isa_devs = platform_device_alloc("serial8250",
PLAT8250_DEV_LEGACY);
if (!serial8250_isa_devs) {
ret = -ENOMEM;
goto unreg_pnp;
}
// Add the platform device to the system
ret = platform_device_add(serial8250_isa_devs);
if (ret)
goto put_dev;
...

}

platform_device_allocThe function is used forAllocate a platform device structure without registering it on the platform bus. The returned structure can later be registered on the platform bus using the functionplatform_device_add()to register it on the platform bus.

Register serial port

serial8250_init(void)The function is mainly responsible for initializing and registering the 8250/16550 serial device driver during system startup. Line 42 of the code is as follows:

1
2
// Register serial port
serial8250_register_ports(&serial8250_reg, &serial8250_isa_devs->dev);
serial8250_register_ports()

serial8250_register_portsThe function content is as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
static void __init
serial8250_register_ports(struct uart_driver *drv, struct device *dev)
{
int i;

for (i = 0; i < nr_uarts; i++) {// Traverse all serial ports
struct uart_8250_port *up = &serial8250_ports[i];

if (up->port.type == PORT_8250_CIR)// If it is a CIR port, skip it
continue;

if (up->port.dev)// If the port already has a device assigned, skip it
continue;

up->port.dev = dev;// Associate the device pointer with the port

if (uart_console_enabled(&up->port))
pm_runtime_get_sync(up->port.dev);

serial8250_apply_quirks(up);// Apply special handling for serial port 8250
uart_add_one_port(drv, &up->port);// Register a serial port with the UART driver
}
}

Register the platform_driver

serial8250_init(void)The function is mainly responsible for initializing and registering the 8250/16550 serial device driver during system startup. Line 44 of the code is as follows

1
2
// Register platform driver
ret = platform_driver_register(&serial8250_isa_driver);

Port registration process analysis

In the SDK source device tree provided by Xunwei, UART 9 is enabled by default. Openarch/arm64/boot/dts/rockchip/rk3568.dtsithe device tree file. The device tree node for the UART 9 controller is as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
uart9: serial@fe6d0000 {
compatible = "rockchip,rk3568-uart", "snps,dw-apb-uart";
reg = <0x0 0xfe6d0000 0x0 0x100>;
interrupts = <GIC_SPI 125 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&cru SCLK_UART9>, <&cru PCLK_UART9>;
clock-names = "baudclk", "apb_pclk";
reg-shift = <2>;
reg-io-width = <4>;
dmas = <&dmac0 18>, <&dmac0 19>;
pinctrl-names = "default";
pinctrl-0 = <&uart9m0_xfer>;
status = "disabled";
}

The compatible attribute value on line 2 issnps,dw-apb-uart. Searching for this value in the Linux source code will locate the corresponding UART driver file, which isdrivers/tty/serial/8250/8250_dw.c

1
2
3
4
5
6
7
8
9
10
11
12
static struct platform_driver dw8250_platform_driver = {
.driver = {
.name = "dw-apb-uart",
.pm = &dw8250_pm_ops,
.of_match_table = dw8250_of_match,
.acpi_match_table = dw8250_acpi_match,
},
.probe = dw8250_probe,
.remove = dw8250_remove,
};

module_platform_driver(dw8250_platform_driver);

It can be seen that Rockchip’s UART is essentially a platform driver. After the node matches successfully, thedw_probefunction is executed, and its content is as follows:

dw_probe()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
static int dw8250_probe(struct platform_device *pdev)
{
struct uart_8250_port uart = {}, *up = &uart;// Initialize a uart_8250_port structure
struct resource *regs = platform_get_resource(pdev, IORESOURCE_MEM, 0);// Get device resource information
struct uart_port *p = &up->port;// Get uart_port pointer
struct device *dev = &pdev->dev;// Get device pointer
struct dw8250_data *data; // Define dw8250_data structure pointer
int irq;
int err;
u32 val;

// Check if device resource information was obtained
if (!regs) {
dev_err(dev, "no registers defined\n");
return -EINVAL;
}

irq = platform_get_irq(pdev, 0);
if (irq < 0)// Check if the interrupt number has been obtained
return irq;

spin_lock_init(&p->lock); // Initialize the lock
p->mapbase = regs->start;// Set the physical address of the device
p->irq = irq;// Set the interrupt number of the device
p->handle_irq = dw8250_handle_irq;// Set the interrupt handler function
p->pm = dw8250_do_pm; // Set the power management function of the device
p->type = PORT_8250; // Set the device type
p->flags = UPF_SHARE_IRQ | UPF_FIXED_PORT;// Set the device flags
p->dev = dev;// Set the device pointer
p->iotype = UPIO_MEM; // Set the IO type
p->serial_in = dw8250_serial_in;// Set the read function
p->serial_out = dw8250_serial_out;// Set the write function
p->set_ldisc = dw8250_set_ldisc; // Set the line discipline function
p->set_termios = dw8250_set_termios;// Set the terminal parameter function

// Map the physical address of the device to memory space
p->membase = devm_ioremap(dev, regs->start, resource_size(regs));
if (!p->membase)
return -ENOMEM;

// Allocate memory space for the dw8250_data structure
data = devm_kzalloc(dev, sizeof(*data), GFP_KERNEL);
if (!data)
return -ENOMEM;

data->data.dma.fn = dw8250_fallback_dma_filter;// Set DMA function pointer
data->usr_reg = DW_UART_USR;// Set UART status register address
p->private_data = &data->data;// Set device private data pointer

// Read device property "snps,uart-16550-compatible" to determine if it is compatible with 16550
data->uart_16550_compatible = device_property_read_bool(dev,
"snps,uart-16550-compatible");

// Read device property "reg-shift" to get the address offset value
err = device_property_read_u32(dev, "reg-shift", &val);
if (!err)
p->regshift = val;

// Read device property "reg-io-width" to get the IO width
err = device_property_read_u32(dev, "reg-io-width", &val);
if (!err && val == 4) {
p->iotype = UPIO_MEM32;// Set IO type to 32-bit
p->serial_in = dw8250_serial_in32;// Set read function to 32-bit
p->serial_out = dw8250_serial_out32;// Set write function to 32-bit
}

// If the property "dcd-override" exists, always set the DCD state to active
if (device_property_read_bool(dev, "dcd-override")) {
/* Always report DCD as active */
data->msr_mask_on |= UART_MSR_DCD;
data->msr_mask_off |= UART_MSR_DDCD;
}

// If the property "dsr-override" exists, always set the DSR state to active
if (device_property_read_bool(dev, "dsr-override")) {
/* Always report DSR as active */
data->msr_mask_on |= UART_MSR_DSR;
data->msr_mask_off |= UART_MSR_DDSR;
}

// If the property "cts-override" exists, always set the CTS state to active
if (device_property_read_bool(dev, "cts-override")) {
/* Always report CTS as active */
data->msr_mask_on |= UART_MSR_CTS;
data->msr_mask_off |= UART_MSR_DCTS;
}

// If the property "ri-override" exists, always set the RI state to inactive
if (device_property_read_bool(dev, "ri-override")) {
/* Always report Ring indicator as inactive */
data->msr_mask_off |= UART_MSR_RI;
data->msr_mask_off |= UART_MSR_TERI;
}

/* Always ask for fixed clock rate from a property. */
// Read property "clock-frequency" to get the clock frequency
device_property_read_u32(dev, "clock-frequency", &p->uartclk);

/* If there is separate baudclk, get the rate from it. */
data->clk = devm_clk_get_optional(dev, "baudclk");// If the "baudclk" clock exists, get the clock frequency from it
if (data->clk == NULL)// Fail if the clock frequency is not defined
data->clk = devm_clk_get_optional(dev, NULL);
if (IS_ERR(data->clk))
return PTR_ERR(data->clk);

INIT_WORK(&data->clk_work, dw8250_clk_work_cb);
data->clk_notifier.notifier_call = dw8250_clk_notifier_cb;

err = clk_prepare_enable(data->clk);
if (err)
dev_warn(dev, "could not enable optional baudclk: %d\n", err);

if (data->clk)
p->uartclk = clk_get_rate(data->clk);

/* If no clock rate is defined, fail. */
if (!p->uartclk) {
dev_err(dev, "clock rate not defined\n");
err = -EINVAL;
goto err_clk;
}

data->pclk = devm_clk_get_optional(dev, "apb_pclk");
if (IS_ERR(data->pclk)) {
err = PTR_ERR(data->pclk);
goto err_clk;
}

err = clk_prepare_enable(data->pclk);
if (err) {
dev_err(dev, "could not enable apb_pclk\n");
goto err_clk;
}

data->rst = devm_reset_control_get_optional_exclusive(dev, NULL);
if (IS_ERR(data->rst)) {
err = PTR_ERR(data->rst);
goto err_pclk;
}
reset_control_deassert(data->rst);
// Apply specific quirks
dw8250_quirks(p, data);

/* If the Busy Functionality is not implemented, don't handle it */
if (data->uart_16550_compatible)// Do not handle the busy flag if the device is not 16550-compatible
p->handle_irq = NULL;

if (!data->skip_autocfg)// Perform port configuration if auto-configuration is not skipped
dw8250_setup_port(p);

/* If we have a valid fifosize, try hooking up DMA */
if (p->fifosize) {// If there is a valid FIFO size, attempt to connect DMA
data->data.dma.rxconf.src_maxburst = p->fifosize / 4;
data->data.dma.txconf.dst_maxburst = p->fifosize / 4;
up->dma = &data->data.dma;
}

// Register the 8250 port
data->data.line = serial8250_register_8250_port(up);
if (data->data.line < 0) {
err = data->data.line;
goto err_reset;
}

/*
* Some platforms may provide a reference clock shared between several
* devices. In this case any clock state change must be known to the
* UART port at least post factum.
*/
if (data->clk) {
err = clk_notifier_register(data->clk, &data->clk_notifier);
if (err)
dev_warn(p->dev, "Failed to set the clock notifier\n");
else
queue_work(system_unbound_wq, &data->clk_work);
}

// Set the private data of the platform device
platform_set_drvdata(pdev, data);

// Set power management to active state
pm_runtime_set_active(dev);
pm_runtime_enable(dev);

return 0;

err_reset:
reset_control_assert(data->rst);

err_pclk:
clk_disable_unprepare(data->pclk);

err_clk:
clk_disable_unprepare(data->clk);

return err;
}

Among them, line 159 of code usesserial8250_register_8250_portfunction to register the 8250 port,serial8250_register_8250_portThe function content is as follows:

serial8250_register_8250_port()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
/**
* serial8250_register_8250_port - register a serial port
* @up: serial port template
*
* Configure the serial port specified by the request. If the
* port exists and is in use, it is hung up and unregistered
* first.
*
* The port is then probed and if necessary the IRQ is autodetected
* If this fails an error is returned.
*
* On success the port is ready to use and the line number is returned.
*/
int serial8250_register_8250_port(struct uart_8250_port *up)
{
struct uart_8250_port *uart;
int ret = -ENOSPC;

if (up->port.uartclk == 0)
return -EINVAL;

mutex_lock(&serial_mutex);

uart = serial8250_find_match_or_unused(&up->port);
if (uart && uart->port.type != PORT_8250_CIR) {
....
if (uart->port.type != PORT_8250_CIR) {
if (serial8250_isa_config != NULL)
serial8250_isa_config(0, &uart->port,
&uart->capabilities);

serial8250_apply_quirks(uart);
ret = uart_add_one_port(&serial8250_reg,
&uart->port);
if (ret)
goto err;

ret = uart->port.line;
} else {
dev_info(uart->port.dev,
"skipping CIR port at 0x%lx / 0x%llx, IRQ %d\n",
uart->port.iobase,
(unsigned long long)uart->port.mapbase,
uart->port.irq);

ret = 0;
}

/* Initialise interrupt backoff work if required */
if (up->overrun_backoff_time_ms > 0) {
uart->overrun_backoff_time_ms =
up->overrun_backoff_time_ms;
INIT_DELAYED_WORK(&uart->overrun_backoff,
serial_8250_overrun_backoff_work);
} else {
uart->overrun_backoff_time_ms = 0;
}
}

mutex_unlock(&serial_mutex);

return ret;

err:
uart->port.dev = NULL;
mutex_unlock(&serial_mutex);
return ret;
}
EXPORT_SYMBOL(serial8250_register_8250_port);

Among them,serial8250_find_match_or_unused The function content is as follows

serial8250_find_match_or_unused()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
static struct uart_8250_port *serial8250_find_match_or_unused(struct uart_port *port)
{
int i;

/*
* First, find a port entry which matches.
*/
/*
* First,Find a matching port entry。
*/
for (i = 0; i < nr_uarts; i++)
if (uart_match_port(&serial8250_ports[i].port, port))
return &serial8250_ports[i];

/* try line number first if still available */
i = port->line;/* If there is an available port number, try to use it */
if (i < nr_uarts && serial8250_ports[i].port.type == PORT_UNKNOWN &&
serial8250_ports[i].port.iobase == 0)
return &serial8250_ports[i];
/*
* We didn't find a matching entry, so look for the first
* free entry. We look for one which hasn't been previously
* used (indicated by zero iobase).
*/
for (i = 0; i < nr_uarts; i++)
if (serial8250_ports[i].port.type == PORT_UNKNOWN &&
serial8250_ports[i].port.iobase == 0)
return &serial8250_ports[i];

/*
* That also failed. Last resort is to find any entry which
* doesn't have a real port associated with it.
*/
/*
* If still not found,The final attempt is to find an entry not associated with an actual port。
*/
for (i = 0; i < nr_uarts; i++)
if (serial8250_ports[i].port.type == PORT_UNKNOWN)
return &serial8250_ports[i];

return NULL;
}

serial8250_register_8250_port()Line 33uart_add_one_port()The function registers a UART port with the tty core layer,uart_add_one_port()The function content is as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/**
* uart_add_one_port - attach a driver-defined port structure
* @drv: pointer to the uart low level driver structure for this port
* @uport: uart port structure to use for this port.
*
* This allows the driver to register its own uart_port structure
* with the core driver. The main purpose is to allow the low
* level uart drivers to expand uart_port, rather than having yet
* more levels of structures.
*/
int uart_add_one_port(struct uart_driver *drv, struct uart_port *uport)
{
struct uart_state *state;
struct tty_port *port;
int ret = 0;
struct device *tty_dev;
int num_groups;

...

/*
* Register the port whether it's detected or not. This allows
* setserial to be used to alter this port's parameters.
*/
tty_dev = tty_port_register_device_attr_serdev(port, drv->tty_driver,
uport->line, uport->dev, port, uport->tty_groups);

...

return ret;
}

tty_port_register_device_attr_serdev The function content is as follows:

tty_port_register_device_attr_serdev()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/**
* tty_port_register_device_attr_serdev - register tty or serdev device
* @port: tty_port of the device
* @driver: tty_driver for this device
* @index: index of the tty
* @device: parent if exists, otherwise NULL
* @drvdata: driver data for the device
* @attr_grp: attribute group for the device
*
* Register a serdev or tty device depending on if the parent device has any
* defined serdev clients or not.
*/
/*
* Register a tty device to tty core layer,If the registered device is serdev device,then do not create cdev。
*/
struct device *tty_port_register_device_attr_serdev(struct tty_port *port,
struct tty_driver *driver, unsigned index,
struct device *device, void *drvdata,
const struct attribute_group **attr_grp)
{
struct device *dev;

tty_port_link_device(port, driver, index);// Link the tty port to the device
// Register serdev device
dev = serdev_tty_port_register(port, device, driver, index);
if (PTR_ERR(dev) != -ENODEV) {
/* Skip creating cdev if we registered a serdev device */
return dev;/* If a serdev device is registered, cdev is not created */
}
// If the registered device is not a serdev device, cdev is created
return tty_register_device_attr(driver, index, device, drvdata,
attr_grp);
}
EXPORT_SYMBOL_GPL(tty_port_register_device_attr_serdev);

This function is used to register a tty device to the tty core layer. If the registered device is a serdev device, cdev is not created.

First, it links the tty port to the device, then attempts to register the serdev device. If a serdev device is registered, it directly returns the registration result; otherwise, by calling thetty_register_device_attr()function, it creates cdev and registers the device to the tty core layer.tty_register_device_attrThe function content is as follows:

tty_register_device_attr()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
/**
* tty_register_device_attr - register a tty device
* @driver: the tty driver that describes the tty device
* @index: the index in the tty driver for this tty device
* @device: a struct device that is associated with this tty device.
* This field is optional, if there is no known struct device
* for this tty device it can be set to NULL safely.
* @drvdata: Driver data to be set to device.
* @attr_grp: Attribute group to be set on device.
*
* Returns a pointer to the struct device for this tty device
* (or ERR_PTR(-EFOO) on error).
*
* This call is required to be made to register an individual tty device
* if the tty driver's flags have the TTY_DRIVER_DYNAMIC_DEV bit set. If
* that bit is not set, this function should not be called by a tty
* driver.
*
* Locking: ??
*/
/*
* Register a tty device to tty core layer,including creating cdev,and adding attribute groups。
*/
struct device *tty_register_device_attr(struct tty_driver *driver,
unsigned index, struct device *device,
void *drvdata,
const struct attribute_group **attr_grp)
{
char name[64];// device name buffer
dev_t devt = MKDEV(driver->major, driver->minor_start) + index;// calculate device number
struct ktermios *tp;
struct device *dev;
int retval;

if (index >= driver->num) {// check if index is out of range
pr_err("%s: Attempt to register invalid tty line number (%d)\n",
driver->name, index);
return ERR_PTR(-EINVAL);
}

if (driver->type == TTY_DRIVER_TYPE_PTY)// Generate device name based on driver type
pty_line_name(driver, index, name);
else
tty_line_name(driver, index, name);

dev = kzalloc(sizeof(*dev), GFP_KERNEL);// Allocate memory space for device structure
if (!dev)
return ERR_PTR(-ENOMEM);

// Set various attributes of the device structure
dev->devt = devt;
dev->class = tty_class;
dev->parent = device;
dev->release = tty_device_create_release;
dev_set_name(dev, "%s", name);// Set device name
dev->groups = attr_grp;// Set attribute group
dev_set_drvdata(dev, drvdata);

dev_set_uevent_suppress(dev, 1);// Set suppress uevent

// Register device to kernel
retval = device_register(dev);
if (retval)
goto err_put;

if (!(driver->flags & TTY_DRIVER_DYNAMIC_ALLOC)) {
/*
* Free any saved termios data so that the termios state is
* reset when reusing a minor number.
*/
/*
* If the driver is not dynamically allocated,Then release any saved terminal parameter data,
* So that when reusing a minor device number,The terminal parameter state will be reset。
*/
tp = driver->termios[index];
if (tp) {
driver->termios[index] = NULL;
kfree(tp);
}
// Add cdev to tty core layer
retval = tty_cdev_add(driver, devt, index, 1);
if (retval)
goto err_del;
}

dev_set_uevent_suppress(dev, 0);// Unsuppress uevent
kobject_uevent(&dev->kobj, KOBJ_ADD);// Send uevent to notify device added

return dev;

err_del:
device_del(dev); // Delete device
err_put:
put_device(dev);// Release the memory space of the device structure

return ERR_PTR(retval);
}
EXPORT_SYMBOL_GPL(tty_register_device_attr);

This function is used to register a tty device to the tty core layer, including creating a cdev and adding attribute groups.

First, it generates the device name based on the driver type, allocates memory space for the device structure, and sets various attributes of the device.

Then, it registers the device to the kernel and decides whether to add the cdev to the tty core layer based on whether the driver is dynamically allocated.

Finally, it un-suppresses the uevent and sends a uevent to notify that the device has been added, and returns the device structure pointer.

tty_cdev_add()

Adding cdev to the tty core layer uses thetty_cdev_addfunction, as shown below:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/*
* To the tty core layer add cdev。
*/
static int tty_cdev_add(struct tty_driver *driver, dev_t dev,
unsigned int index, unsigned int count)
{
int err;

/* init here, since reused cdevs cause crashes */
driver->cdevs[index] = cdev_alloc();// Allocate a cdev structure
if (!driver->cdevs[index])// Return an error if allocation fails
return -ENOMEM;
driver->cdevs[index]->ops = &tty_fops; // Set the operations of cdev
driver->cdevs[index]->owner = driver->owner;// Set the owner of cdev
err = cdev_add(driver->cdevs[index], dev, count);// Add cdev to the kernel
if (err)
kobject_put(&driver->cdevs[index]->kobj);// Release resources when adding fails
return err;
}

This function is used to add a cdev to the tty core layer. First, it allocates a cdev structure and sets its operations and owner. Then, it calls thecdev_add()function to add the cdev to the kernel. If the addition fails, it releases the allocated resources.

In line 13 of the above code,tty_fopsis the file operations structure in the tty driver, which defines the operation functions for tty device files.

These functions include implementations for operations such as opening, closing, reading, writing, and controlling tty device files. Generally, these functions call the corresponding tty core layer functions to complete operations on the underlying tty device.

tty_fopsThe structure is as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static const struct file_operations tty_fops = {
.llseek = no_llseek,
.read_iter = tty_read,
.write_iter = tty_write,
.splice_read = generic_file_splice_read,
.splice_write = iter_file_splice_write,
.poll = tty_poll,
.unlocked_ioctl = tty_ioctl,
.compat_ioctl = tty_compat_ioctl,
.open = tty_open,
.release = tty_release,
.fasync = tty_fasync,
.show_fdinfo = tty_show_fdinfo,
};

When user space operates on a tty device file, it is actually calling the corresponding operation functions in tty_fops. Below is a simple example showing how to operate on a tty device file in user space

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
int main() {
int fd;
char buffer[128];
// Open the tty device file
fd = open("/dev/ttyS0", O_RDWR);
if (fd < 0) {
perror("Failed to open tty device");
return -1;
}
// Write data to the tty device file
write(fd, "Hello, tty device!", 18);
// Read data from the tty device file
read(fd, buffer, sizeof(buffer));
printf("Received data: %s\n", buffer);
// Close the tty device file
close(fd);
return 0;
}

Serial port programming

In Linux systems, file I/O operations and ioctl operations can be used for serial port programming.

  • File I/O operations can be used to read and write serial port data

  • ioctl operations can be used to set serial port parameters, control flow control, and obtain serial port status, among other operations

Serial port device node

In Linux systems, each device is represented by a device node, which is a file associated with the device
and exists as a file in the /dev directory.

Serial port device nodes usually start with tty, and the specific naming convention varies depending on the type and number of serial ports. After the development board system starts, use the following command to print the terminal device nodes, as shown in the figure below:

/dev/tty
/dev/tty

  • dev/ttyX (X is a number, such as 0, 1, 2, 3, etc.) device node: tty is short for teletype. In Linux,/dev/ttyXthey represent local terminals. The Linux kernel generates 63 local terminals during initialization, including/dev/tty1~/dev/tty63a total of 63 local terminals, which can be connected to the development board’s LCD display, keyboard, mouse, etc.

  • Serial port terminal device node: From the development board schematic, it can be seen that the iTOP-3568 development board has four serial ports: UART2, UART4, UART7, and UART9. Among them, UART2 is the serial debug terminal, and its corresponding device node is/dev/ttyFIQ0, and the other three serial ports UART4, UART7, and UART9 correspond to/dev/ttyS4/dev/ttyS7/dev/ttyS9

  • USB-based virtual serial portsttyGS0andttyUSBX(X is a number, such as 0, 1, 2, 3, etc.) are all USB virtual serial ports. Among them, ttyGS0 is the serial port virtualized by the USB for flashing. After the system starts, you can enter the development board console via the “adb shell” command in a Windows terminal. ttyUSBX here is the virtual serial port for the 4G module.

struct termios structure

struct termiosis a structure in the Linux kernel used to describe parameters of terminal devices (including serial port devices).

It is defined in the<linux/termios.h>header file, containing multiple fields for configuring and managing the attributes and behavior of terminal devices, including input/output baud rate, data bits, parity bits, stop bits, etc. Its definition is as follows:

1
2
3
4
5
6
7
struct termios {
tcflag_t c_iflag; // Input mode flags
tcflag_t c_oflag; // Output mode flags
tcflag_t c_cflag; // Control mode flags
tcflag_t c_lflag; // Local mode flags
cc_t c_cc[NCCS]; // Control character array
};

Below are some important fields of the struct termios structure:

  • tcflag_t c_iflag: This field contains input mode flags, used to configure the input behavior of the terminal device, such as input control characters, input data processing, etc.
  • tcflag_t c_oflag: This field contains output mode flags, used to configure the output behavior of the terminal device, such as output data processing, output control characters, etc.
  • tcflag_t c_cflag: This field contains control mode flags, used to configure the control parameters of the terminal device, such as baud rate, data bits, stop bits, parity bits, etc.
  • tcflag_t c_lflag: This field contains local mode flags, used to configure the local operations and input/output behavior of the terminal device.
  • cc_t c_cc[NCCS]: This field contains an array of special control characters, used to configure the control characters of the terminal device, such as erase character, end character, stop character, etc.

Input mode

Input mode settings

MembersMeaning of corresponding members
IGNBRKIgnore input termination conditions
BRKINTSend SIGINT signal when input termination condition is detected
IGNPARIgnore framing errors and parity errors
PARMRKMark parity errors
INPCKPerform parity check on received data
ISTRIPStrip all received data to 7 bits, i.e., remove the eighth bit
INLCRMap received NL (newline) to CR (carriage return)
IGNCRIgnore received CR (carriage return)
ICRNLMap received CR (carriage return) to NL (newline)
IUCLCMap received uppercase characters to lowercase characters
IXONEnable output software flow control

Output mode

Output mode controls how output characters are processed, i.e., how character data sent by the application is handled before being transmitted to the serial port or screen. It can be used forc_oflagThe macros for the member are as follows:

MemberMeaning of the corresponding member
OPOSTEnable output processing; if this flag is not set, other flags are ignored
OLCUCConvert uppercase characters in output to lowercase characters
ONLCRConvert newline (NL ‘\n’) in output to carriage return (CR ‘\r’)
OCRNLConvert carriage return (CR ‘\r’) in output to newline (NL ‘\n’)
ONOCRDo not output carriage return (CR) at column 0
ONLRETDo not output carriage return
OFILLSend fill characters to provide delay
OFDELIf this flag is set, the fill character is DEL; otherwise, it is NULL character

Control mode

In this structure, the most importantc_cflag, can control the hardware characteristics of the terminal device. For example, for a serial port, this field is important and can set hardware features such as baud rate, data bits, parity bits, and stop bits. By settingstruct termiosin the structurec_cflagThe member’s flags configure the control mode. Can be used forc_cflagThe member’s flags are as follows:

Baud Rate bitmask

Constant NameMeaning
B00 baud (DTR dropped)
B18001800 baud
B24002400 baud
B48004800 baud
B96009600 baud
B1920019200 baud
B3840038400 baud
B5760057600 baud
B115200115200 baud

Data Bits Bitmask

Constant NameMeaning
CS55 Data Bits
CS66 Data Bits
CS77 Data Bits
CS88 Data Bits

Stop Bits Bitmask

Constant NameMeaning
CSTOPB2 Stop Bits (default is 1 Stop Bit)

Other Control Flags

Constant NameMeaning
CREADReceive Enable
PARENBParity enable
PARODDUse odd parity instead of even parity
HUPCLHang up on last close (drop DTR)
CLOCALLocal connection (do not change port owner)
LOBLKBlock job control output
CNET_CTSRTSHardware flow control enable

Local mode

Local mode is used to control the terminal’s local data processing and working mode. By setting thestruct termiosstructure member’sc_lflagflags, the local mode is configured. The availablec_lflagmember flags are as follows:

Constant nameMeaning description
INPCKEnable parity check function. When enabled, the system checks received data for parity errors.
IGNPARIgnore parity errors. Even if a parity error is detected, data is not discarded or an exception is raised.
PARMRKWhen a parity error occurs, mark the character (usually by inserting a\033and\000) for debugging.
ISTRIPClear the 8th bit (most significant bit) of all received data bytes, i.e., keep only the lower 7 bits (for 7-bit character sets).
IXONEnableoutput software flow control(XON/XOFF). When the buffer is full, send XOFF (^S) to pause transmission; when idle, send XON (^Q) to resume.
IXOFFEnableinput software flow control. Allows the device to actively send XON/XOFF control characters at the receiving end to control the sender.
IXANYAllow any character (not just XON) to trigger flow control restart. By default, only XON can restart transmission.
IGNBRKIgnore the break condition. That is, ignore line break signals (such as prolonged low level).
BRKINTWhen a break is detected, send aSIGINTsignal to the process (usually used to interrupt program execution).
INLCRConvert newline characters (NL,\n) converted to carriage return (CR,\r). Common in some older terminals.
IGNCRIgnore carriage return (CR,\r) in input, do nothing.
ICRNLConvert carriage return (CR,\r) to newline (NL,\n). This is a common end-of-line handling method.
ICANONEnable canonical mode. In this mode, input is processed line by line (ending with newline), supporting editing (e.g., backspace, delete), echo, etc. Disabling enters raw mode.

Special control characters

Special control characters are character combinations such as Ctrl+C, Ctrl+Z, etc. When a user types such key combinations, the terminal takes special action.struct termiosIn the structure,c_ccthe array maps various special characters to corresponding support functions. Each character position (array index) is defined by corresponding macros, as shown below

Constant nameFunctionDefault keyUsage Scenarios
VKILLDelete Entire LineCtrl+UEdit Long Commands
VEOFEnd of FileCtrl+DSubmit Input or Close
VEOLEnd-of-Line MarkerCRCompatibility with Old Terminals
VEOL2Second End-of-LineLFMultiple Line Break Formats
VMINMinimum Character Count-Non-Canonical Mode Read Control
VTIMETimeout Duration-Non-Canonical Mode Read Control
VINTRInterruptCtrl+CTerminate Program
VQUITPauseCtrl+ZSuspend Process
VERASEDelete CharacterBackspaceSingle Character Editing

Common Serial Port Control Functions

tcgetattr()

Function: Get the attribute configuration of the current terminal (or serial port device).
Prototype

1
2
#include <termios.h>
int tcgetattr(int fd, struct termios *termios_p);
  • Parameters
    • fd: File descriptor (e.g., opened serial port/dev/ttyS0
    • termios_p: Pointer tostruct termiospointer, used to store the current configuration
  • Return Value: Returns 0 on success, -1 on failure
  • Purpose: Reads the current serial port settings such as baud rate, data bits, parity, flow control, input/output mode, etc.

Usually, this function is called before modifying the serial port to save the original configuration, so that it can be restored when the program exits.


tcsetattr()

Function: Sets the attribute configuration of the terminal (or serial port).
Prototype

1
int tcsetattr(int fd, int optional_actions, const struct termios *termios_p);
  • Parameters
    • fd: File descriptor
    • optional_actions: Specifies when to apply the new settings. Common values:
      • TCSANOW: Takes effect immediately
      • TCSADRAIN: Takes effect after output is complete (commonly used for output-related settings)
      • TCSAFLUSH: Takes effect immediately after clearing the input/output buffer
    • termios_p: Contains the new configurationstruct termiospointer
  • Return value: Returns 0 on success, -1 on failure
  • Purpose: Apply new serial port parameters (e.g., baud rate, 8N1 configuration, raw mode, etc.)

⚠️ After modification, it is recommended to check the return value to ensure the setting is successful.


cfgetispeed() and cfgetospeed()

Function: Get the input (receive) and output (transmit) baud rates respectively.
Prototype

1
2
speed_t cfgetispeed(const struct termios *termios_p);
speed_t cfgetospeed(const struct termios *termios_p);
  • Return Value: Baud rate constant (e.g.,B9600,B115200), not the actual value (e.g., 9600)
  • Note: Returns aspeed_ttype ofmask value, cannot be used directly as an integer

The actual baud rate needs to be converted via lookup table or system-specific methods (some systems providecfmakeraw()orioctlto obtain the actual rate).


cfsetispeed() and cfsetospeed()

Function: Set input and output baud rates respectively.

Prototype

1
2
int cfsetispeed(struct termios *termios_p, speed_t speed);
int cfsetospeed(struct termios *termios_p, speed_t speed);
  • Parameters

    • termios_p: Pointer to thetermiosstructure to be modified
    • speed: Baud rate constant (e.g.,B9600,B115200
  • Return value: Returns 0 on success, -1 on failure

  • Typical usage

    1
    2
    3
    4
    5
    struct termios tty;
    tcgetattr(fd, &tty);
    cfsetospeed(&tty, B115200);
    cfsetispeed(&tty, B115200);
    tcsetattr(fd, TCSANOW, &tty);

Typically, the transmit and receive baud rates are set to the same value (full-duplex communication), unless special requirements exist.


tcflush() and tcflow()

tcflush()

Function: Flush the terminal’s input or output queue.
Prototype

1
int tcflush(int fd, int queue_selector);
  • queue_selectorValues:
    • TCIFLUSH: Flushinput queue(data received but not read)
    • TCOFLUSH: Flushoutput queue(data to be sent but not yet transmitted)
    • TCIOFLUSH: Flush both input and output queues
  • Purpose: Clear dirty data before reconfiguring the serial port or restarting communication.

tcflow()

Function: Controls the data flow of the serial port (pause/resume transmission).
Prototype

1
int tcflow(int fd, int action);
  • actionValues:
    • TCOOFF: PauseOutput(Sender stops sending)
    • TCOON: ResumeOutput
    • TCIOFF: PauseInput(Send XOFF to the peer, requesting it to pause)
    • TCION: ResumeInput(Send XON to the peer, allowing it to continue sending)
  • Usage: Manually implement software flow control (XON/XOFF), or handle buffer overflow.

💡 Note:tcflow()depends onIXON/IXOFFwhether the flag is enabled.

Serial Port Operation Flow

Set the Baud Rate of the Serial Port

When writing serial port applications, setting the baud rate is one of the necessary steps, as it determines the data transmission speed.

In Linux, setting the baud rate typically uses the cfsetspeed() function. The required header file and function prototype for cfsetspeed() are as follows:

1
2
3
#include <termios.h>
#include <unistd.h>
int cfsetspeed(struct termios *termios_p, speed_t speed);

It accepts astruct termiosstructure as an input parameter and returns an integer value indicating whether the operation was successful.

This function actually sets the baud rate in thec_cflagfield. For example, to set the baud rate to 115200, you can call the following code:

1
2
3
4
5
6
7
struct termios options;
// Get the current terminal configuration
tcgetattr(fd, &options);
// Set the baud rate to 115200
cfsetspeed(&options, B115200);
// Write the new terminal configuration to the terminal
tcsetattr(fd, TCSANOW, &options);

The following points need to be noted:

  • First, you need to open the serial port device file and obtain the serial port’s attributes, including the baud rate and other properties. You can use thetcgetattr()function to get the attribute values and store them in atermiosstruct variable.
  • When setting the baud rate, you need to call thecfsetspeed()function and pass a baud rate constant as a parameter. Common baud rate constants include B9600, B115200, etc. These constants can be found in the header filetermios.h.
  • After setting, you need to use thetcsetattr()function to write the attribute values back to the serial port device.

Setting the Data Bit Size

In serial communication, the data bit refers to the number of actual data bits contained in each character (byte). Typically, a character contains 8 bits (i.e., 8 zeros or ones), but it can sometimes be 7 bits or other values.

When writing serial port applications, you need to use thestruct termiosmember variable in thec_cflagstruct to set the data bit size. Specifically, you need to clear the bits related to the data bit in thec_cflagmember variable, and then set the corresponding value as needed.

The clear operation typically uses the bitwise AND (&) operator and the bitwise NOT (~) operator. The specific steps are as follows:

1
new_cfg.c_cflag &= ~CSIZE; // Clear the bits related to the data bits.

Here, CSIZE is a macro definition representing the bit mask for the data bits. The macro is usually defined in thetermios.hheader file, with its value as follows:

1
#define CSIZE 0x00000300 /* Character length mask */

Through the clear operation, all bits related to the data bits are set to 0. Next, the bitwise OR (|) operator and macro definitions can be used to set the specific number of data bits, for example:

1
new_cfg.c_cflag |= CS8; // Set the number of data bits to 8 bits.

At this point, the CS8 macro definition will be interpreted as a bit mask containing 8 bits, and it is set into thec_cflagmember variable via the bitwise OR operation, thus completing the setting of the data bits.

Set the parity bit.

The configuration of the serial port’s parity bit involves a total ofstruct termiostwo member variables in the structure:c_cflagandc_iflag

First, for thec_cflagmember, it is necessary to addPARENBFlag to enable the parity check function of the serial port. Only after enabling the parity check function will a parity bit be generated for output data, thereby performing parity check on input data;

At the same time, forc_iflagmembers, it is also necessary to addINPCKflag, so that parity check can be performed on the received data. The code is as follows:

Odd parity enable

1
2
new_cfg.c_cflag |= (PARODD | PARENB); // Set to odd parity
new_cfg.c_iflag |= INPCK; // Enable parity check

Even parity enable

1
2
3
new_cfg.c_cflag &= ~PARODD; // Set to even parity
new_cfg.c_cflag |= PARENB; // Enable parity check
new_cfg.c_iflag |= INPCK; // Perform parity check on input data

No parity

1
2
new_cfg.c_cflag &= ~PARENB; // Disable parity check
new_cfg.c_iflag &= ~INPCK; // Do not perform parity check

Set stop bits

In serial communication, the stop bit is used to specify the end position of each data frame.

After transmitting a complete data byte, one or more stop bits are usually required so that the receiving end can determine the end of a data frame.

The number of stop bits is typically 1 or 2 bits, with 1 stop bit being widely used and 2 stop bits being less common.

In Linux, by setting thestruct termiosmember variable in thec_cflagstructure to control theCSTOPBflag bit to control the number of stop bits. WhenCSTOPBis 0, only 1 stop bit is used; whenCSTOPBis 1, 2 stop bits are used.

For example, the following code sets the stop bit of the serial port to 1 bit:

1
new_cfg.c_cflag &= ~CSTOPB; // Set stop bit to 1 bit

Example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
#include <stdio.h>
#include <termios.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>

/* Function to set serial port parameters */
int set_uart(int fd, int speed, int bits, char check, int stop) {
struct termios newtio, oldtio;

// Step 1: Save the original serial port configuration
if(tcgetattr(fd, &oldtio) != 0) {
printf("tcgetattr oldtio error\n");
return -1;
}

bzero(&newtio, sizeof(newtio));

// Step 2: Set the control mode flags
newtio.c_cflag |= CLOCAL | CREAD;
newtio.c_cflag &= ~CSIZE;

// Step 3: Set Data Bits
switch(bits) {
case 7:
newtio.c_cflag |= CS7;
break;
case 8:
newtio.c_cflag |= CS8;
break;
}

// Step 4: Set Parity Bit
switch(check) {
case 'O': // Even Parity
newtio.c_cflag |= PARENB;
newtio.c_cflag |= PARODD;
newtio.c_iflag |= (INPCK | ISTRIP);
break;
case 'E': // Odd Parity
newtio.c_cflag |= PARENB;
newtio.c_cflag &= ~PARODD;
newtio.c_iflag |= (INPCK | ISTRIP);
break;
case 'N': // No Parity
newtio.c_cflag &= ~PARENB;
break;
}

// Step 5: Set Baud Rate
switch(speed) {
case 9600:
cfsetispeed(&newtio, B9600);
cfsetospeed(&newtio, B9600);
break;
case 115200:
cfsetispeed(&newtio, B115200);
cfsetospeed(&newtio, B115200);
break;
}

// Step 6: Set Stop Bits
switch(stop) {
case 1:
newtio.c_cflag &= ~CSTOPB; // 1 Stop Bit
break;
case 2:
newtio.c_cflag |= CSTOPB; // 2 Stop Bits
break;
}

// Step 7: Flush Input Queue
tcflush(fd, TCIFLUSH);

// Step 8: Apply Configuration Immediately
if (tcsetattr(fd, TCSANOW, &newtio) != 0) {
printf("tcsetattr newtio error\n");
return -2;
}

return 0;
}

int main(int argc, char *argv[]) {
int fd;
char buf[128];
int count;

// Step 9: Open Serial Port Device
fd = open("/dev/ttyS9", O_RDWR | O_NOCTTY | O_NDELAY);

if (fd < 0) {
printf("open error \n");
return -1;
}

// Set Serial Port Parameters
set_uart(fd, 115200, 8, 'N', 1);

// Write Data
write(fd, argv[1], strlen(argv[1]));
sleep(1);

// Read Data
count = read(fd, buf, sizeof(buf));
buf[count] = '\0';

// Output the read data
printf("read message is %s\n", buf);

// Close the serial port device
close(fd);

return 0;
}

GPS module programming

The Global Navigation Satellite System is a system that uses satellite technology to provide precise time and location information to users worldwide.

The positioning module is often connected to the CPU via a serial port, and then the CPU uploads the coordinates through other methods such as Wi-Fi or Bluetooth.

The GPS module schematic is as follows

GPS module schematic
GPS module schematic

Among them,BUF_GPS_RSTthe interface is left floating and not connected. The following is the connection table for the 20-pin base connecting to the RK3568 development board GPIO interface:

GPS module pin numberGPS module pin nameConnected development board pin numberConnected development board pin name
2BUF_GPS_TXD8UART9_RX_M1
3BUF_GPS_RXD6UART9_TX_M1
11GND19/20GND
19VDD33_A312/4VCC3V3_SYS

After the GPS module is connected, the antenna should be placed outdoors

Introduction to GPS data frames

GPS data
GPS data

Listed here are the common data types in the GPS NMEA format, including:

  • GPRMC (Recommended Minimum Specific GPS/Transit data): The GPRMC data type provides the most basic GPS positioning information such as position, speed, and heading. This data type is commonly used in navigation systems and automatic ship navigation systems.
  • GPVTG (Track Made Good and Ground Speed): The GPVTG data type provides course over ground and ground speed information, used to display navigation information for moving objects such as ships and vehicles. It provides the course over ground and speed relative to the ground.
  • GPGGA (Global Positioning System Fix Data): The GPGGA data type includes position fix, time, position accuracy, altitude, and other location information, typically used by receivers to display the current position in real time.
  • GPGSA (GPS DOP and Active Satellites): The GPGSA data type includes DOP values and the current satellite positioning status. It is used to provide information such as the number of available satellites in GPS satellite measurement data and the calculated position dilution of precision (DOP).
  • GPGSV (GPS Satellites in View): The GPGSV data type provides information on currently visible satellites, including the satellite’s PRN, elevation, azimuth, signal strength, and more. This information helps the receiver find more satellites and improve positioning accuracy.
  • GPGLL (Geographic Position - Latitude/Longitude): The GPGLL data type provides longitude and latitude information, used to describe the receiver’s current latitude and longitude position.

We only need to focus on the GPRMC message, as shown in the figure below:

GPRMC
GPRMC

GPRMC (Recommended Minimum Specific GNSS Data) is a common GPS data frame format used to transmit GPS position information between GPS devices or between GPS devices and other devices. The following is the information contained in the GPRMC data frame:

Field NumberField Value (Example)MeaningDetailed Explanation
0$GPRMCMessage IDIndicates this is the “Recommended Minimum Positioning Information” frame.GPIndicates provided by GPS system; other systems like GLONASS useGL, BeiDou usesBDorGB
1083634.00UTC timeFormat:hhmmss.sssIndicates08:36:34.000 UTC→ Beijing time = UTC + 8 hours →16:36:34 (4:36 PM)
2AStatus indicator-AValid positioning(Active / Valid) -VInvalid positioning(Void / Invalid), possibly due to weak signal or no satellite lock
33854.62194Latitude (value)Format:ddmm.mmmmi.e.**38°54.62194′**Convert to decimal degrees:38 + 54.62194/60 ≈ 38.910366° N
4NLatitude hemisphere-N: North -S: South
511526.10876Longitude (value)Format:dddmm.mmmmi.e.**115°26.10876′**Convert to decimal degrees:115 + 26.10876/60 ≈ 115.435146° E
6ELongitude hemisphere-E: East -W: West
70.932Ground speed (knots)Unit:knots1 knot = 1 nautical mile/hour ≈ 1.852 km/h →0.932 × 1.852 ≈ 1.73 km/h(walking speed)
855.00Course Over GroundUnit:degrees (°), withtrue north as 0°Range: 0.0 ~ 359.9° 55° indicates northeast direction
9200624UTC DateFormat:ddmmyyJune 20, 2024
10(empty)Magnetic DeclinationOptional field, indicating the angle between magnetic north and true north (unit: degrees)
11(empty)Magnetic declination directionEorW, used with field 10
12(Empty)Positioning mode-A: AutonomousD: DGPSE: Dead ReckoningN: No positioning (Note: Some modules output in this field, some put it in$GPGSA)
13D*50ChecksumFrom$the first character after*to all characters beforeXOR checksumUsed to verify data integrity

Example

gps.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#ifndef __GPS_H__
#define __GPS_H__

// Define the structure gprmc_data to store parsed GPS data
struct gprmc_data {
char id; // Data identifier (unused)
int time; // UTC time (hhmmss.sss format)
char state; // Status indicator (A=valid, V=invalid)
float latitude; // Latitude (ddmm.mmmm format)
char NS; // Latitude hemisphere (N=Northern, S=Southern)
float longitude; // Longitude (dddmm.mmmm format)
char EW; // Longitude hemisphere (E=East, W=West)
float speed; // Ground speed (knots)
int date; // UTC date (ddmmyy format)
char mode; // Mode indicator (A=autonomous, D=differential)
char check; // Checksum (unused)
};

// Declare function prototype: set serial port parameters
extern int set_uart(int fd, int speed, int bits, char check, int stop);

// Declare function prototype: Parse GPS data
extern void get_gps_data(char *buff, struct gprmc_data *gps_data);

#endif

gps.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include <stdio.h>
#include <termios.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include "gps.h"

// Function: get_gps_data
// Description: Parse GPS data from the data buffer and populate it into the structure
// Parameters:
// - buff: Pointer to the buffer containing GPS data
// - gps_data: Pointer to the structure gprmc_Pointer to data, used to store the parsed GPS data
// Return value: None
void get_gps_data(char *buff, struct gprmc_data *gps_data) {
char *p=NULL;
// Find the position in the buffer starting with "$GPRMC"
p = strstr(buff, "$GPRMC");
if (p == NULL) {
// printf("Error: $GPRMC not found in buffer.\n");
return; // If "$GPRMC" is not found, return directly
}
// Use the sscanf function to parse data from string p according to the specified format and store it into the corresponding member variables of the gps_data structure
sscanf(p,"$GPRMC,%d.00,%c,%f,%c,%f,%c,%f,,%d,,,%c,%*c",\
&(gps_data->time),\
&(gps_data->state),\
&(gps_data->latitude),\
&(gps_data->NS),\
&(gps_data->longitude),\
&(gps_data->EW),\
&(gps_data->speed),\
&(gps_data->date),\
&(gps_data->mode));


// Print part of the parsed GPS data to verify if the parsing is correct (optional)
printf("state:%c, %c:%f, %c:%f\n", gps_data->state, gps_data->NS,
gps_data->latitude, gps_data->EW, gps_data->longitude);

}

uart.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
#include <stdio.h>
#include <termios.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>

/* Function to set serial port parameters */
int set_uart(int fd, int speed, int bits, char check, int stop) {
struct termios newtio, oldtio;

// Step 1: Save the original serial port configuration
if(tcgetattr(fd, &oldtio) != 0) {
printf("tcgetattr oldtio error\n");
return -1;
}

bzero(&newtio, sizeof(newtio));

// Step 2: Set the control mode flag
newtio.c_cflag |= CLOCAL | CREAD;
newtio.c_cflag &= ~CSIZE;

// Step 3: Set Data Bits
switch(bits) {
case 7:
newtio.c_cflag |= CS7;
break;
case 8:
newtio.c_cflag |= CS8;
break;
}

// Step 4: Set Parity Bit
switch(check) {
case 'O': // Even Parity
newtio.c_cflag |= PARENB;
newtio.c_cflag |= PARODD;
newtio.c_iflag |= (INPCK | ISTRIP);
break;
case 'E': // Odd Parity
newtio.c_cflag |= PARENB;
newtio.c_cflag &= ~PARODD;
newtio.c_iflag |= (INPCK | ISTRIP);
break;
case 'N': // No Parity
newtio.c_cflag &= ~PARENB;
break;
}

// Step 5: Set Baud Rate
switch(speed) {
case 9600:
cfsetispeed(&newtio, B9600);
cfsetospeed(&newtio, B9600);
break;
case 115200:
cfsetispeed(&newtio, B115200);
cfsetospeed(&newtio, B115200);
break;
}

// Step 6: Set Stop Bits
switch(stop) {
case 1:
newtio.c_cflag &= ~CSTOPB; // 1 Stop Bit
break;
case 2:
newtio.c_cflag |= CSTOPB; // 2 Stop Bits
break;
}

// Step 7: Flush Input Queue
tcflush(fd, TCIFLUSH);

// Step 8: Apply Configuration Immediately
if (tcsetattr(fd, TCSANOW, &newtio) != 0) {
printf("tcsetattr newtio error\n");
return -2;
}

return 0;
}