一、前言

本文基于韦东山 IMX6ULL 开发板,从零实现最简 Linux 字符设备驱动(Hello 驱动),包含:原理流程、架构说明、内核函数详解、全套带注释源码、Makefile 配置、编译报错排查、开发板加载测试全流程,直接适配 CSDN 博客发布。

二、整体开发环境

项目 详细信息
硬件 韦东山 IMX6ULL 开发板
开发主机 Ubuntu 虚拟机
内核源码 /home/book/100ask_imx6ull-sdk/Linux-4.9.88
交叉编译器 gcc-linaro 6.2.1(arm-linux-gnueabihf)
调试方式 ADB/NFS 文件传输 + 串口终端

三、驱动整体运行架构 & 流程图解

1、三层交互架

用户空间(hello_drv_test)
        ↓ open/read/write
内核VFS虚拟文件系统
        ↓ 匹配调用
驱动层(hello_drv.c 内核模块)
        ↓
/dev/hello 设备节点(访问入口)

2、驱动加载流程

  1. insmod hello_drv.ko 加载模块
  2. 自动执行module_init入口函数
  3. 注册字符设备 → 创建设备 class → 自动生成 /dev/hello 节点
  4. 等待用户程序读写访问

3、读写交互流程

  1. 应用 open /dev/hello → 触发驱动open函数
  2. 应用 write 写入字符串 → copy_from_user拷贝到内核缓冲区
  3. 应用 read 读取数据 → copy_to_user把内核数据发给应用
  4. 应用 close → 触发驱动 release 函数

四、核心内核 API 详解(新手必看)

1、注册 / 注销字符设备

register_chrdev 注册驱动
int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops);
  • major:主设备号,填 0 让内核自动分配
  • name:驱动名称,会展示在/proc/devices
  • fops:绑定 open/read/write 回调函数结构体
unregister_chrdev 注销驱动

卸载驱动时必须调用,释放内核资源。

2、自动创建设备节点相关

class_create / class_destroy

创建 / 销毁设备类,是自动生成/dev/xxx的前提。

device_create / device_destroy

根据设备类 + 设备号,自动在/dev目录生成设备文件。MKDEV(major,0):拼接主、次设备号。

3、内核 & 用户空间数据拷贝(重中之重)

copy_from_user 用户→内核

把 APP 写入的数据,安全拷贝到内核缓冲区,禁止直接 memcpy

copy_to_user 内核→用户

把内核缓存数据,安全发给应用层。

原理:内核空间与用户空间地址隔离,专用函数会做权限校验、地址合法性检查。

五、全套带超详细注释 驱动源码 hello_drv.c

#include <linux/module.h>    // 模块基础:入口/出口声明
#include <linux/fs.h>        // 文件操作、字符设备注册
#include <linux/errno.h>     // 错误码定义
#include <linux/kernel.h>    // printk内核打印
#include <linux/init.h>      // __init __exit 宏
#include <linux/device.h>    // class/device创建设备节点

// 全局变量
static int major = 0;                 // 主设备号,0=自动分配
static char kernel_buf[1024];         // 内核数据缓存
static struct class *hello_class;     // 设备类指针

// 取最小值,防止缓冲区溢出
#define MIN(a, b) (a < b ? a : b)

// 打开设备触发
static int hello_drv_open(struct inode *node, struct file *file)
{
    printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
    return 0;
}

// 读设备:内核数据发给APP
static ssize_t hello_drv_read(struct file *file, char __user *buf, size_t size, loff_t *offset)
{
    printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
    // 内核 -> 用户空间
    copy_to_user(buf, kernel_buf, MIN(1024, size));
    return MIN(1024, size);
}

// 写设备:APP数据传给内核
static ssize_t hello_drv_write(struct file *file, const char __user *buf, size_t size, loff_t *offset)
{
    printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
    // 用户空间 -> 内核
    copy_from_user(kernel_buf, buf, MIN(1024, size));
    return MIN(1024, size);
}

// 关闭设备触发
static int hello_drv_close(struct inode *node, struct file *file)
{
    printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
    return 0;
}

// 绑定所有操作函数到内核
static struct file_operations hello_drv = {
    .owner   = THIS_MODULE,
    .open    = hello_drv_open,
    .read    = hello_drv_read,
    .write   = hello_drv_write,
    .release = hello_drv_close,
};

// 驱动入口:insmod执行
static int __init hello_init(void)
{
    // 1.注册字符设备
    major = register_chrdev(0, "hello", &hello_drv);

    // 2.创建设备类
    hello_class = class_create(THIS_MODULE, "hello_class");
    if(IS_ERR(hello_class))
    {
        unregister_chrdev(major, "hello");
        return -1;
    }

    // 3.自动创建 /dev/hello
    device_create(hello_class, NULL, MKDEV(major, 0), NULL, "hello");
    return 0;
}

// 驱动出口:rmmod执行
static void __exit hello_exit(void)
{
    // 倒序释放资源
    device_destroy(hello_class, MKDEV(major, 0));
    class_destroy(hello_class);
    unregister_chrdev(major, "hello");
}

// 声明入口、出口
module_init(hello_init);
module_exit(hello_exit);

// 必须加GPL协议,否则内核污染警告
MODULE_LICENSE("GPL");

六、适配 IMX6ULL 专用 Makefile

makefile

# 内核源码路径(必须改成自己板子真实路径)
KERN_DIR = /home/book/100ask_imx6ull-sdk/Linux-4.9.88

# IMX6ULL架构+交叉编译器
ARCH = arm
CROSS_COMPILE = arm-linux-gnueabihf-

PWD := $(shell pwd)

# 编译为内核模块
obj-m += hello_drv.o

all:
	make -C $(KERN_DIR) M=$(PWD) modules
	$(CROSS_COMPILE)gcc -o hello_drv_test hello_drv_test.c

clean:
	make -C $(KERN_DIR) M=$(PWD) modules clean
	rm -rf modules.order Module.symvers hello_drv_test

七、编译前环境配置(关键!)

终端执行,临时配置交叉编译器环境:

export PATH=$PATH:/home/book/100ask_imx6ull-sdk/ToolChain/gcc-linaro-6.2.1-2016.11-x86_64_arm-linux-gnueabihf/bin
export ARCH=arm
export CROSS_COMPILE=arm-linux-gnueabihf-

# 验证是否生效
arm-linux-gnueabihf-gcc -v

输出版本号,即可执行:

make

编译成功生成:

  • hello_drv.ko 驱动模块
  • hello_drv_test 应用测试程序

八、实操遇到的报错 + 根治方案

报错 1:内核路径不存在

plaintext

No such file or directory

原因:Makefile 里写了 RK3399/STM32MP157 的内核路径,和 IMX6ULL 不匹配。解决:

  1. ls /home/book/100ask_imx6ull-sdk/Linux-4.9.88 确认路径
  2. 修改 Makefile 中KERN_DIR为真实路径

报错 2:arm-linux-gnueabihf-gcc: not found

原因:没配置交叉编译器环境变量。解决:执行上面环境配置三条 export 命令,再重新 make。

报错 3:adb push hello_drv 提示找不到文件

原因:驱动文件是hello_drv.ko,少写后缀.ko。正确命令:

bash

运行

adb push hello_drv.ko hello_drv_test /home/book/nfs_rootfs/

九、开发板端加载 & 测试全程命令

1、进入 NFS 共享目录

cd /home/book/nfs_rootfs
ls

2、加载驱动

insmod hello_drv.ko

3、查看驱动是否注册

cat /proc/devices | grep hello
ls /dev/hello -l

4、功能读写测试

# 写入字符串
./hello_drv_test -w wiki.100ask.net

# 读取字符串
./hello_drv_test -r

5、卸载驱动

rmmod hello_drv

十、新手永久避坑总结

  1. 不同开发板,KERN_DIR 必须严格对应,不能抄别人的;
  2. 编译嵌入式驱动,必须配置交叉编译器环境;
  3. 内核和用户数据交换,只能用copy_from_user/copy_to_user
  4. 驱动模块一定带.ko后缀,传输、加载不能漏;
  5. 卸载命令是rmmod,不要写成 rrmod。
Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐