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

大家好,欢迎来到这门实战课。我是你们的讲师,一个在嵌入式Linux驱动领域摸爬滚打了十几年的老工程师。

今天咱们聊第一个话题:什么是嵌入式Linux驱动?

说白了,驱动就是操作系统和硬件之间的“翻译官”。你想想看,Linux内核不认识你板子上的那个WiFi芯片,也不认识那个LED灯。驱动的作用,就是告诉内核:“嘿,这个硬件该怎么用,我给你封装好接口。”

我个人的理解是:驱动开发,本质上是在写“硬件操作说明书”,只不过这份说明书是用C语言写的,而且Linux内核能读懂。

1.1 驱动到底长什么样?

一个典型的驱动,包含这几部分:

  • 初始化函数:硬件上电、注册设备
  • 操作函数集:比如 open、read、write、ioctl
  • 中断处理:硬件有事件来了,怎么响应
  • 注销函数:卸载驱动时清理资源

嗯,这里要注意:驱动不是应用程序。它没有 main 函数,它是一组被内核调用的函数集合。

核心概念:驱动运行在内核空间,应用程序运行在用户空间。两者通过系统调用交互。驱动崩溃 = 整个系统崩溃,所以写驱动要格外小心。

1.2 驱动开发环境搭建

好,理论说完了,咱们来点实际的。环境搭不好,后面全是白搭。我见过太多新手卡在这一步了。

1.2.1 交叉编译链

为什么需要交叉编译链?因为你的开发电脑是x86架构,而机顶盒是ARM架构。你不能在x86上编译出ARM能跑的程序——除非用交叉编译链。

我个人习惯用 arm-linux-gnueabihf- 这套工具链。安装很简单:

# 以Ubuntu为例
sudo apt-get install gcc-arm-linux-gnueabihf

# 验证安装
arm-linux-gnueabihf-gcc --version

我曾经遇到过一个问题:新手买了开发板,结果板子是ARMv7架构,却装了ARMv8的交叉编译链。编译出来的驱动一加载就报错。嗯,架构一定要匹配。

小技巧:不确定板子的架构?在板子上执行 uname -a 或者 cat /proc/cpuinfo,一看便知。

1.2.2 内核源码树

驱动编译需要内核头文件。你不能随便拿一个内核源码来编译,必须用目标板子上运行的那个内核版本的源码。

为什么?因为内核的API会变。你用5.10的内核头文件编译的驱动,加载到4.19的内核上,大概率会崩溃。

获取内核源码树的方式:

  • 从芯片厂商(比如海思、晶晨、全志)获取BSP包
  • 或者从开发板官网下载
  • 再或者,自己用 git clone 拉取对应版本

配置好内核源码树后,记得先编译一遍:

cd /path/to/kernel
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- defconfig
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- modules_prepare

这一步会生成 Module.symvers 等必要文件。少了它,你的驱动编译会报一堆未定义符号的错误。

避坑指南:我曾经因为偷懒,直接用 make menuconfig 然后保存退出,结果内核配置和板子实际配置不一致,驱动加载时符号版本不匹配。后来我学乖了,一定用板子自带的 .config 文件。

1.3 Hello World 驱动模块

好了,环境搭好了,咱们写第一个驱动。这是驱动界的“Hello World”,虽然简单,但五脏俱全。

1.3.1 编写代码

创建一个文件 hello.c

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

static int __init hello_init(void)
{
    printk(KERN_INFO "Hello, World! 驱动已加载\n");
    return 0;
}

static void __exit hello_exit(void)
{
    printk(KERN_INFO "再见,驱动已卸载\n");
}

module_init(hello_init);
module_exit(hello_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("一个简单的Hello World驱动");

注意看,这里没有 main 函数。入口是 module_init 指定的函数,出口是 module_exit 指定的函数。

printk 是内核的打印函数,相当于用户空间的 printf。它的输出可以通过 dmesg 命令查看。

1.3.2 编写Makefile

再创建一个 Makefile

obj-m := hello.o

KDIR := /path/to/your/kernel/tree
CROSS_COMPILE := arm-linux-gnueabihf-

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

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

这里 obj-m := hello.o 告诉内核:我要编译一个名为 hello 的模块。KDIR 指向你的内核源码树路径。

1.3.3 编译与加载

执行 make,你会得到 hello.ko 文件。这就是编译好的驱动模块。

把它传到板子上,然后:

# 加载驱动
insmod hello.ko

# 查看内核日志
dmesg | tail

# 你应该能看到:Hello, World! 驱动已加载

# 卸载驱动
rmmod hello

# 再次查看日志
dmesg | tail

# 你应该能看到:再见,驱动已卸载

重要提醒insmodmodprobe 的区别?insmod 直接加载指定文件,不处理依赖。modprobe 会处理依赖关系,但需要把模块放到 /lib/modules/ 下。新手先用 insmod 就好。

1.4 我踩过的坑

最后分享几个我当年踩过的坑,希望你们能绕过去:

  • 内核版本不匹配:编译环境的内核版本和板子运行的内核版本必须一致,否则加载时报 Invalid module format
  • 缺少符号导出:如果你的驱动依赖其他模块导出的符号,需要先加载那个模块
  • printk 看不到输出:检查日志级别,有时候需要 echo 8 > /proc/sys/kernel/printk 才能看到

嗯,今天就到这里。下一章咱们会深入讲解字符设备驱动,那才是真正的实战开始。

记住:驱动开发没有捷径,多动手、多踩坑、多总结。我在下一章等你。