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 togdb.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-jsonreturns 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.