forked from M-Labs/thermostat
Compare commits
2 Commits
00d5feaa8d
...
1fcfe41a63
Author | SHA1 | Date |
---|---|---|
linuswck | 1fcfe41a63 | |
linuswck | 9fce19a418 |
|
@ -271,8 +271,6 @@ with the following keys.
|
||||||
| `tec_u_meas` | Volts | Measurement of the voltage across the TEC |
|
| `tec_u_meas` | Volts | Measurement of the voltage across the TEC |
|
||||||
| `pid_output` | Amperes | PID control output |
|
| `pid_output` | Amperes | PID control output |
|
||||||
|
|
||||||
Note: With Thermostat v2 and below, the voltage and current readouts `i_tec` and `tec_i` are disabled and null due to faulty hardware that introduces a lot of noise in the signal.
|
|
||||||
|
|
||||||
## PID Tuning
|
## PID Tuning
|
||||||
|
|
||||||
The thermostat implements a PID control loop for each of the TEC channels, more details on setting up the PID control loop can be found [here](./doc/PID%20tuning.md).
|
The thermostat implements a PID control loop for each of the TEC channels, more details on setting up the PID control loop can be found [here](./doc/PID%20tuning.md).
|
||||||
|
|
218
src/channels.rs
218
src/channels.rs
|
@ -20,9 +20,15 @@ use crate::{
|
||||||
command_handler::JsonBuffer,
|
command_handler::JsonBuffer,
|
||||||
pins::{self, Channel0VRef, Channel1VRef},
|
pins::{self, Channel0VRef, Channel1VRef},
|
||||||
steinhart_hart,
|
steinhart_hart,
|
||||||
hw_rev,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub enum PinsAdcReadTarget {
|
||||||
|
VREF,
|
||||||
|
DacVfb,
|
||||||
|
ITec,
|
||||||
|
VTec,
|
||||||
|
}
|
||||||
|
|
||||||
pub const CHANNELS: usize = 2;
|
pub const CHANNELS: usize = 2;
|
||||||
pub const R_SENSE: f64 = 0.05;
|
pub const R_SENSE: f64 = 0.05;
|
||||||
|
|
||||||
|
@ -33,18 +39,17 @@ pub const MAX_TEC_I: f64 = 3.0;
|
||||||
const DAC_OUT_V_MAX: f64 = 3.0;
|
const DAC_OUT_V_MAX: f64 = 3.0;
|
||||||
|
|
||||||
// TODO: -pub
|
// TODO: -pub
|
||||||
pub struct Channels<'a> {
|
pub struct Channels {
|
||||||
channel0: Channel<Channel0>,
|
channel0: Channel<Channel0>,
|
||||||
channel1: Channel<Channel1>,
|
channel1: Channel<Channel1>,
|
||||||
pub adc: ad7172::Adc<pins::AdcSpi, pins::AdcNss>,
|
pub adc: ad7172::Adc<pins::AdcSpi, pins::AdcNss>,
|
||||||
/// stm32f4 integrated adc
|
/// stm32f4 integrated adc
|
||||||
pins_adc: pins::PinsAdc,
|
pins_adc: pins::PinsAdc,
|
||||||
pub pwm: pins::PwmPins,
|
pub pwm: pins::PwmPins,
|
||||||
hwrev: &'a hw_rev::HWRev,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Channels<'a> {
|
impl Channels {
|
||||||
pub fn new(pins: pins::Pins, hwrev: &'a hw_rev::HWRev) -> Self {
|
pub fn new(pins: pins::Pins) -> Self {
|
||||||
let mut adc = ad7172::Adc::new(pins.adc_spi, pins.adc_nss).unwrap();
|
let mut adc = ad7172::Adc::new(pins.adc_spi, pins.adc_nss).unwrap();
|
||||||
// Feature not used
|
// Feature not used
|
||||||
adc.set_sync_enable(false).unwrap();
|
adc.set_sync_enable(false).unwrap();
|
||||||
|
@ -62,7 +67,7 @@ impl<'a> Channels<'a> {
|
||||||
let channel1 = Channel::new(pins.channel1, adc_calibration1);
|
let channel1 = Channel::new(pins.channel1, adc_calibration1);
|
||||||
let pins_adc = pins.pins_adc;
|
let pins_adc = pins.pins_adc;
|
||||||
let pwm = pins.pwm;
|
let pwm = pins.pwm;
|
||||||
let mut channels = Channels { channel0, channel1, adc, pins_adc, pwm, hwrev };
|
let mut channels = Channels { channel0, channel1, adc, pins_adc, pwm };
|
||||||
for channel in 0..CHANNELS {
|
for channel in 0..CHANNELS {
|
||||||
channels.calibrate_dac_value(channel);
|
channels.calibrate_dac_value(channel);
|
||||||
channels.set_i(channel, ElectricCurrent::new::<ampere>(0.0));
|
channels.set_i(channel, ElectricCurrent::new::<ampere>(0.0));
|
||||||
|
@ -104,7 +109,7 @@ impl<'a> Channels<'a> {
|
||||||
pub fn get_center(&mut self, channel: usize) -> ElectricPotential {
|
pub fn get_center(&mut self, channel: usize) -> ElectricPotential {
|
||||||
match self.channel_state(channel).center {
|
match self.channel_state(channel).center {
|
||||||
CenterPoint::Vref =>
|
CenterPoint::Vref =>
|
||||||
self.read_vref(channel),
|
self.adc_read(channel, PinsAdcReadTarget::VREF, 8),
|
||||||
CenterPoint::Override(center_point) =>
|
CenterPoint::Override(center_point) =>
|
||||||
ElectricPotential::new::<volt>(center_point.into()),
|
ElectricPotential::new::<volt>(center_point.into()),
|
||||||
}
|
}
|
||||||
|
@ -153,32 +158,112 @@ impl<'a> Channels<'a> {
|
||||||
i_set
|
i_set
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn read_dac_feedback(&mut self, channel: usize) -> ElectricPotential {
|
/// AN4073: ADC Reading Dispersion can be reduced through Averaging
|
||||||
|
pub fn adc_read(&mut self, channel: usize, adc_read_target: PinsAdcReadTarget, avg_pt: u16) -> ElectricPotential {
|
||||||
|
let mut sample: u32 = 0;
|
||||||
match channel {
|
match channel {
|
||||||
0 => {
|
0 => {
|
||||||
let sample = self.pins_adc.convert(
|
sample = match adc_read_target {
|
||||||
&self.channel0.dac_feedback_pin,
|
PinsAdcReadTarget::VREF => {
|
||||||
stm32f4xx_hal::adc::config::SampleTime::Cycles_480
|
match &self.channel0.vref_pin {
|
||||||
);
|
Channel0VRef::Analog(vref_pin) => {
|
||||||
let mv = self.pins_adc.sample_to_millivolts(sample);
|
for _ in (0..avg_pt).rev() {
|
||||||
|
sample += self
|
||||||
|
.pins_adc
|
||||||
|
.convert(vref_pin, stm32f4xx_hal::adc::config::SampleTime::Cycles_480)
|
||||||
|
as u32;
|
||||||
|
}
|
||||||
|
sample / avg_pt as u32
|
||||||
|
},
|
||||||
|
Channel0VRef::Disabled(_) => {2048 as u32}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PinsAdcReadTarget::DacVfb => {
|
||||||
|
for _ in (0..avg_pt).rev() {
|
||||||
|
sample += self
|
||||||
|
.pins_adc
|
||||||
|
.convert(&self.channel0.dac_feedback_pin,stm32f4xx_hal::adc::config::SampleTime::Cycles_480)
|
||||||
|
as u32;
|
||||||
|
}
|
||||||
|
sample / avg_pt as u32
|
||||||
|
}
|
||||||
|
PinsAdcReadTarget::ITec => {
|
||||||
|
for _ in (0..avg_pt).rev() {
|
||||||
|
sample += self
|
||||||
|
.pins_adc
|
||||||
|
.convert(&self.channel0.itec_pin, stm32f4xx_hal::adc::config::SampleTime::Cycles_480)
|
||||||
|
as u32;
|
||||||
|
}
|
||||||
|
sample / avg_pt as u32
|
||||||
|
}
|
||||||
|
PinsAdcReadTarget::VTec => {
|
||||||
|
for _ in (0..avg_pt).rev() {
|
||||||
|
sample += self
|
||||||
|
.pins_adc
|
||||||
|
.convert(&self.channel0.tec_u_meas_pin, stm32f4xx_hal::adc::config::SampleTime::Cycles_480)
|
||||||
|
as u32;
|
||||||
|
}
|
||||||
|
sample / avg_pt as u32
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let mv = self.pins_adc.sample_to_millivolts(sample as u16);
|
||||||
ElectricPotential::new::<millivolt>(mv as f64)
|
ElectricPotential::new::<millivolt>(mv as f64)
|
||||||
}
|
}
|
||||||
1 => {
|
1 => {
|
||||||
let sample = self.pins_adc.convert(
|
sample = match adc_read_target {
|
||||||
&self.channel1.dac_feedback_pin,
|
PinsAdcReadTarget::VREF => {
|
||||||
stm32f4xx_hal::adc::config::SampleTime::Cycles_480
|
match &self.channel1.vref_pin {
|
||||||
);
|
Channel1VRef::Analog(vref_pin) => {
|
||||||
let mv = self.pins_adc.sample_to_millivolts(sample);
|
for _ in (0..avg_pt).rev() {
|
||||||
|
sample += self
|
||||||
|
.pins_adc
|
||||||
|
.convert(vref_pin, stm32f4xx_hal::adc::config::SampleTime::Cycles_480)
|
||||||
|
as u32;
|
||||||
|
}
|
||||||
|
sample / avg_pt as u32
|
||||||
|
},
|
||||||
|
Channel1VRef::Disabled(_) => {2048 as u32}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PinsAdcReadTarget::DacVfb => {
|
||||||
|
for _ in (0..avg_pt).rev() {
|
||||||
|
sample += self
|
||||||
|
.pins_adc
|
||||||
|
.convert(&self.channel1.dac_feedback_pin, stm32f4xx_hal::adc::config::SampleTime::Cycles_480)
|
||||||
|
as u32;
|
||||||
|
}
|
||||||
|
sample / avg_pt as u32
|
||||||
|
}
|
||||||
|
PinsAdcReadTarget::ITec => {
|
||||||
|
for _ in (0..avg_pt).rev() {
|
||||||
|
sample += self
|
||||||
|
.pins_adc
|
||||||
|
.convert(&self.channel1.itec_pin, stm32f4xx_hal::adc::config::SampleTime::Cycles_480)
|
||||||
|
as u32;
|
||||||
|
}
|
||||||
|
sample / avg_pt as u32
|
||||||
|
}
|
||||||
|
PinsAdcReadTarget::VTec => {
|
||||||
|
for _ in (0..avg_pt).rev() {
|
||||||
|
sample += self
|
||||||
|
.pins_adc
|
||||||
|
.convert(&self.channel1.tec_u_meas_pin, stm32f4xx_hal::adc::config::SampleTime::Cycles_480)
|
||||||
|
as u32;
|
||||||
|
}
|
||||||
|
sample / avg_pt as u32
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let mv = self.pins_adc.sample_to_millivolts(sample as u16);
|
||||||
ElectricPotential::new::<millivolt>(mv as f64)
|
ElectricPotential::new::<millivolt>(mv as f64)
|
||||||
}
|
}
|
||||||
_ => unreachable!(),
|
_ => unreachable!()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn read_dac_feedback_until_stable(&mut self, channel: usize, tolerance: ElectricPotential) -> ElectricPotential {
|
pub fn read_dac_feedback_until_stable(&mut self, channel: usize, tolerance: ElectricPotential) -> ElectricPotential {
|
||||||
let mut prev = self.read_dac_feedback(channel);
|
let mut prev = self.adc_read(channel, PinsAdcReadTarget::DacVfb, 1);
|
||||||
loop {
|
loop {
|
||||||
let current = self.read_dac_feedback(channel);
|
let current = self.adc_read(channel, PinsAdcReadTarget::DacVfb, 1);
|
||||||
if (current - prev).abs() < tolerance {
|
if (current - prev).abs() < tolerance {
|
||||||
return current;
|
return current;
|
||||||
}
|
}
|
||||||
|
@ -186,83 +271,6 @@ impl<'a> Channels<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn read_itec(&mut self, channel: usize) -> ElectricPotential {
|
|
||||||
match channel {
|
|
||||||
0 => {
|
|
||||||
let sample = self.pins_adc.convert(
|
|
||||||
&self.channel0.itec_pin,
|
|
||||||
stm32f4xx_hal::adc::config::SampleTime::Cycles_480
|
|
||||||
);
|
|
||||||
let mv = self.pins_adc.sample_to_millivolts(sample);
|
|
||||||
ElectricPotential::new::<millivolt>(mv as f64)
|
|
||||||
}
|
|
||||||
1 => {
|
|
||||||
let sample = self.pins_adc.convert(
|
|
||||||
&self.channel1.itec_pin,
|
|
||||||
stm32f4xx_hal::adc::config::SampleTime::Cycles_480
|
|
||||||
);
|
|
||||||
let mv = self.pins_adc.sample_to_millivolts(sample);
|
|
||||||
ElectricPotential::new::<millivolt>(mv as f64)
|
|
||||||
}
|
|
||||||
_ => unreachable!(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// should be 1.5V
|
|
||||||
pub fn read_vref(&mut self, channel: usize) -> ElectricPotential {
|
|
||||||
match channel {
|
|
||||||
0 => {
|
|
||||||
match &self.channel0.vref_pin {
|
|
||||||
Channel0VRef::Analog(vref_pin) => {
|
|
||||||
let sample = self.pins_adc.convert(
|
|
||||||
vref_pin,
|
|
||||||
stm32f4xx_hal::adc::config::SampleTime::Cycles_480
|
|
||||||
);
|
|
||||||
let mv = self.pins_adc.sample_to_millivolts(sample);
|
|
||||||
ElectricPotential::new::<millivolt>(mv as f64)
|
|
||||||
},
|
|
||||||
Channel0VRef::Disabled(_) => ElectricPotential::new::<volt>(1.5)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
1 => {
|
|
||||||
match &self.channel1.vref_pin {
|
|
||||||
Channel1VRef::Analog(vref_pin) => {
|
|
||||||
let sample = self.pins_adc.convert(
|
|
||||||
vref_pin,
|
|
||||||
stm32f4xx_hal::adc::config::SampleTime::Cycles_480
|
|
||||||
);
|
|
||||||
let mv = self.pins_adc.sample_to_millivolts(sample);
|
|
||||||
ElectricPotential::new::<millivolt>(mv as f64)
|
|
||||||
},
|
|
||||||
Channel1VRef::Disabled(_) => ElectricPotential::new::<volt>(1.5)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => unreachable!(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn read_tec_u_meas(&mut self, channel: usize) -> ElectricPotential {
|
|
||||||
match channel {
|
|
||||||
0 => {
|
|
||||||
let sample = self.pins_adc.convert(
|
|
||||||
&self.channel0.tec_u_meas_pin,
|
|
||||||
stm32f4xx_hal::adc::config::SampleTime::Cycles_480
|
|
||||||
);
|
|
||||||
let mv = self.pins_adc.sample_to_millivolts(sample);
|
|
||||||
ElectricPotential::new::<millivolt>(mv as f64)
|
|
||||||
}
|
|
||||||
1 => {
|
|
||||||
let sample = self.pins_adc.convert(
|
|
||||||
&self.channel1.tec_u_meas_pin,
|
|
||||||
stm32f4xx_hal::adc::config::SampleTime::Cycles_480
|
|
||||||
);
|
|
||||||
let mv = self.pins_adc.sample_to_millivolts(sample);
|
|
||||||
ElectricPotential::new::<millivolt>(mv as f64)
|
|
||||||
}
|
|
||||||
_ => unreachable!(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Calibrates the DAC output to match vref of the MAX driver to reduce zero-current offset of the MAX driver output.
|
/// 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 thermostat DAC applies a control voltage signal to the CTLI pin of MAX driver chip to control its output current.
|
||||||
|
@ -390,12 +398,12 @@ impl<'a> Channels<'a> {
|
||||||
|
|
||||||
// Get current passing through TEC
|
// Get current passing through TEC
|
||||||
pub fn get_tec_i(&mut self, channel: usize) -> ElectricCurrent {
|
pub fn get_tec_i(&mut self, channel: usize) -> ElectricCurrent {
|
||||||
(self.read_itec(channel) - self.read_vref(channel)) / ElectricalResistance::new::<ohm>(0.4)
|
(self.adc_read(channel, PinsAdcReadTarget::ITec, 16) - self.adc_read(channel, PinsAdcReadTarget::VREF, 16)) / ElectricalResistance::new::<ohm>(0.4)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get voltage across TEC
|
// Get voltage across TEC
|
||||||
pub fn get_tec_v(&mut self, channel: usize) -> ElectricPotential {
|
pub fn get_tec_v(&mut self, channel: usize) -> ElectricPotential {
|
||||||
(self.read_tec_u_meas(channel) - ElectricPotential::new::<volt>(1.5)) * 4.0
|
(self.adc_read(channel, PinsAdcReadTarget::VTec, 16) - ElectricPotential::new::<volt>(1.5)) * 4.0
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_pwm(&mut self, channel: usize, pin: PwmPin, duty: f64) -> f64 {
|
fn set_pwm(&mut self, channel: usize, pin: PwmPin, duty: f64) -> f64 {
|
||||||
|
@ -448,8 +456,8 @@ impl<'a> Channels<'a> {
|
||||||
|
|
||||||
fn report(&mut self, channel: usize) -> Report {
|
fn report(&mut self, channel: usize) -> Report {
|
||||||
let i_set = self.get_i(channel);
|
let i_set = self.get_i(channel);
|
||||||
let i_tec = if self.hwrev.major > 2 {Some(self.read_itec(channel))} else {None};
|
let i_tec = self.adc_read(channel, PinsAdcReadTarget::ITec, 16);
|
||||||
let tec_i = if self.hwrev.major > 2 {Some(self.get_tec_i(channel))} else {None};
|
let tec_i = self.get_tec_i(channel);
|
||||||
let dac_value = self.get_dac(channel);
|
let dac_value = self.get_dac(channel);
|
||||||
let state = self.channel_state(channel);
|
let state = self.channel_state(channel);
|
||||||
let pid_output = ElectricCurrent::new::<ampere>(state.pid.y1);
|
let pid_output = ElectricCurrent::new::<ampere>(state.pid.y1);
|
||||||
|
@ -464,7 +472,7 @@ impl<'a> Channels<'a> {
|
||||||
pid_engaged: state.pid_engaged,
|
pid_engaged: state.pid_engaged,
|
||||||
i_set,
|
i_set,
|
||||||
dac_value,
|
dac_value,
|
||||||
dac_feedback: self.read_dac_feedback(channel),
|
dac_feedback: self.adc_read(channel, PinsAdcReadTarget::DacVfb, 1),
|
||||||
i_tec,
|
i_tec,
|
||||||
tec_i,
|
tec_i,
|
||||||
tec_u_meas: self.get_tec_v(channel),
|
tec_u_meas: self.get_tec_v(channel),
|
||||||
|
@ -562,8 +570,8 @@ pub struct Report {
|
||||||
i_set: ElectricCurrent,
|
i_set: ElectricCurrent,
|
||||||
dac_value: ElectricPotential,
|
dac_value: ElectricPotential,
|
||||||
dac_feedback: ElectricPotential,
|
dac_feedback: ElectricPotential,
|
||||||
i_tec: Option<ElectricPotential>,
|
i_tec: ElectricPotential,
|
||||||
tec_i: Option<ElectricCurrent>,
|
tec_i: ElectricCurrent,
|
||||||
tec_u_meas: ElectricPotential,
|
tec_u_meas: ElectricPotential,
|
||||||
pid_output: ElectricCurrent,
|
pid_output: ElectricCurrent,
|
||||||
}
|
}
|
||||||
|
|
|
@ -138,7 +138,7 @@ fn main() -> ! {
|
||||||
|
|
||||||
let mut store = flash_store::store(dp.FLASH);
|
let mut store = flash_store::store(dp.FLASH);
|
||||||
|
|
||||||
let mut channels = Channels::new(pins, &hwrev);
|
let mut channels = Channels::new(pins);
|
||||||
for c in 0..CHANNELS {
|
for c in 0..CHANNELS {
|
||||||
match store.read_value::<ChannelConfig>(CHANNEL_CONFIG_KEY[c]) {
|
match store.read_value::<ChannelConfig>(CHANNEL_CONFIG_KEY[c]) {
|
||||||
Ok(Some(config)) =>
|
Ok(Some(config)) =>
|
||||||
|
|
Loading…
Reference in New Issue