【Linux 驱动开发】二. linux内核模块

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时使用)

本文重点围绕以下四个方面展开,帮助你从"了解驱动开发流程"走向"掌握可落地的实践方法":

  1. 驱动开发思路
  2. Source Insight 工程建立与同步
  3. LED 驱动的硬件与软件原理
  4. Linux 内核模块(驱动)开发全流程

前提:驱动开发思路:

驱动开发思路:从需求到代码的闭环

公式: 原理图 → 芯片手册 → Linux 内核 → 驱动框架 → 应用交互 → 调试验证

  1. 了解业务需求
    • 确定用户空间需要怎样访问设备(文件节点 / sysfs / netlink 等)。
    • 明确控制目标(开关 LED、读写传感器、DMA 传输等)。
  2. 硬件分析
    • 阅读原理图:确认使用的 GPIO、供电、外设接口。
    • 阅读芯片手册(TRM):了解寄存器、时序、备用模式。
    • 确认设备是否由设备树描述、是否需要电源管理、复用控制。
  3. 驱动框架选择
    • 字符设备、块设备、platform driver、I²C/SPI 驱动、miscdevice、子系统(如 LED framework、input、V4L2 等)。
  4. API 设计
    • 定义 open/read/write/ioctl/poll 等函数行为。
    • 规划内核与用户层的数据结构与协议(结构体布局、命令字、阻塞/非阻塞设计)。
  5. 核心实现步骤
    • 分配/注册设备号或 platform 设备。
    • 内存映射 ioremap、GPIO 配置、时钟/复用控制。
    • 中断申请 (request_irq)、定时器、工作队列等。
    • 导出 sysfs 属性、debugfs 节点或 proc 接口。
    • 支持电源管理(suspend/resume)与错误恢复。
  6. 测试与调试
    • dmesg 查看日志,结合 printk/dev_info
    • 使用 strace / hexdump / cat / shell 脚本驱动测试。
    • 测量波形(示波器/逻辑分析仪)验证实际硬件反应。
    • 内核调试工具:ftracekgdbdynamic debugtrace_printk
  7. 发布与维护
    • 模块参数、sysfs 节点、文档 (Documentation/)。
    • Kconfig 选项及 Makefile 集成。

若进入主线需符合内核编码规范 (scripts/checkpatch.pl)。


Source_Insight 工程创建和同步:

Source Insight(SI)是经典的 Windows 端代码浏览工具,适用于阅读 Linux 内核。

  1. 环境准备
    安装 Source Insight 4.x(建议升级到最新版本以获得更好的索引和 Unicode 支持)。
    将 Linux 内核源码(如 linux-5.4.31)复制到 Windows 机器(可通过 Samba/FTP/共享盘)。
  2. 创建工程

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 集成,自动维护索引。

  1. SI 使用技巧

使用 Ctrl + .(跳转符号定义),Ctrl + ,(返回)快速定位。

配合集成的正则搜索定位 struct file_operations 等关键结构。

自定义宏/typedef 解析:ProjectProject SettingsLanguage → 添加编译宏(如 CONFIG_ARM, __KERNEL__ 等),增强解析准确度。

打开 Relation Window 查看函数调用关系,辅助理解调用栈。

led操作原理:

  1. 硬件层面

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 承载能力(最大驱动电流)。
  1. 软件层面

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 驱动开发的关键要点,并在实际项目中自信实践。祝开发顺利!

相关推荐
zy happy3 小时前
VMware虚拟机添加新的硬盘
java·linux·jvm·docker
LCG元3 小时前
嵌入式开发踩坑排查实战:从 STM32 外设初始化到 Linux SPI 驱动的 15 类硬件疑难故障深度复盘
linux·stm32·单片机
代码AC不AC3 小时前
【Linux】Socket编程TCP
linux·tcp·socket编程
扛枪的书生3 小时前
SeaweedFS 学习总结
linux
piaoyiren3 小时前
基于 STM32F103 蓝牙遥控云台超声波避障小车
stm32·单片机·嵌入式硬件·蓝牙·超声波测距·直流电机
学逆向的3 小时前
汇编——寻址方式
开发语言·汇编·网络安全
guslegend4 小时前
领域驱动设计,微服务设计为什么要选择DDD?
微服务·云原生·架构
minglie14 小时前
香橙派zero3配个iic屏显示ip
linux·学习
edwarddamon4 小时前
SSH 指定端口建立隧道:打通内网访问完整教程
linux·命令行
XUHUOJUN4 小时前
Azure Local 2606 Release 解读(2602→2606 演进与升级价值·下篇):升级路径与实战清单
架构·azure local