Skip to content

Commit

Permalink
charger: limit input current to 500 mA for 3 seconds
Browse files Browse the repository at this point in the history
Prevents race condition during USB-PD negotiation when power supply switches to
higher voltage level with maximum input current lower than 3 A.
  • Loading branch information
surban committed Dec 23, 2023
1 parent 61a60dc commit aa59b6a
Show file tree
Hide file tree
Showing 4 changed files with 102 additions and 64 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,4 @@ devicetree/*.dtbo
/compile_commands.json

enqt*
pi_power_shield1*
18 changes: 16 additions & 2 deletions openemc-firmware/src/board.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ use stm32f1xx_hal::{afio, i2c};

use crate::{
boot,
bq25713::Bq25713Cfg,
bq25713::{Bq25713Cfg, InputCurrentLimit},
cfg::Cfg,
i2c_reg_slave,
supply::{max14636::Max14636, FixedSinkPdo},
supply::{max14636::Max14636, FixedSinkPdo, PowerSupply},
Delay, Duration, PowerMode, I2C_BUFFER_SIZE,
};
use openemc_shared::boot::BootInfo;
Expand Down Expand Up @@ -156,6 +156,20 @@ pub trait Board {
None
}

/// Calculate the input current limit for the connected power supply.
///
/// `active` specifies for how long the supplied power supply configuration has been unchanged.
fn input_current_limit(&mut self, power_supply: &PowerSupply, active: Duration) -> InputCurrentLimit {
InputCurrentLimit {
max_input_current_ma: if active.to_secs() >= 3 {
power_supply.max_current_ma()
} else {
power_supply.max_current_ma().min(500)
},
ico: matches!(power_supply, PowerSupply::UsbDcp),
}
}

/// Sets the power LED to the specified state.
fn set_power_led(&mut self, _state: bool) {}

Expand Down
62 changes: 35 additions & 27 deletions openemc-firmware/src/bq25713.rs
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,15 @@ impl From<Charging> for u8 {
}
}

/// Input current limit.
#[derive(Format, Default, Clone, PartialEq, Eq)]
pub struct InputCurrentLimit {
/// Maximum input current in mA.
pub max_input_current_ma: u32,
/// Specifies whether the input current optimizer (ICO) is enabled.
pub ico: bool,
}

/// BQ25713 battery charger instance.
pub struct Bq25713<I2C> {
addr: u8,
Expand All @@ -246,8 +255,7 @@ pub struct Bq25713<I2C> {
measurement: Option<Bq25713Measurement>,
status: Bq25713Status,
charge_enabled: bool,
max_input_current_ma: Option<u32>,
ico: bool,
input_current_limit: Option<InputCurrentLimit>,
chrg_ok: bool,
_i2c: PhantomData<I2C>,
}
Expand Down Expand Up @@ -299,8 +307,7 @@ where
measurement: None,
status: Default::default(),
charge_enabled: false,
max_input_current_ma: None,
ico: false,
input_current_limit: None,
chrg_ok: false,
_i2c: PhantomData,
}
Expand Down Expand Up @@ -389,8 +396,8 @@ where

// Disable input current.
self.chrg_ok = false;
self.max_input_current_ma = Some(0);
self.program_max_input_current(i2c)?;
self.input_current_limit = Some(InputCurrentLimit::default());
self.program_input_current_limit(i2c)?;
self.program_charge_enabled(i2c)?;

// Enable low power mode.
Expand Down Expand Up @@ -463,52 +470,53 @@ where
self.write(i2c, REG_CHARGER_STATUS_LO, &[0x00])
}

/// Sets the input current limit in mA.
///
/// `ico` specifies whether the input current optimizer (ICO) is enabled.
pub fn set_max_input_current(&mut self, i2c: &mut I2C, ma: u32, ico: bool) -> Result<()> {
/// Sets the input current limit settings.
pub fn set_input_current_limit(&mut self, i2c: &mut I2C, limit: &InputCurrentLimit) -> Result<()> {
self.check_initialized()?;

defmt::debug!("Setting maximum input current to {} mA and ICO to {}", ma, ico);
self.max_input_current_ma = Some(ma);
self.ico = ico;
self.program_max_input_current(i2c)?;
defmt::debug!(
"Setting maximum input current to {} mA and ICO to {}",
limit.max_input_current_ma,
limit.ico
);
self.input_current_limit = Some(limit.clone());
self.program_input_current_limit(i2c)?;

Ok(())
}

/// Gets the input current limit in mA.
pub fn max_input_current(&self) -> u32 {
self.max_input_current_ma.unwrap_or_default()
/// Gets the input current limit.
pub fn input_current_limit(&self) -> Option<&InputCurrentLimit> {
self.input_current_limit.as_ref()
}

/// Programs the maximum input current into the BQ25713.
fn program_max_input_current(&mut self, i2c: &mut I2C) -> Result<()> {
let Some(max_input_current_ma) = self.max_input_current_ma else {
defmt::trace!("No maximum input current has been set");
fn program_input_current_limit(&mut self, i2c: &mut I2C) -> Result<()> {
let Some(limit) = self.input_current_limit.clone() else {
defmt::trace!("No input current limit has been set");
return Ok(());
};

// Enable IDPM.
self.modify(i2c, REG_CHARGE_OPTION_0_LO, |v| v | (1 << 1))?;

if self.chrg_ok {
defmt::trace!("Programming maximum input current {} mA", self.max_input_current_ma);
defmt::trace!("Programming maximum input current {} mA", limit.max_input_current_ma);

// Enable or disable ICO.
self.set_ico(i2c, self.ico)?;
self.set_ico(i2c, limit.ico)?;

// Program input current.
let v = (max_input_current_ma / 50) as u8 & 0b01111111;
let v = (limit.max_input_current_ma / 50) as u8 & 0b01111111;
self.write(i2c, REG_IIN_HOST, &[v])?;

// Enable or disable ICO.
self.set_ico(i2c, self.ico)?;
self.set_ico(i2c, limit.ico)?;

// Verify programmed current.
let r = self.read(i2c, REG_IIN_HOST, 1)?[0];
let r_dpm = self.read(i2c, REG_IIN_DPM, 1)?[0];
if r != v || (r_dpm != v && !self.ico) {
if r != v || (r_dpm != v && !limit.ico) {
defmt::error!(
"Programmed input current {:x}, but read back is {:x} and I_DPM is {:x}",
v,
Expand Down Expand Up @@ -683,7 +691,7 @@ where
self.update_status(i2c)?;
self.update_adc(i2c)?;
self.set_charge_current(i2c, self.cfg.max_charge_ma)?;
self.program_max_input_current(i2c)?;
self.program_input_current_limit(i2c)?;
self.program_charge_enabled(i2c)?;

Ok(())
Expand All @@ -710,7 +718,7 @@ where
}

fn do_chrg_ok_changed(&mut self, i2c: &mut I2C) -> Result<()> {
self.program_max_input_current(i2c)?;
self.program_input_current_limit(i2c)?;
self.program_charge_enabled(i2c)?;
Ok(())
}
Expand Down
85 changes: 50 additions & 35 deletions openemc-firmware/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,8 @@ pub type IrqState = irq::IrqState<{ board::PORTS }>;

#[rtic::app(device = stm32f1::stm32f103, peripherals = true, dispatchers = [SPI1, SPI2, SPI3])]
mod app {
use crate::bq25713::InputCurrentLimit;

use super::*;

/// System timer.
Expand Down Expand Up @@ -860,12 +862,17 @@ mod app {
/// Updates the power supply status.
#[task(
shared = [i2c2, stusb4500, max14636, bq25713, power_supply, irq, board, &power_mode],
local = [first: Option<Instant> = None, charger_set: bool = false],
local = [
first: Option<Instant> = None,
last_change: Option<Instant> = None,
limit_opt: Option<InputCurrentLimit> = None,
],
capacity = 3
)]
fn power_supply_update(cx: power_supply_update::Context) {
let grace_period: Duration = 10u64.secs();
let first = cx.local.first.get_or_insert_with(monotonics::now);
let last_change = cx.local.last_change.get_or_insert_with(monotonics::now);

(
cx.shared.i2c2,
Expand All @@ -890,41 +897,10 @@ mod app {
report = report.merge(&max14636_report);
}

if Some(&report) != power_supply.as_ref() || !*cx.local.charger_set {
// Update power supply status.
if Some(&report) != power_supply.as_ref() {
defmt::info!("Power supply: {:?}", report);

// Configure battery charger.
match (i2c2, bq25713) {
(Some(i2c2), Some(bq25713)) if !report.is_unknown() => {
let max_current = report.max_current_ma();
let ico = matches!(&report, PowerSupply::UsbDcp);

defmt::info!(
"Setting BQ25713 maximum input current to {} mA and ICO to {}",
max_current,
ico
);
let res = bq25713.set_max_input_current(i2c2, max_current, ico).and_then(|_| {
if max_current > 0 && !bq25713.is_charge_enabled() {
bq25713.set_charge_enable(i2c2, true)?;
} else if max_current == 0 && bq25713.is_charge_enabled() {
bq25713.set_charge_enable(i2c2, false)?;
}
Ok(())
});
match res {
Ok(()) => *cx.local.charger_set = true,
Err(err) => {
defmt::error!("Cannot configure BQ25713 charging: {}", err);
*cx.local.charger_set = false;
}
}
}
_ => {
*cx.local.charger_set = true;
}
}

// Check for power on and shutdown in charging mode.
if *cx.shared.power_mode == PowerMode::Charging {
if board.check_power_on_requested() {
Expand All @@ -940,7 +916,46 @@ mod app {

// Store report and notify host.
irq.pend_soft(IrqState::SUPPLY | IrqState::BATTERY);
*power_supply = Some(report);
*power_supply = Some(report.clone());
*last_change = monotonics::now();
}

// Calculate input current limit.
let limit_opt = if report.is_unknown() {
None
} else {
Some(board.input_current_limit(&report, monotonics::now() - *last_change))
};

// Configure battery charger.
match &limit_opt {
_ if limit_opt == *cx.local.limit_opt => (),
Some(limit) => match (i2c2, bq25713) {
(Some(i2c2), Some(bq25713)) => {
defmt::info!(
"Setting BQ25713 maximum input current to {} mA and ICO to {}",
limit.max_input_current_ma,
limit.ico
);

let res = bq25713.set_input_current_limit(i2c2, limit).and_then(|_| {
if limit.max_input_current_ma > 0 && !bq25713.is_charge_enabled() {
bq25713.set_charge_enable(i2c2, true)?;
} else if limit.max_input_current_ma == 0 && bq25713.is_charge_enabled() {
bq25713.set_charge_enable(i2c2, false)?;
}
Ok(())
});
match res {
Ok(()) => *cx.local.limit_opt = limit_opt,
Err(err) => defmt::error!("Cannot configure BQ25713 charging: {}", err),
}
}
_ => {
*cx.local.limit_opt = limit_opt;
}
},
None => *cx.local.limit_opt = limit_opt,
}
});

Expand Down

0 comments on commit aa59b6a

Please sign in to comment.