一、什么是字符设备
字符设备
是 Linux 驱动中最基本的一类设备驱动,字符设备就是一个一个字节,按照字节流进行读写操作的设备
,读写数据是分先后顺序的。比如我们最常见的点灯、按键、IIC、SPI, LCD
等等都是字符设备,这些设备的驱动就叫做字符设备驱动。
二、字符设备驱动开发步骤
1、驱动模块的加载和卸载
Linux 驱动有两种运行方式,第一种
就是将驱动编译进 Linux 内核中,这样当 Linux 内核启动的时候就会自动运行驱动程序。第二种
就是将驱动编译成模块(Linux 下模块扩展名为.ko),在Linux 内核启动以后使用“modprobe”命令加载驱动模块。
在调试驱动的时候一般都选择将其编译为模块,这样我们修改驱动以后只需要编译一下驱动代码即可,不需要编译整个 Linux 代码。而且在调试的时候只需要加载或者卸载驱动模块即可,不需要重启整个系统。总之,将驱动编译为模块最大的好处就是方便开发,当驱动开发完成,确定没有问题以后就可以将驱动编译进Linux 内核中,当然也可以不编译进 Linux 内核中,具体看自己的需求。
模块有加载
和卸载
两种操作,我们在编写驱动的时候需要注册这两种操作函数,模块的加载和卸载注册函数如下:
module_init(xxx_init); //注册模块加载函数
module_exit(xxx_exit); //注册模块卸载函数
为了搞清楚这两个函数的使用方法,我们可以参照一下内核的写法。最后写出相应代码demo_project.c
。
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/init.h>
static int __init demo_project_init(void)//定义了个名为demo_project_init的驱动入口函数,并且使用了“__init”来修饰
{
printk("demo_project_init!\r\n");//内核打印函数,相当于“printf”
return 0;
}
static void __exit demo_project_exit(void)//定义了个名为 demo_project_exit 的驱动出口函数,并且使用了“__exit”来修饰。
{
printk("demo_project_exit!\r\n");
}
module_init(demo_project_init);//调用函数 module_init 来声明 demo_project_init 为驱动入口函数,当加载驱动的时候 demo_project_init函数就会被调用。
module_exit(demo_project_exit);//调用函数 module_exit来声明 demo_project_exit为驱动出口函数,当加载驱动的时候 demo_project_exit函数就会被调用。
MODULE_AUTHOR("Liuhao");//注明作者
MODULE_DESCRIPTION("test_moudle");//注明代码描述
MODULE_LICENSE("GPL");//注明权限
然后我们现在写一下Makefile
。
KERNELDIR := /home/lh/linux/alientek_linux/linux-imx-rel_imx_4.1.15_2.1.0_ga
CURRENT_PATH := $(shell pwd)
obj-m := demo_project.o
build: kernel_modules
kernel_modules:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
第 1 行,KERNELDIR 表示开发板所使用的 Linux 内核源码目录,使用绝对路径,大家根据自己的实际情况填写即可。
第 2 行,CURRENT_PATH 表示当前路径,直接通过运行“pwd”命令来获取当前所处路径。
第 3 行,obj-m 表示将 demo_project.c 这个文件编译为 chrdevbasedemo_project.ko 模块。
第 8 行,具体的编译命令,后面的 modules 表示编译模块,-C 表示将当前的工作目录切换到指定目录中,也就是 KERNERLDIR 目录。M 表示模块源码目录,“make modules”命令中加入 M=dir 以后程序会自动到指定的 dir 目录中读取模块的源码并将其编译为.ko 文件。
然后,我们make
一下:
这样一来发现最终编译是成功的。当然,我们也需要提前写好工作区下的两个配置文件。
第一个是c_cpp_properties.json
:
{
"configurations": [
{
"name": "Linux",
"includePath": [
"${workspaceFolder}/**",
"/home/lh/linux/alientek_linux/linux-imx-rel_imx_4.1.15_2.1.0_ga/include",
"/home/lh/linux/alientek_linux/linux-imx-rel_imx_4.1.15_2.1.0_ga/arch/arm/include",
"/home/lh/linux/alientek_linux/linux-imx-rel_imx_4.1.15_2.1.0_ga/arch/arm/include/generated/"
],
"defines": [],
"compilerPath": "/usr/bin/clang",
"cStandard": "c11",
"cppStandard": "c++17",
"intelliSenseMode": "clang-x64"
}
],
"version": 4
}
第二个是settings.json
:
{
"search.exclude": {
"**/node_modules": true,
"**/bower_components": true,
"**/*.o":true,
"**/*.su":true,
"**/*.cmd":true,
"Documentation":true,
},
"files.exclude": {
"**/.git": true,
"**/.svn": true,
"**/.hg": true,
"**/CVS": true,
"**/.DS_Store": true,
"**/*.o":true,
"**/*.su":true,
"**/*.cmd":true,
"Documentation":true,
}
}
make
之前的目录是:
make
之后的目录是:
然后,我们将生成的.ko
文件复制到对应目录下面去。
cp ./demo_project.ko /home/lh/linux/nfs/rootfs/lib/modules/4.1.15/
这样一来,我们在串口终端的相应目录下也看到了这个文件。
在第一次使用这个模块的时候,我们可能会报错。
在这个时候我们在串口终端输入depmod
后恢复正常。最终是生成了四个文件:
然后,我们分别使用modprobe demo_project.ko
和rmmod demo_project.ko
来加载和卸载设备,使用lsmod
命令来查看设备,效果如下:
这样一来,驱动模块的加载和卸载就算顺利完成了。
2、字符设备注册与注销
对于字符设备驱动而言,当驱动模块加载成功以后需要注册字符设备
,同样,卸载驱动模块的时候也需要注销掉字符设备
。字符设备的注册和注销函数原型如下所示:
static inline int register_chrdev(unsigned int major, const char *name,const struct file_operations *fops)
static inline void unregister_chrdev(unsigned int major, const char *name)
根据这两个函数,利用现有的内核资源,编辑出相应的驱动代码:
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#define DEMO_PROJECT_MAJOR 200//主设备号
#define DEMO_PROJECT_NAME "demo_project"//设备名称
static int demo_project_open(struct inode *inode, struct file *filp){
printk("demo_project_open!\r\n");
return 0;
}
static int demo_project_release(struct inode *inode, struct file *filp){
printk("demo_project_release!\r\n");
return 0;
}
static ssize_t demo_project_read(struct file *file, char __user *buffer, size_t count,loff_t *ppos){
printk("demo_project_read!\r\n");
return 0;
}
static ssize_t demo_project_write(struct file *file, const char __user *buffer,size_t count, loff_t *ppos){
printk("demo_project_write!\r\n");
return 0;
}
const struct file_operations demo_project_fops={
.owner = THIS_MODULE,
.open = demo_project_open,
.release = demo_project_release,
.write = demo_project_write,
.read = demo_project_read,
};
static int __init demo_project_init(void)
{
int res_register_chrdev;
//注册字符设备
res_register_chrdev=register_chrdev(DEMO_PROJECT_MAJOR,DEMO_PROJECT_NAME,&demo_project_fops);
if (res_register_chrdev < 0) {
printk(KERN_ERR "demo_project: couldn't get a major number.\n");
return res_register_chrdev;
}
printk("demo_project_init!\r\n");
return 0;
}
static void __exit demo_project_exit(void)
{
//注销字符设备
unregister_chrdev(DEMO_PROJECT_MAJOR,DEMO_PROJECT_NAME);
printk("demo_project_exit!\r\n");
}
module_init(demo_project_init);
module_exit(demo_project_exit);
MODULE_AUTHOR("Liuhao");
MODULE_DESCRIPTION("test_moudle");
MODULE_LICENSE("GPL");
这里面有一个设备号的概念:为了方便管理,Linux 中每个设备都有一个设备号,设备号由主设备号和次设备号两部分组成,主设备号表示某一个具体的驱动,次设备号表示使用这个驱动的各个设备。Linux 提供了一个名为 dev_t 的数据类型表示设备号,dev_t 定义在文件 include/linux/types.h 里面,dev_t 其实就是 unsigned int 类型,是一个 32 位的数据类型。这 32 位的数据构成了主设备号和次设备号两部分,其中高 12 位为主设备号,低 20 位为次设备号。因此 Linux系统中主设备号范围为 0~4095。
我们使用cat /proc/devices
命令查看一下有哪些设备,其中字符设备如下:
3、初步编写字符设备应用程序
首先,我们利用man
命令查看open
、close
、read
、write
和printf
的详细信息,里面的信息包括了包含的头文件、函数的写法、函数的输入输出要求以及注意事项等信息。是我们编写应用程序最重要的参考依据。
我们需要在vscode中创建一个名为demo_projectAPP.c
的文件,经过我们熟练地使用man
指令,写出下面的程序:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
/*
*argc:应用程序的参数个数
*argv[]:具体的参数内容,字符串形式
*./demo_projectAPP <filename>
*/
int main(int argc,char *argv[]){
//文件打开//
int fd=0;
char *filename;
filename=argv[1];
fd=open(filename,O_RDWR);
if(fd<0){
printf("Can`t open file %s\r\n",filename);
return -1;
}
//文件读取//
int ret_read=0;
char read_buff[100];
ret_read=read(fd,read_buff,10);
if(ret_read<0){
printf("Can`t read this file \r\n");
}
//文件写入//
int ret_write=0;
char write_buff[100];
ret_write=write(fd,write_buff,10);
if(ret_write<0){
printf("Can`t write this file \r\n");
}
/
int ret_close=0;
ret_close=close(fd);
if(ret_close<0){
printf("Can`t close this file \r\n");
}
}
我们使用arm-linux-gnueabinf-gcc
来编译一下该应用程序
将生成的文件拷贝到相应目录下
cp demo_projectAPP /home/lh/linux/nfs/rootfs/lib/modules/4.1.15/
驱动加载成功需要在/dev 目录下创建一个与之对应的设备节点文件,应用程序就是通过操作这个设备节点文件来完成对具体设备的操作。输入如下命令创建/dev/demo_project
这个设备节点文件:
mknod /dev/demo_project c 200 0
其中mknod
是创建节点命令,/dev/demo_project
是要创建的节点文件,c
表示这是个字符设备,200
是设备的主设备号,0
是设备的次设备号。创建完成以后就会存在/dev/demo_project
这个文件,可以使用ls /dev/demo_project-l
命令查看
重新利用make
编译模块并拷贝.ko至相应目录下,重复第一小节的操作。我们最后利用cat /proc/devices
命令查看一下有哪些设备。
发现有主设备号为200的设备。最后运行这个应用程序
到这里就大功告成了。
4、完善字符设备应用程序
这一小节写起来会非常轻松了,这里主要是使用到了四个函数:atoi()
、memcpy
、copy_to_user
和copy_from_user
。
demo_project.c
的代码如下:
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/crash_dump.h>
#include <linux/uaccess.h>
#include <linux/io.h>
#define DEMO_PROJECT_MAJOR 200
#define DEMO_PROJECT_NAME "demo_project"
static char read_buff[100];
static char write_buff[100];
static char kerneldata[]={"Kernel data!"};
static int demo_project_open(struct inode *inode, struct file *filp){
//printk("demo_project_open!\r\n");
return 0;
}
static int demo_project_release(struct inode *inode, struct file *filp){
//printk("demo_project_release!\r\n");
return 0;
}
static ssize_t demo_project_read(struct file *file, char __user *buffer, size_t count,loff_t *ppos){
int ret_value=0;
//printk("demo_project_read!\r\n");
//向用户空间发送数据
memcpy(read_buff,kerneldata,count);
ret_value=copy_to_user(buffer,read_buff,count);
if(ret_value==0){
printk("Kernel send data success!\r\n");
}else{
printk("Kernel send data fail!\r\n");
}
return 0;
}
static ssize_t demo_project_write(struct file *file, const char __user *buffer,size_t count, loff_t *ppos){
//printk("demo_project_write!\r\n");
int ret_value=0;
ret_value=copy_from_user(write_buff,buffer,count);
if(ret_value==0){
printk("Kernel received data is%s\r\n",write_buff);
}else{
printk("Kernel received data fail!");
}
return 0;
}
const struct file_operations demo_project_fops={
.owner = THIS_MODULE,
.open = demo_project_open,
.release = demo_project_release,
.write = demo_project_write,
.read = demo_project_read,
};
static int __init demo_project_init(void)
{
int res_register_chrdev;
res_register_chrdev=register_chrdev(DEMO_PROJECT_MAJOR,DEMO_PROJECT_NAME,&demo_project_fops);
if (res_register_chrdev < 0) {
printk(KERN_ERR "demo_project: couldn't get a major number.\n");
return res_register_chrdev;
}
printk("demo_project_init!\r\n");
return 0;
}
static void __exit demo_project_exit(void)
{
unregister_chrdev(DEMO_PROJECT_MAJOR,DEMO_PROJECT_NAME);
printk("demo_project_exit!\r\n");
}
module_init(demo_project_init);
module_exit(demo_project_exit);
MODULE_AUTHOR("Liuhao");
MODULE_DESCRIPTION("test_moudle");
MODULE_LICENSE("GPL");
demo_projectAPP.c
的代码如下:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include "string.h"
/*
*argc:应用程序的参数个数
*argv[]:具体的参数内容,字符串形式
*./demo_projectAPP <filename>
*/
static char usrdata[]={"usr data!"};
int main(int argc,char *argv[]){
//语法判断
if(argc!=3){
printf("EOERR USEAGE");
return -1;
}
//文件打开//
int fd=0;
char *filename;
filename=argv[1];
fd=open(filename,O_RDWR);
if(fd<0){
printf("Can`t open file %s\r\n",filename);
return -1;
}
//文件读取//
int ret_read=0;
char read_buff[100];
if(atoi(argv[2])==1){
ret_read=read(fd,read_buff,10);
if(ret_read<0){
printf("Can`t read this file \r\n");
}else{
printf("APP read data is %s\r\n",read_buff);
}
}
//文件写入//
int ret_write=0;
char write_buff[100];
if(atoi(argv[2])==2){
memcpy(write_buff,usrdata,sizeof(usrdata));
ret_write=write(fd,write_buff,10);
if(ret_write<0){
printf("Can`t write this file \r\n");
}else{
printf("APP write data success!\r\n");
}
}
//文件关闭
int ret_close=0;
ret_close=close(fd);
if(ret_close<0){
printf("Can`t close this file \r\n");
}
}
三、字符设备驱动开发实验效果
我们在终端进行操作,得到相应的结果:
OK,到这里我们就大功告成了!