• 首页 首页 icon
  • 工具库 工具库 icon
    • IP查询 IP查询 icon
  • 内容库 内容库 icon
    • 快讯库 快讯库 icon
    • 精品库 精品库 icon
    • 问答库 问答库 icon
  • 更多 更多 icon
    • 服务条款 服务条款 icon

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

武飞扬头像
Elec Liu
帮助5

一、什么是字符设备

字符设备是 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,到这里我们就大功告成了!

这篇好文章是转载于:学新通技术网

  • 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
  • 本站站名: 学新通技术网
  • 本文地址: /boutique/detail/tanhfheehf
系列文章
更多 icon
同类精品
更多 icon
继续加载