dac_fix and review #52

Merged
sb10q merged 4 commits from dac_fix into master 2021-01-25 16:40:38 +08:00
9 changed files with 94 additions and 70 deletions

View File

@ -101,7 +101,7 @@ formatted as line-delimited JSON.
| `report mode <off/on>` | Set report mode |
| `pwm` | Show current PWM settings |
| `pwm <0/1> max_i_pos <amp>` | Set PWM duty cycle for **max_i_pos** to *ampere* |
| `pwm <0/1> max_i_neg <amp>` | Set PWM duty cycle for **max_i_neg** to *ampere* |
| `pwm <0/1> max_i_neg <amp>` | Set PWM duty cycle for **max_i_neg** to *- ampere* |
| `pwm <0/1> max_v <volts>` | Set PWM duty cycle for **max_v** to *volt* |
| `pwm <0/1> i_set <amp>` | Disengage PID, set **i_set** DAC to *ampere* |
| `pwm <0/1> pid` | Set PWM to be controlled by PID |
@ -176,9 +176,13 @@ postfilter rate can be tuned with the `postfilter` command.
## Thermo-Electric Cooling (TEC)
- Connect Peltier device 0 to TEC0- and TEC0+.
- Connect Peliter device 1 to TEC1- and TEC1+.
- The GND pin is for shielding not for sinking Peltier currents.
- Connect TEC module device 0 to TEC0- and TEC0+.
- Connect TEC module device 1 to TEC1- and TEC1+.
- The GND pin is for shielding not for sinking TEC module currents.
When using a TEC module with the Thermostat, the Thermostat expects the thermal load (where the thermistor is located) to heat up with a positive software current set point, and cool down with a negative current set point.
Testing heat flow direction with a low set current is recommended before installation of the TEC module.
### Limits
@ -199,6 +203,16 @@ Example: set the maximum voltage of channel 0 to 1.5 V.
pwm 0 max_v 1.5
```
Example: set the maximum negative current of channel 0 to -3 A.
```
pwm 0 max_i_neg 3
```
Example: set the maximum positive current of channel 1 to 3 A.
```
pwm 0 max_i_pos 3
```
### Open-loop mode
To manually control TEC output current, omit the limit parameter of

View File

@ -2,8 +2,6 @@
## Note on hardware setup
When using a TEC module with the Thermostat, the Thermostat expects the thermal load (where the thermistor is connected) to heat up when a positive current flow from the TEC + terminal, through the TEC, to the TEC - terminal, and cool down when the current flows in the reverse direction.
The heat sinking side of the TEC module should be thermally bonded to a large heat-sinking thermal mass to ensure maximum temperature stability, a large optical table had provided good results in tests.
The thermal load under control should be well insulated from the surrounding for maximum stability, closed cell foam had been tested showing good results.
@ -76,6 +74,8 @@ At the end of the test, the ultimate gain `Ku`, oscillation period `Pu` and a fe
Multiple suggested sets of PID parameters based on different calculation rules are displayed. While all sets are expected to work, the different sets trade off response time with overshoot differently, and testing is needed to see which set works best for the system on hand.
With a well designed and constructed setup, the PID parameters calculated by the auto tune utility can provide mK level control stability with no manual tweaking.
With a well designed and constructed setup, the PID parameters calculated by the auto tune utility together with some manual tweaking can yield sub-mK control stability.
![tec3pessen](./assets/tec3pessen.png)
Below shows data captured on an experiment setup, with 300uK stability over 12 hours.
![twelve_hours](./assets/twelve_hours.png)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

BIN
doc/assets/twelve_hours.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

View File

@ -28,18 +28,18 @@ class Series:
self.y_data = self.y_data[drop:]
series = {
'adc': Series(),
'sens': Series(lambda x: x * 0.0001),
'temperature': Series(lambda t: t - target_temperature),
'i_set': Series(),
# 'adc': Series(),
# 'sens': Series(lambda x: x * 0.0001),
'temperature': Series(),
# 'i_set': Series(),
'pid_output': Series(),
'vref': Series(),
'dac_value': Series(),
'dac_feedback': Series(),
'i_tec': Series(),
# 'vref': Series(),
# 'dac_value': Series(),
# 'dac_feedback': Series(),
# 'i_tec': Series(),
'tec_i': Series(),
'tec_u_meas': Series(),
'interval': Series(),
# 'interval': Series(),
}
series_lock = Lock()

View File

@ -1,4 +1,8 @@
use stm32f4xx_hal::hal::digital::v2::OutputPin;
use uom::si::{
f64::ElectricPotential,
electric_potential::volt,
};
use crate::{
ad5680,
ad7172,
@ -12,13 +16,12 @@ pub struct Channel0;
/// Marker type for the second channel
pub struct Channel1;
pub struct Channel<C: ChannelPins> {
pub state: ChannelState,
/// for `i_set`
pub dac: ad5680::Dac<C::DacSpi, C::DacSync>,
/// 1 / Volts
pub dac_factor: f64,
/// Measured vref of MAX driver chip
pub vref_meas: ElectricPotential,
pub shdn: C::Shdn,
pub vref_pin: C::VRefPin,
pub itec_pin: C::ItecPin,
@ -32,12 +35,12 @@ impl<C: ChannelPins> Channel<C> {
let state = ChannelState::new(adc_calibration);
let mut dac = ad5680::Dac::new(pins.dac_spi, pins.dac_sync);
let _ = dac.set(0);
// sensible dummy preset. calibrate_i_set() must be used.
let dac_factor = ad5680::MAX_VALUE as f64 / 5.0;
// sensible dummy preset taken from datasheet. calibrate_dac_value() should be used to override this value.
let vref_meas = ElectricPotential::new::<volt>(1.5);
Channel {
state,
dac, dac_factor,
dac, vref_meas,
shdn: pins.shdn,
vref_pin: pins.vref_pin,
itec_pin: pins.itec_pin,

View File

@ -22,6 +22,8 @@ use crate::{
pub const CHANNELS: usize = 2;
pub const R_SENSE: f64 = 0.05;
// DAC chip outputs 0-5v, which is then passed through a resistor dividor to provide 0-3v range
const DAC_OUT_V_MAX: f64 = 3.0;
// TODO: -pub
pub struct Channels {
@ -106,53 +108,43 @@ impl Channels {
}
/// i_set DAC
fn get_dac(&mut self, channel: usize) -> (ElectricPotential, ElectricPotential) {
let dac_factor = match channel.into() {
0 => self.channel0.dac_factor,
1 => self.channel1.dac_factor,
_ => unreachable!(),
};
fn get_dac(&mut self, channel: usize) -> ElectricPotential {
let voltage = self.channel_state(channel).dac_value;
let max = ElectricPotential::new::<volt>(ad5680::MAX_VALUE as f64 / dac_factor);
(voltage, max)
voltage
}
pub fn get_i(&mut self, channel: usize) -> (ElectricCurrent, ElectricCurrent) {
pub fn get_i(&mut self, channel: usize) -> ElectricCurrent {
let center_point = self.get_center(channel);
let r_sense = ElectricalResistance::new::<ohm>(R_SENSE);
let (voltage, max) = self.get_dac(channel);
let voltage = self.get_dac(channel);
let i_tec = (voltage - center_point) / (10.0 * r_sense);
let max = (max - center_point) / (10.0 * r_sense);
(i_tec, max)
i_tec
}
/// i_set DAC
fn set_dac(&mut self, channel: usize, voltage: ElectricPotential) -> (ElectricPotential, ElectricPotential) {
let dac_factor = match channel.into() {
0 => self.channel0.dac_factor,
1 => self.channel1.dac_factor,
_ => unreachable!(),
};
let value = (voltage.get::<volt>() * dac_factor) as u32;
let value = match channel {
fn set_dac(&mut self, channel: usize, voltage: ElectricPotential) -> ElectricPotential {
let value = ((voltage / ElectricPotential::new::<volt>(DAC_OUT_V_MAX)).get::<ratio>() * (ad5680::MAX_VALUE as f64)) as u32 ;
match channel {
0 => self.channel0.dac.set(value).unwrap(),
1 => self.channel1.dac.set(value).unwrap(),
_ => unreachable!(),
};
let voltage = ElectricPotential::new::<volt>(value as f64 / dac_factor);
self.channel_state(channel).dac_value = voltage;
let max = ElectricPotential::new::<volt>(ad5680::MAX_VALUE as f64 / dac_factor);
(voltage, max)
voltage
}
pub fn set_i(&mut self, channel: usize, i_tec: ElectricCurrent) -> (ElectricCurrent, ElectricCurrent) {
let center_point = self.get_center(channel);
pub fn set_i(&mut self, channel: usize, i_tec: ElectricCurrent) -> ElectricCurrent {
let vref_meas = match channel.into() {
0 => self.channel0.vref_meas,
1 => self.channel1.vref_meas,
_ => unreachable!(),
};
let center_point = vref_meas;
let r_sense = ElectricalResistance::new::<ohm>(R_SENSE);
let voltage = i_tec * 10.0 * r_sense + center_point;
let (voltage, max) = self.set_dac(channel, voltage);
let voltage = self.set_dac(channel, voltage);
let i_tec = (voltage - center_point) / (10.0 * r_sense);
let max = (max - center_point) / (10.0 * r_sense);
(i_tec, max)
i_tec
}
pub fn read_dac_feedback(&mut self, channel: usize) -> ElectricPotential {
@ -255,12 +247,29 @@ impl Channels {
}
}
/// Calibrate the I_SET DAC using the DAC_FB ADC pin.
/// Calibrates the DAC output to match vref of the MAX driver to reduce zero-current offset of the MAX driver output.
///
/// These loops perform a breadth-first search for the DAC setting
/// that will produce a `target_voltage`.
/// 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, channel: usize) {
let target_voltage = ElectricPotential::new::<volt>(2.5);
let samples = 50;
let mut target_voltage = ElectricPotential::new::<volt>(0.0);
for _ in 0..samples {
target_voltage = target_voltage + self.get_center(channel);
}
target_voltage = target_voltage / samples as f64;
let mut start_value = 1;
let mut best_error = ElectricPotential::new::<volt>(100.0);
@ -285,10 +294,10 @@ impl Channels {
best_error = error;
start_value = prev_value;
let dac_factor = value as f64 / dac_feedback.get::<volt>();
let vref = (value as f64 / ad5680::MAX_VALUE as f64) * ElectricPotential::new::<volt>(DAC_OUT_V_MAX);
match channel {
0 => self.channel0.dac_factor = dac_factor,
1 => self.channel1.dac_factor = dac_factor,
0 => self.channel0.vref_meas = vref,
1 => self.channel1.vref_meas = vref,
_ => unreachable!(),
}
}
@ -345,11 +354,10 @@ impl Channels {
}
}
pub fn get_max_v(&mut self, channel: usize) -> (ElectricPotential, ElectricPotential) {
let vref = self.channel_state(channel).vref;
let max = 4.0 * vref;
pub fn get_max_v(&mut self, channel: usize) -> ElectricPotential {
let max = 4.0 * ElectricPotential::new::<volt>(3.3);
let duty = self.get_pwm(channel, PwmPin::MaxV);
(duty * max, max)
duty * max
}
pub fn get_max_i_pos(&mut self, channel: usize) -> (ElectricCurrent, ElectricCurrent) {
@ -402,8 +410,7 @@ impl Channels {
}
pub fn set_max_v(&mut self, channel: usize, max_v: ElectricPotential) -> (ElectricPotential, ElectricPotential) {
let vref = self.channel_state(channel).vref;
let max = 4.0 * vref;
let max = 4.0 * ElectricPotential::new::<volt>(3.3);
let duty = (max_v / max).get::<ratio>();
let duty = self.set_pwm(channel, PwmPin::MaxV, duty);
(duty * max, max)
@ -425,10 +432,10 @@ impl Channels {
fn report(&mut self, channel: usize) -> Report {
let vref = self.channel_state(channel).vref;
let (i_set, _) = self.get_i(channel);
let i_set = self.get_i(channel);
let i_tec = self.read_itec(channel);
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 pid_output = state.pid.last_output.map(|last_output|
ElectricCurrent::new::<ampere>(last_output)
@ -473,8 +480,8 @@ impl Channels {
PwmSummary {
channel,
center: CenterPointJson(self.channel_state(channel).center.clone()),
i_set: self.get_i(channel).into(),
max_v: self.get_max_v(channel).into(),
i_set: (self.get_i(channel), ElectricCurrent::new::<ampere>(3.0)).into(),
max_v: (self.get_max_v(channel), ElectricPotential::new::<volt>(5.0)).into(),
max_i_pos: self.get_max_i_pos(channel).into(),
max_i_neg: self.get_max_i_neg(channel).into(),
}

View File

@ -71,7 +71,7 @@ struct PwmLimits {
impl PwmLimits {
pub fn new(channels: &mut Channels, channel: usize) -> Self {
let (max_v, _) = channels.get_max_v(channel);
let max_v = channels.get_max_v(channel);
let (max_i_pos, _) = channels.get_max_i_pos(channel);
let (max_i_neg, _) = channels.get_max_i_neg(channel);
PwmLimits {

View File

@ -316,7 +316,7 @@ fn main() -> ! {
send_line(&mut socket, b"{}");
}
Command::CenterPoint { channel, center } => {
let (i_tec, _) = channels.get_i(channel);
let i_tec = channels.get_i(channel);
let state = channels.channel_state(channel);
state.center = center;
if !state.pid_engaged {