点灯大师--点个正点原子阿尔法开发板的灯
创始人
2024-05-30 13:48:29

点灯大师–点个正点原子阿尔法开发板的灯


文章目录

  • 点灯大师--点个正点原子阿尔法开发板的灯
  • 正点原子阿尔法开发板点灯
    • 1、使能 GPIO1 时钟
    • 2、设置 GPIO1_IO03 的复用功能
    • 3、配置 GPIO1_IO03
    • 4、设置 GPIO
    • 5、控制 GPIO 的输出电平
  • 五种点灯的方法
    • 1.在一个驱动文件中实现寄存器初始化和寄存器操作
    • 3.pinctl和gpio子系统描述硬件信息
      • 3.1修改设备树文件
        • 添加 pinctrl 节点
        • 添加 LED 设备节点
        • 3、检查 PIN 是否被其他外设使用
      • 3.2驱动代码
    • 4.platform驱动
    • 5.设备树platform
      • 设备树
      • 驱动代码
  • 通用GPIO引脚操作方法
    • 1 GPIO 模块一般结构
    • 2 GPIO 寄存器操作
    • 3.imx6ull寄存器操作
      • 3.1IMX6ULL 的 GPIO 模块结构
      • 3.2CCM 用于设置是否向 GPIO 模块提供时钟
      • 3.3 IOMUXC:引脚的模式(Mode、功能)
      • 3.4 GPIO 模块内部
      • 3.5 读 GPIO
      • 3.6 写 GPIO


正点原子阿尔法开发板点灯

1、使能 GPIO1 时钟

GPIO1 的时钟由 CCM_CCGR1 的 bit27 和 bit26 这两个位控制,将这两个位都设置位 11 即可。本教程所有例程已经将 I.MX6U 的所有外设时钟都已经打开了,因此这一步可以不用做。
在这里插入图片描述

2、设置 GPIO1_IO03 的复用功能

找到 GPIO1_IO03 的复用寄存器“IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03”的地址为0X020E0068,然后设置此寄存器,将 GPIO1_IO03 这个 IO 复用为 GPIO 功能,也就是 ALT5。
在这里插入图片描述

3、配置 GPIO1_IO03

找到 GPIO1_IO03 的配置寄存器“IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO03”的地址为0X020E02F4,根据实际使用情况,配置此寄存器。
在这里插入图片描述

4、设置 GPIO

我们已经将 GPIO1_IO03 复用为了 GPIO 功能,所以我们需要配置 GPIO。找到 GPIO3 对应的 GPIO 组寄存器地址,在《IMX6ULL 参考手册》的 1357 页,如图 8.3.1 所示:
在这里插入图片描述

5、控制 GPIO 的输出电平

经过前面几步, GPIO1_IO03 已经配置好了,只需要向 GPIO1_DR 寄存器的 bit3 写入 0 即可控制 GPIO1_IO03 输出低电平,打开 LED,向 bit3 写入 1 可控制 GPIO1_IO03 输出高电平,关闭 LED。
在这里插入图片描述

五种点灯的方法

1.在一个驱动文件中实现寄存器初始化和寄存器操作

led.c

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include #define LED_MAJOR       200     /* 主设备号 */
#define LED_NAME        "led"   /* 设备名字 */#define LEDOFF  0               /* 关灯 */
#define LEDON   1               /* 开灯 *//* 寄存器物理地址 */
#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;/** @description     : LED打开/关闭* @param - sta     : LEDON(0) 打开LED,LEDOFF(1) 关闭LED* @return          : 无*/
void led_switch(u8 sta)
{u32 val = 0;if(sta == LEDON) {val = readl(GPIO1_DR);val &= ~(1 << 3);   writel(val, GPIO1_DR);}else if(sta == LEDOFF) {val = readl(GPIO1_DR);val|= (1 << 3); writel(val, GPIO1_DR);}   
}/** @description     : 打开设备* @param - inode   : 传递给驱动的inode* @param - filp    : 设备文件,file结构体有个叫做private_data的成员变量*                    一般在open的时候将private_data指向设备结构体。* @return          : 0 成功;其他 失败*/
static int led_open(struct inode *inode, struct file *filp)
{return 0;
}/** @description     : 从设备读取数据 * @param - filp    : 要打开的设备文件(文件描述符)* @param - buf     : 返回给用户空间的数据缓冲区* @param - cnt     : 要读取的数据长度* @param - offt    : 相对于文件首地址的偏移* @return          : 读取的字节数,如果为负值,表示读取失败*/
static ssize_t led_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{return 0;
}/** @description     : 向设备写数据 * @param - filp    : 设备文件,表示打开的文件描述符* @param - buf     : 要写给设备写入的数据* @param - cnt     : 要写入的数据长度* @param - offt    : 相对于文件首地址的偏移* @return          : 写入的字节数,如果为负值,表示写入失败*/
static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{int retvalue;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];       /* 获取状态值 */if(ledstat == LEDON) {  led_switch(LEDON);      /* 打开LED灯 */} else if(ledstat == LEDOFF) {led_switch(LEDOFF); /* 关闭LED灯 */}return 0;
}/** @description     : 关闭/释放设备* @param - filp    : 要关闭的设备文件(文件描述符)* @return          : 0 成功;其他 失败*/
static int led_release(struct inode *inode, struct file *filp)
{return 0;
}/* 设备操作函数 */
static struct file_operations led_fops = {.owner = THIS_MODULE,.open = led_open,.read = led_read,.write = led_write,.release =  led_release,
};/** @description : 驱动出口函数* @param       : 无* @return      : 无*/
static int __init led_init(void)
{int retvalue = 0;u32 val = 0;/* 初始化LED *//* 1、寄存器地址映射 */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);/* 2、使能GPIO1时钟 */val = readl(IMX6U_CCM_CCGR1);val &= ~(3 << 26);  /* 清楚以前的设置 */val |= (3 << 26);   /* 设置新值 */writel(val, IMX6U_CCM_CCGR1);/* 3、设置GPIO1_IO03的复用功能,将其复用为*    GPIO1_IO03,最后设置IO属性。*/writel(5, SW_MUX_GPIO1_IO03);/*寄存器SW_PAD_GPIO1_IO03设置IO属性*bit 16:0 HYS关闭*bit [15:14]: 00 默认下拉*bit [13]: 0 kepper功能*bit [12]: 1 pull/keeper使能*bit [11]: 0 关闭开路输出*bit [7:6]: 10 速度100Mhz*bit [5:3]: 110 R0/6驱动能力*bit [0]: 0 低转换率*/writel(0x10B0, SW_PAD_GPIO1_IO03);/* 4、设置GPIO1_IO03为输出功能 */val = readl(GPIO1_GDIR);val &= ~(1 << 3);   /* 清除以前的设置 */val |= (1 << 3);    /* 设置为输出 */writel(val, GPIO1_GDIR);/* 5、默认关闭LED */val = readl(GPIO1_DR);val |= (1 << 3);    writel(val, GPIO1_DR);/* 6、注册字符设备驱动 */retvalue = register_chrdev(LED_MAJOR, LED_NAME, &led_fops);if(retvalue < 0){printk("register chrdev failed!\r\n");return -EIO;}return 0;
}/** @description : 驱动出口函数* @param       : 无* @return      : 无*/
static void __exit led_exit(void)
{/* 取消映射 */iounmap(IMX6U_CCM_CCGR1);iounmap(SW_MUX_GPIO1_IO03);iounmap(SW_PAD_GPIO1_IO03);iounmap(GPIO1_DR);iounmap(GPIO1_GDIR);/* 注销字符设备驱动 */unregister_chrdev(LED_MAJOR, LED_NAME);
}module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");

2.使用设备树描述硬件信息
在根节点“/”下创建一个名为“alphaled”的子节点,打开 imx6ull-alientek-emmc.dts 文件,在根节点“/”最后面输入如下所示内容:

alphaled {#address-cells = <1>;#size-cells = <1>;compatible = "atkalpha-led";status = "okay";reg = < 0X020C406C 0X04 /* CCM_CCGR1_BASE */0X020E0068 0X04 /* SW_MUX_GPIO1_IO03_BASE */0X020E02F4 0X04 /* SW_PAD_GPIO1_IO03_BASE */0X0209C000 0X04 /* GPIO1_DR_BASE */0X0209C004 0X04 >; /* GPIO1_GDIR_BASE */
};

led.c

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include #define DTSLED_CNT          1           /* 设备号个数 */
#define DTSLED_NAME         "dtsled"    /* 名字 */
#define LEDOFF                  0           /* 关灯 */
#define LEDON                   1           /* 开灯 *//* 映射后的寄存器虚拟地址指针 */
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;/* dtsled设备结构体 */
struct dtsled_dev{dev_t devid;            /* 设备号   */struct cdev cdev;       /* cdev     */struct class *class;        /* 类        */struct device *device;  /* 设备    */int major;              /* 主设备号   */int minor;              /* 次设备号   */struct device_node  *nd; /* 设备节点 */
};struct dtsled_dev dtsled;   /* led设备 *//** @description     : LED打开/关闭* @param - sta     : LEDON(0) 打开LED,LEDOFF(1) 关闭LED* @return          : 无*/
void led_switch(u8 sta)
{u32 val = 0;if(sta == LEDON) {val = readl(GPIO1_DR);val &= ~(1 << 3);   writel(val, GPIO1_DR);}else if(sta == LEDOFF) {val = readl(GPIO1_DR);val|= (1 << 3); writel(val, GPIO1_DR);}   
}/** @description     : 打开设备* @param - inode   : 传递给驱动的inode* @param - filp    : 设备文件,file结构体有个叫做private_data的成员变量*                    一般在open的时候将private_data指向设备结构体。* @return          : 0 成功;其他 失败*/
static int led_open(struct inode *inode, struct file *filp)
{filp->private_data = &dtsled; /* 设置私有数据 */return 0;
}/** @description     : 从设备读取数据 * @param - filp    : 要打开的设备文件(文件描述符)* @param - buf     : 返回给用户空间的数据缓冲区* @param - cnt     : 要读取的数据长度* @param - offt    : 相对于文件首地址的偏移* @return          : 读取的字节数,如果为负值,表示读取失败*/
static ssize_t led_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{return 0;
}/** @description     : 向设备写数据 * @param - filp    : 设备文件,表示打开的文件描述符* @param - buf     : 要写给设备写入的数据* @param - cnt     : 要写入的数据长度* @param - offt    : 相对于文件首地址的偏移* @return          : 写入的字节数,如果为负值,表示写入失败*/
static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{int retvalue;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];       /* 获取状态值 */if(ledstat == LEDON) {  led_switch(LEDON);      /* 打开LED灯 */} else if(ledstat == LEDOFF) {led_switch(LEDOFF); /* 关闭LED灯 */}return 0;
}/** @description     : 关闭/释放设备* @param - filp    : 要关闭的设备文件(文件描述符)* @return          : 0 成功;其他 失败*/
static int led_release(struct inode *inode, struct file *filp)
{return 0;
}/* 设备操作函数 */
static struct file_operations dtsled_fops = {.owner = THIS_MODULE,.open = led_open,.read = led_read,.write = led_write,.release =  led_release,
};/** @description : 驱动出口函数* @param       : 无* @return      : 无*/
static int __init led_init(void)
{u32 val = 0;int ret;u32 regdata[14];const char *str;struct property *proper;/* 获取设备树中的属性数据 *//* 1、获取设备节点:alphaled */dtsled.nd = of_find_node_by_path("/alphaled");if(dtsled.nd == NULL) {printk("alphaled node nost find!\r\n");return -EINVAL;} else {printk("alphaled node find!\r\n");}/* 2、获取compatible属性内容 */proper = of_find_property(dtsled.nd, "compatible", NULL);if(proper == NULL) {printk("compatible property find failed\r\n");} else {printk("compatible = %s\r\n", (char*)proper->value);}/* 3、获取status属性内容 */ret = of_property_read_string(dtsled.nd, "status", &str);if(ret < 0){printk("status read failed!\r\n");} else {printk("status = %s\r\n",str);}/* 4、获取reg属性内容 */ret = of_property_read_u32_array(dtsled.nd, "reg", regdata, 10);if(ret < 0) {printk("reg property read failed!\r\n");} else {u8 i = 0;printk("reg data:\r\n");for(i = 0; i < 10; i++)printk("%#X ", regdata[i]);printk("\r\n");}/* 初始化LED */
#if 0/* 1、寄存器地址映射 */IMX6U_CCM_CCGR1 = ioremap(regdata[0], regdata[1]);SW_MUX_GPIO1_IO03 = ioremap(regdata[2], regdata[3]);SW_PAD_GPIO1_IO03 = ioremap(regdata[4], regdata[5]);GPIO1_DR = ioremap(regdata[6], regdata[7]);GPIO1_GDIR = ioremap(regdata[8], regdata[9]);
#elseIMX6U_CCM_CCGR1 = of_iomap(dtsled.nd, 0);SW_MUX_GPIO1_IO03 = of_iomap(dtsled.nd, 1);SW_PAD_GPIO1_IO03 = of_iomap(dtsled.nd, 2);GPIO1_DR = of_iomap(dtsled.nd, 3);GPIO1_GDIR = of_iomap(dtsled.nd, 4);
#endif/* 2、使能GPIO1时钟 */val = readl(IMX6U_CCM_CCGR1);val &= ~(3 << 26);  /* 清楚以前的设置 */val |= (3 << 26);   /* 设置新值 */writel(val, IMX6U_CCM_CCGR1);/* 3、设置GPIO1_IO03的复用功能,将其复用为*    GPIO1_IO03,最后设置IO属性。*/writel(5, SW_MUX_GPIO1_IO03);/*寄存器SW_PAD_GPIO1_IO03设置IO属性*bit 16:0 HYS关闭*bit [15:14]: 00 默认下拉*bit [13]: 0 kepper功能*bit [12]: 1 pull/keeper使能*bit [11]: 0 关闭开路输出*bit [7:6]: 10 速度100Mhz*bit [5:3]: 110 R0/6驱动能力*bit [0]: 0 低转换率*/writel(0x10B0, SW_PAD_GPIO1_IO03);/* 4、设置GPIO1_IO03为输出功能 */val = readl(GPIO1_GDIR);val &= ~(1 << 3);   /* 清除以前的设置 */val |= (1 << 3);    /* 设置为输出 */writel(val, GPIO1_GDIR);/* 5、默认关闭LED */val = readl(GPIO1_DR);val |= (1 << 3);    writel(val, GPIO1_DR);/* 注册字符设备驱动 *//* 1、创建设备号 */if (dtsled.major) {     /*  定义了设备号 */dtsled.devid = MKDEV(dtsled.major, 0);register_chrdev_region(dtsled.devid, DTSLED_CNT, DTSLED_NAME);} else {                        /* 没有定义设备号 */alloc_chrdev_region(&dtsled.devid, 0, DTSLED_CNT, DTSLED_NAME); /* 申请设备号 */dtsled.major = MAJOR(dtsled.devid); /* 获取分配号的主设备号 */dtsled.minor = MINOR(dtsled.devid); /* 获取分配号的次设备号 */}printk("dtsled major=%d,minor=%d\r\n",dtsled.major, dtsled.minor);  /* 2、初始化cdev */dtsled.cdev.owner = THIS_MODULE;cdev_init(&dtsled.cdev, &dtsled_fops);/* 3、添加一个cdev */cdev_add(&dtsled.cdev, dtsled.devid, DTSLED_CNT);/* 4、创建类 */dtsled.class = class_create(THIS_MODULE, DTSLED_NAME);if (IS_ERR(dtsled.class)) {return PTR_ERR(dtsled.class);}/* 5、创建设备 */dtsled.device = device_create(dtsled.class, NULL, dtsled.devid, NULL, DTSLED_NAME);if (IS_ERR(dtsled.device)) {return PTR_ERR(dtsled.device);}return 0;
}/** @description : 驱动出口函数* @param       : 无* @return      : 无*/
static void __exit led_exit(void)
{/* 取消映射 */iounmap(IMX6U_CCM_CCGR1);iounmap(SW_MUX_GPIO1_IO03);iounmap(SW_PAD_GPIO1_IO03);iounmap(GPIO1_DR);iounmap(GPIO1_GDIR);/* 注销字符设备驱动 */cdev_del(&dtsled.cdev);/*  删除cdev */unregister_chrdev_region(dtsled.devid, DTSLED_CNT); /* 注销设备号 */device_destroy(dtsled.class, dtsled.devid);class_destroy(dtsled.class);
}module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");

3.pinctl和gpio子系统描述硬件信息

3.1修改设备树文件

添加 pinctrl 节点

I.MX6U-ALPHA 开发板上的 LED 灯使用了 GPIO1_IO03 这个 PIN,打开 imx6ull-alientekemmc.dts,在 iomuxc 节点的 imx6ul-evk 子节点下创建一个名为“pinctrl_led”的子节点,节点内容如下所示:

pinctrl_led: ledgrp {fsl,pins = ;
};

第 3 行将 GPIO1_IO03 这个 PIN 复用为 GPIO1_IO03,电气属性值为 0X10B0

添加 LED 设备节点

在根节点“/”下创建 LED 灯节点,节点名为“gpioled”,节点内容如下:

gpioled {#address-cells = <1>;#size-cells = <1>;compatible = "atkalpha-gpioled";pinctrl-names = "default";pinctrl-0 = <&pinctrl_led>;led-gpio = <&gpio1 3 GPIO_ACTIVE_LOW>;status = "okay";
};

第 6 行, pinctrl-0 属性设置 LED 灯所使用的 PIN 对应的 pinctrl 节点。
第 7 行, led-gpio 属性指定了 LED 灯所使用的 GPIO,在这里就是 GPIO1 的 IO03,低电平有效。稍后编写驱动程序的时候会获取 led-gpio 属性的内容来得到 GPIO 编号,因为 gpio 子系统的 API 操作函数需要 GPIO 编号。

3、检查 PIN 是否被其他外设使用

3.2驱动代码

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include #define GPIOLED_CNT         1           /* 设备号个数 */
#define GPIOLED_NAME        "gpioled"   /* 名字 */
#define LEDOFF              0           /* 关灯 */
#define LEDON               1           /* 开灯 *//* gpioled设备结构体 */
struct gpioled_dev{dev_t devid;            /* 设备号   */struct cdev cdev;       /* cdev     */struct class *class;    /* 类        */struct device *device;  /* 设备    */int major;              /* 主设备号   */int minor;              /* 次设备号   */struct device_node  *nd; /* 设备节点 */int led_gpio;           /* led所使用的GPIO编号        */
};struct gpioled_dev gpioled; /* led设备 *//** @description     : 打开设备* @param - inode   : 传递给驱动的inode* @param - filp    : 设备文件,file结构体有个叫做private_data的成员变量*                    一般在open的时候将private_data指向设备结构体。* @return          : 0 成功;其他 失败*/
static int led_open(struct inode *inode, struct file *filp)
{filp->private_data = &gpioled; /* 设置私有数据 */return 0;
}/** @description     : 从设备读取数据 * @param - filp    : 要打开的设备文件(文件描述符)* @param - buf     : 返回给用户空间的数据缓冲区* @param - cnt     : 要读取的数据长度* @param - offt    : 相对于文件首地址的偏移* @return          : 读取的字节数,如果为负值,表示读取失败*/
static ssize_t led_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{return 0;
}/** @description     : 向设备写数据 * @param - filp    : 设备文件,表示打开的文件描述符* @param - buf     : 要写给设备写入的数据* @param - cnt     : 要写入的数据长度* @param - offt    : 相对于文件首地址的偏移* @return          : 写入的字节数,如果为负值,表示写入失败*/
static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{int retvalue;unsigned char databuf[1];unsigned char ledstat;struct gpioled_dev *dev = filp->private_data;retvalue = copy_from_user(databuf, buf, cnt);if(retvalue < 0) {printk("kernel write failed!\r\n");return -EFAULT;}ledstat = databuf[0];       /* 获取状态值 */if(ledstat == LEDON) {  gpio_set_value(dev->led_gpio, 0);   /* 打开LED灯 */} else if(ledstat == LEDOFF) {gpio_set_value(dev->led_gpio, 1);   /* 关闭LED灯 */}return 0;
}/** @description     : 关闭/释放设备* @param - filp    : 要关闭的设备文件(文件描述符)* @return          : 0 成功;其他 失败*/
static int led_release(struct inode *inode, struct file *filp)
{return 0;
}/* 设备操作函数 */
static struct file_operations gpioled_fops = {.owner = THIS_MODULE,.open = led_open,.read = led_read,.write = led_write,.release =  led_release,
};/** @description : 驱动出口函数* @param       : 无* @return      : 无*/
static int __init led_init(void)
{int ret = 0;/* 设置LED所使用的GPIO *//* 1、获取设备节点:gpioled */gpioled.nd = of_find_node_by_path("/gpioled");if(gpioled.nd == NULL) {printk("gpioled node not find!\r\n");return -EINVAL;} else {printk("gpioled node find!\r\n");}/* 2、 获取设备树中的gpio属性,得到LED所使用的LED编号 */gpioled.led_gpio = of_get_named_gpio(gpioled.nd, "led-gpio", 0);if(gpioled.led_gpio < 0) {printk("can't get led-gpio");return -EINVAL;}printk("led-gpio num = %d\r\n", gpioled.led_gpio);/* 3、设置GPIO1_IO03为输出,并且输出高电平,默认关闭LED灯 */ret = gpio_direction_output(gpioled.led_gpio, 1);if(ret < 0) {printk("can't set gpio!\r\n");}/* 注册字符设备驱动 *//* 1、创建设备号 */if (gpioled.major) {        /*  定义了设备号 */gpioled.devid = MKDEV(gpioled.major, 0);register_chrdev_region(gpioled.devid, GPIOLED_CNT, GPIOLED_NAME);} else {                        /* 没有定义设备号 */alloc_chrdev_region(&gpioled.devid, 0, GPIOLED_CNT, GPIOLED_NAME);  /* 申请设备号 */gpioled.major = MAJOR(gpioled.devid);   /* 获取分配号的主设备号 */gpioled.minor = MINOR(gpioled.devid);   /* 获取分配号的次设备号 */}printk("gpioled major=%d,minor=%d\r\n",gpioled.major, gpioled.minor);   /* 2、初始化cdev */gpioled.cdev.owner = THIS_MODULE;cdev_init(&gpioled.cdev, &gpioled_fops);/* 3、添加一个cdev */cdev_add(&gpioled.cdev, gpioled.devid, GPIOLED_CNT);/* 4、创建类 */gpioled.class = class_create(THIS_MODULE, GPIOLED_NAME);if (IS_ERR(gpioled.class)) {return PTR_ERR(gpioled.class);}/* 5、创建设备 */gpioled.device = device_create(gpioled.class, NULL, gpioled.devid, NULL, GPIOLED_NAME);if (IS_ERR(gpioled.device)) {return PTR_ERR(gpioled.device);}return 0;
}/** @description : 驱动出口函数* @param       : 无* @return      : 无*/
static void __exit led_exit(void)
{/* 注销字符设备驱动 */cdev_del(&gpioled.cdev);/*  删除cdev */unregister_chrdev_region(gpioled.devid, GPIOLED_CNT); /* 注销设备号 */device_destroy(gpioled.class, gpioled.devid);class_destroy(gpioled.class);
}module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");

4.platform驱动

driver.c

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include #define LEDDEV_CNT      1           /* 设备号长度    */
#define LEDDEV_NAME     "platled"   /* 设备名字     */
#define LEDOFF          0
#define LEDON           1/* 寄存器名 */
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;/* leddev设备结构体 */
struct leddev_dev{dev_t devid;            /* 设备号  */struct cdev cdev;       /* cdev     */struct class *class;    /* 类        */struct device *device;  /* 设备       */int major;              /* 主设备号 */      
};struct leddev_dev leddev;   /* led设备 *//** @description     : LED打开/关闭* @param - sta     : LEDON(0) 打开LED,LEDOFF(1) 关闭LED* @return          : 无*/
void led0_switch(u8 sta)
{u32 val = 0;if(sta == LEDON){val = readl(GPIO1_DR);val &= ~(1 << 3);   writel(val, GPIO1_DR);}else if(sta == LEDOFF){val = readl(GPIO1_DR);val|= (1 << 3); writel(val, GPIO1_DR);}   
}/** @description     : 打开设备* @param - inode   : 传递给驱动的inode* @param - filp    : 设备文件,file结构体有个叫做private_data的成员变量*                    一般在open的时候将private_data指向设备结构体。* @return          : 0 成功;其他 失败*/
static int led_open(struct inode *inode, struct file *filp)
{filp->private_data = &leddev; /* 设置私有数据  */return 0;
}/** @description     : 向设备写数据 * @param - filp    : 设备文件,表示打开的文件描述符* @param - buf     : 要写给设备写入的数据* @param - cnt     : 要写入的数据长度* @param - offt    : 相对于文件首地址的偏移* @return          : 写入的字节数,如果为负值,表示写入失败*/
static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{int retvalue;unsigned char databuf[1];unsigned char ledstat;retvalue = copy_from_user(databuf, buf, cnt);if(retvalue < 0) {return -EFAULT;}ledstat = databuf[0];       /* 获取状态值 */if(ledstat == LEDON) {led0_switch(LEDON);     /* 打开LED灯 */}else if(ledstat == LEDOFF) {led0_switch(LEDOFF);    /* 关闭LED灯 */}return 0;
}/* 设备操作函数 */
static struct file_operations led_fops = {.owner = THIS_MODULE,.open = led_open,.write = led_write,
};/** @description     : flatform驱动的probe函数,当驱动与*                    设备匹配以后此函数就会执行* @param - dev     : platform设备* @return          : 0,成功;其他负值,失败*/
static int led_probe(struct platform_device *dev)
{   int i = 0;int ressize[5];u32 val = 0;struct resource *ledsource[5];printk("led driver and device has matched!\r\n");/* 1、获取资源 */for (i = 0; i < 5; i++) {ledsource[i] = platform_get_resource(dev, IORESOURCE_MEM, i); /* 依次MEM类型资源 */if (!ledsource[i]) {dev_err(&dev->dev, "No MEM resource for always on\n");return -ENXIO;}ressize[i] = resource_size(ledsource[i]);   }   /* 2、初始化LED *//* 寄存器地址映射 */IMX6U_CCM_CCGR1 = ioremap(ledsource[0]->start, ressize[0]);SW_MUX_GPIO1_IO03 = ioremap(ledsource[1]->start, ressize[1]);SW_PAD_GPIO1_IO03 = ioremap(ledsource[2]->start, ressize[2]);GPIO1_DR = ioremap(ledsource[3]->start, ressize[3]);GPIO1_GDIR = ioremap(ledsource[4]->start, ressize[4]);val = readl(IMX6U_CCM_CCGR1);val &= ~(3 << 26);              /* 清除以前的设置 */val |= (3 << 26);               /* 设置新值 */writel(val, IMX6U_CCM_CCGR1);/* 设置GPIO1_IO03复用功能,将其复用为GPIO1_IO03 */writel(5, SW_MUX_GPIO1_IO03);writel(0x10B0, SW_PAD_GPIO1_IO03);/* 设置GPIO1_IO03为输出功能 */val = readl(GPIO1_GDIR);val &= ~(1 << 3);           /* 清除以前的设置 */val |= (1 << 3);            /* 设置为输出 */writel(val, GPIO1_GDIR);/* 默认关闭LED1 */val = readl(GPIO1_DR);val |= (1 << 3) ;   writel(val, GPIO1_DR);/* 注册字符设备驱动 *//*1、创建设备号 */if (leddev.major) {     /*  定义了设备号 */leddev.devid = MKDEV(leddev.major, 0);register_chrdev_region(leddev.devid, LEDDEV_CNT, LEDDEV_NAME);} else {                        /* 没有定义设备号 */alloc_chrdev_region(&leddev.devid, 0, LEDDEV_CNT, LEDDEV_NAME); /* 申请设备号 */leddev.major = MAJOR(leddev.devid); /* 获取分配号的主设备号 */}/* 2、初始化cdev */leddev.cdev.owner = THIS_MODULE;cdev_init(&leddev.cdev, &led_fops);/* 3、添加一个cdev */cdev_add(&leddev.cdev, leddev.devid, LEDDEV_CNT);/* 4、创建类 */leddev.class = class_create(THIS_MODULE, LEDDEV_NAME);if (IS_ERR(leddev.class)) {return PTR_ERR(leddev.class);}/* 5、创建设备 */leddev.device = device_create(leddev.class, NULL, leddev.devid, NULL, LEDDEV_NAME);if (IS_ERR(leddev.device)) {return PTR_ERR(leddev.device);}return 0;
}/** @description     : platform驱动的remove函数,移除platform驱动的时候此函数会执行* @param - dev     : platform设备* @return          : 0,成功;其他负值,失败*/
static int led_remove(struct platform_device *dev)
{iounmap(IMX6U_CCM_CCGR1);iounmap(SW_MUX_GPIO1_IO03);iounmap(SW_PAD_GPIO1_IO03);iounmap(GPIO1_DR);iounmap(GPIO1_GDIR);cdev_del(&leddev.cdev);/*  删除cdev */unregister_chrdev_region(leddev.devid, LEDDEV_CNT); /* 注销设备号 */device_destroy(leddev.class, leddev.devid);class_destroy(leddev.class);return 0;
}/* platform驱动结构体 */
static struct platform_driver led_driver = {.driver     = {.name   = "imx6ul-led",         /* 驱动名字,用于和设备匹配 */},.probe      = led_probe,.remove     = led_remove,
};/** @description : 驱动模块加载函数* @param       : 无* @return      : 无*/
static int __init leddriver_init(void)
{return platform_driver_register(&led_driver);
}/** @description : 驱动模块卸载函数* @param       : 无* @return      : 无*/
static void __exit leddriver_exit(void)
{platform_driver_unregister(&led_driver);
}module_init(leddriver_init);
module_exit(leddriver_exit);
MODULE_LICENSE("GPL");

device.c

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include /* * 寄存器地址定义*/
#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)
#define REGISTER_LENGTH             4/* @description     : 释放flatform设备模块的时候此函数会执行   * @param - dev     : 要释放的设备 * @return          : 无*/
static void led_release(struct device *dev)
{printk("led device released!\r\n"); 
}/*  * 设备资源信息,也就是LED0所使用的所有寄存器*/
static struct resource led_resources[] = {[0] = {.start  = CCM_CCGR1_BASE,.end    = (CCM_CCGR1_BASE + REGISTER_LENGTH - 1),.flags  = IORESOURCE_MEM,},  [1] = {.start  = SW_MUX_GPIO1_IO03_BASE,.end    = (SW_MUX_GPIO1_IO03_BASE + REGISTER_LENGTH - 1),.flags  = IORESOURCE_MEM,},[2] = {.start  = SW_PAD_GPIO1_IO03_BASE,.end    = (SW_PAD_GPIO1_IO03_BASE + REGISTER_LENGTH - 1),.flags  = IORESOURCE_MEM,},[3] = {.start  = GPIO1_DR_BASE,.end    = (GPIO1_DR_BASE + REGISTER_LENGTH - 1),.flags  = IORESOURCE_MEM,},[4] = {.start  = GPIO1_GDIR_BASE,.end    = (GPIO1_GDIR_BASE + REGISTER_LENGTH - 1),.flags  = IORESOURCE_MEM,},
};/** platform设备结构体 */
static struct platform_device leddevice = {.name = "imx6ul-led",.id = -1,.dev = {.release = &led_release,},.num_resources = ARRAY_SIZE(led_resources),.resource = led_resources,
};/** @description : 设备模块加载 * @param       : 无* @return      : 无*/
static int __init leddevice_init(void)
{return platform_device_register(&leddevice);
}/** @description : 设备模块注销* @param       : 无* @return      : 无*/
static void __exit leddevice_exit(void)
{platform_device_unregister(&leddevice);
}module_init(leddevice_init);
module_exit(leddevice_exit);
MODULE_LICENSE("GPL");

5.设备树platform

设备树

添加 pinctrl 节点
I.MX6U-ALPHA 开发板上的 LED 灯使用了 GPIO1_IO03 这个 PIN,打开 imx6ull-alientekemmc.dts,在 iomuxc 节点的 imx6ul-evk 子节点下创建一个名为“pinctrl_led”的子节点,节点内容如下所示:

pinctrl_led: ledgrp {fsl,pins = ;
};

第 3 行将 GPIO1_IO03 这个 PIN 复用为 GPIO1_IO03,电气属性值为 0X10B0

添加 LED 设备节点
在根节点“/”下创建 LED 灯节点,节点名为“gpioled”,节点内容如下:

gpioled {#address-cells = <1>;#size-cells = <1>;compatible = "atkalpha-gpioled";pinctrl-names = "default";pinctrl-0 = <&pinctrl_led>;led-gpio = <&gpio1 3 GPIO_ACTIVE_LOW>;status = "okay";
};

第 6 行, pinctrl-0 属性设置 LED 灯所使用的 PIN 对应的 pinctrl 节点。
第 7 行, led-gpio 属性指定了 LED 灯所使用的 GPIO,在这里就是 GPIO1 的 IO03,低电平有效。稍后编写驱动程序的时候会获取 led-gpio 属性的内容来得到 GPIO 编号,因为 gpio 子系统的 API 操作函数需要 GPIO 编号。

3、检查 PIN 是否被其他外设使用

驱动代码

driver.c

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include #define LEDDEV_CNT      1               /* 设备号长度    */
#define LEDDEV_NAME     "dtsplatled"    /* 设备名字     */
#define LEDOFF          0
#define LEDON           1/* leddev设备结构体 */
struct leddev_dev{dev_t devid;                /* 设备号  */struct cdev cdev;           /* cdev     */struct class *class;        /* 类        */struct device *device;      /* 设备       */int major;                  /* 主设备号 */  struct device_node *node;   /* LED设备节点 */int led0;                   /* LED灯GPIO标号 */
};struct leddev_dev leddev;       /* led设备 *//** @description     : LED打开/关闭* @param - sta     : LEDON(0) 打开LED,LEDOFF(1) 关闭LED* @return          : 无*/
void led0_switch(u8 sta)
{if (sta == LEDON )gpio_set_value(leddev.led0, 0);else if (sta == LEDOFF)gpio_set_value(leddev.led0, 1); 
}/** @description     : 打开设备* @param - inode   : 传递给驱动的inode* @param - filp    : 设备文件,file结构体有个叫做private_data的成员变量*                    一般在open的时候将private_data指向设备结构体。* @return          : 0 成功;其他 失败*/
static int led_open(struct inode *inode, struct file *filp)
{filp->private_data = &leddev; /* 设置私有数据  */return 0;
}/** @description     : 向设备写数据 * @param - filp    : 设备文件,表示打开的文件描述符* @param - buf     : 要写给设备写入的数据* @param - cnt     : 要写入的数据长度* @param - offt    : 相对于文件首地址的偏移* @return          : 写入的字节数,如果为负值,表示写入失败*/
static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{int retvalue;unsigned char databuf[2];unsigned char ledstat;retvalue = copy_from_user(databuf, buf, cnt);if(retvalue < 0) {printk("kernel write failed!\r\n");return -EFAULT;}ledstat = databuf[0];if (ledstat == LEDON) {led0_switch(LEDON);} else if (ledstat == LEDOFF) {led0_switch(LEDOFF);}return 0;
}/* 设备操作函数 */
static struct file_operations led_fops = {.owner = THIS_MODULE,.open = led_open,.write = led_write,
};/** @description     : flatform驱动的probe函数,当驱动与*                    设备匹配以后此函数就会执行* @param - dev     : platform设备* @return          : 0,成功;其他负值,失败*/
static int led_probe(struct platform_device *dev)
{   printk("led driver and device was matched!\r\n");/* 1、设置设备号 */if (leddev.major) {leddev.devid = MKDEV(leddev.major, 0);register_chrdev_region(leddev.devid, LEDDEV_CNT, LEDDEV_NAME);} else {alloc_chrdev_region(&leddev.devid, 0, LEDDEV_CNT, LEDDEV_NAME);leddev.major = MAJOR(leddev.devid);}/* 2、注册设备      */cdev_init(&leddev.cdev, &led_fops);cdev_add(&leddev.cdev, leddev.devid, LEDDEV_CNT);/* 3、创建类      */leddev.class = class_create(THIS_MODULE, LEDDEV_NAME);if (IS_ERR(leddev.class)) {return PTR_ERR(leddev.class);}/* 4、创建设备 */leddev.device = device_create(leddev.class, NULL, leddev.devid, NULL, LEDDEV_NAME);if (IS_ERR(leddev.device)) {return PTR_ERR(leddev.device);}/* 5、初始化IO */   leddev.node = of_find_node_by_path("/gpioled");if (leddev.node == NULL){printk("gpioled node nost find!\r\n");return -EINVAL;} leddev.led0 = of_get_named_gpio(leddev.node, "led-gpio", 0);if (leddev.led0 < 0) {printk("can't get led-gpio\r\n");return -EINVAL;}gpio_request(leddev.led0, "led0");gpio_direction_output(leddev.led0, 1); /* led0 IO设置为输出,默认高电平    */return 0;
}/** @description     : platform驱动的remove函数,移除platform驱动的时候此函数会执行* @param - dev     : platform设备* @return          : 0,成功;其他负值,失败*/
static int led_remove(struct platform_device *dev)
{gpio_set_value(leddev.led0, 1);     /* 卸载驱动的时候关闭LED */gpio_free(leddev.led0);             /* 释放IO             */cdev_del(&leddev.cdev);             /*  删除cdev */unregister_chrdev_region(leddev.devid, LEDDEV_CNT); /* 注销设备号 */device_destroy(leddev.class, leddev.devid);class_destroy(leddev.class);return 0;
}/* 匹配列表 */
static const struct of_device_id led_of_match[] = {{ .compatible = "atkalpha-gpioled" },{ /* Sentinel */ }
};/* platform驱动结构体 */
static struct platform_driver led_driver = {.driver     = {.name   = "imx6ul-led",         /* 驱动名字,用于和设备匹配 */.of_match_table = led_of_match, /* 设备树匹配表        */},.probe      = led_probe,.remove     = led_remove,
};/** @description : 驱动模块加载函数* @param       : 无* @return      : 无*/
static int __init leddriver_init(void)
{return platform_driver_register(&led_driver);
}/** @description : 驱动模块卸载函数* @param       : 无* @return      : 无*/
static void __exit leddriver_exit(void)
{platform_driver_unregister(&led_driver);
}module_init(leddriver_init);
module_exit(leddriver_exit);
MODULE_LICENSE("GPL");

通用GPIO引脚操作方法

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

1 GPIO 模块一般结构

  • 有多组 GPIO,每组有多个 GPIO

  • 使能:电源/时钟

  • 模式(Mode):引脚可用于 GPIO 或其他功能

  • 方向:引脚 Mode 设置为 GPIO 时,可以继续设置它是输出引脚,还是输入引脚

  • 数值:
    对于输出引脚,可以设置寄存器让它输出高、低电平

    对于输入引脚,可以读取寄存器得到引脚的当前电平

2 GPIO 寄存器操作

  • 芯片手册一般有相关章节,用来介绍: power/clock
    可以设置对应寄存器使能某个 GPIO 模块(Module)

    有些芯片的 GPIO 是没有使能开关的,即它总是使能的

  • 一个引脚可以用于 GPIO、串口、 USB 或其他功能,有对应的寄存器来选择引脚的功能

  • 对于已经设置为 GPIO 功能的引脚,有方向寄存器用来设置它的方向:输出、输入

  • 对于已经设置为 GPIO 功能的引脚,有数据寄存器用来写、读引脚电平状态GPIO 寄存器的 2 种操作方法: 原则:不能影响到其他位

  1. 直接读写:读出、修改对应位、写入

a) 要设置 bit n:

val = data_reg;
val = val | (1<

b) 要清除 bit n:

val = data_reg;
val = val & ~(1<
  1. set-and-clear protocol:

set_reg, clr_reg, data_reg 三个寄存器对应的是同一个物理寄存器,

a) 要设置 bit n:

 set_reg = (1<

b) 要清除 bit n:

 clr_reg = (1<

3.imx6ull寄存器操作

3.1IMX6ULL 的 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 模块本身,框图如图 4.2
    在这里插入图片描述

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

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

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

CCM_CCGR 寄存器中某 2 位的取值含义如下:
在这里插入图片描述

图 4.3 CCM_CCGR

  • 00:该 GPIO 模块全程被关闭
  • 01:该 GPIO 模块在 CPU run mode 情况下是使能的;在 WAIT 或 STOP模式下,关闭
  • 10:保留
  • 11:该 GPIO 模块全程使能

GPIO2 时钟控制
在这里插入图片描述

GPIO1、 GPIO5 时钟控制:
在这里插入图片描述

GPIO3 时钟控制:
在这里插入图片描述

GPIO4 时钟控制:
在这里插入图片描述

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

参考资料:芯片手册《Chapter 32: IOMUX Controller (IOMUXC)》。
在这里插入图片描述

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

  1. 选择功能:

a) IOMUXC_SW_MUX_CTL_PAD_ : Mux pad xxx,选择某个 pad 的功能

b) IOMUXC_SW_MUX_CTL_GRP_: Mux grp xxx,选择某组引脚的功能

某个引脚,或是某组预设的引脚,都有 8 个可选的模式(alternate (ALT)MUX_MODE)。
在这里插入图片描述

比如:
在这里插入图片描述

设置上下拉电阻等参数:

a) IOMUXC_SW_PAD_CTL_PAD_: pad pad xxx,设置某个 pad 的参数

b) IOMUXC_SW_PAD_CTL_GRP_: pad grp xxx,设置某组引脚的参数

在这里插入图片描述

比如:
在这里插入图片描述

3.4 GPIO 模块内部

框图如下:
在这里插入图片描述

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

  1. GPIOx_GDIR:设置引脚方向,每位对应一个引脚, 1-output, 0-input
    在这里插入图片描述

  2. GPIOx_DR:设置输出引脚的电平,每位对应一个引脚, 1-高电平, 0-低电平
    在这里插入图片描述

  3. GPIOx_PSR:读取引脚的电平,每位对应一个引脚, 1-高电平, 0-低电平
    在这里插入图片描述

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 某位的值

需要注意的是,你可以设置该引脚的 loopback 功能,这样就可以从GPIOx_PSR 中读到引脚的有实电平;你从 GPIOx_DR 中读回的只是上次设置的值,它并不能反应引脚的真实电平,比如可能因为硬件故障导致该引脚跟地短路了,你通过设置 GPIOx_DR 让它输出高电平并不会起效果。

相关内容

热门资讯

北京的名胜古迹 北京最著名的景... 北京从元代开始,逐渐走上帝国首都的道路,先是成为大辽朝五大首都之一的南京城,随着金灭辽,金代从海陵王...
苗族的传统节日 贵州苗族节日有... 【岜沙苗族芦笙节】岜沙,苗语叫“分送”,距从江县城7.5公里,是世界上最崇拜树木并以树为神的枪手部落...
长白山自助游攻略 吉林长白山游... 昨天介绍了西坡的景点详细请看链接:一个人的旅行,据说能看到长白山天池全凭运气,您的运气如何?今日介绍...
应用未安装解决办法 平板应用未... ---IT小技术,每天Get一个小技能!一、前言描述苹果IPad2居然不能安装怎么办?与此IPad不...
脚上的穴位图 脚面经络图对应的... 人体穴位作用图解大全更清晰直观的标注了各个人体穴位的作用,包括头部穴位图、胸部穴位图、背部穴位图、胳...