零基础入门学用物联网(ESP8266) 第一部分 基础知识篇(二)

参考教程:https://www.bilibili.com/video/BV1L7411c7jw/?spm_id_from=333.1387.favlist.content.click

四、ESP8266作为网络服务器

1、建立基本网络服务器

(1)网络服务器有很多种类型,通常承担网络服务器工作的设备都是运算能力比较强大的电脑。ESP866-NodeMCU虽然只能实现一些基本的网络服务功能,不过这些基本的网络服务功能已经足够开发物联网项目了。

(2)如果ESP866-NodeMCU作为服务器,与电脑主机、手机都接入同一个Wi-Fi网络中,那么电脑或者手机想要访问ESP866-NodeMCU的网站首页,大致流程如下所示。

(3)ESP8266WebServer类是网络服务器的抽象,创建对象时,需将应用层服务对应的运输层端口号传入构造函数中,针对HTTP协议,其使用的运输层熟知端口号为80。ESP8266WebServer类的成员函数有如下几个(包括但不限于):

①begin:启动ESP8266的网络服务功能,该函数无参数。

②on:配置网络服务器如何处理用户端发来的不同的HTTP请求。

其有三个参数,第一个参数为用户端可能请求访问的网站目录(以字符串的形式表示,比如根目录"/"),第二个参数为请求行中的方法(可省略,省略则不判断),第三个参数为一个函数句柄,当用户端用指定方法请求访问第一个参数指定的目录时,服务器执行函数句柄指向的子函数,子函数中一般有响应HTTP请求并发出响应报文的语句

③onNotFound:配置网络服务器如何处理用户端发来的无法满足的HTTP请求。

其只有一个参数,为一个函数句柄,当用户端请求访问的目录不存在时,服务器执行函数句柄指向的子函数,子函数中一般有响应HTTP请求并发出"错误提示"响应报文的语句

④send:发送HTTP响应报文。

其有三个参数,第一个参数为状态码(比如404表示找不到页面),第二个参数为首部行的首部字段名(比如"text/plain"表示纯文本,"text/html"表示网页代码),第三个参数为首部字段名对应的值(比如纯文本对应的应为一段文本,用户收到后将其解读为纯文本信息),有时候没有需要特别添加的首部行,则后两个参数可省略

⑤handleClient:处理客户端的请求(这不是配置函数,而是功能函数,需要反复调用),该函数无参数。

(4)使用ESP8266实现最基本的网页服务功能,其程序流程如下图所示。

(5)将以下示例程序粘贴到代码区中,然后将其编译并上传NodeMCU开发板,开启电脑热点"Zevalin_Computer",ESP8266会建立起一个小型服务器,接下来打开浏览器,在地址栏中输入NodeMCU的IP地址并按下回车,可以看到"Hello from ESP8266"的文本信息。

cpp 复制代码
#include <ESP8266WiFi.h>        //ESP8266WiFi库
#include <ESP8266WiFiMulti.h>   //ESP8266WiFiMulti库
#include <ESP8266WebServer.h>   //ESP8266WebServer库
 
ESP8266WiFiMulti wifiMulti;     //建立ESP8266WiFiMulti对象,对象名称是'wifiMulti'
ESP8266WebServer esp8266_server(80); //建立ESP8266WebServer对象,对象名称为esp8266_server
 
void handleRoot() {     //处理网站根目录"/"的访问请求 
  esp8266_server.send(200, "text/plain", "Hello from ESP8266");
}
void handleNotFound(){  //处理404情况
  esp8266_server.send(404, "text/plain", "404: Not found");
}

void setup(void){
  Serial.begin(9600);          //启动串口通讯
 
  //配置Wi-Fi网络的所有可能选项
  wifiMulti.addAP("Zevalin_Computer", "00114514");
  wifiMulti.addAP("Zevalin_esp8266", "00114514");
 
  int i = 0;                                 
  while (wifiMulti.run() != WL_CONNECTED) {   //连接信号最强的Wi-Fi网络
    delay(1000);    
    Serial.print(i++); Serial.print(' ');
  } 
  Serial.println('\n');
  Serial.print("Connected to ");  Serial.println(WiFi.SSID());
  Serial.print("IP address:\t");  Serial.println(WiFi.localIP());
  
//--------"启动网络服务功能"程序部分开始--------
  esp8266_server.begin();
  esp8266_server.on("/", handleRoot);
  esp8266_server.onNotFound(handleNotFound);        
//--------"启动网络服务功能"程序部分结束--------
  Serial.println("HTTP esp8266_server started"); //告知用户ESP8266网络服务功能已经启动
}

void loop(void){
  esp8266_server.handleClient();     //处理http服务器访问
}
                                                                     

2、通过网络服务实现开发板基本控制

(1)进一步介绍ESP8266WebServer类的成员函数:

①on函数除了能够配置收到用户端请求(GET方法)后给出相应的回应,还能配置收到用户端请求(POST请求)后做出相应的行为,比如"点灯"。

②sendHeader函数用于向HTTP响应报文中的响应头部添加一个首部行,它与send函数的区别是,调用sendHeader添加首部行后不会将响应报文立即发送,而调用send函数则会立即将响应报文立即发送,且send函数会自动构建状态行、自动附加必要的头部。

(2)使用ESP8266实现通过网络服务实现开发板基本的点灯控制,其程序流程如下图所示。

(3)将以下示例程序粘贴到代码区中,然后将其编译并上传NodeMCU开发板,开启电脑热点"Zevalin_Computer",ESP8266会建立起一个小型服务器,接下来打开浏览器,在地址栏中输入NodeMCU的IP地址并按下回车,可以看到一个标识文本为"Toggle LED"的按键,按下它后浏览器向开发板发送POST请求,开发板就会切换LED的状态,同时浏览器的页面将跳转回根目录。

cpp 复制代码
#include <ESP8266WiFi.h>        //ESP8266WiFi库
#include <ESP8266WiFiMulti.h>   //ESP8266WiFiMulti库
#include <ESP8266WebServer.h>   //ESP8266WebServer库
 
ESP8266WiFiMulti wifiMulti;     //建立ESP8266WiFiMulti对象,对象名称是'wifiMulti'
ESP8266WebServer esp8266_server(80); //建立ESP8266WebServer对象,对象名称为esp8266_server
 
void handleRoot() {      //处理根目录请求(网站主页给出一个"Toggle LED"按键,按下它将用POST方法发送"/LED"的目录请求)         
  esp8266_server.send(200, "text/html", "<form action=\"/LED\" method=\"POST\"><input type=\"submit\" value=\"Toggle LED\"></form>");
}
void handleLED() {       //处理LED控制请求          
  digitalWrite(LED_BUILTIN,!digitalRead(LED_BUILTIN)); //改变LED的点亮或者熄灭状态
  esp8266_server.sendHeader("Location","/");           //跳转回页面根目录
  esp8266_server.send(303);                            //发送Http相应代码303跳转  
}
void handleNotFound(){  //处理404情况
  esp8266_server.send(404, "text/plain", "404: Not found");
}

void setup(void){
  Serial.begin(9600);           //启动串口通讯
  pinMode(LED_BUILTIN, OUTPUT); //设置内置LED引脚为输出模式以便控制LED
 
  wifiMulti.addAP("Zevalin_Computer", "00114514");
  wifiMulti.addAP("Zevalin_esp8266", "00114514");
  int i = 0;                                 
  while (wifiMulti.run() != WL_CONNECTED) {   //连接信号最强的Wi-Fi网络
    delay(1000); Serial.print(i++); Serial.print(' ');
  } 
  Serial.println('\n');
  Serial.print("Connected to "); Serial.println(WiFi.SSID());
  Serial.print("IP address:\t");  Serial.println(WiFi.localIP());
  
  esp8266_server.begin();                            //启动网站服务
  esp8266_server.on("/", HTTP_GET, handleRoot);    //设置服务器根目录即'/'的函数'handleRoot'
  esp8266_server.on("/LED", HTTP_POST, handleLED); //设置处理LED控制请求的函数'handleLED'
  esp8266_server.onNotFound(handleNotFound);       //设置处理404情况的函数'handleNotFound'
  Serial.println("HTTP esp8266_server started"); //告知用户ESP8266网络服务功能已经启动
}
void loop(void){
  esp8266_server.handleClient();     //处理http服务器访问
}

3、通过网络服务将开发板引脚状态显示在网页中

(1)为了便于学习,本节将使用D3引脚作为演示案例,因为它已经与开发板上的FLASH按键开关连接好了,当没有按下该按键时D3引脚将会保持高电平状态,当按下该按键时D3引脚会保持低电平状态。

(2)使用ESP8266实现通过网络服务将开发板引脚状态显示在网页中,其程序流程如下图所示。

(3)将以下示例程序粘贴到代码区中,然后将其编译并上传NodeMCU开发板,开启电脑热点"Zevalin_Computer",ESP8266会建立起一个小型服务器,接下来打开浏览器,在地址栏中输入NodeMCU的IP地址并按下回车,可以看到网页中打印出D3引脚的电平信息,按下FLASH开关不松开,然后刷新网页,可以看到网页中打印出D3引脚的电平信息发生变化。

cpp 复制代码
#include <ESP8266WiFi.h>        //ESP8266WiFi库
#include <ESP8266WiFiMulti.h>   //ESP8266WiFiMulti库
#include <ESP8266WebServer.h>   //ESP8266WebServer库

#define buttonPin D3            //按钮引脚D3宏定义
ESP8266WiFiMulti wifiMulti;     //建立ESP8266WiFiMulti对象,对象名称是'wifiMulti'
ESP8266WebServer esp8266_server(80); //建立ESP8266WebServer对象,对象名称为esp8266_server
bool pinState;       //存储引脚状态的变量
 
void handleRoot(){   
  String displayPinState;                    //定义存储按键状态的字符串变量
  if(pinState == HIGH){                      //当按键引脚D3为高电平
    displayPinState = "Button State: HIGH"; //字符串赋值高电平信息
  }
  else{                                       //当按键引脚D3为低电平
    displayPinState = "Button State: LOW";  //字符串赋值低电平信息
  }
  esp8266_server.send(200, "text/plain", displayPinState);  //向浏览器发送按键状态信息  
}
void handleNotFound(){  //处理404情况
  esp8266_server.send(404, "text/plain", "404: Not found");
}

void setup(void){
  Serial.begin(9600);               //启动串口通讯
  pinMode(buttonPin, INPUT_PULLUP); //将按键引脚设置为输入上拉模式
  wifiMulti.addAP("Zevalin_Computer", "00114514");
  wifiMulti.addAP("Zevalin_esp8266", "00114514");
  int i = 0;                                 
  while (wifiMulti.run() != WL_CONNECTED) {   //连接信号最强的Wi-Fi网络
    delay(1000); Serial.print(i++); Serial.print(' ');
  } 
  Serial.println('\n');
  Serial.print("Connected to "); Serial.println(WiFi.SSID());
  Serial.print("IP address:\t"); Serial.println(WiFi.localIP());

  esp8266_server.begin();                        //启动网站服务
  esp8266_server.on("/", handleRoot);           //设置服务器根目录的函数'handleRoot'
  esp8266_server.onNotFound(handleNotFound);    //设置处理404情况的函数'handleNotFound'
  Serial.println("HTTP esp8266_server started"); //告知用户ESP8266网络服务功能已经启动
}
void loop(void){
  esp8266_server.handleClient();     //不断处理http服务器访问
  pinState = digitalRead(buttonPin); //不断获取引脚状态
}

(4)以上程序虽然能让客户端获取引脚状态,但如果引脚状态更新,而客户端又没有发送新的HTTP请求(刷新网页相当于重新发出一次HTTP请求),那么客户端显示的引脚状态是错误的。为了解决这个问题,可以修改handleRoot函数,使它向客户端发送的不再是简单的D3引脚电平状态字符串,而是一串稍复杂的html代码,其对应的网页除了打印D3引脚电平状态信息以外,还会每隔5s自动刷新一次,即发送一次新的HTTP请求,这样,服务器就能每隔5s将最新的D3引脚电平状态与客户端同步一次。

cpp 复制代码
void handleRoot() {   //处理网站目录"/"的访问请求 
  esp8266_server.send(200, "text/html", sendHTML(pinState));  
}

String sendHTML(bool buttonState){
  String htmlCode = "<!DOCTYPE html> <html>\n";
  htmlCode +="<head><meta http-equiv='refresh' content='5'/>\n";   //该页面每隔5s自动刷新一次,也就是说每隔5s发出一次访问根目录请求
  htmlCode +="<title>ESP8266 Butoon State</title>\n";
  htmlCode +="<style>html { font-family: Helvetica; display: inline-block; margin: 0px auto; text-align: center;}\n";
  htmlCode +="body{margin-top: 50px;} h1 {color: #444444;margin: 50px auto 30px;} h3 {color: #444444;margin-bottom: 50px;}\n";
  htmlCode +="</style>\n";
  htmlCode +="</head>\n";
  htmlCode +="<body>\n";
  htmlCode +="<h1>ESP8266 BUTTON STATE</h1>\n";
  
  if(buttonState)    //根据D3引脚状态制定相应的html代码,客户端接收后将其对应的网页信息打印在浏览器中
    {htmlCode +="<p>Button Status: HIGH</p>\n";}
  else
    {htmlCode +="<p>Button Status: LOW</p>\n";}
    
  htmlCode +="</body>\n";
  htmlCode +="</html>\n";
  
  return htmlCode;
}

五、ESP8266闪存文件系统及应用

1、ESP8266闪存文件系统介绍

(1)通常来说,ESP8266闪存文件系统(Serial Peripheral Interface Flash File System)有4MB的空间,1MB空间用于存储应用程序,3MB空间用于存储文件(其中有一部分是系统文件,不可修改)。

(2)SPIFFS是FS库提供的一个类(或者说实例),是对闪存文件系统的抽象。SPIFFS提供的几个成员函数如下:

①format函数:用于清除闪存中专门用于存放文件的"SPIFFS文件系统分区"里的数据,不会影响主程序。

②begin函数:用于启动闪存文件系统,并返回启动结果(True表示成功,False表示失败,失败并不意味着系统损坏,可能是配置有问题导致的)。

③open函数:用于以指定方式打开指定文件,其第一个参数为文件路径(或者说被操作文件的位置及名称),第二个参数为打开方式(字符串"w"表示写访问,字符串"r"表示读访问,字符串"a"表示追加信息)。

④exists函数:用于判断闪存中是否有指定文件,函数参数为文件路径(或者说指定文件的位置及名称)。

⑤openDir函数:用于打开指定路径的"目录",并返回一个Dir对象,函数参数为被读取的文件夹路径。

⑥remove函数:用于移除指定文件,函数参数为文件路径(或者说指定文件的位置及名称),如移除成功则返回True,否则返回False(如需要移除的文件本就不存在)。

⑦info函数:用于获取闪存文件系统信息,以FSInfo对象的类型返回。

(3)File也是FS库提供的一个类,是对文件的抽象,使用open函数打开一个文件时,open函数的返回值类型就是File。File类提供的成员函数如下:

①println函数:向File对象指向的文件中写入字符串信息,函数参数为字符串。(用"w"方式打开文件时,每次写入前都会先将文件原有内容清除,而用"a"方式打开文件时,每次写入都是直接在文件原有内容后面追加新内容)

②read函数:从File对象指向的文件中读取一个字符的信息并返回,然后文件指针右移一字节,指向下一个字符。

③close函数:关闭File对象指向的文件,防止其它代码段对文件进行误操作。

④size函数:获取File对象指向的文件大小并返回,单位为字节。

(4)Dir也是FS库提供的一个类,是对目录的抽象,有一个文件指针成员指向目录中的其中一个文件,使用openDir函数打开一个文件夹时,openDir函数的返回值类型就是Dir。Dir类提供的成员函数如下:

①next函数:用于检查目录中是否还有"下一个文件",如有则返回True,文件指针成员指向"下一个文件",如无则返回False。

②fileName函数:用于获取Dir对象的文件指针指向的文件的名称。

(5)FSInfo也是FS库提供的一个类,其成员属性是闪存文件系统的信息,比如可用空间总和、已用空间等,使用info函数获取闪存文件系统信息时,info函数的返回值类型就是FSInfo。

2、ESP8266闪存文件系统基本操作示例

(1)在菜单栏中选择"工具"→"Flash Size",弹出的几个选项用于指示用户需要使用的闪存文件系统空间大小,比如程序员需要使用1MB的闪存文件系统空间,则选择"4MB(FS:1MB OTA:~1019KB)"即可。

(2)将以下示例程序粘贴到代码区中,然后将其编译并上传NodeMCU开发板,这样即可通过程序向闪存文件系统写入指定信息。

cpp 复制代码
#include <FS.h>  
 
String file_name = "/taichi-maker/notes.txt"; //被读取的文件位置和名称
 
void setup() {
  Serial.begin(9600);
  Serial.println("");
  
  Serial.println("SPIFFS format start");
  SPIFFS.format();     //格式化闪存文件系统
  Serial.println("SPIFFS format finish");
  
  if(SPIFFS.begin()){  //启动闪存文件系统
    Serial.println("SPIFFS Started.");
  }
  else 
  {
    Serial.println("SPIFFS Failed to Start.");
  }
  
  File dataFile = SPIFFS.open(file_name, "w"); //建立File对象用于向SPIFFS中的file对象(即/notes.txt)写入信息
  dataFile.println("Hello IOT World.");        //向dataFile写入字符串信息
  dataFile.close();                             //完成文件写入后关闭文件
  Serial.println("Finished Writing data to SPIFFS");
}
 
void loop() {
  
}

(3)在上一程序运行后,将以下示例程序粘贴到代码区中,然后将其编译并上传NodeMCU开发板,这样即可通过程序从闪存文件系统读取先前写入的信息。

cpp 复制代码
#include <FS.h>
 
String file_name = "/taichi-maker/notes.txt";              //被读取的文件位置和名称
 
void setup() {
  Serial.begin(9600);
  Serial.println("");
  
  if(SPIFFS.begin()){   //启动闪存文件系统
    Serial.println("SPIFFS Started.");
  }
  else {
    Serial.println("SPIFFS Failed to Start.");
  }
 
  //确认闪存中是否有file_name文件
  if (SPIFFS.exists(file_name)){
    Serial.print(file_name);
    Serial.println(" FOUND.");
  } 
  else {
    Serial.print(file_name);
    Serial.print(" NOT FOUND.");
  }
 
  //建立File对象用于从SPIFFS中读取文件
  File dataFile = SPIFFS.open(file_name, "r"); 
 
  //读取文件内容并且通过串口监视器输出文件信息
  for(int i=0; i<dataFile.size(); i++){
    Serial.print((char)dataFile.read());    //从文件中读取1个字符并以char类型输出
  }
 
  //完成文件读取后关闭文件
  dataFile.close();                           
}

void loop() {

}

(4)在上一程序运行后,将以下示例程序粘贴到代码区中,然后将其编译并上传NodeMCU开发板,这样即可通过程序向闪存文件系统中的文件添加信息。

cpp 复制代码
#include <FS.h>
 
String file_name = "/taichi-maker/notes.txt";              //被读取的文件位置和名称
 
void setup(){
  Serial.begin(9600);
  Serial.println("");
  
  if(SPIFFS.begin()){  //启动闪存文件系统
    Serial.println("SPIFFS Started.");
  }
  else{
    Serial.println("SPIFFS Failed to Start.");
  }
 
  //确认闪存中是否有file_name文件
  if (SPIFFS.exists(file_name)){
    Serial.print(file_name);
    Serial.println(" FOUND.");
 
    File dataFile = SPIFFS.open(file_name, "a"); //建立File对象用于向SPIFFS中的file对象(即/notes.txt)写入信息
    dataFile.println("This is Appended Info.");  //向dataFile添加字符串信息
    dataFile.close();                            //完成文件操作后关闭文件   
    Serial.println("Finished Appending data to SPIFFS");
    
  }
  else{
    Serial.print(file_name);
    Serial.print(" NOT FOUND.");
  }               
}
 
void loop(){

}

(5)在上一程序运行后,将以下示例程序粘贴到代码区中,然后将其编译并上传NodeMCU开发板,这样即可通过程序读取闪存文件系统指定目录中的内容。

cpp 复制代码
#include <FS.h>
 
String file_name = "/taichi-maker/myFile.txt"; //被读取的文件位置和名称
String folder_name = "/taichi-maker";          //被读取的文件夹
 
void setup(){
  Serial.begin(9600);
  Serial.println("");
  
  if(SPIFFS.begin()){   //启动闪存文件系统
    Serial.println("SPIFFS Started.");
  }
  else{
    Serial.println("SPIFFS Failed to Start.");
  }
 
  File dataFile = SPIFFS.open(file_name, "w"); //建立File对象用于向SPIFFS中的file对象(即myFile.txt)写入信息
  dataFile.println("Hello Taichi-Maker.");     //向dataFile写入字符串信息
  dataFile.close();                             //完成文件写入后关闭文件
  Serial.println(F("Finished Writing data to SPIFFS"));
 
  //显示目录中文件内容以及文件大小
  Dir dir = SPIFFS.openDir(folder_name);  //建立"目录"对象
  
  while (dir.next()){    //dir.next()用于检查目录中是否还有"下一个文件"
    Serial.println(dir.fileName()); //输出文件名
  }
}
 
void loop(){

}

(6)在上一程序运行后,将以下示例程序粘贴到代码区中,然后将其编译并上传NodeMCU开发板,这样即可通过程序从闪存文件系统中删除指定文件。

cpp 复制代码
#include <FS.h>
 
String file_name = "/taichi-maker/notes.txt";              //被读取的文件位置和名称
 
void setup(){
  Serial.begin(9600);
  Serial.println("");
  
  if(SPIFFS.begin()){   //启动闪存文件系统
    Serial.println("SPIFFS Started.");
  }
  else{
    Serial.println("SPIFFS Failed to Start.");
  }
  
  if (SPIFFS.remove(file_name)){  //从闪存中删除file_name文件
    Serial.print(file_name);
    Serial.println(" remove sucess");
  }
  else{
    Serial.print(file_name);
    Serial.println(" remove fail");
  }                       
}
 
void loop(){

}

(7)在上一程序运行后,将以下示例程序粘贴到代码区中,然后将其编译并上传NodeMCU开发板,这样即可通过程序获取闪存文件系统的信息,如可用空间总和等。

cpp 复制代码
#include <FS.h>
 
FSInfo fs_info;
 
void setup(){
  Serial.begin(9600);
 
  SPIFFS.begin();       //启动SPIFFS
  Serial.println(""); Serial.println("SPIFFS Started.");
 
  //闪存文件系统信息
  SPIFFS.info(fs_info);
 
  //可用空间总和(单位:字节)
  Serial.print("totalBytes: ");     
  Serial.print(fs_info.totalBytes); 
  Serial.println(" Bytes"); 
 
  //已用空间(单位:字节)
  Serial.print("usedBytes: "); 
  Serial.print(fs_info.usedBytes);
  Serial.println(" Bytes"); 
 
  //最大文件名字符限制(含路径和'\0')
  Serial.print("maxPathLength: "); 
  Serial.println(fs_info.maxPathLength);
 
  //最多允许打开文件数量
  Serial.print("maxOpenFiles: "); 
  Serial.println(fs_info.maxOpenFiles);
 
  //存储块大小
  Serial.print("blockSize: "); 
  Serial.println(fs_info.blockSize);
 
  //存储页大小
  Serial.print("pageSize: ");
  Serial.println(fs_info.pageSize);
}
 
void loop(){

}

3、通过Arduino IDE向闪存文件系统上传文件

相关推荐
写Cpp的小黑黑2 小时前
MQTT 协议中的 Last Will、Message Expiration 和 Retained Messages 机制详解
物联网
北京耐用通信2 小时前
耐达讯自动化CC linkie转Devicenet网关:架起三菱PLC与电导率仪跨协议“沟通之桥”
人工智能·物联网·网络协议·自动化·信息与通信
敬畏_上帝2 小时前
433无线接收解码-实测可到10-20米
单片机·嵌入式硬件
学嵌入式的小杨同学2 小时前
STM32 进阶封神之路(十二):串口实战全攻略 —— 发送 / 接收 / 中断 /printf 重定向(库函数 + 寄存器)
stm32·单片机·嵌入式硬件·mcu·硬件架构·pcb·嵌入式实时数据库
✎ ﹏梦醒͜ღ҉繁华落℘3 小时前
单片机基础知识 -- 大端模式 与 小端模式
单片机·嵌入式硬件
混分巨兽龙某某3 小时前
基于ESP32与Qt Creator的WIFI空间透视项目(代码开源)
qt·嵌入式·esp32·wifi空间透视
雾削木3 小时前
STM32 基于外部时钟源的 PWM 测量
stm32·单片机·嵌入式硬件
qq_411262423 小时前
esp的深度睡眠关机功耗很高,一般软件方面应该查哪里?
单片机·嵌入式硬件
San_a dreamer fish3 小时前
STM32开发入门(二):
stm32·单片机·嵌入式硬件