系列文章目录
第一篇:LoRa通信基础
第二篇:协议层设计与CRC校验
第三篇:可靠传输机制(ACK/超时/重传)
第四篇:W25Q128固件管理
第五篇:完整的固件发送
一、为什么需要可靠传输?
LoRa 是无线通信,容易受到干扰、信号衰减等影响,导致数据丢失。为了确保固件升级的可靠性,需要实现:
ACK 确认:发送方收到接收方的确认,才知道数据已到达
超时重传:发送方等待 ACK 超时后,重新发送数据
SEQ 序号:每个数据包有唯一序号,用于识别和去重
二、SEQ 序号机制
2.1 序号的作用
识别数据包:每个数据包有唯一序号
确认接收:ACK 携带序号,告诉发送方哪个包被接收
三、优化函数
3.1 优化命令与通信协议构成
在命令上,我们添加了发送ACK的宏定义,在发送的通信协议上,我们添加了数据包序列号
3.2优化发送函数
四、ACK 确认机制
4.1 ACK 帧结构
此处开始编写应用层函数,添加High_App_OTA.c 与 High_App_OTA.h文件
在High_App_OTA.h中添加结构体
//ACK结构体 typedef struct { uint8_t node_id; //应答的ACK节点ID uint16_t ack_seq; //数据包 uint8_t ack_flag; //应答标志位 }ACK_Frame_t;
4.2 发送 ACK
在High_App_OTA.c文件中创建发送ACK函数,在此函数中调用llcc68的发送函数,把ACK有关的参数发送出去
/** * 发送ACK * id * seq * 0成功 | 1失败 */ uint8_t Int_llcc68_lora_send_ack(uint8_t id, uint16_t seq) { ACK_Frame_t g_ack_frame; g_ack_frame.node_id = id; g_ack_frame.ack_seq = seq; g_ack_frame.ack_flag = 1; uint8_t res = Int_llcc68_lora_send(HEAD, id, CMD_SEND_ACK, seq, (uint8_t *)&g_ack_frame, sizeof(ACK_Frame_t)); if(res != 0) { return 1; } else return 0; }在接收到节点数据时,如果校验全部通过,则发送ACK给节点,表示收到消息且消息无误
/** * 处理节点上报的数据 */ void Int_Process_Node_Data(uint8_t node_id,uint16_t seq, uint8_t *data, uint16_t len) { switch (node_id) { case NODE_ID_01: //i to do: Int_llcc68_lora_send_ack(NODE_ID_01,seq); break; default: break; }
4.3 接收与处理ACK
4.3.1 优化接收解析函数
Frame_t g_ota_frame; 声明全局变量
把这个接收解析函数移到应用层函数代码文件去,并在原有的接收函数前,添加case语句,如果发送的命令是发送ACK的话则进入ACK应答处理函数
4.3.2 处理ACK应答函数
参数是ACK_Frame_t 的函数指针,在接收到数据后,进入case语句,如果命令是发送ACK,则进入此函数,记录ACK应答的ID、数据包序列号,并把标志位置1,后续有对ID进行细分操作的函数。
ACK_Frame_t ack_frame; //声明全局变量 /** * AKC应答处理函数 * frame指针 */ static uint8_t High_OTA_Rece_ACK_Handler(Frame_t *frame) { ack_frame.node_id = frame->id; //获取ACK应答的ID ack_frame.ack_seq = frame->seq; //获取当前信息的seq值 ack_frame.ack_flag = 1; //标志位置1,表示应答 return 0; }五、超时重传机制
3.1 等待 ACK(带超时)
在发送消息后,我们都需要等待ACK,以确保我们的消息确认无误发送给了对方
在规定时间上,要是发完消息没收到该节点发送的ACK则超时,进行有限次数的重传
#define OTA_ACK_TIMEOUT 3000 // 超时时间 3 秒 /** * @brief 等待ACK应答 * @param id 期待ACK应答的节点ID * @param seq 期望的序列号 * @return 0=成功, 1=超时 */ uint8_t High_App_OTA_Wait_Ack(uint8_t node_id,uint16_t seq) { uint32_t start_tick = 0; start_tick = HAL_GetTick(); //获取当前系统的时间 while (1) { if (ack_frame.ack_flag == 1 && ack_frame.node_id == node_id) // 是否应答 与 ID是否匹配 { if (ack_frame.ack_seq == seq) // 判断应答序列号是否对的上 { ack_frame.ack_flag = 0; // 清零标志位 ack_frame.node_id = 0; // 清空应答ID return 0; } else { printf("seq error\r\n"); ack_frame.ack_flag = 0; // 清零标志位 ack_frame.node_id = 0; // 清空应答ID return 1; } } if (HAL_GetTick() - start_tick > OTA_ACK_TIMEOUT) // 判断是否超时 { printf("ack timeout\r\n"); return 1; } } }3.2 带重传的发送
#define OTA_MAX_RETRY 3 // 最大重试次数 /** * @brief 发送数据并等待ACK(带重传) * @param node_id 目标节点ID * @param seq 序列号 * @param data 数据 * @param len 数据长度 * @return 0=成功, 1=失败 */ uint8_t High_OTA_Send_With_Retry(uint8_t node_id, uint16_t seq,uint8_t *data, uint16_t len) { uint8_t retry = 0; while (retry < OTA_MAX_RETPY) { // 1. 发送数据 Int_llcc68_lora_send(HEAD, node_id, CMD_SEND_DATA,seq, data, len); printf("Send seq=%d, retry=%d/%d\r\n", seq, retry + 1, OTA_MAX_RETRY); // 2. 等待ACK if (High_App_OTA_Wait_Ack(node_id,seq) == 0) { printf("ACK received! seq=%d\r\n", seq); return 0; // 成功 } // 3. 超时,重试 retry++; printf("Timeout, retry %d/%d\r\n", retry, OTA_MAX_RETRY); HAL_Delay(100); } printf("retpy max !\r\n"); return 1; // 失败 }六、测试
main中导入头文件
6.1 测试网关发送数据给节点并等待ACK应答
F429网关代码:初始化llcc68,并调用发送数据并等待ACK,在主循环中调用接收函数
F103节点代码:F103负责接收网关发送的数据,在接收到数据且解析无误时,发送ACK给网关。
6.1.1 结果
节点端:F103节点端接收到网关发送的消息,且接收无误,发送ACK给网关
网关端:发送id为1,seq为1,成功发送且收到ACK回应
6.2 节点发送数据给网关 与 超时功能测试
F103节点端:进行初始化,并给网关发送消息1,2,3,4,5 seq为1
F429网关端:在main函数中进行初始化且在循环中调用接收函数并且为了测试超时函数是否能准确运行,把接收到信息且无误发送ACK给注释了,也就是不发送ACK给节点
6.2.1 结果
节点端:发送消息给网关 id为1,seq为1,尝试发送了三次,最后超时退出
七、总结
本篇实现了完整的可靠传输机制:
| 功能 | 状态 |
|---|---|
| ACK确认 | ✅ 已实现 |
| 超时重传 | ✅ 已实现 |
| SEQ序号 | ✅ 已实现 |
下一篇预告:实现 W25Q128 固件管理,包括元数据存储、串口接收、PC通信协议。