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

相关推荐
飞睿科技1 小时前
解析ESP-SparkBot开源大模型AI桌面机器人的ESP32-S3核心方案
人工智能·嵌入式硬件·物联网·机器人·esp32·乐鑫科技·ai交互
我的IT修行1 小时前
架构领航,智绘转型新蓝图——Visual EAM企业架构管理平台赋能全域增长
微服务·架构
HalvmånEver1 小时前
Linux:信号保存下(信号二)
linux·运维·服务器·c++·学习·信号
独断万古他化2 小时前
Docker 入门前置:容器虚拟化基础之Namespace 空间隔离
linux·docker·容器
Gogo8162 小时前
Node.js 后端架构的“隐秘角落”:从 Fastify 引擎到类型系统的博弈
架构·node.js
算力魔方AIPC2 小时前
DeepX OCR:以 DeepX NPU 加速 PaddleOCR 推理,在 ARM 与 x86 平台交付可规模化的高性能 OCR 能力
arm开发·人工智能·ocr
SMF19192 小时前
【Vmware】windows物理机共享文件给vm虚拟机中的Centos系统
linux·运维·centos
本妖精不是妖精2 小时前
CentOS 7 安装 Node.js v18.x 完整教程
linux·centos·node.js
txinyu的博客2 小时前
静态库 & 动态库
linux·运维·服务器