Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion mujina-hb2.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ write_register = 17
enable_gpio = 437

[hardware.amlogic_control_board.startup]
default_fan_percent = 50
default_fan_percent = 100
initial_voltage = 12.0
psu_settle_ms = 2000
reset_assert_ms = 100
Expand Down
8 changes: 7 additions & 1 deletion mujina-miner/src/api/v0.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,13 @@ async fn patch_miner(
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
// Result layers: timeout / channel-closed / command-error.
let Ok(Ok(Ok(()))) = tokio::time::timeout(Duration::from_secs(5), rx).await else {
// 5 minutes covers a full ResumeMining round-trip: the scheduler
// calls assign_job_to_threads -> handle_work_assignment ->
// initialize_chips on every hash thread, which includes a 500ms
// power-on wait, full enumeration, register config, and the
// BM1366 frequency ramp (typically ~2 min end-to-end). PauseMining
// is fast (just disable_chips) so this bound only matters on resume.
let Ok(Ok(Ok(()))) = tokio::time::timeout(Duration::from_secs(300), rx).await else {
return Err(StatusCode::INTERNAL_SERVER_ERROR);
};
}
Expand Down
146 changes: 146 additions & 0 deletions mujina-miner/src/asic/bm13xx/chain_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,19 @@ pub struct ChainConfig {

/// Hardware control interfaces for this chain.
pub peripherals: ChainPeripherals,

/// After the broadcast register-config phase completes, the hash
/// thread sends a UartBaud broadcast to switch the chips to this
/// baud rate, then calls [`ChainPeripherals::set_chip_uart_baud`]
/// to switch the controller side. Per-chip writes and steady-state
/// mining traffic then run at this rate.
///
/// `None` keeps the link at the rate the SerialStream was opened
/// with (default 115200). Set this to a higher rate (e.g.
/// `BaudRate::Baud3M`) on boards with long chains where 115200 is
/// the chain throughput bottleneck — confirmed for BHB56902 via the
/// LuxOS capture (see `captures/luxos-bhb56902-findings.md`).
pub post_broadcast_chip_baud: Option<super::protocol::BaudRate>,
}

/// Hardware interfaces for a chain.
Expand All @@ -75,6 +88,137 @@ pub struct ChainPeripherals {

/// Voltage regulator control (optional, may be shared across chains).
pub voltage_regulator: Option<Arc<Mutex<dyn VoltageRegulator + Send>>>,

/// Optional handle for retuning the controller-side chip UART baud
/// rate mid-init. Boards that own the [`SerialStream`] for the chip
/// channel can wrap its `SerialControl` here so the hash thread can
/// switch to a higher rate after sending the chip-side
/// [`UartBaud`](super::protocol::Register::UartBaud) broadcast.
pub chip_uart_baud: Option<Arc<Mutex<dyn ChipUartBaudControl + Send>>>,

/// Optional cross-chain ramp coordinator. When multiple chains
/// share a single voltage rail (e.g. all three hashboards on an
/// S19k Pro share the APW12), independent per-actor voltage
/// commands would race on the shared mutex and cause the rail to
/// oscillate between chains' step values. Setting this on every
/// chain in the cohort makes them rendezvous at each ramp step
/// and elect one leader per step to drive the shared PSU.
/// `None` keeps the legacy single-chain behaviour where each
/// actor commands its own voltage every step.
pub ramp_coordinator: Option<Arc<ChainCoordinator>>,
}

/// Cross-chain coordinator for shared-rail boards.
///
/// All chains in the cohort call [`ChainCoordinator::sync_voltage_step`]
/// at the top of every frequency-ramp step. The call rendezvous on a
/// barrier (so the slowest chain holds the rest), the elected leader
/// drives the shared voltage regulator on behalf of everyone, and all
/// chains then wait the same `voltage_settle` before returning. After
/// the call returns each chain sends its per-chain PLL command at the
/// same effective voltage.
pub struct ChainCoordinator {
barrier: tokio::sync::Barrier,
}

impl ChainCoordinator {
/// Create a coordinator for a cohort of `chain_count` chains.
pub fn new(chain_count: usize) -> Self {
Self {
barrier: tokio::sync::Barrier::new(chain_count),
}
}

/// Rendezvous all chains at this step, command the shared voltage
/// from the elected leader, then settle.
///
/// `voltage_v`/`voltage_settle` are computed identically by every
/// chain (same step index, same chip target, same domain count),
/// so it's safe for only the leader to actually drive the rail.
/// Returns the wait result so callers can short-circuit logging
/// if only the leader should log.
pub async fn sync_voltage_step(
&self,
regulator: &Arc<Mutex<dyn VoltageRegulator + Send>>,
voltage_v: f32,
voltage_settle: std::time::Duration,
) -> Result<bool, anyhow::Error> {
let result = self.barrier.wait().await;
let is_leader = result.is_leader();
if is_leader {
regulator
.lock()
.await
.set_voltage(voltage_v)
.await
.map_err(|e| {
anyhow::anyhow!("ramp-coord leader set_voltage({voltage_v}V): {e}")
})?;
}
tokio::time::sleep(voltage_settle).await;
Ok(is_leader)
}
}

/// Boxed type for the chip → controller response stream.
pub type ChipRxStream = std::pin::Pin<
Box<
dyn futures::Stream<Item = Result<super::protocol::Response, std::io::Error>>
+ Send
+ 'static,
>,
>;

/// Boxed type for the controller → chip command sink.
pub type ChipTxSink = std::pin::Pin<
Box<
dyn futures::Sink<super::protocol::Command, Error = std::io::Error>
+ Send
+ 'static,
>,
>;

/// Controller-side handle for changing the chip-channel UART baud rate.
///
/// Implementations wrap a [`SerialControl`](crate::transport::serial::SerialControl)
/// or equivalent so the hash thread can drive a mid-init baud bump.
///
/// The recommended pattern on the Amlogic `meson_uart` driver is the
/// "close + reopen" approach (matching LuxOS — see
/// `captures/luxos-bhb56902-findings.md` for the 3 separate `open64()`
/// calls observed during init): drop the current `SerialStream`, open
/// a fresh one at the new baud rate, and return a new pair of typed
/// channels. The `tcsetattr(Drain)` baud-switch path drops chips at
/// 3 Mbaud on BHB56902.
#[async_trait::async_trait]
pub trait ChipUartBaudControl {
/// Pre-open a second `/dev/ttyS2` handle at `current_baud_rate`
/// (matching the current kernel termios so the hardware baud
/// doesn't change yet) and return fresh I/O channels backed by
/// that handle. The implementation must keep the new handle's
/// control side alive internally so it can be retuned in
/// `finalize_baud_switch`.
///
/// This is the open-without-bumping side of the two-phase LuxOS
/// baud-switch pattern: two fds end up open on the same device,
/// the old writer carries the chip-side `UartBaud` broadcast at
/// the current rate, and only afterward does the new fd get
/// retuned to the target rate.
async fn prepare_new_stream(
&mut self,
current_baud_rate: u32,
) -> anyhow::Result<(ChipRxStream, ChipTxSink)>;

/// Apply `target_baud_rate` to the stream prepared by the last
/// `prepare_new_stream` call. Per Linux `termios` semantics, this
/// also retunes the underlying device — so the hash thread must
/// have already finished its broadcast on the OLD writer and
/// waited for chips to apply their internal baud change before
/// calling this.
async fn finalize_baud_switch(
&mut self,
target_baud_rate: u32,
) -> anyhow::Result<()>;
}

#[cfg(test)]
Expand Down Expand Up @@ -111,6 +255,8 @@ mod tests {
let peripherals = ChainPeripherals {
asic_enable: enable,
voltage_regulator: None,
chip_uart_baud: None,
ramp_coordinator: None,
};

// Hash thread enables
Expand Down
Loading