嵌入式Linux内核基础:从源码到驱动的第一课
大家好,我是你们的老朋友。今天咱们正式开始Camera驱动系列的第一章——嵌入式Linux内核基础。说实话,这部分内容看起来有点枯燥,但它是后面所有Camera驱动开发的基石。我当年刚入行时,就是在这块吃了不少亏,所以今天我会把那些坑都指出来。
核心要点:本章覆盖内核源码获取与编译、模块开发、设备树、字符设备驱动、内存管理、并发控制六大模块。掌握这些,你就能看懂Linux内核驱动的基本骨架。
1. 内核源码获取与编译
做Camera驱动,第一步就是拿到内核源码。我个人习惯直接从kernel.org下载主线内核,但实际项目中更多是用芯片厂商提供的BSP内核。比如高通、MTK的平台,他们会在官方内核基础上打上自己的补丁。
获取源码后,编译是基本功。我建议你第一次编译时用默认配置,先跑通流程:
# 下载源码
wget https://cdn.kernel.org/pub/linux/kernel/v5.x/linux-5.15.tar.xz
tar -xf linux-5.15.tar.xz
cd linux-5.15
# 配置内核
make defconfig # 默认配置
make menuconfig # 图形化配置(推荐)
# 编译内核
make -j$(nproc) # 多核编译,省时间
# 编译模块
make modules
make modules_install
这里有个坑——编译选项一定要和你的硬件平台匹配。我曾经在某个项目上,因为忘了配置DMA相关的选项,导致Camera驱动死活跑不起来,查了两天才发现是内核配置的问题。嗯,从那以后我每次编译前都会仔细检查.config文件。
我的小技巧:用make savedefconfig生成最小配置,方便版本管理。别把整个.config提交到git,那玩意儿太大了。
2. 内核模块开发基础
内核模块说白了就是一段可以动态加载到内核的代码。Camera驱动通常以模块形式存在,方便调试和更新。最基本的框架长这样:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
static int __init my_camera_init(void)
{
printk(KERN_INFO "Camera module loaded!\n");
// 这里做硬件初始化、注册设备等
return 0;
}
static void __exit my_camera_exit(void)
{
printk(KERN_INFO "Camera module unloaded!\n");
// 这里做资源释放
}
module_init(my_camera_init);
module_exit(my_camera_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple camera driver module");
你想想看,module_init和module_exit就是驱动的入口和出口。加载模块时内核调用module_init,卸载时调用module_exit。我在项目中经常用这种方式来测试硬件寄存器读写——先写一个简单的模块,加载后看看能不能正确访问Camera的I2C地址。
注意:模块中的printk默认输出到内核日志,用dmesg查看。别指望在终端直接看到打印信息,我第一次调试时就傻等了半天。
3. 设备树(Device Tree)基础语法与实战
设备树是嵌入式Linux的硬件描述语言。说白了,它就是告诉内核:你的板子上有哪些外设,它们挂在哪个总线上,地址是多少,中断号是多少。Camera设备在设备树中通常这样描述:
/ {
camera_sensor: camera@10 {
compatible = "sony,imx219";
reg = <0x10>;
clocks = <&clkc 15>;
clock-names = "xvclk";
DOVDD-supply = <&vgen2>;
reset-gpios = <&gpio1 15 GPIO_ACTIVE_LOW>;
port {
camera_ep: endpoint {
remote-endpoint = <&csi_ep>;
data-lanes = <1 2>;
};
};
};
};
这里compatible属性是关键,内核通过它来匹配驱动。我在做某个项目时,发现Camera驱动加载失败,查了半天发现是compatible字符串写错了——驱动里用的是"imx219",设备树里写的是"imx219_v2",就多了一个后缀,结果对不上。
设备树的语法其实不复杂,核心就是节点(node)和属性(property)。节点用{}包裹,属性用=赋值。常用的数据类型有:
- 字符串:
compatible = "sony,imx219"; - 32位整数:
reg = <0x10>; - 布尔值:
status = "okay";或"disabled" - GPIO:
reset-gpios = <&gpio1 15 GPIO_ACTIVE_LOW>;
实战建议:先用dtc -I dtb -O dts反编译已有的设备树,看看别人是怎么写的。这是最快的学习方式。
4. 字符设备驱动框架(file_operations结构体)
Camera驱动本质上是一个字符设备。用户空间的应用程序通过open/read/write/ioctl等系统调用与驱动交互。而驱动则通过file_operations结构体来注册这些回调函数:
static struct file_operations camera_fops = {
.owner = THIS_MODULE,
.open = camera_open,
.release = camera_release,
.read = camera_read,
.write = camera_write,
.unlocked_ioctl = camera_ioctl,
};
static int __init camera_init(void)
{
int major;
major = register_chrdev(0, "camera_dev", &camera_fops);
if (major < 0) {
printk(KERN_ERR "Failed to register camera device\n");
return major;
}
printk(KERN_INFO "Camera device registered, major=%d\n", major);
return 0;
}
这里register_chrdev会分配一个主设备号。用户空间通过mknod /dev/camera c major 0创建设备节点后,就能用open打开它了。我记得第一次写字符设备驱动时,忘了实现release函数,结果每次close设备后资源都没释放,最后系统内存被吃光了。
对于Camera驱动,ioctl是最常用的接口。我们用它来设置分辨率、帧率、曝光时间等参数。比如:
static long camera_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
switch (cmd) {
case CAMERA_SET_RESOLUTION:
// 解析用户传入的分辨率参数
// 配置Camera sensor寄存器
break;
case CAMERA_START_STREAM:
// 启动视频流
break;
default:
return -ENOTTY;
}
return 0;
}
5. 内核内存管理(kmalloc/kfree/dma_alloc_coherent)
驱动开发中,内存管理是绕不开的话题。Camera驱动尤其需要关注DMA内存,因为Camera sensor产生的数据量很大,需要通过DMA直接传输到内存。
先看常规的内存分配:
// 分配普通内存
void *buf = kmalloc(1024, GFP_KERNEL);
if (!buf) {
printk(KERN_ERR "kmalloc failed\n");
return -ENOMEM;
}
// 使用...
kfree(buf);
但Camera数据流通常需要连续物理内存,因为DMA控制器只能访问物理地址。这时候就要用dma_alloc_coherent:
dma_addr_t dma_handle;
void *cpu_addr;
cpu_addr = dma_alloc_coherent(dev, size, &dma_handle, GFP_KERNEL);
if (!cpu_addr) {
printk(KERN_ERR "DMA allocation failed\n");
return -ENOMEM;
}
// cpu_addr 是CPU视角的虚拟地址
// dma_handle 是DMA控制器视角的物理地址
// 使用完后释放
dma_free_coherent(dev, size, cpu_addr, dma_handle);
这里有个关键点:dma_alloc_coherent分配的内存是一致性DMA缓冲区,CPU和DMA控制器看到的缓存是一致的,不需要手动做缓存同步。我在一个项目中用过dma_alloc_noncoherent,结果因为缓存一致性问题,图像数据总是花屏,后来换成coherent版本就解决了。
避坑指南:千万不要在中断上下文用GFP_KERNEL标志分配内存,它可能会睡眠。中断中请用GFP_ATOMIC。
6. 内核并发控制(spinlock/mutex)
驱动开发中,并发问题是最容易出bug的地方。Camera驱动通常涉及多个执行路径:用户线程调用ioctl、中断处理程序处理帧完成事件、内核线程处理图像数据。这些路径如果同时访问共享资源,就会出问题。
Linux内核提供了两种主要的锁机制:
| 锁类型 | 使用场景 | 能否睡眠 | 性能 |
|---|---|---|---|
| spinlock(自旋锁) | 中断上下文、短临界区 | 不能 | 高(忙等待) |
| mutex(互斥锁) | 进程上下文、长临界区 | 能 | 中(可能调度) |
看个实际例子。假设我们要保护一个共享的帧缓冲区:
// 使用mutex保护进程上下文中的共享数据
static DEFINE_MUTEX(frame_lock);
static struct frame_buffer *g_frame;
int camera_read_frame(struct frame_buffer *buf)
{
mutex_lock(&frame_lock);
if (!g_frame) {
mutex_unlock(&frame_lock);
return -ENODATA;
}
memcpy(buf, g_frame, sizeof(*g_frame));
mutex_unlock(&frame_lock);
return 0;
}
// 使用spinlock保护中断中的共享数据
static DEFINE_SPINLOCK(irq_lock);
static int frame_count;
irqreturn_t camera_irq_handler(int irq, void *dev_id)
{
unsigned long flags;
spin_lock_irqsave(&irq_lock, flags);
frame_count++;
spin_unlock_irqrestore(&irq_lock, flags);
return IRQ_HANDLED;
}
注意看,中断处理程序中用了spin_lock_irqsave,它会同时关闭本地中断,防止死锁。我刚开始写驱动时,在中断里用了mutex_lock,结果内核直接panic了——因为mutex可能导致睡眠,而中断上下文是不能睡眠的。
我的经验:能用mutex就别用spinlock。spinlock的忙等待会浪费CPU,而且容易引入死锁。只有在中断上下文或者对性能要求极高的场景才用spinlock。
本章知识体系总览
下面这张图总结了本章的核心知识点和它们之间的关系。你可以把它当作一个思维导图来用:
好了,以上就是嵌入式Linux内核基础的核心内容。这些知识就像盖房子的地基,虽然看不见摸不着,但决定了上层建筑是否稳固。下一章我们会深入Camera驱动特有的V4L2框架,到时候你会发现,今天学的这些字符设备、内存管理、并发控制知识全都会用上。
课后练习:试着写一个简单的字符设备驱动,实现open/read/ioctl三个接口。然后在设备树中添加一个虚拟的Camera节点,用compatible属性匹配你的驱动。最后用spinlock保护一个计数器,在中断处理函数中递增它。