授人以鱼不如授人以渔,目的不是为了教会你具体项目开发,而是学会学习的能力。希望大家分享给你周边需要的朋友或者同学,说不定大神成长之路有博哥的奠基石。。。
QQ技术互动交流群:ESP8266&32 物联网开发 群号622368884,不喜勿喷
一、你如果想学基于Arduino的ESP8266开发技术
一、基础篇
二、网络篇
- ESP8266开发之旅 网络篇① 认识一下Arduino Core For ESP8266
- ESP8266开发之旅 网络篇② ESP8266 工作模式与ESP8266WiFi库
- ESP8266开发之旅 网络篇③ Soft-AP——ESP8266WiFiAP库的使用
- ESP8266开发之旅 网络篇④ Station——ESP8266WiFiSTA库的使用
- ESP8266开发之旅 网络篇⑤ Scan WiFi——ESP8266WiFiScan库的使用
- ESP8266开发之旅 网络篇⑥ ESP8266WiFiGeneric——基础库
- ESP8266开发之旅 网络篇⑦ TCP Server & TCP Client
- ESP8266开发之旅 网络篇⑧ SmartConfig——一键配网
- ESP8266开发之旅 网络篇⑨ HttpClient——ESP8266HTTPClient库的使用
- ESP8266开发之旅 网络篇⑩ UDP服务
- ESP8266开发之旅 网络篇⑪ WebServer——ESP8266WebServer库的使用
- ESP8266开发之旅 网络篇⑫ 域名服务——ESP8266mDNS库
- ESP8266开发之旅 网络篇⑬ SPIFFS——ESP8266 Flash文件系统
- ESP8266开发之旅 网络篇⑭ web配网
- ESP8266开发之旅 网络篇⑮ 真正的域名服务——DNSServer
- ESP8266开发之旅 网络篇⑯ 无线更新——OTA固件更新
三、应用篇
四、高级篇
笔者本书的主题是基于Arduino平台来开发ESP8266。那么从另外一个角度来看待这句话,可以理解为:把ESP8266当作一款类似于Arduino UNO型号(为什么不是Mega2560呢?可以从硬件资源方向考虑)的Arduino开发板,用Arduino平台的开发方式来开发项目,只不过ESP8266是在Arduino UNO的基础上加了网络功能。
本章将介绍ESP8266作为Arduino UNO开发板的一些重要开发知识点。
主要分为8个部分:
1.ESP8266 Arduino程序结构
2.计时和延时(Timing and delays)
3.NodeMcu 端口映射
4.数字IO(Digital IO)
5.中断功能
6.模拟输入(ADC)
7.模拟输出(PWM)
8.串口通信(Serial)
1. Arduino程序结构
在第2章中,笔者提供了一个测试用例,让我们来回顾一下,代码如下:
/**
* Demo:
* 测试ESP8266 demo
* 打印ESP8266模块信息
* 1.打印Arduino Core For ESP8266 版本,笔者是2.4.2版本
* 2.打印Flash的唯一性芯片id(读者可以思考一下是否可以用来做点什么唯一性参考)
* 3.打印Flash实际大小
* 4.打印IDE配置的使用Flash大小
* 5.打印IDE配置的Flash连接通信的频率
* 6.打印Flash连接模式:QIO QOUT DIO DOUT,可以理解为Flash传输速率
* @author 单片机菜鸟
* @date 2018/10/22
*/
void setup() {
// put your setup code here, to run once:
Serial.begin(115200);
//使能软件看门狗的触发间隔
ESP.wdtEnable(5000);
}
void loop() {
//喂狗
ESP.wdtFeed();
FlashMode_t ideMode = ESP.getFlashChipMode();
String coreVersion = ESP.getCoreVersion();
Serial.print(F("Arduino Core For ESP8266 Version: "));
Serial.println(coreVersion);
Serial.printf("Flash real id(唯一标识符): %08X\n", ESP.getFlashChipId());
Serial.printf("Flash 实际大小: %u KBytes\n", ESP.getFlashChipRealSize()/1024);
Serial.printf("IDE配置Flash大小: %u KBytes,往往小于实际大小\n", ESP.getFlashChipSize()/1024);
Serial.printf("IDE配置Flash频率 : %u MHz\n", ESP.getFlashChipSpeed()/1000000);
Serial.printf("Flash ide mode: %s\n\n", (ideMode == FM_QIO ? "QIO" : ideMode == FM_QOUT ? "QOUT" : ideMode == FM_DIO ? "DIO" : ideMode == FM_DOUT ? "DOUT" : "UNKNOWN"));
delay(1000);
}
去掉代码细节,会得到类似于Arduino编程的代码结构:
/**
* ESP8266 Arduino程序结构
* @author 单片机菜鸟
* @date 2018/10/24
*/
void setup() {
// 这里开始写初始化代码,只会执行一次
}
void loop() {
//这里写运行代码,重复执行
}
对于习惯c语言编程的读者,以上代码又可以抽象成以下伪代码结构:
/**
* ESP8266 Arduino程序伪代码结构
* @author 单片机菜鸟
* @date 2018/10/24
*/
void main(){
watchdogEnable();//启动看门狗
setup();//初始化函数
while(1){
loop();//业务代码函数
}
}
代码解析
1.在ESP8266 Arduino编程中,默认会开启看门狗功能,也就是对应伪代码的watchdogEnable(),意味着我们需要适当喂狗,不然会触发看门狗复位;
2.setup()方法:初始化函数,只会运行一次,所以一般情况下,我们都会在这里配置好初始化参数,比如IO口模式、串口波特率设置等等;
3.loop()方法:不断重复执行,这里编写我们的业务代码,同时要注意执行喂狗操作。
2. 计时和延时(Timing and delays)
时间控制,基本上可以说存在于每一个项目代码中。目前在Arduino中跟时间控制有关的方法包括以下几个:
delay(ms)
暂停一个给定的毫秒数的时间间隔。
delayMicroseconds(us)
暂停一个给定的微秒数的时间间隔。
millis()
返回重启(reset)后所经过的毫秒数。
micros()
返回重启(reset)后所经过的微秒数
温馨提示
通常,我们控制LED灯闪烁都会加上一个delay延时来达到切换亮灭时间长度。但是delay有个缺点就是:在给定的时间间隔内是不能做其他操作,这样对于一些需要响应按键操作的场景就不适用了。那么有没有什么办法既能延时又能不影响其他操作呢?当然,这就是millis()的妙用,通过获取两个时间点的毫秒数,然后计算它们的差值,差值时间间隔内是可以执行其他操作的。代码片段如下:
long debouncdDelay = 60;//延时间隔
long lastDebounceTime = 0; //最近记录的一次时间
// 判断时间间隔是否大于设定的时间间隔。
if(millis()-lastDebounceTime>debouncdDelay){
lastDebounceTime = millis();
}
3. NodeMcu 端口映射
在前面,笔者有说到,本书的实验案例是基于NodeMcu这块ESP8266开发板来进行的,其中NodeMcu的核心芯片是ESP8266-12F。要想知道ESP8266-12F给我们提供了什么功能模块,首先了解一下它有什么引脚端口以及NodeMcu与它之间的引脚端口映射关系。
3.1 ESP8266-12F
首先,认识一下ESP8266-12F的引脚定义,通常会隐藏pin6-pin11,如下图:
当然,笔者也会提供完整的引脚图以便对比,如下图:
分析引脚图,可以得出几个结论:
1.ESP8266-12F总共有22个引脚,对应了第1章选型表的SMD-22封装工艺,同时有GPIO0-GPIO16共17个通用IO口,但是得注意有些IO口还可以完成其他功能(也叫做引脚复用),诸如Serial、I2C、SPI,由相应的函数库完成;
2.ESP8266具有一个可用的单通道ADC;
3.GPIO6-GPIO11(复用引脚CS、MISO、MOSI、SCK)用于连接外部flash,对用户不可用,试图使用这些引脚作为IO将会导致程序奔溃;
4.支持SPI总线通信,对应引脚为GPIO12-GPIO15;
5.支持I2C总线,对应引脚为GPIO4-GPIO5;
6.支持串口通信Serial、Serial1,默认对应引脚GPIO1-GPIO3;
3.2 NodeMcu
接下来,先了解一下NoodeMcu的实物图,如下图:
同时,读者也需要知道ESP8266-12F与NodeMcu的端口映射关系,如下图:
可以看出:
1.中间的DEVKIT部分,就是NodeMcu提供给外界的端口,对应实物图上标注的端口名称;
2.除开中间部分,其他部分基本上对应ESP8266引脚,以不同颜色块来区分不同功能;
温馨提示
NodeMcu上的CLK、SD0、CMD、SD1、SD2引脚,是用于连接外接flash芯片,不应该用于连接其他模块,悬空即可,以防程序奔溃。
或许笔者会觉得看图有点复杂,所以笔者总结了下面的GPIO引脚映射表,以供参考:
NodeMCU的引脚名称 | ESP8266内部GPIO引脚号 | 可复用功能 | 备注 |
---|---|---|---|
D0 | GPIO16 | 无 | 可用,只能用作GPIO读/写,不支持特殊功能 |
D1 | GPIO5 | I2C总线的SCL | 可用 |
D2 | GPIO4 | I2C总线的SDA | 可用 |
D3 | GPIO0 | 无 | 不可用,烧录固件或者运行模式控制端口 |
D4 | GPIO2 | Serial1的TX | Serial1没有RX |
D5 | GPIO14 | SPI总线的SCLK | 可用 |
D6 | GPIO12 | SPI总线的MISO | 可用 |
D7 | GPIO13 | SPI总线的MOSI、Serial的RX | 可用 |
D8 | GPIO15 | SPI总线的CS、Serial的TX | 可用 |
D9 | GPIO3 | Serial的RX | 可用 |
D10 | GPIO1 | Serial的TX | 可用 |
SD2 | GPIO9 | 无 | 尽量不用 |
SD3 | GPIO10 | 无 | 尽量不用 |
从上面表格可以看出,我们大约11个GPIO引脚可用。而11个中的2个引脚通常被保留用于RX和TX,以便进行串口通信。因此最后,只剩下8个通用I / O引脚,即D0到D8(除开D3特殊用途)。
温馨提示
请注意,D0 / GPIO16引脚只能用作GPIO读/写,不支持特殊功能。
4. 数字IO(Digital IO)
上面说到,ESP8266-12F(也可以大胆说ESP8266-12系列)最终只剩下8个通用的I/O引脚以供我们使用,即是NodeMcu上的D0-D8(除D3之外)。
Arduino中的引脚号直接与ESP8266 GPIO的引脚号对应通信。pinMode/digitalRead/digitalWrite函数不变,所以要读取GPIO2,可调用digitalRead(2)。除了D0可以设置为INPUT(输入)、OUTPUT(输出)或者INPUT_PULLDOWN(输入,默认下拉,也就是低电平),剩余的数字IO引脚可以设置为INPUT(输入)、OUTPUT(输出)或者INPUT_PULLUP(输入,默认上拉,也就是高电平)。
下面,将在NodeMcu的D1引脚上写一个LED Blink的Arduino草图:
/**
* LED灯闪烁实验
*/
void setup() {
pinMode(D1, OUTPUT); // 初始化D1引脚为输出引脚
}
void loop() {
digitalWrite(D1, LOW); // 亮灯
delay(1000); // 延时1s
digitalWrite(D1, HIGH);// 灭灯
delay(1000); // 延时1s
}
注意
某些开发板和模块,仍将使用第9和第11引脚(如果闪存芯片工作于DIO模式,与默认的QIO模式相反),它们可用于IO。
5. 中断功能
中断可以理解为在正常的运行流程中突然插入的操作,这就像你在忙于工作的时候,领导突然叫你去买个下午茶,然后你就去把下午茶买回来,再继续工作。基于ESP8266的NodeMcu的数字IO的中断功能是通过attachInterrupt,detachInterrupt函数所支持的。除了D0/GPIO16,中断可以绑定到任意GPIO的引脚上。所支持的标准中断类型有:CHANGE(改变沿,电平从低到高或者从高到低)、RISING(上升沿,电平从低到高)、FALLING(下降沿,电平从高到低)。
首先,我们来看看Arduino IDE中用于中断的函数。
1.attachInterrupt()
该功能用于在将指定引脚设置为响应中断。
函数: attachInterrupt(pin, function, mode);
参数:
pin:要设置中断编号,注意,这里不是引脚编号。
function:中断发生时运行的函数, 这个函数不带任何参数,不返回任何内容。
Interrupt type/mode:它定义中断被触发的条件方式。
CHANGE:改变沿,引脚电平从低变为高或者从高变为低时触发中断。
RISING:上升沿,引脚电平从低变为高时触发中断。
FALLING:下降沿,引脚电平从高变为低时触发中断。
返回值: 无;
2.detachInterrupt()
该功能用于禁用指定GPIO引脚上的中断。
函数: detachInterrupt(pin)
参数:
pin:要禁用的中断的GPIO引脚。
返回值: 无;
3.digitalPinToInterrupt()
该功能用于获取指定GPIO引脚的中断号。
函数: digitalPinToInterrupt(pin)
参数:
pin:要获取中断号的GPIO引脚。
例子
将NodeMcu的D2引脚设置为上升沿中断。在D2上外接一个按键,按键通过电阻下拉到地。当发生中断的时候,我们在串口监视器上打印“Hello ESP8266”。
例子代码
/**
* 功能描述:ESP8266中断演示
*/
void setup() {
Serial.begin(115200);//设置串口波特率
attachInterrupt(digitalPinToInterrupt(D2), InterruptFunc, RISING);//设置中断号、响应函数、触发方式
}
void loop() {
}
/**
* 中断响应函数
*/
void InterruptFunc(){
Serial.println("Hello ESP8266");
}
6. 模拟输入(ADC)
学过模拟电路或者数字电路的人都会听过ADC,它又叫做模数转换器,用于将模拟信号转换成可视化的数字形式。ESP8266具有内置的10位ADC,只有一个ADC通道,即只有一个ADC输入引脚可读取来自外部器件的模拟电压。
ESP8266上的ADC通道和芯片供电电压复用,也就是说我们可以将其设置为测量系统电压或者外部电压。
6.1 测量外部电压
相关方法
analogRead(A0),用于读取施加在模块的ADC引脚上的外部电压;
输入电压范围
0 - 1.0V之间;
测量精度
由于ADC具有10位分辨率,因此会给出0-1023的值范围;
注意点
为了支持外部电压范围(0-3.3v),NodeMcu做了一个电阻分压器,如图所示:
例程
编写一个读取NodeMcu的ADC引脚上的模拟电压。我们这里使用电位器在ADC引脚上提供0-3.3V的可变电压。如下图连接线:
代码如下:
/**
* 功能描述:ESP8266 ADC 读取外部电压
* 在串口调试器查看效果
*/
void setup() {
Serial.begin(115200);//配置波特率
}
void loop() {
Serial.print("ADC Value: ");
Serial.println(analogRead(A0));//输出0-1023 对应 外部输入电压 0-1.0v
//延时1s
delay(1000);
}
6.2 测量系统电压
相关方法
ESP.getVcc(),读取NodeMCU模块的VCC电压,单位是mV;
注意点
ADC引脚必须保持悬空;在读取VCC电源电压之前,应更改ADC模式以读取系统电压。
要ADC_MODE(mode)在#include行后面改变ADC模式。
模式是ADC_TOUT(对于外部电压),ADC_VCC(对于系统电压)。默认情况下,它读取外部电压。
例程
编写ESP8266读取系统电压,代码如下:
/**
* 功能描述:ESP8266 ADC 读取系统电压
* 在串口调试器查看效果
*/
ADC_MODE(ADC_VCC);//设置ADC模式为读取系统电压
void setup() {
Serial.begin(115200);
}
void loop() {
Serial.print("ESP8266当前系统电压(mV): ");
Serial.println(ESP.getVcc());
delay(1000);
}
7. 模拟输出(PWM)
PWM(Pulse Width Modulation,脉宽调制),是在保持波的频率不变的同时改变脉宽的技术。当我们需要连续控制电压变化,实现呼吸灯或者电机转速的时候,就要用到PWM,如下图。
首先,我们来理解一下占空比。一个脉冲周期由一个ON周期(VCCC)和一个OFF周期(GND)组成。一段时间内ON周期占据脉冲周期的比例就叫做占空比。
DutyCycle(percentage)=Ton/TotalPeriodX100
例如,一个10ms的脉冲保持ON 2ms,那么根据公式,占空比是20%。
注意点
脉冲频率一般都是固定的,跟占空比没有关系。
NodeMcu PWM引脚
如下图,标注PWM引脚。
基本上数字IO都可以作为PWM复用引脚,除了D0。不过需要注意的是,D3尽量不用,它内部连接ESP8266 GPIO0。
NodeMcu PWM有关Arduino函数
1.analogWrite()
该功能用于在指定的引脚上启用软件PWM。
函数: analogWrite(pin,val)
参数:
pin:要启用软件PWM的GPIO引脚。
val:数值,一般在0到PWMRANGE范围,默认PWMRANGE是1023。
返回值: 无;
注意点:
analogWrite(pin, 0)用于禁用指定引脚上的PWM。
2.analogWriteRange()
该功能用于改变PWMRANGE数值。
函数: analogWriteRange(new_range)
参数:
new_range:新的PWMRANGE数值。
返回值: 无;
注意点:
可以理解为PWM精度范围。同样的PWM频率下,默认占空数值0-123。如果你改变PWMRANGE为2047,那么占空数值就变成0-2047。精度高了一倍。
3.analogWriteFreq()
该功能用于改变PWM频率。
函数: analogWriteFreq(new_frequency)
参数:
new_frequency:新PWM频率,默认是1kHZ。
返回值: 无;
注意点:
百度上很多资料都说PWM频率范围为1-1KHz。但是通过查看源码,如下:
static uint16_t analogFreq = 1000;
extern void __analogWriteFreq(uint32_t freq) {
if (freq < 100) {
analogFreq = 100;
} else if (freq > 40000) {
analogFreq = 40000;
} else {
analogFreq = freq;
}
}
可以看出,Arduino For ESP8266的PWM频率范围应该是100Hz-40KHz。
PWM例程
呼吸灯,LED灯明暗连续变化。代码如下:
/**
* 功能描述:ESP8266 PWM演示例程
* @author 单片机菜鸟
* @date 2018/10/25
*/
#define PIN_LED D6
void setup() {
// 这里开始写初始化代码,只会执行一次
pinMode(PIN_LED,OUTPUT);
analogWrite(PIN_LED,0);
}
void loop() {
//这里写运行代码,重复执行
for(int val=0;val<1024;val++){
//占空比不断增大 亮度渐亮
analogWrite(PIN_LED,val);
delay(2);
}
for(int val=1023;val>=0;val--){
//占空比不断变小 亮度渐暗
analogWrite(PIN_LED,1023);
delay(2);
}
}
8. 串口通信(Serial)
ESP8266的串口通信与传统的Arduino设备完全一样。除了硬件FIFO(128字节用于TX和RX)之外,硬件串口还有额外的256字节的TX和RX缓存。发送和接收全都由中断驱动。当FIFO/缓存满时,write函数会阻塞工程代码的执行,等待空闲空间。当FIFO/缓存空时,read函数也会阻塞工程代码的执行,等待串口数据进来。
NodeMcu上有两组串口,Serial和Serial1。
Serial使用UART0,默认对应引脚是GPIO1(TX)和GPIO3(RX)。在Serial.begin执行之后,调用Serial.swap()可以将Serial重新映射到GPIO15(TX)和GPIO13(RX)。再次调用Serial.swap()将Serial重新映射回GPIO1和GPIO3。不过,一般情况下,默认就好。
串口映射例程
/**
* 功能描述:ESP8266 Serial映射例程
* @author 单片机菜鸟
* @date 2018/10/25
*/
void setup() {
// 这里开始写初始化代码,只会执行一次
Serial.begin(115200);
Serial.println("GPIO1(TX),GPIO3(RX)");
//调用映射方法
Serial.swap();
Serial.println("GPIO15(TX),GPIO13(RX)");
//重新映射回来
Serial.swap();
Serial.println("GPIO1(TX),GPIO3(RX)");
}
void loop() {
//这里写运行代码,重复执行
}
Serial1使用UART1,默认对应引脚是GPIO2(TX)。Serial1不能用于接收数据,因为它的RX引脚被用于flash芯片连接。要使用Serial1,请调用Serial.begin(baudrate)。代码如下:
/**
* 功能描述:ESP8266 串口例程
* @author 单片机菜鸟
* @date 2018/10/25
*/
void setup() {
// 这里开始写初始化代码,只会执行一次
Serial.begin(115200);
Serial.println("Hello Serial");
Serial1.begin(115200);
Serial1.println("Hello Serial1");
}
void loop() {
//这里写运行代码,重复执行
}
如果不使用Serial1并且不映射串口,可以将UART0的TX映射到GPIO2,具体操作是:在Serial.begin()之后调用Serial.set_tx(2)或者直接调用Serial.begin(baud,config,mode,2)。
默认情况下,当调用Serial.begin后,将禁用WiFi库的诊断输出。要想再次启动调试输出,请调用Serial.setDebugOutput(true)。要将调试输出映射到Serial1时,需要调用Serial1.setDebugOutput(true)。
调用Serial.setRxBufferSize(size_t size)允许定义接收缓冲区的大小,默认值是256(缓冲区也是使用内存,意味着不能一味地去增大这个值)。
Serial和Serial1对象都支持5,6,7,8个数据位,奇数(O)、偶数(E)和无(N)奇偶校验,以及1或者2个停止位。要设置所需的模式,请调用Serial.begin(baudrate, SERIAL_8N1), Serial.begin(baudrate, SERIAL_6E2)等。
Serial和Serial1都实现了一种新方法用来获取当前的波特率设置。要获取当前的波特率,请调用Serial.baudRate(),Serial1.baudRate()。代码如下:
/**
* 功能描述:ESP8266 串口波特率例程
* @author 单片机菜鸟
* @date 2018/10/25
*/
void setup() {
// 这里开始写初始化代码,只会执行一次
// 设置当前波特率为57600
Serial.begin(57600);
// 获取当前波特率
int br = Serial.baudRate();
// 将打印 "Serial is 57600 bps"
Serial.printf("Serial is %d bps", br);
}
void loop() {
//这里写运行代码,重复执行
}
Serial和Serial1都属于硬件串口(HardwareSerial)的实例,如果读者需要使用ESP8266 软件串口的功能,请参考以下库:https://github.com/plerup/espsoftwareserial。
为了检测进入Serial的未知波特率的数据,可以调用Serial.detectBaudrate(time_t timeoutMillis)。这个方法尝试在timeoutMillis ms的时间内检测波特率,检测成功返回波特率,检测失败返回0。detectBaudrate()方法在Serial.begin()被调用之前调用(因为它不需要用到接收缓冲区或者串口配置),并且它不能检测数据位位数或者停止位。这个检测过程不会去改变数据的波特率,所以可以在检测成功之后,调用Serial.begin(detectedBaudrate)。
串口用处
一般来说,串口通信用在两个方面:
1.与外围串口设备传输数据,比如蓝牙模块、Arduino等等;
2.开发过程中用来调试代码,通过串口输出Debug信息了解程序运行信息。例程如下:
/**
* Demo1:
* statin模式下,创建一个连接到可接入点(wifi热点),并且打印IP地址
* @author 单片机菜鸟
* @date 2019/09/02
*/
#include <ESP8266WiFi.h>
#define AP_SSID "xxxxx" //这里改成你的wifi名字
#define AP_PSW "xxxxx"//这里改成你的wifi密码
//以下三个定义为调试定义
#define DebugBegin(baud_rate) Serial.begin(baud_rate)
#define DebugPrintln(message) Serial.println(message)
#define DebugPrint(message) Serial.print(message)
void setup(){
//设置串口波特率,以便打印信息
DebugBegin(115200);
//延时2s 为了演示效果
delay(2000);
DebugPrintln("Setup start");
//启动STA模式,并连接到wifi网络
WiFi.begin(AP_SSID, AP_PSW);
DebugPrint(String("Connecting to ")+AP_SSID);
//判断网络状态是否连接上,没连接上就延时500ms,并且打出一个点,模拟连接过程
//笔者扩展:加入网络一直都连不上 是否可以做个判断,由你们自己实现
while (WiFi.status() != WL_CONNECTED){
delay(500);
DebugPrint(".");
}
DebugPrintln("");
DebugPrint("Connected, IP address: ");
//输出station IP地址,这里的IP地址由DHCP分配
DebugPrintln(WiFi.localIP());
DebugPrintln("Setup End");
}
void loop() {
}
9. 总结
总体上讲,本章基础内容比较多,笔者介绍ESP8266在Arduino平台上的一些基础知识点,包括程序结构、NodeMcu端口映射、ESP8266 数字IO、PWM、ADC、串口通信等等。
本章目的很简单,就是为了告诉读者,ESP8266到底给我们提供了什么可利用硬件资源,以方便我们项目开发。