Linux 内核驱动开发与 BSP 移植:从设备树到内核模块的系统构建

Linux 内核驱动开发与 BSP 移植:从设备树到内核模块的系统构建

一、BSP 移植的"最后一公里":驱动不亮,板子就是块砖

拿到一块新的 SoC 开发板,Bootloader 跑通了、内核启动了、根文件系统挂载了------但网卡不通、显示黑屏、GPIO 没反应。这种情况在嵌入式开发中太常见了。内核能跑起来只说明 CPU 和内存没问题,外设能不能用,全看驱动有没有正确加载。

BSP(Board Support Package)移植的本质就是把 SoC 的硬件描述(设备树)和驱动代码(内核模块)对接起来。设备树告诉内核"板子上有什么硬件",驱动代码告诉内核"怎么操作这些硬件"。两者缺一不可,任何一端配置错误,外设就无法工作。

二、Linux 设备驱动模型的底层机制

2.1 设备树、驱动与总线的三角关系

graph TD A[设备树 DTS] -->|解析| B[platform_device] C[驱动代码] -->|注册| D[platform_driver] B --> E[总线匹配<br/>of_device_id] D --> E E -->|匹配成功| F[probe 函数调用] F --> G[硬件初始化] F --> H[中断注册] F --> I[设备节点创建] G --> J[外设可用] H --> J I --> J style E fill:#9f9,stroke:#333

设备树(DTS)在编译时被转换为 DTB(Device Tree Blob),内核启动时解析 DTB 生成 platform_device 结构。驱动代码通过 module_platform_driver 宏注册 platform_driver。总线核心根据 compatible 属性进行匹配,匹配成功后调用驱动的 probe 函数完成硬件初始化。

2.2 内核模块的加载与卸载流程

sequenceDiagram participant U as 用户空间 participant K as 内核 participant M as 模块 U->>K: insmod my_driver.ko K->>M: module_init() M->>M: 注册 platform_driver M->>K: of_device_id 匹配 K->>M: probe() M->>M: 申请内存/中断资源 M->>M: 初始化硬件寄存器 M->>K: 创建 /dev/my_device K->>U: 设备节点就绪 U->>K: rmmod my_driver K->>M: module_exit() M->>M: 释放中断/内存 M->>M: 注销 platform_driver M->>K: 删除设备节点

2.3 设备树中的关键属性

属性 作用 示例
compatible 驱动匹配标识 "vendor,my-device"
reg 寄存器地址与大小 <0x4000 0x100>
interrupts 中断号与触发方式 <0 25 4>
clocks 时钟源引用 <&clk_periph>
status 设备启用状态 "okay" / "disabled"
pinctrl-* 引脚复用配置 <&pinctrl_uart0>

三、内核驱动开发与 BSP 移植的代码实践

3.1 设备树配置

dts 复制代码
// arch/arm/boot/dts/my-board.dtsi
// 自定义开发板的设备树配置

/ {
    // 串口配置
    serial@40008000 {
        compatible = "vendor,my-uart";
        reg = <0x40008000 0x100>;
        interrupts = <0 25 4>;  // SPI 中断号 25,高电平触发
        clocks = <&clk_periph>;
        clock-names = "apb";
        status = "okay";
        pinctrl-0 = <&pinctrl_uart0>;
        pinctrl-names = "default";
    };

    // I2C 控制器配置
    i2c@40005000 {
        compatible = "vendor,my-i2c";
        reg = <0x40005000 0x200>;
        interrupts = <0 20 4>;
        clocks = <&clk_periph>;
        clock-frequency = <100000>;  // 100kHz 标准模式
        status = "okay";
        #address-cells = <1>;
        #size-cells = <0>;

        // I2C 总线上的从设备
        sensor@48 {
            compatible = "vendor,temp-sensor";
            reg = <0x48>;
            interrupt-parent = <&gpio0>;
            interrupts = <12 2>;  // GPIO12,下降沿触发
        };
    };

    // GPIO 控制器配置
    gpio0: gpio@40006000 {
        compatible = "vendor,my-gpio";
        reg = <0x40006000 0x400>;
        interrupts = <0 30 4>;
        gpio-controller;
        #gpio-cells = <2>;
        interrupt-controller;
        #interrupt-cells = <2>;
        ngpios = <32>;
        status = "okay";
    };
};

// 引脚复用配置
&pinctrl {
    pinctrl_uart0: uart0-pins {
        pins = "PA0", "PA1";
        function = "uart0";
        bias-pull-up;
        drive-strength = <4>;  // 4mA 驱动能力
    };
};

3.2 平台驱动开发

c 复制代码
// drivers/my_driver/my_uart.c
// 自定义 UART 驱动实现
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/of.h>
#include <linux/of_device.h>
#include <linux/serial_core.h>
#include <linux/clk.h>
#include <linux/interrupt.h>
#include <linux/io.h>

#define DRIVER_NAME "my-uart"
#define UART_FIFO_SIZE 16

// 寄存器偏移定义
#define UART_DR     0x00  // 数据寄存器
#define UART_SR     0x04  // 状态寄存器
#define UART_CR     0x08  // 控制寄存器
#define UART_BR     0x0C  // 波特率寄存器
#define UART_IMSC   0x10  // 中断屏蔽寄存器
#define UART_ICR    0x14  // 中断清除寄存器

// 控制寄存器位定义
#define CR_UARTEN   BIT(0)  // UART 使能
#define CR_TXEN     BIT(1)  // 发送使能
#define CR_RXEN     BIT(2)  // 接收使能

// 驱动私有数据结构
struct my_uart_port {
    struct uart_port port;
    void __iomem *base;     // 映射后的寄存器基地址
    struct clk *clk;        // 时钟句柄
    int irq;                // 中断号
    u32 current_baud;       // 当前波特率
};

// 中断处理函数
static irqreturn_t my_uart_irq(int irq, void *dev_id)
{
    struct uart_port *port = dev_id;
    struct my_uart_port *up =
        container_of(port, struct my_uart_port, port);
    u32 status;

    status = readl(up->base + UART_SR);

    // 接收中断:从 FIFO 读取数据
    if (status & BIT(0)) {
        while (status & BIT(0)) {
            u8 ch = readl(up->base + UART_DR) & 0xFF;
            // 将数据推入 TTY 缓冲区
            uart_insert_char(port, status, BIT(1),
                             ch, TTY_NORMAL);
            status = readl(up->base + UART_SR);
        }
        tty_flip_buffer_push(&port->state->port);
    }

    // 发送中断:继续发送缓冲区中的数据
    if (status & BIT(2)) {
        // 清除发送中断标志
        writel(BIT(2), up->base + UART_ICR);
        // 触发上层继续写入
        uart_write_wakeup(port);
    }

    return IRQ_HANDLED;
}

// 启动端口
static int my_uart_startup(struct uart_port *port)
{
    struct my_uart_port *up =
        container_of(port, struct my_uart_port, port);
    int ret;

    // 申请中断
    ret = request_irq(up->irq, my_uart_irq,
                      IRQF_SHARED, DRIVER_NAME, port);
    if (ret) {
        dev_err(port->dev, "failed to request IRQ %d\n", up->irq);
        return ret;
    }

    // 使能 UART:先使能收发,再使能模块
    writel(CR_TXEN | CR_RXEN, up->base + UART_CR);
    writel(CR_UARTEN | CR_TXEN | CR_RXEN, up->base + UART_CR);

    // 使能接收中断
    writel(BIT(0), up->base + UART_IMSC);

    return 0;
}

// 关闭端口
static void my_uart_shutdown(struct uart_port *port)
{
    struct my_uart_port *up =
        container_of(port, struct my_uart_port, port);

    // 禁用所有中断
    writel(0, up->base + UART_IMSC);

    // 禁用 UART
    writel(0, up->base + UART_CR);

    // 释放中断
    free_irq(up->irq, port);
}

// 设置波特率
static void my_uart_set_termios(struct uart_port *port,
                                 struct ktermios *termios,
                                 struct ktermios *old)
{
    struct my_uart_port *up =
        container_of(port, struct my_uart_port, port);
    unsigned long clk_rate;
    u32 baud_div;

    // 计算波特率分频值
    clk_rate = clk_get_rate(up->clk);
    baud_div = clk_rate / (16 * port->baud_base) - 1;

    // 禁用 UART 再配置
    writel(0, up->base + UART_CR);
    writel(baud_div, up->base + UART_BR);
    writel(CR_UARTEN | CR_TXEN | CR_RXEN, up->base + UART_CR);

    up->current_baud = port->baud_base;
}

// UART 操作函数集
static const struct uart_ops my_uart_ops = {
    .startup    = my_uart_startup,
    .shutdown   = my_uart_shutdown,
    .set_termios = my_uart_set_termios,
    .type       = NULL,  // 按需实现
    .release_port = NULL,
    .request_port = NULL,
    // 其他操作按需填充
};

// 驱动 probe 函数
static int my_uart_probe(struct platform_device *pdev)
{
    struct device_node *np = pdev->dev.of_node;
    struct my_uart_port *up;
    struct resource *res;
    int ret;

    up = devm_kzalloc(&pdev->dev, sizeof(*up), GFP_KERNEL);
    if (!up)
        return -ENOMEM;

    // 获取寄存器基地址并映射
    res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    up->base = devm_ioremap_resource(&pdev->dev, res);
    if (IS_ERR(up->base))
        return PTR_ERR(up->base);

    // 获取中断号
    up->irq = platform_get_irq(pdev, 0);
    if (up->irq < 0)
        return up->irq;

    // 获取时钟
    up->clk = devm_clk_get(&pdev->dev, "apb");
    if (IS_ERR(up->clk))
        return PTR_ERR(up->clk);

    ret = clk_prepare_enable(up->clk);
    if (ret)
        return ret;

    // 初始化 uart_port 结构
    up->port.dev = &pdev->dev;
    up->port.mapbase = res->start;
    up->port.irq = up->irq;
    up->port.ops = &my_uart_ops;
    up->port.fifosize = UART_FIFO_SIZE;
    up->port.uartclk = clk_get_rate(up->clk);
    up->port.iotype = UPIO_MEM;
    up->port.flags = UPF_BOOT_AUTOCONF;

    platform_set_drvdata(pdev, up);

    return 0;
}

// 驱动 remove 函数
static int my_uart_remove(struct platform_device *pdev)
{
    struct my_uart_port *up = platform_get_drvdata(pdev);

    clk_disable_unprepare(up->clk);
    return 0;
}

// 设备树匹配表
static const struct of_device_id my_uart_of_match[] = {
    { .compatible = "vendor,my-uart" },
    { /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, my_uart_of_match);

// 平台驱动结构
static struct platform_driver my_uart_driver = {
    .probe  = my_uart_probe,
    .remove = my_uart_remove,
    .driver = {
        .name = DRIVER_NAME,
        .of_match_table = my_uart_of_match,
    },
};

module_platform_driver(my_uart_driver);

MODULE_AUTHOR("Embedded Team");
MODULE_DESCRIPTION("Custom UART Driver for My SoC");
MODULE_LICENSE("GPL");

3.3 BSP 移植自动化脚本

bash 复制代码
#!/bin/bash
# bsp_build.sh
# BSP 构建脚本:从内核配置到镜像生成的完整流程

set -e

# 配置变量
KERNEL_DIR="${KERNEL_DIR:-/opt/linux}"
CROSS_COMPILE="${CROSS_COMPILE:-arm-linux-gnueabihf-}"
ARCH="${ARCH:-arm}"
DTS_NAME="my-board"
DEFCONFIG="my_board_defconfig"

echo "=== BSP Build Start ==="

# Step 1: 内核配置
echo "[1/5] Applying defconfig..."
cd "${KERNEL_DIR}"
make ARCH="${ARCH}" CROSS_COMPILE="${CROSS_COMPILE}" "${DEFCONFIG}"

# Step 2: 编译设备树
echo "[2/5] Compiling device tree..."
make ARCH="${ARCH}" CROSS_COMPILE="${CROSS_COMPILE}" \
    "${DTS_NAME}.dtb"

# Step 3: 编译内核
echo "[3/5] Compiling kernel..."
make ARCH="${ARCH}" CROSS_COMPILE="${CROSS_COMPILE}" \
    -j"$(nproc)" zImage

# Step 4: 编译模块
echo "[4/5] Compiling modules..."
make ARCH="${ARCH}" CROSS_COMPILE="${CROSS_COMPILE}" \
    -j"$(nproc)" modules

# Step 5: 安装模块到目标根文件系统
echo "[5/5] Installing modules..."
MODULES_OUT="${MODULES_OUT:-/opt/rootfs}"
make ARCH="${ARCH}" CROSS_COMPILE="${CROSS_COMPILE}" \
    INSTALL_MOD_PATH="${MODULES_OUT}" \
    modules_install

echo "=== BSP Build Complete ==="
echo "Kernel:   arch/arm/boot/zImage"
echo "DTB:      arch/arm/boot/dts/${DTS_NAME}.dtb"
echo "Modules:  ${MODULES_OUT}/lib/modules/"

四、驱动开发与 BSP 移植的架构权衡

4.1 主线内核 vs 厂商 BSP 的选择

厂商提供的 BSP 通常包含大量私有驱动和补丁,外设开箱即用,但内核版本可能落后主线 2---3 年,安全补丁缺失。主线内核代码质量更高、社区维护活跃,但新 SoC 的驱动可能尚未合入主线。在产品化阶段,建议基于厂商 BSP 做安全补丁回移;在长期维护阶段,逐步将驱动适配到主线内核。

4.2 设备树 Overlay 的灵活性风险

设备树 Overlay 允许运行时动态修改硬件配置(如插入扩展板),但 Overlay 的调试困难------修改不生效时很难定位是 Overlay 本身的问题还是驱动兼容性问题。在产品量产阶段,建议将所有硬件配置固化到主设备树中,避免使用 Overlay。

4.3 中断处理的上半部与下半部

中断处理函数(上半部)应尽可能短,只做最紧急的硬件操作;耗时的数据处理放在下半部(tasklet、workqueue)中执行。如果在中断处理函数中执行耗时操作,会导致其他中断被屏蔽,系统响应性急剧下降。

五、总结

BSP 移植的核心是理清设备树、驱动与总线的匹配关系。设备树描述硬件,驱动操作硬件,总线负责匹配------三者缺一不可。驱动开发中,资源申请必须使用 devm_* 系列函数确保自动释放,中断处理必须区分上半部和下半部,寄存器访问必须使用 readl/writel 而非直接指针解引用。

落地路径:先用厂商 BSP 验证硬件可用性,确认所有外设正常工作;再逐步将驱动代码从厂商私有目录迁移到标准内核目录结构中;最后建立自动化构建脚本,确保每次内核配置变更后都能一键编译出完整的 BSP 镜像。移植不是一次性工作,而是随着硬件迭代持续适配的过程。

相关推荐
思陌Ai算法定制1 小时前
【心血管影像AI预测预后】心肌梗死后心脏MRI如何更早识别高风险患者?
人工智能·影像组学·心血管影像·心脏mri·心肌梗死·stemi·微循环阻塞
云烟成雨TD1 小时前
Agent Scope Java 2.x 系列【6】消息层
java·人工智能·agent
云烟成雨TD1 小时前
Spring AI Alibaba 1.x 系列【74】Agentic RAG 与混合 RAG
java·人工智能·spring
Tiansan66661 小时前
郑州AI问答推广:2家优质服务商剖析
人工智能·郑州ai问答推广2家
“码”力全开1 小时前
解耦异构算力:基于 Docker 与边缘计算的 GB28181/RTSP 企业级 AI 视频管理平台架构设计(含源码交付)
人工智能·docker·边缘计算
云烟成雨TD1 小时前
Spring AI Alibaba 1.x 系列【79】图执行生命周期的可观测性基础设施
java·人工智能·spring
kishu_iOS&AI1 小时前
LLM —— Milvmus向量数据库
数据库·人工智能·milvus
celiahul1 小时前
结构化内容:让网站同时适配搜索引擎与 AI 工具
人工智能·搜索引擎
qq_8573058191 小时前
OpenCV入门
人工智能·opencv·计算机视觉