前言,没事就碎碎念
以前跟着正点原子的文档做过一两个简单驱动程序实验,但是总感觉思路不够清晰,后面看韦东山的视频,发现二者结合起来刚好合适,其中韦东山视频理论和框架讲的清楚,正点原子的更像是他们开发板的使用手册。
一开始学习驱动,我感觉比较合适的路线是先简单过一遍裸机,跟着正点原子教程体验一遍uboot移植,内核移植,根文件系统制作,对整个嵌入式系统从硬件到软件有一个较为系统的理解,这个过程需要有一定的C语言、C++,操作系统、计算机网络、编译原理,计算机体系结构或者计算机组成原理基础。再从应用编程和驱动编程开始,我认为应用和驱动是相辅相成的,可以同时进行。开始学习的时候可以先按照框架进行编程,先学会使用,再往下深究,自顶向下和自底向上相结合,根据学习的反馈进行调节,学过自动化的都知道反馈吧。说白了,嵌入式其实就是我们的生产工作,学会使用这个工作,再赋能我们的创造力,才能成为一个好的产品。如果有创造力却不会工具,那想法没法落地,有工具但是没有专业知识赋能,那只能是拧螺丝,所以二者相辅相成,不可分割。目前,我也该到了学习工具的时候了,以前学的那些什么信号处理、自控原理、人工智能,都是顶层的,那怎么落地呢?需要补充底层知识了。
言归正传,该如何编写第一个hello驱动程序
废话不多说,我们先看代码
点击查看代码
//hello_driver.c
#include "asm/uaccess.h"
#include "linux/err.h"
#include "linux/export.h"
#include "linux/kdev_t.h"
#include "linux/printk.h"
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/miscdevice.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/mutex.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/stat.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/tty.h>
#include <linux/kmod.h>
#include <linux/gfp.h>
#include <linux/printk.h>
// (1)确定主设备号,也可以让内核分配
static int major = 0;
static char kernel_buf[1024]; //内核缓存空间
static struct class *hello_class;
#define MIN(a, b) (a < b ? a : b)
// (3)实现对应的 drv_open/drv_read/drv_write 等函数,填入 file_operations 结构体
//为了不用声明,将函数定义放到前面,但是逻辑顺序应该是在后面
static ssize_t hello_drv_read (struct file * file, char __user * buf, size_t size, loff_t * offset)
{
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
copy_to_user(buf, kernel_buf, MIN(1024, size));
return MIN(1024, size);
}
static ssize_t hello_drv_write (struct file * file, const char __user * buf, size_t size, loff_t * offset)
{
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
copy_from_user(kernel_buf, buf, MIN(1024, size));
return MIN(1024, size);
}
static int hello_drv_open (struct inode * node, struct file * file)
{
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
return 0;
}
static int hello_drv_close (struct inode * node, struct file *file)
{
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
return 0;
}
// (2)定义自己的 file_operations
static struct file_operations hello_drv = {
.owner = THIS_MODULE,
.open = hello_drv_open,
.read = hello_drv_read,
.write = hello_drv_write,
.release = hello_drv_close,
};
// (4)把 file_operations 结构体告诉内核:register_chrdev 注册驱动程序,因此先跳到步骤5
// (5)谁来注册驱动程序啊?得有一个入口函数:安装驱动程序时,就会去调用这个入口函数
static int __init hello_init(void)
{
int err;
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
major = register_chrdev(0, "hello", &hello_drv);
hello_class = class_create(THIS_MODULE, "hello_class");
err = PTR_ERR(hello_class);
if (IS_ERR(hello_class))
{
unregister_chrdev(major, "hello");
return -1;
}
device_create(hello_class, NULL, MKDEV(major, 0), NULL, "hello");
return 0;
}
// (6)有入口函数就应该有出口函数:卸载驱动程序时,出口函数调用unregister_chrdev
static void __exit hello_exit(void)
{
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
device_destroy(hello_class, MKDEV(major, 0));
class_destroy(hello_class);
unregister_chrdev(major, "hello");
}
// (7)其他完善:提供设备信息,自动创建设备节点:class_create,device_create
module_init(hello_init); //凭什么说上面的Init函数就是入口函数?这里这个宏的作用就是告诉内核这是入口函数
module_exit(hello_exit);
MODULE_LICENSE("GPL");
在解释上述的驱动程序之前,我们需要先理清相关概念,什么是驱动程序?什么是应用程序?
在linux操作系统中,内存被分为用户空间和内核空间,它们之间是存在界限的,不可以直接逾越,但是存在用户空间和内核空间交互的接口,这个接口叫做系统调用,而系统调用本质上就是中断。这部分涉及操作系统基础知识,推荐B站观看哈工大李治军老师操作系统课程中有关系统调用那部分的内容。
从应用空间进入内核空间的执行过程可以简单如下归纳:
用户空间中应用程序调用系统调用函数,系统调用函数封装了系统调用号,然后触发软中断,进入内核执行相应的软中断处理函数,这个中断处理函数根据传入的系统调用号去一个系统调用内核函数指针数组里找到对应的函数指针,然后执行这个函数,这个函数也就是内核中的驱动函数。可参考下图。
好了,大概了解系统调用的过程后,我们应该对如何编写内核驱动代码有思路了吧?
就是编写对应的内核系统调用函数!这是核心,以这个核心,扩展出整个编写内核驱动代码的框架和流程。
再多考虑一下,我们要编写的驱动程序既然属于内核代码,那么我们的的代码是不是应该和内核一起编译?
是的!但是Linux也提供了另外一种方式,就是可以将驱动代码编译成.ko格式的文件,然后运行操作系统的时候再加载。这样是不是更方便了?
是的,很方便!但是使用方便也是有条件的,那就是我们要遵守内核驱动代码编写的规则,这个规则也就是我们编写驱动程序的框架。想一想,你不遵守别人的规则,你还想享受别人的便利?自由是有代价的!所以我选择遵守这个规则,并且这个规则很好,为什么不遵守呢?
跟着韦东山老师的hello驱动部分的视频敲一遍代码,按照视频里的逻辑来编写代码,多试几遍,就会有一个比较清晰的思路,而且视频里的思路也确实是做项目写代码时的思路
linux中驱动代码的编写框架如下:
(1)确定主设备号,也可以让内核分配
(2)定义自己的 file_operations 结构体
(3)实现对应的 drv_open/drv_read/drv_write 等函数,填入 file_operations 结构体
(4)把 file_operations 结构体告诉内核:register_chrdev
(5)谁来注册驱动程序啊?得有一个入口函数:安装驱动程序时,就会去调用这个入口函数
(6)有入口函数就应该有出口函数:卸载驱动程序时,出口函数调用unregister_chrdev
(7)其他完善:提供设备信息,自动创建设备节点:class_create,device_create
一般情况下,我们可以参考别人的代码来写我们的代码,在linux内核的driver/char目录下有个misc.c驱动程序,我们仿照这个程序来写
首先,把misc.c文件中的头文件都包含进来
点击查看代码
#include "asm/uaccess.h"
#include "linux/err.h"
#include "linux/export.h"
#include "linux/kdev_t.h"
#include "linux/printk.h"
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/miscdevice.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/mutex.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/stat.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/tty.h>
#include <linux/kmod.h>
#include <linux/gfp.h>
我们要实现对应的write、read、open、close等内核函数,并填入file_operation结构体。(对于这个程序而言,这些函数足够了,以后写别的驱动程序可能还要实现别的功能)
我们可以先把这个结构体定义出来一个变量,并给相应的成员赋值,再去实现这些函数
点击查看代码
#include "asm/uaccess.h"
#include "linux/err.h"
#include "linux/export.h"
#include "linux/kdev_t.h"
#include "linux/printk.h"
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/miscdevice.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/mutex.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/stat.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/tty.h>
#include <linux/kmod.h>
#include <linux/gfp.h>
// (2)定义自己的 file_operations
static struct file_operations hello_drv = {
.owner = THIS_MODULE,
.open = hello_drv_open,
.read = hello_drv_read,
.write = hello_drv_write,
.release = hello_drv_close,
};
紧接着为了不用在这个结构体前面声明这些函数,我们把函数定义写到这个结构体前面。其中MIN(a, b)这个宏是在写这些函数的时候发现要返回一个整数值而又增加进去的。做项目就是这样的,一开始只是想出一个框架,在实现的过程中会根据具体情况补充细节
点击查看代码
#include "asm/uaccess.h"
#include "linux/err.h"
#include "linux/export.h"
#include "linux/kdev_t.h"
#include "linux/printk.h"
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/miscdevice.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/mutex.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/stat.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/tty.h>
#include <linux/kmod.h>
#include <linux/gfp.h>
#define MIN(a, b) (a < b ? a : b)
// (3)实现对应的 drv_open/drv_read/drv_write 等函数,填入 file_operations 结构体
//为了不用声明,将函数定义放到前面,但是逻辑顺序应该是在后面
static ssize_t hello_drv_read (struct file * file, char __user * buf, size_t size, loff_t * offset)
{
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
copy_to_user(buf, kernel_buf, MIN(1024, size));
return MIN(1024, size);
}
static ssize_t hello_drv_write (struct file * file, const char __user * buf, size_t size, loff_t * offset)
{
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
copy_from_user(kernel_buf, buf, MIN(1024, size));
return MIN(1024, size);
}
static int hello_drv_open (struct inode * node, struct file * file)
{
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
return 0;
}
static int hello_drv_close (struct inode * node, struct file *file)
{
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
return 0;
}
// (2)定义自己的 file_operations
static struct file_operations hello_drv = {
.owner = THIS_MODULE,
.open = hello_drv_open,
.read = hello_drv_read,
.write = hello_drv_write,
.release = hello_drv_close,
};
接下来就需要把这个file_operation结构体告诉内核,那么如何告诉内核?什么时候告诉内核?肯定是在加载驱动的时候告诉内核。那么谁来加载驱动?肯定需要一个入口函数。因此在加载驱动的入口函数需要使用注册函数register_chrdev。既然有入口函数,就有出口函数,用于卸载驱动,并且卸载驱动的流程应该和加载时相反,因此继续添加入口函数和出口函数。
点击查看代码
#include "asm/uaccess.h"
#include "linux/err.h"
#include "linux/export.h"
#include "linux/kdev_t.h"
#include "linux/printk.h"
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/miscdevice.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/mutex.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/stat.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/tty.h>
#include <linux/kmod.h>
#include <linux/gfp.h>
#define MIN(a, b) (a < b ? a : b)
// (3)实现对应的 drv_open/drv_read/drv_write 等函数,填入 file_operations 结构体
//为了不用声明,将函数定义放到前面,但是逻辑顺序应该是在后面
static ssize_t hello_drv_read (struct file * file, char __user * buf, size_t size, loff_t * offset)
{
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
copy_to_user(buf, kernel_buf, MIN(1024, size));
return MIN(1024, size);
}
static ssize_t hello_drv_write (struct file * file, const char __user * buf, size_t size, loff_t * offset)
{
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
copy_from_user(kernel_buf, buf, MIN(1024, size));
return MIN(1024, size);
}
static int hello_drv_open (struct inode * node, struct file * file)
{
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
return 0;
}
static int hello_drv_close (struct inode * node, struct file *file)
{
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
return 0;
}
// (2)定义自己的 file_operations
static struct file_operations hello_drv = {
.owner = THIS_MODULE,
.open = hello_drv_open,
.read = hello_drv_read,
.write = hello_drv_write,
.release = hello_drv_close,
};
// (5)谁来注册驱动程序啊?得有一个入口函数:安装驱动程序时,就会去调用这个入口函数
static int __init hello_init(void)
{
int err;
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
major = register_chrdev(0, "hello", &hello_drv);
return 0;
}
// (6)有入口函数就应该有出口函数:卸载驱动程序时,出口函数调用unregister_chrdev
static void __exit hello_exit(void)
{
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
unregister_chrdev(major, "hello");
}
最后,这就完善了吗?编写的入口函数和出口函数内核如何识别?在linux操作系统中,一切皆文件,你的驱动程序是不是要去驱动一个具体的东西?这个东西如何被系统识别?是不是要创建一个关于这个设备的节点文件来表示这个驱动设备?并且还要把这个节点和所写的驱动代码联系起来。简洁地说就是,一个具体地设备在系统里被抽象成为一个文件,操作这个文件需要通过我们所编写的驱动代码,从而实现最终操作这个具体的设备。如果再加上应用层的话,描述起来就是:在应用程序通过文件名使用系统调用函数,系统调用进入内核操作这个文件在内核中对于的驱动代码,从而实现操作这个硬件。因此我们要在内核中创建这个设备对应的文件,指定哪个函数是入口函数。通常,我们应该是在加载驱动程序时创建文件,卸载驱动程序时销毁文件,因此我们要继续完善这些内容。最终完整版代码就是文章最开始时的那部分代码,如果不想往上翻的话,我在这里也贴出来。
点击查看代码
hello_driver.c
#include "asm/uaccess.h"
#include "linux/err.h"
#include "linux/export.h"
#include "linux/kdev_t.h"
#include "linux/printk.h"
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/miscdevice.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/mutex.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/stat.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/tty.h>
#include <linux/kmod.h>
#include <linux/gfp.h>
#include <linux/printk.h>
// (1)确定主设备号,也可以让内核分配
static int major = 0;
static char kernel_buf[1024]; //内核缓存空间
static struct class *hello_class;
#define MIN(a, b) (a < b ? a : b)
// (3)实现对应的 drv_open/drv_read/drv_write 等函数,填入 file_operations 结构体
//为了不用声明,将函数定义放到前面,但是逻辑顺序应该是在后面
static ssize_t hello_drv_read (struct file * file, char __user * buf, size_t size, loff_t * offset)
{
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
copy_to_user(buf, kernel_buf, MIN(1024, size));
return MIN(1024, size);
}
static ssize_t hello_drv_write (struct file * file, const char __user * buf, size_t size, loff_t * offset)
{
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
copy_from_user(kernel_buf, buf, MIN(1024, size));
return MIN(1024, size);
}
static int hello_drv_open (struct inode * node, struct file * file)
{
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
return 0;
}
static int hello_drv_close (struct inode * node, struct file *file)
{
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
return 0;
}
// (2)定义自己的 file_operations
static struct file_operations hello_drv = {
.owner = THIS_MODULE,
.open = hello_drv_open,
.read = hello_drv_read,
.write = hello_drv_write,
.release = hello_drv_close,
};
// (4)把 file_operations 结构体告诉内核:register_chrdev 注册驱动程序,因此先跳到步骤5
// (5)谁来注册驱动程序啊?得有一个入口函数:安装驱动程序时,就会去调用这个入口函数
static int __init hello_init(void)
{
int err;
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
major = register_chrdev(0, "hello", &hello_drv);
hello_class = class_create(THIS_MODULE, "hello_class");
err = PTR_ERR(hello_class);
if (IS_ERR(hello_class))
{
unregister_chrdev(major, "hello");
return -1;
}
device_create(hello_class, NULL, MKDEV(major, 0), NULL, "hello");
return 0;
}
// (6)有入口函数就应该有出口函数:卸载驱动程序时,出口函数调用unregister_chrdev
static void __exit hello_exit(void)
{
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
device_destroy(hello_class, MKDEV(major, 0));
class_destroy(hello_class);
unregister_chrdev(major, "hello");
}
// (7)其他完善:提供设备信息,自动创建设备节点:class_create,device_create
module_init(hello_init); //凭什么说上面的Init函数就是入口函数?这里这个宏的作用就是告诉内核这是入口函数
module_exit(hello_exit);
MODULE_LICENSE("GPL");
对应的应用层代码属于应用编程部分,比较简单,这里就不赘述了
点击查看代码
hello_test.c
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
/* 写: ./hello_test /dev/xxx 100ask
* 读: ./hello_test /dev/xxx
*/
int main(int argc, char **argv)
{
int fd;
int len;
char buf[100];
if (argc < 2)
{
printf("Usage: \n");
printf("%s <dev> [string]\n", argv[0]);
return -1;
}
// open
fd = open(argv[1], O_RDWR);
if (fd < 0)
{
printf("can not open file %s\n", argv[1]);
return -1;
}
if (argc == 3)
{
// write
len = write(fd, argv[2], strlen(argv[2])+1);
printf("write ret = %d\n", len);
}
else
{
// read
len = read(fd, buf, 100);
buf[99] = '\0';
printf("read str : %s\n", buf);
}
// close
close(fd);
return 0;
}
文章中涉及的文件句柄、inode的知识属于操作系统中磁盘管理部分的知识,建议观看哈工大李治军老师的视频。
题外话:如果想对驱动开发和应用开发有更好的理解,操作系统的知识还是很重要的,融合了计算机软件和计算机组成原理,理解了操作系统也就理解了计算机是如何工作的。软件和硬件是不分家的,理解这点对于嵌入式学习者特别重要。
在韦东山的第一个hello驱动程序章节的最后还有一些关于module_init、module_exit、register_chrdev、class_destroy和device_create的原理介绍,可以看看加深理解,在《嵌入式应用开发完全手册》第五篇317页
标签:__,驱动程序,drv,static,file,Linux,include,hello From: https://www.cnblogs.com/starstxg/p/18115692