Code Composer Studio Debugging: Breakpoints and Memory Inspection on CC2340
You added a printf to track a variable value. Then another. Then a third inside a loop, which flooded your terminal so badly you added a counter to throttle it. Now you’ve got 14 lines of debug output code wrapped around 6 lines of actual logic, your timing behavior has shifted because UART takes milliseconds you don’t have, and you still can’t figure out why the LED skips every eleventh blink.
There’s a debugger sitting right inside Code Composer Studio that lets you freeze execution on any line, inspect every variable in scope, and read raw memory addresses, including peripheral registers, without adding a single line of code to your project. Most developers new to CCS never touch it beyond clicking the play button. That ends today.
We’ll work through CCS’s debugging tools using a real CC2340R5 LaunchPad and a modified blinky example. By the end, you’ll know how to set conditional breakpoints, step through execution, and inspect memory at the byte level. You need CCS 12.x or later, the SimpleLink CC23xx SDK installed, and your LP-EM-CC2340R5 connected via USB. If you need help with that setup, see our CC2340R5 LaunchPad setup guide.
What the Debug Perspective Actually Shows You
Click the bug icon in the toolbar (or Run → Debug). CCS compiles your project, flashes the binary to the CC2340R5 over the board’s built-in XDS110 debug probe (which uses SWD, Serial Wire Debug, a two-wire alternative to JTAG), and halts execution at main(). The IDE switches to the Debug Perspective, a workspace layout purpose-built for debugging.
Here’s what you’re looking at:
+-----------------------------------------------+
| Debug View | Source Editor |
| (call stack) | (your .c file, halted line) |
|----------------| |
| Variables | |
| Expressions |--------------------------------|
|----------------| Memory Browser / Console |
| Registers | |
+-----------------------------------------------+The panels that matter right now:
- Debug View shows your call stack and thread. This is where you see where execution stopped.
- Source Editor displays your C file, with a highlighted line showing the current program counter position.
- Variables lists local and global variables with live values, updated each time execution halts.
- Expressions holds custom watch expressions you define (any valid C expression).
- Memory Browser shows raw memory contents at any address you choose.
- Registers displays CPU and peripheral register values.
If a panel disappears, open it from View → [panel name]. If everything looks wrong, reset with Window → Perspective → Reset Perspective.
Using CCS Theia? The layout is similar, but you access debug panels from the Debug sidebar icon on the left. TI’s Theia migration guide covers the differences.
The Code We’ll Debug
Here’s our example, a modified blinky with an intentional bug. Import the SDK’s blinky example and replace the main thread logic with this:
#include <ti/drivers/GPIO.h>
#include "ti_drivers_config.h"
volatile int blinkCount = 0;
volatile int maxBlinks = 10;
void *mainThread(void *arg0)
{
GPIO_init();
while (1)
{
blinkCount++;
if (blinkCount >= maxBlinks)
{
blinkCount = 0;
// BUG: developer intended to change blink speed here
// but forgot to add the logic — counter just resets
}
GPIO_toggle(CONFIG_GPIO_LED_0);
sleep(1);
}
}The LED blinks, the counter increments, but the “change speed after 10 blinks” behavior the developer intended never happens. We’ll use the debugger to trace exactly why.
Breakpoints: More Than Just “Pause Here”
Setting a Basic Line Breakpoint
Double-click the left margin (the gutter) next to blinkCount++. A blue circle appears. That’s your breakpoint. Press F8 (or click Resume) to run. Execution halts the moment that line is reached.
Look at the Debug View: you’ll see the call stack showing mainThread(). The source editor highlights the halted line. You’re frozen in time, mid-execution, with full visibility into program state.
Conditional Breakpoints Save You from Clicking Resume 50 Times
Right-click your breakpoint → Breakpoint Properties. Set these fields:
| Field | Value |
|---|---|
| Condition | blinkCount == 9 |
| Hit Count | (leave blank) |
| Hardware BP | ✓ (default) |
Click OK and resume. Instead of halting on every single iteration, execution stops only when blinkCount equals 9, the last iteration before the reset logic fires. You jumped straight to the interesting moment without babysitting the debugger.
You can use any valid C expression as a condition: blinkCount % 5 == 0, blinkCount > maxBlinks - 2, even pointer comparisons.
The 4-Breakpoint Limit You Need to Know About
The CC2340R5 uses an ARM Cortex-M0+ core, which has a hardware unit called the FPB (Flash Patch and Breakpoint) that supports exactly 4 hardware breakpoints. CCS uses hardware breakpoints by default because software breakpoints require rewriting flash, which is slow and sometimes impossible on flash-based targets.
If you set a fifth breakpoint, CCS warns you. This isn’t a CCS limitation; it’s a silicon constraint.
Practical rule: keep active breakpoints at 4 or fewer. Disable the ones you’re not currently using.
Managing Breakpoints Without Losing Track
Open the Breakpoints view (it’s a tab near Variables/Expressions). From here you can:
- Enable/disable individual breakpoints with checkboxes
- Skip All Breakpoints to run freely without deleting them
- Remove All for a clean slate
Breakpoints persist between debug sessions. If you’re confused about why execution keeps stopping in a function you forgot about, check this view first.
Stepping Through Code: The Muscle Memory You Need
With execution halted at a breakpoint, you have four movement options:
Step Over (F6) executes the current line and halts on the next one. This is your workhorse. Use it to move line by line through your logic without diving into function implementations.
Step Into (F5) enters the function being called on the current line. Try it on GPIO_toggle() and you’ll land inside the SimpleLink SDK driver code. Useful when you suspect a function isn’t doing what you expect.
Step Out (F7) finishes executing the current function and halts when it returns to the caller. Your escape hatch after an accidental Step Into.
Run to Line is accessed by right-clicking any line → Run to Line. Execution continues and halts there. Think of it as a breakpoint you don’t have to clean up.
Try this workflow: Set a breakpoint on the if (blinkCount >= maxBlinks) line. Resume. When it halts, press F6 twice. Did execution enter the if block or skip it? Check the highlighted line. Then look at blinkCount in the Variables view. The answer is right there.
Watching Variables and Expressions in Real Time
The Variables View: Instant Visibility
When execution is halted, the Variables view automatically shows local variables and globals in scope. You’ll see blinkCount and maxBlinks with their current integer values, updating every time you halt or step.
A critical note: if you see <optimized out> instead of a value, the compiler optimized the variable into a register or eliminated it entirely. This is why our example uses volatile. It forces the compiler to keep the variable in memory. During development, also set your optimization level to -O0 (none) in Project Properties → Build → Compiler → Optimization to maximize debugger visibility.
Watch Expressions: Your Custom Dashboard
The Expressions view is more powerful. Click Add Expression and enter any valid C expression:
blinkCountfor simple variable trackingblinkCount % 2for a computed expression, evaluated when halted&blinkCountto show the memory address of the variable*((volatile uint32_t*)0x400A1000)to read a raw memory address, cast as a 32-bit integer
That last one bridges directly into memory inspection. Any address you can look up in the CC2340R5 Technical Reference Manual (GPIO data output, timer counters, peripheral status registers) you can read right here as a watch expression.
Reading Raw Memory with the Memory Browser
Open View → Memory Browser. This tool shows you raw bytes at any memory address. No variable names, no abstractions, just what’s actually stored in RAM or mapped to peripheral registers.
Step 1: Find the address of blinkCount. In the Expressions view, add &blinkCount. Let’s say it shows 0x20000004.
Step 2: Enter 0x20000004 in the Memory Browser address bar. You’ll see something like:
Address | +0 +1 +2 +3
0x2000_0004 | 05 00 00 00 ← blinkCount = 5 (little-endian)The CC2340R5 is little-endian, so 05 00 00 00 is the integer 5. If blinkCount were 256, you’d see 00 01 00 00.
Step 3: Inspect a peripheral register. The CC2340R5 datasheet gives GPIO register addresses. Enter the base address of the GPIO output enable register (DOE31_0) to verify that your LED pin is configured as an output. If the corresponding bit is set, the GPIO driver initialized correctly.
You can change the display format in the Memory Browser toolbar: hex, decimal, float, or even character rendering. Use whatever makes the data meaningful for what you’re investigating.
One thing to know: memory values are a snapshot from the moment execution halted. They don’t live-update while you stare at them. CCS does have a Real-Time mode for that, but that’s a topic for another article.
Finding the Bug: A Complete Walkthrough
Let’s put it all together to diagnose why our LED never changes speed.
- Set a conditional breakpoint on the
ifline with conditionblinkCount >= maxBlinks. - Press F8 to resume. Execution runs through 10 blink iterations and halts when the condition is true.
- Check the Variables view:
blinkCountshows10,maxBlinksshows10. The condition evaluated correctly. - Press F6 to step over. You’re inside the
ifblock.blinkCountgets set to0. - Press F6 again. You step past the comment and straight to the closing brace. There’s no speed-change code. The developer wrote the counter reset but never implemented the actual feature.
- Open the Memory Browser at
&blinkCountto double-check:0A 00 00 00(that’s 10 in hex) before the reset,00 00 00 00after stepping past the assignment.
Bug found. No printf needed. No code changes. No reflashing. Total time: about 30 seconds once you know where to look.
Pitfalls That Will Waste Your Time (And How to Avoid Them)
<optimized out>variables → Set optimization to-O0in project build settings. Usevolatilefor variables modified in interrupts or watched during debug.- “No source available” when stepping → You’ve entered a precompiled SDK library. Press F7 (Step Out) to return to your code.
- Breakpoint won’t set on a line → The compiler optimized that line away, or it’s a non-executable statement like a variable declaration with no initializer. Try the next line.
- “Exceeded hardware breakpoint limit” → Disable or remove unused breakpoints. You only get 4 on the CC2340R5.
- LaunchPad not connecting → Verify XDS110 drivers are installed. Try Help → Check for Updates in CCS. Unplug and replug the USB cable.
Build This Into Your Daily Development Workflow
Breakpoints and memory inspection cover roughly 80% of the debugging you’ll do on embedded targets. The investment is learning the keyboard shortcuts (F5, F6, F7, F8) until they’re reflexive, and knowing that the Memory Browser exists for the moments when the Variables view isn’t enough.
Once these tools feel natural, you’re ready for the next tier: data watchpoints that halt on memory writes, RTOS-aware debugging with SysConfig and TI-RTOS thread visibility, and BLE stack debugging where timing and state machines make printf truly useless. Those build on everything you’ve practiced here.
For more on the CC23xx platform, head to our TI CC23xx Development pillar page.
Hubble Network enables Bluetooth connectivity from any CC2340 device directly to satellites—no gateways, no infrastructure. See how it works →