驱动学习(二):从零开始:驱动工程搭建与字符设备驱动框架详解(基于IMX6ULL)

一、 驱动工程搭建:模块的加载与卸载

在Linux驱动开发中,我们可以将驱动代码编译进内核(静态加载),也可以编译成模块(.ko文件),在需要时动态加载(动态加载)。动态加载更加灵活,是我们开发调试时的首选。

1. 模块加载与卸载函数

Linux内核为我们提供了两个宏来声明模块的入口和出口:

函数名 功能
module_init 驱动模块加载入口(insmod时调用)
module_exit 驱动模块卸载入口(rmmod时调用)

2. 驱动初始化流程(了解内核启动顺序)

内核启动时,各个驱动的初始化是有严格顺序的。Linux通过不同的initcall级别来管理:

函数名 功能 级别
pure_initcall 内存池、基础硬件自检加载,最早执行 0
core_initcall 调度器、中断器、时钟源内核核心初始化 1
postcore_initcall CPU缓存、总线控制器,核心功能完成后初始化 2
arch_initcall Soc外设、平台设备注册,架构相关初始化 3
subsys_initcall 网络协议栈、文件系统框架,子系统初始化 4
fs_initcall 虚拟文件系统VFS、根文件系统挂载 5
device_initcall 字符设备、块设备、I2C控制器、设备驱动初始化 6
late_initcall 调试接口、延迟加载模块,最后阶段初始化 7

注意 :我们编写的一般驱动通常属于device_initcall级别,也就是第6级。


二、 字符设备驱动框架

字符设备是Linux驱动中最基础的一类设备(如LED、按键、串口等)。它的核心思想是:应用层通过open/read/write等标准文件接口,经过VFS(虚拟文件系统),最终调用到驱动层对应的操作函数。

1. 字符设备流程

字符设备驱动可以通过静态加载 (编译进内核)和动态加载(insmod)两种方式加载。我们主要采用动态加载。

核心流程如下:

  1. 创建一个cdev结构体。
  2. 关联dev_t(设备号)和file_operations(文件操作集)。
  3. 向内核注册cdev。
  4. 应用层通过设备号找到cdev,进而找到file_operations,执行具体的硬件操作。

图解逻辑 :

用户空间(open) -> VFS -> 设备号(dev_t) -> cdev结构体 -> file_operations结构体 -> 具体驱动函数(open/read/write)。

2. file_operations 结构体

这是驱动开发中最核心的结构体,它定义了驱动能对设备做什么操作。我们需要根据需求实现其中的函数指针。

c 复制代码
struct file_operations {
    struct module *owner;
    loff_t (*llseek) (struct file *, loff_t, int);
    ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
    ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
    // ... (省略部分)
    int (*open) (struct inode *, struct file *);
    int (*release) (struct inode *, struct file *); // 对应close
    // ...
};

3. 字符设备函数接口详解

(1) cdev操作函数
函数名 功能
cdev_init 初始化字符设备结构体
cdev_alloc 申请字符设备结构体
cdev_add 添加字符设备(注册到内核)
cdev_del 删除字符设备

函数原型:

c 复制代码
void cdev_init(struct cdev *cdev, const struct file_operations *fops);
int cdev_add(struct cdev *p, dev_t dev, unsigned count);
void cdev_del(struct cdev *p);
(2) 设备号操作接口

设备号dev_t由主设备号(Major)和次设备号(Minor)组成。

函数名 功能
register_chrdev_region 静态注册已知设备号范围
alloc_chrdev_region 动态分配设备号(推荐,由内核自动分配主设备号)
unregister_chrdev_region 释放注册的设备号
register_chrdev 注册设备号并关联fops(老版本内核使用,不推荐)

动态分配示例:

c 复制代码
dev_t dev_num;
int major;
alloc_chrdev_region(&dev_num, 0, 1, "my_led"); // 分配1个设备号
major = MAJOR(dev_num); // 获取主设备号
(3) 设备类与设备创建(自动创建设备节点)

在旧版本中我们需要手动mknod创建设备节点,现在我们可以利用内核自动创建。

函数名 功能
class_create 创建设备类目录(在/sys/class下)
class_destroy 销毁设备类
device_create 创建设备(会在/dev下生成设备节点)
device_destroy 销毁设备

三、 sys文件系统

1. 什么是sysfs?

sysfs是一个虚拟文件系统,挂载在/sys目录下。它将内核中的设备、驱动、总线等信息以文件的形式展示给用户空间。它的文件不对应硬盘上的文件,而是在内存中。

2. sys目录结构

  • /sys/devices:按物理设备类型分类。
  • /sys/bus:按总线类型分类。
  • /sys/class:按设备功能分类(如leds, input)。
  • /sys/kernel:内核相关配置。

3. sys文件系统的使用

在驱动中,我们常用它来导出信息或进行调试:

  1. 设备信息显示 :用户可以通过cat或echo直接读写属性。
  2. 动态设备管理:实时反映热插拔状态。
  3. 驱动调试:直接读取/写入寄存器。

常用API:

函数名 功能
kobject_create_and_add 创建并添加kobject
sysfs_create_file 创建属性文件
device_create_file 在设备目录下创建文件

四、 LED灯驱动实战

结合前面的字符设备框架,我们来编写一个简单的LED驱动。在IMX6ULL中,LED通常通过GPIO控制。

1. 内存映射接口(ioremap)

在Linux中,不能直接访问物理地址(如寄存器地址)。我们需要将物理地址映射为虚拟地址。

函数名 功能
ioremap 将物理地址映射为内核虚拟地址
iounmap 解除映射
devm_ioremap 带资源管理的映射(驱动卸载自动解除,推荐)
copy_to_user 内核层向用户层拷贝数据
copy_from_user 用户层向内核层拷贝数据

2. 驱动编写思路

  1. 定义设备号、cdev、class、device结构体。
  2. 编写open、write、release函数。
    • open:通常用于初始化硬件或增加引用计数。
    • write:接收应用层传来的数据(如1或0),通过copy_from_user获取,然后操作GPIO寄存器。
    • release:关闭设备时的清理工作。
  3. 编写入口函数__init:
    • 注册设备号 alloc_chrdev_region。
    • 初始化cdev cdev_init 并添加 cdev_add。
    • 创建类 class_create。
    • 创建设备 device_create。
    • 映射寄存器 ioremap。
  4. 编写出口函数__exit:
    • 反向操作:iounmap、device_destroy、class_destroy、cdev_del、unregister_chrdev_region。

3. 代码框架示例

c 复制代码
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of.h>
#include <linux/of_address.h>

#define LED_CNT 1
#define LED_NAME "led"

// 假设寄存器地址(实际需查数据手册)
#define CCM_CCGR1_BASE (0X020C406C)
#define SW_MUX_GPIO1_IO03_BASE (0X020E0068)
#define GPIO1_DR_BASE (0X0209C000)
#define GPIO1_GDIR_BASE (0X0209C004)

// 定义设备结构体
struct led_dev {
    dev_t devid;
    struct cdev cdev;
    struct class *class;
    struct device *device;
    int major;
    int minor;
    void __iomem *ccm_ccgr1;
    void __iomem *sw_mux_gpio1_io03;
    void __iomem *gpio1_dr;
    void __iomem *gpio1_gdir;
};

struct led_dev leddev;

// 写函数
static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
    int ret;
    unsigned char databuf[1];
    unsigned char ledstat;

    ret = copy_from_user(databuf, buf, cnt);
    if(ret < 0) return -EFAULT;

    ledstat = databuf[0];
    if(ledstat == 1) { // 开灯
        // 操作寄存器
        u32 val = readl(leddev.gpio1_dr);
        val &= ~(1 << 3);
        writel(val, leddev.gpio1_dr);
    } else if(ledstat == 0) { // 关灯
        u32 val = readl(leddev.gpio1_dr);
        val |= (1 << 3);
        writel(val, leddev.gpio1_dr);
    }
    return cnt;
}

static int led_open(struct inode *inode, struct file *filp)
{
    filp->private_data = &leddev;
    return 0;
}

static int led_release(struct inode *inode, struct file *filp)
{
    return 0;
}

static struct file_operations led_fops = {
    .owner = THIS_MODULE,
    .open = led_open,
    .write = led_write,
    .release = led_release,
};

// 入口函数
static int __init led_init(void)
{
    // 1. 注册字符设备号
    if(leddev.major) {
        leddev.devid = MKDEV(leddev.major, 0);
        register_chrdev_region(leddev.devid, LED_CNT, LED_NAME);
    } else {
        alloc_chrdev_region(&leddev.devid, 0, LED_CNT, LED_NAME);
        leddev.major = MAJOR(leddev.devid);
        leddev.minor = MINOR(leddev.devid);
    }

    // 2. 初始化cdev
    leddev.cdev.owner = THIS_MODULE;
    cdev_init(&leddev.cdev, &led_fops);
    cdev_add(&leddev.cdev, leddev.devid, LED_CNT);

    // 3. 创建类和设备
    leddev.class = class_create(THIS_MODULE, LED_NAME);
    leddev.device = device_create(leddev.class, NULL, leddev.devid, NULL, LED_NAME);

    // 4. 初始化硬件(ioremap)
    leddev.ccm_ccgr1 = ioremap(CCM_CCGR1_BASE, 4);
    leddev.sw_mux_gpio1_io03 = ioremap(SW_MUX_GPIO1_IO03_BASE, 4);
    leddev.gpio1_dr = ioremap(GPIO1_DR_BASE, 4);
    leddev.gpio1_gdir = ioremap(GPIO1_GDIR_BASE, 4);

    // 具体寄存器配置代码略...
    printk("led init success!\n");
    return 0;
}

// 出口函数
static void __exit led_exit(void)
{
    iounmap(leddev.ccm_ccgr1);
    iounmap(leddev.sw_mux_gpio1_io03);
    iounmap(leddev.gpio1_dr);
    iounmap(leddev.gpio1_gdir);
    
    cdev_del(&leddev.cdev);
    unregister_chrdev_region(leddev.devid, LED_CNT);
    device_destroy(leddev.class, leddev.devid);
    class_destroy(leddev.class);
    printk("led exit!\n");
}

module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");

五、 补充:misc设备驱动(杂项设备)

除了标准的字符设备,Linux还有一类特殊的字符设备叫Misc设备(杂项设备)。

  • 特点 :主设备号固定为10,次设备号由内核动态分配。
  • 优势:编写更简单,不需要手动申请设备号、创建cdev等繁琐步骤。
  • 适用场景:看门狗、加密硬件、非标准设备等。

如果你的LED驱动不想写那么长,可以尝试用Misc框架重写。

常用接口:

函数名 功能
misc_register 注册混杂设备
misc_deregister 注销混杂设备

结构体:

c 复制代码
struct miscdevice {
    int minor;
    const char *name;
    const struct file_operations *fops;
    // ...
};

总结

  1. 驱动工程搭建 :理解module_init和module_exit,知道驱动是动态加载的。
  2. 字符设备框架 :核心是file_operations,流程是"申请设备号 -> 初始化cdev -> 创建设备节点"。
  3. sys文件系统:提供了内核与用户空间的交互接口。
  4. LED实战 :通过ioremap映射寄存器,在write函数中根据应用层数据控制硬件。

希望这篇博客能帮你打通Linux驱动开发的"任督二脉"!如有疑问,欢迎在评论区留言交流。

相关推荐
幼儿园蛋小黄1 小时前
嵌入式学习总结:从 I2C 到 SPI 的驱动开发实战
驱动开发·学习
Yyq130208696821 小时前
紫荆M100,RISC-V开源架构,工业控制,电机驱动,国产替代
单片机·嵌入式硬件·risc-v
叶子野格1 小时前
《FPGA基本知识累积:看开发指南要懂的术语》
学习·fpga开发
qq_284274051 小时前
液压泵工作原理与性能参数笔记:能量转换与吸压油三必要条件、压力分级、排量流量与三种效率特性曲线、径向轴向柱塞泵变量原理及配流配油盘(含考点)
笔记·学习·自动化·制造
星 海1 小时前
【json】cJSON库解析文件
c语言·json·cjson
2501_922639581 小时前
率能半导体 SS6208L半桥MOSFET驱动器:12V/4A集成方案,覆盖电子烟、无线充电、半桥应用三大场景
单片机·嵌入式硬件·硬件工程
Zip-List2 小时前
工作小记-pytorch的算子优化: 转gpu核函数 python调c
c语言·c++·pytorch·python·cuda
li星野2 小时前
【学习记录】射线补偿原理与实现:从物理模型到MATLAB代码
开发语言·学习·matlab
yangmu32032 小时前
单片机发展史:从 8051 到 AIoT 芯片
学习