基于Cortex-A7架构的嵌入式linux ARM驱动开发<1>——字符设备驱动开发

一、什么是字符设备

字符设备是 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.kormmod 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命令查看openclosereadwriteprintf的详细信息,里面的信息包括了包含的头文件、函数的写法、函数的输入输出要求以及注意事项等信息。是我们编写应用程序最重要的参考依据。
在这里插入图片描述
我们需要在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()memcpycopy_to_usercopy_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,到这里我们就大功告成了!

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Elec Liu

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值