Linux字符设备驱动实战:ioctl接口与GPIO操作指南

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 函数时,通常需要完成以下步骤:

  1. 根据 cmd 判断要执行的操作,使用 switch 语句分发。
  2. 对于需要传递数据的命令,使用 copy_to_user、copy_from_user、get_user 或 put_user 完成用户空间与内核空间的数据交换。
  3. 执行对应的设备控制操作。
  4. 对于未识别的命令,返回 -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」的链路。
相关推荐
silvamux_1 小时前
OpenAI Python SDK v1 迁移实战
开发语言·python
旋生万物1 小时前
螺旋数不是玄学:Python 实现广义复数与等角螺线
开发语言·人工智能·python·云原生·aigc·软件工程·后端开发
ZoeLandia1 小时前
TypeScript 07:工程化——import type、声明文件与 tsconfig
开发语言·前端·typescript
Zenova EdgeOS1 小时前
Linux Nginx 反向代理实战:工业边缘 API 接入与安全加固
linux·nginx·安全·边缘计算·工业网关
小小程序猴11 小时前
用工程视角建模企业AI培训周期:三段账本估算器(Python实现)
开发语言·人工智能·python
小白鸽i1 小时前
【Linux】已安装的软件如何重新构建deb安装包?
linux·运维·服务器
其实防守也摸鱼1 小时前
CTF Web 6 题全解 Writeup
linux·sql·学习·算法·安全·ctf·xss
qq_401700411 小时前
CAN 开源库大全(嵌入式/上位机,适配STM32、Linux、Qt,附带Github地址)
linux·stm32·开源
开开心心_Every1 小时前
文件查重软件批量删除重复文件释放空间
java·开发语言·c#·计算机外设·ocr·excel·perl