btfz-sdr/gnuradio_demo/pluto-ble-adv-rx
Claude Code and agent skill by btfz-sdr/gnuradio_demo
- community
Checking that the SKILL.md file is still in the repository.
PlutoSDR(AD9361) 纯 C 接收 BLE LE 1M 广播包 —— 解调链设计、access code 一次 100% 命中的根因、全相位 AA 搜索的两个坑、离线回放验证流程。改 ble_phy.c/排查 AA 匹配不到 40-40/漏包/移植 GNU Radio gfsk_demod 到嵌入式时用本技能。
Install
npx vskill@latest install btfz-sdr/gnuradio_demo/pluto-ble-adv-rxSKILL.md
PlutoSDR BLE 广播包接收(LE 1M)
工程根目录:本文件所在目录往上两级(即 ble_phy.c 所在目录)。 编译+上机:make run CH=37 SAMPLE=10(scp + ssh root@pluto.local,密码 analog)。
触发条件
- 改
ble_phy.c(解调、AA 搜索、解白化、CRC、包解析) - access code 匹配率上不去("只能 32/40""从来没有 40/40")
- 收包比预期少(漏包)、解出来的 PDU 是乱码、CRC 不过
- 把 GNU Radio
gfsk_demod的思路搬到嵌入式实时目标 - 出现"射频看着没问题但判决比特是随机的"这类 GFSK/FSK 症状
工程文件地图
| 文件 | 作用 | |
|---|---|---|
ble_phy.c / ble_phy.h | 解调器(IQ → 比特 → AA → PDU)+ 发射端通用函数(CRC/白化) | |
pluto_ble_adv_rx.c | 主程序:IIO 采集、AD9361 配置、分块喂解调器 | |
ble_replay.c | 离线回放:把录到的 IQ 喂同一个 ble_phy.c(宿主机 gcc) | |
makefile | build / run / capture(网络 IIO 抓 IQ)/ replay | |
doc/demodulator.md | 解调链设计、四个问题的证据、调参表、实测数据 | |
doc/gnuradio-gfsk-demod.md | GNU Radio 参考实现的确切参数与公式、为什么不能照搬 | |
tools/ | 独立参考实现:合成包生成器、AA 全相位扫描、候选解码(含 README) |
处理链(当前实现)
链路上没有 DC/频偏扣除,也没有时钟恢复环。与 GNU Radio digital.gfsk_demod(鉴频 → M&M → 硬判决)的刻意不同:
- 加一级 1 符号 boxcar(FSK 匹配滤波)。10 Msps 下单样点鉴频的相位步进只有
±0.157 rad,噪声按 5 MHz 级带宽进来,不平均判不出来(约 +10 dB Eb/N0)。
- 定时用前馈全相位并行,不用反馈环。每样点只把 1 bit 推进"相位 = 样点 % sps"
那个窗口并只比较该窗口 → O(1)/样点,覆盖全部相位空间。 M&M 环在 BLE 上不可用:8 bit 前导码太短收不敛,且环在眼图交叉点另有一个 稳定平衡点(实测锁在交叉点,软判决幅度 0.16 而正确采样点 1.5)。
四个已定位的坑(都在真实空口上量化过)
1. access code 比特序("40/40 永远不可能"的元凶)
BLE 的 0x8E89BED6 是 LSB 先发,空中字节顺序是 D6 BE 89 8E。 按书写顺序拼 8E 89 BE D6 当模板 = 拿反的地址做相关, 任何真实包最多落到随机水平(~20/40),解调器再完美也到不了 40/40。
2. CRC-24 的空中字节序
三个 CRC 字节是 swap(高字节), swap(中字节), swap(低字节)(swap = 字节内比特反转), 不是 LSB-first。任何生成器都要拿真实包验证一次: 美的设备 PDU ...414331313639 → CRC 字节 A1 5B A5。
3. AA 命中的相位 ≠ 能解出 PDU 的相位 —— 每个相位都要各起一路收包(漏包主因)
40 bit 的 AA 对采样时刻不敏感(判断裕量大),后面 ~300 bit 的 PDU 不是: 逐包把 10 个定时相位都试一遍后,同一个真实包平均只有 4.9/10 个相位能过 CRC, 其余相位 AA 误差同样是 0,但 PDU 里错 1~2 bit → CRC 必挂。
所以"AA 一命中就锁死那个相位、收包期间还停掉 AA 搜索"≈ 按 5/10 的概率抽签。 实测同一批 2.1 s 录波:修前 ch37/ch38/ch39 = 4 / 7 / 16 包,修后 = 18 / 23 / 47 包。
正确做法:每个定时相位各起一路收包 —— AA 在哪个相位命中就在哪个相位上独立累计 PDU 位,谁先过 CRC 谁算数;同一包会有多路同时成功,报告端按样点位置去重 (100 样点足够,真实 BLE 包至少 88 符号 = 880 样点)。 实现要点:某一路刚起头时,当前样点的 bit 已经含在 40 bit 窗口里, 不要再往这一路追加一次 —— 否则整包错位 1 bit、全部解不出来(这个坑我踩过, 现象是"改完一个包都不出了")。
4. 不要盲目扣 DC/频偏(试过,删掉了)
"AD9361 有残余 LO 偏置所以要在鉴频输出上扣 DC"听着很合理,实现成 "只喂 |d| < 0.5 rad 的样点、时间常数 13 ms 的门控跟踪器"在 0.26 s 录波上 确实从 0 包变 2 包。但在 2.1 s 录波上量化后是整体有害的:
- 跟踪器收敛的是噪声的不对称性(-0.061 rad/样点),不是包处的眼图偏置(~0);
- 扣掉它等于把眼图平移 0.6(信号只有 ±1.5),弱符号成片翻错:
门控 DC 6 包 vs 不扣 DC 16 包;全 6 段录波 35 vs 45 包。
所以链路不做 DC 扣除,和 GNU Radio/参考实现一致。真看到眼图整体偏一侧, 先查增益(削顶会产生真实的鉴频偏置),别用盲目估计器补。
排查方法论
- 先证明锅在谁身上:拿原始录波用匹配滤波滑窗量 AA 相关(
tools/aa_scan.py)。
随机比特的汉明距离均值约 20/40;真实干净信号匹配滤波后是 40/40、软幅度 ~±1.5。 若真实信号 40/40 而 C 的输出落在随机统计上 → 锅在解调器,不在射频/增益。 (旧代码基线:257801 bit 内最高 34/40、≥32 的 16 个、CRC OK 0 个 = 纯随机。)
- 拿独立实现当基准(
tools/decode_cand.py,不共用ble_phy.c):
同一段录波它解出多少 CRC-OK 包,C 就该接近多少。差很多就继续查。
- 录波 + 回放(不接设备也能迭代):
``bash make capture CH=37 SEC=2 FILE=ch37.bin # 走网络 IIO,不需要登录 Pluto make replay FILE=ch37.bin CH=37 ` ble_replay.c 与设备端共用同一个 ble_phy.c`,离线结论可以代表设备端。
- 合成信号回归(不依赖空口):
python3 tools/gen_ble.py 30 && make replay FILE=synth_snr30.bin CH=37
- 要定位"某个包为什么丢了",把 C 内部的候选表和独立参考的候选表按全局样点号
对齐比较。注意 `demodulate_ble_packets` 的 `i` 是块内下标(每次调用从 0 开始, 主程序按 262144 分块),跟全局样点号差一个块偏移 —— 直接拿它和录波位置比会得出 完全错误的"这里没有候选"结论。要自己加 static long long g_n 数全局序号。
上机/环境注意事项
- Pluto 上没有
timeout(busybox),也没有sshpass:
非交互 ssh/scp 用 SSH_ASKPASS=<脚本> SSH_ASKPASS_REQUIRE=force setsid -w ssh/scp ...; scp 必须加 -O(固件里没有 /usr/libexec/sftp-server)。 askpass 脚本是从环境变量 `PLUTO_PW` 取密码的(脚本本体只有 printf "%s\n" "$PLUTO_PW"): 新 shell 里忘了 export 就会稳定报 Permission denied,很容易误判成"设备挂了/密码不对"。 工程里的 pluto.sh/pluto_scp.sh 会自己带上,手写命令前先 source pluto_lib.sh。 批量 scp 还会偶发静默失败:传完必须核对设备端文件的 md5/size, 否则会出现"程序跑了但一个包都没有"——其实是文件根本没传上去。
- 不要留进程在设备上:
(cmd > log 2>&1 & echo $! > pid); sleep N; kill $(cat pid)。 - 某些固件上
iio_attr ... hardwaregain <n>会返回 EINVAL(即使模式已是 manual)——
在主程序里无副作用(程序只设 fast_attack AGC),但辅助命令要写成非致命。
- 采样率通过
ble_phy_set_sample_rate(fs_mhz)传入,sps 由此推导(默认 10 MHz → 10 样点/符号)。
采样率低于 4 MHz 时匹配滤波增益太小,不建议。
实测基线(当前版本,改动后可对比)
| 场景 | 结果 | |
|---|---|---|
| 离线回放真机录波(各 2.1 s,ch37/38/39) | 18 / 23 / 47 包;独立 Python 参考 18 / 18 / 36(参考每位置只试一个相位,所以它自己漏包) | |
| 上机实时 ch37 / 16 s(10 Msps, fast_attack) | 61 包、无 refill 溢出 | |
| 上机实时 ch38 / ch39(各 12 s,旧版本) | 25 包 / 24 包,多设备(含 type 0x7) | |
| 宿主机回放速度 | ~1.9x 实时 | |
| 合成回归 | ≥10 dB 出包、≤5 dB 不出(dB 是总带内 SNR,等效带内约 +20 dB) |
改动解调器后的验收顺序:合成回归 → 三段录波回放 → 上机 16 s。任一明显低于上表就是回退。
已知局限
- 单信道:一次只收一个广播信道,只能拿到该信道约 1/3 的广播包。
要扫遍周围设备需加 37→38→39 跳频;换频时解调器状态要复位 (ble_phy.c 里已有 static 的 ble_reset_state(),跳频场景需要开放一个重置入口)。
- ADV 解析目前只输出 PDU 类型 + AdvA;名称/厂商数据等 AD 结构还没解析。
- AA 门限是硬判决(40 bit 里允许 ≤3 错)。若要再压灵敏度,可考虑 AA 软判决相关
(参考 tools/aa_scan.py 的归一化相关度),代价是误报要靠 CRC 兜。
Security
- Security scanPassed, 90 of 100Pattern checks on the skill's files.
- Model reviewPassed
Quality evaluation
- Task evaluationNot runNo evaluation yet. Run one in Skill Studio.
Source
- SKILL.md in the repositoryCheckingPlutoSDR专项/00-examples/06-pluto_ble_adv_rx/skills/pluto-ble-adv-rx/SKILL.md
Popularity
- Repository stars
- 201
- Repository forks
- 45
- 7-day trend score
- 0
Stars and forks are for the whole GitHub repository, not this skill alone. Not refreshed yet.
Works with
Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.
- Claude Code
- Cursor
- GitHub Copilot
- Windsurf
- Codex
- Gemini CLI
- Cline
- Amp
- Roo Code
- Goose
- Continue
- Kilo Code
- Junie
- Trae
- OpenHands
- Qwen Code
- AI Agent
- Tabnine Chat
- Sourcegraph Cody
- Amazon Q
- Firebase Genkit
- Sweep
- Devin
- Phind
- Mentat
- Claude Desktop
- Aider
- GPT Pilot
- Plandex
- bolt.new
- v0
- Replit Agent
- Anse
- TypingMind
- BetterChatGPT
- LibreChat
- Warp AI
- Pieces
- Fig AI
Embed badge
[](https://verified-skill.com/skills/btfz-sdr/gnuradio_demo/pluto-ble-adv-rx)