SummaryMy GekkoScience Compac F, a USB Bitcoin miner built around one Bitmain BM1397 chip, answered register reads but would not hash. The cause was a core voltage of 1.398 V, set by a trim potentiometer, where the chip needs about 1.45 to 1.55 V. I also wrote a Python driver for it and checked the driver by re-mining Bitcoin's 2009 genesis block until the chip returned Satoshi's nonce, 0x7c2bac1d. The bug that took longest was in my own USB read code: 9-byte replies were being split across two reads and thrown away.
This is the story of the machine behind Strike a Block, told from the repository's own notes and commit history. Everything in it is in the open-source repo: the driver, the protocol reference in the README, and the commits with their dates.
The symptom: alive, but not hashing
The Compac F is a USB stick with one BM1397 chip, 672 hashing cores, and an FTDI FT230X USB-to-serial bridge. Mine enumerated over USB and the chip answered. A chip-ID query came back with the BM1397 signature, 13 97, and the clock register read back with its lock bit set. But no run ever returned a nonce.
Instead, from reset onward, the chip sent a steady flood of well-formed 9-byte frames that all pointed at register 0x40. The values looked like bit-error patterns: mostly exact zeros, some near-all-ones words, a few repeats. They were not hashes.
The useful test was to stop trusting my code. I ran stock cgminer, the reference miner for these sticks, against a local stratum server with wire logging on. It saw the same 0x40 flood from reset, reported a failure, and hashed at 0.000. My driver and cgminer now failed in exactly the same way, which meant the remaining problem was hardware.
The repair: one trim potentiometer
The serial and register side of the chip worked perfectly while the hashing array did nothing. That pattern points at the core power rail. The Compac F sets its core voltage with a small onboard potentiometer, and the guidance for this stick is roughly 1.45 to 1.55 V.
On 15 July 2026 I measured the rail from a heatsink screw to the output of the buck inductor: 1.398 V. That is alive, but 50 to 150 mV under the window. I turned the pot to bring it to 1.50 V and the cores started returning nonces.
Two more conditions turned out to matter. The chip never hashed at 100 MHz and did at 300 MHz. And with no fan, about 15 seconds of hashing heat-soaked the die and the nonces stopped until it cooled for several minutes.
The same day, the stick mined its first block at Bitcoin's original difficulty: the text "Compac F Genesis", nonce 0x256a2ec3, hash 000000002bfdd443…0f69afec.
Why write a driver at all
I wanted a driver I could actually read. The repo's first commit is dated 17 January 2026, and for months that code produced zero nonces. Some of that was the voltage. Some of it was four separate protocol mistakes that a line-by-line comparison against cgminer's driver-gekko.c turned up in July. The interesting ones are below.
Reset is not on DTR or RTS
Older GekkoScience sticks reset the chip through serial control lines. On the Compac F the chip's reset pin, nRST, hangs off the FTDI's CBUS pin CB1, and you drive it by putting the FTDI into CBUS bit-bang mode. The line is active-low, and the BM1397 wants a long hold.
# CBUS bitbang reset. CB1 = nRST is ACTIVE-LOW: 0xF2 drives CB1 high
# (chip running), 0xF0 drives it low (chip held in reset). Sequence is
# high -> low (1000ms hold) -> high, matching cgminer.
usb_val = (FTDI_BITMODE_CBUS << 8) | 0xF2
dev.ctrl_transfer(FTDI_TYPE_OUT, FTDI_REQUEST_BITMODE, usb_val, interface)
time.sleep(0.030)
usb_val = (FTDI_BITMODE_CBUS << 8) | 0xF0
dev.ctrl_transfer(FTDI_TYPE_OUT, FTDI_REQUEST_BITMODE, usb_val, interface)
time.sleep(1.0)
usb_val = (FTDI_BITMODE_CBUS << 8) | 0xF2
dev.ctrl_transfer(FTDI_TYPE_OUT, FTDI_REQUEST_BITMODE, usb_val, interface)
time.sleep(0.200)
From ftdi_reset() in test_direct_usb.py, with the progress prints removed.
The sequence worked before I understood it. My own log messages had the polarity backwards ("hold reset" on the step that releases it) until a commit on 20 July fixed the labels. An earlier version also switched the FTDI out of bit-bang mode after the reset, which hands the pin back to its EEPROM function and can glitch nRST. cgminer stays in bit-bang mode for the whole session, and now so does this driver.
Every frame starts 55 AA, including work
Everything sent to the chip is prefixed 55 AA. Everything it sends back starts AA 55. One of my two code paths left the prefix off the 54-byte task packets, and a chip that receives work without it drops the work silently. A task is 54 bytes, and 56 on the wire.
The task packet and its word order
A miner does not send the chip a whole 80-byte block header. It hashes the first 64 bytes once on the host, keeps the SHA-256 state (the midstate), and sends that plus the last 12 bytes before the nonce.
The quirk is in the shaded fields. In the raw header, bytes 64 to 75 are the merkle-root tail, then nTime, then nBits. On the wire they go out as nBits, nTime, merkle tail: the order of the 4-byte words is reversed, but the bytes inside each word are left alone. The midstate follows the same rule, so the chip receives state words h7 down to h0.
My first version reversed all the bytes, which leaves every word byte-swapped. The chip will hash whatever it is given, so that mistake does not produce an error. It produces nonces that never validate against your header. The fix is a three-line helper:
def reverse_words(data: bytes) -> bytes:
if len(data) % 4 != 0:
raise ValueError(f"Data length must be a multiple of 4, got {len(data)}")
return b''.join(data[i:i + 4] for i in range(len(data) - 4, -1, -4))
From compacf_interface.py (docstring omitted). build_task_packet() applies it to both the 12-byte data region and the 32-byte midstate.
The health check: re-mine block 0
Byte-order bugs are hard here because a wrong nonce is just a number. So I gave the chip a header with a known answer: Bitcoin's own genesis block, timestamp 1231006505. If the task packet and the reply parsing are both right, the chip has to return the nonce Satoshi's block carries, 0x7c2bac1d.
It does, and it settled the last byte-order question. The reply carries the nonce as raw little-endian header bytes:
RX data: aa551dac2b7c0b1087
*** NONCE FOUND: 0x7c2bac1d, job=0x10 (VALID) ts=1231006505 ***
From a run log dated 9 November 2026. Bytes 2 to 5 are the nonce, byte 7 is the job ID.
This became the standing hardware test, because an ordinary header is a bad one. A fixed header has on average one difficulty-1 solution in its whole 232 nonce space, and the count follows a Poisson distribution, so about 37% of arbitrary headers have none. A run that finds nothing is usually luck. The genesis header has a solution on every sweep, so if that fails, the hardware is at fault. The service's daemon runs this check whenever a strike comes up empty. You can take the same header apart yourself with the genesis block decoder.
The bug that took longest: frames split across reads
After all of that, the genesis nonce still showed up on some runs and not others. Same header, same chip, same code.
Every message from the chip is a 9-byte frame. But the FTDI bridge does not know about frames. It has a latency timer, set here to 1 ms, and when the timer fires it flushes whatever bytes it holds to the host. A USB bulk read therefore returns at an arbitrary point in the byte stream, often in the middle of a frame.
My receive loop parsed each read on its own. A frame that straddled two reads was neither a complete frame at the end of the first nor a recognisable one at the start of the second, so both halves were discarded. No error, no warning. The nonce just never appeared.
The fix is the standard one for any byte stream: keep a buffer that persists across reads, append each read to it, consume only complete frames, and carry the remainder forward.
def extract_frames(rx_buf):
frames = []
i = 0
n = len(rx_buf)
while n - i >= 2:
if rx_buf[i] == 0xAA and rx_buf[i + 1] == 0x55:
if n - i < 9:
break # partial frame — wait for the rest
frames.append(bytes(rx_buf[i:i + 9]))
i += 9
else:
i += 1
return frames, bytearray(rx_buf[i:])
From test_direct_usb.py (docstring omitted). The caller does rx_buf.extend(resp) and then frames, rx_buf = extract_frames(rx_buf) on every pass.
That landed on 20 July 2026. The check afterwards was three genesis runs at 300 MHz. Each found 0x7c2bac1d on 111 of 111 sweeps.
The FTDI has two related traps. It prefixes two modem-status bytes to every 64-byte USB packet, not just the first one in a read, so they have to be stripped per packet or they land inside your frames. And a bulk read length has to be a multiple of 64. A 255-byte read overflowed and lost data once the buffer filled.
The risk grows with the nonce rate. Solutions arrive in a burst at the end of each sweep, and a 64-byte USB packet has room for 62 payload bytes after the status bytes, which is less than seven 9-byte frames. A big enough burst always crosses a packet boundary somewhere.
What it does now
The stick runs at 300 MHz and is the miner behind this site. A later change to how often it is fed work took it from 78 to about 137 GH/s. The driver and protocol notes are in the GitHub repo.
If you want to see it work, strike a block: you type one line, and this chip mines a standalone Bitcoin-format block that carries it. The block is not on Bitcoin's blockchain and is not a coin or a token.