-
-
Save vvb333007/70163ccd29dc5391119eb8fa2745da71 to your computer and use it in GitHub Desktop.
ESP32 / ESP32-S2 / ESP32-S3 hardware debugging features and how to use them in your own sketches.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // Example sketch showing how on ESP32, ESP32-S2 and ESP32-S3: | |
| // | |
| // 1. Set up memory write protection (a variable in this example) (see esp_cpu_set_watchpoint()) | |
| // 2. Skip an instruction (debug_exception_handler) | |
| // 3. Handle a high-priority interrupt (di.S) | |
| // | |
| // A breakpoint can be set and intercepted in a similar way (see esp_cpu_set_breakpoint()). | |
| // DBREAK and IBREAK can be combined for controlled single-step execution. | |
| // | |
| // Author: vvb333007@gmail.com | |
| #include <Arduino.h> | |
| // Exception Frame. | |
| // | |
| struct exc_frame { | |
| uint32_t ps; | |
| uint32_t pc; // pointer to the instruction that triggered the interrupt | |
| uint32_t registers[20]; | |
| }; | |
| extern "C" void debug_exception_handler(struct exc_frame *frame); | |
| // Interrupt trigger counter | |
| static int triggered = 0; | |
| // Address of the instruction that triggered the interrupt | |
| static uintptr_t address = 0; | |
| // DebugInterrupt handler. (Generated by the CPU when protected memory is accessed) | |
| // Data watchpoints and code breakpoints arrive here, but in this example | |
| // we only handle data watchpoints. | |
| // Skips the instruction that triggered the interrupt and increments | |
| // the event counter. | |
| // Called from di.S, parameters are passed via stack. | |
| // | |
| void IRAM_ATTR debug_exception_handler(struct exc_frame *f) { | |
| // Instruction length lookup table. | |
| // Instruction length determined by its first byte. | |
| // Length may vary from 2 to 4 bytes, including 3-byte instructions. | |
| // | |
| static const uint8_t xt_insn_len[] = { XCHAL_BYTE0_FORMAT_LENGTHS }; | |
| // PC register = address of the instruction that triggered the interrupt. | |
| // Read the first instruction byte through IBUS. Harvard architecture after all. | |
| // | |
| // 1. IBUS requires aligned 32-bit access. | |
| // 2. Xtensa instructions can be 2, 3, or 4 bytes long. | |
| // | |
| // Therefore we read an aligned word and then shift it. | |
| // No other option — otherwise the CPU will hang. | |
| // | |
| // The first instruction byte (`opcode`) determines the instruction length. | |
| // | |
| uintptr_t pc = (uintptr_t)f->pc; | |
| // Save the address of the instruction that triggered the interrupt | |
| address = pc; | |
| uint32_t aligned = pc & ~3; | |
| uint32_t word = *(volatile uint32_t *)aligned; | |
| uint32_t shift = (pc & 3) * 8; | |
| uint8_t opcode = (word >> shift) & 255; | |
| // Skip the instruction | |
| f->pc = f->pc + xt_insn_len[opcode]; | |
| // Or this way, if we do not want to skip it: | |
| // esp_cpu_clear_watchpoint(0); | |
| // | |
| // Increment the counter | |
| triggered++; | |
| } | |
| // Test variable. This one will be protected. | |
| // volatile so the compiler doesn't optimize it away | |
| static volatile int test = 666; | |
| void setup() { | |
| Serial.begin(115200); | |
| // Enable protection using ESP-IDF: | |
| // Watchpoint#0, variable test, length = 4 bytes | |
| // | |
| // Length may be from 1 to 64 bytes, but the address must be aligned | |
| // to the protection size. For example, a 16-byte array must be aligned | |
| // on a 16-byte boundary. __attribute__((aligned)) to the rescue. | |
| // | |
| // Watchpoint#1 is already occupied by FreeRTOS and there are no others. | |
| // There are only two hardware watchpoints available. | |
| // | |
| esp_cpu_set_watchpoint(0, (const void *)&test, 4 , ESP_CPU_WATCHPOINT_STORE); | |
| } | |
| void loop() { | |
| // Write into protected memory: | |
| // this should trigger an interrupt and invoke debug_exception_handler(). | |
| // | |
| // The variable value will NOT change because our handler simply skips | |
| // the instruction. | |
| // | |
| // The handler may re-execute the instruction — in that case there is no | |
| // need to modify PC, but esp_cpu_clear_watchpoint(0) should be called. | |
| // | |
| // Otherwise, after re-executing the instruction, another interrupt will | |
| // immediately be generated and so on forever. | |
| // | |
| test = triggered; | |
| // A sketch without delay is not a sketch. | |
| delay(1000); | |
| // Expected output (an example): "Triggered=321, test=666, address=4200xxxx" | |
| Serial.printf("Triggered=%d, test=%d, address=%x\r\n", triggered, test, address); | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // Intercept DebugInterrupt and call a handler written in C (Xtensa, windowed ABI) | |
| // | |
| // Interrupt handlers for levels 4, 5, 6 and 7 must be written in assembly. | |
| // This is because the system does not invoke them like a regular C function. | |
| // This file contains an assembly adapter that calls regular C code. | |
| // | |
| #ifdef __XTENSA__ | |
| #include <xtensa/coreasm.h> | |
| #include <xtensa/corebits.h> | |
| #include <xtensa/config/system.h> | |
| #include "sdkconfig.h" | |
| #include "xtensa_rtos.h" | |
| #include "soc/soc.h" | |
| #include "xt_asm_utils.h" | |
| #include "xtensa_context.h" | |
| #define CAUSE_DEBUGEXCEPTION (1) | |
| #define XT_DEBUGCAUSE_DI (5) | |
| #if (CONFIG_ESP32_ECO3_CACHE_LOCK_FIX && CONFIG_BTDM_CTRL_HLI) | |
| // ESP32 v3.00 contains a hardware bug. We do not really care about the details, | |
| // and honestly we do not fully understand them either. | |
| // | |
| // The code below contains two workaround snippets copied from xtensa_vectors.S | |
| // (without the slightest understanding of how they actually work). | |
| // | |
| # define BUGGY_CHIP | |
| #endif | |
| .section .iram1, "ax" | |
| // User C function | |
| .extern debug_exception_handler | |
| .type debug_exception_handler, @function | |
| // Weak symbol, may be overridden | |
| .global xt_debugexception | |
| .type xt_debugexception, @function | |
| .align 4 | |
| .literal_position | |
| .align 4 | |
| // DebugExceptionVector saves register A0 and jumps to xt_debugexception | |
| // A0 is saved using: | |
| // wsr a0, EXCSAVE+XCHAL_DEBUGLEVEL | |
| // | |
| // See xtensa_vectors.S:_DebugExceptionVector | |
| // | |
| xt_debugexception: | |
| #ifdef BUGGY_CHIP | |
| // The original workaround code contains a strange line. | |
| // Commented it out just in case: A0 is already saved, | |
| // no need to trash somebody else's stack. | |
| // | |
| // s32i a0, sp, XT_STK_EXIT | |
| // Read bit 13 of CPU CoreID. | |
| // If it is 0, we are running on core 0; otherwise on core 1. | |
| rsr.prid a0 | |
| extui a0, a0, 13, 1 | |
| // Try to determine whether this interrupt came from BT | |
| // or whether this is a regular DebugInterrupt. | |
| // | |
| // If we think BT caused it, execute some magical delay. | |
| // No idea why. Espressif does this in their code, | |
| // so let's not argue. | |
| // | |
| #if (CONFIG_BTDM_CTRL_PINNED_TO_CORE == PRO_CPU_NUM) | |
| beqz a0, 1f | |
| #else | |
| bnez a0, 1f | |
| #endif | |
| rsr a0, DEBUGCAUSE | |
| extui a0, a0, XT_DEBUGCAUSE_DI, 1 | |
| bnez a0, debug_di_exc | |
| 1: | |
| #endif // BUGGY_CHIP | |
| // Set exception code in EXCCAUSE just in case. | |
| // | |
| movi a0, CAUSE_DEBUGEXCEPTION | |
| wsr a0, EXCCAUSE | |
| // This trampoline code is based on the GDBStub trampoline. | |
| // It should be rewritten eventually: there is no reason to copy | |
| // everything into Level1. | |
| // | |
| // The code should simply use Level6 everywhere. | |
| // | |
| // For now... | |
| // | |
| // Copy EPC and EXCSAVE from DEBUGLEVEL (Level6) | |
| // into EXCEPTIONLEVEL (Level1). | |
| // | |
| rsr a0,(EPC + XCHAL_DEBUGLEVEL) | |
| wsr a0,EPC_1 | |
| // Restore our A0 saved by _DebugExceptionVector | |
| // and also save it into Level1. | |
| // | |
| rsr a0,(EXCSAVE + XCHAL_DEBUGLEVEL) | |
| wsr a0,EXCSAVE_1 | |
| // Build trampoline | |
| // | |
| // Interrupt levels 4,5,6 and 7 are high-priority interrupts, | |
| // therefore handlers must be written in assembly. | |
| // | |
| // Since writing EVERYTHING in assembly sounds unpleasant, | |
| // set up an environment allowing us to call a C function. | |
| // | |
| // Allocate stack space for exception frame. | |
| // Pointer to this frame will be passed to the C handler. | |
| // | |
| // The C code may modify the frame contents | |
| // (for example change PC). | |
| // | |
| addi sp, sp, -XT_STK_FRMSZ | |
| // Start saving context | |
| // | |
| // Save A0 (return address) | |
| s32i a0, sp, XT_STK_EXIT | |
| s32i a0, sp, XT_STK_A0 | |
| // Save Processor State | |
| rsr a0, PS | |
| s32i a0, sp, XT_STK_PS | |
| // Save PC | |
| // | |
| // PC was copied into Level1 above, | |
| // therefore EPC_1 instead of EPC_6 | |
| rsr a0, EPC_1 | |
| s32i a0, sp, XT_STK_PC | |
| // Save standard context | |
| call0 _xt_context_save | |
| // Save SP | |
| addi a7, sp, XT_STK_FRMSZ | |
| s32i a7, sp, XT_STK_A1 | |
| // _xt_context_save() does not save A12 and A13. | |
| // Have to do it manually :( | |
| s32i a12, sp, XT_STK_A12 | |
| s32i a13, sp, XT_STK_A13 | |
| // Save EXCCAUSE and VADDR. | |
| // | |
| // Not really needed, but keep them for completeness. | |
| rsr a0, EXCCAUSE | |
| s32i a0, sp, XT_STK_EXCCAUSE | |
| rsr a0, EXCVADDR | |
| s32i a0, sp, XT_STK_EXCVADDR | |
| rsr a0, EXCSAVE_1 // remove this line later | |
| // Configure PS before calling C: | |
| // | |
| // Clear EXCM and disable level1...level5 interrupts | |
| // (everything except NMI and DEBUGINTERRUPT). | |
| // | |
| movi a0, PS_INTLEVEL(5) | PS_UM | PS_WOE | |
| wsr a0, PS | |
| // Save PC. | |
| // | |
| // This value will be available from C as frame->pc. | |
| // | |
| // It may be modified and execution will continue | |
| // at the new PC after handler exit. | |
| // | |
| rsr a0,(EPC + XCHAL_DEBUGLEVEL) | |
| s32i a0, sp, XT_STK_PC | |
| // Pass stack pointer to handler. | |
| // | |
| // Stack already contains the exception frame. | |
| // | |
| // After callx4 register A6 becomes A2 | |
| // (first function argument). | |
| mov a6, sp | |
| rsr a9, EPS_6 | |
| s32i a9, sp, XT_STK_PS | |
| // Finally. Call C handler. | |
| movi a11, debug_exception_handler | |
| callx4 a11 | |
| // Restore EPC_6 from exception frame->pc | |
| // | |
| l32i a0, sp, XT_STK_PC | |
| wsr a0,(EPC + XCHAL_DEBUGLEVEL) | |
| // Restore everything and return. | |
| // | |
| // Execution resumes from EPC_6. | |
| call0 _xt_context_restore | |
| l32i a12, sp, XT_STK_A12 | |
| l32i a13, sp, XT_STK_A13 | |
| // Return address (A0) | |
| l32i a0, sp, XT_STK_EXIT | |
| // Stack (A1==SP) | |
| addi sp, sp, XT_STK_FRMSZ | |
| rfi XCHAL_DEBUGLEVEL | |
| #ifdef BUGGY_CHIP | |
| .align 4 | |
| debug_di_exc: | |
| // Just a delay | |
| movi a0, 243 | |
| 2: | |
| addi a0, a0, -1 | |
| .rept 4 | |
| nop | |
| .endr | |
| bnez a0, 2b | |
| // Restore return address (A0) and return | |
| rsr a0, EXCSAVE+XCHAL_DEBUGLEVEL | |
| rfi XCHAL_DEBUGLEVEL | |
| #endif // BUGGY_CHIP | |
| // Mandatory lines below. | |
| // | |
| // Without them the linker will not pull this file | |
| // into the project. | |
| .global ld_include_xt_debugexception | |
| ld_include_xt_debugexception: | |
| #endif // __XTENSA__ |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment