Linux驱动学习—设备树及设备树下的platform总线

1、什么是设备树?

设备树是一种描述硬件资源的数据结构。他通过bootloader将硬件资源传给内核,使得内核和硬件资源 描述相对独立。

2、设备树的由来

2.1 平台总线的由来

要想了解为什么会有设备树,设备树是怎么来的,我们就要先来回顾以下在没有设备树之前我们是怎么来写一个驱动程序的。以字符设备驱动代码框架为例,我们一起一起来回顾下。任何的设备驱动的编写,Linux已经为我们打好了框架,我们只需要做完形填空一样填写就可以了。

下面是注册字符设备驱动框架图:

具体过程可以参考下面这篇文章:

Linux驱动学习—字符设备驱动注册详解-CSDN博客

下面是注册杂项设备驱动框架图:

有关杂项设备驱动注册流程可以参考下面这篇:

Linux驱动学习—杂项设备驱动注册-CSDN博客

通过这些框架,我们可以很容易编写我们 的驱动代码,但是,当我们用这个框架非常熟练的时候,我们就会发现虽然这个方法很简单,但是非常不容易扩展,当有很多很多类似的设备的时候,如果我们都是按照这个框架来完成,那就要写很多遍这个流程,但是多个相似设备之间真正差异的地方只有初始化硬件部分,其他步骤的代码基本都是一样的。这样就会造成大量的重复代码。但是,我们在编写驱动代码的时候,要尽量做到代码的复用,也就是一套驱动尽量可以兼容很多设备,如果我们还按照这个来编写就不太符合我们的规则了。

为了实现这个目标,我们就要吧通用的代码和有差异的代码分离出来,来增强我们驱动代码的可移植性。所以,设备驱动分离的思想就应运而生了,在Linux中,我们是在写代码的时候进行分离。分离是把一些不相似的东西放到device.c,把相似的东西放到driver.c,如果有很多相似的设备或者平台,我们只要修改device.c就可以了,这样我们重复性的工作就大大的减少了。这就是平台总线的由来。

2.2 平台总线这个方法有什么弊端呢?(设备树由来)

当我们用这个方法用习惯以后就会发现,假如soc不变,我们每换一个平台,都要修改C文件,并且还要重新编译。而且会在arch/arm/plat-xxx和arch/arm/mach-xxx下面留下大量关于板级细节的代码。并不是说这个方法不好,只是从Linux的发展来看,这些代码相对于Linux内核来说就是“垃圾代码”,而且这些“垃圾代码”非常多。

为了改变这个现状,设备树也就被引进到Linux上了,用来剔除相对内核来说的“垃圾代码”,即用设备树文件来描述这些设备信息,也就是代替device.c文件,虽然拿到了内核外面,但是platform匹配基本不变,并且相比于之前的方法,使用设备树不仅可以去掉大量的“垃圾代码”,并且采用文本格式,方便阅读和修改,如果需要修改部分资源,我们也不用在重新编译内核了,只需要把设备树源文件编译成二进制文件,在通过bootloader传递给内核就可以了。内核对其进行解析和展开得到关于硬件的拓扑图。我们通过内核提供的接口剖获取设备树的节点和属性就可以了。即内核对于同一soc的不同主板,只需要换设备树文件dtb即可实现不同主板的无差异支持,而无需更换内核文件。

3、设备树的基本概念

3.1 为啥叫设备树呢?

因为他的语法结构像树一样,所以管它叫设备树

3.2常用名词解释

<1>DT:Device Tree           //设备树
<2>FDT:Flattened Device Tree//展开设备树//开放固件,设备树起源于OF,所以我们在设备树中可以看到很多of字母的函数
<3>device tree source(dts)  //设备树代码
<4>device tree source includeDTB(dtsi) //更通用的设备树代码,也就是相同芯片但不同平台都可以使用的代码
<5>device tree blob(dtb)    //DTS编译后得到的DTB文件
<6>device tree complier(dtc) //设备树编译器

dtsi:一个 SOC 可以作出很多不同的板子,这些不同的板子肯定是有共同的信息, 将这些共同的信息提取出来作为一个通用的文件,其他的.dts 文件直接引用这个通用文件即可,这个通用文件就是.dtsi 文件,类似于 C 语言中的头文件。

DTS,DTSI,DTB,DTC他们之间的关系如下:

4、设备树基本语法

4.1 设备树基本框架

<1>设备从根节点开始,每个设备都是节点。
<2>节点和节点之间可以相互嵌套,形成父子关系。
<3>设备的属性用key-value对(键值对)来描述,每个属性用分号结束

4.2 设备树语法

4.2.1节点

什么是节点呢?节点就好比一颗大树,从树的主干开始,然后有一节一节的树枝,这个就叫节点。在代码中的节点是什么样子的呢。我们把上面模板的根节点摘出来,如下所示,这个就是根节点。相当于大树的树干。

/{
};//分号

而树枝就相当于设备树的子节点,同样我们把子节点摘出来就是根节点里面的node1和node2,如下所示:

/{  //根节点node1//子节点node1{};node2//子节点node2{};
};//分号

一个树枝是不是也可以继续分成好几个树枝呢,也就是说子节点里面可以包含子子节点。所以child-node1和child-node2是node1和node2的子节点,如下所示:

/{  //根节点node1//子节点node1{child-node1//子子节点{};};node2//子节点node2{child-node2//子子节点{};};
};//分号
4.2.2 节点名称

节点的命名有一个固定的格式。

格式:<名称>[@<设备地址>]

(1)<名称>节点的名称也不是任意起的,一般要体现设备的类型而不是特点的型号,比如网口,应该命名为ethernet,而不是随意起一个,比如111。

(2)<设备地址>就是用来访问该设备的基地址。但并不是说在操作过程中来描述一个地址,其主要用来区分用。

(3)注意事项:A、同一级的节点只要地址不一样,名称是可以不唯一的。

B、设备地址是一个可选选项,可以不写。但为了任意区分和理解,一般是都写的。

4.2.3 节点别名

当我们找一个节点的时候,必须书写完整的节点路径,如果节点名很长,那么我们在引用的时候就十分不方便,所以,设备树允许我们用下面的形式为节点标注引用(起别名)。举例:

uart8:serial@02288000

其中uart8就是这个节点名称的别名,serial@02288000就是节点名称。

4.2.4 节点引用

一般往节点里面添加内容的时候,不会直接把直接添加的内容写到节点里面,而是通过节点的引用来添加。

举例:

&uart8{pinctrl-names = "default";pinctrl-0 = <&pinctrl_uart8>;status = "okay";
};

&uart8表示引用节点别名为uart8的节点。并往节点添加以下内容:

    pinctrl-names = "default";pinctrl-0 = <&pinctrl_uart8>;status = "okay";

注意事项:编译设备树的时候,相同的节点的不同属性信息都会被合并,相同节点的先相同的属性会被重写,使用引用可以避免移植者四处找节点。如dts和dtsi里面都有根节点,但最终会合并成一个根节点。

4.2.5属性
(1)reg属性

reg属性用来描述一个设备的地址范围。格式:

reg=<add1 lenth1 [add2 length2]...>

举例:

serial@02288000{reg=<101F2000 0x1000>;//101F2000是起始地址,0x1000是长度
};
(2)#address-cells和#size-cells属性

#address-cells用来设置子节点中reg地址的数量

#size-cells用来设置子节点中reg地址长度的数量

举例:

cpu{#address-cells = <1>;//用来设置子节点中reg地址的数量#size-cells = <1>;//用来设置子节点中reg地址长度的数量serial@02288000{reg=<101F2000 0x1000>;//101F2000是起始地址,0x1000是长度};
};

其中#address-cells和#size-cells均为1,也就是说我们子节点里面的reg属性里这个寄存器组的起始地址只有一个,长度也只有一个。所以101F2000是起始地址,0x1000是长度。

(3)compatible属性

compatible是一个自负床列表,可以在代码中进行匹配。

举例:

compatible = “led";
(4)status属性

status属性的值类型是字符串,这里我们只要记住两个常用的即可,一个是okay,表示涉笔可以正常使用,一个disable,表示设备不能正常使用。

5、在设备树中添加自定义节点

5.1 命令查看设备树节点

<1> cd /proc/device-tree/下就可看到
<2> cd /sys/firmware/devicetree/base/下就可看到

这是设置uboot环境变量的:

5.2 安装dtc工具

(1)直接make dtbs出现这种情况,说明环境没有配置对,则需要安装dtc工具

(2)安装dtc工具

apt-get install device-tre-compiler

5.3 实验:添加一个节点

如下图,添加一个节点test,对这个节点取别名为test1,然后节点引用&test1,一般往节点里面添加内容的时候,不会直接把直接添加的内容写到节点里面,而是通过节点的引用来添加。所以最终的compatible和status属性是节点引用里面的内容。

添加完之后编译dts,在内核源码路径下输入以下命令即可编译:

make ARCH=arm CROSS_COMPILE=arm-linux-guneabihf-  dtbs

把编译的dtb烧录到开发板,cd /proc/device-tree/目录下可以看到test节点已经生成。cat /proc/devicetree/test/compatible发现是test1234,cat /proc/devicetree/test/status发现是okay。

6、设备树中常见的of操作函数

设备都是以节点的形式“挂”到设备树上的,因此姚秀昂获取这个设备的其他属性信息,必须先获取到这个设备的节点。linux内核实验device_node结构体来描述一个节点,此结构体的第一在文件include/linux/of.h中,如下:

struct device_node {const char *name;//节点名字const char *type;//设备类型phandle phandle;const char *full_name;//节点全名struct fwnode_handle fwnode;
​struct  property *properties;//属性struct  property *deadprops;    /* removed properties */struct  device_node *parent;//父节点struct  device_node *child;//子节点struct  device_node *sibling;struct  kobject kobj;unsigned long _flags;void    *data;
#if defined(CONFIG_SPARC)const char *path_component_name;unsigned int unique_id;struct of_irq_controller *irq_trans;
#endif
};

节点的属性信息里面保存了驱动所需要的内容,因此对于属性值的提取非常重要,Linux内核中使用结构体property表示属性,此结构体同样定义在include/linux/of.h中,如下:

struct property {char    *name;//属性名字int length;//属性长度void    *value;//属性值struct property *next;//下一个属性unsigned long _flags;unsigned int unique_id;struct bin_attribute attr;
};

6.1获取设备树文件节点里面资源的步骤

<1>步骤一:查找我们要找的节点。

<2>步骤一:查找我们要找的属性值。

6.2 查找节点常用的of函数

<1>of_find_node_by_path函数

作用:函数通过路径来查找指定的节点。

函数原型:

static inline struct device_node *of_find_node_by_path(const char *path)
参数:
path:带有全路径的节点名,可以使用节点的别名,比如"/test"就是test这个节点的全路径。使用节点别名的路径是"/test1".
返回值:成功就返回找到的节点。失败则返回NULL。
<2>of_get_parent函数

作用:用于获取节点的父节点(如果有父节点的话)。

struct device_node *of_get_parent(const struct device_node *node);
node:要查找的父节点的节点
返回值:找到的父节点。
<3>of_get_next_child函数

作用:用于迭代的查找子节点。

static inline struct device_node *of_get_next_child(const struct device_node *node, 
struct device_node *prev)
参数如下:
node:父节点。
prev:前一个子节点,也就是从哪一个子节点开始迭代的查找下一个子节点。可以设置为NULL表示从第一个子节点开始。
返回值:找到的下一个子节点。

6.3 查找节点属性常用的of函数

<1>of_get_property函数

作用:用于查找指定的属性。

static inline const void *of_get_property(const struct device_node *node,const char *name,int *lenp)
参数如下:   
np:设备节点。
name:属性名字。
lenp:属性值的字节数。
返回值:找到的属性。
<2> of_property_read_u8、of_property_read_u16、of_property_read_u32、of_property_read_u64

有些属性只有一个整型值,者四个函数就是用于读取这种只有一个整型值的属性,分别用于读取u8、u16、u32、u64类型属性值,函数原型如下:

static inline int of_property_read_u8(const struct device_node *np,const char *propname,u8 *out_value);
static inline int of_property_read_u16(const struct device_node *np,const char *propname,u16 *out_value);
static inline int of_property_read_u32(const struct device_node *np,const char *propname,u32 *out_value);
static inline int of_property_read_u64(const struct device_node *np,const char *propname, u64 *out_value);
参数如下:
np:设备节点。
proname:要读取的属性名字。
out_value:读取的值。
返回值:0,读取成功,负值,读取失败。
<3>of_property_read_u8_array、of_property_read_u16_array、of_property_read_u32_array、of_property_read_u64_array

这四个函数分别是读取属性中u8、u16、u32、u64类型的数组数据,比如大多数的reg属性都是数组数据,可以使用这4个函数一次读取reg属性中的所有数据。这四个函数的原型如下:

static inline int of_property_read_u8_array(const struct device_node *np,const char *propname,u8 *out_values, size_t sz);
static inline int of_property_read_u16_array(const struct device_node *np,const char *propname, u16 *out_values, size_t sz);
static inline int of_property_read_u32_array(const struct device_node *np,const char *propname, u32 *out_values, size_t sz);
static inline int of_property_read_u64_array(const struct device_node *np,const char *propname, u64 *out_values, size_t sz);
参数:
np:设备节点。
proname:要读取的属性名字。
out_values:读取的数组值。
返回值:0,读取成功,负值,读取失败。
<4>of_property_read_string

作用:用于读取属性只呢个字符串值

int of_property_read_string(const struct device_node *np,const char *propname,const char **out_string);
参数:
np:设备节点。
proname:要读取的属性名字。
out_values:读取的字符串值。
返回值:0,读取成功,负值,读取失败。     
 

6.4 实验:把5.3添加的一个节点的值和属性读取出来

 
#include <linux/init.h>
#include <linux/module.h>
#include <linux/of.h>
​
struct device_node *test_device_node;
struct property *test_node_property;
int size;
u32 out_values[2]={0};
const char *str=NULL;
​
static int hello_init(void)
{int ret = 0;printk("hello_init\n");//查找要查找的节点test_device_node = of_find_node_by_path("/test");if(test_device_node == NULL) {printk("test_device_node find error\n");return -1;}printk("test_device_node name is %s\n",test_device_node->name);//test//获取compatible属性内容test_node_property = of_find_property(test_device_node, "compatible", &size);if(test_node_property == NULL) {printk("test_node_property find error\n");return -1;}printk("test_node_property name is %s\n",test_node_property->name);//compatibleprintk("test_node_property->value is %s\n",test_node_property->value);//test1234//获取reg属性内容ret = of_property_read_u32_array(test_device_node, "reg", out_values, 2);if(ret < 0) {printk("of_property_read_u32_array is error\n");return -1;}printk("out_values[0]  is 0x%08x\n",out_values[0]);//0x020ac000printk("out_values[1]  is 0x%08x\n",out_values[1]);//0x00000004//获取status属性内容ret = of_property_read_string(test_device_node, "status", &str);if(ret < 0) {printk("of_property_read_string is error\n");return -1;}printk("status is %s\n",str);//okayreturn 0;
}
​
static void hello_exit(void) 
{printk("hello_exit\n");
}
​
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");

加载驱动,打印如下:

7、设备树下的platform总线

7.1 传统方法下的platform总线

Linux驱动学习—平台总线模型-CSDN博客

之前这篇文章中是使用传统的方法对平台总线进行学习,什么是传统方法呢,就是硬件设备信息部分写在device.c,驱动部分写在driver.c中。而设备树下的platform总线 则是用设备树文件代替device.c。所以使用设备树的方法在配置好设备树文件后,只需编写driver.c。

7.2 of_iomap函数

作用:of_iomap函数用于直接内存映射,以前我们会通过ioremap函数来完成物理地址到虚拟地址的映射。

函数原型:

void __iomem *of_iomap(struct device_node *node, int index);
参数:
np:设备节点
index:reg属性中要完成内存映射的段,如果reg属性只有一段的话inbdex就设置0。
返回值:经过内存映射后的虚拟内存首地址,如果为NULL的话就表示内存映射失败。

7.3 实验代码

Linux驱动学习—平台总线模型-CSDN博客

直接在上面文章的4.3小节platform driver.c上修改,主要实现的功能就是映射GPIO5的数据寄存器的内存地址,实现对数据寄存器的操作,从而实现对蜂鸣器引脚控制。

#include <linux/init.h>
#include <linux/module.h>
#include <linux/platform_device.h> 
#include <linux/of.h>
#include <linux/of_address.h>
​
struct device_node *test_device_node;
struct property *test_node_property;
int size;
u32 out_values[2]={0};
const char *str=NULL;
unsigned int *vir_gpio_dr;
​
static const of_device_id of_match_table_test[] = {//匹配表{.compatible = "test1234"},
};
​
static const platform_device_id beep_id_table ={.name = "beep_test",
};
​
/*设备树节点compatible属性与of_match_table_test的compatible相匹配就会进入该函数,pdev是匹配成功后传入的设备树节点*/
int beep_probe(struct platform_device *pdev)
{int ret = 0;printk("beep_probe\n");/*//查找要查找的节点 pdev是匹配成功后传入的设备树节点,所以不需要用之前的方法进行查找了test_device_node = of_find_node_by_path("/test");if(test_device_node == NULL) {printk("test_device_node find error\n");return -1;}printk("test_device_node name is %s\n",test_device_node->name);//test//获取compatible属性内容test_node_property = of_find_property(test_device_node, "compatible", &size);if(test_node_property == NULL) {printk("test_node_property find error\n");return -1;}printk("test_node_property name is %s\n",test_node_property->name);//compatibleprintk("test_node_property->value is %s\n",test_node_property->value);//test1234*///获取reg属性内容ret = of_property_read_u32_array(pdev->dev.of_node, "reg", out_values, 2);if(ret < 0) {printk("of_property_read_u32_array is error\n");return -1;}printk("out_values[0]  is 0x%08x\n",out_values[0]);//0x020ac000printk("out_values[1]  is 0x%08x\n",out_values[1]);//0x00000004vir_gpio_dr = of_iomap(pdev->dev.of_node, 0);if(vir_gpio_dr == NULL) {printk("of_iomap  error\n");return -1;}return 0;
}
​
int beep_remove(struct platform_device *pdev)
{pritnk("beep_remove \n");return 0;
}
​
strcut platform_driver beep_device = {.probe = beep_probe,.remove = beep_remove,.driver = {.owner = THIS_MODULE,.name  = "123",.of_match_table = of_match_table_test,//匹配表 },.id_table = &beep_id_table,
};
​
static int beep_driver_init(void)
{int ret = -1;ret = platform_driver_register(&beep_device);if(ret < 0) {printk("platform_driver_register error \n");}printk("platform_driver_register ok\n");return 0;
}
​
static void  beep_driver_exit(void)
{platform_driver_unregister(&beep_device);printk("beep_driver_exit \n");
}
​
module_init(beep_driver_init);
module_exit(beep_driver_exit);
MODULE_LICENSE("GPL");

编译加载驱动,这样寄存器地址就获取成功了,我们可以注册一个杂项设备对数据引脚进行操作高低电平,从而实现对蜂鸣器的操作。

*vir_gpio_dr |= (1<<1);
*vir_gpio_dr &= ~(1<<1);

7.3.1 匹配优先级

先是platform_driver.driver.of_match_table.compatible,然后是platform_driver.driver.id_table,最后是platform_driver.driver.name

注意:设备树节点compatible属性与of_match_table_test的compatible相匹配就会进入probe函数,其函数参数pdev是匹配成功后传入的设备树节点

7.3.2 reg有多组参数时,of_iomap如何传参

当然,如果reg有多组参数的话,这一个是不一样的,举个例子:

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/312772.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

WPF 新手指引弹窗

新手指引弹窗介绍 我们在第一次使用某个软件时&#xff0c;通常会有一个“新手指引”教学引导。WPF实现“新手指引”非常方便&#xff0c;且非常有趣。接下来我们就开始制作一个简单的”新手指引”(代码简单易懂&#xff0c;便于移植)&#xff0c;引用到我们的项目中又可添加一…

认识Linux基本指令之 “touch mkdir rm”

01.touch指令 语法:touch [选项]... 文件... 功能&#xff1a;touch命令参数可更改文档或目录的日期时间&#xff0c;包括存取时间和更改时间&#xff0c;或者新建一个不存在的文件 常用选项&#xff1a; -a 或--timeatime或--timeaccess或--timeuse只更改存取时间。 -c…

node版本管理器nvm的下载和使用

介绍 nvm 全名 node.js version management&#xff0c;顾名思义是一个nodejs的版本管理工具。通过它可以安装和切换不同版本的nodejs。 下载和安装 在下载和安装nvm前&#xff0c;需要确保当前电脑没有安装node&#xff0c;否则则需要先把原来的node卸载了。 下载地址&#…

【JavaFX】基于JavaFX11 构建可编辑、对象存储、修改立即保存、支持条件过滤的TableView

文章目录 效果设计思路二、使用步骤1. 创建实体类2.读取本地文件数据3. 定义表格TableView总结效果 如图所示,这是一个存储application.properties内容的表格。这里的文件application.properties是从Linux服务器上获取来的。 当点击检索按钮,并输入条件匹配字符时,TableVie…

【2024最新版】neo4j安装配置

neo4j安装 写在最前面下载配置环境&#xff08;还是不行&#xff1f;&#xff09;启动neo4jpython中调用 写在最前面 之前我安装过&#xff0c;还写了一篇笔记 结果意外发现没有了&#xff0c;而且和之前安装的步骤不一样了&#xff0c;因此再次记录安装过程 下载 https://ne…

Plantuml之JSON数据语法介绍(二十五)

简介&#xff1a; CSDN博客专家&#xff0c;专注Android/Linux系统&#xff0c;分享多mic语音方案、音视频、编解码等技术&#xff0c;与大家一起成长&#xff01; 优质专栏&#xff1a;Audio工程师进阶系列【原创干货持续更新中……】&#x1f680; 优质专栏&#xff1a;多媒…

x-cmd pkg | bat - cat 命令的现代化替代品

目录 简介首次用户功能特点进一步阅读 简介 bat 是 cat 命令的替代品&#xff0c;对 cat 命令进行功能扩展&#xff0c;如语法高亮、自动分页等&#xff0c;为用户提供更友好的显示和定制选项。对于需要在终端频繁查看文本内容的用户&#xff0c;推荐用 bat。 首次用户 使用 …

【网络面试(3)】浏览器委托协议栈完成消息的收发

前面的博客中&#xff0c;提到过很多次&#xff0c;浏览器作为应用程序&#xff0c;本身是不具备向网络中发送网络请求的能力&#xff0c;要委托操作系统的内核协议栈来完成。协议栈再调用网卡驱动&#xff0c;通过网卡将请求消息发送出去&#xff0c;本篇博客就来探讨一下这个…

【操作系统】虚拟存储器

5.1 虚拟存储器概述 之前介绍的各种存储器管理方式有一个共同的特点&#xff0c;即它们都要求将一个作业全部装入内存后方能运行。于是&#xff0c;出现了下面这样两种情况&#xff1a; (1) 有的作业很大&#xff0c;其所要求的内存空间超过了内存总容量&#xff0c;作业不能全…

CDH 6.3.2集成flink 1.18 zookeeper版本不匹配Flink-yarn启动失败

CDH 6.3.2集成flink 1.18 zookeeper版本不匹配Flink-yarn不能正常启动&#xff0c;而在CHD Web页面&#xff0c;flink日志报错提示不明确&#xff0c;不能定位具体错误。CM WEB启动失败错误日志如下图所示&#xff1a; CM查看完成错误日志 [31/Dec/2023 10:45:09 0000] 26000…

EOS链Ubuntu环境Install Prebuilt Binaries(安装预构建的二进制文件)的安装

[TOC](EOS链Ubuntu环境Install Prebuilt Binaries(安装预构建的二进制文件)的安装) EOS官网&#xff1a;https://eos.io/ 第一步 Ubuntu安装命令&#xff1a; 以下有两种安装方式&#xff0c;可以任选其一&#xff1a; 本文章已经上传绑定资源&#xff0c;也可以用命令安装。…

使用.Net nanoFramework 驱动ESP32的OLED显示屏

本文介绍如何使用.Net nanoFramework 驱动ESP32的OLED显示屏。我们将会从最基础的部分开始&#xff0c;逐步深入&#xff0c;让你能够理解并实现整个过程。无论你是初学者还是有一定经验的开发者&#xff0c;这篇文章都会对你有所帮助。 1. 硬件准备 1.1 ESP32开发板 这里我们…