From 331935799299ec286d695c9fed24af4766727572 Mon Sep 17 00:00:00 2001 From: Bryan Cantrill Date: Mon, 25 Nov 2024 13:36:02 -0800 Subject: [PATCH] add support for PSU firmware update (#1934) This adds support for the PSC to perform automatic and autonomous updates of the PSU firmware on the MWOCP68-3600 series via a new task, psu_update. --- Cargo.lock | 27 +- Cargo.toml | 1 + app/psc/base.toml | 9 +- drv/i2c-devices/src/mwocp68.rs | 416 +++++++++++++++++++++++- drv/psc-psu-update/Cargo.toml | 27 ++ drv/psc-psu-update/build.rs | 9 + drv/psc-psu-update/src/main.rs | 390 ++++++++++++++++++++++ drv/psc-psu-update/src/mwocp68-0701.bin | Bin 0 -> 32768 bytes drv/psc-psu-update/src/mwocp68-0762.bin | Bin 0 -> 32768 bytes task/sensor-polling/src/main.rs | 1 + 10 files changed, 871 insertions(+), 9 deletions(-) create mode 100644 drv/psc-psu-update/Cargo.toml create mode 100644 drv/psc-psu-update/build.rs create mode 100644 drv/psc-psu-update/src/main.rs create mode 100644 drv/psc-psu-update/src/mwocp68-0701.bin create mode 100644 drv/psc-psu-update/src/mwocp68-0762.bin diff --git a/Cargo.lock b/Cargo.lock index 3bf1cf0cf..5e164766b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -143,6 +143,12 @@ dependencies = [ "nodrop", ] +[[package]] +name = "array-init" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d62b7694a562cdf5a74227903507c56ab2cc8bdd1f781ed5cb4cf9c9f810bfc" + [[package]] name = "arrayvec" version = "0.7.4" @@ -1753,6 +1759,21 @@ dependencies = [ "userlib", ] +[[package]] +name = "drv-psc-psu-update" +version = "0.1.0" +dependencies = [ + "array-init 2.1.0", + "build-i2c", + "build-util", + "counters", + "drv-i2c-api", + "drv-i2c-devices", + "ringbuf", + "static-cell", + "userlib", +] + [[package]] name = "drv-psc-seq-api" version = "0.1.0" @@ -3875,8 +3896,8 @@ checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" [[package]] name = "pmbus" -version = "0.1.1" -source = "git+https://github.com/oxidecomputer/pmbus#735a70bbc90707a963ec1731c8a0fa427a013f21" +version = "0.1.4" +source = "git+https://github.com/oxidecomputer/pmbus#44568ce7eb86fe0b03dd088a75ad76ea0d8529bb" dependencies = [ "anyhow", "convert_case", @@ -4342,7 +4363,7 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca37e3e4d1b39afd7ff11ee4e947efae85adfddf4841787bfa47c470e96dc26d" dependencies = [ - "array-init", + "array-init 0.0.4", "serde", "smallvec 0.6.14", ] diff --git a/Cargo.toml b/Cargo.toml index 1c376057b..80a83aaf7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,6 +31,7 @@ path = "lib/counters" [workspace.dependencies] anyhow = { version = "1.0.31", default-features = false, features = ["std"] } +array-init = { version = "2.1.0" } arrayvec = { version = "0.7.4", default-features = false } atty = { version = "0.2", default-features = false } bitfield = { version = "0.13", default-features = false } diff --git a/app/psc/base.toml b/app/psc/base.toml index 55396ba01..9a303fa2e 100644 --- a/app/psc/base.toml +++ b/app/psc/base.toml @@ -6,7 +6,7 @@ fwid = true [kernel] name = "psc" -requires = {flash = 32868, ram = 5216} +requires = {flash = 32868, ram = 6000} features = ["dump"] [caboose] @@ -298,6 +298,13 @@ max-sizes = {flash = 16384, ram = 2048 } start = true task-slots = ["i2c_driver", "sensor"] +[tasks.psu_update] +name = "drv-psc-psu-update" +priority = 4 +max-sizes = {flash = 65536, ram = 8192 } +start = true +task-slots = ["i2c_driver"] + [tasks.dump_agent] name = "task-dump-agent" priority = 5 diff --git a/drv/i2c-devices/src/mwocp68.rs b/drv/i2c-devices/src/mwocp68.rs index 5dccabb02..516612014 100644 --- a/drv/i2c-devices/src/mwocp68.rs +++ b/drv/i2c-devices/src/mwocp68.rs @@ -27,13 +27,68 @@ pub struct Mwocp68 { mode: Cell>, } +#[derive(Copy, Clone, PartialEq)] +pub struct FirmwareRev(pub [u8; 4]); + +#[derive(Copy, Clone, PartialEq, Default)] +pub struct SerialNumber(pub [u8; 12]); + +// +// The boot loader command -- sent via BOOT_LOADER_CMD -- is unfortunately odd +// in that its command code is overloaded with BOOT_LOADER_STATUS. (That is, +// a read to the command code is BOOT_LOADER_STATUS, a write is +// BOOT_LOADER_CMD.) This is behavior that the PMBus crate didn't necessarily +// envision, so it can't necessarily help us out; we define the single-byte +// payload codes here rather than declaratively in the PMBus crate. +// +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +#[repr(u8)] +pub enum BootLoaderCommand { + ClearStatus = 0x00, + RestartProgramming = 0x01, + BootPrimary = 0x12, + BootSecondary = 0x02, + BootPSUFirmware = 0x03, +} + #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub enum Error { - BadRead { cmd: u8, code: ResponseCode }, - BadWrite { cmd: u8, code: ResponseCode }, - BadData { cmd: u8 }, - BadValidation { cmd: u8, code: ResponseCode }, - InvalidData { err: pmbus::Error }, + BadRead { + cmd: u8, + code: ResponseCode, + }, + BadWrite { + cmd: u8, + code: ResponseCode, + }, + BadData { + cmd: u8, + }, + BadValidation { + cmd: u8, + code: ResponseCode, + }, + InvalidData { + err: pmbus::Error, + }, + BadFirmwareRevRead { + code: ResponseCode, + }, + BadFirmwareRev { + index: u8, + }, + BadFirmwareRevLength, + UpdateInBootLoader, + UpdateNotInBootLoader, + UpdateAlreadySuccessful, + BadBootLoaderStatus { + data: u8, + }, + BadBootLoaderCommand { + cmd: BootLoaderCommand, + code: ResponseCode, + }, + ChecksumNotSuccessful, } impl From for Error { @@ -62,6 +117,66 @@ impl From for Error { } } +/// +/// Defines the state of the firmware update. Once `UpdateSuccessful` +/// has been returned, the update is complete. +/// +#[derive(Copy, Clone, Debug, PartialEq)] +pub enum UpdateState { + /// The boot loader key has been written + WroteBootLoaderKey, + + /// The product key has been written + WroteProductKey, + + /// The boot loader has been booted + BootedBootLoader, + + /// Programming of firmware has been indicated to have started + StartedProgramming, + + /// A block has been written; the next offset is at [`offset`], and the + /// running checksum is in [`checksum`] + WroteBlock { offset: usize, checksum: u64 }, + + /// The last block has been written; the checksum is in [`checksum`] + WroteLastBlock { checksum: u64 }, + + /// The checksum has been sent for verification + SentChecksum, + + /// The checksum has been verified + VerifiedChecksum, + + /// The PSU has been rebooted + RebootedPSU, + + /// The entire update is complete and successful + UpdateSuccessful, +} + +impl UpdateState { + /// + /// Return the milliseconds of delay associated with the current state. + /// Note that some of these values differ slightly from Murata's "PSU + /// Firmware Update Process" document in that they reflect revised + /// guidance from Murata. + /// + fn delay_ms(&self) -> u64 { + match self { + Self::WroteBootLoaderKey => 3_000, + Self::WroteProductKey => 3_000, + Self::BootedBootLoader => 1_000, + Self::StartedProgramming => 2_000, + Self::WroteBlock { .. } | Self::WroteLastBlock { .. } => 100, + Self::SentChecksum => 2_000, + Self::VerifiedChecksum => 4_000, + Self::RebootedPSU => 5_000, + Self::UpdateSuccessful => 0, + } + } +} + impl Mwocp68 { pub fn new(device: &I2cDevice, index: u8) -> Self { Mwocp68 { @@ -350,6 +465,297 @@ impl Mwocp68 { Ok(val) } + /// Will return true if the device is present and valid -- false otherwise + pub fn present(&self) -> bool { + Mwocp68::validate(&self.device).unwrap_or_default() + } + + pub fn power_good(&self) -> Result { + use commands::mwocp68::STATUS_WORD::*; + + let status = pmbus_read!(self.device, STATUS_WORD)?; + Ok(status.get_power_good_status() == Some(PowerGoodStatus::PowerGood)) + } + + /// + /// Returns the firmware revision of the primary MCU (AC input side). + /// + pub fn firmware_revision(&self) -> Result { + const REVISION_LEN: usize = 14; + + let mut data = [0u8; REVISION_LEN]; + let expected = b"XXXX-YYYY-0000"; + + let len = self + .device + .read_block(CommandCode::MFR_REVISION as u8, &mut data) + .map_err(|code| Error::BadFirmwareRevRead { code })?; + + // + // Per ACAN-114, we are expecting this to be of the format: + // + // XXXX-YYYY-0000 + // + // Where XXXX is the firmware revision on the primary MCU (AC input + // side) and YYYY is the firmware revision on the secondary MCU (DC + // output side). We aren't going to be rigid about the format of + // either revision, but we will be rigid about the rest of the format. + // + if len != REVISION_LEN { + return Err(Error::BadFirmwareRevLength); + } + + for index in 0..len { + if expected[index] == b'X' || expected[index] == b'Y' { + continue; + } + + if data[index] != expected[index] { + return Err(Error::BadFirmwareRev { index: index as u8 }); + } + } + + // + // Return the primary MCU version + // + Ok(FirmwareRev([data[0], data[1], data[2], data[3]])) + } + + /// + /// Returns the serial number of the PSU. + /// + pub fn serial_number(&self) -> Result { + let mut serial = SerialNumber::default(); + + let _ = self + .device + .read_block(CommandCode::MFR_SERIAL as u8, &mut serial.0) + .map_err(|code| Error::BadFirmwareRevRead { code })?; + + Ok(serial) + } + + fn get_boot_loader_status( + &self, + ) -> Result { + use pmbus::commands::mwocp68::CommandCode; + let cmd = CommandCode::BOOT_LOADER_STATUS as u8; + let mut data = [0u8]; + + match self.device.read_block(cmd, &mut data) { + Ok(1) => Ok(()), + Ok(len) => Err(Error::BadBootLoaderStatus { data: len as u8 }), + Err(code) => Err(Error::BadRead { cmd, code }), + }?; + + match BOOT_LOADER_STATUS::CommandData::from_slice(&data[0..]) { + Some(status) => Ok(status), + None => Err(Error::BadBootLoaderStatus { data: data[0] }), + } + } + + fn get_boot_loader_mode(&self) -> Result { + // + // This unwrap is safe because the boot loader mode is a single bit. + // + Ok(self.get_boot_loader_status()?.get_mode().unwrap()) + } + + fn boot_loader_command(&self, cmd: BootLoaderCommand) -> Result<(), Error> { + use pmbus::commands::mwocp68::CommandCode; + + // + // The great unfortunateness: BOOT_LOADER_STATUS is overloaded to + // be BOOT_LOADER_CMD on a write. + // + let data = [CommandCode::BOOT_LOADER_STATUS as u8, 1, cmd as u8]; + + self.device + .write(&data) + .map_err(|code| Error::BadBootLoaderCommand { cmd, code })?; + + Ok(()) + } + + /// + /// Perform a firmware update, implementating the procedure contained + /// within Murata's "PSU Firmware Update Process" document. Note that + /// this function must be called initially with a state of `None`; it will + /// return either an error, or the next state in the update process, + /// along with a specified delay in milliseconds. It is up to the caller + /// to assure that the returned delay has been observed before calling + /// back into continue the update. + /// + pub fn update( + &self, + state: Option, + payload: &[u8], + ) -> Result<(UpdateState, u64), Error> { + use pmbus::commands::mwocp68::CommandCode; + use BOOT_LOADER_STATUS::Mode; + + let write_boot_loader_key = || -> Result { + const MWOCP68_BOOT_LOADER_KEY: &[u8] = b"InVe"; + let mut data = [0u8; MWOCP68_BOOT_LOADER_KEY.len() + 2]; + + data[0] = CommandCode::BOOT_LOADER_KEY as u8; + data[1] = MWOCP68_BOOT_LOADER_KEY.len() as u8; + data[2..].copy_from_slice(MWOCP68_BOOT_LOADER_KEY); + + self.device + .write(&data) + .map_err(|code| Error::BadWrite { cmd: data[0], code })?; + + Ok(UpdateState::WroteBootLoaderKey) + }; + + let write_product_key = || -> Result { + const MWOCP68_PRODUCT_KEY: &[u8] = b"M5813-0000000000"; + let mut data = [0u8; MWOCP68_PRODUCT_KEY.len() + 1]; + + data[0] = CommandCode::BOOT_LOADER_PRODUCT_KEY as u8; + data[1..].copy_from_slice(MWOCP68_PRODUCT_KEY); + + self.device + .write(&data) + .map_err(|code| Error::BadWrite { cmd: data[0], code })?; + + Ok(UpdateState::WroteProductKey) + }; + + let boot_boot_loader = || -> Result { + self.boot_loader_command(BootLoaderCommand::BootPrimary)?; + Ok(UpdateState::BootedBootLoader) + }; + + let start_programming = || -> Result { + self.boot_loader_command(BootLoaderCommand::RestartProgramming)?; + Ok(UpdateState::StartedProgramming) + }; + + let write_block = || -> Result { + const BLOCK_LEN: usize = 32; + + let (mut offset, mut checksum) = match state { + Some(UpdateState::WroteBlock { offset, checksum }) => { + (offset, checksum) + } + Some(UpdateState::StartedProgramming) => (0, 0), + _ => panic!(), + }; + + let mut data = [0u8; BLOCK_LEN + 1]; + data[0] = CommandCode::BOOT_LOADER_MEMORY_BLOCK as u8; + data[1..].copy_from_slice(&payload[offset..offset + BLOCK_LEN]); + + self.device + .write(&data) + .map_err(|code| Error::BadWrite { cmd: data[0], code })?; + + checksum = data[1..] + .iter() + .fold(checksum, |c, &d| c.wrapping_add(d.into())); + offset += BLOCK_LEN; + + if offset >= payload.len() { + Ok(UpdateState::WroteLastBlock { checksum }) + } else { + Ok(UpdateState::WroteBlock { offset, checksum }) + } + }; + + let send_checksum = || -> Result { + let Some(UpdateState::WroteLastBlock { checksum }) = state else { + panic!(); + }; + + let data = [ + CommandCode::IMAGE_CHECKSUM as u8, + 2, + (checksum & 0xff) as u8, + ((checksum >> 8) & 0xff) as u8, + ]; + + self.device + .write(&data) + .map_err(|code| Error::BadWrite { cmd: data[0], code })?; + + Ok(UpdateState::SentChecksum) + }; + + let verify_checksum = || -> Result { + use BOOT_LOADER_STATUS::ChecksumSuccessful; + + let status = self.get_boot_loader_status()?; + + match status.get_checksum_successful() { + Some(ChecksumSuccessful::Successful) => { + Ok(UpdateState::VerifiedChecksum) + } + Some(ChecksumSuccessful::NotSuccessful) | None => { + Err(Error::ChecksumNotSuccessful) + } + } + }; + + let reboot_psu = || -> Result { + self.boot_loader_command(BootLoaderCommand::BootPSUFirmware)?; + Ok(UpdateState::RebootedPSU) + }; + + let verify_success = || -> Result { + Ok(UpdateState::UpdateSuccessful) + }; + + // + // We want to confirm that our boot loader is in the state that + // we think it should be in. On the one hand, this will fail in + // a non-totally-unreasonable fashion if we don't check this -- but + // we have an opportunity to assert our in-device state and fail + // cleanly if it doesn't match, and it feels like we should take it. + // + let expected = match state { + None + | Some(UpdateState::WroteBootLoaderKey) + | Some(UpdateState::WroteProductKey) + | Some(UpdateState::RebootedPSU) => Mode::NotBootLoader, + + Some(UpdateState::BootedBootLoader) + | Some(UpdateState::StartedProgramming) + | Some(UpdateState::WroteBlock { .. }) + | Some(UpdateState::WroteLastBlock { .. }) + | Some(UpdateState::SentChecksum) + | Some(UpdateState::VerifiedChecksum) => Mode::BootLoader, + + Some(UpdateState::UpdateSuccessful) => { + return Err(Error::UpdateAlreadySuccessful); + } + }; + + if self.get_boot_loader_mode()? != expected { + return Err(match expected { + Mode::BootLoader => Error::UpdateNotInBootLoader, + Mode::NotBootLoader => Error::UpdateInBootLoader, + }); + } + + let next = match state { + None => write_boot_loader_key()?, + Some(UpdateState::WroteBootLoaderKey) => write_product_key()?, + Some(UpdateState::WroteProductKey) => boot_boot_loader()?, + Some(UpdateState::BootedBootLoader) => start_programming()?, + Some(UpdateState::StartedProgramming) + | Some(UpdateState::WroteBlock { .. }) => write_block()?, + Some(UpdateState::WroteLastBlock { .. }) => send_checksum()?, + Some(UpdateState::SentChecksum) => verify_checksum()?, + Some(UpdateState::VerifiedChecksum) => reboot_psu()?, + Some(UpdateState::RebootedPSU) => verify_success()?, + Some(UpdateState::UpdateSuccessful) => panic!(), + }; + + Ok((next, next.delay_ms())) + } + pub fn i2c_device(&self) -> &I2cDevice { &self.device } diff --git a/drv/psc-psu-update/Cargo.toml b/drv/psc-psu-update/Cargo.toml new file mode 100644 index 000000000..bfc2b02a9 --- /dev/null +++ b/drv/psc-psu-update/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "drv-psc-psu-update" +description = "Task for PSU firmware update" +version = "0.1.0" +edition = "2021" + +[dependencies] +drv-i2c-api = { path = "../i2c-api" } +drv-i2c-devices = { path = "../i2c-devices" } +counters = { path = "../../lib/counters" } +ringbuf = { path = "../../lib/ringbuf", features = ["counters"] } +userlib = { path = "../../sys/userlib", features = ["panic-messages"] } +static-cell = { path = "../../lib/static-cell" } +array-init.workspace = true + +[build-dependencies] +build-util = {path = "../../build/util"} +build-i2c = { path = "../../build/i2c" } + +[[bin]] +name = "drv-psc-psu-update" +test = false +doctest = false +bench = false + +[lints] +workspace = true diff --git a/drv/psc-psu-update/build.rs b/drv/psc-psu-update/build.rs new file mode 100644 index 000000000..660f92b3f --- /dev/null +++ b/drv/psc-psu-update/build.rs @@ -0,0 +1,9 @@ +// 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/. + +fn main() -> Result<(), Box> { + build_util::expose_target_board(); + build_i2c::codegen(build_i2c::Disposition::Devices)?; + Ok(()) +} diff --git a/drv/psc-psu-update/src/main.rs b/drv/psc-psu-update/src/main.rs new file mode 100644 index 000000000..5584d6675 --- /dev/null +++ b/drv/psc-psu-update/src/main.rs @@ -0,0 +1,390 @@ +// 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/. + +//! Server for updating all PSUs to the contained binary payload. +//! +//! We have the capacity to dynamically update the MWOCP68 power supply units +//! connected to the PSC. This update does not involve any interruption of the +//! PSU while it is being performed, but necessitates a reset of the PSU once +//! completed. We want these updates to be automatic and autonomous; there is +//! little that the control plane can know that we do not know -- and even less +//! for the operator. +//! +//! This task contains within it a payload that is the desired firmware image +//! (`MWOCP68_FIRMWARE_PAYLOAD`), along with the `MFR_REVISION` that that +//! pyaload represents (`MWOCP68_FIRMWARE_VERSION`). This task will check +//! every PSU periodically to see if the PSU's firmware revision matches the +//! revision specified as corresponding to the payload; if they don't match (or +//! rather, until they do), an attempt will be made to update the PSU. Each +//! PSU will be updated sequentially: while we can expect a properly configured +//! and operating rack to support the loss of any one PSU, we do not want to +//! induce the loss of more than one simultaneously due to update. If an +//! update fails, the update of that PSU will be exponentially backed off and +//! repeated (up to a backoff of about once per day). Note that we will +//! continue to check PSUs that we have already updated should they be replaced +//! with a PSU with downrev firmware. The state of this task can be +//! ascertained by looking at the `PSU` variable (which contains all of the +//! per-PSU state) as well as the ring buffer. +//! + +#![no_std] +#![no_main] + +use drv_i2c_api::*; +use drv_i2c_devices::mwocp68::{ + Error as Mwocp68Error, FirmwareRev, Mwocp68, SerialNumber, UpdateState, +}; +use ringbuf::*; +use static_cell::ClaimOnceCell; +use userlib::*; + +use core::ops::Add; + +task_slot!(I2C, i2c_driver); + +const TIMER_INTERVAL_MS: u64 = 10_000; + +use i2c_config::devices; + +#[cfg(any(target_board = "psc-b", target_board = "psc-c"))] +static DEVICES: [fn(TaskId) -> I2cDevice; 6] = [ + devices::mwocp68_psu0mcu, + devices::mwocp68_psu1mcu, + devices::mwocp68_psu2mcu, + devices::mwocp68_psu3mcu, + devices::mwocp68_psu4mcu, + devices::mwocp68_psu5mcu, +]; + +static PSU: ClaimOnceCell<[Psu; 6]> = ClaimOnceCell::new( + [Psu { + last_checked: None, + present: None, + power_good: None, + serial_number: None, + firmware_matches: None, + firmware_revision: None, + update_started: None, + update_succeeded: None, + update_failure: None, + update_backoff: None, + }; 6], +); + +#[derive(Copy, Clone, Debug, PartialEq, counters::Count)] +enum Trace { + #[count(skip)] + None, + PowerGoodFailed(u8, drv_i2c_devices::mwocp68::Error), + FirmwareRevFailed(u8, drv_i2c_devices::mwocp68::Error), + AttemptingUpdate(u8), + BackingOff(u8), + UpdateFailed, + UpdateFailedState(Option), + UpdateFailure(Mwocp68Error), + UpdateState(UpdateState), + WroteBlock, + UpdateSucceeded(u8), + UpdateDelay(u64), + PSUReplaced(u8), + SerialNumberError(u8, drv_i2c_devices::mwocp68::Error), + PGError(u8, drv_i2c_devices::mwocp68::Error), + PowerNotGood(u8), +} + +// +// The actual firmware revision and payload. It is very important that the +// revision match the revision contained within the payload, lest we will +// believe that the update has failed when it has in fact succeeded! +// +const MWOCP68_FIRMWARE_REV: FirmwareRev = FirmwareRev(*b"0762"); +const MWOCP68_FIRMWARE_PAYLOAD: &[u8] = include_bytes!("mwocp68-0762.bin"); + +counted_ringbuf!(Trace, 64, Trace::None); + +#[derive(Copy, Clone, PartialOrd, PartialEq)] +struct Ticks(u64); + +impl Ticks { + fn now() -> Self { + Self(sys_get_timer().now) + } +} + +impl Add for Ticks { + type Output = Self; + + fn add(self, other: Self) -> Self { + Self(self.0 + other.0) + } +} + +#[derive(Copy, Clone, Default)] +struct Psu { + /// When did we last check this device? + last_checked: Option, + + /// Is the device physically present? + present: Option, + + /// Is the device on and with POWER_GOOD set? + power_good: Option, + + /// The last serial number read + serial_number: Option, + + /// The last firmware revision read + firmware_revision: Option, + + /// Does the firmware we have match the firmware here? + firmware_matches: Option, + + /// What time did we start an update? + update_started: Option, + + /// What time did the update complete? + update_succeeded: Option, + + /// What time did the update last fail, if any? + update_failure: Option<(Ticks, Option, Option)>, + + /// How long should the next update backoff, if at all? (In ticks.) + update_backoff: Option, +} + +impl Psu { + fn update_should_be_attempted(&mut self, dev: &Mwocp68, ndx: u8) -> bool { + let now = Ticks::now(); + + self.last_checked = Some(now); + self.power_good = None; + self.firmware_matches = None; + self.firmware_revision = None; + + if !dev.present() { + self.present = Some(false); + + // + // If we are seeing our device as not present, we will clear our + // backoff value: if/when a PSU is plugged back in, we want to + // attempt to update it immediately if the firmware revision + // doesn't match our payload. + // + self.update_backoff = None; + return false; + } + + self.present = Some(true); + + // + // If we can read the serial number, we're going to store it -- and + // if we previously stored one and it DOESN'T match, we want to + // clear our backoff value so we don't delay at all in potentially + // trying to update the firmware of the (replaced) PSU. (If we can't + // read the serial number at all, we want to continue to potentially + // update the firmware.) + // + match (dev.serial_number(), self.serial_number) { + (Ok(read), Some(stored)) if read != stored => { + ringbuf_entry!(Trace::PSUReplaced(ndx)); + self.update_backoff = None; + self.serial_number = Some(read); + } + (Ok(_), Some(_)) => {} + (Ok(read), None) => { + self.serial_number = Some(read); + } + (Err(code), _) => { + ringbuf_entry!(Trace::SerialNumberError(ndx, code)); + } + } + + match dev.power_good() { + Ok(power_good) => { + self.power_good = Some(power_good); + + if !power_good { + return false; + } + } + Err(err) => { + ringbuf_entry!(Trace::PowerGoodFailed(ndx, err)); + return false; + } + } + + match dev.firmware_revision() { + Ok(revision) => { + self.firmware_revision = Some(revision); + + if revision == MWOCP68_FIRMWARE_REV { + self.firmware_matches = Some(true); + return false; + } + + self.firmware_matches = Some(false); + } + Err(err) => { + ringbuf_entry!(Trace::FirmwareRevFailed(ndx, err)); + return false; + } + } + + if let (Some(started), Some(backoff)) = + (self.update_started, self.update_backoff) + { + if started + backoff > now { + // + // Indicate we are backing off, but in a way that won't flood + // the ring buffer with the backing off of a single PSU. + // + ringbuf_entry!(Trace::BackingOff(ndx)); + return false; + } + } + + true + } + + fn update_firmware(&mut self, dev: &Mwocp68, ndx: u8) { + ringbuf_entry!(Trace::AttemptingUpdate(ndx)); + self.update_started = Some(Ticks::now()); + + // + // Before we start, update our backoff. We'll double our backoff, up + // to a cap of around a day. + // + self.update_backoff = match self.update_backoff { + Some(backoff) if backoff.0 < 86_400_000 => { + Some(Ticks(backoff.0 * 2)) + } + Some(backoff) => Some(backoff), + None => Some(Ticks(75_000)), + }; + + let mut state = None; + + let mut update_failed = |state, err| { + // + // We failed. Record everything we can! + // + if let Some(err) = err { + ringbuf_entry!(Trace::UpdateFailure(err)); + } + + ringbuf_entry!(Trace::UpdateFailed); + ringbuf_entry!(Trace::UpdateFailedState(state)); + self.update_failure = Some((Ticks::now(), state, err)); + }; + + loop { + match dev.update(state, MWOCP68_FIRMWARE_PAYLOAD) { + Err(err) => { + update_failed(state, Some(err)); + break; + } + + Ok((UpdateState::UpdateSuccessful, _)) => { + let state = Some(UpdateState::UpdateSuccessful); + + // + // We should be back up! As a final measure, we are going + // to check that the firmware revision matches the + // revision we think we just wrote. If it doesn't, there + // is something amiss: it may be that the image is + // corrupt or that the version doesn't otherwise match. + // Regardless, we consider that to be an update failure. + // + match dev.firmware_revision() { + Ok(revision) if revision != MWOCP68_FIRMWARE_REV => { + update_failed(state, None); + break; + } + + Err(err) => { + update_failed(state, Some(err)); + break; + } + + Ok(_) => {} + } + + // + // We're on the new firmware! And now, a final final + // check: make sure that we are power-good. It is very + // unclear what to do here if are NOT power-good: we know + // that we WERE power-good before we started, so it + // certainly seems possible that we have put a firmware + // update on this PSU which has somehow incapacitated it. + // We would rather not put the system in a compromised + // state by continuing to potentially brick PSUs -- but we + // also want to assure that we make progress should this + // ever resolve (e.g., by pulling the bricked PSU). We will + // remain here until we see the updated PSU go power-good; + // if it never does, we will at least not attempt to put + // the (potentially) bad update anywhere else! + // + loop { + match dev.power_good() { + Ok(power_good) if power_good => break, + Ok(_) => { + ringbuf_entry!(Trace::PowerNotGood(ndx)); + } + Err(err) => { + ringbuf_entry!(Trace::PGError(ndx, err)); + } + } + + hl::sleep_for(TIMER_INTERVAL_MS); + } + + ringbuf_entry!(Trace::UpdateSucceeded(ndx)); + self.update_succeeded = Some(Ticks::now()); + self.update_backoff = None; + break; + } + + Ok((next, delay)) => { + match next { + UpdateState::WroteBlock { .. } => { + ringbuf_entry!(Trace::WroteBlock); + } + _ => { + ringbuf_entry!(Trace::UpdateState(next)); + ringbuf_entry!(Trace::UpdateDelay(delay)); + } + } + + hl::sleep_for(delay); + state = Some(next); + } + } + } + } +} + +#[export_name = "main"] +fn main() -> ! { + let i2c_task = I2C.get_task_id(); + + let psus = PSU.claim(); + + let devs: [Mwocp68; 6] = array_init::array_init(|ndx: usize| { + Mwocp68::new(&DEVICES[ndx](i2c_task), 0) + }); + + loop { + hl::sleep_for(TIMER_INTERVAL_MS); + + for (ndx, psu) in psus.iter_mut().enumerate() { + let dev = &devs[ndx]; + + if psu.update_should_be_attempted(dev, ndx as u8) { + psu.update_firmware(dev, ndx as u8); + } + } + } +} + +include!(concat!(env!("OUT_DIR"), "/i2c_config.rs")); diff --git a/drv/psc-psu-update/src/mwocp68-0701.bin b/drv/psc-psu-update/src/mwocp68-0701.bin new file mode 100644 index 0000000000000000000000000000000000000000..c22f31d652fdb665ae5f916098c2b3ca5db83fe9 GIT binary patch literal 32768 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 Hsu%ws;Wuth literal 0 HcmV?d00001 diff --git a/drv/psc-psu-update/src/mwocp68-0762.bin b/drv/psc-psu-update/src/mwocp68-0762.bin new file mode 100644 index 0000000000000000000000000000000000000000..baa93ca9b673006a6e58983ee7ddeec72f32143f GIT binary patch literal 32768 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 for task_sensor_api::NoData { | Mwocp68Error::BadValidation { code, .. } => code.into(), Mwocp68Error::BadData { .. } | Mwocp68Error::InvalidData { .. } => Self::DeviceError, + _ => Self::DeviceError, }, } }