From cdb025dd90d07b23143ff6e8b3cebc2e0acd71df Mon Sep 17 00:00:00 2001 From: Spotpear <136059044+Spotpear@users.noreply.github.com> Date: Sat, 8 Nov 2025 07:07:53 +0800 Subject: [PATCH] sp-esp32-s3-1.28-box (#1397) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 修复屏幕条纹 * 增加充电标识 --------- Co-authored-by: Spotpear --- main/boards/sp-esp32-s3-1.28-box/config.h | 4 + .../sp-esp32-s3-1.28-box/power_manager.h | 189 ++++++++++++++ .../sp-esp32-s3-1.28-box.cc | 240 +++++++++++++++--- 3 files changed, 400 insertions(+), 33 deletions(-) create mode 100644 main/boards/sp-esp32-s3-1.28-box/power_manager.h diff --git a/main/boards/sp-esp32-s3-1.28-box/config.h b/main/boards/sp-esp32-s3-1.28-box/config.h index 3dbea9a5..cde7dcb4 100644 --- a/main/boards/sp-esp32-s3-1.28-box/config.h +++ b/main/boards/sp-esp32-s3-1.28-box/config.h @@ -49,4 +49,8 @@ #define DISPLAY_SPI_SCLK_HZ (40 * 1000 * 1000) +// 电量检测相关引脚定义 +#define BATTERY_ADC_PIN GPIO_NUM_1 // 电池电压检测ADC引脚 +#define BATTERY_CHARGING_PIN GPIO_NUM_41 // 充电状态检测引脚 + #endif // _BOARD_CONFIG_H_ diff --git a/main/boards/sp-esp32-s3-1.28-box/power_manager.h b/main/boards/sp-esp32-s3-1.28-box/power_manager.h new file mode 100644 index 00000000..36e8ca97 --- /dev/null +++ b/main/boards/sp-esp32-s3-1.28-box/power_manager.h @@ -0,0 +1,189 @@ +#pragma once +#include +#include + +#include +#include +#include +#include +#include + +class PowerManager { +private: + esp_timer_handle_t timer_handle_; + std::function on_charging_status_changed_; + std::function on_low_battery_status_changed_; + + gpio_num_t charging_pin_; + std::vector adc_values_; + uint32_t battery_level_ = 0; + bool is_charging_ = false; + bool is_low_battery_ = false; + int ticks_ = 0; + const int kBatteryAdcInterval = 60; + const int kBatteryAdcDataCount = 3; + const int kLowBatteryLevel = 20; + + adc_oneshot_unit_handle_t adc_handle_; + adc_channel_t adc_channel_; + + void CheckBatteryStatus() { + // Get charging status + bool new_charging_status = gpio_get_level(charging_pin_) == 1; + if (new_charging_status != is_charging_) { + is_charging_ = new_charging_status; + if (on_charging_status_changed_) { + on_charging_status_changed_(is_charging_); + } + ReadBatteryAdcData(); + return; + } + + // 如果电池电量数据不足,则读取电池电量数据 + if (adc_values_.size() < kBatteryAdcDataCount) { + ReadBatteryAdcData(); + return; + } + + // 如果电池电量数据充足,则每 kBatteryAdcInterval 个 tick 读取一次电池电量数据 + ticks_++; + if (ticks_ % kBatteryAdcInterval == 0) { + ReadBatteryAdcData(); + } + } + + void ReadBatteryAdcData() { + int adc_value; + ESP_ERROR_CHECK(adc_oneshot_read(adc_handle_, adc_channel_, &adc_value)); + + // 将 ADC 值添加到队列中 + adc_values_.push_back(adc_value); + if (adc_values_.size() > kBatteryAdcDataCount) { + adc_values_.erase(adc_values_.begin()); + } + uint32_t average_adc = 0; + for (auto value : adc_values_) { + average_adc += value; + } + average_adc /= adc_values_.size(); + + // 定义电池电量区间 - 根据实际硬件调整这些值 + const struct { + uint16_t adc; + uint8_t level; + } levels[] = { + {1980, 0}, + {2081, 20}, + {2163, 40}, + {2250, 60}, + {2340, 80}, + {2480, 100} + }; + + // 低于最低值时 + if (average_adc < levels[0].adc) { + battery_level_ = 0; + } + // 高于最高值时 + else if (average_adc >= levels[5].adc) { + battery_level_ = 100; + } else { + // 线性插值计算中间值 + for (int i = 0; i < 5; i++) { + if (average_adc >= levels[i].adc && average_adc < levels[i+1].adc) { + float ratio = static_cast(average_adc - levels[i].adc) / (levels[i+1].adc - levels[i].adc); + battery_level_ = levels[i].level + ratio * (levels[i+1].level - levels[i].level); + break; + } + } + } + + // Check low battery status + if (adc_values_.size() >= kBatteryAdcDataCount) { + bool new_low_battery_status = battery_level_ <= kLowBatteryLevel; + if (new_low_battery_status != is_low_battery_) { + is_low_battery_ = new_low_battery_status; + if (on_low_battery_status_changed_) { + on_low_battery_status_changed_(is_low_battery_); + } + } + } + + ESP_LOGI("PowerManager", "ADC value: %d average: %ld level: %ld", adc_value, average_adc, battery_level_); + } + +public: + PowerManager(gpio_num_t charging_pin, adc_channel_t adc_channel) + : charging_pin_(charging_pin), adc_channel_(adc_channel) { + // 初始化充电引脚 + gpio_config_t io_conf = {}; + io_conf.intr_type = GPIO_INTR_DISABLE; + io_conf.mode = GPIO_MODE_INPUT; + io_conf.pin_bit_mask = (1ULL << charging_pin_); + io_conf.pull_down_en = GPIO_PULLDOWN_DISABLE; + io_conf.pull_up_en = GPIO_PULLUP_ENABLE; + gpio_config(&io_conf); + + // 创建电池电量检查定时器 + esp_timer_create_args_t timer_args = { + .callback = [](void* arg) { + PowerManager* self = static_cast(arg); + self->CheckBatteryStatus(); + }, + .arg = this, + .dispatch_method = ESP_TIMER_TASK, + .name = "battery_check_timer", + .skip_unhandled_events = true, + }; + ESP_ERROR_CHECK(esp_timer_create(&timer_args, &timer_handle_)); + ESP_ERROR_CHECK(esp_timer_start_periodic(timer_handle_, 100000)); + + // 初始化 ADC + adc_oneshot_unit_init_cfg_t init_config = { + .unit_id = ADC_UNIT_1, + .ulp_mode = ADC_ULP_MODE_DISABLE, + }; + ESP_ERROR_CHECK(adc_oneshot_new_unit(&init_config, &adc_handle_)); + + adc_oneshot_chan_cfg_t chan_config = { + .atten = ADC_ATTEN_DB_12, + .bitwidth = ADC_BITWIDTH_12, + }; + ESP_ERROR_CHECK(adc_oneshot_config_channel(adc_handle_, adc_channel_, &chan_config)); + } + + ~PowerManager() { + if (timer_handle_) { + esp_timer_stop(timer_handle_); + esp_timer_delete(timer_handle_); + } + if (adc_handle_) { + adc_oneshot_del_unit(adc_handle_); + } + } + + bool IsCharging() { + // 如果电量已经满了,则不再显示充电中 + if (battery_level_ == 100) { + return false; + } + return is_charging_; + } + + bool IsDischarging() { + // 没有区分充电和放电,所以直接返回相反状态 + return !is_charging_; + } + + uint8_t GetBatteryLevel() { + return battery_level_; + } + + void OnLowBatteryStatusChanged(std::function callback) { + on_low_battery_status_changed_ = callback; + } + + void OnChargingStatusChanged(std::function callback) { + on_charging_status_changed_ = callback; + } +}; \ No newline at end of file diff --git a/main/boards/sp-esp32-s3-1.28-box/sp-esp32-s3-1.28-box.cc b/main/boards/sp-esp32-s3-1.28-box/sp-esp32-s3-1.28-box.cc index a885feb3..77364b9d 100644 --- a/main/boards/sp-esp32-s3-1.28-box/sp-esp32-s3-1.28-box.cc +++ b/main/boards/sp-esp32-s3-1.28-box/sp-esp32-s3-1.28-box.cc @@ -24,9 +24,16 @@ #include "power_save_timer.h" #include #include +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "power_manager.h" #define TAG "Spotpear_ESP32_S3_1_28_BOX" +LV_FONT_DECLARE(font_puhui_16_4); +LV_FONT_DECLARE(font_awesome_16_4); + + class Cst816d : public I2cDevice { public: struct TouchPoint_t { @@ -37,17 +44,21 @@ public: Cst816d(i2c_master_bus_handle_t i2c_bus, uint8_t addr) : I2cDevice(i2c_bus, addr) { uint8_t chip_id = ReadReg(0xA3); ESP_LOGI(TAG, "Get chip ID: 0x%02X", chip_id); + last_chip_id_ = chip_id; read_buffer_ = new uint8_t[6]; } ~Cst816d() { - delete[] read_buffer_; + if (read_buffer_) { + delete[] read_buffer_; + read_buffer_ = nullptr; + } } void UpdateTouchPoint() { + if (!read_buffer_) return; ReadRegs(0x02, read_buffer_, 6); - if (read_buffer_[0] == 0xFF) - { + if (read_buffer_[0] == 0xFF) { read_buffer_[0] = 0x00; } tp_.num = read_buffer_[0] & 0x01; @@ -55,15 +66,44 @@ public: tp_.y = ((read_buffer_[3] & 0x0F) << 8) | read_buffer_[4]; } - const TouchPoint_t& GetTouchPoint() { + const TouchPoint_t& GetTouchPoint() const { return tp_; } + static bool Probe(i2c_master_bus_handle_t i2c_bus, uint8_t addr, uint8_t& chip_id) { + if (!i2c_bus) return false; + i2c_master_dev_handle_t dev = nullptr; + i2c_device_config_t cfg = { + .dev_addr_length = I2C_ADDR_BIT_LEN_7, + .device_address = addr, + .scl_speed_hz = 400 * 1000, + .scl_wait_us = 0, + .flags = { + .disable_ack_check = 0, + }, + }; + esp_err_t ret = i2c_master_bus_add_device(i2c_bus, &cfg, &dev); + if (ret != ESP_OK || dev == nullptr) { + return false; + } + uint8_t reg = 0xA3; + uint8_t id = 0; + ret = i2c_master_transmit_receive(dev, ®, 1, &id, 1, 100); + i2c_master_bus_rm_device(dev); + if (ret == ESP_OK) { + chip_id = id; + return true; + } + return false; + } + private: uint8_t* read_buffer_ = nullptr; TouchPoint_t tp_; + uint8_t last_chip_id_ = 0; }; + class CustomLcdDisplay : public SpiLcdDisplay { public: CustomLcdDisplay(esp_lcd_panel_io_handle_t io_handle, @@ -84,16 +124,18 @@ public: } }; + class Spotpear_ESP32_S3_1_28_BOX : public WifiBoard { private: - i2c_master_bus_handle_t codec_i2c_bus_; - i2c_master_bus_handle_t i2c_bus_; + i2c_master_bus_handle_t codec_i2c_bus_ = nullptr; + i2c_master_bus_handle_t i2c_bus_ = nullptr; Button boot_button_; - Display* display_; - esp_timer_handle_t touchpad_timer_; - Cst816d* cst816d_; - PowerSaveTimer* power_save_timer_; + Display* display_ = nullptr; + esp_timer_handle_t touchpad_timer_ = nullptr; + Cst816d* cst816d_ = nullptr; + PowerSaveTimer* power_save_timer_ = nullptr; esp_lcd_panel_handle_t panel_ = nullptr; + PowerManager* power_manager_ = nullptr; void InitializePowerSaveTimer() { rtc_gpio_init(GPIO_NUM_3); @@ -111,6 +153,12 @@ private: }); power_save_timer_->OnShutdownRequest([this]() { ESP_LOGI(TAG, "Shutting down"); + // 关闭ES8311音频编解码器 + auto codec = GetAudioCodec(); + if (codec) { + codec->EnableInput(false); + codec->EnableOutput(false); + } rtc_gpio_set_level(GPIO_NUM_3, 0); // 启用保持功能,确保睡眠期间电平不变 rtc_gpio_hold_en(GPIO_NUM_3); @@ -120,6 +168,17 @@ private: power_save_timer_->SetEnabled(true); } + void InitializePowerManager() { + power_manager_ = new PowerManager(BATTERY_CHARGING_PIN, ADC_CHANNEL_0); + power_manager_->OnChargingStatusChanged([this](bool is_charging) { + if (is_charging) { + power_save_timer_->SetEnabled(false); + } else { + power_save_timer_->SetEnabled(true); + } + }); + } + void InitializeCodecI2c() { // Initialize I2C peripheral i2c_master_bus_config_t i2c_bus_cfg = { @@ -127,12 +186,12 @@ private: .sda_io_num = AUDIO_CODEC_I2C_SDA_PIN, .scl_io_num = AUDIO_CODEC_I2C_SCL_PIN, .clk_source = I2C_CLK_SRC_DEFAULT, - .glitch_ignore_cnt = 7, - .intr_priority = 0, - .trans_queue_depth = 0, - .flags = { - .enable_internal_pullup = 1, - }, + // .glitch_ignore_cnt = 7, + // .intr_priority = 0, + // .trans_queue_depth = 0, + // .flags = { + // .enable_internal_pullup = 1, + // }, }; ESP_ERROR_CHECK(i2c_new_master_bus(&i2c_bus_cfg, &codec_i2c_bus_)); } @@ -151,18 +210,23 @@ private: .enable_internal_pullup = 1, }, }; - ESP_ERROR_CHECK(i2c_new_master_bus(&i2c_bus_cfg, &i2c_bus_)); + esp_err_t ret = i2c_new_master_bus(&i2c_bus_cfg, &i2c_bus_); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "i2c_new_master_bus failed: %s", esp_err_to_name(ret)); + i2c_bus_ = nullptr; + } } + static void touchpad_timer_callback(void* arg) { - auto& board = (Spotpear_ESP32_S3_1_28_BOX&)Board::GetInstance(); - auto touchpad = board.GetTouchpad(); + auto* board = static_cast(arg); + if (!board || !board->cst816d_) return; static bool was_touched = false; static int64_t touch_start_time = 0; const int64_t TOUCH_THRESHOLD_MS = 500; // 触摸时长阈值,超过500ms视为长按 - touchpad->UpdateTouchPoint(); - auto touch_point = touchpad->GetTouchPoint(); + board->cst816d_->UpdateTouchPoint(); + auto touch_point = board->cst816d_->GetTouchPoint(); // 检测触摸开始 if (touch_point.num > 0 && !was_touched) { @@ -179,7 +243,7 @@ private: auto& app = Application::GetInstance(); if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) { - board.ResetWifiConfiguration(); + board->ResetWifiConfiguration(); } app.ToggleChatState(); } @@ -188,19 +252,59 @@ private: void InitializeCst816DTouchPad() { ESP_LOGI(TAG, "Init Cst816D"); + + // RST/INT 管脚初始化 + gpio_config_t io_conf = {}; + io_conf.intr_type = GPIO_INTR_DISABLE; + io_conf.mode = GPIO_MODE_OUTPUT; + io_conf.pin_bit_mask = (1ULL << TP_PIN_NUM_TP_RST); + io_conf.pull_down_en = GPIO_PULLDOWN_DISABLE; + io_conf.pull_up_en = GPIO_PULLUP_DISABLE; + gpio_config(&io_conf); + + gpio_config_t int_conf = {}; + int_conf.intr_type = GPIO_INTR_DISABLE; + int_conf.mode = GPIO_MODE_INPUT; + int_conf.pin_bit_mask = (1ULL << TP_PIN_NUM_TP_INT); + int_conf.pull_down_en = GPIO_PULLDOWN_DISABLE; + int_conf.pull_up_en = GPIO_PULLUP_ENABLE; + gpio_config(&int_conf); + + // 触摸芯片复位序列 + gpio_set_level(TP_PIN_NUM_TP_RST, 0); + vTaskDelay(pdMS_TO_TICKS(5)); + gpio_set_level(TP_PIN_NUM_TP_RST, 1); + vTaskDelay(pdMS_TO_TICKS(50)); + + // 探测是否存在触摸芯片 + uint8_t chip_id = 0; + if (!i2c_bus_) { + ESP_LOGW(TAG, "Touch I2C bus not initialized, skip touch"); + return; + } + bool touch_available = Cst816d::Probe(i2c_bus_, 0x15, chip_id); + if (!touch_available) { + ESP_LOGW(TAG, "CST816D not found, running in non-touch mode"); + // 释放触摸I2C,避免无设备时反复报错 + i2c_del_master_bus(i2c_bus_); + i2c_bus_ = nullptr; + return; + } + cst816d_ = new Cst816d(i2c_bus_, 0x15); // 创建定时器,10ms 间隔 esp_timer_create_args_t timer_args = { .callback = touchpad_timer_callback, - .arg = NULL, + .arg = this, .dispatch_method = ESP_TIMER_TASK, .name = "touchpad_timer", .skip_unhandled_events = true, }; - ESP_ERROR_CHECK(esp_timer_create(&timer_args, &touchpad_timer_)); - ESP_ERROR_CHECK(esp_timer_start_periodic(touchpad_timer_, 10 * 1000)); // 10ms = 10000us + if (esp_timer_create(&timer_args, &touchpad_timer_) == ESP_OK) { + esp_timer_start_periodic(touchpad_timer_, 10 * 1000); // 10ms = 10000us + } } // SPI初始化 @@ -228,6 +332,7 @@ private: panel_config.bits_per_pixel = 16; ESP_ERROR_CHECK(esp_lcd_new_panel_gc9a01(io_handle, &panel_config, &panel_handle)); + panel_ = panel_handle; ESP_ERROR_CHECK(esp_lcd_panel_reset(panel_handle)); ESP_ERROR_CHECK(esp_lcd_panel_init(panel_handle)); ESP_ERROR_CHECK(esp_lcd_panel_invert_color(panel_handle, true)); @@ -240,6 +345,18 @@ private: uint8_t data_0x63[] = { 0x18, 0x11, 0x71, 0xF1, 0x70, 0x70, 0x18, 0x13, 0x71, 0xF3, 0x70, 0x70 }; esp_lcd_panel_io_tx_param(io_handle, 0x63, data_0x63, sizeof(data_0x63)); + uint8_t data_0x36[] = { 0x48}; + esp_lcd_panel_io_tx_param(io_handle, 0x36, data_0x36, sizeof(data_0x36)); + + // uint8_t data_0x74[] = { 0x10, 0x85, 0x80, 0x00, 0x00, 0x4E, 0x00}; + // esp_lcd_panel_io_tx_param(io_handle, 0x74, data_0x74, sizeof(data_0x74)); + + uint8_t data_0xC3[] = { 0x1F}; + esp_lcd_panel_io_tx_param(io_handle, 0xC3, data_0xC3, sizeof(data_0xC3)); + + uint8_t data_0xC4[] = { 0x1F}; + esp_lcd_panel_io_tx_param(io_handle, 0xC4, data_0xC4, sizeof(data_0xC4)); + display_ = new CustomLcdDisplay(io_handle, panel_handle, DISPLAY_WIDTH, DISPLAY_HEIGHT, DISPLAY_OFFSET_X, DISPLAY_OFFSET_Y, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y, DISPLAY_SWAP_XY); @@ -257,21 +374,59 @@ private: public: Spotpear_ESP32_S3_1_28_BOX() : boot_button_(BOOT_BUTTON_GPIO) { + // 先初始化触摸的I2C并探测/初始化触摸(若无触摸则跳过) + InitializeCodecI2c_Touch(); + InitializeCst816DTouchPad(); - gpio_set_direction(TP_PIN_NUM_TP_INT, GPIO_MODE_INPUT); - int level = gpio_get_level(TP_PIN_NUM_TP_INT); - if (level == 1) { - InitializeCodecI2c_Touch(); - InitializeCst816DTouchPad(); - } - InitializePowerSaveTimer(); + // 初始化音频I2C InitializeCodecI2c(); + + // 显示相关先建立起来 InitializeSpi(); InitializeGc9a01Display(); InitializeButtons(); - GetBacklight()->RestoreBrightness(); + if (GetBacklight()) { + GetBacklight()->RestoreBrightness(); + } + + // 显示和背光可用后再初始化省电逻辑,避免空指针 + InitializePowerSaveTimer(); + InitializePowerManager(); } + ~Spotpear_ESP32_S3_1_28_BOX() { + if (touchpad_timer_) { + esp_timer_stop(touchpad_timer_); + esp_timer_delete(touchpad_timer_); + touchpad_timer_ = nullptr; + } + if (cst816d_) { + delete cst816d_; + cst816d_ = nullptr; + } + if (power_save_timer_) { + delete power_save_timer_; + power_save_timer_ = nullptr; + } + if (power_manager_) { + delete power_manager_; + power_manager_ = nullptr; + } + if (display_) { + delete display_; + display_ = nullptr; + } + if (i2c_bus_) { + i2c_del_master_bus(i2c_bus_); + i2c_bus_ = nullptr; + } + if (codec_i2c_bus_) { + i2c_del_master_bus(codec_i2c_bus_); + codec_i2c_bus_ = nullptr; + } + } + + virtual Led* GetLed() override { static SingleLed led(BUILTIN_LED_GPIO); return &led; @@ -297,6 +452,25 @@ public: return cst816d_; } + virtual bool GetBatteryLevel(int& level, bool& charging, bool& discharging) override { + if (!power_manager_) { + level = 0; + charging = false; + discharging = true; + return false; + } + + static bool last_discharging = false; + charging = power_manager_->IsCharging(); + discharging = power_manager_->IsDischarging(); + if (discharging != last_discharging) { + power_save_timer_->SetEnabled(discharging); + last_discharging = discharging; + } + level = power_manager_->GetBatteryLevel(); + return true; + } + virtual void SetPowerSaveMode(bool enabled) override { if (!enabled) { power_save_timer_->WakeUp();