1987 words
10 minutes
第一个驱动程序
第一个驱动程序
一、
linux-4.9/├── arch/ # 架构专属代码(核心:arm/32位ARM、arm64/64位ARM64)├── include/ # 全局头文件目录(驱动编译核心依赖,含linux/、asm/、asm-generic/)│ ├── linux/ # 内核通用头文件(如module.h、init.h、types.h,驱动必包含)│ ├── asm/ # 架构专属头文件(软链接,32位指向asm-arm,64位指向asm-arm64)│ └── asm-generic/ # 跨架构通用头文件(兜底兼容,解决架构头文件缺失)├── kernel/ # 内核核心功能代码(进程、调度等,驱动编译间接依赖)└── scripts/ # 编译脚本、工具链(驱动编译的Makefile依赖此目录脚本)三、Linux驱动核心框架(字符设备驱动,全文实操的驱动类型)
全文实现的是 Linux最基础、最通用的字符设备驱动,是嵌入式驱动开发入门核心框架,完整分层+核心要素如下:
1. 驱动整体分层(用户层 ↔ 内核层)
应用程序(APP) → 系统调用 → Linux内核 → 字符设备驱动 → 硬件- 应用层:通过
open/read/write/ioctl等标准接口操作驱动,无需关心硬件细节; - 内核层:驱动作为内核模块,注册字符设备节点,承接应用层调用,实现硬件操作逻辑;
- 核心特点:驱动与应用解耦、驱动与内核版本绑定、以模块(.ko)形式动态加载到内核。
2. 字符设备驱动核心框架(必实现4大核心函数+2大入口出口)
(1)2个核心入口/出口(驱动模块生命周期)
- 加载入口:
module_init(驱动初始化函数)→ 驱动insmod加载时执行,完成设备注册、资源申请; - 卸载出口:
module_exit(驱动卸载函数)→ 驱动rmmod卸载时执行,完成设备注销、资源释放。
(2)4个核心设备操作函数(应用层接口映射)
驱动需实现 file_operations 结构体,绑定应用层接口对应的内核处理函数,核心必实现:
.open // 应用层open()调用时执行(设备打开,初始化硬件/状态).read // 应用层read()调用时执行(硬件→内核→应用,数据读取).write // 应用层write()调用时执行(应用→内核→硬件,数据写入).release// 应用层close()调用时执行(设备关闭,释放临时资源)(3)驱动核心宏定义(必加,标识驱动信息)
MODULE_LICENSE("GPL"); // 声明开源协议(必须,否则内核加载警告)MODULE_AUTHOR("XXX"); // 驱动作者(可选)MODULE_DESCRIPTION("XXX");// 驱动描述(可选)四、全文实操:驱动代码 + 应用程序代码
1. 核心驱动代码(my_drv.c,字符设备驱动最简实现)
#include <linux/init.h>#include <linux/module.h>#include <linux/fs.h>#include <linux/uaccess.h>
// 设备号、设备名定义#define DEV_NAME "my_char_dev"static int major;
// 1. 设备打开函数static int my_drv_open(struct inode *inode, struct file *filp) { printk("my_drv open success!\n"); return 0;}
// 2. 设备读取函数static ssize_t my_drv_read(struct file *filp, char __user *buf, size_t size, loff_t *off) { char data[] = "Hello Driver!"; copy_to_user(buf, data, sizeof(data)); // 内核→用户层数据拷贝 return sizeof(data);}
// 3. 设备写入函数static ssize_t my_drv_write(struct file *filp, const char __user *buf, size_t size, loff_t *off) { char kbuf[128] = {0}; copy_from_user(kbuf, buf, size); // 用户层→内核数据拷贝 printk("APP write data: %s\n", kbuf); return size;}
// 4. 设备关闭函数static int my_drv_release(struct inode *inode, struct file *filp) { printk("my_drv close success!\n"); return 0;}
// 绑定操作函数集static struct file_operations fops = { .owner = THIS_MODULE, .open = my_drv_open, .read = my_drv_read, .write = my_drv_write, .release = my_drv_release,};
// 驱动加载入口static int __init my_drv_init(void) { // 注册字符设备,自动分配主设备号 major = register_chrdev(0, DEV_NAME, &fops); if (major < 0) { printk("driver register failed!\n"); return major; } printk("driver register success! major = %d\n", major); return 0;}
// 驱动卸载出口static void __exit my_drv_exit(void) { unregister_chrdev(major, DEV_NAME); // 注销字符设备 printk("driver unregister success!\n");}
module_init(my_drv_init);module_exit(my_drv_exit);
MODULE_LICENSE("GPL");MODULE_AUTHOR("huangzy");MODULE_DESCRIPTION("A133 Simple Char Driver");2. 配套应用程序代码(app.c,操作驱动的用户层程序)
#include <stdio.h>#include <sys/types.h>#include <sys/stat.h>#include <fcntl.h>#include <unistd.h>#include <string.h>
int main() { int fd; char r_buf[128] = {0}; char w_buf[] = "Hello APP to Driver!";
// 1. 打开驱动设备(需先手动创建设备节点:mknod /dev/mydev c 主设备号 0) fd = open("/dev/mydev", O_RDWR); if (fd < 0) { perror("open dev failed"); return -1; }
// 2. 读取驱动数据 read(fd, r_buf, sizeof(r_buf)); printf("read from driver: %s\n", r_buf);
// 3. 向驱动写入数据 write(fd, w_buf, strlen(w_buf));
// 4. 关闭驱动设备 close(fd); return 0;}五、全文核心Makefile
驱动Makefile(32位ARM,适配A133开发板)
obj-m := my_drv.o # 要编译的驱动文件(xxx.c → xxx.o → xxx.ko)KERNELDIR := /home/huangzy/桌面/a133-linux/kernel/linux-4.9 # 内核源码路径PWD := $(shell pwd) # 当前驱动目录路径ARCH := arm # 指定32位ARM架构CROSS_COMPILE := arm-linux-gnueabi- # 32位ARM交叉编译器
# 编译驱动模块(固定规则)all: $(MAKE) -C $(KERNELDIR) M=$(PWD) ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) modules
# 清理编译产物clean: $(MAKE) -C $(KERNELDIR) M=$(PWD) ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) clean六、全文所有核心指令(编译、加载、运行、调试,按流程排序)
【第一阶段】驱动编译指令(驱动目录执行,生成.ko文件)
make clean && make # 直接执行Makefile,生成my_drv.ko【第二阶段】应用程序编译指令(应用程序目录执行,生成可执行文件)
arm-linux-gnueabi-gcc app.c -o app # 生成32位APP可执行文件【第三阶段】开发板端核心指令(驱动加载、运行、调试,关键流程)
1. 驱动加载(将.ko模块动态加载到内核)
# 驱动加载insmod my_drv.ko2. 查看驱动加载状态(验证是否加载成功)
lsmod | grep my_drv # 过滤驱动名,显示则加载成功dmesg | tail -10 # 查看内核打印信息,确认驱动注册状态3. 创建设备节点(应用层操作驱动的唯一入口,必须执行)
# 格式:mknod /dev/设备名 c 主设备号 次设备号(次设备号填0即可)mknod /dev/mydev c 240 0 # 主设备号从dmesg打印的日志中获取4. 运行应用程序(操作驱动,测试读写功能)
chmod +x app # 赋予执行权限./app # 运行APP,触发驱动的open/read/write操作5. 驱动卸载(从内核中移除驱动模块)
rmmod my_drv # 卸载驱动(直接写驱动名,无需加.ko)七、遇到问题
驱动加载失败(未解决)
root@kickpi:/mnt/host_share# insmod my_drv.koinsmod: ERROR: could not insert module my_drv.ko: Invalid module format可能原因
- 虚拟机使用x86.
- 虚拟机用arm32编译(开发板是arm64),但是官方给的是32.
- 改用64之后缺少头文件。
- 还有一些小的文件配置问题重新编译一下就能解决。
其他问题
虚拟机内存不足
解决虚拟机内(Ubuntu)的空间不足问题,需要先扩展虚拟机磁盘容量,再在Ubuntu中分配新增空间,步骤如下:
步骤1:在VMware中扩展虚拟机磁盘的最大容量
- 关闭Ubuntu虚拟机。
- 打开VMware,选中虚拟机 → 点击「编辑虚拟机设置」→ 选择「硬盘」→ 点击「扩展」。
- 在弹出的窗口中,设置新的最大磁盘容量(比如设为50GB),点击「扩展」(此操作不会立刻占用主机对应空间)。
步骤2:在Ubuntu中分配新增的磁盘空间
需要用gparted工具(图形化分区工具)将新增空间合并到当前系统分区:
-
安装gparted 启动Ubuntu,打开终端执行:
Terminal window sudo apt install gparted -
打开gparted并识别磁盘 执行
sudo gparted启动工具,在右上角选择虚拟机的磁盘(通常是/dev/sda)。 -
调整分区
- 先删除磁盘末尾的“未分配空间”前的“扩展分区”(若存在);
- 右键点击当前系统分区(通常是
/dev/sda1)→ 选择「调整大小/移动」; - 拖动分区的右侧滑块,将未分配空间全部纳入系统分区,点击「调整大小」;
- 点击工具栏的「√」(应用所有操作),等待完成。
步骤3:验证空间
操作完成后,打开终端执行df -h,即可看到系统分区的可用空间已增加。
Some information may be outdated