20251231 - Linux 字符设备驱动开发笔记:分层设计

这份笔记的核心逻辑在于 "分层设计" :将驱动框架 (和内核打交道)与硬件操作(和寄存器打交道)彻底剥离。

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 
相关推荐
一路往蓝-Anbo3 小时前
STM32单线串口通讯实战(二):链路层核心 —— DMA环形缓冲与收发切换时序
c语言·开发语言·stm32·单片机·嵌入式硬件·物联网
Zeku4 小时前
20251230 - 为什么Linux驱动开发中必须要用到ioremap来访问硬件?
stm32·freertos·linux驱动开发·linux应用开发
QK_004 小时前
STM32--工程移植
stm32·单片机·嵌入式硬件
QK_005 小时前
STM32--DMA
stm32·单片机·嵌入式硬件
福尔摩斯张5 小时前
STM32数码管和LCD显示技术深度解析(超详细)
数据库·stm32·单片机·嵌入式硬件·mongodb
d111111111d5 小时前
STM32 DMA传输配置详解:数据宽度与传输方向设置指南
笔记·stm32·单片机·嵌入式硬件·学习
Archie_IT5 小时前
基于STM32F103C8T6标准库的OLED显示屏中文汉字显示实现_资料编号39
stm32·单片机·嵌入式硬件·mcu·硬件工程·信息与通信·信号处理
charlie1145141915 小时前
FreeRTOS:中断(ISR)与 RTOS 安全 API
开发语言·c·freertos·实时操作系统
一路往蓝-Anbo5 小时前
STM32单线串口通讯实战(三):协议层设计 —— 帧结构、多机寻址与硬件唤醒
c语言·开发语言·stm32·单片机·嵌入式硬件·物联网