news 2026/4/25 17:12:55

内存分配,GPIO驱动,中断处理

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
内存分配,GPIO驱动,中断处理

内核内存分配

函数内存来源物理地址连续?虚拟地址连续?速度大小用途释放函数
kmalloc内核 slab 池✅ 连续✅ 连续小内存 (<几 MB)驱动常规缓存、结构体、日常内存kfree()
vmalloc零散物理页拼接❌ 不连续✅ 连续大内存 (几十 MB+)软件大块缓存,不碰硬件vfree()
ioremap硬件物理地址硬件原生映射后连续-外设寄存器硬件地址映射,操作外设必备iounmap()

Linux 内核空间本身是连续虚拟地址,但是:

  1. 内核不能用 malloc /new(那是用户空间 C/C++ 的)
  2. 内核自己有一套专属内存分配函数
  3. 你写驱动(内核模块)申请内存,就只有这三个主力:kmallocvmallocioremap

1. kmalloc

是什么

内核小块、物理连续内存分配函数。最常用、驱动 90% 场景优先用它

底层特点

  • 分配的虚拟地址 & 物理地址都是连续
  • 内核低地址、slab 内存池分配
  • 分配速度极快
  • 有大小限制:一般最大几 MB 以内(不能申请超大块)
  • 虚拟地址 = 物理地址 + 内核偏移(非常好转换)

常用函数

// 申请内存 void *kmalloc(size_t size, gfp_t flags); // 释放内存(必须配对!漏释放内存泄漏) void kfree(const void *objp);

最常用标志位GFP_KERNEL:内核普通分配,睡眠等待可用内存。

char *buf = kmalloc(1024, GFP_KERNEL); if(buf == NULL) { // 分配失败 } // 使用完必须释放 kfree(buf);

使用场景(驱动里什么时候用)

  • 驱动结构体、小缓冲区
  • 设备私有数据、小缓存
  • 数据长度不大(几百字节、几 KB、最大几 MB)所有普通驱动内存申请,优先 kmalloc

2. vmalloc

是什么

内核大块、虚拟地址连续、物理地址不连续内存分配。

底层特点

  • 虚拟地址连续(你指针用起来正常)
  • 物理内存完全不连续,靠内核页表拼接起来
  • 分配速度(要建页表)
  • 没有严格大小限制,可以申请很大内存(几十 MB、几百 MB)
  • 不能用于硬件 DMA、外设地址映射

函数:

void *vmalloc(unsigned long size); void vfree(const void *addr);
char *big_buf = vmalloc(1024*1024*10); // 10MB vfree(big_buf);

使用场景

  • 驱动里需要超大缓冲区
  • 软件内部缓存、大量数据存储
  • 不需要和硬件打交道的大块内存

为什么驱动不优先用?

速度慢,而且不能给硬件用,硬件只能识别物理连续内存


3. ioremap(最重要,驱动灵魂函数)

前面kmalloc / vmalloc都是申请内核空闲内存ioremap根本不是申请内存!

是什么

把硬件的物理地址 → 映射成内核虚拟地址外设(寄存器、GPIO、内存硬件)的物理地址,内核不能直接访问,必须映射。

内核不能直接操作物理地址,所以必须:物理地址(硬件)==ioremap==>内核虚拟地址

函数:

// 物理地址 映射为虚拟地址 void __iomem *ioremap(resource_size_t phys_addr, size_t size); // 解除映射 void iounmap(void __iomem *addr);

特点

  • 不申请新内存,只是地址重映射
  • 只用于硬件寄存器、外设物理地址
  • 操作 GPIO、串口、SPI、屏幕、芯片外设全部靠它

典型驱动用法

// 某外设物理地址 0x12345678 void __iomem *vir_addr = ioremap(0x12345678, 4); // 操作寄存器 writel(0x1, vir_addr); readl(vir_addr); // 退出驱动必须解除映射 iounmap(vir_addr);

GPIO 点灯驱动

一、先搞懂:GPIO 是什么

GPIO = General Purpose Input Output通用输入输出引脚就是芯片上的一个引脚:

  • 驱动输出高电平→ 灯亮
  • 驱动输出低电平→ 灯灭

Linux 内核不能直接操作硬件物理地址,所以流程固定:

硬件物理地址↓ioremap映射成内核虚拟地址↓ 内核通过虚拟地址操作寄存器↓ 控制引脚高低电平 → 点灯 / 灭灯

二、GPIO 硬件底层原理(必懂)

芯片控制一个引脚,靠3 个寄存器(所有 ARM 开发板通用)

  1. 配置寄存器(GPIOx_CRL/CRH)设置这个引脚是输入模式 / 输出模式
  2. 数据置位寄存器(BSRR)写 1 → 引脚输出高电平(灯亮)
  3. 数据复位寄存器(BRR)写 1 → 引脚输出低电平(灯灭)

内核不能直接访问这些硬件物理地址,所以必须用ioremap做地址映射。

三、整个驱动完整流程(骨架)

  1. 模块加载:__init
    • ioremap 映射 GPIO 相关物理地址
    • 配置引脚为输出模式
  2. 实现字符设备驱动接口file_operations
    • open:打开设备
    • write:应用层传 1 点灯,传 0 灭灯
    • release:关闭设备
  3. 模块卸载:__exit
    • iounmap 解除地址映射
  4. 上层应用 C 程序:open /write 控制灯

四、完整完整代码(逐行详解版)

全部标准 Linux 内核驱动 C 代码,注释拉满,你每一行都能看懂。

gpio_led.c 驱动源码

#include <linux/module.h> #include <linux/fs.h> #include <linux/io.h> #include <linux/uaccess.h> // ************ 1. 开发板GPIO硬件物理地址(以STM32/ARM通用地址举例)************ // 真实芯片每个地址不同,这里用经典点灯地址举例 #define GPIO_PHYS_BASE 0x38001000 // GPIO寄存器物理基地址 #define REG_SIZE 0x20 // 寄存器空间大小 // 内核虚拟地址指针 static void __iomem *gpio_virt_base; // 设备号、设备名 #define LED_MAJOR 231 #define LED_NAME "my_led" // ************ 2. 操作函数 ************ // 打开设备 static int led_open(struct inode *inode, struct file *file) { printk("led device open\n"); return 0; } // 应用层 write 进来控制灯:buf=1亮,buf=0灭 static ssize_t led_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos) { unsigned char val; // 从用户空间拷贝数据到内核 copy_from_user(&val, buf, 1); if(val == 1) { // 点灯:设置引脚高电平 // 偏移地址:BSRR 置位寄存器 writel(1 << 5, gpio_virt_base + 0x18); printk("led on\n"); } else { // 灭灯:设置引脚低电平 // 偏移地址:BRR 复位寄存器 writel(1 << 5, gpio_virt_base + 0x28); printk("led off\n"); } return count; } // 关闭设备 static int led_release(struct inode *inode, struct file *file) { printk("led device close\n"); return 0; } // ************ 3. 字符设备操作结构体(驱动灵魂)************ static struct file_operations led_fops = { .owner = THIS_MODULE, .open = led_open, .write = led_write, .release = led_release, }; // ************ 4. 驱动加载函数 ************ static int __init led_init(void) { int ret; // 1. 注册字符设备 ret = register_chrdev(LED_MAJOR, LED_NAME, &led_fops); if(ret < 0) { printk("register chrdev fail\n"); return ret; } // 2. 重点:ioremap 物理地址 -> 内核虚拟地址 gpio_virt_base = ioremap(GPIO_PHYS_BASE, REG_SIZE); if(gpio_virt_base == NULL) { printk("ioremap fail\n"); unregister_chrdev(LED_MAJOR, LED_NAME); return -ENOMEM; } // 3. 配置GPIO为输出模式 // 操作配置寄存器,设置第5号引脚为输出 writel(0x00000010, gpio_virt_base + 0x00); printk("gpio led driver init ok\n"); return 0; } // ************ 5. 驱动卸载函数 ************ static void __exit led_exit(void) { // 1. 先解除地址映射(必须!顺序不能乱) iounmap(gpio_virt_base); // 2. 注销字符设备 unregister_chrdev(LED_MAJOR, LED_NAME); printk("gpio led driver exit ok\n"); } // 模块加载、卸载入口 module_init(led_init); module_exit(led_exit); MODULE_LICENSE("GPL"); MODULE_DESCRIPTION("GPIO LED Driver"); MODULE_AUTHOR("XXX");

五、配套 Makefile(驱动专用,不用 CMake)

KERNELDIR ?= /lib/modules/$(shell uname -r)/build PWD := $(shell pwd) all: $(MAKE) -C $(KERNELDIR) M=$(PWD) modules clean: $(MAKE) -C $(KERNELDIR) M=$(PWD) clean

六、上层应用测试程序(user.c 用户空间程序)

你在用户态 C 程序,像操作普通文件一样控制灯

#include <stdio.h> #include <fcntl.h> #include <unistd.h> int main(int argc, char *argv[]) { int fd; unsigned char buf; // 打开驱动设备节点 fd = open("/dev/my_led", O_WRONLY); if(fd < 0) { perror("open fail"); return -1; } // 传入 1 点灯 buf = 1; write(fd, &buf, 1); sleep(2); // 传入 0 灭灯 buf = 0; write(fd, &buf, 1); close(fd); return 0; }

七、完整运行步骤(命令全套)

1. 编译驱动

make

生成.ko内核模块文件

2. 加载驱动

insmod gpio_led.ko

3. 创建设备节点(应用层才能 open)

mknod /dev/my_led c 231 0

4. 编译应用程序并运行

gcc user.c -o user ./user

运行后:灯亮 2 秒 → 自动熄灭

5. 卸载驱动

rmmod gpio_led

中断处理

一、先搞懂:为什么需要中断?

我们知道GPIO 轮询点灯:应用层一直read()读引脚电平,内核一直在循环查询引脚有没有变化。

缺点:

  1. 极度浪费 CPU
  2. CPU 死循环轮询,什么都干不了
  3. 响应慢

中断的思想

CPU 该干嘛干嘛,平时完全不管硬件。硬件引脚电平变化了 → 主动发信号打断 CPU → CPU 暂停手上工作 → 执行中断处理函数 → 处理完回到原来工作。

这就是嵌入式、驱动最核心的机制:异步事件处理按键、红外、网卡、串口、时钟全部靠中断。

二、Linux 中断核心基础概念

1. 中断号

每个硬件中断都有一个唯一编号,叫做irq 中断号内核通过这个编号识别是哪个硬件发来的中断。

2. 中断处理函数

你自己写的函数,硬件触发中断后,内核自动调用这个函数

3. 申请中断 API(内核核心函数)

int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *name, void *dev_id);

参数逐个解释(必背):

  1. irq:硬件中断号
  2. handler:你的中断回调函数
  3. flags:中断触发方式
    • IRQF_TRIGGER_RISING上升沿触发(按键按下)
    • IRQF_TRIGGER_FALLING下降沿触发(按键松开)
    • IRQF_TRIGGER_BOTH双边沿都触发
  4. name:中断名字,cat /proc/interrupts可以看到
  5. dev_id:设备私有数据,传参用,释放中断要用到

4. 释放中断(必须配对)

void free_irq(unsigned int irq, void *dev_id);

申请了就必须释放,不释放内核资源泄漏。

三、最大难点:中断上下文 & 上下半部(必懂,面试必问)

1. 什么是中断上下文?

硬件触发中断时,CPU 暂停用户进程、暂停内核普通线程,强行跑你的中断函数。此时环境叫做中断上下文

中断上下文严格限制(超级重要,写驱动必踩坑)
  1. 不能睡眠(不能调用kmalloc(..., GFP_KERNEL)、不能msleep、不能等待)
  2. 不能调用会阻塞的函数
  3. 不能和用户空间做复杂数据交互
  4. 执行时间必须极短!越快越好

原因:CPU 被你霸占了,所有进程都卡住。

2. 为什么要有【上半部 + 下半部】

中断函数不能耗时,但是很多处理(消抖、数据处理、点灯、复杂逻辑)很慢。Linux 解决方案:上下半部拆分

  • 上半部(硬中断)request_irq注册的函数,只做最简单的事:标记事件、触发下半部。执行极快,立刻返回。

  • 下半部(软中断 / 任务队列 tasklet)处理耗时任务:点灯、数据处理、打印、业务逻辑。可以稍微耗时,不阻塞 CPU

驱动里 99% 场景用tasklet 任务(最简单、最常用)

// 1. 定义tasklet DECLARE_TASKLET(my_tasklet, tasklet_func, data); // 2. 上半部中断函数里调度 tasklet_schedule(&my_tasklet);

四、完整实战案例:按键中断控制 LED(经典全套驱动)

场景:按键引脚触发中断 → 内核中断响应 → 翻转 LED 灯亮灭包含:中断申请、上下半部 tasklet、GPIO 操作、字符设备框架、完整代码 + 逐行注释、Makefile、原理全部齐全。

4.1 硬件原理

  • 按键:GPIO 输入引脚,电平变化产生中断
  • LED:GPIO 输出引脚,高电平亮,低电平灭

4.2 完整驱动代码 irq_led.c

#include <linux/module.h> #include <linux/fs.h> #include <linux/io.h> #include <linux/interrupt.h> // 中断头文件 #include <linux/uaccess.h> // ========== 硬件物理地址(开发板通用) ========== #define LED_GPIO_PHYS 0x38001000 #define KEY_GPIO_PHYS 0x38002000 #define REG_SIZE 0x20 // 虚拟地址 static void __iomem *led_virt; static void __iomem *key_virt; // 设备号、设备名 #define MAJOR_NUM 232 #define DEV_NAME "irq_led" // LED电平状态 static int led_state = 0; // ========== 下半部:tasklet 任务 ========== // 任务处理函数:真正控制LED翻转 static void led_tasklet_func(unsigned long arg) { // 翻转LED led_state = !led_state; if(led_state) { // 点灯:置位寄存器 writel(1 << 5, led_virt + 0x18); printk(KERN_INFO "LED ON\n"); } else { // 灭灯:复位寄存器 writel(1 << 5, led_virt + 0x28); printk(KERN_INFO "LED OFF\n"); } } // 定义tasklet(内核宏) DECLARE_TASKLET(my_tasklet, led_tasklet_func, 0); // ========== 上半部:中断处理函数 ========== // 硬件触发中断,只会进这里 static irqreturn_t key_irq_handler(int irq, void *dev_id) { // 上半部只做一件事:调度下半部 tasklet_schedule(&my_tasklet); return IRQ_HANDLED; } // ========== 字符设备接口 ========== static int irq_open(struct inode *inode, struct file *file) { printk(KERN_INFO "device open\n"); return 0; } static int irq_release(struct inode *inode, struct file *file) { printk(KERN_INFO "device close\n"); return 0; } static struct file_operations fops = { .owner = THIS_MODULE, .open = irq_open, .release = irq_release, }; // ========== 驱动加载入口 ========== static int __init irq_led_init(void) { int ret; // 1. 注册字符设备 ret = register_chrdev(MAJOR_NUM, DEV_NAME, &fops); if(ret < 0) { printk(KERN_ERR "chrdev register fail\n"); return ret; } // 2. ioremap 映射LED、按键物理地址 led_virt = ioremap(LED_GPIO_PHYS, REG_SIZE); key_virt = ioremap(KEY_GPIO_PHYS, REG_SIZE); if(!led_virt || !key_virt) { printk(KERN_ERR "ioremap fail\n"); ret = -ENOMEM; goto err_map; } // 3. 配置GPIO writel(0x00000010, led_virt + 0x00); // LED为输出 writel(0x00000000, key_virt + 0x00); // 按键为输入 // 4. 申请中断!!核心 // 中断号举例 56,上升沿触发 ret = request_irq(56, key_irq_handler, IRQF_TRIGGER_RISING, "key_irq", NULL); if(ret < 0) { printk(KERN_ERR "request irq fail\n"); goto err_irq; } printk(KERN_INFO "irq led driver init ok\n"); return 0; err_irq: iounmap(key_virt); err_map: iounmap(led_virt); unregister_chrdev(MAJOR_NUM, DEV_NAME); return ret; } // ========== 驱动卸载入口 ========== static void __exit irq_led_exit(void) { // 1. 杀死tasklet任务 tasklet_kill(&my_tasklet); // 2. 释放中断 free_irq(56, NULL); // 3. 解除地址映射 iounmap(key_virt); iounmap(led_virt); // 4. 注销字符设备 unregister_chrdev(MAJOR_NUM, DEV_NAME); printk(KERN_INFO "irq led driver exit ok\n"); } module_init(irq_led_init); module_exit(irq_led_exit); MODULE_LICENSE("GPL"); MODULE_DESCRIPTION("GPIO Key Interrupt LED Driver");

五、完整流程梳理(你串起所有知识点)

  1. 硬件按键电平变化
  2. 芯片产生中断信号,发送给 CPU
  3. Linux 内核捕获中断号
  4. 调用你注册的上半部中断函数key_irq_handler
  5. 上半部极快执行,只调度tasklet下半部,立刻返回
  6. 内核空闲时执行下半部led_tasklet_func
  7. 操作 GPIO 寄存器,翻转 LED 灯亮灭
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/25 17:12:35

Postman便携版终极指南:5分钟掌握免安装API开发神器

Postman便携版终极指南&#xff1a;5分钟掌握免安装API开发神器 【免费下载链接】postman-portable &#x1f680; Postman portable for Windows 项目地址: https://gitcode.com/gh_mirrors/po/postman-portable Postman便携版是一款专为Windows用户设计的免安装API开发…

作者头像 李华
网站建设 2026/4/25 17:11:16

UHD:开源高性能企业级软件定义无线电驱动框架深度解析

UHD&#xff1a;开源高性能企业级软件定义无线电驱动框架深度解析 【免费下载链接】uhd The USRP™ Hardware Driver Repository 项目地址: https://gitcode.com/gh_mirrors/uh/uhd UHD&#xff08;USRP™ Hardware Driver&#xff09;是Ettus Research开发的开源高性能…

作者头像 李华
网站建设 2026/4/25 17:10:54

AI Agent的延迟优化与性能调优

AI Agent的延迟优化与性能调优:从原理到实战 在当今智能化浪潮中,AI Agent正从实验室走向千行百业——从实时响应的智能客服、毫秒必争的自动驾驶,到工业场景下的智能监控,Agent的延迟表现直接决定了用户体验、生产效率甚至系统安全。然而,随着Agent能力的增强(如多模态…

作者头像 李华
网站建设 2026/4/25 17:05:20

2025届最火的五大降重复率神器实际效果

Ai论文网站排名&#xff08;开题报告、文献综述、降aigc率、降重综合对比&#xff09; TOP1. 千笔AI TOP2. aipasspaper TOP3. 清北论文 TOP4. 豆包 TOP5. kimi TOP6. deepseek 专为免费提供的 AI 论文资源&#xff0c;其主要来源是预印本平台以及开放获取期刊&#xff0…

作者头像 李华