clcass
This commit is contained in:
1 parent
9dba818a12
commit
6a0e3f0070
1 file changed
+178
-97
+178
-97
@@ -1,9 +1,17 @@
|
||||
//! Kettle controller: press the switch to boil to a setpoint, then brew-timer.
|
||||
//! Kettle controller (finite state machine): press the switch to boil to a
|
||||
//! setpoint, then run a brew timer.
|
||||
//!
|
||||
//! The button behaves like a kettle's on-switch: pressing it engages the heater
|
||||
//! (the state latches in firmware, like the switch staying down), and the
|
||||
//! heater runs until the water reaches the setpoint, at which point it switches
|
||||
//! off ("pops up") and a brew timer starts.
|
||||
//! States: Idle → Heating → Brewing → Idle, plus Fault (sensor disconnected).
|
||||
//! * a press while below setpoint: Idle/Brewing → Heating (heater on)
|
||||
//! * reaching the setpoint: Heating → Brewing (heater off, timer)
|
||||
//! * timer expiry: Brewing → Idle
|
||||
//! * raw ADC == 0 (open thermistor): any → Fault (heater off)
|
||||
//!
|
||||
//! The `Kettle` struct owns the FSM state and the actuators (heater + LEDs);
|
||||
//! `main` owns the sensor/button/timer harness and drives `Kettle`'s methods.
|
||||
//!
|
||||
//! The button behaves like a kettle's on-switch (latches the heater on until
|
||||
//! the setpoint is reached). It is polled every POLL_MS so a quick tap registers.
|
||||
//!
|
||||
//! Sensor — NTC divider on the 3V rail (NOT 5V; ADC ref is VDDA ~= 3.0 V):
|
||||
//!
|
||||
@@ -12,9 +20,6 @@
|
||||
//! └── 100nF ──── GND (optional noise filter)
|
||||
//! R_ntc = R_FIXED * (ADC_MAX - raw) / raw (ratiometric), then Beta -> degC
|
||||
//!
|
||||
//! On-switch — the board's B1 USER button (PA0, active-high). It is polled
|
||||
//! every POLL_MS (not just once per control tick), so a quick tap registers.
|
||||
//!
|
||||
//! Outputs (LEDs via their own ~220-330R resistor, anode to pin, cathode GND):
|
||||
//!
|
||||
//! PC5 (P1 pin 19) HEATER -> relay/SSR (high = heat on)
|
||||
@@ -37,6 +42,10 @@ use f411::{
|
||||
config::{AdcConfig, SampleTime},
|
||||
Adc,
|
||||
},
|
||||
gpio::{
|
||||
gpioc::{PC2, PC4, PC5},
|
||||
Output, PushPull,
|
||||
},
|
||||
pac,
|
||||
prelude::*,
|
||||
rcc::Config,
|
||||
@@ -58,13 +67,145 @@ const ADC_MAX: f32 = 4095.0;
|
||||
const SETPOINT_C: f32 = 25.0;
|
||||
|
||||
// ---- Timing ----
|
||||
// Control cadence: temperature is read and the timer advances once per tick.
|
||||
const TICK_MS: u32 = 1000;
|
||||
// Button poll interval within a tick — short enough to catch a normal tap.
|
||||
const POLL_MS: u32 = 20;
|
||||
// brew duration, in ticks (= seconds at TICK_MS = 1000). Short for the bench;
|
||||
// set to your brew/steep time (e.g. 180 for 3 minutes).
|
||||
const BREW_TICKS: u32 = 10;
|
||||
const TICK_MS: u32 = 1000; // control cadence (temperature read, timer advance)
|
||||
const POLL_MS: u32 = 20; // button poll interval within a tick
|
||||
const BREW_TICKS: u32 = 10; // brew duration in ticks (= seconds); set to taste
|
||||
|
||||
/// Controller state.
|
||||
#[derive(Clone, Copy, PartialEq, defmt::Format)]
|
||||
enum State {
|
||||
/// Off, waiting for a press.
|
||||
Idle,
|
||||
/// Heater engaged, climbing toward the setpoint.
|
||||
Heating,
|
||||
/// Setpoint reached; heater off, brew timer counting down (ticks left).
|
||||
Brewing { remaining: u32 },
|
||||
/// Thermistor reads open/short; heater forced off until it recovers.
|
||||
Fault,
|
||||
}
|
||||
|
||||
/// The kettle FSM plus the actuators it drives.
|
||||
struct Kttl {
|
||||
state: State,
|
||||
heater: PC5<Output<PushPull>>,
|
||||
led_heating: PC2<Output<PushPull>>, // red: heater on
|
||||
led_at_temp: PC4<Output<PushPull>>, // blue: at/above setpoint
|
||||
compass: LedCompass, // on-board LEDs; north = brew timer
|
||||
prev_pressed: bool, // for press edge detection
|
||||
}
|
||||
|
||||
impl Kttl {
|
||||
fn new(
|
||||
heater: PC5<Output<PushPull>>,
|
||||
led_heating: PC2<Output<PushPull>>,
|
||||
led_at_temp: PC4<Output<PushPull>>,
|
||||
mut compass: LedCompass,
|
||||
pressed: bool,
|
||||
) -> Self {
|
||||
// Only the north LED is used; park the rest off.
|
||||
compass.e.off();
|
||||
compass.s.off();
|
||||
compass.w.off();
|
||||
let mut kettle = Self {
|
||||
state: State::Idle,
|
||||
heater,
|
||||
led_heating,
|
||||
led_at_temp,
|
||||
compass,
|
||||
prev_pressed: pressed,
|
||||
};
|
||||
kettle.apply_outputs(0.0);
|
||||
kettle
|
||||
}
|
||||
|
||||
fn state(&self) -> State {
|
||||
self.state
|
||||
}
|
||||
|
||||
/// Per-tick, temperature/timer-driven transitions.
|
||||
fn tick(&mut self, temp_c: f32) {
|
||||
self.state = match self.state {
|
||||
State::Fault => {
|
||||
defmt::info!("thermistor reconnected");
|
||||
State::Idle
|
||||
}
|
||||
State::Heating if temp_c >= SETPOINT_C => {
|
||||
defmt::info!("setpoint reached — switched OFF, brew {} s", BREW_TICKS);
|
||||
State::Brewing { remaining: BREW_TICKS }
|
||||
}
|
||||
State::Brewing { remaining } => {
|
||||
let r = remaining - 1;
|
||||
if r == 0 {
|
||||
defmt::info!("brew COMPLETE");
|
||||
State::Idle
|
||||
} else {
|
||||
State::Brewing { remaining: r }
|
||||
}
|
||||
}
|
||||
other => other, // Idle, or Heating still below setpoint
|
||||
};
|
||||
self.apply_outputs(temp_c);
|
||||
}
|
||||
|
||||
/// Open/short thermistor: force the Fault state and the heater off.
|
||||
fn fault(&mut self) {
|
||||
if self.state != State::Fault {
|
||||
defmt::warn!("thermistor disconnected — heater forced OFF");
|
||||
}
|
||||
self.state = State::Fault;
|
||||
self.apply_outputs(0.0);
|
||||
}
|
||||
|
||||
/// Handle the on-switch level. A press edge engages the heater, but only
|
||||
/// from Idle/Brewing and only while below the setpoint.
|
||||
fn press(&mut self, pressed: bool, temp_c: f32) {
|
||||
if pressed && !self.prev_pressed {
|
||||
match self.state {
|
||||
State::Idle | State::Brewing { .. } if temp_c < SETPOINT_C => {
|
||||
self.state = State::Heating;
|
||||
self.apply_outputs(temp_c);
|
||||
defmt::info!("switched ON");
|
||||
}
|
||||
State::Idle | State::Brewing { .. } => {
|
||||
defmt::info!("press ignored — already at/above setpoint");
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
self.prev_pressed = pressed;
|
||||
}
|
||||
|
||||
/// Drive all outputs from the current state.
|
||||
fn apply_outputs(&mut self, temp_c: f32) {
|
||||
match self.state {
|
||||
State::Heating => {
|
||||
self.heater.set_high();
|
||||
self.led_heating.set_high();
|
||||
self.led_at_temp.set_state((temp_c >= SETPOINT_C).into());
|
||||
self.compass.n.off();
|
||||
}
|
||||
State::Brewing { .. } => {
|
||||
self.heater.set_low();
|
||||
self.led_heating.set_low();
|
||||
self.led_at_temp.set_state((temp_c >= SETPOINT_C).into());
|
||||
self.compass.n.on();
|
||||
}
|
||||
State::Idle => {
|
||||
self.heater.set_low();
|
||||
self.led_heating.set_low();
|
||||
self.led_at_temp.set_state((temp_c >= SETPOINT_C).into());
|
||||
self.compass.n.off();
|
||||
}
|
||||
State::Fault => {
|
||||
// Temperature is invalid in Fault — drive everything off.
|
||||
self.heater.set_low();
|
||||
self.led_heating.set_low();
|
||||
self.led_at_temp.set_low();
|
||||
self.compass.n.off();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[entry]
|
||||
fn main() -> ! {
|
||||
@@ -80,44 +221,32 @@ fn main() -> ! {
|
||||
|
||||
let gpioc = p.GPIOC.split(&mut rcc);
|
||||
let therm = gpioc.pc1.into_analog();
|
||||
let mut heater = gpioc.pc5.into_push_pull_output(); // actuator (relay/SSR later)
|
||||
let mut led_heating = gpioc.pc2.into_push_pull_output(); // red: heater on
|
||||
let mut led_at_temp = gpioc.pc4.into_push_pull_output(); // blue: at/above setpoint
|
||||
|
||||
// On-board user LEDs (PD12-15) used as the brew-timer indicator; only the
|
||||
// north LED (LD3, orange) is driven.
|
||||
let mut compass = LedCompass::new(p.GPIOD.split(&mut rcc));
|
||||
|
||||
heater.set_low();
|
||||
led_heating.set_low();
|
||||
led_at_temp.set_low();
|
||||
compass.n.off();
|
||||
compass.e.off();
|
||||
compass.s.off();
|
||||
compass.w.off();
|
||||
let heater = gpioc.pc5.into_push_pull_output();
|
||||
let led_heating = gpioc.pc2.into_push_pull_output();
|
||||
let led_at_temp = gpioc.pc4.into_push_pull_output();
|
||||
let compass = LedCompass::new(p.GPIOD.split(&mut rcc));
|
||||
|
||||
let mut adc = Adc::new(p.ADC1, true, AdcConfig::default(), &mut rcc);
|
||||
let mut delay = cp.SYST.delay(&rcc.clocks);
|
||||
|
||||
defmt::info!("kettle: setpoint {} C, brew {} s", SETPOINT_C, BREW_TICKS);
|
||||
|
||||
// ---- state (carried across ticks) ----
|
||||
let mut heating = false; // kettle switched on, heating toward setpoint
|
||||
let mut countdown: Option<u32> = None; // brew-timer ticks remaining
|
||||
let mut prev_pressed = button.is_high(); // for press edge detection
|
||||
let mut kettle = Kttl::new(heater, led_heating, led_at_temp, compass, button.is_high());
|
||||
|
||||
loop {
|
||||
let raw = adc.convert(&therm, SampleTime::Cycles_480);
|
||||
|
||||
// Fail-safe: a disconnected thermistor reads raw 0; force the heater off
|
||||
// rather than risk boiling dry, and freeze the rest of the state.
|
||||
// ---- Fault path: open/short thermistor reads raw 0 ----
|
||||
if raw == 0 {
|
||||
heating = false;
|
||||
heater.set_low();
|
||||
led_heating.set_low();
|
||||
defmt::warn!("raw=0 — thermistor disconnected? heater forced OFF");
|
||||
prev_pressed = button.is_high();
|
||||
delay.delay_ms(TICK_MS);
|
||||
kettle.fault();
|
||||
// Wait a tick; keep tracking the button so we don't fire a phantom
|
||||
// press edge on recovery (temp is irrelevant while faulted).
|
||||
let mut waited = 0;
|
||||
while waited < TICK_MS {
|
||||
delay.delay_ms(POLL_MS);
|
||||
waited += POLL_MS;
|
||||
kettle.press(button.is_high(), 0.0);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -126,73 +255,25 @@ fn main() -> ! {
|
||||
let inv_t = 1.0 / T0 + (1.0 / BETA) * libm::logf(r_ntc / R0);
|
||||
let temp_c = 1.0 / inv_t - 273.15;
|
||||
|
||||
// Reaching the setpoint switches the kettle off ("pops up") and starts
|
||||
// the brew timer.
|
||||
if heating && temp_c >= SETPOINT_C {
|
||||
heating = false;
|
||||
countdown = Some(BREW_TICKS);
|
||||
defmt::info!("setpoint reached — switched OFF, brew {} s", BREW_TICKS);
|
||||
}
|
||||
kettle.tick(temp_c);
|
||||
|
||||
let heat_on = heating;
|
||||
heater.set_state(heat_on.into());
|
||||
led_heating.set_state(heat_on.into());
|
||||
led_at_temp.set_state((temp_c >= SETPOINT_C).into());
|
||||
|
||||
// Advance the brew timer.
|
||||
if let Some(n) = countdown {
|
||||
let n = n - 1;
|
||||
if n == 0 {
|
||||
countdown = None;
|
||||
defmt::info!("brew COMPLETE");
|
||||
} else {
|
||||
countdown = Some(n);
|
||||
defmt::info!("brew: {} s left", n);
|
||||
}
|
||||
}
|
||||
|
||||
// brew-timer indicator: north LED (orange) lit while counting.
|
||||
if countdown.is_some() {
|
||||
compass.n.on();
|
||||
} else {
|
||||
compass.n.off();
|
||||
}
|
||||
|
||||
// defmt has no float-precision hint, so render the temperature to one
|
||||
// decimal by hand via deci-degrees; heater state is shown as a word.
|
||||
// Status line. defmt has no float-precision hint, so render the
|
||||
// temperature to one decimal by hand via deci-degrees.
|
||||
let t_dc = libm::roundf(temp_c * 10.0) as i32; // tenths of a degree C
|
||||
defmt::info!(
|
||||
"T={=i32}.{=u32} C set={} C heater {}",
|
||||
"T={=i32}.{=u32} C set={} C {}",
|
||||
t_dc / 10,
|
||||
(t_dc % 10).unsigned_abs(),
|
||||
SETPOINT_C,
|
||||
if heat_on { "ON" } else { "off" },
|
||||
kettle.state()
|
||||
);
|
||||
|
||||
// Wait one control tick, polling the on-switch frequently so a quick tap
|
||||
// isn't missed. A press engages the heater immediately and cancels any
|
||||
// running brew timer — like pushing the kettle switch back down.
|
||||
// ---- wait one tick, polling the on-switch for a press event ----
|
||||
let mut waited = 0;
|
||||
while waited < TICK_MS {
|
||||
delay.delay_ms(POLL_MS);
|
||||
waited += POLL_MS;
|
||||
|
||||
let pressed = button.is_high();
|
||||
if pressed && !prev_pressed {
|
||||
// Engage only if there is actually heating to do. Uses the most
|
||||
// recent temperature reading (from the top of this tick).
|
||||
if temp_c < SETPOINT_C {
|
||||
heating = true;
|
||||
countdown = None;
|
||||
heater.set_high();
|
||||
led_heating.set_high();
|
||||
compass.n.off();
|
||||
defmt::info!("switched ON");
|
||||
} else {
|
||||
defmt::info!("press ignored — already at/above setpoint");
|
||||
}
|
||||
}
|
||||
prev_pressed = pressed;
|
||||
kettle.press(button.is_high(), temp_c);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user