实训平台:龙芯 2K0300 久久派开发板(LoongArch64 架构)
实训环境:Ubuntu 22.04 虚拟机 + VMware + MobaXterm + LoongOS
实训周期:2026年7月
一、实训背景与目标
本次计算机实训的主题是龙芯(LoongArch)嵌入式 Linux 系统开发。实训使用的硬件平台是龙芯 2K0300"久久派"开发板——这是一款搭载龙芯自主研发的 LoongArch64 架构处理器(LA264 核心)的嵌入式开发平台,运行 LoongOS 嵌入式 Linux 系统(内核版本 4.19.190+,含 PREEMPT_RT rt79 实时抢占补丁),固件层使用龙芯平台标准的 PMON 引导程序。
与常见的 x86 或 ARM 嵌入式开发不同,龙芯平台有自己独特的技术生态:专属的交叉编译工具链(loongarch64-linux-gnu-gcc 8.3.0)、定制化的板级支持包(BSP)、PMON 固件体系、以及尚在完善中的开源社区支持。这些特性使得本次实训不仅是一次技术学习,更是一次国产自主平台的实践探索。
实训的目标非常明确——在龙芯 2K0300 平台上,从零开始完整走通嵌入式 Linux 开发的核心链路:从最上层的用户空间应用编程(Shell/Python),到交叉编译环境搭建和 C 语言程序部署,再到 Linux 内核模块驱动开发和设备树配置,最后触及系统固件构建(OpenWrt)。五个模块逐层深入,每一层都涉及不同的工具链、编程范式、和调试方法。
实训全程在 Ubuntu 22.04 虚拟机(VMware 运行于 Windows 11 宿主机)上进行代码编写和交叉编译,使用 MobaXterm 通过串口连接开发板进行操作,文件传输依赖 U 盘(因网络始终未调通)。整个过程中借助 Claude Code AI 进行实时错误诊断和原理讲解——这也是本次实训的一个特色:在 AI 辅助下,问题排查的效率得到了数量级的提升。
实训最大的特点是 "困难驱动学习"——五个实验模块下来,累计遇到了大大小小 20 余个实际问题,涵盖命令语法错误、编译工具缺失、内核版本不匹配、设备树语法错误、文件系统兼容性、USB 控制器驱动缺陷、内核启动 panic 等方方面面。每个问题都迫使我深入理解背后的机制,而这种"先踩坑、再理解"的方式,比顺利地走完一遍实验指导书要深刻得多。以下将按照实际实验顺序,以遇到的问题和解决过程为主线展开叙述。
二、实验环境详情
2.1 硬件配置
组件规格说明开发板龙芯 2K0300 久久派龙芯中科推出的嵌入式开发平台,面向 IoT 和工业控制场景处理器LoongArch64 LA264 核心龙芯自主设计的 64 位 RISC 指令集架构,区别于 x86/ARM/MIPS板载存储eMMC 8GB(/dev/mmcblk0)PMON 固件中识别为 emmc0,通过 ls2k_sdio 驱动访问,支持 HS200 高速模式板载内存DDR3/DDR4(约 512MB 可用)内核启动日志显示 Memory: 457496K/524288K available网络接口千兆以太网(Synopsys DWMAC1000 / stmmac 驱动)实训期间因网络配置问题未调通USB 接口dwc2 OTG 控制器用于 U 盘数据传输、USB 转串口串口UART,参数 115200/8/N/1,无流控通过 USB 转串口线连接 PC,是实验期间与开发板交互的主要通道扩展引脚GPIO 83(GPA80-3)等连接 LED,低电平有效(active-low)接线方式
2.2 软件环境
组件规格详细说明目标操作系统LoongOSYocto 项目构建的嵌入式 Linux,根文件系统高度精简内核版本Linux 4.19.190+含 rt79 PREEMPT_RT 实时抢占补丁,编译器为 GCC 8.3.0内核编译者zhangruijie@zhangruijie-virtual-machine说明该内核是由同一实验环境编译的固件PMON龙芯平台标准固件,相当于 x86 的 BIOS/UEFI主机 OSUbuntu 22.04 LTS(VMware 虚拟机)用户名 zhangruijie,NAT 网络模式宿主机 OSWindows 11 家庭中文版 10.0.26200运行 VMware 和 MobaXterm交叉编译器loongarch64-linux-gnu-gcc 8.3.0LoongArch GNU toolchain rc1.6 (20241115)工具链安装路径/usr/local/gcc-13.2.0-loongarch64-linux-gnu/虽然目录名写的是 13.2.0,实际装的是 8.3 版本串口终端MobaXterm同时支持 SSH、SFTP、串口三种连接方式AI 辅助工具Claude Code(deepseek-v4-pro 模型)运行在 Windows Git Bash 环境下
2.3 三机协作的物理拓扑
整个实训的物理连接关系如下:
┌─────────────────┐ USB 转串口 ┌──────────────────┐
│ Windows 11 PC │ ◄────────────────► │ 龙芯 2K0300 │
│ (宿主机) │ 115200/8/N/1 │ 久久派开发板 │
│ │ │ LoongOS 4.19 │
│ 运行: │ │ IP: 192.168.1.3 │
│ - VMware │ └──────────────────┘
│ - MobaXterm │
│ - Claude Code │
└──┬───────────────┘
│
│ VMware 虚拟网络 (NAT)
│
┌──▼──────────────────┐
│ Ubuntu 22.04 VM │
│ (编译环境) │
│ │
│ - 交叉工具链 │
│ - 内核源码 │
│ - 编译产物 │
│ IP: 192.168.42.x │
└─────────────────────┘
需要注意的关键约束:Ubuntu 虚拟机使用 NAT 模式,IP 在 192.168.42.0/24 网段;宿主机连校园 Wi-Fi,IP 在 10.135.51.0/24 网段;开发板在 192.168.1.0/24 网段。三者之间两两不在同一子网,网络完全不通。因此文件传输只能通过 U 盘物理搬运——这是实训期间最大的工程约束之一。
三、实验过程 —— 以困难与解决为主线
模块一:GPIO 用户空间 LED 控制
实验目标
掌握 Linux sysfs 接口在用户空间操作 GPIO 的方法,先后使用 Shell 脚本和 Python 脚本控制 GPIO 83 号引脚驱动 LED 闪烁。这是嵌入式 Linux 开发的"Hello World"——从最熟悉的应用层入手,建立对 Linux 硬件抽象机制的第一印象。
实验原理
Linux 内核为用户空间提供了一套优雅的 GPIO 控制接口——sysfs。它的设计哲学是"一切皆文件":把硬件引脚的操作抽象为对普通文件的读写。整个操作流程分为标准的四步:
第一步:导出 GPIO。 向 /sys/class/gpio/export 写入引脚编号(如 83),内核会为该 GPIO 分配资源并在 sysfs 中创建对应的控制目录 /sys/class/gpio/gpio83/。
第二步:设置方向。 向 gpio83/direction 写入 "out"(输出模式,用于控制 LED)或 "in"(输入模式,用于读取按键等)。
第三步:读写电平。 向 gpio83/value 写入 "1"(高电平,典型 3.3V)或 "0"(低电平,0V)。这里有一个容易混淆的点——LED 的实际亮灭取决于硬件接线方式:
- 高电平驱动(active-high):LED 阳极接 GPIO,阴极接地,写
"1"= 亮,写"0"= 灭 - 低电平驱动(active-low):LED 阳极接 VCC,阴极经电阻接 GPIO,写
"0"= 亮,写"1"= 灭
我们的开发板 GPIO 83 是低电平驱动方式,所以 Python 代码中 write("0") 才是"点亮"。如果搞反了这个逻辑,调试时会出现程序逻辑和别人代码对不上的困惑。
第四步:释放 GPIO。 程序退出前向 /sys/class/gpio/unexport 写入引脚编号,内核回收资源。这一步很重要——如果程序异常退出(如 Ctrl+C 中断)而没有 unexport,GPIO 会一直处于导出状态,下次运行时可能遇到"Device or resource busy"错误。
Python 脚本的模块化设计
Shell 脚本版本很简单,就是顺序执行四步操作。Python 版本做了更完善的设计:使用函数封装每个步骤(gpio_export()、gpio_write()、blink()、gpio_unexport()),支持两种运行模式——固定次数闪烁(默认 5 次,执行完自动退出并清理)和无限闪烁(Ctrl+C 中断后通过 try/finally 确保 GPIO 被正确释放)。Python 版本还用 time.sleep(1) 控制 1 秒的闪烁间隔,用 print() 输出当前运行状态方便观察。
遇到的困难
第一个实验就让我深刻体会到了嵌入式开发与桌面开发的一个关键区别:你以为理所当然存在的东西,在嵌入式系统里可能都没有。
困难一:权限错误 —— 但原因不是权限
我以 root 用户登录开发板后,自信地输入了第一条命令:
echo 83 > /sys/class/gpio/gpio83
Shell 直接返回 Permission denied。我反复 whoami 确认自己是 root,一度怀疑是文件系统挂载成了只读。后来才知道这里有两个错误同时存在:
- GPIO 83 根本没有被 export,
/sys/class/gpio/gpio83/这个目录还不存在——我试图往一个不存在的路径写数据 - 就算目录存在,
gpio83是一个目录名,不能直接往里写数据,需要写到gpio83/value或gpio83/direction
正确的第一步是 echo 83 > /sys/class/gpio/export。这个错误暴露了一个思维定式:我把 Linux 错误信息的字面意思当成了真实原因。Permission denied 在这里是"目标路径不存在且无法创建"的误导性表达——有时候错误信息只是表象,需要结合上下文深入分析。
困难二:vim 困境 —— Linux 新手的必经之路
通过 MobaXterm SSH 连接开发板后,我输入 vim led_blink.py 想编辑文件。屏幕上瞬间出现了满屏的波浪号(~ 表示空行)和底部一行看不清的模式提示。我尝试了 Esc、q、Ctrl+C、Ctrl+Z,甚至直接关闭 MobaXterm 窗口重新连接——终端里还残留着 vim 的界面。这是典型的"不知道自己在哪个模式"的 vim 新手困境。最终在 AI 指导下输入 :q!(冒号 q 感叹号 回车)强制退出。
困难三:系统精简 —— nano 去哪了
逃离 vim 后我决定换个友好的编辑器:
nano led_blink.py
-sh: nano: command not found。LoongOS 是 Yocto 构建的高度裁剪系统,只保留了最基础的 vi,nano、vim、gedit 等通通没有。解决方案是使用 shell 的 heredoc 语法直接在终端写文件:
cat > led_blink.py << 'EOF'
# 代码写在这里
EOF
后来 MobaXterm 的 SFTP 侧边栏文件拖拽功能调通后,文件传输才变得轻松。
困难四:Python 环境阉割 —— pathlib 消失
脚本写好后执行 python3 led_blink.py:
ModuleNotFoundError: No module named 'pathlib'
pathlib 是 Python 3.4 以后的标准库,桌面上从来不会缺。但 LoongOS 的 Python 显然是去掉了"不必要"模块的精简编译版本。我将所有 Path() 路径操作替换为 open() + 字符串拼接,去掉了 pathlib 依赖。
困难五:终端粘贴缩进毁坏
用 heredoc 方式粘贴多行 Python 代码时,终端经常出现缩进随机偏移——有的行多了两格,有的行少了两格,导致 IndentationError: unexpected indent。这是因为 MobaXterm 在传输大段文本时,对换行符和空格的处理偶尔不稳定。解决办法没有捷径,只能逐行用 vi 检查和修正缩进。
最终成果与反思
两个脚本最终都成功运行,LED 以 1 秒间隔规律闪烁。这次实验让我第一次感受到:嵌入式系统是一个"减法世界"——桌面 Linux 上理所当然的一切(nano、完整的 Python 标准库、tab 补全)在嵌入式环境里都是"可选配置",有时候甚至被裁剪掉了。从"加法思维"(我需要装什么)转变为"减法思维"(系统有什么我就用什么),是嵌入式开发入门的第一个门槛。
模块二:交叉编译工具链搭建与 C 语言 LED 程序部署
实验目标
在 x86_64 Ubuntu 虚拟机上搭建 LoongArch64 交叉编译环境,用 C 语言重写 LED 闪烁程序(相比 Python/Shell 有更好的性能和更小的运行依赖),通过 Makefile 管理本地编译和交叉编译两种构建目标,最后将交叉编译产物通过 U 盘部署到开发板并成功运行。
实验原理 —— 交叉编译的本质
交叉编译(Cross Compilation) 是嵌入式开发最核心的概念之一。它的定义很简单:编译平台(Build Platform)和目标运行平台(Target Platform)是不同的 CPU 架构。在我们的实验中:
- Build Platform:x86_64 Ubuntu(
uname -m输出x86_64) - Target Platform:LoongArch64(龙芯 2K0300 处理器)
交叉编译器 loongarch64-linux-gnu-gcc 本身是一个运行在 x86_64 上的程序,但它输出的二进制文件是按照 LoongArch64 指令集编码的,只能在龙芯 CPU 上执行。这种"在 A 平台上编译给 B 平台用"的能力是嵌入式开发的基础设施——因为嵌入式设备通常性能有限、存储空间小,不适合直接在板子上跑编译器和链接器。
实验要求同时进行本地编译和交叉编译,通过对比来直观理解两者的区别:
led_blink_native:用gcc本地编译 →ELF 64-bit LSB executable, x86-64→ 能在 Ubuntu 上运行(但没有 GPIO 硬件,运行会失败)led_blink_cross:用loongarch64-linux-gnu-gcc交叉编译 →ELF 64-bit LSB executable, LoongArch→ 必须在龙芯开发板上运行
通过 file 命令查看 ELF 头中的架构标识,可以一目了然地验证交叉编译是否成功。这种"看一眼 ELF 头"的习惯在以后的嵌入式工作中会经常用到。
遇到的困难
这个模块是整个实训中"工程琐事"最多的环节。C 代码本身不到 60 行,但从下载工具链到程序在开发板上跑起来,耗费了大量时间在与代码无关的事情上。这恰恰反映了嵌入式开发的真实面貌——很多时候瓶颈不在编程能力,而在工程环境的搭建和调试。
阶段一:工具链下载 —— 一场"找源"之旅
实验指导书给出的第一步是下载工具链,但没说从哪里下载。于是开始了漫长的尝试:
尝试 1:一键安装脚本。 指导书给了一个 gitcode.com 的脚本链接。下载后 cat install_gcc13.sh,发现内容竟然是 HTML 登录页面——gitcode.com 的 raw 文件需要认证才能访问。
尝试 2:GitHub。 搜索到龙芯官方的 GitHub 仓库有工具链发布。wget https://github.com/loongson/...,连接超时。在国内网络环境下访问 GitHub 极其不稳定,下载速度经常降到零。
尝试 3:apt 包管理器。 sudo apt install gcc-loongarch64-linux-gnu。返回 E: Unable to locate package。Ubuntu 22.04 官方仓库没有收录龙芯交叉编译器。
尝试 4:龙芯官方 FTP。 最终在 ftp.loongnix.cn(龙芯社区软件源)找到了工具链的直接下载链接:
wget http://ftp.loongnix.cn/toolchain/loongson-gnu-toolchain-8.3-x86_64-loongarch64-linux-gnu-rc1.6.tar.xz
下载成功。这个经历让我认识到国产平台的软件分发现状——不像 ARM 的 gcc-arm-none-eabi 那样 apt install 就搞定,龙芯的工具链需要从官网或社区源手动获取。版本方面也做了妥协,指导书要求的是 GCC 13.2.0,而 FTP 上稳定版是 8.3.0,(涉及 BSP 版本兼容性问题——BSP 1.0 用 GCC 8.3 + Linux 5.10,BSP 2.0 用 GCC 13.3 + Linux 6.12,开发板跑的是 BSP 1.0 内核)。
阶段二:命令行操作的一连串小问题
下载完成后还有一系列操作上的坑:
- 目录双层嵌套:执行
mv toolchain /usr/local/gcc-13.2.0时,目标目录已存在,mv 的行为是把源目录放入目标目录内部而非替换它,结果路径变成/usr/local/gcc-13.2.0/toolchain/bin/...。PATH环境变量配的路径完全找不到可执行文件。解决方法是tar -xvf xxx.tar.xz --strip-components=1 -C /usr/local/gcc-13.2.0直接解压到目标位置。 - 命令被终端换行截断:复制粘贴多行命令时,MobaXterm 有时会在不该换行的地方截断,导致命令参数不完整。这个问题的根源是 Windows 和 Linux 的换行符差异(
\r\nvs\n),以及终端缓冲区对长命令的处理方式。
阶段三:Makefile 的 Tab 地狱 —— 不可见字符的陷阱
按照实验指导书编写好 Makefile 后,自信地敲下 make:
Makefile:2: *** missing separator. Stop.
我把第 2 行反复看了不下五遍,语法完全正确。搜索后才知道:Makefile 每个 target 下的命令行(recipe)必须以 Tab 字符开头,不能用空格缩进。 在编辑器里,Tab 和 4 个空格看起来一模一样,但 make 程序是通过 ASCII 码来区分的——Tab = \t = 0x09,空格 = 0x20。
为什么我会用空格?因为实验指导书的代码是通过网页浏览器复制粘贴的,浏览器渲染时把 Tab 替换成了空格显示,复制时 Tab 就永久丢失了。解决办法是在编辑器中开启"显示空白字符"(VS Code 中是 Editor: Render Whitespace),或者用命令修复:
sed -i 's/^ /\t/' Makefile
这个坑几乎每个第一次写 Makefile 的人都会遇到。它还揭示了一个更普遍的规律:编程工具链对"不可见字符"的依赖是新手陷阱的重灾区。Python 的缩进(空格和 Tab 混用)、Makefile 的 recipe 行、Shell 脚本的换行符(\r\n vs \n),都是看起来一样但实际上不同的东西。学会区分和检测不可见字符是程序员的基本功。
阶段四:VMware USB 控制器兼容性 —— 硬件虚拟化也有坑
编译完成后,需要把可执行文件从 Ubuntu 虚拟机传输到开发板。网络不通(后面会讲到),只能用 U 盘。在 VMware 菜单中点击"连接 U330 USB 设备"后,Ubuntu 里执行 lsblk——空空如也。
查看内核日志 sudo dmesg | tail -20:
usb 1-1: Invalid ep0 maxpacket: 9
usb 1-1: unable to enumerate USB device
VMware 的 USB 2.0(EHCI)控制器和这个朗科 U 盘不兼容。这是 VMware 已知的 USB 直通(passthrough)兼容性问题。解决方法:关闭虚拟机 → 虚拟机设置 → USB 控制器 → 从 USB 2.0 改为 USB 3.0(xHCI)→ 启动虚拟机 → 重新连接 U 盘。lsblk 这次正确显示了 /dev/sdb(58.6GB)。
阶段五:文件系统兼容性 —— 开发板不认识 FAT32
U 盘在 Ubuntu 上挂载、复制文件、卸载,插到开发板上,执行:
mount /dev/sda1 /mnt
返回 unknown filesystem type 'vfat'。尝试加载内核模块:
modprobe vfat
返回 FATAL: Module vfat not found。这意味着开发板内核编译时完全没有包含 vfat/FAT32 支持——既没有编译进内核,也没有编译为模块。
大多数 U 盘出厂默认 FAT32 格式是为了跨平台兼容(Windows/Mac/Linux 都能读写),但嵌入式 Linux 为了精简内核体积,只保留了 ext4 等最必需的文件系统驱动。解决方案是将 U 盘重新格式化为 ext4:
sudo mkfs.ext4 /dev/sdb1
风险提示:这会清空 U 盘上所有数据!格式化前务必确认 U 盘里没有需要保留的文件。格式化后还需重新拔插 U 盘让内核识别新的分区表和文件系统签名。
阶段六:U 盘设备名漂移 —— 没有持久化绑定
每次拔插 U 盘后,它的设备名都在变化:
sda → sdb → sdc → sde → ...
这是因为 Linux 内核按照 USB 设备枚举的先后顺序动态分配块设备节点名(/dev/sd*),没有任何持久化绑定(不像 /dev/disk/by-uuid/ 或 /dev/disk/by-id/ 那样稳定)。开发板上连 lsblk 命令都没有(系统裁剪 again),只能用 ls /dev/sd* 和 fdisk -l 来确认当前设备名。
阶段七:ext4 超级块损坏
格式化 ext4 后挂载时又出错:
EXT4-fs (sdb1): couldn't mount as ext3 due to feature incompatibilities
文件系统超级块(superblock)在格式化后出现了不一致。fsck.ext4 -y /dev/sdb1 修复后正常。
阶段八:网络困境 —— 为什么放弃 SCP
整个实训期间,Ubuntu 虚拟机和开发板的网络始终不通。经过详细排查:
- Ubuntu 虚拟机:VMware NAT 模式,IP 为
192.168.42.x - Windows 宿主机:连校园 Wi-Fi,IP 为
10.135.51.x - 开发板:静态度
192.168.1.3 - 三者之间两两不在同一子网,
ping全部返回Network is unreachable - 没有路由设备连接这三个网段
尝试将 VMware 网络模式改为桥接(Bridged),但桥接后虚拟机获取到的是校园网 DHCP 地址(10.135.51.x),仍然和开发板的 192.168.1.x 不在同一网段。想要打通需要额外配置路由规则或 NAT 转发,在实训时间有限的情况下不现实。最终决定全程使用 U 盘物理传输——每次多花几分钟拔插操作,但它不依赖任何网络配置,在隔离的嵌入式开发环境中反而最可靠。
阶段九:开发板目录结构差异
在开发板上执行 mkdir -p /root && cp /mnt/led_blink_cross /root/ 时报错 /root 目录不存在。标准 Linux 发行版中 root 用户的家目录是 /root,但 LoongOS 的根文件系统中省略了这个目录(root 直接使用 / 作为家目录)。改为 cp /mnt/led_blink_cross ~/ 后正常。
最终成果与反思
# Ubuntu 上交叉编译
$ make cross
loongarch64-linux-gnu-gcc -o led_blink_cross led_blink.c
$ file led_blink_cross
led_blink_cross: ELF 64-bit LSB executable, LoongArch, version 1 (SYSV),
dynamically linked, interpreter /lib64/ld.so.1, for GNU/Linux 4.15.0, not stripped
# 开发板上运行
# ./led_blink_cross
LED blinking finished! # LED 实际闪烁了 5 次,每次 1 秒亮、1 秒灭
C 语言 LED 程序在龙芯开发板上成功运行。这个模块耗费时间最长的部分不是编写 C 逻辑,而是九个"非代码"问题的逐一排查。它让我深刻体会到:嵌入式开发的瓶颈往往不在编程能力,而在工程环境——从哪里下载工具链、文件怎么过墙(物理的和网络的)、设备为什么不被识别、文件系统为什么挂不上。 这些问题在 x86 桌面开发中几乎不存在(apt install + 本地运行搞定一切),但在嵌入式领域却是每天的日常。
模块三:PMON 固件基础操作
实验目标
了解龙芯平台 PMON 固件的基本功能、命令体系、启动流程、和自动启动配置方法。能够从 PMON 命令行手动加载 Linux 内核并引导系统启动。
实验原理 —— PMON 在系统启动中的位置
PMON 是龙芯平台的系统固件,在系统启动链中处于最底层的位置。它在系统中的角色完全等同于 x86 PC 的 BIOS 或 UEFI。当按下电源键后,SoC 首先执行固化在 ROM/Flash 中的 PMON 代码:
- 上电自检(POST):初始化 CPU 核心、DDR 内存控制器、基础时钟树
- 外设枚举:扫描并初始化 eMMC、USB Host、以太网等外设,为它们分配 PMON 内部设备名(如
emmc0、usb0、syn0、syn1) - 查找启动配置:按优先级顺序查找——先检查根目录是否有
boot.cfg文件,没有则读取autocmd环境变量,再没有则使用al/al1/rd/append变量组合 - 加载内核:根据配置中的路径,从指定设备加载内核 ELF 文件(vmlinuz)到内存的指定地址
- 加载 initrd(可选):如果配置了 ramdisk,将其加载到内存
- 传递设备树(DTB):将
.dtb二进制文件加载到内存并告知内核其位置 - 设置内核命令行参数:
console=、root=、rdinit=、rootdelay=等 - 跳转执行:CPU 程序计数器跳转到内核入口地址,此后由 Linux 内核接管一切
如果在启动过程中按了空格键,或者自动启动配置缺失/失败,就会进入 PMON 交互命令行,提示符为 PMON>。这时管理员可以手动执行 load、initrd、g(go)等命令来加载和启动系统。
PMON 核心命令速查
命令语法示例功能说明hh显示帮助信息和可用命令列表versvers查看 PMON 固件版本号devlsdevls列出所有检测到的设备及其 PMON 设备名loadload (usb0,0)/boot/vmlinuz从指定设备的指定路径加载 ELF 格式内核到内存initrdinitrd (usb0,0)/boot/initrd.img加载 ramdisk(initramfs)镜像gg console=ttyS0,115200 root=/dev/mmcblk0p1设置内核命令行参数并从入口地址开始执行setset autocmd "load (usb0,0)/boot/vmlinuz;g"设置环境变量(注意值中不能有空格)unsetunset autocmd删除环境变量floadfload (usb0,0)/pmon.bin烧录/更新 PMON 固件本身pingping 192.168.1.1测试网络连通性ifconfigifconfig syn0 192.168.1.100配置网络接口 IPfdtfdt (usb0,0)/boot/loongson_2k0300.dtb手动加载设备树文件blbl列出启动配置信息
PMON 与 Linux 的设备命名对照
一个关键认知是:PMON 的设备命名和 Linux 内核的设备命名是两套完全独立的体系。它们在同一个硬件上使用不同的名称:
硬件实体PMON 中的名称Linux 内核中的名称板载 eMMC 存储emmc0/dev/mmcblk0(块设备),mmc0(控制器)USB 大容量存储usb0/dev/sda(或 sdb/sdc,取决于枚举顺序)千兆以太网syn0, syn1eth0, eth1(或 stmmac)回环设备loopdev0/dev/loop0
理解这个对应关系在后续的内核启动调试中起到了关键作用——当内核 panic 报 Cannot open root device "mmcblk0p1" 时,我们可以回到 PMON 用 devls 确认 emmc0 硬件是否被检测到,从而判断问题是出在硬件层面还是内核的设备树配置层面。
遇到的困难与反思
这个实验以理论学习为主,最大的困难不是技术操作,而是思维模式的切换。在前两个模块中,我们一直在 Linux 操作系统内部工作——有熟悉的 bash shell、ls / cd / cat 命令、tab 补全、命令历史、文件系统层级结构。但 PMON 是裸机固件:没有文件系统的概念((usb0,0)/boot/vmlinuz 是 PMON 自己定义的一种设备路径语法,不是 Linux 路径),没有 tab 补全,输错参数就要全部重打,命令总数也就十几个。
这种切换的本质是从"操作系统提供的丰富抽象"降到"固件提供的极简抽象"。在操作系统里你习惯了 cd /boot && ls -la,在 PMON 里你得用完全不同的语法 devls 先看有哪些设备,然后凭记忆输入完整的内核路径。如果忘记设备名或路径,只能重启再看。
另外,设备树的传递过程值得特别理解:PMON 负责从存储设备读取 DTB 文件,将其加载到内存的指定位置,并在跳转到内核前把 DTB 的内存地址告知内核。如果 DTB 文件损坏(后面实验会遇到的 dtb chsum err!!!),或者 PMON 没有正确传递地址,内核就无法识别平台设备(GPIO 控制器、MMC 控制器等),导致启动失败。这个机制在后面实验四的内核 panic 排查中成为了分析问题的关键线索。
模块四:Linux 内核模块驱动开发 —— GPIO LED 驱动
实验目标
这是本次实训最核心、难度最高、耗时最长的实验。目标是:获取龙芯定制 Linux 4.19 内核源码,编写完整的 GPIO LED 驱动模块,通过交叉编译生成 .ko(Kernel Object)文件,在开发板上使用 insmod 加载驱动,驱动加载时自动点亮 LED 并通过 printk 内核日志输出学号。实验还涉及修改内核 Kconfig/Makefile 构建系统将自定义驱动集成进去,以及修改设备树(DTS)文件为 GPIO 引脚添加正确的 pinctrl 配置。
实验原理
内核模块 vs 用户空间程序的本质区别
前面模块一和模块二中,我们通过 sysfs 接口从用户空间控制 GPIO。调用链是:用户程序 → write() 系统调用 → VFS 虚拟文件系统 → sysfs 文件系统 → GPIO 子系统 → 硬件寄存器。每次 GPIO 操作都要穿越用户态到内核态的边界(系统调用开销),数据要经过多层抽象转换。
内核模块则完全不同——它直接运行在内核空间,享有最高的 CPU 特权级(Ring 0),调用的是内核内部 API(gpio_request()、gpio_direction_output()、gpio_set_value() 等),直接操作硬件寄存器,没有系统调用开销。内核模块没有 main() 函数,取而代之的是 module_init(fn) 和 module_exit(fn) 两个宏——前者指定模块加载时执行的初始化函数,后者指定模块卸载时执行的清理函数。
编译后的内核模块是 .ko(Kernel Object)文件,通过 insmod 命令加载、rmmod 卸载、lsmod 查看已加载模块列表、dmesg 查看内核日志(包括 printk 输出)。模块必须是 GPL 兼容许可证才能使用内核的 GPL-only 符号。
Kconfig/Makefile 构建系统
Linux 内核使用一套精密的配置和构建系统来管理数千个驱动和功能选项。要向内核添加一个自定义驱动,需要修改两个文件:
Kconfig:定义配置选项的元数据。它告诉 make menuconfig 界面应该显示什么选项、该选项的依赖关系、以及它对应什么编译行为(tristate 表示可以选 Y/M/N):
config LEDS_2K0300_GPIO
tristate "LED Support for Loongson 2K0300 GPIO"
depends on LEDS_CLASS && GPIOLIB
help
This option enables LED driver for Loongson 2K0300
using GPIO 83.
Makefile:定义编译规则。它告诉内核构建系统当某个配置项被选中时应该编译哪些源文件:
obj-$(CONFIG_LEDS_2K0300_GPIO) += leds-2k0300-gpio.o
这行代码的妙处在于 $(CONFIG_LEDS_2K0300_GPIO) 会根据 .config 中的配置值自动展开为 y、m、或空——分别对应编译进内核(obj-y)、编译为模块(obj-m)、或不编译。
设备树(Device Tree)机制详解
x86 PC 上的硬件很多是"可枚举"的——PCI 总线、USB 总线等都有标准的设备发现协议,内核启动时能自动扫描并识别外接设备。但嵌入式 SoC 平台不一样:大量的外设(GPIO 控制器、I²C、SPI、UART、MMC/SDIO 控制器等)是直接挂在芯片内部的内存映射总线上的,没有标准的自动发现机制。
设备树(Device Tree)就是为了解决这个问题。它是一种数据结构(源文件 .dts 编译为二进制 .dtb),在系统启动时由固件(PMON)传递给内核,告诉内核:
- 这个 SoC 上有哪些硬件(设备节点)
- 它们挂载在哪个总线上(
compatible属性用于匹配驱动) - 它们的寄存器基地址、中断号、时钟源是什么(
reg、interrupts、clocks) - GPIO 引脚的多功能复用如何配置(
pinctrl子系统)
在我们的实验中,设备树的样子大概是:
&gpio {
led_pin: led {
gpios = <&gpio 83 GPIO_ACTIVE_HIGH>;
label = "heartbeat";
};
};
&gpio 是引用(label reference)已定义的 GPIO 控制器节点,gpios = <&gpio 83 ...> 指定使用该控制器的第 83 号引脚。
内核模块加载的三道 ABI 校验机制
Linux 内核为模块加载设下了层层校验,确保模块与当前运行内核的二进制接口(ABI)完全兼容。理解这三道锁是成功加载模块的关键:
第一道:vermagic(版本魔术字符串)。编译模块时,构建系统会自动在 .ko 文件的 .modinfo 段中嵌入一个"版本魔术字符串",包含:
vermagic: 4.19.190+ SMP preempt mod_unload modversions LOONGARCH64
insmod 首先将模块中的 vermagic 与运行中内核的 vermagic 做字符串比对,任何差异都会直接导致加载失败。
第二道:module_layout 符号。这不是一个普通函数,而是一个数据结构校验和。它编码了内核核心数据结构 struct module 的大小和关键成员的内存偏移量。如果正在运行的内核和编译模块时使用的内核在抢占模型(PREEMPT vs PREEMPT_RT)、SMP 配置、内存管理选项等方面有差异,struct module 的布局就会不同,module_layout 的 CRC 值就会变化。
第三道:CONFIG_MODVERSIONS(符号级 CRC 校验)。启用后,内核为每个导出的符号(函数、变量)计算 CRC32 校验值。模块编译时会记录所有引用符号的 CRC 值,insmod 加载时将这些 CRC 与运行内核的实际情况逐一比对。即使内核版本号完全一样,只要某个内核配置项(尤其是影响函数签名的)不同,CRC 就会不匹配。
这三道锁层层递进:vermagic 是粗粒度的字符串匹配,module_layout 是数据结构级别的校验,modversions 是符号级别的校验。全部通过后才能成功加载。理解这些机制是后续调试 insmod 失败的基础。
遇到的困难
阶段一:编译困难
问题现象原因分析解决过程内核源码不在工作目录tar -xvf linux-4.19-0627.tar.gz 报文件不存在浏览器下载时使用了默认路径 ~/Downloads/,而我当时的工作目录是 ~/loongarch-dev/在 Downloads 目录找到后 mv 迁移过来缺少编译依赖make menuconfig 报 fatal error: openssl/opensslv.h: No such file or directory虚拟机是相对干净的 Ubuntu 22.04,缺少内核编译所需的大量开发库sudo apt install libssl-dev libelf-dev flex bison bc没有精确匹配的 defconfigmake loongson_2k0300_defconfig 报 No rule to make target2K0300 是较新的开发板,该版内核源码中并没有预置同名的默认配置以最接近的 loongson2_defconfig 为基础,再通过 make menuconfig 手动设置 CONFIG_LOCALVERSION="+" 等选项
阶段二:设备树困难 —— "看不到的依赖"
交叉编译走通了(make modules 成功生成 .ko),但设备树这边接连爆雷:
问题 1:dtbs 编译无任何输出。 执行 make dtbs 后静默结束,arch/loongarch/boot/dts/loongson/ 下空空如也。排查后发现 .config 中 CONFIG_BUILTIN_DTB_NAME=""——内核不知道要为哪个板子编译 DTB。通过 make menuconfig → "Boot options" → "Built-in DTB name" 设置为 loongson_2k0300_pai_99。
问题 2:DTC include 路径错误。 设置 DTB 名称后再次编译:
fatal error: dt-bindings/gpio/gpio.h: No such file or directory
DTS 源文件顶部有 #include <dt-bindings/gpio/gpio.h>,这个 <> 的搜索路径中包含 arch/loongarch/boot/dts/include/,但这个目录不存在——它应该是一个符号链接,指向内核源码根目录的 include/。为什么它不存在?可能是在解压或迁移源码时符号链接丢失了(某些压缩格式不保留符号链接)。
cd arch/loongarch/boot/dts
ln -s ../../../../include include
创建符号链接后 include 问题解决。这件事让我学到一个经验:解压内核源码后,先检查符号链接是否完整。
问题 3:pinctrl 标签未定义。 引用了 .dtsi 头文件中不存在的 pinctrl 标签。需要仔细检查所 include 的 .dtsi 文件中实际定义了哪些标签,确保引用名称完全一致。
阶段三:模块加载困难 —— 内核设下的三道校验锁
编译终于全部通过,leds-2k0300-gpio.ko 成功生成。通过 U 盘拷到开发板上,敲下 insmod:
insmod leds_2k0300_gpio.ko
第一道锁:module_layout 版本号后缀不匹配
insmod: ERROR: could not insert module leds_2k0300_gpio.ko: Invalid module format
dmesg 查看内核日志:
leds_2k0300_gpio: disagrees about version of symbol module_layout
我首先用 modinfo 查看模块信息:
vermagic: 4.19.190 SMP preempt mod_unload modversions LOONGARCH64
而开发板 uname -r 输出:
4. 19.190+
就差了末尾的 + 号,这个 + 来自内核配置项 CONFIG_LOCALVERSION="+"(Yocto 构建系统默认加的后缀,表示代码树有未提交的修改)。我们在 Ubuntu 的内核 .config 中加上 CONFIG_LOCALVERSION="+" 重新编译模块,vermagic 对齐。
第二道锁:CONFIG_MODVERSIONS CRC 校验不匹配
版本对齐后再次 insmod,又报新的错误:
leds_2k0300_gpio: disagrees about version of symbol gpio_request
leds_2k0300_gpio: Unknown symbol gpio_request (err -22)
这表示开发板内核启用了 CONFIG_MODVERSIONS,每个导出符号都有 CRC 校验和。要让 CRC 完全匹配,需要获得开发板的精确内核配置。理想方案是从 /proc/config.gz 导出配置后完整重编译内核,但这有两个前置条件:
CONFIG_IKCONFIG_PROC必须已在内核中启用(否则/proc/config.gz不存在)- 完整重编译内核在我们环境中需要数十分钟
在我们的场景下,时间不允许完整重编译。经过分析后做出的决策是:使用 insmod -f(--force)强制加载,跳过 CRC 校验。 这不是盲目试错——我们的驱动只使用了标准 GPIO 子系统 API(gpio_request、gpio_direction_output、gpio_set_value、gpio_free),这些 API 的签名和实现在标准内核和 RT 内核中是完全一致的,不受 PREEMPT_RT 抢占模型差异的影响。强制加载不会导致内核崩溃或数据损坏。
insmod -f leds_2k0300_gpio.ko
成功。
关于
insmod -f的深入思考:在工程实践中,"强制"操作永远应该是最后的选择而非第一反应。如果一个新人遇到insmod失败就立刻-f,那就是在蒙。但如果理解了 vermagic 的作用、module_layout 的编码内容、modversions 的 CRC 机制、以及 RT 补丁对内核数据结构的影响范围,然后做出"标准 GPIO API 不受影响、强制加载是安全的"这个判断,那就是真正的工程能力。"知其所以然"的变通,和"不知其所以然"的蛮干,表面上看起来做了同一件事,本质完全不同。 这是本次实训最重要的方法论收获。
阶段补充:内核启动 panic —— 设备树问题的连锁反应
在实验过程中,开发板还出现过一次严重的启动失败——内核 panic。这对理解设备树的作用是一次"反面教材"式的加深:
[ 5.734658] mmc0: new HS200 MMC card at address 0001
[ 5.750356] mmcblk0: mmc0:0001 8GTF4R 7.28 GiB
...
[ 9.947322] VFS: Cannot open root device "mmcblk0p1" or unknown-block(0,0): error -6
[ 9.955287] Kernel panic - not syncing: VFS: Unable to mount root fs on unknown-block(0,0)
分析日志:
- 在 5.73 秒,MMC 控制器(
ls2k_sdio驱动)探测到了 eMMC 卡,显示了正确的容量(7.28 GiB) - 但在 9.95 秒,内核报告找不到
mmcblk0p1块设备,error -6 =ENXIO(No such device or address) - 关键线索在 PMON 启动阶段的输出中:
dtb chsum err!!!
这个 dtb chsum err!!! 意味着 PMON 传递给内核的设备树二进制文件校验和错误——文件可能已经损坏。设备树中 MMC 控制器的描述信息可能不完整或错误,导致内核虽然在底层硬件枚举阶段探测到了卡(这也是 mmc0: new HS200 MMC card 能打出来的原因——硬件枚举是控制器驱动直接做的,不完全依赖设备树),但在上层的块设备注册阶段(block layer)失败了——因为没有正确的设备树节点,块设备层不知道要创建 /dev/mmcblk0 这个设备。
时序排查:将内核命令行参数 rootdelay 从 5 秒增大到 15 秒后依然 panic,排除了"设备还没就绪"的时序假说。
两套设备名验证:回到 PMON 用 devls 确认 emmc0 设备存在——硬件没问题,eMMC 卡是好的。问题出在 PMON → 内核的数据传递环节。这个排查链条完美体现了"理解 PMON 设备名和 Linux 设备名的对应关系"的实用价值。
最终成果
# insmod -f leds_2k0300_gpio.ko
# dmesg | tail -5
[ 2988.090198] Simple LED driver loading
[ 2988.097566] loongson pinctrl 16000490.pinctrl: request pin 83 (GPA80-3) for GPA80:83
[ 2988.097591] LED on GPIO 83 turned ON
[ 2988.097591] Student ID: 2024304788
两行关键日志:
loongson pinctrl ... request pin 83 ... for GPA80:83— 引脚控制子系统成功为 GPIO 83 分配了引脚复用功能LED on GPIO 83 turned ON— LED 亮了Student ID: 2024304788— 学号输出成功
两个多小时的鏖战,从编译到设备树到模块加载,终于在看到 LED 亮起的那一刻得到了回报。这是整个实训最具挑战也最有成就感的时刻。
四、实训收获 —— 技术、工程、方法论
4.1 技术知识体系
整个实训帮助我建立起了一个清晰的嵌入式 Linux 分层认知模型:
┌──────────────────────────────────────────┐
│ 系统构建层 │ OpenWrt / Yocto / Buildroot │ ← 固件镜像的自动化生产
├──────────────────────────────────────────┤
│ 内核驱动层 │ .ko 模块、设备树、pinctrl │ ← 与硬件直接对话
├──────────────────────────────────────────┤
│ 编译部署层 │ 交叉编译、Makefile │ ← 连接开发和运行的桥梁
├──────────────────────────────────────────┤
│ 应用编程层 │ C / Shell / Python │ ← 用户空间的逻辑
├──────────────────────────────────────────┤
│ 固件引导层 │ PMON │ ← 硬件苏醒的第一段代码
└──────────────────────────────────────────┘
具体的技术收获包括:
- GPIO 子系统:从 sysfs 用户空间接口到内核 API(
gpio_request/gpio_set_value/gpio_free),理解了"文件即硬件"的 Unix 哲学如何在 Linux 中不同层次体现 - 交叉编译:深刻理解 Build ≠ Target,掌握了
ARCH、CROSS_COMPILE、工具链安装、file命令验证架构的完整流程 - 内核模块开发:
module_init/module_exit框架、Kconfig/Makefile 构建系统集成、printk 日志级别 - 设备树:DTS 语法(label reference
&、pinctrl)、DTC 编译、include 路径依赖、dtb 在启动流程中的传递机制 - 模块加载校验:vermagic、module_layout、CONFIG_MODVERSIONS 三道锁的递进关系和破解方法
- 龙芯生态:LoongArch64 指令集、PMON 固件、Loongnix 软件源、2K0300 BSP 版本兼容性
- 内核启动调试:panic 日志分析、
ENXIO错误码含义、rootdelay参数调优、设备树完整性验证
4.2 调试思维的建立 —— 最大的成长
回顾实训全程,最有长远价值的不是某一个技术点,而是一套面对未知错误的系统化分析思路。
以前遇到报错,我的反应是:看到错误信息 → 感到慌张 → 复制粘贴去搜索引擎 → 找到第一个看起来相关的结果 → 照做。这种方法的缺点是:不理解原因,下次遇到类似的变种错误还是不会。
经过这五个模块 20+ 个问题的训练,现在遇到报错会按这样的流程思考:
- 准确阅读错误信息:逐词理解报错文本,而不是扫一眼就下结论。
Permission denied不一定是权限问题,missing separator指的是不可见字符 Tab 缺失。 - 区分症状和根因:
module_layout不匹配是症状,根因是内核配置中CONFIG_LOCALVERSION和CONFIG_MODVERSIONS与开发板不一致。只治症状(如随意insmod -f)不是真正的解决问题。 - 逐层排除:文件在不在 → 命令对不对 → 版本匹不匹配 → 配置一不一致 → 硬件是否正常。每一层都排除了再到下一层。
- 理解后再做取舍:
insmod -f跳过了 CRC 校验,在确认 GPIO API 不受 RT 补丁影响的前提下是安全的工程权衡。理解了什么被跳过了、为什么跳过是安全的——这是"知其所以然"的工程判断。 - 善用工具但不依赖工具:
dmesg查看内核日志、modinfo查看模块 vermagic、file验证 ELF 架构、PMONdevls确认硬件——每个工具都有它最适合的诊断场景。
4.3 国产平台的实践认知
本次实训的特殊之处在于使用了龙芯 LoongArch 平台而非常见的 ARM(STM32/Raspberry Pi)。通过实训对国产自主平台有了直观认识:
- 技术成熟度:龙芯的 GNU 工具链已经很完善,GCC 8.3 对 LoongArch64 的支持稳定可靠。PMON 固件功能完整。Linux 内核主线从 5.19 开始逐步合入 LoongArch 支持。
- 生态现状:基础工具链成熟,但上层生态(包管理器覆盖、OpenWrt 支持、社区文档)与 ARM 相比还有差距。很多软件包需要从 FTP 或源码手动获取,不像 ARM 那样
apt install就能搞定。 - 发展趋势:龙芯生态正在快速完善中(BSP 从 1.0 到 2.0,内核从 5.10 到 6.12,GCC 从 8.3 到 13.3),国产自主处理器的软件生态建设是一个长期但方向明确的工程。
4.4 AI 辅助学习的方法论
实训全程在 Claude Code 的辅助下完成。关于如何使用 AI 来辅助编程学习,我有几点体会:
- AI 最适合的角色是"实时导师":当终端报出一个错误时,AI 能够同时在几个维度给出反馈——错误的字面含义、可能的根因(通常给出 2-3 个假设)、每种假设的验证方法、以及背后的原理。这个"一条龙"式的诊断流程,比搜索引擎逐个页面翻阅的效率高出一个数量级。
- AI 不能替代自己的判断:
insmod -f的方案是 AI 给出的(带着风险提示),但最终采不采用,取决于自己是否真的理解了 module_layout 和 modversions 的含义。如果无脑执行 AI 的每一条建议,学到的是零。 - AI 辅助下,"不怕遇到问题"成为可能:以前遇到一个完全不懂的错误(如"module_layout 不匹配"),可能要花很长时间搜索、理解、尝试。现在 AI 能在几分钟内讲清楚来龙去脉,让"踩坑 → 学习"的循环极大加速。这也是为什么我能在一次实训中经历 20+ 个问题却依然能推进——每个问题都成为了一个高效的学习机会。
五、总结
本次实训以龙芯 2K0300 久久派开发板为硬件平台,围绕嵌入式 Linux 开发的核心技术链路,从应用层到底层固件,逐层深入,完成了五个模块的学习与实践:
模块内容概述层次核心困难关键收获GPIO 用户空间 LED 控制Shell/Python + sysfs GPIO 83 闪烁应用层裁剪系统缺工具、vim 困境、Python 标准库不全建立"嵌入式是减法世界"的认知交叉编译与 C 程序部署工具链安装、Makefile、C LED、U 盘传输编译层工具链下载源不稳定、Tab 缩进、USB 兼容性、文件系统不匹配、网络不通理解 Build ≠ Target,掌握工程环境搭建能力PMON 固件基础固件命令体系、启动流程、自动启动配置固件层裸机思维切换、设备命名体系差异理解固件在启动链中的角色内核模块驱动开发.ko 模块、Kconfig/Makefile、设备树、insmod 加载内核层编译依赖缺失、设备树 include 路径/语法、module_layout 版本不匹配、CRC 校验失败、内核 panic掌握驱动开发全流程和模块加载校验机制OpenWrt 系统构建初探构建系统架构、menuconfig 配置、软件包选择系统层依赖安装、源码获取理解从源码到固件的自动化构建流程
20 余个问题,从命令拼写到内核 ABI 校验,从 USB 控制器驱动到设备树传递机制——每一个问题的排查和解决,都迫使我深入理解背后的原理。
实训最大的价值不在于"点亮了一个 LED",也不在于"写出了第一个内核模块",而在于建立起了一套面对未知问题时的系统化分析框架——遇到报错不慌张,逐层定位根因,充分理解原理后再做工程决策。这种"调试思维"不是一个具体的技术栈,而是一种可以迁移到任何技术栈的底层能力。它会伴随我在计算机领域的每一步成长。
在国产自主处理器平台上完成这次实训,还多了一层特殊意义。从龙芯的指令集到 PMON 固件,从 Loongnix 的软件源到 LoongOS 的实时内核。在这个国际技术环境日趋复杂的时代,了解并掌握自主平台的技术体系,是我们这一代计算机专业学生的责任,也是机遇。
📅 实训时间:2026年7月
