From f1130b00e66a1f915f39f5bf3b5032e43445b251 Mon Sep 17 00:00:00 2001 From: Anton Chernov Date: Sun, 6 Sep 2026 15:41:09 +0300 Subject: [PATCH 1/4] Added waveform voltage/timebase scaling --- CMakeLists.txt | 17 ++++ core/inc/waveform_scaling.h | 84 ++++++++++++++++++ core/src/waveform_scaling.cpp | 61 +++++++++++++ tests/waveform_scaling_test.cpp | 150 ++++++++++++++++++++++++++++++++ 4 files changed, 312 insertions(+) create mode 100644 core/inc/waveform_scaling.h create mode 100644 core/src/waveform_scaling.cpp create mode 100644 tests/waveform_scaling_test.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 5f19640..3b45ba2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -47,6 +47,7 @@ set(SOURCES_LIST capture/src/raw_packet_queue.cpp capture/src/waveform_parser.cpp capture/src/waveform_ring_buffer.cpp + core/src/waveform_scaling.cpp ${imgui_SOURCE_DIR}/imgui.cpp ${imgui_SOURCE_DIR}/imgui_draw.cpp ${imgui_SOURCE_DIR}/imgui_tables.cpp @@ -62,6 +63,7 @@ set(HEADERS_LIST capture/inc/raw_packet_queue.h capture/inc/waveform_parser.h capture/inc/waveform_ring_buffer.h + core/inc/waveform_scaling.h ) set(RAW_PACKET_QUEUE_TEST_SOURCES_LIST @@ -76,6 +78,10 @@ set(WAVEFORM_RING_BUFFER_TEST_SOURCES_LIST tests/waveform_ring_buffer_test.cpp ) +set(WAVEFORM_SCALING_TEST_SOURCES_LIST + tests/waveform_scaling_test.cpp +) + if(CMAKE_BUILD_TYPE MATCHES "Debug") message(STATUS ">>> Debug build") add_compile_definitions(_DEBUG) @@ -103,6 +109,7 @@ target_include_directories(${APP_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/usb/inc ${CMAKE_CURRENT_SOURCE_DIR}/capture/inc + ${CMAKE_CURRENT_SOURCE_DIR}/core/inc ${imgui_SOURCE_DIR} ${imgui_SOURCE_DIR}/backends ) @@ -153,6 +160,16 @@ if(BUILD_TESTING) enable_project_warnings(waveform_ring_buffer_tests) add_test(NAME waveform_ring_buffer COMMAND waveform_ring_buffer_tests) add_dependencies(${APP_NAME} waveform_ring_buffer_tests) + add_executable(waveform_scaling_tests + ${WAVEFORM_SCALING_TEST_SOURCES_LIST} + core/src/waveform_scaling.cpp + ) + target_include_directories(waveform_scaling_tests PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/core/inc + ) + enable_project_warnings(waveform_scaling_tests) + add_test(NAME waveform_scaling COMMAND waveform_scaling_tests) + add_dependencies(${APP_NAME} waveform_scaling_tests) add_test( NAME release_updater COMMAND ${Python3_EXECUTABLE} diff --git a/core/inc/waveform_scaling.h b/core/inc/waveform_scaling.h new file mode 100644 index 0000000..2b784fd --- /dev/null +++ b/core/inc/waveform_scaling.h @@ -0,0 +1,84 @@ +/** + * @file waveform_scaling.h + * @version 0.2.8 + * @authors Anton Chernov + * @date 2026-09-06 + * @date @showdate "%Y-%m-%d" + * @par + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef WAVEFORM_SCALING_H_ +#define WAVEFORM_SCALING_H_ + +/******************************* Included files ******************************/ +#include +#include + +/********************************* Definitions ********************************/ + +namespace oscilloscope { +namespace core { + +/** @brief Oscilloscope grid divisions along the horizontal (time) axis */ +static const double kHorizontalDivisions = 10.0; + +/** @brief Oscilloscope grid divisions along the vertical (voltage) axis */ +static const double kVerticalDivisions = 8.0; + +/** @brief Raw sample value that represents zero volts */ +static const uint8_t kAdcCenterValue = 128U; + +/** @brief Raw ADC counts spanning one vertical division */ +static const double kAdcCountsPerDivision = 256.0 / kVerticalDivisions; + +/** @brief Seconds/division for each supported timebase selection */ +static const double kTimebaseSecondsPerDivision[10] = { + 4.0e-9, 20.0e-9, 100.0e-9, 1.0e-6, 10.0e-6, + 100.0e-6, 1.0e-3, 10.0e-3, 100.0e-3, 1.0 +}; + +/** @brief Volts/division for each supported voltage scale selection */ +static const double kVoltageScaleVoltsPerDivision[8] = { + 0.02, 0.05, 0.1, 0.2, 0.5, 1.0, 2.0, 5.0 +}; + +/********************* Application Programming Interface *********************/ + +/** + * @brief Converts a raw ADC sample to a signed voltage + * @param[in] rawSample Raw 8-bit sample value from the capture buffer + * @param[in] voltsPerDivision Selected voltage scale for the sample's channel + * @returns The sample value in volts, centered on zero at the ADC midpoint + */ +double sampleToVolts(uint8_t rawSample, double voltsPerDivision); + +/** + * @brief Converts a sample index into elapsed capture time + * @param[in] sampleIndex Zero-based index of the sample in the capture + * @param[in] sampleCount Total number of samples spanning the capture + * @param[in] secondsPerDivision Selected timebase for the capture + * @returns The elapsed time in seconds, or zero when sampleCount is zero + */ +double sampleIndexToSeconds( + size_t sampleIndex, + size_t sampleCount, + double secondsPerDivision +); + +} // namespace core +} // namespace oscilloscope +/******************************************************************************/ +#endif //! WAVEFORM_SCALING_H_ diff --git a/core/src/waveform_scaling.cpp b/core/src/waveform_scaling.cpp new file mode 100644 index 0000000..0a7bce4 --- /dev/null +++ b/core/src/waveform_scaling.cpp @@ -0,0 +1,61 @@ +/** + * @file waveform_scaling.cpp + * @version 0.2.8 + * @authors Anton Chernov + * @date 2026-09-06 + * @date @showdate "%Y-%m-%d" + * @par + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/******************************* Included files ******************************/ +#include "waveform_scaling.h" + +/********************* Application Programming Interface *********************/ + +/** @fn oscilloscope::core::sampleToVolts */ +double oscilloscope::core::sampleToVolts( + uint8_t rawSample, + double voltsPerDivision +) { + const double centeredCounts = + static_cast(rawSample) - + static_cast(kAdcCenterValue); + + return (centeredCounts / kAdcCountsPerDivision) * voltsPerDivision; +} +/*----------------------------------------------------------------------------*/ + +/** @fn oscilloscope::core::sampleIndexToSeconds */ +double oscilloscope::core::sampleIndexToSeconds( + size_t sampleIndex, + size_t sampleCount, + double secondsPerDivision +) { + double ret_val = 0.0; + + if (sampleCount != 0U) { + const double captureSeconds = + secondsPerDivision * kHorizontalDivisions; + + ret_val = + (static_cast(sampleIndex) / + static_cast(sampleCount)) * + captureSeconds; + } + + return ret_val; +} +/******************************************************************************/ diff --git a/tests/waveform_scaling_test.cpp b/tests/waveform_scaling_test.cpp new file mode 100644 index 0000000..81d58e8 --- /dev/null +++ b/tests/waveform_scaling_test.cpp @@ -0,0 +1,150 @@ +/** + * @file waveform_scaling_test.cpp + * @version 0.2.8 + * @authors Anton Chernov + * @date 2026-09-06 + * @date @showdate "%Y-%m-%d" + * @par + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/******************************* Included files *******************************/ +#include +#include + +#include "waveform_scaling.h" + +/********************************* Definitions ********************************/ + +namespace { + +using oscilloscope::core::sampleIndexToSeconds; +using oscilloscope::core::sampleToVolts; + +static const double kEpsilon = 1.0e-9; + +/***************************** Private prototypes *****************************/ + +static bool expect(bool condition, const char *message); +static bool nearlyEqual(double actual, double expected); +static bool testSampleToVolts(); +static bool testSampleIndexToSeconds(); + +/****************************** Private functions *****************************/ + +/** @fn expect */ +static bool expect(bool condition, const char *message) { + bool result = condition; + + if (!condition) { + std::cerr << "FAILED: " << message << std::endl; + } + + return result; +} +/*----------------------------------------------------------------------------*/ + +/** @fn nearlyEqual */ +static bool nearlyEqual(double actual, double expected) { + return std::fabs(actual - expected) < kEpsilon; +} +/*----------------------------------------------------------------------------*/ + +/** @fn testSampleToVolts */ +static bool testSampleToVolts() { + bool passed = true; + + passed = + expect( + nearlyEqual(sampleToVolts(128U, 1.0), 0.0), + "Midpoint sample must map to zero volts" + ) && passed; + passed = + expect( + nearlyEqual(sampleToVolts(0U, 1.0), -4.0), + "Minimum sample must map to -4 divisions worth of volts" + ) && passed; + passed = + expect( + nearlyEqual(sampleToVolts(255U, 1.0), 3.96875), + "Maximum sample must map to the topmost division fraction" + ) && passed; + passed = + expect( + nearlyEqual(sampleToVolts(160U, 0.5), 0.5), + "One division above center must scale with volts/division" + ) && passed; + passed = + expect( + nearlyEqual(sampleToVolts(96U, 0.5), -0.5), + "One division below center must scale with volts/division" + ) && passed; + + return passed; +} +/*----------------------------------------------------------------------------*/ + +/** @fn testSampleIndexToSeconds */ +static bool testSampleIndexToSeconds() { + bool passed = true; + + passed = + expect( + nearlyEqual(sampleIndexToSeconds(0U, 1000U, 1.0e-3), 0.0), + "First sample must be at time zero" + ) && passed; + passed = + expect( + nearlyEqual( + sampleIndexToSeconds(500U, 1000U, 1.0e-3), + 5.0e-3 + ), + "Midpoint sample must be at half the total capture time" + ) && passed; + passed = + expect( + nearlyEqual( + sampleIndexToSeconds(1000U, 1000U, 1.0e-3), + 10.0e-3 + ), + "Sample index at sampleCount must reach the full capture time" + ) && passed; + passed = + expect( + nearlyEqual(sampleIndexToSeconds(5U, 0U, 1.0e-3), 0.0), + "Zero sampleCount must not divide by zero" + ) && passed; + + return passed; +} + +} // namespace + +/********************* Application Programming Interface *********************/ + +/** @fn main */ +int main() { + bool passed = true; + int result = 1; + + passed = testSampleToVolts() && passed; + passed = testSampleIndexToSeconds() && passed; + if (passed) { + result = 0; + } + + return result; +} +/******************************************************************************/ From 31b274c23c4b5d1451851e0a01029ba9a1a7088c Mon Sep 17 00:00:00 2001 From: Anton Chernov Date: Sun, 6 Sep 2026 15:42:51 +0300 Subject: [PATCH 2/4] Applied scaling to waveform status display --- app/main.cpp | 50 ++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 46 insertions(+), 4 deletions(-) diff --git a/app/main.cpp b/app/main.cpp index 5645b49..672659c 100644 --- a/app/main.cpp +++ b/app/main.cpp @@ -84,11 +84,16 @@ #include "acquisition_loop.h" #include "usb_device.h" +#include "waveform_scaling.h" using oscilloscope::capture::SAcquisitionLoop; using oscilloscope::capture::SWaveformSamples; using oscilloscope::capture::EAcquisitionOperation; using oscilloscope::capture::EAcquisitionState; +using oscilloscope::core::kTimebaseSecondsPerDivision; +using oscilloscope::core::kVoltageScaleVoltsPerDivision; +using oscilloscope::core::sampleIndexToSeconds; +using oscilloscope::core::sampleToVolts; using oscilloscope::usb::EScanStatus; using oscilloscope::usb::EUsbTransferStatus; using oscilloscope::usb::SUsbConnection; @@ -335,6 +340,19 @@ int main (void) { "500 mV/div", "1 V/div", "2 V/div", "5 V/div" }; + static_assert( + IM_ARRAYSIZE(timebases) == + sizeof(kTimebaseSecondsPerDivision) / + sizeof(kTimebaseSecondsPerDivision[0]), + "Timebase labels must match the scaling table" + ); + static_assert( + IM_ARRAYSIZE(voltageScales) == + sizeof(kVoltageScaleVoltsPerDivision) / + sizeof(kVoltageScaleVoltsPerDivision[0]), + "Voltage scale labels must match the scaling table" + ); + while (running) { SDL_Event event; while (SDL_PollEvent(&event) != 0) { @@ -562,15 +580,39 @@ int main (void) { } ImGui::EndChild(); - char waveformStatus[64]; + char waveformStatus[128]; + + if (hasWaveform && latestWaveform.sampleCount != 0U) { + size_t triggerSampleIndex = + static_cast(latestTriggerPoint); + + if (triggerSampleIndex >= latestWaveform.sampleCount) { + triggerSampleIndex = 0U; + } + + const double channelOneVolts = sampleToVolts( + latestWaveform.channelOne[triggerSampleIndex], + kVoltageScaleVoltsPerDivision[voltsPerDivision[0]] + ); + const double channelTwoVolts = sampleToVolts( + latestWaveform.channelTwo[triggerSampleIndex], + kVoltageScaleVoltsPerDivision[voltsPerDivision[1]] + ); + const double triggerSeconds = sampleIndexToSeconds( + triggerSampleIndex, + latestWaveform.sampleCount, + kTimebaseSecondsPerDivision[timebase] + ); - if (hasWaveform) { snprintf( waveformStatus, sizeof(waveformStatus), - "Waveform %zu samples (trigger %u)", + "Waveform %zu samples (trigger %u) CH1 %.3fV CH2 %.3fV @ %.3gs", latestWaveform.sampleCount, - static_cast(latestTriggerPoint) + static_cast(latestTriggerPoint), + channelOneVolts, + channelTwoVolts, + triggerSeconds ); } else { From 9ff857823d74a660baf09e425f609a7518fc1699 Mon Sep 17 00:00:00 2001 From: Anton Chernov Date: Sun, 6 Sep 2026 15:44:05 +0300 Subject: [PATCH 3/4] Documented waveform scaling stage --- HISTORY.md | 23 +++++++++++++++++++++++ README.md | 12 ++++++++---- docs/mainpage.md | 3 ++- 3 files changed, 33 insertions(+), 5 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index dc93e26..2213bda 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -365,3 +365,26 @@ Records key decisions, structural changes, and completed development stages. - Verified application shutdown during acquisition: no hang or crash; the status LED blinked red briefly, then turned off. +### Stage 4 - Scaling logic + +- Added a new `core` module with deterministic, hardware-independent + functions that convert raw two-channel capture samples into volts and + elapsed capture time. +- Centered voltage scaling on the ADC midpoint (raw sample 128) with 32 raw + counts per vertical grid division, matching the legacy `glbox`/ + `hantekdsoathread` display convention (8 vertical divisions across the + 256-value 8-bit sample range). +- Modeled elapsed sample time as a fraction of the full capture spanning the + 10 horizontal grid divisions at the selected timebase. +- Added shared voltage-scale and timebase lookup tables as the single source + of truth for both the scaling math and the existing UI combo-box labels, + with compile-time checks that the tables and labels stay in sync. +- Wired the status line to show the scaled CH1/CH2 voltage and elapsed time + at the decoded trigger sample, proving the scaling pipeline end to end + without yet plotting the waveform shape, which remains a separate future + task. +- Added deterministic CTest coverage for characteristic and boundary raw + sample values and sample-time fractions, including a zero-sample-count + guard. +- Verified warning-free Debug and Release builds and a full CTest pass. + diff --git a/README.md b/README.md index 1420a26..a53ac6f 100644 --- a/README.md +++ b/README.md @@ -22,9 +22,11 @@ hardware-independent parser functions. Each supported-device entry supplies its capture protocol, including endpoints, packet size, commands, channel layout, sample count, and completion state. After a completed capture, the acquisition worker reads and queues the complete profile-defined sample buffer, then starts -the next capture. Live waveform rendering is not yet implemented. A processing -worker decodes complete captures and publishes the latest frame and trigger -point safely for rendering. +the next capture. A processing worker decodes complete captures and publishes +the latest frame and trigger point safely for rendering. Deterministic +functions convert raw capture samples into volts and elapsed capture time from +the selected voltage-scale and timebase settings. Live waveform rendering is +not yet implemented. ## Planned Stack @@ -43,7 +45,9 @@ Oscilloscope/ ├── capture/ Sample acquisition and processing. │ ├── inc/ Capture module headers. │ └── src/ Capture module implementations. -├── core/ Shared types and application logic. +├── core/ Voltage/timebase scaling and shared application types. +│ ├── inc/ Core module headers. +│ └── src/ Core module implementations. ├── docs/ Project documentation. ├── firmware/ Default path for local device firmware files. ├── render/ Oscilloscope waveform rendering. diff --git a/docs/mainpage.md b/docs/mainpage.md index 00e3b49..0d2d32f 100644 --- a/docs/mainpage.md +++ b/docs/mainpage.md @@ -31,6 +31,7 @@ libusb for communication with supported instruments. - complete profile-defined capture reads after a successful trigger. - thread-safe publication of the latest decoded waveform and trigger point. - a per-device capture profile selected from the supported-device table. +- deterministic voltage and timebase scaling of raw capture samples. ## Architecture @@ -38,7 +39,7 @@ libusb for communication with supported instruments. | --- | --- | | `app/` | Application entry point, event loop, and UI composition | | `capture/` | Sample acquisition and buffering | -| `core/` | Planned shared application types and state | +| `core/` | Voltage/timebase scaling and shared application types | | `docs/` | Generated documentation sources | | `firmware/` | Default path for local device firmware files | | `render/` | Planned waveform rendering | From 1ce30b5556f3e4d8d5ce83844218feab1cabe8ed Mon Sep 17 00:00:00 2001 From: Anton Chernov Date: Sun, 6 Sep 2026 19:49:22 +0300 Subject: [PATCH 4/4] Documented known live-capture bug on real DSO-2250 hardware --- CMakeLists.txt | 20 ++ HISTORY.md | 39 +++ README.md | 14 +- app/main.cpp | 156 +++++++--- capture/inc/acquisition_loop.h | 19 +- capture/src/acquisition_loop.cpp | 342 ++++++++++++++++++++-- core/.gitkeep | 0 core/inc/instrument_model.h | 45 +++ core/inc/instrument_scaling_profile.h | 70 +++++ core/inc/waveform_scaling.h | 36 +-- core/src/instrument_scaling_profile.cpp | 80 +++++ core/src/waveform_scaling.cpp | 13 +- docs/Doxyfile | 4 +- tests/instrument_scaling_profile_test.cpp | 195 ++++++++++++ tests/waveform_scaling_test.cpp | 49 +++- usb/inc/usb_device.h | 30 +- usb/src/usb_device.cpp | 27 +- 17 files changed, 1021 insertions(+), 118 deletions(-) delete mode 100644 core/.gitkeep create mode 100644 core/inc/instrument_model.h create mode 100644 core/inc/instrument_scaling_profile.h create mode 100644 core/src/instrument_scaling_profile.cpp create mode 100644 tests/instrument_scaling_profile_test.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 3b45ba2..0ee8962 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -48,6 +48,7 @@ set(SOURCES_LIST capture/src/waveform_parser.cpp capture/src/waveform_ring_buffer.cpp core/src/waveform_scaling.cpp + core/src/instrument_scaling_profile.cpp ${imgui_SOURCE_DIR}/imgui.cpp ${imgui_SOURCE_DIR}/imgui_draw.cpp ${imgui_SOURCE_DIR}/imgui_tables.cpp @@ -64,6 +65,8 @@ set(HEADERS_LIST capture/inc/waveform_parser.h capture/inc/waveform_ring_buffer.h core/inc/waveform_scaling.h + core/inc/instrument_model.h + core/inc/instrument_scaling_profile.h ) set(RAW_PACKET_QUEUE_TEST_SOURCES_LIST @@ -82,6 +85,10 @@ set(WAVEFORM_SCALING_TEST_SOURCES_LIST tests/waveform_scaling_test.cpp ) +set(INSTRUMENT_SCALING_PROFILE_TEST_SOURCES_LIST + tests/instrument_scaling_profile_test.cpp +) + if(CMAKE_BUILD_TYPE MATCHES "Debug") message(STATUS ">>> Debug build") add_compile_definitions(_DEBUG) @@ -170,6 +177,19 @@ if(BUILD_TESTING) enable_project_warnings(waveform_scaling_tests) add_test(NAME waveform_scaling COMMAND waveform_scaling_tests) add_dependencies(${APP_NAME} waveform_scaling_tests) + add_executable(instrument_scaling_profile_tests + ${INSTRUMENT_SCALING_PROFILE_TEST_SOURCES_LIST} + core/src/instrument_scaling_profile.cpp + ) + target_include_directories(instrument_scaling_profile_tests PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/core/inc + ) + enable_project_warnings(instrument_scaling_profile_tests) + add_test( + NAME instrument_scaling_profile + COMMAND instrument_scaling_profile_tests + ) + add_dependencies(${APP_NAME} instrument_scaling_profile_tests) add_test( NAME release_updater COMMAND ${Python3_EXECUTABLE} diff --git a/HISTORY.md b/HISTORY.md index 2213bda..22a6c1d 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -388,3 +388,42 @@ Records key decisions, structural changes, and completed development stages. guard. - Verified warning-free Debug and Release builds and a full CTest pass. +### Stage 4 - Per-instrument scaling profile + +- Introduced `EInstrumentModel` and `SInstrumentScalingProfile` so timebase + steps, voltage-scale steps, grid divisions, and the ADC zero-volt center + value are looked up per connected instrument instead of one fixed table. +- Replaced the single shared voltage/timebase lookup tables and their + compile-time label checks with `findInstrtScalingProfile()`, resolved once + per frame from the connected or last-scanned device identity, falling back + to the DSO-2250 profile so the UI always has scale steps to display. +- Added deterministic CTest coverage for profile lookup, including the + fallback and an unknown-model case. + +### Known issue - live capture on real DSO-2250 hardware reports no waveform + +- Symptom: `GetCaptureState` polling never leaves the empty-buffer state on a + physical Hantek DSO-2250 (state byte stays `0x00`), so no channel data is + ever read and the display keeps showing no waveform, while Demo mode and + every deterministic CTest continue to pass. +- Investigated and ruled out across several verified, warning-free + Debug/Release builds, each checked against fresh USB captures from the + physical device: a missing `ForceTrigger` before polling, an incomplete + `configureCapture()` command sequence, missing empty-response retry + handling, resending `TriggerEnabled` on every poll, and a missing + `SetOffset` (`CONTROL_SETOFFSET`, request `0xB4`) channel/trigger offset + command derived from the device's calibration table - all implemented, + hardware-tested, and individually confirmed insufficient. +- Found by correlating each USB command byte with its immediately following + response byte across a full Windows reference driver capture (naive + per-endpoint response counts are misleading, because `GetCaptureState`, + `GetChannelData`, and the unrelated logic-channel/auto-range subsystem all + share bulk-IN endpoint `0x86`): the reference driver's completed-capture + state value is `3`, not the previously assumed `2`. Corrected + `captureCompleteState` from `2U` to `3U` for both DSO-2250 profiles in + `usb/src/usb_device.cpp`. +- This correction is warning-free in Debug/Release and passes all CTest + cases, but still did NOT resolve the symptom on hardware retest - the + root cause remains open. Treated as a known, unresolved bug pending + further hardware-side USB capture analysis. + diff --git a/README.md b/README.md index a53ac6f..4f8caa5 100644 --- a/README.md +++ b/README.md @@ -215,10 +215,20 @@ and restores the previous files if the build fails. CI can update only the version metadata by passing `--skip-build`. +## Known Issues + +- Live capture on a physical Hantek DSO-2250 never reports a waveform: + `GetCaptureState` polling stays at the empty-buffer state, so no channel + data is read. Demo mode and the full CTest suite are unaffected. See + `HISTORY.md` ("Known issue - live capture on real DSO-2250 hardware + reports no waveform") for the investigation and ruled-out causes. + ## Next Steps -1. Render live and demo waveforms on the display grid. -2. Implement the two-channel model, timebase, and instrument controls. +1. Diagnose the live-capture-never-completes issue on real DSO-2250 + hardware. +2. Render live and demo waveforms on the display grid. +3. Implement the two-channel model, timebase, and instrument controls. The full goals, constraints, and architecture are documented in `WorkingDocs/TECHNICAL_SPECIFICATION.md`. diff --git a/app/main.cpp b/app/main.cpp index 672659c..a08b7b6 100644 --- a/app/main.cpp +++ b/app/main.cpp @@ -74,6 +74,7 @@ /******************************* Included files ******************************/ #include #include +#include #include #include @@ -84,16 +85,18 @@ #include "acquisition_loop.h" #include "usb_device.h" +#include "instrument_scaling_profile.h" #include "waveform_scaling.h" using oscilloscope::capture::SAcquisitionLoop; using oscilloscope::capture::SWaveformSamples; using oscilloscope::capture::EAcquisitionOperation; using oscilloscope::capture::EAcquisitionState; -using oscilloscope::core::kTimebaseSecondsPerDivision; -using oscilloscope::core::kVoltageScaleVoltsPerDivision; +using oscilloscope::core::EInstrumentModel; +using oscilloscope::core::findInstrtScalingProfile; using oscilloscope::core::sampleIndexToSeconds; using oscilloscope::core::sampleToVolts; +using oscilloscope::core::SInstrumentScalingProfile; using oscilloscope::usb::EScanStatus; using oscilloscope::usb::EUsbTransferStatus; using oscilloscope::usb::SUsbConnection; @@ -107,7 +110,8 @@ using oscilloscope::usb::SUsbScanResult; static const uint32_t kUsbPresenceIntervalMs = 1000U; /** @brief Cleared device identity used to reset connection bookkeeping */ -static const SUsbDeviceInfo kEmptyDeviceInfo = { NULL, 0U, 0U, 0U, 0U }; +static const SUsbDeviceInfo kEmptyDeviceInfo = + { NULL, EInstrumentModel::eUnknown, 0U, 0U, 0U, 0U }; #ifdef __GNUC__ // GCC/MinGW only const char kVersionInfo[] __attribute__((section(".version"), used)) = @@ -242,6 +246,18 @@ static void pollUsbPresence( std::string *deviceStatus ); +/** + * @brief Resolves the display-scaling profile for the active instrument + * @param[in] connectedDevice Currently connected device identity, if any + * @param[in] usbScanResult Latest supported-device scan result + * @returns Matching profile; falls back to the DSO-2250 profile so the UI + * always has scale steps to display, even before a connection + */ +static const SInstrumentScalingProfile* resolveActiveScalingProfile( + const SUsbDeviceInfo &connectedDevice, + const SUsbScanResult &usbScanResult +); + /********************* Application Programming Interface *********************/ /** @fn main */ @@ -331,27 +347,9 @@ int main (void) { SWaveformSamples latestWaveform{}; uint32_t latestTriggerPoint = 0U; bool hasWaveform = false; - const char* timebases[] = { - "4 ns/div", "20 ns/div", "100 ns/div", "1 us/div", "10 us/div", - "100 us/div", "1 ms/div", "10 ms/div", "100 ms/div", "1 s/div" - }; - const char* voltageScales[] = { - "20 mV/div", "50 mV/div", "100 mV/div", "200 mV/div", - "500 mV/div", "1 V/div", "2 V/div", "5 V/div" - }; - - static_assert( - IM_ARRAYSIZE(timebases) == - sizeof(kTimebaseSecondsPerDivision) / - sizeof(kTimebaseSecondsPerDivision[0]), - "Timebase labels must match the scaling table" - ); - static_assert( - IM_ARRAYSIZE(voltageScales) == - sizeof(kVoltageScaleVoltsPerDivision) / - sizeof(kVoltageScaleVoltsPerDivision[0]), - "Voltage scale labels must match the scaling table" - ); + + /* Timebase/voltage-scale labels and values come from the active + * instrument's scaling profile, resolved once per frame below. */ while (running) { SDL_Event event; @@ -549,9 +547,41 @@ int main (void) { } ImGui::EndDisabled(); } + + const SInstrumentScalingProfile *scalingProfile = + resolveActiveScalingProfile(connectedDevice, usbScanResult); + std::vector timebaseLabels; + std::vector voltageScaleLabels; + + for (size_t i = 0U; i < scalingProfile->timebaseStepCount; ++i) { + timebaseLabels.push_back(scalingProfile->timebaseSteps[i].label); + } + for (size_t i = 0U; i < scalingProfile->voltageStepCount; ++i) { + voltageScaleLabels.push_back( + scalingProfile->voltageSteps[i].label + ); + } + if (timebase >= static_cast(timebaseLabels.size())) { + timebase = static_cast(timebaseLabels.size()) - 1; + } + for (int channel = 0; channel < 2; ++channel) { + if ( + voltsPerDivision[channel] >= + static_cast(voltageScaleLabels.size()) + ) { + voltsPerDivision[channel] = + static_cast(voltageScaleLabels.size()) - 1; + } + } + ImGui::Separator(); ImGui::TextUnformatted("Horizontal"); - ImGui::Combo("Timebase", &timebase, timebases, IM_ARRAYSIZE(timebases)); + ImGui::Combo( + "Timebase", + &timebase, + timebaseLabels.data(), + static_cast(timebaseLabels.size()) + ); ImGui::Separator(); for (int channel = 0; channel < 2; ++channel) { ImGui::PushID(channel); @@ -560,8 +590,8 @@ int main (void) { ImGui::Combo( "Scale", &voltsPerDivision[channel], - voltageScales, - IM_ARRAYSIZE(voltageScales) + voltageScaleLabels.data(), + static_cast(voltageScaleLabels.size()) ); ImGui::PopID(); } @@ -592,16 +622,23 @@ int main (void) { const double channelOneVolts = sampleToVolts( latestWaveform.channelOne[triggerSampleIndex], - kVoltageScaleVoltsPerDivision[voltsPerDivision[0]] + scalingProfile->voltageSteps[voltsPerDivision[0]] + .valuePerDivision, + scalingProfile->adcCenterValue, + scalingProfile->adcCountsPerDivision ); const double channelTwoVolts = sampleToVolts( latestWaveform.channelTwo[triggerSampleIndex], - kVoltageScaleVoltsPerDivision[voltsPerDivision[1]] + scalingProfile->voltageSteps[voltsPerDivision[1]] + .valuePerDivision, + scalingProfile->adcCenterValue, + scalingProfile->adcCountsPerDivision ); const double triggerSeconds = sampleIndexToSeconds( triggerSampleIndex, latestWaveform.sampleCount, - kTimebaseSecondsPerDivision[timebase] + scalingProfile->timebaseSteps[timebase].valuePerDivision, + scalingProfile->horizontalDivisions ); snprintf( @@ -683,6 +720,32 @@ static bool isUsbDevicePresent( } /*----------------------------------------------------------------------------*/ +/** @fn resolveActiveScalingProfile */ +static const SInstrumentScalingProfile* resolveActiveScalingProfile( + const SUsbDeviceInfo &connectedDevice, + const SUsbScanResult &usbScanResult +) { + EInstrumentModel model = connectedDevice.model; + + if (model == EInstrumentModel::eUnknown) { + if (!usbScanResult.devices.empty()) { + model = usbScanResult.devices.front().model; + } + else { + model = EInstrumentModel::eHantekDso2250; + } + } + + const SInstrumentScalingProfile *profile = findInstrtScalingProfile(model); + + if (profile == NULL) { + profile = findInstrtScalingProfile(EInstrumentModel::eHantekDso2250); + } + + return profile; +} +/*----------------------------------------------------------------------------*/ + /** @fn formatUsbConnectionStatus */ static std::string formatUsbConnectionStatus( const SUsbScanResult &scanResult, @@ -752,31 +815,52 @@ static std::string formatAcquisitionError( } switch (operation) { - case EAcquisitionOperation::eBeginCommand: + case EAcquisitionOperation::eBeginCmd: status += " while beginning command"; break; - case EAcquisitionOperation::eSpeedBeforeCommand: + case EAcquisitionOperation::eSpeedBeforeCmd: case EAcquisitionOperation::eSpeedBeforeResponse: status += " while checking connection speed"; break; - case EAcquisitionOperation::eCaptureStateCommand: + case EAcquisitionOperation::eCaptureStateCmd: status += " while sending capture-state command"; break; case EAcquisitionOperation::eCaptureStateResponse: status += " while reading capture state"; break; - case EAcquisitionOperation::eChannelDataCommand: + case EAcquisitionOperation::eChannelDataCmd: status += " while requesting channel data"; break; case EAcquisitionOperation::eChannelDataResponse: status += " while reading channel data"; break; - case EAcquisitionOperation::eCaptureStartCommand: + case EAcquisitionOperation::eCaptureStartCmd: status += " while starting capture"; break; - case EAcquisitionOperation::eTriggerEnabledCommand: + case EAcquisitionOperation::eTriggerEnabledCmd: status += " while enabling trigger"; break; + case EAcquisitionOperation::eForceTriggerCmd: + status += " while forcing trigger"; + break; + case EAcquisitionOperation::eSetFilterCmd: + status += " while setting filters"; + break; + case EAcquisitionOperation::eSetTriggerNSampleRateCmd: + status += " while setting trigger/sample rate"; + break; + case EAcquisitionOperation::eSetVoltageNCouplingCmd: + status += " while setting voltage/coupling"; + break; + case EAcquisitionOperation::eSetRelaysCmd: + status += " while setting input relays"; + break; + case EAcquisitionOperation::eGetChannelLevelCmd: + status += " while reading channel-level calibration"; + break; + case EAcquisitionOperation::eSetOffsetCmd: + status += " while setting channel/trigger offset"; + break; case EAcquisitionOperation::eNone: default: break; diff --git a/capture/inc/acquisition_loop.h b/capture/inc/acquisition_loop.h index 20eb85b..819a286 100644 --- a/capture/inc/acquisition_loop.h +++ b/capture/inc/acquisition_loop.h @@ -49,15 +49,22 @@ enum class EAcquisitionState { /** @brief Identifies the transfer that failed in a polling transaction */ enum class EAcquisitionOperation { eNone, /**< No transfer has failed */ - eBeginCommand, /**< Begin-command control write */ - eSpeedBeforeCommand, /**< Speed control read before command write */ - eCaptureStateCommand, /**< Capture-state bulk command write */ + eBeginCmd, /**< Begin-command control write */ + eSpeedBeforeCmd, /**< Speed control read before command write */ + eCaptureStateCmd, /**< Capture-state bulk command write */ eSpeedBeforeResponse, /**< Speed control read before response read */ eCaptureStateResponse, /**< Capture-state bulk response read */ - eChannelDataCommand, /**< Channel-data bulk command write */ + eChannelDataCmd, /**< Channel-data bulk command write */ eChannelDataResponse, /**< Channel-data bulk response read */ - eCaptureStartCommand, /**< Capture-start bulk command write */ - eTriggerEnabledCommand /**< Trigger-enable bulk command write */ + eCaptureStartCmd, /**< Capture-start bulk command write */ + eTriggerEnabledCmd, /**< Trigger-enable bulk command write */ + eForceTriggerCmd, /**< Force-trigger bulk command write */ + eSetFilterCmd, /**< Set-filter bulk command write */ + eSetTriggerNSampleRateCmd, /**< Set-trigger/sample-rate command */ + eSetVoltageNCouplingCmd, /**< Set-voltage/coupling command */ + eSetRelaysCmd, /**< Set-relays vendor control write */ + eGetChannelLevelCmd, /**< Channel-level calibration table read */ + eSetOffsetCmd /**< Set-offset vendor control write */ }; /** @brief Acquisition state safe to read from any thread */ diff --git a/capture/src/acquisition_loop.cpp b/capture/src/acquisition_loop.cpp index f017730..49ef502 100644 --- a/capture/src/acquisition_loop.cpp +++ b/capture/src/acquisition_loop.cpp @@ -48,6 +48,58 @@ static const unsigned int kTransferTimeoutMs = 250U; static const unsigned int kTransferAttempts = 1U; static const unsigned int kMaxConsecutiveFailures = 3U; +/** @brief Raw capture-state value meaning "buffer empty" for every model */ +static const uint8_t kCaptureEmptyState = 0U; + +/** + * @brief Consecutive empty polls tolerated before re-arming the trigger + * @details A single force-trigger pulse can arrive before the capture + * engine has re-armed, so the capture is retried periodically while the + * buffer stays empty. TEMPORARY fixed threshold; Stage 5 will replace this + * with real trigger-mode handling driven by the UI. + */ +static const unsigned int kForceRestartThreshold = 3U; + +/** + * @brief Fixed capture configuration sent once before the first capture + * @details TEMPORARY until Stage 5 (see WorkingDocs/TECHNICAL_SPECIFICATION.md) + * wires the Timebase/Scale/Trigger UI controls to the device: a single + * known-safe configuration unblocks acquisition meanwhile: both channels + * enabled at 5V/division AC coupling (capacitor-coupled inputs, safer + * unattended default), 1ms/division, rising-edge auto-trigger on CH1. + */ +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 Channel/trigger offset DAC configuration sent once before the + * first capture + * @details TEMPORARY until Stage 5 wires the vertical-position/trigger- + * level UI controls: the offset DACs are seeded from the per-unit + * calibration table (read once at connect) at their centered value, since + * this application does not yet let the user move the trace or trigger + * level away from the middle of the screen. Without any offset write the + * device never leaves its power-on capture state. + */ +static const uint16_t kControlValueChannelLevel = 0x0008U; +/** 2 channels x 9 ranges x 2 (start,end) 16-bit entries */ +static const uint16_t kChannelLevelTableBytes = 72U; +/** Index of the 5V range entry (ranges are stored from 10mV to 5V) */ +static const size_t kChannelLevelRangeIndex = 8U; +/** Constant high-byte marker seen on every offset DAC write */ +static const uint8_t kOffsetDacMarkerByte = 0x20U; +/** Centered trigger-level DAC value */ +static const uint8_t kDefaultTriggerOffsetByte = 0x7FU; + /***************************** Private prototypes *****************************/ /** @@ -92,6 +144,32 @@ static usb::SUsbTransferResult readCaptureData( EAcquisitionOperation *failedOperation ); +/** + * @brief Sends the fixed pre-capture configuration (filters, voltage + * range/coupling relays, trigger source/slope, and sample rate) + * @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 + * @note TEMPORARY: sends one hardcoded configuration; Stage 5 replaces this + * with values derived from the Timebase/Scale/Trigger UI controls. + */ +static usb::SUsbTransferResult configureCapture( + 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 + * control request (2 channels x 9 ranges x {start,end} 16-bit entries) + * @param[in] channelIndex Channel index (0 = CH1, 1 = CH2) + * @returns High byte of the calibration range midpoint for the 5V range + */ +static uint8_t channelLevelCenterByte( + const uint8_t *channelLevels, + uint8_t channelIndex +); + /** * @brief Starts a capture and enables its trigger * @param[in] connection USB connection controlling the capture @@ -241,10 +319,18 @@ static void pollCaptureState( connection.captureProtocol.channelCount; unsigned int consecutiveFailures = 0U; unsigned int delayMs = kPollIntervalMs; + bool captureConfigured = false; bool captureStartRequired = true; + unsigned int emptyCaptureCount = 0U; while (!loop->stopRequested.load()) { - if (captureStartRequired) { + if (!captureConfigured) { + transferResult = configureCapture(connection, &failedOperation); + if (transferResult.status == usb::EUsbTransferStatus::eSuccess) { + captureConfigured = true; + } + } + else if (captureStartRequired) { transferResult = restartCapture(connection, &failedOperation); if (transferResult.status == usb::EUsbTransferStatus::eSuccess) { captureStartRequired = false; @@ -270,6 +356,7 @@ static void pollCaptureState( captureState.captureState == connection.captureProtocol.captureCompleteState ) { + emptyCaptureCount = 0U; transferResult = readCaptureData( connection, captureData.data(), @@ -290,6 +377,19 @@ static void pollCaptureState( ); } } + else if (captureState.captureState == kCaptureEmptyState) { + ++emptyCaptureCount; + if (emptyCaptureCount >= kForceRestartThreshold) { + emptyCaptureCount = 0U; + transferResult = restartCapture( + connection, + &failedOperation + ); + } + } + else { + emptyCaptureCount = 0U; + } } } @@ -362,6 +462,9 @@ static usb::SUsbTransferResult executePollingTransaction( EAcquisitionOperation *failedOperation ) { uint8_t speedBuffer[kSpeedResponseLen]; + uint8_t triggerEnabledCommand[2] = { + connection.captureProtocol.triggerEnabledCmd, 0U + }; uint8_t captureStateCommand[2] = { connection.captureProtocol.captureStateCommand, 0U }; @@ -369,14 +472,27 @@ static usb::SUsbTransferResult executePollingTransaction( usb::EUsbTransferStatus::eSuccess, 0, "" }; + /* The device only reports a completed capture while its trigger stays + armed; re-asserting this on every poll (not only after a restart) + matches the real capture-state poll cadence observed on the wire. */ result = executeCommand( connection, - captureStateCommand, - sizeof(captureStateCommand), - EAcquisitionOperation::eCaptureStateCommand, + triggerEnabledCommand, + sizeof(triggerEnabledCommand), + EAcquisitionOperation::eTriggerEnabledCmd, failedOperation ); + if (result.status == usb::EUsbTransferStatus::eSuccess) { + result = executeCommand( + connection, + captureStateCommand, + sizeof(captureStateCommand), + EAcquisitionOperation::eCaptureStateCmd, + failedOperation + ); + } + if (result.status == usb::EUsbTransferStatus::eSuccess) { *failedOperation = EAcquisitionOperation::eSpeedBeforeResponse; result = readConnectionSpeed(connection, speedBuffer); @@ -437,7 +553,7 @@ static usb::SUsbTransferResult readCaptureData( connection, channelDataCommand, sizeof(channelDataCommand), - EAcquisitionOperation::eChannelDataCommand, + EAcquisitionOperation::eChannelDataCmd, failedOperation ); } @@ -468,6 +584,180 @@ static usb::SUsbTransferResult readCaptureData( return result; } +/*----------------------------------------------------------------------------*/ + +/** @fn channelLevelCenterByte */ +static uint8_t channelLevelCenterByte( + const uint8_t *channelLevels, + uint8_t channelIndex +) { + const size_t base = ( + ((static_cast(channelIndex) * 9U) + kChannelLevelRangeIndex) * + 2U * 2U + ); + const uint16_t offsetStart = static_cast( + channelLevels[base] | + (static_cast(channelLevels[base + 1U]) << 8U) + ); + const uint16_t offsetEnd = static_cast( + channelLevels[base + 2U] | + (static_cast(channelLevels[base + 3U]) << 8U) + ); + const uint32_t center = ( + static_cast(offsetStart) + static_cast(offsetEnd) + ) / 2U; + + return static_cast(center >> 8U); +} + +/*----------------------------------------------------------------------------*/ + +/** @fn configureCapture */ +static usb::SUsbTransferResult configureCapture( + const usb::SUsbConnection &connection, + EAcquisitionOperation *failedOperation +) { + const uint8_t filterCommand[8] = { + connection.captureProtocol.setFilterCmd, 0x0FU, + 0U, 0U, 0U, 0U, 0U, 0U + }; + const uint8_t tsrByte1 = static_cast( + kDefaultTriggerSource | + (kDefaultSampleSizeCode << 2U) | + (kDefaultTimeBaseFastCode << 5U) + ); + const uint8_t tsrByte2 = static_cast( + kDefaultSelectedChannel | (kDefaultTriggerSlope << 3U) + ); + const uint8_t triggerNSampleRateCmd[12] = { + connection.captureProtocol.setTriggerNSampleRateCmd, 0U, + tsrByte1, tsrByte2, + static_cast(kDefaultTimeBaseValue), + static_cast(kDefaultTimeBaseValue >> 8U), + static_cast(kDefaultTriggerPosition), + static_cast(kDefaultTriggerPosition >> 8U), + 0U, 0U, + static_cast(kDefaultTriggerPosition >> 16U), + 0U + }; + const uint8_t voltageByte = static_cast( + (2U - (kDefaultVoltageRangeCode % 3U)) | + ((2U - (kDefaultVoltageRangeCode % 3U)) << 2U) | + (3U << 4U) + ); + const uint8_t voltageCommand[8] = { + connection.captureProtocol.setVoltageNCouplingCmd, 0x0FU, + voltageByte, 0U, 0U, 0U, 0U, 0U + }; + /* Relay bitmap: index 3 and 6 flip to DC coupling for CH1/CH2 (base + state is AC); the 5V range needs no attenuator relay change and CH1 + as trigger source needs no EXT relay change. */ + uint8_t relays[17] = { + 0x00U, 0x04U, 0x08U, 0x02U, 0x20U, 0x40U, 0x10U, 0x01U, + 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U + }; + uint8_t channelLevels[kChannelLevelTableBytes]; + /* Offset DAC write: bytes 0/2/4 are a constant marker; 1 and 3 hold + the CH1/CH2 vertical-position DAC value (centered, seeded from the + calibration table read below); 5 holds the trigger-level DAC value + (centered, no calibration involved). */ + uint8_t offset[17] = { + kOffsetDacMarkerByte, 0U, + kOffsetDacMarkerByte, 0U, + kOffsetDacMarkerByte, kDefaultTriggerOffsetByte, + 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U + }; + usb::SUsbTransferResult result = { + usb::EUsbTransferStatus::eSuccess, 0, "" + }; + + if (kDefaultCouplingDc != 0U) { + relays[3] = static_cast(~relays[3]); + relays[6] = static_cast(~relays[6]); + } + + result = executeCommand( + connection, + filterCommand, + sizeof(filterCommand), + EAcquisitionOperation::eSetFilterCmd, + failedOperation + ); + if (result.status == usb::EUsbTransferStatus::eSuccess) { + result = executeCommand( + connection, + triggerNSampleRateCmd, + sizeof(triggerNSampleRateCmd), + EAcquisitionOperation::eSetTriggerNSampleRateCmd, + failedOperation + ); + } + if (result.status == usb::EUsbTransferStatus::eSuccess) { + result = executeCommand( + connection, + voltageCommand, + sizeof(voltageCommand), + EAcquisitionOperation::eSetVoltageNCouplingCmd, + failedOperation + ); + } + if (result.status == usb::EUsbTransferStatus::eSuccess) { + *failedOperation = EAcquisitionOperation::eSetRelaysCmd; + result = usb::controlWrite( + connection, + connection.captureProtocol.setRelaysControlRequest, + relays, + sizeof(relays), + 0U, + 0U, + kTransferTimeoutMs, + kTransferAttempts + ); + } + if (result.status == usb::EUsbTransferStatus::eSuccess) { + *failedOperation = EAcquisitionOperation::eGetChannelLevelCmd; + result = usb::controlRead( + connection, + connection.captureProtocol.controlCommandRequest, + channelLevels, + kChannelLevelTableBytes, + kControlValueChannelLevel, + 0U, + kTransferTimeoutMs, + kTransferAttempts, + kChannelLevelTableBytes + ); + } + if (result.status == usb::EUsbTransferStatus::eSuccess) { + offset[1] = channelLevelCenterByte(channelLevels, 0U); + offset[3] = channelLevelCenterByte(channelLevels, 1U); + *failedOperation = EAcquisitionOperation::eSetOffsetCmd; + result = usb::controlWrite( + connection, + connection.captureProtocol.setOffsetControlRequest, + offset, + sizeof(offset), + 0U, + 0U, + kTransferTimeoutMs, + kTransferAttempts + ); + } + if (result.status == usb::EUsbTransferStatus::eSuccess) { + result = executeCommand( + connection, + triggerNSampleRateCmd, + sizeof(triggerNSampleRateCmd), + EAcquisitionOperation::eSetTriggerNSampleRateCmd, + failedOperation + ); + } + if (result.status == usb::EUsbTransferStatus::eSuccess) { + *failedOperation = EAcquisitionOperation::eNone; + } + + return result; +} /*----------------------------------------------------------------------------*/ @@ -476,11 +766,14 @@ static usb::SUsbTransferResult restartCapture( const usb::SUsbConnection &connection, EAcquisitionOperation *failedOperation ) { - const uint8_t captureStartCommand[2] = { - connection.captureProtocol.captureStartCommand, 0U + const uint8_t captureStartCmd[2] = { + connection.captureProtocol.captureStartCmd, 0U }; - const uint8_t triggerEnabledCommand[2] = { - connection.captureProtocol.triggerEnabledCommand, 0U + const uint8_t triggerEnabledCmd[2] = { + connection.captureProtocol.triggerEnabledCmd, 0U + }; + const uint8_t forceTriggerCmd[2] = { + connection.captureProtocol.forceTriggerCmd, 0U }; usb::SUsbTransferResult result = { usb::EUsbTransferStatus::eSuccess, 0, "" @@ -488,17 +781,32 @@ static usb::SUsbTransferResult restartCapture( result = executeCommand( connection, - captureStartCommand, - sizeof(captureStartCommand), - EAcquisitionOperation::eCaptureStartCommand, + captureStartCmd, + sizeof(captureStartCmd), + EAcquisitionOperation::eCaptureStartCmd, failedOperation ); if (result.status == usb::EUsbTransferStatus::eSuccess) { result = executeCommand( connection, - triggerEnabledCommand, - sizeof(triggerEnabledCommand), - EAcquisitionOperation::eTriggerEnabledCommand, + triggerEnabledCmd, + sizeof(triggerEnabledCmd), + EAcquisitionOperation::eTriggerEnabledCmd, + failedOperation + ); + } + if (result.status == usb::EUsbTransferStatus::eSuccess) { + /* Free-running auto-trigger: this application has no manual + trigger-source/level controls, so every capture is forced. + Without this, the device stays armed and waiting for a real + trigger edge that may never occur, and no capture ever + reaches captureCompleteState. TEMPORARY until Stage 5 adds + real trigger-mode selection. */ + result = executeCommand( + connection, + forceTriggerCmd, + sizeof(forceTriggerCmd), + EAcquisitionOperation::eForceTriggerCmd, failedOperation ); } @@ -523,7 +831,7 @@ static usb::SUsbTransferResult executeCommand( usb::EUsbTransferStatus::eSuccess, 0, "" }; - *failedOperation = EAcquisitionOperation::eBeginCommand; + *failedOperation = EAcquisitionOperation::eBeginCmd; result = usb::controlWrite( connection, kControlBeginCommand, @@ -535,7 +843,7 @@ static usb::SUsbTransferResult executeCommand( kTransferAttempts ); if (result.status == usb::EUsbTransferStatus::eSuccess) { - *failedOperation = EAcquisitionOperation::eSpeedBeforeCommand; + *failedOperation = EAcquisitionOperation::eSpeedBeforeCmd; result = readConnectionSpeed(connection, speedBuffer); } if (result.status == usb::EUsbTransferStatus::eSuccess) { diff --git a/core/.gitkeep b/core/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/core/inc/instrument_model.h b/core/inc/instrument_model.h new file mode 100644 index 0000000..d6b3796 --- /dev/null +++ b/core/inc/instrument_model.h @@ -0,0 +1,45 @@ +/** + * @file instrument_model.h + * @version 0.2.8 + * @authors Anton Chernov + * @date 2026-09-06 + * @date @showdate "%Y-%m-%d" + * @par + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef INSTRUMENT_MODEL_H_ +#define INSTRUMENT_MODEL_H_ + +/********************************* Definitions ********************************/ + +namespace oscilloscope { +namespace core { + +/** + * @brief Identifies a physical instrument model + * @details Stable across a model's USB identities (for example the DSO-2250 + * bootloader and operational VID/PID pairs share one entry), so it + * can key model-specific data such as display scaling profiles. + */ +enum class EInstrumentModel { + eUnknown, /**< No device, or a device without a scaling profile */ + eHantekDso2250 /**< Hantek DSO-2250, bootloader or operational state */ +}; + +} // namespace core +} // namespace oscilloscope +/******************************************************************************/ +#endif //! INSTRUMENT_MODEL_H_ diff --git a/core/inc/instrument_scaling_profile.h b/core/inc/instrument_scaling_profile.h new file mode 100644 index 0000000..363750e --- /dev/null +++ b/core/inc/instrument_scaling_profile.h @@ -0,0 +1,70 @@ +/** + * @file instrument_scaling_profile.h + * @version 0.2.8 + * @authors Anton Chernov + * @date 2026-09-06 + * @date @showdate "%Y-%m-%d" + * @par + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef INSTRUMENT_SCALING_PROFILE_H_ +#define INSTRUMENT_SCALING_PROFILE_H_ + +/******************************* Included files ******************************/ +#include +#include + +#include "instrument_model.h" + +/********************************* Definitions ********************************/ + +namespace oscilloscope { +namespace core { + +/** @brief One labeled per-division step of a timebase or voltage control */ +struct SScaleStep { + const char *label; /**< Display text, for example "20 mV/div" */ + double valuePerDivision; /**< Seconds or volts represented by one division */ +}; + +/** @brief Describes the display-scaling characteristics of an instrument model */ +struct SInstrumentScalingProfile { + const SScaleStep *timebaseSteps; /**< Selectable timebase steps */ + const SScaleStep *voltageSteps; /**< Selectable voltage-scale steps */ + double horizontalDivisions; /**< Grid divisions along the time axis */ + double verticalDivisions; /**< Grid divisions along the voltage axis */ + double adcCountsPerDivision; /**< Raw ADC counts spanning one division */ + size_t timebaseStepCount; /**< Number of entries in timebaseSteps */ + size_t voltageStepCount; /**< Number of entries in voltageSteps */ + EInstrumentModel model; /**< Instrument model this profile describes */ + uint8_t adcCenterValue; /**< Raw sample value that means zero volts */ +}; + +/********************* Application Programming Interface *********************/ + +/** + * @brief Finds the display-scaling profile for an instrument model + * @param[in] model Instrument model identifier to look up + * @returns Matching profile, or NULL when the model has no scaling profile + */ +const SInstrumentScalingProfile* findInstrtScalingProfile( + EInstrumentModel model +); + +} // namespace core +} // namespace oscilloscope +/******************************************************************************/ +#endif //! INSTRUMENT_SCALING_PROFILE_H_ diff --git a/core/inc/waveform_scaling.h b/core/inc/waveform_scaling.h index 2b784fd..a5eb163 100644 --- a/core/inc/waveform_scaling.h +++ b/core/inc/waveform_scaling.h @@ -32,50 +32,36 @@ namespace oscilloscope { namespace core { -/** @brief Oscilloscope grid divisions along the horizontal (time) axis */ -static const double kHorizontalDivisions = 10.0; - -/** @brief Oscilloscope grid divisions along the vertical (voltage) axis */ -static const double kVerticalDivisions = 8.0; - -/** @brief Raw sample value that represents zero volts */ -static const uint8_t kAdcCenterValue = 128U; - -/** @brief Raw ADC counts spanning one vertical division */ -static const double kAdcCountsPerDivision = 256.0 / kVerticalDivisions; - -/** @brief Seconds/division for each supported timebase selection */ -static const double kTimebaseSecondsPerDivision[10] = { - 4.0e-9, 20.0e-9, 100.0e-9, 1.0e-6, 10.0e-6, - 100.0e-6, 1.0e-3, 10.0e-3, 100.0e-3, 1.0 -}; - -/** @brief Volts/division for each supported voltage scale selection */ -static const double kVoltageScaleVoltsPerDivision[8] = { - 0.02, 0.05, 0.1, 0.2, 0.5, 1.0, 2.0, 5.0 -}; - /********************* Application Programming Interface *********************/ /** * @brief Converts a raw ADC sample to a signed voltage * @param[in] rawSample Raw 8-bit sample value from the capture buffer * @param[in] voltsPerDivision Selected voltage scale for the sample's channel + * @param[in] adcCenterValue Raw sample value that represents zero volts + * @param[in] adcCountsPerDivision Raw ADC counts spanning one vertical division * @returns The sample value in volts, centered on zero at the ADC midpoint */ -double sampleToVolts(uint8_t rawSample, double voltsPerDivision); +double sampleToVolts( + uint8_t rawSample, + double voltsPerDivision, + uint8_t adcCenterValue, + double adcCountsPerDivision +); /** * @brief Converts a sample index into elapsed capture time * @param[in] sampleIndex Zero-based index of the sample in the capture * @param[in] sampleCount Total number of samples spanning the capture * @param[in] secondsPerDivision Selected timebase for the capture + * @param[in] horizontalDivisions Grid divisions spanned by the full capture * @returns The elapsed time in seconds, or zero when sampleCount is zero */ double sampleIndexToSeconds( size_t sampleIndex, size_t sampleCount, - double secondsPerDivision + double secondsPerDivision, + double horizontalDivisions ); } // namespace core diff --git a/core/src/instrument_scaling_profile.cpp b/core/src/instrument_scaling_profile.cpp new file mode 100644 index 0000000..31fa6f7 --- /dev/null +++ b/core/src/instrument_scaling_profile.cpp @@ -0,0 +1,80 @@ +/** + * @file instrument_scaling_profile.cpp + * @version 0.2.8 + * @authors Anton Chernov + * @date 2026-09-06 + * @date @showdate "%Y-%m-%d" + * @par + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/******************************* Included files ******************************/ +#include "instrument_scaling_profile.h" + +/****************************** Module variables ******************************/ + +namespace oscilloscope { +namespace core { + +/** @brief Timebase steps for the Hantek DSO-2250 */ +static const SScaleStep kDso2250TimebaseSteps[] = { + { "4 ns/div", 4.0e-9 }, { "20 ns/div", 20.0e-9 }, + { "100 ns/div", 100.0e-9 }, { "1 us/div", 1.0e-6 }, + { "10 us/div", 10.0e-6 }, { "100 us/div", 100.0e-6 }, + { "1 ms/div", 1.0e-3 }, { "10 ms/div", 10.0e-3 }, + { "100 ms/div", 100.0e-3 }, { "1 s/div", 1.0 } +}; + +/** @brief Voltage-scale steps for the Hantek DSO-2250 */ +static const SScaleStep kDso2250VoltageSteps[] = { + { "20 mV/div", 0.02 }, { "50 mV/div", 0.05 }, { "100 mV/div", 0.1 }, + { "200 mV/div", 0.2 }, { "500 mV/div", 0.5 }, { "1 V/div", 1.0 }, + { "2 V/div", 2.0 }, { "5 V/div", 5.0 } +}; + +/** @brief Display-scaling profile for every known instrument model */ +static const SInstrumentScalingProfile kInstrScalingProfiles[] = { + { + kDso2250TimebaseSteps, kDso2250VoltageSteps, + 10.0, 8.0, 256.0 / 8.0, + sizeof(kDso2250TimebaseSteps) / sizeof(kDso2250TimebaseSteps[0]), + sizeof(kDso2250VoltageSteps) / sizeof(kDso2250VoltageSteps[0]), + EInstrumentModel::eHantekDso2250, 128U + } +}; + +/********************* Application Programming Interface *********************/ + +/** @fn oscilloscope::core::findInstrtScalingProfile */ +const SInstrumentScalingProfile* findInstrtScalingProfile( + EInstrumentModel model +) { + const SInstrumentScalingProfile *result = NULL; + const size_t profileCount = + sizeof(kInstrScalingProfiles) / sizeof(kInstrScalingProfiles[0]); + + for (size_t index = 0U; index < profileCount; ++index) { + if (kInstrScalingProfiles[index].model == model) { + result = &kInstrScalingProfiles[index]; + break; + } + } + + return result; +} + +} // namespace core +} // namespace oscilloscope +/******************************************************************************/ diff --git a/core/src/waveform_scaling.cpp b/core/src/waveform_scaling.cpp index 0a7bce4..3d93286 100644 --- a/core/src/waveform_scaling.cpp +++ b/core/src/waveform_scaling.cpp @@ -28,13 +28,15 @@ /** @fn oscilloscope::core::sampleToVolts */ double oscilloscope::core::sampleToVolts( uint8_t rawSample, - double voltsPerDivision + double voltsPerDivision, + uint8_t adcCenterValue, + double adcCountsPerDivision ) { const double centeredCounts = static_cast(rawSample) - - static_cast(kAdcCenterValue); + static_cast(adcCenterValue); - return (centeredCounts / kAdcCountsPerDivision) * voltsPerDivision; + return (centeredCounts / adcCountsPerDivision) * voltsPerDivision; } /*----------------------------------------------------------------------------*/ @@ -42,13 +44,14 @@ double oscilloscope::core::sampleToVolts( double oscilloscope::core::sampleIndexToSeconds( size_t sampleIndex, size_t sampleCount, - double secondsPerDivision + double secondsPerDivision, + double horizontalDivisions ) { double ret_val = 0.0; if (sampleCount != 0U) { const double captureSeconds = - secondsPerDivision * kHorizontalDivisions; + secondsPerDivision * horizontalDivisions; ret_val = (static_cast(sampleIndex) / diff --git a/docs/Doxyfile b/docs/Doxyfile index ca8980e..d20cc89 100644 --- a/docs/Doxyfile +++ b/docs/Doxyfile @@ -913,7 +913,9 @@ INPUT = ./mainpage.md \ ../usb/inc \ ../usb/src \ ../capture/inc \ - ../capture/src + ../capture/src \ + ../core/src \ + ../core/inc # This tag can be used to specify the character encoding of the source files # that doxygen parses. Internally doxygen uses the UTF-8 encoding. Doxygen uses diff --git a/tests/instrument_scaling_profile_test.cpp b/tests/instrument_scaling_profile_test.cpp new file mode 100644 index 0000000..9cea615 --- /dev/null +++ b/tests/instrument_scaling_profile_test.cpp @@ -0,0 +1,195 @@ +/** + * @file instrument_scaling_profile_test.cpp + * @version 0.2.8 + * @authors Anton Chernov + * @date 2026-09-06 + * @date @showdate "%Y-%m-%d" + * @par + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/******************************* Included files *******************************/ +#include +#include + +#include "instrument_scaling_profile.h" + +/********************************* Definitions ********************************/ + +namespace { + +using oscilloscope::core::EInstrumentModel; +using oscilloscope::core::findInstrtScalingProfile; +using oscilloscope::core::SInstrumentScalingProfile; + +static const double kEpsilon = 1.0e-9; + +/* Expected values must match the DSO-2250 entry in + * core/src/instrument_scaling_profile.cpp; update both together. */ +static const double kExpectedHorizontalDivisions = 10.0; +static const double kExpectedVerticalDivisions = 8.0; +static const uint8_t kExpectedAdcCenterValue = 128U; /**< Raw sample=0V */ +static const double kExpectedAdcCountsPerDivision = 32.0; /**< 256/8 divs */ +static const size_t kExpectedTimebaseStepCount = 10U; +static const size_t kExpectedVoltageStepCount = 8U; +static const double kExpectedFastestTimebaseStep = 4.0e-9; /**< 4 ns/div */ +static const double kExpectedSlowestTimebaseStep = 1.0; /**< 1 s/div */ +static const double kExpectedSmallestVoltageStep = 0.02; /**< 20 mV/div */ +static const double kExpectedLargestVoltageStep = 5.0; /**< 5 V/div */ + +/***************************** Private prototypes *****************************/ + +static bool expect(bool condition, const char *message); +static bool nearlyEqual(double actual, double expected); +static bool testUnknownModelHasNoProfile(); +static bool testHantekDso2250Profile(); + +/****************************** Private functions *****************************/ + +/** @fn expect */ +static bool expect(bool condition, const char *message) { + bool result = condition; + + if (!condition) { + std::cerr << "FAILED: " << message << std::endl; + } + + return result; +} +/*----------------------------------------------------------------------------*/ + +/** @fn nearlyEqual */ +static bool nearlyEqual(double actual, double expected) { + return std::fabs(actual - expected) < kEpsilon; +} +/*----------------------------------------------------------------------------*/ + +/** @fn testUnknownModelHasNoProfile */ +static bool testUnknownModelHasNoProfile() { + return expect( + findInstrtScalingProfile(EInstrumentModel::eUnknown) == NULL, + "Unknown model must have no scaling profile" + ); +} +/*----------------------------------------------------------------------------*/ + +/** @fn testHantekDso2250Profile */ +static bool testHantekDso2250Profile() { + bool passed = true; + const SInstrumentScalingProfile *profile = + findInstrtScalingProfile(EInstrumentModel::eHantekDso2250); + + passed = + expect(profile != NULL, "DSO-2250 must have a scaling profile") && + passed; + if (profile != NULL) { + passed = + expect( + nearlyEqual( + profile->horizontalDivisions, + kExpectedHorizontalDivisions + ), + "DSO-2250 must have 10 horizontal divisions" + ) && passed; + passed = + expect( + nearlyEqual( + profile->verticalDivisions, + kExpectedVerticalDivisions + ), + "DSO-2250 must have 8 vertical divisions" + ) && passed; + passed = + expect( + profile->adcCenterValue == kExpectedAdcCenterValue, + "DSO-2250 ADC center value must be 128" + ) && passed; + passed = + expect( + nearlyEqual( + profile->adcCountsPerDivision, + kExpectedAdcCountsPerDivision + ), + "DSO-2250 must have 32 ADC counts per division" + ) && passed; + passed = + expect( + profile->timebaseStepCount == kExpectedTimebaseStepCount, + "DSO-2250 must have 10 timebase steps" + ) && passed; + passed = + expect( + profile->voltageStepCount == kExpectedVoltageStepCount, + "DSO-2250 must have 8 voltage-scale steps" + ) && passed; + passed = + expect( + nearlyEqual( + profile->timebaseSteps[0].valuePerDivision, + kExpectedFastestTimebaseStep + ), + "Fastest DSO-2250 timebase step must be 4 ns/div" + ) && passed; + passed = + expect( + nearlyEqual( + profile->timebaseSteps[ + profile->timebaseStepCount - 1U + ].valuePerDivision, + kExpectedSlowestTimebaseStep + ), + "Slowest DSO-2250 timebase step must be 1 s/div" + ) && passed; + passed = + expect( + nearlyEqual( + profile->voltageSteps[0].valuePerDivision, + kExpectedSmallestVoltageStep + ), + "Smallest DSO-2250 voltage step must be 20 mV/div" + ) && passed; + passed = + expect( + nearlyEqual( + profile->voltageSteps[ + profile->voltageStepCount - 1U + ].valuePerDivision, + kExpectedLargestVoltageStep + ), + "Largest DSO-2250 voltage step must be 5 V/div" + ) && passed; + } + + return passed; +} + +} // namespace + +/********************* Application Programming Interface *********************/ + +/** @fn main */ +int main() { + bool passed = true; + int result = 1; + + passed = testUnknownModelHasNoProfile() && passed; + passed = testHantekDso2250Profile() && passed; + if (passed) { + result = 0; + } + + return result; +} +/******************************************************************************/ diff --git a/tests/waveform_scaling_test.cpp b/tests/waveform_scaling_test.cpp index 81d58e8..bc9b9d4 100644 --- a/tests/waveform_scaling_test.cpp +++ b/tests/waveform_scaling_test.cpp @@ -35,6 +35,14 @@ using oscilloscope::core::sampleToVolts; static const double kEpsilon = 1.0e-9; +/* Fixture values mirror the Hantek DSO-2250 profile (see + * core/src/instrument_scaling_profile.cpp) for realistic numbers, but the + * functions under test take them as plain parameters and do not read any + * instrument-specific table themselves. */ +static const uint8_t kAdcCenterValue = 128U; /**< Raw sample = 0V */ +static const double kAdcCountsPerDivision = 32.0; /**< 256 counts / 8 divs */ +static const double kHorizontalDivisions = 10.0; /**< Grid divs, full capture */ + /***************************** Private prototypes *****************************/ static bool expect(bool condition, const char *message); @@ -68,27 +76,44 @@ static bool testSampleToVolts() { passed = expect( - nearlyEqual(sampleToVolts(128U, 1.0), 0.0), + nearlyEqual( + sampleToVolts( + kAdcCenterValue, 1.0, kAdcCenterValue, kAdcCountsPerDivision + ), + 0.0 + ), "Midpoint sample must map to zero volts" ) && passed; passed = expect( - nearlyEqual(sampleToVolts(0U, 1.0), -4.0), + nearlyEqual( + sampleToVolts(0U, 1.0, kAdcCenterValue, kAdcCountsPerDivision), + -4.0 + ), "Minimum sample must map to -4 divisions worth of volts" ) && passed; passed = expect( - nearlyEqual(sampleToVolts(255U, 1.0), 3.96875), + nearlyEqual( + sampleToVolts(255U, 1.0, kAdcCenterValue, kAdcCountsPerDivision), + 3.96875 + ), "Maximum sample must map to the topmost division fraction" ) && passed; passed = expect( - nearlyEqual(sampleToVolts(160U, 0.5), 0.5), + nearlyEqual( + sampleToVolts(160U, 0.5, kAdcCenterValue, kAdcCountsPerDivision), + 0.5 + ), "One division above center must scale with volts/division" ) && passed; passed = expect( - nearlyEqual(sampleToVolts(96U, 0.5), -0.5), + nearlyEqual( + sampleToVolts(96U, 0.5, kAdcCenterValue, kAdcCountsPerDivision), + -0.5 + ), "One division below center must scale with volts/division" ) && passed; @@ -102,13 +127,16 @@ static bool testSampleIndexToSeconds() { passed = expect( - nearlyEqual(sampleIndexToSeconds(0U, 1000U, 1.0e-3), 0.0), + nearlyEqual( + sampleIndexToSeconds(0U, 1000U, 1.0e-3, kHorizontalDivisions), + 0.0 + ), "First sample must be at time zero" ) && passed; passed = expect( nearlyEqual( - sampleIndexToSeconds(500U, 1000U, 1.0e-3), + sampleIndexToSeconds(500U, 1000U, 1.0e-3, kHorizontalDivisions), 5.0e-3 ), "Midpoint sample must be at half the total capture time" @@ -116,14 +144,17 @@ static bool testSampleIndexToSeconds() { passed = expect( nearlyEqual( - sampleIndexToSeconds(1000U, 1000U, 1.0e-3), + sampleIndexToSeconds(1000U, 1000U, 1.0e-3, kHorizontalDivisions), 10.0e-3 ), "Sample index at sampleCount must reach the full capture time" ) && passed; passed = expect( - nearlyEqual(sampleIndexToSeconds(5U, 0U, 1.0e-3), 0.0), + nearlyEqual( + sampleIndexToSeconds(5U, 0U, 1.0e-3, kHorizontalDivisions), + 0.0 + ), "Zero sampleCount must not divide by zero" ) && passed; diff --git a/usb/inc/usb_device.h b/usb/inc/usb_device.h index 4550c84..606a9c5 100644 --- a/usb/inc/usb_device.h +++ b/usb/inc/usb_device.h @@ -30,6 +30,8 @@ #include +#include "instrument_model.h" + /********************************* Definitions ********************************/ namespace oscilloscope { @@ -57,11 +59,12 @@ enum class EConnectionStatus { /** @brief Identifies one supported USB oscilloscope instance */ struct SUsbDeviceInfo { - const char *modelName; /**< Supported model display name */ - uint16_t vendorId; /**< USB vendor identifier */ - uint16_t productId; /**< USB product identifier */ - uint8_t busNumber; /**< USB bus number */ - uint8_t deviceAddress; /**< Address assigned on the USB bus */ + const char *modelName; /**< Supported model display name */ + core::EInstrumentModel model; /**< Model identifier for lookups */ + uint16_t vendorId; /**< USB vendor identifier */ + uint16_t productId; /**< USB product identifier */ + uint8_t busNumber; /**< USB bus number */ + uint8_t deviceAddress; /**< Address assigned on the USB bus */ }; /** @brief Holds the outcome and matching devices from a USB scan */ @@ -82,8 +85,21 @@ struct SUsbCaptureProtocol { uint8_t captureCompleteState; /**< State value indicating a full buffer */ uint8_t captureStateCommand; /**< Command byte that reads capture state */ uint8_t channelDataCommand; /**< Command byte that reads sample data */ - uint8_t captureStartCommand; /**< Command byte that starts capture */ - uint8_t triggerEnabledCommand; /**< Command byte that enables trigger */ + uint8_t captureStartCmd; /**< Command byte that starts capture */ + uint8_t triggerEnabledCmd; /**< Command byte that enables trigger */ + uint8_t forceTriggerCmd; /**< Command byte that forces a trigger */ + uint8_t setFilterCmd; /**< Command byte that sets channel filters */ + uint8_t setTriggerNSampleRateCmd; /**< Command byte that sets + trigger/sample-rate regs */ + uint8_t setVoltageNCouplingCmd; /**< Command byte that sets + voltage range/coupling */ + uint8_t setRelaysControlRequest; /**< Vendor control request that sets + attenuator/coupling relays */ + uint8_t controlCommandRequest; /**< Vendor control request that reads + device tables (selected by + wValue, e.g. channel-level data) */ + uint8_t setOffsetControlRequest; /**< Vendor control request that sets + the channel/trigger offset DACs */ }; /** @brief Describes an active USB connection to a supported device */ diff --git a/usb/src/usb_device.cpp b/usb/src/usb_device.cpp index 5bbb0f1..cb6aa81 100644 --- a/usb/src/usb_device.cpp +++ b/usb/src/usb_device.cpp @@ -39,6 +39,7 @@ struct SSupportedDevice { const char *modelName; const char *firmwareBaseName; SUsbCaptureProtocol captureProtocol; + core::EInstrumentModel model; uint16_t vendorId; uint16_t productId; uint16_t operationalVendorId; @@ -48,9 +49,9 @@ struct SSupportedDevice { }; /** @brief Delay between scans while waiting for FX2 re-enumeration */ -static const unsigned int kFirmwareReenumerationPollDelayUs = 100000U; +static const unsigned int kFwReenumPollDelayUs = 100000U; /** @brief Maximum scans while waiting for the operational USB device */ -static const unsigned int kFirmwareReenumerationAttempts = 50U; +static const unsigned int kFwReenumAttempts = 50U; /****************************** Module variables ******************************/ @@ -59,12 +60,16 @@ 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, 2U, 6U, 5U, 3U, 4U }, + { 32768U, 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, 2U, 6U, 5U, 3U, 4U }, + { 32768U, 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 } }; @@ -206,6 +211,7 @@ SUsbScanResult enumerateSupportedDevices() { if (supportedDevice != NULL) { result.devices.push_back({ supportedDevice->modelName, + supportedDevice->model, descriptor.idVendor, descriptor.idProduct, libusb_get_bus_number(deviceList[index]), @@ -304,12 +310,11 @@ SUsbConnectionResult connectToDevice( for ( reenumerationAttempt = 0U; - (reenumerationAttempt < - kFirmwareReenumerationAttempts) && - (device == NULL); + (reenumerationAttempt < kFwReenumAttempts) && + (device == NULL); ++reenumerationAttempt ) { - usleep(kFirmwareReenumerationPollDelayUs); + usleep(kFwReenumPollDelayUs); deviceCount = libusb_get_device_list( context, &deviceList @@ -379,7 +384,8 @@ SUsbConnectionResult connectToDevice( ); if (setInterfaceResult != LIBUSB_SUCCESS) { - result.status = EConnectionStatus::eSetInterfaceFailed; + result.status = + EConnectionStatus::eSetInterfaceFailed; result.errorMessage = libusb_error_name(setInterfaceResult); libusb_release_interface( @@ -489,6 +495,7 @@ bool getConnectedDeviceInfo( if (supportedDevice != NULL) { *deviceInfo = { supportedDevice->modelName, + supportedDevice->model, descriptor.idVendor, descriptor.idProduct, libusb_get_bus_number(device), @@ -725,7 +732,7 @@ static SFirmwarePaths resolveFirmwarePaths( const SSupportedDevice &supportedDevice ) { static const char* const candidateDirs[] = { - "firmware", "../firmware", "../../firmware" + "firmware", "../firmware", "/usr/share/HantekDSO" }; SFirmwarePaths paths; const char *firmwareDir = getenv("OSCILLOSCOPE_FIRMWARE_DIR");