- ntc.{h,c}(宿主机可测):分压端 mV → Ω → NCP18XH103F03RB 的 R-T 表插值;
开路 / 短路 / 表外一律「读不到」(t_thermal 因此满速)
- thermal_esp.{h,c}(仅 IDF):ADC1 oneshot + 曲线校准读两路 NTC;两个 PCNT 单元数 TACH 边沿
- app_main:task_thermal 任务体、t_thermal_init 放在上电恢复之前、status 带热管理快照
- test_ntc.c 4 例;全工程 429 例全绿;版本 0.1.0-a16
- 板 B:两路 NTC 38.2 / 34.0 °C;风扇两路 rpm 0 报停转(拉满电流不涨,疑未插风扇,待确认)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
171 行
6.5 KiB
C
171 行
6.5 KiB
C
/* `t_thermal` 回调的 ESP 实现。契约见 thermal_esp.h;判据在 t_thermal.c(宿主机测过),
|
||
换算在 ntc.c(同上)。**这里只放驱动胶水,不许出现判据。** */
|
||
#include "thermal_esp.h"
|
||
|
||
#include "board_pins.h"
|
||
#include "driver/pulse_cnt.h"
|
||
#include "esp_adc/adc_cali.h"
|
||
#include "esp_adc/adc_cali_scheme.h"
|
||
#include "esp_adc/adc_oneshot.h"
|
||
#include "esp_log.h"
|
||
#include "ntc.h"
|
||
#include "t_thermal.h"
|
||
|
||
static const char *TAG = "thermal";
|
||
|
||
/* 上臂电源的名义值。比例式测量,偏 1 % 只带来 ~0.6 °C(ntc.h),对风扇闭环无所谓,不值得多占一路 ADC 去量它。 */
|
||
#define THERM_VREF_MV 3300
|
||
|
||
/* 一次读几次取平均。ADC 单次噪声 ±10 mV 量级 ≈ ±0.3 °C;8 次之后可以忽略。1 s 一回,开销无所谓。 */
|
||
#define THERM_AVG 8
|
||
|
||
/* PCNT 计数器是 16 位有符号的;开 `accum_count` + 在上限放观察点之后,驱动会把溢出
|
||
累进 `pcnt_unit_get_count()` 的返回值里(int,回绕后按 uint32 用即可)。 */
|
||
#define TACH_LIMIT 30000
|
||
|
||
/* 毛刺滤波:TACH 最快 ~6000 RPM × 2 PPR = 200 Hz,周期 5 ms;1 µs 的滤波远在它下面,
|
||
但能把 Q6 开关 25 kHz PWM 串进来的尖刺挡掉。 */
|
||
#define TACH_GLITCH_NS 1000
|
||
|
||
static const int k_ntc_pin[T_THERMAL_NTC_COUNT] = { BOARD_PIN_NTC1, BOARD_PIN_NTC2 };
|
||
static const int k_tach_pin[T_THERMAL_FAN_COUNT] = {
|
||
[THERM_FAN_IN] = BOARD_PIN_FAN_TACH_IN,
|
||
[THERM_FAN_OUT] = BOARD_PIN_FAN_TACH_OUT,
|
||
};
|
||
|
||
static adc_oneshot_unit_handle_t s_adc;
|
||
static adc_channel_t s_ch[T_THERMAL_NTC_COUNT];
|
||
static adc_cali_handle_t s_cali[T_THERMAL_NTC_COUNT];
|
||
static bool s_adc_ok[T_THERMAL_NTC_COUNT];
|
||
static int32_t s_last_mv[T_THERMAL_NTC_COUNT] = { -1, -1 };
|
||
|
||
static pcnt_unit_handle_t s_pcnt[T_THERMAL_FAN_COUNT];
|
||
static bool s_pcnt_ok[T_THERMAL_FAN_COUNT];
|
||
|
||
static hal_err_t adc_up(void)
|
||
{
|
||
const adc_oneshot_unit_init_cfg_t ucfg = { .unit_id = ADC_UNIT_1 };
|
||
if (adc_oneshot_new_unit(&ucfg, &s_adc) != ESP_OK) {
|
||
return HAL_ERR_STATE;
|
||
}
|
||
hal_err_t first = HAL_OK;
|
||
for (int i = 0; i < T_THERMAL_NTC_COUNT; i++) {
|
||
adc_unit_t unit;
|
||
/* 由引脚反查通道,不写死「CH3/CH4」:board_pins.h 是引脚的唯一出处,
|
||
而且这一步顺带证明了它确实落在 ADC1 上(ADC2 在 WiFi 开着时读不了)。 */
|
||
if (adc_oneshot_io_to_channel(k_ntc_pin[i], &unit, &s_ch[i]) != ESP_OK ||
|
||
unit != ADC_UNIT_1) {
|
||
ESP_LOGE(TAG, "IO%d 不在 ADC1 上", k_ntc_pin[i]);
|
||
first = (first == HAL_OK) ? HAL_ERR_ARG : first;
|
||
continue;
|
||
}
|
||
/* 12 dB 衰减:量程到 ~3.1 V,覆盖整张 R-T 表(−40 °C 时 3.14 V 会略微饱和,
|
||
那一端本来就不关心 —— 低温不需要风扇)。 */
|
||
const adc_oneshot_chan_cfg_t ccfg = { .atten = ADC_ATTEN_DB_12,
|
||
.bitwidth = ADC_BITWIDTH_DEFAULT };
|
||
if (adc_oneshot_config_channel(s_adc, s_ch[i], &ccfg) != ESP_OK) {
|
||
first = (first == HAL_OK) ? HAL_ERR_STATE : first;
|
||
continue;
|
||
}
|
||
const adc_cali_curve_fitting_config_t kcfg = {
|
||
.unit_id = ADC_UNIT_1,
|
||
.chan = s_ch[i],
|
||
.atten = ADC_ATTEN_DB_12,
|
||
.bitwidth = ADC_BITWIDTH_DEFAULT,
|
||
};
|
||
if (adc_cali_create_scheme_curve_fitting(&kcfg, &s_cali[i]) != ESP_OK) {
|
||
/* eFuse 里没有校准值(不该发生在 WROOM-1 上):没校准的读数误差可达
|
||
±100 mV ≈ ±3 °C,宁可报「读不到」让风扇满速,也不给一个看着正常的错数。 */
|
||
ESP_LOGE(TAG, "NTC%d 的 ADC 校准不可用", i + 1);
|
||
first = (first == HAL_OK) ? HAL_ERR_STATE : first;
|
||
continue;
|
||
}
|
||
s_adc_ok[i] = true;
|
||
}
|
||
return first;
|
||
}
|
||
|
||
static hal_err_t pcnt_up(int idx)
|
||
{
|
||
const pcnt_unit_config_t ucfg = {
|
||
.low_limit = -TACH_LIMIT,
|
||
.high_limit = TACH_LIMIT,
|
||
.flags.accum_count = 1,
|
||
};
|
||
pcnt_unit_handle_t u = NULL;
|
||
if (pcnt_new_unit(&ucfg, &u) != ESP_OK) {
|
||
return HAL_ERR_STATE;
|
||
}
|
||
const pcnt_glitch_filter_config_t fcfg = { .max_glitch_ns = TACH_GLITCH_NS };
|
||
(void)pcnt_unit_set_glitch_filter(u, &fcfg);
|
||
|
||
/* ⚠️ 只数边沿、不看电平:Q9/Q10 反相(t_thermal.h 硬件第 3 条),
|
||
数上升沿还是下降沿都一样 —— 每个 TACH 脉冲正好一个。板上 R753/R757 上拉,不开内部上拉。 */
|
||
const pcnt_chan_config_t ccfg = { .edge_gpio_num = k_tach_pin[idx], .level_gpio_num = -1 };
|
||
pcnt_channel_handle_t c = NULL;
|
||
if (pcnt_new_channel(u, &ccfg, &c) != ESP_OK ||
|
||
pcnt_channel_set_edge_action(c, PCNT_CHANNEL_EDGE_ACTION_INCREASE,
|
||
PCNT_CHANNEL_EDGE_ACTION_HOLD) != ESP_OK ||
|
||
pcnt_unit_add_watch_point(u, TACH_LIMIT) != ESP_OK ||
|
||
pcnt_unit_enable(u) != ESP_OK || pcnt_unit_clear_count(u) != ESP_OK ||
|
||
pcnt_unit_start(u) != ESP_OK) {
|
||
return HAL_ERR_STATE;
|
||
}
|
||
s_pcnt[idx] = u;
|
||
s_pcnt_ok[idx] = true;
|
||
return HAL_OK;
|
||
}
|
||
|
||
hal_err_t thermal_esp_init(void)
|
||
{
|
||
hal_err_t first = adc_up();
|
||
for (int i = 0; i < T_THERMAL_FAN_COUNT; i++) {
|
||
const hal_err_t e = pcnt_up(i);
|
||
if (e != HAL_OK) {
|
||
ESP_LOGE(TAG, "TACH%d(IO%d)的 PCNT 起不来", i, k_tach_pin[i]);
|
||
if (first == HAL_OK) {
|
||
first = e;
|
||
}
|
||
}
|
||
}
|
||
return first;
|
||
}
|
||
|
||
bool thermal_esp_temp(void *ctx, int idx, int32_t *milli_c)
|
||
{
|
||
(void)ctx;
|
||
if (idx < 0 || idx >= T_THERMAL_NTC_COUNT || !s_adc_ok[idx]) {
|
||
return false;
|
||
}
|
||
int32_t sum = 0;
|
||
for (int n = 0; n < THERM_AVG; n++) {
|
||
int mv = 0;
|
||
if (adc_oneshot_get_calibrated_result(s_adc, s_cali[idx], s_ch[idx], &mv) != ESP_OK) {
|
||
s_last_mv[idx] = -1;
|
||
return false;
|
||
}
|
||
sum += mv;
|
||
}
|
||
s_last_mv[idx] = sum / THERM_AVG;
|
||
return ntc_mc_from_mv(s_last_mv[idx], THERM_VREF_MV, milli_c);
|
||
}
|
||
|
||
bool thermal_esp_tach(void *ctx, int idx, uint32_t *edges)
|
||
{
|
||
(void)ctx;
|
||
if (idx < 0 || idx >= T_THERMAL_FAN_COUNT || !s_pcnt_ok[idx]) {
|
||
return false;
|
||
}
|
||
int v = 0;
|
||
if (pcnt_unit_get_count(s_pcnt[idx], &v) != ESP_OK) {
|
||
return false;
|
||
}
|
||
*edges = (uint32_t)v; /* 累计值;t_thermal 用 uint32 相减,回绕安全 */
|
||
return true;
|
||
}
|
||
|
||
int32_t thermal_esp_last_mv(int idx)
|
||
{
|
||
return (idx >= 0 && idx < T_THERMAL_NTC_COUNT) ? s_last_mv[idx] : -1;
|
||
}
|