1. 嵌入式Linux驱动开发概述

大家好,我是你们这门课的老朋友。做了十几年嵌入式Linux驱动,踩过的坑比写过的代码还多。今天咱们先不急着写代码,把地基打牢。这一章,我带你看看嵌入式系统到底是个啥,Linux内核长什么样,以及怎么把开发环境搭起来。

1.1 嵌入式系统简介

嵌入式系统,说白了就是「专机专用」的计算机。它不像你桌上的PC,能跑各种软件。它被固化在某个设备里,干一件事,甚至一辈子就干一件事。

我最早接触嵌入式系统,是在一个工业控制项目里。那会儿用的还是ARM9,主频才200MHz,内存32MB。你想想看,这点资源跑个Linux都费劲,但人家在工厂里稳定跑了七八年没重启过。这就是嵌入式系统的魅力——资源受限,但可靠性极高。

嵌入式系统的核心特征:

  • 专用性:软硬件针对特定任务定制,没有多余功能
  • 资源受限:CPU主频低、内存小、存储空间有限
  • 实时性要求:很多场景需要在规定时间内响应外部事件
  • 低功耗:电池供电的设备,功耗是硬指标
  • 高可靠性:一旦部署,可能几年不关机、不重启

一个典型的嵌入式系统,硬件上包含这几部分:

组件 说明 常见例子
处理器 (CPU/SoC) 系统的核心,负责运算和控制 ARM Cortex-A/M/R, RISC-V, MIPS
内存 (RAM) 程序运行时的临时存储 DDR3/DDR4, SRAM
存储 (ROM/Flash) 存放固件、操作系统和应用程序 NAND Flash, eMMC, SPI Flash
外设接口 与外部世界交互的通道 UART, I2C, SPI, GPIO, USB, Ethernet
传感器/执行器 感知环境或执行动作 温度传感器, 电机, 显示屏

软件层面呢?从底往上依次是:引导加载程序(Bootloader) → 操作系统内核 → 驱动程序 → 文件系统 → 应用程序。咱们这门课,就聚焦在「驱动程序」这一层,它是连接硬件和操作系统的桥梁。

1.2 Linux内核架构概览

Linux内核是个庞然大物,但别怕。咱们做驱动开发,不需要把整个内核吃透。你只需要搞清楚它怎么组织、驱动在内核里处于什么位置就行。

我个人习惯把内核看成一个大管家。应用程序是主人,硬件是仆人,内核就是那个调度一切、协调一切的管家。驱动呢?就是管家手里那本「设备使用手册」——告诉管家怎么跟不同的仆人打交道。

来看内核的宏观架构:

Linux 内核架构概览 用户空间 (User Space) — 应用程序、C库 系统调用接口 (System Call Interface) 内核空间 (Kernel Space) 进程管理 (调度、IPC) 内存管理 (MMU、页分配) 文件系统 (VFS、具体FS) 网络协议栈 (TCP/IP、Socket) 设备驱动框架 (字符/块/网络设备驱动) 硬件层 (Hardware) — CPU、内存、外设控制器 驱动开发者的主战场:设备驱动框架层,向下操作硬件寄存器,向上通过系统调用服务应用程序

看到红色那块了吗?那就是咱们的「主战场」——设备驱动框架。它下面直接跟硬件打交道,上面通过系统调用接口服务用户空间的应用程序。

Linux把设备驱动分成三大类:

  • 字符设备驱动:像串口、GPIO、I2C这类,数据按字节流读写。最常见,也是咱们课程的重点。
  • 块设备驱动:像eMMC、SD卡、硬盘,数据以块为单位读写。有缓存机制,比字符设备复杂。
  • 网络设备驱动:像以太网、WiFi,数据以数据包为单位收发。不走文件系统那套接口。

我的经验:刚入门时,先死磕字符设备驱动。它最简单、最直观,但包含了驱动开发的所有核心概念——file_operations、设备号、内存映射、中断处理。把这些搞透了,块设备和网络设备只是换层皮而已。

1.3 驱动开发环境搭建

环境搭不好,后面全是坑。我见过太多新手卡在编译环境上,代码写好了,一编译全是「找不到头文件」的错误。咱们一步步来。

1.3.1 交叉编译工具链

为什么需要交叉编译?因为你的开发机是x86架构,而目标板可能是ARM、RISC-V或者MIPS。x86的编译器编译出来的程序,ARM芯片跑不了。所以你需要一个在x86上运行、但生成ARM机器码的编译器——这就是交叉编译工具链。

工具链的命名有规律,比如:

工具链前缀 目标架构 示例
arm-linux-gnueabihf- ARM 32位,硬浮点 arm-linux-gnueabihf-gcc
aarch64-linux-gnu- ARM 64位 aarch64-linux-gnu-gcc
riscv64-linux-gnu- RISC-V 64位 riscv64-linux-gnu-gcc

安装方式很简单。以Ubuntu为例,安装ARM 32位工具链:

sudo apt-get install gcc-arm-linux-gnueabihf

验证是否安装成功:

arm-linux-gnueabihf-gcc --version

看到版本信息就对了。

注意:我曾经在一个项目里,因为工具链版本跟内核版本不匹配,编译出来的驱动模块加载时直接报「Unknown symbol」错误。排查了两天才发现是工具链太老,生成的ELF格式跟内核不兼容。所以,工具链版本尽量跟内核文档推荐的保持一致。

1.3.2 内核源码树

驱动是内核的一部分,编译驱动时必须依赖内核源码树。源码树里包含了驱动需要的头文件、内核模块编译规则、以及各种宏定义。

获取内核源码有两种方式:

  1. 从内核官网下载:https://www.kernel.org/,选一个稳定版本。
  2. 从芯片厂商获取:比如NXP、TI、Rockchip都会提供适配好的内核源码包。我建议用这个,因为厂商已经帮你打好了板级支持补丁。

下载后解压:

tar -xvf linux-5.10.tar.xz
cd linux-5.10

然后配置内核。如果你有目标板的配置文件(通常叫 defconfig),直接加载:

make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- xxx_defconfig

如果没有,可以先编译一次内核,生成必要的头文件:

make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- menuconfig
# 保存退出
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- modules_prepare

modules_prepare 这个目标很关键。它只编译内核源码树中驱动模块依赖的部分,不会编译整个内核。速度快,够用。

1.3.3 第一个驱动模块的编译框架

环境搭好了,咱们写个最简单的驱动模块——不操作硬件,只打印一句话。目的是验证编译环境是否正常。

创建一个 hello_drv.c:

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>

static int __init hello_init(void)
{
    printk(KERN_INFO "Hello, Embedded Linux Driver!\n");
    return 0;
}

static void __exit hello_exit(void)
{
    printk(KERN_INFO "Goodbye, Embedded Linux Driver!\n");
}

module_init(hello_init);
module_exit(hello_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple hello world module");

再创建一个 Makefile:

obj-m := hello_drv.o

KERNEL_DIR ?= /path/to/your/kernel/source
CROSS_COMPILE ?= arm-linux-gnueabihf-

all:
    make -C $(KERNEL_DIR) M=$(PWD) ARCH=arm CROSS_COMPILE=$(CROSS_COMPILE) modules

clean:
    make -C $(KERNEL_DIR) M=$(PWD) ARCH=arm CROSS_COMPILE=$(CROSS_COMPILE) clean

注意:KERNEL_DIR 要替换成你实际的内核源码路径。M=$(PWD) 告诉内核构建系统,去当前目录找驱动源码。

编译:

make

如果一切顺利,你会看到 hello_drv.ko 文件生成。这就是内核模块文件。

小技巧:编译时如果报错,先检查 KERNEL_DIR 路径对不对。我习惯在 Makefile 里用 ?= 赋值,这样可以在命令行覆盖:make KERNEL_DIR=/home/user/linux-5.10,灵活很多。

把 .ko 文件拷贝到目标板上,加载测试:

insmod hello_drv.ko
rmmod hello_drv.ko

用 dmesg 查看内核日志,应该能看到我们打印的那两句话。

嗯,到这里,你的驱动开发环境就算搭好了。别看步骤简单,这背后涉及了交叉编译、内核模块机制、Makefile语法。把这些搞明白,后面写真正的硬件驱动时,你才能把精力放在逻辑上,而不是被编译环境折腾。