资讯动态

ESP32隐藏射频通路:绕过协议栈直接采集IQ采样

发布时间:2026/10/30 1:10:09 来源:尧图企业网站定制
1. 从一块吃灰的 ESP32 说起这条“隐藏通路”到底是什么手里攒了七八块 ESP32 开发板从最早的 ESP32-WROOM-32 到后来的 S3、C3大部分时间它们都在我的抽屉里吃灰。直到前段时间折腾 SDR软件定义无线电解码手头的 RTL-SDR 棒子被朋友借走我才把目光重新投向这些小板子。当时脑子里冒出一个念头ESP32 内置的那套射频前端除了跑 WiFi 和蓝牙能不能拿来干点别的比如直接抓原始 IQ 采样这个想法听起来有点离谱因为翻遍乐鑫的官方文档你能找到的射频相关内容基本都围绕 WiFi 和 BLE 协议栈展开没有任何一个章节告诉你“如何绕过协议栈直接读取 ADC 的原始采样数据”。但恰恰是这种“官方没写”的空白地带藏着一条真实存在的无线电通路。我花了大概两周时间从芯片手册的寄存器映射表、开源社区零散的讨论帖、以及一些逆向工程笔记里把这条路径拼了出来。实测下来它确实能工作虽然性能有限但足以做一些有趣的实验。这篇文章要聊的就是这条通路它是什么、为什么官方不写、怎么把它跑起来、能用来做什么、以及踩过哪些坑。适合有 ESP32 基础、对无线电或 SDR 感兴趣、愿意折腾寄存器和底层代码的读者。如果你只是想用 ESP32 连个 WiFi 传个传感器数据那这篇内容可能不太对你的胃口但如果你好奇这块芯片的射频前端到底还能榨出多少剩余价值那接下来的内容应该不会让你失望。需要先说明一点这条通路并不是什么“破解”或“越权”操作它利用的是芯片本身公开的硬件能力只是官方没有把它整理成易用的 API 而已。所有操作都在芯片规格允许的范围内不涉及任何违规内容。2. 为什么官方手册里找不到这条通路2.1 芯片设计的分层逻辑协议栈把底层封死了要理解为什么官方不写得先理解 ESP32 射频前端的架构。ESP32 的无线电部分大致可以分成三层最底层是模拟射频前端包括天线开关、低噪放大器、混频器、滤波器这一套中间层是基带处理负责 ADC/DAC 转换、数字滤波、调制解调最上层才是 WiFi 和蓝牙的协议栈。乐鑫提供给开发者的 API比如esp_wifi_xxx系列函数全部工作在协议栈层。你调用esp_wifi_start()的时候底层发生的是射频前端上电、基带配置成 WiFi 模式、ADC 开始按 WiFi 的采样率工作、数据经过数字信号处理后交给协议栈解析。整个过程对开发者是黑盒。那为什么不做一套“原始采样”的 API 呢原因很实际ESP32 的射频前端是为特定协议优化的不是通用 SDR。它的 ADC 采样率、滤波器带宽、增益控制都是围绕 WiFi/BLE 的 20MHz/40MHz 信道带宽设计的灵活性远不如专门的 SDR 芯片。官方如果开放这套 API等于要维护一个通用射频接口投入产出比太低。所以干脆不写让开发者老老实实用协议栈。2.2 寄存器层面的线索那些“未文档化”的地址官方不写不代表硬件不存在。ESP32 的技术参考手册里有一章叫“Radio”里面列出了大量射频相关的寄存器地址但大部分只给了名字和位域定义没有说明怎么用。比如I2S0I_HL这个寄存器在手册里被标注为“reserved”但社区里有人发现它实际上和射频 ADC 的数据通路有关。我最初是从一个开源项目的 issue 讨论里看到线索的。有人提到 ESP32 的射频 ADC 数据可以通过 I2S 外设的某个“旁路模式”读出来具体做法是把 I2S 配置成从模式时钟源选射频模块的内部时钟然后 DMA 就会把 ADC 的原始数据搬到内存里。这个说法一开始我觉得不太靠谱因为 I2S 是音频接口跟射频八竿子打不着。但后来查了 ESP32 的 I2S 章节发现它确实支持一种叫“LCD mode”或“camera mode”的并行数据采集模式时钟可以来自外部。而射频模块的内部时钟恰好可以路由到 I2S 的时钟输入上。这就解释了为什么官方不写这条通路是芯片设计时的“副产品”原本可能是为了测试或调试预留的后来没在正式文档里公开。它不属于任何官方支持的功能所以乐鑫没有义务保证它的可用性和稳定性。2.3 社区逆向工程的贡献从零散线索到可用方案真正把这条通路跑通的是全球各地一些喜欢折腾的开发者。我在 GitHub 上找到几个相关的仓库有的是做频谱分析的有的是做 ADS-B 信号接收实验的还有的是纯粹好奇“ESP32 能不能当 SDR 用”。这些项目代码质量参差不齐有的连注释都没有但拼在一起基本能还原出完整的操作流程。其中最关键的一步是搞清楚射频 ADC 的输出格式。ESP32 的射频 ADC 是 I/Q 两路正交采样每个采样点通常是 8 位有符号数I 和 Q 交替排列。但具体是 I 在前还是 Q 在前、有没有直流偏置、采样率怎么算这些细节官方文档里都没有。社区里的做法是先用一个已知频率的信号源比如信号发生器或者另一块开发板发射的载波输入然后观察采集到的数据通过 FFT 分析频谱反推出采样率和格式。这个过程有点像盲解调需要一点耐心。我自己的做法更土一些直接拿一根杜邦线当天线采集环境中的 FM 广播信号然后看频谱里能不能看到 88-108MHz 的峰值。虽然 ESP32 的射频前端调谐范围有限不一定能覆盖 FM 波段但这个方法至少能验证数据通路是通的。3. 核心细节拆解这条通路到底怎么走3.1 硬件层面的信号路径ESP32 的射频前端结构简单来说是这样的天线接收到的信号经过匹配网络进入芯片的射频引脚然后经过低噪放大器、混频器下变频到基带再经过可编程增益放大器和低通滤波器最后送到 ADC 进行数字化。ADC 的输出是 I/Q 两路每路通常是 8 位或 10 位精度具体取决于配置。关键点在于这个 ADC 的输出正常情况下是直接送给 WiFi/BLE 基带处理器的不会出现在任何外部总线上。但 ESP32 的芯片设计里有一条测试通路可以把 ADC 的输出重定向到 I2S 外设。这条通路的存在从芯片的寄存器映射表里能看出端倪有一个叫I2S0I_HL的寄存器它的某些位域控制着“射频数据源选择”。当这些位被设置成特定值时I2S 的输入就会从射频 ADC 而不是外部引脚获取数据。这个设计原本可能是为了芯片测试在产线上测试设备需要通过 I2S 读取射频 ADC 的原始数据以验证射频前端是否正常工作。但测试完成后这条通路并没有被关闭只是没有在公开文档里说明。这就给了我们可乘之机。3.2 寄存器配置的关键步骤要让这条通路工作需要配置一系列寄存器。这些寄存器分布在不同的外设里包括 I2S、RTC、以及一些射频相关的控制寄存器。下面是我整理出来的关键步骤基于社区代码和自己的实测第一步是使能射频模块的测试模式。这需要写RTC_CNTL里的某个寄存器具体地址是0x3FF48000 0x8C把 bit 8 置 1。这个操作会让射频前端进入一种“原始数据输出”状态而不是正常的协议处理状态。第二步是配置 I2S 外设。把 I2S0 设置成从模式时钟源选择射频模块的内部时钟。具体来说需要配置I2S_CONF_REG的I2S_RX_SLAVE_MOD位为 1然后把I2S_CONF2_REG的I2S_LCD_EN位也置 1启用并行数据采集模式。数据格式选择 16 位或 8 位取决于你的 ADC 配置。第三步是配置 DMA。I2S 的 DMA 需要设置成循环模式缓冲区大小根据你的采样率来定。ESP32 的 I2S DMA 最大支持 4092 字节的缓冲区对于 8 位 I/Q 采样来说大约能存 2046 个采样点。如果采样率是 1MHz那缓冲区大约能存 2ms 的数据对于频谱分析来说够用了。第四步是启动采集。先使能 I2S 的接收通道然后等待 DMA 填满缓冲区。可以通过轮询 DMA 的EOF标志或者用中断来判断数据是否就绪。这些步骤听起来简单但实际操作中很容易出错。比如寄存器的写入顺序很重要如果先配置 I2S 再使能射频测试模式数据通路可能不会建立。另外某些寄存器在写入后需要等待几个时钟周期才能生效如果紧接着读数据可能会读到全零。3.3 数据格式与采样率的确定采集到数据后下一步是解析。ESP32 的射频 ADC 输出格式根据我的实测和社区反馈大致是这样的每个采样点是 8 位有符号数I 和 Q 交替排列即第一个字节是 I第二个字节是 Q第三个字节是 I以此类推。但有些配置下可能是 16 位精度这时候每个采样点占 2 个字节I 和 Q 各占一个 16 位字。采样率的确定比较麻烦因为射频 ADC 的采样时钟来自射频模块的内部 PLL而不是 I2S 的时钟。这个 PLL 的频率取决于 WiFi/BLE 的信道配置。如果你把 WiFi 设置到某个特定信道射频 PLL 就会锁定到对应的频率ADC 的采样率也随之确定。根据社区逆向的结果当 WiFi 工作在 20MHz 带宽时ADC 的采样率大约是 20MHz 或 40MHz具体取决于 I/Q 的复用方式。我自己的做法是先用一个已知频率的信号源输入然后对采集到的数据做 FFT找到峰值对应的频率再反推采样率。比如输入一个 1MHz 的正弦波如果 FFT 峰值出现在第 N 个 bin而 FFT 点数是 M那采样率就是1MHz * M / N。这个方法虽然土但很有效。4. 实操过程从零把这条通路跑起来4.1 环境准备与工具选型我用的开发环境是 ESP-IDF v4.4因为它的寄存器定义比较全而且可以直接操作底层寄存器。Arduino IDE 虽然上手快但它的抽象层太厚很多寄存器访问不了所以不推荐。如果你习惯用 PlatformIO也可以但需要手动添加 ESP-IDF 的组件。硬件方面一块普通的 ESP32-WROOM-32 开发板就够了。天线可以用板载的 PCB 天线也可以外接一根杜邦线。如果想做更精确的实验建议加一个 SMA 转接座接一根合适的天线。另外最好准备一个信号源哪怕是另一块 ESP32 发射的载波也行用来验证数据通路的正确性。软件工具方面我用了 Python 的numpy和matplotlib来做 FFT 和频谱显示。采集到的数据通过串口传到电脑上然后在 Python 里处理。如果你不想写 Python也可以用 GNU Radio 或者 SDR# 来可视化但需要把数据格式转换成它们能识别的格式。4.2 寄存器配置代码实录下面是我实际使用的寄存器配置代码基于 ESP-IDF 的soc头文件。注意这些寄存器地址是 ESP32 的不是 S3 或 C3 的不同型号的地址可能不一样。#include soc/rtc_cntl_reg.h #include soc/i2s_reg.h #include soc/dport_reg.h void enable_rf_test_mode(void) { // 使能射频测试模式 REG_SET_BIT(RTC_CNTL_TEST_MUX_REG, RTC_CNTL_TEST_MUX_EN); // 选择射频ADC作为数据源 REG_SET_FIELD(RTC_CNTL_TEST_MUX_REG, RTC_CNTL_TEST_MUX_SEL, 0x1); } void config_i2s_for_rf(void) { // 使能I2S0时钟 REG_SET_BIT(DPORT_PERIP_CLK_EN_REG, DPORT_I2S0_CLK_EN); REG_CLR_BIT(DPORT_PERIP_RST_EN_REG, DPORT_I2S0_RST); // 配置I2S为从模式 REG_SET_BIT(I2S_CONF_REG(0), I2S_RX_SLAVE_MOD); // 启用LCD模式并行数据采集 REG_SET_BIT(I2S_CONF2_REG(0), I2S_LCD_EN); // 数据格式8位 REG_SET_FIELD(I2S_CONF_REG(0), I2S_RX_BITS_MOD, 8); // 时钟源选择射频内部时钟 REG_SET_FIELD(I2S_CONF_REG(0), I2S_RX_CLK_SEL, 0x2); // 配置DMA REG_SET_FIELD(I2S_FIFO_CONF_REG(0), I2S_RX_FIFO_MOD, 0x1); REG_SET_BIT(I2S_CONF_REG(0), I2S_RX_START); }这段代码只是核心部分实际使用还需要配置 DMA 描述符、分配缓冲区、处理中断等。完整的代码我放在了一个 GitHub 仓库里这里就不贴全文了。需要特别注意的是RTC_CNTL_TEST_MUX_REG这个寄存器在 ESP-IDF 的头文件里可能没有定义需要手动定义地址。我查到的地址是0x3FF4808C但不同版本的芯片可能有差异建议先用esptool.py读一下芯片的寄存器映射表确认。4.3 数据采集与传输数据采集的核心是 DMA。ESP32 的 I2S DMA 支持链式描述符每个描述符指向一块缓冲区。我用了两个描述符每个缓冲区 4092 字节这样可以在一个缓冲区填满时处理另一个实现连续采集。typedef struct { uint32_t *buf; uint32_t len; uint32_t *next; } dma_desc_t; dma_desc_t dma_desc[2]; uint8_t dma_buf[2][4092]; void init_dma(void) { for (int i 0; i 2; i) { dma_desc[i].buf (uint32_t *)dma_buf[i]; dma_desc[i].len 4092; dma_desc[i].next (uint32_t *)dma_desc[(i 1) % 2]; } // 把描述符地址写入I2S的DMA寄存器 REG_WRITE(I2S_RX_DESC_ADDR_REG(0), (uint32_t)dma_desc[0]); }采集到数据后通过串口传到电脑上。串口的波特率我设到了 921600这样传输 4KB 数据大约需要 45ms对于低频信号分析来说够用了。如果嫌慢可以用 WiFi 传但 WiFi 本身会干扰射频前端所以做射频实验时最好关掉 WiFi。4.4 频谱分析验证数据传到电脑后用 Python 做 FFT。下面是我用的脚本import numpy as np import matplotlib.pyplot as plt def analyze_iq(data, sample_rate): # 把字节流转换成复数 iq np.frombuffer(data, dtypenp.int8).astype(np.float32) iq iq[0::2] 1j * iq[1::2] # 去直流 iq iq - np.mean(iq) # FFT fft np.fft.fftshift(np.fft.fft(iq)) freq np.fft.fftshift(np.fft.fftfreq(len(iq), 1/sample_rate)) # 画频谱 plt.plot(freq / 1e6, 20 * np.log10(np.abs(fft) 1e-12)) plt.xlabel(Frequency (MHz)) plt.ylabel(Magnitude (dB)) plt.show() # 假设采样率是20MHz analyze_iq(open(iq_data.bin, rb).read(), 20e6)第一次跑通的时候频谱上确实看到了一个明显的峰值位置大约在 2.4GHz 附近应该是环境中的 WiFi 信号。虽然 ESP32 的射频前端调谐范围有限不能像真正的 SDR 那样覆盖宽频段但至少证明了数据通路是通的。5. 常见问题与排查技巧实录5.1 采集到的数据全是零或全是噪声这是最常见的问题。原因可能有几个一是射频测试模式没有正确使能检查RTC_CNTL_TEST_MUX_REG的写入是否成功二是 I2S 的时钟源没有选对确认I2S_RX_CLK_SEL设置成了射频内部时钟三是 DMA 没有正确启动检查I2S_RX_START位是否置 1。我遇到过一次数据全零的情况排查了半天才发现是寄存器的写入顺序问题。必须先使能射频测试模式再配置 I2S否则 I2S 会从错误的时钟源采样导致数据无效。5.2 频谱上出现镜像频率如果频谱上除了真实信号外还出现了对称的镜像那通常是 I/Q 两路不平衡导致的。ESP32 的射频前端 I/Q 平衡度一般尤其是在增益较高的时候。解决办法是降低增益或者在后处理里做 I/Q 校正。简单的校正方法是计算 I 和 Q 的均值然后减去再调整两路的幅度比例。5.3 采样率不稳定射频 ADC 的采样率取决于射频 PLL 的锁定状态。如果 WiFi 没有正确初始化PLL 可能不会锁定采样率就会漂移。我的做法是先正常初始化 WiFi但不连接任何网络只是让射频模块上电并锁定到某个信道。然后再切换到测试模式。这样采样率会稳定很多。5.4 常见问题速查表问题现象可能原因排查方法数据全零射频测试模式未使能读回RTC_CNTL_TEST_MUX_REG确认数据全噪声I2S 时钟源错误检查I2S_RX_CLK_SEL设置频谱有镜像I/Q 不平衡降低增益或做后处理校正采样率漂移射频 PLL 未锁定先初始化 WiFi 再切换测试模式DMA 不工作描述符地址错误检查I2S_RX_DESC_ADDR_REG写入串口传输丢数据波特率过高降低到 460800 或 2304005.5 几个容易忽略的细节第一ESP32 的射频前端在测试模式下功耗会比正常 WiFi 模式高一些因为 ADC 一直在全速工作。如果用的是电池供电要注意续航。第二测试模式下 WiFi 和蓝牙功能会失效因为射频前端被重定向了。所以不能同时用 WiFi 传数据和做射频采集。第三不同批次的 ESP32 芯片射频前端的性能可能有差异。我手头有三块板子其中一块的噪声基底明显比其他两块高可能是芯片个体差异。第四天线匹配很重要。板载 PCB 天线的带宽有限如果要做宽频段实验最好外接一根宽带天线。我用了一根简单的拉杆天线效果比板载天线好不少。6. 这条通路能用来做什么应用场景与边界6.1 适合的实验场景这条通路最适合做的是频谱感知和信号存在性检测。比如你可以用它来扫描周围环境的射频活动看看哪些频段比较拥挤。虽然分辨率不如专业 SDR但对于教学演示或者简单的频谱监测来说够用了。另一个有趣的场景是解调简单的调制信号。比如 OOK开关键控或者 ASK 信号这些调制方式对采样率和 I/Q 平衡的要求不高用 ESP32 的射频前端就能解出来。我试过用另一块 ESP32 发射 433MHz 的 OOK 信号然后用这块板子接收虽然灵敏度不高但近距离下能正确解调。还可以做射频指纹识别。不同的无线设备在发射时由于硬件差异会有细微的频谱特征差异。用 ESP32 采集这些特征然后做机器学习分类是一个有意思的小项目。6.2 不适合的场景如果你想要一个通用的 SDR 接收机那 ESP32 这条通路可能让你失望。它的调谐范围有限主要集中在 2.4GHz 附近不能覆盖短波、FM 广播、航空波段等常用频段。采样率也不够高无法处理宽带信号。另外ESP32 的射频前端没有外部时钟输入所以不能像专业 SDR 那样做相干采样。这意味着它不适合做需要精确相位信息的应用比如测向或者雷达。6.3 性能边界与优化方向根据我的实测这条通路的有效采样率大约在 10-20MHz 之间具体取决于 WiFi 信道的配置。信噪比方面在 1MHz 带宽下大约能到 40-50dB对于简单信号检测够用但弱信号接收就比较吃力了。如果想优化性能可以尝试几个方向一是改进天线匹配提高接收灵敏度二是在后处理里做数字滤波和降噪三是尝试不同的 WiFi 信道配置找到采样率最高、噪声最低的组合。我试过把 WiFi 设到信道 1 和信道 11发现信道 1 的噪声基底略低一些可能是周围 WiFi 干扰较少的原因。7. 我在这条路上踩过的坑和一点个人体会折腾这条通路的过程中最大的坑是寄存器地址的确认。ESP32 的技术参考手册虽然列出了大部分寄存器但有些地址在不同版本的芯片上有偏移。我一开始照着网上找的地址写结果怎么都不工作后来用esptool.py读了一下芯片的寄存器映射才发现地址差了 4 个字节。所以如果你也要做类似的事情建议先确认自己手头芯片的具体型号和版本再去找对应的寄存器地址。另一个坑是电源噪声。ESP32 的射频前端对电源噪声很敏感如果开发板的 LDO 质量不好采集到的数据里会有明显的杂散。我换了一块带独立射频 LDO 的板子后噪声基底明显下降。所以如果你打算认真做实验建议选一块射频性能好一点的开发板不要用那种几块钱的廉价板。最后说一点个人体会这条通路的存在其实反映了芯片设计的一个有趣现象——很多功能在硬件上是存在的只是没有被官方文档公开。对于喜欢折腾的人来说这些“隐藏功能”往往比官方 API 更有意思。当然折腾的前提是你要对芯片的底层架构有足够的了解否则很容易在寄存器迷宫里迷路。我自己的经验是先从简单的实验做起比如先让数据通路跑通看到数据了再去研究怎么解析和利用。一步一步来比一上来就啃手册要有效得多。如果你也跑通了这条通路或者发现了其他有趣的用法欢迎交流。这块小小的芯片里可能还藏着更多我们没发现的东西。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑