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 b4c7a0f..5b15c20 100644 Binary files a/factory_flash_v0100.bin and b/factory_flash_v0100.bin differ 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()