Firmware raises the CPU clock from 96 to 192 MHz to finish a burst of work, the voltage is still settling at its lower level, and a timing path that closed at 0.9 V fails at the sagging rail. The chip crashes exactly when it tries to go faster. The ordering rule exists because a frequency is only safe at the voltage that closes its timing, and this lesson makes the rule structural so no driver can invert it.
Going up, the voltage rises first and the frequency follows only after the rail settles inside tolerance; going down, the frequency drops first and the voltage follows. The reason is timing closure: each operating point is characterised at its voltage, and running a higher frequency at a lower voltage violates the signoff the STA lessons closed. The controller below enforces the order as a request-acknowledge exchange with the regulator and the clock generator: it never asserts the frequency step until the voltage-good returns, and never asserts the voltage step-down until the frequency step completes. The wait states are the same species as the sequencer waits, with the same timeout treatment. One thing keeps the order honest across transitions: the controller remembers which operating point it is at (the at_high register), set when a climb finishes and cleared when a descent finishes. Without that memory the outputs fall back to a default when the machine returns to idle, and the voltage can drop while the frequency stays high.
// NanoSoC teaching values: DVFS order controller with its own checker and
// self-checking testbench, one complete unit. Two gears: HIGH (the NanoSoC
// point) and LOW. Up: raise V, wait V_GOOD, then raise f. Down: lower f, wait
// F_DONE, then lower V. The regulator and clock interfaces are illustrative.
module nano_dvfs_ctrl (
input logic clk_per,
input logic rst_per_n,
input logic up_req,
input logic dn_req,
input logic v_good,
input logic f_done,
output logic v_high_req,
output logic f_high_req,
output logic busy
);
typedef enum logic [2:0] {
S_IDLE, S_V_UP, S_F_UP, S_F_DN, S_V_DN
} state_t;
state_t state, nxt;
logic at_high; // memory of the current operating point (reset: HIGH)
always_ff @(posedge clk_per or negedge rst_per_n) begin
if (!rst_per_n) begin
state <= S_IDLE;
at_high <= 1'b1;
end else begin
state <= nxt;
if (state == S_F_UP && f_done) at_high <= 1'b1; // arrived at HIGH
if (state == S_V_DN && v_good) at_high <= 1'b0; // arrived at LOW
end
end
always_comb begin
nxt = state;
case (state)
S_IDLE: begin
if (up_req && !at_high) nxt = S_V_UP;
else if (dn_req && at_high) nxt = S_F_DN;
end
S_V_UP: if (v_good) nxt = S_F_UP; // voltage first, then frequency
S_F_UP: if (f_done) nxt = S_IDLE;
S_F_DN: if (f_done) nxt = S_V_DN; // frequency first, then voltage
S_V_DN: if (v_good) nxt = S_IDLE;
default: nxt = S_IDLE;
endcase
end
// Voltage is high at HIGH and through the whole climb and the first part of
// the descent; it falls only in S_V_DN, after the frequency has fallen.
assign v_high_req = (at_high && state != S_V_DN) ||
(state == S_V_UP) || (state == S_F_UP);
// Frequency is high only at HIGH (not while falling) or in the final climb step.
assign f_high_req = (at_high && state != S_F_DN && state != S_V_DN) ||
(state == S_F_UP);
assign busy = (state != S_IDLE);
endmodule
// The ordering rule as a checker: a high-frequency request without a high-voltage
// request, or a high actual frequency on a low actual voltage, latches an error.
module dvfs_order_chk (
input logic clk,
input logic v_req, f_req, v_act, f_act,
output logic err
);
initial err = 1'b0;
always @(posedge clk)
if ((f_req && !v_req) || (f_act && !v_act)) err <= 1'b1;
endmodule
The transition sequence is drawn as voltage-step then frequency-step going up and the mirror going down, with the settle wait dimensioned.
The frequency step is gated on a voltage-good signal from the regulator domain, synchronised, never on a fixed delay alone.
No software path can request a frequency the current voltage does not support; the request interface offers operating points, not raw dividers.
The testbench above runs both transitions against a model of the regulator and the clock generator and checks one assertion on every clock: the frequency request is never high while the voltage request is low, and the actual frequency is never high on a low actual voltage. The checker is itself tested: a second instance is fed the inverted case and must latch its error, so the check cannot pass by being blind. Extend it with the refused cases: a frequency request while voltage-good is low must stall, a down-request at the low point must be ignored, and a down-request must complete the frequency drop before the voltage moves. If the chip crashes only when going faster, capture the rail during the transition: a spike-free but slow regulator means the settle wait is shorter than the regulator's actual settling time, and the fix is the wait, not the regulator.
The voltage gate: the frequency rises at the old voltage with no settle wait, which is exactly the crash case. Raw divider access must be locked or removed; operating-point requests through the controller are the only path.