feat: verify assembly bootloader factory recovery

This commit is contained in:
十七
2026-11-05 21:17:16 +08:00
parent 72393b6ef7
commit 12f6bb62f2
7 changed files with 892 additions and 38 deletions
+33
View File
@@ -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)
+675
View File
@@ -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
+8 -1
View File
@@ -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)
+60 -34
View File
@@ -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
+112
View File
@@ -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 检测擦除和写入完成状态。
Binary file not shown.
+4 -3
View File
@@ -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()