ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

RK3568平台Buildroot启用Python3:配置要点与板级验证全解析

RK3568平台Buildroot启用Python3:配置要点与板级验证全解析

做RK3568平台开发的朋友应该都有这种体会:buildroot构建出来的系统干净、可控,但一旦想在板子上跑Python脚本做测试,总会遇到各种“看着不对”的问题。这篇笔记(系列第113篇)把我在buildroot里启用Python、交叉编译、烧录验证、动手跑测试这一整条链路走了一遍,整理出可以直接照抄的配置项、脚本和排坑记录。适合正在用RK3568做量产方案,或者想在嵌入式板卡上用Python做自动化验证和调试的同学参考。

先说结论:buildroot里启用Python3并不难,难的是你知道哪些子选项必须开、模块被裁剪后怎么快速补、板子上跑测试时哪些表现是正常的、哪些是配置有问题。下面从配置开始,一步步讲清楚。

1. 先把思路捋清楚:buildroot里的Python到底该怎么用

1.1 为什么不直接交叉编译一个Python散装包

很多人第一反应是:Python官网源码拿下来,configure的时候指定一下--host=aarch64-linux-gnu,然后make、make install,把整个目录拷贝到rootfs里不就行了?我早期也这么干过,确实能跑,但也确实给自己挖了不少坑。

首先,Python标准库里有相当一部分模块不是纯Python,它们依赖底层C库,比如ssl依赖openssl,ctypes依赖libffi,zlib依赖zlib库,sqlite3依赖libsqlite3。自己交叉编译时,每个依赖库都得手动指定版本、手动配置头文件和动态库路径,稍不留神就会链接到host机器的库上,编译出来的解释器在板子上要么直接启动不了,要么运行到某个模块时崩掉。

其次,就算交叉编译成功,这套Python是游离在buildroot构建体系之外的。rootfs里其他组件升级、库版本变化,Python不会跟着适配;defconfig也没法记录Python的配置,换一台机器重新构建系统,所有工作还得再来一遍。项目一大,这种散装方案根本维护不动。

1.2 buildroot集成的Python有什么不同

buildroot把Python当作普通软件包来管理,它会在构建阶段自动使用当前工具链和target库里已经存在的组件。你在menuconfig里勾选了Python3之后,如果还勾了BR2_PACKAGE_PYTHON3_SSL,buildroot会确保openssl先被编译好,然后Python的_ssl模块再链接上它。所有依赖关系都是构建系统自动处理的,不需要你手动到处找依赖。

这个设计带来的实际好处是:

  • 版本一致:Python解释器、标准库扩展模块、底层依赖库都来自同一次构建,不会出现版本错位。
  • 可裁剪:不需要的模块可以不编,镜像体积小,启动速度快。
  • 可复现:一份defconfig保存所有关键配置,换机器也能构建出一模一样的系统。
  • 维护成本低:后续升级Python小版本或增删模块,只改menuconfig再重新make就行。

所以这篇文章讲的“buildroot下的Python测试”,其实不只是测试Python本身,更是在验证整个rootfs的组件依赖关系是否正确。

1.3 适用场景判断

在RK3568上用buildroot跑Python测试,我总结下来最合适的几个场景是:

  • 板级自动化验证:开机后自动跑一轮硬件检测,比如读温度、查网络、控制GPIO,把结果上报。
  • 协议联调:用socket或串口和PC端工具收发数据,验证通信链路。
  • 快速写调试工具:临时读寄存器、解析日志、抓包分析,用Python比写C快很多。
  • 轻量业务逻辑:配置解析、简单状态机、数据上报这类对性能不敏感的应用。

不合适的场景也很明显:对实时性要求高的控制逻辑、纯浮点密集计算、资源极度受限(比如内存只剩几十MB)的小系统,就别硬上Python了,老老实实用C。

2. 改造defconfig:让buildroot把Python3编进rootfs

2.1 打开menuconfig的正确路径

我习惯在buildroot顶层目录执行:

make menuconfig

然后按如下路径找到Python3:

Target packages -> Interpreter languages and scripting -> python3

需要注意的是,不同buildroot版本里这个包的名字略有区别,有些版本叫python3,有些版本分python和python3两个入口。我用的版本是2024.02 LTS版的代码主线,入口就是python3。如果你用的版本里看不到python3,先把BSP工具链、目标架构相关配置确认好,再回到菜单里找。Target packages菜单下的选项非常多,可以在menuconfig里按/键输入关键字搜索,这样定位起来快很多。

2.2 推荐开启的子选项和理由

无论是做测试还是写业务逻辑,我建议至少把这些子选项打开:

子选项说明为什么推荐
python3 mathmath模块基础数学函数和常量,测试脚本里用到math.sin、math.isclose这类函数就需要
python3 sslssl模块需要验证HTTPS、TLS证书时会用到,做联网测试强烈建议开
python3 zlibzlib压缩模块处理压缩数据、计算CRC时常用,很多依赖包会间接用到
python3 threadingthreading模块多线程测试和并发脚本的基础
python3 jsonjson模块JSON解析和序列化,跟上位机通信时基本离不开
python3 sqlite3sqlite3模块如果要在板子上存结构化数据,这一步很实用,但要额外编译libsqlite3
python3 setuptoolssetuptools包需要安装第三方Python库时的基础依赖,建议先开着备用

有些人会问,为什么不开python3 pip?我的建议是量产固件里尽量不要放pip,一方面是体积问题,另一方面是pip安装的库不受buildroot管理,会破坏rootfs一致性。开发调试阶段想装第三方包,可以在PC上交叉装到staging目录,或者直接在板子上临时开写权限装,但不要编进最终固件。

子选项之间的相互依赖,buildroot会在保存配置的时候自动把需要的底层库选上。比如勾选python3 ssl,它会把BR2_PACKAGE_OPENSSL带上;勾选python3 sqlite3,会把BR2_PACKAGE_SQLITE带上。你不需要自己去Target packages里找这些依赖库,但也别随便把它们取消掉。

2.3 把这些配置固化到defconfig里

开完menuconfig直接保存当然可行,但更推荐的做法是把关键配置写进板级的defconfig文件,比如configs/my_rk3568_board_defconfig。这样整个Python相关的配置就和板子绑定在一起了,后续同步代码、构建CI都方便。

在defconfig里至少加这几行:

BR2_PACKAGE_PYTHON3=y BR2_PACKAGE_PYTHON3_MATH=y BR2_PACKAGE_PYTHON3_SSL=y BR2_PACKAGE_PYTHON3_ZLIB=y BR2_PACKAGE_PYTHON3_THREADING=y BR2_PACKAGE_PYTHON3_JSON=y BR2_PACKAGE_PYTHON3_SETUPTOOLS=y

需要注意的是,不同buildroot版本里这些宏的名字可能会有细微差别。最稳妥的办法是:先在menuconfig里手动勾选并保存一次,然后查看output/.config里对应的BR2_PACKAGE_PYTHON3_*选项,把实际生成的宏名抄进defconfig。直接凭记忆写宏名,版本一升级很容易踩到配置不生效的坑。

2.4 编译产物核对清单

配置好之后,重新编译:

make clean make -j$(nproc)

注意我习惯先make clean。因为新增Python包会改变根文件系统的整体依赖,不clean有时候会出现旧镜像残留、包没编进去的现象。当然,如果只是改Python子选项,不做clean只执行make python3-rebuild && make也可以,速度更快,但第一次编译建议干净构建。

构建完成后,重点核对下面几个输出:

  • output/target/usr/bin/python3:解释器本体。
  • output/target/usr/lib/python3.11/:标准库目录,里面应该有os.py、socket.py、json/等纯Python文件,以及_socket.cpython-311-aarch64-linux-gnu.so这类扩展模块。
  • output/target/usr/lib/libpython3.11.so.1.0:动态库文件,有没有生成很关键,后面排查libpython找不到的问题时就要看它。

再看一下体积:

du -sh output/target/usr/lib/python3.11 du -sh output/build/python3-3.11.*/

以我的构建结果来看,Python3解释器加常用模块,在rootfs里大约占10到15MB。如果你的固件空间很紧张,可以通过关闭不必要的子选项来瘦身,但别为了省空间把zlib也关掉,好多模块会间接依赖它。

3. 板级验证:判断Python环境是不是真的能用

3.1 烧录启动后的第一轮检查

把buildroot生成的镜像(比如output/images/sdcard.img)烧录到SD卡或eMMC,板子启动后用串口登录,先看几个最基础的信息:

uname -a cat /etc/os-release which python3 python3 -V

正常输出里which python3应该指向/usr/bin/python3,python3 -V显示类似Python 3.11.9。如果这个环节就报错,按错误信息分两类处理:

  • 报not found或no such file or directory:大概率是动态链接器路径不对,用readelf -l /usr/bin/python3看一下INTERP段指向的是什么,正常应该是/lib/ld-linux-aarch64.so.1。
  • 报wrong ELF class:说明rootfs架构和板子不匹配,检查buildroot的BR2_aarch64是不是开了。

还有一个小细节:登录串口后先确认根文件系统的挂载方式。很多量产rootfs是只读的,如果你打算临时往系统里写测试脚本,可以先mount -t tmpfs tmpfs /tmp,再把脚本放/tmp下跑,避免重启后rootfs被覆盖或只读导致写入失败。

3.2 写一个环境自检脚本测标准库

不是版本号对得上就万事大吉。我给板子写的第一份自检脚本check_env.py,专门验证那些容易踩坑的标准库是否真的能加载:

import platform import sys import os import json import socket import zlib import math import threading print("machine:", platform.machine()) print("python:", sys.version.split()[0]) print("executable:", sys.executable) data = {"board": "rk3568", "test": "env-check", "value": 42} s = json.dumps(data) assert json.loads(s)["value"] == 42 print("json ok") plain = b"buildroot python test" compressed = zlib.compress(plain) assert zlib.decompress(compressed) == plain print("zlib ok") result = math.isclose(0.1 + 0.2, 0.3, rel_tol=1e-9) print("math ok, isclose(0.1+0.2,0.3):", result) sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.close() print("socket ok") t = threading.Thread(target=lambda: None) t.start() t.join() print("thread ok") print("check_env done")

脚本很短,但已经把json、zlib、math、socket、threading这几个最常用的模块都过了一遍。在buildroot里,这些模块有的默认编译,有的需要手动开子选项,跑一遍能快速确认你之前的defconfig配置到底有没有生效。

3.3 顺便量一下体积、启动时间和内存

测试Python环境是否适合量产,还要关心三个量化指标:启动时间、常驻内存、磁盘占用。

time python3 check_env.py free -m

在RK3568上,Python3从启动到执行完一个简单脚本,实测大概在0.15到0.25秒之间,常驻内存大约15到25MB。这个数值对2GB内存的板子来说完全没压力,但如果你的产品是256MB内存的老平台,就要认真评估了。

还有一个技巧:如果板子内存紧张,可以用python3 -S启动,跳过site目录导入,内存会少一点,但第三方包也暂时用不了。这个选项适合做精简的启动型服务,不适合常规测试。

4. 实战用例:在RK3568上跑通三类Python测试

4.1 场景一:板卡和主机之间的socket通信

嵌入式板卡最常见的测试需求就是和上位机通信。我在PC上写一个监听服务端,等板子主动连接,然后互发一段数据,验证网络链路、缓冲区设置、粘包处理是否正常。

PC端简单服务端tcp_server.py:

import socket HOST = "0.0.0.0" PORT = 8899 srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM) srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) srv.bind((HOST, PORT)) srv.listen(1) print("waiting for board...") conn, addr = srv.accept() print("board connected:", addr) data = conn.recv(1024) print("recv:", data) conn.sendall(b"ok-from-pc") conn.close() srv.close()

板卡端tcp_client.py:

import socket import time sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(10) try: sock.connect(("192.168.1.100", 8899)) sock.sendall(b"hello-from-rk3568") resp = sock.recv(1024) print("recv:", resp) except socket.timeout: print("connect or recv timeout") finally: sock.close()

跑的时候有几个容易被忽略的地方:

  • 板卡的IP和PC必须在同一网段,检查ifconfig和route -n,必要时用ifconfig eth0 192.168.1.x netmask 255.255.255.0临时指定IP。
  • 不要一上来就发大包。我测试时从64字节开始递增加载,确认板卡的接收缓冲区和系统TCP窗口设置符合预期。
  • 如果PC端多次收到半包或粘包,说明应用层需要定义消息边界。这不是buildroot的问题,而是你测试脚本设计时需要考虑到,可以在数据头加长度字段。

4.2 场景二:通过libgpiod控制GPIO

RK3568的GPIO在新内核里基本都能通过libgpiod工具来控制。buildroot里如果勾选了BR2_PACKAGE_LIBGPIOD,目标板上就会有gpiodetect、gpioinfo、gpioset、gpioget这几个命令。我见过很多人(包括我自己早期)还去写/sys/class/gpio,但新内核默认不启用GPIO_SYSFS,直接写节点根本不生效。

用Python脚本调用gpioset控制一个LED,我最常用的是这种写法:

import subprocess def gpio_set(chip, line, value): subprocess.run( ["gpioset", chip, f"{line}={value}"], check=True, capture_output=True, ) def gpio_get(chip, line): result = subprocess.run( ["gpioget", chip, str(line)], check=True, capture_output=True, text=True, ) return result.stdout.strip()

参数格式不同版本略有差异,我用的buildroot版本里,gpioset gpiochip0 12=1是有效写法,意思是把gpiochip0的第12号线拉高。你上板之前先执行gpioinfo看一下芯片号和线号,RK3568上一般有gpiochip0到gpiochip3,具体哪个chip对应哪组引脚,要看你的设备树。

这里要提醒一个性能问题:通过subprocess调用命令,一次操作大约有几十到上百毫秒的开销,如果用来做PWM模拟或高频翻转,效果会很差。这种场景应该直接在设备树里配PWM,或者用C写一个小的控制服务,Python只负责下发指令。

4.3 场景三:温度采集加JSON上报的回路测试

这个测试把文件读取、数值处理、JSON序列化、socket上报串在一起,最能模拟实际业务。RK3568核心板自带温度传感器,通常能在/sys/class/thermal/thermal_zone0/temp读到数值,单位是毫摄氏度。

import json import socket import time def read_temp(): with open("/sys/class/thermal/thermal_zone0/temp", "r") as f: raw = int(f.read().strip()) return raw / 1000.0 def send_report(payload): sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(5) sock.connect(("192.168.1.100", 8900)) sock.sendall(json.dumps(payload).encode()) sock.close() times = [] sampling = 5 for i in range(sampling): times.append(read_temp()) time.sleep(1) report = { "board": "rk3568", "avg_temp_c": round(sum(times) / len(times), 2), "max_temp_c": max(times), "min_temp_c": min(times), } print(report) send_report(report)

跑通之后,我把采样周期改成10秒、连续跑一小时,观察Python进程的内存变化情况。用cat /proc/<pid>/status | grep VmRSS盯一下常驻内存,如果持续增长,多半是脚本里有对象引用没释放。

4.4 场景四:大文件哈希校验的性能摸底

量产阶段经常要在板子上校验固件包、升级包完整性,Python的hashlib在这里特别方便。我在板子上放了一个约200MB的测试文件,写了一段分块计算SHA256的代码:

import hashlib import time CHUNK_SIZE = 1024 * 1024 digest = hashlib.sha256() start = time.monotonic() with open("/tmp/test.bin", "rb") as f: while True: block = f.read(CHUNK_SIZE) if not block: break digest.update(block) elapsed = time.monotonic() - start print("sha256:", digest.hexdigest()) print("elapsed:", round(elapsed, 3), "s")

在RK3568上,200MB文件的SHA256校验用Python脚本实测大约在1到1.5秒之间。这个速度对量产校验来说是完全可以接受的。为什么能这么快?因为hashlib底层的哈希运算走的是openssl的C实现,Python只是负责做文件分块读取,真正的计算热点不在Python解释器里。这也解释了一个普遍现象:纯Python代码跑得慢,但涉及底层C库的模块其实并不慢。

做这个测试的意义在于:以后你在评估一个功能到底能不能用Python实现时,就有了一个直观的参考基准值,而不是“能跑就行”。

5. 填坑实录:buildroot下Python测试常见问题速查

5.1 动态库找不到的连锁反应

我遇到最多的问题就是:rootfs里明明有python3,执行却在启动时直接挂了,报错内容是error while loading shared libraries: libpython3.11.so.1.0: cannot open shared object file。

这类问题通常是两个原因叠加出来的:一是libpython3.11.so.1.0确实没放进rootfs;二是即使放进去了,动态链接器在默认路径里找不到它。

排查步骤:

# 板子上执行 find / -name "libpython*" 2>/dev/null # 查看python3动态节区的INTERP和NEEDED readelf -l /usr/bin/python3 | grep INTERP readelf -d /usr/bin/python3 | grep NEEDED

正常情况下,libpython3.11.so.1.0应该在/usr/lib/下。如果找到但加载不到,检查一下/etc/ld.so.conf是否正确,然后执行ldconfig重建动态链接器缓存。buildroot默认rootfs一般没有这个问题,但如果你们在buildroot之外手工裁剪过文件系统,删掉了/etc/ld.so.conf或者ldconfig缓存,就会出现这种奇怪的启动失败。

5.2 标准模块按需补齐

在板上执行import sqlite3报ModuleNotFoundError,基本就是buildroot配置里没勾选python3 sqlite3。我刚开始也以为Python3全量编译后所有标准模块都会在,但buildroot恰恰相反,很多模块默认不编,属于显式选择项。

遇到ModuleNotFoundError的通用处理思路是:

make menuconfig # 进入 Target packages -> Interpreter languages and scripting -> python3 # 勾选对应子选项,然后 make python3-rebuild make

改配置后只重编Python包就行,整机make会浪费时间。重编完再烧录或更新rootfs,重新进板子验证。有些模块还依赖额外的第三方库,比如sqlite3依赖libsqlite3,buildroot会自动选上,所以正常情况下你只需要勾Python子选项就好。

5.3 浮点性能预期管理

跑同一个浮点密集脚本,板子上比PC慢几十倍,这个现象我遇到过,也见过同事在论坛上吐槽。核心问题是:RK3568虽然支持硬浮点和NEON,但如果buildroot的工具链配置成软浮点,或者Python解释器本身没有用对编译选项,性能就会直线下降。

检查方法:

cat /proc/cpuinfo | grep Features

正常输出里应有fp、asimd,这是ARMv8硬浮点和NEON对应的feature。如果完全没有,说明工具链或内核配置有问题。再一个可以快速验证的简单办法:直接在板子上跑一段math计算,看是否明显异常慢。

我的个人建议是:浮点密集计算的代码不要纯用Python写。RK3568上跑Python本身的优势是逻辑表达快、调硬件方便,不是算得快。真要算,把热点用C扩展实现,或者干脆用C写独立服务,Python只做数据交互。

5.4 网络相关的两个隐藏坑

网络测试时最容易忽略的是DNS配置。板子上connect(("www.example.com", 80))报gaierror,但直连IP又正常,十有八九是/etc/resolv.conf没配置,或者网段内根本没有可用的DNS服务器。在没有路由器DHCP的环境里,直接在/etc/resolv.conf里写nameserver也能临时解决。

另一个坑是长时间socket测试之后出现假连接:对方已经断开,板子这边不知道,还保持ESTABLISHED状态。原因是没有设置TCP keepalive。测试脚本最好加一行:

sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)

嵌入式网络环境比PC复杂得多,交换机老化、网线质量问题、瞬时断电,都可能导致连接假死。keepalive不设,测试记录可能一上来就看到一堆超时。

5.5 输出缓冲带来的调试错觉

很多人习惯这样查看输出:

python3 test.py > /tmp/log.txt

然后程序跑着跑着崩了,打开log发现最后几条println没写进去,就怀疑是程序执行到那里出了问题。实际上这通常是stdout缓冲惹的祸:当stdout不是终端时,Python默认采用块缓冲,缓冲区没满或进程没有正常退出,内容就不会落盘。

解决方法很简单,脚本开头加一句:

import sys sys.stdout.reconfigure(line_buffering=True)

或者每次print带flush=True。嵌入式开发配合systemd服务或串口重定向时,这个细节能节省大量排查时间。

5.6 常见问题速查表

现象可能原因快速解决
python3启动报libpython找不到动态库未安装或动态链接器路径问题检查readelf -d、find库文件、ldconfig
import xxx报ModuleNotFoundErrorbuildroot子选项未开启menuconfig勾选对应子项后重编
浮点脚本性能异常工具链未开硬浮点/NEON或纯Python算法低效查/proc/cpuinfo、热点用C实现
域名解析报gaierror/etc/resolv.conf未配置或DNS不可达设置nameserver、确认网关路由
重定向日志缺失stdout块缓冲print(..., flush=True)或reconfigure(line_buffering=True)
socket连接假死未设置SO_KEEPALIVEsetsockopt(SOL_SOCKET, SO_KEEPALIVE, 1)
GPIO写/sys/class/gpio不生效新内核默认关闭GPIO_SYSFS改用libgpiod工具或C接口

最后再分享一个小习惯:我现在会把check_env.py、网络测试脚本、GPIO测试脚本都放到板子的/usr/share/self-test/目录下,固件发布前让产线跑一遍。这样既验证了buildroot配置正确性,也把整套Python依赖关系固化成了可重复的回归测试。别嫌这一步麻烦,Python环境在buildroot里能不能算真正“测过”,标准不是能打开解释器,而是你实际要用的每个模块都能在这块板子上按要求跑起来。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进