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()等封装宏,它们自带模块名前缀,便于过滤。
/dev/ttySTM0,波特率115200。如果dmesg看不到输出,先确认console=ttySTM0,115200已写入内核启动参数,并检查printk级别是否被过滤。七、在STM32MP开发板上验证
完整验证流程可以归纳为:主机交叉编译得到.ko,传输到开发板,insmod加载并传参,dmesg确认init日志,lsmod确认模块存在,/sys/module读取参数,最后rmmod卸载并确认exit日志。若全部通过,说明工具链、内核版本、模块机制、参数传递和日志系统均已打通,可以进入下一步GPIO或platform驱动开发。
完成上述验证后,你已经掌握了内核模块从源码到运行的最小闭环。后续无论是写GPIO驱动、I2C设备驱动还是platform driver,都会沿用同一套编译、加载、参数传递和printk调试方法,只是把hello_init中的打印替换为真实的硬件寄存器操作或子系统注册。