Implemented auto fan control
Signed-off-by: Egor Savkin <es@m-labs.hk>
This commit is contained in:
parent
583d06a78b
commit
66143d2373
12
README.md
12
README.md
@ -125,7 +125,7 @@ formatted as line-delimited JSON.
|
|||||||
| `dfu` | Reset device and enters USB device firmware update (DFU) mode |
|
| `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 |
|
| `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` | 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
|
## USB
|
||||||
@ -271,3 +271,13 @@ with the following keys.
|
|||||||
## 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 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.
|
||||||
|
@ -26,6 +26,9 @@ pub const CHANNELS: usize = 2;
|
|||||||
pub const R_SENSE: f64 = 0.05;
|
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
|
// 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 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)]
|
#[derive(Serialize, Copy, Clone)]
|
||||||
pub struct HWRev {
|
pub struct HWRev {
|
||||||
@ -41,7 +44,8 @@ pub struct Channels {
|
|||||||
/// stm32f4 integrated adc
|
/// stm32f4 integrated adc
|
||||||
pins_adc: pins::PinsAdc,
|
pins_adc: pins::PinsAdc,
|
||||||
pub pwm: pins::PwmPins,
|
pub pwm: pins::PwmPins,
|
||||||
pub hw_rev: HWRev
|
hw_rev: HWRev,
|
||||||
|
fan_auto: bool
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Channels {
|
impl Channels {
|
||||||
@ -63,7 +67,8 @@ 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, 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 {
|
for channel in 0..CHANNELS {
|
||||||
channels.channel_state(channel).vref = channels.read_vref(channel);
|
channels.channel_state(channel).vref = channels.read_vref(channel);
|
||||||
channels.calibrate_dac_value(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
|
(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 {
|
fn set<P: hal::PwmPin<Duty=u16>>(pin: &mut P, duty: f64) -> f64 {
|
||||||
let max = pin.get_max_duty();
|
let max = pin.get_max_duty();
|
||||||
let value = ((duty * (max as f64)) as u16).min(max);
|
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 {
|
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)
|
self.set_pwm(0, PwmPin::Fan, duty)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_fan_pwm(&mut self) -> u32 {
|
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 {
|
fn report(&mut self, channel: usize) -> Report {
|
||||||
@ -557,12 +566,19 @@ impl Channels {
|
|||||||
serde_json_core::to_vec(&summaries)
|
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> {
|
pub fn fan_summary(&mut self, tacho: Option<u32>) -> Result<JsonBuffer, serde_json_core::ser::Error> {
|
||||||
if self.fan_available() {
|
if self.fan_available() {
|
||||||
let summary = FanSummary {
|
let summary = FanSummary {
|
||||||
fan_pwm: self.get_fan_pwm(),
|
fan_pwm: self.get_fan_pwm(),
|
||||||
tacho: tacho.unwrap_or(u32::MAX),
|
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)
|
serde_json_core::to_vec(&summary)
|
||||||
} else {
|
} else {
|
||||||
@ -570,6 +586,16 @@ impl Channels {
|
|||||||
serde_json_core::to_vec(&summary)
|
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>;
|
type JsonBuffer = Vec<u8, U1024>;
|
||||||
@ -650,4 +676,5 @@ pub struct FanSummary {
|
|||||||
fan_pwm: u32,
|
fan_pwm: u32,
|
||||||
tacho: u32,
|
tacho: u32,
|
||||||
abs_max_tec_i: f64,
|
abs_max_tec_i: f64,
|
||||||
|
auto_mode: bool,
|
||||||
}
|
}
|
||||||
|
@ -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> {
|
fn fan (socket: &mut TcpSocket, channels: &mut Channels, fan_pwm: Option<u32>, tacho_value: Option<u32>) -> Result<Handler, Error> {
|
||||||
match fan_pwm {
|
match fan_pwm {
|
||||||
Some(val) => {
|
Some(val) => {
|
||||||
|
channels.set_fan_auto_mode(val == 0);
|
||||||
channels.set_fan_pwm(val);
|
channels.set_fan_pwm(val);
|
||||||
Ok(Handler::Handled)
|
|
||||||
},
|
},
|
||||||
None => {
|
None => {
|
||||||
match channels.fan_summary(tacho_value) {
|
match channels.fan_summary(tacho_value) {
|
||||||
Ok(buf) => {
|
Ok(buf) => {
|
||||||
send_line(socket, &buf);
|
send_line(socket, &buf);
|
||||||
|
return Ok(Handler::Handled);
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("unable to serialize fan summary: {:?}", e);
|
error!("unable to serialize fan summary: {:?}", e);
|
||||||
@ -361,9 +362,10 @@ impl Handler {
|
|||||||
return Err(Error::ReportError);
|
return Err(Error::ReportError);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
Ok(Handler::Handled)
|
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
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> {
|
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> {
|
||||||
|
@ -303,8 +303,7 @@ fn pwm_setup(input: &[u8]) -> IResult<&[u8], Result<(PwmPin, f64), Error>> {
|
|||||||
)
|
)
|
||||||
),
|
),
|
||||||
result_with_pin(PwmPin::MaxV)
|
result_with_pin(PwmPin::MaxV)
|
||||||
),
|
))
|
||||||
)
|
|
||||||
)(input)
|
)(input)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -19,14 +19,14 @@ use stm32f4xx_hal::{
|
|||||||
stm32::{CorePeripherals, Peripherals, SCB},
|
stm32::{CorePeripherals, Peripherals, SCB},
|
||||||
time::{U32Ext, MegaHertz},
|
time::{U32Ext, MegaHertz},
|
||||||
watchdog::IndependentWatchdog,
|
watchdog::IndependentWatchdog,
|
||||||
|
gpio::{Edge, ExtiPin},
|
||||||
|
syscfg::SysCfgExt
|
||||||
};
|
};
|
||||||
use smoltcp::{
|
use smoltcp::{
|
||||||
time::Instant,
|
time::Instant,
|
||||||
socket::TcpSocket,
|
socket::TcpSocket,
|
||||||
wire::EthernetAddress,
|
wire::EthernetAddress,
|
||||||
};
|
};
|
||||||
use stm32f4xx_hal::gpio::{Edge, ExtiPin};
|
|
||||||
use stm32f4xx_hal::syscfg::SysCfgExt;
|
|
||||||
|
|
||||||
mod init_log;
|
mod init_log;
|
||||||
use init_log::init_log;
|
use init_log::init_log;
|
||||||
@ -220,6 +220,8 @@ fn main() -> ! {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
channels.fan_ctrl();
|
||||||
|
|
||||||
cortex_m::interrupt::free(net::clear_pending);
|
cortex_m::interrupt::free(net::clear_pending);
|
||||||
server.poll(instant)
|
server.poll(instant)
|
||||||
.unwrap_or_else(|e| {
|
.unwrap_or_else(|e| {
|
||||||
|
Loading…
Reference in New Issue
Block a user