microchip-devtools 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- microchip_devtools/__init__.py +0 -0
- microchip_devtools/_project.py +14 -0
- microchip_devtools/format/__init__.py +0 -0
- microchip_devtools/format/uncrustify.py +119 -0
- microchip_devtools/list_cmds.py +31 -0
- microchip_devtools/mcc/__init__.py +0 -0
- microchip_devtools/mcc/check_peripheral.py +207 -0
- microchip_devtools/mcc/mcc_refresh.py +343 -0
- microchip_devtools/mcc/parse_hardware.py +374 -0
- microchip_devtools/setup_env/__init__.py +0 -0
- microchip_devtools/setup_env/_ui.py +63 -0
- microchip_devtools/setup_env/checks.py +178 -0
- microchip_devtools/setup_env/defaults.py +33 -0
- microchip_devtools/setup_env/runner.py +334 -0
- microchip_devtools/xc32/__init__.py +0 -0
- microchip_devtools/xc32/merge_hex.py +238 -0
- microchip_devtools/xc32/validate_fmt3.py +230 -0
- microchip_devtools-0.1.0.dist-info/METADATA +16 -0
- microchip_devtools-0.1.0.dist-info/RECORD +21 -0
- microchip_devtools-0.1.0.dist-info/WHEEL +4 -0
- microchip_devtools-0.1.0.dist-info/entry_points.txt +10 -0
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
"""
|
|
2
|
+
setup_env runner — Environment checker and project installer.
|
|
3
|
+
|
|
4
|
+
Called by each project's thin pymake/setup_env.py wrapper, which passes
|
|
5
|
+
the merged DEFAULTS (COMMON_DEFAULTS | PROJECT_DEFAULTS).
|
|
6
|
+
|
|
7
|
+
Usage (via project wrapper):
|
|
8
|
+
poetry run setup_env [action]
|
|
9
|
+
|
|
10
|
+
Actions:
|
|
11
|
+
check Verify all prerequisites (default)
|
|
12
|
+
install Run `poetry install` to set up the Python environment
|
|
13
|
+
env Show current ENV variable values vs. their defaults
|
|
14
|
+
init-env Create a .env file from defaults
|
|
15
|
+
all Run check → install in sequence
|
|
16
|
+
|
|
17
|
+
Exit codes:
|
|
18
|
+
0 All checks passed / action completed successfully
|
|
19
|
+
1 One or more checks failed
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import argparse
|
|
23
|
+
import os
|
|
24
|
+
import sys
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
|
|
27
|
+
from dotenv import load_dotenv as _dotenv_load
|
|
28
|
+
from rich.panel import Panel
|
|
29
|
+
from rich.prompt import Prompt
|
|
30
|
+
from rich.table import Table
|
|
31
|
+
|
|
32
|
+
from microchip_devtools.setup_env._ui import console
|
|
33
|
+
from microchip_devtools.setup_env.checks import (
|
|
34
|
+
check_boot_hex,
|
|
35
|
+
check_cppcheck,
|
|
36
|
+
check_dfp,
|
|
37
|
+
check_make,
|
|
38
|
+
check_poetry,
|
|
39
|
+
check_programmer,
|
|
40
|
+
check_python,
|
|
41
|
+
check_uncrustify,
|
|
42
|
+
check_xc32,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
_ENV_DESCRIPTIONS: dict[str, str] = {
|
|
46
|
+
"XC32_PATH": "Path to Microchip XC32 toolchain bin/ directory",
|
|
47
|
+
"DFP_PATH": "Path to the Device Family Pack (DFP) root directory",
|
|
48
|
+
"IPE_CMD": "Path to the MPLAB IPE command-line script (ipecmd.sh)",
|
|
49
|
+
"BOOT_HEX": "Path to the bootloader .hex file used by the flash-with-boot target",
|
|
50
|
+
"PROGRAMMER": "Programmer/debugger tool short name (e.g. PK5, ICD4, SNAP)",
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _load_dotenv(env_file: Path) -> None:
|
|
55
|
+
_dotenv_load(dotenv_path=env_file, override=False)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _resolve(key: str, defaults: dict[str, str]) -> str:
|
|
59
|
+
return os.path.expandvars(
|
|
60
|
+
os.path.expanduser(os.environ.get(key, defaults.get(key, "")))
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def check(defaults: dict[str, str], env_file: Path) -> bool:
|
|
65
|
+
console.print()
|
|
66
|
+
console.rule("Checking prerequisites", style="bold blue")
|
|
67
|
+
console.print()
|
|
68
|
+
|
|
69
|
+
results: list[bool] = [
|
|
70
|
+
check_python(),
|
|
71
|
+
check_poetry(),
|
|
72
|
+
check_make(),
|
|
73
|
+
check_cppcheck(),
|
|
74
|
+
check_uncrustify(),
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
if "XC32_PATH" in defaults:
|
|
78
|
+
results.append(check_xc32(_resolve("XC32_PATH", defaults), env_file))
|
|
79
|
+
if "DFP_PATH" in defaults:
|
|
80
|
+
results.append(check_dfp(_resolve("DFP_PATH", defaults), env_file))
|
|
81
|
+
if "BOOT_HEX" in defaults:
|
|
82
|
+
results.append(check_boot_hex(_resolve("BOOT_HEX", defaults), env_file))
|
|
83
|
+
if "PROGRAMMER" in defaults:
|
|
84
|
+
results.append(check_programmer(_resolve("PROGRAMMER", defaults)))
|
|
85
|
+
|
|
86
|
+
passed = sum(results)
|
|
87
|
+
total = len(results)
|
|
88
|
+
console.print()
|
|
89
|
+
|
|
90
|
+
if all(results):
|
|
91
|
+
console.print(
|
|
92
|
+
Panel(
|
|
93
|
+
f"[green]✔ All {total} checks passed. The project is ready to build.[/green]\n"
|
|
94
|
+
f" Run [bold]make[/bold] or [bold]poetry run mchp-setup install[/bold] to continue.",
|
|
95
|
+
border_style="green",
|
|
96
|
+
padding=(0, 2),
|
|
97
|
+
)
|
|
98
|
+
)
|
|
99
|
+
else:
|
|
100
|
+
failed = total - passed
|
|
101
|
+
console.print(
|
|
102
|
+
Panel(
|
|
103
|
+
f"[red]✗ {failed} of {total} checks failed.[/red]\n"
|
|
104
|
+
f" Fix the issues above, then run [bold]poetry run mchp-setup check[/bold] again.",
|
|
105
|
+
border_style="red",
|
|
106
|
+
padding=(0, 2),
|
|
107
|
+
)
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
console.print()
|
|
111
|
+
return all(results)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def install(
|
|
115
|
+
defaults: dict[str, str], env_file: Path, skip_checks: bool = False
|
|
116
|
+
) -> None:
|
|
117
|
+
if not skip_checks:
|
|
118
|
+
ok = check(defaults, env_file)
|
|
119
|
+
if not ok:
|
|
120
|
+
console.print("[red] Skipping install because checks failed.[/red]\n")
|
|
121
|
+
sys.exit(1)
|
|
122
|
+
|
|
123
|
+
console.print()
|
|
124
|
+
console.rule("Installing Python environment", style="bold blue")
|
|
125
|
+
console.print()
|
|
126
|
+
try:
|
|
127
|
+
import subprocess
|
|
128
|
+
|
|
129
|
+
subprocess.run(["poetry", "env", "remove", "python"], check=False)
|
|
130
|
+
subprocess.run(["poetry", "install", "--no-root"], check=True)
|
|
131
|
+
console.print("\n[green] Done. Python environment is ready.[/green]\n")
|
|
132
|
+
except Exception as exc:
|
|
133
|
+
console.print(f"\n[red] `poetry install` failed: {exc}[/red]\n")
|
|
134
|
+
sys.exit(1)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def env(defaults: dict[str, str]) -> None:
|
|
138
|
+
console.print()
|
|
139
|
+
console.rule("Environment variables", style="bold blue")
|
|
140
|
+
console.print()
|
|
141
|
+
|
|
142
|
+
table = Table(
|
|
143
|
+
show_header=True,
|
|
144
|
+
header_style="bold",
|
|
145
|
+
padding=(0, 1),
|
|
146
|
+
show_edge=False,
|
|
147
|
+
)
|
|
148
|
+
table.add_column("Variable", style="bold cyan", no_wrap=True)
|
|
149
|
+
table.add_column("Current value")
|
|
150
|
+
table.add_column("Default", style="dim")
|
|
151
|
+
|
|
152
|
+
for key, default in defaults.items():
|
|
153
|
+
current = os.environ.get(key)
|
|
154
|
+
if current is None:
|
|
155
|
+
value_display = f"[yellow](default) {default}[/yellow]"
|
|
156
|
+
elif current == default:
|
|
157
|
+
value_display = current
|
|
158
|
+
else:
|
|
159
|
+
value_display = f"[green]{current}[/green] [yellow]← overridden[/yellow]"
|
|
160
|
+
table.add_row(key, value_display, default)
|
|
161
|
+
|
|
162
|
+
console.print(table)
|
|
163
|
+
console.print()
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _prompt_env_form(defaults: dict[str, str], env_file: Path) -> None:
|
|
167
|
+
console.print(
|
|
168
|
+
Panel(
|
|
169
|
+
"[bold yellow]ACTION REQUIRED — Configure your environment[/bold yellow]\n\n"
|
|
170
|
+
" Review each variable below.\n"
|
|
171
|
+
" Press [bold]Enter[/bold] to keep the default, or type a new value to override it.\n"
|
|
172
|
+
" Variables you override will be [bold green]uncommented[/bold green] in [cyan].env[/cyan].\n"
|
|
173
|
+
" Variables left at default stay commented out (no effect on the build).",
|
|
174
|
+
border_style="yellow",
|
|
175
|
+
padding=(1, 2),
|
|
176
|
+
title="[bold yellow] .env setup [/bold yellow]",
|
|
177
|
+
)
|
|
178
|
+
)
|
|
179
|
+
console.print()
|
|
180
|
+
|
|
181
|
+
chosen: dict[str, str | None] = {}
|
|
182
|
+
for key, default in defaults.items():
|
|
183
|
+
desc = _ENV_DESCRIPTIONS.get(key, "")
|
|
184
|
+
if desc:
|
|
185
|
+
console.print(f" [dim]{desc}[/dim]")
|
|
186
|
+
value = Prompt.ask(
|
|
187
|
+
f" [bold cyan]{key}[/bold cyan]",
|
|
188
|
+
default=default,
|
|
189
|
+
console=console,
|
|
190
|
+
)
|
|
191
|
+
chosen[key] = value if value != default else None
|
|
192
|
+
console.print()
|
|
193
|
+
|
|
194
|
+
overridden = {k: v for k, v in chosen.items() if v is not None}
|
|
195
|
+
|
|
196
|
+
lines = [
|
|
197
|
+
"# .env — local overrides for environment variables",
|
|
198
|
+
"# Shell-level exports always take priority over values set here.",
|
|
199
|
+
"# Uncomment and edit only the variables you need to override.",
|
|
200
|
+
"",
|
|
201
|
+
]
|
|
202
|
+
for key, default in defaults.items():
|
|
203
|
+
desc = _ENV_DESCRIPTIONS.get(key, "")
|
|
204
|
+
if desc:
|
|
205
|
+
lines.append(f"# {desc}")
|
|
206
|
+
if key in overridden:
|
|
207
|
+
lines.append(f"{key}={overridden[key]}")
|
|
208
|
+
else:
|
|
209
|
+
lines.append(f"# {key}={default}")
|
|
210
|
+
lines.append("")
|
|
211
|
+
|
|
212
|
+
env_file.write_text("\n".join(lines))
|
|
213
|
+
|
|
214
|
+
if overridden:
|
|
215
|
+
keys_fmt = ", ".join(f"[cyan]{k}[/cyan]" for k in overridden)
|
|
216
|
+
console.print(
|
|
217
|
+
Panel(
|
|
218
|
+
f"[green]✔ .env updated.[/green] Overridden: {keys_fmt}\n\n"
|
|
219
|
+
f" Run [bold]poetry run mchp-setup check[/bold] to verify your setup.",
|
|
220
|
+
border_style="green",
|
|
221
|
+
padding=(0, 2),
|
|
222
|
+
)
|
|
223
|
+
)
|
|
224
|
+
else:
|
|
225
|
+
console.print(
|
|
226
|
+
Panel(
|
|
227
|
+
"[green]✔ .env created.[/green] All variables left at their defaults (commented out).\n\n"
|
|
228
|
+
" Edit [cyan].env[/cyan] manually any time to override a value.\n"
|
|
229
|
+
" Run [bold]poetry run mchp-setup check[/bold] to verify your setup.",
|
|
230
|
+
border_style="green",
|
|
231
|
+
padding=(0, 2),
|
|
232
|
+
)
|
|
233
|
+
)
|
|
234
|
+
console.print()
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def init_env(defaults: dict[str, str], env_file: Path, force: bool = False) -> None:
|
|
238
|
+
env_example = env_file.parent / ".env.example"
|
|
239
|
+
|
|
240
|
+
if env_file.exists() and not force:
|
|
241
|
+
console.print(
|
|
242
|
+
"[yellow] .env already exists. Use --force to overwrite it.[/yellow]\n"
|
|
243
|
+
)
|
|
244
|
+
return
|
|
245
|
+
|
|
246
|
+
if env_example.exists():
|
|
247
|
+
import shutil
|
|
248
|
+
|
|
249
|
+
shutil.copy(env_example, env_file)
|
|
250
|
+
console.print(f"[green] Created .env from {env_example.name}.[/green]")
|
|
251
|
+
_prompt_env_form(defaults, env_file)
|
|
252
|
+
return
|
|
253
|
+
|
|
254
|
+
lines = [
|
|
255
|
+
"# .env — local overrides for environment variables",
|
|
256
|
+
"# Shell-level exports always take priority over values set here.",
|
|
257
|
+
"# Uncomment and edit only the variables you need to override.",
|
|
258
|
+
"",
|
|
259
|
+
]
|
|
260
|
+
for key, default in defaults.items():
|
|
261
|
+
desc = _ENV_DESCRIPTIONS.get(key, "")
|
|
262
|
+
if desc:
|
|
263
|
+
lines.append(f"# {desc}")
|
|
264
|
+
lines.append(f"# {key}={default}")
|
|
265
|
+
lines.append("")
|
|
266
|
+
|
|
267
|
+
env_file.write_text("\n".join(lines))
|
|
268
|
+
console.print(
|
|
269
|
+
f"[green] Created .env with {len(defaults)} commented-out variables.[/green]"
|
|
270
|
+
)
|
|
271
|
+
_prompt_env_form(defaults, env_file)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def main(defaults: dict[str, str] | None = None) -> None:
|
|
275
|
+
"""Entry point. Projects pass merged COMMON_DEFAULTS | PROJECT_DEFAULTS."""
|
|
276
|
+
if defaults is None:
|
|
277
|
+
import sys
|
|
278
|
+
|
|
279
|
+
from microchip_devtools.setup_env.defaults import COMMON_DEFAULTS
|
|
280
|
+
|
|
281
|
+
cwd = str(Path.cwd())
|
|
282
|
+
if cwd not in sys.path:
|
|
283
|
+
sys.path.insert(0, cwd)
|
|
284
|
+
|
|
285
|
+
try:
|
|
286
|
+
from pymake import PROJECT_DEFAULTS # type: ignore
|
|
287
|
+
|
|
288
|
+
defaults = COMMON_DEFAULTS | PROJECT_DEFAULTS
|
|
289
|
+
except ImportError:
|
|
290
|
+
defaults = COMMON_DEFAULTS
|
|
291
|
+
|
|
292
|
+
env_file = Path(".env")
|
|
293
|
+
_load_dotenv(env_file)
|
|
294
|
+
|
|
295
|
+
parser = argparse.ArgumentParser(
|
|
296
|
+
prog="setup_env",
|
|
297
|
+
description="Verify and install everything needed to compile the firmware.",
|
|
298
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
299
|
+
epilog="""
|
|
300
|
+
actions:
|
|
301
|
+
check Verify all prerequisites (default)
|
|
302
|
+
install Run poetry install
|
|
303
|
+
env Show ENV variable values vs. defaults
|
|
304
|
+
init-env Create a .env file from defaults
|
|
305
|
+
all check → install in sequence
|
|
306
|
+
""",
|
|
307
|
+
)
|
|
308
|
+
parser.add_argument(
|
|
309
|
+
"action",
|
|
310
|
+
nargs="?",
|
|
311
|
+
default="check",
|
|
312
|
+
choices=["check", "install", "env", "init-env", "all"],
|
|
313
|
+
)
|
|
314
|
+
parser.add_argument("--skip-checks", action="store_true")
|
|
315
|
+
parser.add_argument("--force", action="store_true")
|
|
316
|
+
|
|
317
|
+
args = parser.parse_args()
|
|
318
|
+
|
|
319
|
+
assert defaults is not None
|
|
320
|
+
|
|
321
|
+
if args.action == "check":
|
|
322
|
+
ok = check(defaults, env_file)
|
|
323
|
+
sys.exit(0 if ok else 1)
|
|
324
|
+
elif args.action == "install":
|
|
325
|
+
install(defaults, env_file, skip_checks=args.skip_checks)
|
|
326
|
+
elif args.action == "env":
|
|
327
|
+
env(defaults)
|
|
328
|
+
elif args.action == "init-env":
|
|
329
|
+
init_env(defaults, env_file, force=args.force)
|
|
330
|
+
elif args.action == "all":
|
|
331
|
+
ok = check(defaults, env_file)
|
|
332
|
+
if not ok:
|
|
333
|
+
sys.exit(1)
|
|
334
|
+
install(defaults, env_file, skip_checks=True)
|
|
File without changes
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
microchip_devtools.xc32.merge_hex — Merge bootloader + app HEX into a single image.
|
|
4
|
+
|
|
5
|
+
Optionally patches:
|
|
6
|
+
- A signature word at a given physical address (for bootloader app-valid checks)
|
|
7
|
+
- DEVCFG0 config bits to enable EJTAG debugging (PIC32MK-specific)
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
mchp-merge-hex boot.hex app.hex out.hex [options]
|
|
11
|
+
|
|
12
|
+
Options:
|
|
13
|
+
--sig-addr ADDR Physical address to write the signature word (hex, e.g. 0x1D0FFFF8)
|
|
14
|
+
--sig-word VALUE Signature word value (hex, default: 0x00000000)
|
|
15
|
+
--ejtag-addr ADDR Physical address of DEVCFG0 config word for EJTAG enable (optional)
|
|
16
|
+
|
|
17
|
+
If --sig-addr is omitted, no signature patch is applied.
|
|
18
|
+
If --ejtag-addr is omitted, no DEVCFG0 patch is applied.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import argparse
|
|
22
|
+
import os
|
|
23
|
+
import sys
|
|
24
|
+
|
|
25
|
+
END_RECORD = ":00000001FF"
|
|
26
|
+
|
|
27
|
+
# PIC32 DEVCFG0 bit constants (architecture-level, same for all PIC32 devices):
|
|
28
|
+
# DEBUG[1:0] at bits 1:0 — 0b00 = debugger enabled, 0b11 = disabled
|
|
29
|
+
# JTAGEN at bit 2 — 1 = JTAG enabled, 0 = disabled
|
|
30
|
+
_DEVCFG0_DEBUG_MASK = 0x00000003
|
|
31
|
+
_DEVCFG0_JTAGEN_BIT = 0x00000004
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _ihex_checksum(record_bytes: list[int]) -> int:
|
|
35
|
+
return (~sum(record_bytes) + 1) & 0xFF
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _patch_word(lines: list[str], addr: int, word: int) -> list[str]:
|
|
39
|
+
"""Write *word* (little-endian, 4 bytes) at physical *addr* in an Intel HEX line list."""
|
|
40
|
+
word_bytes = [(word >> (8 * i)) & 0xFF for i in range(4)]
|
|
41
|
+
|
|
42
|
+
current_ela = 0
|
|
43
|
+
result: list[str] = []
|
|
44
|
+
patched = False
|
|
45
|
+
|
|
46
|
+
for line in lines:
|
|
47
|
+
stripped = line.strip()
|
|
48
|
+
if not stripped.startswith(':'):
|
|
49
|
+
result.append(line)
|
|
50
|
+
continue
|
|
51
|
+
|
|
52
|
+
byte_count = int(stripped[1:3], 16)
|
|
53
|
+
addr_offset = int(stripped[3:7], 16)
|
|
54
|
+
record_type = int(stripped[7:9], 16)
|
|
55
|
+
data_hex = stripped[9: 9 + byte_count * 2]
|
|
56
|
+
|
|
57
|
+
if record_type == 0x04:
|
|
58
|
+
current_ela = int(data_hex, 16)
|
|
59
|
+
result.append(line)
|
|
60
|
+
continue
|
|
61
|
+
|
|
62
|
+
if record_type != 0x00:
|
|
63
|
+
result.append(line)
|
|
64
|
+
continue
|
|
65
|
+
|
|
66
|
+
base_addr = (current_ela << 16) | addr_offset
|
|
67
|
+
end_addr = base_addr + byte_count
|
|
68
|
+
|
|
69
|
+
if base_addr <= addr < end_addr:
|
|
70
|
+
data = bytearray.fromhex(data_hex)
|
|
71
|
+
off = addr - base_addr
|
|
72
|
+
for i in range(4):
|
|
73
|
+
if off + i < byte_count:
|
|
74
|
+
data[off + i] = word_bytes[i]
|
|
75
|
+
|
|
76
|
+
core = [byte_count,
|
|
77
|
+
(addr_offset >> 8) & 0xFF,
|
|
78
|
+
addr_offset & 0xFF,
|
|
79
|
+
0x00] + list(data)
|
|
80
|
+
chk = _ihex_checksum(core)
|
|
81
|
+
result.append(f":{byte_count:02X}{addr_offset:04X}00{data.hex().upper()}{chk:02X}")
|
|
82
|
+
patched = True
|
|
83
|
+
else:
|
|
84
|
+
result.append(line)
|
|
85
|
+
|
|
86
|
+
if not patched:
|
|
87
|
+
raise RuntimeError(
|
|
88
|
+
f"[merge_hex] ERROR: address 0x{addr:08X} not found in merged HEX.\n"
|
|
89
|
+
f" Expected a data record covering that address."
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
return result
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _patch_devcfg0_ejtag(lines: list[str], devcfg0_addr: int) -> list[str]:
|
|
96
|
+
"""Enable EJTAG in the DEVCFG0 config word at *devcfg0_addr*.
|
|
97
|
+
|
|
98
|
+
Clears DEBUG[1:0] (bits 1:0) and sets JTAGEN (bit 2).
|
|
99
|
+
Without this patch the debugger attach fails with error 0x104 when
|
|
100
|
+
the bootloader was built with JTAGEN=OFF and DEBUG=OFF.
|
|
101
|
+
"""
|
|
102
|
+
current_ela = 0
|
|
103
|
+
result: list[str] = []
|
|
104
|
+
patched = False
|
|
105
|
+
|
|
106
|
+
for line in lines:
|
|
107
|
+
stripped = line.strip()
|
|
108
|
+
if not stripped.startswith(':'):
|
|
109
|
+
result.append(line)
|
|
110
|
+
continue
|
|
111
|
+
|
|
112
|
+
byte_count = int(stripped[1:3], 16)
|
|
113
|
+
addr_offset = int(stripped[3:7], 16)
|
|
114
|
+
record_type = int(stripped[7:9], 16)
|
|
115
|
+
data_hex = stripped[9: 9 + byte_count * 2]
|
|
116
|
+
|
|
117
|
+
if record_type == 0x04:
|
|
118
|
+
current_ela = int(data_hex, 16)
|
|
119
|
+
result.append(line)
|
|
120
|
+
continue
|
|
121
|
+
|
|
122
|
+
if record_type != 0x00:
|
|
123
|
+
result.append(line)
|
|
124
|
+
continue
|
|
125
|
+
|
|
126
|
+
base_addr = (current_ela << 16) | addr_offset
|
|
127
|
+
end_addr = base_addr + byte_count
|
|
128
|
+
|
|
129
|
+
if base_addr <= devcfg0_addr < end_addr:
|
|
130
|
+
data = bytearray.fromhex(data_hex)
|
|
131
|
+
off = devcfg0_addr - base_addr
|
|
132
|
+
|
|
133
|
+
word = data[off] | (data[off+1] << 8) | (data[off+2] << 16) | (data[off+3] << 24)
|
|
134
|
+
orig = word
|
|
135
|
+
word &= ~_DEVCFG0_DEBUG_MASK
|
|
136
|
+
word |= _DEVCFG0_JTAGEN_BIT
|
|
137
|
+
|
|
138
|
+
for i in range(4):
|
|
139
|
+
data[off + i] = (word >> (8 * i)) & 0xFF
|
|
140
|
+
|
|
141
|
+
core = [byte_count,
|
|
142
|
+
(addr_offset >> 8) & 0xFF,
|
|
143
|
+
addr_offset & 0xFF,
|
|
144
|
+
0x00] + list(data)
|
|
145
|
+
chk = _ihex_checksum(core)
|
|
146
|
+
result.append(f":{byte_count:02X}{addr_offset:04X}00{data.hex().upper()}{chk:02X}")
|
|
147
|
+
patched = True
|
|
148
|
+
|
|
149
|
+
print(f"[merge_hex] DEVCFG0 : 0x{orig:08X} → 0x{word:08X} "
|
|
150
|
+
f"(JTAGEN=ON, DEBUG=ON) at phys 0x{devcfg0_addr:08X}")
|
|
151
|
+
else:
|
|
152
|
+
result.append(line)
|
|
153
|
+
|
|
154
|
+
if not patched:
|
|
155
|
+
raise RuntimeError(
|
|
156
|
+
f"[merge_hex] ERROR: DEVCFG0 at 0x{devcfg0_addr:08X} not found.\n"
|
|
157
|
+
f" The bootloader HEX must include config word records."
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
return result
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def merge(
|
|
164
|
+
boot_path: str,
|
|
165
|
+
app_path: str,
|
|
166
|
+
out_path: str,
|
|
167
|
+
sig_addr: int | None = None,
|
|
168
|
+
sig_word: int = 0x00000000,
|
|
169
|
+
ejtag_addr: int | None = None,
|
|
170
|
+
) -> None:
|
|
171
|
+
if not os.path.exists(boot_path):
|
|
172
|
+
sys.exit(f"[merge_hex] ERROR: bootloader HEX not found: {boot_path}")
|
|
173
|
+
if not os.path.exists(app_path):
|
|
174
|
+
sys.exit(f"[merge_hex] ERROR: app HEX not found: {app_path}\n"
|
|
175
|
+
f" Run 'make' first to build the app.")
|
|
176
|
+
|
|
177
|
+
with open(boot_path, "r", encoding="utf-8") as f:
|
|
178
|
+
boot_lines = [l.rstrip("\r\n") for l in f]
|
|
179
|
+
|
|
180
|
+
with open(app_path, "r", encoding="utf-8") as f:
|
|
181
|
+
app_lines = [l.rstrip("\r\n") for l in f]
|
|
182
|
+
|
|
183
|
+
boot_stripped = [l for l in boot_lines if l.upper().strip() != END_RECORD]
|
|
184
|
+
merged = boot_stripped + app_lines
|
|
185
|
+
|
|
186
|
+
if sig_addr is not None:
|
|
187
|
+
merged = _patch_word(merged, sig_addr, sig_word)
|
|
188
|
+
print(f"[merge_hex] signed : 0x{sig_word:08X} written at phys 0x{sig_addr:08X}")
|
|
189
|
+
|
|
190
|
+
if ejtag_addr is not None:
|
|
191
|
+
merged = _patch_devcfg0_ejtag(merged, ejtag_addr)
|
|
192
|
+
|
|
193
|
+
os.makedirs(os.path.dirname(out_path) or ".", exist_ok=True)
|
|
194
|
+
with open(out_path, "w", newline="\n", encoding="utf-8") as f:
|
|
195
|
+
for line in merged:
|
|
196
|
+
f.write(line + "\n")
|
|
197
|
+
|
|
198
|
+
print(f"[merge_hex] boot : {boot_path} ({len(boot_stripped)} records)")
|
|
199
|
+
print(f"[merge_hex] app : {app_path} ({len(app_lines)} records)")
|
|
200
|
+
print(f"[merge_hex] output : {out_path} ({len(merged)} records total)")
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def main() -> None:
|
|
204
|
+
parser = argparse.ArgumentParser(
|
|
205
|
+
description="Merge bootloader + app HEX and optionally patch signature/debug config bits."
|
|
206
|
+
)
|
|
207
|
+
parser.add_argument("boot", help="Bootloader HEX file")
|
|
208
|
+
parser.add_argument("app", help="App HEX file")
|
|
209
|
+
parser.add_argument("out", help="Output merged HEX file")
|
|
210
|
+
parser.add_argument(
|
|
211
|
+
"--sig-addr",
|
|
212
|
+
type=lambda x: int(x, 0),
|
|
213
|
+
default=None,
|
|
214
|
+
metavar="ADDR",
|
|
215
|
+
help="Physical address for signature word patch (e.g. 0x1D0FFFF8). Omit to skip.",
|
|
216
|
+
)
|
|
217
|
+
parser.add_argument(
|
|
218
|
+
"--sig-word",
|
|
219
|
+
type=lambda x: int(x, 0),
|
|
220
|
+
default=0x00000000,
|
|
221
|
+
metavar="VALUE",
|
|
222
|
+
help="Signature word value (default: 0x00000000)",
|
|
223
|
+
)
|
|
224
|
+
parser.add_argument(
|
|
225
|
+
"--ejtag-addr",
|
|
226
|
+
type=lambda x: int(x, 0),
|
|
227
|
+
default=None,
|
|
228
|
+
metavar="ADDR",
|
|
229
|
+
help="Physical address of DEVCFG0 for EJTAG enable (e.g. 0x1FC03FCC). Omit to skip.",
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
args = parser.parse_args()
|
|
233
|
+
merge(args.boot, args.app, args.out,
|
|
234
|
+
sig_addr=args.sig_addr, sig_word=args.sig_word, ejtag_addr=args.ejtag_addr)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
if __name__ == "__main__":
|
|
238
|
+
main()
|