目录
前言
主要内容就是搞了个Linux2.6字符设备驱动的编码框架,然后简单使用驱动代码编写了GPIO子系统,配置了一下两个LED灯io口,最后使用应用程编写代码调用底层驱动的API接口,使两个LED灯闪烁。
一、Linux2.6字符设备驱动的编写
Linux2.6字符设备驱动的编写可以理解为杂项设备驱动编写的Promax版。他比杂项驱动设备更加丰富,文件集成度很高,支持的设备号也更多。但同时编写方式也比较麻烦。
1.设备号
杂项设备驱动编写设备号的主设备号固定为10,次设备号为0-255。也就意味着杂项设备最多支持255个外围设备。随着现在技术的不断发展,人们对智能产品的需求也变多了起来,杂项设备驱动编写所支持的255个设备根本不够。为此,Linux2.6字符设备驱动编写的方式也就诞生了。
Linux2.6字符设备驱动的设备号也是包含主设备号和次设备号。不同的是,Linux2.6编写的设备驱动主设备号的范围为2的12次方(4096),次设备号的范围为2的20次方。由此可以看出Linux2.6字符设备驱动的编写方式能够支持大量设备。
完整的设备号 = 主设备号+次设备号
2.注册设备号
和杂项类似,共有两种方法:
1.静态申请
就是需要你自己去搞一个没有使用的完整设备号,之后使用静态申请设备号的函数,去内核里申请。如果该设备号有设备使用,就会失败。
函数原型:
int register_chrdev_region(dev_t from, unsigned int count, const char *name);
函数头文件:#include <linux/fs.h>
函数功能:静态申请设备号。
函数参数:
dev_t from
: 要注册的起始设备号。
unsigned int count
: 要注册的次设备号数量。
const char *name
: 字符设备的名称,用于标识设备的类型。
函数返回值:成功为0,失败返回负数。
2.动态申请
就是直接使用动态申请设备号函数,之后内核会自动给你申请一个没有被的完整设备号。
函数原型:
int alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, const char *name);
函数头文件:#include <linux/fs.h>
函数功能:动态申请设备号。
函数参数:
dev_t *dev
: 指向 dev_t 类型的指针,用于存储分配的主设备号和次设备号。调用函数时,该指针所指向的变量会被更新为分配的设备号。
unsigned int firstminor
: 要分配的首个次设备号。次设备号的范围从 firstminor 到 firstminor + count - 1。
unsigned int count
: 需要的次设备号数量。
const char *name
: 字符设备的名称,用于标识设备的类型,通常用于调试。
函数返回值:成功为0,失败返回负数。
3.释放设备号
只需调用设备号释放函数即可,非常简单。
函数原型:
void unregister_chrdev_region(dev_t from, unsigned int count);
函数头文件:#include <linux/fs.h>
函数功能:释放设备号。
函数参数:
dev_t from
: 要取消注册的起始设备号。
unsigned int count
: 要取消注册的次设备号数量。
函数返回值:无
4.核心结构体
struct cdev {
struct kobject kobj;
struct module *owner;
const struct file_operations *ops;
struct list_head list;
dev_t dev;
unsigned int count;
} __randomize_layout;
该结构体使用时,一般只需要定义一个结构体变量即可。
定义的两种方式
方式 | 效果 |
---|---|
struct cdev dev;(最常用) | 系统会自动的开辟空间,成员变量能直接使用 |
struct cdev *dev; | 只是声明了一个指向 struct cdev 结构体的指针,需要分配内存并初始化它,通常使用 cdev_init() 函数 |
核心结构体空间的申请:
struct cdev *cdev_alloc(void)
— 就是开辟核心结构体的所需空间,类似于malloc。
核心结构体空间的释放:
void kfree(void *p)
5.设备相关的 API 函数
1.初始化核心结构体
函数:
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
{
memset(cdev, 0, sizeof *cdev);
INIT_LIST_HEAD(&cdev->list);
kobject_init(&cdev->kobj, &ktype_cdev_default);
cdev->ops = fops;
}
函数功能: 用于初始化核心结构体。
函数头文件: <linux/cdev.h>
函数参数:
struct cdev *cdev
: 指向待初始化的 cdev 结构体的指针(就是你定义的核心结构体)。
const struct file_operations *fops
: 指向 file_operations 结构体的指针(就是定义操作设备方法集合的结构体变量),用于定义设备的操作方法(如 open, read, write 等)。
函数返回值: 无。
2.向内核申请linux2.6字符设备
函数:
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
{
int error;
p->dev = dev;
p->count = count;
if (WARN_ON(dev == WHITEOUT_DEV))
return -EBUSY;
error = kobj_map(cdev_map, dev, count, NULL,
exact_match, exact_lock, p);
if (error)
return error;
kobject_get(p->kobj.parent);
return 0;
}
函数功能: 向内核中申请linux2.6字符设备
函数头文件: <linux/cdev.h>
函数参数:
struct cdev *p
: 指向待添加的 cdev 结构体的指针。(定义的核心结构体指针类型)
dev_t dev
: 设备号。
unsigned count
: 设备的数量。
函数返回值: 成功返回0,失败返回负数。
3.释放申请的设备
函数:
void cdev_del(struct cdev *p)
{
cdev_unmap(p->dev, p->count);
kobject_put(&p->kobj);
}
函数功能: 用于从系统中删除一个已经注册的 cdev 结构体,并释放相关资源。(释放申请的设备)
函数头文件: <linux/cdev.h>
函数参数:
struct cdev *p
: 指向待添加的 cdev 结构体的指针。(定义的核心结构体指针类型)
函数返回值:无。
注意:卸载函数里的步骤要和加载函数里的相反,类似预栈的先进后出,这里是先注册的后卸载。
Linux2.6 没有自动创建设备节点的功能
这里需要你手动创建
创建指令
mknod /dev/xxx c 主设备号 次设备号
6.自动创建设备节点
由于Linux2.6字符设备没有自动创建设备节点的功能,手动创建又很麻烦,为此,我们可以抄写杂项字符设备的方法自动创建设备节点。
1.创建一个类,方便管理注册的设备
函数原型:
struct class * class_create(struct module *owner,const char *name)
函数头文件:#include<linux/device.h>
函数参数:
owner
:固定的值 THIS_MODULE
name
: 创建类的名字。
函数返回值:成功返回指向 struct class,失败 NULL。
2.自动创建设备节点
函数:
struct device *device_create(
struct class *class,
struct device *parent,
dev_t devt,
void *drvdata,
const char *fmt,.....)
函数头文件:#include<linux/device.h>
函数参数:
class
:创建的类
parent
:父设备 — 写 NULL
devt
:设备号
drvdata
:内核的私有数据 — 写 NULL
fmt
:一般就是你创建的设备节点名字
函数返回值:成功返回一个指向 struct device, 失败 NULL。
3.销毁类
函数:
void class_destroy(struct class *cls)
函数头文件:#include<linux/device.h>
函数参数:
cls
:定义类的变量名
函数返回值:无。
4.销毁设备节点
函数:
void device_destroy(struct class *class,dev_t devt)
函数头文件:#include<linux/device.h>
函数参数:
class
:定义的类名
devt
:设备号
函数返回值:无
二、GPIO 子系统
GPIO(通用输入输出)子系统提供了一个通用的接口(内核封装好的函数),对设备进行操作用于控制和管理各种设备上的 GPIO 引脚。GPIO 引脚通常用于与外部硬件进行数字信号的交互,如开关、LED、按钮等。Linux GPIO 子系统通过提供标准化的接口,使得不同硬件平台上的 GPIO 引脚能够以一致的方式进行操作。
在使用 GPIO 口区操作硬件的时候,你需要先申请注册才能使用当前的 gpio 口的资源。
1.申请所需gpio口资源
函数:
int gpio_request(unsigned gpio, const char *label)
函数头文件:#include <linux/gpio.h>
函数参数:
gpio
:这里就是你要申请注册的 gpio 口的编号(一般内核已经提前写好了固定的宏,直接用宏即可)
label
:标签,一般没有太大作用,就是标识。
函数返回值:成功返回 0 失败负数
gpio 口的编号:使用通用公式可以去计算出来这个gpio 口编号。
2.释放gpio口资源
函数:
void gpio_free(unsigned gpio)
函数头文件: #include <linux/gpio.h>
函数参数:
gpio
:就是你想要释放的 gpio 口对应的编号
函数返回值:无
3. 配置 gpio 口的工作模式
因为目标是改变LED灯的状态,所以这里只说配置模式为输入的情况。这里配置的 gpio 口模式为输入和输出那么这里的输入和输出是针对于 CPU 来说的。
函数:
int gpio_direction_input(unsigned gpio)
函数头文件:#include <linux/gpio.h>
函数参数:
gpio
:就是你想要配置的 gpio 口对应的编号
函数返回值:成功返回 0 ,失败返回负数。
4.获取gpio口的电平状态
函数:
int gpio_get_value(unsigned gpio)
函数头文件:#include <linux/gpio.h>
函数参数:
gpio
:就是你想要获取的 gpio 口对应的编号
函数返回值:返回获取的电平的状态 — 高电平或者是低电平 1/0
5.设置 gpio 的电平状态
函数:
void gpio_set_value(unsigned gpio, int value)
函数头文件:#include <linux/gpio.h>
函数参数:
gpio
:就是你想要设置的 gpio 口对应的编号
value
:你想要设置的电平的状态 — 高电平 1 低电平 0
函数返回值:无
三、目标实现
驱动代码:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/miscdevice.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include<linux/device.h>
#include <linux/gpio.h>
dev_t dev;//设备号
struct cdev mydev;
struct class *myclass = NULL;
int gpio_value = 0;
int my_open (struct inode *inode, struct file *fp)
{
gpio_set_value(21,1);
gpio_set_value(22,1);
printk("led_Open ok\n");
return 0;
}
int my_release (struct inode *inode, struct file *fp)
{
gpio_set_value(21,0);
gpio_set_value(22,0);
printk("led_close ok\n");
return 0;
}
ssize_t my_read (struct file *fp, char __user *buf, size_t size, loff_t *off)
{
printk("Read ok\n");
return 0;
}
ssize_t my_write (struct file *fp, const char __user *buf, size_t size, loff_t *off)
{
printk("Write ok\n");
return 0;
}
struct file_operations my_filop = {
.open = my_open,
.release = my_release,
.read = my_read,
.write = my_write
};
static int __init my_open_init(void)
{
int a=0;
gpio_value=gpio_request(21,"led21");
printk("led21_value:%d\n",gpio_value);
gpio_value=gpio_request(22,"led22");
printk("led22_value:%d\n",gpio_value);
gpio_direction_output(21, 1);
gpio_direction_output(22, 1);
a = alloc_chrdev_region(&dev,0, 1, "led_shine");//注册索取设备号
if(a<0)
{
printk("my_misc_register error!\n");
return -ENODEV;
}
printk("设备号注册成功!\n");
printk("主设备号:%d\n",MAJOR(dev));
printk("次设备号:%d\n",MINOR(dev));
cdev_init(&mydev,&my_filop);
cdev_add(&mydev,dev,1);
myclass = class_create(THIS_MODULE, "class_led");
if(myclass == NULL)
{
printk("class_creat error!\n");
return -1;
}
device_create(myclass,NULL,dev,NULL,"led_shine");
return 0;
}
static void __exit my_open_exit(void)
{
device_destroy(myclass,dev);//销毁设备节点
class_destroy(myclass);//销毁设备类
cdev_del(&mydev);//删除字符设备
unregister_chrdev_region(dev,1);//释放设备号
printk("设备注销成功\n");
gpio_free(21);
gpio_free(22);
}
module_init(my_open_init);
module_exit(my_open_exit);
MODULE_LICENSE("GPL");
应用代码:
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main(int argc,char *argv[])
{
int fd =0;
char buffer[100];
const char *data = "Hello, this is a test write!";
if(argc<2)
{
printf("请输入正确的参数\n");
return -1;
}
fd = open(argv[1],O_RDWR);
if(fd<0)
{
perror("open");
return -1;
}
write(fd, data, strlen(data));
read(fd, buffer, sizeof(buffer) - 1);
while(1)
{
fd = open(argv[1],O_RDWR); // --- 底层的open函数
sleep(1);
close(fd);//底层的close
sleep(1);
}
return 0;
}
Makefile
obj-m += led_shine.o #最终生成模块的名字就是 led.ko
KDIR:=/home/zht/RK3588S/kernel #他就是你现在rk3588s里内核的路径
CROSS_COMPILE_FLAG=/home/zht/RK3588S/prebuilts/gcc/linux-x86/aarch64/gcc-arm-10.3-2021.07-x86_64-aarch64-none-linux-gnu/bin/aarch64-none-linux-gnu-
#这是你的交叉编译器路径 --- 这里你也要替换成你自己的交叉编译工具的路径
all:
make -C $(KDIR) M=$(PWD) modules ARCH=arm64 CROSS_COMPILE=$(CROSS_COMPILE_FLAG)
aarch64-none-linux-gnu-gcc app_ledshine.c -o app_ledshine
#调用内核层 Makefile 编译目标为 modules->模块 文件在当前路径
# 架构 ARCH=arm64
clean:
rm -f *.ko *.o *.mod.o *.mod.c *.symvers *.markers *.order app *mod
结果:
驱动层编写流程:
创建:
1.先使用gpio_request申请GPIO口所需的资源。
2.配置工作模式。由于是控制LED灯,所以这里将模式设置为输出模式gpio_direction_output。
3.使用alloc_chrdev_region注册设备号。
4.使用cdev_init初始化设备。
5.使用cdev_add向内核申请linux2.6字符设备。
下面是自动创建节点的部分:
6.使用class_create创建一个类方便管理注册的设备。
7.使用device_create创建设备节点。
销毁:
1.device_destroy销毁设备节点。
2.class_destroy销毁设备类。
3.cdev_del删除字符设备。
4.unregister_chrdev_region释放设备号。
5.gpio_free释放GPIO口的资源。