一、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=0或SP=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亿):应加U或UL后缀大于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 | %llx | C99标准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_t | 32位 | 0 ~ 4,294,967,295 | 不支持 | 10位 |
| int32_t | 32位 | 不支持 | -2,147,483,648 ~ 2,147,483,647 | 10位 |
| uint64_t | 64位 | 0 ~ 18,446,744,073,709,551,615 | 不支持 | 20位 |
| int64_t | 64位 | 不支持 | -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));
}
| 函数 | 输入 | 输出 | 用途 | 时区处理 |
|---|---|---|---|---|
| gmtime | time_t 秒数 | struct tm (UTC时间) | 将Unix时间戳转换为UTC时间 | 无时区转换,总是UTC |
| mktime | struct tm (本地时间) | time_t 秒数 | 将本地时间转换为Unix时间戳 | 考虑时区,将本地时间转为UTC时间戳 |
| localtime | time_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 参数详解(加密方式)
| 值 | 加密方式 | 说明 |
|---|---|---|
| 0 | OPEN | 开放,无加密 |
| 1 | WEP | WEP加密(已过时,不安全) |
| 2 | WPA_PSK | WPA-PSK加密 |
| 3 | WPA2_PSK | WPA2-PSK加密(当前最安全通用) |
| 4 | WPA_WPA2_PSK | WPA/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,所以串口一也要保持相同的速度。

1892

被折叠的 条评论
为什么被折叠?



