From 6dec8329c02045a8af30e25765630799ff525638 Mon Sep 17 00:00:00 2001 From: Anton Chernov Date: Mon, 7 Sep 2026 19:12:11 +0300 Subject: [PATCH] Added render both channel waveforms to the display area; Corrected the offset calibration decode and added a zero reference --- HISTORY.md | 53 ++++++++++++- app/main.cpp | 131 ++++++++++++++++++++++++++++--- capture/src/acquisition_loop.cpp | 63 +++++++++------ 3 files changed, 214 insertions(+), 33 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index 9f20835..203a335 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -243,7 +243,6 @@ Records key decisions, structural changes, and completed development stages. ### Next USB tasks -- Decode capture-state responses and acquired sample packets. - Add deterministic fault-injection tests for timeout and transfer-error recovery, then verify recovery with a connected physical device. @@ -503,3 +502,55 @@ Records key decisions, structural changes, and completed development stages. over USB. On-screen rendering is not yet implemented, so Stage 4 remains open until the captured samples are drawn. +### Stage 4 - Render both channel waveforms to the display area + +- Added a `drawChannelWaveform()` helper in `app/main.cpp` that maps each raw + ADC byte to a vertical division via `(sample - adcCenterValue) / + adcCountsPerDivision`, spreads the samples across the plot width, and draws + the trace with `ImDrawList::AddPolyline`. +- Rendered both channels in the display child after the grid: CH1 in yellow + and CH2 in cyan, each gated by its channel-enable checkbox. +- Moved the active scaling-profile resolution ahead of the display group so + the plot and the Controls panel share the same profile lookup. +- Confirmed on hardware: both channel traces are drawn and track the numeric + readout. The traces sit below center because the captured baseline is + offset from the assumed ADC center; vertical positioning/offset calibration + is left for a later stage. + +### Stage 4 - Corrected the offset calibration decode and added a zero reference + +- Fixed the endianness of `channelLevelCenter()` in + `capture/src/acquisition_loop.cpp`: the offset calibration table stores each + 16-bit value most-significant byte first, so the previous little-endian read + produced garbage DAC values (35200/23040) instead of the calibrated + midpoints. The decode now yields 137 for CH1 and 90 for CH2, matching the + same-device reference dump. +- Added a per-channel software zero reference in `app/main.cpp`: the running + mean of each captured channel feeds a `Set zero` control that stores the + present baseline as the zero-volt reference. Both the voltage readout and the + waveform rendering use this per-channel reference instead of a fixed ADC + center, so a grounded input reads 0 V regardless of where the offset DAC + places the trace on the ADC range. +- Confirmed on hardware: with no signal, pressing `Set zero` brings both + channels to 0 V and centers their traces. This mirrors the original + instrument's manual zero-set behavior. + +### Stage 4 - Matched the offset calibration index to the 5V/div range + +- Recorded a controlled reference capture (`WorkingDocs/Win7-zero-positions.pcapng`) + of the original application at 5 V/div, AC, gain factor 1, both channels, 4 ns + timebase, moving each channel's zero marker top/center/bottom and pressing + reset-to-zero. Decoding its `B4` SetOffset writes showed the zero (reset) + position equals the mid-point of the offset DAC range: CH1 range 13..142 + (centre 77), CH2 range 5..133 (centre 68). +- These ranges match the calibration table's third gain slot `(13,143)` / + `(5,134)`, confirming that 5 V/div with gain factor 1 uses calibration index + 2, not index 3. Updated `kChannelLevelRangeIndex` from 3 to 2 in + `capture/src/acquisition_loop.cpp` so the device is seeded with the same + DAC values the original software writes for this range. +- The captured no-signal baseline still sits low (about -5.7 V before pressing + `Set zero`); moving the analog baseline toward the ADC centre for symmetric + headroom is deferred to a later stage. The software zero reference already + yields a correct 0 V reading. + + diff --git a/app/main.cpp b/app/main.cpp index f1ea2ef..fb2fa0c 100644 --- a/app/main.cpp +++ b/app/main.cpp @@ -142,6 +142,28 @@ static void drawOscilloscopeGrid( const ImVec2 &size ); +/** + * @brief Draws one channel's waveform as a polyline into a draw list + * @param[in] drawList ImGui draw list to render into + * @param[in] position Top-left corner of the plot area in screen space + * @param[in] size Plot-area dimensions in pixels + * @param[in] samples Raw ADC bytes to plot, one per horizontal step + * @param[in] sampleCount Number of samples to read from @p samples + * @param[in] profile Scaling profile giving the ADC-to-division mapping + * @param[in] zeroReference Raw ADC level treated as zero volts for this channel + * @param[in] color Polyline color + */ +static void drawChannelWaveform( + ImDrawList *drawList, + const ImVec2 &position, + const ImVec2 &size, + const uint8_t *samples, + size_t sampleCount, + const SInstrumentScalingProfile *profile, + double zeroReference, + ImU32 color +); + /** * @brief Checks whether a device is still present in a scan result * @param[in] scanResult Latest supported-device scan result @@ -330,6 +352,8 @@ int main (void) { bool channelEnabled[] = {true, true}; int timebase = 6; int voltsPerDivision[] = {7, 7}; + float channelZeroReference[] = {128.0f, 128.0f}; + float channelBaselineMean[] = {128.0f, 128.0f}; SUsbScanResult usbScanResult = oscilloscope::usb::enumerateSupportedDevices(); bool demoMode = @@ -390,6 +414,27 @@ int main (void) { ) ) { hasWaveform = true; + /* Track per-channel mean baseline for the Set zero action. */ + if (latestWaveform.sampleCount != 0U) { + unsigned long sumOne = 0UL; + unsigned long sumTwo = 0UL; + size_t sampleIndex = 0U; + + for ( + sampleIndex = 0U; + sampleIndex < latestWaveform.sampleCount; + ++sampleIndex + ) { + sumOne += latestWaveform.channelOne[sampleIndex]; + sumTwo += latestWaveform.channelTwo[sampleIndex]; + } + channelBaselineMean[0] = static_cast( + static_cast(sumOne) / latestWaveform.sampleCount + ); + channelBaselineMean[1] = static_cast( + static_cast(sumTwo) / latestWaveform.sampleCount + ); + } } if (ImGui::BeginMainMenuBar()) { @@ -428,6 +473,9 @@ int main (void) { ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoBringToFrontOnFocus ); + const SInstrumentScalingProfile *scalingProfile = + resolveActiveScalingProfile(connectedDevice, usbScanResult); + ImGui::BeginGroup(); ImGui::TextUnformatted("Display"); ImVec2 displaySize = ImGui::GetContentRegionAvail(); @@ -440,11 +488,31 @@ int main (void) { true, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse ); - drawOscilloscopeGrid( - ImGui::GetWindowDrawList(), - ImGui::GetCursorScreenPos(), - ImGui::GetContentRegionAvail() - ); + ImDrawList *waveformDrawList = ImGui::GetWindowDrawList(); + const ImVec2 waveformOrigin = ImGui::GetCursorScreenPos(); + const ImVec2 waveformSize = ImGui::GetContentRegionAvail(); + + drawOscilloscopeGrid(waveformDrawList, waveformOrigin, waveformSize); + if (hasWaveform && (latestWaveform.sampleCount != 0U)) { + if (channelEnabled[0]) { + drawChannelWaveform( + waveformDrawList, waveformOrigin, waveformSize, + latestWaveform.channelOne.data(), + latestWaveform.sampleCount, scalingProfile, + static_cast(channelZeroReference[0]), + IM_COL32(255, 214, 0, 255) + ); + } + if (channelEnabled[1]) { + drawChannelWaveform( + waveformDrawList, waveformOrigin, waveformSize, + latestWaveform.channelTwo.data(), + latestWaveform.sampleCount, scalingProfile, + static_cast(channelZeroReference[1]), + IM_COL32(64, 200, 255, 255) + ); + } + } ImGui::EndChild(); statusPosition = ImGui::GetCursorScreenPos(); ImGui::EndGroup(); @@ -548,8 +616,6 @@ int main (void) { ImGui::EndDisabled(); } - const SInstrumentScalingProfile *scalingProfile = - resolveActiveScalingProfile(connectedDevice, usbScanResult); std::vector timebaseLabels; std::vector voltageScaleLabels; @@ -595,6 +661,10 @@ int main (void) { ); ImGui::PopID(); } + if (ImGui::Button("Set zero", ImVec2(-1.0f, 0.0f))) { + channelZeroReference[0] = channelBaselineMean[0]; + channelZeroReference[1] = channelBaselineMean[1]; + } ImGui::Separator(); if (ImGui::Checkbox("Demo mode", &demoMode)) { updateDemoMode( @@ -624,14 +694,14 @@ int main (void) { latestWaveform.channelOne[triggerSampleIndex], scalingProfile->voltageSteps[voltsPerDivision[0]] .valuePerDivision, - scalingProfile->adcCenterValue, + static_cast(channelZeroReference[0] + 0.5f), scalingProfile->adcCountsPerDivision ); const double channelTwoVolts = sampleToVolts( latestWaveform.channelTwo[triggerSampleIndex], scalingProfile->voltageSteps[voltsPerDivision[1]] .valuePerDivision, - scalingProfile->adcCenterValue, + static_cast(channelZeroReference[1] + 0.5f), scalingProfile->adcCountsPerDivision ); const double triggerSeconds = sampleIndexToSeconds( @@ -1024,4 +1094,47 @@ static void drawOscilloscopeGrid( ); } } +/*----------------------------------------------------------------------------*/ + +/** @fn drawChannelWaveform */ +static void drawChannelWaveform( + ImDrawList *drawList, + const ImVec2 &position, + const ImVec2 &size, + const uint8_t *samples, + size_t sampleCount, + const SInstrumentScalingProfile *profile, + double zeroReference, + ImU32 color +) { + std::vector points; + const float centerY = position.y + size.y * 0.5f; + size_t index = 0U; + + if ((samples != NULL) && (profile != NULL) && (sampleCount > 1U)) { + const float pixelsPerDivision = + static_cast(size.y / profile->verticalDivisions); + + points.reserve(sampleCount); + for (index = 0U; index < sampleCount; ++index) { + const double divisions = + (static_cast(samples[index]) - zeroReference) / + profile->adcCountsPerDivision; + const float x = position.x + size.x * + static_cast(index) / + static_cast(sampleCount - 1U); + const float y = centerY - + static_cast(divisions) * pixelsPerDivision; + + points.push_back(ImVec2(x, y)); + } + drawList->AddPolyline( + points.data(), + static_cast(points.size()), + color, + ImDrawFlags_None, + 1.5f + ); + } +} /******************************************************************************/ diff --git a/capture/src/acquisition_loop.cpp b/capture/src/acquisition_loop.cpp index 4778fe9..886375e 100644 --- a/capture/src/acquisition_loop.cpp +++ b/capture/src/acquisition_loop.cpp @@ -121,10 +121,15 @@ static const uint8_t kDso2250GetLogicalData[2] = { 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; +/** Calibration entry index for the active 5V/div, gain-factor-1 offset step + (matches the same-device 5V/div capture that resets to DAC 77/68 counts) */ +static const size_t kChannelLevelRangeIndex = 2U; +/** High-nibble marker for the CH1 offset DAC high byte */ +static const uint8_t kOffsetDacMarkerCh1 = 0x20U; +/** High-nibble marker for the CH2 offset DAC high byte */ +static const uint8_t kOffsetDacMarkerCh2 = 0x30U; +/** High-nibble marker for the trigger-level DAC high byte */ +static const uint8_t kOffsetDacMarkerTrigger = 0x20U; /** Centered trigger-level DAC value */ static const uint8_t kDefaultTriggerOffsetByte = 0x7FU; @@ -203,13 +208,13 @@ static usb::SUsbTransferResult configureDso2250Timebase( ); /** - * @brief Computes the centered offset DAC byte for one channel + * @brief Computes the centered offset DAC value 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 + * @returns 12-bit calibration range midpoint DAC value for the 5V range */ -static uint8_t channelLevelCenterByte( +static uint16_t channelLevelCenter( const uint8_t *channelLevels, uint8_t channelIndex ); @@ -630,8 +635,8 @@ static usb::SUsbTransferResult readCaptureData( } /*----------------------------------------------------------------------------*/ -/** @fn channelLevelCenterByte */ -static uint8_t channelLevelCenterByte( +/** @fn channelLevelCenter */ +static uint16_t channelLevelCenter( const uint8_t *channelLevels, uint8_t channelIndex ) { @@ -640,18 +645,18 @@ static uint8_t channelLevelCenterByte( 2U * 2U ); const uint16_t offsetStart = static_cast( - channelLevels[base] | - (static_cast(channelLevels[base + 1U]) << 8U) + (static_cast(channelLevels[base]) << 8U) | + channelLevels[base + 1U] ); const uint16_t offsetEnd = static_cast( - channelLevels[base + 2U] | - (static_cast(channelLevels[base + 3U]) << 8U) + (static_cast(channelLevels[base + 2U]) << 8U) | + channelLevels[base + 3U] ); const uint32_t center = ( static_cast(offsetStart) + static_cast(offsetEnd) ) / 2U; - return static_cast(center >> 8U); + return static_cast(center); } /*----------------------------------------------------------------------------*/ @@ -760,14 +765,15 @@ static usb::SUsbTransferResult configureCapture( 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). */ + /* Offset DAC write: each channel and the trigger use a 12-bit DAC value + stored as [marker | value>>8 (low nibble)] in the high byte and the + value low byte next. CH1/CH2 values are seeded from the per-unit + calibration table read below so the zero-volt level lands at + mid-scale; the trigger level is centered (no calibration involved). */ uint8_t offset[17] = { - kOffsetDacMarkerByte, 0U, - kOffsetDacMarkerByte, 0U, - kOffsetDacMarkerByte, kDefaultTriggerOffsetByte, + kOffsetDacMarkerCh1, 0U, + kOffsetDacMarkerCh2, 0U, + kOffsetDacMarkerTrigger, kDefaultTriggerOffsetByte, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U }; usb::SUsbTransferResult result = { @@ -826,8 +832,19 @@ static usb::SUsbTransferResult configureCapture( ); } if (result.status == usb::EUsbTransferStatus::eSuccess) { - offset[1] = channelLevelCenterByte(channelLevels, 0U); - offset[3] = channelLevelCenterByte(channelLevels, 1U); + const uint16_t centerChannelOne = + channelLevelCenter(channelLevels, 0U); + const uint16_t centerChannelTwo = + channelLevelCenter(channelLevels, 1U); + + offset[0] = static_cast( + kOffsetDacMarkerCh1 | ((centerChannelOne >> 8U) & 0x0FU) + ); + offset[1] = static_cast(centerChannelOne & 0xFFU); + offset[2] = static_cast( + kOffsetDacMarkerCh2 | ((centerChannelTwo >> 8U) & 0x0FU) + ); + offset[3] = static_cast(centerChannelTwo & 0xFFU); *failedOperation = EAcquisitionOperation::eSetOffsetCmd; result = usb::controlWrite( connection,