拿到ESP32这块小板子的人,第一反应基本都是:一颗能跑WiFi和蓝牙的物联网芯片。我一开始也是这么用的,直到某天在一个硬件逆向工程社区里看到有人晒波形,说用ESP32收到了本地调频广播电台的节目,才意识到手里这块芯片,远比纸面参数上写的要野得多。那条通路,官方参考手册里确实没有专门写过,但在射频链路底层,它客观存在,而且完全可用。
这篇文章不是讲怎样给ESP32外接一个收音机模块,也不是让你买一块软件无线电板卡。它是把ESP32那颗WiFi射频前端,硬生生改造成一条能解调FM广播的无线电通路,让88到108MHz的调频信号绕开所有专用芯片,直接落进这颗物联网芯片的数字域里。整个过程会涉及射频前端结构、正交采样、数字鉴频和立体声解码,也会给出一套可以动手复现的工程框架。适合手里正好有ESP32开发板、又对无线电接收和信号处理感兴趣的嵌入式玩家。如果你只想要一个能出声的FM收音机,某宝十块钱的模块更省事;但如果你想弄明白"为什么一颗WiFi芯片能收到FM广播",这篇文章就是为你写的。
1. 一条被官方手册"遗漏"的射频通路
1.1 它到底是什么——把WiFi前端当宽频接收机用
要理解这条隐蔽通路,先得换个角度看待WiFi收发机。ESP32的射频部分,本质上就是一个完整的零中频接收机:天线进来的信号,先经过低噪声放大器,再和本振信号做正交混频,得到I/Q两路基带信号,之后经过可编程增益放大和低通滤波,最后才进入ADC变成数字量。
这套链路的设计目标是接收2.4GHz频段的WiFi信号,芯片原厂在做射频校准时,也只会针对2.4GHz频段去优化灵敏度、镜像抑制和带内平坦度。但问题来了:接收链路本身并不知道自己"应该"只工作在2.4GHz。真正决定接收频率的,是锁相环给混频器提供的本振频率。只要这个本振能被配置到88到108MHz,混频器就能把FM广播信号搬到基带。射频前端剩余的工作,和接收一个2.4GHz WiFi包的信号路径没有本质区别。
这就是"官方没写进手册"的原因所在。手册里对WiFi射频的描述,全部围绕2.4GHz收发功能展开,寄存器级别的说明也只会给WiFi/BT服务。但在硬件层面,本振的覆盖范围远远超出了标称频段,低通滤波器的带宽也足以容纳FM广播信号。于是一部分工程师通过寄存器逆向的方式,绕开官方接口,直接把本振频率拨到FM频段,让这条"超纲"的通路暴露出来。严格来说,它不是芯片原厂设计时准备的隐藏功能,而是射频链路天然具备的能力被社区发掘了出来。
1.2 为什么值得折腾——相比外接收音机模块的优势
最直接的价值,是省钱和省一块芯片。一片两三块钱的ESP32,加上一根十几厘米的导线,就能完成FM广播接收加音频解码的全部工作。外接方案通常要再加一颗收音机芯片、一个I2S音频解码器,不但增加了物料成本,还多了两颗芯片之间的调试工作。而这条隐藏通路走的是芯片内部链路,射频信号从天线进来,经过混频、采样、数字解调,最终从板载DAC或者I2S接口输出音频,全过程不需要任何外部射频器件。
第二个优势是高度可编程。传统收音机芯片的解调参数大多是芯片内部固定好的,你能控制的只有频率、音量之类的高层参数。而这条通路把原始I/Q采样数据交到了你的代码里,鉴频带宽、去加重时间常数、立体声解码策略,全都由软件决定。甚至你可以把基带I/Q数据直接通过串口丢给电脑,在通用信号分析软件里看频谱,这本质就是一个简易软件无线电前端。
第三个价值,是在学习层面。这套链路把教科书里"超外差接收""正交解调""锁相环""数字鉴频"这些概念,全部压缩在一颗实际工作的芯片里。当你亲耳听到扬声器里传来的电台声音,再回头看那些公式,理解深度是完全不一样的。
2. 拆开ESP32的射频前端:看清这条隐蔽通路
2.1 WiFi接收链路的工作模式与超范围本振
要操作这条通路,先得理解ESP32射频接收的结构。整个接收链路可以简化为五个环节:天线接口、低噪声放大器、正交混频器、可编程增益放大器、低通滤波器。
第一环节负责把空间中的电磁波转化为微弱电压信号;低噪声放大器把这个电压信号放大,同时尽量不引入额外噪声;正交混频器则是核心中的核心,它同时和两路本振信号相乘,一路相位为0度,另一路相位为90度,得到I和Q两路正交信号。这一步的本质是频域搬移:让载波频率上的信号,落到零频附近,变成基带信号。
这里有个关键点,零中频架构本身对镜像抑制要求极高,因为I/Q两路的幅度和相位一旦不平衡,镜像信号就会直接叠加到有用信号上。WiFi链路在校准后通常能做到-40dB以上的镜像抑制,但在FM频段没有校准数据,实际操作时I/Q不平衡会更明显。不过FM广播信号的特点恰恰是抗强度失真能力强,即使镜像和幅度偏差存在,声音依然可以辨识,只是信噪比会下降。
本振频率的配置是关键中的关键。ESP32的射频锁相环基于一个40MHz的参考时钟,通过分数分频器可以产生很宽的频率范围。社区逆向工程给出的经验是,本振可以覆盖几十兆赫兹到接近3GHz的范围,FM广播频段就在其中。操作方式也不是通过官方SDK暴露的接口,而是直接向射频寄存器写入配置值。不同芯片版本和不同SDK版本,寄存器偏移量可能不一致,这也是新手最容易卡壳的地方。
要特别提醒的是,把本振调到FM频段之后,芯片的WiFi功能实际上已经不在正常工作状态了。即使你只是想顺便扫个2.4GHz信道,也建议先把寄存器恢复出厂配置,再初始化WiFi协议栈。否则轻则连接不稳定,重则协议栈异常。
2.2 信号流、关键参数与数学原理
信号在芯片内的流动路径,决定了你能拿到什么数据。FM频段的射频信号被本振搬到基带后,经过可变增益放大和低通滤波,进入ADC采样。与WiFi协议包处理流程不同的是,这条隐蔽通路需要把ADC输出的I/Q采样数据通过DMA搬运出来,送到ESP32的内存里做软件解调。社区常见的做法是借用I2S外设的DMA通道来搬运这些数据。
采样率的选择很有讲究。FM广播的标准频偏是±75kHz,加上保护带宽,单边信号一般不超过200kHz。按照奈奎斯特定理,I/Q两路的采样率至少需要400kHz。实际操作中,大家通常选择512kHz或者更高一点的采样率做过采样。过采样的好处是降低抗混叠滤波器的压力,同时给数字滤波留出更大的过渡带。但采样率也不能无限高,一方面DMA带宽有限,另一方面CPU还要实时处理这些数据,采样率太高会让解调线程跑不满,反而出现丢数据。
数字解调的核心是鉴频。频率调制的信息,蕴含在信号的瞬时频率变化里,而瞬时频率就是瞬时相位的导数。数字域里最常用的方法是差分叉积鉴频器,大致思路是:先计算相邻两个采样点之间的瞬时相位差,然后做归一化。这里有一个实现上的坑,相位计算涉及反正切,直接调用库函数慢,而且相位跳变时容易产生毛刺;差分叉积法巧妙地用近似运算绕开了相位unwrap问题,在嵌入式平台上表现稳定得多。
再说立体声解码。FM广播的立体声复合信号里,基带部分是L+R信号,38kHz副载波上承载L-R差分信号,19kHz位置还有一个导频信号用于指示立体声存在。软件解码时,先用一个窄带滤波器提取19kHz导频,锁相恢复38kHz副载波,再通过乘法器把L-R分量搬移到基带,最后做矩阵运算得到左右声道。这一整套流程在ESP32的双核240MHz主频下是可以实时完成的,但需要把信号处理过程拆得足够细,合理利用两个核心。
3. 从零开始把FM广播"请"进ESP32
3.1 硬件准备与天线接线
硬件清单非常简短,一块ESP32开发板、一根杜邦线、一个耳机或带功放的小喇叭,最多再加一个USB供电线。不要小看这根杜邦线,它就是你的天线。
FM广播频段的波长约为3米,理论上1/4波长天线约75厘米。但实际测试中,由于接收链路还有低噪声放大器的增益兜底,哪怕只用一根十几厘米的杜邦线,也能收到信号较强的本地电台。天线并不是越长越好,尤其是没有做阻抗匹配的情况下,天线和射频输入端口之间的失配会导致信号反射,反而恶化接收。
我的建议是,先用一根约70厘米的杜邦线做全尺寸试验线,把它尽量拉直,远离金属桌面和人体。等确认通路没问题之后,再缩短天线找平衡点。天线直接插在开发板标注为"ANT"的射频接口上,如果手头板子没有引出射频接口,那就只能把导线焊在靠近射频屏蔽罩的焊盘上,这个位置因板子而异,需要对着原理图找。不要尝试把天线接到普通GPIO引脚上,那些引脚没有射频前端,信号进不来。
供电方面,FM接收的瞬时功耗不算高,但USB供电纹波比较大的时候,底噪会明显抬升。如果条件允许,用锂电池供电效果会更好。实测下来,同一个电台,电池供电时背景噪声明显比USB供电低一到两档。
3.2 软件工程搭建与射频寄存器配置
软件层面,第一步是准备开发环境。我建议直接用Arduino框架加ESP32支持包,调试起来最省事。第二步,需要找到社区维护的射频寄存器映射信息。这个信息不会出现在官方头文件里,一般以补丁或笔记的形式流传在硬件逆向论坛。你需要确认自己的芯片是原版双核ESP32还是S3、C3等新型号,因为射频寄存器地址并不通用。
工程配置的核心,是让I2S外设工作在"接收模式"并读取ADC数据。这里有个很绕的点:官方SDK把I2S外设设计成音频编解码器接口,正常情况下你不可能从I2S读到WiFi基带数据。实际操作中,需要先通过寄存器操作把射频链路切换到宽频接收模式,再把I2S的DMA通道指向射频基带数据缓冲区。这个操作顺序非常关键,顺序反了,I2S根本不会产生数据。
下面这段代码是射频配置部分的示意,具体寄存器地址请根据你自己芯片版本的寄存器映射表替换:
// 示意代码:把ESP32射频本振配置到FM频段
// 注意:表中地址只是占位符,请替换为社区逆向笔记中的真实地址
#define RF_PLL_CFG_REG (0x3FF00000 + 0x0A)
#define RF_RX_PATH_REG (0x3FF00000 + 0x1A)
static void radio_set_frequency(uint32_t freq_khz) {
// 40MHz参考时钟下,计算分频比
// 这里只演示写入流程,真实寄存器还需要同时配置小数分频器
uint32_t div_val = (uint32_t)((uint64_t)freq_khz * 1000ULL / 40000000ULL);
REG_WRITE(RF_PLL_CFG_REG, div_val);
// 等待锁相环稳定
delay(10);
}
如果你不想改寄存器,还有一个更取巧的办法:在SDK编译阶段,用社区提供的头文件覆盖掉官方射频初始化函数,把本振频率从2.4GHz改成目标FM频点。这种方式侵入性小一些,但对编译流程的理解要求更高。不管哪种方式,只要本振锁定到比如98.6MHz,混频器输出的基带信号里就会出现这个电台的FM广播成分。
I2S配置的另一个关键点是字长和通道数。FM广播的I/Q数据是两路,正好对应I2S的左右声道,每路采样深度建议用16位,采样率按前面说的512kHz设置。DMA缓冲区要开得足够大,至少能容纳数千个采样点,否则高频数据源源不断进来时,中断处理稍微慢一点就会丢包。我用的是64KB的DMA缓冲,解调线程周期性地从缓冲区搬数据,实测连续运行几小时没有因为缓冲区溢出而断流。
3.3 数字解调与立体声输出:核心代码逻辑
射频数据从DMA通道拿到之后,真正的信号处理才刚开始。整个解调链路可以分成四个阶段:I/Q直流偏置校正、鉴频、去加重、立体声解码。
I/Q直流偏置校正很关键,混频器输出的基带信号会有一个直流分量,如果不减掉,鉴频之后的输出会出现一个很大的固定偏移,反映到声音上就是持续的背景嗡声。校正方法是实时计算I/Q的滑动平均值,然后从原始信号里减掉。这个操作在嵌入式平台上开销很小,用环形缓冲区就能实现。
鉴频用差分叉积法,下面这段是核心处理循环的示意:
// 差分叉积鉴频器
// iq_buffer 存放I/Q交织数据,N是本次处理的有效样本数
for (int n = 1; n < N; n++) {
float i_prev = iq_buffer[2 * (n - 1)];
float q_prev = iq_buffer[2 * (n - 1) + 1];
float i_cur = iq_buffer[2 * n];
float q_cur = iq_buffer[2 * n + 1];
// 叉积近似:I*ΔQ - Q*ΔI
float demod = i_prev * (q_cur - q_prev) - q_prev * (i_cur - i_prev);
// 幅度归一化,消除信号强度波动带来的增益变化
float mag_sq = i_prev * i_prev + q_prev * q_prev;
if (mag_sq > 1e-6f) demod = demod / mag_sq;
else demod = 0.0f;
// 存入解调缓冲区
demod_buffer[n] = demod;
}
解调后的信号频谱里,0到15kHz是L+R成分,19kHz是导频,38kHz是L-R差分信号。去加重滤波器要放在立体声解码之前,它是一个时间常数约50微秒或75微秒的一阶低通滤波器。不同地区采用的去加重时间常数不同,中国和欧洲是50微秒,北美是75微秒。如果听感高音偏亮、噪声加重的锯齿感明显,可以试试切换这个参数。
立体声解码需要先恢复38kHz副载波。最稳妥的方式是用软件锁相环锁定19kHz导频,再将本振信号二倍频到38kHz。软件锁相环实现起来有些繁琐,但性能稳定,长时间工作也不会漂移。如果你只是临时想验证通路,可以先不做立体声,直接把鉴频输出经过低通滤波后当作单声道音频输出。听到的电台声音依然清晰,只是没有左右声道的分离感。
音频输出方面,ESP32板载DAC只有两个通道,而且输出质量一般,适合推耳机。想推动喇叭,最好再接一个I2S音频解码芯片,比如常见的低成本音频解码器。解码后的音频数据通过I2S外设发送到解码器,功率放大后再接喇叭。我最初图省事直接用板载DAC接耳机,音量开大时失真比较明显,换成外接解码器之后才算出正常水平。
3.4 效果实测记录
我特意把实验安排在傍晚,那时候调频电台的信号传播比较稳定,台也最多。接好天线,寄存器配置完,I2S开始往外吐数据,第一件事不是急着解调,而是先抓一小段I/Q数据,打印到串口,用电脑端的串口绘图器看频谱。这一步能最快地确认射频通路是否真的锁定了电台。
实测记录的频谱图里,能看到明显的多普勒形态:中心频率处有一个尖峰,两边延伸出调频信号的特征边带。确认信号在基带出现之后,我才打开解调输出。第一次听到扬声器里传出电台主持人说话声的时候,还是有点兴奋的,毕竟这颗芯片买回来的时候,厂商从来没承诺过它能干这个。
立体声解码效果比预想中好。城市里的强台,信噪比足够高,锁相环锁定导频后,左右声道分离度能达到可以接受的水平。但郊区或者楼内信号弱的电台,立体声解码会频繁失锁,反而比强制单声道更难听。所以我在程序里加了一个信噪比估计算法,导频幅值低于阈值时自动切回单声道模式。
4. 踩坑实录与排查速查表
4.1 收不到台、信号弱的排查
这是最常见的卡壳点。第一步先确认本振真的锁定了。很多次调试,程序跑得流畅,I2S也出数据,但频谱上看不到任何信号峰,就是因为寄存器写进去的分频值没生效。排查办法是反复读写寄存器,把读回的值和写入值对比,确认锁相环状态寄存器里的锁定标志位有没有置位。
第二步检查天线位置。天线放在金属桌面旁边、人体手握住、靠近USB线,都会让接收灵敏度急剧恶化。FM频段的信号虽然波长较长,穿透力强,但近场环境里的感应干扰依然会把微弱信号淹没。把天线拉直,离开USB线20厘米以上,很多"收不到台"的问题其实就解决了。
第三步是增益设置。可编程增益放大器的档位如果太低,前端信号放大不足,ADC输出噪声主导,信号被淹没;如果太高,强台又会过载失真。社区经验是先从中间档位开始,然后观察解调输出的噪声底,逐步增减增益,直到出现清晰的台。
4.2 声音破音、单声道、噪声大的处理
破音最常见的原因是鉴频后的信号幅度超过了输出动态范围,尤其强台信号过载时。解决方法是把解调输出的自动增益控制打开,用滑动窗口统计信号的峰值电平,动态调整输出增益。这和传统收音机里的自动增益控制是一回事。
如果声音明显发闷,检查去加重滤波器的参数。很多人直接套用欧洲参数,听感尚可;但如果你所在区域的电台采用75微秒去加重,高音衰减会过大,声音发闷。切换参数后,高音通透感会立刻改善。
立体声解码噪声大,大概率不是解码器的问题,而是基带信噪比不够。FM解调有个特性:信噪比低于阈值时,噪声会急剧恶化,这就是所谓的门限效应。解决办法一是缩短天线改大信号,二是手动切换到单声道模式。在单声道模式下,解码链路少了一个频带搬移环节,噪声表现会好不少。
4.3 与WiFi/BT共存问题
这个实验和WiFi功能天然冲突,因为射频前端被占用了。但如果你还有蓝牙耳机传输的需求,可以做个简单的时分复用:用定时器周期性地在FM接收和WiFi/BT模式之间切换。FM接收每次只需要几十毫秒的连续信号,采够一段样本之后,让射频回到WiFi模式,等下次定时器触发再切回来。
时分复用方案有个前提,FM立体声解码对数据连续性很敏感,切换间隙太长会明显影响声音连续性。我的做法是每次切回FM模式连续采样200毫秒,解调后的音频数据放入环形缓冲区,由音频输出线程持续播放,这样能掩盖切换带来的中断感。实际测试中,蓝牙数据吞吐会受到影响,但普通音频传输勉强能用。
更彻底的规避方案,是在同一块主板上挂两块ESP32,一块专职FM接收,另一块负责WiFi/BT功能。两块芯片之间用UART或者SPI通信,各司其职,互不干扰。代价是多一块芯片成本和额外的通信延迟,但工程上更干净。
4.4 合规提醒与一个值得保留的小技巧
这条通路的本质是接收FM广播,属于合法的公众接收频段,在绝大多数地区,收听调频广播不需要许可。使用它去接收非公开通信、或者对频谱造成干扰,那就是完全不同的性质了。做技术实验的底线,是只接收合法的广播信号,不发射任何无线电波,不加装功率放大电路。ESP32的射频前端本来就是接收为主,这个实验也不会发射信号,但还是要明确边界。
最后分享一个小技巧,如果你手头没有示波器,可以通过改变本振频率,用解调输出的噪声底变化来粗略感知天线谐振。把本振从88MHz扫到108MHz,每隔200kHz记录一次噪声底,你会发现某个频段噪声明显降低,那通常意味着天线在当前环境下谐振较强,信号接收条件最好的频段就在附近。这个办法虽然粗糙,但能在没有仪器的情况下帮你找到最佳的接收位置,也算是穷玩无线电的一点乐趣。
这条隐藏的无线电通路,说到底不是芯片原厂精心准备的彩蛋,而是射频链路天然能力被社区重新发现的结果。对我来说,它的价值不在于省下一块收音机芯片,而是提醒自己:芯片数据手册写的是设计目标,硬件上能做到的事往往比纸面描述更宽。ESP32只是恰好把"宽频接收"这个门留了一条缝,顺着缝往里走,能学到的东西远比一个能响的FM收音机多。
1480



被折叠的 条评论
为什么被折叠?



