3. Linux内核模块基础与STM32MP驱动加载

在STM32MP平台上开发外设驱动,第一步通常不是直接写一个完整的platform driver,而是先写一个最小的内核模块,把它编译成.ko文件,再通过insmod或modprobe加载到运行中的Linux内核里。这个过程能帮助我们确认交叉编译工具链、内核头文件、模块版本、开发板启动方式是否全部对齐。本节围绕“最小可加载模块”展开,覆盖模块编写、编译、加载卸载、参数传递、符号导出、printk调试,并在STM32MP开发板上完成第一次验证。

一、内核模块与STM32MP的编译环境

内核模块是可以在内核运行时动态加载的二进制代码,扩展名通常为.ko。它运行在内核空间,不能调用用户空间的C库,只能使用内核导出的函数和宏。对于STM32MP157这类ARM Cortex-A7双核平台,模块必须使用与目标板内核完全一致的配置和版本进行交叉编译,否则加载时会报version magic不匹配或invalid module format。

典型环境由三部分组成:主机端交叉编译器(如arm-linux-gnueabihf-gcc或STM32MP官方SDK中的arm-ostl-linux-gnueabi-gcc)、已配置并编译过的内核源码树、以及目标板上的运行内核。三者版本必须一致。假设使用STM32MP15-EV1开发板,内核版本为5.10,则主机端应先确认:

# 查看交叉编译器
arm-ostl-linux-gnueabi-gcc --version

# 进入内核源码目录,确认已编译
cd ~/stm32mp1-kernel
make ARCH=arm CROSS_COMPILE=arm-ostl-linux-gnueabi- stm32mp15_defconfig
make ARCH=arm CROSS_COMPILE=arm-ostl-linux-gnueabi- -j8

# 确认Module.symvers和include/generated/autoconf.h存在
ls Module.symvers include/generated/autoconf.h

如果目标板内核是厂商预编译的,主机端必须拿到对应的内核源码和.config,否则模块的版本信息无法匹配。STM32MP的OpenSTLinux发行版通常把内核源码放在SDK的layers/meta-st/meta-st-stm32mp/recipes-kernel/linux/路径下,也可以从开发板/lib/modules/$(uname -r)/build软链接指向的目录获取。

二、编写第一个最小模块

最小模块需要包含linux/module.h和linux/init.h,并实现module_init和module_exit两个入口。下面是一个带模块参数和printk输出的示例,文件名为hello_stm32mp.c:

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

static int led_gpio = 0;
static char *board_name = "STM32MP157";

module_param(led_gpio, int, 0644);
MODULE_PARM_DESC(led_gpio, "GPIO number for LED");
module_param(board_name, charp, 0644);
MODULE_PARM_DESC(board_name, "Board name string");

static int __init hello_init(void)
{
    printk(KERN_INFO "hello_stm32mp: init, board=%s, led_gpio=%d\n",
           board_name, led_gpio);
    return 0;
}

static void __exit hello_exit(void)
{
    printk(KERN_INFO "hello_stm32mp: exit\n");
}

module_init(hello_init);
module_exit(hello_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Course Example");
MODULE_DESCRIPTION("Minimal STM32MP kernel module");
MODULE_VERSION("1.0");

这里module_param声明了两个模块参数:整型led_gpio和字符串board_name,权限0644表示在sysfs中可读可写。MODULE_LICENSE("GPL")必须写,否则内核会标记为污染,且无法使用GPL-only符号。printk的KERN_INFO表示日志级别,输出会进入内核环形缓冲区,可通过dmesg查看。

三、Makefile与交叉编译

模块的Makefile与普通应用不同,需要借助内核的kbuild系统。一个可用的Makefile如下:

obj-m += hello_stm32mp.o

KERNELDIR ?= ~/stm32mp1-kernel
ARCH ?= arm
CROSS_COMPILE ?= arm-ostl-linux-gnueabi-

all:
	make -C $(KERNELDIR) M=$(PWD) ARCH=$(ARCH) \
		CROSS_COMPILE=$(CROSS_COMPILE) modules

clean:
	make -C $(KERNELDIR) M=$(PWD) ARCH=$(ARCH) \
		CROSS_COMPILE=$(CROSS_COMPILE) clean

执行make后,目录中会生成hello_stm32mp.ko以及.mod.c、.o等中间文件。可以用modinfo检查模块信息:

modinfo hello_stm32mp.ko

输出中应能看到license: GPL、vermagic: 5.10.0 ... SMP preempt mod_unload ARMv7等字段。vermagic必须与目标板uname -r一致,否则加载失败。

四、加载、卸载与参数传递

把hello_stm32mp.ko通过scp或NFS传到开发板,例如放到/lib/modules/$(uname -r)/extra/。加载方式有两种:

# 直接加载,指定参数
insmod hello_stm32mp.ko led_gpio=98 board_name="STM32MP157-EV1"

# 或使用modprobe(需先depmod)
depmod -a
modprobe hello_stm32mp led_gpio=98 board_name="STM32MP157-EV1"

# 查看内核日志
dmesg | tail -n 20

# 查看已加载模块
lsmod | grep hello

# 查看模块参数
cat /sys/module/hello_stm32mp/parameters/led_gpio
cat /sys/module/hello_stm32mp/parameters/board_name

# 卸载
rmmod hello_stm32mp
dmesg | tail -n 5

预期日志中会出现hello_stm32mp: init, board=STM32MP157-EV1, led_gpio=98,卸载时出现hello_stm32mp: exit。如果insmod报Unknown symbol,说明模块引用了未导出的符号;如果报Invalid module format,优先检查vermagic和内核版本。

五、符号导出与模块间依赖

当多个模块需要互相调用时,被调用方要用EXPORT_SYMBOL或EXPORT_SYMBOL_GPL导出符号。例如模块A提供stm32mp_led_set(),模块B调用它:

/* 模块A:provider.c */
int stm32mp_led_set(int gpio, int value)
{
    /* 实际GPIO操作 */
    return 0;
}
EXPORT_SYMBOL_GPL(stm32mp_led_set);

/* 模块B:consumer.c */
extern int stm32mp_led_set(int gpio, int value);
static int __init consumer_init(void)
{
    stm32mp_led_set(98, 1);
    return 0;
}

加载顺序必须是先insmod provider.ko,再insmod consumer.ko;卸载顺序相反。使用modprobe时,depmod会根据Module.symvers自动生成依赖关系,无需手动控制顺序。

六、printk调试与日志级别

printk是内核模块最直接的调试手段。常用级别包括KERN_EMERG、KERN_ALERT、KERN_CRIT、KERN_ERR、KERN_WARNING、KERN_NOTICE、KERN_INFO、KERN_DEBUG。默认控制台只显示高于console_loglevel的消息,可通过dmesg -n 8或echo 8 > /proc/sys/kernel/printk临时提高。更推荐使用pr_info()、pr_err()、dev_info()等封装宏,它们自带模块名前缀,便于过滤。

在STM32MP开发板上,串口控制台通常为/dev/ttySTM0,波特率115200。如果dmesg看不到输出,先确认console=ttySTM0,115200已写入内核启动参数,并检查printk级别是否被过滤。

七、在STM32MP开发板上验证

完整验证流程可以归纳为:主机交叉编译得到.ko,传输到开发板,insmod加载并传参,dmesg确认init日志,lsmod确认模块存在,/sys/module读取参数,最后rmmod卸载并确认exit日志。若全部通过,说明工具链、内核版本、模块机制、参数传递和日志系统均已打通,可以进入下一步GPIO或platform驱动开发。

STM32MP 内核模块编译—加载—验证链路 主机侧(交叉编译) hello_stm32mp.c Makefile(obj-m + KERNELDIR) arm-ostl-linux-gnueabi-gcc 编译产物 hello_stm32mp.ko vermagic 5.10.0 license: GPL 参数: led_gpio, board_name make STM32MP157 开发板 insmod hello_stm32mp.ko led_gpio=98 dmesg | tail → init 日志 rmmod → exit 日志 scp/NFS 加载验证闭环与关键判断点 1. 编译 生成 .ko 2. 加载 insmod / modprobe 3. 观察 dmesg / lsmod 4. 读参数 /sys/module/... 5. 卸载 rmmod 常见失败判断 Invalid module format → vermagic/内核版本不一致;Unknown symbol → 符号未导出或依赖模块未先加载;无日志 → printk级别或console参数问题

完成上述验证后,你已经掌握了内核模块从源码到运行的最小闭环。后续无论是写GPIO驱动、I2C设备驱动还是platform driver,都会沿用同一套编译、加载、参数传递和printk调试方法,只是把hello_init中的打印替换为真实的硬件寄存器操作或子系统注册。