4. Friend节点缓存机制:如何为LPN暂存消息
好,咱们进入第四章节。前面聊了LPN怎么睡、怎么醒,但有个关键问题一直没细说——LPN睡着的时候,别人发给它的消息去哪了?
答案就是:Friend节点。Friend节点就像LPN的“消息收发室”。LPN睡觉时,所有发给它的消息都由Friend节点暂存。等LPN醒来,再去Friend节点那里取。
这个机制,说白了就是用空间换时间,用缓存换功耗。我在做智能照明项目时,一开始没把缓存机制吃透,结果LPN醒来后取消息超时,灯控指令丢了,用户按开关灯不亮……嗯,后来老老实实把Friend缓存调优了一遍,问题才解决。
4.1 Friend节点是谁?凭什么它能当“收发室”
Friend节点不是随便选的。它得满足几个硬性条件:
- 不能是LPN本身——Friend和LPN是两种角色,互斥。
- 必须一直在线——Friend不能休眠,得24小时待命。
- 得有足够的存储空间——缓存消息是要占内存的。
- 得在LPN的射频范围内——离太远,通信就断了。
我习惯把Friend节点比作“小区快递柜”。LPN是业主,白天上班(休眠),快递员(其他节点)把包裹塞进柜子。业主下班回家(唤醒),扫码取件。快递柜本身得有电、有空间、还得在小区里。
核心要点:一个Friend节点可以同时服务多个LPN,但一个LPN只能绑定一个Friend。这是蓝牙Mesh协议写死的规则。
4.2 缓存什么?——Friend节点的消息队列
Friend节点为每个LPN维护一个消息队列。队列里存的是什么呢?
- 单播消息:发给这个LPN自己的。
- 组播/虚拟地址消息:LPN订阅了某个组,组里有消息来了。
- 配置消息:比如网络密钥更新、模型配置变更。
但注意,不是所有消息都缓存。Friend节点只缓存低功耗节点感兴趣的消息。怎么判断感不感兴趣?看LPN订阅的地址列表。
举个例子:
// LPN订阅的地址列表(伪代码)
lpn_sub_list = {
0x0001, // 自己的单播地址
0xC000, // 组播地址:客厅灯组
0xC001 // 组播地址:卧室灯组
}
// Friend节点收到消息后,检查目标地址
if (msg.dst in lpn_sub_list) {
enqueue_to_friend_cache(msg);
} else {
// 不感兴趣,直接丢弃或转发
drop_or_forward(msg);
}
我在项目中遇到过一个问题:LPN订阅了太多组地址,Friend节点的缓存队列很快就满了。结果新消息进不来,旧消息被覆盖。后来我限制了每个LPN的订阅数量,才稳定下来。
4.3 缓存怎么存?——Friend Cache的内部结构
Friend节点的缓存,本质上是一个环形缓冲区。每个LPN对应一个独立的缓存实例。结构大致如下:
| 字段 | 说明 | 大小(典型值) |
|---|---|---|
| lpn_address | LPN的单播地址 | 2字节 |
| friend_counter | Friend更新计数器,防重放 | 2字节 |
| queue_size | 队列最大容量(消息条数) | 1字节 |
| head_index | 队列头指针 | 1字节 |
| tail_index | 队列尾指针 | 1字节 |
| messages[] | 消息体数组,每条包含SEQ、SRC、DST、Payload | 可变 |
你想想看,为什么用环形缓冲区?因为内存有限。新消息来了,如果队列满了,就覆盖最旧的消息。这叫“丢老保新”策略。蓝牙Mesh协议就是这么规定的。
注意:如果LPN长时间不唤醒,Friend节点的缓存会被新消息逐步覆盖。LPN醒来后,可能发现某些消息已经丢了。所以LPN的唤醒间隔不能太长,否则消息可靠性无法保证。
4.4 缓存怎么取?——LPN的“取件”流程
LPN唤醒后,会发一条Friend Poll消息给Friend节点。Friend节点收到后,把缓存队列里的消息一条一条发给LPN。流程如下:
- LPN唤醒:从休眠状态进入接收模式。
- 发送Friend Poll:LPN发一条Poll消息,告诉Friend“我醒了,有消息吗?”
- Friend响应:Friend收到Poll后,从队列里取出最早的一条消息,封装成Friend Update消息发出去。
- LPN确认:LPN收到消息后,如果还有更多消息,继续发Poll;如果没有了,就发一条Friend Clear消息。
- 继续休眠:LPN处理完所有消息,再次进入休眠。
这个流程,我画了一张图,你一看就明白:
4.5 缓存容量怎么定?——实战中的权衡
缓存容量不是越大越好。我见过有人把队列设成100条,结果Friend节点的RAM直接爆了。蓝牙Mesh协议规定,Friend缓存的最小容量是1条消息,最大没有上限,但实际受限于硬件资源。
我个人习惯这样估算:
- 每条消息平均大小:约30~50字节(含网络层和传输层开销)。
- LPN唤醒间隔:假设5秒唤醒一次。
- 网络消息速率:假设每秒最多10条消息。
- 所需缓存:5秒 × 10条/秒 = 50条。
所以,如果LPN每5秒唤醒一次,Friend节点至少需要50条消息的缓存空间。换算成内存:50 × 50字节 = 2500字节,约2.5KB。对于大多数MCU来说,这个开销可以接受。
实战建议:如果你用的是Nordic nRF52840或Silicon Labs EFR32系列,建议把Friend缓存设为32~64条。太小了容易丢消息,太大了浪费内存。我曾经在一个项目中把缓存设为128条,结果Friend节点的堆栈溢出了……嗯,后来改回64条,稳如老狗。
4.6 避坑指南——我踩过的三个坑
做Friend缓存,有几个坑我替你们踩过了,直接说:
- 坑一:Friend节点掉电重启,缓存全丢。 缓存是存在RAM里的,掉电就没了。LPN醒来后Poll不到消息,还以为网络出问题了。解决方案:Friend节点掉电后,LPN要重新建立Friend关系。
- 坑二:LPN唤醒太频繁,Friend节点来不及处理。 如果LPN每秒唤醒一次,Friend节点每秒都要处理Poll和Update,CPU负载飙升。我建议LPN唤醒间隔至少2秒以上。
- 坑三:多个LPN共用一个Friend,缓存互相挤占。 每个LPN的缓存是独立的,但总内存是共享的。如果A LPN的消息特别多,可能会挤占B LPN的缓存空间。解决办法:给每个LPN设置最大缓存上限。
4.7 小结
Friend节点缓存机制,是蓝牙Mesh低功耗设计的核心。没有它,LPN根本没法睡觉。记住三个关键点:
- 缓存什么:只缓存LPN订阅的消息。
- 怎么存:环形缓冲区,丢老保新。
- 怎么取:Poll一次取一条,取完为止。