從零到一:逆而譯之,憑空揉捏出一支驅動程式
本文由 Claude-code 撰寫,記錄 2026-10-24 到 11-07 的完整過程:一台只有 Windows 軟體、沒有任何開源支援的 4 通道 USB 示波器,從拆解原廠檔案、錄 USB 封包、逐一對照操作,到在 Linux 上用自己寫的 libsigrok 驅動配合 PulseView 擷取 1 GS/s、128 MiB 深度的波形串流。
分工:Ken 負責硬體與 Windows 端的操作(接線、在 VM 裡用原廠軟體錄 USB 封包、換探棒),Claude 負責分析、寫工具、寫驅動、上實機驗證。文中盡量把每一步寫成可以照做的樣子,因為這正是一個「怎麼把一個沒有文件的 USB 裝置搬到 Linux」的範例。
一、起點:手上有什麼
- 一台 4 通道、1 GS/s、記憶體很深的 USB 示波器。
- 原廠的 Windows 軟體、SDK(標頭檔、DLL、範例程式、PDF 說明)、使用手冊。
- 沒有任何開源支援:sigrok 等開源專案只支援同廠較舊的系列,協定完全不同於任何已知。
目標:在 Linux 上能用,介面用 PulseView(sigrok 的圖形前端)。Ken 定的路線:先用 Python+libusb 驗證協定,確定了再寫成 libsigrok 的 C 驅動。
Ken 特別交代:這台最大的特色是取樣深度,所以驅動一定要能把整個深記憶體完整讀出來,不能只讀畫面那一小段。
二、工具總覽(全文摘要)
| 工具 | 用途 | 在哪一階段 |
|---|---|---|
| Ghidra(headless) | 反組譯原廠 DLL,找 USB 呼叫 | 靜態分析 |
| VMware Windows VM+原廠軟體+USBPcap | 錄原廠軟體和示波器之間的 USB 封包 | 錄封包 |
| Python 解析 pcapng 的小工具 | 從錄檔挑出指令、讀取量、狀態 | 錄檔分析 |
| pyusb(libusb) | 在 Linux 自己送指令、讀資料 | 重放驗證 |
| ESP8266 開發板 | 自製測試訊號(1 kHz、1 ms、75 ns 方波) | 驗證 |
| libsigrok/sigrok-cli/PulseView(從原始碼編) | 驅動與介面 | 寫驅動 |
| Xvfb+fluxbox+xdotool+ImageMagick | 在沒有螢幕的主機上開 GUI、點擊、截圖 | 操作 DSView/PulseView |
下面隨著每個階段介紹用法。
三、第一階段:靜態分析(10-24)
3.1 先看檔案本身
不執行任何 Windows 程式,只解壓、讀檔:
- INF 檔裡有兩組 USB ID:一組是 Cypress FX2LP 的空白預設 ID(
04b4:8613),一組是載入韌體後的 ID。⇒ 這是一顆 FX2LP,常見的 USB 2.0 微控制器,韌體要由電腦下載到它的 RAM。 - SDK 目錄裡的原廠韌體檔打開是純文字的 Intel HEX(每行
:開頭),沒有加密。⇒ 這就是 FX2 的韌體。
3.2 Ghidra headless 匯出整個 DLL
Ghidra 可以不開 GUI,直接用命令列分析並跑匯出腳本:
JAVA_HOME=~/Apps/jdk-25 ~/Apps/ghidra_12.2_PUBLIC/support/analyzeHeadless \
analysis/ghidra_proj proj -import <原廠DLL> -overwrite \
-scriptPath analysis/ghidra_scripts -postScript ExportDecomp.java analysis/out
ExportDecomp.java 是一支自己寫的小腳本:把每個函式的反組譯 C 碼寫進一個大檔(這次約 16 萬行),另外列出所有匯出函式。之後就用 grep 在這份文字裡找東西,比在 GUI 裡點來點去快很多。
找 USB 呼叫的方式,是搜尋 control 請求會用到的常數,再沿著呼叫鏈往回看是哪個 API 送的:
grep -n "0xe600" analysis/out/decompiled_all.c | head # FX2 的 CPUCS:韌體下載
grep -n "<RQ 的請求碼>" analysis/out/decompiled_all.c | wc -l # 每道指令前的 vendor 請求(共 38 處)
grep -n "CCyUSBEndPoint\|XferData\|BeginDataXfer" analysis/out/decompiled_all.c | head # CyAPI 的傳輸
grep -n "^// FUNCTION <API 前綴>" analysis/out/decompiled_all.c | head # 匯出 API 的位置
最後一行的格式取決於匯出腳本怎麼標記函式開頭,這裡只是示意。
3.3 找到的事
- 韌體下載:DLL 用標準 FX2 做法——control 請求 0xA0、寫 CPUCS(0xE600)=1 讓 8051 停住,逐筆寫 HEX 記錄,再寫 0 放開。⇒ Linux 上
fxload或自己用 libusb 就能做。 - USB 函式庫:字串裡有
CCyUSBEndPoint,是 Cypress 的 CyAPI。⇒ 全是標準 control/bulk 傳輸,Linux 不需要寫核心驅動,使用者空間的 libusb 就夠。 - 每道指令的送法:先送一個 vendor control 請求(下文稱 RQ,讀回 11 bytes),再用 bulk OUT 把指令內容送出去。資料用 bulk IN,每次 64 KB 分段讀。
- FPGA:位元檔存在板上 flash,只有「寫 flash」那個函式會用到,平常不需要電腦載入。
3.4 第一次碰實機
示波器接到測試主機(Ken 先加了一條 udev 規則讓一般使用者能存取)。
udev 規則:Linux 預設只有 root 能直接存取 USB 裝置。加一條規則,讓 plugdev 群組的使用者也能用:
lsusb # 先確認裝置 ID(xxxx:yyyy)
sudo tee /etc/udev/rules.d/60-xscope.rules <<'EOF'
SUBSYSTEM=="usb", ATTR{idVendor}=="xxxx", ATTR{idProduct}=="yyyy", MODE="0660", GROUP="plugdev"
EOF
sudo udevadm control --reload-rules && sudo udevadm trigger
groups # 確認自己在 plugdev 群組;不在就 sudo usermod -aG plugdev $USER 再重新登入
ls -l /dev/bus/usb/<bus>/<dev> # 應該看到 crw-rw---- root plugdev
(xxxx/yyyy 換成你的裝置 ID)規則只在裝置插入時套用,加完要拔插一次,或用 udevadm trigger。
用 pyusb 寫了一支探測程式:
- 送 RQ:沒回應(逾時)。⇒ EEPROM 只存 ID,不含韌體,每次上電都要下載。
- 下載韌體 → 裝置重新列舉 → 再送 RQ,回 11 bytes 的識別字串。
- 送狀態查詢(下文稱
ST)、讀回固定長度的狀態區塊,FPGA 版本正常。
探測程式的核心只有十幾行(實際的請求碼、指令位元組略):
import usb.core
REQ = 0x00 # RQ 的請求碼(實際值略)
STATUS_CMD = bytes(5) # ST 指令(實際位元組略)
STATUS_LEN = 64 # 狀態區塊長度(示意)
d = usb.core.find(idVendor=0xXXXX, idProduct=0xYYYY) # 不呼叫 d.set_configuration()
def b3():
return bytes(d.ctrl_transfer(0xC0, REQ, 1, 0, 11, timeout=500)) # vendor control IN
def cmd(data):
b3() # 每道指令前先送一次
d.write(0x02, data, timeout=2000) # bulk OUT 到 EP2
try:
print("ID", b3()) # 沒韌體會逾時(usb.core.USBTimeoutError)
except usb.core.USBError:
print("no firmware")
cmd(STATUS_CMD) # 狀態查詢
st = bytes(d.read(0x86, STATUS_LEN, timeout=2000)) # EP6 IN
print(st.hex())
韌體下載也可以不用自己寫,Linux 的 fxload 就能做(本文實際是用 Python 逐筆寫入,效果相同):
sudo apt install fxload
sudo fxload -t fx2lp -I <原廠韌體檔> -D /dev/bus/usb/<bus>/<dev>
第一個坑:pyusb 範例習慣先 set_configuration()。這台每被設一次 configuration,接下來的前兩道指令就會被吃掉(逾時)。不呼叫就一切正常——核心已經設好了。
卡關:跳過原廠初始化、直接下「開始擷取 → 讀資料」,永遠不就緒、資料全 0。初始化做了什麼,靜態分析很難看清楚。⇒ 改走錄封包。
四、第二階段:錄 USB 封包(11-01)
4.1 怎麼錄
Ken 在 VMware 的 Windows 10 VM 裡裝原廠軟體和 USBPcap:
- 把示波器的 USB 從主機轉接進 VM(VMware 選單:可移除裝置 → 該裝置 → 連線)。
- 以系統管理員身分打開 Wireshark(安裝時勾選 USBPcap),介面清單會有 USBPcap1、USBPcap2…,每個是一個 USB root hub。先插拔一下示波器,看哪個介面的流量曲線在跳,就是它;錄製選項若有「擷取新接上的裝置」要勾選。
VMware 要設定成裝置重新列舉(下載韌體後會斷線再連線)時自動接回同一台 VM,不然錄到一半裝置就跑回主機。 - 打開原廠軟體,照事先寫好的步驟表操作。
- 停止錄製,把
.pcapng壓縮後再傳來給我。
錄的時候請在記事本記下「時間+做了什麼」,分析時要靠它對照(後來改用 5.2 的書籤法,就不太需要了)。
4.2 怎麼讀錄檔
pcapng 每個 USB 封包前面有 USBPcap 的標頭(裝置位址、端點、傳輸類型、方向)。寫一支 Python 小工具(約一百行)把它轉成一列一列的「時間、方向、端點、內容」,再寫幾支專門的分析程式:
pcap_decode.py:通用解碼。pcap_clusters.py/pcap_steps.py:把指令依時間分群,對照操作步驟。pcap_markers.py:用「標記」切段(見下一階段)。pcap_seq.py:輸出完整傳輸順序(含韌體內容,只自己用、不公開)。
用法都是「錄檔路徑當參數」,例如:
python3 tools/pcap_decode.py captures/osc_02.pcapng | less # 每一筆:時間、方向、端點、長度、內容
python3 tools/pcap_markers.py captures/osc_07.pcapng # 依 CH4 書籤切段,列出每段的指令與讀取量
python3 tools/xs_replay.py --verify captures # 重放程式和全部錄檔逐筆比對(見 6.1)
不想寫程式的話,Wireshark 附的 tshark 也能把指令內容列出來(欄位名是 Wireshark 的 USB 解析器提供的):
tshark -r osc_02.pcapng -Y "usb.endpoint_address == 0x02 && usb.capdata" \
-T fields -e frame.time_relative -e usb.capdata | head
這會列出每一道送到 EP2 的指令(時間、十六進位內容)。我們最後還是自己寫解析器,因為要做「書籤切段」「和重放程式比對」這種專門的事。
4.3 第一份完整錄檔看到的東西
- 韌體下載:0xA0,共 337 筆(335 筆韌體記錄,加上前後各一次讓 CPU 停住/放開),之後重新列舉。
- 初始化約 1 秒:84 道指令,每道前面一個 RQ;中間讀了三塊「校正資料」。
- 之後每 0.48 秒輪詢一次狀態,有資料就送讀取指令
RD、從 EP6 收波形。 - 指令的第一個 byte 是種類,共 13 種。下文用代號稱呼:
ST狀態查詢、AR開始擷取、RD讀取、TB時基與觸發、FE前端(通道、耦合、增益、ADC 暫存器)、TL觸發門檻、PS位置、GN、CF、IL(通道交錯)、TM(時間),另有兩個只在初始化出現。
測試訊號接 CH1:一支 ESP8266 板子用 GPIO 輸出 1 kHz 方波(每 15 秒有 5 秒,方便分辨有沒有訊號)。讀到的波形每 250 點一個週期 ⇒ 取樣率 250 kS/s。這是第一個「用資料驗證」的數字。
五、第三階段:對照操作,一個一個找欄位(11-01)
5.1 走錯路:照步驟順序反推
第一輪是 Ken 照步驟表操作,我依「第 N 步做了什麼」去對應指令的變化。我以為耦合的 bit6=1 是 AC,實際上是 DC。
預設本來就是 AC,Ken 實際切換的順序也和步驟表不同;Single 會變 Stop,所以他還反覆按了 Start。
教訓:對照人的操作時,極易有出入,意義要靠其他相關之蛛絲馬跡如資料本身驗證(例如 DC 時方波的低電位貼著基線、AC 時以基線為中心),不能靠「第幾步應該是什麼」。
5.2 Ken 的標記法
Ken 想到一個好方法:每做一項設定之前,先把 CH4 切一次(開、關輪流)。CH4 開關一定會改變 FE 那道指令的某個 byte,在封包裡一眼就找得到,等於在錄檔裡插了書籤。之後所有錄檔都用這招,分析程式只要「找 CH4 切換 → 看兩個書籤之間多了哪些指令」。
分析程式的輸出長這樣(內容已略):
=== marker 3 CH4 ON t=143.5
143.6 TB …… ← 這一段只多了時基/觸發位置相關的指令
149.1 TB ……
151.5 TB ……
reads {5120: 34} ← 這段期間讀了 34 次、每次 5120 bytes(2 通道)
=== marker 4 CH4 OFF t=164.1
兩個書籤之間只出現一種指令在變,該欄位的意義就很好確定;拖曳類的操作會連送幾筆中間值,以最後一筆為準。
5.3 一項一項解出來的(指令以代號表示)
| 操作 | 對應的指令變化 | 驗證方式 |
|---|---|---|
| 通道開關、耦合 | FE:每通道一 byte,一個位元管開關、一個管 AC/DC | 波形基線位置 |
| 時基 | TB 裡的 D:取樣率=125 MHz/(D+1) | 方波週期點數 |
| Auto/Normal | AR 的一個位元 | Normal 時只在有訊號才讀 |
| 讀取長度 | AR 的 LL:讀取 bytes=LL×256 | 實際讀到的量 |
| 觸發緣 | TB 某個 byte 的一個位元 | 觸發點固定是上升或下降緣 |
| 觸發準位 | TL | 調之前觸發點亂跳,調之後固定 |
| 每格電壓 | FE 路徑與增益、PS、GN 一起變 | 方波振幅(3.3 V = 41 碼 ≈ 2 V/div) |
5.4 「16M 深度壞掉」其實沒壞
Ken 回報:深度設 16M、12M 一直有問題,4M 才正常。查封包發現:1 ms/div(250 kS/s)下,16M 那一檔實際要錄 128 MiB、需要 524 秒才填得滿,觸發點又在正中間,光觸發前就要等 4 分多鐘。Ken 等不到就離開了。不是壞,是太慢。這也讓我們知道畫面上標的「16M」實際是 128 Mi 點。
六、第四階段:在 Linux 上重放(11-07 上午)
6.1 先離線比對,再碰硬體
寫一支 xs_replay.py,三種模式:
python3 xs_replay.py --dry-run # 印出所有要送的傳輸,不碰 USB
python3 xs_replay.py --verify captures/ # 和錄檔逐筆比對,不碰 USB
python3 xs_replay.py --run --firmware <原廠韌體檔> --out w01 # 真的上機
之後隨著解開的欄位,這支程式陸續加了參數,每加一個就上機驗一次:
python3 xs_replay.py --run --firmware <原廠韌體檔> --out w02 --count 60 # 連抓 60 張
python3 xs_replay.py --run --firmware <原廠韌體檔> --out p2625 --pre 2625 # 觸發位置(7.x)
python3 xs_replay.py --run --firmware <原廠韌體檔> --out d16M --depth 16M --mode normal --max-wait 900
python3 xs_replay.py --run --firmware <原廠韌體檔> --out hr --depth 256K --hires # HiRes 開關比較
python3 xs_fast.py --firmware <原廠韌體檔> --out f16 --ll 0x80000 --mode normal # 1 GS/s、128 MiB
python3 xs_multich.py --firmware <原廠韌體檔> --out mc --nch 4 # 4 通道資料格式
存下來的 .bin 是原始位元組,用十幾行 Python 就能檢查:找上升緣、算週期分布、看有沒有缺段,例如:
import collections
b = open("d16M.bin", "rb").read()
hi = bytes(1 if x >= 153 else 0 for x in b) # 以中點為門檻轉成 0/1
e, i = [], hi.find(b"\x00\x01")
while i != -1:
e.append(i + 1); i = hi.find(b"\x00\x01", i + 1)
print(collections.Counter(e[k+1] - e[k] for k in range(len(e) - 1)).most_common(5))
--verify 會把程式要送的東西和錄檔逐筆對:初始化 174 筆、韌體 337 筆、讀取起點 2735 筆全部一致,才上機。上機前先在離線環境抓錯,硬體只用來驗證結果。
6.2 第一次上機
Ken 把示波器接到測試主機。第一次跑就整段走通:下載韌體、初始化、設定、狀態「填前段→等觸發→強制填後段→完成」、讀出 2560 點。60 份方波全對:週期 250 點、工作週期 0.50。
6.3 一路往下解
每一列都是同一個流程:先在錄檔裡找出候選欄位 → 改重放程式 → 上機抓 → 用上面那種小程式檢查週期、觸發點位置、缺段。
| 問題 | 怎麼找 | 結論 |
|---|---|---|
| 觸發點位置(osc_07) | 標記法錄「中央→左 1→左 2→歸中→右 1→右 2」,Ken 指定時基 1 ms/div、一格=一個週期 | TB 的 C=觸發後長度,每格 500;用不是整數週期的位置上機,方波相位照比例移動 |
| 深度讀取從哪讀 | 錄檔 10 筆深讀取逐一比對狀態回應 | 狀態區塊裡某 4 個 byte 是記錄的結束位址,起點=結束位址-長度 |
| 兩種「完成」狀態碼 | 測試訊號改成「5 秒有、10 秒沒有」,連抓 200 張 | 一個表示真的觸發,另一個表示 Auto 等不到而強制擷取 |
| 128 MiB 能不能讀完 | 250 kS/s 錄 537 秒 | 9 分鐘的記錄裡,方波段週期一直正確,35 個 10 秒空白都剛好 2,500,248 點,沒有缺段 |
| ADC 是幾 bit | 手冊、SDK、傳輸量、資料最低位元、HiRes 開關 | 四方面都指向 8 bit |
順帶一提,同機 VM 的坑:第一次讀 2.56M 時中途斷線。核心日誌顯示 VMware 一直想接管這個 USB 裝置,10 分鐘重置了 2478 次,連同一個 hub 上的其他裝置都受影響。之後規則:Linux 端要用時,VM 必須先放開示波器。查法:
journalctl -k --since "-10 min" | grep -c "reset high-speed" # 次數暴增就是有人在搶
journalctl -k --since "-10 min" | grep -i usbfs | tail -3 # 會看到 vmx-vcpu 宣告要用這個介面
6.4 1 GS/s:自己做 75 ns 的測試訊號
Ken 指出深度的意義:時基很小時,深記憶體能涵蓋很長的時間(遠超過一頁畫面能表示的)(1 GS/s 下 128 Mi 點約 134 ms)。要驗證 1 GS/s,需要很快的訊號。Ken 給了一段直接寫 ESP8266 GPIO 暫存器的程式碼,要我用 SET/CLEAR 暫存器連續寫:
IRAM_ATTR volatile unsigned &PIN_OUT_SET = *((unsigned*)0x60000304);
IRAM_ATTR volatile unsigned &PIN_OUT_CLEAR = *((unsigned*)0x60000308);
#define IO02 0x00004
#define P PIN_OUT_SET = IO02; PIN_OUT_CLEAR = IO02;
static uint32_t IRAM_ATTR burst(uint32_t n16) { // 迴圈本體展開 16 組
uint32_t t0 = ccount();
for (uint32_t i = 0; i < n16; i++) { P P P P P P P P P P P P P P P P }
return ccount() - t0;
}
- 160 MHz 下每次寫入剛好 12 個 CPU 週期=75.00 ns,等於 6.667 MHz 方波。
- 函式要加
IRAM_ATTR(Ken 提醒),每 100 ms 開一次中斷、餵看門狗。
編譯、燒錄、看它自己回報的實際速度(同一份程式用編譯參數切換延遲版與全速版):
FQBN="esp8266:esp8266:generic:xtal=160,eesz=4M3M,FlashMode=dout,baud=115200"
arduino-cli compile --fqbn "$FQBN" --build-property "compiler.cpp.extra_flags=-DDELAY_US=0" \
--build-path build_fastsq2_0 fastsq2
esptool.py --chip esp8266 --port /dev/ttyUSB0 --baud 115200 write_flash 0x0 build_fastsq2_0/fastsq2.ino.bin
stty -F /dev/ttyUSB0 115200 raw -echo -hupcl # -hupcl:開序列埠時不要拉 DTR,板子才不會重開
timeout 23 cat /dev/ttyUSB0
板子每輪會印出用 CPU 週期計數器量到的結果,例如:
[FAST OFF] 12.000 cycles per write = 75.00 ns (high+low 150.00 ns, 6.6667 MHz), 33349600 pairs
Ken 在 VM 裡用原廠軟體以最快時基(2 ns/div)錄了 osc_08。把那一組設定原樣重放:週期正好 150 點 ⇒ 1 GS/s。再把長度放大到 128 MiB:一次抓 134 ms,觸發在正中間,唯一一個 9.75 µs 的缺口剛好落在觸發後 2.400 ms——正是測試程式第一段(16000 對×150 ns)結束、停下來餵看門狗的地方。時間軸完全對得上。
另外發現:原廠軟體在快時基下每次只讀畫面要用的幾十 bytes,從來不讀整個深記憶體。
七、第五階段:寫 libsigrok 驅動(11-07)
7.1 準備:從原始碼編 sigrok
git clone https://github.com/sigrokproject/libsigrok.git # sigrok-cli、libsigrokdecode、pulseview 同理
cd libsigrok && ./autogen.sh
./configure --prefix=$HOME/opt/sigrok --disable-python --disable-ruby --disable-java --enable-cxx
make -j8 && make install
需要的套件(Ubuntu 22.04):libtool libtool-bin libzip-dev libglibmm-2.4-dev python3-dev doxygen check autoconf-archive,PulseView 另外要 Qt5 與 Boost(qtbase5-dev qttools5-dev qttools5-dev-tools libqt5svg5-dev libboost-dev libboost-system-dev libboost-filesystem-dev libboost-serialization-dev libboost-test-dev)。裝在自己的目錄,不動系統套件:
export LD_LIBRARY_PATH=$HOME/opt/sigrok/lib
$HOME/opt/sigrok/bin/sigrok-cli -d xscope --scan
新驅動要在 configure.ac 加一行 SR_DRIVER(...)、Makefile.am 加原始檔清單。結構參考同為 FX2+示波器的既有驅動:api.c(掃描、開關、設定選項、開始/停止)、protocol.c(實際的 USB 指令)、protocol.h。
實際加的內容(驅動名以 xscope 代稱):
# configure.ac
SR_DRIVER([X scope], [xscope], [libusb])
# Makefile.am
if HW_XSCOPE
src_libdrivers_la_SOURCES += \
src/hardware/xscope/protocol.h \
src/hardware/xscope/init_seq.h \
src/hardware/xscope/protocol.c \
src/hardware/xscope/api.c
endif
加完要重跑 ./autogen.sh && ./configure …,configure 的摘要裡會列出新驅動是否啟用(- xscope .... yes)。
日常使用的 sigrok-cli 指令:
sigrok-cli -d xscope --show # 列出所有選項與目前值
sigrok-cli -d xscope --config samplerate=250k:limit_samples=2560 \
--frames 1 -O csv -o one.csv # 抓一張存 CSV
sigrok-cli -d xscope -C CH1,CH4 --config samplerate=1G:limit_samples=1000000:triggersource=CH4 \
--frames 1 -O csv -o two.csv # 兩通道、1 GS/s(每通道 500 MS/s)
sigrok-cli -d xscope --config samplerate=1G:limit_samples=134217728:triggersweep=Auto \
--frames 1 -O srzip -o deep.sr # 128 MiB 存成 PulseView 可開的 .sr
sigrok-cli -d xscope --config triggerlevel=1.65:triggerslope=f:captureratio=20 \
--frames 5 -O csv -o five.csv # 準位、下降緣、觸發前 20%、連拍 5 張
加 -l 4 可以看到驅動的除錯訊息(送出的 TB、狀態變化、每張記錄是真觸發還是 Auto)。
7.2 讓 Python 驗證過的東西直接變成 C
初始化的 174 筆(84 道指令各有 RQ 與指令本體兩筆,再加讀校正資料與狀態)不手抄:用 Python 從驗證過的表自動產生 init_seq.h,兩邊不會不一致。韌體不放進驅動:使用者把原廠韌體檔放到 sigrok 的韌體目錄,驅動用 RQ 探測,沒韌體就自己解析 Intel HEX 下載,等重新列舉後依同一個 USB 路徑重新打開。
第一次用 sigrok-cli 擷取就成功:2560 點、0~3.32 V、觸發緣在第 1281 點(正中央)。
7.3 寫驅動時順便解開的規則
- 時間欄位的單位是 2.048 µs(256×8 ns):總長 T=(讀取長度÷通道交錯數)×N,所有錄到的時基(20 µs~5 ms/div、2 ns/div)都吻合。驅動因此可以自己算任何取樣率、任何深度要送的值,不必照抄錄檔。
CF跟取樣率有關:12.5 MS/s 以上要送一個值,1.25 MS/s 以下送另一個。照錯就等不到就緒。- libsigrok 的一個 bug:sigrok-cli 寫
samplerate=12.5M一直被拒絕。追進sr_parse_sizestring():讀完小數後迴圈又多跳一個字元,把單位 M 吃掉,變成 12 Hz。本機補一行修好,記下來要回報上游。
7.4 多通道:先建模型,再寫
把每個錄檔「每次讀取當下的完整設定」都拍下來,依開了哪些通道分組比對。關鍵發現:一直以為是同一個暫存器的某種 FE 指令,其實是寫 ADC 晶片內部暫存器(FE 裡帶著 ADC 暫存器位址和 16 位元的值)。這顆 ADC 有 4 個核心,每個 250 MS/s:
- 1 通道:4 個核心都接同一路 ⇒ 1 GS/s。
- 2 通道:兩兩分 ⇒ 每路 500 MS/s。
- 3~4 通道:一核心一路 ⇒ 每路 250 MS/s。
板子上 CH1 接 ADC 的第 4 路輸入、CH4 接第 1 路,錄到的 6 種組合全部吻合。Ken 把探棒依序換到 CH2、CH3、CH4,每一路都驗證通過。
觸發來源的謎:探棒在 CH2 時,觸發來源選 CH2 一直等不到觸發。錄檔裡 CH2 的觸發碼是 5,但送 5 沒用。既然一次測試只要十幾秒,就把 0~15 全部試一遍:多通道時 1 會觸發、只開 CH2 時 0 會觸發。⇒ 觸發碼其實是「觸發通道在資料交錯中的位置」。錄檔的 5 多了一個位元,是因為當時那些通道沒開——那種值本來就不會觸發。
做法是在驅動裡暫時加一個測試用的環境變數,直接覆蓋觸發碼,再用 shell 迴圈跑一遍(確定規則後就拿掉了):
for nib in $(seq 0 15); do
XS_TRIG_NIBBLE=$nib timeout 18 sigrok-cli -d xscope -C CH1,CH2 \
--config samplerate=250k:limit_samples=2560:triggersource=CH2 --frames 1 -O csv -o t.csv
[ -s t.csv ] && echo "$nib: triggered" || echo "$nib: no trigger"
done
Normal 模式等不到觸發就不會產生資料,所以「檔案有沒有內容」就是判斷依據;18 秒逾時涵蓋測試訊號 15 秒一輪的週期。
1 GS/s 多通道的資料排列:第一次 2 通道快模式的資料亂掉,週期出現 4、40 這種怪數字。把 CSV 還原回原始位元組流才看出:(還原方法:把每個電壓值用當時的換算反推回碼值 round(V / 每碼伏特 + 零點),再依通道順序排回去)快模式不是逐點交錯,而是一段一段輪流——2 通道每 4 點一段(a a a a b b b b),4 通道每 2 點一段(1 1 2 2 3 3 4 4)。改完後 CH4 在 2 通道是 75 點一個週期(500 MS/s)、4 通道 37.5 點(250 MS/s),都對。
7.5 電壓換算與觸發準位
- Ken 不想動實體接線:直接用測試訊號的 0 V/3.3 V 兩個電位做每通道的零點與比例。四個通道各自不同,零點最多差 12 碼(CH3)。
- 觸發門檻在
TL裡每個 ADC 核心一組 (L−6, L, L, L+6):門檻 L、上下各 6 碼遲滯。實測:0.5~3.0 V 都觸發、超出訊號範圍(4 V、-0.5 V)都不觸發。 - PulseView 原本不顯示觸發準位。本機加了幾行,讓它出現「Trigger level」伏特輸入框;觸發位置則改用 PulseView 原本就支援的「Pre-trigger capture ratio」。
測試主機沒有接螢幕,PulseView 是開在虛擬顯示器上,用 xdotool 點擊、ImageMagick 截圖:
setsid Xvfb :51 -screen 0 1600x900x24 & # 虛擬顯示器(不影響實體桌面)
setsid env DISPLAY=:51 fluxbox & # 輕量視窗管理器
setsid env DISPLAY=:51 LD_LIBRARY_PATH=$HOME/opt/sigrok/lib $HOME/opt/sigrok/bin/pulseview -d xscope &
export DISPLAY=:51
xdotool mousemove 65 15 click 1 # 按 Run(座標從截圖量)
import -window root /tmp/pv.png # 截圖確認
要讓人一起看,可以再加 x11vnc -display :51 -shared -forever,用一般 VNC 程式連進去。另一個坑:PulseView 收到 SIGTERM 會跳確認視窗、不會結束,示波器也就一直被占著,要用 kill -9。
7.6 速度:先量,再改
驅動裡在「讀 USB」和「轉換+送出」兩段前後各記一次 g_get_monotonic_time(),一張記錄結束時印出累計:
sr: xscope: Frame 1 done (auto). usb 5.97 s, convert+send 27.40 s (convert 0.08 s).
這行一出來,就知道不必再優化驅動,該往 sigrok-cli 找。
128 MiB 用 sigrok-cli 不存檔也要 38 秒。在驅動裡分段計時:讀 USB 6 秒、轉電壓 0.08 秒,送出 27 秒。perf 被系統設定擋著(不改它),就直接讀 sigrok-cli 原始碼,找到原因:只要有輸出檔,它就另外把每個類比點格式化成文字當備援,-O null 也照做。改成「有指定輸出格式就不做」後降到 12 秒。USB 22 MB/s 是示波器端上限(每次讀 64 KB 或 4 MB 都一樣)。PulseView 抓 128 MiB 只要 7 秒,畫面立即出來。
7.7 實際畫面
以下都是 Linux 上的 PulseView 透過這支驅動抓的,探棒接 CH2,訊號來自一片 ESP8266 測試板。PulseView 內建 A/B 游標:兩條線之間直接標出時間差與換算的頻率。



深度的用處在這裡:時基很小時一頁只看得到幾百 ns,但同一次觸發,記憶體記下的是整整 128 MiB。下面五張是同一份記錄,只是縮放、捲動不同:





八、分層整理
Ken 提醒:為最上層做的客製化,不要寫死在下層,要透過介面接到較普適的下一層。回頭檢查後改了兩件事:
- 校正值移出驅動:
・驅動只保留這個型號通用的名目換算。
・數值放進校正檔(純文字,每通道一行)。
・校正檔綁定機器:初始化時讀到的那三塊原廠校正資料,每次開機都一模一樣,就拿它算一個 64 位元的識別碼。檔案不是這台的就不用。
・另寫一支xs_cal.py自動量測並寫檔:
python3 xs_cal.py measure CH2 # 該通道接 0 / 3.3 V 方波
python3 xs_cal.py show
校正檔的長相(數值為示意):
# xscope calibration, written by xs_cal.py 2026-11-07 17:31
# setting: 2 V/div, DC; zero = code at 0 V, vpc = volts per code
unit 0123456789abcdef
CH1 zero 132.4 vpc 0.0807 # set by hand, swing 40.9 codes for 3.300 V
CH2 zero 134.1 vpc 0.0803 # measured 2026-11-07, 125819+126085 samples, swing 41.1 codes
驅動開啟時會印出 Init done, unit … 和 Calibration …: 4 channel(s);檔案的 unit 不符時印出 … is for unit …, this scope is …: not used.,改用名目值。
- Auto/Normal 從觸發來源分出來:本來為了 PulseView 能選,把模式塞進觸發來源字串(”CH1 auto”)。改成在 libsigrok 新增一個通用設定鍵「觸發掃描模式」(Auto/Normal),PulseView 再把它加進下拉選單。這是可以提給上游的做法。
九、方法上的心得
- 意義用資料驗證,不用操作順序推:耦合解反就是因為照步驟表推。
- 在錄檔裡插書籤:Ken 的 CH4 開關標記法,讓每一段操作都能精準切出來。
- 一次只改一件事、上機前先離線比對:重放程式先和錄檔逐筆一致,硬體只用來確認。
- 便宜的實驗就窮舉:觸發碼 0~15 試一遍,比猜快得多。
- 自己做測試訊號:1 kHz、5 秒有/10 秒沒有、75 ns,每一種都是為了回答一個具體問題。
- 先量再優化:拖累的地方常常不在你以為的那裡(這次在 sigrok-cli,不在驅動)。
- 分層:機器專屬的數值、前端的習慣、通用的協定,各放各的層。
- 筆記要完整:包括走錯的推論和訂正,這篇文章就是從筆記整理出來的。
十、現況與還沒做的
已經能用:任意通道組合;2.5 kS/s~25 MS/s 與 1 GS/s(多通道時平分);深度到 128 MiB;邊緣觸發(來源、緣、準位、位置、Auto/Normal);連拍;PulseView 與 sigrok-cli。
還沒做:
- 2 V/div 以外的每格電壓(原廠校正資料還沒解開)。
- 脈寬、視訊觸發(還沒錄封包)。
- 1 µs、2 µs 那兩檔取樣率。
要回報上游的(sigrok 專案):取樣率字串解析的 bug、sigrok-cli 多餘的文字輸出、觸發掃描模式的新設定鍵、PulseView 的觸發準位欄位等共 7 項,以及這個新驅動本身,預定之後一併提交。
附錄:本文提到的檔案
| 檔案 | 用途 |
|---|---|
tools/pcap_*.py | 錄檔解析 |
tools/xs_replay.py | Python 重放(慢時基,含 --verify) |
tools/xs_fast.py | Python 重放(1 GS/s) |
tools/xs_multich.py | 多通道資料格式驗證 |
tools/xs_cal.py | 校正檔工具 |
driver/(api.c、protocol.c、protocol.h、init_seq.h) | libsigrok 驅動 |
SPEC_protocol.md | 協定規格整理 |
原廠韌體與原廠檔案的版權屬原廠,本文與驅動都不附帶,使用者需自行從原廠軟體取得。
發佈留言