diff --git a/.github/buildomat/jobs/observer-a.sh b/.github/buildomat/jobs/observer-a.sh new file mode 100755 index 0000000000..0bf2690ceb --- /dev/null +++ b/.github/buildomat/jobs/observer-a.sh @@ -0,0 +1,16 @@ +#!/bin/bash +#: +#: name = "build observer-a" +#: variety = "basic" +#: target = "ubuntu-22.04" +#: rust_toolchain = true +#: output_rules = [ +#: "=/work/*.zip", +#: "=/work/this_is_not_signed.txt", +#: ] + +set -o errexit +set -o pipefail +set -o xtrace + +exec .github/buildomat/build-one.sh observer-a app/observer/rev-a.toml default diff --git a/Cargo.lock b/Cargo.lock index b5f340d180..d262f6ec69 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2018,6 +2018,29 @@ dependencies = [ "zerocopy-derive 0.8.27", ] +[[package]] +name = "drv-observer-seq-server" +version = "0.1.0" +dependencies = [ + "build-i2c", + "build-util", + "counters", + "drv-i2c-api", + "drv-i2c-devices", + "drv-packrat-vpd-loader", + "drv-psc-seq-api", + "drv-stm32xx-sys-api", + "ereports", + "fixedstr", + "idol", + "microcbor", + "ringbuf", + "static-cell", + "task-jefe-api", + "task-packrat-api", + "userlib", +] + [[package]] name = "drv-onewire" version = "0.1.0" @@ -4287,6 +4310,19 @@ dependencies = [ "memchr", ] +[[package]] +name = "observer" +version = "0.1.0" +dependencies = [ + "build-util", + "cfg-if", + "cortex-m", + "cortex-m-rt", + "drv-stm32h7-startup", + "kern", + "stm32h7", +] + [[package]] name = "once_cell" version = "1.21.3" diff --git a/app/observer/Cargo.toml b/app/observer/Cargo.toml new file mode 100644 index 0000000000..a972778eea --- /dev/null +++ b/app/observer/Cargo.toml @@ -0,0 +1,31 @@ +[package] +edition = "2024" +readme = "README.md" +name = "observer" +version = "0.1.0" + +[features] +dump = ["kern/dump"] +measurement-handoff = ["drv-stm32h7-startup/measurement-handoff"] + +[dependencies] +cfg-if = { workspace = true } +cortex-m = { workspace = true } +cortex-m-rt = { workspace = true } +stm32h7 = { workspace = true, features = ["rt", "stm32h753"] } + +drv-stm32h7-startup = { path = "../../drv/stm32h7-startup", features = ["h753"] } +kern = { path = "../../sys/kern" } + +[build-dependencies] +build-util = {path = "../../build/util"} + +# this lets you use `cargo fix`! +[[bin]] +name = "observer" +test = false +doctest = false +bench = false + +[lints] +workspace = true diff --git a/app/observer/README.md b/app/observer/README.md new file mode 100644 index 0000000000..48d6c0d9af --- /dev/null +++ b/app/observer/README.md @@ -0,0 +1,9 @@ +# Observer Service Processor (SP) firmware + +The Observer interfaces with the power shelf in the Oxide rack. +This hardware is the successor to the Power Shelf Controller (PSC). + +This folder contains the firmware that runs on its service processor (SP). + +The Root of Trust firmware is common across multiple boards and can be found +in the [`oxide-rot-1` subfolder](../oxide-rot-1). diff --git a/app/observer/base.toml b/app/observer/base.toml new file mode 100644 index 0000000000..1dbceb3c77 --- /dev/null +++ b/app/observer/base.toml @@ -0,0 +1,516 @@ +target = "thumbv7em-none-eabihf" +chip = "../../chips/stm32h7" +default-ram = "axi_sram" +stacksize = 896 + +[kernel] +name = "observer" +requires = {flash = 32868, ram = 6000} +features = ["dump", "measurement-handoff"] +extern-regions = [{ region = "dtcm", shared = true }] + +[caboose] +tasks = ["control_plane_agent", "packrat"] +region = "flash" +size = 256 + +[tasks.jefe] +name = "task-jefe" +priority = 0 +max-sizes = {flash = 16384, ram = 4096} +start = true +features = ["dump", "fault-notification"] +stacksize = 2232 +notifications = ["fault", "timer"] +extern-regions = ["sram1", "sram2", "sram3", "sram4"] + +[tasks.jefe.config.on-state-change] +net = "jefe-state-change" + +[tasks.jefe.config.on-task-fault] +packrat = "task-faulted" + +[tasks.jefe.config.allowed-callers] +set_reset_reason = ["sys"] +request_reset = ["hiffy", "control_plane_agent"] + +[tasks.sys] +name = "drv-stm32xx-sys" +features = ["h753", "exti", "no-panic"] +priority = 1 +max-sizes = {flash = 4096, ram = 2048} +uses = ["rcc", "gpios", "system_flash", "syscfg", "exti"] +start = true +task-slots = ["jefe"] +notifications = ["exti-wildcard-irq"] + +[tasks.sys.interrupts] +"exti.exti0" = "exti-wildcard-irq" +"exti.exti1" = "exti-wildcard-irq" +"exti.exti2" = "exti-wildcard-irq" +"exti.exti3" = "exti-wildcard-irq" +"exti.exti4" = "exti-wildcard-irq" +"exti.exti9_5" = "exti-wildcard-irq" +"exti.exti15_10" = "exti-wildcard-irq" + +[tasks.sys.config.gpio-irqs.rot_irq] +port = "J" +pin = 13 +owner = {name = "sprot", notification = "rot_irq"} + +# TODO add PRESENT_L IRQs (instead of polling); they're on PJ0-5 + +[tasks.sys.config.gpio-irqs.psu_pwr_ok_0] +port = "J" +pin = 6 +owner = {name = "sequencer", notification = "psu_pwr_ok_0"} + +[tasks.sys.config.gpio-irqs.psu_pwr_ok_1] +port = "J" +pin = 7 +owner = {name = "sequencer", notification = "psu_pwr_ok_1"} + +[tasks.sys.config.gpio-irqs.psu_pwr_ok_2] +port = "J" +pin = 8 +owner = {name = "sequencer", notification = "psu_pwr_ok_2"} + +[tasks.sys.config.gpio-irqs.psu_pwr_ok_3] +port = "J" +pin = 9 +owner = {name = "sequencer", notification = "psu_pwr_ok_3"} + +[tasks.sys.config.gpio-irqs.psu_pwr_ok_4] +port = "J" +pin = 10 +owner = {name = "sequencer", notification = "psu_pwr_ok_4"} + +[tasks.sys.config.gpio-irqs.psu_pwr_ok_5] +port = "J" +pin = 11 +owner = {name = "sequencer", notification = "psu_pwr_ok_5"} + +[tasks.rng_driver] +features = ["h753", "ereport"] +name = "drv-stm32h7-rng" +priority = 6 +uses = ["rng"] +start = true +stacksize = 512 +task-slots = ["sys", "packrat"] + +[tasks.i2c_driver] +name = "drv-stm32xx-i2c-server" +stacksize = 1048 +features = ["h753"] +priority = 2 +uses = ["i2c2", "i2c3", "i2c4"] +start = true +task-slots = ["sys"] +notifications = ["i2c2-irq", "i2c3-irq", "i2c4-irq"] + +[tasks.i2c_driver.interrupts] +"i2c2.event" = "i2c2-irq" +"i2c2.error" = "i2c2-irq" +"i2c3.event" = "i2c3-irq" +"i2c3.error" = "i2c3-irq" +"i2c4.event" = "i2c4-irq" +"i2c4.error" = "i2c4-irq" + +[tasks.packrat] +name = "task-packrat" +priority = 1 +stacksize = 1400 +start = true +task-slots = ["jefe"] +features = ["ereport"] +notifications = ["task-faulted"] + +[tasks.sequencer] +name = "drv-observer-seq-server" +priority = 4 +stacksize = 4096 +start = true +task-slots = ["jefe", "packrat", "i2c_driver", "sys"] +notifications = [ + "psu_pwr_ok_0", + "psu_pwr_ok_1", + "psu_pwr_ok_2", + "psu_pwr_ok_3", + "psu_pwr_ok_4", + "psu_pwr_ok_5", + "timer", +] + +[tasks.update_server] +name = "stm32h7-update-server" +priority = 2 +max-sizes = {flash = 16384, ram = 4096} +stacksize = 2048 +start = true +uses = ["flash_controller"] +extern-regions = ["bank2"] +interrupts = {"flash_controller.irq" = "flash-irq"} +notifications = ["flash-irq"] + +[tasks.hiffy] +name = "task-hiffy" +features = ["h753", "stm32h7", "i2c", "gpio", "sprot", "turbo"] +priority = 5 +max-sizes = {flash = 32768} +stacksize = 1200 +start = true +task-slots = ["sys", "i2c_driver", "sprot"] +notifications = ["timer"] + +[tasks.validate] +name = "task-validate" +priority = 3 +max-sizes = {flash = 16384, ram = 4096 } +stacksize = 1000 +start = true +task-slots = ["i2c_driver"] + +[tasks.net] +name = "task-net" +stacksize = 8000 +priority = 4 +features = ["mgmt", "h753", "psc", "vlan", "vpd-mac", "spi1"] +max-sizes = {flash = 131072, ram = 65536, sram1_mac = 16384} +sections = {eth_bulk = "sram1_mac"} +uses = ["eth", "tim16"] +start = true +notifications = [ + "eth-irq", + "mdio-timer-irq", + "wake-timer", + "jefe-state-change", +] +task-slots = ["sys", "packrat", "jefe", {spi_driver = "spi1_driver"}] +# TODO see if we can use SPI1 exclusively here + +[tasks.net.interrupts] +"eth.irq" = "eth-irq" +"tim16.irq" = "mdio-timer-irq" + +[tasks.spi1_driver] +name = "drv-stm32h7-spi-server" +priority = 3 +features = ["spi1", "h753"] +uses = ["spi1"] +start = true +interrupts = {"spi1.irq" = "spi-irq"} +stacksize = 872 +task-slots = ["sys"] +notifications = ["spi-irq"] + +[tasks.spi2_driver] +name = "drv-stm32h7-spi-server" +priority = 3 +features = ["spi2", "h753"] +uses = ["spi2"] +start = true +interrupts = {"spi2.irq" = "spi-irq"} +stacksize = 872 +task-slots = ["sys"] +notifications = ["spi-irq"] + +# TODO add SPI3 driver for ignition flash reprogramming + +[tasks.control_plane_agent] +name = "task-control-plane-agent" +priority = 6 +stacksize = 7000 +start = true +uses = [] +task-slots = [ + "jefe", + "dump_agent", + "net", + "update_server", + "sys", + "validate", + "sensor", + "sprot", + "packrat", + "user_leds", + "vpd", +] +features = ["observer", "vlan", "vpd"] +notifications = ["usart-irq", "socket", "timer"] +# usart-irq is unused but present in the code + +[tasks.sprot] +name = "drv-stm32h7-sprot-server" +priority = 3 +max-sizes = {flash = 65536, ram = 32768} +stacksize = 16384 +start = true +task-slots = ["sys"] +uses = ["spi4"] +features = ["sink_test", "use-spi-core", "h753", "spi4"] +notifications = ["spi-irq", "rot-irq", "timer"] +interrupts = {"spi4.irq" = "spi-irq"} + +[tasks.udpecho] +name = "task-udpecho" +priority = 5 +max-sizes = {flash = 16384, ram = 8192} +stacksize = 4096 +start = true +task-slots = ["net"] +features = ["vlan"] +notifications = ["socket"] + +[tasks.udpbroadcast] +name = "task-udpbroadcast" +priority = 5 +max-sizes = {flash = 16384, ram = 8192} +stacksize = 4096 +start = true +task-slots = ["net", "packrat"] +features = ["vlan"] +notifications = ["socket"] + +[tasks.eeprom] +name = "drv-eeprom" +priority = 3 +max-sizes = {flash = 2048, ram = 256} +stacksize = 256 +start = true +task-slots = ["i2c_driver"] + +[tasks.vpd] +name = "task-vpd" +priority = 3 +max-sizes = {flash = 8192, ram = 1024} +start = true +task-slots = ["sys", "i2c_driver"] +stacksize = 800 + +[tasks.user_leds] +name = "drv-user-leds" +features = ["stm32h7"] +priority = 2 +max-sizes = {flash = 2048, ram = 1024} +start = true +task-slots = ["sys"] +notifications = ["timer"] + +[tasks.power] +name = "task-power" +priority = 4 +max-sizes = {flash = 32768, ram = 4096} +stacksize = 2504 +start = true +task-slots = ["i2c_driver", "sensor", "sys"] +features = ["observer"] +notifications = ["timer"] + +[tasks.sensor] +name = "task-sensor" +priority = 3 +max-sizes = {flash = 16384, ram = 8192 } +stacksize = 1024 +start = true + +[tasks.sensor_polling] +name = "task-sensor-polling" +priority = 4 +max-sizes = {flash = 16384, ram = 2048 } +start = true +task-slots = ["i2c_driver", "sensor"] + +# TODO re-add drv-psc-psu-update or equivalent? + +[tasks.dump_agent] +name = "task-dump-agent" +priority = 5 +max-sizes = {flash = 32768, ram = 16384 } +start = true +task-slots = ["sprot", "jefe", "net"] +stacksize = 2400 +extern-regions = [ "sram1", "sram2", "sram3", "sram4" ] +notifications = ["socket"] +features = ["net", "vlan"] + +[tasks.snitch] +name = "task-snitch" +# The snitch should have a priority immediately below that of the net task, +# to minimize the number of components that can starve it from resources. +priority = 5 +stacksize = 1200 +start = true +task-slots = ["net", "packrat"] +features = ["vlan"] +notifications = ["socket"] + +[tasks.idle] +name = "task-idle" +priority = 7 +max-sizes = {flash = 128, ram = 256} +stacksize = 256 +start = true + +[config] +# +# I2C2: Local bus (EEPROM) +# +[[config.i2c.controllers]] +controller = 2 + +# +# I2C_SP_TO_EEPROM_SDA +# I2C_SP_TO_EEPROM_SCL +# +[config.i2c.controllers.ports.F] +name = "local" +description = "Local bus (EEPROM)" +scl.pin = 1 +sda.pin = 0 +af = 4 + +# +# I2C3: Backplane bus +# +[[config.i2c.controllers]] +controller = 3 + +# +# PMBUS_SP_TO_POWER_SHELF_SMBCLK +# PMBUS_SP_TO_POWER_SHELF_SMBDAT +# +[config.i2c.controllers.ports.H] +name = "backplane" +description = "Backplane bus" +scl.pin = 7 +sda.pin = 8 +af = 4 + +# +# I2C4: Backplane bus +# +[[config.i2c.controllers]] +controller = 4 + +# +# I2C_SP_TO_TEMP_SCL +# I2C_SP_TO_TEMP_SDA +# +[config.i2c.controllers.ports.F] +name = "temp" +description = "Temperature sensor bus" +scl.pin = 14 +sda.pin = 15 +af = 4 + +[[config.i2c.devices]] +bus = "temp" +address = 0b1001_000 +device = "tmp117" +description = "Temperature sensor" +refdes = "U29" + +[[config.i2c.devices]] +bus = "local" +address = 0b1010_000 +device = "at24csw080" +name = "local_vpd" +description = "FRU ID EEPROM" +refdes = "U27" + +# TODO add rectifiers (once we have their PMBus spec) and uncomment PMBus usage +# in observer-seq-server. + +[config.spi.spi1] +controller = 1 + +[config.spi.spi1.mux_options.port_a] +outputs = [ + {port = "A", pins = [5], af = 5}, # SCK + {port = "B", pins = [5], af = 5}, # MOSI +] +input = {port = "A", pin = 6, af = 5} + +[config.spi.spi1.devices.ksz8463] +mux = "port_a" +cs = [{port = "A", pin = 4}] + +[config.spi.spi2] +controller = 2 + +[config.spi.spi2.mux_options.port_b] +outputs = [ + {port = "B", pins = [13, 15], af = 5}, +] +input = {port = "B", pin = 14, af = 5} + +[config.spi.spi2.devices.mb86rs64t] +mux = "port_b" +cs = [{port = "B", pin = 12}] + +# TODO SPI3 is the ignition flash reprogramming port + +[config.spi.spi4] +controller = 4 + +[config.spi.spi4.mux_options.rot] +outputs = [ + {port = "E", pins = [2, 6], af = 5}, +] +input = {port = "E", pin = 5, af = 5} + +[config.spi.spi4.devices.rot] +mux = "rot" +cs = [{port = "E", pin = 4}] +clock_divider = "DIV256" + +# VLAN configuration +[config.net.vlans.sidecar1] +vid = 0x301 +trusted = true +port = 1 + +[config.net.vlans.sidecar2] +vid = 0x302 +trusted = true +port = 2 + +# UDP ports in sockets below are assigned in oxidecomputer/oana + +[config.net.sockets.echo] +kind = "udp" +owner = {name = "udpecho", notification = "socket"} +port = 7 +tx = { packets = 3, bytes = 1024 } +rx = { packets = 3, bytes = 1024 } + +[config.net.sockets.broadcast] +kind = "udp" +owner = {name = "udpbroadcast", notification = "socket"} +port = 997 +tx = { packets = 3, bytes = 1024 } +rx = { packets = 3, bytes = 1024 } + +[config.net.sockets.control_plane_agent] +kind = "udp" +owner = {name = "control_plane_agent", notification = "socket"} +port = 11111 +tx = { packets = 3, bytes = 2048 } +rx = { packets = 3, bytes = 2048 } + +[config.net.sockets.dump_agent] +kind = "udp" +owner = {name = "dump_agent", notification = "socket"} +port = 11113 +tx = { packets = 3, bytes = 1024 } +rx = { packets = 3, bytes = 1024 } + +[config.net.sockets.ereport] +kind = "udp" +owner = {name = "snitch", notification = "socket"} +port = 57005 +tx = { packets = 3, bytes = 1024 } +# v0 ereport requests are always 35B, so just make the buffer exactly +# that size... +rx = { packets = 3, bytes = 35 } diff --git a/app/observer/build.rs b/app/observer/build.rs new file mode 100644 index 0000000000..2800895934 --- /dev/null +++ b/app/observer/build.rs @@ -0,0 +1,7 @@ +// 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() { + build_util::expose_target_board(); +} diff --git a/app/observer/dev.toml b/app/observer/dev.toml new file mode 100644 index 0000000000..c7f64dea96 --- /dev/null +++ b/app/observer/dev.toml @@ -0,0 +1,21 @@ +# Configuration fragment for -dev images + +[tasks.jefe.config.allowed-callers] +request_reset = ["udprpc"] + +[tasks.udprpc] +name = "task-udprpc" +priority = 5 +max-sizes = {flash = 32768, ram = 8192} +stacksize = 4096 +start = true +task-slots = ["net"] +features = ["vlan"] +notifications = ["socket"] + +[config.net.sockets.rpc] +kind = "udp" +owner = {name = "udprpc", notification = "socket"} +port = 998 +tx = { packets = 3, bytes = 1024 } +rx = { packets = 3, bytes = 1024 } diff --git a/app/observer/rev-a-dev.toml b/app/observer/rev-a-dev.toml new file mode 100644 index 0000000000..8c071c43a7 --- /dev/null +++ b/app/observer/rev-a-dev.toml @@ -0,0 +1,2 @@ +name = "observer-a-dev" +inherit = ["rev-a.toml", "dev.toml"] diff --git a/app/observer/rev-a.toml b/app/observer/rev-a.toml new file mode 100644 index 0000000000..fb85de3505 --- /dev/null +++ b/app/observer/rev-a.toml @@ -0,0 +1,5 @@ +# This is the production image. We expect `name` to match `board` +name = "observer-a" +board = "observer-a" + +inherit = "base.toml" diff --git a/app/observer/src/main.rs b/app/observer/src/main.rs new file mode 100644 index 0000000000..00078e3fcf --- /dev/null +++ b/app/observer/src/main.rs @@ -0,0 +1,98 @@ +// 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/. + +#![no_std] +#![no_main] + +// We have to do this if we don't otherwise use it to ensure its vector table +// gets linked in. +extern crate stm32h7; + +use stm32h7::stm32h753 as device; + +use drv_stm32h7_startup::ClockConfig; + +use cortex_m_rt::entry; + +#[entry] +fn main() -> ! { + system_init(); + + const CYCLES_PER_MS: u32 = 400_000; + + unsafe { kern::startup::start_kernel(CYCLES_PER_MS) } +} + +fn system_init() { + let cp = cortex_m::Peripherals::take().unwrap(); + let p = device::Peripherals::take().unwrap(); + + // Un-gate the clock to GPIO bank G. + p.RCC.ahb4enr.modify(|_, w| w.gpiogen().set_bit()); + cortex_m::asm::dsb(); + + // PG2:0 are already inputs after reset, without any pull resistors. + // There are external pull resistors on the board. + #[rustfmt::skip] + p.GPIOG.moder.modify(|_, w| w + .moder0().input() + .moder1().input() + .moder2().input()); + + let rev = p.GPIOG.idr.read().bits() & 0b111; + + cfg_if::cfg_if! { + if #[cfg(target_board = "observer-a")] { + let expected_rev = 0b000; + } else { + compile_error!("not a recognized psc board") + } + } + assert_eq!(rev, expected_rev); + + drv_stm32h7_startup::system_init_custom( + cp, + p, + ClockConfig { + source: drv_stm32h7_startup::ClockSource::ExternalCrystal, + // 8MHz HSE freq is within VCO input range of 2-16, so, DIVM=1 to bypass + // the prescaler. + divm: 1, + // VCO must tolerate an 8MHz input range: + vcosel: device::rcc::pllcfgr::PLL1VCOSEL_A::WIDEVCO, + pllrange: device::rcc::pllcfgr::PLL1RGE_A::RANGE8, + // DIVN governs the multiplication of the VCO input frequency to produce + // the intermediate frequency. We want an IF of 800MHz, or a + // multiplication of 100x. + // + // We subtract 1 to get the DIVN value because the PLL effectively adds + // one to what we write. + divn: 100 - 1, + // P is the divisor from the VCO IF to the system frequency. We want + // 400MHz, so: + divp: device::rcc::pll1divr::DIVP1_A::DIV2, + // Q produces kernel clocks; we set it to 200MHz: + divq: 4 - 1, + // R is mostly used by the trace unit and we leave it fast: + divr: 2 - 1, + + // We run the CPU at the full core rate of 400MHz: + cpu_div: device::rcc::d1cfgr::D1CPRE_A::DIV1, + // We down-shift the AHB by a factor of 2, to 200MHz, to meet its + // constraints: + ahb_div: device::rcc::d1cfgr::HPRE_A::DIV2, + // We configure all APB for 100MHz. These are relative to the AHB + // frequency. + apb1_div: device::rcc::d2cfgr::D2PPRE1_A::DIV2, + apb2_div: device::rcc::d2cfgr::D2PPRE2_A::DIV2, + apb3_div: device::rcc::d1cfgr::D1PPRE_A::DIV2, + apb4_div: device::rcc::d3cfgr::D3PPRE_A::DIV2, + + // Flash runs at 200MHz: 2WS, 2 programming cycles. See reference manual + // Table 13. + flash_latency: 2, + flash_write_delay: 2, + }, + ); +} diff --git a/boards/observer-a.toml b/boards/observer-a.toml new file mode 100644 index 0000000000..eb91eeac9c --- /dev/null +++ b/boards/observer-a.toml @@ -0,0 +1,2 @@ +[probe-rs] +chip-name = "STM32H753ZITx" diff --git a/drv/ksz8463/src/lib.rs b/drv/ksz8463/src/lib.rs index 5f9bac6439..dfcae081c0 100644 --- a/drv/ksz8463/src/lib.rs +++ b/drv/ksz8463/src/lib.rs @@ -12,7 +12,7 @@ pub use registers::{MIBCounter, Register}; //////////////////////////////////////////////////////////////////////////////// -#[derive(Copy, Clone, Eq, PartialEq, counters::Count)] +#[derive(Copy, Clone, Eq, PartialEq, counters::Count, Debug)] pub enum Error { SpiError(#[count(children)] SpiError), WrongChipId(u16), diff --git a/drv/observer-seq-server/Cargo.toml b/drv/observer-seq-server/Cargo.toml new file mode 100644 index 0000000000..627651f14b --- /dev/null +++ b/drv/observer-seq-server/Cargo.toml @@ -0,0 +1,36 @@ +[package] +name = "drv-observer-seq-server" +version = "0.1.0" +edition = "2024" + +[dependencies] +drv-packrat-vpd-loader.path = "../packrat-vpd-loader" +drv-psc-seq-api.path = "../psc-seq-api" +drv-stm32xx-sys-api = { path = "../../drv/stm32xx-sys-api", features = ["family-stm32h7"] } +drv-i2c-api = { path ="../../drv/i2c-api", features = ["component-id"] } +drv-i2c-devices.path = "../../drv/i2c-devices" +task-jefe-api.path = "../../task/jefe-api" +task-packrat-api = { path = "../../task/packrat-api", features = ["microcbor"] } +userlib = { path = "../../sys/userlib", features = ["panic-messages"] } +ringbuf = { path = "../../lib/ringbuf", features = ["counters"] } +counters = { path = "../../lib/counters" } +ereports = { path = "../../lib/ereports", features = ["ereporter-macro"] } +static-cell = { path = "../../lib/static-cell" } +microcbor.path = "../../lib/microcbor" +fixedstr = { path = "../../lib/fixedstr", features = ["microcbor"] } + +[build-dependencies] +idol.workspace = true +build-util = { path = "../../build/util" } +build-i2c = { path = "../../build/i2c" } + +# This section is here to discourage RLS/rust-analyzer from doing test builds, +# since test builds don't work for cross compilation. +[[bin]] +name = "drv-observer-seq-server" +test = false +doctest = false +bench = false + +[lints] +workspace = true diff --git a/drv/observer-seq-server/build.rs b/drv/observer-seq-server/build.rs new file mode 100644 index 0000000000..bb6fc7a3e7 --- /dev/null +++ b/drv/observer-seq-server/build.rs @@ -0,0 +1,16 @@ +// 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::build_notifications()?; + build_util::expose_target_board(); + + let disposition = build_i2c::Disposition::Devices; + + if let Err(e) = build_i2c::codegen(disposition) { + println!("cargo::error=code generation failed: {e}"); + std::process::exit(1); + } + Ok(()) +} diff --git a/drv/observer-seq-server/src/main.rs b/drv/observer-seq-server/src/main.rs new file mode 100644 index 0000000000..b987eb166c --- /dev/null +++ b/drv/observer-seq-server/src/main.rs @@ -0,0 +1,1149 @@ +// 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 managing the Observer sequencing process. +//! It's based on the psc-seq-server. +//! +//! # General notes on PSC power supply sequencing +//! +//! There are rules to follow here to avoid glitching the power supplies, +//! because glitching the power supplies here will glitch the entire rack, +//! making you very unpopular very quickly. +//! +//! **`ON_L` signals to the PSUs:** we normally leave our pins high-impedance on +//! these nets, allowing external resistors to pull them low. We only drive them +//! to _disable_ the PSU by driving it high. To achieve this, we leave the pin +//! configured as an input, pre-load the output value as "high," and toggle its +//! mode register between input and output. +//! +//! **`PRESENT_L` signals from the PSUs:** pulled inactive-high by resistors on +//! the board and power shelf. When these go low, assume they will bounce, +//! because they're brought low by a physical connection between pins on our +//! connector. +//! +//! **`OK` signals from the PSUs:** pulled ACTIVE-high by resistors on the +//! board, in all caps because resistors pulling something active is somewhat +//! unusual. The PSU drives this open drain, so it will only go low if the PSU +//! pulls it low to indicate a problem. This implies that, if the PSU is not +//! detected as present, you cannot trust the `OK` signal. +//! +//! +//! # Intended behavior +//! +//! Let's ignore task restarts / crashes for the moment. +//! +//! The PSC is intended to be hot swappable. If the PSC gets plugged in, this +//! task will start anew (along with the rest of the firmware), with RAM cleared +//! and peripherals in reset state. This will also happen if the PSC is plugged +//! into a rack that is then plugged into power -- we can't usefully distinguish +//! these cases, nor do we need to. +//! +//! When the PSC is *removed* from the system, the pull resistors on the power +//! supply ON signals cause the power supplies to turn on. It's important that +//! we don't override this when the PSU is reinserted. So, at startup, the PSC +//! must leave the ON lines undriven, allowing them to float low. +//! +//! Because the PSC's connector is not designed for hot swap, we can't +//! necessarily trust our inputs at power-on. Without a firm "all connections +//! made" indication from the connector, the best we can do is assume that the +//! connector insertion cycle will finish within some time interval. We delay +//! for this time interval before looking at any inputs. During this time, the +//! power supplies will be on. +//! +//! At that point, we can start our main management loop, which continuously +//! does the following for each power supply separately: +//! +//! - Watch for the presence line to be high (PSU removed). +//! - Record that the PSU is missing. +//! - Start driving its ON signal high. +//! - Wait for the presence line to be low (PSU reinserted). +//! - Release its ON signal so it may turn on normally. +//! +//! Simultaneously, while the PSU is not removed, we monitor the OK signal for +//! indication of internal faults, and periodically poll PMBus status registers +//! for faults that may not be indicated through the OK signal. (The behavior of +//! the OK signal is not super clear from Murata's documentation.) If we find a +//! fault, we... +//! +//! - Record as much information as we can reasonably gather. +//! - Start driving the ON signal high to force the PSU off. +//! - Wait some time to allow things to discharge. +//! - Turn the PSU back on. +//! - Wait some time for it to wake. +//! - Start watching the fault signal again. +//! +//! Removing and reinserting a PSU in general clears the fault state _and_ +//! resets the retry counter. +//! +//! +//! # Generalizing to task restarts +//! +//! This task is not intended to restart under normal operation, but bugs +//! happen. We can attempt to maintain glitch-free (or at least low-glitch) +//! operation in the face of this task crashing by doing the following: +//! +//! At task startup, read the status of the ON output pins. If we find that one +//! of the PSUs is off, assume that we turned it off in a previous incarnation +//! before losing state. Begin a fault recovery sequence (above) on that PSU as +//! if we had newly detected a fault. +//! +//! Task crashes will reset the fault counter and timeout. This is unavoidable +//! without keeping state across incarnations, which we're trying to avoid to +//! reduce the likelihood of crashloops. +//! +//! Task crashes may also reactivate a PSU that the control plane had commanded +//! off. Currently this is unavoidable; we might want to record such overrides +//! in the FRAM to be safe. + +#![no_std] +#![no_main] +// TODO remove once the rectifier PMBus config is added +#![allow(dead_code)] + +// use drv_i2c_api::I2cDevice; +use drv_i2c_devices::mwocp68::{self, Mwocp68}; +use drv_packrat_vpd_loader::{Packrat, read_vpd_and_load_packrat}; +use drv_psc_seq_api::PowerState; +use drv_stm32xx_sys_api as sys_api; +use sys_api::{Edge, IrqControl, OutputType, PinSet, Pull, Speed}; +use task_jefe_api::Jefe; +use userlib::*; + +use fixedstr::{FixedStr, FixedString}; +use ringbuf::{counted_ringbuf, ringbuf_entry}; + +task_slot!(SYS, sys); +task_slot!(I2C, i2c_driver); +task_slot!(JEFE, jefe); +task_slot!(PACKRAT, packrat); + +#[derive(Copy, Clone, PartialEq, Eq, counters::Count)] +enum Event { + #[count(skip)] + None, + /// Emitted at task startup when we find that a power supply is probably + /// already on. (Note that if the power supply is not present, we will still + /// detect it as "on" due to the pull resistors.) + FoundEnabled { + now: u64, + #[count(children)] + psu: Slot, + serial: Option>, + }, + /// Emitted at task startup when we find that a power supply appears to have + /// been disabled. + FoundAlreadyDisabled { + now: u64, + #[count(children)] + psu: Slot, + serial: Option>, + }, + /// Emitted when a previously not present PSU's presence pin is asserted. + Inserted { + now: u64, + #[count(children)] + psu: Slot, + serial: Option>, + }, + /// Emitted when a previously present PSU's presence pin is deasserted. + Removed { + now: u64, + #[count(children)] + psu: Slot, + }, + /// Emitted when we decide a power supply should be on. + Enabling { + now: u64, + #[count(children)] + psu: Slot, + }, + /// Emitted when we decide a power supply should be off; the `present` flag + /// means the PSU is being turned off despite being present (`true`) or is + /// being disabled because it's been removed (`false`). + Disabling { + now: u64, + #[count(children)] + psu: Slot, + present: bool, + }, +} + +// Since entries in this ringbuffer contain timestamps, they will never be +// de-duplicated. Thus, disable it. +counted_ringbuf!(Event, 128, Event::None, no_dedup); + +/// More verbose debugging data goes in its own ring buffer, so that we can +/// maintain a longer history of major PSU events while still recording more +/// detailed information about the PSU's status. +/// +/// Each of these entries has a `now` value which can be correlated with the +/// timestamps in the main ringbuf. +/// +/// An entry for each of the rectifier's PMBus status registers (e.g. +/// `STATUS_WORD`, `STATUS_VOUT`, `STATUS_IOUT`, and so on...) is recorded read +/// whenever a rectifier's `PWR_OK` pin changes state. Since exactly one of each +/// register entry is recorded for every `Faulted` and `FaultCleared` entry, we +/// don't really need to spend extra bytes on counting them, so they are marked +/// as `count(skip)`. +#[derive(Copy, Clone, PartialEq, Eq, counters::Count)] +enum Trace { + #[count(skip)] + None, + PowerGoodDeasserted { + now: u64, + #[count(children)] + psu: Slot, + }, + PowerGoodAsserted { + now: u64, + #[count(children)] + psu: Slot, + }, + PowerStillUngood { + now: u64, + #[count(children)] + psu: Slot, + }, + #[count(skip)] + StatusWord { + now: u64, + psu: Slot, + status_word: Result, + }, + #[count(skip)] + StatusIout { + now: u64, + psu: Slot, + status_iout: Result, + }, + #[count(skip)] + StatusVout { + now: u64, + psu: Slot, + status_vout: Result, + }, + #[count(skip)] + StatusInput { + now: u64, + psu: Slot, + status_input: Result, + }, + #[count(skip)] + StatusCml { + now: u64, + psu: Slot, + status_cml: Result, + }, + #[count(skip)] + StatusTemperature { + now: u64, + psu: Slot, + status_temperature: Result, + }, + #[count(skip)] + StatusMfrSpecific { + now: u64, + psu: Slot, + status_mfr_specific: Result, + }, + I2cError { + now: u64, + #[count(children)] + psu: Slot, + err: mwocp68::Error, + }, +} + +// Since entries in this ringbuffer contain timestamps, they will never be +// de-duplicated. Thus, disable it. +counted_ringbuf!(__TRACE, Trace, 32, Trace::None, no_dedup); + +/// PSU numbers represented as an enum. This is intended for use with +/// `counted_ringbuf!`, instead of representing PSU numbers as raw u8s, which +/// cannot derive `counters::Count` (and would have to generate a counter table +/// with 256 entries rather than just 6). +#[derive(Copy, Clone, Eq, PartialEq, counters::Count)] +#[repr(u8)] +enum Slot { + Psu0 = 0, + Psu1 = 1, + Psu2 = 2, + Psu3 = 3, + Psu4 = 4, + Psu5 = 5, +} + +const STATUS_LED: sys_api::PinSet = sys_api::Port::A.pin(3); + +// The per-PSU signal definitions below all refer to this constant for the +// number of PSUs. It's not intended to be easily configurable, since that'd +// require hardware changes. +const PSU_COUNT: usize = 6; + +// The ON signals are conveniently all routed to a single port: +const PSU_ENABLE_L_PORT: sys_api::Port = sys_api::Port::I; +// The ON signals are routed to the following pins on their port: +const PSU_ENABLE_L_PINS: [usize; PSU_COUNT] = [8, 9, 10, 11, 12, 13]; +// Convenient mask for referring to all the ON pins simultaneously, since we can +// do that, since they're all on one port. +const ALL_PSU_ENABLE_L_PINS: sys_api::PinSet = + PSU_ENABLE_L_PORT.pins(PSU_ENABLE_L_PINS); + +// The PRESENT signals are conveniently all routed to a single port: +const PSU_PRESENT_L_PORT: sys_api::Port = sys_api::Port::J; +// The PRESENT signals are routed to the following pins on their port: +const PSU_PRESENT_L_PINS: [usize; PSU_COUNT] = [0, 1, 2, 3, 4, 5]; +// Convenient mask for referring to all the PRESENT pins simultaneously, since +// we can do that, since they're all on one port. +const ALL_PSU_PRESENT_L_PINS: sys_api::PinSet = + PSU_PRESENT_L_PORT.pins(PSU_PRESENT_L_PINS); + +// The `PWR_OK` signals are conveniently all routed to a single port: +const PSU_PWR_OK_PORT: sys_api::Port = sys_api::Port::J; +// The `PWR_OK` signals are routed to the following pins on their port: +const PSU_PWR_OK_PINS: [usize; PSU_COUNT] = [6, 7, 8, 9, 10, 11]; +// Convenient mask for referring to all the `PWR_OK` pins simultaneously, since +// we can do that, since they're all on one port. +const ALL_PSU_PWR_OK_PINS: sys_api::PinSet = + PSU_PWR_OK_PORT.pins(PSU_PWR_OK_PINS); + +// Our notification configuration system doesn't have any concept of arrays, so, +// collect its predefined masks into convenient arrays. +const PSU_PWR_OK_NOTIF: [u32; PSU_COUNT] = [ + notifications::PSU_PWR_OK_0_MASK, + notifications::PSU_PWR_OK_1_MASK, + notifications::PSU_PWR_OK_2_MASK, + notifications::PSU_PWR_OK_3_MASK, + notifications::PSU_PWR_OK_4_MASK, + notifications::PSU_PWR_OK_5_MASK, +]; + +// TODO uncomment once the rectifier PMBus config is added +// /// In order to get the PMBus devices by PSU index, we need a little lookup table guy. +// const PSU_PMBUS_DEVS: [fn(TaskId) -> (I2cDevice, u8); PSU_COUNT] = [ +// i2c_config::pmbus::v54_psu0, +// i2c_config::pmbus::v54_psu1, +// i2c_config::pmbus::v54_psu2, +// i2c_config::pmbus::v54_psu3, +// i2c_config::pmbus::v54_psu4, +// i2c_config::pmbus::v54_psu5, +// ]; + +const PSU_SLOTS: [Slot; PSU_COUNT] = [ + Slot::Psu0, + Slot::Psu1, + Slot::Psu2, + Slot::Psu3, + Slot::Psu4, + Slot::Psu5, +]; + +/// How long to wait after task startup before we start trying to inspect +/// things. +const STARTUP_SETTLE_MS: u64 = 500; // Current value is somewhat arbitrary. + +/// How long to leave a PSU off on fault before attempting to re-enable it. +const FAULT_OFF_MS: u64 = 5_000; // Current value is somewhat arbitrary. + +/// How long to wait after a PSU is inserted, before we attempt to turn it on. +/// This does double-duty in both debouncing the presence line, and ensuring +/// that things are firmly mated before activating anything. +const INSERT_DEBOUNCE_MS: u64 = 1_000; // Current value is somewhat arbitrary. + +/// How long after exiting a fault state before we require the PSU to start +/// asserting OK. Or, conversely, how long to ignore the OK output after +/// re-enabling a faulted PSU. +/// +/// We have observed delays of up to 92 ms in practice. Leaving the PSU enabled +/// in a fault state shouldn't be destructive, so we've padded this to avoid +/// flapping. +const PROBATION_MS: u64 = 1000; + +/// How often to check the status of polled inputs. +/// +/// This should be fast enough to reliably spot removed sleds. +const POLL_MS: u64 = 500; + +#[derive(Copy, Clone)] +#[must_use] +enum ActionRequired { + /// Requests that this PSU be enabled by setting the corresponding + /// `ENABLE_L` low. + EnableMe, + /// Requests that this PSU be disabled by setting the corresponding + /// `ENABLE_L` high. `attempt_snapshot` will be `true` if the PSU is + /// believed to still be present and recording data may be useful, or + /// `false` if the PSU is believed removed and isn't worth polling. + DisableMe { attempt_snapshot: bool }, +} + +#[derive(Copy, Clone)] +enum PsuState { + /// The PSU is detected as not present. In this state, we cannot trust the + /// OK signal, and we deassert the ENABLE signal. + NotPresent, + /// The PSU is detected as present. + Present(PresentState), +} + +#[derive(Copy, Clone)] +enum PresentState { + /// We are allowing the ON signal to float active (low). + /// + /// This is the initial state upon either detecting a new PSU, or power + /// up/restart in cases where the PSU is not forced off. + /// + /// We will exit this state if the OK line is pulled low, or if we detect a + /// fault. + On { + /// If `true`, the PSU was power-cycled by the PSC in attempt to clear a + /// fault. If it reasserts `PWR_OK`, that indicates that the fault has + /// cleared; otherwise, the fault is persistent. + /// + /// If `false`, the PSU was either newly inserted, or a previous fault + /// has cleared. A new fault should produce a new fault ereport. + was_faulted: bool, + }, + + /// The PSU has just appeared and we're waiting a bit to confirm that it's + /// stable before turning it on. (Waiting in this state provides some + /// debouncing for contact scrape.) + NewlyInserted { settle_deadline: u64 }, + /// The PSU has unexpectedly deasserted the OK signal, or failed to assert + /// it within a reasonable amount of time after being turned on. + Faulted { + // Try to turn the PSU back on when this time is reached, but only if + // the fault has cleared. Otherwise, we will stay in the fault state + // with a "sticky fault" situation. + turn_on_deadline: u64, + }, + + /// We are allowing the ON signal to float active, as in the `On` state, but + /// we're not convinced the PSU is okay. We enter this state when bringing a + /// PSU out of an observed fault state, and it causes us to ignore its OK + /// output for a brief period (the deadline parameter, initialized as + /// current time plus `DEADLINE_MS`). + /// + /// We do this because PSUs have been observed, in practice, taking up to + /// ~100ms to assert OK after being enabled. + /// + /// Once the deadline elapses, we'll transition to the `On` state and start + /// requiring OK to be asserted. + OnProbation { deadline: u64 }, +} + +#[unsafe(export_name = "main")] +fn main() -> ! { + let sys = sys_api::Sys::from(SYS.get_task_id()); + + // The chassis LED is active high and pulled down by an external resistor. + // If this is a task restart, our previous incarnation may have configured + // the STATUS_LED pin as an output and turned the LED on. + // + // Turn it back off and reconfigure the pin (a no-op if it's already + // configured). + // + // This sequence should not glitch in practice (though it also doesn't much + // matter if we glitch an LED). + sys.gpio_reset(STATUS_LED); + sys.gpio_configure_output( + STATUS_LED, + sys_api::OutputType::PushPull, + sys_api::Speed::Low, + sys_api::Pull::None, + ); + + // Populate packrat with our mac address and identity. Doing this now lets + // the netstack wake up and start being useful while we're mucking around + // with GPIOs below. + let packrat = Packrat::from(PACKRAT.get_task_id()); + read_vpd_and_load_packrat(&packrat, I2C.get_task_id()); + + let mut ereporter = Ereporter::claim_static_resources(packrat); + + let jefe = Jefe::from(JEFE.get_task_id()); + jefe.set_state(PowerState::A2 as u32); + + // Delay to allow things to settle, in case we were hot-plugged. + hl::sleep_for(STARTUP_SETTLE_MS); + + // Check the status of the PSU ON nets, which indicate the current commanded + // status of the PSUs. We can use this information to seed our state + // machines, and also to make sure we don't glitch the PSUs. + // + // Note that, on power-on reset, these pins default to being configured + // Analog, preventing us from reading their state. This is okay. In Analog + // mode, an STM32 pin is defined as reading as 0, so we will see any such + // pins as "PSU is ON" and switch the pin to input below. It is only if this + // task has _restarted_ that we'll find pins set to input seeing 0, or + // output seeing 1. + let initial_psu_enabled: [bool; PSU_COUNT] = { + let bits = sys.gpio_read(ALL_PSU_ENABLE_L_PINS); + // ON signals are active-low, so we check for the _absence_ of the bit: + core::array::from_fn(|i| bits & (1 << PSU_ENABLE_L_PINS[i]) == 0) + }; + + // Since we mostly just toggle the PSU ON nets between input and output, we + // don't actually want to configure them at all at this stage. They're + // either set input (in which case the PSU is being asked to be "on") or + // output (in which case we're holding the PSU off, and will start a fault + // resume sequence shortly). + // + // Ensure that the subset of pins that are currently undriven (which is to + // say, ENABLE line low, PSU on) are set as inputs. Leave any pins observed + // as 1 configured as they are. (See the rationale for this above on the + // initial read.) + sys.gpio_configure_input( + { + let mut inpins = PinSet { + port: PSU_ENABLE_L_PORT, + pin_mask: 0, + }; + for (on, pinno) in + initial_psu_enabled.into_iter().zip(PSU_ENABLE_L_PINS) + { + if on { + inpins = inpins.and_pin(pinno); + } + } + // This set might be empty. That's ok; sys tolerates this. + inpins + }, + Pull::None, + ); + + // While we are not going to explicitly configure any pins as outputs at + // this stage, for toggling the pins between input and output to work + // properly, we need to pre-arrange for the pins to be high once they _are_ + // set to output. We do that here. If the pin is input, this has no effect; + // if it's output, this should be a no-op because our previous incarnation + // will have done this before setting it to output. + sys.gpio_set_to(ALL_PSU_ENABLE_L_PINS, true); + + // Now, configure the presence/OK detect nets. We want these to be inputs; + // at power-on reset they're analog. Switching pins between those two modes + // cannot glitch, and nobody would be listening if it did. + sys.gpio_configure_input(ALL_PSU_PWR_OK_PINS, Pull::None); + sys.gpio_configure_input(ALL_PSU_PRESENT_L_PINS, Pull::None); + + // Collect all of the pin-change notifications we want into a mask word. + // We'll use this each time we want to listen for pins. + let all_pin_notifications = { + let mut bits = 0; + for mask in PSU_PWR_OK_NOTIF { + bits |= mask; + } + bits + }; + + // Turn on pin change notifications on all of our input nets. + sys.gpio_irq_configure(all_pin_notifications, Edge::Both); + + // Set up our state machines for each PSU. We'll need to read the presence + // pins to determine whether a PSU is present and if we should ask it for + // its serial number. + let present_l_bits = sys.gpio_read(ALL_PSU_PRESENT_L_PINS); + let start_time = sys_get_timer().now; + + let mut psus: [Psu; PSU_COUNT] = core::array::from_fn(|i| { + // TODO uncomment once the rectifier PMBus config is added + // let dev = { + // let i2c = I2C.get_task_id(); + // let make_dev = PSU_PMBUS_DEVS[i]; + // let (dev, rail) = make_dev(i2c); + // Mwocp68::new(&dev, rail) + // }; + let slot = PSU_SLOTS[i]; + let fruid = PsuFruid::default(); + let state = if present_l_bits & (1 << PSU_PRESENT_L_PINS[i]) == 0 { + // Hello, who are you? + + // TODO uncomment once the rectifier PMBus config is added + // fruid.refresh(&dev, slot, start_time); + + // ...and how are you doing? + PsuState::Present(if initial_psu_enabled[i] { + ringbuf_entry!(Event::FoundEnabled { + now: start_time, + psu: slot, + serial: fruid.serial + }); + PresentState::On { was_faulted: false } + } else { + // PSU was forced off by our previous incarnation. Schedule it to + // turn back on in the future if things clear up. + ringbuf_entry!(Event::FoundAlreadyDisabled { + now: start_time, + psu: slot, + serial: fruid.serial + }); + PresentState::Faulted { + turn_on_deadline: start_time.saturating_add(FAULT_OFF_MS), + } + }) + } else { + PsuState::NotPresent + }; + Psu { + slot, + state, + // TODO uncomment once the rectifier PMBus config is added + // dev, + fruid, + } + }); + + // Turn the chassis LED on to indicate that we're alive. + sys.gpio_set(STATUS_LED); + // TODO: if we wanted to kick jefe into a greater-than-A2 state, this'd be + // where it happens. + + // Poll things. + sys_set_timer(Some(start_time), notifications::TIMER_MASK); + let sleep_notifications = all_pin_notifications | notifications::TIMER_MASK; + loop { + sys.gpio_irq_control(all_pin_notifications, IrqControl::Enable) + .unwrap_lite(); + + let present_l_bits = sys.gpio_read(ALL_PSU_PRESENT_L_PINS); + let ok_bits = sys.gpio_read(ALL_PSU_PWR_OK_PINS); + + let now = sys_get_timer().now; + for i in 0..PSU_COUNT { + // Presence signals are active LOW. + let present = if present_l_bits & (1 << PSU_PRESENT_L_PINS[i]) == 0 + { + Present::Yes + } else { + Present::No + }; + // PWR_OK signals are active HIGH. + let ok = if ok_bits & (1 << PSU_PWR_OK_PINS[i]) != 0 { + Status::Good + } else { + Status::NotGood + }; + match psus[i].step(now, present, ok, &mut ereporter) { + None => (), + + Some(ActionRequired::EnableMe) => { + ringbuf_entry!(Event::Enabling { + now, + psu: PSU_SLOTS[i] + }); + // Enable the PSU by allowing `ENABLE_L` to float low, by no + // longer asserting high. + sys.gpio_configure_input( + PSU_ENABLE_L_PORT.pin(PSU_ENABLE_L_PINS[i]), + Pull::None, + ); + } + Some(ActionRequired::DisableMe { attempt_snapshot }) => { + if attempt_snapshot { + // TODO snapshot goes here + } + ringbuf_entry!(Event::Disabling { + now, + psu: PSU_SLOTS[i], + present: attempt_snapshot, + }); + + // Pull `ENABLE_L` high to disable the PSU. + sys.gpio_configure_output( + PSU_ENABLE_L_PORT.pin(PSU_ENABLE_L_PINS[i]), + OutputType::PushPull, + Speed::Low, + Pull::None, + ); + } + } + } + + // Wait for a pin change or timer. + let n = sys_recv_notification(sleep_notifications); + // If the timer bit is set _and the timer has actually fired_... + if n.has_timer_fired(notifications::TIMER_MASK) { + // Reset our timer forward. + sys_set_timer( + Some(now.saturating_add(POLL_MS)), + notifications::TIMER_MASK, + ); + } + // Ignore pin change notification bits, we just handle all the pins + // above. We also _enable_ the pin change interrupts at the top of the + // loop. + } +} + +#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] +enum Present { + #[default] + No, + Yes, +} + +#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] +enum Status { + #[default] + NotGood, + Good, +} + +struct Psu { + slot: Slot, + state: PsuState, + // TODO uncomment once the rectifier PMBus config is added + // dev: Mwocp68, + /// Because we would like to include the PSU's FRU ID information in the + /// ereports generated when a PSU is *removed*, we must cache it here rather + /// than reading it from the device when we generate an ereport for it. + fruid: PsuFruid, +} + +impl Psu { + /// Advances the PSU management state machine given the current time (`now`) + /// and the state of the `present` and `pwr_ok` inputs. + /// + /// This may be called at unpredictable intervals, and may be called more + /// than once for the same timestamp value. The implementation **must** use + /// `now` and the timer to control any time-sensitive operations. + fn step( + &mut self, + now: u64, + present: Present, + pwr_ok: Status, + ereporter: &mut Ereporter, + ) -> Option { + match (self.state, present, pwr_ok) { + (PsuState::NotPresent, Present::No, _) => { + // ignore the power good line, it is meaningless. + None + } + + // Regardless of our current state, if we observe the present line + // low, treat the PSU as having been disconnected. + // + // Other than detecting removal, the main side effect of this + // decision is that the "NewlyInserted" settle time starts after the + // contacts are _done_ scraping, not when they start. + (PsuState::Present(_), Present::No, _) => { + ringbuf_entry!(Event::Removed { + now, + psu: self.slot + }); + let _ = ereporter.deliver_ereport(&PsuRemovedEreport { + fields: self.ereport_fields(), + }); + + self.state = PsuState::NotPresent; + // Clear the FRUID serial only *after* we have put it in the ereport. + self.fruid = PsuFruid::default(); + + Some(ActionRequired::DisableMe { + attempt_snapshot: false, + }) + } + + // In a not-present situation we have to ignore the OK line entirely + // and only watch for the presence line to indicate the PSU has + // appeared. + (PsuState::NotPresent, Present::Yes, _) => { + let settle_deadline = now.wrapping_add(INSERT_DEBOUNCE_MS); + self.state = PsuState::Present(PresentState::NewlyInserted { + settle_deadline, + }); + // Hello, who are you? + self.fruid = PsuFruid::default(); + self.refresh_fruid(now); + ringbuf_entry!(Event::Inserted { + now, + psu: self.slot, + serial: self.fruid.serial + }); + // No external action required until our timer elapses. + None + } + + ( + PsuState::Present(PresentState::NewlyInserted { + settle_deadline, + }), + Present::Yes, + _, + ) => { + // Hello, who are you? + self.refresh_fruid(now); + if settle_deadline <= now { + // The PSU is still present (since the Present::No case above + // didn't fire) and our deadline has elapsed. Let's treat this + // as valid! + self.state = PsuState::Present(PresentState::On { + was_faulted: false, + }); + let _ = ereporter.deliver_ereport(&PsuInsertedEreport { + fields: self.ereport_fields(), + }); + + Some(ActionRequired::EnableMe) + } else { + // Remain in this state. + None + } + } + + // yay! + ( + PsuState::Present(PresentState::On { was_faulted }), + Present::Yes, + Status::Good, + ) => { + // Just in case we were previously unable to read any FRUID + // values due to I2C weather, try to refresh them + self.refresh_fruid(now); + + // If we just turned this PSU back on after a fault, reasserting + // POWER_GOOD means that the fault has cleared. + if was_faulted { + // Clear our tracking of the fault. If we fault again, treat + // that as a new fault. + self.state = PsuState::Present(PresentState::On { + was_faulted: false, + }); + ringbuf_entry!( + __TRACE, + Trace::PowerGoodAsserted { + now, + psu: self.slot, + } + ); + // Report that the fault has gone away. + let _ = ereporter.deliver_ereport(&PowerGoodEreport { + pmbus_status: self.read_pmbus_status(now), + fields: self.ereport_fields(), + }); + } + + None + } + ( + PsuState::Present(PresentState::On { was_faulted }), + Present::Yes, + Status::NotGood, + ) => { + // The PSU appears to have pulled the OK signal into the "not + // OK" state to indicate an internal fault! + + let turn_on_deadline = now.wrapping_add(FAULT_OFF_MS); + self.state = PsuState::Present(PresentState::Faulted { + turn_on_deadline, + }); + // Did we just restart after a fault? If not, this is a new + // fault, which should be reported. + if !was_faulted { + ringbuf_entry!( + __TRACE, + Trace::PowerGoodDeasserted { + now, + psu: self.slot, + } + ); + let _ = ereporter.deliver_ereport(&PowerUngoodEreport { + fields: self.ereport_fields(), + pmbus_status: self.read_pmbus_status(now), + }); + } else { + ringbuf_entry!( + __TRACE, + Trace::PowerStillUngood { + now, + psu: self.slot, + } + ); + }; + + Some(ActionRequired::DisableMe { + attempt_snapshot: true, + }) + } + + ( + PsuState::Present(PresentState::Faulted { turn_on_deadline }), + Present::Yes, + _, + ) => { + if turn_on_deadline <= now { + // We turn the PSU back on _without regard_ to the OK signal + // state, because the PSU won't assert OK when it's off! We + // learned this the hard way. See #1800. + self.state = PsuState::Present(PresentState::OnProbation { + deadline: now.saturating_add(PROBATION_MS), + }); + Some(ActionRequired::EnableMe) + } else { + None + } + } + ( + PsuState::Present(PresentState::OnProbation { deadline }), + Present::Yes, + _, + ) => { + // Just in case we were previously unable to read any FRUID + // values due to I2C weather, try to refresh them + self.refresh_fruid(now); + if deadline <= now { + // Take PSU out of probation state and start monitoring its + // OK line. + self.state = PsuState::Present(PresentState::On { + was_faulted: true, + }); + None + } else { + // Remain in this state. + None + } + } + } + } + + fn refresh_fruid(&mut self, _now: u64) { + // TODO uncomment once the rectifier PMBus config is added + // self.fruid.refresh(&self.dev, self.slot, now); + } + + fn read_pmbus_status(&mut self, _now: u64) -> ereports::pwr::PmbusStatus { + ereports::pwr::PmbusStatus::default() + + // TODO uncomment once the rectifier PMBus config is added + /* + let status_word = + retry_i2c_txn(now, self.slot, || self.dev.status_word()) + .map(|data| data.0); + ringbuf_entry!( + __TRACE, + Trace::StatusWord { + psu: self.slot, + now, + status_word + } + ); + + let status_iout = + retry_i2c_txn(now, self.slot, || self.dev.status_iout()) + .map(|data| data.0); + ringbuf_entry!( + __TRACE, + Trace::StatusIout { + psu: self.slot, + now, + status_iout + } + ); + + let status_vout = + retry_i2c_txn(now, self.slot, || self.dev.status_vout()) + .map(|data| data.0); + ringbuf_entry!( + __TRACE, + Trace::StatusVout { + psu: self.slot, + now, + status_vout + } + ); + let status_input = + retry_i2c_txn(now, self.slot, || self.dev.status_input()) + .map(|data| data.0); + ringbuf_entry!( + __TRACE, + Trace::StatusInput { + psu: self.slot, + now, + status_input, + } + ); + + let status_cml = + retry_i2c_txn(now, self.slot, || self.dev.status_cml()) + .map(|data| data.0); + ringbuf_entry!( + __TRACE, + Trace::StatusCml { + psu: self.slot, + now, + status_cml + } + ); + + let status_temperature = + retry_i2c_txn(now, self.slot, || self.dev.status_temperature()) + .map(|data| data.0); + ringbuf_entry!( + __TRACE, + Trace::StatusTemperature { + psu: self.slot, + now, + status_temperature + } + ); + + let status_mfr_specific = + retry_i2c_txn(now, self.slot, || self.dev.status_mfr_specific()) + .map(|data| data.0); + ringbuf_entry!( + __TRACE, + Trace::StatusMfrSpecific { + psu: self.slot, + now, + status_mfr_specific + } + ); + + ereports::pwr::PmbusStatus { + word: status_word.ok(), + iout: status_iout.ok(), + vout: status_vout.ok(), + input: status_input.ok(), + cml: status_cml.ok(), + temp: status_temperature.ok(), + mfr: status_mfr_specific.ok(), + } + */ + } + + fn ereport_fields(&self) -> EreportFields { + let rail = { + // This is a little silly, but it stops us from having to 6 separate + // instances of the string "V54_PSU" in the binary... + let mut v54_psu = *b"V54_PSUx"; + v54_psu[7] = match self.slot { + Slot::Psu0 => b'0', + Slot::Psu1 => b'1', + Slot::Psu2 => b'2', + Slot::Psu3 => b'3', + Slot::Psu4 => b'4', + Slot::Psu5 => b'5', + }; + FixedString::try_from_utf8(&v54_psu[..]).unwrap_lite() + }; + EreportFields { + // TODO uncomment once the rectifier PMBus config is added + // refdes: FixedStr::from_str(self.dev.i2c_device().component_id()), + refdes: FixedStr::from_str("UNKNOWN"), + rail, + slot: self.slot as u8, + fruid: self.fruid, + } + } +} + +#[derive(Copy, Clone, Default, microcbor::Encode)] +struct PsuFruid { + mfr: Option>, + mpn: Option>, + serial: Option>, + fw_rev: Option>, +} + +impl PsuFruid { + fn refresh(&mut self, dev: &Mwocp68, psu: Slot, now: u64) { + if self.mfr.is_none() { + self.mfr = retry_i2c_txn(now, psu, || dev.mfr_id()) + .ok() + .and_then(|v| FixedString::try_from_utf8(&v.0[..]).ok()); + } + + if self.serial.is_none() { + self.serial = retry_i2c_txn(now, psu, || dev.serial_number()) + .ok() + .and_then(|v| FixedString::try_from_utf8(&v.0[..]).ok()); + } + + if self.mpn.is_none() { + self.mpn = retry_i2c_txn(now, psu, || dev.model_number()) + .ok() + .and_then(|v| FixedString::try_from_utf8(&v.0[..]).ok()); + } + + if self.fw_rev.is_none() { + self.fw_rev = retry_i2c_txn(now, psu, || dev.firmware_revision()) + .ok() + .and_then(|v| FixedString::try_from_utf8(&v.0[..]).ok()); + } + } +} + +fn retry_i2c_txn( + now: u64, + psu: Slot, + mut txn: impl FnMut() -> Result, +) -> Result { + // Chosen by fair dice roll, seems reasonable-ish? + let mut retries_remaining = 3; + loop { + match txn() { + Ok(x) => return Ok(x), + Err(err) => { + ringbuf_entry!(__TRACE, Trace::I2cError { now, psu, err }); + + if retries_remaining == 0 { + return Err(err); + } + + retries_remaining -= 1; + } + } + } +} + +include!(concat!(env!("OUT_DIR"), "/notifications.rs")); + +include!(concat!(env!("OUT_DIR"), "/i2c_config.rs")); + +ereports::declare_ereporter! { + struct Ereporter { + PsuInserted(PsuInsertedEreport), + PsuRemoved(PsuRemovedEreport), + PowerGood(PowerGoodEreport), + PowerUngood(PowerUngoodEreport) + } +} + +#[derive(microcbor::Encode)] +#[ereport(class = "hw.insert.psu", version = 0)] +struct PsuInsertedEreport { + #[cbor(flatten)] + fields: EreportFields, +} + +#[derive(microcbor::Encode)] +#[ereport(class = "hw.remove.psu", version = 0)] +struct PsuRemovedEreport { + #[cbor(flatten)] + fields: EreportFields, +} + +#[derive(microcbor::Encode)] +#[ereport(class = "hw.pwr.pwr_good.good", version = 0)] +struct PowerGoodEreport { + #[cbor(flatten)] + fields: EreportFields, + pmbus_status: ereports::pwr::PmbusStatus, +} + +#[derive(microcbor::Encode)] +#[ereport(class = "hw.pwr.pwr_good.bad", version = 0)] +struct PowerUngoodEreport { + #[cbor(flatten)] + fields: EreportFields, + pmbus_status: ereports::pwr::PmbusStatus, +} + +#[derive(microcbor::EncodeFields)] +struct EreportFields { + refdes: FixedStr<'static, 20>, // Component ID max length + rail: FixedString<8>, // "V54_PSUx" + slot: u8, + fruid: PsuFruid, +} diff --git a/drv/psc-seq-server/build.rs b/drv/psc-seq-server/build.rs index 19a69a848d..bb6fc7a3e7 100644 --- a/drv/psc-seq-server/build.rs +++ b/drv/psc-seq-server/build.rs @@ -4,6 +4,7 @@ fn main() -> Result<(), Box> { build_util::build_notifications()?; + build_util::expose_target_board(); let disposition = build_i2c::Disposition::Devices; diff --git a/drv/stm32h7-sprot-server/src/main.rs b/drv/stm32h7-sprot-server/src/main.rs index b4bbf390d7..594051d5e3 100644 --- a/drv/stm32h7-sprot-server/src/main.rs +++ b/drv/stm32h7-sprot-server/src/main.rs @@ -122,6 +122,7 @@ cfg_if::cfg_if! { target_board = "sidecar-d", target_board = "psc-b", target_board = "psc-c", + target_board = "observer-a", target_board = "gemini-bu-1", target_board = "grapefruit-a", target_board = "grapefruit-b", diff --git a/drv/user-leds/src/main.rs b/drv/user-leds/src/main.rs index 804d2c51d1..2ed154fe46 100644 --- a/drv/user-leds/src/main.rs +++ b/drv/user-leds/src/main.rs @@ -96,6 +96,7 @@ cfg_if::cfg_if! { target_board = "gimlet-f", target_board = "psc-b", target_board = "psc-c", + target_board = "observer-a", target_board = "oxcon2023g0", target_board = "grapefruit-a", target_board = "grapefruit-b", @@ -486,6 +487,7 @@ cfg_if::cfg_if! { target_board = "gimlet-f", target_board = "psc-b", target_board = "psc-c", + target_board = "observer-a", ))] { const LEDS: &[(drv_stm32xx_sys_api::PinSet, bool)] = &[ (drv_stm32xx_sys_api::Port::A.pin(3), false), diff --git a/drv/vsc-err/src/lib.rs b/drv/vsc-err/src/lib.rs index a20a739732..d8c200c504 100644 --- a/drv/vsc-err/src/lib.rs +++ b/drv/vsc-err/src/lib.rs @@ -12,7 +12,7 @@ use drv_spi_api::SpiError; use idol_runtime::ServerDeath; -#[derive(Copy, Clone, Eq, PartialEq, counters::Count)] +#[derive(Copy, Clone, Eq, PartialEq, counters::Count, Debug)] pub enum VscError { SpiError(#[count(children)] SpiError), ServerDied, diff --git a/task/control-plane-agent/Cargo.toml b/task/control-plane-agent/Cargo.toml index d8bb9ba575..4ab7d06571 100644 --- a/task/control-plane-agent/Cargo.toml +++ b/task/control-plane-agent/Cargo.toml @@ -66,6 +66,7 @@ gimlet = ["compute-sled"] cosmo = ["compute-sled"] sidecar = ["drv-sidecar-seq-api", "drv-monorail-api", "drv-ignition-api", "drv-transceivers-api", "p256", "sha2", "drv-rng-api"] psc = ["drv-user-leds-api"] +observer = ["drv-user-leds-api"] minibar = ["drv-ignition-api"] vpd = ["task-vpd-api"] vlan = ["task-net-api/vlan"] diff --git a/task/control-plane-agent/src/main.rs b/task/control-plane-agent/src/main.rs index bfb414e2b3..7a30270fce 100644 --- a/task/control-plane-agent/src/main.rs +++ b/task/control-plane-agent/src/main.rs @@ -39,6 +39,7 @@ pub(crate) mod dump; #[cfg_attr(feature = "compute-sled", path = "mgs_compute_sled.rs")] #[cfg_attr(feature = "sidecar", path = "mgs_sidecar.rs")] #[cfg_attr(feature = "psc", path = "mgs_psc.rs")] +#[cfg_attr(feature = "observer", path = "mgs_psc.rs")] #[cfg_attr(feature = "minibar", path = "mgs_minibar.rs")] mod mgs_handler; diff --git a/task/net/src/bsp/observer_a.rs b/task/net/src/bsp/observer_a.rs new file mode 100644 index 0000000000..43895a092e --- /dev/null +++ b/task/net/src/bsp/observer_a.rs @@ -0,0 +1,149 @@ +// 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/. + +#[cfg(not(all(feature = "ksz8463", feature = "mgmt", feature = "vlan")))] +compile_error!("this BSP requires the ksz8463, mgmt, and vlan features"); + +use crate::{ + bsp_support::{self, Ksz8463}, + mgmt, notifications, pins, +}; +use drv_psc_seq_api::PowerState; +use drv_spi_api::SpiServer; +use drv_stm32h7_eth as eth; +use drv_stm32xx_sys_api::{Alternate, Port, Sys}; +use task_jefe_api::Jefe; +use task_net_api::{ + ManagementCounters, ManagementLinkStatus, MgmtError, PhyError, +}; +use userlib::{FromPrimitive, sys_recv_notification}; +use vsc7448_pac::types::PhyRegisterAddress; + +//////////////////////////////////////////////////////////////////////////////// + +pub struct BspImpl(mgmt::Bsp); + +// TODO V2P5_PHY_A2_PG and V1P0_PHY_A2_PG are now routed to Ignition rather than +// the SP. Do we need to wait for them? +const PG_PINS: [drv_stm32xx_sys_api::PinSet; 0] = []; + +impl bsp_support::Bsp for BspImpl { + // This system wants to be woken periodically to do logging + const WAKE_INTERVAL: Option = Some(500); + + /// Stateless function to configure ethernet pins before the Bsp struct + /// is actually constructed + fn configure_ethernet_pins(sys: &Sys) { + pins::RmiiPins { + refclk: Port::A.pin(1), // CLK_50MHZ_RMII_KSZ8463 + crs_dv: Port::A.pin(7), // RMII_KSZ8463_TO_SP_RX_DV + tx_en: Port::G.pin(11), // RMII_SP_TO_KSZ8463_TX_EN_ + txd0: Port::G.pin(13), // RMII_SP_TO_KSZ8463_TXD0 + txd1: Port::G.pin(12), // RMII_SP_TO_KSZ8463_TXD1 + + rxd0: Port::C.pin(4), // RMII_KSZ8463_TO_SP_RXD0 + rxd1: Port::C.pin(5), // RMII_KSZ8463_TO_SP_RXD1 + af: Alternate::AF11, + } + .configure(sys); + + pins::MdioPins { + mdio: Port::A.pin(2), // MIM_SP_TO_VSC8562_MDIO_V3P3 + mdc: Port::C.pin(1), // MIM_SP_TO_VSC8562_MDC_V3P3 + af: Alternate::AF11, + } + .configure(sys); + } + + fn preinit() { + // Wait for the sequencer to turn read our VPD. + let jefe = Jefe::from(crate::JEFE.get_task_id()); + + loop { + // This laborious list is intended to ensure that new power states + // have to be added explicitly here. + match PowerState::from_u32(jefe.get_state()) { + Some(PowerState::A2) => { + break; + } + Some(PowerState::Init) | None => { + // This happens before we're in a valid power state. + // + // Only listen to our Jefe notification. + sys_recv_notification( + notifications::JEFE_STATE_CHANGE_MASK, + ); + } + } + } + } + + fn new(eth: ð::Ethernet, sys: &Sys) -> Self { + let spi = bsp_support::claim_spi(sys); + let ksz8463_dev = spi.device(drv_spi_api::devices::KSZ8463); + let bsp = mgmt::Config { + // A2_EN is now controlled by Ignition + power_en: None, + slow_power_en: false, + power_good: &PG_PINS, + + ksz8463: Ksz8463::new(ksz8463_dev), + ksz8463_nrst: Port::E.pin(1), // SP_TO_KSZ8463_RESET_L + ksz8463_rst_type: mgmt::Ksz8463ResetSpeed::Normal, + ksz8463_vlan_mode: ksz8463::VLanMode::Mandatory, + + // SP_TO_VSC8562_COMA_MODE_V3P3 + vsc85x2_coma_mode: Some(Port::C.pin(3)), + + // SP_TO_VSC8562_RESET_L_V3P3 + vsc85x2_nrst: Port::C.pin(2), + + vsc85x2_base_port: 0b11110, // Based on resistor strapping + } + .build(sys, eth); + + Self(bsp) + } + + fn wake(&self, eth: ð::Ethernet) { + self.0.wake(eth); + } + + fn phy_read( + &mut self, + port: u8, + reg: PhyRegisterAddress, + eth: ð::Ethernet, + ) -> Result { + self.0.phy_read(port, reg, eth) + } + + fn phy_write( + &mut self, + port: u8, + reg: PhyRegisterAddress, + value: u16, + eth: ð::Ethernet, + ) -> Result<(), PhyError> { + self.0.phy_write(port, reg, value, eth) + } + + fn ksz8463(&self) -> &Ksz8463 { + &self.0.ksz8463 + } + + fn management_link_status( + &self, + eth: ð::Ethernet, + ) -> Result { + self.0.management_link_status(eth) + } + + fn management_counters( + &self, + eth: &crate::eth::Ethernet, + ) -> Result { + self.0.management_counters(eth) + } +} diff --git a/task/net/src/main.rs b/task/net/src/main.rs index 9f231a4b83..4174354383 100644 --- a/task/net/src/main.rs +++ b/task/net/src/main.rs @@ -39,6 +39,7 @@ mod server; any(target_board = "psc-b", target_board = "psc-c"), path = "bsp/psc_bc.rs" )] +#[cfg_attr(target_board = "observer-a", path = "bsp/observer_a.rs")] #[cfg_attr(target_board = "gimletlet-1", path = "bsp/gimletlet_mgmt.rs")] #[cfg_attr( all(target_board = "gimletlet-2", feature = "gimletlet-nic"), diff --git a/task/net/src/mgmt.rs b/task/net/src/mgmt.rs index f2d8a31c14..6115809e53 100644 --- a/task/net/src/mgmt.rs +++ b/task/net/src/mgmt.rs @@ -13,7 +13,7 @@ use ringbuf::*; use task_net_api::{ ManagementCounters, ManagementLinkStatus, MgmtError, PhyError, }; -use userlib::{UnwrapLite, hl::sleep_for}; +use userlib::hl::sleep_for; use vsc85xx::{Counter, VscError, vsc85x2::Vsc85x2}; use vsc7448_pac::{phy, types::PhyRegisterAddress}; @@ -34,12 +34,12 @@ enum Trace { #[count(skip)] None, Ksz8463Err { - port: KszPort, + port: Option, #[count(children)] err: KszError, }, Vsc85x2Err { - port: u8, + port: Option, #[count(children)] err: VscError, }, @@ -103,7 +103,10 @@ impl Config { // VSC8552 over 100-BASE FX self.ksz8463 .configure(ksz8463::Mode::Fiber, self.ksz8463_vlan_mode) - .unwrap_lite(); + .inspect_err(|&err| { + ringbuf_entry!(Trace::Ksz8463Err { port: None, err }) + }) + .unwrap(); self.ksz8463 } @@ -162,7 +165,11 @@ impl Config { sys.gpio_reset(coma_mode); } - vsc85x2.unwrap_lite() // TODO + vsc85x2 + .inspect_err(|&err| { + ringbuf_entry!(Trace::Vsc85x2Err { port: None, err }) + }) + .unwrap() } } @@ -232,7 +239,7 @@ impl Bsp { } Err(err) => { ringbuf_entry!(Trace::Ksz8463Err { - port: port.into(), + port: Some(port.into()), err }); return Err(MgmtError::KszError); @@ -247,7 +254,10 @@ impl Bsp { s.vsc85x2_100base_fx_link_up[i] = (sr.0 & (1 << 2)) != 0 } Err(err) => { - ringbuf_entry!(Trace::Vsc85x2Err { port, err }); + ringbuf_entry!(Trace::Vsc85x2Err { + port: Some(port), + err + }); return Err(MgmtError::VscError); } }; @@ -256,7 +266,10 @@ impl Bsp { s.vsc85x2_sgmii_link_up[i] = status.mac_link_status() != 0 } Err(err) => { - ringbuf_entry!(Trace::Vsc85x2Err { port, err }); + ringbuf_entry!(Trace::Vsc85x2Err { + port: Some(port), + err + }); return Err(MgmtError::VscError); } }; @@ -275,7 +288,10 @@ impl Bsp { let out = match self.ksz8463.read_mib_counter(port, reg) { Ok(c) => c, Err(err) => { - ringbuf_entry!(Trace::Ksz8463Err { port, err }); + ringbuf_entry!(Trace::Ksz8463Err { + port: Some(port), + err + }); return Err(MgmtError::KszError); } }; @@ -313,7 +329,10 @@ impl Bsp { let decode_tx_rx = |v, port| match v { Ok((tx, rx)) => Ok((decode_counter(tx), decode_counter(rx))), Err(err) => { - ringbuf_entry!(Trace::Vsc85x2Err { port, err }); + ringbuf_entry!(Trace::Vsc85x2Err { + port: Some(port), + err + }); Err(MgmtError::VscError) } }; diff --git a/task/packrat/build.rs b/task/packrat/build.rs index c7e9964e3a..b56fe8eff2 100644 --- a/task/packrat/build.rs +++ b/task/packrat/build.rs @@ -39,6 +39,10 @@ fn main() -> Result<()> { "packrat's `gimlet` feature should not be enabled when ", "building for PSCs", )), + Some("observer-a") => panic!(concat!( + "packrat's `gimlet` feature should not be enabled when ", + "building for Observers", + )), Some("sidecar-b" | "sidecar-c" | "sidecar-d") => panic!(concat!( "packrat's `gimlet` feature should not be enabled when ", "building for sidecars", diff --git a/task/power/Cargo.toml b/task/power/Cargo.toml index dc6a9f8547..b6f7e3d3ef 100644 --- a/task/power/Cargo.toml +++ b/task/power/Cargo.toml @@ -40,6 +40,7 @@ gimlet = ["drv-cpu-seq-api", "h753"] cosmo = ["drv-cpu-seq-api", "h753"] sidecar = ["drv-sidecar-seq-api", "h753"] psc = ["drv-stm32xx-sys-api", "h753"] +observer = ["drv-stm32xx-sys-api", "h753"] dc2024 = ["drv-stm32xx-sys-api", "h753"] h743 = ["build-i2c/h743"] h753 = ["build-i2c/h753"] diff --git a/task/power/src/bsp/observer_a.rs b/task/power/src/bsp/observer_a.rs new file mode 100644 index 0000000000..5f29e827db --- /dev/null +++ b/task/power/src/bsp/observer_a.rs @@ -0,0 +1,47 @@ +// 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/. + +use crate::{PowerControllerConfig, PowerState}; + +// TODO add rectifiers (once we have their SMBus spec) +pub(crate) const CONTROLLER_CONFIG_LEN: usize = 0; +pub(crate) static CONTROLLER_CONFIG: [PowerControllerConfig; + CONTROLLER_CONFIG_LEN] = []; + +pub(crate) fn get_state() -> PowerState { + PowerState::A2 +} + +pub(crate) struct State(()); + +impl State { + pub(crate) fn init() -> Self { + // Before talking to the power shelves, we have to enable an I2C buffer + userlib::task_slot!(SYS, sys); + use drv_stm32xx_sys_api::*; + + let sys_task = SYS.get_task_id(); + let sys = Sys::from(sys_task); + + let i2c_en = Port::H.pin(10); // SP_TO_POWER_SHELF_PMBUS_BUFFER_EN + sys.gpio_set(i2c_en); + sys.gpio_configure_output( + i2c_en, + OutputType::PushPull, + Speed::Low, + Pull::None, + ); + + State(()) + } + + pub(crate) fn handle_timer_fired( + &self, + _devices: &[crate::Device], + _state: PowerState, + ) { + } +} + +pub const HAS_RENDMP_BLACKBOX: bool = false; diff --git a/task/power/src/main.rs b/task/power/src/main.rs index 0c511ca6e3..4c648c54a9 100644 --- a/task/power/src/main.rs +++ b/task/power/src/main.rs @@ -497,6 +497,7 @@ macro_rules! mwocp68_controller { any(target_board = "psc-b", target_board = "psc-c"), path = "bsp/psc_bc.rs" )] +#[cfg_attr(target_board = "observer-a", path = "bsp/observer_a.rs")] #[cfg_attr( any( target_board = "sidecar-b", diff --git a/task/sensor-polling/src/main.rs b/task/sensor-polling/src/main.rs index 39c536a969..ea5708e934 100644 --- a/task/sensor-polling/src/main.rs +++ b/task/sensor-polling/src/main.rs @@ -132,9 +132,10 @@ fn main() -> ! { //////////////////////////////////////////////////////////////////////////////// +#[cfg(any(target_board = "psc-b", target_board = "psc-c",))] use i2c_config::{devices, sensors}; -#[cfg(any(target_board = "psc-b", target_board = "psc-c"))] +#[cfg(any(target_board = "psc-b", target_board = "psc-c",))] static SENSORS: [TemperatureSensor; 6] = [ TemperatureSensor::new( Device::Mwocp68, @@ -174,6 +175,11 @@ static SENSORS: [TemperatureSensor; 6] = [ ), ]; +// TODO fill in sensors once we know how to talk to the MWOCP67, and +// unconditionally import i2c_config +#[cfg(target_board = "observer-a")] +static SENSORS: [TemperatureSensor; 0] = []; + //////////////////////////////////////////////////////////////////////////////// include!(concat!(env!("OUT_DIR"), "/i2c_config.rs"));