From 30350a3651d6fd8e9a0896838415b04d3a5dec87 Mon Sep 17 00:00:00 2001 From: Egor Savkin Date: Wed, 14 Dec 2022 17:25:29 +0800 Subject: [PATCH 01/16] Draft support fan pwm settings Signed-off-by: Egor Savkin --- src/channels.rs | 18 +++++++++++++++++- src/command_handler.rs | 4 ++++ src/command_parser.rs | 15 ++++++++++++++- src/main.rs | 2 +- src/pins.rs | 24 +++++++++++++++++++----- 5 files changed, 55 insertions(+), 8 deletions(-) diff --git a/src/channels.rs b/src/channels.rs index 7aa34e9..5786310 100644 --- a/src/channels.rs +++ b/src/channels.rs @@ -342,12 +342,20 @@ impl Channels { get(&self.pwm.max_i_neg0), (0, PwmPin::MaxV) => get(&self.pwm.max_v0), + (0, PwmPin::Tacho) => + get(&self.pwm.tacho), + (0, PwmPin::Fan) => + get(&self.pwm.fan), (1, PwmPin::MaxIPos) => get(&self.pwm.max_i_pos1), (1, PwmPin::MaxINeg) => get(&self.pwm.max_i_neg1), (1, PwmPin::MaxV) => get(&self.pwm.max_v1), + (1, PwmPin::Tacho) => + get(&self.pwm.tacho), + (1, PwmPin::Fan) => + get(&self.pwm.fan), _ => unreachable!(), } @@ -381,7 +389,7 @@ impl Channels { (self.read_tec_u_meas(channel) - ElectricPotential::new::(1.5)) * 4.0 } - fn set_pwm(&mut self, channel: usize, pin: PwmPin, duty: f64) -> f64 { + pub fn set_pwm(&mut self, channel: usize, pin: PwmPin, duty: f64) -> f64 { fn set>(pin: &mut P, duty: f64) -> f64 { let max = pin.get_max_duty(); let value = ((duty * (max as f64)) as u16).min(max); @@ -397,12 +405,16 @@ impl Channels { set(&mut self.pwm.max_i_neg0, duty), (0, PwmPin::MaxV) => set(&mut self.pwm.max_v0, duty), + (0, PwmPin::Fan) => + set(&mut self.pwm.fan, duty), (1, PwmPin::MaxIPos) => set(&mut self.pwm.max_i_pos1, duty), (1, PwmPin::MaxINeg) => set(&mut self.pwm.max_i_neg1, duty), (1, PwmPin::MaxV) => set(&mut self.pwm.max_v1, duty), + (1, PwmPin::Fan) => + set(&mut self.pwm.fan, duty), _ => unreachable!(), } @@ -481,6 +493,8 @@ impl Channels { max_v: (self.get_max_v(channel), ElectricPotential::new::(5.0)).into(), max_i_pos: self.get_max_i_pos(channel).into(), max_i_neg: self.get_max_i_neg(channel).into(), + tacho: self.get_pwm(0, PwmPin::Tacho), + fan: self.get_pwm(0, PwmPin::Fan), } } @@ -578,6 +592,8 @@ pub struct PwmSummary { max_v: PwmSummaryField, max_i_pos: PwmSummaryField, max_i_neg: PwmSummaryField, + tacho: f64, + fan: f64, } #[derive(Serialize)] diff --git a/src/command_handler.rs b/src/command_handler.rs index 3144b0c..4932975 100644 --- a/src/command_handler.rs +++ b/src/command_handler.rs @@ -199,6 +199,10 @@ impl Handler { let current = ElectricCurrent::new::(value); channels.set_max_i_neg(channel, current); } + PwmPin::Tacho => {} + PwmPin::Fan => { + channels.set_pwm(channel, PwmPin::Fan, value); + } } send_line(socket, b"{}"); Ok(Handler::Handled) diff --git a/src/command_parser.rs b/src/command_parser.rs index 622f819..71770de 100644 --- a/src/command_parser.rs +++ b/src/command_parser.rs @@ -127,6 +127,8 @@ pub enum PwmPin { MaxIPos, MaxINeg, MaxV, + Tacho, + Fan } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] @@ -299,7 +301,18 @@ fn pwm_setup(input: &[u8]) -> IResult<&[u8], Result<(PwmPin, f64), Error>> { ) ), result_with_pin(PwmPin::MaxV) - )) + ), + map( + preceded( + tag("fan"), + preceded( + whitespace, + float + ) + ), + result_with_pin(PwmPin::Fan) + ) + ) )(input) } diff --git a/src/main.rs b/src/main.rs index c1d7267..5be9240 100644 --- a/src/main.rs +++ b/src/main.rs @@ -119,7 +119,7 @@ fn main() -> ! { timer::setup(cp.SYST, clocks); let (pins, mut leds, mut eeprom, eth_pins, usb) = Pins::setup( - clocks, dp.TIM1, dp.TIM3, + 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, diff --git a/src/pins.rs b/src/pins.rs index f7b55a1..9f98ed7 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, @@ -114,7 +114,7 @@ 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, @@ -135,10 +135,10 @@ impl Pins { let pins_adc = Adc::adc1(adc1, true, Default::default()); let pwm = PwmPins::setup( - clocks, tim1, tim3, + clocks, tim1, tim3, tim8, gpioc.pc6, gpioc.pc7, gpioe.pe9, gpioe.pe11, - gpioe.pe13, gpioe.pe14 + gpioe.pe13, gpioe.pe14, gpioc.pc8, gpioc.pc9 ); let (dac0_spi, dac0_sync) = Self::setup_dac0( @@ -283,19 +283,24 @@ pub struct PwmPins { pub max_i_pos1: PwmChannels, pub max_i_neg0: PwmChannels, pub max_i_neg1: PwmChannels, + pub tacho: PwmChannels, + pub fan: PwmChannels, } impl PwmPins { - fn setup( + fn setup( clocks: Clocks, tim1: TIM1, tim3: TIM3, + tim8: TIM8, max_v0: PC6, max_v1: PC7, max_i_pos0: PE9, max_i_pos1: PE11, max_i_neg0: PE13, max_i_neg1: PE14, + tacho: PC8, + fan: PC9, ) -> PwmPins { let freq = 20u32.khz(); @@ -312,6 +317,14 @@ impl PwmPins { init_pwm_pin(&mut max_v0); init_pwm_pin(&mut max_v1); + let channels = ( + tacho.into_alternate(), + fan.into_alternate(), + ); + let (mut tacho, mut fan) = Timer::new(tim8, &clocks).pwm(channels, freq); + init_pwm_pin(&mut tacho); + init_pwm_pin(&mut fan); + let channels = ( max_i_pos0.into_alternate(), max_i_pos1.into_alternate(), @@ -329,6 +342,7 @@ impl PwmPins { max_v0, max_v1, max_i_pos0, max_i_pos1, max_i_neg0, max_i_neg1, + tacho, fan } } } -- 2.42.0 From 4223f7a4ad58cb118ce61743f1398255f8433775 Mon Sep 17 00:00:00 2001 From: Egor Savkin Date: Thu, 15 Dec 2022 16:47:52 +0800 Subject: [PATCH 02/16] Draft tacho support Signed-off-by: Egor Savkin --- src/channels.rs | 14 +++++--------- src/command_handler.rs | 9 ++++----- src/command_parser.rs | 1 - src/main.rs | 28 +++++++++++++++++++++++++--- src/pins.rs | 23 +++++++++-------------- 5 files changed, 43 insertions(+), 32 deletions(-) diff --git a/src/channels.rs b/src/channels.rs index 5786310..8667010 100644 --- a/src/channels.rs +++ b/src/channels.rs @@ -342,8 +342,6 @@ impl Channels { get(&self.pwm.max_i_neg0), (0, PwmPin::MaxV) => get(&self.pwm.max_v0), - (0, PwmPin::Tacho) => - get(&self.pwm.tacho), (0, PwmPin::Fan) => get(&self.pwm.fan), (1, PwmPin::MaxIPos) => @@ -352,8 +350,6 @@ impl Channels { get(&self.pwm.max_i_neg1), (1, PwmPin::MaxV) => get(&self.pwm.max_v1), - (1, PwmPin::Tacho) => - get(&self.pwm.tacho), (1, PwmPin::Fan) => get(&self.pwm.fan), _ => @@ -485,7 +481,7 @@ impl Channels { serde_json_core::to_vec(&summaries) } - fn pwm_summary(&mut self, channel: usize) -> PwmSummary { + fn pwm_summary(&mut self, channel: usize, tacho: u32) -> PwmSummary { PwmSummary { channel, center: CenterPointJson(self.channel_state(channel).center.clone()), @@ -493,15 +489,15 @@ impl Channels { max_v: (self.get_max_v(channel), ElectricPotential::new::(5.0)).into(), max_i_pos: self.get_max_i_pos(channel).into(), max_i_neg: self.get_max_i_neg(channel).into(), - tacho: self.get_pwm(0, PwmPin::Tacho), + tacho: tacho * 1000 / 1024, fan: self.get_pwm(0, PwmPin::Fan), } } - pub fn pwm_summaries_json(&mut self) -> Result { + pub fn pwm_summaries_json(&mut self, tacho: u32) -> Result { let mut summaries = Vec::<_, U2>::new(); for channel in 0..CHANNELS { - let _ = summaries.push(self.pwm_summary(channel)); + let _ = summaries.push(self.pwm_summary(channel, tacho)); } serde_json_core::to_vec(&summaries) } @@ -592,7 +588,7 @@ pub struct PwmSummary { max_v: PwmSummaryField, max_i_pos: PwmSummaryField, max_i_neg: PwmSummaryField, - tacho: f64, + tacho: u32, fan: f64, } diff --git a/src/command_handler.rs b/src/command_handler.rs index 4932975..70857e3 100644 --- a/src/command_handler.rs +++ b/src/command_handler.rs @@ -121,8 +121,8 @@ impl Handler { Ok(Handler::Handled) } - fn show_pwm(socket: &mut TcpSocket, channels: &mut Channels) -> Result { - match channels.pwm_summaries_json() { + fn show_pwm(socket: &mut TcpSocket, channels: &mut Channels, tacho: u32) -> Result { + match channels.pwm_summaries_json(tacho) { Ok(buf) => { send_line(socket, &buf); } @@ -199,7 +199,6 @@ impl Handler { let current = ElectricCurrent::new::(value); channels.set_max_i_neg(channel, current); } - PwmPin::Tacho => {} PwmPin::Fan => { channels.set_pwm(channel, PwmPin::Fan, value); } @@ -345,14 +344,14 @@ 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 { + 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: u32) -> 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::Pwm) => Handler::show_pwm(socket, channels, tacho_value), 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), diff --git a/src/command_parser.rs b/src/command_parser.rs index 71770de..996d853 100644 --- a/src/command_parser.rs +++ b/src/command_parser.rs @@ -127,7 +127,6 @@ pub enum PwmPin { MaxIPos, MaxINeg, MaxV, - Tacho, Fan } diff --git a/src/main.rs b/src/main.rs index 5be9240..c9ee36a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -25,6 +25,8 @@ use smoltcp::{ socket::TcpSocket, wire::EthernetAddress, }; +use stm32f4xx_hal::gpio::{Edge, ExtiPin}; +use stm32f4xx_hal::syscfg::SysCfgExt; mod init_log; use init_log::init_log; @@ -102,7 +104,7 @@ fn main() -> ! { cp.SCB.enable_icache(); cp.SCB.enable_dcache(&mut cp.CPUID); - let dp = Peripherals::take().unwrap(); + let mut dp = Peripherals::take().unwrap(); let clocks = dp.RCC.constrain() .cfgr .use_hse(HSE) @@ -118,7 +120,7 @@ fn main() -> ! { timer::setup(cp.SYST, clocks); - let (pins, mut leds, mut eeprom, eth_pins, usb) = Pins::setup( + let (pins, mut leds, mut eeprom, eth_pins, usb, mut tacho) = 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, @@ -170,11 +172,18 @@ fn main() -> ! { let hwaddr = EthernetAddress(eui48); info!("EEPROM MAC address: {}", hwaddr); + 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| { Server::::run(iface, |server| { leds.r1.off(); let mut should_reset = false; + let (mut tacho_cnt, mut tacho_value) = (0u32, 0u32); + let mut prev_epoch = i64::from(timer::now()) >> 10; + loop { let mut new_ipv4_config = None; let instant = Instant::from_millis(i64::from(timer::now())); @@ -184,6 +193,19 @@ fn main() -> ! { } let instant = Instant::from_millis(i64::from(timer::now())); + let tacho_input = tacho.check_interrupt(); + tacho.clear_interrupt_pending_bit(); + if tacho_input { + tacho_cnt += 1; + } + + let epoch = instant.millis >> 10; + if epoch > prev_epoch { + tacho_value = tacho_cnt; + tacho_cnt = 0; + prev_epoch = epoch; + } + cortex_m::interrupt::free(net::clear_pending); server.poll(instant) .unwrap_or_else(|e| { @@ -206,7 +228,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, tacho_value) { 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 9f98ed7..9abadab 100644 --- a/src/pins.rs +++ b/src/pins.rs @@ -120,7 +120,7 @@ impl Pins { 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, PC8>) { let gpioa = gpioa.split(); let gpiob = gpiob.split(); let gpioc = gpioc.split(); @@ -138,9 +138,11 @@ impl Pins { clocks, tim1, tim3, tim8, gpioc.pc6, gpioc.pc7, gpioe.pe9, gpioe.pe11, - gpioe.pe13, gpioe.pe14, gpioc.pc8, gpioc.pc9 + gpioe.pe13, gpioe.pe14, gpioc.pc9 ); + // let tacho = gpioc.pc8.into_floating_input().into_input_pin()(); + let (dac0_spi, dac0_sync) = Self::setup_dac0( clocks, spi4, gpioe.pe2, gpioe.pe4, gpioe.pe6 @@ -215,7 +217,7 @@ impl Pins { hclk: clocks.hclk(), }; - (pins, leds, eeprom, eth_pins, usb) + (pins, leds, eeprom, eth_pins, usb, gpioc.pc8) } /// Configure the GPIO pins for SPI operation, and initialize SPI @@ -283,12 +285,11 @@ pub struct PwmPins { pub max_i_pos1: PwmChannels, pub max_i_neg0: PwmChannels, pub max_i_neg1: PwmChannels, - pub tacho: PwmChannels, pub fan: PwmChannels, } impl PwmPins { - fn setup( + fn setup( clocks: Clocks, tim1: TIM1, tim3: TIM3, @@ -299,8 +300,7 @@ impl PwmPins { max_i_pos1: PE11, max_i_neg0: PE13, max_i_neg1: PE14, - tacho: PC8, - fan: PC9, + fan: PC9, ) -> PwmPins { let freq = 20u32.khz(); @@ -317,12 +317,7 @@ impl PwmPins { init_pwm_pin(&mut max_v0); init_pwm_pin(&mut max_v1); - let channels = ( - tacho.into_alternate(), - fan.into_alternate(), - ); - let (mut tacho, mut fan) = Timer::new(tim8, &clocks).pwm(channels, freq); - init_pwm_pin(&mut tacho); + let mut fan = Timer::new(tim8, &clocks).pwm(fan.into_alternate(), freq); init_pwm_pin(&mut fan); let channels = ( @@ -342,7 +337,7 @@ impl PwmPins { max_v0, max_v1, max_i_pos0, max_i_pos1, max_i_neg0, max_i_neg1, - tacho, fan + fan } } } -- 2.42.0 From d117c784d9f11454add3246b7d279211576883a0 Mon Sep 17 00:00:00 2001 From: Egor Savkin Date: Mon, 19 Dec 2022 17:04:01 +0800 Subject: [PATCH 03/16] Draft hw rev support and fan command Signed-off-by: Egor Savkin --- README.md | 1 + src/channels.rs | 64 +++++++++++++++++++++++++++++------------- src/command_handler.rs | 31 ++++++++++++++------ src/command_parser.rs | 30 +++++++++++++------- src/fan_ctrl.rs | 0 src/main.rs | 49 +++++++++++++++++++++----------- src/pins.rs | 12 ++++++-- 7 files changed, 131 insertions(+), 56 deletions(-) create mode 100644 src/fan_ctrl.rs diff --git a/README.md b/README.md index 10d2fc7..ce4d504 100644 --- a/README.md +++ b/README.md @@ -124,6 +124,7 @@ formatted as line-delimited JSON. | `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 ` | Set fan power with values from 0 to 100, where 0 is auto mode (TODO) | ## USB diff --git a/src/channels.rs b/src/channels.rs index 8667010..813493e 100644 --- a/src/channels.rs +++ b/src/channels.rs @@ -19,12 +19,19 @@ use crate::{ pins, steinhart_hart, }; +use crate::pins::HWRevPins; pub const CHANNELS: usize = 2; 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 const DAC_OUT_V_MAX: f64 = 3.0; +#[derive(Serialize, Copy, Clone)] +pub struct HWRev { + pub major: u8, + pub minor: u8 +} + // TODO: -pub pub struct Channels { channel0: Channel, @@ -33,6 +40,7 @@ pub struct Channels { /// stm32f4 integrated adc pins_adc: pins::PinsAdc, pub pwm: pins::PwmPins, + pub hw_rev: HWRev } impl Channels { @@ -54,7 +62,7 @@ impl Channels { let channel1 = Channel::new(pins.channel1, adc_calibration1); let pins_adc = pins.pins_adc; 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) }; for channel in 0..CHANNELS { channels.channel_state(channel).vref = channels.read_vref(channel); channels.calibrate_dac_value(channel); @@ -63,6 +71,17 @@ impl 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 channel_state>(&mut self, channel: I) -> &mut ChannelState { match channel.into() { 0 => &mut self.channel0.state, @@ -336,22 +355,20 @@ impl Channels { match (channel, pin) { (_, PwmPin::ISet) => panic!("i_set is no pwm pin"), + (_, PwmPin::Fan) => + get(&self.pwm.fan), (0, PwmPin::MaxIPos) => get(&self.pwm.max_i_pos0), (0, PwmPin::MaxINeg) => get(&self.pwm.max_i_neg0), (0, PwmPin::MaxV) => get(&self.pwm.max_v0), - (0, PwmPin::Fan) => - get(&self.pwm.fan), (1, PwmPin::MaxIPos) => get(&self.pwm.max_i_pos1), (1, PwmPin::MaxINeg) => get(&self.pwm.max_i_neg1), (1, PwmPin::MaxV) => get(&self.pwm.max_v1), - (1, PwmPin::Fan) => - get(&self.pwm.fan), _ => unreachable!(), } @@ -395,22 +412,20 @@ impl Channels { match (channel, pin) { (_, PwmPin::ISet) => panic!("i_set is no pwm pin"), + (_, PwmPin::Fan) => + set(&mut self.pwm.fan, duty), (0, PwmPin::MaxIPos) => set(&mut self.pwm.max_i_pos0, duty), (0, PwmPin::MaxINeg) => set(&mut self.pwm.max_i_neg0, duty), (0, PwmPin::MaxV) => set(&mut self.pwm.max_v0, duty), - (0, PwmPin::Fan) => - set(&mut self.pwm.fan, duty), (1, PwmPin::MaxIPos) => set(&mut self.pwm.max_i_pos1, duty), (1, PwmPin::MaxINeg) => set(&mut self.pwm.max_i_neg1, duty), (1, PwmPin::MaxV) => set(&mut self.pwm.max_v1, duty), - (1, PwmPin::Fan) => - set(&mut self.pwm.fan, duty), _ => unreachable!(), } @@ -437,7 +452,16 @@ impl Channels { (duty * max, max) } - fn report(&mut self, channel: usize) -> Report { + pub fn set_fan_pwm(&mut self, fan_pwm: u32) -> f64 { + let duty = fan_pwm as f64 / 100.0; + self.set_pwm(0, PwmPin::Fan, duty) + } + + pub fn get_fan_pwm(&mut self) -> u32 { + (self.get_pwm(0, PwmPin::Fan) * 100.0) as u32 + } + + fn report(&mut self, channel: usize, tacho: Option) -> Report { let vref = self.channel_state(channel).vref; let i_set = self.get_i(channel); let i_tec = self.read_itec(channel); @@ -462,13 +486,15 @@ impl Channels { tec_i, tec_u_meas: self.get_tec_v(channel), pid_output, + tacho, + hwrev: self.hw_rev } } - pub fn reports_json(&mut self) -> Result { + pub fn reports_json(&mut self, tacho: Option) -> Result { let mut reports = Vec::<_, U2>::new(); for channel in 0..CHANNELS { - let _ = reports.push(self.report(channel)); + let _ = reports.push(self.report(channel, tacho)); } serde_json_core::to_vec(&reports) } @@ -481,7 +507,7 @@ impl Channels { serde_json_core::to_vec(&summaries) } - fn pwm_summary(&mut self, channel: usize, tacho: u32) -> PwmSummary { + fn pwm_summary(&mut self, channel: usize) -> PwmSummary { PwmSummary { channel, center: CenterPointJson(self.channel_state(channel).center.clone()), @@ -489,15 +515,14 @@ impl Channels { max_v: (self.get_max_v(channel), ElectricPotential::new::(5.0)).into(), max_i_pos: self.get_max_i_pos(channel).into(), max_i_neg: self.get_max_i_neg(channel).into(), - tacho: tacho * 1000 / 1024, - fan: self.get_pwm(0, PwmPin::Fan), + fan: self.get_fan_pwm(), } } - pub fn pwm_summaries_json(&mut self, tacho: u32) -> Result { + pub fn pwm_summaries_json(&mut self) -> Result { let mut summaries = Vec::<_, U2>::new(); for channel in 0..CHANNELS { - let _ = summaries.push(self.pwm_summary(channel, tacho)); + let _ = summaries.push(self.pwm_summary(channel)); } serde_json_core::to_vec(&summaries) } @@ -549,6 +574,8 @@ pub struct Report { tec_i: ElectricCurrent, tec_u_meas: ElectricPotential, pid_output: ElectricCurrent, + tacho: Option, + hwrev: HWRev, } pub struct CenterPointJson(CenterPoint); @@ -588,8 +615,7 @@ pub struct PwmSummary { max_v: PwmSummaryField, max_i_pos: PwmSummaryField, max_i_neg: PwmSummaryField, - tacho: u32, - fan: f64, + fan: u32, } #[derive(Serialize)] diff --git a/src/command_handler.rs b/src/command_handler.rs index 70857e3..ea8d107 100644 --- a/src/command_handler.rs +++ b/src/command_handler.rs @@ -93,8 +93,8 @@ impl Handler { Ok(Handler::Handled) } - fn show_report(socket: &mut TcpSocket, channels: &mut Channels) -> Result { - match channels.reports_json() { + fn show_report(socket: &mut TcpSocket, channels: &mut Channels, tacho: Option) -> Result { + match channels.reports_json(tacho) { Ok(buf) => { send_line(socket, &buf[..]); } @@ -121,8 +121,8 @@ impl Handler { Ok(Handler::Handled) } - fn show_pwm(socket: &mut TcpSocket, channels: &mut Channels, tacho: u32) -> Result { - match channels.pwm_summaries_json(tacho) { + fn show_pwm(socket: &mut TcpSocket, channels: &mut Channels) -> Result { + match channels.pwm_summaries_json() { Ok(buf) => { send_line(socket, &buf); } @@ -200,7 +200,7 @@ impl Handler { channels.set_max_i_neg(channel, current); } PwmPin::Fan => { - channels.set_pwm(channel, PwmPin::Fan, value); + channels.set_fan_pwm(value as u32); } } send_line(socket, b"{}"); @@ -344,14 +344,26 @@ 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, tacho_value: u32) -> Result { + fn fan (channels: &mut Channels, fan_pwm: Option) -> Result { + match fan_pwm { + Some(val) => { + channels.set_fan_pwm(val); + Ok(Handler::Handled) + }, + None => { + 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, tacho_value: Option) -> 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::Input) => Handler::show_report(socket, channels, tacho_value), Command::Show(ShowCommand::Pid) => Handler::show_pid(socket, channels), - Command::Show(ShowCommand::Pwm) => Handler::show_pwm(socket, channels, tacho_value), + 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), @@ -366,7 +378,8 @@ 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::Fan {fan_pwm} => Handler::fan(channels, fan_pwm) } } } \ No newline at end of file diff --git a/src/command_parser.rs b/src/command_parser.rs index 996d853..82069a2 100644 --- a/src/command_parser.rs +++ b/src/command_parser.rs @@ -179,6 +179,9 @@ pub enum Command { rate: Option, }, Dfu, + Fan { + fan_pwm: Option + } } fn end(input: &[u8]) -> IResult<&[u8], ()> { @@ -301,16 +304,6 @@ fn pwm_setup(input: &[u8]) -> IResult<&[u8], Result<(PwmPin, f64), Error>> { ), result_with_pin(PwmPin::MaxV) ), - map( - preceded( - tag("fan"), - preceded( - whitespace, - float - ) - ), - result_with_pin(PwmPin::Fan) - ) ) )(input) } @@ -532,6 +525,22 @@ fn ipv4(input: &[u8]) -> IResult<&[u8], Result> { ))(input) } +fn fan(input: &[u8]) -> IResult<&[u8], Result> { + let (input, _) = tag("fan")(input)?; + let (input, fan_pwm) = alt(( + |input| { + let (input, _) = whitespace(input)?; + let (input, value) = unsigned(input)?; + let (input, _) = end(input)?; + Ok((input, Some(value.unwrap_or(0)))) + }, + value(None, end) + ))(input)?; + + let result = Ok(Command::Fan { fan_pwm }); + Ok((input, result)) +} + fn command(input: &[u8]) -> IResult<&[u8], Result> { alt((value(Ok(Command::Quit), tag("quit")), load, @@ -545,6 +554,7 @@ fn command(input: &[u8]) -> IResult<&[u8], Result> { steinhart_hart, postfilter, value(Ok(Command::Dfu), tag("dfu")), + fan, ))(input) } diff --git a/src/fan_ctrl.rs b/src/fan_ctrl.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/main.rs b/src/main.rs index c9ee36a..9cb1322 100644 --- a/src/main.rs +++ b/src/main.rs @@ -152,6 +152,8 @@ fn main() -> ! { } } + let fan_available = channels.hw_rev.major == 2 && channels.hw_rev.minor == 2; + // default net config: let mut ipv4_config = Ipv4Config { address: [192, 168, 1, 26], @@ -172,17 +174,26 @@ fn main() -> ! { let hwaddr = EthernetAddress(eui48); info!("EEPROM MAC address: {}", hwaddr); - tacho.make_interrupt_source(&mut dp.SYSCFG.constrain()); - tacho.trigger_on_edge(&mut dp.EXTI, Edge::Rising); - tacho.enable_interrupt(&mut dp.EXTI); + 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| { Server::::run(iface, |server| { leds.r1.off(); let mut should_reset = false; - let (mut tacho_cnt, mut tacho_value) = (0u32, 0u32); - let mut prev_epoch = i64::from(timer::now()) >> 10; + let (mut tacho_cnt, mut tacho_value) = (0u32, None); + let mut prev_epoch: i64 = 0; loop { let mut new_ipv4_config = None; @@ -193,17 +204,23 @@ fn main() -> ! { } let instant = Instant::from_millis(i64::from(timer::now())); - let tacho_input = tacho.check_interrupt(); - tacho.clear_interrupt_pending_bit(); - if tacho_input { - tacho_cnt += 1; - } - let epoch = instant.millis >> 10; - if epoch > prev_epoch { - tacho_value = tacho_cnt; - tacho_cnt = 0; - prev_epoch = epoch; + if fan_available { + let mut tacho_input = false; + cortex_m::interrupt::free(|_cs| { + tacho_input = tacho.check_interrupt(); + tacho.clear_interrupt_pending_bit(); + }); + if tacho_input { + tacho_cnt += 1; + } + + let epoch = instant.secs(); + if epoch > prev_epoch { + tacho_value = Some(tacho_cnt); + tacho_cnt = 0; + prev_epoch = epoch; + } } cortex_m::interrupt::free(net::clear_pending); @@ -245,7 +262,7 @@ fn main() -> ! { } } else if socket.can_send() { if let Some(channel) = session.is_report_pending() { - match channels.reports_json() { + match channels.reports_json(tacho_value) { Ok(buf) => { send_line(&mut socket, &buf[..]); session.mark_report_sent(channel); diff --git a/src/pins.rs b/src/pins.rs index 9abadab..dd565a1 100644 --- a/src/pins.rs +++ b/src/pins.rs @@ -101,6 +101,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, @@ -108,6 +115,7 @@ pub struct Pins { pub pwm: PwmPins, pub channel0: ChannelPinSet, pub channel1: ChannelPinSet, + pub hwrev: HWRevPins } impl Pins { @@ -141,8 +149,6 @@ impl Pins { gpioe.pe13, gpioe.pe14, gpioc.pc9 ); - // let tacho = gpioc.pc8.into_floating_input().into_input_pin()(); - let (dac0_spi, dac0_sync) = Self::setup_dac0( clocks, spi4, gpioe.pe2, gpioe.pe4, gpioe.pe6 @@ -189,6 +195,8 @@ impl Pins { pwm, channel0, channel1, + hwrev: HWRevPins {hwrev0: gpiod.pd0, hwrev1: gpiod.pd1, + hwrev2: gpiod.pd2, hwrev3: gpiod.pd3} }; let leds = Leds::new(gpiod.pd9, gpiod.pd10.into_push_pull_output(), gpiod.pd11.into_push_pull_output()); -- 2.42.0 From 583d06a78b23c615dc8046d25643b45feff12aba Mon Sep 17 00:00:00 2001 From: Egor Savkin Date: Tue, 20 Dec 2022 12:56:37 +0800 Subject: [PATCH 04/16] Make fan cmd show the stats Signed-off-by: Egor Savkin --- README.md | 3 ++- flake.nix | 2 +- src/channels.rs | 36 +++++++++++++++++++++++++++++------- src/command_handler.rs | 22 ++++++++++++++++------ src/fan_ctrl.rs | 0 src/main.rs | 11 ++++------- 6 files changed, 52 insertions(+), 22 deletions(-) delete mode 100644 src/fan_ctrl.rs diff --git a/README.md b/README.md index ce4d504..955c638 100644 --- a/README.md +++ b/README.md @@ -95,7 +95,7 @@ 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 | @@ -124,6 +124,7 @@ formatted as line-delimited JSON. | `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 0 to 100, where 0 is auto mode (TODO) | diff --git a/flake.nix b/flake.nix index c28f806..02b7c70 100644 --- a/flake.nix +++ b/flake.nix @@ -69,7 +69,7 @@ buildInputs = with pkgs; [ rustPlatform.rust.rustc rustPlatform.rust.cargo - openocd dfu-util + openocd dfu-util gcc-arm-embedded-10 ] ++ (with python3Packages; [ numpy matplotlib ]); diff --git a/src/channels.rs b/src/channels.rs index 813493e..b40656b 100644 --- a/src/channels.rs +++ b/src/channels.rs @@ -1,3 +1,4 @@ +use core::cmp::max_by; use heapless::{consts::{U2, U1024}, Vec}; use serde::{Serialize, Serializer}; use smoltcp::time::Instant; @@ -82,6 +83,10 @@ impl Channels { } } + pub fn fan_available(&self) -> bool { + self.hw_rev.major == 2 && self.hw_rev.minor == 2 + } + pub fn channel_state>(&mut self, channel: I) -> &mut ChannelState { match channel.into() { 0 => &mut self.channel0.state, @@ -461,7 +466,7 @@ impl Channels { (self.get_pwm(0, PwmPin::Fan) * 100.0) as u32 } - fn report(&mut self, channel: usize, tacho: Option) -> Report { + fn report(&mut self, channel: usize) -> Report { let vref = self.channel_state(channel).vref; let i_set = self.get_i(channel); let i_tec = self.read_itec(channel); @@ -486,15 +491,14 @@ impl Channels { tec_i, tec_u_meas: self.get_tec_v(channel), pid_output, - tacho, hwrev: self.hw_rev } } - pub fn reports_json(&mut self, tacho: Option) -> Result { + pub fn reports_json(&mut self) -> Result { let mut reports = Vec::<_, U2>::new(); for channel in 0..CHANNELS { - let _ = reports.push(self.report(channel, tacho)); + let _ = reports.push(self.report(channel)); } serde_json_core::to_vec(&reports) } @@ -515,7 +519,6 @@ impl Channels { max_v: (self.get_max_v(channel), ElectricPotential::new::(5.0)).into(), max_i_pos: self.get_max_i_pos(channel).into(), max_i_neg: self.get_max_i_neg(channel).into(), - fan: self.get_fan_pwm(), } } @@ -553,6 +556,20 @@ impl Channels { } serde_json_core::to_vec(&summaries) } + + pub fn fan_summary(&mut self, tacho: Option) -> Result { + if self.fan_available() { + let summary = FanSummary { + fan_pwm: self.get_fan_pwm(), + tacho: tacho.unwrap_or(u32::MAX), + abs_max_tec_i: max_by(self.get_tec_i(0).abs().value, self.get_tec_i(1).abs().value, |a, b| a.partial_cmp(b).unwrap()) + }; + serde_json_core::to_vec(&summary) + } else { + let summary: Option<()> = None; + serde_json_core::to_vec(&summary) + } + } } type JsonBuffer = Vec; @@ -574,7 +591,6 @@ pub struct Report { tec_i: ElectricCurrent, tec_u_meas: ElectricPotential, pid_output: ElectricCurrent, - tacho: Option, hwrev: HWRev, } @@ -615,7 +631,6 @@ pub struct PwmSummary { max_v: PwmSummaryField, max_i_pos: PwmSummaryField, max_i_neg: PwmSummaryField, - fan: u32, } #[derive(Serialize)] @@ -629,3 +644,10 @@ pub struct SteinhartHartSummary { channel: usize, params: steinhart_hart::Parameters, } + +#[derive(Serialize)] +pub struct FanSummary { + fan_pwm: u32, + tacho: u32, + abs_max_tec_i: f64, +} diff --git a/src/command_handler.rs b/src/command_handler.rs index ea8d107..0fcf252 100644 --- a/src/command_handler.rs +++ b/src/command_handler.rs @@ -93,8 +93,8 @@ impl Handler { Ok(Handler::Handled) } - fn show_report(socket: &mut TcpSocket, channels: &mut Channels, tacho: Option) -> Result { - match channels.reports_json(tacho) { + fn show_report(socket: &mut TcpSocket, channels: &mut Channels) -> Result { + match channels.reports_json() { Ok(buf) => { send_line(socket, &buf[..]); } @@ -344,14 +344,24 @@ impl Handler { Ok(Handler::Reset) } - fn fan (channels: &mut Channels, fan_pwm: Option) -> Result { + fn fan (socket: &mut TcpSocket, channels: &mut Channels, fan_pwm: Option, tacho_value: Option) -> Result { match fan_pwm { Some(val) => { channels.set_fan_pwm(val); Ok(Handler::Handled) }, None => { - Err(Error::ReportError) + match channels.fan_summary(tacho_value) { + Ok(buf) => { + send_line(socket, &buf); + } + Err(e) => { + error!("unable to serialize fan summary: {:?}", e); + let _ = writeln!(socket, "{{\"error\":\"{:?}\"}}", e); + return Err(Error::ReportError); + } + }; + Ok(Handler::Handled) } } } @@ -361,7 +371,7 @@ impl Handler { 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, tacho_value), + 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), @@ -379,7 +389,7 @@ impl Handler { Command::Ipv4(config) => Handler::set_ipv4(socket, store, config), Command::Reset => Handler::reset(channels), Command::Dfu => Handler::dfu(channels), - Command::Fan {fan_pwm} => Handler::fan(channels, fan_pwm) + Command::Fan {fan_pwm} => Handler::fan(socket, channels, fan_pwm, tacho_value) } } } \ No newline at end of file diff --git a/src/fan_ctrl.rs b/src/fan_ctrl.rs deleted file mode 100644 index e69de29..0000000 diff --git a/src/main.rs b/src/main.rs index 9cb1322..77f6d8b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -152,7 +152,7 @@ fn main() -> ! { } } - let fan_available = channels.hw_rev.major == 2 && channels.hw_rev.minor == 2; + let fan_available = channels.fan_available(); // default net config: let mut ipv4_config = Ipv4Config { @@ -206,12 +206,9 @@ fn main() -> ! { let instant = Instant::from_millis(i64::from(timer::now())); if fan_available { - let mut tacho_input = false; - cortex_m::interrupt::free(|_cs| { - tacho_input = tacho.check_interrupt(); - tacho.clear_interrupt_pending_bit(); - }); + let tacho_input = tacho.check_interrupt(); if tacho_input { + tacho.clear_interrupt_pending_bit(); tacho_cnt += 1; } @@ -262,7 +259,7 @@ fn main() -> ! { } } else if socket.can_send() { if let Some(channel) = session.is_report_pending() { - match channels.reports_json(tacho_value) { + match channels.reports_json() { Ok(buf) => { send_line(&mut socket, &buf[..]); session.mark_report_sent(channel); -- 2.42.0 From 66143d2373498453dc4ffe868a6c7a3dc3445852 Mon Sep 17 00:00:00 2001 From: Egor Savkin Date: Tue, 20 Dec 2022 14:46:18 +0800 Subject: [PATCH 05/16] Implemented auto fan control Signed-off-by: Egor Savkin --- README.md | 12 +++++++++++- src/channels.rs | 39 +++++++++++++++++++++++++++++++++------ src/command_handler.rs | 8 +++++--- src/command_parser.rs | 3 +-- src/main.rs | 6 ++++-- 5 files changed, 54 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 955c638..6c9c81e 100644 --- a/README.md +++ b/README.md @@ -125,7 +125,7 @@ formatted as line-delimited JSON. | `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 0 to 100, where 0 is auto mode (TODO) | +| `fan ` | Set fan power with values from 0 to 100, where 0 is auto mode | ## USB @@ -271,3 +271,13 @@ 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 two 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 +linearly correlates with the actual fan speed. +2. `fan ` - 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. +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. diff --git a/src/channels.rs b/src/channels.rs index b40656b..40927be 100644 --- a/src/channels.rs +++ b/src/channels.rs @@ -26,6 +26,9 @@ pub const CHANNELS: usize = 2; 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 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 { @@ -41,7 +44,8 @@ pub struct Channels { /// stm32f4 integrated adc pins_adc: pins::PinsAdc, pub pwm: pins::PwmPins, - pub hw_rev: HWRev + hw_rev: HWRev, + fan_auto: bool } impl Channels { @@ -63,7 +67,8 @@ impl Channels { let channel1 = Channel::new(pins.channel1, adc_calibration1); let pins_adc = pins.pins_adc; let pwm = pins.pwm; - let mut channels = Channels { channel0, channel1, adc, pins_adc, pwm, hw_rev: Self::detect_hw_rev(&pins.hwrev) }; + let mut channels = Channels { channel0, channel1, adc, pins_adc, pwm, + hw_rev: Self::detect_hw_rev(&pins.hwrev), fan_auto: true }; for channel in 0..CHANNELS { channels.channel_state(channel).vref = channels.read_vref(channel); channels.calibrate_dac_value(channel); @@ -407,7 +412,7 @@ impl Channels { (self.read_tec_u_meas(channel) - ElectricPotential::new::(1.5)) * 4.0 } - pub fn set_pwm(&mut self, channel: usize, pin: PwmPin, duty: f64) -> f64 { + fn set_pwm(&mut self, channel: usize, pin: PwmPin, duty: f64) -> f64 { fn set>(pin: &mut P, duty: f64) -> f64 { let max = pin.get_max_duty(); let value = ((duty * (max as f64)) as u16).min(max); @@ -458,12 +463,16 @@ impl Channels { } pub fn set_fan_pwm(&mut self, fan_pwm: u32) -> f64 { - let duty = fan_pwm as f64 / 100.0; + 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) * 100.0) as 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 { @@ -557,12 +566,19 @@ impl Channels { serde_json_core::to_vec(&summaries) } + 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)) + } + pub fn fan_summary(&mut self, tacho: Option) -> Result { if self.fan_available() { let summary = FanSummary { fan_pwm: self.get_fan_pwm(), tacho: tacho.unwrap_or(u32::MAX), - abs_max_tec_i: max_by(self.get_tec_i(0).abs().value, self.get_tec_i(1).abs().value, |a, b| a.partial_cmp(b).unwrap()) + abs_max_tec_i: self.current_abs_max_tec_i(), + auto_mode: self.fan_auto }; serde_json_core::to_vec(&summary) } else { @@ -570,6 +586,16 @@ impl Channels { 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; @@ -650,4 +676,5 @@ pub struct FanSummary { fan_pwm: u32, tacho: u32, abs_max_tec_i: f64, + auto_mode: bool, } diff --git a/src/command_handler.rs b/src/command_handler.rs index 0fcf252..79a2b5a 100644 --- a/src/command_handler.rs +++ b/src/command_handler.rs @@ -347,13 +347,14 @@ impl Handler { fn fan (socket: &mut TcpSocket, channels: &mut Channels, fan_pwm: Option, tacho_value: Option) -> Result { match fan_pwm { Some(val) => { + channels.set_fan_auto_mode(val == 0); channels.set_fan_pwm(val); - Ok(Handler::Handled) }, None => { match channels.fan_summary(tacho_value) { Ok(buf) => { send_line(socket, &buf); + return Ok(Handler::Handled); } Err(e) => { error!("unable to serialize fan summary: {:?}", e); @@ -361,9 +362,10 @@ impl Handler { return Err(Error::ReportError); } }; - Ok(Handler::Handled) } - } + }; + 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, tacho_value: Option) -> Result { diff --git a/src/command_parser.rs b/src/command_parser.rs index 82069a2..b87b40a 100644 --- a/src/command_parser.rs +++ b/src/command_parser.rs @@ -303,8 +303,7 @@ fn pwm_setup(input: &[u8]) -> IResult<&[u8], Result<(PwmPin, f64), Error>> { ) ), result_with_pin(PwmPin::MaxV) - ), - ) + )) )(input) } diff --git a/src/main.rs b/src/main.rs index 77f6d8b..7314c43 100644 --- a/src/main.rs +++ b/src/main.rs @@ -19,14 +19,14 @@ use stm32f4xx_hal::{ stm32::{CorePeripherals, Peripherals, SCB}, time::{U32Ext, MegaHertz}, watchdog::IndependentWatchdog, + gpio::{Edge, ExtiPin}, + syscfg::SysCfgExt }; use smoltcp::{ time::Instant, socket::TcpSocket, wire::EthernetAddress, }; -use stm32f4xx_hal::gpio::{Edge, ExtiPin}; -use stm32f4xx_hal::syscfg::SysCfgExt; mod init_log; use init_log::init_log; @@ -220,6 +220,8 @@ fn main() -> ! { } } + channels.fan_ctrl(); + cortex_m::interrupt::free(net::clear_pending); server.poll(instant) .unwrap_or_else(|e| { -- 2.42.0 From 58650d37f13e676bd93b01b95ae32050a1a314c1 Mon Sep 17 00:00:00 2001 From: Egor Savkin Date: Thu, 22 Dec 2022 17:28:08 +0800 Subject: [PATCH 06/16] Refactor and coefficients implemented Move all the fan logic to the separate file. Add controls for controlling curve. Signed-off-by: Egor Savkin --- README.md | 72 +++++++------- src/channels.rs | 87 ++-------------- src/command_handler.rs | 32 ++++-- src/command_parser.rs | 49 ++++++--- src/fan_ctrl.rs | 218 +++++++++++++++++++++++++++++++++++++++++ src/main.rs | 59 +++-------- src/pins.rs | 33 +++---- 7 files changed, 351 insertions(+), 199 deletions(-) create mode 100644 src/fan_ctrl.rs diff --git a/README.md b/README.md index 6c9c81e..b7187ba 100644 --- a/README.md +++ b/README.md @@ -94,38 +94,40 @@ 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 | -| `fan` | Show current fan settings and sensors' measurements | -| `fan ` | Set fan power with values from 0 to 100, where 0 is auto mode | +| 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 0 to 100, where 0 is auto mode | +| `fcurve ` | Set fan controller coefficients (see *Fan control* section) | +| `fan-restore` | Set fan controller coefficients to defaults (see *Fan control* section) | ## USB @@ -274,10 +276,14 @@ The thermostat implements a PID control loop for each of the TEC channels, more ## Fan control -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 linearly correlates with the actual fan speed. 2. `fan ` - 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. 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. +3. `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. +4. `fan-restore` - restore fan settings to defaults: `auto = true, a = 1.0, b = 0.0, c = 0.0`. diff --git a/src/channels.rs b/src/channels.rs index 40927be..c594c34 100644 --- a/src/channels.rs +++ b/src/channels.rs @@ -19,22 +19,13 @@ use crate::{ command_parser::{CenterPoint, PwmPin}, pins, steinhart_hart, + fan_ctrl::HWRev, }; -use crate::pins::HWRevPins; pub const CHANNELS: usize = 2; 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 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 pub struct Channels { @@ -44,8 +35,7 @@ pub struct Channels { /// stm32f4 integrated adc pins_adc: pins::PinsAdc, pub pwm: pins::PwmPins, - hw_rev: HWRev, - fan_auto: bool + pub hwrev: HWRev, } impl Channels { @@ -68,7 +58,7 @@ impl Channels { let pins_adc = pins.pins_adc; let pwm = pins.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 { channels.channel_state(channel).vref = channels.read_vref(channel); channels.calibrate_dac_value(channel); @@ -77,21 +67,6 @@ impl 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>(&mut self, channel: I) -> &mut ChannelState { match channel.into() { 0 => &mut self.channel0.state, @@ -365,8 +340,6 @@ impl Channels { match (channel, pin) { (_, PwmPin::ISet) => panic!("i_set is no pwm pin"), - (_, PwmPin::Fan) => - get(&self.pwm.fan), (0, PwmPin::MaxIPos) => get(&self.pwm.max_i_pos0), (0, PwmPin::MaxINeg) => @@ -422,8 +395,6 @@ impl Channels { match (channel, pin) { (_, PwmPin::ISet) => panic!("i_set is no pwm pin"), - (_, PwmPin::Fan) => - set(&mut self.pwm.fan, duty), (0, PwmPin::MaxIPos) => set(&mut self.pwm.max_i_pos0, duty), (0, PwmPin::MaxINeg) => @@ -462,19 +433,6 @@ impl Channels { (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 { let vref = self.channel_state(channel).vref; let i_set = self.get_i(channel); @@ -500,7 +458,7 @@ impl Channels { tec_i, tec_u_meas: self.get_tec_v(channel), pid_output, - hwrev: self.hw_rev + hwrev: self.hwrev } } @@ -566,39 +524,14 @@ impl Channels { 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::(), self.get_tec_i(1).abs().get::(), |a, b| a.partial_cmp(b).unwrap_or(core::cmp::Ordering::Equal)) } - - pub fn fan_summary(&mut self, tacho: Option) -> Result { - 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; +pub type JsonBuffer = Vec; #[derive(Serialize)] pub struct Report { @@ -670,11 +603,3 @@ pub struct SteinhartHartSummary { channel: usize, params: steinhart_hart::Parameters, } - -#[derive(Serialize)] -pub struct FanSummary { - fan_pwm: u32, - tacho: u32, - abs_max_tec_i: f64, - auto_mode: bool, -} diff --git a/src/command_handler.rs b/src/command_handler.rs index 79a2b5a..edf05fd 100644 --- a/src/command_handler.rs +++ b/src/command_handler.rs @@ -22,7 +22,8 @@ use super::{ config::ChannelConfig, dfu, flash_store::FlashStore, - session::Session + session::Session, + FanCtrl, }; use uom::{ @@ -199,9 +200,6 @@ impl Handler { let current = ElectricCurrent::new::(value); channels.set_max_i_neg(channel, current); } - PwmPin::Fan => { - channels.set_fan_pwm(value as u32); - } } send_line(socket, b"{}"); Ok(Handler::Handled) @@ -344,14 +342,14 @@ impl Handler { Ok(Handler::Reset) } - fn fan (socket: &mut TcpSocket, channels: &mut Channels, fan_pwm: Option, tacho_value: Option) -> Result { + fn fan (socket: &mut TcpSocket, fan_pwm: Option, fan_ctrl: &mut FanCtrl) -> Result { match fan_pwm { Some(val) => { - channels.set_fan_auto_mode(val == 0); - channels.set_fan_pwm(val); + fan_ctrl.set_auto_mode(val == 0); + fan_ctrl.set_pwm(val); }, None => { - match channels.fan_summary(tacho_value) { + match fan_ctrl.summary() { Ok(buf) => { send_line(socket, &buf); return Ok(Handler::Handled); @@ -368,7 +366,19 @@ impl Handler { 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) -> Result { + fn fan_coeff (socket: &mut TcpSocket, fan_ctrl: &mut FanCtrl, k_a: f64, k_b: f64, k_c: f64) -> Result { + 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 { + 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 { match command { Command::Quit => Ok(Handler::CloseSocket), Command::Reporting(_reporting) => Handler::reporting(socket), @@ -391,7 +401,9 @@ impl Handler { Command::Ipv4(config) => Handler::set_ipv4(socket, store, config), Command::Reset => Handler::reset(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), } } } \ No newline at end of file diff --git a/src/command_parser.rs b/src/command_parser.rs index b87b40a..df2372b 100644 --- a/src/command_parser.rs +++ b/src/command_parser.rs @@ -1,16 +1,7 @@ use core::fmt; use core::num::ParseIntError; use core::str::{from_utf8, Utf8Error}; -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, -}; +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}; use num_traits::{Num, ParseFloatError}; use serde::{Serialize, Deserialize}; @@ -127,7 +118,6 @@ pub enum PwmPin { MaxIPos, MaxINeg, MaxV, - Fan } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] @@ -181,7 +171,13 @@ pub enum Command { Dfu, Fan { fan_pwm: Option - } + }, + FanCoeff { + k_a: f64, + k_b: f64, + k_c: f64, + }, + FanDefaults, } fn end(input: &[u8]) -> IResult<&[u8], ()> { @@ -540,6 +536,33 @@ fn fan(input: &[u8]) -> IResult<&[u8], Result> { Ok((input, result)) } +fn fan_coeff(input: &[u8]) -> IResult<&[u8], Result> { + 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> { alt((value(Ok(Command::Quit), tag("quit")), load, @@ -553,7 +576,9 @@ fn command(input: &[u8]) -> IResult<&[u8], Result> { steinhart_hart, postfilter, value(Ok(Command::Dfu), tag("dfu")), + value(Ok(Command::FanDefaults), tag("fan-restore")), fan, + fan_coeff, ))(input) } diff --git a/src/fan_ctrl.rs b/src/fan_ctrl.rs new file mode 100644 index 0000000..7f9a2f1 --- /dev/null +++ b/src/fan_ctrl.rs @@ -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; +pub type TachoPin = PC8>; + +const MAX_TEC_I: f64 = 3.0; +// as stated in the schematics +const MAX_FAN_PWM: f64 = 100.0; +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, + 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); + } + + 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(); + } + + pub fn summary(&mut self) -> Result { + 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, +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 7314c43..798c3b4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,7 +10,7 @@ use panic_abort as _; use panic_semihosting as _; use log::{error, info, warn}; - +use core::cell::RefCell; use cortex_m::asm::wfi; use cortex_m_rt::entry; use stm32f4xx_hal::{ @@ -19,7 +19,6 @@ use stm32f4xx_hal::{ stm32::{CorePeripherals, Peripherals, SCB}, time::{U32Ext, MegaHertz}, watchdog::IndependentWatchdog, - gpio::{Edge, ExtiPin}, syscfg::SysCfgExt }; use smoltcp::{ @@ -56,6 +55,8 @@ mod flash_store; mod dfu; mod command_handler; use command_handler::Handler; +mod fan_ctrl; +use fan_ctrl::FanCtrl; const HSE: MegaHertz = MegaHertz(8); #[cfg(not(feature = "semihosting"))] @@ -120,7 +121,7 @@ fn main() -> ! { 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, dp.GPIOA, dp.GPIOB, dp.GPIOC, dp.GPIOD, dp.GPIOE, dp.GPIOF, dp.GPIOG, dp.I2C1, @@ -137,22 +138,22 @@ fn main() -> ! { usb::State::setup(usb); - let mut store = flash_store::store(dp.FLASH); - + let mut channels = RefCell::new(Channels::new(pins)); - let mut channels = Channels::new(pins); + let mut store = flash_store::store(dp.FLASH); for c in 0..CHANNELS { match store.read_value::(CHANNEL_CONFIG_KEY[c]) { Ok(Some(config)) => - config.apply(&mut channels, c), + config.apply(channels.get_mut(), c), Ok(None) => error!("flash config not found for channel {}", c), Err(e) => error!("unable to load config {} from flash: {:?}", c, e), } } - - let fan_available = channels.fan_available(); + // considered safe since `channels` is being mutated in a single thread, + // 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: let mut ipv4_config = Ipv4Config { @@ -174,54 +175,22 @@ fn main() -> ! { let hwaddr = EthernetAddress(eui48); 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| { Server::::run(iface, |server| { leds.r1.off(); let mut should_reset = false; - let (mut tacho_cnt, mut tacho_value) = (0u32, None); - let mut prev_epoch: i64 = 0; - loop { let mut new_ipv4_config = None; 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 { server.for_each(|_, session| session.set_report_pending(channel.into())); } + fan_ctrl.cycle(); 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); server.poll(instant) .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. Ok(SessionInput::Nothing) => {} 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::Handled) => {}, Ok(Handler::CloseSocket) => socket.close(), @@ -261,7 +230,7 @@ fn main() -> ! { } } else if socket.can_send() { if let Some(channel) = session.is_report_pending() { - match channels.reports_json() { + match channels.get_mut().reports_json() { Ok(buf) => { send_line(&mut socket, &buf[..]); session.mark_report_sent(channel); diff --git a/src/pins.rs b/src/pins.rs index dd565a1..1b53a51 100644 --- a/src/pins.rs +++ b/src/pins.rs @@ -26,15 +26,18 @@ use stm32f4xx_hal::{ TIM1, TIM3, TIM8 }, timer::Timer, - time::U32Ext, + time::{U32Ext, KiloHertz}, }; use eeprom24x::{self, Eeprom24x}; use stm32_eth::EthPins; use crate::{ channel::{Channel0, Channel1}, leds::Leds, + fan_ctrl::{TachoPin, FanPin} }; +const PWM_FREQ: KiloHertz = KiloHertz(20u32); + pub type Eeprom = Eeprom24x< I2c>, @@ -128,7 +131,7 @@ impl Pins { 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, PC8>) { + ) -> (Self, Leds, Eeprom, EthernetPins, USB, FanPin, TachoPin) { let gpioa = gpioa.split(); let gpiob = gpiob.split(); let gpioc = gpioc.split(); @@ -143,10 +146,10 @@ impl Pins { let pins_adc = Adc::adc1(adc1, true, Default::default()); let pwm = PwmPins::setup( - clocks, tim1, tim3, tim8, + clocks, tim1, tim3, gpioc.pc6, gpioc.pc7, gpioe.pe9, gpioe.pe11, - gpioe.pe13, gpioe.pe14, gpioc.pc9 + gpioe.pe13, gpioe.pe14 ); let (dac0_spi, dac0_sync) = Self::setup_dac0( @@ -225,7 +228,9 @@ impl Pins { 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 @@ -293,24 +298,20 @@ pub struct PwmPins { pub max_i_pos1: PwmChannels, pub max_i_neg0: PwmChannels, pub max_i_neg1: PwmChannels, - pub fan: PwmChannels, } impl PwmPins { - fn setup( + fn setup( clocks: Clocks, tim1: TIM1, tim3: TIM3, - tim8: TIM8, max_v0: PC6, max_v1: PC7, max_i_pos0: PE9, max_i_pos1: PE11, max_i_neg0: PE13, max_i_neg1: PE14, - fan: PC9, ) -> PwmPins { - let freq = 20u32.khz(); fn init_pwm_pin>(pin: &mut P) { pin.set_duty(0); @@ -320,14 +321,11 @@ impl PwmPins { max_v0.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) = Timer::new(tim3, &clocks).pwm(channels, 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, PWM_FREQ); init_pwm_pin(&mut max_v0); 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 = ( max_i_pos0.into_alternate(), max_i_pos1.into_alternate(), @@ -335,7 +333,7 @@ impl PwmPins { max_i_neg1.into_alternate(), ); 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_neg0); init_pwm_pin(&mut max_i_pos1); @@ -344,8 +342,7 @@ impl PwmPins { PwmPins { max_v0, max_v1, max_i_pos0, max_i_pos1, - max_i_neg0, max_i_neg1, - fan + max_i_neg0, max_i_neg1 } } } -- 2.42.0 From ea2eb51b27055ae3483024385f32ee104055c5a3 Mon Sep 17 00:00:00 2001 From: Egor Savkin Date: Fri, 23 Dec 2022 13:13:33 +0800 Subject: [PATCH 07/16] Add fan warnings Signed-off-by: Egor Savkin --- src/fan_ctrl.rs | 73 +++++++++++++++++++++++++++++++++++++++++++++---- src/main.rs | 10 +++++-- 2 files changed, 75 insertions(+), 8 deletions(-) diff --git a/src/fan_ctrl.rs b/src/fan_ctrl.rs index 7f9a2f1..3c6d9cc 100644 --- a/src/fan_ctrl.rs +++ b/src/fan_ctrl.rs @@ -26,9 +26,10 @@ const MAX_TEC_I: f64 = 3.0; const MAX_FAN_PWM: f64 = 100.0; const MIN_FAN_PWM: f64 = 1.0; const TACHO_MEASURE_MS: i64 = 2500; +const TACHO_LOW_THRESHOLD: u32 = 100; const DEFAULT_K_A: f64 = 1.0; const DEFAULT_K_B: f64 = 0.0; -const DEFAULT_K_C: f64 = 0.0; +const DEFAULT_K_C: f64 = 0.04; #[derive(Serialize, Copy, Clone)] pub struct HWRev { @@ -36,11 +37,20 @@ pub struct HWRev { pub minor: u8, } +#[derive(Serialize, Clone, Copy, PartialEq)] +pub enum FanStatus { + OK, + NotAvailable, + Stalled, + LowSignal, +} + struct TachoCtrl { tacho: TachoPin, tacho_cnt: u32, tacho_value: Option, prev_epoch: i64, + past_record: u64, } pub struct FanCtrl<'a> { @@ -52,6 +62,7 @@ pub struct FanCtrl<'a> { k_b: f64, k_c: f64, channels: &'a mut Channels, + last_status: FanStatus } impl<'a> FanCtrl<'a> { @@ -74,14 +85,22 @@ impl<'a> FanCtrl<'a> { k_b: DEFAULT_K_B, k_c: DEFAULT_K_C, channels, + last_status: FanStatus::OK, } } - pub fn cycle(&mut self) { + pub fn cycle(&mut self) -> Result<(), FanStatus>{ if self.available { self.tacho.cycle(); } self.adjust_speed(); + let diagnose = self.diagnose(); + if diagnose != self.last_status { + self.last_status = diagnose; + Err(diagnose) + } else { + Ok(()) + } } pub fn summary(&mut self) -> Result { @@ -91,6 +110,7 @@ impl<'a> FanCtrl<'a> { tacho: self.tacho.get(), abs_max_tec_i: self.channels.current_abs_max_tec_i(), auto_mode: self.fan_auto, + status: self.diagnose(), k_a: self.k_a, k_b: self.k_b, k_c: self.k_c, @@ -139,6 +159,13 @@ impl<'a> FanCtrl<'a> { value as f64 / (max as f64) } + fn diagnose(&mut self) -> FanStatus { + if !self.available { + return FanStatus::NotAvailable; + } + self.tacho.diagnose() + } + fn get_pwm(&self) -> u32 { let duty = self.fan.get_duty(); let max = self.fan.get_max_duty(); @@ -147,16 +174,17 @@ impl<'a> FanCtrl<'a> { } impl TachoCtrl { - pub fn new(tacho: TachoPin) -> Self { + fn new(tacho: TachoPin) -> Self { TachoCtrl { tacho, tacho_cnt: 0, tacho_value: None, prev_epoch: 0, + past_record: 0, } } - pub fn init(&mut self, exti: &mut EXTI, syscfg: &mut SysCfg) { + fn init(&mut self, exti: &mut EXTI, syscfg: &mut SysCfg) { // These lines do not cause NVIC to run the ISR, // since the interrupt should be unmasked in the cortex_m::peripheral::NVIC. // Also using interrupt-related workaround is the best @@ -169,7 +197,17 @@ impl TachoCtrl { self.tacho.enable_interrupt(exti); } - pub fn cycle(&mut self) { + #[inline] + fn add_record(&mut self, value: u32) { + self.past_record = self.past_record << 2; + if value >= TACHO_LOW_THRESHOLD { + self.past_record += 0b11; + } else if value > 0 && self.tacho_cnt < TACHO_LOW_THRESHOLD { + self.past_record += 0b10; + } + } + + fn cycle(&mut self) { let tacho_input = self.tacho.check_interrupt(); if tacho_input { self.tacho.clear_interrupt_pending_bit(); @@ -179,14 +217,25 @@ impl TachoCtrl { 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.add_record(self.tacho_cnt); self.tacho_cnt = 0; self.prev_epoch = instant.millis; } } - pub fn get(&self) -> u32 { + fn get(&self) -> u32 { self.tacho_value.unwrap_or(u32::MAX) } + + fn diagnose(&mut self) -> FanStatus { + if self.past_record & 0b11 == 0b11 { + FanStatus::OK + } else if self.past_record & 0xAAAAAAAAAAAAAAAA > 0 { + FanStatus::LowSignal + } else { + FanStatus::Stalled + } + } } impl HWRev { @@ -212,7 +261,19 @@ pub struct FanSummary { tacho: u32, abs_max_tec_i: f64, auto_mode: bool, + status: FanStatus, k_a: f64, k_b: f64, k_c: f64, +} + +impl FanStatus { + pub fn fmt_u8(&self) -> &'static [u8] { + match *self { + FanStatus::OK => "Fan is OK".as_bytes(), + FanStatus::NotAvailable => "Fan is not available".as_bytes(), + FanStatus::Stalled => "Fan is stalled".as_bytes(), + FanStatus::LowSignal => "Fan is low signal".as_bytes(), + } + } } \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 798c3b4..1005315 100644 --- a/src/main.rs +++ b/src/main.rs @@ -187,10 +187,10 @@ fn main() -> ! { if let Some(channel) = updated_channel { server.for_each(|_, session| session.set_report_pending(channel.into())); } - fan_ctrl.cycle(); + + let fan_status = fan_ctrl.cycle(); let instant = Instant::from_millis(i64::from(timer::now())); - cortex_m::interrupt::free(net::clear_pending); server.poll(instant) .unwrap_or_else(|e| { @@ -241,6 +241,12 @@ fn main() -> ! { } } } + match fan_status { + Ok(_) => {} + Err(status) => { + send_line(&mut socket, status.fmt_u8()); + } + }; } }); } else { -- 2.42.0 From 630635486e7c4f9cbbd755a11a2548063a5b72ea Mon Sep 17 00:00:00 2001 From: Egor Savkin Date: Fri, 23 Dec 2022 13:23:18 +0800 Subject: [PATCH 08/16] Polish minor issues Signed-off-by: Egor Savkin --- README.md | 2 +- flake.nix | 2 +- src/command_parser.rs | 12 +++++++++++- src/main.rs | 4 ++-- 4 files changed, 15 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index b7187ba..e66c5b5 100644 --- a/README.md +++ b/README.md @@ -286,4 +286,4 @@ Please note that power doesn't correlate with the actual speed linearly. 3. `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. -4. `fan-restore` - restore fan settings to defaults: `auto = true, a = 1.0, b = 0.0, c = 0.0`. +4. `fan-restore` - restore fan settings to defaults: `auto = true, a = 1.0, b = 0.0, c = 0.04`. diff --git a/flake.nix b/flake.nix index 02b7c70..c28f806 100644 --- a/flake.nix +++ b/flake.nix @@ -69,7 +69,7 @@ buildInputs = with pkgs; [ rustPlatform.rust.rustc rustPlatform.rust.cargo - openocd dfu-util gcc-arm-embedded-10 + openocd dfu-util ] ++ (with python3Packages; [ numpy matplotlib ]); diff --git a/src/command_parser.rs b/src/command_parser.rs index df2372b..497b741 100644 --- a/src/command_parser.rs +++ b/src/command_parser.rs @@ -1,7 +1,17 @@ use core::fmt; use core::num::ParseIntError; use core::str::{from_utf8, Utf8Error}; -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}; +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, +}; use num_traits::{Num, ParseFloatError}; use serde::{Serialize, Deserialize}; diff --git a/src/main.rs b/src/main.rs index 1005315..dd1263e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -138,9 +138,9 @@ fn main() -> ! { usb::State::setup(usb); - let mut channels = RefCell::new(Channels::new(pins)); - let mut store = flash_store::store(dp.FLASH); + + let mut channels = RefCell::new(Channels::new(pins)); for c in 0..CHANNELS { match store.read_value::(CHANNEL_CONFIG_KEY[c]) { Ok(Some(config)) => -- 2.42.0 From 21fc244eac8a08a958c607478e26285ef9ba1def Mon Sep 17 00:00:00 2001 From: Egor Savkin Date: Wed, 4 Jan 2023 16:01:34 +0800 Subject: [PATCH 09/16] Fan status via quadratic regression * statuses: detect slow fan via quadratic regression and halts (hard stops) with constant threshold * remove unsafe channels' copy and make FanCtrl own the channels * scale fan values so that they would fit 0.05-1.0 PWM at all times * rename fan-restore to fcurve-restore * style, names and docs adjustments Signed-off-by: Egor Savkin --- README.md | 70 +++++++++---------- src/command_handler.rs | 44 ++++++------ src/command_parser.rs | 14 ++-- src/fan_ctrl.rs | 149 +++++++++++++++++++++++++---------------- src/main.rs | 16 ++--- 5 files changed, 164 insertions(+), 129 deletions(-) diff --git a/README.md b/README.md index e66c5b5..5275de7 100644 --- a/README.md +++ b/README.md @@ -94,40 +94,40 @@ The scope of this setting is per TCP session. Send commands as simple text string terminated by `\n`. Responses are 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 | -| `fan` | Show current fan settings and sensors' measurements | -| `fan ` | Set fan power with values from 0 to 100, where 0 is auto mode | -| `fcurve ` | Set fan controller coefficients (see *Fan control* section) | -| `fan-restore` | Set fan controller coefficients to defaults (see *Fan control* section) | +| 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 0 to 100, where 0 is auto mode | +| `fcurve ` | Set fan controller curve coefficients (see *Fan control* section) | +| `fcurve-restore` | Set fan controller curve coefficients to defaults (see *Fan control* section) | ## USB @@ -286,4 +286,4 @@ Please note that power doesn't correlate with the actual speed linearly. 3. `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. -4. `fan-restore` - restore fan settings to defaults: `auto = true, a = 1.0, b = 0.0, c = 0.04`. +4. `fcurve-restore` - restore fan settings to defaults: `auto = true, a = 1.0, b = 0.0, c = 0.00`. diff --git a/src/command_handler.rs b/src/command_handler.rs index edf05fd..6049b99 100644 --- a/src/command_handler.rs +++ b/src/command_handler.rs @@ -342,7 +342,7 @@ impl Handler { Ok(Handler::Reset) } - fn fan (socket: &mut TcpSocket, fan_pwm: Option, fan_ctrl: &mut FanCtrl) -> Result { + fn fan(socket: &mut TcpSocket, fan_pwm: Option, fan_ctrl: &mut FanCtrl) -> Result { match fan_pwm { Some(val) => { fan_ctrl.set_auto_mode(val == 0); @@ -366,43 +366,43 @@ impl Handler { Ok(Handler::Handled) } - fn fan_coeff (socket: &mut TcpSocket, fan_ctrl: &mut FanCtrl, k_a: f64, k_b: f64, k_c: f64) -> Result { - fan_ctrl.set_coefficients(k_a, k_b, k_c); + fn fan_curve(socket: &mut TcpSocket, fan_ctrl: &mut FanCtrl, k_a: f64, k_b: f64, k_c: f64) -> 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 { + fn fan_defaults(socket: &mut TcpSocket, fan_ctrl: &mut FanCtrl) -> Result { 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 { + pub fn handle_command(command: Command, socket: &mut TcpSocket, session: &Session, leds: &mut Leds, store: &mut FlashStore, ipv4_config: &mut Ipv4Config, fan_ctrl: &mut FanCtrl) -> Result { 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, &mut fan_ctrl.channels), + Command::Show(ShowCommand::Pid) => Handler::show_pid(socket, &mut fan_ctrl.channels), + Command::Show(ShowCommand::Pwm) => Handler::show_pwm(socket, &mut fan_ctrl.channels), + Command::Show(ShowCommand::SteinhartHart) => Handler::show_steinhart_hart(socket, &mut fan_ctrl.channels), + Command::Show(ShowCommand::PostFilter) => Handler::show_post_filter(socket, &mut fan_ctrl.channels), Command::Show(ShowCommand::Ipv4) => Handler::show_ipv4(socket, ipv4_config), - Command::PwmPid { channel } => Handler::engage_pid(socket, channels, leds, channel), - Command::Pwm { channel, pin, value } => Handler::set_pwm(socket, channels, leds, channel, pin, value), - Command::CenterPoint { channel, center } => Handler::set_center_point(socket, channels, channel, center), - Command::Pid { channel, parameter, value } => Handler::set_pid(socket, channels, channel, parameter, value), - Command::SteinhartHart { channel, parameter, value } => Handler::set_steinhart_hart(socket, channels, channel, parameter, value), - Command::PostFilter { channel, rate: None } => Handler::reset_post_filter(socket, channels, channel), - Command::PostFilter { channel, rate: Some(rate) } => Handler::set_post_filter(socket, channels, channel, rate), - Command::Load { channel } => Handler::load_channel(socket, channels, store, channel), - Command::Save { channel } => Handler::save_channel(socket, channels, channel, store), + Command::PwmPid { channel } => Handler::engage_pid(socket, &mut fan_ctrl.channels, leds, channel), + Command::Pwm { channel, pin, value } => Handler::set_pwm(socket, &mut fan_ctrl.channels, leds, channel, pin, value), + Command::CenterPoint { channel, center } => Handler::set_center_point(socket, &mut fan_ctrl.channels, channel, center), + Command::Pid { channel, parameter, value } => Handler::set_pid(socket, &mut fan_ctrl.channels, channel, parameter, value), + Command::SteinhartHart { channel, parameter, value } => Handler::set_steinhart_hart(socket, &mut fan_ctrl.channels, channel, parameter, value), + Command::PostFilter { channel, rate: None } => Handler::reset_post_filter(socket, &mut fan_ctrl.channels, channel), + Command::PostFilter { channel, rate: Some(rate) } => Handler::set_post_filter(socket, &mut fan_ctrl.channels, channel, rate), + Command::Load { channel } => Handler::load_channel(socket, &mut fan_ctrl.channels, store, channel), + Command::Save { channel } => Handler::save_channel(socket, &mut fan_ctrl.channels, channel, store), Command::Ipv4(config) => Handler::set_ipv4(socket, store, config), - Command::Reset => Handler::reset(channels), - Command::Dfu => Handler::dfu(channels), + Command::Reset => Handler::reset(&mut fan_ctrl.channels), + Command::Dfu => Handler::dfu(&mut fan_ctrl.channels), Command::Fan {fan_pwm} => Handler::fan(socket, fan_pwm, fan_ctrl), - Command::FanCoeff { k_a, k_b, k_c } => Handler::fan_coeff(socket, fan_ctrl, k_a, k_b, k_c), + Command::FanCurve { k_a, k_b, k_c } => Handler::fan_curve(socket, fan_ctrl, k_a, k_b, k_c), Command::FanDefaults => Handler::fan_defaults(socket, fan_ctrl), } } diff --git a/src/command_parser.rs b/src/command_parser.rs index 497b741..66c2777 100644 --- a/src/command_parser.rs +++ b/src/command_parser.rs @@ -182,7 +182,7 @@ pub enum Command { Fan { fan_pwm: Option }, - FanCoeff { + FanCurve { k_a: f64, k_b: f64, k_c: f64, @@ -546,9 +546,9 @@ fn fan(input: &[u8]) -> IResult<&[u8], Result> { Ok((input, result)) } -fn fan_coeff(input: &[u8]) -> IResult<&[u8], Result> { +fn fan_curve(input: &[u8]) -> IResult<&[u8], Result> { let (input, _) = tag("fcurve")(input)?; - let (input, coeffs) = alt(( + let (input, curve) = alt(( |input| { let (input, _) = whitespace(input)?; let (input, k_a) = float(input)?; @@ -566,8 +566,8 @@ fn fan_coeff(input: &[u8]) -> IResult<&[u8], Result> { 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 }), + let result = match curve { + Some(curve) => Ok(Command::FanCurve { k_a: curve.0, k_b: curve.1, k_c: curve.2 }), None => Err(Error::ParseFloat) }; Ok((input, result)) @@ -586,9 +586,9 @@ fn command(input: &[u8]) -> IResult<&[u8], Result> { steinhart_hart, postfilter, value(Ok(Command::Dfu), tag("dfu")), - value(Ok(Command::FanDefaults), tag("fan-restore")), + value(Ok(Command::FanDefaults), tag("fcurve-restore")), fan, - fan_coeff, + fan_curve, ))(input) } diff --git a/src/fan_ctrl.rs b/src/fan_ctrl.rs index 3c6d9cc..4999ff9 100644 --- a/src/fan_ctrl.rs +++ b/src/fan_ctrl.rs @@ -1,4 +1,3 @@ -use core::{cmp::max_by}; use serde::Serialize; use stm32f4xx_hal::{ pwm::{self, PwmChannels}, @@ -21,15 +20,33 @@ use crate::{ pub type FanPin = PwmChannels; pub type TachoPin = PC8>; -const MAX_TEC_I: f64 = 3.0; // as stated in the schematics -const MAX_FAN_PWM: f64 = 100.0; -const MIN_FAN_PWM: f64 = 1.0; +const MAX_TEC_I: f64 = 3.0; + +const MAX_USER_FAN_PWM: f64 = 100.0; +const MIN_USER_FAN_PWM: f64 = 1.0; +const MAX_FAN_PWM: f64 = 1.0; +// below this value, motor pulse signal is too weak to be registered by tachometer +const MIN_FAN_PWM: f64 = 0.05; + const TACHO_MEASURE_MS: i64 = 2500; -const TACHO_LOW_THRESHOLD: u32 = 100; +// by default up to 2 cycles are skipped on changes in PWM output, +// and the halt threshold will help detect the failure during these skipped cycles +const TACHO_HALT_THRESHOLD: u32 = 250; +const TACHO_SKIP_CYCLES: u8 = 2; + const DEFAULT_K_A: f64 = 1.0; const DEFAULT_K_B: f64 = 0.0; -const DEFAULT_K_C: f64 = 0.04; +const DEFAULT_K_C: f64 = 0.0; + +// This regression is from 6% to 25% lower than values registered in the experiments. +// Actual values would be better estimated by logarithmic regression, but that would require more +// runtime computation, and wouldn't give significant correlation difference +// (0.996 for log and 0.966 for quadratic regression). +const TACHO_REGRESSION_A: f64 = -0.04135128436; +const TACHO_REGRESSION_B: f64 = 6.23015531; +const TACHO_REGRESSION_C: f64 = 403.6833577; + #[derive(Serialize, Copy, Clone)] pub struct HWRev { @@ -41,8 +58,8 @@ pub struct HWRev { pub enum FanStatus { OK, NotAvailable, - Stalled, - LowSignal, + TooSlow, + Halted } struct TachoCtrl { @@ -50,10 +67,9 @@ struct TachoCtrl { tacho_cnt: u32, tacho_value: Option, prev_epoch: i64, - past_record: u64, } -pub struct FanCtrl<'a> { +pub struct FanCtrl { fan: FanPin, tacho: TachoCtrl, fan_auto: bool, @@ -61,12 +77,13 @@ pub struct FanCtrl<'a> { k_a: f64, k_b: f64, k_c: f64, - channels: &'a mut Channels, - last_status: FanStatus + pub channels: Channels, + last_status: FanStatus, + skip_cycles: u8, } -impl<'a> FanCtrl<'a> { - pub fn new(mut fan: FanPin, tacho: TachoPin, channels: &'a mut Channels, exti: &mut EXTI, syscfg: &mut SysCfg) -> Self { +impl FanCtrl { + pub fn new(mut fan: FanPin, tacho: TachoPin, channels: Channels, exti: &mut EXTI, syscfg: &mut SysCfg) -> Self { let available = channels.hwrev.fan_available(); let mut tacho_ctrl = TachoCtrl::new(tacho); @@ -85,17 +102,20 @@ impl<'a> FanCtrl<'a> { k_b: DEFAULT_K_B, k_c: DEFAULT_K_C, channels, - last_status: FanStatus::OK, + last_status: if available { FanStatus::OK } else { FanStatus::NotAvailable }, + skip_cycles: 0 } } - pub fn cycle(&mut self) -> Result<(), FanStatus>{ + pub fn cycle(&mut self) -> Result<(), FanStatus> { if self.available { - self.tacho.cycle(); + if self.tacho.cycle() { + self.skip_cycles >>= 1; + } } self.adjust_speed(); let diagnose = self.diagnose(); - if diagnose != self.last_status { + if (self.skip_cycles == 0 || diagnose == FanStatus::Halted) && diagnose != self.last_status { self.last_status = diagnose; Err(diagnose) } else { @@ -126,9 +146,7 @@ impl<'a> FanCtrl<'a> { 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; + 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); } } @@ -139,7 +157,7 @@ impl<'a> FanCtrl<'a> { } #[inline] - pub fn set_coefficients(&mut self, k_a: f64, k_b: f64, k_c: f64) { + pub fn set_curve(&mut self, k_a: f64, k_b: f64, k_c: f64) { self.k_a = k_a; self.k_b = k_b; self.k_c = k_c; @@ -148,28 +166,50 @@ impl<'a> FanCtrl<'a> { #[inline] pub fn restore_defaults(&mut self) { self.set_auto_mode(true); - self.set_coefficients(DEFAULT_K_A, DEFAULT_K_B, DEFAULT_K_C); + self.set_curve(DEFAULT_K_A, DEFAULT_K_B, DEFAULT_K_C); } pub fn set_pwm(&mut self, fan_pwm: u32) -> f64 { - let duty = fan_pwm as f64 / MAX_FAN_PWM; + let fan_pwm = fan_pwm.min(MAX_USER_FAN_PWM as u32).max(MIN_USER_FAN_PWM as u32); + self.skip_cycles = if (self.tacho.get() as f64) <= Self::threshold_for_pwm(fan_pwm as f64) { + TACHO_SKIP_CYCLES + } else { self.skip_cycles }; + let duty = Self::scale_number(fan_pwm as f64, MIN_FAN_PWM, MAX_FAN_PWM, MIN_USER_FAN_PWM, MAX_USER_FAN_PWM); let max = self.fan.get_max_duty(); let value = ((duty * (max as f64)) as u16).min(max); self.fan.set_duty(value); value as f64 / (max as f64) } + #[inline] + fn threshold_for_pwm(fan_pwm: f64) -> f64 { + (TACHO_REGRESSION_A * fan_pwm + TACHO_REGRESSION_B) * fan_pwm + TACHO_REGRESSION_C + } + + #[inline] + fn scale_number(unscaled: f64, to_min: f64, to_max: f64, from_min: f64, from_max: f64) -> f64 { + (to_max - to_min) * (unscaled - from_min) / (from_max - from_min) + to_min + } + fn diagnose(&mut self) -> FanStatus { if !self.available { return FanStatus::NotAvailable; } - self.tacho.diagnose() + let threshold = Self::threshold_for_pwm(self.get_pwm() as f64) as u32; + let tacho = self.tacho.get(); + if tacho >= threshold { + FanStatus::OK + } else if tacho >= TACHO_HALT_THRESHOLD { + FanStatus::TooSlow + } else { + FanStatus::Halted + } } fn get_pwm(&self) -> u32 { let duty = self.fan.get_duty(); let max = self.fan.get_max_duty(); - ((duty as f64 / (max as f64)) * MAX_FAN_PWM) as u32 + (Self::scale_number(duty as f64 / (max as f64), MIN_USER_FAN_PWM, MAX_USER_FAN_PWM, MIN_FAN_PWM, MAX_FAN_PWM) + 0.5) as u32 } } @@ -180,34 +220,24 @@ impl TachoCtrl { tacho_cnt: 0, tacho_value: None, prev_epoch: 0, - past_record: 0, } } 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. + // since the interrupt is masked 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. + // since tying the IC's PC8 with the PWM's PC9 to the same TIM8 is not supported. + // The possible solution would be to update the library to >=v0.14.*, + // and use its Timer's counter functionality. self.tacho.make_interrupt_source(syscfg); self.tacho.trigger_on_edge(exti, Edge::Rising); self.tacho.enable_interrupt(exti); } - #[inline] - fn add_record(&mut self, value: u32) { - self.past_record = self.past_record << 2; - if value >= TACHO_LOW_THRESHOLD { - self.past_record += 0b11; - } else if value > 0 && self.tacho_cnt < TACHO_LOW_THRESHOLD { - self.past_record += 0b10; - } - } - - fn cycle(&mut self) { + // returns whether the epoch elapsed + fn cycle(&mut self) -> bool { let tacho_input = self.tacho.check_interrupt(); if tacho_input { self.tacho.clear_interrupt_pending_bit(); @@ -217,25 +247,17 @@ impl TachoCtrl { 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.add_record(self.tacho_cnt); self.tacho_cnt = 0; self.prev_epoch = instant.millis; + true + } else { + false } } fn get(&self) -> u32 { self.tacho_value.unwrap_or(u32::MAX) } - - fn diagnose(&mut self) -> FanStatus { - if self.past_record & 0b11 == 0b11 { - FanStatus::OK - } else if self.past_record & 0xAAAAAAAAAAAAAAAA > 0 { - FanStatus::LowSignal - } else { - FanStatus::Stalled - } - } } impl HWRev { @@ -272,8 +294,23 @@ impl FanStatus { match *self { FanStatus::OK => "Fan is OK".as_bytes(), FanStatus::NotAvailable => "Fan is not available".as_bytes(), - FanStatus::Stalled => "Fan is stalled".as_bytes(), - FanStatus::LowSignal => "Fan is low signal".as_bytes(), + FanStatus::TooSlow => "Fan is too slow".as_bytes(), + FanStatus::Halted => "Fan is halted".as_bytes(), } } -} \ No newline at end of file +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_scaler() { + for x in 1..100 { + assert_eq!((FanCtrl::scale_number( + FanCtrl::scale_number(x as f64, MIN_FAN_PWM, MAX_FAN_PWM, MIN_USER_FAN_PWM, MAX_USER_FAN_PWM), + MIN_USER_FAN_PWM, MAX_USER_FAN_PWM, MIN_FAN_PWM, MAX_FAN_PWM) + 0.5) as i32, + x); + } + } +} diff --git a/src/main.rs b/src/main.rs index dd1263e..ef629ef 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 core::cell::RefCell; use cortex_m::asm::wfi; use cortex_m_rt::entry; use stm32f4xx_hal::{ @@ -140,20 +139,19 @@ fn main() -> ! { let mut store = flash_store::store(dp.FLASH); - let mut channels = RefCell::new(Channels::new(pins)); + let mut channels = Channels::new(pins); for c in 0..CHANNELS { match store.read_value::(CHANNEL_CONFIG_KEY[c]) { Ok(Some(config)) => - config.apply(channels.get_mut(), c), + config.apply(&mut channels, c), Ok(None) => error!("flash config not found for channel {}", c), Err(e) => error!("unable to load config {} from flash: {:?}", c, e), } } - // considered safe since `channels` is being mutated in a single thread, - // while mutex would be excessive - let mut fan_ctrl = FanCtrl::new(fan, tacho, unsafe{ &mut *channels.as_ptr() }, &mut dp.EXTI, &mut dp.SYSCFG.constrain()); + + let mut fan_ctrl = FanCtrl::new(fan, tacho, channels, &mut dp.EXTI, &mut dp.SYSCFG.constrain()); // default net config: let mut ipv4_config = Ipv4Config { @@ -183,7 +181,7 @@ fn main() -> ! { loop { let mut new_ipv4_config = None; let instant = Instant::from_millis(i64::from(timer::now())); - let updated_channel = channels.get_mut().poll_adc(instant); + let updated_channel = fan_ctrl.channels.poll_adc(instant); if let Some(channel) = updated_channel { server.for_each(|_, session| session.set_report_pending(channel.into())); } @@ -213,7 +211,7 @@ fn main() -> ! { // Do nothing and feed more data to the line reader in the next loop cycle. Ok(SessionInput::Nothing) => {} Ok(SessionInput::Command(command)) => { - match Handler::handle_command(command, &mut socket, channels.get_mut(), session, &mut leds, &mut store, &mut ipv4_config, &mut fan_ctrl) { + match Handler::handle_command(command, &mut socket, session, &mut leds, &mut store, &mut ipv4_config, &mut fan_ctrl) { Ok(Handler::NewIPV4(ip)) => new_ipv4_config = Some(ip), Ok(Handler::Handled) => {}, Ok(Handler::CloseSocket) => socket.close(), @@ -230,7 +228,7 @@ fn main() -> ! { } } else if socket.can_send() { if let Some(channel) = session.is_report_pending() { - match channels.get_mut().reports_json() { + match fan_ctrl.channels.reports_json() { Ok(buf) => { send_line(&mut socket, &buf[..]); session.mark_report_sent(channel); -- 2.42.0 From 33070abd81504990fbd6a301821dfa8ca673931f Mon Sep 17 00:00:00 2001 From: Egor Savkin Date: Thu, 5 Jan 2023 13:04:21 +0800 Subject: [PATCH 10/16] Make commands more consistent Signed-off-by: Egor Savkin --- src/command_handler.rs | 47 ++++++++++-------- src/command_parser.rs | 110 +++++++++++++++++++++++++++++------------ 2 files changed, 104 insertions(+), 53 deletions(-) diff --git a/src/command_handler.rs b/src/command_handler.rs index 6049b99..7a4c8f1 100644 --- a/src/command_handler.rs +++ b/src/command_handler.rs @@ -342,26 +342,29 @@ impl Handler { Ok(Handler::Reset) } - fn fan(socket: &mut TcpSocket, fan_pwm: Option, fan_ctrl: &mut FanCtrl) -> Result { - match fan_pwm { - Some(val) => { - fan_ctrl.set_auto_mode(val == 0); - fan_ctrl.set_pwm(val); - }, - None => { - match fan_ctrl.summary() { - Ok(buf) => { - send_line(socket, &buf); - return Ok(Handler::Handled); - } - Err(e) => { - error!("unable to serialize fan summary: {:?}", e); - let _ = writeln!(socket, "{{\"error\":\"{:?}\"}}", e); - return Err(Error::ReportError); - } - }; + fn fan(socket: &mut TcpSocket, fan_pwm: u32, fan_ctrl: &mut FanCtrl) -> Result { + fan_ctrl.set_auto_mode(false); + fan_ctrl.set_pwm(fan_pwm); + send_line(socket, b"{}"); + 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 { + fan_ctrl.set_auto_mode(true); send_line(socket, b"{}"); Ok(Handler::Handled) } @@ -401,9 +404,11 @@ impl Handler { Command::Ipv4(config) => Handler::set_ipv4(socket, store, config), Command::Reset => Handler::reset(&mut fan_ctrl.channels), Command::Dfu => Handler::dfu(&mut fan_ctrl.channels), - Command::Fan {fan_pwm} => Handler::fan(socket, fan_pwm, fan_ctrl), + Command::FanSet {fan_pwm} => Handler::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::FanDefaults => Handler::fan_defaults(socket, fan_ctrl), + Command::FanCurveDefaults => Handler::fan_defaults(socket, fan_ctrl), } } } \ No newline at end of file diff --git a/src/command_parser.rs b/src/command_parser.rs index 66c2777..1846eb4 100644 --- a/src/command_parser.rs +++ b/src/command_parser.rs @@ -179,15 +179,17 @@ pub enum Command { rate: Option, }, Dfu, - Fan { - fan_pwm: Option + FanSet { + fan_pwm: u32 }, + FanAuto, + ShowFan, FanCurve { k_a: f64, k_b: f64, k_c: f64, }, - FanDefaults, + FanCurveDefaults, } fn end(input: &[u8]) -> IResult<&[u8], ()> { @@ -532,45 +534,56 @@ fn ipv4(input: &[u8]) -> IResult<&[u8], Result> { fn fan(input: &[u8]) -> IResult<&[u8], Result> { let (input, _) = tag("fan")(input)?; - let (input, fan_pwm) = alt(( + alt(( |input| { let (input, _) = whitespace(input)?; - let (input, value) = unsigned(input)?; - let (input, _) = end(input)?; - Ok((input, Some(value.unwrap_or(0)))) - }, - value(None, end) - ))(input)?; - let result = Ok(Command::Fan { fan_pwm }); - Ok((input, result)) + 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)?; + let (input, _) = end(input)?; + Ok((input, result)) + }, + value(Ok(Command::ShowFan), end) + ))(input) } fn fan_curve(input: &[u8]) -> IResult<&[u8], Result> { let (input, _) = tag("fcurve")(input)?; - let (input, curve) = alt(( + 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, 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)?; + let (input, _) = end(input)?; + 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() }))) + } else { + Err(nom::Err::Incomplete(Needed::Size(3))) + } + }, + ))(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))) - } + Ok((input, result)) }, - value(None, end) - ))(input)?; - - let result = match curve { - Some(curve) => Ok(Command::FanCurve { k_a: curve.0, k_b: curve.1, k_c: curve.2 }), - None => Err(Error::ParseFloat) - }; - Ok((input, result)) + value(Err(Error::Incomplete), end) + ))(input) } fn command(input: &[u8]) -> IResult<&[u8], Result> { @@ -586,7 +599,6 @@ fn command(input: &[u8]) -> IResult<&[u8], Result> { steinhart_hart, postfilter, value(Ok(Command::Dfu), tag("dfu")), - value(Ok(Command::FanDefaults), tag("fcurve-restore")), fan, fan_curve, ))(input) @@ -810,4 +822,38 @@ 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)); + } } -- 2.42.0 From a645bfb6e82e5a37a5871045d74f5152841cd31a Mon Sep 17 00:00:00 2001 From: Egor Savkin Date: Thu, 5 Jan 2023 13:05:22 +0800 Subject: [PATCH 11/16] Remove all tacho/status related logic Signed-off-by: Egor Savkin --- README.md | 17 +++-- src/command_handler.rs | 4 +- src/fan_ctrl.rs | 150 +---------------------------------------- src/main.rs | 15 ++--- src/pins.rs | 10 +-- 5 files changed, 22 insertions(+), 174 deletions(-) diff --git a/README.md b/README.md index 5275de7..c8f0e50 100644 --- a/README.md +++ b/README.md @@ -125,9 +125,10 @@ formatted as line-delimited JSON. | `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 0 to 100, where 0 is auto mode | +| `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-restore` | Set fan controller curve coefficients to defaults (see *Fan control* section) | +| `fcurve default` | Set fan controller curve coefficients to defaults (see *Fan control* section) | ## USB @@ -277,13 +278,11 @@ The thermostat implements a PID control loop for each of the TEC channels, more ## 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`, `tacho`, `abs_max_tec_i`, `auto_mode`. Please note that `tacho` shows *approximate* value, which -linearly correlates with the actual fan speed. -2. `fan ` - 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. -Values from `1` to `100` are used for setting the power from minimum to maximum respectively. +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 the square of the TEC's current. +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. -3. `fcurve ` - set coefficients of the controlling curve `a*x^2 + b*x + c`, where `x` is `abs_max_tec_i/MAX_TEC_I`, +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. -4. `fcurve-restore` - restore fan settings to defaults: `auto = true, a = 1.0, b = 0.0, c = 0.00`. +5. `fcurve default` - restore fan curve settings to defaults: `a = 1.0, b = 0.0, c = 0.0`. diff --git a/src/command_handler.rs b/src/command_handler.rs index 7a4c8f1..695bb7e 100644 --- a/src/command_handler.rs +++ b/src/command_handler.rs @@ -342,7 +342,7 @@ impl Handler { Ok(Handler::Reset) } - fn fan(socket: &mut TcpSocket, fan_pwm: u32, fan_ctrl: &mut FanCtrl) -> Result { + fn set_fan(socket: &mut TcpSocket, fan_pwm: u32, fan_ctrl: &mut FanCtrl) -> Result { fan_ctrl.set_auto_mode(false); fan_ctrl.set_pwm(fan_pwm); send_line(socket, b"{}"); @@ -404,7 +404,7 @@ impl Handler { Command::Ipv4(config) => Handler::set_ipv4(socket, store, config), Command::Reset => Handler::reset(&mut fan_ctrl.channels), Command::Dfu => Handler::dfu(&mut fan_ctrl.channels), - Command::FanSet {fan_pwm} => Handler::fan(socket, fan_pwm, fan_ctrl), + 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), diff --git a/src/fan_ctrl.rs b/src/fan_ctrl.rs index 4999ff9..cdf9022 100644 --- a/src/fan_ctrl.rs +++ b/src/fan_ctrl.rs @@ -2,23 +2,14 @@ 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; -pub type TachoPin = PC8>; // as stated in the schematics const MAX_TEC_I: f64 = 3.0; @@ -26,27 +17,13 @@ const MAX_TEC_I: f64 = 3.0; const MAX_USER_FAN_PWM: f64 = 100.0; const MIN_USER_FAN_PWM: f64 = 1.0; const MAX_FAN_PWM: f64 = 1.0; -// below this value, motor pulse signal is too weak to be registered by tachometer +// below this value, motor pulse signal is too weak const MIN_FAN_PWM: f64 = 0.05; -const TACHO_MEASURE_MS: i64 = 2500; -// by default up to 2 cycles are skipped on changes in PWM output, -// and the halt threshold will help detect the failure during these skipped cycles -const TACHO_HALT_THRESHOLD: u32 = 250; -const TACHO_SKIP_CYCLES: u8 = 2; - const DEFAULT_K_A: f64 = 1.0; const DEFAULT_K_B: f64 = 0.0; const DEFAULT_K_C: f64 = 0.0; -// This regression is from 6% to 25% lower than values registered in the experiments. -// Actual values would be better estimated by logarithmic regression, but that would require more -// runtime computation, and wouldn't give significant correlation difference -// (0.996 for log and 0.966 for quadratic regression). -const TACHO_REGRESSION_A: f64 = -0.04135128436; -const TACHO_REGRESSION_B: f64 = 6.23015531; -const TACHO_REGRESSION_C: f64 = 403.6833577; - #[derive(Serialize, Copy, Clone)] pub struct HWRev { @@ -54,83 +31,46 @@ pub struct HWRev { pub minor: u8, } -#[derive(Serialize, Clone, Copy, PartialEq)] -pub enum FanStatus { - OK, - NotAvailable, - TooSlow, - Halted -} - -struct TachoCtrl { - tacho: TachoPin, - tacho_cnt: u32, - tacho_value: Option, - prev_epoch: i64, -} - pub struct FanCtrl { fan: FanPin, - tacho: TachoCtrl, fan_auto: bool, available: bool, k_a: f64, k_b: f64, k_c: f64, pub channels: Channels, - last_status: FanStatus, - skip_cycles: u8, } impl FanCtrl { - pub fn new(mut fan: FanPin, tacho: TachoPin, channels: Channels, exti: &mut EXTI, syscfg: &mut SysCfg) -> Self { + pub fn new(mut fan: FanPin, channels: Channels) -> 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); } 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, - last_status: if available { FanStatus::OK } else { FanStatus::NotAvailable }, - skip_cycles: 0 } } - pub fn cycle(&mut self) -> Result<(), FanStatus> { - if self.available { - if self.tacho.cycle() { - self.skip_cycles >>= 1; - } - } + pub fn cycle(&mut self) { self.adjust_speed(); - let diagnose = self.diagnose(); - if (self.skip_cycles == 0 || diagnose == FanStatus::Halted) && diagnose != self.last_status { - self.last_status = diagnose; - Err(diagnose) - } else { - Ok(()) - } } pub fn summary(&mut self) -> Result { 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, - status: self.diagnose(), k_a: self.k_a, k_b: self.k_b, k_c: self.k_c, @@ -165,15 +105,11 @@ impl FanCtrl { #[inline] pub fn restore_defaults(&mut self) { - self.set_auto_mode(true); self.set_curve(DEFAULT_K_A, DEFAULT_K_B, DEFAULT_K_C); } pub fn set_pwm(&mut self, fan_pwm: u32) -> f64 { let fan_pwm = fan_pwm.min(MAX_USER_FAN_PWM as u32).max(MIN_USER_FAN_PWM as u32); - self.skip_cycles = if (self.tacho.get() as f64) <= Self::threshold_for_pwm(fan_pwm as f64) { - TACHO_SKIP_CYCLES - } else { self.skip_cycles }; let duty = Self::scale_number(fan_pwm as f64, MIN_FAN_PWM, MAX_FAN_PWM, MIN_USER_FAN_PWM, MAX_USER_FAN_PWM); let max = self.fan.get_max_duty(); let value = ((duty * (max as f64)) as u16).min(max); @@ -181,31 +117,11 @@ impl FanCtrl { value as f64 / (max as f64) } - #[inline] - fn threshold_for_pwm(fan_pwm: f64) -> f64 { - (TACHO_REGRESSION_A * fan_pwm + TACHO_REGRESSION_B) * fan_pwm + TACHO_REGRESSION_C - } - #[inline] fn scale_number(unscaled: f64, to_min: f64, to_max: f64, from_min: f64, from_max: f64) -> f64 { (to_max - to_min) * (unscaled - from_min) / (from_max - from_min) + to_min } - fn diagnose(&mut self) -> FanStatus { - if !self.available { - return FanStatus::NotAvailable; - } - let threshold = Self::threshold_for_pwm(self.get_pwm() as f64) as u32; - let tacho = self.tacho.get(); - if tacho >= threshold { - FanStatus::OK - } else if tacho >= TACHO_HALT_THRESHOLD { - FanStatus::TooSlow - } else { - FanStatus::Halted - } - } - fn get_pwm(&self) -> u32 { let duty = self.fan.get_duty(); let max = self.fan.get_max_duty(); @@ -213,53 +129,6 @@ impl FanCtrl { } } -impl TachoCtrl { - fn new(tacho: TachoPin) -> Self { - TachoCtrl { - tacho, - tacho_cnt: 0, - tacho_value: None, - prev_epoch: 0, - } - } - - fn init(&mut self, exti: &mut EXTI, syscfg: &mut SysCfg) { - // These lines do not cause NVIC to run the ISR, - // since the interrupt is masked 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. - // The possible solution would be to update the library to >=v0.14.*, - // and use its Timer's counter functionality. - self.tacho.make_interrupt_source(syscfg); - self.tacho.trigger_on_edge(exti, Edge::Rising); - self.tacho.enable_interrupt(exti); - } - - // returns whether the epoch elapsed - fn cycle(&mut self) -> bool { - 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; - true - } else { - false - } - } - - 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(), @@ -280,26 +149,13 @@ impl HWRev { #[derive(Serialize)] pub struct FanSummary { fan_pwm: u32, - tacho: u32, abs_max_tec_i: f64, auto_mode: bool, - status: FanStatus, k_a: f64, k_b: f64, k_c: f64, } -impl FanStatus { - pub fn fmt_u8(&self) -> &'static [u8] { - match *self { - FanStatus::OK => "Fan is OK".as_bytes(), - FanStatus::NotAvailable => "Fan is not available".as_bytes(), - FanStatus::TooSlow => "Fan is too slow".as_bytes(), - FanStatus::Halted => "Fan is halted".as_bytes(), - } - } -} - #[cfg(test)] mod test { use super::*; diff --git a/src/main.rs b/src/main.rs index ef629ef..538dd6e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -18,7 +18,6 @@ use stm32f4xx_hal::{ stm32::{CorePeripherals, Peripherals, SCB}, time::{U32Ext, MegaHertz}, watchdog::IndependentWatchdog, - syscfg::SysCfgExt }; use smoltcp::{ time::Instant, @@ -104,7 +103,7 @@ fn main() -> ! { cp.SCB.enable_icache(); cp.SCB.enable_dcache(&mut cp.CPUID); - let mut dp = Peripherals::take().unwrap(); + let dp = Peripherals::take().unwrap(); let clocks = dp.RCC.constrain() .cfgr .use_hse(HSE) @@ -120,7 +119,7 @@ fn main() -> ! { timer::setup(cp.SYST, clocks); - let (pins, mut leds, mut eeprom, eth_pins, usb, fan, tacho) = Pins::setup( + let (pins, mut leds, mut eeprom, eth_pins, usb, fan) = 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, @@ -151,7 +150,7 @@ fn main() -> ! { } } - let mut fan_ctrl = FanCtrl::new(fan, tacho, channels, &mut dp.EXTI, &mut dp.SYSCFG.constrain()); + let mut fan_ctrl = FanCtrl::new(fan, channels); // default net config: let mut ipv4_config = Ipv4Config { @@ -186,7 +185,7 @@ fn main() -> ! { server.for_each(|_, session| session.set_report_pending(channel.into())); } - let fan_status = fan_ctrl.cycle(); + fan_ctrl.cycle(); let instant = Instant::from_millis(i64::from(timer::now())); cortex_m::interrupt::free(net::clear_pending); @@ -239,12 +238,6 @@ fn main() -> ! { } } } - match fan_status { - Ok(_) => {} - Err(status) => { - send_line(&mut socket, status.fmt_u8()); - } - }; } }); } else { diff --git a/src/pins.rs b/src/pins.rs index 1b53a51..e89b872 100644 --- a/src/pins.rs +++ b/src/pins.rs @@ -33,7 +33,7 @@ use stm32_eth::EthPins; use crate::{ channel::{Channel0, Channel1}, leds::Leds, - fan_ctrl::{TachoPin, FanPin} + fan_ctrl::FanPin }; const PWM_FREQ: KiloHertz = KiloHertz(20u32); @@ -131,7 +131,7 @@ impl Pins { 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, FanPin, TachoPin) { + ) -> (Self, Leds, Eeprom, EthernetPins, USB, FanPin) { let gpioa = gpioa.split(); let gpiob = gpiob.split(); let gpioc = gpioc.split(); @@ -228,9 +228,9 @@ impl Pins { hclk: clocks.hclk(), }; - let fan = Timer::new(tim8, &clocks).pwm(gpioc.pc9.into_alternate(), 20u32.khz()); + let fan = Timer::new(tim8, &clocks).pwm(gpioc.pc9.into_alternate(), PWM_FREQ); - (pins, leds, eeprom, eth_pins, usb, fan, gpioc.pc8) + (pins, leds, eeprom, eth_pins, usb, fan) } /// Configure the GPIO pins for SPI operation, and initialize SPI @@ -342,7 +342,7 @@ impl PwmPins { PwmPins { max_v0, max_v1, max_i_pos0, max_i_pos1, - max_i_neg0, max_i_neg1 + max_i_neg0, max_i_neg1, } } } -- 2.42.0 From e6d928ef4e04098b4cfa3ff53fcb7117d8f85cd6 Mon Sep 17 00:00:00 2001 From: Egor Savkin Date: Fri, 6 Jan 2023 16:42:29 +0800 Subject: [PATCH 12/16] Fix PR comments Signed-off-by: Egor Savkin --- README.md | 2 +- src/command_handler.rs | 34 ++++++++++++++++----------------- src/fan_ctrl.rs | 43 +++++++++++++----------------------------- src/main.rs | 10 +++++----- src/pins.rs | 17 ++++++++++------- 5 files changed, 46 insertions(+), 60 deletions(-) diff --git a/README.md b/README.md index c8f0e50..7ba5996 100644 --- a/README.md +++ b/README.md @@ -279,7 +279,7 @@ The thermostat implements a PID control loop for each of the TEC channels, more 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 the square of the TEC's current. +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`, diff --git a/src/command_handler.rs b/src/command_handler.rs index 695bb7e..76f507a 100644 --- a/src/command_handler.rs +++ b/src/command_handler.rs @@ -381,29 +381,29 @@ impl Handler { Ok(Handler::Handled) } - pub fn handle_command(command: Command, socket: &mut TcpSocket, session: &Session, leds: &mut Leds, store: &mut FlashStore, ipv4_config: &mut Ipv4Config, fan_ctrl: &mut FanCtrl) -> Result { + 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 { 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, &mut fan_ctrl.channels), - Command::Show(ShowCommand::Pid) => Handler::show_pid(socket, &mut fan_ctrl.channels), - Command::Show(ShowCommand::Pwm) => Handler::show_pwm(socket, &mut fan_ctrl.channels), - Command::Show(ShowCommand::SteinhartHart) => Handler::show_steinhart_hart(socket, &mut fan_ctrl.channels), - Command::Show(ShowCommand::PostFilter) => Handler::show_post_filter(socket, &mut fan_ctrl.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, &mut fan_ctrl.channels, leds, channel), - Command::Pwm { channel, pin, value } => Handler::set_pwm(socket, &mut fan_ctrl.channels, leds, channel, pin, value), - Command::CenterPoint { channel, center } => Handler::set_center_point(socket, &mut fan_ctrl.channels, channel, center), - Command::Pid { channel, parameter, value } => Handler::set_pid(socket, &mut fan_ctrl.channels, channel, parameter, value), - Command::SteinhartHart { channel, parameter, value } => Handler::set_steinhart_hart(socket, &mut fan_ctrl.channels, channel, parameter, value), - Command::PostFilter { channel, rate: None } => Handler::reset_post_filter(socket, &mut fan_ctrl.channels, channel), - Command::PostFilter { channel, rate: Some(rate) } => Handler::set_post_filter(socket, &mut fan_ctrl.channels, channel, rate), - Command::Load { channel } => Handler::load_channel(socket, &mut fan_ctrl.channels, store, channel), - Command::Save { channel } => Handler::save_channel(socket, &mut fan_ctrl.channels, channel, store), + Command::PwmPid { channel } => Handler::engage_pid(socket, channels, leds, channel), + Command::Pwm { channel, pin, value } => Handler::set_pwm(socket, channels, leds, channel, pin, value), + Command::CenterPoint { channel, center } => Handler::set_center_point(socket, channels, channel, center), + Command::Pid { channel, parameter, value } => Handler::set_pid(socket, channels, channel, parameter, value), + Command::SteinhartHart { channel, parameter, value } => Handler::set_steinhart_hart(socket, channels, channel, parameter, value), + Command::PostFilter { channel, rate: None } => Handler::reset_post_filter(socket, channels, channel), + Command::PostFilter { channel, rate: Some(rate) } => Handler::set_post_filter(socket, channels, channel, rate), + Command::Load { channel } => Handler::load_channel(socket, channels, store, channel), + Command::Save { channel } => Handler::save_channel(socket, channels, channel, store), Command::Ipv4(config) => Handler::set_ipv4(socket, store, config), - Command::Reset => Handler::reset(&mut fan_ctrl.channels), - Command::Dfu => Handler::dfu(&mut fan_ctrl.channels), + Command::Reset => Handler::reset(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), diff --git a/src/fan_ctrl.rs b/src/fan_ctrl.rs index cdf9022..25e13cb 100644 --- a/src/fan_ctrl.rs +++ b/src/fan_ctrl.rs @@ -1,3 +1,4 @@ +use num_traits::Float; use serde::Serialize; use stm32f4xx_hal::{ pwm::{self, PwmChannels}, @@ -6,7 +7,7 @@ use stm32f4xx_hal::{ use crate::{ pins::HWRevPins, - channels::{Channels, JsonBuffer}, + channels::JsonBuffer, }; pub type FanPin = PwmChannels; @@ -17,8 +18,8 @@ const MAX_TEC_I: f64 = 3.0; const MAX_USER_FAN_PWM: f64 = 100.0; const MIN_USER_FAN_PWM: f64 = 1.0; const MAX_FAN_PWM: f64 = 1.0; -// below this value, motor pulse signal is too weak -const MIN_FAN_PWM: f64 = 0.05; +// below this value motor circuit assumes there is no signal and runs full speed +const MIN_FAN_PWM: f64 = 0.02; const DEFAULT_K_A: f64 = 1.0; const DEFAULT_K_B: f64 = 0.0; @@ -38,12 +39,12 @@ pub struct FanCtrl { k_a: f64, k_b: f64, k_c: f64, - pub channels: Channels, + abs_max_tec_i: f64, } impl FanCtrl { - pub fn new(mut fan: FanPin, channels: Channels) -> Self { - let available = channels.hwrev.fan_available(); + pub fn new(mut fan: FanPin, hwrev: &HWRev) -> Self { + let available = hwrev.fan_available(); if available { fan.set_duty(0); @@ -57,11 +58,12 @@ impl FanCtrl { k_a: DEFAULT_K_A, k_b: DEFAULT_K_B, k_c: DEFAULT_K_C, - channels, + abs_max_tec_i: 0f64, } } - pub fn cycle(&mut self) { + pub fn cycle(&mut self, abs_max_tec_i: f64) { + self.abs_max_tec_i = abs_max_tec_i; self.adjust_speed(); } @@ -69,7 +71,7 @@ impl FanCtrl { if self.available { let summary = FanSummary { fan_pwm: self.get_pwm(), - abs_max_tec_i: self.channels.current_abs_max_tec_i(), + abs_max_tec_i: self.abs_max_tec_i, auto_mode: self.fan_auto, k_a: self.k_a, k_b: self.k_b, @@ -84,26 +86,23 @@ impl FanCtrl { 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; + 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); } } - #[inline] pub fn set_auto_mode(&mut self, fan_auto: bool) { self.fan_auto = fan_auto; } - #[inline] pub fn set_curve(&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_curve(DEFAULT_K_A, DEFAULT_K_B, DEFAULT_K_C); } @@ -117,7 +116,6 @@ impl FanCtrl { value as f64 / (max as f64) } - #[inline] fn scale_number(unscaled: f64, to_min: f64, to_max: f64, from_min: f64, from_max: f64) -> f64 { (to_max - to_min) * (unscaled - from_min) / (from_max - from_min) + to_min } @@ -125,7 +123,7 @@ impl FanCtrl { fn get_pwm(&self) -> u32 { let duty = self.fan.get_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) + 0.5) as u32 + Self::scale_number(duty as f64 / (max as f64), MIN_USER_FAN_PWM, MAX_USER_FAN_PWM, MIN_FAN_PWM, MAX_FAN_PWM).round() as u32 } } @@ -155,18 +153,3 @@ pub struct FanSummary { k_b: f64, k_c: f64, } - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn test_scaler() { - for x in 1..100 { - assert_eq!((FanCtrl::scale_number( - FanCtrl::scale_number(x as f64, MIN_FAN_PWM, MAX_FAN_PWM, MIN_USER_FAN_PWM, MAX_USER_FAN_PWM), - MIN_USER_FAN_PWM, MAX_USER_FAN_PWM, MIN_FAN_PWM, MAX_FAN_PWM) + 0.5) as i32, - x); - } - } -} diff --git a/src/main.rs b/src/main.rs index 538dd6e..ba2d21b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -150,7 +150,7 @@ fn main() -> ! { } } - let mut fan_ctrl = FanCtrl::new(fan, channels); + let mut fan_ctrl = FanCtrl::new(fan, &channels.hwrev); // default net config: let mut ipv4_config = Ipv4Config { @@ -180,12 +180,12 @@ fn main() -> ! { loop { let mut new_ipv4_config = None; let instant = Instant::from_millis(i64::from(timer::now())); - let updated_channel = fan_ctrl.channels.poll_adc(instant); + let updated_channel = channels.poll_adc(instant); if let Some(channel) = updated_channel { server.for_each(|_, session| session.set_report_pending(channel.into())); } - fan_ctrl.cycle(); + fan_ctrl.cycle(channels.current_abs_max_tec_i()); let instant = Instant::from_millis(i64::from(timer::now())); cortex_m::interrupt::free(net::clear_pending); @@ -210,7 +210,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, 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) { Ok(Handler::NewIPV4(ip)) => new_ipv4_config = Some(ip), Ok(Handler::Handled) => {}, Ok(Handler::CloseSocket) => socket.close(), @@ -227,7 +227,7 @@ fn main() -> ! { } } else if socket.can_send() { if let Some(channel) = session.is_report_pending() { - match fan_ctrl.channels.reports_json() { + match channels.reports_json() { Ok(buf) => { send_line(&mut socket, &buf[..]); session.mark_report_sent(channel); diff --git a/src/pins.rs b/src/pins.rs index e89b872..87ff6b3 100644 --- a/src/pins.rs +++ b/src/pins.rs @@ -26,7 +26,7 @@ use stm32f4xx_hal::{ TIM1, TIM3, TIM8 }, timer::Timer, - time::{U32Ext, KiloHertz}, + time::U32Ext, }; use eeprom24x::{self, Eeprom24x}; use stm32_eth::EthPins; @@ -36,8 +36,6 @@ use crate::{ fan_ctrl::FanPin }; -const PWM_FREQ: KiloHertz = KiloHertz(20u32); - pub type Eeprom = Eeprom24x< I2c>, @@ -228,7 +226,11 @@ impl Pins { hclk: clocks.hclk(), }; - let fan = Timer::new(tim8, &clocks).pwm(gpioc.pc9.into_alternate(), PWM_FREQ); + // Though there is not enough evidence available for concrete fan model, + // it is generally advised to have higher PWM frequencies for brushless motors, + // so that it would produce less audible noise. + // Source: https://www.controleng.com/articles/understanding-the-effect-of-pwm-when-controlling-a-brushless-dc-motor/ + let fan = Timer::new(tim8, &clocks).pwm(gpioc.pc9.into_alternate(), 80u32.khz()); (pins, leds, eeprom, eth_pins, usb, fan) } @@ -312,6 +314,7 @@ impl PwmPins { max_i_neg0: PE13, max_i_neg1: PE14, ) -> PwmPins { + let freq = 20u32.khz(); fn init_pwm_pin>(pin: &mut P) { pin.set_duty(0); @@ -321,8 +324,8 @@ impl PwmPins { max_v0.into_alternate(), max_v1.into_alternate(), ); - //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, PWM_FREQ); + //let (mut max_v0, mut max_v1) = pwm::tim3(tim3, channels, clocks, freq); + let (mut max_v0, mut max_v1) = Timer::new(tim3, &clocks).pwm(channels, freq); init_pwm_pin(&mut max_v0); init_pwm_pin(&mut max_v1); @@ -333,7 +336,7 @@ impl PwmPins { max_i_neg1.into_alternate(), ); let (mut max_i_pos0, mut max_i_pos1, mut max_i_neg0, mut max_i_neg1) = - Timer::new(tim1, &clocks).pwm(channels, PWM_FREQ); + Timer::new(tim1, &clocks).pwm(channels, freq); init_pwm_pin(&mut max_i_pos0); init_pwm_pin(&mut max_i_neg0); init_pwm_pin(&mut max_i_pos1); -- 2.42.0 From 069e178966c4bfe2f95e36454e3c5edb01556ab2 Mon Sep 17 00:00:00 2001 From: Egor Savkin Date: Thu, 2 Feb 2023 10:52:22 +0800 Subject: [PATCH 13/16] Change PWM freq to 25kHz Signed-off-by: Egor Savkin --- src/fan_ctrl.rs | 4 ++-- src/pins.rs | 8 +++----- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/fan_ctrl.rs b/src/fan_ctrl.rs index 25e13cb..e0e03e6 100644 --- a/src/fan_ctrl.rs +++ b/src/fan_ctrl.rs @@ -18,8 +18,8 @@ const MAX_TEC_I: f64 = 3.0; const MAX_USER_FAN_PWM: f64 = 100.0; const MIN_USER_FAN_PWM: f64 = 1.0; const MAX_FAN_PWM: f64 = 1.0; -// below this value motor circuit assumes there is no signal and runs full speed -const MIN_FAN_PWM: f64 = 0.02; +// below this value motor's autostart feature may fail +const MIN_FAN_PWM: f64 = 0.04; const DEFAULT_K_A: f64 = 1.0; const DEFAULT_K_B: f64 = 0.0; diff --git a/src/pins.rs b/src/pins.rs index 87ff6b3..64b3821 100644 --- a/src/pins.rs +++ b/src/pins.rs @@ -226,11 +226,9 @@ impl Pins { hclk: clocks.hclk(), }; - // Though there is not enough evidence available for concrete fan model, - // it is generally advised to have higher PWM frequencies for brushless motors, - // so that it would produce less audible noise. - // Source: https://www.controleng.com/articles/understanding-the-effect-of-pwm-when-controlling-a-brushless-dc-motor/ - let fan = Timer::new(tim8, &clocks).pwm(gpioc.pc9.into_alternate(), 80u32.khz()); + // According to `SUNON DC Brushless Fan & Blower(255-E)` catalogue p.36-37 + // Model name: MF35101V1-1000U-G99 + let fan = Timer::new(tim8, &clocks).pwm(gpioc.pc9.into_alternate(), 25u32.khz()); (pins, leds, eeprom, eth_pins, usb, fan) } -- 2.42.0 From 83d5c28a67b38c03733e6f8e5acda866cc8ae51e Mon Sep 17 00:00:00 2001 From: Egor Savkin Date: Tue, 7 Feb 2023 12:04:59 +0800 Subject: [PATCH 14/16] Disable fan auto mode by default for Thermostat v2.2 Signed-off-by: Egor Savkin --- src/fan_ctrl.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/fan_ctrl.rs b/src/fan_ctrl.rs index e0e03e6..9b3fda9 100644 --- a/src/fan_ctrl.rs +++ b/src/fan_ctrl.rs @@ -54,7 +54,9 @@ impl FanCtrl { FanCtrl { fan, available, - fan_auto: true, + // do not enable auto mode by default, + // but allow to turn it on on customer's own risk + fan_auto: hwrev.fan_auto_mode_available(), k_a: DEFAULT_K_A, k_b: DEFAULT_K_B, k_c: DEFAULT_K_C, @@ -142,6 +144,12 @@ impl HWRev { 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)] -- 2.42.0 From 2c9436a0b300ecab1ea8bc670cead571ecef2215 Mon Sep 17 00:00:00 2001 From: Egor Savkin Date: Thu, 16 Feb 2023 13:37:17 +0800 Subject: [PATCH 15/16] Move HWRev to separate file, address PR comments, emit warning on the using PWM on unsupported fans Signed-off-by: Egor Savkin --- README.md | 1 + src/channels.rs | 12 ++--- src/command_handler.rs | 35 +++++++++++-- src/command_parser.rs | 16 ++++-- src/fan_ctrl.rs | 115 +++++++++++++++++++---------------------- src/hw_rev.rs | 41 +++++++++++++++ src/main.rs | 10 ++-- src/pins.rs | 3 +- 8 files changed, 149 insertions(+), 84 deletions(-) create mode 100644 src/hw_rev.rs diff --git a/README.md b/README.md index 7ba5996..975e4d0 100644 --- a/README.md +++ b/README.md @@ -129,6 +129,7 @@ formatted as line-delimited JSON. | `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 | ## USB diff --git a/src/channels.rs b/src/channels.rs index c594c34..c86305d 100644 --- a/src/channels.rs +++ b/src/channels.rs @@ -1,5 +1,5 @@ use core::cmp::max_by; -use heapless::{consts::{U2, U1024}, Vec}; +use heapless::{consts::U2, Vec}; use serde::{Serialize, Serializer}; use smoltcp::time::Instant; use stm32f4xx_hal::hal; @@ -17,9 +17,9 @@ use crate::{ channel::{Channel, Channel0, Channel1}, channel_state::ChannelState, command_parser::{CenterPoint, PwmPin}, + command_handler::JsonBuffer, pins, steinhart_hart, - fan_ctrl::HWRev, }; pub const CHANNELS: usize = 2; @@ -35,7 +35,6 @@ pub struct Channels { /// stm32f4 integrated adc pins_adc: pins::PinsAdc, pub pwm: pins::PwmPins, - pub hwrev: HWRev, } impl Channels { @@ -57,8 +56,7 @@ impl Channels { let channel1 = Channel::new(pins.channel1, adc_calibration1); let pins_adc = pins.pins_adc; let pwm = pins.pwm; - let mut channels = Channels { channel0, channel1, adc, pins_adc, pwm, - hwrev: HWRev::detect_hw_rev(&pins.hwrev)}; + let mut channels = Channels { channel0, channel1, adc, pins_adc, pwm }; for channel in 0..CHANNELS { channels.channel_state(channel).vref = channels.read_vref(channel); channels.calibrate_dac_value(channel); @@ -458,7 +456,6 @@ impl Channels { tec_i, tec_u_meas: self.get_tec_v(channel), pid_output, - hwrev: self.hwrev } } @@ -531,8 +528,6 @@ impl Channels { } } -pub type JsonBuffer = Vec; - #[derive(Serialize)] pub struct Report { channel: usize, @@ -550,7 +545,6 @@ pub struct Report { tec_i: ElectricCurrent, tec_u_meas: ElectricPotential, pid_output: ElectricCurrent, - hwrev: HWRev, } pub struct CenterPointJson(CenterPoint); diff --git a/src/command_handler.rs b/src/command_handler.rs index 76f507a..50547c0 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::{ @@ -24,6 +25,7 @@ use super::{ flash_store::FlashStore, session::Session, FanCtrl, + hw_rev::HWRev, }; use uom::{ @@ -56,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 { @@ -345,7 +349,11 @@ impl Handler { fn set_fan(socket: &mut TcpSocket, fan_pwm: u32, fan_ctrl: &mut FanCtrl) -> Result { fan_ctrl.set_auto_mode(false); 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) } @@ -365,11 +373,15 @@ impl Handler { fn fan_auto(socket: &mut TcpSocket, fan_ctrl: &mut FanCtrl) -> Result { 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) } - fn fan_curve(socket: &mut TcpSocket, fan_ctrl: &mut FanCtrl, k_a: f64, k_b: f64, k_c: f64) -> Result { + 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) @@ -381,7 +393,21 @@ impl Handler { 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 { + 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), @@ -409,6 +435,7 @@ impl Handler { 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 1846eb4..8a32053 100644 --- a/src/command_parser.rs +++ b/src/command_parser.rs @@ -185,11 +185,12 @@ pub enum Command { FanAuto, ShowFan, FanCurve { - k_a: f64, - k_b: f64, - k_c: f64, + k_a: f32, + k_b: f32, + k_c: f32, }, FanCurveDefaults, + ShowHWRev, } fn end(input: &[u8]) -> IResult<&[u8], ()> { @@ -573,7 +574,7 @@ fn fan_curve(input: &[u8]) -> IResult<&[u8], Result> { 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, 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 { Err(nom::Err::Incomplete(Needed::Size(3))) } @@ -601,6 +602,7 @@ fn command(input: &[u8]) -> IResult<&[u8], Result> { value(Ok(Command::Dfu), tag("dfu")), fan, fan_curve, + value(Ok(Command::ShowHWRev), tag("hwrev")), ))(input) } @@ -856,4 +858,10 @@ mod test { 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 index 9b3fda9..9480493 100644 --- a/src/fan_ctrl.rs +++ b/src/fan_ctrl.rs @@ -6,65 +6,62 @@ use stm32f4xx_hal::{ }; use crate::{ - pins::HWRevPins, - channels::JsonBuffer, + hw_rev::HWRev, + command_handler::JsonBuffer, }; pub type FanPin = PwmChannels; // 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 MIN_USER_FAN_PWM: f64 = 1.0; -const MAX_FAN_PWM: f64 = 1.0; +const MAX_USER_FAN_PWM: f32 = 100.0; +const MIN_USER_FAN_PWM: f32 = 1.0; +const MAX_FAN_PWM: f32 = 1.0; // 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_B: f64 = 0.0; -const DEFAULT_K_C: f64 = 0.0; - - -#[derive(Serialize, Copy, Clone)] -pub struct HWRev { - pub major: u8, - pub minor: u8, -} +const DEFAULT_K_A: f32 = 1.0; +const DEFAULT_K_B: f32 = 0.0; +const DEFAULT_K_C: f32 = 0.0; pub struct FanCtrl { fan: FanPin, fan_auto: bool, available: bool, - k_a: f64, - k_b: f64, - k_c: f64, - abs_max_tec_i: f64, + default_auto: bool, + pwm_enabled: bool, + k_a: f32, + k_b: f32, + k_c: f32, + abs_max_tec_i: f32, } 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 default_auto = hwrev.fan_default_auto(); - if available { - fan.set_duty(0); - fan.enable(); - } - - FanCtrl { + let mut fan_ctrl = FanCtrl { fan, available, // do not enable auto mode by default, - // but allow to turn it on on customer's own risk - fan_auto: hwrev.fan_auto_mode_available(), + // but allow to turn it on on user's own risk + default_auto, + fan_auto: default_auto, + pwm_enabled: false, k_a: DEFAULT_K_A, k_b: DEFAULT_K_B, 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.adjust_speed(); } @@ -99,7 +96,7 @@ impl FanCtrl { 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_b = k_b; self.k_c = k_c; @@ -109,55 +106,47 @@ impl FanCtrl { 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 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 value = ((duty * (max as f64)) as u16).min(max); + let value = ((duty * (max as f32)) as u16).min(max); 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 } fn get_pwm(&self) -> u32 { let duty = self.fan.get_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 { - 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 } + fn enable_pwm(&mut self) { + if self.available { + self.fan.set_duty(0); + self.fan.enable(); + self.pwm_enabled = true; } } - - 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)] pub struct FanSummary { fan_pwm: u32, - abs_max_tec_i: f64, + abs_max_tec_i: f32, auto_mode: bool, - k_a: f64, - k_b: f64, - k_c: f64, + 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..413dc72 --- /dev/null +++ b/src/hw_rev.rs @@ -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 { + // 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 { + serde_json_core::to_vec(&self) + + } +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index ba2d21b..4fa16d8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -55,6 +55,8 @@ mod command_handler; use command_handler::Handler; mod fan_ctrl; use fan_ctrl::FanCtrl; +mod hw_rev; +use hw_rev::HWRev; const HSE: MegaHertz = MegaHertz(8); #[cfg(not(feature = "semihosting"))] @@ -138,6 +140,8 @@ fn main() -> ! { let mut store = flash_store::store(dp.FLASH); + let hwrev = HWRev::detect_hw_rev(&pins.hwrev); + let mut channels = Channels::new(pins); for c in 0..CHANNELS { match store.read_value::(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: let mut ipv4_config = Ipv4Config { @@ -185,7 +189,7 @@ fn main() -> ! { 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())); 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. Ok(SessionInput::Nothing) => {} 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::Handled) => {}, Ok(Handler::CloseSocket) => socket.close(), diff --git a/src/pins.rs b/src/pins.rs index 64b3821..be62540 100644 --- a/src/pins.rs +++ b/src/pins.rs @@ -227,7 +227,8 @@ impl Pins { }; // 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()); (pins, leds, eeprom, eth_pins, usb, fan) -- 2.42.0 From afdab2f02556cb1409ffc869837f92736fa2b2f5 Mon Sep 17 00:00:00 2001 From: Egor Savkin Date: Tue, 21 Mar 2023 17:33:22 +0800 Subject: [PATCH 16/16] Make hwrev to be settings provider Signed-off-by: Egor Savkin --- README.md | 2 +- src/command_handler.rs | 16 +++++-- src/command_parser.rs | 3 -- src/fan_ctrl.rs | 97 +++++++++++++++++++++--------------------- src/hw_rev.rs | 63 ++++++++++++++++++++++----- src/main.rs | 7 +-- src/pins.rs | 21 ++++----- 7 files changed, 126 insertions(+), 83 deletions(-) diff --git a/README.md b/README.md index 975e4d0..f1bcc1b 100644 --- a/README.md +++ b/README.md @@ -129,7 +129,7 @@ formatted as line-delimited JSON. | `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 | +| `hwrev` | Show hardware revision, and settings related to it | ## USB diff --git a/src/command_handler.rs b/src/command_handler.rs index 50547c0..b2d3ae0 100644 --- a/src/command_handler.rs +++ b/src/command_handler.rs @@ -347,12 +347,16 @@ impl Handler { } 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.is_default_auto() { + 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 on your own risk!\" }"); + send_line(socket, b"{ \"warning\": \"this fan doesn't have full PWM support. Use it at your own risk!\" }"); } Ok(Handler::Handled) } @@ -372,11 +376,15 @@ impl Handler { } 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.is_default_auto() { + 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 on your own risk!\" }"); + send_line(socket, b"{ \"warning\": \"this fan doesn't have full PWM support. Use it at your own risk!\" }"); } Ok(Handler::Handled) } diff --git a/src/command_parser.rs b/src/command_parser.rs index 8a32053..b9e70bb 100644 --- a/src/command_parser.rs +++ b/src/command_parser.rs @@ -549,7 +549,6 @@ fn fan(input: &[u8]) -> IResult<&[u8], Result> { Ok((input, Ok(Command::FanSet { fan_pwm: value.unwrap_or(0)}))) }, ))(input)?; - let (input, _) = end(input)?; Ok((input, result)) }, value(Ok(Command::ShowFan), end) @@ -572,7 +571,6 @@ fn fan_curve(input: &[u8]) -> IResult<&[u8], Result> { 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, 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 { @@ -580,7 +578,6 @@ fn fan_curve(input: &[u8]) -> IResult<&[u8], Result> { } }, ))(input)?; - let (input, _) = end(input)?; Ok((input, result)) }, value(Err(Error::Incomplete), end) diff --git a/src/fan_ctrl.rs b/src/fan_ctrl.rs index 9480493..b95a723 100644 --- a/src/fan_ctrl.rs +++ b/src/fan_ctrl.rs @@ -6,7 +6,7 @@ use stm32f4xx_hal::{ }; use crate::{ - hw_rev::HWRev, + hw_rev::HWSettings, command_handler::JsonBuffer, }; @@ -17,43 +17,32 @@ const MAX_TEC_I: f32 = 3.0; const MAX_USER_FAN_PWM: f32 = 100.0; const MIN_USER_FAN_PWM: f32 = 1.0; -const MAX_FAN_PWM: f32 = 1.0; -// below this value motor's autostart feature may fail -const MIN_FAN_PWM: f32 = 0.04; -const DEFAULT_K_A: f32 = 1.0; -const DEFAULT_K_B: f32 = 0.0; -const DEFAULT_K_C: f32 = 0.0; pub struct FanCtrl { - fan: FanPin, + fan: Option, fan_auto: bool, - available: bool, - default_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: FanPin, hwrev: HWRev) -> Self { - let available = hwrev.fan_available(); - let default_auto = hwrev.fan_default_auto(); - + pub fn new(fan: Option, hw_settings: HWSettings) -> Self { let mut fan_ctrl = FanCtrl { fan, - available, // do not enable auto mode by default, - // but allow to turn it on on user's own risk - default_auto, - fan_auto: default_auto, + // but allow to turn it at the user's own risk + fan_auto: hw_settings.fan_pwm_recommended, pwm_enabled: false, - k_a: DEFAULT_K_A, - k_b: DEFAULT_K_B, - k_c: DEFAULT_K_C, + 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(); @@ -63,11 +52,16 @@ impl FanCtrl { pub fn cycle(&mut self, abs_max_tec_i: f32) { self.abs_max_tec_i = abs_max_tec_i; - self.adjust_speed(); + 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.available { + if self.hw_settings.fan_available { let summary = FanSummary { fan_pwm: self.get_pwm(), abs_max_tec_i: self.abs_max_tec_i, @@ -83,15 +77,6 @@ impl FanCtrl { } } - pub fn adjust_speed(&mut self) { - if self.fan_auto && self.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 set_auto_mode(&mut self, fan_auto: bool) { self.fan_auto = fan_auto; } @@ -103,44 +88,58 @@ impl FanCtrl { } pub fn restore_defaults(&mut self) { - self.set_curve(DEFAULT_K_A, DEFAULT_K_B, DEFAULT_K_C); + 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.pwm_enabled { - self.enable_pwm() + 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 = 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 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); - self.fan.set_duty(value); + fan.set_duty(value); value as f32 / (max as f32) } - pub fn is_default_auto(&self) -> bool { - self.default_auto + pub fn fan_pwm_recommended(&self) -> bool { + self.hw_settings.fan_pwm_recommended } - 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 + pub fn fan_available(&self) -> bool { + self.hw_settings.fan_available } fn get_pwm(&self) -> u32 { - let duty = self.fan.get_duty(); - let max = self.fan.get_max_duty(); - 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 + 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) { - if self.available { - self.fan.set_duty(0); - self.fan.enable(); + 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, diff --git a/src/hw_rev.rs b/src/hw_rev.rs index 413dc72..ef022a1 100644 --- a/src/hw_rev.rs +++ b/src/hw_rev.rs @@ -2,7 +2,7 @@ use serde::Serialize; use crate::{ pins::HWRevPins, - command_handler::JsonBuffer + command_handler::JsonBuffer, }; #[derive(Serialize, Copy, Clone)] @@ -11,6 +11,23 @@ pub struct HWRev { 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 { @@ -24,18 +41,42 @@ impl HWRev { } } - pub fn fan_available(&self) -> bool { - self.major == 2 && self.minor == 2 - } - - pub fn fan_default_auto(&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 + 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 { - serde_json_core::to_vec(&self) - + 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 4fa16d8..639f356 100644 --- a/src/main.rs +++ b/src/main.rs @@ -56,7 +56,6 @@ use command_handler::Handler; mod fan_ctrl; use fan_ctrl::FanCtrl; mod hw_rev; -use hw_rev::HWRev; const HSE: MegaHertz = MegaHertz(8); #[cfg(not(feature = "semihosting"))] @@ -121,7 +120,7 @@ fn main() -> ! { timer::setup(cp.SYST, clocks); - let (pins, mut leds, mut eeprom, eth_pins, usb, fan) = Pins::setup( + 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, @@ -140,8 +139,6 @@ fn main() -> ! { let mut store = flash_store::store(dp.FLASH); - let hwrev = HWRev::detect_hw_rev(&pins.hwrev); - let mut channels = Channels::new(pins); for c in 0..CHANNELS { match store.read_value::(CHANNEL_CONFIG_KEY[c]) { @@ -154,7 +151,7 @@ fn main() -> ! { } } - let mut fan_ctrl = FanCtrl::new(fan, hwrev); + let mut fan_ctrl = FanCtrl::new(fan, hw_settings); // default net config: let mut ipv4_config = Ipv4Config { diff --git a/src/pins.rs b/src/pins.rs index be62540..6294661 100644 --- a/src/pins.rs +++ b/src/pins.rs @@ -33,7 +33,8 @@ use stm32_eth::EthPins; use crate::{ channel::{Channel0, Channel1}, leds::Leds, - fan_ctrl::FanPin + fan_ctrl::FanPin, + hw_rev::{HWRev, HWSettings}, }; pub type Eeprom = Eeprom24x< @@ -116,7 +117,6 @@ pub struct Pins { pub pwm: PwmPins, pub channel0: ChannelPinSet, pub channel1: ChannelPinSet, - pub hwrev: HWRevPins } impl Pins { @@ -129,7 +129,7 @@ impl Pins { 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, FanPin) { + ) -> (Self, Leds, Eeprom, EthernetPins, USB, Option, HWRev, HWSettings) { let gpioa = gpioa.split(); let gpiob = gpiob.split(); let gpioc = gpioc.split(); @@ -196,10 +196,12 @@ impl Pins { pwm, channel0, channel1, - hwrev: HWRevPins {hwrev0: gpiod.pd0, hwrev1: gpiod.pd1, - hwrev2: gpiod.pd2, hwrev3: gpiod.pd3} }; + 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(); @@ -226,12 +228,11 @@ impl Pins { hclk: clocks.hclk(), }; - // According to `SUNON DC Brushless Fan & Blower(255-E)` catalogue p.36-37 - // 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 = 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) + (pins, leds, eeprom, eth_pins, usb, fan, hwrev, hw_settings) } /// Configure the GPIO pins for SPI operation, and initialize SPI -- 2.42.0