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 设备树配置
// 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 平台驱动开发
// 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 移植自动化脚本
#!/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 镜像。移植不是一次性工作,而是随着硬件迭代持续适配的过程。
更多推荐



所有评论(0)