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

news2024/9/22 19:25:48

目录

  • 前言
  • 一、Linux2.6字符设备驱动的编写
    • 1.设备号
    • 2.注册设备号
    • 3.释放设备号
    • 4.核心结构体
    • 5.设备相关的 API 函数
    • 6.自动创建设备节点
  • 二、GPIO 子系统
    • 1.申请所需gpio口资源
    • 2.释放gpio口资源
    • 3. 配置 gpio 口的工作模式
    • 4.获取gpio口的电平状态
    • 5.设置 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>

dev_t dev;//设备号
struct cdev mydev;
struct class *myclass = NULL;

int my_open (struct inode *inode, struct file *fp)
{
	printk("Open ok\n");
	return 0;
}

int my_release (struct inode *inode, struct file *fp)
{
	printk("Release 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;
	a = alloc_chrdev_region(&dev,0, 1, "led_test");//注册索取设备号
	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_test");
	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");
}

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

结果:
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2101540.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

路由引入(ospf+rip)

1.搭建拓扑图 2.配置接口ip地址

IO进程(线程篇)

知识点链接 https://www.yuque.com/aihenaobaijin/camuoq/lscmvf6z1arklau4?singleDoc# 《IO进程》 建议先学习知识点&#xff0c;再进行下面的练习 线程 概念 线程是一个轻量级的进程&#xff0c;为了提高系统的性能引入线程 线程和进程是参与统一的调度 在同一个进程中可…

【RSA】简单说说什么是RSA非对称加密

希望文章能给到你启发和灵感&#xff5e; 如果觉得文章对你有帮助的话&#xff0c;点赞 关注 收藏 支持一下博主吧&#xff5e; 阅读指南 开篇说明一、基础环境说明1.1 硬件环境1.2 软件环境 二、什么是非对称加密2.1 常见的非对称加密有哪些&#xff1f;2.2 哪些场景适合使用…

渗透测试靶机---- DC系列 DC-4

渗透测试靶机---- DC系列 DC-4 开启靶机&#xff0c;登录页面&#xff0c;平平无奇 扫描ip 端口&#xff0c;服务等信息 访问80 登录窗&#xff01;&#xff01;&#xff01; 这里说明了admin信息&#xff0c;那么就直接爆破这个admin的密码 密码&#xff1a;happy 登录成功 在…

64位Office API声明语句第001讲

跟我学VBA&#xff0c;我这里专注VBA, 授人以渔。我98年开始&#xff0c;从源码接触VBA已经20余年了&#xff0c;随着年龄的增长&#xff0c;越来越觉得有必要把这项技能传递给需要这项技术的职场人员。希望职场和数据打交道的朋友&#xff0c;都来学习VBA,利用VBA,起码可以提高…

华为云征文|使用sysbench对Mysql应用加速测评

文章目录 ❀前言❀测试环境准备❀测试工具选择❀测试工具安装❀mysql配置❀未开启Mysql加速测试❀开启Mysql加速测试❀总结 ❀前言 大家好&#xff0c;我是早九晚十二。 昨天有梳理一篇关于华为云最新推出的云服务器产品Flexus云服务器X。当时有说过&#xff0c;这次的华为云F…

0.3 学习Stm32经历过的磨难

文章目录 用库函数传参 能否按位或STM32库函数XXX_GetFlagStatus和XXX_GetITStatus的区别 用库函数传参 能否按位或 答案是看清况&#xff0c;而不是一股脑的写&#xff01;&#xff08;血泪的经验啊&#xff09; 可行的情况&#xff1a; //如gpio初始化结构体中的gpiopin参…

坐牢第三十四天(c++)

一.作业 1.栈的手写 #include <iostream> using namespace std; // 封装一个栈 class stcak { private:int *data; //int max_size; // 最大容量int top; // 下标 public:// 无参构造函数stcak();// 有参构造函数stcak(int size);// 拷贝构造函数stcak(const s…

Linux调试器-gdb的使用

&#x1f308;个人主页&#xff1a;Yui_ &#x1f308;Linux专栏&#xff1a;Linux &#x1f308;C语言笔记专栏&#xff1a;C语言笔记 &#x1f308;数据结构专栏&#xff1a;数据结构 &#x1f308;C专栏&#xff1a;C 1.前置知识 程序的发布方式一般有两种&#xff0c;deb…

sql-labs51-55通关攻略

第51关 一.查询数据库 1and updatexml(1,concat(0x7e,(select database()),0x7e),1)-- 二.查表 and updatexml(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schemadatabase() ),0x7e),1) -- 三.查列 and updatexml(1,concat…

都2024年了你还缺客源?十分钟教你如何获取!

你是否还在为如何找到精准的客源而烦恼&#xff1f;别担心&#xff0c;今天我们就来分享一些客源采集方法&#xff0c;让你十分钟内掌握技巧&#xff0c;轻松获取全国各地各行各业的客源。 精准采集客源 1. 拓客工具 专业的拓客工具可以帮助你精准地采集到全国各地的客源信息。…

【SPSS】基于因子分析法对葡萄酒数据进行分析

&#x1f935;‍♂️ 个人主页&#xff1a;艾派森的个人主页 ✍&#x1f3fb;作者简介&#xff1a;Python学习者 &#x1f40b; 希望大家多多支持&#xff0c;我们一起进步&#xff01;&#x1f604; 如果文章对你有帮助的话&#xff0c; 欢迎评论 &#x1f4ac;点赞&#x1f4…

使用合同比对工具时,有哪些常见问题和解决方案?

在使用合同比对工具的过程中&#xff0c;企业可能会面临一系列挑战&#xff0c;这些问题可能会影响工具的效率和效果。以下是一些常见的问题&#xff1a; 1.兼容性问题&#xff1a;在不同的工作环境中&#xff0c;合同文档可能以不同的格式存在&#xff0c;如PDF、Word、Excel…

手撕Python之散列类型

1.字典 思考&#xff1a;如果有多个数据&#xff0c;例如&#xff1a;“凯子”&#xff0c;“男”&#xff0c;19&#xff0c;如何快速存储这些数据 多数我们是通过列表进行存储的 li[凯子,男,19] 在定义完这个列表之后我们如何来找到数据凯子呢&#xff1f; 我们可以通过…

数据结构基本知识

一、什么是数据结构 1.1、组织存储数据 ---------》内存&#xff08;存储&#xff09; 1.2、研究目的 如何存储数据&#xff08;变量&#xff0c;数组....)程序数据结构算法 1.3、常见保存数据的方法 数组&#xff1a;保存自己的数据指针&#xff1a;是间接访问已经存在的…

【笔试练习】深信服校园招聘c/c 软件开发H卷

题目链接 一、填空题 如图所示&#xff0c;平面上有两条平行的线段&#xff0c;上面的线段有A0~A3 4个点&#xff0c;下面的线段有B0到B5 6个点&#xff0c;现在需要把所有的点都连接起来&#xff0c;有如下约束&#xff1a; 每个端点&#xff0c;都至少有一条到另一平行线上端…

仿微信聊天系统开发功能架构分析

仿微信聊天系统是一种旨在模仿微信核心聊天功能的应用或软件&#xff0c;它允许用户通过即时通讯进行交流。该系统通常由客户端、服务器端和数据库组成&#xff0c;以支持用户间的实时消息传送。以下是对仿微信聊天系统的一个概述&#xff1a; 一、系统架构 客户端 用户界面&…

【面试05】PID控制算法

一、 PID算法简介 PID&#xff08;Proportional-Integral-Derivative&#xff09;控制算法是一种经典的反馈控制方法&#xff0c;广泛应用于自动控制系统&#xff0c;例如温度控制、速度控制、位置控制等。 PID控制算法的核心包含三个部分&#xff1a;比例项&#xff08;P&…

一键掌控园区运营,数字化管理平台如何实现?

在当今数字化时代&#xff0c;高效的管理是企业成功的关键。对于各类园区而言&#xff0c;如何实现一键掌控园区运营&#xff0c;成为了提升竞争力的重要课题。幸运的是&#xff0c;数字化管理平台的出现如同一把智慧钥匙&#xff0c;为实现园区运营提供了完美的解决方案。 数字…

<Rust>egui学习之小部件(九):如何在窗口中添加下拉列表combobox部件?

前言 本专栏是关于Rust的GUI库egui的部件讲解及应用实例分析&#xff0c;主要讲解egui的源代码、部件属性、如何应用。 环境配置 系统&#xff1a;windows 平台&#xff1a;visual studio code 语言&#xff1a;rust 库&#xff1a;egui、eframe 概述 本文是本专栏的第九篇博…