feat: complete assembly mesh relay control path

- synchronize STM8 application and external flash layout updates\n- add assembly bootloader and assembly application test projects\n- add UART1 interrupt receive ring buffer\n- parse JDY-28M Mesh outer frames and source addresses\n- add CRC-16/MODBUS validation\n- implement relay control command 0x01 and response 0x81\n- support unicast and broadcast relay control frames\n- update host protocol, OTA documents, and serial tools
This commit is contained in:
十七
2026-11-05 16:12:51 +08:00
parent df1aad3d16
commit ebef6a8694
40 changed files with 2133 additions and 307 deletions
+33
View File
@@ -0,0 +1,33 @@
PROJECT := stm8_bootloader_asm
BUILD_DIR := build
SRC_DIR := src
AS := sdasstm8
LD := sdldstm8
PACKIHX := packihx
ASMFLAGS := -plosgff
LINKFLAGS := -mjwxu -b HOME=0x8000 -b CODE=0x8068
OBJECTS := $(BUILD_DIR)/bootloader_vectors.rel
IHX := $(BUILD_DIR)/$(PROJECT).ihx
HEX := $(BUILD_DIR)/$(PROJECT).hex
.PHONY: all clean
all: $(HEX)
$(BUILD_DIR):
mkdir -p $(BUILD_DIR)
$(BUILD_DIR)/bootloader_vectors.rel: $(SRC_DIR)/bootloader_vectors.asm | $(BUILD_DIR)
$(AS) $(ASMFLAGS) -o $@ $<
$(IHX): $(OBJECTS)
$(LD) $(LINKFLAGS) -i $@ $^
$(HEX): $(IHX)
$(PACKIHX) $< > $@
clean:
rm -rf $(BUILD_DIR)
+39
View File
@@ -0,0 +1,39 @@
# STM8 汇编 Bootloader(并行验证工程)
本目录是独立的汇编 Bootloader 实验工程,不修改现有 `STM8_Bootloader` C 版本。
当前阶段已经实现固定中断向量、应用程序跳转、SPI1初始化、W25Q80BV JEDEC ID
读取、OTA元数据处理、OTA固件搬运、升级成功结果写回、应用损坏自动恢复,以及
上电长按后的出厂镜像恢复流程:先分段计算CRC16/MODBUS,校验通过后擦除内部
应用区,再逐字节写入并回读比较,成功后跳转到应用程序。固件读取采用小步处理,
不把整包镜像装入RAM。
OTA元数据使用:镜像地址0x003000、元数据地址0x005000;校验成功搬运后将
0x005010写为成功结果码,并清除0x00500F升级请求。若OTA校验、擦除、写入或
回读失败,Bootloader直接跳转主程序。若应用首字节不是0x82,则自动检查并尝试
恢复出厂镜像;本汇编Bootloader不启用看门狗,也不依赖看门狗复位。
该流程已经完成编译和链接验证,但仍需要连接实际W25Q80BV验证JEDEC读取、外部
Flash数据读取、内部Flash擦写时序以及恢复失败时的现场行为。
如果外部Flash检测失败,当前策略仍是允许内部主程序继续运行;这是为了避免外部
Flash故障导致设备完全无法工作。实际量产策略还需要在搬运功能完成后再补充应用
有效性判断和故障兜底。
## 地址规划
- Bootloader:`0x8000~0x84FF`
- Application:`0x8500~0x9FFF`
- `0x8500` 为应用中断向量表起始地址,复位入口地址从 `0x8501` 动态读取
- TIM4 使用通用转发:`0x8064 -> 0x8564`
- TIM4 IRQ23:固定向量 `0x8064`,转发目标 `0x8564`
- UART1 RX IRQ18:固定向量 `0x8050`,转发目标 `0x8550`
## 构建
```sh
make
```
输出文件为 `build/stm8_bootloader_asm.hex`。当前工程与 C Bootloader 并行存在,
未验证前不要烧录到量产设备。
+618
View File
@@ -0,0 +1,618 @@
;-------------------------------------------------------------------------------
; STM8S003F3 汇编 Bootloader 实验入口
;
; 本文件当前实现固定中断向量、PA3上电长按检测和应用跳转;
; 外部 Flash 搬运功能将在后续阶段加入。
; STM8 中断向量每项为:0x82 + 3 字节目标地址。
;-------------------------------------------------------------------------------
.module bootloader_vectors
.optsdcc -mstm8
.area HOME (ABS)
; 固定向量表从 Bootloader 的 0x8000 开始。
; 未使用的中断统一指向默认处理函数。
int _Asm_Reset
int _Asm_DefaultInterrupt
int _Asm_DefaultInterrupt
int _Asm_DefaultInterrupt
int _Asm_DefaultInterrupt
int _Asm_DefaultInterrupt
int _Asm_DefaultInterrupt
int _Asm_DefaultInterrupt
int _Asm_DefaultInterrupt
int _Asm_DefaultInterrupt
int _Asm_DefaultInterrupt
int _Asm_DefaultInterrupt
int _Asm_DefaultInterrupt
int _Asm_DefaultInterrupt
int _Asm_DefaultInterrupt
int _Asm_DefaultInterrupt
int _Asm_DefaultInterrupt
int _Asm_DefaultInterrupt
int _Asm_DefaultInterrupt
int _Asm_DefaultInterrupt
; IRQ18:UART1接收,跳转到主程序 0x8550。
int _Asm_UART1Vector
int _Asm_DefaultInterrupt
int _Asm_DefaultInterrupt
int _Asm_DefaultInterrupt
int _Asm_DefaultInterrupt
; IRQ23:TIM4更新,转发到应用程序固定向量槽 0x8564。
int _Asm_TIM4Vector
.area CODE
; 复位后读取应用程序复位向量并动态跳转。
; 0x8500 是应用程序复位向量项,格式为:0x82 + 3字节入口地址;
; 因此从0x8501开始读取三字节入口地址,避免写死主程序启动地址。
_Asm_Reset:
sim
call _Asm_SPI_Init
call _Asm_SPI_CheckJedec
; 外部Flash故障时不阻止已有主程序运行。
tnz a
jrne _Asm_JumpApplication
; PA3配置为带上拉输入,按键按下时读取为低电平。
mov 0x5002, #0x00
mov 0x5003, #0x08
mov 0x5004, #0x00
; TIM4配置为约1ms更新事件,仅轮询状态标志,不开TIM4中断。
mov 0x5340, #0x00
mov 0x5343, #0x00
mov 0x5347, #0x07
mov 0x5348, #0x7C
mov 0x5346, #0x00
mov 0x5345, #0x01
mov 0x5344, #0x00
mov 0x5340, #0x01
; 未按键时进入正常启动检查;按住后累计5秒。
ld a, 0x5001
and a, #0x08
jrne _Asm_NormalBoot
ldw x, #5000
_Asm_KeyWaitLoop:
ld a, 0x5001
and a, #0x08
jrne _Asm_NormalBoot
_Asm_WaitOneMs:
btjf 0x5344, #0, _Asm_WaitOneMs
mov 0x5344, #0x00
decw x
jrne _Asm_KeyWaitLoop
; 长按已超过5秒,优先执行出厂固件恢复。
call _Asm_CheckFactoryMetadata
jrne _Asm_NormalBoot
jpf _Asm_FactoryRecovery
_Asm_NormalBoot:
; 普通启动先处理已经提交的OTA请求。
call _Asm_CheckOtaMetadata
jrne _Asm_NormalAfterOtaCheck
jpf _Asm_OtaRecovery
_Asm_NormalAfterOtaCheck:
; 应用区无有效复位向量时,自动尝试出厂恢复。
ld a, 0x8500
cp a, #0x82
jreq _Asm_JumpApplication
call _Asm_CheckFactoryMetadata
jreq _Asm_FactoryMetadataReady
jpf _Asm_JumpApplication
_Asm_FactoryMetadataReady:
jpf _Asm_FactoryRecovery
; 没有有效镜像时也直接交给应用入口;最终行为由主程序自身决定。
jpf _Asm_JumpApplication
_Asm_JumpApplication:
jpf [0x8501]
;-------------------------------------------------------------------------------
; SPI1初始化:PC5=SCK、PC6=MOSI、PC7=MISO,PD4=W25Q80BV CS#。
;-------------------------------------------------------------------------------
_Asm_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
; 主机、软件片选、空闲低电平、数据高位先行,SPI时钟约500kHz。
mov 0x5200, #0x4C
mov 0x5201, #0x03
ret
;-------------------------------------------------------------------------------
; SPI发送一个字节并返回同时收到的字节,A寄存器作为输入和返回值。
;-------------------------------------------------------------------------------
_Asm_SPI_Transfer:
ld 0x5204, a
_Asm_SPI_WaitTx:
btjf 0x5203, #1, _Asm_SPI_WaitTx
_Asm_SPI_WaitRx:
btjf 0x5203, #0, _Asm_SPI_WaitRx
ld a, 0x5204
ret
;-------------------------------------------------------------------------------
; 读取W25Q80BV JEDEC ID,成功返回A=0,失败返回A=1。
; 期望值:EF 40 14。
;-------------------------------------------------------------------------------
_Asm_SPI_CheckJedec:
bres 0x500F, #4
ld a, #0x9F
call _Asm_SPI_Transfer
ld a, #0x00
call _Asm_SPI_Transfer
cp a, #0xEF
jrne _Asm_SPI_JedecFail
ld a, #0x00
call _Asm_SPI_Transfer
cp a, #0x40
jrne _Asm_SPI_JedecFail
ld a, #0x00
call _Asm_SPI_Transfer
cp a, #0x14
jrne _Asm_SPI_JedecFail
bset 0x500F, #4
ld a, #0x00
ret
_Asm_SPI_JedecFail:
bset 0x500F, #4
ld a, #1
ret
;-------------------------------------------------------------------------------
; 从W25Q80BV读取一个字节。
; 输入:X=低16位地址;当前工程所有固定分区均位于0x010000以下,地址高字节为0。
; 返回:A=读取到的数据。
;-------------------------------------------------------------------------------
_Asm_SPI_ReadByteX:
bres 0x500F, #4
ld a, #0x03
call _Asm_SPI_Transfer
ld a, #0x00
call _Asm_SPI_Transfer
ld a, xh
call _Asm_SPI_Transfer
ld a, xl
call _Asm_SPI_Transfer
ld a, #0xFF
call _Asm_SPI_Transfer
push a
bset 0x500F, #4
pop a
ret
;-------------------------------------------------------------------------------
; 检查出厂固件元数据。
; 元数据地址:0x002000;布局与C Bootloader保持一致:
; +0x00~+0x03:魔数FAC1,按低字节在前保存,即31 43 41 46;
; +0x04~+0x07:固件长度,高字节在前;
; +0x0E:镜像有效标志,必须为0xA5。
; 返回:A=0表示元数据有效,A=1表示无效。
; 同时把长度低16位暂存到RAM 0x0000/0x0001,供后续搬运阶段使用。
;-------------------------------------------------------------------------------
_Asm_CheckFactoryMetadata:
; 出厂镜像从外部Flash 0x000000开始读取。
ld a, #0x00
ld 0x0004, a
ld 0x0005, a
ldw x, #0x2000
call _Asm_SPI_ReadByteX
cp a, #0x31
jrne _Asm_FactoryMetadataInvalid
incw x
call _Asm_SPI_ReadByteX
cp a, #0x43
jrne _Asm_FactoryMetadataInvalid
incw x
call _Asm_SPI_ReadByteX
cp a, #0x41
jrne _Asm_FactoryMetadataInvalid
incw x
call _Asm_SPI_ReadByteX
cp a, #0x46
jrne _Asm_FactoryMetadataInvalid
; 读取4字节固件长度,只接受不超过应用区的长度。
ldw x, #0x2004
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
jreq _Asm_FactoryMetadataInvalid
ld a, 0x0000
cp a, #0x1B
jrugt _Asm_FactoryMetadataInvalid
; 读取元数据中的CRC16,高字节在前,保存到RAM 0x0002/0x0003。
ldw x, #0x200A
call _Asm_SPI_ReadByteX
ld 0x0002, a
incw x
call _Asm_SPI_ReadByteX
ld 0x0003, a
; 读取有效标志。
ldw x, #0x200E
call _Asm_SPI_ReadByteX
cp a, #0xA5
jrne _Asm_FactoryMetadataInvalid
ld a, #0x00
ret
_Asm_FactoryMetadataInvalid:
ld a, #0x01
ret
;-------------------------------------------------------------------------------
; 检查OTA元数据:魔数为OTA1,Flash中按低字节在前保存为31 41 54 4F。
; 元数据位于0x005000,固件位于0x003000;升级请求位于+0x0F。
;-------------------------------------------------------------------------------
_Asm_CheckOtaMetadata:
ldw x, #0x5000
call _Asm_SPI_ReadByteX
cp a, #0x31
jrne _Asm_OtaMetadataInvalid
incw x
call _Asm_SPI_ReadByteX
cp a, #0x41
jrne _Asm_OtaMetadataInvalid
incw x
call _Asm_SPI_ReadByteX
cp a, #0x54
jrne _Asm_OtaMetadataInvalid
incw x
call _Asm_SPI_ReadByteX
cp a, #0x4F
jrne _Asm_OtaMetadataInvalid
ldw x, #0x5004
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
ld a, 0x0000
cp a, #0x1B
jrugt _Asm_OtaMetadataInvalid
ldw x, #0x500A
call _Asm_SPI_ReadByteX
ld 0x0002, a
incw x
call _Asm_SPI_ReadByteX
ld 0x0003, a
ldw x, #0x5010
call _Asm_SPI_ReadByteX
; +0x10为升级结果,不影响请求判断;+0x0F必须为0xA5。
ldw x, #0x500F
call _Asm_SPI_ReadByteX
cp a, #0xA5
jrne _Asm_OtaMetadataInvalid
ld a, #0x30
ld 0x0004, a
ld a, #0x00
ld 0x0005, a
ld a, #0x00
ret
_Asm_OtaMetadataInvalid:
ld a, #0x01
ret
;-------------------------------------------------------------------------------
; 出厂镜像恢复主流程。
; 先逐字节计算外部镜像CRC16,校验成功后才擦除内部应用区;这样即使
; 出厂Flash内容损坏,也不会先破坏当前仍可运行的主程序。
;-------------------------------------------------------------------------------
_Asm_FactoryRecovery:
call _Asm_ValidateFactoryImage
jreq _Asm_FactoryAfterValidate
jpf _Asm_JumpApplication
_Asm_FactoryAfterValidate:
call _Asm_EraseApplication
jreq _Asm_FactoryAfterErase
jpf _Asm_JumpApplication
_Asm_FactoryAfterErase:
call _Asm_CopyFactoryImage
jreq _Asm_FactoryAfterCopy
jpf _Asm_JumpApplication
_Asm_FactoryAfterCopy:
call _Asm_VerifyApplication
jreq _Asm_FactoryAfterVerify
jpf _Asm_JumpApplication
_Asm_FactoryAfterVerify:
jpf [0x8501]
;-------------------------------------------------------------------------------
; OTA镜像搬运成功后,将结果码写入0x005010并清除升级请求0x00500F。
; 这里只写单字节,避免修改已经包含镜像描述信息的其它元数据字段。
;-------------------------------------------------------------------------------
_Asm_WriteOtaResult:
; WREN
bres 0x500F, #4
ld a, #0x06
call _Asm_SPI_Transfer
bset 0x500F, #4
; PAGE PROGRAM,地址0x005010,结果码由RAM 0x000C提供。
bres 0x500F, #4
ld a, #0x02
call _Asm_SPI_Transfer
ld a, #0x00
call _Asm_SPI_Transfer
ld a, #0x50
call _Asm_SPI_Transfer
ld a, #0x10
call _Asm_SPI_Transfer
ld a, 0x000C
call _Asm_SPI_Transfer
bset 0x500F, #4
; 等待W25Q80BV内部编程完成。
_Asm_OtaResultWait:
bres 0x500F, #4
ld a, #0x05
call _Asm_SPI_Transfer
ld a, #0xFF
call _Asm_SPI_Transfer
push a
bset 0x500F, #4
pop a
and a, #0x01
jrne _Asm_OtaResultWait
; 清除升级请求标志。
bres 0x500F, #4
ld a, #0x06
call _Asm_SPI_Transfer
bset 0x500F, #4
bres 0x500F, #4
ld a, #0x02
call _Asm_SPI_Transfer
ld a, #0x00
call _Asm_SPI_Transfer
ld a, #0x50
call _Asm_SPI_Transfer
ld a, #0x0F
call _Asm_SPI_Transfer
ld a, #0x00
call _Asm_SPI_Transfer
bset 0x500F, #4
ld a, #0x00
ret
; OTA恢复入口:复用同一套CRC、擦除、写入和回读逻辑。
_Asm_OtaRecovery:
call _Asm_ValidateFactoryImage
jreq _Asm_OtaAfterValidate
ld a, #0x02
ld 0x000C, a
call _Asm_WriteOtaResult
jpf _Asm_JumpApplication
_Asm_OtaAfterValidate:
call _Asm_EraseApplication
jreq _Asm_OtaAfterErase
ld a, #0x03
ld 0x000C, a
call _Asm_WriteOtaResult
jpf _Asm_JumpApplication
_Asm_OtaAfterErase:
call _Asm_CopyFactoryImage
jreq _Asm_OtaAfterCopy
ld a, #0x03
ld 0x000C, a
call _Asm_WriteOtaResult
jpf _Asm_JumpApplication
_Asm_OtaAfterCopy:
call _Asm_VerifyApplication
jreq _Asm_OtaAfterVerify
ld a, #0x04
ld 0x000C, a
call _Asm_WriteOtaResult
jpf _Asm_JumpApplication
_Asm_OtaAfterVerify:
ld a, #0x01
ld 0x000C, a
call _Asm_WriteOtaResult
jpf [0x8501]
;-------------------------------------------------------------------------------
; CRC16/MODBUS 单字节更新。输入A为数据,累计值保存在0x000A(低字节)、
; 0x000B(高字节)。函数不申请栈缓冲区,适合1KB RAM设备。
;-------------------------------------------------------------------------------
_Asm_CrcByte:
ld 0x000C, a
ld a, 0x000A
xor a, 0x000C
ld 0x000A, a
ld a, #8
ld 0x000D, a
_Asm_CrcBitLoop:
btjf 0x000A, #0, _Asm_CrcShiftOnly
srl 0x000B
rrc 0x000A
ld a, 0x000A
xor a, #0xA1
ld 0x000A, a
ld a, 0x000B
xor a, #0xA0
ld 0x000B, a
jra _Asm_CrcNextBit
_Asm_CrcShiftOnly:
srl 0x000B
rrc 0x000A
_Asm_CrcNextBit:
dec 0x000D
jrne _Asm_CrcBitLoop
ret
; 递减0x0000/0x0001保存的16位剩余长度,返回A=0表示减后为0。
_Asm_DecRemaining:
ld a, 0x0001
tnz a
jrne _Asm_DecRemainingLow
dec 0x0000
_Asm_DecRemainingLow:
dec 0x0001
ld a, 0x0000
or a, 0x0001
ret
;-------------------------------------------------------------------------------
; 分段读取并校验出厂镜像CRC16。
;-------------------------------------------------------------------------------
_Asm_ValidateFactoryImage:
ld a, #0xFF
ld 0x000A, a
ld 0x000B, a
ldw x, 0x0004
ldw y, 0x0000
_Asm_ValidateLoop:
ld a, 0x0000
or a, 0x0001
jreq _Asm_ValidateCompare
call _Asm_SPI_ReadByteX
call _Asm_CrcByte
incw x
call _Asm_DecRemaining
jra _Asm_ValidateLoop
_Asm_ValidateCompare:
ld a, 0x000B
cp a, 0x0002
jrne _Asm_ValidateFail
ld a, 0x000A
cp a, 0x0003
jrne _Asm_ValidateFail
ld a, #0x00
ret
_Asm_ValidateFail:
ld a, #0x01
ret
;-------------------------------------------------------------------------------
; 擦除内部应用区0x8500~0x9FFF,每次擦除一个64字节块。
;-------------------------------------------------------------------------------
_Asm_EraseApplication:
ld a, #0x56
ld 0x5062, a
ld a, #0xAE
ld 0x5062, a
ldw x, #0x8500
_Asm_EraseBlock:
ld a, #0x20
ld 0x505B, a
ld a, #0xDF
ld 0x505C, a
ld a, #0x00
ld (x), a
_Asm_EraseWait:
btjf 0x505F, #2, _Asm_EraseWait
addw x, #64
cpw x, #0xA000
jrne _Asm_EraseBlock
ld a, #0x00
ret
;-------------------------------------------------------------------------------
; 从出厂区逐字节写入内部应用区。写入后由单独校验流程回读确认。
;-------------------------------------------------------------------------------
_Asm_CopyFactoryImage:
ld a, 0x000E
ld 0x0000, a
ld a, 0x000F
ld 0x0001, a
ldw x, 0x0004
ldw y, #0x8500
_Asm_CopyLoop:
ld a, 0x0000
or a, 0x0001
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:
btjf 0x505F, #2, _Asm_CopyWait
incw x
incw y
call _Asm_DecRemaining
jra _Asm_CopyLoop
_Asm_CopyOK:
ld a, #0x00
ret
;-------------------------------------------------------------------------------
; 回读校验内部应用区。每个字节都与外部出厂镜像重新比较,避免只依赖
; Flash控制器的EOP标志;校验失败时不允许跳转到应用程序。
;-------------------------------------------------------------------------------
_Asm_VerifyApplication:
ld a, 0x000E
ld 0x0000, a
ld a, 0x000F
ld 0x0001, a
ldw x, 0x0004
ldw y, #0x8500
_Asm_VerifyLoop:
ld a, 0x0000
or a, 0x0001
jreq _Asm_VerifyOK
call _Asm_SPI_ReadByteX
ld 0x000C, a
ld a, (y)
cp a, 0x000C
jrne _Asm_VerifyFail
incw x
incw y
call _Asm_DecRemaining
jra _Asm_VerifyLoop
_Asm_VerifyOK:
ld a, #0x00
ret
_Asm_VerifyFail:
ld a, #0x01
ret
; 未使用中断:清除全局中断状态并停机,避免错误向量执行随机代码。
_Asm_DefaultInterrupt:
sim
_Asm_DefaultLoop:
jra _Asm_DefaultLoop
; 汇编中断跳板不使用C函数序言或尾声,直接跳到主程序向量槽。
_Asm_UART1Vector:
jpf 0x8550
_Asm_TIM4Vector:
jpf 0x8564