From c24133cbab80110e1aebedb37826fb04d2689aa8 Mon Sep 17 00:00:00 2001 From: Steins7 Date: Mon, 20 Dec 2021 12:42:00 +0000 Subject: [PATCH] Resolve "Implement temperature computations" --- Cargo.toml | 1 + docs/Graphs.ods | Bin 26845 -> 30414 bytes src/config.rs | 44 +++++++++++++++++ src/lcd_gui.rs | 92 ++++++++++++++++++++++++++++------- src/main.rs | 73 ++++++++++++++++++++-------- src/state.rs | 127 ++++++++++++++++++++++++++++++++++++++++++++++++ src/utils.rs | 116 +++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 415 insertions(+), 38 deletions(-) create mode 100644 src/config.rs create mode 100644 src/state.rs create mode 100644 src/utils.rs diff --git a/Cargo.toml b/Cargo.toml index 2619f66..14ffaf2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ cortex-m-rt = "0.6.10" cortex-m-semihosting = "0.3.3" panic-halt = "0.2.0" hd44780-driver = "0.3.0" +libm = "0.2.1" [dependencies.stm32f1xx-hal] version = "0.7.0" diff --git a/docs/Graphs.ods b/docs/Graphs.ods index a6a7b2b52fc4d9fc5d5c0a8d6fe07a3e29550b1f..f1eb706f00cfd7d8e497a93180408405f1ae033e 100644 GIT binary patch delta 26743 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 DSZk_F delta 23897 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 O9G>UHiK}LLmHR&nDKt<3 diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..a433624 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,44 @@ +pub enum ConfigError { + LoadError, +} + +pub enum FanOutput { + P1, + P2, +} + +pub enum FanState { + ON, + OFF, +} + +pub struct SystemConfig { + pub max_temp_diff: f32, + pub min_temp_diff: f32, + pub ext_offset: f32, + pub p1_offset: f32, + pub p2_offset: f32, + pub fan_output: FanOutput, +} + +impl SystemConfig { + + pub fn new() -> SystemConfig { + SystemConfig { + max_temp_diff: 8.0, + min_temp_diff: 4.0, + ext_offset: 0.0, + p1_offset: 0.0, + p2_offset: 0.0, + fan_output: FanOutput::P1, + } + } + + pub fn load() -> Result { + unimplemented!(); + } + + pub fn store() -> SystemConfig { + unimplemented!(); + } +} diff --git a/src/lcd_gui.rs b/src/lcd_gui.rs index b185180..355def2 100644 --- a/src/lcd_gui.rs +++ b/src/lcd_gui.rs @@ -1,4 +1,7 @@ -use core::{sync::atomic::{AtomicBool, Ordering}}; +use core::{ + sync::atomic::{AtomicBool, Ordering}, + fmt, +}; use embedded_hal::{ //digital::v1_compat::OldOutputPin, @@ -13,6 +16,33 @@ use hd44780_driver::{ bus::DataBus, }; +use crate::{ + config::SystemConfig, + state::SystemState, +}; + +//---Temp Enum-------------------------------------------------------------------------------------- +/// A simple enum to handle displaying temps on the GUI. Temperatures can be valid or not. An +/// invalid temperature is displayed as "inval°C" whereas a valid temperature is displayed as +/// XX.XX°C +pub enum Temp { + Valid (f32), + Invalid, +} + +impl fmt::Display for Temp { + fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + match self { + Temp::Valid (deg) => formatter.write_fmt(format_args!("{:.2}", deg)), + Temp::Invalid => formatter.write_str("inval"), + } + } +} + +//---LCDGui Struct---------------------------------------------------------------------------------- +/// Manages the lcd screen and inputs (encoder + button) and display relevant information. Can also +/// be used to configure the system. The update() function should be called frequently to refresh +/// the GUI (every 0.2s is enough) pub struct LCDGui where L: LCDScreen, @@ -24,6 +54,8 @@ where button: &'static AtomicBool, backlight: Option, count: u16, + ext_deg: Temp, + p1_deg: Temp, } impl LCDGui @@ -38,7 +70,15 @@ where use hd44780_driver::{Cursor, CursorBlink, Display}; let count = qei.count(); - let mut gui = LCDGui {lcd, qei, button, backlight, count}; + let mut gui = LCDGui { + lcd, + qei, + button, + backlight, + count, + ext_deg: Temp::Invalid, + p1_deg: Temp::Invalid, + }; if let Some(bl) = &mut gui.backlight { let _ = bl.set_high(); }; @@ -56,24 +96,40 @@ where gui } - pub fn run(&mut self) -> ! { - loop { - //TODO deduplicate button detection - if self.button.swap(false, Ordering::AcqRel) { - self.lcd.write_str("paf").unwrap(); - } + pub fn update(&mut self, state: &mut SystemState) { - if self.count != self.qei.count() { - self.count = self.qei.count(); - self.lcd.set_cursor_pos(0x40); - self.lcd.write_str(" ").unwrap(); - self.lcd.set_cursor_pos(0x40); - write!(self.lcd, "{}", self.qei.count()/2).unwrap(); - } - - // put device in sleep mode until next interrupt (button or timer) - cortex_m::asm::wfi(); + self.ext_deg = match state.ext_temp() { + Some(deg) => Temp::Valid(deg), + None => Temp::Invalid, + }; + + self.p1_deg = match state.p1_temp() { + Some(deg) => Temp::Valid(deg), + None => Temp::Invalid, + }; + + //TODO deduplicate button detection + if self.button.swap(false, Ordering::AcqRel) { + self.lcd.write_str("paf").unwrap(); } + + if self.count != self.qei.count() { + self.count = self.qei.count(); + self.lcd.set_cursor_pos(0x40); + self.lcd.write_str(" ").unwrap(); + self.lcd.set_cursor_pos(0x40); + write!(self.lcd, "{}", self.qei.count()/2).unwrap(); + } + + self.lcd.set_cursor_pos(0); + self.lcd.write_str(" ").unwrap(); + self.lcd.set_cursor_pos(0); + let _ = write!(self.lcd, "{}\u{00df}C", self.ext_deg); + + self.lcd.set_cursor_pos(0x40); + self.lcd.write_str(" ").unwrap(); + self.lcd.set_cursor_pos(0x40); + let _ = write!(self.lcd, "{}\u{00df}C", self.p1_deg); } } diff --git a/src/main.rs b/src/main.rs index fab3563..b788158 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,10 +1,6 @@ #![no_std] #![no_main] -mod lcd_gui; - -use lcd_gui::LCDGui; - extern crate panic_halt; use core::{ @@ -22,6 +18,7 @@ use embedded_hal::digital::{ use stm32f1xx_hal::{ gpio::*, + adc::{Adc, SampleTime}, pac, pac::{interrupt, Interrupt, EXTI}, prelude::*, @@ -30,13 +27,27 @@ use stm32f1xx_hal::{ rtc::Rtc, }; +mod lcd_gui; +use lcd_gui::LCDGui; + +mod utils; +use utils::{ + TemperatureProbe, +}; + +mod config; +use config::SystemConfig; + +mod state; +use state::SystemState; + //-------------------------------------------------------------------------------------------------- /* system config */ const GUI_TICK_SEC: f32 = 0.2; const HEARTBEAT_SEC: f32 = 1.0; -const TEMPS_TICK_SEC: u32 = 5; +const TEMPS_TICK_SEC: u32 = 2; //-------------------------------------------------------------------------------------------------- /* interrupt variables */ @@ -53,10 +64,9 @@ static BUTTON_PIN: Mutex>>>> static BUTTON_FLAG: AtomicBool = AtomicBool::new(false); // temps interrupt -static RED_LED: Mutex>>>> - = Mutex::new(RefCell::new(None)); static RTC: Mutex>> = Mutex::new(RefCell::new(None)); static G_EXTI: Mutex>> = Mutex::new(RefCell::new(None)); +static TEMP_FLAG: AtomicBool = AtomicBool::new(false); //-------------------------------------------------------------------------------------------------- /* interrupt service routines */ @@ -101,9 +111,7 @@ fn RTCALARM() { rtc.as_mut().unwrap().set_alarm(time + TEMPS_TICK_SEC); //also clears the flag }); - cortex_m::interrupt::free(|cs| { - let _ = RED_LED.borrow(cs).borrow_mut().as_mut().unwrap().toggle(); - }); + TEMP_FLAG.store(true, Ordering::Release); } //-------------------------------------------------------------------------------------------------- @@ -146,14 +154,6 @@ fn main() -> ! { TICK_TIMER.borrow(cs).borrow_mut().replace(timer) }); - // setup LED - let mut red_led = gpiob.pb11.into_push_pull_output(&mut gpiob.crh); - let _ = red_led.set_high(); - - cortex_m::interrupt::free(|cs| { - RED_LED.borrow(cs).borrow_mut().replace(red_led) - }); - // set EXTI 17 (see note in 18.4.2, in short : needed for rtc ISR to trigger) dp.EXTI.ftsr.write(|w| w.tr17().set_bit()); dp.EXTI.imr.write(|w| w.mr17().set_bit()); @@ -167,7 +167,7 @@ fn main() -> ! { cortex_m::interrupt::free(|cs| { RTC.borrow(cs).borrow_mut().replace(rtc) }); - + let mut afio = dp.AFIO.constrain(&mut rcc.apb2); // Setup display @@ -255,8 +255,41 @@ fn main() -> ! { //dp.PWR.cr.write(|w| w.pdds().stop_mode()); //dp.PWR.cr.write(|w| w.lpds().set_bit()); cp.SCB.set_sleepdeep(); + + // setup adc + let mut adc = Adc::adc1(dp.ADC1, &mut rcc.apb2, clocks); + adc.set_sample_time(SampleTime::T_71); + let adc = RefCell::new(adc); + let ext1 = gpioa.pa4.into_analog(&mut gpioa.crl); + let ext2 = gpioa.pa5.into_analog(&mut gpioa.crl); + let p1_1 = gpioa.pa2.into_analog(&mut gpioa.crl); + let p1_2 = gpioa.pa3.into_analog(&mut gpioa.crl); + let mut ext_probe = TemperatureProbe::new(&adc, ext1, ext2).unwrap(); + let mut p1_probe = TemperatureProbe::new(&adc, p1_1, p1_2).unwrap(); + + // initialize system state + let config = SystemConfig::new(); + let mut state = SystemState::new(config, gpiob.pb12.into_push_pull_output(&mut gpiob.crh)); + /* run */ - gui.run(); + let _ = TEMP_FLAG.swap(true, Ordering::Release); + + loop { + + //compute temps + if TEMP_FLAG.swap(false, Ordering::AcqRel) { + let ext_temp = ext_probe.read().ok(); + let p1_temp = p1_probe.read().ok(); + + state.update(ext_temp, p1_temp, None); + } + + gui.update(&mut state); + + // put device in sleep mode until next interrupt (button or timer) + cortex_m::asm::wfi(); + } } + diff --git a/src/state.rs b/src/state.rs new file mode 100644 index 0000000..b892465 --- /dev/null +++ b/src/state.rs @@ -0,0 +1,127 @@ + +use embedded_hal::digital::v2::OutputPin; + +use crate::config::{ + SystemConfig, + FanOutput, +}; + +pub enum FanState { + ON, + OFF, +} + +pub struct Fan { + state: FanState, + pin: O, +} + +impl Fan { + + pub fn new(mut pin: O) -> Fan { + + // erroring here means the pin is wrongly configured, there is nothing else to do... + pin.set_low().map_err(|_| "fan pin configuration").unwrap(); + Fan { + state: FanState::OFF, + pin, + } + } + + pub fn on(&mut self) { + self.pin.set_high().map_err(|_| "fan pin configuration").unwrap(); + self.state = FanState::ON; + } + + pub fn off(&mut self) { + self.pin.set_low().map_err(|_| "fan pin configuration").unwrap(); + self.state = FanState::OFF; + } + + pub fn state(&self) -> &FanState { &self.state } +} + +pub struct SystemState { + config: SystemConfig, + ext_temp: Option, + p1_temp: Option, + p2_temp: Option, + fan: Fan, +} + +impl SystemState { + + pub fn new(config: SystemConfig, fan_control_pin: O) -> SystemState { + + SystemState { + config, + ext_temp: None, + p1_temp: None, + p2_temp: None, + fan: Fan::new(fan_control_pin), + } + } + + pub fn config(&self) -> &SystemConfig { &self.config } + pub fn ext_temp(&self) -> Option { self.ext_temp } + pub fn p1_temp(&self) -> Option { self.p1_temp } + pub fn p2_temp(&self) -> Option { self.p2_temp } + + pub fn update_config(&mut self, config: SystemConfig) { + + // remove offsets + let ext_temp = self.ext_temp.map(|temp| temp - self.config.ext_offset); + let p1_temp = self.p1_temp.map(|temp| temp - self.config.p1_offset); + let p2_temp = self.p2_temp.map(|temp| temp - self.config.p2_offset); + + // replace config + self.config = config; + + // reset fan state before recomputing fan state + self.fan.off(); + + // recompute offsets and fan state + self.update(ext_temp, p1_temp, p2_temp); + } + + pub fn update(&mut self, ext_temp: Option, p1_temp: Option, p2_temp: Option) { + + // apply offsets + self.ext_temp = ext_temp.map(|temp| temp + self.config.ext_offset); + self.p1_temp = p1_temp.map(|temp| temp + self.config.p1_offset); + self.p2_temp = p2_temp.map(|temp| temp + self.config.p2_offset); + + // select right probe and check if data is available + let p = match match self.config.fan_output { + FanOutput::P1 => self.p1_temp, + FanOutput::P2 => self.p2_temp, + } { + Some(temp) => temp, + None => { + self.fan.off(); + return; + }, + }; + let ext = match self.ext_temp { + Some(temp) => temp, + None => { + self.fan.off(); + return; + }, + }; + + // compute fan state + match self.fan.state() { + FanState::ON => { + if (p - ext) < self.config.min_temp_diff { + self.fan.off(); + } + }, + FanState::OFF => { + if (p - ext) > self.config.max_temp_diff { + self.fan.on(); + } + }, + } + } +} diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..1c396f5 --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,116 @@ +use core::{ + marker::PhantomData, + cell::RefCell, +}; + +use embedded_hal::adc::*; + +use libm::*; + +//-------------------------------------------------------------------------------------------------- +/* 2nd order linearisation factor for the temperature probe */ +const A: f32 = -0.00058; +const B: f32 = 0.0677; +const C: f32 = -0.07; + +//-------------------------------------------------------------------------------------------------- +/* error management */ +#[derive(Debug)] +pub enum ProbeError { + ReadError, + Initializing, +} + +//-------------------------------------------------------------------------------------------------- +/* TemperatureProbe */ +const FILTER_TIME_CONSTANT: f32 = 100.0; + +pub struct TemperatureProbe<'a, ADC, O, PINP, PINN> +where + PINP: Channel, + PINN: Channel, + O: OneShot + OneShot, +{ + adc: &'a RefCell, + pos_pin: PINP, + neg_pin: PINN, + phantom: PhantomData, + + filtered_temp: f32, + stabilized: bool, +} + +fn read_temp<'a, ADC, O, PINP, PINN>(adc: &'a RefCell, pos: &mut PINP, neg: &mut PINN) + -> Result +where + PINP: Channel, + PINN: Channel, + O: OneShot + OneShot, +{ + //first read is bugged, may be due to fake chip + let _ = adc.borrow_mut().read(pos); + let pos = adc.borrow_mut().read(pos) + .map_err(|_| ProbeError::ReadError)? as f32; + + //first read is bugged, may be due to fake chip + let _ = adc.borrow_mut().read(neg); + let neg = adc.borrow_mut().read(neg) + .map_err(|_| ProbeError::ReadError)? as f32; + + // compute back voltage, then reverse linearisation to compute temperature + let volt = (pos - neg) * 3.3 / 4095.0; + Ok((-B + sqrtf(B*B - 4.0*(C - volt)*A)) / (2.0*A)) +} + +impl<'a, ADC, O, PINP, PINN> TemperatureProbe<'a, ADC, O, PINP, PINN> +where + PINP: Channel, + PINN: Channel, + O: OneShot + OneShot, +{ + pub fn new(adc: &'a RefCell, pos: PINP, neg: PINN) + -> Result, ProbeError> { + + let mut pos_pin = pos; + let mut neg_pin = neg; + // compute first temp approximation to speed up stabilization + let mut temp = 0.0; + for _ in 0..10 { + temp += read_temp(&adc, &mut pos_pin, &mut neg_pin)?; + } + temp = temp/10.0; + + Ok(TemperatureProbe { + adc, + pos_pin, + neg_pin, + phantom: PhantomData, + filtered_temp: temp, + stabilized: false, + }) + } + + pub fn read(&mut self) -> Result { + + let temp = read_temp(self.adc, &mut self.pos_pin, &mut self.neg_pin)?; + + match self.stabilized { + true => { + self.filtered_temp += (temp - self.filtered_temp)/FILTER_TIME_CONSTANT; + Ok(self.filtered_temp) + }, + false => { // filter is not yet stabilized + let old_temp = self.filtered_temp; + self.filtered_temp += (temp - self.filtered_temp)/FILTER_TIME_CONSTANT; + + match fabsf(old_temp - self.filtered_temp) < 0.01 { + true => self.stabilized = true, + false => (), + } + + Err(ProbeError::Initializing) + }, + } + } +} +