Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
372595f
feat(board): add BIRDS board stub with USB detection
johnny9 Feb 15, 2026
1b0fe17
feat(transport): add 9-bit serial encoding for BZM2
johnny9 Feb 15, 2026
7555f16
feat(bzm2): add protocol definitions and codec
johnny9 Feb 16, 2026
af62af3
feat(board): wire BIRDS board to BZM2 hash thread
johnny9 Feb 17, 2026
bfea9d6
feat(bzm2): implement bring-up initialization flow
johnny9 Feb 17, 2026
e93009a
feat(bzm2): add WRITEJOB command encoding
johnny9 Feb 17, 2026
116b7b3
feat(bzm2): enforce enhanced-mode WRITEJOB framing
johnny9 Feb 17, 2026
b1a9d92
feat(bzm2): send rolled-midstate jobs from HashTask
johnny9 Feb 17, 2026
da88e6c
feat(bzm2): map READRESULT responses to shares
johnny9 Feb 17, 2026
23eb1ad
fix(bzm2): harden frame decoding and clarify WRITEJOB API
johnny9 Feb 24, 2026
0ff5db7
feat(hash_thread): implement BZM2 share validation
johnny9 Feb 24, 2026
1ad085e
feat(board): initialize BIRDS data port over BZM2
johnny9 Mar 6, 2026
f40971d
refactor(bzm2): simplify hash thread internals
johnny9 Mar 6, 2026
4ffece7
refactor(bzm2): remove verbose hash thread diagnostics
johnny9 Mar 6, 2026
84cb51f
refactor(bzm2): extract hash thread hashing helpers
johnny9 Mar 6, 2026
0004077
refactor(bzm2): extract hash thread work helpers
johnny9 Mar 6, 2026
edf90c9
refactor(bzm2): extract hash thread assignment tracker
johnny9 Mar 6, 2026
41803eb
refactor(bzm2): extract hash thread bring-up flow
johnny9 Mar 6, 2026
d3097dd
refactor(bzm2): reuse bitcoin SHA-256 engine
johnny9 Mar 6, 2026
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
40 changes: 40 additions & 0 deletions mujina-miner/src/asic/bzm2/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
//! Error types for BZM2 protocol operations.

use thiserror::Error;

/// Validation failures detected while encoding or decoding BZM2 frames.
#[derive(Error, Debug)]
pub enum ProtocolError {
/// A register write command was constructed without any payload bytes.
#[error("register write payload cannot be empty")]
EmptyWritePayload,

/// A register write payload exceeded the 8-bit on-wire length field.
#[error("register write payload too large: {0} bytes")]
WritePayloadTooLarge(usize),

/// READREG only supports 1-, 2-, or 4-byte responses.
#[error("invalid read register byte count: {0} (expected 1, 2, or 4)")]
InvalidReadRegCount(u8),

/// WRITEJOB only accepts `job_ctl` values that the hardware understands.
#[error("invalid job control value: {0} (expected 1 or 3)")]
InvalidJobControl(u8),

/// The codec was asked to decode a READREG response size it does not
/// implement.
#[error("unsupported read register response size: {0} (expected 1 or 4)")]
UnsupportedReadRegResponseSize(usize),

/// A frame exceeded what the bridge format can encode in one command.
#[error("frame too large to encode: {0} bytes")]
FrameTooLarge(usize),

/// A NOOP response did not return the expected `2ZB` signature bytes.
#[error("invalid NOOP signature: {0:02x?}")]
InvalidNoopSignature([u8; 3]),

/// The decoder saw a response opcode that is not currently supported.
#[error("unsupported response opcode: 0x{0:02x}")]
UnsupportedResponseOpcode(u8),
}
223 changes: 223 additions & 0 deletions mujina-miner/src/asic/bzm2/init.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
//! BZM2 data-port initialization helpers.
//!
//! This module performs the board-time transport probe that happens before the
//! hashing thread takes ownership of the UART. Initialization here uses the
//! real protocol codec and returns a ready-to-use framed transport on success.

use anyhow::{Context, Result, anyhow, bail};
use futures::SinkExt;
use tokio::io::AsyncReadExt;
use tokio::time::{self, Duration};
use tokio_stream::StreamExt;
use tokio_util::codec::{FramedRead, FramedWrite};

use super::{
Bzm2Protocol, FrameCodec, HexBytes, ReadRegData, Response,
protocol::{DEFAULT_ASIC_ID, NOOP_STRING},
};
use crate::transport::serial::{SerialControl, SerialReader, SerialStream, SerialWriter};

/// Default BZM2 UART baud rate used by the BIRDS data port.
pub const DEFAULT_BZM2_DATA_BAUD: u32 = 5_000_000;

/// Default timeout for each initialization request/response step.
pub const DEFAULT_IO_TIMEOUT: Duration = Duration::from_secs(2);

/// Result of probing one ASIC during board initialization.
#[derive(Debug, Clone, Copy)]
pub struct ProbeResult {
/// Logical ASIC index that was probed.
pub logical_asic: u8,
/// Hardware UART ID observed on the response path.
pub asic_hw_id: u8,
/// Raw `ASIC_ID` register value returned by the chip.
pub asic_id: u32,
}

/// Framed BZM2 data-port transport that has already passed initialization.
pub struct InitializedDataPort {
/// Probe metadata collected during initialization.
pub probe: ProbeResult,
/// Decoded response stream for subsequent hashing logic.
pub reader: FramedRead<SerialReader, FrameCodec>,
/// Encoded command sink for subsequent hashing logic.
pub writer: FramedWrite<SerialWriter, FrameCodec>,
/// Control handle associated with the serial data port.
pub control: SerialControl,
}

fn expect_noop_response(response: Response) -> Result<u8> {
match response {
Response::Noop {
asic_hw_id,
signature,
} if signature == *NOOP_STRING => Ok(asic_hw_id),
Response::Noop { signature, .. } => {
bail!("NOOP signature mismatch: got {:02x?}", signature)
}
other => bail!("expected NOOP response, got {:?}", other),
}
}

fn expect_asic_id_response(expected_asic_hw_id: u8, response: Response) -> Result<u32> {
match response {
Response::ReadReg {
asic_hw_id,
data: ReadRegData::U32(asic_id),
} if asic_hw_id == expected_asic_hw_id => Ok(asic_id),
Response::ReadReg { asic_hw_id, data } => bail!(
"READREG(ASIC_ID) response mismatch: expected ASIC 0x{expected_asic_hw_id:02X}, got ASIC 0x{asic_hw_id:02X} with payload {:?}",
data
),
other => bail!("expected READREG(ASIC_ID) response, got {:?}", other),
}
}

async fn next_response(
reader: &mut FramedRead<SerialReader, FrameCodec>,
timeout: Duration,
context: &str,
) -> Result<Response> {
let response = time::timeout(timeout, reader.next())
.await
.with_context(|| format!("timeout waiting for {context}"))?
.transpose()
.with_context(|| format!("read error while waiting for {context}"))?
.ok_or_else(|| anyhow!("BZM2 response stream closed while waiting for {context}"))?;
Ok(response)
}

/// Open, probe, and return an initialized BZM2 data port using default
/// transport settings.
pub async fn initialize_data_port(
serial_port: &str,
logical_asic: u8,
) -> Result<InitializedDataPort> {
initialize_data_port_with_options(
serial_port,
logical_asic,
DEFAULT_BZM2_DATA_BAUD,
DEFAULT_IO_TIMEOUT,
)
.await
}

/// Open, probe, and return an initialized BZM2 data port using explicit
/// transport settings.
pub async fn initialize_data_port_with_options(
serial_port: &str,
logical_asic: u8,
baud: u32,
timeout: Duration,
) -> Result<InitializedDataPort> {
let protocol = Bzm2Protocol::new();
let serial = SerialStream::new(serial_port, baud)
.with_context(|| format!("failed to open serial port {}", serial_port))?;
let (mut raw_reader, writer, control) = serial.split();

// Reset/power-up can leave transient bytes on the data UART. Drain any
// pending bytes before issuing the first command.
drain_input_noise(&mut raw_reader).await;

let mut reader = FramedRead::new(raw_reader, FrameCodec::default());
let mut writer = FramedWrite::new(writer, FrameCodec::default());

writer
.send(protocol.noop(DEFAULT_ASIC_ID))
.await
.context("failed to send NOOP")?;
let noop_response = next_response(&mut reader, timeout, "NOOP response").await?;
let asic_hw_id = expect_noop_response(noop_response)?;

writer
.send(protocol.read_asic_id(asic_hw_id))
.await
.context("failed to send READREG(ASIC_ID)")?;
let asic_id_response = next_response(&mut reader, timeout, "READREG(ASIC_ID) response").await?;
let asic_id = expect_asic_id_response(asic_hw_id, asic_id_response)?;

Ok(InitializedDataPort {
probe: ProbeResult {
logical_asic,
asic_hw_id,
asic_id,
},
reader,
writer,
control,
})
}

async fn drain_input_noise(reader: &mut SerialReader) {
let mut scratch = [0u8; 256];
loop {
match time::timeout(Duration::from_millis(20), reader.read(&mut scratch)).await {
Ok(Ok(0)) => break,
Ok(Ok(n)) => {
tracing::debug!(
bytes = n,
rx = %HexBytes(&scratch[..n]),
"BZM2 init drained residual input"
);
continue;
}
Ok(Err(_)) => break,
Err(_elapsed) => break,
}
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_expect_noop_response_accepts_expected_signature() {
let asic_hw_id = expect_noop_response(Response::Noop {
asic_hw_id: DEFAULT_ASIC_ID,
signature: *NOOP_STRING,
})
.unwrap();
assert_eq!(asic_hw_id, DEFAULT_ASIC_ID);
}

#[test]
fn test_expect_noop_response_rejects_non_noop_response() {
let error = expect_noop_response(Response::ReadReg {
asic_hw_id: DEFAULT_ASIC_ID,
data: ReadRegData::U32(0x1234_5678),
})
.expect_err("non-NOOP response must fail");
assert!(error.to_string().contains("expected NOOP response"));
}

#[test]
fn test_expect_asic_id_response_accepts_matching_u32_payload() {
let asic_id = expect_asic_id_response(
DEFAULT_ASIC_ID,
Response::ReadReg {
asic_hw_id: DEFAULT_ASIC_ID,
data: ReadRegData::U32(0x1234_5678),
},
)
.unwrap();
assert_eq!(asic_id, 0x1234_5678);
}

#[test]
fn test_expect_asic_id_response_rejects_mismatched_payload_type() {
let error = expect_asic_id_response(
DEFAULT_ASIC_ID,
Response::ReadReg {
asic_hw_id: DEFAULT_ASIC_ID,
data: ReadRegData::U8(0x12),
},
)
.expect_err("unexpected payload type must fail");
assert!(
error
.to_string()
.contains("READREG(ASIC_ID) response mismatch")
);
}
}
37 changes: 37 additions & 0 deletions mujina-miner/src/asic/bzm2/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
//! BZM2 ASIC family support.
//!
//! The BZM2 implementation is split into focused modules:
//! - [`protocol`] owns wire-format types and the Tokio codec.
//! - [`thread`] owns the `HashThread` actor and chip bring-up sequence.
//! - [`init`] owns board-time transport probing before the hash thread takes
//! over the UART.
//! - [`error`] contains protocol-specific validation errors.
//!
//! BIRDS boards use this module for both board-time initialization and
//! production hashing. Keeping the low-level helpers centralized avoids board
//! code having to duplicate protocol details.

use std::fmt;

/// Wrapper for formatting byte slices as space-separated uppercase hex.
pub(crate) struct HexBytes<'a>(pub(crate) &'a [u8]);

impl fmt::Display for HexBytes<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for (i, byte) in self.0.iter().enumerate() {
if i > 0 {
write!(f, " ")?;
}
write!(f, "{:02X}", byte)?;
}
Ok(())
}
}

pub mod error;
pub mod init;
pub mod protocol;
pub mod thread;

pub use error::ProtocolError;
pub use protocol::{Bzm2Protocol, Command, FrameCodec, Opcode, ReadRegData, Response};
Loading