Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
5cdcfb9
docs: codify doc comment prose conventions
rkuester Jul 2, 2026
212e9f0
refactor(bm13xx): rename ChipType to ChipModel
rkuester Apr 22, 2026
b62ead7
feat(types): add unit-aware Frequency type
rkuester Apr 22, 2026
4683e20
refactor(bm13xx): move PLL calculation to ChipConfig
rkuester Apr 22, 2026
fc9c0b6
fix(tps546): drop CP bit from ON_OFF_CONFIG
rkuester Apr 22, 2026
c57fdc1
fix(tps546): use family name in doc comments and logs
rkuester Apr 22, 2026
454a4c4
refactor(tps546): add PMBus voltage control primitives
rkuester Apr 22, 2026
3c3160e
refactor(bitaxe): use TPS546 primitives directly
rkuester Apr 22, 2026
892e2f4
refactor(tps546): remove obsolete set_vout helper
rkuester Apr 22, 2026
7d6ad3b
fix(bm13xx): correct JobFull length byte to match factory firmware
rkuester Apr 23, 2026
942afa3
test(bm13xx): drop duplicate job_full_encoding test
rkuester Apr 23, 2026
412528a
refactor(bm13xx): split Command by frame family
rkuester Apr 23, 2026
5a1364d
refactor(bm13xx): unify register value type names
rkuester Apr 23, 2026
a63f4b6
refactor(bm13xx): unify Register variant shapes
rkuester Apr 24, 2026
8fc9d82
refactor(bm13xx): tighten Sink Error bound in hash thread
rkuester Apr 24, 2026
eef21c5
refactor(bm13xx): introduce Destination for register commands
rkuester Apr 24, 2026
c0bb360
refactor(bm13xx): wrap each command variant in its own struct
rkuester Apr 24, 2026
2aa516b
refactor(bm13xx): use a struct for command flags
rkuester Apr 25, 2026
c8d9b47
refactor(bm13xx): split protocol into submodules
rkuester Apr 25, 2026
c53937b
refactor(bm13xx): delete BM13xxProtocol
rkuester May 3, 2026
a854ea1
fix(bm13xx): correct domain-boundary drive strength
rkuester Jul 3, 2026
e5b0e35
test(bm13xx): round-trip each register value type
rkuester May 3, 2026
1627a63
refactor(bm13xx): collapse duplicated sink bounds
rkuester May 3, 2026
a282c16
refactor(bm13xx): name PLL VCO flag values
rkuester May 3, 2026
3193051
feat(bm13xx): add chain and topology domain model
rkuester May 3, 2026
ef85839
refactor(bm13xx): type 0xA8 as SoftResetControl
rkuester Jul 3, 2026
0a14991
refactor(bm13xx): type 0xA4 as MidstateConfig
rkuester Jul 3, 2026
cd4b601
refactor(bm13xx): type 0x3C as CoreMailbox
rkuester Jul 4, 2026
f382bc3
refactor(bm13xx): type 0x54 as AnalogMux
rkuester Jul 4, 2026
a8343ed
refactor(bm13xx): type 0x18 as MiscControl
rkuester Jul 4, 2026
921d012
refactor(bm13xx): type 0x2C as UartRelay
rkuester Jul 4, 2026
f4906a7
refactor(bm13xx): close ChipModel over supported models
rkuester Jul 4, 2026
af00678
feat(bm13xx): parameterize codec by chip model
rkuester Jul 4, 2026
0307d33
refactor(bm13xx): wrap each response variant in its own struct
rkuester Jul 4, 2026
cc035ff
feat(bm13xx): add reader task demuxing chip responses
rkuester Jul 4, 2026
5af9309
feat(bm13xx): add driver for register conversations
rkuester Jul 4, 2026
f217009
refactor(bm13xx): type 0x10 as HashCountingNumber
rkuester Jul 5, 2026
a1ca748
WIP docs(bm13xx): rewrite search space distribution
rkuester Jul 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
32 changes: 32 additions & 0 deletions CODE_STYLE.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,38 @@ Use standard Rust documentation format:
- Include examples for complex functionality
- Document panics, errors, and safety requirements

Doc comment prose follows Rust's API documentation conventions
([RFC 1574], building on [RFC 505]), the style used throughout the
standard library. The basics:

- Begin with a one-line summary. rustdoc shows this line by itself
in module indexes, so it must stand alone.
- End the summary with a period. This includes noun-phrase
summaries for types and fields: `/// Pool connection
configuration.`
- Describe functions and methods in third-person present tense:
`/// Returns the length in bytes.`, not `/// Return the length
in bytes`. A doc comment describes what the item does; it is not
a command to the reader.
- Separate the summary from any further detail with a blank `///`
line.

```rust
/// Creates a frequency from a count of megahertz.
///
/// Values are stored internally as integer Hz, so fractional
/// megahertz such as 62.5 are exact.
pub fn from_mhz(mhz: f32) -> Self {
```

Older code predates this rule and mixes imperative, period-less
summaries. Bring doc comments into conformance when you modify
them; do not restyle otherwise-untouched code in a functional
commit.

[RFC 505]: https://rust-lang.github.io/rfcs/0505-api-comment-conventions.html
[RFC 1574]: https://rust-lang.github.io/rfcs/1574-more-api-documentation-conventions.html

See CODING_GUIDELINES.md for guidance on what to document and comment
style.

Expand Down
933 changes: 777 additions & 156 deletions mujina-miner/src/asic/bm13xx/PROTOCOL.md

Large diffs are not rendered by default.

332 changes: 332 additions & 0 deletions mujina-miner/src/asic/bm13xx/chain.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,332 @@
//! Live runtime model of the chip serial chain, built from a TopologySpec.
//!
//! TopologySpec is the static blueprint; Chain is what the miner actively
//! tracks during a session. It owns the Chip and Domain instances, holds
//! the serial addresses assigned during enumeration, and carries per-chip
//! runtime stats that mutate as work flows.
//!
//! Chain order and domain grouping are orthogonal. Chain order is how
//! chips are wired for serial communication; domains group chips that
//! share voltage rails. A domain's chips may not be contiguous in chain
//! order.

use std::time::Instant;

use crate::types::HashRate;

use super::topology::TopologySpec;

/// The complete chain model.
///
/// Owns all chips and domains. Structure reflects physical wiring; state
/// reflects current operation.
#[derive(Debug, Clone)]
pub struct Chain {
chips: Vec<Chip>,
domains: Vec<Domain>,
}

impl Chain {
/// Builds the chain structure from a topology specification.
///
/// Creates chips and domains based on the topology. Addresses are not
/// yet assigned; call `assign_addresses()` after construction.
pub fn from_topology(spec: &TopologySpec) -> Self {
let chip_count = spec.expected_chip_count();

let chips: Vec<Chip> = (0..chip_count)
.map(|i| Chip {
address: 0,
domain: spec.domain_for(ChipId(i)),
stats: ChipStats::default(),
})
.collect();

let domain_count = spec.domain_count();
let mut domains: Vec<Domain> = (0..domain_count)
.map(|i| Domain {
id: DomainId(i),
chips: Vec::new(),
})
.collect();

for (i, chip) in chips.iter().enumerate() {
domains[chip.domain.index()].chips.push(ChipId(i));
}

Self { chips, domains }
}

/// Assigns chip addresses with the standard interval of 2.
///
/// Interval 2 is the convention observed in firmware captures:
/// - S21 Pro (65 chips): addresses 0x00, 0x02, ... 0x80
/// - S19 J Pro (126 chips): addresses 0x00, 0x02, ... 0xFA
///
/// Returns error if chip count exceeds address space (max 128 chips).
pub fn assign_addresses(&mut self) -> Result<(), AddressOverflowError> {
self.assign_addresses_with_interval(2)
}

/// Assigns addresses with an explicit interval for unusual boards.
pub fn assign_addresses_with_interval(
&mut self,
interval: u8,
) -> Result<(), AddressOverflowError> {
if self.chips.is_empty() {
return Ok(());
}

let last_address = (self.chips.len() - 1) * interval as usize;
if last_address > 0xFF {
return Err(AddressOverflowError {
chip_count: self.chips.len(),
interval,
});
}

for (i, chip) in self.chips.iter_mut().enumerate() {
chip.address = (i * interval as usize) as u8;
}

Ok(())
}

pub fn chip(&self, id: ChipId) -> &Chip {
&self.chips[id.index()]
}

pub fn chip_mut(&mut self, id: ChipId) -> &mut Chip {
&mut self.chips[id.index()]
}

pub fn domain(&self, id: DomainId) -> &Domain {
&self.domains[id.index()]
}

/// Iterates over all chips in chain order.
pub fn chips(&self) -> impl Iterator<Item = (ChipId, &Chip)> {
self.chips.iter().enumerate().map(|(i, c)| (ChipId(i), c))
}

/// Iterates over all domains.
pub fn domains(&self) -> impl Iterator<Item = &Domain> {
self.domains.iter()
}

/// Iterates over domains from far end toward host (for domain
/// configuration).
pub fn domains_far_to_near(&self) -> impl Iterator<Item = &Domain> {
self.domains.iter().rev()
}

/// Returns the first chip of the domain in chain order.
pub fn domain_first(&self, id: DomainId) -> ChipId {
*self.domain(id).chips.first().expect("domain has no chips")
}

/// Returns the last chip of the domain in chain order.
pub fn domain_last(&self, id: DomainId) -> ChipId {
*self.domain(id).chips.last().expect("domain has no chips")
}

pub fn chip_count(&self) -> usize {
self.chips.len()
}

pub fn domain_count(&self) -> usize {
self.domains.len()
}
}

/// Individual BM13xx ASIC chip.
#[derive(Debug, Clone)]
pub struct Chip {
/// Serial address (assigned during enumeration)
pub address: u8,
/// Which voltage domain this chip belongs to
pub domain: DomainId,
/// Runtime statistics
pub stats: ChipStats,
}

/// A voltage domain: a group of chips sharing power rails.
#[derive(Debug, Clone)]
pub struct Domain {
pub id: DomainId,
/// Chips in this domain, ordered by chain position
pub chips: Vec<ChipId>,
}

/// Runtime statistics for a single chip.
#[derive(Debug, Clone, Default)]
pub struct ChipStats {
pub shares_found: u64,
pub last_share_time: Option<Instant>,
pub estimated_hashrate: HashRate,
pub healthy: bool,
}

/// Type-safe index for chips in the chain.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ChipId(pub usize);

impl ChipId {
pub fn index(self) -> usize {
self.0
}
}

/// Type-safe index for voltage domains.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct DomainId(pub usize);

impl DomainId {
pub fn index(self) -> usize {
self.0
}
}

/// Error when chip addresses would overflow the 8-bit address space.
#[derive(Debug, Clone)]
pub struct AddressOverflowError {
pub chip_count: usize,
pub interval: u8,
}

impl std::fmt::Display for AddressOverflowError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"address overflow: {} chips with interval {} exceeds 0xFF",
self.chip_count, self.interval
)
}
}

impl std::error::Error for AddressOverflowError {}

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

/// Hardware constants from protocol captures.
mod fixtures {
/// Antminer S21 Pro: 65 BM1370 chips in 13 voltage domains.
pub mod s21_pro {
pub const CHIP_COUNT: usize = 65;
pub const DOMAIN_COUNT: usize = 13;
pub const CHIPS_PER_DOMAIN: usize = 5;
pub const FIRST_CHIP_ADDRESS: u8 = 0x00;
pub const LAST_CHIP_ADDRESS: u8 = 0x80; // (65-1) * 2
/// Last chip address in each domain (chips 4, 9, 14, ... 64).
pub const DOMAIN_END_ADDRESSES: [u8; 13] = [
0x08, 0x12, 0x1C, 0x26, 0x30, 0x3A, 0x44, 0x4E, 0x58, 0x62, 0x6C, 0x76, 0x80,
];
}

/// Antminer S19 J Pro: 126 BM1362 chips, single domain.
pub mod s19_jpro {
pub const CHIP_COUNT: usize = 126;
pub const FIRST_CHIP_ADDRESS: u8 = 0x00;
pub const LAST_CHIP_ADDRESS: u8 = 0xFA; // (126-1) * 2
}

/// Bitaxe Gamma: single BM1370 chip.
pub mod bitaxe {
pub const CHIP_COUNT: usize = 1;
pub const CHIP_ADDRESS: u8 = 0x00;
}
}

#[test]
fn s21_pro_addresses() {
use fixtures::s21_pro::*;

let topology = TopologySpec::uniform_domains(DOMAIN_COUNT, CHIPS_PER_DOMAIN, true);
let mut chain = Chain::from_topology(&topology);
chain.assign_addresses().unwrap();

assert_eq!(chain.chip_count(), CHIP_COUNT);
assert_eq!(chain.chip(ChipId(0)).address, FIRST_CHIP_ADDRESS);
assert_eq!(
chain.chip(ChipId(CHIP_COUNT - 1)).address,
LAST_CHIP_ADDRESS
);
}

#[test]
fn s21_pro_domain_ends() {
use fixtures::s21_pro::*;

let topology = TopologySpec::uniform_domains(DOMAIN_COUNT, CHIPS_PER_DOMAIN, true);
let mut chain = Chain::from_topology(&topology);
chain.assign_addresses().unwrap();

let actual: Vec<u8> = (0..DOMAIN_COUNT)
.map(|d| chain.chip(chain.domain_last(DomainId(d))).address)
.collect();

assert_eq!(actual.as_slice(), DOMAIN_END_ADDRESSES);
}

#[test]
fn s19_jpro_addresses() {
use fixtures::s19_jpro::*;

let topology = TopologySpec::single_domain(CHIP_COUNT);
let mut chain = Chain::from_topology(&topology);
chain.assign_addresses().unwrap();

assert_eq!(chain.chip(ChipId(0)).address, FIRST_CHIP_ADDRESS);
assert_eq!(
chain.chip(ChipId(CHIP_COUNT - 1)).address,
LAST_CHIP_ADDRESS
);
}

#[test]
fn bitaxe_single_chip() {
use fixtures::bitaxe::*;

let topology = TopologySpec::single_domain(CHIP_COUNT);
let mut chain = Chain::from_topology(&topology);
chain.assign_addresses().unwrap();

assert_eq!(chain.chip_count(), 1);
assert_eq!(chain.chip(ChipId(0)).address, CHIP_ADDRESS);
}

#[test]
fn address_overflow_detected() {
// 129 chips * interval 2 = last address 256, which overflows u8
let topology = TopologySpec::single_domain(129);
let mut chain = Chain::from_topology(&topology);

let result = chain.assign_addresses();
assert!(result.is_err());
}

#[test]
fn domain_chips_in_chain_order() {
// With contiguous domains, chips 0-4 are domain 0, 5-9 are domain 1, etc.
let topology = TopologySpec::uniform_domains(3, 5, true);
let chain = Chain::from_topology(&topology);

let domain0_chips: Vec<usize> = chain
.domain(DomainId(0))
.chips
.iter()
.map(|c| c.index())
.collect();
assert_eq!(domain0_chips, vec![0, 1, 2, 3, 4]);

let domain1_chips: Vec<usize> = chain
.domain(DomainId(1))
.chips
.iter()
.map(|c| c.index())
.collect();
assert_eq!(domain1_chips, vec![5, 6, 7, 8, 9]);
}
}
Loading
Loading