Skip to content

Commit aa843e7

Browse files
authored
cpu_seq: Fix a variety of hilarious bugs in sequencer IRQ handling (#2390)
Depends on #2392 There are several things wrong with the current handling of sequencer FPGA interrupts (on Cosmo) and PMBus alerts in the Gimlet sequencer. To wit: 1. On Cosmo, the `cosmo_seq_server` task does not handle the `NIC_MAPO` interrupt from the sequencer FPGA. When the NIC MAPOs, we should be transitioning from A0+HP back to A0, rather than just ignoring the NIC MAPO event. Presently, ignoring NIC MAPOs result in an improper thermal shutdown (see #2384), as the `thermal` task continues to poll the NIC's temperature sensor despite it no longer being powered, and extrapolates a worst-case temperature trend in the face of I2C NACKs from the temperature sensor, which eventually result in a thermal shutdown. This isn't great. 2. Both the Cosmo sequencer FPGA interrupt and the Gimlet V<sub>core</sub> VRM PMBus alert handling are implemented using the STM32 external interrupt (EXTI) peripheral, which only supports edge-triggered interrupts (not level-triggered). On Cosmo, where the FPGA IRQ line can be triggered by multiple bits in the FPGA's Interrupt Flags Register (IFR) being set, this means that we can miss interrupts in a situation where one IFR bit is set and then, *after* Hubris reads the IFR, a second IFR bit is *also* set while the `FPGA1_TO_SP_IRQ_L` line remains asserted. In this case, any bits which are set after the initial IFR read are completely missed by Hubris. Sadly, it turns out that this is often what happens in the event of a voltage sag on `V12_SYS_A2` similar to mfg-quality#140. If the voltage dips enough to both trigger the RAA229620A's V<sub>in</sub> undervolt warning **and** enough to MAPO the T6, the T6 MAPO tends to occur while we are in the interrupt handler but after the initial IFR read, so the NIC MAPO interrupt tends to be missed. Cool. 3. Our code that attempts to clear PMBus faults in the RAA229620As (on Cosmo)/RAA229618 (on Gimlet)...doesn't due that (see #2389), due to multiple _hilarious_ bugs: - We are currently sending the PMBus `CLEAR_FAULTS` command without a PMBus page (rail) associated with it. It turns out that this does _not_ actually result in clearing faults on all pages. Instead, it results in, uh, nothing happening. Quoth the PMBus standard: > Commands to clear a bit are gated by the PAGE command. The > CLEAR_FAULTS can be made to clear all faults on all pages by > setting the page command to FFh. > > --- _PMBus Power System Mgt Protocol Specification – Part II – > Revision 1.3.1_; §10.3 (p. 44) So, sending the command without a page doesn't actually do anything. That's great. - The second thing that stops this from working is _technically_ our fault but is due to a decision on the part of the vendor which is both profoundly bone-headed and somewhat poorly documented (as is traditional). As @nathanaelhuffman discovered, the RAA229620A documentation for the `VIN_UV_FAULT_RESPONSE` register, states the following: > Configures the input undervoltage fault response. **For a fault > to be considered cleared, the input voltage must rise by 1/16th > of the UV fault threshold value.** > > --- _R16DS0309EU0100 Rev.1.00_ §11.36 "VIN_OV_FAULT_RESPONSE" (p. > 57) (emphasis mine) When we configure the undervoltage warning, [we set the threshold to 11.75V][1]. If you remember fractions from...elementary school?[^1] you may have already realized that 1/16 * 11.75V = 0.734375V. This means that for the VRM to clear the fault, the input voltage must reach 12.484375V (since 11.75V + 0.734375V = 12.484375V). Yes, that is *almost half a volt above the nominal voltage for the 12V rail*. It isn't supposed to do that! It's supposed to be 12V!! So, even if a voltage sag has ended and the input voltage to the VRM has returned to 12V, the fault will not clear because the voltage has not increased by the requisite 1/16th of the undervolt threshold. Isn't that wonderful? 5. There was also a FPGA bug in which the `NIC_MAPO` bit did not actually get unset as expected. This branch unwinds the entire Litany of Sadness and Pain described above, by doing the following things: - Integrating with @nathanaelhuffman's changes in oxidecomputer/quartz#467, which fixes the bug with clearing the `IFR[NIC_MAPO]` bit, and also changes all the IFR flags to be write-1-to-clear. Now, we can clear them properly in the Cosmo sequencer's routine for handling FPGA IRQs. This required #2392 as the new FPGA register map wouldn't build without it. - Actually making the NIC MAPO interrupt transition the system state from A0+HP to A0, which (in conjunction with #2391) fixes #2384. - Changing the code for clearing RAA22960A and RAA229618 PMBus faults to do paged writes rather than unpaged writes, fixing #2389. - Changing the V<sub>in</sub> undervolt warning threshold from 11.75V to 11V, so that the fault actually clears when the input voltage returns to normal. - On Cosmo, changing the code for handling the EXTI notification to continue reading the FPGA IFR register and clearing any set interrupt bits until the IRQ line is actually deasserted in a loop, to compensate for the possibility of additional bits being set whilst we are already handling an interrupt but after we have read the IFR. This does not apply to the PMBus alerts from the RAA229620As, as they are set in the IFR as long as the corresponding PMBus alert line to the FPGA is asserted, and cannot be cleared by writing back a 1 to the IFR. Instead, we mask them out by setting the corresponding bits in the FPGA Interrupt Enable Register (IER), so that they can remain asserted without the FPGA continuing to assert the IRQ. - Masking out the RAA229620A PMBus alerts _half-solves_ the problem, but it doesn't reset the IER when the PMBus fault actually does clear, so we wouldn't be notified of a _subsequent_ fault condition. Thus, we must also set a flag that tells the sequencer task to continue trying to clear faults in the VRM(s) that asserted the PMBus alert periodically until the fault actually goes away. If it does, we then re-enable IRQs for the corresponding `PMALERT_L` pin(s). - A slightly simpler version of that also had to happen on Gimlet. It's simpler because Gimlet only monitors a single RAA229618. Whew okay yeah, I think that's everything. This was a whole thing. Fixes #2384 Fixes #2389 [1]: https://github.com/oxidecomputer/hubris/blob/939c48a1ff8e3ab059d8813f6a58b82fdaccc562/drv/cosmo-seq-server/src/vcore.rs#L85-L91 [^1]: Or middle school? When do children learn fractions?
1 parent 49bc625 commit aa843e7

12 files changed

Lines changed: 1202 additions & 145 deletions

File tree

drv/cosmo-seq-server/src/main.rs

Lines changed: 201 additions & 32 deletions
Large diffs are not rendered by default.

drv/cosmo-seq-server/src/vcore.rs

Lines changed: 155 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ use super::i2c_config;
1616
use drv_i2c_api::ResponseCode;
1717
use drv_i2c_devices::raa229620a::{self, Raa229620A};
1818
use fixedstr::FixedStr;
19+
use pmbus::commands::raa229620a::STATUS_WORD;
1920
use ringbuf::*;
2021
use userlib::{sys_get_timer, units, TaskId};
2122

@@ -24,14 +25,15 @@ pub(super) struct VCore {
2425
vddcr_cpu0: Raa229620A,
2526
/// `PWR_CONT2`: This regulator controls `VDDCR_CPU1` and `VDDIO_SP5` rails.
2627
vddcr_cpu1: Raa229620A,
28+
faulted: Vrms,
2729
packrat: task_packrat_api::Packrat,
2830
}
2931

3032
#[derive(Copy, Clone, PartialEq, microcbor::Encode)]
3133
pub(crate) enum Rail {
32-
#[cbor(rename = "VDDCR_CPU0")]
34+
#[cbor(rename = "VDDCR_CPU0_A0")]
3335
VddcrCpu0,
34-
#[cbor(rename = "VDDCR_CPU1")]
36+
#[cbor(rename = "VDDCR_CPU1_A0")]
3537
VddcrCpu1,
3638
}
3739

@@ -49,10 +51,9 @@ enum Trace {
4951
Initializing,
5052
Initialized,
5153
LimitsLoaded,
52-
FaultsCleared(Rails),
5354
PmbusAlert {
5455
timestamp: u64,
55-
rails: Rails,
56+
alerted: Vrms,
5657
},
5758
Reading {
5859
timestamp: u64,
@@ -64,6 +65,10 @@ enum Trace {
6465
power_good: bool,
6566
faulted: bool,
6667
},
68+
TriedToClearFaults {
69+
vddcr_cpu0: Status,
70+
vddcr_cpu1: Status,
71+
},
6772
StatusWord(Rail, Result<u16, ResponseCode>),
6873
StatusInput(Rail, Result<u8, ResponseCode>),
6974
StatusVout(Rail, Result<u8, ResponseCode>),
@@ -75,20 +80,89 @@ enum Trace {
7580
}
7681

7782
#[derive(Copy, Clone, PartialEq)]
78-
pub struct Rails {
79-
pub vddcr_cpu0: bool,
80-
pub vddcr_cpu1: bool,
83+
enum Status {
84+
NotFaulted,
85+
// This is on purpose; Humility will display the ringbuf entry as just the
86+
// name of the variant, without the enum name.
87+
#[allow(clippy::enum_variant_names)]
88+
StatusWord(u16),
89+
I2cError(ResponseCode),
90+
}
91+
92+
impl Status {
93+
fn from_result(
94+
result: Result<STATUS_WORD::CommandData, ResponseCode>,
95+
) -> Self {
96+
match result {
97+
Ok(s) => Status::StatusWord(s.0),
98+
Err(e) => Status::I2cError(e),
99+
}
100+
}
101+
102+
fn is_faulted(&self) -> bool {
103+
match self {
104+
Status::NotFaulted => false,
105+
Status::StatusWord(word) => {
106+
// Mask out the "off" bit, as "off" is not a fault.
107+
//
108+
// This is unfortunately the least annoying way to implement
109+
// "test if any bits _other_ than this one are set" in the
110+
// `pmbus` crate's current API...
111+
let off_mask = {
112+
let mut mask = STATUS_WORD::CommandData(0);
113+
mask.set_off(STATUS_WORD::Off::PowerOff);
114+
!mask.0
115+
};
116+
// Any other `STATUS_WORD` bits indicate a fault.
117+
word & off_mask != 0
118+
}
119+
Status::I2cError(_) => true,
120+
}
121+
}
122+
}
123+
124+
#[derive(Copy, Clone, PartialEq)]
125+
pub struct Vrms {
126+
pub pwr_cont1: bool,
127+
pub pwr_cont2: bool,
81128
}
82129

83130
ringbuf!(Trace, 60, Trace::None);
84131

85132
///
86-
/// We are going to set our input undervoltage warn limit to be 11.75 volts.
133+
/// Limit value for input undervoltage *warnings*.
87134
/// Note that we will not fault if VIN goes below this (that is, we will not
88135
/// lose POWER_GOOD), but the part will indicate an input fault and pull
89136
/// on its PMBus alert pin.
90137
///
91-
const VCORE_UV_WARN_LIMIT: units::Volts = units::Volts(11.75);
138+
/// * * *
139+
///
140+
/// Okay, so this number is actually a bit finnicky. Per the RAA22960A
141+
/// datasheet, in the documentation for the `VIN_UV_FAULT_RESPONSE` register,
142+
/// we find the following (emphasis mine):
143+
///
144+
/// > Configures the input undervoltage fault response. For a fault to be
145+
/// > considered cleared, **the input voltage must rise by 1/16th of the UV
146+
/// > fault threshold value.**
147+
/// >
148+
/// > --- R16DS0309EU0100 Rev.1.00 § 11.36, "VIN_UV_FAULT_RESPONSE" (p. 57)
149+
///
150+
/// While the documentation does not explicitly state that this also applies
151+
/// to *warnings* (and there is no corresponding `VIN_UV_WARN_RESPONSE`
152+
/// register, as the response is always just to assert the PMBus alert pin),
153+
/// it stands to reason that warnings would also only clear when voltage
154+
/// rises by 1/16th of the warning threshold value. Empirical testing reveals
155+
/// that this is indeed the case.
156+
///
157+
/// So, the important thing here is that, when selecting a warning threshold,
158+
/// we must ensure that `lim + (1/16 * lim)` is less than the expected nominal
159+
/// input voltage, or else the warning will not clear even if the input voltage
160+
/// returns to nominal.
161+
///
162+
/// For a 12V input, 11V seems like a reasonable undervoltage warning limit.
163+
/// 1/16 * 11V = 0.6875V, so the warning will clear if the input voltage rises
164+
/// by at least 11.6875V.
165+
const VCORE_UV_WARN_LIMIT: units::Volts = units::Volts(11.0);
92166

93167
///
94168
/// We want to collect enough samples (at ~900µs per sample per regulator, or
@@ -108,14 +182,23 @@ impl VCore {
108182

109183
let (device, rail) = i2c_config::pmbus::vddcr_cpu1_a0(i2c);
110184
let vddcr_cpu1 = Raa229620A::new(&device, rail);
185+
111186
Self {
112187
vddcr_cpu0,
113188
vddcr_cpu1,
189+
faulted: Vrms {
190+
pwr_cont1: false,
191+
pwr_cont2: false,
192+
},
114193
packrat,
115194
}
116195
}
117196

118-
pub fn initialize_uv_warning(&self) -> Result<(), ResponseCode> {
197+
pub fn is_still_faulted(&self) -> bool {
198+
self.faulted.pwr_cont1 || self.faulted.pwr_cont2
199+
}
200+
201+
pub fn initialize_uv_warning(&mut self) -> Result<(), ResponseCode> {
119202
ringbuf_entry!(Trace::Initializing);
120203

121204
// Set our warn limit
@@ -128,10 +211,10 @@ impl VCore {
128211
ringbuf_entry!(Trace::LimitsLoaded);
129212

130213
// Clear our faults
131-
self.clear_faults(Rails {
132-
vddcr_cpu0: true,
133-
vddcr_cpu1: true,
134-
})?;
214+
self.try_to_clear_faults(Vrms {
215+
pwr_cont1: true,
216+
pwr_cont2: true,
217+
});
135218

136219
// The higher-level sequencer code will unmask the FPGA interrupts for
137220
// our guys.
@@ -141,52 +224,81 @@ impl VCore {
141224
Ok(())
142225
}
143226

144-
pub fn clear_faults(&self, which_rails: Rails) -> Result<(), ResponseCode> {
145-
if which_rails.vddcr_cpu0 {
146-
retry_i2c_txn(Rail::VddcrCpu0, PmbusCmd::ClearFaults, || {
147-
self.vddcr_cpu0.clear_faults()
148-
})?;
227+
pub fn can_we_unmask_any_vrm_irqs_again(&mut self) -> Vrms {
228+
self.try_to_clear_faults(self.faulted);
229+
Vrms {
230+
pwr_cont1: !self.faulted.pwr_cont1,
231+
pwr_cont2: !self.faulted.pwr_cont2,
149232
}
233+
}
150234

151-
if which_rails.vddcr_cpu1 {
152-
retry_i2c_txn(Rail::VddcrCpu1, PmbusCmd::ClearFaults, || {
153-
self.vddcr_cpu1.clear_faults()
235+
fn try_to_clear_faults(&mut self, which_vrms: Vrms) {
236+
fn clear_faults(
237+
rail: Rail,
238+
dev: &Raa229620A,
239+
) -> Result<STATUS_WORD::CommandData, ResponseCode> {
240+
retry_i2c_txn(rail, PmbusCmd::ClearFaults, || {
241+
dev.clear_faults_on_all_rails()
154242
})?;
243+
Ok(dev.status_word()?)
155244
}
156245

157-
ringbuf_entry!(Trace::FaultsCleared(which_rails));
246+
let vddcr_cpu0 = if which_vrms.pwr_cont1 {
247+
Status::from_result(clear_faults(Rail::VddcrCpu0, &self.vddcr_cpu0))
248+
} else {
249+
Status::NotFaulted
250+
};
158251

159-
Ok(())
252+
let vddcr_cpu1 = if which_vrms.pwr_cont2 {
253+
Status::from_result(clear_faults(Rail::VddcrCpu1, &self.vddcr_cpu1))
254+
} else {
255+
Status::NotFaulted
256+
};
257+
258+
ringbuf_entry!(Trace::TriedToClearFaults {
259+
vddcr_cpu0,
260+
vddcr_cpu1
261+
});
262+
263+
self.faulted.pwr_cont1 = vddcr_cpu0.is_faulted();
264+
self.faulted.pwr_cont2 = vddcr_cpu1.is_faulted();
160265
}
161266

162267
pub fn handle_pmbus_alert(
163-
&self,
164-
mut rails: Rails,
268+
&mut self,
269+
vrms: Vrms,
165270
now: u64,
166271
ereport_buf: &mut [u8],
167272
) {
168273
ringbuf_entry!(Trace::PmbusAlert {
169274
timestamp: now,
170-
rails,
275+
alerted: vrms,
171276
});
172277

173-
let cpu0_state = self.record_pmbus_status(
174-
now,
175-
Rail::VddcrCpu0,
176-
rails.vddcr_cpu0,
177-
ereport_buf,
178-
);
179-
rails.vddcr_cpu0 |= cpu0_state.faulted;
278+
let mut input_fault = false;
279+
if !self.faulted.pwr_cont1 {
280+
let state = self.record_pmbus_status(
281+
now,
282+
Rail::VddcrCpu0,
283+
vrms.pwr_cont1,
284+
ereport_buf,
285+
);
286+
input_fault |= state.input_fault;
287+
self.faulted.pwr_cont1 |= state.faulted;
288+
}
180289

181-
let cpu1_state = self.record_pmbus_status(
182-
now,
183-
Rail::VddcrCpu1,
184-
rails.vddcr_cpu1,
185-
ereport_buf,
186-
);
187-
rails.vddcr_cpu1 |= cpu1_state.faulted;
290+
if !self.faulted.pwr_cont2 {
291+
let state = self.record_pmbus_status(
292+
now,
293+
Rail::VddcrCpu1,
294+
vrms.pwr_cont1,
295+
ereport_buf,
296+
);
297+
input_fault |= state.input_fault;
298+
self.faulted.pwr_cont2 |= state.faulted;
299+
}
188300

189-
if cpu0_state.input_fault || cpu1_state.input_fault {
301+
if input_fault {
190302
for _ in 0..VCORE_NSAMPLES {
191303
let vddcr_cpu0_vin =
192304
self.vddcr_cpu0.read_vin().unwrap_or_else(|e| {
@@ -214,7 +326,7 @@ impl VCore {
214326
//
215327
// Record our readings, along with a timestamp. On the one hand,
216328
// it's a little exceesive to record a timestamp on every
217-
// reading: it's in milliseconds, and because it takes ~900µs
329+
// reading: it's in milliseconds, and because it takes ~900µs``ws
218330
// per reading (so ~1800us to read from both regulators), we
219331
// expect the timestamp to (basically) be incremented by 2 with
220332
// every reading (with a duplicate timestamp occuring every ~7-9
@@ -231,24 +343,6 @@ impl VCore {
231343
});
232344
}
233345
}
234-
235-
// The only way to make the pins deassert (and thus, the IRQ go
236-
// away) is to tell the guys to clear the fault.
237-
// N.B.: unlike other FPGA sequencer alerts, we need not clear the
238-
// IFR bits for these; they are hot as long as the PMALERT pin from
239-
// the RAA229620As is asserted.
240-
//
241-
// Per the RAA229620A datasheet (R16DS0309EU0200 Rev.2.00, page 36),
242-
// clearing the fault in the regulator will deassert PMALERT_L,
243-
// releasing the IRQ, but the fault bits to be reset if the fault
244-
// condition still exists. Note that this does *not* cause the device to
245-
// restart if it has shut down. The behavior of "CLEAR_FAULTS" is really
246-
// much closer to "ACKNOWLEDGE_PMBUS_ALERT", since it doesn't actually
247-
// seem to effect the state of the regulator.
248-
//
249-
// TODO(eliza): we will want to handle a shut down regulator more
250-
// intelligently in future...
251-
let _ = self.clear_faults(rails);
252346
}
253347

254348
fn record_pmbus_status(

drv/cpu-seq-api/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@ pub enum StateChangeReason {
7575
A0Mapo,
7676
/// System Management Error
7777
SmerrAssert,
78+
/// NIC MAPO fault from the sequencer.
79+
NicMapo,
7880
/// The system powered off for reasons we can't explain
7981
Unknown,
8082
}

drv/gimlet-seq-server/src/main.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ enum I2cTxn {
6565
VCoreOff,
6666
VCoreUndervoltageInitialize,
6767
VCorePmbusStatus,
68+
VCoreClearFaults,
6869
SocOn,
6970
SocOff,
7071
}
@@ -727,6 +728,10 @@ impl<S: SpiServer> NotificationHandler for ServerImpl<S> {
727728
}
728729
}
729730

731+
if self.vcore.is_faulted() {
732+
self.vcore.try_to_clear_faults();
733+
}
734+
730735
if let Some(interval) = self.poll_interval() {
731736
self.deadline += interval;
732737
sys_set_timer(Some(self.deadline), notifications::TIMER_MASK);
@@ -1163,10 +1168,14 @@ impl<S: SpiServer> ServerImpl<S> {
11631168
// for a thermtrip or for someone disabling NIC_PWREN_L. If we are in
11641169
// any other state, we don't need to poll.
11651170
//
1171+
// If the Vcore VRM has a PMBus alert, we must try to clear its faults
1172+
// periodically, regardless of the current power state.
1173+
//
11661174
fn poll_interval(&self) -> Option<u64> {
11671175
match self.state {
11681176
PowerState::A0 => Some(10),
11691177
PowerState::A0PlusHP => Some(100),
1178+
_ if self.vcore.is_faulted() => Some(100),
11701179
_ => None,
11711180
}
11721181
}

0 commit comments

Comments
 (0)