Support fan PWM settings #73
70
README.md
70
README.md
|
@ -94,40 +94,40 @@ The scope of this setting is per TCP session.
|
||||||
Send commands as simple text string terminated by `\n`. Responses are
|
Send commands as simple text string terminated by `\n`. Responses are
|
||||||
formatted as line-delimited JSON.
|
formatted as line-delimited JSON.
|
||||||
|
|
||||||
| Syntax | Function |
|
| Syntax | Function |
|
||||||
|----------------------------------|---------------------------------------------------------------------------|
|
|----------------------------------|-------------------------------------------------------------------------------|
|
||||||
| `report` | Show current input |
|
| `report` | Show current input |
|
||||||
| `report mode` | Show current report mode |
|
| `report mode` | Show current report mode |
|
||||||
| `report mode <off/on>` | Set report mode |
|
| `report mode <off/on>` | Set report mode |
|
||||||
| `pwm` | Show current PWM settings |
|
| `pwm` | Show current PWM settings |
|
||||||
| `pwm <0/1> max_i_pos <amp>` | Set maximum positive output current |
|
| `pwm <0/1> max_i_pos <amp>` | Set maximum positive output current |
|
||||||
| `pwm <0/1> max_i_neg <amp>` | Set maximum negative output current |
|
| `pwm <0/1> max_i_neg <amp>` | Set maximum negative output current |
|
||||||
| `pwm <0/1> max_v <volt>` | Set maximum output voltage |
|
| `pwm <0/1> max_v <volt>` | Set maximum output voltage |
|
||||||
| `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 measure from 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 |
|
||||||
| `pid <0/1> ki <value>` | Set integral gain |
|
| `pid <0/1> ki <value>` | Set integral gain |
|
||||||
| `pid <0/1> kd <value>` | Set differential gain |
|
| `pid <0/1> kd <value>` | Set differential gain |
|
||||||
| `pid <0/1> output_min <amp>` | Set mininum output |
|
| `pid <0/1> output_min <amp>` | Set mininum output |
|
||||||
| `pid <0/1> output_max <amp>` | Set maximum output |
|
| `pid <0/1> output_max <amp>` | Set maximum output |
|
||||||
| `s-h` | Show Steinhart-Hart equation parameters |
|
| `s-h` | Show Steinhart-Hart equation parameters |
|
||||||
| `s-h <0/1> <t0/b/r0> <value>` | Set Steinhart-Hart parameter for a channel |
|
| `s-h <0/1> <t0/b/r0> <value>` | Set Steinhart-Hart parameter for a channel |
|
||||||
| `postfilter` | Show postfilter settings |
|
| `postfilter` | Show postfilter settings |
|
||||||
| `postfilter <0/1> off` | Disable postfilter |
|
| `postfilter <0/1> off` | Disable postfilter |
|
||||||
| `postfilter <0/1> rate <rate>` | Set postfilter output data rate |
|
| `postfilter <0/1> rate <rate>` | Set postfilter output data rate |
|
||||||
| `load [0/1]` | Restore configuration for channel all/0/1 from flash |
|
| `load [0/1]` | Restore configuration for channel all/0/1 from flash |
|
||||||
| `save [0/1]` | Save configuration for channel all/0/1 to flash |
|
| `save [0/1]` | Save configuration for channel all/0/1 to flash |
|
||||||
| `reset` | Reset the device |
|
| `reset` | Reset the device |
|
||||||
| `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 |
|
| `fan <value>` | Set fan power with values from 0 to 100, where 0 is auto mode |
|
||||||
| `fcurve <a> <b> <c>` | Set fan controller coefficients (see *Fan control* section) |
|
| `fcurve <a> <b> <c>` | Set fan controller curve coefficients (see *Fan control* section) |
|
||||||
| `fan-restore` | Set fan controller coefficients to defaults (see *Fan control* section) |
|
| `fcurve-restore` | Set fan controller curve coefficients to defaults (see *Fan control* section) |
|
||||||
|
|
||||||
|
|
||||||
## USB
|
## USB
|
||||||
|
@ -286,4 +286,4 @@ Please note that power doesn't correlate with the actual speed linearly.
|
||||||
3. `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`,
|
3. `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`,
|
||||||
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. receives values from 0 to 1 linearly tied to the maximum current. The controlling curve should produce values from 0 to 1,
|
||||||
as below and beyond values would be substituted by 0 and 1 respectively.
|
as below and beyond values would be substituted by 0 and 1 respectively.
|
||||||
4. `fan-restore` - restore fan settings to defaults: `auto = true, a = 1.0, b = 0.0, c = 0.04`.
|
4. `fcurve-restore` - restore fan settings to defaults: `auto = true, a = 1.0, b = 0.0, c = 0.00`.
|
||||||
|
|
|
@ -342,7 +342,7 @@ impl Handler {
|
||||||
Ok(Handler::Reset)
|
Ok(Handler::Reset)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn fan (socket: &mut TcpSocket, fan_pwm: Option<u32>, fan_ctrl: &mut FanCtrl) -> Result<Handler, Error> {
|
fn fan(socket: &mut TcpSocket, fan_pwm: Option<u32>, fan_ctrl: &mut FanCtrl) -> Result<Handler, Error> {
|
||||||
match fan_pwm {
|
match fan_pwm {
|
||||||
Some(val) => {
|
Some(val) => {
|
||||||
fan_ctrl.set_auto_mode(val == 0);
|
fan_ctrl.set_auto_mode(val == 0);
|
||||||
|
@ -366,43 +366,43 @@ impl Handler {
|
||||||
Ok(Handler::Handled)
|
Ok(Handler::Handled)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn fan_coeff (socket: &mut TcpSocket, fan_ctrl: &mut FanCtrl, k_a: f64, k_b: f64, k_c: f64) -> Result<Handler, Error> {
|
fn fan_curve(socket: &mut TcpSocket, fan_ctrl: &mut FanCtrl, k_a: f64, k_b: f64, k_c: f64) -> Result<Handler, Error> {
|
||||||
fan_ctrl.set_coefficients(k_a, k_b, k_c);
|
fan_ctrl.set_curve(k_a, k_b, k_c);
|
||||||
send_line(socket, b"{}");
|
send_line(socket, b"{}");
|
||||||
Ok(Handler::Handled)
|
Ok(Handler::Handled)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn fan_defaults (socket: &mut TcpSocket, fan_ctrl: &mut FanCtrl) -> Result<Handler, Error> {
|
fn fan_defaults(socket: &mut TcpSocket, fan_ctrl: &mut FanCtrl) -> Result<Handler, Error> {
|
||||||
fan_ctrl.restore_defaults();
|
fan_ctrl.restore_defaults();
|
||||||
send_line(socket, b"{}");
|
send_line(socket, b"{}");
|
||||||
Ok(Handler::Handled)
|
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, fan_ctrl: &mut FanCtrl) -> Result<Self, Error> {
|
pub fn handle_command(command: Command, socket: &mut TcpSocket, session: &Session, leds: &mut Leds, store: &mut FlashStore, ipv4_config: &mut Ipv4Config, fan_ctrl: &mut FanCtrl) -> Result<Self, Error> {
|
||||||
match command {
|
match command {
|
||||||
Command::Quit => Ok(Handler::CloseSocket),
|
Command::Quit => Ok(Handler::CloseSocket),
|
||||||
Command::Reporting(_reporting) => Handler::reporting(socket),
|
Command::Reporting(_reporting) => Handler::reporting(socket),
|
||||||
Command::Show(ShowCommand::Reporting) => Handler::show_report_mode(socket, session),
|
Command::Show(ShowCommand::Reporting) => Handler::show_report_mode(socket, session),
|
||||||
Command::Show(ShowCommand::Input) => Handler::show_report(socket, channels),
|
Command::Show(ShowCommand::Input) => Handler::show_report(socket, &mut fan_ctrl.channels),
|
||||||
Command::Show(ShowCommand::Pid) => Handler::show_pid(socket, channels),
|
Command::Show(ShowCommand::Pid) => Handler::show_pid(socket, &mut fan_ctrl.channels),
|
||||||
Command::Show(ShowCommand::Pwm) => Handler::show_pwm(socket, channels),
|
Command::Show(ShowCommand::Pwm) => Handler::show_pwm(socket, &mut fan_ctrl.channels),
|
||||||
Command::Show(ShowCommand::SteinhartHart) => Handler::show_steinhart_hart(socket, channels),
|
Command::Show(ShowCommand::SteinhartHart) => Handler::show_steinhart_hart(socket, &mut fan_ctrl.channels),
|
||||||
Command::Show(ShowCommand::PostFilter) => Handler::show_post_filter(socket, channels),
|
Command::Show(ShowCommand::PostFilter) => Handler::show_post_filter(socket, &mut fan_ctrl.channels),
|
||||||
Command::Show(ShowCommand::Ipv4) => Handler::show_ipv4(socket, ipv4_config),
|
Command::Show(ShowCommand::Ipv4) => Handler::show_ipv4(socket, ipv4_config),
|
||||||
Command::PwmPid { channel } => Handler::engage_pid(socket, channels, leds, channel),
|
Command::PwmPid { channel } => Handler::engage_pid(socket, &mut fan_ctrl.channels, leds, channel),
|
||||||
Command::Pwm { channel, pin, value } => Handler::set_pwm(socket, channels, leds, channel, pin, value),
|
Command::Pwm { channel, pin, value } => Handler::set_pwm(socket, &mut fan_ctrl.channels, leds, channel, pin, value),
|
||||||
Command::CenterPoint { channel, center } => Handler::set_center_point(socket, channels, channel, center),
|
Command::CenterPoint { channel, center } => Handler::set_center_point(socket, &mut fan_ctrl.channels, channel, center),
|
||||||
Command::Pid { channel, parameter, value } => Handler::set_pid(socket, channels, channel, parameter, value),
|
Command::Pid { channel, parameter, value } => Handler::set_pid(socket, &mut fan_ctrl.channels, channel, parameter, value),
|
||||||
Command::SteinhartHart { channel, parameter, value } => Handler::set_steinhart_hart(socket, channels, channel, parameter, value),
|
Command::SteinhartHart { channel, parameter, value } => Handler::set_steinhart_hart(socket, &mut fan_ctrl.channels, channel, parameter, value),
|
||||||
Command::PostFilter { channel, rate: None } => Handler::reset_post_filter(socket, channels, channel),
|
Command::PostFilter { channel, rate: None } => Handler::reset_post_filter(socket, &mut fan_ctrl.channels, channel),
|
||||||
Command::PostFilter { channel, rate: Some(rate) } => Handler::set_post_filter(socket, channels, channel, rate),
|
Command::PostFilter { channel, rate: Some(rate) } => Handler::set_post_filter(socket, &mut fan_ctrl.channels, channel, rate),
|
||||||
Command::Load { channel } => Handler::load_channel(socket, channels, store, channel),
|
Command::Load { channel } => Handler::load_channel(socket, &mut fan_ctrl.channels, store, channel),
|
||||||
Command::Save { channel } => Handler::save_channel(socket, channels, channel, store),
|
Command::Save { channel } => Handler::save_channel(socket, &mut fan_ctrl.channels, channel, store),
|
||||||
Command::Ipv4(config) => Handler::set_ipv4(socket, store, config),
|
Command::Ipv4(config) => Handler::set_ipv4(socket, store, config),
|
||||||
Command::Reset => Handler::reset(channels),
|
Command::Reset => Handler::reset(&mut fan_ctrl.channels),
|
||||||
Command::Dfu => Handler::dfu(channels),
|
Command::Dfu => Handler::dfu(&mut fan_ctrl.channels),
|
||||||
Command::Fan {fan_pwm} => Handler::fan(socket, fan_pwm, fan_ctrl),
|
Command::Fan {fan_pwm} => Handler::fan(socket, fan_pwm, fan_ctrl),
|
||||||
Command::FanCoeff { k_a, k_b, k_c } => Handler::fan_coeff(socket, fan_ctrl, k_a, k_b, k_c),
|
Command::FanCurve { k_a, k_b, k_c } => Handler::fan_curve(socket, fan_ctrl, k_a, k_b, k_c),
|
||||||
Command::FanDefaults => Handler::fan_defaults(socket, fan_ctrl),
|
Command::FanDefaults => Handler::fan_defaults(socket, fan_ctrl),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -182,7 +182,7 @@ pub enum Command {
|
||||||
Fan {
|
Fan {
|
||||||
fan_pwm: Option<u32>
|
fan_pwm: Option<u32>
|
||||||
},
|
},
|
||||||
FanCoeff {
|
FanCurve {
|
||||||
k_a: f64,
|
k_a: f64,
|
||||||
k_b: f64,
|
k_b: f64,
|
||||||
k_c: f64,
|
k_c: f64,
|
||||||
|
@ -546,9 +546,9 @@ fn fan(input: &[u8]) -> IResult<&[u8], Result<Command, Error>> {
|
||||||
Ok((input, result))
|
Ok((input, result))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn fan_coeff(input: &[u8]) -> IResult<&[u8], Result<Command, Error>> {
|
fn fan_curve(input: &[u8]) -> IResult<&[u8], Result<Command, Error>> {
|
||||||
let (input, _) = tag("fcurve")(input)?;
|
let (input, _) = tag("fcurve")(input)?;
|
||||||
let (input, coeffs) = alt((
|
let (input, curve) = alt((
|
||||||
|input| {
|
|input| {
|
||||||
let (input, _) = whitespace(input)?;
|
let (input, _) = whitespace(input)?;
|
||||||
let (input, k_a) = float(input)?;
|
let (input, k_a) = float(input)?;
|
||||||
|
@ -566,8 +566,8 @@ fn fan_coeff(input: &[u8]) -> IResult<&[u8], Result<Command, Error>> {
|
||||||
value(None, end)
|
value(None, end)
|
||||||
))(input)?;
|
))(input)?;
|
||||||
|
|
||||||
let result = match coeffs {
|
let result = match curve {
|
||||||
Some(coeffs) => Ok(Command::FanCoeff { k_a: coeffs.0, k_b: coeffs.1, k_c: coeffs.2 }),
|
Some(curve) => Ok(Command::FanCurve { k_a: curve.0, k_b: curve.1, k_c: curve.2 }),
|
||||||
None => Err(Error::ParseFloat)
|
None => Err(Error::ParseFloat)
|
||||||
};
|
};
|
||||||
Ok((input, result))
|
Ok((input, result))
|
||||||
|
@ -586,9 +586,9 @@ fn command(input: &[u8]) -> IResult<&[u8], Result<Command, Error>> {
|
||||||
steinhart_hart,
|
steinhart_hart,
|
||||||
postfilter,
|
postfilter,
|
||||||
value(Ok(Command::Dfu), tag("dfu")),
|
value(Ok(Command::Dfu), tag("dfu")),
|
||||||
value(Ok(Command::FanDefaults), tag("fan-restore")),
|
value(Ok(Command::FanDefaults), tag("fcurve-restore")),
|
||||||
fan,
|
fan,
|
||||||
fan_coeff,
|
fan_curve,
|
||||||
))(input)
|
))(input)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
147
src/fan_ctrl.rs
147
src/fan_ctrl.rs
|
@ -1,4 +1,3 @@
|
||||||
use core::{cmp::max_by};
|
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use stm32f4xx_hal::{
|
use stm32f4xx_hal::{
|
||||||
pwm::{self, PwmChannels},
|
pwm::{self, PwmChannels},
|
||||||
|
@ -21,15 +20,33 @@ use crate::{
|
||||||
pub type FanPin = PwmChannels<TIM8, pwm::C4>;
|
pub type FanPin = PwmChannels<TIM8, pwm::C4>;
|
||||||
pub type TachoPin = PC8<Input<Floating>>;
|
pub type TachoPin = PC8<Input<Floating>>;
|
||||||
|
|
||||||
const MAX_TEC_I: f64 = 3.0;
|
|
||||||
// as stated in the schematics
|
// as stated in the schematics
|
||||||
const MAX_FAN_PWM: f64 = 100.0;
|
const MAX_TEC_I: f64 = 3.0;
|
||||||
const MIN_FAN_PWM: f64 = 1.0;
|
|
||||||
|
const MAX_USER_FAN_PWM: f64 = 100.0;
|
||||||
|
|||||||
|
const MIN_USER_FAN_PWM: f64 = 1.0;
|
||||||
|
const MAX_FAN_PWM: f64 = 1.0;
|
||||||
|
// below this value, motor pulse signal is too weak to be registered by tachometer
|
||||||
|
const MIN_FAN_PWM: f64 = 0.05;
|
||||||
|
|
||||||
const TACHO_MEASURE_MS: i64 = 2500;
|
const TACHO_MEASURE_MS: i64 = 2500;
|
||||||
const TACHO_LOW_THRESHOLD: u32 = 100;
|
// by default up to 2 cycles are skipped on changes in PWM output,
|
||||||
|
// and the halt threshold will help detect the failure during these skipped cycles
|
||||||
|
const TACHO_HALT_THRESHOLD: u32 = 250;
|
||||||
|
const TACHO_SKIP_CYCLES: u8 = 2;
|
||||||
|
|
||||||
const DEFAULT_K_A: f64 = 1.0;
|
const DEFAULT_K_A: f64 = 1.0;
|
||||||
const DEFAULT_K_B: f64 = 0.0;
|
const DEFAULT_K_B: f64 = 0.0;
|
||||||
const DEFAULT_K_C: f64 = 0.04;
|
const DEFAULT_K_C: f64 = 0.0;
|
||||||
|
|
||||||
|
// This regression is from 6% to 25% lower than values registered in the experiments.
|
||||||
|
// Actual values would be better estimated by logarithmic regression, but that would require more
|
||||||
|
// runtime computation, and wouldn't give significant correlation difference
|
||||||
|
// (0.996 for log and 0.966 for quadratic regression).
|
||||||
|
const TACHO_REGRESSION_A: f64 = -0.04135128436;
|
||||||
|
const TACHO_REGRESSION_B: f64 = 6.23015531;
|
||||||
|
const TACHO_REGRESSION_C: f64 = 403.6833577;
|
||||||
|
|
||||||
|
|
||||||
#[derive(Serialize, Copy, Clone)]
|
#[derive(Serialize, Copy, Clone)]
|
||||||
pub struct HWRev {
|
pub struct HWRev {
|
||||||
|
@ -41,8 +58,8 @@ pub struct HWRev {
|
||||||
pub enum FanStatus {
|
pub enum FanStatus {
|
||||||
OK,
|
OK,
|
||||||
NotAvailable,
|
NotAvailable,
|
||||||
Stalled,
|
TooSlow,
|
||||||
LowSignal,
|
Halted
|
||||||
}
|
}
|
||||||
|
|
||||||
struct TachoCtrl {
|
struct TachoCtrl {
|
||||||
|
@ -50,10 +67,9 @@ struct TachoCtrl {
|
||||||
tacho_cnt: u32,
|
tacho_cnt: u32,
|
||||||
tacho_value: Option<u32>,
|
tacho_value: Option<u32>,
|
||||||
prev_epoch: i64,
|
prev_epoch: i64,
|
||||||
past_record: u64,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct FanCtrl<'a> {
|
pub struct FanCtrl {
|
||||||
fan: FanPin,
|
fan: FanPin,
|
||||||
tacho: TachoCtrl,
|
tacho: TachoCtrl,
|
||||||
fan_auto: bool,
|
fan_auto: bool,
|
||||||
|
@ -61,12 +77,13 @@ pub struct FanCtrl<'a> {
|
||||||
k_a: f64,
|
k_a: f64,
|
||||||
k_b: f64,
|
k_b: f64,
|
||||||
k_c: f64,
|
k_c: f64,
|
||||||
channels: &'a mut Channels,
|
pub channels: Channels,
|
||||||
last_status: FanStatus
|
last_status: FanStatus,
|
||||||
|
skip_cycles: u8,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> FanCtrl<'a> {
|
impl FanCtrl {
|
||||||
pub fn new(mut fan: FanPin, tacho: TachoPin, channels: &'a mut Channels, exti: &mut EXTI, syscfg: &mut SysCfg) -> Self {
|
pub fn new(mut fan: FanPin, tacho: TachoPin, channels: Channels, exti: &mut EXTI, syscfg: &mut SysCfg) -> Self {
|
||||||
sb10q
commented
this doesn't need to be this doesn't need to be ``pub``
|
|||||||
let available = channels.hwrev.fan_available();
|
let available = channels.hwrev.fan_available();
|
||||||
|
|
||||||
let mut tacho_ctrl = TachoCtrl::new(tacho);
|
let mut tacho_ctrl = TachoCtrl::new(tacho);
|
||||||
|
@ -85,17 +102,20 @@ impl<'a> FanCtrl<'a> {
|
||||||
k_b: DEFAULT_K_B,
|
k_b: DEFAULT_K_B,
|
||||||
k_c: DEFAULT_K_C,
|
k_c: DEFAULT_K_C,
|
||||||
channels,
|
channels,
|
||||||
last_status: FanStatus::OK,
|
last_status: if available { FanStatus::OK } else { FanStatus::NotAvailable },
|
||||||
|
skip_cycles: 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn cycle(&mut self) -> Result<(), FanStatus>{
|
pub fn cycle(&mut self) -> Result<(), FanStatus> {
|
||||||
if self.available {
|
if self.available {
|
||||||
self.tacho.cycle();
|
if self.tacho.cycle() {
|
||||||
|
self.skip_cycles >>= 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
self.adjust_speed();
|
self.adjust_speed();
|
||||||
let diagnose = self.diagnose();
|
let diagnose = self.diagnose();
|
||||||
if diagnose != self.last_status {
|
if (self.skip_cycles == 0 || diagnose == FanStatus::Halted) && diagnose != self.last_status {
|
||||||
self.last_status = diagnose;
|
self.last_status = diagnose;
|
||||||
Err(diagnose)
|
Err(diagnose)
|
||||||
} else {
|
} else {
|
||||||
|
@ -126,9 +146,7 @@ impl<'a> FanCtrl<'a> {
|
||||||
if self.fan_auto && self.available {
|
if self.fan_auto && self.available {
|
||||||
let scaled_current = self.channels.current_abs_max_tec_i() / MAX_TEC_I;
|
let scaled_current = self.channels.current_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()
|
||||||
let pwm = max_by(MAX_FAN_PWM * (scaled_current * (scaled_current * self.k_a + self.k_b) + self.k_c),
|
let pwm = (MAX_USER_FAN_PWM * (scaled_current * (scaled_current * self.k_a + self.k_b) + self.k_c)) as u32;
|
||||||
MIN_FAN_PWM,
|
|
||||||
|a, b| a.partial_cmp(b).unwrap_or(core::cmp::Ordering::Equal)) as u32;
|
|
||||||
self.set_pwm(pwm);
|
self.set_pwm(pwm);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -139,7 +157,7 @@ impl<'a> FanCtrl<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn set_coefficients(&mut self, k_a: f64, k_b: f64, k_c: f64) {
|
pub fn set_curve(&mut self, k_a: f64, k_b: f64, k_c: f64) {
|
||||||
self.k_a = k_a;
|
self.k_a = k_a;
|
||||||
self.k_b = k_b;
|
self.k_b = k_b;
|
||||||
self.k_c = k_c;
|
self.k_c = k_c;
|
||||||
|
@ -148,28 +166,50 @@ impl<'a> FanCtrl<'a> {
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn restore_defaults(&mut self) {
|
pub fn restore_defaults(&mut self) {
|
||||||
self.set_auto_mode(true);
|
self.set_auto_mode(true);
|
||||||
self.set_coefficients(DEFAULT_K_A, DEFAULT_K_B, DEFAULT_K_C);
|
self.set_curve(DEFAULT_K_A, DEFAULT_K_B, DEFAULT_K_C);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_pwm(&mut self, fan_pwm: u32) -> f64 {
|
pub fn set_pwm(&mut self, fan_pwm: u32) -> f64 {
|
||||||
let duty = fan_pwm as f64 / MAX_FAN_PWM;
|
let fan_pwm = fan_pwm.min(MAX_USER_FAN_PWM as u32).max(MIN_USER_FAN_PWM as u32);
|
||||||
|
self.skip_cycles = if (self.tacho.get() as f64) <= Self::threshold_for_pwm(fan_pwm as f64) {
|
||||||
|
TACHO_SKIP_CYCLES
|
||||||
|
} else { self.skip_cycles };
|
||||||
|
let duty = Self::scale_number(fan_pwm as f64, MIN_FAN_PWM, MAX_FAN_PWM, MIN_USER_FAN_PWM, MAX_USER_FAN_PWM);
|
||||||
let max = self.fan.get_max_duty();
|
let max = self.fan.get_max_duty();
|
||||||
let value = ((duty * (max as f64)) as u16).min(max);
|
let value = ((duty * (max as f64)) as u16).min(max);
|
||||||
self.fan.set_duty(value);
|
self.fan.set_duty(value);
|
||||||
value as f64 / (max as f64)
|
value as f64 / (max as f64)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn threshold_for_pwm(fan_pwm: f64) -> f64 {
|
||||||
|
(TACHO_REGRESSION_A * fan_pwm + TACHO_REGRESSION_B) * fan_pwm + TACHO_REGRESSION_C
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn scale_number(unscaled: f64, to_min: f64, to_max: f64, from_min: f64, from_max: f64) -> f64 {
|
||||||
|
(to_max - to_min) * (unscaled - from_min) / (from_max - from_min) + to_min
|
||||||
|
}
|
||||||
|
|
||||||
fn diagnose(&mut self) -> FanStatus {
|
fn diagnose(&mut self) -> FanStatus {
|
||||||
if !self.available {
|
if !self.available {
|
||||||
return FanStatus::NotAvailable;
|
return FanStatus::NotAvailable;
|
||||||
}
|
}
|
||||||
self.tacho.diagnose()
|
let threshold = Self::threshold_for_pwm(self.get_pwm() as f64) as u32;
|
||||||
|
let tacho = self.tacho.get();
|
||||||
|
if tacho >= threshold {
|
||||||
|
FanStatus::OK
|
||||||
|
} else if tacho >= TACHO_HALT_THRESHOLD {
|
||||||
|
FanStatus::TooSlow
|
||||||
|
} else {
|
||||||
|
FanStatus::Halted
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_pwm(&self) -> u32 {
|
fn get_pwm(&self) -> u32 {
|
||||||
let duty = self.fan.get_duty();
|
let duty = self.fan.get_duty();
|
||||||
let max = self.fan.get_max_duty();
|
let max = self.fan.get_max_duty();
|
||||||
((duty as f64 / (max as f64)) * MAX_FAN_PWM) as u32
|
(Self::scale_number(duty as f64 / (max as f64), MIN_USER_FAN_PWM, MAX_USER_FAN_PWM, MIN_FAN_PWM, MAX_FAN_PWM) + 0.5) as u32
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -180,34 +220,24 @@ impl TachoCtrl {
|
||||||
tacho_cnt: 0,
|
tacho_cnt: 0,
|
||||||
tacho_value: None,
|
tacho_value: None,
|
||||||
prev_epoch: 0,
|
prev_epoch: 0,
|
||||||
past_record: 0,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn init(&mut self, exti: &mut EXTI, syscfg: &mut SysCfg) {
|
fn init(&mut self, exti: &mut EXTI, syscfg: &mut SysCfg) {
|
||||||
// These lines do not cause NVIC to run the ISR,
|
// These lines do not cause NVIC to run the ISR,
|
||||||
// since the interrupt should be unmasked in the cortex_m::peripheral::NVIC.
|
// since the interrupt is masked in the cortex_m::peripheral::NVIC.
|
||||||
// Also using interrupt-related workaround is the best
|
// Also using interrupt-related workaround is the best
|
||||||
// option for the current version of stm32f4xx-hal,
|
// option for the current version of stm32f4xx-hal,
|
||||||
// since tying the IC's PC8 with the PWM's PC9 to the same TIM8 is not supported,
|
// since tying the IC's PC8 with the PWM's PC9 to the same TIM8 is not supported.
|
||||||
// and therefore would require even more weirder and unsafe hacks.
|
// The possible solution would be to update the library to >=v0.14.*,
|
||||||
// Also such hacks wouldn't guarantee it to be more precise.
|
// and use its Timer's counter functionality.
|
||||||
self.tacho.make_interrupt_source(syscfg);
|
self.tacho.make_interrupt_source(syscfg);
|
||||||
self.tacho.trigger_on_edge(exti, Edge::Rising);
|
self.tacho.trigger_on_edge(exti, Edge::Rising);
|
||||||
self.tacho.enable_interrupt(exti);
|
self.tacho.enable_interrupt(exti);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
// returns whether the epoch elapsed
|
||||||
fn add_record(&mut self, value: u32) {
|
fn cycle(&mut self) -> bool {
|
||||||
self.past_record = self.past_record << 2;
|
|
||||||
if value >= TACHO_LOW_THRESHOLD {
|
|
||||||
self.past_record += 0b11;
|
|
||||||
} else if value > 0 && self.tacho_cnt < TACHO_LOW_THRESHOLD {
|
|
||||||
self.past_record += 0b10;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn cycle(&mut self) {
|
|
||||||
let tacho_input = self.tacho.check_interrupt();
|
let tacho_input = self.tacho.check_interrupt();
|
||||||
if tacho_input {
|
if tacho_input {
|
||||||
self.tacho.clear_interrupt_pending_bit();
|
self.tacho.clear_interrupt_pending_bit();
|
||||||
|
@ -217,25 +247,17 @@ impl TachoCtrl {
|
||||||
let instant = Instant::from_millis(i64::from(timer::now()));
|
let instant = Instant::from_millis(i64::from(timer::now()));
|
||||||
if instant.millis - self.prev_epoch >= TACHO_MEASURE_MS {
|
if instant.millis - self.prev_epoch >= TACHO_MEASURE_MS {
|
||||||
self.tacho_value = Some(self.tacho_cnt);
|
self.tacho_value = Some(self.tacho_cnt);
|
||||||
self.add_record(self.tacho_cnt);
|
|
||||||
self.tacho_cnt = 0;
|
self.tacho_cnt = 0;
|
||||||
self.prev_epoch = instant.millis;
|
self.prev_epoch = instant.millis;
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get(&self) -> u32 {
|
fn get(&self) -> u32 {
|
||||||
self.tacho_value.unwrap_or(u32::MAX)
|
self.tacho_value.unwrap_or(u32::MAX)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn diagnose(&mut self) -> FanStatus {
|
|
||||||
if self.past_record & 0b11 == 0b11 {
|
|
||||||
FanStatus::OK
|
|
||||||
} else if self.past_record & 0xAAAAAAAAAAAAAAAA > 0 {
|
|
||||||
FanStatus::LowSignal
|
|
||||||
} else {
|
|
||||||
FanStatus::Stalled
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HWRev {
|
impl HWRev {
|
||||||
|
@ -272,8 +294,23 @@ impl FanStatus {
|
||||||
match *self {
|
match *self {
|
||||||
FanStatus::OK => "Fan is OK".as_bytes(),
|
FanStatus::OK => "Fan is OK".as_bytes(),
|
||||||
FanStatus::NotAvailable => "Fan is not available".as_bytes(),
|
FanStatus::NotAvailable => "Fan is not available".as_bytes(),
|
||||||
FanStatus::Stalled => "Fan is stalled".as_bytes(),
|
FanStatus::TooSlow => "Fan is too slow".as_bytes(),
|
||||||
FanStatus::LowSignal => "Fan is low signal".as_bytes(),
|
FanStatus::Halted => "Fan is halted".as_bytes(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_scaler() {
|
||||||
|
for x in 1..100 {
|
||||||
|
assert_eq!((FanCtrl::scale_number(
|
||||||
|
FanCtrl::scale_number(x as f64, MIN_FAN_PWM, MAX_FAN_PWM, MIN_USER_FAN_PWM, MAX_USER_FAN_PWM),
|
||||||
|
MIN_USER_FAN_PWM, MAX_USER_FAN_PWM, MIN_FAN_PWM, MAX_FAN_PWM) + 0.5) as i32,
|
||||||
|
x);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
16
src/main.rs
16
src/main.rs
|
@ -10,7 +10,6 @@ use panic_abort as _;
|
||||||
use panic_semihosting as _;
|
use panic_semihosting as _;
|
||||||
|
|
||||||
use log::{error, info, warn};
|
use log::{error, info, warn};
|
||||||
use core::cell::RefCell;
|
|
||||||
use cortex_m::asm::wfi;
|
use cortex_m::asm::wfi;
|
||||||
use cortex_m_rt::entry;
|
use cortex_m_rt::entry;
|
||||||
use stm32f4xx_hal::{
|
use stm32f4xx_hal::{
|
||||||
|
@ -140,20 +139,19 @@ fn main() -> ! {
|
||||||
|
|
||||||
let mut store = flash_store::store(dp.FLASH);
|
let mut store = flash_store::store(dp.FLASH);
|
||||||
|
|
||||||
let mut channels = RefCell::new(Channels::new(pins));
|
let mut channels = Channels::new(pins);
|
||||||
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)) =>
|
||||||
config.apply(channels.get_mut(), c),
|
config.apply(&mut channels, c),
|
||||||
Ok(None) =>
|
Ok(None) =>
|
||||||
error!("flash config not found for channel {}", c),
|
error!("flash config not found for channel {}", c),
|
||||||
Err(e) =>
|
Err(e) =>
|
||||||
error!("unable to load config {} from flash: {:?}", c, e),
|
error!("unable to load config {} from flash: {:?}", c, e),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// considered safe since `channels` is being mutated in a single thread,
|
|
||||||
// while mutex would be excessive
|
let mut fan_ctrl = FanCtrl::new(fan, tacho, channels, &mut dp.EXTI, &mut dp.SYSCFG.constrain());
|
||||||
let mut fan_ctrl = FanCtrl::new(fan, tacho, unsafe{ &mut *channels.as_ptr() }, &mut dp.EXTI, &mut dp.SYSCFG.constrain());
|
|
||||||
|
|
||||||
// default net config:
|
// default net config:
|
||||||
let mut ipv4_config = Ipv4Config {
|
let mut ipv4_config = Ipv4Config {
|
||||||
|
@ -183,7 +181,7 @@ fn main() -> ! {
|
||||||
loop {
|
loop {
|
||||||
let mut new_ipv4_config = None;
|
let mut new_ipv4_config = None;
|
||||||
let instant = Instant::from_millis(i64::from(timer::now()));
|
let instant = Instant::from_millis(i64::from(timer::now()));
|
||||||
let updated_channel = channels.get_mut().poll_adc(instant);
|
let updated_channel = fan_ctrl.channels.poll_adc(instant);
|
||||||
if let Some(channel) = updated_channel {
|
if let Some(channel) = updated_channel {
|
||||||
server.for_each(|_, session| session.set_report_pending(channel.into()));
|
server.for_each(|_, session| session.set_report_pending(channel.into()));
|
||||||
}
|
}
|
||||||
|
@ -213,7 +211,7 @@ fn main() -> ! {
|
||||||
// Do nothing and feed more data to the line reader in the next loop cycle.
|
// Do nothing and feed more data to the line reader in the next loop cycle.
|
||||||
Ok(SessionInput::Nothing) => {}
|
Ok(SessionInput::Nothing) => {}
|
||||||
Ok(SessionInput::Command(command)) => {
|
Ok(SessionInput::Command(command)) => {
|
||||||
match Handler::handle_command(command, &mut socket, channels.get_mut(), session, &mut leds, &mut store, &mut ipv4_config, &mut fan_ctrl) {
|
match Handler::handle_command(command, &mut socket, session, &mut leds, &mut store, &mut ipv4_config, &mut fan_ctrl) {
|
||||||
Ok(Handler::NewIPV4(ip)) => new_ipv4_config = Some(ip),
|
Ok(Handler::NewIPV4(ip)) => new_ipv4_config = Some(ip),
|
||||||
Ok(Handler::Handled) => {},
|
Ok(Handler::Handled) => {},
|
||||||
Ok(Handler::CloseSocket) => socket.close(),
|
Ok(Handler::CloseSocket) => socket.close(),
|
||||||
|
@ -230,7 +228,7 @@ fn main() -> ! {
|
||||||
}
|
}
|
||||||
} else if socket.can_send() {
|
} else if socket.can_send() {
|
||||||
if let Some(channel) = session.is_report_pending() {
|
if let Some(channel) = session.is_report_pending() {
|
||||||
match channels.get_mut().reports_json() {
|
match fan_ctrl.channels.reports_json() {
|
||||||
Ok(buf) => {
|
Ok(buf) => {
|
||||||
send_line(&mut socket, &buf[..]);
|
send_line(&mut socket, &buf[..]);
|
||||||
session.mark_report_sent(channel);
|
session.mark_report_sent(channel);
|
||||||
|
|
Loading…
Reference in New Issue
Those naive values will need proper testing and determination at some point.