/* `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; }