当前位置: 首页 > news >正文

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 镜像。移植不是一次性工作,而是随着硬件迭代持续适配的过程。

http://www.jsqmd.com/news/974062/

相关文章:

  • 质量流量计选哪家好?2026国产选型指南(附厂家对比) - 仪表人老张
  • 芙蓉区黄金回收答疑:长沙上门回收靠谱吗?详细拆解上门交易的真实利弊与避坑细节 - 奢侈品回收测评
  • 做好主题集群架构,你的AI引用率可以提升3.2倍
  • 从光谱分析到UWB定位:聊聊Savitzky-Golay滤波器这个‘老古董’为何在物联网时代又火了
  • 别再死记硬背了!用Spring Boot + MySQL实战演示四种隔离级别下的数据‘错乱’现场
  • SpringBoot项目实战:用Milvus 2.0和虹软SDK,5步搞定一个简易人脸检索系统
  • 汉服文化网站毕设资源包:SSM后端+Vue前端,含源码、数据库、文档、演示视频与答辩材料
  • 高校课程管理毕设源码包:SpringBoot后端+Vue前端+MySQL脚本+详细文档
  • PHP简单工厂与抽象工厂对比
  • MATLAB版DTW孤立词识别工程:含语音预处理、MFCC特征提取与模板匹配全流程代码
  • 三月七小助手:如何让星穹铁道的日常任务自动化帮你每天节省2小时?
  • 2026大一寸证件照怎么做?尺寸规格+免费制作APP/小程序保姆教程 - 软件小管家
  • 卫星语义通信中的特征敏感排序技术解析
  • 点云数据里一键抠出平面、圆柱、长方体等常见3D形状的Python小工具
  • 从环境变量到源码:彻底搞懂QML模块导入失败的那些坑
  • 星宸SSD202D芯片全解析:从硬件选型到Linux SDK上手,东山Pi开发板为何适合入门?
  • C#版Modbus全协议通信工具包:ASCII/RTU/TCP/UDP四模一体支持
  • STM32F103R6在Proteus里跑PWM和正弦波输出的完整仿真工程包(含Keil项目+HEX固件)
  • 别再乱写注释了!手把手教你用Doxygen生成专业API文档(附常用标记速查表)
  • OpenFPGA环境搭建踩坑实录:从GTK3到TBB,手把手解决编译中的5个常见报错
  • 魔兽争霸III全面优化指南:Warcraft Helper让你的经典游戏焕发新生
  • 从银行U盾到手机APP:聊聊HOTP/TOTP那些年我们踩过的‘坑’与最佳实践
  • BMS设计避坑指南:BQ76PL455电压采集不准?STM32通信干扰?这些细节你注意了吗?
  • SpringBoot+Vue实现的应急物资管理系统源码(含论文、开题报告与数据库脚本)
  • Adobe Dimension 2024深度测评
  • 2026合肥免砸砖漏水维修全攻略|卫生间/阳台/厨房/屋顶根治方法+避坑指南|苏易修缮 - 苏易修缮
  • 临安母婴除甲醛CMA甲醛检测治理公司深度测评:绿呼吸环保稳居榜首 - 一休咨询
  • C#写的实时运动检测小工具:接摄像头或视频文件,画框标出移动物体(VS工程直接编译运行)
  • 2026沈阳市权威认证贵金属回收 TOP5+黄金回收白银回收铂金回收门店地址电话推荐
  • 特征函数:连接概率论与信号处理的‘隐藏桥梁’,一个例子讲透