当前位置: 首页 > news >正文

Linux学习笔记——设备驱动

设备驱动

      • 主设备号和次设备号的用途
        • 主设备号(Major Number)
        • 次设备号(Minor Number)
      • 字符型驱动设备如何创建设备文件
        • 手动创建
        • 自动创建
      • 如何在设备驱动程序中注册字符设备
        • 方法一:使用 `cdev` 接口
        • 方法二:使用传统注册函数
      • /dev 目录下的设备文件如何创建
        • 常见三种方式:
      • Linux字符设备与块设备的主要区别
      • 为什么驱动中操作物理地址前要使用 ioremap?
      • insmod/rmmod 加载卸载模块时执行哪些函数?
        • 注意事项:
      • NAND 驱动的 probe 流程
      • Linux 驱动开发中常用的调试方法
      • ioremap(映射外设寄存器)
        • 函数原型
        • 头文件
        • 参数说明
        • 作用
        • 举个简单例子:
      • open(打开设备文件)
        • 函数原型
        • 参数说明
        • 作用
      • read(从设备或文件读取数据)
        • 函数原型
        • 参数说明
        • 返回值
        • 举个例子:
      • write(向设备或文件写入数据)
        • 函数原型
        • 参数说明
        • 返回值
        • 注意事项
        • 举个例子:
      • copy_to_user(内核 → 用户)
        • 函数原型
        • 参数说明
        • 返回值
        • 用途
        • 举个例子:
      • copy_from_user(用户 → 内核)
        • 函数原型
        • 参数说明
        • 返回值
        • 用途
        • 举个例子:
      • 小结表

主设备号和次设备号的用途

主设备号(Major Number)

主设备号用于标识设备所对应的驱动程序。内核通过主设备号来找到设备所用的驱动程序。虽然现代Linux内核支持多个驱动共享主设备号,但一般仍遵循“一个主设备号对应一个驱动”的设计。

次设备号(Minor Number)

次设备号由驱动程序使用,用于区分主设备号所表示的多个设备实例。例如,一个硬盘驱动程序可能通过不同的次设备号来区分不同的分区。驱动程序可以根据次设备号获取特定设备的指针或者作为索引。


字符型驱动设备如何创建设备文件

手动创建

使用 mknod 命令创建:

mknod /dev/led c 250 0
  • /dev/led:设备文件名
  • c:字符设备类型
  • 250:主设备号
  • 0:次设备号
自动创建

UDEV/MDEV 是运行在用户空间的设备管理程序,用于动态创建和删除设备节点。通常在系统启动脚本 /etc/init.d/rcS 中执行 mdev -s 实现自动创建设备节点。


如何在设备驱动程序中注册字符设备

方法一:使用 cdev 接口
void cdev_init(struct cdev *cdev, struct file_operations *fops);
  • cdev:指向 struct cdev 的指针,字符设备结构体。
  • fops:指向 file_operations 结构体的指针,定义设备的操作函数。
方法二:使用传统注册函数
int register_chrdev(unsigned int major, const char *name, struct file_operations *fops);
  • major:主设备号(0表示自动分配)
  • name:驱动程序名称
  • fops:设备操作函数集

该函数会为 major 注册 0-255 的次设备号,并为每个设备建立默认的 cdev 结构体。


/dev 目录下的设备文件如何创建

常见三种方式:
  1. devfs机制(已淘汰)

    • 旧内核中使用,2.6 之前版本为主。
  2. udev机制(主流)

    • 基于内核通知事件,使用 device_create()class_create() 接口配合自动创建设备节点。
  3. 手动创建(mknod)

    • 在脚本或调试阶段,开发者常用 mknod 命令手动创建设备文件。

Linux字符设备与块设备的主要区别

类型特点示例
字符设备按字节顺序读写,不支持随机访问串口、鼠标、键盘、摄像头等
块设备支持随机访问,按块进行读写(如512B)硬盘、U盘、SD卡等

为什么驱动中操作物理地址前要使用 ioremap?

在保护模式下,CPU 不能直接访问物理地址。ioremap 会将设备的物理地址映射到虚拟地址空间,使驱动程序能通过虚拟地址访问硬件寄存器。它是内核提供的访问I/O内存的重要接口。


insmod/rmmod 加载卸载模块时执行哪些函数?

  • insmod 加载模块时,会执行 module_init() 中注册的初始化函数。
  • rmmod 卸载模块时,会执行 module_exit() 中注册的退出函数。
注意事项:
  • init 中申请的资源,如内存、中断、GPIO 等,必须在 exit 中进行对称释放,避免资源泄漏。

NAND 驱动的 probe 流程

当 NAND 驱动的 probe 函数执行时,大致步骤如下:

  1. 与 NAND 芯片通讯,读取 NAND ID。
  2. 查表获取 NAND 芯片的参数信息:如厂家、页大小、擦除块大小、芯片大小等。
  3. 调用 nand_scan() 函数自动完成如下任务:
    • 初始化硬件接口
    • 扫描坏块表(BBT)
    • 建立 MTD 结构体

Linux 驱动开发中常用的调试方法

  1. printk() 日志输出:驱动调试首选,输出到 dmesg
  2. 查看OOPS信息:系统崩溃时记录错误现场。
  3. strace:追踪用户态程序的系统调用。
  4. 内核 hacking 选项:编译内核时开启 debug 相关配置。
  5. ioctl 调试:用户程序通过 ioctl 与驱动交互,调试灵活性高。
  6. /proc 文件系统:通过 /proc 暴露调试信息给用户态。
  7. kgdb(内核级gdb):进行内核级断点调试。

好的,下面是面向小白整理的《Linux驱动开发常用函数》详细内容介绍,涵盖 ioremapopenreadwritecopy_to_usercopy_from_user 函数的基本用法、参数解释和使用举例。



ioremap(映射外设寄存器)

函数原型
void *ioremap(unsigned long phys_addr, unsigned long size);
void *__ioremap(unsigned long phys_addr, unsigned long size, unsigned long flags);
头文件
#include <linux/io.h>
参数说明
  • phys_addr:要映射的物理地址,即外设寄存器地址。
  • size:要映射的内存空间大小(单位:字节)。
  • flags:映射权限/属性(只在 __ioremap() 中使用)。
作用

将外设的物理地址(比如网卡寄存器地址)映射到内核虚拟地址空间,这样驱动程序就可以通过虚拟地址来访问硬件寄存器。

举个简单例子:

假设有一块网卡,它的控制寄存器地址是 0xFE000000,大小为 0x100:

void __iomem *ioaddr;
ioaddr = ioremap(0xFE000000, 0x100);

访问第 2 个寄存器(每个寄存器4字节):

iowrite32(value, ioaddr + 4);  // 写入
val = ioread32(ioaddr + 4);    // 读取

open(打开设备文件)

函数原型
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
参数说明
  • pathname:文件或设备路径,如 /dev/mydevice
  • flags:打开模式(如 O_RDONLYO_WRONLYO_RDWR)。
  • mode:创建文件时的权限(通常用不到)。
作用

用于打开一个设备文件(或普通文件),返回一个文件描述符 fd,之后的 readwrite 都是基于这个 fd


read(从设备或文件读取数据)

函数原型
ssize_t read(int fd, void *buf, size_t count);
参数说明
  • fd:文件描述符(通过 open 获取)。
  • buf:读取数据保存的位置。
  • count:想读取的字节数。
返回值
  • 返回实际读取的字节数;
  • 返回 0 表示文件/设备已经没有数据;
  • 失败返回 -1。
举个例子:
char buf[100];
int len = read(fd, buf, 100);  // 从设备读取 100 字节

write(向设备或文件写入数据)

函数原型
ssize_t write(int fd, const void *buf, size_t count);
参数说明
  • fd:文件描述符。
  • buf:要写入的数据缓冲区。
  • count:要写入的数据字节数。
返回值
  • 返回实际写入的字节数;
  • 失败返回 -1。
注意事项
  1. write 不会自动处理缓冲区偏移,程序员需手动处理;
  2. write(fd, p1 + len, strlen(p1) - len) 是常见处理方式;
  3. Linux内核中 BUFSIZ 是系统默认缓冲大小,但不是写入上限。
举个例子:
char msg[] = "Hello driver";
int len = write(fd, msg, strlen(msg));  // 写入字符串到设备

copy_to_user(内核 → 用户)

函数原型
unsigned long copy_to_user(void *to, const void *from, unsigned long n);
参数说明
  • to:目标地址(用户空间)。
  • from:源地址(内核空间)。
  • n:拷贝的字节数。
返回值
  • 成功返回 0;
  • 失败返回未成功复制的字节数。
用途

在驱动中,把读取到的数据“传递”回用户空间程序。

举个例子:
copy_to_user(user_buf, kernel_buf, len);

copy_from_user(用户 → 内核)

函数原型
unsigned long copy_from_user(void *to, const void *from, unsigned long n);
参数说明
  • to:目标地址(内核空间)。
  • from:源地址(用户空间)。
  • n:拷贝的字节数。
返回值
  • 成功返回 0;
  • 失败返回未成功复制的字节数。
用途

在驱动中,把用户空间传来的参数读入到内核空间以供处理。

举个例子:
copy_from_user(kernel_buf, user_buf, len);

小结表

函数名功能简述常见场景
ioremap物理地址 → 虚拟地址映射设备寄存器
open打开设备文件用户程序打开 /dev/xxx
read从设备读数据应用程序读取
write向设备写数据应用程序写入
copy_to_user内核 → 用户数据拷贝驱动向用户返回数据
copy_from_user用户 → 内核数据拷贝用户给驱动传参数

相关文章:

  • ‌UniApp 安卓打包完整步骤(小白向)
  • 前端面试宝典---创建对象的配置
  • android 下提示 SQLITECIPHER driver not loaded
  • 链式触发器
  • 如何使用Tomcat
  • Day 9
  • Node 处理 request 的过程中,都会更新哪些 metadata 和 property
  • 餐饮厨房开源监控安全系统的智能革命
  • 小刚说C语言刷题——第20讲 循环之嵌套循环
  • 【面经】兼顾频繁插入/删除和查询访问 非阻塞网络I/O模型 connect的阻塞性 `unique_ptr`的使用场景和析构机制
  • 20年AB1解码java
  • 【PyTorch项目实战】卷积(Convolution ) + 反卷积(Deconvolution)
  • 文章记单词 | 第27篇(六级)
  • WePY 框架:小程序开发的“Vue式”利器!!!
  • gogs私服对应SSH 协议配置
  • 基于 OpenHarmony 5.0 的星闪轻量型设备应用开发——Ch3 设备驱动开发
  • python基础:位置互换
  • 【前端】【React】useCallback的作用与使用场景总结
  • 银行业务知识序言
  • 基于labview的多功能数据采集系统