4、固件分包策略:LoRa传输速率限制下的固件分包策略
好,咱们接着聊固件分包。说实话,这是整个OTA方案里最让我头疼过的一个环节。你想想看,LoRa的传输速率就那么点——典型的SF12下,每秒也就传几十个字节。你要把几十KB甚至上百KB的固件传过去,不分包?那是不可能的。
但分包不是简单地把文件切碎就完事了。包大小怎么定?包序号怎么编?总包数怎么算?这里头门道不少。我踩过的坑,今天一次性说清楚。
4.1 包大小:不是越大越好,也不是越小越好
包大小,说白了就是每个数据包能装多少有效载荷。LoRaWAN标准里,单个数据包的最大载荷是51字节(SF7)到242字节(SF12)。但实际项目中,我建议你留点余量。
我个人习惯的做法:
- SF7-SF9:包大小取32字节。为什么?因为加上协议头、CRC校验、序号这些开销,刚好能塞进一个LoRa包。我在一个智能水表项目里试过64字节,结果经常丢包重传,效率反而更低。
- SF10-SF12:包大小取64字节。这时候速率低,但单包容量大。你想想看,SF12下每秒才传几十字节,如果包太小,握手开销占比太高,不划算。
核心原则:包大小 = (LoRa单包最大载荷 - 协议开销) × 0.8。留20%的余量给重传、ACK、时间戳等。
我记得有一次,一个同事把包大小设成了240字节(SF12下),结果节点端缓冲区溢出,固件写到一半就挂了。嗯,这里要注意:节点端的RAM通常很小,包太大容易撑爆。
4.2 包序号:从0开始,还是从1开始?
这个问题看似简单,但真有人搞错过。包序号的作用是让接收端知道:我收到的是第几个包,有没有漏掉。
我建议的序号方案:
- 使用16位无符号整数:范围0-65535。对于大多数固件(通常不超过512KB,每包64字节,最多8192包),完全够用。
- 从0开始编号:第0包是固件头,包含固件版本、CRC、总包数等元信息。第1包开始才是真正的固件数据。
- 序号连续递增:不要跳号。我曾经见过一个方案,序号用时间戳的低16位,结果时间回绕导致序号冲突,节点端固件写花了。
小技巧:包序号可以同时作为写入Flash的地址偏移量。比如每包64字节,第N包就写入Flash基地址 + N × 64。这样接收端不用额外计算,直接写就行。
4.3 总包数:提前算好,还是动态计算?
总包数 = ceil(固件总大小 / 每包有效载荷)。这个公式很简单,但有个坑:固件总大小是编译后才知道的。你不可能在代码里写死。
我的做法:
- 编译时生成:在编译脚本里,用
stat或ls -l获取固件文件大小,然后除以包大小,得到总包数。把这个值写入固件头。 - 服务器端计算:服务器在开始OTA前,先读取固件文件,计算总包数,然后通过下行命令告诉节点。
- 节点端校验:节点收到第0包(固件头)后,解析出总包数,和自己算的对比。不一致就拒绝升级。
注意:总包数不要用浮点数。我见过有人用ceil(size / packet_size),结果浮点精度问题导致总包数少1。用整数运算:total_packets = (size + packet_size - 1) / packet_size。
4.4 分包策略的完整流程
说了这么多,咱们用一张图把整个分包策略串起来。这是我画的一个流程图,展示了从固件文件到LoRa数据包的完整过程。
4.5 实际项目中的分包代码示例
光说不练假把式。下面是我在一个LoRa温湿度传感器OTA项目中实际用过的分包代码。注意看注释,都是血泪教训。
// 分包函数:将固件数据分成多个LoRa数据包
// 参数:firmware_data - 固件数据指针
// firmware_size - 固件总大小(字节)
// packet_size - 每个数据包的有效载荷大小(字节)
// 返回:总包数
uint16_t split_firmware(uint8_t *firmware_data, uint32_t firmware_size, uint16_t packet_size) {
uint16_t total_packets;
uint16_t i;
// 计算总包数,使用整数运算避免浮点精度问题
total_packets = (firmware_size + packet_size - 1) / packet_size;
// 第0包:固件头,包含元信息
// 这里我吃过亏:一开始没放CRC,结果节点端收到错误固件也不知道
packet_t header_packet;
header_packet.seq = 0; // 包序号0
header_packet.data[0] = (firmware_size >> 24) & 0xFF; // 固件大小高字节
header_packet.data[1] = (firmware_size >> 16) & 0xFF;
header_packet.data[2] = (firmware_size >> 8) & 0xFF;
header_packet.data[3] = firmware_size & 0xFF; // 固件大小低字节
header_packet.data[4] = (total_packets >> 8) & 0xFF; // 总包数高字节
header_packet.data[5] = total_packets & 0xFF; // 总包数低字节
header_packet.data[6] = firmware_crc32 >> 24; // 固件CRC
header_packet.data[7] = firmware_crc32 >> 16;
header_packet.data[8] = firmware_crc32 >> 8;
header_packet.data[9] = firmware_crc32 & 0xFF;
header_packet.crc = calc_crc16(&header_packet, sizeof(packet_t) - 2);
// 发送第0包
lora_send(&header_packet, sizeof(packet_t));
// 第1包开始:真正的固件数据
for (i = 1; i < total_packets; i++) {
packet_t data_packet;
uint32_t offset = (i - 1) * packet_size; // 数据偏移量
uint16_t copy_size;
// 最后一段可能不足packet_size
if (offset + packet_size > firmware_size) {
copy_size = firmware_size - offset;
} else {
copy_size = packet_size;
}
data_packet.seq = i; // 包序号从1开始
memcpy(data_packet.data, firmware_data + offset, copy_size);
data_packet.crc = calc_crc16(&data_packet, sizeof(packet_t) - 2);
// 发送数据包
lora_send(&data_packet, sizeof(packet_t));
// 等待ACK,超时重传
// 嗯,这里要注意:不要发太快,LoRa是半双工的
wait_for_ack(i, 3); // 最多重试3次
}
return total_packets;
}
4.6 避坑指南:我踩过的三个坑
做OTA这么多年,分包这块我栽过不少跟头。挑三个最典型的说说:
- 坑一:包大小没留余量。 我曾经把包大小设成LoRa单包最大载荷,结果加上协议头后超了,节点端直接丢弃。后来我学乖了,包大小 = 最大载荷 - 4字节(协议头开销)。
- 坑二:序号从1开始,但忘了第0包。 有一次我写代码,序号从1开始,结果接收端不知道总包数是多少。后来我强制规定:第0包必须是固件头,包含元信息。
- 坑三:总包数用浮点数计算。 这个前面提过,
ceil(100.0 / 64.0)在某些编译器下会得到1.0而不是2.0。用整数运算,稳得很。
我的建议:分包策略定下来后,写个自动化测试脚本。每次改固件大小,都跑一遍测试,确保分包、发送、接收、重组全链路没问题。我在团队里推了这个做法后,OTA失败率从15%降到了2%以下。
好了,分包策略就聊到这儿。记住:包大小留余量、序号连续递增、总包数整数运算。这三点做到了,OTA的底层就稳了八成。
公众号:蓝海资料掘金营,微信deep3321