Skip to content

Commit

Permalink
add LM5066 support (#2000)
Browse files Browse the repository at this point in the history
This adds support for the LM5066 hotswap conntroller. Note that this
also adds a device for Gimletlet boards to allow for the code to be
tested, but Gimletlets will operate correctly without the device
physically connected.
  • Loading branch information
bcantrill authored Feb 5, 2025
1 parent 406e7ee commit 12f3b21
Show file tree
Hide file tree
Showing 12 changed files with 360 additions and 13 deletions.
2 changes: 1 addition & 1 deletion app/gimlet/base.toml
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ max-sizes = {flash = 65536, ram = 16384 }
stacksize = 3800
start = true
task-slots = ["i2c_driver", "sensor", "gimlet_seq"]
notifications = ["timer", "external_badness"]
notifications = ["timer"]

[tasks.hiffy]
name = "task-hiffy"
Expand Down
25 changes: 25 additions & 0 deletions app/gimletlet/app.toml
Original file line number Diff line number Diff line change
Expand Up @@ -259,3 +259,28 @@ pins = [
# SPI6 CS repurposed for debugging
{ name = "DEBUG", pin = { port = "G", pin = 8, af = 0, direction = "output"}}
]

#
# This device definition is only provided as a way to test this driver absent
# a larger board that has it; unless your Gimletlet is in a basement known
# to be roamed by a certain board-killing cat, it's unlikely to have this
# device attached! (But other than the `power` task reporting it as being
# absent, there should be no harm to it being missing.)
#
[[config.i2c.devices]]
device = "lm5066"
controller = 4
address = 0x16
description = "LM5066 evaluation board"
power = { rails = [ "LM5066_EVL_VOUT" ] }
sensors = { voltage = 1, current = 1, temperature = 1 }

[tasks.power]
name = "task-power"
priority = 6
max-sizes = {flash = 65536, ram = 16384 }
stacksize = 3800
start = true
task-slots = ["i2c_driver", "sensor"]
notifications = ["timer"]

6 changes: 4 additions & 2 deletions drv/i2c-devices/src/adm1272.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,9 @@ impl From<Error> for ResponseCode {
Error::BadRead { code, .. } => code,
Error::BadWrite { code, .. } => code,
Error::BadValidation { code, .. } => code,
_ => panic!(),
Error::BadData { .. }
| Error::InvalidData { .. }
| Error::InvalidConfig => ResponseCode::BadDeviceState,
}
}
}
Expand Down Expand Up @@ -85,7 +87,7 @@ enum Trace {
None,
}

ringbuf!(Trace, 32, Trace::None);
ringbuf!(Trace, 8, Trace::None);

impl Adm1272 {
pub fn new(device: &I2cDevice, rsense: Ohms) -> Self {
Expand Down
2 changes: 1 addition & 1 deletion drv/i2c-devices/src/adt7420.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ impl From<Error> for ResponseCode {
match err {
Error::BadValidate { code } => code,
Error::BadTempRead { code } => code,
_ => panic!(),
Error::BadID { .. } => ResponseCode::BadDeviceState,
}
}
}
Expand Down
13 changes: 12 additions & 1 deletion drv/i2c-devices/src/bmr491.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,19 @@ pub struct Bmr491 {

#[derive(Debug)]
pub enum Error {
/// I2C error on PMBus read from device
BadRead { cmd: u8, code: ResponseCode },

/// I2C error on PMBus write to device
BadWrite { cmd: u8, code: ResponseCode },

/// Failed to parse PMBus data from device
BadData { cmd: u8 },

/// I2C error attempting to validate device
BadValidation { cmd: u8, code: ResponseCode },

/// PMBus data returned from device is invalid
InvalidData { err: pmbus::Error },
}

Expand All @@ -49,7 +58,9 @@ impl From<Error> for ResponseCode {
Error::BadRead { code, .. } => code,
Error::BadWrite { code, .. } => code,
Error::BadValidation { code, .. } => code,
_ => panic!(),
Error::BadData { .. } | Error::InvalidData { .. } => {
ResponseCode::BadDeviceState
}
}
}
}
Expand Down
13 changes: 12 additions & 1 deletion drv/i2c-devices/src/isl68224.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,19 @@ impl core::fmt::Display for Isl68224 {

#[derive(Debug)]
pub enum Error {
/// I2C error on PMBus read from device
BadRead { cmd: u8, code: ResponseCode },

/// I2C error on PMBus write to device
BadWrite { cmd: u8, code: ResponseCode },

/// Failed to parse PMBus data from device
BadData { cmd: u8 },

/// I2C error attempting to validate device
BadValidation { cmd: u8, code: ResponseCode },

/// PMBus data returned from device is invalid
InvalidData { err: pmbus::Error },
}

Expand All @@ -56,7 +65,9 @@ impl From<Error> for ResponseCode {
Error::BadRead { code, .. } => code,
Error::BadWrite { code, .. } => code,
Error::BadValidation { code, .. } => code,
_ => panic!(),
Error::BadData { .. } | Error::InvalidData { .. } => {
ResponseCode::BadDeviceState
}
}
}
}
Expand Down
2 changes: 2 additions & 0 deletions drv/i2c-devices/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
//! - [`ds2482`]: DS2482-100 1-wire initiator
//! - [`emc2305`]: EMC2305 fan driver
//! - [`isl68224`]: ISL68224 power controller
//! - [`lm5066`]: LM5066 hot swap controller
//! - [`ltc4282`]: LTC4282 high current hot swap controller
//! - [`m24c02`]: M24C02 EEPROM, used in MWOCP68 power shelf
//! - [`m2_hp_only`]: M.2 drive; identical to `nvme_bmc`, with the limitation
Expand Down Expand Up @@ -238,6 +239,7 @@ pub mod bmr491;
pub mod ds2482;
pub mod emc2305;
pub mod isl68224;
pub mod lm5066;
pub mod ltc4282;
pub mod m24c02;
pub mod m2_hp_only;
Expand Down
246 changes: 246 additions & 0 deletions drv/i2c-devices/src/lm5066.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

//! Driver for the LM5066 hot-swap controller
use core::cell::Cell;

use crate::{
pmbus_validate, BadValidation, CurrentSensor, TempSensor, Validate,
VoltageSensor,
};
use drv_i2c_api::*;
use num_traits::float::FloatCore;
use pmbus::commands::*;
use ringbuf::*;
use userlib::units::*;

#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum Error {
/// I2C error on PMBus read from device
BadRead { cmd: u8, code: ResponseCode },

/// I2C error on PMBus write to device
BadWrite { cmd: u8, code: ResponseCode },

/// Failed to parse PMBus data from device
BadData { cmd: u8 },

/// I2C error attempting to validate device
BadValidation { cmd: u8, code: ResponseCode },

/// PMBus data returned from device is invalid
InvalidData { err: pmbus::Error },

/// Device setup is invalid
InvalidDeviceSetup,
}

impl From<BadValidation> for Error {
fn from(value: BadValidation) -> Self {
Self::BadValidation {
cmd: value.cmd,
code: value.code,
}
}
}

impl From<pmbus::Error> for Error {
fn from(err: pmbus::Error) -> Self {
Error::InvalidData { err }
}
}

impl From<Error> for ResponseCode {
fn from(err: Error) -> Self {
match err {
Error::BadRead { code, .. } => code,
Error::BadWrite { code, .. } => code,
Error::BadValidation { code, .. } => code,
Error::BadData { .. }
| Error::InvalidData { .. }
| Error::InvalidDeviceSetup => ResponseCode::BadDeviceState,
}
}
}

#[derive(Copy, Clone)]
#[allow(dead_code)]
struct Coefficients {
current: pmbus::Coefficients,
power: pmbus::Coefficients,
}

#[derive(Copy, Clone)]
pub enum CurrentLimitStrap {
/// CL pin is strapped to VDD, denoting the Low setting
VDD,
/// CL pin is strapped to GND (or left floating), denoting the high setting
GND,
}

pub struct Lm5066 {
/// Underlying I2C device
device: I2cDevice,
/// Value of the rsense resistor, in milliohms
rsense: i32,
/// Sense of current limit pin
cl: CurrentLimitStrap,
/// Our (cached) coefficients
coefficients: Cell<Option<Coefficients>>,
/// Our (cached) device setup
device_setup: Cell<Option<lm5066::DEVICE_SETUP::CommandData>>,
}

impl core::fmt::Display for Lm5066 {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "lm5066: {}", &self.device)
}
}

#[derive(Copy, Clone, PartialEq)]
enum Trace {
CurrentCoefficients(pmbus::Coefficients),
PowerCoefficients(pmbus::Coefficients),
DeviceSetup(lm5066::DEVICE_SETUP::CommandData),
None,
}

ringbuf!(Trace, 8, Trace::None);

impl Lm5066 {
pub fn new(
device: &I2cDevice,
rsense: Ohms,
cl: CurrentLimitStrap,
) -> Self {
Self {
device: *device,
rsense: (rsense.0 * 1000.0).round() as i32,
cl,
coefficients: Cell::new(None),
device_setup: Cell::new(None),
}
}

fn read_device_setup(
&self,
) -> Result<lm5066::DEVICE_SETUP::CommandData, Error> {
if let Some(ref device_setup) = self.device_setup.get() {
return Ok(*device_setup);
}

let device_setup = pmbus_read!(self.device, lm5066::DEVICE_SETUP)?;
ringbuf_entry!(Trace::DeviceSetup(device_setup));
self.device_setup.set(Some(device_setup));

Ok(device_setup)
}

///
/// The coefficients for the LM5066 depend on the value of the current
/// sense resistor and the sense of the current limit (CL) strap.
/// Unfortunately, DEVICE_SETUP will not tell us the physical sense of
/// this strap; we rely on this information to be provided when the
/// device is initialized.
///
fn load_coefficients(&self) -> Result<Coefficients, Error> {
use lm5066::DEVICE_SETUP::*;

if let Some(coefficients) = self.coefficients.get() {
return Ok(coefficients);
}

let device_setup = self.read_device_setup()?;

let setting = device_setup
.get_current_setting()
.ok_or(Error::InvalidDeviceSetup)?;

let config = device_setup
.get_current_config()
.ok_or(Error::InvalidDeviceSetup)?;

let cl = match config {
CurrentConfig::Pin => self.cl,
CurrentConfig::SMBus => match setting {
CurrentSetting::Low => CurrentLimitStrap::VDD,
CurrentSetting::High => CurrentLimitStrap::GND,
},
};

//
// From Table 43 of the LM5066 datasheet. Note that the datasheet has
// an admonishment about adjusting R to keep m to within a signed
// 16-bit quantity (that is, no larger than 32767), but we actually
// treat m as a 32-bit quantity so there is no need to clamp it here.
// (At the maximum of 200 mΩ, m is well within a 32-bit quantity.)
//
let current = match cl {
CurrentLimitStrap::GND => pmbus::Coefficients {
m: 5405 * self.rsense,
b: -600,
R: -2,
},
CurrentLimitStrap::VDD => pmbus::Coefficients {
m: 10753 * self.rsense,
b: -1200,
R: -2,
},
};

ringbuf_entry!(Trace::CurrentCoefficients(current));

let power = match cl {
CurrentLimitStrap::GND => pmbus::Coefficients {
m: 605 * self.rsense,
b: -8000,
R: -3,
},
CurrentLimitStrap::VDD => pmbus::Coefficients {
m: 1204 * self.rsense,
b: -6000,
R: -3,
},
};

ringbuf_entry!(Trace::PowerCoefficients(power));

self.coefficients.set(Some(Coefficients { current, power }));
Ok(self.coefficients.get().unwrap())
}

pub fn i2c_device(&self) -> &I2cDevice {
&self.device
}
}

impl Validate<Error> for Lm5066 {
fn validate(device: &I2cDevice) -> Result<bool, Error> {
let expected = b"LM5066\0\0";
pmbus_validate(device, CommandCode::MFR_MODEL, expected)
.map_err(Into::into)
}
}

impl TempSensor<Error> for Lm5066 {
fn read_temperature(&self) -> Result<Celsius, Error> {
let temp = pmbus_read!(self.device, lm5066::READ_TEMPERATURE_1)?;
Ok(Celsius(temp.get()?.0))
}
}

impl CurrentSensor<Error> for Lm5066 {
fn read_iout(&self) -> Result<Amperes, Error> {
let iout = pmbus_read!(self.device, lm5066::MFR_READ_IIN)?;
Ok(Amperes(iout.get(&self.load_coefficients()?.current)?.0))
}
}

impl VoltageSensor<Error> for Lm5066 {
fn read_vout(&self) -> Result<Volts, Error> {
let vout = pmbus_read!(self.device, lm5066::READ_VOUT)?;
Ok(Volts(vout.get()?.0))
}
}
Loading

0 comments on commit 12f3b21

Please sign in to comment.