一:内核层框架
在介绍linux驱动之前先介绍一下系统。
系统分为两层:
1.系统层
2.内核层
对于内核层就要说一下其中的内核层运行的框架了
代码如下:
//头文件
#include "linux/kernel.h"
#include "linux/module.h"
//入口函数
static int __init myled_init(void)
{
return 0;
}
//出口函数
static void __exit myled_exit(void)
{ }
//函数声明
module_init(myled_init);
module_exit(myled_exit);
//协议
MODULE_LICENSE("GPL");
内核的框架已经写好了,那应该如何编译了
这代码不同于以往的系统代码,如果直接编译会报错,它是运行在内核层,所以要靠内核编译。
所以就要提起一个名为:Makefile的驱动编译了,适用于任何驱动编译,任意的内核版本
通用makefile代码:
obj-m += led.o//如果要用来编译自己的程序代码,修改成自己的
#obj-m:代表模块 module (驱动) 目标-> led.o->led.c
KDIR:=/home/lyx/RK3588S/kernel
#代表你的编译的所用的内核的位置
CROSS_COMPILE_FLAG=/home/lyx/RK3588S/prebuilts/gcc/linuxx86/aarch64/gcc-arm-10.3-2021.07-x86_64-aarch64-none-linuxgnu/bin/aarch64-none-linux-gnu-
#这是你的交叉编译器路径
all:
make -C $(KDIR) M=$(PWD) modules ARCH=arm64
CROSS_COMPILE=$(CROSS_COMPILE_FLAG)
#调用内核层 Makefile 编译目标为 modules->模块 文件在当前路径
# 架构 ARCH=arm64
clean:
rm -f *.ko *.o *.mod.o *.mod.c *mod *.symvers *.markers *.order
二:驱动开发
1:何为驱动
我的理解就是,驱动硬件正常工作的程序代码就是驱动。
在 STM32 里面:
无非就是编写寄存器代码初始化外设->通信传感器
在 Linux 下驱动:
不像 STM32 一样,拿一个 LED 灯为例子
STM32:
初始化 GPIO 寄存器(库函数)
在 main-> 点灯/关灯/闪灯
Linux 下:
也可以像 STM32 一样在内核层直接开灯!
内核层不断的加载卸载实际上很浪费资源。效率很低
Linux 下内核模块代码,是独立运行的单元
很难像 STM32 那样一个代码一个工程代表整个单片机运行程序
就算是我像 STM32 一样写代码把灯的功能写死->运行在内核层!
请问系统开发工程师(不懂驱动) 他怎么操作 LED 灯
我写了一个驱动 你作为智能家居的软件开发工程师 你想在你的应用里面控制我写的驱动底层的 LED 灯! 真正的 Linux 驱动: 不像 STM32 一样把驱动写死了 首先 Linux 下的驱动符合驱动的原则:驱使硬件正常工作代码 把 LED 灯这个设备在 内核层 "抽象" 为一个 文件(设备文件) 我们内部写的驱动就是在完成这样的事->硬件变成文件 系统上层开发者他操作底层设备->操作文件 以 LED 灯为例: LED 灯->我写驱动->led 文件 /dev/myled 上层开发者->打开/dev/led->灯就开了 上层开发者->关闭/dev/led->灯就灭了 |
2:linux分层思想
在是stm32中
是直接操作对应寄存器,这样的特点是高效,简单明了。
缺点为:
难移植,不通用。如果换个芯片就要推到重新再来一遍。
而在linux中提出了分层的思想,如何实现的了,如下图所示:
我认为所谓的分层就是对应的中间接口层,通用的接口函数,能最大的保证代码的通用性,而在Linux系统中,实现的是所有的系统都在做的是软硬件分离。
Linux因为分层软件硬件,我们不管使用什么平台->开发驱动的框架,代码都是一样的。
在 Linux 下分层思想处处可见,不仅仅把驱动做了分层
底层的各种外设也做了大量的分层
SPI 分为: Linux 通用接口层
厂商 BSP 适配的驱动层
甚至最新的 platfrom 也在统一分层思想:
一个简简单单的驱动他也想分为两层:
硬件信息:提供 LED 灯的 引脚和电平状态
通用驱动: LED 灯驱动->没有指定具体 LED 灯引脚、电平状态
总结:
Linux 分层思想: 主要就是告诉大家, Linux 下的所有的函数接口 是通用的,隔离硬件的,软硬件分离 你写的代码原则上适用于任何的平台 实际上在发展中: 把驱动分层: 硬件层 : 硬件信息层 ->渐渐演变成 设备树 软件层 : 软件代码层 ->通用了 所有的平台 把 BSP 厂商驱动:分为两层: 硬件层 软件层 Linux 下的驱动: 我们现在初学驱动的时候暂时写驱动不考虑分层 Linux 下驱动的特点: 不是直接在内核层操作硬件 而是写好 "接口"(文件 打开 关闭 读写) 让上层操作! |
3:驱动开发的框架
1:写内核框架
2:在内核框架->
把设备抽象为文件(内核驱动接口->杂项驱动)Linux中有一句话,叫一切皆文件
设备的设备号->设备ID ->让内核管理
设备内核操作接口->文件操作接口(内核你驱动开发者要单独实现一套)
你写的内核层的 open close read write 跟 上层(系统层一一对应)
这是也是你留给上层的 操作接口!
3:编译成 .ko –> insmod xxx.ko –>生成设备文件(/dev/xxxx)
4:调用 加载/入口函数->内核框架->生成设备文件
5:上层/你自己 调用 文件操作 -> 操作设备
4:驱动文件的特点:
1:系统的特殊文件之一:
管道、套接字、块设备文件、字符设备文件、低级IO/非缓冲区IO来操作文件
一共分为三大类:
字符设备文件:
一般指的是除了存储 网络 设备之外的所有的其他设备
块设备文件:
正常的芯片->厂商都完成了这类芯片初始化
小型存储器->SPI_FLAHS
网络设备文件:
基本上你开发网络设备只有两种:wifi和4G、5G
设备号
原则上是一个 32bit 的无符号数字
理论来说内核最大挂在的设备-> 2^32->40 亿
分为主设备号(占设备号的高 12bit)
次设备号(占设备号的低 20bit)
其中主设备号的范围-> 0-254
其中次设备号的范围-> 0-255
实际上系统最多运行挂载:
255 个 255: 255*256
字符设备传输方式:按字节传输
三:杂项的驱动开发
1:杂项驱动设备文件的特点
杂项:
指一般设备原则上是不分类的设备。
一类设备独占一个设备
所有的杂项设备的主设备号为10
次设备号杂项自动分配->杂项开发不需要考虑设备号的问题
2:杂项驱动开发的接口
主要就是两个函数:
misc_register();//杂项注册函数
misc_deregister();//杂项注销函数
头文件为:
<linux/miscdevice.h>
函数原型:
int misc_register(struct miscdevice *misc)
函数的参数: minor:次设备号 |
然后是注销函数
void misc_deregister(struct miscdevice *misc)
3:举例使用
#include "linux/kernel.h"
#include "linux/module.h"
#include "linux/miscdevice.h"
//1:申请一个杂项核心结构体
struct miscdevice misc;//变量全局的变量,空间为编译器系统开辟的全局区空间
//申请内核层文件操作接口集合结构体
struct file_operations misc_fops;
int xyd_led_open(struct inode * i, struct file * f)
{
printk(KERN_EMERG"这是内核层的 open 被调用了!\r\n");
return 0;
}
int xyd_led_close(struct inode * i, struct file * f)
{
printk(KERN_EMERG"这是内核层的 close 被调用了!\r\n");
return 0;
}
//加载函数
static int __init myled_init(void)
{
//2:填充杂项核心结构体
misc.minor = 255;//让系统自动给我分配一个次设备号
misc.name = "xyd_led";//生成一个设备文件-> /dev/xyd_led
misc.fops = &misc_fops; //指定一个操作函数集
//3:填充 misc_fops 里面的函数接口
misc_fops.owner = THIS_MODULE; //固定写法
misc_fops.open = xyd_led_open;//系统层打开文件调用内核层接口函数
misc_fops.release = xyd_led_close;//系统层关闭文件调用内核层接口函数
return misc_register(&misc);
}
//卸载函数
static void __exit myled_exit(void)
{
//4:卸载的时候取消注册杂项设备
misc_deregister(&misc);
}
module_init(myled_init);
module_exit(myled_exit);
MODULE_LICENSE("GPL");
四:GPIO子系统
1:什么是GPIO子系统
在Linux提供的中间层接口中,这个接口是通用的接口之一,这个接口可以在任意的Linux系统下控制GPIO口,这个功能有限->只能控制GPIO的两大功能:
输入:获取点平状态
输出:输出 0/1
2 :GPIO子系统的接口
gpio_free(unsigned gpio);//释放不再使用这个 IO 口
gpio_request(unsigned gpio,const char *label);//获取/申请 一个 IO 口使用
gpio:
|
gpio_direction_output(gpio_num,value);//引脚调节为 输出模式
gpio_direction_input(gpio_num);//调节引脚为 输入模式
gpio_set_value(gpio_num,value); // 设置引脚当前的输出状态
gpio_get_value(gpio_num); // 获取当前引脚的状态->不限制输入还是输出
头文件为:
#include <linux/gpio.h>
3:举例:点亮LED灯
#include "linux/module.h"
#include "linux/kernel.h"
#include "linux/miscdevice.h"
#include "linux/gpio.h"
#include "linux/fs.h" // struct file_operations 结构体定义处
struct miscdevice xyd_led_device;
struct file_operations misc_device_ops;
//加载函数
int xyd_led_open(struct inode * i , struct file * f)
{
gpio_set_value(21,1);
gpio_set_value(22,1);
return 0;
}
int xyd_led_close(struct inode * i , struct file * f)
{
gpio_set_value(21,0);
gpio_set_value(22,0);
return 0;
}
static int __init xyd_init_led(void)
{
//1:把我的 GPIO 口申请一下
gpio_request(21, "xyd_led1");
gpio_request(22, "xyd_led2");
//2:我把我的引脚输出
gpio_direction_output(21,0);
gpio_direction_output(22,0);
//3: 注册杂项设备
xyd_led_device.minor = 255;
xyd_led_device.name = "xyd_led";
xyd_led_device.fops = &misc_device_ops;
misc_device_ops.owner = THIS_MODULE;
misc_device_ops.open = xyd_led_open;
misc_device_ops.release = xyd_led_close;
misc_register(&xyd_led_device);
return 0;
}
static void __exit xyd_exit_led(void)
{
misc_deregister(&xyd_led_device); //取消设备的注册
gpio_free(21);
gpio_free(22);
}
module_init(xyd_init_led);
module_exit(xyd_exit_led);
MODULE_LICENSE("GPL");