☰
树莓派4 UART多串口配置与通信实战指南
2026/10/15 21:39:50 网站建设 项目流程

1. 为什么树莓派 4 的 UART 值得单独拿出来聊

树莓派 4 这块板子,但凡你玩过一阵子,大概率都会碰到一个绕不开的需求:接一个串口设备。可能是 GPS 模块、可能是某款工业传感器、可能是和另一块单片机做点对点通信,也可能是调试一个老设备的控制台。这时候你会发现,树莓派 4 的 UART 配置和树莓派 3 相比,变化不小,尤其是那个默认分配给蓝牙的 PL011 硬件串口,坑了不少人。

我自己第一次在树莓派 4 上接串口模块的时候,就是照着树莓派 3 的老教程改的/boot/config.txt,结果折腾了大半天,数据死活收不到。后来才发现,树莓派 4 的 UART 映射逻辑和树莓派 3 不一样,mini UART 和 PL011 的分配关系变了,而且默认情况下 GPIO 14/15 上引出来的那个串口,性能并不理想。

这篇内容就是把我自己在树莓派 4 上做 UART 多串口配置通信的完整过程整理出来。核心要解决的问题有三个:第一,搞清楚树莓派 4 上 UART 的硬件资源到底是怎么分配的;第二,把主串口从蓝牙手里抢回来,让 GPIO 上的串口变成稳定可用的 PL011;第三,在需要多个串口的场景下,通过软件或硬件方式扩展出额外的串口通道,并且保证它们能同时稳定工作。

适合谁看?如果你手上有一块树莓派 4,想接串口设备但被各种教程绕晕了,或者你已经跑通了单个串口,但想同时接两三个串口设备做数据采集和转发,那这篇内容应该能帮你省下不少试错时间。我会尽量把每一步为什么这么做讲清楚,而不是只丢一堆命令让你抄。

2. 树莓派 4 UART 硬件资源与映射关系拆解

2.1 树莓派 4 上到底有几个 UART

先把这个事情说清楚,不然后面配置全是糊涂账。树莓派 4 的 SoC(BCM2711)内部其实有多个 UART 控制器,但在实际使用中,我们最常打交道的是两个:

  • PL011:这是 ARM 体系里标准的硬件串口,性能稳定,波特率精度高,带独立的 FIFO,是真正意义上的“好串口”。
  • mini UART:这是树莓派早期为了省成本搞出来的一个简化版串口,它的波特率依赖于 GPU 核心频率,一旦 CPU 频率发生变化,波特率就会飘,导致通信不稳定。

在树莓派 3 上,PL011 默认是分配给蓝牙的,GPIO 14/15 上引出来的是 mini UART。到了树莓派 4,情况变了:PL011 的分配逻辑做了调整,默认情况下,GPIO 14/15 上引出来的串口变成了 PL011,而蓝牙则改用 mini UART。听起来好像是好事,但问题在于,树莓派 4 默认并没有把 PL011 完全释放给 GPIO,而是通过一个设备树覆盖层(overlay)来管理这个映射关系。

除此之外,树莓派 4 还可以通过额外的 UART 控制器扩展出更多串口。BCM2711 支持最多 6 个 UART(包括 PL011 和几个 mini UART 变体),但这些额外的 UART 需要通过在/boot/config.txt里加载对应的 overlay 才能启用,并且它们会占用特定的 GPIO 引脚。

2.2 为什么默认配置下串口不好用

很多人拿到树莓派 4,直接在 GPIO 14/15 上接串口模块,然后发现要么收不到数据,要么数据乱码,要么波特率稍微高一点就不稳定。原因主要有两个:

第一,默认情况下,树莓派 4 的串口控制台(serial console)是开启的。也就是说,系统会把内核启动日志和登录终端绑定在这个串口上。你接一个外部设备上去,系统会往上面吐一堆启动信息,你的设备如果解析不了这些数据,就会表现得很奇怪。更麻烦的是,如果你往串口发数据,可能会被系统当成登录命令来处理。

第二,虽然树莓派 4 默认把 PL011 分配给了 GPIO 14/15,但蓝牙那边仍然占着一部分资源,而且如果你没有显式地禁用 serial console 并启用 UART overlay,PL011 可能并没有完全按照你期望的方式工作。

我实测下来的经验是:只要你要用 GPIO 上的串口做正经通信,第一步永远是先把 serial console 关掉,然后把 UART 的状态确认清楚,再谈其他。

2.3 多串口扩展的硬件基础

树莓派 4 的 40 针 GPIO 里,默认只有 GPIO 14(TXD)和 GPIO 15(RXD)这一组串口引脚。如果你想接多个串口设备,有几种思路:

第一种是用 USB 转串口模块。这是最省事的办法,插上去就多一个/dev/ttyUSBx,不需要动 GPIO 配置。缺点是每个模块都要占一个 USB 口,而且质量参差不齐的模块容易掉线。

第二种是启用树莓派 4 额外的 UART 控制器。通过加载dtoverlay=uart2、dtoverlay=uart3之类的 overlay,可以把其他 UART 映射到指定的 GPIO 引脚上。这样你就能在不占用 USB 口的情况下,多出几个硬件串口。缺点是可用引脚有限,而且不同 UART 的引脚分配是固定的,需要提前规划好。

第三种是用 I2C 或 SPI 转串口芯片,比如 SC16IS752 这类双通道 UART 扩展芯片。这种方式可以扩展出多个串口,但需要额外的驱动配置,复杂度较高。

我自己的项目里,通常是第一种和第二种混着用:关键设备走硬件 UART,次要设备走 USB 转串口。这样兼顾了稳定性和灵活性。

3. 单串口基础配置:从零到能收发数据

3.1 关闭 serial console 并释放 PL011

不管你后面要不要搞多串口,这一步都是基础。树莓派 4 默认会把串口用作系统控制台,你必须先把这个功能关掉,才能让串口完全归你的应用程序使用。

具体操作分两步。第一步,修改/boot/cmdline.txt,把里面的console=serial0,115200这个参数删掉。这个文件里所有内容都在一行,你只需要找到那个参数,连同前面的空格一起删掉就行。改完之后大概是这个样子:

console=tty1 root=PARTUUID=xxxxxxxx-02 rootfstype=ext4 elevator=deadline fsck.repair=yes rootwait

第二步,关掉串口登录服务。在树莓派 4 上,通常是这两个服务在管这件事:

sudo systemctl stop serial-getty@ttyS0.service sudo systemctl disable serial-getty@ttyS0.service sudo systemctl stop serial-getty@ttyAMA0.service sudo systemctl disable serial-getty@ttyAMA0.service

注意,ttyS0和ttyAMA0这两个设备名在不同配置下会指向不同的 UART。树莓派 4 默认情况下,ttyAMA0对应的是 PL011,ttyS0对应的是 mini UART。你把 serial console 关掉之后,这两个设备就都可以被应用程序打开了。

提示:改完/boot/cmdline.txt之后必须重启才能生效。重启后可以用cat /proc/cmdline确认一下参数是否已经去掉。

3.2 启用 UART 并确认设备节点

接下来要确保 UART 硬件本身是启用的。在/boot/config.txt里,你需要确认或添加这一行:

enable_uart=1

这一行的作用是让固件在启动时初始化 UART 硬件。对于树莓派 4 来说,加上这一行之后,PL011 会被分配到 GPIO 14/15 上,并且系统会创建对应的设备节点。

重启之后,你可以用下面这个命令来确认串口设备是否存在:

ls -l /dev/serial*

正常情况下,你会看到类似这样的输出:

lrwxrwxrwx 1 root root 5 Jan 1 00:00 /dev/serial0 -> ttyAMA0 lrwxrwxrwx 1 root root 5 Jan 1 00:00 /dev/serial1 -> ttyS0

这里serial0指向ttyAMA0,说明 PL011 已经分配给了 GPIO 上的串口。serial1指向ttyS0,那是给蓝牙用的 mini UART。你要用的就是serial0或者直接操作ttyAMA0。

3.3 用 Python 做一次回环测试

配置确认没问题之后,先别急着接外部设备,做一次回环测试最稳妥。所谓回环测试,就是把树莓派的 TXD 和 RXD 用一根杜邦线短接起来,然后发什么就应该收到什么。

硬件上,用一根母对母杜邦线,把 GPIO 14(物理引脚 8)和 GPIO 15(物理引脚 10)连起来。然后跑下面这段 Python 代码:

import serial import time ser = serial.Serial( port='/dev/serial0', baudrate=115200, parity=serial.PARITY_NONE, stopbits=serial.STOPBITS_ONE, bytesize=serial.EIGHTBITS, timeout=1 ) test_data = b'Hello UART' ser.write(test_data) time.sleep(0.1) received = ser.read(len(test_data)) print(f'Sent: {test_data}') print(f'Received: {received}') ser.close()

如果一切正常,你会看到发送和接收的数据完全一致。如果收到的数据是乱码或者为空,那就要回头检查波特率、接线和 serial console 是否真的关掉了。

注意:回环测试用的杜邦线一定要插紧。我遇到过好几次因为接触不良导致数据时有时无的情况,排查了半天以为是软件问题,结果就是线松了。

4. 多串口扩展配置与通信实现

4.1 通过 overlay 启用额外硬件串口

树莓派 4 的 SoC 支持多个 UART,你可以通过在/boot/config.txt里加载 overlay 来启用它们。常用的几个 overlay 和对应的引脚如下:

OverlayTXD 引脚RXD 引脚设备节点
uart2GPIO 4 (pin 7)GPIO 5 (pin 29)/dev/ttyAMA1
uart3GPIO 8 (pin 24)GPIO 9 (pin 21)/dev/ttyAMA2
uart4GPIO 12 (pin 32)GPIO 13 (pin 33)/dev/ttyAMA3
uart5GPIO 16 (pin 36)GPIO 17 (pin 11)/dev/ttyAMA4

要启用 uart2 和 uart3,你只需要在/boot/config.txt末尾加上:

dtoverlay=uart2 dtoverlay=uart3

重启之后,用ls /dev/ttyAMA*就能看到多出来的设备节点。这里有个细节需要注意:启用这些额外 UART 之后,对应的 GPIO 引脚就不能再作为普通 GPIO 使用了,所以在规划引脚的时候要提前留好余量。

另外,这些额外 UART 默认也是 PL011 类型的硬件串口,性能比 mini UART 好得多,波特率可以跑到 921600 甚至更高。我在一个数据采集项目里同时用了 uart2 和 uart3,分别接两个传感器,跑 460800 波特率,连续工作几天都没有丢包。

4.2 用 USB 转串口模块做灵活扩展

如果你不想占用 GPIO 引脚,或者需要更多串口,USB 转串口模块是最直接的选择。市面上常见的芯片有 CH340、CP2102、FT232 等,在树莓派 4 上基本都能免驱识别,插上去就会多出/dev/ttyUSB0、/dev/ttyUSB1这样的设备节点。

不过这里有几个实操经验值得分享。第一,不同批次的 CH340 模块质量差异很大,有些便宜的模块在高温或者长时间工作后会出现掉线,建议关键场景用 CP2102 或 FT232 芯片的模块。第二,多个 USB 转串口模块同时插上去时,设备节点的编号可能会在重启后发生变化,导致你的程序打开错误的串口。解决办法是通过 udev 规则给每个模块绑定固定的别名。

写一条 udev 规则,比如在/etc/udev/rules.d/99-usb-serial.rules里加上:

SUBSYSTEM=="tty", ATTRS{idVendor}=="1a86", ATTRS{idProduct}=="7523", ATTRS{serial}=="USB001", SYMLINK+="ttySensorA" SUBSYSTEM=="tty", ATTRS{idVendor}=="10c4", ATTRS{idProduct}=="ea60", ATTRS{serial}=="USB002", SYMLINK+="ttySensorB"

这样不管系统怎么分配ttyUSBx,你的程序始终可以通过/dev/ttySensorA和/dev/ttySensorB来访问对应的设备。idVendor和idProduct可以用lsusb命令查到,serial属性可以用udevadm info查看。

4.3 多串口并发通信的代码结构

当你同时操作多个串口时,代码结构就很重要了。最简单的做法是开多个线程,每个线程负责一个串口的读写。但这里有个坑:Python 的serial库在多个线程同时读写同一个串口时,可能会出现数据竞争。所以原则是一个串口只由一个线程负责,不同串口之间通过队列来交换数据。

下面是一个简化的多串口采集框架:

import serial import threading import queue import time class SerialReader(threading.Thread): def __init__(self, port, baudrate, data_queue): super().__init__() self.port = port self.baudrate = baudrate self.data_queue = data_queue self.running = True self.ser = None def run(self): self.ser = serial.Serial( port=self.port, baudrate=self.baudrate, timeout=0.1 ) while self.running: if self.ser.in_waiting > 0: data = self.ser.read(self.ser.in_waiting) self.data_queue.put((self.port, data)) time.sleep(0.01) self.ser.close() def stop(self): self.running = False if __name__ == '__main__': data_queue = queue.Queue() readers = [ SerialReader('/dev/ttyAMA0', 115200, data_queue), SerialReader('/dev/ttyAMA1', 460800, data_queue), SerialReader('/dev/ttyUSB0', 9600, data_queue), ] for r in readers: r.start() try: while True: port, data = data_queue.get() print(f'[{port}] {data.hex()}') except KeyboardInterrupt: for r in readers: r.stop() for r in readers: r.join()

这个框架的好处是每个串口独立运行,互不干扰,主线程只需要从队列里取数据做统一处理。如果你要做数据转发,比如把 uart2 收到的数据转发到 uart3,也只需要在队列消费端做判断和转发即可。

提示:in_waiting属性在部分 USB 转串口芯片上可能不准,如果发现数据读取有延迟,可以改用固定长度读取或者基于超时的读取方式。

5. 常见问题排查与避坑经验实录

5.1 串口数据乱码或收不到数据

这是最常见的问题,排查顺序建议如下:

排查项检查方法可能原因
波特率确认双方一致最常见,尤其是非标准波特率
接线TXD 接 RXD,RXD 接 TXD交叉接线,不是直连
地线确认 GND 已连接不共地会导致电平参考错误
serial consolecat /proc/cmdline未关闭会干扰通信
设备节点ls -l /dev/serial*指向了错误的 UART
权限ls -l /dev/ttyAMA0当前用户不在 dialout 组

权限这个问题特别容易被忽略。树莓派上默认用户是pi,但如果你新建了用户或者用sudo跑程序,可能会遇到权限问题。把用户加到dialout组就能解决:

sudo usermod -a -G dialout $USER

改完之后需要重新登录才生效。

5.2 高波特率下数据丢失

如果你把波特率设到 460800 或更高,发现数据偶尔丢失,大概率是以下几个原因:

第一,mini UART 的波特率精度不够。如果你不小心用的是ttyS0(mini UART),高波特率下必然不稳定。确认你用的是ttyAMA0或ttyAMA1这类 PL011 设备。

第二,CPU 负载过高导致串口缓冲区溢出。PL011 虽然有 FIFO,但容量有限。如果你的程序读取不够及时,数据就会丢。解决办法是提高读取线程的优先级,或者用select/poll来监听串口事件,而不是轮询。

第三,USB 转串口模块的芯片性能不足。有些便宜的 CH340 模块在 460800 下就会丢数据,换 CP2102 或 FT232 就好了。

5.3 多个串口同时工作时的资源冲突

当你启用多个硬件 UART 时,最容易碰到的问题是引脚冲突。比如你启用了uart2,它占用了 GPIO 4 和 GPIO 5,但你的项目里正好又需要用 GPIO 4 做其他事情,那就冲突了。所以在项目规划阶段,就要把引脚分配表列清楚。

另一个问题是中断冲突。虽然树莓派 4 的多个 UART 有独立的中断线,但在高负载下,如果多个串口同时高速收发,CPU 的中断处理压力会比较大。我实测下来,同时跑三个 460800 波特率的串口,树莓派 4 的 CPU 占用大概在 15% 到 20% 之间,还算可以接受。但如果你还要跑其他重负载任务,就要考虑降低波特率或者减少同时工作的串口数量。

5.4 设备节点在重启后变化

这个问题主要出现在 USB 转串口模块上。每次重启,系统可能会以不同的顺序枚举 USB 设备,导致/dev/ttyUSB0和/dev/ttyUSB1对调。如果你在代码里硬编码了设备节点,就会打开错误的串口。

解决办法前面提过了,就是用 udev 规则绑定固定别名。这里再补充一个细节:有些便宜的 USB 转串口模块没有唯一的序列号,导致 udev 无法区分。这种情况下,你可以通过 USB 物理端口号来绑定,比如KERNELS=="1-1.2"这样的属性。用udevadm info -a -n /dev/ttyUSB0可以查到完整的属性链。

6. 多串口通信的进阶玩法与性能调优

6.1 用 select 实现非阻塞多路复用

如果你的串口数量不多,而且不想开一堆线程,可以用select或者poll来做单线程多路复用。Python 的select模块可以同时监听多个文件描述符,哪个有数据就处理哪个。这种方式比多线程更轻量,但代码结构会稍微复杂一点。

import serial import select ports = ['/dev/ttyAMA0', '/dev/ttyAMA1', '/dev/ttyUSB0'] sers = [serial.Serial(p, 115200, timeout=0) for p in ports] while True: readable, _, _ = select.select(sers, [], [], 0.1) for ser in readable: data = ser.read(ser.in_waiting) print(f'[{ser.port}] {data.hex()}')

这种方式的优点是资源占用低,缺点是如果某个串口的数据处理逻辑很耗时,会阻塞其他串口的读取。所以适合数据量不大、处理逻辑简单的场景。

6.2 调整串口缓冲区大小

Linux 内核为每个串口维护了发送和接收缓冲区。默认情况下,接收缓冲区可能只有 4KB 左右。如果你短时间内收到大量数据,而应用程序读取不及时,缓冲区就会溢出。你可以通过setserial或者直接写/proc来调整,但更推荐的方式是在打开串口时设置rtscts硬件流控,让硬件来控制数据流。

如果设备支持硬件流控,把rtscts=True加上,能极大减少数据丢失的概率。不过要注意,硬件流控需要额外的两根线(RTS 和 CTS),接线时要一并接好。

6.3 长时间运行稳定性建议

如果你的树莓派要连续跑几天甚至几周做串口通信,有几个经验值得参考:

第一,给串口读取线程加上异常捕获和自动重连机制。USB 转串口模块偶尔会因为电磁干扰或者供电波动而掉线,程序要能检测到SerialException并尝试重新打开串口。

第二,定期检查串口设备是否还在。可以通过os.path.exists('/dev/ttyAMA0')来判断,如果设备消失了,说明硬件出了问题。

第三,日志要写清楚。每个串口的收发数据量、错误次数、重连次数都记录下来,方便出问题的时候回溯。我一般会用 Python 的logging模块,按天切分日志文件,避免单个文件过大。

第四,供电要稳定。树莓派 4 对电源比较敏感,如果同时接了多个 USB 转串口模块,建议用质量好一点的电源适配器,输出电流至少 3A。供电不足会导致 USB 设备频繁掉线,表现就是串口时好时坏。

6.4 一个实际的多串口数据转发场景

最后分享一个我自己做过的场景:树莓派 4 同时接了一个 GPS 模块(走 uart2,9600 波特率)、一个环境传感器(走 uart3,115200 波特率)和一个老式控制器(走 USB 转串口,4800 波特率)。树莓派需要把这三个设备的数据汇总,然后通过 uart0 转发给另一块主控板。

实现思路很简单:三个读取线程分别采集数据,统一放到一个队列里,主线程从队列取数据,加上时间戳和设备标识,打包成固定格式,通过 uart0 发出去。整个系统跑了一周多,没有出现丢包或者死机。关键点在于:每个串口的读取线程独立、队列做缓冲、发送端做流量控制。

这个方案的可扩展性也不错,如果以后要加第四个设备,只需要再加一个读取线程和对应的串口配置就行,主线程的逻辑基本不用动。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询