diff --git a/.gitignore b/.gitignore index 62da2bd..e9d6d8b 100644 --- a/.gitignore +++ b/.gitignore @@ -14,5 +14,14 @@ venv/ .ruff_cache/ htmlcov/ .coverage -debugforge.toml .claude/ + +# Sensitive config (contains credentials) +debugforge.toml +*.env +.env.* + +# IDE +.idea/ +.vscode/ +*.swp diff --git a/README.md b/README.md index cb08184..324b530 100644 --- a/README.md +++ b/README.md @@ -1,513 +1,190 @@ -

- DebugForge Logo -

+# DebugForge -

DebugForge

+将 TRACE32 接入 AI Agent,实现自动化调试、Debug 定位的 MCP Server 工具。支持本地和远程环境。 -

- Bridge Lauterbach TRACE32 to AI Agents via MCP -

- -

- CI - License: MIT - Python 3.10+ - MCP -

- -

- DebugForge is an MCP server that connects Lauterbach TRACE32 debuggers to AI agents.
- It gives Claude Code, Codex, Qwen and other AI agents direct access to your hardware debugger
- — enabling them to autonomously read target state, locate software bugs, and drive the fix-debug cycle end-to-end. -

- ---- - -## Highlights - -- **59 MCP Tools** — Full TRACE32 access for AI agents: execution control, breakpoints, memory, registers, variables, symbols, build, and more -- **Autonomous Debug Loop** — AI agents can build, flash, debug, locate root causes, fix code, and re-verify — the full edit-compile-debug cycle -- **Debug Skills** — Accumulate reusable debugging knowledge; AI agents learn from past issues and apply proven strategies -- **Project-Aware** — Configure once via `debugforge.toml`, your AI agent automatically knows your ELF paths, flash scripts, toolchain, and TRACE32 setup -- **Real Hardware** — Battle-tested on TC397 TriCore via USB. Your AI agent controls actual silicon, not a simulator -- **Advanced Breakpoints** — Conditional, data watchpoints, hit-count, task-specific, action triggers, and temporary breakpoints -- **Deep Inspection** — AI agents can read call stacks with locals, expand structs, view disassembly, inspect peripherals -- **Any MCP Agent** — Works with Claude Code, Codex CLI, Qwen, or any MCP-compatible AI assistant -- **Zero Lock-in** — MIT licensed, open source, no vendor dependencies beyond TRACE32 itself - -## Architecture +## 架构 ``` -┌─────────────────┐ MCP (stdio) ┌──────────────┐ PYRCL/TCP ┌──────────────┐ -│ │◄───────────────────────────►│ │◄─────────────────────►│ │ -│ AI Agent │ JSON-RPC tool calls │ DebugForge │ Remote Control API │ TRACE32 │ -│ (Claude Code, │ (59 debugging tools) │ MCP Server │ (lauterbach-trace32 │ PowerView │ -│ Codex, etc.) │ │ │ -rcl) │ │ -│ │◄───────────────────────────►│ │◄─────────────────────►│ │ -└─────────────────┘ Results └──────────────┘ Hardware └──────┬───────┘ - │ - ┌──────▼───────┐ - │ Target MCU │ - │ (e.g. TC397) │ - └──────────────┘ +┌─────────────────────────────────────────────────────────────┐ +│ 本地 Linux (Agent) │ +│ - 源码/ELF/CMM │ +│ - AI Agent (PYRCL 客户端) │ +│ - DebugForge MCP Server │ +└────────────────┬────────────────────────┬───────────────────┘ + │ SCP (端口 22) │ PYRCL (端口 20000) + │ 传输 ELF/CMM │ 远程控制 TRACE32 + ▼ ▼ +┌─────────────────────────────────────────────────────────────┐ +│ 远程 Windows (your-windows-host) │ +│ - OpenSSH Server │ +│ - TRACE32 │ +│ - 硬件板子 (TC38x/TC39x) │ +└─────────────────────────────────────────────────────────────┘ ``` -## Quick Start - -```bash -# 1. Install DebugForge -pip install debugforge - -# 2. Install TRACE32 Python package (from your TRACE32 installation) -pip install /demo/api/python/rcl/dist/lauterbach_trace32_rcl-*.whl - -# 3. Add to your AI agent's MCP config (e.g. .claude/settings.json) -``` - -```json -{ - "mcpServers": { - "debugforge": { - "command": "debugforge" - } - } -} -``` - -```bash -# 4. Start TRACE32 with API port enabled, then ask your AI agent: -# "Connect to TRACE32, load the firmware, find why the system crashes at boot" -``` - -## Installation - -### Prerequisites - -| Requirement | Details | -|-------------|---------| -| Python | 3.10 or higher | -| TRACE32 | PowerView running with Remote API enabled | -| PYRCL | `lauterbach-trace32-rcl` package from your TRACE32 installation | +## 快速开始 -### Step 1: Install DebugForge +### 1. 安装 ```bash -pip install debugforge -``` - -Or install from source: - -```bash -git clone https://github.com/YangPan2020/debugforge.git -cd debugforge pip install -e . ``` -### Step 2: Install TRACE32 Python Library +### 2. 配置 -The PYRCL wheel is bundled with your TRACE32 installation: +复制配置模板并填入实际信息: ```bash -pip install /demo/api/python/rcl/dist/lauterbach_trace32_rcl-*.whl -``` - -> Replace `` with your TRACE32 installation directory (e.g., `/opt/t32`, `C:\T32`). - -### Step 3: Enable TRACE32 Remote API - -Add these lines to your TRACE32 configuration file (`.t32` or `config.t32`): - -``` -RCL=NETTCP -PORT=20000 -``` - -Then restart TRACE32 PowerView. - -## Configuration - -### MCP Client Setup - -All MCP-compatible AI agents use the same configuration — register `debugforge` as an MCP server: - -```json -{ - "mcpServers": { - "debugforge": { - "command": "debugforge" - } - } -} -``` - -| Agent | Config File | Notes | -|-------|-------------|-------| -| Claude Code | `.claude/settings.json` | Also supports `env` field for auto-connect | -| Gemini CLI | `.gemini/settings.json` | | -| GitHub Copilot | `.vscode/mcp.json` | VS Code extension | -| Antigravity CLI | MCP config | | -| Codex CLI | MCP config | | -| DeepSeek | MCP config | | -| Hermes Agent | MCP config | | - -**Auto-connect example** (skip manual `connect()` call): - -```json -{ - "mcpServers": { - "debugforge": { - "command": "debugforge", - "env": { - "T32_AUTO_CONNECT": "true", - "T32_PORT": "20000" - } - } - } -} +cp debugforge.toml.example debugforge.toml ``` -### Project Configuration (`debugforge.toml`) - -Create a `debugforge.toml` in your project root to tell your AI agent about your debugging environment: +编辑 `debugforge.toml`: ```toml -[trace32] -install_path = "/opt/t32" # TRACE32 installation directory +[mode] +mode = "remote" # 或 "local" [connection] -node = "localhost" # TRACE32 host address -port = 20000 # API port (match your .t32 config) -protocol = "TCP" # TCP or UDP -auto_connect = true # Connect when server starts - -[project] -elf = "output/build/firmware.elf" # ELF file with debug symbols -map = "output/build/firmware.map" # MAP file (optional) +node = "192.168.1.100" # TRACE32 所在主机 IP +port = 20000 -[toolchain] -compiler_path = "/opt/gcc-arm/bin" # Compiler toolchain path -objdump = "arm-none-eabi-objdump" # Disassembly tool -readelf = "arm-none-eabi-readelf" # ELF analysis tool -nm = "arm-none-eabi-nm" # Symbol table tool +[remote] +host = "192.168.1.100" +winrm_user = "user@domain.local" +winrm_password = "your_password" +ssh_user = "username" +staging_dir = "D:\\T32\\debugforge" -[build] -command = "make -j8" # Build command -clean_command = "make clean" # Clean command -working_dir = "." # Build working directory +[project] +elf = "/path/to/your/firmware.elf" [scripts] -flash = "tools/Trace32/flash.cmm" # Flash programming script -init = "tools/Trace32/startup.cmm" # Target init script - -[debug] -skills_dir = ".debugforge/skills" # Debug knowledge base directory +flash = "/path/to/your/flash_script.cmm" ``` -All relative paths are resolved from the directory containing `debugforge.toml`. - -See [`debugforge.example.toml`](debugforge.example.toml) for a complete template. - -### Environment Variables - -Environment variables override `debugforge.toml` values (highest priority): - -| Variable | Default | Description | -|----------|---------|-------------| -| `T32_INSTALL_PATH` | — | TRACE32 installation directory | -| `T32_NODE` | `localhost` | TRACE32 host address | -| `T32_PORT` | `20000` | TRACE32 API port | -| `T32_PROTOCOL` | `TCP` | Communication protocol (TCP/UDP) | -| `T32_AUTO_CONNECT` | `false` | Auto-connect on server start | -| `DEBUGFORGE_CONFIG` | `./debugforge.toml` | Path to config file | - -**Priority:** Environment Variables > `debugforge.toml` > Built-in Defaults - -## Available Tools (59) - -### Connection & Configuration - -| Tool | Description | -|------|-------------| -| `connect` | Connect to a TRACE32 PowerView instance | -| `disconnect` | Disconnect from TRACE32 | -| `status` | Get connection status, TRACE32 version, and system state | -| `get_project_config` | Get the loaded project configuration (paths, scripts, settings) | - -### Execution Control - -| Tool | Description | -|------|-------------| -| `go` | Start/continue program execution | -| `step` | Single-step execution (into, over, or out) | -| `halt` | Stop program execution | -| `reset` | Reset the target CPU | -| `get_state` | Get current CPU execution state | -| `go_till` | Run until a specific address (temporary breakpoint) | -| `go_up` | Run until current function returns to caller | -| `go_return` | Run to the return instruction of current function | - -### Breakpoints - -| Tool | Description | -|------|-------------| -| `set_breakpoint` | Set a program/read/write/readwrite breakpoint | -| `list_breakpoints` | List all active breakpoints | -| `delete_breakpoint` | Delete a breakpoint at a specific address/symbol | -| `clear_all_breakpoints` | Delete all breakpoints at once | -| `toggle_breakpoint` | Enable/disable a breakpoint without deleting | - -### Advanced Breakpoints +> **注意**: `debugforge.toml` 包含敏感凭据信息,已被 `.gitignore` 排除,不会被提交到仓库。 -| Tool | Description | -|------|-------------| -| `set_conditional_breakpoint` | Breakpoint with HLL condition (e.g., `i > 100`) | -| `set_data_breakpoint` | Trigger on memory access with optional value match | -| `set_count_breakpoint` | Stop after N-th hit (loop debugging) | -| `set_task_breakpoint` | Trigger only for a specific OS task/thread | -| `set_action_breakpoint` | Execute a TRACE32 command on hit | -| `set_temporary_breakpoint` | Auto-deletes after first hit | +### 3. 配置远程 Windows -### Memory +参考 [WINDOWS_SSH_SETUP.md](WINDOWS_SSH_SETUP.md) 安装 OpenSSH Server。 -| Tool | Description | -|------|-------------| -| `read_memory` | Read target memory (hex dump format) | -| `write_memory` | Write data to target memory | -| `data_set` | Write a value to a memory address using Data.Set | +### 4. 运行 MCP Server -### Registers +```bash +debugforge +``` -| Tool | Description | -|------|-------------| -| `read_register` | Read a single CPU register | -| `read_registers` | Read multiple registers at once | -| `write_register` | Write a value to a CPU register | +或通过 examples 中的脚本直接测试: -### Variables +```bash +python examples/remote_debug.py +python examples/remote_debug.py --pyrcl +``` -| Tool | Description | -|------|-------------| -| `read_variable` | Read a C/C++ variable by symbol name | -| `write_variable` | Write a value to a C/C++ variable | -| `var_view` | View a variable/struct/array with full expansion | -| `var_set` | Set a C/C++ variable to a new value | +## 工作流 -### Symbols +### Remote 模式 -| Tool | Description | -|------|-------------| -| `symbol_by_name` | Look up symbol address by name | -| `symbol_by_address` | Look up symbol name by address | +1. **文件传输**: SCP/WinRM 传输 ELF + CMM 到 Windows staging 目录 +2. **CMM Wrapper**: 生成 wrapper CMM,将 ELF 路径替换为 Windows 路径 +3. **PYRCL 连接**: 通过 TCP 连接远程 TRACE32 +4. **执行 CMM**: 远程 TRACE32 执行 CMM 烧录 ELF +5. **源码映射**: 设置 `Symbol.SourcePATH.Translate` 让 TRACE32 找到源码 -### Commands & Scripting +### Local 模式 -| Tool | Description | -|------|-------------| -| `execute_command` | Execute any TRACE32 PRACTICE command | -| `run_practice` | Run a PRACTICE (.cmm) script with timeout | -| `evaluate` | Evaluate a TRACE32 expression or function | - -### Debug Views - -| Tool | Description | -|------|-------------| -| `get_callstack` | Get call stack with function names | -| `get_locals` | Get call stack with all local variables per frame | -| `get_data_dump` | Formatted memory dump (hex + ASCII) | -| `get_register_view` | Full register view with all flags | -| `get_disassembly` | Disassembly listing at address or current PC | -| `get_source_listing` | Source code around current execution point | -| `get_window` | Get text content of any TRACE32 window command | - -### System & OS Awareness - -| Tool | Description | -|------|-------------| -| `get_task_list` | List OS tasks/threads | -| `get_task_stack` | Get stack info for OS tasks | -| `get_peripheral_view` | View peripheral register contents | - -### Build & Binary Analysis - -| Tool | Description | -|------|-------------| -| `build_project` | Execute the configured build command | -| `clean_project` | Execute the configured clean command | -| `disassemble` | Disassemble a function or address range (objdump) | -| `analyze_map` | Analyze MAP file for memory layout and symbols | -| `analyze_elf` | Analyze ELF file structure (readelf) | - -### Workflow Orchestration - -| Tool | Description | -|------|-------------| -| `flash_and_run` | Flash firmware → reset → run to breakpoint | -| `build_flash_run` | Build → flash → reset → run (full edit-compile-debug loop) | - -### Debug Skills (Knowledge Base) - -| Tool | Description | -|------|-------------| -| `list_debug_skills` | List all debug skills in the knowledge base | -| `get_debug_skill` | Get full content of a specific debug skill | -| `search_debug_skills` | Search skills by keywords matching symptoms | -| `save_debug_skill` | Save a new debug skill from a successful debug session | - -## Usage Examples +1. **PYRCL 连接**: 连接本地 TRACE32 +2. **执行 CMM**: 直接执行本地 CMM 脚本 -### Basic Debug Session +## 配置详解 -``` -You: "Connect to TRACE32 and help me find why the system crashes after boot" - -AI Agent workflow: - 1. get_project_config() → learns your ELF path and scripts - 2. connect() → connects to TRACE32 - 3. execute_command("SYStem.Up") → powers up the target - 4. execute_command("Data.LOAD.Elf /path/to/firmware.elf") - 5. set_breakpoint("main") - 6. go() → runs to main - 7. step("over") → steps through code - 8. get_callstack() → analyzes the call stack - 9. read_variable("error_code") → checks variables - → "Found it: error_code = -1 because init_hardware() fails at line 84" -``` +参考 `debugforge.toml.example` 获取完整配置项说明。 -### Flash and Debug +### 模式选择 -``` -You: "Flash the new firmware and verify it boots correctly" - -AI Agent workflow: - 1. get_project_config() → gets flash script path - 2. connect() - 3. run_practice("tools/flash.cmm") → flashes firmware - 4. reset() - 5. set_breakpoint("main") - 6. go() - 7. get_state() → confirms target stopped at main - → "Firmware flashed and verified — target stopped at main() successfully" +```toml +[mode] +mode = "remote" # "local" 或 "remote" ``` -### Advanced Debugging +### 远程连接 -``` -You: "The buffer overflow happens somewhere in process_packet(). Set a watchpoint." - -AI Agent workflow: - 1. symbol_by_name("rx_buffer") → gets buffer address - 2. set_data_breakpoint(address="0xD0001000", access="write", size=256) - 3. go() - 4. get_callstack() → sees who wrote beyond the buffer - 5. get_disassembly() → examines the offending instruction - 6. read_memory("0xD0001000", 512) → shows the corrupted data - → "Overflow at process_packet+0x4C: memcpy writes 320 bytes into 256-byte buffer" +```toml +[remote] +host = "192.168.1.100" # 远程 Windows IP +winrm_port = 5985 # WinRM 端口 +winrm_user = "user@domain" # WinRM 用户名 +winrm_password = "password" # WinRM 密码 +ssh_user = "username" # SSH 用户名 +ssh_password = "" # SSH 密码 (留空使用 key) +ssh_port = 22 # SSH 端口 +staging_dir = "D:\\T32\\debugforge" # 文件传输目标目录 ``` -### Autonomous Fix Cycle +### 源码路径映射 +```toml +[[remote.source_translates]] +from = "/home/user/project" +to = "D:\\project" ``` -You: "The watchdog is resetting the MCU. Find and fix the root cause." - -AI Agent workflow: - 1. search_debug_skills("watchdog timeout reset") → finds matching skill - 2. get_debug_skill("watchdog-timeout") → reads proven strategy - 3. build_flash_run() → build + flash + run to main - 4. set_conditional_breakpoint("wdt_reset_handler", condition="reset_count > 0") - 5. go() → runs until watchdog fires - 6. get_callstack() → finds stuck function - 7. get_locals() → sees infinite loop condition - ... fixes code ... - 8. build_flash_run() → rebuild, reflash, verify - 9. save_debug_skill(...) → captures new knowledge - → "Fixed: infinite loop in spi_wait_ready() when peripheral clock is disabled" -``` - -## Supported AI Agents - -| Agent | Status | Configuration | -|-------|--------|---------------| -| [Claude Code](https://claude.com/claude-code) | ✅ Tested | `.claude/settings.json` | -| [Gemini CLI](https://github.com/google-gemini/gemini-cli) | ✅ Compatible | `.gemini/settings.json` | -| [GitHub Copilot](https://github.com/features/copilot) | ✅ Compatible | `.vscode/mcp.json` | -| [Codex CLI](https://github.com/openai/codex) | ✅ Compatible | MCP stdio transport | -| [Antigravity CLI](https://github.com/antigravity-official/antigravity-cli) | ✅ Compatible | MCP stdio transport | -| [DeepSeek](https://www.deepseek.com/) | ✅ Compatible | MCP stdio transport | -| [Hermes Agent](https://github.com/hermes-agent/hermes) | ✅ Compatible | MCP stdio transport | -| [Qwen Agent](https://github.com/QwenLM/Qwen-Agent) | ✅ Compatible | MCP stdio transport | -| Any MCP Client | ✅ Compatible | Standard MCP protocol | -## Development +让 TRACE32 在 debug 时能找到源码文件。需要先将源码复制到 Windows 对应目录。 -### Setup +## 依赖 ```bash -git clone https://github.com/YangPan2020/debugforge.git -cd debugforge -pip install -e ".[dev]" +pip install paramiko scp pywinrm ``` -### Run Tests +## 文件说明 -```bash -pytest tests/ -v +``` +debugforge/ +├── src/debugforge/ # MCP Server 核心代码 +├── examples/ # 示例脚本 +│ ├── remote_debug.py # 远程调试示例 (WinRM + PYRCL) +│ ├── remote_debug_tc38x.py # TC38x 远程调试示例 +│ ├── test_all_tools.py # 全工具测试 +│ └── debug_tc397_live.py # TC397 完整调试工作流 +├── debugforge.toml.example # 配置模板 +├── WINDOWS_SSH_SETUP.md # Windows OpenSSH 配置指南 +└── README.md # 本文档 ``` -### Code Style +## 故障排查 -This project uses [Ruff](https://docs.astral.sh/ruff/) for linting and formatting: +### SSH 连接失败 -```bash -ruff check src/ tests/ -ruff format src/ tests/ +``` +[FATAL] 连接失败: SSH connection failed ``` -### Contributing - -See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines. - -## Autonomous Debug Workflow +- 检查 Windows OpenSSH Server 是否安装并运行 +- 检查防火墙是否允许端口 22 +- 测试: `ssh @` -DebugForge supports a full autonomous debug loop. Include [`debugforge-workflow.md`](debugforge-workflow.md) in your AI agent's instructions to enable: +### PYRCL 连接失败 ``` -Problem Description → Understand Code → Build → Flash → Debug → Locate Bug → Fix → Rebuild → Verify → Done +[FATAL] 连接失败: Connection refused ``` -**Phases:** -1. **Understand** — Parse symptoms, search debug skills for matching patterns -2. **Analyze** — Read source, inspect MAP/ELF, disassemble critical functions -3. **Debug on Hardware** — Build/flash/run, set breakpoints, inspect state -4. **Fix & Verify** — Modify code, rebuild, reflash, confirm fix -5. **Capture Knowledge** — Save new debug patterns as reusable skills - -### Debug Skills +- 检查远程 TRACE32 是否启动 +- 检查 TRACE32 配置: `RCL=NETTCP PORT=20000` +- 检查防火墙是否允许端口 20000 +- 测试: `telnet 20000` -Debug skills are reusable Markdown files in `.debugforge/skills/` that capture proven debugging strategies: +### CMM 执行失败 -```bash -.debugforge/skills/ -├── stack-overflow.md -├── hardfault-analysis.md -└── peripheral-init-failure.md +``` +CMM 异常: ... ``` -AI agents search these automatically when debugging, and save new skills after resolving novel issues. - -## Roadmap - -- [ ] HTTP/SSE transport for remote debugging -- [ ] Auto-start/manage TRACE32 lifecycle -- [ ] Multi-core debugging (multiple simultaneous connections) -- [ ] Trace data analysis and visualization -- [ ] Flash programming tools (dedicated, beyond script execution) +- 检查 ELF 文件是否成功传输到 staging_dir +- 检查 CMM 中的路径是否正确 +- 查看远程 TRACE32 窗口错误信息 ## License -[MIT](LICENSE) — free for personal and commercial use. - ---- - -

- Built with ❤️ for the embedded debugging community -

+MIT diff --git a/WINDOWS_SSH_SETUP.md b/WINDOWS_SSH_SETUP.md new file mode 100644 index 0000000..5cc6141 --- /dev/null +++ b/WINDOWS_SSH_SETUP.md @@ -0,0 +1,112 @@ +# Windows OpenSSH 配置指南 + +## 1. 安装 OpenSSH Server + +### Windows 10/11 (1809+) + +1. 打开 **设置** → **应用** → **可选功能** +2. 点击 **添加功能** +3. 搜索 **OpenSSH 服务器**,安装 + +### 或使用 PowerShell (管理员) + +```powershell +# 检查是否已安装 +Get-WindowsCapability -Online | Where-Object Name -like 'OpenSSH.Server*' + +# 安装 +Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0 + +# 启动服务 +Start-Service sshd + +# 设置为自动启动 +Set-Service -Name sshd -StartupType Automatic + +# 配置防火墙 +New-NetFirewallRule -Name sshd -DisplayName 'OpenSSH Server (sshd)' -Enabled True -Direction Inbound -Protocol TCP -Action Allow -LocalPort 22 +``` + +## 2. 配置 SSH + +编辑 `C:\ProgramData\ssh\sshd_config`: + +```powershell +notepad C:\ProgramData\ssh\sshd_config +``` + +确保以下配置存在且未注释: + +``` +Port 22 +PubkeyAuthentication yes +PasswordAuthentication yes +``` + +重启服务: + +```powershell +Restart-Service sshd +``` + +## 3. 测试连接 + +从 Linux 测试: + +```bash +ssh @ +``` + +如果成功,说明 OpenSSH Server 已就绪。 + +## 4. 配置 SSH Key (推荐) + +### 在 Linux 生成密钥 + +```bash +ssh-keygen -t rsa -b 4096 +``` + +### 复制公钥到 Windows + +```bash +ssh-copy-id @ +``` + +或手动复制: + +```bash +cat ~/.ssh/id_rsa.pub | ssh @ "mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys" +``` + +### Windows 端权限设置 + +```powershell +# 修复 authorized_keys 权限 +icacls C:\Users\\.ssh\authorized_keys /inheritance:r /grant ":(R)" /grant "SYSTEM:(R)" +``` + +## 5. 验证 SCP + +```bash +scp test.txt @:D:\\T32\\debugforge\\ +``` + +如果成功,说明文件传输已就绪。 + +## 6. 常见问题 + +### 连接超时 + +- 检查防火墙是否允许端口 22 +- 检查 sshd 服务是否运行: `Get-Service sshd` + +### 权限拒绝 + +- 检查 `authorized_keys` 文件权限 +- 检查 `sshd_config` 中 `PubkeyAuthentication yes` + +### 中文用户名/密码 + +- 确保 Windows 用户名是英文 (如 `username`) +- 密码建议使用英文字符 diff --git a/debugforge.toml.example b/debugforge.toml.example new file mode 100644 index 0000000..97f6565 --- /dev/null +++ b/debugforge.toml.example @@ -0,0 +1,50 @@ +# DebugForge Configuration Template +# Copy this file to debugforge.toml and fill in your actual values. +# debugforge.toml is gitignored and will NOT be committed. + +[mode] +mode = "remote" # "local" or "remote" + +[trace32] +install_path = "C:\\T32" + +[connection] +node = "localhost" # TRACE32 host (use remote.host for remote mode) +port = 20000 # TRACE32 RCL port +protocol = "TCP" +auto_connect = false + +[remote] +host = "192.168.1.100" # Remote Windows IP +winrm_port = 5985 # WinRM port +winrm_user = "user@domain.local" # WinRM username (domain account) +winrm_password = "your_password" # WinRM password +winrm_transport = "ntlm" # WinRM transport: ntlm, kerberos, basic +ssh_user = "username" # SSH username for file transfer +ssh_password = "" # SSH password (leave empty for key auth) +ssh_port = 22 # SSH port +staging_dir = "C:\\T32" # Remote staging directory + +[[remote.source_translates]] +from = "/home/user/project" +to = "D:\\project" + +[project] +elf = "/path/to/your/firmware.elf" + +[scripts] +flash = "/path/to/your/flash_script.cmm" + +[toolchain] +compiler_path = "" +objdump = "tricore-elf-objdump" +readelf = "tricore-elf-readelf" +nm = "tricore-elf-nm" + +[build] +command = "make -j8" +clean_command = "make clean" +working_dir = "" + +[debug] +skills_dir = "" diff --git a/examples/debug_tc397_live.py b/examples/debug_tc397_live.py new file mode 100644 index 0000000..0c7d4d2 --- /dev/null +++ b/examples/debug_tc397_live.py @@ -0,0 +1,243 @@ +#!/usr/bin/env python3 +""" +Example: TC397 automated debug workflow (full pipeline) + +Demonstrates complete debug workflow: +1. Connect Trace32 via PYRCL +2. Flash download via CMM script +3. Set breakpoints and run +4. Inspect registers, variables, memory +5. Callstack and source analysis +6. OS awareness (NuttX tasks) +7. Step execution with variable tracking +8. Peripheral register read + +Usage: + 1. Copy debugforge.toml.example to debugforge.toml + 2. Configure connection and project paths + 3. Run: python examples/debug_tc397_live.py +""" + +from __future__ import annotations + +import os +import sys +import time +import traceback + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src')) + +from lauterbach.trace32.rcl import connect as t32_connect +from debugforge.config import load_config +from debugforge.state import state + + +def banner(title: str): + print(f"\n{'='*60}") + print(f" {title}") + print(f"{'='*60}") + + +def _read_window(dbg, command: str, max_size: int = 32768) -> str: + content = bytearray() + offset = 0 + while True: + try: + length, chunk = dbg._get_window_content(command, 4096, offset, "ASCII") + except Exception: + break + if length == 0 or not chunk: + break + content += chunk[:length] + offset += length + if offset >= max_size: + break + return content.decode("utf-8", errors="replace") + + +def cmd(dbg, c: str): + print(f" > {c}") + try: + dbg.fnc.error_occurred() + dbg.cmd(c) + print(f" OK") + except Exception as e: + print(f" [WARN] {e}") + + +def sym_addr(dbg, name: str) -> int | None: + try: + s = dbg.symbol.query_by_name(name) + if s.address and s.address.value is not None: + return s.address.value + except Exception: + pass + return None + + +def pc_to_func(dbg, pc: int) -> str: + try: + addr = dbg.address.from_string(f"0x{pc:08X}") + sym = dbg.symbol.query_by_address(addr) + return sym.name or "(unknown)" + except Exception: + return "(unknown)" + + +def phase1_connect(cfg): + banner("Phase 1: Connect Trace32") + print(f" Config: node={cfg.node}, port={cfg.port}, protocol={cfg.protocol}") + print(f" ELF: {cfg.elf}") + print(f" Flash script: {cfg.scripts.get('flash', '(none)')}") + + dbg = t32_connect(node=cfg.node, port=cfg.port, protocol=cfg.protocol) + ver = dbg.fnc.software_version() + print(f" Connected, version: {ver}") + + state.debugger = dbg + state.node = cfg.node + state.port = cfg.port + state.protocol = cfg.protocol + + return dbg + + +def phase2_flash(dbg, cfg): + banner("Phase 2: Flash Download & JTAG Connect") + + flash_script = cfg.scripts.get("flash", "") + elf_path = cfg.elf + + if not flash_script: + print(" [SKIP] No flash script configured, loading symbols only") + cmd(dbg, f'Data.LOAD.Elf "{elf_path}" /NoCODE') + return + + print(f" Flash script: {flash_script}") + print(f" ELF: {elf_path}") + + try: + dbg.cmm(f'"{flash_script}"', timeout=120) + print(f" Flash script executed") + except Exception as e: + print(f" [WARN] cmm() exception: {e}") + dbg.cmd(f'CD.DO "{flash_script}"') + time.sleep(15) + + print(f"\n Verifying symbols:") + found = 0 + for name in ["core_main", "nx_start", "core0_main", "rtfw_init"]: + addr = sym_addr(dbg, name) + if addr is not None: + print(f" {name:<40s} @ 0x{addr:08X}") + found += 1 + print(f" Found {found} key symbols") + + +def phase3_breakpoints_and_run(dbg, cfg): + banner("Phase 3: Set Breakpoints & Run") + + cmd(dbg, "Break.Delete /ALL") + + breakpoints = ["core_main", "core0_main", "nx_start"] + for bp_name in breakpoints: + addr = sym_addr(dbg, bp_name) + if addr is not None: + cmd(dbg, f"Break.Set {bp_name}") + else: + print(f" {bp_name}: symbol not found, skipping") + + print("\n Go (run)...") + cmd(dbg, "Go") + time.sleep(3) + + print(" Break (halt)...") + dbg.break_() + time.sleep(0.5) + + try: + pc = dbg.register.read_by_name("PC").value + func = pc_to_func(dbg, pc) + print(f" PC = 0x{pc:08X} -> {func}") + except Exception as e: + print(f" State: {e}") + + +def phase4_inspect_state(dbg): + banner("Phase 4: Inspect Runtime State") + + print("\n [4a] Core registers:") + for rname in ["PC", "SP", "A0", "A1", "D0", "D1", "D2", "D3"]: + try: + reg = dbg.register.read(rname) + val = reg.value + if isinstance(val, int): + print(f" {reg.name:<8s} = 0x{val:08X}") + else: + print(f" {reg.name:<8s} = {val}") + except Exception as e: + print(f" {rname:<8s} = [error: {e}]") + + print("\n [4b] Memory (CPU0 DSPR):") + try: + addr = dbg.address.from_string("D:0xD0000000") + data = dbg.memory.read(addr, length=64) + for offset in range(0, len(data), 16): + chunk = data[offset:offset+16] + hex_str = " ".join(f"{b:02X}" for b in chunk) + print(f" +{offset:04X}: {hex_str}") + except Exception as e: + print(f" [error: {e}]") + + +def phase5_step(dbg): + banner("Phase 5: Step Execution") + for i in range(5): + print(f"\n --- Step {i+1} ---") + try: + if dbg.fnc.state_run(): + dbg.break_() + time.sleep(0.3) + dbg.cmd("Step") + pc = dbg.register.read_by_name("PC").value + func = pc_to_func(dbg, pc) + print(f" PC = 0x{pc:08X} -> {func}") + except Exception as e: + print(f" [error: {e}]") + break + + +def main(): + cfg = load_config() + + print("=" * 60) + print(" DebugForge: TC397 Automated Debug Workflow") + print(" Target: Infineon AURIX TC397 (TriCore)") + print("=" * 60) + + try: + dbg = phase1_connect(cfg) + phase2_flash(dbg, cfg) + phase3_breakpoints_and_run(dbg, cfg) + phase4_inspect_state(dbg) + phase5_step(dbg) + + banner("Done") + cmd(dbg, "Break.Delete /ALL") + print(" All breakpoints cleared") + + except ConnectionError as e: + print(f"\n[FATAL] Connection failed: {e}") + print("Check:") + print(" 1. Trace32 PowerView is running") + print(" 2. config.t32 has RCL=NETTCP PORT=20000") + print(" 3. Target board connected via JTAG") + sys.exit(1) + except Exception as e: + print(f"\n[FATAL] Unexpected error: {e}") + traceback.print_exc() + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/examples/remote_debug.py b/examples/remote_debug.py new file mode 100644 index 0000000..7e643fd --- /dev/null +++ b/examples/remote_debug.py @@ -0,0 +1,127 @@ +#!/usr/bin/env python3 +""" +Example: Remote debug workflow via WinRM + PYRCL + +Demonstrates: +1. WinRM upload ELF + CMM to remote Windows +2. WinRM start Trace32 +3. PYRCL connect and execute debug commands + +Usage: + 1. Copy debugforge.toml.example to debugforge.toml + 2. Fill in your remote connection details + 3. Run: python examples/remote_debug.py +""" + +import os +import sys + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src')) + +from debugforge.config import load_config + + +def run_winrm_cmd(session, cmd: str, label: str = ""): + """Execute a PowerShell command and print results.""" + if label: + print(f"\n{label}") + + r = session.run_ps(cmd) + out = r.std_out.decode('utf-8', errors='replace').strip() + err = r.std_err.decode('utf-8', errors='replace').strip() + + if out: + print(f" stdout: {out}") + if err and "CLIXML" not in err: + print(f" stderr: {err[:500]}") + print(f" exit: {r.status_code}") + + return r.status_code == 0 + + +def main(): + import winrm + + cfg = load_config() + remote = cfg.remote + + if not remote.host: + print("Error: remote.host not configured in debugforge.toml") + sys.exit(1) + + elf_path = cfg.elf + flash_script = cfg.scripts.get("flash", "") + + if not elf_path or not os.path.exists(elf_path): + print(f"Error: ELF file not found: {elf_path}") + sys.exit(1) + + print("=" * 60) + print("DebugForge: Remote Debug via WinRM + PYRCL") + print("=" * 60) + print(f" Host: {remote.host}") + print(f" WinRM: {remote.winrm_url}") + print(f" Staging: {remote.staging_dir}") + + # Connect WinRM + print(f"\nConnecting WinRM: {remote.winrm_url}") + session = winrm.Session( + remote.winrm_url, + auth=(remote.winrm_user, remote.winrm_password), + transport=remote.winrm_transport, + ) + + # 1. Create remote directory + run_winrm_cmd( + session, + f'New-Item -ItemType Directory -Path "{remote.staging_dir}" -Force | Out-Null; Write-Output "Directory ready"', + "Create remote directory", + ) + + # 2. Start Trace32 + t32_cmd = r''' +$t32 = "C:\T32\bin\windows64\t32mtc.exe" +$config = "C:\T32\config.t32" +$proc = Get-Process t32mtc -ErrorAction SilentlyContinue +if ($proc) { + Write-Output "Trace32 already running (PID: $($proc.Id))" +} else { + Start-Process -FilePath $t32 -ArgumentList "-c", $config + Start-Sleep -Seconds 5 + Write-Output "Trace32 started" +} +''' + run_winrm_cmd(session, t32_cmd, "Check/Start Trace32") + + # 3. Verify Trace32 RCL port + run_winrm_cmd( + session, + f'Test-NetConnection -ComputerName {remote.host} -Port {cfg.port} -WarningAction SilentlyContinue | Select-Object TcpTestSucceeded', + "Verify Trace32 port", + ) + + print(f"\nTrace32 ready at {remote.host}:{cfg.port}") + + # 4. Optional: connect via PYRCL + if '--pyrcl' in sys.argv: + print(f"\nConnecting via PYRCL...") + from lauterbach.trace32.rcl import connect + dbg = connect(node=remote.host, port=str(cfg.port), protocol=cfg.protocol) + print(f"Connected! Version: {dbg.fnc.version_software()}") + + if flash_script: + remote_cmm = f"{remote.staging_dir}\\{os.path.basename(flash_script)}" + print(f"Executing CMM: {remote_cmm}") + dbg.cmd(f'DO "{remote_cmm}"') + + for translate in remote.source_translates: + src = translate.get("from", "") + dst = translate.get("to", "") + if src and dst: + dbg.cmd(f'SYmbol.SourcePATH.Translate "{src}" "{dst}"') + + print("Done!") + + +if __name__ == "__main__": + main() diff --git a/examples/remote_debug_tc38x.py b/examples/remote_debug_tc38x.py new file mode 100644 index 0000000..d8c7fd5 --- /dev/null +++ b/examples/remote_debug_tc38x.py @@ -0,0 +1,124 @@ +#!/usr/bin/env python3 +""" +Example: Remote debug TC38x via network drive + PYRCL + +Demonstrates direct file access via mapped network drive (Z:) and +safe Trace32 shutdown via QUIT command. + +Safety measures: +- Must execute dbg.cmd('QUIT') before exit to cleanly close Trace32 +- Force-killing the process can cause hardware errors +- All exit paths (normal/exception/signal) execute QUIT + +Usage: + 1. Copy debugforge.toml.example to debugforge.toml + 2. Fill in your connection details + 3. Run: python examples/remote_debug_tc38x.py +""" + +import os +import sys +import signal +import logging +import time + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src')) + +from lauterbach.trace32.rcl import connect +from debugforge.config import load_config + +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s [%(levelname)s] %(message)s', + datefmt='%H:%M:%S' +) + +dbg_connection = None + + +def safe_quit_trace32(dbg): + """Safely close Trace32 via QUIT command.""" + if not dbg: + return + try: + logging.info('[T32] Sending QUIT command to safely close Trace32 PowerView...') + dbg.cmm('QUIT') + logging.info('Trace32 closed safely') + time.sleep(2) + except Exception as e: + logging.error(f'Error closing Trace32: {e}') + + +def signal_handler(signum, frame): + sig_name = signal.Signals(signum).name + logging.warning(f'Received signal {sig_name}, shutting down Trace32...') + global dbg_connection + if dbg_connection: + safe_quit_trace32(dbg_connection) + sys.exit(128 + signum) + + +def main(): + global dbg_connection + + signal.signal(signal.SIGINT, signal_handler) + signal.signal(signal.SIGTERM, signal_handler) + + cfg = load_config() + remote = cfg.remote + host = remote.host or cfg.node + port = str(cfg.port) + + logging.info("=" * 70) + logging.info("Remote Debug TC38x") + logging.info(f" Host: {host}:{port}") + logging.info("=" * 70) + + dbg = None + try: + logging.info(f"[1/4] Connecting Trace32 ({host}:{port})...") + dbg = connect(node=host, port=port, protocol='TCP', timeout=120) + dbg_connection = dbg + logging.info("Connected") + + logging.info("[2/4] Verifying connection...") + version = dbg.fnc.version_software() + logging.info(f"Trace32 version: {version}") + + cmm_path = cfg.scripts.get("flash", "") + elf_path = cfg.elf + + if cmm_path: + logging.info(f"[3/4] Executing CMM: {cmm_path}") + logging.info(f" Loading ELF: {elf_path}") + result = dbg.cmm(cmm_path) + logging.info(f"CMM execution complete") + time.sleep(3) + + logging.info("[4/4] Verifying ELF load...") + try: + pc_value = dbg.register.read("PC") + logging.info(f"ELF loaded, PC: 0x{pc_value.value:08X}") + except Exception as e: + logging.warning(f"Cannot read PC: {e}") + + logging.info("Debug session complete, shutting down...") + + except ConnectionError as e: + logging.error(f"Connection failed: {e}") + logging.error("Check: 1) Trace32 running 2) RCL=NETTCP PORT=20000 3) Firewall") + return 1 + except Exception as e: + logging.error(f"Error: {e}") + import traceback + traceback.print_exc() + return 1 + finally: + safe_quit_trace32(dbg) + dbg_connection = None + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/examples/test_all_tools.py b/examples/test_all_tools.py new file mode 100644 index 0000000..d8bd1c7 --- /dev/null +++ b/examples/test_all_tools.py @@ -0,0 +1,248 @@ +#!/usr/bin/env python3 +""" +Example: Test all DebugForge debug tools via PYRCL + +Comprehensive test of all TRACE32 debug capabilities: +- Connection, execution, registers, memory +- Breakpoints, symbols, variables, commands +- Views, multicore, system config, trace +- Benchmark, notifications, watchpin, OS awareness + +Usage: + 1. Copy debugforge.toml.example to debugforge.toml + 2. Ensure Trace32 is running with RCL enabled + 3. Run: python examples/test_all_tools.py +""" + +import os +import sys +import time + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src')) + +from lauterbach.trace32.rcl import connect +from debugforge.config import load_config + +results = [] + + +def test(name, func): + print(f"\n[{name}]", flush=True) + try: + result = func() + print(f" PASS: {result}", flush=True) + results.append((name, "PASS", str(result)[:200])) + return result + except Exception as e: + print(f" FAIL: {type(e).__name__}: {e}", flush=True) + results.append((name, "FAIL", f"{type(e).__name__}: {e}")) + return None + + +def main(): + cfg = load_config() + host = cfg.remote.host or cfg.node + port = str(cfg.port) + + # ========== CONNECT ========== + print("=" * 70, flush=True) + print("Phase 1: Connect to Trace32", flush=True) + print("=" * 70, flush=True) + + print(f"Connecting to {host}:{port}...", flush=True) + dbg = connect(node=host, port=port, protocol=cfg.protocol, timeout=30) + print("Connected!", flush=True) + ver = dbg.fnc.version_software() + print(f" Version: {ver}", flush=True) + + # ========== LOAD ELF ========== + print("\n" + "=" * 70, flush=True) + print("Phase 2: Load ELF", flush=True) + print("=" * 70, flush=True) + + cmm_path = cfg.scripts.get("flash", "") + if cmm_path: + print(f"Executing: {cmm_path}", flush=True) + try: + dbg.cmm(cmm_path) + print("CMM executed", flush=True) + except Exception as e: + print(f"CMM error: {e}", flush=True) + print("Continuing anyway...", flush=True) + time.sleep(3) + else: + print("No flash script configured, skipping", flush=True) + + # ========== CONNECTION TOOLS ========== + print("\n" + "=" * 70, flush=True) + print("Phase 3: Test All Debug Tools", flush=True) + print("=" * 70, flush=True) + + print("\n--- Connection Tools ---", flush=True) + + def t_status(): + v = dbg.fnc.version_software() + cpu = dbg.fnc.system_cpu() + up = dbg.fnc.system_up() + return f"v={v}, cpu={cpu}, up={up}" + test("connection.status", t_status) + + # ========== EXECUTION TOOLS ========== + print("\n--- Execution Tools ---", flush=True) + + def t_halt(): + dbg.break_() + time.sleep(0.5) + halt = dbg.fnc.state_halt() + return f"Halted: {halt}" + test("execution.halt", t_halt) + + def t_state(): + halt = dbg.fnc.state_halt() + run = dbg.fnc.state_run() + target = dbg.fnc.state_target() + power = dbg.fnc.state_power() + return f"halt={halt}, run={run}, target={target}, power={power}" + test("execution.state", t_state) + + def t_step(): + dbg.step() + time.sleep(0.2) + pc = dbg.register.read("PC") + return f"Stepped to PC=0x{pc.value:X}" + test("execution.step", t_step) + + def t_go(): + dbg.go() + time.sleep(1) + run = dbg.fnc.state_run() + dbg.break_() + time.sleep(0.3) + return f"Running was: {run}" + test("execution.go", t_go) + + # ========== REGISTERS TOOLS ========== + print("\n--- Register Tools ---", flush=True) + + def t_reg_read(): + pc = dbg.register.read("PC") + sp = dbg.register.read("SP") + return f"PC=0x{pc.value:X}, SP=0x{sp.value:X}" + test("registers.read", t_reg_read) + + def t_reg_write(): + d0 = dbg.register.read("D0") + old = d0.value + dbg.register.write("D0", 0x12345678) + d0_new = dbg.register.read("D0") + dbg.register.write("D0", old) + return f"D0: 0x{old:X} -> 0x{d0_new.value:X} -> restored" + test("registers.write", t_reg_write) + + # ========== MEMORY TOOLS ========== + print("\n--- Memory Tools ---", flush=True) + + def t_mem_read(): + pc = dbg.register.read("PC") + data = dbg.memory.read(pc.value, 16) + hexs = " ".join(f"{b:02X}" for b in data) + return f"@0x{pc.value:X}: {hexs}" + test("memory.read", t_mem_read) + + def t_mem_read32(): + pc = dbg.register.read("PC") + val = dbg.memory.read_uint32(pc.value) + return f"@0x{pc.value:X}: uint32=0x{val:X}" + test("memory.read_uint32", t_mem_read32) + + # ========== BREAKPOINTS TOOLS ========== + print("\n--- Breakpoint Tools ---", flush=True) + + def t_bp_set(): + pc = dbg.register.read("PC") + addr = pc.value + 0x10 + dbg.breakpoint.set(addr) + bps = dbg.breakpoint.list() + return f"Set BP @0x{addr:X}, total={len(bps)}" + test("breakpoints.set", t_bp_set) + + def t_bp_list(): + bps = dbg.breakpoint.list() + if bps: + info = [(f"0x{bp.address:X}" if hasattr(bp, 'address') else str(bp)) for bp in bps[:5]] + return f"{len(bps)} BP(s): {info}" + return "0 breakpoints" + test("breakpoints.list", t_bp_list) + + def t_bp_delete(): + bps = dbg.breakpoint.list() + if bps: + dbg.cmd("BREAK.DELETE") + return "All breakpoints deleted" + return "No BP to delete" + test("breakpoints.delete", t_bp_delete) + + # ========== SYMBOLS TOOLS ========== + print("\n--- Symbol Tools ---", flush=True) + + def t_sym_query_name(): + syms = dbg.symbol.query_by_name("main") + if syms: + s = syms[0] + attrs = {a: getattr(s, a, '?') for a in ['name', 'address', 'size'] if hasattr(s, a)} + return f"'main' found: {attrs}" + return "No 'main' symbol" + test("symbols.query_by_name", t_sym_query_name) + + def t_sym_exist(): + exist = dbg.fnc.symbol_exist("main") + return f"symbol_exist('main') = {exist}" + test("symbols.exist", t_sym_exist) + + # ========== MULTICORE TOOLS ========== + print("\n--- Multicore Tools ---", flush=True) + + def t_mc_cpu(): + cpu = dbg.fnc.system_cpu() + return f"CPU: {cpu}" + test("multicore.cpu", t_mc_cpu) + + # ========== SYSTEM CONFIG TOOLS ========== + print("\n--- System Config Tools ---", flush=True) + + def t_sys_mode(): + mode = dbg.fnc.system_mode() + return f"Mode: {mode}" + test("system_config.mode", t_sys_mode) + + # ========== TRACE TOOLS ========== + print("\n--- Trace Tools ---", flush=True) + + def t_trace_state(): + state = dbg.fnc.trace_state() + return f"Trace state: {state}" + test("trace.state", t_trace_state) + + # ========== SUMMARY ========== + print("\n" + "=" * 70, flush=True) + print("TEST SUMMARY", flush=True) + print("=" * 70, flush=True) + + passed = sum(1 for _, s, _ in results if s == "PASS") + failed = sum(1 for _, s, _ in results if s == "FAIL") + + print(f"\nTotal: {len(results)} | Passed: {passed} | Failed: {failed}", flush=True) + + if failed > 0: + print(f"\nFailed tests:", flush=True) + for name, s, msg in results: + if s == "FAIL": + print(f" FAIL {name}: {msg}", flush=True) + + print("\n" + "=" * 70, flush=True) + print("DONE", flush=True) + print("=" * 70, flush=True) + + +if __name__ == "__main__": + main() diff --git a/remote_rcl_config.t32 b/remote_rcl_config.t32 new file mode 100644 index 0000000..ce04b4d --- /dev/null +++ b/remote_rcl_config.t32 @@ -0,0 +1,19 @@ +; ============================================================ +; remote_rcl_config.t32 +; 远程机器上的 TRACE32 配置文件 +; +; 使用方法 (在远程机器上): +; t32mtc -c /path/to/remote_rcl_config.t32 +; +; 或者追加到现有的 config.t32 文件中: +; 复制 RCL= 和 PORT= 部分到 config.t32 的空行之间 +; ============================================================ + +; 必须: RCL 前后各一个空行 + +RCL=NETTCP +PORT=20000 +PACKLEN=4096 + +; 允许多个客户端同时连接 (可选) +; RCLMULTIPLE=ENABLE diff --git a/setup_remote_t32.sh b/setup_remote_t32.sh new file mode 100755 index 0000000..4ad1cdb --- /dev/null +++ b/setup_remote_t32.sh @@ -0,0 +1,95 @@ +#!/bin/bash +# ============================================================ +# setup_remote_t32.sh +# 在远程机器上配置和启动 TRACE32 (RCL 模式) +# +# 使用方法: +# 1. 拷贝此脚本到远程机器 +# 2. ssh user@remote "bash setup_remote_t32.sh" +# 或直接 scp 后远程执行 +# +# 前置条件: +# - 远程机器已安装 TRACE32 (t32mtc 在 PATH 或 /opt/t32_*/) +# - JTAG 硬件已连接 +# ============================================================ + +set -e + +T32_INSTALL="${T32_INSTALL:-/opt/t32_202509}" +T32_BIN="${T32_INSTALL}/bin" +RCL_PORT="${RCL_PORT:-20000}" +WORK_DIR="/tmp/debugforge" + +echo "============================================" +echo " DebugForge: Remote TRACE32 Setup" +echo " T32 Install: ${T32_INSTALL}" +echo " RCL Port: ${RCL_PORT}" +echo "============================================" + +# 1. 查找 t32mtc +if [ -x "${T32_BIN}/t32mtc" ]; then + T32MT="${T32_BIN}/t32mtc" +elif command -v t32mtc &>/dev/null; then + T32MT=$(command -v t32mtc) +else + echo "[ERROR] t32mtc not found!" + echo " Tried: ${T32_BIN}/t32mtc" + echo " Set T32_INSTALL to your TRACE32 installation path" + exit 1 +fi +echo " t32mtc: ${T32MT}" + +# 2. 创建工作目录 +mkdir -p "${WORK_DIR}/elf" +mkdir -p "${WORK_DIR}/cmm" +mkdir -p "${WORK_DIR}/tmp" +echo " Work dir: ${WORK_DIR}" + +# 3. 生成 RCL 配置 +CONFIG_FILE="${WORK_DIR}/config_rcl.t32" +cat > "${CONFIG_FILE}" </dev/null; then + if ss -tlnp 2>/dev/null | grep -q ":${RCL_PORT} "; then + echo " [WARN] Port ${RCL_PORT} already in use!" + echo " Another TRACE32 instance may be running" + echo " Kill it first or use a different port" + fi +elif command -v netstat &>/dev/null; then + if netstat -tlnp 2>/dev/null | grep -q ":${RCL_PORT} "; then + echo " [WARN] Port ${RCL_PORT} already in use!" + fi +fi + +# 5. 启动 TRACE32 (后台模式) +echo "" +echo " Starting TRACE32 in background..." +echo " Command: ${T32MT} -c ${CONFIG_FILE}" +echo "" +echo " 如需前台运行 (看 GUI):" +echo " ${T32MT} -c ${CONFIG_FILE}" +echo "" +echo " 如需后台运行:" +echo " nohup ${T32MT} -c ${CONFIG_FILE} &" +echo "" +echo " 测试连接 (从本地):" +echo " python remote_debug.py --host $(hostname -I | awk '{print $1}')" +echo "" +echo " ✓ Setup complete!" +echo "============================================" diff --git a/src/debugforge/config.py b/src/debugforge/config.py index 08c7e10..1d5b665 100644 --- a/src/debugforge/config.py +++ b/src/debugforge/config.py @@ -16,6 +16,29 @@ tomllib = None +@dataclass +class RemoteConfig: + """Remote connection configuration (WinRM/SSH).""" + + host: str = "" + mode: str = "local" + winrm_port: int = 5985 + winrm_user: str = "" + winrm_password: str = "" + winrm_transport: str = "ntlm" + ssh_user: str = "" + ssh_password: str = "" + ssh_port: int = 22 + staging_dir: str = "" + source_translates: list[dict[str, str]] = field(default_factory=list) + + @property + def winrm_url(self) -> str: + if not self.host: + return "" + return f"http://{self.host}:{self.winrm_port}/wsman" + + @dataclass class DebugForgeConfig: """DebugForge project configuration.""" @@ -34,7 +57,7 @@ class DebugForgeConfig: map: str = "" # Scripts (resolved to absolute) - scripts: dict[str, str] = field(default_factory=dict) + scripts: dict[str, str | list[str]] = field(default_factory=dict) # Toolchain settings compiler_path: str = "" @@ -50,6 +73,9 @@ class DebugForgeConfig: # Debug settings skills_dir: str = "" + # Remote settings + remote: RemoteConfig = field(default_factory=RemoteConfig) + # Base directory for resolving relative paths _base_dir: str = field(default="", repr=False) @@ -128,7 +154,10 @@ def load_config(config_path: str | None = None) -> DebugForgeConfig: # [scripts] scripts_section = data.get("scripts", {}) for key, val in scripts_section.items(): - cfg.scripts[key] = _resolve_path(val, base_dir) + if isinstance(val, str): + cfg.scripts[key] = _resolve_path(val, base_dir) + elif isinstance(val, list): + cfg.scripts[key] = [_resolve_path(v, base_dir) for v in val if isinstance(v, str)] # [toolchain] tc_section = data.get("toolchain", {}) @@ -155,6 +184,37 @@ def load_config(config_path: str | None = None) -> DebugForgeConfig: if "skills_dir" in debug_section: cfg.skills_dir = _resolve_path(debug_section["skills_dir"], base_dir) + # [mode] + mode_section = data.get("mode", {}) + if "mode" in mode_section: + cfg.remote.mode = mode_section["mode"] + + # [remote] + remote_section = data.get("remote", {}) + if "host" in remote_section: + cfg.remote.host = remote_section["host"] + if "winrm_port" in remote_section: + cfg.remote.winrm_port = int(remote_section["winrm_port"]) + if "winrm_user" in remote_section: + cfg.remote.winrm_user = remote_section["winrm_user"] + if "winrm_password" in remote_section: + cfg.remote.winrm_password = remote_section["winrm_password"] + if "winrm_transport" in remote_section: + cfg.remote.winrm_transport = remote_section["winrm_transport"] + if "ssh_user" in remote_section: + cfg.remote.ssh_user = remote_section["ssh_user"] + if "ssh_password" in remote_section: + cfg.remote.ssh_password = remote_section["ssh_password"] + if "ssh_port" in remote_section: + cfg.remote.ssh_port = int(remote_section["ssh_port"]) + if "staging_dir" in remote_section: + cfg.remote.staging_dir = remote_section["staging_dir"] + if "port" in remote_section: + cfg.port = int(remote_section["port"]) + cfg.remote.host = cfg.remote.host or cfg.node + if "source_translates" in remote_section: + cfg.remote.source_translates = remote_section["source_translates"] + # Environment variable overrides (highest priority) env_install = os.environ.get("T32_INSTALL_PATH", "") if env_install: diff --git a/src/debugforge/server.py b/src/debugforge/server.py index a1d5c9b..3e1f1f9 100644 --- a/src/debugforge/server.py +++ b/src/debugforge/server.py @@ -46,6 +46,12 @@ async def lifespan(server: FastMCP): from debugforge.tools import build # noqa: E402, F401 from debugforge.tools import workflow # noqa: E402, F401 from debugforge.tools import skills # noqa: E402, F401 +from debugforge.tools import trace # noqa: E402, F401 +from debugforge.tools import multicore # noqa: E402, F401 +from debugforge.tools import benchmark # noqa: E402, F401 +from debugforge.tools import system_config # noqa: E402, F401 +from debugforge.tools import watchpin # noqa: E402, F401 +from debugforge.tools import notifications # noqa: E402, F401 def main(): diff --git a/src/debugforge/tools/benchmark.py b/src/debugforge/tools/benchmark.py new file mode 100644 index 0000000..9d2e9b4 --- /dev/null +++ b/src/debugforge/tools/benchmark.py @@ -0,0 +1,219 @@ +"""BenchMark Counter (BMC) performance analysis tools for TriCore. + +Provides access to on-chip hardware performance counters for profiling +cache hits/misses, instruction counts, branch predictions, and other +architectural events. + +Reference: debugger_tricore.pdf "CPU specific BMC Commands" section. +""" + +from __future__ import annotations + +from debugforge.server import mcp +from debugforge.state import state +from debugforge.tools.views import _read_window + + +@mcp.tool() +async def bmc_configure(counter: str, event: str) -> str: + """Configure a BenchMark Counter to count a specific hardware event. + + Maps a performance event to a counter slot. The counter accumulates + while the target runs. Read with bmc_read() after halting. + + Args: + counter: Counter slot — "M1CNT", "M2CNT", "M3CNT", "M4CNT", etc. + event: Hardware event to count. Common TriCore events: + "CYCLECOUNT" — CPU clock cycles + "INSTRUCTIONCOUNT" — instructions executed + "DATA_X_HIT" — data cache/buffer hits + "DATA_X_CLEAN" — data cache/buffer misses + "DATA_READ" — data read accesses + "DATA_WRITE" — data write accesses + "PROGRAM_X_HIT" — instruction cache hits + "PROGRAM_X_MISS" — instruction cache misses + "JUMP" — jump/branch instructions + "BRANCH_PREDICTED" — correctly predicted branches + "BRANCH_NOT_PREDICTED" — mispredicted branches + + Returns: + Confirmation of counter configuration + """ + dbg = state.require_connection() + try: + dbg.cmd(f"BMC.{counter}.EVENT {event}") + return f"BMC configured: {counter} → counting {event}" + except ConnectionError: + raise + except Exception as e: + return f"Error configuring BMC {counter}: {e}" + + +@mcp.tool() +async def bmc_read(counters: list[str] | None = None) -> str: + """Read current BenchMark Counter values. + + Target must be halted. Returns raw counter values since last reset. + + Args: + counters: List of counter names to read (e.g., ["M1CNT", "M2CNT"]). + If empty/None, reads all available counters. + + Returns: + Counter names and their current values + """ + dbg = state.require_connection() + try: + result = _read_window(dbg, "BMC.state") + if not result.strip(): + return "BMC state not available (counters may not be configured)" + return f"BMC State:\n{result}" + except ConnectionError: + raise + except Exception as e: + return f"Error reading BMC: {e}" + + +@mcp.tool() +async def bmc_reset() -> str: + """Reset all BenchMark Counters to zero. + + Clears all counter values for a fresh measurement. + + Returns: + Confirmation + """ + dbg = state.require_connection() + try: + # Reset each common counter + for cnt in ["M1CNT", "M2CNT", "M3CNT", "M4CNT"]: + try: + dbg.cmd(f"BMC.{cnt}.EVENT OFF") + except Exception: + pass + # Re-open state to clear + dbg.cmd("BMC.state") + return "All BMC counters reset" + except ConnectionError: + raise + except Exception as e: + return f"Error resetting BMC: {e}" + + +@mcp.tool() +async def bmc_cache_analysis( + function: str = "", + run_duration_ms: int = 1000, +) -> str: + """Run a quick cache performance analysis. + + Configures counters for cache hit/miss ratio, runs the target briefly, + then reports the results. Useful for identifying cache bottlenecks. + + Args: + function: Function to analyze (empty = current execution point) + run_duration_ms: How long to run in milliseconds (default: 1000) + + Returns: + Cache hit/miss statistics + """ + dbg = state.require_connection() + try: + results = ["Cache Performance Analysis:"] + + # Configure counters for cache analysis + dbg.cmd("BMC.M1CNT.EVENT DATA_X_HIT") + results.append(" M1CNT: Data cache hits") + dbg.cmd("BMC.M2CNT.EVENT DATA_X_CLEAN") + results.append(" M2CNT: Data cache misses") + dbg.cmd("BMC.M3CNT.EVENT PROGRAM_X_HIT") + results.append(" M3CNT: Instruction cache hits") + dbg.cmd("BMC.M4CNT.EVENT PROGRAM_X_MISS") + results.append(" M4CNT: Instruction cache misses") + + # Set a breakpoint if function specified + if function: + dbg.cmd(f"Break.Set {function} /Program") + dbg.cmd("Go") + results.append(f" Running to {function}...") + else: + dbg.cmd("Go") + results.append(" Running...") + + # Wait for the specified duration + import asyncio + await asyncio.sleep(run_duration_ms / 1000.0) + + # Halt and read + dbg.cmd("Break") + results.append(" Halted. Reading counters...") + + # Read BMC state + bmc_result = _read_window(dbg, "BMC.state") + if bmc_result.strip(): + results.append(f"\n{bmc_result}") + + return "\n".join(results) + except ConnectionError: + raise + except Exception as e: + return f"Error in cache analysis: {e}" + + +@mcp.tool() +async def bmc_set_atob(counter: str, enabled: bool = True) -> str: + """Enable or disable A-to-B mode on a BMC counter. + + In A-to-B mode, the counter only counts events between the Alpha + and Bravo marker breakpoints. Use set_breakpoint with actions + "alpha"/"bravo" to set markers. + + Args: + counter: Counter name (e.g., "M1CNT") + enabled: True to enable A-to-B mode, False to disable + + Returns: + Confirmation + """ + dbg = state.require_connection() + try: + state_str = "ON" if enabled else "OFF" + dbg.cmd(f"BMC.{counter}.ATOB {state_str}") + return f"BMC {counter} A-to-B mode: {state_str}" + except ConnectionError: + raise + except Exception as e: + return f"Error setting A-to-B mode: {e}" + + +@mcp.tool() +async def bmc_profile_chart(counters: list[str] | None = None) -> str: + """Display a BMC profile chart with counter data mapped to instruction flow. + + Shows which functions/addresses consumed the most events (cycles, + cache misses, etc.). Requires BMC counters to have been recorded + during execution. + + Args: + counters: Counter names to include (e.g., ["M1CNT", "M2CNT"]). + Empty = all configured counters. + + Returns: + Profile chart showing event distribution across code + """ + dbg = state.require_connection() + try: + if counters: + cnt_str = " ".join(counters) + cmd = f"SNOOPer.PROfileChart.COUNTER %Up {cnt_str}" + else: + cmd = "SNOOPer.PROfileChart.COUNTER %Up M1CNT M2CNT M3CNT" + + result = _read_window(dbg, cmd) + if not result.strip(): + return "No profile data available (record BMC data first using bmc_cache_analysis)" + return result + except ConnectionError: + raise + except Exception as e: + return f"Error getting BMC profile chart: {e}" diff --git a/src/debugforge/tools/breakpoints.py b/src/debugforge/tools/breakpoints.py index 47e8734..3c6eb2f 100644 --- a/src/debugforge/tools/breakpoints.py +++ b/src/debugforge/tools/breakpoints.py @@ -143,3 +143,22 @@ async def toggle_breakpoint(address: str, enabled: bool) -> str: except Exception as e: action = "enabling" if enabled else "disabling" return f"Error {action} breakpoint at {address}: {e}" + + +@mcp.tool() +async def get_breakpoint_count() -> str: + """Get the total number of breakpoints currently set. + + Returns: + Count of all breakpoints (enabled and disabled) + """ + dbg = state.require_connection() + try: + bps = dbg.breakpoint.list() + count = len(bps) if bps else 0 + enabled_count = sum(1 for bp in bps if bp.enabled) if bps else 0 + return f"Total breakpoints: {count} (enabled: {enabled_count}, disabled: {count - enabled_count})" + except ConnectionError: + raise + except Exception as e: + return f"Error getting breakpoint count: {e}" diff --git a/src/debugforge/tools/execution.py b/src/debugforge/tools/execution.py index 9ce1a9e..0ae49bc 100644 --- a/src/debugforge/tools/execution.py +++ b/src/debugforge/tools/execution.py @@ -1,9 +1,10 @@ -"""Execution control tools for TRACE32.""" +"""Execution control tools for TRACE32 — run, step, halt, reset, source location.""" from __future__ import annotations from debugforge.server import mcp from debugforge.state import state +from debugforge.tools.views import _read_window @mcp.tool() @@ -33,22 +34,35 @@ async def step(mode: str = "into") -> str: New program counter address after step """ dbg = state.require_connection() + mode_norm = mode.lower().strip() cmd_map = { "into": "Step", "over": "Step.Over", "out": "Step.Out", + "into_hll": "Step.Hll", + "over_hll": "Step.HllOver", + "next_line": "Step.HllOver", + "next_source_line": "Step.HllOver", + "next_assembly": "Step.Over", + "source_line": "Step.Hll", } - cmd = cmd_map.get(mode) + cmd = cmd_map.get(mode_norm) if cmd is None: - return f"Invalid step mode '{mode}'. Use: into, over, out" + return ( + f"Invalid step mode '{mode}'. Use: into, over, out, into_hll, " + "over_hll (step one source line), next_line, next_assembly, source_line" + ) try: dbg.cmd(cmd) try: pc = dbg.fnc.register_pc() - return f"Stepped ({mode}). PC = 0x{pc:08X}" + loc = _current_source_location(dbg) + if loc: + return f"Stepped ({mode_norm}). PC = 0x{pc:08X}\nSource: {loc}" + return f"Stepped ({mode_norm}). PC = 0x{pc:08X}" except Exception: - return f"Stepped ({mode}). Use 'read_register' to check PC." + return f"Stepped ({mode_norm}). Use 'read_register' to check PC." except Exception as e: return f"Error during step: {e}" @@ -110,3 +124,217 @@ async def get_state() -> str: return info except Exception as e: return f"Error getting state: {e}" + + +def _current_source_location(dbg) -> str: + """Return current source location as 'file:line (function)' or empty.""" + try: + result = dbg.eval('sYmbol.FUNCtion(PP())') + except Exception: + result = "" + try: + src = dbg.eval('sYmbol.SOURCE(PP())') + except Exception: + src = "" + if src: + loc = f"{src}" + if result: + loc += f" in {result}" + return loc + return result if result else "" + + +@mcp.tool() +async def get_source_location() -> str: + """Get the current source file and line of the halted CPU. + + Returns the HLL source location (file:line) and enclosing function name + for the current PC. The target must be halted and symbols must be loaded. + + Returns: + Formatted source location (file, line, function, module) + """ + dbg = state.require_connection() + try: + pc = dbg.fnc.register_pc() + loc = _current_source_location(dbg) + if loc: + return f"PC = 0x{pc:08X}\n{loc}" + return f"PC = 0x{pc:08X} (no source location available)" + except ConnectionError: + raise + except Exception as e: + return f"Error getting source location: {e}" + + +@mcp.tool() +async def step_mode_list() -> str: + """List all supported step modes and their behavior. + + Returns: + Table of step modes with descriptions + """ + rows = [ + ("into", "Step into function calls (assembly-level)"), + ("over", "Step over function calls (assembly-level)"), + ("out", "Step out of current function to caller"), + ("into_hll", "Step into function calls (source-line)"), + ("over_hll", "Step over one HLL source line (next source line)"), + ("next_line", "Alias for over_hll — next source line"), + ("next_assembly", "Alias for over — next assembly instruction"), + ("source_line", "Alias for into_hll — one source line"), + ] + lines = ["Supported step modes:", ""] + for name, desc in rows: + lines.append(f" {name:20s} {desc}") + return "\n".join(lines) + + +@mcp.tool() +async def get_current_function() -> str: + """Get the name of the function the CPU is currently executing. + + Returns: + Function name (or address if no debug symbols) + """ + dbg = state.require_connection() + try: + result = dbg.eval('sYmbol.FUNCtion(PP())') + if result: + return f"Current function: {result}" + pc = dbg.fnc.register_pc() + return f"Current function: unknown (no symbol at PC = 0x{pc:08X})" + except ConnectionError: + raise + except Exception as e: + return f"Error getting current function: {e}" + + +@mcp.tool() +async def run_to_line(file: str, line: int) -> str: + """Run until execution reaches a specific source file line. + + Sets a temporary breakpoint at the given source location and resumes + execution. The target stops when that line is reached. + + Args: + file: Source file name (can be partial, e.g., 'main.c' or full path) + line: Line number in the source file + + Returns: + Confirmation of temporary breakpoint + """ + dbg = state.require_connection() + try: + # TRACE32 syntax: Break.Set "file"\line /Program /Temp + escaped_file = file.replace('"', '""') + dbg.cmd(f'Break.Set "{escaped_file}"\\{line} /Program') + dbg.cmd("Go") + return f"Running to {file}:{line}..." + except ConnectionError: + raise + except Exception as e: + return f"Error setting run-to-line at {file}:{line}: {e}" + + +@mcp.tool() +async def get_run_stats() -> str: + """Get quick run/halt statistics for the target. + + Combines CPU state, current function, source location, and elapsed + runtime (if available) in one call. + + Returns: + Aggregated run statistics + """ + dbg = state.require_connection() + try: + parts = ["# Run Statistics", ""] + try: + run_state = dbg.fnc.state_run() + parts.append(f"Running: {run_state}") + except Exception as e: + parts.append(f"State check failed: {e}") + + try: + pc = dbg.fnc.register_pc() + parts.append(f"PC: 0x{pc:08X}") + except Exception: + pass + + loc = _current_source_location(dbg) + if loc: + parts.append(f"Location: {loc}") + + try: + sys_mode = dbg.fnc.system_mode() + parts.append(f"System mode: {sys_mode}") + except Exception: + pass + + return "\n".join(parts) + except ConnectionError: + raise + except Exception as e: + return f"Error getting run stats: {e}" + + +@mcp.tool() +async def get_practice_state() -> str: + """Get the state of any running PRACTICE (.cmm) script. + + Shows whether a PRACTICE script is active, which script is running, + and the current line being executed. + + Returns: + PRACTICE script state + """ + dbg = state.require_connection() + try: + result = _read_window(dbg, "PRactice.state") + if not result.strip(): + return "No PRACTICE script running" + return f"PRACTICE State:\n{result}" + except ConnectionError: + raise + except Exception as e: + return f"Error getting PRACTICE state: {e}" + + +@mcp.tool() +async def abort_practice() -> str: + """Abort the currently running PRACTICE script. + + Stops the active .cmm script execution immediately. + + Returns: + Confirmation + """ + dbg = state.require_connection() + try: + dbg.cmd("PRactice.ABORT") + return "PRACTICE script aborted" + except ConnectionError: + raise + except Exception as e: + return f"Error aborting PRACTICE script: {e}" + + +@mcp.tool() +async def get_message_line() -> str: + """Read the current TRACE32 message line content. + + The message line is the status bar at the bottom of the TRACE32 window + that displays status messages from commands and scripts. + + Returns: + Current message line text + """ + dbg = state.require_connection() + try: + result = dbg.eval('MSG.line()') + return f"Message line: '{result}'" if result else "Message line: (empty)" + except ConnectionError: + raise + except Exception as e: + return f"Error reading message line: {e}" diff --git a/src/debugforge/tools/memory.py b/src/debugforge/tools/memory.py index c15b9fe..416892f 100644 --- a/src/debugforge/tools/memory.py +++ b/src/debugforge/tools/memory.py @@ -80,3 +80,73 @@ async def write_memory( return f"Invalid hex data '{data}': {e}" except Exception as e: return f"Error writing memory at {address}: {e}" + + +@mcp.tool() +async def read_memory_cached( + address: str, + length: int, + width: int = 32, +) -> str: + """Read memory using cache-aware access (D: prefix). + + Shows data as the CPU sees it through its data cache, not the stale + value on the bus. Essential for debugging code that modifies cached + memory (e.g., shared variables in LMU on TC39x). + + Args: + address: Memory address as hex string (e.g., "0x80000000") + length: Number of bytes to read + width: Access width in bits — 8, 16, 32, or 64 (default: 32) + + Returns: + Hex dump of cached memory contents + """ + return await read_memory(address, length, width, access="D:") + + +@mcp.tool() +async def read_memory_physical( + address: str, + length: int, + width: int = 32, +) -> str: + """Read physical memory bypassing the CPU cache. + + Reads the actual value on the memory bus, which may differ from what + the CPU sees if the data cache has not been flushed. + + Args: + address: Memory address as hex string (e.g., "0x80000000") + length: Number of bytes to read + width: Access width in bits — 8, 16, 32, or 64 (default: 32) + + Returns: + Hex dump of physical memory contents + """ + return await read_memory(address, length, width, access="") + + +@mcp.tool() +async def list_access_classes() -> str: + """List all available memory access classes for TRACE32. + + Shows the different access class prefixes that can be used to read + memory through different paths (cache, bus, peripheral, etc.). + + Returns: + Table of access classes with descriptions + """ + rows = [ + ("(none)", "Default access — bus-level (may show stale cache data)"), + ("D:", "Data access — shows CPU's view through data cache"), + ("P:", "Program access — instruction memory space"), + ("DC:", "Data cache direct — read data cache contents"), + ("IC:", "Instruction cache direct — read I-cache contents"), + ("PER:", "Peripheral access — SFR/peripheral registers"), + ("DUALPORT:", "Dual-port RAM access (if supported)"), + ] + lines = ["Memory Access Classes:", ""] + for prefix, desc in rows: + lines.append(f" {prefix:12s} {desc}") + return "\n".join(lines) diff --git a/src/debugforge/tools/multicore.py b/src/debugforge/tools/multicore.py new file mode 100644 index 0000000..bf2bc4d --- /dev/null +++ b/src/debugforge/tools/multicore.py @@ -0,0 +1,208 @@ +"""Multicore debugging tools for TriCore AURIX — SMP, AMP, iAMP. + +Covers core selection, multicore configuration, cross-core synchronization, +and chip stepping detection. + +Reference: debugger_tricore.pdf "Multicore Debugging" section. +""" + +from __future__ import annotations + +from debugforge.server import mcp +from debugforge.state import state +from debugforge.tools.views import _read_window + + +@mcp.tool() +async def select_core(core: int) -> str: + """Select the active CPU core for debugging. + + In multicore setups, switches the debugger focus to the specified core. + Subsequent commands (breakpoints, register reads, etc.) target this core. + + Args: + core: Core number (0-based, e.g., 0, 1, 2 for TC397) + + Returns: + Confirmation with current core state + """ + dbg = state.require_connection() + try: + dbg.cmd(f"SYStem.CPU {core}.") + # Query current state + run_state = dbg.fnc.state_run() + if run_state: + return f"Core {core} selected — Running" + else: + try: + pc = dbg.fnc.register_pc() + return f"Core {core} selected — Stopped at PC = 0x{pc:08X}" + except Exception: + return f"Core {core} selected — Stopped" + except ConnectionError: + raise + except Exception as e: + return f"Error selecting core {core}: {e}" + + +@mcp.tool() +async def get_chip_info() -> str: + """Get the detected chip/device information. + + Returns chip stepping, device variant, and CPU configuration. + Useful for verifying the correct CPU is selected. + + Returns: + Chip stepping, CPU type, and stepping information + """ + dbg = state.require_connection() + try: + result = _read_window(dbg, "SYStem.CPU") + info_parts = ["Chip Information:"] + + # Try to get chip stepping via PRACTICE function + try: + stepping = dbg.eval('CHIP.STEPping()') + info_parts.append(f" Chip Stepping: {stepping}") + except Exception: + pass + + # Get system mode + try: + sys_mode = dbg.fnc.system_mode() + info_parts.append(f" System Mode: {sys_mode}") + except Exception: + pass + + if result.strip(): + info_parts.append(f"\n CPU Configuration:\n{result}") + + return "\n".join(info_parts) + except ConnectionError: + raise + except Exception as e: + return f"Error getting chip info: {e}" + + +@mcp.tool() +async def configure_multicore(core_count: int) -> str: + """Configure multicore topology for AURIX devices. + + Mounts multiple cores into one chip for shared resources (trace, MCDS). + Must be called before SYStem.Up in multicore scenarios. + + Based on SYStem.CONFIG.CORE command from debugger_tricore.pdf: + SYStem.CONFIG.CORE maps each core to a chip position. + + Args: + core_count: Number of cores to configure (e.g., 6 for TC397, 3 for TC377) + + Returns: + Configuration results per core + """ + dbg = state.require_connection() + try: + results = [] + for i in range(core_count): + core_id = i + 1 # SYStem.CONFIG.CORE uses 1-based numbering + chip_pos = 1 # All cores in same chip + dbg.cmd(f"SYStem.CONFIG.CORE {core_id}. {chip_pos}.") + results.append(f" Core {i} → Chip position {chip_pos}, core slot {core_id}") + + return f"Multicore configured ({core_count} cores):\n" + "\n".join(results) + except ConnectionError: + raise + except Exception as e: + return f"Error configuring multicore: {e}" + + +@mcp.tool() +async def sync_cores(action: str = "go") -> str: + """Synchronize Go/Step/Break across multiple cores. + + Uses the SYnch command to coordinate execution state between cores + in SMP or synchronized AMP mode. + + Args: + action: Synchronization action: + "go" — run all synchronized cores + "break" — halt all synchronized cores + "step" — single-step all synchronized cores + + Returns: + Confirmation of sync operation + """ + dbg = state.require_connection() + try: + cmd_map = { + "go": "SYnch.Go", + "break": "SYnch.Break", + "step": "SYnch.Step", + } + cmd = cmd_map.get(action) + if not cmd: + return f"Invalid sync action '{action}'. Use: go, break, step" + + dbg.cmd(cmd) + return f"Synchronized {action} across all cores" + except ConnectionError: + raise + except Exception as e: + return f"Error syncing cores ({action}): {e}" + + +@mcp.tool() +async def get_all_cores_state() -> str: + """Get the execution state of all CPU cores. + + Queries each core's running/stopped status and PC value. + Useful in multicore debugging to understand the global system state. + + Returns: + Table showing state of each core + """ + dbg = state.require_connection() + try: + result = _read_window(dbg, "SYStem.CONFIG.state") + if result.strip(): + return f"System Configuration:\n{result}" + + # Fallback: try to get state of current core + run_state = dbg.fnc.state_run() + if run_state: + return "Current core: Running" + else: + try: + pc = dbg.fnc.register_pc() + return f"Current core: Stopped at PC = 0x{pc:08X}" + except Exception: + return "Current core: Stopped" + except ConnectionError: + raise + except Exception as e: + return f"Error getting core states: {e}" + + +@mcp.tool() +async def detect_cpu() -> str: + """Auto-detect the connected AURIX device. + + Uses SYStem.DETECT.CPU to identify the connected chip automatically. + Works with TC27x, TC37x, TC39x and other AURIX families. + + Returns: + Detected CPU type and stepping + """ + dbg = state.require_connection() + try: + dbg.cmd("SYStem.DETECT.CPU") + # Try to read chip stepping + try: + stepping = dbg.eval('CHIP.STEPping()') + return f"Auto-detected CPU: {stepping}" + except Exception: + return "CPU auto-detection executed (use get_chip_info for details)" + except ConnectionError: + raise + except Exception as e: + return f"Error detecting CPU: {e}" diff --git a/src/debugforge/tools/notifications.py b/src/debugforge/tools/notifications.py new file mode 100644 index 0000000..7645982 --- /dev/null +++ b/src/debugforge/tools/notifications.py @@ -0,0 +1,175 @@ +"""Event notification and message line tools for TRACE32. + +Provides tools for TRACE32 message line (MSG), event notifications, and +communication between PRACTICE scripts and the debugger. + +Reference: debugger_tricore.pdf "Event Notifications", "MSG command" sections. +""" + +from __future__ import annotations + +from debugforge.server import mcp +from debugforge.state import state + + +@mcp.tool() +async def send_message(text: str, level: str = "info") -> str: + """Display a message in the TRACE32 message line (MSG). + + Messages appear in the TRACE32 GUI status bar and can be used for + debugging status, progress updates, or user notifications. + + Args: + text: Message text to display + level: Message level — "info", "warning", "error" + + Returns: + Confirmation + """ + dbg = state.require_connection() + try: + level_map = { + "info": "MSG", + "warning": "MSG !", + "error": "MSG !!", + } + cmd = level_map.get(level, "MSG") + dbg.cmd(f'{cmd} "{text}"') + return f"Message displayed: [{level}] {text}" + except ConnectionError: + raise + except Exception as e: + return f"Error sending message: {e}" + + +@mcp.tool() +async def clear_message() -> str: + """Clear the TRACE32 message line. + + Removes any displayed message from the status bar. + + Returns: + Confirmation + """ + dbg = state.require_connection() + try: + dbg.cmd("MSG") + return "Message line cleared" + except ConnectionError: + raise + except Exception as e: + return f"Error clearing message: {e}" + + +@mcp.tool() +async def enable_event_notifications(events: list[str] | None = None) -> str: + """Enable event notifications for debugger state changes. + + When enabled, TRACE32 can send notifications to external tools or scripts + when specific events occur (e.g., target halts, breakpoints hit). + + Args: + events: List of event types to monitor: + "break" — target halted + "go" — target running + "reset" — system reset detected + "power" — power cycle detected + Default: ["break", "go"] + + Returns: + Confirmation of enabled events + """ + dbg = state.require_connection() + try: + evt_list = events or ["break", "go"] + results = [] + + for evt in evt_list: + evt_map = { + "break": "ENDBREAK", + "go": "ENDGO", + "reset": "ENDRESET", + "power": "ENDPOWER", + } + evt_code = evt_map.get(evt) + if evt_code: + dbg.cmd(f"EVENT.ON {evt_code}") + results.append(f" Enabled: {evt}") + else: + results.append(f" Warning: unknown event '{evt}' (skipped)") + + return f"Event notifications configured:\n" + "\n".join(results) + except ConnectionError: + raise + except Exception as e: + return f"Error enabling event notifications: {e}" + + +@mcp.tool() +async def set_event_action(event: str, command: str) -> str: + """Set a command to execute when a specific event occurs. + + Allows automated responses to debugger events (e.g., run a script + when target halts, collect trace data on breakpoint hit). + + Args: + event: Event type — "break", "go", "reset", "power" + command: TRACE32 command to execute (e.g., "DO my_script.cmm", + "Var.view myVar", "Trace.List") + + Returns: + Confirmation + """ + dbg = state.require_connection() + try: + evt_map = { + "break": "ENDBREAK", + "go": "ENDGO", + "reset": "ENDRESET", + "power": "ENDPOWER", + } + evt_code = evt_map.get(event) + if not evt_code: + return f"Invalid event '{event}'. Use: break, go, reset, power" + + escaped_cmd = command.replace('"', '""') + dbg.cmd(f'EVENT.{evt_code} "{escaped_cmd}"') + return f"Event action set: {event} → {command}" + except ConnectionError: + raise + except Exception as e: + return f"Error setting event action: {e}" + + +@mcp.tool() +async def set_debugger_mode(mode: str) -> str: + """Set the debugger operational mode. + + Controls how TRACE32 responds to target state changes. + + Args: + mode: Debugger mode: + "attach" — attach to running target without resetting + "standby" — monitor power cycles, auto-reconnect + "no_standby" — disable power cycle monitoring + + Returns: + Confirmation + """ + dbg = state.require_connection() + try: + mode_map = { + "attach": "SYStem.Mode.Attach", + "standby": "SYStem.Mode.StandBy", + "no_standby": "SYStem.Mode.NoStandBy", + } + cmd = mode_map.get(mode) + if not cmd: + return f"Invalid mode '{mode}'. Use: attach, standby, no_standby" + + dbg.cmd(cmd) + return f"Debugger mode set to: {mode}" + except ConnectionError: + raise + except Exception as e: + return f"Error setting debugger mode: {e}" diff --git a/src/debugforge/tools/system_config.py b/src/debugforge/tools/system_config.py new file mode 100644 index 0000000..fa84385 --- /dev/null +++ b/src/debugforge/tools/system_config.py @@ -0,0 +1,262 @@ +"""System configuration tools for TriCore AURIX debugging. + +Covers reset behavior, cache read options, peripheral suspend, watchdog +handling, system options, and startup script execution. + +Reference: debugger_tricore.pdf "System Options", "Reset Behavior", +"Accessing Cached Memory", "Suspending Peripherals" sections. +""" + +from __future__ import annotations + +from debugforge.server import mcp +from debugforge.state import state +from debugforge.tools.views import _read_window + + +@mcp.tool() +async def set_reset_behavior(behavior: str) -> str: + """Configure reset behavior for the debugger. + + Determines how TRACE32 handles soft resets, hard resets, and power cycles. + + Args: + behavior: Reset behavior mode: + "restore_go" — halt briefly to restore debug resources, then continue + "run_restore" — restore debug resources while CPU runs (may miss breakpoints) + "halt" — halt CPU at reset vector after reset + + Returns: + Confirmation of reset behavior setting + """ + dbg = state.require_connection() + try: + behavior_map = { + "restore_go": "RestoreGo", + "run_restore": "RunRestore", + "halt": "Halt", + } + val = behavior_map.get(behavior) + if not val: + return f"Invalid behavior '{behavior}'. Use: restore_go, run_restore, halt" + + dbg.cmd(f"SYStem.Option.RESetBehavior {val}") + return f"Reset behavior set to: {val}" + except ConnectionError: + raise + except Exception as e: + return f"Error setting reset behavior: {e}" + + +@mcp.tool() +async def set_cache_read(enabled: bool = True) -> str: + """Enable or disable cache-aware memory reads. + + When ON, the default data access class (D:) shows cached data from the + CPU's point of view instead of stale bus-level data. + Essential for debugging code that modifies cached memory (e.g., LMU on TC39x). + + Args: + enabled: True to enable cache-aware reads, False to disable + + Returns: + Confirmation + """ + dbg = state.require_connection() + try: + state_str = "ON" if enabled else "OFF" + dbg.cmd(f"SYStem.Option.DCREAD {state_str}") + return f"Cache-aware reads (DCREAD): {state_str}" + except ConnectionError: + raise + except Exception as e: + return f"Error setting DCREAD: {e}" + + +@mcp.tool() +async def set_peripheral_suspend(enabled: bool = True) -> str: + """Enable or disable automatic peripheral suspend when CPU halts. + + When ON, peripheral modules (timers, watchdogs, communication) are + automatically suspended when the debugger halts the CPU. This prevents + watchdog timeouts and timer overflows during debugging. + + Args: + enabled: True to enable peripheral suspend, False to disable + + Returns: + Confirmation + """ + dbg = state.require_connection() + try: + state_str = "ON" if enabled else "OFF" + dbg.cmd(f"SYStem.Option.PERSTOP {state_str}") + return f"Peripheral suspend (PERSTOP): {state_str}" + except ConnectionError: + raise + except Exception as e: + return f"Error setting PERSTOP: {e}" + + +@mcp.tool() +async def suspend_peripheral(peripheral: str, suspend_mode: str = "hard") -> str: + """Configure suspend mode for a specific peripheral module. + + Sets how a peripheral responds when the CPU is halted by the debugger. + + Args: + peripheral: Peripheral register name (e.g., "GPT120_OCS", "STM0_OCS") + suspend_mode: "hard" — suspend immediately on CPU halt + "soft" — suspend after current operation completes + "none" — never suspend + + Returns: + Confirmation + """ + dbg = state.require_connection() + try: + mode_map = { + "hard": "Set hard suspend", + "soft": "Set soft suspend", + "none": "No suspend", + } + mode_val = mode_map.get(suspend_mode) + if not mode_val: + return f"Invalid suspend mode '{suspend_mode}'. Use: hard, soft, none" + + dbg.cmd(f'PER.Set.ByName .{peripheral}.SUS "{mode_val}"') + return f"Peripheral '{peripheral}' suspend: {suspend_mode}" + except ConnectionError: + raise + except Exception as e: + return f"Error configuring peripheral suspend: {e}" + + +@mcp.tool() +async def set_standby_mode(enabled: bool = True) -> str: + """Enable or disable standby mode for power cycle detection. + + When ON, TRACE32 monitors VTREF to detect power cycles and can + automatically reconnect and restore debug state. + + Args: + enabled: True to enable standby mode, False to disable + + Returns: + Confirmation + """ + dbg = state.require_connection() + try: + mode = "StandBy" if enabled else "NoStandBy" + dbg.cmd(f"SYStem.Mode {mode}") + return f"Standby mode: {mode}" + except ConnectionError: + raise + except Exception as e: + return f"Error setting standby mode: {e}" + + +@mcp.tool() +async def get_system_options() -> str: + """Get the current system options and configuration. + + Shows all active SYStem.Option settings including PERSTOP, DCREAD, + reset behavior, and other debug options. + + Returns: + Formatted system options + """ + dbg = state.require_connection() + try: + result = _read_window(dbg, "SYStem.Option") + if not result.strip(): + # Fallback: query known options individually + parts = ["System Options:"] + try: + dbg.cmd("SYStem.CONFIG.state") + config_result = _read_window(dbg, "SYStem.CONFIG.state") + if config_result.strip(): + parts.append(config_result) + except Exception: + pass + return "\n".join(parts) + return f"System Options:\n{result}" + except ConnectionError: + raise + except Exception as e: + return f"Error getting system options: {e}" + + +@mcp.tool() +async def load_symbol_file(elf_path: str = "") -> str: + """Load debug symbols from an ELF file. + + Loads symbol and debug information for source-level debugging. + If no path given, uses the configured ELF path from debugforge.toml. + + Args: + elf_path: Path to ELF file. Empty = use configured path. + + Returns: + Confirmation of symbol loading + """ + from debugforge.state import config as cfg + + dbg = state.require_connection() + path = elf_path or cfg.elf + if not path: + return "Error: No ELF path specified and no [project] elf configured in debugforge.toml" + + try: + dbg.cmd(f'Data.LOAD.Elf "{path}"') + return f"Symbols loaded from: {path}" + except ConnectionError: + raise + except Exception as e: + return f"Error loading symbols from {path}: {e}" + + +@mcp.tool() +async def run_cmm_script(script_path: str, timeout: float = 30.0) -> str: + """Execute a TRACE32 PRACTICE (.cmm) script file. + + Args: + script_path: Path to the .cmm script file + timeout: Maximum execution time in seconds (default: 30) + + Returns: + Script execution result + """ + dbg = state.require_connection() + try: + dbg.cmm(f'DO "{script_path}"', timeout=timeout) + return f"Script executed: {script_path}" + except ConnectionError: + raise + except TimeoutError: + return f"Script timed out after {timeout}s: {script_path}" + except Exception as e: + return f"Error running script {script_path}: {e}" + + +@mcp.tool() +async def set_system_option(option: str, value: str) -> str: + """Set a specific SYStem.Option value. + + Generic tool for any SYStem.Option.* setting not covered by dedicated tools. + + Args: + option: Option name (e.g., "PERSTOP", "DCREAD", "RESYNC") + value: Value to set (e.g., "ON", "OFF", "RestoreGo") + + Returns: + Confirmation + """ + dbg = state.require_connection() + try: + dbg.cmd(f"SYStem.Option.{option} {value}") + return f"SYStem.Option.{option} = {value}" + except ConnectionError: + raise + except Exception as e: + return f"Error setting option {option}: {e}" diff --git a/src/debugforge/tools/trace.py b/src/debugforge/tools/trace.py new file mode 100644 index 0000000..c38901a --- /dev/null +++ b/src/debugforge/tools/trace.py @@ -0,0 +1,294 @@ +"""On-chip Trace (MCDS) control tools for TRACE32 — TriCore AURIX. + +Implements on-chip trace recording: program trace, data trace (read/write), +timestamped trace, and trace-trigger breakpoints. Based on MCDS (Multi-core +Debug Support) hardware on AURIX devices. + +Reference: debugger_tricore.pdf "On-chip Trace" section. +""" + +from __future__ import annotations + +from debugforge.server import mcp +from debugforge.state import state +from debugforge.tools.views import _read_window + + +@mcp.tool() +async def trace_start( + sources: list[str] | None = None, + timestamps: bool = True, +) -> str: + """Start on-chip trace recording using MCDS. + + Configures and starts the MCDS trace engine. Must be called before running + the target. Call trace_stop() and trace_list() after the target halts. + + Args: + sources: List of trace sources to enable. Supported values: + "program" — instruction flow (PC trace) + "write_addr" — data write addresses + "write_data" — data write values + "read_addr" — data read addresses + "read_data" — data read values + Default: ["program"] + timestamps: Enable trace timestamps (default: True) + + Returns: + Confirmation of trace configuration + """ + dbg = state.require_connection() + try: + results = [] + + # Open MCDS state window + dbg.cmd("MCDS.state") + results.append("MCDS state window opened") + + # Enable MCDS clock + dbg.cmd("CLOCK.ON") + results.append("MCDS clock enabled") + + # Enable timestamps + if timestamps: + dbg.cmd("MCDS.TimeStamp.ON") + results.append("Timestamps enabled") + + # Configure trace sources + src_list = sources or ["program"] + source_map = { + "program": "TriCore.Program", + "write_addr": "TriCore.WriteAddr", + "write_data": "TriCore.WriteData", + "read_addr": "TriCore.ReadAddr", + "read_data": "TriCore.ReadData", + } + + for src in src_list: + mcds_src = source_map.get(src) + if mcds_src: + dbg.cmd(f"MCDS.SOURCE.Set {mcds_src} ON") + results.append(f"Source enabled: {mcds_src}") + else: + results.append(f"Warning: unknown source '{src}' (skipped)") + + return f"Trace configured:\n" + "\n".join(f" {r}" for r in results) + except ConnectionError: + raise + except Exception as e: + return f"Error starting trace: {e}" + + +@mcp.tool() +async def trace_stop() -> str: + """Stop the on-chip trace recording. + + Call after the target halts (e.g., at a breakpoint or after manual halt). + + Returns: + Confirmation that trace recording stopped + """ + dbg = state.require_connection() + try: + dbg.cmd("Trace.Off") + return "Trace recording stopped" + except ConnectionError: + raise + except Exception as e: + return f"Error stopping trace: {e}" + + +@mcp.tool() +async def trace_list(max_records: int = 100) -> str: + """Display the recorded trace buffer contents. + + Shows the trace list with instruction flow, data accesses, and timestamps + (if enabled). The target must be halted. + + Args: + max_records: Maximum number of trace records to display (default: 100) + + Returns: + Formatted trace listing + """ + dbg = state.require_connection() + try: + result = _read_window(dbg, "Trace.List", max_size=max_records * 200) + if not result.strip(): + return "Trace buffer empty (no trace data recorded, or trace not started)" + return result + except ConnectionError: + raise + except Exception as e: + return f"Error reading trace: {e}" + + +@mcp.tool() +async def trace_clear() -> str: + """Clear the trace buffer. + + Resets the MCDS trace buffer for a fresh recording. + + Returns: + Confirmation + """ + dbg = state.require_connection() + try: + dbg.cmd("Trace.Clear") + return "Trace buffer cleared" + except ConnectionError: + raise + except Exception as e: + return f"Error clearing trace: {e}" + + +@mcp.tool() +async def trace_set_trigger( + address: str, + trigger_type: str = "stop", +) -> str: + """Set a trace trigger breakpoint. + + Configures a breakpoint that controls trace recording start/stop. + + Args: + address: Function name, symbol, or address (e.g., "sieve", "0x80001000"). + Supports symbol.EXIT(func) syntax for function exit triggers. + trigger_type: "stop" — stop recording at this point (TraceTrigger) + "enable" — enable recording at this point (TraceEnable) + "disable" — disable recording at this point + + Returns: + Confirmation of trace trigger + """ + dbg = state.require_connection() + try: + type_map = { + "stop": "/TraceTrigger", + "enable": "/TraceEnable", + "disable": "/TraceDisable", + } + flag = type_map.get(trigger_type) + if not flag: + return f"Invalid trigger type '{trigger_type}'. Use: stop, enable, disable" + + dbg.cmd(f"Break.Set {address} /Program {flag}") + return f"Trace trigger set: {trigger_type} @ {address}" + except ConnectionError: + raise + except Exception as e: + return f"Error setting trace trigger: {e}" + + +@mcp.tool() +async def trace_data_write( + variable: str, + data_value: str = "", + data_width: str = "auto", +) -> str: + """Configure data write trace on a variable. + + Records every write access to the specified variable in the trace buffer. + Optionally filters by data value. + + Args: + variable: Variable name or address (e.g., "flags[12]", "myVar", "0xD0000100") + data_value: Optional filter — only trace writes matching this value (e.g., "0x33") + data_width: Data width for value matching — "byte", "word", "long", "quad", "auto" + + Returns: + Confirmation of data write trace setup + """ + dbg = state.require_connection() + try: + cmd = f"Var.Break.Set {variable} /Write /Onchip /TraceEnable" + if data_value: + width_map = {"byte": "Byte", "word": "Word", "long": "Long", + "quad": "Quad", "auto": "auto"} + w = width_map.get(data_width, "auto") + cmd += f" /Data.{w} {data_value}" + + dbg.cmd(cmd) + + # Enable data trace sources + dbg.cmd("MCDS.SOURCE.Set TriCore.WriteAddr ON") + dbg.cmd("MCDS.SOURCE.Set TriCore.WriteData ON") + + msg = f"Data write trace enabled on '{variable}'" + if data_value: + msg += f" (filter: value={data_value}, width={data_width})" + return msg + except ConnectionError: + raise + except Exception as e: + return f"Error configuring data write trace: {e}" + + +@mcp.tool() +async def trace_data_read( + variable: str, +) -> str: + """Configure data read trace on a variable. + + Records every read access to the specified variable in the trace buffer. + + Args: + variable: Variable name or address (e.g., "myVar", "0xD0000100") + + Returns: + Confirmation of data read trace setup + """ + dbg = state.require_connection() + try: + dbg.cmd(f"Var.Break.Set {variable} /Read /Onchip /TraceEnable") + dbg.cmd("MCDS.SOURCE.Set TriCore.ReadAddr ON") + dbg.cmd("MCDS.SOURCE.Set TriCore.ReadData ON") + return f"Data read trace enabled on '{variable}'" + except ConnectionError: + raise + except Exception as e: + return f"Error configuring data read trace: {e}" + + +@mcp.tool() +async def trace_profile_chart() -> str: + """Display a profile chart from recorded trace data. + + Shows instruction execution statistics per function based on trace data. + Requires trace data to have been recorded first. + + Returns: + Formatted profile chart + """ + dbg = state.require_connection() + try: + result = _read_window(dbg, "Trace.PROfileChart") + if not result.strip(): + return "No profile data available (record trace data first)" + return result + except ConnectionError: + raise + except Exception as e: + return f"Error getting trace profile: {e}" + + +@mcp.tool() +async def trace_state() -> str: + """Get current MCDS trace state and configuration. + + Shows the MCDS state window with current trace source settings, + buffer status, and configuration. + + Returns: + MCDS state information + """ + dbg = state.require_connection() + try: + result = _read_window(dbg, "MCDS.state") + if not result.strip(): + return "MCDS state not available (on-chip trace may not be supported)" + return result + except ConnectionError: + raise + except Exception as e: + return f"Error getting MCDS state: {e}" diff --git a/src/debugforge/tools/watchpin.py b/src/debugforge/tools/watchpin.py new file mode 100644 index 0000000..4d5c2f9 --- /dev/null +++ b/src/debugforge/tools/watchpin.py @@ -0,0 +1,181 @@ +"""WatchPin hardware trigger output tools for TriCore AURIX. + +Provides control over hardware WatchPin outputs that can generate logic analyzer +triggers or oscilloscope signals when the target halts or hits specific conditions. + +Reference: debugger_tricore.pdf "CPU specific WatchPin Commands" section. +""" + +from __future__ import annotations + +from debugforge.server import mcp +from debugforge.state import state +from debugforge.tools.views import _read_window + + +@mcp.tool() +async def watchpin_enable() -> str: + """Enable WatchPin output hardware. + + Activates the WatchPin output pins on the debugger. These pins generate + logic signals that can be connected to external test equipment. + + Returns: + Confirmation + """ + dbg = state.require_connection() + try: + dbg.cmd("WatchPin.Enable") + return "WatchPin outputs enabled" + except ConnectionError: + raise + except Exception as e: + return f"Error enabling WatchPin: {e}" + + +@mcp.tool() +async def watchpin_disable() -> str: + """Disable WatchPin output hardware. + + Deactivates all WatchPin outputs. + + Returns: + Confirmation + """ + dbg = state.require_connection() + try: + dbg.cmd("WatchPin.Disable") + return "WatchPin outputs disabled" + except ConnectionError: + raise + except Exception as e: + return f"Error disabling WatchPin: {e}" + + +@mcp.tool() +async def watchpin_configure( + pin: int, + trigger: str = "break", + polarity: str = "high", +) -> str: + """Configure a WatchPin to trigger on specific events. + + Args: + pin: WatchPin number (1, 2, 3, or 4 depending on hardware) + trigger: Trigger condition: + "break" — pulse when target halts + "go" — pulse when target runs + "trace_start" — pulse when trace recording starts + "trace_stop" — pulse when trace recording stops + polarity: Output polarity — "high" (active high) or "low" (active low) + + Returns: + Confirmation of WatchPin configuration + """ + dbg = state.require_connection() + try: + trigger_map = { + "break": "Break", + "go": "Go", + "trace_start": "TraceStart", + "trace_stop": "TraceStop", + } + pol_map = { + "high": "High", + "low": "Low", + } + + trig_val = trigger_map.get(trigger) + pol_val = pol_map.get(polarity) + + if not trig_val: + return f"Invalid trigger '{trigger}'. Use: break, go, trace_start, trace_stop" + if not pol_val: + return f"Invalid polarity '{polarity}'. Use: high, low" + + dbg.cmd(f"WatchPin.Set {pin}. {trig_val} {pol_val}") + return f"WatchPin {pin} configured: trigger={trigger}, polarity={polarity}" + except ConnectionError: + raise + except Exception as e: + return f"Error configuring WatchPin {pin}: {e}" + + +@mcp.tool() +async def watchpin_set_action_breakpoint( + address: str, + pin: int, + pulse_width_us: int = 100, +) -> str: + """Set an action breakpoint that pulses a WatchPin when hit. + + Useful for correlating software events with external hardware (e.g., oscilloscope). + The target briefly stops, the WatchPin pulses, then execution resumes. + + Args: + address: Address or symbol name + pin: WatchPin number to pulse + pulse_width_us: Pulse width in microseconds (default: 100) + + Returns: + Confirmation + """ + dbg = state.require_connection() + try: + # Set WatchPin output via PRACTICE command + wp_cmd = f"WatchPin.Set {pin}. High" + dbg.cmd(f'Break.Set {address} /Program /CMD "{wp_cmd}" /RESUME') + return f"Action breakpoint at {address} — pulses WatchPin {pin} ({pulse_width_us}us)" + except ConnectionError: + raise + except Exception as e: + return f"Error setting WatchPin action breakpoint: {e}" + + +@mcp.tool() +async def watchpin_state() -> str: + """Get current WatchPin configuration and state. + + Shows which WatchPins are enabled, their trigger conditions, and polarity. + + Returns: + WatchPin state information + """ + dbg = state.require_connection() + try: + result = _read_window(dbg, "WatchPin.state") + if not result.strip(): + return "WatchPin state not available (hardware may not support WatchPin)" + return f"WatchPin State:\n{result}" + except ConnectionError: + raise + except Exception as e: + return f"Error getting WatchPin state: {e}" + + +@mcp.tool() +async def watchpin_manual_pulse(pin: int, duration_ms: int = 10) -> str: + """Manually pulse a WatchPin for testing. + + Useful for verifying WatchPin hardware connections before setting up + automatic triggers. + + Args: + pin: WatchPin number + duration_ms: Pulse duration in milliseconds (default: 10) + + Returns: + Confirmation + """ + dbg = state.require_connection() + try: + dbg.cmd(f"WatchPin.Set {pin}. High") + # Wait for duration + import asyncio + await asyncio.sleep(duration_ms / 1000.0) + dbg.cmd(f"WatchPin.Set {pin}. Low") + return f"WatchPin {pin} pulsed for {duration_ms}ms" + except ConnectionError: + raise + except Exception as e: + return f"Error pulsing WatchPin {pin}: {e}" diff --git a/t32_debugforge.t32 b/t32_debugforge.t32 new file mode 100644 index 0000000..9866dd0 --- /dev/null +++ b/t32_debugforge.t32 @@ -0,0 +1,11 @@ +; DebugForge TRACE32 Configuration +; Enables Remote Control Layer (RCL) for Python automation + +OS= +SYS=/opt/t32_202509 +TMP=/tmp/t32_debugforge + +; Remote Control via NETTCP (required by lauterbach-trace32-rcl Python package) +RCL=NETTCP +PORT=20000 +PACKLEN=4096