4. 实现Camera设备的IOCTL接口框架

任务说明

本案例为Linux内核V4L2摄像头驱动实现ioctl接口框架。目标是为Camera设备定义ioctl命令集,并实现基本命令分发,包括查询设备能力(QUERYCAP)与设置图像格式(S/FMT)。通过本案例,你将得到一个可直接编译运行的最小字符设备驱动,支持用户空间通过标准ioctl调用完成设备交互。

环境与依赖

  • Linux内核源码树(建议5.10+),已配置内核头文件
  • 内核模块编译工具(make, gcc)
  • 开发板或虚拟机,具备root权限加载模块

核心实现

定义三个ioctl命令:查询驱动能力、获取当前格式、设置新格式。命令编号使用V4L2标准宏,确保与用户空间头文件<linux/videodev2.h>兼容。驱动内部维护一个格式状态结构,通过互斥锁保护并发访问。

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/uaccess.h>
#include <linux/mutex.h>
#include <linux/videodev2.h>
#include <linux/slab.h>

#define CAMERA_DEV_NAME "camera_ioctl"
#define CAMERA_CLASS_NAME "camera_class"

static dev_t camera_dev_num;
static struct cdev camera_cdev;
static struct class *camera_class;
static struct device *camera_device;

struct camera_format {
    __u32 width;
    __u32 height;
    __u32 pixelformat;
};

static struct camera_format current_fmt = {
    .width = 640,
    .height = 480,
    .pixelformat = V4L2_PIX_FMT_YUYV,
};

static DEFINE_MUTEX(fmt_lock);

static int camera_open(struct inode *inode, struct file *file)
{
    return 0;
}

static int camera_release(struct inode *inode, struct file *file)
{
    return 0;
}

static long camera_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
    void __user *user_arg = (void __user *)arg;
    int ret = 0;

    switch (cmd) {
    case VIDIOC_QUERYCAP: {
        struct v4l2_capability cap = {0};

        strscpy(cap.driver, "camera_ioctl_drv", sizeof(cap.driver));
        strscpy(cap.card, "Camera IOCTL Demo", sizeof(cap.card));
        strscpy(cap.bus_info, "platform:demo", sizeof(cap.bus_info));
        cap.version = KERNEL_VERSION(1, 0, 0);
        cap.capabilities = V4L2_CAP_VIDEO_CAPTURE | V4L2_CAP_STREAMING;

        if (copy_to_user(user_arg, &cap, sizeof(cap)))
            ret = -EFAULT;
        break;
    }
    case VIDIOC_G_FMT: {
        struct v4l2_format vfmt = {0};

        vfmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
        vfmt.fmt.pix.width = current_fmt.width;
        vfmt.fmt.pix.height = current_fmt.height;
        vfmt.fmt.pix.pixelformat = current_fmt.pixelformat;
        vfmt.fmt.pix.field = V4L2_FIELD_NONE;
        vfmt.fmt.pix.bytesperline = current_fmt.width * 2;
        vfmt.fmt.pix.sizeimage = current_fmt.width * current_fmt.height * 2;

        if (copy_to_user(user_arg, &vfmt, sizeof(vfmt)))
            ret = -EFAULT;
        break;
    }
    case VIDIOC_S_FMT: {
        struct v4l2_format vfmt;

        if (copy_from_user(&vfmt, user_arg, sizeof(vfmt))) {
            ret = -EFAULT;
            break;
        }

        if (vfmt.type != V4L2_BUF_TYPE_VIDEO_CAPTURE) {
            ret = -EINVAL;
            break;
        }

        mutex_lock(&fmt_lock);
        current_fmt.width = vfmt.fmt.pix.width;
        current_fmt.height = vfmt.fmt.pix.height;
        current_fmt.pixelformat = vfmt.fmt.pix.pixelformat;
        mutex_unlock(&fmt_lock);

        vfmt.fmt.pix.bytesperline = current_fmt.width * 2;
        vfmt.fmt.pix.sizeimage = current_fmt.width * current_fmt.height * 2;
        vfmt.fmt.pix.field = V4L2_FIELD_NONE;

        if (copy_to_user(user_arg, &vfmt, sizeof(vfmt)))
            ret = -EFAULT;
        break;
    }
    default:
        ret = -ENOTTY;
        break;
    }

    return ret;
}

static struct file_operations camera_fops = {
    .owner = THIS_MODULE,
    .open = camera_open,
    .release = camera_release,
    .unlocked_ioctl = camera_ioctl,
};

static int __init camera_init(void)
{
    int ret;

    ret = alloc_chrdev_region(&camera_dev_num, 0, 1, CAMERA_DEV_NAME);
    if (ret < 0)
        return ret;

    cdev_init(&camera_cdev, &camera_fops);
    ret = cdev_add(&camera_cdev, camera_dev_num, 1);
    if (ret)
        goto err_cdev;

    camera_class = class_create(THIS_MODULE, CAMERA_CLASS_NAME);
    if (IS_ERR(camera_class)) {
        ret = PTR_ERR(camera_class);
        goto err_class;
    }

    camera_device = device_create(camera_class, NULL, camera_dev_num,
                                  NULL, CAMERA_DEV_NAME);
    if (IS_ERR(camera_device)) {
        ret = PTR_ERR(camera_device);
        goto err_device;
    }

    pr_info("camera_ioctl: initialized (major=%d, minor=%d)\n",
            MAJOR(camera_dev_num), MINOR(camera_dev_num));
    return 0;

err_device:
    class_destroy(camera_class);
err_class:
    cdev_del(&camera_cdev);
err_cdev:
    unregister_chrdev_region(camera_dev_num, 1);
    return ret;
}

static void __exit camera_exit(void)
{
    device_destroy(camera_class, camera_dev_num);
    class_destroy(camera_class);
    cdev_del(&camera_cdev);
    unregister_chrdev_region(camera_dev_num, 1);
    pr_info("camera_ioctl: exited\n");
}

module_init(camera_init);
module_exit(camera_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Kernel Camera Course");
MODULE_DESCRIPTION("Camera device ioctl interface framework");

编译与加载

# Makefile
obj-m += camera_ioctl.o

# 编译命令
make -C /lib/modules/$(uname -r)/build M=$(pwd) modules

# 加载模块
sudo insmod camera_ioctl.ko

# 查看设备节点(应生成 /dev/camera_ioctl)
ls -l /dev/camera_ioctl

验证方法

编写用户空间测试程序,依次执行以下操作:

  1. 打开/dev/camera_ioctl设备节点
  2. 调用VIDIOC_QUERYCAP,检查返回的driver字段是否为camera_ioctl_drv
  3. 调用VIDIOC_S_FMT设置格式为1280x720,像素格式V4L2_PIX_FMT_NV12
  4. 调用VIDIOC_G_FMT读取当前格式,确认宽高与像素格式与设置一致

通过条件:所有ioctl调用返回0,且G_FMT读回的值与S_FMT设置的值完全匹配。若返回-ENOTTY,说明命令分发异常;若返回-EFAULT,说明用户空间地址访问错误。

常见问题

  • 设备节点未生成:检查dmesg输出,确认模块加载时是否报错,以及/dev目录权限。
  • ioctl返回-ENOTTY:确认用户空间头文件版本与内核命令编号一致,避免_IOR/_IOW类型不匹配。
  • 并发访问冲突:S_FMT与G_FMT同时调用时,互斥锁确保格式状态一致性,测试时可多线程验证。