模块 -> 字符设备驱动 -> 平台总线 -> 设备树 -> 中断 -> 优化机制

# 开发环境搭建

env set -f ethaddr 00:80:E1:42:60:10
env set serverip 192.168.60.6
env set gatewayip 192.168.60.1
env set ipaddr 192.168.60.8
env set -f bootcmd tftp 0xc2000000 uImage\;tftp 0xc4000000 stm32mp157a-fsmp1a.dtb\;bootm 0xc2000000 - 0xc4000000
env set -f bootargs root=/dev/nfs nfsroot=192.168.60.6:/opt/rootfs intr rsize=1024 wsize=1024 rootwait rw earlyprintk ip=192.168.60.8 console=ttySTM0,115200 init=/linuxrc
env save
run bootcmd
1
2
3
4
5
6
7
8

命令行传参

# 模块

# 简单字符驱动

// 申请设备号
register_chrdev()
// 创建类
class_create()
// 创建设备节点
device_create()
// 硬件初始化
ioremap()

PTR_ERR()
IS_ERR()
1
2
3
4
5
6
7
8
9
10
11

copy_from_user

GFP_KERNEL是 linux 内存分配器的标志,标识着内存分配器将要采取的行为,是内核内存分配时最常用的, 无内存可用时可引起休眠。

I/O remap

几乎每一种外设都是通过读写设备商店相关寄存器来进行的,通常包括控制寄存器、状态寄存器和数据寄存器三大类,外设的寄存器通常被连续地址编址。

CPU 体系不同,通常对 IO 端口的编址方式有两种:

1 I/O 映射方式

x86 处理器为外设专门实现了一个单独的地址空间,称为 I/O 地址空间或者 I/O 端口空间, CPU 通过专门的 I/O 指令来访问这一空间中的地址单元。

2 内存映射方式

RISC 指令系统的 CPU 通常只实现一个物理地址空间,外设 I/O 端口成为内存的一部分。 CPU 可以像访问内存单元一样访问外设 I/O 端口,而不需要设立专门的外设 I/O 指令。

这两种方法在硬件实现上的差异对于软件来说是完全透明的,驱动开发人员可以将内存映射方式的 I/O 端口和外设内存统一看作是 I/O 内存资源。

一般来说,系统运行时,外设的 I/O 内存资源的物理地址是已知的,由硬件的设计决定,但是 CPU 通常并没有为已知的外设 I/O 内存资源的物理地址预定义虚拟地址范围,驱动程序 必须将它们映射到核心虚地址空间内(通过页表),然后才能根据映射所得到的核心虚地址范围,通过访存指令访问这些 I/O 内存资源。

linux 在 io.h 中声明了函数 ioremap(),用来将 I/O 内存资源的物理地址映射到核心虚拟地址空间(3Gb-4Gb)中,也就是内核空间。

将 I/O 内存资源的物理地址映射成核心虚拟地址后,理论上讲我们就可以像读写 RAM 一样直接读写 I/O 内存资源了。

示例:

#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/device.h>
#include <linux/io.h>
#include <linux/slab.h>
#include <linux/uaccess.h>
#include "stm32mp157.h"

#define PRINTK printk

struct stm32mp157_led{
    int major;
    struct class *class;
    struct device *dev;
    struct gpio *gpioe;
    struct gpio *gpiof;
    struct gpio *gpioz;
    struct rcc *rcc;
};

struct stm32mp157_led *led;

int led_open(struct inode * node, struct file * filp)
{
    static int a = 10;
    filp->private_data = &a;
    printk("----------%s----------\n",__FUNCTION__);
    return 0;
}

int led_release(struct inode * node, struct file * filp)
{
    printk("----------%s----------\n",__FUNCTION__);
    return 0;
}

ssize_t led_write(struct file * filp, const char __user * buff, size_t count, loff_t * pos)
{
    int ret;
    int flag;
    struct inode * node;
    int major;
    int minor;
    int *num;
    printk("----------%s----------\n",__FUNCTION__);
    node = filp->f_path.dentry->d_inode;
    major = imajor(node);
    minor = iminor(node);
    printk("major=%d\n",major);
    printk("minor=%d\n",minor);
    num = (int *)filp->private_data;
    printk("a=%d\n",*num);
    ret = copy_from_user(&flag, buff, count);
    if(ret > 0)//表示没有拷贝成功地个数,失败
    {
        printk("copy_from_user error\n");
        return -EFAULT;
    }
    if(flag == 1)//开灯
    {
        led->gpioz->ODR |= (0x1 << 5)|(0x1 << 6)|(0x1 << 7);
        led->gpioe->ODR |= (0x1 << 8)|(0x1 << 10);
        led->gpiof->ODR |= (0x1 << 10);
    }
    else
    {
        led->gpioz->ODR &= ~((0x1 << 5)|(0x1 << 6)|(0x1 << 7));
        led->gpioe->ODR &= ~((0x1 << 8)|(0x1 << 10));
        led->gpiof->ODR &= ~(0x1 << 10);
    }
    return 0;
}

// 文件操作
const struct file_operations led_fops = {
    .open = led_open,
    .release = led_release,
    .write = led_write
};


//入口函数
static int __init led_drv_init(void)
{
    int ret = 0;
    // * 创建全局的结构体对象,GFP_KERNEL
    led = kzalloc(sizeof(struct stm32mp157_led), GFP_KERNEL);
    if (IS_ERR(led)) {
        PRINTK("kzalloc error\n");
        return -ENOMEM;
    }

    // * 申请设备号
    led->major = register_chrdev(0, "led_drv01", &led_fops);
    if (led->major < 0) {
        PRINTK("register_chrdev error\n");
        ret = led->major;
        goto CHR_DEV_ERR;
    }

    // * 创建类
    led->class = class_create(THIS_MODULE, "led_drv01");
    if (IS_ERR(led->class)) {
        PRINTK("class_create error\n");
        ret = PTR_ERR(led->class);
        goto CLASS_CREATE_ERR;
    }

    // * 创建设备节点
    led->dev = device_create(led->class, NULL, MKDEV(led->major, 0), NULL, "led_drv01");
    if (IS_ERR(led->dev)) {
        PRINTK("device_create error\n");
        ret = PTR_ERR(led->dev);
        goto DEVICE_CREATE_ERR;
    }

    // * 硬件初始化
    // 物理地址转换为虚拟地址
    led->gpioe = ioremap(GPIOE, sizeof(struct gpio));
    if (IS_ERR(led->gpioe)) {
        PRINTK("ioremap error\n");
        ret = PTR_ERR(led->gpioe);
        goto IOREMAP_ERR1;
    }
    led->gpiof = ioremap(GPIOF, sizeof(struct gpio));
    if (IS_ERR(led->gpiof)) {
        PRINTK("ioremap error\n");
        ret = PTR_ERR(led->gpiof);
        goto IOREMAP_ERR2;
    }
    led->gpioz = ioremap(GPIOZ, sizeof(struct gpio));
    if (IS_ERR(led->gpioz)) {
        PRINTK("ioremap error\n");
        ret = PTR_ERR(led->gpioz);
        goto IOREMAP_ERR3;
    }
    led->rcc = ioremap(RCC, sizeof(struct rcc));
    if (IS_ERR(led->rcc)) {
        PRINTK("ioremap error\n");
        ret = PTR_ERR(led->rcc);
        goto IOREMAP_ERR4;
    }

    // 使能时钟
    led->rcc->PLL4CR|= (1<<0);
    while((led->rcc->PLL4CR & (1<<1)) == 0);
    led->rcc->MP_AHB4ENSETR |= (1 << 4)|(1 << 5);

    led->gpioz->MODER &= ~((0x3 << 10)|(0x3 << 12)|(0x3 << 14));
    led->gpioz->MODER |= (0x1 << 10)|(0x1 << 12)|(0x1 << 14); //设置为输出模式
    led->gpioz->OTYPER &= ~((0x1 << 5)|(0x1 << 6)|(0x1 << 7));//推娩输出
    led->gpioz->OSPEEDR &= ~((0x3 << 10)|(0x3 << 12)|(0x3 << 14)); //低速
    led->gpioz->ODR &= ~((0x1 << 5)|(0x1 << 6)|(0x1 << 7));

    led->gpioe->MODER &= ~((0x3 << 16)|(0x3 << 20));
    led->gpioe->MODER |= (0x1 << 16)|(0x1 << 20); //设置为输出模式
    led->gpioe->OTYPER &= ~((0x1 << 8)|(0x1 << 10));//推娩输出
    led->gpioe->OSPEEDR &= ~((0x3 << 16)|(0x3 << 20)); //低速
    led->gpioe->ODR &= ~((0x1 << 8)|(0x1 << 10));

    led->gpiof->MODER &= ~(0x3 << 20);
    led->gpiof->MODER |= (0x1 << 20); //设置为输出模式
    led->gpiof->OTYPER &= ~(0x1 << 10);//推娩输出
    led->gpiof->OSPEEDR &= ~(0x3 << 20); //低速
    led->gpiof->ODR &= ~(0x1 << 10);

    return 0;


IOREMAP_ERR4:
    iounmap(led->rcc);
IOREMAP_ERR3:
    iounmap(led->gpioz);
IOREMAP_ERR2:
    iounmap(led->gpiof);
IOREMAP_ERR1:
    iounmap(led->gpioe);
DEVICE_CREATE_ERR:
    class_destroy(led->class);
CLASS_CREATE_ERR:
    unregister_chrdev(led->major, "led_drv01");
CHR_DEV_ERR:
    kfree(led);

    return ret;
}

//出口函数
static void __exit led_drv_exit(void)
{
    iounmap(led->rcc);
    iounmap(led->gpioz);
    iounmap(led->gpiof);
    iounmap(led->gpioe);
    device_destroy(led->class, MKDEV(led->major, 0));
    class_destroy(led->class);
    unregister_chrdev(led->major, "led_drv01");
    kfree(led);
}


//定义入口
module_init(led_drv_init);
module_exit(led_drv_exit);
MODULE_LICENSE("GPL");
MODULE_ALIAS("led_drv01");//别名
MODULE_AUTHOR("shachi");//作者
MODULE_DESCRIPTION("测试;点灯");//描述
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

调用示例:

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <stdlib.h>

int main(void)
{
    int fd;
    int flag;
    fd = open("/dev/led_drv01", O_RDWR);
    if(fd < 0)
    {
        perror("open");
        exit(1);
    }

    while(1)
    {
        flag = 1;
        write(fd, &flag, sizeof(flag));
        sleep(1);
        flag = 0;
        write(fd, &flag, sizeof(flag));
        sleep(1);
    }
    close(fd);
}
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

Makefile:

KERNEL_DIR := /home/shachi/fsmp1a/linux-5.4.31
CUR_DIR := $(shell pwd)
DRV_NAME := led_drv01
APP_NAME := led_app
obj-m += $(DRV_NAME).o

.PHONY: all clean install check

all:
	make -C $(KERNEL_DIR) M=$(CUR_DIR) modules
	$(CC) -o $(APP_NAME) $(APP_NAME).c

clean:
	make -C $(KERNEL_DIR) M=$(CUR_DIR) clean
	rm -rf $(APP_NAME)

install:
	cp -raf *.ko /opt/rootfs/module_drvs
	cp -raf $(APP_NAME) /opt/rootfs/module_drvs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

ioctl 用来用户空间给内核空间发命令,类似 copy_from_user()。命令直接发送到内核。

直接操作寄存器:

_IOW() 宏。

_IO() 宏。

unlock_ioctl() 函数。

通过内核提供的函数操作:

readl_relaxed()

通过设备树操作 GPIO

register_chrdev_region()申请设备号,静态分配。

cdev_alloc(),任何一个字符设备都对应一个 cdev 结构体变量(对象)。

cdev_init()

cdev_add()

先申请设备号再把设备号和 file_operation 关联起来。

alloc_chrdev_region() 动态分配。

杂项设备

<linux/miscdevice.h>

注册杂项设备 misc_register()。 取消注册杂项设备:misc_deregister()

所有的杂项设备主设备号都是 10,自己指定次设备号。

imajor()

iminor()

系统调用

open()// SYSCALL_DEFINE3(open, ...)
// ->
do_sys_open()
// ->
get_unused_fd_flags() // 查找一个未被使用的 fd
// ->
f = do_filp_open() // 创建 struct *file
// ->
fd_install(fd, f) // 把 f 放入 fd 中。


do_filp_open() 中:

path_open_at()

// ->

do_o_path()

// ->
vfs_open()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
read() -> __vfs_read() -> file->f_op->read
1

# 重要结构体

struct file_operation

struct cdev

struct inode

struct file

struct miscdevice

# 杂项设备驱动

# 内存分配

kmalloc ,vmalloc,kzalloc

kfree

-ENOMEM

# 内存文件系统

查阅芯片手册,GPIO 的基地址。

RCC 的基地址

module_param()

cat /proc/devices 查看设备号

ls /dev 查看设备节点

modinfo

mknod

/dev 下设备结点

vmalloc

kazlloc

kmalloc

GFP_KERNEL

-ENOMEM

原理图

芯片手册

使能时钟-> 配置寄存器

ioctl

内核读写寄存器 readl_relaxed writel_relaxed

printk 控制打印级别。/proc/sys/kernel/printk

多个文件编译成一个模块

模块间的调用,通过 EXPORT_SYMBOL() 宏。

#define PRINTK printk
1

内存文件系统:(目录中的内容在内存中),上电有内容,掉电就没有

/sys 存放驱动信息

/sys/class

/sys/dev

/sys/bus

/sys/devices

/dev 存放设备节点

/proc 存放正在进行的进程相关的信息

Kset 结构体

Kobject 结构体

Ktype 结构体

平台总线。 platform.c

平台驱动(相似操作) -> 平台总线 -> 平台设备(差异化数据:地址、中断、自定义数据)。

struct platform_device{
    id_table;(名字列表)
    probe;(匹配成功,调用 probe 函数)
    remove;
    ...
}
1
2
3
4
5
6

平台总线 /sys/bus/platform/drivers 下创建目录

平台驱动匹配 id_table 不成功就会匹配 driver.name

最好不要写死操作,只更改差异化数据,将数据通过 platform_device 传过去。

platform_device 结构体的 dev.platform_data 可以接受数据。

平台总线主要是解决了相似代码冗余、代码移植的问题,写法就是把以前的驱动程序拆成两个程序: 平台驱动和平台设备,前者里面是相似的操作过程,后者是差异化的数据。

简单字符设备驱动:

  1. 创建全局结构体对象。
  2. 申请设备号。
  3. 创建类、设备节点。
  4. 硬件初始化(映射、读写)。
  5. 实现 fops(读、写、ioctl)。

也可以采用杂项设备写法。

问题:

  1. 相似设备代码冗余。
  2. soc 升级代码难以复用。

解决方法:

平台总线(框架思维):平台驱动和平台设备两个代码,平台总线匹配成功后会调用平台驱动的 probe 函数, 获取平台设备的数据传到平台驱动,在 probe 函数中进行创建节点、创建类、硬件初始化等操作。

第四天

设备树。

操作系统不关心外设的板级信息,但为了尽可能兼容尽量多的芯片、外设,同时为了防止代码膨胀,引入了 设备树。

  1. 防止大量板级信息编译到内核。
  2. 防止板级信息被修改就会导致内核重新编译。

将板级信息写成配置文件,单独编译,放置在 arch/arm/boot/dts 目录下,这样,修改设备树就不会 影响内核代码。

dts 设备树源文件(板子私有数据)。

dtsi 设备树头文件,公共的资源。

dtc 编译器编译成 dtb 文件,内核加载解析成节点 node,转化成 platform_device。

platform_driver 结构体的 of_match_table 函数。

将 dtb 反编译成 dts 查看 address-cells 和 size-cells 。

获取设备树中自定义数据:

of_property_count_strings()

of_property_read_string_array()

of_property_read_string_index()

of_property_count_u32_elems()

of_property_read_u32()

of_property_read_u32_array()

of_property_read_bool() 判断是否存在。

of_get_child_by_name()

设备树的转化过程:

i2c、spi 等总线节点的子节点,应该交给对应的总线驱动程序来处理,它们不应该转换为 platform_device。

GPIO

先把所用引脚配置为 GPIO 功能,通过 pinctrl 子系统实现。

最后更新: 7/10/2023, 10:46:13 AM