CH585 CH587 蓝牙固件自动调整频偏
什么是频偏
调制特性
在调整频偏前,先了解一下 BLE 是怎么区分射频信号的 0/1 的。
BLE(低功耗蓝牙)空中数据交互核心依赖于 GFSK(高斯频移键控)调制。在 GFSK 中,区分射频信号中的 bit 0 和 bit 1,不是通过信号“有无”或简单的幅度高低来判断,而是通过信号频率的偏移方向来区分:正偏差表示 bit 1,负偏差表示 bit 0。如下图所示,是蓝牙核心手册中关于 GFSK 的截图。

(内容相关路径是:BLUETOOTH CORE SPECIFICATION Version 5.4 | Vol 6, Part A | 3.1 MODULATION CHARACTERISTIC 中)
从上图可知:
| 比特值 | 频率偏移 | 实际表现 |
|---|---|---|
| 0 | 负偏移(-Δf) | 载波频率 降低 |
| 1 | 正偏移(+Δf) | 载波频率 升高 |
| 参数 | BLE (LE PHY) |
|---|---|
| 调制指数 | 0.45 ~ 0.55 |
| 最小频偏 (1 M/s) | 185 kHz |
| 最小频偏 (2 M) | 370 kHz |
小结:调制特性关注的是 bit 0 和 bit 1 之间的频率差,确保两者的频率差异足够大,接收端才能可靠分辨。因此,物理层并不传输绝对频率值(类似 PWM 中开启表示 bit 0、关闭表示 bit 1),而是通过相对于信道中心频率的偏移方向来表示 bit 0 或 bit 1。
下面是 GFSK 调制中 bit 0 与 bit 1 的区分逻辑:
从调制特性 GFSK 可知,空中传输是通过相对于信道中心频率的偏移方向来表示 bit 0 或 bit 1 的。
既然数据是依靠相对于信道中心频率的偏移来承载的,那么一旦信道中心频率本身出现误差,数据的解调也会不准确。因此,蓝牙规范对信道中心频率同样有明确的要求。如下图:
(内容相关路径是:BLUETOOTH CORE SPECIFICATION Version 5.4 | Vol 6, Part A 3.3 RADIO FREQUENCY TOLERANCE 中)
小结:
中心频率偏差是指发射载波相对于信道标称中心频率的偏移。若该偏移超出规范允许范围(如 ±150 kHz),会导致信号偏离信道,落入邻道或带外,造成邻道干扰与邻道噪声混入,导致数据无法解调。
一般讨论的频偏指的就是中心频率偏差,需要调整的也是中心频率偏差。
中心频率偏差对信号的影响过程如下:

频偏调整方式
晶振负载电容
为什么 BLE 的频偏可以通过外部的高速晶振的负载电容调整?
BLE 的 2.4G 是通过外部高速晶振经 PLL 倍频后输出的,时钟源就是外部的高速晶振,所以调整外部的高速晶振的频偏就可以调整 2.4G 的频偏。
CH584/5 芯片内置负载电容,所以可以直接使用代码调整。
注:可以直接调用相关 API 调整负载电容,没有特别的限制。
BLE 频偏调整的链路如下:

获取频偏
CFO(Carrier Frequency Offset,载波频率偏移)用于衡量发射端与接收端之间的基准时钟同步状态,反映的是发射机射频载波频率与接收机本振频率之间的不匹配程度,获取到的是相对误差。
SDK 中关于 CFO 函数:
/** * @brief read cfo * * @param None. * * @return the value of cfo. */ extern int16_t BLE_ReadCfo( void );
注:该函数的返回值 1 单位表示 0.625 kHz,
小结:
因为获取的是相对频率差,所以需要以频偏足够准确的发射端作为基准。例如,可以使用官方评估板配合最新 SDK 中的主机代码,先经频谱仪确认其实际频偏后再作为基准使用;也可以选用较新的苹果手机作为基准。
代码实现
逻辑框图

实现逻辑
CH584/5 负载电容枚举:
typedef enum { HSECap_10p = 0, // 10pF | 10进制: 0 | 16进制: 0x00 | 2进制: 0000b HSECap_12p, // 12pF | 10进制: 1 | 16进制: 0x01 | 2进制: 0001b HSECap_14p, // 14pF | 10进制: 2 | 16进制: 0x02 | 2进制: 0010b HSECap_16p, // 16pF | 10进制: 3 | 16进制: 0x03 | 2进制: 0011b HSECap_18p, // 18pF | 10进制: 4 | 16进制: 0x04 | 2进制: 0100b HSECap_20p, // 20pF | 10进制: 5 | 16进制: 0x05 | 2进制: 0101b HSECap_22p, // 22pF | 10进制: 6 | 16进制: 0x06 | 2进制: 0110b HSECap_24p, // 24pF | 10进制: 7 | 16进制: 0x07 | 2进制: 0111b HSECap_2p, // 2pF | 10进制: 8 | 16进制: 0x08 | 2进制: 1000b HSECap_4p, // 4pF | 10进制: 9 | 16进制: 0x09 | 2进制: 1001b HSECap_6p, // 6pF | 10进制: 10 | 16进制: 0x0A | 2进制: 1010b HSECap_8p // 8pF | 10进制: 11 | 16进制: 0x0B | 2进制: 1011b } HSECapTypeDef;
根据上表可知 CH584/5 系列的负载电容寄存器值是非线性的,在调整、计算的过程中需要做 2 次转化
1、将目前在使用的负载电容值转化为线性的(0 pF -> 0,2 pF -> 1,4 pF -> 2,6 pF -> 3),如:
①大于 8:减去 8,2 pF 的线性值 0,实际寄存器值 0x08,所以要减去 8,8-8=0
②小于 8:加上 4,10 pF 线性值 4,实际寄存器值 0x00,所以要加上 4,4+0=4
经过上文的步骤得知线性挡位后,按需加上或减去差值,计算出调整目标值,并且确认调整目标值合法后,需要将线性值转化为实际枚举值。
代码
uint8_t get_curr_cap_load(void){ uint8_t reg_val = R8_XT32M_TUNE; uint8_t cap_load = (reg_val >> 4) & 0x0F; uint8_t cap_val = 0; if ((cap_load & 0x08) == 0) { // Bit 7 为 0 (0000b~0111b): 电容 = cap_load * 2 + 10 (对应 10pF ~ 24pF) cap_val = cap_load * 2 + 10; } else { // Bit 7 为 1 (1000b~1111b): 取低 3 位计算,电容 = (cap_load & 0x07) * 2 + 2 (对应 2pF ~ 16pF) cap_val = (cap_load & 0x07) * 2 + 2; } return cap_val; } #define FREQ_TGT_KHZ 50 void Adjust_Cap_LOAD (int32_t freq_offset_khz){ // 在 ±50kHz范围内直接退出,不用再次校准 if (freq_offset_khz >= -FREQ_TGT_KHZ && freq_offset_khz <= FREQ_TGT_KHZ) { PRINT("Offset within +/-%d kHz. No adjustment needed. Current Load Cap: %d pF\n", FREQ_TGT_KHZ, get_curr_c_load()); return; } PRINT("===== Start Cap Adjustment =====\n"); PRINT("Input Frequency: %d kHz Current Load Cap: %d pF\n", freq_offset_khz,get_curr_cap_load()); // 读取当前负载电容配置 uint8_t current_enum = (R8_XT32M_TUNE >> 4) & 0x0F; // 计算负载电容调整量,加15目的是向上取整,经测试2pf可以调整30khz的频率 int32_t delta_steps; if (freq_offset_khz > 0) { delta_steps = -((freq_offset_khz + 15) / 30); } else { delta_steps = ((-freq_offset_khz) + 15) / 30; } // 一次性转换:当前负载电容 -> 步进值 -> 目标步进(限幅) -> 目标枚举 //由于58的负载电容配置是非线性的所以需要转化一下才可使用 int32_t cur_step = (current_enum >= 8) ? (current_enum - 8) : (current_enum + 4);//例:2pf->挡位0 实际 寄存器值 0x08-> 8-8=0 int32_t tgt_step = cur_step + delta_steps; // 边界限幅 (0 ~ 11 档) if (tgt_step > 11) tgt_step = 11; if (tgt_step < 0) tgt_step = 0; // 转换为 HSECapTypeDef 枚举 //例:2pf->挡位0 实际 寄存器值 0x08-> 0+8=8 uint8_t target_enum = (tgt_step < 4) ? (tgt_step + 8) : (tgt_step - 4); PRINT("Current enum: %d | Delta Steps: %+d | Target Enum: %d\n", current_enum, delta_steps, target_enum); // 5. 写入寄存器 if (target_enum != current_enum) { PRINT("Change Enum: %d -> %d\n", current_enum, target_enum); HSECFG_Capacitance(target_enum); PRINT("After Adjustment Load Cap: %d pF\n",get_curr_cap_load()); } PRINT("===== Cap Adjustment End =====\n"); }
代码调用
可以直接在广播扫描回复内,筛选特定主机 MAC 地址(频偏较小的主机)的扫描回复请求后,触发负载电容调整函数。

实际运行
运行LOG 如下:
注意:开启校准前,主机要持续地扫描从机,有交互 CFO 才可以获取到相对频偏。
Receive scan req from 02 02 03 e4 c2 84 .. CFO:85 frequency 53 ===== Start Cap Adjustment ===== Input Frequency: 53 kHz Current Load Cap: 22 pF Current enum: 6 | Delta Steps: -2 | Target Enum: 4 Change Enum: 6 -> 4 After Adjustment Load Cap: 18 pF ===== Cap Adjustment End ===== Receive scan req from 02 02 03 e4 c2 84 .. CFO:-16 frequency -10 Offset within +/-50 kHz. No adjustment needed. Current Load Cap: 18 pF Receive scan req from 02 02 03 e4 c2 84 .. CFO:-12 frequency -7 Offset within +/-50 kHz. No adjustment needed. Current Load Cap: 18 pF Receive scan req from 02 02 03 e4 c2 84 .. CFO:-14 frequency -8 Offset within +/-50 kHz. No adjustment needed. Current Load Cap: 18 pF
独立文件
#include "frequency_calibration.h" uint8_t get_curr_cap_load(void){ uint8_t reg_val = R8_XT32M_TUNE; uint8_t cap_load = (reg_val >> 4) & 0x0F; uint8_t cap_val = 0; if ((cap_load & 0x08) == 0) { // Bit 7 为 0 (0000b~0111b): 电容 = cap_load * 2 + 10 (对应 10pF ~ 24pF) cap_val = cap_load * 2 + 10; } else { // Bit 7 为 1 (1000b~1111b): 取低 3 位计算,电容 = (cap_load & 0x07) * 2 + 2 (对应 2pF ~ 16pF) cap_val = (cap_load & 0x07) * 2 + 2; } return cap_val; } #define FREQ_TGT_KHZ 50 void Adjust_Cap_LOAD (int32_t freq_offset){ // 在 ±50kHz范围内直接退出,不用再次校准 if (freq_offset >= -FREQ_TGT_KHZ && freq_offset <= FREQ_TGT_KHZ) { PRINT("Offset within +/-%d kHz. No adjustment needed. Current Load Cap: %d pF\n", FREQ_TGT_KHZ, get_curr_cap_load()); return; } PRINT("===== Start Cap Adjustment =====\n"); PRINT("Input Frequency: %d kHz Current Load Cap: %d pF\n", freq_offset,get_curr_cap_load()); // 读取当前负载电容配置 uint8_t current_enum = (R8_XT32M_TUNE >> 4) & 0x0F; // 计算负载电容调整量,加15目的是向上取整,经测试2pf可以调整30khz的频率 int32_t delta_steps; if (freq_offset > 0) { delta_steps = -((freq_offset + 15) / 30); } else { delta_steps = ((-freq_offset) + 15) / 30; } // 转换:当前负载电容 -> 步进值 -> 目标步进(限幅) -> 目标枚举 //由于58的负载电容配置是非线性的所以需要转化一下才可使用 int32_t cur_step = (current_enum >= 8) ? (current_enum - 8) : (current_enum + 4);//例:2pf->挡位0 实际 寄存器值 0x08-> 8-8=0 int32_t tgt_step = cur_step + delta_steps; // 边界限幅 (0 ~ 11 档) if (tgt_step > 11) tgt_step = 11; if (tgt_step < 0) tgt_step = 0; // 转换为 HSECapTypeDef 枚举 //例:2pf->挡位0 实际 寄存器值 0x08-> 0+8=8 uint8_t target_enum = (tgt_step < 4) ? (tgt_step + 8) : (tgt_step - 4); PRINT("Current enum: %d | Delta Steps: %+d | Target Enum: %d\n", current_enum, delta_steps, target_enum); // 5. 写入寄存器 if (target_enum != current_enum) { PRINT("Change Enum: %d -> %d\n", current_enum, target_enum); HSECFG_Capacitance(target_enum); PRINT("After Adjustment Load Cap: %d pF\n",get_curr_cap_load()); } PRINT("===== Cap Adjustment End =====\n"); }
frequency_calibration.h
#ifndef FREQUENCY_CALIBRATION_H #define FREQUENCY_CALIBRATION_H #ifdef __cplusplus extern "C" { #endif #include "CONFIG.h" #include "HAL.h" void Adjust_Cap_LOAD (int32_t frequency); #ifdef __cplusplus } #endif #endif
以上就是频偏调整的方法与思路。

浙公网安备 33010602011771号