Skip to content

Commit 3f60725

Browse files
committed
fix(web): add auto slot mode and FPB_PATCH_COMP_ID macro for NuttX comp alignment
NuttX arm_breakpoint_add auto-assigns FPB comparators from comp 0, ignoring the user-specified comp_id. This causes enable/disable to operate on the wrong hardware comparator when manually selecting a slot other than 0. - Add 'Auto' option (value=-1) to slot dropdown, selected by default - Auto mode uses find_slot_for_target (first-empty strategy), matching NuttX's allocation algorithm so logical slot = physical comparator - Replace hardcoded fpb_enable_patch(N, ...) in patch template with FPB_PATCH_COMP_ID macro, injected via -D at compile time - Remove Slot line from template header comment - Skip slot-occupied prompt in auto mode (backend Smart Reuse handles it) - Add i18n translations for slot_auto (en/zh-CN/zh-TW)
1 parent 2e5d11b commit 3f60725

12 files changed

Lines changed: 365 additions & 14 deletions

File tree

Tools/WebServer/core/compiler.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,7 @@ def compile_inject(
235235
source_file: str = None,
236236
inject_functions: List[str] = None,
237237
inject_marker_lines: List[int] = None,
238+
comp_id: int = -1,
238239
) -> Tuple[Optional[bytes], Optional[Dict[str, int]], str]:
239240
"""
240241
Compile injection code from source content to binary.
@@ -413,6 +414,10 @@ def compile_inject(
413414

414415
cmd.extend(["-o", obj_file, compile_source])
415416

417+
# Inject FPB_PATCH_COMP_ID define for patch enable/disable in user code
418+
if comp_id >= 0:
419+
cmd.extend(["-D", f"FPB_PATCH_COMP_ID={comp_id}"])
420+
416421
if verbose:
417422
logger.info(f"Compile: {' '.join(cmd)}")
418423

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
# NuttX FPB Comp 自动分配问题整改方案
2+
3+
> 日期: 2026-04-07
4+
> 状态: 待实施
5+
6+
## 1. 问题描述
7+
8+
### 1.1 现象
9+
10+
在 NuttX 平台上使用 dpatch 模式注入时,手动指定 `--comp 1` 后:
11+
- `fl -c info` 显示 `Slot[0]` 的 COMP 寄存器有值(on),`Slot[1]` 的 COMP 为 0(off)
12+
- `fl -c enable --comp 1 --enable 1` 返回 `[FLERR] Failed to enable patch 1: -2`
13+
- 但 patch 代码内部的 `fpb_enable_patch(0, false/true)` 可以正常工作(因为恰好对上了 comp 0)
14+
15+
### 1.2 根因
16+
17+
NuttX 的 `arm_breakpoint_add``arch/arm/src/armv8-m/arm_dbgmonitor.c`)不支持指定硬件 comparator 编号,而是从 comp 0 开始遍历,找到第一个空闲的自动分配:
18+
19+
```c
20+
// NuttX arm_breakpoint_add 核心逻辑
21+
for (i = 0; i < num; i++) {
22+
uint32_t comp = getreg32(FPB_COMP0 + i * 4);
23+
if (comp == fpb_comp) // 已设置,返回
24+
return 0;
25+
else if (comp & ENABLE) // 被占用,跳过
26+
continue;
27+
else // 空闲,使用这个
28+
putreg32(fpb_comp, FPB_COMP0 + i * 4);
29+
return 0;
30+
}
31+
```
32+
33+
因此 `fpb_debugmon_nuttx.c``set_redirect(comp_id=1, ...)` 调用 `up_debugpoint_add` 后,NuttX 实际将断点写入了硬件 FPB_COMP[0](第一个空闲的)。`comp_id` 只是 `g_debugmon_state.redirects[]` 数组的索引,与硬件 comparator 编号不对应。
34+
35+
`fpb_enable_patch(1, true)` 直接操作 `FPB_COMP(1)` 寄存器,该寄存器为空,返回 `FPB_ERR_INVALID_PARAM`
36+
37+
### 1.3 影响范围
38+
39+
| 场景 | 是否受影响 | 说明 |
40+
|------|:----------:|------|
41+
| dpatch --comp 0(且 comp 0 空闲) | ✅ 正常 | NuttX 自动分配到 comp 0,恰好对上 |
42+
| dpatch --comp 1(且 comp 0 空闲) | ❌ 异常 | NuttX 分配到 comp 0,但 enable 操作 comp 1 |
43+
| dpatch --comp 1(且 comp 0 已占用) | ✅ 正常 | NuttX 跳过 comp 0,分配到 comp 1 |
44+
| patch/tpatch(FPBv1 REMAP 模式) | ✅ 正常 | 直接写 FPB_COMP 寄存器,不走 NuttX API |
45+
| patch 代码内 fpb_enable_patch | ✅ 正常 | 操作的是实际有值的硬件 comp |
46+
47+
核心矛盾:用户指定的 `comp_id` 是逻辑槽位号,NuttX 分配的是物理 comparator 号,两者不一定相等。
48+
49+
## 2. 整改方案
50+
51+
### 2.1 思路
52+
53+
不改下位机固件。在上位机侧保证逻辑槽位号与物理 comparator 号一致:
54+
55+
1. **Slot 下拉菜单默认自动模式**:复用 `find_slot_for_target` 的分配逻辑(从 slot 0 开始找第一个空闲),与 NuttX `arm_breakpoint_add` 的分配算法一致
56+
2. **Patch 模板中 comp id 参数化**:通过编译宏 `-DFPB_PATCH_COMP_ID=N` 传入,避免源码中硬编码
57+
58+
### 2.2 整改点一:Slot 下拉菜单增加自动模式
59+
60+
**文件**: `templates/partials/editor.html`
61+
62+
`slotSelect` 下拉菜单最前面增加 "Auto" 选项,value 为 -1,设为默认选中:
63+
64+
```html
65+
<select id="slotSelect" ...>
66+
<option value="-1" selected data-i18n="device.slot_auto">Auto</option>
67+
<option value="0" ...>Slot 0</option>
68+
...
69+
</select>
70+
```
71+
72+
**文件**: `static/js/core/slots.js`
73+
74+
- `onSlotSelectChange`:当选择 Auto(-1)时,`state.selectedSlot = -1`
75+
- `updateSlotUI`:Auto 模式下状态栏显示 "Slot: Auto"
76+
77+
**文件**: `static/js/features/patch.js``performInject`
78+
79+
`state.selectedSlot === -1` 时,传 `comp: -1` 给后端。后端 `inject()` 已有 `comp < 0` 时调用 `find_slot_for_target` 的逻辑,无需改动。
80+
81+
**关键点**`find_slot_for_target` 的分配算法是"找第一个空闲 slot",与 NuttX `arm_breakpoint_add` 的"找第一个空闲 comp"一致,因此自动模式下逻辑槽位号 = 物理 comparator 号。
82+
83+
**重复注入行为**`find_slot_for_target` 已实现 Smart Reuse 策略 — 如果同一个 `target_addr` 已在某个 slot 中,直接复用该 slot(先 unpatch 再重新注入),不会新开 slot。
84+
85+
### 2.3 整改点二:Patch 模板 comp id 参数化
86+
87+
**现状**`static/js/features/patch.js``generatePatchTemplate`):
88+
89+
```c
90+
/**
91+
* Patch for: fl_hello
92+
* Slot: 0
93+
* Original: 0x08001234
94+
*/
95+
...
96+
fpb_enable_patch(0, false);
97+
ORIG_FL_HELLO();
98+
fpb_enable_patch(0, true);
99+
```
100+
101+
slot 值写死在源码里。如果用户切换 slot 或使用自动模式重新注入,源码里的 comp id 不会更新。
102+
103+
**整改后**:
104+
105+
模板中 `fpb_enable_patch` 改为使用宏,头部注释去掉 Slot 行:
106+
107+
```c
108+
/**
109+
* Patch for: fl_hello
110+
* Original: 0x08001234
111+
*/
112+
...
113+
fpb_enable_patch(FPB_PATCH_COMP_ID, false);
114+
ORIG_FL_HELLO();
115+
fpb_enable_patch(FPB_PATCH_COMP_ID, true);
116+
```
117+
118+
`FPB_PATCH_COMP_ID` 完全由编译时 `-D` 注入,源码中不定义默认值。未定义时编译报错,这是期望行为 — 强制要求通过构建系统传入,避免静默使用错误的 comp id。
119+
120+
**文件**: `core/compiler.py``compile_inject`
121+
122+
新增 `comp_id` 参数,在构建编译命令时追加 `-DFPB_PATCH_COMP_ID=N`
123+
124+
```python
125+
def compile_inject(
126+
...
127+
comp_id: int = -1, # 新增
128+
) -> Tuple[...]:
129+
```
130+
131+
```python
132+
# 在 cmd 构建完成后
133+
if comp_id >= 0:
134+
cmd.extend(["-D", f"FPB_PATCH_COMP_ID={comp_id}"])
135+
```
136+
137+
**文件**: `fpb_inject.py``inject`
138+
139+
`actual_comp` 传递给第二次 `compile_inject`(此时 comp 已确定):
140+
141+
```python
142+
# 第二次编译(已知 actual_comp)
143+
data, inject_symbols, error = self.compile_inject(
144+
...
145+
comp_id=actual_comp,
146+
)
147+
```
148+
149+
第一次编译(用于计算 code_size,base_addr=0x20000000)也需要传一个临时值。由于第一次编译只是为了确定大小,comp_id 不影响二进制大小,传 0 即可:
150+
151+
```python
152+
# 第一次编译(确定大小)
153+
data, inject_symbols, error = self.compile_inject(
154+
...
155+
comp_id=0, # 临时值,不影响 code_size
156+
)
157+
```
158+
159+
## 3. 兼容性
160+
161+
| 场景 | 影响 |
162+
|------|------|
163+
| 已有 patch 源码(硬编码 comp id) | 不受影响,`-D` 不会覆盖源码中的字面量 |
164+
| 已有 patch 源码(使用 `FPB_PATCH_COMP_ID` 宏) |`-D` 自动赋值,正常工作 |
165+
| MCP 工具注入(comp=-1) | 已支持自动分配,无需改动 |
166+
| 手动选择 Slot 0~7 | 行为不变,传指定的 comp id |
167+
| NuttX dpatch 自动模式 | 修复,逻辑槽位 = 物理 comp |
168+
169+
## 4. 涉及文件
170+
171+
| 文件 | 改动 |
172+
|------|------|
173+
| `templates/partials/editor.html` | slotSelect 增加 Auto 选项(默认选中) |
174+
| `static/js/core/slots.js` | 支持 selectedSlot = -1,状态栏显示 "Auto" |
175+
| `static/js/features/patch.js` | 模板使用 `FPB_PATCH_COMP_ID` 宏,去掉 Slot 注释行 |
176+
| `core/compiler.py` | 新增 comp_id 参数,编译时注入 `-DFPB_PATCH_COMP_ID=N` |
177+
| `fpb_inject.py` | 两次 compile_inject 调用传递 comp_id |

Tools/WebServer/fpb_inject.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,7 @@ def compile_inject(
371371
source_file: str = None,
372372
inject_functions: list = None,
373373
inject_marker_lines: list = None,
374+
comp_id: int = -1,
374375
) -> Tuple[Optional[bytes], Optional[Dict[str, int]], str]:
375376
"""Compile injection code from source content or file to binary."""
376377
return compiler_utils.compile_inject(
@@ -385,6 +386,7 @@ def compile_inject(
385386
source_file=source_file,
386387
inject_functions=inject_functions,
387388
inject_marker_lines=inject_marker_lines,
389+
comp_id=comp_id,
388390
)
389391

390392
# ========== Injection Workflow ==========
@@ -531,6 +533,7 @@ def inject(
531533
source_file=source_file,
532534
inject_functions=inject_functions,
533535
inject_marker_lines=inject_marker_lines,
536+
comp_id=actual_comp if actual_comp >= 0 else 0,
534537
)
535538
if error:
536539
return False, {"error": error}
@@ -561,6 +564,7 @@ def inject(
561564
source_file=source_file,
562565
inject_functions=inject_functions,
563566
inject_marker_lines=inject_marker_lines,
567+
comp_id=actual_comp,
564568
)
565569
if error:
566570
return False, {"error": error}

Tools/WebServer/static/js/core/slots.js

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,12 @@ function updateSlotUI() {
7474
document.getElementById('activeSlotCount').textContent =
7575
`${activeCount}/${maxSlots}`;
7676
const slotDisplay = document.getElementById('currentSlotDisplay');
77-
const slotValue = state.selectedSlot != null ? state.selectedSlot : '-';
77+
const slotValue =
78+
state.selectedSlot != null
79+
? state.selectedSlot < 0
80+
? 'Auto'
81+
: state.selectedSlot
82+
: '-';
7883
slotDisplay.textContent = t('statusbar.slot', 'Slot: {{slot}}', {
7984
slot: slotValue,
8085
});
@@ -87,11 +92,11 @@ function updateSlotUI() {
8792
const slotSelect = document.getElementById('slotSelect');
8893
slotSelect.value = state.selectedSlot;
8994

90-
// Disable v2-only slots in dropdown
95+
// Disable v2-only slots in dropdown (skip Auto option with value -1)
9196
for (let i = 0; i < slotSelect.options.length; i++) {
9297
const option = slotSelect.options[i];
9398
const slotId = parseInt(option.value);
94-
option.disabled = slotId >= maxSlots;
99+
option.disabled = slotId >= 0 && slotId >= maxSlots;
95100
}
96101
}
97102

Tools/WebServer/static/js/core/state.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ let logPollInterval = null;
1515
let autoInjectPollInterval = null;
1616
let lastAutoInjectStatus = 'idle';
1717
let autoInjectProgressHideTimer = null;
18-
let selectedSlot = 0;
18+
let selectedSlot = -1;
1919
let fpbVersion = 1; // 1=FPB v1 (6 slots), 2=FPB v2 (8 slots)
2020
let slotStates = Array(8)
2121
.fill()

Tools/WebServer/static/js/features/patch.js

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -142,15 +142,15 @@ static ${funcName}_fn_t const ${macroName} = (${funcName}_fn_t)(${origAddr} | 1)
142142
if (returnType === 'void') {
143143
callOrigSection = `
144144
/* Disable patch -> call original -> re-enable patch */
145-
fpb_enable_patch(${slot}, false);
145+
fpb_enable_patch(FPB_PATCH_COMP_ID, false);
146146
ORIG_${funcName.toUpperCase()}(${argList});
147-
fpb_enable_patch(${slot}, true);`;
147+
fpb_enable_patch(FPB_PATCH_COMP_ID, true);`;
148148
} else {
149149
callOrigSection = `
150150
/* Disable patch -> call original -> re-enable patch */
151-
fpb_enable_patch(${slot}, false);
151+
fpb_enable_patch(FPB_PATCH_COMP_ID, false);
152152
${returnType} result = ORIG_${funcName.toUpperCase()}(${argList});
153-
fpb_enable_patch(${slot}, true);
153+
fpb_enable_patch(FPB_PATCH_COMP_ID, true);
154154
155155
return result;`;
156156
}
@@ -450,7 +450,10 @@ async function performInject() {
450450
return;
451451
}
452452

453-
if (state.slotStates[state.selectedSlot].occupied) {
453+
if (
454+
state.selectedSlot >= 0 &&
455+
state.slotStates[state.selectedSlot].occupied
456+
) {
454457
const slotFunc = state.slotStates[state.selectedSlot].func;
455458
const overwrite = confirm(
456459
`⚠️ ${t('messages.slot_occupied_by', 'Slot {{slot}} is already occupied by "{{func}}".', { slot: state.selectedSlot, func: slotFunc })}\n\n` +

Tools/WebServer/static/js/locales/en.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,7 @@ window.i18nResources['en'] = {
397397
clear_all: 'Clear All',
398398
reinject: 'Re-inject',
399399
slot_n: 'Slot {{n}}',
400+
slot_auto: 'Auto',
400401
fpb_v2_only: 'FPB v2 only',
401402
fpb_v2_required: 'This slot requires FPB v2 hardware',
402403
bytes: 'Bytes',

Tools/WebServer/static/js/locales/zh-CN.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,7 @@ window.i18nResources['zh-CN'] = {
389389
clear_all: '清除所有',
390390
reinject: '重新注入',
391391
slot_n: '槽位 {{n}}',
392+
slot_auto: '自动',
392393
fpb_v2_only: '仅 FPB v2',
393394
fpb_v2_required: '此补丁需要 FPB v2 硬件',
394395
bytes: '字节',

Tools/WebServer/static/js/locales/zh-TW.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,7 @@ window.i18nResources['zh-TW'] = {
390390
clear_all: '清除所有',
391391
reinject: '重新注入',
392392
slot_n: '槽位 {{n}}',
393+
slot_auto: '自動',
393394
fpb_v2_only: '僅 FPB v2',
394395
fpb_v2_required: '此補丁需要 FPB v2 硬體',
395396
bytes: '位元組',

Tools/WebServer/templates/partials/editor.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
<div class="flex-col" style="gap: 2px">
99
<label style="font-size: 0.7rem; opacity: 0.7" data-i18n="editor.slot">SLOT</label>
1010
<select id="slotSelect" class="vscode-select" style="width: 80px" onchange="onSlotSelectChange()">
11+
<option value="-1" selected data-i18n="device.slot_auto">Auto</option>
1112
<option value="0" data-i18n="device.slot_n" data-i18n-options='{"n": 0}'>Slot 0</option>
1213
<option value="1" data-i18n="device.slot_n" data-i18n-options='{"n": 1}'>Slot 1</option>
1314
<option value="2" data-i18n="device.slot_n" data-i18n-options='{"n": 2}'>Slot 2</option>

0 commit comments

Comments
 (0)