Implemented auto fan control

Signed-off-by: Egor Savkin <es@m-labs.hk>
This commit is contained in:
Egor Savkin 2022-12-20 14:46:18 +08:00
parent 583d06a78b
commit 66143d2373
5 changed files with 54 additions and 14 deletions

View File

@ -125,7 +125,7 @@ formatted as line-delimited JSON.
| `dfu` | Reset device and enters USB device firmware update (DFU) mode |
| `ipv4 <X.X.X.X/L> [Y.Y.Y.Y]` | Configure IPv4 address, netmask length, and optional default gateway |
| `fan` | Show current fan settings and sensors' measurements |
| `fan <value>` | Set fan power with values from 0 to 100, where 0 is auto mode (TODO) |
| `fan <value>` | Set fan power with values from 0 to 100, where 0 is auto mode |
## USB
@ -271,3 +271,13 @@ with the following keys.
## 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).
## Fan control
Fan control is available for the thermostat revisions with integrated fan system. For this purpose two commands are available:
1. `fan` - show fan stats: `fan_pwm`, `tacho`, `abs_max_tec_i`, `auto_mode`. Please note that `tacho` shows *approximate* value, which
linearly correlates with the actual fan speed.
2. `fan <value>` - set the fan power with the value from `0` to `100`. Since there is no hardware way to disable the fan,
`0` value is used for enabling automatic fan control mode, which correlates with the square of the TEC's current.
Values from `1` to `100` are used for setting the power from minimum to maximum respectively.
Please note that power doesn't correlate with the actual speed linearly.

View File

@ -26,6 +26,9 @@ 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;
const MAX_TEC_I: f64 = 3.0; // as stated in the schemes
const MAX_FAN_PWM: f64 = 100.0;
const MIN_FAN_PWM: f64 = 1.0;
#[derive(Serialize, Copy, Clone)]
pub struct HWRev {
@ -41,7 +44,8 @@ pub struct Channels {
/// stm32f4 integrated adc
pins_adc: pins::PinsAdc,
pub pwm: pins::PwmPins,
pub hw_rev: HWRev
hw_rev: HWRev,
fan_auto: bool
}
impl Channels {
@ -63,7 +67,8 @@ impl Channels {
let channel1 = Channel::new(pins.channel1, adc_calibration1);
let pins_adc = pins.pins_adc;
let pwm = pins.pwm;
let mut channels = Channels { channel0, channel1, adc, pins_adc, pwm, hw_rev: Self::detect_hw_rev(&pins.hwrev) };
let mut channels = Channels { channel0, channel1, adc, pins_adc, pwm,
hw_rev: Self::detect_hw_rev(&pins.hwrev), fan_auto: true };
for channel in 0..CHANNELS {
channels.channel_state(channel).vref = channels.read_vref(channel);
channels.calibrate_dac_value(channel);
@ -407,7 +412,7 @@ impl Channels {
(self.read_tec_u_meas(channel) - ElectricPotential::new::<volt>(1.5)) * 4.0
}
pub fn set_pwm(&mut self, channel: usize, pin: PwmPin, duty: f64) -> f64 {
fn set_pwm(&mut self, channel: usize, pin: PwmPin, duty: f64) -> f64 {
fn set<P: hal::PwmPin<Duty=u16>>(pin: &mut P, duty: f64) -> f64 {
let max = pin.get_max_duty();
let value = ((duty * (max as f64)) as u16).min(max);
@ -458,12 +463,16 @@ impl Channels {
}
pub fn set_fan_pwm(&mut self, fan_pwm: u32) -> f64 {
let duty = fan_pwm as f64 / 100.0;
let duty = fan_pwm as f64 / MAX_FAN_PWM;
self.set_pwm(0, PwmPin::Fan, duty)
}
pub fn get_fan_pwm(&mut self) -> u32 {
(self.get_pwm(0, PwmPin::Fan) * 100.0) as u32
(self.get_pwm(0, PwmPin::Fan) * MAX_FAN_PWM) as u32
}
pub fn set_fan_auto_mode(&mut self, fan_auto: bool) {
self.fan_auto = fan_auto;
}
fn report(&mut self, channel: usize) -> Report {
@ -557,12 +566,19 @@ impl Channels {
serde_json_core::to_vec(&summaries)
}
fn current_abs_max_tec_i(&mut self) -> f64 {
max_by(self.get_tec_i(0).abs().get::<ampere>(),
self.get_tec_i(1).abs().get::<ampere>(),
|a, b| a.partial_cmp(b).unwrap_or(core::cmp::Ordering::Equal))
}
pub fn fan_summary(&mut self, tacho: Option<u32>) -> Result<JsonBuffer, serde_json_core::ser::Error> {
if self.fan_available() {
let summary = FanSummary {
fan_pwm: self.get_fan_pwm(),
tacho: tacho.unwrap_or(u32::MAX),
abs_max_tec_i: max_by(self.get_tec_i(0).abs().value, self.get_tec_i(1).abs().value, |a, b| a.partial_cmp(b).unwrap())
abs_max_tec_i: self.current_abs_max_tec_i(),
auto_mode: self.fan_auto
};
serde_json_core::to_vec(&summary)
} else {
@ -570,6 +586,16 @@ impl Channels {
serde_json_core::to_vec(&summary)
}
}
pub fn fan_ctrl(&mut self) {
if self.fan_auto && self.fan_available() {
let scaled_current = self.current_abs_max_tec_i() / MAX_TEC_I;
let pwm = max_by(scaled_current * scaled_current * MAX_FAN_PWM,
MIN_FAN_PWM,
|a, b| a.partial_cmp(b).unwrap_or(core::cmp::Ordering::Equal)) as u32;
self.set_fan_pwm(pwm);
}
}
}
type JsonBuffer = Vec<u8, U1024>;
@ -650,4 +676,5 @@ pub struct FanSummary {
fan_pwm: u32,
tacho: u32,
abs_max_tec_i: f64,
auto_mode: bool,
}

View File

@ -347,13 +347,14 @@ impl Handler {
fn fan (socket: &mut TcpSocket, channels: &mut Channels, fan_pwm: Option<u32>, tacho_value: Option<u32>) -> Result<Handler, Error> {
match fan_pwm {
Some(val) => {
channels.set_fan_auto_mode(val == 0);
channels.set_fan_pwm(val);
Ok(Handler::Handled)
},
None => {
match channels.fan_summary(tacho_value) {
Ok(buf) => {
send_line(socket, &buf);
return Ok(Handler::Handled);
}
Err(e) => {
error!("unable to serialize fan summary: {:?}", e);
@ -361,10 +362,11 @@ impl Handler {
return Err(Error::ReportError);
}
};
}
};
send_line(socket, b"{}");
Ok(Handler::Handled)
}
}
}
pub fn handle_command (command: Command, socket: &mut TcpSocket, channels: &mut Channels, session: &Session, leds: &mut Leds, store: &mut FlashStore, ipv4_config: &mut Ipv4Config, tacho_value: Option<u32>) -> Result<Self, Error> {
match command {

View File

@ -303,8 +303,7 @@ fn pwm_setup(input: &[u8]) -> IResult<&[u8], Result<(PwmPin, f64), Error>> {
)
),
result_with_pin(PwmPin::MaxV)
),
)
))
)(input)
}

View File

@ -19,14 +19,14 @@ use stm32f4xx_hal::{
stm32::{CorePeripherals, Peripherals, SCB},
time::{U32Ext, MegaHertz},
watchdog::IndependentWatchdog,
gpio::{Edge, ExtiPin},
syscfg::SysCfgExt
};
use smoltcp::{
time::Instant,
socket::TcpSocket,
wire::EthernetAddress,
};
use stm32f4xx_hal::gpio::{Edge, ExtiPin};
use stm32f4xx_hal::syscfg::SysCfgExt;
mod init_log;
use init_log::init_log;
@ -220,6 +220,8 @@ fn main() -> ! {
}
}
channels.fan_ctrl();
cortex_m::interrupt::free(net::clear_pending);
server.poll(instant)
.unwrap_or_else(|e| {