Files
stm8loader/scripts/stm8_bootloader_test.py
2025-12-19 22:25:54 +08:00

524 lines
19 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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 <addr> <len> 读取内存")
print(f"{Fore.YELLOW} write <addr> <data> 写入内存")
print(f"{Fore.YELLOW} erase 擦除Flash")
print(f"{Fore.YELLOW} reset 复位MCU")
print(f"{Fore.YELLOW} go <addr> 跳转到指定地址执行")
print(f"{Fore.YELLOW} echo <text> 回显文本")
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()