board.c 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. #include "iot_board.h"
  2. #include "dac.h"
  3. #include "dac_njw1195.h"
  4. #include "driver/gpio.h"
  5. #include "esp_adc/adc_oneshot.h"
  6. #include "esp_log.h"
  7. #include "esp_check.h"
  8. #include "esp_sleep.h"
  9. static const char TAG[] = "MusicBoxBoard";
  10. #define DAC_XSMT_GPIO 19
  11. #define BAT_ADC_GPIO 36
  12. #define BAT_ADC_CHAN ADC_CHANNEL_0
  13. #define CHG_FULL_GPIO 34
  14. #define CHG_ING_GPIO 35
  15. #define BAT_CAL_K 0.008635f // Empirically calibrated: Raw 2103 -> 18.16V (18.16 / 2103)
  16. #define BAT_CELLS 5
  17. #define BAT_VCELL_MIN 3.0f
  18. #define BAT_VCELL_MAX 4.2f
  19. static bool s_board_initialized = false;
  20. static adc_oneshot_unit_handle_t s_adc_handle = NULL;
  21. typedef struct {
  22. float voltage;
  23. int percentage;
  24. } bat_curve_t;
  25. // Professional Li-ion 18650 discharge curve mapping (light load)
  26. // 已根据常见的 18650 锂电池放电平台进行了优化修正
  27. static const bat_curve_t s_bat_curve[] = {
  28. {4.15f, 100}, // 满电 (预留一点电压降)
  29. {4.08f, 90},
  30. {4.00f, 80},
  31. {3.93f, 70},
  32. {3.87f, 60},
  33. {3.82f, 50}, // 常见的 50% 放电平台中点
  34. {3.79f, 40},
  35. {3.75f, 30},
  36. {3.70f, 20},
  37. {3.60f, 10}, // 低于 3.6V 电量下降极快
  38. {3.40f, 5},
  39. {3.20f, 0} // 保护板关断临界点
  40. };
  41. #define BAT_CURVE_POINTS (sizeof(s_bat_curve) / sizeof(s_bat_curve[0]))
  42. esp_err_t iot_board_init(void) {
  43. if (s_board_initialized) {
  44. return ESP_OK;
  45. }
  46. ESP_LOGI(TAG, "Initializing Music Box board...");
  47. // 1. Initialize DAC_XSMT GPIO
  48. gpio_config_t xsmt_conf = {
  49. .pin_bit_mask = (1ULL << DAC_XSMT_GPIO),
  50. .mode = GPIO_MODE_OUTPUT,
  51. .pull_up_en = GPIO_PULLUP_DISABLE,
  52. .pull_down_en = GPIO_PULLDOWN_DISABLE,
  53. .intr_type = GPIO_INTR_DISABLE,
  54. };
  55. ESP_ERROR_CHECK(gpio_config(&xsmt_conf));
  56. gpio_set_level(DAC_XSMT_GPIO, 1); // Unmute external ES/DAC
  57. // 2. Register and initialize NJW1195AV DAC
  58. dac_register(&dac_njw1195_ops);
  59. esp_err_t err = dac_init(NULL);
  60. if (err != ESP_OK) {
  61. ESP_LOGE(TAG, "Failed to initialize NJW1195AV DAC: %s", esp_err_to_name(err));
  62. return err;
  63. }
  64. // 3. Initialize Battery ADC
  65. adc_oneshot_unit_init_cfg_t unit_cfg = {
  66. .unit_id = ADC_UNIT_1,
  67. };
  68. ESP_ERROR_CHECK(adc_oneshot_new_unit(&unit_cfg, &s_adc_handle));
  69. adc_oneshot_chan_cfg_t chan_cfg = {
  70. .atten = ADC_ATTEN_DB_12,
  71. .bitwidth = ADC_BITWIDTH_12,
  72. };
  73. ESP_ERROR_CHECK(adc_oneshot_config_channel(s_adc_handle, BAT_ADC_CHAN, &chan_cfg));
  74. s_board_initialized = true;
  75. ESP_LOGI(TAG, "Music Box board initialized successfully");
  76. return ESP_OK;
  77. }
  78. esp_err_t iot_board_deinit(void) {
  79. if (!s_board_initialized) {
  80. return ESP_OK;
  81. }
  82. dac_deinit();
  83. if (s_adc_handle) {
  84. adc_oneshot_del_unit(s_adc_handle);
  85. s_adc_handle = NULL;
  86. }
  87. s_board_initialized = false;
  88. return ESP_OK;
  89. }
  90. bool iot_board_is_init(void) {
  91. return s_board_initialized;
  92. }
  93. board_res_handle_t iot_board_get_handle(int id) {
  94. (void)id;
  95. return NULL;
  96. }
  97. const char *iot_board_get_info(void) {
  98. return BOARD_NAME;
  99. }
  100. void iot_board_init_lvgl_resources(void) {
  101. // No LVGL/Display
  102. }
  103. void board_power_off(void) {
  104. dac_njw1195_set_mute(true);
  105. esp_deep_sleep_start();
  106. }
  107. bool board_battery_read(int *percent, bool *charging) {
  108. if (!s_board_initialized || s_adc_handle == NULL) {
  109. return false;
  110. }
  111. // 5-sample median filter
  112. int samples[5];
  113. for (int i = 0; i < 5; i++) {
  114. adc_oneshot_read(s_adc_handle, BAT_ADC_CHAN, &samples[i]);
  115. }
  116. // Bubble sort
  117. for (int i = 0; i < 4; i++) {
  118. for (int j = 0; j < 4 - i; j++) {
  119. if (samples[j] > samples[j+1]) {
  120. int temp = samples[j];
  121. samples[j] = samples[j+1];
  122. samples[j+1] = temp;
  123. }
  124. }
  125. }
  126. int raw = samples[2]; // Median value
  127. bool is_charging = false;
  128. // Read real-time charger GPIOs (平时上拉高1,插入充电/充满触发为低0)
  129. int full = gpio_get_level(CHG_FULL_GPIO);
  130. int ing = gpio_get_level(CHG_ING_GPIO);
  131. // 只要有任意引脚被拉低(0),即代表接上了充电器 (充电中: full=1, ing=0; 充满: full=0, ing=1)
  132. is_charging = (full == 0 || ing == 0);
  133. if (charging) {
  134. *charging = is_charging;
  135. }
  136. float vbat = raw * BAT_CAL_K;
  137. float vcell_raw = vbat / BAT_CELLS;
  138. float vcell = vcell_raw;
  139. // Compensate for internal resistance voltage jump during fast charging
  140. if (is_charging && vcell > 3.0f && vcell < 4.2f) {
  141. vcell -= 0.1f; // Deduct approx 0.1V per cell charging offset (0.5V total)
  142. }
  143. // ESP_LOGI(TAG, "Battery Read -> Raw: %d, V_bat: %.2fV, V_cell: %.2fV (Compensated: %.2fV), Chg: %d",
  144. // raw, vbat, vcell_raw, vcell, is_charging);
  145. int pct = 0;
  146. if (vcell >= s_bat_curve[0].voltage) {
  147. pct = 100;
  148. } else if (vcell <= s_bat_curve[BAT_CURVE_POINTS - 1].voltage) {
  149. pct = 0;
  150. } else {
  151. for (int i = 0; i < BAT_CURVE_POINTS - 1; i++) {
  152. if (vcell <= s_bat_curve[i].voltage && vcell > s_bat_curve[i + 1].voltage) {
  153. float v_range = s_bat_curve[i].voltage - s_bat_curve[i + 1].voltage;
  154. float p_range = s_bat_curve[i].percentage - s_bat_curve[i + 1].percentage;
  155. float v_diff = vcell - s_bat_curve[i + 1].voltage;
  156. pct = s_bat_curve[i + 1].percentage + (int)(v_diff / v_range * p_range + 0.5f);
  157. break;
  158. }
  159. }
  160. }
  161. if (percent) {
  162. *percent = pct;
  163. }
  164. return true;
  165. }