当前位置: 首页 > news >正文

51单片机-红外遥控器(NEC标准)-实验(红外遥控及调速电机)

作者:Whappy

时间:2024.9.20

总结一下!基础实验到这儿里就圆满结束,历经25天,将51单片机学完并亲自手敲代码近5000行,在手敲代码过程中,明显感觉的看和敲,明显就是不同的感觉,创作不易,在原有的代码上加上自己的想法并加以实现!接下向STM32发起进攻,开始学习!

介绍:

硬件电路

针对于上图两个PNP的三极管主要用来调制的,是因为自然界中有很多红外光,我们想要得到我们需要的的红外光,就要进行调制,通过一个38hz的频率进行调制,因为自然界的红外光不可能以38hz的频率跳动,后期通过滤波器将我们需要的波形提取来就可以了!

        还有红外接收头,OUT引脚接收的脉冲特别快,需要我们及时处理,所以,我们采取51单片机的外部中断来对这个脉冲进行处理!

发送和接收装置

通过上述解释:这个高电平和低电平不是我们平常说的一个周期内的高电平,而是一个周期中高低低电平所持续的时间,同样,低电平也是,看上图!

NEC编码

示波器采集的按键波形变化,还是地址码+地址反码+命令字+命令字反码

实物外观

可以看出要控制器所对应的键码值!

51单片机的外部中断

这个外部中断,也是我们常用的一种处理手段,外部中断也比较简单,只需要打开相关寄存器,配置一下外部中断服务函数即可!如下代码

void INT0_Init(void)  //打开外部中断相关的的寄存器(寄存器可单独配置)
{IT0 = 1;  //配置位低电平触发模式IE0 = 0;  //中断标志位EX0 = 1;    //外部中断0使能EA = 1;    //中断总使能PX0 = 1;    //中断最高优先级
}/*
//外部中断0 服务函数
void Int0_Routine()	interrupt 0
{Number++;
}
*///注:配置完成之后就可以在主函数中初始化了,在主函数下面加上我们的中断服务函数,即可触发中断

实验一:红外遥控实验(其余代码参考往期实验)

这个程序是使用红外遥控器控制数值的显示,并在LCD1602上输出。主要功能是通过红外接收数据,显示遥控器的地址码、命令码,并且根据接收到的命令对变量 Num 进行增减。

主要流程:

  1. LCD 初始化:

    • LCD_Init() 初始化 LCD1602 显示屏。
    • 在第一行显示 "ADDR CMD NUM" 作为标题。
    • 在第二行初始化显示地址码、命令码和变量 Num 的值。
  2. 红外接收初始化:

    • IR_Init() 初始化红外接收功能,准备接收遥控器信号。
  3. 循环处理红外信号:

    • IR_GetDataFlag()IR_GetRepeatFlag() 判断是否接收到数据帧或连发帧。
    • 如果接收到信号:
      • 调用 IR_GetAddress() 获取遥控器地址码并显示在LCD上。
      • 调用 IR_GetCommand() 获取遥控器命令码并显示在LCD上。
  4. 处理命令:

    • 如果接收到的命令码是 IR_VOL_MINUS(对应遥控器的音量减键),变量 Num 自减。
    • 如果接收到的命令码是 IR_VOL_ADD(对应遥控器的音量加键),变量 Num 自增。
  5. 显示数值:

    • 更新LCD,显示变量 Num 的值。

通过遥控器上的音量加减键,用户可以控制数值 Num 的增减,并实时在LCD上看到变化。

main.c

#include <REGX52.H>
#include "Delay.h"
#include "LCD1602.h"
#include "IR.h"unsigned char Num;
unsigned char Address;
unsigned char Command;void main()
{LCD_Init();LCD_ShowString(1,1,"ADDR  CMD  NUM");LCD_ShowString(2,1,"00    00   000");IR_Init();while(1){if(IR_GetDataFlag() || IR_GetRepeatFlag())	//如果收到数据帧或者收到连发帧{Address=IR_GetAddress();		//获取遥控器地址码Command=IR_GetCommand();		//获取遥控器命令码LCD_ShowHexNum(2,1,Address,2);	//显示遥控器地址码LCD_ShowHexNum(2,7,Command,2);	//显示遥控器命令码if(Command==IR_VOL_MINUS)		//如果遥控器VOL-按键按下{Num--;						//Num自减}if(Command==IR_VOL_ADD)			//如果遥控器VOL+按键按下{Num++;						//Num自增}LCD_ShowNum(2,12,Num,3);		//显示Num}}
}

Timer0.c

#include <REGX52.H>// 定时器0初始化函数,设置定时器0为模式1(16位定时器模式),每1毫秒计时一次,工作在11.0592MHz晶振下
void Timer0_Init()		
{TMOD &= 0xF0;        // 清除定时器0的模式位(低4位),保留高4位(定时器1的设置)TMOD |= 0x01;        // 设置定时器0为模式1(16位定时器)TL0 = 0;             // 设置定时器0低8位初值为0TH0 = 0;             // 设置定时器0高8位初值为0TF0 = 0;             // 清除定时器0的溢出标志位(TF0)TR0 = 0;             // 关闭定时器0的计时
}// 设置定时器0计数器值
void Timer0_SetCounter(unsigned int Value)
{TH0 = Value / 256;   // 将高8位写入TH0寄存器TL0 = Value % 256;   // 将低8位写入TL0寄存器
}// 获取定时器0当前的计数值
unsigned int Timer0_GetCounter(void)
{return (TH0 << 8) | TL0;  // 将TH0和TL0的值组合成16位数并返回
}// 启动或停止定时器0的计时功能
// 参数:Flag为1时启动定时器,Flag为0时停止定时器
void Timer0_Run(unsigned char Flag)
{TR0 = Flag;  // 根据Flag设置TR0位,1表示启动定时器0,0表示停止定时器0
}

注释说明:

  • Timer0_Init():初始化定时器0为模式1(16位定时器模式),并设置初值。此模式下,定时器从TH0|TL0组合的16位计数器开始计数,直到溢出。
  • Timer0_SetCounter():根据传入的Value值,设置定时器的当前计数值。
  • Timer0_GetCounter():获取当前定时器的计数值(16位数),将高8位和低8位组合返回。
  • Timer0_Run():启动或停止定时器的计时功能,通过Flag参数来控制。

INT0.c

#include <REGX52.H>// 外部中断0初始化函数
void INT0_Init(void)
{IT0 = 1;  // 设置外部中断0为下降沿触发模式(1为下降沿触发,0为低电平触发)IE0 = 0;  // 清除外部中断0的中断标志位EX0 = 1;  // 使能外部中断0EA = 1;   // 全局中断使能PX0 = 1;  // 设置外部中断0为高优先级(1为高优先级,0为低优先级)
}/*
// 外部中断0的中断服务函数
void Int0_Routine() interrupt 0
{Number++;  // 每次触发外部中断0时,将变量 Number 自增
}
  • INT0_Init():初始化外部中断0,将其配置为下降沿触发,并使能相关中断。通过设置中断优先级位来设置其为高优先级中断。
    • IT0 = 1:配置外部中断0为下降沿触发。
    • IE0 = 0:清除中断标志位,确保没有待处理的中断。
    • EX0 = 1:使能外部中断0。
    • EA = 1:开启全局中断,使单片机响应中断。
    • PX0 = 1:设置外部中断0为高优先级。
  • Int0_Routine():这是外部中断0的中断服务函数,执行中断时该函数会被调用。在该例子中,每次外部中断触发时,变量 Number 会自增。

注:中断服务函数 Int0_Routine() 被注释掉,可以根据需要进行启用。

IR.c  (核心代码:主要处理红外遥控接收和发送数据)

#include <REGX52.H>
#include "Timer0.h"
#include "INT0.h"unsigned int IR_Time;              // 用于记录红外信号的时间间隔
unsigned char IR_State;            // 用于记录红外接收状态机的状态
unsigned char IR_Data[4];          // 用于存储接收到的红外数据信息
unsigned char IR_pData;            // 用于记录当前接收数据的位索引unsigned char IR_DataFlag;         // 用于标记是否接收到数据帧
unsigned char IR_RepeatFlag;       // 用于标记是否接收到连发信号
unsigned char IR_Address;          // 存储接收到的红外遥控器地址
unsigned char IR_Command;          // 存储接收到的红外遥控器命令// 红外遥控初始化函数
void IR_Init(void) 
{INT0_Init();           // 初始化外部中断0,用于捕获红外接收信号的下降沿Timer0_Init();         // 初始化定时器0,用于计时
}// 获取数据帧标志位的函数
unsigned char IR_GetDataFlag(void)  
{if(IR_DataFlag)        // 如果收到数据帧标志置位{IR_DataFlag = 0;   // 清除数据帧标志return 1;          // 返回1表示已接收到数据帧}return 0;              // 否则返回0
}// 获取连发信号标志位的函数
unsigned char IR_GetRepeatFlag(void) 
{if(IR_RepeatFlag)      // 如果收到连发信号标志置位{IR_RepeatFlag = 0; // 清除连发信号标志return 1;          // 返回1表示已接收到连发信号}return 0;              // 否则返回0
}// 获取接收到的红外遥控地址
unsigned char IR_GetAddress(void)  
{return IR_Address;
}// 获取接收到的红外遥控命令
unsigned char IR_GetCommand(void) 
{return IR_Command;
}// 外部中断0服务函数,处理红外接收的信号
void Int0_Routine() interrupt 0
{// 状态0:空闲状态,检测到信号开始,重置计时器if(IR_State == 0)  {Timer0_SetCounter(0);    // 将定时器计数器清零Timer0_Run(1);           // 启动定时器IR_State = 1;            // 设置状态为1,等待信号}// 状态1:等待Start信号或Repeat信号else if(IR_State == 1)  {IR_Time = Timer0_GetCounter(); // 获取上次中断到此次中断的时间Timer0_SetCounter(0);          // 计时器清零准备下一次测量// 检测到Start信号if(IR_Time > (12442-500) && IR_Time < (12442+500)){IR_State = 2;        // 转换到状态2,准备接收数据}// 检测到Repeat信号else if(IR_Time > (10368-500) && IR_Time < (10368+500)){IR_RepeatFlag = 1;   // 置连发信号标志Timer0_Run(0);       // 停止定时器IR_State = 0;        // 返回空闲状态}else{IR_State = 1;        // 未收到有效信号,保持状态1}}// 状态2:接收数据else if(IR_State == 2)  {IR_Time = Timer0_GetCounter();  // 获取时间间隔Timer0_SetCounter(0);           // 计时器清零// 判断是逻辑0还是逻辑1if(IR_Time > (1032-500) && IR_Time < (1032+500)){IR_Data[IR_pData/8] &= ~(0x01 << (IR_pData % 8)); // 写入逻辑0IR_pData++;         // 数据位索引加1}else if(IR_Time > (2074-500) && IR_Time < (2074+500)){IR_Data[IR_pData/8] |= (0x01 << (IR_pData % 8));  // 写入逻辑1IR_pData++;         // 数据位索引加1}else{IR_pData = 0;       // 出现错误,数据位清零,返回状态1IR_State = 1;}// 如果接收到完整的32位数据if(IR_pData >= 32){IR_pData = 0;// 检查数据的有效性if((IR_Data[0] == ~IR_Data[1]) && (IR_Data[2] == ~IR_Data[3])){IR_Address = IR_Data[0];     // 提取地址码IR_Command = IR_Data[2];     // 提取命令码IR_DataFlag = 1;             // 置数据帧标志位}Timer0_Run(0);  // 停止定时器IR_State = 0;   // 返回空闲状态}}
}

代码思路

  1. 初始化部分

    • 调用 IR_Init() 函数来初始化外部中断0和定时器0,确保红外接收能够计时和响应信号的变化。

  2. 状态机控制

    • 通过 IR_State 进行状态管理。程序根据中断触发的时间判断信号类型(启动信号、连发信号或数据)。

    • 根据不同状态,程序分别执行启动计时、判断信号类型、接收并解码红外数据的操作。

  3. 数据接收和处理

    • 红外信号的编码通过时间间隔来区分逻辑0和逻辑1。程序根据红外信号的时间长短判断数据位,最终解码成地址和命令。

    • 采用位操作将红外信号按位存入 IR_Data 数组,并在接收到完整32位数据后校验数据的有效性(利用地址和命令码的互补关系)。

  4. 标志位与状态管理

    • 使用 IR_DataFlagIR_RepeatFlag 来标记是否接收到完整数据帧或连发信号。

    • 外部模块可以通过 IR_GetDataFlag() 等函数获取这些标志位,并作出相应的处理。

函数调用关系

  • IR_Init() 初始化中断和定时器。

  • Int0_Routine() 是外部中断服务函数,负责处理红外信号,状态机在这个函数中运行。

  • IR_GetDataFlag()IR_GetRepeatFlag() 函数提供了对外的接口,用于其他模块判断红外遥控信号状态。

  • IR_GetAddress()IR_GetCommand() 函数用于获取解码后的红外遥控地址和命令。

目的和好处

  • 状态机的设计:通过 IR_State 实现多阶段处理红外信号(如空闲、判断信号、数据接收),结构清晰,便于调试与扩展。

  • 标志位管理:通过 IR_DataFlagIR_RepeatFlag 等标志位,模块化地提供状态信息,便于其他模块获取数据而不直接干扰中断逻辑。

  • 位操作与定时器结合:通过定时器捕获时间差,结合位操作解码数据,使得红外信号的处理精确而高效。

思想方法

  • 模块化设计:将不同功能(如定时器、外部中断、信号解码)分离为不同的函数,方便维护和扩展。

  • 状态机模型:通过有限状态机的方式处理信号,能够清晰管理不同阶段的任务,避免逻辑混乱。

  • 时间敏感的处理:通过定时器精确测量时间,结合中断机制,高效地接收和处理红外信号。

实验二:红外遥控控制电机调速实验(其余代码往期实验)

目的是通过红外遥控器来控制电机的转速,并通过数码管显示当前速度值。具体功能如下:

  1. 红外遥控输入

    • 程序接收红外遥控器的命令码,根据不同的按键(如 IR_0, IR_1, IR_2, IR_3)来设置不同的电机速度。

  2. 电机速度控制

    • 程序根据遥控器的命令,通过 Motor_SetSpeed() 函数来调整电机的实际转速。设定的速度分为4个级别:停止、50%、75%、100%。

  3. 数码管显示

    • 使用数码管 Nixie() 来显示当前的速度状态,将当前的速度级别(0、1、2、3)显示在数码管上。

总体思想

  1. 模块化设计

    • 程序分为几个独立模块,如 Motor_Init() 初始化电机,IR_Init() 初始化红外接收,Motor_SetSpeed() 设置电机转速,Nixie() 用于显示当前速度值。这样的设计便于调试和扩展。

  2. 红外控制

    • 利用红外接收模块,通过接收遥控器发送的命令码,改变电机的速度。程序通过不断检查是否有数据帧到来,一旦收到有效数据,就解析命令码并做出响应。

  3. 状态机思想

    • 代码使用简单的状态判断(速度级别为0到3),根据不同的命令调整电机的运行状态,并通过显示屏实时反馈当前状态。这种逻辑使得代码结构清晰,处理不同输入时能够快速响应。

  4. 解耦合和灵活性

    • 将电机控制和遥控器输入解耦合,遥控器只需发出简单的命令,电机部分负责实现速度控制。通过简单扩展,系统可以支持更多的命令和功能,例如控制电机的方向或其他附加功能。

这样做的好处

  • 可扩展性:功能可以方便地扩展,比如增加更多速度级别或其他控制命令,代码结构简单易懂,模块化设计便于维护。

  • 用户友好:通过遥控器方便地控制电机运行,数码管实时显示当前状态,用户能直观地知道电机运行情况。

  • 高效运行:通过红外信号触发的状态机结构,使得程序在低资源消耗下响应快速。

main.c

void main()
{Motor_Init();        // 电机初始化IR_Init();           // 红外遥控初始化while(1)             // 主循环{if(IR_GetDataFlag())  // 如果接收到红外遥控的有效数据帧{Command = IR_GetCommand();  // 获取遥控器发来的命令码// 根据不同的遥控命令码设置速度值if(Command == IR_0) { Speed = 0; }   // 命令码为 0,速度设为 0if(Command == IR_1) { Speed = 1; }   // 命令码为 1,速度设为 1if(Command == IR_2) { Speed = 2; }   // 命令码为 2,速度设为 2if(Command == IR_3) { Speed = 3; }   // 命令码为 3,速度设为 3// 根据速度值调整电机的实际转速if(Speed == 0) { Motor_SetSpeed(0); }    // 速度0,电机停止if(Speed == 1) { Motor_SetSpeed(50); }   // 速度1,电机以50%的功率运行if(Speed == 2) { Motor_SetSpeed(75); }   // 速度2,电机以75%的功率运行if(Speed == 3) { Motor_SetSpeed(100); }  // 速度3,电机以100%的功率运行}Nixie(1, Speed);  // 数码管显示当前速度值}
}

IR.c(同上)

INT0.c(同上)

Timer0.c(同上)
Timer1.c

#include <REGX52.H>// 定时器1初始化函数,用于配置定时器1,产生一个100微秒的定时中断。
/**
* @brief 定时器初始化(51单片机软件内置配置的定时器)
* @param 无
* @retval 无
*/
void Timer1_Init()		//100微秒@11.0592MHz
{TMOD &= 0x0F;		// 清除定时器1的模式位,保持定时器0的模式不变TMOD |= 0x10;		// 设置定时器1为模式1,即16位定时模式TL0 = 0xA4;		    // 设置定时器1的低位初值为0xA4TH0 = 0xFF;		    // 设置定时器1的高位初值为0xFFTF0 = 0;		    // 清除定时器1的溢出标志位TR0 = 1;		    // 启动定时器1// 启用定时器中断ET0  = 1;  // 打开定时器1中断EA = 1;    // 打开总中断PT0 = 0;   // 设置定时器1中断优先级为低优先级
}/* 定时器中断函数模板 */
/*
void Timer0_Rountine(void)  interrupt 3
{static unsigned int T0Count;  // 用于记录定时器0的计数TL0 = 0x66;		// 设置定时初值TH0 = 0xFC;		// 设置定时初值T0Count++;      // 每次中断发生时自增1if(T0Count >= 1000)  // 1000次中断(即1秒)后执行翻转P2_0口{T0Count = 0;P2_0 = ~P2_0;  // 反转P2_0引脚的输出电平,达到控制外部设备的效果}	
}
*/

Motor.c

#include <REGX52.H>
#include "Timer1.h"sbit Motor = P1^0;  // 将 P1 口的第0位定义为 Motor 引脚,用于控制电机的开关unsigned char Counter, Compare;  // Counter 用于计时,Compare 用于占空比比较/*** @brief 电机初始化函数,初始化定时器1* @param 无* @retval 无*/
void Motor_Init()
{Timer1_Init();  // 调用定时器1初始化函数,开始计时
}/*** @brief 设置电机转速* @param Speed 速度参数,范围为0-100,用于调节电机的占空比* @retval 无*/
void Motor_SetSpeed(unsigned char Speed)
{Compare = Speed;  // 将传入的速度值赋给 Compare,作为占空比参考值
}/*** @brief 定时器1中断服务函数*        使用定时器实现 PWM 调速功能* @param 无* @retval 无*/
void Timer1_Rountine(void) interrupt 3  // 定时器1中断函数
{TL0 = 0xA4;  // 设置定时器初值,确保每次中断后保持相同的定时时间TH0 = 0xFF;  // 设置定时器高位初值Counter++;         // 每次中断时,计时器 Counter 自增1Counter %= 100;    // 将 Counter 限制在 0 到 99 的范围内,形成 100 个周期(模拟 PWM 占空比)// 比较 Counter 和 Compare 的值,用于控制占空比if(Counter < Compare){Motor = 1;  // 如果 Counter 小于 Compare,电机引脚输出高电平,电机通电工作}else{Motor = 0;  // 如果 Counter 大于或等于 Compare,电机引脚输出低电平,电机停止工作}
}

代码说明

  1. Motor_Init():

    • 初始化电机控制模块,内部调用了 Timer1_Init() 函数,启动定时器1以实现电机的速度控制。

  2. Motor_SetSpeed():

    • 该函数用于设置电机的转速。传入的 Speed 参数(范围0-100)表示电机速度的占空比,控制电机开启的时间占整个周期的比例。

  3. Timer1_Rountine():

    • 定时器1的中断服务函数,用于实现电机的PWM(脉宽调制)控制。

    • 定时器每触发一次中断,Counter 自增并与设定的占空比 Compare 进行比较:

      • 如果 Counter 小于 Compare,电机引脚输出高电平(电机运行)。

      • 如果 Counter 大于或等于 Compare,电机引脚输出低电平(电机停止)。

    • 通过这种方式,程序控制了电机开启与关闭的比例,从而调整电机的转速。

总体思想

通过定时器中断实现 PWM 控制,以调节电机的工作状态。Counter 循环自增形成一个周期,并根据占空比 Compare 控制电机的开关,从而实现电机的转速控制。

最后总结

通过C语言编程控制51单片机的几种常见功能,包括红外遥控解码电机转速控制定时器中断外部中断等。这些功能的核心思想是利用单片机的定时器、中断机制和外设来实现对外部设备的控制和数据采集。以下是关键点的总结:

  1. 红外遥控解码

    • 通过外部中断 INT0 捕获红外信号的下降沿,用定时器测量信号脉冲宽度,从而解码出遥控器的地址和命令。

    • 利用状态机进行红外信号的解析,包括起始信号数据位解码重复信号处理。

    • 解码后的命令可以用于控制电机或其他设备的操作。

  2. 定时器的使用

    • 定时器用于生成精准的时间延迟、控制设备(如电机)、或周期性执行任务。

    • 在定时器中断服务函数中,计时器 Counter 和占空比 Compare 的比较决定了电机的开关状态,从而实现 PWM 调速。

  3. PWM 控制电机

    • 通过定时器中断产生的PWM信号控制电机的转速。利用占空比来控制电机工作时间的比例,从而调整电机的转速。

    • 电机控制分为初始化、设置速度和根据设定的速度输出对应的PWM信号,形成灵活的调速机制。

  4. 外部中断

    • 外部中断用于响应外部事件,例如红外遥控信号的输入。中断服务函数快速响应并执行特定任务,确保即使主程序忙碌也能处理紧急事件。

  5. 思想方法

    • 模块化设计:通过封装函数实现功能模块的独立性,如红外解码、定时器配置、电机控制等。这种设计思路使代码具有良好的扩展性和维护性。

    • 状态机:红外解码使用状态机方法,根据接收信号的不同状态(如空闲、等待、解码等)做出相应的处理,确保程序运行稳定。

    • 中断优先机制:中断机制保证了单片机可以及时响应重要事件,如定时器溢出或外部信号输入,而不会影响主程序的执行。

优点和好处:

  • 实时性强:中断机制保证了高优先级任务(如定时和外部输入)的及时响应,不会因为主程序的延迟而错失事件。

  • 精确控制:利用定时器实现精准的时间控制,结合PWM调制实现了对电机速度的精确调节。

  • 模块化和可扩展性:各个功能模块相互独立,便于后期扩展新功能或修改现有功能。

通过这些机制和设计,代码能够高效地管理外部设备和信号输入,实现自动化控制和调节功能。


http://www.mrgr.cn/news/31706.html

相关文章:

  • Chrome 浏览器开启打印模式
  • 1、C语言学习专栏介绍
  • 【含开题报告+文档+PPT+源码】基于Spring Boot智能综合交通出行管理平台的设计与实现
  • Vulnhub靶场 Billu_b0x 练习
  • Vue前端开发,组件及组件的使用
  • C++编程:利用环形缓冲区优化 TCP 发送流程,避免 Short Write 问题
  • vmware workstation player 17.5.1 安装教程和资源
  • Linux笔记
  • Java的IO流(一)
  • 常见排序(C语言版)
  • Windows系统使用PHPStudy搭建Cloudreve私有云盘公网环境远程访问
  • 【后端】【nginx】nginx常用命令
  • 影刀RPA实战:网页爬虫之药品数据
  • 2024 “华为杯” 中国研究生数学建模竞赛(E题)深度剖析|高速公路应急车道启用建模|数学建模完整代码+建模过程全解全析
  • 高校心理辅导系统:Spring Boot技术实现指南
  • linux----进程地址空间
  • 2024华为杯C题详细完整思路和视频讲解
  • 数据飞轮崛起:数据中台真的过时了吗?
  • 树莓派配置Qt+OpenCV
  • 数据结构|二叉搜索树
  • 【模板进阶】完美转发
  • 【CPU】CPU的物理核、逻辑核、超线程判断及L1、L2、L3缓存、CacheLine和CPU的TBL说明
  • Rust 运算符快速了解
  • 2024华为杯数学建模研赛F题建模代码思路文章研究生数学建模
  • thinkphp8 从入门到放弃(后面会完善用到哪里写到哪)
  • 【图文详解】什么是微服务?什么是SpringCloud?