嵌入式驱动开发时导入头文件的问题
includePath 里面填的是【内核源码里面的头文件路径】,不是 Ubuntu 系统(/usr/include)的路径!
驱动代码 #include <linux/cdev.h>、#include <linux/i2c.h> 这些头文件,不在 Ubuntu 系统头文件里,在内核源码包里面。

配置头文件,配置的是驱动开发中arm内核的路径下的头文件:

编译驱动模块时,Makefile为什么不需要指定编译器:

核心一句话
不是不用编译器,是编译器交给内核顶层 Makefile 帮你选好了,你这个外部模块 Makefile 不用自己写 gcc/arm-linux-gnueabihf-gcc!
我们拆开讲,结合你 IMX6ULL 驱动这个场景:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
这条命令的含义:
进入内核源码目录
KERNELDIR,执行内核顶层的 Makefile,让它帮我们编译外部模块。
1. 内核顶层 Makefile 里面,已经提前配置好交叉编译器
你在编译内核的时候,已经在内核里配置过: ARCH=arm、CROSS_COMPILE=arm-linux-gnueabihf- 内核顶层 Makefile 会根据这两个变量,自动拼出完整编译器: arm-linux-gnueabihf-gcc、arm-linux-gnueabihf-ld……
当你执行 make modules(内核的内置目标),内核会自动使用这套交叉编译器来编译你的 chrdevbase.c,不需要你在驱动的小 Makefile 里再写 gcc。
关键点: 驱动的.c 文件,不是直接用 gcc 去编译,是内核编译框架接管整个编译流程。
2. 和普通应用程序 Makefile 对比(反差很大)
普通用户态 C 程序(写 app)Makefile:
# 应用程序,自己指定编译器
test: test.c
arm-linux-gnueabihf-gcc test.c -o test
应用程序是独立编译,没有内核框架帮忙,所以必须手动指定编译器、头文件、链接库。
内核模块(驱动)Makefile:
obj-m := chrdevbase.o
all:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
你只需要告诉内核:我这个模块叫 chrdevbase,源码在当前目录,剩下的编译器、编译选项、头文件路径、链接规则,全部由内核顶层 Makefile 搞定。
内核会自动:
- 选对交叉编译器
- 带上内核头文件(
linux/cdev.h这些) - 加上内核编译参数(宏、内存对齐、内核编译标志)
- 最后打包成
.ko模块
3. 那什么时候需要手动指定 CROSS_COMPILE?
两种情况:
- 内核本身没有预先配置好 CROSS_COMPILE 你可以在驱动 Makefile 传进去:
KERNELDIR := /home/xxx/linux-imx-xxx
CROSS_COMPILE := arm-linux-gnueabihf-
ARCH := arm
all:
$(MAKE) -C $(KERNELDIR) M=$(PWD) ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) modules
- 换别的交叉编译器、临时切换架构的时候,手动传参。
如果你之前编译内核的时候,已经在
.config或者内核编译脚本里固定了 ARCH 和 CROSS_COMPILE,驱动 Makefile 就不用再加。
4. 补充一个坑(很多新手踩)
✅ 内核编译模块,不能直接用 ubuntu 主机的 gcc(x86 的 gcc),编译出来是 x86 的代码,放到 ARM 开发板上加载 ko 会报错。 正是因为内核顶层 Makefile 帮我们锁定了交叉编译器,才不会搞错。
查看设备号:

/****************************************************************************************************************************************************************************************
* 结构体初始化对象时,为什么需要有个.号,有什么作用?
****************************************************************************************************************************************************************************************/
一句话结论
这个写法叫 C 语言指定初始化(指定成员初始化 designated initializer),. 是语法的一部分,必须写,作用是:显式指定给结构体里面哪一个成员赋值。
static struct file_operations chrdevbase_fops={
.owner = THIS_MODULE,
.open = chrdevbase_open,
.release = chrdevbase_release,
.read = chrdevbase_read,
.write = chrdevbase_write,
};
.open 含义:给 struct file_operations 结构体里名字叫 open 的成员赋值。
1. 对比两种结构体初始化方式
方式 1:带. 指定初始化(内核驱动标准写法,你现在这种)
顺序可以随便调换,只认成员名字,和顺序无关:
// 顺序乱了也没关系!
static struct file_operations chrdevbase_fops={
.write = chrdevbase_write,
.open = chrdevbase_open,
.read = chrdevbase_read,
};
方式 2:不带.,按顺序初始化(普通 C 初始化)
严格必须按照结构体成员定义的先后顺序写,不能乱序,非常容易出错:
// 必须严格跟着结构体里面成员的顺序,不能写错位置
static struct file_operations chrdevbase_fops={
THIS_MODULE,
chrdevbase_open,
chrdevbase_release,
chrdevbase_read,
chrdevbase_write,
};
👉 一旦内核源码升级,struct file_operations 结构体里面新增成员、调整成员顺序,不带.的写法直接全部错位,驱动直接崩。
2. 内核驱动为什么强制推荐用 . 这种写法?
struct file_operations 这个结构体在内核不同版本里面成员是会变的:
- 4.1.15(你 IMX6ULL 这个版本)成员少
- 高版本内核(5.x、6.x)会新增很多成员(比如
.unlocked_ioctl、.llseek)
用 .成员名 = xxx: ✅ 只关心你用到的成员,没写的成员自动初始化为 0/NULL ✅ 结构体内部顺序改动、新增成员,你的驱动代码不用改,不会错位 ✅ 可读性极强,一眼就能看出来:这个函数是给 .open 回调的
这就是 Linux 内核编码规范,所有驱动的 file_operations 全部都是这种带
.的写法。
3. 回答你两个核心疑问
.必须写吗? 在这种【指定成员初始化】语法里,必须写; 如果你不写.,就变成按顺序初始化,是另外一种语法。- 能不能去掉
.? 语法上可以去掉(变成顺序初始化),但是驱动开发强烈不建议,兼容性差,踩坑概率极高。
4. 补充小例子帮你理解
struct student {
int age;
char *name;
};
// 指定初始化,.不能省
struct student s = {
.name = "zhangsan",
.age = 18,
};
.name 就是告诉编译器:把右边字符串地址赋值给 name 这个成员。
5. 顺带联动你前面的知识点
应用程序写代码两种写法都常见;内核驱动几乎全部统一用 .成员名 指定初始化,就是为了适配内核版本变化,防止结构体成员顺序变动带来隐蔽 bug。
/****************************************************************************************************************************************************************************************
* truct file_operations结构体中的read、write、open、release 这些回调函数的参数列表,是不是可以修改?是不是固定的?
****************************************************************************************************************************************************************************************/
一句话结论
struct file_operations 里面的 read、write、open、release 这些回调函数的参数列表,是内核固定写死的,不能随便改! 你写的函数必须严格匹配内核声明的函数原型,否则编译报错、或者运行时直接崩溃。(自己的理解是file_operations是结构体,结构体里面定义了)
我们逐个拆解你截图里 write 的 4 个参数:
static ssize_t chrdevbase_write(struct file *filp, const char __user *buf, size_t count, loff_t *ppos)
这就是内核规定好的 write 回调原型,一个都不能乱改。
1、逐个说明每个 fops 回调固定原型(IMX6ULL 4.1.15 内核)
// open
int (*open)(struct inode *inode, struct file *filp);
// release
int (*release)(struct inode *inode, struct file *filp);
// read
ssize_t (*read)(struct file *filp, char __user *buf, size_t count, loff_t *ppos);
// write
ssize_t (*write)(struct file *filp, const char __user *buf, size_t count, loff_t *ppos);
✅ 返回值类型、参数个数、每个参数的类型、顺序,全部由内核定义死
- 参数个数不能多、不能少
- 参数类型不能随便换(比如不能把
__user *buf改成普通char *buf) - 返回值类型也不能随便改成 int
原理:内核在应用层调用
write()系统调用的时候,内核会按照这个固定格式去调用你注册的函数。 如果你函数签名对不上,内核传参就错位,直接出内存错误。
2、但是!函数名字是你自己随便起的
区分两个东西,很多新手在这里混淆:
.write = chrdevbase_write,
write:是file_operations结构体里面的成员名,固定不变chrdevbase_write:是你自己写的函数名字,随便起名,比如 my_write、dev_write 都可以 只要这个函数本身的参数、返回值和内核原型一致就行。
示例:
static ssize_t my_dev_write(struct file *filp, const char __user *buf, size_t count, loff_t *ppos)
{
return 0;
}
static struct file_operations chrdevbase_fops={
.write = my_dev_write,
};
这个完全合法。
/****************************************************************************************************************************************************************************************
* 字符设备结构cdev
****************************************************************************************************************************************************************************************/


上面cdev变量初始化,其实就是初始化结构体的成员变量。

这里实际是把file_operations结构体创建的对象的地址赋值给cdev->ops,file_operations就是包含open、read、write的函数的结构体对象:
static struct file_operations chrdevbase_fops={ .write = chrdevbase_write, .open = chrdevbase_open, .read = chrdevbase_read, };



浙公网安备 33010602011771号