Linux字符型驱动开发—基于友善之臂2416开发板

驱动程序(Device Driver)是一种可以使计算机和设备通信的特殊程序,相当于内核和硬件之间的接口,操作系统只能通过这个接口,才能控制硬件设备的工作。驱动程序接受上层软件(应用程序、内核)的请求,完成对硬件的操作,屏蔽了硬件的细节。Linux平台下的驱动程序将硬件设备抽象成一个文件,应用程序操作文件即操作硬件。

1 Linux驱动分类

linux系统下驱动分为字符设备驱动、块设备驱动和网络设备驱动三种,以下分别介绍三种驱动。

1.1 字符设备驱动

字符设备是指存取时没有缓存的设备,采用字符流方式访问的设备,此类设备支持按字节/字符来顺序读写数据,通常不支持随机存取 ,常见的字符设备有:led、key、camera、显卡、串口等。

1.2 块设备驱动

应用程序与驱动程序之间的数据交互是以块为单位的,应用程序可以随机访问设备数据,程序可自行确定读取数据的位置。常见的块设备有:U盘、eMMC、SD卡等,应用程序可以寻址磁盘上的任何位置,并由此读取数据。

1.3 网络设备驱动

数据包传输方式访问的设备,和上两者不同,通过ifconfig来创建和配置设备。网络驱动同块驱动最大的不同在于网络驱动异步接受外界数据,而块驱动只对内核的请求作出响应。常见的网络设备驱动有:蓝牙、wifi、网卡等。

2 Linux字符驱动开发框架

在Linux内核中,使用cdev结构体来描述一个字符设备,该结构体的定义在include/linux/cdev.h文件中,cdev结构体中的成员如下:

struct cdev {
	struct kobject kobj;  
	struct module *owner;                 //该字符设备所在的内核模块的对象指针
	const struct file_operations *ops;    //该结构描述了字符设备所能实现的方法
	struct list_head list;     
	dev_t dev;                            //字符设备的设备号,由主设备号和次设备号构成
	unsigned int count;                   //隶属于同一主设备号的次设备号的个数
};

其中比较重要的是file_operations这个结构体,每个设备驱动都实现这个结构体中所定义的部分或全部函数,不同的版本的内核file_operaTIons定义会稍有不同,linux 3.6中的定义如下:

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 (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
	ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
	int (*readdir) (struct file *, void *, filldir_t);
	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 (*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 **);
	long (*fallocate)(struct file *file, int mode, loff_t offset,
			  loff_t len);
};

当应用程序对设备文件进行诸如open、close、read、write等操作时,Linux内核将通过file_operations结构访问驱动程序提供的函数。例如,当应用程序对设备文件执行读操作时,内核将调用file_operations结构中的read函数。因此,驱动开发就是相当于在实现驱动的入口和出口函数基础上再把file_operations结构体中的函数实现,入口、出口函数在驱动模块加载和卸载时调用,file_operations结构体中的函数在设备控制时调用。

3 Linux字符驱动实例

在linux源码drivers/char目录下新建first_driver.c文件,文件中内容如下所示:

#include <linux/fs.h>
#include <linux/module.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/uaccess.h>
#include <linux/types.h>
#include <asm/io.h>

static struct class *first_drv_class;
static struct device *first_drv_class_dev;

static dev_t devno; //设备号
static int major = 231; //主设备号
static int minor = 0; //次设备号
static char *module_name = "first_driver"; //模块名

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

static int  first_drv_write(struct file *file, const char __user *buf, size_t
count, loff_t *ppos)
{
	printk("first_drv_write\n");
	return 0;
}

static struct file_operations first_drv_fops =
{
	.owner = THIS_MODULE,
	// 将对应的函数关联在file_operations的结构体中
	.open = first_drv_open,
	.write = first_drv_write,
};
//驱动入口
static int  first_drv_init(void)
{
	int ret;
	devno = MKDEV(major,minor); //创建设备号
	ret = register_chrdev( major, module_name, &first_drv_fops); //注册驱动告诉内核把这驱动加入到内核的链表中
	
	first_drv_class = class_create( THIS_MODULE, "first_driver_demo" ); //让代码在/dev下自动生成设备  (创建一个类)
	first_drv_class_dev = device_create( first_drv_class, NULL, devno, NULL, module_name); //创建设备文件(在类下面生成一个设备)
	return 0;
}

static void  first_drv_exit(void)
{
  printk("first_drv_exit\n");
	device_destroy(first_drv_class,devno);
	class_destroy(first_drv_class);
	unregister_chrdev( major, module_name);
}
//内核将通过这个宏,来直到这个驱动的入口和出口函数
module_init(first_drv_init);
module_exit(first_drv_exit);

MODULE_AUTHOR("zhy <1521772422@qq.com>");
MODULE_LICENSE("GPL");

4 Linux字符驱动编译

我使用了友善之臂2416的开发板,参考了提供的用户手册中的驱动模块编译方式,具体步骤如下:
1 在linux-3.6/drivers/char/kconfig 文件中config MINI2451_ADC后面添加first_driver的config:

config MINI2451_ADC
	tristate "ADC driver for FriendlyARM Mini2451 development boards"
	depends on MACH_MINI2451
	default y
	help
	  this is ADC driver for FriendlyARM Mini2451 development boards

config MINI2451_FIRST_DRIVER
	tristate "My first driver for FriendlyARM Mini2451 development boards"
	depends on MACH_MINI2451
	default y
	help
	  this is demo driver for FriendlyARM Mini2451 development boards

2 在linux3.6目录执行:

cp mini2451_linux_config .config
make menuconfig

出现kernal配置界面。
在这里插入图片描述3 进入Device Drivers -》Character devices ,菜单中就可以看到刚才所添加的选项了。按下空格键将会选择为" M “,此意为要把该选项编译为模块方式;再按下空格会变为” * “,意为要把该选项编译到内核中,在此我们选择” M "即可,如图:
在这里插入图片描述
4 在linux-3.6/drivers/char/Makefile最末尾添加first_driver的编译选项。

obj-$(CONFIG_MINI2451_BUTTONS)	+= mini2451_buttons.o
obj-$(CONFIG_MINI2451_BUZZER)	+= mini2451_pwm.o
obj-$(CONFIG_MINI2451_ADC)	+= mini2451_adc.o
obj-$(CONFIG_MINI2451_FIRST_DRIVER)  += first_driver.o

5 在linux3.6目录执行:

make modules

就可以在linux-3.6/drivers/char目录生成所需要的内核驱动文件first_driver.ko了。将编译出的first_driver.ko拷贝到开发板/lib/modules/3.6.0-FriendlyARM 目录。

cp /sdcard/first_driver.ko /lib/modules/3.6.0-FriendlyARM/
cd lib/modules/3.6.0-FriendlyARM/

然后在板子中使用insmod命令加载写好的驱动程序:

insmod first_driver.ko

执行后没有任何打印出来,按理加载模块时会调用first_drv_init函数,会有printk打印出来,但是控制台没有任何消息。
在这里插入图片描述使用lsmod命令查看发现驱动已经被加载:

lsmod

在这里插入图片描述在/dev/目录下也能发现first_driver这个设备
在这里插入图片描述

网上查找博客发现应该时printk log等级设置的原因,使用demsg查看内核日志:

demsg

在这里插入图片描述first_drv_init函数调用时的log果然有被打印,所以是printk log等级设置过低的原因,先查看一下当前打印等级。

cat /proc/sys/kernel/printk

显示出的4个数据分别对应控制台日志级别、默认的消息日志级别、最低的控制台日志级别和默认的控制台日志级别。
使用下面的命令设置当前日志级别:

echo 8 > /proc/sys/kernel/printk 

这样所有级别 < 8的消息都可以显示在控制台上,但是一顿操作后仍然打印不出来,原因没有查明,所以还是老老实实的使用dmesg命令查看。
如果想卸载模块,执行以下命令:

rmmod first_driver

注意 : 要使模块能够正常卸载 , 必须把模块放入开发板/lib/modules/3.6.0-FriendlyARM 目录。

5 Linux字符驱动测试

驱动程序写完了,编写测试程序调用一下驱动,以下是测试程序:

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

int main(int argc, char **argv)
{
    int fd;      //声明设备描述符
    int val = 1;  //随便定义变量传入到
    //根据设备描述符打开设备,之前定义的为first_driver
    fd = open("/dev/first_driver", O_RDWR);  
    if(fd < 0)          //打开失败
	  	printf("can't open\n");  
    write(fd, &val, 4);  //根据文件描述符调用write
    
    return 0;
}

编译测试程序:

arm-linux-gcc driver_test.c -o driver_test

将编译好的driver_test拷贝到开发板中运行,。

/sdcard/driver_test

使用dmesg 命令查看log
在这里插入图片描述

可以看到first_drv_open和first_drv_write函数有被调用,证明编写的驱动程序ok。

参考文章

1 驱动程序
2 Linux驱动主要类型简介
3 Linux驱动分类简介
4 详细到吐血 —— 树莓派驱动开发入门:从读懂框架到自己写驱动
5 linux内核打印数据到串口控制台,printk数据不打印问题

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值