一灯大师:在IMX6ULL上点亮LED之光

一.imx6ull GPIO原理

1. STM32 GPIO回顾

我们一般拿到一款全新的芯片,第一个要做的事情的就是驱动其 GPIO,控制其 GPIO 输出高低电平,我们学习 I.MX6U 也一样的,先来学习一下 I.MX6U 的 GPIO。在学习 I.MX6U的 GPIO 之前,我们先来回顾一下 STM32 的 GPIO 初始化(如果没有学过 STM32 就不用回顾了),我们以最常见的 STM32F103 为例来看一下 STM32 的 GPIO 初始化,示例代码如下:

void LED_Init(void)
{
    GPIO_InitTypeDef GPIO_InitStructure;

    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);//使能 PB 端口时钟

    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5; //PB5 端口配置
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //推挽输出
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; //IO 口速度
    GPIO_Init(GPIOB, &GPIO_InitStructure); //根据设定参数初始化 GPIOB.5

    GPIO_SetBits(GPIOB,GPIO_Pin_5); //PB.5 输出高
}

上述代码就是使用库函数来初始化 STM32 的一个 IO 为输出功能,可以看出上述初始化代码中重点要做的事情有一下几个:

①、使能指定 GPIO 的时钟。

②、初始化 GPIO,比如输出功能、上拉、速度等等。

③、 STM32 有的 IO 可以作为其它外设引脚,也就是 IO 复用,如果要将 IO 作为其它外设引脚使用的话就需要设置 IO 的复用功能。

④、最后设置 GPIO 输出高电平或者低电平。

STM32 的 GPIO 初始化就是以上四步,那么会不会也适用于 I.MX6U 的呢? I.MX6U 的GPIO 是不是也需要开启相应的时钟?是不是也可以设置复用功能?是不是也可以设置输出或输入、上下拉、速度等等这些?我们现在都不知道,只有去看 I.MX6U 的数据手册和参考手册才能知道,带着上面四个疑问打开这两份手册,然后就是“啃”手册。

2. imx6ull原理图

可以看到LED挂在GPIO1_IO03上,输出高电平就是熄灭LED灯,输出低电平就是点亮LED灯

3. imx6ull寄存器查看

对于imx6ull我们基本上在LED上会用到以下章节的内容:

CCM: Clock Controller Module (时钟控制模块) – 章节18

IOMUXC : IOMUX Controller,IO复用控制器 – 章节32

GPIO: General-purpose input/output,通用的输入输出口 – 章节28

3.1 GPIO的模块结构

参考资料:芯片手册《Chapter 28: General Purpose Input/Output (GPIO)》

有5组GPIO(GPIO1~GPIO5),每组引脚最多有32个,但是可能实际上并没有那么多。

GPIO1有32个引脚:GPIO1_IO0~GPIO1_IO31;

GPIO2有22个引脚:GPIO2_IO0~GPIO2_IO21;

GPIO3有29个引脚:GPIO3_IO0~GPIO3_IO28;

GPIO4有29个引脚:GPIO4_IO0~GPIO4_IO28;

GPIO5有12个引脚:GPIO5_IO0~GPIO5_IO11;

GPIO的控制涉及4大模块:CCM、IOMUXC、GPIO模块本身,框图如下:

3.2 CCM用于设置是否向GPIO模块提供时钟

参考资料:芯片手册《Chapter 18: Clock Controller Module (CCM)》

GPIOx要用CCM_CCGRy寄存器中的2位来决定该组GPIO是否使能。哪组GPIO用哪个CCM_CCGR寄存器来设置,请看上图红框部分。

CCM_CCGR寄存器中某2位的取值含义如下:

① 00:该GPIO模块全程被关闭

② 01:该GPIO模块在CPU run mode情况下是使能的;在WAIT或STOP模式下,关闭

③ 10:保留

④ 11:该GPIO模块全程使能

GPIO2时钟控制:

GPIO1时钟控制:

GPIO3时钟控制:

GPIO4时钟控制:

3.3 IOMUXC:引脚的模式(Mode、功能)

参考资料:芯片手册《Chapter 32: IOMUX Controller (IOMUXC)》

对于某个/某组引脚,IOMUXC中有2个寄存器用来设置它:

① 选择功能:

IOMUXC_SW_MUX_CTL_PAD_<PADNAME> :Mux pad xxx,选择某个pad的功能

IOMUXC_SW_MUX_CTL_GRP_<GROUP NAME>:Mux grp xxx,选择某组引脚的功能

某个引脚,或是某组预设的引脚,都有8个可选的模式(alternate (ALT) MUX_MODE)。

某个引脚,或是某组预设的引脚,都有8个可选的模式(alternate (ALT) MUX_MODE)。

比如:

② 设置上下拉电阻等参数:

IOMUXC_SW_PAD_CTL_PAD_<PAD_NAME>:pad pad xxx,设置某个pad的参数

IOMUXC_SW_PAD_CTL_GRP_<GROUP NAME>:pad grp xxx,设置某组引脚的参数

比如:

3.4 GPIO模块内部

我们暂时只需要关心3个寄存器:

① GPIOx_GDIR:设置引脚方向,每位对应一个引脚,1-output,0-input

② GPIOx_DR:设置输出引脚的电平,每位对应一个引脚,1-高电平,0-低电平

③ GPIOx_PSR:读取引脚的电平,每位对应一个引脚,1-高电平,0-低电平

GPIO1Memory map如下:

3.5 读GPIO

翻译一下:

① 设置CCM_CCGRx寄存器中某位使能对应的GPIO模块// 默认是使能的,上图省略了

② 设置IOMUX来选择引脚用于GPIO

③ 设置GPIOx_GDIR中某位为0,把该引脚设置为输入功能

④ 读GPIOx_DR或GPIOx_PSR得到某位的值(读GPIOx_DR返回的是GPIOx_PSR的值)

3.6 写GPIO

翻译一下:

① 设置CCM_CCGRx寄存器中某位使能对应的GPIO模块// 默认是使能的,上图省略了

② 设置IOMUX来选择引脚用于GPIO

③ 设置GPIOx_GDIR中某位为1,把该引脚设置为输出功能

④ 写GPIOx_DR某位的值

二.Linux 下 LED 灯驱动原理

Linux 下的任何外设驱动,最终都是要配置相应的硬件寄存器。所以本章的 LED 灯驱动最终也是对 I.MX6ULL 的 IO 口进行配置,与裸机实验不同的是,在 Linux 下编写驱动要符合 Linux的驱动框架。I.MX6U-ALPHA 开发板上的 LED 连接到 I.MX6ULL 的 GPIO1_IO03 这个引脚上,因此本章实验的重点就是编写 Linux 下 I.MX6UL 引脚控制驱动。

1. 地址映射

在编写驱动之前,我们需要先简单了解一下 MMU 这个神器, MMU 全称叫做 MemoryManage Unit,也就是内存管理单元。在老版本的 Linux 中要求处理器必须有 MMU,但是现在Linux 内核已经支持无 MMU 的处理器了。 MMU 主要完成的功能如下:

①、完成虚拟空间到物理空间的映射。

②、内存保护,设置存储器的访问权限,设置虚拟存储空间的缓冲特性。

我们重点来看一下第①点,也就是虚拟空间到物理空间的映射,也叫做地址映射。首先了解两个地址概念:虚拟地址(VA,Virtual Address)、物理地址(PA, Physcical Address)。对于 32 位的处理器来说,虚拟地址范围是 2^32=4GB,我们的开发板上有 512MB 的 DDR3,这 512MB 的内存就是物理内存,经过 MMU 可以将其映射到整个 4GB 的虚拟空间 ,如图:

物理内存只有 512MB,虚拟内存有 4GB,那么肯定存在多个虚拟地址映射到同一个物理地址上去,虚拟地址范围比物理地址范围大的问题处理器自会处理,这里我们不要去深究,因为MMU 是很复杂的一个东西 。

Linux 内核启动的时候会初始化 MMU,设置好内存映射,设置好以后 CPU 访问的都是虚拟 地 址 。 比 如 I.MX6ULL 的 GPIO1_IO03 引 脚 的 复 用 寄 存 器IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03 的地址为 0X020E0068。如果没有开启 MMU 的话直接向 0X020E0068 这个寄存器地址写入数据就可以配置 GPIO1_IO03 的复用功能。现在开启了 MMU,并且设置了内存映射,因此就不能直接向 0X020E0068 这个地址写入数据了。我们必须得到 0X020E0068 这个物理地址在 Linux 系统里面对应的虚拟地址,这里就涉及到了物理内存和虚拟内存之间的转换,需要用到两个函数: ioremap 和 iounmap。

1.1 ioremap 函数

ioremap 函 数 用 于 获 取 指 定 物 理 地 址 空 间 对 应 的 虚 拟 地 址 空 间 , 定 义 在arch/arm/include/asm/io.h 文件中,定义如下:

#define ioremap(cookie,size)        __arm_ioremap((cookie), (size), MT_DEVICE)
void __iomem *__arm_ioremap(phys_addr_t phys_addr, size_t size, unsigned int mtype)
{
    return arch_ioremap_caller(phys_addr, size, mtype,
        __builtin_return_address(0));
}

ioremap 是个宏,有两个参数: cookie 和 size,真正起作用的是函数__arm_ioremap,此函数有三个参数和一个返回值,这些参数和返回值的含义如下:

phys_addr:要映射给的物理起始地址。

size:要映射的内存空间大小。

mtype: ioremap 的类型,可以选择 MT_DEVICE、 MT_DEVICE_NONSHARED、MT_DEVICE_CACHED 和 MT_DEVICE_WC, ioremap 函数选择 MT_DEVICE。

返回值: __iomem 类型的指针,指向映射后的虚拟空间首地址。

假如我们要获取 I.MX6ULL 的 IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03 寄存器对应的虚拟地址,使用如下代码即可:

#define SW_MUX_GPIO1_IO03_BASE (0X020E0068)
static void __iomem* SW_MUX_GPIO1_IO03;
SW_MUX_GPIO1_IO03 = ioremap(SW_MUX_GPIO1_IO03_BASE, 4); 

宏 SW_MUX_GPIO1_IO03_BASE 是寄存器物理地址, SW_MUX_GPIO1_IO03 是映射后的虚拟地址。对于 I.MX6ULL 来说一个寄存器是 4 字节(32 位)的,因此映射的内存长度为 4。映射完成以后直接对SW_MUX_GPIO1_IO03 进行读写操作即可。

1.2 iounmap 函数

卸载驱动的时候需要使用 iounmap 函数释放掉 ioremap 函数所做的映射, iounmap 函数原型如下:

#define iounmap                __arm_iounmap
void __arm_iounmap(volatile void __iomem *io_addr)
{
    arch_iounmap(io_addr);
}

iounmap 只有一个参数 addr,此参数就是要取消映射的虚拟地址空间首地址。假如我们现在要取消掉 IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03 寄存器的地址映射,使用如下代码即可:

iounmap(SW_MUX_GPIO1_IO03);

2. I/O 内存访问函数

这里说的 I/O 是输入/输出的意思,并不是我们学习单片机的时候讲的 GPIO 引脚。这里涉及到两个概念: I/O 端口和 I/O 内存。当外部寄存器或内存映射到 IO 空间时,称为 I/O 端口。当外部寄存器或内存映射到内存空间时,称为 I/O 内存。但是对于 ARM 来说没有 I/O 空间这个概念,因此 ARM 体系下只有 I/O 内存(可以直接理解为内存)。使用 ioremap 函数将寄存器的物理地址映射到虚拟地址以后,我们就可以直接通过指针访问这些地址,但是 Linux 内核不建议这么做,而是推荐使用一组操作函数来对映射后的内存进行读写操作。

2.1 读操作函数

读操作函数有如下几个:

u8 readb(const volatile void __iomem *addr)
u16 readw(const volatile void __iomem *addr)
u32 readl(const volatile void __iomem *addr)

readb、 readw 和 readl 这三个函数分别对应 8bit、 16bit 和 32bit 读操作,参数 addr 就是要读取写内存地址,返回值就是读取到的数据。

2.2 写操作函数

void writeb(u8 value, volatile void __iomem *addr)
void writew(u16 value, volatile void __iomem *addr)
void writel(u32 value, volatile void __iomem *addr)

writeb、 writew 和 writel 这三个函数分别对应 8bit、 16bit 和 32bit 写操作,参数 value 是要写入的数值, addr 是要写入的地址。

三. 编写LED代码

  1. driver代码
#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"

static struct class *led_class;

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


static int led_open(struct inode *inode, struct file *filp)
{
    printk("led_open\r\n");
    return 0;
}


static ssize_t led_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
    printk("led_read\r\n");
    return 0;
}

static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
    printk("led_write\r\n");
    int retvalue;
    u32 val = 0;
    unsigned char databuf[1];
    unsigned char ledstat;

    retvalue = copy_from_user(databuf, buf, cnt);
    if(retvalue < 0) {
        printk("kernel write failed!\r\n");
        return -EFAULT;
    }

    ledstat = databuf[0];

    printk("ledstat:%d\r\n",ledstat);

    if(ledstat == 1)
    {
        val = readl(GPIO1_DR);
        val &= ~(1 << 3);    
        writel(val, GPIO1_DR);
    }
    else if(ledstat == 0)
    {
        val = readl(GPIO1_DR);
        val|= (1 << 3);    
        writel(val, GPIO1_DR);
    }

    return 1;
}


static int led_release(struct inode *inode, struct file *filp)
{
    printk("led_release\r\n");
    return 0;
}



static struct file_operations led_fops = {
    .owner = THIS_MODULE,
    .open = led_open,
    .read = led_read,
    .write = led_write,
    .release =     led_release,
};


static int __init led_driver_init(void)
{
    u32 val = 0;
    int retvalue = 0;
    printk("led_driver_init\r\n");

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

    val = readl(IMX6U_CCM_CCGR1);
    val &= ~(3 << 26);
    val |= (3 << 26);
    writel(val, IMX6U_CCM_CCGR1);

    writel(5, SW_MUX_GPIO1_IO03);

    writel(0x10B0, SW_PAD_GPIO1_IO03);

    val = readl(GPIO1_GDIR);
    val &= ~(1 << 3);
    val |= (1 << 3);
    writel(val, GPIO1_GDIR);

    val = readl(GPIO1_DR);
    val |= (1 << 3);    
    writel(val, GPIO1_DR);

    
    retvalue = register_chrdev(LED_MAJOR, LED_NAME, &led_fops);
    if(retvalue < 0){
        printk("register chrdev failed!\r\n");
        return -EIO;
    }
    
    led_class = class_create(THIS_MODULE,"led_class");

    device_create(led_class,NULL,MKDEV(LED_MAJOR,0),NULL,"led"); /* /dev/led */
    
    return 0;
}

static void __exit led_driver_exit(void)
{
    printk("led_driver_exit\r\n");

    iounmap(IMX6U_CCM_CCGR1);
    iounmap(SW_MUX_GPIO1_IO03);
    iounmap(SW_PAD_GPIO1_IO03);
    iounmap(GPIO1_DR);
    iounmap(GPIO1_GDIR);
    device_destroy(led_class,MKDEV(LED_MAJOR,0));
    class_destroy(led_class);
    unregister_chrdev(LED_MAJOR, LED_NAME);
}

module_init(led_driver_init);
module_exit(led_driver_exit);
MODULE_LICENSE("GPL"); 

2.测试app

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>



int main(int argc, char *argv[])
{
    int fd;
    int ret;
    uint8_t led;
    fd  = open(argv[1], O_RDWR);

    if(!strcmp("led_on",argv[2]))
    {
        printf("led on\r\n");
        led = 1;
        write(fd,&led,sizeof(led));
    }

    if(!strcmp("led_off",argv[2]))
    {
        led = 0;
        printf("led on\r\n");
        write(fd,&led,sizeof(led));
    }

    
    close(fd);

    
}

测试方法:

点亮 LED灯 ./test_app /dev/led led_on

熄灭 LED灯 ./test_app /dev/led led_on

3.Makefile

KERNELDIR := /home/zhongjun/project/board/yuanzi/imx6ull/nfs/kernel
CURRENT_PATH := $(shell pwd)

obj-m := led_drv.o

build: kernel_modules

kernel_modules:
    $(MAKE) -C $(KERNELDIR) $(KBUILD_CFLAGS) M=$(CURRENT_PATH) modules
    $(CROSS_COMPILE)gcc -o test_app test_app.c
clean:
    $(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
    rm -rf test_app

参考:

1.https://weidongshan.blog.csdn.net/article/details/122475478

2.【韦东山】嵌入式Linux应用开发完全手册V4.0_韦东山全系列视频文档-IMX6ULL开发板.docx

3.【正点原子】I.MX6U嵌入式Linux驱动开发指南V1.4.pdf

物联沃分享整理
物联沃-IOTWORD物联网 » 一灯大师:在IMX6ULL上点亮LED之光

发表评论