# JLink MCU 固件动态调试实战教程

> 基于 HC32A4A8 (Cortex-M4) + SEGGER J-Link V9 + macOS 的真机调试经验总结

---

## 一、环境准备

### 1.1 硬件连接

| 设备 | 连接方式 | 备注 |
|---|---|---|
| J-Link V9 | USB -> Mac | 确认驱动已识别 |
| HC32A4A8 | SWD 接口 | VCC/GND/SWDIO/SWCLK/RESET |
| 目标板 | 上电 | VTref 应在 3.3V 左右 |

**验证连接：**
```bash
# 查看 J-Link 是否被系统识别
lsusb | grep -i segger
# 或直接运行 JLinkExe 看是否能连上
/Applications/SEGGER/JLink_V942/JLinkExe
```

### 1.2 软件环境

```bash
# 1. 安装 SEGGER J-Link Software (官网下载)
# 2. 配置设备支持 (JLinkDevices.xml + 芯片库)
# 3. 确认 JLinkExe 路径
which JLinkExe
# 输出示例: /Applications/SEGGER/JLink_V942/JLinkExe

# 4. 安装 Python 依赖
pip3 install pexpect
```

---

## 二、JLink Commander 基础命令

### 2.1 连接目标

启动 JLinkExe 后，按顺序输入：

```
device HC32A4A8xI      # 选择目标芯片
si SWD                  # 选择 SWD 接口
speed 4000              # 设置接口速度 4MHz
connect                 # 连接目标
```

成功连接后输出示例：
```
Found SW-DP with ID 0x2BA01477
Found Cortex-M4 r0p1, Little endian.
Cortex-M4 identified.
```

### 2.2 核心命令速查表

| 命令 | 全称 | 功能 | 示例 |
|---|---|---|---|
| `h` | halt | 停止目标运行 | `h` |
| `g` | go | 恢复目标运行 | `g` |
| `s` | step | **单步执行一条指令** | `s` |
| `regs` | registers | 读取所有寄存器 | `regs` |
| `mem` | memory | 读取内存（字节） | `mem 0x20000000,32` |
| `mem16` | memory16 | 读取内存（半字） | `mem16 0x0001A350,4` |
| `mem32` | memory32 | 读取内存（字） | `mem32 0x1FFE999C,1` |
| `w1` | write8 | 写入 1 字节 | `w1 0x20000000, 0xAA` |
| `w2` | write16 | 写入 2 字节 | `w2 0x0001A466, 0xE7FF` |
| `w4` | write32 | 写入 4 字节 | `w4 0x20000000, 0x12345678` |
| `SetBP` | Set Breakpoint | **设置硬件断点** | `SetBP 0x0001A466` |
| `ClrBP` | Clear Breakpoint | 清除硬件断点 | `ClrBP 1` |
| `BP` | Breakpoint | **设置软件断点** | `BP 0x0001A350` |
| `r` | reset | 复位目标 | `r` |
| `q` | quit | 退出 JLinkExe | `q` |

### 2.3 重要陷阱：单步命令

```
❌ si  = SelectInterface （不是单步！）
✅ s   = Step （单步执行）
✅ step = Step （同上）
```

**血的教训：** 在 JLink Commander 中输入 `si` 会切换接口模式（SWD/JTAG），而不是单步执行。真正的单步命令是 `s`。

---

## 三、硬件断点 vs 软件断点

### 3.1 核心区别

| 特性 | 硬件断点 (SetBP) | 软件断点 (BP) |
|---|---|---|
| 实现原理 | FPB 单元比较地址 | 替换指令为 `BKPT` |
| 数量限制 | 6 个 (Cortex-M4) | 无限制 |
| 对 Flash 影响 | 无 | 修改 Flash 内容 |
| 适用范围 | 所有内存区域 | 仅可写内存（Flash/RAM） |
| 设置命令 | `SetBP 0xADDR` | `BP 0xADDR` |
| 清除命令 | `ClrBP <Handle>` | `BP`（清除所有） |

### 3.2 实战选择建议

```python
# 场景1：调试 Flash 中的代码（推荐硬件断点）
SetBP 0x0001A466        # 在 0x81 处理入口设置硬件断点

# 场景2：需要超过 6 个断点（使用软件断点）
BP 0x0001A350
BP 0x0001A372
BP 0x0001A37C

# 场景3：调试 RAM 中的代码（两者皆可）
SetBP 0x20001000
BP 0x20001000
```

### 3.3 验证断点是否生效

```bash
# 方法1：读取机器码确认软件断点
mem16 0x0001A350,1
# 如果设置了软件断点，应显示: 0001A350 = BE00 (BKPT 指令)

# 方法2：运行后观察是否停止
g
# 如果断点命中，JLink 会自动 halt 并显示寄存器
```

---

## 四、Python 自动化调试脚本

### 4.1 基础模板

```python
#!/usr/bin/env python3
import pexpect
import sys

JLINK_EXE = "/Applications/SEGGER/JLink_V942/JLinkExe"

def send_cmd(child, cmd, timeout=10):
    """发送命令并等待 J-Link> 提示符"""
    child.sendline(cmd)
    child.expect(r'J-Link>', timeout=timeout)
    return child.before

def main():
    # 启动 JLinkExe
    child = pexpect.spawn(JLINK_EXE, encoding='utf-8', timeout=30)
    child.logfile_read = sys.stdout  # 打印所有交互输出

    # 连接目标
    child.expect(r'J-Link>', timeout=15)
    send_cmd(child, "device HC32A4A8xI")
    send_cmd(child, "si SWD")
    send_cmd(child, "speed 4000")
    child.sendline("connect")
    child.expect([r'Cortex-M4 identified', r'J-Link>'], timeout=30)
    child.expect(r'J-Link>', timeout=10)

    # 读取内存
    send_cmd(child, "h")
    send_cmd(child, "mem32 0x1FFE999C,1")
    send_cmd(child, "regs")

    # 退出
    child.sendline("q")
    child.expect(pexpect.EOF, timeout=5)

if __name__ == "__main__":
    main()
```

### 4.2 寄存器解析

```python
import re

def parse_reg(regs_output, reg_name):
    """从 regs 输出中解析指定寄存器值"""
    for line in regs_output.splitlines():
        if line.startswith(reg_name + ' ='):
            val = line.split('=')[1].strip().split(',')[0]
            return int(val, 16)
    return None

def parse_pc(regs_output):
    return parse_reg(regs_output, 'PC')

# 使用示例
regs = send_cmd(child, "regs", timeout=5)
pc = parse_pc(regs)
r0 = parse_reg(regs, 'R0')
r4 = parse_reg(regs, 'R4')
print(f"PC=0x{pc:08X} R0=0x{r0:08X} R4=0x{r4:08X}")
```

### 4.3 单步追踪模板

```python
def step_trace(child, start_addr, end_addrs, max_steps=100):
    """从当前位置单步执行，记录关键节点"""
    steps = 0
    while steps < max_steps:
        send_cmd(child, "s")  # 注意：是 s 不是 si！
        regs = send_cmd(child, "regs", timeout=3)
        pc = parse_pc(regs)
        r0 = parse_reg(regs, 'R0')
        steps += 1

        marker = ""
        if pc in end_addrs:
            marker = " <-- 到达目标地址"
        print(f"Step {steps:3d}: PC=0x{pc:08X} R0=0x{r0:08X}{marker}")

        if pc in end_addrs:
            break
    return steps

# 使用：追踪 0x81 处理函数直到返回
step_trace(child, 0x1A350, [0x1A462, 0x1A6D2])
```

---

## 五、高级调试技巧

### 5.1 断点 + 车机测试联动

```python
import subprocess
import time

def debug_with_dut(child):
    # 1. 设置断点
    send_cmd(child, "SetBP 0x0001A466")
    send_cmd(child, "g")

    # 2. 启动车机测试程序（非阻塞）
    adb_proc = subprocess.Popen(
        ["adb", "shell", "su", "0", "sh", "-c",
         "cd /data/local/tmp && ./mcu_upgrade_bug_repro"],
        stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True
    )

    # 3. 等待断点命中（或超时）
    try:
        child.expect(r'(Breakpoint|Halt| halted)', timeout=60)
        print("[!] 断点命中！")
        regs = send_cmd(child, "regs", timeout=5)
        # 分析寄存器...
    except pexpect.exceptions.TIMEOUT:
        print("[!] 超时：断点未命中")

    # 4. 恢复并清理
    send_cmd(child, "ClrBP *", timeout=3)
    send_cmd(child, "g", timeout=3)
```

### 5.2 运行时内存快照

```python
def snapshot_ram(child, addr, size, filename):
    """导出指定内存区域到文件"""
    send_cmd(child, f"savebin {filename} 0x{addr:08X} 0x{size:X}", timeout=30)
    print(f"[+] RAM 已导出: {filename}")

# 示例：导出 128KB RAM
snapshot_ram(child, 0x20000000, 0x20000, "/tmp/mcu_ram_dump.bin")
```

### 5.3 Patch 固件验证假设

```python
def patch_and_verify(child, addr, orig_code, patch_code):
    """临时 Patch 固件，验证修改效果"""
    send_cmd(child, "h")

    # 读取原值确认
    orig = send_cmd(child, f"mem16 0x{addr:08X},1")
    print(f"原值: {orig.strip()}")

    # Patch
    send_cmd(child, f"w2 0x{addr:08X}, 0x{patch_code:04X}")
    patched = send_cmd(child, f"mem16 0x{addr:08X},1")
    print(f"Patch 后: {patched.strip()}")

    # 运行目标验证
    send_cmd(child, "g")
    time.sleep(30)

    # 恢复原始机器码
    send_cmd(child, "h")
    send_cmd(child, f"w2 0x{addr:08X}, 0x{orig_code:04X}")
    restored = send_cmd(child, f"mem16 0x{addr:08X},1")
    print(f"恢复后: {restored.strip()}")

# 示例：将 cbz 改为无条件跳转
patch_and_verify(child, 0x0001A466, 0xB360, 0xE7FF)
```

---

## 六、常见问题与解决方案

### Q1: 断点设置后始终不命中

**排查步骤：**
1. 确认地址正确（Thumb 模式不需要 +1）
2. 尝试软件断点 `BP` 替代硬件断点 `SetBP`
3. 确认目标确实执行到了该地址（通过 `h` 后检查 PC）
4. 检查是否是中断/异常处理函数（断点应在主代码中）

### Q2: `si` 命令导致接口切换

**解决：** JLink Commander 中 `si` = SelectInterface，**不是单步**。使用 `s` 或 `step`。

### Q3: `ClrBP *` 报错 Syntax Error

**解决：** JLink Commander 不支持 `ClrBP *`。需要逐个清除：`ClrBP 1`, `ClrBP 2`... 或使用 `BP` 命令清除软件断点。

### Q4: pexpect 脚本卡住

**原因：** `child.expect(r'J-Link>')` 在某些输出模式下可能不匹配。
**解决：** 增加超时，或在关键步骤后添加 `time.sleep(0.5)`。

### Q5: 如何确定 Flash 基址

**方法：**
```bash
# 1. 查看启动时的 PC 值
h
# PC 通常在复位向量附近，如 0x00000200

# 2. bin 文件偏移 = Flash 地址（对于从 0 启动的 MCU）
# 如 bin offset 0x01A350 = Flash addr 0x0001A350
```

---

## 七、实战案例：追踪函数调用链

### 场景
MCU 收到 0x81 命令后无响应，需要定位处理函数入口。

### 步骤

```python
# 1. 在可能的分发函数设置多个断点
bps = [0x1A350, 0x1A372, 0x1A37C, 0x1A3A0, 0x1A466, 0x1A46E]
for addr in bps:
    send_cmd(child, f"SetBP 0x{addr:08X}")

# 2. 运行并触发测试
send_cmd(child, "g")
# ... 运行车机测试 ...

# 3. 如果断点未命中，改用主动 halt + 单步
send_cmd(child, "h")
regs = send_cmd(child, "regs", timeout=5)
pc = parse_pc(regs)

if 0x1A350 <= pc <= 0x1A6D4:
    print(f"[!] PC 在目标函数内: 0x{pc:08X}")
    # 单步追踪
    step_trace(child, pc, [0x1A462, 0x1A6D2])
else:
    print(f"[i] PC=0x{pc:08X}，不在目标函数内")
    # 分析 LR 找到调用者
    lr = parse_reg(regs, 'R14(LR)')
    print(f"调用者返回地址: 0x{lr:08X}")
```

---

## 八、参考资源

- [SEGGER J-Link User Guide](https://www.segger.com/downloads/jlink/UM08001)
- [ARM Cortex-M4 FPB 文档](https://developer.arm.com/documentation/100166/0001)
- JLink Commander 内置帮助：`?`

---

*本教程基于 HC32A4A8 + J-Link V9 + macOS 的实战经验编写，适用于类似的 Cortex-M 系列 MCU 调试。*
