[正点原子]Linux驱动学习笔记--1.第一个Linux驱动(字符设备驱动)

作者 by adtxl / 2022-10-22 / 暂无评论 / 49 个足迹

1. 实验目的

一个简单的字符设备驱动,了解基本的驱动开发的概念

2. 知识点

2.1 驱动的加载与卸载

示例代码 40.2.1.1 字符设备驱动模块加载和卸载函数模板
1 /* 驱动入口函数 */
2 static int __init xxx_init(void)
3 {
4 /* 入口函数具体内容 */
5 return 0;
6 }
7 8
/* 驱动出口函数 */
9 static void __exit xxx_exit(void)
10 {
11 /* 出口函数具体内容 */
12 }
13
14 /* 将上面两个函数指定为驱动的入口和出口函数 */
15 module_init(xxx_init);
16 module_exit(xxx_exit);

modprobe 命令相比 insmod 要智能一些。 modprobe 命令主要智能在提供了模块的依赖性分析、错误检查、错误报告等功能,推荐使用 modprobe 命令来加载驱动.
第一次使用modprobe前需要使用depmod创建依赖关系。

驱动卸载使用rmmod或者modprobe -r命令。使用 modprobe 命令可以卸载掉驱动模块所依赖的其他模块,前提是这些依赖模块已经没有被其他模块所使用,否则就不能使用 modprobe 来卸载驱动模块。

2.2 字符设备的注册与注销

对于字符设备驱动而言,当驱动模块加载成功以后需要注册字符设备,同样,卸载驱动模块的时候也需要注销掉字符设备。字符设备的注册和注销函数原型如下所示:

static inline int register_chrdev(unsigned int major, const char *name,
const struct file_operations *fops)
static inline void unregister_chrdev(unsigned int major, const char *name)

register_chrdev 函数用于注册字符设备,此函数一共有三个参数,这三个参数的含义如下:

major: 主设备号, Linux 下每个设备都有一个设备号,设备号分为主设备号和次设备号两
部分,关于设备号后面会详细讲解。
name:设备名字,指向一串字符串。
fops: 结构体 file_operations 类型指针,指向设备的操作函数集合变量。

unregister_chrdev函数用户注销字符设备,此函数有两个参数,这两个参数含义如下:
major: 要注销的设备对应的主设备号。
name: 要注销的设备对应的设备名。

一般字符设备的注册在驱动模块的入口函数 xxx_init 中进行,字符设备的注销在驱动模块的出口函数 xxx_exit 中进行。

2.2 实现设备的具体操作函数

file_operations 结构体就是设备的具体操作函数,根据驱动的要求去实现具体的函数,例如:

static struct file_operations test_fops = {
    .owner = THIS_MODULE,
    .open = chrtest_open,
    .read = chrtest_read,
    .write = chrtest_write,
    .release = chrtest_release,
};

2.3 设备号

  • 设备号的组成

为了方便管理, Linux 中每个设备都有一个设备号,设备号由主设备号和次设备号两部分组成,主设备号表示某一个具体的驱动,次设备号表示使用这个驱动的各个设备。
Linux 提供了一个名为 dev_t 的数据类型表示设备号, dev_t 定义在文件 include/linux/types.h 里面,定义如下:

示例代码 40.3.1 设备号 dev_t
12 typedef __u32 __kernel_dev_t;
......
15 typedef __kernel_dev_t dev_t;

可以看出 dev_t__u32 类型的,而__u32 定义在文件 include/uapi/asm-generic/int-ll64.h里面,定义如下:

示例代码 40.3.2 __u32 类型
26 typedef unsigned int __u32;

综上所述, dev_t 其实就是 unsigned int 类型,是一个 32 位的数据类型。这 32 位的数据构成了主设备号和次设备号两部分,其中高 12 位为主设备号, 低 20 位为次设备号。因此 Linux系统中主设备号范围为 0~4095,所以大家在选择主设备号的时候一定不要超过这个范围。在文件 include/linux/kdev_t.h 中提供了几个关于设备号的操作函数(本质是宏),如下所示:

示例代码 40.3.3 设备号操作函数
6  #define MINORBITS 20
7  #define MINORMASK ((1U << MINORBITS) - 1)
8
9  #define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))
10 #define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))
11 #define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))

第 6 行,宏 MINORBITS 表示次设备号位数,一共是 20 位。
第 7 行,宏 MINORMASK 表示次设备号掩码。
第 9 行,宏 MAJOR 用于从 dev_t 中获取主设备号,将 dev_t 右移 20 位即可。
第 10 行,宏 MINOR 用于从 dev_t 中获取次设备号,取 dev_t 的低 20 位的值即可。
第 11 行,宏 MKDEV 用于将给定的主设备号和次设备号的值组合成 dev_t 类型的设备号。

  • 设备号的分配
  1. 静态分配

直接写死,先通过/proc/devices节点查看已经使用了的设备号,然后找个没用的使用。这种方式不推荐,不好维护。

  1. 动态分配设备号

Linux 社区推荐使用动态分配设备号,在注册字符设备之前先申请一个设备号,系统会自动给你一个没有被使用的设备号,这样就避免了冲突。
卸载驱动的时候释放掉这个设备号即可,设备号的申请函数如下:

int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)

函数 alloc_chrdev_region 用于申请设备号,此函数有 4 个参数:

dev:保存申请到的设备号。
baseminor: 次设备号起始地址, alloc_chrdev_region 可以申请一段连续的多个设备号,这些设备号的主设备号一样,但是次设备号不同,次设备号以 baseminor 为起始地址地址开始递增。一般 baseminor 为 0,也就是说次设备号从 0 开始。
count: 要申请的设备号数量。
name:设备名字。

注销字符设备之后要释放掉设备号,设备号释放函数如下:

void unregister_chrdev_region(dev_t from, unsigned count)

此函数有两个参数:
from:要释放的设备号。
count: 表示从 from 开始,要释放的设备号数量。

2.4 copy_to_user()&copy_from_user()

因为内核空间不能直接操作用户空间的内存,需要这两个函数用来在用户空间和内核空间之间传递数据。

copy_to_user 函数原型如下:

static inline long copy_to_user(void __user *to, const void *from, unsigned long n)
static inline long copy_from_user(void __user *to, const void *from, unsigned long n)

参数 to 表示目的,参数 from 表示源,参数 n 表示要复制的数据长度。如果复制成功,返回值为 0,如果复制失败则返回负数。

2.5 c库文件操作基本函数

编写测试 APP 就是编写 Linux 应用,需要用到 C 库里面和文件操作有关的一些函数,比如open、 read、 write 和 close 这四个函数。

  • open 函数

open 函数原型如下:

int open(const char *pathname, int flags)

open 函数参数含义如下:
pathname:要打开的设备或者文件名。
flags: 文件打开模式,以下三种模式必选其一:

  • O_RDONLY 只读模式
  • O_WRONLY 只写模式
  • O_RDWR 读写模式

因为我们要对 chrdevbase 这个设备进行读写操作,所以选择 O_RDWR。除了上述三种模式以外还有其他的可选模式,通过逻辑或来选择多种模式:

  • O_APPEND 每次写操作都写入文件的末尾
  • O_CREAT 如果指定文件不存在,则创建这个文件
  • O_EXCL 如果要创建的文件已存在,则返回 -1,并且修改 errno 的值
  • O_TRUNC 如果文件存在,并且以只写/读写方式打开,则清空文件全部内容
  • O_NOCTTY 如果路径名指向终端设备,不要把这个设备用作控制终端。
  • O_NONBLOCK 如果路径名指向 FIFO/块文件/字符文件,则把文件的打开和后继I/O 设置为非阻塞
  • DSYNC 等待物理 I/O 结束后再 write。在不影响读取新写入的数据的前提下,不等待文件属性更新。
  • O_RSYNC read 等待所有写入同一区域的写操作完成后再进行。
  • O_SYNC 等待物理 I/O 结束后再 write,包括更新文件属性的 I/O。

返回值:如果文件打开成功的话返回文件的文件描述符。

在 Ubuntu 中输入“man 2 open”即可查看 open 函数的详细内容

  • read函数

read 函数原型如下:

ssize_t read(int fd, void *buf, size_t count)

read 函数参数含义如下:
fd:要读取的文件描述符,读取文件之前要先用 open 函数打开文件, open 函数打开文件成功以后会得到文件描述符。
buf: 数据读取到此 buf 中。
count: 要读取的数据长度,也就是字节数。
返回值: 读取成功的话返回读取到的字节数;如果返回 0 表示读取到了文件末尾;
如果返回负值,表示读取失败。
在 Ubuntu 中输入“man 2 read”命令即可查看 read 函数的详细内容。

  • write 函数

write 函数原型如下:

ssize_t write(int fd, const void *buf, size_t count);

write 函数参数含义如下:
fd:要进行写操作的文件描述符,写文件之前要先用 open 函数打开文件, open 函数打开文件成功以后会得到文件描述符。
buf: 要写入的数据。
count: 要写入的数据长度,也就是字节数。
返回值: 写入成功的话返回写入的字节数;如果返回 0 表示没有写入任何数据;如果返回负值,表示写入失败。
在 Ubuntu 中输入“man 2 write”命令即可查看 write 函数的详细内容。

  • close 函数

close 函数原型如下:

int close(int fd);

close 函数参数含义如下:
fd:要关闭的文件描述符。
返回值: 0 表示关闭成功,负值表示关闭失败。
在 Ubuntu 中输入“man 2 close”命令即可
查看 close 函数的详细内容。

2.6 创建设备节点文件

驱动加载成功需要在/dev 目录下创建一个与之对应的设备节点文件,应用程序就是通过操作这个设备节点文件来完成对具体设备的操作。输入如下命令创建/dev/chrdevbase 这个设备节点文件:
mknod /dev/chrdevbase c 200 0
其中“mknod”是创建节点命令,“/dev/chrdevbase”是要创建的节点文件,“c”表示这是个字符设备,“ 200”是设备的主设备号,“ 0”是设备的次设备号。创建完成以后就会存在/dev/chrdevbase 这个文件,可以使用“ls /dev/chrdevbase -l”命令查看

3. 实验程序&测试

遇到个问题,加载驱动时报错如下,这是因为编译时生成的kernel版本带有+号,与rootfs里的路径不同导致的,可以通过在kernel里添加个空.scmversion文件来去除路径里的+号https://github.com/ADTXL/imx_linux/commit/32d4d6d091773e26e157d629c25fd9062f7070cc。

image.png

https://github.com/ADTXL/imx_linux/commit/d0d5d65567296c3967369068eea9bab7b0411a69

测试OK.

独特见解