前言
现在为止也开发了许多杰理,中科蓝讯TWS蓝牙耳机、音响项目 SDK的案子,在调试案子时不断的向前辈们学习到了很多关于蓝牙音响、蓝牙TWS耳机专业的知识。想在这里做一个学习汇总,方便各位同行和对杰理芯片SDK感兴趣的小伙伴们学习;
本章详细讲解中科蓝讯SDK开发-BT897X、BT891X、AB573X、AB572X 系列 NTC过温保护功能教程
1、NTC硬件电路原理
1.1 什么是 NTC
NTC(Negative Temperature Coefficient)热敏电阻是一种电阻值随温度升高而降低的半导体器件。其核心参数:
| 参数 | 本工程值 | 含义 |
|---|---|---|
| R25 | 10KΩ | 25°C 时的标称阻值 |
| B 值 | 3380 | 材料常数(描述 R-T 曲线斜率) |
| 封装 | 贴片/环氧 | 根据产品形态选择 |
1.2 串联分压电路的基本原理
先抛开热敏电阻,看最基础的串联分压电路:两个电阻首尾相接串在电源和地之间,从中间节点引出输出电压。
V_in(电源电压) │ ┌───┴───┐ │ R1 │ └───┬───┘ │ ├──────── V_out(输出电压,即我们采样的点) │ ┌───┴───┐ │ R2 │ └───┬───┘ │ GND串联电路的三个基本事实:
- 电流相同:R1 和 R2 串联在同一条回路上,流过它们的电流 I 完全相同
- 欧姆定律:每个电阻两端的电压 U = I × R
- 电压相加:两个电阻上的电压加起来等于电源电压,U_R1 + U_R2 = V_in
分压公式三步推导:
| 步骤 | 求什么 | 公式 |
|---|---|---|
| ① | 回路总电阻 | R_total = R1 + R2 |
| ② | 回路电流 | I = V_in / (R1 + R2) |
| ③ | R2 上的电压(即输出) | V_out = I × R2 =V_in × R2 / (R1 + R2) |
一条直觉规律:串联电路中电压按阻值比例分配——谁的阻值大,谁分到的电压就多:
- R2 = R1 时:各分一半,V_out = V_in / 2
- R2 远大于 R1 时:R2 几乎分走大部分电压
- R2 远小于 R1 时:R2 只能分到小部分电压
数字例子(V_in = 3.7V,R1 = 100K):
| R2 阻值 | V_out = 3.7 × R2/(100K+R2) | 说明 |
|---|---|---|
| 368.7K | 2.91V | R2 大,分走大部分电压 |
| 100K | 1.85V | R2 = R1,恰好一半 |
| 22.4K | 0.68V | R2 小,只分到零头 |
1.3 从分压到测温:把下臂换成热敏电阻
现在把上面分压电路里的固定电阻 R2换成 NTC 热敏电阻(记作 R_ntc),R1 就是上拉电阻 R_pu,中间节点接 ADC 引脚——这就是 NTC 测温电路:
VDD │ ┌───┴───┐ │ R_pu │ ← 上臂:固定上拉电阻(本工程 100K) └──────┘ │ ├──────── V_adc → 接 ADC 引脚(PB0) │ ┌───┴───┐ │ R_ntc │ ← 下臂:热敏电阻(阻值随温度变化) └──────┘ │ GND代入 1.2 节的分压公式:
V_adc = VDD × R_ntc / (R_pu + R_ntc)关键变化:R_ntc 不再是固定值,而是随温度变化(NTC 特性:温度越高阻值越小)。于是温度一变,下臂分到的电压就跟着变:
| 温度变化 | R_ntc | 下臂分到的电压 | V_adc | ADC 读数 |
|---|---|---|---|---|
| 温度升高 | 变小 | 变少 | 降低 | 变小 |
| 温度降低 | 变大 | 变多 | 升高 | 变大 |
用本工程的数值感受(VDD=3.7V,R_pu=100K):
| 温度 | R_ntc | V_adc | 含义 |
|---|---|---|---|
| 0°C | 368.7K | 2.91V | 冷 → 阻值大 → 电压高 |
| 25°C | 100K | 1.85V | 常温 → 恰好一半 |
| 60°C | 22.4K | 0.68V | 热 → 阻值小 → 电压低 |
测温链条就此成立:
温度 → R_ntc(R-T 特性) → V_adc(分压公式) → ADC 读数(量化)反过来用:测出 ADC 读数 → 反推 R_ntc → 查 R-T 表得温度。这就是“用 saradc 采样热敏电阻分压值推算温度”的全部原理。
最后一步量化:ADC 把 V_adc 换算成 0~255 的数字:
ADC = V_adc / VDD × 255 = R_ntc / (R_pu + R_ntc) × 255注意分子 V_adc 和参考 VDD 都正比于电源电压,相除后 VDD 被约掉——所以 ADC 读数只取决于电阻比值,与电池电量无关(比率式测量,详见 1.4 节)。
1.4 SARADC 采样精度
AB573x 的 SARADC 硬件分辨率为10-bit(0~1023),软件提供两套读取接口:
| 接口 | 精度 | 范围 | 定义(api_saradc.h L25-26) |
|---|---|---|---|
saradc_get_value10(ch) | 10-bit | 0~1023 | saradc_cb.value[ch](原始值) |
saradc_get_value8(ch) | 8-bit | 0~255 | (u8)(saradc_cb.value[ch] >> 2)(右移 2 位) |
NTC 功能使用的是 8-bit 接口(system.c L63):
if (saradc_get_value8(ADCCH_NTC) <= xcfg_cb.ntc_thd_val)因此阈值公式中的满量程是255(而非 1023)。ntc_thd_val在 xcfg.h 中定义为u8类型,范围 0~255,与 8-bit 接口匹配。
其他信息:
| 特性 | 值 |
|---|---|
| 采样通道 | ADCCH_PB0(channel 0,config.h L697 定义) |
| 参考电压 | VDD(电源电压) |
| 转换方式 | ping-pong:每 5ms 读取上次结果并启动下次转换 |
由于 ADC 参考电压与上拉电阻供电都是 VDD(比率式测量),ADC 读数仅取决于电阻分压比值,与电池电压高低无关。电池从 4.2V 放电到 3.0V,同一温度下 ADC 读数不变,阈值无需随电压修正。
扩展:若需要更高精度的温度检测,可自行修改
sys_ntc_check()使用saradc_get_value10()接口,此时公式中的 255 换成 1023,阈值范围变为 0~1023。但需注意xcfg_cb.ntc_thd_val是 u8,最大只能存 255,若要用 10-bit 需额外扩展配置字段或用编译期宏代替。
1.5 为什么选 100K 上拉 + 100K NTC(阻抗匹配原理)
分压电路的输出灵敏度(NTC 每变化 1Ω,ADC 变化多少)为:
dV/dR_ntc = VDD × R_pu / (R_pu + R_ntc)²对 R_pu 求极值可知:当 R_pu = R_ntc 时灵敏度最大,此时分压点恰好在 VDD/2(ADC 中点 128)。
选择 100K 上拉 + 100K NTC 的三个理由:
| 理由 | 说明 |
|---|---|
| ① 阻抗匹配 | 25°C 时 R_ntc=100K=R_pu,ADC 读中点 128,常温区分辨率最高 |
| ② 低功耗 | 回路电流 I = VDD/(100K+100K) ≈ 3.7V/200K ≈18μA,对电池友好 |
| ③ 自热小 | 电流小 → NTC 自身发热小 → 测温更准 |
1.6 若用 10K 上拉配 100K NTC 会怎样(失配分析)
如果把上拉换成 10K(内部或外部),而 NTC 仍是 100K,则 R_pu ≠ R_ntc,严重失配。对比两种方案的 ADC 读数(NTC 均为 100K B4250):
| 温度 | R_ntc (KΩ) | 方案A:100K上拉 | 方案B:10K上拉 |
|---|---|---|---|
| 0°C | 368.7 | 201 | 248 |
| 10°C | 212.8 | 174 | 244 |
| 20°C | 127.5 | 143 | 236 |
| 25°C | 100.0 | 128(中点) | 232(饱和区) |
| 30°C | 79.0 | 113 | 226 |
| 40°C | 50.5 | 86 | 213 |
| 50°C | 33.2 | 64 | 196 |
| 60°C | 22.4 | 47 | 176 |
| 70°C | 15.4 | 34 | 155 |
结论:方案B(10K 上拉)在常温区(0~30°C)ADC 被压在 226~248 的饱和区,20°C 的变化只换来 22 个 ADC 计数;而方案A(100K 上拉)同样区间用了 113~201 共 88 个计数,分辨率约为方案B的 4 倍。失配时分压点被 R_ntc “拉死”在接近 VDD 的位置,温度变化几乎不改变 ADC 读数,等效温度误差(每 1 个 ADC 计数代表多少°C)显著变大。
所以:上拉电阻必须与 NTC 在目标温度附近的阻值同数量级。本工程 NTC 是 100K,上拉就必须用 100K,不能用 10K。
2. 阈值计算方法(外部 100K 上拉方案)
2.1 公式
ntc_thd_val = R_ntc / (R_pu + R_ntc) × 255本工程 R_pu = 100K = 100000Ω,代入后:
ntc_thd_val = R_ntc / (100000 + R_ntc) × 255| 符号 | 含义 | 来源 |
|---|---|---|
ntc_thd_val | 填入 代码中的ADC 的阈值(0~255) | 计算结果 |
R_ntc | 目标保护温度下 NTC 的阻值(Ω) | 查 NTC datasheet 的 R-T 表(100K B4250) |
R_pu | 上拉电阻 =100000Ω | 原理图 R4 |
255 | ADC 采样精度(8-bit 满量程) | 固定值 |
触发逻辑:当 ADC 采样值≤ ntc_thd_val时,温度已达或超过保护温度,执行关机保护。
2.2 计算步骤
假设要在60°C触发保护:
| 步骤 | 操作 | 结果 |
|---|---|---|
| ① 确定目标温度 | 保护温度点 | 60°C |
| ② 查 R-T 表 | 100K B4250 在 60°C 的阻值 | R_ntc ≈ 22.2KΩ = 22200Ω |
| ③ 代入公式 | 22200 / (100000 + 22200) × 255 | = 22200 / 122200 × 255 =46.32569 |
| ④ 取整填入 | 四舍五入 | ntc_thd_val = 47 |
3. 软件配置层
3.1 编译期开关(config.h L693-697)
/***************************************************************************** * Module : NTC预警关机功能配置 ******************************************************************************/ #define USER_NTC 0 // 总开关:0=关闭,1=编译NTC代码 #define ADCCH_NTC ADCCH_PB0 // NTC使用的ADC通道(PB0 = channel 0)USER_NTC:编译期总开关。设为 0 时,所有 NTC 代码都不会被编译(#if USER_NTC包裹)。ADCCH_NTC:选择 NTC 连接的 GPIO 对应的 SARADC 通道。可选ADCCH_PB0~PB7/ADCCH_PE4~PE7(见config_define.hL62-70)。
3.2 出厂配置(earphone.setting + xcfg.xm)
即使USER_NTC=1编译通过,还有运行期开关(Downloader 工具配置):
| 工具名称 | 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|---|
| 是否使能NTC | xcfg_cb.ntc_en | bool | False | 运行期总开关 |
| NTC使用内部10K上拉 | xcfg_cb.ntc_user_inner_pu | bool | False | 使能内部上拉 |
| NTC阈值 | xcfg_cb.ntc_thd_val | u8 | 100 | 8-bit ADC 阈值(≤此值触发) |
对应xcfg.xm定义(L26-28):
config(CHECK, "是否使能NTC", "是否使能NTC", NTC_EN, 0); config(CHECK, "NTC使用内部10K上拉", "...使能程序中的宏USER_NTC后才有效", NTC_USER_INNER_PU, 0, NTC_EN); config(BYTE, "NTC阈值", "NTC阈值", NTC_THD_VAL, 0, 255, 100, NTC_EN);对应xcfg.h位域(L27-29):
u32 ntc_en : 1; //是否使能NTC u32 ntc_user_inner_pu : 1; //NTC使用内部10K上拉 u8 ntc_thd_val; //NTC阈值(0~255)3.3 配置联动关系(双重开关)
┌─────────────────────────────────────────────────────────────────┐ │ USER_NTC = 1 ? │ │ ↓ YES ↓ NO │ │ 代码被编译 所有NTC代码不存在 │ │ ↓ │ │ xcfg_cb.ntc_en = True ? │ │ ↓ YES ↓ False │ │ 运行期检测生效 sys_ntc_check()立即return 0 │ │ ↓ │ │ xcfg_cb.ntc_user_inner_pu ? │ │ ↓ True ↓ False │ │ adcch_io_pu10k_enable() 使用外部上拉(硬件设计) │ │ ↓ │ │ xcfg_cb.ntc_thd_val = ? │ │ (与ADC采样值比较的阈值) │ └─────────────────────────────────────────────────────────────────┘要启用 NTC 保护,必须同时满足:
config.h中USER_NTC = 1(编译期)- Downloader 工具中勾选"是否使能NTC"(
ntc_en = True) - 正确配置阈值(
ntc_thd_val) - 硬件上 NTC 接到对应 ADC 引脚
4. 软件运行框架
4.1 完整数据流图
┌─────────────── 初始化阶段 ───────────────────────────────────────┐ │ │ │ sys_init() → bsp_saradc_init() → saradc_init() + saradc_start() │ │ → adkey_init() │ │ ├─ if(ntc_user_inner_pu) adcch_io_pu10k_enable() │ │ └─ if(ntc_en) adc_ch |= BIT(ADCCH_NTC) │ │ → saradc_set_channel(adc_ch) //使能NTC ADC通道 │ │ → sys_set_tmr_enable(1, 1) //启动5ms/1ms定时器 │ │ │ └───────────────────────────────────────────────────────────────────┘ ↓ 每5ms循环 ┌─────────────── 采样阶段(SARADC 硬件) ──────────────────────────┐ │ │ │ SARADC 自动转换(ping-pong 模式): │ │ 每轮 bsp_saradc_process(): │ │ ① saradc_is_finish() → 检查上轮转换完成 │ │ ② saradc_get_result() → 读取结果到 saradc_cb.value[] │ │ ③ saradc_start() → 启动下轮转换 │ │ 结果:saradc_cb.value[ADCCH_NTC] = 10-bit ADC值 │ │ │ └───────────────────────────────────────────────────────────────────┘ ↓ 同一次5ms中断 ┌─────────────── 检测阶段(sys_ntc_check) ────────────────────────┐ │ │ │ usr_tmr5ms_thread() [system.c L99-195, 5ms中断上下文] │ │ ├─ bsp_key_scan() // L116: 含 bsp_saradc_process() │ │ │ └─ 读取ADC结果到 saradc_cb.value[] │ │ └─ sys_ntc_check() // L131-133: NTC检测 │ │ ├─ if(!ntc_en) return 0 // 未使能直接跳过 │ │ ├─ adc_val = saradc_get_value8(ADCCH_NTC) // 取8-bit值 │ │ ├─ if(adc_val <= ntc_thd_val) // ★超温判定 │ │ │ ├─ n_cnt++ // 去抖计数 │ │ │ └─ if(n_cnt >= 20) // 连续20次=100ms │ │ │ → sys_cb.pwroff_flag.ntc_ind = 1 // ★置标志 │ │ │ → return 1 │ │ └─ else: n_cnt = 0 // 恢复正常,清零计数 │ │ │ └───────────────────────────────────────────────────────────────────┘ ↓ 标志位被设置后 ┌─────────────── 执行阶段(pwroff_process) ───────────────────────┐ │ │ │ func_process() [func.c L534-651, MAIN线程每轮消息泵调用] │ │ └─ pwroff_process() [func.c L234-270] │ │ ├─ if(pwroff->all_flag != 0) // ★检查所有关机标志 │ │ │ ├─ charge_full_ind → tone_en=0 (无声关机) │ │ │ ├─ aux_insert_ind → tone_en=2 (AUX提示音) │ │ │ └─ else(含ntc_ind) → tone_en=1 (播放关机提示音) │ │ └─ func_cb.sta = FUNC_PWROFF // ★切换到关机态 │ │ │ │ func_run() while(1) → case FUNC_PWROFF: │ │ └─ func_pwroff(tone_en=1) [func_lowpwr.c L16-107] │ │ ├─ 播放 RES_IDX_POWEROFF 关机提示音 │ │ ├─ 断开蓝牙 func_bt_chk_off() │ │ ├─ 等待按键松开 │ │ ├─ dac_power_off() + bsp_saradc_exit() │ │ └─ sfunc_pwrdown(1) → lowpwr_pwroff_mode() → 关机 │ │ │ └───────────────────────────────────────────────────────────────────┘4.2 sys_ntc_check() 源码详解
代码讲解:
// system.c L54-76 #if USER_NTC AT(.com_text.ntc) u8 sys_ntc_check(void) { if (!xcfg_cb.ntc_en) { // ① 运行期开关检查(Downloader配置) return 0; } static int n_cnt = 0; // ② 去抖计数器(static保持跨调用) if (xcfg_cb.ntc_en) { if (saradc_get_value8(ADCCH_NTC) <= xcfg_cb.ntc_thd_val) { // ③ 核心判定 if (n_cnt >= 20) { // ④ 连续20次×5ms = 100ms 去抖 sys_cb.pwroff_flag.ntc_ind = 1; // ⑤ 置关机标志 return 1; } else { n_cnt++; // ⑥ 计数递增 } } else { n_cnt = 0; // ⑦ ADC恢复(温度回落),清零重来 } } return 0; } #endif去抖设计解读:
- 100ms 去抖(20×5ms):足够滤除 ADC 噪声/瞬间干扰
- "归零式"去抖:中间只要有一次不满足就清零重来(非"累计式"),确保只有持续超温才触发
- 一旦
ntc_ind=1,函数每次都返回 1(因为n_cnt >= 20持续为真),但标志已置位不影响
4.3 关机标志联合体
// system.h L27-44 struct pwroff_tag { volatile union { struct { u8 charge_full_ind : 1; // bit0: 充满关机 u8 ntc_ind : 1; // bit1: ★NTC过温关机 u8 low_bat_ind : 1; // bit2: 低电关机 u8 aux_insert_ind : 1; // bit3: AUX插入关机 u8 pwr_key_ind : 1; // bit4: 按键关机 u8 timeout_ind : 1; // bit5: 超时关机 u8 user_ind : 1; // bit6: APP/TWS等用户操作 }; u8 all_flag; // 任意一位置1 → all_flag != 0 }; u8 tone_en; // 关机提示音类型 ... };pwroff_process()(func.c L252)通过all_flag != 0一次性检测所有关机源。NTC 触发后:
tone_en = 1(播放正常关机提示音RES_IDX_POWEROFF)- 进入
FUNC_PWROFF→func_pwroff()→ 断蓝牙 → 关 DAC →sfunc_pwrdown()→ 芯片进入 Power Down 模式
4.4 调用时序图
时间轴 ──────────────────────────────────────────────────────────────→ 5ms中断: │────│────│────│────│────│────│────│... ↓ ↓ ↓ ↓ ↓ ↓ ↓ NTC检测: [正常][正常][超温][超温][超温]...[超温×20][触发!] n=0 n=0 n=1 n=2 n=3 n=20 ntc_ind=1 ↓ MAIN线程消息泵: func_process() ↓ pwroff_process() ↓ all_flag≠0 → FUNC_PWROFF ↓ func_pwroff(tone_en=1) ↓ 播放关机音 → 断BT → powerdown5. 充电模式下 NTC 保护缺失分析
5.1 问题本质
公版 SDK 设计:NTC 保护仅在非充电模式(耳机放电态)生效。充电模式下无 NTC 保护。
根因分析(两条路径都不工作):
| 条件 | 非充电模式 | 充电模式 (CHARGE_LOW_POWER_EN=0) | 充电模式 (CHARGE_LOW_POWER_EN=1) |
|---|---|---|---|
| 5ms 中断 | ✓ 正常触发 | ✓ 仍在触发 | ⚠ Timer0 重配为 RC2M 1ms,调度改变 |
| sys_ntc_check() | ✓ 被调用 | ✓ 仍被调用 | ⚠ 可能被调用但 SARADC 时钟切 RC |
| ntc_ind 被置位 | ✓ | ✓ 可以置位 | ⚠ 不确定 |
| pwroff_process() | ✓ 被 func_process 调用 | ✗ 不被调用 | ✗ 不被调用 |
| 关机动作执行 | ✓ | ✗ 永远不执行 | ✗ 永远不执行 |
关键断点:func_charge_process()(func_charge.c L134-175)有自己独立的while(1)循环:
void func_charge_process(void) { u32 counter = 0; while (1) { WDT_CLR(); delay_5ms(1); // 等待5ms(中断仍触发) u8 charge_sta = sfunc_charge_process(counter++); // 仅充电处理 if(charge_sta == CHAG_STA_OFF) { // 只检查"DC拔出" func_cb.sta = func_cb.last; break; } // ← 这里从不检查 pwroff_flag! // ← 从不调用 func_process() / pwroff_process()! } }即使 5ms 中断里sys_ntc_check()把ntc_ind置了 1,充电循环永远不读取该标志,所以保护动作永远不执行。
5.2 如何为充电模式添加 NTC 保护
在sfunc_charge_process()或func_charge_process()的循环中,添加 NTC 检测调用:
方案 A:在 sfunc_charge_process() 中添加(推荐)
// func_charge.c → sfunc_charge_process() 内添加 u8 sfunc_charge_process(u32 counter) { ... if ((counter % 20) == 0) { // 每100ms检测一次(counter每次+1=5ms) charge_process(); #if USER_NTC if (sys_ntc_check()) { // ★添加NTC检测 return CHAG_STA_OFF; // 超温 → 退出充电(或自定义处理) } #endif } ... }方案 B:在 func_charge_process() while循环中直接检查标志
while (1) { WDT_CLR(); delay_5ms(1); #if USER_NTC if (sys_cb.pwroff_flag.ntc_ind) { // ★检查NTC标志(5ms ISR中已置位) // 充电中NTC超温处理: // 方案1:停止充电 + 关机 sfunc_charge_exit(); sys_cb.pwroff.tone_en = 0; // 充电中不播提示音(喇叭可能未使能) func_cb.sta = FUNC_PWROFF; break; // 方案2:仅停止充电,不关机 // charge_off(); // sys_cb.pwroff_flag.ntc_ind = 0; // 清标志 // break; // 回到上个功能态 } #endif u8 charge_sta = sfunc_charge_process(counter++); ... }方案选择建议:
- 充电时超温通常是电池安全问题,建议停止充电 + 关机(方案 B + 方案 1)
- 若产品需要"停止充电但不关机"(如仅降低充电电流),可自定义处理逻辑
- 注意:充电模式下 DAC/提示音可能已被关闭(
dac_power_off()infunc_charge_enter),所以tone_en=0静默关机更合理
5.3 CHARGE_LOW_POWER_EN=1 时的额外注意
若启用了充电低功耗模式(CHARGE_LOW_POWER_EN=1):
- 系统时钟切换到 RC2M(2MHz)
- Timer0 重配为 1ms ISR(而非原来的 5ms)
- SARADC 时钟切换到 RC
func_charge_process()使用delay_us(350)而非delay_5ms(1)
此时:
usr_tmr5ms_thread()的调用频率可能改变(不再是精确 5ms)- SARADC 采样精度可能受 RC 时钟影响
- 若要在充电低功耗模式下做 NTC,需验证 ADC 采样准确性,或在
func_charge_process的 tight loop 中直接调用bsp_saradc_process()+sys_ntc_check()
制作不易!喜欢的小伙伴给个小赞赞!喜欢我的小伙伴点个关注!有不懂的地方和需要的资源随时问我哟!