forked from M-Labs/thermostat
implement support for fan PWM
Co-authored-by: Egor Savkin <es@m-labs.hk> Co-committed-by: Egor Savkin <es@m-labs.hk>
This commit is contained in:
parent
5688b2f1bb
commit
570c0324b3
78
README.md
78
README.md
@ -94,36 +94,42 @@ The scope of this setting is per TCP session.
|
||||
Send commands as simple text string terminated by `\n`. Responses are
|
||||
formatted as line-delimited JSON.
|
||||
|
||||
| Syntax | Function |
|
||||
| --- | --- |
|
||||
| `report` | Show current input |
|
||||
| `report mode` | Show current report mode |
|
||||
| `report mode <off/on>` | Set report mode |
|
||||
| `pwm` | Show current PWM settings |
|
||||
| `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_v <volt>` | Set maximum output voltage |
|
||||
| `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 |
|
||||
| `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 |
|
||||
| `pid` | Show PID configuration |
|
||||
| `pid <0/1> target <deg_celsius>` | Set the PID controller target temperature |
|
||||
| `pid <0/1> kp <value>` | Set proportional gain |
|
||||
| `pid <0/1> ki <value>` | Set integral gain |
|
||||
| `pid <0/1> kd <value>` | Set differential gain |
|
||||
| `pid <0/1> output_min <amp>` | Set mininum output |
|
||||
| `pid <0/1> output_max <amp>` | Set maximum output |
|
||||
| `s-h` | Show Steinhart-Hart equation parameters |
|
||||
| `s-h <0/1> <t0/b/r0> <value>` | Set Steinhart-Hart parameter for a channel |
|
||||
| `postfilter` | Show postfilter settings |
|
||||
| `postfilter <0/1> off` | Disable postfilter |
|
||||
| `postfilter <0/1> rate <rate>` | Set postfilter output data rate |
|
||||
| `load [0/1]` | Restore configuration for channel all/0/1 from flash |
|
||||
| `save [0/1]` | Save configuration for channel all/0/1 to flash |
|
||||
| `reset` | Reset the device |
|
||||
| `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 |
|
||||
| Syntax | Function |
|
||||
|----------------------------------|-------------------------------------------------------------------------------|
|
||||
| `report` | Show current input |
|
||||
| `report mode` | Show current report mode |
|
||||
| `report mode <off/on>` | Set report mode |
|
||||
| `pwm` | Show current PWM settings |
|
||||
| `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_v <volt>` | Set maximum output voltage |
|
||||
| `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 |
|
||||
| `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 |
|
||||
| `pid` | Show PID configuration |
|
||||
| `pid <0/1> target <deg_celsius>` | Set the PID controller target temperature |
|
||||
| `pid <0/1> kp <value>` | Set proportional gain |
|
||||
| `pid <0/1> ki <value>` | Set integral gain |
|
||||
| `pid <0/1> kd <value>` | Set differential gain |
|
||||
| `pid <0/1> output_min <amp>` | Set mininum output |
|
||||
| `pid <0/1> output_max <amp>` | Set maximum output |
|
||||
| `s-h` | Show Steinhart-Hart equation parameters |
|
||||
| `s-h <0/1> <t0/b/r0> <value>` | Set Steinhart-Hart parameter for a channel |
|
||||
| `postfilter` | Show postfilter settings |
|
||||
| `postfilter <0/1> off` | Disable postfilter |
|
||||
| `postfilter <0/1> rate <rate>` | Set postfilter output data rate |
|
||||
| `load [0/1]` | Restore configuration for channel all/0/1 from flash |
|
||||
| `save [0/1]` | Save configuration for channel all/0/1 to flash |
|
||||
| `reset` | Reset the device |
|
||||
| `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 1 to 100 |
|
||||
| `fan auto` | Enable automatic fan speed control |
|
||||
| `fcurve <a> <b> <c>` | Set fan controller curve coefficients (see *Fan control* section) |
|
||||
| `fcurve default` | Set fan controller curve coefficients to defaults (see *Fan control* section) |
|
||||
| `hwrev` | Show hardware revision, and settings related to it |
|
||||
|
||||
|
||||
## USB
|
||||
@ -269,3 +275,15 @@ 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 four commands are available:
|
||||
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`.
|
||||
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.
|
||||
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`,
|
||||
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.
|
||||
5. `fcurve default` - restore fan curve settings to defaults: `a = 1.0, b = 0.0, c = 0.0`.
|
||||
|
@ -1,4 +1,5 @@
|
||||
use heapless::{consts::{U2, U1024}, Vec};
|
||||
use core::cmp::max_by;
|
||||
use heapless::{consts::U2, Vec};
|
||||
use serde::{Serialize, Serializer};
|
||||
use smoltcp::time::Instant;
|
||||
use stm32f4xx_hal::hal;
|
||||
@ -16,6 +17,7 @@ use crate::{
|
||||
channel::{Channel, Channel0, Channel1},
|
||||
channel_state::ChannelState,
|
||||
command_parser::{CenterPoint, PwmPin},
|
||||
command_handler::JsonBuffer,
|
||||
pins,
|
||||
steinhart_hart,
|
||||
};
|
||||
@ -518,9 +520,13 @@ impl Channels {
|
||||
}
|
||||
serde_json_core::to_vec(&summaries)
|
||||
}
|
||||
}
|
||||
|
||||
type JsonBuffer = Vec<u8, U1024>;
|
||||
pub 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))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct Report {
|
||||
|
@ -1,6 +1,7 @@
|
||||
use smoltcp::socket::TcpSocket;
|
||||
use log::{error, warn};
|
||||
use core::fmt::Write;
|
||||
use heapless::{consts::U1024, Vec};
|
||||
use super::{
|
||||
net,
|
||||
command_parser::{
|
||||
@ -22,7 +23,9 @@ use super::{
|
||||
config::ChannelConfig,
|
||||
dfu,
|
||||
flash_store::FlashStore,
|
||||
session::Session
|
||||
session::Session,
|
||||
FanCtrl,
|
||||
hw_rev::HWRev,
|
||||
};
|
||||
|
||||
use uom::{
|
||||
@ -55,6 +58,8 @@ pub enum Error {
|
||||
FlashError
|
||||
}
|
||||
|
||||
pub type JsonBuffer = Vec<u8, U1024>;
|
||||
|
||||
fn send_line(socket: &mut TcpSocket, data: &[u8]) -> bool {
|
||||
let send_free = socket.send_capacity() - socket.send_queue();
|
||||
if data.len() > send_free + 1 {
|
||||
@ -341,16 +346,85 @@ impl Handler {
|
||||
Ok(Handler::Reset)
|
||||
}
|
||||
|
||||
pub fn handle_command (command: Command, socket: &mut TcpSocket, channels: &mut Channels, session: &Session, leds: &mut Leds, store: &mut FlashStore, ipv4_config: &mut Ipv4Config) -> Result<Self, Error> {
|
||||
fn set_fan(socket: &mut TcpSocket, fan_pwm: u32, fan_ctrl: &mut FanCtrl) -> Result<Handler, Error> {
|
||||
if !fan_ctrl.fan_available() {
|
||||
send_line(socket, b"{ \"warning\": \"this thermostat doesn't have fan!\" }");
|
||||
return Ok(Handler::Handled);
|
||||
}
|
||||
fan_ctrl.set_auto_mode(false);
|
||||
fan_ctrl.set_pwm(fan_pwm);
|
||||
if fan_ctrl.fan_pwm_recommended() {
|
||||
send_line(socket, b"{}");
|
||||
} else {
|
||||
send_line(socket, b"{ \"warning\": \"this fan doesn't have full PWM support. Use it at your own risk!\" }");
|
||||
}
|
||||
Ok(Handler::Handled)
|
||||
}
|
||||
|
||||
fn show_fan(socket: &mut TcpSocket, fan_ctrl: &mut FanCtrl) -> Result<Handler, Error> {
|
||||
match fan_ctrl.summary() {
|
||||
Ok(buf) => {
|
||||
send_line(socket, &buf);
|
||||
Ok(Handler::Handled)
|
||||
}
|
||||
Err(e) => {
|
||||
error!("unable to serialize fan summary: {:?}", e);
|
||||
let _ = writeln!(socket, "{{\"error\":\"{:?}\"}}", e);
|
||||
Err(Error::ReportError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn fan_auto(socket: &mut TcpSocket, fan_ctrl: &mut FanCtrl) -> Result<Handler, Error> {
|
||||
if !fan_ctrl.fan_available() {
|
||||
send_line(socket, b"{ \"warning\": \"this thermostat doesn't have fan!\" }");
|
||||
return Ok(Handler::Handled);
|
||||
}
|
||||
fan_ctrl.set_auto_mode(true);
|
||||
if fan_ctrl.fan_pwm_recommended() {
|
||||
send_line(socket, b"{}");
|
||||
} else {
|
||||
send_line(socket, b"{ \"warning\": \"this fan doesn't have full PWM support. Use it at your own risk!\" }");
|
||||
}
|
||||
Ok(Handler::Handled)
|
||||
}
|
||||
|
||||
fn fan_curve(socket: &mut TcpSocket, fan_ctrl: &mut FanCtrl, k_a: f32, k_b: f32, k_c: f32) -> Result<Handler, Error> {
|
||||
fan_ctrl.set_curve(k_a, k_b, k_c);
|
||||
send_line(socket, b"{}");
|
||||
Ok(Handler::Handled)
|
||||
}
|
||||
|
||||
fn fan_defaults(socket: &mut TcpSocket, fan_ctrl: &mut FanCtrl) -> Result<Handler, Error> {
|
||||
fan_ctrl.restore_defaults();
|
||||
send_line(socket, b"{}");
|
||||
Ok(Handler::Handled)
|
||||
}
|
||||
|
||||
fn show_hwrev(socket: &mut TcpSocket, hwrev: HWRev) -> Result<Handler, Error> {
|
||||
match hwrev.summary() {
|
||||
Ok(buf) => {
|
||||
send_line(socket, &buf);
|
||||
Ok(Handler::Handled)
|
||||
}
|
||||
Err(e) => {
|
||||
error!("unable to serialize HWRev summary: {:?}", e);
|
||||
let _ = writeln!(socket, "{{\"error\":\"{:?}\"}}", e);
|
||||
Err(Error::ReportError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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, hwrev: HWRev) -> Result<Self, Error> {
|
||||
match command {
|
||||
Command::Quit => Ok(Handler::CloseSocket),
|
||||
Command::Reporting(_reporting) => Handler::reporting(socket),
|
||||
Command::Show(ShowCommand::Reporting) => Handler::show_report_mode(socket, session),
|
||||
Command::Show(ShowCommand::Input) => Handler::show_report(socket, channels),
|
||||
Command::Show(ShowCommand::Pid) => Handler::show_pid(socket, channels),
|
||||
Command::Show(ShowCommand::Pwm) => Handler::show_pwm(socket, channels),
|
||||
Command::Show(ShowCommand::SteinhartHart) => Handler::show_steinhart_hart(socket, channels),
|
||||
Command::Show(ShowCommand::PostFilter) => Handler::show_post_filter(socket, channels),
|
||||
Command::Show(ShowCommand::Input) => Handler::show_report(socket, channels),
|
||||
Command::Show(ShowCommand::Pid) => Handler::show_pid(socket, channels),
|
||||
Command::Show(ShowCommand::Pwm) => Handler::show_pwm(socket, channels),
|
||||
Command::Show(ShowCommand::SteinhartHart) => Handler::show_steinhart_hart(socket, channels),
|
||||
Command::Show(ShowCommand::PostFilter) => Handler::show_post_filter(socket, channels),
|
||||
Command::Show(ShowCommand::Ipv4) => Handler::show_ipv4(socket, ipv4_config),
|
||||
Command::PwmPid { channel } => Handler::engage_pid(socket, channels, leds, channel),
|
||||
Command::Pwm { channel, pin, value } => Handler::set_pwm(socket, channels, leds, channel, pin, value),
|
||||
@ -363,7 +437,13 @@ impl Handler {
|
||||
Command::Save { channel } => Handler::save_channel(socket, channels, channel, store),
|
||||
Command::Ipv4(config) => Handler::set_ipv4(socket, store, config),
|
||||
Command::Reset => Handler::reset(channels),
|
||||
Command::Dfu => Handler::dfu(channels)
|
||||
Command::Dfu => Handler::dfu(channels),
|
||||
Command::FanSet {fan_pwm} => Handler::set_fan(socket, fan_pwm, fan_ctrl),
|
||||
Command::ShowFan => Handler::show_fan(socket, fan_ctrl),
|
||||
Command::FanAuto => Handler::fan_auto(socket, fan_ctrl),
|
||||
Command::FanCurve { k_a, k_b, k_c } => Handler::fan_curve(socket, fan_ctrl, k_a, k_b, k_c),
|
||||
Command::FanCurveDefaults => Handler::fan_defaults(socket, fan_ctrl),
|
||||
Command::ShowHWRev => Handler::show_hwrev(socket, hwrev),
|
||||
}
|
||||
}
|
||||
}
|
@ -10,6 +10,7 @@ use nom::{
|
||||
sequence::preceded,
|
||||
multi::{fold_many0, fold_many1},
|
||||
error::ErrorKind,
|
||||
Needed,
|
||||
};
|
||||
use num_traits::{Num, ParseFloatError};
|
||||
use serde::{Serialize, Deserialize};
|
||||
@ -178,6 +179,18 @@ pub enum Command {
|
||||
rate: Option<f32>,
|
||||
},
|
||||
Dfu,
|
||||
FanSet {
|
||||
fan_pwm: u32
|
||||
},
|
||||
FanAuto,
|
||||
ShowFan,
|
||||
FanCurve {
|
||||
k_a: f32,
|
||||
k_b: f32,
|
||||
k_c: f32,
|
||||
},
|
||||
FanCurveDefaults,
|
||||
ShowHWRev,
|
||||
}
|
||||
|
||||
fn end(input: &[u8]) -> IResult<&[u8], ()> {
|
||||
@ -520,6 +533,57 @@ fn ipv4(input: &[u8]) -> IResult<&[u8], Result<Command, Error>> {
|
||||
))(input)
|
||||
}
|
||||
|
||||
fn fan(input: &[u8]) -> IResult<&[u8], Result<Command, Error>> {
|
||||
let (input, _) = tag("fan")(input)?;
|
||||
alt((
|
||||
|input| {
|
||||
let (input, _) = whitespace(input)?;
|
||||
|
||||
let (input, result) = alt((
|
||||
|input| {
|
||||
let (input, _) = tag("auto")(input)?;
|
||||
Ok((input, Ok(Command::FanAuto)))
|
||||
},
|
||||
|input| {
|
||||
let (input, value) = unsigned(input)?;
|
||||
Ok((input, Ok(Command::FanSet { fan_pwm: value.unwrap_or(0)})))
|
||||
},
|
||||
))(input)?;
|
||||
Ok((input, result))
|
||||
},
|
||||
value(Ok(Command::ShowFan), end)
|
||||
))(input)
|
||||
}
|
||||
|
||||
fn fan_curve(input: &[u8]) -> IResult<&[u8], Result<Command, Error>> {
|
||||
let (input, _) = tag("fcurve")(input)?;
|
||||
alt((
|
||||
|input| {
|
||||
let (input, _) = whitespace(input)?;
|
||||
let (input, result) = alt((
|
||||
|input| {
|
||||
let (input, _) = tag("default")(input)?;
|
||||
Ok((input, Ok(Command::FanCurveDefaults)))
|
||||
},
|
||||
|input| {
|
||||
let (input, k_a) = float(input)?;
|
||||
let (input, _) = whitespace(input)?;
|
||||
let (input, k_b) = float(input)?;
|
||||
let (input, _) = whitespace(input)?;
|
||||
let (input, k_c) = float(input)?;
|
||||
if k_a.is_ok() && k_b.is_ok() && k_c.is_ok() {
|
||||
Ok((input, Ok(Command::FanCurve { k_a: k_a.unwrap() as f32, k_b: k_b.unwrap() as f32, k_c: k_c.unwrap() as f32 })))
|
||||
} else {
|
||||
Err(nom::Err::Incomplete(Needed::Size(3)))
|
||||
}
|
||||
},
|
||||
))(input)?;
|
||||
Ok((input, result))
|
||||
},
|
||||
value(Err(Error::Incomplete), end)
|
||||
))(input)
|
||||
}
|
||||
|
||||
fn command(input: &[u8]) -> IResult<&[u8], Result<Command, Error>> {
|
||||
alt((value(Ok(Command::Quit), tag("quit")),
|
||||
load,
|
||||
@ -533,6 +597,9 @@ fn command(input: &[u8]) -> IResult<&[u8], Result<Command, Error>> {
|
||||
steinhart_hart,
|
||||
postfilter,
|
||||
value(Ok(Command::Dfu), tag("dfu")),
|
||||
fan,
|
||||
fan_curve,
|
||||
value(Ok(Command::ShowHWRev), tag("hwrev")),
|
||||
))(input)
|
||||
}
|
||||
|
||||
@ -754,4 +821,44 @@ mod test {
|
||||
center: CenterPoint::Vref,
|
||||
}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_fan_show() {
|
||||
let command = Command::parse(b"fan");
|
||||
assert_eq!(command, Ok(Command::ShowFan));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_fan_set() {
|
||||
let command = Command::parse(b"fan 42");
|
||||
assert_eq!(command, Ok(Command::FanSet {fan_pwm: 42}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_fan_auto() {
|
||||
let command = Command::parse(b"fan auto");
|
||||
assert_eq!(command, Ok(Command::FanAuto));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_fcurve_set() {
|
||||
let command = Command::parse(b"fcurve 1.2 3.4 5.6");
|
||||
assert_eq!(command, Ok(Command::FanCurve {
|
||||
k_a: 1.2,
|
||||
k_b: 3.4,
|
||||
k_c: 5.6
|
||||
}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_fcurve_default() {
|
||||
let command = Command::parse(b"fcurve default");
|
||||
assert_eq!(command, Ok(Command::FanCurveDefaults));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_hwrev() {
|
||||
let command = Command::parse(b"hwrev");
|
||||
assert_eq!(command, Ok(Command::ShowHWRev));
|
||||
}
|
||||
}
|
||||
|
151
src/fan_ctrl.rs
Normal file
151
src/fan_ctrl.rs
Normal file
@ -0,0 +1,151 @@
|
||||
use num_traits::Float;
|
||||
use serde::Serialize;
|
||||
use stm32f4xx_hal::{
|
||||
pwm::{self, PwmChannels},
|
||||
pac::TIM8,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
hw_rev::HWSettings,
|
||||
command_handler::JsonBuffer,
|
||||
};
|
||||
|
||||
pub type FanPin = PwmChannels<TIM8, pwm::C4>;
|
||||
|
||||
// as stated in the schematics
|
||||
const MAX_TEC_I: f32 = 3.0;
|
||||
|
||||
const MAX_USER_FAN_PWM: f32 = 100.0;
|
||||
const MIN_USER_FAN_PWM: f32 = 1.0;
|
||||
|
||||
|
||||
pub struct FanCtrl {
|
||||
fan: Option<FanPin>,
|
||||
fan_auto: bool,
|
||||
pwm_enabled: bool,
|
||||
k_a: f32,
|
||||
k_b: f32,
|
||||
k_c: f32,
|
||||
abs_max_tec_i: f32,
|
||||
hw_settings: HWSettings,
|
||||
}
|
||||
|
||||
impl FanCtrl {
|
||||
pub fn new(fan: Option<FanPin>, hw_settings: HWSettings) -> Self {
|
||||
let mut fan_ctrl = FanCtrl {
|
||||
fan,
|
||||
// do not enable auto mode by default,
|
||||
// but allow to turn it at the user's own risk
|
||||
fan_auto: hw_settings.fan_pwm_recommended,
|
||||
pwm_enabled: false,
|
||||
k_a: hw_settings.fan_k_a,
|
||||
k_b: hw_settings.fan_k_b,
|
||||
k_c: hw_settings.fan_k_c,
|
||||
abs_max_tec_i: 0f32,
|
||||
hw_settings,
|
||||
};
|
||||
if fan_ctrl.fan_auto {
|
||||
fan_ctrl.enable_pwm();
|
||||
}
|
||||
fan_ctrl
|
||||
}
|
||||
|
||||
pub fn cycle(&mut self, abs_max_tec_i: f32) {
|
||||
self.abs_max_tec_i = abs_max_tec_i;
|
||||
if self.fan_auto && self.hw_settings.fan_available {
|
||||
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()
|
||||
let pwm = (MAX_USER_FAN_PWM * (scaled_current * (scaled_current * self.k_a + self.k_b) + self.k_c)) as u32;
|
||||
self.set_pwm(pwm);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn summary(&mut self) -> Result<JsonBuffer, serde_json_core::ser::Error> {
|
||||
if self.hw_settings.fan_available {
|
||||
let summary = FanSummary {
|
||||
fan_pwm: self.get_pwm(),
|
||||
abs_max_tec_i: self.abs_max_tec_i,
|
||||
auto_mode: self.fan_auto,
|
||||
k_a: self.k_a,
|
||||
k_b: self.k_b,
|
||||
k_c: self.k_c,
|
||||
};
|
||||
serde_json_core::to_vec(&summary)
|
||||
} else {
|
||||
let summary: Option<()> = None;
|
||||
serde_json_core::to_vec(&summary)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_auto_mode(&mut self, fan_auto: bool) {
|
||||
self.fan_auto = fan_auto;
|
||||
}
|
||||
|
||||
pub fn set_curve(&mut self, k_a: f32, k_b: f32, k_c: f32) {
|
||||
self.k_a = k_a;
|
||||
self.k_b = k_b;
|
||||
self.k_c = k_c;
|
||||
}
|
||||
|
||||
pub fn restore_defaults(&mut self) {
|
||||
self.set_curve(self.hw_settings.fan_k_a,
|
||||
self.hw_settings.fan_k_b,
|
||||
self.hw_settings.fan_k_c);
|
||||
}
|
||||
|
||||
pub fn set_pwm(&mut self, fan_pwm: u32) -> f32 {
|
||||
if self.fan.is_none() || (!self.pwm_enabled && !self.enable_pwm()) {
|
||||
return 0f32;
|
||||
}
|
||||
let fan = self.fan.as_mut().unwrap();
|
||||
let fan_pwm = fan_pwm.min(MAX_USER_FAN_PWM as u32).max(MIN_USER_FAN_PWM as u32);
|
||||
let duty = scale_number(fan_pwm as f32, self.hw_settings.min_fan_pwm, self.hw_settings.max_fan_pwm, MIN_USER_FAN_PWM, MAX_USER_FAN_PWM);
|
||||
let max = fan.get_max_duty();
|
||||
let value = ((duty * (max as f32)) as u16).min(max);
|
||||
fan.set_duty(value);
|
||||
value as f32 / (max as f32)
|
||||
}
|
||||
|
||||
pub fn fan_pwm_recommended(&self) -> bool {
|
||||
self.hw_settings.fan_pwm_recommended
|
||||
}
|
||||
|
||||
pub fn fan_available(&self) -> bool {
|
||||
self.hw_settings.fan_available
|
||||
}
|
||||
|
||||
fn get_pwm(&self) -> u32 {
|
||||
if let Some(fan) = &self.fan {
|
||||
let duty = fan.get_duty();
|
||||
let max = fan.get_max_duty();
|
||||
scale_number(duty as f32 / (max as f32), MIN_USER_FAN_PWM, MAX_USER_FAN_PWM, self.hw_settings.min_fan_pwm, self.hw_settings.max_fan_pwm).round() as u32
|
||||
} else { 0 }
|
||||
}
|
||||
|
||||
fn enable_pwm(&mut self) -> bool {
|
||||
if self.fan.is_some() && self.hw_settings.fan_available {
|
||||
let fan = self.fan.as_mut().unwrap();
|
||||
fan.set_duty(0);
|
||||
fan.enable();
|
||||
self.pwm_enabled = true;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fn scale_number(unscaled: f32, to_min: f32, to_max: f32, from_min: f32, from_max: f32) -> f32 {
|
||||
(to_max - to_min) * (unscaled - from_min) / (from_max - from_min) + to_min
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct FanSummary {
|
||||
fan_pwm: u32,
|
||||
abs_max_tec_i: f32,
|
||||
auto_mode: bool,
|
||||
k_a: f32,
|
||||
k_b: f32,
|
||||
k_c: f32,
|
||||
}
|
82
src/hw_rev.rs
Normal file
82
src/hw_rev.rs
Normal file
@ -0,0 +1,82 @@
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::{
|
||||
pins::HWRevPins,
|
||||
command_handler::JsonBuffer,
|
||||
};
|
||||
|
||||
#[derive(Serialize, Copy, Clone)]
|
||||
pub struct HWRev {
|
||||
pub major: u8,
|
||||
pub minor: u8,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone)]
|
||||
pub struct HWSettings {
|
||||
pub fan_k_a: f32,
|
||||
pub fan_k_b: f32,
|
||||
pub fan_k_c: f32,
|
||||
pub min_fan_pwm: f32,
|
||||
pub max_fan_pwm: f32,
|
||||
pub fan_pwm_freq_hz: u32,
|
||||
pub fan_available: bool,
|
||||
pub fan_pwm_recommended: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone)]
|
||||
struct HWSummary<'a> {
|
||||
rev: &'a HWRev,
|
||||
settings: &'a HWSettings,
|
||||
}
|
||||
|
||||
impl HWRev {
|
||||
pub fn detect_hw_rev(hwrev_pins: &HWRevPins) -> Self {
|
||||
let (h0, h1, h2, h3) = (hwrev_pins.hwrev0.is_high(), hwrev_pins.hwrev1.is_high(),
|
||||
hwrev_pins.hwrev2.is_high(), hwrev_pins.hwrev3.is_high());
|
||||
match (h0, h1, h2, h3) {
|
||||
(true, true, true, false) => HWRev { major: 1, minor: 0 },
|
||||
(true, false, false, false) => HWRev { major: 2, minor: 0 },
|
||||
(false, true, false, false) => HWRev { major: 2, minor: 2 },
|
||||
(_, _, _, _) => HWRev { major: 0, minor: 0 }
|
||||
}
|
||||
}
|
||||
|
||||
pub fn settings(&self) -> HWSettings {
|
||||
match (self.major, self.minor) {
|
||||
(2, 2) => HWSettings {
|
||||
fan_k_a: 1.0,
|
||||
fan_k_b: 0.0,
|
||||
fan_k_c: 0.0,
|
||||
// below this value motor's autostart feature may fail,
|
||||
// according to internal experiments
|
||||
min_fan_pwm: 0.04,
|
||||
max_fan_pwm: 1.0,
|
||||
// According to `SUNON DC Brushless Fan & Blower(255-E)` catalogue p.36-37
|
||||
// model MF35101V1-1000U-G99 doesn't have a PWM wire, but we'll follow their others models'
|
||||
// recommended frequency, as it is said by the Thermostat's schematics that we can
|
||||
// use PWM, but not stated at which frequency
|
||||
fan_pwm_freq_hz: 25_000,
|
||||
fan_available: true,
|
||||
// see https://github.com/sinara-hw/Thermostat/issues/115 and
|
||||
// https://git.m-labs.hk/M-Labs/thermostat/issues/69#issuecomment-6464 for explanation
|
||||
fan_pwm_recommended: false,
|
||||
},
|
||||
(_, _) => HWSettings {
|
||||
fan_k_a: 0.0,
|
||||
fan_k_b: 0.0,
|
||||
fan_k_c: 0.0,
|
||||
min_fan_pwm: 0.0,
|
||||
max_fan_pwm: 0.0,
|
||||
fan_pwm_freq_hz: 0,
|
||||
fan_available: false,
|
||||
fan_pwm_recommended: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn summary(&self) -> Result<JsonBuffer, serde_json_core::ser::Error> {
|
||||
let settings = self.settings();
|
||||
let summary = HWSummary { rev: self, settings: &settings };
|
||||
serde_json_core::to_vec(&summary)
|
||||
}
|
||||
}
|
15
src/main.rs
15
src/main.rs
@ -10,7 +10,6 @@ use panic_abort as _;
|
||||
use panic_semihosting as _;
|
||||
|
||||
use log::{error, info, warn};
|
||||
|
||||
use cortex_m::asm::wfi;
|
||||
use cortex_m_rt::entry;
|
||||
use stm32f4xx_hal::{
|
||||
@ -54,6 +53,9 @@ mod flash_store;
|
||||
mod dfu;
|
||||
mod command_handler;
|
||||
use command_handler::Handler;
|
||||
mod fan_ctrl;
|
||||
use fan_ctrl::FanCtrl;
|
||||
mod hw_rev;
|
||||
|
||||
const HSE: MegaHertz = MegaHertz(8);
|
||||
#[cfg(not(feature = "semihosting"))]
|
||||
@ -118,8 +120,8 @@ fn main() -> ! {
|
||||
|
||||
timer::setup(cp.SYST, clocks);
|
||||
|
||||
let (pins, mut leds, mut eeprom, eth_pins, usb) = Pins::setup(
|
||||
clocks, dp.TIM1, dp.TIM3,
|
||||
let (pins, mut leds, mut eeprom, eth_pins, usb, fan, hwrev, hw_settings) = Pins::setup(
|
||||
clocks, dp.TIM1, dp.TIM3, dp.TIM8,
|
||||
dp.GPIOA, dp.GPIOB, dp.GPIOC, dp.GPIOD, dp.GPIOE, dp.GPIOF, dp.GPIOG,
|
||||
dp.I2C1,
|
||||
dp.SPI2, dp.SPI4, dp.SPI5,
|
||||
@ -136,7 +138,6 @@ fn main() -> ! {
|
||||
usb::State::setup(usb);
|
||||
|
||||
let mut store = flash_store::store(dp.FLASH);
|
||||
|
||||
|
||||
let mut channels = Channels::new(pins);
|
||||
for c in 0..CHANNELS {
|
||||
@ -150,6 +151,8 @@ fn main() -> ! {
|
||||
}
|
||||
}
|
||||
|
||||
let mut fan_ctrl = FanCtrl::new(fan, hw_settings);
|
||||
|
||||
// default net config:
|
||||
let mut ipv4_config = Ipv4Config {
|
||||
address: [192, 168, 1, 26],
|
||||
@ -183,6 +186,8 @@ fn main() -> ! {
|
||||
server.for_each(|_, session| session.set_report_pending(channel.into()));
|
||||
}
|
||||
|
||||
fan_ctrl.cycle(channels.current_abs_max_tec_i() as f32);
|
||||
|
||||
let instant = Instant::from_millis(i64::from(timer::now()));
|
||||
cortex_m::interrupt::free(net::clear_pending);
|
||||
server.poll(instant)
|
||||
@ -206,7 +211,7 @@ fn main() -> ! {
|
||||
// Do nothing and feed more data to the line reader in the next loop cycle.
|
||||
Ok(SessionInput::Nothing) => {}
|
||||
Ok(SessionInput::Command(command)) => {
|
||||
match Handler::handle_command(command, &mut socket, &mut channels, session, &mut leds, &mut store, &mut ipv4_config) {
|
||||
match Handler::handle_command(command, &mut socket, &mut channels, session, &mut leds, &mut store, &mut ipv4_config, &mut fan_ctrl, hwrev) {
|
||||
Ok(Handler::NewIPV4(ip)) => new_ipv4_config = Some(ip),
|
||||
Ok(Handler::Handled) => {},
|
||||
Ok(Handler::CloseSocket) => socket.close(),
|
||||
|
25
src/pins.rs
25
src/pins.rs
@ -23,7 +23,7 @@ use stm32f4xx_hal::{
|
||||
I2C1,
|
||||
OTG_FS_GLOBAL, OTG_FS_DEVICE, OTG_FS_PWRCLK,
|
||||
SPI2, SPI4, SPI5,
|
||||
TIM1, TIM3,
|
||||
TIM1, TIM3, TIM8
|
||||
},
|
||||
timer::Timer,
|
||||
time::U32Ext,
|
||||
@ -33,6 +33,8 @@ use stm32_eth::EthPins;
|
||||
use crate::{
|
||||
channel::{Channel0, Channel1},
|
||||
leds::Leds,
|
||||
fan_ctrl::FanPin,
|
||||
hw_rev::{HWRev, HWSettings},
|
||||
};
|
||||
|
||||
pub type Eeprom = Eeprom24x<
|
||||
@ -101,6 +103,13 @@ pub struct ChannelPinSet<C: ChannelPins> {
|
||||
pub tec_u_meas_pin: C::TecUMeasPin,
|
||||
}
|
||||
|
||||
pub struct HWRevPins {
|
||||
pub hwrev0: stm32f4xx_hal::gpio::gpiod::PD0<Input<Floating>>,
|
||||
pub hwrev1: stm32f4xx_hal::gpio::gpiod::PD1<Input<Floating>>,
|
||||
pub hwrev2: stm32f4xx_hal::gpio::gpiod::PD2<Input<Floating>>,
|
||||
pub hwrev3: stm32f4xx_hal::gpio::gpiod::PD3<Input<Floating>>,
|
||||
}
|
||||
|
||||
pub struct Pins {
|
||||
pub adc_spi: AdcSpi,
|
||||
pub adc_nss: AdcNss,
|
||||
@ -114,13 +123,13 @@ impl Pins {
|
||||
/// Setup GPIO pins and configure MCU peripherals
|
||||
pub fn setup(
|
||||
clocks: Clocks,
|
||||
tim1: TIM1, tim3: TIM3,
|
||||
tim1: TIM1, tim3: TIM3, tim8: TIM8,
|
||||
gpioa: GPIOA, gpiob: GPIOB, gpioc: GPIOC, gpiod: GPIOD, gpioe: GPIOE, gpiof: GPIOF, gpiog: GPIOG,
|
||||
i2c1: I2C1,
|
||||
spi2: SPI2, spi4: SPI4, spi5: SPI5,
|
||||
adc1: ADC1,
|
||||
otg_fs_global: OTG_FS_GLOBAL, otg_fs_device: OTG_FS_DEVICE, otg_fs_pwrclk: OTG_FS_PWRCLK,
|
||||
) -> (Self, Leds, Eeprom, EthernetPins, USB) {
|
||||
) -> (Self, Leds, Eeprom, EthernetPins, USB, Option<FanPin>, HWRev, HWSettings) {
|
||||
let gpioa = gpioa.split();
|
||||
let gpiob = gpiob.split();
|
||||
let gpioc = gpioc.split();
|
||||
@ -189,6 +198,10 @@ impl Pins {
|
||||
channel1,
|
||||
};
|
||||
|
||||
let hwrev = HWRev::detect_hw_rev(&HWRevPins {hwrev0: gpiod.pd0, hwrev1: gpiod.pd1,
|
||||
hwrev2: gpiod.pd2, hwrev3: gpiod.pd3});
|
||||
let hw_settings = hwrev.settings();
|
||||
|
||||
let leds = Leds::new(gpiod.pd9, gpiod.pd10.into_push_pull_output(), gpiod.pd11.into_push_pull_output());
|
||||
|
||||
let eeprom_scl = gpiob.pb8.into_alternate().set_open_drain();
|
||||
@ -215,7 +228,11 @@ impl Pins {
|
||||
hclk: clocks.hclk(),
|
||||
};
|
||||
|
||||
(pins, leds, eeprom, eth_pins, usb)
|
||||
let fan = if hw_settings.fan_available {
|
||||
Some(Timer::new(tim8, &clocks).pwm(gpioc.pc9.into_alternate(), hw_settings.fan_pwm_freq_hz.hz()))
|
||||
} else { None };
|
||||
|
||||
(pins, leds, eeprom, eth_pins, usb, fan, hwrev, hw_settings)
|
||||
}
|
||||
|
||||
/// Configure the GPIO pins for SPI operation, and initialize SPI
|
||||
|
Loading…
Reference in New Issue
Block a user