RK3568(二)——字符设备驱动开发

server/2024/12/17 6:59:43/

最基础的字符设备驱动开始,重点学习 Linux 下字符设备驱动开发框架。

驱动框架

Linux 应用程序对驱动程序的调用:

在 Linux 中一切皆为文件,驱动加载成功以后会在“/dev”目录下生成一个相应的文件,应用程序通过对这个名为“/dev/xxx”(xxx 是具体的驱动文件名字)的文件进行相应的操作即可实现对硬件的操作。

  • open和 close 就是打开和关闭 led 驱动的函数
  • write 函数来操作,向驱动写入数据,read 函数从驱动中读取相应的状态

因为驱动是运行在内核空间中,用户空间想要对驱动进行操作,必须要通过系统调用进行。

应用程序使用到的函数在具体驱动程序中都有与之对应的函数,比如应用程序中调用了 open 这个函数,那么在驱动程序中也得有一个名为 open 的函数。

字符驱动开发步骤

学 Linux 驱动开发重点是学习其驱动框架。

驱动加载和卸载

Linux驱动有两种运行方式:

  1. 将驱动编译到内核中,当Linux内核启动自动运行驱动程序
  2. 将驱动编译成模块(Linux下模块扩展名为.ko),在内核启动之后通过**modprobe****insmod**命令加载驱动模块。

在调试驱动的时候一般都选择将其编译为模块,将驱动编译为模块最大的好处就是方便开发,这里我们统一用**<font style="color:#DF2A3F;">modprobe</font>**进行模块加载。

模块有加载和卸载两种操作,我们在编写驱动的时候需要注册这两种操作函数,模块的加载和卸载注册函数如下:

module_init(xxx_init);

module_exit(xxx_exit);

/**************************************************************** chrdevbase_init - 函数名* description  : 驱动入口函数* @param       : 无* 返回值        : 0 成功
***************************************************************/
static int __init chrdevbase_init(void)
{int retvalue = 0;/* 注册字符设备驱动 */retvalue = register_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME, &chrdevbase_fops);if (retvalue < 0)       {printk("chrdevbase driver register failed\r\n");}printk("chrdevbase_init() \r\n");return 0;
}/**************************************************************** chrdevbase_exit - 函数名* description  : 驱动出口函数* @param       : 无* 返回值        : 0 成功
***************************************************************/
static void __exit chrdevbase_exit(void)
{/* 注销字符设备驱动 */unregister_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME);printk("chrdevbase_exit() \r\n");
}
  • 当使用“modprobe”命令加载驱动的时候,xxx_init 这个函数就会被调用
  • 当使用“rmmod”命令卸载具体驱动的时候 xxx_exit 函数就会被调用

注:加载模块命令insmod 和modprobe的区别:

  • insmod 是最简单的模块加载命令,此命令用于加载指定的.ko 模块,insmod 命令不能解决模块的依赖关系。
  • modprobe 命令主要智能在提供了模块的依赖性分析、错误检查、错误报告等功能,推荐使用 modprobe 命令来加载驱动

字符设备注册和注销

对于字符设备驱动而言,当驱动模块加载成功以后需要注册字符设备,同样,卸载驱动模块的时候也需要注销掉字符设备

/* 注册字符设备 */
static inline int register_chrdev(unsigned int major, const char *name,const struct file_operations *fops)
{return __register_chrdev(major, 0, 256, name, fops);
}
/* 注销字符设备 */
static inline void unregister_chrdev(unsigned int major, const char *name)
{__unregister_chrdev(major, 0, 256, name);
}

一般字符设备的注册在驱动模块的入口函数 xxx_init 中进行,字符设备的注销在驱动模块的出口函数 xxx_exit 中进行。****如上节所示

  • test_fops 就是设备的操作函数集合
  • 要注意的一点就是,选择没有被使用的主设备号,输入命令“cat /proc/devices”可以查看当前已经被使用掉的设备号
  • 查看已使用设备号cat /proc/devices

实现设备具体操作

file_operations 结构体就是设备的具体操作函数。完成变量 test_fops 的初始化,设置好针对 chrtest 设备的操作函数。

/*
* 设备操作函数结构体
* 将驱动函数映射为系统调用
*/
static struct file_operations chrdevbase_fops = 
{.owner = THIS_MODULE,.open = chrdevbase_open,.read = chrdevbase_read,.write = chrdevbase_write,.release = chrdevbase_release,
};
#define CHRDEVBASE_MAJOR 200            /* 主设备号 */
#define CHRDEVBASE_NAME "chrdevbase"    /* 设备名 */static char readbuf[100];               /* 读缓冲区 */
static char writebuf[100];           /* 写缓冲区 */
static char kerneldata[] = {"kernel data!"};/**************************************************************** chrdevbase_open - 函数名* description  : 打开设备* @param-inode : 传递给驱动的 inode* @param-filp  : 设备文件,file 结构体有个叫做 private_data 的成员变量* 一般在 open 的时候将 private_data 指向设备结构体。** 返回值       : 0 成功
***************************************************************/
static int chrdevbase_open(struct inode *inode, struct file *filp)
{printk("chrdevbase open! \r\n");return 0;
}/**************************************************************** chrdevbase_read - 函数名* @description   : 从设备读取数据* @param-filp   : 要打开的文件设备* @param-buf    : 返回给用户空间的数据缓冲区* @param-cnt    : 读取的数据长度* @param-off_t  : 相对于文件首地址的偏移* @return       :读取的字节数
***************************************************************/
static ssize_t chrdevbase_read(struct file *filp, char __user *buf, size_t cnt, loff_t *off_t)
{int retvalue = 0;/* 完成内核空间的数据到用户空间的复制 */memcpy(readbuf, kerneldata, sizeof(kerneldata));retvalue = copy_to_user(buf, readbuf, cnt);if (retvalue==0){printk("kernel senddata success! \r\n");}else{printk("kernel senddata failed! \r\n ");}return 0;
}/**************************************************************** chrdevbase_write - 函数名* @description   : 向设备写入数据* @param-filp   : 设备文件,要打开的文件设备* @param-buf    : 要写入设备的数据* @param-cnt    : 写入的数据长度* @param-off_t  : 相对于文件首地址的偏移* @return       :写入的文件数 
***************************************************************/
static ssize_t chrdevbase_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *off_t)
{int retvalue = 0;/* 接收用户空间的数据并打印 */retvalue = copy_from_user(writebuf, buf, cnt);if (retvalue==0){printk("kernel recevdata:%s \r\n", writebuf);}else{printk("kernel recevdata failed! \r\n ");}return 0;
}/**************************************************************** chrdevbase_release - 函数名* description  : 关闭释放设备* @param-inode : 传递给驱动的 inode* @param-filp  : 设备文件,要关闭的文件设备描述符** 返回值       : 0 成功
***************************************************************/
static int chrdevbase_release(struct inode *inode, struct file *filp)
{printk("chrdevbase release! \r\n");return 0;
}
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>/**************************************************************** 文件名   : chrdevbase.c* 功能     : 这是一个简单的虚拟设备驱动程序,用于演示内核模块的初始化和清理* 作者     : zxk* 创建日期: 2024年11月18日
***************************************************************/#define CHRDEVBASE_MAJOR 200            /* 主设备号 */
#define CHRDEVBASE_NAME "chrdevbase"    /* 设备名 */static char readbuf[100];               /* 读缓冲区 */
static char writebuf[100];           /* 写缓冲区 */
static char kerneldata[] = {"kernel data!"};/**************************************************************** chrdevbase_open - 函数名* description  : 打开设备* @param-inode : 传递给驱动的 inode* @param-filp  : 设备文件,file 结构体有个叫做 private_data 的成员变量* 一般在 open 的时候将 private_data 指向设备结构体。** 返回值       : 0 成功
***************************************************************/
static int chrdevbase_open(struct inode *inode, struct file *filp)
{printk("chrdevbase open! \r\n");return 0;
}/**************************************************************** chrdevbase_read - 函数名* @description   : 从设备读取数据* @param-filp   : 要打开的文件设备* @param-buf    : 返回给用户空间的数据缓冲区* @param-cnt    : 读取的数据长度* @param-off_t  : 相对于文件首地址的偏移* @return       :读取的字节数
***************************************************************/
static ssize_t chrdevbase_read(struct file *filp, char __user *buf, size_t cnt, loff_t *off_t)
{int retvalue = 0;/* 完成内核空间的数据到用户空间的复制 */memcpy(readbuf, kerneldata, sizeof(kerneldata));retvalue = copy_to_user(buf, readbuf, cnt);if (retvalue==0){printk("kernel senddata success! \r\n");}else{printk("kernel senddata failed! \r\n ");}return 0;
}/**************************************************************** chrdevbase_write - 函数名* @description   : 向设备写入数据* @param-filp   : 设备文件,要打开的文件设备* @param-buf    : 要写入设备的数据* @param-cnt    : 写入的数据长度* @param-off_t  : 相对于文件首地址的偏移* @return       :写入的文件数 
***************************************************************/
static ssize_t chrdevbase_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *off_t)
{int retvalue = 0;/* 接收用户空间的数据并打印 */retvalue = copy_from_user(writebuf, buf, cnt);if (retvalue==0){printk("kernel recevdata:%s \r\n", writebuf);}else{printk("kernel recevdata failed! \r\n ");}return 0;
}/**************************************************************** chrdevbase_release - 函数名* description  : 关闭释放设备* @param-inode : 传递给驱动的 inode* @param-filp  : 设备文件,要关闭的文件设备描述符** 返回值       : 0 成功
***************************************************************/
static int chrdevbase_release(struct inode *inode, struct file *filp)
{printk("chrdevbase release! \r\n");return 0;
}/*
* 设备操作函数结构体
* 将驱动函数映射为系统调用
*/
static struct file_operations chrdevbase_fops = 
{.owner = THIS_MODULE,.open = chrdevbase_open,.read = chrdevbase_read,.write = chrdevbase_write,.release = chrdevbase_release,
};/**************************************************************** chrdevbase_init - 函数名* description  : 驱动入口函数* @param       : 无* 返回值        : 0 成功
***************************************************************/
static int __init chrdevbase_init(void)
{int retvalue = 0;/* 注册字符设备驱动 */retvalue = register_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME, &chrdevbase_fops);if (retvalue < 0)       {printk("chrdevbase driver register failed\r\n");}printk("chrdevbase_init() \r\n");return 0;
}/**************************************************************** chrdevbase_exit - 函数名* description  : 驱动出口函数* @param       : 无* 返回值        : 0 成功
***************************************************************/
static void __exit chrdevbase_exit(void)
{/* 注销字符设备驱动 */unregister_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME);printk("chrdevbase_exit() \r\n");
}/* 将上面两个函数指定为驱动的入口和出口函数 */
module_init(chrdevbase_init);
module_exit(chrdevbase_exit);MODULE_LICENSE("GPL");
MODULE_AUTHOR("ALIENTEK");
MODULE_INFO(intree, "Y");

Linux设备号机制

设备号由主设备号和次设备号两部分组成,主设备号表示某一个具体的驱动,次设备号表示使用这个驱动的各个设备

设备号组成:

  • 主设备号表示某一个具体的驱动
  • 次设备号表示使用这个驱动的各个设备

设备号分配

  • 静态分配:使用“cat /proc/devices”命令即可查看当前系统中所有已经使用了的设备号,分配未使用的。
  • 动态分配

Linux 社区推荐使用动态分配设备号,在注册字符设备之前先申请一个设备号,系统会自动给你一个没有被使用的设备号,这样就避免了冲突。卸载驱动的时候释放掉这个设备号即可

设备号的申请函数:

int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
void unregister_chrdev_region(dev_t from, unsigned count)

驱动实验流程

应用程序调用 open 函数打开 chrdevbase 这个设备,打开以后可以使用 write 函数向chrdevbase 的写缓冲区 writebuf 中写入数据(不超过 100 个字节),也可以使用 read 函数读取读缓冲区 readbuf 中的数据操作,操作完成以后应用程序使用 close 函数关闭 chrdevbase 设备。

添加头文件路径

{"configurations": [{"name": "Linux","includePath": ["${workspaceFolder}/**","/home/zxk/work/rk3568_linux_sdk/kernel/arch/arm64/include","/home/zxk/work/rk3568_linux_sdk/kernel/include","/home/zxk/work/rk3568_linux_sdk/kernel/arch/arm64/include/generated"],"defines": [],"compilerPath": "/usr/bin/gcc","cStandard": "c17","cppStandard": "gnu++14","intelliSenseMode": "linux-gcc-x64"}],"version": 4
}

实现过程

  • chrdevbase_open 函数,当应用程序调用 open 函数的时候此函数就会调用。
  • chrdevbase_read 函数,应用程序调用 read 函数从设备中读取数据的时候此函数会执行,因为内核空间不能直接操作用户空间的内存,因此需要借助 copy_to_user 函数来完成内核空间的数据到用户空间的复制。
  • chrdevbase_write 函数,应用程序调用 write 函数向设备写数据的时候此函数就会执行。
  • chrdevbase_release 函数,应用程序调用 close 关闭设备文件的时候此函数会执行
  • module_init 和 module_exit 这两个函数来指定驱动的入口和出口函数。
  • 为了欺骗内核,给本驱动添加 intree 标记,如果不加就会有“loading out-of-treemodule taints kernel.”这个警告

驱动测试APP-应用层

#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"/**************************************************************** 文件名   : chrdevbaseAPP.c* 功能     : 这是一个chrdevbase 驱测试 APP* 作者     : zxk* 创建日期: 2024年11月18日* 其他     :使用方法 ./chrdevbaseAPP /dev/chrdevbase <1>|<2>*              argv[2] 1:读文件*              argv[2] 2:写文件
***************************************************************/static char usrdata[] = {"usr data!"};/**************************************************************** description  :  main主函数* @param-argc  :  argv 数组元素个数* @param-argv  :  具体参数* * @return        : 0 成功
***************************************************************/
int main(int argc, char *argv[])
{int fd, retvalue;char *filename;char readbuf[100], writebuf[100];if (argc != 3){printf(" Error Usage!\r\n");return -1;}filename = argv[1];fd = open(filename, O_RDWR);if (fd < 0){printf(" Can't open the file %s \r\n", argv[1]);return -1;}   if (atoi(argv[2]) == 1)     /* 从驱动读取数据 */{retvalue = read(fd, readbuf, 50);if (retvalue < 0){printf(" read the file %s failed!\r\n", filename);}else{printf("read data is %s \r\n", readbuf);}}if (atoi(argv[2]) == 2)     /* 写数据到驱动文件 */{memcpy(usrdata, writebuf, sizeof(usrdata));retvalue = write(fd, writebuf, 50);if (retvalue < 0){printf("write file %s failed!\r\n", filename);}}/* 关闭设备 */retvalue = close(fd);if (retvalue < 0){printf("Can't close the file %s!\r\n", filename);return -1;}return 0;
}

驱动和测试函数编译

驱动编译成模块

编写Makefile文件

# ARCH := arm64
KERNELDIR := /home/zxk/work/rk3568_linux_sdk/kernel
CURRENT_PATH := /home/zxk/linux_driver/01_chrdevbaseobj-m := chrdevbase.obulid: kernel_moduleskernel_modules:$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean

编译模块

make ARCH=arm64 //<font style="color:#DF2A3F;">ARCH=arm64 必须指定,否则编译会失败</font>

编译成功以后就会生成一个叫做 chrdevbaes.ko 的文件,此文件就是 chrdevbase 设备的驱动模块。

编译APP测试文件-交叉编译

/opt/atk-dlrk356x-toolchain/bin/aarch64-buildroot-linux-gnu-gcc chrdevbaseApp.c -o chrdevbaseApp

开发板测试驱动

上传编译后的驱动和测试文件

scp ./01_chrdevbase/chrdevbase.ko root@192.168.137.65:/lib/modules/4.19.232/
scp ./01_chrdevbase/chrdevbaseAPP root@192.168.137.65:/lib/modules/4.19.232/

加载驱动模块

  1. 输入“depmod”命令以后会自动生成 modules.alias、modules.symbols 和 modules.dep 等等一些 modprobe 所需的文件
depmod
  1. modprobe命令加载 chrdevbase.ko 驱动文件
modprobe chrdevbase.ko
  1. 查看是否加载成功
1.看到“chrdevbase init!”这一行
2.cat /proc/devices		# 查看当前系统中有没有 chrdevbase 这个设备
  1. 创建设备节点
mknod /dev/chrdevbase c 200 0
# 创建完成以后就会存在/dev/chrdevbase 这个文件,可以使用“ls /dev/chrdevbase -l”命令查看
  1. chrdevbase 设备操作测试
./chrdevbaseApp /dev/chrdevbase 1
./chrdevbaseApp /dev/chrdevbase 2
  1. 卸载驱动模块
rmmod chrdevbase

http://www.ppmy.cn/server/150831.html

相关文章

10个富士胶片模拟的设置

二色彩 1、色彩的加减控制全局的饱和度增减&#xff1b; 2、色彩效果只提升暖色系饱和度&#xff1b; 3、FX蓝色大幅度提升蓝色系饱和度&#xff1b; 4、三个参数都不改变颜色的色相。 2.1 色彩 色彩调整的是拍摄画面整体的色彩饱和程度 2.2色彩效果 调整的是画面中暖色…

Go 语言新手入门:快速掌握 Go 基础

Go 语言&#xff08;又叫 Golang&#xff09;是一种由 Google 开发的开源编程语言&#xff0c;设计初衷是简洁、高效&#xff0c;并且能够处理大规模并发程序。Go 语言自发布以来&#xff0c;逐渐在 Web 开发、云计算、微服务和系统编程等领域获得了广泛应用。它以简单的语法、…

TikTok为何选择矩阵起号?——深入解析短视频运营策略

TikTok为何选择矩阵起号&#xff1f;——深入解析短视频运营策略 随着短视频平台的迅速崛起&#xff0c;TikTok成为全球用户分享和消费内容的重要阵地。对于企业、品牌和内容创作者而言&#xff0c;如何高效地获取流量、扩大影响力并实现变现成为关键问题。在这种背景下&#…

在Ubuntu 2404上使用最新的PicGo

在转向Ubuntu之后&#xff0c;果断下载了今年最新的Ubuntu2404,但是随之而来的是底层组件的更新&#xff0c;很多以前可以畅快使用的软件&#xff0c;因为需要老版本的组件而不能正确运行&#xff0c;PicGo就是如此 我们从这里打开Release列表 其中Ubuntu可用的只有这个AppIma…

UEFI 中的 GUI

字符串 UEFI 中字符串有两种&#xff0c;一种是 Unicode16 编码 的字符串&#xff0c;另一种是 ASCII 字符串。 Unicode16 字符串是 UEFI 默认使用的字符串&#xff1b; ASCII 字符串以 \0 结尾&#xff0c;每个字符占用 1 字节。 L"Hello World" 是 Unicode16 字符…

游戏引擎学习第49天

仓库: https://gitee.com/mrxiao_com/2d_game 回顾 我们当时在讨论我们必须要进行一些改进&#xff0c;以便在游戏中实现更好的碰撞检测。当时展示了一种非常基本的形式&#xff0c;以十字路口为例来实现碰撞交叉工作。然后我们意识到需要升级到更复杂的水平&#xff0c;以便…

35.2 thanos-sidecar源码阅读

本节重点介绍 : sidercar 都干了什么 执行prometheus的探活继承所有prometheus v1的查询方法&#xff0c;封装成http-client用上面的http-client 注册grpc-server&#xff0c;外部可以调grpc方法通过sidecar查询prometheus数据初始化对象存储的bkt用bkt创建shipper对象&#x…

CDMP、CDGA和CDGP的区别

CDMP&#xff08;Certified Data Management Professional&#xff09;、CDGA&#xff08;Certified Data Governance Associate&#xff09;和 CDGP&#xff08;Certified Data Governance Professional&#xff09;是数据管理和数据治理领域的三种认证&#xff0c;它们的定位…