《i.MX6ULL LED 驱动实战:内核模块开发与 GPIO 控制》

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);

表示释放对应的虚拟地址映射。


注意事项

  1. 不能直接对映射指针解引用:

    c 复制代码
    *GPIO1_DR = 1; // 错误

    应使用内核提供的接口:

    c 复制代码
    writel(1, GPIO1_DR);
    val = readl(GPIO1_DR);
  2. 映射范围应与寄存器手册对应。

    如果寄存器区域是 0x1000 字节,应完整映射,否则可能越界。

  3. 同一物理区域重复调用 ioremap 会得到不同虚拟地址,但都指向同一物理区域,不推荐这样做。

  4. 用户态程序不能使用 ioremap,它是内核态函数。用户态可通过 /dev/memmmap() 实现类似效果。


类比说明

环境 行为 比喻
裸机 直接写寄存器地址 拿钥匙直接开门
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 的控制。

相关推荐
闻道且行之3 小时前
KickPi RK3568平台SPI内核驱动开发
驱动开发·rk3568·kernel·spi·kickpi
FJW0208143 小时前
【Linux】输入输出管理
linux·运维·服务器
晨曦夜月4 小时前
vim及其模式的操作
linux·编辑器·vim
zl_dfq4 小时前
Linux基础开发工具 之 【yum、vim、gcc/g++】
linux·1024程序员节
古一木4 小时前
ROS1+Vscode
ide·vscode·编辑器
sukalot4 小时前
windows显示驱动开发-缩放桌面图像(二)
windows·驱动开发
YONYON-R&D5 小时前
VSCODE 调试C程序时 打印中文
ide·vscode·编辑器
冷天气会感冒5 小时前
关闭VSCode的GitHub Copilot功能
vscode·github·copilot
守望时空335 小时前
使用virt-manager图形化创建和管理KVM虚拟机
linux·kvm