Bootstrap

杂项驱动开发

预告

-- 1、杂项驱动设备开发

-- 2、linux2.6设备驱动开发

-- 3、设备树(驱动的分层思想)

-- 4、平台设备总线(驱动分层)

-- 5、内核接口

目录

预告

回顾

驱动开发的基础知识

linux的分层思想

上半节课的总结

驱动开发的框架

-- 1、写一个内核框架

-- 2、在内核框架-》把设备抽象为文件(这就需要讲内核驱动的接口)

-- 3、编译成.ko-》用insmod xxx.ko-》生成设备文件(/dev/xxxx)

-- 4、调用加载/入口函数(这里面有你写的内核框架),然后生成设备文件

-- 5、上层人员/你,通过调用文件操作,来操作设备

设备驱动文件的特点

-- 1、是系统的特殊文件之一:

-- 2、设备驱动文件分为三大类:字符设备文件,块设备文件,网络设备文件。

-- 3、所有的设备文件都有一个设备号

-- 4、字符设备传输按字节来传输的

杂项的驱动开发

-- 1、杂项驱动设备文件的特点

-- 2、杂项驱动开发的接口

misc_register函数,叫做杂项的注册函数

misc_deregister函数,叫做杂项的取消注册

-- 3、写一个杂项驱动

补充:c90错误

GPIO子系统

1、什么是GPIO子系统

2、GPIO子系统的接口

-- (1)gpio_request();

-- (2)gpio_free();//释放不再使用这个IO口

-- (3)gpio_direction_input();//设置引脚为输入模式

-- (4)gpio_direction_output();//调节引脚为输出模式

-- (5)gpio_set_value();//设置引脚当前的输出状态

-- (6)gpio_get_value();//获取当前引脚的状态-》不限制是输入还是输出

杂项驱动下的beep


回顾

-- 系统分为两层:一层是系统层,一层是内核层

-- 我们之前所写的所有的程序,都是运行在系统层面的(这个层面的特点:安全,可靠,内存都是虚拟的)

-- 之前我们所学习的系统层面的知识包括接口包括框架,在内核层是无法使用的,所以我们学习了一个内核层运行的框架

-- 之后引入内核层的框架


-- 该驱动代码直接编译百分百报错!他是运行在内核层的,靠内核编译!

-- 所以我们提供了一个通用的Makefile(适用于任何的驱动编译,任意的内核版本)

-- !insomd加载函数,rmmod卸载函数

  • 注意:不能连续执行两次加载函数(因为内核的代码,内核的驱动只能被加载一次)

-- 下午还带着大家安装了一个软件:source insight 4.0 -》原则上是收费的

  • 作用就是看内核:大概有57000个文件

  • 这么大的数据量,放到不管哪个代码阅读软件身上,都是不可能的,例如放在vscode上,运行一段时间直接会死掉。

-- 目前据我了解,也就sorce insight可以打开这么大的文件,但是同步的时间较为漫长

驱动开发的基础知识

-- 1、何为驱动?

  • 驱使硬件正常工作的代码就叫做驱动

在stm32里面:
无非就是编写寄存器代码初始化外设-》通信传感器
在linux下驱动:
不像stm32一样

例如:LED灯
stm32:
初始化GPIO寄存器(库函数)
在main-》点灯/关灯/闪灯
Linux下:
也可以像STM32一样在内核层直接开灯。 但是内核层不断的加载卸载实际上很浪费资源,效率很低。linux下内核模块代码,是独立运行的单元
很难像stm32那样一个代码一个工程代表整个单片机运行程序

就算是我stm32一样写代码把等的功能写死-》运行在内核层! 请问系统开发工程师(不懂驱动的),他怎么操作LED灯。

linux的分层思想

-- STM32分层

alt text

-- linux分层

alt text

-- linux因为分层(在软件和硬件之间多了个中间层),不管换什么平台-》开发驱动的框架,代码都是一样的。

中间层:实现软硬件分离

-- 中间层就一个目的:统一开发接口

-- 在linux下分层思想处处可见,不仅仅把驱动做了分层,底层的各种外设也做了大量的分层

alt text

-- 例如SPI分为

  • linux通用接口层
  • 厂商BSP适配的驱动层

-- 甚至最新的platfrom也在统一分层思想:

-- 一个简简单单的驱动他也想分为两层:

  • 硬件信息:提供LED风的引脚和电平状态

  • 通用驱动:LED灯驱动-》没有指定具体LED灯的引脚,电平状态


-- linux最终发展:把厂商的驱动也做了分层

-- 最终的框架是这样的,不仅把我们写的驱动给分层了,把厂商的驱动也做了分层,分为硬件信息和底层驱动

alt text


-- linux下你所学习的所有接口,所有的函数都是真通用!(适用于任意版本的linux(除了linux2.6之前)),适用于任意的linux硬件/包括安卓/包括开源鸿蒙。


-- 假如我写了一个驱动,你作为智能家居的软件开发工程师,你想在你的应用里面控制我写的驱动底层的LED灯!难道你要再学一下驱动吗?

-- 真正的Linux驱动:
不像STM32一样把驱动写死了,首先linux下的驱动符合驱动的原则:趋势硬件工作

linux做的主要工作就是把这个LED灯的这个设备在内核层“抽象”为一个文件(设备文件)

我们内部写的驱动就是在完成这样的事-》(把硬件变成文件)

-- 系统上层开发者他操作底层设备就是在操作文件

以LED灯为例:
我的LED灯的设备,经过我写的驱动,就会在/dev/目录下生成一个文件/dev/myled

上层开发者打开/dev/myled文件,-》灯就开了       
上层开发者关闭/dev/myled文件,-》灯就灭了

上半节课的总结

-- linux分层思想:

主要是想告诉大家,linux下的所有的函数接口是通用的,隔离硬件的,软硬件分离,你写的代码原则适用于任何的平台

-- 实际上在发展中:

把驱动也做了分层,分为硬件层(硬件信息层)和软件层(软件代码层)

  • 而软件代码层就在所有的平台通用了,而硬件信息层就渐渐演变成了设备树,

bsp厂商把驱动分为两层:硬件层,软件层


-- 我们现在初学的linux下的驱动,暂时不考虑分层

-- linux下驱动的特点:不是直接在内核层操作硬件,而是写好“接口“,这个接口就是文件(文件的打开、关闭、读写),让上层操作。


驱动开发的框架

刚才大家也看到了一个真实的驱动:
1:为什么要生成设备文件
2:为什么不再内核直接操作 LED 灯
3:为什么要用上层操作 LED 灯
⚫ 这三个问题
可以用一个答案解决:
便于管理 使用 通用。
我感觉最合适的原因:他就是为了让上层人员使用底层的硬件!
为什么安卓开发工程师它可以使用闪光灯!->强灯->IO 口!->LED
之所以生成设备文件:
就是为了让上层方便操作
以便于后续项目开发!
你写的驱动就是把硬件所有可操作的接口留给上层
留给上层就是留给用户!

-- 驱动框架肯定运行在内核层

-- 1、写一个内核框架

-- 2、在内核框架-》把设备抽象为文件(这就需要讲内核驱动的接口)

我们讲的第一个内核驱动的接口就是杂项驱动(开发驱动最简单的方法)

怎么把设备抽象为文件呢?

  • 1、设备的设备号(也就是设备ID),让内核管理
  • 2、还要有设备内核的操作接口,就是文件操作接口(内核驱动开发者要单独实现一套)

例:其实就是上层调用read,然后去调用内核层的read函数

-- 你写的内核层的open,close,read,write跟上层(也就是系统层)是一一对应的。这也是你留给上层人员的操作接口。

-- 3、编译成.ko-》用insmod xxx.ko-》生成设备文件(/dev/xxxx)

-- 4、调用加载/入口函数(这里面有你写的内核框架),然后生成设备文件

-- 5、上层人员/你,通过调用文件操作,来操作设备

设备驱动文件的特点

-- 1、是系统的特殊文件之一:

(系统的特殊文件都有哪些,有管道,套接字,块设备文件,字符设备文件),这些文件都有个特点,都用低级io(也就是非缓冲区IO)来操作文件(例如open,而不是fopen)

-- 2、设备驱动文件分为三大类:字符设备文件,块设备文件,网络设备文件。

  • 字符设备文件(char):一般指的是除了存储和网络设备之外的所有的其他设备,包括鼠标,按键,触摸屏,LED灯,蜂鸣器,键盘,摄像头,LCD

//文件前面的权限前面的C

  • 块设备文件(是非常少解除的开发驱动的类型):
    正常的芯片,(一般是厂商都完成了这类芯片的初始化)
    小型存储器(SPI_SPIFLASHS),也很少见到

  • 网络设备文件:基本上你开发网络设备只会开发两种:wifi,4G/5G

-- 所以说我们关注的点就是字符设备文件

-- 3、所有的设备文件都有一个设备号

-- 设备文件都有一个设备号,设备号是一个独立且不重复的数字

-- 设备号原则上是一个32bit的无符号数字

-- 所以理论来说内核最大的挂载的设备-》2^32->40亿个设备

-- 分为主设备号(占设备号的高12bit),次设备号(占设备号的高20bit)

其中主设备号的范围 - 》0-255 其中次设备号的范围 - 》0-255

-- 实际上系统最多运行挂载:255个255:每一个主设备可以有255个次设备(255*255)

-- 4、字符设备传输按字节来传输的


-- 接下来我们所讲的开发在没有特殊提示下,一般说的都是字符设备文件!

其中字符设备文件的开发又分为三类:

  • 1、杂项开发方法(第一个讲解驱动开发方法,也是最简单的)
  • 2、经典开发方法(删掉,不再考虑了)
  • 3、linux2.6开发方法(第一个讲解驱动开发方法,是最标准的)

杂项的驱动开发

-- !!!杂项是信盈达起的名字,有的人不认,叫做:

  • 字符设备的开发方法(cdev)

-- 1、杂项驱动设备文件的特点

-- 杂项:指的是一类设备,原则上是不想分类的设备!

一类设备独占一个主设备,所有的杂项设备的主设备号都是10

杂项的次设备号自动分配

-- 也就说杂项发开不用考虑设备号的问题

-- 2、杂项驱动开发的接口

-- 有两个

一个是misc_register函数,叫做杂项的注册函数

一个是misc_deregister函数,叫做杂项的取消注册


-- 我们在讲解内核接口的时候,我们在内核开发是属于使用者,而非研究者(不用研究内核源码)

要学会如何传参,如何调用,而不是如何实现这个代码


misc_register函数,叫做杂项的注册函数

-- 函数的功能:杂项设备的注册,注册完毕后会在/dev/生成对应的设备文件,再将设备的信息注册到内核(链表(不需要研究))

-- 函数的头文件:<linux/miscdevice.h>

-- 函数的原型:int misc_register(struct miscdevice *misc)

-- 函数的参数:misc:注册杂项设备的核心结构体,内部有其注册的详细信息。

-- 其中只用对三个变量初始化,其他成员变量大家不用理会,已经被函数内部做过了初始化。

  • minor:次设备号(那么为什么不用主设备号(major),因为主设备号是固定的,就是10)

原则我们不清楚哪个设备号可以被占用,我们一般填255-》让函数自动分配可用的次设备号。

  • name:这个就是你注册设备要生成的设备文件名,比如你填写的是xyd_led,就会生成/dev/xyd_led这个设备文件!

  • fops:这个结构体内部是大量的函数指针,也是你注册这个设备文件对应的内核层的接口。虽说实现的接口对,但不是让你都实现,需要根据自己的实际情况,选择性实现。最起码需要实现的三个成员变量:
    struct module *owner;
    int (*open) (struct inode *inode, struct file *file);
    int (*release) (struct inode *inode, struct file *file);

-- 还有read和write。。。。。。。。。。。。。。。。。。。。。

struct file_operations {
struct module *owner;
ssize_t (*read)(struct file *, char *, size_t, loff_t *);
ssize_t (*write)(struct file *, char *, size_t, loff_t *);
int (*open) (struct inode *, struct file *);
int (*release) (struct inode *, struct file *);
}

这个结构体很大,但是并不是所有的成员变量你都需要实现
根据自己的需求:举个例子:作为一个 LED 灯来说
我只有那些功能:开灯(open)和关灯(close)
作为一个按键来说:只有 按下和松开->需要读取数据
只需要 open close read->读取按键的数据(按下了还是松开)
作为 BH1750 为例子:
只需要 open close read (返回光照传感器数据!)就行
所有的驱动一般来说:至少实现 open close!!!
还需要实现: owner->固定填写 THIS_MODULE

其中owner:固定填写 THIS_MODULE

-- 函数的返回值:注册设备成功返回0,失败返回非0


misc_deregister函数,叫做杂项的取消注册

-- 函数的原型:void misc_deregister(struct miscdevice *misc)

如何注册的杂项,直接把注册的那个结构体填入其中,百分百能取消注册。

-- 3、写一个杂项驱动

  • main.c
#include "stdio.h"
 #include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include "unistd.h"

int main()
{
	
	
	while(1)
	{
		int fd = open("/dev/xyd_led", O_RDWR);
        sleep(1);
        close(fd);
        sleep(1); // 1s 一次
	}
	return 0;
}


  • 1.c
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/miscdevice.h>
#include <linux/fs.h>


struct miscdevice mymisc;
struct file_operations myfops;

int myled_open(struct inode *i, struct file *f)
{
    printk("这个是内核层的open!\r\n");

    //所有的时钟你不用操心
    //申请GPIO
    return 0;

}

int myled_close(struct inode *i, struct file *f)
{
     printk("这个是内核层的close!\r\n");
     return 0;
}

static int __init myled_init(void)
{
    int ret = 0;

    mymisc.minor = 255;
    mymisc.name ="xyd_led";
    mymisc.fops = &myfops;

    myfops.owner = THIS_MODULE;
    myfops.open = &myled_open;
    myfops.release = &myled_close;

   
    ret = misc_register(&mymisc);
    return ret;
}

static void __exit myled_exit(void)
{


}

module_init(myled_init);
module_exit(myled_exit);

MODULE_LICENSE("GPL");

-- 总结上午讲的内容,也就是写了一个最简单的驱动,也就是杂项驱动,目前没有功能,纯打印。生成了一个驱动设备文件-》杂项驱动设备文件-》/dev/xyd_led,主设备号为10(杂项最大特点之一)

alt text

补充:c90错误

-- 如果代码是这么写的,将变量的定义放在后面,就会出现错误。但其实这个错误是c90的,c90是c语言的一个标准,c90要求所有的变量必须先定义,后使用。但是c99就不要求了,所以编译器会报错,但是不影响编译,只是警告而已。只需要将Makefile中的89都改为99即可,但是只改这个还不行,Makefile中还写了一个标准将警告也当作错误了,要把警告也当成错误的语句注释掉。这样就不会再报错了

alt text

alt text

-- 更改Makefile

alt text

alt text

alt text

-- 上午代码的意义:系统层调用了内核层,系统曾和内核层的数据交换

/

-- 下面来看中间层

GPIO子系统


1、什么是GPIO子系统

-- 他就是linux提供的中间层接口,这是一个真通用的接口之一。

-- 这个接口就可以在任意的linux系统下控制GPIO口,但是这个功能有限,只能控制GPIO的两大功能:输入(获取电平的状态),输出(输出0/1)

2、GPIO子系统的接口

-- (1)gpio_request();
  • 获取/申请 一个IO口使用

但是就算是申请失败,你也可以控制

-- 函数的功能:向内核申请使用一个GPIO口

-- 函数的头文件:<linux/gpio.h>

-- 函数的原型:int gpio_request(unsigned gpio, const char *label)

-- 函数的参数:gpio:他就是你要控制的IO口的编号,这个编号是可以算出来的:

linux下GPIO规律:引脚都是从0编号开始的。

以stm32为例:stm32f103zet6
PA0-PA15,PB0-PB15
PA0在Linux下对应的编号为0
PA1在Linux下对应的编号为1
。。。。。
PB0在Linux下对应的编号为16
PA1在Linux下对应的编号为17

  • 几乎所有的厂商的芯片的GPIO口,引脚都是规整的(除了三星的一些芯片),以瑞芯微为例:
    分大组(数字编号 0123。。。) ,每个大组有司机小组ABCD

和小组(字母编号ABCD。。。)。,每个小组有8个引脚

举个例子:GPIO0_A6:就是0组的第0小组的第6个引脚
GPIO4_B6:就是4组的第1小组的第6个引脚

-- 总结:一个大组:32个引脚。一个小组8个引脚

-- 所以GPIO0_A6就是032+08+6=6
-- 所以GPIO4_B6就是432+18+6=142

-- 根据原理图可知:LED的两个引脚是

  • GPIO0_C5(LED1):0+2*8+5 = 21

  • GPIO0_C6(LED2):0+2*8+6 = 22

/写的代码引脚写的是21和22

-- label:标签名字(这个名字无所谓,但是不要填”aaaa“,太不专业)

-- 函数的返回值:如果这个IO口,没有人,没有驱动/没有程序之前申请过,那就给你返回一个正确-》0,如果检测到了,有人之前申请过还没释放,返回非0.


-- (2)gpio_free();//释放不再使用这个IO口

-- 函数的原型:void gpio_free(unsigned gpio)

-- 函数的参数:gpio:你要释放的IO口编号

-- 直接就释放了引脚

-- (3)gpio_direction_input();//设置引脚为输入模式

-- 函数的原型:int gpio_direction_input(unsigned gpio)

-- 函数的参数:gpio:你要设置的引脚编号

-- 函数的返回值:如果设置成功,返回0,失败返回非0

-- (4)gpio_direction_output();//调节引脚为输出模式

-- 函数的原型:int gpio_direction_output(unsigned gpio, int value)

-- 函数的参数:gpio:你要设置的引脚编号,value:你要设置的引脚的输出状态(初值)

-- 函数的返回值:如果设置成功,返回0,失败返回非0

-- (5)gpio_set_value();//设置引脚当前的输出状态

-- 函数的原型:void gpio_set_value(unsigned gpio, int value)

-- 函数的参数:gpio:你要设置的引脚编号,value:你要设置的引脚的输出状态(0/1)

-- (6)gpio_get_value();//获取当前引脚的状态-》不限制是输入还是输出

-- 函数的原型:int gpio_get_value(unsigned gpio)

-- 函数的参数:gpio:你要获取的引脚编号

-- 函数的返回值:返回当前引脚的状态(0/1)


杂项驱动下的beep

-- 先创建两个文件一个写内核层,一个写系统层,然后打开vscode

alt text

-- 然后先将框架写好,之后可以直接复制上节课的头文件路径,也可以再次配置头文件路径。注意要写成绝对路径

alt text

alt text

-- 然后先注册一个设备,先将要写的内核层的open和close函数的名字写好

alt text

-- 然后写内核层的open和close函数,如何找到函数原型

-- 右键open转到声明,之后找到open和close的函数原型

alt text

-- 将搜到的函数原型复制过来,并将参数加上

alt text

-- 我们看原理图可知蜂鸣器的引脚为GPIO1_A4= 132+08+4=36,所以先申请这个引脚

alt text

-- 由原理图可知,高电平为导通,蜂鸣器响,低电平不导通,蜂鸣器不响

-- 申请设备并且赋初值。

alt text

-- 然后写内核层控制硬件的函数

alt text

-- 之后写系统层的代码,打开文件和关闭文件

-- 先用man 2 open找到文件函数头文件

alt text

-- 注意打开的文件名就是/dev下的刚刚设置的名字

alt text

-- 注意要将编译main.c也写到makefile中

alt text

-- 之后make编译

alt text

-- 连接开发板

alt text

alt text

-- 注意!!!:会出现的错误,有关结构体的参数,一定要先赋值

alt text

-- 推送之后,再开一个终端打开adb终端

alt text

adb shell

-- 然后加载驱动,之后执行main函数

alt text

-- 注意:驱动只能加载一次,如果加载过了再次加载会出现错误,如果重新更改过代码,首先应该再次push进adb,之后要重启adb终端,可以输入reboot指令。

-- 再次加载驱动之后执行main函数就可以看到蜂鸣器响起来了

;