#!/usr/bin/env python3 """ STM8 RAM Bootloader测试脚本 用于测试bootloader功能 工作流程: 1. 默认加载同级目录下的boot2.bin发送,可通过选项指定 2. 执行后,等待用户按下开发板的复位键 3. MCU复位后,会以波特率9600发送0x00 0x0D到python 4. 收到后开始发送boot2.bin,从最后一块开始向前发送,每发送128字节后,MCU会应答一个0x01 5. 发送完成后进入交互shell,用户可以输入命令执行响应的操作(先不实现) 用法: python3 stm8_bootloader_test.py [-p PORT] [-b BAUDRATE] [--bin BIN_FILE] """ import sys import os import time import argparse import serial import struct import colorama from colorama import Fore, Style from typing import Optional, List colorama.init(autoreset=True) class STM8BootloaderTester: def __init__(self, port: str, baudrate: int = 9600, bin_file: str = None): self.port = port self.baudrate = baudrate # 如果未指定bin文件,使用脚本同级目录下的boot2.bin if bin_file is None: script_dir = os.path.dirname(os.path.abspath(__file__)) self.bin_file = os.path.join(script_dir, "boot2.bin") else: self.bin_file = bin_file self.ser = None self.bootloader_size = 0 def open_serial(self) -> bool: """打开串口""" try: print(f"{Fore.CYAN}打开串口 {self.port}, 波特率 {self.baudrate}...") self.ser = serial.Serial( port=self.port, baudrate=self.baudrate, bytesize=serial.EIGHTBITS, parity=serial.PARITY_NONE, stopbits=serial.STOPBITS_ONE, timeout=1.0, # 读取超时1秒 write_timeout=1.0 # 写入超时1秒 ) if self.ser.is_open: print(f"{Fore.GREEN}串口打开成功!") return True else: print(f"{Fore.RED}串口打开失败!") return False except serial.SerialException as e: print(f"{Fore.RED}串口错误: {e}") return False def close_serial(self): """关闭串口""" if self.ser and self.ser.is_open: self.ser.close() print(f"{Fore.YELLOW}串口已关闭") def check_file_size(self, file_size: int, chunk_size: int = 128) -> bool: """检查文件大小是否为chunk_size的整数倍""" if file_size % chunk_size != 0: print(f"{Fore.RED}错误: 文件大小 {file_size} 不是 {chunk_size} 的整数倍!") print(f"{Fore.YELLOW}请确保bootloader大小为 {chunk_size} 字节的整数倍") return False return True def read_bin_file(self) -> Optional[bytes]: """读取bin文件""" try: if not os.path.exists(self.bin_file): print(f"{Fore.RED}错误: 文件 {self.bin_file} 不存在!") return None with open(self.bin_file, 'rb') as f: data = f.read() self.bootloader_size = len(data) print(f"{Fore.GREEN}读取 {self.bootloader_size} 字节来自 {self.bin_file}") # 检查文件大小是否为128的整数倍 if not self.check_file_size(self.bootloader_size): return None # 显示文件信息 if self.bootloader_size > 0: print(f"{Fore.CYAN}文件首16字节: {data[:16].hex(' ')}") if self.bootloader_size > 16: print(f"{Fore.CYAN}文件尾16字节: {data[-16:].hex(' ')}") return data except Exception as e: print(f"{Fore.RED}读取文件错误: {e}") return None def wait_for_mcu_ready(self, timeout: int = 30) -> bool: """等待MCU发送就绪信号 0x00 0x0D""" print(f"\n{Fore.YELLOW}等待MCU就绪信号...") print(f"{Fore.YELLOW}请按下开发板上的复位键!") print(f"{Fore.YELLOW}等待超时: {timeout} 秒") start_time = time.time() last_print_time = start_time expected_bytes = b'\x00\x0D' buffer = b'' try: while time.time() - start_time < timeout: current_time = time.time() elapsed = int(current_time - start_time) # 每5秒打印一次状态 if current_time - last_print_time >= 5: print(f"{Fore.YELLOW}已等待 {elapsed} 秒...") last_print_time = current_time # 检查串口是否有数据 if self.ser.in_waiting > 0: # 读取一个字节 byte = self.ser.read(self.ser.in_waiting) if byte: buffer += byte # 检查是否匹配就绪信号 if buffer[-2:] == expected_bytes: print(f"{Fore.GREEN}收到MCU就绪信号: {buffer.hex(' ')}") return True # 显示接收到的数据(调试用) if buffer: print(f"{Fore.CYAN}[调试] 接收缓冲区: {buffer.hex(' ')}") # 保留缓冲区最后一个字节 buffer = buffer[-1:] time.sleep(0.01) except Exception as e: print(f"{Fore.RED}等待MCU就绪时出错: {e}") print(f"{Fore.RED}超时! 未收到MCU就绪信号") return False def reverse_bytes_within_chunks(self, data: bytes, chunk_size: int = 128) -> bytes: """在每个128字节块内反转字节顺序""" if len(data) % chunk_size != 0: raise ValueError(f"数据长度({len(data)})必须是{chunk_size}的整数倍") result = bytearray() num_chunks = len(data) // chunk_size for i in range(num_chunks): start_idx = i * chunk_size end_idx = start_idx + chunk_size chunk = data[start_idx:end_idx] # 反转块内字节顺序 reversed_chunk = bytes(reversed(chunk)) result.extend(reversed_chunk) return bytes(result) def send_bootloader_reverse(self, data: bytes, chunk_size: int = 128) -> bool: """反向发送bootloader数据(从最后一块开始,每块内字节也反转)""" if not data: print(f"{Fore.RED}错误: 没有数据可发送") return False # 首先在每个128字节块内反转字节顺序 reversed_data = self.reverse_bytes_within_chunks(data, chunk_size) total_size = len(reversed_data) num_chunks = total_size // chunk_size print(f"\n{Fore.CYAN}开始反向发送bootloader...") print(f"{Fore.CYAN}总大小: {total_size} 字节") print(f"{Fore.CYAN}分块大小: {chunk_size} 字节") print(f"{Fore.CYAN}总块数: {num_chunks}") print(f"{Fore.CYAN}发送顺序: 从最后一块到第一块,每块内字节顺序已反转") success_count = 0 fail_count = 0 try: # 从最后一块开始发送 for i in range(num_chunks - 1, -1, -1): start_idx = i * chunk_size end_idx = start_idx + chunk_size chunk = reversed_data[start_idx:end_idx] # 原始块索引(反转前) original_chunk_index = i original_start_addr = original_chunk_index * chunk_size print(f"\n{Fore.YELLOW}[块 {num_chunks-i}/{num_chunks}]") print(f"{Fore.CYAN} 原始位置: 0x{original_start_addr:04X}-0x{original_start_addr+chunk_size-1:04X}") print(f"{Fore.CYAN} 发送大小: {len(chunk)} 字节") print(f"{Fore.CYAN} 数据(已反转): {chunk[:16].hex(' ')}" + ("..." if len(chunk) > 16 else "")) # 发送数据块 bytes_sent = self.ser.write(chunk) if bytes_sent != len(chunk): print(f"{Fore.RED} 发送失败! 期望 {len(chunk)} 字节, 实际 {bytes_sent} 字节") fail_count += 1 continue print(f"{Fore.GREEN} 发送成功: {bytes_sent} 字节") # 等待MCU应答 print(f"{Fore.YELLOW} 等待MCU应答...") try: # 读取应答 ack = self.ser.read(1) if ack == b'\x00': print(f"{Fore.GREEN} 收到确认: 0x00") success_count += 1 elif ack: print(f"{Fore.RED} 收到错误应答: {ack.hex()}") fail_count += 1 else: print(f"{Fore.RED} 应答超时!") fail_count += 1 except Exception as e: print(f"{Fore.RED} 读取应答时出错: {e}") fail_count += 1 # 显示进度 progress = (num_chunks - i) / num_chunks * 100 print(f"{Fore.CYAN} 进度: {progress:.1f}% ({num_chunks-i}/{num_chunks})") except Exception as e: print(f"{Fore.RED}发送过程中出错: {e}") return False print(f"\n{Fore.CYAN}=== 发送完成 ===") print(f"{Fore.GREEN}成功块数: {success_count}") print(f"{Fore.RED}失败块数: {fail_count}") return fail_count == 0 def interactive_shell(self): """交互式命令行""" print(f"\n{Fore.CYAN}{'='*60}") print(f"{Fore.CYAN}进入交互模式") print(f"{Fore.CYAN}输入 'help' 查看可用命令") print(f"{Fore.CYAN}输入 'exit' 退出") print(f"{Fore.CYAN}{'='*60}") commands = { 'help': self._cmd_help, 'read': self._cmd_read, 'write': self._cmd_write, 'erase': self._cmd_erase, 'reset': self._cmd_reset, 'go': self._cmd_go, 'echo': self._cmd_echo, 'info': self._cmd_info, } while True: try: # 获取用户输入 cmd_input = input(f"\n{Fore.GREEN}boot> ").strip() if not cmd_input: continue # 检查是否为退出命令 if cmd_input.lower() in ['exit', 'quit', 'q']: print(f"{Fore.YELLOW}退出交互模式") break # 分割命令和参数 parts = cmd_input.split() cmd = parts[0].lower() args = parts[1:] if len(parts) > 1 else [] # 执行命令 if cmd in commands: commands[cmd](args) else: print(f"{Fore.RED}未知命令: {cmd}") print(f"{Fore.YELLOW}输入 'help' 查看可用命令") except KeyboardInterrupt: print(f"\n{Fore.YELLOW}检测到Ctrl+C,退出交互模式") break except Exception as e: print(f"{Fore.RED}命令执行错误: {e}") def _cmd_help(self, args): """显示帮助""" print(f"{Fore.CYAN}可用命令:") print(f"{Fore.YELLOW} help 显示此帮助信息") print(f"{Fore.YELLOW} read 读取内存") print(f"{Fore.YELLOW} write 写入内存") print(f"{Fore.YELLOW} erase 擦除Flash") print(f"{Fore.YELLOW} reset 复位MCU") print(f"{Fore.YELLOW} go 跳转到指定地址执行") print(f"{Fore.YELLOW} echo 回显文本") print(f"{Fore.YELLOW} info 显示信息") print(f"{Fore.YELLOW} exit 退出交互模式") def _cmd_read(self, args): """读取内存命令""" if len(args) < 2: print(f"{Fore.RED}用法: read <地址> <长度>") print(f"{Fore.YELLOW}示例: read 0x8000 16") return try: addr = int(args[0], 0) length = int(args[1], 0) print(f"{Fore.CYAN}读取内存:") print(f"{Fore.CYAN} 地址: 0x{addr:04X}") print(f"{Fore.CYAN} 长度: {length} 字节") # TODO: 实现读取命令 print(f"{Fore.YELLOW}[待实现] 读取功能") except ValueError as e: print(f"{Fore.RED}参数错误: {e}") def _cmd_write(self, args): """写入内存命令""" if len(args) < 2: print(f"{Fore.RED}用法: write <地址> <数据>") print(f"{Fore.YELLOW}示例: write 0x8000 0x01 0x02 0x03") return try: addr = int(args[0], 0) data = [int(x, 0) for x in args[1:]] print(f"{Fore.CYAN}写入内存:") print(f"{Fore.CYAN} 地址: 0x{addr:04X}") print(f"{Fore.CYAN} 数据: {bytes(data).hex(' ')}") print(f"{Fore.CYAN} 长度: {len(data)} 字节") # TODO: 实现写入命令 print(f"{Fore.YELLOW}[待实现] 写入功能") except ValueError as e: print(f"{Fore.RED}参数错误: {e}") def _cmd_erase(self, args): """擦除Flash命令""" print(f"{Fore.YELLOW}擦除Flash...") # TODO: 实现擦除命令 print(f"{Fore.YELLOW}[待实现] 擦除功能") def _cmd_reset(self, args): """复位命令""" print(f"{Fore.YELLOW}复位MCU...") # TODO: 实现复位命令 print(f"{Fore.YELLOW}[待实现] 复位功能") def _cmd_go(self, args): """跳转执行命令""" if len(args) < 1: print(f"{Fore.RED}用法: go <地址>") print(f"{Fore.YELLOW}示例: go 0x8000") return try: addr = int(args[0], 0) print(f"{Fore.YELLOW}跳转到地址: 0x{addr:04X}") # TODO: 实现跳转命令 print(f"{Fore.YELLOW}[待实现] 跳转功能") except ValueError as e: print(f"{Fore.RED}参数错误: {e}") def _cmd_echo(self, args): """回显命令""" if args: text = ' '.join(args) print(f"{Fore.CYAN}回显: {text}") else: print(f"{Fore.RED}用法: echo <文本>") def _cmd_info(self, args): """显示信息命令""" print(f"{Fore.CYAN}Bootloader测试工具信息:") print(f"{Fore.CYAN} 串口: {self.port}") print(f"{Fore.CYAN} 波特率: {self.baudrate}") print(f"{Fore.CYAN} Bootloader文件: {self.bin_file}") print(f"{Fore.CYAN} Bootloader大小: {self.bootloader_size} 字节") def run(self): """运行测试""" print(f"{Fore.CYAN}{'='*60}") print(f"{Fore.CYAN}STM8 RAM Bootloader 测试工具") print(f"{Fore.CYAN}{'='*60}") # 1. 打开串口 if not self.open_serial(): return False try: # 2. 读取bin文件 bin_data = self.read_bin_file() if not bin_data: return False # 3. 等待MCU就绪 if not self.wait_for_mcu_ready(): return False # 4. 发送bootloader(反向发送) if not self.send_bootloader_reverse(bin_data): print(f"{Fore.RED}Bootloader发送失败!") return False # 5. 进入交互模式 self.interactive_shell() return True except KeyboardInterrupt: print(f"\n{Fore.YELLOW}用户中断!") return False except Exception as e: print(f"{Fore.RED}运行时错误: {e}") import traceback traceback.print_exc() return False finally: self.close_serial() def list_serial_ports(): """列出可用串口""" import serial.tools.list_ports ports = serial.tools.list_ports.comports() if not ports: print(f"{Fore.YELLOW}未找到可用串口!") return [] print(f"{Fore.CYAN}可用串口:") for i, port in enumerate(ports): print(f"{Fore.GREEN} [{i}] {port.device}: {port.description}") return ports def main(): parser = argparse.ArgumentParser(description="STM8 RAM Bootloader测试工具") parser.add_argument("-p", "--port", help="串口号 (如 COM3, /dev/ttyUSB0)") parser.add_argument("-b", "--baudrate", type=int, default=9600, help="串口波特率 (默认: 9600)") parser.add_argument("--bin", default=None, help="Bootloader bin文件路径 (默认: 脚本目录下的boot2.bin)") parser.add_argument("--list", action="store_true", help="列出可用串口") args = parser.parse_args() # 如果指定了--list,列出串口后退出 if args.list: list_serial_ports() return # 如果没有指定串口,列出并让用户选择 if not args.port: ports = list_serial_ports() if not ports: print(f"{Fore.RED}请使用 -p 参数指定串口!") return try: choice = input(f"\n{Fore.CYAN}请选择串口号 [0-{len(ports)-1}]: ") idx = int(choice) if 0 <= idx < len(ports): args.port = ports[idx].device else: print(f"{Fore.RED}无效的选择!") return except (ValueError, IndexError): print(f"{Fore.RED}无效的输入!") return # 创建测试器并运行 tester = STM8BootloaderTester( port=args.port, baudrate=args.baudrate, bin_file=args.bin ) success = tester.run() if success: print(f"\n{Fore.GREEN}测试完成!") else: print(f"\n{Fore.RED}测试失败!") # 等待用户按键退出 input(f"\n{Fore.YELLOW}按Enter键退出...") if __name__ == "__main__": main()