嵌入式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 是安全拷贝用户态数据到内核态的核心函数,这两个环节完成了参数从应用层到驱动层的传递。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值