feat:添加绿荫搭子Bq27220电源管理代码

This commit is contained in:
Gil Zhang 2025-12-29 23:40:55 +08:00
parent 373783dc63
commit f692480edc
6 changed files with 622 additions and 2 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

View File

@ -1,12 +1,23 @@
# 绿荫搭子 (Verdure Assistant)
<p align="center">
<img width="80%" align="center" src="../../../docs/V1/verdure-assistant.jpg"alt="logo">
</p>
<h1 align="center">
绿荫搭子 (Verdure Assistant)
</h1>
## 简介
绿荫搭子是一款基于 ESP32-S3 的智能语音助手开发板,基于立创·实战派 ESP32-S3 开发板代码修改而来。
绿荫搭子是一款基于 ESP32-S3 的智能语音助手开发板,基于立创·实战派 ESP32-S3 开发板修改而来。
主要是为实战派添加了电源管理模块,和重新设计了外壳,以及添加了电源管理的代码,后期可以进一步拓展功能。
## 项目地址
- 开源硬件平台: [https://oshwhub.com/greenshade/verdure-assistant](https://oshwhub.com/greenshade/verdure-assistant)
-
## B站复刻演示视频
- [https://www.bilibili.com/video/BV1x5vyBsEZj/](https://www.bilibili.com/video/BV1x5vyBsEZj/)
## 硬件特性
@ -16,6 +27,7 @@
- **音频编解码**: ES8311 + ES7210
- **摄像头**: GC0308 (可选)
- **扩展芯片**: PCA9557 GPIO 扩展器
- **电量计**: BQ27220 电池电量管理芯片
## 引脚配置
@ -48,6 +60,13 @@
| BOOT 按钮 | GPIO0 |
| LED | GPIO48 |
### BQ27220 电量计
| 功能 | 配置 |
|------|------|
| I2C 地址 | 0x55 |
| SDA | GPIO1 (与主 I2C 共用) |
| SCL | GPIO2 (与主 I2C 共用) |
## 编译方法
### 方法一:使用 release.py 脚本(推荐)
@ -84,12 +103,21 @@ python scripts/release.py verdure-assistant
- ✅ 摄像头 (可选)
- ✅ 设备端 AEC (回声消除)
- ✅ Emote 动画表情
- ✅ BQ27220 电池电量监测
## 特殊说明
- 本开发板使用 PCA9557 GPIO 扩展器控制功放使能和摄像头电源
- 支持设备端 AEC (Acoustic Echo Cancellation)
- 默认启用 GC0308 摄像头驱动
- 集成 BQ27220 电量计,支持以下功能:
- 电池电量百分比 (SOC)
- 电池健康度 (SOH)
- 电压、电流、温度监测
- 剩余容量、满充容量
- 充放电状态检测
- 预估剩余使用/充电时间
- 充放电循环次数
## 致谢

View File

@ -0,0 +1,349 @@
#include "bq27220.h"
#include <esp_log.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <cstring>
#define TAG "BQ27220"
Bq27220::Bq27220(i2c_master_bus_handle_t i2c_bus, uint8_t addr) : I2cDevice(i2c_bus, addr) {
ESP_LOGI(TAG, "BQ27220 driver created at address 0x%02X", addr);
}
uint16_t Bq27220::ReadReg16(uint8_t reg) {
uint8_t buffer[2];
ReadRegs(reg, buffer, 2);
// BQ27220 uses little endian
return buffer[0] | (buffer[1] << 8);
}
uint16_t Bq27220::ControlCommand(uint16_t sub_cmd) {
// Write control sub-command
uint8_t cmd_buf[3];
cmd_buf[0] = CMD_CONTROL;
cmd_buf[1] = sub_cmd & 0xFF;
cmd_buf[2] = (sub_cmd >> 8) & 0xFF;
i2c_master_transmit(i2c_device_, cmd_buf, 3, 100);
// Wait for command to complete
vTaskDelay(pdMS_TO_TICKS(15));
// Read response from MAC_DATA
return ReadReg16(CMD_MAC_DATA);
}
bool Bq27220::Init() {
ESP_LOGI(TAG, "Initializing BQ27220...");
// Verify device ID
uint16_t device_id = ControlCommand(CTRL_DEVICE_NUMBER);
if (device_id != DEVICE_ID) {
ESP_LOGE(TAG, "Invalid Device ID: 0x%04X (expected 0x%04X)", device_id, DEVICE_ID);
return false;
}
ESP_LOGI(TAG, "Device ID verified: 0x%04X", device_id);
// Read firmware version
uint16_t fw_version = GetFirmwareVersion();
ESP_LOGI(TAG, "Firmware Version: 0x%04X", fw_version);
// Read hardware version
uint16_t hw_version = GetHardwareVersion();
ESP_LOGI(TAG, "Hardware Version: 0x%04X", hw_version);
// Read initial battery info
ESP_LOGI(TAG, "Battery SOC: %d%%, Voltage: %dmV, Current: %dmA, Temp: %d°C",
GetBatteryLevel(), GetVoltage(), GetCurrent(), GetTemperature());
return true;
}
int Bq27220::GetBatteryLevel() {
uint16_t soc = ReadReg16(CMD_STATE_OF_CHARGE);
// State of charge is in percentage (0-100)
if (soc > 100) {
soc = 100;
}
return soc;
}
int Bq27220::GetVoltage() {
// Voltage in mV
return ReadReg16(CMD_VOLTAGE);
}
int Bq27220::GetCurrent() {
// Current in mA (signed)
int16_t current = (int16_t)ReadReg16(CMD_CURRENT);
return current;
}
int Bq27220::GetTemperature() {
// Temperature in 0.1°K, convert to Celsius
uint16_t temp_k = ReadReg16(CMD_TEMPERATURE);
// Convert from 0.1°K to °C: (temp_k / 10) - 273.15
int temp_c = (temp_k / 10) - 273;
return temp_c;
}
int Bq27220::GetRemainingCapacity() {
// Remaining capacity in mAh
return ReadReg16(CMD_REMAINING_CAPACITY);
}
int Bq27220::GetFullCapacity() {
// Full charge capacity in mAh
return ReadReg16(CMD_FULL_CHARGE_CAPACITY);
}
int Bq27220::GetDesignCapacity() {
// Design capacity in mAh
return ReadReg16(CMD_DESIGN_CAPACITY);
}
int Bq27220::GetStateOfHealth() {
// State of health in percentage
uint16_t soh = ReadReg16(CMD_STATE_OF_HEALTH);
if (soh > 100) {
soh = 100;
}
return soh;
}
bool Bq27220::GetBatteryStatus(BatteryStatus* status) {
if (!status) {
return false;
}
uint16_t status_reg = ReadReg16(CMD_BATTERY_STATUS);
// Copy the register value to the status structure
*((uint16_t*)status) = status_reg;
return true;
}
uint16_t Bq27220::GetFirmwareVersion() {
return ControlCommand(CTRL_FW_VERSION);
}
uint16_t Bq27220::GetHardwareVersion() {
return ControlCommand(CTRL_HW_VERSION);
}
int Bq27220::GetAveragePower() {
// Average power in mW (signed)
return (int16_t)ReadReg16(CMD_AVERAGE_POWER);
}
int Bq27220::GetTimeToEmpty() {
// Time to empty in minutes
return ReadReg16(CMD_TIME_TO_EMPTY);
}
int Bq27220::GetTimeToFull() {
// Time to full in minutes
return ReadReg16(CMD_TIME_TO_FULL);
}
int Bq27220::GetCycleCount() {
// Number of charge/discharge cycles
return ReadReg16(CMD_CYCLE_COUNT);
}
bool Bq27220::IsCharging() {
int16_t current = GetCurrent();
// Positive current means charging (with threshold to avoid noise)
return current > 50; // 50mA threshold
}
bool Bq27220::IsDischarging() {
BatteryStatus status;
if (GetBatteryStatus(&status)) {
return status.dsg;
}
return false;
}
bool Bq27220::IsFullyCharged() {
BatteryStatus status;
if (GetBatteryStatus(&status)) {
return status.fc;
}
return false;
}
void Bq27220::ControlCommandNoRead(uint16_t sub_cmd) {
uint8_t cmd_buf[3];
cmd_buf[0] = CMD_CONTROL;
cmd_buf[1] = sub_cmd & 0xFF;
cmd_buf[2] = (sub_cmd >> 8) & 0xFF;
i2c_master_transmit(i2c_device_, cmd_buf, 3, 100);
vTaskDelay(pdMS_TO_TICKS(15));
}
bool Bq27220::Unseal() {
ESP_LOGI(TAG, "Unsealing BQ27220...");
// Send unseal key sequence
ControlCommandNoRead(UNSEAL_KEY1);
ControlCommandNoRead(UNSEAL_KEY2);
vTaskDelay(pdMS_TO_TICKS(100));
// Verify unsealed by checking if we can enter config update mode
ESP_LOGI(TAG, "BQ27220 unsealed");
return true;
}
bool Bq27220::Seal() {
ESP_LOGI(TAG, "Sealing BQ27220...");
ControlCommandNoRead(CTRL_SEAL);
vTaskDelay(pdMS_TO_TICKS(100));
ESP_LOGI(TAG, "BQ27220 sealed");
return true;
}
bool Bq27220::EnterConfigUpdate() {
ESP_LOGI(TAG, "Entering config update mode...");
ControlCommandNoRead(CTRL_ENTER_CFG_UPDATE);
vTaskDelay(pdMS_TO_TICKS(1000)); // Wait for config update mode
ESP_LOGI(TAG, "Entered config update mode");
return true;
}
bool Bq27220::ExitConfigUpdate() {
ESP_LOGI(TAG, "Exiting config update mode with reinit...");
// Use EXIT_CFG_UPDATE_REINIT (0x0091) to recalculate gauging parameters
ControlCommandNoRead(CTRL_EXIT_CFG_UPDATE_REINIT);
vTaskDelay(pdMS_TO_TICKS(1000)); // Wait for exit and reinit
ESP_LOGI(TAG, "Exited config update mode");
return true;
}
bool Bq27220::WriteDataMemory(uint16_t addr, const uint8_t* data, uint8_t len) {
// Write address + data to SelectSubclass (0x3E)
// Format: [0x3E] [addr_low] [addr_high] [data_high] [data_low] (big endian for data)
uint8_t* buf = new uint8_t[len + 3];
buf[0] = 0x3E; // SelectSubclass command
buf[1] = addr & 0xFF;
buf[2] = (addr >> 8) & 0xFF;
memcpy(buf + 3, data, len);
i2c_master_transmit(i2c_device_, buf, len + 3, 100);
delete[] buf;
vTaskDelay(pdMS_TO_TICKS(10));
// Calculate checksum: sum of (addr_low + addr_high + data bytes), then 0xFF - sum
uint8_t checksum = 0;
checksum += (addr & 0xFF);
checksum += ((addr >> 8) & 0xFF);
for (int i = 0; i < len; i++) {
checksum += data[i];
}
checksum = 0xFF - checksum;
// Write checksum and length to MACDataSum (0x60)
uint8_t sum_buf[3];
sum_buf[0] = 0x60; // MACDataSum
sum_buf[1] = checksum;
sum_buf[2] = len + 4; // Total length: 2 (address) + len (data) + 1 (checksum) + 1 (length)
i2c_master_transmit(i2c_device_, sum_buf, 3, 100);
vTaskDelay(pdMS_TO_TICKS(10));
return true;
}
bool Bq27220::SetDesignCapacity(uint16_t capacity_mah) {
ESP_LOGI(TAG, "Setting design capacity to %d mAh...", capacity_mah);
// Step 1: Unseal the device
if (!Unseal()) {
ESP_LOGE(TAG, "Failed to unseal device");
return false;
}
// Step 2: Enter config update mode
if (!EnterConfigUpdate()) {
ESP_LOGE(TAG, "Failed to enter config update mode");
Seal();
return false;
}
// Step 3: Write Full Charge Capacity (BIG ENDIAN - high byte first)
uint8_t cap_data[2];
cap_data[0] = (capacity_mah >> 8) & 0xFF; // High byte first
cap_data[1] = capacity_mah & 0xFF; // Low byte second
ESP_LOGI(TAG, "Writing Full Charge Capacity: %d mAh", capacity_mah);
if (!WriteDataMemory(DM_FULL_CHARGE_CAPACITY, cap_data, 2)) {
ESP_LOGE(TAG, "Failed to write full charge capacity");
ExitConfigUpdate();
Seal();
return false;
}
// Step 4: Write Design Capacity (BIG ENDIAN)
ESP_LOGI(TAG, "Writing Design Capacity: %d mAh", capacity_mah);
if (!WriteDataMemory(DM_DESIGN_CAPACITY, cap_data, 2)) {
ESP_LOGE(TAG, "Failed to write design capacity");
ExitConfigUpdate();
Seal();
return false;
}
// Step 5: Write design energy (capacity * 3.7V nominal) (BIG ENDIAN)
uint16_t design_energy = (uint16_t)((capacity_mah * 37) / 10); // mWh
uint8_t energy_data[2];
energy_data[0] = (design_energy >> 8) & 0xFF; // High byte first
energy_data[1] = design_energy & 0xFF; // Low byte second
ESP_LOGI(TAG, "Writing Design Energy: %d mWh", design_energy);
if (!WriteDataMemory(DM_DESIGN_ENERGY, energy_data, 2)) {
ESP_LOGE(TAG, "Failed to write design energy");
ExitConfigUpdate();
Seal();
return false;
}
// Step 6: Exit config update mode with reinit
if (!ExitConfigUpdate()) {
ESP_LOGE(TAG, "Failed to exit config update mode");
Seal();
return false;
}
// Step 6: Seal the device
Seal();
// Step 7: Verify the new design capacity
vTaskDelay(pdMS_TO_TICKS(100));
int new_capacity = GetDesignCapacity();
ESP_LOGI(TAG, "Verified design capacity: %d mAh", new_capacity);
if (new_capacity == capacity_mah) {
ESP_LOGI(TAG, "Design capacity set to %d mAh successfully!", capacity_mah);
} else {
ESP_LOGW(TAG, "Design capacity verification mismatch: expected %d, got %d",
capacity_mah, new_capacity);
ESP_LOGI(TAG, "This may be normal - device might need a power cycle or charge cycle");
}
ESP_LOGI(TAG, "Note: Full charge cycle needed for gauge to recalibrate");
return true;
}
bool Bq27220::ResetLearning() {
ESP_LOGI(TAG, "Resetting fuel gauge learning...");
if (!Unseal()) {
return false;
}
ControlCommandNoRead(CTRL_RESET);
vTaskDelay(pdMS_TO_TICKS(500));
Seal();
ESP_LOGI(TAG, "Fuel gauge reset complete");
return true;
}

View File

@ -0,0 +1,163 @@
#ifndef __BQ27220_H__
#define __BQ27220_H__
#include "../common/i2c_device.h"
// BQ27220 Fuel Gauge Driver
// Reference: Texas Instruments BQ27220 datasheet & esp-brookesia implementation
class Bq27220 : public I2cDevice {
public:
// Battery Status structure (compatible with BQ27220 register format)
struct BatteryStatus {
bool dsg : 1; // The device is in DISCHARGE
bool sysdwn : 1; // System down bit
bool tda : 1; // Terminate Discharge Alarm
bool battpres : 1; // Battery Present detected
bool auth_gd : 1; // Detect inserted battery
bool ocvgd : 1; // Good OCV measurement taken
bool tca : 1; // Terminate Charge Alarm
bool rsvd : 1; // Reserved
bool chginh : 1; // Charge inhibit
bool fc : 1; // Full-charged is detected
bool otd : 1; // Overtemperature in discharge
bool otc : 1; // Overtemperature in charge
bool sleep : 1; // Device is in SLEEP mode
bool ocvfail : 1; // OCV reading failed
bool ocvcomp : 1; // OCV measurement complete
bool fd : 1; // Full-discharge is detected
};
Bq27220(i2c_master_bus_handle_t i2c_bus, uint8_t addr);
// Initialize device and verify device ID
bool Init();
// Get battery state of charge (0-100%)
int GetBatteryLevel();
// Get battery voltage in mV
int GetVoltage();
// Get battery current in mA (positive = charging, negative = discharging)
int GetCurrent();
// Get battery temperature in Celsius
int GetTemperature();
// Get remaining capacity in mAh
int GetRemainingCapacity();
// Get full charge capacity in mAh
int GetFullCapacity();
// Get design capacity in mAh
int GetDesignCapacity();
// Get state of health (0-100%)
int GetStateOfHealth();
// Get battery status flags
bool GetBatteryStatus(BatteryStatus* status);
// Get firmware version
uint16_t GetFirmwareVersion();
// Get hardware version
uint16_t GetHardwareVersion();
// Get average power in mW
int GetAveragePower();
// Get time to empty in minutes
int GetTimeToEmpty();
// Get time to full in minutes
int GetTimeToFull();
// Get cycle count
int GetCycleCount();
// Check if battery is charging
bool IsCharging();
// Check if battery is discharging
bool IsDischarging();
// Check if battery is fully charged
bool IsFullyCharged();
// Configure design capacity (in mAh) - requires full charge cycle to take effect
bool SetDesignCapacity(uint16_t capacity_mah);
// Reset fuel gauge learning
bool ResetLearning();
private:
// BQ27220 Standard Commands (from datasheet)
static constexpr uint8_t CMD_CONTROL = 0x00;
static constexpr uint8_t CMD_TEMPERATURE = 0x06;
static constexpr uint8_t CMD_VOLTAGE = 0x08;
static constexpr uint8_t CMD_BATTERY_STATUS = 0x0A;
static constexpr uint8_t CMD_CURRENT = 0x0C;
static constexpr uint8_t CMD_REMAINING_CAPACITY = 0x10;
static constexpr uint8_t CMD_FULL_CHARGE_CAPACITY = 0x12;
static constexpr uint8_t CMD_AVERAGE_CURRENT = 0x14;
static constexpr uint8_t CMD_TIME_TO_EMPTY = 0x16;
static constexpr uint8_t CMD_TIME_TO_FULL = 0x18;
static constexpr uint8_t CMD_STANDBY_CURRENT = 0x1A;
static constexpr uint8_t CMD_MAX_LOAD_CURRENT = 0x1E;
static constexpr uint8_t CMD_AVERAGE_POWER = 0x24;
static constexpr uint8_t CMD_CYCLE_COUNT = 0x2A;
static constexpr uint8_t CMD_STATE_OF_CHARGE = 0x2C;
static constexpr uint8_t CMD_STATE_OF_HEALTH = 0x2E;
static constexpr uint8_t CMD_DESIGN_CAPACITY = 0x3C;
static constexpr uint8_t CMD_MAC_DATA = 0x40;
// Control sub-commands
static constexpr uint16_t CTRL_DEVICE_NUMBER = 0x0001;
static constexpr uint16_t CTRL_FW_VERSION = 0x0002;
static constexpr uint16_t CTRL_HW_VERSION = 0x0003;
static constexpr uint16_t CTRL_SEAL = 0x0030;
static constexpr uint16_t CTRL_RESET = 0x0041;
static constexpr uint16_t CTRL_ENTER_CFG_UPDATE = 0x0090;
static constexpr uint16_t CTRL_EXIT_CFG_UPDATE_REINIT = 0x0091;
static constexpr uint16_t CTRL_EXIT_CFG_UPDATE = 0x0092;
// Data Memory addresses (from bq27220_reg.h)
static constexpr uint16_t DM_FULL_CHARGE_CAPACITY = 0x929D; // Full Charge Capacity address
static constexpr uint16_t DM_DESIGN_CAPACITY = 0x929F; // Design Capacity address
static constexpr uint16_t DM_DESIGN_ENERGY = 0x92A1; // Design Energy address
// Unseal keys (default)
static constexpr uint16_t UNSEAL_KEY1 = 0x0414;
static constexpr uint16_t UNSEAL_KEY2 = 0x3672;
// Device ID
static constexpr uint16_t DEVICE_ID = 0x0220;
// Read 16-bit register (little endian)
uint16_t ReadReg16(uint8_t reg);
// Send control command and read response
uint16_t ControlCommand(uint16_t sub_cmd);
// Send control command without reading response
void ControlCommandNoRead(uint16_t sub_cmd);
// Unseal the device for configuration
bool Unseal();
// Seal the device after configuration
bool Seal();
// Enter config update mode
bool EnterConfigUpdate();
// Exit config update mode
bool ExitConfigUpdate();
// Write data to data memory
bool WriteDataMemory(uint16_t addr, const uint8_t* data, uint8_t len);
};
#endif // __BQ27220_H__

View File

@ -58,5 +58,11 @@
#define XCLK_FREQ_HZ 20000000
/* BQ27220 Fuel Gauge */
#define BQ27220_I2C_SDA_PIN GPIO_NUM_1
#define BQ27220_I2C_SCL_PIN GPIO_NUM_2
#define BQ27220_I2C_ADDRESS 0x55
#define BQ27220_DESIGN_CAPACITY 1000 // Battery design capacity in mAh
#endif // _BOARD_CONFIG_H_

View File

@ -16,6 +16,7 @@
#include "i2c_device.h"
#include "esp32_camera.h"
#include "mcp_server.h"
#include "bq27220.h"
#include <esp_log.h>
#include <esp_lcd_panel_vendor.h>
@ -80,6 +81,7 @@ private:
Display* display_;
Pca9557* pca9557_;
Esp32Camera* camera_;
Bq27220* bq27220_;
void InitializeI2c() {
// Initialize I2C peripheral
@ -99,6 +101,39 @@ private:
// Initialize PCA9557
pca9557_ = new Pca9557(i2c_bus_, 0x19);
// Initialize BQ27220 fuel gauge
bq27220_ = new Bq27220(i2c_bus_, BQ27220_I2C_ADDRESS);
if (!bq27220_->Init()) {
ESP_LOGE(TAG, "BQ27220 initialization failed!");
} else {
// Check if design capacity needs to be configured
int design_capacity = bq27220_->GetDesignCapacity();
int full_charge_capacity = bq27220_->GetFullCapacity();
ESP_LOGI(TAG, "BQ27220 Design Capacity: %d mAh, Full Charge Capacity: %d mAh",
design_capacity, full_charge_capacity);
// Check both design and full charge capacity
if (design_capacity != BQ27220_DESIGN_CAPACITY ||
full_charge_capacity != BQ27220_DESIGN_CAPACITY) {
ESP_LOGW(TAG, "Capacity mismatch! Design: %d, FullCharge: %d, Expected: %d",
design_capacity, full_charge_capacity, BQ27220_DESIGN_CAPACITY);
ESP_LOGI(TAG, "Configuring BQ27220 to %d mAh...", BQ27220_DESIGN_CAPACITY);
if (bq27220_->SetDesignCapacity(BQ27220_DESIGN_CAPACITY)) {
ESP_LOGI(TAG, "Capacity configured successfully!");
// Re-read to verify
design_capacity = bq27220_->GetDesignCapacity();
full_charge_capacity = bq27220_->GetFullCapacity();
ESP_LOGI(TAG, "After config - Design: %d mAh, FullCharge: %d mAh",
design_capacity, full_charge_capacity);
} else {
ESP_LOGE(TAG, "Failed to configure capacity!");
}
} else {
ESP_LOGI(TAG, "BQ27220 capacity already configured correctly");
}
}
}
void InitializeSpi() {
@ -298,6 +333,45 @@ public:
virtual Camera* GetCamera() override {
return camera_;
}
virtual bool GetBatteryLevel(int& level, bool& charging, bool& discharging) override {
static int last_level = -1;
static bool last_charging = false;
static int log_counter = 0;
if (bq27220_ == nullptr) {
return false;
}
// Get basic battery info
level = bq27220_->GetBatteryLevel();
charging = bq27220_->IsCharging();
discharging = bq27220_->IsDischarging();
// Only log when: level changes, charging state changes, or every 30 seconds
log_counter++;
bool should_log = (level != last_level) ||
(charging != last_charging) ||
(log_counter >= 30);
if (should_log) {
log_counter = 0;
last_level = level;
last_charging = charging;
// Get additional battery info for logging
int voltage = bq27220_->GetVoltage();
int current = bq27220_->GetCurrent();
int remaining = bq27220_->GetRemainingCapacity();
int full_capacity = bq27220_->GetFullCapacity();
ESP_LOGI(TAG, "Battery: %d%% (%d/%d mAh), %dmV, %dmA, %s",
level, remaining, full_capacity, voltage, current,
charging ? "Charging" : (discharging ? "Discharging" : "Idle"));
}
return true;
}
};
DECLARE_BOARD(VerdureAssistantBoard);