ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

ESP32做SDR:二十元开发板实现软件定义无线电接收

ESP32做SDR:二十元开发板实现软件定义无线电接收

1. 从一块二十块钱的开发板说起:ESP32做SDR这件事到底靠不靠谱

第一次在群里看到有人甩出一句“ESP32能当SDR用了”,我的反应和大多数人一样——眩晕瘫坐不至于,但确实愣了几秒。ESP32这块芯片,玩嵌入式的人太熟了,二十来块钱一片,双核240MHz,带WiFi和蓝牙,平时干的是读传感器、点灯、驱动屏幕、跑个小车的活儿。SDR是什么?软件定义无线电,传统印象里那是需要高速ADC、FPGA、动辄几百兆采样率的东西,跟一个主打低功耗物联网的MCU怎么看都不像能扯上关系。

但仔细扒了一圈资料、翻了几个开源项目之后,我发现这事没那么玄乎,也没那么简单。说它“能当SDR”,准确的理解应该是:ESP32可以在极其有限的条件下,实现特定频段、特定带宽的无线电信号接收与简单解调,而不是替代一台RTL-SDR或者Airspy。这个区别非常关键,直接决定了你对这个项目的预期,也决定了你会不会花了两天时间最后骂骂咧咧地放弃。

这篇文章适合几类人看:手里有ESP32开发板、对无线电感兴趣但没接触过SDR的嵌入式玩家;玩过RTL-SDR、想看看MCU能做到什么程度的无线电爱好者;以及单纯好奇“这玩意儿到底怎么实现的”的技术围观群众。我会从原理层面讲清楚为什么ESP32能做这件事、它的物理极限在哪里,然后给出可复现的实操路径,最后把我踩过的坑和排查经验整理出来。不管你是刚点亮第一块ESP32的新手,还是已经用ESP-IDF写过不少项目的老手,应该都能从中找到有用的东西。

2. 核心原理拆解:ESP32凭什么能碰SDR的边

2.1 先搞清楚SDR到底在干什么

要理解ESP32能不能做SDR,得先弄明白SDR的本质。传统无线电接收机的信号链路是硬件搭出来的:天线收到信号,经过滤波、放大、混频、检波,最后出来音频或者数据。每一级都是固定的电路,想换个频段就得换硬件。SDR的思路是把尽可能多的处理搬到软件里:天线信号经过最少的模拟前端(通常就是一个宽带放大器加一个高速ADC),数字化之后,滤波、混频、解调全部用代码算出来。

这里面的核心瓶颈是ADC。根据奈奎斯特采样定理,你要接收中心频率为f、带宽为B的信号,ADC的采样率至少要到2B以上(实际中还要考虑过采样和抗混叠)。FM广播带宽约200kHz,那采样率至少400ksps;航空波段AM信号带宽窄一些,大概几十ksps就够。所以SDR对ADC的要求,取决于你想接收什么信号。

RTL-SDR之所以便宜好用,是因为它用了R820T2这颗调谐芯片,把射频信号先降到中频,再用一颗8bit、2.4Msps的ADC采样,通过USB把IQ数据送到电脑上处理。关键点在于:真正的高速采样和下变频是硬件芯片干的,电脑只负责后面的数字信号处理。

2.2 ESP32的硬件底子到底够不够

ESP32的射频部分本来是为WiFi和蓝牙设计的,支持2.4GHz频段,内部有射频收发链路。但问题在于,这条链路是封闭的,你没法直接拿到原始IQ数据。ESP32的ADC是SAR型,标称12位,实际有效位数大概9到10位,采样率理论上最高2Msps,但这是在特定条件下的极限值,实际用起来受WiFi蓝牙共存、DMA带宽、CPU负载影响很大。

那为什么还有人做出来了?关键在于思路的转变:不去碰2.4GHz的WiFi射频链路,而是用ESP32的ADC直接采样外部输入的模拟信号。也就是说,你需要外接一个简单的模拟前端——一根天线、一个合适的带通滤波器、可能再加一级LNA——把信号送到ESP32的ADC引脚上。ESP32负责的是采样和后续的数字信号处理,射频前端的工作交给了外部电路。

这个方案的本质是:ESP32充当了一个低速ADC加DSP处理器的角色。它的采样率决定了能处理的信号带宽,它的算力决定了能跑多复杂的解调算法。根据社区里几个开源项目的实测数据,ESP32在稳定工作的情况下,ADC采样率大概能跑到1Msps左右,对应理论带宽500kHz。实际上考虑到抗混叠和算法开销,能有效处理的带宽大概在100到200kHz之间。

2.3 为什么这件事值得折腾

你可能会问:花这么大劲,就为了接收一两百kHz带宽的信号,有什么意义?直接买个RTL-SDR不好吗?

这个问题我认真想过。从纯功能角度,ESP32做SDR确实打不过任何一款入门级SDR接收机。但它有几个独特的价值点:第一,它是一个完全独立的嵌入式方案,不需要连接电脑,可以做成一个巴掌大的独立接收设备,配上屏幕和电池就能户外使用;第二,它的成本极低,一块ESP32加上几个分立元件,总成本可能不到三十块钱;第三,也是最重要的一点,它是一个极好的学习平台。当你亲手用ADC采样、用代码实现FFT、用算法解调出第一个信号的时候,你对SDR的理解会发生质的变化,这是插上RTL-SDR点开SDR#软件永远得不到的体验。

注意:ESP32做SDR接收,接收频段取决于你外接的模拟前端。ESP32本身不负责调谐,它只是采样。你想接收什么频段,就需要对应的前端电路把信号搬移到ADC能处理的范围内。

3. 实操前的准备工作:硬件选型和软件环境搭建

3.1 硬件清单与选型逻辑

动手之前先把东西备齐。我列一个最小系统的清单,然后解释每一项为什么这么选。

组件推荐型号选型理由大致成本
主控板ESP32-WROOM-32最通用的型号,社区资料多,ADC引脚引出方便15-25元
天线拉杆天线或自制偶极子根据目标频段裁剪长度,短波用长线,FM用半波振子5-20元
带通滤波器自制LC或成品滤除目标频段外的强信号,防止ADC饱和5-15元
低噪放可选,MMIC如BGA2801弱信号时提升信噪比,强信号时不需要3-10元
偏置器自制给LNA供电,同时隔离射频和直流2-5元
显示屏SSD1306 OLED 128x64显示频谱和解调信息,I2C接口省引脚8-15元
连接线杜邦线、SMA转接线射频连接尽量短,减少损耗和干扰若干

关于ESP32型号的选择,我建议用ESP32-WROOM-32或者ESP32-S3。WROOM-32的好处是资料最全,ADC引脚在GPIO32到GPIO39之间,其中GPIO34到GPIO39是输入-only,适合做ADC输入。S3的话算力更强,有向量指令可以加速FFT,但价格稍贵,资料相对少一些。如果你是第一次做这个项目,从WROOM-32开始最稳妥。

天线这块要重点说一下。很多人随便拿一根杜邦线就当天线用,结果什么信号都收不到,然后怀疑是代码问题。天线的长度和形式直接决定了你能否有效接收目标频段的信号。一个简单的计算公式:半波振子长度(米)等于150除以频率(MHz)。比如你想接收100MHz附近的FM广播,半波振子大概1.5米;接收短波7MHz,那就要20米左右了。实际中受空间限制,可以用四分之一波长或者加感线圈来缩短。

3.2 软件环境搭建:从Arduino IDE到ESP-IDF

软件环境这块,我推荐两条路线,根据你的经验水平来选。

如果你之前主要用Arduino IDE开发ESP32,那继续用Arduino框架是最快上手的路径。需要安装的东西:Arduino IDE 2.x版本、ESP32开发板支持包(建议用2.0.11或2.0.14版本,稳定性好)、以及必要的库。安装ESP32支持包的时候,国内网络环境下载可能比较慢,可以找离线包手动安装。具体操作是在Arduino IDE的“首选项”里填入开发板管理器地址,然后在开发板管理器里搜索esp32安装。如果下载速度实在受不了,去社区找对应版本的离线包,解压到Arduino的hardware目录下也能用。

如果你追求性能和底层控制,那就上ESP-IDF。ESP-IDF是乐鑫官方的开发框架,对ADC、DMA、I2S的控制更直接,做高速采样的时候优势明显。安装ESP-IDF推荐用VSCode加ESP-IDF插件的方式,比纯命令行友好很多。安装过程中会下载工具链和Python环境,同样可能遇到网络慢的问题,可以配置国内镜像源加速。安装完成之后,用idf.py create-project创建一个新项目,就可以开始了。

提示:不管用哪个框架,都建议先把一个最简单的ADC采样程序跑通,用串口打印原始数据,确认硬件连接和基础功能没问题,再往上叠加SDR相关的代码。这样出问题的时候排查范围小很多。

3.3 采样率与时钟配置的关键参数

这是整个项目最核心的技术点之一。ESP32的ADC时钟源来自APB时钟,默认80MHz,通过分频器产生ADC的工作时钟。在Arduino框架下,analogRead()函数的采样率大概只有几十ksps,远远不够。要跑到1Msps左右,必须用底层寄存器操作或者I2S DMA的方式。

用I2S DMA采ADC数据是社区里比较成熟的方案。原理是把ADC配置成连续采样模式,通过I2S外设的DMA通道把数据搬运到内存缓冲区,CPU只需要处理缓冲区里的数据,不用管采样时序。这样可以把采样率做到比较高,同时CPU还有余力做信号处理。

具体的配置参数:I2S采样率设置为1MHz,DMA缓冲区大小设为1024或2048个采样点,双缓冲模式。ADC衰减设为11dB(对应输入范围0到3.3V左右),分辨率设为12位。实际有效位数大概9到10位,所以信噪比不会太好,但对于接收较强的广播信号来说够用了。

这里有个坑要注意:ESP32的ADC在WiFi开启的时候噪声会明显增大。如果你不需要WiFi功能,在采样期间把WiFi关掉,信噪比会好很多。如果确实需要WiFi,尽量把采样和WiFi传输在时间上错开。

4. 从采样到解调:完整信号链的实现过程

4.1 模拟前端电路的设计与搭建

ESP32的ADC输入范围是0到3.3V(衰减11dB时),而且只能接受正电压。射频信号是交流信号,有正有负,所以需要加一个直流偏置电路,把信号抬升到1.65V左右,让交流信号在1.65V上下摆动,这样ADC才能正确采样。

最基本的偏置电路用两个等值电阻分压产生1.65V,然后通过一个隔直电容把射频信号耦合进来。但这样做的缺点是输入阻抗不够稳定,而且没有滤波。更好的做法是用一个运算放大器搭一个偏置加法电路,同时兼做低通抗混叠滤波。运放选带宽足够的型号,比如NE5532或者OPA2350,增益带宽积至少要到几十MHz。

带通滤波器根据你的目标频段来设计。以接收FM广播(88到108MHz)为例,你需要一个中心频率98MHz、带宽20MHz左右的带通滤波器。可以用LC集总参数做,但98MHz的频率下电感电容的值很小,手工焊接比较困难。更实际的做法是用一个成品FM带通滤波器,或者用传输线结构的滤波器。如果目标频段更低,比如短波或者中波,LC滤波器的实现就容易得多。

低噪放这块,如果信号比较强(比如本地FM广播),可以不加。如果信号弱,加一级MMIC放大器会有明显改善。注意LNA的增益不要太高,否则强信号会让ADC饱和,产生严重的互调失真。我一般会加一个可调衰减器或者用PIN二极管做AGC,但这就复杂了,新手可以先不加,根据实际接收效果再决定。

4.2 采样数据的预处理:去偏置与数字滤波

ADC采到的原始数据是0到4095之间的整数,对应0到3.3V。信号的中心在1.65V左右,对应ADC读数大概2048。第一步预处理就是把直流偏置去掉,把数据变成以0为中心的有符号数。这个操作很简单,每个采样值减去2048就行。

接下来是数字滤波。虽然模拟前端已经有抗混叠滤波,但ESP32的ADC采样率是1MHz,而实际有用的信号带宽可能只有100kHz,所以还需要数字低通滤波来进一步抑制带外噪声。在ESP32上跑FIR滤波是可行的,但要注意算力开销。一个64阶的FIR滤波器,在240MHz主频下,处理1Msps的采样流大概占用不到10%的CPU。如果阶数太高,比如256阶,那CPU占用就会很明显了。

更高效的做法是用CIC滤波器或者简单的滑动平均。滑动平均本质上是一个FIR滤波器,系数全为1,实现起来只需要加减法,不需要乘法,速度极快。比如16点滑动平均,可以把采样率从1MHz降到62.5kHz,同时滤除高频噪声。降采样之后的数据量小了,后续FFT和解调的负担也轻了。

实操心得:预处理阶段的数据类型选择很重要。用float做运算方便但慢,用int16_t做定点运算快但要注意溢出。我建议在滤波和降采样阶段用int32_t累加,输出用int16_t,FFT阶段再转float。这样兼顾速度和精度。

4.3 FFT频谱分析与信号识别

有了预处理后的IQ数据(实际上ESP32做的是实数采样,严格来说只有I路,没有Q路,但可以通过希尔伯特变换或者直接对实数做FFT来获取频谱),下一步就是做FFT看频谱。ESP32上跑FFT,推荐用乐鑫官方提供的esp-dsp库,里面有针对ESP32优化的FFT函数,支持实数FFT和复数FFT,速度比手写的快很多。

FFT的点数选择要权衡频率分辨率和时间分辨率。1024点FFT,在62.5kHz采样率下,频率分辨率是61Hz,时间窗口约16ms。对于看FM广播的频谱来说,这个分辨率足够了。2048点的话分辨率翻倍,但计算量也翻倍。我一般用1024点,在ESP32上跑一次大概几百微秒,完全跟得上实时显示。

频谱出来之后,你可以通过找峰值来确定信号的中心频率。比如在88到108MHz范围内(经过下变频后对应到基带的0到20MHz,再经过采样和降采样后对应到0到31.25kHz),找到幅度最大的几个峰,那就是电台信号。然后根据峰值的位置,把信号搬移到零频附近,做进一步解调。

4.4 FM解调的实现细节

FM解调的核心是求瞬时频率。对于实数采样信号,可以先做希尔伯特变换得到解析信号,然后求相邻采样点的相位差,相位差就对应瞬时频率。另一种更简单的方法是对信号做反正切鉴频:把信号延迟一个采样周期,然后计算原信号和延迟信号的叉积与点积,叉积除以点积的反正切就是相位差。

在ESP32上实现FM解调,我推荐用第二种方法,因为计算量小,不需要做希尔伯特变换。具体步骤是:对预处理后的信号x[n],计算x[n]和x[n-1]的叉积cross等于x[n]乘以x[n-1]的共轭的虚部(对于实数信号,cross等于x[n]乘以x[n-1]的延迟版本,这里需要先构造解析信号或者用近似方法),点积dot等于x[n]乘以x[n-1]的实部。然后相位差等于atan2(cross, dot)。这个相位差就正比于瞬时频率偏移,也就是解调出来的音频信号。

解调出来的音频信号还需要经过低通滤波和去加重。去加重是FM广播的标准处理,时间常数一般是75微秒(美国)或50微秒(欧洲)。去加重的实现就是一个简单的一阶高通滤波器,用代码几行就能搞定。最后把音频信号通过DAC输出或者通过I2S送到外部DAC芯片,就能听到声音了。

ESP32自带两个8位DAC引脚(GPIO25和GPIO26),可以直接输出音频。但8位分辨率对于音频来说有点低,会有明显的量化噪声。更好的方案是用I2S接一个外部DAC,比如MAX98357或者PCM5102,音质会好很多。如果只是听个响,内置DAC也凑合能用。

5. 常见问题与排查技巧实录

5.1 采样数据全是噪声怎么办

这是最常见的问题,十有八九是模拟前端的问题。排查步骤按顺序来:先用示波器或者万用表确认ADC输入引脚的直流电平是不是在1.65V左右。如果偏差太大,检查偏置电路的分压电阻是不是焊错了或者虚焊。然后确认天线有没有接好,天线和ADC输入之间的耦合电容是不是合适。如果这些都正常,那可能是信号太弱,需要加LNA或者换更好的天线。

还有一个容易被忽略的点:ESP32开发板上的ADC引脚可能被其他功能占用了。比如GPIO34在某些板子上接了摄像头或者SD卡。用之前查一下开发板的原理图,确认引脚是空闲的。另外,ADC2的引脚在WiFi开启时是不能用的,只能用ADC1的引脚(GPIO32到GPIO39)。

5.2 频谱显示有信号但解调不出声音

这种情况通常是解调参数不对。首先确认你解调的信号确实是FM信号,而不是AM或者数字信号。FM广播的信号带宽大概200kHz,在频谱上看起来是一个比较宽的峰,而AM信号是一个窄峰。如果确认是FM,检查鉴频器的参数:延迟采样点数是不是1,atan2的计算是不是正确,去加重的截止频率是不是设对了。

另一个可能的原因是采样率不够。如果ADC实际采样率只有500kHz,那能处理的信号带宽只有250kHz,对于200kHz带宽的FM信号来说勉强够用,但边缘可能会失真。可以试着降低目标信号的带宽要求,或者提高采样率。

5.3 编译速度慢和烧录失败的处理

用Arduino IDE编译ESP32项目,尤其是带WiFi和蓝牙库的项目,编译时间可能长达几分钟。加快编译速度的方法有几个:关闭不需要的库,用预编译的库文件,升级电脑的CPU和内存(这个最直接但成本高),或者换用PlatformIO。PlatformIO的编译缓存机制比Arduino IDE好很多,第二次编译同样的项目会快很多。

烧录失败的话,先检查USB线是不是数据线(有些线只能充电不能传数据),然后确认开发板驱动装好了。ESP32烧录的时候需要进入下载模式,有些板子自动下载电路做得不好,需要手动按住BOOT键再按RESET键。如果串口一直显示“等待上电同步”,大概率是这个问题。

问题现象可能原因排查方法解决方案
采样全是噪声偏置电压不对万用表测ADC引脚电压调整分压电阻,确认在1.65V
频谱无信号峰天线未接好检查天线连接和长度重新裁剪天线,确保接触良好
解调无声音鉴频参数错误检查延迟和atan2计算调整延迟点数,验证去加重
编译超时库太多查看编译日志移除无用库,换PlatformIO
烧录失败未进入下载模式观察串口输出手动按住BOOT再复位
WiFi开启后噪声大ADC2与WiFi冲突确认使用的ADC通道换用ADC1引脚,或关闭WiFi

5.4 提升接收效果的几个实用技巧

第一个技巧是给ADC输入加一个简单的RC低通滤波器,截止频率设在采样率的一半以下。比如采样率1MHz,RC截止频率设500kHz。这个滤波器可以滤除高频噪声,改善信噪比。电阻选1kΩ,电容选300pF左右。

第二个技巧是用多个ADC引脚做时间交织采样。ESP32有两个ADC,理论上可以交替采样把等效采样率翻倍。但实际中两个ADC之间的一致性不好,需要校准,实现起来比较复杂。新手不建议尝试。

第三个技巧是在FFT之前加窗函数。不加窗的话,频谱泄漏会比较严重,弱信号可能被强信号的旁瓣淹没。汉宁窗或者汉明窗都是不错的选择,计算量也不大。esp-dsp库里有现成的窗函数可以调用。

6. 这个项目的边界在哪里:理性看待ESP32 SDR的能力

折腾了这么久,我对ESP32做SDR的能力边界有了比较清晰的认识。它能做的事情:接收本地强信号的FM广播、接收航空波段AM信号、接收气象卫星的APT信号(需要合适的天线和前端)、做简单的频谱监测。它不能做的事情:接收弱信号、处理宽带信号、替代专业SDR接收机、做高精度的信号分析。

如果你追求的是实用性,想稳定接收各种信号,那还是买一台RTL-SDR或者更好的接收机。但如果你追求的是学习价值,想真正理解SDR的工作原理,那ESP32这个平台能给你的东西远超它的价格。当你第一次从自己焊的电路上、从自己写的代码里,听到电台主持人的声音时,那种成就感是插上成品设备永远体会不到的。

最后分享一个我在调试过程中总结的小经验:每次修改代码之后,先用一个已知的信号源(比如信号发生器或者另一个ESP32发射的测试信号)验证接收链路是否正常,再去接收真实信号。这样可以快速区分是代码问题还是信号问题,省去很多瞎猜的时间。另外,把关键的中间数据通过串口打印出来,比如ADC原始值、滤波后的值、FFT的峰值位置,比盯着屏幕上的最终结果瞎调要高效得多。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进