☰
树莓派4 UART多串口配置与通信稳定性实战指南
2026/10/15 18:00:06 网站建设 项目流程

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

很多人第一次在树莓派 4 上接串口设备时,都会遇到一个很迷惑的现象:明明代码写得没问题,接线也反复检查过,但串口就是收不到数据,或者发出去的数据对方完全没反应。更让人抓狂的是,同样的代码在树莓派 3 上跑得好好的,换到树莓派 4 就不行了。这不是你的问题,而是树莓派 4 在 UART 的硬件分配和默认配置上做了比较大的调整。

树莓派 4 相比前代,最大的变化是蓝牙和无线模块占用了原本分配给硬件串口的 PL011 UART,导致 40 针 GPIO 上引出的串口默认变成了一个性能较弱的 mini UART。这个 mini UART 的时钟源依赖于核心频率,一旦 CPU 频率发生变化,波特率就会跟着漂移,通信自然就不稳定。所以,树莓派 4 UART 多串口配置通信这件事,核心要解决的就是两个问题:第一,把稳定可靠的硬件串口重新映射到 GPIO 上;第二,在此基础上扩展出多个可用的串口通道,满足同时连接多个外设的需求。

这篇文章适合三类人看:一是刚接触树莓派、想用串口连接传感器或单片机的初学者;二是已经在用树莓派做项目、但被串口不稳定问题困扰的开发者;三是需要在一台树莓派上同时管理多个串口设备、做数据汇聚或协议转换的进阶玩家。我会从底层机制讲起,把设备树覆盖、串口映射、多串口扩展、实际通信测试这几个环节全部拆开,配上可以直接复制的配置和命令。你不需要有很深的 Linux 内核知识,但需要会用基本的终端命令和编辑配置文件。

在开始之前,先明确一个概念:树莓派 4 上可用的 UART 不止一个。除了 40 针引脚上引出的那组,芯片内部还有额外的 UART 控制器,可以通过设备树覆盖的方式启用并映射到其他 GPIO 引脚上。这就是实现“多串口”的硬件基础。理解了这一点,后面的配置思路就会清晰很多。

2. 树莓派 4 的 UART 硬件底子与默认映射的坑

2.1 PL011 和 mini UART 的本质区别

树莓派 4 的 SoC 里其实有两个 UART 控制器:一个是PL011,这是 ARM 平台上的标准高性能串口控制器,有独立的时钟源,波特率稳定,支持硬件流控,FIFO 缓冲区也更大;另一个是mini UART,它的设计初衷是作为辅助串口,时钟直接来源于核心时钟,没有独立晶振,所以当 CPU 频率动态调整时,它的波特率会跟着变。

在树莓派 3 及更早的型号上,PL011 默认映射到 40 针 GPIO 的 14/15 脚(也就是物理引脚 8 和 10),蓝牙走的是 mini UART 或者通过其他方式连接。但到了树莓派 4,蓝牙模块被接到了 PL011 上,GPIO 上的串口就变成了 mini UART。这个改动直接导致了很多从旧型号迁移过来的项目出现通信不稳定、丢包、乱码等问题。

你可以用一条命令快速确认当前 GPIO 串口到底用的是哪个控制器:

ls -l /dev/serial*

如果看到/dev/serial0指向的是ttyS0,那基本就是 mini UART;如果指向ttyAMA0,那就是 PL011。更准确的方法是查看/proc/device-tree下的节点信息,或者直接看dmesg里串口注册的日志。

2.2 为什么 mini UART 在树莓派 4 上问题特别明显

mini UART 的波特率计算公式是:波特率 = 核心时钟频率 / (8 × (波特率寄存器值 + 1))。核心时钟频率默认是动态变化的,从 200MHz 到 1.5GHz 都有可能。虽然系统在串口激活时通常会尝试锁定核心频率,但这个机制并不总是可靠,尤其是在系统负载变化大、温度触发降频、或者电源管理介入的时候。

实际表现就是:你用 115200 的波特率通信,大部分时间正常,但偶尔会出现几个字节的乱码,或者在高负载时直接断连。这种间歇性故障最难排查,因为你看代码、看接线都找不出问题。我见过不少人为此换了好几块树莓派,最后才发现是 mini UART 的锅。

所以,把 PL011 重新映射到 GPIO 上,是树莓派 4 串口配置的第一步,也是最重要的一步。这一步不做,后面的多串口配置都是建立在沙滩上的。

2.3 禁用蓝牙控制台对串口的占用

在默认的 Raspberry Pi OS 配置里,PL011 被蓝牙和串口控制台同时占用。串口控制台的作用是让你通过串口登录系统,但对于大多数项目来说,我们不需要这个功能,反而它会干扰正常的串口通信。所以需要先禁用串口控制台,释放 PL011。

具体操作是编辑/boot/config.txt(在较新的系统里可能是/boot/firmware/config.txt),找到或者添加以下行:

enable_uart=1 dtoverlay=disable-bt

enable_uart=1确保串口硬件被启用,dtoverlay=disable-bt则禁用蓝牙对 PL011 的占用。同时,还需要禁用串口控制台服务:

sudo systemctl disable serial-getty@ttyAMA0.service sudo systemctl disable hciuart

做完这些之后重启,再用ls -l /dev/serial*检查,应该就能看到/dev/serial0指向ttyAMA0了。这时候 GPIO 14/15 上的串口就是稳定的 PL011,波特率不会再漂移。

注意:禁用蓝牙后,板载蓝牙功能就不可用了。如果你的项目同时需要蓝牙和稳定串口,就需要考虑用额外的 UART 控制器来扩展,而不是占用 PL011。这一点在后面的多串口方案里会详细讲。

3. 用设备树覆盖启用额外的 UART 通道

3.1 树莓派 4 芯片内部到底有几个 UART

树莓派 4 用的 SoC 内部一共有6 个 UART 控制器,分别叫 uart0 到 uart5。其中 uart0 就是 PL011,uart1 是 mini UART,uart2 到 uart5 是额外的 PL011 兼容控制器。默认情况下,只有 uart0 和 uart1 被启用并映射到引脚上,其余几个处于未激活状态。

这意味着,如果你需要多个串口,完全不需要外接 USB 转串口模块,直接通过设备树覆盖把 uart2、uart3、uart4、uart5 启用并映射到空闲的 GPIO 引脚上就行。这样做的好处是:延迟低、不占用 USB 带宽、不增加额外硬件成本,而且所有串口在系统里都表现为标准的/dev/ttyAMA*设备,编程方式和普通串口完全一致。

不过要注意,每个 UART 需要占用两个 GPIO 引脚(TX 和 RX),如果你还需要硬件流控(RTS/CTS),那就要占用四个引脚。树莓派 4 的 40 针 GPIO 里,真正可以自由复用的引脚数量有限,所以规划引脚分配的时候要提前算好。

3.2 可用的 UART 覆盖层与引脚对应关系

树莓派官方提供了一系列设备树覆盖层,用来启用额外的 UART。常用的有:

覆盖层名称启用的 UART默认 TX 引脚默认 RX 引脚备注
uart2uart2GPIO 0GPIO 1与 ID_EEPROM 冲突,需谨慎
uart3uart3GPIO 4GPIO 5常用组合
uart4uart4GPIO 8GPIO 9常用组合
uart5uart5GPIO 12GPIO 13与 PWM 有冲突

这些覆盖层可以直接写在/boot/config.txt里,格式是dtoverlay=uart2、dtoverlay=uart3这样。加载后重启,系统里就会出现对应的/dev/ttyAMA1、/dev/ttyAMA2等设备。

但这里有个细节:设备树覆盖层加载的顺序会影响设备编号。比如你先加载 uart2 再加载 uart3,和反过来加载,最终生成的/dev/ttyAMA*编号可能不一样。所以在实际项目里,建议固定加载顺序,并且在代码里通过设备树别名或者 udev 规则来绑定设备,而不是硬编码 ttyAMA 编号。

3.3 引脚冲突的排查与规避

树莓派 4 的 GPIO 复用功能非常密集,很多引脚同时承担多种角色。比如 GPIO 12 和 13 默认可能被音频子系统或者 PWM 占用,GPIO 0 和 1 上接了 HAT 的 ID EEPROM。如果你随便选一组引脚就启用 UART,很可能会发现系统启动异常,或者某个原有功能失效。

我的做法是:在规划阶段先列一张表,把项目需要用到的所有外设引脚都写出来,然后对照树莓派的引脚复用表,找出没有冲突的组合。对于大多数项目来说,uart3(GPIO 4/5)和 uart4(GPIO 8/9)是比较安全的选择,因为这两个组合冲突最少,而且引脚位置在物理排针上也比较好接线。

如果你不确定某个引脚当前被什么占用,可以用raspi-gpio get命令查看所有 GPIO 的功能状态:

raspi-gpio get

输出会显示每个引脚当前的功能(INPUT、OUTPUT、ALT0 到 ALT5 等)。ALT 功能就对应着不同的外设映射。通过这个命令,你可以在配置前就发现潜在的冲突。

提示:修改/boot/config.txt后一定要重启才能生效。如果重启后系统起不来,可以把 SD 卡拔下来挂到电脑上,直接编辑 config.txt 把新加的覆盖层注释掉,再插回去启动。这是最稳妥的恢复方式。

4. 多串口通信的实际测试与编程要点

4.1 用命令行工具快速验证每个串口

配置完设备树覆盖并重启后,第一件事是确认所有串口设备都正常出现了:

ls -l /dev/ttyAMA*

你应该能看到 ttyAMA0 以及新增的 ttyAMA1、ttyAMA2 等。接下来用stty命令设置波特率,然后用echo和cat做回环测试。最简单的办法是把同一个串口的 TX 和 RX 用杜邦线短接,然后:

stty -F /dev/ttyAMA1 115200 raw -echo cat /dev/ttyAMA1 & echo "test" > /dev/ttyAMA1

如果终端里回显了 "test",说明这个串口的发送和接收通路都是通的。对每个新增的串口都做一遍这个测试,确保硬件层面没有问题。

对于两个不同串口之间的通信测试,可以把串口 A 的 TX 接到串口 B 的 RX,串口 B 的 TX 接到串口 A 的 RX(交叉接线),然后在一个终端里往 A 发数据,在另一个终端里从 B 读数据。这种测试能验证多串口同时工作的能力。

4.2 Python 下的多串口并发读写

在实际项目里,用 Python 操作串口是最常见的。pyserial库对多串口的支持很好,每个串口实例独立管理自己的缓冲区和超时。下面是一个多串口并发读写的骨架代码:

import serial import threading import time def reader(port, baudrate, name): ser = serial.Serial(port, baudrate, timeout=1) while True: data = ser.read(ser.in_waiting or 1) if data: print(f"[{name}] 收到: {data.hex()}") time.sleep(0.01) ports = [ ("/dev/ttyAMA0", 115200, "UART0"), ("/dev/ttyAMA1", 9600, "UART1"), ("/dev/ttyAMA2", 115200, "UART2"), ] threads = [] for port, baud, name in ports: t = threading.Thread(target=reader, args=(port, baud, name), daemon=True) t.start() threads.append(t) for t in threads: t.join()

这段代码的关键点在于:每个串口跑在独立的线程里,timeout=1保证读操作不会永久阻塞,in_waiting用来判断当前缓冲区里有多少字节可读。实际使用中,你可能还需要加入数据解析、校验、转发等逻辑,但并发读的框架就是这样。

注意:Python 的 GIL 在多线程下确实会限制 CPU 密集型任务的并行度,但串口读写属于 I/O 密集型,GIL 的影响很小。如果串口数量很多(比如超过 6 个),可以考虑用asyncio配合pyserial-asyncio来做异步读写,资源占用会更低。

4.3 波特率、数据位、校验位的匹配问题

多串口场景下,不同外设的串口参数往往不一样。有的传感器是 9600 波特率,有的是 115200;有的用 8N1,有的用 7E1。这些参数必须在打开串口时正确设置,否则收到的就是乱码或者完全没数据。

pyserial的设置方式:

ser = serial.Serial( port="/dev/ttyAMA1", baudrate=9600, bytesize=serial.SEVENBITS, parity=serial.PARITY_EVEN, stopbits=serial.STOPBITS_ONE, timeout=1 )

这里bytesize对应数据位,parity对应校验位,stopbits对应停止位。很多初学者会忽略校验位和停止位的设置,默认用 8N1,结果接上老设备就是不通。我的经验是:拿到任何一个串口设备,第一件事是查它的通信协议文档,把波特率、数据位、校验位、停止位这四个参数确认清楚,再写代码。

另外,树莓派的 PL011 串口对非标准波特率的支持比较好,比如 250000、500000 这类,但 mini UART 就不行。这也是为什么强调要用 PL011 的原因之一。

5. 多串口项目里那些文档不会写的坑

5.1 设备编号漂移与固定绑定

前面提到过,/dev/ttyAMA*的编号并不是永久固定的。如果你改了设备树覆盖的加载顺序,或者系统升级后驱动加载顺序变了,编号就可能变。对于需要长期稳定运行的项目,硬编码/dev/ttyAMA1是有风险的。

解决办法有两种。第一种是用 udev 规则,根据串口的物理地址或者设备树路径创建固定的符号链接。比如:

sudo nano /etc/udev/rules.d/99-serial.rules

在里面写:

KERNEL=="ttyAMA1", SYMLINK+="ttySensorA" KERNEL=="ttyAMA2", SYMLINK+="ttySensorB"

这样代码里就可以用/dev/ttySensorA来打开串口,不管底层编号怎么变,符号链接始终指向正确的设备。

第二种方法是在代码启动时扫描所有可用串口,通过发送探测指令来识别设备。这种方法更灵活,但实现起来复杂一些,适合设备数量多、经常插拔的场景。

5.2 电平匹配与电气隔离

树莓派的 GPIO 是 3.3V 电平,而很多工业设备、老式单片机用的是 5V 或者 RS-232 电平。直接把 5V 的 TX 接到树莓派的 RX 上,短时间可能能用,但长期会损坏 GPIO,甚至烧掉 SoC。RS-232 的正负电压更是绝对不能直接接。

正确的做法是:5V 设备用电平转换模块(比如双向电平转换板或者简单的分压电阻),RS-232 设备用 MAX3232 之类的电平转换芯片,RS-485 设备用 MAX485 模块。如果通信环境有电机、继电器等干扰源,还应该加光耦隔离或者磁耦隔离,否则串口数据会频繁出错。

我踩过的一个坑是:在一个有步进电机的项目里,串口线没有屏蔽,也没有隔离,电机一动串口就丢数据。后来换了带屏蔽的双绞线,加上光耦隔离模块,问题才彻底解决。串口通信的稳定性,一半靠软件配置,一半靠硬件布线。

5.3 缓冲区溢出与流控

当串口数据量大、处理不及时的时候,内核的串口缓冲区会溢出,导致数据丢失。PL011 的硬件 FIFO 只有 16 字节,内核缓冲区默认也不大。如果你用 115200 波特率持续接收数据,而 Python 端的处理逻辑比较慢,就很容易丢包。

解决办法有三个:一是提高读取频率,用独立线程专门读串口,读到数据就放进队列,主线程从队列取数据处理;二是启用硬件流控(RTS/CTS),让接收方在缓冲区快满时通知发送方暂停;三是提高内核串口缓冲区大小,通过setserial或者修改内核参数来调整。

硬件流控需要额外的两根线,而且不是所有设备都支持。在设备支持的前提下,这是最可靠的防丢包手段。在pyserial里启用流控:

ser = serial.Serial( port="/dev/ttyAMA1", baudrate=115200, rtscts=True, timeout=1 )

如果设备不支持流控,那就只能靠软件层面的队列和快速读取来缓解。

5.4 系统负载对串口实时性的影响

树莓派 4 虽然性能不错,但毕竟不是实时系统。当 CPU 负载很高、或者系统在做大量磁盘 I/O 的时候,串口中断的响应可能会延迟,导致数据接收不及时。对于大多数应用来说,这种延迟在毫秒级别,可以接受。但如果你的项目对实时性要求很高,比如做高速数据采集或者精确的时序控制,就需要考虑用实时内核补丁,或者把串口处理逻辑放到独立的核心上(通过taskset绑定 CPU 亲和性)。

另一个实用技巧是:把串口读取线程的优先级调高,用nice或者chrt命令给它更高的调度优先级。这样即使系统繁忙,串口线程也能及时得到调度。

sudo chrt -f 50 python3 serial_reader.py

这条命令让 Python 脚本以 FIFO 实时调度策略运行,优先级 50。注意不要设得太高,否则可能影响系统其他关键进程。

6. 把多串口方案落地到实际项目里的思路

6.1 一个典型的多设备数据汇聚场景

假设你有一个环境监测项目,需要同时连接三种设备:一个气象传感器走 9600 波特率、一个颗粒物传感器走 115200、一个继电器控制板走 19200。三台设备都是串口接口,但参数不同,而且需要同时工作、互不干扰。

用树莓派 4 的多串口方案,可以这样分配:PL011(ttyAMA0)接气象传感器,因为它的数据量不大但对稳定性要求高;uart3(ttyAMA1)接颗粒物传感器,数据量大,需要较高的波特率;uart4(ttyAMA2)接继电器控制板,通信频率低,参数独立。三个串口在系统里独立存在,各自用独立的线程读写,数据汇总到一个主进程做处理和上报。

这种架构的好处是:没有 USB 转串口的兼容性问题,没有 USB 带宽争抢,延迟低且稳定。整个系统只需要一块树莓派和几根杜邦线,成本极低。

6.2 串口数据转发的中间件设计

在多串口项目里,经常需要把一个串口收到的数据转发到另一个串口,或者转发到网络。这时候最好设计一个简单的中间件层,把“数据来源”和“数据去向”解耦。

我的做法是:每个串口对应一个读取线程,读到的数据打上时间戳和来源标识,放进一个线程安全的队列。然后有一个或多个处理线程从队列取数据,根据来源和内容决定转发目标。转发目标可以是另一个串口、一个 MQTT 主题、一个 TCP 连接,或者写入数据库。

这种设计的灵活性在于:增加新的串口设备只需要加一个读取线程和相应的路由规则,不需要改动现有逻辑。对于需要长期维护的项目来说,这种解耦非常重要。

6.3 日志与故障回溯

多串口系统一旦出问题,排查起来比单串口复杂得多。所以从项目一开始就要做好日志记录。每个串口的原始数据、解析后的数据、转发记录、错误信息,都应该带上时间戳和串口标识写入日志文件。

日志文件建议按天分割,避免单个文件过大。可以用 Python 的logging模块配合TimedRotatingFileHandler来实现。另外,对于串口通信错误(比如校验失败、超时、帧错误),要单独记录并统计频率。如果某个串口的错误率突然升高,往往意味着硬件连接松动、干扰增大或者设备本身出了问题。

我习惯在日志里记录每个串口的累计收发字节数和错误计数,通过定期输出这些统计信息,可以提前发现潜在问题,而不是等到系统完全瘫痪才去查。

6.4 开机自启与进程守护

项目部署到现场后,需要保证树莓派开机后串口程序自动运行,并且在程序崩溃时能自动重启。最简单的方式是用 systemd 服务:

[Unit] Description=Serial Multi-Port Service After=multi-user.target [Service] Type=simple ExecStart=/usr/bin/python3 /home/pi/serial_project/main.py Restart=always RestartSec=5 User=pi [Install] WantedBy=multi-user.target

把这个文件保存为/etc/systemd/system/serial-multi.service,然后:

sudo systemctl daemon-reload sudo systemctl enable serial-multi.service sudo systemctl start serial-multi.service

Restart=always保证程序异常退出后 5 秒自动重启。这样即使现场断电恢复,系统也能自动进入工作状态,不需要人工干预。

提示:在 systemd 服务里运行 Python 脚本时,注意环境变量和工作目录的设置。如果脚本里用了相对路径,最好在服务文件里用WorkingDirectory指定工作目录,否则可能找不到文件。

7. 一些关于稳定性的个人经验

串口通信这件事,看起来简单,但要做到长期稳定运行,细节非常多。我做了几个基于树莓派多串口的项目之后,总结出几条比较实用的经验。

第一,能用 PL011 就不要用 mini UART。这是铁律。mini UART 只适合做临时的调试输出,不适合任何正式的数据通信。树莓派 4 上把 PL011 映射到 GPIO 之后,稳定性会有质的提升。

第二,引脚规划要留余量。不要把所有空闲 GPIO 都分配给串口,留几个给指示灯、按键或者未来的扩展。串口引脚一旦确定,后期改动成本很高,因为涉及接线和代码里的设备路径。

第三,测试要覆盖满负载和长时间运行。很多问题在短时间测试里看不出来,跑上几个小时甚至几天才会暴露。我一般会在项目上线前做至少 72 小时的老化测试,让所有串口同时以最大速率收发数据,观察是否有丢包、错误累积或者进程崩溃。

第四,硬件保护不能省。电平转换、隔离、屏蔽、合理的接地,这些看起来是“额外成本”,但比起现场出问题后跑一趟的代价,这点投入非常值得。尤其是工业环境或者户外场景,电气干扰远比实验室里严重。

第五,日志和监控要提前做。不要等到出问题了才想起来加日志。项目一开始就把关键指标记录下来,后面排查问题会轻松很多。哪怕只是简单地记录每个串口的收发计数和错误计数,也能在故障初期给出明确的信号。

这套多串口方案我已经在好几个项目里实际用过,从环境监测到设备控制,从实验室原型到现场部署,整体表现很稳。树莓派 4 的额外 UART 控制器是一个被很多人忽略的资源,把它用起来,可以省掉一堆 USB 转串口模块,让整个系统更简洁、更可靠。

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

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

立即咨询