博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
【嵌入式Linux驱动开发】七、驱动开发的利器 - 设备树的闪亮登场
阅读量:2029 次
发布时间:2019-04-28

本文共 14712 字,大约阅读时间需要 49 分钟。

  受身无间者永远不死,寿长乃无间地狱中之大劫。

                      ----电影《无间道》经典台词

文章目录


一、设备树引入与作用

  上一节在介绍总线设备驱动模型时,提到platform平台修改引脚时,设备端的代码需要重新编译生成platform_device结构体,同时过多的设备对应的.c文件也会造成过多的冗余代码,致使以优雅冠称的Linux臃肿不堪,Linus祖师爷自然要发火,于是就有了设备树。

  设备树和总线驱动模型Platform是类似的,都是构造platform_device,不涉及驱动程序核心。而设备树是如何做到优雅的呢?看下图左侧部分,使用配置文件让系统生成Platform_device!

在这里插入图片描述

  需要牢记的一点是:设备树不可能用来写驱动。

  设备树只是用来给内核里的驱动程序, 指定硬件的信息。比如 LED 驱动,在内核的驱动程序里去操作寄存器,但是操作哪一个引脚?这由设备树指定。
  一个单板启动时, u-boot 先运行,它的作用是启动内核。 U-boot 会把内核和设备树文件都读入内存,然后启动内核。在启动内核时会把设备树在内存中的地址告诉内核。

二、设备树的语法

2.1 怎么描述这棵树?

  既然称之为设备树,得名原因自然像一个树的形状咯,如下图。

在这里插入图片描述

  描述设备树的文件叫做 DTS(Device Tree Source),它需要编译为 dtb(device tree blob)文件供内核使用。所以这里我们要谈的设备树语法,当然就是DTS语法了!还需要知道的是.dtsi文件,它是芯片的公共设备树文件(i是include),我们写的.dts文件会追加到了.dtsi文件中。

我们先来直观感受一个设备树实例。

  • 树状表达
    在这里插入图片描述
  • 代码表达
    在这里插入图片描述

2.2 DTS文件的格式

  • 文件布局
/dts-v1/; // 表示版本[memory reservations] // 格式为: /memreserve/ 
;/ {
[property definitions] [child nodes]};
  • node 的格式
    • 设备树中的基本单元,被称为“node”。
    • label 是标号,可以省略, label 的作用是为了方便地引用 node。
[label:] node-name[@unit-address] {
[properties definitions] [child nodes]};

举例:

/dts-v1/;/ {
uart0: uart@fe001000 {
compatible="ns16550"; reg=<0xfe001000 0x100>; };};
  • 引用node的两种方法
// ①、在根节点之外使用 label 引用 node:&uart0 {
status = “disabled”;};// ②、在根节点之外使用全路径:&{
/uart@fe001000} {
status = “disabled”;};

2.3 dtsi文件和头文件

  dts 中可以包含.h 头文件,也可以包含 dtsi 文件,在.h 头文件中可以定义一些宏。dts 文件之所以支持“#include”语法,是因为 arm-linux-gnueabihf-gcc 帮忙。如果只用 dtc(类比gcc) 工具,它是不支持”#include”语法的,只支持“/include”语法。

/dts-v1/;#include 
#include "imx6ull.dtsi"/ {
……};

2.4 常用的属性(properties)

  • compatible
    • 根节点下的 compatible 属性
      • 用来选择哪一个“machine desc”: 一个内核可以支持machine A,也支持 machine B,内核启动后会根据根节点的 compatible 属性找到对应的machine desc (机器描述)结构体,执行其中的初始化函数。
      • compatible 的值,建议取这样的形式: “manufacturer,model”,即“厂家名,模块名”。
    • 设备节点下的compatible 属性
      • 用来表示兼容的驱动。比如对于某个 LED,内核中可能有 A、 B、 C 三个驱动都支持它,那可以这样写。内核启动时,就会为这个 LED 按这样的优先顺序为它找到驱动程序: A、 B、 C。
led {
compatible = “A”, “B”, “C”;};
  • model

    • model 用来准确地定义这个硬件是什么
    • 比如一个单板的设备文件的根节点下compatible属性,可以兼容内核中的“smdk2440”,也兼容“mini2440”。但是它到底是什么板?用 model 属性来明确。
    • 比如有 2 款板子配置基本一致, 它们的 compatible 是一样的,那么就通过 model 来分辨这 2 款板子
  • status

    • 最后追加的值会覆盖前面的值,即只有最后一次赋的值才有效!
    • dtsi 文件中定义了很多设备,但是在你的板子上某些设备是没有的。这时你可以给这个设备节点添加一个 status 属性,设置为“disabled”。
    • 通常使用的属性值:“okay”和“disabled”

示例

//address-cells 为 1,所以 reg 中用 1 个数来表示地址,即用 0x80000000 来表示地址//size-cells 为 1,所以 reg 中用 1 个数来表示大小,即用 0x20000000 表示大小/ {
#address-cells = <1>; #size-cells = <1>; memory {
reg = <0x80000000 0x20000000>; };};
  • #address-cells、 #size-cells

    • cell 指一个 32 位的数值
    • address-cells: address 要用多少个 32 位数来表示;size-cells: size 要用多少个 32 位数来表示。
  • reg

    • 本意是 register,用来表示寄存器地址。但在设备树里,它可以用来描述一段空间。
    • reg 属性的值,是一系列的“address size”,用多少个 32 位的数来表示 address 和 size,由其父节点的#address-cells、 #size-cells 决定。

示例:

/dts-v1/;/ {
#address-cells = <1>; #size-cells = <1>; memory {
reg = <0x80000000 0x20000000>; };};

对于 ARM 系统,寄存器和内存是统一编址的,即访问寄存器时用某块地址,访问内存时用某块地址,在访问方法上没有区别。

  • name(已过时)

    • 它的值是字符串,用来表示节点的名字。在跟 platform_driver 匹配时,优先级最低。compatible 属性在匹配过程中,优先级最高。
  • device_type(已过时)

    • 它的值是字符串,用来表示节点的类型。在跟 platform_driver 匹配时,优先级为中。compatible 属性在匹配过程中,优先级最高。

2.5 属性格式

  • 设备节点的属性形式是“name=value”,其中 value 有多种取值方式
    • arrays of cells,1个或多个cell【32 位】,用尖括号< >表示。
      • 如:interrupts = <17 0xc>;clock-frequency = <0x00000001 0x00000000>;
    • string(字符串),用" "表示。
      • 如:compatible = "simple-bus";
    • A bytestring(字节【8位】序列) ,用中括号[ ]表示。
      • 如:local-mac-address = [00 00 12 34 56 78]; // 每个 byte 使用 2 个 16 进制数来表示
    • 当然了也可以是以上三种方式的组合,用,分隔开每种表示方法。
      • 如: example = <0xf00f0000 19>, "a strange property format";

2.6 常用的节点(node)

  • 根节点 /

    • dts 文件中必须有一个根节点
    • 根节点中必须有这4个属性:#address-cells 、#size-cells、compatible、model
  • CPU节点

    • 一般不需要我们设置,在 dtsi 文件中都定义好了。
  • memory 节点

    • 芯片厂家不可能事先确定你的板子使用多大的内存,所以 memory 节点需要板厂设置.
  • chosen 节点

    • 可以通过设备树文件给内核传入一些参数,这要在 chosen 节点中设置 bootargs 属性。
chosen {
bootargs = "noinitrd root=/dev/mtdblock4 rw init=/linuxrc console=ttySAC0,115200";};

三、内核对设备树的处理

  • 如何查看设备树的节点信息

    • 系统启动后可以在根文件系统中看到设备树的节点信息:ls /proc/device-tree
  • 从源代码文件 dts 文件开始,设备树的处理过程为:

    • ① dts 在 PC 机上被编译为 dtb 文件;
    • ② u-boot 把 dtb 文件传给内核;
    • ③ 内核解析 dtb 文件,把每一个节点都转换为 device_node 结构体
    • ④ 对于某些 device_node 结构体,会被转换为 platform_device 结构体
      在这里插入图片描述
  • 根节点被保存在全局变量 of_root 中, 从 of_root 开始可以访问到任意节点。

3.1 谁可以转换为 platform_device?

  • 哪些设备树节点会被转换为 platform_device?
    • 情形A - 父节点是根节点:该节点含有compatile 属性即可转化为 platform_device
    • 情形B - 父节点非根节点:需要满足两个条件 ①、该节点还有compatile 属性 ②父节点 compatile 属性为下面4个之一:“simple-bus”,“simple-mfd”,“isa”,“arm,amba-bus”,才能转化为platform_device。
    • 情形C - 还需要记住一个:总线 I2C、SPI 节点下的子节点:不转换为 platform_device
      • 某个总线下的子节点, 应该交给对应的总线驱动程序来处理, 它们不应该被转换为platform_device。

示例讲解:

/ {
mytest {
compatile = "mytest", "simple-bus"; mytest@0 {
compatile = "mytest_0"; }; }; i2c {
compatile = "samsung,i2c"; at24c02 {
compatile = "at24c02"; }; }; spi {
compatile = "samsung,spi"; flash@0 {
compatible = "winbond,w25q32dw"; spi-max-frequency = <25000000>; reg = <0>; }; }; };

1、/mytest 会被转换为 platform_device(依据情形A)。它的子节点/mytest/mytest@0 也会被转换为 platform_device(依据情形B)。

2、/i2c 节点一般表示 i2c 控制器, 它会被转换为 platform_device, 在内核中有对应的platform_driver。/i2c/at24c02 节点不会被转换为 platform_device, 它被如何处理完全由父节点的platform_driver 决定, 一般是被创建为一个 i2c_client。
3、/spi 节点一般也是用来表示 SPI 控制器, 它会被转换为 platform_device,在内核中有对应的 platform_driver。/spi/flash@0 节点不会被转换为 platform_device, 它被如何处理完全由父节点的platform_driver 决定, 一般是被创建为一个 spi_device。

3.2 怎么转换为 platform_device?

  结论如下:

  • A. platform_device 中含有 resource 数组, 它来自 device_node 的 reg, interrupts 属性;
  • B. platform_device.dev.of_node 指向 device_node, 可以通过它获得其他属性

3.3 platform_device 如何与 platform_driver 配对?

  从设备树转换得来的 platform_device 会被注册进内核里,以后当我们每注册一个platform_driver 时, 它们就会两两确定能否配对,如果能配对成功就调用 platform_driver 的probe 函数。套路是一样的,我们需要将上一节谈到的“匹配规则”再完善一下。源码如下:

在这里插入图片描述

  • ①. 最先比较: platform_device. driver_override 和 platform_driver.driver.name
    • 可以设置 platform_device 的 driver_override,强制选择某个 platform_driver。
  • ②. 接下来比较:设备树信息
    • 比较: platform_device. dev.of_node 和 platform_driver.driver.of_match_table。
      • 首先,如果 of_match_table 中含有 compatible 值,就跟 dev 的 compatile 属性比较,若一致则成功,否则返回失败;
      • 其次,如果 of_match_table 中含有 type 值,就跟 dev 的 device_type 属性比较,若一致则成功,否则返回失败;
      • 最后,如果 of_match_table 中含有 name 值,就跟 dev 的 name 属性比较,若一致则成功,否则返回失败。
    • 而设备树中建议不再使用 devcie_type 和 name 属性,所以基本上只使用设备节点的compatible 属性来寻找匹配的 platform_driver
    • 由设备树节点转换得来的 platform_device 中,含有一个结构体: of_node,类型如下:
      在这里插入图片描述
    • 如果一个 platform_driver 支持设备树,它的 platform_driver.driver.of_match_table 是一个数组,类型如下:
      在这里插入图片描述
  • ③. 然后比较: platform_device. name 和 platform_driver.id_table[i].name
    • Platform_driver.id_table 是“platform_device_id”指针,表示该 drv 支持若干个 device,它里面列出了各个 device 的{.name, .driver_data}, 其中的“name”表示该 drv 支持的设备的名字, driver_data 是些提供给该 device 的私有数据。
  • ④. 最后比较: platform_device.name 和 platform_driver.driver.name
    • platform_driver.id_table 可能为空,这时可以根据 platform_driver.driver.name 来寻找同名的 platform_device。

一个图概括所有的配对过程

在这里插入图片描述

3.4 没有转换为 platform_device 的节点,如何使用?

  任意驱动程序里,都可以直接访问设备树。可以使用of相关函数找到节点,读出里面的值。具体内容参考4.3。

四、内核里操作设备树常用的of函数

4.1 内核中设备树相关的头文件介绍

  内核源码中 include/linux/目录下有很多 of 开头的头文件, of 表示“open firmware”即开

放固件。设备树的处理过程是: dtb -> device_node -> platform_device

4.1.1 处理 dtb

of_fdt.h 	// dtb 文件的相关操作函数, 我们一般用不到,			// 因为 dtb 文件在内核中已经被转换为 device_node 树(它更易于使用)

4.1.2 处理device_node

of.h 			// 提供设备树的一般处理函数,				// 比如 of_property_read_u32(读取某个属性的 u32 值),				// of_get_child_count(获取某个 device_node 的子节点数)of_address.h 	// 地址相关的函数,				// 比如 of_get_address(获得 reg 属性中的 addr, size 值)				// of_match_device (从 matches 数组中取出与当前设备最匹配的一项)of_dma.h 		// 设备树中 DMA 相关属性的函数of_gpio.h 		// GPIO 相关的函数of_graph.h 		// GPU 相关驱动中用到的函数, 从设备树中获得 GPU 信息of_iommu.h 		// 很少用到of_irq.h   		// 中断相关的函数of_mdio.h  		// MDIO (Ethernet PHY) APIof_net.h 		// OF helpers for network devices.of_pci.h 		// PCI 相关函数of_pdt.h 		// 很少用到of_reserved_mem.h // reserved_mem 的相关函数

4.1.3 处理 platform_device

of_platform.h 	// 把 device_node 转换为 platform_device 时用到的函数,				// 比如 of_device_alloc(根据 device_node 分配设置 platform_device),				// of_find_device_by_node (根据 device_node 查找到 platform_device),				// of_platform_bus_probe (处理 device_node 及它的子节点)of_device.h 	// 设备相关的函数, 比如 of_match_device

4.2 platform_device 相关的函数

  of_platform.h 中声明了很多函数,但是作为驱动开发者,我们只使用其中的 1、 2 个。其他的都是给内核自己使用的,内核使用它们来处理设备树,转换得到 platform_device。

4.2.1 of_find_device_by_node

函数原型为:

extern struct platform_device *of_find_device_by_node(struct device_node *np);

  设备树中的每一个节点,在内核里都有一个 device_node;你可以使用 device_node 去找到对应的 platform_device

4.2.2 platform_get_resource

  这个函数跟设备树没什么关系, 但是设备树中的节点被转换为 platform_device 后, 设备树中的 reg 属性、 interrupts 属性也会被转换为“resource”(呼应前面的:怎么转换为 platform_device?)。这时,你可以使用这个函数取出这些资源。

数原型为:

/*** platform_get_resource - get a resource for a device* @dev: platform device* @type: resource type 	// 取哪类资源? IORESOURCE_MEM、 IORESOURCE_REG* 						// IORESOURCE_IRQ 等* @num: resource index 	// 这类资源中的哪一个?*/struct resource *platform_get_resource(struct platform_device *dev,unsigned int type, unsigned int num);

对于设备树节点中的 reg 属性,它属性 IORESOURCE_MEM 类型的资源;

对于设备树节点中的 interrupts 属性,它属性 IORESOURCE_IRQ 类型的资源。

4.3 有些节点不会生成 platform_device,怎么访问它们?

  内核会把 dtb 文件解析出一系列的 device_node 结构体,我们可以直接访问这些device_node。内核源码 incldue/linux/of.h 中声明了 device_node 和属性 property 的操作函数,device_node 和 property 的结构体定义如下:

在这里插入图片描述

4.3.1 找到节点

  • of_find_node_by_path

    • 根据路径找到节点,比如“/”就对应根节点,“/memory”对应 memory 节点。
    • static inline struct device_node *of_find_node_by_path(const char *path);
  • of_find_node_by_name

    • 根据名字找到节点, 节点如果定义了 name 属性,那我们可以根据名字找到它。
    • extern struct device_node *of_find_node_by_name(struct device_node *from, const char *name);
      • 参数 from 表示从哪一个节点开始寻找,传入 NULL 表示从根节点开始寻找。
        但是在设备树的官方规范中不建议使用“name”属性,所以这函数也不建议使用
  • of_find_node_by_type

    • 根据类型找到节点,节点如果定义了 device_type 属性,那我们可以根据类型找到它。
    • extern struct device_node *of_find_node_by_type(struct device_node *from, const char *type);
      • 参数 from 表示从哪一个节点开始寻找,传入 NULL 表示从根节点开始寻找。
        但是在设备树的官方规范中不建议使用“device_type”属性,所以这函数也不建议使用
  • of_find_compatible_node

    • 根据 compatible 找到节点,节点如果定义了 compatible 属性,那我们可以根据
      compatible 属性找到它。
    • extern struct device_node *of_find_compatible_node(struct device_node *from, const char *type, const char *compat);
      • 参数 from 表示从哪一个节点开始寻找,传入 NULL 表示从根节点开始寻找。
      • 参数 compat 是一个字符串,用来指定 compatible 属性的值;
      • 参数 type 是一个字符串,用来指定 device_type 属性的值,可以传入 NULL。
  • of_find_node_by_phandle

    • 根据 phandle 找到节点。dts 文件被编译为 dtb 文件时,每一个节点都有一个数字 ID,这些数字 ID 彼此不同。可以使用数字 ID 来找到 device_node。这些数字 ID 就是 phandle。
    • extern struct device_node *of_find_node_by_phandle(phandle handle);
      • 参数 from 表示从哪一个节点开始寻找,传入 NULL 表示从根节点开始寻找。
  • of_get_parent

    • 找到 device_node 的父节点。
    • extern struct device_node *of_get_parent(const struct device_node *node);
      • 参数 from 表示从哪一个节点开始寻找,传入 NULL 表示从根节点开始寻找。
  • of_get_next_parent

    • 这个函数名比较奇怪,怎么可能有“next parent”?它实际上也是找到 device_node 的父节点,跟 of_get_parent 的返回结果是一样的。差别在于它多调用下列函数,把 node 节点的引用计数减少了 1。这意味着调用of_get_next_parent 之后,你不再需要调用 of_node_put 释放 node 节点。
    • extern struct device_node *of_get_next_parent(struct device_node *node);
      • 参数 from 表示从哪一个节点开始寻找,传入 NULL 表示从根节点开始寻找。
  • of_get_next_child

    • 取出下一个子节点。
    • extern struct device_node *of_get_next_child(const struct device_node *node, struct device_node *prev);
      • 参数 node 表示父节点;prev 表示上一个子节点,设为 NULL 时表示想找到第 1 个子节点。
      • 不断调用 of_get_next_child 时,不断更新 pre 参数, 就可以得到所有的子节点。
  • of_get_next_available_child

    • 取出下一个“可用”的子节点,有些节点的 status 是“disabled”,那就会跳过这些节点。
    • struct device_node *of_get_next_available_child(const struct device_node *node, struct device_node *prev);
      • 参数 node 表示父节点;prev 表示上一个子节点,设为 NULL 时表示想找到第 1 个子节点。
  • of_get_child_by_name

    • 根据名字取出子节点.

    • extern struct device_node *of_get_child_by_name(const struct device_node *node, const char *name);

      • 参数 node 表示父节点;name 表示子节点的名字。

4.3.2 找到属性

  内核源码 incldue/linux/of.h 中声明了 device_node 的操作函数,当然也包括属性的操作函数。

  • of_find_property
    • 找到节点中的属性。
    • extern struct property *of_find_property(const struct device_node *np, const char *name, int *lenp);
      • 参数 np 表示节点,我们要在这个节点中找到名为 name 的属性。lenp 用来保存这个属性的长度, 即它的值的长度。

举个例子,在设备树中,节点大概是这样:

xxx_node {
xxx_pp_name = “hello”;};

上述节点中,“xxx_pp_name”就是属性的名字,值的长度是 6

4.3.3 获取属性的值

  • of_get_property

    • 根据名字找到节点的属性,并且返回它的值
    • const void *of_get_property(const struct device_node *np, const char *name, int *lenp)
      • 参数 np 表示节点,我们要在这个节点中找到名为 name 的属性,然后返回它的值。lenp 用来保存这个属性的长度, 即它的值的长度。
  • of_property_count_elems_of_size

    • 根据名字找到节点的属性,确定它的值有多少个元素(elem)。
    • int of_property_count_elems_of_size(const struct device_node *np, const char *propname, int elem_size)
      • 参数 np 表示节点,我们要在这个节点中找到名为 propname 的属性,然后返回下列结果:
      • return prop->length / elem_size;

举个例子,在设备树中,节点大概是这样:

xxx_node {
xxx_pp_name = <0x50000000 1024> <0x60000000 2048>;};

调用 of_property_count_elems_of_size(np, “xxx_pp_name”, 8)时,返回值是 2 - ?(疑问)

调用 of_property_count_elems_of_size(np, “xxx_pp_name”, 4)时,返回值是 4 - ?(疑问)

  • 读整数 u32/u64
    • static inline int of_property_read_u32(const struct device_node *np, const char *propname, u32 *out_value);
    • extern int of_property_read_u64(const struct device_node *np, const char *propname, u64 *out_value);

举个例子,在设备树中,节点大概是这样

xxx_node {
name1 = <0x50000000>; name2 = <0x50000000 0x60000000>;};

调用 of_property_read_u32 (np, “name1”, &val)时, val 将得到值 0x50000000;

调用 of_property_read_u64 (np, “name2”, &val)时, val 将得到值0x0x6000000050000000。

  • 读某个整数 u32/u64
    • extern int of_property_read_u32_index(const struct device_node *np, const char *propname, u32 index, u32 *out_value)

举个例子,在设备树中,节点大概是这样:

xxx_node {
name2 = <0x50000000 0x60000000>;};

调用 of_property_read_u32 (np, “name2”, 1, &val)时, val 将得到值 0x0x60000000。?(疑问)

  • 读数组
int of_property_read_variable_u8_array( const struct device_node *np,										const char *propname, u8 *out_values,										size_t sz_min, size_t sz_max);int of_property_read_variable_u16_array(const struct device_node *np,										const char *propname, u16 *out_values,										size_t sz_min, size_t sz_max);int of_property_read_variable_u32_array(const struct device_node *np,										const char *propname, u32 *out_values, 										size_t sz_min, size_t sz_max);int of_property_read_variable_u64_array(const struct device_node *np,										const char *propname, u64 *out_values,										size_t sz_min, size_t sz_max);

举个例子,在设备树中,节点大概是这样

xxx_node {
name2 = <0x50000012 0x60000034>;};

上述例子中属性 name2 的值,长度为 8。

调用 of_property_read_variable_u8_array (np, “name2”, out_values, 1, 10)时, out_values
中将会保存这 8 个字节: 0x12,0x00,0x00,0x50,0x34,0x00,0x00,0x60。

调用 of_property_read_variable_u16_array (np, “name2”, out_values, 1, 10)时, out_values中将会保存这 4 个 16 位数值: 0x0012, 0x5000,0x0034,0x6000。

总之,这些函数要么能取到全部的数值,要么一个数值都取不到;如果值的长度在 sz_min 和 sz_max 之间,就返回全部的数值;否则一个数值都不返回。

  • 读字符串
    • int of_property_read_string(const struct device_node *np, const char *propname, const char **out_string);
      • 返回节点 np 的属性(名为 propname)的值, (*out_string)指向这个值,把它当作字符串。

4.4 如何修改设备树?

  一个写得好的驱动程序, 它会尽量确定所用资源。只把不能确定的资源留给设备树, 让设备树来指定。根据原理图确定"驱动程序无法确定的硬件资源", 再在设备树文件中填写对应内容。那么, 所填写内容的格式是什么?

4.4.1 使用芯片厂家提供的工具

  有些芯片,厂家提供了对应的设备树生成工具,可以选择某个引脚用于某些功能,就可以自动生成设备树节点。你再把这些节点复制到内核的设备树文件里即可。

4.4.2 看绑定文档

内核文档 Documentation/devicetree/bindings/

做得好的厂家也会提供设备树的说明文档

4.4.3 参考同类型单板的设备树文件

4.4.4 网上搜索

4.4.5 实在没办法时, 只能去研究驱动源码


  本节,我们初步介绍了设备树的引入,设备树的作用,设备树的基本语法以及设备树常用的of函数。关于of函数当然有很多,我们不可能每个都要记住,还是那个原则,Learn by doing,做着学着,做着记着!下一节我们将通过设备树实际操作LED,来吧,一起感受下设备树的魅力吧!

转载地址:http://rfnaf.baihongyu.com/

你可能感兴趣的文章
02.HTML中使用JavaScript--JavaScript高级程序设计(笔记)
查看>>
05.看板方法——持续改进的文化
查看>>
欢迎使用CSDN-markdown编辑器
查看>>
bash shell简述
查看>>
bash 重定向
查看>>
bash 命令执行环境
查看>>
物理内存的管理
查看>>
高效能人士的七个习惯——由内而外全面造就自己
查看>>
为什么精英都是清单控(笔记)——工作清单
查看>>
怦然心动的人生整理魔法(笔记)——物品类别整理
查看>>
让人生发生戏剧性变化的整理魔法(笔记)
查看>>
按物品类别整理的心动收纳法(笔记)
查看>>
番茄工作图解——序(笔记)
查看>>
每天最重要的2小时——序(笔记)
查看>>
29.开源项目--git远程仓库的概念
查看>>
36.开源项目--git搭建本地Git服务器
查看>>
01.创新与企业家精神——创新实践
查看>>
17.创新与企业家精神——攻其软肋
查看>>
14.openssl编程——错误处理
查看>>
29.openssl编程——PKCS7
查看>>