嵌入式 Linux Hello 驱动
本文基于韦东山 IMX6ULL 开发板,从零实现最简 Linux 字符设备驱动(Hello 驱动),包含:原理流程、架构说明、内核函数详解、全套带注释源码、Makefile 配置、编译报错排查、开发板加载测试全流程,直接适配 CSDN 博客发布。不同开发板,KERN_DIR 必须严格对应,不能抄别人的;编译嵌入式驱动,必须配置交叉编译器环境;内核和用户数据交换,只能用;驱动模块一定带.ko后缀,传
·
一、前言
本文基于韦东山 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、驱动加载流程
insmod hello_drv.ko加载模块- 自动执行
module_init入口函数 - 注册字符设备 → 创建设备 class → 自动生成 /dev/hello 节点
- 等待用户程序读写访问
3、读写交互流程
- 应用 open
/dev/hello→ 触发驱动open函数 - 应用 write 写入字符串 →
copy_from_user拷贝到内核缓冲区 - 应用 read 读取数据 →
copy_to_user把内核数据发给应用 - 应用 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 不匹配。解决:
ls /home/book/100ask_imx6ull-sdk/Linux-4.9.88确认路径- 修改 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
十、新手永久避坑总结
- 不同开发板,KERN_DIR 必须严格对应,不能抄别人的;
- 编译嵌入式驱动,必须配置交叉编译器环境;
- 内核和用户数据交换,只能用
copy_from_user/copy_to_user; - 驱动模块一定带
.ko后缀,传输、加载不能漏; - 卸载命令是
rmmod,不要写成 rrmod。
更多推荐
所有评论(0)