第二讲:物理层移植——射频前端配置、ASK/FSK调制解调实现、载波频率校准

各位同学,欢迎来到物理层移植这一讲。

说实话,物理层是整个协议栈里最「接地气」的一层。你想想看,上层协议写得再漂亮,如果射频前端发不出信号,或者解调出来的数据全是错的,那一切都白搭。我当年刚入行时,就吃过这个亏——协议栈跑得飞起,结果一上频谱仪,载波偏了20kHz,整个项目差点重做。

好,咱们今天就把这三块内容啃透:射频前端怎么配、ASK/FSK怎么调、载波频率怎么校。

一、射频前端配置——别小看这几行寄存器

射频前端,说白了就是天线和基带之间的「翻译官」。它负责把数字信号变成电磁波,再把电磁波变回数字信号。

我个人习惯,拿到一个新平台,第一件事不是写代码,而是看数据手册里的射频前端框图。你得搞清楚:

  • 发射链路:DAC → 混频器 → PA → 匹配网络 → 天线
  • 接收链路:天线 → LNA → 混频器 → 滤波器 → ADC

配置时,有几个关键参数必须盯死:

参数 典型值 我的经验
发射功率 +10 dBm ~ +20 dBm 别一上来就开最大,先调低,测完匹配再往上加
接收灵敏度 -110 dBm ~ -90 dBm 这个值跟LNA偏置电流强相关,省电模式要小心
带宽 125 kHz / 250 kHz / 500 kHz 带宽越窄,灵敏度越高,但频率偏移容忍度越低
PA ramp time 10 μs ~ 50 μs 太快会频谱超标,太慢浪费功耗
⚠ 注意: 射频前端配置顺序很重要。我见过有人先开PA再配PLL,结果频率还没锁定就发射了,频谱上一片毛刺。正确的顺序是:先配PLL并等待锁定 → 再配调制器 → 最后开PA。

二、ASK/FSK调制解调实现——从波形到比特

ASK和FSK是电子标签最常用的两种调制方式。为什么?因为实现简单、功耗低。

2.1 ASK调制——振幅键控

ASK说白了就是「有信号代表1,没信号代表0」。但实际工程中,不会真的把载波完全关断,那样频谱会很难看。

我建议的做法是:

  • 用深度调制(调制深度 > 90%)表示逻辑1
  • 用浅调制(调制深度 10%~30%)表示逻辑0

代码实现上,我习惯用查表法:

// ASK调制示例:调制深度可配置
void ask_modulate(uint8_t *data, uint16_t len, uint8_t depth) {
    for (int i = 0; i < len; i++) {
        for (int j = 7; j >= 0; j--) {
            if (data[i] & (1 << j)) {
                // 逻辑1:高振幅
                set_dac_output(MAX_AMPLITUDE);
            } else {
                // 逻辑0:低振幅(按depth比例)
                set_dac_output(MAX_AMPLITUDE * depth / 100);
            }
            delay_symbol();  // 一个符号周期
        }
    }
}
💡 小技巧: 解调ASK时,别直接用ADC采样值做判决。我踩过坑——信号有衰落,直接判决误码率很高。正确做法是:先做滑动平均滤波,再用自适应阈值判决。阈值可以取最近N个采样点的中值。

2.2 FSK调制——频移键控

FSK比ASK抗干扰能力强,但实现稍微复杂一点。它用两个不同的频率表示0和1。

我记得有一次做项目,客户要求FSK频偏±50kHz。我直接用PLL的频偏寄存器配了,结果测试发现解调出来的数据全是乱的。后来查了半天,发现是PLL的锁定时间跟不上符号切换速度。

这里有个关键公式:

频偏 = (f1 - f0) / 2
调制指数 h = 频偏 × 符号周期

工程经验:

  • h > 1 时,两个频率正交性好,解调容易,但占用带宽大
  • 0.5 < h < 1 时,带宽和性能折中,最常用
  • h < 0.5 时,叫MSK,带宽最窄,但解调复杂

FSK解调我推荐用零交叉检测法:

// FSK零交叉检测解调
uint8_t fsk_demodulate(int16_t *samples, uint16_t len) {
    uint16_t zero_cross_count = 0;
    for (int i = 1; i < len; i++) {
        if ((samples[i-1] > 0) && (samples[i] <= 0)) {
            zero_cross_count++;
        }
    }
    // 零交叉次数多 → 频率高 → 逻辑1
    // 零交叉次数少 → 频率低 → 逻辑0
    return (zero_cross_count > THRESHOLD) ? 1 : 0;
}
🔑 核心要点: 无论是ASK还是FSK,解调的关键都在于「同步」。没有符号同步,一切都是空中楼阁。我建议在数据包前面加一段固定的前导码(比如0x55或0xAA),用来做同步头。

三、载波频率校准——别让你的设备「跑偏」

载波频率校准,说白了就是让实际发射频率和设定频率一致。为什么需要校准?因为晶振有误差,温度会漂移,PCB走线也有寄生参数。

我曾经遇到一个案例:产品在实验室测得好好的,一拿到北方冬天户外,全部掉线。查了两天,发现是晶振在低温下频率偏了30ppm,接收端解调失败。

校准方法主要有三种:

  1. 晶振微调:通过调整晶振的负载电容,改变振荡频率。精度一般,适合粗调。
  2. PLL补偿:在PLL的N分频器里做数字补偿。精度高,但需要知道当前温度。
  3. 闭环校准:发射一个已知频率,用接收机测回来,做闭环调整。最准,但需要额外硬件。

我个人的推荐做法是:

  • 量产时做一次出厂校准,把晶振误差存到Flash里
  • 运行时每隔一段时间做温度补偿,查表修正
  • 如果对精度要求极高(比如 < 1ppm),上TCXO(温度补偿晶振)

校准流程代码示例:

// 载波频率校准流程
void freq_calibrate(void) {
    int16_t freq_error;
    
    // 1. 读取当前温度
    int16_t temp = read_temperature();
    
    // 2. 查温度补偿表
    int16_t compensation = lookup_compensation(temp);
    
    // 3. 设置PLL频偏
    set_pll_freq_offset(compensation);
    
    // 4. 发射校准信号(比如连续波)
    tx_continuous_wave();
    
    // 5. 用频谱仪或内置接收机测量实际频率
    freq_error = measure_freq_error();
    
    // 6. 如果误差超出范围,做二次修正
    if (abs(freq_error) > MAX_ALLOWED_ERROR) {
        set_pll_freq_offset(compensation + freq_error);
    }
    
    // 7. 保存校准值
    save_calibration_data(compensation + freq_error);
}
⚠ 避坑指南: 我曾经犯过一个错误——在校准前没有等PLL完全锁定。结果校准值全是错的,越校越偏。记住:PLL锁定时间通常需要几百微秒到几毫秒,别省这点时间。

四、移植实战中的常见问题

最后,我总结几个移植时最容易踩的坑:

  • 寄存器地址不同:不同厂家的射频芯片,寄存器映射完全不同。别想当然地复制粘贴。
  • 时序要求不同:有的芯片要求SPI时钟空闲时高电平,有的要求低电平。我吃过这个亏,烧了三块板子。
  • 匹配网络差异:同样的芯片,换一块PCB,匹配网络就得重新调。别指望「通用配置」。
  • 中断优先级:射频中断通常要求实时响应。如果被其他低优先级中断阻塞,会导致丢包。

嗯,物理层移植的内容就讲到这里。下一讲我们会进入数据链路层,聊聊帧格式和CRC校验。到时候我会分享一个我当年调试CRC时遇到的「灵异事件」——明明算法没错,校验就是通不过。你猜是什么原因?

好,今天就到这儿。有问题随时找我。