嵌入式设计实验一:简单字符设备驱动


一、实验目的

  • 掌握简单字符设备驱动程序编写方法。
  • 编写应用程序对驱动程序进行测试,学习应用程序与驱动程序之间的调用过程。

二、实验环境

ubuntu 12.04 内核3.2.14

三、实验内容及实验原理

写一个简单的字符设备驱动程序,要求:

  • 定义一个全局结构指针,初始值为NULL,该数据结构中包含一个大小为1024的buffer和一个count整形变量
  • 在open中对该全局结构进行NULL判断,为NULL则为其分配内存,并将buffer初始化为0,将count自加
  • 在release中如果count为0,则释放,否则进行count自减
  • 在read里面对该buffer进行读取
  • 在write里面对该buffer进行赋值(任意赋值即可)
    写测试程序进行测试

四、实验结果及其分析

1.编译模块(设备驱动程序)

(1)创建模块文件xxx.c

gedit rwbuf.c
区分版本:这里不同,低版本内核2.6以下的是ioctl,高版本内核2.6及其以上的是unlocked_ioctl。
在这里插入图片描述

// 模块
#include <linux/module.h>
// 内核
#include <linux/kernel.h>
// struct file_operations
#include <linux/fs.h>
// copy_to_user() & copy_from_user
#include <linux/uaccess.h>
// <string.h>不行,用于memset和strcpy
#include <linux/string.h>
// kmalloc和kfree
#include <linux/slab.h>

// rwbuf.c,  driver for virtual char-device
#define RW_CLEAR 0x123
// 设备名
#define DEVICE_NAME "rwbuf"

// buffer数组的最大长度
#define RWBUF_MAX_SIZE 1024
// 设备中存储信息的全局结构体指针
typedef struct Data
{
    // 用于打开和释放的验证,0表示未使用,1表示已使用
    int count;
    // 表示字符串
    char buffer[RWBUF_MAX_SIZE];
} Data, *DataPtr;

DataPtr myDataPtr = NULL;

// 设备的打开:只返回0表示成功无法表示报错
int rwbuf_open(struct inode *inode, struct file *file)
{
    // 如果未被使用
    if (myDataPtr == NULL)
    {
        // 分配堆
        myDataPtr = (DataPtr)kmalloc(sizeof(Data), GFP_KERNEL);
        // 字符数组清零
        memset(myDataPtr->buffer, 0, sizeof(myDataPtr->buffer));
        // 初始化为1,表示有人在使用了
        myDataPtr->count = 1;
        printk("[rwbuf_open-kmalloc-success]myDataPtr->count = %d.\n", myDataPtr->count);
        return 0;
    }
    // 已被使用
    else
    {
        // 增加一个用户
        myDataPtr->count += 1;
        printk("[rwbuf_open-countup]myDataPtr->count = %d.\n", myDataPtr->count);
        return 0;
    }
}

// 设备的关闭:返回-1表示错误;返回0表示成功
int rwbuf_release(struct inode *inode, struct file *file)
{
    // 当未打开时,报错
    if (myDataPtr == NULL)
    {
        printk("[rwbuf_release-error]\n");
        return -1;
    }
    // 当有用户使用时,则自减
    else
    {
        myDataPtr->count -= 1;
        printk("[rwbuf_release-countdown]myDataPtr->count = %d.\n", myDataPtr->count);

        // 当没用用户使用时,释放
        if (myDataPtr->count == 0)
        {
            // 释放堆区
            kfree(myDataPtr);
            // 还要让指针回归NULL,不要野指针
            myDataPtr = NULL;
            printk("[rwbuf_release-free-success]\n");
            return 0;
        }
        return 0;
    }
}

// 设备的读操作:返回-1表示错误;返回(0, RWBUF_MAX_SIZE]表示成功
ssize_t rwbuf_read(struct file *file, char *buf, size_t count, loff_t *f_pos)
{
    // 判断读取的长度是否有效
    if (strlen(myDataPtr->buffer) > 0 && strlen(myDataPtr->buffer) <= RWBUF_MAX_SIZE)
    {
        // 从内核空间rwbuf复制到用户空间buf
        copy_to_user(buf, myDataPtr->buffer, count);
        printk("[rwbuf_read-success]the size of myDataPtr->buffer after read = %d\n", strlen(myDataPtr->buffer));
        return count;
    }
    else
    {
        printk("[rwbuf_read-error] strlen(myDataPtr->buffer) = %d\n", strlen(myDataPtr->buffer));
        return -1;
    }
}

// 设备的写操作接:返回-1表示错误;返回(0, RWBUF_MAX_SIZE]表示成功
ssize_t rwbuf_write(struct file *file, const char *buf, size_t count, loff_t *f_pos)
{
    // 判断写入的长度是否有效
    if (count > 0 && count <= RWBUF_MAX_SIZE)
    {
        // 从用户空间buf复制到内核空间rwbuf
        copy_from_user(myDataPtr->buffer, buf, count);
        printk("[rwbuf_write-success] the size of myDataPtr->buffer after write = %d\n", strlen(myDataPtr->buffer));
        return count;
    }
    else
    {
        printk("[rwbuf_write-error] length of string = %d\n", count);
        return -1;
    }
}

// 设备的ioctl:返回-1表示错误;返回0表示成功
long rwbuf_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
    printk("[RW_CLEAR:%x],[cmd:%x]\n", RW_CLEAR, cmd);
    // 命令
    if (cmd == RW_CLEAR)
    {
        // 表示清零
        memset(myDataPtr->buffer, 0, sizeof(myDataPtr->buffer));
        printk("[rwbuf_ioctl-success] the size of myDataPtr->buffer after ioctl = %d\n", strlen(myDataPtr->buffer));
        return 0;
    }
    // 无此命令时
    else
    {
        printk("[rwbuf_ioctl-error] the size of myDataPtr->buffer after ioctl = %d\n", strlen(myDataPtr->buffer));
        return -1;
    }
}

// rwbuf_fops要在rwf_buf_init()前面声明,因为register_chrdev()函数要使用到
static struct file_operations rwbuf_fops =
    {
        open : rwbuf_open,
        release : rwbuf_release,
        read : rwbuf_read,
        write : rwbuf_write,
        unlocked_ioctl : rwbuf_ioctl
    };

// module_init()内的初始化函数:返回-1表示错误;返回0表示成功
static int __init rwbuf_init()
{
    // 表示注册成功与否:-1表示失败,0表示成功(同register_chrdev返回值)。初始化为-1
    int ret = -1;
    /*
    参数1:设备的种类,即主设备号
    参数2:设备的名称
    参数3:和VFS对接的接口,即上面的结构体变量
    */
    ret = register_chrdev(60, DEVICE_NAME, &rwbuf_fops);
    // 注册失败
    if (ret == -1)
    {
        printk("[rwbuf_init-register-failed]\n");
    }
    // 注册成功
    else
    {
        printk("[rwbuf_init-register-success]\n");
    }
    // 返回ret(同register_chrdev返回值)
    return ret;
}

// module_exit()内的退出函数。
static void __exit rwbuf_exit()
{
    unregister_chrdev(60, DEVICE_NAME);
    printk("[rwbuf_exit-unregister-success]\n");
}

// 内核模块入口,相当于main()函数,完成模块初始化
module_init(rwbuf_init);
// 卸载时调用的函数入口,完成模块卸载
module_exit(rwbuf_exit);
// GPL协议证书
MODULE_LICENSE("GPL");

意思:

结构体static struct file_operations rwbuf_fops是定义函数指针
比如将open指向rwbuf_open,我们调用的时候就打open就是调用rwbuf_open。其中ioctl特殊,调用的时候用ioctl才对,用unlocked_ioctl反而不对,而且定义成旧版本的ioctl,对高版本内核更不对(编译不报错,但读进入的数cmd就成一个莫名其妙的8位十六进制数)。
结构体rwbuf_fopsrwf_buf_init_module()中被用到,生效对结构体变量的操作。
if(register_chrdev(60, DEVICE_NAME, &rwbuf_fops))

(2)Makefile

gedit Makefile
内容:

obj-m := rwbuf.o
KERNELDIR := /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
modules:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
clean:
	rm -rf *.o *~ core .depend .*.cmd *.ko *.mod.c .tmp_versions modules.order  Module.symvers

(3)编译

sudo make
在这里插入图片描述

2.创建设备文件(设备进入点)

(1)创建

sudo mknod /dev/rwbuf c 60 0

意思:
在/dev目录下创建一个名为rwbuf的设备文件
设备文件的类型是c(字符型设备)
主设备号是60
次设备号是0

(2)赋权

sudo chmod 777 /dev/rwbuf
更改设备的权限。没有读写权限的话,就会读出一堆无意义的乱码。

3.插入内核模块(加载设备驱动程序)

先清理一下缓存,不然一会就可能输出一大堆多余东西,影响到我们想要看到的输出东西
sudo dmesg -c
插入内核模块(加载设备驱动程序)
sudo insmod rwbuf.ko
查看是否成功
dmesg
在这里插入图片描述

4.测试用户应用程序(调用驱动程序)

(1)读写

gedit writeAndRead.c
#include <stdio.h>
// 为了open()中的O_RDWR
#include <fcntl.h>

// 定义设备进入点(设备名)
#define DEVICE_NAME "/dev/rwbuf"

int main()
{
    // 声明文件描述符
    int fd;
    // 记录关闭设备的返回值
    int ret;
    // 写入到设备的内容
    char buff_write[10] = "volume";
    // 读取到设备的结果
    char buff_read[10] = "";

    // 调用打开设备函数。注意O_RDWR是字母O
    fd = open(DEVICE_NAME, O_RDWR);
    // 打开失败
    if (fd == -1)
    {
        printf("[open-error]\n");
        return 0;
    }
    else
    {
        printf("[open-success]\n");
    }

    // 调用驱动程序的写操作接口函数
    if (write(fd, buff_write, 5) == -1)
    {
        printf("[write-error]\n");
        return 0;
    }
    // 写入成功
    else
    {
        printf("[write-success] %s\n", buff_write);
    }

    // 调用驱动程序的读操作接口函数
    if (read(fd, buff_read, 5) == -1)
    {
        printf("[read-error]\n");
        return 0;
    }
    // 读取成功
    else
    {
        buff_read[5] = '\0';
        printf("[read-success] %s\n", buff_read);
    }

    // 设备关闭
    close(fd);
    printf("[close-success]\n");

    return 0;
}

运行:
gcc -o writeAndRead.out -g writeAndRead.c
./writeAndRead.out
在这里插入图片描述
在这里插入图片描述

(2)清除程序

gedit ioctl.c

#include <stdio.h>
// 为了open()中的O_RDWR
#include <fcntl.h>

// 定义设备进入点(设备名)
#define DEVICE_NAME "/dev/rwbuf"

int main()
{
    // 声明文件描述符
    int fd;
    // 记录关闭设备的返回值
    int ret;
    // 写入到设备的内容
    char buff_write[10] = "volume";
    // 读取到设备的结果
    char buff_read[10] = "";
    // 控制命令
    unsigned int cmd = 0x123;

    // 调用打开设备函数。注意O_RDWR是字母O
    fd = open(DEVICE_NAME, O_RDWR);
    // 打开失败
    if (fd == -1)
    {
        printf("[open-error]\n");
        return 0;
    }
    else
    {
        printf("[open-success]\n");
    }

    // 调用驱动程序的写操作接口函数
    if (write(fd, buff_write, 5) == -1)
    {
        printf("[write-error]\n");
        return 0;
    }
    // 写入成功
    else
    {
        printf("[write-success] %s\n", buff_write);
    }

    // 调用驱动程序的写操作接口函数
    if (ioctl(fd, cmd) == -1)
    {
        printf("[ioctl-error]\n");
        return 0;
    }
    else
    {
        printf("[ioctl-success]\n");
    }

    // 调用驱动程序的读操作接口函数
    if (read(fd, buff_read, 5) == -1)
    {
        printf("[read-error]\n");
        return 0;
    }
    // 读取成功
    else
    {
        buff_read[5] = '\0';
        printf("[read-success] %s\n", buff_read);
    }

    // 设备关闭
    close(fd);
    printf("[close-success]\n");

    return 0;
}

运行:
gcc -o ioctl.out -g ioctl.c
./ioctl.out

在这里插入图片描述
在这里插入图片描述

5.野指针问题

这个属于个人书写代码的规范不好,
在这里插入图片描述

如果kfree(myDataPrt)后没有将myDataPrt=NULL;就会再次打开后判读此条件失败,从而count是乱值。

五、心得体会与建议

感觉还是遇到了不少的难处,并没有看着那么简单

1.隐式声明函数‘copy_from_user’

2.总结copy_from_user()和copy_to_user()

编译内核函数copy_from_user()和copy_to_user()

3.\xffffffe7\xffffffa4\xffffffba\问题

Linux驱动开发:\xffffffe7\xffffffa4\xffffffba\问题

4.调整代码结构

(1)问题

驱动程序

// buffer数组的最大长度
#define RWBUF_MAX_SIZE 1024
// 设备中存储信息的全局结构体指针
typedef struct Data
{
    // 用于打开和释放的验证,0表示未使用,1表示已使用
    int count;
    // 表示字符串
    char buffer[RWBUF_MAX_SIZE];
} Data, *DataPtr;

DataPtr myDataPtr = NULL;

// 设备的关闭:返回-1表示错误;返回0表示成功
int rwbuf_release(struct inode *inode, struct file *file)
{
    // 当未打开时,报错
    if (myDataPtr == NULL)
    {
        printk("[rwbuf_release-error]\n");
        return -1;
    }
    // 当没用用户使用时,释放
    if (myDataPtr->count == 0)
    {
        kfree(myDataPtr);
        printk("[rwbuf_release-free-success]myDataPtr->count = %d.\n", myDataPtr->count);
        return 0;
    }
    // 当有用户使用时,则自减
    else
    {
        myDataPtr->count -= 1;
        printk("[rwbuf_release-countdown]myDataPtr->count = %d.\n", myDataPtr->count);
        return 0;
    }
}

在这里插入图片描述

用户测试程序

// 设备关闭
close(fd);
printf("[close-success]\n");

close(fd);
printf("[close-release-success]\n");

在这里插入图片描述

可以看到没出现对应printk("[rwbuf_release-free-success]myDataPtr->count = %d.\n", myDataPtr->count);的结果,即没有释放成功。

(2)解决

既然close(fd)只能生效一次,那么移动到里面就行了。

// 设备的关闭:返回-1表示错误;返回0表示成功
int rwbuf_release(struct inode *inode, struct file *file)
{
    // 当未打开时,报错
    if (myDataPtr == NULL)
    {
        printk("[rwbuf_release-error]\n");
        return -1;
    }
    // 当有用户使用时,则自减
    else
    {
        myDataPtr->count -= 1;
        printk("[rwbuf_release-countdown]myDataPtr->count = %d.\n", myDataPtr->count);

        // 当没用用户使用时,释放
        if (myDataPtr->count == 0)
        {
            kfree(myDataPtr);
            printk("[rwbuf_release-free-success]\n");
            return 0;
        }
        return 0;
    }
}

在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值