预告
-- 1、杂项驱动设备开发
-- 2、linux2.6设备驱动开发
-- 3、设备树(驱动的分层思想)
-- 4、平台设备总线(驱动分层)
-- 5、内核接口
目录
-- 2、在内核框架-》把设备抽象为文件(这就需要讲内核驱动的接口)
-- 3、编译成.ko-》用insmod xxx.ko-》生成设备文件(/dev/xxxx)
-- 4、调用加载/入口函数(这里面有你写的内核框架),然后生成设备文件
-- 2、设备驱动文件分为三大类:字符设备文件,块设备文件,网络设备文件。
-- (2)gpio_free();//释放不再使用这个IO口
-- (3)gpio_direction_input();//设置引脚为输入模式
-- (4)gpio_direction_output();//调节引脚为输出模式
-- (5)gpio_set_value();//设置引脚当前的输出状态
-- (6)gpio_get_value();//获取当前引脚的状态-》不限制是输入还是输出
回顾
-- 系统分为两层:一层是系统层,一层是内核层
-- 我们之前所写的所有的程序,都是运行在系统层面的(这个层面的特点:安全,可靠,内存都是虚拟的)
-- 之前我们所学习的系统层面的知识包括接口包括框架,在内核层是无法使用的,所以我们学习了一个内核层运行的框架
-- 之后引入内核层的框架
-- 该驱动代码直接编译百分百报错!他是运行在内核层的,靠内核编译!
-- 所以我们提供了一个通用的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分层
-- linux分层
-- linux因为分层(在软件和硬件之间多了个中间层),不管换什么平台-》开发驱动的框架,代码都是一样的。
中间层:实现软硬件分离
-- 中间层就一个目的:统一开发接口
-- 在linux下分层思想处处可见,不仅仅把驱动做了分层,底层的各种外设也做了大量的分层
-- 例如SPI分为
- linux通用接口层
- 厂商BSP适配的驱动层
-- 甚至最新的platfrom也在统一分层思想:
-- 一个简简单单的驱动他也想分为两层:
-
硬件信息:提供LED风的引脚和电平状态
-
通用驱动:LED灯驱动-》没有指定具体LED灯的引脚,电平状态
-- linux最终发展:把厂商的驱动也做了分层
-- 最终的框架是这样的,不仅把我们写的驱动给分层了,把厂商的驱动也做了分层,分为硬件信息和底层驱动
-- 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(杂项最大特点之一)
补充:c90错误
-- 如果代码是这么写的,将变量的定义放在后面,就会出现错误。但其实这个错误是c90的,c90是c语言的一个标准,c90要求所有的变量必须先定义,后使用。但是c99就不要求了,所以编译器会报错,但是不影响编译,只是警告而已。只需要将Makefile中的89都改为99即可,但是只改这个还不行,Makefile中还写了一个标准将警告也当作错误了,要把警告也当成错误的语句注释掉。这样就不会再报错了
-- 更改Makefile
-- 上午代码的意义:系统层调用了内核层,系统曾和内核层的数据交换
/
-- 下面来看中间层
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
-- 然后先将框架写好,之后可以直接复制上节课的头文件路径,也可以再次配置头文件路径。注意要写成绝对路径
-- 然后先注册一个设备,先将要写的内核层的open和close函数的名字写好
-- 然后写内核层的open和close函数,如何找到函数原型
-- 右键open转到声明,之后找到open和close的函数原型
-- 将搜到的函数原型复制过来,并将参数加上
-- 我们看原理图可知蜂鸣器的引脚为GPIO1_A4= 132+08+4=36,所以先申请这个引脚
-- 由原理图可知,高电平为导通,蜂鸣器响,低电平不导通,蜂鸣器不响
-- 申请设备并且赋初值。
-- 然后写内核层控制硬件的函数
-- 之后写系统层的代码,打开文件和关闭文件
-- 先用man 2 open找到文件函数头文件
-- 注意打开的文件名就是/dev下的刚刚设置的名字
-- 注意要将编译main.c也写到makefile中
-- 之后make编译
-- 连接开发板
-- 注意!!!:会出现的错误,有关结构体的参数,一定要先赋值
-- 推送之后,再开一个终端打开adb终端
adb shell
-- 然后加载驱动,之后执行main函数
-- 注意:驱动只能加载一次,如果加载过了再次加载会出现错误,如果重新更改过代码,首先应该再次push进adb,之后要重启adb终端,可以输入reboot指令。
-- 再次加载驱动之后执行main函数就可以看到蜂鸣器响起来了