All uncommitted work from ESP32 GDB stub development session. Includes: - GDB stub (uos_gdbstub.c/.h/_entry.S) - Startup vector table rewrite - ESP32 HAL integration files - Boot chain design docs - AGENTS.md absolute rules (commit before refactor, no unauthorized changes) - All prior deepseek session work This commit prevents further data loss. No claims of correctness.
532 lines
22 KiB
Markdown
532 lines
22 KiB
Markdown
# Xtensa LX6 (ESP32) Debug Exception Architecture — GDB Stub Reference
|
||
|
||
**Target:** ESP32 (Xtensa LX6, core `esp32_v3_49_prod`, XEA2, CALL0 ABI)
|
||
**Purpose:** Definitive reference for building a bare-metal GDB stub
|
||
**Sources:** `core-isa.h`, `corebits.h`, `specreg.h`, `xt_specreg.h`, `xtensa_vectors.S`, IDF `esp_gdbstub`, `xt_utils.h`
|
||
|
||
---
|
||
|
||
## 1. Debug Exception Mechanism
|
||
|
||
### How `break` triggers a debug exception
|
||
|
||
The `break` instruction is a 3-byte opcode. When executed, it raises a **debug exception** at **interrupt level 6** (`XCHAL_DEBUGLEVEL = 6`). Debug exceptions do NOT go through the normal EXCCAUSE/vector mechanism for level 1–5; they vector to a **dedicated debug exception vector**.
|
||
|
||
Key facts:
|
||
- **`break s, t`** — the two immediate operands (s, t) are information-only (available to the debug handler via DEBUGCAUSE). `break 0, 0` is the standard software breakpoint.
|
||
- **`break.n`** — a 16-bit (narrow) variant of break, flagged separately in DEBUGCAUSE.
|
||
- Debug exceptions are taken at **INTLEVEL 6**, which is higher than all normal interrupts (levels 1–5) but below NMI (level 7).
|
||
- The **EXCCAUSE** register is NOT set by the debug exception itself for `break` — DEBUGCAUSE records the reason. (However, the IDF panic handler manually writes EXCCAUSE to `PANIC_RSN_DEBUGEXCEPTION` for its own bookkeeping.)
|
||
|
||
### Registers used on debug exception entry
|
||
|
||
| Register | SR# | Purpose |
|
||
|----------|-----|---------|
|
||
| **EPC6** | 182 | Exception PC — saved PC of the instruction that triggered (or the next instruction for ICOUNT step) |
|
||
| **EPS6** | 198 | Exception PS — saved PS (processor state) register |
|
||
| **EXCSAVE6** | 214 | Scratch save register for level 6 — conventionally used to save `a0` (return address) at vector entry |
|
||
|
||
The hardware automatically saves:
|
||
- `PC → EPC6`
|
||
- `PS → EPS6`
|
||
|
||
Then vectors to the debug exception vector address.
|
||
|
||
### What EXCSAVE6 is used for
|
||
|
||
At the debug vector entry, `a0` is the only register you can use (no stack frame yet). The standard convention (from `xtensa_vectors.S`) is:
|
||
```asm
|
||
_DebugExceptionVector:
|
||
wsr a0, EXCSAVE6 /* save a0 — only safe scratch at level 6 */
|
||
j xt_debugexception /* jump to handler */
|
||
```
|
||
`EXCSAVE6` is the handler's dedicated scratch slot to preserve the interrupted code's `a0` before it sets up a stack frame.
|
||
|
||
### EXCCAUSE for debug exceptions
|
||
|
||
The debug exception does **not** use the standard EXCCAUSE values (0–63). Instead, the reason is in **DEBUGCAUSE** (SR 233). However, when the panic handler is invoked, it manually sets:
|
||
```c
|
||
EXCCAUSE = PANIC_RSN_DEBUGEXCEPTION // (an IDF-specific value, not an ISA EXCCAUSE)
|
||
```
|
||
|
||
---
|
||
|
||
## 2. Special Register Numbers (ESP32 / LX6)
|
||
|
||
All register numbers confirmed from `specreg.h` / `xt_specreg.h`:
|
||
|
||
### Debug-specific registers
|
||
|
||
| Register | SR Number | Symbol | R/W | Description |
|
||
|----------|-----------|--------|-----|-------------|
|
||
| **IBREAKENABLE** | **96** | `IBREAKENABLE` | RW | Bitmask: bit n enables IBREAKA_n |
|
||
| **IBREAKA_0** | **128** | `IBREAKA_0` | RW | Instruction breakpoint address 0 |
|
||
| **IBREAKA_1** | **129** | `IBREAKA_1` | RW | Instruction breakpoint address 1 |
|
||
| **DBREAKA_0** | **144** | `DBREAKA_0` | RW | Data breakpoint (watchpoint) address 0 |
|
||
| **DBREAKA_1** | **145** | `DBREAKA_1` | RW | Data breakpoint address 1 |
|
||
| **DBREAKC_0** | **160** | `DBREAKC_0` | RW | Data breakpoint control 0 |
|
||
| **DBREAKC_1** | **161** | `DBREAKC_1` | RW | Data breakpoint control 1 |
|
||
| **DEBUGCAUSE** | **233** | `DEBUGCAUSE` | RO | Debug exception cause (set by hardware on debug exception) |
|
||
| **ICOUNT** | **236** | `ICOUNT` | RW | Instruction count register (for single-stepping) |
|
||
| **ICOUNTLEVEL** | **237** | `ICOUNTLEVEL` | RW | Interrupt level at which ICOUNT triggers a debug exception |
|
||
|
||
### Level-6 exception state registers
|
||
|
||
| Register | SR Number | Symbol | Description |
|
||
|----------|-----------|--------|-------------|
|
||
| **EPC_6** | **182** | `EPC_6` | Exception PC for level 6 (debug) |
|
||
| **EPS_6** | **198** | `EPS_6` | Exception PS for level 6 (debug) |
|
||
| **EXCSAVE_6** | **214** | `EXCSAVE_6` | Scratch save for level 6 |
|
||
| **DEPC** | **192** | `DEPC` | *(Alias for EPC at debug level in some docs; ESP32 uses EPC_6)* |
|
||
|
||
### Other relevant registers
|
||
|
||
| Register | SR Number | Symbol | Description |
|
||
|----------|-----------|--------|-------------|
|
||
| **CONFIGID0** | **176** | `CONFIGID0` | RO — processor config ID (hi 32 bits) |
|
||
| **CONFIGID1** | **208** | `CONFIGID1` | RO — processor config ID (lo 32 bits) |
|
||
| **PS** | **230** | `PS` | Processor state register |
|
||
| **VECBASE** | **231** | `VECBASE` | Vector base address register |
|
||
| **EXCCAUSE** | **232** | `EXCCAUSE` | Exception cause register |
|
||
| **EXCVADDR** | **238** | `EXCVADDR` | Exception virtual address |
|
||
| **CCOUNT** | **234** | `CCOUNT` | Cycle count register |
|
||
| **PRID** | **235** | `PRID` | Processor ID |
|
||
|
||
### EPC/EPS/EXCSAVE series base values
|
||
|
||
```
|
||
EPC series base: 176 → EPC_1 = 177, EPC_2 = 178, ..., EPC_6 = 182, EPC_7 = 183
|
||
EPS series base: 192 → EPS_2 = 194, EPS_3 = 195, ..., EPS_6 = 198, EPS_7 = 199
|
||
EXCSAVE series base: 208 → EXCSAVE_1 = 209, ..., EXCSAVE_6 = 214, EXCSAVE_7 = 215
|
||
```
|
||
|
||
Note: There is no EPC_0 or EPS_0 or EPS_1 (level 0 and level 1 exceptions use different mechanisms — level 1 uses EPC_1/EPS_1... wait, actually: EPC_1=177, but EPS_1 does not exist in the table because level 1 exception PS is saved/restored differently in XEA2).
|
||
|
||
### Hardware config values
|
||
|
||
```c
|
||
XCHAL_NUM_IBREAK = 2 /* 2 instruction breakpoints */
|
||
XCHAL_NUM_DBREAK = 2 /* 2 data watchpoints */
|
||
XCHAL_DEBUGLEVEL = 6 /* debug interrupt level */
|
||
XCHAL_NMILEVEL = 7 /* NMI level */
|
||
XCHAL_NUM_INTLEVELS = 6 /* levels 1-6 (not counting level 0) */
|
||
XCHAL_EXCM_LEVEL = 3 /* level masked by PS.EXCM */
|
||
XCHAL_HW_CONFIGID0 = 0xC2BCFFFE
|
||
XCHAL_HW_CONFIGID1 = 0x1CC5FE96
|
||
```
|
||
|
||
---
|
||
|
||
## 3. DEBUGCAUSE Register (SR 233)
|
||
|
||
Read-only. Set by hardware on entry to the debug exception vector. Bit layout from `corebits.h`:
|
||
|
||
| Bit | Mask | Name | Meaning |
|
||
|-----|------|------|---------|
|
||
| 0 | 0x01 | **ICOUNT** | ICOUNT reached zero (single-step triggered) |
|
||
| 1 | 0x02 | **IBREAK** | Instruction breakpoint match |
|
||
| 2 | 0x04 | **DBREAK** | Data breakpoint (watchpoint) match |
|
||
| 3 | 0x08 | **BREAK** | `break` instruction (3-byte) executed |
|
||
| 4 | 0x10 | **BREAKN** | `break.n` instruction (2-byte narrow) executed |
|
||
| 5 | 0x20 | **DEBUGINT** | External debug interrupt (OCD) |
|
||
|
||
Reading: `rsr a0, DEBUGCAUSE` gives the OR of all active causes.
|
||
|
||
### Interpreting DEBUGCAUSE in a GDB stub
|
||
|
||
```
|
||
DEBUGCAUSE & 0x01 → ICOUNT → single-step completed
|
||
DEBUGCAUSE & 0x02 → IBREAK → hardware breakpoint hit
|
||
DEBUGCAUSE & 0x04 → DBREAK → watchpoint hit (load/store)
|
||
DEBUGCAUSE & 0x08 → BREAK → software breakpoint (break 0,0) hit
|
||
DEBUGCAUSE & 0x10 → BREAKN → narrow software breakpoint hit
|
||
DEBUGCAUSE & 0x20 → DEBUGINT→ OCD external debug interrupt
|
||
```
|
||
|
||
For a software breakpoint placed by GDB (`break 0,0`), DEBUGCAUSE bit 3 (BREAK) is set.
|
||
|
||
---
|
||
|
||
## 4. Vector Table Layout
|
||
|
||
ESP32 uses relocatable vectors (`XCHAL_HAVE_VECBASE = 1`). All vectors are offset from **VECBASE** (SR 231).
|
||
|
||
### Vector offsets in VECBASE
|
||
|
||
| Offset | Vector | Symbol |
|
||
|--------|--------|--------|
|
||
| 0x000 | Window overflow 4 | `_WindowOverflow4` |
|
||
| 0x040 | Window underflow 4 | `_WindowUnderflow4` |
|
||
| 0x080 | Window overflow 8 | |
|
||
| 0x0C0 | Window underflow 8 | |
|
||
| 0x100 | Window overflow 12 | |
|
||
| 0x140 | Window underflow 12 | |
|
||
| 0x180 | **Level 2 interrupt** | `_Level2InterruptVector` |
|
||
| 0x1C0 | **Level 3 interrupt** | `_Level3InterruptVector` |
|
||
| 0x200 | **Level 4 interrupt** | `_Level4InterruptVector` |
|
||
| 0x240 | **Level 5 interrupt** | `_Level5InterruptVector` |
|
||
| **0x280** | **Level 6 interrupt (= DEBUG)** | `_DebugExceptionVector` |
|
||
| 0x2C0 | Level 7 interrupt (= NMI) | `_NMIExceptionVector` |
|
||
| 0x300 | **Kernel exception** | `_KernelExceptionVector` |
|
||
| 0x340 | **User exception** | `_UserExceptionVector` |
|
||
| 0x3C0 | **Double exception** | `_DoubleExceptionVector` |
|
||
|
||
### Debug exception vector
|
||
|
||
```
|
||
XCHAL_DEBUG_VECOFS = 0x00000280
|
||
XCHAL_DEBUG_VECTOR_VADDR = 0x40000280 (at reset VECBASE = 0x40000000)
|
||
XCHAL_INTLEVEL6_VECOFS = 0x00000280 (same as DEBUG — level 6 IS debug)
|
||
```
|
||
|
||
**Critical:** On the ESP32, **interrupt level 6 IS the debug level**. There are no regular interrupts at level 6 (`XCHAL_INTLEVEL6_MASK = 0x00000000`). The debug exception vector at offset `0x280` from VECBASE is the entry point for ALL debug exceptions.
|
||
|
||
### Reset VECBASE value
|
||
|
||
```
|
||
XCHAL_VECBASE_RESET_VADDR = 0x40000000 (IRAM0 — instruction RAM)
|
||
```
|
||
|
||
### Debug vector code (from `xtensa_vectors.S`)
|
||
|
||
```asm
|
||
.section .DebugExceptionVector.text, "ax"
|
||
.global _DebugExceptionVector
|
||
.align 4
|
||
_DebugExceptionVector:
|
||
wsr a0, EXCSAVE6 /* preserve a0 in EXCSAVE6 */
|
||
j xt_debugexception /* jump to handler (not call — a0 is saved) */
|
||
```
|
||
|
||
The vector is minimal — it saves `a0` to `EXCSAVE6` and jumps to the full handler. The handler then:
|
||
1. Sets up a stack frame
|
||
2. Saves EPC6, EPS6 to the frame
|
||
3. Reads DEBUGCAUSE to determine why
|
||
4. Handles the debug event
|
||
5. Restores context and executes `rfi 6`
|
||
|
||
---
|
||
|
||
## 5. PS Register (SR 230) — Full Bit Layout
|
||
|
||
From `corebits.h`:
|
||
|
||
```
|
||
31 19 18 17 16 15 9 8 6 5 4 3 0
|
||
+-----------------------+-----+-------+--------+-------+-----+----+--------+
|
||
| reserved | WOE | CALLINC| reserved| OWB | UM |EXCM| INTLEVEL|
|
||
+-----------------------+-----+-------+--------+-------+-----+----+--------+
|
||
```
|
||
|
||
| Bits | Mask | Field | Description |
|
||
|------|------|-------|-------------|
|
||
| [3:0] | 0x0000000F | **INTLEVEL** | Current interrupt level (0–15). Interrupts at or below this level are masked. |
|
||
| [4] | 0x00000010 | **EXCM** | Exception mode. When set, masks interrupts to `XCHAL_EXCM_LEVEL` (= 3 on ESP32). |
|
||
| [5] | 0x00000020 | **UM** | User mode (1 = user/kernel mode distinction; 0 = privileged) |
|
||
| [7:6] | 0x000000C0 | **RING** | Current protection ring (0–3). ESP32 has `XCHAL_MMU_RINGS = 1`, so always 0. |
|
||
| [11:8] | 0x00000F00 | **OWB** | Old Window Base (saved WINDOWBASE on exception entry for windowed ABI) |
|
||
| [17:16] | 0x00030000 | **CALLINC** | Call increment (windowed ABI — how many registers the current call frame uses: 4, 8, or 12) |
|
||
| [18] | 0x00040000 | **WOE** | Window Overflow Enable (1 = window exceptions enabled; 0 = disabled) |
|
||
|
||
### CALL0 ABI considerations
|
||
|
||
For the CALL0 ABI (no windowed registers):
|
||
- `WOE = 0` — window overflow exceptions disabled
|
||
- `CALLINC = 0` — no window call increment
|
||
- `OWB` — unused/irrelevant
|
||
|
||
ABI-specific PS values for setting up a handler:
|
||
```c
|
||
// CALL0 ABI:
|
||
#define PS_GDBSTUB_HANDLER PS_INTLEVEL(5) | PS_UM // = 0x25
|
||
// The IDF gdbstub uses: PS_INTLEVEL(5) | PS_UM | PS_WOE // but WOE is harmless if WOE=0 for CALL0
|
||
|
||
// For user exception exit (CALL0):
|
||
#define PS_FOR_C_CALL PS_INTLEVEL(XCHAL_DEBUGLEVEL - 2) | PS_UM // INTLEVEL(4) | UM = 0x24
|
||
```
|
||
|
||
### INTLEVEL semantics
|
||
|
||
- `INTLEVEL = 0`: All interrupts enabled
|
||
- `INTLEVEL = N`: Masks interrupts at level ≤ N
|
||
- `INTLEVEL = 6`: Debug level — masks everything except NMI and debug exceptions
|
||
- `INTLEVEL = 7`: NMI level — masks everything
|
||
|
||
When a debug exception is taken, PS is saved to EPS6 and the new PS.INTLEVEL is set to 6.
|
||
|
||
### Important PS note for rfi 6
|
||
|
||
Before executing `rfi 6`, EPS6 must contain the PS value you want to restore. The INTLEVEL in EPS6 determines the post-return interrupt mask. Typically, you restore the original (pre-exception) PS from your saved frame.
|
||
|
||
---
|
||
|
||
## 6. The `rfi 6` Instruction (Return from Interrupt)
|
||
|
||
### Syntax
|
||
```
|
||
rfi n
|
||
```
|
||
where `n` is the interrupt level (1–7). For debug exceptions: **`rfi 6`**.
|
||
|
||
### Semantics
|
||
|
||
`rfi n` performs an atomic return-from-interrupt:
|
||
|
||
1. **Restores PS** from `EPS_n` (for n=6: from `EPS6`, SR 198)
|
||
2. **Sets PC** from `EPC_n` (for n=6: from `EPC6`, SR 182)
|
||
3. Begins execution at the restored PC
|
||
4. The new PS.INTLEVEL takes effect (unmasking interrupts as appropriate)
|
||
|
||
### What must be in EPC6/EPS6 before `rfi 6`
|
||
|
||
| Register | Must contain |
|
||
|----------|-------------|
|
||
| **EPC6** (SR 182) | The **PC to resume at**. For a breakpoint: the address of the instruction *after* the `break` (or the same address if you want to re-execute). For a single-step: the PC of the next instruction to execute. |
|
||
| **EPS6** (SR 198) | The **PS to restore** — the original pre-exception PS (interrupt level, UM, EXCM, etc. of the interrupted code). |
|
||
|
||
### Critical detail for software breakpoints
|
||
|
||
When `break 0,0` fires, `EPC6` points to the **address of the break instruction itself** (or the next instruction depending on the implementation). The IDF gdbstub reads EPC6 into the frame and adjusts it:
|
||
|
||
```asm
|
||
rsr a0, (EPC + XCHAL_DEBUGLEVEL) /* read EPC6 */
|
||
s32i a0, sp, XT_STK_PC /* save to frame */
|
||
```
|
||
|
||
On return, the handler writes the (possibly modified) PC back to EPC6 before `rfi 6`:
|
||
```asm
|
||
l32i a0, sp, XT_STK_PC
|
||
wsr a0, (EPC + XCHAL_DEBUGLEVEL) /* restore EPC6 */
|
||
...
|
||
rfi 6
|
||
```
|
||
|
||
**If you placed a software breakpoint (`break 0,0`) in flash/IRAM, you MUST:**
|
||
1. Restore the original instruction bytes before resuming
|
||
2. Set EPC6 to the address of that (now-restored) instruction
|
||
3. Execute `rfi 6`
|
||
|
||
Otherwise the CPU will re-hit the breakpoint immediately.
|
||
|
||
### rsync requirement
|
||
|
||
After `wsr` to PS, you should execute `rsync` before relying on the new PS value. After `wsr` to other registers, `isync` may be needed before the change takes effect on the instruction stream.
|
||
|
||
---
|
||
|
||
## 7. ICOUNT Single-Step Mechanism
|
||
|
||
### How it works
|
||
|
||
**ICOUNT** (SR 236) is a signed 32-bit counter that increments on every instruction executed at an interrupt level **≥ ICOUNTLEVEL**. When ICOUNT transitions from negative to zero (i.e., reaches zero), a **debug exception is generated** if the current interrupt level ≥ ICOUNTLEVEL.
|
||
|
||
### The magic value: ICOUNT = 0xFFFFFFFE (= -2)
|
||
|
||
```
|
||
ICOUNT = 0xFFFFFFFE (= signed -2)
|
||
ICOUNTLEVEL = N
|
||
```
|
||
|
||
**Sequence of events:**
|
||
1. Set `ICOUNTLEVEL = N` — only count instructions executed at INTLEVEL ≥ N
|
||
2. Set `ICOUNT = 0xFFFFFFFE` — start at -2
|
||
3. Execute `rfi 6` to return to the target code
|
||
4. **First instruction** executes: ICOUNT increments to `0xFFFFFFFF` (-1)
|
||
5. **Second instruction** executes: ICount increments to `0x00000000` (0) — **this triggers the debug exception**
|
||
|
||
Wait — this would step TWO instructions. But the IDF code says "do one step." Let me clarify:
|
||
|
||
**Actually, the correct interpretation:** The debug exception fires when ICOUNT *would increment to zero*, meaning the exception is taken *before* executing the instruction that would make it zero. So:
|
||
|
||
- ICOUNT = -2, after `rfi 6`:
|
||
- Instruction 1 executes → ICOUNT becomes -1
|
||
- Instruction 2 is about to execute → ICOUNT would become 0 → **debug exception fires INSTEAD of executing instruction 2**
|
||
|
||
So **one instruction executes**, then the debug exception fires. EPC6 points to the instruction that was *about* to execute (the second one), i.e., the **next instruction after the step**.
|
||
|
||
**Correction/Refinement (from IDF code):**
|
||
|
||
The IDF `esp_gdbstub_do_step()` does:
|
||
```c
|
||
void esp_gdbstub_do_step(esp_gdbstub_frame_t *frame)
|
||
{
|
||
uint32_t level = s_scratch.regfile.ps;
|
||
level &= 0x7; // current INTLEVEL from saved PS
|
||
level += 1; // set ICOUNTLEVEL one above current
|
||
|
||
WSR(ICOUNTLEVEL, level);
|
||
WSR(ICOUNT, -2); // 0xFFFFFFFE
|
||
}
|
||
```
|
||
|
||
The `-2` value means: execute **one more instruction** after returning, then trap.
|
||
|
||
### ICOUNTLEVEL selection
|
||
|
||
`ICOUNTLEVEL` must be set such that the instruction to be stepped runs at a level ≥ ICOUNTLEVEL. The IDF sets it to `(current PS.INTLEVEL + 1)`.
|
||
|
||
**Why +1?** Because when the code resumes after `rfi 6`, it runs at the PS.INTLEVEL saved in EPS6. If ICOUNTLEVEL equals that INTLEVEL, the counter would NOT increment (instructions only count when INTLEVEL ≥ ICOUNTLEVEL... actually instructions count when the current level is **≤** ICOUNTLEVEL per the ISA — see clarification below).
|
||
|
||
**ISA rule (from Xtensa ISA Reference):** ICOUNT increments on each instruction executed at an interrupt level **less than or equal to** ICOUNTLEVEL. So to count all normal code (INTLEVEL 0–5), set ICOUNTLEVEL ≥ the code's INTLEVEL.
|
||
|
||
The IDF sets `ICOUNTLEVEL = current_intlevel + 1` to ensure:
|
||
- The stepped instruction (at the code's original INTLEVEL) is counted
|
||
- But interrupts at higher levels are not accidentally counted
|
||
|
||
### Clearing single-step
|
||
|
||
```c
|
||
void esp_gdbstub_clear_step(void)
|
||
{
|
||
WSR(ICOUNT, 0); // Set to 0 — but since 0 is the trigger, this would immediately fire
|
||
WSR(ICOUNTLEVEL, 0); // Set level to 0 — no instructions execute at level ≤ 0...
|
||
// actually INTLEVEL 0 means count nothing useful
|
||
}
|
||
```
|
||
|
||
Setting `ICOUNTLEVEL = 0` effectively disables stepping because no executed instruction has INTLEVEL ≤ 0 (except... level 0 code itself, but the check is "INTLEVEL ≤ ICOUNTLEVEL" — if both are 0, it would count).
|
||
|
||
**The reliable way to disable:** Set `ICOUNTLEVEL = 0` AND `ICOUNT = 0`. When ICOUNTLEVEL is 0, only level-0 instructions increment ICOUNT. When ICOUNT is already 0, it stays at 0 and the debug exception condition (transition through zero) doesn't re-fire because there's no increment happening.
|
||
|
||
Actually, the precise ISA semantics: a debug exception due to ICOUNT occurs when **ICOUNT transitions from non-zero to zero** during an increment. Setting ICOUNT=0 directly (via wsr) does **not** trigger the exception — only the hardware increment path does.
|
||
|
||
### Summary of single-step setup
|
||
|
||
```
|
||
To single-step exactly ONE instruction:
|
||
|
||
1. Read the target code's PS (from EPS6 / saved frame)
|
||
2. Extract INTLEVEL: level = ps & 0xF
|
||
3. Set ICOUNTLEVEL = level (count instructions at this level or below)
|
||
4. Set ICOUNT = 0xFFFFFFFE (-2)
|
||
5. Execute rfi 6 to resume
|
||
6. One instruction executes, ICOUNT → -1 → next increment would be 0 → debug exception
|
||
7. EPC6 now points to the instruction AFTER the one that executed
|
||
```
|
||
|
||
---
|
||
|
||
## Quick Reference: Building the GDB Stub
|
||
|
||
### Software breakpoint (insert)
|
||
|
||
Replace the target instruction with `break 0, 0` (3-byte encoding). Save the original bytes for restoration.
|
||
|
||
### Software breakpoint (handle)
|
||
|
||
```asm
|
||
; In debug exception handler:
|
||
rsr a0, DEBUGCAUSE
|
||
bbci a0, 3, not_a_break ; bit 3 = BREAK
|
||
; It's a software breakpoint
|
||
; EPC6 points at/near the break instruction
|
||
; Restore original instruction, report to GDB
|
||
```
|
||
|
||
### Hardware breakpoint (IBREAK)
|
||
|
||
```c
|
||
// Set breakpoint at addr using IBREAKA0:
|
||
wsr(IBREAKA_0, addr);
|
||
uint32_t en = rsr(IBREAKENABLE);
|
||
en |= (1 << 0); // enable bit 0
|
||
wsr(IBREAKENABLE, en);
|
||
```
|
||
|
||
### Watchpoint (DBREAK)
|
||
|
||
```c
|
||
// Set watchpoint at addr, size bytes:
|
||
uint32_t dbreakc = 0x3F; // mask: all 6 bits = full 32-bit
|
||
dbreakc <<= (ffs(size) - 1); // shift for byte select
|
||
dbreakc &= 0x3F;
|
||
if (on_read) dbreakc |= (1 << 30); // DBREAKC_LOADBREAK
|
||
if (on_write) dbreakc |= (1 << 31); // DBREAKC_STOREBREAK
|
||
|
||
wsr(DBREAKA_0, addr);
|
||
wsr(DBREAKC_0, dbreakc);
|
||
```
|
||
|
||
### DBREAKC register bit layout
|
||
|
||
| Bits | Field | Description |
|
||
|------|-------|-------------|
|
||
| [31] | STOREBREAK | Trigger on store |
|
||
| [30] | LOADBREAK | Trigger on load |
|
||
| [5:0] | MASK | Byte select mask (0x3F = all bytes; shift left by log2(size)) |
|
||
|
||
### Debug exception handler skeleton (CALL0 ABI)
|
||
|
||
```asm
|
||
_DebugExceptionVector:
|
||
wsr a0, EXCSAVE6 ; save a0
|
||
j _gdbstub_debug_handler ; jump to handler
|
||
|
||
_gdbstub_debug_handler:
|
||
; Save context (a0 already in EXCSAVE6)
|
||
mov a0, sp
|
||
addi sp, sp, -FRAME_SIZE
|
||
s32i a0, sp, OFF_SP
|
||
|
||
rsr a0, EPS6 ; save PS
|
||
s32i a0, sp, OFF_PS
|
||
rsr a0, EPC6 ; save PC
|
||
s32i a0, sp, OFF_PC
|
||
rsr a0, EXCSAVE6 ; recover original a0
|
||
s32i a0, sp, OFF_A0
|
||
rsr a0, DEBUGCAUSE ; get debug cause
|
||
s32i a0, sp, OFF_DEBUGCAUSE
|
||
|
||
; ... save remaining regs, call C handler ...
|
||
|
||
; Set up PS for C handler execution (allow level 5 interrupts, clear EXCM)
|
||
movi a0, PS_INTLEVEL(5) | PS_UM ; = 0x25 for CALL0
|
||
wsr a0, PS
|
||
rsync
|
||
|
||
; Call C handler with frame pointer
|
||
mov a2, sp ; arg0 = frame pointer (CALL0 ABI)
|
||
call0 gdbstub_debug_entry
|
||
|
||
; Restore and return
|
||
l32i a0, sp, OFF_PC
|
||
wsr a0, EPC6 ; restore PC (may be modified by GDB)
|
||
l32i a0, sp, OFF_PS
|
||
wsr a0, EPS6 ; restore PS
|
||
; ... restore remaining regs ...
|
||
l32i a0, sp, OFF_SP
|
||
addi sp, sp, FRAME_SIZE
|
||
|
||
rfi 6 ; return from debug exception
|
||
```
|
||
|
||
---
|
||
|
||
## Key Constants Summary
|
||
|
||
| Constant | Value | Source |
|
||
|----------|-------|--------|
|
||
| `XCHAL_DEBUGLEVEL` | **6** | core-isa.h:405 |
|
||
| `XCHAL_NMILEVEL` | **7** | core-isa.h:407 |
|
||
| `XCHAL_NUM_IBREAK` | **2** | core-isa.h:613 |
|
||
| `XCHAL_NUM_DBREAK` | **2** | core-isa.h:614 |
|
||
| Debug vector offset | **0x280** | core-isa.h:588 |
|
||
| `XCHAL_VECBASE_RESET_VADDR` | **0x40000000** | core-isa.h:549 |
|
||
| `XCHAL_EXCM_LEVEL` | **3** | core-isa.h:351 |
|
||
| CONFIGID0 | **0xC2BCFFFE** | core-isa.h:187 |
|
||
| CONFIGID1 | **0x1CC5FE96** | core-isa.h:188 |
|
||
|
||
### Register quick-access (for `rsr`/`wsr`)
|
||
|
||
```
|
||
EPC_6 = 182 EPS_6 = 198 EXCSAVE_6 = 214
|
||
DEBUGCAUSE = 233 ICOUNT = 236 ICOUNTLEVEL = 237
|
||
IBREAKENABLE = 96 IBREAKA_0 = 128 IBREAKA_1 = 129
|
||
DBREAKA_0 = 144 DBREAKA_1 = 145 DBREAKC_0 = 160 DBREAKC_1 = 161
|
||
PS = 230 VECBASE = 231 EXCCAUSE = 232
|
||
CONFIGID0 = 176 CONFIGID1 = 208
|
||
```
|
||
|
||
---
|
||
|
||
*Document generated from ESP-IDF v5.5.4 HAL source and Xtensa LX6 configuration headers.*
|