MQTT协议连接华为云服务器为RTC提供实时时间校准

一、At指令连接云服务器

STA 模式AT指令顺序:

AT\r\n                                                                                测试指令

AT+RST\r\n                                                                       复位

AT+CWMODE=1\r\n                                                         设置为STA模式

AT+CWJAP="xyd","12345678"\r\n                                    连接路由wifi是2.4G

AT+CIPMUX=0\r\n                                                            设置单连接

AT+CIPSTART="TCP","IP",PORT                                    输入IP地址输入PORT端口, 连接服务器

AT+CIPMODE=1\r\n                                                         设置通信为透传模式

AT+CIPSEND\r\n                                                              发送数据(透传模式下)

AT+CIPCLOSE                                                                 断开服务器

通过AT指令获取实时时间

AT+CIPSTART="TCP","api.pinduoduo.com",80\r\n

二、MQTT协议连接华为云

        华为云与设备链接:链接报文->链接确认报文->发布报文->发布确认报文->订阅报文->订阅确认报文->心跳报文->心跳响应报文。

        MQTT报文种类:

       1、 链接报文->链接确认报文:

        CLIENTID 在MQTT华为云(及所有MQTT协议)中是一个唯一标识符,用于在MQTT broker(服务器)中识别每个客户端连接。

        其再进行订阅和发布指令时先清楚缓冲区,是为了避免服务器向设备发送控制指令,设备没接收到还没来得及处理清零,就被拿去接收服务器发来的确认报文,从而造成确认报文接收错误。

#define USERNAME        "692ff555bf22cc5a8c0cd677_air_202"
#define PASSWORD       "da01bc9761e04f89f183977ad1f43282d6b4ba2b10b55039d643ae1d18dd0c9f"
#define CLIENTID        "692ff555bf22cc5a8c0cd677_air_202_0_0_2025120406"

//连接报文
void MQTT_Connect(void)
{
	//清空缓冲区,提前清除一下连接之前接收到的数据
	memset(esp.rxbuff,0,1024);
	esp.rxlen = 0;
	esp.rxover = 0;

	//定义对应变量
	MQTTPacket_connectData data = MQTTPacket_connectData_initializer;//初始化MQTT CONNECT报文的数据结构
	unsigned char buf[200];
	int buflen = sizeof(buf);

	//给结构体参数赋值
	data.clientID.cstring = CLIENTID;//设置MQTT客户端的唯一标识符
	data.keepAliveInterval = 60;//保活时间,每60秒检查一次连接
	data.cleansession = 0   ;//清理会话标志
	data.username.cstring = USERNAME;// 设置用户名
	data.password.cstring = PASSWORD;// 设置密码

	//形成连接报文 并且发送
	int len = MQTTSerialize_connect(buf, buflen, &data);
	Usart3_Tx_Buff(buf,len);
	
	Delay_nms(1000);//等待连接确认报文接收完毕 0x20(报文类型) 0x02(剩余长度) 0x00(链接确认标志) 0x00(链接返回码)

    //验证链接确认报文
	if(esp.rxbuff[0] == 0x20 && esp.rxbuff[1] == 0x02)
	{
		if(esp.rxbuff[3] == 0x00)
			printf("连接成功\r\n");
	}

	//清空缓冲区,提前清除一下连接之前接收到的数据
	memset(esp.rxbuff,0,1024);
	esp.rxlen = 0;
	esp.rxover = 0;
}

       1.1 MQTTPacket_connectData data = MQTTPacket_connectData_initializer; 代码含义

MQTTPacket_connectData 为结构体, MQTTPacket_connectData_initializer这是一个初始化宏,将所有字段设为默认值。

        1.2、data.clientID.cstring = CLIENTID;的代码含义
a、data: MQTTPacket_connectData 结构体实例
b、clientID: 结构体中的客户端标识符字段
c、 cstring: 以null结尾的字符串形式
d、CLIENTID: 你的客户端ID字符串

      1. 3、 clientID 是MQTT连接的"身份证"必须满足
a、 唯一性:同一MQTT代理上不能有重复ID
b、 长度:通常1-23个字符(MQTT 3.1规范)
c、 内容:可打印字符,不能包含空字符

      1.4、 链接确认报文中链接确认标志

a、其结果只能是0x00/0x01,因为高七位为Reservrd保留位,剩下的一位为SP,SP= 0: 没有之前的会话(新会话) ,SP = 1: 服务器恢复了之前的会话。

b、客户端连接时的cleansession标志决定SP位的意义:情况1:cleansession = 1(清理会话),服务器必须,忽略任何之前的会话状态 ,返回 SP = 0 ,开始一个新会话。 情况2:cleansession = 0(恢复会话),服务器尝试恢复会话 SP = 1:成功恢复了之前的会话 , SP = 0:没有之前的会话,开始新会话。

c、SP=1 的实际意义:服务器保留了客户端的,订阅列表(所有之前的订阅),未送达的消息(QoS 1和QoS 2), 可能的其他会话状态。设备断开后重连,SP=1表示:  这些订阅依然有效 不需要重新订阅, 断开期间的消息可能被保留。

d、SP=0的实际意义 :是一个全新的会话 ,没有之前的订阅需要恢复 , 客户端需要重新订阅所有主题。

e、cleansession的关系,cleansession=1 → 必须SP=0;cleansession=0 → SP=0SP=1

        1.5链接确认报文中链接返回码 是CONNACK报文的核心,告诉你连接是否成功以及失败的原因:

返回码含义常见原因解决方案
0x00连接成功-可以开始通信
0x01协议版本不支持客户端版本太新/太旧调整MQTTVersion
0x02客户端ID无效ID为空、太长、非法字符使用有效的clientID
0x03服务器不可用服务器过载、维护等待后重试
0x04用户名密码错误认证失败检查凭证和标志位
0x05未授权权限不足联系管理员

        2、发布报文->发布确认报文

         PUB_TOPIC 在MQTT华为云(以及所有MQTT实现)中指的是发布主题(Publish Topic),是设备向云端发送消息时使用的主题路径。

#define PUB_TOPIC        "$oc/devices/692ff555bf22cc5a8c0cd677_air_202/sys/properties/report"//主题名
//发布报文
void MQTT_Publish(void)
{
    
    //清空缓冲区
	memset(esp.rxbuff,0,1024);
	esp.rxlen = 0;
	esp.rxover = 0;	

	//定义对应变量
	unsigned char buf[500];
	int buflen = sizeof(buf);

    //初始化MQTT主题字符串
	MQTTString topicString = MQTTString_initializer;// 初始化MQTT字符串结构
	topicString.cstring = PUB_TOPIC;// 设置发布主题
	
    Dht_Get();//温湿度获取

	uint8_t paybuff[300] = {0};//保存负载 
	sprintf((char *)paybuff,"{\"services\":[{\"properties\":\
{\"tem\":%.1f,\"hum\":%.1f},\
\"service_id\":\"environment\",\"event_time\":null}]}",dht11.temp,dht11.hum); 
	
	int	len = MQTTSerialize_publish(buf, buflen, 1, 1, 0, 0x1234, topicString, (unsigned char*)paybuff, strlen((char *)paybuff));
	Usart3_Tx_Buff(buf,len);
	
	Delay_nms(1000);//等待连接确认报文接收完毕 0x40(报文类型) 0x02(剩余长度) 0x12(报文标识符MSB) 0x34(报文标识符LSB)

    //发布确认报文
	if(esp.rxbuff[0] == 0x40 && esp.rxbuff[1] == 0x02)
	{
		if(esp.rxbuff[2] == 0x12 && esp.rxbuff[3] == 0x34)
			printf("MQTT发送成功\r\n");
	}

	//清空缓冲区
	memset(esp.rxbuff,0,1024);
	esp.rxlen = 0;
	esp.rxover = 0;	

}

        温湿度代码

typedef struct{
	float temp;
	float hum;
	uint8_t temp_int;
	uint8_t temp_dec;
	uint8_t hum_int;
	uint8_t hum_dec;
}DHT11;

extern DHT11 dht11;

void Dht_Get(void)
{
	uint8_t buff[5] = {0};
//通信阶段
	//起始 输出18-30ms 低电平
	DHT_OUT_L;Delay_nms(20);DHT_OUT_H;
	//响应 输入83us低电平 87us高电平
	//等待低电平
	uint8_t count = 0;
	while(DHT_INT == 1){
		Delay_nus(1);count ++;
		if(count > 100)
			return;
	}
	//等待高电平
	 count = 0;
	while(DHT_INT == 0){
		Delay_nus(1);count ++;
		if(count > 100)
			return;
	}
	//数据传输 输入40位数据
	uint8_t i = 0;
	for(i = 0; i < 40; i++)
	{
		count = 0;
		while(DHT_INT == 1){
			Delay_nus(1);count ++;
			if(count > 100)
				return;
		}
		//等待高电平
		 count = 0;
		while(DHT_INT == 0){
			Delay_nus(1);count ++;
			if(count > 100)
				return;
		}
		Delay_nus(40);
		if(DHT_INT == 1)
		{
			buff[i/8] |= 0x01<<(7-i%8);
		}else{
			buff[i/8] &= ~(0x01<<(7-i%8));
		}
				
	}
//数据处理
	//buff[0]  buff[1]  buff[2]  buff[3]  buff[4]
	//湿度整数 湿度小数 温度整数 温度小数  校验
	if((uint8_t)(buff[0]+buff[1]+buff[2]+buff[3])!= buff[4])
		return;
	dht11.temp_int = buff[2];
	if(((buff[3]>>7)&0x01) == 1)
	{
		dht11.temp_dec = buff[3]&(~(0x01<<7));
		dht11.temp = -(dht11.temp_int + dht11.temp_dec * 0.1);
	}else{
		dht11.temp_dec = buff[3];
		dht11.temp = dht11.temp_int + dht11.temp_dec * 0.1;
	}
	dht11.hum_int = buff[0];
	dht11.hum_dec = buff[1];
	dht11.hum =dht11.hum_int + dht11.hum_dec * 0.1;
	
}

        2.1初始化MQTT主题字符串含义

a、方式A:cstring= 指向字符串常量的指针

// 使用C风格字符串(以null结尾)
MQTTString topicString = MQTTString_initializer;
topicString.cstring = "home/temperature";

// 库内部会调用strlen()计算长度
// 优点:简单直观
// 缺点:每次都需要计算长度,效率稍低

b、方式B:lenstring = 结构体包含数据和长度

// 使用带长度的字符串
MQTTString topicString = MQTTString_initializer;
char* topic = "home/temperature";
topicString.lenstring.data = topic;
topicString.lenstring.len = strlen(topic);  // 明确指定长度

// 优点:
// 1. 效率高(无需重复计算长度)
// 2. 可以包含null字符(二进制数据)
// 3. 更符合MQTT协议规范

c、 MQTT协议中,字符串有两种编码方式:1. 普通字符串:前2字节表示长度,后跟字符串数据 2. UTF-8字符串:MQTT 3.1.1要求主题必须是UTF-8。MQTTString结构体封装了这两种需求。

d、MQTT主题规则:1. 不能包含null字符 2. 不能包含通配符(+ #),除非用作通配符 3. 建议使用URL安全字符。

e、 cstring 和 lenstring 两种表示方式cstring简单但效率稍低(需要计算长度)lenstring高效且功能更强(支持二进制数据)。

       2.2、MQTTSerialize_publish 序列化一个MQTT PUBLISH消息

int MQTTSerialize_publish(
    unsigned char* buf,           // 输出缓冲区
                                  //序列化后的PUBLISH报文将存放在这里

    int buflen,                   // 缓冲区大小

    unsigned char dup,            // 重发标志 (1)
                                  // dup = 0: 首次发送的消息
                                  // dup = 1: 重发的消息(例如,未收到确认时重发)

    int qos,                      // QoS等级 (1)
                                  // QoS的三种级别:
                                  // 0: 最多一次(可能丢失)
                                  // 1: 至少一次(可能重复)
                                  // 2: 恰好一次(可靠但开销大)

    unsigned char retained,       // 保留标志 (0)
                                  // retained = 0: 消息不保留(新订阅者不会收到)
                                  // retained = 1: 消息保留(新订阅者会收到最后一条保留消息)
                                  // 使用场景:
                                  // - retained=1: 设备状态、最后已知值
                                  // - retained=0: 实时数据、一次性通知   
  
    unsigned short packetid,      // 数据包ID (0x1234)
                                  // 1. 唯一标识这个QoS 1/2的消息
                                  // 2. 用于确认机制(PUBACK/PUBREC/PUBREL/PUBCOMP)
                                  // 3. 客户端和服务器都需要维护packetid状态

    MQTTString topicName,         // 主题字符串

    unsigned char* payload,       // 消息载荷
                                  // 消息内容

    int payloadlen                // 载荷长度
                                  // 计算长度
);

         重发标志dup 常常伴随着QOS>0时设置为1,当QoS>0的消息未收到确认时,需要重发,所以方便起见直接默认为重发消息。

        3、订阅报文->订阅确认报文

#define SUB_TOPIC        "$oc/devices/692ff555bf22cc5a8c0cd677_air_202/sys/commands/#"//订阅主题

//订阅报文
void MQTT_Subscribe(void)
{
    //清空缓冲区
	memset(esp.rxbuff,0,1024);
	esp.rxlen = 0;
	esp.rxover = 0;	

	//定义对应变量
	unsigned char buf[500];
	int buflen = sizeof(buf);
	
	MQTTString topicString = MQTTString_initializer;
	topicString.cstring = SUB_TOPIC;//订阅基础主题
	int req_qos = 0;
	
    //形成订阅报文 并且发送
	int	len = MQTTSerialize_subscribe(buf, buflen, 0, 0x4321, 1, &topicString, &req_qos);
	Usart3_Tx_Buff(buf,len);
	
	Delay_nms(1000);//等待连接确认报文接收完毕 0x90(报文类型) 0x03(剩余长度) 0x43(识别符MSB) 0x21(识别符LSB) 0x00(返回码值)
    
    //验证报文确认
	if(esp.rxbuff[0] == 0x90 && esp.rxbuff[1] == 0x03)
	{
		if(esp.rxbuff[2] == 0x43 && esp.rxbuff[3] == 0x21)
		{
			if(esp.rxbuff[4] == 0x00 )
				printf("MQTT订阅成功\r\n");
		}
			
	}
	//清空缓冲区
	memset(esp.rxbuff,0,1024);
	esp.rxlen = 0;
	esp.rxover = 0;	
}

         3.1、SUB_TOPIC宏定义内容

$oc/devices/{device_id}/sys/commands/#
├── $oc           : 华为云IoT服务标识
├── devices       : 设备目录
├── 692ff555bf22cc5a8c0cd677_air_202 : 设备ID
├── sys           : 系统相关
├── commands      : 命令
└── #            : 多级通配符(订阅所有命令)

sys/commands - 系统命令通道;# - 订阅所有子主题(通配符所有命令);sys/commands/# 表示订阅系统命令。

        4、心跳报文->心跳响应报文

        心跳报文:保持设备和服务器的连接。 发送的是时间间隔根据连接报文里面—保持连接的参数。data.keepAliveInterval = 60;//保活时间,每60秒检查一次连接。 含义: 1. 客户端承诺:在60秒内至少发送一个控制报文  2. 服务器承诺:如果1.5倍时间内无消息,认为客户端离线。即客户端必须:每60秒内至少发一个报文 服务器判断:90秒无消息则断开连接。

        常见策略: 实际心跳间隔 ≈ keepAliveInterval × 0.75  这样可以保证即使偶尔丢失也能在超时前重试。所以心跳不是随意发送的,而是严格按照CONNECT报文中的keepAliveInterval参数来控制的,这是MQTT协议保持连接的核心机制。

//心跳报文的发送与接收
void MQTT_REQ(void)
{
	memset(esp.rxbuff,0,1024);//清空接收缓冲区
	esp.rxlen=0;
	esp.rxover=0;	

	uint8_t buff[3] = {0xc0,0x00,0x00};//0xc0报文类型
	Usart3_Tx_Buff(buff,2);//发送发布报文
	Delay_nms(1000);//等待接收订阅响应报文 

	if(esp.rxbuff[0] == 0xD0)//0xD0报文类型
		printf("mqtt 心跳成功\r\n");

	memset(esp.rxbuff,0,1024);//清空接收缓冲区
	esp.rxlen=0;
	esp.rxover=0;	
}

三、华为云设备时间同步

1、设备时间同步请求

        在发送设备时间同步请求之前,需要先订阅服务器器下行主题。

        从华为云官方文档中可以获得官方示例,图示下行主题为Topic: $oc/devices/{device_id}/sys/events/down其中{device_id}为自己的设备ID,需要修改订阅报文的函数,来订阅时间下行主题,其为事件主题。

        1.1订阅时间下行主题

#define TIME_SUB_TOPIC   "$oc/devices/692ff555bf22cc5a8c0cd677_air_202/sys/events/down"//时间下行主题

//订阅时间下行主题报文
void MQTT_Send_Down_Time(void)
{

    //清空缓冲区
	memset(esp.rxbuff,0,1024);//esp.rxbuff接收数据存放位置,在接收中断里定义,函数为清零
	esp.rxlen = 0;//接收到的数据长度
	esp.rxover = 0;//接收到数据的标志位
	
	//定义对应变量
	unsigned char buf[500];//存放整个订阅报文全部内容包含固定包头、可变包头、有效载荷
	int buflen = sizeof(buf);//计算数组大小

	//定义订阅主题的结构体
	MQTTString topicString = MQTTString_initializer;//定义结构体
	topicString.cstring = TIME_SUB_TOPIC;//赋值下行主题
	int req_qos = 0;//定义服务质量等级

	//形成订阅报文并发送
	int	len = MQTTSerialize_subscribe(buf, buflen, 0, 0x4123, 1, &topicString, &req_qos);
	Usart3_Tx_Buff(buf,len);//发送报文
	
	Delay_nms(1000);//等待连接确认报文接收完毕 0x90(报文类型) 0x03(剩余长度) 0x43(识别符高八位) 0x21(识别符低八位) 0x00(返回码值QOS1、2、3或失败)

    //判断订阅确认报文
	if(esp.rxbuff[0] == 0x90 && esp.rxbuff[1] == 0x03)
	{
		if(esp.rxbuff[2] == 0x41 && esp.rxbuff[3] == 0x23)
		{
			if(esp.rxbuff[4] == 0x00 )
				printf("MQTT下行时间订阅成功\r\n");
		}
			
	}
	//清空缓冲区
	memset(esp.rxbuff,0,1024);//esp.rxbuff接收数据存放位置,在接收中断里定义,函数为清零
	esp.rxlen = 0;//接收到的数据长度
	esp.rxover = 0;//接收到数据的标志位
}

        关于官方形成报文函数解析

int MQTTSerialize_subscribe(
    unsigned char* buf,        // 输出缓冲区
                               //序列化后的MQTT SUBSCRIBE报文将存储在这里

    int buflen,                // 缓冲区长度
                               // 需要足够大以容纳整个SUBSCRIBE报文

    unsigned char dup,         // 重发标志
                               // 0 = 首次发送(不是重发包)
                               // 1 = 重发包(例如,未收到确认时重发)

    unsigned short packetid,   // 数据包ID
                               // 作用:
                               // 1. 唯一标识这个SUBSCRIBE请求
                               // 2. 代理回复SUBACK时会包含相同的packetid
                               // 3. 用于匹配请求和响应

    int count,                 // 主题过滤器数量
                               // 可以一次订阅多个主题:
                               // count = 3 表示订阅3个不同的主题过滤器

    MQTTString* topicFilters,  // 主题过滤器数组
                               //MQTTString 结构体(来自Paho MQTT库)

    int* requestedQoSs         // 请求的QoS数组
                               // 可以是0, 1, 2
                               // 如果订阅多个主题,需要数组:
);

        1.2发布时间上行主题

        由官方示例可知上行主题为: $oc/devices/{device_id}/sys/events/up

#define TIME_PUB_TOPIC   "$oc/devices/692ff555bf22cc5a8c0cd677_air_202/sys/events/up"//发布上行时间主题
#define device_id         "692ff555bf22cc5a8c0cd677_air_202"//设备ID

typedef struct{
	uint64_t device_recv_time;//设备接收时间戳ms级别
	uint64_t device_send_time;//设备发送时间戳ms级别
    uint64_t server_recv_time;//服务器接收时间戳ms级别
    uint64_t server_send_time;//服务器发送时间戳ms级别
}PARAS; 
extern PARAS paras;

//发布上行时间报文
void MQTT_Publish_Up_Time(void)
{
	
    //清空缓冲区
	memset(esp.rxbuff,0,1024);
	esp.rxlen = 0;
	esp.rxover = 0;

	//定义对应变量
	unsigned char buf[500];
	int buflen = sizeof(buf);

	MQTTString topicString = MQTTString_initializer;
	topicString.cstring = TIME_PUB_TOPIC;//发布的报文主题
	uint8_t paybuff[300] = {0};//保存负载 
   
	paras.device_send_time = ((uint64_t)RTC_GetCounter())*1000ULL;//获取发送时的设备RTC时间戳转换为ms级别

    //华为云要求的时间发布格式
	sprintf((char *)paybuff,"{ \
    \"object_device_id\": \"%s\", \
    \"services\": [{ \
        \"service_id\": \"$time_sync\", \
        \"event_type\": \"time_sync_request\", \
        \"event_time\": \"20151212T121212Z\",\
         \"paras\": { \
            \"device_send_time\": %" PRIu64
         "}"
    "}] "
"}",device_id,paras.device_send_time); 
	
    //形成发布报文并发送
	int	len = MQTTSerialize_publish(buf, buflen, 1, 1, 0, 0x1324, topicString, (unsigned char*)paybuff, strlen((char *)paybuff));
	Usart3_Tx_Buff(buf,len);
	
	Delay_nms(1000);//等待连接确认报文接收完毕 0x40 0x02 0x12 0x34
    
	if(esp.rxbuff[0] == 0x40 && esp.rxbuff[1] == 0x02)
	{
		if(esp.rxbuff[2] == 0x13 && esp.rxbuff[3] == 0x24)
		{
			printf("MQTT上行时间发送成功\r\n");
			paras.device_recv_time = ((uint64_t)RTC_GetCounter())*1000ULL;//获取接收时的设备RTC时间戳转换为ms级别
			Cloud_Time();//计算实时时间
		}
	}

	//清空缓冲区
	memset(esp.rxbuff,0,1024);
	esp.rxlen = 0;
	esp.rxover = 0;
}

        1.2.1、paras.device_send_time = ((uint64_t)RTC_GetCounter())*1000ULL;

        之所以RTC_GetCounter()的前面加强制转换uint64_t,是因为RTC_GetCounter()返回值是uint32_t,其乘上1000,是乘法在32位上进行,变成13位的数据,直接超出范围越界失真,得到错误的数据,所以需要提前强制转成uint64_t,来确保数据一定能存下。

         要注意去检测,自己RTC获取的数值是相对变量还是全部值,如果是相对变量,是需要加上偏移量例如//#define RTC_BASE_UNIX_TIME 1700000000  // 需要正确设置,具体偏移量是多少还需要自己去研究。不过大部分都是直接获取的时间戳的值,是不需要叠加偏移量,只是提供一个出问题时排查时的一个方向。

        例如 31536000, 1971 (1年) 946684800, 2000年  1262304000,  2010 1577836800,  2020 1609459200,  2021 1640995200,  2022 1672531200,  2023所以要先运行 RTC_Diagnostic() 了解RTC行为确定RTC的起始时间点,或者是没有偏移直接是全部时间的时间戳。

        1.2.2、添加ULL的作用

         对于13位整数字面量,必须加 ULL(Unsigned Long Long) 后缀!是强制编译器将其解释为64位无符号整数,避免溢出和未定义行为。因为没有后缀,编译器尝试用最合适的类型,例如1234567890123  危险!默认可能是32位有符号long,有后缀,是为了明确指定类型。例如1640995200000UL; UL可能还是32位,因为对于long类型有点编译器认为是4字节有的认为是8字节,不保险。

+--------+---------------------+----------------------+-------------------+
| 后缀   | 含义                | 典型大小             | 适用场景          |
+--------+---------------------+----------------------+-------------------+
| 无     | 编译器决定          | 可能是32或64位       | 小常数(<2³¹)      |
| U      | 无符号              | 同上                 | 无符号小常数      |
| L      | long                | 32或64位(平台相关) | 平台相关的大数    |
| UL     | unsigned long       | 同上                 | 平台相关无符号    |
| LL     | long long           | 通常64位             | 保证64位有符号    |
| ULL    | unsigned long long  | 通常64位             | 保证64位无符号    |

        也可以使用宏定义#include <inttypes.h> ,类型明确的代码 uint64_t timestamp = UINT64_C(1234567890123); int64_t negative_big = INT64_C(-9876543210123); 打印时用正确的格式宏 printf("值: %" PRIu64 "\n", timestamp); printf("值: %" PRId64 "\n", negative_big);

       必须加ULL的情况如 直接赋值uint64_t big1 = 1234567890123ULL; 计算中  uint64_t big2 = 1000000000000ULL * 365ULL;  

        规则小于2³¹(21亿):可以不加后缀2³¹ 到 2³²-1(42亿):应加UUL后缀大于2³²(42亿):必须加ULL后缀。简单原则:对于任何超过10位的十进制整数,总是使用ULL后缀,这样才安全。

        1.2.3、sprintf中paras.device_send_time上传

        使用宏定义打印方法

#include <stdio.h>
#include <stdint.h>
#include <inttypes.h>  // 必须包含这个头文件

int main() {
    uint64_t big_number = 1582685678789ULL;
    
    // 使用PRIu64宏
    printf("正确打印: %" PRIu64 "\n", big_number);
    printf("十六进制: 0x%" PRIx64 "\n", big_number);
    
    printf("%%lld:  %lld\n", big_number); // 类型不匹配,未定义行为
    printf("%%llu: %llu\n", big_number); // 可能正确,但类型不严格匹配
    
    return 0;
}
类型有符号无符号十六进制说明
int%d%u%x基本整型
long%ld%lu%lx平台相关
long long%lld%llu%llxC99标准64位
uint64_t-%"PRIu64"%"PRIx64"可移植
size_t%zd%zu%zx大小类型

        %lld 确实只能安全处理10位十进制数(因为 long long 最大约9.2×10¹⁸,即19位,但某些平台/编译器有差异)最多 19位十进制数,但要注意类型匹配。在某些平台 long long 可能只有32位,所以代码中采用的PRIu64宏格式打印。

        1.3网络时间协议(NTP)

        网络时间协议(NTP)的核心算法,用于在设备与服务器时间同步时消除网络传输延迟的影响。

公式:(server_recv_time + server_send_time + device_recv_time - device_send_time) / 2;该公式消除了双向网络延迟的影响结果更精确,比简单取平均值更好,其广泛应用于NTP、PTP等时间同步协议,以获得更精确的时间同步效果。

//时间更新函数
void Time_Updated(void)
{
	uint32_t timestamp = 0;
    //消除网络延迟公式
	timestamp = (uint32_t)((paras.server_recv_time + paras.server_send_time+ paras.device_recv_time - paras.device_send_time) / 2/1000);
    //RTC时间戳写入函数
	Rtc_UpData(timestamp);
}

        由于华为云使用的都是ms级(13位)时间戳,而RTC使用的s级(10位)的时间戳,所以需要进行转换,s->ms,直接拿秒级时间戳乘上1000,ms->s,直接拿毫秒级时间戳除上1000即可;公式计算完之后除于1000,再强转为32位的可以避免报数据类型不匹配的警告。

//更新时间
void Rtc_UpData(uint32_t sec)
{
	//1.使能后背区域访问
			/* Enable PWR and BKP clocks */
			RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE);
			/* Allow access to BKP Domain */
			PWR_BackupAccessCmd(ENABLE);
			RTC_SetCounter(sec);
			/* Wait until last write operation on RTC registers has finished */
			RTC_WaitForLastTask();
}

        1.4从接收到的下放时间数据中提取时间

#include <string.h>

//获取云平台时间
void Cloud_Time(void)
{
	char *search = NULL;
	char buff[14] = {0};
	if(esp.rxover == 1)
	{
		search = strstr((char*)&esp.rxbuff[10],(char*)"device_send_time");
		printf("%s\r\n",esp.rxbuff);
		if(search!=NULL)
		{
			search = search+strlen("device_send_time\":");
			for(uint8_t i = 0; i < 13; i++,search++)
			{
				buff[i] = *search;
			}
			paras.device_send_time = String_To_Int(buff);
		}
		search = strstr((char*)&esp.rxbuff[10],(char*)"server_recv_time");
		if(search!=NULL)
		{
			search = search+strlen("server_recv_time\":");
			for(uint8_t i = 0; i < 13; i++,search++)
			{
				buff[i] = *search;
			}
			paras.server_recv_time = String_To_Int(buff);
		}
		search = strstr((char*)&esp.rxbuff[10],(char*)"server_send_time");
		if(search!=NULL)
		{
			search = search+strlen("server_send_time\":");
			for(uint8_t i = 0; i < 13; i++,search++)
			{
				buff[i] = *search;
			}
			paras.server_send_time = String_To_Int(buff);
		}
	}
}

        strstr 是C标准库中的字符串查找函数,用于在主字符串中查找子字符串的首次出现位置。假如haystack:主字符串(要搜索的字符串)needle:子字符串(要查找的字符串)返回值:找到:返回子字符串首次出现位置的指针;未找到:返回 NULL;如果 needle 是空字符串:返回 haystack

        strstr的停止标志,主字符串结束符 '\0,找到完整匹配,子字符串为空时停止,又因为在接收数据时,华为云会回传确认报文,其确认报文是十六进制的形式展现的,所以在显示接收内容字符串的之前会有一串乱码,就是因为ASKII码表中没有对应的字符,而根据上面的确认报文判断中,发现很容易出现0x00,而0x00ASKII码对应的是'\0,所以会导致strstr函数异常停止,所以我们选择(char*)&esp.rxbuff[10],接收数据偏移10位,来错开0x00,等不是字符串的确认报文。之所以(char*)强转一下,是因为stm32中我们常用uint8_t来定义,尽管uint8_t是char类型,类型重定义得来,尽管勾选c语言微库和加了相应c语言头文件,它还是报类型不匹配的警告,就进行强转来解决。

      可以通过添加  #include <stdint.h>头文件,就可以包含32和c语言的各种数据类型,就可以省去添加STM32的头文件。其他的头文件如下。

// 如果只需要精确宽度整数:
//C99标准精确宽度整数类型,确保在不同平台上具有一致的大小和行为。
#include <stdint.h>

// 如果需要打印这些整数:
// 包含<stdint.h> + 格式化宏
#include <inttypes.h>  

// 如果做POSIX系统编程(文件/进程/网络):
//传统UNIX/POSIX系统类型
#include <sys/types.h>

// 如果需要布尔类型:
#include <stdbool.h>

// 如果只需要size_t/NULL:
//标准定义(size_t, NULL等)
#include <stddef.h>

        1.5、数字字符串转十进制数字

        因为华为云发来的时间戳是字符串,所以我们需要转成十进制取用。需要注意因为我们时间戳是13位的,uint32_t 最大值: 4294967295,十进制: 4294967295,十六进制: 0xFFFFFFFF,最多存储: 10 位十进制数,所以我们需要用uint64_t(long long)类型来接收收处理,否则将发生数据回环产生负值,从而导致功能无法实现。

数据类型位数无符号范围有符号范围最大十进制位数
uint32_t32位0 ~ 4,294,967,295不支持10位
int32_t32位不支持-2,147,483,648 ~ 2,147,483,64710位
uint64_t64位0 ~ 18,446,744,073,709,551,615不支持20位
int64_t64位不支持-9.22×10¹⁸ ~ 9.22×10¹⁸19位

        所以你如果用uint32_t来承接数据你将会得到1970年2月份的值,排时i将会很难受。例如

数学角度上数据回环:

原始值: 1234567890123

uint32_t 最大值: 4294967295

2³² = 4294967296

计算过程:

取模基数: 2³² = 4294967296

取模结果: 1234567890123 % 4294967296 = 1912276171

倍数: 1234567890123 / 4294967296 = 287 次

余数: 1234567890123 - (287 × 4294967296) = 1912276171

验证: 1234567890123 = 287 × 4294967296 + 1912276171

C语言转换: (uint32_t)1234567890123 = 1912276171

当二进制最高位为1时也可能解析出负数。

二进制角度上高32位截断只取低32位

转换为32位时:

丢弃高32位: 00010001 10100011 01010111 11000101

保留低32位: 01110100 00001011 

低32位的值 = 1912276171

所以计算机中的"数据回环"实际上就是模运算在二进制层面的体现。

// 字符串转十进制
uint64_t String_To_Int(const char *str) {
		uint64_t  result = 0;
    int i = 0;
    // 转换数字
    while (str[i] >= '0' && str[i] <= '9') {
        result = result * 10 + (str[i] - '0');
        i++;
    }
    return result;
}

        1.6、RTC时间打印

/获取实时时间

void RTC_GetTime(void)
{
	uint32_t sec = RTC_GetCounter();//获取当前秒数值
	info = localtime(&sec);
	if((info->tm_hour+8) > 24)
	{
		info->tm_hour = info->tm_hour+8-24;
		info->tm_mday = info->tm_mday+1;
	}
	printf("%d年-%d月-%d日",(info->tm_year+1900), (info->tm_mon+1),info->tm_mday);
	printf("%d时-%d分-%d秒\r\n",(info->tm_hour+8), (info->tm_min),info->tm_sec);
	//printf("当前的本地时间和日期:%s", asctime(info));
}
函数输入输出用途时区处理
gmtimetime_t 秒数struct tm (UTC时间)将Unix时间戳转换为UTC时间无时区转换,总是UTC
mktimestruct tm (本地时间)time_t 秒数将本地时间转换为Unix时间戳考虑时区,将本地时间转为UTC时间戳
localtimetime_t 秒数struct tm (本地时间)将Unix时间戳转换为本地时间应用系统时区

        本来是打算使用RTC时间日期溢出越界自动处理的如下

// 获取实时时间(自动处理溢出)
void RTC_GetTime(void)
{
    // 获取RTC时间
    uint32_t rtc_sec = RTC_GetCounter();
    time_t unix_time = (time_t)rtc_sec;
    
    // 转换为UTC时间
    struct tm *utc_time = gmtime(&unix_time);
    if (utc_time == NULL) {
        printf("错误:gmtime失败\n");
        return;
    }
    
    // 手动调整为北京时间(UTC+8)
    // 先复制结构体,避免修改全局变量
    struct tm beijing_time = *utc_time;
    beijing_time.tm_hour += 8;
    
    // 使用mktime自动处理溢出(跨日、跨月、跨年)
    // mktime会自动标准化时间结构
    time_t adjusted = mktime(&beijing_time);
    
    // 再次转换为tm结构
    struct tm *final_time = localtime(&adjusted);
    
    printf("%04d年-%02d月-%02d日 %02d时-%02d分-%02d秒\r\n",
           final_time->tm_year + 1900,
           final_time->tm_mon + 1,
           final_time->tm_mday,
           final_time->tm_hour,
           final_time->tm_min,
           final_time->tm_sec);
}

        最终无法解决gmtime(&unix_time)返回值一直为NULL,而放弃。无论怎么确保unix_time有数据且正确gmtime就是返回NULL。要是有大佬能解决还望评论区告知,谢谢。

 四、通过AP模式更改WiFi

       4.1、 AP 模式的AT指令顺序

AT+RST                                                                  //复位

AT                                                                           //测试AT      

AT+CWMODE=2                                                    //设置模块为AP模式

AT+CWSAP ="TCP_Server","12345678",5,3         //设置 ESP8266 散发的热点信息

AT+CIFSR                                                              //查询本机 IP 地址

AT+CIPMUX=1                                                      //开启多链接

AT+CIPSERVER=1                                                //开启服务器

等待接收消息

     其中 "TCP_Server" - 热点名称(SSID)"12345678" - 热点密码(8-64字符)5 - 信道(Channel),常用1-133 - 加密方式(关键参数)设置 ESP8266 散发的热点信息中5,3的意思分别是最大链接数和安全性为WPA2。

ecn 参数详解(加密方式)

加密方式说明
0OPEN开放,无加密
1WEPWEP加密(已过时,不安全)
2WPA_PSKWPA-PSK加密
3WPA2_PSKWPA2-PSK加密(当前最安全通用)
4WPA_WPA2_PSKWPA/WPA2混合模式

        尽量选择3,因为4会受各种影响如固件Bug或限制导致无法连接,选择WPA2_PSK最安全更可靠。

        4.2、STA模式连接客户端

uint8_t  Esp_Cloud(void)
{
	uint8_t  data = 0;
	uint8_t count = 0;
	uint16_t time = 14;
	while(time--)
	{
			switch(count)
			{
				case 0:
					data = SendCmdAndJudgeAck((uint8_t *)"AT\r\n",(uint8_t *)"OK",1000);
					if(data == 1)
						count++;
					break;
				case 1:
					Usart3_Tx_Str((uint8_t *)"AT+RST\r\n");
					count++;
					Delay_nms(1000);
					break;
				case 2:
					data = SendCmdAndJudgeAck((uint8_t *)"AT+CWMODE=1\r\n",(uint8_t *)"OK",1000);
					if(data == 1)
						count++;
					break;
				case 3:
				{
					uint8_t arr[100] = {0};
					sFLASH_ReadBuffer((uint8_t *)&wifi,WIFIADDR,sizeof(wifi));//读取账号密码
					printf("name:%s\r\n",wifi.name);
					printf("word:%s\r\n",wifi.word);
					sprintf((char *)arr,"AT+CWJAP=\"%s\",\"%s\"\r\n",(char *)wifi.name,(char *)wifi.word);
					data = SendCmdAndJudgeAck(arr,(uint8_t *)"OK",10000);
					if(data == 1)
						count++;
					break;
				}
				case 4:
					data = SendCmdAndJudgeAck((uint8_t *)"AT+CIPMUX=0\r\n",(uint8_t *)"OK",1000);
					if(data == 1)
						count++;
					break;
				case 5:
				{
					uint8_t arr[100] = {0};
					sprintf((char *)arr,"AT+CIPSTART=\"TCP\",\"%s\",%d\r\n",(char *)IP,PORT);
					data = SendCmdAndJudgeAck(arr,(uint8_t *)"OK",1000);
					if(data == 1)
						count++;
					break;
				}
				case 6:
					data = SendCmdAndJudgeAck((uint8_t *)"AT+CIPMODE=1\r\n",(uint8_t *)"OK",1000);
					if(data == 1)
						count++;
					break;
				case 7:
					data = SendCmdAndJudgeAck((uint8_t *)"AT+CIPSEND\r\n",(uint8_t *)">",1000);
					//清空缓冲区
					memset(esp.rxbuff,0,1024);
					esp.rxlen = 0;
					esp.rxover = 0;
					if(data == 1)
						count++;
					break;
                //链接报文
				case 8:
					MQTT_Connect();
					count++;
					break;
				case 9:
                //订阅报文
					MQTT_Subscribe();
					count++;
					break;
				case 10:
                //订阅下行报文
					MQTT_Send_Down_Time();
					count++;
					break;
				case 11:
                //订阅下行时间报文
					MQTT_Publish_Up_Time();
					RTC_Init((uint32_t)(paras.server_send_time/1000));//RTC初始化
					//printf ("%llu\r\n",paras.server_send_time/1000);
					Delay_nms(1);
					RTC_GetTime();//显示RTC时间
					//printf ("%llu\r\n",((uint64_t)RTC_GetCounter()));
					printf("设备链接完成\r\n");
					return 1;
				
			}
		}
	printf("设备链接失败\r\n");
	return 0;
}

        RTC初始化之后Delay_nms(1);延时1ms的目的是因为,如果不延时直接获取RTC时间戳,其值不准确,怀疑可能计数器还没存到输入的时间戳对应的值,就读取导致数据出错。所以延时1ms等数据完全处理完之后再读取。

        不加延时时               加延时时 

        1765186207               1765186293   
        1997年-12月-20日12时-44分-39秒     2025年-12月-8日17时-31分-19秒
        882593079               1765186279

//发送AT指令并判断返回值
uint8_t  SendCmdAndJudgeAck(uint8_t *cmd,uint8_t *ack,uint16_t outtime)
{
	memset(esp.rxbuff,0,1024);
	esp.rxlen = 0;
	esp.rxover = 0;
	
	Usart3_Tx_Str(cmd);
	while(outtime--)
	{
		Delay_nms(1);
		if(esp.rxover == 1)
		{
			if(strstr((char*)esp.rxbuff,"OK") != NULL)
				return 1;
			else 
				esp.rxover = 0;
		}
	}
	return 0;
}

        发送AT指令并判断回传值是否为OK。

         4.3、AP模式设置WiFi账号密码

typedef struct{
	uint8_t word[20];
	uint8_t name[20];
}WIFI;
extern WIFI wifi;

#define WIFIADDR 0x001000//W25Q64存储地址

uint8_t  ESP_AP(void)
{
	uint8_t apnum = 0;
	uint8_t data = 0;
	uint16_t time = 25;
	while(time--)
	{
		switch(apnum)
		{
			case 0:
						Usart3_Tx_Str((uint8_t *)"AT+RST\r\n");
						apnum++;
						Delay_nms(1000);
			break;
			case 1:
				data = SendCmdAndJudgeAck((uint8_t *)"AT\r\n",(uint8_t *)"OK",1000);
						if(data == 1)
							apnum++;
			break;
			case 2:
				data = SendCmdAndJudgeAck((uint8_t *)"AT+CWMODE=2\r\n",(uint8_t *)"OK",1000);
						if(data == 1)
							apnum++;
						break;
			case 3:
				data = SendCmdAndJudgeAck((uint8_t *)"AT+CWSAP=\"ESP8266ZH\",\"12345678\",5,3\r\n",(uint8_t *)"OK",10000);
						if(data == 1)
							apnum++;
			break;
			case 4:
				data = SendCmdAndJudgeAck((uint8_t *)"AT+CIFSR\r\n",(uint8_t *)"OK",1000);
						if(data == 1)
							apnum++;
			break;
			case 5:
				data = SendCmdAndJudgeAck((uint8_t *)"AT+CIPMUX=1\r\n",(uint8_t *)"OK",1000);
						if(data == 1)
							apnum++;
			break;
			case 6:
				data = SendCmdAndJudgeAck((uint8_t *)"AT+CIPSERVER=1\r\n",(uint8_t *)"OK",1000);
				//清空缓冲区
				memset(esp.rxbuff,0,1024);
				esp.rxlen = 0;
				esp.rxover = 0;
						if(data == 1)
							apnum++;
                printf ("请用户在三分钟内发送要设置WiFi的账号密码\r\n");
			break;
			//等待用户发送过来信息、解析
			//+IPD,1,77:{"wifiname":"jxhh","password":"gheusu","mycity":"","alarm_h":"","alarm_m":""}
			case 7:
			{
				uint16_t num = 10000;
				while(num--)
				{
					Delay_nms(1);
					if(esp.rxover == 1)
					{
						uint8_t wifi_flag = 0;
						char *name = NULL;
						char *word = NULL;
						name = strstr((char*)&esp.rxbuff[10],(char*)"wifiname");
						if(name != NULL)
						{
							name += 11;//指针偏移到所需数据开通
							uint8_t i = 0;
							while(*name != '"')
							{
								wifi.name[i] = *name;
								i++,name++;
							}
							wifi_flag++;
						}
						word = strstr((char*)&esp.rxbuff[10],(char*)"password");
						if(name != NULL)
						{
							word += 11;//指针偏移到所需数据开通
							uint8_t j = 0;
							while(*word != '"')
							{
								wifi.word[j] = *word;
								j++,word++;
							}
							wifi_flag++;
						}
						if(wifi_flag == 2)
						{
							printf("name:%s\r\n",wifi.name);
							printf("word:%s\r\n",wifi.word);
							sFLASH_EraseSector(WIFIADDR);//扇区擦除
							sFLASH_WriteBuffer((uint8_t *)&wifi,WIFIADDR,sizeof(wifi));//写入账号密码
							printf("AP链接完成\r\n");
							return 1;
						}
						//清空缓冲区
						memset(esp.rxbuff,0,1024);
						esp.rxlen = 0;
						esp.rxover = 0;
						
					}
				}
			}
		}
	}
	printf("AP链接超时\r\n");
	return 0;
}

        给用户三分钟的时间,链接并设置WiFi,超时自动失败,先链接AP服务器散布的热点,然后进行网络配置,输入ESP8266的默认IP:192.168.4.1 端口: 333。然后再在对应位置输入WiFi账号和密码,点击发送就会以这个格式发送+IPD,1,77:{"wifiname":"jxhh","password":"gheusu","mycity":"","alarm_h":"","alarm_m":""},ESP8266就会收到,在case7中处理,并存入W25Q64中,在每次进入STA模式中从W25Q64中读取账号密码。

        4.4、按键切换模式

#include "main.h"
LCDSHOW LCD_Show = {0};

uint8_t esp_cloud_flag = 0;
uint8_t data[8] = {0};
int main(void) 
{ 
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
	Relay_Init();
	Led_Init();
	Key_Init();
	Beep_Init();
	Time_Init();
	SysTick_Config(72000);
	//PWM_Init();
	Uart1_Init();
	Uart2_Init();
//LCD_Init();
	Su03t_Uart4_Init();
	Dht11_Init();
	ADC1_Init();
	sFLASH_Init();
	//BH1750_Init();
  BH1750_IIC_Init();
  ESP_Uart3_Init();
	
	//esp_cloud_flag = Esp_Cloud();
	//esp_ap_flag = ESP_AP();
	uint16_t key_num = 0;
	Delay_nms(600);
	printf("\r\n------------------------------------------------\r\n");
	printf("请在5s内选择要进入的模式,按键1设置模式,按键2运行模式\r\n");
	printf("------------------------------------------------\r\n");
	
	
	for(uint16_t esp_num = 5000; esp_num>0; esp_num--)
	{
		Delay_nms(1);
		key_num = Read_Key();
		if(key_num != 0)
		{
			if(key_num == 1)
			{
				ESP_AP();
				esp_cloud_flag = 0;
				break ;
			}else if(key_num == 2)
			{
				esp_cloud_flag = Esp_Cloud();
				break;
			}
		}
	}
	
	if(esp_cloud_flag == 0)
	{
		esp_cloud_flag = Esp_Cloud();
	}
	
	while(1) 
	{
		key_num = Read_Key();
		if(key_num == 1)
		{
			esp_cloud_flag = 0;
			Switchover();
			ESP_AP();
		}
		if(key_num == 2)
		{
			esp_cloud_flag = Esp_Cloud();
		}
		
		
		if( esp_cloud_flag == 1)
		{
				if(MQTT_Pub_time > 100000)
			{
				MQTT_Pub_time = 0;
				MQTT_Publish();
			}
			if(MQTT_YunAnaly_time > 300)
			{
				MQTT_YunAnaly_time = 0;
				YunCmdAnalysis();
			}
			if(MQTT_REQ_time > 42000)
			{
				MQTT_REQ_time = 0;
				MQTT_REQ();
				MQTT_Publish_Up_Time();
				Time_Updated();
				RTC_GetTime();
			}
		}
		
		

		
	}
}


        刚开始会进入模式选择,按键1为AP模式,按键2开启STA模式,运行状态下按下按键1,切换为AP模式设置新的账号密码,按下按键2,切换回STA模式链接客户端服务器获取实时时间。

void Switchover(void)
{
	uint8_t apnum = 0;
	uint8_t data = 0;
	uint16_t time = 4;
	while(time--)
	{
		switch(apnum)
		{
			case 0:
				Usart3_Tx_Str((uint8_t *)"+++");
			Delay_nms(1000);
				apnum++;
			break;
			case 1:
					data = SendCmdAndJudgeAck((uint8_t *)"AT+CIPCLOSE\r\n",(uint8_t *)"OK",1000);
						if(data == 1)
							return ;
			break;
		}
	}
}

        此函数为STA到AP模式之间的过渡处理,因为STA模式最终会一直保持透传模式,一切指令都会当作数据传输,所以需要输入”+++“退出透传模式,恢复命令模式,且需要注意的时输入“+++”并且前后需要至少有1秒的静默时间(不加任何数据),模块就会退出透传,返回命令模式。再发送"AT+CIPCLOSE\r\n"断开服务器,就可以执行AP指令代码了。

void USART3_IRQHandler(void)
{
	uint8_t data = 0;
	if(USART_GetITStatus(USART3,USART_IT_RXNE) != RESET)
	{
		USART_ClearITPendingBit(USART3,USART_IT_RXNE);
		data = USART3->DR; //接收ESP的数据
		USART1 ->DR = data;//u1发送data的数据
		esp.rxbuff[esp.rxlen] = data;
		esp.rxlen++;esp.rxlen %= 1024;
	}
	if(USART_GetITStatus(USART3,USART_IT_IDLE) != RESET)
	{
		data = USART3->SR;
		data = USART3->DR;
		esp.rxover = 1;
//		su03t.flag = 1;
	}
}
void USART1_IRQHandler(void)
{
	uint8_t data = 0;
	if(USART_GetITStatus(USART1,USART_IT_RXNE) != RESET)
	{
		USART_ClearITPendingBit(USART1,USART_IT_RXNE);
		data = USART1->DR; //接收PC的数据
		USART3 ->DR = data;//u3发送data的数据
	}
}

//printf重定向
#include "stdio.h"
int fputc(int a,FILE *p)
{
    Usart1_Tx(a);
    return a;
}

        且需要注意为了确保串口1和串口3数据转发准确,且需要保持相同的波特率,因为ESP8266的速度是115200,所以串口一也要保持相同的速度。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值