Linux属于一个不断发展且较为成熟的系统体系,既然它自身便是一种体系,更加需要我们按照其系统的规则进行开发与维护,所以在学习过程中更多的是完成框架与改写驱动的过程。
此处略去了linux环境配置,uboot,linux移植等等步骤,直接进入驱动编写环节。最开始我们开发一般是在裸机上进行,随着设备的主频变快,资源加多,需要我们进行手工管理的资源也更加庞杂,这时便出现了基于MCU的操作系统,如ucOS、FreeRTOS等等。
对于linux系统来说,主要分为用户态与内核态,内核Kernel帮助我们对处理器的任务调度、资源分配进行自主运行,用户态可以由用户自由操作。
如同我们用windows一样,我们只用点击桌面图标,便能进入应用程序,其中调用显卡、声卡、CPU的具体操作都不用我们亲自进行,而是系统帮助我们进行协调。
在linux下,内核态为用户态提供了大量的API,用户态程序可以调用这些API向内核下达指令,内核态收到指令后,会调用某个具体外设的驱动程序(设置具体的GPIO高低电平等),驱动该外设正常运行。
根据此执行流程便衍生出了应用程序编写与驱动程序编写两个方向。
本次主要分析字符型设备的驱动框架,因为所开发驱动程序是运行在MCU(IMX6ULL)上,因此linux驱动程序需要依托MCU运行的linux内核文件进行开发,因此本次选择Ubuntu配置联合交叉编译器作为开发环境,选择vscode作为开发环境。
驱动程序可以作为内核的一部分编译进zImage,也可以挂载。一般开发流程为:驱动测试无误后,便可直接写进内核,所以这里先使用挂载方式进行学习。编译好的linux内核文件文件下有一个文件夹drivers,该文件夹里面为linux自带驱动,也就是我们开发需要参照的模板。
使用vscode打开drivers文件,这里打开fmc-chrdev.c文件进行分析,由于只关心基本框架,所以略去了函数内容。
/*
* Copyright (C) 2012 CERN (www.cern.ch)
一些…………………………
*/
#include
#include
#include
#include
#include
#include
#include
#include
#include /* 头文件 */
static LIST_HEAD(fc_devices);
static DEFINE_SPINLOCK(fc_lock);
/* at open time, we must identify our device */
static int fc_open(struct inode *ino, struct file *f)
{
}
static int fc_release(struct inode *ino, struct file *f)
{
}
/* read and write are simple after the default llseek has been used */
static ssize_t fc_read(struct file *f, char __user *buf, size_t count,
loff_t *offp)
{
}
static ssize_t fc_write(struct file *f, const char __user *buf, size_t count,
loff_t *offp)
{
}
static const struct file_operations fc_fops = {
.owner = THIS_MODULE,
.open = fc_open,
.release = fc_release,
.llseek = generic_file_llseek,
.read = fc_read,
.write = fc_write,
};
static struct fmc_driver fc_drv = {
.version = FMC_VERSION,
.driver.name = KBUILD_MODNAME,
.probe = fc_probe,
.remove = fc_remove,
/* no table: we want to match everything */
};
static int fc_probe(struct fmc_device *fmc)
{
int ret;
int index = 0;
}
static int fc_remove(struct fmc_device *fmc)
{
}
static int fc_init(void) /*模块初始化函数*/
{
int ret;
ret = fmc_driver_register(&fc_drv);
return ret;
}
static void fc_exit(void) /*模块退出函数*/
{
fmc_driver_unregister(&fc_drv);
}
module_init(fc_init);
module_exit(fc_exit); /*向kernel申请模块初始化和退出函数*/
MODULE_LICENSE("GPL");
首先需要调用**module_init();module_exit();**向内核申请模块驱动初始化和退出函数,在初始化函数和退出函数中需要完成对字符驱动的注册(register)与(unregister)。而对驱动的读写需要借助结构体file_operations fc_fops进行完成,结构体内建立了驱动的读写函数可与用户态的App进行数据交互。
根据现有驱动程序可以构建如下所示的驱动大致框架:
include <linux/types.h>
#include
#include
#include
#include
#include
#define CHRDEVBASE_MAJOR 199 /*定义设备主设备号*/
#define CHARDEVBASE_NAME "chrdevbase" /*定义设备名称*/
/*驱动open函数*/
static int chrdevbase_open(struct inode * inode, struct file * file)
{
printk("kernerl:open_func:chrdevbase_open\r\n");
return 0;
}
/*驱动release函数(关闭)*/
static int chrdevbase_release(struct inode * inode, struct file * file)
{
printk("kernerl:release_func:chrdevbase_release\r\n");
return 0;
}
/*驱动write函数(写)*/
static ssize_t chrdevbase_write(struct file * file, const char __user * buf,
size_t count, loff_t *ppos)
{
printk("kernerl:write_func:chrdevbase_write\r\n");
return 0;
}
/*驱动read函数(读)*/
static ssize_t chrdevbase_read(struct file *filp, char __user *buf, size_t cnt, loff_t *off)
{
printk("kernerl:readfunc:chrdevbase_read\r\n");
return 0;
}
/*文件操作结构体*/
/*.owner为chrdevbase_fops.owner的简写*/
/*这里是为了实现只给结构体中某些成员进行幅值*/
static 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)
{
printk("printk: chrdevbase is init\r\n");
/*参数为:主设备号,设备名称,设备文件操作结构体*/
register_chrdev(CHRDEVBASE_MAJOR,CHARDEVBASE_NAME,&chrdevbase_fops);
return 0;
}
/*模块退出函数*/
void __exit chrdevbase_exit(void)
{
printk("printk: chrdevbase is exit\r\n");
/*参数为:主设备号,设备名称*/
unregister_chrdev(CHRDEVBASE_MAJOR, CHARDEVBASE_NAME);
}
/*模块入口函数*/
module_init(chrdevbase_init);
module_exit(chrdevbase_exit);
MODULE_LICENSE("GPL");
如下图所示,便是驱动的整体编写的流程,这里只是叙述了编写的流程,不代表系统的执行流程便是如此。需要注意的是file_operations结构体含有多个操作函数,定义时需要按需求进行实现(具体参见fs.h文件[1588行])。
下面代码段即是file_operations结构体的具体定义,可以看到结构体中含有大量的文件操作函数指针,因此在.c文件进行创建具体的实例时,需要将==函数地址(函数名)==传递给函数指针。
struct file_operations {
struct module *owner;
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
int (*iterate) (struct file *, struct dir_context *);
unsigned int (*poll) (struct file *, struct poll_table_struct *);
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
int (*mmap) (struct file *, struct vm_area_struct *);
int (*mremap)(struct file *, struct vm_area_struct *);
int (*open) (struct inode *, struct file *);
int (*flush) (struct file *, fl_owner_t id);
int (*release) (struct inode *, struct file *);
int (*fsync) (struct file *, loff_t, loff_t, int datasync);
int (*aio_fsync) (struct kiocb *, int datasync);
int (*fasync) (int, struct file *, int);
int (*lock) (struct file *, int, struct file_lock *);
ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
int (*check_flags)(int);
int (*flock) (struct file *, int, struct file_lock *);
ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
int (*setlease)(struct file *, long, struct file_lock **, void **);
long (*fallocate)(struct file *file, int mode, loff_t offset,
loff_t len);
void (*show_fdinfo)(struct seq_file *m, struct file *f);
#ifndef CONFIG_MMU
unsigned (*mmap_capabilities)(struct file *);
#endif
};
为了调用底层驱动程序,需要编写应用程序app进行测试(chrdevapp.c):
#include
#include
#include
#include
#include
char writebuf[20] = "hello word!";
int main(int argc,char *argv[]) /* argc 为输入参数个数 argv为输出参数内容*/
{
int fd;
char *filename;
char readbuf[20];
filename = argv[1];
printf("filename:%s\r\n",filename);
sleep(2);
printf("app:run open\r\n");
sleep(1);
if((fd = open(filename,O_RDWR))<0)
{
printf("error open fd :%d\r\n",fd);
return -1;
}
sleep(2);
printf("app:run read\r\n");
sleep(1);
if(read(fd,readbuf,10)<0)
{
printf("error read\r\n");
return -1;
}
sleep(1);
printf("app:run write\r\n");
sleep(1);
if(write(fd,writebuf,12)<0)
{
printf("error write\r\n");
return -1;
}
sleep(1);
printf("app:run close\r\n");
sleep(1);
if(close(fd)<0)
{
printf("error close\r\n");
return -1;
}
sleep(1);
return 0;
}
以上的==open()、read()、wrtie()、close()==函数都是用户态的API函数,其主要使用方式可以通过Ubuntu下的man指令进行查看(因为我们的Ubuntu系统运行在用户态,所以两者可运行的函数一致,可以进行借鉴)
man 2 open
KERNELDIR := /home/xxx/linux/linux_boot/linux-imx-rel_imx_4.1.15_2.1.0_ga
CURRENT_PATH := $(shell pwd)
obj-m := chrdevbase.o #编译目标
build: kernel_modules #编译内核模块
kernel_modules:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules #以模块的形式进入linux内核目录进行编译
clean:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
app使用 gcc命令编译即可
arm-linux-gnueabihf-gcc chrdevapp.c chrdevapp
上述过程中有两个地方:
①手动创建了设备号为199的文件节点;在内核函数fs.h文件中,可以struct file_operations的定义以及man 2 open手册中看到open()函数:
int (*open)(struct inode*, struct file *); //fs.h文件中定义
int open(const char *pathname,int flags); //man手册中定义
可以发现在内核态open函数需要文件节点参数作为函数参数,而用户态需要文件路径进行操作。因此这里需要将/dev/chrdevbase 与设备号 199联系起来,因此便可以通过文件操作实现对外设的操作。
②运行App指令:./chrdevapp /dev/chrdevbase 该指令会传递给main函数(argc与argv[]),即:
argv[0] = ./chrdevapp
argv[1] = /dev/chrdevbase
其执行流程大致入下图,app不断调用底层驱动程序,而API函数完成内核态与用户态的交互: