linux内核模块
- 前提:驱动开发思路:
-
- [Source_Insight 工程创建和同步:](#Source_Insight 工程创建和同步:)
- led操作原理:
- [1. linux内核模块](#1. linux内核模块)
-
- [1.1 模块组成](#1.1 模块组成)
- [1.2 使用 Kbuild 编译模块](#1.2 使用 Kbuild 编译模块)
-
- [1.2.1 编写Makefile文件](#1.2.1 编写Makefile文件)
- [1.2.2 编译内核模块](#1.2.2 编译内核模块)
- [1.3 在开发板中测试内核模块](#1.3 在开发板中测试内核模块)
-
- [1.3.1 将编译好的KO文件拷贝到文件系统中](#1.3.1 将编译好的KO文件拷贝到文件系统中)
- [1.3.2 在开发板的命令行加载模块到系统中](#1.3.2 在开发板的命令行加载模块到系统中)
- [1.4 驱动开发工具 ---- source insight](#1.4 驱动开发工具 ---- source insight)
- [1.5 模块的特点](#1.5 模块的特点)
-
- [1.5.1 模块传参](#1.5.1 模块传参)
- [1.5.2 模块调用](#1.5.2 模块调用)
- [2. 完整的驱动组成](#2. 完整的驱动组成)
-
- [2.1 申请设备号](#2.1 申请设备号)
- [2.2 创建设备结点](#2.2 创建设备结点)
-
- [2.2.1 手动创建](#2.2.1 手动创建)
- [2.2.2 在驱动中创建](#2.2.2 在驱动中创建)
- [2.2.3 硬件初始化](#2.2.3 硬件初始化)
- [2.2.4 实现操作接口](#2.2.4 实现操作接口)
- [3. 应用与驱动之间传递数据](#3. 应用与驱动之间传递数据)
-
- [3.1 应用数据传给内核 ------ 实现write时使用](#3.1 应用数据传给内核 ------ 实现write时使用)
- [3.2 内核数据传给应用 ------ 实现read时使用](#3.2 内核数据传给应用 ------ 实现read时使用)
本文重点围绕以下四个方面展开,帮助你从"了解驱动开发流程"走向"掌握可落地的实践方法":
- 驱动开发思路
- Source Insight 工程建立与同步
- LED 驱动的硬件与软件原理
- Linux 内核模块(驱动)开发全流程
前提:驱动开发思路:
驱动开发思路:从需求到代码的闭环
公式: 原理图 → 芯片手册 → Linux 内核 → 驱动框架 → 应用交互 → 调试验证
- 了解业务需求
- 确定用户空间需要怎样访问设备(文件节点 / sysfs / netlink 等)。
- 明确控制目标(开关 LED、读写传感器、DMA 传输等)。
- 硬件分析
- 阅读原理图:确认使用的 GPIO、供电、外设接口。
- 阅读芯片手册(TRM):了解寄存器、时序、备用模式。
- 确认设备是否由设备树描述、是否需要电源管理、复用控制。
- 驱动框架选择
- 字符设备、块设备、platform driver、I²C/SPI 驱动、miscdevice、子系统(如 LED framework、input、V4L2 等)。
- API 设计
- 定义
open/read/write/ioctl/poll等函数行为。 - 规划内核与用户层的数据结构与协议(结构体布局、命令字、阻塞/非阻塞设计)。
- 定义
- 核心实现步骤
- 分配/注册设备号或 platform 设备。
- 内存映射
ioremap、GPIO 配置、时钟/复用控制。 - 中断申请 (
request_irq)、定时器、工作队列等。 - 导出 sysfs 属性、
debugfs节点或proc接口。 - 支持电源管理(
suspend/resume)与错误恢复。
- 测试与调试
dmesg查看日志,结合printk/dev_info。- 使用
strace/hexdump/cat/shell脚本驱动测试。 - 测量波形(示波器/逻辑分析仪)验证实际硬件反应。
- 内核调试工具:
ftrace、kgdb、dynamic debug、trace_printk。
- 发布与维护
- 模块参数、sysfs 节点、文档 (
Documentation/)。 - Kconfig 选项及 Makefile 集成。
- 模块参数、sysfs 节点、文档 (
若进入主线需符合内核编码规范 (scripts/checkpatch.pl)。



Source_Insight 工程创建和同步:
Source Insight(SI)是经典的 Windows 端代码浏览工具,适用于阅读 Linux 内核。

- 环境准备
安装 Source Insight 4.x(建议升级到最新版本以获得更好的索引和 Unicode 支持)。
将 Linux 内核源码(如 linux-5.4.31)复制到 Windows 机器(可通过 Samba/FTP/共享盘)。 - 创建工程
2.1. 新建工程
打开 SI → Project → New Project → 填写工程名称与存放目录。
建议目录结构:
c
D:\si_project\
├─ linux-5.4.31\ (解压后的内核源码)
└─ linux_5_4_31.si4project\
2.2. 导入源码
Project → Add Tree → 选择内核源码根目录。
过滤不需要索引的目录(如 Documentation/, scripts/, .git/ 等),以加快索引速度。
2.3. 初次索引与同步
SI 会自动生成符号索引。首次可能较慢,耐心等待。
如果已经有教师打包好的 .si4project 工程,可以直接解压并双击工程文件(如 linux_5_4_31.siproj)即可打开。
2.4. 后续同步
当 Linux 内核源码发生更新或你切换分支时,使用 Project → Synchronize Files 更新索引。
也可采用命令行脚本(siproject.exe -sync)与 Git hook 集成,自动维护索引。
- SI 使用技巧
使用
Ctrl + .(跳转符号定义),Ctrl + ,(返回)快速定位。配合集成的正则搜索定位
struct file_operations等关键结构。自定义宏/typedef 解析:
Project→Project Settings→Language→ 添加编译宏(如CONFIG_ARM,__KERNEL__等),增强解析准确度。打开
Relation Window查看函数调用关系,辅助理解调用栈。
led操作原理:

- 硬件层面
1.1. 电路结构
- LED 通常与限流电阻串联连接到 MCU 的 GPIO 引脚(开漏或推挽输出)。
- 引脚复用:GPIO 可能与其他功能(如 UART、SPI)共用,需要在引脚复用配置(MUX/Alternate Function)中设定为 GPIO 模式。
1.2. 控制方式
- 推挽输出: 通过 GPIO 输出高/低电平直接控制 LED。
- 开漏 + 上拉/下拉: 输出低电平点亮 LED,高电平灭灯(或反向)。
- PWM 调光: 利用定时器产生 PWM 信号,实现亮度调整。
1.3. 供电与电流限制
- 限流电阻选择基于 LED 正向压降与最大允许电流。
- 审核板级电源(3.3V/5V)与 GPIO 承载能力(最大驱动电流)。
- 软件层面
2.1. 寄存器基础
- Mode 寄存器: 配置引脚方向(输入/输出/复用)。
- Output Data 寄存器: 写入 1/0 控制高低电平。
- Pull-up/down 寄存器: 设置上下拉。
- Speed 寄存器: 控制输出速度(影响驱动能力)。
2.2. 地址映射
- 物理地址 →
ioremap→ 虚拟地址。 - 访问寄存器时注意内存屏障 (
wmb/rmb) 以及volatile的使用。
2.3. 驱动方式选择
- 简单控制:直接
ioremap+readl/writel。 - 规范做法:使用内核 LED 子系统(
drivers/leds/)、GPIO 子系统(gpiolib),由设备树配置gpio-leds。 - 高可靠场景:配合电源管理、消抖、状态同步。
1. linux内核模块
1.1 模块组成
c
//头文件
#include <linux/init.h>
#include <linux/module.h>
//模块加载(入口)函数
static int __init hello_drv_init(void)
{
printk("---------^_^ %s--------------\n",__FUNCTION__);
pr_info("hello_drv: init\n");
return 0;
}
//模块卸载(出口)函数
static void __exit hello_drv_exit(void)
{
printk("---------^_^ %s--------------\n",__FUNCTION__);
pr_info("hello_drv: exit\n");
}
//声明与认证
module_init(hello_drv_init);
module_exit(hello_drv_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("Simple hello driver");
MODULE_VERSION("1.0");
推荐使用
pr_info/pr_err/pr_debug系列,便于统一控制日志等级。
MODULE_LICENSE("GPL")避免taints kernel警告,同时符合开源协议。添加
MODULE_AUTHOR等信息方便管理。
1.2 使用 Kbuild 编译模块
1.2.1 编写Makefile文件
bash
#指定内核源码的路径
KERN_DIR = /home/peter/fsmp1/kernel/linux-5.4.31
CUR_DIR = `pwd`
#编译的规则
all:
make -C $(KERN_DIR) M=$(CUR_DIR) modules
#清除生成的文件
clean:
make -C $(KERN_DIR) M=$(CUR_DIR) clean
#将ko文件拷贝到开发板中
install:
cp *.ko /opt/myrootfs/drv_modules
#指定要编译的文件名
obj-m = hello_drv.o
1.2.2 编译内核模块
bash
farsight@ubuntu:~/mp157/driver/day02_code/module_v1$ make
make -C /home/farsight/mp157/kernel/linux-stm32mp-5.4.31-r0/linux-5.4.31 M=`pwd` modules #编译内核模块
make[1]: Entering directory '/home/farsight/mp157/kernel/linux-stm32mp-5.4.31-r0/linux-5.4.31'
CC [M] /home/farsight/mp157/driver/day02_code/module_v1/hello_drv.o
Building modules, stage 2.
MODPOST 1 modules
CC [M] /home/farsight/mp157/driver/day02_code/module_v1/hello_drv.mod.o
LD [M] /home/farsight/mp157/driver/day02_code/module_v1/hello_drv.ko
make[1]: Leaving directory '/home/farsight/mp157/kernel/linux-stm32mp-5.4.31-r0/linux-5.4.31'
1.3 在开发板中测试内核模块
1.3.1 将编译好的KO文件拷贝到文件系统中
bash
mkdir /opt/rootfs/drv_modules //在文件系统目录下创建一个保持ko文件的目录,以后所有的驱动都可以放到此处
cp *.ko /opt/rootfs/drv_modules/
1.3.2 在开发板的命令行加载模块到系统中
bash
[root@fsmp1a drv_modules]# insmod hello_drv.ko //加载模块
[10327.293051] hello_drv: loading out-of-tree module taints kernel.
[10327.297828] hello_drv: module verification failed: signature and/or required key missing - tainting kernel
[10327.307755] ---------^_^ hello_drv_init-------------- //当加载模块时,会执行模块加载函数
//查看被加载的模块
[root@fsmp1a drv_modules]# lsmod
Module Size Used by Tainted: G
hello_drv 16384 0
//卸载被加载的模块
[root@fsmp1a drv_modules]# rmmod hello_drv.ko //卸载模块
[10571.793067] ---------^_^ hello_drv_exit-------------- //当卸载模块时,会执行模块卸载函数
1.4 驱动开发工具 ---- source insight
bash
使用soruce insight 开发驱动,必须要在source insig中创建linux内核源码工程
1》将linux内核源码拷贝到windows中
2》打开source insight 创建工程
3》将linux内核源码文件加入工程中
4》同步工程文件
如果不想自己建工程,可以将老师建好的工程解压到windows中
安装好source insig之后,按下面的方式打开工程
1》解压下面的工程文件
si4.0_linux-5.4.31.zip
2》进入目录:
linux-5.4.31\linux_5_4_31.si4project
3》双击工程文件:
linux_5_4_31.siproj
1.5 模块的特点
1.5.1 模块传参
bash
在insmod模块时,可以给模块传递参数
例如:
//头文件
#include <linux/init.h>
#include <linux/module.h>
//定义参数
int age;
char *name;
//模块加载(入口)函数
static int __init hello_drv_init(void)
{
printk("---------^_^ %s--------------\n",__FUNCTION__);
printk("name = %s,age = %d\n",name,age); //打印参数
return 0;
}
//模块卸载(出口)函数
static void __exit hello_drv_exit(void)
{
printk("---------^_^ %s--------------\n",__FUNCTION__);
}
//声明参数
module_param(age, int, 0644);
module_param(name, charp, 0644);
//声明与认证
module_init(hello_drv_init);
module_exit(hello_drv_exit);
//编译,并在开发板中测试
[root@fsmp1a drv_modules]# insmod hello_drv.ko age=18 name="peter" //加载模块时,可以传递参数给模块
[14651.791186] ---------^_^ hello_drv_init--------------
[14651.794782] name = peter,age = 18
[root@fsmp1a drv_modules]# ls /sys/module/hello_drv/parameters/ //模块中的参数会在该目录下创建同名的文件
age name
[root@fsmp1a drv_modules]# cat /sys/module/hello_drv/parameters/age //文件中保持参数的值
18
[root@fsmp1a drv_modules]# cat /sys/module/hello_drv/parameters/name //文件中保持参数的值
peter
1.5.2 模块调用

bash
//在开发板中加载模块
[root@fsmp1a drv_modules]# insmod myadd.ko //先加载被调用模块
[root@fsmp1a drv_modules]# insmod hello_drv.ko x=12 y=45 //然后加载调用模块
[15862.389999] ---------^_^ hello_drv_init--------------
[15862.393596] name = Rose,age = 14
[15862.396868] ---------^_^ myadd------------
[15862.400901] sum = 57
[root@fsmp1a drv_modules]# ls /sys/module/hello_drv/parameters/
age name x y
2. 完整的驱动组成
2.1 申请设备号
c
static inline int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops)
//参数1 ---- major:主设备号
major > 0 ,表示静态指定主设备号
major = 0 ,表示动态分配主设备,register_chrdev会返回主设备号
//参数2 ---- name:字符串,驱动描述信息,自定义
//参数3 ---- 结构体指针:struct file_operations
struct file_operations {
struct module *owner;
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
int (*mmap) (struct file *, struct vm_area_struct *);
}
//返回值------成功:如果major>0,返回0,如果major=0,返回主设备号,失败:错误码
例如:
#if 0
//静态指定
ret = register_chrdev(led_major,"led_drv", &led_fops);
if(ret<0){
printk("register_chrdev error\n");
return -EINVAL;
}
#else
//动态分配
led_major = register_chrdev(0,"led_drv", &led_fops);
if(led_major<0){
printk("register_chrdev error\n");
return -EINVAL;
}
#endif
//在开发板中测试
[root@HQYJ drv_modules]# insmod led_drv.ko
[17450.363146] -----------^_^ led_drv_init-------------
[root@HQYJ drv_modules]# cat /proc/devices
Character devices:
128 ptm
136 pts
153 spi
166 ttyACM
180 usb
189 usb_device
226 drm
241 led_drv
242 rpmb
243 ttyGS
244 ttyUSI
245 ttySTM
246 bsg
247 watchdog
248 tee
249 iio
250 ptp
251 pps
252 cec
2.2 创建设备结点
2.2.1 手动创建
bash
mknod [OPTION]... NAME TYPE [MAJOR MINOR]
例如:
[root@fsmp1a drv_modules]# mknod /dev/hello c 241 7
[root@fsmp1a drv_modules]# ls -l /dev/hello
crw-r--r-- 1 root root 241, 7 Jan 3 02:37 /dev/hello
2.2.2 在驱动中创建
c
struct device *device_create(struct class *class, struct device *parent,
dev_t devt, void *drvdata, const char *fmt, ...)
//参数1 ---- 结构体指针 :struct class
//参数2 ---- 父节点,一般为:NULL
//参数3 ---- 设备号:32位的整数,由主设备号和次设备号组成
主设备号:占高12位,表示一类设备
次设备号:占低20位,表示具体的设备编号
#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS)) //从设备号中获取主设备号
#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK)) //从设备号中获取次设备号
#define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi)) //将主次设备号转为设备号
//参数4 ----- 私有数据,一般为:NULL
//参数5 ----- 设备节点名称
//变参 ------配合参数5定义设备节点名称
//返回值 ---成功:返回结构体struct device的指针,失败:NULL
struct class * class_create(struct module *owner, const char *name)
//参数1 ---- 当前模块,一般为:THIS_MODULE
//参数2 ---- 字符串,描述信息,自定义
//返回值 ---- 成功:结构体指针struct class * ,失败:NULL
例如:
//创建类
led_clz = class_create(THIS_MODULE, "led_class");
if(!led_clz){
printk("class_create error\n");
ret = PTR_ERR(led_clz);
goto err_unregister_chrdev;
}
//2,创建设备结点
led_dev = device_create(led_clz, NULL, MKDEV(led_major, 5), NULL, "led%d",2);
if(!led_dev){
printk("device_create error\n");
ret = PTR_ERR(led_dev);
goto err_class_destr;
}
//在开发板中测试:
[root@HQYJ drv_modules]# insmod led_drv.ko
[20104.412653] -----------^_^ led_drv_init-------------
[root@HQYJ drv_modules]# ls -l /dev/led*
crw------- 1 root root 241, 5 Jan 1 23:51 /dev/led2
2.2.3 硬件初始化
驱动开发思路:看原理图 ----> 看芯片手册 ----> 硬件初始化(地址映射/中断申请) -----> 实现操作接口
c
static inline void __iomem *ioremap(phys_addr_t offset, size_t size)
//参数1 ----- 物理地址
//参数2 ----- 要映射的空间大小
//返回值 ---- 成功:虚拟空间的地址,失败:NULL
例如:
#if 0
gpioz_mode = ioremap(GPIOZ, 4);
gpioz_odr = ioremap(GPIOZ+0x14,4);
#else
gpioz_mode = ioremap(GPIOZ,24);
if(!gpioz_mode){
printk("ioremap error\n");
ret = PTR_ERR(gpioz_mode);
goto err_device_destr;
}
gpioz_odr = gpioz_mode + 5;
#endif
2.2.4 实现操作接口
c
int led_drv_open(struct inode *inode, struct file *filp)
{
int i;
printk("-----------^_^ %s-------------\n",__FUNCTION__);
//将gpio设置为输出模式
*gpioz_mode &= ~(0x3f<<10);
*gpioz_mode |= 0x15 << 10;
//让 led 闪烁
for(i = 0; i < 4; i++){
*gpioz_odr |= 0x7 << 5;
msleep(500);
*gpioz_odr &= ~(0x7 << 5);
msleep(500);
}
return 0;
}
const struct file_operations led_fops = {
.open = led_drv_open,
};
3. 应用与驱动之间传递数据
3.1 应用数据传给内核 ------ 实现write时使用
c
static unsigned long copy_from_user(void *to, const void __user *from, unsigned long n)
//参数1 --- 内核空间地址
//参数2 --- 应用数据的空间地址
//参数3 --- 数据长度
//返回值 ---- 成功:0,失败:返回为copy的数据
例如:
ssize_t led_drv_write(struct file *filp, const char __user *buf, size_t size, loff_t *flags)
{
int ret;
int value;
printk("-----------^_^ %s-------------\n",__FUNCTION__);
//将应用数据转为内核数据
ret = copy_from_user(&value, buf, size);
if(ret > 0){
printk("copy_from_user error\n");
return -EINVAL;
}
//判断应用传递的数据 1---开灯,0 --- 关灯
if(value){
//开灯
*gpioz_odr |= 0x7 << 5;
}else{
//关灯
*gpioz_odr &= ~(0x7 << 5);
}
return size;
}
3.2 内核数据传给应用 ------ 实现read时使用
c
static inline int copy_to_user(void __user volatile *to, const void *from, unsigned long n)
//参数1 --- 应用空间地址
//参数2 --- 内核数据的空间地址
//参数3 --- 数据长度
//返回值 ---- 成功:0,失败:返回为copy的数据
以上。驱动开发本质是 连接硬件与内核子系统,提供稳定的用户接口。 其关键步骤分为:分析原理图与手册 → 确定驱动框架 → 编写并注册设备 → 掌握内核 API → 完成用户态交互。
建议实现的路径:可以先从简单的字符设备(LED)入手,逐步扩展到中断、DMA、子系统框架,再到设备树、PM、电源管理与主线合入流程。
其中,我们会使用到的工具链有:Source Insight 辅助阅读,Kbuild 管理编译,insmod/modprobe 部署测试,dmesg 观察日志。
希望本文能帮助你全面梳理嵌入式 Linux 驱动开发的关键要点,并在实际项目中自信实践。祝开发顺利!