From 12f6bb62f292a69fad400b5a3cc484abfa0b69ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8D=81=E4=B8=83?= <1415613786@qq.com> Date: Thu, 5 Nov 2026 21:17:16 +0800 Subject: [PATCH] feat: verify assembly bootloader factory recovery --- STM8_SPI_Test/Makefile | 33 ++ STM8_SPI_Test/src/spi_test.asm | 675 +++++++++++++++++++++++ STM8_app_asm/Makefile | 9 +- STM8_boot_asm/src/bootloader_vectors.asm | 94 ++-- docs/BOOTLOADER_FACTORY_RECOVERY_TEST.md | 112 ++++ factory_flash_v0100.bin | Bin 8210 -> 8210 bytes make_factory_flash.py | 7 +- 7 files changed, 892 insertions(+), 38 deletions(-) create mode 100644 STM8_SPI_Test/Makefile create mode 100644 STM8_SPI_Test/src/spi_test.asm create mode 100644 docs/BOOTLOADER_FACTORY_RECOVERY_TEST.md diff --git a/STM8_SPI_Test/Makefile b/STM8_SPI_Test/Makefile new file mode 100644 index 0000000..7d4c675 --- /dev/null +++ b/STM8_SPI_Test/Makefile @@ -0,0 +1,33 @@ +# W25Q80BV SPI 独立测试工程。 +# 本工程直接从0x8000启动,不依赖Bootloader和正式主程序。 + +PROJECT := stm8_spi_test +BUILD_DIR := build +AS := sdasstm8 +LD := sdldstm8 +PACKIHX := packihx + +ASMFLAGS := -plosgff +LINKFLAGS := -mjwxu -b HOME=0x8000 -b CODE=0x8068 +OBJECT := $(BUILD_DIR)/spi_test.rel +IHX := $(BUILD_DIR)/$(PROJECT).ihx +HEX := $(BUILD_DIR)/$(PROJECT).hex + +.PHONY: all clean + +all: $(HEX) + +$(BUILD_DIR): + mkdir -p $(BUILD_DIR) + +$(OBJECT): src/spi_test.asm | $(BUILD_DIR) + $(AS) $(ASMFLAGS) -o $@ $< + +$(IHX): $(OBJECT) + $(LD) $(LINKFLAGS) -i $@ $^ + +$(HEX): $(IHX) + $(PACKIHX) $< > $@ + +clean: + rm -rf $(BUILD_DIR) diff --git a/STM8_SPI_Test/src/spi_test.asm b/STM8_SPI_Test/src/spi_test.asm new file mode 100644 index 0000000..efe673d --- /dev/null +++ b/STM8_SPI_Test/src/spi_test.asm @@ -0,0 +1,675 @@ +;------------------------------------------------------------------------------- +; STM8S003F3P6TR - W25Q80BV SPI独立读写测试固件 +; +; 测试目的: +; 1. 读取W25Q80BV JEDEC ID; +; 2. 擦除一个专用测试扇区; +; 3. 写入固定测试数据; +; 4. 读回并逐字节比较。 +; +; 硬件连接: +; PC5 -> W25Q80BV CLK +; PC6 -> W25Q80BV DI/MOSI +; PC7 <- W25Q80BV DO/MISO +; PD4 -> W25Q80BV CS# +; PB5 -> 测试LED +; +; 测试区域:外部Flash 0x01F000所在的4KB扇区。 +; 该区域位于预留空间,不会覆盖出厂固件、出厂元数据、OTA固件和OTA元数据。 +; +; LED结果指示: +; 测试成功:PB5约每500ms翻转一次; +; 测试失败:PB5约每100ms翻转一次。 +; +; 注意:本固件会擦除并写入外部Flash 0x01F000所在扇区,测试前请确认该扇区 +; 没有保存需要保留的数据。 +;------------------------------------------------------------------------------- + +.module spi_test +.optsdcc -mstm8 + +.area HOME (ABS) +int _Reset +int _DefaultInterrupt +int _DefaultInterrupt +int _DefaultInterrupt +int _DefaultInterrupt +int _DefaultInterrupt +int _DefaultInterrupt +int _DefaultInterrupt +int _DefaultInterrupt +int _DefaultInterrupt +int _DefaultInterrupt +int _DefaultInterrupt +int _DefaultInterrupt +int _DefaultInterrupt +int _DefaultInterrupt +int _DefaultInterrupt +int _DefaultInterrupt +int _DefaultInterrupt +int _DefaultInterrupt +int _DefaultInterrupt +int _DefaultInterrupt +int _DefaultInterrupt +int _DefaultInterrupt +int _DefaultInterrupt + +.area CODE + +_Reset: + sim + ldw x, #0x03FF + ldw sp, x + ; HSI/1,系统主频16MHz。 + mov 0x50C6, #0x00 + ; PB5推挽输出,初始低电平。 + bres 0x5005, #5 + bset 0x5007, #5 + bset 0x5008, #5 + bres 0x5009, #5 + call _UART_Init + ldw x, #_TextStart + call _UART_SendText + call _SPI_Init + mov 0x0003, #0x01 + call _SPI_CheckJedec + tnz a + jrne _TestFail + ldw x, #_TextJedecOK + call _UART_SendText + mov 0x0003, #0x05 + call _ReadFactoryImageInfo + tnz a + jrne _TestFail + ldw x, #_TextFactoryPass + call _UART_SendText + +_TestPass: + ; A=0表示通过,使用长周期指示。 + ld a, #0x00 + ld 0x0000, a + ldw x, #500 + jra _BlinkLoop + +_TestFail: + ld a, 0x0003 + cp a, #0x01 + jreq _ReportJedecFail + cp a, #0x02 + jreq _ReportEraseFail + cp a, #0x03 + jreq _ReportWriteFail + cp a, #0x05 + jreq _ReportFactoryFail + ldw x, #_TextVerifyFail + call _UART_SendText + jra _TestFailLed +_ReportJedecFail: + ldw x, #_TextJedecFail + call _UART_SendText + jra _TestFailLed +_ReportEraseFail: + ldw x, #_TextEraseFail + call _UART_SendText + jra _TestFailLed +_ReportWriteFail: + ldw x, #_TextWriteFail + call _UART_SendText + jra _TestFailLed +_ReportFactoryFail: + ldw x, #_TextFactoryFail + call _UART_SendText +_TestFailLed: + ; 0x0003保存失败阶段,进入分组闪烁指示,避免只能知道“失败”而不知道 + ; 失败发生在JEDEC、擦除、写入还是回读校验阶段。 + ld a, 0x0003 + ld 0x0004, a + ldw x, #100 + call _DelayMsApprox + ; 每组开始前先熄灭LED约1秒,作为组间分隔。 + bset 0x5005, #5 + ldw x, #1000 + call _DelayMsApprox +_ErrorGroup: + ; 每次点亮约100ms,再熄灭约100ms。 + bres 0x5005, #5 + ldw x, #100 + call _DelayMsApprox + bset 0x5005, #5 + ldw x, #100 + call _DelayMsApprox + dec 0x0004 + jrne _ErrorGroup + ; 一组错误码结束后暂停约1秒,然后重复指示。 + ldw x, #1000 + call _DelayMsApprox + ld a, 0x0003 + ld 0x0004, a + jra _ErrorGroup + +_BlinkLoop: + call _DelayMsApprox + bcpl 0x5005, #5 + ldw x, #500 + ld a, 0x0000 + tnz a + jrne _FailDelaySelect + jra _BlinkLoop +_FailDelaySelect: + ldw x, #100 + jra _BlinkLoop + +;------------------------------------------------------------------------------- +; SPI1初始化:PC5/PC6推挽输出,PC7输入;PD4为软件片选。 +; SPI模式0,主机模式,约500kHz,MSB first。 +;------------------------------------------------------------------------------- +_SPI_Init: + mov 0x500C, #0x60 + mov 0x500D, #0x60 + mov 0x500E, #0x00 + mov 0x500F, #0x10 + mov 0x5011, #0x10 + mov 0x5012, #0x10 + mov 0x5013, #0x00 + bset 0x500F, #4 + mov 0x5200, #0x4C + mov 0x5201, #0x03 + ret + +;------------------------------------------------------------------------------- +; UART1初始化,PD5为TX,PD6为RX,16MHz系统时钟下配置为9600bps。 +; 测试日志只使用TX发送,RX保持输入状态。 +;------------------------------------------------------------------------------- +_UART_Init: + bset 0x5011, #5 + bset 0x5012, #5 + bres 0x5011, #6 + bset 0x5012, #6 + ; CR2:同时打开UART发送器和接收器,TXE轮询发送才能继续执行。 + mov 0x5235, #0x0C + mov 0x5233, #0x03 + mov 0x5232, #0x68 + mov 0x5234, #0x00 + mov 0x5236, #0x00 + ret + +; 发送A寄存器中的一个UART字节。 +_UART_SendByte: + push a +_UART_WaitTXE: + ld a, 0x5230 + and a, #0x80 + jreq _UART_WaitTXE + pop a + ld 0x5231, a + ret + +; 发送以0结尾的ASCII文本,X指向字符串首地址。 +_UART_SendText: + ld a, (x) + tnz a + jreq _UART_SendTextDone + call _UART_SendByte + incw x + jra _UART_SendText +_UART_SendTextDone: + ret + +; A寄存器发送一个SPI字节,同时返回收到的字节。 +_SPI_Transfer: + ld 0x5204, a +_SPI_WaitTx: + btjf 0x5203, #1, _SPI_WaitTx +_SPI_WaitRx: + btjf 0x5203, #0, _SPI_WaitRx + ld a, 0x5204 + ret + +; 读取JEDEC ID,W25Q80BV期望返回EF 40 14。 +_SPI_CheckJedec: + bres 0x500F, #4 + ld a, #0x9F + call _SPI_Transfer + ld a, #0x00 + call _SPI_Transfer + cp a, #0xEF + jrne _SPI_Fail + ld a, #0x00 + call _SPI_Transfer + cp a, #0x40 + jrne _SPI_Fail + ld a, #0x00 + call _SPI_Transfer + cp a, #0x14 + jrne _SPI_Fail + bset 0x500F, #4 + ld a, #0x00 + ret +_SPI_Fail: + bset 0x500F, #4 + ld a, #0x01 + ret + +; 等待W25Q80BV非忙状态,超时返回失败。 +_SPI_WaitReady: + ldw x, #0xFFFF +_SPI_ReadyLoop: + bres 0x500F, #4 + ld a, #0x05 + call _SPI_Transfer + ld a, #0x00 + call _SPI_Transfer + push a + bset 0x500F, #4 + pop a + and a, #0x01 + jreq _SPI_ReadyOK + decw x + jrne _SPI_ReadyLoop + ld a, #0x01 + ret +_SPI_ReadyOK: + ld a, #0x00 + ret + +; 发送写使能命令WREN。 +_SPI_WriteEnable: + bres 0x500F, #4 + ld a, #0x06 + call _SPI_Transfer + bset 0x500F, #4 + ret + +;------------------------------------------------------------------------------- +; 从W25Q80BV读取一个字节。 +; 输入:X为16位地址;当前出厂固件和元数据地址均低于0x10000。 +; 返回:A为读取到的数据。 +;------------------------------------------------------------------------------- +_SPI_ReadByteX: + bres 0x500F, #4 + ld a, #0x03 + call _SPI_Transfer + ld a, #0x00 + call _SPI_Transfer + ld a, xh + call _SPI_Transfer + ld a, xl + call _SPI_Transfer + ld a, #0xFF + call _SPI_Transfer + push a + bset 0x500F, #4 + pop a + ret + +;------------------------------------------------------------------------------- +; 读取出厂固件和元数据,并由STM8重新计算出厂固件CRC16/MODBUS。 +; 返回:A=0表示元数据和固件CRC均正确,A=1表示检查失败。 +; 串口输出实际读取到的关键字段,便于确认CH341A烧录内容。 +;------------------------------------------------------------------------------- +_ReadFactoryImageInfo: + ; 读取并输出魔数0x2000~0x2003。 + ldw x, #0x2000 + call _SPI_ReadByteX + ld 0x0005, a + incw x + call _SPI_ReadByteX + ld 0x0006, a + incw x + call _SPI_ReadByteX + ld 0x0007, a + incw x + call _SPI_ReadByteX + ld 0x0008, a + ldw x, #_TextMagic + call _UART_SendText + ld a, 0x0005 + call _UART_SendHexByte + ld a, 0x0006 + call _UART_SendHexByte + ld a, 0x0007 + call _UART_SendHexByte + ld a, 0x0008 + call _UART_SendHexByte + ldw x, #_TextCRLF + call _UART_SendText + ld a, 0x0005 + cp a, #0x31 + jreq _FactoryMagic1OK + jpf _FactoryInfoFail +_FactoryMagic1OK: + ld a, 0x0006 + cp a, #0x43 + jreq _FactoryMagic2OK + jpf _FactoryInfoFail +_FactoryMagic2OK: + ld a, 0x0007 + cp a, #0x41 + jreq _FactoryMagic3OK + jpf _FactoryInfoFail +_FactoryMagic3OK: + ld a, 0x0008 + cp a, #0x46 + jreq _FactoryMagic4OK + jpf _FactoryInfoFail +_FactoryMagic4OK: + + ; 读取4字节固件长度,保存低16位供CRC循环使用。 + ldw x, #0x2004 + call _SPI_ReadByteX + ; 32位长度的高字节必须为0。 + tnz a + jreq _FactoryLengthByte0OK + jpf _FactoryInfoFail +_FactoryLengthByte0OK: + incw x + call _SPI_ReadByteX + ; 32位长度的次高字节也必须为0。 + tnz a + jreq _FactoryLengthByte1OK + jpf _FactoryInfoFail +_FactoryLengthByte1OK: + incw x + call _SPI_ReadByteX + ld 0x0000, a + incw x + call _SPI_ReadByteX + ld 0x0001, a + ldw x, #_TextLength + call _UART_SendText + ld a, 0x0000 + call _UART_SendHexByte + ld a, 0x0001 + call _UART_SendHexByte + ldw x, #_TextCRLF + call _UART_SendText + + ; 输出版本号。 + ldw x, #_TextVersion + call _UART_SendText + ldw x, #0x2008 + call _SPI_ReadByteX + call _UART_SendHexByte + incw x + call _SPI_ReadByteX + call _UART_SendHexByte + ldw x, #_TextCRLF + call _UART_SendText + + ; 读取并输出元数据保存的CRC16,高字节在0x0002,低字节在0x0003。 + ldw x, #0x200A + call _SPI_ReadByteX + ld 0x0002, a + incw x + call _SPI_ReadByteX + ld 0x0003, a + ldw x, #_TextStoredCRC + call _UART_SendText + ld a, 0x0002 + call _UART_SendHexByte + ld a, 0x0003 + call _UART_SendHexByte + ldw x, #_TextCRLF + call _UART_SendText + + ; 输出并检查镜像有效标志。 + ldw x, #0x200E + call _SPI_ReadByteX + ld 0x0009, a + ldw x, #_TextValid + call _UART_SendText + ld a, 0x0009 + call _UART_SendHexByte + ldw x, #_TextCRLF + call _UART_SendText + ld a, 0x0009 + cp a, #0xA5 + jrne _FactoryInfoFail + + ; 从外部Flash 0x000000开始逐字节计算完整固件CRC。 + ld a, #0xFF + ld 0x000A, a + ld 0x000B, a + ldw x, #0x0000 +_FactoryCrcLoop: + ld a, 0x0000 + or a, 0x0001 + jreq _FactoryCrcDone + call _SPI_ReadByteX + call _CRC_UpdateByte + incw x + call _DecLength + jra _FactoryCrcLoop +_FactoryCrcDone: + ldw x, #_TextCalcCRC + call _UART_SendText + ld a, 0x000B + call _UART_SendHexByte + ld a, 0x000A + call _UART_SendHexByte + ldw x, #_TextCRLF + call _UART_SendText + ld a, 0x000B + cp a, 0x0002 + jrne _FactoryInfoFail + ld a, 0x000A + cp a, 0x0003 + jrne _FactoryInfoFail + ld a, #0x00 + ret +_FactoryInfoFail: + ld a, #0x01 + ret + +; CRC-16/MODBUS逐字节更新,输入A,累加值低字节0x000A、高字节0x000B。 +_CRC_UpdateByte: + xor a, 0x000A + ld 0x000A, a + mov 0x000C, #0x08 +_CRC_BitLoop: + srl 0x000B + rrc 0x000A + jrc _CRC_Poly + jra _CRC_Next +_CRC_Poly: + ld a, 0x000A + xor a, #0x01 + ld 0x000A, a + ld a, 0x000B + xor a, #0xA0 + ld 0x000B, a +_CRC_Next: + dec 0x000C + jrne _CRC_BitLoop + ret + +; 递减0x0000/0x0001保存的16位剩余长度。 +_DecLength: + ld a, 0x0001 + tnz a + jrne _DecLengthLow + dec 0x0000 +_DecLengthLow: + dec 0x0001 + ret + +; 输出A的两个十六进制字符。 +_UART_SendHexByte: + push a + swap a + and a, #0x0F + call _UART_SendHexNibble + pop a + and a, #0x0F + call _UART_SendHexNibble + ret +_UART_SendHexNibble: + cp a, #0x0A + jrult _UART_SendDigit + add a, #0x37 + jra _UART_SendHexOut +_UART_SendDigit: + add a, #0x30 +_UART_SendHexOut: + call _UART_SendByte + ret + +; 擦除0x01F000所在4KB扇区。 +_SPI_EraseSector: + call _SPI_WriteEnable + bres 0x500F, #4 + ld a, #0x20 + call _SPI_Transfer + ld a, #0x01 + call _SPI_Transfer + ld a, #0xF0 + call _SPI_Transfer + ld a, #0x00 + call _SPI_Transfer + bset 0x500F, #4 + call _SPI_WaitReady + ret + +; 向0x01F000写入8字节测试数据:00 55 AA FF 12 34 56 78。 +_SPI_WriteTestData: + call _SPI_WriteEnable + bres 0x500F, #4 + ld a, #0x02 + call _SPI_Transfer + ld a, #0x01 + call _SPI_Transfer + ld a, #0xF0 + call _SPI_Transfer + ld a, #0x00 + call _SPI_Transfer + ld a, #0x00 + call _SPI_Transfer + ld a, #0x55 + call _SPI_Transfer + ld a, #0xAA + call _SPI_Transfer + ld a, #0xFF + call _SPI_Transfer + ld a, #0x12 + call _SPI_Transfer + ld a, #0x34 + call _SPI_Transfer + ld a, #0x56 + call _SPI_Transfer + ld a, #0x78 + call _SPI_Transfer + bset 0x500F, #4 + call _SPI_WaitReady + ret + +; 读取8字节并逐字节比较测试数据。 +_SPI_VerifyTestData: + bres 0x500F, #4 + ld a, #0x03 + call _SPI_Transfer + ld a, #0x01 + call _SPI_Transfer + ld a, #0xF0 + call _SPI_Transfer + ld a, #0x00 + call _SPI_Transfer + ldw x, #_TestData + mov 0x0001, #0x08 +_VerifyLoop: + ld a, #0xFF + call _SPI_Transfer + ld 0x0002, a + ld a, (x) + cp a, 0x0002 + jrne _VerifyFail + incw x + dec 0x0001 + jrne _VerifyLoop + bset 0x500F, #4 + ld a, #0x00 + ret +_VerifyFail: + bset 0x500F, #4 + ld a, #0x01 + ret + +; 固定测试数据表。 +_TestData: + .db 0x00, 0x55, 0xAA, 0xFF, 0x12, 0x34, 0x56, 0x78 + +; 粗略毫秒延时,X中的次数由调用方设置。 +_DelayMsApprox: + pushw x +_DelayOuter: + ldw y, #1600 +_DelayInner: + decw y + jrne _DelayInner + popw x + decw x + jrne _DelayMsApprox + ret + +_DefaultInterrupt: + sim +_DefaultLoop: + jra _DefaultLoop + +; UART测试日志文本,均以回车换行结尾并以0结束。 +_TextStart: + .ascii "SPI TEST START\r\n" + .db 0 +_TextJedecOK: + .ascii "JEDEC OK\r\n" + .db 0 +_TextEraseOK: + .ascii "ERASE OK\r\n" + .db 0 +_TextWriteOK: + .ascii "WRITE OK\r\n" + .db 0 +_TextVerifyOK: + .ascii "VERIFY OK\r\n" + .db 0 +_TextPass: + .ascii "SPI TEST PASS\r\n" + .db 0 +_TextJedecFail: + .ascii "JEDEC FAIL\r\n" + .db 0 +_TextEraseFail: + .ascii "ERASE FAIL\r\n" + .db 0 +_TextWriteFail: + .ascii "WRITE FAIL\r\n" + .db 0 +_TextVerifyFail: + .ascii "VERIFY FAIL\r\n" + .db 0 +_TextFactoryFail: + .ascii "FACTORY IMAGE FAIL\r\n" + .db 0 +_TextFactoryPass: + .ascii "FACTORY IMAGE PASS\r\n" + .db 0 +_TextMagic: + .ascii "MAGIC: " + .db 0 +_TextLength: + .ascii "LENGTH: " + .db 0 +_TextVersion: + .ascii "VERSION: " + .db 0 +_TextStoredCRC: + .ascii "STORED CRC: " + .db 0 +_TextValid: + .ascii "VALID: " + .db 0 +_TextCalcCRC: + .ascii "CALC CRC: " + .db 0 +_TextCRLF: + .ascii "\r\n" + .db 0 diff --git a/STM8_app_asm/Makefile b/STM8_app_asm/Makefile index 24310b1..828ee04 100644 --- a/STM8_app_asm/Makefile +++ b/STM8_app_asm/Makefile @@ -6,16 +6,18 @@ BUILD_DIR := build AS := sdasstm8 LD := sdldstm8 PACKIHX := packihx +MAKEBIN := makebin ASMFLAGS := -plosgff LINKFLAGS := -mjwxu -b HOME=0x8500 -b CODE=0x8568 OBJECT := $(BUILD_DIR)/app_vectors.rel IHX := $(BUILD_DIR)/$(PROJECT).ihx HEX := $(BUILD_DIR)/$(PROJECT).hex +BIN := $(BUILD_DIR)/$(PROJECT).bin .PHONY: all clean -all: $(HEX) +all: $(HEX) $(BIN) $(BUILD_DIR): mkdir -p $(BUILD_DIR) @@ -29,5 +31,10 @@ $(IHX): $(OBJECT) $(HEX): $(IHX) $(PACKIHX) $< > $@ +# 生成用于写入W25Q80BV出厂区的主程序裸BIN。 +# -s提供完整STM8地址空间,-o跳过0x0000~0x84FF,仅保留0x8500之后的应用数据。 +$(BIN): $(IHX) + $(MAKEBIN) -p -s 0x10000 -o 0x8500 $< $@ + clean: rm -rf $(BUILD_DIR) diff --git a/STM8_boot_asm/src/bootloader_vectors.asm b/STM8_boot_asm/src/bootloader_vectors.asm index 75d983d..de006f8 100644 --- a/STM8_boot_asm/src/bootloader_vectors.asm +++ b/STM8_boot_asm/src/bootloader_vectors.asm @@ -50,11 +50,18 @@ int _Asm_TIM4Vector ; 因此从0x8501开始读取三字节入口地址,避免写死主程序启动地址。 _Asm_Reset: sim + ; STM8复位后的默认HSI时钟为16MHz/8,约2MHz。 + ; Bootloader和应用程序均使用16MHz系统时钟。 + ldw x, #0x03FF + ldw sp, x + mov 0x50C6, #0x00 call _Asm_SPI_Init call _Asm_SPI_CheckJedec ; 外部Flash故障时不阻止已有主程序运行。 tnz a - jrne _Asm_JumpApplication + jreq _Asm_SpiOK + jpf _Asm_JumpApplication +_Asm_SpiOK: ; PA3配置为带上拉输入,按键按下时读取为低电平。 mov 0x5002, #0x00 mov 0x5003, #0x08 @@ -84,7 +91,9 @@ _Asm_WaitOneMs: jrne _Asm_KeyWaitLoop ; 长按已超过5秒,优先执行出厂固件恢复。 call _Asm_CheckFactoryMetadata - jrne _Asm_NormalBoot + jreq _Asm_FactoryKeyMetaOK + jpf _Asm_NormalBoot +_Asm_FactoryKeyMetaOK: jpf _Asm_FactoryRecovery _Asm_NormalBoot: @@ -223,21 +232,21 @@ _Asm_CheckFactoryMetadata: ; 读取4字节固件长度,只接受不超过应用区的长度。 ldw x, #0x2004 call _Asm_SPI_ReadByteX + ; 长度为高字节在前:前两个字节必须为0。 + tnz a + jrne _Asm_FactoryMetadataInvalid + incw x + call _Asm_SPI_ReadByteX + tnz a + jrne _Asm_FactoryMetadataInvalid + incw x + call _Asm_SPI_ReadByteX ld 0x0000, a ld 0x000E, a incw x call _Asm_SPI_ReadByteX ld 0x0001, a ld 0x000F, a - incw x - call _Asm_SPI_ReadByteX - ; 当前应用区小于64KB,高16位必须为0。 - tnz a - jrne _Asm_FactoryMetadataInvalid - incw x - call _Asm_SPI_ReadByteX - tnz a - jrne _Asm_FactoryMetadataInvalid ; 长度不能为0,也不能超过0x1800字节的应用空间。 ld a, 0x0000 or a, 0x0001 @@ -287,20 +296,21 @@ _Asm_CheckOtaMetadata: jrne _Asm_OtaMetadataInvalid ldw x, #0x5004 call _Asm_SPI_ReadByteX + ; OTA长度同样为4字节高字节在前,前两个字节必须为0。 + tnz a + jrne _Asm_OtaMetadataInvalid + incw x + call _Asm_SPI_ReadByteX + tnz a + jrne _Asm_OtaMetadataInvalid + incw x + call _Asm_SPI_ReadByteX ld 0x0000, a ld 0x000E, a incw x call _Asm_SPI_ReadByteX ld 0x0001, a ld 0x000F, a - incw x - call _Asm_SPI_ReadByteX - tnz a - jrne _Asm_OtaMetadataInvalid - incw x - call _Asm_SPI_ReadByteX - tnz a - jrne _Asm_OtaMetadataInvalid ld a, 0x0000 or a, 0x0001 jreq _Asm_OtaMetadataInvalid @@ -460,7 +470,9 @@ _Asm_CrcBitLoop: srl 0x000B rrc 0x000A ld a, 0x000A - xor a, #0xA1 + ; CRC-16/MODBUS反射算法多项式为0xA001,拆成寄存器操作时: + ; 高字节异或0xA0,低字节异或0x01。 + xor a, #0x01 ld 0x000A, a ld a, 0x000B xor a, #0xA0 @@ -493,7 +505,8 @@ _Asm_ValidateFactoryImage: ld a, #0xFF ld 0x000A, a ld 0x000B, a - ldw x, 0x0004 + ; 出厂固件镜像从外部Flash 0x000000开始,不能跳过前4字节向量表。 + ldw x, #0x0000 ldw y, 0x0000 _Asm_ValidateLoop: ld a, 0x0000 @@ -518,26 +531,32 @@ _Asm_ValidateFail: ret ;------------------------------------------------------------------------------- -; 擦除内部应用区0x8500~0x9FFF,每次擦除一个64字节块。 +; 擦除内部应用区0x8500~0x9FFF。 +; 这里采用STM8官方支持的字节擦除方式:逐地址写入0x00并等待EOP。 +; 不使用块擦除模式,避免块擦除关键步骤必须从RAM执行的问题。 ;------------------------------------------------------------------------------- _Asm_EraseApplication: ld a, #0x56 ld 0x5062, a ld a, #0xAE ld 0x5062, a - ldw x, #0x8500 -_Asm_EraseBlock: - ld a, #0x20 + ; 退出块擦除/块编程模式,恢复普通字节擦除/编程方式。 + ld a, #0x00 ld 0x505B, a - ld a, #0xDF + ld a, #0xFF ld 0x505C, a + ldw x, #0x8500 +_Asm_EraseByte: + ; 字节擦除由向目标地址写入0x00触发;写指令后CPU会等待本次操作完成。 ld a, #0x00 ld (x), a _Asm_EraseWait: + ; 本项目STM8S003寄存器定义中,FLASH_IAPSR.EOP为bit2(掩码0x04)。 + ; 置1表示当前擦除操作已完成。 btjf 0x505F, #2, _Asm_EraseWait - addw x, #64 + incw x cpw x, #0xA000 - jrne _Asm_EraseBlock + jrne _Asm_EraseByte ld a, #0x00 ret @@ -549,7 +568,15 @@ _Asm_CopyFactoryImage: ld 0x0000, a ld a, 0x000F ld 0x0001, a - ldw x, 0x0004 + ; 搬运阶段采用STM8的单字节编程方式: + ; 只向目标地址执行一次写操作,不设置块编程CR2/NCR2模式。 + ; 这样每个字节写入后都可以单独等待EOP完成。 + ld a, #0x00 + ld 0x505B, a + ld a, #0xFF + ld 0x505C, a + ; 搬运必须从出厂镜像第0字节开始,保持应用向量表与0x8500对齐。 + ldw x, #0x0000 ldw y, #0x8500 _Asm_CopyLoop: ld a, 0x0000 @@ -557,13 +584,11 @@ _Asm_CopyLoop: jreq _Asm_CopyOK call _Asm_SPI_ReadByteX ld 0x000C, a - ld a, #0x01 - ld 0x505B, a - ld a, #0xFE - ld 0x505C, a ld a, 0x000C ld (y), a _Asm_CopyWait: + ; 本项目STM8S003寄存器定义中,FLASH_IAPSR.EOP为bit2(掩码0x04)。 + ; 置1表示当前字节编程已完成。 btjf 0x505F, #2, _Asm_CopyWait incw x incw y @@ -582,7 +607,8 @@ _Asm_VerifyApplication: ld 0x0000, a ld a, 0x000F ld 0x0001, a - ldw x, 0x0004 + ; 回读校验与写入过程使用同一个外部Flash起始地址0x000000。 + ldw x, #0x0000 ldw y, #0x8500 _Asm_VerifyLoop: ld a, 0x0000 diff --git a/docs/BOOTLOADER_FACTORY_RECOVERY_TEST.md b/docs/BOOTLOADER_FACTORY_RECOVERY_TEST.md new file mode 100644 index 0000000..24915bd --- /dev/null +++ b/docs/BOOTLOADER_FACTORY_RECOVERY_TEST.md @@ -0,0 +1,112 @@ +# 汇编 Bootloader 出厂固件恢复验证记录 + +## 1. 验证目的 + +验证当 STM8S003F3P6TR 内部应用 Flash 没有主程序时,汇编 Bootloader 能否从外部 W25Q80BV 读取出厂固件,完成内部应用区恢复,并跳转到主程序执行。 + +## 2. 当前地址规划 + +| 区域 | 地址范围 | 说明 | +|---|---:|---| +| Bootloader | `0x8000~0x84FF` | 固定汇编 Bootloader 区域 | +| 主程序 | `0x8500~0x9FFF` | 汇编主程序运行区域 | +| 出厂固件 | W25Q80BV `0x000000~0x001FFF` | 最大 8KiB | +| 出厂元数据 | W25Q80BV `0x002000~0x002FFF` | 4KiB 区域 | + +## 3. 外部 Flash 内容验证 + +独立 SPI 测试固件已经读取并校验出厂镜像,串口输出如下: + +```text +SPI TEST START +JEDEC OK +MAGIC: 31434146 +LENGTH: 06AE +VERSION: 0100 +STORED CRC: DF89 +VALID: A5 +CALC CRC: DF89 +FACTORY IMAGE PASS +``` + +因此确认: + +- W25Q80BV JEDEC ID 读取正常; +- 出厂固件魔数正确; +- 出厂固件长度为 `0x06AE`; +- 出厂固件版本为 `0x0100`; +- 元数据保存 CRC 为 `0xDF89`; +- Bootloader 有效标志为 `0xA5`; +- 单片机重新计算 CRC 后仍为 `0xDF89`。 + +## 4. Bootloader 恢复流程 + +当前汇编 Bootloader 的出厂恢复流程为: + +1. 设置系统时钟为 16MHz; +2. 初始化 SPI1; +3. 读取并检查 W25Q80BV JEDEC ID; +4. 检查出厂固件元数据; +5. 按 `CRC-16/MODBUS` 逐字节校验出厂固件; +6. 解锁 STM8 程序 Flash; +7. 对 `0x8500~0x9FFF` 应用区逐字节擦除; +8. 从 W25Q80BV `0x000000` 开始逐字节搬运到内部 Flash `0x8500`; +9. 回读内部 Flash,并与外部出厂镜像逐字节比较; +10. 跳转到主程序复位向量执行。 + +## 5. 现场验证结果 + +验证前先清除内部 Flash 的主程序区域,使 `0x8500` 之后没有有效主程序。重新烧录 Bootloader 并上电后,设备能够自动完成出厂固件搬运,随后主程序正常启动,并输出蓝牙初始化相关信息: + +```text +AT+ROLE +AT+ROLE2 +AT+ROLE +AT+ROLE2 +AT+ROLE +``` + +验证结论: + +**内部 Flash 无主程序时,Bootloader 自动从 W25Q80BV 搬运出厂固件并成功启动主程序,功能验证通过。** + +## 6. 调试代码清理 + +为便于现场确认,曾临时加入 Bootloader UART1 阶段输出。恢复流程验证完成后,已经删除: + +- Bootloader UART1 初始化代码; +- 临时阶段字符输出; +- 元数据诊断输出; +- CRC、擦除、搬运、回读和跳转诊断输出。 + +正式 Bootloader 不再占用 UART1 发送调试功能。 + +## 7. 本次修正的关键问题 + +### 7.1 元数据长度解析 + +固件长度字段是 4 字节高字节在前: + +```text +00 00 06 AE +``` + +前两个字节必须为 `0x00`,后两个字节才是实际长度 `0x06AE`。 + +### 7.2 CRC 多项式 + +`CRC-16/MODBUS` 反射算法使用多项式 `0xA001`。在当前两个字节寄存器实现中: + +```text +高字节异或:0xA0 +低字节异或:0x01 +``` + +### 7.3 STM8 Flash 擦除方式 + +块擦除流程要求连续装载 4 个字节,并且关键步骤需要从 RAM 执行。当前 Bootloader 采用逐字节擦除方式,向应用区每个地址写入 `0x00`,每次等待 EOP 完成,避免块擦除流程在 Flash 中执行时卡住。 + +### 7.4 EOP 标志 + +当前项目使用的 STM8S003 寄存器定义中,`FLASH_IAPSR.EOP` 掩码为 `0x04`,即 bit2。Bootloader 按 bit2 检测擦除和写入完成状态。 + diff --git a/factory_flash_v0100.bin b/factory_flash_v0100.bin index b4c7a0ffcf1a7d1d2e98fd614629c1bfdb6f4b66..5b15c20fbf1229175a3f377f897dec09ee77863d 100644 GIT binary patch literal 8210 zcmeHHO=u)V6t4M6W|Ey%5)H)cs>Nng1!lpSB$Z^PyEC24Una9;=vh__>mFsV<|c@4 z2lK~VJO~TBmo$#B7zyNHweCT0<3ai=UPQd?VeukJmKEIb>#m7pWIzOuHSp$pudC|S zd*6HYw#fGFEzt)5vfL5yx*w6F2ODW3&1@5j6g;=Uj|;Rj|$=lk#J_kZD$7h9Av)j zWZOGR-wbx%l{NNCu)FZLF>=;OenEEL33h))07MXyh2$V`M&OJH$h^1$SGynF zliA}O|3l^u=@B7!PkbtKwbt`~F4!NE^Zb`!|6#bB{uuf~as&71GkjQs)Pzl}pjj2y z6-+B|6s#+#DJUv9uV6TWqfQ4|wJ1w&uoD;A`Bt#=gIQyV4fe>DmcJy|Ji=e~Vtmqz zbM7Vhw3p63&jQX~(5cF;`s_xQOw_K|U7pdM;d-c36BGFC_NJ@SAtMTAR=)DiKnR*6oj)h($?EQjfs{t7y`K+h?o0HfHyQG4EnTvSbe zRW-eO7t%gV`eC~-onZ-2!+?EUVUPd$Haf&W#AeTduMFQ1X=2Tp^Ty?jG#o#s>S--P>}G4k_1A)mzv F`3pb8dV>G} literal 8210 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 diff --git a/make_factory_flash.py b/make_factory_flash.py index d8f13c7..a0ed25b 100644 --- a/make_factory_flash.py +++ b/make_factory_flash.py @@ -13,8 +13,9 @@ from pathlib import Path import re -# 主程序Makefile生成的裸二进制文件,已经去除了Bootloader地址空间。 -FIRMWARE_PATH = Path("build/mesh_relay.bin") +# 当前汇编主程序Makefile生成的裸二进制文件,已经去除了Bootloader地址空间。 +# 该文件从应用链接地址0x8500对应的第一个字节开始,不包含Bootloader。 +FIRMWARE_PATH = Path("STM8_app_asm/build/stm8_app_asm.bin") # 主程序版本定义文件,脚本会自动读取其中的FIRMWARE_VERSION宏。 VERSION_HEADER_PATH = Path("include/app_config.h") @@ -95,7 +96,7 @@ def main() -> None: if not FIRMWARE_PATH.is_file(): raise SystemExit( f"找不到固件文件:{FIRMWARE_PATH}\n" - "请先在项目根目录执行 make。" + "请先执行 make -C STM8_app_asm。" ) firmware = FIRMWARE_PATH.read_bytes()