相关链接:https://www.cnblogs.com/lxq-247A3/p/16337398.html

1.字符设备和块设备

字符设备:能够像字节流一样被访问的设备,驱动程序至少实现open,close,read和write系统调用。字符终端、串口、鼠 标、键盘、摄像头、声卡和显卡等。

块设备:可以存取任意字节数,能够容纳文件系统。如:u盘,SD卡,磁盘等。

网络设备:可以是一个硬件设备,或者是软件设备,没有相应的read write,面向流的一种特殊设备。

相同点:都是通过/dev目录下的文件系统节点来访问。

系统调用:read、open、close、write,注意fopen

2.驱动加载

静态加载:把驱动直接编译到内核中,这种加载方式比较麻烦,特别是我们需要修改驱动的时候,就需要每次都编译内核,编译内核是比较费时间的

动态加载:使用insmod命令添加模块(.ko),在不需要的时候用rmmod命令卸载模块,采用这种动态加载的方式便于驱动程序的调试,同时可以针对产品的功能需求,进行内核的裁剪,将不需要的驱动去除,大大减小了内核的存储容量。

3.查看模块相关信息

  1. 查看驱动模块中打印信息的命令:dmesg

  2. 查看字符设备信息可以用lsmod 和modprobe

    • lsmod可以查看模块的依赖关系

    • modprobe在加载模块时会加载其他依赖的模块

  3. 显示当前使用的中断号cat /proc/interrupt

4.主设备号和次设备号

创建chartest设备:mknod chartest c 4 64

主设备号:标识设备对应的驱动程序,Linux中为12位

次设备号:通过次设备号获得一个指向内核设备的直接指针,也可将此设备号当作设备本地数组的索引。占20位

chartest 由驱动程序4管理,该文件所指的设备是64号设备

5.内核和用户空间之间的拷贝

copy_to_user():完成内核空间到用户空间的复制

copy_from_user():完成用户空间到内核空间的复制

一般用于file_operations(文件操作结构体)里的read,write,ioctl等内存数据交换作用的函数。如果ioctl没有用到内存数据复制,那么就不会用到这两个函数。

6.引入模块机制的好处

该机制有助于缩短模块的开发周期。即:注册和卸载都很灵活方便。

7.字符设备基本框架

7.1 模块加载

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


```cpp
#include <linux/init.h>
#include <linux/module.h>

/* 驱动入口函数 */
static int __init xxx_init(void)
{
/* 入口函数具体内容 */
return 0;
}
/* 驱动出口函数 */
static void __exit xxx_exit(void)
{
/* 出口函数具体内容 */
}
/* 将上面两个函数指定为驱动的入口和出口函数 */
module_init(xxx_init);
module_exit(xxx_exit)
MODULE_LICENSE("GPL");//模块GPL许可声明
MODULE_AUTHOR("VICCZYQ");//模块作者等信息
```

```shell
sudo insmod hello.ko//加载模块
sudo rmmod hello//卸载模块
```

**注意在init函数中申请的资源在exit函数中要释放**

对于字符设备驱动而言,当驱动模块加载成功以后需要注册字符设备。卸载驱动模块的时也需要注销掉字符设备。

7.2 注册字符设备驱动

对于字符设备驱动而言,当驱动模块加载成功以后需要注册字符设备。卸载驱动模块的时也需要注销掉字符设备。一般字符设备的注册驱动模块的入口函数 xxx_init 中进行,字符设备的注销驱动模块的出口函数 xxx_exit 中进行。

1
2
3
4
5
6
7
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)

7.3 内存映射

在Linux中不能直接访问寄存器,要想要操作寄存器需要完成物理地址到虚拟空间的映射。物理内存和虚拟内存之间的转换,需要用到: ioremap 和 iounmap两个函数

1
2
3
#define addr (0X020E0068)  // 物理地址
static void __iomem* va; //指向映射后的虚拟空间首地址的指针
va=ioremap(addr, 4); // 得到虚拟地址首地址

注意卸载时候要用iounmap

1
iounmap(va);

7.4 数据传递

1
2
3
4
5
static inline long copy_from_user(void *to, const void __user * from, unsigned long n)
static inline long copy_to_user(void __user *to, const void *from, unsigned long n)
//to:目标地址
//from:源地址
//n:拷贝的字节数

7.5 字符设备最基本框架

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
#define CHRDEVBASE_MAJOR 200			//手动设置主设备号
#define CHRDEVBASE_NAME "chrdevbase" //设备名称
//内核缓存区
static char readbuf[100]; //读数据缓存
static char writebuf[100]; //写数据缓存
static char kerneldata[] = {"kernel data!"}; //测试数据
//硬件寄存器
#define GPIO_TEST_BASE (0x01234567) //宏定义寄存器映射地址
static void __iomem *GPIO_TEST; // __iomem 类型的指针,指向映射后的虚拟空间首地址
//打开设备
static int chrdevbase_open(struct inode *inode, struct file *filp)
{
return 0;
}
// 从设备读取数据
static ssize_t chrdevbase_read(struct file *filp , char __user *buf , size_t cnt , loff_t *offt)
{
int retvalue = 0;
unsigned char databuf[1];
// 读取硬件寄存器
#if 0
//读取寄存器状态
databuf[0] = readl(GPIO_TEST);
retvalue = copy_to_user(buf , databuf, cnt);
//读取内核内存
#else
//测试数据拷贝到读数据缓存中
memcpy(readbuf , kerneldata , sizeof(kerneldata));
//内核中数据(读缓存)拷贝到用户空间
retvalue = copy_to_user(buf , readbuf , cnt);
#endif

if(retvalue == 0) printk("kernel senddate ok!\n");
else printk("kernel senddate failed!\n");
return 0;
}
//向设备写数据
static ssize_t chrdevbase_write(struct file *filp, const char __user *buf, size_t cnt , loff_t *offt)
{
int retvalue = 0;
//写硬件寄存器
#if 0
writel(buf[0],GPIO_TEST);
//写内核缓存
#else
//用户数据拷贝到内核空间(写缓存)
retvalue = copy_from_user(writebuf , buf ,cnt);
#endif
if(retvalue == 0) printk("kernel recevdate : %s\n",writebuf);
else printk("kernel recevdate failed!");
return 0;
}
//关闭/释放设备
static int chrdevbase_release(struct inode *inode , struct file *filp)
{
return 0;
}
//设备操作函数
static struct file_operations chrdevbase_fops = {
.owner = THIS_MODULE,
.open = chrdevbase_open,
.read = chrdevbase_read,
.write = chrdevbase_write,
.release = chrdevbase_release,
};
/* 驱动入口函数 */
static int __init chrdevbase_init(void)
{
int retvalue = 0;
//寄存器物理映射,物理地址映射到虚拟地址指针
GPIO_TEST= ioremap(GPIO_TEST_BASE, 4);
//注册字符设备驱动
retvalue = register_chrdev(CHRDEVBASE_MAJOR, //主设备号
CHRDEVBASE_NAME, //设备名称
&chrdevbase_fops); //设备操作函数集合

if(retvalue < 0) printk("chrdevbase driver register failed\n");
printk("chrdevbase_init()\r\n");
return 0;
}
/* 驱动出口函数 */
static void __exit chrdevbase_exit(void)
{
//解除寄存器映射
iounmap(GPIO_TEST);
//注消字符设备驱动
unregister_chrdev(CHRDEVBASE_MAJOR , //主设备号
CHRDEVBASE_NAME); //设备名称
printk("chrdevbase_exit()\r\n");
}
/* 将上面两个函数指定为驱动的入口和出口函数 */
module_init(chrdevbase_init);
module_exit(chrdevbase_exit);

MODULE_LICENSE("GPI");//GPL模块许可证
MODULE_AUTHOR("songwei");//作者信息

7.6 创建驱动节点文件

1
mknod /dev/chrdevbase c 200 0

创建完成以后就会存在/dev/chrdevbase 这个文件,可以使用“ls /dev/chrdevbase -l”命令查看

8.DMA和中断

DMA:一种无须CPU的参与就可以让外设系统内存之间进行数据传输的硬件机制

中断:是指CPU在执行程序的过程中,出现了某些突发事件时CPU必须暂停执行当前的程序,转去处理突发事件,处理完毕后CPU又返回源程序被中断的位置并继续执行。

中断和DMA的区别就是DMA不需CPU参与而中断是需要CPU参与的。

9.驱动的Makefile框架

1
2
3
4
5
6
BASE_KERNEL ?= 内核的源码目录
obj-m += 驱动源文件名.o
all:
make -C $(BASE_KERNEL) M=$(PWD) modules
clean:
make -C $(BASE_KERNEL) M=$(PWD) clean

eg.驱动源文件为abc.c和hello.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 若是虚拟机自身内核,可以定义变量为: /lib/modules/$(shell uname -r)/build
# 定义内核源码根目录
BASE_KERNEL ?= /lib/modules/$(shell uname -r)/build

# 定义目标名
TARGET := hello

# 将多个对象文件合并到同一行
obj-m := abc.o hello.o

all:
make -C $(BASE_KERNEL) M=$(PWD) modules

clean:
make -C $(BASE_KERNEL) M=$(PWD) clean

10 启动过程

10.1 U-boot 启动流程

第一阶段:

  • 硬件设备初始化
  • 将第二阶段的代码加载到RAM
  • 设置好栈空间
  • 跳转到第二阶段代码入口处

第二阶段:

  • 初始化本阶段的硬件设备
  • 将Linux内核从Flash读入RAM
  • 为Linux内核设置启动参数
  • 调用Linux内核

10.2 Linux系统启动过程

  1. 内核引导
  2. 运行初始化,init
  3. 系统初始化
  4. 建立终端
  5. 用户登录系统