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
8 changed files with 149 additions and 84 deletions
Showing only changes of commit 2c9436a0b3 - Show all commits

View File

@ -129,6 +129,7 @@ formatted as line-delimited JSON.
| `fan auto` | Enable automatic fan speed control | | `fan auto` | Enable automatic fan speed control |
| `fcurve <a> <b> <c>` | Set fan controller curve coefficients (see *Fan control* section) | | `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) | | `fcurve default` | Set fan controller curve coefficients to defaults (see *Fan control* section) |
| `hwrev` | Show hardware revision |
## USB ## USB

View File

@ -1,5 +1,5 @@
use core::cmp::max_by; use core::cmp::max_by;
use heapless::{consts::{U2, U1024}, Vec}; use heapless::{consts::U2, Vec};
use serde::{Serialize, Serializer}; use serde::{Serialize, Serializer};
use smoltcp::time::Instant; use smoltcp::time::Instant;
use stm32f4xx_hal::hal; use stm32f4xx_hal::hal;
@ -17,9 +17,9 @@ use crate::{
channel::{Channel, Channel0, Channel1}, channel::{Channel, Channel0, Channel1},
channel_state::ChannelState, channel_state::ChannelState,
command_parser::{CenterPoint, PwmPin}, command_parser::{CenterPoint, PwmPin},
command_handler::JsonBuffer,
pins, pins,
steinhart_hart, steinhart_hart,
fan_ctrl::HWRev,
}; };
pub const CHANNELS: usize = 2; pub const CHANNELS: usize = 2;
@ -35,7 +35,6 @@ pub struct Channels {
/// stm32f4 integrated adc /// stm32f4 integrated adc
pins_adc: pins::PinsAdc, pins_adc: pins::PinsAdc,
pub pwm: pins::PwmPins, pub pwm: pins::PwmPins,
pub hwrev: HWRev,
} }
impl Channels { impl Channels {
@ -57,8 +56,7 @@ impl Channels {
let channel1 = Channel::new(pins.channel1, adc_calibration1); let channel1 = Channel::new(pins.channel1, adc_calibration1);
let pins_adc = pins.pins_adc; let pins_adc = pins.pins_adc;
let pwm = pins.pwm; let pwm = pins.pwm;
let mut channels = Channels { channel0, channel1, adc, pins_adc, pwm, let mut channels = Channels { channel0, channel1, adc, pins_adc, pwm };
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);
@ -458,7 +456,6 @@ 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.hwrev
} }
} }
@ -531,8 +528,6 @@ impl Channels {
} }
} }
pub type JsonBuffer = Vec<u8, U1024>;
#[derive(Serialize)] #[derive(Serialize)]
pub struct Report { pub struct Report {
channel: usize, channel: usize,
@ -550,7 +545,6 @@ pub struct Report {
tec_i: ElectricCurrent, tec_i: ElectricCurrent,
tec_u_meas: ElectricPotential, tec_u_meas: ElectricPotential,
pid_output: ElectricCurrent, pid_output: ElectricCurrent,
hwrev: HWRev,
} }
pub struct CenterPointJson(CenterPoint); pub struct CenterPointJson(CenterPoint);

View File

@ -1,6 +1,7 @@
use smoltcp::socket::TcpSocket; use smoltcp::socket::TcpSocket;
use log::{error, warn}; use log::{error, warn};
use core::fmt::Write; use core::fmt::Write;
use heapless::{consts::U1024, Vec};
use super::{ use super::{
net, net,
command_parser::{ command_parser::{
@ -24,6 +25,7 @@ use super::{
flash_store::FlashStore, flash_store::FlashStore,
session::Session, session::Session,
FanCtrl, FanCtrl,
hw_rev::HWRev,
}; };
use uom::{ use uom::{
@ -56,6 +58,8 @@ pub enum Error {
FlashError FlashError
} }
pub type JsonBuffer = Vec<u8, U1024>;
fn send_line(socket: &mut TcpSocket, data: &[u8]) -> bool { fn send_line(socket: &mut TcpSocket, data: &[u8]) -> bool {
let send_free = socket.send_capacity() - socket.send_queue(); let send_free = socket.send_capacity() - socket.send_queue();
if data.len() > send_free + 1 { if data.len() > send_free + 1 {
@ -345,7 +349,11 @@ impl Handler {
fn set_fan(socket: &mut TcpSocket, fan_pwm: u32, fan_ctrl: &mut FanCtrl) -> Result<Handler, Error> { fn set_fan(socket: &mut TcpSocket, fan_pwm: u32, fan_ctrl: &mut FanCtrl) -> Result<Handler, Error> {
fan_ctrl.set_auto_mode(false); fan_ctrl.set_auto_mode(false);
fan_ctrl.set_pwm(fan_pwm); fan_ctrl.set_pwm(fan_pwm);
send_line(socket, b"{}"); if fan_ctrl.is_default_auto() {
send_line(socket, b"{}");
} else {
send_line(socket, b"{ \"warning\": \"this fan doesn't have full PWM support. Use it on your own risk!\" }");
}
Ok(Handler::Handled) Ok(Handler::Handled)
} }
@ -365,11 +373,15 @@ impl Handler {
fn fan_auto(socket: &mut TcpSocket, fan_ctrl: &mut FanCtrl) -> Result<Handler, Error> { fn fan_auto(socket: &mut TcpSocket, fan_ctrl: &mut FanCtrl) -> Result<Handler, Error> {
fan_ctrl.set_auto_mode(true); fan_ctrl.set_auto_mode(true);
send_line(socket, b"{}"); if fan_ctrl.is_default_auto() {
send_line(socket, b"{}");
} else {
send_line(socket, b"{ \"warning\": \"this fan doesn't have full PWM support. Use it on your own risk!\" }");
}
Ok(Handler::Handled) Ok(Handler::Handled)
} }
fn fan_curve(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: f32, k_b: f32, k_c: f32) -> Result<Handler, Error> {
fan_ctrl.set_curve(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)
@ -381,7 +393,21 @@ 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, fan_ctrl: &mut FanCtrl) -> Result<Self, Error> { 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 { match command {
Command::Quit => Ok(Handler::CloseSocket), Command::Quit => Ok(Handler::CloseSocket),
Command::Reporting(_reporting) => Handler::reporting(socket), Command::Reporting(_reporting) => Handler::reporting(socket),
@ -409,6 +435,7 @@ impl Handler {
Command::FanAuto => Handler::fan_auto(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::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::FanCurveDefaults => Handler::fan_defaults(socket, fan_ctrl),
Command::ShowHWRev => Handler::show_hwrev(socket, hwrev),
} }
} }
} }

View File

@ -185,11 +185,12 @@ pub enum Command {
FanAuto, FanAuto,
ShowFan, ShowFan,
FanCurve { FanCurve {
k_a: f64, k_a: f32,
k_b: f64, k_b: f32,
k_c: f64, k_c: f32,
}, },
FanCurveDefaults, FanCurveDefaults,
ShowHWRev,
} }
fn end(input: &[u8]) -> IResult<&[u8], ()> { fn end(input: &[u8]) -> IResult<&[u8], ()> {
@ -573,7 +574,7 @@ fn fan_curve(input: &[u8]) -> IResult<&[u8], Result<Command, Error>> {
let (input, k_c) = float(input)?; let (input, k_c) = float(input)?;
let (input, _) = end(input)?; let (input, _) = end(input)?;
if k_a.is_ok() && k_b.is_ok() && k_c.is_ok() { if k_a.is_ok() && k_b.is_ok() && k_c.is_ok() {
Ok((input, Ok(Command::FanCurve { k_a: k_a.unwrap(), k_b: k_b.unwrap(), k_c: k_c.unwrap() }))) 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 { } else {
Err(nom::Err::Incomplete(Needed::Size(3))) Err(nom::Err::Incomplete(Needed::Size(3)))
} }
@ -601,6 +602,7 @@ fn command(input: &[u8]) -> IResult<&[u8], Result<Command, Error>> {
value(Ok(Command::Dfu), tag("dfu")), value(Ok(Command::Dfu), tag("dfu")),
fan, fan,
fan_curve, fan_curve,
value(Ok(Command::ShowHWRev), tag("hwrev")),
))(input) ))(input)
} }
@ -856,4 +858,10 @@ mod test {
let command = Command::parse(b"fcurve default"); let command = Command::parse(b"fcurve default");
assert_eq!(command, Ok(Command::FanCurveDefaults)); assert_eq!(command, Ok(Command::FanCurveDefaults));
} }
#[test]
fn parse_hwrev() {
let command = Command::parse(b"hwrev");
assert_eq!(command, Ok(Command::ShowHWRev));
}
} }

View File

@ -6,65 +6,62 @@ use stm32f4xx_hal::{
}; };
use crate::{ use crate::{
pins::HWRevPins, hw_rev::HWRev,
channels::JsonBuffer, command_handler::JsonBuffer,
}; };
pub type FanPin = PwmChannels<TIM8, pwm::C4>; pub type FanPin = PwmChannels<TIM8, pwm::C4>;
// as stated in the schematics // as stated in the schematics
const MAX_TEC_I: f64 = 3.0; const MAX_TEC_I: f32 = 3.0;
const MAX_USER_FAN_PWM: f64 = 100.0; const MAX_USER_FAN_PWM: f32 = 100.0;
const MIN_USER_FAN_PWM: f64 = 1.0; const MIN_USER_FAN_PWM: f32 = 1.0;
const MAX_FAN_PWM: f64 = 1.0; const MAX_FAN_PWM: f32 = 1.0;
// below this value motor's autostart feature may fail // below this value motor's autostart feature may fail
const MIN_FAN_PWM: f64 = 0.04; const MIN_FAN_PWM: f32 = 0.04;
const DEFAULT_K_A: f64 = 1.0; const DEFAULT_K_A: f32 = 1.0;
const DEFAULT_K_B: f64 = 0.0; const DEFAULT_K_B: f32 = 0.0;
const DEFAULT_K_C: f64 = 0.0; const DEFAULT_K_C: f32 = 0.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.
#[derive(Serialize, Copy, Clone)]
pub struct HWRev {
pub major: u8,
pub minor: u8,
}
pub struct FanCtrl { pub struct FanCtrl {
fan: FanPin, fan: FanPin,
fan_auto: bool, fan_auto: bool,
available: bool, available: bool,
k_a: f64, default_auto: bool,
k_b: f64, pwm_enabled: bool,
k_c: f64, k_a: f32,
abs_max_tec_i: f64, k_b: f32,
k_c: f32,
abs_max_tec_i: f32,
} }
impl FanCtrl { impl FanCtrl {
pub fn new(mut fan: FanPin, hwrev: &HWRev) -> Self { pub fn new(fan: FanPin, hwrev: HWRev) -> Self {
let available = hwrev.fan_available(); let available = hwrev.fan_available();
let default_auto = hwrev.fan_default_auto();
if available { let mut fan_ctrl = FanCtrl {
fan.set_duty(0);
fan.enable();
}
FanCtrl {
fan, fan,
available, available,
// do not enable auto mode by default, // do not enable auto mode by default,
// but allow to turn it on on customer's own risk // but allow to turn it on on user's own risk
fan_auto: hwrev.fan_auto_mode_available(), default_auto,
fan_auto: default_auto,
pwm_enabled: false,
k_a: DEFAULT_K_A, k_a: DEFAULT_K_A,
k_b: DEFAULT_K_B, k_b: DEFAULT_K_B,
k_c: DEFAULT_K_C, k_c: DEFAULT_K_C,
abs_max_tec_i: 0f64, abs_max_tec_i: 0f32,
};
if fan_ctrl.fan_auto {
fan_ctrl.enable_pwm();
} }
fan_ctrl
} }
pub fn cycle(&mut self, abs_max_tec_i: f64) { pub fn cycle(&mut self, abs_max_tec_i: f32) {
self.abs_max_tec_i = abs_max_tec_i; self.abs_max_tec_i = abs_max_tec_i;
self.adjust_speed(); self.adjust_speed();
Review

inline

inline
} }
@ -99,7 +96,7 @@ impl FanCtrl {
self.fan_auto = fan_auto; self.fan_auto = fan_auto;
} }
pub fn set_curve(&mut self, k_a: f64, k_b: f64, k_c: f64) { pub fn set_curve(&mut self, k_a: f32, k_b: f32, k_c: f32) {
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;
@ -109,55 +106,47 @@ impl FanCtrl {
self.set_curve(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) -> f32 {
if !self.pwm_enabled {
self.enable_pwm()
}
let fan_pwm = fan_pwm.min(MAX_USER_FAN_PWM as u32).max(MIN_USER_FAN_PWM as u32); let fan_pwm = fan_pwm.min(MAX_USER_FAN_PWM as u32).max(MIN_USER_FAN_PWM as u32);
let duty = Self::scale_number(fan_pwm as f64, MIN_FAN_PWM, MAX_FAN_PWM, MIN_USER_FAN_PWM, MAX_USER_FAN_PWM); let duty = Self::scale_number(fan_pwm as f32, 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 f32)) as u16).min(max);
self.fan.set_duty(value); self.fan.set_duty(value);
value as f64 / (max as f64) value as f32 / (max as f32)
} }
fn scale_number(unscaled: f64, to_min: f64, to_max: f64, from_min: f64, from_max: f64) -> f64 { pub fn is_default_auto(&self) -> bool {
self.default_auto
}
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 (to_max - to_min) * (unscaled - from_min) / (from_max - from_min) + to_min
} }
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();
Self::scale_number(duty as f64 / (max as f64), MIN_USER_FAN_PWM, MAX_USER_FAN_PWM, MIN_FAN_PWM, MAX_FAN_PWM).round() as u32 Self::scale_number(duty as f32 / (max as f32), MIN_USER_FAN_PWM, MAX_USER_FAN_PWM, MIN_FAN_PWM, MAX_FAN_PWM).round() as u32
} }
}
impl HWRev { fn enable_pwm(&mut self) {
pub fn detect_hw_rev(hwrev_pins: &HWRevPins) -> Self { if self.available {
let (h0, h1, h2, h3) = (hwrev_pins.hwrev0.is_high(), hwrev_pins.hwrev1.is_high(), self.fan.set_duty(0);
hwrev_pins.hwrev2.is_high(), hwrev_pins.hwrev3.is_high()); self.fan.enable();
match (h0, h1, h2, h3) { self.pwm_enabled = true;
(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
}
pub fn fan_auto_mode_available(&self) -> bool {
// see https://github.com/sinara-hw/Thermostat/issues/115 and
// https://git.m-labs.hk/M-Labs/thermostat/issues/69#issuecomment-6464 for explanation
self.fan_available() && self.minor != 2
}
} }
#[derive(Serialize)] #[derive(Serialize)]
pub struct FanSummary { pub struct FanSummary {
fan_pwm: u32, fan_pwm: u32,
abs_max_tec_i: f64, abs_max_tec_i: f32,
auto_mode: bool, auto_mode: bool,
k_a: f64, k_a: f32,
k_b: f64, k_b: f32,
k_c: f64, k_c: f32,
} }

41
src/hw_rev.rs Normal file
View File

@ -0,0 +1,41 @@
use serde::Serialize;
use crate::{
pins::HWRevPins,
command_handler::JsonBuffer
};
#[derive(Serialize, Copy, Clone)]
pub struct HWRev {
pub major: u8,
pub minor: u8,
}
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
}
pub fn fan_default_auto(&self) -> bool {
Review

fan_pwm_recommended?

``fan_pwm_recommended``?
// see https://github.com/sinara-hw/Thermostat/issues/115 and
// https://git.m-labs.hk/M-Labs/thermostat/issues/69#issuecomment-6464 for explanation
self.fan_available() && self.minor != 2
}
pub fn summary(&self) -> Result<JsonBuffer, serde_json_core::ser::Error> {
serde_json_core::to_vec(&self)
Review

blank line

blank line
}
}

View File

@ -55,6 +55,8 @@ mod command_handler;
use command_handler::Handler; use command_handler::Handler;
mod fan_ctrl; mod fan_ctrl;
use fan_ctrl::FanCtrl; use fan_ctrl::FanCtrl;
mod hw_rev;
use hw_rev::HWRev;
const HSE: MegaHertz = MegaHertz(8); const HSE: MegaHertz = MegaHertz(8);
#[cfg(not(feature = "semihosting"))] #[cfg(not(feature = "semihosting"))]
@ -138,6 +140,8 @@ fn main() -> ! {
let mut store = flash_store::store(dp.FLASH); let mut store = flash_store::store(dp.FLASH);
let hwrev = HWRev::detect_hw_rev(&pins.hwrev);
let mut channels = 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]) {
@ -150,7 +154,7 @@ fn main() -> ! {
} }
} }
let mut fan_ctrl = FanCtrl::new(fan, &channels.hwrev); let mut fan_ctrl = FanCtrl::new(fan, hwrev);
// default net config: // default net config:
let mut ipv4_config = Ipv4Config { let mut ipv4_config = Ipv4Config {
@ -185,7 +189,7 @@ fn main() -> ! {
server.for_each(|_, session| session.set_report_pending(channel.into())); server.for_each(|_, session| session.set_report_pending(channel.into()));
} }
fan_ctrl.cycle(channels.current_abs_max_tec_i()); fan_ctrl.cycle(channels.current_abs_max_tec_i() as f32);
let instant = Instant::from_millis(i64::from(timer::now())); let instant = Instant::from_millis(i64::from(timer::now()));
cortex_m::interrupt::free(net::clear_pending); cortex_m::interrupt::free(net::clear_pending);
@ -210,7 +214,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, &mut fan_ctrl) { 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::NewIPV4(ip)) => new_ipv4_config = Some(ip),
Ok(Handler::Handled) => {}, Ok(Handler::Handled) => {},
Ok(Handler::CloseSocket) => socket.close(), Ok(Handler::CloseSocket) => socket.close(),

View File

@ -227,7 +227,8 @@ impl Pins {
}; };
// According to `SUNON DC Brushless Fan & Blower(255-E)` catalogue p.36-37 // According to `SUNON DC Brushless Fan & Blower(255-E)` catalogue p.36-37
// Model name: MF35101V1-1000U-G99 // model MF35101V1-1000U-G99 doesn't have a PWM wire, so it is advised to have
// higher frequency to have less audible noise.
let fan = Timer::new(tim8, &clocks).pwm(gpioc.pc9.into_alternate(), 25u32.khz()); let fan = Timer::new(tim8, &clocks).pwm(gpioc.pc9.into_alternate(), 25u32.khz());
(pins, leds, eeprom, eth_pins, usb, fan) (pins, leds, eeprom, eth_pins, usb, fan)