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 a6a7b2b..f1eb706 100644 Binary files a/docs/Graphs.ods and b/docs/Graphs.ods differ 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) + }, + } + } +} +