Linux platform驱动开发与设备树资源适配 - 第1节
1.1 Linux内核驱动概述
Linux内核驱动是操作系统与硬件设备之间的桥梁。它负责管理硬件设备的初始化、数据收发、中断处理以及电源管理等核心功能。在嵌入式系统中,驱动程序的稳定性和效率直接决定了产品的性能与可靠性。
核心认知:驱动开发不仅仅是“写代码控制硬件”,更是理解内核框架、总线模型、内存管理与并发同步的综合工程。掌握Platform总线与设备树,是进入现代嵌入式Linux驱动开发的必经之路。
1.1.1 内核模块基础
Linux内核采用模块化设计,允许在运行时动态加载和卸载功能代码,这些可动态加载的代码片段称为内核模块。模块机制使得内核可以保持精简,同时支持丰富的硬件驱动。
一个最简单的内核模块包含两个核心函数:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
static int __init hello_init(void)
{
printk(KERN_INFO "Hello, Kernel Module Loaded!\n");
return 0;
}
static void __exit hello_exit(void)
{
printk(KERN_INFO "Goodbye, Kernel 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");
| 元素 | 说明 |
|---|---|
module_init() |
指定模块加载时的入口函数 |
module_exit() |
指定模块卸载时的清理函数 |
__init |
标记初始化函数,加载后释放内存 |
__exit |
标记退出函数,模块内建时忽略 |
MODULE_LICENSE |
声明模块许可证,影响内核符号导出 |
编译与测试:使用
make -C /lib/modules/$(uname -r)/build M=$PWD modules 编译模块,通过 insmod 加载,rmmod 卸载,lsmod 查看已加载模块列表。
1.1.2 驱动分类
Linux内核将设备驱动划分为三大类,每一类对应不同的抽象模型和用户空间接口:
- 字符设备:以字节流方式访问,顺序读写,如串口、GPIO、I2C、SPI设备。通过
file_operations结构体实现open/read/write/ioctl等操作。 - 块设备:以数据块为单位随机访问,支持缓冲区与调度,如eMMC、NAND Flash、SD卡。通过
block_device_operations和请求队列管理I/O。 - 网络设备:负责数据包的收发,遵循网络协议栈接口,如以太网控制器、WiFi模块。通过
net_device_ops实现ndo_open/ndo_start_xmit等回调。
常见误区:初学者容易混淆“字符设备”与“块设备”的界限。判断标准不是硬件类型,而是访问模式:是否支持随机访问、是否有缓冲区。例如,一个简单的LED灯驱动一定是字符设备,而U盘驱动则是块设备。
下表从多个维度对比三类驱动的核心差异:
| 维度 | 字符设备 | 块设备 | 网络设备 |
|---|---|---|---|
| 数据单位 | 字节流 | 数据块(512B/4KB) | 数据包(MTU) |
| 访问方式 | 顺序/随机 | 随机(支持寻址) | 异步收发 |
| 用户接口 | /dev/xxx + open/read/write |
/dev/xxx + 文件系统挂载 |
socket / netlink |
| 内核接口 | file_operations |
block_device_operations + 请求队列 |
net_device_ops + NAPI |
| 典型硬件 | UART、GPIO、I2C、音频 | eMMC、SATA、NVMe | 以太网、WiFi、CAN |
1.2 驱动开发环境搭建
搭建一套完整的驱动开发环境是入门的第一步。嵌入式驱动开发通常采用交叉编译方式:在性能强大的宿主机(x86)上编写和编译代码,生成目标平台(ARM/RISC-V)的可执行文件,再部署到目标板运行。
1.2.1 交叉编译链
交叉编译链(Cross Toolchain)包含编译器、链接器、调试器等工具,针对特定目标架构生成二进制代码。常用工具链包括:
- ARM GCC:
arm-linux-gnueabihf-gcc,适用于ARM Cortex-A系列 - AArch64 GCC:
aarch64-linux-gnu-gcc,适用于ARMv8 64位 - RISC-V GCC:
riscv64-linux-gnu-gcc,适用于RISC-V平台
环境验证:安装完成后,执行
arm-linux-gnueabihf-gcc --version 查看版本信息。若输出正常,说明工具链已就绪。
1.2.2 内核源码树
驱动模块的编译依赖内核源码树(Kernel Source Tree),它提供了内核头文件、Makefile规则以及模块编译框架。获取内核源码树的两种方式:
- 下载官方内核:从
kernel.org下载对应版本源码,解压后配置编译。 - 使用板级SDK:厂商提供的BSP包中通常包含预配置的内核源码树,可直接用于模块编译。
编译内核模块的标准Makefile模板:
# Makefile for external kernel module
obj-m := hello.o
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
all:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
clean:
$(MAKE) -C $(KERNELDIR) M=$(PWD) clean
| 变量 | 含义 |
|---|---|
obj-m |
指定要编译为模块的目标文件 |
KERNELDIR |
内核源码树路径,指向已编译的内核 |
M=$(PWD) |
告诉内核构建系统当前模块源码所在目录 |
避坑指南:内核源码树必须预先编译完成,且版本与目标运行内核一致。使用
uname -r 确认版本号,避免因版本不匹配导致模块加载失败(Invalid module format 错误)。
1.2.3 开发环境清单
下表汇总了搭建驱动开发环境所需的软件组件及其用途:
| 组件 | 示例 | 用途 |
|---|---|---|
| 交叉编译器 | arm-linux-gnueabihf-gcc |
将C源码编译为目标架构的二进制 |
| 内核源码树 | linux-5.10 |
提供内核头文件与模块编译框架 |
| 设备树编译器 | dtc |
将DTS编译为DTB二进制 |
| 根文件系统 | Buildroot / Yocto | 提供目标板运行时的用户空间环境 |
| 调试工具 | gdb-multiarch / kgdb |
内核模块源码级调试 |
学习建议:初学者可以先在x86宿主机上使用
make modules_prepare 准备内核头文件,直接编译并加载模块到本地内核进行验证,待熟悉流程后再切换到交叉编译环境。