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把设备驱动分成三大类:
- 字符设备驱动:像串口、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 内核源码树
驱动是内核的一部分,编译驱动时必须依赖内核源码树。源码树里包含了驱动需要的头文件、内核模块编译规则、以及各种宏定义。
获取内核源码有两种方式:
- 从内核官网下载:
https://www.kernel.org/,选一个稳定版本。 - 从芯片厂商获取:比如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语法。把这些搞明白,后面写真正的硬件驱动时,你才能把精力放在逻辑上,而不是被编译环境折腾。