#!/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 += """
Section Start End Size
{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)