#include #include "HardwareTimer.h" #include #include #include #include #include #define LED_PIN 2 // Note: LED is attached to UART1 TX output namespace { // Max length of debug command const unsigned MAX_COMMAND_LENGTH = 64; #define TIMERTYPE_HARDWARE 1 #define TIMERTYPE_SIMPLE 2 #define TIMERTYPE_TIMER 3 /* * This example uses the hardware timer for best timing accuracy. There is only one of these on the ESP8266, * so it may not be available if another module requires it. * * Most software timing applications can use a `SimpleTimer`, which is good for intervals of up to about * 429 seconds, or around 2 hours if you compile with USE_US_TIMER=0. * * For longer intervals and delegate callback support use a `Timer`. */ #define TIMER_TYPE TIMERTYPE_HARDWARE /* * We use the timer to blink the LED at this rate */ #define BLINK_INTERVAL_MS 1000 /* * HardwareTimer defaults to non-maskable mode, so the timer callback cannot be interrupted even by the * debugger. To use break/watchpoints we must set the timer to use maskable mode. */ #define HWTIMER_TYPE eHWT_Maskable #if TIMER_TYPE == TIMERTYPE_HARDWARE HardwareTimer1 procTimer; // Hardware timer callbacks must always be in IRAM #define CALLBACK_ATTR IRAM_ATTR #elif TIMER_TYPE == TIMERTYPE_SIMPLE SimpleTimer procTimer; #define CALLBACK_ATTR GDB_IRAM_ATTR #else Timer procTimer; #define CALLBACK_ATTR GDB_IRAM_ATTR #endif // A simple log file stored on the host GdbFileStream logFile; #define LOG_FILENAME "testlog.txt" // Handles messages from SDK OsMessageInterceptor osMessageInterceptor; // Supports `consoleOff` command to prevent re-enabling when debugger is attached bool consoleOffRequested; // IFS::Gdb::FileSystem gdbfs; // Forward declarations bool handleCommand(const String& cmd); void readConsole(); /* * Notice: Software breakpoints work only on code that is in RAM. * In Sming you have to use the GDB_IRAM_ATTR to do this. */ void CALLBACK_ATTR blink() { static bool ledState; ledState = !ledState; digitalWrite(LED_PIN, ledState); } void showPrompt() { switch(gdb_present()) { case eGDB_Attached: Serial.print(_F("\r(Attached) ")); break; case eGDB_Detached: Serial.print(_F("\r(Detached) ")); break; case eGDB_NotPresent: default: Serial.print(_F("\r(Non-GDB) ")); } } void onDataReceived(Stream& source, char arrivedChar, unsigned short availableCharsCount) { static LineBuffer commandBuffer; // Error detection unsigned status = Serial.getStatus(); if(status != 0) { Serial.println(); if(bitRead(status, eSERS_Overflow)) { Serial.println(_F("** RECEIVE OVERFLOW **")); } if(bitRead(status, eSERS_BreakDetected)) { Serial.println(_F("** BREAK DETECTED **")); } if(bitRead(status, eSERS_FramingError)) { Serial.println(_F("** FRAMING ERROR **")); } if(bitRead(status, eSERS_ParityError)) { Serial.println(_F("** PARITY ERROR **")); } // Discard what is likely to be garbage Serial.clear(SERIAL_RX_ONLY); commandBuffer.clear(); showPrompt(); return; } switch(commandBuffer.process(source, Serial)) { case LineBufferBase::Action::clear: showPrompt(); break; case LineBufferBase::Action::submit: { if(commandBuffer) { handleCommand(String(commandBuffer)); commandBuffer.clear(); } showPrompt(); break; } default: break; } } /* * Demonstrate opening and reading a file from the host. */ void readFile(const char* filename, bool display) { int file = gdbfs.open(filename, File::ReadOnly); Serial << _F("gdbfs.open(\"") << filename << "\") = " << file << endl; if(file >= 0) { OneShotFastMs timer; char buf[256]; size_t total{0}; int len; do { len = gdbfs.read(file, buf, sizeof(buf)); if(len > 0) { total += size_t(len); if(display) { Serial.write(buf, len); } } } while(len == sizeof(buf)); auto elapsed = timer.elapsedTime(); Serial.println(); Serial << _F("gdbfs.read() = ") << len << _F(", total = ") << total << _F(", elapsed = ") << elapsed.toString() << _F(", av. ") << (total == 0 ? 0 : 1000U * total / elapsed) << _F(" bytes/sec") << endl; gdbfs.close(file); } } /* * A more advanced way to use host File I/O using asynchronous syscalls. * The initial open() is performed via readFileAsync(), the remaining operations are handled * in this callback function. */ void asyncReadCallback(const GdbSyscallInfo& info) { // Buffer for performing asynchronous reads static char buf[256]; // State information so we can calculate average throughput static struct { long start; // millis() when open() call completed size_t total; // Total number of file bytes read so far } transfer; switch(info.command) { case eGDBSYS_open: { int fd = info.result; String filename(FPSTR(info.open.filename)); Serial << _F("gdb_syscall_open(\"") << filename << "\") = " << fd << endl; if(fd > 0) { transfer.start = millis(); transfer.total = 0; gdb_syscall_read(fd, buf, sizeof(buf), asyncReadCallback); } break; } case eGDBSYS_read: { // m_printf(_F("\rgdb_syscall_read() = %d, total = %u"), info.result, prog.total); if(info.result > 0) { transfer.total += info.result; } if(info.result == sizeof(buf)) { gdb_syscall_read(info.read.fd, buf, sizeof(buf), asyncReadCallback); } else { gdb_syscall_close(info.read.fd, asyncReadCallback); } break; } case eGDBSYS_close: { long elapsed = millis() - transfer.start; long bps = (transfer.total == 0) ? 0 : 1000U * transfer.total / elapsed; Serial << _F("readFileAsync: total = ") << transfer.total << _F(", elapsed = ") << elapsed << _F(" ms, av. ") << bps << _F(" bytes/sec") << endl; readConsole(); break; } default:; } } /* * Read a file using callbacks. * Note that filename must be in persistent memory (e.g. flash string) as call may not be started * immediately. */ void readFileAsync(const char* filename) { gdb_syscall_open(filename, O_RDONLY, 0, asyncReadCallback); } void fileStat(const char* filename) { gdb_stat_t stat; int res = gdb_syscall_stat(filename, &stat); Serial << _F("gdb_syscall_stat(\"") << filename << _F("\") returned ") << res << endl; if(res != 0) { return; } #define PRT(x) Serial << _F(" " #x " = ") << stat.x << endl #define PRT_HEX(x) Serial << _F(" " #x " = 0x") << String(stat.x, HEX) << endl #define PRT_TIME(x) Serial << _F(" " #x " = ") << DateTime(stat.x).toFullDateTimeString() << endl PRT(st_dev); PRT(st_ino); PRT_HEX(st_mode); PRT(st_nlink); PRT_HEX(st_uid); PRT_HEX(st_gid); PRT(st_rdev); PRT(st_size); PRT(st_blksize); PRT(st_blocks); PRT_TIME(st_atime); PRT_TIME(st_mtime); PRT_TIME(st_ctime); #undef PRT #undef PRT_HEX #undef PRT_TIME } /* * Keep commands and their description together to ensure 'help' is consistent. * This also helps to keep the code clean and easy to read. */ #define COMMAND_MAP(XX) \ XX(readfile1, "Use syscall file I/O functions to read and display a host file\n" \ "Calls are blocking so the application is paused during the entire operation") \ XX(readfile2, "Read a larger host file asynchronously\n" \ "Data is processed in a callback function to avoid pausing the application un-necessarily") \ XX(stat, "Use `syscall_stat` function to get details for a host file") \ XX(ls, "Use `syscall_system` function to perform a directory listing on the host") \ XX(time, "Use `syscall_gettimeofday` to get current time from host") \ XX(log, "Show state of log file\n" \ "The log file is written to \"" LOG_FILENAME "\" in the current working directory") \ XX(break, "Demonstrate `gdb_do_break()` function to pause this application and obtain a GDB command prompt\n" \ "Similar to Ctrl+C except we control exactly where the application stops") \ XX(queueBreak, "Demonstrate `gdb_do_break()` function called via task queue\n" \ "If you run `bt` you'll see a much smaller stack trace") \ XX(consoleOff, "Break into debugger and stop reading from console\n" \ "Do this if you need to set live breakpoints or observe debug output,\n" \ "as both are blocked during console reading") \ XX(hang, "Enter infinite loop to force a watchdog timeout\n" \ "Tests the crash handler which should display a message,\n" \ "then break into the debugger, if available") \ XX(read0, "Read from invalid address\n" \ "Attempting to read from address #0 will trigger a LOAD_PROHIBITED exception") \ XX(write0, "Write to invalid address\n" \ "Attempting to write to address #0 will trigger a STORE_PROHIBITED exception") \ XX(malloc0, "Call malloc(0)") \ XX(freetwice, "Free allocated memory twice") \ XX(restart, "Restart the system\n" \ "GDB should reconnect automatically, but if not run from a terminal.\n" \ "Windows versions of GDB don't handle serial control lines well,\n" \ "so a nodeMCU, for example, may restart in the wrong mode") \ XX(disconnect, "Terminates the connection between the debugger and the remote debug target\n" \ "Calls gdb_detach() - the application will resume normal operation") \ XX(help, "Display this command summary") /* * Macro to simplify command handler function creation. * Function returns true to start another 'readConsole' request. * If the operation is completed via callback then it returns false instead, and the readConsole called at that point. */ #define COMMAND_HANDLER(name) static bool handleCommand_##name() COMMAND_HANDLER(readfile1) { // Read a small file and display it readFile(_F("Makefile"), true); return true; } COMMAND_HANDLER(readfile2) { // Read a larger file asynchronously and analyse transfer speed Serial.println(_F("Please wait...")); readFileAsync(PSTR("README.md")); return false; // When read has completed, readConsole() will be called again } COMMAND_HANDLER(stat) { fileStat(_F("Makefile")); return true; } COMMAND_HANDLER(time) { gdb_timeval_t tv; int res = gdb_syscall_gettimeofday(&tv, nullptr); if(res < 0) { Serial << _F("gdb_syscall_gettimeofday() returned ") << res << endl; } else { Serial << _F("tv_sec = ") << tv.tv_sec << _F(", tv_usec = ") << tv.tv_usec << ", " << DateTime(tv.tv_sec).toFullDateTimeString() << _F(" UTC") << endl; } return true; } COMMAND_HANDLER(log) { if(logFile.isValid()) { Serial << _F("Log file is open, size = ") << logFile.getPos() << _F(" bytes") << endl; } else { Serial.println(_F("Log file not available")); } return true; } COMMAND_HANDLER(ls) { int res = gdb_syscall_system(PSTR("ls -la")); Serial << _F("gdb_syscall_system() returned ") << res << endl; return true; } COMMAND_HANDLER(break) { Serial.println(_F("Calling gdb_do_break()")); gdb_do_break(); return true; } COMMAND_HANDLER(queueBreak) { Serial.println(_F("Queuing a call to gdb_do_break()\r\n" "This differs from `break` in that a console read will be in progress when the break is called")); System.queueCallback(handleCommand_break); return true; } COMMAND_HANDLER(consoleOff) { Serial.println(_F("To re-enable console reading, enter `call readConsole()` from GDB prompt")); gdb_do_break(); consoleOffRequested = true; return false; } COMMAND_HANDLER(hang) { Serial.println(_F("Entering infinite loop...")); Serial.flush(); while(true) { // } return true; } COMMAND_HANDLER(read0) { Serial.println(_F("Crashing app by reading from address 0\r\n" "At GDB prompt, enter `set $pc = $pc + 3` to skip offending instruction,\r\n" "then enter `c` to continue")); Serial.flush(); uint8_t value = *(volatile uint8_t*)0; Serial << _F("Value at address 0 = 0x") << String(value, HEX, 2) << endl; return true; } COMMAND_HANDLER(write0) { Serial.println(_F("Crashing app by writing to address 0\r\n" "At GDB prompt, enter `set $pc = $pc + 3` to skip offending instruction,\r\n" "then enter `c` to continue")); Serial.flush(); *(volatile uint8_t*)0 = 0; Serial.println(_F("...still running!")); return true; } /** * @brief See if the OS debug message is something we're interested in. * @param msg * @retval bool true if we want to report this */ bool __noinline parseOsMessage(OsMessage& msg) { m_printf(_F("[OS] %s\r\n"), msg.getBuffer()); if(msg.startsWith(_F("E:M "))) { Serial.println(_F("** OS Memory Error **")); return true; } else if(msg.contains(_F(" assert "))) { Serial.println(_F("** OS Assert **")); return true; } else { return false; } } /** * @brief Called when the OS outputs a debug message using os_printf, etc. * @param msg The message */ void onOsMessage(OsMessage& msg) { // Note: We do the check in a separate function to avoid messing up the stack pointer if(parseOsMessage(msg)) { if(gdb_present() == eGDB_Attached) { gdb_do_break(); } else { #ifdef ARCH_ESP8266 register uint32_t sp __asm__("a1"); debug_print_stack(sp + 0x10, 0x3fffffb0); #endif } } } COMMAND_HANDLER(malloc0) { Serial.println( _F("Attempting to allocate a zero-length array results in an OS debug message.\r\n" "The message starts with 'E:M ...' and can often indicate a more serious memory allocation issue.")); auto mem = os_malloc(0); os_free(mem); return true; } COMMAND_HANDLER(freetwice) { Serial.println(_F("Attempting to free the same memory twice is a common bug.\r\n" "On the test system we see an assertion failure message from the OS.")); auto mem = static_cast(os_malloc(123)); os_free(mem); os_free(mem); return true; } COMMAND_HANDLER(restart) { Serial.println(_F("Restarting....")); System.restart(); return false; } COMMAND_HANDLER(disconnect) { // End console test Serial.print(_F("Calling gdb_detach() - ")); if(gdb_present() == eGDB_Attached) { Serial.println(_F("resuming normal program execution.")); } else if(gdb_present() == eGDB_Detached) { Serial.println(_F("not attached, so does nothing")); } else { Serial.println(_F("Application isn't compiled using ENABLE_GDB so this does nothing.")); } Serial.flush(); gdb_detach(); return false; } COMMAND_HANDLER(help) { Serial.print(_F("LiveDebug interactive debugger sample. Available commands:\r\n")); auto print = [](const char* tag, const char* desc) { const unsigned indent = 10; Serial << " " << String(tag).pad(indent) << " : "; // Print multi-line descriptions in sections to maintain correct line indentation String s; s.pad(2 + indent + 3); for(;;) { auto end = strchr(desc, '\n'); if(end == nullptr) { Serial.println(desc); break; } else { Serial.write(desc, end - desc); Serial.println(); desc = end + 1; Serial.print(s); } } }; #define XX(tag, desc) print(_F(#tag), _F(desc)); COMMAND_MAP(XX) #undef XX return true; } /** * @brief User typed a command. Deal with it. * @retval bool true to continue reading another command */ bool handleCommand(const String& cmd) { if(logFile.isValid()) { logFile << _F("handleCommand('") << cmd << "')" << endl; } #define XX(tag, desc) \ if(cmd.equalsIgnoreCase(F(#tag))) { \ return handleCommand_##tag(); \ } COMMAND_MAP(XX) #undef XX Serial << _F("Unknown command '") << cmd << _F("', try 'help'") << endl; return true; } /* * Completion callback for console read test. See readConsole(). * * When the syscall is executed, GDB is instructed to read a line of text from the console. * GDB implements a line-editor, so information will only be sent when you hit return. * Typing Ctrl+D sends the line immediately without any return (note: this doesn't work on Windows.) * * We continue running until GDB is ready to send the result, which is written to the * buffer provided in the original call. This callback function then gets called via the * task queue. * * Data received will include any return character typed. */ void onConsoleReadCompleted(const GdbSyscallInfo& info) { int result = info.result; char* bufptr = static_cast(info.read.buffer); debug_i("gdb_read_console() returned %d", result); if(result > 0) { // Remove trailing newline character unsigned len = result; if(bufptr[len - 1] == '\n') { --len; } if(len > 0) { String cmd(bufptr, len); if(!handleCommand(cmd)) { return; // Don't call readConsole } } } // Start another console read readConsole(); } /* * Demonstrate GDB console access. * We actually queue this so it can be called directly from GDB to re-enable console reading * after using the `consoleOff` command. */ void readConsole() { consoleOffRequested = false; System.queueCallback(InterruptCallback([]() { showPrompt(); if(gdb_present() == eGDB_Attached) { // Issue the syscall static char buffer[MAX_COMMAND_LENGTH]; int res = gdb_console_read(buffer, MAX_COMMAND_LENGTH, onConsoleReadCompleted); if(res < 0) { Serial.printf(_F("gdb_console_read() failed, %d\r\n"), res); Serial.println(_F("Is GDBSTUB_ENABLE_SYSCALL enabled ?")); showPrompt(); } /* * GDB executes the system call, finished in onReadCompleted(). * Note that any serial output gets ignored by GDB whilst executing a system * call. */ } else { /* * GDB is either detached or not present, serial callback will process input */ } })); } void printTimerDetails() { Serial << procTimer << ", maxTicks = " << procTimer.maxTicks() << ", maxTime = " << procTimer.micros().ticksToTime(procTimer.maxTicks()).value() << endl; } } // namespace extern "C" void gdb_on_attach(bool attached) { debug_i("GdbAttach(%d)", attached); if(attached) { // Open a log file on the host to demonstrate use of GdbFileStream logFile.open(F(LOG_FILENAME), File::WriteOnly | File::Create); debug_i("open log %d", logFile.getLastError()); logFile.println(); logFile.println(_F("\r\n=== OPENED ===")); gdb_timeval_t tv; gdb_syscall_gettimeofday(&tv, nullptr); logFile.println(DateTime(tv.tv_sec).toFullDateTimeString()); // Start interacting with GDB if(!consoleOffRequested) { readConsole(); } } else { // Note: GDB is already detached so underlying call to gdb_syscall_close() will fail silently logFile.close(); } } void GDB_IRAM_ATTR init() { Serial.begin(SERIAL_BAUD_RATE); Serial.onDataReceived(onDataReceived); Serial.systemDebugOutput(true); Serial.println(_F("LiveDebug sample\r\n" "Explore some capabilities of the GDB debugger.\r\n")); // Install a debug output hook to monitor OS debug messages osMessageInterceptor.begin(onOsMessage); if(gdb_present() != eGDB_Attached) { System.onReady(showPrompt); } pinMode(LED_PIN, OUTPUT); procTimer.initializeMs(blink).start(); printTimerDetails(); }