69、Linux字符设备驱动实战

Linux字符设备驱动实战

一、核心概念回顾(先理清基础)

1.1 字符设备驱动是什么?

字符设备驱动是操作字符设备的内核程序,数据按"字节流"顺序访问,核心作用是:

  • 向上给应用程序提供统一的文件操作接口(open/read/write/close);
  • 向下对接硬件,实现具体的硬件控制逻辑(本文为演示,暂用printk替代硬件操作)。

1.2 驱动实现三要素(你关注的核心)

  1. 硬件操作方法:实现open/read/write/close等函数,封装硬件操作逻辑;
  2. 设备号申请:设备号是驱动的"身份证"(32位,高12位主设备号+低20位次设备号),主设备号代表设备类型,次设备号区分同类设备;
  3. 驱动注册:将驱动的操作方法、设备号注册到内核,让内核识别并关联设备。

1.3 关键工具与命令

工具/命令 作用
arm-linux-gnueabihf-gcc 交叉编译器,编译ARM架构的应用程序
make zImage 编译Linux内核,将驱动模块打包进内核镜像
cat /proc/devices 查看已注册的驱动设备号(主设备号+设备名)
mknod 手动创建设备节点(应用程序通过设备节点访问驱动)
dmesg 查看内核打印信息(驱动中的printk输出)

二、驱动代码深度解析(你的demo_driver.c

你的demo_driver.c完美实现了字符设备驱动的三要素,我们逐部分拆解核心逻辑:

2.1 第一步:实现硬件操作方法(file_operations结构体)

驱动的核心是struct file_operations结构体,它定义了应用程序可调用的操作接口,对应代码中的:

c 复制代码
// 定义硬件操作方法(本文为演示,仅打印调试信息)
static int open(struct inode *inode, struct file *file) {
  printk("demo1_init open\n");  // 应用调用open时触发
  return 0;
}

static ssize_t read(struct file *file, char __user *buf, size_t size, loff_t *loff) {
  printk("demo1_init read\n");   // 应用调用read时触发
  return 0;
}

static ssize_t write(struct file *file, const char __user *buf, size_t size, loff_t *loff) {
  printk("demo1_init write\n");  // 应用调用write时触发
  return 0;
}

static int close(struct inode *inode, struct file *file) {
  printk("demo1_init close\n");  // 应用调用close时触发
  return 0;
}

// 绑定操作方法到file_operations结构体
static struct file_operations fops = {
  .owner = THIS_MODULE,  // 声明驱动所属模块(避免模块被意外卸载)
  .open = open,          // 关联open函数
  .read = read,          // 关联read函数
  .write = write,        // 关联write函数
  .release = close       // 关联close函数(应用close时实际调用release)
};

原理 :应用程序调用open("/dev/demo1", O_RDWR)时,内核会通过设备号找到对应的驱动,进而调用驱动中绑定的open函数。

2.2 第二步:申请设备号(静态申请方式)

设备号是驱动与内核的"约定凭证",你用了静态申请(指定固定主设备号255、次设备号0),代码解析:

c 复制代码
#define DEV_MAJOR 255  // 主设备号(自定义,需确保未被占用)
#define DEV_MINOR 0    // 次设备号
#define DEV_NAME "demo1"  // 设备名

static dev_t dev;  // 存储组合后的32位设备号

// 组合主次设备号:MKDEV(主设备号, 次设备号)
dev = MKDEV(DEV_MAJOR, DEV_MINOR);

// 静态申请设备号:参数(设备号, 设备数量, 设备名)
register_chrdev_region(dev, 1, DEV_NAME);

注意 :静态申请需确保主设备号未被其他驱动占用,可通过cat /proc/devices查看已占用的主设备号。若不想手动指定,也可使用alloc_chrdev_region动态申请(内核自动分配未占用的主设备号)。

2.3 第三步:向系统注册驱动(cdev结构体)

内核通过cdev结构体管理字符设备驱动,需将file_operations与设备号关联后注册到内核,代码解析:

c 复制代码
static struct cdev cdev;  // 字符设备驱动核心结构体

// 初始化cdev:绑定cdev与file_operations
cdev_init(&cdev, &fops);

// 注册cdev到内核:参数(cdev指针, 设备号, 设备数量)
cdev_add(&cdev, dev, 1);

驱动加载/卸载 :通过module_initmodule_exit宏定义驱动的加载与卸载入口:

c 复制代码
// 驱动加载时执行(insmod或内核启动时)
static int __init demo1_init(void) {
  // 设备号申请+驱动注册逻辑(上文已解析)
  printk("-------------------------------------- demo1_init\n");
  return 0;
}

// 驱动卸载时执行(rmmod)
static void __exit demo1_exit(void) {
  cdev_del(&cdev);  // 从内核删除cdev
  unregister_chrdev_region(dev, 1);  // 释放设备号
  printk("-------------------------------------- demo1_exit\n");
}

module_init(demo1_init);  // 注册加载入口
module_exit(demo1_exit);  // 注册卸载入口

三、应用程序解析(你的main.c

应用程序通过"设备节点"(/dev/demo1)访问驱动,本质是调用驱动的file_operations接口,代码逻辑:

c 复制代码
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>

int main(int argc, char *argv[]) {
  // 打开设备节点:对应驱动的open函数
  int fd = open("/dev/demo1", O_RDWR);
  if (fd < 0) {
    perror("open demo1 failed");  // 打开失败(如设备节点未创建)
    return 1;
  }

  char buf[10] = {0};
  read(fd, buf, sizeof buf);  // 读驱动:对应驱动的read函数
  write(fd, buf, sizeof buf); // 写驱动:对应驱动的write函数
  close(fd);                  // 关闭驱动:对应驱动的close函数

  return 0;
}

应用与驱动的交互流程
app: open() → 内核: sys_open() → 驱动: open() → 内核返回文件描述符fd → app: read/write/close() → 驱动: 对应函数执行

四、完整实操流程(从编译到验证)

你已经完成了代码编写和应用编译,接下来按以下步骤完成驱动编译、烧录与验证:

4.1 步骤1:将驱动添加到内核(关键!)

要让驱动随内核启动加载,需将demo_driver.c添加到内核源码,修改对应的MakefileKconfig(参考之前的内核编译笔记):

  1. demo_driver.c拷贝到内核源码的drivers/char/目录(字符设备驱动默认目录);

  2. 修改drivers/char/Makefile,添加一行:

    makefile 复制代码
    obj-$(CONFIG_DEMO_DRIVER) += demo_driver.o
  3. 修改drivers/char/Kconfig,添加驱动配置选项:

    kconfig 复制代码
    config DEMO_DRIVER
        tristate "Demo Character Device Driver"
        help
          This is a demo character device driver for IMX6ULL.
  4. 执行menuconfig启用驱动:

    bash 复制代码
    make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- menuconfig

    找到"Character devices" → 勾选"Demo Character Device Driver"(选Y,编译进内核),保存退出。

4.2 步骤2:编译内核生成zImage

bash 复制代码
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- all -j16

编译成功后,在arch/arm/boot/目录下生成包含demo驱动的zImage镜像。

4.3 步骤3:烧录内核并启动开发板

zImage和对应的设备树(imx6ull.dtb)通过TFTP下载到开发板,或烧录到SD卡,启动开发板。

4.4 步骤4:验证驱动是否注册成功

  1. 查看设备号:在开发板终端执行,若能看到255 demo1,说明驱动注册成功:

    bash 复制代码
    cat /proc/devices
  2. 创建设备节点:应用程序通过设备节点访问驱动,执行以下命令创建(主次设备号需与驱动一致):

    bash 复制代码
    mknod /dev/demo1 c 255 0
    • c:表示字符设备;
    • 255:主设备号;
    • 0:次设备号。

4.5 步骤5:运行应用程序验证

  1. 将编译好的demo1app通过NFS拷贝到开发板(参考之前的NFS挂载笔记);

  2. 运行应用程序:

    bash 复制代码
    ./demo1app
  3. 查看驱动打印信息:执行dmesg,能看到以下输出,说明应用与驱动交互成功:

    复制代码
    -------------------------------------- demo1_init
    demo1_init open
    demo1_init read
    demo1_init write
    demo1_init close

五、常见问题排查

5.1 执行cat /proc/devices看不到demo1

  • 驱动未编译进内核:检查menuconfig是否启用了DEMO_DRIVER,重新编译内核;
  • 主设备号冲突:修改DEV_MAJOR为未被占用的数值(如240),重新编译。

5.2 应用open("/dev/demo1")失败

  • 未创建设备节点:执行mknod /dev/demo1 c 255 0
  • 设备节点权限不足:执行chmod 777 /dev/demo1开放权限。

5.3 dmesg看不到printk输出

  • 内核打印级别限制:执行echo 8 > /proc/sys/kernel/printk,开放所有级别打印。
相关推荐
2501_941982052 小时前
企微自动化开发:安全与效率的平衡术
数据库·mysql·企业微信
阿里-于怀2 小时前
Kubernetes 官方再出公告,强调立即迁移 Ingress NGINX
java·大数据·数据库·ingress nginx
女王大人万岁2 小时前
Go语言JSON标准库(encoding/json):功能解析与实战指南
服务器·开发语言·后端·golang·json
TangDuoduo00052 小时前
【Linux下LED基础设备驱动】
linux·驱动开发
devmoon2 小时前
30秒一键连接Polkadot区块链网络和测试网
网络·web3·区块链·智能合约·polkadot
玄同7652 小时前
数据库全解析:从关系型到向量数据库,LLM 开发中的选型指南
数据库·人工智能·知识图谱·milvus·知识库·向量数据库·rag
开开心心就好2 小时前
图片校正漂白工具永久免费,矫正实时预览
网络·人工智能·windows·计算机视觉·计算机外设·电脑·excel
AAAAA92402 小时前
物联网海外网络摄像头市场分析:技术、合规与商业模式新趋势
网络·物联网
cyber_两只龙宝2 小时前
haproxy--使用socat工具实现对haproxy权重配置的热更新
linux·运维·负载均衡·haproxy·socat