基于Myir的i.MX6ULL核心板开发简介

米尔公司(Myir)推出的MYC-Y6ULY2核心板,是基于NXP高效能i.MX6ULL处理器的一款入门级、高性价比的嵌入式平台,适用于开发各类普及型嵌入式Linux应用系统。该核心板内置了网络芯片,在外围电路设计上比较方便。目前市场上有NAND和EMMC两种版本,可根据具体需求选择不同容量的核心板。

i.MX6ULL处理器的主要特性如下表所示。

11

MYC-Y6ULY2核心板的系统框图如下所示。

11

MYC-Y6ULY2核心板和底板采用1.0mm间距的邮票孔相连方式,其顶视图如下所示。

111

本例使用的型号为MYC-Y6ULY2-256N256D-50-C的核心板,为NANDFLASH版本,其RAM和FLASH容量均为256M,具体规格参数如下表所示。

11

核心板的引脚功能介绍如下表所示。

111

i.MX6UL和i.MX6ULL处理器启动时会首先执行芯片内部Boot ROM中的程序。Boot ROM会根据BOOT_MODE寄存器、eFUSEs、启动相关的GPIO等状态来决定下一步执行动作。

BOOT_MODE寄存器的值取决于初始上电阶段BOOT_MODE0和BOOT_MODE1两个管脚的电平,以此决定CPU的启动模式,具体对应如下表所示。

111

通过前面的引脚图中可知,在核心板内部已经对BOOT_MODE0和BOOT_MODE1管脚通过10K电阻上拉(即上图中的保留值),当需要把这两个管脚设置为低电平时,可用1K电阻下拉至地。

在研发阶段,通常将CPU设置为internal Boot模式,在此模式下CPU在上电复位时会读取处理器LCD_DATA0-DATA23管脚的电平状态来决定启动的设备。在MYC-Y6ULY2核心板内部已经对LCD_DATA0-DATA23做了相应的处理,底板设计时只需对以下两个管脚做配置即可。

下表是基于eMMC版本核心板启动位配置。

111

下表是基于NANDFLASH版本核心板启动位配置,即本例的配置方式。

111

MYC-Y6ULY2核心板将i.MX6UL/i.MX6ULL芯片的所有IO管脚全部引出,除了ENET1占用的管脚以及Nand Flash/eMMC占用的管脚,其它管脚可以基于芯片支持的功能由用户自由分配相应的信号。 

MYC-Y6ULY2-256N256D-50-C核心板上基于NANDFLASH的,默认情况下,它被划分为5个分区,可在开发板上通过命令“cat /proc/mtd”查看,具体如下图所示。

1553657-20260701214352155-831411823

从上图中可以看出,mtd3分区就是专门用来存储设备树的,修改后的设备树二进制文件需要烧写到mtd3分区中,在核心板上使用以下命令来进行。

1、flash_erase /dev/mtd3 0 0。此命令先擦除mtd3分区的内容。

2、nandwrite -p /dev/mtd3 ./*.dtb。此命令用来向mtd3分区烧写dtb文件。dtb文件可以通过nfs方式获取,其文件名无关紧要。

在写入后为确保已实际写入NANDFLASH中,可以执行一下同步sync命令。另外,也可以使用相同的方法来烧写内核和根文件系统。

目前核心板上运行的Linux内核版本为5.10.9。其内核源代码可通过网址下载。解压后在源码目录直接执行build.sh脚本进行编译。

下面讨论一下与设备树有关的几个文件,这些文件都位于源码目录arch/arm/boot/dts/下。

1、imx6ully2-pinfunc.h及imx6ully2-pinfunc-snvs.h。

2、imx6ull-y2.dtsi及imx6ul-g2.dtsi。

3、myb-imx6ull-14x14-base.dts及myb-imx6ul-14x14.dtsi。

4、myd-y6ull-gpmi-weim.dts。

最终的设备树是myd-y6ull-gpmi-weim.dts,它也是编译后生成的设备树(名称为myd-y6ull-gpmi-weim.dtb),在该设备树中有&iomuxc节点,可进行GPIO端口(除5以外)的pinctrl子系统配置。myd-y6ull-gpmi-weim.dts包含了上一级设备树myb-imx6ull-14x14-base.dts,在该设备树中有&iomuxc_snvs节点,可进行GPIO5端口的pinctrl子系统配置。myb-imx6ull-14x14-base.dts包含了上一级设备树imx6ull-y2.dtsi和myb-imx6ul-14x14.dtsi,在imx6ull-y2.dtsi中包含有根节点及soc节点,可进行gpio子系统的配置。imx6ull-y2.dtsi包含了上一级设备树imx6ul-g2.dtsi和两个头文件imx6ully2-pinfunc.h及imx6ully2-pinfunc-snvs.h,在这些头文件中可以查到pinctrl配置需要的端口名称。

综上所述,在myd-y6ull-gpmi-weim.dts和myb-imx6ull-14x14-base.dts中配置pinctrl子系统,所需端口名称在imx6ully2-pinfunc.h及imx6ully2-pinfunc-snvs.h中查找。然后在imx6ull-y2.dtsi中配置gpio子系统。

在默认设备树配置下,GPIO5_0被用作key输入,GPIO5_1被用作系统心跳led指示(在myb-imx6ul-14x14.dtsi中配置)。经过测试,空闲的IO端口有:GPIO1_5、GPIO1_9、GPIO4_14、GPIO5_2\3\4\7\8等8个引脚。也就是说,这些引脚没有被系统或其他应用使用,可以用来做为输入/出引脚。

下面看一下在设备树文件myb-imx6ul-14x14.dtsi中描述的系统心跳led灯节点配置,如下图所示。

111

从上述配置中可以看到,默认的系统led使用的是引脚gpio5_1。相应的pinctrl配置在文件myb-imx6ul-14x14.dtsi的&iomuxc子节点中,具体如下所示。

111

假设要把它改到gpio5_4引脚上,只需要把节点leds中的gpios部分改为“gpios = <&gpio5 4 GPIO_ACTICE_LOW>”,然后把pinctrl中的fsl,pins改为MX6UL_PAD_SNVS_TAMPER4_GPIO5_IO04 0x1b0b0即可。然后在源码根目录下执行“sudo make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- dtbs”进行设备树的编译。成功后把文件myd-y6ull-gpmi-weim.dtb拷贝到NFS的挂载目录下,然后在开发板端依次执行以下命令:(1)flash_erase /dev/mtd3 0 0;(2)nandwrite -p /dev/mtd3 /mnt/myd-y6ull-gpmi-weim.dtb;(3)sync。然后重启开发板,启动后就可看到系统的心跳led灯已经换到了gpio5_4引脚上了,整个过程不需要更改驱动程序。

最后来看一个按键的中断线程实例。假设按键接在gpio5_3,默认为高电平,按下为低电平。

先配置设备树,先打开文件myb-imx6ull-14x14-base.dts,在&iomuxc_snvs节点下,输入以下内容来配置pinctrl。

        pinctrl_button: buttongrp {
                fsl,pins = <
                                MX6ULL_PAD_SNVS_TAMPER3__GPIO5_IO03        0x10b0
                        >;
                };

然后打开文件imx6ull-y2.dtsi,在根节点下输入以下内容来配置gpio子系统。

                my_button {
                        compatible = "my,button";
                        gpios = <&gpio5 3 GPIO_ACTIVE_LOW>;   //低电平有效
                        pinctrl-names = "default";
                        pinctrl-0 = <&pinctrl_button>;
                };

然后是驱动程序,名称为btn.c。

#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/platform_device.h>
#include <linux/mod_devicetable.h>
#include <linux/interrupt.h>
#include <linux/gpio/consumer.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/wait.h>
#include <linux/atomic.h>
#include <linux/slab.h>
#include <linux/delay.h>
#define DEVICE_NAME "button"           //定义设备名称
#define CLASS_NAME  "my_button_class"  //定义类名称
//以下定义一个btn的私有结构体
struct my_button {
    dev_t dev_id;                      //定义设备号
    struct cdev btn_cdev;              //定义字符型结构体
    struct device *device_button;      //创建的设备
    struct gpio_desc *gpio;            //定义端口描述符
    int irq;                           //定义中断号
    wait_queue_head_t wq;              //定义等待队列
    atomic_t available;                //定义原子变量    
};
static struct class *btn_class;        //定义类结构体
//实现open函数,为file_oprations结构体成员函数
static int button_open(struct inode *inode, struct file *filp)
{
    struct my_button *btn = container_of(inode->i_cdev, struct my_button, btn_cdev);
    filp->private_data = btn;
    return 0;
}
//实现read函数,为file_oprations结构体成员函数
static ssize_t button_read(struct file *filp, char __user *buf, size_t count, loff_t *pos)
{
    struct my_button *btn = filp->private_data;
    int ret, val=1;
    ret = wait_event_interruptible(btn->wq, atomic_read(&btn->available));
    if (ret)
        return ret;
    //已确认有事件,清除标志并返回“按下”
    if (put_user(val, (int __user *)buf))
        return -EFAULT;
    atomic_set(&btn->available, 0);
    return sizeof(val);
}
//实现release函数,为file_oprations结构体函数
static int button_release(struct inode *inode, struct file *filp)
{
    return 0;
}
//填充一个file_oprations类型的结构体,名为button_fops,包含上述声明的成员函数
static const struct file_operations button_fops = {
    .owner   = THIS_MODULE,
    .open    = button_open,
    .read    = button_read,
    .release = button_release,
};
//中断线程处理函数
static irqreturn_t button_thread(int irq, void *dev_id)
{
    struct my_button *btn = dev_id;
    int state;
    msleep(20);        //延时20ms消抖
    //软件(或硬件)消抖后电平已稳定,直接读取逻辑值
    state = gpiod_get_value(btn->gpio);
    if (state)
    {
        atomic_set(&btn->available, 1);
        wake_up_interruptible(&btn->wq);
    }
    return IRQ_HANDLED;
}
/*----------------平台驱动函数集-----------------*/
static int button_probe(struct platform_device *pdev)
{
    int ret;
    struct my_button *btn;
    //以下动态申请led结构体大小的内存
    btn = devm_kzalloc(&pdev->dev, sizeof(*btn), GFP_KERNEL);
    if (!btn)
        return -ENOMEM;
    //获取GPIO并配置为输入(匹配无前缀"gpios"属性)
    btn->gpio = devm_gpiod_get(&pdev->dev, NULL, GPIOD_IN);
    if (IS_ERR(btn->gpio)) {
        ret = PTR_ERR(btn->gpio);
        printk("Failed to get GPIO: %d\n", ret);
        return ret;
    }
    //硬件消抖:i.MX6ULL支持,最大约128ms,此处预设20ms
//    ret = gpiod_set_debounce(btn->gpio, 20000);
//    if (ret && ret != -ENOTSUPP) {
//        printk("Failed to set debounce: %d\n", ret);
//        return ret;
//    }
    //获取中断号
    btn->irq = gpiod_to_irq(btn->gpio);
    if (btn->irq < 0) {
        printk("Failed to get IRQ\n");
        return btn->irq;
    }
    //申请线程化中断(下降沿触发 + ONESHOT)
    ret = devm_request_threaded_irq(&pdev->dev, btn->irq, NULL, button_thread, IRQF_TRIGGER_FALLING | IRQF_ONESHOT, DEVICE_NAME, btn);
    if (ret) {
        printk("Failed to request IRQ\n");
        return ret;
    }
    atomic_set(&btn->available, 0);       //初始化原子计数值
    init_waitqueue_head(&btn->wq);        //初始化等待队列
    //下面申请主设备号
    ret = alloc_chrdev_region(&btn->dev_id, 0, 1, DEVICE_NAME);
    if (ret) {
        printk("alloc_chrdev_region failed\n");
        return ret;
    }
    btn->btn_cdev.owner = THIS_MODULE;
    //绑定前面声明的file_oprations类型的结构体到字符设备
    cdev_init(&btn->btn_cdev, &button_fops);
    //填充上面申请到的主设备号到字符设备
    ret = cdev_add(&btn->btn_cdev, btn->dev_id, 1);
    if (ret) {
        printk("cdev_add failed\n");
        goto err_region;
    }
    //创建一个设备节点
    btn->device_button = device_create(btn_class, NULL, btn->dev_id, NULL, DEVICE_NAME);
    if (IS_ERR(btn->device_button)) {
        ret = PTR_ERR(btn->device_button);
        printk("device_create failed\n");
        goto err_cdev;
    }
    //以下把btn数据信息存入在平台驱动结构体中pdev->dev->driver_data中,后面移除时会用到
    platform_set_drvdata(pdev, btn);
        printk("platform driver probed!\n");
    return 0;
//以下为出错返回
err_cdev:
    cdev_del(&btn->btn_cdev);
err_region:
    unregister_chrdev_region(btn->dev_id, 1);
    return ret;
}
//remove函数中,删除设备并释放设备号
static int button_remove(struct platform_device *pdev)
{
    //platform_get_drvdata,获取当前按键对应的结构体
    struct my_button *btn = platform_get_drvdata(pdev);
    unregister_chrdev_region(btn->dev_id, 1);        //释放主设备号
    cdev_del(&btn->btn_cdev);                        //删除字符设备
    device_destroy(btn_class, btn->dev_id);          //销毁设备节点
    printk("platform driver removed!\n");
    return 0;
}
//填充of_device_id结构体,名为button,用于指明匹配表
static const struct of_device_id button[] = {
    { .compatible = "my,button" },            //匹配内容
    { /* sentinel */ }
};
//以下填充一个platform_driver结构体
static struct platform_driver button_driver = {
    .probe  = button_probe,              //指定probe函数成员
    .remove = button_remove,             //指定remove函数成员
    .driver = {
        .name = "my_button",             //指定设备名称
        .owner = THIS_MODULE,
        .of_match_table = button,        //指定匹配表名称
    },
};
//以下定义模块的入口函数
static int __init button_init(void)
{
    btn_class = class_create(THIS_MODULE, CLASS_NAME);
    if (IS_ERR(btn_class))
        return PTR_ERR(btn_class);
    //注册一个platform驱动
    platform_driver_register(&button_driver);
    printk("button platform driver initted!\n");
    return 0;
}
//以下定义模块的出口函数
static void __exit button_exit(void)
{
    //释放一个platform驱动
    platform_driver_unregister(&button_driver);
    class_destroy(btn_class);        //销毁类
    printk("button platform driver exited!\n");
}
module_init(button_init);
module_exit(button_exit);
MODULE_LICENSE("GPL");
MODULE_INFO(intree,"Y");

下面是配套的Makefile,内容如下。

KERNEL_DIR=/opt/myir-imx-linux/
ARCH=arm
CROSS_COMPILE=arm-linux-gnueabihf-
export  ARCH  CROSS_COMPILE
obj-m := btn.o
all:
        $(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) modules
modules clean:
        $(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) clean

最后是应用程序,文件名为app.c。

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
int main(void)
{
    int fd, val;
    int press_count = 0;   // 应用程序自己维护的计数器
    fd = open("/dev/button", O_RDONLY);
    if (fd < 0) {
        perror("open");
        return 1;
    }
    printf("Waiting for button press...\n");
    while (1) {
        if (read(fd, &val, sizeof(val)) == sizeof(val)) {
            if (val == 1) {
                press_count++;
                printf("Button pressed! total count = %d\n", press_count);
            }
        }
    }
    close(fd);
    return 0;
}

在开发板上的运行结果如下图所示。

111

在上述驱动程序中,使用了现代Linux的一些特点,比如使用devm的系统托管方式(无需手动释放),而没有使用传统方式(需要手动释放)。其次,使用了中断线程处理方式,而没有使用传统的中断处理方式。传统中断处理程序中要考虑执行时间不能太长,而线程化中断则是把整个中断处理放到内核线程,由调度器管理,在其中还可以进入休眠,更易控制实时性,大有取代tasklet的趋势。第三,在驱动代码中可以使用i.mx6ull支持的硬件消抖方式(代码中被注释掉的gpiod_set_debounce(btn->gpio, 20000)一句),但实际使用下来,发现硬件消抖的效果并不理想, 所以本例还是在中断线程中使用休眠20ms(代码中的msleep(20)一句)的方式来进行消抖(注意不要软硬件消抖同时使用),实际效果很好。由于是在线程中休眠,并不影响效率。第四,本例使用了中断休眠的方式,即在没有按键按下时,驱动程序在read函数中通过调用wait_event_interruptible函数进行休眠(即阻塞于此),当有按键按下时,在中断线程处理函数中通过调用wake_up_interruptible函数进行唤醒,程序从阻塞的地方继续执行。这使得应用程序在执行read时,若没有按键按下,也会处于阻塞休眠状态,直到按键按下再读取驱动提供的键值。通过这种方式,中断可以大大减轻CPU的负担,提升效率。第五,本例中的中断触发方式由驱动程序决定,并不在设备树中配置。

下面简要介绍一下驱动代码中用到的相关API函数。

1、获取gpio引脚函数:struct gpio_desc *devm_gpiod_get(struct device *dev, const char *con_id, enum gpiod_flags flags),参数dev指定设备指针(即调用者)。参数con_id是gpio的连接ID,用于匹配设备树属性名(即前面提到的“-gpios”前的字符串),当该参数为NULL时,它会在设备树中查找不带前缀的 "gpios"或 "gpio"属性(虽然本例中使用了,但并不推荐)。参数flags是gpio的初始化标志,使用宏表示,一般有:GPIOD_IN(输入方向),GPIOD_OUT_LOW(输出方向,逻辑低电平),GPIOD_OUT_HIGH(输出方向,逻辑高电平),GPIOD_OUT_LOW_OPEN_DRAIN / GPIOD_OUT_HIGH_OPEN_DRAIN(开漏)等。该函数是 Linux内核gpio子系统中新式描述符API的核心函数,用于从设备树(或 ACPI/平台数据)中获取、申请并自动管理一个gpio引脚的生命周期。它可以取代传统的of_get_named_gpio + gpio_request + gpio_direction_*组合,让代码更简洁、更安全。

2、获取中断号函数:int gpiod_to_irq(const struct gpio_desc *desc),只有一个参数desc,是gpio引脚的描述符。

3、注册中断线程函数:int devm_request_threaded_irq(struct device *dev, unsigned int irq, irq_handler_t handler, irq_handler_t thread_fn, unsigned long flags, const char *name, void *dev_id),参数dev指定设备指针(即调用者)。参数irq是Linux中断号,由前面的gpiod_to_irq函数获取。参数handler是硬中断处理函数句柄,如果不使用可设置为NULL(比如本例)。参数thread_fn是线程化中断处理函数句柄,它在内核线程中执行,可睡眠、使用互斥锁等,本例就使用该模式。参数flags为中断标志位,用于配置触发方式、共享、oneshot等行为,其中参数oneshot是指在处理一个中断期间,保持该中断线为屏蔽状态,直到线程处理函数执行完毕再重新打开,一般在中断中都会使用。参数name为中断名称,它会显示在/proc/interrupts及ps中。参数dev_id为私有数据指针,会原样传递给handler及thread_fn,通常传设备结构体指针。

4、初始化等待队列头函数:void init_waitqueue_head(wait_queue_head_t *wq),只在一个参数wq,是指向需要初始化的等待队列头变量。该函数的作用是初始化一个等待队列头,使其成为可用的等待队列。在使用任何等待队列之前,必须调用此函数进行初始化。

5、中断休眠状态设置函数:int wait_event_interruptible(wait_queue_head_t wq, int condition),参数wq是前面已初始化的等待队列头。参数condition是唤醒条件,为非零时进程继续运行,为零时进程休眠。

6、中断唤醒函数:void wake_up_interruptible(wait_queue_head_t *wq),只有一个参数wq,同样是指向已初始化的等待队列头。

7、配置硬件去抖时间函数:int gpiod_set_debounce(struct gpio_desc *desc, unsigned int debounce_us),参数desc为gpio描述符。参数debounce_us为消抖时间,单位为微秒。

最后再强调一下,通过前缀devm申请的资源是由系统内核托管的,不需要在退出时手动释放。

posted @ 2026-07-08 22:12  fxzq  阅读(44)  评论(0)    收藏  举报