LED驱动准备
在上一篇裸机开发实验中,我们是直接向寄存器地址写入数据的。
但在 Linux 驱动开发 中就不能这样做了。因为在 Linux 内核中,不允许直接操作物理地址 。
这是由于系统启用了 MMU(内存管理单元) ,内核采用了 虚拟内存机制。
换句话说,我们在裸机中使用的那些物理地址(如
0x020E0068),在 Linux 环境中已经不再是可直接访问的。我们必须先获得该寄存器物理地址对应的虚拟地址,才能对其进行读写。
为此,Linux 提供了两个非常关键的函数:
ioremap()------ 将物理地址映射为内核可访问的虚拟地址iounmap()------ 在不再使用时解除映射这对函数是驱动开发中访问硬件寄存器的"入口"和"出口"。
接下来是对两个函数的讲解
ioremap() 函数
原型
c
void __iomem *ioremap(resource_size_t phys_addr, unsigned long size);
功能
将设备的物理地址区间映射到内核虚拟地址空间,并返回虚拟地址指针。
参数说明
| 参数 | 含义 |
|---|---|
phys_addr |
起始物理地址,比如寄存器地址 0x0209C000 |
size |
要映射的字节大小,通常是 4、8 或 0x1000 |
返回值
- 成功:返回一个类型为
void __iomem *的虚拟地址指针 - 失败:返回
NULL
示例
c
#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() 函数
原型
c
void iounmap(void __iomem *addr);
功能
释放之前通过 ioremap() 映射的虚拟地址空间。
使用场景
- 驱动卸载(
module_exit)时; - 或者资源释放阶段。
示例
c
iounmap(GPIO1_DR);
表示释放对应的虚拟地址映射。
注意事项
-
不能直接对映射指针解引用:
c*GPIO1_DR = 1; // 错误应使用内核提供的接口:
cwritel(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()》这一篇博客所介绍的
字符设备驱动开发内容这段代码的重点就是:
- 地址映射
- 释放映射
下面是代码展示:
c
#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);//输出低电平,点亮LED
writel(val,GPIO1_DR);
}
else if(sta==LED_OFF)
{
val = readl(GPIO1_DR);
val |= (1 << 3);//输出高电平,熄灭LED
writel(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;
}
else
printk("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用户态测试代码
这个就比较简单了,代码如下:
c
#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 0
int 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 的控制。