文号
945268
阅读
1401
回复
0
收藏
0
本文在AI协助下创作,作者声明AI的作用仅限于局部修饰性、辅助性工作,文章内容的严肃性由作者负责。
ESP32 内部温度传感器与 ADC2 的深层硬件冲突
2025/07/03
原创
IP:海南
关键词:ESP32,温度传感,ADC

摘要:

本文详细记录了一次针对 ESP32 芯片内部温度传感器失灵问题的复杂排查过程。该问题表现为:在 ADC2 通道被使用后,内部温度传感器的读数会固定在一个错误的数值(默认无效值 128°F / 53.33°C)。

本文从初步的现象分析入手,经历了对官方驱动、HAL 层代码的深入探查,最终发现问题根源在于 Arduino 核心层为初代 ESP32 调用了一个未文档化的 ROM 函数,该函数与 ADC2 驱动之间存在硬件寄存器层面的冲突。

本文提供了导致问题产生的分析链条,并给出了应用层的稳定解决方案。

1. 问题背景与初步现象

在开发风信子项目所下属的架构验证板时,笔者遇到了一个棘手的现象。

该板集成有电池电压检测与芯片温度检测两项基本感知功能。

电压方面,将电池通过分压电路的分压,接入 GPIO12 (ADC2_CH5),通过 ADC 读数进行简单转换;

温度方面,则使用固化于 ESP32 内部只读存储器的接口:

temprature_sens_read()


简化的代码流程如下:


#include <driver/adc.h>
#include <esp_adc_cal.h>

// 电压测量相关的宏定义
#define VOLTAGE_TEST_R1 10000
#define VOLTAGE_TEST_R2 1000
#define VOLTAGE_ADC_WIDTH ADC_WIDTH_BIT_12
#define VOLTAGE_ADC_ATTEN ADC_ATTEN_DB_0
#define VOLTAGE_ADC_VREF_MV 1100
#define VOLTAGE_ADC_CHANNEL ADC2_CHANNEL_5

// 电压 ADC 全局变量
static esp_adc_cal_characteristics_t adc_chars;

// 声明内部温度传感器接口函数
extern "C" uint8_t temprature_sens_read();

// 初始化电压采集 ADC
void Initialize_Voltage_ADC() {
  adc2_config_channel_atten(VOLTAGE_ADC_CHANNEL, VOLTAGE_ADC_ATTEN);
  esp_adc_cal_characterize(ADC_UNIT_2, VOLTAGE_ADC_ATTEN, VOLTAGE_ADC_WIDTH, VOLTAGE_ADC_VREF_MV, &adc_chars);
}

// 采集当前电池电压
float Get_Battery_Voltage() {
  // 定义业务逻辑变量
  const int NUM_SAMPLES = 24;
  int valid_samples = 0;
  uint32_t total_raw_adc = 0;
  // 通过过采样获取准确 ADC 读数
  for (int i = 0; i < NUM_SAMPLES; i++) {
    int single_raw_adc;
    if (adc2_get_raw(VOLTAGE_ADC_CHANNEL, VOLTAGE_ADC_WIDTH, &single_raw_adc) == ESP_OK){
      total_raw_adc += single_raw_adc;
      valid_samples++;
    }
    delay(2);
  }
  // 如果采样全部失败
  if (valid_samples == 0) {
    Serial.println("ADC2 sample failed completely!");
    return -1145.14f;
  }
  // 计算原始 ADC 读数的平均值,并将其转换为电压值
  uint32_t average_raw_adc = total_raw_adc / valid_samples;
  uint32_t voltage_mv = esp_adc_cal_raw_to_voltage(average_raw_adc, &adc_chars);
  float bat_voltage = ((float)voltage_mv / 1000.0f) * (VOLTAGE_TEST_R1 + VOLTAGE_TEST_R2) / VOLTAGE_TEST_R2;
  // 返回时乘以一个校准系数
  return bat_voltage * 0.9518f;
}

// 程序入口
void setup() {
  Serial.begin(115200);
  delay(1000);
}

// 简化的流程
void loop() {
  // 读取数值并在串口打印
  float Chip_Temp = (temprature_sens_read() - 32) / 1.8;
  float BAT_Voltage = Get_Battery_Voltage();
  Serial.printf("Chip_Temp: %.2f | Batt_Volt: %.2f\n", Chip_Temp, BAT_Voltage);
  delay(2000);
}


核心症状:

  • 在未使用 adc2_get_raw() 函数对 ADC2 进行读取行为的情况下,调用 temprature_sens_read() 函数可以成功获取正确的芯片温度。


  • 一旦程序使用 adc2_get_raw() 函数对 ADC2 进行读取,后续对

     temprature_sens_read() 的调用都将失败,返回一个固定的错误值  128 。该值会在内部的温度读取方法中不经检查的换算,常见表现形态为 128℉ / 53.33℃ 


日志输出:

12:10:39.418 -> Chip_Temp: 51.11 | Batt_Volt: 2.13 
12:10:41.471 -> Chip_Temp: 53.33 | Batt_Volt: 2.32 
12:10:43.551 -> Chip_Temp: 53.33 | Batt_Volt: 2.33 
12:10:45.598 -> Chip_Temp: 53.33 | Batt_Volt: 2.32


2. 初步排查与错误的方向

在发现温度传感器数值异常之后,通过注释操作缩小了诱因范围,并最终定位于读取电压值的操作。


最初,因对 ESP32 硬件设计的不了解,认为内置温度传感器同样使用 ADC2 ,怀疑是二者访问共享 ADC 资源与时钟时产生的竞态问题,因此使用了 FreeRTOS 提供的互斥锁(Mutex)来保护 ADC 相关的操作区。然而,即便有互斥锁的保护,问题依旧稳定复现。这表明,这并非简单的软件层面的资源竞争,而是更深层次的问题。

接下来查询资料,发现温度传感器实际使用的是芯片内部的 Sigma‑Delta ADC 通道,理应不会与外部的 ADC2 产生交集。于是笔者开始怀疑是因为 temprature_sens_read() 相对过时,与新版函数存在非预期的干涉行为,开始了接口升级的不归路。经历了数十次的代码重构与 IDF 版本切换,最终才意识到,笔者手上的 ESP32 完全不支持新版函数😭😭😭😭,兜兜转转最终还是回到了原点。


在软件上的研究似乎已经到达尽头,接下来笔者将排查方向转向了已知的 ESP32 硬件限制。

根据乐鑫官方文档,当 WiFi 启动后,ADC2 的使用会受限。随后笔者尝试关闭 WiFi 功能,但问题仍未得到解决,该因素随后即被排除。

#include <WiFi.h>

void setup() {
  Serial.begin(115200);
  delay(1000);
  WiFi.mode(WIFI_OFF);   // 关闭 Wi-Fi
  btStop();              // 关闭 蓝牙
}


但在排查 WiFi 因素的过程中,笔者在乐鑫官方的 GitHub Issue 中发现了一个与 WiFi 有关的内置温度传感器问题。(XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX)


虽然该 issue 使用的是新版接口函数,且具体行为是读取失败导致看门狗超时,看似与本文所碰到的问题关联不大。但能够查阅到的与 ADC2 和内置温度传感器沾边的资料属实过少,笔者便仔细地进行了阅读。


其中关于 硬件寄存器被意外重置 的讨论引起了我的注意:

The issue is that WiFi resets these three registers

SENS.sar_tctrl.tsens_power_up

SENS.sar_tctrl.tsens_power_up_force

SENS.sar_tctrl.tsens_xpd_force


WiFi 功能与 ADC2 的冲突很大一部分就是 WiFi 使用了 ADC2 ,并对其寄存器资源的使用产生影响。WiFi 会莫名其妙的对 tsens(即温度传感器)的寄存器进行重置。


既然 WiFi 与 ADC2 存在使用冲突,那么对温度传感器寄存器的非预期重置行为很可能也来自于 ADC2 实例的运行。


经过进一步查证,笔者再次修正了一个关键的错误认知:

ESP32 的内部温度传感器与外部 ADC2 通道,并非使用独立的 ADC 模块,而是分时复用了同一个 SAR ADC (Successive Approximation Register ADC) 硬件单元。


这就是冲突的根源。当 adc2_get_raw() 被调用时,ADC2 驱动会获得 SAR ADC 模块的控制权,并配置相关寄存器(如 SENS_SAR_TSENS_CTRL_REG)以适应外部电压的测量。问题在于,在测量结束后,ADC2 驱动没有将这些寄存器恢复到内部温度传感器所期望的状态。


随后我对相关寄存器状态进行了检查,果然,在运行 ADC2 实例后,这些寄存器确实遭到了更改,当程序再次调用 temprature_sens_read()  时,这个 ROM 函数发现其赖以工作的硬件环境已被破坏,导致运行失败,返回了固定的错误码 128。温度传感器进入无效状态。


由此可以推定:ADC2 实例的运行可能会对其它寄存器产生影响,造成非预期行为。由 WiFi 所造成的温度传感器问题也来自于 ADC2 实例。


根据目前所排查的信息,只基本确认其会对内置温度传感器产生影响,而其他可能造成的非预期行为还有待研究。


3. 最终解决方案

继续阅读 issue:

Since the temperature sensor is accessed in multiple locations (temp_sensor and the WiFi stack), it needs to be protected by a critical section.

However, in order for this to work, the code needs to guarantee in all locations that it also restores the register state after reading the sensor.


Long story short, it turns out that WiFi does not restore the tsens registers to their previous state after using it – I tested it with IDF 4.4 before IDF 4.5 came out.


Therefore: temperature_sensor_ll_get_raw_value needs to also ensure that the temperature sensor is powered up that is, that all the registers are set correctly. If the sensors do need to be powered up, the task then needs be delayed for the output of temperature sensor to stabilize. Critical sections should not delay or wait, so you need to repeatedly exit and enter the section until the state is how you need it to be. Since the core issue is the WiFi code (closed-source) and it needs to properly restore the state of the temperature sensor, I didn’t bother making it work with critical sections, but ……


解决方案指向修改处于 ESP-IDF SDK 中的一个底层 HAL 文件 (Hardware Abstraction Layer) ——  temperature_sensor_ll.h ,通过在读取温度前强制重置相关硬件寄存器来解决冲突。

这是一个新版的函数,目前难以检索到关于该文件的信息,但可以确定的是曾存在于 5.2.3 版本的新系列芯片的 ESP-IDF SDK中,目前可能遭到删除。(?存疑)

既然无法修改固化在 ROM 中的 temprature_sens_read() 函数,也无法修改 ADC2 的驱动行为,那么最直接的解决方案就是在调用这个温度测量接口之前,手动将硬件寄存器强制恢复到正确状态。

这是一个看似正确的方向,不过笔者的脑子一时没转过弯来,然后在项目环境中翻箱倒柜的寻找 temperature_sensor_ll.h ,甚至做了两次全盘扫描。

后来转念一想,改寄存器在哪不是改啊,于是实验了最终解决方案,并成功解决了这个深层的硬件冲突问题:

// 保护性的封装函数,用于在读取温度前手动修复硬件状态
float safeTemperatureRead() {
    
  // 操作寄存器,强制开启温度传感器
  SET_PERI_REG_MASK(SENS_SAR_MEAS_WAIT2_REG, SENS_FORCE_XPD_SAR_M);
  SET_PERI_REG_BITS(SENS_SAR_TSENS_CTRL_REG, SENS_TSENS_CLK_DIV, 10, SENS_TSENS_CLK_DIV_S);
  CLEAR_PERI_REG_MASK(SENS_SAR_TSENS_CTRL_REG, SENS_TSENS_POWER_UP);
  CLEAR_PERI_REG_MASK(SENS_SAR_TSENS_CTRL_REG, SENS_TSENS_DUMP_OUT);
  SET_PERI_REG_MASK(SENS_SAR_TSENS_CTRL_REG, SENS_TSENS_POWER_UP_FORCE);
  SET_PERI_REG_MASK(SENS_SAR_TSENS_CTRL_REG, SENS_TSENS_POWER_UP);
  // 等待一小段时间让传感器稳定
  vTaskDelay(pdMS_TO_TICKS(10));
  return temperatureRead();
}


日志显示正常,温度值不再为无效的53.33

00:52:13.135 -> Chip_Temp: 48.33 | Batt_Volt: 4.13 
00:52:15.281 -> Chip_Temp: 48.78 | Batt_Volt: 4.15 
00:52:17.694 -> Chip_Temp: 49.44 | Batt_Volt: 4.11 
00:52:19.927 -> Chip_Temp: 50.00 | Batt_Volt: 4.33


后记


虽然笔者在文章中所写的步骤和思路似乎少而简单,但实际上中间还有大量已经遗忘的试错过程和深度挖掘过程。该 BUG 从开始研究到研究结束之间的工作量共计约15小时。


老实说,笔者对于 ESP32 的基本认知了解仍然不足,先前也没有如此深入的排查问题。在解决这个冲突的过程中算是对该系列的基本问题有了清晰的了解,也略窥其运行逻辑底层,得到了不少的经验:


  • 硬件手册的重要性

    ESP32 的外设之间存在着文档中未必详尽的深层硬件耦合,理解其共用的底层模块(如 SAR ADC)是解决此类问题的关键。

  • 抽象层的代价

    Arduino 核心层等封装库在提供便利的同时,也可能隐藏底层的复杂性和实现差异。当遇到疑难问题时,必须有勇气深入其源代码一探究竟。

  • ROM 函数的存在

    需要意识到 ESP32 中存在一些无法修改的、固化在硬件里的功能,当遇到幽灵问题时,这可能是一个排查方向。

  • 变通的解决思路

    当无法通过打补丁的方式修复底层问题时,通过在上层进行预处理和后处理来封装一个健壮的接口,也是一种行之有效的解决策略。


希望这份报告能为遇到类似问题的开发者提供一份有价值的参考。

笔记
{{note.content}}
{{n.user.username}}
{{fromNow(n.toc)}} {{n.status === noteStatus.disabled ? "已屏蔽" : ""}} {{n.status === noteStatus.unknown ? "正在审核" : ""}} {{n.status === noteStatus.deleted ? '已删除' : ''}}
  • 编辑
  • 删除
  • {{n.status === 'disabled' ? "解除屏蔽" : "屏蔽" }}
我也是有底线的