Support fan PWM settings #73

Merged
sb10q merged 16 commits from esavkin/thermostat:69-fan_pwm into master 2023-03-22 17:15:49 +08:00
7 changed files with 351 additions and 199 deletions
Showing only changes of commit 58650d37f1 - Show all commits

View File

@ -94,38 +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) |
| `fan-restore` | Set fan controller coefficients to defaults (see *Fan control* section) |
## USB ## USB
@ -274,10 +276,14 @@ The thermostat implements a PID control loop for each of the TEC channels, more
## Fan control ## Fan control
sb10q marked this conversation as resolved
Review

Why is it so approximate?

Why is it so approximate?
Review

Mostly because I cannot guarantee it is precise and would not like to see how someone would tie something-very-important to its value.

Mostly because I cannot guarantee it is precise and would not like to see how someone would tie something-very-important to its value.
Fan control is available for the thermostat revisions with integrated fan system. For this purpose two commands are available: 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`, `tacho`, `abs_max_tec_i`, `auto_mode`. Please note that `tacho` shows *approximate* value, which 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. 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, 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. `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. 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. 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`,
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.
4. `fan-restore` - restore fan settings to defaults: `auto = true, a = 1.0, b = 0.0, c = 0.0`.

View File

@ -19,22 +19,13 @@ use crate::{
command_parser::{CenterPoint, PwmPin}, command_parser::{CenterPoint, PwmPin},
pins, pins,
steinhart_hart, steinhart_hart,
fan_ctrl::HWRev,
}; };
use crate::pins::HWRevPins;
pub const CHANNELS: usize = 2; 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)]
pub struct HWRev {
pub major: u8,
pub minor: u8
}
// TODO: -pub // TODO: -pub
pub struct Channels { pub struct Channels {
@ -44,8 +35,7 @@ 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,
hw_rev: HWRev, pub hwrev: HWRev,
fan_auto: bool
} }
impl Channels { impl Channels {
@ -68,7 +58,7 @@ impl Channels {
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, let mut channels = Channels { channel0, channel1, adc, pins_adc, pwm,
hw_rev: Self::detect_hw_rev(&pins.hwrev), fan_auto: true }; hwrev: HWRev::detect_hw_rev(&pins.hwrev)};
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);
@ -77,21 +67,6 @@ impl Channels {
channels channels
} }
fn detect_hw_rev(hwrev_pins: &HWRevPins) -> HWRev {
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 fan_available(&self) -> bool {
self.hw_rev.major == 2 && self.hw_rev.minor == 2
}
pub fn channel_state<I: Into<usize>>(&mut self, channel: I) -> &mut ChannelState { pub fn channel_state<I: Into<usize>>(&mut self, channel: I) -> &mut ChannelState {
match channel.into() { match channel.into() {
0 => &mut self.channel0.state, 0 => &mut self.channel0.state,
@ -365,8 +340,6 @@ impl Channels {
match (channel, pin) { match (channel, pin) {
(_, PwmPin::ISet) => (_, PwmPin::ISet) =>
panic!("i_set is no pwm pin"), panic!("i_set is no pwm pin"),
(_, PwmPin::Fan) =>
get(&self.pwm.fan),
(0, PwmPin::MaxIPos) => (0, PwmPin::MaxIPos) =>
get(&self.pwm.max_i_pos0), get(&self.pwm.max_i_pos0),
(0, PwmPin::MaxINeg) => (0, PwmPin::MaxINeg) =>
@ -422,8 +395,6 @@ impl Channels {
match (channel, pin) { match (channel, pin) {
(_, PwmPin::ISet) => (_, PwmPin::ISet) =>
panic!("i_set is no pwm pin"), panic!("i_set is no pwm pin"),
(_, PwmPin::Fan) =>
set(&mut self.pwm.fan, duty),
(0, PwmPin::MaxIPos) => (0, PwmPin::MaxIPos) =>
set(&mut self.pwm.max_i_pos0, duty), set(&mut self.pwm.max_i_pos0, duty),
(0, PwmPin::MaxINeg) => (0, PwmPin::MaxINeg) =>
@ -462,19 +433,6 @@ impl Channels {
(duty * max, max) (duty * max, max)
} }
pub fn set_fan_pwm(&mut self, fan_pwm: u32) -> f64 {
let duty = fan_pwm as f64 / MAX_FAN_PWM;
self.set_pwm(0, PwmPin::Fan, duty)
}
pub fn get_fan_pwm(&mut self) -> u32 {
(self.get_pwm(0, PwmPin::Fan) * 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 {
let vref = self.channel_state(channel).vref; let vref = self.channel_state(channel).vref;
let i_set = self.get_i(channel); let i_set = self.get_i(channel);
@ -500,7 +458,7 @@ impl Channels {
tec_i, tec_i,
tec_u_meas: self.get_tec_v(channel), tec_u_meas: self.get_tec_v(channel),
pid_output, pid_output,
hwrev: self.hw_rev hwrev: self.hwrev
} }
} }
@ -566,39 +524,14 @@ impl Channels {
serde_json_core::to_vec(&summaries) serde_json_core::to_vec(&summaries)
} }
fn current_abs_max_tec_i(&mut self) -> f64 { pub fn current_abs_max_tec_i(&mut self) -> f64 {
max_by(self.get_tec_i(0).abs().get::<ampere>(), max_by(self.get_tec_i(0).abs().get::<ampere>(),
self.get_tec_i(1).abs().get::<ampere>(), self.get_tec_i(1).abs().get::<ampere>(),
|a, b| a.partial_cmp(b).unwrap_or(core::cmp::Ordering::Equal)) |a, b| a.partial_cmp(b).unwrap_or(core::cmp::Ordering::Equal))
} }
pub fn fan_summary(&mut self, tacho: Option<u32>) -> Result<JsonBuffer, serde_json_core::ser::Error> {
if self.fan_available() {
let summary = FanSummary {
fan_pwm: self.get_fan_pwm(),
tacho: tacho.unwrap_or(u32::MAX),
abs_max_tec_i: self.current_abs_max_tec_i(),
auto_mode: self.fan_auto
};
serde_json_core::to_vec(&summary)
} else {
let summary: Option<()> = None;
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>; pub type JsonBuffer = Vec<u8, U1024>;
#[derive(Serialize)] #[derive(Serialize)]
pub struct Report { pub struct Report {
@ -670,11 +603,3 @@ pub struct SteinhartHartSummary {
channel: usize, channel: usize,
params: steinhart_hart::Parameters, params: steinhart_hart::Parameters,
} }
#[derive(Serialize)]
pub struct FanSummary {
fan_pwm: u32,
tacho: u32,
abs_max_tec_i: f64,
auto_mode: bool,
}

View File

@ -22,7 +22,8 @@ use super::{
config::ChannelConfig, config::ChannelConfig,
dfu, dfu,
flash_store::FlashStore, flash_store::FlashStore,
session::Session session::Session,
FanCtrl,
}; };
use uom::{ use uom::{
@ -199,9 +200,6 @@ impl Handler {
let current = ElectricCurrent::new::<ampere>(value); let current = ElectricCurrent::new::<ampere>(value);
channels.set_max_i_neg(channel, current); channels.set_max_i_neg(channel, current);
} }
PwmPin::Fan => {
channels.set_fan_pwm(value as u32);
}
} }
send_line(socket, b"{}"); send_line(socket, b"{}");
Ok(Handler::Handled) Ok(Handler::Handled)
@ -344,14 +342,14 @@ impl Handler {
Ok(Handler::Reset) Ok(Handler::Reset)
} }
fn fan (socket: &mut TcpSocket, channels: &mut Channels, fan_pwm: Option<u32>, tacho_value: Option<u32>) -> 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) => {
channels.set_fan_auto_mode(val == 0); fan_ctrl.set_auto_mode(val == 0);
channels.set_fan_pwm(val); fan_ctrl.set_pwm(val);
}, },
None => { None => {
match channels.fan_summary(tacho_value) { match fan_ctrl.summary() {
Ok(buf) => { Ok(buf) => {
send_line(socket, &buf); send_line(socket, &buf);
return Ok(Handler::Handled); return Ok(Handler::Handled);
@ -368,7 +366,19 @@ impl Handler {
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, tacho_value: Option<u32>) -> Result<Self, Error> { fn fan_coeff (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);
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)
}
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> {
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),
@ -391,7 +401,9 @@ impl Handler {
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(channels),
Command::Dfu => Handler::dfu(channels), Command::Dfu => Handler::dfu(channels),
Command::Fan {fan_pwm} => Handler::fan(socket, channels, fan_pwm, tacho_value) 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::FanDefaults => Handler::fan_defaults(socket, fan_ctrl),
} }
} }
} }

View File

@ -1,16 +1,7 @@
use core::fmt; use core::fmt;
use core::num::ParseIntError; use core::num::ParseIntError;
use core::str::{from_utf8, Utf8Error}; use core::str::{from_utf8, Utf8Error};
use nom::{ use nom::{IResult, branch::alt, bytes::complete::{is_a, tag, take_while1}, character::{is_digit, complete::{char, one_of}}, combinator::{complete, map, opt, value}, sequence::preceded, multi::{fold_many0, fold_many1}, error::ErrorKind, Needed};
IResult,
branch::alt,
bytes::complete::{is_a, tag, take_while1},
character::{is_digit, complete::{char, one_of}},
combinator::{complete, map, opt, value},
sequence::preceded,
multi::{fold_many0, fold_many1},
error::ErrorKind,
};
use num_traits::{Num, ParseFloatError}; use num_traits::{Num, ParseFloatError};
use serde::{Serialize, Deserialize}; use serde::{Serialize, Deserialize};
@ -127,7 +118,6 @@ pub enum PwmPin {
MaxIPos, MaxIPos,
MaxINeg, MaxINeg,
MaxV, MaxV,
Fan
} }
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
@ -181,7 +171,13 @@ pub enum Command {
Dfu, Dfu,
Fan { Fan {
fan_pwm: Option<u32> fan_pwm: Option<u32>
} },
FanCoeff {
k_a: f64,
k_b: f64,
k_c: f64,
},
FanDefaults,
} }
fn end(input: &[u8]) -> IResult<&[u8], ()> { fn end(input: &[u8]) -> IResult<&[u8], ()> {
@ -540,6 +536,33 @@ 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>> {
let (input, _) = tag("fcurve")(input)?;
let (input, coeffs) = alt((
|input| {
let (input, _) = whitespace(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)?;
let (input, _) = end(input)?;
if k_a.is_ok() && k_b.is_ok() && k_c.is_ok() {
Ok((input, Some((k_a.unwrap(), k_b.unwrap(), k_c.unwrap()))))
} else {
Err(nom::Err::Incomplete(Needed::Size(3)))
}
},
value(None, end)
))(input)?;
let result = match coeffs {
Some(coeffs) => Ok(Command::FanCoeff { k_a: coeffs.0, k_b: coeffs.1, k_c: coeffs.2 }),
None => Err(Error::ParseFloat)
};
Ok((input, result))
}
fn command(input: &[u8]) -> IResult<&[u8], Result<Command, Error>> { fn command(input: &[u8]) -> IResult<&[u8], Result<Command, Error>> {
alt((value(Ok(Command::Quit), tag("quit")), alt((value(Ok(Command::Quit), tag("quit")),
load, load,
@ -553,7 +576,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")),
fan, fan,
fan_coeff,
))(input) ))(input)
} }

218
src/fan_ctrl.rs Normal file
View File

@ -0,0 +1,218 @@
use core::{cmp::max_by};
use serde::Serialize;
use stm32f4xx_hal::{
pwm::{self, PwmChannels},
pac::TIM8,
gpio::{
Floating, Input, ExtiPin,
gpioc::PC8, Edge,
},
stm32::EXTI,
syscfg::{SysCfg},
};
use smoltcp::time::Instant;
use crate::{
pins::HWRevPins,
channels::{Channels, JsonBuffer},
timer
};
pub type FanPin = PwmChannels<TIM8, pwm::C4>;
pub type TachoPin = PC8<Input<Floating>>;
const MAX_TEC_I: f64 = 3.0;
// as stated in the schematics
const MAX_FAN_PWM: f64 = 100.0;
Review

Those naive values will need proper testing and determination at some point.

Those naive values will need proper testing and determination at some point.
const MIN_FAN_PWM: f64 = 1.0;
const TACHO_MEASURE_MS: i64 = 2500;
const DEFAULT_K_A: f64 = 1.0;
const DEFAULT_K_B: f64 = 0.0;
const DEFAULT_K_C: f64 = 0.0;
#[derive(Serialize, Copy, Clone)]
pub struct HWRev {
pub major: u8,
pub minor: u8,
}
struct TachoCtrl {
tacho: TachoPin,
tacho_cnt: u32,
tacho_value: Option<u32>,
prev_epoch: i64,
}
pub struct FanCtrl<'a> {
fan: FanPin,
tacho: TachoCtrl,
fan_auto: bool,
available: bool,
k_a: f64,
k_b: f64,
k_c: f64,
channels: &'a mut Channels,
}
impl<'a> FanCtrl<'a> {
pub fn new(mut fan: FanPin, tacho: TachoPin, channels: &'a mut Channels, exti: &mut EXTI, syscfg: &mut SysCfg) -> Self {
let available = channels.hwrev.fan_available();
let mut tacho_ctrl = TachoCtrl::new(tacho);
if available {
fan.set_duty(0);
fan.enable();
tacho_ctrl.init(exti, syscfg);
}
Review

inline

inline
FanCtrl {
fan,
tacho: tacho_ctrl,
available,
fan_auto: true,
k_a: DEFAULT_K_A,
k_b: DEFAULT_K_B,
k_c: DEFAULT_K_C,
channels,
}
}
pub fn cycle(&mut self) {
if self.available {
self.tacho.cycle();
}
self.adjust_speed();
}
Review

this doesn't need to be pub

this doesn't need to be ``pub``
pub fn summary(&mut self) -> Result<JsonBuffer, serde_json_core::ser::Error> {
if self.available {
let summary = FanSummary {
fan_pwm: self.get_pwm(),
tacho: self.tacho.get(),
abs_max_tec_i: self.channels.current_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 adjust_speed(&mut self) {
if self.fan_auto && self.available {
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()
let pwm = max_by(MAX_FAN_PWM * (scaled_current * (scaled_current * self.k_a + self.k_b) + self.k_c),
MIN_FAN_PWM,
|a, b| a.partial_cmp(b).unwrap_or(core::cmp::Ordering::Equal)) as u32;
self.set_pwm(pwm);
}
}
#[inline]
pub fn set_auto_mode(&mut self, fan_auto: bool) {
self.fan_auto = fan_auto;
}
#[inline]
pub fn set_coefficients(&mut self, k_a: f64, k_b: f64, k_c: f64) {
self.k_a = k_a;
self.k_b = k_b;
self.k_c = k_c;
}
#[inline]
pub fn restore_defaults(&mut self) {
self.set_auto_mode(true);
self.set_coefficients(DEFAULT_K_A, DEFAULT_K_B, DEFAULT_K_C);
}
pub fn set_pwm(&mut self, fan_pwm: u32) -> f64 {
let duty = fan_pwm as f64 / MAX_FAN_PWM;
let max = self.fan.get_max_duty();
let value = ((duty * (max as f64)) as u16).min(max);
self.fan.set_duty(value);
value as f64 / (max as f64)
}
fn get_pwm(&self) -> u32 {
let duty = self.fan.get_duty();
let max = self.fan.get_max_duty();
((duty as f64 / (max as f64)) * MAX_FAN_PWM) as u32
}
}
impl TachoCtrl {
pub fn new(tacho: TachoPin) -> Self {
TachoCtrl {
tacho,
tacho_cnt: 0,
tacho_value: None,
prev_epoch: 0,
}
}
pub fn init(&mut self, exti: &mut EXTI, syscfg: &mut SysCfg) {
// These lines do not cause NVIC to run the ISR,
// since the interrupt should be unmasked in the cortex_m::peripheral::NVIC.
// Also using interrupt-related workaround is the best
// 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,
// and therefore would require even more weirder and unsafe hacks.
// Also such hacks wouldn't guarantee it to be more precise.
self.tacho.make_interrupt_source(syscfg);
self.tacho.trigger_on_edge(exti, Edge::Rising);
self.tacho.enable_interrupt(exti);
}
pub fn cycle(&mut self) {
let tacho_input = self.tacho.check_interrupt();
if tacho_input {
self.tacho.clear_interrupt_pending_bit();
self.tacho_cnt += 1;
}
let instant = Instant::from_millis(i64::from(timer::now()));
if instant.millis - self.prev_epoch >= TACHO_MEASURE_MS {
self.tacho_value = Some(self.tacho_cnt);
self.tacho_cnt = 0;
self.prev_epoch = instant.millis;
}
}
pub fn get(&self) -> u32 {
self.tacho_value.unwrap_or(u32::MAX)
}
}
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 fan_available(&self) -> bool {
self.major == 2 && self.minor == 2
}
}
#[derive(Serialize)]
pub struct FanSummary {
fan_pwm: u32,
tacho: u32,
abs_max_tec_i: f64,
auto_mode: bool,
k_a: f64,
k_b: f64,
k_c: f64,
}

View File

@ -10,7 +10,7 @@ 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::{
@ -19,7 +19,6 @@ 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 syscfg::SysCfgExt
}; };
use smoltcp::{ use smoltcp::{
@ -56,6 +55,8 @@ mod flash_store;
mod dfu; mod dfu;
mod command_handler; mod command_handler;
use command_handler::Handler; use command_handler::Handler;
mod fan_ctrl;
use fan_ctrl::FanCtrl;
const HSE: MegaHertz = MegaHertz(8); const HSE: MegaHertz = MegaHertz(8);
#[cfg(not(feature = "semihosting"))] #[cfg(not(feature = "semihosting"))]
@ -120,7 +121,7 @@ fn main() -> ! {
timer::setup(cp.SYST, clocks); timer::setup(cp.SYST, clocks);
let (pins, mut leds, mut eeprom, eth_pins, usb, mut tacho) = Pins::setup( let (pins, mut leds, mut eeprom, eth_pins, usb, fan, tacho) = Pins::setup(
clocks, dp.TIM1, dp.TIM3, dp.TIM8, clocks, dp.TIM1, dp.TIM3, dp.TIM8,
dp.GPIOA, dp.GPIOB, dp.GPIOC, dp.GPIOD, dp.GPIOE, dp.GPIOF, dp.GPIOG, dp.GPIOA, dp.GPIOB, dp.GPIOC, dp.GPIOD, dp.GPIOE, dp.GPIOF, dp.GPIOG,
dp.I2C1, dp.I2C1,
@ -137,22 +138,22 @@ fn main() -> ! {
usb::State::setup(usb); usb::State::setup(usb);
let mut channels = RefCell::new(Channels::new(pins));
let mut store = flash_store::store(dp.FLASH); let mut store = flash_store::store(dp.FLASH);
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(&mut channels, c), config.apply(channels.get_mut(), 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,
let fan_available = channels.fan_available(); // while mutex would be excessive
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 {
@ -174,54 +175,22 @@ fn main() -> ! {
let hwaddr = EthernetAddress(eui48); let hwaddr = EthernetAddress(eui48);
info!("EEPROM MAC address: {}", hwaddr); info!("EEPROM MAC address: {}", hwaddr);
if fan_available {
// These lines do not cause NVIC to run the ISR,
// since the interrupt should be unmasked in the cortex_m::peripheral::NVIC.
// Also using interrupt-related workaround is the best
// 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,
// and therefore would require even more weirder and unsafe hacks.
// Also such hacks wouldn't guarantee it to be more precise.
tacho.make_interrupt_source(&mut dp.SYSCFG.constrain());
tacho.trigger_on_edge(&mut dp.EXTI, Edge::Rising);
tacho.enable_interrupt(&mut dp.EXTI);
}
net::run(clocks, dp.ETHERNET_MAC, dp.ETHERNET_DMA, eth_pins, hwaddr, ipv4_config.clone(), |iface| { net::run(clocks, dp.ETHERNET_MAC, dp.ETHERNET_DMA, eth_pins, hwaddr, ipv4_config.clone(), |iface| {
Server::<Session>::run(iface, |server| { Server::<Session>::run(iface, |server| {
leds.r1.off(); leds.r1.off();
let mut should_reset = false; let mut should_reset = false;
let (mut tacho_cnt, mut tacho_value) = (0u32, None);
let mut prev_epoch: i64 = 0;
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.poll_adc(instant); let updated_channel = channels.get_mut().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()));
} }
fan_ctrl.cycle();
let instant = Instant::from_millis(i64::from(timer::now())); let instant = Instant::from_millis(i64::from(timer::now()));
if fan_available {
let tacho_input = tacho.check_interrupt();
if tacho_input {
tacho.clear_interrupt_pending_bit();
tacho_cnt += 1;
}
let epoch = instant.secs();
if epoch > prev_epoch {
tacho_value = Some(tacho_cnt);
tacho_cnt = 0;
prev_epoch = epoch;
}
}
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| {
@ -244,7 +213,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, &mut channels, session, &mut leds, &mut store, &mut ipv4_config, tacho_value) { match Handler::handle_command(command, &mut socket, channels.get_mut(), 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(),
@ -261,7 +230,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.reports_json() { match channels.get_mut().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);

View File

@ -26,15 +26,18 @@ use stm32f4xx_hal::{
TIM1, TIM3, TIM8 TIM1, TIM3, TIM8
}, },
timer::Timer, timer::Timer,
time::U32Ext, time::{U32Ext, KiloHertz},
}; };
use eeprom24x::{self, Eeprom24x}; use eeprom24x::{self, Eeprom24x};
use stm32_eth::EthPins; use stm32_eth::EthPins;
use crate::{ use crate::{
channel::{Channel0, Channel1}, channel::{Channel0, Channel1},
leds::Leds, leds::Leds,
fan_ctrl::{TachoPin, FanPin}
}; };
const PWM_FREQ: KiloHertz = KiloHertz(20u32);
pub type Eeprom = Eeprom24x< pub type Eeprom = Eeprom24x<
I2c<I2C1, ( I2c<I2C1, (
PB8<AlternateOD<{ stm32f4xx_hal::gpio::AF4 }>>, PB8<AlternateOD<{ stm32f4xx_hal::gpio::AF4 }>>,
@ -128,7 +131,7 @@ impl Pins {
spi2: SPI2, spi4: SPI4, spi5: SPI5, spi2: SPI2, spi4: SPI4, spi5: SPI5,
adc1: ADC1, adc1: ADC1,
otg_fs_global: OTG_FS_GLOBAL, otg_fs_device: OTG_FS_DEVICE, otg_fs_pwrclk: OTG_FS_PWRCLK, otg_fs_global: OTG_FS_GLOBAL, otg_fs_device: OTG_FS_DEVICE, otg_fs_pwrclk: OTG_FS_PWRCLK,
) -> (Self, Leds, Eeprom, EthernetPins, USB, PC8<Input<Floating>>) { ) -> (Self, Leds, Eeprom, EthernetPins, USB, FanPin, TachoPin) {
let gpioa = gpioa.split(); let gpioa = gpioa.split();
let gpiob = gpiob.split(); let gpiob = gpiob.split();
let gpioc = gpioc.split(); let gpioc = gpioc.split();
@ -143,10 +146,10 @@ impl Pins {
let pins_adc = Adc::adc1(adc1, true, Default::default()); let pins_adc = Adc::adc1(adc1, true, Default::default());
let pwm = PwmPins::setup( let pwm = PwmPins::setup(
clocks, tim1, tim3, tim8, clocks, tim1, tim3,
gpioc.pc6, gpioc.pc7, gpioc.pc6, gpioc.pc7,
gpioe.pe9, gpioe.pe11, gpioe.pe9, gpioe.pe11,
gpioe.pe13, gpioe.pe14, gpioc.pc9 gpioe.pe13, gpioe.pe14
); );
let (dac0_spi, dac0_sync) = Self::setup_dac0( let (dac0_spi, dac0_sync) = Self::setup_dac0(
@ -225,7 +228,9 @@ impl Pins {
hclk: clocks.hclk(), hclk: clocks.hclk(),
}; };
(pins, leds, eeprom, eth_pins, usb, gpioc.pc8) let fan = Timer::new(tim8, &clocks).pwm(gpioc.pc9.into_alternate(), 20u32.khz());
(pins, leds, eeprom, eth_pins, usb, fan, gpioc.pc8)
} }
/// Configure the GPIO pins for SPI operation, and initialize SPI /// Configure the GPIO pins for SPI operation, and initialize SPI
@ -293,24 +298,20 @@ pub struct PwmPins {
pub max_i_pos1: PwmChannels<TIM1, pwm::C2>, pub max_i_pos1: PwmChannels<TIM1, pwm::C2>,
pub max_i_neg0: PwmChannels<TIM1, pwm::C3>, pub max_i_neg0: PwmChannels<TIM1, pwm::C3>,
pub max_i_neg1: PwmChannels<TIM1, pwm::C4>, pub max_i_neg1: PwmChannels<TIM1, pwm::C4>,
pub fan: PwmChannels<TIM8, pwm::C4>,
} }
impl PwmPins { impl PwmPins {
fn setup<M1, M2, M3, M4, M5, M6, M7>( fn setup<M1, M2, M3, M4, M5, M6>(
clocks: Clocks, clocks: Clocks,
tim1: TIM1, tim1: TIM1,
tim3: TIM3, tim3: TIM3,
tim8: TIM8,
max_v0: PC6<M1>, max_v0: PC6<M1>,
max_v1: PC7<M2>, max_v1: PC7<M2>,
max_i_pos0: PE9<M3>, max_i_pos0: PE9<M3>,
max_i_pos1: PE11<M4>, max_i_pos1: PE11<M4>,
max_i_neg0: PE13<M5>, max_i_neg0: PE13<M5>,
max_i_neg1: PE14<M6>, max_i_neg1: PE14<M6>,
fan: PC9<M7>,
) -> PwmPins { ) -> PwmPins {
let freq = 20u32.khz();
fn init_pwm_pin<P: hal::PwmPin<Duty=u16>>(pin: &mut P) { fn init_pwm_pin<P: hal::PwmPin<Duty=u16>>(pin: &mut P) {
pin.set_duty(0); pin.set_duty(0);
@ -320,14 +321,11 @@ impl PwmPins {
max_v0.into_alternate(), max_v0.into_alternate(),
max_v1.into_alternate(), max_v1.into_alternate(),
); );
//let (mut max_v0, mut max_v1) = pwm::tim3(tim3, channels, clocks, freq); //let (mut max_v0, mut max_v1) = pwm::tim3(tim3, channels, clocks, PWM_FREQ);
let (mut max_v0, mut max_v1) = Timer::new(tim3, &clocks).pwm(channels, freq); let (mut max_v0, mut max_v1) = Timer::new(tim3, &clocks).pwm(channels, PWM_FREQ);
init_pwm_pin(&mut max_v0); init_pwm_pin(&mut max_v0);
init_pwm_pin(&mut max_v1); init_pwm_pin(&mut max_v1);
let mut fan = Timer::new(tim8, &clocks).pwm(fan.into_alternate(), freq);
init_pwm_pin(&mut fan);
let channels = ( let channels = (
max_i_pos0.into_alternate(), max_i_pos0.into_alternate(),
max_i_pos1.into_alternate(), max_i_pos1.into_alternate(),
@ -335,7 +333,7 @@ impl PwmPins {
max_i_neg1.into_alternate(), max_i_neg1.into_alternate(),
); );
let (mut max_i_pos0, mut max_i_pos1, mut max_i_neg0, mut max_i_neg1) = let (mut max_i_pos0, mut max_i_pos1, mut max_i_neg0, mut max_i_neg1) =
Timer::new(tim1, &clocks).pwm(channels, freq); Timer::new(tim1, &clocks).pwm(channels, PWM_FREQ);
init_pwm_pin(&mut max_i_pos0); init_pwm_pin(&mut max_i_pos0);
init_pwm_pin(&mut max_i_neg0); init_pwm_pin(&mut max_i_neg0);
init_pwm_pin(&mut max_i_pos1); init_pwm_pin(&mut max_i_pos1);
@ -344,8 +342,7 @@ impl PwmPins {
PwmPins { PwmPins {
max_v0, max_v1, max_v0, max_v1,
max_i_pos0, max_i_pos1, max_i_pos0, max_i_pos1,
max_i_neg0, max_i_neg1, max_i_neg0, max_i_neg1
fan
} }
} }
} }