第一课:课程导学与环境搭建

各位同学,欢迎来到《嵌入式网络摄像头Linux驱动开发实战》。我是你们的老朋友,一个在嵌入式Linux领域摸爬滚打了十几年的工程师。

这一章,咱们先把“地基”打牢。说白了,就是搭建开发环境、搞定内核、跑通第一个驱动模块。别小看这一步,我见过太多人卡在环境上,代码写好了却跑不起来,那叫一个憋屈。

1.1 为什么环境搭建这么重要?

你想想看,嵌入式开发不像你在Windows上写个Hello World那么简单。我们是在一台电脑(宿主机)上写代码,编译出能在另一台设备(目标板)上运行的程序。这中间隔着一道“交叉编译”的坎。

我刚开始做驱动时,有一次在Ubuntu上直接编译了个驱动,然后兴冲冲地拷到ARM板上一加载……嗯,内核报错说“格式不对”。后来才意识到,我用的gcc是x86架构的,而目标板是ARM架构的。从那以后,我再也不敢搞混编译链了。

核心概念:交叉编译链就是一套工具,让你在PC上生成目标板能执行的二进制文件。没有它,寸步难行。

1.2 Ubuntu开发环境搭建

我个人习惯用Ubuntu 20.04 LTS,稳定、软件源丰富。当然,你用18.04或22.04也行,差别不大。

安装完系统后,第一件事就是装必要的软件包。别嫌麻烦,这一步省了,后面编译内核时会哭的。

sudo apt-get update
sudo apt-get install build-essential kernel-package libncurses5-dev \
    bison flex libssl-dev libelf-dev git vim

这里我解释一下:build-essential包含了gcc、g++、make等基础工具;libncurses5-dev是内核配置菜单的图形库;libssl-devlibelf-dev是编译内核时需要的依赖。

小技巧:我建议你装一个tree命令,查看目录结构特别方便。sudo apt-get install tree 就行。

1.3 交叉编译链的安装

接下来是重头戏——交叉编译链。我们用的是ARM架构,所以需要arm-linux-gnueabihf这个工具链。

为什么是“hf”?因为它支持硬件浮点运算。如果你的目标板是纯ARM(比如ARM9),那就用arm-linux-gnueabi。我在项目中遇到过,有人用错了版本,结果程序一跑浮点运算就崩溃,排查了半天。

安装方法很简单,直接apt安装:

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

装完后,验证一下:

arm-linux-gnueabihf-gcc --version

看到版本信息,就说明成功了。

注意:如果你用的是64位Ubuntu,可能需要安装32位兼容库:sudo apt-get install libc6:i386。否则交叉编译链可能无法运行。

1.4 内核源码获取与编译

驱动是内核的一部分,所以我们需要内核源码。我建议从官方仓库下载,或者用你目标板厂商提供的内核。这里我用主线内核做演示。

wget https://cdn.kernel.org/pub/linux/kernel/v5.x/linux-5.10.100.tar.xz
tar -xf linux-5.10.100.tar.xz
cd linux-5.10.100

然后配置内核。对于初学者,我推荐用默认配置:

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

如果你想定制,可以用make menuconfig。嗯,这里要注意,menuconfig需要ncurses库,刚才我们已经装过了。

编译内核:

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

-j4表示用4个线程并行编译,能快不少。我第一次编译时不知道这个参数,单线程跑了快一个小时……

编译完成后,你会看到arch/arm/boot/zImage,这就是内核镜像。

1.5 第一个驱动模块:Hello World

终于到了激动人心的时刻——写第一个驱动模块。说白了,就是一个“Hello World”,但它是内核模块,不是用户态程序。

先创建一个目录:

mkdir ~/hello_driver
cd ~/hello_driver

然后写代码。新建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! Module loaded.\n");
    return 0;
}

static void __exit hello_exit(void)
{
    printk(KERN_INFO "Goodbye, World! Module removed.\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.o

KDIR := /home/yourname/linux-5.10.100
CROSS := arm-linux-gnueabihf-

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

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

注意,KDIR要改成你内核源码的实际路径。我习惯用绝对路径,避免出错。

编译:

make

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

1.6 模块的加载与卸载

hello.ko拷贝到目标板上,然后:

insmod hello.ko   # 加载模块
lsmod             # 查看已加载的模块
rmmod hello       # 卸载模块

dmesg查看内核日志:

dmesg | tail

你会看到:

[ 1234.5678] Hello, World! Module loaded.
[ 1235.6789] Goodbye, World! Module removed.

看到这两行,恭喜你,第一个驱动模块跑通了!

避坑指南:我曾经在加载模块时遇到“Invalid module format”错误。后来发现是内核版本不匹配。记住,模块必须和运行中的内核版本完全一致,包括编译配置。所以,最好用目标板上的内核源码来编译模块。

1.7 本章小结

这一章我们做了三件事:

  • 搭建了Ubuntu开发环境,安装了必要的工具
  • 配置了ARM交叉编译链,编译了内核
  • 编写并加载了第一个驱动模块

这些是后续所有课程的基础。别着急,慢慢来。下一章,我们会深入驱动框架,开始写真正有用的驱动。

记住,驱动开发没有捷径,但每一步都算数。有问题随时交流,咱们下节课见。

课后练习:试着修改Hello World模块,让它打印出当前进程的PID和名字。提示:使用current宏。