Skip to content

feat: add pattern generator SCPI commands - #202

Open
IM-TechieScientist wants to merge 5 commits into
fossasia:dev26from
IM-TechieScientist:digital-pattern-generator-scpi
Open

feat: add pattern generator SCPI commands#202
IM-TechieScientist wants to merge 5 commits into
fossasia:dev26from
IM-TechieScientist:digital-pattern-generator-scpi

Conversation

@IM-TechieScientist

@IM-TechieScientist IM-TechieScientist commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Adds the SCPI/application layer for the digital pattern generator introduced in #199.

This PR wires the pattern generator core into the SCPI command table and adds the protocol handlers needed to configure/start/stop pattern output from the host.

This is meant to be merged after #199 and thus is stacked on top.

Summary by Sourcery

Integrate the new digital pattern generator into the SCPI protocol stack and platform, providing host-configurable pattern output over PIO/DMA.

New Features:

  • Expose SCPI commands to configure, upload data to, start, stop, and query status of the digital pattern generator.
  • Introduce a system-level pattern generator abstraction managing configuration, modes, and run state over the low-level pattern output backend.
  • Add a PIO/DMA-based low-level pattern output driver for generating digital patterns on configurable GPIO pins.

Enhancements:

  • Wire the pattern generator into the common protocol reset and main protocol task loop so it is reset and serviced with other instruments.
  • Extend the build configuration to compile the new pattern generator application, system, and platform modules.

@sourcery-ai

sourcery-ai Bot commented Jul 28, 2026

Copy link
Copy Markdown

Reviewer's Guide

Adds a complete SCPI-facing digital pattern generator, wiring the new pattern generator core into the protocol stack, introducing a mid-layer controller, and a PIO/DMA-based low-level pattern output engine, plus integrating periodic servicing and reset into the existing application framework.

Sequence diagram for SCPI PG:STARt command path

sequenceDiagram
    participant Host
    participant SCPI_Server as SCPI_Server
    participant PG_SCPI as scpi_cmd_pattern_generator_*
    participant PG_App as pg_*
    participant PatternGenerator as pattern_generator_*
    participant PatternOutputLL as pattern_output_ll_*

    Host->>SCPI_Server: PG:DATA <block>
    SCPI_Server->>PG_SCPI: scpi_cmd_pattern_generator_data
    PG_SCPI->>PG_App: pg_upload_data
    PG_App-->>PG_SCPI: bool

    Host->>SCPI_Server: PG:STARt
    SCPI_Server->>PG_SCPI: scpi_cmd_pattern_generator_start
    PG_SCPI->>PG_App: pg_start
    PG_App->>PatternGenerator: pattern_generator_start
    PatternGenerator->>PatternOutputLL: pattern_output_ll_start
    PatternOutputLL-->>PatternGenerator: bool
    PatternGenerator-->>PG_App: bool
    PG_App-->>PG_SCPI: bool
    PG_SCPI-->>SCPI_Server: SCPI_RES_OK / SCPI_RES_ERR
Loading

File-Level Changes

Change Details Files
Wire pattern generator SCPI commands into the common protocol, lifecycle, and main protocol task loop.
  • Register PG SCPI handlers in the global SCPI command table with PG:CONFigure, PG:DATA, PG:STARt/STOP, and PG:STATus? mnemonics
  • Add forward declarations for pattern generator SCPI handlers and pg_reset_state/pg_task usage in the common protocol module
  • Reset pattern generator state as part of protocol_reset and invoke pg_task() from protocol_task()
src/application/protocol/common.c
src/application/protocol/pg.c
src/application/pattern_generator_commands.h
src/application/pattern_generator_commands.c
Introduce an application-level pattern generator controller that validates configuration, owns pattern buffers, and exposes a small API used by SCPI handlers.
  • Maintain pattern generator configuration (pins, rate, mode, pattern length) and a static pattern buffer with bounds and validity checks
  • Lazily initialise and reconfigure the underlying system-level PatternGenerator as needed, guarding against changes while running
  • Expose helper functions for setting pins, rate, mode, uploading data, start/stop, status queries, and periodic task handling
src/application/pattern_generator_commands.c
src/application/pattern_generator_commands.h
Add a system-level PatternGenerator abstraction that wraps the low-level PIO/DMA pattern output backend.
  • Define PatternGeneratorConfig and PatternGeneratorMode types and a PatternGenerator struct owning platform-level state and run state
  • Implement init/configure/deinit that validate configs and (re)configure the underlying PatternOutputLL while disallowing changes while running
  • Implement start/stop/task/is_running to manage pattern lifetime and polling completion via the low-level backend
src/system/pattern_generator.c
src/system/pattern_generator.h
Implement a PIO + DMA-based low-level pattern output engine for driving up to 8 pins at a given rate, with optional looping via chained DMA.
  • Provide PatternOutputLLConfig/PatternOutputLL structures and a default_config helper that sets sensible PIO/SM defaults
  • Load and unload a minimal two-instruction PIO program that PULLs data and OUTs to pins, including wrap and clkdiv configuration
  • Set up DMA channels (data and control), including bus priority tweaks, to stream 32-bit words into the PIO TX FIFO, with optional chained looping
  • Expose configure/start/stop/is_busy APIs that enforce pin/rate limits, manage state machine enable/disable, DMA aborts, and FIFO resets
src/platform/pattern_output_ll.c
src/platform/pattern_output_ll.h
Update build configuration to compile the new pattern generator protocol, application, system, and platform modules.
  • Add pg.c, pattern_generator_commands.c, pattern_generator.c, and pattern_output_ll.c to the main target_sources list
CMakeLists.txt

Possibly linked issues

  • #0: PR fully implements the SCPI digital pattern generator described in the issue, including PG:CONF, DATA, START, STOP.

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@IM-TechieScientist
IM-TechieScientist marked this pull request as ready for review July 28, 2026 12:22
Copilot AI review requested due to automatic review settings July 28, 2026 12:22

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue, and left some high level feedback:

  • The pin/rate validation logic is duplicated with slightly different constraints between the SCPI layer (e.g. PG_MAX_PIN_COUNT, PG_MAX_RATE_HZ, config_is_valid in pattern_generator_commands.c) and the system layer (config_is_valid in pattern_generator.c); consider centralizing these constraints to a single place to avoid drift and make future changes safer.
  • In pattern_output_ll_init you unconditionally set bus_ctrl_hw->priority to DMA RW priority, which globally affects the bus for the whole system; if other subsystems rely on different priorities it may be worth capturing and restoring the previous value or moving this to a more global/central init path instead of doing it per-pattern-generator instance.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The pin/rate validation logic is duplicated with slightly different constraints between the SCPI layer (e.g. `PG_MAX_PIN_COUNT`, `PG_MAX_RATE_HZ`, `config_is_valid` in `pattern_generator_commands.c`) and the system layer (`config_is_valid` in `pattern_generator.c`); consider centralizing these constraints to a single place to avoid drift and make future changes safer.
- In `pattern_output_ll_init` you unconditionally set `bus_ctrl_hw->priority` to DMA RW priority, which globally affects the bus for the whole system; if other subsystems rely on different priorities it may be worth capturing and restoring the previous value or moving this to a more global/central init path instead of doing it per-pattern-generator instance.

## Individual Comments

### Comment 1
<location path="src/application/pattern_generator_commands.c" line_range="43-44" />
<code_context>
+
+static bool apply_config(void)
+{
+    if (!config_is_valid()) {
+        return false;
+    }
+
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Align configuration validation limits with low-level constraints to improve error consistency

`config_is_valid` only enforces `rate_hz <= PG_MAX_RATE_HZ`, while `pattern_output_ll_configure` additionally requires `clk_div >= 1.0f` based on the actual peripheral clock. As a result, some rates that pass this check will still be rejected later with less clear SCPI errors.

To make failures consistent and predictable, either derive `PG_MAX_RATE_HZ` from `PLATFORM_get_peripheral_clock_speed` (or a documented worst-case), or replicate the `clk_div >= 1` constraint here so all invalid configs are rejected at the same layer with a clear, shared limit.

Suggested implementation:

```c
static bool config_is_valid(void)
{
    /* Ensure configuration constraints match low-level limits so invalid
     * configurations are rejected consistently before calling into the
     * pattern_output_ll layer.
     */
    const uint32_t peripheral_clk_hz = PLATFORM_get_peripheral_clock_speed();
    const float clk_div = (float)peripheral_clk_hz / (float)state.rate_hz;

    return state.pin_count >= 1 &&
           state.pin_count <= PG_MAX_PIN_COUNT &&
           state.pin_base + state.pin_count <= 30 &&
           state.rate_hz >= 1 &&
           state.rate_hz <= PG_MAX_RATE_HZ &&
           clk_div >= 1.0f;
}

```

```c
#include <string.h>

#include "system/pattern_generator.h"
#include "system/platform.h"

```

To keep this check perfectly aligned with `pattern_output_ll_configure`, ensure that the `clk_div` computation here matches whatever that function uses (e.g., if it divides by additional factors such as prescalers or pattern word widths, mirror that logic instead of the simple `peripheral_clk_hz / state.rate_hz` used above). If `PLATFORM_get_peripheral_clock_speed` is already declared via another header in this translation unit, you can omit the additional `#include "system/platform.h"` to avoid redundant includes.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +43 to +44
if (!config_is_valid()) {
return false;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (bug_risk): Align configuration validation limits with low-level constraints to improve error consistency

config_is_valid only enforces rate_hz <= PG_MAX_RATE_HZ, while pattern_output_ll_configure additionally requires clk_div >= 1.0f based on the actual peripheral clock. As a result, some rates that pass this check will still be rejected later with less clear SCPI errors.

To make failures consistent and predictable, either derive PG_MAX_RATE_HZ from PLATFORM_get_peripheral_clock_speed (or a documented worst-case), or replicate the clk_div >= 1 constraint here so all invalid configs are rejected at the same layer with a clear, shared limit.

Suggested implementation:

static bool config_is_valid(void)
{
    /* Ensure configuration constraints match low-level limits so invalid
     * configurations are rejected consistently before calling into the
     * pattern_output_ll layer.
     */
    const uint32_t peripheral_clk_hz = PLATFORM_get_peripheral_clock_speed();
    const float clk_div = (float)peripheral_clk_hz / (float)state.rate_hz;

    return state.pin_count >= 1 &&
           state.pin_count <= PG_MAX_PIN_COUNT &&
           state.pin_base + state.pin_count <= 30 &&
           state.rate_hz >= 1 &&
           state.rate_hz <= PG_MAX_RATE_HZ &&
           clk_div >= 1.0f;
}
#include <string.h>

#include "system/pattern_generator.h"
#include "system/platform.h"

To keep this check perfectly aligned with pattern_output_ll_configure, ensure that the clk_div computation here matches whatever that function uses (e.g., if it divides by additional factors such as prescalers or pattern word widths, mirror that logic instead of the simple peripheral_clk_hz / state.rate_hz used above). If PLATFORM_get_peripheral_clock_speed is already declared via another header in this translation unit, you can omit the additional #include "system/platform.h" to avoid redundant includes.

@Saksham-Sirohi Saksham-Sirohi left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot flagged the following:

  • Rebase onto #199 before merge.

@CloudyPadmal CloudyPadmal added the conflicts has conflicts that need to be resolved before moving forward label Jul 29, 2026
@IM-TechieScientist
IM-TechieScientist force-pushed the digital-pattern-generator-scpi branch from 4ddd628 to 609ea52 Compare July 31, 2026 13:16
@IM-TechieScientist

Copy link
Copy Markdown
Contributor Author

Rebased and fixed conflicts

@CloudyPadmal CloudyPadmal removed the conflicts has conflicts that need to be resolved before moving forward label Aug 1, 2026
enum {
PG_DEFAULT_PIN_BASE = 16,
PG_DEFAULT_PIN_COUNT = 1,
PG_DEFAULT_RATE_HZ = 1000,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't this default rate conflict with the clk_div min check?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, ive fixed it now

state.pin_base = pin_base;
state.pin_count = pin_count;

if (pg_initialized && !apply_config()) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't pg_initialized be set to false here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have made changes to set it to false

@CloudyPadmal CloudyPadmal left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you have any test results from the max rate @ the advertised 75 MHz?

@CloudyPadmal CloudyPadmal left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just noticed that there is no README update for PG commands

@IM-TechieScientist

Copy link
Copy Markdown
Contributor Author

added readme docs for the commands

@IM-TechieScientist

Copy link
Copy Markdown
Contributor Author

@CloudyPadmal about the test results. I can verify it works properly until around 25 Mhz. This is using the pslab pico's logic analyser and pulseview.

image

While I can see the waveforms at 75 Mhz, the timings and shape of waveform is a bit off, my guess would be this is because I am sampling at 150 mhz and the signal itself is 75, we are not sampling at the correct required rate. Could also be due to amateur wiring quality worsening signal quality at high frequencies.
image
image

The PG works at 75 mhz, just that I can't verify the timing since I don't have access to a proper faster logic analyser.

@CloudyPadmal

Copy link
Copy Markdown
Contributor

I would like to take a second look at PG_MAX_RATE_HZ = 75000000

The number itself is consistent as 150 MHz sysclk/2 instructions per sample gives 1.0 clock divider, so the config is accepted. The problem is what happens after that. The PIO does a PULL for every sample and discards the unused bits, so each sample costs a full 32-bit word. At 75 MHz that is 300 MB/s of DMA from SRAM into an 8 FIFO, sharing the bus with USB I wonder if it would keep up.

What concerns me more is that we won't know when it doesn't. PULL blocks, so on an underrun the state machine just stalls and holds the last value. The timing stretches, PG:STATus? still reports running, and the user gets a wrong waveform with no error anywhere.

@IM-TechieScientist

Copy link
Copy Markdown
Contributor Author

Okay yes, that makes sense. How would you suggest I move forward?
Should I lower the advertised limit or should I try to pack the pattern bits where we put multiple time samples in one 32 bit word

@CloudyPadmal

Copy link
Copy Markdown
Contributor

I think the latter would be the best

@IM-TechieScientist

Copy link
Copy Markdown
Contributor Author

okay, I will work on this. I think the pr would become bulky if made the changes in this one. Should I make another PR after this one gets merged?

@CloudyPadmal

Copy link
Copy Markdown
Contributor

Since I'm a bit familiar with this PR, let's do the changes here. It'll be easier to go through.

@IM-TechieScientist

Copy link
Copy Markdown
Contributor Author

@CloudyPadmal so I have implemented packed pattern samples.
PIO now does one initial blocking PULL, wraps only the OUT pins, pin_count instruction uses autopull with threshold pin_count * floor(32 / pin_count)

This means one uint32_t now carries multiple samples like
1 pin - 32 samples/word
2 pins - 16 samples/word
and so on.

Allso added explicit PC reset on start() so repeated starts execute the initial PULL.

Also implemented underrun tracking by:
Using PIO FDEBUG_TXSTALL as the underrun/stall indicator.
Clearing TXSTALL immediately before enabling the state machine after DMA setup to avoid startup false positives.
Counting TXSTALL only while LOOP mode is active so normal one shot completion does not get reported as an underrun.

Also added PG:UNDerrun? to query the underrun count.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants