What Cascade does · USB & APIB
Chris asked for this page: "I would like a page in The Correction Log about how the app communicates with the instrument and the USB-APIB interface. there is a page in tehre now, but it does not cover technical details about the USB protocol translating the commands and things like that> I would like to understand that more fully. One of the reasons i am interested in this, is that there may be a way to make an APIB to Ethernet interface. Companies like Prologix have done this with GPIB controllers, for instance."
Everything below comes from three places, and each statement says which: the app's code (Cascade/Cascade/APDevice.swift), USB captures of APWIN (T92, a three-minute capture of 111,118 commands, is used for the counts and timings), and AP's manuals. Where something is a guess, it says so.
Cascade (macOS) ──libusb──▶ USB cable ──▶ S1/S2 USB adapter ──APIB cable──▶ SYS-2522
APWIN (Windows) ──driver──▶ USB cable ──▶ (the same adapter) ──APIB cable──▶ SYS-2522
APWIN, originally ──ISA bus──▶ ISA-WIN card, 8 I/O ports at 0x2B8 ──APIB cable──▶ System Two
0x16D0, product ID 0x0AF0 (repo protocol_notes.md, APDevice.swift). It replaces the interface card AP shipped: an ISA card that took eight I/O port addresses, 0x2B8 by default, selectable with two jumpers (installation guide p.75).:AGEN:FREQ 1000HZ and does the work itself (GPIB reference p.8, p.12–14). Over APIB nothing like that happens: the PC reads and writes the instrument's registers directly and does all the thinking (uploading DSP programs, waiting for readings to settle, scaling results). A rear switch chooses one or the other; in APIB mode the GPIB port goes dead.What the app does at connect, verbatim from APDevice.swift (and the same as APWIN in the captures):
libusb_open_device_with_vid_pid(0x16D0, 0x0AF0)
set_configuration(1) if not already; claim_interface(0)
control IN bmRequestType 0xC0, bRequest 0x54, wValue 0, wIndex 0, length 2 → replies 00 0C
control OUT bmRequestType 0x40, bRequest 0x53, wValue 0xFFFF, wIndex 0, length 0
control OUT bmRequestType 0x40, bRequest 0x55, wValue 0x0000, wIndex 0, length 0
After those three vendor requests, everything is bulk transfers: commands out on endpoint 0x06, replies in on endpoint 0x82. The code's comments call the adapter's chip an "FX2" (a common Cypress USB microcontroller); that has not been checked against the device's own descriptors.
Each command is one bulk OUT transfer. If the command produces a reply, the reply is the next bulk IN transfer, before the next command is sent; many commands produce none. (Getting this pairing rule wrong once produced about 4,000 false differences in a replay-versus-capture comparison: docs/usb_pairs.py.)
The first byte is the opcode. These are all the opcodes in T92, with how many times each appears:
| opcode | length out | reply | count in T92 | what it does | how sure |
|---|---|---|---|---|---|
8A | 4 or 5 | none | 16,356 | write a register: 8A <width> <reg> <data…> | known |
EB | 3 | 1 or 2 | 55,482 | read a register: EB <width> <reg> | known |
E6 | 4 | 2 | 17,440 | write/arm a DSP port: E6 <dsp> 0A <value> → 01 <echo> | known |
E9 | 3 | 5 | 9,531 | read a DSP value with status: E9 <dsp> 0A → 01 <status> <24-bit value> | known |
ED | 7 | 5 | 9,306 | DSP read at an address | partly |
EC | 11 | 1 | 1,239 | write one DSP parameter: EC <dsp> 0A 00 00 <addr> 2A <3 data bytes> <flag> → 01 (accepted) or 00 (refused, DSP busy) | known |
E8 | 6 | 2 | 460 | DSP transaction, used while loading coefficients | partly |
0B | 6 | 2 | 378 | a combined write-then-read (0B 01 37 EB 01 37) | partly |
08 | 18–192 | 5 or 64 | 207 | DSP program block upload (the 192-byte ones reply with 64 bytes) | known |
EF | 66 | 2 | 487 | DSP calibration data block | partly |
03 | up to 3,098 | none | 26 | large data block | unknown content |
83 | 5–26 | none | 49 | data block, variant | unknown |
82 | 2 | none | 115 | select a slot or module (82 <slot>) | partly |
02 | 4 | 1 | 39 | read a module's identity (slot enumeration, EEPROM) | known |
01 | 3 | 1 | 3 | identify the device | known |
Two observations about the structure, both inferences rather than documentation:
E… family always get a reply; 8A, 82, 83 and 03 never do. It looks like a bit in the opcode decides whether the adapter waits for the bus to answer.8A and EBFor six months the app's code named the second byte of 8A and EB a "module" (0x01 system, 0x02 "sys2"). It is a width. In all 72,000 of these commands in T92, 8A 01 … is always 4 bytes long (one data byte) and 8A 02 … always 5 (two data bytes); EB 01 xx always returns one byte and EB 02 xx always two. The third byte is the register address, in one flat space. (Found while writing this page, 2026-10-21; the code still uses the old names.)
Examples, all verified on the instrument:
8A 02 21 7D A8 generator frequency word: 7000 Hz (two bytes, little-endian)
8A 01 20 81 generator mode: analog IMD
8A 01 2B 48 generator output: balanced, floating, 40 Ω
EB 02 1C → 70 44 channel A level mantissa: 0x4470 = 17520 (T92, during SMPTE)
EB 01 0D → 0D the instrument's handshake counter
Two-byte values are little-endian. An early note read the second byte of a level reply (7C) as a "valid" flag; it is the high byte of the number.
E6, E9, ED, EC, 08The second byte of these is a DSP address: 0x30 and 0x38 are analyzer channels A and B, 0x40 and 0x48 the generator side (the app's naming, from captures). The third byte is almost always 0A, a DSP port. E9 replies carry a status byte (0x86 busy, 0x87 done) ahead of a 24-bit fixed-point value.
The DSP programs travel over this link too: APWIN uploads them, 207 08 blocks in T92's re-initialisation. That is a large part of why every Cascade measurement begins by replaying a recorded setup.
Measured in T92 from the capture's own timestamps:
| quantity | value |
|---|---|
| time from a command to its reply, typical | about 100 µs (median 96 µs for EB, 97 for E6, 125 for E9, 194 for EC, 224 for ED) |
a DSP program block (08) | about 1,700 µs |
| gap between one command and the next | median 110 µs |
| commands with a reply | about 85 % |
| average rate over the whole capture | 613 commands a second (APWIN spends much of its time waiting) |
How many commands real work takes, from the app's own recordings:
| task | commands |
|---|---|
| FFT setup replay | 122,703 |
| T92 replay up to SMPTE IMD | 57,053 |
| THD setup (GenMon, −20 dBV) | 50,396 |
| connect: cold start + generator config + measurement setup | 40,065 + 600 + 13,237 |
And it is a conversation, not a stream. APWIN, and Cascade, poll until the instrument says ready before the next write: a DSP write sent while the DSP is busy is refused (EC replies 00), and in October that left a replay broken until the next one. The instrument also keeps its state across USB disconnects (a hung run in October needed a power cycle).
The comparison Chris drew, Prologix, works for GPIB because GPIB is the easy case: a published standard (IEEE-488), and an instrument that understands text. A box can take a line of text from the network and put it on the bus, and the round trip hardly matters, because one command does a lot of work.
APIB is the hard case: tens of thousands of small register transactions per measurement, most of them waiting on a reply. So the question is not whether the bytes can travel over Ethernet (they can) but where the waiting happens. Three ways to build it:
1. Tunnel the USB. Put the existing adapter on a small computer next to the instrument and forward the USB traffic over the network (Linux has a standard way to do this, USB/IP). Cascade would not change. But every reply-bearing command pays a network round trip instead of 100 µs. On a wired network a round trip is commonly a few hundred µs; on Wi-Fi often several ms (typical figures, not measured here). For the 122,703-command FFT setup that is roughly the difference between tens of seconds and many minutes. It works; it is slow.
2. Network the commands, not the USB (recommended to try first). The same small computer and adapter, but running a small server that speaks this opcode protocol. Cascade sends commands over TCP instead of libusb; the server sends them to the adapter and returns the replies. The win comes from moving the waiting to the instrument's side:
8A, 82, 03) are streamed without waiting;Cascade's code already funnels every transaction through a few functions, so swapping the transport is a contained change. Nothing about the instrument has to be learned that we don't already know.
3. A true APIB-Ethernet adapter, like a Prologix box: a microcontroller with Ethernet on one side and the APIB connector on the other, replacing the USB adapter entirely. This needs what we don't have: APIB's electrical layer and bus timing. Getting it means putting a logic analyzer on the APIB cable between today's adapter and the instrument while a known command sequence runs, and matching bus activity to the USB commands above. It is the most work, and the most interesting. It could also absorb approach 2's server into the box.
Approach 2 gives a networked Cascade with the hardware already on the bench and teaches us which operations need to live next to the instrument, which is the design input approach 3 would need anyway.
03, 83 and much of the register space.diag_usb.py in the repo root lists them; one run settles it next time the adapter is connected).