news 2026/11/2 9:21:51

LoRa CAD信道活动检测:扫描与唤醒双端参数自协商实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
LoRa CAD信道活动检测:扫描与唤醒双端参数自协商实践

简介:本资源为一件已公开的发明专利申请文本,聚焦物联网LoRa无线模块的CAD(空闲信道评估)嗅探技术,面向从事低功耗广域网通信、无线唤醒与射频参数自适应研究的工程师、专利撰写者及高校师生,可用于解决双方射频参数不确定时难以快速建立连接、唤醒耗时过长导致功耗偏高的问题。包内共1个PDF文件,即申请公布号为CN 110572843 A的专利全文,约777KB,包含权利要求书、说明书及附图,完整记载了双模块在CAD检测模式与嗅探状态下的交互流程、射频参数序列帧的遍历匹配规则以及唤醒时刻窗口内探测帧与信标帧的收发时序。资源已有261人学习下载,读者可从中获取方法权利要求与系统权利要求的撰写范式、CAD检测与CadDone/CadDelected信号的判断逻辑,以及降低唤醒时间、提升通信能效的具体实现思路,适合作为专利写作参考或LoRa低功耗通信方案设计的技术依据。

1. 从一页专利到能跑的嗅探:LoRa CAD 到底省下了哪一段等待

一个很典型的现场:几十个节点散在几百米范围内,射频参数出厂就写死了,换一个批次模组,一多半就连不上。调试的人拿着频率和扩频因子一个组合一个组合地试,试到第三个太阳都下山了。麻烦的根源在于 LoRa 的物理层约束非常硬——频率、扩频因子、带宽三个参数必须完全一致,对端才可能解出前导码。而芯片本身给了另一条路:信道活动检测,也就是 CAD。它只需要一个符号周期就能判断信道上有没有相同参数的 LoRa 前导码,而接收模式必须把整个包收完才能确定。

这份资源是一份发明专利文本,讲的是怎么把 CAD 从"一个中断"变成"一套可执行的双端状态机":扫描模式下两个模块互换探针帧,一边轮询参数一边应答,用 50~1000ms 的代价把参数对齐;唤醒模式下用一串嗅探帧去撞对方极短的唤醒窗口,撞上就回信标,然后转入正常收发。它适合做 LoRa 私有组网、电池节点低占空比唤醒、现场参数自协商的从业者;如果你已经在用成熟协议栈且不需要跨参数互通,这套东西基本用不上。

2. 参数空间与 CAD 中断:先搞清楚什么条件下才可能"撞上"

动手写代码之前,得先把"什么参数会影响 CAD、什么参数不影响"这件事分清楚,否则序列会做得又长又慢,成功率还上不去。

2.1 Freq/SF/BW 三元组与序列的笛卡尔积

LoRa 解前导码靠的是频率、扩频因子、带宽三者的组合匹配。编码率 CR、显式/隐式包头、载荷 CRC 开关这些参数影响的是载荷能不能解开,不影响前导码的相关运算。这个区分对嗅探设计非常关键:CAD 检测的是前导码,所以 CR 填错顶多让后面的数据帧解不出来,但 CAD 照样会给出 CadDetected;反过来,SF 差一级、带宽差一档、中心频点偏几十 kHz,CAD 就绝对不会响应。

常见做法是把待扫描的参数写成三元组序列,而不是只写频率:

参数是否影响 CAD说明
中心频率 Freq影响偏一个信道带宽就检测不到
扩频因子 SF影响SF 决定符号速率,也决定 CAD 耗时
带宽 BW影响125kHz 与 500kHz 的前导码互不相关
编码率 CR不影响只影响载荷解码
包头模式不影响显式/隐式只影响包结构
同步字部分影响有些型号 CAD 不校验同步字,需实测确认

因为 CR 和包头模式被排除在外,序列维度直接少了一半。假设现场只可能有 3 个频点、6 档 SF、1 档带宽,全笛卡尔积是 18 个组合。听起来不多,但每次 CAD 的耗时随 SF 指数增长,SF12 在 125kHz 下单次就要 32ms 左右,18 个组合走一轮接近半秒,功耗和实时性都得重新算账。所以序列必须裁剪:先用已知的频段范围和带宽范围砍掉一大半,剩下的按"最可能先命中的排在前面"来排序。

2.2 CAD 的七个动作与 CadDone/CadDetected 的先后关系

芯片进入 CAD 之后并不是"一直听",而是一段固定流程:锁 PLL、接收机抓取一个符号周期的信道样本、关掉接收机和 PLL、调制解调器在数字域做样本与理想前导码波形的相关运算、产生 CadDone 中断(相关成功时同时置 CadDetected)、芯片回到待机、软件根据结果决定是否切到接收模式。

这段流程决定了软件层的判据顺序:先等 CadDone,再读 CadDetected。反过来写就会翻车——CadDetected 是伴随 CadDone 一起出现的标志位,你在 CadDone 之前去读,读到的永远是上一次的残留值。

// 一次完整的 CAD 周期,以 SX127x 系列为例(具体位定义以对应型号手册为准) radio_set_params(px.freq, px.sf, px.bw); // 写 RegFrf / RegModemCfg1..3 radio_clear_irq(); // 清掉上一次的 CadDone / CadDetected radio_start_cad(); // RegOpMode 切到 CAD 模式 if (radio_wait_irq(IRQ_CAD_DONE, CAD_TIMEOUT_MS)) { if (radio_get_irq(IRQ_CAD_DETECTED)) { // 相关成功,信道上存在同参数前导码 } }

这段代码里最容易被忽略的是radio_clear_irq()。CadDone 和 CadDetected 是锁存型标志,不清就会把上一轮的"命中"带进下一轮,表现出来就是序列里某个参数永远"成功",但实际数据一帧都收不到。CAD_TIMEOUT_MS不能拍脑袋写死,它至少要比一个符号周期长出一截:SF11 在 125kHz 下符号周期大约 16ms,加上 PLL 锁定和处理开销,给 30~50ms 才算稳妥;如果序列里混了 SF12,超时就得放到 60ms 以上。超时给太短的直接后果是软件以为"没信号",实际上是芯片还在算。

2.3 为什么 CAD 比 Rx 便宜:符号周期对整包时长

把数量级摆出来就清楚了。符号周期 Tsym = 2^SF / BW:

SFBW单符号周期单次 CAD 大致耗时
7125kHz1.02ms2~4ms
9125kHz4.10ms6~10ms
11125kHz16.4ms20~30ms
12125kHz32.8ms35~50ms
12500kHz8.19ms10~15ms

接收一个普通数据包,光前导码就要 8 个符号,再加载荷、CRC、收尾,SF9 下几十毫秒起步,SF12 下轻松过百毫秒。也就是说,一次 CAD 的代价大致等于"收一个包"的十分之一到三分之一,而且不需要把包收完整。当你需要在 18 个参数组合里找出唯一正确的那一个时,用 Rx 逐个蹲守是做不完的,用 CAD 至少能在一秒量级内收工。

还有一层工程上的好处:CAD 期间接收机只开一个符号周期,其余时间射频前端是关的。对电池节点来说,这意味着"守听"这件事的占空比可以做到极低——唤醒模式那一章的所有时序设计,本质上都是在利用这一点。

3. 扫描模式复现:两个状态机如何互换探针帧

扫描模式解决的是"双方都不知道对方具体参数,只知道一个范围"的场景。思路是把参数序列拆成两半:监听方在自己的序列上轮询 CAD,嗅探方在自己的序列上发探针帧并同时自检。谁先撞上谁就先开口。

3.1 射频参数序列的构造与 S2 ⊆ S1 的硬约束

两个序列的关系是这套方法的地基:嗅探方的序列 S2 必须是监听方序列 S1 的子集。原因很直白——监听方的 CAD 只在 S1 里的参数上蹲守,如果嗅探方某个参数不在 S1 里,那个参数无论发多少次都不会有人应答,白烧电。

# 构造两个序列:S2 必须严格取 S1 的子集,且按命中概率排序 S1 = [ (470_300_000, 9, 125_000), # P1 (470_300_000, 10, 125_000), # P2 (470_300_000, 11, 125_000), # P3 (471_500_000, 9, 125_000), # P4 (471_500_000, 10, 125_000), # P5 (471_500_000, 11, 125_000), # P6 ] # 现场统计里 P4 命中率最高,所以排在嗅探序列第一位,缩短平均耗时 S2 = [S1[3], S1[1], S1[5]] assert all(p in S1 for p in S2), "S2 必须是 S1 的子集" print(len(S1), len(S2)) # 6 3

参数说明:三维元组的顺序固定为(频率, 扩频因子, 带宽),三个值都要带上,只写频率会导致带宽不一致的组合同样进序列,白白拉长扫描轮次。子集断言不是形式主义,现场改参数的时候最容易犯的错就是两边序列各改各的,最后变成"能通但很玄学"。

3.2 第一模块侧:Px 自增、CadDetected 判定与 x 回绕

监听方的逻辑是一个不会结束的循环:取序列里的一个参数、初始化 CAD、等 CadDone、判 CadDetected。命中就回一个探针帧加一个信标帧,然后切接收;没命中就换下一个参数。序列走完了不是退出,而是回绕到开头继续——因为对端可能才刚刚上电。

// 第一模块(监听方):在 S1 上轮询 CAD,命中后回 L1 探针帧 + L2 信标帧 #define N_PARAM 6 static const rf_param_t S1[N_PARAM] = { {470300000, 9, 125000}, {470300000, 10, 125000}, {470300000, 11, 125000}, {471500000, 9, 125000}, {471500000, 10, 125000}, {471500000, 11, 125000}, }; void module_a_scan(void) { int x = 0; for (;;) { rf_param_t px = S1[x]; radio_set_params(px.freq, px.sf, px.bw); radio_clear_irq(); radio_start_cad(); if (radio_wait_irq(IRQ_CAD_DONE, cad_timeout_ms(px.sf, px.bw))) { if (radio_get_irq(IRQ_CAD_DETECTED)) { radio_tx_frame(L1); // 回第一 CAD 嗅探帧 radio_tx_frame(L2); // 紧跟信标帧 radio_rx_continuous(FIRST_RX_TIMEOUT_MS); // 同参数进接收 if (radio_rx_ok()) return; // 收到应用数据,收工 } } x = (x + 1) % N_PARAM; // x > n 时回绕到 1,继续下一轮 } }

几个点值得说明。cad_timeout_ms()应该按 SF 和 BW 算出来,而不是常量,否则 SF12 那几档会稳定超时。命中后先发 L1 再发 L2 是原文的顺序,L1 的作用是让嗅探方也能用 CAD 确认一次"对方真的在",L2 才是携带必要信息的信标。切接收之后必须有超时,原文里对应"第一预设时间",这个值我一般给到 200~500ms——比一次完整数据帧的时长略长即可,太长会让命中失败的那一轮拖得很慢。

3.3 第二模块侧:嗅探帧节奏与 y 越界退出

嗅探方的动作是:设参数、发 K1、自检 CAD、命中就切接收等信标,没命中就换下一个参数。走完整个 S2 还没结果,就判定本次嗅探失败并结束,这一点和监听方的"无限回绕"不同,也是原文里明确区分的。

// 第二模块(嗅探方):在 S2 上发 K1 并自检,命中后切接收等信标 #define M_PARAM 3 static const rf_param_t S2[M_PARAM] = { {471500000, 9, 125000}, // Q1 {470300000, 10, 125000}, // Q2 {470300000, 11, 125000}, // Q3 }; bool module_b_sniff(void) { for (int y = 0; y < M_PARAM; y++) { rf_param_t qy = S2[y]; radio_set_params(qy.freq, qy.sf, qy.bw); radio_tx_frame(K1); // 先发一帧嗅探帧 radio_clear_irq(); radio_start_cad(); // 再自检,等对方回 L1 if (!radio_wait_irq(IRQ_CAD_DONE, cad_timeout_ms(qy.sf, qy.bw))) continue; if (!radio_get_irq(IRQ_CAD_DETECTED)) continue; radio_rx_continuous(SECOND_RX_TIMEOUT_MS); // 用 Qy 进接收 if (radio_rx_frame(&beacon) && beacon.type == L2) { radio_tx_app_data(); // 嗅探成功,发应用数据 return true; } } return false; // y > m,嗅探失败,结束本次嗅探 }

参数上要注意SECOND_RX_TIMEOUT_MS和监听方的FIRST_RX_TIMEOUT_MS的关系。嗅探方是在发出 K1 之后才开始等信标的,而监听方要先收 K1 再发 L1 和 L2,中间的帧间隔、处理延迟都算在嗅探方的等待窗口里。经验值上,嗅探方的接收超时要比监听方短一些,300ms 左右比较合适——等太久没有意义,对方如果命中了,信标几乎是在毫秒级内回来的。

3.4 超时窗口:第一/第二预设时间怎么给

两个超时值不是随手填的常数,它们对应的是"这一轮判定失败要花多久"。给短了,本来能成功的轮次被判死;给长了,失败轮次拖慢整体。

一个可用的算法是:先算出该参数下的单包时长,再乘以一个系数。

def symbol_time(sf, bw): return (2 ** sf) / bw # 秒 def cad_timeout_ms(sf, bw): tsym = symbol_time(sf, bw) return int(tsym * 1000 * 2) + 10 # 两个符号周期 + 10ms 处理余量 def rx_timeout_ms(payload_bytes, sf, bw, preamble=8): tsym = symbol_time(sf, bw) n_sym = preamble + (payload_bytes * 8) / sf * 1.25 # 粗略估算 return int(tsym * 1000 * (n_sym + 4)) print(cad_timeout_ms(11, 125_000)) # SF11/125k print(rx_timeout_ms(20, 9, 125_000)) # SF9/125k 收 20 字节

第一组结果大概在 40ms 上下,第二组在 300ms 上下。这两个数直接决定"失败一轮"的成本,也就决定了序列长度还能不能撑得住。如果你把 S1 做到 18 个组合、每轮平均 100ms,走完一轮就是 1.8 秒——这已经超出大多数低功耗场景能接受的范围了,所以前面说的"序列裁剪"不是优化项,是前提条件。

4. 唤醒模式复现:T1/T2/T3/T4 四个时刻怎么排

唤醒模式和扫描模式的差别在于信息不对称的方向反过来了:这次嗅探方知道对方大概什么时候会醒,也知道唤醒后对方用什么参数来做检测。目标变成"用最小的唤醒窗口,把对方叫醒并让它收到数据"。

4.1 时序不等式的推导:为什么信标必须晚于 T4+Td

原文给的约束是 T2 < T1 < T4 ≤ T3 < T4 + Td。逐个拆开看:

T2 到 T3 是嗅探方连续发射嗅探帧的窗口,T3 时刻它切到接收模式。T1 是唤醒方开始做 E 次 CAD 检测的时刻,必须落在 T2 之后,否则唤醒方开机的时候嗅探方还没开始发,第一轮检测必然空转。T4 是唤醒方实际检测到嗅探帧、产生 CadDetected 的时刻,它天然满足 T1 < T4,并且不可能晚于 T3——晚于 T3 说明嗅探方已经停了,那个前导码根本不存在。

最关键的是最后一段:T3 < T4 + Td。Td 是唤醒方从"最后一次命中"到"发出信标"之间的固定延迟。这个不等式的物理含义是——信标必须在嗅探方切到接收模式之后才发出去,否则嗅探方还在发射,收不到。

提示:这条不等式是整个唤醒模式的命门。Td 给短了,信标发得比嗅探方切接收还早,表现为"CAD 能测到、数据永远收不到",排查时极难定位。

反推 Td 的取值范围:因为 T4 ≤ T3,所以 Td 至少要为 0,实际必须留出保护余量,也就是 Td ≥ (T3 − T4) + 保护时间。而 T4 可能非常接近 T3(嗅探方最后一帧撞上了),所以工程上 Td 一般按"嗅探方窗口长度"来配,而不是按"剩余时间"来配。

4.2 嗅探端:T2 到 T3 连续发射与 T3 切换接收

嗅探端的动作很朴素:在窗口内尽量密集地发,到点就切接收。

// 嗅探端:在 [T2, T3] 内连发嗅探帧 J,T3 时刻转入接收等信标 void module_b_wake_burst(uint32_t t2, uint32_t t3) { radio_set_params(P0.freq, P0.sf, P0.bw); wait_until(t2); uint32_t now = millis(); uint16_t seq = 0; while (now < t3) { radio_tx_frame(&(frame_t){ .type = J, .seq = seq++ }); now = millis(); } radio_rx_continuous(BEACON_WAIT_MS); // T3 起进入接收 if (radio_rx_frame(&beacon) && beacon.type == L2) { radio_tx_app_data(); } }

这里的参数 P0 是固定的,不再是序列——唤醒模式下双方已经约好了同一个射频参数,不需要扫描。seq字段用来让唤醒方区分不同的嗅探帧,虽然 CAD 本身不关心载荷内容,但调试时靠它判断"到底收到了几帧"。BEACON_WAIT_MS要比 Td 加一个包的时长更长,否则信标还在路上就超时了,这一条和 4.1 的不等式是配套的。

还有一个容易忽略的细节:窗口长度 T3 − T2 必须大于"发一帧 J 的时长 + 一次切换的余量"。如果窗口只够发半帧,最后一帧会被截断,截断的帧前导码可能不完整,CAD 照样能测到(因为 CAD 只看前导码),但后面切接收时会因为包不完整而丢包——这就是典型的"看起来成功了,实际一帧数据都没收到"。

4.3 唤醒端:E 次 CAD 检测、T4 刷新与 Td 到期发信标

唤醒端的核心是"检测到就续命":每命中一次就把 T4 往后推,并且再补 E 次检测,这样只要嗅探端还在发,唤醒时刻就一直在往后延。

// 唤醒端:T1 起做 E 次 CAD,命中后刷新 T4 并再补 E 次,T4+Td 到点发信标 void module_a_wake_detect(uint32_t t1, uint32_t td, int e) { wait_until(t1); radio_set_params(P0.freq, P0.sf, P0.bw); uint32_t t4 = 0; bool hit = false; for (int i = 0; i < e; i++) { radio_clear_irq(); radio_start_cad(); if (radio_wait_irq(IRQ_CAD_DONE, cad_timeout_ms(P0.sf, P0.bw)) && radio_get_irq(IRQ_CAD_DETECTED)) { t4 = millis(); // 记录 CadDetected 时刻 hit = true; i = 0; // 命中就重新计数,等价于“再补 E 次” } } if (!hit) return; // E 次全空,本次嗅探结束 while ((millis() - t4) < td) { /* 极浅休眠 */ } radio_tx_frame(L2); // T4 + Td 到点,以 P0 发信标 radio_rx_continuous(APP_RX_MS); }

i = 0这一行是原逻辑里"再进行 E 次 CAD 检测"的直译,但它在实现上有个明显风险:如果信道上持续有同参数前导码(比如正好有另一个设备在发),这个 for 循环永远退不出去。血泪经验是必须加一个总次数上限或总时长上限,超出就强制跳出。

const int MAX_CAD_ROUNDS = 64; // 兜底上限,防止死循环 int rounds = 0; ... if (++rounds > MAX_CAD_ROUNDS) break;

这个上限值不需要很精确,它的作用只是把"卡死"变成"失败退出"。没有这一行的版本,在现场一旦遇到干扰源,整个节点就再也不发信标也不进休眠,电池两小时耗光。

4.4 一张参数表:TS / E / Td / T3−T2 的取值区间

符号含义典型取值约束与影响
TS唤醒端 CAD 检测启动周期200~1000ms越小越灵敏,功耗越高
E单次唤醒的 CAD 检测次数1~3超过 3 次基本是白烧电
T3 − T2嗅探端连续发射窗口50~200ms要容纳至少 3~5 帧完整嗅探帧
Td命中到发信标的固定延迟≥ (T3 − T4) + 1 包时长配小了信标必丢
BEACON_WAIT_MS嗅探端等信标窗口Td + 2 包时长与 Td 联动,不能独立调

表里最需要联调的是 Td 和 BEACON_WAIT_MS 这一对。实际调试时我的做法是把 Td 从大到小扫一遍:先给一个明显偏大的值(比如 300ms),确认能通,再以 20ms 为步长往下压,压到开始丢包就回退两档。这个值对温度、晶振偏差敏感,别指望一次调完能用一整年。

E 的取值也要说清楚。E 加大确实能提高命中概率,但每次 CAD 都是一次 PLL 锁定加一次相关运算,SF12 下单次就要 30ms 以上,E=5 意味着一次唤醒里至少 150ms 的活跃时间。低占空比设计里,这个数字直接决定电池寿命。我通常把 E 控制在 1~3,靠 TS 拉长周期来省电,而不是靠 E 大来抓信号。

5. 避坑:五个把嗅探成功率按到地板的细节

5.1 前导码比 CAD 窗口短,CadDetected 永远起不来

现象:嗅探方确定自己在发,逻辑分析仪也看得到空中有波形,但两边的 CadDetected 一次都没置起来。

原因:嗅探帧太短。CAD 的相关运算是对着一个符号周期的样本做的,如果帧的前导码只有 4 个符号,而对方 CAD 的采样窗口正好落在载荷段,那一段里没有任何前导码波形,相关必然失败。同理,发送帧的前导码长度设得过短,会显著降低被撞上的概率。

解决:把前导码长度拉到 8 个符号以上,并且在嗅探帧的载荷后面留一小段填充,让整个帧的空中时长至少覆盖两到三个 CAD 周期。改之前先量一下自己帧的实际空中时间,别只看代码里的长度字段。

5.2 双方驻留时间错拍,参数明明在序列里也对不上

现象:S2 确实是 S1 的子集,代码逐行对过没有错,但成功率极低,偶尔能通一次。

原因:监听方在 Px 上只停了一个 CAD 周期(毫秒级),测不到就立刻换下一个参数。而嗅探方发一帧 K1 本身就要几毫秒到几十毫秒,发完还得再做一次 CAD。两边的节奏完全不在一个数量级上,嗅探方发 K1 的时刻,监听方往往已经在序列的另一头了。所谓"命中"变成了一个概率极低的事件。

解决:让监听方在每个参数上多停一会儿,或者让嗅探方每个参数连发多帧。前者改for循环里的驻留计数,后者在发 K1 的地方加个 3~5 次的小循环。我更倾向后者,因为它不增加监听方的功耗,只在嗅探方那一侧多花一点点时间。

5.3 唤醒模式测得到前导码,信标却永远收不到

现象:唤醒端的日志清楚显示 CadDetected 触发了、T4 也记上了,但嗅探端从头到尾没收到 L2。

原因:Td 配小了,信标在嗅探端还没切到接收模式的时候就发完了。这个坑在 4.1 的不等式里已经写过,但现场特别容易踩,因为 T4 每次都不一样——如果最后一次命中恰好发生在 T3 前一瞬间,留给 Td 的余量就几乎为零。

解决:Td 不要按"平均剩余时间"算,按"最坏情况"算,也就是按 T3 − T4 = 0 来算,取 Td ≥ 一个完整嗅探窗口。宁可多等几十毫秒,也不要出现"测得到收不到"这种最难查的半成功状态。

5.4 只对齐 SF 不对齐 BW,换台设备就失联

现象:实验室里同一个批次的两块板子怎么都能通,现场换成不同批次的模组,参数序列完全一样,成功率直接掉到零。

原因:序列里只写了频率和扩频因子,带宽那一维是隐含的默认值。不同批次的模组出厂默认带宽可能不同,或者封装库里的初始化顺序把带宽覆盖掉了。带宽不一致时前导码的符号速率不同,相关运算直接不成立,CAD 一点反应都没有。

解决:把带宽显式写进三元组,并且在每次切参数之后回读配置寄存器确认写入生效。这一步看起来多余,但它能把"配了但没生效"这类问题挡在调试阶段,而不是留到现场。

5.5 为了省电把 E 调大,功耗反而翻倍

现象:按"多检测几次更容易被叫醒"的思路把 E 从 1 调到 8,结果待机电流没降,平均功耗还涨了一大截。

原因:E 次 CAD 是串行执行的,每次都要重新锁 PLL、重新采样、重新做相关运算。E=8 在 SF11 下意味着一次唤醒里超过 200ms 的射频活跃时间,而唤醒周期本身可能才 1 秒。占空比从 2% 飙到 20%,省电设计直接失效。

解决:E 保持小值(1~3),靠拉长 TS 和优化序列顺序来提升整体命中率。真正影响成功率的不是单次检测几次,而是"对方发的时候你到底在不在听"——这个问题由时序设计解决,不由 E 解决。

6. 把"能通"变成"可度量":嗅探成功率与首包时延的量化方法

这套东西最容易出问题的地方,是它"有时能通"。断断续续的成功会让人误以为是干扰,然后花几天时间去查天线和电源。我的做法是先把它变成两个可测的数字,再谈优化。

6.1 成功率与首包时延的两个统计口径

第一个指标是嗅探成功率:固定一组已知参数,重复 N 次(N 至少 200),统计从嗅探方发出第一帧 K1 开始,到收到应用数据首字节为止的成功次数占比。这个数字要在 90% 以上才算可用,低于 80% 基本说明时序有余量不足的地方。

第二个指标是首包时延:成功那部分的耗时分布,看的是中位数和 P95,而不是平均值。平均值会被少数极端值带偏,而低功耗场景真正在意的是 P95——最慢的那 5% 决定了你的唤醒窗口要开多大。

import statistics def summarize(latencies_ms, attempts): ok = len(latencies_ms) s = sorted(latencies_ms) return { "success_rate": ok / attempts, "median_ms": statistics.median(s), "p95_ms": s[int(len(s) * 0.95) - 1], "max_ms": s[-1], } print(summarize([85, 120, 96, 210, 143, 98, 130], 10))

参数说明:attempts是总尝试次数而不是成功次数,成功率的分母必须用总次数,否则会把失败样本悄悄丢掉。p95_ms的取值方式在样本量小时会偏保守,样本少于 100 的时候建议直接看max_ms。这套统计我在每次改完时序参数后都跑一遍,前后对比,比"感觉好像好了一点"可靠得多。

6.2 参数兜底:从全量穷举到按 SNR 排序的热缓存

序列顺序对平均耗时的影响比想象中大。全量穷举时平均要扫一半序列,但如果把上次成功过的参数排到最前面,第二次上电往往第一发就命中。

具体做法是留一小块非易失存储,存最近若干次成功的三元组,按成功次数排序;每次嗅探开始时先按缓存顺序试,缓存全失败再回落到完整序列。缓存不用大,4 条就够,写次数也能控制住。这个改动不涉及任何时序调整,纯粹是把"上次的答案"当作先验,实测能把首包时延的中位数压掉一大截。

另外值得加的是失败计数。每一轮嗅探结束时把"走到第几个参数才成功"记下来,连续多轮都停在序列尾部,说明序列的前半段全是无效组合,该裁剪了。这个计数比任何日志都好用,因为它直接告诉你是谁在拖慢平均时间。

最后说一个教训。早期我搭这类双端对拍的东西,习惯直接上代码,边跑边调,觉得时序参数"试试就知道了"。结果有一次在现场卡了整整两天,最后发现是 Td 比 T3 − T4 小了 15ms——一个在任何日志里都不会出现的数字。从那以后我每次做这类东西,都强制先在纸上把时间轴画出来,把 T1、T2、T3、T4、T4+Td 五个点挨个标上,保护余量单独写成一行,确认不等式成立之后才允许自己打开编辑器。这个习惯救过我很多次,也希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/11/2 9:21:05

HBase实时大数据处理核心优势与架构实践

1. HBase在实时大数据处理中的核心优势HBase作为Hadoop生态中的分布式列式数据库&#xff0c;在实时大数据处理场景中展现出独特的价值。与传统的批处理系统不同&#xff0c;HBase通过其特有的架构设计实现了高吞吐、低延迟的数据访问能力。我在实际项目中多次采用HBase构建实时…

作者头像 李华
网站建设 2026/11/2 9:20:45

Agent判断器选型与部署:Laya、Jev如何给大模型装刹车

最近调试Agent的时候&#xff0c;我又遇到了那个熟悉的翻车现场&#xff1a;用户只是随口问了一句“今天出门要带伞吗”&#xff0c;结果Agent一口气调用了天气、日历、地图三个工具&#xff0c;最后返回一句“根据你的日程安排&#xff0c;建议带伞”。问题不在于大模型答错了…

作者头像 李华
网站建设 2026/11/2 9:19:37

MFC单文档自绘标题栏:WM_NCCALCSIZE、双缓冲与高DPI适配

简介&#xff1a;面向Windows桌面开发者的MFC自定义界面实践包&#xff0c;聚焦单文档框架下自行绘制非客户区&#xff0c;重点解决默认标题栏、边框与关闭按钮样式难以个性化的问题。内容以C源码为主体&#xff0c;配合头文件、资源脚本、图标位图与可运行示例&#xff0c;覆盖…

作者头像 李华
网站建设 2026/11/2 9:18:55

AI应用底座是什么?从模型到企业稳定服务的最后一公里

1. 先厘清一个问题&#xff1a;企业AI落地卡在了哪里这两年几乎没有哪家企业不谈AI&#xff0c;大模型、智能体、AI代码助手这些名词已经渗透进各种汇报PPT里。但我接触过不少团队&#xff0c;表面热闹&#xff0c;实际一盘点&#xff1a;试点项目做了七八个&#xff0c;真正进…

作者头像 李华
网站建设 2026/11/2 9:17:08

基于AT89C52与DS18B20的数字温度计设计与报警实现

简介&#xff1a;面向电子信息、自动化及单片机课程设计人群的毕业论文文档&#xff0c;围绕基于AT89C52单片机与DS18B20智能温度传感器的数字温度计展开&#xff0c;可用于课程设计、毕业设计选题参考与测温报警方案借鉴。压缩包共1个doc文件&#xff0c;约301KB&#xff0c;正…

作者头像 李华
网站建设 2026/11/2 9:16:07

Zipkin分布式追踪系统:原理、实践与性能优化

1. Zipkin的核心价值与应用场景分布式系统已经成为现代互联网架构的标配&#xff0c;但随之而来的复杂性也带来了新的挑战。当用户请求在数十个微服务间流转时&#xff0c;如何快速定位性能瓶颈&#xff1f;如何理解服务间的依赖关系&#xff1f;这正是Zipkin这类分布式追踪系统…

作者头像 李华