| title | mini-scope – User Guide |
|---|---|
| subtitle | Watch and record dsPIC33CK variables live, without halting the CPU |
| date | October 22, 2026 |
A debugger has to halt the CPU to read variables, which is harmful in a motor control application. mini-scope reads the variables while the application keeps running: a small module in the controller samples the selected variables at a fixed rate and sends them over a UART to the PC, where they are shown as curves in the browser.
There are two modes:
- Stream: continuous display, like a chart recorder. Suited to slow signals such as speed, temperature or states.
- Capture: records a short time window at any sample rate, started by a trigger such as a current rise or a fault. Works like a storage oscilloscope.
| Item | Purpose |
|---|---|
| Python 3.9 or later | PC user interface; the first start installs everything else |
| USB-UART adapter, 3.3 V, 115200 baud | connection to the board, galvanically isolated if possible |
scope.c and scope.h in the firmware project |
module in the controller |
| the project's ELF file | lets you pick variables by name |
It also works without hardware, see section 7.
- Add
firmware\scope.candfirmware\scope.hto the MPLAB X project. - Call
scope_init();once inmain(), after the clock and pin setup.scope_init()configures PPS, UART, timer and interrupts. Nothing else is needed. - If your application already uses UART1 or Timer1, select a free one at
the top of
scope.h:SCOPE_UART1, 2 or 3, andSCOPE_TIMERSCOPE_T1,SCOPE_CCP1…SCOPE_CCP4orSCOPE_NONE. To use other pins than the ICSP header, setSCOPE_TX_RPandSCOPE_RX_RP(RBn = RP(32 + n), RCn = RP(48 + n)). Direction and digital mode are set automatically. The values are listed in section 10. - If RAM is tight, reduce the buffers in
scope.h. By default mini-scope uses about 4.7 KB. WithSCOPE_CAPBUF 1024u,SCOPE_TXBUF 256uandSCOPE_MAX_CH 8it uses about 1.4 KB. The full table is inREADME.md. The user interface adapts automatically. - Check the peripheral clock:
SCOPE_FP_HZinscope.hmust equal FOSC/2. The default is 32 MHz. - Build and program.
All settings are described in README.md.
By default the data runs over the ICSP header: TX on PGD (RB5), RX on PGC (RB6).
- Program the firmware with the PICkit, then disconnect the PICkit.
- Connect the USB-UART adapter to the ICSP header:
| ICSP pin | Signal | Adapter |
|---|---|---|
| 3 | GND | GND |
| 4 | PGD = TX from the controller | RX |
| 5 | PGC = RX to the controller | TX |
| 2 | 3V3 | do not connect |
- Power the board from its own supply.
Debugging and mini-scope cannot run at the same time, because both use the same pins.
Double-click start_gui.bat.
On the first start it looks for Python 3.9 or later, creates a Python
environment .venv in the project folder and installs the required packages.
This takes about a minute and needs internet access (behind a proxy, set
HTTPS_PROXY first). Later starts take a few seconds. The browser opens at
http://localhost:8080.
From top to bottom the page has these areas:
| Area | Contents |
|---|---|
| Title bar | Save, Save as ..., Load from ... for the settings |
| Connection | Port, Baud, status line, Connect button |
| ELF file | path to the ELF, Load button |
| Channels | one row per variable: Variable, Address, type, Factor, delete |
| Write variable | next to the channels: one row per variable: Variable, Address, type, Value, write, delete |
| Settings | Stream or Capture, Sample period, trigger |
| Buttons | Start, Stop (only the one that applies is filled: Start while stopped, Stop while running), Autoscale, Reset (restarts the target firmware), CSV, PNG, Note for the PNG |
| Chart | curves, legend, zoom slider |
| Cursors and measurements | below the chart: time and value cursors, cursor readout, table with the measurements |
- Under Port, select the adapter's COM port.
- Leave Baud at 115200 unless the firmware uses a different rate.
- Click Connect. The status line must show "connected, target responds". The button now reads Disconnect, and Port and Baud are locked while connected.
- Disconnect stops a running measurement and releases the COM port, for example to program the board again.
- Click Browse … and select the ELF file of your project in the file
dialog. It is usually
…\MyProject.X\dist\default\production\MyProject.X.production.elf. The file is loaded right away. Alternatively, type or paste the path into ELF file and click Load. The number of variables found appears next to the buttons. - Click the Variable field. A list of all variables with their address and size drops down. Typing filters the list.
- Click a variable. Address and type are filled in automatically.
- Add more variables with + Channel, remove them with the trash can.
The type defines how the bytes are read:
| Type | Meaning |
|---|---|
s8, s16, s32 |
signed, for example int16_t |
u8, u16, u32 |
unsigned, for example uint16_t |
f32 |
float |
A signed type is suggested. For unsigned variables, change it to u… by
hand.
Factor multiplies the value for display. Use it to convert to physical units, for example Q15 to amperes, or to bring signals of different magnitude onto one scale. The factor only affects the display and the CSV export, not the trigger. A changed factor applies at once, also during a measurement.
Without an ELF file, enter the Address directly, for example from the
.map file, and choose the type by hand.
- Select Stream.
- Enter the Sample period µs, for example 1000 for 1 kHz.
- Set Window (samples) to the number of samples to show.
- Click Start.
To the right of the settings, mini-scope shows the data rate the stream needs and the rate the UART can carry. If the sample period is too short, an orange warning appears instead: "⚠ Too fast for the UART …", together with the shortest sample period that still fits. Clicking Start anyway shows the warning again; samples are then dropped and the status line counts the lost frames. Increase the period, use fewer channels, or use Capture instead.
- Select Capture.
- Enter the Sample period µs. A few µs are possible here.
- Set up the trigger:
- Trigger channel: number of the channel in the list, the first one is 0.
- Edge: rising, falling or none. "None" records immediately.
- Threshold (raw): a raw value, without the factor.
- Pretrigger %: how much of the recording lies before the trigger.
- Auto re-arm: when checked, the trigger is re-armed after every recording. Unchecked, you get exactly one recording.
- Click Start. As soon as the condition occurs, the recording appears. The trigger point is at 0 ms.
The recording length is the capture buffer (4 KB by default) divided by the bytes per sample. It is shown to the right of the settings as capture depth. Fewer channels mean a longer recording.
- Autoscale: sets the factors so that all channels use the same range. The channel with the largest values keeps its factor 1; the others get a round factor (1, 2 or 5 × 10ⁿ) that brings them up to it. Channels that are constantly 0 are left alone. It is a snapshot of the data shown: if a signal grows later, click it again. The CSV export then contains the scaled values; set the factors back to 1 for raw values.
- Zoom: mouse wheel in the chart, or drag the slider below it.
- Hide a channel: click its name in the legend.
- Read values: hover over the curve.
Measurements: the table below the chart shows for every channel Min, Max, Pk-Pk, Mean, RMS, and, for periodic signals, frequency, period and duty cycle. The values include the channel factor. They cover the data in the chart, or only the part between the two time cursors when both are set. Frequency, period and duty count the rising crossings through the middle between Min and Max, with 10 % hysteresis against noise; they need at least two full periods. The duty cycle is counted in samples, so with few samples per period it is only accurate to one sample.
Cursors: tick Time cursors, Value cursors or both below the chart, then click into the chart. The first click sets cursor 1, the second cursor 2; after that a click moves the nearer one. With both kinds on, a click sets a time cursor and Shift+click a value cursor. Clear removes all cursors.
- Time cursors T1, T2 (vertical lines): show t1, t2, Δt and 1/Δt, and add the value of every channel at T1 and T2 and their difference to the table. The measurements then cover the range between T1 and T2.
- Value cursors Y1, Y2 (horizontal lines): show y1, y2 and Δy.
Tip: click Stop first, then the curve stands still while you place the cursors.
You can write a new value into a variable while the application runs, also during a measurement, for example a gain or a set point.
- Under Write variable (next to Channels), pick the variable from the list, as for a channel. Without an ELF, enter its address.
- Check the type (
s16forint16_t,u16foruint16_t,f32forfloatand so on) and enter the Value. - Press Enter in the value field, or click the arrow next to it. You can change the value and write again as often as you like.
- + Variable adds a row, so several variables are at hand at the same time; the bin removes a row.
The value is raw, without the channel factor. The target does not answer the write; you see the effect in the curves. Values out of range for the type, and odd addresses for 16- and 32-bit variables, are rejected by the user interface.
Two demo variables in example_main.c are made for trying this out, best with
sine and ramp running as channels in Stream mode:
| Variable | Type | Effect | Default |
|---|---|---|---|
sine_gain |
s16 | amplitude of sine, Q15: 16384 halves it, 0 makes it flat |
32767 |
ramp_step |
u16 | increment of ramp per main loop pass: 2 makes the ramp twice as steep |
1 |
Caution: writing to the wrong address can crash the application or change hardware registers. Only write to variables you know.
CSV and PNG write their file into the folder of the settings file in
use: normally the project folder with mini-scope.json, after Save as ...
or Load from ... the folder of that file (see 6.8). The name carries the
date and time, for example mini-scope_20261022_231107.png; a message shows
the full path.
CSV saves the data currently shown, that is the stream window or the last recording. The first column is the time in ms; the file is comma separated with a decimal point.
PNG saves a picture of 1920 × 1080 pixels, the 16:9 format of a PowerPoint slide, on a white background. It contains:
- on top the text from the field Note for the PNG next to the button, for example board, operating point or what was changed,
- mode, sample period, number of samples, lost frames or the trigger settings, port, baud rate and ELF file,
- the chart as zoomed on the screen, with the cursors and the factors in the legend,
- the measurement table and the cursor readout,
- date and time, small in the bottom right corner.
The picture is drawn on the PC with matplotlib, so it looks the same whatever the browser window size.
The buttons next to the title keep the whole setup: port, baud rate, ELF file, channels, the Write variable rows with their values, mode, sample period, window, trigger and the note for the PNG.
- Save writes
mini-scope.jsonin the project folder. This file is loaded at every start, so the user interface comes up as you left it. - Save as ... asks for a file name, for example one file per board or per measurement task.
- Load from ... opens a file dialog to pick a
.jsonfile.
Port and baud rate are only taken over while not connected, and only if the
port exists on this PC. With start_sim.bat the ELF of the simulator stays
selected. Addresses follow the variable names: when the ELF is loaded, at
start or with Load after a rebuild, every channel and every write row with
a name gets the variable's current address.
Click Stop, then close the console window or press Ctrl+C.
Double-click start_sim.bat. It builds the test project test.X for the
simulator and starts the user interface.
- Port MDB is preselected. Click Connect.
- Wait until the progress bar has finished, typically 40–60 s. During this time MPLAB X starts the simulator in the background and loads the firmware.
- Click Load. The ELF path is already filled in.
- Select variables, for example
sineandramp, then continue as in section 6.
The simulator runs much slower than real time, and data appears with a delay
of a few seconds. That is good enough for a functional test. Details are in
SIMULATOR.md.
The most common problems are listed below. TROUBLESHOOTING.md goes through
all of them step by step, including how to find out the real clock frequency.
| Symptom | Cause and remedy |
|---|---|
| "Python 3.9 or newer was not found" | install Python from python.org with "Add python.exe to PATH" ticked |
| "Package installation failed" | no internet access, or a proxy: set HTTPS_PROXY=http://proxy:port and start again |
| "port open, but no answer from target" | RX and TX swapped, wrong baud rate, scope_init() not called, or SCOPE_FP_HZ does not match the clock |
| Port missing from the list | plug in the adapter and reload the page (F5) |
| "Target rejected the configuration" | too many channels (SCOPE_MAX_CH, default 16) or an odd address on a 16- or 32-bit channel |
| Curves jump, lost frames increase | stream too fast: increase the period or use Capture |
| No capture appears | the trigger condition never occurs: check the threshold as a raw value, or try edge "none" |
| Values look wrong | wrong type, for example s16 instead of u16, or s32 instead of f32 |
| Variables no longer match after rebuilding the firmware | Load the ELF again, the addresses may have moved |
Linker error multiple definition of _T1Interrupt (or _U1RXInterrupt …) |
the application already uses this timer or UART: select another one in scope.h |
| Simulator: progress stalls for more than 90 s | close the console window and run start_sim.bat again |
- 32-bit values are read in two steps. If a higher-priority interrupt writes the variable in between, a single value can be wrong.
- If your code updates a variable in several steps (first set, then correct), mini-scope may catch the intermediate value. Compute the value in a local variable first and write it with a single assignment.
- With very short sample periods, every sample costs an interrupt. In a motor
control application, set
SCOPE_TIMER = SCOPE_NONEand callscope_sample()at the end of the control ISR. Sampling is then synchronous to the PWM.
Values for SCOPE_TX_RP and SCOPE_RX_RP in scope.h, for the
dsPIC33CK64MC103 (36 pins). The default is RB5/RB6 on the ICSP header.
| Port pin | SCOPE_TX_RP / SCOPE_RX_RP |
Package pin (36-pin) | Other functions on the pin | Note |
|---|---|---|---|---|
| RB0 | 32 | 17 | OSCI, CLKI, AN5 | not with a crystal or external clock |
| RB1 | 33 | 18 | OSCO, CLKO, AN6 | not with a crystal |
| RB2 | 34 | 19 | AN1, AN7, ANA0, CMP1D, OA2OUT, INT0 | analog pin, set to digital automatically |
| RB3 | 35 | 20 | PGD2, AN8, OA2IN- | ICSP channel 2 |
| RB4 | 36 | 21 | PGC2, OA2IN+ | ICSP channel 2 |
| RB5 | 37 | 24 | PGD3 | default TX: ICSP channel 3 |
| RB6 | 38 | 25 | PGC3 | default RX: ICSP channel 3 |
| RB7 | 39 | 26 | TDO, AN2 | JTAG must be off (JTAGEN = OFF) |
| RB8 | 40 | 27 | PGD1, AN10, SCL1 | ICSP channel 1, I²C1 |
| RB9 | 41 | 28 | PGC1, AN11, SDA1 | ICSP channel 1, I²C1 |
| RB10 | 42 | 33 | TMS, PWM3H | PWM output, JTAG |
| RB11 | 43 | 34 | TCK, PWM3L | PWM output, JTAG |
| RB12 | 44 | 35 | TDI, PWM2H | PWM output, JTAG |
| RB13 | 45 | 36 | PWM2L | PWM output |
| RB14 | 46 | 1 | PWM1H | PWM output |
| RB15 | 47 | 2 | PWM1L | PWM output |
| RC0 | 48 | 4 | AN12 | analog pin, set to digital automatically |
| RC1 | 49 | 12 | AN13, CMP1B, OA3IN-, ISRC0 | analog pin, set to digital automatically |
| RC2 | 50 | 13 | AN14, OA3IN+, ISRC1 | analog pin, set to digital automatically |
| RC3 | 51 | 16 | AN15, IBIAS2 | analog pin, set to digital automatically |
| RC4 | 52 | 29 | – | free digital pin |
| RC5 | 53 | 30 | – | free digital pin |
Port A (RA0 … RA4) is not remappable and cannot be used. Source: the device
description in the dsPIC33CK-MC_DFP pack (DSPIC33CK64MC103.PIC); the
package pin numbers apply to the 36-pin dsPIC33CK64MC103.
Notes:
- If the debugger uses the same ICSP channel (
ICSconfiguration bit), do not debug while mini-scope runs on these pins. - On motor control boards the PWM pins are usually taken by the power stage.



