Browse Source

initial commit

pull/2/head
Fernando Trias 4 years ago
commit
08765b363b
  1. 1
      .gitignore
  2. 55
      README.md
  3. 714
      TeensyDebug.cpp
  4. 43
      TeensyDebug.h
  5. 53
      examples/breakpoint_test/breakpoint_test.ino
  6. 603
      gdbstubs.cpp
  7. 3
      keywords.txt
  8. 10
      library.properties
  9. 16
      license.txt

1
.gitignore

@ -0,0 +1 @@
.vscode

55
README.md

@ -0,0 +1,55 @@
GDB stubs on Teensy
===================
I gave this some thought over the weekend and I think live debugging is possible even if C_DEBUGEN cannot be changed. The idea is to use a GDB stub to remotely debug the Teensy and instead of using BKPT, using SVC. This proposal will enable limited breakpoints and live examination of data.
First, I'll explain GDB stubs a little. Then I'll discuss how to use it. I'm writing this here to see if this has already been done and to get feedback.
GDB stubs
---------
GDB stubs provides a simple interface between GDB and a remote target via a serial interface, such as a socket, pipe, UART, USB, etc. It works like this:
```
[PC running GDB] <--(serial)--> [Teensy GDB stub]
```
Teensyduino comes with a GDB executable that supports ARM. The ELF file contains debugging info if compiled with the "Debug" option that Teensyduino provides. Thus, GDB loads up the ELF and knows all the symbols and sees the source code. GDB queries Teensy for information like memory data and registers as needed. It also sends Teensy breaking and execution commands.
Process
---------
This is how it would work with Teensy:
1. By using interrupts or the EventResponder, the Teensy listens to GDB commands from a serial device.
2. When it gets commands like memory queries, memory sets and things that don't require halting, it responds with the data requested.
3. When it receives a halt command, Teensy will just go into a loop querying for commands and responding. It won't return to it's caller until GDB tells it to do so. Thus, execution of the main thread will stop but interrupts will continue. Because interrupts continue, on the plus side, the Teensy won't die and USB and other features will stay active. On the other hand, sometimes you just the want the system to halt. Perhaps there could be an option to halt all interrupts as well or change the priority. Keeping interrupts going is probably easier for beginners and models what desktop apps do (when an apps stops, the OS keeps going).
So far, this will enable us to do simple live debugging. You can't set breakpoints yet, but you'll be able to stop execution and examine the state.
4. Provide a special "breakpoint" instruction that you can insert into your code. Each breakpoint will have a flag in RAM to determine if it is enabled or not. If enabled, when execution reaches it, it will execute an interrupt (software or SVC). If disabled, execution just keeps going. Breakpoints are enabled/disabled based on commands received from GDB.
So far, this provides fixed hardcoded breakpoints. You can stop the code at any place and examine/change variables, stack, etc.
5. If a function is placed in RAM with the FASTRAM option, you can dynamically insert/remove SVC calls in the code, in the same way that standard debuggers work. Thus, if you want arbitrary breakpoints in certain code, just specify FASTRAM. Again, breakpoints like this can be set and enabled/disabled by GDB.
In theory, most code can be placed in RAM by changing the gcc compile options, or by linking with an alternative LD file. Perhaps Teensyduino can provide a feature to do this in the future? The Teensy 4 has plenty of RAM to store most programs. You can store some parts, like core code, in flash and all user code in RAM.
6. On the Teensy 3.1/3.2, we could as well use the Flash Patch Block to set and remove SVC calls using patching. Thus, you can dynamically set breakpoints in flash. Teensy 4 doesn't support this.
Other considerations
---------
The serial connection can be anything. To start it's probably best to use a UART but in theory it could be USB Serial, CAN, network, Raw connection, etc.
Right now, running GDB will have to be done manually. But in the future, GDB could be piped to Arduino's serial monitor. Both GDB's output and Teensy's serial output could be sent to the display. GDB can receive commands from the Send window. If, in addition to this, we use USB for the serial connection to GDB, then Teensy will have onboard live debugging available with no special setup or hardware required.
```
[Arduino] [ s1] <-- [Teensy]
[Serial ] <-> [gdb proxy ]
[Monitor] [ s2] <-> [GDB]
```
If C_DEBUGEN can be disabled, then the Teensy 4 can also have dynamic breakpoints in flash and everything else becomes somewhat simpler to implement.

714
TeensyDebug.cpp

@ -0,0 +1,714 @@
/**
* @brief TeensyDebug library. Used by gdb stub and also stand-alone.
*
*/
/** References
https://forum.pjrc.com/threads/26358-Software-Debugger-Stack
https://forum.pjrc.com/threads/28058-Teensyduino-1-22-Features?highlight=C_DEBUGEN
http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.ddi0344k/Cegjcacg.html
https://web.eecs.umich.edu/~prabal/teaching/resources/eecs373/ARMv7-M_ARM.pdf
https://os.mbed.com/users/va009039/code/lpcterm2/annotate/5e4107edcbdb/Target2.cpp/
https://sourceforge.net/p/openocd/code/ci/master/tree/src/target/cortex_m.c#l303
https://docs.google.com/viewer?a=v&pid=forums&srcid=MTQ3NDIxMjYyNTI1NDYxNDY0MzcBMTQyMjM2NDMzNTU1NzM4ODY4NjEBQzAtUlFoeFFJMGNKATAuMQEBdjI&authuser=0
https://developer.apple.com/library/archive/documentation/DeveloperTools/gdb/gdb/gdb_33.html
https://elixir.bootlin.com/linux/v3.3/source/arch/mn10300/kernel/gdb-stub.c
https://ftp.gnu.org/old-gnu/Manuals/gdb/html_node/gdb_129.html#SEC134
https://github.com/turboscrew/rpi_stub
https://www.embecosm.com/appnotes/ean4/embecosm-howto-rsp-server-ean4-issue-2.html
https://github.com/redox-os/binutils-gdb/blob/master/gdb/arm-tdep.c
https://github.com/redox-os/binutils-gdb/blob/master/gdb/stubs/i386-stub.c
*/
#include <Arduino.h>
/**
* @brief Memory maps missing from headers
*
*/
#define FP_CTRL (*(uint32_t*)0xE0002000)
#define FP_REMAP (*(uint32_t*)0xE0002004)
#define FP_COMP(n) (((uint32_t*)0xE0002008)[n])
#define FP_COMP0 (*(uint32_t*)0xE0002008)
#define FP_COMP1 (*(uint32_t*)0xE000200C)
#define FP_COMP2 (*(uint32_t*)0xE0002010)
#define FP_COMP3 (*(uint32_t*)0xE0002014)
#define FP_COMP4 (*(uint32_t*)0xE0002018)
#define FP_COMP5 (*(uint32_t*)0xE000201C)
#define FP_COMP6 (*(uint32_t*)0xE0002020)
#define FP_COMP7 (*(uint32_t*)0xE0002024)
#define FP_COMP_MASK 0x1FFFFFFC
#define FP_REMAP_MASK 0x1FFFFFF0
#define FP_REMAP_RMPSPT (1<<29)
#define ARM_DHCSR (*(uint32_t*)0xE000EDF0)
#define ARM_DCRSR (*(uint32_t*)0xE000EDF4)
#define ARM_DCRDR (*(uint32_t*)0xE000EDF8)
//#define ARM_DEMCR (*(uint32_t*)0xE000EDFC) // defined in header
#define FP_LAR_UNLOCK_KEY 0xC5ACCE55
#define FP_LAR (*(unsigned int*) 0xE0000FB0)
#define FP_LSR (*(unsigned int*) 0xE0000FB4)
#ifdef __MK20DX256__
#define RAM_START ((void*)0x1FFF8000)
#define RAM_END ((void*)0x2FFFFFFF)
#endif
#ifdef __IMXRT1062__
#define RAM_START ((void*)0x00000000)
#define RAM_END ((void*)0x5FFFFFFF)
#endif
/*
* Breakpoint setup
*/
void *breakpoints[32];
uint16_t *remap_table;
const int sw_breakpoint_count = 32;
void *sw_breakpoint_addr[sw_breakpoint_count];
uint16_t sw_breakpoint_code[sw_breakpoint_count];
int swdebug_clearBreakpoint(void *p) {
uint32_t addr = ((uint32_t)p) & 0x1FFFFFFE;
for(int i=0; i<sw_breakpoint_count; i++) {
if (sw_breakpoint_addr[i] == (void*)addr) {
sw_breakpoint_addr[i] = 0;
uint16_t *memory = (uint16_t*)addr;
*memory = sw_breakpoint_code[i];
Serial.print("restore ");Serial.print(addr, HEX);Serial.print("=");Serial.println(*memory, HEX);
return 0;
}
}
return -1;
}
int swdebug_setBreakpoint(void *p) {
uint32_t addr = ((uint32_t)p) & 0x1FFFFFFE;
for(int i=0; i<sw_breakpoint_count; i++) {
if (sw_breakpoint_addr[i] == 0) {
sw_breakpoint_addr[i] = (void*)addr;
uint16_t *memory = (uint16_t*)addr;
sw_breakpoint_code[i] = *memory;
Serial.print("overwrite ");Serial.print(addr, HEX);Serial.print(" from ");Serial.println(*memory, HEX);
*memory = 0xdf10; // SVC 10
return 0;
}
}
return -1;
}
int swdebug_isBreakpoint(void *p) {
uint32_t addr = ((uint32_t)p) & 0x1FFFFFFE;
for(int i=0; i<sw_breakpoint_count; i++) {
if (sw_breakpoint_addr[i] == (void*)addr) {
return 1;
}
}
return 0;
}
#ifdef HAS_FP_MAP
int hwdebug_clearBreakpoint(void *p, int n) {
FP_COMP(n) = 0;
return 0;
}
int hwdebug_setBreakpoint(void *p, int n) {
if (p == 0) {
FP_COMP(n) = 0;
// Serial.print("break0 ");Serial.println(n);
}
else {
uint32_t pc = ((uint32_t)p) & 0x1FFFFFFE;
if (pc & 0b10) { // must be aligned, so go to next instruction
// store the first instruction
pc -= 2;
remap_table[(n<<1) + 0] = ((uint16_t*)pc)[0];
remap_table[(n<<1) + 1] = 0xdf10; // svc 10 instruction
}
else {
// store the next instruction
remap_table[(n<<1) + 0] = 0xdf10; // svc 10 instruction
remap_table[(n<<1) + 1] = ((uint16_t*)pc)[1];
}
uint32_t addr = pc & 0x1FFFFFFC;
FP_COMP(n) = addr | 1;
breakpoints[n] = p;
// Serial.print("break ");Serial.print(n);Serial.print(" at ");Serial.print(pc, HEX);Serial.print("=");Serial.println(addr, HEX);
}
return 0;
}
void hwdebug_disableBreakpoint(int n) {
FP_COMP(n) &= 0xFFFFFFFE;
Serial.print("break ");Serial.print(n);Serial.println(" disable");
}
void hwdebug_enableBreakpoint(int n) {
FP_COMP(n) |= 1;
Serial.print("break ");Serial.print(n);Serial.println(" enable");
}
int hwdebug_getBreakpoint(void *p) {
for (int n=1; n<6; n++) {
if (breakpoints[n]== p) {
return n;
}
}
return -1;
}
int hwdebug_isBreakpoint(void *p) {
for (int n=1; n<6; n++) {
if (breakpoints[n]== p) {
return 1;
}
}
return 0;
}
#endif
const int hc_breakpoint_count = 32;
void *hc_breakpoint_addr[hc_breakpoint_count];
int hc_breakpoint_enabled[hc_breakpoint_count];
int hc_breakpoint_trip = -1;
int debug_isHardcoded(void *addr) {
// if (addr >= RAM_START && addr <= RAM_END) {
// return 0;
// }
uint16_t *p = (uint16_t*)addr;
// SVC 0x11 is reserved for hardcoded breaks
if (p[0] == 0xdf11) {
return 1;
}
return 0;
}
int hcdebug_clearBreakpoint(int n) {
hc_breakpoint_enabled[n] = 0;
return 0;
}
int hcdebug_setBreakpoint(int n) {
hc_breakpoint_enabled[n] = 1;
return 0;
}
int hcdebug_isEnabled(int n) {
return hc_breakpoint_enabled[n];
}
int hcdebug_isBreakpoint(int n) {
return hc_breakpoint_enabled[n];
}
void hcdebug_tripBreakpoint(int n) {
hc_breakpoint_trip = n;
}
void debug_initBreakpoints() {
for(int i=0; i<sw_breakpoint_count; i++) {
sw_breakpoint_addr[i] = 0;
}
for(int i=0; i<hc_breakpoint_count; i++) {
hc_breakpoint_enabled[i] = 0;
}
#ifdef HAS_FP_MAP
hwdebug_clearBreakpoint(0, 0);
hwdebug_clearBreakpoint(0, 1);
hwdebug_clearBreakpoint(0, 2);
hwdebug_clearBreakpoint(0, 3);
hwdebug_clearBreakpoint(0, 4);
hwdebug_clearBreakpoint(0, 5);
#endif
}
int debug_clearBreakpoint(void *p, int n) {
if (p >= RAM_START && p <= RAM_END) {
return swdebug_clearBreakpoint(p);
}
else if (p < (void*)0xF) {
return hcdebug_clearBreakpoint((int)p);
}
else {
#ifdef HAS_FP_MAP
return hwdebug_clearBreakpoint(p, n);
#else
return -1;
#endif
}
}
int debug_setBreakpoint(void *p, int n) {
if (p >= RAM_START && p <= RAM_END) {
return swdebug_setBreakpoint(p);
}
else if (p < (void*)0xF) {
return hcdebug_setBreakpoint((int)p);
}
else {
#ifdef HAS_FP_MAP
return hwdebug_setBreakpoint(p, n);
#else
return -1;
#endif
}
}
int debug_isBreakpoint(void *p) {
if (p >= RAM_START && p <= RAM_END) {
return swdebug_isBreakpoint(p);
}
else if (p < (void*)0xF) {
return hcdebug_isBreakpoint((int)p);
}
else {
#ifdef HAS_FP_MAP
return hwdebug_isBreakpoint(p);
#else
return -1;
#endif
}
}
/*
* Breakpint handlers
*/
void (*callback)() = NULL;
// If debugactive=0, this means the actual breakpoint. If debugactive=1, this is the followon breakpoint.
int debugactive = 0;
int debugreset = 0;
int debugcount = 0;
int debugenabled = 0;
int debugstep = 0;
// During the initial breakpoint, the next address to break on
uint32_t nextpc = 0;
const char *hard_fault_debug_text[] = {
"debug", "nmi", "hard", "mem", "bus", "usage"
};
uint32_t debug_id = 0;
struct save_registers_struct {
uint32_t r0;
uint32_t r1;
uint32_t r2;
uint32_t r3;
uint32_t r12;
uint32_t lr;
uint32_t pc;
uint32_t xPSR;
uint32_t r4;
uint32_t r5;
uint32_t r6;
uint32_t r7;
uint32_t r8;
uint32_t r9;
uint32_t r10;
uint32_t r11;
uint32_t sp;
} save_registers;
// Structure of ISR stack
struct stack_isr {
uint32_t r0;
uint32_t r1;
uint32_t r2;
uint32_t r3;
uint32_t r12;
uint32_t lr;
uint32_t pc;
uint32_t xPSR;
};
struct stack_isr *stack;
void print_registers() {
Serial.print("r0=");Serial.println(save_registers.r0);
Serial.print("r1=");Serial.println(save_registers.r1);
Serial.print("r2=");Serial.println(save_registers.r2);
Serial.print("r3=");Serial.println(save_registers.r3);
Serial.print("r12=");Serial.println(save_registers.r12);
Serial.print("lr=0x");Serial.println(save_registers.lr, HEX);
Serial.print("pc=0x");Serial.println(save_registers.pc, HEX);
Serial.print("r4=");Serial.println(save_registers.r4);
Serial.print("r5=");Serial.println(save_registers.r5);
Serial.print("r6=");Serial.println(save_registers.r6);
Serial.print("r7=");Serial.println(save_registers.r7);
Serial.print("r8=");Serial.println(save_registers.r8);
Serial.print("r9=");Serial.println(save_registers.r9);
Serial.print("r10=");Serial.println(save_registers.r10);
Serial.print("r11=");Serial.println(save_registers.r11);
Serial.print("sp=0x");Serial.println(save_registers.sp,HEX);
}
void debug_action() {
Serial.println("****DEBUG");
print_registers();
Serial.println("****");
}
void debug_monitor() {
uint32_t breakaddr = save_registers.pc - 2;
// is this the first breakpoint or are we in a sequence?
if (debugactive == 0) {
// Adjust original SP to before the interrupt call
save_registers.sp += 20;
// Serial.print("break at ");Serial.println(breakaddr, HEX);
if (callback) {
callback();
}
else {
debug_action();
}
if (debug_isHardcoded((void*)breakaddr)) {
// Serial.print("hard coded at ");Serial.println(breakaddr, HEX);
// do nothing because we continue on to next instruction
}
else if (debug_isBreakpoint((void*)breakaddr)) {
// our location is a breakpoint so we need to return it to the original and rerun
// the instruction; to prevent a problem, set breakpoint to next instruction
// and at next break, set it back
debug_clearBreakpoint((void*)breakaddr, 1);
// break at next instruction
debug_setBreakpoint((void*)(save_registers.pc), 0);
// set to rerun current instruction
stack->pc = breakaddr;
// we need to process the next breakpoint differently
debugactive = 1;
// the original breakpoint needs to be put back after next break
debugreset = breakaddr;
}
}
else {
// clear the temporary breakpoint
debug_clearBreakpoint((void*)breakaddr, 0);
// reset to re-run the instruction
stack->pc = breakaddr;
// if we need to reset the original, do so
if (debugreset) {
debug_setBreakpoint((void*)debugreset, 1);
debugreset = 0;
}
// are we stepping instruction by instruction?
if (debugstep) {
// we're stepping so process any commands and break at next
// and stay in this mode
if (callback) {
callback();
}
else {
debug_action();
}
debug_setBreakpoint((void*)save_registers.pc, 0);
}
else {
// we're not stepping so reset mode
debugactive = 0;
}
}
}
#define SAVE_REGISTERS \
"ldr r0, =stack \n" \
"str sp, [r0] \n" \
"ldr r0, =save_registers \n" \
"ldr r2, [sp, #0] \n" \
"str r2, [r0, #0] \n" \
"ldr r2, [sp, #4] \n" \
"str r2, [r0, #4] \n" \
"ldr r2, [sp, #8] \n" \
"str r2, [r0, #8] \n" \
"ldr r2, [sp, #12] \n" \
"str r2, [r0, #12] \n" \
"ldr r2, [sp, #16] \n" \
"str r2, [r0, #16] \n" \
\
"ldr r2, [sp, #20] \n" \
"str r2, [r0, #20] \n" \
"ldr r2, [sp, #24] \n" \
"str r2, [r0, #24] \n" \
"ldr r2, [sp, #28] \n" \
"str r2, [r0, #28] \n" \
\
"str r4, [r0, #32] \n" \
"str r5, [r0, #36] \n" \
"str r6, [r0, #40] \n" \
"str r7, [r0, #44] \n" \
"str r8, [r0, #48] \n" \
"str r9, [r0, #52] \n" \
"str r10, [r0, #56] \n" \
"str r11, [r0, #60] \n" \
"str sp, [r0, #64] \n"
void (*original_software_isr)() = NULL;
__attribute__((noinline, naked))
void debug_call_isr() {
// Are we in debug mode? If not, just jump to original ISR
if (debugenabled == 0) {
if (original_software_isr) {
asm volatile("mov pc,%0" : : "r" (original_software_isr));
}
return;
}
asm volatile(
"ldr r0, =stack \n"
"str sp, [r0] \n"
"push {lr} \n");
debug_monitor(); // process the debug event
asm volatile("pop {pc}");
}
void debug_call_isr_setup() {
debugcount++;
debugenabled = 1;
// process in lower priority so services can keep running
NVIC_SET_PENDING(IRQ_SOFTWARE);
}
__attribute__((noinline, naked))
void svcall_isr() {
asm volatile(SAVE_REGISTERS);
asm volatile("push {lr}");
debug_call_isr_setup();
asm volatile("pop {pc}");
}
#pragma GCC push_options
#pragma GCC optimize ("O0")
__attribute__((naked))
void svc_call_table() {
asm volatile(
"svc #0x10 \n"
"nop \n"
"svc #0x10 \n"
"nop \n"
"svc #0x10 \n"
"nop \n"
"svc #0x10 \n"
"nop \n"
"svc #0x10 \n"
"nop \n"
"svc #0x10 \n"
"nop \n"
"svc #0x10 \n"
"nop \n"
);
}
#pragma GCC pop_options
uint32_t debug_getRegister(const char *reg) {
if (reg[0] == 'r') {
if (reg[2] == 0) { // r0-r9
switch(reg[1]) {
case '0': return save_registers.r0;
case '1': return save_registers.r1;
case '2': return save_registers.r2;
case '3': return save_registers.r3;
case '4': return save_registers.r4;
case '5': return save_registers.r5;
case '6': return save_registers.r6;
case '7': return save_registers.r7;
case '8': return save_registers.r8;
case '9': return save_registers.r9;
}
}
else if (reg[1] == '1') { // r10-r12
switch(reg[1]) {
case '0': return save_registers.r10;
case '1': return save_registers.r11;
case '2': return save_registers.r12;
}
}
}
else if (strcmp(reg, "lr")==0) return save_registers.lr;
else if (strcmp(reg, "pc")==0) return save_registers.pc;
else if (strcmp(reg, "sp")==0) return save_registers.sp;
else if (strcmp(reg, "cpsr")==0) return save_registers.xPSR;
return -1;
}
/**
* Fault debug messages
*/
void flash_blink(int n) {
volatile int p = 0;
pinMode(13, OUTPUT);
while(1) {
for(int c=0; c<n; c++) {
for(int i=0; i<20000000; i++) {p++;}
digitalWrite(13, HIGH);
for(int i=0; i<20000000; i++) {p++;}
digitalWrite(13, LOW);
}
for(int i=0; i<100000000; i++) {p++;}
}
}
int debug_crash = 0;
void hard_fault_debug(int n) {
// if (debug_crash) flash_blink(n);
Serial1.print("****FAULT ");
Serial1.println(hard_fault_debug_text[n]);
Serial1.print("r0=");Serial1.println(stack->r0, HEX);
Serial1.print("r1=");Serial1.println(stack->r1, HEX);
Serial1.print("r2=");Serial1.println(stack->r2, HEX);
Serial1.print("r3=");Serial1.println(stack->r3, HEX);
Serial1.print("r12=");Serial1.println(stack->r12, HEX);
Serial1.print("lr=0x");Serial1.println(stack->lr, HEX);
Serial1.print("pc=0x");Serial1.println(stack->pc, HEX);
stack->pc += 2;
debug_crash = 1;
}
// uint32_t hard_fault_debug_addr = (uint32_t)hard_fault_debug;
#define xfault_isr_stack(fault) \
asm volatile(SAVE_REGISTERS); \
debug_id = fault; \
NVIC_SET_PENDING(IRQ_SOFTWARE); \
asm volatile("bx lr")
#define fault_isr_stack(fault) \
asm volatile("ldr r0, =stack \n str sp, [r0]"); \
asm volatile("push {lr}"); \
hard_fault_debug(fault); \
asm volatile("pop {pc}")
__attribute__((noinline, naked)) void nmi_isr(void) { fault_isr_stack(1); }
__attribute__((noinline, naked)) void hard_fault_isr(void) { fault_isr_stack(2); }
__attribute__((noinline, naked)) void memmanage_fault_isr(void) { fault_isr_stack(3); }
__attribute__((noinline, naked)) void bus_fault_isr(void) { fault_isr_stack(4); }
__attribute__((noinline, naked)) void usage_fault_isr(void) { fault_isr_stack(5); }
/**
* Initialization code
*
*/
void dumpmem(void *mem, int sz) {
Serial.print((uint32_t)mem, HEX);
Serial.print("=");
for(int i=0; i<sz; i++) {
Serial.print(((uint8_t*)mem)[i], HEX);
Serial.print(":");
}
Serial.println();
}
// store the address of the stack pointer where we pre-allocate space since the remap
// table must be in ram above 0x20000000 and this ram is in the stack area.
uint32_t save_stack;
void debug_init() {
#ifdef HAS_FP_MAP
// find next aligned block in the stack
uint32_t xtable = (save_stack + 0x100) & 0xFFFFFFC0;
// copy table
uint32_t *sourcemem = (uint32_t*)(((uint32_t)svc_call_table & 0xFFFFFFFE));
uint32_t *destmem = (uint32_t*)xtable;
for(int i=0; i<6; i++) {
destmem[i] = sourcemem[i];
}
// enable the remap, but don't assign any yet
FP_LAR = FP_LAR_UNLOCK_KEY; // doesn't do anything, but might in some other processors
FP_REMAP = xtable;
remap_table = (uint16_t *)xtable;
FP_CTRL = 0b11;
// delay(3000);
// Serial.println(FP_CTRL, HEX);
// dumpmem(sourcemem, 32);
// dumpmem(destmem, 32);
// dumpmem(xtable, 32);
#endif
_VectorsRam[2] = nmi_isr;
_VectorsRam[3] = hard_fault_isr;
_VectorsRam[4] = memmanage_fault_isr;
_VectorsRam[5] = bus_fault_isr;
_VectorsRam[6] = usage_fault_isr;
_VectorsRam[11] = svcall_isr;
// chaing the software ISR handler
original_software_isr = _VectorsRam[IRQ_SOFTWARE + 16];
_VectorsRam[IRQ_SOFTWARE + 16] = debug_call_isr;
NVIC_SET_PRIORITY(IRQ_SOFTWARE, 208); // 255 = lowest priority
NVIC_ENABLE_IRQ(IRQ_SOFTWARE);
debug_initBreakpoints();
}
// We will rename the original setup() to this by using a #define
void setup_main();
void gdb_init();
#ifdef HAS_FP_MAP
/*
* The setup function must allocate space on the stack for the remap table; this space must
* reside above 0x20000000 and this area is reserved in the stack. This is OK because the function
* calling setup() is main(), which never returns. So taking a chunk of stack won't affect it. If
* main() does ever want to return, it will have to dealloate this memory.
*/
void __attribute__((naked)) setup() {
#ifdef HAS_FP_MAP
asm volatile("sub sp, #512"); // allocate 512 bytes so we have room to align data
asm volatile("mov %0, sp" : "=r" (save_stack) ); // save the location
#endif
asm volatile("push {lr}"); // save the return address
debug_init(); // perform debug initialization
gdb_init();
setup_main(); // call the "real" setup function
asm volatile("pop {pc}"); // get original return address
}
#else
void setup() {
debug_init(); // perform debug initialization
gdb_init();
setup_main(); // call the "real" setup function
}
#endif
/**
* Class
*/
#include "TeensyDebug.h"
int Debug::setBreakpoint(void *p, int n) { return debug_setBreakpoint(p, n); }
int Debug::clearBreakpoint(void *p, int n) { return debug_clearBreakpoint(p, n); }
void Debug::setCallback(void (*c)()) { callback = c; }
uint32_t Debug::getRegister(const char *reg) { return debug_getRegister(reg); }
Debug debug;

43
TeensyDebug.h

@ -0,0 +1,43 @@
#ifndef TEENSY_DEBUG_H
#define TEENSY_DEBUG_H
// is this a Teensy 3.2? If so, we can support hardware interrupts
// and should hijack setup() to set that up.
#ifdef __MK20DX256__
#define HAS_FP_MAP
#endif
// If this is used internally, not need to remap
#ifndef DEBUG_INTERNAL
// rename the original setup() because we need to hijack it
#define setup setup_main
#endif
int hcdebug_isEnabled(int n);
int hcdebug_setBreakpoint(int n);
int debug_setBreakpoint(void *p, int n);
int debug_clearBreakpoint(void *p, int n);
void debug_setCallback(void (*c)());
uint32_t debug_getRegister(const char *reg);
class Debug {
public:
int setBreakpoint(void *p, int n=1);
int clearBreakpoint(void *p, int n=1);
void setCallback(void (*c)());
uint32_t getRegister(const char *reg);
};
extern Debug debug;
#define breakpoint(n) {if (hcdebug_isEnabled(n)) {asm volatile("svc #0x11");}}
#define breakpoint_enable(n) {hcdebug_setBreakpoint(n);}
#define halt() {asm volatile("svc #0x11");}
#define DEBUGRUN FASTRUN
// set optimizations to 0 so that instructions will match
// C statements; otherwise, gcc will reorg code
#pragma GCC optimize("O0")
#endif

53
examples/breakpoint_test/breakpoint_test.ino

@ -0,0 +1,53 @@
#include "TeensyDebug.h"
volatile int mark = 45;
volatile int mark2 = 55;
void break_me() {
Serial.println("BREAK!");
Serial.println(debug.getRegister("pc"), HEX);
}
//DEBUGRUN
void testme() {
int local1=1;
int local2=2;
// halt();
mark = 5;
mark2++;
mark2++;
}
void level2() {
int local1b=15;
int local2b=25;
testme();
}
void level1() {
int local1a=45;
int local2a=65;
level2();
}
void setup() {
delay(3000);
Serial.begin(115200);
// debug.setCallback(break_me);
// debug.setBreakpoint(testme, 1);
*(int*)0 = 0;
// breakpoint_enable(0);
// breakpoint(0);
}
extern int debugcount;
void loop() {
// level1();
Serial.print("act=");Serial.println(debugcount);
Serial.print("mark=");Serial.println(mark);
delay(2000);
}

603
gdbstubs.cpp

@ -0,0 +1,603 @@
/**
* @file gdbstubs.cpp
* @author Fernando Trias
* @brief Implement GDB stub using TeensyDebug interface
* @version 0.1
* @date 2020-06-09
*
* @copyright Copyright (c) 2020 Fernando Trias
*
*/
#include <Arduino.h>
#include <HardwareSerial.h>
#define DEBUG_INTERNAL
#include "TeensyDebug.h"
/**
* Code to communicate with GDB. Use standard nomenclature.
* devInit() is not standard. It must be called at initialization.
*
*/
#define dev Serial1
/**
* @brief Get the next character from the serial
*
* @return int Character or -1 if error
*/
int getDebugChar() {
unsigned int timeout = millis() + 1000;
while(dev.available() <= 0) {
delay(1);
if (millis() > timeout) {
Serial.println("{timeout}");
return -1;
}
}
char c = dev.read();
Serial.print("{");Serial.print(c);Serial.print("}");
return c;
}
/**
* @brief Send a character to the serial
*
* @param c Character to send (one 8-bit byte)
*/
void putDebugChar(int c) {
Serial.print("[");Serial.print((char)c);Serial.print("]");
dev.write(c);
}
/**
* @brief Initialize serial
*
*/
void devInit() {
dev.begin(9600);
}
// Signal codes for ARM faults
const char *signal_text[] = {
"S05", "S10", "S11", "S11", "S10", "S04"
};
/**
* @brief Calculate checksum for message
*
* @param c Packet
* @return int Checksum
*/
int calcChecksum(const char *c) {
uint8_t sum = 0;
while(*c) {
sum += *c++;
}
return sum;
}
// constants for hex conversions
char int2hex[] = { '0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f' };
/**
* @brief Convert a hex char to a number
*
* @param ch Hex char '0', '1', etc.
* @return int Number 0-15
*/
int hex(unsigned char ch) {
if (ch >= 'a' && ch <= 'f')
return ch - 'a' + 10;
if (ch >= '0' && ch <= '9')
return ch - '0';
if (ch >= 'A' && ch <= 'F')
return ch - 'A' + 10;
return -1;
}
/**
* @brief Take a memory span and format it in hexadecimal
*
* @param buff Output in ascii hexadecimal
* @param addr Memory address to convert
* @param sz Number of bytes to convert
* @return char* The address of the last character (a \0) in buff
*/
char *mem2hex(char *buff, void *addr, int sz) {
for (int i = 0; i < sz; i++) {
uint8_t b = ((uint8_t*)addr)[i];
*buff++ = int2hex[b];
}
*buff = 0;
return buff;
}
/**
* While we find nice hex chars, build an int.
* Return number of chars processed.
*/
/**
* @brief Convert ascii hex into an integer. Used when parsing commands.
*
* @param ptr Pointer to (char*) with text. Updated as text is parsed.
* @param intValue Pointer to (int) that holds value parsed
* @return int Number of characters parsed
*/
static int hexToInt(const char **ptr, int *intValue)
{
int numChars = 0;
int hexValue;
*intValue = 0;
while (**ptr) {
hexValue = hex(**ptr);
if (hexValue < 0)
break;
*intValue = (*intValue << 4) | hexValue;
numChars++;
(*ptr)++;
}
return (numChars);
}
/**
* @brief Send result text to GDB (formatting and calculating checksum)
*
* @param result String of message to send
*/
void sendResult(const char *result) {
int checksum = calcChecksum(result);
const char *presult = result;
putDebugChar('$');
while (*presult) {
putDebugChar(*presult++);
}
putDebugChar('#');
putDebugChar(int2hex[checksum >> 4]);
putDebugChar(int2hex[checksum & 0x0F]);
// dev.flush();
}
// global flag to enabling or suspending debug system
volatile int debug_active = 1;
// global flag indicating debug should is simulating a "halt"; set to 0 to continue
volatile int halt_state = 0;
// global flag indicating Ctrl-C, causing system to "break" as soon as possible
volatile int cause_break = 0;
// main routine for processing GDB commands and states
void processGDB();
// from debug class indicating a fault
extern int debug_id;
// from debug indicting we are "stepping" instructions
extern int debugstep;
/**
* @brief Routing for processing breakpoints
*
*/
#pragma GCC push_options
#pragma GCC optimize ("O0")
void process_onbreak() {
// send the signal
sendResult(signal_text[debug_id]);
// go into halt state and stay until flag is cleared
halt_state = 1;
while(halt_state) {
delay(10);
yield();
}
}
#pragma GCC pop_options
/**
* @brief Add 32-bit ascii hexadecimal to string buffer
*
* @param p Buffer to hold ascii hex
* @param n Number to encode
* @return char* Pointer last item (a \0) so you can continue appending
*/
char *append32(char *p, int n) {
uint8_t *x = (uint8_t *) &n;
for(int i=0; i<4; i++) {
int c = x[i];
*p++ = int2hex[c >> 4];
*p++ = int2hex[c & 0x0F];
}
return p;
}
/**
* @brief Process 'g' command to return registers
*
* @param cmd Original command
* @param result String with encoded text to return
* @return int 0 = success
*/
int process_g(const char *cmd, char *result) {
result = append32(result, debug.getRegister("r0"));
result = append32(result, debug.getRegister("r1"));
result = append32(result, debug.getRegister("r2"));
result = append32(result, debug.getRegister("r3"));
result = append32(result, debug.getRegister("r4"));
result = append32(result, debug.getRegister("r5"));
result = append32(result, debug.getRegister("r6"));
result = append32(result, debug.getRegister("r7"));
result = append32(result, debug.getRegister("r8"));
result = append32(result, debug.getRegister("r9"));
result = append32(result, debug.getRegister("r10"));
result = append32(result, debug.getRegister("r11"));
result = append32(result, debug.getRegister("r12"));
result = append32(result, debug.getRegister("sp"));;
result = append32(result, debug.getRegister("lr"));
result = append32(result, debug.getRegister("pc"));
result = append32(result, debug.getRegister("cpsr"));
*result = 0;
return 0;
}
/**
* @brief Process 'G' to write registers. Not supported.
*
* @param cmd
* @param result
* @return int
*/
int process_G(const char *cmd, char *result) {
strcpy(result, "E01");
return 0;
}
/**
* @brief Process 'm' to read memory
*
* @param cmd Original command
* @param result String to return
* @return int 0 for success
*/
int process_m(const char *cmd, char *result) {
int addr, sz=4;
cmd++; // skip cmd
hexToInt(&cmd, &addr);
if (*cmd == ',') {
cmd++; // skip comma
hexToInt(&cmd, &sz);
}
Serial.print("read at ");Serial.println(addr, HEX);
if (addr == 0) {
strcpy(result, "E01");
return 0;
}
uint8_t *m = (uint8_t *)addr;
for (int i=0; i<sz; i++) {
uint8_t d = m[i];
*(result++) = int2hex[d >> 4];
*(result++) = int2hex[d & 0x0F];
}
*result = 0;
return 0;
}
/**
* @brief Process 'M' to write memory
*
* @param cmd Original command
* @param result Results 'OK'
* @return int
*/
int process_M(const char *cmd, char *result) {
int addr, sz;
cmd++; // skip command
hexToInt(&cmd, &addr);
cmd++; // skip comma
hexToInt(&cmd, &sz);
cmd++;
uint8_t *memory = (uint8_t*)addr;
for(int i=0; i<sz; i++) {
int c_high = hex(*cmd++);
int c_low = hex(*cmd++);
memory[i] = (c_high << 4) + c_low;
}
strcpy(result, "OK");
return 0;
}
/**
* @brief Process 'c' continue
*
* @param cmd Original command
* @param result String result
* @return int 1 to signal caller
*/
int process_c(const char *cmd, char *result) {
halt_state = 0; // not halted
debugstep = 0; // not stepping
strcpy(result, "");
return 1;
}
/**
* @brief Process 's' step command to step a single instruction.
* Arguments to start in a different address are not supported.
*
* @param cmd Original command
* @param result String with ENN or blank
* @return int 1 to continue; 0 if errors
*/
int process_s(const char *cmd, char *result) {
cmd++;
if (*cmd) {
// int addr;
// hexToInt(&cmd, &addr);
// we don't suppor starting in a different address
strcpy(result, "E01"); // SNN
return 0;
}
debugstep = 1; // just step
halt_state = 0;
strcpy(result, ""); // return comes from the actual break
return 1;
}
/**
* @brief Process '?' query for last stop reason. TODO.
*
* @param cmd
* @param result
* @return int
*/
int process_question(const char *cmd, char *result) {
sprintf(result, "S0%d", debug_id);
// strcpy(result, "S00");
return 0;
}
/**
* @brief 'B' deprecated and not supported
*
* @param cmd
* @param result
* @return int
*/
int process_B(const char *cmd, char *result) {
strcpy(result, "E10");
return 0;
}
/**
* @brief Process 'z' clear breakpoint at address
*
* @param cmd Original command
* @param result Result is ENN or OK
* @return int 0 if success
*/
int process_z(const char *cmd, char *result) {
int btype, addr;
cmd++;
hexToInt(&cmd, &btype);
cmd++;
hexToInt(&cmd, &addr);
// cmd++;
// hexToInt(&cmd, &sz);
if (debug.clearBreakpoint((void*)addr)) {
strcpy(result, "E01");
strcpy(result, "OK");
}
else {
strcpy(result, "OK");
}
return 0;
}
/**
* @brief Process 'Z' set breakpoint at address
*
* @param cmd Original command
* @param result Result is ENN or OK
* @return int
*/
int process_Z(const char *cmd, char *result) {
int btype, addr;
cmd++;
hexToInt(&cmd, &btype); // don't care. We figure out what's best.
cmd++;
hexToInt(&cmd, &addr);
// optional size not used because we only support Thumb
// cmd++;
// hexToInt(&cmd, &sz);
if (debug.setBreakpoint((void*)addr)) {
strcpy(result, "E01");
}
else {
strcpy(result, "OK");
}
return 0;
}
/**
* @brief Process 'q' query command. For now report back only PacketSize.
*
* @param cmd Original command
* @param result Results or ""
* @return int 0
*/
int process_q(const char *cmd, char *result) {
if (strncmp(cmd, "qSupported", 10) == 0) {
strcpy(result, "PacketSize=1024");
return 0;
}
strcpy(result, "");
return 0;
}
/**
* @brief Process a command by calling appropriate delegation function
*
* @param cmd Command and parameters
* @param result Result to send back
* @return int 0
*/
int processCommand(const char *cmd, char *result) {
switch(cmd[0]) {
case 'g': return process_g(cmd, result);
case 'G': return process_G(cmd, result);
case 'm': return process_m(cmd, result);
case 'M': return process_M(cmd, result);
case 'c': return process_c(cmd, result);
case 's': return process_s(cmd, result);
case '?': return process_question(cmd, result);
// case 'B': return process_B(cmd, result);
case 'z': return process_z(cmd, result);
case 'Z': return process_Z(cmd, result);
case 'q': return process_q(cmd, result);
}
result[0] = 0;
return 0;
}
/**
* @brief Read input, if available and process any commands
*
*/
void processGDBinput() {
char result[1024];
// no data? do nothing
if (dev.available() <= 0) return;
int c = getDebugChar();
if (c < 0) {
Serial.println("Error reading");
return;
}
// GDB ack'd our last command; don't do anything yet with this
if (c == '+') {
Serial.println("ACK");
return;
}
// GDB had a problem with last command; should resend. TODO
if (c == '-') {
Serial.println("NAK");
return;
}
// User hit Ctrl-C or other break
if (c == 0x03) {
Serial.println("Ctrl-C");
cause_break = 1; // later?
return;
}
// If we don't have a valid start command, then something went wrong so
// we just ignore it.
if (c != '$') {
Serial.print("Bad char: ");
Serial.println((char)c, HEX);
return; // wait for start char
}
// buffer to read command; matches our PacketSize
const int cmd_max = 1024;
char cmd[cmd_max]; // buffer
char *pcmd = cmd; // pointer to last char
// int sum;
int checksum = 0; // to store checksum
// 2-second timeout
while(1) {
// read next char or timeout
c = getDebugChar();
if (c == -1) {
Serial.println("read error");
putDebugChar('-');
return;
}
if (c == '#') break; // checksum follows
*pcmd++ = c;
if (pcmd >= cmd+cmd_max) { // overrun
pcmd = cmd;
}
}
*pcmd = 0;
Serial.print("got command:");
Serial.println(cmd);
c = getDebugChar();
checksum = hex(c) << 4;
c = getDebugChar();
checksum += hex(c);
if (checksum != calcChecksum(cmd)) {
Serial.println("bad checksum");
Serial.println(calcChecksum(cmd), HEX);
putDebugChar('-');
return;
}
// all good, so ACK
putDebugChar('+');
int r = processCommand(cmd, result);
// r == 1 means there are no results for now. A step or continue
// don't return immediate results. Results are returned upon
// hitting the break or successful step
if (r==1) return;
// toss results back to GDB
sendResult(result);
}
/**
* @brief Process GDB messages, including Break
*
*/
void processGDB() {
// static unsigned int nexttick = millis() + 1000;
// if (millis() > nexttick) {
// Serial.println("tick");
// nexttick += 1000;
// }
if (! debug_active) return;
processGDBinput();
if (cause_break) {
cause_break = 0;
asm volatile("svc 0x12");
}
}
// void setup_main();
IntervalTimer gdb_timer;
void gdb_init() {
if (debug_active) {
devInit();
gdb_timer.begin(processGDB, 1000);
debug.setCallback(process_onbreak);
// We could halt at startup, but maybe it's best to let user
// explicitly do so with a breakpoint(n)
// debug.setBreakpoint(setup_main, 1);
}
}

3
keywords.txt

@ -0,0 +1,3 @@
debug KEYWORD1
gdb KEYWORD2
Teensy KEYWORD3

10
library.properties

@ -0,0 +1,10 @@
name=TeensyDebug
version=0.0.1
author=Fernando Trias
maintainer=Fernando Trias
sentence=Debugging using GDB on PJRC Teensy.
paragraph=This debugging library uses GDB stub to enable remote debugging of live programs running on the Teensy.
category=Other
url=https://github.com/ftrias/TeensyDebug
architectures=*
includes=TeensyDebug.h

16
license.txt

@ -0,0 +1,16 @@
Copyright 2020 by Fernando Trias
Permission is hereby granted, free of charge, to any person obtaining a copy of this software
and associated documentation files (the "Software"), to deal in the Software without restriction,
including without limitation the rights to use, copy, modify, merge, publish, distribute,
sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or
substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Loading…
Cancel
Save