1. 嵌入式Linux驱动开发概述
各位同学,欢迎来到《全志T113 ARM嵌入式Linux外设驱动开发实战》的第一章。我是你们这门课的主讲工程师。在正式开始动手写代码之前,我觉得有必要先聊聊“我们到底在做什么”。
嵌入式Linux驱动开发,说白了就是让Linux内核认识并控制你板子上的那些硬件外设。比如让CPU通过I2C总线去读一个温度传感器的数值,或者通过SPI接口去刷新一块LCD屏幕。你写的驱动,就是硬件和操作系统之间的“翻译官”。
我刚开始接触这行的时候,也踩过不少坑。记得有一次,我辛辛苦苦写了一个GPIO按键驱动,编译完加载到内核里,结果按下去系统直接死机。后来查了半天,发现是中断处理函数里调了一个会睡眠的函数。嗯,这种问题,你光看代码是看不出来的,得对内核的机制有深刻理解才行。
所以,这一章我们先打好地基。把嵌入式系统、Linux内核架构、开发环境搭建这些概念理清楚,最后再亲手编译加载一个“Hello World”驱动模块。走完这一遍,你就算正式入门了。
1.1 嵌入式系统简介
什么是嵌入式系统?我的理解是:专门为特定任务而设计的计算机系统。它不像你的PC机那样可以装各种软件、打游戏、写文档。它通常资源有限(CPU主频低、内存小、存储少),但对功耗、实时性、可靠性要求极高。
你想想看,你家里的智能路由器、空调遥控器、汽车里的ECU、甚至你手上的智能手表,这些都是嵌入式系统。它们内部都有一颗微处理器或者微控制器,跑着裁剪过的操作系统(或者干脆裸机),专门干一件事。
我们这门课用的全志T113,就是一颗典型的嵌入式SoC(System on Chip)。它把ARM Cortex-A7 CPU、GPU、DDR控制器、各种外设控制器(I2C、SPI、UART、USB等)都集成在一个芯片里。你只需要给它配上内存颗粒和存储芯片,就能跑起一个完整的Linux系统。
核心要点:嵌入式开发与桌面开发最大的区别在于——交叉编译。你的开发环境(PC)和目标运行环境(开发板)是两套不同的架构。你在PC上写的代码,需要用交叉编译链编译成ARM架构的机器码,然后传到板子上运行。
1.2 Linux内核架构概览
Linux内核是一个宏内核(Monolithic Kernel),但同时也吸收了微内核的一些设计思想,比如模块化。它的核心架构可以分为几个大块:
- 进程管理:负责创建、调度、销毁进程。Linux的调度器(CFS)非常优秀,能保证多任务公平运行。
- 内存管理:管理物理内存和虚拟内存。包括页分配、slab分配器、内存映射等。
- 文件系统:Linux下“一切皆文件”。VFS(虚拟文件系统)层屏蔽了底层不同文件系统(ext4、FAT、NFS等)的差异。
- 网络协议栈:实现了TCP/IP协议族,以及各种网络设备驱动。
- 设备驱动:这是我们这门课的重点。它负责与硬件交互,向上层提供统一的接口。
下面这张图,是我画的一个简化版Linux内核架构图。你可以看到,驱动模块位于内核的最底层,直接与硬件打交道,同时通过系统调用接口向上层应用提供服务。
我的经验:刚开始学驱动开发,不要试图一下子搞懂整个内核。你只需要关注“设备驱动”这一块,以及它如何通过系统调用与用户空间交互。其他的子系统,用到的时候再去查,效率更高。
1.3 驱动开发环境搭建
环境搭建是新手最容易卡壳的地方。我见过太多同学,代码写好了,结果编译不过,或者烧录到板子上跑不起来。这里我给大家梳理一个标准流程。
1.3.1 交叉编译链
你的PC是x86架构,全志T113是ARM Cortex-A7架构。所以你不能用PC上的gcc直接编译。你需要一个交叉编译工具链。
对于全志T113,我推荐使用Linaro提供的gcc-arm-linux-gnueabihf工具链。安装方法很简单:
# 下载工具链(以Ubuntu 20.04为例)
wget https://releases.linaro.org/components/toolchain/binaries/latest-7/arm-linux-gnueabihf/gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf.tar.xz
# 解压到指定目录
sudo tar -xvf gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf.tar.xz -C /opt/
# 配置环境变量(建议写入 ~/.bashrc)
export PATH=$PATH:/opt/gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf/bin
# 验证安装
arm-linux-gnueabihf-gcc --version
注意:工具链版本必须与你的内核版本匹配。我曾经因为用了太新的工具链编译旧内核,结果链接阶段报了一堆莫名其妙的错误。全志官方SDK一般会指定工具链版本,最好用他们推荐的。
1.3.2 TFTP/NFS 网络启动
在开发阶段,你不可能每次修改驱动都重新烧写整个系统镜像。那样太慢了。我们通常使用网络启动的方式:
- TFTP:用于从服务器下载内核镜像(uImage/zImage)和设备树(.dtb)到开发板的内存中。
- NFS:用于将开发板的根文件系统挂载到PC上的一个目录。这样你在PC上修改文件,开发板立刻就能看到。
配置步骤大致如下:
- 在PC上安装并配置TFTP服务器(tftpd-hpa)和NFS服务器(nfs-kernel-server)。
- 将编译好的内核镜像和设备树文件放到TFTP的根目录(通常是 /srv/tftp)。
- 将你的根文件系统解压到一个目录(比如 /srv/nfs/rootfs),并在 /etc/exports 中导出。
- 在开发板的U-Boot中设置启动参数:
# U-Boot 环境变量示例
setenv serverip 192.168.1.100 # PC的IP
setenv ipaddr 192.168.1.10 # 开发板的IP
setenv bootcmd 'tftp 0x42000000 uImage; tftp 0x43000000 sun8i-t113.dtb; bootm 0x42000000 - 0x43000000'
setenv bootargs 'console=ttyS0,115200 root=/dev/nfs nfsroot=192.168.1.100:/srv/nfs/rootfs rw ip=192.168.1.10'
saveenv
boot
我建议:第一次配置网络启动可能会遇到各种问题(比如NFS权限、防火墙、网线没插好)。我的习惯是先用TFTP单独下载内核,如果能启动到挂载根文件系统那一步,再排查NFS的问题。分步验证,不要想一口气吃成胖子。
1.4 Hello World 驱动模块
好了,理论讲完了,我们动手写第一个驱动模块。它什么都不做,只是在加载和卸载的时候打印一句话。但麻雀虽小,五脏俱全。通过它,你可以验证你的整个开发环境是否搭建正确。
1.4.1 编写代码
创建一个文件 hello.c:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
static int __init hello_init(void)
{
printk(KERN_INFO "Hello, T113 World!\n");
printk(KERN_INFO "Module loaded successfully.\n");
return 0;
}
static void __exit hello_exit(void)
{
printk(KERN_INFO "Goodbye, T113 World!\n");
printk(KERN_INFO "Module unloaded.\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple Hello World module for T113");
1.4.2 编写Makefile
驱动模块的编译必须依赖内核源码树。Makefile 这样写:
# Makefile for hello module
obj-m := hello.o
# 指定内核源码路径,这里假设你已经解压并编译过内核
KDIR := /path/to/your/t113/linux-kernel
# 指定交叉编译工具链前缀
CROSS_COMPILE := arm-linux-gnueabihf-
all:
$(MAKE) -C $(KDIR) M=$(PWD) ARCH=arm CROSS_COMPILE=$(CROSS_COMPILE) modules
clean:
$(MAKE) -C $(KDIR) M=$(PWD) ARCH=arm CROSS_COMPILE=$(CROSS_COMPILE) clean
1.4.3 编译与加载
在PC上执行 make,你会得到 hello.ko 文件。这就是编译好的内核模块。
把它传到开发板上(通过TFTP、NFS或者scp),然后在开发板的终端里执行:
# 加载模块
insmod hello.ko
# 查看内核打印信息
dmesg | tail -5
# 你应该能看到:
# [ 1234.5678] Hello, T113 World!
# [ 1234.5679] Module loaded successfully.
# 卸载模块
rmmod hello.ko
# 再次查看
dmesg | tail -5
# [ 1234.5680] Goodbye, T113 World!
# [ 1234.5681] Module unloaded.
恭喜!如果你看到了这些打印信息,说明你的交叉编译环境、内核源码、开发板网络连接全部都是通的。你已经成功迈出了嵌入式Linux驱动开发的第一步。
避坑指南:如果你执行 insmod 后没有任何输出,或者报错“Invalid module format”,请检查以下几点:
- 内核源码版本是否与开发板上运行的内核版本完全一致?
- Makefile中的KDIR路径是否正确?
- 交叉编译链是否与编译内核时用的是同一个?
我曾经因为内核源码目录下没有执行过 make modules_prepare,导致编译出来的模块符号版本不匹配,加载失败。记得先在内核源码根目录执行一下:
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- modules_prepare
1.5 本章小结
这一章我们走完了嵌入式Linux驱动开发的完整流程:从理解系统架构,到搭建交叉编译环境,再到编写并加载第一个驱动模块。你可能会觉得内容有点多,但这些都是基本功。
我个人习惯把开发环境配置好之后,先跑一遍这个Hello World模块,确认整个链路是通的。这样后面写复杂驱动的时候,出了问题就能把范围缩小到代码逻辑本身,而不是环境问题。