嵌入式Linux驱动开发从入门到精通 - 第1节

1.1 嵌入式系统与Linux内核简介

嵌入式系统是以应用为中心、以计算机技术为基础,软硬件可裁剪,适用于对功能、可靠性、成本、体积、功耗有严格要求的专用计算机系统。从智能手表、路由器到工业控制器、汽车ECU,嵌入式系统无处不在。

Linux内核因其开源、稳定、支持广泛的硬件架构(ARM、RISC-V、x86、MIPS等)以及强大的网络和文件系统能力,成为嵌入式操作系统的首选。嵌入式Linux通常指经过裁剪、定制后运行在资源受限设备上的Linux内核及用户空间工具集。

核心概念:嵌入式Linux ≠ 桌面Linux。嵌入式环境通常没有显示器、键盘,存储空间从几MB到几GB不等,内核需要针对特定硬件进行配置和编译。

嵌入式系统的典型分层结构

嵌入式系统软件分层架构 应用层(用户应用程序、GUI、协议栈) 系统库层(C库、libc、glibc/musl、中间件) Linux内核层(进程调度、内存管理、驱动、文件系统、网络) 硬件抽象层 / Bootloader(U-Boot、设备树、固件) 硬件层(SoC、外设、传感器、存储器、总线) 驱动开发位于内核层与硬件层之间,是连接软件与硬件的桥梁

1.2 驱动开发在嵌入式系统中的角色

设备驱动程序是内核中负责管理和控制特定硬件设备的软件组件。它向上提供统一的操作接口(如open、read、write、ioctl),向下直接操作硬件寄存器、处理中断、管理DMA传输。

角色维度 说明 典型工作
硬件抽象 屏蔽硬件差异,提供标准接口 实现file_operations结构体
资源管理 管理中断、内存、DMA、时钟等 申请IRQ、分配DMA缓冲区
数据交换 在用户空间与硬件之间搬运数据 copy_to_user / copy_from_user
电源管理 控制设备功耗状态 suspend/resume回调
错误处理 检测并恢复硬件异常 超时重试、看门狗复位
常见误区:驱动开发不是简单地“写代码控制硬件”,而是要在内核的复杂并发环境(多进程、中断、SMP)中保证正确性、实时性和稳定性。一个bug可能导致整个系统崩溃。

驱动在系统中的位置

设备驱动在Linux内核中的位置 用户空间(应用程序、系统调用接口) 虚拟文件系统(VFS) 设备驱动层(字符/块/网络驱动框架) 硬件设备(寄存器、中断控制器、外设总线) 驱动是内核中唯一直接与硬件交互的软件层

1.3 驱动开发环境搭建

嵌入式驱动开发采用交叉编译模式:在性能强大的宿主机(x86 PC)上编写和编译代码,生成的目标二进制在目标板(ARM/RISC-V)上运行。环境搭建是入门的第一道门槛。

交叉编译工具链

交叉编译工具链包含编译器、汇编器、链接器、调试器以及目标板的C库。常用工具链有:

  • Linaro GCC:针对ARM Cortex-A系列,社区广泛使用
  • Buildroot / Yocto:完整嵌入式Linux构建系统,自动生成工具链
  • ARM GNU Toolchain:ARM官方维护,支持arm-none-eabi和arm-linux-gnueabihf
  • RISC-V GNU Toolchain:RISC-V架构官方工具链
组件 宿主机工具 目标板工具
编译器 gcc (x86) arm-linux-gnueabihf-gcc
汇编器 as (x86) arm-linux-gnueabihf-as
链接器 ld (x86) arm-linux-gnueabihf-ld
调试器 gdb (x86) arm-linux-gnueabihf-gdb
目标C库 glibc (x86) arm-linux-gnueabihf/libc.so

内核源码树

驱动模块的编译需要依赖内核源码树中的头文件和Makefile规则。内核源码树是编译驱动模块的“脚手架”。

最佳实践:建议使用与目标板运行版本完全一致的内核源码。可通过 uname -r 查看目标板内核版本,然后从 kernel.org 下载对应源码。

内核源码树的关键目录结构:

linux-6.1.28/
├── arch/          # 体系结构相关代码(arm64, riscv, x86等)
├── block/         # 块设备层
├── crypto/        # 加密API
├── Documentation/ # 内核文档
├── drivers/       # 设备驱动(我们的主战场)
│   ├── char/      # 字符设备驱动
│   ├── net/       # 网络设备驱动
│   ├── i2c/       # I2C总线驱动
│   ├── spi/       # SPI总线驱动
│   └── ...        # 其他子系统
├── fs/            # 文件系统实现
├── include/       # 内核头文件
├── init/          # 内核初始化
├── kernel/        # 核心子系统(调度、信号、时间等)
├── lib/           # 内核库函数
├── mm/            # 内存管理
├── net/           # 网络协议栈
├── scripts/       # 编译脚本和工具
├── Makefile       # 顶层Makefile
└── Kconfig        # 内核配置菜单

环境搭建步骤

  1. 安装交叉编译工具链(以ARM为例):
    sudo apt install gcc-arm-linux-gnueabihf binutils-arm-linux-gnueabihf
  2. 下载并解压内核源码:
    wget https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.1.28.tar.xz
    tar -xf linux-6.1.28.tar.xz
    cd linux-6.1.28
  3. 配置内核(使用目标板默认配置或通用配置):
    make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- defconfig
  4. 编译内核(可选,但建议先编译一次以生成模块依赖):
    make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -j$(nproc)
  5. 验证工具链:
    arm-linux-gnueabihf-gcc --version
    arm-linux-gnueabihf-objdump --version

1.4 第一个Hello World内核模块

内核模块(Loadable Kernel Module, LKM)是可以在运行时动态加载到内核中的代码片段,无需重新编译整个内核。这是驱动开发的基本单元。

模块代码

// hello.c - 第一个内核模块
#include <linux/module.h>    // 所有模块都需要
#include <linux/kernel.h>    // printk、KERN_INFO等
#include <linux/init.h>      // module_init / module_exit宏

static int __init hello_init(void)
{
    printk(KERN_INFO "Hello, Embedded Linux Driver World!\n");
    printk(KERN_INFO "Module loaded successfully.\n");
    return 0;  // 0表示成功,负数表示错误
}

static void __exit hello_exit(void)
{
    printk(KERN_INFO "Goodbye, kernel module unloaded.\n");
}

module_init(hello_init);
module_exit(hello_exit);

MODULE_LICENSE("GPL");               // 许可证声明
MODULE_AUTHOR("Your Name");          // 作者
MODULE_DESCRIPTION("A simple Hello World kernel module");
MODULE_VERSION("1.0");

Makefile

# Makefile - 用于编译内核模块
obj-m := hello.o

# 内核源码树路径(根据实际环境修改)
KDIR := /home/user/linux-6.1.28

# 交叉编译工具链前缀
CROSS := arm-linux-gnueabihf-

all:
    $(MAKE) -C $(KDIR) M=$(PWD) ARCH=arm CROSS_COMPILE=$(CROSS) modules

clean:
    $(MAKE) -C $(KDIR) M=$(PWD) ARCH=arm CROSS_COMPILE=$(CROSS) clean

编译与测试流程

Hello World模块编译与测试流程 ① 编写hello.c和Makefile ② 执行make编译模块 ③ 生成hello.ko模块文件 ④ 拷贝到目标板 ⑤ insmod hello.ko加载模块 ⑥ dmesg查看内核日志 ⑦ rmmod hello卸载模块 ⑧ dmesg验证卸载消息 提示:目标板需运行与编译内核源码版本一致的内核,否则模块可能无法加载

关键命令速查

命令 功能 示例
insmod 加载模块到内核 insmod hello.ko
rmmod 从内核卸载模块 rmmod hello
modprobe 智能加载(自动处理依赖) modprobe hello
lsmod 列出已加载模块 lsmod | grep hello
modinfo 查看模块信息 modinfo hello.ko
dmesg 查看内核日志 dmesg | tail -10
调试小技巧:在开发阶段,可以在宿主机上使用QEMU模拟目标板,配合NFS挂载根文件系统,实现“编译-传输-加载”的快速迭代,无需反复烧写SD卡或Flash。

模块加载后的内核日志输出

# 在目标板终端执行
# insmod hello.ko
# dmesg | tail -5
[ 1234.567890] Hello, Embedded Linux Driver World!
[ 1234.567895] Module loaded successfully.

# rmmod hello
# dmesg | tail -3
[ 1240.123456] Goodbye, kernel module unloaded.