2. Linux 内核与驱动基础:内核编译与模块加载、设备树解析、字符设备驱动框架、GPIO 与 PWM 驱动实战

好,咱们进入第二章。这一章是硬桥硬马的底层功夫。说白了,你要在嵌入式 Linux 上部署 AI 模型,内核和驱动这关绕不过去。模型跑在用户态,但最终要操作硬件——摄像头、GPIO、PWM 调速风扇、SPI 闪存……这些全得靠驱动。

我刚开始做嵌入式时,总觉得驱动是内核工程师的事。后来发现,模型推理框架移植时,经常要自己调设备树、改 GPIO 中断、甚至写个简单的字符设备驱动来验证硬件通路。嗯,这一章就是帮你把这些基本功打扎实。

2.1 内核编译与模块加载

先聊内核编译。你拿到一块开发板,厂家给了内核源码,第一件事就是把它编译出来。我个人习惯用 make menuconfig 来配置,图形界面直观,不容易漏选项。

核心流程: 配置 → 编译内核 → 编译模块 → 安装模块 → 制作设备树 → 打包启动镜像

编译命令其实就几条,但参数别搞错:

# 配置内核(基于厂家默认配置)
make ARCH=arm64 defconfig
make ARCH=arm64 menuconfig

# 编译内核和模块
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- -j4

# 安装模块到目标目录
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- modules_install INSTALL_MOD_PATH=./modules

这里有个坑——交叉编译工具链版本。我曾经在一个项目里用了太新的 GCC 编译内核,结果启动时内核 panic,折腾了两天才发现是工具链 ABI 不兼容。建议用厂家 SDK 配套的工具链,或者 Linaro 的稳定版。

模块加载这块,insmod 和 modprobe 的区别你要清楚:

命令 特点 适用场景
insmod 直接加载 .ko 文件,不处理依赖 调试单个模块,路径明确
modprobe 自动解析依赖,加载所有需要的模块 生产环境,模块间有依赖关系
rmmod 卸载模块 调试或热插拔场景
小技巧: 调试时用 insmod 配合 dmesg 看打印信息,能快速定位模块初始化失败的原因。我习惯在模块入口函数里加 printk 打印模块名和参数,方便确认加载顺序。

2.2 设备树(Device Tree)解析

设备树是嵌入式 Linux 的硬件描述语言。你想想看,PC 上有 ACPI 自动枚举硬件,但嵌入式平台千奇百怪,SoC 厂家得告诉内核「我这颗芯片有哪些外设、地址是多少、中断号是多少」。设备树就是干这个的。

一个典型的设备树节点长这样:

/ {
    model = "TI AM335x BeagleBone Black";
    compatible = "ti,am335x-bone-black", "ti,am335x-bone", "ti,am33xx";

    leds {
        pinctrl-names = "default";
        pinctrl-0 = <&user_led_pins>;

        led2 {
            label = "beaglebone:green:heartbeat";
            gpios = <&gpio1 21 GPIO_ACTIVE_HIGH>;
            linux,default-trigger = "heartbeat";
        };
    };
};

这里有几个关键概念:

  • compatible:驱动匹配的关键字符串,驱动里通过 of_match_table 来匹配
  • reg:寄存器地址和大小,描述硬件资源
  • interrupts:中断号,注意 SPI 和 GPIO 中断的编码方式不同
  • pinctrl:引脚复用配置,这个最容易出错
避坑指南: 我曾经在设备树里把 GPIO 编号写错了一位,结果 led 死活不亮。查了两天,最后发现是 &gpio1 21 这个 21 是 bank 内的偏移,不是全局编号。不同厂家的 GPIO 编号规则不一样,一定要看 datasheet。

设备树编译命令:

# 编译 .dts 为 .dtb
dtc -I dts -O dtb -o am335x-boneblack.dtb am335x-boneblack.dts

# 反编译 .dtb 为 .dts(调试用)
dtc -I dtb -O dts -o am335x-boneblack.dts am335x-boneblack.dtb

调试设备树时,我常用 ls /proc/device-tree/ 来查看内核实际解析到的设备树内容,确认节点有没有被正确识别。

2.3 字符设备驱动框架

字符设备驱动是嵌入式 Linux 里最基础的驱动模型。GPIO、PWM、I2C、SPI 这些设备,本质上都是字符设备。框架其实很固定,核心就是 file_operations 结构体:

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

static int my_open(struct inode *inode, struct file *filp) {
    pr_info("my_device opened\n");
    return 0;
}

static ssize_t my_read(struct file *filp, char __user *buf, size_t len, loff_t *off) {
    // 从硬件读取数据,拷贝到用户空间
    return 0;
}

static ssize_t my_write(struct file *filp, const char __user *buf, size_t len, loff_t *off) {
    // 从用户空间接收数据,写入硬件
    return len;
}

static struct file_operations my_fops = {
    .owner = THIS_MODULE,
    .open = my_open,
    .read = my_read,
    .write = my_write,
};

注册字符设备的步骤我总结为四步:

  1. 分配设备号:alloc_chrdev_region 动态分配,或者 register_chrdev_region 静态指定
  2. 初始化 cdev:cdev_init 绑定 file_operations
  3. 添加设备:cdev_add 告诉内核这个设备存在
  4. 创建设备节点:class_create + device_create 自动在 /dev 下生成节点
关键点: 用户态通过 open/read/write/ioctl 操作设备文件,驱动里对应的函数会被调用。这就是「一切皆文件」的思想。

调试字符设备驱动时,我习惯在 open 和 release 里加打印,确认用户态程序是否正确打开了设备。另外,copy_to_user 和 copy_from_user 这两个函数一定要用,不能直接解引用用户空间指针——内核态和用户态的地址空间是隔离的。

2.4 GPIO 与 PWM 驱动实战

好,到了动手环节。GPIO 和 PWM 是嵌入式里最常用的外设接口。模型部署时,你可能需要用 GPIO 控制复位引脚、用 PWM 调节散热风扇转速。

GPIO 驱动

Linux 内核提供了 gpiolib 子系统,操作 GPIO 变得很简单:

#include <linux/gpio.h>
#include <linux/of_gpio.h>

// 从设备树获取 GPIO 编号
int gpio = of_get_named_gpio(np, "enable-gpios", 0);
if (gpio < 0) {
    pr_err("Failed to get GPIO\n");
    return gpio;
}

// 申请并配置 GPIO
gpio_request(gpio, "my_device_enable");
gpio_direction_output(gpio, 0);  // 初始输出低电平

// 控制电平
gpio_set_value(gpio, 1);  // 拉高
gpio_set_value(gpio, 0);  // 拉低

这里要注意:gpio_request 一定要检查返回值,我曾经漏了检查,结果两个驱动抢同一个 GPIO,系统直接死锁。

PWM 驱动

PWM 驱动在 Linux 中通过 pwm 子系统管理。设备树里配置好 PWM 控制器和通道,驱动里调用 API 设置频率和占空比:

#include <linux/pwm.h>

struct pwm_device *pwm = pwm_get(&pdev->dev, NULL);
if (IS_ERR(pwm)) {
    pr_err("Failed to get PWM\n");
    return PTR_ERR(pwm);
}

// 配置 PWM:频率 1kHz,占空比 50%
pwm_config(pwm, 500000, 1000000);  // duty_ns=500us, period_ns=1000us
pwm_enable(pwm);
经验之谈: PWM 的频率和占空比计算要小心。我遇到过占空比设置成 0 导致 PWM 输出一直为低,风扇不转;也遇到过 period 设置太小,超出硬件支持范围,API 返回 -EINVAL。建议先查 datasheet 确认 PWM 控制器的时钟源和分频系数。

下面这张图总结了本章的知识体系,帮你理清内核、设备树、驱动和应用层的关系:

Linux 内核与驱动基础结构图 Linux 内核与驱动基础结构 用户空间应用(AI 推理框架) 系统调用(open/read/write/ioctl) 内核驱动层 字符设备驱动 GPIO 子系统 PWM 子系统 设备树(Device Tree)硬件描述 硬件外设(GPIO 控制器、PWM 定时器) 关键文件 /dev/ 设备节点 /proc/device-tree/ 调试工具 dmesg / insmod / rmmod dtc / gpio-utils
本章小结: 内核编译是基础,设备树是硬件与驱动的桥梁,字符设备框架是驱动开发的模板,GPIO 和 PWM 是实战的起点。把这些搞明白,后面移植推理框架时遇到硬件相关问题,你就能快速定位是驱动问题还是设备树配置问题。