本文将系统梳理Linux设备驱动开发的完整知识体系,涵盖启动流程、内核编译、驱动实现和高级驱动框架,为嵌入式开发者提供一站式学习路径。
一、Linux驱动开发基础架构
1.1 硬件与存储体系
Linux驱动开发的核心是硬件抽象,理解硬件架构是开发的基础。
存储设备分类与特性
| 存储类型 | 特点 | 常见形式 | 访问方式 | 数据保持 |
|---|---|---|---|---|
| RAM | 随机存取,速度快,容量小 | SRAM/DRAM/SDRAM/DDR | 线性访问 | 掉电丢失 |
| ROM | 只读存储,速度慢,容量大 | EPROM/EEPROM/Flash | 非线性访问 | 掉电不丢失 |
| Flash | 结合两者优点 | NAND/NOR/eMMC | 块/页访问 | 掉电不丢失 |
Linux系统中的角色分工
硬件层:ARM芯片 + RAM + ROM + 外设(网卡、串口等)
↓
Bootloader:硬件初始化与内核引导
↓
Linux内核:核心资源管理(文件/进程/网络/内存/设备)
↓
根文件系统:应用程序与配置文件的运行环境
↓
用户空间:应用程序与系统服务
1.2 ARM开发模式实践
开发环境搭建
典型的嵌入式开发采用交叉编译模式:
-
宿主机(PC):Ubuntu系统,用于代码编译
-
目标机(开发板):ARM开发板,运行Linux系统
-
连接方式:
-
UART串口:用于控制台调试
-
以太网:用于文件传输(NFS)和远程调试
-
NFS挂载配置示例
# 在开发板上挂载主机的NFS目录
mount -o nolock,nfsvers=3 192.168.1.3:/home/linux/nfs /mnt
# 配置说明:
# 192.168.1.3 - Ubuntu主机的IP地址
# /home/linux/nfs - 主机上共享的目录(需在/etc/exports中配置)
# /mnt - 开发板上的挂载点
二、深入理解Linux启动流程
2.1 IMX6ULL启动过程详解
SD卡启动流程(完整版)
// 伪代码表示的启动流程
void imx6ull_boot_from_sd(void) {
// 阶段1:ROM启动程序
// ---------------------------------
// 1. 芯片上电,执行内部ROM代码
// 2. 根据BOOT_MODE引脚选择启动设备(SD卡)
// 3. 从SD卡加载Bootloader前部到内部RAM(96KB)
// 阶段2:Bootloader初始化
// ---------------------------------
// 4. Bootloader前部程序执行:
// - 初始化DDR内存控制器
// - 将Bootloader后部搬移到DDR
// - 跳转到DDR执行后部程序
// 5. Bootloader后部程序执行:
// - 初始化串口、网卡等外设
// - 设置环境变量
// 阶段3:内核加载
// ---------------------------------
// 6. 从SD卡加载zImage到内存0x80800000
// 7. 设置启动参数(bootargs)
// 8. 跳转到0x80800000执行内核
// 阶段4:内核启动
// ---------------------------------
// 9. 内核解压并初始化
// 10. 挂载SD卡上的根文件系统
// 11. 执行init进程,启动用户空间
}
网络启动(TFTP+NFS)配置
# U-Boot环境变量配置
setenv ipaddr 192.168.1.100 # 开发板IP
setenv serverip 192.168.1.3 # TFTP服务器IP
setenv ethaddr 00:04:9f:04:d2:35 # MAC地址
# 内核启动参数
setenv bootargs console=ttymxc0,115200 \
root=/dev/nfs \
nfsroot=192.168.1.3:/home/linux/nfs/rootfs,nfsvers=3 \
ip=192.168.1.100 \
init=/linuxrc
# TFTP下载内核和设备树
tftp 0x80800000 zImage # 下载内核到内存
tftp 0x83000000 imx6ull.dtb # 下载设备树
# 启动内核
bootz 0x80800000 - 0x83000000
Bootloader的关键初始化操作
// Bootloader初始化的核心任务
void bootloader_init(void) {
// 1. CPU模式初始化
set_cpu_mode(SVC_MODE); // 设置管理模式
// 2. 异常向量表设置
setup_exception_vector_table();
// 3. 堆栈指针初始化
init_stack_pointers();
// 4. 关键外设关闭
disable_interrupts(); // 关中断
disable_watchdog(); // 关看门狗
disable_cache(); // 关Cache
disable_mmu(); // 关MMU
// 5. 内存控制器初始化
init_ddr_controller();
// 6. 外设初始化
init_uart(); // 串口,用于调试输出
init_network(); // 网卡,用于网络启动
// 7. 内核加载与启动
load_kernel_to_memory();
setup_boot_parameters();
jump_to_kernel(); // 控制权移交内核
}
三、Linux内核编译与配置系统
3.1 Kconfig与Makefile协同工作
内核配置系统架构
Kconfig文件 (配置定义)
↓
make menuconfig (图形配置界面)
↓
.config文件 (配置结果存储)
↓
Makefile文件 (编译规则)
↓
目标文件.o (编译输出)
配置选项示例
# drivers/char/Kconfig 中的配置定义
config DEMO_DRIVER
bool "Demo driver support"
default n
help
This is a demo driver for learning.
Say Y here if you want to enable it.
# drivers/char/Makefile 中的编译规则
obj-$(CONFIG_DEMO_DRIVER) += demo.o
3.2 内核编译完整流程
# 步骤1:准备内核源码
tar -xvf linux-4.1.15.tar.gz
chmod 0777 linux-4.1.15 -R # 修改权限避免权限问题
# 步骤2:应用默认配置
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- imx_v7_defconfig
# 步骤3:自定义配置
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- menuconfig
# 步骤4:编译内核
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- all -j16
# 步骤5:获取编译结果
# arch/arm/boot/zImage - 压缩内核镜像
# arch/arm/boot/Image - 原始内核镜像
# arch/arm/boot/uImage - U-Boot格式镜像
# 步骤6:安装测试
cp arch/arm/boot/zImage /tftpboot/ # 复制到TFTP目录
3.3 内核镜像格式详解
// 三种内核镜像格式对比
/*
1. Image - 原始镜像
+-----------------+
| 内核二进制代码 | 直接可执行,但体积大
+-----------------+
2. zImage - 压缩镜像
+-----------------+
| 解压程序(头部) | 自解压格式,减小传输体积
|-----------------|
| 压缩的内核代码 |
+-----------------+
3. uImage - U-Boot镜像
+-----------------+
| 64字节U-Boot头 | 包含加载地址、入口点等信息
|-----------------|
| zImage |
+-----------------+
*/
3.4 向内核添加新驱动模块
# 示例:添加demo驱动到drivers/char目录
# 1. 创建驱动源文件
vim drivers/char/demo.c
# 2. 修改Makefile,添加编译规则
# 在drivers/char/Makefile末尾添加:
obj-$(CONFIG_DEMO) += demo.o
# 3. 修改Kconfig,添加配置选项
# 在drivers/char/Kconfig中添加:
config DEMO
tristate "Demo device support"
default n
help
This driver is for demo purpose.
# 4. 配置并编译
make menuconfig # 在Character devices中找到并启用DEMO
make zImage # 重新编译内核
四、Linux设备驱动开发实战
4.1 设备驱动分类与特点
| 驱动类型 | 数据访问方式 | 典型设备 | 设备号管理 |
|---|---|---|---|
| 字符设备 | 字节流,顺序访问 | 串口、键盘、LED | 主/次设备号 |
| 块设备 | 数据块,随机访问 | 硬盘、SD卡、U盘 | 主/次设备号 |
| 网络设备 | 数据包,协议栈 | 网卡、WiFi | 按接口名管理 |
4.2 字符设备驱动开发模板
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#define DEVICE_NAME "demo"
#define DEVICE_COUNT 1
#define DEVICE_MAJOR 0 // 0表示动态分配
static dev_t dev_num; // 设备号
static struct cdev demo_cdev; // 字符设备结构
static struct class *demo_class; // 设备类
static struct device *demo_device; // 设备节点
// 文件操作函数集
static struct file_operations demo_fops = {
.owner = THIS_MODULE,
.open = demo_open,
.read = demo_read,
.write = demo_write,
.release = demo_release,
.unlocked_ioctl = demo_ioctl,
};
// 驱动初始化
static int __init demo_init(void)
{
int ret;
// 1. 申请设备号(动态分配)
ret = alloc_chrdev_region(&dev_num, 0, DEVICE_COUNT, DEVICE_NAME);
if (ret < 0) {
printk(KERN_ERR "Failed to allocate device number\n");
return ret;
}
printk(KERN_INFO "Device major number = %d\n", MAJOR(dev_num));
// 2. 初始化cdev结构
cdev_init(&demo_cdev, &demo_fops);
demo_cdev.owner = THIS_MODULE;
// 3. 添加cdev到系统
ret = cdev_add(&demo_cdev, dev_num, DEVICE_COUNT);
if (ret < 0) {
printk(KERN_ERR "Failed to add cdev\n");
goto err_cdev;
}
// 4. 创建设备类(自动在/sys/class创建)
demo_class = class_create(THIS_MODULE, DEVICE_NAME);
if (IS_ERR(demo_class)) {
ret = PTR_ERR(demo_class);
goto err_class;
}
// 5. 创建设备节点(自动在/dev创建)
demo_device = device_create(demo_class, NULL, dev_num, NULL, DEVICE_NAME);
if (IS_ERR(demo_device)) {
ret = PTR_ERR(demo_device);
goto err_device;
}
printk(KERN_INFO "Demo driver loaded successfully\n");
return 0;
err_device:
class_destroy(demo_class);
err_class:
cdev_del(&demo_cdev);
err_cdev:
unregister_chrdev_region(dev_num, DEVICE_COUNT);
return ret;
}
// 驱动退出
static void __exit demo_exit(void)
{
device_destroy(demo_class, dev_num);
class_destroy(demo_class);
cdev_del(&demo_cdev);
unregister_chrdev_region(dev_num, DEVICE_COUNT);
printk(KERN_INFO "Demo driver unloaded\n");
}
module_init(demo_init);
module_exit(demo_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple demo driver");
4.3 设备号管理与节点创建
设备号组成解析
// 设备号是32位无符号整数
typedef u32 dev_t;
// 设备号分解宏
#define MAJOR(dev) ((unsigned int) ((dev) >> 20)) // 高12位:主设备号
#define MINOR(dev) ((unsigned int) ((dev) & 0xfffff)) // 低20位:次设备号
#define MKDEV(ma, mi) (((ma) << 20) | (mi)) // 合成设备号
// 设备号范围
// 主设备号:0-4095(12位)
// 次设备号:0-1048575(20位)
手动创建设备节点
# 查看已注册的设备号
cat /proc/devices
# 输出示例:
# Character devices:
# 1 mem
# 4 tty
# 5 /dev/tty
# 10 misc
# 89 i2c
# 250 demo
# 手动创建设备节点
mknod /dev/demo1 c 250 0 # 主设备号250,次设备号0
mknod /dev/demo2 c 250 1 # 主设备号250,次设备号1
# 节点权限设置
chmod 666 /dev/demo1 # 所有用户可读写
4.4 ioctl命令编码规范
Linux内核提供了标准的ioctl命令编码宏:
#include <linux/ioctl.h>
// ioctl命令编码格式(32位)
// +---------+--------+--------+----------------+
// | 31...30 | 29...16| 15...8 | 7...0 |
// +---------+--------+--------+----------------+
// | dir | size | type | nr (number) |
// +---------+--------+--------+----------------+
// 方向位定义
#define _IOC_NONE 0U // 无数据传输
#define _IOC_WRITE 1U // 写数据(从用户到内核)
#define _IOC_READ 2U // 读数据(从内核到用户)
// 常用宏定义
#define _IO(type, nr) _IOC(_IOC_NONE, (type), (nr), 0)
#define _IOR(type, nr, size) _IOC(_IOC_READ, (type), (nr), sizeof(size))
#define _IOW(type, nr, size) _IOC(_IOC_WRITE, (type), (nr), sizeof(size))
#define _IOWR(type, nr, size) _IOC(_IOC_READ|_IOC_WRITE, (type), (nr), sizeof(size))
// 示例:定义LED控制命令
#define LED_MAGIC 'L'
#define LED_ON _IO(LED_MAGIC, 0) // 打开LED,无参数
#define LED_OFF _IO(LED_MAGIC, 1) // 关闭LED,无参数
#define SET_BRIGHT _IOW(LED_MAGIC, 2, int) // 设置亮度,传入int参数
#define GET_BRIGHT _IOR(LED_MAGIC, 3, int) // 获取亮度,返回int参数
五、Platform驱动模型详解
5.1 Platform模型架构
Platform驱动模型是Linux内核为**片上系统(SoC)**外设设计的驱动框架:
总线类型(bus_type)
├── I2C总线(I2C)
├── SPI总线(SPI)
└── Platform总线(虚拟总线)
├── Platform驱动(Driver)
│ ├── 设备操作方法
│ ├── 驱动注册(probe)
│ └── 驱动卸载(remove)
└── Platform设备(Device)
├── 设备资源(引脚、寄存器等)
├── 设备名称
└── 设备私有数据
5.2 Platform设备与驱动结构
// Platform设备结构体
struct platform_device {
const char *name; // 设备名称,用于匹配驱动
int id; // 设备ID
struct device dev; // 基础设备结构
u32 num_resources; // 资源数量
struct resource *resource; // 资源数组(内存、IRQ等)
// ... 其他成员
};
// Platform驱动结构体
struct platform_driver {
int (*probe)(struct platform_device *); // 探测函数
int (*remove)(struct platform_device *); // 移除函数
void (*shutdown)(struct platform_device *); // 关机函数
int (*suspend)(struct platform_device *, pm_message_t); // 挂起
int (*resume)(struct platform_device *); // 恢复
struct device_driver driver; // 基础驱动结构
const struct platform_device_id *id_table; // 设备ID表
};
// 资源结构体(描述硬件资源)
struct resource {
resource_size_t start; // 资源起始地址
resource_size_t end; // 资源结束地址
const char *name; // 资源名称
unsigned long flags; // 资源类型标志
// IORESOURCE_MEM - 内存资源
// IORESOURCE_IRQ - 中断资源
// IORESOURCE_IO - IO端口资源
};
5.3 Platform驱动开发示例
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/io.h>
// 设备资源定义
static struct resource demo_resources[] = {
[0] = {
.start = 0x0209C000, // 寄存器物理地址
.end = 0x0209C003,
.flags = IORESOURCE_MEM, // 内存资源
.name = "demo_regs",
},
[1] = {
.start = 100, // 中断号
.end = 100,
.flags = IORESOURCE_IRQ, // 中断资源
.name = "demo_irq",
},
};
// Platform设备定义
static struct platform_device demo_device = {
.name = "demo_device", // 设备名称
.id = -1, // 自动分配ID
.num_resources = ARRAY_SIZE(demo_resources),
.resource = demo_resources,
};
// Platform驱动probe函数
static int demo_probe(struct platform_device *pdev)
{
struct resource *res;
void __iomem *regs;
int irq_num;
printk(KERN_INFO "Demo device probed\n");
// 1. 获取内存资源
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
if (!res) {
printk(KERN_ERR "Failed to get memory resource\n");
return -ENODEV;
}
// 2. 映射物理地址到虚拟地址
regs = devm_ioremap_resource(&pdev->dev, res);
if (IS_ERR(regs)) {
return PTR_ERR(regs);
}
// 3. 获取中断资源
res = platform_get_resource(pdev, IORESOURCE_IRQ, 0);
if (res) {
irq_num = res->start;
// 注册中断处理函数
// request_irq(irq_num, demo_isr, 0, "demo", pdev);
}
// 4. 初始化字符设备驱动(复用第四部分的代码)
// ...
return 0;
}
// Platform驱动remove函数
static int demo_remove(struct platform_device *pdev)
{
printk(KERN_INFO "Demo device removed\n");
// 注销字符设备驱动
// ...
return 0;
}
// Platform驱动定义
static struct platform_driver demo_driver = {
.probe = demo_probe,
.remove = demo_remove,
.driver = {
.name = "demo_device", // 必须与设备名称匹配
.owner = THIS_MODULE,
},
};
// 模块初始化和退出
static int __init demo_platform_init(void)
{
int ret;
// 注册Platform设备
ret = platform_device_register(&demo_device);
if (ret) {
printk(KERN_ERR "Failed to register platform device\n");
return ret;
}
// 注册Platform驱动
ret = platform_driver_register(&demo_driver);
if (ret) {
platform_device_unregister(&demo_device);
printk(KERN_ERR "Failed to register platform driver\n");
return ret;
}
return 0;
}
static void __exit demo_platform_exit(void)
{
platform_driver_unregister(&demo_driver);
platform_device_unregister(&demo_device);
}
module_init(demo_platform_init);
module_exit(demo_platform_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Platform driver demo");
5.4 Platform模型匹配机制
Platform驱动与设备的匹配主要通过以下几种方式:
// 1. 名称匹配(最常用)
// 设备名称与驱动名称完全一致
platform_device.name = "demo_device";
platform_driver.driver.name = "demo_device";
// 2. ID表匹配
static struct platform_device_id demo_id_table[] = {
{ "demo_device", 0 }, // 名称和ID
{ "demo_device_v2", 1 },
{ } // 结束标记
};
// 3. 设备树匹配(现代Linux常用)
// 设备树中:
// demo_device {
// compatible = "vendor,demo-device";
// reg = <0x0209C000 0x1000>;
// interrupts = <100>;
// };
// 驱动中:
static const struct of_device_id demo_of_match[] = {
{ .compatible = "vendor,demo-device" },
{ }
};
MODULE_DEVICE_TABLE(of, demo_of_match);
六、开发工具与调试技巧
6.1 ctags代码导航工具
# 在内核源码目录生成tags文件
ctags -R .
# 常用Vim命令
# 光标置于符号上,按Ctrl+]跳转到定义
# 按Ctrl+o跳回之前位置
# 按Ctrl+t跳转到标签栈中的上一个位置
# 生成cscope数据库(更强大)
cscope -Rbkq
6.2 内核调试技巧
# 1. 打印调试信息
printk(KERN_DEBUG "Debug info: value = %d\n", value);
# 2. 动态调试
echo "file demo.c +p" > /sys/kernel/debug/dynamic_debug/control
# 3. /proc接口调试
# 创建proc文件查看驱动状态
proc_create("demo_status", 0644, NULL, &demo_proc_fops);
# 4. sysfs接口调试
# 通过sysfs导出驱动参数
device_create_file(dev, &dev_attr_debug_level);
七、总结与学习路径
通过本文的系统梳理,我们建立了完整的Linux驱动开发知识体系:
-
基础概念:理解硬件架构、存储类型和Linux系统组成
-
启动流程:掌握从Bootloader到内核启动的完整过程
-
内核编译:熟悉内核配置系统和编译方法
-
驱动开发:掌握字符设备驱动的完整实现
-
高级框架:理解Platform等现代驱动模型
学习建议:
-
从简单的字符设备驱动开始实践
-
使用QEMU等模拟器进行实验,降低硬件依赖
-
阅读内核源码中相似的驱动作为参考
-
参与开源项目,积累实际开发经验
下一步学习方向:
-
设备树(Device Tree)的使用
-
中断处理与并发控制
-
DMA与内存管理
-
电源管理框架
-
驱动调试与性能优化
Linux驱动开发是一个需要理论与实践并重的领域。通过系统学习和持续实践,你将能够为各种硬件设备开发高质量的驱动程序,深入理解Linux内核的工作机制。