news 2026/2/17 17:52:31

嵌入式Linux初探索——点灯背后的驱动层与应用层及其交互

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
嵌入式Linux初探索——点灯背后的驱动层与应用层及其交互

一、驱动层

#include <linux/kernel.h> #include <linux/module.h> #include <linux/slab.h> #include <linux/init.h> #include <linux/fs.h> #include <linux/delay.h> #include <linux/poll.h> #include <linux/mutex.h> #include <linux/wait.h> #include <linux/uaccess.h> #include <linux/device.h> #include <asm/io.h> static int major; static struct class *led_class; /* registers */ // IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3 地址:0x02290000 + 0x14 static volatile unsigned int *IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3; // GPIO5_GDIR 地址:0x020AC004 static volatile unsigned int *GPIO5_GDIR; //GPIO5_DR 地址:0x020AC000 static volatile unsigned int *GPIO5_DR; static ssize_t led_write(struct file *filp, const char __user *buf, size_t count, loff_t *ppos) { char val; int ret; /* copy_from_user : get data from app */ ret = copy_from_user(&val, buf, 1); /* to set gpio register: out 1/0 */ if (val) { /* set gpio to let led on */ *GPIO5_DR &= ~(1<<3); } else { /* set gpio to let led off */ *GPIO5_DR |= (1<<3); } return 1; } static int led_open(struct inode *inode, struct file *filp) { /* enable gpio5 * configure gpio5_io3 as gpio * configure gpio5_io3 as output */ *IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3 &= ~0xf; *IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3 |= 0x5; *GPIO5_GDIR |= (1<<3); return 0; } static struct file_operations led_fops = { .owner = THIS_MODULE, .write = led_write, .open = led_open, }; /* 入口函数 */ static int __init led_init(void) { printk("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__); major = register_chrdev(0, "100ask_led", &led_fops); /* ioremap */ // IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3 地址:0x02290000 + 0x14 IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3 = ioremap(0x02290000 + 0x14, 4); // GPIO5_GDIR 地址:0x020AC004 GPIO5_GDIR = ioremap(0x020AC004, 4); //GPIO5_DR 地址:0x020AC000 GPIO5_DR = ioremap(0x020AC000, 4); led_class = class_create(THIS_MODULE, "myled"); device_create(led_class, NULL, MKDEV(major, 0), NULL, "myled"); /* /dev/myled */ return 0; } static void __exit led_exit(void) { iounmap(IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3); iounmap(GPIO5_GDIR); iounmap(GPIO5_DR); device_destroy(led_class, MKDEV(major, 0)); class_destroy(led_class); unregister_chrdev(major, "100ask_led"); } module_init(led_init); module_exit(led_exit); MODULE_LICENSE("GPL");

ioremap(0x020AC000, 4);是 Linux 内核中物理地址映射到虚拟地址的核心函数,是驱动操作硬件寄存器的必经步骤,我会从「函数作用、参数含义、使用场景、注意事项」四个维度讲清楚它的用法:

在 Linux 内核中,不能直接访问物理地址(比如你代码中的 GPIO 寄存器物理地址0x020AC000),原因有二:

  1. 内核运行在虚拟地址空间,CPU 只能识别虚拟地址;
  2. 直接操作物理地址会破坏内存保护机制,导致系统不稳定。

ioremap()的核心功能:

将一段物理内存地址(如硬件寄存器的物理地址)映射到内核的虚拟地址空间,返回可直接访问的虚拟地址指针,驱动通过这个指针就能操作硬件寄存器。

简单说:ioremap是驱动访问硬件寄存器的 “桥梁”,没有它,驱动无法操作物理地址的寄存器。

函数原型与参数解析

1. 函数原型(内核头文件<asm/io.h>中定义)
void __iomem *ioremap(phys_addr_t phys_addr, size_t size);
  • 返回值:void __iomem *→ 映射后的内核虚拟地址指针(__iomem是内核标记,表明这是 IO 内存地址,仅用于提醒开发者,无语法作用);
  • 失败返回:NULL(映射失败,需检查物理地址是否合法)。
2. 你的代码参数解析
GPIO5_DR = ioremap(0x020AC000, 4);
参数取值含义
第一个参数0x020AC000要映射的物理起始地址(这里是 GPIO5 数据寄存器(DR)的物理地址)
第二个参数4要映射的地址长度(单位:字节),4 表示映射 4 字节(32 位寄存器)
3. 映射后如何使用?

映射成功后,GPIO5_DR就指向了该物理地址对应的虚拟地址,驱动可通过指针操作寄存器:

// 关闭 LED:向 GPIO5_DR 寄存器的第 3 位写 1 *GPIO5_DR |= (1 << 3); // 打开 LED:向 GPIO5_DR 寄存器的第 3 位写 0 *GPIO5_DR &= ~(1 << 3);

三、使用流程(完整规范)

ioremap必须和iounmap配对使用,避免内存泄漏,完整流程:

// 1. 定义虚拟地址指针(volatile 防止编译器优化,unsigned int 适配 32 位寄存器) static volatile unsigned int *GPIO5_DR; // 2. 驱动初始化时映射物理地址 static int __init led_init(void) { // 映射 GPIO5_DR 物理地址 0x020AC000,长度 4 字节 GPIO5_DR = ioremap(0x020AC000, 4); if (!GPIO5_DR) { // 检查映射是否成功 printk(KERN_ERR "ioremap GPIO5_DR failed!\n"); return -ENOMEM; } // 操作寄存器... return 0; } // 3. 驱动卸载时释放映射(必须!) static void __exit led_exit(void) { if (GPIO5_DR) { iounmap(GPIO5_DR); // 解除映射,释放虚拟地址空间 GPIO5_DR = NULL; } }

关键注意事项

1. 物理地址的来源

你代码中的0x020AC0000x020AC004等地址,来自:

  • 芯片的数据手册(Datasheet)参考手册(Reference Manual)
  • 开发板厂商提供的硬件文档(比如 IMX6ULL 的 GPIO 寄存器物理地址分布);→ 物理地址写错会导致驱动操作错误的寄存器,甚至系统崩溃。
2. 映射长度的选择
  • 单个 32 位寄存器:长度填 4(字节);
  • 连续多个寄存器:填总长度(比如映射 GPIO5 的所有寄存器,物理地址从 0x020AC000 到 0x020AC0FF,长度填 0x100);→ 长度只需覆盖要操作的寄存器,无需映射整个地址段,节省虚拟地址空间。
方式能否直接用安全性兼容性
直接操作物理地址极低
ioremap 映射后操作全架构

二、应用层

#include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <string.h> #include <unistd.h> #include <stdio.h> // ledtest /dev/myled on // ledtest /dev/myled off int main(int argc, char **argv) { int fd; char status = 0; if (argc != 3) { printf("Usage: %s <dev> <on|off>\n", argv[0]); printf(" eg: %s /dev/myled on\n", argv[0]); printf(" eg: %s /dev/myled off\n", argv[0]); return -1; } // open fd = open(argv[1], O_RDWR); if (fd < 0) { printf("can not open %s\n", argv[0]); return -1; } // write if (strcmp(argv[2], "on") == 0) { status = 1; } write(fd, &status, 1); return 0; }

三、makefile

# 1. 使用不同的开发板内核时, 一定要修改KERN_DIR # 2. KERN_DIR中的内核要事先配置、编译, 为了能编译内核, 要先设置下列环境变量: # 2.1 ARCH, 比如: export ARCH=arm64 # 2.2 CROSS_COMPILE, 比如: export CROSS_COMPILE=aarch64-linux-gnu- # 2.3 PATH, 比如: export PATH=$PATH:/home/book/100ask_roc-rk3399-pc/ToolChain-6.3.1/gcc-linaro-6.3.1-2017.05-x86_64_aarch64-linux-gnu/bin # 注意: 不同的开发板不同的编译器上述3个环境变量不一定相同, # 请参考各开发板的高级用户使用手册 KERN_DIR = /home/embedfire/ebf-image-builder/ebf_linux_kernel/build_image/build ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- export ARCH CROSS_COMPILE all: make -C $(KERN_DIR) M=`pwd` modules $(CROSS_COMPILE)gcc -o ledtest ledtest.c clean: make -C $(KERN_DIR) M=`pwd` modules clean rm -rf modules.order rm -f ledtest obj-m += led_drv.o

本文基于 Linux 内核驱动开发,实现通过应用程序控制硬件 LED 亮灭的功能,核心涉及用户态与内核态的数据交互GPIO 寄存器操作字符设备驱动框架三大知识点。下面先完整梳理驱动层和应用层程序的核心逻辑,再解答实战中遇到的关键疑问。

四、程序核心逻辑梳理

这里推荐结合上一篇文章中的疑问一起看来理解,看完你就会恍然大悟,明白应用层调用时,跳转到驱动层做了哪些事情,并且是如何进行数据来往的

嵌入式 Linux 字符设备驱动编写:从原理到实战https://blog.csdn.net/weixin_73503631/article/details/155773721?spm=1011.2415.3001.5331

一、数据传递的完整流程拆解

我们先把整个过程拆解成应用层→系统调用→驱动层三个关键环节,让你能清晰看到数据的流向:

1. 应用层:准备数据并发起写入

你的测试程序ledtest做了这些事:

// 1. 解析命令行参数,判断是 "on" 还是 "off" if (strcmp(argv[2], "on") == 0) { status = 1; // on → 赋值为1 } else { status = 0; // off → 赋值为0(代码里隐含,未显式写) } // 2. 调用 write 函数,把 status 的值写入设备文件 write(fd, &status, 1);
  • write(fd, &status, 1)不是直接操作驱动,而是用户态的系统调用接口,会触发内核的系统调用处理逻辑。
  • 这里传递的核心数据是status(1 或 0),长度 1 字节。
2. 系统调用:用户态→内核态的桥梁

当你调用write()时,操作系统会做这些底层操作:

  1. 从用户态切换到内核态;
  2. 根据你打开的fd(设备文件描述符),找到对应的内核驱动文件操作结构体file_operations
  3. 调用驱动中注册的led_write函数,并把应用层的参数(bufcount等)传递过去。
3. 驱动层:接收数据并操作硬件

驱动的led_write函数是核心处理逻辑,关键代码如下:

static ssize_t led_write(struct file *filp, const char __user *buf, size_t count, loff_t *ppos) { char val; int ret; // 核心:把用户态(应用层)的数据拷贝到内核态 ret = copy_from_user(&val, buf, 1); // 根据拷贝来的值控制 LED if (val) { // val=1(对应应用层的 on) *GPIO5_DR &= ~(1<<3); // LED 亮 } else { // val=0(对应应用层的 off) *GPIO5_DR |= (1<<3); // LED 灭 } return 1; }

这里的关键函数是copy_from_user

  • 作用:安全地把用户态内存的数据拷贝到内核态内存(因为用户态和内核态的内存空间是隔离的,不能直接访问);
  • 参数说明:
    • &val:内核态的变量,用来接收数据;
    • buf:应用层传递过来的内存地址(&status);
    • 1:拷贝 1 字节数据。

二、关键细节补充

  1. 为什么不能直接访问用户态内存?内核态有最高权限,直接访问用户态内存会有安全风险(比如用户态传递非法地址),copy_from_user会做合法性检查,避免内核崩溃。

  2. 数据传递的对应关系

    应用层系统调用驱动层
    status(1/0)write()buf(用户态地址)
    &status(用户态地址)内核态参数传递copy_from_user拷贝到val
    1(数据长度)count参数copy_from_user的第三个参数
  3. 完整调用链路

    plaintext

    应用层:./ledtest /dev/myled on → 设置 status=1 → write(fd, &status, 1) → 系统调用 sys_write → 内核找到 led_fops.write → 执行 led_write → copy_from_user 拿到 val=1 → 操作 GPIO 寄存器 → LED 亮

总结

  1. 应用层通过命令行参数解析出on/off,转换成 1/0 存入status变量;
  2. 调用write()系统调用,把status的地址和长度传递给内核;
  3. 驱动通过copy_from_user从用户态内存拷贝数据到内核态,再根据数据操作硬件寄存器,最终控制 LED。

核心关键点是:write系统调用是用户态与内核态的桥梁,copy_from_user是安全拷贝用户态数据到内核态的核心函数,这两个环节完成了参数从应用层到驱动层的传递。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/2/5 19:17:23

人工智能能让医疗变得更有人性化吗?

几周前&#xff0c;我去看了医生&#xff0c;查看一些检查结果。一切都很顺利——甚至是惊人的普通。但这次预约中有一部分让我感到意外。医生根据我的健康和年龄给了我建议后&#xff0c;她把电脑显示器转向我&#xff0c;给我展示了一个色彩丰富的仪表盘&#xff0c;上面写满…

作者头像 李华
网站建设 2026/2/16 14:13:34

光电传感器:机器人大战中的隐形指挥官

在机器人擂台的硝烟中&#xff0c;光电传感器正以其独特的 “光电魔法”&#xff0c;悄然改写着机器人大战的胜负规则。这些看似微小的元件&#xff0c;如同机器人的 “眼睛” 和 “神经末梢”&#xff0c;在高速对抗中精准捕捉战场信息&#xff0c;让冰冷的机械战士拥有了智能…

作者头像 李华
网站建设 2026/2/15 4:13:19

RunCat终极指南:让你的桌面工作充满活力的智能伴侣

RunCat终极指南&#xff1a;让你的桌面工作充满活力的智能伴侣 【免费下载链接】RunCat_for_windows A cute running cat animation on your windows taskbar. 项目地址: https://gitcode.com/GitHub_Trending/ru/RunCat_for_windows 你是否曾经在长时间编程时感到孤独&…

作者头像 李华
网站建设 2026/2/7 12:09:23

吊舱传感器传输模式解析

一、传输核心要点吊舱传输系统的设计围绕以下几个不可妥协的核心要点展开&#xff1a;1. 高带宽与低延迟的平衡&#xff1a;带宽&#xff1a;需传输高清&#xff08;1080p/4K&#xff09;、甚至超高清、高帧率&#xff08;60fps&#xff09;的视频流&#xff0c;以及红外热成像…

作者头像 李华
网站建设 2026/2/2 15:11:15

鸣潮自动化工具终极指南:3步轻松实现游戏效率翻倍 [特殊字符]

鸣潮自动化工具终极指南&#xff1a;3步轻松实现游戏效率翻倍 &#x1f680; 【免费下载链接】ok-wuthering-waves 鸣潮 后台自动战斗 自动刷声骸上锁合成 自动肉鸽 Automation for Wuthering Waves 项目地址: https://gitcode.com/GitHub_Trending/ok/ok-wuthering-waves …

作者头像 李华
网站建设 2026/2/13 14:31:51

保姆级教程:Qwen3 模型 + LLaMA-Factory,零基础也能学会大模型微调

在人工智能技术日新月异的当下&#xff0c;大型语言模型&#xff08;LLM&#xff09;已成为自然语言处理&#xff08;NLP&#xff09;领域的核心驱动力&#xff0c;从日常对话机器人到专业领域的文本分析&#xff0c;其应用场景不断拓展。不过&#xff0c;尽管预训练模型已通过…

作者头像 李华