一、环境介绍
单片机采用:STM32F103C8T6
上网方式:采用ESP8266,也可以使用其他设备代替,只要支持TCP协议即可。比如:GSM模块、有线网卡等。
开发软件:keil5
物联网平台: 腾讯IOT物联网物联网平台。腾讯的物联网平台比起其他厂家的物联网平台更加有优势,腾讯物联网平台可以将数据推到微信小程序上,用户可以直接使用小程序绑定设备,完成与设备之间交互,现在用户基本都会使用微信,所以使用起来非常方便。
本文章配套使用的STM32设备端完整源代码下载地址: https://download.csdn.net/download/xiaolong1126626497/18785807
STM32+ESP8266使用MQTT协议连接OneNET 中国移动物联网开发平台:https://blog.csdn.net/xiaolong1126626497/article/details/107385118
STM32+ESP8266使用MQTT协议连接阿里云物联网开发平台:https://blog.csdn.net/xiaolong1126626497/article/details/107311897
二、功能介绍
本文章接下会介绍如何在腾讯物联网平台上创建设备、配置设备、推送到微信小程序、并编写STM32设备端代码,使用ESP8266联网登录腾讯物联网平台,完成数据交互。
功能: STM32采集环境温度、湿度、光照强度实时上传至物联网平台,在微信小程序页面上,用户可以实时查看这些数据,并且可以通过界面上的按钮控制设备端的电机、LED灯的开关,完成数据上传和远程控制。
说明: STM32设备端所有代码均有自己全部编写,没有使用任何厂家的SDK,MQTT协议也是参考MQTT官方文档编写;ESP8266也没有使用任何专用固件,所以代码的移植性非常高。 任何能够联网的设备都可以参考本篇文章代码连接腾讯物联网平台,达到相同的效果。
三、登录腾讯物联网平台创建设备
腾讯云官网: https://cloud.tencent.com/
下面是手机上的截图:操作过程
现在设备是离线状态,是无法查看的,接下来就使用MQTT客户端模拟设备,登录测试。
四、使用MQTT客户端模拟设备--测试
4.1 下载MQTT客户端
MQTT客户端可执行文件下载地址(.exe): https://download.csdn.net/download/xiaolong1126626497/18784012
这个MQTT客户端采用QT开发,如果需要了解它的源码,请看这里: https://blog.csdn.net/xiaolong1126626497/article/details/116779490
4.2 查看物联网平台端口号与域名(IP地址)
官方文档: https://cloud.tencent.com/document/product/634/32546
通过这里得到信息: 如果是广州域的设备(其实哪里都一样,只是服务器距离的远近),就填入 <产品ID>.iotcloud.tencentdevices.com ,端口号是 1883(这是密匙认证的端口号,如果是证书认证就是另一个)。
查看产品ID的方法:
得打产品ID之后,那么要连接我的设备,域名就填: 8O76VHCU7Y.iotcloud.tencentdevices.com 端口就填: 1883
由于我的测试用的MQTT客户端不支持域名输入,只支持IP地址输入,所有我这里需要先将域名转为IP地址在进行下面的测试,ESP8266内部支持域名解析的,所有可以直接输入域名即可,不需要做这一步。
在线解析域名的网址: https://site.ip138.com/8O76VHCU7Y.iotcloud.tencentdevices.com/
得到广州腾讯云的IP地址为: 106.55.124.154
4.3 生成MQTT登录参数
就像我们登录QQ、登录微信需要账号密码一样,设备登录物联网平台也需要类似的东西。
官方文档地址: https://cloud.tencent.com/document/product/634/32546
上面需要的参数,在设备调试页面,点击具体的设备进行查看:
Python源代码:
-
#!/usr/bin/python
-
# -*- coding: UTF-8 -*-
-
import base64
-
import hashlib
-
import hmac
-
import random
-
import string
-
import time
-
import sys
-
# 生成指定长度的随机字符串
-
def RandomConnid(length):
-
return
''.join(random.choice(string.ascii_uppercase + string.digits)
for _
in range(length))
-
# 生成接入物联网通信平台需要的各参数
-
def IotHmac(productID, devicename, devicePsk):
-
# 1. 生成 connid 为一个随机字符串,方便后台定位问题
-
connid = RandomConnid(
5)
-
# 2. 生成过期时间,表示签名的过期时间,从纪元1970年1月1日 00:00:00 UTC 时间至今秒数的 UTF8 字符串
-
expiry = int(time.time()) +
30*
24*
60 *
60
-
# 3. 生成 MQTT 的 clientid 部分, 格式为 ${productid}${devicename}
-
clientid =
"{}{}".format(productID, devicename)
-
# 4. 生成 MQTT 的 username 部分, 格式为 ${clientid};${sdkappid};${connid};${expiry}
-
username =
"{};12010126;{};{}".format(clientid, connid, expiry)
-
# 5. 对 username 进行签名,生成token
-
secret_key = devicePsk.encode(
'utf-8')
# convert to bytes
-
data_to_sign = username.encode(
'utf-8')
# convert to bytes
-
secret_key = base64.b64decode(secret_key)
# this is still bytes
-
token = hmac.new(secret_key, data_to_sign, digestmod=hashlib.sha256).hexdigest()
-
# 6. 根据物联网通信平台规则生成 password 字段
-
password =
"{};{}".format(token,
"hmacsha256")
-
return {
-
"clientid" : clientid,
-
"username" : username,
-
"password" : password
-
}
-
if __name__ ==
'__main__':
-
# 参数分别填入: 产品ID,设备名称,设备密匙
-
print(IotHmac(
"8O76VHCU7Y",
"SmartAgriculture",
"OHXqYLklNBU4xLqqoZbXMQ=="))
得到的登录信息如下:
-
clientid:
8O
76VHCU
7YSmartAgriculture
-
username:
8O
76VHCU
7YSmartAgriculture;
12010126;J
4MCD;
1623766532
-
password: a
962b
484079864239148b
255281d
54372aa
66247aa
8d
6259d
11aa
6fef
650fd
5b;hmacsha
256
4.4 了解主题上报和订阅的格式
登录之前需要先了解如何订阅设备的主题和上报的数据流格式。
如果设备端想要得到APP页面的按钮状态就需要订阅属性下发和属性上报的响应,主题格式就是这样的:
-
格式:
-
$thing
/down/property
/8O76VHCU7Y/设备名称
-
示例:
-
$thing
/down/property
/8O76VHCU7Y/SmartAgriculture
如果设备端想要像APP页面上传数据,那么就需要使用属性上报--发布主题:
-
格式:
-
$thing
/up/property
/8O76VHCU7Y/${deviceName}
-
示例:
-
$thing
/up/property
/8O76VHCU7Y/SmartAgriculture
设备端向APP页面上报属性时,需要上传具体的数据,数据流的格式如下:
官方文档: https://cloud.tencent.com/document/product/1081/34916
比如: 我的产品里有温度、湿度、电机三个设备,我可以选择一次上传3个设备的信息,数据格式就这样写:
{"method":"report","clientToken":"123","params":{"temperature":20.23,"humidity":50,"Motor":1}}
其中: "temperature"、"humidity"、"Motor" 是设备的标识符,根据自己的情况修改,冒号后面就是给这个设备上传的具体数据。
4.5 使用MQTT客户端登录设备测试
万事俱备,下面就使用MQTT客户端进行登录测试。
MQTT客户端操作步骤:
1. 填写相关参数
2. 点击登录
3. 订阅主题
4. 发布主题
5. 去APP页面查看信息
4.6 微信小程序效果
已经收到MQTT客户端上传的数据,点击按钮,MQTT客户端也会收到按钮下发的数据。
五、STM32设备端代码
本文章配套使用的STM32设备端完整源代码下载地址: https://download.csdn.net/download/xiaolong1126626497/18785807
5.1 下载程序
5.2 连接状态
STM32设备上按下按键后,手机打开微信小程序可以看到实时上传的数据,速度非常快。
5.3 main.c文件
-
#include "stm32f10x.h"
-
#include "led.h"
-
#include "delay.h"
-
#include "key.h"
-
#include "usart.h"
-
#include <string.h>
-
#include "timer.h"
-
#include "bluetooth.h"
-
#include "esp8266.h"
-
#include "mqtt.h"
-
-
//腾讯物联网服务器的设备信息
-
#define MQTT_ClientID "8O76VHCU7YSmartAgriculture"
-
#define MQTT_UserName "8O76VHCU7YSmartAgriculture;12010126;J4MCD;1623766532"
-
#define MQTT_PassWord "a962b484079864239148b255281d54372aa66247aa8d6259d11aa6fef650fd5b;hmacsha256"
-
-
//订阅与发布的主题
-
#define SET_TOPIC "$thing/down/property/8O76VHCU7Y/SmartAgriculture" //订阅
-
#define POST_TOPIC "$thing/up/property/8O76VHCU7Y/SmartAgriculture" //发布
-
-
char mqtt_message[
200];
//上报数据缓存区
-
-
int main()
-
{
-
u32 time_cnt=
0;
-
u32 i;
-
u8 key;
-
LED_Init();
-
BEEP_Init();
-
KEY_Init();
-
USART1_Init(
115200);
-
TIMER1_Init(
72,
20000);
//超时时间20ms
-
USART2_Init(
9600);
//串口-蓝牙
-
TIMER2_Init(
72,
20000);
//超时时间20ms
-
USART3_Init(
115200);
//串口-WIFI
-
TIMER3_Init(
72,
20000);
//超时时间20ms
-
USART1_Printf(
"正在初始化WIFI请稍等.\n");
-
if(ESP8266_Init())
-
{
-
USART1_Printf(
"ESP8266硬件检测错误.\n");
-
}
-
else
-
{
-
//加密端口
-
//USART1_Printf("WIFI:%d\n",ESP8266_STA_TCP_Client_Mode("OnePlus5T","1126626497","183.230.40.16",8883,1));
-
-
//非加密端口
-
USART1_Printf(
"WIFI:%d\n",ESP8266_STA_TCP_Client_Mode(
"CMCC-Cqvn",
"99pu58cb",
"106.55.124.154",
1883,
1));
-
-
}
-
-
//2. MQTT协议初始化
-
MQTT_Init();
-
//3. 连接OneNet服务器
-
while(MQTT_Connect(MQTT_ClientID,MQTT_UserName,MQTT_PassWord))
-
{
-
USART1_Printf(
"服务器连接失败,正在重试...\n");
-
delay_ms(
500);
-
}
-
USART1_Printf(
"服务器连接成功.\n");
-
-
//3. 订阅主题
-
if(MQTT_SubscribeTopic(SET_TOPIC,
0,
1))
-
{
-
USART1_Printf(
"主题订阅失败.\n");
-
}
-
else
-
{
-
USART1_Printf(
"主题订阅成功.\n");
-
}
-
-
while(
1)
-
{
-
key=KEY_Scan(
0);
-
if(key==
2)
-
{
-
time_cnt=
0;
-
sprintf(mqtt_message,
"{\"method\":\"report\",\"clientToken\":\"123\",\"params\":{\"temperature\":20.23,\"humidity\":50,\"Motor\":1}}");
-
MQTT_PublishData(POST_TOPIC,mqtt_message,
0);
-
USART1_Printf(
"发送状态1\r\n");
-
}
-
else
if(key==
3)
-
{
-
time_cnt=
0;
-
sprintf(mqtt_message,
"{\"method\":\"report\",\"clientToken\":\"123\",\"params\":{\"temperature\":10.23,\"humidity\":60,\"Motor\":0}}");
-
MQTT_PublishData(POST_TOPIC,mqtt_message,
0);
-
USART1_Printf(
"发送状态0\r\n");
-
}
-
-
if(USART3_RX_FLAG)
-
{
-
USART3_RX_BUFFER[USART3_RX_CNT]=
'\0';
-
for(i=
0;i<USART3_RX_CNT;i++)
-
{
-
USART1_Printf(
"%c",USART3_RX_BUFFER[i]);
-
}
-
USART3_RX_CNT=
0;
-
USART3_RX_FLAG=
0;
-
}
-
-
//定时发送心跳包,保持连接
-
delay_ms(
10);
-
time_cnt++;
-
if(time_cnt==
500)
-
{
-
MQTT_SentHeart();
//发送心跳包
-
time_cnt=
0;
-
}
-
}
-
}
-
5.4 mqtt.c
-
#include "mqtt.h"
-
-
u8 *mqtt_rxbuf;
-
u8 *mqtt_txbuf;
-
u16 mqtt_rxlen;
-
u16 mqtt_txlen;
-
u8 _mqtt_txbuf[
256];
//发送数据缓存区
-
u8 _mqtt_rxbuf[
256];
//接收数据缓存区
-
-
typedef
enum
-
{
-
//名字 值 报文流动方向 描述
-
M_RESERVED1 =
0 ,
// 禁止 保留
-
M_CONNECT ,
// 客户端到服务端 客户端请求连接服务端
-
M_CONNACK ,
// 服务端到客户端 连接报文确认
-
M_PUBLISH ,
// 两个方向都允许 发布消息
-
M_PUBACK ,
// 两个方向都允许 QoS 1消息发布收到确认
-
M_PUBREC ,
// 两个方向都允许 发布收到(保证交付第一步)
-
M_PUBREL ,
// 两个方向都允许 发布释放(保证交付第二步)
-
M_PUBCOMP ,
// 两个方向都允许 QoS 2消息发布完成(保证交互第三步)
-
M_SUBSCRIBE ,
// 客户端到服务端 客户端订阅请求
-
M_SUBACK ,
// 服务端到客户端 订阅请求报文确认
-
M_UNSUBSCRIBE ,
// 客户端到服务端 客户端取消订阅请求
-
M_UNSUBACK ,
// 服务端到客户端 取消订阅报文确认
-
M_PINGREQ ,
// 客户端到服务端 心跳请求
-
M_PINGRESP ,
// 服务端到客户端 心跳响应
-
M_DISCONNECT ,
// 客户端到服务端 客户端断开连接
-
M_RESERVED2 ,
// 禁止 保留
-
}_typdef_mqtt_message;
-
-
//连接成功服务器回应 20 02 00 00
-
//客户端主动断开连接 e0 00
-
const u8 parket_connetAck[] = {
0x20,
0x02,
0x00,
0x00};
-
const u8 parket_disconnet[] = {
0xe0,
0x00};
-
const u8 parket_heart[] = {
0xc0,
0x00};
-
const u8 parket_heart_reply[] = {
0xc0,
0x00};
-
const u8 parket_subAck[] = {
0x90,
0x03};
-
-
void MQTT_Init(void)
-
{
-
//缓冲区赋值
-
mqtt_rxbuf = _mqtt_rxbuf;
-
mqtt_rxlen =
sizeof(_mqtt_rxbuf);
-
mqtt_txbuf = _mqtt_txbuf;
-
mqtt_txlen =
sizeof(_mqtt_txbuf);
-
memset(mqtt_rxbuf,
0,mqtt_rxlen);
-
memset(mqtt_txbuf,
0,mqtt_txlen);
-
-
//无条件先主动断开
-
MQTT_Disconnect();
-
delay_ms(
100);
-
MQTT_Disconnect();
-
delay_ms(
100);
-
}
-
-
/*
-
函数功能: 登录服务器
-
函数返回值: 0表示成功 1表示失败
-
*/
-
u8 MQTT_Connect(char *ClientID,char *Username,char *Password)
-
{
-
u8 i,j;
-
int ClientIDLen =
strlen(ClientID);
-
int UsernameLen =
strlen(Username);
-
int PasswordLen =
strlen(Password);
-
int DataLen;
-
mqtt_txlen=
0;
-
//可变报头+Payload 每个字段包含两个字节的长度标识
-
DataLen =
10 + (ClientIDLen+
2) + (UsernameLen+
2) + (PasswordLen+
2);
-
-
//固定报头
-
//控制报文类型
-
mqtt_txbuf[mqtt_txlen++] =
0x10;
//MQTT Message Type CONNECT
-
//剩余长度(不包括固定头部)
-
do
-
{
-
u8 encodedByte = DataLen %
128;
-
DataLen = DataLen /
128;
-
// if there are more data to encode, set the top bit of this byte
-
if ( DataLen >
0 )
-
encodedByte = encodedByte |
128;
-
mqtt_txbuf[mqtt_txlen++] = encodedByte;
-
}
while ( DataLen >
0 );
-
-
//可变报头
-
//协议名
-
mqtt_txbuf[mqtt_txlen++] =
0;
// Protocol Name Length MSB
-
mqtt_txbuf[mqtt_txlen++] =
4;
// Protocol Name Length LSB
-
mqtt_txbuf[mqtt_txlen++] =
'M';
// ASCII Code for M
-
mqtt_txbuf[mqtt_txlen++] =
'Q';
// ASCII Code for Q
-
mqtt_txbuf[mqtt_txlen++] =
'T';
// ASCII Code for T
-
mqtt_txbuf[mqtt_txlen++] =
'T';
// ASCII Code for T
-
//协议级别
-
mqtt_txbuf[mqtt_txlen++] =
4;
// MQTT Protocol version = 4 对于 3.1.1 版协议,协议级别字段的值是 4(0x04)
-
//连接标志
-
mqtt_txbuf[mqtt_txlen++] =
0xc2;
// conn flags
-
mqtt_txbuf[mqtt_txlen++] =
0;
// Keep-alive Time Length MSB
-
mqtt_txbuf[mqtt_txlen++] =
100;
// Keep-alive Time Length LSB 100S心跳包 保活时间
-
-
mqtt_txbuf[mqtt_txlen++] = BYTE1(ClientIDLen);
// Client ID length MSB
-
mqtt_txbuf[mqtt_txlen++] = BYTE0(ClientIDLen);
// Client ID length LSB
-
memcpy(&mqtt_txbuf[mqtt_txlen],ClientID,ClientIDLen);
-
mqtt_txlen += ClientIDLen;
-
-
if(UsernameLen >
0)
-
{
-
mqtt_txbuf[mqtt_txlen++] = BYTE1(UsernameLen);
//username length MSB
-
mqtt_txbuf[mqtt_txlen++] = BYTE0(UsernameLen);
//username length LSB
-
memcpy(&mqtt_txbuf[mqtt_txlen],Username,UsernameLen);
-
mqtt_txlen += UsernameLen;
-
}
-
-
if(PasswordLen >
0)
-
{
-
mqtt_txbuf[mqtt_txlen++] = BYTE1(PasswordLen);
//password length MSB
-
mqtt_txbuf[mqtt_txlen++] = BYTE0(PasswordLen);
//password length LSB
-
memcpy(&mqtt_txbuf[mqtt_txlen],Password,PasswordLen);
-
mqtt_txlen += PasswordLen;
-
}
-
-
-
memset(mqtt_rxbuf,
0,mqtt_rxlen);
-
MQTT_SendBuf(mqtt_txbuf,mqtt_txlen);
-
for(j=
0;j<
10;j++)
-
{
-
delay_ms(
50);
-
if(USART3_RX_FLAG)
-
{
-
memcpy((
char *)mqtt_rxbuf,USART3_RX_BUFFER,USART3_RX_CNT);
-
-
//memcpy
-
-
for(i=
0;i<USART3_RX_CNT;i++)USART1_Printf(
"%#x ",USART3_RX_BUFFER[i]);
-
-
USART3_RX_FLAG=
0;
-
USART3_RX_CNT=
0;
-
}
-
//CONNECT
-
if(mqtt_rxbuf[
0]==parket_connetAck[
0] && mqtt_rxbuf[
1]==parket_connetAck[
1])
//连接成功
-
{
-
return
0;
//连接成功
-
}
-
}
-
-
return
1;
-
}
-
-
/*
-
函数功能: MQTT订阅/取消订阅数据打包函数
-
函数参数:
-
topic 主题
-
qos 消息等级 0:最多分发一次 1: 至少分发一次 2: 仅分发一次
-
whether 订阅/取消订阅请求包 (1表示订阅,0表示取消订阅)
-
返回值: 0表示成功 1表示失败
-
*/
-
u8 MQTT_SubscribeTopic(char *topic,u8 qos,u8 whether)
-
{
-
u8 i,j;
-
mqtt_txlen=
0;
-
int topiclen =
strlen(topic);
-
-
int DataLen =
2 + (topiclen+
2) + (whether?
1:
0);
//可变报头的长度(2字节)加上有效载荷的长度
-
//固定报头
-
//控制报文类型
-
if(whether)mqtt_txbuf[mqtt_txlen++] =
0x82;
//消息类型和标志订阅
-
else mqtt_txbuf[mqtt_txlen++] =
0xA2;
//取消订阅
-
-
//剩余长度
-
do
-
{
-
u8 encodedByte = DataLen %
128;
-
DataLen = DataLen /
128;
-
// if there are more data to encode, set the top bit of this byte
-
if ( DataLen >
0 )
-
encodedByte = encodedByte |
128;
-
mqtt_txbuf[mqtt_txlen++] = encodedByte;
-
}
while ( DataLen >
0 );
-
-
//可变报头
-
mqtt_txbuf[mqtt_txlen++] =
0;
//消息标识符 MSB
-
mqtt_txbuf[mqtt_txlen++] =
0x0A;
//消息标识符 LSB
-
//有效载荷
-
mqtt_txbuf[mqtt_txlen++] = BYTE1(topiclen);
//主题长度 MSB
-
mqtt_txbuf[mqtt_txlen++] = BYTE0(topiclen);
//主题长度 LSB
-
memcpy(&mqtt_txbuf[mqtt_txlen],topic,topiclen);
-
mqtt_txlen += topiclen;
-
-
if(whether)
-
{
-
mqtt_txbuf[mqtt_txlen++] = qos;
//QoS级别
-
}
-
-
for(i=
0;i<
10;i++)
-
{
-
memset(mqtt_rxbuf,
0,mqtt_rxlen);
-
MQTT_SendBuf(mqtt_txbuf,mqtt_txlen);
-
for(j=
0;j<
10;j++)
-
{
-
delay_ms(
50);
-
if(USART3_RX_FLAG)
-
{
-
memcpy((
char *)mqtt_rxbuf,(
char*)USART3_RX_BUFFER,USART3_RX_CNT);
-
USART3_RX_FLAG=
0;
-
USART3_RX_CNT=
0;
-
}
-
-
if(mqtt_rxbuf[
0]==parket_subAck[
0] && mqtt_rxbuf[
1]==parket_subAck[
1])
//订阅成功
-
{
-
return
0;
//订阅成功
-
}
-
}
-
}
-
return
1;
//失败
-
}
-
-
//MQTT发布数据打包函数
-
//topic 主题
-
//message 消息
-
//qos 消息等级
-
u8 MQTT_PublishData(char *topic, char *message, u8 qos)
-
{
-
int topicLength =
strlen(topic);
-
int messageLength =
strlen(message);
-
static u16 id=
0;
-
int DataLen;
-
mqtt_txlen=
0;
-
//有效载荷的长度这样计算:用固定报头中的剩余长度字段的值减去可变报头的长度
-
//QOS为0时没有标识符
-
//数据长度 主题名 报文标识符 有效载荷
-
if(qos) DataLen = (
2+topicLength) +
2 + messageLength;
-
else DataLen = (
2+topicLength) + messageLength;
-
-
//固定报头
-
//控制报文类型
-
mqtt_txbuf[mqtt_txlen++] =
0x30;
// MQTT Message Type PUBLISH
-
-
//剩余长度
-
do
-
{
-
u8 encodedByte = DataLen %
128;
-
DataLen = DataLen /
128;
-
// if there are more data to encode, set the top bit of this byte
-
if ( DataLen >
0 )
-
encodedByte = encodedByte |
128;
-
mqtt_txbuf[mqtt_txlen++] = encodedByte;
-
}
while ( DataLen >
0 );
-
-
mqtt_txbuf[mqtt_txlen++] = BYTE1(topicLength);
//主题长度MSB
-
mqtt_txbuf[mqtt_txlen++] = BYTE0(topicLength);
//主题长度LSB
-
memcpy(&mqtt_txbuf[mqtt_txlen],topic,topicLength);
//拷贝主题
-
mqtt_txlen += topicLength;
-
-
//报文标识符
-
if(qos)
-
{
-
mqtt_txbuf[mqtt_txlen++] = BYTE1(id);
-
mqtt_txbuf[mqtt_txlen++] = BYTE0(id);
-
id++;
-
}
-
memcpy(&mqtt_txbuf[mqtt_txlen],message,messageLength);
-
mqtt_txlen += messageLength;
-
-
MQTT_SendBuf(mqtt_txbuf,mqtt_txlen);
-
return mqtt_txlen;
-
}
-
-
void MQTT_SentHeart(void)
-
{
-
MQTT_SendBuf((u8 *)parket_heart,
sizeof(parket_heart));
-
}
-
-
void MQTT_Disconnect(void)
-
{
-
MQTT_SendBuf((u8 *)parket_disconnet,
sizeof(parket_disconnet));
-
}
-
-
void MQTT_SendBuf(u8 *buf,u16 len)
-
{
-
USARTx_DataSend(USART3,buf,len);
-
}
转载:https://blog.csdn.net/xiaolong1126626497/article/details/116902653