最近帮客户装一台工控机,来来回回跑了好几趟,核心就三个环节:系统安装、Qt配置、PCIe驱动安装。听起来都不难,但实际做下来,每个环节都藏着至少一个能让进度卡半天的坑——启动盘选错模式导致装机失败,Qt跑起来缺平台插件,驱动编译通过了但insmod报verification failed。这篇文章把这次从裸机到PCIe设备可用的完整过程整理出来,包括我在BIOS、内核、驱动签名上踩过的坑和最终的解决路径。不管你是要给新机器装Linux工作站,还是给采集卡、运动控制卡、PCIe网卡这类设备配驱动,这套流程都适用,照着走能少走很多弯路。
1. 开工前的准备:系统选型、UEFI模式与启动盘
1.1 系统选型:工控环境为什么优先选Ubuntu LTS
很多人装机之前纠结发行版,我直接说结论:如果设备上没有特殊限制,新工控机我基本都装Ubuntu 22.04 LTS。
原因很朴素——PCIe设备的驱动厂商在编译示例代码和驱动时,最常验证的就是Ubuntu LTS和RHEL系。你去采集卡、运动控制卡、网卡厂商的官网下载Linux驱动,解压出来看README,大概率写着“Tested on Ubuntu 20.04/22.04”之类的字样。Ubuntu的LTS版本支持周期有五年,到2027年之前都能持续收到更新,对工控这种“装完就不想再动系统”的场景非常合适。
为什么不追最新版?我之前在一台机器上试过从24.04装某厂家的采集卡驱动,SDK编译时直接提示一个头文件路径变了,翻了一下源码,原来是新内核移除了某个旧接口。第三方驱动跟不上新内核的情况在工控领域太太太常见了。反过来,如果你的机器是那种刚出的新款主板,集成网卡、NVMe控制器特别新,Ubuntu 22.04的默认内核可能不识别,那就另说,得上HWE内核或者换新一点的发行版。
如果客户明确要求用麒麟、统信这类国产发行版,流程也不用推翻重来,它们大多基于Debian系,后面说的换源、装编译工具、装内核头文件这些操作路径非常接近,区别主要在包名和软件源地址上。我建议拿到机器后先问清楚驱动厂商有没有针对该发行版编译验证过,没有的话还是以Ubuntu为基准环境,再考虑迁移。
1.2 UEFI、Secure Boot和CSM:这三个BIOS选项必须先搞明白
新主板默认基本都是UEFI启动,装系统时U盘也必须是UEFI模式启动,否则会出现“U盘能引导但安装程序进不去”或者“装完了开机提示找不到引导设备”的尴尬。
更关键的是Secure Boot。这个安全功能在Windows 8之后默认开启,它会让系统只加载经过签名认证的引导程序和内核模块。问题在于,我们后手要自己编译PCIe驱动模块,手动编译出来的.ko模块基本都没有有效签名,Secure Boot开启时内核会拒绝加载,报module verification failed: signature and/or required key missing。
针对开发调试用的工控机,我的建议是在BIOS里直接关闭Secure Boot,把引导模式设置为纯UEFI。CSM(Compatibility Support Module)这个兼容模块,如果开启,理论上能支持Legacy模式启动,但会带来不少不确定性,纯UEFI反而更干净。具体路径因主板而异,一般在Boot或者Security选项卡下面,找Secure Boot并设为Disabled,CSM设为Disabled或UEFI Only。
可能有朋友问:“关掉Secure Boot安不安全?”工控机通常部署在内网或专用环境,面对的威胁模型和裸奔上网的民用电脑不同。安全性和可维护性之间总得做个取舍,你每次驱动加载失败都要去搞签名,生产的连续性受影响,成本更高。后面我会讲用MOK签名来保留Secure Boot的方案,适合那些对安全要求确实严格的项目。
1.3 制作U盘启动盘的几种方式对比
启动盘我试过不少工具,简单做个对比:
| 工具 | 适用平台 | 优点 | 注意点 |
|---|---|---|---|
| Rufus | Windows | 成熟稳定,GPT/UEFI配置直观 | 写U盘时会格式化,别选错盘 |
| Ventoy | Windows/Linux | 一个U盘可以放多个ISO,启动时选 | 个别主板对Ventoy支持不好 |
| balenaEtcher | Windows/macOS/Linux | 界面简单,一键写盘 | 写完后看不到U盘原分区,适合一次性工具 |
| dd命令 | Linux | 不依赖图形界面,服务器上也能做 | 命令错误会毁掉其他磁盘,务必核对盘符 |
我个人的主力是Ventoy——先装好一份Ventoy到U盘,后面在工地上想换系统版本,直接拷一个ISO进U盘就行,不用反复写盘。不过如果你的客户主板特别老,Ventoy识别不了,那就老老实实用Rufus写。
用Rufus写盘时,记住几个关键选择:分区类型选GPT,目标系统选UEFI(非CSM),文件系统FAT32。Ventoy默认已经处理好这些,直接在BIOS里选UEFI开头的U盘启动项即可。
如果你手头只有Linux环境,用dd命令也完全没问题:
sudo dd if=ubuntu-22.04.3-desktop-amd64.iso of=/dev/sdb bs=4M status=progress sync这里特别注意,of=后面跟的是磁盘设备(比如/dev/sdb),不是分区(/dev/sdb1),而且一定要确认/dev/sdb确实是你的U盘。写盘完成后,如果系统提示无法挂载某个分区,别紧张,那是U盘上原有的ISO分区被覆盖了,正常现象。
2. 系统安装全流程:分区策略、换源与内核头文件
2.1 安装过程中的分区策略
安装Ubuntu时,我几乎不用默认的“清除整个磁盘并安装Ubuntu”方案。不是说它不好,而是手动分区能让你清楚知道每个分区的用途,后续扩容、备份、识别问题都更有底气。
推荐的分区方案如下:
| 分区 | 大小 | 文件系统 | 挂载点 | 说明 |
|---|---|---|---|---|
| sda1 | 512MB~1GB | FAT32 | /boot/efi | EFI系统分区,UEFI引导必需 |
| sda2 | 100GB~200GB | ext4 | / | 根分区,系统、/usr、/opt都在这里 |
| sda3 | 内存大小或16GB | swap | /swap | 交换分区,内存紧张时兜底 |
| sda4 | 剩余全部 | ext4 | /home | 用户数据、工程代码、SDK都放这里 |
如果机器内存有32GB甚至更多,swap可以不分或者只给8GB。分swap我一般在“可用空间”的位置建一个逻辑分区,类型选交换空间即可,不用单独搞swap文件。
根分区给多少合适?如果要把Qt整个装在/opt目录下,加上系统本体、编译中间文件,100GB是起步水平。SDK和驱动源码我喜欢放在/home下,理由很朴素——重装系统时/home的数据基本不受影响,驱动源码和编译好的产物还能留着。
有客户机器需要保留原有的Windows系统,那就更不能用整盘自动方案了。手动分区时,EFI分区可以沿用Windows已有的那一个(FAT32就能被GRUB识别),再在空闲空间里划分根分区和/home分区。
2.2 系统装完后的第一件事:换源、更新、装SSH和编译工具链
刚装完的Ubuntu系统,默认软件源指向国外服务器,apt update经常慢到怀疑人生。我上机第一步永远是先把源切成国内镜像,这一步不做好,后面安装Qt依赖、编译工具时会等得头疼。
Ubuntu 22.04的软件源文件在/etc/apt/sources.list,可以用sed一把替换成阿里云镜像:
sudo sed -i 's@//.*archive.ubuntu.com@//mirrors.aliyun.com@g; s@//security.ubuntu.com@//mirrors.aliyun.com@g' /etc/apt/sources.list sudo apt update注意:如果你装的是Ubuntu 24.04,源配置文件路径变成了
/etc/apt/sources.list.d/ubuntu.sources,内容格式也换成了deb822格式,别拿老方案硬套,打开文件看一眼再改。
切换完源之后,立刻执行全量更新,然后安装一套开发必备工具:
sudo apt update && sudo apt upgrade -y sudo apt install -y build-essential gcc g++ make git cmake \ linux-headers-$(uname -r) ssh net-tools这一条命令里有几个关键包值得说一下。build-essential会拉取gcc、g++、make等编译基础组件;linux-headers-$(uname -r)是编译内核模块的必需品,它的版本必须和当前运行的内核完全一致,否则编译出来的.ko在加载时会报版本校验错误。
SSH服务建议顺手装上,很多工控机放在机柜或者现场,没有配显示器,装了SSH之后远程维护会方便很多。这也算是我这几年调试设备的一个习惯:任何Linux机器,只要允许,先开SSH再接网线。
2.3 内核版本管理的两个坑:不要随意升级,别忘锁版本
装完系统、驱动也跑通之后,接下来最大的不确定性就来自内核自动升级。
ubuntu的apt upgrade会连内核一起升级。内核一升级,原本编译好的PCIe驱动模块就废了——模块文件还在,但加载时会提示版本不匹配,因为里面的vermagic和当前内核不一致。这时候如果你人在现场,机器重启后采集卡或网卡突然不工作了,排查起来真的会让人抓狂。
我的处理策略是:生产机器装完系统、装好驱动之后,用下面命令把内核锁住:
sudo apt-mark hold linux-image-$(uname -r) sudo apt-mark hold linux-headers-$(uname -r)这样即使后面执行apt upgrade,内核也不会被替换。当然,锁住内核意味着无法获得安全补丁,如果对安全有严格要求,那更推荐用DKMS方案来管理驱动模块,这个我在第5章详细展开。至少要知道:“跑得好好的,重启后驱动不对了”这个经典故障,八成和内核升级有关。
3. Qt环境配置:版本选型、离线安装与编译器匹配
3.1 先定Qt版本:5.15.2还是6.5 LTS
Qt环境配置这个环节,最核心的决策就是版本选择。
工控上位机开发这一块,目前最稳的组合依然是Qt 5.15.2 LTS。原因是很多PCIe设备的厂商SDK,尤其是采集卡、运动控制卡厂商提供的示例程序和动态库,都是基于Qt5编译的,直接拿Qt6跑可能会遇到第三方库ABI不兼容、插件加载失败等问题。5.15.2是Qt5时代最后一个社区开源版本,稳定性经过多年检验,资料也多,遇到问题网上基本都有答案。
如果你是新项目、新团队,没有历史包袱,直接上Qt 6.5 LTS问题也不大。但前提是确认清楚你依赖的第三方库有Qt6版本。
有读者可能会问:“项目里写的就是Qt 5.9,能装吗?”Qt 5.9的年代比较久,在Ubuntu 22.04默认gcc 11的环境下编译,偶尔会有源码级的不兼容问题。如果厂商SDK明确基于Qt 5.9,我建议要么用Docker把编译环境固定成老版本,要么干脆把项目迁移到5.15.2,后者通常难度不大。
3.2 离线安装包比在线安装器更省心
Qt的在线安装器(online installer)需要注册登录账号才能下载组件,在服务器或者内网环境下特别不方便。我推荐直接用离线安装包,从国内镜像站把qt-opensource-linux-x64-5.15.2.run下载下来,体积大概3GB多,拷到U盘上就能装。
下载完成后:
chmod +x qt-opensource-linux-x64-5.15.2.run ./qt-opensource-linux-x64-5.15.2.run安装过程中会让你选择组件,这里别全选,磁盘空间占用很大且用不上。最小方案是勾选以下内容:
- Qt 5.15.2下的“Desktop gcc 64-bit”
- 如果需要图表、串口等模块,勾上Qt Charts、Qt Serial Port、Qt Serial Bus等
- 底部的Qt Creator我建议勾上,虽然不是必须,但调试起来方便
如果安装过程中提示缺xcb库,多半是系统缺少图形依赖。先补齐基础库再重试安装:
sudo apt install -y libxcb-xinerama0 libxcb-cursor0 libgl1-mesa-dev这个坑在无桌面环境或者精简安装的系统上经常出现,提前装好能省一轮来回。
3.3 环境变量与编译器:qmake能找到、库能链接才叫配好
安装完Qt不配环境变量,就跟买了一台新电视机不插电源一样——硬件都在,就是用不了。
把下面几行加到~/.bashrc末尾:
export QTDIR=$HOME/Qt5.15.2/5.15.2/gcc_64 export PATH=$QTDIR/bin:$PATH export LD_LIBRARY_PATH=$QTDIR/lib:$LD_LIBRARY_PATH export QT_QPA_PLATFORM_PLUGIN_PATH=$QTDIR/plugins/platforms然后执行:
source ~/.bashrc qmake -v如果能看到qmake的版本信息,基本就配置成功了。QT_QPA_PLATFORM_PLUGIN_PATH这个变量很多人会忘记设置,结果程序编译通过、一运行却报“could not find or load the Qt platform plugin xcb”,头大半天。只要库路径和插件路径都指过去,这个问题基本不会再出现。
编译器和Qt版本的匹配也要留意。Qt 5.15.2的官方二进制包用的还是比较常规的gcc ABI,Ubuntu 22.04自带的gcc 11可以直接用,不需要额外安装老版本gcc。但如果你的系统是Ubuntu 24.04默认gcc 13,某些老Qt程序编译时会因为C++ ABI变化导致链接失败,这就是另一个层面的问题,解决办法是装gcc-12再配合CMake的CMAKE_CXX_COMPILER指定。
3.4 配合VSCode开发时的几个关键配置
不少同行现在喜欢用VSCode写Qt,而不是Qt Creator。我的经验是:VSCode看代码、做Git操作确实爽,但要先用命令行确认qmake能运行,CMake构建也没问题,再回编辑器写代码。
CMake项目里,最关键的是让CMake能找到Qt库。千万别只依赖find_package(Qt5),它会从系统路径找Qt,而你装在自己目录下的Qt它根本不知道。正确做法是指定前缀:
cmake -B build -DCMAKE_PREFIX_PATH=$QTDIR cmake --build buildVSCode的C/C++插件默认也不认识Qt的头文件目录,需要在c_cpp_properties.json中手动添加include路径:
{ "configurations": [ { "name": "Linux", "includePath": [ "${workspaceFolder}/**", "/home/yourname/Qt5.15.2/5.15.2/gcc_64/include", "/home/yourname/Qt5.15.2/5.15.2/gcc_64/include/QtWidgets", "/home/yourname/Qt5.15.2/5.15.2/gcc_64/include/QtCore" ] } ] }配置好之后,代码补全、跳转定义、查找引用这些功能就正常了。至于可视化界面设计,我习惯还是用Qt Designer单独编辑.ui文件,VSCode里通过任务命令调用designer也可以,但没必要太过折腾,编辑器只是工具,能提高效率就好。
4. PCIe驱动安装:从识别设备到模块加载全链路
4.1 lspci是驱动工作的第一站
安装PCIe驱动之前,先搞清楚系统到底认不认这块设备。我见过不少同行上来就编译驱动,结果编译了半天发现设备根本没被系统枚举到,白白浪费时间。
用lspci查看:
lspci -nnk输出里找设备名称,比如一块PCIe 2.5G网卡会显示类似:
05:00.0 Ethernet controller [0200]: Realtek Semiconductor Co., Ltd. RTL8125 2.5GbE Controller [10ec:8125] (rev 05) Subsystem: ... Kernel driver in use: r8169这里[10ec:8125]就是设备ID(厂商ID:设备ID),这个信息后面找驱动、核对驱动版本都很有用。Kernel driver in use: r8169说明当前内核已经带了一个驱动。很多时候“设备没网”是因为系统自动加载的通用驱动和厂商驱动冲突,而不是驱动没装。
如果lspci里根本没有你的设备,先别急着编译驱动,回去检查:
- 设备是否插紧,PCIe插槽是否和工作;
- BIOS里PCIe设备是否被禁用;
- 主板PCIe插槽的兼容模式设置。
设备都没枚举出来,后面一切操作归零。
4.2 驱动源码编译:Makefile、内核头文件和一个经典错误
拿到厂商驱动源码包,解压后一般长这样:有src目录、Makefile文件、autorun.sh脚本,还有README。最省事的方式是跑厂商提供的脚本:
sudo ./autorun.sh这个脚本内部通常会自己编译、拷贝模块到系统目录、执行depmod和modprobe,适合大多数场景。但如果autorun.sh运行报错,或者你想自己掌控安装过程,手动编译也是几分钟的事:
tar -xf r8125-9.012.02.tar.bz2 cd r8125-9.012.02 make sudo make install sudo modprobe r8125编译过程中出现“找不到内核头文件”或者“Makefile: ... linux/version.h: No such file or directory”这类错误,原因几乎都是第2章那句linux-headers-$(uname -r)没装,或者系统升级内核后没重新装对应头文件。回到字符界面重新确认:
uname -r ls /usr/src/linux-headers-$(uname -r)如果/usr/src下没有对应目录,说明头文件确实缺失,用apt补上再回去编译。
编译好的模块是.ko文件,可以用modinfo查看信息:
modinfo r8125.ko filename: ...r8125.ko vermagic: 5.15.0-91-generic SMP mod_unload modversions看到vermagic里的内核版本和uname -r一致,才能放心加载。
4.3 加载、卸载与开机自启动的正确姿势
模块加载有两个命令:insmod和modprobe。我的建议是能用modprobe就用modprobe,它会自动处理模块依赖关系,insmod只是“硬塞进去”,不检查依赖,很容易出现模块加载了,但它依赖的其他模块没加载,功能异常。
加载模块后,最好验证一下是否真的生效。以r8125网卡为例:
lsmod | grep r8125 ethtool -i enp5s0ethtool输出的driver字段如果显示r8125,说明驱动已经接管了这块网卡。
如果发现系统里还有另一个驱动在抢设备(比如内核自带的r8169),需要把旧驱动禁止掉:
echo "blacklist r8169" | sudo tee /etc/modprobe.d/blacklist-r8169.conf sudo update-initramfs -u禁止旧驱动这一步经常被忽略,结果就是新驱动“装上了”但设备还是被老驱动占用着,一切像没发生一样。
设置开机自动加载驱动,可以在/etc/modules-load.d/下建一个conf文件,把模块名写进去:
echo "r8125" | sudo tee /etc/modules-load.d/r8125.conf这样重启后模块会被自动加载,不用每次手动modprobe。
5. 驱动不工作的排查链路:dmesg、模块签名与DKMS
5.1 排查第一步永远是dmesg
驱动加载失败时,大多数人第一反应是搜“xxx驱动加载不出来”,但不如先看内核日志。内核把每一个模块加载过程、PCIe总线枚举、设备驱动绑定都记录在dmesg里,这是最直接、信息量最大的线索。
dmesg | tail -50 dmesg | grep -Ei 'error|fail|pcie'我遇到的几次典型情况:
| dmesg日志关键片段 | 含义 | 处理方向 |
|---|---|---|
module verification failed: signature and/or required key missing | 模块没有有效签名 | 关闭Secure Boot或做MOK签名 |
Unknown symbol in module, or unknown parameter | 内核API版本不匹配 | 确认头文件版本与uname -r一致,重新编译 |
could not insert: Device or resource busy | 设备被别的驱动占用 | 查lsmod,停用冲突驱动 |
Direct firmware load for ... failed | 缺少固件文件 | 把固件拷贝到/lib/firmware下 |
这几个错误看着吓人,原因其实都挺明确,按表格里的方向排查,基本都能解决。
5.2 模块签名问题:关Secure Boot还是用MOK
如果你装完驱动,dmesg里出现required key not available或者module verification failed,那铁定是Secure Boot挡路。
最快捷的解决办法是进BIOS把Secure Boot关闭,这个过程我在第1章已经讲过,适用于开发调试和绝大多数内网部署场景。但有些客户的IT安全策略强制要求开启Secure Boot,那就要走MOK签名流程:
# 安装签名工具 sudo apt install -y mokutil # 生成签名密钥(如果厂商驱动包内置了签名脚本,可跳过) openssl req -new -x509 -newkey rsa:2048 -keyout MOK.priv -outform DER -out MOK.der -nodes -days 36500 -subj "/CN=My Module Sign Key/" # 导入密钥,会让设置一个一次性密码 sudo mokutil --import MOK.der # 重启,进入蓝色MOK管理界面,选Enroll MOK,输入刚才的密码 sudo reboot重启后,再用/usr/src/linux-headers-$(uname -r)/scripts/sign-file工具给编译好的模块签名:
sudo /usr/src/linux-headers-$(uname -r)/scripts/sign-file sha256 MOK.priv MOK.der r8125.ko然后再尝试加载模块。这个过程是麻烦了一点,但确实能兼顾安全启动限制和自定义驱动。我自己的体会是:项目现场遇到这类问题,先看是否允许关Secure Boot;不允许,再走MOK签名,两条路都走不通时才考虑换系统。
5.3 内核一升级驱动就失效:DKMS是正解
前面说过锁内核能防住驱动失效,但锁内核的副作用是无法获得安全更新。如果你既想保持内核更新,又想驱动一直可用,那就得用DKMS(Dynamic Kernel Module Support)。
DKMS会在内核升级后自动重新编译已注册的模块,相当于把“手动重编驱动”这件事自动化了。大部分厂商的autorun.sh脚本会顺手注册DKMS,比如r8125驱动装完就有了dkms支持。如果厂商没做,手动注册也不复杂。
先写一个dkms.conf放在源码目录下:
PACKAGE_NAME="r8125" PACKAGE_VERSION="9.012.02" BUILT_MODULE_NAME[0]="r8125" DEST_MODULE_LOCATION[0]="/kernel/drivers/net/ethernet/realtek" MAKE="make -C src/ KERNELDIR=/lib/modules/${kernelver}/build" CLEAN="make -C src clean" AUTOINSTALL="yes"然后注册:
sudo dkms add . sudo dkms build -m r8125 -v 9.012.02 sudo dkms install -m r8125 -v 9.012.02之后每次系统升级内核,DKMS会自动重新编译模块,驱动稳定跟着内核走。对于需要长期维护的系统,我觉得DKMS是唯一推荐的做法,省心程度高出一个档次。
6. 用系统备份固化成果
最后再分享一个我这几年养成的习惯。装好系统、配好Qt、驱动加载正常后,别急着把机器交付到现场,先用Clonezilla给系统盘做一个镜像备份。这样同一型号的设备再装第二台、第三台时,直接恢复镜像,十分钟搞定一遍装机的全部工作。即便后续有人误操作搞坏了系统,恢复镜像也比重新走一遍全流程快得多。
整个装机流程里,我踩得最深的一个坑是内核升级后驱动失效,折腾了几个小时排查。如果你也按照“系统选型→分区→换源→装头文件→装Qt→跑通驱动→固化备份”这个顺序走,绝大部分坑都可以在源头避开,希望这篇文章能帮你省下这些时间。