一、驱动层
#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),原因有二:
- 内核运行在虚拟地址空间,CPU 只能识别虚拟地址;
- 直接操作物理地址会破坏内存保护机制,导致系统不稳定。
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. 物理地址的来源
你代码中的 0x020AC000、0x020AC004 等地址,来自:
- 芯片的 数据手册(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 寄存器操作、字符设备驱动框架三大知识点。下面先完整梳理驱动层和应用层程序的核心逻辑,再解答实战中遇到的关键疑问。
四、程序核心逻辑梳理
这里推荐结合上一篇文章中的疑问一起看来理解,看完你就会恍然大悟,明白应用层调用时,跳转到驱动层做了哪些事情,并且是如何进行数据来往的
一、数据传递的完整流程拆解
我们先把整个过程拆解成应用层→系统调用→驱动层三个关键环节,让你能清晰看到数据的流向:
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() 时,操作系统会做这些底层操作:
- 从用户态切换到内核态;
- 根据你打开的
fd(设备文件描述符),找到对应的内核驱动文件操作结构体file_operations; - 调用驱动中注册的
led_write函数,并把应用层的参数(buf、count等)传递过去。
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 字节数据。
二、关键细节补充
-
为什么不能直接访问用户态内存?内核态有最高权限,直接访问用户态内存会有安全风险(比如用户态传递非法地址),
copy_from_user会做合法性检查,避免内核崩溃。 -
数据传递的对应关系
应用层 系统调用 驱动层 status(1/0)write()buf(用户态地址)&status(用户态地址)内核态参数传递 copy_from_user拷贝到val1(数据长度)count参数copy_from_user的第三个参数 -
完整调用链路
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 亮
总结
- 应用层通过命令行参数解析出
on/off,转换成 1/0 存入status变量; - 调用
write()系统调用,把status的地址和长度传递给内核; - 驱动通过
copy_from_user从用户态内存拷贝数据到内核态,再根据数据操作硬件寄存器,最终控制 LED。
核心关键点是:write 系统调用是用户态与内核态的桥梁,copy_from_user 是安全拷贝用户态数据到内核态的核心函数,这两个环节完成了参数从应用层到驱动层的传递。

1085

被折叠的 条评论
为什么被折叠?



