查看: 627|回复: 0

[分享] 【经验分享】i.MX6ULL开发:驱动开发4——点亮LED(寄存器版)

[复制链接]
  • TA的每日心情
    开心
    2020-12-18 12:56
  • 签到天数: 55 天

    [LV.5]常住居民I

    71

    主题

    221

    帖子

    0

    管理员

    Rank: 9Rank: 9Rank: 9

    积分
    1588
    最后登录
    2024-4-22
    发表于 2022-8-22 13:54:32 | 显示全部楼层 |阅读模式
    本篇,就要来实际操作一下GPIO,实现板子上LED灯的亮灭控制。

    在介绍如何通过寄存器来控制LED之前,需要先来了解一下有关Linux地址映射相关的知识。
    1 地址映射
    Linux或是STM32,对于硬件的控制,本质都是操作寄存器,在对应的地址进行数据的读写。若是在裸机开发中,可以控制CPU直接操作寄存器的地址,实现相应的功能,其过程是这样的:
    图片 9.png
    linux环境,一般是不会直接访问物理内存,因为如果用户不小心修改了内存中的数据,很有可能造成错误甚至系统崩溃。为了避免这些问题,linux内核便引入了MMU和TLB进行内存地址映射,通过访问虚拟地址实现对实际物理地址的读写:
    图片 10.png
    1.1 MMU介绍
    MMU,Memory Manage Unit,即内存管理单元,它提供统一的内存空间抽象,程序通过访问虚拟内存中的地址,MMU将虚拟地址(Virtual Address)翻译成实际的物理地址(Physical Address) ,之后CPU即可操作实际的物理地址。
    MMU具有如下功能:
    • 保护内存: MMU给一些指定的内存块设置了读、写以及可执行的权限,这些权限存储在页表当中,MMU会检查CPU当前所处的是特权模式还是用户模式,只有和操作系统所设置的权限匹配才可以访问。
    • 提供方便统一的内存空间抽象,实现虚拟地址到物理地址的转换:CPU可以运行在虚拟的内存当中,虚拟内存一般要比实际的物理内存大很多,使得CPU可以运行比较大的应用程序。

    1.2 TLB介绍
    TLB,Translation Lookaside Buffer,即转译后备缓冲器,也称页表缓存,里面存放的是一些页表文件(虚拟地址到物理地址的转换表),又称为快表技术。
    当CPU第一次查找一个虚拟地址时,硬件通过3级页表(page table)得到最终的PPN(Physical Page Number),TLB会保存虚拟地址到物理地址的映射关系。这样在下一次访问同一个虚拟地址时,处理器通过查看TLB来直接返回物理地址,而不需要通过page table得到结果,从而提高地址转换的效率。
    1.3 I/O映射函数
    Linux内核启动的时候会初始化MMU,设置好内存映射,设置好以后CPU访问的都是虚拟地址。
    那在程序编写的时候,如何进行物理内存和虚拟内存之间的转换呢?这就需要用到两个函数:ioremap和iounmap。
    ioremap()
    ioremap函数用将物理地址映射为虚拟地址。
    1. <font face="Arial" size="3">#define ioremap(cookie,size) __arm_ioremap((cookie), (size), MT_DEVICE)

    2. /**
    3. * paddr: 被映射的 IO 起始地址(物理地址)
    4. * size: 需要映射的空间大小,以字节为单位
    5. * return: 一个指向__iomem类型的指针,映射成功后便返回一段虚拟地址空间的起始地址
    6. */
    7. void __iomem * __arm_ioremap(phys_addr_t phys_addr, size_t size, unsigned int mtype)
    8. {
    9. return arch_ioremap_caller(phys_addr, size, mtype, __builtin_return_address(0));
    10. }
    11. </font>
    复制代码
    iounmap()
    iounmap函数的作用是释放掉ioremap函数所做的映射,即反向操作,在卸载驱动的时候需要调用。
    1. <font face="Arial" size="3">/**
    2. * addr: 要取消映射的虚拟地址空间首地址
    3. * return: void
    4. */
    5. void iounmap (volatile void __iomem *addr)</font>
    复制代码
    1.4 I/O内存访问函数
    在使用ioremap函数将物理地址转换成虚拟地址之后,理论上我们便可以直接读写 I/O 内存,但是为了符合驱动的跨平台以及可移植性,我们应该使用 linux 中指定的函数(如:iowrite8()、iowrite16()、iowrite32()、ioread8()、ioread16()、ioread32() 等)去读写 I/O 内存,而非直接通过映射后的指向虚拟地址的指针进行访问。读写 I/O 内存的函数如下:
    1. <font face="Arial" size="3">unsigned int ioread8(void __iomem *addr);  /*读取一个字节*/
    2. unsigned int ioread16(void __iomem *addr); /*读取一个字*/
    3. unsigned int ioread32(void __iomem *addr); /*读取一个双字*/

    4. void iowrite8(u8 b, void __iomem *addr);   /*写入一个字节*/
    5. void iowrite16(u16 b, void __iomem *addr); /*写入一个字*/
    6. void iowrite32(u32 b, void __iomem *addr); /*写入一个双字*/</font>
    复制代码
    对于读I/O而言,他们都只有一个 __iomem 类型指针的参数,指向被映射后的地址,返回值为读取到的数据;
    对于写I/O而言他们都有两个参数,第一个为要写入的数据,第二个参数为要写入的地址,返回值为空。
    与这些函数相似的还有writeb、writew、writel、readb、readw、readl 等
    1. <font face="Arial" size="3">u8  readb(const volatile void __iomem *addr);
    2. u16 readw(const volatile void __iomem *addr);
    3. u32 readl(const volatile void __iomem *addr);

    4. void writeb(u8 value,  volatile void __iomem *addr);
    5. void writew(u16 value, volatile void __iomem *addr);
    6. void writel(u32 value, volatile void __iomem *addr);</font>
    复制代码
    在 ARM 架构下,writex(readx)函数与 iowritex(ioreadx)有一些区别,writex(readx)不进行端序的检查,而 iowritex(ioreadx)会进行端序的检查。
    2 程序编写2.1 LED驱动程序
    led驱动也是属于字符设备驱动的,之前介绍了新旧两种字符驱动的写法,本篇led驱动就按照新字符设置驱动的写法来编写。
    关于新字符设备的驱动模块,可参考之前的文章:【i.MX6ULL】驱动开发2--新字符设备开发模板
    这里再放一张新字符设备开发的模板框架
    图片 11.png
    2.1.1 字符设备的基本框架
    1. <font face="Arial" size="3">//字符设备结构体
    2. struct newchrled_dev{
    3. dev_t         devid; /* 设备号   */
    4. struct cdev   cdev;  /* cdev     */
    5. struct class  *class; /* 类       */
    6. struct device *device; /* 设备     */
    7. int           major; /* 主设备号 */
    8. int           minor; /* 次设备号 */
    9. };
    10. struct newchrled_dev chrdevled; /* led设备 */

    11. //打开 读取 写入 释放, 基础文件操作函数
    12. static int chrdevled_open(struct inode *inode, struct file *filp)
    13. {
    14.    /*设置chrdevled为私有数据*/
    15.    return 0;
    16. }
    17. static ssize_t chrdevled_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
    18. {
    19. return 0;
    20. }
    21. static ssize_t chrdevled_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
    22. {
    23. return 0;
    24. }
    25. static int chrdevled_release(struct inode *inode, struct file *filp)
    26. {
    27. return 0;
    28. }

    29. //设备操作函数结构体
    30. static struct file_operations chrdevled_fops = {
    31. .owner   = THIS_MODULE,
    32. .open    = chrdevled_open,
    33. .read    = chrdevled_read,
    34. .write   = chrdevled_write,
    35. .release = chrdevled_release,
    36. };

    37. //驱动入口函数
    38. static int __init chrdevled_init(void)
    39. {
    40.    /* 初始化LED */
    41.    /* 注册字符设备驱动(操作chrdevled_fops) */
    42.    return 0;
    43. }

    44. //驱动出口函数
    45. static void __exit chrdevled_exit(void)
    46. {
    47.    /* 取消IO映射 */
    48.    /* 注销字符设备驱动 */
    49. }

    50. //驱动的入口和出口函数
    51. module_init(chrdevled_init);
    52. module_exit(chrdevled_exit);

    53. //LICENSE和作者信息
    54. MODULE_LICENSE("GPL");
    55. MODULE_AUTHOR("xxpcb");</font>
    复制代码
    2.1.2 具体完善
    1)GPIO寄存器宏定义
    需要配置相关的寄存器,就要对照着LED这个GPIO的硬件按需配置。
    有关GPIO的各种寄存器的使用原理介绍,请参考上篇文章的介绍。
    图片 12.png
    1. <font face="Arial" size="3">/* 寄存器物理地址 */
    2. #define CCM_CCGR1_BASE    (0X020C406C)
    3. #define SW_MUX_SNVS_TAMPER3_BASE    (0X02290014)
    4. #define SW_PAD_SNVS_TAMPER3_BASE    (0X02290058)
    5. #define GPIO5_DR_BASE    (0X020AC000)
    6. #define GPIO5_GDIR_BASE    (0X020AC004)

    7. /* 映射后的寄存器虚拟地址指针 */
    8. static void __iomem *IMX6U_CCM_CCGR1;
    9. static void __iomem *SW_MUX_SNVS_TAMPER3;
    10. static void __iomem *SW_PAD_SNVS_TAMPER3;
    11. static void __iomem *GPIO5_DR;
    12. static void __iomem *GPIO5_GDIR;</font>
    复制代码
    • CCM 是用来进行时钟的使能,其寄存器包括CCGR0~CCGR6,因为LED用到GPIO属于GPIO5,它对应的时钟配置寄存器就是CCM_CCGR1
    • MUX 是用来将IO复用为GPIO
    • PAD 是用来配置IO的基本参数(驱动能力、压摆率、上下拉等)
    • GPIO5_DR 数据寄存器,当GPIO为输出模式时,用来设置对应的高低电平
    • GPIO5_GDIR 方向寄存器,用来设置输入还是输出

    以上是先对这些需要使用的寄存器的地址声明宏定义(这些寄存器的地址可通过查阅i.MX6ULL数据手册得到),然后再声明对应的虚拟地址的指针,因为Linux开始MMU后,就不能直接对寄存器的地址直接操作了,需要使用映射后的虚拟地址。
    2)GPIO硬件初始化
    主要包括以下几步:
    • 寄存器地址映射:将需要用的寄存器的物理地址映射为虚拟地址
    • 使能GPIO1时钟:就是配置CCM_CCGR1寄存器
    • 设置GPIO5_IO03的复用功能:配置MUX和PAD寄存器
    • 设置GPIO5_IO03为输出功能:配置GPIO5_GDIR方向寄存器
    • 初始默认关闭LED:配置GPIO5_DR数据寄存器

    具体配置过程如下,主要这里使用"与"和"或"的位运算操作,来配置寄存器中对应位的值。
    1. <font face="Arial" size="3">static void led_hardware_init(void)
    2. {
    3.    u32 val = 0;

    4.    /* 1、寄存器地址映射 */
    5.    IMX6U_CCM_CCGR1 = ioremap(CCM_CCGR1_BASE, 4);
    6.    SW_MUX_SNVS_TAMPER3 = ioremap(SW_MUX_SNVS_TAMPER3_BASE, 4);
    7.    SW_PAD_SNVS_TAMPER3 = ioremap(SW_PAD_SNVS_TAMPER3_BASE, 4);
    8.    GPIO5_DR = ioremap(GPIO5_DR_BASE, 4);
    9.    GPIO5_GDIR = ioremap(GPIO5_GDIR_BASE, 4);

    10.    /* 2、使能GPIO1时钟 */
    11.    val = readl(IMX6U_CCM_CCGR1);
    12.    val &= ~(3 << 26);    /* 清除以前的设置 */
    13.    val |= (3 << 26);    /* 设置新值 */
    14.    writel(val, IMX6U_CCM_CCGR1);

    15.    /* 3、设置GPIO5_IO03的复用功能,将其复用为GPIO5_IO03,最后设置IO属性 */
    16.    writel(5, SW_MUX_SNVS_TAMPER3);

    17.    /*寄存器SW_PAD_SNVS_TAMPER3设置IO属性
    18.     *bit 16:0 HYS关闭
    19.     *bit [15:14]: 00 默认下拉
    20.     *bit [13]: 0 kepper功能
    21.     *bit [12]: 1 pull/keeper使能
    22.     *bit [11]: 0 关闭开路输出
    23.     *bit [7:6]: 10 速度100Mhz
    24.     *bit [5:3]: 110 R0/6驱动能力
    25.     *bit [0]: 0 低转换率
    26.     */
    27.    writel(0x10B0, SW_PAD_SNVS_TAMPER3);

    28.    /* 4、设置GPIO5_IO03为输出功能 */
    29.    val = readl(GPIO5_GDIR);
    30.    val &= ~(1 << 3);    /* 清除以前的设置 */
    31.    val |= (1 << 3);    /* 设置为输出 */
    32.    writel(val, GPIO5_GDIR);

    33.    /* 5、默认关闭LED */
    34.    val = readl(GPIO5_DR);
    35.    val |= (1 << 3);
    36.    writel(val, GPIO5_DR);
    37. }</font>
    复制代码
    3)字符设备初始化
    需要定义led字符设备结构体,来管理这个led设备。
    1. <font face="Arial" size="3">/*newchr设备结构体 */
    2. struct newchrled_dev{
    3.    dev_t         devid;    /* 设备号   */
    4.    struct cdev   cdev;     /* cdev     */
    5.    struct class  *class;   /* 类       */
    6.    struct device *device;  /* 设备     */
    7.    int           major;    /* 主设备号 */
    8.    int           minor;    /* 次设备号 */
    9. };

    10. struct newchrled_dev chrdevled; /* led设备 */</font>
    复制代码
    具体的led字符设备初始化流程:
    • 初始化LED的GPIO(上面刚介绍)
    • 创建设备号
    • 初始化cdev字符设备
    • 添加cdev字符设备
    • 创建类
    • 创建设备

    1. <font face="Arial" size="3">static int __init chrdevled_init(void)
    2. {
    3.    /* 初始化LED */
    4.    led_hardware_init();

    5.    /* 注册字符设备驱动 */
    6.    /* 1、创建设备号 */
    7.    if (chrdevled.major) /* 定义了设备号 */
    8.    {
    9.        chrdevled.devid = MKDEV(chrdevled.major, 0);
    10.        register_chrdev_region(chrdevled.devid, chrdevled_CNT, chrdevled_NAME);
    11.    }
    12.    else /* 没有定义设备号 */
    13.    {
    14.        alloc_chrdev_region(&chrdevled.devid, 0, chrdevled_CNT, chrdevled_NAME);    /* 申请设备号 */
    15.        chrdevled.major = MAJOR(chrdevled.devid);    /* 获取分配号的主设备号 */
    16.        chrdevled.minor = MINOR(chrdevled.devid);    /* 获取分配号的次设备号 */
    17.    }
    18.    printk("chrdevled major=%d,minor=%d\n",chrdevled.major, chrdevled.minor);

    19.    /* 2、初始化cdev */
    20.    chrdevled.cdev.owner = THIS_MODULE;
    21.    cdev_init(&chrdevled.cdev, &chrdevled_fops);

    22.    /* 3、添加一个cdev */
    23.    cdev_add(&chrdevled.cdev, chrdevled.devid, chrdevled_CNT);

    24.    /* 4、创建类 */
    25.    chrdevled.class = class_create(THIS_MODULE, chrdevled_NAME);
    26.    if (IS_ERR(chrdevled.class))
    27.    {
    28.        return PTR_ERR(chrdevled.class);
    29.    }

    30.    /* 5、创建设备 */
    31.    chrdevled.device = device_create(chrdevled.class, NULL, chrdevled.devid, NULL, chrdevled_NAME);
    32.    if (IS_ERR(chrdevled.device))
    33.    {
    34.        return PTR_ERR(chrdevled.device);
    35.    }

    36.    printk("chrdevled init done!\n");
    37.    return 0;
    38. }</font>
    复制代码
    4)LED亮灭控制
    驱动程序中,对于LED的控制,可以分为两步。
    第一步是接收和解析应用层发来的控制数据(0或1来控制亮灭),将控制参数传递给具体的开关led的函数:
    1. <font face="Arial" size="3">static ssize_t chrdevled_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
    2. {
    3.    unsigned char databuf[1];
    4.    unsigned char ledstat;

    5.    /* 接收用户空间传递给内核的数据并且打印出来 */
    6.    if(0 != copy_from_user(databuf, buf, cnt))
    7.    {
    8.        printk("kernel recevdata failed!\n");
    9.        return -EFAULT;
    10.    }

    11.    ledstat = databuf[0];        /* 获取状态值 */

    12.    if(ledstat == LEDON)
    13.    {
    14.        led_switch(LEDON);        /* 打开LED灯 */
    15.        printk("led on!\n");
    16.    }
    17.    else if(ledstat == LEDOFF)
    18.    {
    19.        led_switch(LEDOFF);       /* 关闭LED灯 */
    20.        printk("led off!\n");
    21.    }

    22.    return 0;
    23. }</font>
    复制代码
    第二步就是根据指令参数,通过控制数据寄存器GPIO5_DR来实现GPIO的高低电平输出,从而实现LED的亮灭:
    1. <font face="Arial" size="3">void led_switch(u8 sta)
    2. {
    3.    u32 val = 0;
    4.    if(sta == LEDON)
    5.    {
    6.        val = readl(GPIO5_DR);
    7.        val &= ~(1 << 3);
    8.        writel(val, GPIO5_DR);
    9.    }
    10.    else if(sta == LEDOFF)
    11.    {
    12.        val = readl(GPIO5_DR);
    13.        val|= (1 << 3);
    14.        writel(val, GPIO5_DR);
    15.    }
    16. }</font>
    复制代码
    5)驱动退出
    驱动不再使用时,需要注销相关的设备:
    首先释放掉这些地址映射:
    1. <font face="Arial" size="3">static void led_hardware_exit(void)
    2. {
    3.    iounmap(IMX6U_CCM_CCGR1);
    4.    iounmap(SW_MUX_SNVS_TAMPER3);
    5.    iounmap(SW_PAD_SNVS_TAMPER3);
    6.    iounmap(GPIO5_DR);
    7.    iounmap(GPIO5_GDIR);
    8. }</font>
    复制代码
    具体的注销过程:
    1. <font face="Arial" size="3">static void __exit chrdevled_exit(void)
    2. {
    3.    /* 取消IO映射 */
    4.    led_hardware_exit();

    5.    /* 注销字符设备驱动 */
    6.    cdev_del(&chrdevled.cdev);/*  删除cdev */
    7.    unregister_chrdev_region(chrdevled.devid, chrdevled_CNT); /* 注销设备号 */

    8.    device_destroy(chrdevled.class, chrdevled.devid);
    9.    class_destroy(chrdevled.class);

    10.    printk("chrdevled exit done!\n");
    11. }</font>
    复制代码
    驱动程序基本就是这些,完整的程序见gitee仓库:https://gitee.com/xxpcb/imx6ull
    2.2 LED应用程序
    写完了驱动程序(BSP),还要写对应的应用程序(APP)。
    目前的应用程序比较简短,因为在Linux中,一切皆文件,所以,对于LED的控制,就是通过向文件中写入0或1来实现LED的亮灭。
    先来对0和1进行宏定义:
    1. <font face="Arial" size="3">#define LEDOFF   0 /*长灭*/
    2. #define LEDON    1 /*长亮*/</font>
    复制代码
    然后就是main函数了:
    1. <font face="Arial" size="3">int main(int argc, char *argv[])
    2. {
    3.    int fd, retvalue;
    4.    char *filename;
    5.    unsigned char databuf[1];

    6.    if(argc != 3)
    7.    {
    8.        printf("Error Usage!\r\n");
    9.        return -1;
    10.    }

    11.    filename = argv[1];

    12.    /* 打开led驱动文件 */
    13.    fd  = open(filename, O_RDWR);
    14.    if(fd < 0)
    15.    {
    16.        printf("Can't open file %s\r\n", filename);
    17.        return -1;
    18.    }

    19.    /* 要执行的操作:打开或关闭 */
    20.    databuf[0] = atoi(argv[2]);

    21.    /* 向设备驱动(/dev/chrdevled)写数据 */
    22.    retvalue = write(fd, databuf, sizeof(databuf));
    23.    if(retvalue < 0)
    24.    {
    25.        printf("write file %s failed!\r\n", filename);
    26.        close(fd);
    27.        return -1;
    28.    }

    29.    /* 关闭设备 */
    30.    retvalue = close(fd);
    31.    if(retvalue < 0)
    32.    {
    33.        printf("Can't close file %s\r\n", filename);
    34.        return -1;
    35.    }

    36.    return 0;
    37. }</font>
    复制代码
    3 实验测试
    3.1 程序编译与下载
    再来复习一下基本步骤:
    • ubuntu中通过gcc交叉编译器编译出led的驱动程序和应用程序
    • 搭建局域网环境(电脑和linux板子连接到同一个路由器下,Linux板子以及烧录了镜像文件,能够正常运行)
    • 通过tftp服务将两个文件发送到linux板子的对应目录中(/lib/modules/4.1.15目录)
    • 进行字符设备的加载,以及文件读写测试(控制led亮灭)

    图片 13.png


    程序的具体编译过程与之前的类似,这里不再赘述,可参考之前的文章(如这篇:【i.MX6ULL】驱动开发2--新字符设备开发模板)
    3.2 实验现象
    首先来看一下板子上LED的位置,如下图的电路上的标号D14处:
    图片 14.png
    然后在串口中,按照之前介绍字符设备的加载流程,先加载led字符设备,然后就可以下向应用程序写1或0来控制led的亮灭了。
    图片 15.png
    led点亮的效果如下:
    图片 16.png
    4 总结
    本篇主要介绍了如何通过操作寄存器来点亮i.MX6ULL开发板上的led,通过编写LED对应的驱动程序和应用程序,实现程序设计的分层。
    因为Linux使用了MMU进行虚拟地址管理,因此在操作寄存器时,要进行地址映射后再操作。最后通过程序的实际测试,验证了led的亮灭功能。
    本篇的完整程序见gitee仓库:https://gitee.com/xxpcb/imx6ull


    签到
    回复

    使用道具 举报

    您需要登录后才可以回帖 注册/登录

    本版积分规则

    关闭

    站长推荐上一条 /4 下一条

    Archiver|手机版|小黑屋|恩智浦技术社区

    GMT+8, 2024-4-25 08:19 , Processed in 0.116326 second(s), 19 queries , MemCache On.

    Powered by Discuz! X3.4

    Copyright © 2001-2024, Tencent Cloud.

    快速回复 返回顶部 返回列表