Linux驱动(一)之最简单的驱动程序

1、前言

为什么要有驱动?为了防止像我等小菜程序员写应用程序的时候权限过高直接去操作底层设备,给设备造成不可挽回的损失,所以要过度一下,让大牛们将底层封装好,应用开发工程师只需要通过特定的接口来完成特定的功能就可以了。

2、应用

通常情况下,应用开发只需要open一个/dev目录下的文件,然后执行write、read等操作即可。那具体是什么原理呢,以应用程序调用open函数为例,改函数为c库中的函数,实际上会产生中断,将权限交给内核,而内核则会根据相关参数去调用具体驱动程序实现的open函数。

2.1 驱动程序分析

接下来我们来分析一个最简单的字符驱动程序:

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/delay.h>
#include <asm/uaccess.h>
#include <asm/irq.h>
#include <asm/io.h>
#include <asm/arch/regs-gpio.h>
#include <asm/hardware.h>

/*
1. 构造file_operations
2. 填充file_operations
3. 注册file_operations
4. 编写入口和出口
*/

unsigned int major = 146;

int my_dev_open(struct inode * inode, struct file * file)
{
    printk("my_dev_open\n");
    return 0;
}

ssize_t my_dev_write(struct file *file, const char __user *buffer, size_t count, loff_t *ppos)
{
    printk("my_dev_write\n");
    return 0;
}

static const struct file_operations fops = {
	.owner		= THIS_MODULE,
	.open		= my_dev_open,
	.write		= my_dev_write
};

int my_dev_init(void)
{
    printk("my_dev_init\n");
    register_chrdev(major, "mydev", &fops);
    return 0;
}

void my_dev_exit(void)
{
    printk("my_dev_exit\n");
    unregister_chrdev(major, "mydev");
    return;
}

module_init(my_dev_init);
module_exit(my_dev_exit);
MODULE_LICENSE("GPL");

my_dev_init和my_dev_exit函数是我们加载和卸载驱动内核帮我们调用的接口,这两个接口需要分别用module_init和module_exit函数进行修饰。

从头看,我们定义了my_dev_open和my_dev_write函数,并将这两个函数赋值给了file_operations结构体,然后通过register_chrdev函数将该结构体进行注册。注册到底是干啥?看注册函数的三个参数,major, "mydev", &fops,第一个参数是主设备号,什么是设备号呢,就是内核用来区分设备的一个数字,比如鼠标是1,键盘是2,我们在程序中将其写死为了146(没有什么特殊的,随便写的),第二个参数"mydev"是一个名字,第三个参数则是填充了我们自己写的open和write函数的file_operations结构体。整个过程的意思可以这样理解,就是内核维护了一个设备结构体数组,注册函数实现了向数组中添加了一项信息,添加的结构体数组的下标就是主设备号,结构体数组的内容有名字、file_operations等。

多说一下主设备号,执行cat /proc/devices可以查看当前设备,写死的设备号不要与当前设备重复

将上述代码命令为mydev.c,使用Makefile编译成ko文件,Makefile如下,注意KERN_DIR 要改为你虚拟机或者开发板内核文件目录。

KERN_DIR = /home/02-Kernel/linux-2.6.22.6

all:
	make -C $(KERN_DIR) M=`pwd` modules 

clean:
	make -C $(KERN_DIR) M=`pwd` modules clean
	rm -rf modules.order

obj-m	+= mydev.o

2.2 驱动安装

将ko文件拷贝至目标板中,然后执行insmod mydev.ko命令进行安装,可以看到驱动程序中my_dev_init函数添加的打印信息,即my_dev_init接口在执行isnmod命令时得到调用,再次执行 cat /proc/devices可以看到驱动程序完成了注册。

接下来使用mknod /dec/mydev c 146 0命令创建一个设备节点,该命令的含义:

mknod         // 创建设备节点
/dec/mydev    // 节点名称 
c             // c表示字符设备节点
146           // 主设备号
0             // 次设备号

执行完成后可以ls -l /dev查看创建的设备节点:

2.3 应用程序分析

编写测试的应用程序代码如下,我们在应用程序中打开了之前创建的设备节点,然后调用write接口向该节点写入了一个数字:

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>


int main(int argc, char **argv)
{
    int fd;
    char val = 1;

    fd = open("/dev/mydev", O_RDWR);
    if (fd < 0)
    {
        printf("error, can't open /dev/mydev\n");
        return 0;
    }
    
    write(fd, &val, 1);   
    
    return 0;
}

将程序保存为main.c,直接使用gcc工具进行编译即可:arm-linux-gcc -o main main.c,注意要改为使用自己虚拟机或者开发板的编译工具。

直接运行编译好的程序,可以看到应用程序执行的open和write函数最终调用了我们编写的驱动程序中的open和write接口:

3、总结

通过一个简单的字符驱动程序讲解了一下linux驱动的框架和调用流程,后续再讲解自动创建字符设备节点等优化内容。

猜你喜欢

转载自blog.csdn.net/cesheng3410/article/details/128548632