Merge branch '5-implement-temperature-computations' into 'dev'
Resolve "Implement temperature computations" See merge request Steins7/fan_monitor!4
This commit is contained in:
commit
891e3d03cd
@ -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"
|
||||
|
||||
BIN
docs/Graphs.ods
BIN
docs/Graphs.ods
Binary file not shown.
44
src/config.rs
Normal file
44
src/config.rs
Normal file
@ -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<ConfigError, SystemConfig> {
|
||||
unimplemented!();
|
||||
}
|
||||
|
||||
pub fn store() -> SystemConfig {
|
||||
unimplemented!();
|
||||
}
|
||||
}
|
||||
@ -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<L, Q, B>
|
||||
where
|
||||
L: LCDScreen,
|
||||
@ -24,6 +54,8 @@ where
|
||||
button: &'static AtomicBool,
|
||||
backlight: Option<B>,
|
||||
count: u16,
|
||||
ext_deg: Temp,
|
||||
p1_deg: Temp,
|
||||
}
|
||||
|
||||
impl<L, Q, B> LCDGui<L, Q, B>
|
||||
@ -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<O: OutputPin>(&mut self, state: &mut SystemState<O>) {
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
73
src/main.rs
73
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<RefCell<Option<gpiob::PB8<Input<PullUp>>>>>
|
||||
static BUTTON_FLAG: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
// temps interrupt
|
||||
static RED_LED: Mutex<RefCell<Option<gpiob::PB11<Output<PushPull>>>>>
|
||||
= Mutex::new(RefCell::new(None));
|
||||
static RTC: Mutex<RefCell<Option<Rtc>>> = Mutex::new(RefCell::new(None));
|
||||
static G_EXTI: Mutex<RefCell<Option<EXTI>>> = 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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
127
src/state.rs
Normal file
127
src/state.rs
Normal file
@ -0,0 +1,127 @@
|
||||
|
||||
use embedded_hal::digital::v2::OutputPin;
|
||||
|
||||
use crate::config::{
|
||||
SystemConfig,
|
||||
FanOutput,
|
||||
};
|
||||
|
||||
pub enum FanState {
|
||||
ON,
|
||||
OFF,
|
||||
}
|
||||
|
||||
pub struct Fan<O: OutputPin> {
|
||||
state: FanState,
|
||||
pin: O,
|
||||
}
|
||||
|
||||
impl<O: OutputPin> Fan<O> {
|
||||
|
||||
pub fn new(mut pin: O) -> Fan<O> {
|
||||
|
||||
// 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<O: OutputPin> {
|
||||
config: SystemConfig,
|
||||
ext_temp: Option<f32>,
|
||||
p1_temp: Option<f32>,
|
||||
p2_temp: Option<f32>,
|
||||
fan: Fan<O>,
|
||||
}
|
||||
|
||||
impl<O: OutputPin> SystemState<O> {
|
||||
|
||||
pub fn new(config: SystemConfig, fan_control_pin: O) -> SystemState<O> {
|
||||
|
||||
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<f32> { self.ext_temp }
|
||||
pub fn p1_temp(&self) -> Option<f32> { self.p1_temp }
|
||||
pub fn p2_temp(&self) -> Option<f32> { 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<f32>, p1_temp: Option<f32>, p2_temp: Option<f32>) {
|
||||
|
||||
// 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();
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
116
src/utils.rs
Normal file
116
src/utils.rs
Normal file
@ -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<ADC>,
|
||||
PINN: Channel<ADC>,
|
||||
O: OneShot<ADC, u16, PINP> + OneShot<ADC, u16, PINN>,
|
||||
{
|
||||
adc: &'a RefCell<O>,
|
||||
pos_pin: PINP,
|
||||
neg_pin: PINN,
|
||||
phantom: PhantomData<ADC>,
|
||||
|
||||
filtered_temp: f32,
|
||||
stabilized: bool,
|
||||
}
|
||||
|
||||
fn read_temp<'a, ADC, O, PINP, PINN>(adc: &'a RefCell<O>, pos: &mut PINP, neg: &mut PINN)
|
||||
-> Result<f32, ProbeError>
|
||||
where
|
||||
PINP: Channel<ADC>,
|
||||
PINN: Channel<ADC>,
|
||||
O: OneShot<ADC, u16, PINP> + OneShot<ADC, u16, PINN>,
|
||||
{
|
||||
//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<ADC>,
|
||||
PINN: Channel<ADC>,
|
||||
O: OneShot<ADC, u16, PINP> + OneShot<ADC, u16, PINN>,
|
||||
{
|
||||
pub fn new(adc: &'a RefCell<O>, pos: PINP, neg: PINN)
|
||||
-> Result<TemperatureProbe<ADC, O, PINP, PINN>, 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<f32, ProbeError> {
|
||||
|
||||
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)
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user