1. ioctl相关知识
在字符设备驱动中,除了通过read和write进行数据读写外,还经常需要向设备发送控制命令,例如设置波特率、读取设备状态、复位设备等。这些控制操作通过ioctl系统调用完成。ioctl 是 Input/Output Control 的缩写,用于向设备发送控制命令并传递参数。
ioctl 系统调用与驱动接口
用户空间通过ioctl系统调用访问设备,其原型如下:
int ioctl(int fd, unsigned long request, ...);fd:文件描述符,即open设备节点时返回的描述符。request:请求命令号,用于标识要执行的控制操作。...:可变参数,通常是一个无符号长整型参数,用于传递数据或指针。- 返回值:成功返回0(或非负值),失败返回 -1 并设置
errno。
在内核驱动中,ioctl系统调用最终会映射到struct file_operations中的unlocked_ioctl函数。其原型如下:
long (*unlocked_ioctl) (struct file *filp, unsigned int cmd, unsigned long arg);filp:指向struct file的指针,即当前打开的文件实例。cmd:命令号,由用户空间传入,用于区分不同的控制操作。arg:参数,可以是整数值,也可以是指向用户空间数据的指针。- 返回值:成功返回0,失败返回负的错误码。
1.1 ioctl命令号编码规则
为了防止对错误的设备使用正确的命令,命令号应该在系统范围内唯一。如果不同设备驱动使用相同的命令号,那么当应用程序向错误的设备发送命令时,该设备可能无法识别,甚至可能执行错误的操作。为此,Linux 内核建议把一个命令号分为四个段,每个段占不同的 bit 位,通过方向、类型、编号和大小四个维度来唯一标识一条命令,从而降低命令号冲突的风险。
Linux 内核使用一个32位的整数来编码 ioctl 命令号,通过<linux/ioctl.h>中提供的一组宏来构造和解析命令号。命令号由四个部分组成:
- dir(方向):占用第30、31位,表示数据传输方向,取值包括
_IOC_NONE(无数据传输)、_IOC_READ(从设备读)、_IOC_WRITE(向设备写)以及两者的组合。 - type(类型):占用第8到15位,通常使用一个字符(如
'M')来标识设备类型,用于区分不同设备的命令。 - nr(编号):占用第0到7位,表示命令的序号,用于区分同一设备下的不同命令。
- size(大小):占用第16到29位,表示传递参数的数据大小(以字节为单位)。
内核提供的常用宏如下:
#define _IO(type, nr) _IOC(_IOC_NONE, (type), (nr), 0) #define _IOR(type, nr, size) _IOC(_IOC_READ, (type), (nr), sizeof(size)) #define _IOW(type, nr, size) _IOC(_IOC_WRITE, (type), (nr), sizeof(size)) #define _IOWR(type, nr, size) _IOC(_IOC_READ | _IOC_WRITE, (type), (nr), sizeof(size))_IO(type, nr):定义无数据传输的命令,如设备复位、启动等。_IOR(type, nr, size):定义从设备读取数据的命令,如读取设备状态。_IOW(type, nr, size):定义向设备写入数据的命令,如设置设备参数。_IOWR(type, nr, size):定义双向数据传输的命令,如既写入参数又读取结果。
示例:
#define MY_DEVICE_TYPE 'M' #define MY_IOCTL_RESET _IO(MY_DEVICE_TYPE, 0) #define MY_IOCTL_GET_STATE _IOR(MY_DEVICE_TYPE, 1, int) #define MY_IOCTL_SET_SPEED _IOW(MY_DEVICE_TYPE, 2, int) #define MY_IOCTL_EXCHANGE _IOWR(MY_DEVICE_TYPE, 3, struct my_data)1.2 ioctl函数实现
在驱动中实现unlocked_ioctl函数时,通常需要完成以下步骤:
- 根据
cmd判断要执行的操作,使用switch语句分发。 - 对于需要传递数据的命令,使用
copy_to_user、copy_from_user、get_user或put_user完成用户空间与内核空间的数据交换。 - 执行对应的设备控制操作。
- 对于未识别的命令,返回
-ENOTTY(表示不支持的 ioctl 命令)。
在unlocked_ioctl中,arg参数既可以是一个整数,也可以是一个指向用户空间数据的指针,具体含义由命令号决定。如果传递的是一个整数,可以直接使用;如果是一个指针,就必须小心处理。当用一个指针引用用户空间时,我们必须确保用户地址是合法的,可通过access_ok函数验证地址(而不是传递参数),该函数在<asm/uaccess.h>中声明:
int access_ok(int type, const void *addr, unsigned long size);type:访问类型,通常使用VERIFY_READ(只读)或VERIFY_WRITE(可写)。addr:要验证的用户空间地址。size:要访问的字节数。- 返回值:地址合法返回非0,非法返回0。
示例:
static long my_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) { int ret = 0; int value; switch (cmd) { case MY_IOCTL_RESET: /* 执行设备复位操作 */ printk("device reset\n"); break; case MY_IOCTL_GET_STATE: /* 读取设备状态并返回给用户空间 */ value = 1; /* 假设设备状态为1 */ ret = put_user(value, (int __user *)arg); if (ret != 0) { printk("put_user failed\n"); return ret; } break; case MY_IOCTL_SET_SPEED: /* 从用户空间读取参数并设置设备 */ ret = get_user(value, (int __user *)arg); if (ret != 0) { printk("get_user failed\n"); return ret; } printk("set speed to %d\n", value); break; default: /* 未识别的命令 */ return -ENOTTY; } return 0; }在struct file_operations中注册unlocked_ioctl:
static struct file_operations my_fops = { .owner = THIS_MODULE, .open = my_open, .release = my_release, .read = my_read, .write = my_write, .unlocked_ioctl = my_ioctl, };1.3 用户空间调用ioctl
用户空间的应用程序通过ioctl系统调用访问设备,需要包含<sys/ioctl.h>头文件,并定义与驱动一致的命令号。示例代码如下:
#include <stdio.h> #include <fcntl.h> #include <sys/ioctl.h> #define MY_DEVICE_TYPE 'M' #define MY_IOCTL_RESET _IO(MY_DEVICE_TYPE, 0) #define MY_IOCTL_GET_STATE _IOR(MY_DEVICE_TYPE, 1, int) #define MY_IOCTL_SET_SPEED _IOW(MY_DEVICE_TYPE, 2, int) int main(void) { int fd; int state; int speed = 9600; fd = open("/dev/my_char_dev", O_RDWR); if (fd < 0) { perror("open"); return -1; } /* 发送复位命令 */ ioctl(fd, MY_IOCTL_RESET); /* 设置设备参数 */ ioctl(fd, MY_IOCTL_SET_SPEED, &speed); /* 读取设备状态 */ ioctl(fd, MY_IOCTL_GET_STATE, &state); printf("device state: %d\n", state); close(fd); return 0; }使用 ioctl 的注意事项
- 命令号必须在内核驱动和用户空间程序中保持一致,否则会导致命令无法识别。
- 传递指针参数时,必须使用
copy_to_user、copy_from_user、get_user或put_user,不能直接解引用用户空间指针。 - 在解引用用户空间指针前,应使用
access_ok验证地址的合法性,避免非法地址导致内核崩溃。 - 对于未识别的命令,应返回
-ENOTTY,而不是返回0。 - 在
ioctl中传递结构体等复杂数据时,应使用copy_to_user和copy_from_user,而不是get_user和put_user。 - 命令号中的 type 字段应使用唯一的字符,避免与其他设备驱动冲突。
- 在较新的内核版本中,
ioctl系统调用映射到unlocked_ioctl,而不是旧的ioctl字段,编写驱动时应使用unlocked_ioctl。
2. GPIO驱动相关知识
GPIO(General Purpose Input/Output,通用输入/输出)是嵌入式 Linux 驱动开发中最基础也最常用的资源之一。通过 GPIO 驱动可以控制引脚的电平输出,也可以读取外部输入的电平状态,从而实现 LED 控制、按键检测、传感器读取等功能。在 Linux 内核中,GPIO 子系统提供了一套统一的接口,驱动开发者可以通过这些接口申请、配置和使用 GPIO 引脚。
2.1 申请GPIO
在使用一个 GPIO 引脚之前,驱动必须先向内核申请该引脚的独占使用权,避免多个驱动同时操作同一个引脚造成冲突。内核提供的申请函数如下:
#include <linux/gpio.h> int gpio_request(unsigned gpio, const char *label); void gpio_free(unsigned gpio);gpio:要申请的 GPIO 编号,通常由硬件原理图或设备树指定。label:为 GPIO 指定的名称,便于调试时识别,例如"led-red"。- 返回值:成功返回 0,失败返回负的错误码(如
-EBUSY表示已被占用)。
示例:
int ret; ret = gpio_request(led_gpio, "led-red"); if (ret < 0) { printk("failed to request gpio %d: %d\n", led_gpio, ret); return ret; }2.2 设置GPIO功能
申请到 GPIO 后,还需要配置引脚的工作模式,包括方向(输入/输出)、初始电平、以及是否使用内部上拉/下拉电阻等。内核提供了一组函数来完成这些配置:
int gpio_direction_input(unsigned gpio); int gpio_direction_output(unsigned gpio, int value); int gpio_set_value(unsigned gpio, int value); int gpio_get_value(unsigned gpio);gpio_direction_input(gpio):将引脚配置为输入模式,用于读取外部信号。gpio_direction_output(gpio, value):将引脚配置为输出模式,并同时设置初始电平(value为 0 或 1)。gpio_set_value(gpio, value):设置输出电平,仅在引脚已配置为输出时有效。gpio_get_value(gpio):读取当前引脚电平,返回 0 或 1。
示例:将 GPIO 配置为输出并点亮 LED:
/* 配置为输出,初始电平为 0(LED 灭) */ ret = gpio_direction_output(led_gpio, 0); if (ret < 0) { printk("failed to set gpio %d as output\n", led_gpio); gpio_free(led_gpio); return ret; } /* 输出高电平,点亮 LED */ gpio_set_value(led_gpio, 1);2.3 获取/设置GPIO电平状态
在实际驱动中,经常需要动态地读取或改变 GPIO 的电平状态。读取电平用于判断按键是否按下、传感器输出是高还是低;设置电平用于控制 LED 亮灭、继电器通断等。相关函数说明如下:
int gpio_get_value(unsigned gpio); void gpio_set_value(unsigned gpio, int value);gpio_get_value(gpio):读取指定 GPIO 的当前电平,返回 0(低电平)或 1(高电平)。该函数可在中断上下文或原子上下文中调用。gpio_set_value(gpio, value):将指定 GPIO 的输出电平设置为value(0 或 1)。该函数同样可在原子上下文中调用。
示例:读取按键状态并打印:
int key_state; key_state = gpio_get_value(key_gpio); printk("key state: %d\n", key_state); if (key_state == 1) { /* 按键按下,点亮 LED */ gpio_set_value(led_gpio, 1); } else { gpio_set_value(led_gpio, 0); }2.4 释放GPIO口
当驱动卸载或不再需要使用某个 GPIO 时,必须释放之前申请的引脚,以便其他驱动或模块可以重新使用。释放函数如下:
void gpio_free(unsigned gpio);示例:在驱动退出函数中释放 GPIO:
static void __exit my_gpio_exit(void) { gpio_free(led_gpio); gpio_free(key_gpio); printk("GPIO driver exited\n"); }需要注意的是,gpio_free只能释放之前通过gpio_request成功申请的 GPIO,重复释放或释放未申请的引脚会导致未定义行为。在驱动的probe函数中申请 GPIO,在remove函数中释放,是推荐的标准做法。
3. 练习
下面通过一个完整的练习,把前面介绍的 ioctl 和 GPIO 驱动知识综合起来:编写一个字符设备驱动,操作开发板上的 LED 灯,并向上层应用提供操作接口,让应用能够根据需求点亮或熄灭对应的灯。
3.1 需求分析
本练习要实现以下功能:
- 驱动申请开发板上的若干 GPIO 引脚,分别对应不同的 LED 灯。
- 通过
ioctl命令,让应用可以指定点亮或熄灭某一盏灯。 - 应用通过
open打开设备节点,再通过ioctl发送控制命令。
3.2 驱动代码实现
首先定义命令号和 LED 编号。这里假设开发板上有 4 个 LED,分别对应 GPIO 编号 0 到 3:
#include <linux/init.h> #include <linux/module.h> #include <linux/fs.h> #include <linux/cdev.h> #include <linux/device.h> #include <linux/gpio.h> #include <linux/uaccess.h> #define LED_DEVICE_TYPE 'L' #define LED_IOCTL_ON _IOW(LED_DEVICE_TYPE, 0, int) /* 点亮指定 LED */ #define LED_IOCTL_OFF _IOW(LED_DEVICE_TYPE, 1, int) /* 熄灭指定 LED */ #define LED_NUM 4 static int led_gpios[LED_NUM] = { 0, 1, 2, 3 }; /* 根据实际硬件修改 */接着实现open、release和unlocked_ioctl函数。在unlocked_ioctl中,根据命令号判断是点亮还是熄灭,并通过gpio_set_value设置对应 LED 的电平:
static int led_open(struct inode *inode, struct file *filp) { return 0; } static int led_release(struct inode *inode, struct file *filp) { return 0; } static long led_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) { int led_num; /* 从用户空间读取 LED 编号 */ if (get_user(led_num, (int __user *)arg) != 0) { return -EFAULT; } /* 校验 LED 编号是否合法 */ if (led_num < 0 || led_num >= LED_NUM) { return -EINVAL; } switch (cmd) { case LED_IOCTL_ON: gpio_set_value(led_gpios[led_num], 1); /* 点亮 */ break; case LED_IOCTL_OFF: gpio_set_value(led_gpios[led_num], 0); /* 熄灭 */ break; default: return -ENOTTY; } return 0; }在模块加载函数中,申请 GPIO 并注册字符设备;在卸载函数中,释放 GPIO 并注销设备:
static dev_t led_dev; static struct cdev led_cdev; static struct class *led_class; static int __init led_init(void) { int ret; int i; /* 申请并配置 GPIO */ for (i = 0; i < LED_NUM; i++) { ret = gpio_request(led_gpios[i], "led"); if (ret < 0) { printk("failed to request gpio %d\n", led_gpios[i]); goto err_gpio; } gpio_direction_output(led_gpios[i], 0); /* 初始为熄灭 */ } /* 注册字符设备 */ ret = alloc_chrdev_region(led_dev, 0, 1, "led"); if (ret < 0) { goto err_region; } cdev_init(led_cdev, led_fops); led_cdev.owner = THIS_MODULE; ret = cdev_add(led_cdev, led_dev, 1); if (ret < 0) { goto err_cdev; } /* 创建设备节点 /dev/led */ led_class = class_create(THIS_MODULE, "led"); if (IS_ERR(led_class)) { ret = PTR_ERR(led_class); goto err_class; } device_create(led_class, NULL, led_dev, NULL, "led"); printk("LED driver loaded\n"); return 0; err_class: cdev_del(led_cdev); err_cdev: unregister_chrdev_region(led_dev, 1); err_region: err_gpio: while (i > 0) { i--; gpio_free(led_gpios[i]); } return ret; } static void __exit led_exit(void) { int i; device_destroy(led_class, led_dev); class_destroy(led_class); cdev_del(led_cdev); unregister_chrdev_region(led_dev, 1); for (i = 0; i < LED_NUM; i++) { gpio_free(led_gpios[i]); } printk("LED driver unloaded\n"); } module_init(led_init); module_exit(led_exit); MODULE_LICENSE("GPL");最后,定义struct file_operations并注册unlocked_ioctl:
static struct file_operations led_fops = { .owner = THIS_MODULE, .open = led_open, .release = led_release, .unlocked_ioctl = led_ioctl, };3.3 应用层测试程序
应用层程序打开设备节点/dev/led,通过ioctl命令点亮或熄灭指定的 LED。示例代码如下:
#include <stdio.h> #include <fcntl.h> #include <sys/ioctl.h> #define LED_DEVICE_TYPE 'L' #define LED_IOCTL_ON _IOW(LED_DEVICE_TYPE, 0, int) #define LED_IOCTL_OFF _IOW(LED_DEVICE_TYPE, 1, int) int main(int argc, char *argv[]) { int fd; int led_num; int on; if (argc != 3) { printf("usage: %s <led_num> 0|1\n", argv[0]); printf(" led_num: 0 ~ 3\n"); printf(" 0: 熄灭, 1: 点亮\n"); return -1; } led_num = atoi(argv[1]); on = atoi(argv[2]); fd = open("/dev/led", O_RDWR); if (fd < 0) { perror("open"); return -1; } if (on) { ioctl(fd, LED_IOCTL_ON, &led_num); } else { ioctl(fd, LED_IOCTL_OFF, &led_num); } close(fd); return 0; }编译并运行测试程序:
# 编译应用 gcc -o led_test led_test.c 点亮 0 号 LED ./led_test 0 1 熄灭 0 号 LED ./led_test 0 0 点亮 2 号 LED ./led_test 2 13.4 练习要点总结
- 驱动中通过
gpio_request申请 GPIO,通过gpio_direction_output配置为输出模式,通过gpio_set_value控制电平。 - 应用与驱动之间通过
ioctl命令传递 LED 编号,命令号使用_IOW宏定义,方向为写。 - 在
unlocked_ioctl中,使用get_user从用户空间读取 LED 编号,避免直接解引用用户空间指针。 - 模块加载时申请 GPIO 并注册设备,卸载时释放 GPIO 并注销设备,保证资源正确回收。
4.总结
本文围绕字符设备驱动中的ioctl控制接口和 GPIO 引脚操作展开,核心内容可归纳为以下四点:
- ioctl 命令号编码:命令号在系统范围内唯一,由方向、类型、编号和大小四个字段组成,通过
_IO、_IOR、_IOW、_IOWR宏构造,避免不同设备间命令冲突。 - ioctl 驱动实现:在
unlocked_ioctl中按cmd分发操作,传递指针参数时使用copy_to_user、copy_from_user、get_user、put_user完成内核与用户空间的数据交换,并用access_ok校验地址合法性。 - GPIO 使用流程:遵循「申请—配置—使用—释放」的标准流程,即
gpio_request申请引脚、gpio_direction_input或gpio_direction_output配置方向、gpio_get_value或gpio_set_value读写电平、gpio_free释放引脚。 - 综合练习:通过 LED 驱动实例,将 ioctl 命令分发与 GPIO 电平控制结合,应用层用
_IOW命令号指定点亮或熄灭某一盏灯,完整走通「驱动申请 GPIO—注册设备—应用调用 ioctl—控制 LED」的链路。