通过前面两个系列的学习,我们已经了解DSS系统,LCD基本原理,DSS设备树的配置等基本知识。本文简单学习和梳理LCD设备驱动的代码,方便项目中快速bring up和debug。

此系列文章基于TI的AM572x EVM开发板,使用参考代码linux-4.14.67+gitAUTOINC+d315a9bb00-gd315a9bb00

1. 设备的枚举

我们知道linux设备驱动模型里面分为设备和驱动两部分,设备由device tree负责定义,内核代码会解析设备树枚举出设备;但在阅读代码的过程中会发现OMAP相关的LCD驱动代码有3部分,分别位于:

drivers/video/fbdev/omap/

drivers/video/fbdev/omap2/

drivers/gpu/drm/omapdrm/displays/

这3部分代码很类似,前面两个是基于fb,后面一个是基于drm,我们可以通过编译选项来确认到底用到哪部分代码。以config文件tisdk_am57xx-evm_defconfig为例,它定义了如下2个宏控。

CONFIG_MACH_OMAP_GENERIC
CONFIG_SOC_DRA7XX

我们找到machine的初始化代码arch/arm/mach-omap2/board-generic.c,通过上面的2个宏控找到对应的代码入口函数定义: DT_MACHINE_START

#ifdef CONFIG_SOC_DRA7XX
static const char *const dra74x_boards_compat[] __initconst = {"ti,dra762","ti,am5728","ti,am5726","ti,dra742","ti,dra7",NULL,
};DT_MACHINE_START(DRA74X_DT, "Generic DRA74X (Flattened Device Tree)")
#if defined(CONFIG_ZONE_DMA) && defined(CONFIG_ARM_LPAE).dma_zone_size  = SZ_2G,
#endif.reserve  = omap_reserve,.smp        = smp_ops(omap4_smp_ops),.map_io       = dra7xx_map_io,.init_early    = dra7xx_init_early,.init_late = dra7xx_init_late,.init_irq   = omap_gic_of_init,.init_machine   = omap_generic_init,.init_time = omap5_realtime_timer_init,.dt_compat = dra74x_boards_compat,.restart    = omap44xx_restart,
MACHINE_ENDstatic const char *const dra72x_boards_compat[] __initconst = {"ti,am5718","ti,am5716","ti,dra722","ti,dra718",NULL,
};DT_MACHINE_START(DRA72X_DT, "Generic DRA72X (Flattened Device Tree)")
#if defined(CONFIG_ZONE_DMA) && defined(CONFIG_ARM_LPAE).dma_zone_size  = SZ_2G,
#endif.reserve  = omap_reserve,.map_io     = dra7xx_map_io,.init_early    = dra7xx_init_early,.init_late = dra7xx_init_late,.init_irq   = omap_gic_of_init,.init_machine   = omap_generic_init,.init_time = omap5_realtime_timer_init,.dt_compat = dra72x_boards_compat,.restart    = omap44xx_restart,
MACHINE_END
#endif

其中的omap_generic_init中就包含了dss设备的初始化。其中的pdata_quirks_init函数将枚举platform设备,即dss@58000000 这个设备会被当成平台设备枚举出来;但是它的子设备如ports,dispc@58001000,encoder@58060000等则不会被枚举(参考of_platform_populate的实现)。接下来会调用omapdss_init_of();在omapdss_init_of()函数之中,再一次调用了of_platform_populate(),才将dss中的子设备建立起来。再然后调用omapdss_init_fbdev()进行fb设备的初始化,不过我们可以看到由于没有定义CONFIG_FB_OMAP2,这个函数实现为空。omapdss_init_fbdev()当中会创建fb相关的设备,对应的驱动在drivers/video/fbdev/omap2/omapfb/dss中实现,由于没有枚举设备,对应的驱动自然也不会被加载。当然通过宏的设置也可以确认对于当前的配置,我们使用的代码位于drivers/gpu/drm/omapdrm/displays/。

2. 驱动的加载

既然已经知道dss驱动使用的是drm中,可以很容易找到初始化的地方,位于文件drivers/gpu/drm/omapdrm/dss/dss.c。

/* INIT */
static struct platform_driver * const omap_dss_drivers[] = {&omap_dsshw_driver,&omap_dispchw_driver,
#ifdef CONFIG_OMAP2_DSS_DSI&omap_dsihw_driver,
#endif
#ifdef CONFIG_OMAP2_DSS_VENC&omap_venchw_driver,
#endif
#ifdef CONFIG_OMAP4_DSS_HDMI&omapdss_hdmi4hw_driver,
#endif
#ifdef CONFIG_OMAP5_DSS_HDMI&omapdss_hdmi5hw_driver,
#endif
};static int __init omap_dss_init(void)
{return platform_register_drivers(omap_dss_drivers,ARRAY_SIZE(omap_dss_drivers));
}

根据配置确定驱动如下,到此为止剩下的工作就就是逐个分析这些驱动的实现。

 &omap_dsshw_driver,&omap_dispchw_driver,&omapdss_hdmi4hw_driver,&omapdss_hdmi5hw_driver,

2.1 dss hardware driver

该驱动match id是dra7-dss( { .compatible = "ti,dra7-dss",  .data = &dra7xx_dss_feats }),单独增加了的设备数据(dra7xx_dss_feats)作为dss的feature,feature对应了硬件的信息如时钟,ports(系列1中的interface),也可以从如下的驱动数据结构中看出该驱动的主要作用是对硬件进行操作,比如pll,clk,ports等。

static struct {struct platform_device *pdev;void __iomem    *base;struct regmap  *syscon_pll_ctrl;u32        syscon_pll_ctrl_offset;struct clk   *parent_clk;struct clk  *dss_clk;unsigned long  dss_clk_rate;unsigned long  cache_req_pck;unsigned long cache_prate;struct dispc_clock_info cache_dispc_cinfo;enum dss_clk_source dsi_clk_source[MAX_NUM_DSI];enum dss_clk_source dispc_clk_source;enum dss_clk_source lcd_clk_source[MAX_DSS_LCD_MANAGERS];bool        ctx_valid;u32       ctx[DSS_SZ_REGS / sizeof(u32)];const struct dss_features *feat;struct dss_pll   *video1_pll;struct dss_pll  *video2_pll;
} dss;

和大多数驱动一样,probe中主要进行配置、参数初始化,便于后期使用。主要关心如下4个函数。

 r = dss_init_ports(pdev);r = initialize_omapdrm_device();/* Add all the child devices as components. */device_for_each_child(&pdev->dev, &match, dss_add_child_component);r = component_master_add_with_match(&pdev->dev, &dss_component_ops, match);

2.1.1 dss_init_ports

dss_init_ports初始化平台支持的port,这里的feat就是前面驱动data中那个feature,它定义了我们当前平台支持的ports有哪些,而当前平台是通过compatible来确定的(如前述,"ti,dra7-dss")。因此,可以确认我们使用的平台3个port都是DPI,这和系列文档1中DPI1,2,3是对应的。

static const enum omap_display_type dra7xx_ports[] = {OMAP_DISPLAY_TYPE_DPI,OMAP_DISPLAY_TYPE_DPI,OMAP_DISPLAY_TYPE_DPI,
};

同样feat中4个outputs的定义如下,我们也可以在系列1或者spec中找到对应的硬件模块。

static const enum omap_dss_output_id omap5_dss_supported_outputs[] = {/* OMAP_DSS_CHANNEL_LCD */OMAP_DSS_OUTPUT_DPI | OMAP_DSS_OUTPUT_DBI |OMAP_DSS_OUTPUT_DSI1 | OMAP_DSS_OUTPUT_DSI2,/* OMAP_DSS_CHANNEL_DIGIT */OMAP_DSS_OUTPUT_HDMI,/* OMAP_DSS_CHANNEL_LCD2 */OMAP_DSS_OUTPUT_DPI | OMAP_DSS_OUTPUT_DBI |OMAP_DSS_OUTPUT_DSI1,/* OMAP_DSS_CHANNEL_LCD3 */OMAP_DSS_OUTPUT_DPI | OMAP_DSS_OUTPUT_DBI |OMAP_DSS_OUTPUT_DSI2,
};

dss_init_ports的函数如下,我们需要分析一下该函数,因为DPI往下连接的就是LCD,也是我们最关心的配置部分。这个函数首先调用port = of_graph_get_port_by_id(parent, i);得到设备树中定义的port,然后调用dpi_init_port对其进行初始化。即根据设备树定义的port初始化一个dpi_data结构,并通过omapdss_register_output(out);将这个结构加入到output_list链表当中,我们将看到在后面的panel驱动中会查找这个链表。这里有一个配置是data-lines = <0x18>;表示LCD使用的数据宽度,也就是系列1中DPI协议中数据线的宽度。

static int dss_init_ports(struct platform_device *pdev)
{struct device_node *parent = pdev->dev.of_node;struct device_node *port;int i;for (i = 0; i < dss.feat->num_ports; i++) {port = of_graph_get_port_by_id(parent, i);if (!port)continue;switch (dss.feat->ports[i]) {case OMAP_DISPLAY_TYPE_DPI:dpi_init_port(pdev, port, dss.feat->model);break;case OMAP_DISPLAY_TYPE_SDI:sdi_init_port(pdev, port);break;default:break;}}return 0;
}

2.1.2 initialize_omapdrm_device

这个函数会注册一个omapdrm平台设备,该设备的注册最终会导致对应的设备驱动被加载,可以知道这个驱动位于drivers/gpu/drm/omapdrm/omap_drv.c中。

static int initialize_omapdrm_device(void)
{omap_drm_device = platform_device_register_simple("omapdrm", 0, NULL, 0);if (IS_ERR(omap_drm_device))return PTR_ERR(omap_drm_device);return 0;
}

从目录名字可以知道该模块属于drm模块而且并不是omap特有的,其实看该模块的实现也能知道,这个模块是drm模块和硬件模块通信的初始化模块,比如probe中的函数drm_dev_register,如注释,这个函数是将自己注册到drm模块当中;该模块的初始化标志着外设模块(OMAP中的dss模块)和drm模块最终联系起来了。

 /** Register the DRM device with the core and the connectors with* sysfs.*/ret = drm_dev_register(ddev, 0);

在后面的章节学习panel驱动时我们还会看到它和drm驱动模块相关联。但我们将详细的学习放到以后的文章中进行,我们可以先简单看一下probe中drm driver的结构体,当看到MAJOR,MINOR、open,ioctl是不是觉得十分熟悉?

static struct drm_driver omap_drm_driver = {.driver_features = DRIVER_MODESET | DRIVER_GEM  | DRIVER_PRIME |DRIVER_ATOMIC | DRIVER_RENDER,.open = dev_open,.lastclose = dev_lastclose,
#ifdef CONFIG_DEBUG_FS.debugfs_init = omap_debugfs_init,
#endif.prime_handle_to_fd = drm_gem_prime_handle_to_fd,.prime_fd_to_handle = drm_gem_prime_fd_to_handle,.gem_prime_export = omap_gem_prime_export,.gem_prime_import = omap_gem_prime_import,.gem_free_object = omap_gem_free_object,.gem_vm_ops = &omap_gem_vm_ops,.dumb_create = omap_gem_dumb_create,.dumb_map_offset = omap_gem_dumb_map_offset,.ioctls = ioctls,.num_ioctls = DRM_OMAP_NUM_IOCTLS,.fops = &omapdriver_fops,.name = DRIVER_NAME,.desc = DRIVER_DESC,.date = DRIVER_DATE,.major = DRIVER_MAJOR,.minor = DRIVER_MINOR,.patchlevel = DRIVER_PATCHLEVEL,
};

2.1.3 master add with match

这两个函数将dss设备作为一个master,并将其子节点作为componet加到该master的match链表当中,在有componet增加成功的时候相应的回调被调用,并bring up master。

其中device_for_each_child遍历dss节点下的子节点,每个子节点会对应生成一个component_match结构,该结构中的componet_match_array中的data指针即对应着子节点设备,最终所有的子节点的component生成一个match数组;component_master_add_with_match将这个macth数组和master也就是dss设备关联起来,其实就是生成一个struct master结构体,该结构体包括master、match数组和ops回调函数,这个结构体会被挂载到一个全局链表。 component_master_add_with_match函数最后还会调用try_to_bring_up_master,这个函数的目的bringup master,其实就是查看master对应的componet中是否有匹配并调用ops回调函数中的bind函数。同理,对于component来说,在增加component的时候也会调用bind函数(比如我们在下一节中看到的dispc,就是一个component)。

 /* Add all the child devices as components. */device_for_each_child(&pdev->dev, &match, dss_add_child_component);r = component_master_add_with_match(&pdev->dev, &dss_component_ops, match);

2.2 component driver

dispc驱动位于drivers/gpu/drm/omapdrm/dss/dispc.c当中,它的match列表为 { .compatible = "ti,dra7-dispc",  .data = &omap54xx_dispc_feats }。该模块的probe函数如下,可以看到它直接调用component_add,根据上一节的介绍,这个函数会导致ops中的bind函数被调用,也就是dispc_bind被会被调用,详细分析bind函数在后面再继续学习。同理,hdmi模块等其它componet也类似。

static int dispc_probe(struct platform_device *pdev)
{return component_add(&pdev->dev, &dispc_component_ops);
}

2.3 dpi driver

在am5728上LCD的接口是dpi,和lcd驱动关系最大的应该算是dpi驱动了。找到对应的文件/drivers/gpu/omapdrm/displays/panle-dpi.c会发现这个模块的match列表为{ .compatible = "omapdss,panel-dpi", },而相应的设备树中找不到哪个节点的compatible属性和之相同,这是怎么回事呢?

2.3.1 修改match列表

通过查看代码,可以发现在drivers/gpu/drm/omapdrm/dss/omapdss-boot-init.c文件,会修改节点的compatible属性并加上"omapdss",并且这个模块在驱动模块加载之前就已经初始化完成,这也就使得dpi驱动模块能够有机会被加载。

subsys_initcall(omapdss_boot_init);

在这个模块中会调用omapdss_walk_device遍历dss节点和子节点,找到“ports”或“port”节点,然后根据remote-endpoint找到远程endpoint节点,以实际一个例子(为了方便下面以及后面贴出来的设备树是反编译出来的结果,实际上和按照系列2中找出来节点一样),找到0x248这个节点也即lcd_in这节点,在其parent节点的compatible上增加"omapdss,",即最终的compatible是"omapdss,osddisplays,osd070t1718-19ts","omapdss,panel-dpi";所以panel-dpi.c中的模块会被加载。

 display {phandle = <0x24b>;label = "lcd";enable-gpios = <0xaf 0x5 0x0>;backlight = <0x246>;compatible = "osddisplays,osd070t1718-19ts", "panel-dpi";port {endpoint {phandle = <0x248>;remote-endpoint = <0x247>;};};panel-timing {vsync-len = <0xd>;vsync-active = <0x0>;vfront-porch = <0x16>;vback-porch = <0xa>;vactive = <0x1e0>;pixelclk-active = <0x1>;hsync-len = <0x1e>;hsync-active = <0x0>;hfront-porch = <0xd2>;hback-porch = <0x10>;hactive = <0x320>;de-active = <0x1>;clock-frequency = <0x1f78a40>;};};

2.3.2 注册display

在设备驱动挂载之后probe函数被调用,probe中会解析dispaly中的配置,初始化struct omap_dss_device结构体,并注册到系统,即增加到panel_list列表;omap_dss_device结构体已经指定了panel的类型(如dpi驱动中的类型为OMAP_DISPLAY_TYPE_DPI)和panel的参数(如下面的timing)等。

int omapdss_register_display(struct omap_dss_device *dssdev)
{struct omap_dss_driver *drv = dssdev->driver;int id;/** Note: this presumes that all displays either have an DT alias, or* none has.*/id = of_alias_get_id(dssdev->dev->of_node, "display");if (id < 0)id = disp_num_counter++;dssdev->alias_id = id;/* Use 'label' property for name, if it exists */of_property_read_string(dssdev->dev->of_node, "label", &dssdev->name);if (dssdev->name == NULL)dssdev->name = devm_kasprintf(dssdev->dev, GFP_KERNEL,"display%d", id);if (drv && drv->get_timings == NULL)drv->get_timings = omapdss_default_get_timings;mutex_lock(&panel_list_mutex);list_add_tail(&dssdev->panel_list, &panel_list);mutex_unlock(&panel_list_mutex);return 0;
}

我们知道display和dpi接口是对应的,代码是怎么样把它们联系起来?查看probe中的函数panel_dpi_probe_of,它会通过reg属性找到dss节点中的port。

in = omapdss_of_find_source_for_first_ep(node);

对于具体的例子来说就是下面这个节点,这样就如设备树中定义的那样(通过remote-endpoint),这两者相互联系起来了。

         ports {#size-cells = <0x0>;#address-cells = <0x1>;port {reg = <0x0>;endpoint {phandle = <0x247>;remote-endpoint = <0x248>;data-lines = <0x18>;};};};

2.3.3 display的connect

上一节看到display的注册只是将display加到了一个全局链表上,并将dispaly(或者称为LCD模块)和DPI接口联系起来,实际上它们还要通过connect将两者进行初始化。在注册display的时候,我们看到omap_dss_device结构体中有一个driver变量,它被赋值为panel_dpi_ops,该变量中的connect会最终调用DPI中的connect进行初始化,下面继续分析。

static struct omap_dss_driver panel_dpi_ops = {.connect = panel_dpi_connect,.disconnect    = panel_dpi_disconnect,.enable     = panel_dpi_enable,.disable    = panel_dpi_disable,.set_timings   = panel_dpi_set_timings,.get_timings   = panel_dpi_get_timings,.check_timings = panel_dpi_check_timings,
};

回到drivers/gpu/drm/omap/drm/omap_drv.c文件,在前面章节中已经知道该模块被会加载,并且调用了drm_dev_register来注册drm设备。在probe当中,我们看ret = omap_connect_dssdevs(ddev);函数的实现,这个函数中的connect是不是就是有前面的panel_dpi_ops呢?跟踪omap_collect_dssdevs()可以最终找到omap_dss_get_next_device函数,而omap_dss_get_next_device函数确实是在查找panel_list列表,猜测正确。

 omap_collect_dssdevs(ddev);for (i = 0; i < priv->num_dssdevs; i++) {struct omap_dss_device *dssdev = priv->dssdevs[i];r = dssdev->driver->connect(dssdev);if (r == -EPROBE_DEFER)goto cleanup;else if (r)dev_warn(dssdev->dev, "could not connect display: %s\n",dssdev->name);elseworking |= BIT(i);}

可以看到panel-dpi.c中的connect最终调用的是dpi接口中的connect

static int panel_dpi_connect(struct omap_dss_device *dssdev)
{struct panel_drv_data *ddata = to_panel_data(dssdev);struct omap_dss_device *in = ddata->in;int r;if (omapdss_device_is_connected(dssdev))return 0;r = in->ops.dpi->connect(in, dssdev);if (r)return r;return 0;
}

也就是dss_init_ports中的ops中的connect。具体包括初始化上电,pll,以及dss_mgr_connect等等,后面再学习。

static int dpi_connect(struct omap_dss_device *dssdev,struct omap_dss_device *dst)
{struct dpi_data *dpi = dpi_get_data_from_dssdev(dssdev);enum omap_channel channel = dpi->output.dispc_channel;int r;r = dpi_init_regulator(dpi);if (r)return r;dpi_init_pll(dpi);r = dss_mgr_connect(channel, dssdev);if (r)return r;r = omapdss_output_set_device(dssdev, dst);if (r) {DSSERR("failed to connect output to new device: %s\n",dst->name);dss_mgr_disconnect(channel, dssdev);return r;}return 0;
}

2.3.4 题外话

目前为止我们只看到dpi接口的LCD驱动,在系列1中我们知道OMAP平台支持其它接口如DSI,DBI等,这些接口的驱动如何学习呢,其实看display/目录下的文件可知,对于其它的LCD驱动,最终目的都是去注册omapdss_register_display,既然已经将流程打通,具体遇到问题或者需要时再来学习不迟。

3. DPI显示屏的配置

通过前面的学习,我可以知道基本所有的配置都在设备树当中,且都在本系列文章中列出,比如电源video-supply,色彩深度data-lines,使能引脚enable-gpios,背光backlight等等。以及最重要的时序部分panel-timing,时序部分需要结合panel的spec进行设置,和系列1中dpi的timing图类似。

TI OMAP平台BSP学习笔记之 - LCD 驱动(3)相关推荐

  1. TI OMAP平台BSP学习笔记之 - UBOOT(1)

    1. Bootloader 和 TI Uboot Bootloader的一种,用来引导系统,通常HLOS如LINUX,WINDOWS等系统的镜像保存在硬盘.EMMC等介质中,Bootloader的主要 ...

  2. *基于RT-Thread的战舰开发板连接Onenent云平台(学习笔记)**

    基于RT-Thread的战舰开发板连接Onenent云平台(学习笔记) 摘要:本文主要是我在使用正点原子开发板在rt_thread框架下连接onenet云平台的学习笔记.此文主要介绍配置步骤和开发过程 ...

  3. 中国移动物联网开放平台OneNET学习笔记(1)——设备接入(MQTT协议)OneNET Studio篇

    一.平台简介 中国移动物联网开放平台(OneNET) 是中移物联网有限公司基于物联网技术和产业特点打造的开放平台和生态环境,适配各种网络环境和协议类型,支持各类传感器和智能硬件的快速接入和大数据服务, ...

  4. 关于NB-IOT模块链接阿里物联网平台的学习笔记-记录

    关于NB-IOT模块链接阿里物联网平台思路的学习笔记-记录 叙述 调试思路总结 调试过程 AT命令-方式一 AT命令-方式二 AT命令-方式三 软件 关于遇到问题 总结 叙述 前一段是写了一篇&quo ...

  5. 开源的容器虚拟化平台Docker学习笔记,个人私藏分享,不谢!

    一.Docker 简介 Docker 两个主要部件: Docker: 开源的容器虚拟化平台 Docker Hub: 用于分享.管理 Docker 容器的 Docker SaaS 平台 -- Docke ...

  6. linux2.6驱动学习笔记之字符驱动

    1.字符驱动组成 1.1字符驱动的模块加载与卸载 //设备结构体模板 struct xxx_dev_t { struct cdev cdev; ...... }xxx_dev; 在字符驱动模块加载函数 ...

  7. Linux驱动学习笔记之触摸屏驱动

    触摸屏归纳为输入子系统,这里主要是针对电阻屏,其使用过程如下 当用触摸笔按下时,产生中断. 在中断处理函数处理函数中启动ADC转换x,y坐标. ADC结束,产生ADC中断 在ADC中断处理函数里上报( ...

  8. 【嵌入式Linux学习笔记】Linux驱动开发

    Linux系统构建完成后,就可以基于该环境方便地进行开发了,相关的开发流程与MCU类似,但是引入了设备树的概念,编写应用代码要相对复杂一点.但是省去了很多配置工作. 学习视频地址:[正点原子]STM3 ...

  9. 树莓派学习笔记——Linux I2C驱动说明

    1.前言 [linux内核说明] 通常情况下,I2C设备由内核驱动控制,但是某些情况下I2C设备也可由用户空间控制.如果在用户空间控制I2C设备,需要访问/dev目录中所提供的接口,在使用I2C设备之 ...

最新文章

  1. boundingRectWithSize 的使用
  2. 6.QT信号槽的时序分析
  3. curl可以访问但httpclient不能访问_exta进程不能访问+ASM实例的解决方法
  4. 第四轮全国学科评估中获评A+的高校及学科(A+高校排行榜)
  5. 定位Bean 扫描路径
  6. 【转载】创建型-工厂方法模式
  7. unicode字符大全可复制_说说Excel不可见字符的那些事
  8. 又是一年中秋节,好想举杯邀明月
  9. Go语言学习Day06
  10. java访问方法修饰词四个_java中的四个修饰词(访问权限)
  11. linux bc安装的代码,BCLinux安装教程新篇
  12. 个性化推荐认知之----数字化转型浪潮下,产品经理应如何重新认知个性化推荐?...
  13. CSDN浏览器助手插件[少了很多糟心的广告]
  14. 三星数据被黑客泄露、罗马尼亚加油站网络遭勒索攻击|3月8日全球网络安全热点
  15. python写excel,请大表哥喝杯茶
  16. dataframe建一个空的,R创建一个空的data.frame
  17. java使用JSON-RPC进行BTC、LTC钱包开发
  18. redis的incr+expire的坑
  19. 应用程序错误电脑黑屏_电脑运行程序出现APPCRASH错误的三种解决方法
  20. 物体重心的特点是什么_物体的重心

热门文章

  1. 电脑UEFI启动是什么?
  2. 在TITAN RTX 2080Ti 上安装 Ubuntu18.04+Nvidia-430显卡驱动+配置深度学习环境(1)
  3. 数据结构学习记录---天勤线性表综合应用题(2)
  4. 从消息推送来看,华为、小米做得最好
  5. 小麦苗博客用到的图片
  6. 60个Vue常见问题汇总及解决方案
  7. 清北学堂 2017-10-05
  8. MOSFET类型识别小结
  9. android 适配7.0,Android7.0适配心得(一)_拍照兼容
  10. 手机python编程软件 turtle,安卓手机python编程软件