这份笔记的核心逻辑在于 "分层设计" :将驱动框架 (和内核打交道)与硬件操作(和寄存器打交道)彻底剥离。
Linux 字符设备驱动开发笔记:分层设计与实现细节
0. 核心架构设计 (The Big Picture)
整个驱动项目被拆分为三个核心层次,这种设计是为了代码复用 和逻辑解耦。
- 上层(用户空间) :
led_test.c-> 负责业务逻辑(什么时候开,什么时候关)。 - 中层(驱动框架) :
led_drv.c-> 负责向内核注册设备,提供标准的open/write外设接口,不涉及具体硬件寄存器。 - 下层(硬件操作) :
led_opr.c-> 负责具体的单板硬件实现(ioremap, 读写寄存器),不涉及与内核机制交互。 - 接口(契约) :
led_opr.h-> 定义中层和下层交互的结构体struct led_operations。
1. 驱动框架层 (led_drv.c)
文件角色:这是驱动的"管家"。它不知道 LED 怎么亮,它只知道如何接待用户(APP)的请求,并把请求转发给具体的硬件操作函数。
核心逻辑 :负责向内核注册"我是谁",并提供标准的"文件操作接口"。它不包含 任何具体的寄存器操作代码,而是通过调用 p_led_opr 指针来指挥硬件层。
1.1 关键结构体与变量
c
/* 1. 全局变量定义 */
static struct class *led_class; // 用于自动创建设备节点的类
struct led_operations *p_led_opr; // [关键] 指向硬件操作结构体的指针
/* 2. file_operations 结构体:连接用户空间与内核空间 */
static struct file_operations led_drv = {
.owner = THIS_MODULE,
.open = led_drv_open, // 对应应用层的 open()
.read = led_drv_read, // 对应应用层的 read()
.write = led_drv_write, // 对应应用层的 write()
.release = led_drv_close, // 对应应用层的 close()
};
1.2 关键函数实现
(1) led_drv_open:初始化硬件
当应用程序调用 open 时触发。
c
static int led_drv_open (struct inode *node, struct file *file)
{
/* 核心步骤1:获取次设备号 */
// iminor 从 inode 中提取次设备号 (0, 1, 2...),代表具体是哪个 LED
int minor = iminor(node);
/* 核心步骤2:调用底层初始化 */
// 驱动层只发号施令,不知道具体怎么初始化硬件寄存器
p_led_opr->init(minor);
return 0;
}
(2) led_drv_write:控制硬件
当应用程序调用 write 时触发。
c
static ssize_t led_drv_write (struct file *file, const char __user *buf, size_t size, loff_t *offset)
{
char status;
struct inode *inode = file_inode(file);
int minor = iminor(inode); // 获取次设备号
/* 核心步骤1:安全拷贝数据 */
// 用户空间内存(buf)不能直接访问,必须用 copy_from_user
// 把用户传来的 1 字节数据拷贝到内核栈变量 status 中
copy_from_user(&status, buf, 1); // 打通内核与用户的数据传输
/* 核心步骤2:调用底层控制 */
// 根据次设备号和状态(on/off)控制 LED
p_led_opr->ctl(minor, status);
return 1;
}
(3) led_init:入口函数 (insmod 时调用)
这是整个驱动的起点,完成了最关键的"三步走"。
c
static int __init led_init(void)
{
int i;
/* 第一步:注册字符设备 */
// 参数 0 让内核自动分配主设备号 major
// 第三个参数是内核对字符设备的操作结构体file_operations
major = register_chrdev(0, "100ask_led", &led_drv);
/* 第二步:创建类 (自动创建设备节点的前提) */
// 创建一个类 (在 /sys/class/ 下生成 100ask_led_class 目录)
led_class = class_create(THIS_MODULE, "100ask_led_class");
/* 第三步:获取硬件操作对象 [最关键的连接点] */
// 调用 led_opr.c 提供的函数,获取硬件操作结构体地址
p_led_opr = get_board_led_opr();
/* 第四步:循环创建设备节点 */
// 根据 led_opr 中定义的 LED 数量 (num),创建 /dev/100ask_led0, /dev/100ask_led1...
for (i = 0; i < p_led_opr->num; i++)
device_create(led_class, NULL, MKDEV(major, i), NULL, "100ask_led%d", i);
return 0;
}
(4) led_exit:出口函数 (rmmod 时调用)
必须按照与 init 相反的顺序销毁资源。
c
static void __exit led_exit(void)
{
int i;
/* 1. 销毁设备节点 */
for (i = 0; i < p_led_opr->num; i++)
device_destroy(led_class, MKDEV(major, i));
/* 2. 销毁类 */
class_destroy(led_class);
/* 3. 注销字符设备 */
unregister_chrdev(major, "100ask_led");
}
2. 接口定义层 (led_opr.h)
核心逻辑:定义驱动层和硬件层通信的"标准合同"。
c
struct led_operations {
int num; // LED 的总数量
int (*init) (int which); // 函数指针:初始化指定 LED
int (*ctl) (int which, char status); // 函数指针:控制指定 LED 亮/灭
};
// 暴露给驱动层调用的函数原型
struct led_operations *get_board_led_opr(void);
3. 硬件操作层 (led_opr.c)
核心逻辑 :脏活累活都在这。负责具体的物理地址映射和寄存器读写。代码与具体的开发板(如 i.MX6ULL)强相关。
3.1 物理寄存器地址映射 (ioremap)
由于启用了 MMU,驱动不能直接操作物理地址,必须通过 ioremap 映射为虚拟地址。
c
/* 这里的地址查阅芯片手册得到 */
static int board_demo_led_init (int which)
{
if (which == 0) // 针对 LED0 的初始化
{
/* 单例模式:防止多次映射 */
if (!CCM_CCGR1)
{
// 时钟控制寄存器映射
CCM_CCGR1 = ioremap(0x20C406C, 4);
// GPIO 数据寄存器映射
GPIO5_DR = ioremap(0x020AC000 + 0, 4);
// ... 其他寄存器映射 ...
}
/* 寄存器操作:读-改-写 */
// 1. 使能 GPIO5 时钟 (设置 bit[31:30] 为 11)
*CCM_CCGR1 |= (3<<30);
// 2. 设置 GPIO5_IO03 为输出模式 (bit 3 置 1)
*GPIO5_GDIR |= (1<<3);
}
return 0;
}
3.2 硬件控制实现
c
// led_drv.c中的写好file_operations中的write调用了:p_led_opr->ctl(minor, status);
static int board_demo_led_ctl (int which, char status)
{
if (which == 0)
{
if (status) /* 这里的逻辑取决于电路图,假设低电平点亮 */
{
// 输出低电平:清除 bit 3
*GPIO5_DR &= ~(1<<3);
}
else
{
// 输出高电平:设置 bit 3
*GPIO5_DR |= (1<<3);
}
}
return 0;
}
3.3 暴露接口实现
c
/* 填充结构体 */
static struct led_operations board_demo_led_opr = {
.num = 1,
.init = board_demo_led_init,
.ctl = board_demo_led_ctl,
};
/* 提供给驱动层的获取函数 */
struct led_operations *get_board_led_opr(void)
{
return &board_demo_led_opr;
}
4. 应用测试层 (led_test.c)
核心逻辑:用户空间的命令行工具。
c
int main(int argc, char **argv)
{
/* 1. 参数检查 */
if (argc != 3) { ... }
/* 2. 打开设备文件 */
// argv[1] 是 "/dev/100ask_led0"
fd = open(argv[1], O_RDWR);
/* 3. 写入控制命令 */
if (0 == strcmp(argv[2], "on")) {
status = 1;
write(fd, &status, 1); // 触发驱动的 write -> ctl
} else {
status = 0;
write(fd, &status, 1);
}
// ...
}
5. 编译构建 (Makefile)
核心逻辑 :将多个 .c 文件编译并链接成一个 .ko 模块。
Makefile
# 1. 链接规则 [关键点]
# 下面两行告诉内核:请把 led_drv.o 和 led_opr.o 链接在一起,
# 生成一个名为 100ask_led.ko 的模块
# -m即-modules,可加载内核模块(External Modules)
100ask_led-y := led_drv.o led_opr.o
obj-m += 100ask_led.o
# 2. 编译命令
all:
# -C 指定内核源码路径,M=`pwd` 回到当前目录编译模块
make -C $(KERN_DIR) M=`pwd` modules
# 编译测试程序
$(CROSS_COMPILE)gcc -o led_test led_test.c