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

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

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

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

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

i.MX6UL和i.MX6ULL处理器启动时会首先执行芯片内部Boot ROM中的程序。Boot ROM会根据BOOT_MODE寄存器、eFUSEs、启动相关的GPIO等状态来决定下一步执行动作。
BOOT_MODE寄存器的值取决于初始上电阶段BOOT_MODE0和BOOT_MODE1两个管脚的电平,以此决定CPU的启动模式,具体对应如下表所示。

通过前面的引脚图中可知,在核心板内部已经对BOOT_MODE0和BOOT_MODE1管脚通过10K电阻上拉(即上图中的保留值),当需要把这两个管脚设置为低电平时,可用1K电阻下拉至地。
在研发阶段,通常将CPU设置为internal Boot模式,在此模式下CPU在上电复位时会读取处理器LCD_DATA0-DATA23管脚的电平状态来决定启动的设备。在MYC-Y6ULY2核心板内部已经对LCD_DATA0-DATA23做了相应的处理,底板设计时只需对以下两个管脚做配置即可。
下表是基于eMMC版本核心板启动位配置。

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

MYC-Y6ULY2核心板将i.MX6UL/i.MX6ULL芯片的所有IO管脚全部引出,除了ENET1占用的管脚以及Nand Flash/eMMC占用的管脚,其它管脚可以基于芯片支持的功能由用户自由分配相应的信号。
MYC-Y6ULY2-256N256D-50-C核心板上基于NANDFLASH的,默认情况下,它被划分为5个分区,可在开发板上通过命令“cat /proc/mtd”查看,具体如下图所示。

从上图中可以看出,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灯节点配置,如下图所示。

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

假设要把它改到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; }
在开发板上的运行结果如下图所示。

在上述驱动程序中,使用了现代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申请的资源是由系统内核托管的,不需要在退出时手动释放。

浙公网安备 33010602011771号