UART 串口(Serial Port)也叫串行通信接口,通常也叫做 COM 接口 ,是一种计算机与外部设备(如串口通信设备)进行数据通信的异步全双工接口 。它通过串行传输 方式,即一次只发送一个比特位(bit)来进行数据传输。
具体来说,典型的串口通信只需要 3 根线,分别是地线(GND),发送线(TX),接收线(RX),如下图所示,发送和接收各用一条线,没有时钟线 。
波特率 波特率(Baud Rate)是串口通信中的关键参数,它指的是每秒传输的比特位数或者码元速率。
在数字信道中,一个脉冲信号就是一个码元,如下图所示。码元速率表示在 1 秒内能发送多少码元或脉冲信号。
通信的双方必须设定相同的波特率才能确保数据能够正确传输。常见的标准波特率如 9600、115200 等,通常满足大多数应用需求。
但在特定情况下,可能需要设置非标准的波特率,此时需确保所有通信设备都能支持和正确配置该波特率。
对于高于 1.5Mbps 的波特率的设置,可能需要通过时钟的小数分频或整数分频来实现。
如果无法通过分频来达到所需波特率,则可能需要调整 PLL(锁相环)设置,调整 PLL 存在一定风险,因为它可能会影响到设备的其他模块。
比特率 是指单位时间内传输的比特数量 ,通常用 bps(bit per second)表示,单位是 bit/s。与之相比,波特率 则是每秒钟传输的码元或脉冲信号的数量 。这俩者之间的关系可以用公式表示为比特率=波特率*log2(M) ,其中 M 表示每个码元承载的信息量。
一个码元实际上就是一个脉冲信号,它可能携带 1 个 bit,2 个bit 或者更多 bit 的数据,这取决于通信系统的具体实现方式。在二进制系统中,比特率等于波特率,因为每个码元正好承载 1 个 bit 的信息 。
举例:假如串口的波特率为 9600,那么在二进制系统中,一秒钟可以传送多少个字节呢?
一个字节等于 8 个 bit,也就是 8 个高低电平变化,因为在二进制系统中,比特率等于波特率。因此一秒钟可以传输的字节数为 9600/8=1200 字节。
串口通讯协议 数据流结构 在串口通信中,除了关注波特率外,数据流的结构也至关重要。
每帧数据包括 11 位:
1位起始位
8 位数据位
1 位奇偶校验位
1 位停止位
起始位 :表示数据传输的开始,数据线上空闲状态是 1,从高电平(空闲状态)拉低表示开始传输数据。
数据位 :指每个字节中的数据位数,通常为 7 或 8 位。
校验位 :用于验证传输数据的准确性,其类型包括奇校验、偶校验、0 校验、和 1 校验。
奇校验(odd parity) : 当实际数据中 1 的个数为偶数的时候,校验位为 1,否则这个校验位就是 0。
偶校验(even parity) : 当实际数据中 1 的个数为偶数的时候,校验位为 0,否则这个校验位就是 1。
0 校验(space parity) : 校验位恒为 0,如果为 1 表示错误。
1 校验(mark parity) : 校验位恒为 1,如果为 0 表示错误。
停止位 :指在每个数据字节传输结束后发送的位数,通常为 1 或 2 位。
时序波形分析
当数据线空闲时是高电平 ,而在数据传输时则会被拉低 ,图中的第一个脉冲对应于起始位。紧随其后的是 8 位数据位,它们按照最低有效位(LSB)优先的顺序传输。
例如,数据位“00110001”转换成十六进制是 0x31,换算成十进制是 49。在 ASCII码中,49 对应的字符是’1’,因此传输的数据是 1。数据传输完成之后,总线被拉高。
串口通信接口的类型 UART 只规定了收发的时序,即“先发起始位,再发数据位,校验位,最后发停止位”,只规定了高低电平,但是并没有规定高电平指的是多少伏,低电平指的是多少伏。
串口一般的接口电平有 TTL ,RS232 ,RS485 ,RS422 ,并且每种接口通常需要相应的电平转换芯片 。直接使用处理器引出的串口接口时,通常是 TTL 电平 。
然而,处理器的不同型号或供应商可能存在电平差异,这意味着在某些情况下不能直接连接设备。因此,为了确保正常通信必须进行适当的电平转换。一般的串口接口电平对比如下表所示:
电平标准
逻辑电平定义
典型高/低电平电压
信号类型
最大传输距离
主要特点
TTL
0 = 低电平 1 = 高电平
低电平:0 V 高电平:1.8 V / 2.5 V / 3.3 V / 5 V(取决于供电)
单端信号
几米(通常 ≤ 2–5 m)
板内或板间短距离通信 ,与 MCU、传感器等直接兼容 , 抗干扰能力弱,不适用于长距离
RS-232
0 = 正电压 (+3 V ~ +15 V) 1 = 负电压 (-3 V ~ -15 V)
逻辑 0(空号):+3 V ~ +15 V 逻辑 1(传号):-3 V ~ -15 V 常用 ±12 V
单端(但使用正负电压)
约 15–50 米(速率越低距离越长)
支持点对点通信,抗干扰能力较强于 TTL,需电平转换芯片(如 MAX232),已逐渐被 USB 等替代
RS-422
差分信号: 1 = A > B(+2 V ~ +6 V) 0 = A < B(-2 V ~ -6 V)
差分电压:±2 V ~ ±6 V
差分信号 (全双工,4 线)
最大约 1200 米 (@ 100 kbps)
支持全双工通信,强抗共模干扰能力,适用于工业环境,通常为 1 发送器,多接收器
RS-485
差分信号: 1 = A > B(+1.5 V ~ +5 V) 0 = A < B(-1.5 V ~ -5 V)
差分电压:≥ ±1.5 V(典型 ±2 V ~ ±5 V)
差分信号 (半双工或全双工,2 或 4 线)
最大约 1200 米 (@ 100 kbps) (理论可达 1219 米)
支持多点通信(最多 32~256 节点) ,广泛用于工业总线(如 Modbus),抗干扰强,适合长距离、噪声环境
RS232接口 RS232(Recommended Standard 232) 协议是由美国电子工业协会在 1970 年制定的串行通信标准。该标准统一了串口通信的连接器和引脚定义 ,如下图所示,并明确规定了每个连接器引脚的电平标准
DB9 引脚说明 DB9 是一种常见的串口连接器,通常用于 RS-232 串口通信和其他串行通信应用。它包含 9个引脚,每个引脚都有特定的功能。下面是 DB9 连接器的引脚说明:
Pin 1 - DCD (Data Carrier Detect) :数据载波检测。指示远程设备是否准备好进行通信。
Pin 2 - RXD (Receive Data) :接收数据。从远程设备接收数据流。
Pin 3 - TXD (Transmit Data) :发送数据。向远程设备发送数据流。
Pin 4 - DTR (Data Terminal Ready) :数据终端就绪。指示数据终端设备(如计算机)准备好进行通信。
Pin 5 - GND (Ground) :地线。电气接地,用于电路的参考和屏蔽。
Pin 6 - DSR (Data Set Ready) :数据集准备好。指示远程设备准备好接收和发送数据。
Pin 7 - RTS (Request to Send) :请求发送。发送方使用此信号请求开始发送数据。
Pin 8 - CTS (Clear to Send) :清除发送。接收方使用此信号表示准备好接收数据。
Pin 9 - RI (Ring Indicator) :铃信号指示器。指示远程设备发送了铃响信号。
RS232 本质上也是串口协议,与串口协议相同。但它规定了串口的物理接口和电平特性,因此在硬件层面有所不同,但在软件编程和串口通信的实现上并无区别
电平特性
RS232 接收-发送端的电信号是{ % emp 相对于公共地线(GND)的电压信号 %}。
在 RS232 标准中,电压差值在+3V 至+15V 之间被定义为逻辑“0” ,而在**-3V 至-15V 之间则表示逻辑“1”**。电压差值在-3V 至+3V 之间的情况未定义。
通常,实际应用中希望电压差的绝对值在 5V 至 15V 之间,以确保可靠的信号传输。
在数据发送时,发送端驱动器输出+5V 至+15V 的正电压信号表示逻辑“0”,而-5V 至-15V 的负电压信号表示逻辑“1”。
在数据接收时,只要检测到大于3V 的电信号,即视为有效信号。
RS232 接口在发送和接收时具有相对较高的电流能力,可以处理相对较大的电流负载,这使得它适合于长距离通信和连接外部设备。
RS232 信号具有较高的抗干扰能力,能够在工业环境中稳定运行,不易受到电磁干扰的影响。
RS232 包括以下信号线:
发送线(Tx)
接收线(Rx)
地线(Ground)
数据终端就绪(DTR)
数据就绪(DSR)
请求发送(RTS)
清除发送(CTS)
环路信号检测(DSR)
RS232 支持各种波特率,通常从低速率到较高速率,最高可以达到数百千位每秒(kbps)
iTOP-RK3568 开发板上,调试串口使用了 MAX3232 芯片将 TTL 电平转换成 232 电平,调试串口的原理图如下图所示
RS485接口 RS485 标准是由电信行业协会(Telecommunications Industry Association, TIA)和电子工业联盟(Electronic Industries Alliance, EIA)共同制定的。该标准的主要目的是解决长距离通信需求(最长达 1200 米)以及提供优秀的抗干扰性能 。
特点和优势
长距离通信能力 :RS485 能够在理想条件下实现长达 1200 米的通信距离,适用于需要远距离数据传输的场合。
抗干扰能力强 :由于采用差分信号传输方式,RS485 能有效抵御电磁干扰(EMI)和射频干扰(RFI),保证数据传输的稳定性和可靠性。
多站能力 :RS485 支持多个设备(最多 32 个)在同一条总线上进行通信,每个设备可以独立地发送和接收数据,从而实现灵活的网络组网。
应用广泛:RS485 广泛应用于工业自动化控制系统、建筑自动化、智能家居系统、电力系统监控等领域,满足对长距离、高速和可靠通信的要求
电平特性 RS485 的电平特性如下:
差分信号传输 :RS485 使用差分信号进行数据传输,即数据信号是相对于两条信号线(通常标记为 A 和 B 线)之间的电压差来表示的。这种差分信号传输方式使得 RS485 具有良好的抗干扰能力和长距离传输能力。
电压范围 :
逻辑“ 1”以两线间的电压差为+(2~6)V 表示 ;
逻辑“ 0”以两线间的电压差为-(2~6)V表示 。
接口信号电平比 RS232 降低了 ,不易损坏接口电路的芯片,且**该电平与 TTL 电平兼容**,可方便与 TTL 电路连接。
电流能力:RS485 发送器具有较强的驱动能力,能够推动较长的通信线路和多个接收器。接收器能够处理较大的输入电流,以确保信号的可靠接收。
电气特性:RS485 支持多个设备(最多 32 个)在同一条总线上进行通信 。支持长达 1200米的通信距离,在特定情况下甚至可以更长
iTOP-RK3568 开发板上使用 SIT3485E 芯片 将 TTL 电平转换为 485 电平,原理图如下图所示:
可知 RK3568 开发板的 RS485 接口实际上是通过 SIT3485E 芯片转接的串口 7。
SIT3485E芯片
SIT3485E 是一款 3.0V~5.5V 宽电源供电、总线端口 ESD 水平达到 15KV HBM 以上、总线耐压范围达到±15V、半双工 、低功耗 ,功能完全满足 TIA/EIA-485 标准要求的 RS-485 收发器 。
SIT3485E 包括一个驱动器 和一个接收器 ,两者均可独立使能与关闭 。当两者均禁用时,驱动器与接收器均输出高阻态。
SIT3485E 具有 1/8 负载,允许 256 个 SIT3485E 收发器并接在同一通信总线上。可实现高达 12Mbps 的无差错数据传输。
SIT3485E 工作电压范围为 3.0~5.5 V,具备失效安全(fail-safe)、限流保护、过压保护等功能。
引脚序号
引脚名称
引脚功能
1
RO
接收器输出端 。 当 /RE 为低电平时,若 A-B ≥ -10mV,RO 输出为高电平; 若 A-B ≤ -200mV,RO 输出为低电平。
2
/RE
接收器输出使能控制 。 当 /RE 接低电平时,接收器输出使能,RO 输出有效; 当 /RE 接高电平时,接收器输出禁能,RO 为高阻态; /RE 接高电平且 DE 接低电平时,器件进入低功耗关断模式。
3
DE
驱动器输出使能控制 。 DE 接高电平时驱动器输出有效,DE 为低电平时输出为高阻态; /RE 接高电平且 DE 接低电平时,器件进入低功耗关断模式。
4
DI
DI 驱动器输入 。 DE 为高电平时,DI 上的低电平使驱动器同相端 A 输出为低电平,驱动器反相端 B 输出为高电平; DI 上的高电平将使同相端输出为高电平,反相端输出为低。
5
GND
接地
6
A
接收器同相输入和驱动器同相输出端
7
B
接收器反相输入和驱动器反相输出端
8
VCC
接电源
由引脚定义图可知:
RO 引脚为接收器输出端,在原理图上连接到 UART7_RX_M1 引脚。
DI引脚为 DI 驱动器输入引脚,在原理图上连接到 UART7_TX_M1 引脚。
A 引脚是接收器同向输入和驱动器同向输出端
B 引脚是接收器反向输入和驱动器反向输出端,A 和 B 就是 RS485 的差分对了。
RE 引脚是接收器输出使能控制引脚
当/RE 接低电平时,接收器输出使能,RO 输出有效
当/RE 接高电平时,接收器输出禁能,RO 为高阻态。
DE 引脚是驱动器输出使能控制引脚,DE 接高电平时驱动器输出有效,DE 为低电平时输出为高阻态。
RE 接高电平且 DE 接低电平时,器件进入低功耗模式。
简单来说:
RE 为低电平时,RO 输出有效。反之 RO 高阻态,也就是无效。RO 接的 UART7_RX_M1,UART7_RX_M1 是串口的接收引脚。因此,/RE 低电平时,串口可以接收数据,反之串口无法接收数据。
DE 是高电平时,DI 有效,DI 接的是 UART7_TX_M1,UART7_RX_M1 是串口发送引脚,因此,DE 是高电平时,DI 上的低电平使驱动器 同向端 A 输出为低电平,驱动器反向端 B 输出为高电平,串口可以发送数据,反之串口无法发送数据。
RE 和 DE 有效电平刚好相反 ,又因为 485 是半双工 ,不能同时收发。所以RE 和 DE 肯定不能同时开启,即RE 和 DE 的电平必须相同 。这样RE 开启,就等于 DE 关闭,所以这两个引脚连在了一起。
这样就很清晰了,如果 GPIO0_C6 输出低电平,RE 和 DE 是低电平,RE 是低电平时,串口可以接收数据,DE 是低电平时,串口不能发送数据。
如果 GPIO0_C6 输出高电平,RE 和 DE 是高电平,RE 是高电平时,串口不能接收数据,DE 是高电平时,串口可以发送数据。
也就是说,我们需要用 GPIO0_C6 这个引脚的高低电平来控制 RS485 是发送还是接收。因此,我们需要编写一个驱动来达到这个目的。迅为提供的源码中默认配置了驱动来控制GPIO0_C6。
自收发 485 电路 除了软件控制外,还可以通过硬件实现自动切换,以实现 RS485 的自动收发功能。软件控制发送和接收存在一定的时间差,为了减少这种时间差,迅为开发板的底板原理图进行了优化,兼容了硬件实现自动收发。具体的修改方式是将 R295 去掉,并将下图中标记为 DNP 的元件都进行焊接。
当 UART_TX_M1 为高电平时,Q17 的基极也为高电平,导致 Q17 导通,RE 和 DE 为低电平,从而串口处于接收数据模式
当 UART_TX_M1 为低电平时,Q17的基极为低电平,导致Q17截止,RE 和DE为高电平,串口处于发送数据模式。由于UART_TX_M1 在空闲时为高电平,表明串口处于接收数据状态;当 UART_TX_M1 被拉低时,串口则处于发送数据状态。
既然 UART_TX_M1 高电平表示接收模式,那发送 1 时也是高电平。这样芯片会一直处于接收模式,导致 1 无法发送出去,是吗?
当转接芯片处于接收模式时,A 和 B 两个引脚会处于高阻态模式 。
高阻态意味着非常大的电阻,几乎等同于断路。此时,A 由上拉电阻拉高,B 由下拉电阻拉低 。
因此,A 高、B 低表示通信中的 1 。通过这种“接收模式”,我们巧妙地将“1”发送出去了。
这一优化省去了一个 RS485 收发控制 IO,将 RS485 完全当作串口使用,方便编写驱动。
串口子系统框架 串口子系统框架是 Linux 内核中专门用于处理串口设备的模块化框架,框架图如下图所示。
应用层 :位于最顶层,是串口子系统中用户空间应用程序与内核空间之间的接口,应用层包括了用户空间的串口应用程序,如串口通信工具 minicom 等。
字符设备层 :位于应用层的下方,负责将用户空间的串口读写请求传递给内核空间的tty_core 层。字符设备层将串口设备看作一个特殊的字符设备,通过字符设备接口来进行操作。
tty_core 层 :位于字符设备层的下方,是 Linux 内核中用于管理串口设备的核心模块,它处理串口设备的基本功能,如数据传输,控制,缓冲管理等。tty_core 层与具体的串口硬件无关,是串口设备的通用处理层。
uart_core 层 :位于 tty_core 层的下方,提供了串口设备的底层驱动接口,负责与具体的串口硬件进行通信。uart_core 层负责控制串口数据的发送和接收、中断处理、时钟管理等底层操作。
硬件层 :位于最底层,是串口子系统中与具体硬件有关的部分。硬件层包括了串口硬件的驱动程序,与具体的串口控制器进行通信,实现对硬件的底层控制和操作。
配置串口驱动 在瑞芯微官方提供的 SDK 内核源码里面,串口驱动使用的是 8250 通用串口驱动,以下是主要驱动文件
drivers/tty/serial/8250/8250_core.c 8250 串口驱动核心
drivers/tty/serial/8250/8250_dw.c Synopsis DesignWare 8250 串口驱动
drivers/tty/serial/8250/8250_dma.c 8250 串口 DMA 驱动
drivers/tty/serial/8250/8250_port.c 8250 串口端口操作
drivers/tty/serial/8250/8250_early.c 8250 串口 early console 驱动
make menuconfig中选中驱动
1 2 Device Driver / Character devices / Serial drivers [*] Console on 8250 /16550 and compatible serial port
迅为提供的 SDK 源码设备树中默认打开了串口 9,打开arch/arm64/boot/dts/rockchip/rk3568.dtsi设备树文件,串口 9 控制器的设备树节点如下所示:
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:指定设备的兼容性字符串,表示此串口设备兼容于 rockchip,rk3568-uart 和snps,dw-apb-uart 两种串口控制器。这有助于设备树绑定相应的驱动程序。
reg:指定串口设备的地址和大小,0xfe6d0000 是串口设备的基地址,0x100 表示地址空间的大小。
interrupts:指定串口设备的中断信息,包括中断类型和中断号。
clocks:指定串口设备所使用的时钟源,包括波特率时钟和 APB 时钟。
clock-names:指定时钟源的名称,用于与时钟源的具体配置相匹配。
reg-shift:表示地址偏移量的位数,即每个寄存器的偏移量是按字节还是按字(byte)。
reg-io-width:表示对设备地址和数据的访问宽度,这里是 4 表示 4 字节宽度。
dmas:指定串口设备使用的 DMA 控制器以及 DMA 通道号,用于数据传输的 DMA 操作。
dma-names:可选值为
tx打开 tx dma
rx打开 rx dma
!tx关闭 tx dma
!rx关闭 rx dma
pinctrl-names 和 pinctrl-0:用于串口设备的管脚控制,以配置和管理串口设备的引脚设置。可选参数为如下所示:
&uart9m0_xfer 配置 tx 和 rx 引脚为 iomux group 0
&uart9m1_xfer 配置 tx 和 rx 引脚为 iomux group 1
&uart9m0_ctsn 和&uart1m0_rtsn 配置硬件自动流控 cts 和 rts 引脚为 iomux group 0
&uart9m1_ctsn和&uart1m1_rtsn 配置硬件自动流控 cts 和 rts 引脚为 iomux group 1
status:表示串口设备的状态,这里是 “disabled” 表示设备当前处于禁用状态。如果设置为“okay”表示打开设备
uart_driver 注册流程分析 uart 相关底层结构体 uart 相关底层结构体之间的关系如下所示
struct uart_driver uart_driver 结构体代 表 UART 驱动,uart_driver 定义在 include/linux/serial_core.h 文件中,内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 struct uart_driver { struct module *owner ; const char *driver_name; const char *dev_name; int major; int minor; int nr; struct console *cons ; struct uart_state *state ; struct tty_driver *tty_driver ; };
struct uart_driver 封装了 tty_driver,使得底层的 UART 驱动不需要关心 tty_driver。
struct uart_port uart_port 是针对一个串口的抽象,定义在 kernel/include/linux/serial_core.h,其内部包含一个 uart_state 类型的成员变量,内容如下:
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; unsigned long iobase; unsigned char __iomem *membase; 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; unsigned long irqflags; unsigned int uartclk; unsigned int fifosize; unsigned char x_char; unsigned char regshift; unsigned char iotype; unsigned char quirks; #define UPIO_PORT (SERIAL_IO_PORT) #define UPIO_HUB6 (SERIAL_IO_HUB6) #define UPIO_MEM (SERIAL_IO_MEM) #define UPIO_MEM32 (SERIAL_IO_MEM32) #define UPIO_AU (SERIAL_IO_AU) #define UPIO_TSI (SERIAL_IO_TSI) #define UPIO_MEM32BE (SERIAL_IO_MEM32BE) #define UPIO_MEM16 (SERIAL_IO_MEM16) #define UPQ_NO_TXEN_TEST BIT(0) unsigned int read_status_mask; unsigned int ignore_status_mask; struct uart_state *state ; struct uart_icount icount ; struct console *cons ; upf_t flags; #define UPF_FOURPORT ((__force upf_t) ASYNC_FOURPORT ) #define UPF_SAK ((__force upf_t) ASYNC_SAK ) #define UPF_SPD_HI ((__force upf_t) ASYNC_SPD_HI ) #define UPF_SPD_VHI ((__force upf_t) ASYNC_SPD_VHI ) #define UPF_SPD_CUST ((__force upf_t) ASYNC_SPD_CUST ) #define UPF_SPD_WARP ((__force upf_t) ASYNC_SPD_WARP ) #define UPF_SPD_MASK ((__force upf_t) ASYNC_SPD_MASK ) #define UPF_SKIP_TEST ((__force upf_t) ASYNC_SKIP_TEST ) #define UPF_AUTO_IRQ ((__force upf_t) ASYNC_AUTO_IRQ ) #define UPF_HARDPPS_CD ((__force upf_t) ASYNC_HARDPPS_CD ) #define UPF_SPD_SHI ((__force upf_t) ASYNC_SPD_SHI ) #define UPF_LOW_LATENCY ((__force upf_t) ASYNC_LOW_LATENCY ) #define UPF_BUGGY_UART ((__force upf_t) ASYNC_BUGGY_UART ) #define UPF_MAGIC_MULTIPLIER ((__force upf_t) ASYNC_MAGIC_MULTIPLIER ) #define UPF_NO_THRE_TEST ((__force upf_t) (1 << 19)) #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)) #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)) #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 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; unsigned int mctrl; unsigned int timeout; unsigned int type; const struct uart_ops *ops ; unsigned int custom_divisor; unsigned int line; unsigned int minor; resource_size_t mapbase; resource_size_t mapsize; struct device *dev ; unsigned long sysrq; unsigned int sysrq_ch; unsigned char has_sysrq; unsigned char sysrq_seq; unsigned char hub6; unsigned char suspended; unsigned char console_reinit; const char *name; struct attribute_group *attr_group ; const struct attribute_group **tty_groups ; struct serial_rs485 rs485 ; struct gpio_desc *rs485_term_gpio ; struct serial_iso7816 iso7816 ; void *private_data; };
struct uart_state struct uart_state 是一个结构体,定义在 include/linux/serial_core.h,其内部包含一个 tty_port 类型的成员变量,通常用于表示UART 驱动程序的状态信息。通过 uart_driver 结构体中的 state 成员指针,可以访问和操作与UART 设备状态有关的数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 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 结构体 uart_ops 包含了一系列函数指针,这些函数指针定义了对 UART 端口进行操作的接口。每个函数指针对应一个特定的操作,例如发送数据、设置控制信号、启动或停止传输等。
通过 uart_ops 结构体,上层应用程序或驱动程序可以调用这些函数指针来操作 UART 端口,实现数据传输和控制操作。
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 struct uart_ops { unsigned int (*tx_empty) (struct uart_port *) ; void (*set_mctrl)(struct uart_port *, unsigned int mctrl); unsigned int (*get_mctrl) (struct uart_port *) ; void (*stop_tx)(struct uart_port *); void (*start_tx)(struct uart_port *); void (*throttle)(struct uart_port *); void (*unthrottle)(struct uart_port *); void (*send_xchar)(struct uart_port *, char ch); void (*stop_rx)(struct uart_port *); void (*enable_ms)(struct uart_port *); void (*break_ctl)(struct uart_port *, int ctl); int (*startup)(struct uart_port *); void (*shutdown)(struct uart_port *); void (*flush_buffer)(struct uart_port *); void (*set_termios)(struct uart_port *, struct ktermios *new, struct ktermios *old); void (*set_ldisc)(struct uart_port *, struct ktermios *); void (*pm)(struct uart_port *, unsigned int state, unsigned int oldstate); const char *(*type)(struct uart_port *); void (*release_port)(struct uart_port *); int (*request_port)(struct uart_port *); void (*config_port)(struct uart_port *, int ); int (*verify_port)(struct uart_port *, struct serial_struct *); int (*ioctl)(struct uart_port *, unsigned int , unsigned long ); #ifdef CONFIG_CONSOLE_POLL int (*poll_init)(struct uart_port *); void (*poll_put_char)(struct uart_port *, unsigned char ); int (*poll_get_char)(struct uart_port *); #endif };
uart_driver 注册分析 drivers/tty/serial/8250/8250_core.c 文件中的函数主要是与 8250 系列 UART 驱动程序的核心功能相关的。这个文件实现了 8250 这种串行通信设备的核心操作,包括初始化、配置、中断处理、数据传输等功能。
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 static int __init serial8250_init (void ) { int ret; if (nr_uarts == 0 ) return -ENODEV; serial8250_isa_init_ports(); pr_info("Serial: 8250/16550 driver, %d ports, IRQ sharing %sabled\n" , nr_uarts, share_irqs ? "en" : "dis" ); #ifdef CONFIG_SPARC ret = sunserial_register_minors(&serial8250_reg, UART_NR); #else serial8250_reg.nr = UART_NR; ret = uart_register_driver(&serial8250_reg); #endif if (ret) goto out; ret = serial8250_pnp_init(); if (ret) goto unreg_uart_drv; serial8250_isa_devs = platform_device_alloc("serial8250" , PLAT8250_DEV_LEGACY); if (!serial8250_isa_devs) { ret = -ENOMEM; goto unreg_pnp; } ret = platform_device_add(serial8250_isa_devs); if (ret) goto put_dev; serial8250_register_ports(&serial8250_reg, &serial8250_isa_devs->dev); ret = platform_driver_register(&serial8250_isa_driver); if (ret == 0 ) goto out; platform_device_del(serial8250_isa_devs); put_dev: platform_device_put(serial8250_isa_devs); unreg_pnp: serial8250_pnp_exit(); unreg_uart_drv: #ifdef CONFIG_SPARC sunserial_unregister_minors(&serial8250_reg, UART_NR); #else uart_unregister_driver(&serial8250_reg); #endif out: return ret; }
初始化8250串口 serial8250_isa_init_ports() serial8250_init(void)函数主要负责在系统启动时初始化和注册 8250/16550 串口设备驱动,其中第 10 行的 serial8250_isa_init_ports();函数初始化 8250 串口,函数如下所示:
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 ; static int first = 1 ; int i, irqflag = 0 ; if (!first) return ; first = 0 ; if (nr_uarts > UART_NR) nr_uarts = UART_NR; for (i = 0 ; i < nr_uarts; i++) { struct uart_8250_port *up = &serial8250_ports[i]; struct uart_port *port = &up->port; port->line = i; serial8250_init_port(up); if (!base_ops) base_ops = port->ops; port->ops = &univ8250_port_ops; timer_setup(&up->timer, serial8250_timeout, 0 ); up->ops = &univ8250_driver_ops; up->mcr_mask = ~ALPHA_KLUDGE_MCR; up->mcr_force = ALPHA_KLUDGE_MCR; serial8250_set_defaults(up); } univ8250_port_ops = *base_ops; univ8250_rsa_support(&univ8250_port_ops); if (share_irqs) irqflag = IRQF_SHARED; for (i = 0 , up = serial8250_ports; i < ARRAY_SIZE(old_serial_port) && i < nr_uarts; i++, up++) { struct uart_port *port = &up->port; port->iobase = old_serial_port[i].port; port->irq = irq_canonicalize(old_serial_port[i].irq); port->irqflags = 0 ; port->uartclk = old_serial_port[i].baud_base * 16 ; port->flags = old_serial_port[i].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; if (serial8250_isa_config != NULL ) serial8250_isa_config(i, &up->port, &up->capabilities); } }
serial8250_init_port() 在上面的第 21 行代码执行了 serial8250_init_port 函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 void serial8250_init_port (struct uart_8250_port *up) { struct uart_port *port = &up->port; spin_lock_init(&port->lock); port->pm = NULL ; port->ops = &serial8250_pops; port->has_sysrq = IS_ENABLED(CONFIG_SERIAL_8250_CONSOLE); up->cur_iotype = 0xFF ; } EXPORT_SYMBOL_GPL(serial8250_init_port);
serial8250_pop如下:
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, .set_mctrl = serial8250_set_mctrl, .get_mctrl = serial8250_get_mctrl, .stop_tx = serial8250_stop_tx, .start_tx = serial8250_start_tx, .throttle = serial8250_throttle, .unthrottle = serial8250_unthrottle, .stop_rx = serial8250_stop_rx, .enable_ms = serial8250_enable_ms, .break_ctl = serial8250_break_ctl, .startup = serial8250_startup, .shutdown = serial8250_shutdown, .flush_buffer = serial8250_flush_buffer, .set_termios = serial8250_set_termios, .set_ldisc = serial8250_set_ldisc, .pm = serial8250_pm, .type = serial8250_type, .release_port = serial8250_release_port, .request_port = serial8250_request_port, .config_port = serial8250_config_port, .verify_port = serial8250_verify_port, #ifdef CONFIG_CONSOLE_POLL .poll_get_char = serial8250_get_poll_char, .poll_put_char = serial8250_put_poll_char, #endif };
注册 UART 驱动程序 serial8250_init(void)函数主要负责在系统启动时初始化和注册 8250/16550 串口设备驱动,其中的 uart_register_driver(&serial8250_reg),通过 uart_register_driver 函数向系统注册这个 uart_driver,函数原型如下
1 int uart_register_driver (struct uart_driver *uart)
函数参数和返回值含义如下所示:
uart :要注册的 uart_driver。
返回值 :0,成功;负值,失败。
注销驱动的时候也需要注销掉前面注册的 uart_driver,需要用到 uart_unregister_driver函数原型如下:
1 void uart_unregister_driver (struct uart_driver *uart)
函数参数和返回值含义如下所示:
uart :要注销的 uart_driver。
返回值 :无
serial8250_init(void)函数中 uart_register_driver 函数传入的参数是 serial8250_reg
1 2 3 4 5 6 7 8 static struct uart_driver serial8250_reg = { .owner = THIS_MODULE, .driver_name = "serial" , .dev_name = "ttyS" , .major = TTY_MAJOR, .minor = 64 , .cons = SERIAL8250_CONSOLE, };
uart_register_driver() uart_register_driver 函数内容如下所示,定义在在源码 drivers/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 int uart_register_driver (struct uart_driver *drv) { struct tty_driver *normal ; int i, retval = -ENOMEM; BUG_ON(drv->state); drv->state = kcalloc(drv->nr, sizeof (struct uart_state), GFP_KERNEL); if (!drv->state) goto out; normal = alloc_tty_driver(drv->nr); if (!normal) goto out_kfree; drv->tty_driver = normal; 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); for (i = 0 ; i < drv->nr; i++) { 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); if (retval >= 0 ) return retval; 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_driver 函数中的第 48 行代码设置 tty 驱动器的操作函数,这些操作函数定义了与特定设备相关的操作,如读写、控制等
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, .open = uart_open, .close = uart_close, .write = uart_write, .put_char = uart_put_char, .flush_chars = uart_flush_chars, .write_room = uart_write_room, .chars_in_buffer= uart_chars_in_buffer, .flush_buffer = uart_flush_buffer, .ioctl = uart_ioctl, .throttle = uart_throttle, .unthrottle = uart_unthrottle, .send_xchar = uart_send_xchar, .set_termios = uart_set_termios, .set_ldisc = uart_set_ldisc, .stop = uart_stop, .start = uart_start, .hangup = uart_hangup, .break_ctl = uart_break_ctl, .wait_until_sent= uart_wait_until_sent, #ifdef CONFIG_PROC_FS .proc_show = uart_proc_show, #endif .tiocmget = uart_tiocmget, .tiocmset = uart_tiocmset, .set_serial = uart_set_info_user, .get_serial = uart_get_info_user, .get_icount = uart_get_icount, #ifdef CONFIG_CONSOLE_POLL .poll_init = uart_poll_init, .poll_get_char = uart_poll_get_char, .poll_put_char = uart_poll_put_char, #endif };
uart_register_driver 函 数 中 的 第 61 行 代 码调用 tty_register_driver 函 数 如 下 所 示 , 定 义 在drivers/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 int tty_register_driver (struct tty_driver *driver) { int error; int i; dev_t dev; struct device *d ; if (!driver->major) { 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 (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); list_add(&driver->tty_drivers, &tty_drivers); mutex_unlock(&tty_mutex); 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; } } } proc_tty_register_driver(driver); driver->flags |= TTY_DRIVER_INSTALLED; return 0 ; err_unreg_devs: for (i--; i >= 0 ; i--) tty_unregister_device(driver, i); mutex_lock(&tty_mutex); list_del(&driver->tty_drivers); mutex_unlock(&tty_mutex); err_unreg_char: unregister_chrdev_region(dev, driver->num); err: return error; } EXPORT_SYMBOL(tty_register_driver);
分配平台设备结构体并注册 serial8250_init(void)函数主要负责在系统启动时初始化和注册 8250/16550 串口设备驱动:
1 2 3 4 5 6 7 8 9 10 11 serial8250_isa_devs = platform_device_alloc("serial8250" , PLAT8250_DEV_LEGACY); if (!serial8250_isa_devs) { ret = -ENOMEM; goto unreg_pnp; } ret = platform_device_add(serial8250_isa_devs); if (ret) goto put_dev;
platform_device_alloc 函数用于分配一个平台设备结构体,但不会将其注册到平台总线上 。返回的结构体可以在之后使用函数 platform_device_add()来将其注册到平台总线上。
注册串口端口 serial8250_init(void)函数主要负责在系统启动时初始化和注册 8250/16550 串口设备驱动,其中第 42 行代码,如下所示:
1 2 serial8250_register_ports(&serial8250_reg, &serial8250_isa_devs->dev);
serial8250_register_ports() serial8250_register_ports 函数内容如下所示:
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 __initserial8250_register_ports (struct uart_driver *drv, struct device *dev) { int i; for (i = 0 ; i < nr_uarts; i++) { struct uart_8250_port *up = &serial8250_ports[i]; if (up->port.type == PORT_8250_CIR) continue ; if (up->port.dev) continue ; up->port.dev = dev; if (uart_console_enabled(&up->port)) pm_runtime_get_sync(up->port.dev); serial8250_apply_quirks(up); uart_add_one_port(drv, &up->port); } }
serial8250_init(void)函数主要负责在系统启动时初始化和注册 8250/16550 串口设备驱动,其中第 44 行代码,如下所示
1 2 ret = platform_driver_register(&serial8250_isa_driver);
端口注册流程分析 迅为提供的 SDK 源码设备树中默认打开了串口 9,打开arch/arm64/boot/dts/rockchip/rk3568.dtsi 设备树文件,串口 9 控制器的设备树节点如下所示:
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" ; }
第 2 行 compatible 属性值为snps,dw-apb-uart。在 Linux 源码中搜索这个值即可找到对应的 UART 驱动文件,此文件为 drivers/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);
可以看出瑞芯微的 UART 本质上是一个 platform 驱动,当节点匹配成功之后,执行dw_probe 函数,函数内容如下所示:
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; struct resource *regs = platform_get_resource(pdev, IORESOURCE_MEM, 0 ); struct uart_port *p = &up->port; struct device *dev = &pdev->dev; struct dw8250_data *data ; int irq; int err; u32 val; if (!regs) { dev_err(dev, "no registers defined\n" ); return -EINVAL; } irq = platform_get_irq(pdev, 0 ); if (irq < 0 ) return irq; spin_lock_init(&p->lock); p->mapbase = regs->start; p->irq = irq; p->handle_irq = dw8250_handle_irq; p->pm = dw8250_do_pm; p->type = PORT_8250; p->flags = UPF_SHARE_IRQ | UPF_FIXED_PORT; p->dev = dev; p->iotype = UPIO_MEM; p->serial_in = dw8250_serial_in; p->serial_out = dw8250_serial_out; p->set_ldisc = dw8250_set_ldisc; p->set_termios = dw8250_set_termios; p->membase = devm_ioremap(dev, regs->start, resource_size(regs)); if (!p->membase) return -ENOMEM; data = devm_kzalloc(dev, sizeof (*data), GFP_KERNEL); if (!data) return -ENOMEM; data->data.dma.fn = dw8250_fallback_dma_filter; data->usr_reg = DW_UART_USR; p->private_data = &data->data; data->uart_16550_compatible = device_property_read_bool(dev, "snps,uart-16550-compatible" ); err = device_property_read_u32(dev, "reg-shift" , &val); if (!err) p->regshift = val; err = device_property_read_u32(dev, "reg-io-width" , &val); if (!err && val == 4 ) { p->iotype = UPIO_MEM32; p->serial_in = dw8250_serial_in32; p->serial_out = dw8250_serial_out32; } if (device_property_read_bool(dev, "dcd-override" )) { data->msr_mask_on |= UART_MSR_DCD; data->msr_mask_off |= UART_MSR_DDCD; } if (device_property_read_bool(dev, "dsr-override" )) { data->msr_mask_on |= UART_MSR_DSR; data->msr_mask_off |= UART_MSR_DDSR; } if (device_property_read_bool(dev, "cts-override" )) { data->msr_mask_on |= UART_MSR_CTS; data->msr_mask_off |= UART_MSR_DCTS; } if (device_property_read_bool(dev, "ri-override" )) { data->msr_mask_off |= UART_MSR_RI; data->msr_mask_off |= UART_MSR_TERI; } device_property_read_u32(dev, "clock-frequency" , &p->uartclk); data->clk = devm_clk_get_optional(dev, "baudclk" ); if (data->clk == NULL ) 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 (!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); dw8250_quirks(p, data); if (data->uart_16550_compatible) p->handle_irq = NULL ; if (!data->skip_autocfg) dw8250_setup_port(p); if (p->fifosize) { data->data.dma.rxconf.src_maxburst = p->fifosize / 4 ; data->data.dma.txconf.dst_maxburst = p->fifosize / 4 ; up->dma = &data->data.dma; } data->data.line = serial8250_register_8250_port(up); if (data->data.line < 0 ) { err = data->data.line; goto err_reset; } 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); } platform_set_drvdata(pdev, data); 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; }
其中 159行代码使用 serial8250_register_8250_port 函数注册了 8250 端口,serial8250_register_8250_port 函数内容如下所示:
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 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 ; } 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);
其中 serial8250_find_match_or_unused 函数内容如下所示
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; for (i = 0 ; i < nr_uarts; i++) if (uart_match_port(&serial8250_ports[i].port, port)) return &serial8250_ports[i]; i = port->line; if (i < nr_uarts && serial8250_ports[i].port.type == PORT_UNKNOWN && serial8250_ports[i].port.iobase == 0 ) return &serial8250_ports[i]; 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]; 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()第 33 行 uart_add_one_port() 函数向 tty 核心层注册一个 UART 端口,uart_add_one_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 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; ... 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 函数内容如下所示:
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 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); dev = serdev_tty_port_register(port, device, driver, index); if (PTR_ERR(dev) != -ENODEV) { return dev; } return tty_register_device_attr(driver, index, device, drvdata, attr_grp); } EXPORT_SYMBOL_GPL(tty_port_register_device_attr_serdev);
该函数用于注册一个 tty 设备到 tty 核心层,如果注册的设备是 serdev 设备,则不创建 cdev。
首先,它将 tty 端口链接到设备,然后尝试注册 serdev 设备。如果注册的是 serdev 设备,则直接返回注册结果;否则,通过调用 tty_register_device_attr() 函数创建 cdev 并注册该设备到 tty 核心层。tty_register_device_attr 函数内容如下所示:
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 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 ]; dev_t devt = MKDEV(driver->major, driver->minor_start) + index; struct ktermios *tp ; struct device *dev ; int retval; if (index >= driver->num) { 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) pty_line_name(driver, index, name); else tty_line_name(driver, index, name); dev = kzalloc(sizeof (*dev), GFP_KERNEL); if (!dev) return ERR_PTR(-ENOMEM); dev->devt = devt; dev->class = tty_class; dev->parent = device; dev->release = tty_device_create_release; dev_set_name(dev, "%s" , name); dev->groups = attr_grp; dev_set_drvdata(dev, drvdata); dev_set_uevent_suppress(dev, 1 ); retval = device_register(dev); if (retval) goto err_put; if (!(driver->flags & TTY_DRIVER_DYNAMIC_ALLOC)) { tp = driver->termios[index]; if (tp) { driver->termios[index] = NULL ; kfree(tp); } retval = tty_cdev_add(driver, devt, index, 1 ); if (retval) goto err_del; } dev_set_uevent_suppress(dev, 0 ); kobject_uevent(&dev->kobj, KOBJ_ADD); return dev; err_del: device_del(dev); err_put: put_device(dev); return ERR_PTR(retval); } EXPORT_SYMBOL_GPL(tty_register_device_attr);
该函数用于注册一个 tty 设备到 tty 核心层,包括创建 cdev,并添加属性组。
首先,它会根据驱动类型生成设备名称,并分配设备结构体内存空间,并设置设备的各个属性。
然后,它注册设备到内核,并根据驱动是否动态分配来决定是否添加 cdev 到 tty 核心层。
最后,取消抑制 uevent 并发送 uevent 通知设备已添加,并返回设备结构体指针。
tty_cdev_add() 添加 cdev 到 tty 核心层使用 tty_cdev_add 函数,内容如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 static int tty_cdev_add (struct tty_driver *driver, dev_t dev, unsigned int index, unsigned int count) { int err; driver->cdevs[index] = cdev_alloc(); if (!driver->cdevs[index]) return -ENOMEM; driver->cdevs[index]->ops = &tty_fops; driver->cdevs[index]->owner = driver->owner; err = cdev_add(driver->cdevs[index], dev, count); if (err) kobject_put(&driver->cdevs[index]->kobj); return err; }
该函数用于向 tty 核心层添加 cdev。首先,它分配一个 cdev 结构体,并设置其操作和所有者。然后,它通过调用 cdev_add() 函数将 cdev 添加到内核中。如果添加失败,则释放已分配的资源。
在上面代码第 13 行中,tty_fops 是 tty 驱动中的文件操作结构体,它定义了 tty 设备文件的操作函数。
这些函数包括了对 tty 设备文件进行打开、关闭、读取、写入、控制等操作的实现。一般来说,这些函数会调用相应的 tty 核心层函数来完成对底层 tty 设备的操作。
tty_fops 结构体如下所示:
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, };
当用户空间对 tty 设备文件进行操作时,实际上是在调用对应的 tty_fops 中的操作函数。下面是一个简单的示例,展示了如何在用户空间对 tty 设备文件进行操作
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 ]; fd = open("/dev/ttyS0" , O_RDWR); if (fd < 0 ) { perror("Failed to open tty device" ); return -1 ; } write(fd, "Hello, tty device!" , 18 ); read(fd, buffer, sizeof (buffer)); printf ("Received data: %s\n" , buffer); close(fd); return 0 ; }
串口编程 在 Linux 系统中,可以使用文件 I/O 操作和ioctl 操作来进行串口编程。
串口设备节点 在 Linux 系统中,每个设备都由一个设备节点(device node)来表示,设备节点是与设备相关联的一个文 件,以/dev 目录下的文件形式存在。
串口设备节点通常以 tty 开头,具体命名方式根据串口的类型和数量不同而不同。开发板系统启动之后,使用以下命令打印终端设备节点,如下图所示:
dev/ttyX(X 是一个数字编号,譬如 0、1、2、3 等)设备节点 :tty 是 teletype 的简称,在 Linux 中,/dev/ttyX 代表的都是本地终端, Linux 内核在初始化时所生成的 63 个本地终端,包括/dev/tty1~`/dev/tty63` 一共 63 个本地终端,可以是连接到开发板的 LCD 显示器、键盘和鼠标等。
串口终端设备节点 :从开发板原理图可以了解到,在 iTOP-3568 开发板上有四个串口,分别 UART2、UART4、UART7、UART9,其中 UART2 为串口调试终端,对应的设备节点为/dev/ttyFIQ0,其他三个串口 UART4、UART7、UART9 分别对应/dev/ttyS4、/dev/ttyS7、/dev/ttyS9。
基于 USB 的虚拟串口 :ttyGS0 以及 ttyUSBX(X 是一个数字编号,譬如 0、1、2、3 等)都是 USB 的虚拟串口,其中 ttyGS0 为烧写 usb 虚拟出的串口,在系统启动之后可以在 windows 终端通过“adb shell”命令进入开发板控制台。ttyUSBX 在这里为 4G 模块的虚拟串口。
struct termios 结构体 struct termios 是 Linux 内核中用于描述终端设备(包括串口设备)参数的结构体。
它定义在 <linux/termios.h> 头文件中,包含了多个字段,用于配置和管理终端设备的属性和行为,包括了输入输出波特率、数据位、校验位、停止位等。其定义如下:
1 2 3 4 5 6 7 struct termios { tcflag_t c_iflag; tcflag_t c_oflag; tcflag_t c_cflag; tcflag_t c_lflag; cc_t c_cc[NCCS]; };
下面是 struct termios 结构体的一些重要字段:
tcflag_t c_iflag:该字段包含了输入模式标志,用于配置终端设备的输入行为,如输入控制字符、输入数据处理等。
tcflag_t c_oflag:该字段包含了输出模式标志,用于配置终端设备的输出行为,如输出数据处理、输出控制字符等。
tcflag_t c_cflag:该字段包含了控制模式标志,用于配置终端设备的控制参数,如波特率、数据位数、停止位数、校验位等。
tcflag_t c_lflag:该字段包含了本地模式标志,用于配置终端设备的本地操作以及输入输出行为。
cc_t c_cc[NCCS]:该字段包含了特殊控制字符数组,用于配置终端设备的控制字符,如终端的擦除字符、结束字符、停止字符等。
输入模式 输入模式设置
成员
对应成员的含义
IGNBRK
忽略输入终止条件
BRKINT
当检测到输入终止条件时发送 SIGINT 信号
IGNPAR
忽略帧错误和奇偶校验错误
PARMRK
对奇偶校验错误做出标记
INPCK
对接收到的数据执行奇偶校验
ISTRIP
将所有接收到的数据裁剪为 7 比特位、也就是去除第八位
INLCR
将接收到的 NL(换行符)转换为 CR(回车符)
IGNCR
忽略接收到的 CR(回车符)
ICRNL
将接收到的 CR(回车符)转换为 NL(换行符)
IUCLC
将接收到的大写字符映射为小写字符
IXON
启动输出软件流控
输出模式 输出模式控制输出字符的处理方式,即由应用程序发送出去的字符数据在传递到串口或屏幕之前是如何处理的。可用于 c_oflag 成员的宏如下所示:
成员
对应成员的含义
OPOST
启用输出处理功能,如果不设置该标志则其他标志都被忽略
OLCUC
将输出字符中的大写字符转换成小写字符
ONLCR
将输出中的换行符(NL ‘\n’)转换成回车符(CR ‘\r’)
OCRNL
将输出中的回车符(CR ‘\r’)转换成换行符(NL ‘\n’)
ONOCR
在第 0 列不输出回车符(CR)
ONLRET
不输出回车符
OFILL
发送填充字符以提供延时
OFDEL
如果设置该标志,则表示填充字符为 DEL 字符,否则为 NULL 字符
控制模式 在这个结构体中,最重要的 c_cflag,可以控制模式控制终端设备的硬件特性,譬如对于串口来说,该字段比较重要,可设置串口波特率、数据位、校验位、停止位等硬件特性。通过设置 struct termios 结构中 c_cflag 成员的标志对控制模式进行配置。可用于 c_cflag 成员的标志如下所示:
波特率(Baud Rate)位掩码
常量名称
含义
B0
0 波特率(放弃 DTR)
B1800
1800 波特率
B2400
2400 波特率
B4800
4800 波特率
B9600
9600 波特率
B19200
19200 波特率
B38400
38400 波特率
B57600
57600 波特率
B115200
115200 波特率
数据位(Data Bits)位掩码
常量名称
含义
CS5
5 个数据位
CS6
6 个数据位
CS7
7 个数据位
CS8
8 个数据位
停止位(Stop Bits)位掩码
常量名称
含义
CSTOPB
2 个停止位(不设则是 1 个停止位)
其他控制标志
常量名称
含义
CREAD
接收使能
PARENB
校验位使能
PARODD
使用奇校验而不使用偶校验
HUPCL
最后关闭时挂线(放弃 DTR)
CLOCAL
本地连接(不改变端口所有者)
LOBLK
块作业控制输出
CNET_CTSRTS
硬件流控制使能
本地模式 本地模式用于控制终端的本地数据处理和工作模式。通过设置 struct termios 结构体中c_lflag 成员的标 志对本地模式进行配置。可用于 c_lflag 成员的标志如下所示:
常量名称
含义说明
INPCK
启用奇偶校验功能。当启用时,系统会检查接收到的数据是否有奇偶校验错误。
IGNPAR
忽略奇偶校验错误。即使检测到校验错误,也不丢弃数据或产生异常。
PARMRK
当发生奇偶校验错误时,标记该字符(通常在前面插入 \033 和 \000)。用于调试。
ISTRIP
将所有接收到的数据字节的第8位(最高位)清零,即只保留低7位(用于7位字符集)。
IXON
启动输出软件流控 (XON/XOFF)。当缓冲区满时,发送 XOFF(^S)暂停发送;空闲时发送 XON(^Q)恢复。
IXOFF
启动输入软件流控 。允许设备在接收端主动发送 XON/XOFF 控制字符来控制发送方。
IXANY
允许任意字符(不仅仅是 XON)触发流控重启。默认情况下只有 XON 才能重启传输。
IGNBRK
忽略中断条件(Break Condition)。即忽略线路中断信号(如长时间低电平)。
BRKINT
当检测到中断(Break)时,向进程发送 SIGINT 信号(通常用于中断程序执行)。
INLCR
将输入中的换行符(NL, \n)转换为回车符(CR, \r)。常见于某些旧式终端。
IGNCR
忽略输入中的回车符(CR, \r),不作任何处理。
ICRNL
将输入中的回车符(CR, \r)转换为换行符(NL, \n)。这是常见的行末处理方式。
ICANON
启用“规范模式”(Canonical Mode)。在此模式下,输入按行处理(以换行符结束),支持编辑(如退格、删除)、回显等功能。关闭后进入“原始模式”
特殊控制字符 特殊控制字符是一些字符组合,如 Ctrl+C、Ctrl+Z 等,当用户键入这样的组合键,终端会采取特殊处理方式。struct termios 结构体中 c_cc 数组将各种特殊字符映射到对应的支持函数。每个字符位置(数组下标)由对应的宏定义的,如下所示
常量名
功能
默认键
使用场景
VKILL
删除整行
Ctrl+U
编辑长命令
VEOF
文件结束
Ctrl+D
提交输入或关闭
VEOL
行尾标记
CR
兼容旧终端
VEOL2
第二行尾
LF
多种换行格式
VMIN
最小字符数
-
非规范模式读取控制
VTIME
超时时间
-
非规范模式读取控制
VINTR
中断
Ctrl+C
终止程序
VQUIT
暂停
Ctrl+Z
挂起进程
VERASE
删除字符
Backspace
单字符编辑
常用串口控制函数 tcgetattr() 功能 :获取当前终端(或串口设备)的属性配置。原型 :
1 2 #include <termios.h> int tcgetattr (int fd, struct termios *termios_p) ;
参数 :
fd:文件描述符(如打开的串口 /dev/ttyS0)
termios_p:指向 struct termios 的指针,用于存储当前配置
返回值 :成功返回 0,失败返回 -1
用途 :读取当前串口的波特率、数据位、校验位、流控、输入/输出模式等设置。
通常在修改串口前先调用此函数保存原始配置,以便程序退出时恢复。
tcsetattr() 功能 :设置终端(或串口)的属性配置。原型 :
1 int tcsetattr (int fd, int optional_actions, const struct termios *termios_p) ;
参数 :
fd:文件描述符
optional_actions:指定何时应用新设置,常见值:
TCSANOW:立即生效
TCSADRAIN:等待输出完成后再生效(常用于输出相关设置)
TCSAFLUSH:清空输入/输出缓冲区后立即生效
termios_p:包含新配置的 struct termios 指针
返回值 :成功返回 0,失败返回 -1
用途 :应用新的串口参数(如波特率、8N1 配置、原始模式等)
⚠️ 修改后建议检查返回值,确保设置成功。
cfgetispeed() 和 cfgetospeed() 功能 :分别获取输入(接收)和输出(发送)波特率。原型 :
1 2 speed_t cfgetispeed (const struct termios *termios_p) ;speed_t cfgetospeed (const struct termios *termios_p) ;
返回值 :波特率常量(如 B9600, B115200),不是实际数值(如 9600)
注意 :返回的是 speed_t 类型的掩码值 ,不能直接当作整数使用
实际波特率需通过查表或系统特定方式转换(某些系统提供 cfmakeraw() 或 ioctl 获取真实速率)。
cfsetispeed()和 cfsetospeed() 功能 :分别设置输入和输出波特率。
原型 :
1 2 int cfsetispeed (struct termios *termios_p, speed_t speed) ;int cfsetospeed (struct termios *termios_p, speed_t speed) ;
参数 :
termios_p:指向要修改的 termios 结构
speed:波特率常量(如 B9600, B115200)
返回值 :成功返回 0,失败返回 -1
典型用法 :
1 2 3 4 5 struct termios tty ;tcgetattr(fd, &tty); cfsetospeed(&tty, B115200); cfsetispeed(&tty, B115200); tcsetattr(fd, TCSANOW, &tty);
通常收发波特率设为相同(全双工通信),除非特殊需求。
tcflush() 和 tcflow() tcflush() 功能 :清空终端的输入或输出队列。原型 :
1 int tcflush (int fd, int queue_selector) ;
queue_selector 取值 :
TCIFLUSH:清空输入队列 (已收到但未读取的数据)
TCOFLUSH:清空输出队列 (待发送但未发出的数据)
TCIOFLUSH:同时清空输入和输出队列
用途 :在重新配置串口或重启通信前清除脏数据。
tcflow() 功能 :控制串口的数据流(暂停/恢复传输)。原型 :
1 int tcflow (int fd, int action) ;
action 取值 :
TCOOFF:暂停输出 (发送端停止发送)
TCOON:恢复输出
TCIOFF:暂停输入 (向对端发送 XOFF,要求对方暂停)
TCION:恢复输入 (向对端发送 XON,允许继续发送)
用途 :手动实现软件流控(XON/XOFF),或应对缓冲区溢出。
💡 注意:tcflow() 依赖 IXON/IXOFF 标志是否启用。
串口操作流程 设置串口的波特率 在编写串口应用程序时,设置串口波特率是必须的步骤之一,它决定了数据传输的速率。
在 Linux 中,设置波特率通常使用 cfsetspeed() 函数,cfsetspeed()函数所需头文件和函数原型如下所示:
1 2 3 #include <termios.h> #include <unistd.h> int cfsetspeed (struct termios *termios_p, speed_t speed) ;
它接受一个 struct termios 结构体作为输入参数,同时返回一个整数值,表示操作是否成功。
该函数实际上是对 c_cflag 字段中的波特率进行设置。例如,如果要将波特率设置为115200,可以调用以下代码:
1 2 3 4 5 6 7 struct termios options ;tcgetattr(fd, &options); cfsetspeed(&options, B115200); tcsetattr(fd, TCSANOW, &options);
需要注意以下几点:
需要先打开串口设备文件并且获取串口的属性,包括波特率在内的其他属性。可以使用 tcgetattr() 函数获取属性值,并存储在一个 termios 结构体变量中。
设置波特率时需要调用 cfsetspeed() 函数,并传入一个波特率常量作为参数。常用的波特率常量包括 B9600、B115200 等。这些常量可以在头文件 termios.h 中找到。
设置完成后,需要使用 tcsetattr() 函数将属性值写回到串口设备中。
设置数据位大小 在串口通讯中,数据位指的是每个字符(byte)中实际包含的数据位数。通常情况下,一个字符包含 8 个位(即 8 个 0 或 1),但有时也可能为 7 个位或其他值。
在编写串口应用程序时,需要使用 struct termios 结构体中的 c_cflag 成员变量来设置数据位大小。具体来说,需要将 c_cflag 成员变量中与数据位相关的位清零,然后再根据需要设置相应的值。
清零操作通常使用按位与(&)运算符和位运算中的“非”运算符(~),具体步骤如下:
1 new_cfg.c_cflag &= ~CSIZE;
其中,CSIZE 是一个宏定义,表示数据位的位掩码。宏定义通常定义在 termios.h 头文件中,其取值如下:
1 #define CSIZE 0x00000300
通过清零操作,将数据位相关的位全部置为 0。接下来,可以使用按位或(|)运算符和宏定义来设置具体的数据位数,例如:
此时,CS8 宏定义将会被解释为一个包含 8 位的位掩码,通过按位或运算,将其设置到 c_cflag 成员变量中,从而完成数据位的设置。
设置奇偶校验位 串口的奇偶校验位配置一共涉及到 struct termios 结构体中的两个成员变量:c_cflag 和c_iflag。
首先对于 c_cflag 成员,需要添加 PARENB 标志以使能串口的奇偶校验功能,只有使能奇偶校验功能之后才会对输出数据产生校验位,从而对输入数据进行校验检查;
同时对于c_iflag 成员来说,还需要添加 INPCK 标志,这样才能对接收到的数据执行奇偶校验,代码如下所示:
奇校验使能 1 2 new_cfg.c_cflag |= (PARODD | PARENB); new_cfg.c_iflag |= INPCK;
偶校验使能 1 2 3 new_cfg.c_cflag &= ~PARODD; new_cfg.c_cflag |= PARENB; new_cfg.c_iflag |= INPCK;
无校验 1 2 new_cfg.c_cflag &= ~PARENB; new_cfg.c_iflag &= ~INPCK;
设置停止位 在串口通信中,停止位用于指定每个数据帧的结束位置。
在传输一个完整的数据字节后,通常需要一个或多个停止位,以便接收端能够确定一个数据帧的结束。
停止位的数量通常为 1位或 2 位,其中 1 位停止位被广泛使用,而 2 位停止位则较少使用。
在 Linux 中,通过在 struct termios 结构体中设置 c_cflag 成员变量的 CSTOPB 标志位来控制停止位的数量。当 CSTOPB 为 0 时,仅使用 1 位停止位;当 CSTOPB 为 1 时,则使用 2位停止位。
例如,以下代码将串口的停止位设置为 1 位:
1 new_cfg.c_cflag &= ~CSTOPB;
示例 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> int set_uart (int fd, int speed, int bits, char check, int stop) { struct termios newtio , oldtio ; if (tcgetattr(fd, &oldtio) != 0 ) { printf ("tcgetattr oldtio error\n" ); return -1 ; } bzero(&newtio, sizeof (newtio)); newtio.c_cflag |= CLOCAL | CREAD; newtio.c_cflag &= ~CSIZE; switch (bits) { case 7 : newtio.c_cflag |= CS7; break ; case 8 : newtio.c_cflag |= CS8; break ; } switch (check) { case 'O' : newtio.c_cflag |= PARENB; newtio.c_cflag |= PARODD; newtio.c_iflag |= (INPCK | ISTRIP); break ; case 'E' : newtio.c_cflag |= PARENB; newtio.c_cflag &= ~PARODD; newtio.c_iflag |= (INPCK | ISTRIP); break ; case 'N' : newtio.c_cflag &= ~PARENB; break ; } switch (speed) { case 9600 : cfsetispeed(&newtio, B9600); cfsetospeed(&newtio, B9600); break ; case 115200 : cfsetispeed(&newtio, B115200); cfsetospeed(&newtio, B115200); break ; } switch (stop) { case 1 : newtio.c_cflag &= ~CSTOPB; break ; case 2 : newtio.c_cflag |= CSTOPB; break ; } tcflush(fd, TCIFLUSH); 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; fd = open("/dev/ttyS9" , O_RDWR | O_NOCTTY | O_NDELAY); if (fd < 0 ) { printf ("open error \n" ); return -1 ; } set_uart(fd, 115200 , 8 , 'N' , 1 ); write(fd, argv[1 ], strlen (argv[1 ])); sleep(1 ); count = read(fd, buf, sizeof (buf)); buf[count] = '\0' ; printf ("read message is %s\n" , buf); close(fd); return 0 ; }
GPS模块编程 全球卫星导航系统是一种利用卫星技术为全球用户提供精确时间和位置信息的系统。
定位模块往往和 CPU 通过串口相连,然后 CPU 通过 wifi 蓝牙等其他方式将坐标进行上传。
GPS模块原理图如下
其中 BUF_GPS_RST 接口悬空不接,以下为 20pin 底座连接到 RK3568 开发板 GPIO 接口连接表如下所示:
GPS 模块引脚编号
GPS 模块引脚名称
连接到的开发板引脚编号
连接到的开发板引脚名称
2
BUF_GPS_TXD
8
UART9_RX_M1
3
BUF_GPS_RXD
6
UART9_TX_M1
11
GND
19/20
GND
19
VDD33_A31
2/4
VCC3V3_SYS
GPS 模块连接好之后,天线要放到户外
GPS 数据帧介绍
这里列举的是 GPS NMEA 格式中常见的数据类型,其中包括:
GPRMC (Recommended Minimum Specific GPS/Transit data) :GPRMC 数据类型提供了位置、速度、航向等最基本的 GPS 定位信息。这个数据类型常用于导航系统和船舶自动导航系统。
GPVTG (Track Made Good and Ground Speed) :GPVTG 数据类型提供了航向角和地面速度信息,用于显示船、车等运动物体的导航信息。 它提供了地面航向角和相对地面的速度。
GPGGA (Global Positioning System Fix Data) :GPGGA 数据类型包括了定位解、时间、位置准确度、海拔等位置信息,通常被用于接收机实时显示当前位置。
GPGSA (GPS DOP and Active Satellites) :GPGSA 数据类型包括了 DOP 值和当前卫星定位状态。它用于提供 GPS 卫星测量数据中可用卫星数量、解算出的位置几何因子 DOP 等信息。
GPGSV (GPS Satellites in View) :GPGSV 数据类型提供了现时可视卫星的信息,包括卫星的PRN、仰角、方位角、信号强度等信息。这些信息有助于接收器找到更多卫星并提高位置定位的准确性。
GPGLL (Geographic Position - Latitude/Longitude) :GPGLL 数据类型提供了经度和纬度信息,用于描述接收机的当前经纬度位置。
我们只需要关注 GPRMC 这条信息即可,如下图所示:
GPRMC(Recommended Minimum Specific GNSS Data)是一种常见的 GPS 数据帧格式,用于在 GPS 设备之间或者 GPS 设备与其他设备之间传输 GPS 位置信息。以下是 GPRMC 数据帧中包含的信息:
字段编号
字段值(示例)
含义
详细解释
0
$GPRMC
消息 ID
表示这是“推荐最小定位信息”帧。GP 表示由 GPS 系统提供;其他系统如 GLONASS 用 GL,北斗用 BD 或 GB。
1
083634.00
UTC 时间
格式:hhmmss.sss 表示 08:36:34.000 UTC → 北京时间 = UTC + 8 小时 → 16:36:34(下午 4 点 36 分)
2
A
状态指示
- A:有效定位 (Active / Valid) - V:无效定位 (Void / Invalid),可能因信号弱或未锁定卫星
3
3854.62194
纬度(数值)
格式:ddmm.mmmm 即 38°54.62194′ 转换为十进制度: 38 + 54.62194/60 ≈ 38.910366° N
4
N
纬度半球
- N:北纬(North) - S:南纬(South)
5
11526.10876
经度(数值)
格式:dddmm.mmmm 即 115°26.10876′ 转换为十进制度: 115 + 26.10876/60 ≈ 115.435146° E
6
E
经度半球
- E:东经(East) - W:西经(West)
7
0.932
地面速率(节)
单位:knots(节) 1 节 = 1 海里/小时 ≈ 1.852 km/h → 0.932 × 1.852 ≈ 1.73 km/h(步行速度)
8
55.00
地面航向
单位:度(°) ,以真北为 0° 范围:0.0 ~ 359.9° 55° 表示东北方向
9
200624
UTC 日期
格式:ddmmyy → 2024 年 6 月 20 日
10
(空)
磁偏角
可选字段,表示磁北与真北之间的夹角(单位:度)
11
(空)
磁偏角方向
E 或 W,配合字段 10 使用
12
(空)
定位模式
- A:自主定位(Autonomous) - D:差分定位(DGPS) - E:估算(Dead Reckoning) - N:无定位 (注:部分模块在此字段输出,有些放在 $GPGSA 中)
13
D*50
校验和
从 $ 后第一个字符到 * 前所有字符做 异或(XOR)校验 用于验证数据完整性
示例 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__ struct gprmc_data { char id; int time; char state; float latitude; char NS; float longitude; char EW; float speed; int date; char mode; char check; }; extern int set_uart (int fd, int speed, int bits, char check, int stop) ;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" void get_gps_data (char *buff, struct gprmc_data *gps_data) { char *p=NULL ; p = strstr (buff, "$GPRMC" ); if (p == NULL ) { return ; } 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)); 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> int set_uart (int fd, int speed, int bits, char check, int stop) { struct termios newtio , oldtio ; if (tcgetattr(fd, &oldtio) != 0 ) { printf ("tcgetattr oldtio error\n" ); return -1 ; } bzero(&newtio, sizeof (newtio)); newtio.c_cflag |= CLOCAL | CREAD; newtio.c_cflag &= ~CSIZE; switch (bits) { case 7 : newtio.c_cflag |= CS7; break ; case 8 : newtio.c_cflag |= CS8; break ; } switch (check) { case 'O' : newtio.c_cflag |= PARENB; newtio.c_cflag |= PARODD; newtio.c_iflag |= (INPCK | ISTRIP); break ; case 'E' : newtio.c_cflag |= PARENB; newtio.c_cflag &= ~PARODD; newtio.c_iflag |= (INPCK | ISTRIP); break ; case 'N' : newtio.c_cflag &= ~PARENB; break ; } switch (speed) { case 9600 : cfsetispeed(&newtio, B9600); cfsetospeed(&newtio, B9600); break ; case 115200 : cfsetispeed(&newtio, B115200); cfsetospeed(&newtio, B115200); break ; } switch (stop) { case 1 : newtio.c_cflag &= ~CSTOPB; break ; case 2 : newtio.c_cflag |= CSTOPB; break ; } tcflush(fd, TCIFLUSH); if (tcsetattr(fd, TCSANOW, &newtio) != 0 ) { printf ("tcsetattr newtio error\n" ); return -2 ; } return 0 ; }