1. 课程导入
各位同学,欢迎来到这门实战课。我是你们的老朋友,一个在嵌入式Linux驱动领域摸爬滚打了十几年的工程师。今天咱们先不急着写代码,把地基夯实了,后面盖楼才稳当。
这一章,说白了就是“热身”。我会带大家快速回顾Linux内核模块开发、字符设备驱动框架,以及Makefile和Kconfig构建系统。别觉得基础,我见过太多项目后期出问题,都是因为基础概念没吃透。
1.1 为什么还要学“基础”?
你可能会想:“模块开发、字符设备,这些我早就会了。” 嗯,我理解。但咱们这门课叫《分层设计驱动分离与中间件集成调试优化实战》,后面会涉及复杂的软件架构。如果连最底层的模块加载、卸载、设备号分配都模棱两可,那分层设计时很容易把层与层之间的依赖搞乱。
我个人习惯,每接手一个新项目,第一件事就是重新审视内核模块的入口和出口函数。这就像盖房子前先看地基图纸,虽然枯燥,但能救命。
1.2 Linux内核模块开发基础
内核模块,是Linux动态扩展内核能力的一种机制。它允许我们在不重新编译整个内核的情况下,加载或卸载驱动代码。
1.2.1 模块的“三要素”
每个内核模块,都必须包含三个基本部分:
- 模块入口函数:通常是
module_init()宏指定的函数。模块被加载时,内核会调用它。 - 模块出口函数:通常是
module_exit()宏指定的函数。模块被卸载时,内核会调用它。 - 模块许可证声明:
MODULE_LICENSE("GPL")。没有这个,内核会认为你是“脏”模块,有些内核符号你无法使用。
我曾经在一个项目中,因为忘记加 MODULE_LICENSE,导致内核拒绝加载我写的加密驱动。排查了半天,最后发现是许可证没声明。这种低级错误,希望大家别犯。
1.2.2 一个最简单的模块示例
咱们直接看代码。这是你能写出的最简驱动,但它五脏俱全。
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
static int __init my_driver_init(void)
{
printk(KERN_INFO "Hello, Kernel! My driver is loaded.\n");
return 0;
}
static void __exit my_driver_exit(void)
{
printk(KERN_INFO "Goodbye, Kernel! My driver is unloaded.\n");
}
module_init(my_driver_init);
module_exit(my_driver_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple example Linux kernel module");
注意 __init 和 __exit 这两个宏。它们告诉内核:这些函数只在初始化或退出时使用,之后可以释放掉内存。这是内核的一种优化手段。
printk,但生产环境一定要去掉。我习惯在开发阶段用 pr_debug,配合动态调试,非常灵活。
1.3 字符设备驱动框架
字符设备,是Linux驱动里最基础、最常用的一类设备。它按字节流读写数据,比如串口、GPIO、I2C设备等。
1.3.1 核心数据结构:struct file_operations
这个结构体,是字符设备驱动的“灵魂”。它定义了用户空间程序如何操作你的设备:
| 成员函数 | 说明 | 我常用的场景 |
|---|---|---|
open |
打开设备时调用 | 初始化硬件、分配资源 |
release |
关闭设备时调用 | 释放资源、关闭硬件 |
read |
从设备读取数据 | 从硬件FIFO拷贝数据到用户空间 |
write |
向设备写入数据 | 将用户数据写入硬件寄存器 |
unlocked_ioctl |
设备控制命令 | 设置波特率、配置模式等 |
1.3.2 设备号与设备节点
每个字符设备在内核中都有一个唯一的编号:主设备号 和 次设备号。
- 主设备号:标识设备对应的驱动程序。
- 次设备号:标识同一个驱动管理的不同设备实例。
用户空间通过 /dev 目录下的设备节点文件来访问驱动。比如 /dev/ttyS0。创建节点用 mknod 命令,或者让 udev 自动创建。
alloc_chrdev_region() 动态分配,再也没出过问题。建议你也用动态分配。
1.3.3 字符设备注册流程
写一个字符设备驱动,通常遵循以下步骤:
- 分配设备号:使用
register_chrdev_region()或alloc_chrdev_region()。 - 初始化
cdev结构体:使用cdev_init(),并关联file_operations。 - 添加设备到系统:使用
cdev_add()。 - 创建设备类与设备节点:使用
class_create()和device_create()(推荐,自动生成/dev节点)。 - 实现
file_operations中的函数:这是你的业务逻辑。
下面这张图,展示了整个流程和核心组件的关系:
1.4 Makefile与Kconfig构建系统
驱动写好了,怎么编译?怎么集成到内核源码树里?这就轮到Makefile和Kconfig登场了。
1.4.1 驱动模块的Makefile
一个典型的外部模块Makefile长这样:
obj-m := my_driver.o
my_driver-objs := main.o utils.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:告诉内核构建系统,这个文件要编译成模块(.ko)。my_driver-objs:如果你的模块由多个源文件组成,用这个变量列出所有目标文件。KERNELDIR:指向内核源码树或头文件目录。我一般用uname -r自动获取当前内核版本。
debug 目标,用 EXTRA_CFLAGS += -DDEBUG 开启调试打印。这样开发时方便,发布时直接 make 就能关掉。
1.4.2 Kconfig:让驱动可配置
Kconfig文件用于内核的配置系统(比如 make menuconfig)。它定义了你的驱动是否编译、编译进内核还是编译为模块。
一个简单的Kconfig条目:
config MY_DRIVER
tristate "My Custom Driver Support"
depends on I2C
help
This is a custom driver for my hardware.
Say Y here if you want to build it into the kernel.
Say M if you want to build it as a module.
tristate:三态选项,可选 Y(编译进内核)、M(编译为模块)、N(不编译)。depends on:依赖关系。比如你的驱动依赖I2C子系统,就写depends on I2C。help:帮助信息,给用户看的。
Kconfig和Makefile配合使用。当用户在 menuconfig 中选择了 MY_DRIVER,内核构建系统会自动设置 CONFIG_MY_DRIVER 宏。然后在Makefile里,你可以这样写:
obj-$(CONFIG_MY_DRIVER) += my_driver.o
这样,只有配置了该驱动,才会编译它。
depends on,导致驱动在menuconfig里死活不显示。后来发现是依赖的子系统名字写错了。记住,depends on 后面的名字必须跟对应子系统的Kconfig里的 config 名字一致。
1.5 本章小结
这一章,我们快速回顾了三个核心基础:
- 内核模块:入口/出口函数、许可证声明。
- 字符设备框架:
file_operations、设备号、注册流程。 - 构建系统:Makefile编写、Kconfig配置。
这些内容,是后面所有分层设计、驱动分离、中间件集成的基石。别觉得简单,我每次写新驱动,都会回头看看这些基础代码,确保没有遗漏。