S19j/S19k Pro board support + pause/resume + fan safety (BM1366, Amlogic)#6
Open
Schnitzel wants to merge 12 commits into
Open
S19j/S19k Pro board support + pause/resume + fan safety (BM1366, Amlogic)#6Schnitzel wants to merge 12 commits into
Schnitzel wants to merge 12 commits into
Conversation
The original `s19j_pro_amlogic` path drives noPIC-variant Bitmain hashboards (BHB42603 / BHB42631 / S19j Pro 104T) where the BM1362 chips are powered directly from the APW12 rail. This change extends that path to also drive PIC-variant boards (BHB42601 / S19j Pro, BHB42611 / S19j Pro+, etc.), where per-domain DC-DC regulators are gated by an on-hashboard PIC16F1704 microcontroller. Background on the variant split: https://braiins.com/blog/pic-vs-nopic-bitmain-miners-... Without telling the PIC to enable DC-DC, the BM1362s stay unpowered even when the rail is at the spec voltage; naive `output-on` causes the rail to sag as the closed-by-default regulator inputs draw inrush through their input caps. With the PIC handshake in place, the existing chain UART / ramp / hash-thread code wakes up cleanly and mines. The `PicChain` library (`amlogic_cb_tools::pic`) is the matching piece; opcodes were derived from HashSource's decompiled S21 single_board_test https://github.com/HashSource/bitmain_antminer_binaries and verified byte-for-byte against ftrace captures of LuxOS on a real BHB42601 (i2c-0 events at startup). See `pic-tool` and the `amlogic-cb-tools` PR for the standalone diagnostic. Changes in this commit: - Run `PicChain::handshake()` + `enable_dc_dc()` after PSU enable and before the chain comes up. Failure is a `warn!` so noPIC hardware still bring up via the existing path (handshake will fail on a board with no PIC at the expected address; enable_dc_dc has no effect on a noPIC board because there's no PIC to talk to). - Periodic PIC heartbeat (opcode 0x16) from the telemetry task. LuxOS sends one ~every 1.5 s; without it the PIC disables DC-DC after a watchdog timeout and chips drop mid-ramp. - PIC-mediated temperature reads (opcode 0x3c at registers 0x48..0x4b) via the same handle. These show up in the `temperatures` field of the API as `HB{slot}-PIC{0..3}` and match what LuxOS reports. - Conservative 75 °C overtemp gate in the telemetry task. On trip: disable DC-DC, output-off the PSU, cancel telemetry. Stock Bitmain firmware uses ~85–95 °C for throttle/shutdown; this is more aggressive because bench setups have limited cooling and the failure mode of hot-running a hashboard is severe (the BHB42601 we used to bring this up arced its 12 V input plane the first time). - `disable_dc_dc()` on shutdown before cutting the rail. - Make `set_voltage` resilient against the APW12 firmware variant (fw 0x0010 / hw 0x0071 here) that intermittently NAKs response frames even when the underlying DAC write succeeded. Retry up to 4× with readback comparison; without this the chain ramp stalls on the first NAK and the scheduler refuses to assign jobs. - Voltage range / target: BHB42601 EEPROM specifies 13.20 V at 525 MHz. The generic `voltage_for_frequency_stacked()` formula in `bm13xx::thread_v2` is calibrated for emberone-style 12-chip stacked regulators (per-chip 0.3 V × 12 chips = 3.6 V) and returns ~12.6 V when applied to the 42-domain series chain on S19j Pro (0.3 V × 42). 0.6 V under spec causes chips to fall off the chain mid-ramp. Clamp `voltage_range` min to 13.2 V so the ramp always programs at least spec voltage. Long-term fix is per-chip-family voltage-frequency tables in `chip_config.rs`; left for later. - `disable-watchdog` rejected by this APW12 firmware (NAK on opcode 0x81); downgraded from fatal to warn since LuxOS uses periodic heartbeats instead and works fine. Tested end-to-end on an Antminer S19j Pro (BHB42601, kernel aarch64, userspace armhf, musl static binary). Sustained 18 TH/s on a single hashboard for 10+ minutes, 601 valid shares accepted, peak temp 47 °C (75 °C cutoff never tripped), zero chips lost during ramp. PIC firmware version 0x89 on both PICs at i2c addresses 0x20 and 0x21. Depends on the matching `PicChain` library landing in amlogic-cb-tools (PR skot/amlogic-cb-tools#2).
Brings up the Antminer S19k Pro on the Amlogic A113D control board.
Single hashboard (BHB56902) running BM1366 chips: 77 chips across 11
voltage domains, sustains 39.68 TH/s on the dummy job source at 575
MHz / 13.9V (matches LuxOS's 39.33 TH/s nominal on the same hardware).
New board driver:
- mujina-miner/src/board/s19k_pro_amlogic.rs: PSU bring-up
(no PIC variant), per-domain DC-DC, chain enable/reset, factory
voltage range (13.9–14.5V), telemetry via per-chip temp reads on
i2c-0, multi-fan PWM/tach control.
New BM1366 chip family in bm13xx core:
- chip_config.rs::bm1366(): chip_id 0x1366, PllParams BM1366/BM1368
(fb_div 144–235, postdiv1 > postdiv2), nonce_range 0x5a10_0000,
init_regs with the wire bytes confirmed against a LuxOS UART
capture on BHB56902:
* Reg 0x18 broadcast `ff 0f c1 00`, per-chip `f0 00 c1 00`
* Reg 0x3c broadcasts `80 00 85 40` + `80 00 80 20`, per-chip
adds finalizer `80 00 82 aa`
* Reg 0x58 (IoDriverStrength) `02 11 41 11`
* Reg 0xa8 broadcast `00 07 00 00`, per-chip `00 07 01 f0`
target_frequency_mhz Some(575.0): LuxOS auto-detunes the 645 MHz
EEPROM setpoint to 565 MHz adjusted / 575 MHz operating on this
exact hashboard; mujina at 575 sustains 39.68 TH/s vs 38.35 at
645 (chips are at the edge of stability at 645).
post_perchip_ticket_zero_bits Some(2): mirrors LuxOS's final
`reg 0x14 = 0x000000c0` per-chip TicketMask write. Without it
mujina sees ~4 shares/sec; with it ~1400 shares/sec, and reported
hashrate jumps from 28 to 38+ TH/s on a 77-chip chain.
Per-domain UartRelay support (reg 0x2C):
- chip_config.rs::UartRelayPerDomain spec encodes the 22-write
pattern LuxOS uses on BHB56902 (11 domains x 2 boundary chips
each, byte1 stepping by 0x07). bm1362 boards keep per-chip
uart_relay_perchip; BHB56902 uses per-domain.
Mid-init UART baud bump to 3.125 Mbaud:
- chain_config.rs::ChipUartBaudControl trait: two-phase
prepare_new_stream / finalize_baud_switch so the original fd
carries the chip-side `UartBaud` broadcast at the current rate
and a second fd is retuned to the target rate only after the
broadcast has been settled by the chips. On meson_uart the
in-place tcsetattr(Drain) path dropped chips at 3 Mbaud.
- thread_v2.rs: `BaudRate::Baud3M` maps to 3_125_000 numeric
(matches the chip-side register `0x00003011` actual rate, not
3_000_000). Holds the original SerialControl alive via
SerialControlAdapter._original_keepalive so the first /dev/ttyS2
fd never closes during the session — LuxOS pattern.
Multi-pass verify_chain:
- thread_v2.rs: poll all chips, retry up to two more passes on
missing addresses. On BHB56902 at 3.125 Mbaud the first sweep
consistently shadowed the first ~10 chips (always 0x00..0x12,
closest to host) while hashrate math showed them contributing.
Retries find them; logs missing_addrs on final failure.
PER_CHIP_TIMEOUT bumped 100ms -> 500ms for the same reason.
Two-step nonce result_header layout (BM1362 / BM1366 hack):
- thread_v2.rs: existing HACK that re-extracts job_id from bits
6-3 (5-bit job_id / 3-bit subcore packing) is documented as
required for BM1366 too, not just BM1362. Gating it to BM1362
only collapsed BHB56902 hashrate to ~3 TH/s. Proper fix is
chip-aware Response::Nonce decoding in protocol.rs; the
comment now points at that future refactor.
Config / daemon plumbing:
- config.rs: HashboardModel::S19kPro variant, label "S19k Pro
(Amlogic control board)" / chip "BM1366".
- daemon.rs / backplane.rs: dispatch S19kPro to the new
s19k_pro_amlogic board driver alongside the existing S19jPro
path.
- transport/amlogic.rs: factory wiring for the new board.
Existing boards (EmberOne, S19j Pro Amlogic/Bitcrane) get two
additive None initializations (chip_uart_baud, post_broadcast_chip_baud)
to keep them compiling with the new ChainPeripherals / ChainConfig
fields. No behavior change for those boards.
Validated on hardware:
- One BHB56902 hashboard, Amlogic A113D control board.
- 77/77 chips verified mining (after multi-pass verify) at 575 MHz
/ 13.9V, sustains 39.68 TH/s on the dummy block-881423 source,
matching the LuxOS reported rate on the same hardware.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
PauseMining and ResumeMining were stubs that logged "(not yet implemented)" and only flipped a flag. The API endpoint exists and the UI button calls it, but mining kept running. Replaces the stubs with a working soft-pause: PauseMining: - sets self.paused = true - resets every thread's HashrateEstimator so the API/UI reads 0 TH/s immediately (instead of aging the 5-min window out naturally) - does NOT touch the chip power rail — chips stay warm on the last job ResumeMining: - sets self.paused = false - re-dispatches each source's cached last_job to the threads so they pick up fresh ntime-rolled work right away assign_job_to_threads + handle_new_thread are gated on !self.paused so new stratum jobs don't accidentally restart hashing while paused. handle_share also drops everything when paused — without this, chips still mine the last job they were loaded with and would keep emitting nonces; the scheduler would forward them to the pool even though the user pressed pause. Dropping at handle_share both halts share submission and stops feeding the per-thread hashrate estimator, which is what makes the API hashrate go to 0 within a single sample window. API patch_miner timeout raised 5s -> 300s. PauseMining is fast, but if we ever go back to hard pause (chip power-down + cold re-enumeration) the resume round-trip takes ~2 min for the full chip init + frequency ramp, and the 5s bound was timing the client out while the work completed server-side. Validated end-to-end on a BHB56902 S19k Pro at 192.168.1.222: mining 78.8 TH/s, 14 shares -> pause: 0 TH/s, 14 shares (frozen) -> 60s: 0 TH/s, 14 shares (still frozen) -> resume: 51 TH/s, 21 shares (climbing) -> +30s: 40 TH/s, 83 shares TODO (hard pause): a previous attempt called disable_chips() on pause and re-ran initialize_chips on resume. That worked for the GPIO side but the post-reset chip re-enumeration only saw ~37/77 chips on the BHB56902 (down from 77/77 on a clean cold boot). The host UART / chip-side UART baud relationship recovers, but the chain RST_N pulse seems to leave some chips in an indeterminate state. Left the reset_chip_uart_to_base_baud() helper and the disable_chips path in place for when we revisit this — the actual fix is probably PSU output cycle (drop voltage, settle, bring back up) rather than asic_enable-only reset. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Two related fixes for the API's per-thread hashrate field (`/api/v0/miner` -> boards[].threads[].hashrate), which the web UI surfaces on the per-board detail page. It used to be stuck at 6.39 TH/s no matter what the chain was doing — both during ramp-up (when chain hashrate was 0) and after the chain hit steady state (when total hashrate was ~38 TH/s). 1. BM13xxActor now owns a HashrateEstimator (5-minute window, same as the scheduler's per-thread one) and feeds it the `share.expected_work` of every accepted share. `self.estimated_hashrate` is updated from the estimator after each feed, which is what gets written into `status.hashrate`. The previous code set `status.hashrate = self.estimated_hashrate` once with the static initial value (83 GH/s × chip_count) and never updated it. 2. Both s19k_pro_amlogic and s19j_pro_amlogic seeded the initial thread state with `thread.capabilities().hashrate_estimate` and the `thread_hashrate_value` helper fell back to the same value when `status.hashrate == 0`. Both call sites now use 0 — the actor's estimator takes over as soon as the first share comes in. No more "6.39 TH/s per board while chain is at 0" during the frequency ramp / pause window. `disable_chips` resets the actor's estimator too so a pause/resume cycle starts from a clean window. Validated on .222 (BHB56902): fresh restart now shows per-thread hashrate = 0 during init instead of 6.39 TH/s; will climb to match chain-wide hashrate after the first ~5 min of shares. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Pause flipped the chain-wide hashrate to 0 but the per-board view on /api/v0/miner kept reading the pre-pause hashrate (and is_active: true) because nothing told the hash thread that its scheduler was paused — the actor's own hashrate_estimator kept eating shares from chips that were still hashing the last loaded job. Adds `HashThread::set_paused(bool)` with a default no-op impl (CPU miner and other backends don't have a per-thread display to update), overridden by: - `BM13xxThread` forwards to the actor via a new `ThreadCommand::SetPaused`. On pause the actor flips an internal `paused` flag, drops its hashrate_estimator and zeroes its status (hashrate=0, is_active=false). On resume it just clears the flag — the next share repopulates everything. - `handle_chip_response` short-circuits the share-found branch when `self.paused`, so chips can keep emitting nonces from their last loaded job (which the chain still has, since we don't reset on pause) without polluting the per-thread hashrate. - `BoardStateHashThread` in both `s19k_pro_amlogic` and `s19j_pro_amlogic` overrides `set_paused` to forward to the inner thread AND call `sync_thread_state(Some(false))` on pause so the board-level `state_tx` picks up the new zero immediately. Without this override the trait default no-op ran and the SetPaused command never reached the actor at all. Also drops two stale `status.hashrate = self.estimated_hashrate` writes (in handle_work_assignment + initialize_chips) that were seeding the per-thread display with the static 6.39 TH/s init value before any shares had been recorded. Scheduler's PauseMining / ResumeMining now iterate `self.threads` and call `thread.set_paused(true/false)`. Validated on .222 (BHB56902): mining: chain 52.86 TH/s, per-thread 63.91 TH/s, active=true -> pause: chain 0, per-thread 0, active=false -> resume+20s: chain 47.29 TH/s, per-thread 44.36 TH/s, active=true Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Soft pause (per the previous commit on this branch) stopped dispatching
new jobs and zeroed the UI hashrate, but it left chips fully powered
at their pre-pause core voltage and PLL frequency. They kept rolling
ntime on the last loaded job, drawing roughly idle-mining power, and
the board never cooled. Confirmed on .222: after `PATCH /api/v0/miner
{"paused":true}` shares froze at 521 and the API agreed `is_active:
false / hashrate: 0`, but TMP75 kept climbing.
`disable_chips()` is the real off-switch in the actor, but it's been
broken on BHB56902 since amlogic-s19kpro-support: the post-reset
re-enumeration at 115200 only sees ~37/77 chips back, almost
certainly a chain-RST_N propagation issue with the reset_release_ms
timing we haven't debugged yet. Going around it instead:
- On pause, BoardStateHashThread::set_paused (both s19k and s19j)
forwards to the inner thread (which zeroes the per-board UI and
sets the actor's `paused` flag) and then calls
`psu.set_enabled(false)` -- pulls GPIO437 high, kills the APW12
output. Chips lose power, board cools.
- The actor's SetPaused(true) handler additionally drops
`current_task` and sets `chip_state = Disabled` so the next
UpdateTask after resume routes through `initialize_chips()`
rather than reusing stale per-chip state.
- On resume, BoardStateHashThread::set_paused(false) re-energizes
the rail and waits psu_settle_ms before forwarding to the inner
thread, so the next UpdateTask hits a powered, settled chain.
The actor's SetPaused(false) handler also resets the host fd
back to 115200 (chips will be at default baud after the power
cycle, but tcsetattr persists on the host across resets); this
re-uses the existing `reset_chip_uart_to_base_baud()` helper
that was previously dead code waiting for this caller.
Resume is now a full cold boot: ~5s PSU settle + chip enumeration +
register config + ~84-step frequency ramp to 575 MHz, ~2 minutes
end-to-end. Patch_miner's 300s timeout was already sized for this.
Notes on dispatch and order-of-operations:
- The new path goes through the BoardStateHashThread override on
the noPIC Amlogic boards (s19k_pro, s19j_pro). Backends that
don't have a PSU rail of their own (e.g. Bitaxe USB) still use
the trait default and behave like the old soft pause.
- Pause sequence is "inner first, then PSU off" so the per-board
UI publishes the zero before the chain goes quiet. Resume is
"PSU on + settle, then inner" so the actor doesn't get marked
runnable against a still-coming-up rail.
- The fan-safety telemetry task is unaffected: the TMP75 sensors
sit on the always-on control-board i2c bus, not on the chip
chain, so reads continue while PSU is off; temps fall, the
fan curve drops to its 60% floor, the watchdog doesn't fire.
Validated on .222 (BHB56902, single HB1, noPIC):
mining 3:30 -> 46.4 TH/s, 134 shares
-> PATCH paused:true
"Mining paused -- share submissions and new job dispatch stopped"
"Hard pause: PSU output disabled -- chips drained, board will cool"
hashrate=--, shares stuck at 169 (in-flight nonces landed post-pause)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The decoder (`record_chip_die_temp`, `last_chip_die_temp_c` field, and
the `nonce & 0xFFFF == 0x80` branch in `handle_chip_response`) was
added to extract on-die temperatures from chip "fake nonce" responses
after configuring the analog mux for the thermal diode. Two reasons
to delete it rather than finish wiring it in:
1. On a 297k-frame LuxOS UART capture from a live BHB56902, only
22 frames matched the documented `nonce & 0xFFFF == 0x80`
pattern and every one of them decoded to nonsense (456–2530 °C
after applying the assumed K1−K2·raw/256 formula) — well outside
the BM1366 die's plausible range. The pattern (and/or the
conversion formula) is wrong for this chip.
2. The supporting investigation found that BM1366 doesn't expose
its die thermal diode the way Bitaxe does (Bitaxe routes the
diode pin to an EMC2101 fan controller and reads `temp_c` over
i2c — see `bitaxeorg/ESP-Miner::main/thermal/EMC2101.{c,h}` and
`Thermal_get_chip_temp` in main/thermal/thermal.c). The
BHB56902 hashboard doesn't have an EMC2101; the only thermal
signal exposed is the LM75A pair on the PCB. Bitmain's stock
firmware confirms this: `recon/.../etc/topol_BHB56902.conf`
declares `"sensor": []` in the per-chain block (no chip-internal
sensors) and runs PID off the LM75As with `pid_target_temp: 60`.
Braiins OS+ on this same hardware doesn't even emit a
`chip_temperature_celsius` metric for BHB56902 (verified against
`/Users/michael/Downloads/192.168.1.38/.../metrics.prom`) — they
only publish `hashboard_temperature{type=raw|filtered}`. On
BHB42601 they DO publish an "approximated(pcb_sensor)" chip
temp that is exactly `pcb_temp + 15.0` °C (verified across
5640 samples on `.222` + `.10.8`).
The fan curve and overtemp cutoff in `s19k_pro_amlogic.rs` /
`s19j_pro_amlogic.rs` now drive off TMP75 directly (the same signal
stock Bitmain and Braiins use). The orphaned `bm1366_raw_to_celsius`
reference made the tree fail to build; this removal restores it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Hard pause + resume was getting only ~30-43/77 chips back on cold
re-enumeration, with the missing addresses scattered randomly and
"Failed to decode response" warnings sprayed through the log. The
prior commit (Hard pause: drop PSU rail on pause, cold-boot on
resume) attributed this to "a chain-RST_N propagation issue we
haven't debugged yet" -- it wasn't. The actual cause was a leaked
serial reader task.
`SerialReader`, `SerialWriter`, and `SerialControl` all share
`Arc<SerialInner>`, so the underlying `/dev/ttyS2` fd stays alive
until every Arc is dropped. `maybe_switch_chip_baud` and
`reset_chip_uart_to_base_baud` swap the actor's chip channels onto
a fresh fd, drop the old writer half, and then `await` the old
reader's `JoinHandle` with a 200 ms timeout. But the reader task
has no exit signal -- it only returns when the stream errors,
which never happens for a live tty -- so the timeout just abandons
the task. The task stays alive holding its Arc, holding the OLD
fd open. After the swap, both old and new reader race for kernel
TTY bytes; the kernel hands each byte to whichever asks first.
Frames get split between the two tasks, the codec downstream of
each sees garbage, and most chips look like they "didn't respond"
because their response went to the wrong reader and was dropped.
Fix: `handle.abort()` before awaiting the JoinHandle so the task
drops its Arc, the fd closes, and a single reader owns the chain.
Confirmed on a BHB56902 at 192.168.1.222 -- cold init plus two
back-to-back pause/resume cycles all complete with 77/77 chips, no
chain verification failures, and steady-state hashrate matching
pre-pause (~36-41 TH/s).
Defensive board-level changes for consistency with cold boot:
- Assert RST_N LOW *before* dropping the PSU rail on pause, with
a `reset_assert_ms` hold so the daisy-chained signal reaches
every chip while the rail is still up. Avoids the mid-mining
power-yank that could leave internal flip-flops in
indeterminate states.
- On resume, hold reset asserted for `reset_assert_ms` between
`set_output_low()` and PSU enable so RST_N propagates to all
77 chips before they see power.
- Drop the APW12 voltage back to the cold-boot setpoint (12 V)
before re-enabling. The PSU retains the last commanded voltage
across enable cycles, so without this resume comes back at
13.9 V and chips start hot.
- Best-effort PIC handshake on resume, matching cold-boot
semantics for the PIC-variant S19j Pro hashboards where the
DC-DC gates lose state when the 12 V rail drops. BHB56902
(this test board) is a noPIC variant; the handshake fails
silently in both cold boot and resume.
The legacy comment in `disable_chips` describing this as a
chain-RST_N propagation issue is updated to point at the actual
root cause.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
`S19kProAmlogic` used to pick the first present hashboard out of the
three configured slots and ignore the rest. On a fully-populated
control board that's 1/3 of the available hashrate. This commit
brings up every present slot in parallel on the shared APW12 / Amlogic
SoC, with three coordinated concerns:
1. Detection.
`select_present_hashboards` walks every configured slot and
returns every one whose detect GPIO reads "present". Order is
preserved so per-thread names (HB0/HB1/HB2) match config order.
A missing required slot is still fatal; a missing optional slot
is still skipped.
2. Lockstep voltage ramp.
All chains share one APW12. If each chain runs its own
coordinated voltage-frequency ramp independently, the rail will
oscillate between the chains' step values -- chain N commanding
`set_voltage(12V)` for its step-0 ramp would brown out chain 0
mid-mining at 13.9V. New `ChainCoordinator` in chain_config.rs
wraps a `tokio::sync::Barrier`: every chain calls
`sync_voltage_step` at the top of every ramp step, the
elected leader drives the shared regulator on behalf of the
cohort, and all chains then wait the same VOLTAGE_SETTLE_DELAY
before sending their per-chain PLL command. All chains compute
the same target voltage from the same step index, so it's safe
for only the leader to actually touch the rail.
`ramp_coordinator` is `Option<Arc<ChainCoordinator>>` on
`ChainPeripherals` and only set when there's more than one
chain; single-chain boards (EmberOne, BitAxe, S19j Pro Bitcrane,
and S19k/S19j Pro running on a single hashboard) keep the
legacy "each actor drives its own regulator" path.
3. Concurrent dispatch.
The scheduler dispatches the first UpdateTask to each thread
sequentially with `.await`. Without the ramp barrier this was
fine -- chain 0 fully ramped before chain 1 ever started -- but
with the barrier it deadlocks: chain 0 sits at step 0 waiting
for chains 1 and 2 to arrive while they're still waiting for
their first UpdateTask to clear chain 0's blocking init.
Fix at the actor level: `handle_command` ACKs UpdateTask /
ReplaceTask *before* running `handle_work_assignment`, so the
scheduler unblocks and dispatches to the next thread while the
current actor's initialize_chips runs in the background. Cold
init can take ~2 minutes (enumeration + register config + 84-
step frequency ramp); without this, chain 0 holds chains 1 and
2 out of init for that whole window. The scheduler in
`process_template_event` already discards the `Some(old)` it
gets back, so returning `Ok(self.current_task.clone())` instead
of waiting for the real result is safe.
`S19kProAmlogic::initialize` now health-gates and PIC-handshakes
every selected slot before the PSU comes up, then `create_hash_threads`
spawns one `BoardStateHashThread` per board sharing the APW12 +
`ChainCoordinator`. The pause/resume path on each thread is
idempotent on the shared PSU -- the first thread to pause drops the
rail, subsequent threads' `set_enabled(false)` is a no-op, and
similarly on resume.
Telemetry consolidated.
`native_telemetry_task` now takes a `Vec<AmlogicHashboardConfig>`
and walks every present hashboard each tick. Spawning N independent
tasks would have each one call `state_tx.send_modify` with only its
own hashboard's temperatures, clobbering the others. The overtemp
gate sums across every sensor on every board so any one hot board
still kills the shared rail.
Verified on .222 (BHB56902 x3, all 77-chip BM1366 chains):
* cold boot: all 3 chains enumerate 77/77, all 3 ramp to 575 MHz,
113 TH/s aggregate sustained (45 + 35 + 40 per chain).
* hard pause + resume: rail drops to 0 V across all 3 chains in
one PATCH, rail comes back at the cold-boot setpoint, all 6
ramps (3 cold + 3 resume) complete cleanly with 0 chain
verification failures, hashrate returns to 114 TH/s.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Two boards already destroyed by thermal events under the previous fan
control: a single boot-time PWM write at config.startup.default_fan_percent
(50%), no in-loop adjustment, an overtemp cutoff at 75 °C TMP75 that
never tripped because the TMP75 sensors under-read the BM1366 die by
~15 °C (per Braiins's own metric annotation, where
`chip_temperature_celsius` = `pcb_temperature_celsius` + 15 with
`source="approximated(pcb_sensor)"`), and a swapped HB0/HB2 sensor
address map that silently pointed the cutoff at the wrong slot for any
deployment not running on HB1.
Changes, both in `native_telemetry_task` (s19k_pro_amlogic and
s19j_pro_amlogic):
1. Dynamic fan curve from the hottest TMP75 reading on the board.
Floor 60% below 40 °C, linear ramp to 100% at 60 °C. Logged
every time the target duty changes (and only then) so the log
isn't spammed at steady state.
2. Watchdog: if no TMP75 sample has landed in 30 s (i2c stuck,
sensor died, telemetry task stalled and just resumed), pin the
fans to 100 % until a fresh read recovers.
3. TMP75 hard cutoff lowered 75 -> 65 °C. Stock Bitmain's
pid_target_temp on this board is 60 (per
recon/.../etc/topol_BHB56902.conf) with hot at 80 and dangerous
at 90 -- but those numbers presuppose chassis airflow we don't
have on the bench. 65 °C TMP75 ~= 80 °C estimated die, which
matches stock's "hot" boundary while leaving us with the same
PSU-drop + telemetry-cancel safety path.
4. HB0/HB2 TMP75 address swap fix. `tmp75_addresses` previously
returned [0x4E, 0x4A] for HB0 and [0x48, 0x4C] for HB2; the
stock Bitmain mapping (and `amlogic-cb-tools::hashboard_s19jpro`)
is HB0=[0x48,0x4C], HB1=[0x4D,0x49], HB2=[0x4E,0x4A]. Also
confirmed against `topol_BHB56902.conf`'s `ctrlboardsensor`
block and a live `i2c-probe` sweep on .222. Both s19k_pro and
s19j_pro versions had the swap.
5. mujina.toml and mujina-hb2.toml: default_fan_percent 50 -> 100
so the boot window (before the telemetry task takes over) is at
full airflow instead of the previous half.
Validated on .222 (BHB56902, single HB1, noPIC) running through cold
init and freq ramp: TMP75 climbed 23 -> 43 °C, fan curve tracked
correctly (60% floor through 40 °C, ramping to 66-67% by 43 °C), no
cutoff triggered, no watchdog fallback.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Schnitzel
force-pushed
the
fan-safety-cleanup
branch
from
July 4, 2026 17:19
0431db3 to
538f00b
Compare
This was referenced Jul 4, 2026
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.
Consolidated board-support branch, refreshed to our current integration (ry3t).
This branch now carries the complete, current Amlogic board-support stack for
BM1366-based hashboards, superseding the earlier split PRs #3/#4/#5 (whose code had
diverged from our integration branch as we kept developing). Rather than maintain
four diverged branches, this is the single cumulative board-support PR; #3/#4/#5 are
being closed in its favor.
What's included
the PSU rail, cold-boot on resume), with a hard-resume fix that aborts the stale
chip-UART reader before swapping fds
Follow-on PRs (stacked on this branch)