4. 第一个驱动:Hello World 模块

终于到了动手写代码的时候了。说实话,很多初学者在搭建好环境后,反而不知道从哪里下手。我个人的建议是——先写一个最简单的模块,跑通加载、卸载、看日志的完整流程。这个 Hello World 虽然简单,但它能帮你验证整个工具链是否正常。

4.1 模块代码:hello.c

先看代码,再解释。你创建一个 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 from Zynq!\n");
    return 0;
}

static void __exit hello_exit(void)
{
    printk(KERN_INFO "Goodbye, Zynq World!\n");
}

module_init(hello_init);
module_exit(hello_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple Hello World module for Zynq");

这里有几个关键点,我当年第一次写的时候也踩过坑:

  • module_init / module_exit:这是内核模块的入口和出口宏。加载模块时调用 hello_init,卸载时调用 hello_exit。
  • printk:内核空间的打印函数,不是 printf。它把信息输出到内核日志缓冲区,而不是终端。
  • KERN_INFO:日志级别宏,表示这是一条普通信息。还有 KERN_ERR、KERN_DEBUG 等。
  • __init / __exit:这两个标记告诉内核:初始化函数在运行后可以释放内存,退出函数在模块卸载时使用。
小技巧: 我习惯在 printk 里加上模块名前缀,比如 "hello: ",这样在内核日志里一眼就能找到自己的输出。

4.2 Makefile:构建模块

模块不能像普通程序那样用 gcc 直接编译。你需要内核源码树和交叉编译工具链。Makefile 如下:

obj-m := hello.o

KDIR := /path/to/your/linux-xlnx
CROSS_COMPILE := arm-linux-gnueabihf-
CC := $(CROSS_COMPILE)gcc

all:
    $(MAKE) -C $(KDIR) M=$(PWD) modules

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

解释一下:

  • obj-m := hello.o:告诉内核构建系统,把 hello.c 编译成一个可加载模块。
  • KDIR:指向你之前编译好的内核源码树路径。我一般用绝对路径,避免麻烦。
  • CROSS_COMPILE:交叉编译工具链前缀。Zynq 是 ARM 架构,所以用 arm-linux-gnueabihf-。
  • -C $(KDIR):切换到内核源码目录执行 make。
  • M=$(PWD):指定模块源码在当前目录。
注意: 我曾经因为 KDIR 指向了错误的内核版本,导致编译出来的模块加载时直接报 Invalid module format。一定要确保 KDIR 和你板子上运行的内核是同一个版本。

4.3 编译与加载

在终端执行:

make

如果一切正常,你会看到 hello.ko 生成。把这个文件拷贝到 Zynq 板子上,然后:

insmod hello.ko

没有输出?很正常。模块加载成功不会在终端打印东西。你需要查看内核日志:

dmesg | tail

你应该能看到:

[   12.345678] Hello World from Zynq!

卸载模块:

rmmod hello
dmesg | tail

输出:

[   15.123456] Goodbye, Zynq World!
核心流程: 编写代码 → Makefile 编译 → insmod 加载 → dmesg 查看日志 → rmmod 卸载。这个循环贯穿整个驱动开发过程。

4.4 查看内核日志的几种方式

命令/工具 说明 常用场景
dmesg 显示内核环形缓冲区所有日志 查看最近日志,配合 tail 使用
dmesg -w 实时等待新日志(类似 tail -f) 调试时实时观察模块输出
cat /var/log/kern.log 读取持久化的内核日志文件 查看历史日志,系统重启后仍可查
journalctl -k systemd 系统下的内核日志查看 现代发行版,支持过滤和搜索

我个人最常用的是 dmesg -w。开一个终端窗口跑着它,然后另一个窗口加载模块,日志实时刷新,非常直观。

4.5 模块加载与卸载的完整流程

下面这张图展示了从编译到卸载的完整链路:

Hello World 模块生命周期 编写 hello.c 编写 Makefile make 编译 生成 hello.ko 拷贝到目标板 insmod 加载 dmesg 查看日志 rmmod 卸载 dmesg 确认退出 insmod 调用 module_init → hello_init → printk → 返回 0 → 模块注册成功

4.6 常见问题与避坑

  • insmod 报错 "Invalid module format":内核版本不匹配。检查 KDIR 是否指向正确的内核源码。
  • insmod 报错 "Operation not permitted":权限问题。用 root 执行,或者加 sudo。
  • dmesg 看不到输出:可能是日志级别被过滤了。试试 dmesg -n 7 降低控制台日志级别。
  • rmmod 报错 "Module is in use":有其他进程或模块依赖它。用 lsmod 查看引用计数。
曾经踩过的坑: 我有一回在 Makefile 里忘了加 CROSS_COMPILE,结果编译出来的是 x86 的 .ko 文件。放到 Zynq 板子上加载,直接报 wrong architecture。所以编译前一定要确认工具链。

4.7 小结

Hello World 模块虽然简单,但它验证了整个开发链路的正确性:

  • 交叉编译工具链是否正常
  • 内核源码树是否匹配
  • 模块加载/卸载机制是否工作
  • 内核日志输出是否可见

这个流程你以后会反复使用。每写一个新模块,都是先跑通这个循环,再往里加功能。嗯,基础打牢了,后面的事情就顺了。