IAR Debugging with C-SPY Macros
IAR debugging with C-SPY macros is not a new capability — IAR has offered this scripting layer for many years, and seasoned embedded developers have long used it for boot probes, register inspection, and conditional breakpoints. However, writing a non-trivial .mac file has historically been a niche skill. The syntax is unique, the documentation is dense, and most teams reach for printf or RTT instead. With the arrival of Large Language Models, that barrier is finally falling. LLMs can now generate complex C-SPY macros and orchestration scripts in seconds. As a result, developers can automate the most tedious parts of debugging — monitoring boot-up registers, tracing DMA transitions, hunting timing-sensitive bugs — without writing every line by hand. Moreover, AI-generated logic pairs naturally with IAR’s command-line tools. Teams can now build self-healing CI/CD pipelines and deterministic test suites that catch errors long before the firmware reaches the field.
if somthing doesnt work- look here.
C-SPY Bat and C-SPY Macros in Practice
Introduction
IAR provides two closely related debugging tools. Together, they solve different parts of the same problem:
- C-SPY Bat (cspybat.exe): The headless command-line debugger runner. Essentially, it is the engine that launches and controls a debug session from a script or CI-like flow.
- C-SPY Macro Files (.mac): The automation language used inside the debugger. In other words, it is the playbook that tells the debugger what to do once connected.
In practice, these tools are most useful when failures are timing-sensitive. They also help when bugs occur early in boot, or when raw register state is more reliable than application logs.
Two Ways to Run a Macro: GUI or Command Line
One of the most useful aspects of C-SPY macros is portability. The same .mac file works in both environments — there is no need to maintain two versions of your debug logic.
Inside IAR Embedded Workbench (GUI)
You can attach a macro to a debug session via Project → Options → Debugger → Setup Macros. Alternatively, load it on the fly from the View → Macros menu during a live session. This is ideal for interactive development. You can combine your own automation with the IDE’s source view, watch windows, and disassembly.
From the Command Line (cspybat)
Alternatively, the very same macro can be passed to cspybat.exe via the --macro=<file> argument. As a result, the script you tested interactively can be promoted directly into a CI/CD pipeline. It also works as a one-click reproduction script for a teammate.
Consequently, a typical workflow is to prototype the macro in the GUI — where you can step, inspect, and tweak. Then promote it to cspybat for unattended runs. In other words, the GUI is your laboratory, while cspybat is your factory floor.
1. What C-SPY Bat Is Good For
With cspybat.exe, you can run the same debugger backend found in the IAR IDE — but without the GUI. Consequently, this is ideal for repeatable debugging.
Main Benefits
- Repeatability: A batch flow can rebuild, flash, attach, and run a macro the same way every time. As a result, debugging becomes a controlled experiment.
- Early-Boot Visibility: Many bugs happen before RTT or the UI is reliable. However, cspybat lets you read peripheral registers (like LTDC or DSI) at deterministic times after reset. This makes it easy to see if error flags (like FUIF) are set.
- Automation: Additionally, it fits naturally into .bat or PowerShell wrappers. These work well for nightly validation runs or one-click support scripts.
Common Attach Patterns
--attach_to_running_target: Connects to firmware already executing without a reset.--leave_target_running: Furthermore, this keeps the target alive after the script finishes.
2. What C-SPY Macro Files Are Good For
A .mac file is the heart of IAR debugging with C-SPY macros. It is a debugger scripting language with C-like expressions. However, note that it does not support the C preprocessor (no #define).
Main Benefits
- Direct Automation: It evaluates symbols, places breakpoints, and writes to memory-mapped registers.
- Register-Centric Debugging: For instance, you can use
__readMemory32()and__writeMemory32()to inspect exactly what the hardware sees. This is essential for fixing wrong clock sources or sticky interrupt flags. - Zero-Intrusion Instrumentation: Additionally,
__setCodeBreak()lets you trigger diagnostic messages without editing or recompiling your firmware. - Portable Between GUI and CLI: The same
.macfile runs unchanged in Embedded Workbench and in cspybat. Therefore, debug logic developed interactively can be reused in automated flows.
.
3. C-SPY Bat and C-SPY Macros: Complementary, Not Competing
When practicing IAR debugging with C-SPY macros, it’s tempting to see cspybat and .mac files as alternatives. However, they actually solve two different layers of the same problem.
Two Layers, One Workflow
- cspybat is the driver — it launches the debug session, attaches to the target, downloads firmware, and forwards events to a log. However, it has no opinion about what to do once connected.
- A
.macfile is the brain — it defines what the debugger should actually do. This includes where to break, what registers to read, what messages to print, and when to resume. Yet on its own, a macro is just a text file. It needs a host to execute it.
As a result, the two are designed to work together. The macro carries the debug logic, while either the IDE or cspybat provides the execution environment.
Choosing Where to Run It
- In the IDE: Embedded Workbench loads the
.macfile via the project’s debugger options. Then it runs it inside an interactive session. This is the right choice for development and exploration. - On the command line: cspybat loads the very same
.macfile via--macro=<file>and runs it headlessly. Therefore, this is the right choice for CI, nightly runs, and unattended diagnostics.
In other words, you don’t choose between cspybat and a macro. Instead, you write the macro once and choose which host to run it under depending on the situation.
Probe Support
IAR debugging with C-SPY macros works across every supported probe. Both Embedded Workbench and cspybat use the same underlying C-SPY driver layer. As a result, the supported probes are essentially identical. Specifically, on Arm targets the most common options are:
- I-jet and I-jet Trace — IAR’s native probes. They offer the tightest integration, including ETM trace on supported devices.
- Segger J-Link (all variants) — Widely used and well supported. Ideal when you also want to use RTT for the post-init phase, since RTT is a J-Link feature.
- CMSIS-DAP / DAPLink — The vendor-neutral standard found on many evaluation boards (ST-LINK in CMSIS-DAP mode, MCU-Link, etc.).
- ST-LINK — Supported via its CMSIS-DAP firmware or via the dedicated ST-LINK driver, depending on the IAR version.
- GDB Server — For targets where you connect through an external GDB server (e.g., simulators or third-party probes exposing a GDB interface).
Consequently, the choice of probe is selected via the --backend argument when running cspybat. In the IDE, choose it via Project → Options → Debugger → Driver. Either way, the macro itself is probe-agnostic. Functions like __readMemory32() and __setCodeBreak() behave the same regardless of whether the bits flow over I-jet, J-Link, or CMSIS-DAP. For the authoritative list of supported drivers, refer to the official IAR Embedded Workbench documentation.
4. The API Surface: Essential Commands
. The API Surface: Essential Commands
Common Command-Line Options
--download_only: Flashes the target without starting a full session.--macro=<file>: In addition, this runs a macro automatically upon startup.--backend: Passes options directly to the probe driver (e.g., J-Link).
Useful Macro Functions
__setCodeBreak: The cornerstone of macro-driven debugging. It installs a conditional code breakpoint at a source location and optionally runs a macro action when it fires.__setDataBreak: Installs a hardware data breakpoint (watchpoint) on a memory address.__message: The primary reporting tool. It prints text to the debug log.__readMemory32(address, "Memory"): The workhorse for checking peripheral state.__writeMemory32(value, address, "Memory"): Writes to a memory-mapped register without touching firmware code.__delay(ms): Pauses the macro for a number of milliseconds.__hwReset/__hwResetWithStrategy: Performs a hardware reset on the target.__hwRunToBreakpoint: Resumes execution and waits until a breakpoint is hit.__hwResetRunToBp: Resets the target and runs until a breakpoint is hit.__symbolAddress("name"): Gets the runtime address of a global variable. It is much safer than hard-coding addresses.__clearBreak: Removes a breakpoint by handle.
Understanding __setCodeBreak() — The Cornerstone Function
If there is one function that defines IAR debugging with C-SPY macros, it is __setCodeBreak(). Specifically, it lets you install a breakpoint programmatically. You can specify a condition, a hit count, and an automatic action — without ever touching the IDE’s breakpoint dialog. As a result, the same breakpoint logic that you set up by hand can be captured in a .mac file and replayed identically in CI.
The signature is:
__setCodeBreak(location, count, condition, conditionType, action)
Each parameter plays a distinct role:
location — Where the breakpoint goes
Use the "{file.c}.line" form (e.g., "{voice_recorder.c}.388") for source-level placement. Alternatively, pass an address expression like "main+0x10" for raw locations. The braces around the filename are required.
count — A skip count
0 means “break every time the condition is true.” Conversely, 5 means “let it pass four times, then break on the fifth.” Use this when a bug only appears after a known number of iterations.
condition — A C-style expression
This is evaluated each time the breakpoint is hit. For instance, "buf_index == 42" or "(state == ERROR) && (retry_count > 3)". Passing "1" makes the breakpoint unconditional. Importantly, the symbols you reference must be visible at that source line. Otherwise, C-SPY reports “undefined symbol” at evaluation time.
conditionType — How to interpret the condition
Either "TRUE" (break when the condition is true) or "CHANGED" (break when the value changes since the last hit). The "CHANGED" mode is powerful for catching state machine transitions. You don’t need to know the exact target value in advance.
action — What to do on hit
A macro statement string that runs automatically when the breakpoint fires. Typically, this is either a __message call for inline logging, or a call to another macro function. Pass "" for no action.
Always Check the Return Value
The function returns a breakpoint handle — a non-zero number on success, 0 on failure. Always check the return value. Hardware breakpoint slots are limited (typically 4–8 on Cortex-M devices). Silently failing to install a BP is one of the most common sources of “my macro doesn’t work” frustration.
In other words, __setCodeBreak() is the bridge between static analysis (the line of code you suspect) and dynamic analysis (the runtime condition that actually matters). Combined with __setDataBreak(), it covers virtually every “stop only when something interesting happens” scenario in embedded debugging.
Understanding __setCodeBreak() — The Cornerstone Function
If there is one function that defines IAR debugging with C-SPY macros, it is __setCodeBreak(). Specifically, it lets you install a breakpoint programmatically. You can specify a condition, a hit count, and an automatic action — without ever touching the IDE’s breakpoint dialog. As a result, the same breakpoint logic that you set up by hand can be captured in a .mac file and replayed identically in CI.
****************************
The Debug Session Lifecycle
Here’s the order in which these hooks fire during a typical session:
1. cspybat starts
│
▼
2. Debugger connects to target via probe
│
▼
3. execUserPreload() ← hardware prep BEFORE firmware is loaded
│
▼
4. Firmware downloaded to flash/RAM
│
▼
5. execUserSetup() ← install breakpoints, resolve symbols
│
▼
6. CPU starts running
│
▼
┌────────────────────────────────────────────┐
│ Loop: │
│ execUserExecutionStarted() ← CPU resumes│
│ ... target runs ... │
│ execUserExecutionStopped() ← BP hit / │
│ halt │
│ (your BP action macros fire here) │
└────────────────────────────────────────────┘
│
▼ (if a reset happens at any point)
7. execUserPreReset() ← BEFORE the reset signal
│
▼
[hardware reset occurs]
│
▼
8. execUserReset() ← AFTER the reset
│
▼
9. execUserExit() ← session ending, clean up
│
▼
10. cspybat exits
The C-SPY debugger has its own built-in routine for every debug session:
- Connect to the target
- [hook:
execUserPreload— anything to do here?] - Download firmware
- [hook:
execUserSetup— anything to do here?] - Run the CPU
- [hook:
execUserExecutionStopped— anything to do here when it halts?] - Eventually end the session
- [hook:
execUserExit— anything to clean up?]
If your .mac file defines a function named execUserSetup(), C-SPY calls it at step 4. If you don’t define it, step 4 is silently skipped and the session continues. Same idea for every other reserved name.
Each Hook in Detail
execUserPreload — before firmware is downloaded
Runs after the debugger connects to the target but before your .elf is flashed. Use it for hardware initialization that the chip needs before it can even accept a download.
Typical use: Enabling external SDRAM on a board where your linker places code or data in that SDRAM. Without this hook, the download itself fails because the memory isn’t visible yet.
execUserPreload()
{
__message "[PRELOAD] Enabling external SDRAM controller\n";
__writeMemory32(0x00000001, 0xA0000000, "Memory"); // FMC enable
}
execUserSetup — once, after firmware is loaded
This is the workhorse. It runs exactly once, right after the application is loaded into the target. By the time it fires, all your global symbols are resolvable, breakpoints can be installed, and the CPU is sitting at the reset vector ready to run.
Typical use: Install breakpoints, look up symbol addresses, open log files, print a banner.
execUserSetup()
{
__message "===== SESSION START =====\n";
bp_main = __setCodeBreak("{main.c}.42", 0, "1", "TRUE", "logHit()");
addr_state = __symbolAddress("g_state");
}
This is where 90% of your one-time setup logic belongs.
execUserExecutionStarted — every time the CPU resumes
Fires every time execution begins — at session start when the CPU first runs, and every time you continue from a breakpoint, single-step, etc.
Typical use: Rare. Occasionally useful for measuring how long the CPU was halted, or for re-enabling something you disabled in execUserExecutionStopped.
execUserExecutionStopped — every time the CPU halts
The mirror of the above. Fires every time the CPU pauses — breakpoints, stepping, manual halt, etc.
Typical use: Useful but easy to overuse. The earlier link you shared (the Analog Devices forum post) used this hook to halt peripheral clocks on every breakpoint, so timers don’t keep ticking while you’re inspecting state.
execUserExecutionStopped()
{
// Freeze GPT clocks so timers stay still while we inspect.
__writeMemory32(0x07, 0x40050020, "Memory");
}
⚠️ Be careful: this fires on EVERY halt, including single-steps. If you do something heavy here, stepping becomes glacial.
execUserPreReset and execUserReset — wrap a reset
These two flank a target reset, like brackets:
execUserPreResetruns before the reset is issuedexecUserResetruns after the chip comes back up
Typical use: Save state before reset, restore it after. Or just log that a reset happened so your log file shows a clear demarcation.
execUserPreReset()
{
__message "----- About to reset -----\n";
}
execUserReset()
{
__message "----- Reset complete, CPU at vector table -----\n";
}
execUserExit — session ending
Runs when the debug session is tearing down. This is your cleanup hook.
Typical use: Remove breakpoints, close any files you opened, print a closing banner.
execUserExit()
{
__clearBreak(bp_main);
__closeFile(log_handle);
__message "===== SESSION END =====\n";
} ***************************************************************8
5. Breakpoint Building Blocks
Before diving into a full example, it helps to understand the two breakpoint primitives. These do most of the heavy lifting in IAR debugging with C-SPY macros.
Conditional Code Breakpoints
In real-world debugging, you often don’t care about every hit on a function. Instead, you only care about the one where something has gone wrong. For instance, imagine a voice recorder firmware where the DMA drain loop runs hundreds of times per second. The bug only appears when the buffer index reaches a specific value. Rather than hitting the breakpoint hundreds of times and stepping through manually, you can let C-SPY filter it for you. Use __setCodeBreak() (covered in detail in section 4) to handle this.
Data Breakpoints (Watchpoints)
Sometimes the bug isn’t where the code runs but who is touching a variable. For instance, a global flag mysteriously flips from 0 to 1. You have no idea which function — or worse, which interrupt — is the culprit. In this situation, a code breakpoint is useless because you don’t know where to put it. However, a data breakpoint (also called a watchpoint) solves this elegantly. The CPU itself halts the moment a specific address is read or written, regardless of which code did it.
On Cortex-M devices, data breakpoints are implemented in hardware via the DWT (Data Watchpoint and Trace) unit. As a result, they are non-intrusive and run at full speed. However, you only get a few of them (typically 4).
The __setDataBreak() function takes the following parameters:
__setDataBreak(zone, address, size, accessType, condition, conditionType, action)
- zone — The memory zone, almost always
"Memory". - address — The exact address to watch (use
__symbolAddress()for safety). - size — Width in bytes:
1,2, or4. - accessType —
"R"(read),"W"(write), or"RW"(either). - condition / conditionType — Same semantics as
__setCodeBreak(). Pass"1"and"TRUE"for an unconditional break. - action — Macro statement executed on hit.
A Few Tips
- Conditional code BPs are evaluated by the debugger on every hit. As a result, a hot loop will slow down noticeably. However, data BPs run in hardware and incur essentially zero overhead.
- DWT slots are scarce. Therefore, if you’ve already used them for vector catches or RTT, the call may silently fall back or fail. Always check the return handle.
- Use
__symbolAddress()instead of hard-coded addresses. Otherwise, your macro breaks the moment the linker rearranges RAM. accessType = "RW"is powerful but noisy. As a result, prefer"W"first when hunting corruption bugs. Reads are usually innocent.- Variable scope matters for code BPs. The condition expression must reference a symbol visible at that line. Otherwise, C-SPY reports “undefined symbol” at evaluation time.
Conclusion
Ultimately, IAR debugging with C-SPY macros is strongest when cspybat and .mac files are used together. While cspybat automates the session, .mac files automate the debug logic inside that session. Furthermore, the same macro runs unchanged in both Embedded Workbench and on the command line. It also works across every supported probe, from I-jet to J-Link to CMSIS-DAP. As a result, your interactive debug experiments translate directly into automated CI flows. For STM32 bring-up and fragile timing bugs, this combination is often more reliable than adding temporary application logs. It’s also lower-intrusion. Once the system is past initialization and tasks are running,