Bootstrap

Linux驱动(四):Linux2.6字符设备驱动及GPIO子系统


前言

  主要内容就是搞了个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口的资源。

;