kirdy/src/thermostat/thermostat.rs

408 lines
15 KiB
Rust

use core::f64::NAN;
use core::marker::PhantomData;
use crate::sys_timer;
use crate::thermostat::ad5680;
use crate::thermostat::max1968::{MAX1968, AdcReadTarget, PwmPinsEnum};
use crate::thermostat::ad7172;
use crate::thermostat::pid_state::{PidState, PidSettings};
use crate::thermostat::steinhart_hart;
use crate::thermostat::temp_mon::{TempMon, TempStatus, TempMonSettings};
use serde::{Deserialize, Serialize};
use log::debug;
use uom::si::{
electric_current::ampere,
electric_potential::volt,
electrical_resistance::ohm,
thermodynamic_temperature::degree_celsius,
f64::{ThermodynamicTemperature, ElectricCurrent, ElectricPotential, ElectricalResistance},
ratio::ratio,
};
use miniconf::Tree;
use super::pid_state;
pub const R_SENSE: ElectricalResistance = ElectricalResistance {
dimension: PhantomData,
units: PhantomData,
value: 0.05,
};
#[derive(Clone, Debug, Tree)]
pub struct TecSettings {
pub center_pt: ElectricPotential,
pub max_v_set: ElectricPotential,
pub max_i_pos_set: ElectricCurrent,
pub max_i_neg_set: ElectricCurrent,
pub i_set: ElectricCurrent,
}
impl TecSettings{
pub const DAC_OUT_V_MAX: ElectricPotential = ElectricPotential {
dimension: PhantomData,
units: PhantomData,
value: 3.0,
};
pub const TEC_VSEC_BIAS_V: ElectricPotential = ElectricPotential {
dimension: PhantomData,
units: PhantomData,
value: 1.65,
};
// Kirdy Design Specs:
// MaxV = 5.0V
// MAX Current = +- 1.0A
const MAX_I_SET : ElectricCurrent = ElectricCurrent {
dimension: PhantomData,
units: PhantomData,
value: 1.0,
};
const MAX_V_DUTY_TO_CURRENT_RATE: ElectricPotential = ElectricPotential {
dimension: PhantomData,
units: PhantomData,
value: 4.0 * 3.3,
};
pub const MAX_V_MAX: ElectricPotential = ElectricPotential {
dimension: PhantomData,
units: PhantomData,
value: 5.0,
};
const MAX_V_DUTY_MAX: f64 = TecSettings::MAX_V_MAX.value / TecSettings::MAX_V_DUTY_TO_CURRENT_RATE.value;
const MAX_I_POS_NEG_DUTY_TO_CURRENT_RATE: ElectricCurrent = ElectricCurrent {
dimension: PhantomData,
units: PhantomData,
value: 1.0 / (10.0 * R_SENSE.value / 3.3),
};
pub const MAX_I_POS_CURRENT: ElectricCurrent = ElectricCurrent {
dimension: PhantomData,
units: PhantomData,
value: 1.0,
};
pub const MAX_I_NEG_CURRENT: ElectricCurrent = ElectricCurrent {
dimension: PhantomData,
units: PhantomData,
value: 1.0,
};
// .get::<ratio>() is not implemented for const
const MAX_I_POS_DUTY_MAX: f64 = TecSettings::MAX_I_POS_CURRENT.value / TecSettings::MAX_I_POS_NEG_DUTY_TO_CURRENT_RATE.value;
const MAX_I_NEG_DUTY_MAX: f64 = TecSettings::MAX_I_NEG_CURRENT.value / TecSettings::MAX_I_POS_NEG_DUTY_TO_CURRENT_RATE.value;
}
impl Default for TecSettings {
fn default() -> Self {
Self {
center_pt: ElectricPotential::new::<volt>(1.5),
max_v_set: ElectricPotential::new::<volt>(5.0),
max_i_pos_set: ElectricCurrent::new::<ampere>(1.0),
max_i_neg_set: ElectricCurrent::new::<ampere>(1.0),
i_set: ElectricCurrent::new::<ampere>(0.0),
}
}
}
pub struct Thermostat {
max1968: MAX1968,
ad7172: ad7172::AdcPhy,
pub tec_setting: TecSettings,
pid_ctrl_ch0: PidState,
temp_mon: TempMon,
}
impl Thermostat{
pub fn new (max1968: MAX1968, ad7172: ad7172::AdcPhy) -> Self {
Thermostat{
max1968: max1968,
ad7172: ad7172,
tec_setting: TecSettings::default(),
pid_ctrl_ch0: PidState::default(),
temp_mon: TempMon::default(),
}
}
pub fn setup(&mut self){
self.tec_setup();
let t_adc_ch0_cal = self.t_adc_setup();
self.pid_ctrl_ch0.set_adc_calibration(t_adc_ch0_cal) ;
}
fn tec_setup(&mut self) {
self.power_down();
self.tec_setting = TecSettings::default();
self.set_i(self.tec_setting.i_set);
self.set_max_v(self.tec_setting.max_v_set);
self.set_max_i_pos(self.tec_setting.max_i_pos_set);
self.set_max_i_neg(self.tec_setting.max_i_neg_set);
}
fn t_adc_setup(&mut self)->ad7172::ChannelCalibration{
self.ad7172.set_sync_enable(false).unwrap();
self.ad7172.setup_channel(0, ad7172::Input::Ain0, ad7172::Input::Ain1).unwrap();
let adc_calibration0 = self.ad7172.get_calibration(0)
.expect("adc_calibration0");
self.ad7172.start_continuous_conversion().unwrap();
adc_calibration0
}
pub fn poll_adc_and_update_pid(&mut self) -> bool {
let mut data_rdy = false;
self.ad7172.data_ready().unwrap().map(|_ch| {
let data = self.ad7172.read_data().unwrap();
let state: &mut PidState = &mut self.pid_ctrl_ch0;
state.update(data);
debug!("state.get_pid_engaged(): {:?}", state.get_pid_engaged());
let pid_engaged = state.get_pid_engaged();
if pid_engaged {
match state.update_pid() {
Some(pid_output) => {
self.set_i(ElectricCurrent::new::<ampere>(pid_output));
debug!("Temperature Set Point: {:?} degree", self.pid_ctrl_ch0.get_pid_setpoint().get::<degree_celsius>());
}
None => { }
}
}
let temp = self.get_temperature();
self.temp_mon.update_status(pid_engaged, temp);
debug!("Temperature: {:?} degree", temp.get::<degree_celsius>());
data_rdy = true;
});
data_rdy
}
pub fn get_temp_mon_status(&mut self) -> TempStatus {
self.temp_mon.get_status()
}
pub fn power_up(&mut self){
self.max1968.power_up();
}
pub fn power_down(&mut self){
self.max1968.power_down();
self.pid_ctrl_ch0.reset_pid_state();
self.set_i(ElectricCurrent::new::<ampere>(0.0));
}
fn set_center_pt(&mut self, value: ElectricPotential){
self.tec_setting.center_pt = value;
}
pub fn set_i(&mut self, i_tec: ElectricCurrent) -> ElectricCurrent {
let voltage = i_tec * 10.0 * R_SENSE + self.tec_setting.center_pt;
let voltage = self.max1968.set_dac(voltage, TecSettings::DAC_OUT_V_MAX);
self.tec_setting.i_set = (voltage - self.tec_setting.center_pt) / (10.0 * R_SENSE);
self.tec_setting.i_set
}
pub fn set_max_v(&mut self, max_v: ElectricPotential) -> ElectricPotential {
let duty = (max_v / TecSettings::MAX_V_DUTY_TO_CURRENT_RATE).get::<ratio>();
let duty = self.max1968.set_pwm(PwmPinsEnum::MaxV, duty, TecSettings::MAX_V_DUTY_MAX);
self.tec_setting.max_v_set = duty * TecSettings::MAX_V_DUTY_TO_CURRENT_RATE;
self.tec_setting.max_v_set
}
pub fn set_max_i_pos(&mut self, max_i_pos: ElectricCurrent) -> ElectricCurrent {
let duty = (max_i_pos / TecSettings::MAX_I_POS_NEG_DUTY_TO_CURRENT_RATE).get::<ratio>();
let duty = self.max1968.set_pwm(PwmPinsEnum::MaxPosI, duty, TecSettings::MAX_I_POS_DUTY_MAX);
self.tec_setting.max_i_pos_set = duty * TecSettings::MAX_I_POS_NEG_DUTY_TO_CURRENT_RATE;
self.tec_setting.max_i_pos_set
}
pub fn set_max_i_neg(&mut self, max_i_neg: ElectricCurrent) -> ElectricCurrent {
let duty = (max_i_neg / TecSettings::MAX_I_POS_NEG_DUTY_TO_CURRENT_RATE).get::<ratio>();
let duty = self.max1968.set_pwm(PwmPinsEnum::MaxNegI, duty, TecSettings::MAX_I_NEG_DUTY_MAX);
self.tec_setting.max_i_neg_set = duty * TecSettings::MAX_I_POS_NEG_DUTY_TO_CURRENT_RATE;
self.tec_setting.max_i_neg_set
}
pub fn get_dac_vfb(&mut self) -> ElectricPotential {
self.max1968.adc_read(AdcReadTarget::DacVfb, 16)
}
pub fn get_vref(&mut self) -> ElectricPotential {
self.max1968.adc_read(AdcReadTarget::VREF, 16)
}
pub fn get_tec_i(&mut self) -> ElectricCurrent {
let vref = self.get_vref();
(self.max1968.adc_read(AdcReadTarget::ITec, 16) - vref) / ElectricalResistance::new::<ohm>(0.4)
}
pub fn get_tec_v(&mut self) -> ElectricPotential {
(self.max1968.adc_read(AdcReadTarget::VTec, 16) - TecSettings::TEC_VSEC_BIAS_V) * 4.0
}
/// Calibrates the DAC output to match vref of the MAX driver to reduce zero-current offset of the MAX driver output.
///
/// The thermostat DAC applies a control voltage signal to the CTLI pin of MAX driver chip to control its output current.
/// The CTLI input signal is centered around VREF of the MAX chip. Applying VREF to CTLI sets the output current to 0.
///
/// This calibration routine measures the VREF voltage and the DAC output with the STM32 ADC, and uses a breadth-first
/// search to find the DAC setting that will produce a DAC output voltage closest to VREF. This DAC output voltage will
/// be stored and used in subsequent i_set routines to bias the current control signal to the measured VREF, reducing
/// the offset error of the current control signal.
///
/// The input offset of the STM32 ADC is eliminated by using the same ADC for the measurements, and by only using the
/// difference in VREF and DAC output for the calibration.
///
/// This routine should be called only once after boot, repeated reading of the vref signal and changing of the stored
/// VREF measurement can introduce significant noise at the current output, degrading the stabilily performance of the
/// thermostat.
pub fn calibrate_dac_value(&mut self) {
const STEPS: i32 = 18;
let target_voltage = self.max1968.adc_read(AdcReadTarget::VREF, 64);
let mut start_value = 1;
let mut best_error = ElectricPotential::new::<volt>(100.0);
for step in (0..STEPS).rev() {
debug!("TEC VREF Calibrating: Step {}/{}", step, STEPS);
let mut prev_value = start_value;
for value in (start_value..=ad5680::MAX_VALUE).step_by(1 << step) {
self.max1968.phy.dac.set(value).unwrap();
sys_timer::sleep(5);
let dac_feedback = self.max1968.adc_read(AdcReadTarget::DacVfb, 64);
let error = target_voltage - dac_feedback;
if error < ElectricPotential::new::<volt>(0.0) {
break;
} else if error < best_error {
best_error = error;
start_value = prev_value;
let vref = (value as f64 / ad5680::MAX_VALUE as f64) * TecSettings::DAC_OUT_V_MAX;
self.set_center_pt(vref);
}
prev_value = value;
}
}
}
pub fn set_pid_engaged(&mut self, val: bool) {
self.pid_ctrl_ch0.set_pid_engaged(val);
}
pub fn get_pid_engaged(&mut self) -> bool {
self.pid_ctrl_ch0.get_pid_engaged()
}
pub fn get_status_report(&mut self) -> StatusReport {
StatusReport {
ts: sys_timer::now(),
pid_engaged: self.get_pid_engaged(),
temp_mon_status: self.temp_mon.get_status(),
temperature: self.pid_ctrl_ch0.get_temperature(),
i_set: self.tec_setting.i_set,
tec_i: self.get_tec_i(),
tec_v: self.get_tec_v(),
tec_vref: self.get_vref(),
}
}
pub fn get_temperature(&mut self) -> ThermodynamicTemperature {
match self.pid_ctrl_ch0.get_temperature() {
Some(val) => {
val
}
None => { ThermodynamicTemperature::new::<degree_celsius>(NAN) }
}
}
pub fn get_pid_settings(&mut self) -> pid_state::Parameters {
self.pid_ctrl_ch0.get_pid_settings()
}
pub fn get_steinhart_hart(&mut self) -> steinhart_hart::Parameters {
self.pid_ctrl_ch0.get_sh()
}
pub fn get_tec_settings(&mut self) -> TecSettingSummary {
TecSettingSummary {
center_point: self.tec_setting.center_pt,
i_set: TecSettingsSummaryField { value: self.tec_setting.i_set, max: TecSettings::MAX_I_SET },
max_v: TecSettingsSummaryField { value: self.tec_setting.max_v_set, max: TecSettings::MAX_V_MAX },
max_i_pos: TecSettingsSummaryField { value: self.tec_setting.max_i_pos_set, max: TecSettings::MAX_I_POS_CURRENT },
max_i_neg: TecSettingsSummaryField { value: self.tec_setting.max_i_neg_set, max: TecSettings::MAX_I_NEG_CURRENT },
}
}
pub fn get_calibrated_vdda(&mut self) -> u32 {
self.max1968.get_calibrated_vdda()
}
pub fn set_pid(&mut self, param: PidSettings, val: f64){
self.pid_ctrl_ch0.set_pid_params(param, val);
}
pub fn set_sh_beta(&mut self, beta: f64) {
self.pid_ctrl_ch0.set_sh_beta(beta);
}
pub fn set_sh_r0(&mut self, r0: ElectricalResistance) {
self.pid_ctrl_ch0.set_sh_r0(r0);
}
pub fn set_sh_t0(&mut self, t0: ThermodynamicTemperature) {
self.pid_ctrl_ch0.set_sh_t0(t0);
}
pub fn set_temperature_setpoint(&mut self, t: ThermodynamicTemperature) {
let temp_mon_settings = self.temp_mon.get_settings();
let t = t.min(temp_mon_settings.upper_limit).max(temp_mon_settings.lower_limit);
self.pid_ctrl_ch0.set_pid_setpoint(t);
self.temp_mon.set_setpoint(t);
}
pub fn set_temp_mon_upper_limit(&mut self, t: ThermodynamicTemperature) {
self.temp_mon.set_upper_limit(t);
}
pub fn set_temp_mon_lower_limit(&mut self, t: ThermodynamicTemperature) {
self.temp_mon.set_lower_limit(t);
}
pub fn clear_temp_mon_alarm(&mut self) {
self.temp_mon.clear_alarm();
}
pub fn get_temp_mon_settings(&mut self) -> TempMonSettings {
self.temp_mon.get_settings()
}
}
#[derive(Deserialize, Serialize, Copy, Clone, Debug, Tree)]
pub struct StatusReport {
ts: u32,
pid_engaged: bool,
temp_mon_status: TempStatus,
temperature: Option<ThermodynamicTemperature>,
i_set: ElectricCurrent,
tec_i: ElectricCurrent,
tec_v: ElectricPotential,
tec_vref: ElectricPotential,
}
#[derive(Tree)]
pub struct TecSettingsSummaryField<T> {
value: T,
max: T,
}
#[derive(Tree)]
pub struct TecSettingSummary {
center_point: ElectricPotential,
#[tree]
i_set: TecSettingsSummaryField<ElectricCurrent>,
#[tree]
max_v: TecSettingsSummaryField<ElectricPotential>,
#[tree]
max_i_pos: TecSettingsSummaryField<ElectricCurrent>,
#[tree]
max_i_neg: TecSettingsSummaryField<ElectricCurrent>,
}
#[derive(Tree)]
pub struct SteinhartHartSummary {
#[tree]
params: steinhart_hart::Parameters,
}