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

一、BSP 移植的"最后一公里":驱动不亮,板子就是块砖
拿到一块新的 SoC 开发板,Bootloader 跑通了、内核启动了、根文件系统挂载了------但网卡不通、显示黑屏、GPIO 没反应。这种情况在嵌入式开发中太常见了。内核能跑起来只说明 CPU 和内存没问题,外设能不能用,全看驱动有没有正确加载。
BSP(Board Support Package)移植的本质就是把 SoC 的硬件描述(设备树)和驱动代码(内核模块)对接起来。设备树告诉内核"板子上有什么硬件",驱动代码告诉内核"怎么操作这些硬件"。两者缺一不可,任何一端配置错误,外设就无法工作。
二、Linux 设备驱动模型的底层机制
2.1 设备树、驱动与总线的三角关系
设备树(DTS)在编译时被转换为 DTB(Device Tree Blob),内核启动时解析 DTB 生成 platform_device 结构。驱动代码通过 module_platform_driver 宏注册 platform_driver。总线核心根据 compatible 属性进行匹配,匹配成功后调用驱动的 probe 函数完成硬件初始化。
2.2 内核模块的加载与卸载流程
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 镜像。移植不是一次性工作,而是随着硬件迭代持续适配的过程。