Automated git bisect for firmware projects — find the exact commit that broke your firmware, hands-free.
Instead of manually checking out → building → flashing → reading logs for each commit, this tool automates the entire loop:
git bisect → build → flash → capture logs → pass/fail → repeat
20 commits? ~5 rounds × 1 min each = done in 5 minutes, unattended.
Finding firmware regressions is painful:
- Checkout a commit
- Build (~30s)
- Flash the board (~10s)
- Connect debugger/serial, wait for output
- Read logs, decide pass or fail
- Repeat 5-10 times
firmware-bisect does all of this automatically. You provide the known good/bad commits and tell it how to build, flash, and check results. It does the rest.
# Clone
git clone https://github.com/ylongw/firmware-bisect.git
# Run (all config via CLI)
cd your-firmware-repo
python path/to/fw_bisect.py \
--good v1.0 --bad HEAD \
--build "make -j8" \
--flash "JLinkExe -CommandFile flash.jlink" \
--capture "JLinkRTTLogger -device STM32 -if SWD -speed 4000 -RTTChannel 0 {log_file}" \
--capture-duration 10 \
--pass-pattern "TEST OK"
# Or with a config file
python path/to/fw_bisect.py --good v1.0 --bad HEAD -c bisect.yaml🔍 Bisecting 20 commits (est. 5 rounds)
[1/5] a1b2c3d BUILD ✅ FLASH ✅ VERDICT: ✅ PASS → bisect good (45.2s)
[2/5] e4f5g6h BUILD ✅ FLASH ✅ VERDICT: ❌ FAIL → bisect bad (43.1s)
[3/5] i7j8k9l BUILD ✅ FLASH ✅ VERDICT: ✅ PASS → bisect good (44.8s)
[4/5] m0n1o2p BUILD ✅ FLASH ✅ VERDICT: ❌ FAIL → bisect bad (42.3s)
[5/5] q3r4s5t BUILD ✅ FLASH ✅ VERDICT: ❌ FAIL → bisect bad (43.7s)
──────────────────────────────────────────────────────────────
🎯 First bad commit: q3r4s5t
Author: dev@example.com
Date: 2026-02-25
Message: "refactor: cleanup frame handling"
📋 Changed files:
src/nfc/frame.c (+12 -8)
src/nfc/t4t.c (+3 -1)
📊 Summary: 5 rounds, 2 good, 3 bad, 0 skipped
⏱️ Total time: 219s (3.7min)
──────────────────────────────────────────────────────────────
Create a bisect.yaml (or .json) in your project for reuse:
build:
cmd: "cmake --build .build --target firmware"
artifact: ".build/firmware.hex"
flash:
cmd: "JLinkExe -device STM32H747XI_M7 -if SWD -speed 4000 -autoconnect 1 -CommandFile flash.jlink"
delay: 1.5
capture:
cmd: "JLinkRTTLogger -device STM32H747XI_M7 -if SWD -speed 4000 -RTTChannel 0 {log_file}"
duration: 10
verdict:
pass_pattern: "ALL TESTS PASSED|VERIFY OK"
fail_pattern: "HardFault|ASSERT FAILED|FAIL"
skip_on_build_fail: trueSee examples/ for STM32, ESP32, nRF52, and RP2040 configurations.
You: "Something broke between v1.0 and HEAD"
│
firmware-bisect starts
│
git bisect start HEAD v1.0
│
┌───── loop (log₂ N rounds) ─────┐
│ │
│ 1. Build firmware │
│ 2. Flash to board │
│ 3. Capture logs (RTT/UART) │
│ 4. Check pass/fail pattern │
│ 5. git bisect good/bad │
│ │
└─────────────────────────────────┘
│
🎯 "commit abc123 is the first bad commit"
│
📋 shows diff + changed files
| Build | Flash | Log Capture | |
|---|---|---|---|
| STM32 | CMake, Make | J-Link, OpenOCD, ST-Link | RTT, SWO, UART |
| nRF52/53 | west (Zephyr), Make | J-Link, OpenOCD, nrfjprog | RTT, UART |
| ESP32 | idf.py, PlatformIO | esptool | UART |
| RP2040 | CMake | picotool, OpenOCD | UART, SWD |
| RISC-V | Make, CMake | OpenOCD, probe-rs | UART, semihosting |
| Any | Any command | Any command | Any command |
The tool doesn't care what you use — it just runs the commands you give it.
This tool is designed to be called by AI coding agents (Claude, Codex, Gemini, etc.):
- Structured output:
--jsonflag for machine-readable results - SKILL.md: Agent-readable documentation at the repo root
- Zero dependencies: No pip install needed (PyYAML optional)
- Deterministic: Same inputs → same behavior
- Clear exit codes: 0 = found bad commit, 1 = error, 130 = interrupted
# Example: agent calls fw_bisect and reads JSON output
result = run("python fw_bisect.py --good v1.0 --bad HEAD -c bisect.yaml --json")
bad_commit = json.loads(result)[-1] # last failing commit
diff = run(f"git diff {bad_commit['commit']}~1..{bad_commit['commit']}")
# Agent analyzes the diff to find root causeusage: fw_bisect [-h] [--good GOOD] [--bad BAD] [--repo REPO]
[--config CONFIG] [--build BUILD] [--artifact ARTIFACT]
[--flash FLASH] [--capture CAPTURE]
[--capture-duration DURATION] [--pass-pattern PATTERN]
[--fail-pattern PATTERN] [--log-dir DIR]
[--skip-on-build-fail] [--dry-run] [--json] [--version]
Q: What if some commits don't compile?
Use --skip-on-build-fail. The tool will git bisect skip for those commits instead of marking them as bad.
Q: Do I need PyYAML? Only for YAML config files. JSON config works with zero dependencies. Or just pass everything via CLI flags.
Q: Can I use this without a physical board?
Yes — use --dry-run to test the bisect flow, or skip the flash and capture steps and just bisect on build success/failure.
Q: What if the capture process doesn't exit cleanly? The tool sends SIGTERM to the capture process group after the duration expires, then waits 5s before force-killing.
MIT
