《i.MX6ULL LED 驱动实战:内核模块开发与 GPIO 控制》
LED驱动准备
在上一篇裸机开发实验中,我们是直接向寄存器地址写入数据的。
但在 Linux 驱动开发 中就不能这样做了。因为在 Linux 内核中,不允许直接操作物理地址。
这是由于系统启用了 MMU(内存管理单元),内核采用了 虚拟内存机制。换句话说,我们在裸机中使用的那些物理地址(如
0x020E0068
),在 Linux 环境中已经不再是可直接访问的。
我们必须先获得该寄存器物理地址对应的虚拟地址,才能对其进行读写。为此,Linux 提供了两个非常关键的函数:
ioremap()
—— 将物理地址映射为内核可访问的虚拟地址iounmap()
—— 在不再使用时解除映射这对函数是驱动开发中访问硬件寄存器的“入口”和“出口”。
接下来是对两个函数的讲解
ioremap()
函数
原型
void __iomem *ioremap(resource_size_t phys_addr, unsigned long size);
功能
将设备的物理地址区间映射到内核虚拟地址空间,并返回虚拟地址指针。
参数说明
参数 | 含义 |
---|---|
phys_addr | 起始物理地址,比如寄存器地址 0x0209C000 |
size | 要映射的字节大小,通常是 4、8 或 0x1000 |
返回值
- 成功:返回一个类型为
void __iomem *
的虚拟地址指针 - 失败:返回
NULL
示例
#define GPIO1_DR_BASE (0x0209C000)static void __iomem *GPIO1_DR;GPIO1_DR = ioremap(GPIO1_DR_BASE, 4);
if (!GPIO1_DR) {pr_err("Failed to map GPIO1_DR\n");return -ENOMEM;
}
此时 GPIO1_DR
就是映射后的虚拟地址,可以通过 readl()
和 writel()
安全访问寄存器。
iounmap()
函数
原型
void iounmap(void __iomem *addr);
功能
释放之前通过 ioremap()
映射的虚拟地址空间。
使用场景
- 驱动卸载(
module_exit
)时; - 或者资源释放阶段。
示例
iounmap(GPIO1_DR);
表示释放对应的虚拟地址映射。
注意事项
-
不能直接对映射指针解引用:
*GPIO1_DR = 1; // 错误
应使用内核提供的接口:
writel(1, GPIO1_DR); val = readl(GPIO1_DR);
-
映射范围应与寄存器手册对应。
如果寄存器区域是 0x1000 字节,应完整映射,否则可能越界。 -
同一物理区域重复调用 ioremap 会得到不同虚拟地址,但都指向同一物理区域,不推荐这样做。
-
用户态程序不能使用 ioremap,它是内核态函数。用户态可通过
/dev/mem
和mmap()
实现类似效果。
类比说明
环境 | 行为 | 比喻 |
---|---|---|
裸机 | 直接写寄存器地址 | 拿钥匙直接开门 |
Linux 内核 | ioremap 映射后再访问 | 先申请门禁卡再刷卡开门 |
小结
函数 | 作用 | 常用位置 | 搭配函数 |
---|---|---|---|
ioremap() | 将物理地址映射到虚拟地址 | 驱动初始化、probe函数 | readl() 、writel() |
iounmap() | 释放映射 | 驱动卸载、exit函数 | 无 |
LED内核态驱动代码
这里就继续运用到我们《从系统调用到驱动回调:read() 如何映射到 chrdev_read()》这一篇博客所介绍的
字符设备驱动开发
内容这段代码的重点就是:
- 地址映射
- 释放映射
下面是代码展示:
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>#define Led_Major 200
#define Led_Name "led"#define LED_ON 1
#define LED_OFF 0/* 寄存器物理地址 */
#define CCM_CCGR1_BASE (0X020C406C)
#define SW_MUX_GPIO1_IO03_BASE (0X020E0068)
#define SW_PAD_GPIO1_IO03_BASE (0X020E02F4)
#define GPIO1_DR_BASE (0X0209C000)
#define GPIO1_GDIR_BASE (0X0209C004)/* 映射后的寄存器虚拟地址指针 */
static void __iomem *IMX6U_CCM_CCGR1;
static void __iomem *SW_MUX_GPIO1_IO03;
static void __iomem *SW_PAD_GPIO1_IO03;
static void __iomem *GPIO1_DR;
static void __iomem *GPIO1_GDIR;/*led开关转换*/
void led_switch(u8 sta)
{u32 val=0;if(sta==LED_ON){val=readl(GPIO1_DR);val &=~(1<<3);//输出低电平,点亮LEDwritel(val,GPIO1_DR);}else if(sta==LED_OFF){val = readl(GPIO1_DR);val |= (1 << 3);//输出高电平,熄灭LEDwritel(val, GPIO1_DR);}
}/*设备开启*/
static int leddev_open(struct inode *inode, struct file *filp)
{printk("leddev open!\n");return 0;
}
/*设备读取*/
static ssize_t leddev_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{printk("leddev read!\n");return 0;
}
/*设备写入*/
static ssize_t leddev_write(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{int ret;unsigned char write_buf[1];unsigned char led_stat;ret = copy_from_user(write_buf, buf, cnt);if (ret == 0)printk("write successful!\n");else{printk("write error!\n");return -EFAULT;}led_stat=write_buf[0];if(led_stat==LED_ON)led_switch(LED_ON);else if(led_stat==LED_OFF)led_switch(LED_OFF);return 0;
}static int leddev_release(struct inode *inode, struct file *filp)
{return 0;
}static struct file_operations leddev_fops = {.owner = THIS_MODULE,.open = leddev_open,.read = leddev_read,.write = leddev_write,.release = leddev_release,
};// 模块加载
static int __init chrdev_init(void)
{int ret;u32 val;//初始化LED/* 1、寄存器地址映射 */IMX6U_CCM_CCGR1 = ioremap(CCM_CCGR1_BASE, 4);SW_MUX_GPIO1_IO03 = ioremap(SW_MUX_GPIO1_IO03_BASE, 4);SW_PAD_GPIO1_IO03 = ioremap(SW_PAD_GPIO1_IO03_BASE, 4);GPIO1_DR = ioremap(GPIO1_DR_BASE, 4);GPIO1_GDIR = ioremap(GPIO1_GDIR_BASE, 4);/* 2、使能 GPIO1 时钟 */val=readl(IMX6U_CCM_CCGR1);val &= ~(3 << 26); // 清除以前的设置val |= (3 << 26); //设置新值writel(val,IMX6U_CCM_CCGR1);/* 3、设置 GPIO1_IO03 的复用功能,将其复用为GPIO1_IO03,最后设置 IO 属性。*/writel(5, SW_MUX_GPIO1_IO03);//寄存器 SW_PAD_GPIO1_IO03 设置 IO 属性writel(0x10B0, SW_PAD_GPIO1_IO03);/* 4、设置 GPIO1_IO03 为输出功能 */val = readl(GPIO1_GDIR);val &= ~(1 << 3); // 清除以前的设置 val |= (1 << 3); // 设置为输出 writel(val,GPIO1_GDIR);/* 5、默认关闭 LED */val=readl(GPIO1_DR);val|=(1<<3);writel(val,GPIO1_DR);//注册设备ret = register_chrdev(Led_Major,Led_Name, &leddev_fops);if (ret < 0){printk("register error!\n");return -EIO;}elseprintk("chrdev init successful!\n");return 0;
}// 模块卸载
static void __exit chrdev_exit(void)
{/* 取消映射 */iounmap(IMX6U_CCM_CCGR1);iounmap(SW_MUX_GPIO1_IO03);iounmap(SW_PAD_GPIO1_IO03);iounmap(GPIO1_DR);iounmap(GPIO1_GDIR);/* 注销字符设备驱动 */ unregister_chrdev(Led_Major,Led_Name);printk("chrdev exit.\n");
}module_init(leddev_init);
module_exit(leddev_exit);MODULE_LICENSE("GPL");
MODULE_AUTHOR("Yunes");
MODULE_DESCRIPTION("Simple Led device driver demo");
LED用户态测试代码
这个就比较简单了,代码如下:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>#define LED_ON 1
#define LED_OFF 0int main(int argc, char const *argv[])
{unsigned char led_buf[1];int fd, tmp;if (argc != 3) {printf("Usage: %s <device> <0=OFF | 1=ON>\n", argv[0]);return -1;}char * filename = (char *)argv[1];fd = open(filename, O_RDWR);if (fd < 0) {perror("open");return -1;}led_buf[0]=atoi(argv[2]);tmp = write(fd, led_buf, sizeof(led_buf));if (tmp < 0)perror("write error");close(fd);return 0;
}
总结
这就是本篇博客的全部内容。通过本文,我们不仅了解了物理地址与虚拟地址之间的关系,也掌握了如何在 Linux 驱动中通过寄存器映射操作硬件,最终实现了在开发板上对 LED 的控制。