Debugging TI C2000 with the C2Prog GDB Server


A JTAG debug server and Lua scripting client for scripted tests, CI, and AI assistants

C2Prog includes a GDB server, c2p-gdb-server, and a scripting client, c2p-gdb. The server attaches to a TI C2000™ target through a JTAG debug probe and serves the standard GDB Remote Serial Protocol on a TCP port. The client runs Lua scripts against it. Together they let you inspect and control a C28x or C29x MCU from a shell, a build pipeline, or an AI assistant, without launching an IDE.

Starting the server

c2p-gdb-server --ccs-base-path=<ccs_base> --non-intrusive 28P650DK9,DK8,DK7,DK6,SK7,SK6_JTAG xds110

--ccs-base-path points the server at the TI EmuPack, which supplies the probe drivers. This is either the EmuPack that the C2Prog installer installs, or the ccs_base directory of a Code Composer Studio or UniFlash installation. C2Prog uses the same path for JTAG programming, so you can call c2p-cli get ccs-base-path to print the one your installation is already configured with. An AI assistant with the C2Prog agentic interface registered can also report the configured path and set it on request.

The last two arguments in the example above correspond to the target configuration and the debug probe.

If the server does not recognize a target name, it prints every configuration it accepts, to help you find the one for your device. When several probes are connected, you must add the serial number to distinguish them: xds110:CL850001. On a multi-core F29x device, --core=3 selects CPU3. Once the server is listening, it writes a single JSON ready record to standard output with the port it bound to.

With --non-intrusive, the server attaches without halting the target, so the embedded application keeps running. Without it, the server halts the target when it attaches.

The target keeps its state for as long as the server stays attached, so one client can disconnect and the next one finds the target where it was left. When the server exits, the probe releases the target, so a halted CPU runs again. A one-shot call therefore cannot leave a target halted.

Scripting with c2p-gdb

Scripts are plain Lua with a gdb table for target access. The following reads the latched reset causes of an F29x device without stopping it:

local resc = gdb.read_memory(0x30200040, 1, 4)[1]
print('RESC 0x%08X' % resc)

This one runs to an interrupt service routine and reports the program counter and a global variable. The addresses come from the .map file:

local isr    = 0x00082A10   -- adcA1ISR
local sample = 0x0000C010   -- a global variable
if gdb.run_to(isr, 1000) then
  print('PC     0x%08X' % gdb.read_register('PC'))
  print('sample %d' % gdb.read_memory(sample, 1, 2)[1])
end
gdb.remote_command('resume')
c2p-gdb script --port 3334 isr_check.lua

On C28x devices, addresses correspond to word addresses (as printed in the .map file), and width is the number of bytes per value, 2 or 4. On C29x devices, addresses are byte addresses.

The API covers memory and register access, registers by name (PC, ACC, XAR0 on C28x), halt, step, continue, reset, breakpoints, and the server’s monitor commands. The scripts folder of the installation holds ready-made recipes. For example, status_28x.lua reports the part identification registers and the lock state of each code security zone. watch_28x.lua samples a variable while the application runs. Each recipe states in its header whether it is read-only, and otherwise which halts, resets, and writes it performs.

Running the server for a single call

The c2p-gdb option --server-cmd starts the server for the duration of a single client call. The --single option in the example below makes the server exit once the session closes. Reading the part ID of an F28P65x, for example, is one line:

c2p-gdb script --json --server-cmd="c2p-gdb-server --ccs-base-path=<ccs_base> --single --non-intrusive 28P650DK9,DK8,DK7,DK6,SK7,SK6_JTAG xds110" -e "gdb.emit(gdb.read_memory(0x5D00A, 1, 4))"

The client waits for the server’s ready record, and the call begins as soon as the server is listening. One attach of this kind takes one to three seconds, depending on the probe and the device. A build step or an AI assistant can therefore run one command to obtain one result, with no server to start beforehand or stop afterward.

Breakpoints

On F28x devices, a breakpoint in RAM is set by patching the trap instruction ESTOP0 over the instruction at that address. For a breakpoint in flash, the server places one of the CPU’s address comparators instead.

Two mechanisms supply those comparators. The first two comparators come from the C28x CPU’s own analysis unit. On an F28x device with an ERAD, the Embedded Real-time Analysis and Diagnostic module, the server places every further breakpoint in a free ERAD comparator, which raises the breakpoint budget from two to ten. The F28002x, F28003x, F28004x, F2838x, F28P55x, and F28P65x carry one, for example. These are the same comparators that watchpoints draw on.

On F29x devices, every breakpoint uses one of the CPU’s eight ERAD comparators, in RAM as in flash, so application memory is never modified. The TI debug driver implements no hardware breakpoints on these devices.

ERAD is also an application-visible peripheral, so the application may hold comparators of its own. The server leaves those alone. A breakpoint beyond the last free comparator fails with an error, and monitor status-json reports the comparators in use and the ones still free.

A memory read at a patched address returns the original instruction, so a comparison against the programmed image still matches. Every patch and every comparator is released when the breakpoint or watchpoint is cleared, when the last client disconnects, and when the server shuts down, so nothing is left in the application. Breakpoints therefore do not survive a one-shot call.

Watchpoints

A watchpoint halts the target when it accesses a range of memory. A write watchpoint halts on a store, a read watchpoint on a load, and an access watchpoint on either. All three kinds work on F29x devices and on the F28x families that carry an ERAD. F28x devices without one report watchpoints as unsupported.

The following example finds out which code writes a variable. It halts the target, watches the four bytes of the variable for a store, and then reports the instruction that wrote them. The address is obtained from the .map file, as before:

local var = 0x200E0000   -- a global variable
gdb.remote_command('halt')
gdb.set_watchpoint(var, 4, 'write')
if gdb.cont(5000) then
  print('written by 0x%08X' % gdb.status().watch_pc)
end
gdb.clear_watchpoint(var, 4, 'write')

On F29x devices, watch_pc is the address of the accessing instruction. On F28x devices the ERAD records no such address, so the halted PC is near the access rather than at it.

Watchpoints consume ERAD comparators: one for a write, one for a read, two for an access. On F29x devices these are the same eight the breakpoints use. On F28x devices they are shared with any hardware breakpoint beyond the first two.

A watched range must be a power of two in length and aligned to that length. The example above watches four bytes at 0x200E0000, which satisfies both. The same four bytes at 0x200E0002 would be refused with an error, because that address is not a multiple of four.

Opening the debug port of an F29x device

The F29x debug logic has one access port per CPU and two secure access ports (SEC-AP): one for the C29x CPUs and one for the HSM, the security processor of the device.

The security configuration active on the device can keep the CPU access ports closed. In SSU Mode 3, the mode in which the device enforces its security settings, four debug gates control JTAG access to the CPUs. A gate in password mode opens when a debugger presents that gate’s password over the C29 SEC-AP. While the C29 gate is closed, an attach to a CPU port fails with Connect:95.

The server attaches to the C29 SEC-AP directly, which is access port 1. That port is chosen at startup, because no monitor command can run on a port whose attach has failed. Name your own probe in place of xds110. The probe argument carries JSON, so the shell has to pass the double quotes through. A Unix shell does that with single quotes around the argument:

c2p-gdb-server --ccs-base-path=<ccs_base> 29H85xTU9,TU8-CPU1_JTAG 'xds110:{"ap":1}'

In a Windows command prompt, escape the inner quotes with a backslash instead:

c2p-gdb-server --ccs-base-path=<ccs_base> 29H85xTU9,TU8-CPU1_JTAG "xds110:{\"ap\":1}"

Then present the password and switch to CPU1:

gdb.remote_command('set debug-password 00112233445566778899AABBCCDDEEFF')
print(gdb.remote_command('get debugstatus'))
gdb.remote_command('set core 1')

The password is the 32 hex digits of the profile’s debug.c29.password. get debugstatus presents it and returns the device’s DEBUG_ENABLE_STATUS register, whose bit 0 is set when the C29 gate is open. The server then stays attached to CPU1 for the next client.

Machine-readable output

Every part of the toolchain reports to its caller in a form a program can consume:

  • With --json, the client writes exactly one result document on standard output: either the value the script passed to gdb.emit, or an error message with a traceback for a script error.
  • Exit statuses name the failure class: 5 for a connection failure, 6 for a rejected target access, 7 for a script error.
  • monitor status-json returns the target state: running or halted, the patched breakpoints the server holds, and the comparators that are used and free.
  • The server accepts several connections. One of them can wait on a blocking continue while another reads memory from the running MCU.

A CI job can therefore test a change on real hardware: it flashes the firmware with c2p-cli, runs a script against the target, and distinguishes success from failure.

Messages such as Connect:95 come from the debug probe or the target and pass through both tools unchanged. The manual lists them with their cause and the next step to take.

Working with an AI assistant

With the C2Prog agentic interface registered, an AI coding assistant can read status, memory, and registers without halting the target. It makes each query with one self-contained call and gets a JSON document back. A failure carries an exit status that says whether the probe, the target, or the script is at fault. The assistant can also hold a session open across several calls, which keeps the target halted where it stopped.

One example from real use is the bring-up of a Modbus RTU driver on an F28P65x, where the driver was not answering on the bus. The assistant read the pin multiplexing registers one at a time and compared them against the pin map, then read GPBDAT twice: once with the bus idle, and once while the host adapter held a break condition.

idle    GPBDAT 0x12FFCFF9
break   GPBDAT 0x127FCFF9

Only bit 23 moved, which is GPIO55. The adapter was wired to the wrong pin. Both reads happened while the application kept running, with no scope and no UART console on the board.

The rest of a bring-up runs the same way: the assistant changes the code, builds and flashes it, then runs one script that goes to the point of interest and reads the peripheral registers back. The target runs again once that call ends, unless the assistant holds a session open across multiple operations.

Halting, resetting, stepping, and writing change the state of the target. Your agent host can tell these apart from reads and ask you before a state-changing operation runs. A shipped recipe states in its header which halts, resets, and writes it performs, so you can find out what it does before it runs.

Using C2Prog versus CCS for debugging

Standard GDB clients do not understand the C28x or C29x architecture. Therefore the C2Prog tools cannot support source-level debugging. You must work with the addresses printed in the .map file rather than with variable names and line numbers. CCS remains the tool for stepping through C source. The C2Prog tools fit the case where a script or coding assistant drives the hardware instead of a person.


The Debugging Tools chapter of the C2Prog manual describes the server options, the monitor commands, and the full script API. The GDB tools ship with C2Prog and do not require a paid license.