例程代码路径:ELF 1开发板资料包\03-例程源码\03-2 驱动例程源码\01_helloworld\hello
本章节先写一个helloworld驱动,不涉及对硬件的操作,它的目的是展示驱动程序的基本结构和加载过程。
源码编写
(一)首先包含头文件
#include <linux/module.h> // 包含模块相关函数的头文件
#include <linux/kernel.h> // 包含内核相关函数的头文件
#include <linux/init.h> // 包含初始化和清理函数的头文件
(二)驱动模块入口和出口函数
module_init(helloworld_init); // 指定驱动程序的初始化函数
module_exit(helloworld_exit); // 指定驱动程序的清理函数
module_init()的定义:
#define module_init(init_fn) \
static inline initcall_t __initcall_##init_fn##_wrapper(void) \
{ \
return init_fn; \
} \
int init_module(void) __attribute__((alias(#init_fn)));
module_init()宏接受一个初始化函数作为参数,并将其定义为内联函数__initcall_##init_fn##_wrapper()。然后,它使用__attribute__((alias(#init_fn)))将init_module()函数与初始化函数进行关联。
在Linux内核加载模块时,会调用init_module()函数,它实际上是一个入口点函数。通过将module_init()定义为init_module()的别名,使得初始化函数成为模块加载时的入口点函数。
使用module_init()宏的目的是在驱动程序中指定初始化函数,以便在模块加载时调用该函数执行必要的初始化操作。
当使用insmod将模块加载进内核的时候,初始化函数的代码将会被执行。模块初始化代码只与内核模块管理子系统打交道,并不与应用程序直接交互。
module_exit()的定义:
#define module_exit(exit_fn) \
static inline exitcall_t __exitcall_##exit_fn##_wrapper(void) \
{ \
return exit_fn; \
} \
void cleanup_module(void) __attribute__((alias(#exit_fn)));
module_exit()宏接受一个退出函数作为参数,并将其定义为内联函数__exitcall_##exit_fn##_wrapper()。然后,它使用__attribute__((alias(#exit_fn)))将cleanup_module()函数与退出函数进行关联。
在Linux内核卸载模块时,会调用cleanup_module()函数,它实际上是一个出口点函数。通过将module_exit()定义为cleanup_module()的别名,使得退出函数成为模块卸载时的出口点函数。
使用module_exit()宏的目的是在驱动程序中指定退出函数,以便在模块卸载时调用该函数执行必要的清理操作。
几点说明:
(1)模块退出没有返回值;
(2)__exit标记这段代码仅用于模块卸载;
(3)module_exit不是必须的。但是,没有module_exit定义的模块无法被卸载,如果需要支持模块卸载则必须有module_exit;
当使用当使用rmmod卸载模块时,退出函数的代码将被执行。模块退出代码只与内核模块管理子系统打交道,并不直接与应用程序交互。
(三)入口函数和出口函数实现
static int __init helloworld_init(void)
{
printk(KERN_INFO "Hello, World!\n"); // 打印消息到内核日志
return 0;
}
static void __exit helloworld_exit(void)
{
printk(KERN_INFO "Goodbye, World!\n"); // 打印消息到内核日志
}
内核加载时输出Hello, World!,内核卸载时输出Goodbye, World!
(四)声明模块信息
MODULE_LICENSE("GPL"); // 指定模块的许可证信息
MODULE_AUTHOR("Your Name"); // 指定模块的作者信息
MODULE_DESCRIPTION("A simple Hello World driver"); // 指定模块的描述信息
hello.c示例源码
#include <linux/module.h> // 包含模块相关函数的头文件
#include <linux/kernel.h> // 包含内核相关函数的头文件
#include <linux/init.h> // 包含初始化和清理函数的头文件
static int __init helloworld_init(void)
{
printk(KERN_INFO "Hello, World!\n"); // 打印消息到内核日志
return 0;
}
static void __exit helloworld_exit(void)
{
printk(KERN_INFO "Goodbye, World!\n"); // 打印消息到内核日志
}
module_init(helloworld_init); // 指定驱动程序的初始化函数
module_exit(helloworld_exit); // 指定驱动程序的清理函数
MODULE_LICENSE("GPL"); // 指定模块的许可证信息
MODULE_AUTHOR("Your Name"); // 指定模块的作者信息
MODULE_DESCRIPTION("A simple Hello World driver"); // 指定模块的描述信息
编译
编译驱动分为两种形式,一种是编译现成模块,生成.ko文件然后通过手动加载;另一种是编译到内核,在内核启动时自动加载。一般在驱动调试阶段会编译成模块,这样便于调试,不用每次都重新编译整个内核,本章节以编译成模块进行讲解。
在hello.c同级目录下编写Makefile:
LINUX_KERNEL_PATH = /home/elf/work/linux-imx-imx_4.1.15_2.0.0_ga
CURRENT_PATH := $(shell pwd)
CROSS_COMPILE = arm-poky-linux-gnueabi-
CC = $(CROSS_COMPILE)gcc -march=armv7ve -mfpu=neon -mcpu=cortex-a7 --sysroot=/opt/fsl-imx-x11/4.1.15-2.0.0/sysroots/cortexa7hf-neon-poky-linux-gnueabi
obj-m :=hello.o
all:
$(MAKE) -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) modules
clean:
$(MAKE) -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) modules clean
.PHONY: modules clean
-C:参数后面加.config文件所在的文件夹;
M:参数后面是要编译的模块;
LINUX_KERNEL_PATH:为要依赖内核的目录;
CURRENT_PATH:目前模块的目录;
CROSS_COMPILE:编译器的前缀;
CC:编译器以及参数;
注意:LINUX_KERNEL_PATH的路径要以实际源码存放的路径为准。
注意:复制粘贴过程中可能会出现格式问题。
添加后效果如下:
设置环境变量,编译:
. /opt/fsl-imx-x11/4.1.15-2.0.0/environment-setup-cortexa7hf-neon-poky-linux-gnueabi
elf@ubuntu:~/work/test/01_helloworld/hello$ make
将生成的hello.ko文件拷贝到开发板中测试。在开发板中执行insmod加载驱动,rmmod卸载驱动:
root@ELF1:~# insmod hello.ko
Hello, World!
root@ELF1:~# rmmod hello.ko
Goodbye, World!
可以看出加载时进入到了helloworld_init函数,并打印了Hello, World!,卸载函数时进入了helloworld_exit函数并打印了Goodbye, World!