From 5c3a5cd8f71f386abf2d3676d87d52a39ceb1975 Mon Sep 17 00:00:00 2001 From: Shane Synan Date: Thu, 8 Dec 2022 01:57:49 -0500 Subject: [PATCH] FuriHal, Power, UnitTests: battery charging voltage limit API (#2063) --- applications/debug/unit_tests/minunit.h | 2 +- .../debug/unit_tests/power/power_test.c | 62 +++++++++++++++++++ applications/debug/unit_tests/test_index.c | 2 + .../services/power/power_service/power.c | 15 +++++ .../services/power/power_service/power.h | 1 + .../power_settings_scene_battery_info.c | 1 + .../power_settings_app/views/battery_info.c | 10 +++ .../power_settings_app/views/battery_info.h | 1 + firmware/targets/f7/api_symbols.csv | 2 + firmware/targets/f7/furi_hal/furi_hal_power.c | 29 ++++++++- .../targets/furi_hal_include/furi_hal_power.h | 16 +++++ lib/drivers/bq25896.c | 27 ++++++++ lib/drivers/bq25896.h | 9 +++ 13 files changed, 173 insertions(+), 4 deletions(-) create mode 100644 applications/debug/unit_tests/power/power_test.c diff --git a/applications/debug/unit_tests/minunit.h b/applications/debug/unit_tests/minunit.h index 17eb7b3f..69bfba6d 100644 --- a/applications/debug/unit_tests/minunit.h +++ b/applications/debug/unit_tests/minunit.h @@ -316,7 +316,7 @@ void minunit_print_fail(const char* error); MU__SAFE_BLOCK( \ double minunit_tmp_e; double minunit_tmp_r; minunit_assert++; minunit_tmp_e = (expected); \ minunit_tmp_r = (result); \ - if(fabs(minunit_tmp_e - minunit_tmp_r) > MINUNIT_EPSILON) { \ + if(fabs(minunit_tmp_e - minunit_tmp_r) > (double)MINUNIT_EPSILON) { \ int minunit_significant_figures = 1 - log10(MINUNIT_EPSILON); \ snprintf( \ minunit_last_message, \ diff --git a/applications/debug/unit_tests/power/power_test.c b/applications/debug/unit_tests/power/power_test.c new file mode 100644 index 00000000..ce2c7aad --- /dev/null +++ b/applications/debug/unit_tests/power/power_test.c @@ -0,0 +1,62 @@ +#include +#include +#include "../minunit.h" + +static void power_test_deinit(void) { + // Try to reset to default charging voltage + furi_hal_power_set_battery_charging_voltage(4.208f); +} + +MU_TEST(test_power_charge_voltage_exact) { + // Power of 16mV charge voltages get applied exactly + // (bq25896 charge controller works in 16mV increments) + // + // This test may need adapted if other charge controllers are used in the future. + for(uint16_t charge_mv = 3840; charge_mv <= 4208; charge_mv += 16) { + float charge_volt = (float)charge_mv / 1000.0f; + furi_hal_power_set_battery_charging_voltage(charge_volt); + mu_assert_double_eq(charge_volt, furi_hal_power_get_battery_charging_voltage()); + } +} + +MU_TEST(test_power_charge_voltage_floating_imprecision) { + // 4.016f should act as 4.016 V, even with floating point imprecision + furi_hal_power_set_battery_charging_voltage(4.016f); + mu_assert_double_eq(4.016f, furi_hal_power_get_battery_charging_voltage()); +} + +MU_TEST(test_power_charge_voltage_inexact) { + // Charge voltages that are not power of 16mV get truncated down + furi_hal_power_set_battery_charging_voltage(3.841f); + mu_assert_double_eq(3.840, furi_hal_power_get_battery_charging_voltage()); + + furi_hal_power_set_battery_charging_voltage(3.900f); + mu_assert_double_eq(3.888, furi_hal_power_get_battery_charging_voltage()); + + furi_hal_power_set_battery_charging_voltage(4.200f); + mu_assert_double_eq(4.192, furi_hal_power_get_battery_charging_voltage()); +} + +MU_TEST(test_power_charge_voltage_invalid_clamped) { + // Out-of-range charge voltages get clamped to 3.840 V and 4.208 V + furi_hal_power_set_battery_charging_voltage(3.808f); + mu_assert_double_eq(3.840, furi_hal_power_get_battery_charging_voltage()); + + // NOTE: Intentionally picking a small increment above 4.208 V to reduce the risk of an + // unhappy battery if this fails. + furi_hal_power_set_battery_charging_voltage(4.240f); + mu_assert_double_eq(4.208, furi_hal_power_get_battery_charging_voltage()); +} + +MU_TEST_SUITE(test_power_suite) { + MU_RUN_TEST(test_power_charge_voltage_exact); + MU_RUN_TEST(test_power_charge_voltage_floating_imprecision); + MU_RUN_TEST(test_power_charge_voltage_inexact); + MU_RUN_TEST(test_power_charge_voltage_invalid_clamped); + power_test_deinit(); +} + +int run_minunit_test_power() { + MU_RUN_SUITE(test_power_suite); + return MU_EXIT_CODE; +} diff --git a/applications/debug/unit_tests/test_index.c b/applications/debug/unit_tests/test_index.c index b8760b1f..65fa23c0 100644 --- a/applications/debug/unit_tests/test_index.c +++ b/applications/debug/unit_tests/test_index.c @@ -19,6 +19,7 @@ int run_minunit_test_stream(); int run_minunit_test_storage(); int run_minunit_test_subghz(); int run_minunit_test_dirwalk(); +int run_minunit_test_power(); int run_minunit_test_protocol_dict(); int run_minunit_test_lfrfid_protocols(); int run_minunit_test_nfc(); @@ -44,6 +45,7 @@ const UnitTest unit_tests[] = { {.name = "subghz", .entry = run_minunit_test_subghz}, {.name = "infrared", .entry = run_minunit_test_infrared}, {.name = "nfc", .entry = run_minunit_test_nfc}, + {.name = "power", .entry = run_minunit_test_power}, {.name = "protocol_dict", .entry = run_minunit_test_protocol_dict}, {.name = "lfrfid", .entry = run_minunit_test_lfrfid_protocols}, {.name = "bit_lib", .entry = run_minunit_test_bit_lib}, diff --git a/applications/services/power/power_service/power.c b/applications/services/power/power_service/power.c index 89886b0f..5df611a7 100644 --- a/applications/services/power/power_service/power.c +++ b/applications/services/power/power_service/power.c @@ -12,6 +12,20 @@ void power_draw_battery_callback(Canvas* canvas, void* context) { if(power->info.gauge_is_ok) { canvas_draw_box(canvas, 2, 2, (power->info.charge + 4) / 5, 4); + if(power->info.voltage_battery_charging < 4.2) { + // Battery charging voltage is modified, indicate with cross pattern + canvas_invert_color(canvas); + uint8_t battery_bar_width = (power->info.charge + 4) / 5; + bool cross_odd = false; + // Start 1 further in from the battery bar's x position + for(uint8_t x = 3; x <= battery_bar_width; x++) { + // Cross pattern is from the center of the battery bar + // y = 2 + 1 (inset) + 1 (for every other) + canvas_draw_dot(canvas, x, 3 + (uint8_t)cross_odd); + cross_odd = !cross_odd; + } + canvas_invert_color(canvas); + } if(power->state == PowerStateCharging) { canvas_set_bitmap_mode(canvas, 1); canvas_set_color(canvas, ColorWhite); @@ -132,6 +146,7 @@ static bool power_update_info(Power* power) { info.capacity_full = furi_hal_power_get_battery_full_capacity(); info.current_charger = furi_hal_power_get_battery_current(FuriHalPowerICCharger); info.current_gauge = furi_hal_power_get_battery_current(FuriHalPowerICFuelGauge); + info.voltage_battery_charging = furi_hal_power_get_battery_charging_voltage(); info.voltage_charger = furi_hal_power_get_battery_voltage(FuriHalPowerICCharger); info.voltage_gauge = furi_hal_power_get_battery_voltage(FuriHalPowerICFuelGauge); info.voltage_vbus = furi_hal_power_get_usb_voltage(); diff --git a/applications/services/power/power_service/power.h b/applications/services/power/power_service/power.h index 32433ebc..8b9019c4 100644 --- a/applications/services/power/power_service/power.h +++ b/applications/services/power/power_service/power.h @@ -41,6 +41,7 @@ typedef struct { float current_charger; float current_gauge; + float voltage_battery_charging; float voltage_charger; float voltage_gauge; float voltage_vbus; diff --git a/applications/settings/power_settings_app/scenes/power_settings_scene_battery_info.c b/applications/settings/power_settings_app/scenes/power_settings_scene_battery_info.c index 0085c31d..5fa38df7 100644 --- a/applications/settings/power_settings_app/scenes/power_settings_scene_battery_info.c +++ b/applications/settings/power_settings_app/scenes/power_settings_scene_battery_info.c @@ -7,6 +7,7 @@ static void power_settings_scene_battery_info_update_model(PowerSettingsApp* app .gauge_voltage = app->info.voltage_gauge, .gauge_current = app->info.current_gauge, .gauge_temperature = app->info.temperature_gauge, + .charging_voltage = app->info.voltage_battery_charging, .charge = app->info.charge, .health = app->info.health, }; diff --git a/applications/settings/power_settings_app/views/battery_info.c b/applications/settings/power_settings_app/views/battery_info.c index bbb0acb9..d760164b 100644 --- a/applications/settings/power_settings_app/views/battery_info.c +++ b/applications/settings/power_settings_app/views/battery_info.c @@ -68,6 +68,16 @@ static void draw_battery(Canvas* canvas, BatteryInfoModel* data, int x, int y) { drain_current > HIGH_DRAIN_CURRENT_THRESHOLD ? "mA!" : "mA"); } else if(drain_current != 0) { snprintf(header, 20, "..."); + } else if(data->charging_voltage < 4.2) { + // Non-default battery charging limit, mention it + snprintf(emote, sizeof(emote), "Charged!"); + snprintf(header, sizeof(header), "Limited to"); + snprintf( + value, + sizeof(value), + "%ld.%ldV", + (uint32_t)(data->charging_voltage), + (uint32_t)(data->charging_voltage * 10) % 10); } else { snprintf(header, sizeof(header), "Charged!"); } diff --git a/applications/settings/power_settings_app/views/battery_info.h b/applications/settings/power_settings_app/views/battery_info.h index e62aa44f..7bfacf69 100644 --- a/applications/settings/power_settings_app/views/battery_info.h +++ b/applications/settings/power_settings_app/views/battery_info.h @@ -9,6 +9,7 @@ typedef struct { float gauge_voltage; float gauge_current; float gauge_temperature; + float charging_voltage; uint8_t charge; uint8_t health; } BatteryInfoModel; diff --git a/firmware/targets/f7/api_symbols.csv b/firmware/targets/f7/api_symbols.csv index 7b07bb18..80aaf7fa 100644 --- a/firmware/targets/f7/api_symbols.csv +++ b/firmware/targets/f7/api_symbols.csv @@ -1194,6 +1194,7 @@ Function,+,furi_hal_power_enable_external_3_3v,void, Function,+,furi_hal_power_enable_otg,void, Function,+,furi_hal_power_gauge_is_ok,_Bool, Function,+,furi_hal_power_get_bat_health_pct,uint8_t, +Function,+,furi_hal_power_get_battery_charging_voltage,float, Function,+,furi_hal_power_get_battery_current,float,FuriHalPowerIC Function,+,furi_hal_power_get_battery_design_capacity,uint32_t, Function,+,furi_hal_power_get_battery_full_capacity,uint32_t, @@ -1212,6 +1213,7 @@ Function,+,furi_hal_power_is_charging_done,_Bool, Function,+,furi_hal_power_is_otg_enabled,_Bool, Function,+,furi_hal_power_off,void, Function,+,furi_hal_power_reset,void, +Function,+,furi_hal_power_set_battery_charging_voltage,void,float Function,+,furi_hal_power_shutdown,void, Function,+,furi_hal_power_sleep,void, Function,+,furi_hal_power_sleep_available,_Bool, diff --git a/firmware/targets/f7/furi_hal/furi_hal_power.c b/firmware/targets/f7/furi_hal/furi_hal_power.c index ddb05690..2d709620 100644 --- a/firmware/targets/f7/furi_hal/furi_hal_power.c +++ b/firmware/targets/f7/furi_hal/furi_hal_power.c @@ -341,6 +341,20 @@ bool furi_hal_power_is_otg_enabled() { return ret; } +float furi_hal_power_get_battery_charging_voltage() { + furi_hal_i2c_acquire(&furi_hal_i2c_handle_power); + float ret = (float)bq25896_get_vreg_voltage(&furi_hal_i2c_handle_power) / 1000.0f; + furi_hal_i2c_release(&furi_hal_i2c_handle_power); + return ret; +} + +void furi_hal_power_set_battery_charging_voltage(float voltage) { + furi_hal_i2c_acquire(&furi_hal_i2c_handle_power); + // Adding 0.0005 is necessary because 4.016f is 4.015999794000, which gets truncated + bq25896_set_vreg_voltage(&furi_hal_i2c_handle_power, (uint16_t)(voltage * 1000.0f + 0.0005f)); + furi_hal_i2c_release(&furi_hal_i2c_handle_power); +} + void furi_hal_power_check_otg_status() { furi_hal_i2c_acquire(&furi_hal_i2c_handle_power); if(bq25896_check_otg_fault(&furi_hal_i2c_handle_power)) @@ -470,10 +484,10 @@ void furi_hal_power_info_get(PropertyValueCallback out, char sep, void* context) if(sep == '.') { property_value_out(&property_context, NULL, 2, "format", "major", "2"); - property_value_out(&property_context, NULL, 2, "format", "minor", "0"); + property_value_out(&property_context, NULL, 2, "format", "minor", "1"); } else { property_value_out(&property_context, NULL, 3, "power", "info", "major", "1"); - property_value_out(&property_context, NULL, 3, "power", "info", "minor", "0"); + property_value_out(&property_context, NULL, 3, "power", "info", "minor", "1"); } uint8_t charge = furi_hal_power_get_pct(); @@ -481,7 +495,7 @@ void furi_hal_power_info_get(PropertyValueCallback out, char sep, void* context) const char* charge_state; if(furi_hal_power_is_charging()) { - if(charge < 100) { + if((charge < 100) && (!furi_hal_power_is_charging_done())) { charge_state = "charging"; } else { charge_state = "charged"; @@ -491,6 +505,8 @@ void furi_hal_power_info_get(PropertyValueCallback out, char sep, void* context) } property_value_out(&property_context, NULL, 2, "charge", "state", charge_state); + uint16_t charge_voltage = (uint16_t)(furi_hal_power_get_battery_charging_voltage() * 1000.f); + property_value_out(&property_context, "%u", 2, "charge", "voltage", charge_voltage); uint16_t voltage = (uint16_t)(furi_hal_power_get_battery_voltage(FuriHalPowerICFuelGauge) * 1000.f); property_value_out(&property_context, "%u", 2, "battery", "voltage", voltage); @@ -567,6 +583,13 @@ void furi_hal_power_debug_get(PropertyValueCallback out, void* context) { "charger", "vbat", bq25896_get_vbat_voltage(&furi_hal_i2c_handle_power)); + property_value_out( + &property_context, + "%d", + 2, + "charger", + "vreg", + bq25896_get_vreg_voltage(&furi_hal_i2c_handle_power)); property_value_out( &property_context, "%d", diff --git a/firmware/targets/furi_hal_include/furi_hal_power.h b/firmware/targets/furi_hal_include/furi_hal_power.h index a78d09fe..39a11e99 100644 --- a/firmware/targets/furi_hal_include/furi_hal_power.h +++ b/firmware/targets/furi_hal_include/furi_hal_power.h @@ -121,6 +121,22 @@ void furi_hal_power_check_otg_status(); */ bool furi_hal_power_is_otg_enabled(); +/** Get battery charging voltage in V + * + * @return voltage in V + */ +float furi_hal_power_get_battery_charging_voltage(); + +/** Set battery charging voltage in V + * + * Invalid values will be clamped to the nearest valid value. + * + * @param voltage[in] voltage in V + * + * @return voltage in V + */ +void furi_hal_power_set_battery_charging_voltage(float voltage); + /** Get remaining battery battery capacity in mAh * * @return capacity in mAh diff --git a/lib/drivers/bq25896.c b/lib/drivers/bq25896.c index 1fb9d53e..7e3008d6 100644 --- a/lib/drivers/bq25896.c +++ b/lib/drivers/bq25896.c @@ -132,6 +132,33 @@ bool bq25896_is_otg_enabled(FuriHalI2cBusHandle* handle) { return bq25896_regs.r03.OTG_CONFIG; } +uint16_t bq25896_get_vreg_voltage(FuriHalI2cBusHandle* handle) { + furi_hal_i2c_read_reg_8( + handle, BQ25896_ADDRESS, 0x06, (uint8_t*)&bq25896_regs.r06, BQ25896_I2C_TIMEOUT); + return (uint16_t)bq25896_regs.r06.VREG * 16 + 3840; +} + +void bq25896_set_vreg_voltage(FuriHalI2cBusHandle* handle, uint16_t vreg_voltage) { + if(vreg_voltage < 3840) { + // Minimum value is 3840 mV + bq25896_regs.r06.VREG = 0; + } else { + // Find the nearest voltage value (subtract offset, divide into sections) + // Values are truncated downward as needed (e.g. 4200mV -> 4192 mV) + bq25896_regs.r06.VREG = (uint8_t)((vreg_voltage - 3840) / 16); + } + + // Do not allow values above 23 (0x17, 4208mV) + // Exceeding 4.2v will overcharge the battery! + if(bq25896_regs.r06.VREG > 23) { + bq25896_regs.r06.VREG = 23; + } + + // Apply changes + furi_hal_i2c_write_reg_8( + handle, BQ25896_ADDRESS, 0x06, *(uint8_t*)&bq25896_regs.r06, BQ25896_I2C_TIMEOUT); +} + bool bq25896_check_otg_fault(FuriHalI2cBusHandle* handle) { furi_hal_i2c_read_reg_8( handle, BQ25896_ADDRESS, 0x0C, (uint8_t*)&bq25896_regs.r0C, BQ25896_I2C_TIMEOUT); diff --git a/lib/drivers/bq25896.h b/lib/drivers/bq25896.h index c8da0a06..c8a8526a 100644 --- a/lib/drivers/bq25896.h +++ b/lib/drivers/bq25896.h @@ -36,6 +36,15 @@ void bq25896_disable_otg(FuriHalI2cBusHandle* handle); /** Is otg enabled */ bool bq25896_is_otg_enabled(FuriHalI2cBusHandle* handle); +/** Get VREG (charging) voltage in mV */ +uint16_t bq25896_get_vreg_voltage(FuriHalI2cBusHandle* handle); + +/** Set VREG (charging) voltage in mV + * + * Valid range: 3840mV - 4208mV, in steps of 16mV + */ +void bq25896_set_vreg_voltage(FuriHalI2cBusHandle* handle, uint16_t vreg_voltage); + /** Check OTG BOOST Fault status */ bool bq25896_check_otg_fault(FuriHalI2cBusHandle* handle);