第二讲:物理层移植——射频前端配置、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 | 太快会频谱超标,太慢浪费功耗 |
二、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(); // 一个符号周期
}
}
}
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;
}
三、载波频率校准——别让你的设备「跑偏」
载波频率校准,说白了就是让实际发射频率和设定频率一致。为什么需要校准?因为晶振有误差,温度会漂移,PCB走线也有寄生参数。
我曾经遇到一个案例:产品在实验室测得好好的,一拿到北方冬天户外,全部掉线。查了两天,发现是晶振在低温下频率偏了30ppm,接收端解调失败。
校准方法主要有三种:
- 晶振微调:通过调整晶振的负载电容,改变振荡频率。精度一般,适合粗调。
- PLL补偿:在PLL的N分频器里做数字补偿。精度高,但需要知道当前温度。
- 闭环校准:发射一个已知频率,用接收机测回来,做闭环调整。最准,但需要额外硬件。
我个人的推荐做法是:
- 量产时做一次出厂校准,把晶振误差存到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);
}
四、移植实战中的常见问题
最后,我总结几个移植时最容易踩的坑:
- 寄存器地址不同:不同厂家的射频芯片,寄存器映射完全不同。别想当然地复制粘贴。
- 时序要求不同:有的芯片要求SPI时钟空闲时高电平,有的要求低电平。我吃过这个亏,烧了三块板子。
- 匹配网络差异:同样的芯片,换一块PCB,匹配网络就得重新调。别指望「通用配置」。
- 中断优先级:射频中断通常要求实时响应。如果被其他低优先级中断阻塞,会导致丢包。
嗯,物理层移植的内容就讲到这里。下一讲我们会进入数据链路层,聊聊帧格式和CRC校验。到时候我会分享一个我当年调试CRC时遇到的「灵异事件」——明明算法没错,校验就是通不过。你猜是什么原因?
好,今天就到这儿。有问题随时找我。