Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions HISTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -427,3 +427,79 @@ Records key decisions, structural changes, and completed development stages.
root cause remains open. Treated as a known, unresolved bug pending
further hardware-side USB capture analysis.

## 2026-09-07

### Root cause found - DSO-2250 needs the extended 0x0b-0x0f command group

- Correlated the Windows reference-driver capture against the application's
own USB capture with `tshark` and found the application implements only the
DSO-2090 command set (`0x00`-`0x07`), while the DSO-2250 requires the
extended acquisition-configuration commands `0x0b`-`0x0f`. The DSO-2090
`SetTriggerAndSampleRate` command (`0x01`) is silently ignored by the
DSO-2250, so the capture engine never arms and `GetCaptureState` stays in
its power-on state forever.
- Bulk-OUT command comparison (reference vs application): `0x01` sent 0 vs 2
(the ignored 2090 command), `0x05` GetData 237 vs 0 (analog data never
read), `0x09` GetLogicalData 78 vs 0, and each of `0x0b`-`0x0f` 78 vs 0
(the arming group is entirely absent from the application).

### Recorded - full DSO-2250 bulk-command semantics from OpenHantek

- Documented the DSO-2250 bulk-command semantics decoded from the OpenHantek
project (`hantekprotocol/bulkcode.h`) for future protocol work:
- `0x00` SetFilter, `0x01` SetTriggerAndSampleRate (DSO-2090 only, ignored
by the DSO-2250), `0x02` ForceTrigger, `0x03` StartSampling, `0x04`
EnableTrigger, `0x05` GetData, `0x06` GetCaptureState, `0x07` SetGain.
- `0x08` SetLogicalData, `0x09` GetLogicalData, `0x0a` unknown.
- `0x0b` BSetChannels - enabled-channel selection, `[0b][00][channels][00]`.
- `0x0c` CSetTriggerOrSampleRate - trigger source/bits,
`[0c][00][triggerBits][00]...`.
- `0x0d` DSetBuffer - record-length id, `[0d][00][recordLenId][00]`.
- `0x0e` ESetTriggerOrSampleRate - sample-rate bits,
`[0e][00][srBits][00][sr0][sr1][00][00]`.
- `0x0f` FSetBuffer - trigger position, `[0f][00][postTrig0-2][00]`
`[preTrig0-2][00][00][00]`.
- Completed-capture state value for the DSO-2250 is `3`
(`CAPTURE_READY2250`).
- Recorded the known-good reference configuration bytes captured immediately
before the first successful capture, for the arming group used below:
`0x0c` `0c 0f 02 00 02 00 00 00`, `0x0b` `0b 0f 00 00`, `0x0d`
`0d 0f 01 00`, `0x0e` `0e 00 01 00 00 00 00 00`, `0x0f`
`0f 00 fe d7 07 00 fe ff 07 00 00 00`, sent in the order
`0c, 0b, 0d, 0e, 0f`.

### Stage 4 - Send the DSO-2250 acquisition-configuration command group

- Replaced both DSO-2090 `SetTriggerAndSampleRate` (`0x01`) sends in
`configureCapture()` with a new `configureDso2250Timebase()` helper that
issues the extended arming group `0x0c, 0x0b, 0x0d, 0x0e, 0x0f` using the
known-good reference bytes, reusing the existing begin-command/speed-check
bulk framing.
- Prepended `0x09` GetLogicalData to the arming group and drained the single
512-byte bulk-IN (`0x86`) response it returns, mirroring the reference
driver, so the logic/auto-range subsystem is initialised the same way
before analog capture.
- Removed the now-unused DSO-2090 timebase constants and added the DSO-2250
command payloads as named constants.
- Kept the calibration-derived `SetOffset` (`0xB4`) write, relay
(`0xB5`) configuration, and calibration reads (`0xA2`) unchanged.

### Stage 4 - Corrected the capture read size and confirmed the data path

- Verified on hardware that the arming group works: the acquisition error
changed from silent no-waveform to `USB timeout while reading channel
data`, proving the capture engine now reaches `captureCompleteState` (```3```)
and accepts the `0x05` GetData command.
- Measured the true capture size from the reference dump with `tshark`: one
capture is 40 bulk-IN (`0x86`) packets of 512 bytes = 20480 bytes = 10240
samples per channel (two channels interleaved), selected by record-length
id `1` (`0x0d 0f 01 00`). The profile declared `sampleCount = 32768`, so
the application requested 128 packets, received 40, and blocked on the
41st packet until the read timed out.
- Corrected `sampleCount` from `32768` to `10240` in both DSO-2250 USB
profiles (bootloader and operational) in `usb/src/usb_device.cpp`; the
fixed sample and packet buffers accommodate the smaller size unchanged.
- Confirmed on hardware: the oscilloscope now returns valid waveform data
over USB. On-screen rendering is not yet implemented, so Stage 4 remains
open until the captured samples are drawn.

175 changes: 133 additions & 42 deletions capture/src/acquisition_loop.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -70,15 +70,43 @@ static const unsigned int kForceRestartThreshold = 3U;
*/
static const uint8_t kDefaultVoltageRangeCode = 0U; /**< VOLTAGE_5V */
static const uint8_t kDefaultCouplingDc = 0U; /**< COUPLING_AC */
static const uint8_t kDefaultSelectedChannel = 2U; /**< SELECT_CH1CH2 */
static const uint8_t kDefaultTriggerSource = 1U; /**< TRIGGER_CH1 */
static const uint8_t kDefaultTriggerSlope = 0U; /**< SLOPE_POSITIVE */
static const uint8_t kDefaultSampleSizeCode = 2U; /**< BUFFER_LARGE */
static const uint16_t kDefaultTimeBaseValue = 0xFFF7U; /**< TIME_1ms preset */
/** 1ms/div is past the fast range */
static const uint8_t kDefaultTimeBaseFastCode = 4U;
/** No-offset trigger position */
static const uint32_t kDefaultTriggerPosition = 0x77660U;

/**
* @brief DSO-2250 acquisition-configuration command group (0x0b-0x0f)
* @details The DSO-2090 setTriggerNSampleRate command (0x01) does NOT arm
* the DSO-2250 capture engine. This model needs the extended command group
* 0x0b-0x0f - set channels, trigger source, record length, sample rate and
* trigger position. Without it GetCaptureState never leaves its power-on
* state and no waveform is ever returned. The byte values below reproduce
* the known-good sequence captured from the vendor Windows driver (see
* WorkingDocs USB captures): both channels, internal CH1 trigger, large
* record buffer and a centered trigger position. TEMPORARY fixed
* configuration until Stage 5 wires the Timebase/Scale/Trigger UI controls.
*/
static const uint8_t kDso2250SetTriggerSource[8] = {
0x0CU, 0x0FU, 0x02U, 0x00U, 0x02U, 0x00U, 0x00U, 0x00U
};
/** Channel-enable command: both channels active */
static const uint8_t kDso2250SetChannels[4] = {
0x0BU, 0x0FU, 0x00U, 0x00U
};
/** Record-length command: large capture buffer */
static const uint8_t kDso2250SetRecordLength[4] = {
0x0DU, 0x0FU, 0x01U, 0x00U
};
/** Sample-rate command: default acquisition rate */
static const uint8_t kDso2250SetSampleRate[8] = {
0x0EU, 0x00U, 0x01U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U
};
/** Trigger-position command: centered post/pre-trigger window */
static const uint8_t kDso2250SetTriggerPosition[12] = {
0x0FU, 0x00U, 0xFEU, 0xD7U, 0x07U, 0x00U,
0xFEU, 0xFFU, 0x07U, 0x00U, 0x00U, 0x00U
};
/** GetLogicalData command: primes the logic/auto-range subsystem */
static const uint8_t kDso2250GetLogicalData[2] = {
0x09U, 0x00U
};

/**
* @brief Channel/trigger offset DAC configuration sent once before the
Expand Down Expand Up @@ -158,6 +186,22 @@ static usb::SUsbTransferResult configureCapture(
EAcquisitionOperation *failedOperation
);

/**
* @brief Sends the DSO-2250 acquisition-configuration command group
* @details Primes the logic/auto-range subsystem with GetLogicalData
* (0x09), draining its bulk-IN response, then issues the extended commands
* 0x0b-0x0f that arm the DSO-2250 capture engine (channels, trigger source,
* record length, sample rate, trigger position). Replaces the DSO-2090
* setTriggerNSampleRate (0x01) command, which the DSO-2250 ignores.
* @param[in] connection USB connection to configure
* @param[out] failedOperation First operation that failed
* @returns Result of the first failed operation or the final successful write
*/
static usb::SUsbTransferResult configureDso2250Timebase(
const usb::SUsbConnection &connection,
EAcquisitionOperation *failedOperation
);

/**
* @brief Computes the centered offset DAC byte for one channel
* @param[in] channelLevels Calibration table read via the channel-level
Expand Down Expand Up @@ -612,6 +656,84 @@ static uint8_t channelLevelCenterByte(

/*----------------------------------------------------------------------------*/

/** @fn configureDso2250Timebase */
static usb::SUsbTransferResult configureDso2250Timebase(
const usb::SUsbConnection &connection,
EAcquisitionOperation *failedOperation
) {
struct SConfigCommand {
const uint8_t *payload; /**< Command bytes */
int length; /**< Number of command bytes */
};
const SConfigCommand commands[5] = {
{
kDso2250SetTriggerSource,
static_cast<int>(sizeof(kDso2250SetTriggerSource))
},
{
kDso2250SetChannels,
static_cast<int>(sizeof(kDso2250SetChannels))
},
{
kDso2250SetRecordLength,
static_cast<int>(sizeof(kDso2250SetRecordLength))
},
{
kDso2250SetSampleRate,
static_cast<int>(sizeof(kDso2250SetSampleRate))
},
{
kDso2250SetTriggerPosition,
static_cast<int>(sizeof(kDso2250SetTriggerPosition))
}
};
uint8_t logicalData[kRawUsbPacketMaxSize];
usb::SUsbTransferResult result = {
usb::EUsbTransferStatus::eSuccess, 0, ""
};
size_t index = 0U;

/* GetLogicalData (0x09) primes the logic/auto-range subsystem and
returns one bulk-IN packet that the vendor driver drains before the
analog arming group; the payload itself is unused here. */
result = executeCommand(
connection,
kDso2250GetLogicalData,
static_cast<int>(sizeof(kDso2250GetLogicalData)),
EAcquisitionOperation::eSetTriggerNSampleRateCmd,
failedOperation
);
if (result.status == usb::EUsbTransferStatus::eSuccess) {
*failedOperation = EAcquisitionOperation::eSetTriggerNSampleRateCmd;
result = usb::bulkRead(
connection,
connection.captureProtocol.bulkInEndpoint,
logicalData,
connection.captureProtocol.bulkInPacketLength,
kTransferTimeoutMs,
kTransferAttempts,
connection.captureProtocol.bulkInPacketLength
);
}

for (index = 0U;
(index < 5U) &&
(result.status == usb::EUsbTransferStatus::eSuccess);
++index) {
result = executeCommand(
connection,
commands[index].payload,
commands[index].length,
EAcquisitionOperation::eSetTriggerNSampleRateCmd,
failedOperation
);
}

return result;
}

/*----------------------------------------------------------------------------*/

/** @fn configureCapture */
static usb::SUsbTransferResult configureCapture(
const usb::SUsbConnection &connection,
Expand All @@ -621,25 +743,6 @@ static usb::SUsbTransferResult configureCapture(
connection.captureProtocol.setFilterCmd, 0x0FU,
0U, 0U, 0U, 0U, 0U, 0U
};
const uint8_t tsrByte1 = static_cast<uint8_t>(
kDefaultTriggerSource |
(kDefaultSampleSizeCode << 2U) |
(kDefaultTimeBaseFastCode << 5U)
);
const uint8_t tsrByte2 = static_cast<uint8_t>(
kDefaultSelectedChannel | (kDefaultTriggerSlope << 3U)
);
const uint8_t triggerNSampleRateCmd[12] = {
connection.captureProtocol.setTriggerNSampleRateCmd, 0U,
tsrByte1, tsrByte2,
static_cast<uint8_t>(kDefaultTimeBaseValue),
static_cast<uint8_t>(kDefaultTimeBaseValue >> 8U),
static_cast<uint8_t>(kDefaultTriggerPosition),
static_cast<uint8_t>(kDefaultTriggerPosition >> 8U),
0U, 0U,
static_cast<uint8_t>(kDefaultTriggerPosition >> 16U),
0U
};
const uint8_t voltageByte = static_cast<uint8_t>(
(2U - (kDefaultVoltageRangeCode % 3U)) |
((2U - (kDefaultVoltageRangeCode % 3U)) << 2U) |
Expand Down Expand Up @@ -684,13 +787,7 @@ static usb::SUsbTransferResult configureCapture(
failedOperation
);
if (result.status == usb::EUsbTransferStatus::eSuccess) {
result = executeCommand(
connection,
triggerNSampleRateCmd,
sizeof(triggerNSampleRateCmd),
EAcquisitionOperation::eSetTriggerNSampleRateCmd,
failedOperation
);
result = configureDso2250Timebase(connection, failedOperation);
}
if (result.status == usb::EUsbTransferStatus::eSuccess) {
result = executeCommand(
Expand Down Expand Up @@ -744,13 +841,7 @@ static usb::SUsbTransferResult configureCapture(
);
}
if (result.status == usb::EUsbTransferStatus::eSuccess) {
result = executeCommand(
connection,
triggerNSampleRateCmd,
sizeof(triggerNSampleRateCmd),
EAcquisitionOperation::eSetTriggerNSampleRateCmd,
failedOperation
);
result = configureDso2250Timebase(connection, failedOperation);
}
if (result.status == usb::EUsbTransferStatus::eSuccess) {
*failedOperation = EAcquisitionOperation::eNone;
Expand Down
4 changes: 2 additions & 2 deletions usb/src/usb_device.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -60,14 +60,14 @@ static const SSupportedDevice kSupportedDevices[] = {
/* The bootloader exposes the bulk pair on alt setting 1. */
{
"Hantek DSO-2250 Bootloader", "DSO2250",
{ 32768U, 512U, 0x02U, 0x86U, 2U, true, 3U, 6U, 5U, 3U, 4U, 2U, 0U, 1U,
{ 10240U, 512U, 0x02U, 0x86U, 2U, true, 3U, 6U, 5U, 3U, 4U, 2U, 0U, 1U,
7U, 0xB5U, 0xA2U, 0xB4U },
core::EInstrumentModel::eHantekDso2250,
0x04B4U, 0x2250U, 0x04B5U, 0U, 1U, true
},
{
"Hantek DSO-2250", "DSO2250",
{ 32768U, 512U, 0x02U, 0x86U, 2U, true, 3U, 6U, 5U, 3U, 4U, 2U, 0U, 1U,
{ 10240U, 512U, 0x02U, 0x86U, 2U, true, 3U, 6U, 5U, 3U, 4U, 2U, 0U, 1U,
7U, 0xB5U, 0xA2U, 0xB4U },
core::EInstrumentModel::eHantekDso2250,
0x04B5U, 0x2250U, 0x04B5U, 0U, 0U, false
Expand Down
Loading