"""Execute compiled STM32 firmware in Unicorn; HAL I/O is mocked, no hardware used. Requires scripts/requirements-test.txt and a built Debug or Release ELF. Tests CAN RX -> display state -> ASCII UART DMA commands using actual ARM code. """ import argparse from decimal import Decimal, ROUND_HALF_UP import hashlib import json from pathlib import Path import struct from elftools.elf.elffile import ELFFile from unicorn import Uc, UC_ARCH_ARM, UC_MODE_THUMB, UC_MODE_MCLASS, UC_HOOK_CODE from unicorn.arm_const import ( UC_ARM_REG_R0, UC_ARM_REG_R1, UC_ARM_REG_R2, UC_ARM_REG_R3, UC_ARM_REG_SP, UC_ARM_REG_LR, UC_ARM_REG_PC, UC_ARM_REG_XPSR, ) ROOT = Path(__file__).resolve().parents[1] REGS = (UC_ARM_REG_R0, UC_ARM_REG_R1, UC_ARM_REG_R2, UC_ARM_REG_R3) class Firmware: def __init__(self, path): self.uc = Uc(UC_ARCH_ARM, UC_MODE_THUMB | UC_MODE_MCLASS) self.uc.mem_map(0x08000000, 0x20000) self.uc.mem_map(0x20000000, 0x24000) self.uc.mem_map(0x40000000, 0x30000) self.uc.mem_map(0x50000000, 0x10000) with path.open('rb') as file: elf = ELFFile(file) self.symbols = {s.name: s['st_value'] for s in elf.get_section_by_name('.symtab').iter_symbols()} for segment in elf.iter_segments(): if segment['p_type'] == 'PT_LOAD' and segment['p_filesz']: self.uc.mem_write(segment['p_vaddr'], segment.data()) self.now = 800 self.rx = [] self.commands = [] self.hooks = {self.symbols[name] & ~1: name for name in ( 'HAL_GetTick', 'HAL_GPIO_WritePin', 'HAL_GPIO_TogglePin', 'HAL_FDCAN_GetRxFifoFillLevel', 'HAL_FDCAN_GetRxMessage', 'HAL_FDCAN_GetTxFifoFreeLevel', 'HAL_UART_Transmit_DMA', )} self.uc.hook_add(UC_HOOK_CODE, self.hook) for port, instance in ((1,0x40006400), (2,0x40006800)): self.write_u32(f'hfdcan{port}', instance) def hook(self, uc, address, size, data): name = self.hooks.get(address) if name is None: return result = 0 if name == 'HAL_GetTick': result = self.now elif name == 'HAL_FDCAN_GetRxFifoFillLevel': result = len(self.rx) elif name == 'HAL_FDCAN_GetRxMessage': header, payload = self.rx.pop(0) uc.mem_write(uc.reg_read(UC_ARM_REG_R2), header) uc.mem_write(uc.reg_read(UC_ARM_REG_R3), payload) elif name == 'HAL_UART_Transmit_DMA': command = bytes(uc.mem_read(uc.reg_read(UC_ARM_REG_R1), uc.reg_read(UC_ARM_REG_R2))) assert command.endswith(b'\xff\xff\xff'), command self.commands.append(command[:-3].decode('ascii')) # TX free level is zero: heartbeat never touches hardware in this test. uc.reg_write(UC_ARM_REG_R0, result) uc.reg_write(UC_ARM_REG_PC, uc.reg_read(UC_ARM_REG_LR)) def call(self, name, *args): for register, value in zip(REGS, args): self.uc.reg_write(register, value) self.uc.reg_write(UC_ARM_REG_SP, 0x20023000) self.uc.reg_write(UC_ARM_REG_LR, 0x0801FF01) self.uc.reg_write(UC_ARM_REG_XPSR, 1 << 24) self.uc.emu_start(self.symbols[name] | 1, 0x0801FF00, count=200000) assert self.uc.reg_read(UC_ARM_REG_PC) == 0x0801FF00, f'{name} exceeded instruction budget' return self.uc.reg_read(UC_ARM_REG_R0) def read_u32(self, name, offset=0): return struct.unpack('