dsh-embedded-workbench 0.8.0 → 0.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/lib/index.js +197 -197
- package/lib/types/index.d.ts +43 -43
- package/package.json +5 -3
- package/skills/c-cpp-dev/SKILL.md +121 -121
- package/skills/debug-methodology/SKILL.md +83 -83
- package/skills/debug-methodology/references/iterative-debug-case-study.md +103 -103
- package/skills/embedded-firmware-dev/SKILL.md +133 -133
- package/skills/embedded-firmware-dev/references/architecture-principles.md +204 -204
- package/skills/embedded-firmware-dev/references/embedded-patterns.md +95 -95
- package/skills/embedded-firmware-dev/references/lvgl-pitfalls.md +68 -68
- package/skills/embedded-workbench/SKILL.md +242 -242
- package/skills/embedded-workbench/references/final-qc.md +40 -40
- package/skills/embedded-workbench/references/platform-tool-mapping.md +88 -88
- package/skills/fact-check/SKILL.md +57 -57
- package/skills/hardfault-triage/SKILL.md +237 -237
- package/skills/keil-mdk-build/SKILL.md +237 -237
- package/skills/state-machine-design/SKILL.md +190 -190
- package/src/index.ts +209 -209
|
@@ -1,121 +1,121 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: c-cpp-dev
|
|
3
|
-
description: "Use when writing, reviewing, or refactoring C/C++ code, especially on 32-bit ARM embedded targets. NOT for formatting-only changes, simple file reads, non-embedded C/C++ (desktop/server), or C#/Java despite the 'C' in the name."
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
<HARD-GATE>
|
|
7
|
-
This is a domain implementation skill. If you are planning, designing, or entering plan mode — load `Skill("embedded-workbench")` first to activate the workflow gates (Plan Verification Gate, Approval Gate, Closure Gate). Domain skills carry implementation guidance, not workflow enforcement.
|
|
8
|
-
</HARD-GATE>
|
|
9
|
-
|
|
10
|
-
# C/C++ Development
|
|
11
|
-
|
|
12
|
-
Language baseline is project-specific: check the project's CLAUDE.md, build system (`-std=` flags), or compiler configuration. All rules assume 32-bit ARM target unless otherwise noted.
|
|
13
|
-
|
|
14
|
-
## Code Generation
|
|
15
|
-
|
|
16
|
-
- Respect the project's configured language standard. Don't assume a default.
|
|
17
|
-
- When a header is shared between C and C++ translation units, never define variables in the header — not even with `static`. Use `extern` in the header and exactly one definition in a single `.c` file.
|
|
18
|
-
- Allow `goto` when it simplifies cleanup or reduces complexity, but use it as a maintenance tool, not a first choice.
|
|
19
|
-
- Place Doxygen comments in header files. Keep implementation files focused on behavior.
|
|
20
|
-
- Avoid inline functions in header files. Keep function definitions in source files so the interface stays small.
|
|
21
|
-
- In C, lightweight macros or helper wrappers are acceptable if simple, local, and clearly named.
|
|
22
|
-
|
|
23
|
-
## Memory Layout
|
|
24
|
-
|
|
25
|
-
- Watch local variable bursts inside functions. Large automatic arrays, structs, or many temporaries are stack budget items.
|
|
26
|
-
- Design structures with both semantics and byte alignment in mind. For protocol parsing, byte streams, and wire formats, make packing and endianness explicit.
|
|
27
|
-
- Use `pragma` or packing attributes only when they match a wire-format requirement and are documented clearly.
|
|
28
|
-
- Use double precision and 64-bit integers with caution on 32-bit targets. Operations on 64-bit objects create performance, compatibility, and atomicity problems unless the wider width is clearly required.
|
|
29
|
-
- When a 64-bit value crosses a task, interrupt, or module boundary, make its access pattern explicit: copied, split, guarded, or protected from tearing.
|
|
30
|
-
|
|
31
|
-
## Heap and Pointer Ownership
|
|
32
|
-
|
|
33
|
-
- Treat heap objects and pointer-owned state as lifetime-sensitive resources.
|
|
34
|
-
- Make ownership, release point, and invalidation rules explicit.
|
|
35
|
-
- Avoid retaining borrowed pointers longer than the lifetime that guarantees validity.
|
|
36
|
-
- For heap-backed or pointer-rich code, document invalidation conditions so future changes don't accidentally reuse stale objects.
|
|
37
|
-
|
|
38
|
-
## Embedded C Specifics
|
|
39
|
-
|
|
40
|
-
### Hardware Register Access
|
|
41
|
-
|
|
42
|
-
Memory-mapped peripheral registers must be `volatile` to prevent the compiler from optimizing away repeated reads or writes. Use a `volatile` struct pointer (the CMSIS/HAL pattern) rather than bare casts.
|
|
43
|
-
|
|
44
|
-
```c
|
|
45
|
-
// CORRECT: volatile pointer to peripheral struct
|
|
46
|
-
typedef struct {
|
|
47
|
-
volatile uint32_t SR; // Status @ offset 0x00
|
|
48
|
-
volatile uint32_t DR; // Data @ offset 0x04
|
|
49
|
-
volatile uint32_t BRR; // Baud @ offset 0x08
|
|
50
|
-
volatile uint32_t CR1; // Control @ offset 0x0C
|
|
51
|
-
} UART_Regs;
|
|
52
|
-
#define UART2 ((UART_Regs *)0x40004400)
|
|
53
|
-
|
|
54
|
-
// BAD: missing volatile — compiler may optimize away repeated reads
|
|
55
|
-
#define UART_SR (*(uint32_t *)0x40001000)
|
|
56
|
-
while (UART_SR & TX_BUSY); // May become infinite loop under optimization
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
### Linker Section Placement
|
|
60
|
-
|
|
61
|
-
Use `__attribute__((section(...)))` to place data and code in specific memory regions.
|
|
62
|
-
|
|
63
|
-
```c
|
|
64
|
-
// .noinit: value preserved across watchdog/system reset (not power-on reset)
|
|
65
|
-
uint32_t reset_reason __attribute__((section(".noinit")));
|
|
66
|
-
|
|
67
|
-
// .ramfunc: function kept in RAM for execution during flash programming
|
|
68
|
-
__attribute__((section(".ramfunc")))
|
|
69
|
-
void flash_program_word(uint32_t addr, uint32_t data) { /* ... */ }
|
|
70
|
-
|
|
71
|
-
// DMA-accessible buffer: must not be placed in CCM (tightly-coupled memory)
|
|
72
|
-
uint8_t dma_buffer[1024] __attribute__((section(".dma_ram")));
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
### ISR-Safe vs Non-ISR-Safe Path Separation
|
|
76
|
-
|
|
77
|
-
Keep the ISR path minimal and use separate wrapper functions for task-context work. Never call blocking APIs from an ISR.
|
|
78
|
-
|
|
79
|
-
```c
|
|
80
|
-
// ISR path: minimal, no blocking, use ISR-safe RTOS primitives
|
|
81
|
-
static void uart_rx_isr(void) {
|
|
82
|
-
BaseType_t xHigherPriorityWoken = pdFALSE;
|
|
83
|
-
xSemaphoreGiveFromISR(xUartSem, &xHigherPriorityWoken);
|
|
84
|
-
portYIELD_FROM_ISR(xHigherPriorityWoken);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// Task path: can block, log, allocate, parse
|
|
88
|
-
void uart_rx_process_task(void) {
|
|
89
|
-
if (xSemaphoreTake(xUartSem, portMAX_DELAY) == pdTRUE) {
|
|
90
|
-
parse_and_dispatch(uart_rx_buf);
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
### Cortex-M Fault Handler (naked)
|
|
96
|
-
|
|
97
|
-
The HardFault handler must be `__attribute__((naked))` to prevent the compiler's prologue from corrupting the stack pointer before the exception frame can be captured.
|
|
98
|
-
|
|
99
|
-
```c
|
|
100
|
-
// naked: no prologue — SP is the exact exception frame. Required for fault handlers.
|
|
101
|
-
__attribute__((naked)) void HardFault_Handler(void) {
|
|
102
|
-
__asm volatile(
|
|
103
|
-
"tst lr, #4\n" // Check EXC_RETURN bit 2: MSP vs PSP
|
|
104
|
-
"ite eq\n"
|
|
105
|
-
"mrseq r0, msp\n" // Thread used MSP → read from MSP
|
|
106
|
-
"mrsne r0, psp\n" // Thread used PSP (RTOS) → read from PSP
|
|
107
|
-
"b HardFault_HandlerC\n"
|
|
108
|
-
);
|
|
109
|
-
}
|
|
110
|
-
```
|
|
111
|
-
|
|
112
|
-
## Refactoring
|
|
113
|
-
|
|
114
|
-
- Keep the intent of the original code clear while changing structure. A refactor should improve cohesion, readability, or maintainability without quietly changing behavior.
|
|
115
|
-
- Skip smell-only changes that don't materially improve correctness, risk, maintainability, or clarity.
|
|
116
|
-
- Prefer small, focused extractions over broad rewrites. Split one responsibility at a time.
|
|
117
|
-
- Move a helper into shared code only when reuse is real and the dependency surface stays simple.
|
|
118
|
-
- If a block is short but semantically clear and reused in multiple places, extract it into an interface named by meaning, not implementation detail.
|
|
119
|
-
- If a C module has become difficult to keep readable without fighting the language, consider C++ refactoring — but ask the user before switching languages.
|
|
120
|
-
- When reshaping APIs or control flow, keep the normal, failure, and recovery paths intact. Re-validate all three after the change.
|
|
121
|
-
- If a refactor touches ownership, lifetime, or allocation, consult memory-layout guidance.
|
|
1
|
+
---
|
|
2
|
+
name: c-cpp-dev
|
|
3
|
+
description: "Use when writing, reviewing, or refactoring C/C++ code, especially on 32-bit ARM embedded targets. NOT for formatting-only changes, simple file reads, non-embedded C/C++ (desktop/server), or C#/Java despite the 'C' in the name."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
<HARD-GATE>
|
|
7
|
+
This is a domain implementation skill. If you are planning, designing, or entering plan mode — load `Skill("embedded-workbench")` first to activate the workflow gates (Plan Verification Gate, Approval Gate, Closure Gate). Domain skills carry implementation guidance, not workflow enforcement.
|
|
8
|
+
</HARD-GATE>
|
|
9
|
+
|
|
10
|
+
# C/C++ Development
|
|
11
|
+
|
|
12
|
+
Language baseline is project-specific: check the project's CLAUDE.md, build system (`-std=` flags), or compiler configuration. All rules assume 32-bit ARM target unless otherwise noted.
|
|
13
|
+
|
|
14
|
+
## Code Generation
|
|
15
|
+
|
|
16
|
+
- Respect the project's configured language standard. Don't assume a default.
|
|
17
|
+
- When a header is shared between C and C++ translation units, never define variables in the header — not even with `static`. Use `extern` in the header and exactly one definition in a single `.c` file.
|
|
18
|
+
- Allow `goto` when it simplifies cleanup or reduces complexity, but use it as a maintenance tool, not a first choice.
|
|
19
|
+
- Place Doxygen comments in header files. Keep implementation files focused on behavior.
|
|
20
|
+
- Avoid inline functions in header files. Keep function definitions in source files so the interface stays small.
|
|
21
|
+
- In C, lightweight macros or helper wrappers are acceptable if simple, local, and clearly named.
|
|
22
|
+
|
|
23
|
+
## Memory Layout
|
|
24
|
+
|
|
25
|
+
- Watch local variable bursts inside functions. Large automatic arrays, structs, or many temporaries are stack budget items.
|
|
26
|
+
- Design structures with both semantics and byte alignment in mind. For protocol parsing, byte streams, and wire formats, make packing and endianness explicit.
|
|
27
|
+
- Use `pragma` or packing attributes only when they match a wire-format requirement and are documented clearly.
|
|
28
|
+
- Use double precision and 64-bit integers with caution on 32-bit targets. Operations on 64-bit objects create performance, compatibility, and atomicity problems unless the wider width is clearly required.
|
|
29
|
+
- When a 64-bit value crosses a task, interrupt, or module boundary, make its access pattern explicit: copied, split, guarded, or protected from tearing.
|
|
30
|
+
|
|
31
|
+
## Heap and Pointer Ownership
|
|
32
|
+
|
|
33
|
+
- Treat heap objects and pointer-owned state as lifetime-sensitive resources.
|
|
34
|
+
- Make ownership, release point, and invalidation rules explicit.
|
|
35
|
+
- Avoid retaining borrowed pointers longer than the lifetime that guarantees validity.
|
|
36
|
+
- For heap-backed or pointer-rich code, document invalidation conditions so future changes don't accidentally reuse stale objects.
|
|
37
|
+
|
|
38
|
+
## Embedded C Specifics
|
|
39
|
+
|
|
40
|
+
### Hardware Register Access
|
|
41
|
+
|
|
42
|
+
Memory-mapped peripheral registers must be `volatile` to prevent the compiler from optimizing away repeated reads or writes. Use a `volatile` struct pointer (the CMSIS/HAL pattern) rather than bare casts.
|
|
43
|
+
|
|
44
|
+
```c
|
|
45
|
+
// CORRECT: volatile pointer to peripheral struct
|
|
46
|
+
typedef struct {
|
|
47
|
+
volatile uint32_t SR; // Status @ offset 0x00
|
|
48
|
+
volatile uint32_t DR; // Data @ offset 0x04
|
|
49
|
+
volatile uint32_t BRR; // Baud @ offset 0x08
|
|
50
|
+
volatile uint32_t CR1; // Control @ offset 0x0C
|
|
51
|
+
} UART_Regs;
|
|
52
|
+
#define UART2 ((UART_Regs *)0x40004400)
|
|
53
|
+
|
|
54
|
+
// BAD: missing volatile — compiler may optimize away repeated reads
|
|
55
|
+
#define UART_SR (*(uint32_t *)0x40001000)
|
|
56
|
+
while (UART_SR & TX_BUSY); // May become infinite loop under optimization
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Linker Section Placement
|
|
60
|
+
|
|
61
|
+
Use `__attribute__((section(...)))` to place data and code in specific memory regions.
|
|
62
|
+
|
|
63
|
+
```c
|
|
64
|
+
// .noinit: value preserved across watchdog/system reset (not power-on reset)
|
|
65
|
+
uint32_t reset_reason __attribute__((section(".noinit")));
|
|
66
|
+
|
|
67
|
+
// .ramfunc: function kept in RAM for execution during flash programming
|
|
68
|
+
__attribute__((section(".ramfunc")))
|
|
69
|
+
void flash_program_word(uint32_t addr, uint32_t data) { /* ... */ }
|
|
70
|
+
|
|
71
|
+
// DMA-accessible buffer: must not be placed in CCM (tightly-coupled memory)
|
|
72
|
+
uint8_t dma_buffer[1024] __attribute__((section(".dma_ram")));
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### ISR-Safe vs Non-ISR-Safe Path Separation
|
|
76
|
+
|
|
77
|
+
Keep the ISR path minimal and use separate wrapper functions for task-context work. Never call blocking APIs from an ISR.
|
|
78
|
+
|
|
79
|
+
```c
|
|
80
|
+
// ISR path: minimal, no blocking, use ISR-safe RTOS primitives
|
|
81
|
+
static void uart_rx_isr(void) {
|
|
82
|
+
BaseType_t xHigherPriorityWoken = pdFALSE;
|
|
83
|
+
xSemaphoreGiveFromISR(xUartSem, &xHigherPriorityWoken);
|
|
84
|
+
portYIELD_FROM_ISR(xHigherPriorityWoken);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Task path: can block, log, allocate, parse
|
|
88
|
+
void uart_rx_process_task(void) {
|
|
89
|
+
if (xSemaphoreTake(xUartSem, portMAX_DELAY) == pdTRUE) {
|
|
90
|
+
parse_and_dispatch(uart_rx_buf);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Cortex-M Fault Handler (naked)
|
|
96
|
+
|
|
97
|
+
The HardFault handler must be `__attribute__((naked))` to prevent the compiler's prologue from corrupting the stack pointer before the exception frame can be captured.
|
|
98
|
+
|
|
99
|
+
```c
|
|
100
|
+
// naked: no prologue — SP is the exact exception frame. Required for fault handlers.
|
|
101
|
+
__attribute__((naked)) void HardFault_Handler(void) {
|
|
102
|
+
__asm volatile(
|
|
103
|
+
"tst lr, #4\n" // Check EXC_RETURN bit 2: MSP vs PSP
|
|
104
|
+
"ite eq\n"
|
|
105
|
+
"mrseq r0, msp\n" // Thread used MSP → read from MSP
|
|
106
|
+
"mrsne r0, psp\n" // Thread used PSP (RTOS) → read from PSP
|
|
107
|
+
"b HardFault_HandlerC\n"
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Refactoring
|
|
113
|
+
|
|
114
|
+
- Keep the intent of the original code clear while changing structure. A refactor should improve cohesion, readability, or maintainability without quietly changing behavior.
|
|
115
|
+
- Skip smell-only changes that don't materially improve correctness, risk, maintainability, or clarity.
|
|
116
|
+
- Prefer small, focused extractions over broad rewrites. Split one responsibility at a time.
|
|
117
|
+
- Move a helper into shared code only when reuse is real and the dependency surface stays simple.
|
|
118
|
+
- If a block is short but semantically clear and reused in multiple places, extract it into an interface named by meaning, not implementation detail.
|
|
119
|
+
- If a C module has become difficult to keep readable without fighting the language, consider C++ refactoring — but ask the user before switching languages.
|
|
120
|
+
- When reshaping APIs or control flow, keep the normal, failure, and recovery paths intact. Re-validate all three after the change.
|
|
121
|
+
- If a refactor touches ownership, lifetime, or allocation, consult memory-layout guidance.
|
|
@@ -1,83 +1,83 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: debug-methodology
|
|
3
|
-
description: "Use when debugging embedded firmware issues — analyzing crash logs, investigating state machine lockups, tracing sensor/signal anomalies, or performing structured root-cause analysis after a crash has been located. For fault-register and stack-frame triage, load hardfault-triage first."
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Debug Methodology
|
|
7
|
-
|
|
8
|
-
Debug by tracing values, not symptoms. These patterns come from real debugging sessions where surface-level fixes failed and root-cause analysis succeeded.
|
|
9
|
-
|
|
10
|
-
**REQUIRED BACKGROUND:** If the issue involves state machines or protocol timeouts, load `Skill("state-machine-design")` first. If the issue involves FreeRTOS tasks, ISRs, or NVM storage, load `Skill("embedded-firmware-dev")` first. Understand the domain rules before applying debugging methodology.
|
|
11
|
-
|
|
12
|
-
## Red Flags
|
|
13
|
-
|
|
14
|
-
| You think | Reality |
|
|
15
|
-
|-----------|---------|
|
|
16
|
-
| "I know where the bug is, let me fix it" | You know the symptom location. The root cause is often 3 layers away in a different module. |
|
|
17
|
-
| "One more round of trial fixes and I'll get it" | After 2 failed attempts, you need methodology, not persistence. |
|
|
18
|
-
| "I'll just add a bounds check and call it done" | You're masking a symptom. The real fix is at the data source, not at every consumer. |
|
|
19
|
-
| "The logs look normal, it must be a hardware glitch" | If you haven't correlated timestamps to code branches, you haven't actually read the logs. |
|
|
20
|
-
|
|
21
|
-
## Iron Rules
|
|
22
|
-
|
|
23
|
-
1. **Log first, not code first**: Correlate serial log timestamps to code branches before touching code. One log line at a known timestamp is worth more than reading five source files.
|
|
24
|
-
|
|
25
|
-
2. **Call-point census**: When a value isn't updating, find all call sites of the update function. A single call site (e.g., `sensor_update()` only at `main_loop.c:47`) immediately explains why refresh is delayed or gated by irrelevant conditions.
|
|
26
|
-
|
|
27
|
-
```text
|
|
28
|
-
grep -rn "sensor_update" --include="*.c"
|
|
29
|
-
src/main_loop.c:128: sensor_update(&g_pressure);
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
Only two call sites. If `main_loop.c` is gated on `wifi_connected`, the sensor won't refresh until WiFi connects — that's the root cause.
|
|
33
|
-
|
|
34
|
-
3. **Cache freshness ≠ source data readiness**: In embedded systems, "sensor has data" and "derived cache is refreshed" are concurrent events. Prefer **pull mode** (sync cache on read when source is ready). Avoid pure push mode (periodic background updaters may not fire before the first consumer reads).
|
|
35
|
-
|
|
36
|
-
```c
|
|
37
|
-
// BAD: push mode — timer callback pushes data before consumer asks for it
|
|
38
|
-
static void sensor_timer_cb(TimerHandle_t xTimer) {
|
|
39
|
-
g_sensor.cache = sensor_read_raw(); // Timer owns refresh timing
|
|
40
|
-
}
|
|
41
|
-
float get_temperature(void) {
|
|
42
|
-
return g_sensor.cache.temperature; // Stale if timer hasn't fired yet
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// GOOD: pull mode — consumer triggers refresh when source is ready
|
|
46
|
-
float get_temperature(void) {
|
|
47
|
-
if (sensor_is_ready()) {
|
|
48
|
-
sensor_sync_cache(&g_sensor); // Refresh on read
|
|
49
|
-
}
|
|
50
|
-
return g_sensor.cache.temperature; // As fresh as hardware allows
|
|
51
|
-
}
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
4. **Multi-path convergence**: When multiple independent code paths show the same error, find the shared state or cache they all read. Fix once at the update point — smaller fix, and future callers can't bypass it.
|
|
55
|
-
|
|
56
|
-
5. **Ownership boundary mapping**: Before changing behavior, identify which module owns the truth, which derives policy, and which only consumes. Don't let the GUI control backlight policy. Don't let the backlight module control DND state.
|
|
57
|
-
|
|
58
|
-
6. **Progressive narrowing**: Each investigation round shrinks scope — phenomenon → mechanism → specific state → root cause. Don't try to solve everything at once.
|
|
59
|
-
|
|
60
|
-
7. **Minimal root-cause fix**: The fix is usually 1-2 lines at the data source. If you're changing 5+ call sites, stop and ask: what single state change would make all of them correct without modification?
|
|
61
|
-
|
|
62
|
-
8. **Library source is truth**: After 2-3 rounds of custom implementation failure, stop iterating. Read the library source code (e.g., LVGL's `lv_line.c`, `lv_chart.c`) to understand the native mechanism. Adopt and adapt. Verified patterns beat custom math.
|
|
63
|
-
|
|
64
|
-
## Fix Principles
|
|
65
|
-
|
|
66
|
-
- Draw the failure signature, event timeline, and state transition chain before changing code.
|
|
67
|
-
- For defects near state transitions: trace the **exact state consumed** by that output. Stale behavior often hides in derived state, not the primary truth.
|
|
68
|
-
- **Fix state models, don't mask symptoms**: don't paper over problems with bigger limits, buffers, or retries.
|
|
69
|
-
- Prefer correcting underlying logic over adding special-case branches. Only special-handle when no cleaner alternative exists.
|
|
70
|
-
- If a bug is triggered by entering/leaving/recovering from a state, **verify every entry path** that reaches the relevant helper, not just the reproduced path.
|
|
71
|
-
- After each fix, verify the normal path, failure path, and recovery path.
|
|
72
|
-
|
|
73
|
-
## Exploration
|
|
74
|
-
|
|
75
|
-
For broad codebase searches (finding all callers of a function, locating cross-module patterns), use `Agent(subagent_type: "Explore")` instead of chaining Grep/Glob calls.
|
|
76
|
-
|
|
77
|
-
## Deep Reference
|
|
78
|
-
|
|
79
|
-
This skill's `references/` directory contains:
|
|
80
|
-
|
|
81
|
-
| Reference | Topic | Load When |
|
|
82
|
-
|-----------|-------|-----------|
|
|
83
|
-
| `iterative-debug-case-study.md` | 7-round progressive isolation methodology | Stuck after multiple fix attempts; need a structured debugging approach |
|
|
1
|
+
---
|
|
2
|
+
name: debug-methodology
|
|
3
|
+
description: "Use when debugging embedded firmware issues — analyzing crash logs, investigating state machine lockups, tracing sensor/signal anomalies, or performing structured root-cause analysis after a crash has been located. For fault-register and stack-frame triage, load hardfault-triage first."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Debug Methodology
|
|
7
|
+
|
|
8
|
+
Debug by tracing values, not symptoms. These patterns come from real debugging sessions where surface-level fixes failed and root-cause analysis succeeded.
|
|
9
|
+
|
|
10
|
+
**REQUIRED BACKGROUND:** If the issue involves state machines or protocol timeouts, load `Skill("state-machine-design")` first. If the issue involves FreeRTOS tasks, ISRs, or NVM storage, load `Skill("embedded-firmware-dev")` first. Understand the domain rules before applying debugging methodology.
|
|
11
|
+
|
|
12
|
+
## Red Flags
|
|
13
|
+
|
|
14
|
+
| You think | Reality |
|
|
15
|
+
|-----------|---------|
|
|
16
|
+
| "I know where the bug is, let me fix it" | You know the symptom location. The root cause is often 3 layers away in a different module. |
|
|
17
|
+
| "One more round of trial fixes and I'll get it" | After 2 failed attempts, you need methodology, not persistence. |
|
|
18
|
+
| "I'll just add a bounds check and call it done" | You're masking a symptom. The real fix is at the data source, not at every consumer. |
|
|
19
|
+
| "The logs look normal, it must be a hardware glitch" | If you haven't correlated timestamps to code branches, you haven't actually read the logs. |
|
|
20
|
+
|
|
21
|
+
## Iron Rules
|
|
22
|
+
|
|
23
|
+
1. **Log first, not code first**: Correlate serial log timestamps to code branches before touching code. One log line at a known timestamp is worth more than reading five source files.
|
|
24
|
+
|
|
25
|
+
2. **Call-point census**: When a value isn't updating, find all call sites of the update function. A single call site (e.g., `sensor_update()` only at `main_loop.c:47`) immediately explains why refresh is delayed or gated by irrelevant conditions.
|
|
26
|
+
|
|
27
|
+
```text
|
|
28
|
+
grep -rn "sensor_update" --include="*.c"
|
|
29
|
+
src/main_loop.c:128: sensor_update(&g_pressure);
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Only two call sites. If `main_loop.c` is gated on `wifi_connected`, the sensor won't refresh until WiFi connects — that's the root cause.
|
|
33
|
+
|
|
34
|
+
3. **Cache freshness ≠ source data readiness**: In embedded systems, "sensor has data" and "derived cache is refreshed" are concurrent events. Prefer **pull mode** (sync cache on read when source is ready). Avoid pure push mode (periodic background updaters may not fire before the first consumer reads).
|
|
35
|
+
|
|
36
|
+
```c
|
|
37
|
+
// BAD: push mode — timer callback pushes data before consumer asks for it
|
|
38
|
+
static void sensor_timer_cb(TimerHandle_t xTimer) {
|
|
39
|
+
g_sensor.cache = sensor_read_raw(); // Timer owns refresh timing
|
|
40
|
+
}
|
|
41
|
+
float get_temperature(void) {
|
|
42
|
+
return g_sensor.cache.temperature; // Stale if timer hasn't fired yet
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// GOOD: pull mode — consumer triggers refresh when source is ready
|
|
46
|
+
float get_temperature(void) {
|
|
47
|
+
if (sensor_is_ready()) {
|
|
48
|
+
sensor_sync_cache(&g_sensor); // Refresh on read
|
|
49
|
+
}
|
|
50
|
+
return g_sensor.cache.temperature; // As fresh as hardware allows
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
4. **Multi-path convergence**: When multiple independent code paths show the same error, find the shared state or cache they all read. Fix once at the update point — smaller fix, and future callers can't bypass it.
|
|
55
|
+
|
|
56
|
+
5. **Ownership boundary mapping**: Before changing behavior, identify which module owns the truth, which derives policy, and which only consumes. Don't let the GUI control backlight policy. Don't let the backlight module control DND state.
|
|
57
|
+
|
|
58
|
+
6. **Progressive narrowing**: Each investigation round shrinks scope — phenomenon → mechanism → specific state → root cause. Don't try to solve everything at once.
|
|
59
|
+
|
|
60
|
+
7. **Minimal root-cause fix**: The fix is usually 1-2 lines at the data source. If you're changing 5+ call sites, stop and ask: what single state change would make all of them correct without modification?
|
|
61
|
+
|
|
62
|
+
8. **Library source is truth**: After 2-3 rounds of custom implementation failure, stop iterating. Read the library source code (e.g., LVGL's `lv_line.c`, `lv_chart.c`) to understand the native mechanism. Adopt and adapt. Verified patterns beat custom math.
|
|
63
|
+
|
|
64
|
+
## Fix Principles
|
|
65
|
+
|
|
66
|
+
- Draw the failure signature, event timeline, and state transition chain before changing code.
|
|
67
|
+
- For defects near state transitions: trace the **exact state consumed** by that output. Stale behavior often hides in derived state, not the primary truth.
|
|
68
|
+
- **Fix state models, don't mask symptoms**: don't paper over problems with bigger limits, buffers, or retries.
|
|
69
|
+
- Prefer correcting underlying logic over adding special-case branches. Only special-handle when no cleaner alternative exists.
|
|
70
|
+
- If a bug is triggered by entering/leaving/recovering from a state, **verify every entry path** that reaches the relevant helper, not just the reproduced path.
|
|
71
|
+
- After each fix, verify the normal path, failure path, and recovery path.
|
|
72
|
+
|
|
73
|
+
## Exploration
|
|
74
|
+
|
|
75
|
+
For broad codebase searches (finding all callers of a function, locating cross-module patterns), use `Agent(subagent_type: "Explore")` instead of chaining Grep/Glob calls.
|
|
76
|
+
|
|
77
|
+
## Deep Reference
|
|
78
|
+
|
|
79
|
+
This skill's `references/` directory contains:
|
|
80
|
+
|
|
81
|
+
| Reference | Topic | Load When |
|
|
82
|
+
|-----------|-------|-----------|
|
|
83
|
+
| `iterative-debug-case-study.md` | 7-round progressive isolation methodology | Stuck after multiple fix attempts; need a structured debugging approach |
|
|
@@ -1,103 +1,103 @@
|
|
|
1
|
-
# Iterative Debugging: A Case Study in Progressive Isolation
|
|
2
|
-
|
|
3
|
-
This document models a real embedded debugging journey — not the specific bug, but the **methodology** that uncovered it across 7 rounds of progressive refinement. Use this as a reference for structuring your own debugging sessions.
|
|
4
|
-
|
|
5
|
-
## The Pattern: 7 Rounds of Progressive Narrowing
|
|
6
|
-
|
|
7
|
-
### Round 1 — Symptom: "Loading spinner never resolves"
|
|
8
|
-
|
|
9
|
-
**Initial observation**: After power-on, a sensor display page shows a loading state indefinitely. The sensor appears to be working — data is arriving at the driver level.
|
|
10
|
-
|
|
11
|
-
**First hypothesis**: The warmup timer is too short.
|
|
12
|
-
|
|
13
|
-
**Action**: Double the warmup period.
|
|
14
|
-
|
|
15
|
-
**Result**: No change. The problem is not timing.
|
|
16
|
-
|
|
17
|
-
**Lesson**: Don't tune constants without understanding the state machine. "Not waiting long enough" is the most common wrong first hypothesis.
|
|
18
|
-
|
|
19
|
-
### Round 2 — Mechanism: Timer vs. Driver ownership
|
|
20
|
-
|
|
21
|
-
**Observation**: The warmup timer lives in the sensor aggregation layer. The sensor driver has its own initialization state flowing independently.
|
|
22
|
-
|
|
23
|
-
**Hypothesis**: The timer and driver initialization are racing — the timer expires before the driver completes init.
|
|
24
|
-
|
|
25
|
-
**Action**: Move the warmup gate from the aggregation layer into the driver, where it can directly observe initialization completion.
|
|
26
|
-
|
|
27
|
-
**Result**: Improves reliability but doesn't fully fix. Some edge cases remain.
|
|
28
|
-
|
|
29
|
-
**Lesson**: Cache freshness ≠ source data readiness. The timer firing means "enough time passed," not "the sensor is ready." Couple the gate to the actual readiness signal.
|
|
30
|
-
|
|
31
|
-
### Round 3 — Edge case: Black screen after sensor disconnect/reconnect
|
|
32
|
-
|
|
33
|
-
**Symptom**: Unplugging and re-plugging the sensor during operation causes a permanent black screen instead of recovery.
|
|
34
|
-
|
|
35
|
-
**Investigation**: A global animation timer's callback is firing on a freed LVGL object, corrupting the event dispatch chain (HardFault: INVSTATE, LR in event_send_core, PC in SRAM).
|
|
36
|
-
|
|
37
|
-
**Root cause**: The animation timer outlives the page it belongs to. On page exit, the timer is deleted, but a race window allows one final callback to fire on freed memory.
|
|
38
|
-
|
|
39
|
-
**Fix**: Add a generation counter to the animation timer module. Each Create/Delete cycle increments the counter. The callback checks the generation against its captured value and safely returns if mismatched.
|
|
40
|
-
|
|
41
|
-
**Lesson**: Timer lifecycle bugs produce crashes with a distinctive signature: LR in event dispatch, PC in data memory. When you see this, audit all timer Create/Delete pairs before touching any other code.
|
|
42
|
-
|
|
43
|
-
### Round 4 — Architecture: Gating order matters
|
|
44
|
-
|
|
45
|
-
**Symptom**: The sensor recovers after disconnect, but the display shows "Loading" instead of the measurement.
|
|
46
|
-
|
|
47
|
-
**Investigation**: The recovery path's gating order is wrong. Warmup completion is checked **before** the communication error flag is cleared, so the warmup condition is satisfied (timer expired) while the error condition still blocks display.
|
|
48
|
-
|
|
49
|
-
**Fix**: Reorder the gates: check for errors first, then warmup, then data validity. Each gate must explicitly pass before proceeding to the next.
|
|
50
|
-
|
|
51
|
-
**Lesson**: Multiple independent conditions at a transition gate — verify each one explicitly. Don't assume "timer expired = everything healthy."
|
|
52
|
-
|
|
53
|
-
### Round 5 — Systemic flaw: One-directional state latch
|
|
54
|
-
|
|
55
|
-
**Symptom**: Once the display reaches "Ready" state, it never returns to "Loading" even when the sensor is disconnected and reconnected. The value briefly shows stale data, then disappears.
|
|
56
|
-
|
|
57
|
-
**Investigation**: The state machine transitions `WarmingUp → Ready` when warmup completes, but has no reverse path. When the sensor later encounters an error, the state stays Ready because no code path resets it.
|
|
58
|
-
|
|
59
|
-
**Root cause**: The state transition model assumes forward-only progress. Real systems need bidirectional transitions.
|
|
60
|
-
|
|
61
|
-
**Fix**: Add a reverse guard at the top of the state update function: if `state == Ready && error_active()`, reset to `WarmingUp`. This runs before any forward transitions.
|
|
62
|
-
|
|
63
|
-
**Lesson**: **One-directional latches are a systemic anti-pattern.** If a target state's preconditions can become false while in that state, you need a reverse transition. Audit every state variable: can it ever need to go backward?
|
|
64
|
-
|
|
65
|
-
### Round 6 — Ghost data: Derived state not cleared on reset
|
|
66
|
-
|
|
67
|
-
**Symptom**: After the Round 5 fix, a brief flicker shows a stale measurement value before Loading appears.
|
|
68
|
-
|
|
69
|
-
**Investigation**: The state reset (Ready → WarmingUp) clears `display_state` but does NOT clear `cached_sensor_value`. The old valid cached value passes a downstream `if (value > 0) → show Ready` check in the narrow window before the warmup timer starts.
|
|
70
|
-
|
|
71
|
-
**Fix**: In the reset function, clear `cached_sensor_value = NAN` alongside the state reset. Both must be cleared atomically by a single entry point.
|
|
72
|
-
|
|
73
|
-
**Lesson**: **Derived state invariant**: When resetting a primary state, also reset all derived/cached values computed from it. A single stale derived value can bypass every guard in the system.
|
|
74
|
-
|
|
75
|
-
### Round 7 — Consolidation: Single computation point
|
|
76
|
-
|
|
77
|
-
**Symptom**: Multiple display pages have slightly different loading/error display logic, causing inconsistent behavior across the UI.
|
|
78
|
-
|
|
79
|
-
**Investigation**: The display strategy logic (show value vs. show loading vs. show error) is duplicated across pages, with subtle variations.
|
|
80
|
-
|
|
81
|
-
**Fix**: Extract a single `compute_display_strategy(actual_state, error_info)` function. All pages call it. The function owns the decision; pages only render the result.
|
|
82
|
-
|
|
83
|
-
**Lesson**: When the same decision logic appears in 3+ places, consolidate it. Pages should consume display decisions, not compute them.
|
|
84
|
-
|
|
85
|
-
## Methodology Summary
|
|
86
|
-
|
|
87
|
-
| Round | What Changed | Method |
|
|
88
|
-
|-------|-------------|--------|
|
|
89
|
-
| 1 | Nothing | Tuning constants without understanding — **don't do this** |
|
|
90
|
-
| 2 | Reliability improved | Moved gate to data source — coupling check to actual signal |
|
|
91
|
-
| 3 | Crash fixed | Generation counter pattern — timer lifecycle safety |
|
|
92
|
-
| 4 | Recovery fixed | Gate ordering — explicit precondition verification |
|
|
93
|
-
| 5 | State fixed | Bidirectional transition — reverse guard pattern |
|
|
94
|
-
| 6 | Flicker fixed | Derived state invariant — atomic reset |
|
|
95
|
-
| 7 | Architecture fixed | Single computation point — consolidation |
|
|
96
|
-
|
|
97
|
-
## Key Takeaways
|
|
98
|
-
|
|
99
|
-
1. **Start with the state machine, not the symptoms.** Round 1 wasted time on a constant that Round 5 proved irrelevant.
|
|
100
|
-
2. **Each fix reveals the next layer.** Don't try to fix everything at once. Round 2 exposed Round 3; Round 5 exposed Round 6.
|
|
101
|
-
3. **Derived state is the most common source of subtle bugs.** Cached values, computed flags, display strategies — anything not at the source of truth.
|
|
102
|
-
4. **One-directional state latches are always wrong eventually.** If you can't answer "what makes it go back?", you have a bug waiting to happen.
|
|
103
|
-
5. **Consolidation happens last, not first.** Fix the individual bugs before extracting common patterns.
|
|
1
|
+
# Iterative Debugging: A Case Study in Progressive Isolation
|
|
2
|
+
|
|
3
|
+
This document models a real embedded debugging journey — not the specific bug, but the **methodology** that uncovered it across 7 rounds of progressive refinement. Use this as a reference for structuring your own debugging sessions.
|
|
4
|
+
|
|
5
|
+
## The Pattern: 7 Rounds of Progressive Narrowing
|
|
6
|
+
|
|
7
|
+
### Round 1 — Symptom: "Loading spinner never resolves"
|
|
8
|
+
|
|
9
|
+
**Initial observation**: After power-on, a sensor display page shows a loading state indefinitely. The sensor appears to be working — data is arriving at the driver level.
|
|
10
|
+
|
|
11
|
+
**First hypothesis**: The warmup timer is too short.
|
|
12
|
+
|
|
13
|
+
**Action**: Double the warmup period.
|
|
14
|
+
|
|
15
|
+
**Result**: No change. The problem is not timing.
|
|
16
|
+
|
|
17
|
+
**Lesson**: Don't tune constants without understanding the state machine. "Not waiting long enough" is the most common wrong first hypothesis.
|
|
18
|
+
|
|
19
|
+
### Round 2 — Mechanism: Timer vs. Driver ownership
|
|
20
|
+
|
|
21
|
+
**Observation**: The warmup timer lives in the sensor aggregation layer. The sensor driver has its own initialization state flowing independently.
|
|
22
|
+
|
|
23
|
+
**Hypothesis**: The timer and driver initialization are racing — the timer expires before the driver completes init.
|
|
24
|
+
|
|
25
|
+
**Action**: Move the warmup gate from the aggregation layer into the driver, where it can directly observe initialization completion.
|
|
26
|
+
|
|
27
|
+
**Result**: Improves reliability but doesn't fully fix. Some edge cases remain.
|
|
28
|
+
|
|
29
|
+
**Lesson**: Cache freshness ≠ source data readiness. The timer firing means "enough time passed," not "the sensor is ready." Couple the gate to the actual readiness signal.
|
|
30
|
+
|
|
31
|
+
### Round 3 — Edge case: Black screen after sensor disconnect/reconnect
|
|
32
|
+
|
|
33
|
+
**Symptom**: Unplugging and re-plugging the sensor during operation causes a permanent black screen instead of recovery.
|
|
34
|
+
|
|
35
|
+
**Investigation**: A global animation timer's callback is firing on a freed LVGL object, corrupting the event dispatch chain (HardFault: INVSTATE, LR in event_send_core, PC in SRAM).
|
|
36
|
+
|
|
37
|
+
**Root cause**: The animation timer outlives the page it belongs to. On page exit, the timer is deleted, but a race window allows one final callback to fire on freed memory.
|
|
38
|
+
|
|
39
|
+
**Fix**: Add a generation counter to the animation timer module. Each Create/Delete cycle increments the counter. The callback checks the generation against its captured value and safely returns if mismatched.
|
|
40
|
+
|
|
41
|
+
**Lesson**: Timer lifecycle bugs produce crashes with a distinctive signature: LR in event dispatch, PC in data memory. When you see this, audit all timer Create/Delete pairs before touching any other code.
|
|
42
|
+
|
|
43
|
+
### Round 4 — Architecture: Gating order matters
|
|
44
|
+
|
|
45
|
+
**Symptom**: The sensor recovers after disconnect, but the display shows "Loading" instead of the measurement.
|
|
46
|
+
|
|
47
|
+
**Investigation**: The recovery path's gating order is wrong. Warmup completion is checked **before** the communication error flag is cleared, so the warmup condition is satisfied (timer expired) while the error condition still blocks display.
|
|
48
|
+
|
|
49
|
+
**Fix**: Reorder the gates: check for errors first, then warmup, then data validity. Each gate must explicitly pass before proceeding to the next.
|
|
50
|
+
|
|
51
|
+
**Lesson**: Multiple independent conditions at a transition gate — verify each one explicitly. Don't assume "timer expired = everything healthy."
|
|
52
|
+
|
|
53
|
+
### Round 5 — Systemic flaw: One-directional state latch
|
|
54
|
+
|
|
55
|
+
**Symptom**: Once the display reaches "Ready" state, it never returns to "Loading" even when the sensor is disconnected and reconnected. The value briefly shows stale data, then disappears.
|
|
56
|
+
|
|
57
|
+
**Investigation**: The state machine transitions `WarmingUp → Ready` when warmup completes, but has no reverse path. When the sensor later encounters an error, the state stays Ready because no code path resets it.
|
|
58
|
+
|
|
59
|
+
**Root cause**: The state transition model assumes forward-only progress. Real systems need bidirectional transitions.
|
|
60
|
+
|
|
61
|
+
**Fix**: Add a reverse guard at the top of the state update function: if `state == Ready && error_active()`, reset to `WarmingUp`. This runs before any forward transitions.
|
|
62
|
+
|
|
63
|
+
**Lesson**: **One-directional latches are a systemic anti-pattern.** If a target state's preconditions can become false while in that state, you need a reverse transition. Audit every state variable: can it ever need to go backward?
|
|
64
|
+
|
|
65
|
+
### Round 6 — Ghost data: Derived state not cleared on reset
|
|
66
|
+
|
|
67
|
+
**Symptom**: After the Round 5 fix, a brief flicker shows a stale measurement value before Loading appears.
|
|
68
|
+
|
|
69
|
+
**Investigation**: The state reset (Ready → WarmingUp) clears `display_state` but does NOT clear `cached_sensor_value`. The old valid cached value passes a downstream `if (value > 0) → show Ready` check in the narrow window before the warmup timer starts.
|
|
70
|
+
|
|
71
|
+
**Fix**: In the reset function, clear `cached_sensor_value = NAN` alongside the state reset. Both must be cleared atomically by a single entry point.
|
|
72
|
+
|
|
73
|
+
**Lesson**: **Derived state invariant**: When resetting a primary state, also reset all derived/cached values computed from it. A single stale derived value can bypass every guard in the system.
|
|
74
|
+
|
|
75
|
+
### Round 7 — Consolidation: Single computation point
|
|
76
|
+
|
|
77
|
+
**Symptom**: Multiple display pages have slightly different loading/error display logic, causing inconsistent behavior across the UI.
|
|
78
|
+
|
|
79
|
+
**Investigation**: The display strategy logic (show value vs. show loading vs. show error) is duplicated across pages, with subtle variations.
|
|
80
|
+
|
|
81
|
+
**Fix**: Extract a single `compute_display_strategy(actual_state, error_info)` function. All pages call it. The function owns the decision; pages only render the result.
|
|
82
|
+
|
|
83
|
+
**Lesson**: When the same decision logic appears in 3+ places, consolidate it. Pages should consume display decisions, not compute them.
|
|
84
|
+
|
|
85
|
+
## Methodology Summary
|
|
86
|
+
|
|
87
|
+
| Round | What Changed | Method |
|
|
88
|
+
|-------|-------------|--------|
|
|
89
|
+
| 1 | Nothing | Tuning constants without understanding — **don't do this** |
|
|
90
|
+
| 2 | Reliability improved | Moved gate to data source — coupling check to actual signal |
|
|
91
|
+
| 3 | Crash fixed | Generation counter pattern — timer lifecycle safety |
|
|
92
|
+
| 4 | Recovery fixed | Gate ordering — explicit precondition verification |
|
|
93
|
+
| 5 | State fixed | Bidirectional transition — reverse guard pattern |
|
|
94
|
+
| 6 | Flicker fixed | Derived state invariant — atomic reset |
|
|
95
|
+
| 7 | Architecture fixed | Single computation point — consolidation |
|
|
96
|
+
|
|
97
|
+
## Key Takeaways
|
|
98
|
+
|
|
99
|
+
1. **Start with the state machine, not the symptoms.** Round 1 wasted time on a constant that Round 5 proved irrelevant.
|
|
100
|
+
2. **Each fix reveals the next layer.** Don't try to fix everything at once. Round 2 exposed Round 3; Round 5 exposed Round 6.
|
|
101
|
+
3. **Derived state is the most common source of subtle bugs.** Cached values, computed flags, display strategies — anything not at the source of truth.
|
|
102
|
+
4. **One-directional state latches are always wrong eventually.** If you can't answer "what makes it go back?", you have a bug waiting to happen.
|
|
103
|
+
5. **Consolidation happens last, not first.** Fix the individual bugs before extracting common patterns.
|