03-51单片机定时器和串口通信

news/2025/1/12 18:34:39/

一、单片机>51单片机定时器

1.定时器介绍

1.1为什么要使用定时器

在前面的学习中,用到了 Delay 函数延时,这里学习定时器以后,就可以通过定时器来完成,当然定时器的功能远不止这些:

  1. 51 单片机的定时器既可以定时,又可以计数,故称之为定时器/计数器;
  2. 定时器/计数器和单片机的 CPU 是相互独立的,定时器/计数器工作的过程是自动完成的,不需要 CPU 的参与,相比于 Delay 函数需要 CPU 才能运行,节约了 CPU 资源;
  3. 51 单片机中的定时器/计数器有两种计时/计数方式:一是机器内部的时钟,二是外部的脉冲信号对寄存器中的数据加 1;
  4. 通过定时器/计数器,可以增加单片机的效率,一些简单的重复加 1 的工作可以交给定时器/计数器处理,CPU 转而处理一些复杂的事情,同时可以实现精确定时作用。

1.2定时器的结构

  • 定时器/计数器的结构示意图如下:

在这里插入图片描述

  1. T0 和 T1 引脚对应单片机的 P34 和 P35 管脚,定时器由两个特殊功能的寄存器控制:工作方式寄存器(TMOD)用于确定工作方式和功能、控制寄存器(TCON)用于控制 T0、T1 的启动和停止及设置溢出标志;

  2. 工作方式寄存器(TMOD):其低四位用于 T0,高四位用于 T1,不可位寻址:

    在这里插入图片描述

    GATE 是门控位;

    C/T :定时/计数模式选择位,C/T =0 为定时模式,C/T =1 为计数模式;

    M1和 M0:工作方式设置位,00 01 10 11 分别代表 方式0 方式1 方式2 方式3。

  3. 控制寄存器(TCON):低 4 位用于控制外部中断,高 4 位用于控制定时/计数器的启动和中断申请,可位寻址:

    在这里插入图片描述

    TF1:T1 溢出中断请求标志位,T1 计数溢出时由硬件自动置 TF1为 1,CPU 响应中断后 TF1 由硬件自动清 0;

    TR1:T1 运行控制位,TR1 置 1 时,T1 开始工作,TR1 置 0 时,T1 停止工作;

    TF0 和 TR0 同上,只不过是针对 T0 号引脚;

    IE1:外部中断 1 中断请求标志位;

    IT1:外部中断 1 中断请求标志位;

    IT0 和 IE0 同理;

    TL0和TH0:机器周期脉冲,它们每个占4字节,两个组成了8字节,TL0是低4位,TH0高4位。每发出一次脉冲,计数器 + 1,最大值为65535,超过最大值就会导致中断位置1。可以为其设置初始值,计算每一次脉冲需要多少时间,然后得到固定时间如 1ms 会发生多少次脉冲,这样就能达到计时效果。

1.3定时器工作原理

上面介绍定时器的工作方式有四种,我们主要使用方式1, 16位定时计数器,其工作模式结构体如下:

在这里插入图片描述

在这里插入图片描述

  1. STC89C52RC 单片机内有两个可编程的定时/计数器 T0、T1 和一个特殊功能定时器 T2,定时/计数器的实质是加 1 计数器(16 位),由高 8 位和低 8 位两个寄存器 TH0 和 TL0 组成,最大值为 65535;
  2. 如图时钟系统每来一个脉冲,计数系统就自动加 1,当加到计数器为全 1 时,再输入一个脉冲就使计数器溢出回零,同时使相应的中断标志位 TF0 置 1,向 CPU 发出中断请求。如果是在计数模式,表明计数已满,类似于我们 while 循环时回写一个计数器,每运行一次+1、定时模式,表示定时时间已满,类似于之前用的delay函数,延时已到;
  3. SYSclk:系统时钟,即晶振周期,晶振是一个能发生固定频率振动的器件,本开发板上的晶振为11.0592MHz(前面说错了,但不影响代码逻辑),C/T 为0时使用系统时钟,功能相当于定时器;
  4. T0 Pin:单片机外部引脚,C/T 为 1 时使用 T0 引脚,功能相当于计数器;
  5. GATE:上面介绍它是门控位,它能决定是否能够由 TR0 或 TR1 单独控制启动定时器。如果 GATE 为 0 ,经过非门为 1 ,经过或门,不管外部中断 INTO 是什么,结果还是 1 ,然后与上 TR0 或 TR1,TR0 或 TR1 为 1 代表打开计数器,为 0 关闭,可以单独控制计数器开关状态,不受外部中断 INTO 的影响;
  6. 计数器初值与计数个数的关系为:X=2^16 - N,X 为初值,N 为个数。

1.5定时器与中断

中断系统是为了使 CPU 具有对外界紧急事件的实时处理能力而设置的。

  • 原理图:

在这里插入图片描述

如图所示,当我们正在运行某个程序的时候,发出了某些特定的 “信号” ,如:计时器结束,或计数器达到预定值的时候,就会发出中断请求,CPU 收到中断请求,暂停主程序,转而去执行中断程序,等中断程序执行完成,再返回继续执行主程序。

STC89C52RC 单片机提供了 8 个中断请求源:外部中断0(INTO)、外部中断 1(INT1)、外部中断 2(INT2)、外部中断 3(INT3)、定时器 0中断、定时器 1 中断、定时器 2 中断、串口(UART)中断。当多个中断发生的时候,具体先执行哪个中断程序,要看中断的优先级,优先级高的先执行,低的后执行。如果一个中断程序正在执行,但是有一个中断优先级更高的中断请求,这暂停当前中断程序,去执行优先级更高的中断程序,这个叫中断嵌套。

  • 中断内部结构图:

在这里插入图片描述

  • 说明:目前不使用外部中断,因此外部中断暂时不讲:
    1. T0 对应的是 P3.4 口的附加功能,TF0(TCON.5),片内定时/计数器 T0 溢出中断请求标志,当定时/计数器 T0 溢出时,TF0位置 1 ,并向 CPU 申请中断。
    2. T1 对应的是 P3.5 口的附加功能,TF1(TCON.7),片内定时/计数器 T1溢出中断请求标志,当定时/计数器 T1 溢出时,TF1位置1,并向 CPU 申请中断。
    3. EA=1 时打开总中断开关;ET0=1,开时钟中断 0;PT0=0/1,选择中断触发方式。

2.定时器案例

2.1按键控制流水灯左右移动

  • 代码演示
  • Timer0.c:
#include <REGX52.H>// 手动写的
void Timer0_Init()
{// 配置 TMOD// TMOD=0x01;TMOD=TMOD&0xF0;TMOD=TMOD|0x01;// 配置 TCONTF0=0; // 默认中断是关闭的TR0=1; // 开启0号定时器// 赋初值TH0=0xFC; // 1111 1100TL0=0x18; // 0001 1000// 配置中断ET0=1;EA=1;PT0=0;
}// 软件生成的
void Timer0Init(void)		//1毫秒@12.000MHz
{TMOD &= 0xF0;		//设置定时器模式TMOD |= 0x01;		//设置定时器模式TL0 = 0x18;		//设置定时初值TH0 = 0xFC;		//设置定时初值TF0 = 0;		//清除TF0标志TR0 = 1;		//定时器0开始计时// 软件生成的不包括中断配置,要手动添加ET0=1;EA=1;PT0=0;
}// 定时器中断函数模板,因为定时器会用到主函数的一些东西
// 不太方便模块化
/*// 发生中断请求时执行的中断函数
void Timer0_Routine() interrupt 1
{	// 定义变量用于计算中断函数执行次数,应设置静态,否则函数每次调用结束就释放了,永远无法加到1000static unsigned int Count=0;// 赋初值TH0=0xFC;TL0=0x18;Count++;if(Count >= 1000) // 设置1000即执行中断函数1000次,1000ms=1s{Count=0;}
}*/
  1. 配置 TMOD 的时候,可以直接给它赋值,如TMOD=0x01,但这样有个坏处就是只能给 Timer0 设置,如果本来 Timer1 就有设置,那么这种写法会将高四位置0,将 Timer1 的设置清空。因此,可以先将原本的 TMOD 按位与清空低四位:TMOD=TMOD&0xF0,按位与,遇 1 不变,遇 0 置 0 ;再将 TMOD 按位与上对应的断码,只设置低四位:TMOD |= 0x01。这样就完成了只针对 Timer0 的设置,同时不影响 Timer1 ;

  2. 这里的 TMOD 配置,使用方式1,定时模式,Timer1 时钟,TR0 单独控制,参照表格,将对应位赋值就行;

  3. 配置 TCON 的时候,TF0=0将中断默认设置为关闭,如果设置 1 的话,那么一运行就会发生中断;

  4. 计数器赋初值的时候,因为其由两个四位组成,因此分别对高 4 位和低 4 位赋值。单片机内部时钟频率是外部时钟的 12 分频,12MHZ 晶振,单片机内部的时钟频率就是 12/12MHZ,机器周期=1/1M=1us,即每 1 纳秒发送一个脉冲,计数器加一,那么要想定时 1ms 就需要 1000 次脉冲,但计数器最大值为 65535 ,65536时溢出中断,因此,只需要将初始值赋为 64535 即可;

    64535 == 1111 1100 0001 1000;将其分为高低4位就是:
    TH0=0xFC; // 1111 1100
    TL0=0x18; // 0001 1000不过还另外一种写法:
    TH0=65534/256;
    TL0=65534%256;
    
  5. 中断函数不需要放到主函数里去运行,当发出中断信号的时候,会触发其自动调用。每执行一次中断函数,都要为其赋初值,因为溢出后计数器会变 0,不赋初值,下一次会从 0 开始计数,而不是从我们指定值开始。

  • Timer0.h:
#ifndef __TIMER0_H__
#define __TIMER0_H__// 软件生成的
void Timer0Init(void);#endif
  • Key.c:
#include <REGX52.H>
#include "Delay.h"unsigned char Key()
{unsigned char Num=0;if(P3_1==0){Delayxms(20);while(P3_1==0);Delayxms(20);Num=1;}if(P3_0==0){Delayxms(20);while(P3_0==0);Delayxms(20);Num=2;}if(P3_2==0){Delayxms(20);while(P3_2==0);Delayxms(20);Num=3;}if(P3_3==0){Delayxms(20);while(P3_3==0);Delayxms(20);Num=4;}return Num;
}
  • Key.h:
#ifndef __KEY_H__
#define __KEY_H__unsigned char Key();#endif
  • main.c:
#include <REGX52.H>
#include "Timer0.h"
#include "Key.h"
#include <INTRINS.H>// KeyNum用于保存独立按键的键盘对应数值
// LEDMode用于保存流水灯方向的状态值,为1,流水灯从 D1 - D8 ,反之 D8 - D1
unsigned char KeyNum,LEDMode;void main()
{// 设置默认 D1 亮P2=0xFE;// 初始化定时器Timer0Init();while(1){KeyNum=Key();if(KeyNum){if(KeyNum==1){// 按键控制 LEDMode 在 0 和 1 之间切换LEDMode++;if(LEDMode>=2)LEDMode=0;}}}
}void Timer0_Routine() interrupt 1
{static unsigned int Count;// 赋初值TL0 = 0x18;		TH0 = 0xFC;		Count++;		if(Count>=500) // 每 0.5 s执行一次中断函数{Count=0;if(LEDMode==0)P2=_crol_(P2,1); // 左移函数if(LEDMode==1)P2=_cror_(P2,1); // 右移函数}
}
  • 说明:
    1. 上面代码执行的效果,默认执行流水灯 D1 - D8 每 0.5 秒移动一个,按一次 K1 就变成 D8 - D1 移动,再按一次又恢复 D1 - D8 移动,如此循环;
    2. _crol__cror_是官方只带的库函数,使用时需要包含头文件#include <INTRINS.H>,分别代表左移和右移,第一个参数是要操作的8位寄存器,第二个参数是每次移动的步数;
    3. 要清除这里的左移右移并不是对于我们单片机上的 LED 的左移右移,因为有的 LED 是左到右由低到高排列的,有点是由高到低。其实际是相对一个一字节寄存器的8位就行左右移动的,一般左为低位,右为高位,左移就是低位到高位,右移就是高位到低位。因此,_crol_函数可以理解为是低位到高位移动,_cror_函数是高位到低位移动。

2.2 LCD1602 显示时钟

  • 代码演示
#include <REGX52.H>
#include "LCD1602.h"
#include "Timer0.h"unsigned char Hour, Min, Sec, flag;void main()
{// 初始化 LCD1602LCD_Init();// 初始化 Timer0Timer0Init();LCD_ShowChar(2, 3, ':');LCD_ShowChar(2, 6, ':');while(1){// 显示时间LCD_ShowNum(2, 1, Hour, 2);LCD_ShowNum(2, 4, Min, 2);LCD_ShowNum(2, 7, Sec, 2);}
}void Timer0_Routine() interrupt 1
{	static unsigned int Count;// 赋初值TH0=0xFC;TL0=0x18;Count++;if(Count >= 1000){flag++;if(flag>=2)flag=0;if(flag==0)LCD_ShowString(1, 1, "MyClock ^_^     ");else if(flag==1)LCD_ShowString(1, 1, "MyClock ^.^     ");Count=0;Sec++;if(Sec>=60){Sec=0;Min++;if(Min>=60){Min=0;Hour++;if(Hour>=24){Hour=0;}}}}
}
  • 说明:代码运行效果,就是一个普通的时钟显示,以24小时格式显示,上面的小表情每秒变换一次,这个代码的逻辑很简单,就不详细解释了。

二、串口通信

1.串口通信介绍

1.1串口通信方式分类

单片机>51单片机内部自带UART(Universal Asynchronous Receiver Transmitter,通用异步收发器),可实现单片机的串口通信。

  • 按数据传输方式分为:

    1. 串行通信:指使用一条数据线,将数据一位一位地依次传输,每一位数据占据一个固定的时间长度。只需要少数几条线就可以在系统间交换信息,适用于计算机与计算机、计算机与外设之间的远距离通信。可以理解为只有一条通道传输,同一时间只能过一位数据;
    2. 并行通信:通常是将数据字节的各位用多条数据线同时进行传送,通常是 8位、16 位、32 位等数据一起传输。可以理解为,一条通道同一时间只能传输一位数据,但它是多条通道,因此同一时间可以传输多位数字。
  • 按数据同步方式分为:

    1. 异步通信:指通信的发送与接收设备使用各自的时钟控制数据的发送和接收过程。为使双方的收发协调,要求发送和接收设备的时钟尽可能一致;
    2. 同步通信:要建立发送方时钟对接收方时钟的直接控制,通信双方靠一根时钟线来约定通信速率,使双方达到完全同步。
  • 按数据传输方向分为:

    1. 单工:通信只能由一方发送到另一方,不能反向传输;
    2. 半双工:通信双方可以互相传输数据,但必须分时复用一根数据线,即同一时刻只能一方给另外一方发,不能同时给对方发;
    3. 全双工:通信双方可以在同一时刻互相传输数据。

1.2硬件电路

在这里插入图片描述

  1. 简单双向串口通信有两根通信线(发送端TXD和接收端RXD);
  2. TXD与RXD要交叉连接,即一台计算机的TXD要与另外一台电脑的RXD连接;
  3. 当只需单向的数据传输时,可以直接一根通信线,发送的计算机接TXD,接收的计算机接RXD;
  4. 当电平标准不一致时,需要加电平转换芯片。

1.3电平标准

串口常用的电平标准有如下三种:

  1. TTL电平:5V 表示 1,0V 表示 0,电平值是相对于 GND 而言的;
  2. RS232电平:-3~-15V 表示1,3~15V 表示 0,电平值是相对于 GND 而言的;
  3. RS485电平:两线压差 2~6V 表示1,-2~-6V 表示 0(差分信号),电平值是两根导线的压差。

1.4UART的工作模式

STC89C52的UART有四种工作模式:

  1. 模式0:同步移位寄存器(00);
  2. 模式1:8位UART,波特率可变,常用(01);
  3. 模式2:9位UART,波特率固定(10);
  4. 模式3:9位UART,波特率可变(11)。

1.5串口参数和时序图

串口参数:

  1. 波特率:串口通信的速率,发送和接收各数据位的间隔时间,因为这里的串口通信是异步通信,没有时钟线,因此为保证收发数据的准确性,需要保证两个设备的波特率一致;
  2. 比特率:每秒钟传输二进制代码的位数,单位是:位/秒( bps);
  3. 检验位:用于数据验证,校验方法有奇校验(odd)、偶校验(even)、0 校验(space)、1 校验(mark)以及无校验(noparity);
  4. 停止位:用于数据帧间隔。

在这里插入图片描述

9位数据格式比8位数据格式多了1位数据,是检验位,主要用于检验数据是否有误。

因为数据是一位一位的发,每发一字节8位数据就会在接收端重组这八位数据得到一字节数据,每字节前后会有起始位和停止位,就是为了标记什么时候开始的一字节数据传输,什么时候一字节传输完成,接收方可以重组数据了。

1.6串口模式图

在这里插入图片描述

  1. SBUF:串口数据缓存寄存器,物理上是两个独立的寄存器,但占用相同的地址。写操作时,将数据写入发送寄存器来发送,读操作时,将接收到的数据读到接收寄存器;
  2. 通过时钟 Timer1 的溢出率控制波特率,经过 2 分频或 16 分频来控制收发控制器的采样时间;
  3. 当有数据发送或者有数据接收完成的时候,都会产生中断申请,执行中断函数。

1.7串口相关寄存器

在这里插入图片描述

  • 串口控制寄存器 SCON
  1. FE用来检测帧错误,这里我们用不到;
  2. SM0 和 SM1 为工作方式选择位:有四种模式,1.4节有介绍,这里我们使用模式1,赋值 01;
  3. SM2 是模式2和模式3使用的,这里不介绍,可以去看单片机手册;
  4. REN 允许/禁止串口接收数据,为 1 允许,为 0 禁止;
  5. TI 发送中断标志位;
  6. RI 接收中断标志位。
  • 电源控制寄存器 PCON:SMOD 波特率倍增位。在串口方式 1、方式 2、方式 3 时,波特率与 SMOD 有关,当 SMOD=1 时,波特率提高一倍。复位时,SMOD=0。

2.串口通信案例

2.1通过串口发送数据到电脑

  • 代码演示
  • Uart.c:
#include <REGX52.H>void Uart_Init()
{// 串口配置PCON &= 0x7F; // 将第八位置0,其他位不变SCON=0x40; // 0100 0000// 定时器配置TMOD &= 0x0F;		//设置定时器模式TMOD |= 0x20;		//设置定时器模式TL1 = 0xFA;		//设定定时初值TH1 = 0xFA;		//设定定时器重装值ET1 = 0;		//禁止定时器1中断TR1 = 1;		//启动定时器1
}void Uart_SendByte(unsigned char Byte)
{SBUF=Byte;while(TI==0);TI=0;
}
  • 说明:

    1. 该案例只接收数据,不发送数据,什么模式一通信,因此SCON=0x40
    2. TI:发送中断标志位,在中断服务程序中,必须用软件将其清 0,取消此中断申请,因此这里的函数中加上了TI=0
    3. 这里配置时钟是配置的 Timer1 ,初始化函数可以通过软件生成,但是要设置好对应参数,如图:

    在这里插入图片描述

  • Uart.h:

#ifndef __UART_H__
#define __UART_H__void Uart_Init();
void Uart_SendByte(unsigned char Byte);#endif
  • main.c:
#include <REGX52.H>
#include "Delay.h"
#include "Uart.h"unsigned char sec;void main()
{// 初始化串口Uart_Init();while(1){// 通过串口发送数据Uart_SendByte(sec);Delayxms(1000);sec++;}
}
  • 说明:其运行的结果是单片机通过串口每秒给电脑发送一个字节的数据,从0开始递增,显示格式位十六进制。

2.2电脑通过串口控制 LED

  • 代码演示
  • Uart.c:
#include <REGX52.H>void Uart_Init()
{// 串口配置PCON &= 0x7F; // 将第八位置0,其他位不变SCON=0x50; // 0101 0000 允许接收// 定时器配置TMOD &= 0x0F;		//设置定时器模式TMOD |= 0x20;		//设置定时器模式TL1 = 0xFA;		//设定定时初值TH1 = 0xFA;		//设定定时器重装值ET1 = 0;		//禁止定时器1中断TR1 = 1;		//启动定时器1// 使能中断EA=1;ES=1;
}void Uart_SendByte(unsigned char Byte)
{SBUF=Byte;while(TI==0);TI=0;
}// 中断函数,用于电脑给单片机发送数据(模板)
/*
void Uart_Routine() interrupt 4
{if(RI==1){P2=~SBUF;Uart_SendByte(SBUF);RI=0;}
}
*/
  • Uart.h:
#ifndef __UART_H__
#define __UART_H__void Uart_Init();
void Uart_SendByte(unsigned char Byte);#endif
  • main:
#include <REGX52.H>
#include "Delay.h"
#include "Uart.h"unsigned char sec;void main()
{// 初始化串口Uart_Init();while(1){}
}void Uart_Routine() interrupt 4
{if(RI==1){P2=~SBUF;Uart_SendByte(SBUF);RI=0;}
}
  1. 代码演示效果,通过电脑给单片机发送一字节数据,如发送 ff ,单片机 LED 灯全亮,同时单片机会回给电脑相同的数据;
  2. 这里将SCON=0x50设置为允许接收数据,同时相比上一个案例添加了中断使能;
  3. RI:接收中断标志位,在中断服务程序中,必须用软件将其清 0,取消此中断申请,因此这里的中断函数中加上了RI=0
  4. 因为这里的RI 和 TI是或门,不管谁中断标志位置1了都会发送中断请求,因此需要在这里判断到底是谁发出来中断请求;
  5. 中断序号查询下表:

在这里插入图片描述


http://www.ppmy.cn/news/1562572.html

相关文章

【Word_笔记】Word的修订模式内容改为颜色标记

需求如下&#xff1a;请把修改后的部分直接在原文标出来&#xff0c;不要采用修订模式 步骤1&#xff1a;打开需要转换的word后&#xff0c;同时按住alt和F11 进入&#xff08;Microsoft Visual Basic for Appliations&#xff09; 步骤2&#xff1a;插入 ---- 模块 步骤3&…

Python AI教程之十六:监督学习之决策树(7)和其它算法的比较

ML | 逻辑回归与决策树分类 逻辑回归和决策树分类是目前最流行和最基本的两种分类算法。没有哪种算法比另一种更好,而一种算法的优越性通常归功于所处理数据的性质。 我们可以在不同类别上比较这两种算法—— 标准 逻辑回归 决策树分类 可解释性 难以解释 更易于解释 决策…

四种常见的身份认证与授权机制

在现代 web 应用开发中&#xff0c;安全的身份认证和授权机制对于确保数据安全和访问控制至关重要。本文将探讨四种常见的机制&#xff1a;JWT&#xff08;JSON Web Tokens&#xff09;、Session&#xff08;会话&#xff09;、SSO&#xff08;单点登录&#xff09; 和 OAuth 2…

当Elasticsearch索引数据量过多时,可以采取以下措施进行优化和部署

调整索引分片数量&#xff1a;根据数据量和集群规模&#xff0c;重新分配索引的分片数量。较小的索引分片可以提高查询性能&#xff0c;但过多的分片也会增加管理开销。因此&#xff0c;需要根据具体情况进行权衡。调整副本数量&#xff1a;根据数据量和查询负载&#xff0c;适…

《零基础Go语言算法实战》【题目 2-5】函数参数的值传递和引用传递

《零基础Go语言算法实战》 【题目 2-5】函数参数的值传递和引用传递 下面代码的输出是什么&#xff1f; package main import "fmt" type Test struct { array []int str string } func asign(t Test) { t.array[0] 88 t.str "Go is good" } func ma…

上海亚商投顾:沪指探底回升微涨 机器人概念股午后爆发

上海亚商投顾前言&#xff1a;无惧大盘涨跌&#xff0c;解密龙虎榜资金&#xff0c;跟踪一线游资和机构资金动向&#xff0c;识别短期热点和强势个股。 一.市场情绪 市场全天探底回升&#xff0c;沪指盘中跌超1.6%&#xff0c;创业板指一度跌逾3%&#xff0c;午后集体拉升翻红…

选择器css

1.a标签选择 // 选中所具有herf 的元素 [herf] {color: skyblue; } // 选中所具有herfhttps://fanyi.youdao.com/ 的元素 [herf$"youdao.com"] {color:pink; } // 按此顺序书写 link visited hover active // 未访问状态 a:link {color:orange } // 访问状态 a…

Spring Boot项目中增加MQTT对接

在Spring Boot项目中增加MQTT对接&#xff0c;通常涉及以下几个步骤&#xff1a; 一、搭建MQTT服务器 首先&#xff0c;你需要搭建一个MQTT服务器&#xff08;Broker&#xff09;。这可以通过多种方式实现&#xff0c;例如使用Docker来部署EMQX或Mosquitto等MQTT Broker。 以…