From 570c0324b398672fff42e5bd874f2eacbf48bb67 Mon Sep 17 00:00:00 2001 From: Egor Savkin Date: Wed, 22 Mar 2023 17:15:49 +0800 Subject: [PATCH] implement support for fan PWM Co-authored-by: Egor Savkin Co-committed-by: Egor Savkin --- README.md | 78 +++++++++++++-------- src/channels.rs | 12 +++- src/command_handler.rs | 96 +++++++++++++++++++++++--- src/command_parser.rs | 107 +++++++++++++++++++++++++++++ src/fan_ctrl.rs | 151 +++++++++++++++++++++++++++++++++++++++++ src/hw_rev.rs | 82 ++++++++++++++++++++++ src/main.rs | 15 ++-- src/pins.rs | 25 +++++-- 8 files changed, 516 insertions(+), 50 deletions(-) create mode 100644 src/fan_ctrl.rs create mode 100644 src/hw_rev.rs diff --git a/README.md b/README.md index 10d2fc7..f1bcc1b 100644 --- a/README.md +++ b/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 ` | Set report mode | -| `pwm` | Show current PWM settings | -| `pwm <0/1> max_i_pos ` | Set maximum positive output current | -| `pwm <0/1> max_i_neg ` | Set maximum negative output current | -| `pwm <0/1> max_v ` | Set maximum output voltage | -| `pwm <0/1> i_set ` | Disengage PID, set fixed output current | -| `pwm <0/1> pid` | Let output current to be controlled by the PID | -| `center <0/1> ` | 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 ` | Set the PID controller target temperature | -| `pid <0/1> kp ` | Set proportional gain | -| `pid <0/1> ki ` | Set integral gain | -| `pid <0/1> kd ` | Set differential gain | -| `pid <0/1> output_min ` | Set mininum output | -| `pid <0/1> output_max ` | Set maximum output | -| `s-h` | Show Steinhart-Hart equation parameters | -| `s-h <0/1> ` | Set Steinhart-Hart parameter for a channel | -| `postfilter` | Show postfilter settings | -| `postfilter <0/1> off` | Disable postfilter | -| `postfilter <0/1> 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 [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 ` | Set report mode | +| `pwm` | Show current PWM settings | +| `pwm <0/1> max_i_pos ` | Set maximum positive output current | +| `pwm <0/1> max_i_neg ` | Set maximum negative output current | +| `pwm <0/1> max_v ` | Set maximum output voltage | +| `pwm <0/1> i_set ` | Disengage PID, set fixed output current | +| `pwm <0/1> pid` | Let output current to be controlled by the PID | +| `center <0/1> ` | 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 ` | Set the PID controller target temperature | +| `pid <0/1> kp ` | Set proportional gain | +| `pid <0/1> ki ` | Set integral gain | +| `pid <0/1> kd ` | Set differential gain | +| `pid <0/1> output_min ` | Set mininum output | +| `pid <0/1> output_max ` | Set maximum output | +| `s-h` | Show Steinhart-Hart equation parameters | +| `s-h <0/1> ` | Set Steinhart-Hart parameter for a channel | +| `postfilter` | Show postfilter settings | +| `postfilter <0/1> off` | Disable postfilter | +| `postfilter <0/1> 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 [Y.Y.Y.Y]` | Configure IPv4 address, netmask length, and optional default gateway | +| `fan` | Show current fan settings and sensors' measurements | +| `fan ` | Set fan power with values from 1 to 100 | +| `fan auto` | Enable automatic fan speed control | +| `fcurve ` | 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 ` - 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 ` - 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`. diff --git a/src/channels.rs b/src/channels.rs index 7aa34e9..c86305d 100644 --- a/src/channels.rs +++ b/src/channels.rs @@ -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; + pub fn current_abs_max_tec_i(&mut self) -> f64 { + max_by(self.get_tec_i(0).abs().get::(), + self.get_tec_i(1).abs().get::(), + |a, b| a.partial_cmp(b).unwrap_or(core::cmp::Ordering::Equal)) + } +} #[derive(Serialize)] pub struct Report { diff --git a/src/command_handler.rs b/src/command_handler.rs index 3144b0c..b2d3ae0 100644 --- a/src/command_handler.rs +++ b/src/command_handler.rs @@ -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; + 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 { + fn set_fan(socket: &mut TcpSocket, fan_pwm: u32, fan_ctrl: &mut FanCtrl) -> Result { + 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 { + 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 { + 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 { + 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 { + fan_ctrl.restore_defaults(); + send_line(socket, b"{}"); + Ok(Handler::Handled) + } + + fn show_hwrev(socket: &mut TcpSocket, hwrev: HWRev) -> Result { + 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 { 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), } } } \ No newline at end of file diff --git a/src/command_parser.rs b/src/command_parser.rs index 622f819..b9e70bb 100644 --- a/src/command_parser.rs +++ b/src/command_parser.rs @@ -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, }, 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> { ))(input) } +fn fan(input: &[u8]) -> IResult<&[u8], Result> { + 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> { + 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> { alt((value(Ok(Command::Quit), tag("quit")), load, @@ -533,6 +597,9 @@ fn command(input: &[u8]) -> IResult<&[u8], Result> { 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)); + } } diff --git a/src/fan_ctrl.rs b/src/fan_ctrl.rs new file mode 100644 index 0000000..b95a723 --- /dev/null +++ b/src/fan_ctrl.rs @@ -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; + +// 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, + 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, 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 { + 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, +} diff --git a/src/hw_rev.rs b/src/hw_rev.rs new file mode 100644 index 0000000..ef022a1 --- /dev/null +++ b/src/hw_rev.rs @@ -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 { + let settings = self.settings(); + let summary = HWSummary { rev: self, settings: &settings }; + serde_json_core::to_vec(&summary) + } +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index c1d7267..639f356 100644 --- a/src/main.rs +++ b/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(), diff --git a/src/pins.rs b/src/pins.rs index f7b55a1..6294661 100644 --- a/src/pins.rs +++ b/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 { pub tec_u_meas_pin: C::TecUMeasPin, } +pub struct HWRevPins { + pub hwrev0: stm32f4xx_hal::gpio::gpiod::PD0>, + pub hwrev1: stm32f4xx_hal::gpio::gpiod::PD1>, + pub hwrev2: stm32f4xx_hal::gpio::gpiod::PD2>, + pub hwrev3: stm32f4xx_hal::gpio::gpiod::PD3>, +} + 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, 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