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

Posted Mculover666

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了i.MX6ULL驱动开发 | 03-基于字符设备驱动框架点亮LED相关的知识,希望对你有一定的参考价值。

一、硬件原理

1. 看原理图确定硬件连接

i.MX6ULL开发板上板载了一个用户LED,如图:

LED0连接到GPIO_3,在核心板原理图上查看对应到i.MX6ULL的引脚为GPIO1_IO03

2. 看芯片手册如何控制引脚

因为i.MX系列的外设原理基本一样,所以在本系列文章中,关于外设原理请阅读之前i.MXRT1062中的详细分析。

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,而本文所使用的开发板板载512MB的DDR,这512MB就是物理内存,经过MMU可以将其映射到4GB的虚拟空间,如同:


Linux内核启动的时候会初始化MMU,设置内存映射,这之后CPU访问的都是虚拟地址

2. 地址映射之间的转换

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

(1)ioremap函数

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函数

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

以上是关于i.MX6ULL驱动开发 | 03-基于字符设备驱动框架点亮LED的主要内容,如果未能解决你的问题,请参考以下文章

i.MX6ULL驱动开发 | 08 -基于pinctrl子系统和gpio子系统点亮LED

i.MX6ULL驱动开发1——字符设备开发模板

i.MX6ULL驱动开发 | 34 - 基于SPI框架驱动spi lcd(st7789)

i.MX6ULL驱动开发 | 02-字符设备驱动框架

i.MX6ULL驱动开发 | 36 - 注册spilcd为framebuffer设备并使用lvgl测试

i.MX6ULL驱动开发 | 36 - 注册spilcd为framebuffer设备并使用lvgl测试