file_operations结构体中的函数就是我们要实现的具体操作函数。
注意:
register_chrdev()和 unregister_chrdev()这两个函数是老版本驱动使用的。现在新字符设备驱动已经不再使用这两个函数,而是使用Linux内核推荐的新字符设备驱动API函数。
1、创建CharDeviceXXX.c
输入“cd /home/zgq/linux/Linux_Drivers/回车”,切换到“/home/zgq/linux/Linux_Drivers/”目录
输入“ls回车”,查看“/home/zgq/linux/Linux_Drivers/”目录
输入“mkdir CharDeviceXXX回车”,创建“CharDeviceXXX”目录
输入“ls回车”,查看“/home/zgq/linux/Linux_Drivers/”目录
输入“cd CharDeviceXXX回车”,切换到“/home/zgq/linux/Linux_Drivers/ CharDeviceXXX/”目录
输入“vi CharDeviceXXX.c回车”,打开“CharDeviceXXX.c”
CharDeviceXXX.c文件如下:
#include <linux/types.h>
//数据类型重命名
//使能bool,u8,u16,u32,u64, uint8_t, uint16_t, uint32_t, uint64_t
//使能s8,s16,s32,s64,int8_t,int16_t,int32_t,int64_t
#include <linux/kernel.h> //必须要包含的头文件
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h> //必须要包含的头文件
#include <linux/string.h> //下面要用到字符串,显然也要包含
//#include <linux/device.h> //必须要包含的头文件
//#include <linux/fs.h> //使能结构体"file_operations"
#define CharDeviceXXX_MAJOR 200
//定义主设备号
//静态分配设备号:在串口输入“cat/proc/devices”查询当前已用的主设备号
//然后使用一个“没有被使用的设备号”作为该设备的的主设备号
#define CharDeviceXXX_NAME "CharDeviceXXXName" //定义设备的名字
static char CharDeviceXXX_readbuf[100]; //读缓冲区
static char CharDeviceXXX_writebuf[100]; //写缓冲区
static char My_DataBuffer[] = {"My Data!"};
/* 打开设备 */
static int CharDeviceXXX_open(struct inode *inode, struct file *filp)
{
/* 用户实现具体功能 */
printk("CharDeviceXXX_open!\r\n");
return 0;
}
/* 从设备读取数据,保存到首地址为buf的数据块中,长度为cnt个字节 */
//file结构指针变量flip表示要打开的设备文件
//buf表示用户数据块的首地址
//cnt表示用户数据的长度,单位为字节
//loff_t结构指针变量offt表示“相对于文件首地址的偏移”
static ssize_t CharDeviceXXX_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
int ret = 0;
memcpy(CharDeviceXXX_readbuf, My_DataBuffer,sizeof(My_DataBuffer));
//将My_DataBuffer[]中的所有数据拷贝到CharDeviceXXX_readbuf[]
ret = copy_to_user( buf, CharDeviceXXX_readbuf, cnt );
//将CharDeviceXXX_readbuf[]中的前cnt个字节拷贝到buf[]中
if(ret==0) printk("Send the data to the user, and the result is ok!\r\n");
else printk("Send the data to the user, and the result is failed!\r\n");
return 0;
}
/* 向设备写数据,将数据块首地址为buf的数据,长度为cnt个字节,发送给用户 */
//file结构指针变量flip表示要打开的设备文件
//buf表示用户数据块的首地址
//cnt表示用户数据的长度,单位为字节
//loff_t结构指针变量offt表示“相对于文件首地址的偏移”
static ssize_t CharDeviceXXX_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
int ret = 0;
ret = copy_from_user(CharDeviceXXX_writebuf, buf, cnt);
//将buf[]中的前cnt个字节拷贝到CharDeviceXXX_writebuf[]中
if(ret==0) printk("Receive the data form user , and the result is ok!\r\n");
else printk("Receive the data form user , and the result is failed!\r\n");
return 0;
}
/* 关闭/释放设备 */
static int CharDeviceXXX_release(struct inode *inode, struct file *filp)
{
/* 用户实现具体功能 */
printk("CharDeviceXXX_release!\r\n");
return 0;
}
/*声明file_operations结构变量MyCharDevice_fops*/
/*它是指向设备的操作函数集合变量*/
const struct file_operations CharDeviceXXX_fops = {
.owner = THIS_MODULE,
.open = CharDeviceXXX_open,
.read = CharDeviceXXX_read,
.write = CharDeviceXXX_write,
.release = CharDeviceXXX_release,
};
/*驱动入口函数 */
static int __init CharDeviceXXX_init(void)
{
int ret;
ret = register_chrdev(CharDeviceXXX_MAJOR, CharDeviceXXX_NAME, &CharDeviceXXX_fops);
//注册字符设备驱动
//CharDeviceXXX_MAJOR为主设备号,采用宏CharDeviceXXX_NAME定义设备名字
//CharDeviceXXX_fops是设备的操作函数集合,它是file_operations结构变量
if (ret < 0)
{
pr_err("CharDeviceXXX_init is failed!!!\r\n");
return ret;
}
else pr_err("CharDeviceXXX_init is ok!!!\r\n");
return 0;
}
/*驱动出口函数 */
static void __exit CharDeviceXXX_exit(void)
{/*出口函数具体内容 */
unregister_chrdev(CharDeviceXXX_MAJOR, CharDeviceXXX_NAME);
//注销字符设备驱动
//CharDeviceXXX_MAJOR为主设备号,采用宏CharDeviceXXX_NAME定义设备名字
}
module_init(CharDeviceXXX_init);
//指定CharDeviceXXX_init()为驱动入口函数
module_exit(CharDeviceXXX_exit);
//指定CharDeviceXXX_exit()为驱动出口函数
MODULE_AUTHOR("Zhanggong");//添加作者名字
MODULE_LICENSE("GPL");//LICENSE采用“GPL协议”
MODULE_INFO(intree,"Y");
//去除显示“loading out-of-tree module taints kernel.”
2、Makefile文件的一般模板
输入“vi Makefile回车”
KERNELDIR := /home/zgq/linux/atk-mp1/linux/my_linux/linux-5.4.31
#使用“:=”将其后面的字符串赋值给KERNELDIR
CURRENT_PATH := $(shell pwd)
#采用“shell pwd”获取当前打开的路径
#使用“$(变量名)”引用“变量的值”
obj-m := CharDeviceXXX.o
#生成“obj-m”需要依赖“CharDeviceXXX.o”
build: kernel_modules
#生成“build”需要依赖“kernel_modules”
@echo $(KERNELDIR)
#输出KERNELDIR的值为“/home/zgq/linux/atk-mp1/linux/linux-5.4.31”
@echo $(CURRENT_PATH)
#输出CURRENT_PATH的值为/home/zgq/linux/Linux_Drivers/CharDeviceXXX”
@echo $(MAKE)
#输出MAKE的值为make
kernel_modules:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
#后面的"modules"表示编译成模块
#“KERNELDIR”上面定义为“/home/zgq/linux/atk-mp1/linux/my_linux/linux-5.4.31”,即“指定的工作目录”
#“CURRENT_PATH”上面定义为“当前的工作目录”
#“-C $(KERNELDIR) M=$(CURRENT_PATH) ”表示将“当前的工作目录”切换到“指定的目录”中
#即切换到“/home/zgq/linux/atk-mp1/linux/my_linux/linux-5.4.31”。
#M表示模块源码目录
#在“make和modules”之间加入“M=$(CURRENT_PATH)”,表示切换到由“CURRENT_PATH”指定的目录中读取源码,同时将其编>译为.ko 文件
clean:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
#“KERNELDIR”上面定义为“/home/zgq/linux/atk-mp1/linux/my_linux/linux-5.4.31”,即“指定的工作目录”
#“CURRENT_PATH”上面定义为“当前的工作目录
3、创建“c_cpp_properties.json”
打开VSCode,按下“Ctrl+Shift+P”,打开VSCode控制台,然后输入“C/C++:Edit Configurations(JSON)”,见下图:
打开以后会自动在“.vscode ”目录下生成一个名为“c_cpp_properties.json” 的文件,此文件默认内容如下所示:
{
"configurations": [
{
"name": "Linux",
"includePath": [
"${workspaceFolder}/**"
],
"defines": [],
"compilerPath": "/usr/bin/gcc",
"cStandard": "gnu11",
"cppStandard": "gnu++14",
"intelliSenseMode": "gcc-x64"
}
],
"version": 4
}
修改“includePath”后,“c_cpp_properties.json”文件如下:
{
"configurations": [
{
"name": "Linux",
"includePath": [
"${workspaceFolder}/**",
"/home/zgq/linux/atk-mp1/linux/my_linux/linux-5.4.31",
"/home/zgq/linux/Linux_Drivers/CharDeviceXXX_1",
"/home/zgq/linux/atk-mp1/linux/my_linux/linux-5.4.31/arch/arm/include",
"/home/zgq/linux/atk-mp1/linux/my_linux/linux-5.4.31/include",
"/home/zgq/linux/atk-mp1/linux/my_linux/linux-5.4.31/arch/arm/include/generated"
],
"defines": [],
"compilerPath": "/usr/bin/gcc",
"cStandard": "gnu11",
"cppStandard": "gnu++14",
"intelliSenseMode": "gcc-x64"
}
],
"version": 4
}
4、了解APP程序需要用到的相关函数
编写Linux应用程序,需要用到C库里面“和文件操作有关”的函数。
1)、open()函数
int open(const char *pathname, int flags)
指针变量pathname表示要打开的设备文件名;
flags表示“文件打开模式”:O_RDONLY表示只读模式;
O_WRONLY表示只写模式;
O_RDWR表示读写模式;
其他可选模式:O_APPEND表示每次写操作都写入文件的尾部;O_CREAT表示如果指定文件不存在,则创建这个文件;
O_EXCEL表示如果要创建的文件已经存在,则返回-1,并修改errno的值;O_TRUNC表示如果文件存在,并且以“只写或读写”方式打开,则清空文件全部内容;O_NOCTTY表示如果路径名指向终端设备,不要把这个设备用作控制终端;O_NONBLOCK表示如果路径名指向FIFO/块文字/字符文件,则把文件的打开和后继I/O设置为非阻塞;O_DSYNC表示等待物理I/O结束后再write。在不影响读取新写入的数据的前提下,不等待文件属性更新;O_RSYNC表示read等待所有写入同一区域的写操作完成后再进行;O_SYNC表示等待物理I/O结束后再write,包括更新文件属性的IO;
返回值:如果文件打开成功,则返回“文件描述符”。
在终端输入“man 2 open”可以查询open()这个函数
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
需要包含上面的头文件,才可以使用open()函数
2)、read()函数
ssize_t read(int fd, void *buf, size_t count)
fd表示要进行读操作的“文件描述符”
buf表示将读到的数据保存到“以buf为首地址的数据块”
count表示读取到的“数据长度”,单位为字节
返回值:
大于0表示读取到的字节数
0表示读到了文件的尾部
小于0表示读取失败
在终端输入“man 2 read”可以查询read()这个函数
#include <unistd.h>
需要包含上面这个头文件,才可以使用read()函数
3)、write()函数
ssize_t write(int fd, const void *buf, size_t count)
fd表示要进行写操作的“文件描述符”
buf和count表示将“以buf为首地址的数据块”,长度为count个字节,发送给用户
返回值:
大于0表示写入的字节数
0表示没有写入任何数据
小于0表示写入失败
在终端输入“man 2 write”可以查询write()这个函数
#include <unistd.h>
需要包含上面这个头文件,才可以使用write()函数
4)、close()函数
int close(int fd)
fd表示要关闭的“文件描述符”
返回值:
0表示关闭成功
小于0表示关闭失败
在终端输入“man 2 close”可以查询close()这个函数
#include <unistd.h>
需要包含上面这个头文件,才可以使用close()函数
在终端输入“man 3 memcpy”在第3节中可以查询memcpy()这个函数
#include <string.h>
需要包含上面这个头文件,才可以使用memcpy()函数
linux之man命令 (baidu.com)
man后面的数字代表的内容:
1:用户在shell环境可操作的命令或执行文件;如输入“man 1 ls”就可以查询ls命令
2:系统内核可调用的函数与工具等;如输入“man 2 read”就可以查询read()函数
3:一些常用的函数(function)与函数库(library),大部分为C的函数库(libc);
;如输入“man 3 strstr”就可以查询strstr()函数
4:设备文件说明,通常在/dev下的文件
5:配置文件或某些文件格式
6:游戏(games)
7:惯例与协议等,如Linux文件系统,网络协议,ASCII code等说明
8:系统管理员可用的管理命令
9:跟kernel有关的文件。
5、编写CharDeviceXXX_APP.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 usrdataBuffer[] = {"usr data!"};
//例如当argv[]是指向输入参数“./CharDeviceXXX /dev/chrdevbase 1”
/*
参数argc: argv[]数组元素个数
参数argv[]:是一个指针数组
返回值: 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;
}
//argv[]是指向输入参数“./CharDeviceXXXApp” “/dev/CharDeviceXXX” “1”
filename = argv[1];
//argv[1]指向字符串“/dev/CharDeviceXXX”
fd = open(filename, O_RDWR);
//以“读写方式”打开“/dev/CharDeviceXXX”文件,若成功则fd为“文件描述符”
//fd=0表示标准输入流; fd=1表示标准输出流;fd=2表示错误输出流;
if(fd < 0)
{
printf("Can't open file %s\r\n", filename);
return -1;
}
else printf("open file: %s\r\n", filename);
if(atoi(argv[2]) == 1)
{
retvalue = read(fd, readbuf, 50);
//fd表示要进行读操作的“文件描述符”
//readbuf表示将读到的数据保存到“以readbuf为首地址的数据块”
//50表示读取到的“数据长度”,单位为字节
//返回值大于0表示读取到的字节数
//返回值为0表示读到了文件的尾部
//返回值小于0表示读取文件失败
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, usrdataBuffer, sizeof(usrdataBuffer));
//将usrdataBuffer[]中所有数据拷贝到writebuf[]
retvalue = write(fd, writebuf, 50);
//fd=2表示要进行写操作的“文件描述符”
//将writebuf[]中前50个字节发送给用户
//返回值大于0表示写入的字节数;
//返回值等于0表示没有写入任何数据;
//返回值小于0表示写入失败
if(retvalue < 0)
{
printf("write file %s failed!\r\n", filename);
}
}
/* 关闭设备 */
retvalue = close(fd);
//fd表示要关闭的“文件描述符”
//返回值等于0表示关闭成功
//返回值小于0表示关闭失败
if(retvalue < 0)
{
printf("Can't close file %s\r\n", filename);
return -1;
}
return 0;
}
6、编写脚本文件
CharDeviceXXX_APP.sh文件内容如下:
#!/bin/sh
file="CharDeviceXXX_APP"
if [ -s $file ]
#"-s $file"检测文件是否为空(文件大小是否大于0),不为空返回 true
then
rm CharDeviceXXX_APP
echo clear CharDeviceXXX_APP
else
arm-none-linux-gnueabihf-gcc CharDeviceXXX_APP.c -o CharDeviceXXX_APP
echo CharDeviceXXX_APP
fi
7、测试
输入“make回车”编译生成“CharDeviceXXX.ko”
输入“chmod 777 CharDeviceXXX_APP.sh回车”
将“CharDeviceXXX_APP.sh”赋可执行权限
输入“./CharDeviceXXX_APP.sh回车”,编译生成“CharDeviceXXX_APP”文件
输入“sudo cp CharDeviceXXX.ko CharDeviceXXX_APP /home/zgq/linux/nfs/rootfs/lib/modules/5.4.31/ -f回车”
启动开发板,从网络下载程序
输入“root”
输入“cd /lib/modules/5.4.31/回车”
切换到“/lib/modules/5.4.31/”目录
注意:“lib/modules/5.4.31/”在虚拟机中是位于“/home/zgq/linux/nfs/rootfs/”目录下,但在开发板中,却是位于根目录中。
输入“ls”查看“CharDeviceXXX.ko和CharDeviceXXXApp”是否存在
输入“depmod”,驱动在第一次执行时,需要运行“depmod”
输入“modprobe CharDeviceXXX.ko”,加载“CharDeviceXXX.ko”模块
输入“lsmod”查看有哪些驱动在工作
输入“mknod /dev/CharDeviceXXX c 200 0回车”
//“mknod”是创建节点命令
//“/dev/CharDeviceXXX”表示节点文件
//“c”表示CharDeviceXXX是个字符设备
//“200”表示设备的主设备号
//“0”表示设备的次设备号
输入“ls /dev/CharDeviceXXX -l回车”,发现节点文件“/dev/CharDeviceXXX”
输入“./CharDeviceXXX_APP /dev/CharDeviceXXX 1回车”执行读操作
输入“./CharDeviceXXX_APP /dev/CharDeviceXXX 2回车”执行写操作
输入“cat /proc/devices回车”查询设备号
操作完成,则执行卸载模块:
输入“rmmod CharDeviceXXX.ko”,卸载“CharDeviceXXX.ko”模块
注意:输入“rmmod CharDeviceXXX”也可以卸载“CharDeviceXXX.ko”模块
输入“lsmod”查看有哪些驱动在工作。
至此,CharDeviceXXX设备的整个驱动就验证完成了,驱动工作正常。
8、修改Makefile文件如下:
KERNELDIR := /home/zgq/linux/atk-mp1/linux/my_linux/linux-5.4.31
#使用“:=”将其后面的字符串赋值给KERNELDIR
CURRENT_PATH := $(shell pwd)
#采用“shell pwd”获取当前打开的路径
#使用“$(变量名)”引用“变量的值”
obj-m := CharDeviceXXX.o
#生成“obj-m”需要依赖“CharDeviceXXX.o”
ko: kernel_modules
#生成“build”需要依赖“kernel_modules”
@echo $(KERNELDIR)
#输出KERNELDIR的值为“/home/zgq/linux/atk-mp1/linux/linux-5.4.31”
@echo $(CURRENT_PATH)
#输出CURRENT_PATH的值为/home/zgq/linux/Linux_Drivers/CharDeviceXXX”
@echo $(MAKE)
#输出MAKE的值为make
kernel_modules:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
#后面的"modules"表示编译成模块
#“KERNELDIR”上面定义为“/home/zgq/linux/atk-mp1/linux/my_linux/linux-5.4.31”,即“指定的工作目录”
#“CURRENT_PATH”上面定义为“当前的工作目录”
#“-C $(KERNELDIR) M=$(CURRENT_PATH) ”表示将“当前的工作目录”切换到“指定的目录”中
#即切换到“/home/zgq/linux/atk-mp1/linux/my_linux/linux-5.4.31”。
#M表示模块源码目录
#在“make和modules”之间加入“M=$(CURRENT_PATH)”,表示切换到由“CURRENT_PATH”指定的目录中读取源码,同时将其编>译为.ko 文件
clean_ko:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
#“KERNELDIR”上面定义为“/home/zgq/linux/atk-mp1/linux/my_linux/linux-5.4.31”,即“指定的工作目录”
#“CURRENT_PATH”上面定义为“当前的工作目录
app:
arm-none-linux-gnueabihf-gcc CharDeviceXXX_APP.c -o CharDeviceXXX_APP
clean_app:
rm CharDeviceXXX_APP
9、使用Makefile编译
输入“rm CharDeviceXXX_APP.sh回车”
输入“make clean_ko回车”,清除CharDeviceXXX.*
输入“make ko回车”,编译生成CharDeviceXXX.ko
输入“make clean_app回车”,清除CharDeviceXXX_APP
输入“make app回车”,编译生成CharDeviceXXX_APP
输入“ls -l回车”
输入“sudo cp CharDeviceXXX.ko CharDeviceXXX_APP /home/zgq/linux/nfs/rootfs/lib/modules/5.4.31/ -f回车”