STM32mp157--字符驱动设备开发记录(流程初次学习)

关于驱动,这篇文章讲的很详细,手动收藏一下。

linux设备的实现与理解http://t.csdn.cn/Uo0SN

分为用户空间(用户态)和内核空间(内核态)

Linux操作系统内核和驱动程序运行再内核空间,应用程序运行在用户空间。

一图了解大概,相信有编程基础的人,不会很难理解这个。

 (此图来自正点原子教程)

其实在学习裸机开发的时候,我们就发现驱动的开发就是初始化相应的外设寄存器,那么对比一下,linux驱动开发中肯定也是这样。但是在驱动开发中,重点是学习其驱动的框架

1 驱动的模块的加载和卸载

Linux驱动有两种运行方式:【将驱动编译进内核】   和  【将驱动编译成模块】。

这里使用的是将驱动编译成模块。

新建一个文件夹,并创建一个c文件。

(我是用vscode远程登陆的,大家也可以去配置一下,这样方便一点)

vscode打开我们的内核源代码,搜索关键字file_operations,它是我们内核驱动操作函数集合。打开几个文件,将其中的头文件复制到我们创建的c文件中,当然也可以进入头文件中查看有哪些头文件,但是我们秉着模仿学习的态度,借鉴一下源码时怎么写的。

 模块的加载和卸载有两种操作。

module_init(xxx_init); //注册模块加载函数
module_exit(xxx_exit); //注册模块卸载函数

如何使用呢?

同样是参考别人的代码,我们在源码中搜索module_init,找到对应的文件。

 既然找到了,那么我们就来看一下里面是怎么写的吧。

这个函数有返回值。

 这个没有。

那么我们也学着写一下。

 这样,一个简单的驱动加载和卸载就写完了。

但是,这个驱动文件只是我们另外开的,并不是在内核里面,这样做也是为了保护好内核,怎么解决呢?

当然是编写Makefile文件了!

1 KERNELDIR := /home/dada/linux/atk-mp1/linux/mylinux/linux-5.4.31
2 CURRENT_PATH := $(shell pwd)
3 obj-m := chrdevbase.o
4
5 build: kernel_modules
6
7 kernel_modules:
8	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
9 clean:
10	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean

第一行是指定内核源码目录,也就是我们开发板所使用的Linux内核源码目录,是绝对路径,是大家自己根据自己的情况来写的!

第二行是当前路径。

第七行是具体的编译命令,注意第八行前面不是空格,是TAB。不然你就会报错,说找不到文件。

在当前目录下,输入make命令,编译完之后就会出现一个.ko的文件。

将这个文件复制进我们nfs目录下的/lib/modules/5.4.31中,然后启动我们的开发板。

使用modprobe和rmmode来加载和卸载驱动,这是因为modprobe比insmod更加智能一点。

然后进行测试,这里参考一下教程就行。

2 字符的注册与注销

同样是观察源码里面的编写方式。

 

 同时我们打开注册函数的定义。

 分别一一对应,发现第一个是主设备号,第二个是名字,第三个是 结构体 file_operations 类型指针,指向设备的操作函数集合变量。 

对于第三个,如果不知道的话我们可以查看一下它的定义。

 根据以上的观察,我们的注册函数可以模仿着写了。

定义一下主设备号和名字,采用宏定义。

照着源码复制一下第三个的代码,并且改为自己的名字,这里我们放空,后面再来研究。

 定义好这三个变量之后,写我们的注册函数,主要还是参考前面的,编译有问题我们再改。

 同样的,注销函数也是如此,注销函数比较简单。

 ok,到这里基本上就写完了,下面编译一下,上开发板测试!

加载驱动模块。

用cat命令查看一下我们的设备。

发现里面已经有了!

下面重点看注册函数的第三个。

观察一下,

转到定义看看。

是一个写好的函数,那我们完全可以拿过来模仿一下,先写个简单的打印。

注意,把名字改成自己的哦。

之后也一样。

好,这样基本上就都改好了。

编译一下看看有没有问题。

竟然没问题!

复制进我们的根目录!

接下来就是编写应用程序来测试一下了。

3 测试

创建应用程序APP文件。

 先放头文件。


 这里有个小问题,为什么是双引号呢?双引号跟尖角有什么区别呢?

我找了一下,这篇文章说的很好。

双引号代表查找当前目录,而尖角则会按照编译器设置好的路径去查找。

双引号和尖角号区别http://t.csdn.cn/9cSXL还有就是这些头文件从那里来呢?

我们在编写程序的时候,要用到函数,这些函数所需的头文件就是我们需要加的。

man命令来查看。

man 2 open

 就可以查看到所需要的头文件了。


应用测试程序如下。

#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"

/*
APP运行命令:./chrdevbaseAPP filename
*/
int main(int argc, char *argv[])
{
    int fd, retvalue;                // 定义返回值
    char *filename;                  // 要打开的文件

    if (argc != 2)                   // 判断输入的参数是否是两个
    {                               
        printf("error usage!\r\n");
        return -1;
    }
    filename = argv[1];              // 第二个参数,既要打开的文件
    /*
    打开文件
    */
    fd = open(filename, O_RDWR);     // 读写的方式打开文件

    if (fd < 0)
    {

        printf("Can't open file %s\r\n", filename);
        return -1;
    }

    retvalue = close(fd);
    if (retvalue < 0)
    {
        printf("Can't close file%s\r\n", filename);
        return -1;
    }
}

这里使用交叉编译器编译。

将生成的可执行文件拷贝到开发板根目录下面。

启动开发板!

由于目前是学习阶段,所以设备节点没有自动生成,需要我们手动创建。

输入以下命令。

mknod /dev/chrdevbase c 200 0

 查看是否创建节点成功。

可以看到,已经成功生成设备节点。

接下来进行测试,在测试之前要确保我们的驱动已经加载进来了哦。

 成功打印!

测试成功!!!

很棒!

4 实验

驱动代码

#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
// 此类头文件可以在内核头文件中找到
// 如何找到这些头文件?
// 在内核驱动中搜索字符驱动设备file_operations,找到相关的代码,根据它所选取的头文件来选取,当然也可以凭借经验

#define CHRDEVBASE_MAJOR 200
#define CHRDEVBASE_NAME "chrdevbase"

static char readbuf[100];//读缓冲区
static char writebuf[100];//写缓冲区

static char kerneldata[] = {"kernel data!"};//应用从内核读取到的数据

static int chrdevbase_open(struct inode *inode, struct file *filp)
{
    int ret = 0;
    //printk("chrdevbase_open\r\n");
    return ret;
}

static int chrdevbase_release(struct inode *inode, struct file *filp)
{
    int ret = 0;
    //printk("chrdevbase_release\r\n");
    return ret;
}

/*
* @description : 从设备读取数据
* @param - filp : 要打开的设备文件(文件描述符)
* @param - buf : 返回给用户空间的数据缓冲区
* @param - cnt : 要读取的数据长度
* @param - offt : 相对于文件首地址的偏移
* @return : 读取的字节数,如果为负值,表示读取失败
*/
static ssize_t chrdevbase_read(struct file *filp, char __user *buf, size_t cnt,
                         loff_t *offt)
{
    int ret = 0;

    memcpy(readbuf, kerneldata, sizeof(kerneldata));//将要读取的内容复制到读取缓冲区
    ret = copy_to_user(buf, readbuf, cnt);          //将内核数据返回给用户空间的缓冲区
    if (ret == 0)
    {
        printk("kernel sentdata ok!\r\n");
    }
    else
    {
        printk("kernel sentdata failed!\r\n");
    }
    return 0;
}
/*
* @description : 向设备写数据
* @param - filp : 设备文件,表示打开的文件描述符
* @param - buf : 要写给设备写入的数据
* @param - cnt : 要写入的数据长度
* @param - offt : 相对于文件首地址的偏移
* @return : 写入的字节数,如果为负值,表示写入失败
*/
static ssize_t chrdevbase_write(struct file *filp, const char __user *buf,
                          size_t cnt, loff_t *offt)
{
    int ret = 0;
    ret = copy_from_user(writebuf,buf,cnt);  
     if (ret == 0)
    {
        printk("kernel receviedata ok!\r\n");
    }
    else
    {
        printk("kernel receviedata failed!\r\n");
    }
    return 0;

}

const struct file_operations chrdevbase_fops = {

    .owner = THIS_MODULE,
    .open = chrdevbase_open,
    .release = chrdevbase_release,
    .write = chrdevbase_write,
    .read = chrdevbase_read,

};

// 入口函数
static int __init chrdevbase_init(void)
{
    int ret = 0;
    printk("chrdevbase_init\r\n");

    // 注册字符设备
    ret = register_chrdev(CHRDEVBASE_MAJOR, "chrdevbase", &chrdevbase_fops);
    if (ret < 0)
    {
        printk("chrdevbase driver register failed!\r\n");
    }
    return ret;
}
// 出口函数
static void __exit chrdevbase_fini(void)
{
    printk("chrdevbase_exit\r\n");

    // 注销字符设备
    unregister_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME);
}

/*驱动的注册与卸载*/
module_init(chrdevbase_init); // 入口函数
module_exit(chrdevbase_fini); // 出口函数

// 添加作者信息
MODULE_LICENSE("GPL");
MODULE_AUTHOR("dada");
MODULE_INFO(intree, "Y");

应用测试APP代码

#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"

static char usrdata[] = {"usr data"};

/*
APP运行命令:./chrdevbaseAPP filename <1>/<2> 如果是1表示读数据,如果是2表示写数据
*/
int main(int argc, char *argv[])
{
    int fd, retvalue;                // 定义返回值
    char *filename;                  // 要打开的文件
    char readbuf[100],writebuf[100];

    if (argc != 3)                   // 判断输入的参数是否是两个
    {                               
        printf("error usage!\r\n");
        return -1;
    }
    filename = argv[1];              // 第二个参数,既要打开的文件
    /*
    打开文件
    */
    fd = open(filename, O_RDWR);     // 读写的方式打开文件

    if (fd < 0)
    {

        printf("Can't open file %s\r\n", filename);
        return -1;
    }

    if(atoi(argv[2]) == 1)           //从驱动文件读取数据
    {
        retvalue = read(fd,readbuf,50);
       if (retvalue < 0)
       {
        printf("read file failed! %s\r\n", filename);
        } else{
        printf("usr read data! %s\r\n", readbuf);
        }
    }

    if(atoi(argv[2]) == 2)           //向驱动文件写数据
    {
        memcpy(writebuf,usrdata,sizeof(usrdata));
        retvalue = write(fd,writebuf,50);
       if (retvalue < 0)
       {
        printf("write file failed! %s\r\n", filename);
       } 

    }

    /*
    关闭文件
    */
    retvalue = close(fd);
    if (retvalue < 0)
    {
        printf("Can't close file%s\r\n", filename);
        return -1;
    }
}

最终测试结果

满足预期要求。

成功!

至此,字符设备驱动基本的框架就已经学完了!


 以上内容参考自正点原子STM32mp157驱动开发篇。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值