嵌入式学习:Linux驱动开发总结

一、驱动分类

类型 访问方式 例子
字符设备 按字节流 LED、按键、串口
块设备 按块 硬盘、SD 卡
网络设备 按包 网卡

驱动实验主要是字符设备。

二、驱动的两种编译方式

方式 做法 特点
编译进内核 obj-y 开机自动加载,改驱动要重编内核
编译成模块 obj-m insmod 加载,rmmod 卸载,方便调试

开发阶段用模块方式。

三、字符设备驱动核心概念

概念 说明
设备号 主设备号 + 次设备号,标识驱动和设备
cdev 内核里描述字符设备的结构体
file_operations 描述驱动支持的操作
设备文件 /dev/ 下的文件,应用通过它访问驱动

应用层和驱动层的关系

text

复制代码
应用层:open("/dev/xxx")
   ↓ 系统调用
驱动层:xxx_open()
   ↓ 操作硬件
硬件层:GPIO / I2C / SPI

应用调用 open、read、write、close,驱动里对应的函数就会被调用。

四、字符设备驱动框架

1. 注册流程(固定)

text

复制代码
1. alloc_chrdev_region → 申请设备号
2. cdev_alloc → 申请 cdev
3. pcdev->ops = &fops → 绑定 file_operations
4. cdev_add → 注册字符设备

记忆口诀:号 → 存 → 绑 → 册。

2. 卸载流程(固定)

text

复制代码
1. cdev_del → 删除 cdev
2. unregister_chrdev_region → 释放设备号

3. file_operations

c

复制代码
static struct file_operations fops = {
    .owner = THIS_MODULE,
    .open = xxx_open,
    .release = xxx_release,
    .read = xxx_read,
    .write = xxx_write,
};
函数 对应系统调用
xxx_open open
xxx_release close
xxx_read read
xxx_write write

4. 数据拷贝

函数 方向
copy_from_user 用户 → 内核(write)
copy_to_user 内核 → 用户(read)

内核不能直接访问用户空间,必须用这两个函数。

五、自动创建设备节点

c

复制代码
static struct class *pclass;
static struct device *pdevice;

// 加载时
pclass = class_create(THIS_MODULE, "xxx");
pdevice = device_create(pclass, NULL, devno, NULL, "xxx0");

// 卸载时
device_destroy(pclass, devno);
class_destroy(pclass);

加载后 /dev/xxx0 自动创建,不用 mknod。

六、并发控制

机制 特点
原子操作 简单计数
自旋锁 不睡眠,关中断
信号量 可以睡眠
互斥体 可以睡眠,最常用

多进程同时操作设备时,用互斥体保护临界区。

七、sysfs 调试接口

c

复制代码
static ssize_t xxx_show(struct device *dev, struct device_attribute *attr, char *buf) { ... }
static ssize_t xxx_store(struct device *dev, struct device_attribute *attr, const char *buf, size_t count) { ... }

static struct device_attribute attr = __ATTR(attr, 0664, xxx_show, xxx_store);

// 加载时
device_create_file(pdevice, &attr);

// 卸载时
device_remove_file(pdevice, &attr);

用户通过 echo 和 cat 直接控制设备,不用写应用程序。

八、设备树

1. 为什么用设备树

把硬件信息从驱动中独立出来,实现设备与驱动分离。

2. pinctrl 节点

dts

复制代码
&iomuxc {
    pinctrl_xxx: xxxgrp {
        fsl,pins = <
            MX6UL_PAD_XXX__GPIOX_IOXX  0x10B0
        >;
    };
};

3. 设备节点

dts

复制代码
xxx {
    compatible = "vendor,xxx";
    pinctrl-names = "default";
    pinctrl-0 = <&pinctrl_xxx>;
    gpio-xxx = <&gpioX Y 1>;
    status = "okay";
};

4. 驱动里读设备树

c

复制代码
plednode = of_find_node_by_path("/xxx");
ledgpiono = of_get_named_gpio(plednode, "gpio-xxx", 0);

九、驱动通用框架(核心)

1. 整体框架

text

复制代码
头文件
  ↓
全局变量(保存设备结构)
  ↓
操作函数(read / write / ioctl)
  ↓
file_operations
  ↓
miscdevice
  ↓
probe(匹配成功时调用)
  ↓
remove(卸载时调用)
  ↓
匹配表(of_device_id + xxx_device_id)
  ↓
xxx_driver 结构体
  ↓
xxx_register_driver / xxx_unregister_driver
  ↓
module_init / module_exit
  ↓
MODULE_LICENSE

platform、I2C、SPI,框架都一样,只是 API 不同。

2. 通用模板

头文件

c

复制代码
#include <linux/init.h>
#include <linux/module.h>
#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/xxx.h>          // 总线头文件
#include <linux/miscdevice.h>
#include <asm/uaccess.h>
全局变量

c

复制代码
static struct xxx_device *pdev = NULL;
操作函数

c

复制代码
static ssize_t xxx_read(struct file *fp, char __user *puser, size_t n, loff_t *off)
{
    // 1. 硬件操作
    // 2. copy_to_user
    return sizeof(data);
}

static ssize_t xxx_write(struct file *fp, const char __user *puser, size_t n, loff_t *off)
{
    // 1. copy_from_user
    // 2. 硬件操作
    return n;
}
file_operations

c

复制代码
static struct file_operations fops = {
    .owner = THIS_MODULE,
    .read = xxx_read,
    .write = xxx_write,
};
miscdevice

c

复制代码
static struct miscdevice xxx_misc = {
    .minor = MISC_DYNAMIC_MINOR,
    .name = "xxx_misc",
    .fops = &fops,
};
probe

c

复制代码
static int xxx_probe(struct xxx_device *pdev_param)
{
    pdev = pdev_param;
    // 申请资源、初始化硬件
    misc_register(&xxx_misc);
    return 0;
}
remove

c

复制代码
static int xxx_remove(struct xxx_device *pdev_param)
{
    misc_deregister(&xxx_misc);
    return 0;
}
匹配表

c

复制代码
static struct xxx_device_id xxx_id_table[] = {
    {.name = "xxx"},
    {},
};

static struct of_device_id xxx_of_match_table[] = {
    {.compatible = "vendor,xxx"},
    {},
};
driver 结构体

c

复制代码
static struct xxx_driver xxx_drv = {
    .probe = xxx_probe,
    .remove = xxx_remove,
    .driver = {
        .name = "xxx",
        .owner = THIS_MODULE,
        .of_match_table = xxx_of_match_table,
    },
    .id_table = xxx_id_table,
};
注册和注销

c

复制代码
static int __init xxx_drv_init(void)
{
    return xxx_register_driver(&xxx_drv);
}

static void __exit xxx_drv_exit(void)
{
    xxx_unregister_driver(&xxx_drv);
}

module_init(xxx_drv_init);
module_exit(xxx_drv_exit);

MODULE_LICENSE("GPL");

十、各种总线的对应关系(必背)

项目 platform I2C SPI
设备结构 platform_device i2c_client spi_device
驱动结构 platform_driver i2c_driver spi_driver
设备 ID 表 --- i2c_device_id spi_device_id
probe 参数 platform_device * i2c_client * spi_device *
注册 platform_driver_register i2c_add_driver spi_register_driver
注销 platform_driver_unregister i2c_del_driver spi_unregister_driver
传输结构 --- i2c_msg spi_transfer + spi_message
传输函数 --- i2c_transfer spi_sync

规律:xxx_device、xxx_driver、xxx_register_driver、xxx_unregister_driver。

十一、写驱动的通用流程

text

复制代码
1. 确定总线类型(GPIO / I2C / SPI)
2. 选框架(platform / i2c / spi)
3. 写全局变量(保存设备结构)
4. 写操作函数(read / write)
5. 写 file_operations
6. 写 miscdevice
7. 写 probe(存设备、申请资源、注册 misc)
8. 写 remove(注销 misc)
9. 写匹配表(of_device_id + xxx_device_id)
10. 写 xxx_driver 结构体
11. 写注册和注销
12. module_init / module_exit
13. MODULE_LICENSE

十二、操作函数的区别

总线 操作函数里的硬件操作
platform gpio_set_value / gpio_get_value / readl / writel
I2C i2c_transfer / master_xfer
SPI spi_write_then_read / spi_sync

框架不变,操作函数里的硬件代码不同。

十三、设备树模板

dts

复制代码
xxx {
    compatible = "vendor,xxx";
    reg = <...>;                    // I2C 从机地址 或 SPI 片选编号
    spi-max-frequency = <...>;      // SPI 用
    pinctrl-names = "default";
    pinctrl-0 = <&pinctrl_xxx>;
    gpio-xxx = <&gpioX Y 1>;        // GPIO 用
    interrupt-parent = <&gpioX>;    // 中断用
    interrupts = <Y IRQ_TYPE_EDGE_FALLING>;
    status = "okay";
};

十四、中断 + 等待队列

1. 为什么用中断

轮询方式 CPU 占用高,响应慢。中断方式硬件触发,CPU 占用低,响应快。

2. 中断处理

c

复制代码
static irqreturn_t xxx_handler(int irqno, void *pdev)
{
    condition = 1;
    wake_up_interruptible(&wq);
    return IRQ_HANDLED;
}

// probe 里申请中断
irqno = of_irq_get(pdevice->dev.of_node, 0);
devm_request_irq(dev, irqno, xxx_handler, IRQF_TRIGGER_FALLING, "xxx", NULL);

3. 等待队列

c

复制代码
static wait_queue_head_t wq;
static int condition = 0;

// probe 里初始化
init_waitqueue_head(&wq);

// read 里阻塞等待
condition = 0;
wait_event_interruptible(wq, condition);

4. 流程

text

复制代码
应用 read
    ↓
xxx_read 里 wait_event_interruptible → 进程睡眠
    ↓
硬件触发中断
    ↓
xxx_handler → condition = 1,唤醒进程
    ↓
xxx_read 继续,读数据,返回

十五、中断下半部

1. 为什么需要下半部

中断处理函数不能做耗时操作,否则阻塞其他中断。

部分 作用 特点
上半部 紧急操作 不可打断,要快
下半部 耗时操作 可打断

2. tasklet 和工作队列

方式 上下文 能否睡眠
tasklet 中断上下文 ❌
工作队列 进程上下文 ✅

需要在中断下半部睡眠,必须用工作队列。

十六、input 子系统

Linux 统一管理输入设备(键盘、鼠标、触摸屏、按键)。

c

复制代码
// 1. 申请 input 设备
pkey_dev = input_allocate_device();

// 2. 说明设备能力
input_set_capability(pkey_dev, EV_KEY, KEY_0);

// 3. 注册 input 设备
input_register_device(pkey_dev);

// 4. 上报事件
input_event(pkey_dev, EV_KEY, KEY_0, 1);
input_sync(pkey_dev);

应用程序从 /dev/input/eventX 读取事件。

十七、内核定时器

c

复制代码
static void timeout(unsigned long data)
{
    pr_info("timeout ok!\n");
}

static DEFINE_TIMER(mtimer, timeout, 0, 0);

// 加载时
init_timer(&mtimer);
add_timer(&mtimer);
mod_timer(&mtimer, jiffies + msecs_to_jiffies(100));

// 卸载时
del_timer(&mtimer);

延时执行函数,超时后内核自动调用 timeout。

十八、核心思想

思想 说明
设备与驱动分离 硬件信息放设备树,驱动代码不写死地址
一切皆是文件 应用通过 /dev/xxx 访问驱动
分层设计 上层应用、中层驱动、下层硬件
并发控制 多进程访问用锁保护
中断 + 阻塞 提高效率,降低 CPU 占用
相关推荐
C++ 老炮儿的技术栈1 小时前
std::atomic 是什么
linux·c语言·网络·c++·visual studio
2401_862880822 小时前
input 子系统框架 、platform 驱动框架
linux·驱动开发·嵌入式硬件
Liuqy-052 小时前
线程的互斥与同步机制——互斥锁、条件变量、无名信号量
linux·开发语言·线程·进程·条件变量·同步·互斥
十年磨一剑~2 小时前
LVDS 背光控制问题排查与 DTS 修正文档
linux·ubuntu
有毒的教程2 小时前
Linux底层存储路由配置
linux·运维·网络
keyipatience2 小时前
JS学习笔记(2)数组,函数,作用域,对象
前端·javascript·笔记·学习·html
瑞克-桑切斯2 小时前
2、Shell 条件语句
java·linux·服务器
你想知道什么?2 小时前
词向量总结-学习笔记
人工智能·笔记·学习
yangmu32033 小时前
开发网站的完整流程:从0到1上线指南
运维·学习