Compare commits

..

3 Commits

Author SHA1 Message Date
8669020b42 README: polarity_swapped 2024-08-21 10:14:23 +08:00
11753bdf86 add swap status to the report
Signed-off-by: Egor Savkin <es@m-labs.hk>
2024-08-20 17:55:01 +08:00
419586583f Add command for flipping output polarity
Needed for IDC cable connections, since the IDC connector and front
panel connectors have flipped polarities.
2024-08-20 17:54:34 +08:00
7 changed files with 45 additions and 71 deletions

3
.gitignore vendored
View File

@ -1,5 +1,2 @@
target/
result
*.bin
__pycache__/

View File

@ -256,22 +256,21 @@ 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 |
| `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 |
| 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 |
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].

View File

@ -63,7 +63,7 @@
name = "thermostat-dev-shell";
packages = with pkgs; [
rust llvm
openocd dfu-util rlwrap
openocd dfu-util
] ++ (with python3Packages; [
numpy matplotlib
]);

View File

@ -22,7 +22,6 @@ use crate::{
pins::{self, Channel0VRef, Channel1VRef},
steinhart_hart,
};
use crate::timer::sleep;
pub enum PinsAdcReadTarget {
VREF,
@ -153,13 +152,11 @@ impl Channels {
}
pub fn set_i(&mut self, channel: usize, i_set: ElectricCurrent) -> ElectricCurrent {
let i_set = i_set.min(MAX_TEC_I).max(-MAX_TEC_I);
let mut i_set = i_set.min(MAX_TEC_I).max(-MAX_TEC_I);
self.channel_state(channel).i_set = i_set;
let negate = if self.channel_state(channel).polarity_swapped {
-1.0
} else {
1.0
};
if self.channel_state(channel).polarity_swapped {
i_set = -i_set;
}
let vref_meas = match channel.into() {
0 => self.channel0.vref_meas,
1 => self.channel1.vref_meas,
@ -167,9 +164,9 @@ impl Channels {
};
let center_point = vref_meas;
let r_sense = ElectricalResistance::new::<ohm>(R_SENSE);
let voltage = negate * i_set * 10.0 * r_sense + center_point;
let voltage = i_set * 10.0 * r_sense + center_point;
let voltage = self.set_dac(channel, voltage);
let i_set = negate * (voltage - center_point) / (10.0 * r_sense);
let i_set = (voltage - center_point) / (10.0 * r_sense);
i_set
}
@ -275,6 +272,17 @@ 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.
@ -301,7 +309,7 @@ impl Channels {
let mut start_value = 1;
let mut best_error = ElectricPotential::new::<volt>(100.0);
for step in (5..18).rev() {
for step in (0..18).rev() {
let mut prev_value = start_value;
for value in (start_value..=ad5680::MAX_VALUE).step_by(1 << step) {
match channel {
@ -313,9 +321,8 @@ impl Channels {
}
_ => unreachable!(),
}
sleep(10);
let dac_feedback = self.adc_read(channel, PinsAdcReadTarget::DacVfb, 64);
let dac_feedback = self.read_dac_feedback_until_stable(channel, ElectricPotential::new::<volt>(0.001));
let error = target_voltage - dac_feedback;
if error < ElectricPotential::new::<volt>(0.0) {
break;
@ -391,21 +398,13 @@ impl Channels {
pub fn get_max_i_pos(&mut self, channel: usize) -> (ElectricCurrent, ElectricCurrent) {
let max = ElectricCurrent::new::<ampere>(3.0);
let duty = if self.channel_state(channel).polarity_swapped {
self.get_pwm(channel, PwmPin::MaxINeg)
} else {
self.get_pwm(channel, PwmPin::MaxIPos)
};
let duty = 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 = if self.channel_state(channel).polarity_swapped {
self.get_pwm(channel, PwmPin::MaxIPos)
} else {
self.get_pwm(channel, PwmPin::MaxINeg)
};
let duty = self.get_pwm(channel, PwmPin::MaxINeg);
(duty * max, MAX_TEC_I)
}
@ -461,38 +460,17 @@ 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 = if self.channel_state(channel).polarity_swapped {
self.set_pwm(channel, PwmPin::MaxINeg, duty)
} else {
self.set_pwm(channel, PwmPin::MaxIPos, duty)
};
let duty = 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 = if self.channel_state(channel).polarity_swapped {
self.set_pwm(channel, PwmPin::MaxIPos, duty)
} else {
self.set_pwm(channel, PwmPin::MaxINeg, duty)
};
let duty = 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);

View File

@ -182,7 +182,10 @@ impl Handler {
}
fn swap_polarity (socket: &mut TcpSocket, channels: &mut Channels, channel: usize, swapped: bool) -> Result<Handler, Error> {
channels.swap_polarity(channel, swapped);
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);
send_line(socket, b"{}");
Ok(Handler::Handled)
}

View File

@ -20,7 +20,6 @@ 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
@ -46,8 +45,7 @@ impl ChannelConfig {
pid: state.pid.parameters.clone(),
pid_target: state.pid.target as f32,
pid_engaged: state.pid_engaged,
i_set,
polarity_swapped: state.polarity_swapped,
i_set: i_set,
sh: state.sh.clone(),
pwm,
adc_postfilter,
@ -70,7 +68,6 @@ 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);
}
}

View File

@ -58,7 +58,7 @@ mod hw_rev;
const HSE: MegaHertz = MegaHertz(8);
#[cfg(not(feature = "semihosting"))]
const WATCHDOG_INTERVAL: u32 = 2_000;
const WATCHDOG_INTERVAL: u32 = 1_000;
#[cfg(feature = "semihosting")]
const WATCHDOG_INTERVAL: u32 = 30_000;