news 2026/4/15 10:25:39

嵌入式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/4/10 17:01:59

wl-explorer:Vue开发者的终极文件管理解决方案

wl-explorer&#xff1a;Vue开发者的终极文件管理解决方案 【免费下载链接】wl-explorer 用于vue框架的文件管理器插件&#xff0c;云盘、网盘。File manager plug-in for vue framework, cloud disk. 项目地址: https://gitcode.com/gh_mirrors/wl/wl-explorer 在当今…

作者头像 李华
网站建设 2026/4/11 22:59:53

可控硅驱动光耦:工业控制的隐形守护者

在工业自动化的精密脉络中&#xff0c;信号传输的稳定性与安全性如同设备的神经中枢&#xff0c;一旦受到干扰或破坏&#xff0c;可能引发生产停滞甚至安全事故。可控硅驱动光耦作为工业控制领域的核心元件&#xff0c;凭借其独特的光电隔离技术与卓越的抗干扰性能&#xff0c;…

作者头像 李华
网站建设 2026/4/11 8:37:47

终极Node.js版本管理指南:3步快速安装配置nvm-desktop

在现代Web开发中&#xff0c;Node.js版本管理是每个开发者必须面对的重要课题。nvm-desktop作为一款功能强大的桌面应用程序&#xff0c;专门为Node.js开发者设计&#xff0c;能够高效管理多个Node.js版本。无论你是前端工程师、后端开发者还是全栈程序员&#xff0c;这款工具都…

作者头像 李华
网站建设 2026/4/11 6:55:31

Zabbix 6 与 PHP 5 版本**完全不兼容

Zabbix 6 与 PHP 5 版本完全不兼容&#xff0c;强行匹配会导致 Zabbix 前端无法正常运行&#xff0c;甚至出现严重的功能异常或报错&#xff0c;具体表现和原因如下&#xff1a; 一、直接结果&#xff1a;Zabbix 前端无法启动/运行 Zabbix 6 对 PHP 的最低要求是 PHP 7.2&#…

作者头像 李华
网站建设 2026/4/2 3:06:16

企业微信“群机器人”消息推送的限制与绕过思路技术分析

一、引言 (Introduction) 1.1 背景&#xff1a; 企业微信群机器人是应用快速推送通知到群聊的官方、便捷方式。然而&#xff0c;在外部客户群和个性化营销场景下&#xff0c;其功能限制明显。 1.2 目的&#xff1a; 深入分析企业微信群机器人消息推送的技术限制&#xff0c;并…

作者头像 李华
网站建设 2026/4/12 3:57:02

LocalAI终极部署手册:3分钟快速搭建本地AI服务平台

LocalAI终极部署手册&#xff1a;3分钟快速搭建本地AI服务平台 【免费下载链接】LocalAI 项目地址: https://gitcode.com/gh_mirrors/loc/LocalAI 还在为云端AI服务的高昂费用和隐私担忧而烦恼吗&#xff1f;&#x1f914; LocalAI为您带来了革命性的本地AI解决方案&am…

作者头像 李华