1. ioctl相关知识
在字符设备驱动中,除了通过 read 和 write 进行数据读写外,还经常需要向设备发送控制命令,例如设置波特率、读取设备状态、复位设备等。这些控制操作通过 ioctl 系统调用完成。ioctl 是 Input/Output Control 的缩写,用于向设备发送控制命令并传递参数。
ioctl 系统调用与驱动接口
用户空间通过 ioctl 系统调用访问设备,其原型如下:
c
int ioctl(int fd, unsigned long request, ...);
fd:文件描述符,即open设备节点时返回的描述符。request:请求命令号,用于标识要执行的控制操作。...:可变参数,通常是一个无符号长整型参数,用于传递数据或指针。- 返回值:成功返回0(或非负值),失败返回 -1 并设置
errno。
在内核驱动中,ioctl 系统调用最终会映射到 struct file_operations 中的 unlocked_ioctl 函数。其原型如下:
c
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位,表示传递参数的数据大小(以字节为单位)。
内核提供的常用宏如下:
c
#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):定义双向数据传输的命令,如既写入参数又读取结果。
示例:
c
#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> 中声明:
c
int access_ok(int type, const void *addr, unsigned long size);
type:访问类型,通常使用VERIFY_READ(只读)或VERIFY_WRITE(可写)。addr:要验证的用户空间地址。size:要访问的字节数。- 返回值:地址合法返回非0,非法返回0。
示例:
c
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:
c
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> 头文件,并定义与驱动一致的命令号。示例代码如下:
c
#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 引脚之前,驱动必须先向内核申请该引脚的独占使用权,避免多个驱动同时操作同一个引脚造成冲突。内核提供的申请函数如下:
c
#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表示已被占用)。
示例:
c
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 后,还需要配置引脚的工作模式,包括方向(输入/输出)、初始电平、以及是否使用内部上拉/下拉电阻等。内核提供了一组函数来完成这些配置:
c
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:
c
/* 配置为输出,初始电平为 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 亮灭、继电器通断等。相关函数说明如下:
c
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)。该函数同样可在原子上下文中调用。
示例:读取按键状态并打印:
c
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 时,必须释放之前申请的引脚,以便其他驱动或模块可以重新使用。释放函数如下:
c
void gpio_free(unsigned gpio);
示例:在驱动退出函数中释放 GPIO:
c
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:
c
#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 的电平:
c
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 并注销设备:
c
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:
c
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。示例代码如下:
c
#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;
}
编译并运行测试程序:
c
# 编译应用
gcc -o led_test led_test.c
点亮 0 号 LED
./led_test 0 1
熄灭 0 号 LED
./led_test 0 0
点亮 2 号 LED
./led_test 2 1
3.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」的链路。