速通Linux驱动程序

痛苦,非常的痛苦!学Linux的驱动要么上天要么入土,因此笔者决定写这篇博客帮所有想入门Linux驱动的读者走地更安详。😇另外的,笔者在学习时具有一定程度的硬件知识储备和嵌入式裸机开发经验,因此本文将不会太多地帮助硬件小白,建议阅读学习笔者之前的硬件相关博客或其他大神的入门教程。

Linux设备驱动概念篇

驱动类型

Linux天下无敌,支持所有的硬件外设,所以提出了3个类别的驱动程序。

驱动类型 特点 常见设备
字符设备 操作字节数据 GPIO、I2C、SPI、音频、显卡
块设备 一次操作一组(块)数据 EMMC、NAND、SD Card
网络设备 套接字(socket)操作 以太网

字符设备常常被创建为设备节点,以类似文件的形式存在于/dev/目录下(例如串口/dev/tty),这种形式操作起来和对普通文件的操作极其相似,同时字符设备类驱动是最常见的,做项目时写的最多的,但是3种驱动里最简单的。
另外两种驱动,块设备和网络设备,驱动模型写起来又臭又长,一般IC器件的厂家都把驱动写好了,属于Linux高手的玩物。

因此,本文中将主要分享字符设备驱动的开发。

内核态

Linux在隔离业务应用和硬件这块做的非常好,操作系统上分为了:用户态内核态。内核态提供服务给用户态调用,具有统一的抽象接口,使得用户态程序可以在不了解硬件的情况下很好地运行并操作所需硬件。显然,我们要编写的驱动程序就是工作在内核态,所以编写时要满足Linux的模型。

用户态与内核态

驱动程序运行方式

Linux驱动有两种运行的方法:

  • 静态转载:驱动程序源码和Linux内核代码放一起,编译进Linux内核,启动Linux时自动运行驱动程序。
  • 动态装载(模块机制):驱动程序单独编译成.ko模块,Linux内核正常运行时,在需要的情况下使用insmodmodprobe等方式加载驱动模块。

使用modprobe需要先在/lib/modules/内核版本号/modules.dep文件中注册和声明模块依赖关系;若没有依赖其他驱动,直接使用depmod命令,也可以使用insmod。另外的,动态驱动.ko模块要放置于该驱动目录中

将驱动跑起来后,在/dev目录下会出现相应的设备,这是Linux将“设备作为文件”处理的思想。当应用程序(或SDK)要操作对应的硬件时,使用对应的系统调用接口即可,如openwrite等。

dev目录

驱动编写TIPs

  • 不能使用标准C库!因为驱动程序运行在内核态,C库是基于系统调用实现在用户态。

    可以使用内核版接口,比如printf打印函数在驱动程序中对应调用printk内核实现的打印函数。

  • 必须用GUN C编译!
  • 内核只有很小的定长堆栈!32位机内核栈8KB,64位机16KB。
  • 小心指针!内核理解为裸机程序,没内存保护机制,一旦访问非法内存可能直接死机
  • 避免用浮点!
  • 小心字节序!

小试牛刀——字符设备驱动

环境声明:

  • Ubuntu 18.04
  • GUN Make 4.1
  • gcc 7.5.0

Demo跑一下

这边演示的是最常见而简单的字符型设备驱动,文件名为chrDriver.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/slab.h>

/**********************************
* 以下为驱动级的实现接口
* 装载进 file_operations 结构体
**********************************/

ssize_t mydev_read(struct file *file, char __user *data, size_t size, loff_t * loff)
{
printk(KERN_INFO "mydev_read\n");

return 0;
}

ssize_t mydev_write(struct file *file, const char __user *data, size_t size, loff_t *loff)
{
printk(KERN_INFO "mydev_write\n");

return 0;
}

int mydev_open(struct inode *inode, struct file *file)
{
printk(KERN_INFO "mydev_open\n");

return 0;
}

/**********************************
* 以下为系统级的接口适配
* 实现驱动模块的注册和注销
**********************************/

static dev_t dev_id; // 设备号
static struct cdev *mydev; // 设备信息汇总
static struct class *mydev_class; // 设备文件化类

/* 文件操作集合 */
static struct file_operations mydev_fops = {
.owner = THIS_MODULE,
.read = mydev_read,
.open = mydev_open,
.write = mydev_write,
};

static __init int mydev_init(void)
{
/* 申请设备号 */
alloc_chrdev_region(&dev_id, 1, 1, "mydev");

/* 分配字符设备 */
mydev = cdev_alloc();

/* 设置字符设备 */
cdev_init(mydev, &mydev_fops);

/* 注册字符设备 */
cdev_add(mydev, dev_id, 1);

/* 打印申请到的主次设备号 */
printk(KERN_INFO "major:%d; minor:%d\n", MAJOR(dev_id), MINOR(dev_id));

/* 设备 文件化*/
mydev_class = class_create(THIS_MODULE, "chrDevFile");
/* 创建/dev/mydev*/
device_create(mydev_class, NULL, dev_id, NULL, "chrDevFile");

return 0;
}

static __exit void mydev_exit(void)
{
/* 注销 文件化的 /dev/mydev */
device_destroy(mydev_class, dev_id);
class_destroy(mydev_class);

/* 归还系统资源*/
cdev_del(mydev);
kfree(mydev);
unregister_chrdev_region(dev_id, 1);
}

module_init(mydev_init); // 生成模块时,指明哪个接口用于 注册驱动
module_exit(mydev_exit); // 生成模块时,指明哪个接口用于 注销驱动

/**********************************
* Linux 体系下的小九九
**********************************/
MODULE_LICENSE("GPL");

Makefile的编写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 内核源码
KERNELDIR := /lib/modules/$(shell uname -r)/build
# 当前目录
CURRENT_PATH := $(shell pwd)

# 取消签名认证
CONFIG_MODULE_SIG=n

obj-m := chrDriver.o

build: kernel_modules

kernel_modules:
# 下面第2项,M=$(CURRENT_PATH) 告知源码编译时,驱动程序在这
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean

我们先把这个程序跑起来,有个直观感受再分析代码内容。首先是编译这个驱动程序,当然 上面的.c源文件和Makefile文件要放在同一目录下,跑命令也在同一目录:

1
$ make

接着加载驱动,并观察驱动的加载情况:

1
2
$ sudo insmod chrDriver.ko
$ lsmod | grep chrDriver

加载后,可以查看驱动运行时的信息:

1
2
3
$ dmesg

[ 7937.803754] major:240; minor:1

使用dmesg命令会打印开机以来所有的驱动动作信息,上面展示了和我们驱动有关的一条。执行insmod命令本质上执行源文件中的mydev_init接口,在该接口中我们调用printk打印了两个号码,所以这边我们能够就看到:在[ 7937.803754]这个时刻系统记录了major:240; minor:1信息。

这个小小的驱动程序还实现了“设备文件化”,在/dev/目录下,是可以找到chrDevFile这个“文件”哒,执行:

1
$ ls /dev

最后,测试完成或不需要时,可以卸载驱动:

1
$ sudo rmmod chrDriver

具体原理

上面的.c源文件中,我们用到了3个Linux系统内核的结构/类型:dev_tstruct cdevstruct file_operations。具体来看他们的内容与功能:

  • dev_t:
    • 存放设备号,高12位为主设备号,低20位为次设备号,共为32位
    • 调用宏可获取主MAJOR(dev_id)、次设备号MINOR(dev_id),这就是上面mydev_init接口里printk打印的东西。
  • cdev:
    • 把 驱动模块、字符设备操作接口集、设备号 关联起来
    • 所有东西准备好后,这个家伙负责汇总了统一报告给内核
    • 位于cdev.h文件中的定义:
      1
      2
      3
      4
      5
      6
      7
      8
      struct cdev {
      struct kobject kobj; // 内核对象,所有内核操作都要有这个,类似C++的继承
      struct module *owner; // 所属模块
      const struct file_operations *ops; // 文件操作集
      struct list_head list;
      dev_t dev; // 设备号
      unsigned int count;
      } __randomize_layout;
  • file_operations
    • 位于fs.h文件中的定义:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      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 *);
      ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
      ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);

      int (*iopoll)(struct kiocb *kiocb, bool spin);
      int (*iterate) (struct file *, struct dir_context *);
      int (*iterate_shared) (struct file *, struct dir_context *);
      __poll_t (*poll) (struct file *, struct poll_table_struct *);
      long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
      long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
      int (*mmap) (struct file *, struct vm_area_struct *);
      unsigned long mmap_supported_flags;

      /*基础打开、关闭等*/
      int (*open) (struct inode *, struct file *);
      int (*flush) (struct file *, fl_owner_t id);
      int (*release) (struct inode *, struct file *);

      int (*fsync) (struct file *, loff_t, loff_t, int datasync);
      int (*fasync) (int, struct file *, int);
      int (*lock) (struct file *, int, struct file_lock *);
      ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
      unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
      int (*check_flags)(int);
      int (*setfl)(struct file *, unsigned long);
      int (*flock) (struct file *, int, struct file_lock *);
      ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
      ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
      int (*setlease)(struct file *, long, struct file_lock **, void **);
      long (*fallocate)(struct file *file, int mode, loff_t offset,
      loff_t len);
      void (*show_fdinfo)(struct seq_file *m, struct file *f);
      #ifndef CONFIG_MMU
      unsigned (*mmap_capabilities)(struct file *);
      #endif
      ssize_t (*copy_file_range)(struct file *, loff_t, struct file *,
      loff_t, size_t, unsigned int);
      loff_t (*remap_file_range)(struct file *file_in, loff_t pos_in,
      struct file *file_out, loff_t pos_out,
      loff_t len, unsigned int remap_flags);
      int (*fadvise)(struct file *, loff_t, loff_t, int);
      bool may_pollfree;
      } __randomize_layout;
    • 这个结构体里,我们最基础的就是把实现的readwriteopenrelease函数指针附上。

了结了这3个关键数据类型后,再去看源码就很简单了。我们采取自顶向下的方式,好理解一个驱动模块的工作原理和编写模型:

  • 我们前面提到过,insmod加载.ko驱动模块本质上调用了mydev_init,向内核申请资源和注册驱动程序的信息。依次为:
    1. 申请设备号alloc_chrdev_region
    2. 分配字符设备cdev_alloc
    3. 设置字符设备cdev_init,关联cdevfile_operations两个结构体
    4. 注册字符设备cdev_add
    5. 设备文件化device_create
  • 当驱动程序不用时,rmmod本质上调用了mydev_exit,依次:
    1. 在内核中删除字符设备cdev_del;
    2. 释放申请的字符设备资源kfree;
    3. 归还申请的设备号资源unregister_chrdev_region;
    4. 注销文件化对象device_destroy

文件化驱动对象,的专业术语应该是创建设备节点,这边笔者是为了好理解这么叫的。

在应用层使用这个字符驱动

上面已经说明了,加载驱动模块后在文件系统中我们可以找到/dev/chrDevFile,所以可以把这个设备当作文件一样去处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

int main(int argc, char* argv[])
{
int val = 1;

int fd = open("/dev/chrDevFile", O_RDWR);

write(fd, &val, sizeof(val));

read(fd, &val, sizeof(val));

return 0;
}

编译后运行:

1
2
3
$ gcc test_chrDriver.c -o test_chrDriver
$ sudo chmod 777 /dev/chrDevFile
$ ./test_chrDriver

再次使用dmesg查看:

1
2
3
4
5
6
$ dmesg

[13192.810102] major:240; minor:1
[13306.403353] mydev_open
[13306.403354] mydev_write
[13306.403354] mydev_read

这些驱动打印的信息说明:用户层调用的标准文件操作接口,Linux一一对接到了驱动层我们实现的驱动接口。

进阶前必备利器

  • Linux Kernel Document:内核API在线速查。

    笔者在接下来的进阶部分,将不对每个结构体和接口做具体说明。因为官方的说明是最准确的,而本文将侧重于开发思想的学习,建议读者边查边学。

  • Source Insight:阅读Linux内核代码必备神器

    因为这个SourceInsight是Windows软件,所以想在Linux上跑地先下一个类似虚拟机但不是虚拟机的东西——Wine

    1
    2
    3
    $ sudo apt install wine-development -y
    $ sudo apt install wine64 -y
    $ wine --version

    然后就可以正式下载安装了

    1
    2
    3
    $ wget https://s3-us-west-2.amazonaws.com/assets.sourceinsight.com/download/v4/release/sourceinsight40126-setup.exe

    $ wine sourceinsight40126-setup.exe

    安装的时候,这个软件要导入License,网络上很多,笔者支持正版不提供。如果采用的默认路径(C://Program Files (x86))安装,那么运行以下代码启动Source Insight:

    1
    $ wine ~/.wine/drive_c/Program\ Files\ \(x86\)/Source\ Insight\ 4.0/sourceinsight4.exe

    可以在桌面建立一个这样的sh脚本,后面一键运行更方便。

Linux驱动中的高级武器

线程间同步

互斥量Mutex

Mutex

  • 作用:同一时刻,只能有一个线程持有该锁
  • 具体实现方式为:
    1. 申请占有资源:每个线程在对共享资源操作前都尝试先加锁
    2. 使用资源:成功加锁后才可以对共享资源进行读写操作,
    3. 释放资源:操作结束后解锁
  • 主要应用函数:
    • 初始化动态互斥量对象pthread_mutex_init
    • 初始化静态互斥量对象PTHREAD_MUTEX_INITIALIZER。注意这是一个宏
    • 摧毁互斥量对象pthread_mutex_destroy
    • 必上锁pthread_mutex_lock。注意:这是一个阻塞接口,如果加锁不成功,会等待直到信号量可用
    • 尝试锁pthread_mutex_trylock。注意:这是一个非阻塞接口,如果调用时,加锁不成功,会直接退出并返回对应的值
    • 解锁/释放信号量pthread_mutex_unlock

信号量Semaphore

Semaphore

  • 本质:计数器
  • 特质:
    • 适用于占用资源较久的情况。因为等待资源线程会进入休眠状态
    • 中断里不能用。因为信号量会引起休眠,中断不能休眠
    • 占有时间短的,不适合使用信号量。因为频繁的休眠、切换线程引起的开销要远大于信号量带来的优势
  • 主要应用函数:
    • 初始化动态信号量对象sema_init
    • 初始化静态信号量对象__SEMAPHORE_INITIALIZER。注意这是一个宏
    • 阻塞式获取信号量down
    • 中断式等待信号量down_interruptible
    • 尝试获取信号量down_trylock
    • 释放/提升信号量up

临界资源、临界区、事件

临界资源:多线程之间需要互斥访问的全局变量/共享字段

临界区:加锁区间的代码!

事件:类似于硬件里说的中断,但是Linux里用信号量这些软中断实现的

  • 临界区在任意时刻只允许一个线程对共享资源进行访问
  • 如果有多个线程试图同时访问临界区,那么有线程进入后,其他线程试图访问时将被挂起,直到进入临界区的线程离开。

进程间通信

管道pipe

pipe

在学习Linux的过程中,包括上面我们查驱动表的时候,都用到了|这个符号,也就是管道:

1
$ lsmod | grep chrDriver

在使用的角度来看,管道这一工具使得我们能够把一个程序的结果 转给 另一个程序作为输入。比如上面执行的这行命令,把lsmod查到的驱动结果列表 通过|管道输送给 grep查找‘chrDriver’字符。

管道基于文件描述符的通信方式,当一个管道建立时,它会创建两个文件描述符fd[0]和fd[1],其中fd[0]固定用于读管道,而fd[1]则固定用于写管道,这样就构成了一个半双工的通道。

  • 主要接口函数
    • 创建管道pipe
    • 关闭管道close

信号(signal)

signal

CSDN一篇很细致的信号

共享内存(shared memory)

shared memory

Linux操作系统给每个进程都单独开了不同的虚拟内存空间,但有时需要进程间一起共享大量数据,这就出了共享内存功能,本质上也就是把两边的虚拟内存一起映射到一片共同的真内存上。使用的接口有:

  1. 生成系统IPC资源标识ftok
  2. 获取共享内存的标识符shmget
  3. 关联共享内存和虚拟内存地址shmat
  4. 取消关联共享内存shmdt

以下为共享内存使用的具体实例,线程A和B通过共同读写/tmp/shm实现数据交互。

线程A代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

#include <sys/ipc.h> // 申请IPC资源 的接口
#include <sys/shm.h> // 共享内存 的接口

#define SHM_PATH "/tmp/shm" // 共享内存挂载文件系统的路径
#define SHM_SIZE 128 // 空间大小

int main(int argc, char *argv[])
{
int shmid; // 共享内存标识符
char *addr; // 实际操作的 虚拟地址
key_t key = ftok(SHM_PATH, 0x6666); // IPC 资源号

/*创建 共享内存 标识符*/
shmid = shmget(key, SHM_SIZE, IPC_CREAT|IPC_EXCL|0666);
if (shmid < 0) {
printf("failed to create share memory\n");
return -1;
}

/* 获得 操作的虚拟地址 */
addr = shmat(shmid, NULL, 0);
if (addr <= 0) {
printf("failed to map share memory\n");
return -1;
}

/* 往共享内存 写 */
sprintf(addr, "%s", "Hello World\n");

return 0;
}

线程B代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>

#include <sys/ipc.h> // 申请IPC资源 的接口
#include <sys/shm.h> // 共享内存 的接口

#define SHM_PATH "/tmp/shm" // 共享内存挂载文件系统的路径
#define SHM_SIZE 128 // 空间大小

int main(int argc, char *argv[])
{
int shmid;
char *addr;
key_t key = ftok(SHM_PATH, 0x6666); // IPC 资源号

char buf[128];

/*获取! 共享内存 标识符*/
shmid = shmget(key, SHM_SIZE, IPC_CREAT);
if (shmid < 0) {
printf("failed to get share memory\n");
return -1;
}

/* 获得 操作的虚拟地址 */
addr = shmat(shmid, NULL, 0);
if (addr <= 0) {
printf("failed to map share memory\n");
return -1;
}

/* 从共享内存 读 */
strcpy(buf, addr, 128);
printf("%s", buf);

return 0;
}

跑了A进程再跑B进程,B会打印A写入的”Hello World”。

扩展自定义功能接口IOCTL

上面讲基础字符设备时,’struct file_operations’里定义的除了开关读写等常规接口,还有ioctl相关的接口。这是因为肯定有硬件会细分出特殊功能,比如打一个命令让LED灯变成呼吸灯,这时就要用到ioctl来扩展新功能。

这个ioctl有两个使用的角度:用户(态/程序)和内核(态/驱动)。

  • 用户程序调用ioctl来命令硬件:

    如下是使用的函数原型,第一个是open设备后得到的文件描述符,第二个是这个驱动告知用户可以使用的命令(一般是宏),第三个是配套的变长参数。

    1
    int ioctl(int fd, int cmd, ...) ;
  • 驱动程序实现ioctl的相关接口:

    作为驱动程序,则负责解析按约定协议解析用户发下来的命令。从file_operations结构体声明里抠的函数原型如下:

    1
    2
    long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
    long (*compat_ioctl) (struct file *, unsigned int, unsigned long);

    都是ioctl,这两个接口的区别是啥呢?

    • unlocked_ioctl:在无大内核锁(BKL)的情况下调用
    • compat_ioctl:兼容版(compatible)ioctl,主要目的是为 64 位系统提供 32 位 ioctl 的兼容方法,也是在无大内核锁的情况下调用

    再看参数,用户态里调用ioctl的参数通过系统调用,依次对应于上面两个接口的参数,第一个是fd,第二个是cmd,第三个是变长数据的指针。
    说到命令cmd,在设计驱动程序时,并不是宏定义(define)命令们为1、2、3…这样的int数据,Linux制定了一个生成cmd的标准:

    1
    2
    3
    4
    5
    #define _IOC(dir,type,nr,size) \
    (((dir) << _IOC_DIRSHIFT) | \
    ((type) << _IOC_TYPESHIFT) | \
    ((nr) << _IOC_NRSHIFT) | \
    ((size) << _IOC_SIZESHIFT))
    • dir(direction),标志数据方向,占据2bit,取值为:
      • _IOC_NONE:无数据
      • _IOC_READ:读数据
      • _IOC_WRITE:写数据
      • _IOC_READ | _IOC_WRITE:读写数据
    • type(device type),设备类型,占据8bit,可以为任意ASCII码,作用是使命令有唯一的设备标识
    • nr(number),命令编号/序数,占据8bit,取值范围 0~255
    • size,涉及到ioctl接口的第三个可边长参数arg,占据13bit或者14bit(体系相关,ARM架构一般为14位),指定arg的数据类型及长度(如果在驱动ioctl实现中不检查,可以忽略该参数)

    既然有编码,肯定就有解码,Linux提供对应的解码方法(宏)为:

    1
    2
    3
    4
    #define _IOC_DIR(nr)        (((nr) >> _IOC_DIRSHIFT) & _IOC_DIRMASK)
    #define _IOC_TYPE(nr) (((nr) >> _IOC_TYPESHIFT) & _IOC_TYPEMASK)
    #define _IOC_NR(nr) (((nr) >> _IOC_NRSHIFT) & _IOC_NRMASK)
    #define _IOC_SIZE(nr) (((nr) >> _IOC_SIZESHIFT) & _IOC_SIZEMASK)

最后,总结一下ioctl的使用全流程:

  1. 阅读硬件外设的技术手册
  2. 确定要实现的细分功能,开一个头文件,基于_IOC逐个功能定义cmd值
  3. 实现unlocked_ioctlcompat_ioctl;在里面解析匹配cmd值,执行对应的硬件操作
  4. 将实现的unlocked_ioctl两个接口,通过函数指针的形式赋值给file_operations结构体
  5. 正如“小试牛刀”章节中,申请、注册驱动,创建设备节点
  6. 用户使用open接口打开设备,调用ioctl执行相应的cmd

具体的演示,我们有3个文件:demo.c驱动源码文件,test.c用户源码文件,chrdev.h头文件。请读者自行学习:

  • demo.c

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    218
    219
    220
    221
    222
    223
    224
    225
    #include <linux/module.h>
    #include <linux/fs.h>
    #include <linux/device.h>
    #include <linux/io.h>
    #include <linux/uaccess.h>
    #include <linux/gpio.h>

    #include "chrdev.h"

    #define MY_LED_NUM 4

    int major;
    const char *name = "demoname";

    struct class *cls;
    struct device *dev;

    static unsigned int led_gpio_id[MY_LED_NUM] = {
    (1*32+4), /* GPIO1_4 */
    (1*32+5), /* GPIO1_5 */
    (1*32+6),
    (1*32+7),
    };

    static struct gpio led_struct[MY_LED_NUM] ={
    {(1*32+4), 0, "led1"},
    {(1*32+5), 0, "led2"},
    {(1*32+6), 0, "led3"},
    {(1*32+7), 0, "led4"},
    };

    int demo_open(struct inode *inode, struct file *filp)
    {
    printk("%s -- %d.\n", __FUNCTION__, __LINE__);

    return 0;
    }

    int demo_release(struct inode *inode, struct file *filp)
    {
    printk("%s -- %d.\n", __FUNCTION__, __LINE__);

    return 0;
    }

    ssize_t demo_read(struct file *filp, char __user *userbuf, size_t size, loff_t *offset)
    {
    printk("%s -- %d.\n", __FUNCTION__, __LINE__);
    //copy_to_user(void __user * to, const void * from, size_t n);

    return 0;
    }

    ssize_t demo_write(struct file *filp, const char __user *userbuf, size_t size, loff_t *offset)
    {
    printk("%s -- %d.\n", __FUNCTION__, __LINE__);
    //copy_from_user(void * to, const void __user * from, unsigned long n);

    return 0;
    }

    long demo_ioctl(struct file *filp, unsigned int cmd, unsigned long args)
    {
    led_node_t led_t;
    int ret, led_num;

    ret = copy_from_user(&led_t, (led_node_t *)args, sizeof(led_t)); //成功返回0,失败返回有多少个Bytes未完成copy。
    if (ret)
    {
    printk("copy_from_user failed.\n");
    goto err0;
    }

    printk("%s -- %d.\n", __FUNCTION__, __LINE__);

    led_num = led_t.which;
    switch(cmd)
    {
    case LEDON:
    if (led_num == 1)
    {
    printk("led%d on.\n", led_num);
    gpio_set_value(led_gpio_id[led_num-1], 1);
    }
    else if (led_num == 2)
    {
    printk("led%d on.\n", led_num);
    gpio_set_value(led_gpio_id[led_num-1], 1);
    }
    else if (led_num == 3)
    {
    printk("led%d on.\n", led_num);
    gpio_set_value(led_gpio_id[led_num-1], 1);
    }
    else if (led_num == 4)
    {
    printk("led%d on.\n", led_num);
    gpio_set_value(led_gpio_id[led_num-1], 1);
    }
    break;
    case LEDOFF:
    if (led_num == 1)
    {
    printk("led%d off.\n", led_num);
    gpio_set_value(led_gpio_id[led_num-1], 0);
    }
    else if (led_num == 2)
    {
    printk("led%d off.\n", led_num);
    gpio_set_value(led_gpio_id[led_num-1], 0);
    }
    else if (led_num == 3)
    {
    printk("led%d off.\n", led_num);
    gpio_set_value(led_gpio_id[led_num-1], 0);
    }
    else if (led_num == 4)
    {
    printk("led%d off.\n", led_num);
    gpio_set_value(led_gpio_id[led_num-1], 0);
    }
    break;
    default:
    printk("cmd id error.\n");
    }

    return 0;

    err0:
    return ret;
    }

    const struct file_operations fops = {
    .open = demo_open,
    .release = demo_release,
    .read = demo_read,
    .write = demo_write,
    .unlocked_ioctl = demo_ioctl,
    };

    static int led_init(void)
    {
    int i, retval;

    for (i=0; i<MY_LED_NUM; i++)
    {
    retval = gpio_is_valid(led_gpio_id[i]);
    if (retval == 0)
    {
    printk("gpio is not valid.\n");
    goto err0;
    }
    }

    retval = gpio_request_array(led_struct, MY_LED_NUM);
    if (retval < 0)
    {
    printk("retvla = %d.\n", retval);
    printk("gpio_request_array failed.\n");
    goto err1;
    }

    for (i=0; i<MY_LED_NUM; i++)
    {
    gpio_direction_output(led_gpio_id[i], 0); // 将引脚设置为输出,并初始化为低电平
    }

    err1:
    return retval;
    err0:
    return retval;
    }

    static int __init demo_init(void)
    {
    printk("%s -- %d.\n", __FUNCTION__, __LINE__);

    major = register_chrdev(0, name, &fops);
    if (major <= 0)
    {
    printk("register_chrdev failed.\n");
    goto err0;
    }

    cls = class_create(THIS_MODULE, "char_class");
    if (cls == NULL)
    {
    printk("class_create failed.\n");
    goto err1;
    }

    dev = device_create(cls, NULL, MKDEV(major, 0), NULL, "chrdev%d", 0);
    if (dev ==NULL)
    {
    printk("device_create failed.\n");
    goto err2;
    }

    led_init(); // 设置为输出模式

    return 0;

    err2:
    class_destroy(cls);
    err1:
    unregister_chrdev(major, name);
    err0:
    return major;
    }

    static void __exit demo_exit(void)
    {
    printk("%s -- %d.\n", __FUNCTION__, __LINE__);

    gpio_free_array(led_struct, MY_LED_NUM);

    device_destroy(cls, MKDEV(major, 0));
    class_destroy(cls);
    unregister_chrdev(major, name);
    }

    module_init(demo_init);
    module_exit(demo_exit);
    MODULE_LICENSE("GPL");

  • test.c

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    #include <stdio.h>
    #include <unistd.h>
    #include <sys/ioctl.h>
    #include <sys/types.h>
    #include <sys/stat.h>
    #include <fcntl.h>

    #include "chrdev.h"

    const char *pathname = "/dev/chrdev0";
    #define LED_NUM 4

    int main(void)
    {
    int fd, i;
    led_node_t led;

    fd = open(pathname, O_RDWR, 0666);
    if (fd <= 0)
    {
    printf("open failed.\n");
    return -1;
    }

    for (i=1; i<=LED_NUM; i++)
    {
    led.which = i;

    led.status = 0; // 0表示灭
    ioctl(fd, LEDOFF, &led);
    sleep(1);

    led.status = 1; // 1表示亮
    ioctl(fd, LEDON, &led);
    sleep(1);
    }

    close(fd);

    return 0;
    }
  • chrdev.h

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    #ifndef _CHRDEV_H_
    #define _CHRDEV_H_

    typedef struct led_node
    {
    int which;
    int status;
    }led_node_t;

    #define LED_MAGIC 'q'
    #define LEDON _IOW(LED_MAGIC, 0, struct led_node)
    #define LEDOFF _IOW(LED_MAGIC, 1, struct led_node)

    #endif /* chrdev.h */

平台?总线?驱动?设备?

笔者叹:学到这一关,真不容易啊~🤯马上就要拿下Linux驱动的最后一块高地了!

因为Linux发展的太好了,Linux在服务器、PC机、嵌入式都可战可为,随着现代数字系统越来越复杂,接入CPU的外设越来越多,接口种类也各不尽相同,在/dev/目录中映射设备节点的方法,看的人眼花缭乱,在管理驱动上造成了极大的不便。因此类似Windows的设备管理器,Linux采用总线(Bus)模型来描述和管理,并且映射在了文件系统中,可以查看/sys/bus/目录:

1
2
3
4
5
6
7
8
9
10
$ ls /sys/bus/

ac97 edac machinecheck parport sdio workqueue
acpi eisa mdio_bus pci serial xen
cec event_source memory pci-epf serio xen-backend
clockevents gameport mipi-dsi pci_express snd_seq
clocksource gpio mmc platform spi
container hid nd pnp usb
cpu i2c node rapidio virtio
dax isa nvmem scsi vme

眼尖的硬件选手,已经发现这里的子项就是一个个总线名字,耳熟能详的如:USB、PCI、SPI。再随便挑一个子项ls或者tree展开看看,发现每个里头都是统一的2个文件夹(devices、drivers)+3个文件(drivers_autoprobe、drivers_probe、uevent):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ tree -L2 /sys/bus

/sys/bus
├── spi
│   ├── devices
│   ├── drivers
│   ├── drivers_autoprobe
│   ├── drivers_probe
│   └── uevent
├── usb
│   ├── devices
│   ├── drivers
│   ├── drivers_autoprobe
│   ├── drivers_probe
│   └── uevent
...

如果读者查看iic总线下的devicesdrivers,并和笔者一样在虚拟机里跑的Ubuntu会发现:在drivers目录下有很多不同的项(驱动程序),但devices是空的。再观察cpu总线的devicesdrivers,会发现drivers里只有一个processor,而devices里有很多个cpu。

此时,读者应该对这个Linux的“驱动&设备管理器”有些直觉上的认知了吧?

没错,正如下图(的上部分)所绘制的

  1. Linux总线(Bus)模型将所有内容先是按照通信总线进行分类(因为同类的总线,往往有很多共性,这是OOP思想);
  2. 接着在每个总线下划分了驱动(Driver)设备(Device)
  3. 把所有安装的驱动程序(包括使用中的、未来使用的),放到Driver目录下;
  4. 把实际检测到插入的设备,或虚拟的设备,注册后放到Device下;

这样做的好处有:

  • 整体结构清晰,方便查找
  • 多个相同的设备接入,共用一个驱动程序(比如3个一样的MP3通过USB一起插到电脑),特定参数在device中,不在driver中,适应性更强

但有个特殊情况,有些设备本身是没有总线的。比如LED,就是SoC的一个GPIO口子。所以Linux提供了一个专门放杂物的总线——平台platform。下图完整地展示了 总线模型下的 platform总线下的 驱动和设备 分别是如何实现的:

platform

NOTE:这边引用了大佬“火山上的企鹅”发表于CSDN上的文章一张图掌握 Linux platform 平台设备驱动框架


分清楚了总线、平台、驱动、设备这4个概念后,我们来看具体原理和如何使用:

总线原理

Linux内核实现总线的方式是,管理两个链表:设备链表、驱动链表。

  • 向内核注册一个驱动时,便插入到总线的驱动链表
  • 向内核注册一个设备时,便插入到总线的设备链表
  • 在插入链表后(驱动和设备都会),总线会执行bus_type结构体中的match方法对新插入的 设备/驱动 进行匹配。(以name名字的方式匹配,具体见上面大图中的调用)
  • 匹配成功后,会调用注册驱动时device_driver结构体中的probe方法。
  • 同理,移除设备或驱动时,调用device_driver结构体中的remove方法。

总线的使用

  • 相关数据类型:
    • 总线结构体: bus_type
    • 设备结构体: device
    • 驱动结构体: device_driver
  • 相关接口:
    • 注册总线:bus_register。其实Linux已经编好了大部分的总线驱动
    • 注销总线:bus_unregister
    • 注册设备:device_register
    • 注销设备:device_unregister
    • 注册驱动:driver_register
    • 注销驱动:driver_unregister

考虑到使用过程和平台总线很相似,为了节省篇幅,此处不上代码,读者看下小节的代码就懂了。

平台总线(platform bus)使用

  • 驱动

    1. 填充struct platform_driver
    2. 向系统注册驱动platform_driver_register
    3. 注销驱动platform_driver_unregister
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    //btn_drv.c
    #include <linux/init.h>
    #include <linux/module.h>
    #include <linux/platform_device.h>
    #include "btn_desc.h"

    MODULE_LICENSE("GPL");

    //1)填充 struct platform_driver 的各个成员: .probe = btn_probe
    int btn_probe(struct platform_device *dev)
    {
    printk("call %s\n", __func__);
    return 0;
    }

    //1)填充 struct platform_driver 的各个成员: .remove = btn_remove
    int btn_remove(struct platform_device *dev)
    {
    printk("call %s\n", __func__);
    return 0;
    }

    //1)填充 struct platform_driver 的各个成员: 绑定
    //继承于device_driver的 platform_driver 设备驱动
    struct platform_driver btn_drv =
    {
    //struct device_driver driver
    .driver =
    {
    .name = "mybuttons",
    },

    //int (*probe)(struct platform_device *)
    .probe = btn_probe,
    //int (*remove)(struct platform_device *)
    .remove = btn_remove,
    };

    //2)向系统注册驱动:platform_driver_register
    int __init btn_drv_init(void)
    {
    platform_driver_register(&btn_drv); //1.向dev中加入一个节点, 2.与device设备匹配
    return 0;
    }

    //3)注销驱动:platform_driver_unregister
    void __exit btn_drv_exit(void)
    {
    platform_driver_unregister(&btn_drv);
    }
    module_init(btn_drv_init);
    module_exit(btn_drv_exit);
  • 设备

    1. 填充struct platform_device
    2. 向系统注册设备platform_device_register
    3. 注销设备platform_device_unregister
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    //btn_dev.c
    #include <linux/init.h>
    #include <linux/module.h>
    #include <linux/input.h>
    #include <linux/platform_device.h>
    #include <mach/platform.h>
    #include "btn_desc.h"

    MODULE_LICENSE("GPL");

    //1) 填充 struct platform_device 的各个成员:初始化 resource 结构变量。
    struct resource btn_resource[]=
    {
    {
    .start = IRQ_GPIO_A_START + 28,
    .end = IRQ_GPIO_A_START + 28, //可以不赋值
    .flags = IORESOURCE_IRQ, //中断类型资源
    },
    {
    .start = IRQ_GPIO_B_START + 30,
    .end = IRQ_GPIO_B_START + 30, //可以不赋值
    .flags = IORESOURCE_IRQ,
    },
    };

    //1) 填充 struct platform_device 的各个成员:初始化 私有资源 结构变量。
    btn_desc_t buttons[] = //私有资源,描述信息
    {
    {"up", PAD_GPIO_A+28, KEY_UP},
    {"down", PAD_GPIO_B+30, KEY_DOWN},
    };

    //1) 填充 struct platform_device 的各个成员: 注销设备时,调用
    void btn_release(struct device *dev)
    {
    printk("call %s\n", __func__);
    }

    //1) 填充 struct platform_device 的各个成员:
    struct platform_device btn_dev =
    {
    .name = "mybuttons",
    .dev =
    {
    .release = btn_release,
    .platform_data = (void *)buttons, //私有,平台设备总线
    },
    .resource = btn_resource,
    .num_resources = ARRAY_SIZE(btn_resource), //资源个数
    };

    // 2)向系统注册设备:platform_device_register
    int __init btn_dev_init(void)
    {
    platform_device_register(&btn_dev); //1.向dev中加入一个节点, 2.与driver设备匹配
    return 0;
    }

    //3)注销设备:platform_device_unregister
    void __exit btn_dev_exit(void)
    {
    platform_device_unregister(&btn_dev);
    }
    module_init(btn_dev_init);
    module_exit(btn_dev_exit);

设备树Device Tree

为什么设备树放最后呢?因为设备树其实用的最多,改起来简单方便(对着Datasheet输参数),但是出bug的时候,前面的所有知识缺一不可。

设备树作用:硬件细节不写驱动里,外设数量、参数和总线拓扑关系等,放设备树里随改随用,提高了灵活性和内核的整洁性。

关于设备树在Linux(ARM)的起源,有一句“This whole ARM thing is a f*cking pain in the ass”典故。有兴趣自查。

本质上,设备树模型其实和上面的总线模型很像,如下图,设备树的根节点就是系统总线。

DeviceTree

三个常见词:

  • DTS:设备树的源文件.dts,类似JSON格式,给人读写的
  • DTSI:设备树头文件.dtsi,放通用配置,比如ARM Cortex-A7的配置对于一批SoC都能用
  • DTC:编译器,把.dts文件编译成.dtb文件
  • DTB:二进制设备树文件.dtb,给Linux读的

设备树源文件写法

先看后学:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include "common_part_board_header.dtsi"
#include "common_part_header_2.h>

/ {

#address-cells = <1>;
#size-cells = <1>;

aliases {
serial0 = &uart4;
};

cpus {
#address-cells = <1>;
#size-cells = <0>;

cpu0: cpu@0 {
compatible = "arm,cortex-a7";
device_type = "cpu";
reg = <0>;
clocks = <&scmi0_clk CK_SCMI0_MPU>;
clock-names = "cpu";
operating-points-v2 = <&cpu0_opp_table>;
nvmem-cells = <&part_number_otp>;
nvmem-cell-names = "part_number";
#cooling-cells = <2>;
};
};

soc {
compatible = "simple-bus";
#address-cells = <1>;
#size-cells = <1>;
interrupt-parent = <&intc>;
ranges;

sram: sram@10000000 {
compatible = "mmio-sram";
reg = <0x10000000 0x60000>;
#address-cells = <1>;
#size-cells = <1>;
ranges = <0 0x10000000 0x60000>;
};
};
};
  • 可包含.dtsi头文件引入通用的属性,也可以包含标准C形式的头文件.h
  • 每个dts文件只有一个根节点/,包含了其他文件中的设备树之后自动合并根节点
  • 每个设备必是子节点
  • 节点命名方式:node-name@address,后半部分地址(@address)记录设备地址或寄存器首地址,不用可以不要
  • 节点可起标签(label):label: node-name@unit-address
  • 标签可在其他地方快速索引节点:@label
  • 每个节点都可以有:子节点,属性(键值对,如reg=<0>;
  • 属性中的数值可以是:
    • 字符串compatible = "arm,cortex-a7";
    • 字符串列表compatible = "arm,cortex-a7","arm,cortex-a53";
    • 无符号整数reg = <0 0x123456 100>;
  • Linux内核使用的标准属性
    • compatible,兼容性属性:用来匹配驱动程序
    • model,模块信息:描述模块名字
    • status,设备状态,取值(字符串)为:
      • “okay”:可用
      • “disabled”:不可用,但未来可用。常见于热拔插接口
      • “fail”:出错
      • “fail-sss”:sss是出错内容
    • #address-cells,reg属性中地址的字长,字为32位
    • #size-cells,reg属性中长度信息的字长
    • reg,描述设备地址空间,写法:reg = <address1 length1 address2 length2 address3 length3……>
    • ranges,地址映射/转换表,写法:ranges = <child-bus-address parent-bus-address length>;

追加/修改节点内容

使用#include包含了通用.dtsi头文件的内容后,如果要对其中的某个节点内容进行修改,或者增加,应该使用引用

1
2
3
&i2c1 {
/* 要追加或修改的内容 */
};

设备树如何作用于驱动

设备树二进制文件.dtb被Linux内核解读后,相关参数就可以被驱动程序读取了。驱动程序实现中,基础的硬件交互操作都不用改动,主要是模块初始化函数中,加入读取设备树、匹配设备、获取参数的功能。关键接口:

  • 获取节点:of_find_node_by_path
  • 读属性:of_find_property
  • 读字符:of_property_read_string
  • 读数组:of_property_read_u32_array
  • 内存映射:of_iomap

具体见下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
static int __init led_init(void)
{
u32 val = 0;
int ret;
u32 regdata[12];
const char *str;
struct property *proper;


/* 获取设备树中的属性数据 */
/* 1、获取设备节点:stm32mp1_led */
dtsled.nd = of_find_node_by_path("/stm32mp1_led");
if(dtsled.nd == NULL) {
printk("stm32mp1_led node nost find!\r\n");
return -EINVAL;
} else {
printk("stm32mp1_lcd node find!\r\n");
}


/* 2、获取 compatible 属性内容 */
proper = of_find_property(dtsled.nd, "compatible", NULL);
if(proper == NULL) {
printk("compatible property find failed\r\n");
} else {
printk("compatible = %s\r\n", (char*)proper->value);
}


/* 3、获取 status 属性内容 */
ret = of_property_read_string(dtsled.nd, "status", &str);
if(ret < 0){
printk("status read failed!\r\n");
} else {
printk("status = %s\r\n",str);
}



/* 4、获取 reg 属性内容 */
ret = of_property_read_u32_array(dtsled.nd, "reg", regdata, 12);
if(ret < 0) {
printk("reg property read failed!\r\n");
} else {
u8 i = 0;
printk("reg data:\r\n");
for(i = 0; i < 12; i++)
printk("%#X ", regdata[i]);
printk("\r\n");
}

/* 初始化 LED */
// 略


/* 注册字符设备驱动 */
/* 1、创建设备号 */
if (dtsled.major) { /* 定义了设备号 */
dtsled.devid = MKDEV(dtsled.major, 0);
ret = register_chrdev_region(dtsled.devid, DTSLED_CNT,DTSLED_NAME);
if(ret < 0) {
pr_err("cannot register %s char driver [ret=%d]\n",DTSLED_NAME, DTSLED_CNT);
goto fail_map;
}
} else { /* 没有定义设备号 */
ret = alloc_chrdev_region(&dtsled.devid, 0, DTSLED_CNT,DTSLED_NAME); /* 申请设备号 */
if(ret < 0) {
pr_err("%s Couldn't alloc_chrdev_region, ret=%d\r\n",DTSLED_NAME, ret);
goto fail_map;
}
dtsled.major = MAJOR(dtsled.devid); /* 获取分配号的主设备号 */
dtsled.minor = MINOR(dtsled.devid); /* 获取分配号的次设备号 */
}
printk("dtsled major=%d,minor=%d\r\n",dtsled.major,
dtsled.minor);


/* 2、初始化 cdev */
dtsled.cdev.owner = THIS_MODULE;
cdev_init(&dtsled.cdev, &dtsled_fops);

/* 3、添加一个 cdev */
ret = cdev_add(&dtsled.cdev, dtsled.devid, DTSLED_CNT);
if(ret < 0)
goto del_unregister;

/* 4、创建类 */
dtsled.class = class_create(THIS_MODULE, DTSLED_NAME);
if (IS_ERR(dtsled.class)) {
goto del_cdev;
}

/* 5、创建设备 */
dtsled.device = device_create(dtsled.class, NULL, dtsled.devid, NULL, DTSLED_NAME);
if (IS_ERR(dtsled.device)) {
goto destroy_class;
}

return 0;
destroy_class:
class_destroy(dtsled.class);
del_cdev:
cdev_del(&dtsled.cdev);
del_unregister:
unregister_chrdev_region(dtsled.devid, DTSLED_CNT);
fail_map:
led_unmap();
return -EIO;
}



static void __exit led_exit(void)
{

led_unmap();

cdev_del(&dtsled.cdev);

unregister_chrdev_region(dtsled.devid, DTSLED_CNT);
device_destroy(dtsled.class, dtsled.devid);
class_destroy(dtsled.class);
}
module_init(led_init);
module_exit(led_exit);

设备树从上电到下岗

上面我们说过U-Boot的作用是进行简单基础的初始化,然后引导系统,其实设备树也就是在这个时候开始被加载的,并在内存中被作为参数一样传递给启动的Linux内核。具体过程为:

  1. 系统上电后,U-Boot从SDMMC/EMMC中读取设备树文件(编译好的dtb),并放在内存中;
  2. U-Boot引导Linux内核启动,传递设备树的内存地址(指针)给Linux;
  3. Linux内核解析设备树内容,转换成platform_device类型数据,并注册“设备”进系统;
  4. 驱动程序模块逐个加载,基于platform_driver_register接口注册platform_driver的“驱动”进系统;
  5. 系统通过匹配OF表,在probe方法中,把真正的硬件驱动程序跑起来。(详细匹配原理见上面讲平台的那张大图)。
  • Copyright: Copyright is owned by the author. For commercial reprints, please contact the author for authorization. For non-commercial reprints, please indicate the source.
  • Copyrights © 2022-2023 RY.J
  • Visitors: | Views:

请我喝杯咖啡吧~

支付宝
微信