1. 项目概述

Temperature-nrf51822 是一个面向 Nordic Semiconductor nRF51822 蓝牙低功耗(BLE)系统级芯片(SoC)的轻量级温度测量库。其核心目标并非外接热敏电阻或数字温度传感器,而是 直接读取 nRF51822 片上温度传感器(On-chip Temperature Sensor)的原始 ADC 值,并通过片内校准数据将其转换为摄氏度(°C)的物理温度值 。该库不依赖于任何操作系统(如 FreeRTOS),亦不强制要求使用 Nordic 的 SoftDevice 协议栈,可运行于裸机(Bare-metal)环境,亦可无缝集成至基于 RTOS 的复杂固件中。

nRF51822 的片上温度传感器是其模拟外围模块(ANALOG PERIPHERAL)的一部分,本质上是一个与芯片硅基温度呈线性关系的带隙(Bandgap)电压源,经由内部 6-bit SAR ADC 进行数字化。该 ADC 并非独立外设,而是复用自 ADC 模块的通道 0(AIN0),其参考电压(VREF)固定为 0.6 V,且采样时序、启动方式与常规 ADC 通道存在显著差异。因此,直接调用标准 HAL_ADC 接口无法正确读取温度值——这是本库存在的根本工程动因。

该库的价值在于:它封装了 nRF51822 温度传感特有的硬件操作序列、校准系数提取逻辑与线性化计算模型,将底层寄存器操作(如 TEMP->TASKS_START , TEMP->EVENTS_DATARDY )和片内非易失性存储器(UICR)中预烧录的校准常数( UICR->TEMP )抽象为简洁、健壮的 C 函数接口。对于嵌入式工程师而言,这意味着无需反复查阅 nRF51822 Product Specification (v3.1) 第 22 章 “Analog Peripherals” 与第 34 章 “Memory Layout”,即可在数分钟内将芯片自身温升监控能力集成至产品固件中,用于电池管理、射频功率动态调整、故障预警等关键场景。

2. 硬件原理与校准机制

2.1 片上温度传感器工作原理

nRF51822 的温度传感器并非一个独立的模拟器件,而是利用芯片工艺中带隙基准电压(V BG )对温度的已知漂移特性来实现测温。其核心公式为:

V_TEMP = V_BG * (1 + α * (T - T_REF))

其中:

  • V_TEMP 是温度传感器输出的模拟电压;
  • V_BG 是 0°C 下的带隙基准电压(典型值约 1.25 V);
  • α 是温度系数(典型值约 2.5 mV/°C);
  • T 是当前硅片温度(°C);
  • T_REF 是参考温度(通常为 25°C)。

该模拟电压 V_TEMP 被路由至 ADC 模块的 AIN0 输入端。ADC 使用固定的 0.6 V 内部参考电压进行量化,其数字输出值 ADC_VALUE 与输入电压的关系为:

ADC_VALUE = (V_TEMP / 0.6) * 63   // 6-bit ADC, max value = 63

将上述两式联立,即可推导出 ADC_VALUE T 的线性关系。然而,由于制造工艺偏差,每颗芯片的 V_BG α 均存在个体差异,因此必须通过出厂校准来消除系统误差。

2.2 出厂校准数据存储与读取

Nordic 在芯片出厂测试阶段,会在两个特定温度点(通常是 25°C 和 75°C 或 25°C 和 85°C)下测量其 ADC_VALUE ,并将这两个测量值以 16-bit 整数形式写入芯片的用户信息配置寄存器(UICR)区域。具体地址为 UICR->TEMP ,这是一个 32-bit 寄存器,其低 16-bit 存储 CALIBRATION_VALUE_25C (25°C 时的 ADC 值),高 16-bit 存储 CALIBRATION_VALUE_75C (75°C 时的 ADC 值)。

在固件中读取该校准数据的代码如下(需确保 UICR 已被正确映射):

// 定义 UICR TEMP 寄存器地址
#define NRF_UICR_BASE         (0x10001000UL)
#define UICR_TEMP             (*(volatile uint32_t*)(NRF_UICR_BASE + 0x008))

// 读取校准值
uint16_t cal_25c = (uint16_t)(UICR_TEMP & 0xFFFF);
uint16_t cal_75c = (uint16_t)((UICR_TEMP >> 16) & 0xFFFF);

// 验证校准值有效性(若为默认值 0xFFFFFFFF,则表示未校准)
if ((cal_25c == 0xFFFF) || (cal_75c == 0xFFFF)) {
    // 处理错误:校准数据无效,可返回默认值或触发告警
}

2.3 温度计算模型

基于两点校准法, Temperature-nrf51822 库采用线性插值模型将实时读取的 ADC_VALUE 转换为摄氏温度 T 。其数学推导如下:

T1 = 25°C , T2 = 75°C , ADC1 = cal_25c , ADC2 = cal_75c ,则斜率 m 和截距 b 为:

m = (T2 - T1) / (ADC2 - ADC1) = 50 / (ADC2 - ADC1)
b = T1 - m * ADC1

因此,任意 ADC 值 adc_val 对应的温度为:

T = m * adc_val + b
  = 50 * (adc_val - ADC1) / (ADC2 - ADC1) + 25

此即为库中 temperature_get() 函数的核心计算逻辑。该模型假设在 25°C 至 75°C 区间内, V_TEMP T 的关系是严格线性的,这在 nRF51822 的规格范围内是足够精确的(典型精度 ±2°C)。

3. API 接口详解

Temperature-nrf51822 库提供了一组极简但完备的 C 函数接口,所有函数均声明于头文件 temperature_nrf51822.h 中,并在 temperature_nrf51822.c 中实现。其设计遵循“一次初始化,多次读取”的嵌入式惯用模式。

3.1 初始化函数

void temperature_init(void);

功能 :初始化温度传感器外设,为后续读取做准备。
执行动作

  • 启用温度传感器时钟( NRF_CLOCK->EVENTS_HFCLKSTARTED 等待后,设置 TEMP->POWER = 1 )。
  • 配置 ADC 参考电压为内部 0.6 V( NRF_ADC->CONFIG = (NRF_ADC->CONFIG & ~ADC_CONFIG_REFSEL_Msk) | ADC_CONFIG_REFSEL_Internal )。
  • 关键步骤 :将 ADC 通道选择为 AIN0(温度传感器专用通道),并禁用所有其他通道。
  • 清除可能存在的挂起事件( TEMP->EVENTS_DATARDY = 0 )。

注意事项

  • 此函数 必须 在首次调用 temperature_get() 之前执行,且仅需执行一次。
  • 若系统已启用 SoftDevice,其可能已接管部分时钟和 ADC 配置,此时需确保 temperature_init() 在 SoftDevice 初始化之后调用,或查阅 SoftDevice 文档确认其是否允许应用层访问温度传感器。

3.2 温度读取函数

int32_t temperature_get(void);

功能 :触发一次温度测量,并返回以毫摄氏度(m°C)为单位的整数温度值。
返回值

  • 成功: [T_MIN * 1000, T_MAX * 1000] 范围内的整数,例如 25123 表示 25.123°C
  • 失败: INT32_MIN (即 -2147483648 ),表示测量超时或校准数据无效。

执行流程

  1. 触发采样 :向 TEMP->TASKS_START 寄存器写入 1 ,启动 ADC 转换。
  2. 等待完成 :轮询 TEMP->EVENTS_DATARDY 寄存器,直至其值变为 1 。为防止死循环,库内置一个最大等待计数(例如 0x100000 次循环)。
  3. 读取结果 :从 TEMP->VALUE 寄存器读取 6-bit ADC 值(实际为 32-bit 寄存器,仅低 6-bit 有效)。
  4. 校准计算 :调用内部函数 temperature_calculate(adc_value) ,代入前述线性公式计算最终温度。
  5. 返回结果 :将计算得到的浮点温度乘以 1000 并转换为 int32_t 返回。

关键参数说明

参数/寄存器 地址/位域 说明
TEMP->TASKS_START 0x4000C000 1 启动测量;硬件自动清零。
TEMP->EVENTS_DATARDY 0x4000C100 读为 1 表示 TEMP->VALUE 已更新。
TEMP->VALUE 0x4000C500 32-bit 寄存器,低 6-bit 为有效 ADC 值(0-63)。
UICR->TEMP 0x10001008 32-bit 寄存器,低 16-bit 为 CALIBRATION_VALUE_25C ,高 16-bit 为 CALIBRATION_VALUE_75C

3.3 辅助函数(可选)

bool temperature_is_calibrated(void);
int32_t temperature_get_raw(void);
  • temperature_is_calibrated() :一个便捷的查询函数,用于检查 UICR->TEMP 是否包含有效的校准数据(即非 0xFFFF )。在产品量产前的校准流程中非常有用。
  • temperature_get_raw() :跳过校准计算,直接返回 TEMP->VALUE 的原始 ADC 值。可用于调试、绘制温度-ADC 曲线或实现自定义的非线性补偿算法。

4. 典型应用示例

4.1 裸机环境下的周期性温度监控

以下是一个完整的、可在 main() 函数中直接运行的示例,用于每 2 秒打印一次芯片温度:

#include "nrf.h"
#include "temperature_nrf51822.h"
#include "nrf_delay.h"
#include <stdio.h>

// 假设已实现一个简单的 UART printf(如使用 SEGGER RTT 或 UART HAL)
extern void uart_printf(const char* format, ...);

int main(void) {
    // 1. 初始化系统时钟(根据你的 SDK 版本,可能是 NRF_CLOCK->... 或 sd_clock_hfclk_request())
    // 2. 初始化 UART(用于打印)

    // 3. 初始化温度传感器
    temperature_init();

    while (1) {
        int32_t temp_mdeg = temperature_get();
        if (temp_mdeg != INT32_MIN) {
            float temp_deg = (float)temp_mdeg / 1000.0f;
            uart_printf("Chip Temp: %.3f °C\r\n", temp_deg);
        } else {
            uart_printf("Temp read failed!\r\n");
        }

        // 延迟 2 秒
        nrf_delay_ms(2000);
    }
}

4.2 FreeRTOS 环境下的任务化温度采集

在多任务系统中,将温度采集封装为一个独立任务是更优的工程实践,可避免阻塞主任务。以下示例创建了一个优先级为 2 的温度采集任务,其通过队列将温度数据发送给另一个处理任务:

#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
#include "temperature_nrf51822.h"

// 定义一个用于传递温度数据的队列
QueueHandle_t xTempQueue;

// 温度采集任务
void vTempReadTask(void *pvParameters) {
    const TickType_t xDelay2Seconds = pdMS_TO_TICKS(2000);
    int32_t temp_mdeg;

    for (;;) {
        temp_mdeg = temperature_get();
        if (temp_mdeg != INT32_MIN) {
            // 将温度值(毫摄氏度)发送到队列
            xQueueSend(xTempQueue, &temp_mdeg, portMAX_DELAY);
        }
        vTaskDelay(xDelay2Seconds);
    }
}

// 温度处理任务(示例:简单打印)
void vTempProcessTask(void *pvParameters) {
    int32_t temp_mdeg;

    for (;;) {
        // 从队列接收温度数据,超时 100ms
        if (xQueueReceive(xTempQueue, &temp_mdeg, pdMS_TO_TICKS(100)) == pdPASS) {
            float temp_deg = (float)temp_mdeg / 1000.0f;
            // 假设此处调用一个串口打印函数
            printf("RTOS Task: Chip Temp = %.2f °C\r\n", temp_deg);
        }
    }
}

// 在 FreeRTOS 初始化完成后调用
void start_temperature_tasks(void) {
    // 创建队列,深度为 5,每个元素大小为 sizeof(int32_t)
    xTempQueue = xQueueCreate(5, sizeof(int32_t));
    if (xTempQueue != NULL) {
        xTaskCreate(vTempReadTask, "TempRead", configMINIMAL_STACK_SIZE, NULL, 2, NULL);
        xTaskCreate(vTempProcessTask, "TempProc", configMINIMAL_STACK_SIZE, NULL, 1, NULL);
    }
}

4.3 与 BLE 服务的集成(高级用例)

在 BLE 应用中,可将芯片温度作为自定义特征值(Characteristic)暴露给手机 App。当手机发起读请求时,在 on_read_req 回调中调用 temperature_get() 即可:

// 假设已定义好 BLE 服务和特征值句柄
ble_gatts_char_handles_t m_temp_char_handles;

// BLE 读取回调函数
void on_temperature_char_read(ble_gatts_evt_read_t * p_evt) {
    if (p_evt->handle == m_temp_char_handles.value_handle) {
        int32_t temp_mdeg = temperature_get();
        if (temp_mdeg != INT32_MIN) {
            uint8_t temp_bytes[4];
            // 将 int32_t 转为小端字节序
            temp_bytes[0] = (uint8_t)(temp_mdeg & 0xFF);
            temp_bytes[1] = (uint8_t)((temp_mdeg >> 8) & 0xFF);
            temp_bytes[2] = (uint8_t)((temp_mdeg >> 16) & 0xFF);
            temp_bytes[3] = (uint8_t)((temp_mdeg >> 24) & 0xFF);

            // 通过 GATT 接口将数据发送给客户端
            ble_gatts_value_t gatts_value;
            gatts_value.len = sizeof(temp_bytes);
            gatts_value.offset = 0;
            gatts_value.p_value = temp_bytes;
            sd_ble_gatts_value_set(m_conn_handle, m_temp_char_handles.value_handle, &gatts_value);
        }
    }
}

5. 集成与移植指南

5.1 与 Nordic SDK 的兼容性

Temperature-nrf51822 库的设计初衷是与 Nordic SDK(特别是 SDK 8.x 和 9.x)完全兼容。其关键在于正确处理 SDK 对底层外设的封装。

  • SDK 8.x (S110/S120) :SDK 8 的 nrf_drv_adc 驱动 不支持 温度传感器通道。因此,必须绕过该驱动,直接操作 TEMP 外设寄存器,这正是本库所做的事情。只需确保在 sdk_config.h 中禁用 NRF_DRV_ADC_ENABLED ,以避免潜在冲突。
  • SDK 9.x (S130/S132) :SDK 9 引入了 nrf_drv_temp 驱动,其功能与本库高度重合。若项目已使用 SDK 9,建议优先采用官方驱动,因其经过更严格的测试。但本库仍可作为学习 nrf_drv_temp 实现原理的绝佳范本,其核心逻辑(校准数据读取、线性计算)与官方驱动一致。

5.2 与其他 HAL 库的共存

本库的操作对象是 TEMP 外设,与通用的 ADC UART SPI 等外设在硬件上是隔离的。因此,它可以与 STM32 HAL、CMSIS-DSP 等任何第三方 HAL 库共存, 前提是 这些库不尝试去修改 TEMP 相关的寄存器或时钟配置。在 temperature_init() 中,库会显式地设置 TEMP->POWER ADC->CONFIG ,这不会影响其他外设的正常工作。

5.3 移植到其他 nRF 芯片

虽然本库专为 nRF51822 设计,但其核心思想可轻松移植至同系列芯片:

  • nRF52832/nRF52840 :这些芯片的温度传感器架构已发生重大变化,采用了独立的 TEMP 外设和 12-bit ADC,并拥有更复杂的校准机制(三点校准)。直接移植不可行,但 Temperature-nrf51822 的代码结构(初始化、触发、等待、计算)是优秀的模板。
  • nRF51422 :与 nRF51822 属于同一代,其温度传感器寄存器布局和校准机制完全相同,因此本库可 零修改 地直接使用。

6. 性能与精度分析

6.1 测量时间开销

一次完整的 temperature_get() 调用的时间主要由三部分构成:

  • ADC 转换时间 :nRF51822 的 6-bit ADC 典型转换时间为 12 µs。
  • 轮询等待时间 :这是最主要的开销。由于 ADC 转换是异步的,软件必须轮询 EVENTS_DATARDY 。在 16 MHz CPU 主频下,一次轮询循环约需 3-4 个周期(约 250 ns)。即使在最坏情况下等待 0x100000 次,总时间也仅为 0x100000 * 250ns ≈ 2.5 ms
  • 计算开销 :一次整数除法和乘加运算,在 Cortex-M0 上耗时约数十微秒,可忽略不计。

因此,单次测量的总时间稳定在 2.5 ms 以内 ,这对于绝大多数嵌入式应用(如每秒一次的健康监测)是完全可接受的。

6.2 精度与误差来源

nRF51822 片上温度传感器的典型精度为 ±2°C ,其误差来源主要有:

  • 校准点误差 :出厂校准本身存在 ±0.5°C 的测试误差。
  • 线性模型误差 :在宽温度范围(-40°C 至 +85°C)内, V_TEMP T 的关系并非完美线性,本库的两点校准模型会引入残余误差。
  • PCB 热耦合 :芯片温度反映的是其自身结温,而非环境温度。PCB 上的电源芯片、LED 等发热元件会通过铜箔将热量传导至 nRF51822,导致读数偏高。这是工程实践中最常见的“误差”,需通过合理的 PCB 布局(如将 nRF51822 远离大功率器件、增加散热焊盘)来最小化。

为获得更高精度,可在产品最终组装后,于恒温箱中进行两点(如 25°C 和 50°C)的实测校准,并将新的校准值烧录至 UICR->TEMP ,从而覆盖出厂校准。

7. 故障排查与常见问题

7.1 temperature_get() 总是返回 INT32_MIN

这是最常见的问题,原因及解决方案如下:

  • 校准数据缺失 UICR->TEMP 0xFFFFFFFF 。解决方案:使用 nrfjprog --memwr 0x10001008 --val 0x00190019 (示例值)手动烧录校准值,或在量产编程时一并写入。
  • 外设未初始化 :忘记调用 temperature_init() 。解决方案:在 main() 开头添加该调用。
  • 时钟未就绪 TEMP 外设依赖于高频晶振(HFCLK)。若 NRF_CLOCK->EVENTS_HFCLKSTARTED 未置位, TEMP->POWER = 1 将无效。解决方案:在 temperature_init() 中加入对 EVENTS_HFCLKSTARTED 的等待。

7.2 读数异常波动或为固定值

  • ADC 通道冲突 :确认没有其他代码(如 nrf_drv_adc )正在使用 AIN0 通道。解决方案:检查所有 ADC 初始化代码,确保 ADC_CONFIG_PSEL 不指向 AIN0。
  • 电源噪声 TEMP 传感器对电源噪声敏感。解决方案:在 VDD VDDH 引脚附近增加高质量的 100nF 陶瓷电容,并确保 AVDD 供电干净。

7.3 与 SoftDevice 冲突

当使用 S110/S120 SoftDevice 时,其可能在后台使用 ADC 进行 RSSI 测量。虽然 SoftDevice 会自动管理 ADC 资源,但在极端情况下仍可能导致冲突。解决方案:在调用 temperature_get() 前,调用 sd_power_system_off() 进入系统关闭模式(此时所有外设停止),但这会中断 BLE 连接。更优方案是改用 nrf_drv_temp (SDK 9+)或在 SoftDevice 的 APP_TIMER 回调中进行测量,避开其 ADC 活动高峰期。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐