Compare commits

..

6 Commits

Author SHA1 Message Date
atse 76547be90a i_tec -> i_set
i_tec is reserved for the voltage signal coming out of the MAX1968 chip
for now.
2024-02-14 17:27:12 +08:00
atse 8b975e656e Stop i_set from fluctuating in every report
i_set is a user-provided value that shouldn't fluctuate with every VREF
measurement. Storing i_set as channel state is the simplest way to avoid
that.
2024-02-14 17:21:39 +08:00
atse ae3d8b51d4 Disable feedback current readout on flawed HW Revs
Thermostats v2.2 and below have a noisy and offset feedback current
`tec_i` caused by missing hardware on 2 MAX1968 TEC driver pins:

1. A missing RC filter on the ITEC pin that would have isolated CPU
sampling pulses from the signal; and
2. Some missing buffering on the VREF pin that would have avoided
loading the VREF signal, preventing voltage drops from the nominal 1.5V.

Since the resulting signal `tec_i` derived from these two signals can
have an error of around +/- 100mA, and readback may affect the stability
performance of the Thermostat, disable current readback entirely on
affected hardware revisions for now.

See https://github.com/sinara-hw/Thermostat/issues/117 and
https://github.com/sinara-hw/Thermostat/issues/120.

On hardware revisions v3.x and above, this would be fixed.
2024-01-31 12:12:22 +08:00
atse 17edae44fb README: Proofread fan control documentation 2024-01-30 12:43:19 +08:00
atse 03b4561142 Refactor current_abs_max_tec_i to use uom 2024-01-30 11:41:52 +08:00
atse 631a10938d README: Remove VREF 2024-01-26 17:00:27 +08:00
6 changed files with 70 additions and 64 deletions

View File

@ -106,7 +106,7 @@ formatted as line-delimited JSON.
| `pwm <0/1> i_set <amp>` | Disengage PID, set fixed output current | | `pwm <0/1> i_set <amp>` | Disengage PID, set fixed output current |
| `pwm <0/1> pid` | Let output current to be controlled by the PID | | `pwm <0/1> pid` | Let output current to be controlled by the PID |
| `center <0/1> <volt>` | Set the MAX1968 0A-centerpoint to the specified fixed voltage | | `center <0/1> <volt>` | Set the MAX1968 0A-centerpoint to the specified fixed voltage |
| `center <0/1> vref` | Set the MAX1968 0A-centerpoint to a stable calibrated VREF | | `center <0/1> vref` | Set the MAX1968 0A-centerpoint to measure from VREF |
| `pid` | Show PID configuration | | `pid` | Show PID configuration |
| `pid <0/1> target <deg_celsius>` | Set the PID controller target temperature | | `pid <0/1> target <deg_celsius>` | Set the PID controller target temperature |
| `pid <0/1> kp <value>` | Set proportional gain | | `pid <0/1> kp <value>` | Set proportional gain |
@ -264,7 +264,6 @@ with the following keys.
| `temperature` | Degrees Celsius | Steinhart-Hart conversion result derived from `sens` | | `temperature` | Degrees Celsius | Steinhart-Hart conversion result derived from `sens` |
| `pid_engaged` | Boolean | `true` if in closed-loop mode | | `pid_engaged` | Boolean | `true` if in closed-loop mode |
| `i_set` | Amperes | TEC output current | | `i_set` | Amperes | TEC output current |
| `vref` | Volts | MAX1968 VREF (1.5 V) |
| `dac_value` | Volts | AD5680 output derived from `i_set` | | `dac_value` | Volts | AD5680 output derived from `i_set` |
| `dac_feedback` | Volts | ADC measurement of the AD5680 output | | `dac_feedback` | Volts | ADC measurement of the AD5680 output |
| `i_tec` | Volts | MAX1968 TEC current monitor | | `i_tec` | Volts | MAX1968 TEC current monitor |
@ -272,18 +271,19 @@ 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).
## Fan control ## Fan control
Fan control is available for the thermostat revisions with integrated fan system. For this purpose four commands are available: Fan control commands are available for thermostat revisions with an integrated fan system:
1. `fan` - show fan stats: `fan_pwm`, `abs_max_tec_i`, `auto_mode`, `k_a`, `k_b`, `k_c`. 1. `fan` - show fan stats: `fan_pwm`, `abs_max_tec_i`, `auto_mode`, `k_a`, `k_b`, `k_c`.
2. `fan auto` - enable auto speed controller mode, which correlates with fan curve `fcurve`. 2. `fan auto` - enable auto speed controller mode, where fan speed is controlled by the fan curve `fcurve`.
3. `fan <value>` - set the fan power with the value from `1` to `100` and disable auto mode. There is no way to disable the fan. 3. `fan <value>` - set the fan power with the value from `1` to `100` and disable auto mode. There is no way to completely disable the fan.
Please note that power doesn't correlate with the actual speed linearly. Please note that power doesn't correlate with the actual speed linearly.
4. `fcurve <a> <b> <c>` - set coefficients of the controlling curve `a*x^2 + b*x + c`, where `x` is `abs_max_tec_i/MAX_TEC_I`, 4. `fcurve <a> <b> <c>` - set coefficients of the controlling curve `a*x^2 + b*x + c`, where `x` is `abs_max_tec_i/MAX_TEC_I`, a normalized value in range [0,1],
i.e. receives values from 0 to 1 linearly tied to the maximum current. The controlling curve should produce values from 0 to 1, i.e. the (linear) proportion of current output capacity used, on the channel with the largest current flow. The controlling curve is also clamped to [0,1].
as below and beyond values would be substituted by 0 and 1 respectively. 5. `fcurve default` - restore fan curve coefficients to defaults: `a = 1.0, b = 0.0, c = 0.0`.
5. `fcurve default` - restore fan curve settings to defaults: `a = 1.0, b = 0.0, c = 0.0`.

View File

@ -2,11 +2,13 @@ use smoltcp::time::{Duration, Instant};
use uom::si::{ use uom::si::{
f64::{ f64::{
ElectricPotential, ElectricPotential,
ElectricCurrent,
ElectricalResistance, ElectricalResistance,
ThermodynamicTemperature, ThermodynamicTemperature,
Time, Time,
}, },
electric_potential::volt, electric_potential::volt,
electric_current::ampere,
electrical_resistance::ohm, electrical_resistance::ohm,
thermodynamic_temperature::degree_celsius, thermodynamic_temperature::degree_celsius,
time::millisecond, time::millisecond,
@ -29,6 +31,7 @@ pub struct ChannelState {
/// i_set 0A center point /// i_set 0A center point
pub center: CenterPoint, pub center: CenterPoint,
pub dac_value: ElectricPotential, pub dac_value: ElectricPotential,
pub i_set: ElectricCurrent,
pub pid_engaged: bool, pub pid_engaged: bool,
pub pid: pid::Controller, pub pid: pid::Controller,
pub sh: sh::Parameters, pub sh: sh::Parameters,
@ -44,6 +47,7 @@ impl ChannelState {
adc_interval: Duration::from_millis(100), adc_interval: Duration::from_millis(100),
center: CenterPoint::Vref, center: CenterPoint::Vref,
dac_value: ElectricPotential::new::<volt>(0.0), dac_value: ElectricPotential::new::<volt>(0.0),
i_set: ElectricCurrent::new::<ampere>(0.0),
pid_engaged: false, pid_engaged: false,
pid: pid::Controller::new(pid::Parameters::default()), pid: pid::Controller::new(pid::Parameters::default()),
sh: sh::Parameters::default(), sh: sh::Parameters::default(),

View File

@ -20,7 +20,7 @@ use crate::{
command_handler::JsonBuffer, command_handler::JsonBuffer,
pins, pins,
steinhart_hart, steinhart_hart,
timer, hw_rev,
}; };
pub const CHANNELS: usize = 2; pub const CHANNELS: usize = 2;
@ -29,17 +29,18 @@ pub const R_SENSE: f64 = 0.05;
const DAC_OUT_V_MAX: f64 = 3.0; const DAC_OUT_V_MAX: f64 = 3.0;
// TODO: -pub // TODO: -pub
pub struct Channels { pub struct Channels<'a> {
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 Channels { impl<'a> Channels<'a> {
pub fn new(pins: pins::Pins) -> Self { pub fn new(pins: pins::Pins, hwrev: &'a hw_rev::HWRev) -> 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();
@ -57,7 +58,7 @@ impl Channels {
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 }; let mut channels = Channels { channel0, channel1, adc, pins_adc, pwm, hwrev };
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));
@ -95,16 +96,11 @@ impl Channels {
}) })
} }
/// get the TEC i_set centerpoint /// calculate the TEC i_set centerpoint
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 =>
match channel { self.read_vref(channel),
0 => self.channel0.vref_meas,
1 => self.channel1.vref_meas,
_ => unreachable!(),
}
},
CenterPoint::Override(center_point) => CenterPoint::Override(center_point) =>
ElectricPotential::new::<volt>(center_point.into()), ElectricPotential::new::<volt>(center_point.into()),
} }
@ -117,11 +113,8 @@ impl Channels {
} }
pub fn get_i(&mut self, channel: usize) -> ElectricCurrent { pub fn get_i(&mut self, channel: usize) -> ElectricCurrent {
let center_point = self.get_center(channel); let i_set = self.channel_state(channel).i_set;
let r_sense = ElectricalResistance::new::<ohm>(R_SENSE); i_set
let voltage = self.get_dac(channel);
let i_tec = (voltage - center_point) / (10.0 * r_sense);
i_tec
} }
/// i_set DAC /// i_set DAC
@ -136,13 +129,19 @@ impl Channels {
voltage voltage
} }
pub fn set_i(&mut self, channel: usize, i_tec: ElectricCurrent) -> ElectricCurrent { pub fn set_i(&mut self, channel: usize, i_set: ElectricCurrent) -> ElectricCurrent {
let center_point = self.get_center(channel); 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 r_sense = ElectricalResistance::new::<ohm>(R_SENSE);
let voltage = i_tec * 10.0 * r_sense + center_point; let voltage = i_set * 10.0 * r_sense + center_point;
let voltage = self.set_dac(channel, voltage); let voltage = self.set_dac(channel, voltage);
let i_tec = (voltage - center_point) / (10.0 * r_sense); let i_set = (voltage - center_point) / (10.0 * r_sense);
i_tec self.channel_state(channel).i_set = i_set;
i_set
} }
pub fn read_dac_feedback(&mut self, channel: usize) -> ElectricPotential { pub fn read_dac_feedback(&mut self, channel: usize) -> ElectricPotential {
@ -167,6 +166,17 @@ impl Channels {
} }
} }
pub fn read_dac_feedback_until_stable(&mut self, channel: usize, tolerance: ElectricPotential) -> ElectricPotential {
let mut prev = self.read_dac_feedback(channel);
loop {
let current = self.read_dac_feedback(channel);
if (current - prev).abs() < tolerance {
return current;
}
prev = current;
}
}
pub fn read_itec(&mut self, channel: usize) -> ElectricPotential { pub fn read_itec(&mut self, channel: usize) -> ElectricPotential {
match channel { match channel {
0 => { 0 => {
@ -252,19 +262,15 @@ impl Channels {
/// thermostat. /// thermostat.
pub fn calibrate_dac_value(&mut self, channel: usize) { pub fn calibrate_dac_value(&mut self, channel: usize) {
let samples = 50; let samples = 50;
let target_voltage = { let mut target_voltage = ElectricPotential::new::<volt>(0.0);
let mut target_voltage = ElectricPotential::new::<volt>(0.0); for _ in 0..samples {
for _ in 0..samples { target_voltage = target_voltage + self.get_center(channel);
target_voltage += self.read_vref(channel); }
} target_voltage = target_voltage / samples as f64;
target_voltage /= samples as f64;
target_voltage
};
let mut start_value = 1; let mut start_value = 1;
let mut best_error = ElectricPotential::new::<volt>(100.0); let mut best_error = ElectricPotential::new::<volt>(100.0);
for step in (0..18).rev() { for step in (0..18).rev() {
timer::sleep(5);
let mut prev_value = start_value; let mut prev_value = start_value;
for value in (start_value..=ad5680::MAX_VALUE).step_by(1 << step) { for value in (start_value..=ad5680::MAX_VALUE).step_by(1 << step) {
match channel { match channel {
@ -277,14 +283,7 @@ impl Channels {
_ => unreachable!(), _ => unreachable!(),
} }
let dac_feedback = { let dac_feedback = self.read_dac_feedback_until_stable(channel, ElectricPotential::new::<volt>(0.001));
let mut dac_feedback = ElectricPotential::new::<volt>(0.0);
for _ in 0..samples {
dac_feedback += self.read_dac_feedback(channel);
}
dac_feedback /= samples as f64;
dac_feedback
};
let error = target_voltage - dac_feedback; let error = target_voltage - dac_feedback;
if error < ElectricPotential::new::<volt>(0.0) { if error < ElectricPotential::new::<volt>(0.0) {
break; break;
@ -372,7 +371,7 @@ impl Channels {
// 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) - ElectricPotential::new::<volt>(1.5)) / ElectricalResistance::new::<ohm>(0.4) (self.read_itec(channel) - self.read_vref(channel)) / ElectricalResistance::new::<ohm>(0.4)
} }
// Get voltage across TEC // Get voltage across TEC
@ -430,8 +429,8 @@ impl Channels {
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 = self.read_itec(channel); let i_tec = if self.hwrev.major > 2 {Some(self.read_itec(channel))} else {None};
let tec_i = self.get_tec_i(channel); let tec_i = if self.hwrev.major > 2 {Some(self.get_tec_i(channel))} else {None};
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);
@ -525,9 +524,9 @@ impl Channels {
serde_json_core::to_vec(&summaries) serde_json_core::to_vec(&summaries)
} }
pub fn current_abs_max_tec_i(&mut self) -> f64 { pub fn current_abs_max_tec_i(&mut self) -> ElectricCurrent {
max_by(self.get_tec_i(0).abs().get::<ampere>(), max_by(self.get_tec_i(0).abs(),
self.get_tec_i(1).abs().get::<ampere>(), self.get_tec_i(1).abs(),
|a, b| a.partial_cmp(b).unwrap_or(core::cmp::Ordering::Equal)) |a, b| a.partial_cmp(b).unwrap_or(core::cmp::Ordering::Equal))
} }
} }
@ -544,8 +543,8 @@ pub struct Report {
i_set: ElectricCurrent, i_set: ElectricCurrent,
dac_value: ElectricPotential, dac_value: ElectricPotential,
dac_feedback: ElectricPotential, dac_feedback: ElectricPotential,
i_tec: ElectricPotential, i_tec: Option<ElectricPotential>,
tec_i: ElectricCurrent, tec_i: Option<ElectricCurrent>,
tec_u_meas: ElectricPotential, tec_u_meas: ElectricPotential,
pid_output: ElectricCurrent, pid_output: ElectricCurrent,
} }

View File

@ -207,11 +207,11 @@ impl Handler {
} }
fn set_center_point(socket: &mut TcpSocket, channels: &mut Channels, channel: usize, center: CenterPoint) -> Result<Handler, Error> { fn set_center_point(socket: &mut TcpSocket, channels: &mut Channels, channel: usize, center: CenterPoint) -> Result<Handler, Error> {
let i_tec = channels.get_i(channel); let i_set = channels.get_i(channel);
let state = channels.channel_state(channel); let state = channels.channel_state(channel);
state.center = center; state.center = center;
if !state.pid_engaged { if !state.pid_engaged {
channels.set_i(channel, i_tec); channels.set_i(channel, i_set);
} }
send_line(socket, b"{}"); send_line(socket, b"{}");
Ok(Handler::Handled) Ok(Handler::Handled)

View File

@ -4,7 +4,10 @@ use stm32f4xx_hal::{
pwm::{self, PwmChannels}, pwm::{self, PwmChannels},
pac::TIM8, pac::TIM8,
}; };
use uom::si::{
f64::ElectricCurrent,
electric_current::ampere,
};
use crate::{ use crate::{
hw_rev::HWSettings, hw_rev::HWSettings,
command_handler::JsonBuffer, command_handler::JsonBuffer,
@ -50,8 +53,8 @@ impl FanCtrl {
fan_ctrl fan_ctrl
} }
pub fn cycle(&mut self, abs_max_tec_i: f32) { pub fn cycle(&mut self, abs_max_tec_i: ElectricCurrent) {
self.abs_max_tec_i = abs_max_tec_i; self.abs_max_tec_i = abs_max_tec_i.get::<ampere>() as f32;
if self.fan_auto && self.hw_settings.fan_available { if self.fan_auto && self.hw_settings.fan_available {
let scaled_current = self.abs_max_tec_i / MAX_TEC_I; let scaled_current = self.abs_max_tec_i / MAX_TEC_I;
// do not limit upper bound, as it will be limited in the set_pwm() // do not limit upper bound, as it will be limited in the set_pwm()

View File

@ -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); let mut channels = Channels::new(pins, &hwrev);
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)) =>
@ -185,7 +185,7 @@ fn main() -> ! {
server.for_each(|_, session| session.set_report_pending(channel.into())); server.for_each(|_, session| session.set_report_pending(channel.into()));
} }
fan_ctrl.cycle(channels.current_abs_max_tec_i() as f32); fan_ctrl.cycle(channels.current_abs_max_tec_i());
if channels.pid_engaged() { if channels.pid_engaged() {
leds.g3.on(); leds.g3.on();