嵌入式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_initmodule_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驱动开发必备技能 左侧:驱动开发环境搭建与硬件描述 右侧:驱动核心机制与资源管理 所有知识点最终服务于Camera sensor的驱动实现

好了,以上就是嵌入式Linux内核基础的核心内容。这些知识就像盖房子的地基,虽然看不见摸不着,但决定了上层建筑是否稳固。下一章我们会深入Camera驱动特有的V4L2框架,到时候你会发现,今天学的这些字符设备、内存管理、并发控制知识全都会用上。

课后练习:试着写一个简单的字符设备驱动,实现open/read/ioctl三个接口。然后在设备树中添加一个虚拟的Camera节点,用compatible属性匹配你的驱动。最后用spinlock保护一个计数器,在中断处理函数中递增它。

公众号:蓝海资料掘金营,微信deep3321