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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 52 additions & 1 deletion HISTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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.


131 changes: 122 additions & 9 deletions app/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 =
Expand Down Expand Up @@ -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<float>(
static_cast<double>(sumOne) / latestWaveform.sampleCount
);
channelBaselineMean[1] = static_cast<float>(
static_cast<double>(sumTwo) / latestWaveform.sampleCount
);
}
}

if (ImGui::BeginMainMenuBar()) {
Expand Down Expand Up @@ -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();
Expand All @@ -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<double>(channelZeroReference[0]),
IM_COL32(255, 214, 0, 255)
);
}
if (channelEnabled[1]) {
drawChannelWaveform(
waveformDrawList, waveformOrigin, waveformSize,
latestWaveform.channelTwo.data(),
latestWaveform.sampleCount, scalingProfile,
static_cast<double>(channelZeroReference[1]),
IM_COL32(64, 200, 255, 255)
);
}
}
ImGui::EndChild();
statusPosition = ImGui::GetCursorScreenPos();
ImGui::EndGroup();
Expand Down Expand Up @@ -548,8 +616,6 @@ int main (void) {
ImGui::EndDisabled();
}

const SInstrumentScalingProfile *scalingProfile =
resolveActiveScalingProfile(connectedDevice, usbScanResult);
std::vector<const char*> timebaseLabels;
std::vector<const char*> voltageScaleLabels;

Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -624,14 +694,14 @@ int main (void) {
latestWaveform.channelOne[triggerSampleIndex],
scalingProfile->voltageSteps[voltsPerDivision[0]]
.valuePerDivision,
scalingProfile->adcCenterValue,
static_cast<uint8_t>(channelZeroReference[0] + 0.5f),
scalingProfile->adcCountsPerDivision
);
const double channelTwoVolts = sampleToVolts(
latestWaveform.channelTwo[triggerSampleIndex],
scalingProfile->voltageSteps[voltsPerDivision[1]]
.valuePerDivision,
scalingProfile->adcCenterValue,
static_cast<uint8_t>(channelZeroReference[1] + 0.5f),
scalingProfile->adcCountsPerDivision
);
const double triggerSeconds = sampleIndexToSeconds(
Expand Down Expand Up @@ -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<ImVec2> 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<float>(size.y / profile->verticalDivisions);

points.reserve(sampleCount);
for (index = 0U; index < sampleCount; ++index) {
const double divisions =
(static_cast<double>(samples[index]) - zeroReference) /
profile->adcCountsPerDivision;
const float x = position.x + size.x *
static_cast<float>(index) /
static_cast<float>(sampleCount - 1U);
const float y = centerY -
static_cast<float>(divisions) * pixelsPerDivision;

points.push_back(ImVec2(x, y));
}
drawList->AddPolyline(
points.data(),
static_cast<int>(points.size()),
color,
ImDrawFlags_None,
1.5f
);
}
}
/******************************************************************************/
63 changes: 40 additions & 23 deletions capture/src/acquisition_loop.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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
);
Expand Down Expand Up @@ -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
) {
Expand All @@ -640,18 +645,18 @@ static uint8_t channelLevelCenterByte(
2U * 2U
);
const uint16_t offsetStart = static_cast<uint16_t>(
channelLevels[base] |
(static_cast<uint16_t>(channelLevels[base + 1U]) << 8U)
(static_cast<uint16_t>(channelLevels[base]) << 8U) |
channelLevels[base + 1U]
);
const uint16_t offsetEnd = static_cast<uint16_t>(
channelLevels[base + 2U] |
(static_cast<uint16_t>(channelLevels[base + 3U]) << 8U)
(static_cast<uint16_t>(channelLevels[base + 2U]) << 8U) |
channelLevels[base + 3U]
);
const uint32_t center = (
static_cast<uint32_t>(offsetStart) + static_cast<uint32_t>(offsetEnd)
) / 2U;

return static_cast<uint8_t>(center >> 8U);
return static_cast<uint16_t>(center);
}

/*----------------------------------------------------------------------------*/
Expand Down Expand Up @@ -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 = {
Expand Down Expand Up @@ -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<uint8_t>(
kOffsetDacMarkerCh1 | ((centerChannelOne >> 8U) & 0x0FU)
);
offset[1] = static_cast<uint8_t>(centerChannelOne & 0xFFU);
offset[2] = static_cast<uint8_t>(
kOffsetDacMarkerCh2 | ((centerChannelTwo >> 8U) & 0x0FU)
);
offset[3] = static_cast<uint8_t>(centerChannelTwo & 0xFFU);
*failedOperation = EAcquisitionOperation::eSetOffsetCmd;
result = usb::controlWrite(
connection,
Expand Down
Loading