【嵌入式Linux驱动开发】番外一、ioctl 系统调用详细解析

   劳劳车马未离鞍,临事方知一死难。
  三百年来伤国步,八千里外吊民残。
  秋风宝剑孤臣泪,落日旌旗大将坛。
  海外尘氛犹未息,请君莫作等闲看。
            —李鸿章口占七律


一、ioctl系统调用

  ioctl 系统调用主要用于增加系统调用的硬件控制能力,它可以构建自己的命令,也能接受参数。通过 ioctl 控制硬件 I/O,必须在驱动中为 ioctl()系统调用设计一些控制命令,通过不同的命令实现不同的硬件控制。

  内核空间 iotcl 函数原型如下所示,定义的 ioctl 命令通过 cmd 传递,数据通过 arg 传递。驱动得到 cmd 命令和 arg 参数后,须首先用解析 ioctl 命令的宏定义对命令和参数进行解析判断,没有问题再进行后续处理。

int (*ioctl) (struct inode *inode, struct file *filp, unsigned int cmd, unsigned long arg);
  • file表示文件描述符,cmd表示命令,arg表示与命令相关的参数,至于参数具体表达什么含义,完全由驱动编写者来定义。

用户空间的 ioctl 函数原型如下所示,

int ioctl (int fd, unsigned long cmd, ...)
  • fd 是被打开的设备文件, cmd 是操作设备的命令,“ …”代表可变数目的参数表,通常用 char *argp 来定义,如果 cmd 命令不需要参数,则传入 NULL 即可。

1.1 ioctl 命令构成

  ioctl 操作与硬件平台相关,使用 ioctl 的驱动需要包含<linux/ioctl.h>文件。每个 ioctl 命令cmd实际上都是一个 32 位整型数,各字段和含义如下表所示。

在这里插入图片描述
  例如,0x82187201,它的二进制如下表所示。所以含义为:读:_IOR;参数长度536;幻数114,ASCII为r,功能号1.

字段 31~30 29~16 15~8 7~0
二进制 10 00 0010 0001 1000 0111 0010 0000 0001

  实际上这个命令是<linux/msdos_fs.h>中的 VFAT_IOCTL_READDIR_BOTH 命令:#define VFAT_IOCTL_READDIR_BOTH _IOR('r', 1, struct __fat_dirent[2])

1.2 构造ioctl命令

  为驱动构造 ioctl 命令,首先要为驱动选择一个可用的幻数作为驱动的特征码,以区分不同驱动的命令。内核已经使用了很多幻数,为了防止冲突,最好不要再使用这些系统已经占用的幻数来作为驱动的特征码。已经被使用的幻数列表详见内核源码目录Documentation/ioctl/ioctl-number.txt文件。在不同平台上,幻数所使用情况都不同,为防止冲突,可以选择其它平台使用的幻数来用。选定幻数后,可以这样来进行定义:

#define LED_IOC_MAGIC 'Z'

  ioctl 命令字段的 bit[31:30]表示命令的方向,分别表示使用_IO、 _IOW、 _IOR 和_IOWR
这几个宏定义,分别用于构造不同的命令,具体见下表:

命令 描述
_IO(type,nr) 构造无参数的命令编号
_IOW(type,nr,size) 构造往驱动写入数据的命令编号
_IOR(type,nr,size) 构造从驱动中读取数据的命令编号
_IOWR(type,nr,size) 构造双向传输的命令编号

  这些宏定义中, type 是幻数, nr 是功能号, size 是数据大小。例如,为 LED 驱动构造 ioctl 命令,由于控制 LED 无需数据传输,可以这样定义:

#define SET_LED_ON _IO(LED_IOC_MAGIC, 0)
#define SET_LED_OFF _IO(LED_IOC_MAGIC, 1)

  如果想在 ioctl 中往驱动写入一个 int 型的数据,可以这样定义:

#define CHAR_WRITE_DATA _IOW(CHAR_IOC_MAGIC, 2, int)

  类似的,要从驱动中读取 int 型的数据,则定义为:

#define CHAR_READ_DATA _IOR(CHAR_IOC_MAGIC, 3, int)

  注意:同一份驱动的 ioctl 命令定义,无论有无数据传输以及数据传输方向是否相同,各命令的序号都不能相同。

  定义完全部所需命令后,还需定义一个命令的最大的编号,防止传入参数超过编号范围。

1.3 解析 ioctl 命令

  驱动程序必须对传入的命令进行解析,包括传输方向、命令类型、命令编号以及参数大
小,分别可以通过下表的宏定义完成:

宏定义 描述
_IOC_DIR(nr) 解析命令的传输方向
_IOC_TYPE(nr) 解析命令类型
_IOC_NR(nr) 解析命令序号
_IOC_SIZE(nr) 解析参数大小

  如果解析发现命令出错,可以返回-ENOTTY,如:

if (_IOC_TYPE(cmd) != LED_IOC_MAGIC) {
	return -ENOTTY;
}
if (_IOC_NR(cmd) >= LED_IOC_MAXNR) {
	return -ENOTTY;
}

二、编写程序

  该驱动范例基于 EPC-28x 工控主板(处理器为 i.MX28x)。该主板硬件提供一个 Error指示灯,由处理器的 GPIO1_23 控制,低电平点亮。EPC-28x 的 BSP 实现了 GPIO 底层接口移植,可直接调用 gpio_direction_output、gpio_set_value 等操作接口函数。
  i.MX28 系列处理器的 IO 端口分为 7 个 BANK, GPIO 序号=BANK x 32 + N,例如GPIO1_23 的排列序号是 1 x 32 + 23,等于 55。
  此外, i.MX28x 处理器的引脚通常都具有多种功能,将某个引脚用作 GPIO 功能,需要设置引脚功能复用。这部分代码在这个范例中没有体现出来,需要在 BSP 代码中提前设置好。

程序清单 0.1 - 头文件

  对 LED 设备驱动,采用 ioctl 方法实现,首先需要定义 ioctl 的操作命令。 程序清单 0.1所示代码定义了 2 个操作命令: LED_ON 和 LED_OFF。

#ifndef _LED_DRV_H
#define _LED_DRV_H

#define LED_IOC_MAGIC 'L'
#define LED_ON _IO(LED_IOC_MAGIC, 0)
#define LED_OFF _IO(LED_IOC_MAGIC, 1)

#define LED_IOCTL_MAXNR 2

#endif /*_LED_DRV_H*/

程序清单 0.2 - 驱动程序

  根据 LED 的设备特点,驱动只需实现 open、 release 和 ioctl 三种方法,因此在 fops 中不再为其它不用实现的成员赋值。由于涉及具体的硬件平台,需要操作硬件资源,因此在驱动中必须包含平台相关的头文件,如 hardware.h、 gpio.h 等。

  在 open 和 release 方法中,将 LED 对应的 GPIO 端口设置为输出并且使 LED 处于熄灭状态。在 ioctl 中根据传入的命令,分别使 LED 控制端口输入高电平或者低电平,达到点亮和熄灭 LED 的目的。

  另外,由于 ioctl 在不同版本可能存在变化,故要做好兼容处理。在这个驱动范例中,也实现了这一点,参考代码第 39~43 行和第 74~78 行。要实现内核版本识别,需要在头文件中包<linux/version.h>。程序清单 0.2 是 LED 驱动的一个实现范例,驱动不复杂,不再做过多讲解。

#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/version.h>

#include <asm/mach/arch.h>
#include <mach/hardware.h>
#include <mach/gpio.h>
#include <asm/gpio.h>

#include "led_drv.h"

static int major;
static int minor;
struct cdev *led; /* cdev 数据结构 */
static dev_t devno; /* 设备编号 */
static struct class *led_class;

#define DEVICE_NAME "led"

#define GPIO_LED_PIN_NUM 55 /* gpio 1_23 */

static int led_open(struct inode *inode, struct file *file )
{
	try_module_get(THIS_MODULE);
	gpio_direction_output(GPIO_LED_PIN_NUM, 1);
	return 0;
}

static int led_release(struct inode *inode, struct file *file )
{
	module_put(THIS_MODULE);
	gpio_direction_output(GPIO_LED_PIN_NUM, 1);
	return 0;
}

#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,6,36)
	int led_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
#else
	static int led_ioctl(struct inode *inode, struct file *file, unsigned int cmd, unsigned long arg)
#endif
{
	if (_IOC_TYPE(cmd) != LED_IOC_MAGIC) {
		return -ENOTTY;
	}

	if (_IOC_NR(cmd) > LED_IOCTL_MAXNR) {
		return -ENOTTY;
	}

	switch(cmd) {
	case LED_ON:
		gpio_set_value(GPIO_LED_PIN_NUM, 0);
		break;
	
	case LED_OFF:
		gpio_set_value(GPIO_LED_PIN_NUM, 1);
		break;
	
	default:
		gpio_set_value(27, 0);
		break;
	}
	
	return 0;
}

struct file_operations led_fops = {
	.owner = THIS_MODULE,
	.open = led_open,
	.release = led_release,
#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,6,36)
	unlocked_ioctl = led_ioctl
#else
	ioctl = led_ioctl
#endif
};

static int __init led_init(void)
{
	int ret;
	
	gpio_free(GPIO_LED_PIN_NUM);
	if (gpio_request(GPIO_LED_PIN_NUM, "led_run")) {
		printk("request %s gpio faile \n", "led_run");
		return -1;
	}

	ret = alloc_chrdev_region(&devno, minor, 1, "led"); /* 从系统获取主设备号 */
	major = MAJOR(devno);
	if (ret < 0) {
		printk(KERN_ERR "cannot get major %d \n", major);
		return -1;
	}

	led = cdev_alloc(); /* 分配 led 结构 */
	if (led != NULL) {
		cdev_init(led, &led_fops); /* 初始化 led 结构 */
		led->owner = THIS_MODULE;
		if (cdev_add(led, devno, 1) != 0) { /* 增加 led 到系统中 */
		printk(KERN_ERR "add cdev error!\n");
		goto error;
	}
	} else {
		printk(KERN_ERR "cdev_alloc error!\n");
		return -1;
	}

	led_class = class_create(THIS_MODULE, "led_class");
	if (IS_ERR(led_class)) {
		printk(KERN_INFO "create class error\n");
		return -1;
	}

	device_create(led_class, NULL, devno, NULL, "led");
	return 0;

error:
	unregister_chrdev_region(devno, 1); /* 释放已经获得的设备号 */
	return ret;
}

static void __exit led_exit(void)
{
	cdev_del(led); /* 移除字符设备 */
	unregister_chrdev_region(devno, 1); /* 释放设备号 */
	device_destroy(led_class, devno);
	class_destroy(led_class);
}

module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");

程序清单 0.3 - 应用程序

  驱动编写完成后,还需编写一个测试程序,用来测试驱动的正确性。 程序清单 0.3 是一个简单的测试程序。打开设备后,通过 ioctl 方法, 控制 LED 闪烁 3 次。

  注意, 如果驱动定义了 ioctl 命令,则应用程序必须有这些命令的定义,通常做法是包含驱动头文件。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <errno.h>
#include <fcntl.h>
#include "../led_drv.h"

#define DEV_NAME "/dev/led"

int main(int argc, char *argv[])
{
	int i;
	int fd = 0;
	
	fd = open (DEV_NAME, O_RDONLY);
	if (fd < 0) {
		perror("Open "DEV_NAME" Failed!\n");
		exit(1);
	}
	
	for (i=0; i<3; i++) {
		ioctl(fd, LED_ON);
		sleep(1);
		ioctl(fd, LED_OFF);
		sleep(1);
	}
	
	close(fd);
	return 0;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章