资讯动态

树莓派4B Ubuntu 22.04串口通信配置与Python编程实战指南

发布时间:2026/8/13 3:17:48 来源:尧图企业网站定制
1. 从一次失败的硬件调试说起去年我接手了一个物联网数据采集项目核心需求是通过树莓派4B实时读取多个工业传感器数据。传感器通过RS-485总线连接最终汇总到一个USB转串口适配器上。听起来很简单对吧我当时也是这么想的。我熟练地给树莓派4B刷上了最新的Ubuntu 22.04 LTS Server镜像插上USB转串口线自信满满地打开终端准备用screen或minicom进行测试。结果ls /dev/tty*之后我懵了——设备列表里确实出现了ttyUSB0但无论我怎么设置波特率、数据位、停止位发送AT指令都石沉大海读取到的永远是一片空白或者偶尔蹦出几个乱码。折腾了大半天从怀疑USB线、怀疑传感器、怀疑电源到最后才意识到问题可能出在系统本身。Ubuntu 22.04尤其是运行在树莓派这种ARM架构上的版本其串口管理与我们熟悉的传统x86 Linux桌面版或树莓派官方的Raspberry Pi OS有着一些微妙但至关重要的差异。这次踩坑经历让我意识到“树莓派4B Ubuntu 22.04”这个组合下的串口通信远不是插上线就能用的。它涉及到底层硬件映射、系统服务管理、权限控制以及驱动兼容性等多个层面任何一个环节没打通通信就会失败。这篇文章就是把我从那次失败调试中总结出来并经过后续多个项目验证的一整套“避坑指南”和“最佳实践”梳理出来。无论你是想用树莓派连接Arduino、STM32单片机、GPS模块、4G模块还是像我一样对接工业PLC或传感器这篇内容都将帮你绕开我踩过的那些坑快速建立起稳定可靠的串口通信链路。我们会从最根本的硬件原理和系统配置讲起一直深入到具体的代码示例和高级调试技巧。2. 理解树莓派4B的串口硬件布局不止一个UART在开始配置之前我们必须先搞清楚树莓派4B的“家底”——它到底有几个串口分别对应什么物理接口以及默认状态下谁被谁占用了。这是所有问题的根源很多通信失败都是因为连错了“门”。树莓派4B的SoC博通BCM2711内部实际上包含了多个UART通用异步收发传输器控制器。对我们来说最主要的是以下两个UART0 (PL011)这是一个功能完整的、高性能的UART通常支持硬件流控RTS/CTS。在树莓派官方的Raspberry Pi OS中这个UART默认被映射到了GPIO的TXD (GPIO14) 和 RXD (GPIO15)引脚也就是那个40针扩展排针上的第8和第10脚。同时这个UART在系统内默认被用作主控制台Serial Console也就是说系统的启动日志和登录终端可以通过它输出和接入。UART2 (miniUART)这是一个由GPU核心管理的、功能相对简单的UART其波特率依赖于核心时钟频率稳定性稍逊于PL011。在树莓派4B上这个miniUART在硬件层面也可以被路由到GPIO的TXD/RXD引脚。那么问题来了当我们安装Ubuntu 22.04 for Raspberry Pi时情况发生了变化。Ubuntu镜像为了系统的稳定性和兼容性通常会采用不同的设备树Device Tree配置。一个常见且关键的区别是在Ubuntu 22.04上默认用于GPIO引脚物理排针串口的可能是UART2 (miniUART)而不是UART0 (PL011)。同时UART0 (PL011)可能被分配给了蓝牙模块使用。我们可以通过一个简单的命令来验证当前系统的串口映射关系dmesg | grep -E \tty(AMA|S)\或者更直接地查看设备树别名ls -l /dev/serial*如果输出显示/dev/serial0 - ttyAMA0且/dev/serial1 - ttyS0那么通常ttyAMA0对应PL011ttyS0对应miniUART。你需要进一步确认哪个对应了物理GPIO引脚。为什么这个区别如此重要稳定性PL011 UART的稳定性和抗干扰能力通常优于miniUART特别是在高波特率如115200以上或长距离通信时。功能PL011支持硬件流控这对于某些需要可靠数据传输的场景如某些4G模块是必需的。默认配置如果你按照网上大部分基于Raspberry Pi OS的教程去操作直接操作/dev/ttyAMA0在Ubuntu下很可能发现设备不存在或者操作无响应因为设备名和映射关系可能已经变了。因此我们的第一步配置往往不是急着去写代码而是先明确我们想要使用的物理串口GPIO引脚在当前的Ubuntu系统下对应的设备文件是哪一个以及它当前被什么功能占用了。3. 配置系统释放GPIO串口并设置权限假设我们的目标很明确我要使用树莓派4B GPIO排针上的TXD (GPIO14) / RXD (GPIO15)引脚与外部设备通信。那么我们需要完成以下几步系统级配置。3.1 步骤一禁用串口控制台服务在Ubuntu上默认可能启用了通过串口登录的控制台服务serial-getty.service。这个服务会独占我们想要使用的串口设备导致我们的应用程序无法打开它。我们需要先禁用它。首先检查该服务是否正在运行以及它绑定到了哪个TTY设备sudo systemctl status serial-getty*如果发现类似serial-gettyttyAMA0.service或serial-gettyttyS0.service的服务是active (running)状态那么它就是我们首先要处理的对象。禁用并停止该服务# 假设服务是 ttyAMA0请根据上一步的实际情况替换 sudo systemctl stop serial-gettyttyAMA0.service sudo systemctl disable serial-gettyttyAMA0.service sudo systemctl mask serial-gettyttyAMA0.service这里用了三个命令stop立即停止服务disable防止下次启动mask是更彻底的禁用会创建一个指向/dev/null的符号链接防止其他服务意外唤醒它。3.2 步骤二修改引导配置切换串口映射可选但推荐如果我们希望将更稳定的PL011 UARTttyAMA0分配给GPIO引脚使用并且确保蓝牙不使用它蓝牙可能改用miniUART或其它方式我们需要修改树莓派的引导配置文件/boot/firmware/config.txt注意在Ubuntu for Raspberry Pi上这个路径通常是/boot/firmware/而不是Raspberry Pi OS的/boot/。使用编辑器打开该文件sudo nano /boot/firmware/config.txt在文件末尾添加或修改以下几行关键配置# 禁用蓝牙对PL011 UART的占用这将使蓝牙无法工作如果不需要蓝牙可以这样做 dtoverlaydisable-bt # 或者更优雅的方式明确指定UART的映射关系并保持蓝牙可用使用miniUART # 启用UART并将PL011 (ttyAMA0) 分配给GPIO引脚 enable_uart1 # 告诉系统将蓝牙分配给miniUART (ttyS0) dtoverlayminiuart-bt配置解释enable_uart1这是基础启用UART功能。dtoverlayminiuart-bt这个配置非常关键。它执行了两个操作1) 将PL011 UART (ttyAMA0) 从蓝牙手中“解放”出来分配给GPIO引脚。2) 将蓝牙功能转移到miniUART (ttyS0) 上。这样我们就得到了一个稳定的、可用的/dev/ttyAMA0设备用于通信同时蓝牙功能虽然性能可能受影响依然存在。dtoverlaydisable-bt这是更彻底的方案直接禁用板载蓝牙硬件。如果你完全不需要蓝牙用这个配置最简单也能确保PL011 UART独占。修改完成后保存文件 (CtrlO,Enter,CtrlX)然后必须重启系统以使更改生效。sudo reboot3.3 步骤三验证设备与设置权限重启后再次登录系统进行验证。检查设备文件ls -l /dev/ttyAMA0 /dev/ttyS0 /dev/serial*你应该能看到/dev/ttyAMA0和/dev/ttyS0存在。并且/dev/serial0和/dev/serial1是它们的符号链接。通常经过miniuart-bt配置后/dev/serial0(GPIO串口) 会指向/dev/ttyAMA0。测试串口回环硬件准备 最直接的验证方法是进行回环测试。你需要一根杜邦线将树莓派GPIO14 (TXD) 和 GPIO15 (RXD)短接。这样从该串口发送出去的数据会被自己接收回来。警告在连接任何外部设备前请务必确认电压电平匹配。树莓派GPIO是3.3V电平严禁直接连接5V或更高电压的设备否则会损坏树莓派对于RS-232等不同电平的设备必须使用电平转换模块如MAX3232。使用命令行工具测试 安装一个简单的串口工具如minicom或screen。sudo apt update sudo apt install minicom -y在进行回环短接后打开minicom进行测试sudo minicom -D /dev/ttyAMA0 -b 115200进入minicom界面后默认是空白尝试敲击键盘。如果你能看到屏幕上同步出现你输入的字符说明串口发送和接收通路都是正常的。按CtrlA 然后按X键退出minicom。解决权限问题 你可能注意到上面的测试用了sudo。因为默认情况下串口设备文件属于dialout用户组。为了让普通用户也能无需sudo直接访问串口需要将当前用户加入dialout组sudo usermod -a -G dialout $USER执行此命令后需要注销并重新登录或者开启一个新的终端会话用户组变更才会生效。之后你就可以用minicom -D /dev/ttyAMA0 ...而不需要sudo了。4. 软件层面实现串口通信从命令行到Python系统配置妥当后我们就可以在应用层进行编程了。这里分别介绍命令行工具和Python编程两种方式。4.1 命令行工具快速测试与调试除了刚才用到的minicom还有几个非常实用的命令行工具screen 一个终端复用器但常被用来做简单的串口监视。用法非常简洁screen /dev/ttyAMA0 115200要退出screen按CtrlA 然后按K 再按Y确认。cu 一个历史悠久的呼叫另一台Unix系统的工具也支持串口语法直观。sudo apt install cu cu -l /dev/ttyAMA0 -s 115200输入~.退出。echo和cat 最基础的组合用于快速发送指令和接收数据。# 发送字符串 “AT\r\n” 到串口 echo -e AT\r\n /dev/ttyAMA0 # 从串口读取数据并显示非阻塞方式读取已有数据 timeout 1 cat /dev/ttyAMA0这对于测试像GSM/4G模块这样的AT指令设备非常方便。4.2 Python编程使用pyserial库Python的pyserial库是处理串口通信的事实标准功能强大且易于使用。安装pip install pyserial # 或者使用系统包管理器 sudo apt install python3-serial基础通信示例 下面是一个完整的Python脚本示例实现了打开串口、发送数据、接收数据并解析的基本流程。我们以向一个模拟传感器发送查询指令READ?并等待回复为例。#!/usr/bin/env python3 import serial import time import sys # 配置串口参数 SERIAL_PORT /dev/ttyAMA0 # 根据你的实际设备修改 BAUD_RATE 9600 TIMEOUT 2 # 读超时秒 def main(): try: # 创建串口对象 # 关键参数说明 # port: 设备文件路径 # baudrate: 波特率必须与设备一致 # bytesize: 数据位默认 serial.EIGHTBITS (8) # parity: 校验位默认 serial.PARITY_NONE # stopbits: 停止位默认 serial.STOPBITS_ONE (1) # timeout: 读超时。None为阻塞等待0为非阻塞正数为超时秒数。 # write_timeout: 写超时类似。 ser serial.Serial( portSERIAL_PORT, baudrateBAUD_RATE, bytesizeserial.EIGHTBITS, parityserial.PARITY_NONE, stopbitsserial.STOPBITS_ONE, timeoutTIMEOUT, write_timeoutTIMEOUT ) except serial.SerialException as e: print(f无法打开串口 {SERIAL_PORT}: {e}) sys.exit(1) # 确保串口是打开的 if ser.is_open: print(f串口 {SERIAL_PORT} 已打开波特率 {BAUD_RATE}) else: print(串口打开失败。) sys.exit(1) try: # 示例清空输入输出缓冲区非必需但有时很有用 ser.reset_input_buffer() ser.reset_output_buffer() time.sleep(0.1) # 短暂延时 # 要发送的指令注意换行符。有些设备需要 \r\n有些只需要 \n。 command bREAD?\r\n print(f发送指令: {command.decode(ascii).strip()}) # 发送指令 bytes_written ser.write(command) print(f已发送 {bytes_written} 字节.) # 确保数据被发送出去刷新输出缓冲区 ser.flush() # 等待并读取回复 # 这里我们假设设备会在短时间内回复一行以换行符结尾的数据 response_line ser.readline() if response_line: # decode 将字节转换为字符串errorsignore 忽略无法解码的字符 print(f收到回复: {response_line.decode(ascii, errorsignore).strip()}) else: print(读取超时未收到回复。) # 更复杂的读取示例读取直到遇到特定字符或达到一定字节数 # ser.read_until(expectedb\n, sizeNone) # ser.read(size10) except KeyboardInterrupt: print(\n用户中断。) except Exception as e: print(f通信过程中发生错误: {e}) finally: # 无论如何最后都要关闭串口 ser.close() print(串口已关闭。) if __name__ __main__: main()高级技巧与注意事项编码与解码串口传输的是原始字节bytes。发送时需要将字符串编码为字节如AT.encode(ascii)或bAT。接收时需要将字节解码为字符串如data.decode(utf-8, errorsignore)。务必确认设备使用的字符编码常见的是ASCII或UTF-8。换行符处理这是最常见的坑之一。不同的设备对行结束符的要求不同可能是\r、\n或\r\n。发送时务必匹配设备要求。readline()函数默认以\n为行结束符如果设备只发\r它会一直等待。这时可以使用read_until(b\r)或更通用的方法ser.read(ser.in_waiting)读取所有可用字节然后自己解析。超时设置timeout参数至关重要。设置为None时read()会一直阻塞直到收到指定数量的字节。设置为正数时会在超时后返回已读取的数据可能少于请求的字节数。对于交互式协议合理的超时可以防止程序死锁。流控如果设备支持并启用了硬件流控RTS/CTS需要在初始化时设置rtsctsTrue。软件流控XON/XOFF则设置xonxoffTrue。大多数简单场景不需要。线程安全如果在多线程环境中使用同一个串口对象进行读写需要自行加锁threading.Lock来保证操作原子性因为pyserial的实例本身不是线程安全的。5. 实战排坑那些年我踩过的串口通信“天坑”理论配置和基础代码都跑通了并不意味着在实际项目中就能高枕无忧。下面分享几个我亲身经历或高频被问到的疑难杂症及其排查思路。5.1 现象能发送但收不到数据或者收到全是乱码这是最经典的问题。排查链路应该像侦探破案一样层层递进第一步物理层检查接线确认TXD接对方的RXDRXD接对方的TXD。是的就是交叉连接我见过不止一个资深工程师在加班到深夜时犯这个迷糊。共地确保树莓派的GND和目标设备的GND连接在一起。没有共同的参考地信号无法被正确识别。电平用万用表测量一下信号线电压。树莓派GPIO是3.3V CMOS电平。如果对方是5V TTL或RS-232±12V必须使用电平转换器直接连接必烧毁或通信异常。电源目标设备是否供电充足功率不足的模块可能工作不稳定。第二步参数层检查波特率这是乱码的罪魁祸首之首。发送方和接收方的波特率必须精确一致。9600就是9600115200就是115200差一点都不行。检查设备说明书并尝试所有可能的常用波特率9600, 19200, 38400, 57600, 115200。数据格式数据位8位最常见、停止位1位或2位、校验位无、奇、偶必须完全匹配。一个7位数据位、偶校验的设备用8位无校验去读得到的就是乱码。stty命令可以查看和设置终端参数但对已由程序打开的串口设备可能无效最好在代码中确认。流控如果一方开启了硬件流控RTS/CTS而另一方没有正确连接对应的流控线GPIO的对应引脚数据流就会被挂起表现为发送一段后停止。尝试在代码中明确禁用流控rtsctsFalse, xonxoffFalse。第三步软件与系统层检查设备文件是否正确再次用ls -l /dev/tty*确认你操作的设备文件如/dev/ttyAMA0是否存在并且其符号链接/dev/serial0指向是否正确。权限问题即使加入了dialout组在某些情况下如通过某些IDE或系统服务启动程序环境变量可能不同。尝试用sudo运行你的程序如果sudo能工作而普通用户不能就是权限问题。检查/dev/ttyAMA0的权限ls -l /dev/ttyAMA0应该是crw-rw---- 1 root dialout ...。缓冲区与冲刷发送数据后调用ser.flush()确保数据从Python的内部缓冲区写入操作系统。对于接收有时需要先ser.reset_input_buffer()清空可能存在的陈旧数据。字节序与编码如果传输的是多字节数据如int, float要确认设备使用的字节序大端/小端。如果是文本确认编码。中文乱码常常是UTF-8和GBK不匹配造成的。5.2 现象通信间歇性失败时好时坏这种问题最折磨人可能的原因有电气干扰如果通信线缆较长且靠近电源线或电机等干扰源信号质量会下降。使用带屏蔽的双绞线并尽量远离干扰源。接地环路如果通信双方通过不同的电源适配器供电且地线电位有细微差异可能形成接地环路引入噪声。尝试让双方共用一个高质量的电源。电源噪声树莓派或外设的电源质量差电压纹波大会影响核心时钟稳定性进而影响miniUART的波特率精度。换用官方电源或质量可靠的5V/3A电源。CPU频率缩放树莓派的CPU频率会根据负载动态调整这可能会影响miniUART的时钟源导致波特率漂移。这就是为什么强烈推荐使用PL011 UART因为它有独立的时钟源。如果必须用miniUART可以尝试在/boot/firmware/config.txt中设置core_freq250或core_freq_min250来固定核心频率提升miniUART的稳定性。软件超时与阻塞检查代码中的超时设置。如果read操作没有设置超时timeoutNone而设备在某些情况下没有回复线程就会永远阻塞。始终设置一个合理的超时时间并实现重试机制。5.3 使用strace和dmesg进行深度调试当常规手段无效时需要祭出系统级调试工具。dmesg 内核环缓冲区消息。当你插拔USB转串口适配器时观察dmesg的输出可以看到系统是否识别到了新设备分配了什么设备名如ttyUSB0以及是否有错误信息。# 实时监控内核消息 sudo dmesg -w # 插拔USB设备观察输出你可能会看到类似cp210x converter now attached to ttyUSB0的成功信息也可能会看到pl011: probe of ... failed with error -16这样的错误提示资源冲突。strace 系统调用跟踪器。它可以显示你的程序到底对系统做了什么。例如你的Python程序打不开设备可以用strace跟踪strace python3 your_script.py 21 | grep ttyAMA0你会看到openat系统调用的详细参数和返回值如-1 EACCES (Permission denied)或-1 ENODEV (No such device)这能精准定位是权限错误还是设备不存在。setserial 查看和设置串口底层参数。虽然现代系统多用ioctl但这个工具有时能提供信息。sudo apt install setserial sudo setserial -g /dev/ttyAMA0查看UART类型、端口地址等信息。6. 进阶应用连接USB转串口适配器与多串口管理很多时候GPIO上的一个串口不够用或者需要连接RS-232/RS-485设备这时就需要USB转串口适配器。6.1 驱动与自动识别常见的芯片如CP2102、CH340、PL2303、FT232等在Ubuntu 22.04的内核中通常都有内置驱动。插入适配器后系统一般会自动识别并创建/dev/ttyUSB0、/dev/ttyUSB1等设备文件。使用dmesg | tail可以查看识别日志。驱动问题如果遇到无法识别的情况lsusb能看到设备但无ttyUSB*可能需要手动安装驱动。例如某些旧版PL2303芯片需要特定驱动。但Ubuntu 22.04内核较新对主流芯片支持良好。6.2 固定设备名解决ttyUSB序号漂移问题一个非常实际的问题是当你同时使用多个相同的USB转串口适配器时/dev/ttyUSB0和/dev/ttyUSB1的分配顺序可能在每次重启或插拔后发生变化这会导致你的程序连接到错误的设备。解决方案是通过设备的唯一序列号或物理端口来创建固定的符号链接。这可以通过udev规则实现。找到设备的唯一属性# 插入一个USB转串口设备 lsusb # 找到你的设备记下ID例如 Bus 001 Device 004: ID 10c4:ea60 Silicon Labs CP210x UART Bridge # 使用udevadm获取详细属性其中ID_SERIAL_SHORT是序列号ID_PATH是物理端口路径 sudo udevadm info -a -n /dev/ttyUSB0 | grep -E \(ATTRS{idVendor}|ATTRS{idProduct}|ATTRS{serial}|ATTRS{idVendor}|ATTRS{idProduct})\ # 或者更精确地查找序列号 sudo udevadm info -q property -n /dev/ttyUSB0 | grep ID_SERIAL_SHORT假设我们得到序列号为CP2102_USB_to_UART_123456。创建udev规则文件sudo nano /etc/udev/rules.d/99-usb-serial.rules添加以下内容将YOUR_SERIAL替换为实际的序列号# 为特定序列号的CP2102设备创建固定符号链接 SUBSYSTEM\tty\, ATTRS{idVendor}\10c4\, ATTRS{idProduct}\ea60\, ATTRS{serial}\CP2102_USB_to_UART_123456\, SYMLINK\ttyMySensor\这条规则的意思是当发现一个子系统为tty且供应商ID为10c4产品ID为ea60序列号为指定值的设备时除了创建默认的ttyUSBx再额外创建一个固定的符号链接/dev/ttyMySensor指向它。重新加载udev规则并触发sudo udevadm control --reload-rules sudo udevadm trigger拔插USB设备现在你应该能看到/dev/ttyMySensor这个链接并且它始终指向你的那个特定适配器。在你的代码中就可以放心地使用/dev/ttyMySensor这个固定名称了。6.3 在Python中枚举可用串口对于需要动态发现串口的应用pyserial提供了工具函数import serial.tools.list_ports ports serial.tools.list_ports.comports() for port in ports: print(f设备: {port.device}) print(f 名称: {port.name}) print(f 描述: {port.description}) print(f 硬件ID: {port.hwid}) print(f 制造商: {port.manufacturer}) print(f 产品: {port.product}) print(f 序列号: {port.serial_number}) print(- * 20)你可以根据description、manufacturer、serial_number等属性来筛选出你需要的特定设备。7. 性能优化与可靠通信架构建议当串口用于关键数据采集或控制时稳定性和可靠性至关重要。错误处理与重试机制 不要相信一次通信就能成功。在打开串口、发送、接收的每个环节都要用try...except包裹并实现指数退避的重试逻辑。import time def send_with_retry(ser, data, max_retries3): for attempt in range(max_retries): try: ser.write(data) ser.flush() return True except (serial.SerialTimeoutException, serial.SerialException) as e: print(f发送失败第{attempt1}次重试。错误: {e}) time.sleep(2 ** attempt) # 指数退避 # 可选尝试重新初始化串口 # ser.close() # time.sleep(1) # ser.open() return False使用线程或异步处理 对于需要同时处理多个串口或需要在等待串口数据时执行其他任务的程序建议使用threading模块为每个串口创建一个读写线程或者使用asyncio配合pyserial-asyncio库进行异步操作。避免在主线程中进行阻塞式的read()调用。协议设计 对于自定义设备设计一个简单的应用层协议能极大提升可靠性。例如在每个数据帧前后加上帧头如0xAA、0x55和帧尾并包含长度字段和校验和CRC或累加和。接收方通过帧头定位帧起始通过长度字段确定帧边界通过校验和验证数据完整性。这能有效解决粘包多个帧粘在一起和断帧一个帧被拆开接收的问题。日志记录 将重要的发送和接收数据尤其是原始字节的十六进制表示记录到日志文件或数据库中这在后期排查偶发性故障时是无价之宝。import logging logging.basicConfig(levellogging.DEBUG, format%(asctime)s - %(levelname)s - %(message)s) # 发送时记录 logging.debug(fTx Hex: {command.hex()}) # 接收时记录 logging.debug(fRx Hex: {response_line.hex()})资源清理 确保在程序退出包括异常退出时串口被正确关闭。使用try...finally块或在上下文管理器with serial.Serial(...) as ser:中操作串口可以保证资源释放。从硬件引脚定义到系统服务配置从权限管理到Python代码实现再到深层次的排错与优化打通树莓派4B上的Ubuntu 22.04串口通信需要的就是这份耐心和对细节的把握。希望这篇超过五千字的详细梳理能让你在下次连接传感器、单片机或任何串口设备时少走弯路一次成功。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价