当前位置: 首页 > news >正文

imx6ull-驱动开发篇3——字符设备驱动开发实验

目录

前言

实验程序编写

创建 VSCode 工程

添加头文件路径

编写实验程序

printk函数

设备操作函数​

chrdevbase_open​​

​​chrdevbase_read​​

​​chrdevbase_write​​

​​chrdevbase_release​​

设备注册与注销​

​​chrdevbase_init​​

chrdevbase_exit​​

关键数据结构​

编写测试 APP

C 库文件操作基本函数

open函数

read函数

write函数

close函数

编写测试 APP 程序

编译驱动程序和测试 APP

编译驱动程序

编译测试 APP

运行测试

加载驱动模块

创建设备节点文件

chrdevbase 设备操作测试

卸载驱动模块


前言

在上一讲内容里,字符设备驱动开发步骤,我们详细说明了:模块加载/卸载机制、设备号管理、操作函数实现等。

本讲实验里,以 chrdevbase 这个虚拟设备为例,完整地编写一个字符设备驱动模块。

实验程序编写

chrdevbase 这个虚拟设备,假设有两个缓冲区,一个为读缓冲区,一个为写缓冲区,这两个缓冲区的大小都为 100 字节。在应用程序中可以向 chrdevbase 设备的写缓冲区中写入数据,从读缓冲区中读取数据。

通过实现chrdevbase虚拟设备的功能,我们就能学会字符设备驱动开发的最基本功能。

创建 VSCode 工程

在 Ubuntu 中创建一个目录用来存放 Linux 驱动程序,

在drivers 目录下新建一个名为 1_chrdevbase 的子目录来存放本实验所有文件,

在 1_chrdevbase 目录中新建 VSCode 工程,并且新建 chrdevbase.c 文件。

添加头文件路径

因为是编写 Linux 驱动,因此会用到 Linux 源码中的函数。我们需要在 VSCode 中添加 Linux源码中的头文件路径。

打开 VSCode,按下“Crtl+Shift+P”打开 VSCode 的控制台,然后输入“C/C++: Edit configurations(JSON) ”,打开 C/C++编辑配置文件,如图:

打开以后会自动在.vscode 目录下生成一个名为 c_cpp_properties.json 的文件。

此文件默认内容如下所示:

其中,includePath 表示头文件路径,需要将 Linux 源码里面的头文件路径添加进来:

添加头文件路径以后的 c_cpp_properties.json的文件内容如下所示:

{"configurations": [{"name": "Linux","includePath": ["${workspaceFolder}/**","/home/huax/linux/linux_test/linux-imx-rel_imx_4.1.15_2.1.0_ga/include","/home/huax/linux/linux_test/linux-imx-rel_imx_4.1.15_2.1.0_ga/arch/arm/include","/home/huax/linux/linux_test/linux-imx-rel_imx_4.1.15_2.1.0_ga/arch/arm/include/generated/"],"defines": [],"compilerPath": "/usr/bin/gcc","cStandard": "c11","cppStandard": "c++17","intelliSenseMode": "clang-x64"}],"version": 4
}

分别添加了开发板所使用的 Linux 源码下的:

  • include、
  • arch/arm/include
  • arch/arm/include/generated

这三个目录的路径,注意,这里使用了绝对路径。

编写实验程序

我们之前新建了文件 chrdevbase.c,打开,输入如下内容:

#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>#define CHRDEVBASE_MAJOR 200     /* 主设备号 */
#define CHRDEVBASE_NAME  "chrdevbase" /* 设备名 *//* 缓冲区定义 */
static char readbuf[100];        /* 读缓冲区 */
static char writebuf[100];       /* 写缓冲区 */
static char kerneldata[] = {"kernel data!"};/*** @brief 打开设备* @param inode 传递给驱动的inode* @param filp 设备文件指针(可通过private_data传递设备结构体)* @return 0 成功,其他失败*/
static int chrdevbase_open(struct inode *inode, struct file *filp)
{return 0;
}/*** @brief 从设备读取数据* @param filp 设备文件指针* @param buf 用户空间缓冲区* @param cnt 请求读取的字节数* @param offt 文件偏移指针* @return 实际读取的字节数(负值表示错误)*/
static ssize_t chrdevbase_read(struct file *filp, char __user *buf,size_t cnt, loff_t *offt)
{int retvalue = 0;/* 内核数据准备 */memcpy(readbuf, kerneldata, sizeof(kerneldata));/* 拷贝数据到用户空间 */retvalue = copy_to_user(buf, readbuf, cnt);if (retvalue == 0) {printk("kernel senddata ok!\n");} else {printk("kernel senddata failed!\n");}return 0;
}/*** @brief 向设备写入数据* @param filp 设备文件指针* @param buf 用户空间数据缓冲区* @param cnt 请求写入的字节数* @param offt 文件偏移指针* @return 实际写入的字节数(负值表示错误)*/
static ssize_t chrdevbase_write(struct file *filp, const char __user *buf,size_t cnt, loff_t *offt)
{int retvalue = 0;/* 从用户空间拷贝数据 */retvalue = copy_from_user(writebuf, buf, cnt);if (retvalue == 0) {printk("kernel recevdata:%s\n", writebuf);} else {printk("kernel recevdata failed!\n");}return 0;
}/*** @brief 关闭设备* @param inode inode结构体指针* @param filp 设备文件指针* @return 0 成功,其他失败*/
static int chrdevbase_release(struct inode *inode, struct file *filp)
{return 0;
}/* 设备操作函数结构体 */
static struct file_operations chrdevbase_fops = {.owner = THIS_MODULE,.open = chrdevbase_open,.read = chrdevbase_read,.write = chrdevbase_write,.release = chrdevbase_release,
};/*** @brief 驱动初始化入口* @return 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\n");}printk("chrdevbase init\n");return 0;
}/*** @brief 驱动退出函数*/
static void __exit chrdevbase_exit(void)
{/* 注销字符设备驱动 */unregister_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME);printk("chrdevbase exit\n");
}/* 指定驱动入口/出口函数 */
module_init(chrdevbase_init);
module_exit(chrdevbase_exit);/* 模块信息 */
MODULE_LICENSE("GPL");
MODULE_AUTHOR("huax");

printk函数

在这段代码里,用printk 来输出信息,而不是 printf。

因为在 Linux 内核中没有 printf 这个函数。printf运行在用户态, printk 运行在内核态。

printk 可以根据日志级别对消息进行分类,一共有 8 个消息级别,这 8 个消息级别定义在文件 include/linux/kern_levels.h 里面,定义如下:

#define KERN_SOH "\001"
#define KERN_EMERG KERN_SOH "0" /* 紧急事件,一般是内核崩溃 */
#define KERN_ALERT KERN_SOH "1" /* 必须立即采取行动 */
#define KERN_CRIT KERN_SOH "2" /* 临界条件,比如严重的软件或硬件错误*/
#define KERN_ERR KERN_SOH "3" /* 错误状态,一般设备驱动程序中使用
KERN_ERR 报告硬件错误 */
#define KERN_WARNING KERN_SOH "4" /* 警告信息,不会对系统造成严重影响 */
#define KERN_NOTICE KERN_SOH "5" /* 有必要进行提示的一些信息 */
#define KERN_INFO KERN_SOH "6" /* 提示性的信息 */
#define KERN_DEBUG KERN_SOH "7" /* 调试信息 */

一共定义了 8 个级别,其中 0 的优先级最高, 7 的优先级最低。

举例:

printk(KERN_EMERG "gsmi: Log Shutdown Reason\n");

printk还可以通过消息级别来决定哪些消息可以显示在控制台上。在 include/linux/printk.h 中有个宏 CONSOLE_LOGLEVEL_DEFAULT,定义如下:

#define CONSOLE_LOGLEVEL_DEFAULT 7

CONSOLE_LOGLEVEL_DEFAULT 控制着哪些级别的消息可以显示在控制台上,此宏默认为 7,意味着只有优先级高于 7 的消息才能显示在控制台上。

设备操作函数​

chrdevbase_open​​
  • ​​作用​​:设备打开时调用
  • ​​关键参数​​:filp->private_data(可指向设备结构体,存储设备属性)
  • ​​示例用途​​:初始化硬件或分配资源
​​chrdevbase_read​​
  • ​​作用​​:从设备读取数据到用户空间
  • ​​关键操作​​:内核数据准备(memcpy),通过 copy_to_user安全拷贝到用户空间
  • ​​返回值​​:成功返回0,失败返回负数
​​chrdevbase_write​​
  • ​​作用​​:将用户空间数据写入设备
  • ​​关键操作​​:通过 copy_from_user安全拷贝到内核缓冲区
  • ​​调试输出​​:打印接收到的数据内容
​​chrdevbase_release​​
  • ​​作用​​:关闭设备时释放资源
  • ​​典型操作​​:若 private_data指向动态内存,需在此释放

设备注册与注销​

​​chrdevbase_init​​
  • 功能​​:驱动入口,注册字符设备
  • ​​关键调用​​:register_chrdev(CHRDEVBASE_MAJOR, ...)
  • ​​错误处理​​:检查返回值并打印日志
chrdevbase_exit​​
  • ​​功能​​:驱动出口,注销字符设备

  • ​​关键调用​​:unregister_chrdev(CHRDEVBASE_MAJOR, ...)

关键数据结构​

file_operations​​结构体

  • ​​成员​​:.open、.read、.write、.release
  • ​​作用​​:绑定用户操作与驱动函数

编写测试 APP

C 库文件操作基本函数

编写测试 APP 就是编写 Linux 应用,需要用到 C 库里面和文件操作有关的一些函数,比如open、 read、 write 和 close 这四个函数。

open函数

函数原型如下:

int open(const char *pathname, int flags);

open函数有两个参数:

参数​

​作用​

​典型值​

pathname

设备/文件路径(如 /dev/chrdevbase

字符串路径

flags

打开模式(必选+可选组合)

见下方模式说明

flags: 文件打开模式,下表中的值必须三选一

​模式​

​说明​

​示例场景​

O_RDONLY

只读

读取传感器数据

O_WRONLY

只写

控制LED灯

O_RDWR

读写(最常用)

双向通信设备

flags:常用可选模式(按位或 |组合)​

​模式​

​作用​

​示例​

O_APPEND

每次写入追加到文件末尾

日志文件

O_CREAT

文件不存在时创建(需指定权限)

`O_RDWR

O_TRUNC

打开时清空文件内容(慎用)

临时配置文件

O_NONBLOCK

非阻塞模式(立即返回,不等待设备就绪)

串口设备

O_SYNC

写入后等待物理I/O完成(数据安全性高)

关键数据存储

open函数返回值​

  • ​成功​​:返回 ​​文件描述符​​(正整数,后续操作凭据)

  • ​失败​​:返回 -1,并通过 errno标识错误原因(如 ENOENT文件不存在)

举例:

int fd = open("/dev/chrdevbase", O_RDWR | O_NONBLOCK);
if (fd < 0) {perror("Open failed");exit(1);
}

read函数

函数原型如下:

ssize_t read(int fd, void *buf, size_t count)

read函数有三个参数:

​参数​

​作用​

​注意事项​

fd

文件描述符(由 open返回)

必须有效且已打开

buf

数据存储缓冲区(用户空间内存)

需确保内存足够且可写

count

请求读取的最大字节数

实际读取可能小于此值

read函数的返回值有下面三种情况:

​返回值​

​含义​

​典型场景​

​正整数​

实际读取的字节数

成功读取部分或全部数据

0

文件末尾(EOF)

无更多数据可读(如管道关闭)

​负值​

错误(通过 errno获取具体原因)

设备故障/信号中断/权限不足

举例:

char buffer[100];
int ret = read(fd, buffer, sizeof(buffer));
if (ret < 0) {perror("Read failed");
} else {printf("Read %d bytes: %.*s\n", ret, ret, buffer);
}

write函数

函数原型如下:

ssize_t write(int fd, const void *buf, size_t count);

write函数的参数如下:

参数​

​作用​

​注意事项​

fd

文件描述符(由 open返回)

必须有效且已打开(有写权限)

buf

待写入的数据缓冲区(用户空间)

数据需合法且缓冲区可读

count

请求写入的字节数

实际写入可能小于此值

返回值和read一样,也有3种情况:

​返回值​

​含义​

​典型场景​

​正整数​

实际写入的字节数

成功写入部分或全部数据

0

未写入数据(特殊场景)

如写入空缓冲区或非阻塞设备满

​负值​

错误(通过 errno获取具体原因)

设备故障/空间不足/权限问题

举例:

char data[] = "Hello, Driver!";
int ret = write(fd, data, sizeof(data));
if (ret < 0) {perror("Write failed");
} else {printf("Wrote %d bytes\n", ret);
}

close函数

函数原型如下:

int close(int fd);

​参数​

​作用​

​注意事项​

fd

待关闭的文件描述符

必须是由 open或类似函数返回的有效描述符

返回值: 0 表示关闭成功,负值表示关闭失败。

举例:

int fd = open("/dev/chrdevbase", O_RDWR);
// ... 读写操作 ...
if (close(fd) == -1) {perror("Close failed");
}

编写测试 APP 程序

接下来编写一个简单的测试 APP,测试 APP 很简单通过输入相应的指令来对 chrdevbase 设备执行读或者写操作。

在1_chrdevbase 目录中新建 chrdevbaseApp.c 文件,在此文件中输入如下内容:

#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"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 file %s\r\n", filename);return -1;}if(atoi(argv[2]) == 1){ /* 从驱动文件读取数据 */retvalue = read(fd, readbuf, 50);if(retvalue < 0){printf("read file %s failed!\r\n", filename);}else{/* 读取成功,打印出读取成功的数据 */printf("read data:%s\r\n",readbuf);}}if(atoi(argv[2]) == 2){/* 向设备驱动写数据 */memcpy(writebuf, usrdata, 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 file %s\r\n", filename);return -1;}return 0;
}

数组 usrdata 是测试 APP 要向 chrdevbase 设备写入的数据。

argv[]是 main函数的参数数组,用于接收从命令行传入的参数。

  • argv[0]:程序自身的名称(可执行文件的路径或名称)。
  • argv[1]:要打开的驱动文件或设备文件的路径。
  • argv[2]:操作类型标志,决定是读取(1)还是写入(2)驱动文件。

比如,现在要从 chrdevbase 设备中读取数据,需要输入如下命令:

./chrdevbaseApp /dev/chrdevbase 1

当 argv[2]为 1 的时候,表示要从 chrdevbase 设备中读取数据,一共读取 50 字节的数据,读取到的数据保存在 readbuf 中,读取成功以后就在终端上打印出读取到的数据。

当 argv[2]为 2 的时候,表示要向 chrdevbase 设备写数据。

编译驱动程序和测试 APP

编译驱动程序

首先编译驱动程序,也就是 chrdevbase.c 这个文件,我们需要将其编译为.ko 模块。

创建Makefile 文件,然后在其中输入如下内容:

KERNELDIR := /home/huax/linux/linux_test/linux-imx-rel_imx_4.1.15_2.1.0_gaCURRENT_PATH := $(shell pwd)
obj-m := chrdevbase.obuild: kernel_moduleskernel_modules:$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
  • KERNELDIR 表示自己开发板所使用的 Linux 内核源码目录,使用绝对路径。
  • CURRENT_PATH 表示当前路径,直接通过运行“pwd”命令来获取当前所处路径。
  • obj-m 表示将 chrdevbase.c 这个文件编译为 chrdevbase.ko 模块。

其中具体的编译命令:

$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
    • -C $(KERNELDIR),-C选项告诉 make​​切换工作目录​​到 $(KERNELDIR)(即内核源码目录)
    • ​​M=$(CURRENT_PATH),M=是内核构建系统的特殊参数,指定​​外部模块的源代码目录​​(即当前模块所在的路径)。
    • modules,​​这是内核构建系统的目标(target),表示​​编译外部模块​​。最终会生成 .ko(内核模块)文件。

    Makefile 编写好以后输入“make”命令编译驱动模块,编译成功以后就会生成一个叫做 chrdevbaes.ko 的文件,此文件就是 chrdevbase 设备的驱动模块。

    编译测试 APP

    只有一个文件,直接用gcc编译:

    arm-linux-gnueabihf-gcc chrdevbaseApp.c -o chrdevbaseApp

    编译完成以后会生成一个叫做 chrdevbaseApp 的可执行程序。

    查看chrdevbaseAPP 这个程序的文件信息,可以输入:

    file chrdevbaseApp

    运行测试

    加载驱动模块

    Linux 系统选择通过 TFTP 从网络启动,并且使用 NFS 挂载网络根文件系统。

    启动 Linux 系统,检查开发板根文件系统中有没有“/lib/modules/4.1.15”这个目录,如果没有的话自行创建。4.1.15是因为ALPHA 开发板现在用的是 4.1.15 版本的 Linux 内核。

    将 chrdevbase.ko 和 chrdevbaseAPP 复制到 根文件系统rootfs/lib/modules/4.1.15 目录中,命令如下:

    sudo cp chrdevbase.ko chrdevbaseApp /home/huax/linux/nfs/rootfs/lib/modules/4.1.15/ -f

    拷贝完成以后就会在开发板的 /lib/modules/4.1.15 目录下存在 chrdevbase.ko 和chrdevbaseAPP 这两个文件,如图:

    加载 chrdevbase.ko 驱动文件,可以用以下两个命令:

    insmod chrdevbase.ko
    modprobe chrdevbase.ko

    如果使用 modprobe 加载驱动,提示无法打开“modules.dep”这个文件:

    直接输入 depmod 命令,会自动生成 modules.alias、modules.symbols 和 modules.dep 这三个文件,如图:

    然后再使用modprobe 加载 chrdevbase.ko,结果如图::

    输入“lsmod”命令即可查看当前系统中存在的模块,结果如图:

    存在chrdevbase”这一个模块,再查看当前系统中有没有 chrdevbase 这个设备:

    cat /proc/devices

    创建设备节点文件

    驱动加载成功需要在/dev 目录下创建一个与之对应的设备节点文件,应用程序就是通过操作这个设备节点文件来完成对具体设备的操作。

    输入如下命令创建/dev/chrdevbase 这个设备节点文件:

    mknod /dev/chrdevbase c 200 0
    • /dev/chrdevbase”是要创建的节点文件,
    • “c”表示这是个字符设备,
    • “ 200”是设备的主设备号,
    • “ 0”是设备的次设备号。

    创建完成以后就会存在/dev/chrdevbase 这个文件。

    可以使用“ls /dev/chrdevbase -l”命令查看,结果如图:

    如果 chrdevbaseAPP 想要读写 chrdevbase 设备,直接对/dev/chrdevbase 进行读写操作即可。相当于/dev/chrdevbase 这个文件是 chrdevbase 设备在用户空间中的实现。

    chrdevbase 设备操作测试

    使用 chrdevbaseApp 软件操作 chrdevbase 这个设备,看看读写是否正常。

    首先进行读操作,输入如下命令:

    ./chrdevbaseApp /dev/chrdevbase 1

    结果如图:

    然后测试对 chrdevbase 设备的写操作,输入如下命令:

    ./chrdevbaseApp /dev/chrdevbase 2

    结果如图:

    对 chrdevbase 的读写操作正常,说明我们编写的 chrdevbase 驱动是没有问题的。

    卸载驱动模块

    输入如下命令卸载掉 chrdevbase 这个设备:

    rmmod chrdevbase.ko

    卸载以后,可以使用 lsmod 命令查看 chrdevbase 这个模块还存不存在。

    本讲实验就结束了,以一个虚拟的 chrdevbase 设备为例,完成了第一个字符设备驱动的开发,掌握了字符设备驱动的开发框架以及测试方法。

    http://www.dtcms.com/a/303707.html

    相关文章:

  1. 【C++算法】79.BFS解决FloodFill算法_图像渲染
  2. 【C#|C++】C#调用C++导出的dll之非托管的方式
  3. 数据结构 排序(1)---插入排序
  4. 基于mysql云数据库对比PowerBI vs QuickBI vs FineBI更换数据源的可行性
  5. Kafka——Kafka控制器
  6. 如何选择工业电脑?
  7. 【VOS虚拟操作系统】未来之窗打包工具在前端资源优化中的应用与优势分析——仙盟创梦IDE
  8. Spring AI集成Elasticsearch向量检索时filter过滤失效问题排查与解决方案
  9. ICT模拟零件测试方法--晶体管测试
  10. Linux救援模式之应用篇
  11. 算法第29天|动态规划dp2:不同路径、不同路径Ⅱ、整数拆分、不同的二叉搜索树
  12. PHP云原生架构:容器化、Kubernetes与Serverless实践
  13. 【人工智能】OpenAI的AI代理革命:通向超拟人交互的未来之路
  14. idea 服务器Debug端口启动设置
  15. WD5018电压12V降5V,2A电流输出,应用于车载充电器车载设备供电
  16. AI-调查研究-41-多模态大模型量化 Qwen2.5-VL:技术架构、能力评估与应用场景详解
  17. TOPSIS(Technique for Order Preference by Similarity to Ideal Solution )简介与简单示例
  18. Android Slices:让应用功能在系统级交互中触手可及
  19. 从0到1理解大语言模型:读《大语言模型:从理论到实践(第2版)》笔记
  20. Botanix 主网上线,开启 BTC 可编程新时代!
  21. 【ASP.NET Core】探讨注入EF Core的DbContext在HTTP请求中的生命周期
  22. Ruby 发送邮件 - SMTP
  23. 广泛分布于内侧内嗅皮层全层的速度细胞(speed cells)对NLP中的深层语义分析的积极影响和启示
  24. Angular面试题目和答案大全
  25. SketchUp纹理贴图插件Architextures安装使用图文教程
  26. 深入浅出设计模式——创建型模式之建造者模式
  27. vue让elementUI和elementPlus标签内属性支持rem单位
  28. nginx 413 Request Entity Too Large
  29. uniapp 微信小程序 列表点击分享 不同的信息
  30. 重塑浏览器!微软在Edge加入AI Agent,自动化搜索、预测、整合