#include "iot_board.h" #include "dac.h" #include "dac_njw1195.h" #include "driver/gpio.h" #include "esp_adc/adc_oneshot.h" #include "esp_log.h" #include "esp_check.h" #include "esp_sleep.h" static const char TAG[] = "MusicBoxBoard"; #define DAC_XSMT_GPIO 19 #define BAT_ADC_GPIO 36 #define BAT_ADC_CHAN ADC_CHANNEL_0 #define CHG_FULL_GPIO 34 #define CHG_ING_GPIO 35 #define BAT_CAL_K 0.008635f // Empirically calibrated: Raw 2103 -> 18.16V (18.16 / 2103) #define BAT_CELLS 5 #define BAT_VCELL_MIN 3.0f #define BAT_VCELL_MAX 4.2f static bool s_board_initialized = false; static adc_oneshot_unit_handle_t s_adc_handle = NULL; typedef struct { float voltage; int percentage; } bat_curve_t; // Professional Li-ion 18650 discharge curve mapping (light load) // 已根据常见的 18650 锂电池放电平台进行了优化修正 static const bat_curve_t s_bat_curve[] = { {4.15f, 100}, // 满电 (预留一点电压降) {4.08f, 90}, {4.00f, 80}, {3.93f, 70}, {3.87f, 60}, {3.82f, 50}, // 常见的 50% 放电平台中点 {3.79f, 40}, {3.75f, 30}, {3.70f, 20}, {3.60f, 10}, // 低于 3.6V 电量下降极快 {3.40f, 5}, {3.20f, 0} // 保护板关断临界点 }; #define BAT_CURVE_POINTS (sizeof(s_bat_curve) / sizeof(s_bat_curve[0])) esp_err_t iot_board_init(void) { if (s_board_initialized) { return ESP_OK; } ESP_LOGI(TAG, "Initializing Music Box board..."); // 1. Initialize DAC_XSMT GPIO gpio_config_t xsmt_conf = { .pin_bit_mask = (1ULL << DAC_XSMT_GPIO), .mode = GPIO_MODE_OUTPUT, .pull_up_en = GPIO_PULLUP_DISABLE, .pull_down_en = GPIO_PULLDOWN_DISABLE, .intr_type = GPIO_INTR_DISABLE, }; ESP_ERROR_CHECK(gpio_config(&xsmt_conf)); gpio_set_level(DAC_XSMT_GPIO, 1); // Unmute external ES/DAC // 2. Register and initialize NJW1195AV DAC dac_register(&dac_njw1195_ops); esp_err_t err = dac_init(NULL); if (err != ESP_OK) { ESP_LOGE(TAG, "Failed to initialize NJW1195AV DAC: %s", esp_err_to_name(err)); return err; } // 3. Initialize Battery ADC adc_oneshot_unit_init_cfg_t unit_cfg = { .unit_id = ADC_UNIT_1, }; ESP_ERROR_CHECK(adc_oneshot_new_unit(&unit_cfg, &s_adc_handle)); adc_oneshot_chan_cfg_t chan_cfg = { .atten = ADC_ATTEN_DB_12, .bitwidth = ADC_BITWIDTH_12, }; ESP_ERROR_CHECK(adc_oneshot_config_channel(s_adc_handle, BAT_ADC_CHAN, &chan_cfg)); s_board_initialized = true; ESP_LOGI(TAG, "Music Box board initialized successfully"); return ESP_OK; } esp_err_t iot_board_deinit(void) { if (!s_board_initialized) { return ESP_OK; } dac_deinit(); if (s_adc_handle) { adc_oneshot_del_unit(s_adc_handle); s_adc_handle = NULL; } s_board_initialized = false; return ESP_OK; } bool iot_board_is_init(void) { return s_board_initialized; } board_res_handle_t iot_board_get_handle(int id) { (void)id; return NULL; } const char *iot_board_get_info(void) { return BOARD_NAME; } void iot_board_init_lvgl_resources(void) { // No LVGL/Display } void board_power_off(void) { dac_njw1195_set_mute(true); esp_deep_sleep_start(); } bool board_battery_read(int *percent, bool *charging) { if (!s_board_initialized || s_adc_handle == NULL) { return false; } // 5-sample median filter int samples[5]; for (int i = 0; i < 5; i++) { adc_oneshot_read(s_adc_handle, BAT_ADC_CHAN, &samples[i]); } // Bubble sort for (int i = 0; i < 4; i++) { for (int j = 0; j < 4 - i; j++) { if (samples[j] > samples[j+1]) { int temp = samples[j]; samples[j] = samples[j+1]; samples[j+1] = temp; } } } int raw = samples[2]; // Median value bool is_charging = false; // Read real-time charger GPIOs (平时上拉高1,插入充电/充满触发为低0) int full = gpio_get_level(CHG_FULL_GPIO); int ing = gpio_get_level(CHG_ING_GPIO); // 只要有任意引脚被拉低(0),即代表接上了充电器 (充电中: full=1, ing=0; 充满: full=0, ing=1) is_charging = (full == 0 || ing == 0); if (charging) { *charging = is_charging; } float vbat = raw * BAT_CAL_K; float vcell_raw = vbat / BAT_CELLS; float vcell = vcell_raw; // Compensate for internal resistance voltage jump during fast charging if (is_charging && vcell > 3.0f && vcell < 4.2f) { vcell -= 0.1f; // Deduct approx 0.1V per cell charging offset (0.5V total) } // ESP_LOGI(TAG, "Battery Read -> Raw: %d, V_bat: %.2fV, V_cell: %.2fV (Compensated: %.2fV), Chg: %d", // raw, vbat, vcell_raw, vcell, is_charging); int pct = 0; if (vcell >= s_bat_curve[0].voltage) { pct = 100; } else if (vcell <= s_bat_curve[BAT_CURVE_POINTS - 1].voltage) { pct = 0; } else { for (int i = 0; i < BAT_CURVE_POINTS - 1; i++) { if (vcell <= s_bat_curve[i].voltage && vcell > s_bat_curve[i + 1].voltage) { float v_range = s_bat_curve[i].voltage - s_bat_curve[i + 1].voltage; float p_range = s_bat_curve[i].percentage - s_bat_curve[i + 1].percentage; float v_diff = vcell - s_bat_curve[i + 1].voltage; pct = s_bat_curve[i + 1].percentage + (int)(v_diff / v_range * p_range + 0.5f); break; } } } if (percent) { *percent = pct; } return true; }