资讯详情

i.MX6ULL驱动开发 | 03-基于字符设备驱动框架点亮LED

一、硬件原理

1. 原理图确定硬件连接

i.MX6ULL开发板上有用户LED,如图: LED0连接到GPIO_3.查看核心板原理图中的对应性i.MX6ULL的引脚为

2. 如何控制芯片手册中的引脚?

因为i.MX该系列的外设原理基本相同,因此在本系列文章中,请阅读外设原理i.MXRT详细分析1062中。

  • i.MX RT开发笔记-04 | 使用 IOMUXC 和 GPIO 点亮LED

2.1. IOMUXC选择引脚复用外设

(1)SW_MUX_CTL_PAD寄存器:用于设置引脚IOMUX,选择引脚的功能。 (2)SW_PAD_CTL_PAD寄存器:用于设置引脚属性,如驱动能力、上下拉电阻等。

2.2. GPIO外设

(1)配置GPIO引脚方向 (2)配置GPIO引脚电平

2.3. 外设时钟使能

CCM模块的CCM Clock Gating Register1寄存器(CCM_CCGR1): 其中CG13用来控制GPIO1外设时钟:

二、地址映射——MMU

在MCU寄存器可以通过绝对地址直接访问,但是i.MX6ULL是Cortex-A7内核,带有MMU,事情似乎很糟糕。

1. 地址映射

MMU全称Memory Manage Unit,内存管理单元。MMU主要功能如下:

  • 从虚拟空间到物理空间的映射完成
  • 内存保护、存储器访问权限、虚拟存储空间缓冲特性

对于32位处理器,虚拟地址(VA,Virual Address)的范围是 2 3 2 = 4 G B 2^32=4GB 232=4GB,而,如同:

Linux内核启动时会初始化MMU,设置内存映射,

2. 地址映射之间的转换

我们只知道寄存器的物理地址,CPU访问时需要虚拟地址,Linux内核为我们提供了地址之间的转换函数。

(1)

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

#define ioremap(cookie,size) __arm_ioremap((cookie), (size), MT_DEVICE) 

__arm_ioremap函数定义在arch/arm/mm/ioremap.cw文件中:

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

该函数的参数作用如下:

  • phys_addr:要映射的物理起始地址
  • size:要映射的内存空间大小
  • mtype:ioremap的类型,可以选择 MT_DEVICE、 MT_DEVICE_NONSHARED、MT_DEVICE_CACHED 和 MT_DEVICE_WC

该函数的返回值为映射后的虚拟空间首地址。

eg. 获取 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);

(2)

iounmap函数用来释放ioremap函数所做的映射,同样在文件arch/arm/include/asm/io.h中:

#define iounmap __arm_iounmap

``函数定义如下:

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

该函数只有一个参数,io_addr表示要取消映射的虚拟地址空间首地址。

eg. 取消 I.MX6ULL 的 IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03 寄存器对虚拟地址映射:

iounmap(SW_MUX_GPIO1_IO03);

3. 虚拟内存访问函数

使用ioremap函数将寄存器的物理地址映射到虚拟地址之后,其实可以通过指针直接访问这些内存,但是Linux内核不推荐这么做,而是

这些函数在arch/arm/include/asm/io.h中声明。

(1)读操作函数

#define readb(c) ({ u8 __v = readb_relaxed(c); __iormb(); __v; })
#define readw(c) ({ u16 __v = readw_relaxed(c); __iormb(); __v; })
#define readl(c) ({ u32 __v = readl_relaxed(c); __iormb(); __v; })

这些函数的底层实现函数如下:

static inline u8 __raw_readb(const volatile void __iomem *addr);
static inline u16 __raw_readw(const volatile void __iomem *addr);
static inline u32 __raw_readl(const volatile void __iomem *addr);

(2)写操作函数

#define writeb(v,c) ({ __iowmb(); writeb_relaxed(v,c); })
#define writew(v,c) ({ __iowmb(); writew_relaxed(v,c); })
#define writel(v,c) ({ __iowmb(); writel_relaxed(v,c); })

这些函数的底层实现函数如下:

static inline void __raw_writeb(u8 val, volatile void __iomem *addr);
static inline void __raw_writew(u16 val, volatile void __iomem *addr);
static inline void __raw_writel(u32 val, volatile void __iomem *addr);

四、设备驱动框架如何传递数据

用户应用程序运行在用户态,驱动程序运行在内核态。当应用程序中调用write向驱动程序写入数据时,驱动程序如何get到数据呢?

1. 参数传递

在驱动程序中,我们编写的write函数如下:

static ssize_t led_write(struct file *fp, const char __user *buf, size_t len, loff_t *off)
{ 
        
    return 0;
}

这其中的参数,就用来在调用时,接收应用程序传递下来的数据:

  • fp:文件描述符,表示打开的设备文件描述符
  • buf:要写给设备的数据
  • len:要写入的数据长度
  • off:相对于文件首地址的偏移

返回值是一个 ssize_t 类型,用来返回成功写入的字符数,如果写入失败则返回错误码(通常为负值)。

2. 数据拷贝

我们可以直接用指针访问应用程序传下来的buf,但在Linux中推荐进行一次数据拷贝。

定义在文件arch/arm/include/asm/uaccess.h中。

(1)copy_from_user:从用户空间拷贝数据。

static inline unsigned long __must_check copy_from_user(void *to, const void __user *from, unsigned long n)
{ 
        
	if (access_ok(VERIFY_READ, from, n))
		n = __copy_from_user(to, from, n);
	else /* security hole - plug it */
		memset(to, 0, n);
	return n;
}

(2)copy_to_user:拷贝数据到用户空间。

static inline unsigned long __must_check copy_to_user(void __user *to, const void *from, unsigned long n)
{ 
        
	if (access_ok(VERIFY_WRITE, to, n))
		n = __copy_to_user(to, from, n);
	return n;
}

五、点亮LED

1、 编写驱动代码

#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <asm/uaccess.h>
#include <asm/io.h>

#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 dev_t led_num;
static struct cdev *led_cdev;
static struct class *led_class;
static struct device *led0;

static void __iomem *iMX6ULL_CCM_CCGR1;
static void __iomem *iMX6ULL_SW_MUX_GPIO1_IO03;
static void __iomem *iMX6ULL_SW_PAD_GPIO1_IO03;
static void __iomem *iMX6ULL_GPIO_GDIR;
static void __iomem *iMX6ULL_GPIO1_DR;

/** * @brief LED板级初始化 */
static int led_board_init(void)
{ 
        
    u32 val;

    // 设置寄存器地址映射
    iMX6ULL_CCM_CCGR1 = ioremap(CCM_CCGR1_BASE, 4);
    iMX6ULL_SW_MUX_GPIO1_IO03 = ioremap(SW_MUX_GPIO1_IO03_BASE, 4);
    iMX6ULL_SW_PAD_GPIO1_IO03 = ioremap(SW_PAD_GPIO1_IO03_BASE, 4);
    iMX6ULL_GPIO_GDIR = ioremap(GPIO1_GDIR_BASE, 4);
    iMX6ULL_GPIO1_DR = ioremap(GPIO1_DR_BASE, 4);

    // 使能外设时钟
    val = readl(iMX6ULL_CCM_CCGR1);
    val &= ~(3 << 26);
    val |= (3 << 26);
    writel(val, iMX6ULL_CCM_CCGR1);

    // 设置IOMUXC引脚复用和引脚属性
    writel(5, iMX6ULL_SW_MUX_GPIO1_IO03);
    writel(0x10B0, iMX6ULL_SW_PAD_GPIO1_IO03);

    // 设置GPIO引脚方向
    val = readl(iMX6ULL_GPIO_GDIR);
    val &= ~(1 << 3);
    val |= (1 << 3);
    writel(val, iMX6ULL_GPIO_GDIR);

    // 设置GPIO输出高电平,默认关闭LED
    val = readl(iMX6ULL_GPIO1_DR);
    val |= (1 << 3);
    writel(val, iMX6ULL_GPIO1_DR);

    return 0;
}

/** * @brief LED板级释放 */
static void led_board_deinit(void)
{ 
        
    // 取消寄存器地址映射
    iounmap(iMX6ULL_CCM_CCGR1);
    iounmap(iMX6ULL_SW_MUX_GPIO1_IO03);
    iounmap(iMX6ULL_SW_PAD_GPIO1_IO03);
    iounmap(iMX6ULL_GPIO_GDIR);
    iounmap(iMX6ULL_GPIO1_DR);
}

/** * @brief LED板级释放 */
static void led_board_ctrl(int status)
{ 
        
    u32 val;

    if (status == 1) { 
        
        // 打开LED
        val = readl(iMX6ULL_GPIO1_DR);
        val &= ~(1 << 3);
        writel(val, iMX6ULL_GPIO1_DR);
    } else { 
        
        // 关闭LED
        val = readl(iMX6ULL_GPIO1_DR);
        val |= (1 << 3);
        writel(val, iMX6ULL_GPIO1_DR);
    }
}

static int led_open(struct inode *node, struct file *fp)
{ 
        
    return 0;
}

static ssize_t led_read(struct file *fp, char __user *buf, size_t len, loff_t *off)
{ 
        
    return 0;
}

static ssize_t led_write(struct file *fp, const char __user *buf, size_t len, loff_t *off)
{ 
        
    int ret;
    unsigned char data_buf[1];
    unsigned char led_status;

    // 拷贝用户传入数据
    ret = copy_from_user(data_buf, buf, 1);
    if (ret < 0) { 
        
        printk("led write failed!\n");
        return -EFAULT;
    }

    // 控制LED
    led_status = data_buf[0];
    if (led_status == 0) { 
        
        led_board_ctrl(0);
    } else if (led_status == 1){ 
        
        led_board_ctrl(1);
    }

    return 0;
}

static int led_close(struct inode *node, struct file *fp)
{ 
        

    return 0;
}

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

static int __init led_module_init(void)
{ 
        
    int ret;

    ret = led_board_init();
    if (ret != 0) { 
        
        printk(KERN_WARNING"led_board_init failed!\n");
        return -1;
    }

    // 1. 申请设备号
    ret = alloc_chrdev_region(&led_num, 0, 1, "led");
    if (ret != 0) { 
        
        printk(KERN_WARNING"alloc_chrdev_region failed!\n");
        return -1;
    }

    // 2. 申请cdev
    led_cdev = cdev_alloc();
    if (!led_cdev) { 
        
        printk(KERN_WARNING"cdev_alloc failed!\n");
        return -1;
    }

    // 3. 初始化cdev
    led_cdev->owner = THIS_MODULE;
    led_cdev->ops = &led_fops;

    // 4. 注册cdev
    ret = cdev_add(led_cdev, led_num, 1);
    if (ret != 0) { 
        
        printk(KERN_WARNING"cdev_add failed!\n");
        return -1;
    }

    // 5. 创建设备类
    led_class = class_create(THIS_MODULE, "led_class");
    if (!led_class) { 
        
        printk(KERN_WARNING"class_create failed!\n");
        return -1;
    }

    // 6. 创建设备节点
    led0 = device_create(led_class, NULL, led_num, NULL, "led0");
    if (IS_ERR(led0)) { 
        
        printk(KERN_WARNING"device_create failed!\n");
        return -1;
    }

    return 0;
}

static void __exit led_module_exit(void)
{ 
        
    led_board_deinit();

    // 1. 删除设备节点
    device_destroy(led_class, led_num);

    // 2. 删除设备类
    class_destroy(led_class);

    // 3. 删除cdev
    cdev_del(led_cdev);

    // 4. 释放设备号
    unregister_chrdev_region(led_num, 1);
}

module_init(led_module_init);
module_exit(led_module_exit);

MODULE_AUTHOR("mculover666");
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("a led demo");

2. 编译

KERNEL_DIR = /home/mculover666/imx6ull/kernel/linux-imx6ull
obj-m := led.o

build: kernel_modules

kernel_modules:
	$(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) modules

clean:
	$(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) clean

3. 编写测试程序

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>

int main(int argc, char *argv[])
{ 
        
    int fd;
    int ret;
    char *filename = NULL;
    unsigned char data_buf[1];

    // 检查参数
    if (argc != 3) { 
        
        printf("usage: ./test_led [device] [led status]\n");
    }

    // 打开设备文件
    filename = argv[1];
    fd = open(filename, O_RDWR);
    if (fd < 0) { 
        
        printf("open %s error!\n", filename);
        return 0;
    }

    // 写文件
    data_buf[0] = atoi(argv[2]);

    ret = write(fd, data_buf, sizeof(data_buf));
    if (ret < 0) { 
        
        printf("write %s error!\n", data_buf);
    }

    // 关闭文件
    close(fd);

    return 0;
}

编译:

arm-linux-gnueabihf-gcc test_led.c -o test_led

4. 测试

在板子上运行,加载驱动模块:

insmod led.ko

打开led灯:

./test_led /dev/led0 1

关闭led灯:

./test_led /dev/led0 0

标签: ddr4驱动电阻

锐单商城拥有海量元器件数据手册IC替代型号,打造 电子元器件IC百科大全!

锐单商城 - 一站式电子元器件采购平台