| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195 |
- #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;
- }
|