【嵌入式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;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章