关于驱动,这篇文章讲的很详细,手动收藏一下。
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驱动开发篇。