第二章:Linux内核与驱动基础

大家好,我是你们的嵌入式Linux讲师。今天我们来聊聊驱动开发的基础。说实话,很多初学者一听到「内核模块」、「设备树」就头大。我当年也一样,第一次编译内核模块时,连Makefile都写不对,折腾了一整天。

但这些东西,说白了就是套路。你掌握了框架,剩下的就是填代码。咱们一步步来。

2.1 内核模块编译——别怕,就是个动态库

内核模块是什么?你可以把它理解成Linux内核的「插件」。内核跑起来了,你想加个功能,不用重新编译整个内核,直接insmod一个模块进去就行。我习惯把模块编译和应用程序编译分开想——应用编译用gcc,模块编译用内核的构建系统。

先看一个最简单的模块代码:

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>

static int __init hello_init(void)
{
    printk(KERN_INFO "Hello, camera driver!\n");
    return 0;
}

static void __exit hello_exit(void)
{
    printk(KERN_INFO "Goodbye, camera driver!\n");
}

module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple camera module");

代码很简单,但有个坑——printk不会打印到终端,你得用dmesg看。我曾经在调试摄像头驱动时,死活看不到打印信息,后来才发现忘了看内核日志。

然后是Makefile。嗯,这里要注意:

obj-m := hello.o
KDIR := /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)

all:
    $(MAKE) -C $(KDIR) M=$(PWD) modules

clean:
    $(MAKE) -C $(KDIR) M=$(PWD) clean

编译命令:make。加载:sudo insmod hello.ko。卸载:sudo rmmod hello。就这么简单。

小技巧: 调试时多用modinfo hello.ko查看模块信息,能帮你确认版本和依赖。

2.2 字符设备驱动框架——打开、读写、关闭

字符设备驱动,说白了就是实现openreadwriteclose这几个函数。你想想看,摄像头采集图像,本质上就是应用程序通过read从驱动拿数据。

框架代码长这样:

#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>

static int camera_open(struct inode *inode, struct file *file)
{
    printk(KERN_INFO "Camera device opened\n");
    return 0;
}

static ssize_t camera_read(struct file *file, char __user *buf,
                           size_t count, loff_t *ppos)
{
    // 这里将来会填充图像数据
    printk(KERN_INFO "Camera read called\n");
    return 0;
}

static struct file_operations camera_fops = {
    .owner = THIS_MODULE,
    .open = camera_open,
    .read = camera_read,
};

static int __init camera_init(void)
{
    // 注册字符设备
    // 创建设备节点
    printk(KERN_INFO "Camera driver initialized\n");
    return 0;
}

module_init(camera_init);
module_exit(camera_exit);

这里有个关键点:file_operations结构体。你实现的每个函数指针,都会被内核在对应操作时调用。我在项目中遇到过一个问题——read函数里忘了用copy_to_user,直接给用户空间传内核指针,结果系统直接崩溃。记住,内核和用户空间的数据拷贝必须用专用函数。

警告: 永远不要直接访问用户空间指针!用copy_to_usercopy_from_user

2.3 platform驱动模型——设备与驱动的「相亲」

为什么要有platform驱动?我打个比方:传统PCI设备有标准总线,内核能自动枚举。但嵌入式设备里很多外设(比如摄像头I2C控制器)是直接挂在CPU上的,没有标准总线。这时候就需要platform总线来「牵线搭桥」。

platform驱动模型分两部分:

  • 设备端:描述硬件资源(地址、中断号等)
  • 驱动端:实现操作逻辑

驱动端代码示例:

static int camera_probe(struct platform_device *pdev)
{
    struct resource *res;
    res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    // 映射寄存器地址
    printk(KERN_INFO "Camera probed\n");
    return 0;
}

static int camera_remove(struct platform_device *pdev)
{
    printk(KERN_INFO "Camera removed\n");
    return 0;
}

static struct platform_driver camera_driver = {
    .probe = camera_probe,
    .remove = camera_remove,
    .driver = {
        .name = "camera_drv",
        .of_match_table = camera_of_match,
    },
};

module_platform_driver(camera_driver);

设备端以前用板级文件(arch/arm/mach-xxx),现在都用设备树了。我个人习惯把设备树和驱动分开看——设备树描述「有什么」,驱动描述「怎么用」。

2.4 设备树(DTS)基础——硬件说明书

设备树,说白了就是一份硬件配置清单。内核启动时读这份清单,就知道CPU连了哪些外设、地址是多少、中断用哪个引脚。

一个摄像头节点的例子:

/ {
    camera: camera@1c00000 {
        compatible = "vendor,camera-sensor";
        reg = <0x01c00000 0x1000>;
        interrupts = <0 42 4>;
        clocks = <&clkc 15>;
        status = "okay";
    };
};

关键字段解释:

字段 含义 我的经验
compatible 匹配驱动的名字 必须和驱动里of_match_table一致
reg 寄存器基地址和大小 地址写错会导致驱动probe失败
interrupts 中断号、触发类型 不同芯片的编码方式不同,查手册
status 设备是否启用 调试时设为"disabled"可临时屏蔽

设备树编译命令:dtc -I dts -O dtb -o xxx.dtb xxx.dts。反编译:dtc -I dtb -O dts -o xxx.dts xxx.dtb。我曾经在调试摄像头时,设备树里reg地址少写了一个0,结果驱动probe返回-ENXIO,查了两天才发现。

核心要点: 驱动开发就是「设备树描述硬件 → 驱动解析资源 → 操作寄存器」这条线。你把这个流程走通了,任何外设驱动都大同小异。

2.5 避坑指南

最后分享几个我踩过的坑:

  • 模块加载失败:先dmesg看内核日志,多半是符号依赖或版本不匹配。
  • 设备节点不出现:检查udev规则,或者手动mknod测试。
  • probe不调用:确认compatible字符串和设备树完全一致,包括大小写。
  • 中断不触发:用cat /proc/interrupts查看中断注册情况。

嗯,今天就到这里。下一章我们会把这些知识串起来,真正写一个摄像头驱动的雏形。到时候你会觉得,嗯,其实也没那么难。