From f8cc1a0b4b4e7376ec25d334488eb462c3bdc5cd Mon Sep 17 00:00:00 2001 From: ellensp <530024+ellensp@users.noreply.github.com> Date: Tue, 10 Feb 2026 19:45:01 +1300 Subject: [PATCH] =?UTF-8?q?=F0=9F=A7=91=E2=80=8D=F0=9F=92=BB=20Memory=20Ma?= =?UTF-8?q?p=20Visualizer=20(#28301)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../share/scripts/visualize_memory_map.py | 1562 +++++++++++++++++ 1 file changed, 1562 insertions(+) create mode 100644 buildroot/share/scripts/visualize_memory_map.py diff --git a/buildroot/share/scripts/visualize_memory_map.py b/buildroot/share/scripts/visualize_memory_map.py new file mode 100644 index 0000000000..c74e396286 --- /dev/null +++ b/buildroot/share/scripts/visualize_memory_map.py @@ -0,0 +1,1562 @@ +#!/usr/bin/env python3 +# +# Visualize memory map +# +# Memory Map Visualizer for Marlin Firmware (AVR and ARM/STM32) +# Generates an interactive HTML page showing memory layout with color coding +# Uses nm to extract symbols from ELF file +# +# Author: Dust +# Additional contributor: Thomas Toka (Windows support) +# Implementation assistance: Claude Sonnet 4.5 +# + +import re +import sys +import subprocess +import argparse +import struct +import os +import shutil +from pathlib import Path +from collections import defaultdict +import glob + +def parse_size(size_str): + """Parse size string with optional KB/MB suffix to bytes. + + Examples: + '512KB' -> 524288 + '20KB' -> 20480 + '256' -> 256 + '1MB' -> 1048576 + """ + size_str = str(size_str).strip().upper() + + # Match number with optional unit + match = re.match(r'^([0-9.]+)\s*(B|KB|K|MB|M)?$', size_str) + if not match: + raise argparse.ArgumentTypeError(f"Invalid size format: {size_str}. Use formats like '512KB', '20KB', or '262144'") + + value = float(match.group(1)) + unit = match.group(2) or 'B' + + # Convert to bytes + if unit in ('B', ''): + return int(value) + elif unit in ('KB', 'K'): + return int(value * 1024) + elif unit in ('MB', 'M'): + return int(value * 1024 * 1024) + else: + raise argparse.ArgumentTypeError(f"Unknown unit: {unit}") + +def detect_architecture(elf_path): + """Detect if ELF is AVR or ARM by reading ELF header directly (cross-platform)""" + try: + with open(elf_path, 'rb') as f: + # Read ELF header + elf_header = f.read(20) # First 20 bytes contain what we need + + # Check ELF magic number + if elf_header[:4] != b'\x7fELF': + print(f"Warning: Not a valid ELF file: {elf_path}", file=sys.stderr) + return 'avr' # Default to AVR + + # ELF class (32-bit or 64-bit) is at byte 4 + # Machine type is at bytes 18-19 (little endian) + e_machine = struct.unpack('/.pio/packages/... + 3) User PlatformIO: ~.platformio/packages/... + 4) Fallback to bare tool name (keeps legacy behavior) + """ + exe = '.exe' if os.name == 'nt' else '' + tool = f'avr-nm{exe}' if arch == 'avr' else f'arm-none-eabi-nm{exe}' + + # 1) PATH + found = shutil.which(tool) + if found: + return found + + # Script path: /buildroot/share/scripts/visualize_memory_map.py + # Project root is 3 parents up from scripts/ + script_dir = Path(__file__).resolve().parent + try: + project_root = script_dir.parents[3] + except IndexError: + project_root = Path.cwd() + + # 2) Project-local PlatformIO + pio_packages = project_root / '.pio' / 'packages' + + # 3) User PlatformIO + user_packages = Path.home() / '.platformio' / 'packages' + + if arch == 'avr': + candidates = [ + pio_packages / 'toolchain-atmelavr' / 'bin' / tool, + user_packages / 'toolchain-atmelavr' / 'bin' / tool, + ] + else: + candidates = [ + pio_packages / 'toolchain-gccarmnoneeabi' / 'bin' / tool, + user_packages / 'toolchain-gccarmnoneeabi' / 'bin' / tool, + ] + + for c in candidates: + if c.exists(): + return str(c) + + # 4) Fallback + return tool + +def normalize_address(addr, arch): + """Normalize addresses for different architectures + STM32: Flash at 0x08000000, RAM at 0x20000000 + AVR: Flash at 0x00000000, RAM at 0x00800000+ (varies by chip) + """ + if arch == 'arm': + # STM32 Flash starts at 0x08000000, RAM at 0x20000000 + if addr >= 0x20000000: # RAM + return addr - 0x20000000 + elif addr >= 0x08000000: # Flash + return addr - 0x08000000 + elif arch == 'avr': + # AVR RAM typically starts at 0x00800000 or higher + # Flash starts at 0x00000000 + if addr >= 0x00800000: # RAM region + return addr - 0x00800000 + return addr + +def get_memory_type_arm(addr): + """Determine if ARM address is Flash or RAM""" + if addr >= 0x20000000: + return 'ram' + else: # 0x08000000 range + return 'flash' + +def parse_nm_output(elf_path, arch): + """Parse nm output to extract all symbols with sizes""" + symbols = [] + special_symbols = {} # For heap, stack, bss markers + zero_size_important = [] # For important symbols that nm doesn't report size for + + nm_tool = find_nm_tool(arch) + + # Early, explicit failure with a helpful hint (prevents WinError 2 confusion) + nm_tool_path = Path(nm_tool) + if not nm_tool_path.exists() and shutil.which(nm_tool) is None: + print(f"Error: nm tool not found: {nm_tool}", file=sys.stderr) + print("Hint: Build once with PlatformIO so .pio/packages is populated, or add the toolchain bin directory to PATH.", file=sys.stderr) + print("On Windows for ARM builds, expected path looks like: .pio\\packages\\toolchain-gccarmnoneeabi\\bin\\arm-none-eabi-nm.exe", file=sys.stderr) + sys.exit(1) + + # First, get special symbols and important zero-size markers (without --print-size) + try: + result = subprocess.run( + [nm_tool, '--demangle', str(elf_path)], + capture_output=True, + text=True, + check=True + ) + + for line in result.stdout.strip().split('\n'): + parts = line.split(None, 2) + if len(parts) >= 3: + addr_str, sym_type, name = parts + addr_raw = int(addr_str, 16) + + # Capture special symbols for heap/stack display + if name in ['__heap_start', '__heap_end', '__bss_end', '_end', '__stack', '__data_end', '__bss_start', '_estack', '_sstack']: + # Normalize address for architecture + addr = normalize_address(addr_raw, arch) + special_symbols[name] = addr + + # Capture important zero-size symbols that define memory regions + elif name in ['g_pfnVectors', '__vectors', '__trampolines_start', '__trampolines_end', + '__ctors_start', '__ctors_end', '__dtors_start', '__dtors_end', + '__init', '__fini', '_sidata', '_sdata', '_edata', '_sbss', '_ebss']: + zero_size_important.append({ + 'name': name, + 'addr_raw': addr_raw, + 'addr': normalize_address(addr_raw, arch), + 'sym_type': sym_type, + 'size': 0 + }) + except subprocess.CalledProcessError as e: + print(f"Error running {nm_tool} for special symbols: {e}", file=sys.stderr) + + if special_symbols: + print(f" Special symbols found: {list(special_symbols.keys())}") + for name, addr in special_symbols.items(): + print(f" {name}: 0x{addr:08x}") + + # Now get regular symbols with sizes (using size-sort for better ordering) + try: + result = subprocess.run( + [nm_tool, '--size-sort', '--print-size', '--demangle', str(elf_path)], + capture_output=True, + text=True, + check=True + ) + + for line in result.stdout.strip().split('\n'): + # Format: address size type name + # Example: 00001234 00000100 T _ZN10GcodeSuite3G28Ev + parts = line.split(None, 3) + if len(parts) >= 4: + addr_str, size_str, sym_type, name = parts + addr_raw = int(addr_str, 16) + size = int(size_str, 16) + + # Skip zero-size symbols EXCEPT for important startup symbols + # These mark important regions that nm doesn't report sizes for + if size == 0 and name not in ['__vectors', '__trampolines_start', '__trampolines_end', + '__ctors_start', '__ctors_end', '__dtors_start', '__dtors_end', + '__init', '__fini', 'g_pfnVectors']: + continue + + # Normalize address for architecture + addr = normalize_address(addr_raw, arch) + + # Categorize by symbol type + # t/T = text (code), d/D = initialized data, b/B = uninitialized data (BSS) + # r/R = read-only data, V = weak object + section = '' + if sym_type in 'tTrR': + # For ARM, check actual address to determine section + if arch == 'arm': + mem_type = get_memory_type_arm(addr_raw) + if mem_type == 'flash': + section = '.text' if sym_type in 'tT' else '.rodata' + else: + section = '.data' # Shouldn't happen for t/T/r/R but handle it + else: + section = '.text' if sym_type in 'tT' else '.rodata' + elif sym_type in 'dD': + section = '.data' + elif sym_type in 'bB': + section = '.bss' + elif sym_type in 'vV': # Weak objects + # For ARM, check actual address + if arch == 'arm': + mem_type = get_memory_type_arm(addr_raw) + section = '.data' if mem_type == 'ram' else '.rodata' + else: + section = '.data' # Usually in RAM for weak objects + else: + # For ARM, check actual address for unknown types + if arch == 'arm': + mem_type = get_memory_type_arm(addr_raw) + section = '.data' if mem_type == 'ram' else '.text' + else: + section = '.other' + + # Extract module name from demangled name + module = categorize_symbol(name) + + symbols.append({ + 'name': name, + 'addr': addr, + 'addr_raw': addr_raw, # Store original address for display + 'size': size, + 'section': section, + 'module': module, + 'type': 'symbol', + 'sym_type': sym_type + }) + except subprocess.CalledProcessError as e: + print(f"Error running nm: {e}", file=sys.stderr) + sys.exit(1) + + # Add important zero-size symbols that were captured earlier + for zero_sym in zero_size_important: + # Determine section based on symbol type and address + sym_type = zero_sym['sym_type'] + addr_raw = zero_sym['addr_raw'] + + if sym_type in 'tTrR': + if arch == 'arm': + mem_type = get_memory_type_arm(addr_raw) + section = '.text' if sym_type in 'tT' else '.rodata' if mem_type == 'flash' else '.data' + else: + section = '.text' if sym_type in 'tT' else '.rodata' + elif sym_type in 'dD': + section = '.data' + elif sym_type in 'bB': + section = '.bss' + else: + if arch == 'arm': + mem_type = get_memory_type_arm(addr_raw) + section = '.data' if mem_type == 'ram' else '.text' + else: + section = '.other' + + module = categorize_symbol(zero_sym['name']) + + symbols.append({ + 'name': zero_sym['name'], + 'addr': zero_sym['addr'], + 'addr_raw': zero_sym['addr_raw'], + 'size': 0, + 'section': section, + 'module': module, + 'type': 'symbol', + 'sym_type': sym_type + }) + + # Post-process: Calculate sizes for zero-size symbols based on next symbol + # Sort symbols by address + symbols.sort(key=lambda x: x['addr']) + + for i, sym in enumerate(symbols): + if sym['size'] == 0 and i + 1 < len(symbols): + # Find next symbol with non-zero address in same memory space + _, _, mem_type = categorize_section(sym['section']) + for next_sym in symbols[i + 1:]: + _, _, next_mem_type = categorize_section(next_sym['section']) + # Look for next symbol in same address space + if next_mem_type == mem_type and next_sym['addr'] > sym['addr']: + # Calculate size as gap to next symbol + calculated_size = next_sym['addr'] - sym['addr'] + if calculated_size > 0 and calculated_size < 100000: # Sanity check + sym['size'] = calculated_size + # Update name to show it's important + if sym['name'] == '__vectors': + sym['name'] = 'Interrupt Vectors (__vectors)' + sym['module'] = 'Core' + elif '__trampolines' in sym['name']: + sym['module'] = 'Core' + break + + return symbols, special_symbols + +def categorize_symbol(name, warn_conflicts=False): + """Categorize symbol by module based on name patterns""" + name_lower = name.lower() + + # Pattern definitions with their categories + # NOTE: Order matters! More specific patterns should come before general ones + patterns = [ + (['ubl', 'unified_bed_leveling'], 'UBL'), + (['tmcstepper', 'tmc2130', 'tmc2208', 'tmc2209', 'tmc2660', 'tmc5160', 'tmcmarlin'], 'TMCLibrary'), + (['planner'], 'Planner'), + (['stepper'], 'Stepper'), + (['temperature'], 'Temperature'), + (['ftmotion', 'ft_motion'], 'FT_Motion'), + (['motion', 'homing'], 'Motion'), + (['gcode', 'gcodesuite'], 'GCode'), + (['lcd', 'ui', 'marlinui'], 'LCD/UI'), + (['probe'], 'Probe'), + (['endstop'], 'Endstops'), + (['serial'], 'Serial'), + (['settings', 'eeprom'], 'Settings'), + (['marlincore'], 'Core'), # Note: 'marlin::' checked separately + ] + + matches = [] + + # Check all patterns + for pattern_list, category in patterns: + for pattern in pattern_list: + if pattern in name_lower: + matches.append((category, pattern)) + break # Only need one match per category + + # Special case for marlin:: prefix (case-sensitive) + if 'marlin::' in name: + matches.append(('Core', 'marlin::')) + + # Warn if multiple categories match + if warn_conflicts and len(matches) > 1: + categories = [m[0] for m in matches] + patterns_matched = [m[1] for m in matches] + print(f" ⚠ Ambiguous: '{name}' matches {len(matches)} categories: {', '.join([f'{c} ({p})' for c, p in matches])} → using {categories[0]}") + + # Return first match or 'Other' + if matches: + return matches[0][0] + else: + return 'Other' + +def categorize_section(section_name): + """Categorize section for color coding""" + if section_name.startswith('.text'): + return 'code', '#4CAF50', 'flash' # Green for code + elif section_name.startswith('.data'): + return 'data', '#2196F3', 'ram' # Blue for initialized data + elif section_name.startswith('.bss'): + return 'bss', '#FF9800', 'ram' # Orange for uninitialized data + elif section_name.startswith('.rodata'): + return 'rodata', '#9C27B0', 'flash' # Purple for read-only data + else: + return 'other', '#757575', 'flash' # Gray for other + +def generate_memory_blocks_html(items, module_colors, zoom_level=1, total_memory_size=None, special_symbols=None, arch='avr'): + """Generate HTML for memory blocks as a byte-by-byte map + zoom_level: multiplier (1x = 2 pixels per byte + 1px delimiter, 2x = 4 pixels + 2px delimiter, etc.) + total_memory_size: total available memory (to show unused space at end) + special_symbols: dict of special symbols like __heap_start, __stack for RAM visualization + arch: architecture ('avr' or 'arm') to determine address display format + """ + if not items: + return '
No data
' + + # Calculate actual pixels per byte and delimiter size + pixels_per_byte = zoom_level * 2 + delimiter_width = zoom_level * 1 + + # Find memory range + min_addr = min(item['addr'] for item in items) + max_addr = max(item['addr'] + item['size'] for item in items) + + # Determine address base for ARM architecture + addr_base = 0 + if arch == 'arm' and items: + # Check if this is RAM or Flash by looking at items' original addresses + # Find first item that has addr_raw set + for item in items: + if 'addr_raw' in item: + if item['addr_raw'] >= 0x20000000: + addr_base = 0x20000000 # RAM + else: + addr_base = 0x08000000 # Flash + break + + # If total memory size specified, extend to show full memory + if total_memory_size: + total_size = total_memory_size + else: + total_size = max_addr - min_addr + + # Create byte array for memory map + memory_map = [None] * total_size + + # Fill in the memory map with symbol info + for item in items: + start = item['addr'] - min_addr + end = start + item['size'] + _, default_color, _ = categorize_section(item['section']) + color = module_colors.get(item['module'], default_color) + + for i in range(start, min(end, total_size)): + memory_map[i] = { + 'color': color, + 'name': item['name'], + 'addr': item['addr'], + 'addr_raw': item.get('addr_raw', item['addr']), # Store original address for ARM display + 'size': item['size'], + 'section': item['section'], + 'module': item['module'] + } + + # Add heap/stack regions if this is RAM and we have special symbols + if special_symbols: + heap_start = special_symbols.get('__heap_start', special_symbols.get('_end', special_symbols.get('__bss_end'))) + + # Stack grows downward from top of RAM on AVR + # Reserve top portion of RAM for stack (typically starts 256-512 bytes from top) + if heap_start: + # Check if heap_start is within our RAM range + heap_start_idx = heap_start - min_addr + + if 0 <= heap_start_idx < total_size: + # Stack reserve: last 256 bytes of RAM + stack_reserve_bytes = 256 + stack_start_idx = total_size - stack_reserve_bytes + + # Heap region: from end of BSS to start of stack reserve + heap_end_idx = stack_start_idx + + # Mark heap region + for i in range(heap_start_idx, heap_end_idx): + if 0 <= i < total_size and memory_map[i] is None: + addr = min_addr + i + memory_map[i] = { + 'color': '#404040', + 'name': 'Heap (available)', + 'addr': addr, + 'addr_raw': addr + addr_base if arch == 'arm' else addr, + 'size': heap_end_idx - heap_start_idx, + 'section': '.heap', + 'module': 'Heap' + } + + # Mark stack reserve region (top 256 bytes) + for i in range(stack_start_idx, total_size): + if 0 <= i < total_size and memory_map[i] is None: + addr = min_addr + i + memory_map[i] = { + 'color': '#505050', + 'name': 'Stack (reserve)', + 'addr': addr, + 'addr_raw': addr + addr_base if arch == 'arm' else addr, + 'size': stack_reserve_bytes, + 'section': '.stack', + 'module': 'Stack' + } + + # Generate HTML as rows of pixels + html = '' + # Calculate bytes per row to keep display width constant at ~1000 pixels + bytes_per_row = 1000 // pixels_per_byte + + # Iterate through memory in rows + for row_start in range(0, total_size, bytes_per_row): + row_end = min(row_start + bytes_per_row, total_size) + html += '
\n' + + # Iterate through each byte in this row + i = row_start + while i < row_end: + byte_info = memory_map[i] + + # Find consecutive bytes with same data + count = 1 + while (i + count < row_end and + ((byte_info is None and memory_map[i + count] is None) or + (byte_info is not None and memory_map[i + count] is not None and + memory_map[i + count]['name'] == byte_info['name']))): + count += 1 + + if byte_info is None: + # Unallocated or unused space + addr = i + min_addr + # For ARM, add back the base address for display + display_addr = addr + addr_base if arch == 'arm' else addr + # Check if next byte is different (need delimiter) + next_i = i + count + needs_delimiter = False + if next_i < row_end: + next_info = memory_map[next_i] + if next_info is not None: + needs_delimiter = True + + if addr >= max_addr: + # Unused space at end + if needs_delimiter: + html += f'
\n' + html += f'
\n' + else: + html += f'
\n' + else: + # Gap between symbols - likely linker-inserted code, padding, or alignment + if needs_delimiter: + html += f'
\n' + html += f'
\n' + else: + html += f'
\n' + else: + # Symbol data + display_name = byte_info['name'].replace('&', '&').replace('<', '<').replace('>', '>').replace('"', '"') + + # Check if next byte is different (need delimiter) + next_i = i + count + needs_delimiter = False + if next_i < row_end: + next_info = memory_map[next_i] + if next_info is None or next_info['name'] != byte_info['name']: + needs_delimiter = True + + # For display: ARM uses raw addresses (0x08000000/0x20000000 ranges), AVR uses normalized + display_addr = byte_info['addr_raw'] if arch == 'arm' else byte_info['addr'] + + if needs_delimiter: + html += f'''
+''' + html += f'
\n' + else: + html += f'''
+''' + + i += count + + html += '
\n' + + return html + +def check_pattern_conflicts(): + """Check for pattern conflicts and warn about potential issues""" + patterns = [ + (['ubl', 'unified_bed_leveling'], 'UBL'), + (['planner'], 'Planner'), + (['stepper'], 'Stepper'), + (['temperature'], 'Temperature'), + (['ftmotion', 'ft_motion'], 'FT_Motion'), + (['motion', 'homing'], 'Motion'), + (['gcode', 'gcodesuite'], 'GCode'), + (['lcd', 'ui', 'marlinui'], 'LCD/UI'), + (['probe'], 'Probe'), + (['endstop'], 'Endstops'), + (['serial'], 'Serial'), + (['settings', 'eeprom'], 'Settings'), + (['marlincore'], 'Core'), + ] + + conflicts = [] + + # Check for substring overlaps + for i, (patterns1, cat1) in enumerate(patterns): + for j, (patterns2, cat2) in enumerate(patterns): + if i >= j: # Only check each pair once + continue + for p1 in patterns1: + for p2 in patterns2: + # Check if one is substring of the other + if p1 in p2 or p2 in p1: + # Determine which comes first in the list (has priority) + if i < j: + conflicts.append((p1, cat1, p2, cat2, 'priority')) + else: + conflicts.append((p2, cat2, p1, cat1, 'priority')) + + if conflicts: + print("Pattern conflict analysis:") + for p1, c1, p2, c2, conflict_type in conflicts: + print(f" ℹ '{p1}' ({c1}) vs '{p2}' ({c2}) - '{p1}' has priority") + + return len(conflicts) + +def generate_csv(symbols, flash_size, ram_size, output_dir='.', arch='avr'): + """Generate CSV files for RAM and Flash memory usage""" + import csv + + flash_items = [] + ram_items = [] + + # Separate symbols by memory type + for item in symbols: + _, _, mem_type = categorize_section(item['section']) + if mem_type == 'flash': + flash_items.append(item) + else: + ram_items.append(item) + + # Sort by address (lowest first) + flash_items.sort(key=lambda x: x['addr']) + ram_items.sort(key=lambda x: x['addr']) + + # Calculate totals for percentages + total_flash = sum(item['size'] for item in flash_items) + total_ram = sum(item['size'] for item in ram_items) + + # Generate Flash CSV + flash_csv_path = Path(output_dir) / 'flash.csv' + with open(flash_csv_path, 'w', newline='', encoding='utf-8') as f: + writer = csv.writer(f) + writer.writerow(['Address', 'Size', 'Percentage', 'Name']) + + for item in flash_items: + # For AVR use normalized address, for ARM use raw address + addr = item['addr_raw'] if arch == 'arm' else item['addr'] + size = item['size'] + percentage = (size / total_flash * 100) if total_flash > 0 else 0 + name = item['name'] + writer.writerow([f'0x{addr:08x}', size, f'{percentage:.3f}', name]) + + print(f"✓ Generated Flash CSV: {flash_csv_path}") + print(f" Entries: {len(flash_items):,}") + + # Generate RAM CSV + ram_csv_path = Path(output_dir) / 'ram.csv' + with open(ram_csv_path, 'w', newline='', encoding='utf-8') as f: + writer = csv.writer(f) + writer.writerow(['Address', 'Size', 'Percentage', 'Name']) + + for item in ram_items: + # For AVR use normalized address, for ARM use raw address + addr = item['addr_raw'] if arch == 'arm' else item['addr'] + size = item['size'] + percentage = (size / total_ram * 100) if total_ram > 0 else 0 + name = item['name'] + writer.writerow([f'0x{addr:08x}', size, f'{percentage:.3f}', name]) + + print(f"✓ Generated RAM CSV: {ram_csv_path}") + print(f" Entries: {len(ram_items):,}") + +def generate_html(symbols, special_symbols, output_path, flash_size, ram_size, arch='avr', show_list=False): + """Generate interactive HTML visualization""" + + # Calculate total memory and gather stats + total_flash = 0 + total_ram = 0 + module_stats = defaultdict(lambda: {'flash': 0, 'ram': 0}) + + flash_items = [] + ram_items = [] + + for item in symbols: + cat, color, mem_type = categorize_section(item['section']) + if mem_type == 'flash': + total_flash += item['size'] + module_stats[item['module']]['flash'] += item['size'] + flash_items.append(item) + else: # RAM + total_ram += item['size'] + module_stats[item['module']]['ram'] += item['size'] + ram_items.append(item) + + # Sort by address for linear display + flash_items.sort(key=lambda x: x['addr']) + ram_items.sort(key=lambda x: x['addr']) + + # Memory sizes from command line arguments + FLASH_SIZE = flash_size + RAM_SIZE = ram_size + + print(f" Special symbols found: {list(special_symbols.keys())}") + if special_symbols: + for sym, addr in special_symbols.items(): + print(f" {sym}: 0x{addr:08x}") + + # Calculate section ranges + section_ranges = {} + for item in symbols: + section = item['section'] + addr = item['addr'] + end_addr = addr + item['size'] + + if section not in section_ranges: + section_ranges[section] = {'start': addr, 'end': end_addr, 'type': categorize_section(section)[2]} + else: + section_ranges[section]['start'] = min(section_ranges[section]['start'], addr) + section_ranges[section]['end'] = max(section_ranges[section]['end'], end_addr) + + # Add heap and stack to section ranges if available + if special_symbols: + heap_start = special_symbols.get('__heap_start', special_symbols.get('_end', special_symbols.get('__bss_end'))) + if heap_start and ram_items: + # Find RAM base address + ram_base = min(item['addr'] for item in ram_items) + + # For ARM, check if we have _estack which marks the end of RAM/top of stack + if '_estack' in special_symbols: + # _estack marks the top of the stack area (end of RAM) + stack_end = special_symbols['_estack'] + # Reserve some stack space (ARM typically has larger stacks) + stack_reserve_bytes = min(2048, (RAM_SIZE - (heap_start - ram_base)) // 2) + stack_start = stack_end - stack_reserve_bytes + heap_end = stack_start + else: + # AVR-style: reserve fixed stack space at end of RAM + stack_reserve_bytes = 256 + heap_end = ram_base + RAM_SIZE - stack_reserve_bytes + stack_start = heap_end + stack_end = ram_base + RAM_SIZE + + section_ranges['.heap'] = {'start': heap_start, 'end': heap_end, 'type': 'ram'} + section_ranges['.stack'] = {'start': stack_start, 'end': stack_end, 'type': 'ram'} + + print(f" Heap: 0x{heap_start:08x} - 0x{heap_end:08x} ({heap_end - heap_start} bytes)") + print(f" Stack: 0x{stack_start:08x} - 0x{stack_end:08x} ({stack_end - stack_start} bytes)") + + # Module colors + module_colors = { + 'UBL': '#4CAF50', + 'Planner': '#2196F3', + 'Stepper': '#FF5722', + 'Temperature': '#FF9800', + 'FT_Motion': '#E91E63', + 'Motion': '#9C27B0', + 'GCode': '#00BCD4', + 'LCD/UI': '#FFEB3B', + 'Probe': '#8BC34A', + 'Endstops': '#FF6F00', + 'Serial': '#3F51B5', + 'Settings': '#009688', + 'TMCLibrary': "#C10091", + 'Core': '#795548', + 'Other': '#757575' + } + + # Generate HTML + html = f""" + + + + Marlin Memory Map Visualization + + + +
+

Marlin Firmware Memory Map

+
+
+
{total_ram:,}
+
RAM Used ({100.0 * total_ram / RAM_SIZE:.1f}% of {RAM_SIZE:,})
+
+
+
{total_flash:,}
+
Flash Used ({100.0 * total_flash / FLASH_SIZE:.1f}% of {FLASH_SIZE:,})
+
+
+
{len(symbols):,}
+
Symbols
+
+
+
+ +
+ + + Zoom: + + + + +
+ +
+
+
+

Flash Memory Layout (hover for details)

+
+{generate_memory_blocks_html(flash_items, module_colors, 1, FLASH_SIZE, None, arch)}
+ + + +
+ +
+

RAM Layout (hover for details)

+
+{generate_memory_blocks_html(ram_items, module_colors, 1, RAM_SIZE, special_symbols, arch)}
+ + + +
+
+ +
+

Modules

+""" + + # Add module legend + for module, color in sorted(module_colors.items()): + flash_size = module_stats[module]['flash'] + ram_size = module_stats[module]['ram'] + if flash_size > 0 or ram_size > 0: + html += f'''
+
+ {module} +
+''' + + html += """ +
+

Module Statistics

+""" + + # Add module statistics sorted by total size + for module, stats in sorted(module_stats.items(), key=lambda x: x[1]['flash'] + x[1]['ram'], reverse=True): + if stats['flash'] > 0 or stats['ram'] > 0: + html += f'''
+
{module}
+
Flash: {stats['flash']:,} bytes | RAM: {stats['ram']:,} bytes
+
+''' + + html += """
+
+

Section Map

+ + + + + + + + + + +""" + + # Add section rows sorted by start address + for section, info in sorted(section_ranges.items(), key=lambda x: x[1]['start']): + size = info['end'] - info['start'] + mem_type = info['type'].upper() + html += f''' + + + + + +''' + + html += """ +
SectionStartEndSize
{section}0x{info['start']:08x}0x{info['end']:08x}{size:,}
+
+
+""" + + # Add memory list tables if requested + if show_list: + # Sort items by address + flash_items_sorted = sorted(flash_items, key=lambda x: x['addr']) + ram_items_sorted = sorted(ram_items, key=lambda x: x['addr']) + + # Calculate totals for percentages + total_flash_used = sum(item['size'] for item in flash_items_sorted) + total_ram_used = sum(item['size'] for item in ram_items_sorted) + + html += """ +
+

Memory Usage Details

+ +
+ + +
+ +
+

RAM Symbols (""" + f"{len(ram_items_sorted):,}" + """ entries)

+
+ + + + + + + + + + +""" + + for item in ram_items_sorted: + # For AVR use normalized address, for ARM use raw address + addr = item['addr_raw'] if arch == 'arm' else item['addr'] + size = item['size'] + percentage = (size / total_ram_used * 100) if total_ram_used > 0 else 0 + name = item['name'].replace('&', '&').replace('<', '<').replace('>', '>').replace('"', '"') + html += f''' + + + + + +''' + + html += """ +
+ Address ▲▼ + + Size (bytes) ▲▼ + + Percentage ▲▼ + + Name ▲▼ +
0x{addr:08x}{size:,}{percentage:.3f}%{name}
+
+
+ + +
+""" + + html += """ +
+
+
+
+ + + + +""" + + with open(output_path, 'w') as f: + f.write(html) + + print(f"✓ Generated memory visualization: {output_path}") + print(f" Total Flash: {total_flash:,} bytes") + print(f" Total RAM: {total_ram:,} bytes") + print(f" Symbols: {len(symbols):,}") + print(f" Flash symbols: {len(flash_items):,}") + print(f" RAM symbols: {len(ram_items):,}") + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description='Generate interactive memory map visualization from AVR/ARM ELF file', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog='''Examples: + # Use defaults (256KB Flash/8KB RAM for AVR, 512KB Flash/20KB RAM for ARM) + %(prog)s firmware.elf output.html + + # ATmega1284 with 128KB Flash, 16KB RAM + %(prog)s firmware.elf output.html --flash 128KB --ram 16KB + + # STM32F407 with 1MB Flash, 128KB RAM + %(prog)s firmware.elf output.html --flash 1MB --ram 128KB + + # Check for pattern conflicts and ambiguous symbol matches + %(prog)s firmware.elf output.html --flash 512KB --ram 20KB --warn-conflicts +''' + ) + + parser.add_argument('elf_file', type=Path, + help='Path to the ELF file to analyze') + parser.add_argument('output_file', type=Path, nargs='?', default=Path('memory_map.html'), + help='Output HTML file path (default: memory_map.html)') + parser.add_argument('--flash', type=parse_size, default=None, + help='Total Flash memory size (e.g., 256KB, 512KB, 262144 bytes) (default: 256KB for AVR, 512KB for ARM)') + parser.add_argument('--ram', type=parse_size, default=None, + help='Total RAM size (e.g., 8KB, 20KB, 8192 bytes) (default: 8KB for AVR, 20KB for ARM)') + parser.add_argument('--arch', choices=['avr', 'arm', 'auto'], default='auto', + help='Architecture (default: auto-detect)') + parser.add_argument('--warn-conflicts', action='store_true', + help='Warn about symbols that match multiple category patterns') + parser.add_argument('--csv', action='store_true', + help='Generate CSV files (flash.csv and ram.csv) with memory usage data') + parser.add_argument('--list', action='store_true', + help='Display memory usage data in HTML tables below the visualization') + + args = parser.parse_args() + + if not args.elf_file.exists(): + print(f"Error: ELF file not found: {args.elf_file}") + sys.exit(1) + + # Detect architecture + if args.arch == 'auto': + arch = detect_architecture(args.elf_file) + print(f"Detected architecture: {arch.upper()}") + else: + arch = args.arch + print(f"Using specified architecture: {arch.upper()}") + + # Set default sizes based on architecture if not specified + if args.flash is None: + args.flash = 256*1024 if arch == 'avr' else 512*1024 # ATmega2560: 256KB, STM32F103RE: 512KB + if args.ram is None: + args.ram = 8*1024 if arch == 'avr' else 20*1024 # ATmega2560: 8KB, STM32F103RE: 20KB + + print(f"Memory sizes: Flash={args.flash:,} bytes, RAM={args.ram:,} bytes") + + # Check for pattern conflicts if requested + if args.warn_conflicts: + num_conflicts = check_pattern_conflicts() + if num_conflicts == 0: + print("✓ No pattern conflicts detected") + print() + + symbols, special_symbols = parse_nm_output(args.elf_file, arch) + + # Categorize with conflict warnings if requested + if args.warn_conflicts: + print("Checking for ambiguous symbol matches...") + for symbol in symbols: + symbol['module'] = categorize_symbol(symbol['name'], warn_conflicts=True) + print() + + # Generate CSV files if requested + if args.csv: + output_dir = args.output_file.parent if args.output_file.parent != Path('.') else Path('.') + generate_csv(symbols, args.flash, args.ram, output_dir, arch) + print() + + generate_html(symbols, special_symbols, args.output_file, args.flash, args.ram, arch, args.list)