feat(bm13xx): multi-chip groundwork for EmberOne00 and Bitmain chains#77
Closed
rkuester wants to merge 38 commits into
Closed
feat(bm13xx): multi-chip groundwork for EmberOne00 and Bitmain chains#77rkuester wants to merge 38 commits into
rkuester wants to merge 38 commits into
Conversation
The written style rules cover doc comment mechanics but never specified mood or punctuation for summary lines, and the tree has drifted into a mix of imperative and third-person summaries, with and without terminal periods. Adopt Rust's API documentation conventions (RFC 505 and RFC 1574): a summary is a complete sentence in third-person present tense, ending with a period. The essentials are summarized inline so a reader gets the basics without chasing the RFCs.
The BM1362/BM1366/BM1370/... values in this enum are chip models, not types in any broader sense. Rename the enum to ChipModel and the Register::ChipId.chip_type field to model for consistency. Pure mechanical rename; no behavior change.
Introduce a Frequency type storing Hz as u64 internally, following std::time::Duration's pattern. Provides from_hz, from_khz, from_mhz constructors and hz, khz, mhz accessors.
Introduce a ChipConfig carrying per-chip-model defaults (model, frequency range, PLL search bounds) and factory functions for BM1362 and BM1370. calculate_pll becomes a ChipConfig method driven by the chip's PllParams. When multiple PLL configurations yield the same output frequency, prefer the one with the lowest VCO. S19 J Pro serial captures show VCO stays low during the ramp, whereas the prior first-match approach drifted higher. A ramp-walk test asserts our VCO is never higher than the captured VCO at any step. Remove the old protocol-level frequency struct and its out-of-range error; the PLL search now takes its place, rejecting targets outside the model's frequency range and reporting failure instead of silently falling back to a default divider configuration. Bitaxe's embedded PLL calculator is untouched here and migrates to ChipConfig later.
With CP set, the regulator gates output on both the CONTROL pin and the OPERATION command; noise on the CONTROL pin can glitch the regulator off. Without CP, only the software OPERATION command drives on/off.
The driver resolves both D24A and D24S variants at runtime via device ID, so labeling it as a D24A-specific driver is misleading. Refer to the TPS546 family consistently in the module doc, struct doc, and log messages.
Add three methods that map directly to individual PMBus operations: - set_vout_target: writes VOUT_COMMAND with range validation - enable_output: writes OPERATION = ON and fails unless it reads back - disable_output: writes OPERATION = OFF (immediate) and fails unless it reads back These let callers compose exactly the operations they need: board init calls set_vout_target then clear_faults then enable_output; shutdown calls disable_output; voltage-frequency ramping calls set_vout_target alone. The existing set_vout method stays; callers migrate next.
Init now calls set_vout_target then clear_faults then enable_output in sequence; shutdown calls disable_output. The monolithic set_vout helper cleared faults implicitly and only logged the result of its post-enable readbacks; the primitives now confirm their own OPERATION writes and fail init when the enable does not take. The old status-word diagnostics are gone: a one-shot status peek during soft-start is ambiguous, and continuous power-state monitoring is future work.
The monolithic set_vout method has no callers after Bitaxe migrated to composing set_vout_target, clear_faults, and enable_output directly. Drop it.
Emit 54 (0x36) instead of 86 for the JobFull length byte. Factory firmware captures use 54 on both BM1362 (S19 J Pro) and BM1370 (S21 Pro), and BM1362 EmberOne hardware has mined successfully with 54 in prior prototypes. esp-miner on Bitaxe sends 86 instead; BM1370 seems to tolerate both. Record a structural hypothesis for this specific value in an inline comment. In the tests that compared full-frame bytes against esp-miner captures, compare only the job-data range (bytes 4..86), excluding the length byte and the trailing CRC16, which intentionally diverge. Add a job frame captured from S19 J Pro factory firmware as a fixture, and assert that encoding its fields reproduces the captured frame down to the length byte and CRC16.
Drop job_full_encoding_matches_hardware_capture, a longtime duplicate of job_full_matches_esp_miner_capture.
The wire protocol has two frame families with different framing rules: TYPE=2 register ops use CRC5 with a self-describing length byte, and TYPE=1 jobs use CRC16 with a fixed conventional length. One Command enum covered both, forcing the encoder to re-inspect the variant at runtime to pick the right CRC. Give each family its own enum and FrameCodec an Encoder impl per type, so CRC selection becomes compile-time dispatch. The hash thread still drives register ops and jobs through a single underlying sink because Framed satisfies Sink<RegisterCommand> and Sink<JobCommand> simultaneously.
Some register value types already named themselves after the register they fill (TicketMask, VersionMask, IoDriverStrength). Rename PllConfig, NonceRangeConfig, and BaudRate to follow the same convention.
Register had three variant shapes: struct variants with inline
fields, tuple variants wrapping typed values, and variants
carrying a bare { raw_value: u32 } field. That inconsistency
forced every Register dispatch method to match arms of three
different shapes and required a hand-rolled Debug impl.
Wrap every Register variant in a single struct of the same name,
and give every register value type the same encode and decode
interface. Dispatch becomes uniform, and the hand-rolled Debug
impl becomes a derive.
The hash thread took any Sink whose error implemented Debug and then rendered sink errors by hand. Require the Error trait instead: ? then auto-converts into anyhow::Result, and the generic parameter still avoids tying callers to any concrete error type.
Replace the broadcast/chip_address pair on ReadRegister and WriteRegister with a single Destination enum (Broadcast | Chip). One value is simpler than two correlated fields, and as a side benefit the illegal state (broadcast with a nonzero address) is no longer representable. Wire bytes are unchanged. SetChipAddress and ChainInactive are not affected: their addressing is fixed by the variant. Unroll the send_reg helper at the same time, since every call site is being rewritten anyway.
Give each RegisterCommand and JobCommand variant a dedicated struct that carries its own encode method, collapsing the enum encode methods into one-line dispatchers. Wire bytes unchanged.
Replace the positional build_flags helper with a CommandFlags struct that matches other frame-element encoders.
The protocol module held register definitions and payloads, command framing, response decoding, and the serial codec in one file. Move each into its own sibling module.
Only discover_chips retained a live caller. Inline its body at the Bitaxe discovery site and drop the rest as dead code.
The IO driver strength register holds a 4-bit drive strength for each of the chip's output pins: command, busy, reset, and clock toward the next chip, and response toward the host. Factory firmware runs every pin at strength 1 and raises the clock output to maximum on the last chip of each voltage domain, the chip that drives the clock across the domain gap. Our value for domain-boundary chips came from reading capture bytes in the wrong order, so it strengthened an unnamed high field instead of the clock output: factory firmware sends 00 01 F1 11 where we sent 00 F1 11 11. Name the drive strength fields so the register layout is recorded in the type, correct the boundary value, and check both values byte for byte against frames from the factory captures. Record the field layout in the protocol notes as well.
Add round-trip tests to verify each register payload's encode and decode fns invert. UartBaud::Baud1M is broken: encode and decode use different constants. Mark its test #[should_panic] and fix later.
The hash thread takes its chip-bound sink as a generic with two parallel `Sink` bounds, one per command family. The pair repeats verbatim at every signature that drives the chain and runs past 100 characters, burying the parameter that matters behind where-clause boilerplate. Introduce `ChipCommandSink` as a single-name supertrait of the two `Sink` impls, with a blanket impl that auto-applies to any type already carrying both bounds. Call sites collapse to one name.
Replace magic numbers in the PLL divider constructor with named constants for the VCO threshold and flag bytes, and share one crystal frequency constant with the PLL search loop. Move the flag test next to the constructor. The old test recomputed the expected flag with the same constants the constructor uses, so it could only fail in lockstep. The new one asserts hand-picked flags for VCOs below, at, and above the threshold.
A BM13xx hash board organizes chips along two orthogonal axes: serial chain order and voltage-domain grouping. TopologySpec captures the static wiring (which domain each chain position belongs to). Chain is the live model that owns the Chip and Domain objects, holds enumeration addresses, and carries per-chip runtime stats. Factory methods on TopologySpec cover the common layouts: one domain per chip (EmberOne), a single domain for all chips (Bitaxe), equal-sized cycling domains (S21 Pro), and an explicit mapping with contiguous-domain validation for unusual wiring. Address assignment uses the interval-2 convention seen in firmware captures across the family. Tests pin the assignment against hardware constants from S21 Pro, S19 J Pro, and Bitaxe captures, and reject chip counts that overflow the 8-bit address space.
Register 0xA8 drives soft resets inside the chip. Every write in the captures is either the register's hardware reset default or the default plus the bits that assert a reset on that model of chip. Until now the register was a raw placeholder whose name and values were copied from a reference implementation without understanding. Replace the placeholder, which encoded its value little-endian, with a typed register that encodes big-endian, so values in the code read like the values in the captures and references. Add two constructors, both taking the chip model, to cover the operations we have evidence for: the reset default, broadcast during bring-up, and the core reset, written to each chip just before its cores are configured. Test both operations on both models against the exact bytes from the captures. Stop short of decomposing the register into bit fields: the models disagree on the layout, and most bits have no reliable name in any reference. Record what is known of the bit maps on the type and in the protocol document.
Register 0xA4 configures how the chip generates midstates and rolls version bits. Our old type modeled it as a 16-bit mask plus a magic 16-bit enable constant, byte-swapped relative to the values in the captures and references. Read big-endian, the magic constant is just two flags and a small field: automatic midstate generation, a version-fix flag, and a 2-bit code for how many midstates the chip generates per job. Rename the type to match its wider job and decompose the value into those fields. Keep the constructor for the one configuration every capture uses, so the bytes on the wire do not change. Store the generation code raw rather than as a midstate count, because the mapping from code to count differs by chip model; the doc comment records the mapping. Thin the protocol document's section on this register down to the narrative, following the rule that bit layouts live on the typed register.
Register 0x3C gives indirect access to a small register space inside each hashing core. Replace the raw big-endian word with a mailbox register carrying a typed core command, split into an addressing half (all cores, num, core id) and an operation half (write, read done, core register id, value). Decompiled Bitmain firmware confirms the layout. Its read paths clear the write-enable bit, evidence the write-only captures could not provide. Bring-up writes now construct commands from named core registers (overlap monitor, clock delay, core enable) instead of magic words. Core register 0x0d keeps a raw id; no reference names it.
Register 0x54 carries a small select field in its low bits that routes one of the chip's analog signals, rumored to be the temperature diode, onto the analog mux output. Replace the raw little-endian word with a struct holding the select value, encoded big-endian like the other decomposed registers. A model-keyed constructor owns the selection factory firmware makes, and the bring-up caller sheds a byte-swapped magic constant whose low nibble was the whole payload. References disagree on the field's width, three bits or four; Bitmain's own firmware masks with four, so four wins.
Register 0x18 holds chip-level control bits whose layout shifts between chip generations and whose bits carry only unexplained names in the references, so the value stays an opaque word rather than named fields, the same treatment as the soft reset control. A model-keyed constructor owns the value factory firmware writes during bring-up: a low half word conserved across models under a model-specific high byte. The encoding switches from little to big endian, so values in code now read as the references and captures write them, and the bring-up callers shed a byte-swapped magic constant.
Register 0x2C makes the first and last chip of each voltage domain relay the serial lines across the domain gap. Replace the raw little-endian word with a struct holding the two relay-enable bits, one per direction, and the gap count, a timing parameter of unknown units. The struct encodes big-endian like the other decomposed registers, and a constructor covers the one shape captures show: both directions relayed, each domain with its own gap count. No caller constructs the value yet. Single-chip boards never write the register, and multi-chip chain bring-up, which does, is not yet implemented. A test writes a domain-boundary chip's exact frame from a production chain capture.
The chip model enum carried a BM1397 variant no code could drive and an unknown-id variant that let unrecognized chips flow into code with no way to act on them. Drop both. Chip id decoding now fails with an error on an unrecognized id, and adding a model becomes a compile-time decision at every match over the enum.
Chip models pack the nonce result header differently: BM1370 uses a 4-bit job id and a 4-bit subcore id, while BM1362 and BM1366 use a 5-bit job id and a 3-bit subcore id. The decoder hardcoded the BM1370 layout. Build the codec with a chip model and pick the layout by model. An S19 J Pro capture proves the BM1362 layout: its job ids don't fit in 4 bits. No capture covers the BM1366; a reference driver decodes it the same as the BM1362, so it shares that layout. Job encoding still limits job ids to 4 bits. This commit leaves that alone; the limit will change later, when a chain driver starts sending BM1362 jobs.
Give each response kind a named struct, matching the shape the command types already have. The coming reader task routes nonce and register responses onto separate channels, and the channel payloads need to be standalone types rather than enum variants with loose fields.
One serial wire carries two traffic patterns at once: unsolicited nonce reports and replies to register conversations. A spawned reader task now owns the framed RX stream and routes each frame by kind into one of two bounded channels, nonces on one, register responses on the other. Each channel carries a single kind of traffic, so a consumer handles only the kind it cares about, and neither kind can starve the other. The task alone holds the channel senders, so when the stream ends or fails, from unplug for example, the channels close and consumers see end of stream; channel closure is the only lifecycle signal. Nothing consumes the channels yet; the driver and the actor rewrite arrive next.
Bring-up, verification, and health polling hold long conversations with chips over a protocol without request ids. A new driver owns the command sink, the register response channel, and the timing policy, and offers four verbs: write, read, broadcast read, and write with readback. Reads correlate by content: drain stale responses, send, then accept only a response matching both chip address and register, discarding mismatches, under a per-attempt deadline with bounded retries. Broadcast reads collect replies until a quiet window passes with none new. Verbs take the driver by mutable reference, so conversations cannot interleave on the wire.
Register 0x10 on version-rolling chips is the hash counting number: it limits how many nonce iterations each core performs before the chip advances to the next rolled version. Zero halts hashing, and stock firmware writes a per-model value that scales inversely with frequency. The old NonceRange model guessed a per-chain nonce-mask semantic; its mask constructors emitted values wrong for this register, and the only correct write bypassed them with a byte-swapped hardcode. Store the register as a plain count encoded big-endian, drop the mask constructors, and write the factory value directly where the chip is initialized. Wire bytes are unchanged and checked against the capture frames. Rename the register's mentions in the protocol notes; correcting their content is a separate change. Computing the value from frequency and chain topology follows separately; this change keeps the factory value byte for byte.
Replace the guessed nonce-stride theory in the protocol notes with an evidenced model of how BM13xx chips divide work. A job's search space is the 32-bit nonce field times the 16 rollable version bits. Sub-cores take versions via precomputed midstates, cores own the top nonce bits, and chips start their sweeps at placements seeded by the chip address or set explicitly by the chip nonce offset register, which replaces address placement rather than supplementing it. The hash counting number is a deadline in reference-clock ticks bounding the window each core re-sweeps per batch of rolled versions: zero halts hashing, undersized values shrink the searched space permanently, and oversized ones duplicate work. Ground the model in capture analysis performed for this change: nonce parity matches address-fixed parity at chain scale on two independent BM1366 chains, no fixed bit field anywhere in the nonce carries the chip address, and a threshold of duplicated nonces above the full-slice deadline pins restart-per-batch counting in tick units. Document the full-coverage formula merged in ESP-Miner with an exact integer form computed from the PLL dividers, worked examples, the version epoch and its consequence that factory values require second-scale job refresh, the corrected factory value catalog, and the open questions, notably the unexplained low-bit structure of BM1362 nonces. Also add the chip nonce offset register to the map, correct the initialization sequences to place the hash counting number write after the frequency ramp, and extend hashboard work distribution with extranonce slicing.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Restructures the BM13xx ASIC layer from a single-chip implementation shaped around the Bitaxe into a foundation that can drive multi-chip chains. The first target is the EmberOne00 (12x BM1362), with Bitmain machines behind it: the S19j Pro and S19k Pro class today, and newer models as they're understood. The chain and topology model is built for chains of any length, and the protocol work is grounded in captured factory traffic from long-chain Bitmain hardware. No board bring-up is included yet; this is the protocol, register, and driver groundwork the chain work builds on.
What's here
Frame encoders and decoders are covered by unit tests that check against bytes from captured factory traffic, including S21 Pro and S19j Pro chains. The Bitaxe Gamma path has been exercised on hardware during development.
Status
Draft: history will still be amended. The final documentation commit is labeled WIP while its search-space rewrite is under review. Queued next on this branch: computing the hash counting number from the achieved PLL dividers instead of the fixed constant now in use, and chain-level configuration for bring-up.