forked from M-Labs/thermostat
Compare commits
6 Commits
8669020b42
...
0940520ded
Author | SHA1 | Date | |
---|---|---|---|
0940520ded | |||
2c6b8e8186 | |||
8f8baa0cee | |||
ae4bea0c8a | |||
1f2de942e4 | |||
1041d3ecbb |
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,2 +1,5 @@
|
||||
target/
|
||||
result
|
||||
*.bin
|
||||
|
||||
__pycache__/
|
||||
|
31
README.md
31
README.md
@ -256,21 +256,22 @@ Use the bare `report` command to obtain a single report. Enable
|
||||
continuous reporting with `report mode on`. Reports are JSON objects
|
||||
with the following keys.
|
||||
|
||||
| Key | Unit | Description |
|
||||
| --- | :---: | --- |
|
||||
| `channel` | Integer | Channel `0`, or `1` |
|
||||
| `time` | Seconds | Temperature measurement time |
|
||||
| `adc` | Volts | AD7172 input |
|
||||
| `sens` | Ohms | Thermistor resistance derived from `adc` |
|
||||
| `temperature` | Degrees Celsius | Steinhart-Hart conversion result derived from `sens` |
|
||||
| `pid_engaged` | Boolean | `true` if in closed-loop mode |
|
||||
| `i_set` | Amperes | TEC output current |
|
||||
| `dac_value` | Volts | AD5680 output derived from `i_set` |
|
||||
| `dac_feedback` | Volts | ADC measurement of the AD5680 output |
|
||||
| `i_tec` | Volts | MAX1968 TEC current monitor |
|
||||
| `tec_i` | Amperes | TEC output current feedback derived from `i_tec` |
|
||||
| `tec_u_meas` | Volts | Measurement of the voltage across the TEC |
|
||||
| `pid_output` | Amperes | PID control output |
|
||||
| Key | Unit | Description |
|
||||
| --- | :---: | --- |
|
||||
| `channel` | Integer | Channel `0`, or `1` |
|
||||
| `time` | Seconds | Temperature measurement time |
|
||||
| `adc` | Volts | AD7172 input |
|
||||
| `sens` | Ohms | Thermistor resistance derived from `adc` |
|
||||
| `temperature` | Degrees Celsius | Steinhart-Hart conversion result derived from `sens` |
|
||||
| `pid_engaged` | Boolean | `true` if in closed-loop mode |
|
||||
| `current_swapped` | Boolean | `true` if TEC current direction is swapped relative to front panel |
|
||||
| `i_set` | Amperes | TEC output current |
|
||||
| `dac_value` | Volts | AD5680 output derived from `i_set` |
|
||||
| `dac_feedback` | Volts | ADC measurement of the AD5680 output |
|
||||
| `i_tec` | Volts | MAX1968 TEC current monitor |
|
||||
| `tec_i` | Amperes | TEC output current feedback derived from `i_tec` |
|
||||
| `tec_u_meas` | Volts | Measurement of the voltage across the TEC |
|
||||
| `pid_output` | Amperes | PID control output |
|
||||
|
||||
Note: With Thermostat v2 and below, the voltage and current readouts `i_tec` and `tec_i` are noisy without the hardware fix shown in [this PR][https://git.m-labs.hk/M-Labs/thermostat/pulls/105].
|
||||
|
||||
|
@ -63,7 +63,7 @@
|
||||
name = "thermostat-dev-shell";
|
||||
packages = with pkgs; [
|
||||
rust llvm
|
||||
openocd dfu-util
|
||||
openocd dfu-util rlwrap
|
||||
] ++ (with python3Packages; [
|
||||
numpy matplotlib
|
||||
]);
|
||||
|
@ -22,6 +22,7 @@ use crate::{
|
||||
pins::{self, Channel0VRef, Channel1VRef},
|
||||
steinhart_hart,
|
||||
};
|
||||
use crate::timer::sleep;
|
||||
|
||||
pub enum PinsAdcReadTarget {
|
||||
VREF,
|
||||
@ -152,11 +153,13 @@ impl Channels {
|
||||
}
|
||||
|
||||
pub fn set_i(&mut self, channel: usize, i_set: ElectricCurrent) -> ElectricCurrent {
|
||||
let mut i_set = i_set.min(MAX_TEC_I).max(-MAX_TEC_I);
|
||||
let i_set = i_set.min(MAX_TEC_I).max(-MAX_TEC_I);
|
||||
self.channel_state(channel).i_set = i_set;
|
||||
if self.channel_state(channel).polarity_swapped {
|
||||
i_set = -i_set;
|
||||
}
|
||||
let negate = if self.channel_state(channel).polarity_swapped {
|
||||
-1.0
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
let vref_meas = match channel.into() {
|
||||
0 => self.channel0.vref_meas,
|
||||
1 => self.channel1.vref_meas,
|
||||
@ -164,9 +167,9 @@ impl Channels {
|
||||
};
|
||||
let center_point = vref_meas;
|
||||
let r_sense = ElectricalResistance::new::<ohm>(R_SENSE);
|
||||
let voltage = i_set * 10.0 * r_sense + center_point;
|
||||
let voltage = negate * i_set * 10.0 * r_sense + center_point;
|
||||
let voltage = self.set_dac(channel, voltage);
|
||||
let i_set = (voltage - center_point) / (10.0 * r_sense);
|
||||
let i_set = negate * (voltage - center_point) / (10.0 * r_sense);
|
||||
i_set
|
||||
}
|
||||
|
||||
@ -272,17 +275,6 @@ impl Channels {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read_dac_feedback_until_stable(&mut self, channel: usize, tolerance: ElectricPotential) -> ElectricPotential {
|
||||
let mut prev = self.adc_read(channel, PinsAdcReadTarget::DacVfb, 1);
|
||||
loop {
|
||||
let current = self.adc_read(channel, PinsAdcReadTarget::DacVfb, 1);
|
||||
if (current - prev).abs() < tolerance {
|
||||
return current;
|
||||
}
|
||||
prev = current;
|
||||
}
|
||||
}
|
||||
|
||||
/// 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.
|
||||
@ -309,7 +301,7 @@ impl Channels {
|
||||
let mut start_value = 1;
|
||||
let mut best_error = ElectricPotential::new::<volt>(100.0);
|
||||
|
||||
for step in (0..18).rev() {
|
||||
for step in (5..18).rev() {
|
||||
let mut prev_value = start_value;
|
||||
for value in (start_value..=ad5680::MAX_VALUE).step_by(1 << step) {
|
||||
match channel {
|
||||
@ -321,8 +313,9 @@ impl Channels {
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
sleep(10);
|
||||
|
||||
let dac_feedback = self.read_dac_feedback_until_stable(channel, ElectricPotential::new::<volt>(0.001));
|
||||
let dac_feedback = self.adc_read(channel, PinsAdcReadTarget::DacVfb, 64);
|
||||
let error = target_voltage - dac_feedback;
|
||||
if error < ElectricPotential::new::<volt>(0.0) {
|
||||
break;
|
||||
@ -398,13 +391,21 @@ impl Channels {
|
||||
|
||||
pub fn get_max_i_pos(&mut self, channel: usize) -> (ElectricCurrent, ElectricCurrent) {
|
||||
let max = ElectricCurrent::new::<ampere>(3.0);
|
||||
let duty = self.get_pwm(channel, PwmPin::MaxIPos);
|
||||
let duty = if self.channel_state(channel).polarity_swapped {
|
||||
self.get_pwm(channel, PwmPin::MaxINeg)
|
||||
} else {
|
||||
self.get_pwm(channel, PwmPin::MaxIPos)
|
||||
};
|
||||
(duty * max, MAX_TEC_I)
|
||||
}
|
||||
|
||||
pub fn get_max_i_neg(&mut self, channel: usize) -> (ElectricCurrent, ElectricCurrent) {
|
||||
let max = ElectricCurrent::new::<ampere>(3.0);
|
||||
let duty = self.get_pwm(channel, PwmPin::MaxINeg);
|
||||
let duty = if self.channel_state(channel).polarity_swapped {
|
||||
self.get_pwm(channel, PwmPin::MaxIPos)
|
||||
} else {
|
||||
self.get_pwm(channel, PwmPin::MaxINeg)
|
||||
};
|
||||
(duty * max, MAX_TEC_I)
|
||||
}
|
||||
|
||||
@ -460,17 +461,38 @@ impl Channels {
|
||||
pub fn set_max_i_pos(&mut self, channel: usize, max_i_pos: ElectricCurrent) -> (ElectricCurrent, ElectricCurrent) {
|
||||
let max = ElectricCurrent::new::<ampere>(3.0);
|
||||
let duty = (max_i_pos.min(MAX_TEC_I).max(ElectricCurrent::zero()) / max).get::<ratio>();
|
||||
let duty = self.set_pwm(channel, PwmPin::MaxIPos, duty);
|
||||
let duty = if self.channel_state(channel).polarity_swapped {
|
||||
self.set_pwm(channel, PwmPin::MaxINeg, duty)
|
||||
} else {
|
||||
self.set_pwm(channel, PwmPin::MaxIPos, duty)
|
||||
};
|
||||
(duty * max, max)
|
||||
}
|
||||
|
||||
pub fn set_max_i_neg(&mut self, channel: usize, max_i_neg: ElectricCurrent) -> (ElectricCurrent, ElectricCurrent) {
|
||||
let max = ElectricCurrent::new::<ampere>(3.0);
|
||||
let duty = (max_i_neg.min(MAX_TEC_I).max(ElectricCurrent::zero()) / max).get::<ratio>();
|
||||
let duty = self.set_pwm(channel, PwmPin::MaxINeg, duty);
|
||||
let duty = if self.channel_state(channel).polarity_swapped {
|
||||
self.set_pwm(channel, PwmPin::MaxIPos, duty)
|
||||
} else {
|
||||
self.set_pwm(channel, PwmPin::MaxINeg, duty)
|
||||
};
|
||||
(duty * max, max)
|
||||
}
|
||||
|
||||
pub fn swap_polarity(&mut self, channel: usize, swapped: bool) {
|
||||
if self.channel_state(channel).polarity_swapped != swapped {
|
||||
let i_set = self.channel_state(channel).i_set;
|
||||
let max_i_pos = self.get_max_i_pos(channel).0;
|
||||
let max_i_neg = self.get_max_i_neg(channel).0;
|
||||
|
||||
self.channel_state(channel).polarity_swapped = swapped;
|
||||
self.set_i(channel, i_set);
|
||||
self.set_max_i_pos(channel, max_i_pos);
|
||||
self.set_max_i_neg(channel, max_i_neg);
|
||||
}
|
||||
}
|
||||
|
||||
fn report(&mut self, channel: usize) -> Report {
|
||||
let i_set = self.get_i(channel);
|
||||
let i_tec = self.adc_read(channel, PinsAdcReadTarget::ITec, 16);
|
||||
|
@ -182,10 +182,7 @@ impl Handler {
|
||||
}
|
||||
|
||||
fn swap_polarity (socket: &mut TcpSocket, channels: &mut Channels, channel: usize, swapped: bool) -> Result<Handler, Error> {
|
||||
channels.channel_state(channel).polarity_swapped = swapped;
|
||||
let channel_state = channels.channel_state(channel);
|
||||
let i_set = channel_state.i_set;
|
||||
channels.set_i(channel, i_set);
|
||||
channels.swap_polarity(channel, swapped);
|
||||
send_line(socket, b"{}");
|
||||
Ok(Handler::Handled)
|
||||
}
|
||||
|
@ -20,6 +20,7 @@ pub struct ChannelConfig {
|
||||
pid_target: f32,
|
||||
pid_engaged: bool,
|
||||
i_set: ElectricCurrent,
|
||||
polarity_swapped: bool,
|
||||
sh: steinhart_hart::Parameters,
|
||||
pwm: PwmLimits,
|
||||
/// uses variant `PostFilter::Invalid` instead of `None` to save space
|
||||
@ -45,7 +46,8 @@ impl ChannelConfig {
|
||||
pid: state.pid.parameters.clone(),
|
||||
pid_target: state.pid.target as f32,
|
||||
pid_engaged: state.pid_engaged,
|
||||
i_set: i_set,
|
||||
i_set,
|
||||
polarity_swapped: state.polarity_swapped,
|
||||
sh: state.sh.clone(),
|
||||
pwm,
|
||||
adc_postfilter,
|
||||
@ -68,6 +70,7 @@ impl ChannelConfig {
|
||||
};
|
||||
let _ = channels.adc.set_postfilter(channel as u8, adc_postfilter);
|
||||
let _ = channels.set_i(channel, self.i_set);
|
||||
channels.swap_polarity(channel, self.polarity_swapped);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -58,7 +58,7 @@ mod hw_rev;
|
||||
|
||||
const HSE: MegaHertz = MegaHertz(8);
|
||||
#[cfg(not(feature = "semihosting"))]
|
||||
const WATCHDOG_INTERVAL: u32 = 1_000;
|
||||
const WATCHDOG_INTERVAL: u32 = 2_000;
|
||||
#[cfg(feature = "semihosting")]
|
||||
const WATCHDOG_INTERVAL: u32 = 30_000;
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user