字符设备的驱动
更新记录
version | status | description | date | author |
---|---|---|---|---|
V1.0 | C | Create Document | 2018.12.26 | John Wan |
V2.0 | A | 添加各设备注册函数说明 | 2019.3.20 | John Wan |
V3.0 | M | 根据 《Linux设备驱动开发详解》进行了重新梳理 | 2019.4.23 | John Wan |
status:
C―― Create,
A—— Add,
M—— Modify,
D—— Delete。
注:内核版本 3.0.15
一、驱动程序的开发概述
1.1 应用程序、库、内核、驱动程序的关系
从上到下,一个软件系统可以分为应用程序、库、操作系统(内核)、驱动程序。开发人员可以专注于自己熟悉的部分,对于相邻层,只需了解它的接口,无需关注内部实现细节。但需了解整体的运作逻辑,以及各层实现的功能。
在 Linux 中一切皆文件,那么也是通过文件操作驱动。
这4层的协作关系如图:
(1) 应用程序使用库提供的 open、read、write、ioctl等接口函数进行操作(称为系统调用)(在 gcc 编译工具链 /libc/usr 目录下 fcntl.h、unistd.h、sys/ioctl.h等文件中找到 open、read、write、ioctl 函数原型)。
(2) 而这些接口函数都是设置好相关寄存器的,调用时库就执行 “swi” 指令(ARM架构),不同的函数对应 “swi” 的不同参数,该指令会引起 CPU 异常,进入内核。
(3) 内核的异常处理函数根据这些参数执行各种操作,比如根据设备文件名找到对应的驱动程序,调用驱动程序的相关函数等。一般来说,当应用程序调用 open、read、write等函数后,将会使用驱动程序中的 open、read、write函数来实现相关操作,比如初始化、读、写等。
1.2 Linux 驱动程序的分类
字符设备:是能够像字节流(如文件)一样被访问的设备,就是说对它的读写是以字节为单位的。比如串口在进行收发数据时就是一个字节一个字节进行传输的。字符设备的驱动程序中实现了open、close、read、write等系统调用,应用程序可以通过设备文件(如/dev/ttySAC0等)来访问字符设备。
块设备:块设备上的数据以块的形式存放,比如 NAND Flash 上的数据就是以页为单位存放的。块设备驱动程序向用户层提供的接口与字符设备一样,应用程序也可以通过相应的设备文件(如/dev/mtdblock0、/dev/hda1 等)来调用 open、close、read、write等系统调用,与块设备传输任意字节的数据。对用户而言,字符设备和块设备的访问方式没有差别。
差别:
(1) 由于块设备处理数据必须成块(如以页为单位进行擦除、读、写),因此在操作硬件的接口实现方式不一样。
(2) 数据块上的数据可以有一定的格式,不同的文件系统类型就是用来定义这些格式的。(如硬盘的不同文件格式)
网络设备:同时具有字符设备、块设备的部分特点。如果说它是字符设备,它的输入/输出却是有结构的、成块的(报文、包、帧);如果说它是块设备,它的“块”又不是固定大小的,大道数百甚至数千字节,小到几字节。库、内核提供了另一套和数据包传输相关的函数。
1.3 Linux驱动程序开发步骤
Linux 的内核是由各种驱动组成,对各种设备的支持非常完善,基本上可以找到同类设备,在其基础上进行小幅度修改以符合具体设备驱动。
因此编写驱动程序的难点并不是硬件的具体操作,而是弄清楚现有驱动程序的框架,在这个框架中加入这个硬件。
驱动开发的大致流程如下:
(1) 查看原理图,数据手册,确定设备类型,了解设备的操作方法;
(2) 在内核中找到相近的驱动程序,以它为模板进行开发,有时候需要从零开始;
(3) 实现驱动程序的初始化:如向内核注册这个函数,这样应用程序传入文件名时,内核才能找到相应的驱动程序。
(4) 设计所要实现的操作,如 open、close、read、write等;
(5) 实现中断服务(根据需求添加),或其它功能;
(6) 编译该驱动程序到内核总,或手动用命令加载;
(7) 测试驱动程序。
二、字符设备驱动
2.1 cdev
字符设备驱动结构
在 linux 内核中,使用 cdev
结构体描述一个字符设备:
struct cdev {
struct kobject kobj; /* 内嵌的 kobject 对象 */
struct module *owner; /* 所属模块 */
const struct file_operations *ops; /* 文件操作结构体 */
struct list_head list; /**/
dev_t dev; /* 设备号 */
unsigned int count; /**/
};
2.1.1 file_operation
对于每个系统调用函数,驱动中都有一个与之对应的函数。对于字符设备驱动程序,驱动中对应的函数集合在一个名为 file_operation
类型的数据结构中。原型在文件include\linux\fs.h
中:
/*
* NOTE:
* all file operations except setlease can be called without
* the big kernel lock held in all filesystems.
*/
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 *);
/* remove by cym 20130408 support for MT660.ko */
#if 0
//#ifdef CONFIG_SMM6260_MODEM
#if 1// liang, Pixtree also need to use ioctl interface...
int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
#endif
#endif
/* end remove */
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 *, 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);
/* add by cym 20130408 support for MT6260 and Pixtree */
#if defined(CONFIG_SMM6260_MODEM) || defined(CONFIG_USE_GPIO_AS_I2C)
int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
#endif
/* end add */
};
编写字符设备驱动程序,其实就是为具体硬件的 file_operation
结构填充各函数。
那么具体的某个字符设备的驱动程序与内核之间是如何联系起来的?毕竟会有很多字符设备的驱动程序,而内核需要对这些进行区分。答:通过设备号,主/次设备号。
2.1.2 dev_t dev
设备号
设备驱动不仅有不同类型的划分,同类型设备驱动中还会用主/次设备号进一步划分。主设备号用来标识设备对应的驱动程序,告诉内核使用哪个驱动程序为该设备提供服务;而次设备号则用来标识该驱动程序下具体且唯一的某个设备。
cdev
结构体的 dev_t
成员定义了设备号,为32位,其中高12位为主设备号,低20位为次设备号:原型在 /include/linux/kdev_t.h
:
#define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))
参数
-ma //主设备号
-mi //次设备号
注意到,主、次设备号共同组成了4字节,高12位为主设备号,低20位为次设备号,获取主、次设备号通过:
#define MINORBITS 20
#define MINORMASK ((1U << MINORBITS) - 1)
#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))
#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))
在系统中查看某驱动的设备号,例如:
[root@iTOP-4412]# ls /dev/ttyS0 -l
crw-rw---- 1 root 0 4, 64 Aug 19 01:48
crw-rw----
中的 c
标识字符设备,主设备号为4,次设备号为64。
驱动是可以有唯一的标识,那么内核是如何知道主/次设备号与某个驱动是对应的呢?答:通过驱动注册,在驱动向内核注册的过程中,将设备号与该设备驱动进行绑定。
2.2 字符设备驱动的注册/卸载
向内核进行注册,就是告诉内核,将主设备号与设备驱动对应的 file_operation
绑定,从而建立起它们之间的联系。
通过原型在文件include\linux\fs.h
中的 register_chrdev
函数:
static inline int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops)
{
return __register_chrdev(major, 0, 256, name, fops);
}
参数:
-major //主设备号,0~255,输入0表示由系统分配由函数返回,非零表示设定。
-name //字符设备驱动的名字,可通过lsmod查看
-fops //该字符设备驱动的file_operation数据结构
当然在不需要这种联系时,可通过 unregister_chrdev
取消解除绑定关系。
static inline void unregister_chrdev(unsigned int major, const char *name)
{
__unregister_chrdev(major, 0, 256, name);
}
注:了解register_chrdev()、register_chrdev_region()、alloc_chrdev_region()功能及差异
分配设备编号,注册设备与注销设备的函数均在fs.h中申明,如下:
int register_chrdev_region(dev_t, unsigned, const char *); //静态的申请和注册设备号
int alloc_chrdev_region(dev_t, unsigned, const char *); //动态的申请注册一个设备号
int register_chrdev(unsigned int, const char *,struct file_operations *); //int为0时候动态注册,非零时候静态注册。
int unregister_chrdev(unsigned int, const char *); //注销设备号
void unregister_chrdev_region(dev_t, unsigned); //注销设备号
前面已经了解到设备号的作用,以及申请方式,那么应用程序又是如何找到设备对应的驱动程序的呢?
linux中一切皆文件,应用程序是通过操作文件的方式来进行控制的,例如open("xxx", O_RDWR)
,而在前面说明中,并没有出现生成文件的操作。设备号面向的是内核,而不是应用层。那么要如何给应用层提供接口?答:通过设备节点。
2.3 设备节点的生成与注销
通过设备号可以精确的定位到设备对应的驱动程序,那么给应用层提供接口也是基于设备号来定位具体操作的设备。只不过,对于应用层来说,是以文件的方式进行操作,那么就需要将设备号与文件联系起来,这指的就是设备节点。可通过两种方式生成设备节点:
- 手动
使用 mknode
命令创建,原型:
mknod Name { b | c } Major Minor
参数:
- Name //创建的文件名,设备节点名称
- { b | c } //类型,b表示块设备,c表示字符设备
- Major //主设备号
- Minor //次设备号
- 自动
依赖于用户空间移植了 udev
。
(1) 利用class_create()
函数,根据设备驱动名字创建一个class类
:
/include/linux/device/h
/* This is a #define to keep the compiler from merging different
* instances of the __key variable */
#define class_create(owner, name) \
({ \
static struct lock_class_key __key; \
__class_create(owner, name, &__key); \
})
参数:
-owner //所属,一般为THIS_MODULE
-name //注册主设备号时的驱动名称
(2) 通过device_create()
函数,为每个设备创建设备节点:
struct device *device_create(struct class *class, struct device *parent,
dev_t devt, void *drvdata, const char *fmt, ...)
{
va_list vargs;
struct device *dev;
va_start(vargs, fmt);
dev = device_create_vargs(class, parent, devt, drvdata, fmt, vargs);
va_end(vargs);
return dev;
}
参数:
-class //设备驱动的类
-parent //父类,NULL
-devt //设备号,包括主设备号,次设备号,从次设备号申请了解到主设备号是偏移后或上次设备号
-drvdata //数据,NULL
-fmt //设备节点的名称
加载好的模块可以在 /dev
目录下看到创建的设备节点。
可通过 device_unregister()
函数删除设备节点,通过class_destroy()
函数释放掉申请的class类
。
2.4 cdev
相关操作函数
void cdev_init(struct cdev *, const struct file_operations *);
struct cdev *cdev_alloc(void);
void cdev_put(struct cdev *p);
int cdev_add(struct cdev *, dev_t, unsigned);
void cdev_del(struct cdev *);
cdev_init()
:初始化 cdev
成员,并建立 cdev
和 file_operation
之间的连接;
cdev_alloc()
:动态申请一个 cdev
内存;
cdev_add()、cdev_del()
:分别向系统添加和删除一个 cdev
,完成字符设备的注册和注销。
对 cdev_add()
的调用通常发生在字符设备驱动模块加载函数中,相反的,cdev_del()
函数的调用通常发生在字符设备驱动模块卸载函数中。
字符设备驱动的结构整体如下图所示:
以上就是驱动与内核、应用层之间的连接。那么问题来了,这些的源头也就是驱动的注册是在什么时候开始运行的呢?答:在加载驱动的时候运行。
2.4 模块的加载与卸载
在驱动程序中引入 module_init()
函数 与 module_exit()
函数,在模块进行加载时执行module_init()
函数,卸载时执行module_exit()
函数,例如:
module_init(leds_init); //leds_init 执行的初始化函数
module_exit(leds_exit); //
驱动模块的加载与卸载可通过手动或自动的方式来进行。
2.4.1 手动加载:通过命令的方式
命令:
insmod 文件名 //加载,文件名的后缀 ".KO"
rmmod 文件名 //卸载,
lsmod //查看加载的模块
cat /proc/devices //查看运行中的模块
insmod
和 rmmod
命令是如何来控制驱动程序的呢?
在驱动程序中引入 module_init()
函数 与 module_exit()
函数,当执行insmod
命令时,就会调用 module_init()
函数。执行 rmmod
命令时,调用 module_exit()
函数。
这样,前面各种需要注册、申请的情况都可以放在一个初始化函数中,然后通过 module_init(初始化函数名)
来调用。
问题:
在使用 rmmod
命令时会可能出现 "rmmod: can't change directory to '/lib/modules': No such file or directory"
这个错误。
那么按照提示在 /lib
目录下建立对应的文件夹就行。
2.4.2 自动加载
1)编译进内核
通过在对内核进行配置、编译时,将模块加载,具体的操作方式,另行总结。
2)自动加载模块
三、demo
/*
* a simple char device driver: globalmem without mutex
*
* Copyright (C) 2014 Barry Song (baohua@kernel.org)
*
* Licensed under GPLv2 or later.
*/
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/cdev.h>
#include <linux/slab.h>
#include <linux/uaccess.h>
#define GLOBALMEM_SIZE 0x1000
#define MEM_CLEAR 0x1
#define GLOBALMEM_MAJOR 230
static int globalmem_major = GLOBALMEM_MAJOR;
module_param(globalmem_major, int, S_IRUGO);
struct globalmem_dev {
struct cdev cdev;
unsigned char mem[GLOBALMEM_SIZE];
};
struct globalmem_dev *globalmem_devp;
static int globalmem_open(struct inode *inode, struct file *filp)
{
filp->private_data = globalmem_devp;
return 0;
}
static int globalmem_release(struct inode *inode, struct file *filp)
{
return 0;
}
static long globalmem_ioctl(struct file *filp, unsigned int cmd,
unsigned long arg)
{
struct globalmem_dev *dev = filp->private_data;
switch (cmd) {
case MEM_CLEAR:
memset(dev->mem, 0, GLOBALMEM_SIZE);
printk(KERN_INFO "globalmem is set to zero\n");
break;
default:
return -EINVAL;
}
return 0;
}
static ssize_t globalmem_read(struct file *filp, char __user * buf, size_t size,
loff_t * ppos)
{
unsigned long p = *ppos;
unsigned int count = size;
int ret = 0;
struct globalmem_dev *dev = filp->private_data;
if (p >= GLOBALMEM_SIZE)
return 0;
if (count > GLOBALMEM_SIZE - p)
count = GLOBALMEM_SIZE - p;
if (copy_to_user(buf, dev->mem + p, count)) {
ret = -EFAULT;
} else {
*ppos += count;
ret = count;
printk(KERN_INFO "read %u bytes(s) from %lu\n", count, p);
}
return ret;
}
static ssize_t globalmem_write(struct file *filp, const char __user * buf,
size_t size, loff_t * ppos)
{
unsigned long p = *ppos;
unsigned int count = size;
int ret = 0;
struct globalmem_dev *dev = filp->private_data;
if (p >= GLOBALMEM_SIZE)
return 0;
if (count > GLOBALMEM_SIZE - p)
count = GLOBALMEM_SIZE - p;
if (copy_from_user(dev->mem + p, buf, count))
ret = -EFAULT;
else {
*ppos += count;
ret = count;
printk(KERN_INFO "written %u bytes(s) from %lu\n", count, p);
}
return ret;
}
static loff_t globalmem_llseek(struct file *filp, loff_t offset, int orig)
{
loff_t ret = 0;
switch (orig) {
case 0:
if (offset < 0) {
ret = -EINVAL;
break;
}
if ((unsigned int)offset > GLOBALMEM_SIZE) {
ret = -EINVAL;
break;
}
filp->f_pos = (unsigned int)offset;
ret = filp->f_pos;
break;
case 1:
if ((filp->f_pos + offset) > GLOBALMEM_SIZE) {
ret = -EINVAL;
break;
}
if ((filp->f_pos + offset) < 0) {
ret = -EINVAL;
break;
}
filp->f_pos += offset;
ret = filp->f_pos;
break;
default:
ret = -EINVAL;
break;
}
return ret;
}
static const struct file_operations globalmem_fops = {
.owner = THIS_MODULE,
.llseek = globalmem_llseek,
.read = globalmem_read,
.write = globalmem_write,
.unlocked_ioctl = globalmem_ioctl,
.open = globalmem_open,
.release = globalmem_release,
};
static void globalmem_setup_cdev(struct globalmem_dev *dev, int index)
{
int err, devno = MKDEV(globalmem_major, index);
cdev_init(&dev->cdev, &globalmem_fops);
dev->cdev.owner = THIS_MODULE;
err = cdev_add(&dev->cdev, devno, 1);
if (err)
printk(KERN_NOTICE "Error %d adding globalmem%d", err, index);
}
static int __init globalmem_init(void)
{
int ret;
dev_t devno = MKDEV(globalmem_major, 0);
if (globalmem_major)
ret = register_chrdev_region(devno, 1, "globalmem");
else {
ret = alloc_chrdev_region(&devno, 0, 1, "globalmem");
globalmem_major = MAJOR(devno);
}
if (ret < 0)
return ret;
globalmem_devp = kzalloc(sizeof(struct globalmem_dev), GFP_KERNEL);
if (!globalmem_devp) {
ret = -ENOMEM;
goto fail_malloc;
}
globalmem_setup_cdev(globalmem_devp, 0);
return 0;
fail_malloc:
unregister_chrdev_region(devno, 1);
return ret;
}
module_init(globalmem_init);
static void __exit globalmem_exit(void)
{
cdev_del(&globalmem_devp->cdev);
kfree(globalmem_devp);
unregister_chrdev_region(MKDEV(globalmem_major, 0), 1);
}
module_exit(globalmem_exit);
MODULE_AUTHOR("Barry Song <baohua@kernel.org>");
MODULE_LICENSE("GPL v2");
四、案例:LED
硬件:迅为iTop4412精英板。
arm交叉编译器:arm-2009q3.tar.bz2
内核版本:linux3.0.15,迅为修改后的。
4.1 驱动框架
根据前面的说明,编写如下:
#include <linux/module.h>
#include <linux/kernel.h>
/*注册设备节点的文件结构体*/
#include <linux/fs.h>
/* 驱动的加载,卸载 */
#include <linux/init.h>
/*驱动注册的头文件,包含驱动的结构体和注册和卸载的函数*/
#include <linux/platform_device.h>
#define LED_MAJOR 231
#define DEVICE_NAME "leds"
static struct file_operations leds_fops = {
.owner = THIS_MODULE,
.open = leds_open,
.read = leds_read,
.write = leds_write,
};
static struct class *leds_class;
static struct device *leds_class_devs;
/*
* 执行insmod命令会调用该函数
*/
static int __init leds_init(void)
{
int retval;
/*
* 注册字符类设备
* 参数为 主设备号、设备名字、设备对应的file_operations结构体
* 这样就将设备号与对应结构体绑定,操作设备号的设备文件时,
* 就会调用file_operations的相关成员函数
* 设备号如何写入0,表示由内核自动分配主设备号。
*/
retval = register_chrdev(LED_MAJOR, DEVICE_NAME, &leds_fops);
if (retval < 0) {
printk(DEVICE_NAME "can't register major number\n");
return retval;
}
/*
* 自动生成设备节点
*/
leds_class = class_create(THIS_MODULE, "leds");
if (IS_ERR(leds_class))
return PTR_ERR(leds_class);
leds_class_devs = device_create(leds_class, NULL, MKDEV(LED_MAJOR, 0), NULL, "xyz");
printk(DEVICE_NAME "initialized\n");
return 0;
}
/*
* 执行rmmod命令会调用该函数
*/
static void __exit leds_exit(void)
{
unregister_chrdev(LED_MAJOR, "leds");
device_unregister(leds_class_devs);
class_destroy(leds_class);
}
/*指定驱动程序的初始化函数与卸载函数*/
module_init(leds_init);
module_exit(leds_exit);
MODULE_LICENSE("GPL");
在完成驱动框架后,根据前面的开发说明,就需要编写驱动具体能够执行的操作。例如 open、read、write等。在应用层中进行系统调用时,操作文件首先就是要 open,因此该操作必不可少。而对应驱动中的 open ,一般用来进行硬件初始化。
2.2 硬件初始化
硬件的初始化和STM32一样也分为两种:
- 一种根据芯片手册,直接控制寄存器。
- 一种调用提供的库函数。
2.2.1 根据芯片手册操作寄存器
在操作寄存器之前,需要了解一个概念,那就是:物理地址与虚拟地址,芯片手册上寄存器的地址是物理地址,而由于我使用的开发板运行在linux系统下,那么就需要转换成虚拟地址。那么如何将物理地址转换成虚拟地址?这就需要借助于 /arch/arm/include/asm/io.h
中的 ioremap()
函数与iounmap()
函数。
/* ioremap iounmap*/
#include <asm/io.h>
#define ioremap(cookie,size) __arch_ioremap((cookie), (size), MT_DEVICE)
参数:
-cookie //地址
-size //大小
函数的原型在 /arch/arm/mm/ioremap.c
中。
(1) 首先在模块加载时,就要映射虚拟地址,则在 leds_init()
中添加:
//注意芯片手册,不同的GPIO基地址差异可能较大
#define GPIO_BASE_ADDR 0x11000000
#define GPIO_SIZE 0x0F84
unsigned long pvirtual_addr; //注意不能申明为static, 否则释放会报错
//led2 GPL2_0
#define GPL2CON (*(volatile unsigned long *)(pvirtual_addr + 0x0100))
#define GPL2DAT (*(volatile unsigned long *)(pvirtual_addr + 0x0104))
//led3 GPK1_1
#define GPK1CON (*(volatile unsigned long *)(pvirtual_addr + 0x0060))
#define GPK1DAT (*(volatile unsigned long *)(pvirtual_addr + 0x0064))
在 leds_init()中添加:
//将实际的GPIO地址映射成虚拟地址
pvirtual_addr = (unsigned long)ioremap(GPIO_BASE_ADDR, GPIO_SIZE);
static int leds_open(struct inode *inode, struct file *file)
{
/* 配置led2、led3引脚为输出 */
GPL2CON &= ~(0xF << (0 * 4));
GPL2CON |= (0x1 << (0 * 4));
GPK1CON &= ~(0xF << (1 *4));
GPK1CON |= (0x1 << (1 *4));
/* 默认输出低电平,关闭led */
GPL2DAT &= ~(1 << 0);
GPK1DAT &= ~(1 << 1);
//GPL2DAT |= (1 << 0);
//GPK1DAT |= (1 << 1);
printk(DEVICE_NAME " is open\n");
return 0;
}
static int leds_read(struct file *pfile, char __user *pbuff,
size_t count, loff_t *poff)
{
return 0;
}
static ssize_t leds_write(struct file *pfile, const char __user *pbuf,
size_t count, loff_t *poff)
{
int val;
copy_from_user(&val, pbuf, count);
switch (val) {
case 1:
GPL2DAT &= ~(1 << 0);
GPK1DAT &= ~(1 << 1);
//GPL2DAT |= (1 << 0);
//GPK1DAT |= (1 << 1);
break;
case 2:
GPL2DAT &= ~(1 << 0);
GPK1DAT &= ~(1 << 1);
GPL2DAT |= (1 << 0);
//GPK1DAT |= (1 << 1);
break;
case 3:
GPL2DAT &= ~(1 << 0);
GPK1DAT &= ~(1 << 1);
//GPL2DAT |= (1 << 0);
GPK1DAT |= (1 << 1);
break;
default:
break;
}
printk(DEVICE_NAME " write %d\n", val);
return 0;
}
2.2.2 调用提供的库函数
在该硬件上,调用库函数需要包含以下头文件:
/*Linux中申请GPIO的头文件*/
#include <linux/gpio.h>
/*三星平台的GPIO配置函数头文件*/
/*三星平台EXYNOS系列平台,GPIO配置参数宏定义头文件*/
#include <plat/gpio-cfg.h>
#include <mach/gpio.h>
/*三星平台4412平台,GPIO宏定义头文件*/
#include <mach/gpio-exynos4.h>
条用库函数实现:
static int leds_open(struct inode *inode, struct file *file)
{
/* 配置led2、led3引脚为输出 */
s3c_gpio_cfgpin(EXYNOS4_GPL2(0), S3C_GPIO_OUTPUT);
s3c_gpio_cfgpin(EXYNOS4_GPK1(1), S3C_GPIO_OUTPUT);
gpio_set_value(EXYNOS4_GPL2(0), 0);
gpio_set_value(EXYNOS4_GPK1(1), 0);
printk(DEVICE_NAME " is open\n");
return 0;
}
static int leds_read(struct file *pfile, char __user *pbuff,
size_t count, loff_t *poff)
{
return 0;
}
static ssize_t leds_write(struct file *pfile, const char __user *pbuf,
size_t count, loff_t *poff)
{
int val;
copy_from_user(&val, pbuf, count);
switch (val) {
case 1:
gpio_set_value(EXYNOS4_GPL2(0), 1);
gpio_set_value(EXYNOS4_GPK1(1), 1);
//printk(DEVICE_NAME " i'm here\n");
break;
case 2:
gpio_set_value(EXYNOS4_GPL2(0), 1);
gpio_set_value(EXYNOS4_GPK1(1), 0);
break;
case 3:
gpio_set_value(EXYNOS4_GPL2(0), 0);
gpio_set_value(EXYNOS4_GPK1(1), 1);
break;
default:
break;
}
printk(DEVICE_NAME " write %d\n", val);
return 0;
}
2.3 应用层的测试程序
#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;
fd = open("/dev/xyz", O_RDWR);
if (fd < 0)
printf("can't open is!\n");
if (argc != 2) {
printf("Usage :\n");
printf("%s <on|off>\n", argv[0]);
return 0;
}
if (strcmp(argv[1], "leds") == 0)
val = 1;
else if (strcmp(argv[1], "led2") == 0)
val = 2;
else
val = 3;
write(fd, &val, 4);
return 0;
}
应用程序通过 fd = open("/dev/xyz", O_RDWR);
打开设备节点文件,对应的字符设备驱动程序中的 leds_open
运行,对led的硬件IO口进行初始化。应用程序通过write(fd, &val, 4);
传入参数,对应的字符设备驱动程序中 static ssize_t leds_write(struct file *pfile, const char __user *pbuf, size_t count, loff_t *poff)
运行,val
对应 pbuf
, 4 对应count
。
那么驱动程序中的 copy_from_user(&val, pbuf, count);
是干什么呢?其实就是根据 pbuf
地址,读取 count
数量的数据,赋值给val
,在该程序中相当于 val = *pbuf;
。只不过copy_from_user()
函数,有更好的扩展性以及稳定性。
copy_from_user()
:从用户空间中读取数据到。
copy_to_user()
:从内核空间发送数据到用户空间。
2.4 程序的编译与运行
2.4.1 字符设备驱动程序的编译与加载
驱动程序需要借助 Makefile
进行编译。内容如下:
#!/bin/bash
#通知编译器我们要编译模块的哪些源码
#这里是编译leds.c这个文件编译成中间文件leds.o,要生成的文件名
#这里的 -m 选项表示可动态加载的module,如果是静态,也就是和内核一起编译的,改为 -y
obj-m += leds.o
#源码目录变量,这里用户需要根据实际情况选择路径
#作者是将Linux的源码拷贝到目录/home/topeet/android4.0下并解压的
KDIR := /home/topeet/Android4.0/iTop4412_Kernel_3.0
#当前目录变量
PWD ?= $(shell pwd)
#make命名默认寻找第一个目标
#make -C就是指调用执行的路径
#$(KDIR)Linux源码目录,作者这里指的是/home/topeet/android4.0/iTop4412_Kernel_3.0
#$(PWD)当前目录变量
#modules要执行的操作
all:
make -C $(KDIR) M=$(PWD) modules
#make clean执行的操作是删除后缀为o的文件
clean:
rm -rf *.o
将编写的程序和Makefile
拷贝到 ubuntu,然后切换到拷贝所在的目录下,执行:make
。编译成功会生成 leds.ko
文件。将其拷贝到开发板上,通过 insmod、rmmod、lsmod
命令进行操作。
要注意的几点:
(1) Makefile
中的obj-m += leds.o
也决定了生成 .ko
的文件名。
(2) Makefile
中KDIR
一定不能错,对大小写敏感。
(3) make
一定是在内核已经编译过的情况下,才能使用。也就是需要将上面/home/topeet/Android4.0/iTop4412_Kernel_3.0
路径下源码进行编译,如何编译参考迅为编译内核的视频。
[root@iTOP-4412]# insmod leds_ioremap.ko
[ 3941.561189] ledsinitialized
[root@iTOP-4412]# lsmod
leds_ioremap 1742 0 - Live 0xbf010000
[root@iTOP-4412]# rmmod leds_ioremap
[root@iTOP-4412]#
2.4.2 测试程序的编译与运行
将 leds_test.c
拷贝到linux中,然后调用 arm 交叉编译工具进行编译,前提是安装好 arm 交叉编译工具链。
切换到文件所在目录,运行:arm-none-linux-gnueabi-gcc -o leds_test leds_test.c -static
,不同版本的编译工具命令可能不同。
执行完之后,会生成文件 leds_test
。
将其拷贝到开发板上,在文件所放的目录下运行:./leds_test
(在已加载驱动模块的条件下)。
[root@iTOP-4412]# ./leds_test
[ 2818.836389] leds is open
Usage :
./leds_test <on|off>
[root@iTOP-4412]# ./leds_test leds
[ 2848.438178] leds is open
[ 2848.439284] leds write 1
[root@iTOP-4412]# ./leds_test led2
[ 2851.223358] leds is open
[ 2851.224464] leds write 2
[root@iTOP-4412]# ./leds_test led3
[ 2853.576498] leds is open
[ 2853.577606] leds write 3
参考
- 《嵌入式Linux应用开发完全手册》 - 韦东山,19章,20章
- 韦东山第一期视频,第十二课
- 迅为iTop4412资料
- 《Linux设备驱动开发详解:基于最新的Linux 4.0内核》 - 宋宝华
- Linux字符设备驱动
- Linux字符设备驱动实现