Skip to content

Commit ddc5d18

Browse files
feat(fan-controller): add Bitaxe cooling control
Add fan control and temperature filtering for Bitaxe boards to improve thermal management and protect hardware under high load. Co-authored-by: Johnny Santos <johnnyadsantos@gmail.com>
1 parent ece3334 commit ddc5d18

3 files changed

Lines changed: 641 additions & 57 deletions

File tree

mujina-miner/src/board/bitaxe.rs

Lines changed: 81 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
use anyhow::{Context as _, Result, anyhow, bail};
2+
mod fan_controller;
3+
mod thermal_task;
4+
25
use async_trait::async_trait;
36
use futures::sink::SinkExt;
47
use std::{
@@ -48,6 +51,9 @@ use super::{
4851
pattern::{Match, StringMatch},
4952
};
5053

54+
use fan_controller::{FanControllerConfig, ThermalReadings};
55+
use thermal_task::ThermalTask;
56+
5157
/// Adapter implementing `AsicEnable` for Bitaxe's GPIO-based reset control.
5258
struct BitaxeAsicEnable {
5359
/// Reset pin (directly controls nRST on the BM1370)
@@ -125,8 +131,8 @@ pub struct BitaxeBoard {
125131
asic_nrst: Option<BitaxeRawGpioPin>,
126132
/// I2C bus controller
127133
i2c: BitaxeRawI2c,
128-
/// Fan controller (board-controlled only, not shared with thread)
129-
fan_controller: Option<Emc2101<BitaxeRawI2c>>,
134+
/// Concurrent EMC2101 handle
135+
emc2101: Option<Arc<Mutex<Emc2101<BitaxeRawI2c>>>>,
130136
/// Voltage regulator (shared with thread, cached state)
131137
regulator: Option<Arc<Mutex<Tps546<BitaxeRawI2c>>>>,
132138
/// Writer for sending commands to chips (transferred to hash thread)
@@ -147,6 +153,10 @@ pub struct BitaxeBoard {
147153
/// Channel for publishing board telemetry to the API server.
148154
/// Taken by `spawn_stats_monitor` which publishes periodic snapshots.
149155
telemetry_tx: Option<watch::Sender<BoardTelemetry>>,
156+
/// Fan controller configuration (target temperature, etc.)
157+
fan_config: FanControllerConfig,
158+
/// Handles for the thermal monitor async task
159+
thermal_task_handle: Option<tokio::task::JoinHandle<()>>,
150160
}
151161

152162
impl BitaxeBoard {
@@ -195,7 +205,7 @@ impl BitaxeBoard {
195205
control_channel,
196206
asic_nrst: None,
197207
i2c,
198-
fan_controller: None,
208+
emc2101: None,
199209
regulator: None,
200210
data_writer: Some(FramedWrite::new(data_writer, bm13xx::FrameCodec)),
201211
data_reader: Some(FramedRead::new(tracing_reader, bm13xx::FrameCodec)),
@@ -205,6 +215,8 @@ impl BitaxeBoard {
205215
stats_task_handle: None,
206216
serial_number,
207217
telemetry_tx: Some(telemetry_tx),
218+
fan_config: FanControllerConfig::default(),
219+
thermal_task_handle: None,
208220
})
209221
}
210222

@@ -451,36 +463,6 @@ impl BitaxeBoard {
451463
}
452464
}
453465

454-
/// Initialize the fan controller
455-
async fn init_fan_controller(&mut self) -> Result<()> {
456-
// Clone the I2C bus for the fan controller
457-
let fan_i2c = self.i2c.clone();
458-
let mut fan = Emc2101::new(fan_i2c);
459-
460-
// Initialize the EMC2101
461-
match fan.init().await {
462-
Ok(()) => {
463-
// Set fan to full speed until closed-loop control is implemented
464-
match fan.set_fan_speed(Percent::FULL).await {
465-
Ok(()) => {
466-
debug!("Fan speed set to 100%");
467-
}
468-
Err(e) => {
469-
warn!("Failed to set fan speed: {}", e);
470-
}
471-
}
472-
473-
self.fan_controller = Some(fan);
474-
Ok(())
475-
}
476-
Err(e) => {
477-
warn!("Failed to initialize EMC2101 fan controller: {}", e);
478-
// Continue without fan control - not critical for operation
479-
Ok(())
480-
}
481-
}
482-
}
483-
484466
/// Generate frequency ramp steps for smooth PLL transitions
485467
///
486468
/// Calculates PLL configurations for each frequency step from start to target.
@@ -620,7 +602,6 @@ impl BitaxeBoard {
620602
.await
621603
.context("failed to set I2C frequency")?;
622604

623-
self.init_fan_controller().await?;
624605
self.init_power_controller().await?;
625606

626607
tokio::time::sleep(Duration::from_millis(500)).await;
@@ -668,8 +649,14 @@ impl BitaxeBoard {
668649
// Put chip back in reset
669650
self.hold_in_reset().await?;
670651

652+
let (thermal_readings_tx, thermal_readings_rx) =
653+
watch::channel::<Option<ThermalReadings>>(None);
654+
655+
// Spawn thermal monitoring task
656+
self.spawn_thermal_task(thermal_readings_tx).await?;
657+
671658
// Spawn statistics monitoring task
672-
self.spawn_stats_monitor();
659+
self.spawn_stats_monitor(thermal_readings_rx);
673660

674661
Ok(())
675662
}
@@ -680,10 +667,10 @@ impl BitaxeBoard {
680667
}
681668

682669
/// Spawn a task to periodically log and publish board telemetry.
683-
fn spawn_stats_monitor(&mut self) {
684-
// Clone data needed for the monitoring task
685-
let i2c = self.i2c.clone();
686-
670+
fn spawn_stats_monitor(
671+
&mut self,
672+
thermal_readings_rx: watch::Receiver<Option<ThermalReadings>>,
673+
) {
687674
// Clone the regulator Arc for stats monitoring
688675
let regulator = self
689676
.regulator
@@ -710,9 +697,6 @@ impl BitaxeBoard {
710697
let mut interval = tokio::time::interval(STATS_INTERVAL);
711698
interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
712699

713-
// Create fan controller for the stats task
714-
let mut fan_ctrl = Emc2101::new(i2c);
715-
716700
const LOG_INTERVAL: Duration = Duration::from_secs(30);
717701
let mut last_log = tokio::time::Instant::now();
718702

@@ -722,11 +706,13 @@ impl BitaxeBoard {
722706
loop {
723707
interval.tick().await;
724708

725-
// -- Read sensor values --
709+
// -- Get latest thermal readings --
726710

727-
let asic_temp = fan_ctrl.get_external_temperature().await.ok();
728-
let fan_percent = fan_ctrl.get_fan_speed().await.ok().map(u8::from);
729-
let fan_rpm = fan_ctrl.get_rpm().await.ok();
711+
let thermal_readings = thermal_readings_rx
712+
.borrow()
713+
.as_ref()
714+
.copied()
715+
.unwrap_or_default();
730716

731717
let (vin_mv, vout_mv, iout_ma, power_mw, vr_temp) = {
732718
let mut reg = regulator.lock().await;
@@ -769,14 +755,16 @@ impl BitaxeBoard {
769755
serial: board_serial.clone(),
770756
fans: vec![Fan {
771757
name: "fan".into(),
772-
rpm: fan_rpm,
773-
percent: fan_percent,
758+
rpm: thermal_readings.fan_rpm,
759+
percent: thermal_readings.fan_percent,
774760
target_percent: None,
775761
}],
776762
temperatures: vec![
777763
TemperatureSensor {
778764
name: "asic".into(),
779-
temperature: asic_temp.map(Temperature::from_celsius),
765+
temperature: thermal_readings
766+
.asic_temp_c
767+
.map(Temperature::from_celsius),
780768
},
781769
TemperatureSensor {
782770
name: "vr".into(),
@@ -807,9 +795,9 @@ impl BitaxeBoard {
807795
info!(
808796
board = %board_model,
809797
serial = ?board_serial,
810-
asic_temp_c = ?asic_temp,
811-
fan_percent = ?fan_percent,
812-
fan_rpm = ?fan_rpm,
798+
asic_temp_c = ?thermal_readings.asic_temp_c,
799+
fan_percent = ?thermal_readings.fan_percent,
800+
fan_rpm = ?thermal_readings.fan_rpm,
813801
vr_temp_c = ?vr_temp,
814802
power_w = ?power_mw.map(|mw| mw as f32 / 1000.0),
815803
current_a = ?iout_ma.map(|ma| ma as f32 / 1000.0),
@@ -823,6 +811,27 @@ impl BitaxeBoard {
823811

824812
self.stats_task_handle = Some(handle);
825813
}
814+
815+
/// Initialize fan hardware and spawn the thermal control loop task.
816+
async fn spawn_thermal_task(
817+
&mut self,
818+
thermal_readings_tx: watch::Sender<Option<ThermalReadings>>,
819+
) -> Result<()> {
820+
let fan_hw = Arc::new(Mutex::new(Emc2101::new(self.i2c.clone())));
821+
822+
let thermal_task =
823+
ThermalTask::new(fan_hw.clone(), self.fan_config.clone(), thermal_readings_tx);
824+
825+
let handle = thermal_task
826+
.start()
827+
.await
828+
.context("failed to start thermal task")?;
829+
830+
self.emc2101 = Some(fan_hw);
831+
self.thermal_task_handle = Some(handle);
832+
833+
Ok(())
834+
}
826835
}
827836

828837
#[async_trait]
@@ -857,12 +866,27 @@ impl Board for BitaxeBoard {
857866
}
858867
}
859868

869+
// Cancel the thermal monitoring task
870+
if let Some(handle) = self.thermal_task_handle.take() {
871+
handle.abort();
872+
}
873+
860874
// Reduce fan speed (no more heat generation)
861-
if let Some(ref mut fan) = self.fan_controller {
862-
let shutdown_speed = Percent::new_clamped(25);
863-
if let Err(e) = fan.set_fan_speed(shutdown_speed).await {
864-
warn!("Failed to set fan speed: {}", e);
865-
}
875+
if let Some(ref emc2101_handle) = self.emc2101 {
876+
match emc2101_handle
877+
.lock()
878+
.await
879+
.set_fan_speed(Percent::new_clamped(
880+
self.fan_config.fan_speed_min_pct.round() as u8,
881+
))
882+
.await
883+
{
884+
Ok(()) => debug!(
885+
"Fan set to {}%",
886+
self.fan_config.fan_speed_min_pct.round() as u8
887+
),
888+
Err(e) => warn!("Failed to set the fan speed: {}", e),
889+
};
866890
}
867891

868892
// Cancel the statistics monitoring task

0 commit comments

Comments
 (0)