ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

杰理FM收音机自动搜台实战:RSSI与SNR双阈值标定及常见故障排查

杰理FM收音机自动搜台实战:RSSI与SNR双阈值标定及常见故障排查

前阵子给一款便携收音机做固件,用的是杰理AC69xx平台的方案。产品经理扔过来一句“FM搜台要又快又准”,结果手动搜台一切正常,自动搜台一跑就露馅:市区里该搜的台少搜两三个,郊区反而把噪声当台存进列表,更崩溃的是偶尔会无限循环搜不停。那几天基本就是在“搜台、删台、重搜”的循环里折腾,最后才意识到,问题根本不在于“扫得快不快”,而是对“到底什么算一个有效电台”的判断方式出了问题。

这篇就把杰理系列芯片做FM搜台这件事从头到尾捋清楚:射频链路上搜台到底在搜什么、SDK里的调用流程怎么组织、RSSI和SNR这些参数怎么标定,以及漏台、串台、重复台和死循环这些经典问题到底出在哪。适合正在用杰理芯片做收音机产品、或者想在蓝牙音频方案里塞FM功能的开发者参考。

1. 搜台不是“扫频率”,是“判断信号可信度”

1.1 频率扫描只是看起来简单

FM搜台的表面逻辑任何人看一遍都能懂:在合法频段内,从下限频率到上限频率,按固定步长(比如100kHz)一个点一个点地走,每到一个频点就让接收机稳定下来,读信号强度、信噪比,判断这个频点是不是一个“可听的台”,是就记下来,不是就继续走。

但真正把这一步做到位,问题就来了。FM接收链路是这样的:天线进来的射频信号经过低噪声放大、混频降到中频或者直接零中频,再经过信道滤波器、限幅放大、解调,最后恢复出音频。搜台时我们关心的信号强度RSSI,是在解调之前的射频端读出来的;而信噪比SNR,是解调之后对音频质量的度量。一个描述“信号的绝对强度”,一个描述“信号相对噪声干不干净”,两者不能互相替代。

我见过不少开发者直接在代码里写一句“RSSI大于30就存台”,这在信号环境简单的实验室里能跑,一拿到真实环境就废。原因在于FM信号是恒包络调制,没有电台时,背景噪声同样能让RSSI读数飘得很高,市区底噪动辄30dBμV以上,这时单纯看RSSI,噪声点就会被当成电台存进去。反过来,遇到强台时自动增益控制(AGC)会压缩中频增益,RSSI读数反而掉下来,用固定阈值又容易把好台漏掉。所以搜台必须靠RSSI和SNR双维度判断,这是第一个要建立的认知。

1.2 信号强度的“假象”与信噪比的“真实感”

打个比方。RSSI好比远处有人举着手电筒照你,手电筒很亮,但中间隔了一层雾,你还是看不清来的人是谁。SNR则是你“看清来的人是谁”的程度。FM解调的特殊之处在于,射频端噪声再大,只要信号压过噪声一个量级,解调器就能把信号“钳”出来,音频里几乎听不到噪声;而如果信号强度不够,解调出来的就是一片沙沙声。所以SNR其实更接近人耳的真实听感。

实操中我一般这样看数据:安静郊区下,没台时RSSI可能只有10~15dBμV,SNR低得没法看;一旦有台,RSSI通常会跳到25dBμV以上,SNR也维持在20dB以上。市区相反,没台时RSSI可能已经30dBμV,但SNR很难超过10dB,因为那只是宽带噪声被检波成“强度”,并不是一个真正的载波;出现一个弱台时,SNR可能到15dB左右,虽然信号不强,但人耳已经能听清内容。这几个典型数据可以作为判台的第一感觉:

场景典型噪声底RSSI有效台RSSI有效台SNR
安静郊区10~15dBμV25~40dBμV20~40dB
市区环境25~40dBμV30~50dBμV15~25dB
临界弱台接近底噪20dBμV左右10~15dB
纯噪声20~35dBμV不稳定通常小于8dB

这个表的意义在于,当你准备设阈值时,不要拍脑袋。先把设备拿到目标场景,用串口把RSSI和SNR打印出来,观察“能听清的台”和“纯噪声”两组数据的分界线落在哪,然后去找一个中间值,这样标出来的阈值才有说服力。

2. 杰理SDK中FM搜台的主流程与接口调用拆解

2.1 搜台前要把状态“摆正”

在杰理平台上做FM搜台,第一步不是直接调搜索函数,而是把系统状态摆正。顺序大概是:初始化FM模块、选择频段制和步进、关闭不必要的音效处理、把音量调到基准值、然后打开软静音。

为什么要先静音再搜台?因为频率切换的瞬间,PLL锁相环重新锁定、中频滤波器切换,音频输出会有明显的“咔哒”声,连续扫描时就是一连串机械噪声。搜台期间把音频静音掉,是最基本的体验保障。有些平台SDK里有专门的“扫描模式”接口,会自动处理静音和信号判定,如果没有,就要手动做。

另外,搜台前要把I2C或者芯片内部寄存器访问总线空出来,尤其是杰理这种FM和蓝牙共用一个SoC的方案。如果此时蓝牙正在传音频,先暂停或降低蓝牙任务优先级,否则搜台过程中的寄存器读写会被打断,导致频率还没稳定就跑去读RSSI,读回来的数当然不准。

2.2 逐频点扫描与有效性判断的标准流程

下面是杰理FM模块搜台的主流程示意。接口命名在不同的SDK版本里会变,但调用逻辑基本是这套:

// 示意流程,具体接口以当前SDK的fm头文件为准 fm_band_set(FM_BAND_CN); // 设置87.5~108MHz频段 fm_step_set(FM_STEP_100K); // 设置100kHz步进 fm_mute(1); // 扫描全程静音 for (uint16_t freq = start_khz; freq <= end_khz; freq += step_khz) { fm_set_freq(freq); // 切到目标频点 delay_ms(30); // 等待PLL锁定、信号稳定 uint8_t rssi = fm_get_rssi(); uint8_t snr = fm_get_snr(); if (is_valid_station(rssi, snr)) { uint8_t confirm_cnt = 0; // 连续多次采样,去抖确认 for (uint8_t i = 0; i < 5; i++) { delay_ms(10); if (is_valid_station(fm_get_rssi(), fm_get_snr())) { confirm_cnt++; } } if (confirm_cnt >= 3) { channel_save(freq); // 频率入表 } } } fm_mute(0);

这段流程里最核心的是is_valid_station这个判据函数。我在工程里的写法是:

bool is_valid_station(uint8_t rssi, uint8_t snr) { if (rssi > RSSI_THRESHOLD && snr > SNR_THRESHOLD) { return true; } return false; }

有人说这也太简单了,后文你会看到,阈值本身怎么定,才决定这个函数是宝贝还是摆设。

2.3 找到电台之后的锁定与播放动作

上面是“全频段扫描”的逻辑,适用于用户手动触发“搜全台”,把所有有效台一次全找出来存进列表。还有另一种交互是“自动搜台”,用户按一下键,从当前频率向上或向下找最近的一个有效台,找到就停下来播放。这种逻辑差异体现在循环上:不是遍历整个频段,而是一旦找到有效台立即break退出循环,然后取消静音、恢复音量。

无论哪种模式,找到台后都要注意恢复时序。取消静音之前先确认音频通路已经切换好,避免把上一个频点的残余数据播出来;恢复音量建议用一二百毫秒的渐变,不要直接跳变,否则会有“砰”的爆音。这些细节不做,用户按一次搜台键就觉得“滋啦”一下,好感度直接减半。

3. 决定搜台体验的关键参数:RSSI、SNR与去抖标定

3.1 RSSI会被AGC“骗”,标定时要正视这一点

很多做FM搜台优化的开发者,卡得最久的往往不是流程,而是“为什么同一个频点,清晨能搜到,中午就搜不到”。这在城市环境下大概率是AGC和底噪两者共同作用的结果。

FM接收机为了兼顾强弱信号,会在天线端到中频端分布自动增益控制。信号很强的时候,前端增益被压低,于是射频端读到的RSSI反而不会太高。这时候如果阈值盲目设低,强台不会丢;但如果信号强弱突然交替,AGC起控时间跟不上,RSSI读数会来回跳,临界频点就可能被误判。所以我在实际项目里很少把RSSI当成唯一判据,而是把它当“门槛”而不当“质量”。只要RSSI超过一个基础值,比如20dBμV,接下来主要看SNR。

3.2 SNR是更接近人耳感受的质量标尺

FM信号有“捕获效应”:当存在一个较强的FM信号时,干扰信号会被抑制,解调输出基本只有强台的内容。这种效应让SNR成为一个很“硬”的指标。实测下来,SNR在15dB以上时,人耳已经能听清绝大部分语音和音乐;20dB以上时噪声明显可闻但不难受;超过30dB就是很干净的声音了。

需要特别注意的是立体声。立体声解码对信号质量要求更高,同样的频点,单声道听感不错,切到立体声就可能出现嘶嘶声。如果你的产品支持立体声,我建议判台时对立体声模式单独加5dB左右的SNR余量。也就是说,单声道模式SNR阈值可以设在12~15dB,立体声模式建议设在18~20dB。

3.3 去抖采样是“防误判”的最后一道防线

信号经常是抖动的,尤其在频点刚切换完的瞬间。PLL锁定的时间、信道滤波器的建立时间、AGC的稳定时间叠加起来,频率切过去之后的头20ms内读数都是不可信的。

我的做法是:切频后先等30ms,再进行连续采样。每次采样间隔10ms,连续采样5次,至少有3次满足判据才认定这是台。这样每个候选频点的总耗时大概是80ms左右。全频段从87.5到108MHz,以100kHz步进一共205个频点,满打满算扫一遍要16秒多,确实偏慢。想加快,可以做两级扫描:先用快速采样(每个频点只采一次,阈值放宽)扫一遍,把可能电台的候选频率记下来,再对每个候选点做慢速精确确认,总耗时能压到七八秒。

3.4 一套可参考的阈值起点

下面是几个环境场景下我实测过起点值,注意这是起点,到具体产品上还需要用样机现场微调:

应用场景RSSI阈值SNR阈值去抖次数
安静郊区、弱台优先18dBμV12dB3/5
一般城市环境25dBμV16dB3/5
强干扰商圈/写字楼30dBμV20dB3/5
立体声强制要求30dBμV20dB4/5

阈值设定的总原则是:宁可多搜到一个弱台,也不要存一堆噪声。台上去了可以删,噪声存进去用户会直接质疑产品坏了。

4. 实战排障:漏台、串台、重台与循环不停止的根因

4.1 漏台:先查步进和稳定时间,别急着改阈值

漏台是最常见的投诉。用手动搜台能收到的台,自动搜台就是搜不到。我遇到过的案例,超过一半是下面三个原因之一。

第一个是步进设置错误。有些地方存在100kHz间隔的邻频台,如果SDK默认步进是200kHz,从低到高扫描时,那些落在100kHz上的台就会从扫描网格中间漏过去。排查方法很简单:把步进强制改成100kHz再扫一遍,漏台立刻减少。

第二个是稳定等待时间太短。有些开发者把切频后的等待时间砍到5ms来追求“搜得快”,结果PLL还没锁住就去读RSSI,弱台读数偏低被过滤。测量方法是在固定频点上反复切进切出,用逻辑分析仪或者串口打印连续RSSI值,看看从发出切频指令到读数稳定大概需要多久,然后留出余量。

第三个才是阈值问题。如果你确认步进正确、时间足够,还是漏台,就先调SNR阈值而不是RSSI。因为弱台卡在RSSI边界上很常见,但SNR一旦够好,说明信号确实“解出来了”,人耳基本能听。我习惯把SNR阈值从18dB往下调到12dB试试,弱台就能重新出现。

4.2 串台与邻频干扰:通常是“把噪声当台存了”

串台的表现有两种。一种是搜台中途在一个没有台的地方停下来,播放时能听到隐隐约约的隔壁台声音;另一种是明明应该有台的频率点附近,却连续存了两个频率。

第一种情况根源是邻频强台的带外泄漏和镜像干扰。FM射频前端的滤波器选择性有限,一个强台可能在它实际频率之外好几个步进处仍然产生足够高的RSSI,如果只查RSSI不查SNR,误判就不可避免。解决思路就是回到双阈值判据,并且对候选频点做频率微调验证:如果这个频点算有效台,那么在它附近微调频率时,RSSI会围绕中心先升后降;如果只是泄漏,微调后读数变化没有明显峰值特征。

第二种情况通常需要做“邻频合并”。两个频率间隔不超过2个步进(对于100kHz步进就是200kHz)的候选台,基本可以判断是同一个电台或者其镜像,保存时只保留RSSI更高的那个。这个去重逻辑放在台站表写入前,简单有效。

4.3 重台和循环不停止:扫描边界与回卷逻辑的坑

搜台停不下来和重复存台的根因,多半出在边界处理上。

先说重复存台。很多团队直接按“从87.5到108MHz扫一遍”写循环,却忽视了频谱两端的效应。FM频段边界外的噪声和微弱信号同样可能越过阈值,被当成台存在列表里。我的做法是扫描范围比实际的合法频段向外各扩展两个步进,但对落在合法范围外的候选结果直接丢弃。同时,如果扫描起点不是频段下限,比如用户从89MHz开始向上搜,扫到108MHz后要回卷到87.5MHz继续搜,这时候如果不做“已经访问过该频率”的标记,就会把同一个台重复存两次。

循环不停止则是回卷条件写错了。常见写法是:

do { freq += step_khz; if (freq > end_khz) freq = start_khz; } while (found_count < max_count && freq != original_freq);

注意这个freq != original_freq比较一定要放在扫描结束之后判断,如果放在扫描中途,一旦回卷点恰好在某个已存台的频率上,循环可能提前退出。反过来,如果忘记加“回到起点”的终止条件,搜台就会无限跑。我在调这类bug时,习惯在调试固件里加一个“本次搜索访问过的最大频率数”计数器,超过频段内步进数+2就强制终止并打印卡住的位置,比死盯屏幕高效得多。

4.4 搜台过程的杂音与爆音:静音时序的细节

搜台时出现杂音,往往不是RF问题,而是静音时序没做好。要点有三条:搜索开始前静音,搜索过程中保持静音,搜索到台之后延时50ms左右再取消静音。延时是为了让音频通路稳定下来,避免在第N个频点的残留解码数据刚播出去时突然开音。另一个容易忽略的点是FM和蓝牙共用音频通路,搜台结束后要确保音频路由切回FM,否则会出现“搜到了台但没有声音”的诡异现象。

5. 搜台只是开始:台站表、制式适配与体验优化

5.1 台站表的数据结构与存储策略

搜完台如果不落盘,每次上电重新搜一遍,用户体验就很差。台站表我通常用固定数组管理,结构大概是:

typedef struct { uint16_t freq_khz; uint8_t rssi; uint8_t snr; uint8_t is_valid; } fm_preset_t; fm_preset_t preset_list[FM_PRESET_MAX]; // 通常预留20~40个

数组按频率升序排列,写入前先做去重和排序。存储介质用EEPROM还是Flash取决于平台资源,我建议在用户发起“保存电台”操作或者关机时一次性写入,不要在搜台过程中频繁擦写,否则寿命和功耗都受不了。

5.2 区域制式适配:别让一批货在海外变成废品

FM频段和步进不是全球统一的。中国和欧洲大部分地区是87.5~108MHz,美国是87.9~107.9MHz(奇数频率),日本则是76~90MHz且步进一般是50kHz。如果产品要出口,台站表的上下限、步进、甚至搜索起点都要做成配置项,而不是写死在代码里。

我踩过的坑是:只改频段上下限,忘了步进;某一版固件在日本用50kHz步进搜台,结果一台机器生生搜出80多个“台”,一半是噪声。后来配置表里加入step_khz和stereo_threshold字段,不同区域读不同配置,问题才算根治。

5.3 更进一步的体验优化:RDS、恢复播放与信号稳定性

基础搜台跑通之后,真正拉开产品体验差距的是这些后续优化。

一是RDS(无线数据广播)。如果能收到RDS数据,可以在台站表里同时存电台名称,UI上显示“中国交通广播”而不是一个光秃秃的频率,观感完全不同。注意RDS解码要在音频通路稳定的情况下进行,不要在搜台扫描过程中解析,否则丢帧严重。

二是开机恢复上次播放。上电先读台站表,直接跳到上次关机前的频率播放,比什么都搜不到让用户手动找台强得多。

三是搜台过程中的任务调度。杰理这类双模芯片上,搜台是I2C密集型操作,期间蓝牙如果还在跑协议栈,注意留足带宽。我实际项目里出现过搜台时蓝牙断连的情况,原因是FM搜台循环里连续做了太多delay阻塞,把协议栈的调度饿死了。解法是把搜台放到低优先级任务里,切频和读数的间隔之间主动让出CPU。

最后说一个我自己的习惯:调试搜台时,永远在代码里保留一级“强制打印每个频点的RSSI和SNR”的调试开关。平时关着,现场出问题就打开,让测试人员对着实际收音环境把数据打出来回传。很多时候你以为的“信号差”,其实是某个局部频点的底噪异常,没有这些原始数据,光靠猜阈值不知道要猜到什么时候。搜台这种功能,看着简单,真正把它调到“又快又准”,靠的就是对每一个频点的理解和对细节的死磕。

RELATED READING

延伸阅读

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