字符设备驱动

字符设备框架搭建

(1)创建工程

       新建驱动开发项目文件目录

1、添加头文件路径

        因为是编写 Linux 驱动,因此会用到 Linux 源码中的函数。我们需要在 VSCode 中添加 Linux
源码中的头文件路径。打开 VSCode,按下“Crtl+Shift+P”打开 VSCode 的控制台,然后输入
C/C++: Edit configurations(JSON) ”,打开 C/C++编辑配置文件

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

对该文件进行修改,修改后的c_cpp_properties.json 为

{
    "configurations": [
        {
            "name": "Linux",
            "includePath": [
                "${workspaceFolder}/**",
                "/home/f3161/linux/IMX6ULL/linux/include", 
                "/home/f3161/linux/IMX6ULL/linux/arch/arm/include", 
                "/home/f3161/linux/IMX6ULL/linux/arch/arm/include/generated/"
            ],
            "defines": [],
            "compilerPath": "/usr/bin/clang",
            "cStandard": "c11",
            "cppStandard": "c++17",
            "intelliSenseMode": "clang-x64"
        }
    ],
    "version": 4
}

其中 includPath内为内核目录,修改成自己具体的目录

2、Makefile文件

新建Makefile文件,输入内容如下

KERNELDIR := /home/f3161/linux/IMX6ULL/linux
CURRENT_PATH := $(shell pwd)
obj-m := xxx.o

build: kernel_modules

kernel_modules:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean

第一行为内核路径

第三行是生成的.ko文件名,需要与.c文件对应。

(2)编写驱动程序xxx.c

1、头文件

xxx.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>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_gpio.h>
#include <linux/semaphore.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>
#include <linux/timer.h>
#include <linux/of_irq.h>
#include <linux/irq.h>
#include <linux/wait.h>
#include <linux/poll.h>
#include <linux/fcntl.h>

#define XXX_CNT 1 //设备号个数
#define XXX_NAME "xxx" //设备号名字

2、注册驱动

 /* 驱动入口函数 */
static int __init xxx_init(void)
{
int ret = 0;
return ret;
 }
 /* 驱动出口函数 */
static void __exit xxx_exit(void)
 {
 }
module_init(xxx_init);    
module_exit(xxx_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("FANG");

注意驱动入口出口函数的参数。

3、添加设备结构体

//设备结构体
struct xxx_dev{
   dev_t devid;//设备号
   int major;//主设备号
   int minor;//次设备号
   struct cdev cdev; //字符设备
   struct class *class;//类
   struct device *device;//设备
   struct device_node *nd;//设备树节点
   int xxx_gpio; //GPIO编号
};
struct xxx_dev xxx;

devid:每个设备都有一个设备号,设备号由主设备号(major)和次设备号两部分组成

cdev:用来注册字符设备

class,device:来于自动创建设备节点

xxx_gpio:        设备所用的GPIO编号(设备树需要)       

4、申请设备号

在入口函数static int __init xxx_init(void)中添加

   xxx.major = 0;
   if(xxx.major){ //给定设备号
      xxx.devid = MKDEV(xxx.major, 0);
      ret = register_chrdev_region(xxx.devid, DTSLED_CNT, XXX_NAME);
   }else{ // 申请设备号
      ret = alloc_chrdev_region(&xxx.devid, 0, DTSLED_CNT, XXX_NAME);
      xxx.major = MAJOR(xxx.devid);
   }
   if(ret<0){
      goto fail_devid;
   }

其中,DTSLED_CNT表示设备号个数,需要.c文件开头添加宏 #define DTSLED_CNT 1

        XXX_NAME表示设备号名字,添加宏 #define XXX_NAME "xxx"

        MKDEV用于将给定的主设备号和次设备号的值(这里是0)组合成 dev_t 类型的设备号。

        register_chrdev_region函数使用给定的主设备号和次设备号注册设备号

        函数原型:int register_chrdev_region(dev_t from, unsigned count, const char *name)
         参数 from 是要申请的起始设备号,也就是给定的设备号;参数 count 是要申请的数量,一般都是一个;参数 name 是设备名字。

        alloc_chrdev_region函数是在没有指定设备号时申请设备号

        函数原型:int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)

        参数dev是申请到的设备号baseminor是次设备号起始地址(一般用0),count和name同上。

如果申请失败,则goto fail_devid(后面提及)

在驱动出口函数static void __exit disled_exit(void)中,需要释放设备号

unregister_chrdev_region(xxx.devid, DTSLED_CNT);

5、添加字符设备操作集

static const struct file_operations xxx_fops = {
   .owner = THIS_MODULE,
   .write = xxx_write,
   .open = xxx_open,
   .read = xxx_read,
   .release = xxx_release,
};

添加字符设备操作集对应的函数

static int xxx_open(struct inode *inode, struct file *filp){
      filp->private_data = &xxx;
      return 0;
}
static ssize_t xxx_write(struct file *filp, const char __user *buf, size_t count,loff_t *ppos){
   struct xxx_dev *dev = (struct xxx_dev*)filp->private_data;
   return 0;
}
static int xxx_release(struct inode *inode, struct file *filp){
      struct xxx_dev *dev = (struct xxx_dev*)filp->private_data;
      return 0;
}
static ssize_t xxx_read(struct file *filp, char __user *buf,size_t cnt, loff_t *offt)
 {
    struct xxx_dev *dev = (struct xxx_dev *)filp->private_data;
    return 0;
}

其中设置了文件的私有属性,将设备结构体作为私有数据添加到设备文件中。在上述函数中使用dev变量代替xxx_dev

xxx_write函数

xxx_write函数表示用户空间(应用程序)传递给内核(驱动程序)的数据并且打印出来,因为用户空间内存不能直接访问内核空间的内存,所以需要借助函数 copy_from_user 将用户空间的数据复制到 writebuf 这个内核空间中。

函数原型:

static inline long copy_from_user(void __user *to, const void *from, unsigned long n)

参数 to 表示目的(驱动程序数据),参数 from 表示源(应用程序数据),参数 n 表示要复制的数据长度。如果复制成功,返回值为 0,如果复制失败则返回负数。

所以修改后的xxx_write()函数:——>将应用程序中的数据传递给驱动程序

static char writebuf[100];
static ssize_t xxx_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\r\n", writebuf);
    }else{
        printk("kernel recevdata failed!\r\n");
    }
    return 0;
}
xxx_read函数

xxx_read函数表示内核(驱动程序)向用户空间(应用程序)传递的数据 ,因为内核空间不能直接操作用户空间的内存,因此需要借助 copy_to_user 函数来完成内核空间的数据到用户空间的复制。

函数原型

static inline long copy_to_user(void __user *to, const void *from, unsigned long n)

 参数 to 表示目的(应用程序数据),参数 from 表示源(驱动程序数据),参数 n 表示要复制的数据长度。如果复制成功,返回值为 0,如果复制失败则返回负数。

所以修改后的xxx_read()函数:——>将驱动程序中的kerneldata变量的数据传递给应用程序

static char kerneldata[] = {"kernel data!"};
static char readbuf[100];
static ssize_t xxx_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!\r\n");
    }else{
        printk("kernel senddata failed!\r\n");
    }
    return 0;
}

6、注册字符设备

在入口函数static int __init xxx_init(void)中添加

   xxx.cdev.owner = THIS_MODULE;
   cdev_init(&xxx.cdev, &xxx_fops);
   ret = cdev_add(&xxx.cdev, xxx.devid, XXX_CNT);
   if(ret<0){
    goto fail_cdev;
}
     

cdev_init函数对字符设备进行初始化。

函数原型:void cdev_init(struct cdev *cdev, const struct file_operations *fops)

参数 cdev 就是要初始化的 cdev 结构体变量,参数 fops 就是字符设备文件操作函数集合(标题5中内容)。

cdev_add函数用于向 Linux 系统添加字符设备(cdev 结构体变量)。

函数原型:int cdev_add(struct cdev *p, dev_t dev, unsigned count)

参数 p 指向要添加的字符设备(cdev 结构体变量),参数 dev 就是设备所使用的设备号,参
数 count 是要添加的设备数量。

:如果字符设备注册失败,则goto fail_cdev

在驱动出口函数static void __exit disled_exit(void)中,需要删除字符设备

cdev_del(&xxx.cdev);

7、自动创建设备节点

        设备树是采用树形结构来描述板子上的设备信息的文件,每个设备都是一个节点,叫做设备节点。

需要在入口函数static int __init xxx_init(void)中创建设备节点,

   xxx.class = class_create(THIS_MODULE,XXX_NAME);
   if(IS_ERR(xxx.class)){
      ret = PTR_ERR(xxx.class);
      goto fail_class;
   }
   xxx.device = device_create(xxx.class, NULL, xxx.devid, NULL, XXX_NAME);
      if(IS_ERR(xxx.device)){
      ret = PTR_ERR(xxx.device);
      goto fail_device;
   }

class_create函数用于创建类

函数原型:struct class *class_create (struct module *owner, const char *name)

参数 owner 一般为 THIS_MODULE,参数 name 是类名字。

返回值是个指向结构体 class 的指针,也就是创建的类。

:如果类创建失败,需要goto _class

在驱动出口函数static void __exit disled_exit(void)中,需要摧毁类

class_destroy(xxx.class);

device_create函数用于创建设备

函数原型:struct device *device_create(struct class *class,
                                                                struct device *parent,
                                                                dev_t devt,
                                                                void *drvdata,
                                                                const char *fmt, ...)

        参数 class 就是设备要创建哪个类;

        参数 parent 是父设备,一般为 NULL,也就是没有父设备;

        参数 devt 是设备号;

        参数 drvdata 是设备可能会使用的一些数据,一般为 NULL;

        参数 fmt 是设备名字,如果设置 fmt=xxx 的话,就会生成/dev/xxx这个设备文件;

        返回值就是创建好的设备。

:如果创建失败,则goto fail_device

在驱动出口函数static void __exit disled_exit(void)中,需要摧毁设备

device_destroy(xxx.class, xxx.devid);

8、创建失败goto和return

在入口函数static int __init xxx_init(void)最后中添加goto内容

   return 0;
   fail_rs:
   fail_findnd:
      device_destroy(xxx.class, xxx.devid);
   fail_device://创建设备失败
      class_destroy(xxx.class);
   fail_class://创建类失败
      cdev_del(&xxx.cdev);
   fail_cdev://字符设备失败
      unregister_chrdev_region(xxx.devid, XXX_CNT);
   fail_devid://设备号失败
      return ret;

        如果创建成功,则返回 0,否则返回ret为负数,其中各种goto顺序不能乱,比如如果申请字符设备失败,说明设备号已经申请成功,需要释放设备号;如果创建类失败,说明字符设备和设备号创建成功,需要释放。代码依次执行。

9、驱动出口函数编写

该函数中需要释放字符设备,设备号,类,设备

static void __exit xxx_exit(void){
   //删除设备
   cdev_del(&xxx.cdev);
   //释放设备号
   unregister_chrdev_region(xxx.devid, XXX_CNT);
   //摧毁设备
   device_destroy(xxx.class, xxx.devid);
   //摧毁类
   class_destroy(xxx.class);
}

函数原型:void cdev_del(struct cdev *p)
参数 p 就是要删除的字符设备。

函数原型是:void unregister_chrdev_region(dev_t from, unsigned count)
参数from:要释放的设备号。count:表示从 from 开始,要释放的设备号数量。

函数原型:void device_destroy(struct class *class, dev_t devt)

参数 class 是要删除的设备所处的类,参数 devt 是要删除的设备号。(在驱动出口函数中,需要先摧毁设备,再摧毁类)

函数原型:void class_destroy(struct class *cls);
参数 cls 就是要删除的类。

10、xxx.c整体代码

xxx.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>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_gpio.h>
#include <linux/semaphore.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>
#include <linux/timer.h>
#include <linux/of_irq.h>
#include <linux/irq.h>

#define XXX_CNT 1 //设备号个数
#define XXX_NAME "xxx" //设备号名字
//设备结构体
struct xxx_dev{
   dev_t devid;//设备号
   int major;//主设备号
   struct cdev cdev; //字符设备
   struct class *class;//类
   struct device *device;//设备
   struct device_node *nd;//设备树节点
   int    xxx_gpio; //GPIO编号
};
struct xxx_dev xxx;

static int xxx_open(struct inode *inode, struct file *filp){
      filp->private_data = &xxx;
      return 0;
}
static ssize_t xxx_write(struct file *filp, const char __user *buf, size_t count, loff_t *ppos){
   struct xxx_dev *dev = (struct xxx_dev*)filp->private_data;
   return 0;
}
static int xxx_release(struct inode *inode, struct file *filp){
      struct xxx_dev *dev = (struct xxx_dev*)filp->private_data;
      return 0;
}
static ssize_t xxx_read(struct file *filp, char __user *buf,size_t cnt, loff_t *offt)
 {
    struct xxx_dev *dev = (struct xxx_dev *)filp->private_data;
    return 0;
}
static const struct file_operations xxx_fops = {
   .owner = THIS_MODULE,
   .write = xxx_write,
   .open = xxx_open,
   .release = xxx_release,
   .read = xxx_read,
};
//驱动入口函数
static int __init xxx_init(void){
   int ret = 0;
   //注册设备号
   xxx.major = 0;
   if(xxx.major){ //给定设备号
      xxx.devid = MKDEV(xxx.major, 0);
      ret = register_chrdev_region(xxx.devid, XXX_CNT, XXX_NAME);
   }else{ // 申请设备号
      ret = alloc_chrdev_region(&xxx.devid, 0, XXX_CNT, XXX_NAME);
      xxx.major = MAJOR(xxx.devid);
   }
   if(ret<0){
      goto fail_devid;
   }
   //添加字符设备
   xxx.cdev.owner = THIS_MODULE;
   cdev_init(&xxx.cdev, &xxx_fops);
   ret = cdev_add(&xxx.cdev, xxx.devid, XXX_CNT);
   if(ret<0)
      goto fail_cdev;
   //自动创建设备节点
   xxx.class = class_create(THIS_MODULE,XXX_NAME);
   if(IS_ERR(xxx.class)){
      ret = PTR_ERR(xxx.class);
      goto fail_class;
   }
   xxx.device = device_create(xxx.class, NULL,xxx.devid, NULL, XXX_NAME);
      if(IS_ERR(xxx.device)){
      ret = PTR_ERR(xxx.device);
      goto fail_device;
   }
   
   return 0;
   fail_device:
      class_destroy(xxx.class);
   fail_class:
      cdev_del(&xxx.cdev);
   fail_cdev:
      unregister_chrdev_region(xxx.devid, XXX_CNT);
   fail_devid:
      return ret;
}
//驱动出口函数
static void __exit xxx_exit(void){
   //删除设备
   cdev_del(&xxx.cdev);
   //释放设备号
   unregister_chrdev_region(xxx.devid, XXX_CNT);
   //摧毁设备
   device_destroy(xxx.class, xxx.devid);
   //摧毁类
   class_destroy(xxx.class);
}
//注册驱动
module_init(xxx_init);
module_exit(xxx_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("FANG");

(3)应用程序编写

编写xxx.c文件对应的应用程序代码文件xxxApp.c,用于读取设备驱动文件

1、头文件

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include "linux/ioctl.h"

2、  main函数      

int main(int argc, char *argv[]){ 
return 0;
}

其中,argc是应用程序参数个数

          argv是具体的参数内容(字符串形式)

3、open函数

    int fd = 0,ret=0;
    char *filename;
    filename = argv[1];
    fd = open(filename, O_RDWR);
    if(fd < 0){
        printf("can not open file %s\n",filename);
        return -1;
    }

open()函数原型为 int open(const char *pathname, int flags);

参数 pathname:要打开的设备或者文件名,这里是第二个参数argv[1]
        flags:文件打开模式,以下三种模式必选其一:
                O_RDONLY         只读模式
                O_WRONLY        只写模式
                O_RDWR             读写模式

        如果文件打开成功的话返回文件的文件描述符(int),即fd,后续代码通过这个文件描述符完成对文件的操作。

4、read函数

char readbuf[100];//从驱动读取到的数据存到readbuf中
ret = read(fd, radbuf, 10);
if(ret < 0){
    printf("read file %s failed!\n", filename);
    return -1;
}

read() 函数原型:ssize_t read(int fd, void *buf, size_t count)

参数  fd:要读取的文件描述符,读取文件之前要先用 open 函数打开文件,open 函数打开文件成
功以后会得到文件描述符。
        buf:数据读取到此 buf 中。
        count:要读取的数据长度,也就是字节数。
        返回值:读取成功的话返回读取到的字节数;如果返回 0 表示读取到了文件末尾;如果返
回负值,表示读取失败。

5、write函数

char writebuf[100];//应用程序向驱动写的数据
ret = write(fd,writebuf,sizeof(writebuf));
    if(ret<0){
        printf("write file %s failed!\n" ,filename);
        close(fd);
        return -1;
    }

write()函数原型:ssize_t write(int fd, const void *buf, size_t count);
参数  fd:要进行写操作的文件描述符,写文件之前要先用 open 函数打开文件,open 函数打开文
件成功以后会得到文件描述符。
         buf:要写入的数据。
         count:要写入的数据长度,也就是字节数。
         返回值:写入成功的话返回写入的字节数;如果返回 0 表示没有写入任何数据;如果返回负值,表示写入失败。

6、close函数

close(fd);

 close() 函数原型:int close(int fd);
参数: fd:要关闭的文件描述符。
            返回值:0 表示关闭成功,负值表示关闭失败。

(4)程序测试

1、驱动程序编译

驱动程序文件xxx.c目录下终端运行make编译,生成xxx.ko文件

make

已知:开发版根文件系统通过NFS挂载到ubuntu目录下。

将编译得到的xxx.ko文件拷贝到ubuntu的挂载目录nfs下的lib/modules/4.1.15/中

cp xxx.ko /home/f3161/linux/nfs/lib/modules/4.1.15/ -f

2、应用程序编译

对应用程序xxxApp.c进行编译,生成xxxApp文件

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

将编译得到的xxxApp文件拷贝到ubuntu的挂载目录nfs下的lib/modules/4.1.15/中

cp xxxApp /home/f3161/linux/nfs/lib/modules/4.1.15/ -f

3、开发板终端运行

在windows电脑下使用putty软件与开发板串口通信,作为串口终端使用。

第一次加载驱动的时候需要运行命令depmod

depmod

运行modprobe xxx.ko命令加载驱动

modprobe xxx.ko

输入“lsmod”命令即可查看当前系统中存在的模块 

lsmod

输入命令查看当前系统中的设备

cat /proc/devices

加载应用程序,其中后面可以添加参数

./xxxApp /dev/xxx

卸载驱动

rmmod xxx.ko

  • 15
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Linux字符设备驱动实验是指在Linux操作系统中编写和测试字符设备驱动程序的过程。字符设备驱动程序负责与字符设备进行交互,包括输入输出数据、控制设备和处理设备的状态等。 在进行Linux字符设备驱动实验之前,首先需要了解字符设备字符设备驱动的基本概念及其工作原理。字符设备是指以字符为单位进行输入输出的设备,如串口、打印机等。字符设备驱动是指将操作系统与字符设备进行交互的程序。 在实验中,我们通常需要编写一个字符设备驱动程序,包括初始化设备、读写数据、控制设备等功能。首先,我们需要定义字符设备驱动的数据结构,包括设备号、驱动程序打开、关闭等函数的实现。然后,我们需要实现字符设备驱动的读写函数来实现数据的输入输出。最后,我们可以进行一些附加功能的实现,如控制设备的状态、处理中断等。 在实验过程中,我们需要使用Linux内核提供的字符设备接口来进行字符设备驱动的编写和测试。可以使用一些工具和命令来加载和测试字符设备驱动程序,如insmod、rmmod等。通过这些工具和命令,我们可以加载和卸载字符设备驱动程序,并在用户空间进行数据的读写操作,来测试字符设备驱动的功能和性能。 Linux字符设备驱动实验可以帮助我们深入了解字符设备字符设备驱动的工作原理,并学习Linux内核的开发和调试技术。通过实验,我们可以更好地理解操作系统和驱动程序之间的关系,提高我们在Linux系统开发和嵌入式系统开发中的能力。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值