From ebef6a869491f59f6633fc7d53df756f50ba1060 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 16:12:51 +0800 Subject: [PATCH] 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 --- Makefile | 30 +- STM8_Bootloader/README.md | 2 +- STM8_Bootloader/include/bootloader_config.h | 4 +- STM8_Bootloader/include/w25q64.h | 2 +- STM8_Bootloader/src/bootloader.c | 14 +- STM8_Bootloader/src/main.c | 24 + STM8_Bootloader/src/w25q64.c | 15 +- STM8_Test/Makefile | 45 ++ STM8_Test/include/stm8_test_registers.h | 66 +++ STM8_Test/src/main.c | 76 +++ STM8_app_asm/Makefile | 33 ++ STM8_app_asm/README.md | 20 + STM8_app_asm/src/app_vectors.asm | 448 ++++++++++++++ STM8_boot_asm/Makefile | 33 ++ STM8_boot_asm/README.md | 39 ++ STM8_boot_asm/src/bootloader_vectors.asm | 618 ++++++++++++++++++++ docs/HOST_PROTOCOL_SPEC.md | 80 ++- docs/OTA_STORAGE_AND_PROTOCOL.md | 18 +- docs/TIM4_INTERRUPT_DEBUG_RECORD.md | 248 ++++++++ include/board.h | 7 +- include/business_protocol.h | 22 +- include/interrupt_control.h | 1 - include/mesh_frame.h | 2 +- include/ota_manager.h | 1 - include/stm8s003_registers.h | 1 + include/system_tick.h | 2 + include/uart1.h | 1 - include/w25q64_app.h | 2 +- src/drivers/board.c | 38 +- src/drivers/interrupt_control.c | 4 - src/drivers/system_tick.c | 19 +- src/drivers/uart1.c | 11 - src/drivers/w25q64_app.c | 17 +- src/main.c | 102 +++- src/protocol/bluetooth_manager.c | 16 +- src/protocol/business_protocol.c | 122 ++-- src/protocol/mesh_frame.c | 74 ++- src/protocol/ota_manager.c | 79 ++- tools/merge_intel_hex.py | 66 +++ tools/ota_serial_test.py | 38 +- 40 files changed, 2133 insertions(+), 307 deletions(-) create mode 100644 STM8_Test/Makefile create mode 100644 STM8_Test/include/stm8_test_registers.h create mode 100644 STM8_Test/src/main.c create mode 100644 STM8_app_asm/Makefile create mode 100644 STM8_app_asm/README.md create mode 100644 STM8_app_asm/src/app_vectors.asm create mode 100644 STM8_boot_asm/Makefile create mode 100644 STM8_boot_asm/README.md create mode 100644 STM8_boot_asm/src/bootloader_vectors.asm create mode 100644 docs/TIM4_INTERRUPT_DEBUG_RECORD.md create mode 100644 tools/merge_intel_hex.py diff --git a/Makefile b/Makefile index 02da4c1..2ad560d 100644 --- a/Makefile +++ b/Makefile @@ -11,29 +11,47 @@ OBJCOPY := packihx MAKEBIN := makebin CFLAGS := -mstm8 --std-c99 --opt-code-size -I$(INC_DIR) -# Bootloader占用0x8000~0x87FF,主程序必须从0x8800开始链接。 -LDFLAGS := -mstm8 --out-fmt-ihx --code-loc 0x8800 +# Bootloader占用0x8000~0x84FF,主程序必须从0x8500开始链接。 +LDFLAGS := -mstm8 --out-fmt-ihx --code-loc 0x8500 SOURCES := $(shell find $(SRC_DIR) -name '*.c' -print) +# 当前验证固件暂时关闭 OTA:外部 SPI Flash 驱动和 OTA 状态机源码保留在仓库中, +# 但不参与本次主程序链接,避免 SDCC 将未使用的 OTA 代码整体放入固件。 +SOURCES := $(filter-out $(SRC_DIR)/protocol/ota_manager.c \ + $(SRC_DIR)/drivers/w25q64_app.c,$(SOURCES)) OBJECTS := $(patsubst $(SRC_DIR)/%.c,$(BUILD_DIR)/%.rel,$(SOURCES)) IHX := $(BUILD_DIR)/$(PROJECT).ihx HEX := $(BUILD_DIR)/$(PROJECT).hex BIN := $(BUILD_DIR)/$(PROJECT).bin -.PHONY: all clean size +.PHONY: all full clean size all: $(HEX) $(BIN) +# 整片初次烧录目标:构建Bootloader和主程序后合并绝对地址HEX。 +# OTA仍使用主程序单独生成的BIN,不使用这个完整镜像。 +full: bootloader $(BUILD_DIR)/stm8_mesh_full.hex $(BUILD_DIR)/stm8_mesh_full.bin + +bootloader: + $(MAKE) -C STM8_Bootloader + +$(BUILD_DIR)/stm8_mesh_full.hex: STM8_Bootloader/build/stm8_bootloader.hex $(HEX) tools/merge_intel_hex.py + @mkdir -p $(dir $@) + python3 tools/merge_intel_hex.py $@ $< $(HEX) + +$(BUILD_DIR)/stm8_mesh_full.bin: $(BUILD_DIR)/stm8_mesh_full.hex + $(MAKEBIN) -p -s 0x10000 -o 0x8000 $< $@ + $(HEX): $(IHX) @mkdir -p $(dir $@) $(OBJCOPY) $< > $@ -# 生成OTA使用的裸二进制:跳过0x0000~0x87FF,只保留0x8800之后的应用程序数据。 -# -s 0x10000确保按STM8完整地址空间解析IHX;-o 0x8800去除Bootloader区域; +# 生成OTA使用的裸二进制:跳过0x0000~0x84FF,只保留0x8500之后的应用程序数据。 +# -s 0x10000确保按STM8完整地址空间解析IHX;-o 0x8500去除Bootloader区域; # -p再截断到应用程序最后一个实际占用地址,避免生成无意义的尾部空洞。 $(BIN): $(IHX) @mkdir -p $(dir $@) - $(MAKEBIN) -p -s 0x10000 -o 0x8800 $< $@ + $(MAKEBIN) -p -s 0x10000 -o 0x8500 $< $@ $(IHX): $(OBJECTS) @mkdir -p $(dir $@) diff --git a/STM8_Bootloader/README.md b/STM8_Bootloader/README.md index 7050b47..b17d45d 100644 --- a/STM8_Bootloader/README.md +++ b/STM8_Bootloader/README.md @@ -6,7 +6,7 @@ ```text 0x8000 ~ 0x83FF Bootloader,1KB -0x8400 ~ 0x9FFF Application,7KB +0x8500 ~ 0x9FFF Application,6.75KB ``` 当前版本完成的基础功能: diff --git a/STM8_Bootloader/include/bootloader_config.h b/STM8_Bootloader/include/bootloader_config.h index 2a22cbc..96db07b 100644 --- a/STM8_Bootloader/include/bootloader_config.h +++ b/STM8_Bootloader/include/bootloader_config.h @@ -10,9 +10,9 @@ /** Bootloader 在 STM8 程序 Flash 中的起始地址。 */ #define BOOTLOADER_START_ADDRESS 0x8000u /** Bootloader 预留空间,当前规划为 1KB。 */ -#define BOOTLOADER_SIZE_BYTES 0x0800u +#define BOOTLOADER_SIZE_BYTES 0x0500u /** 应用程序固定入口地址,必须与应用工程链接地址一致。 */ -#define APPLICATION_START_ADDRESS 0x8800u +#define APPLICATION_START_ADDRESS 0x8500u /** STM8S003F3P6 程序 Flash 的最后一个地址。 */ #define APPLICATION_END_ADDRESS 0x9FFFu /** 应用程序允许使用的最大空间。 */ diff --git a/STM8_Bootloader/include/w25q64.h b/STM8_Bootloader/include/w25q64.h index 90c3746..4b4ee2d 100644 --- a/STM8_Bootloader/include/w25q64.h +++ b/STM8_Bootloader/include/w25q64.h @@ -8,7 +8,7 @@ #include /** 初始化STM8硬件SPI和W25Q64片选;当前片选引脚为PD4。 */ -void W25Q64_Init(void); +uint8_t W25Q64_Init(void); /** 读取W25Q64 JEDEC ID,便于启动时确认芯片存在。 */ uint32_t W25Q64_ReadJedecId(void); /** 从W25Q64连续读取数据。 */ diff --git a/STM8_Bootloader/src/bootloader.c b/STM8_Bootloader/src/bootloader.c index 914383c..cd70d58 100644 --- a/STM8_Bootloader/src/bootloader.c +++ b/STM8_Bootloader/src/bootloader.c @@ -391,7 +391,6 @@ void Bootloader_Init(void) { Bootloader_WatchdogInit(); Bootloader_ButtonInit(); - W25Q64_Init(); } /** @@ -491,6 +490,19 @@ void Bootloader_Task(void) uint8_t upgrade_requested = 0u; uint8_t upgrade_success = 0u; uint8_t factory_button_held; + uint8_t flash_ready; + + flash_ready = W25Q64_Init(); + if (flash_ready == 0u) + { + /* 外部Flash故障不能阻止已有主程序启动;只有应用也无效时, + * 才停止喂狗并等待硬件复位。 */ + if (Bootloader_IsApplicationValid() != 0u) + { + Bootloader_JumpToApplication(); + } + while (1) {} + } factory_button_held = Bootloader_IsFactoryButtonHeld(); if (factory_button_held != 0u) diff --git a/STM8_Bootloader/src/main.c b/STM8_Bootloader/src/main.c index 5d36cf6..0580773 100644 --- a/STM8_Bootloader/src/main.c +++ b/STM8_Bootloader/src/main.c @@ -4,6 +4,30 @@ */ #include "bootloader.h" +/** + * @brief Bootloader固定TIM4向量的转发入口。 + * + * STM8S003F3的TIM4 IRQ23固定位于0x8064。Bootloader占用0x8000区域后, + * 应用程序位于0x8500,硬件不会自动改用应用程序向量表。因此这里将 + * Bootloader的TIM4中断请求转发到应用程序TIM4向量槽0x8564。 + */ +void Bootloader_TIM4_Vector(void) __interrupt(23) +{ + /* 使用项目此前已经完成实物验证的普通中断转发方式。 */ + __asm__("jpf 0x8564"); +} + +/** + * @brief Bootloader固定UART1接收向量的转发入口。 + * + * UART1接收为IRQ18,固定硬件向量地址为0x8050。主程序对应向量槽为 + * 0x8550,转发后再由主程序向量进入实际UART接收ISR。 + */ +void Bootloader_UART1_RX_Vector(void) __interrupt(18) +{ + __asm__("jpf 0x8550"); +} + /** * @brief Bootloader 入口函数。 * @return 理论上不会返回。 diff --git a/STM8_Bootloader/src/w25q64.c b/STM8_Bootloader/src/w25q64.c index ca70bfd..41ed27f 100644 --- a/STM8_Bootloader/src/w25q64.c +++ b/STM8_Bootloader/src/w25q64.c @@ -41,9 +41,13 @@ static void W25Q64_Deselect(void) */ static uint8_t W25Q64_Transfer(uint8_t value) { - while ((SPI1_SR & SPI_SR_TXE) == 0u) {} + uint16_t timeout = 60000u; + while ((SPI1_SR & SPI_SR_TXE) == 0u && timeout-- != 0u) {} + if (timeout == 0u) return 0xFFu; SPI1_DR = value; - while ((SPI1_SR & SPI_SR_RXNE) == 0u) {} + timeout = 60000u; + while ((SPI1_SR & SPI_SR_RXNE) == 0u && timeout-- != 0u) {} + if (timeout == 0u) return 0xFFu; return SPI1_DR; } @@ -93,7 +97,7 @@ static uint8_t W25Q64_WaitReady(void) * SPI采用主机模式、模式0、MSB first,并使用较低初始时钟,降低上电阶段 * 外部Flash尚未稳定时出现通信错误的概率。 */ -void W25Q64_Init(void) +uint8_t W25Q64_Init(void) { /* PC5、PC6配置为推挽输出;PC7配置为输入;PD4配置为CS#输出。 */ GPIOC_DDR |= 0x60u; @@ -106,6 +110,11 @@ void W25Q64_Init(void) SPI1_CR1 = (uint8_t)(SPI_CR1_MSTR | SPI_CR1_BR2 | SPI_CR1_BR1 | SPI_CR1_BR0 | SPI_CR1_SPE); SPI1_CR2 = (uint8_t)(SPI_CR2_SSM | SPI_CR2_SSI); + if (W25Q64_ReadJedecId() != 0xEF4014ul) + { + return 0u; + } + return 1u; } /** diff --git a/STM8_Test/Makefile b/STM8_Test/Makefile new file mode 100644 index 0000000..112fcb1 --- /dev/null +++ b/STM8_Test/Makefile @@ -0,0 +1,45 @@ +# STM8S003F3P6TR PB5独立LED测试工程。 +# 本工程不包含Bootloader;测试主程序按0x8500链接,用于和汇编Bootloader合并。 + +PROJECT := pb5_led_test +BUILD_DIR := build +CC := sdcc +PACKIHX := packihx + +CFLAGS := -mstm8 --std-c99 --opt-code-size -Iinclude +LDFLAGS := -mstm8 --out-fmt-ihx --code-loc 0x8000 +APP_LDFLAGS := -mstm8 --out-fmt-ihx --code-loc 0x8500 + +OBJECT := $(BUILD_DIR)/main.rel +IHX := $(BUILD_DIR)/$(PROJECT).ihx +HEX := $(BUILD_DIR)/$(PROJECT).hex +APP_IHX := $(BUILD_DIR)/$(PROJECT)_app.ihx +APP_HEX := $(BUILD_DIR)/$(PROJECT)_app.hex + +.PHONY: all app clean + +all: app + +# 生成供Bootloader跳转测试使用的主程序镜像,链接地址必须为0x8500。 +app: $(APP_HEX) + +$(APP_HEX): $(APP_IHX) + $(PACKIHX) $< > $@ + +$(APP_IHX): $(OBJECT) + @mkdir -p $(BUILD_DIR) + $(CC) $(APP_LDFLAGS) -o $@ $< + +$(HEX): $(IHX) + $(PACKIHX) $< > $@ + +$(IHX): $(OBJECT) + @mkdir -p $(BUILD_DIR) + $(CC) $(LDFLAGS) -o $@ $< + +$(OBJECT): src/main.c + @mkdir -p $(BUILD_DIR) + $(CC) $(CFLAGS) -c $< -o $@ + +clean: + rm -rf $(BUILD_DIR) diff --git a/STM8_Test/include/stm8_test_registers.h b/STM8_Test/include/stm8_test_registers.h new file mode 100644 index 0000000..dd5bdc8 --- /dev/null +++ b/STM8_Test/include/stm8_test_registers.h @@ -0,0 +1,66 @@ +/** + * @file stm8_test_registers.h + * @brief PB5 LED测试工程使用的最小GPIO寄存器定义。 + */ +#ifndef STM8_TEST_REGISTERS_H +#define STM8_TEST_REGISTERS_H + +#include + +/** GPIOA输出数据寄存器地址。 */ +#define GPIOA_ODR_ADDR 0x5000u +/** GPIOA数据方向寄存器地址。 */ +#define GPIOA_DDR_ADDR 0x5002u +/** GPIOA控制寄存器1地址。 */ +#define GPIOA_CR1_ADDR 0x5003u +/** GPIOA控制寄存器2地址。 */ +#define GPIOA_CR2_ADDR 0x5004u +/** GPIOB输出数据寄存器地址。 */ +#define GPIOB_ODR_ADDR 0x5005u +/** GPIOB数据方向寄存器地址。 */ +#define GPIOB_DDR_ADDR 0x5007u +/** GPIOB控制寄存器1地址,置1表示推挽输出。 */ +#define GPIOB_CR1_ADDR 0x5008u +/** GPIOB控制寄存器2地址,置0表示低速输出。 */ +#define GPIOB_CR2_ADDR 0x5009u +/** GPIOC输出数据寄存器地址。 */ +#define GPIOC_ODR_ADDR 0x500Au +/** GPIOC数据方向寄存器地址。 */ +#define GPIOC_DDR_ADDR 0x500Cu +/** GPIOC控制寄存器1地址。 */ +#define GPIOC_CR1_ADDR 0x500Du +/** GPIOC控制寄存器2地址。 */ +#define GPIOC_CR2_ADDR 0x500Eu +/** GPIOD输出数据寄存器地址。 */ +#define GPIOD_ODR_ADDR 0x500Fu +/** GPIOD数据方向寄存器地址。 */ +#define GPIOD_DDR_ADDR 0x5011u +/** GPIOD控制寄存器1地址。 */ +#define GPIOD_CR1_ADDR 0x5012u +/** GPIOD控制寄存器2地址。 */ +#define GPIOD_CR2_ADDR 0x5013u + +/** 时钟分频寄存器地址。 */ +#define CLK_CKDIVR_ADDR 0x50C6u +/** TIM4控制寄存器1地址。 */ +#define TIM4_CR1_ADDR 0x5340u +/** TIM4中断使能寄存器地址。 */ +#define TIM4_IER_ADDR 0x5343u +/** TIM4状态寄存器1地址。 */ +#define TIM4_SR1_ADDR 0x5344u +/** TIM4预分频寄存器地址。 */ +#define TIM4_PSCR_ADDR 0x5347u +/** TIM4自动重装载寄存器地址。 */ +#define TIM4_ARR_ADDR 0x5348u +/** TIM4计数器地址。 */ +#define TIM4_CNTR_ADDR 0x5346u +/** TIM4事件产生寄存器地址。 */ +#define TIM4_EGR_ADDR 0x5345u + +/** PB5对应的位掩码。 */ +#define PB5_MASK 0x20u + +/** 通过地址访问8位STM8外设寄存器。 */ +#define REG8(address) (*((volatile uint8_t *)(address))) + +#endif diff --git a/STM8_Test/src/main.c b/STM8_Test/src/main.c new file mode 100644 index 0000000..1351593 --- /dev/null +++ b/STM8_Test/src/main.c @@ -0,0 +1,76 @@ +/** + * @file main.c + * @brief STM8S003F3P6TR PB5独立硬件测试程序。 + * + * 本测试程序只把PB5配置为推挽输出,并使用TIM4中断以约500ms周期翻转。 + * 程序不初始化UART、SPI、ADC、蓝牙或看门狗,用于验证应用中断向量。 + * + * 当前测试工程直接从0x8000链接,烧录后不依赖原项目的Bootloader。 + */ +#include +#include "stm8_test_registers.h" + +/** + * 产生约500ms的软件延时。 + * + * 该延时用于LED肉眼观察,不作为正式产品的精确定时基准。实际周期会 + * 受到MCU主频、编译器优化和芯片时钟配置影响,但足以判断LED是否翻转。 + */ +static volatile uint16_t tim4_count; + +/** + * TIM4更新中断服务函数。 + * + * SDCC会根据IRQ23把该函数放入应用程序向量槽0x8564。每次中断先清除 + * TIM4更新标志,再累计1ms节拍;累计500次后直接翻转PB5,避免引入其他 + * 模块,从而只验证Bootloader到应用程序的TIM4中断链路。 + */ +void Test_TIM4_ISR(void) __interrupt(23) +{ + REG8(TIM4_SR1_ADDR) = 0x00u; + ++tim4_count; + if (tim4_count >= 500u) + { + tim4_count = 0u; + REG8(GPIOB_ODR_ADDR) ^= PB5_MASK; + } +} + +/** 初始化TIM4为约1ms更新事件:16MHz / 128 / 125 = 1kHz。 */ +static void Test_TIM4_Init(void) +{ + REG8(CLK_CKDIVR_ADDR) = 0x00u; + REG8(TIM4_CR1_ADDR) = 0x00u; + REG8(TIM4_IER_ADDR) = 0x00u; + REG8(TIM4_PSCR_ADDR) = 0x07u; + REG8(TIM4_ARR_ADDR) = 124u; + REG8(TIM4_CNTR_ADDR) = 0u; + REG8(TIM4_EGR_ADDR) = 0x01u; + REG8(TIM4_SR1_ADDR) = 0x00u; + /* 配置完成后允许TIM4更新中断。 */ + REG8(TIM4_IER_ADDR) = 0x01u; + REG8(TIM4_CR1_ADDR) = 0x01u; +} + +/** + * 固件入口函数。 + * + * PB5采用推挽输出,初始电平为低电平,TIM4中断累计500次后翻转一次。 + */ +int main(void) +{ + /* 先写入低电平,再切换为推挽输出,减少上电瞬间的错误脉冲。 */ + REG8(GPIOB_ODR_ADDR) &= (uint8_t)~PB5_MASK; + REG8(GPIOB_DDR_ADDR) |= PB5_MASK; + REG8(GPIOB_CR1_ADDR) |= PB5_MASK; + REG8(GPIOB_CR2_ADDR) &= (uint8_t)~PB5_MASK; + + tim4_count = 0u; + Test_TIM4_Init(); + __asm__("rim"); + + while (1) + { + /* PB5翻转全部由TIM4 ISR完成,主循环保持空闲。 */ + } +} diff --git a/STM8_app_asm/Makefile b/STM8_app_asm/Makefile new file mode 100644 index 0000000..24310b1 --- /dev/null +++ b/STM8_app_asm/Makefile @@ -0,0 +1,33 @@ +# STM8S003F3P6 汇编主程序实验工程。 +# 主程序固定从0x8500开始链接,与汇编Bootloader的新地址布局匹配。 + +PROJECT := stm8_app_asm +BUILD_DIR := build +AS := sdasstm8 +LD := sdldstm8 +PACKIHX := packihx + +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 + +.PHONY: all clean + +all: $(HEX) + +$(BUILD_DIR): + mkdir -p $(BUILD_DIR) + +$(OBJECT): src/app_vectors.asm | $(BUILD_DIR) + $(AS) $(ASMFLAGS) -o $@ $< + +$(IHX): $(OBJECT) + $(LD) $(LINKFLAGS) -i $@ $^ + +$(HEX): $(IHX) + $(PACKIHX) $< > $@ + +clean: + rm -rf $(BUILD_DIR) diff --git a/STM8_app_asm/README.md b/STM8_app_asm/README.md new file mode 100644 index 0000000..bd8c1c8 --- /dev/null +++ b/STM8_app_asm/README.md @@ -0,0 +1,20 @@ +# STM8 汇编主程序实验工程 + +本工程是正式 C 主程序的并行汇编实现,当前只验证最小启动链路: + +1. 应用向量表从 `0x8500` 开始; +2. Bootloader 能够跳转到应用复位入口; +3. TIM4 中断能够从 Bootloader 的固定向量转发到应用程序; +4. PB5 运行灯每约500ms反转一次。 + +应用启动后将 `CLK_CKDIVR` 配置为 HSI/1,主频为16MHz;TIM4和UART1参数均按16MHz计算。 + +当前不包含蓝牙、UART、ADC、继电器和OTA功能。后续每增加一个模块,都应先独立编译并与Bootloader合并后再进行实物验证。 + +## 构建 + +```sh +make +``` + +输出文件:`build/stm8_app_asm.hex`。 diff --git a/STM8_app_asm/src/app_vectors.asm b/STM8_app_asm/src/app_vectors.asm new file mode 100644 index 0000000..3ebfdea --- /dev/null +++ b/STM8_app_asm/src/app_vectors.asm @@ -0,0 +1,448 @@ +;------------------------------------------------------------------------------- +; STM8S003F3P6 汇编主程序最小启动工程 +; +; 应用程序从0x8500开始链接。STM8硬件中断仍然使用固定向量地址,实际产品 +; 由Bootloader在0x8064等固定向量中转发到应用程序对应向量槽。 +;------------------------------------------------------------------------------- + +.module app_vectors +.optsdcc -mstm8 + +.area HOME (ABS) + +; 每个中断向量项占4字节:0x82 + 3字节远跳转地址。 +int _App_Reset +int _App_DefaultInterrupt +int _App_DefaultInterrupt +int _App_DefaultInterrupt +int _App_DefaultInterrupt +int _App_DefaultInterrupt +int _App_DefaultInterrupt +int _App_DefaultInterrupt +int _App_DefaultInterrupt +int _App_DefaultInterrupt +int _App_DefaultInterrupt +int _App_DefaultInterrupt +int _App_DefaultInterrupt +int _App_DefaultInterrupt +int _App_DefaultInterrupt +int _App_DefaultInterrupt +int _App_DefaultInterrupt +int _App_DefaultInterrupt +int _App_DefaultInterrupt +int _App_DefaultInterrupt + +; IRQ18:UART1接收,进入汇编接收中断入口。 +int _App_UART1Vector +int _App_DefaultInterrupt +int _App_DefaultInterrupt +int _App_DefaultInterrupt +int _App_DefaultInterrupt +; IRQ23:TIM4更新中断,Bootloader固定向量会转发到这里。 +int _App_TIM4Vector + +.area CODE + +;------------------------------------------------------------------------------- +; 应用复位入口。 +; 初始化堆栈、PB5和TIM4,然后进入主循环。Bootloader跳转前不依赖应用RAM状态, +; 因此这里主动设置堆栈,保证汇编主程序可以独立运行。 +;------------------------------------------------------------------------------- +_App_Reset: + sim + ldw x, #0x03FF + ldw sp, x + ; CLK_CKDIVR:HSI默认16MHz经/8得到约2MHz;写0后改为HSI/1=16MHz。 + ; 后续TIM4和UART1参数均按16MHz配置。 + mov 0x50C6, #0x00 + + ; PB5:推挽输出,初始输出低电平。当前硬件运行灯按低电平点亮验证。 + bres 0x5005, #5 + bset 0x5007, #5 + bset 0x5008, #5 + bres 0x5009, #5 + + ; TIM4:16MHz/128/125,目标约1ms更新一次。 + mov 0x5340, #0x00 + mov 0x5343, #0x01 + mov 0x5347, #0x07 + mov 0x5348, #0x7C + mov 0x5346, #0x00 + mov 0x5345, #0x01 + mov 0x5344, #0x00 + mov 0x5340, #0x01 + + ; UART1:当前系统时钟16MHz,9600bps使用BRR1=0x68、BRR2=0x03。 + ; PD5为TX推挽输出,PD6为RX输入并开启内部上拉。 + ; 内部上拉用于保证串口线在适配器未驱动或连接接触不稳定时保持空闲高电平, + ; 不改变正常UART接收的逻辑电平。 + ; UART1引脚配置:接收中断只保存一个字节,主循环负责发送回显。 + bset 0x5011, #5 + bset 0x5012, #5 + bres 0x5011, #6 + bset 0x5012, #6 + mov 0x5235, #0x00 + mov 0x5233, #0x03 + mov 0x5232, #0x68 + mov 0x5234, #0x00 + mov 0x5236, #0x00 + ; 清理Bootloader或上电阶段可能遗留的UART状态: + ; 先读SR,再读DR。这样可以清除RXNE及可能的帧错误、噪声错误、溢出错误, + ; 防止刚开启接收中断时立即进入一次“伪接收”或把残留字节当作有效数据。 + ld a, 0x5230 + ld a, 0x5231 + ; UART接收环形缓冲区使用RAM 0x0020~0x002F,共16字节。 + ; 0x0030为写索引,0x0031为读索引,0x0032为溢出计数。 + mov 0x0030, #0x00 + mov 0x0031, #0x00 + mov 0x0032, #0x00 + ; Mesh外层解析状态:0等待F1,1等待DD,2等待09,3跳过4字节地址, + ; 4接收业务区直到0A。0x0035记录业务区长度,0x0036表示最近一帧有效。 + mov 0x0034, #0x00 + mov 0x0035, #0x00 + mov 0x0036, #0x00 + mov 0x5235, #0x2C + rim + ; 发送一次固定的0x55,仅用于现场确认TX链路、波特率和串口接线。 + ; 该字节不是业务协议数据,正式联调确认TX正常后可删除这三行。 + ld a, #0x55 + ld 0x0020, a +_App_StartupTxWait: + ld a, 0x5230 + and a, #0x80 + jreq _App_StartupTxWait + ld a, 0x0020 + ld 0x5231, a + +_App_MainLoop: + ; 主循环从环形缓冲区取出字节。UART中断只负责接收和入队, + ; 当前阶段不再对任意字节原样回显;所有字节统一交给Mesh外层状态机。 + ld a, 0x0030 + cp a, 0x0031 + jreq _App_MainLoop + ld a, 0x0031 + add a, #0x20 + ldw x, #0x0000 + ld xl, a + ld a, (x) + ld 0x0020, a + ld a, 0x0031 + inc a + and a, #0x0F + ld 0x0031, a + ; 将取出的字节交给Mesh外层帧状态机。当前仅识别帧边界, + ; 不执行继电器等业务动作。 + call _App_MeshParseByte + ld a, 0x0036 + jreq _App_MainLoop + call _App_ProcessBusiness + mov 0x0036, #0x00 + jra _App_MainLoop + +;------------------------------------------------------------------------------- +; JDY-28M Mesh外层帧状态机。 +; +; 接收格式:F1 DD 09 | SRC_H SRC_L | DST_H DST_L | BUSINESS | 0A +; 当前阶段只确认固定头、跳过源/目标地址,并以0A作为外层帧结束符。 +; 业务数据最大按20字节外层限制约束为12字节,超长帧直接重新寻找F1。 +;------------------------------------------------------------------------------- +_App_MeshParseByte: + ld a, 0x0020 + ld xl, a + ld a, 0x0034 + cp a, #0x00 + jreq _App_ParseWaitF1 + cp a, #0x01 + jreq _App_ParseWaitDD + cp a, #0x02 + jreq _App_ParseWait09 + cp a, #0x03 + jreq _App_ParseAddress + ; 状态4:业务数据区,0A表示完整外层帧结束。 + cp a, #0x04 + jreq _App_ParseBusiness + mov 0x0034, #0x00 + ret + +_App_ParseWaitF1: + ld a, 0x0020 + cp a, #0xF1 + jrne _App_ParseReturn + mov 0x0034, #0x01 + ret + +_App_ParseWaitDD: + ld a, 0x0020 + cp a, #0xDD + jrne _App_ParseReset + mov 0x0034, #0x02 + ret + +_App_ParseWait09: + ld a, 0x0020 + cp a, #0x09 + jrne _App_ParseReset + mov 0x0034, #0x03 + mov 0x0035, #0x00 + ret + +_App_ParseAddress: + ; 保存外层源地址和目标地址:前2字节为源地址,后2字节为目标地址。 + ld a, 0x0035 + add a, #0x38 + ldw x, #0x0000 + ld xl, a + ld a, 0x0020 + ld (x), a + inc 0x0035 + ld a, 0x0035 + cp a, #0x04 + jrne _App_ParseReturn + mov 0x0034, #0x04 + mov 0x0035, #0x00 + ret + +_App_ParseBusiness: + ld a, 0x0020 + cp a, #0x0A + jreq _App_ParseFrameDone + ; JDY-28M实测可能在Mesh业务数据后携带0D 0A, + ; 0D属于外层结束符的一部分,不应计入业务长度和CRC数据。 + cp a, #0x0D + jreq _App_ParseReturn + ; 保存业务区字节,后续可从0x0040开始读取CMD、LEN、DATA和CRC16。 + ld a, 0x0035 + add a, #0x40 + ldw x, #0x0000 + ld xl, a + ld a, 0x0020 + ld (x), a + inc 0x0035 + ld a, 0x0035 + cp a, #0x0C + jrult _App_ParseReturn + ; 业务区超过外层长度限制,放弃当前帧。 + mov 0x0034, #0x00 + ret + +_App_ParseFrameDone: + ; 业务区至少包含CMD、LEN和CRC16,共4字节。 + ld a, 0x0035 + cp a, #0x04 + jrult _App_ParseReset + ; LEN必须等于业务区长度减去CMD、LEN和CRC16四个字节。 + ld a, 0x0035 + sub a, #0x04 + ld 0x0037, a + ld a, 0x0041 + cp a, 0x0037 + jrne _App_ParseReset + ; 记录一帧外层和业务长度均合法的Mesh数据。 + mov 0x0036, #0x01 + mov 0x0034, #0x00 + ret + +_App_ParseReset: + mov 0x0034, #0x00 +_App_ParseReturn: + ret + +;------------------------------------------------------------------------------- +; 处理当前已通过外层和长度检查的业务帧。 +; 当前仅实现CMD=01继电器控制,CRC计算范围为业务区的CMD、LEN、DATA。 +;------------------------------------------------------------------------------- +_App_ProcessBusiness: + ; 先计算CMD、LEN、DATA的CRC,并与业务帧末尾两个字节比较; + ; CRC错误时静默丢弃,绝不执行继电器动作。 + call _App_CrcBusiness3 + ld a, 0x003D + cp a, 0x0043 + jreq _App_CrcHighOK + jpf _App_BusinessReturn +_App_CrcHighOK: + ld a, 0x003C + cp a, 0x0044 + jreq _App_CrcLowOK + jpf _App_BusinessReturn +_App_CrcLowOK: + ld a, 0x0040 + cp a, #0x01 + jreq _App_CmdOK + jpf _App_BusinessReturn +_App_CmdOK: + ld a, 0x0041 + cp a, #0x01 + jreq _App_LenOK + jpf _App_BusinessReturn +_App_LenOK: + ld a, 0x0042 + cp a, #0x01 + jreq _App_RelayOn + cp a, #0x00 + jreq _App_RelayOff + jpf _App_BusinessReturn + ; 只有DATA=00或01允许控制继电器。 +_App_RelayOff: + mov 0x5000, #0x00 + jra _App_RelayRespond +_App_RelayOn: + mov 0x5000, #0x02 + +_App_RelayRespond: + ; 业务区改写为CMD=81、LEN=1、DATA=当前控制状态。 + mov 0x0040, #0x81 + mov 0x0041, #0x01 + ld a, 0x5000 + and a, #0x02 + jreq _App_ResponseOff + mov 0x0042, #0x01 + jra _App_ResponseCrc +_App_ResponseOff: + mov 0x0042, #0x00 +_App_ResponseCrc: + call _App_CrcBusiness3 + ld a, 0x003D + ld 0x0043, a + ld a, 0x003C + ld 0x0044, a + ; JDY-28M发送格式:41 54 2B AA A1 | 目标地址 | A0 | 业务区 | 0D 0A。 + mov 0x003E, #0x41 + call _App_SendByte + mov 0x003E, #0x54 + call _App_SendByte + mov 0x003E, #0x2B + call _App_SendByte + mov 0x003E, #0xAA + call _App_SendByte + mov 0x003E, #0xA1 + call _App_SendByte + ld a, 0x0038 + ld 0x003E, a + call _App_SendByte + ld a, 0x0039 + ld 0x003E, a + call _App_SendByte + mov 0x003E, #0xA0 + call _App_SendByte + ld a, #0x00 + ld 0x003F, a +_App_SendResponseBusiness: + ld a, 0x003F + add a, #0x40 + ldw x, #0x0000 + ld xl, a + ld a, (x) + ld 0x003E, a + call _App_SendByte + inc 0x003F + ld a, 0x003F + cp a, #0x05 + jrne _App_SendResponseBusiness + ; 按JDY-28M Mesh发送协议保留完整帧尾0D 0A。 + mov 0x003E, #0x0D + call _App_SendByte + mov 0x003E, #0x0A + call _App_SendByte +_App_BusinessReturn: + ret + +; CRC-16/MODBUS,计算业务区0x0040~0x0042,结果低字节在0x003C、高字节在0x003D。 +_App_CrcBusiness3: + mov 0x003C, #0xFF + mov 0x003D, #0xFF + mov 0x003F, #0x00 +_App_CrcByte: + ld a, 0x003F + add a, #0x40 + ldw x, #0x0000 + ld xl, a + ld a, (x) + xor a, 0x003C + ld 0x003C, a + mov 0x003E, #0x08 +_App_CrcBit: + srl 0x003D + rrc 0x003C + jrnc _App_CrcNoPoly + ld a, 0x003C + xor a, #0x01 + ld 0x003C, a + ld a, 0x003D + xor a, #0xA0 + ld 0x003D, a +_App_CrcNoPoly: + dec 0x003E + jrne _App_CrcBit + inc 0x003F + ld a, 0x003F + cp a, #0x03 + jrne _App_CrcByte + ret + +_App_SendByte: + ld a, 0x5230 + and a, #0x80 + jreq _App_SendByte + ld a, 0x003E + ld 0x5231, a + ret + +;------------------------------------------------------------------------------- +; TIM4中断服务函数。 +; 每500次约1ms中断翻转一次PB5,因此目标周期约500ms。计数变量放在RAM中, +; 不使用栈上局部变量,便于后续继续控制RAM占用。 +;------------------------------------------------------------------------------- +_App_TIM4Vector: + push a + pushw x + ld a, 0x5344 + and a, #0x01 + jreq _App_TIM4Exit + mov 0x5344, #0x00 + ldw x, 0x0000 + incw x + cpw x, #500 + jrne _App_TIM4Save + ldw x, #0 + bcpl 0x5005, #5 +_App_TIM4Save: + ldw 0x0000, x +_App_TIM4Exit: + popw x + pop a + iret + +; 未使用中断统一停机,防止错误向量执行未知代码。 +_App_DefaultInterrupt: + sim +_App_DefaultLoop: + jra _App_DefaultLoop + +; UART1接收中断:读取DR清除RXNE,将字节保存到单字节测试缓冲区。 +_App_UART1Vector: + push a + pushw x + ld a, 0x5230 + and a, #0x20 + jreq _App_UART1Exit + ld a, 0x5231 + ld 0x0033, a + ld a, 0x0030 + inc a + and a, #0x0F + cp a, 0x0031 + jreq _App_UART1Exit + ld a, 0x0030 + add a, #0x20 + ldw x, #0x0000 + ld xl, a + ld a, 0x0033 + ld (x), a + ld a, 0x0030 + inc a + and a, #0x0F + ld 0x0030, a +_App_UART1Exit: + popw x + pop a + iret diff --git a/STM8_boot_asm/Makefile b/STM8_boot_asm/Makefile new file mode 100644 index 0000000..ea04535 --- /dev/null +++ b/STM8_boot_asm/Makefile @@ -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) diff --git a/STM8_boot_asm/README.md b/STM8_boot_asm/README.md new file mode 100644 index 0000000..f283a38 --- /dev/null +++ b/STM8_boot_asm/README.md @@ -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 并行存在, +未验证前不要烧录到量产设备。 diff --git a/STM8_boot_asm/src/bootloader_vectors.asm b/STM8_boot_asm/src/bootloader_vectors.asm new file mode 100644 index 0000000..75d983d --- /dev/null +++ b/STM8_boot_asm/src/bootloader_vectors.asm @@ -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 diff --git a/docs/HOST_PROTOCOL_SPEC.md b/docs/HOST_PROTOCOL_SPEC.md index 0a937e9..7453768 100644 --- a/docs/HOST_PROTOCOL_SPEC.md +++ b/docs/HOST_PROTOCOL_SPEC.md @@ -36,12 +36,13 @@ DST_L = 0C ### 1.2 接收端串口收到的 Mesh 数据格式 ```text -F1 DD 09 00 0A | SRC_H SRC_L | BUSINESS | 0D 0A +F1 DD 09 | SRC_H SRC_L | DST_H DST_L | BUSINESS | 0A ``` 这是接收方 UART 侧看到的接收格式,不是设备或主机发送时使用的格式。 -其中两字节是 JDY-28M 接收帧中携带的地址字段;业务发送方短地址仍然放在 -业务帧 `BUSINESS` 的 `SRC_H SRC_L` 字段中。 +其中前两字节是发送方短地址,后两字节是接收方短地址;业务帧中不再重复 +嵌入发送方或接收方地址。部分模块/串口工具可能显示为`0D 0A`,设备解析器 +兼容`0A`和`0D 0A`两种接收表现。 无论发送方是主机还是设备,发送时都统一使用第1.1节的格式: @@ -54,15 +55,16 @@ F1 DD 09 00 0A | SRC_H SRC_L | BUSINESS | 0D 0A JDY-28M完整Mesh帧最大20字节。当前业务帧最大DATA长度为4字节: ```text -BUSINESS = SRC_H SRC_L CMD LEN DATA CRC16_H CRC16_L +BUSINESS = CMD LEN DATA CRC16_H CRC16_L ``` -业务帧长度为`6 + LEN`,完整外层发送帧长度为`16 + LEN`,因此`LEN`最大为4。 +业务帧长度为`4 + LEN`,完整外层发送帧长度为`14 + LEN`,按20字节限制计算, +`LEN`最大为6;当前继电器控制命令使用`LEN=1`。 ## 2. 业务帧格式 ```text -SRC_H SRC_L | CMD | LEN | DATA[0..LEN-1] | CRC16_H CRC16_L +CMD | LEN | DATA[0..LEN-1] | CRC16_H CRC16_L ``` ### 2.1 CRC16规则 @@ -73,7 +75,7 @@ SRC_H SRC_L | CMD | LEN | DATA[0..LEN-1] | CRC16_H CRC16_L 初始值:0xFFFF 输入反射:是 输出反射:是 -计算范围:SRC_H、SRC_L、CMD、LEN和DATA +计算范围:CMD、LEN和DATA 发送顺序:CRC高字节在前,低字节在后 ``` @@ -88,8 +90,8 @@ CRC错误时设备直接丢弃整帧,不执行命令,也不返回错误应 ``` 当请求通过 Mesh 广播地址 `FFFF` 发送时,设备不会立即同时回复。设备会 -根据本机短地址和系统时间生成约20~170ms、按10ms分档的随机延时,然后向业务帧中的 -发送方 `SRC_H SRC_L` 单播回复,以降低多个设备同时应答造成的网络拥塞。 +根据本机短地址和系统时间生成约20~170ms、按10ms分档的随机延时,然后向外层 +接收帧中的发送方地址单播回复,以降低多个设备同时应答造成的网络拥塞。 单播请求仍然直接回复。 错误应答: @@ -118,7 +120,7 @@ CMD = 07 LEN = 00 ``` -发送方短地址位于业务帧的 `SRC_H SRC_L` 字段中,因此该命令不需要 DATA, +发送方短地址由JDY-28M外层提供,因此该命令不需要 DATA, 其他设备收到后不回复。主机或其他设备可据此更新在线状态;如果后续长期 未收到该设备的上线或业务消息,可以结合应用层超时判断设备是否离线。 @@ -154,7 +156,7 @@ DATA[2] = 需要升级的版本高字节 DATA[3] = 需要升级的版本低字节 ``` -设备短地址位于响应业务帧的`SRC_H SRC_L`字段中。主机收到多个`0x90`后, +设备短地址由JDY-28M外层提供。主机收到多个`0x90`后, 生成待升级设备列表,由人工选择设备,再对选中的短地址执行后续OTA流程。 ## 4. 主机下发的普通命令 @@ -173,7 +175,7 @@ DATA[0] = 01:继电器吸合 业务帧结构示例: ```text -SRC_H SRC_L 01 01 01 CRC16_H CRC16_L +01 01 01 CRC16_H CRC16_L ``` 设备执行后返回: @@ -191,6 +193,33 @@ DATA[0] = 当前继电器控制状态 注意:当前没有继电器触点反馈,设备只能确认PA1控制输出已经设置,不能确认触点机械动作是否真实成功。电流异常由故障检测功能另行上报。 +以主机地址`34FA`向设备地址`35FA`发送吸合命令为例,主机串口发送: + +```text +41 54 2B AA A1 35 FA A0 01 01 01 50 E0 0D 0A +``` + +设备接收侧看到: + +```text +F1 DD 09 34 FA 35 FA 01 01 01 50 E0 0A +``` + +设备向主机地址`34FA`回复: + +```text +41 54 2B AA A1 34 FA A0 81 01 01 B8 E1 0D 0A +``` + +广播吸合时,将目标地址改为`FFFF`: + +```text +41 54 2B AA A1 FF FF A0 01 01 01 50 E0 0D 0A +``` + +设备收到广播后执行控制,并向发送方地址回复`0x81`。正式多设备版本应对广播 +应答增加随机错峰延时,避免多个设备同时发送。 + ### 4.2 `0x02` 查询继电器状态 主机发送: @@ -462,7 +491,7 @@ DATA[0] = 01:校验失败 当`DATA[0]=00`时,设备已经完成外部Flash固件校验并写入升级元数据。 设备会先发送`0x8B`应答,随后自动触发MCU复位,由Bootloader把OTA固件 -搬运到内部Flash的`0x8800`区域。`0x8B`不代表Bootloader已经搬运完成, +搬运到内部Flash的`0x8500`区域。`0x8B`不代表Bootloader已经搬运完成, 主机必须继续等待设备重新启动后主动上报的`0x0D`。 ### 7.5 `0x0C` 取消OTA @@ -541,3 +570,28 @@ DATA[0] = 对应的升级结果码 设备收到确认后清除`0x005010`。 如果主机没有发送`0x8D`,设备不会清除结果记录,后续启动时可能再次上报, 便于主机发现升级结果确认丢失。 + +## 9. 整片初次烧录 + +整片初次烧录使用合并固件,不用于OTA: + +```bash +make full +``` + +生成文件: + +```text +build/stm8_mesh_full.hex:带绝对地址的完整HEX +build/stm8_mesh_full.bin:从0x8000开始的连续BIN +``` + +合并地址范围为: + +```text +0x8000~0x84FF:Bootloader +0x8500~0x9FFF:主程序 +``` + +使用ST-LINK V2/SWIM烧录时优先使用HEX文件;OTA升级仍只发送 +`build/mesh_relay.bin`,不能发送合并固件。 diff --git a/docs/OTA_STORAGE_AND_PROTOCOL.md b/docs/OTA_STORAGE_AND_PROTOCOL.md index 151ce72..cbb0b5a 100644 --- a/docs/OTA_STORAGE_AND_PROTOCOL.md +++ b/docs/OTA_STORAGE_AND_PROTOCOL.md @@ -12,10 +12,10 @@ STM8S003F3P6TR 内部程序 Flash 总容量为 8KiB,地址范围为 `0x8000~0x | 地址范围 | 容量 | 用途 | |---|---:|---| -| `0x8000~0x87FF` | 2KiB | Bootloader | -| `0x8800~0x9FFF` | 6KiB | Application 主程序 | +| `0x8000~0x84FF` | 1.25KiB | Bootloader | +| `0x8500~0x9FFF` | 6.75KiB | Application 主程序 | -应用程序必须按照 `0x8800` 重新链接,不能使用默认的 `0x8000` 起始地址。 +应用程序必须按照 `0x8500` 重新链接,不能使用默认的 `0x8000` 起始地址。 ### 1.2 W25Q80BV 外部 Flash @@ -51,9 +51,9 @@ PA3 = 0:按键按下 JDY-28M 控制脚当前分配为:`PA2=PWRC`、`PD2=RESET`。`PA2` 不再作为Flash片选。 -蓝牙状态/故障指示灯连接到 `PB5`,低电平点亮;继电器状态指示灯连接到 `PD3`,高电平点亮。 +蓝牙状态/故障指示灯连接到 `PB4`,低电平点亮;继电器状态指示灯连接到 `PD3`,高电平点亮。 -程序运行/OTA状态灯连接到 `PB4`,低电平点亮:正常运行时按500ms节奏闪烁,OTA接收或校验时按约100ms节奏快速闪烁。 +程序运行/OTA状态灯连接到 `PB5`,低电平点亮:正常运行时按500ms节奏闪烁,OTA接收或校验时按约100ms节奏快速闪烁。 ## 2. 元数据格式 @@ -134,7 +134,7 @@ Bootloader负责: - 清除升级请求; - 检查内部应用程序是否有效; - 应用损坏时恢复出厂固件; -- 跳转到 `0x8800` 启动主程序。 +- 跳转到 `0x8500` 启动主程序。 升级安全顺序必须是: @@ -218,12 +218,12 @@ SRC_H | SRC_L | CMD | LEN | DATA | CRC16_L | CRC16_H 说明: -- `SRC_H SRC_L`:发送方 Mesh 短地址,高字节在前;设备应答时将该地址作为 - JDY-28M 外层目标地址,业务帧中的 SRC 改为设备自身短地址; +- JDY-28M 外层地址:发送方和接收方 Mesh 短地址均由外层提供,高字节在前; + 设备应答时将外层发送方地址作为目标地址; - `CMD`:命令; - `LEN`:DATA长度; - `DATA`:命令数据; -- `CRC16`:CRC-16/MODBUS,计算范围为 SRC、CMD、LEN、DATA,低字节在前; +- `CRC16`:CRC-16/MODBUS,计算范围为 CMD、LEN、DATA,高字节在前; - CRC错误时静默丢弃,不执行继电器动作,不返回错误帧。 JDY-28M的Mesh外层帧负责目标地址、源地址、Mesh头和帧尾,业务帧本身不重复添加地址。 diff --git a/docs/TIM4_INTERRUPT_DEBUG_RECORD.md b/docs/TIM4_INTERRUPT_DEBUG_RECORD.md new file mode 100644 index 0000000..521f7ae --- /dev/null +++ b/docs/TIM4_INTERRUPT_DEBUG_RECORD.md @@ -0,0 +1,248 @@ +# STM8S003F3 TIM4 中断问题排查记录 + +## 1. 问题现象 + +正式主程序烧录后,PB5 程序运行指示灯没有按照约 500 ms 周期闪烁。 + +此前已经确认: + +- 独立 PB5 测试程序可以正常闪烁; +- 测试主程序链接到 `0x8800` 后,Bootloader 可以正常跳转; +- PB5 GPIO 连接和芯片引脚基本正常; +- 正式主程序最初还存在代码超出应用区的问题,但缩减为最小主循环后,问题依然与系统节拍有关。 + +## 2. 逐步排查过程 + +### 2.1 先屏蔽 OTA + +正式主程序在初始化阶段先调用 OTA 和外部 SPI Flash 初始化。为排除 W25Q80BV 初始化阻塞,临时屏蔽: + +```c +OtaManager_Init(); +OtaManager_Task(); +``` + +结果:PB5 仍然没有正常闪烁,因此问题不只在 OTA 初始化。 + +### 2.2 再屏蔽蓝牙和 Mesh + +继续屏蔽蓝牙状态机、Mesh 任务和上线广播,避免 UART 等待影响主循环。 + +结果:PB5 仍然没有正常闪烁,蓝牙任务不是唯一原因。 + +### 2.3 缩减为最小主循环 + +最终只保留: + +```text +Board_Init() +软件延时 +PB5电平翻转 +``` + +并临时关闭: + +- TIM4; +- 全局中断; +- 看门狗; +- UART; +- ADC; +- 蓝牙; +- Mesh; +- 故障管理; +- OTA。 + +结果:PB5 可以正常闪烁。 + +由此确认 Bootloader 跳转、主程序入口和 PB5 GPIO 硬件均正常。 + +### 2.4 逐步恢复 TIM4 + +恢复 TIM4 和正式 `Board_StatusLedTask()` 后,PB5 又变成常亮。 + +随后使用软件延时配合 TIM4、直接读取 `SystemTick_NowMs()`、轮询 `TIM4_SR` 等方法进行区分,最终发现主程序使用 Bootloader 后,应用程序的中断向量不能直接被硬件访问。 + +## 3. 最终原因 + +STM8S003F3 的硬件中断向量地址固定在程序 Flash 起始区域。 + +官方数据手册规定: + +```text +TIM4 IRQ23 向量地址:0x8064 +UART1 RX IRQ18 向量地址:0x8050 +``` + +当前工程的存储布局为: + +```text +0x8000 ~ 0x87FF:Bootloader +0x8800 ~ 0x9FFF:Application +``` + +主程序虽然链接到 `0x8800`,但硬件中断不会自动把向量表移动到 `0x8800`。如果直接在主程序中声明: + +```c +void SystemTick_TIM4_ISR(void) __interrupt(23); +``` + +硬件发生 TIM4 中断时,仍然访问固定地址 `0x8064`,而不是主程序向量表中的 `0x8864`。 + +因此 Bootloader 跳转正常,但应用程序的 TIM4 中断处理函数没有被正确调用,系统毫秒计数不会正常驱动 PB5 状态灯、超时判断和故障计时。 + +## 4. 修正方案 + +采用 Bootloader 固定向量转发方案: + +```text +TIM4硬件中断 + ↓ +Bootloader固定向量 0x8064 + ↓ +转发到主程序向量 0x8864 + ↓ +主程序 SystemTick_TIM4_ISR() + ↓ +SystemTick_TIM4_Handler() +``` + +主程序中保留自己的 TIM4 向量入口: + +```c +void SystemTick_TIM4_ISR(void) __interrupt(23) +{ + SystemTick_TIM4_Handler(); +} +``` + +Bootloader 在固定的 IRQ23 入口中跳转到主程序 TIM4 向量槽: + +```c +void Bootloader_TIM4_Vector(void) __interrupt(23) +{ + __asm__("jpf 0x8864"); +} +``` + +这样主程序内部的实际 ISR 地址可以变化,Bootloader 只需要转发到主程序固定向量槽,不需要写死主程序 ISR 的最终代码地址。 + +## 5. 验证结果 + +修正后使用整片固件烧录: + +```text +build/stm8_mesh_full.hex +``` + +烧录起始地址: + +```text +0x8000 +``` + +PB5 已经能够按照约 500 ms 周期正常闪烁,说明以下链路已经验证通过: + +```text +TIM4 → 0x8064 → Bootloader转发 → 0x8864 → 主程序ISR → 系统毫秒计数 +``` + +## 6. 经验和教训 + +### 6.1 Bootloader 分区不仅影响代码地址,也影响中断体系 + +把主程序从 `0x8000` 移动到 `0x8800`,不能只修改链接地址。还必须同步设计: + +- 中断向量转发; +- 应用程序复位入口; +- Bootloader 跳转前的中断清理; +- Bootloader 和主程序外设状态交接。 + +### 6.2 独立 GPIO 测试不能证明系统节拍正常 + +PB5 独立测试程序只验证了: + +- Bootloader 能跳转; +- GPIO 能输出; +- LED 硬件正常。 + +它不能证明 TIM4、UART 中断或应用程序中断向量正常。因此系统功能应分层验证。 + +### 6.3 必须先做最小可运行程序 + +本次通过逐步屏蔽模块,最终定位到: + +```text +Bootloader跳转和GPIO正常 +业务模块不是第一嫌疑 +系统节拍/中断链路异常 +``` + +以后遇到“主程序没有运行”的问题,应优先建立最小固件: + +```text +GPIO + 软件延时 +GPIO + 定时器轮询 +GPIO + 定时器中断 +GPIO + 看门狗 +最后再逐个恢复业务模块 +``` + +### 6.4 诊断信号的翻转频率必须适合肉眼观察 + +曾经使用“每 1 ms 翻转一次 PB5”的测试方法。该信号约为 500 Hz,LED 肉眼看起来会接近常亮,容易造成误判。 + +诊断 LED 建议使用: + +- 100 ms; +- 500 ms; +- 1 s; + +等肉眼可以明确区分的周期。 + +### 6.5 LED 有效电平必须以实物为准 + +程序注释、原理图标注和实际 LED 驱动极性必须保持一致。调试时应直接测量 MCU 引脚电平,确认: + +```text +PB5 = 1 时 LED 是否点亮 +PB5 = 0 时 LED 是否熄灭 +``` + +不能只根据变量名或旧注释判断。 + +### 6.6 应用固件大小必须按地址范围判断 + +主程序的有效空间是: + +```text +0x8800 ~ 0x9FFF +``` + +不能只看 `mesh_relay.bin` 的文件大小。必须检查链接 map 中的最高代码地址,确认没有超过 `0x9FFF`。 + +### 6.7 中断向量转发必须随新增中断同步维护 + +当前已验证 TIM4 转发。后续启用 UART 接收中断时,还必须增加: + +```text +UART1 RX IRQ18 +固定向量:0x8050 +主程序向量槽:0x8850 +``` + +后续如果 ADC 使用中断,还要增加 ADC1 IRQ22 的转发。 + +## 7. 后续恢复顺序 + +建议严格按照以下顺序继续: + +1. 保持 TIM4 中断转发,验证 PB5 正式状态灯; +2. 恢复看门狗并验证不复位; +3. 恢复 UART 初始化和 UART 接收中断转发; +4. 验证 JDY-28M AT 指令通信; +5. 恢复 ADC 电流采样; +6. 恢复蓝牙 Mesh 状态机; +7. 恢复业务协议和故障管理; +8. 最后恢复 OTA 和 W25Q80BV 访问。 + +每恢复一个模块,都应重新编译、烧录并进行实物验证,不能一次性恢复所有模块后再统一排查。 diff --git a/include/board.h b/include/board.h index a9a5815..5a3e318 100644 --- a/include/board.h +++ b/include/board.h @@ -3,8 +3,9 @@ * @brief 蓝牙 Mesh 继电器硬件板级接口定义。 * * 本阶段先提供稳定的硬件抽象接口,不在业务层直接操作 GPIO 寄存器。 - * PA1 的有效电平已经确认:高电平吸合、低电平断开;PB5 为低电平点亮的 - * 蓝牙组网/故障指示灯,PD3 为高电平点亮的继电器状态灯。 + * PA1 的有效电平已经确认:高电平吸合、低电平断开;PB4 为低电平点亮的 + * 蓝牙组网/故障指示灯,PB5 为低电平点亮的程序运行/OTA状态灯,PD3 为 + * 高电平点亮的继电器状态灯。 */ #ifndef BOARD_H #define BOARD_H @@ -29,7 +30,7 @@ void Board_SetLed3(uint8_t on); /** 设置继电器状态灯,1表示点亮,0表示熄灭;硬件高电平点亮。 */ void Board_SetRelayLed(uint8_t on); -/** 周期驱动PB4程序状态灯;正常运行500ms翻转,OTA期间100ms翻转。 */ +/** 周期驱动PB5程序状态灯;硬件低电平点亮,正常运行500ms翻转,OTA期间100ms翻转。 */ void Board_StatusLedTask(uint8_t ota_active); /** 设置 PWRC:1 表示拉低进入 AT 窗口,0 表示恢复高电平通信。 */ diff --git a/include/business_protocol.h b/include/business_protocol.h index 0a89cc2..de43fd2 100644 --- a/include/business_protocol.h +++ b/include/business_protocol.h @@ -2,9 +2,8 @@ * @file business_protocol.h * @brief STM8 业务帧协议接口。 * - * 业务帧格式:SRC_H SRC_L | CMD | LEN | DATA | CRC16_H CRC16_L。 - * SRC 为发送方的 Mesh 短地址;CRC 使用 CRC-16/MODBUS,计算范围为 - * SRC、CMD、LEN 和 DATA,线上高字节在前。 + * 业务帧格式:CMD | LEN | DATA | CRC16_H CRC16_L。 + * 发送方和接收方短地址由JDY-28M外层提供;CRC计算范围为CMD、LEN和DATA。 */ #ifndef BUSINESS_PROTOCOL_H #define BUSINESS_PROTOCOL_H @@ -14,9 +13,6 @@ /** 业务帧最大 DATA 长度,需为 JDY-28M 外层 20 字节限制预留空间。 */ #define BUSINESS_MAX_DATA_LENGTH 4u -/** 发送方短地址字段长度。 */ -#define BUSINESS_SOURCE_ADDRESS_LENGTH 2u - /** 业务命令:继电器控制。 */ #define CMD_RELAY_CONTROL 0x01u @@ -89,7 +85,7 @@ void BusinessProtocol_QueueDelayedResponse(uint16_t address, * 向解析器输入一个字节。 * * 该接口只负责解析内层业务帧,不执行继电器动作,也不负责 JDY-28M - * 外层 Mesh 封装。外层模块应在确认 `F1 DD 09 00 0A ... 0D 0A` 后, + * 外层 Mesh 封装。外层模块应在确认 `F1 DD 09 SRC DST ... 0A` 后, * 将其中的业务数据逐字节送入本接口。 * * @param value 输入字节。 @@ -97,19 +93,13 @@ void BusinessProtocol_QueueDelayedResponse(uint16_t address, */ uint8_t BusinessProtocol_InputByte(uint8_t value); -/** 读取最近一帧合法业务帧的命令字。 */ -uint8_t BusinessProtocol_GetCommand(void); - -/** 读取最近一帧合法业务帧的数据长度。 */ -uint8_t BusinessProtocol_GetDataLength(void); - -/** 读取最近一帧合法业务帧携带的发送方 Mesh 短地址。 */ -uint16_t BusinessProtocol_GetSourceAddress(void); +/** 设置当前外层接收帧携带的发送方地址,供业务应答使用。 */ +void BusinessProtocol_SetSourceAddress(uint16_t address); /** 执行最近一帧已经通过校验的业务命令,并向源地址返回应答。 */ void BusinessProtocol_HandleFrame(void); -/** CRC-16/MODBUS 计算函数,输入范围包含发送方地址字段。 */ +/** CRC-16/MODBUS计算函数,输入范围为CMD、LEN和DATA。 */ uint16_t BusinessProtocol_Crc16Modbus(const uint8_t *data, uint16_t length); #endif diff --git a/include/interrupt_control.h b/include/interrupt_control.h index 040d8f3..edcb647 100644 --- a/include/interrupt_control.h +++ b/include/interrupt_control.h @@ -12,6 +12,5 @@ void Interrupts_Enable(void); /** 关闭 STM8 全局中断,用于极短的不可打断临界区。 */ -void Interrupts_Disable(void); #endif diff --git a/include/mesh_frame.h b/include/mesh_frame.h index e4da897..00baacf 100644 --- a/include/mesh_frame.h +++ b/include/mesh_frame.h @@ -7,7 +7,7 @@ #include -/** JDY-28M 外层完整帧最大长度,包含帧头、地址、数据和 0D 0A。 */ +/** JDY-28M 外层完整帧最大长度,包含帧头、双地址、数据和结束符。 */ #define JDY_MESH_FRAME_MAX_LENGTH 20u /** 初始化 JDY-28M 外层帧状态机。 */ diff --git a/include/ota_manager.h b/include/ota_manager.h index 2f8e942..2170c0f 100644 --- a/include/ota_manager.h +++ b/include/ota_manager.h @@ -27,7 +27,6 @@ uint8_t OtaManager_GetExpectedOffset(uint16_t *offset); * 读取Bootloader写入的升级结果并准备由主程序主动上报。 * @return 0表示没有待上报结果,非0表示升级结果码。 */ -uint8_t OtaManager_GetBootloaderResult(void); /** 主机确认升级结果后清除待上报状态。 */ void OtaManager_AcknowledgeBootloaderResult(void); diff --git a/include/stm8s003_registers.h b/include/stm8s003_registers.h index 53ac7f2..4e02e06 100644 --- a/include/stm8s003_registers.h +++ b/include/stm8s003_registers.h @@ -12,6 +12,7 @@ /* GPIO 端口寄存器布局:ODR、IDR、DDR、CR1、CR2。 */ #define GPIOA_ODR_ADDR 0x5000u +#define CLK_CKDIVR_ADDR 0x50C6u #define GPIOA_IDR_ADDR 0x5001u #define GPIOA_DDR_ADDR 0x5002u #define GPIOA_CR1_ADDR 0x5003u diff --git a/include/system_tick.h b/include/system_tick.h index 1f0b6ed..db9aa3b 100644 --- a/include/system_tick.h +++ b/include/system_tick.h @@ -15,5 +15,7 @@ uint32_t SystemTick_NowMs(void); /** 在定时器中断服务程序中递增系统节拍。 */ void SystemTick_InterruptHandler(void); +/** TIM4中断处理函数主体,由主程序固定中断向量入口调用。 */ +void SystemTick_TIM4_Handler(void); #endif diff --git a/include/uart1.h b/include/uart1.h index f09cd87..7c67c77 100644 --- a/include/uart1.h +++ b/include/uart1.h @@ -11,7 +11,6 @@ void Uart1_Init(void); /** UART任务入口;接收硬件由中断处理,此接口保留为空操作。 */ -void Uart1_Task(void); /** 向 UART1 发送一个字节。 */ void Uart1_WriteByte(uint8_t value); diff --git a/include/w25q64_app.h b/include/w25q64_app.h index 3511255..e17616f 100644 --- a/include/w25q64_app.h +++ b/include/w25q64_app.h @@ -2,7 +2,7 @@ #ifndef W25Q64_APP_H #define W25Q64_APP_H #include -void W25Q64App_Init(void); +uint8_t W25Q64App_Init(void); uint8_t W25Q64App_ReadByte(uint32_t address, uint8_t *value); uint8_t W25Q64App_Read(uint32_t address, uint8_t *buffer, uint16_t length); uint8_t W25Q64App_EraseSector(uint32_t address); diff --git a/src/drivers/board.c b/src/drivers/board.c index d08476f..6e2376b 100644 --- a/src/drivers/board.c +++ b/src/drivers/board.c @@ -12,13 +12,13 @@ /** 当前 PA1 继电器控制输出的逻辑状态。 */ static uint8_t relay_state; -/** 当前 PB5 蓝牙组网/故障指示灯的逻辑状态。 */ +/** 当前 PB4 蓝牙组网/故障指示灯的逻辑状态。 */ static uint8_t led3_state; /** 当前 PD3 继电器状态灯的逻辑状态。 */ static uint8_t relay_led_state; -/** PB4程序状态灯当前逻辑状态,1表示逻辑点亮。 */ +/** PB5程序状态灯当前逻辑状态,1表示逻辑点亮;硬件为高电平点亮。 */ static uint8_t status_led_state; -/** 上次翻转PB4状态灯的系统时间。 */ +/** 上次翻转PB5状态灯的系统时间。 */ static uint32_t status_led_last_ms; /** 初始化板级资源,并将继电器置于安全断开状态。 */ @@ -45,11 +45,11 @@ void Board_Init(void) REG8(GPIOD_CR1_ADDR) |= PD2_MASK; REG8(GPIOD_CR2_ADDR) &= (uint8_t)~PD2_MASK; - /* PB5:低电平点亮,默认熄灭蓝牙组网/故障指示灯。 */ - REG8(GPIOB_DDR_ADDR) |= PB5_MASK; - REG8(GPIOB_CR1_ADDR) |= PB5_MASK; - REG8(GPIOB_CR2_ADDR) &= (uint8_t)~PB5_MASK; - REG8(GPIOB_ODR_ADDR) |= PB5_MASK; + /* PB4:低电平点亮,默认熄灭蓝牙组网/故障指示灯。 */ + REG8(GPIOB_DDR_ADDR) |= PB4_MASK; + REG8(GPIOB_CR1_ADDR) |= PB4_MASK; + REG8(GPIOB_CR2_ADDR) &= (uint8_t)~PB4_MASK; + REG8(GPIOB_ODR_ADDR) |= PB4_MASK; /* PD3:高电平点亮,默认熄灭继电器状态灯。 */ REG8(GPIOD_DDR_ADDR) |= PD3_MASK; @@ -63,11 +63,11 @@ void Board_Init(void) status_led_state = 0u; status_led_last_ms = 0ul; - /* PB4:低电平点亮,默认熄灭,用于程序运行/OTA状态指示。 */ - REG8(GPIOB_DDR_ADDR) |= PB4_MASK; - REG8(GPIOB_CR1_ADDR) |= PB4_MASK; - REG8(GPIOB_CR2_ADDR) &= (uint8_t)~PB4_MASK; - REG8(GPIOB_ODR_ADDR) |= PB4_MASK; + /* PB5:高电平点亮。初始化为低电平,避免上电瞬间误点亮。 */ + REG8(GPIOB_DDR_ADDR) |= PB5_MASK; + REG8(GPIOB_CR1_ADDR) |= PB5_MASK; + REG8(GPIOB_CR2_ADDR) &= (uint8_t)~PB5_MASK; + REG8(GPIOB_ODR_ADDR) &= (uint8_t)~PB5_MASK; } /** 设置继电器状态,后续接入 PA1 寄存器时保持高电平吸合约定。 */ @@ -96,17 +96,17 @@ uint8_t Board_ReadMeshJoined(void) return ((REG8(GPIOC_IDR_ADDR) & PC3_MASK) != 0u) ? 1u : 0u; } -/** 设置PB5蓝牙组网/故障指示灯;由于低电平点亮,逻辑值需要反相输出。 */ +/** 设置PB4蓝牙组网/故障指示灯;由于低电平点亮,逻辑值需要反相输出。 */ void Board_SetLed3(uint8_t on) { led3_state = (on != 0u) ? 1u : 0u; if (led3_state != 0u) { - REG8(GPIOB_ODR_ADDR) &= (uint8_t)~PB5_MASK; + REG8(GPIOB_ODR_ADDR) &= (uint8_t)~PB4_MASK; } else { - REG8(GPIOB_ODR_ADDR) |= PB5_MASK; + REG8(GPIOB_ODR_ADDR) |= PB4_MASK; } } @@ -125,7 +125,7 @@ void Board_SetRelayLed(uint8_t on) } /** - * @brief 周期驱动PB4程序状态灯。 + * @brief 周期驱动PB5程序状态灯。 * @param ota_active 1表示OTA接收/校验中,0表示正常运行。 */ void Board_StatusLedTask(uint8_t ota_active) @@ -137,9 +137,9 @@ void Board_StatusLedTask(uint8_t ota_active) status_led_last_ms = now; status_led_state = (status_led_state == 0u) ? 1u : 0u; if (status_led_state != 0u) - REG8(GPIOB_ODR_ADDR) &= (uint8_t)~PB4_MASK; + REG8(GPIOB_ODR_ADDR) |= PB5_MASK; else - REG8(GPIOB_ODR_ADDR) |= PB4_MASK; + REG8(GPIOB_ODR_ADDR) &= (uint8_t)~PB5_MASK; } } diff --git a/src/drivers/interrupt_control.c b/src/drivers/interrupt_control.c index 1ecb7a3..eee37e4 100644 --- a/src/drivers/interrupt_control.c +++ b/src/drivers/interrupt_control.c @@ -25,7 +25,3 @@ void Interrupts_Enable(void) * STM8 `sim` 指令会屏蔽可屏蔽中断。临界区应尽可能短,不能在关闭中断后 * 执行 UART 等待、EEPROM 写入或长时间循环,否则可能导致系统节拍丢失。 */ -void Interrupts_Disable(void) -{ - __asm__("sim"); -} diff --git a/src/drivers/system_tick.c b/src/drivers/system_tick.c index facd975..8fa97fb 100644 --- a/src/drivers/system_tick.c +++ b/src/drivers/system_tick.c @@ -20,13 +20,20 @@ static volatile uint32_t system_tick_ms; void SystemTick_Init(void) { system_tick_ms = 0ul; + /* 显式选择HSI不分频,确保TIM4获得稳定的16MHz主时钟。 */ + REG8(CLK_CKDIVR_ADDR) = 0x00u; + /* 先停止定时器并关闭更新中断,避免配置过程中产生伪中断。 */ REG8(TIM4_CR1_ADDR) = 0x00u; - REG8(TIM4_IER_ADDR) = 0x01u; + REG8(TIM4_IER_ADDR) = 0x00u; REG8(TIM4_PSCR_ADDR) = 0x07u; REG8(TIM4_ARR_ADDR) = 124u; REG8(TIM4_CNTR_ADDR) = 0u; + /* 产生一次更新事件,使预分频器和自动重装值立即装载。 */ REG8(TIM4_EGR_ADDR) = 0x01u; + /* 更新事件可能置位 UIF,打开中断前必须先清除。 */ REG8(TIM4_SR1_ADDR) = 0x00u; + /* 所有配置和标志清理完成后,最后才打开更新中断。 */ + REG8(TIM4_IER_ADDR) = 0x01u; REG8(TIM4_CR1_ADDR) = 0x01u; } @@ -48,11 +55,9 @@ void SystemTick_InterruptHandler(void) * STM8S003 的 TIM4 更新中断向量号为 23。本函数只做清除中断标志和递增 * 软件毫秒计数,禁止在中断中执行 UART、EEPROM 或 Mesh 发送等耗时操作。 */ -void SystemTick_TIM4_ISR(void) __interrupt(23) +void SystemTick_TIM4_Handler(void) { - if ((REG8(TIM4_SR1_ADDR) & 0x01u) != 0u) - { - REG8(TIM4_SR1_ADDR) = 0x00u; - SystemTick_InterruptHandler(); - } + /* 进入TIM4更新中断后先清除标志,防止同一中断请求反复进入。 */ + REG8(TIM4_SR1_ADDR) = 0x00u; + SystemTick_InterruptHandler(); } diff --git a/src/drivers/uart1.c b/src/drivers/uart1.c index 7e57748..d1a8de5 100644 --- a/src/drivers/uart1.c +++ b/src/drivers/uart1.c @@ -54,11 +54,6 @@ void Uart1_Init(void) * 接收字节已经由 UART1 接收中断写入环形缓冲区,因此主循环不再轮询UART * 硬件寄存器。保留该接口是为了维持主循环结构和后续扩展接口兼容性。 */ -void Uart1_Task(void) -{ - /* UART硬件接收由Uart1_InterruptHandler()完成,此处无需执行任何操作。 */ -} - /** 发送一个字节;当前阶段不连接真实 UART 寄存器。 */ void Uart1_WriteByte(uint8_t value) { @@ -110,9 +105,3 @@ void Uart1_InterruptHandler(void) Uart1_RxPush(value); } } - -/** UART1接收寄存器非空中断向量,IRQ号19。 */ -void Uart1_RX_IRQHandler(void) __interrupt(19) -{ - Uart1_InterruptHandler(); -} diff --git a/src/drivers/w25q64_app.c b/src/drivers/w25q64_app.c index 6e51542..d03162e 100644 --- a/src/drivers/w25q64_app.c +++ b/src/drivers/w25q64_app.c @@ -4,12 +4,14 @@ #define W25_CS_MASK PD4_MASK #define W25_CMD_READ 0x03u +#define W25_CMD_JEDEC_ID 0x9Fu #define W25_CMD_PROGRAM 0x02u #define W25_CMD_WREN 0x06u #define W25_CMD_RDSR 0x05u #define W25_CMD_ERASE_4K 0x20u #define W25_BUSY 0x01u #define W25_RESULT_ADDR 0x005010ul +static uint8_t w25_ready; static uint8_t Xfer(uint8_t value) { @@ -30,7 +32,7 @@ static uint8_t Ready(void) } static void WriteEnable(void){Select();Xfer(W25_CMD_WREN);Deselect();} -void W25Q64App_Init(void) +uint8_t W25Q64App_Init(void) { REG8(GPIOC_DDR_ADDR)|=(uint8_t)(PC5_MASK|PC6_MASK); REG8(GPIOC_CR1_ADDR)|=(uint8_t)(PC5_MASK|PC6_MASK); @@ -41,10 +43,15 @@ void W25Q64App_Init(void) Deselect(); SPI1_CR1=(uint8_t)(SPI_CR1_MSTR|SPI_CR1_BR2|SPI_CR1_BR1|SPI_CR1_BR0|SPI_CR1_SPE); SPI1_CR2=(uint8_t)(SPI_CR2_SSM|SPI_CR2_SSI); + Select(); Xfer(W25_CMD_JEDEC_ID); + w25_ready = (Xfer(0xFFu) == 0xEFu && Xfer(0xFFu) == 0x40u && + Xfer(0xFFu) == 0x14u) ? 1u : 0u; + Deselect(); + return w25_ready; } uint8_t W25Q64App_ReadByte(uint32_t address,uint8_t *value) { - if(value==(uint8_t *)0)return 0u; + if(!w25_ready || value==(uint8_t *)0)return 0u; Select(); Xfer(W25_CMD_READ); Xfer((uint8_t)(address>>16)); Xfer((uint8_t)(address>>8)); Xfer((uint8_t)address); *value=Xfer(0xFFu); Deselect(); return 1u; @@ -52,7 +59,7 @@ uint8_t W25Q64App_ReadByte(uint32_t address,uint8_t *value) uint8_t W25Q64App_Read(uint32_t address,uint8_t *buffer,uint16_t length) { uint16_t index; - if(buffer==(uint8_t *)0)return 0u; + if(!w25_ready || buffer==(uint8_t *)0)return 0u; Select(); Xfer(W25_CMD_READ); Xfer((uint8_t)(address>>16)); Xfer((uint8_t)(address>>8)); Xfer((uint8_t)address); for(index=0u;index>16)); Xfer((uint8_t)(address>>8)); Xfer((uint8_t)address); Deselect(); @@ -70,7 +78,7 @@ uint8_t W25Q64App_PageProgram(uint32_t address,const uint8_t *data, uint16_t length) { uint16_t index; - if(data==(const uint8_t *)0 || length==0u || length>256u || + if(!w25_ready || data==(const uint8_t *)0 || length==0u || length>256u || (((uint16_t)address&0xFFu)+length)>256u)return 0u; WriteEnable(); Select(); Xfer(W25_CMD_PROGRAM); Xfer((uint8_t)(address>>16)); Xfer((uint8_t)(address>>8)); @@ -82,6 +90,7 @@ uint8_t W25Q64App_PageProgram(uint32_t address,const uint8_t *data, uint8_t W25Q64App_ClearUpgradeResult(void) { uint8_t zero=0u; + if(!w25_ready)return 0u; WriteEnable(); Select(); Xfer(W25_CMD_PROGRAM); Xfer((uint8_t)(W25_RESULT_ADDR >> 16)); Xfer((uint8_t)(W25_RESULT_ADDR >> 8)); diff --git a/src/main.c b/src/main.c index eaac555..73d9bab 100644 --- a/src/main.c +++ b/src/main.c @@ -19,7 +19,39 @@ #include "bluetooth_manager.h" #include "watchdog.h" #include "interrupt_control.h" -#include "ota_manager.h" +#include "stm8s003_registers.h" + +/** + * @brief 主程序固定的TIM4中断向量入口。 + * + * STM8硬件中断向量表相对于应用程序起始地址固定布局。TIM4 IRQ23在主 + * 程序向量表中的位置为0x8500 + 0x64 = 0x8564, + * 由SDCC根据__interrupt(23)自动生成入口。真正的计时处理集中在驱动层。 + */ +/** TIM4诊断中断计数,累计500次后翻转一次PB5。 */ +static volatile uint16_t tim4_test_count; + +void SystemTick_TIM4_ISR(void) __interrupt(23) +{ + REG8(TIM4_SR1_ADDR) = 0x00u; + ++tim4_test_count; + if (tim4_test_count >= 500u) + { + tim4_test_count = 0u; + REG8(GPIOB_ODR_ADDR) ^= PB5_MASK; + } +} + +/** + * @brief 主程序固定的UART1接收中断向量入口。 + * + * STM8S003F3官方中断表中,UART1接收中断为IRQ18,对应主程序向量槽 + * 0x8550。实际接收处理仍集中在uart1.c,避免中断向量入口混入业务逻辑。 + */ +void Uart1_RX_IRQHandler(void) __interrupt(18) +{ + Uart1_InterruptHandler(); +} /** 标记本次上电是否已经完成设备上线广播,避免主循环重复发送。 */ static uint8_t startup_broadcast_sent; @@ -36,22 +68,45 @@ static uint8_t startup_broadcast_sent; int main(void) { Board_Init(); + /* 初始化TIM4并打开其更新中断,验证裸汇编跳板能够正常返回主程序。 */ SystemTick_Init(); Interrupts_Enable(); - Uart1_Init(); - Current_Init(); - BusinessProtocol_Init(); - MeshFrame_Init(); - FaultManager_Init(); - BluetoothManager_Init(); - OtaManager_Init(); - Watchdog_Init(); + tim4_test_count = 0u; + /* + * 当前为最小主循环诊断版本。暂时不初始化 UART、ADC、业务协议、 + * Mesh、故障管理和蓝牙模块,先确认 MCU 能否从主程序入口进入循环, + * 并由系统节拍驱动 PB5 状态灯。 + */ + /* 当前最小入口诊断不初始化UART和ADC,避免外设状态干扰入口验证。 */ + /* BusinessProtocol_Init(); */ + /* MeshFrame_Init(); */ + /* 当前最小入口诊断不初始化故障管理和蓝牙状态机。 */ + /* + * 临时屏蔽 OTA 初始化,用于定位正式主程序是否卡在外部 W25Q80BV + * 的 SPI/JEDEC 初始化阶段。当前测试版本不访问外部 Flash,其他 + * 蓝牙、继电器、电流检测和故障处理初始化保持不变。 + * + * 确认 PB5 可以正常闪烁后,再恢复 OtaManager_Init(),并继续检查 + * W25Q80BV 的供电、CS(PD4)、SPI 引脚和 JEDEC ID 读取过程。 + */ + /* OtaManager_Init(); */ + /* 当前最小入口诊断暂不启动看门狗,避免复位现象干扰入口判断。 */ startup_broadcast_sent = 0u; while (1) { - Uart1_Task(); - Current_Task(); + /* 最小诊断版本暂不执行任何通信、采样和业务任务。 */ + /* UART接收由中断写入环形缓冲区,任务入口保持周期调用。 */ + /* ADC采用轮询采样,不使用新的中断向量。 */ + /* 当前先屏蔽 ADC 后台任务,确认它不会影响主循环和状态灯节拍。 */ + /* Current_Task(); */ + /* + * 临时屏蔽蓝牙初始化状态机和Mesh任务,用于确认正式程序是否能够 + * 正常进入主循环。BluetoothManager_Task() 内部会等待 UART 发送 + * 状态,如果 UART 初始化或引脚复用存在问题,可能在这里阻塞, + * 从而导致后面的 PB5 状态灯任务永远没有机会执行。 + */ + #if 0 if (BluetoothManager_IsReady() == 0u) { BluetoothManager_Task(); @@ -61,7 +116,7 @@ int main(void) MeshFrame_Task(); BluetoothManager_Task(); /* 所有基础模块已完成初始化且蓝牙进入可通信状态后,向Mesh - * 广播一次上线通知。业务帧SRC字段会自动携带本机短地址, + * 广播一次上线通知。JDY-28M外层会自动携带本机短地址, * 其他设备无需回复,避免广播上线时形成应答风暴。 */ if ((startup_broadcast_sent == 0u) && (BluetoothManager_IsReady() != 0u)) @@ -75,15 +130,22 @@ int main(void) } } } - FaultManager_Task(); - BusinessProtocol_Task(); + #endif + /* 周期执行电流持续时间和继电器状态一致性判断。 */ + /* 当前先屏蔽故障管理任务,避免在排查主循环时引入其他调用链。 */ + /* FaultManager_Task(); */ + /* BusinessProtocol_Task(); */ /* 蓝牙完成初始化后再上报Bootloader升级结果,避免结果帧丢在AT阶段。 */ - if (BluetoothManager_IsReady() != 0u) - { - OtaManager_Task(); - } - Board_StatusLedTask(OtaManager_IsActive()); - Watchdog_Refresh(); + /* 临时屏蔽 OTA 任务,与上面的初始化屏蔽保持一致。 */ + /* + * if (BluetoothManager_IsReady() != 0u) + * { + * OtaManager_Task(); + * } + */ + /* 使用TIM4毫秒节拍驱动状态灯,验证中断跳板和ISR返回链路。 */ + Board_StatusLedTask(0u); + /* 最小入口诊断不使用看门狗。 */ } } diff --git a/src/protocol/bluetooth_manager.c b/src/protocol/bluetooth_manager.c index 811f264..1b364f2 100644 --- a/src/protocol/bluetooth_manager.c +++ b/src/protocol/bluetooth_manager.c @@ -7,7 +7,7 @@ * 2. 若不是 ROLE2,发送 ROLE2 和 RESET; * 3. 再次确认 ROLE; * 4. 查询 MADDR,保存 2 字节短地址到 RAM; - * 5. 初始化完成后由 PC3 决定 PB5 蓝牙状态灯常亮或 1 秒闪烁。 + * 5. 初始化完成后由 PC3 决定 PB4 蓝牙状态灯常亮或 1 秒闪烁。 * * 初始化过程不使用阻塞延时。UART 返回内容只识别关键字符和超时,具体 * AT 返回格式仍应在首次联调时用串口工具记录并校准。 @@ -105,6 +105,15 @@ static void BluetoothManager_Enter(BluetoothState state) state_start_ms = SystemTick_NowMs(); } +/** 将一个ASCII十六进制字符转换为数值;非十六进制字符返回0xFF。 */ +static uint8_t BluetoothManager_HexDigit(uint8_t value) +{ + if ((value >= '0') && (value <= '9')) return (uint8_t)(value - '0'); + if ((value >= 'A') && (value <= 'F')) return (uint8_t)(value - 'A' + 10u); + if ((value >= 'a') && (value <= 'f')) return (uint8_t)(value - 'a' + 10u); + return 0xFFu; +} + /** 初始化蓝牙状态机和 RAM 中的短地址。 */ void BluetoothManager_Init(void) { @@ -135,10 +144,7 @@ static void BluetoothManager_ConsumeResponse(void) } if ((bluetooth_state == BT_WAIT_ADDRESS) && (response_address_digits < 4u)) { - uint8_t digit = 0xFFu; - if ((value >= '0') && (value <= '9')) digit = (uint8_t)(value - '0'); - else if ((value >= 'A') && (value <= 'F')) digit = (uint8_t)(value - 'A' + 10u); - else if ((value >= 'a') && (value <= 'f')) digit = (uint8_t)(value - 'a' + 10u); + uint8_t digit = BluetoothManager_HexDigit(value); if (digit != 0xFFu) { response_address_value = (uint16_t)((response_address_value << 4u) | digit); diff --git a/src/protocol/business_protocol.c b/src/protocol/business_protocol.c index 72045d0..ab0a360 100644 --- a/src/protocol/business_protocol.c +++ b/src/protocol/business_protocol.c @@ -11,7 +11,6 @@ #include "mesh_frame.h" #include "fault_manager.h" #include "bluetooth_manager.h" -#include "ota_manager.h" #include "system_tick.h" /** 广播应答的最小随机延时,避免多个设备同时发送。 */ @@ -69,12 +68,8 @@ void BusinessProtocol_QueueDelayedResponse(uint16_t address, /** 业务帧解析状态。 */ typedef enum { - /** 等待发送方短地址高字节。 */ - PARSER_WAIT_SOURCE_HIGH = 0, - /** 等待发送方短地址低字节。 */ - PARSER_WAIT_SOURCE_LOW, /** 等待命令字节。 */ - PARSER_WAIT_CMD, + PARSER_WAIT_CMD = 0, /** 等待 DATA 长度字节。 */ PARSER_WAIT_LENGTH, /** 按 LEN 接收 DATA 区。 */ @@ -89,7 +84,7 @@ typedef enum static BusinessParserState parser_state; /** 当前正在接收的命令。 */ static uint8_t parser_command; -/** 当前正在接收的发送方 Mesh 短地址。 */ +/** 当前外层接收帧携带的发送方 Mesh 短地址。 */ static uint16_t parser_source_address; /** 当前正在接收的数据长度。 */ static uint8_t parser_length; @@ -99,8 +94,8 @@ static uint8_t parser_data_index; static uint8_t parser_data[BUSINESS_MAX_DATA_LENGTH]; /** 当前正在接收的 CRC。 */ static uint16_t parser_received_crc; -/** CRC 计算临时区,依次保存 SRC、CMD、LEN 和 DATA。 */ -static uint8_t parser_crc_input[4u + BUSINESS_MAX_DATA_LENGTH]; +/** CRC 计算临时区,依次保存 CMD、LEN 和 DATA。 */ +static uint8_t parser_crc_input[2u + BUSINESS_MAX_DATA_LENGTH]; /** 最近一帧合法业务帧的内容。 */ static uint8_t last_command; /** 最近一帧合法业务帧的发送方短地址。 */ @@ -118,6 +113,12 @@ static void BusinessProtocol_SendByteResponse(uint16_t address, BusinessProtocol_SendResponse(address, command, &value, 1u); } +/** 发送统一格式的业务错误应答,集中减少各命令分支的重复封装代码。 */ +static void BusinessProtocol_SendError(uint16_t address, uint8_t error) +{ + BusinessProtocol_SendByteResponse(address, CMD_ERROR_RESPONSE, error); +} + /** * 生成一个8位线性反馈移位寄存器伪随机数。 * @@ -159,7 +160,7 @@ static void BusinessProtocol_SendResponse(uint16_t address, /** 初始化业务协议状态机;完整解析器在第二阶段实现。 */ void BusinessProtocol_Init(void) { - parser_state = PARSER_WAIT_SOURCE_HIGH; + parser_state = PARSER_WAIT_CMD; parser_command = 0u; parser_length = 0u; parser_data_index = 0u; @@ -170,6 +171,12 @@ void BusinessProtocol_Init(void) response_random_state = 0xA5u; } +/** 保存JDY-28M外层提供的发送方地址,供当前业务帧校验和应答使用。 */ +void BusinessProtocol_SetSourceAddress(uint16_t address) +{ + parser_source_address = address; +} + /** 处理 UART 数据和待发送帧;当前阶段仅保留任务入口。 */ void BusinessProtocol_Task(void) { @@ -187,8 +194,8 @@ void BusinessProtocol_Task(void) /** * 向业务帧状态机输入一个字节。 * - * 帧格式为 `SRC_H SRC_L CMD LEN DATA CRC16`。CRC 计算范围为 SRC、CMD、 - * LEN 和 DATA;线上 CRC 高字节在前。CRC 错误会恢复到等待地址状态, + * 帧格式为 `CMD LEN DATA CRC16`。CRC 计算范围为 CMD、LEN 和 DATA; + * 线上 CRC 高字节在前。CRC 错误会恢复到等待命令状态, * 不执行任何控制动作。 */ uint8_t BusinessProtocol_InputByte(uint8_t value) @@ -198,16 +205,6 @@ uint8_t BusinessProtocol_InputByte(uint8_t value) switch (parser_state) { - case PARSER_WAIT_SOURCE_HIGH: - parser_source_address = (uint16_t)value << 8u; - parser_state = PARSER_WAIT_SOURCE_LOW; - break; - - case PARSER_WAIT_SOURCE_LOW: - parser_source_address |= value; - parser_state = PARSER_WAIT_CMD; - break; - case PARSER_WAIT_CMD: parser_command = value; parser_state = PARSER_WAIT_LENGTH; @@ -218,7 +215,7 @@ uint8_t BusinessProtocol_InputByte(uint8_t value) parser_data_index = 0u; if (parser_length > BUSINESS_MAX_DATA_LENGTH) { - parser_state = PARSER_WAIT_SOURCE_HIGH; + parser_state = PARSER_WAIT_CMD; } else if (parser_length == 0u) { @@ -245,17 +242,15 @@ uint8_t BusinessProtocol_InputByte(uint8_t value) case PARSER_WAIT_CRC_LOW: parser_received_crc |= value; - parser_crc_input[0] = (uint8_t)(parser_source_address >> 8u); - parser_crc_input[1] = (uint8_t)parser_source_address; - parser_crc_input[2] = parser_command; - parser_crc_input[3] = parser_length; + parser_crc_input[0] = parser_command; + parser_crc_input[1] = parser_length; for (index = 0u; index < parser_length; ++index) { - parser_crc_input[4u + index] = parser_data[index]; + parser_crc_input[2u + index] = parser_data[index]; } expected_crc = BusinessProtocol_Crc16Modbus(parser_crc_input, - (uint16_t)(4u + parser_length)); - parser_state = PARSER_WAIT_SOURCE_HIGH; + (uint16_t)(2u + parser_length)); + parser_state = PARSER_WAIT_CMD; if (expected_crc != parser_received_crc) { return 0u; @@ -271,31 +266,13 @@ uint8_t BusinessProtocol_InputByte(uint8_t value) return 1u; default: - parser_state = PARSER_WAIT_SOURCE_HIGH; + parser_state = PARSER_WAIT_CMD; break; } return 0u; } -/** 读取最近一帧合法业务帧的命令字。 */ -uint8_t BusinessProtocol_GetCommand(void) -{ - return last_command; -} - -/** 读取最近一帧合法业务帧的数据长度。 */ -uint8_t BusinessProtocol_GetDataLength(void) -{ - return last_length; -} - -/** 读取最近一帧合法业务帧携带的发送方 Mesh 短地址。 */ -uint16_t BusinessProtocol_GetSourceAddress(void) -{ - return last_source_address; -} - /** * 执行当前合法业务帧。 * @@ -316,8 +293,7 @@ void BusinessProtocol_HandleFrame(void) case CMD_RELAY_CONTROL: if ((last_length != 1u) || ((last_data[0] != 0u) && (last_data[0] != 1u))) { - value = 0x03u; - BusinessProtocol_SendByteResponse(source_address, CMD_ERROR_RESPONSE, value); + BusinessProtocol_SendError(source_address, 0x03u); break; } Board_SetRelay(last_data[0]); @@ -330,8 +306,7 @@ void BusinessProtocol_HandleFrame(void) case CMD_RELAY_QUERY: if (last_length != 0u) { - value = 0x02u; - BusinessProtocol_SendByteResponse(source_address, CMD_ERROR_RESPONSE, value); + BusinessProtocol_SendError(source_address, 0x02u); break; } BusinessProtocol_SendByteResponse(source_address, @@ -342,8 +317,7 @@ void BusinessProtocol_HandleFrame(void) case CMD_CURRENT_QUERY: if (last_length != 0u) { - value = 0x02u; - BusinessProtocol_SendByteResponse(source_address, CMD_ERROR_RESPONSE, value); + BusinessProtocol_SendError(source_address, 0x02u); break; } current = Current_GetMilliAmpere(); @@ -357,9 +331,7 @@ void BusinessProtocol_HandleFrame(void) case CMD_FIRMWARE_VERSION_QUERY: if (last_length != 0u) { - value = 0x02u; - BusinessProtocol_SendByteResponse(source_address, - CMD_ERROR_RESPONSE, value); + BusinessProtocol_SendError(source_address, 0x02u); break; } current_data[0] = (uint8_t)(FIRMWARE_VERSION >> 8u); @@ -376,6 +348,8 @@ void BusinessProtocol_HandleFrame(void) } break; +#if 0 + /* OTA命令暂时屏蔽。保留完整实现,待主程序空间和硬件验证完成后恢复。 */ case CMD_OTA_RESULT_ACK: if (last_length == 1u) { @@ -415,25 +389,23 @@ void BusinessProtocol_HandleFrame(void) * 返回“成功”而实际校验尚未完成。 */ break; } +#endif case CMD_THRESHOLD_CONFIG: if (last_length != 2u) { - value = 0x02u; - BusinessProtocol_SendByteResponse(source_address, CMD_ERROR_RESPONSE, value); + BusinessProtocol_SendError(source_address, 0x02u); break; } threshold = ((uint16_t)last_data[0] << 8u) | last_data[1]; if ((threshold == 0u) || (threshold > 30000u)) { - value = 0x03u; - BusinessProtocol_SendByteResponse(source_address, CMD_ERROR_RESPONSE, value); + BusinessProtocol_SendError(source_address, 0x03u); break; } if (FaultManager_SetThreshold(threshold) == 0u) { - value = 0x05u; - BusinessProtocol_SendByteResponse(source_address, CMD_ERROR_RESPONSE, value); + BusinessProtocol_SendError(source_address, 0x05u); break; } BusinessProtocol_SendResponse(source_address, @@ -442,14 +414,12 @@ void BusinessProtocol_HandleFrame(void) break; case CMD_DEVICE_ONLINE: - /* 上线通知是设备间广播事件。发送方短地址已经位于业务帧SRC - * 字段中,因此不需要DATA,也不返回应答,避免多个设备同时 + /* 上线通知是设备间广播事件。发送方短地址已经由JDY-28M外层 + * 提供,因此不需要DATA,也不返回应答,避免多个设备同时 * 上线时产生广播应答风暴。 */ if (last_length != 0u) { - value = 0x02u; - BusinessProtocol_SendByteResponse(source_address, - CMD_ERROR_RESPONSE, value); + BusinessProtocol_SendError(source_address, 0x02u); } break; @@ -458,9 +428,7 @@ void BusinessProtocol_HandleFrame(void) * 在新版本高于当前版本时回复,避免全网无意义应答。 */ if (last_length != 4u) { - value = 0x02u; - BusinessProtocol_SendByteResponse(source_address, - CMD_ERROR_RESPONSE, value); + BusinessProtocol_SendError(source_address, 0x02u); break; } /* 这里直接复用最近一帧的DATA缓冲区生成应答,避免额外申请 @@ -488,16 +456,18 @@ void BusinessProtocol_HandleFrame(void) BusinessProtocol_SendByteResponse(source_address, CMD_ERROR_RESPONSE, value); break; } - current_data[0] = (uint8_t)(BluetoothManager_GetShortAddress() >> 8u); - current_data[1] = (uint8_t)BluetoothManager_GetShortAddress(); + /* 只读取一次短地址:除去重复函数调用,也避免地址在两次读取之间 + * 被异步状态更新时出现高、低字节不属于同一地址的情况。 */ + threshold = BluetoothManager_GetShortAddress(); + current_data[0] = (uint8_t)(threshold >> 8u); + current_data[1] = (uint8_t)threshold; BusinessProtocol_SendResponse(source_address, CMD_ADDRESS_QUERY | CMD_RESPONSE_FLAG, current_data, 2u); break; default: - value = 0x06u; - BusinessProtocol_SendByteResponse(source_address, CMD_ERROR_RESPONSE, value); + BusinessProtocol_SendError(source_address, 0x06u); break; } } diff --git a/src/protocol/mesh_frame.c b/src/protocol/mesh_frame.c index e5daffc..0f4081d 100644 --- a/src/protocol/mesh_frame.c +++ b/src/protocol/mesh_frame.c @@ -3,8 +3,8 @@ * @brief JDY-28M Mesh 外层帧实现。 * * 发送格式(所有发送方统一):41 54 2B AA A1 | 目标地址高低字节 | A0 | 业务帧 | 0D 0A。 - * 接收格式(接收方 UART 侧):F1 DD 09 00 0A | 地址高低字节 | 业务帧 | 0D 0A。 - * 发送方地址由业务帧 SRC_H/SRC_L 携带;JDY-28M 外层接收地址不是发送方地址。 + * 接收格式(接收方 UART 侧):F1 DD 09 | 发送方地址 | 接收方地址 | 业务帧 | 0A。 + * 业务帧不再重复携带地址,发送方和接收方地址均由JDY-28M外层提供。 */ #include #include "business_protocol.h" @@ -15,16 +15,15 @@ #define RX_PREFIX_0 0xF1u #define RX_PREFIX_1 0xDDu #define RX_PREFIX_2 0x09u -#define RX_PREFIX_3 0x00u -#define RX_PREFIX_4 0x0Au #define TX_PREFIX_0 0x41u #define TX_PREFIX_1 0x54u #define TX_PREFIX_2 0x2Bu #define TX_PREFIX_3 0xAAu #define TX_PREFIX_4 0xA1u #define MESH_DATA_FLAG 0xA0u -#define MESH_TAIL_0 0x0Du -#define MESH_TAIL_1 0x0Au +#define MESH_RX_TAIL 0x0Au +#define MESH_TX_TAIL_0 0x0Du +#define MESH_TX_TAIL_1 0x0Au /** 外层解析状态。 */ typedef enum @@ -35,26 +34,26 @@ typedef enum MESH_WAIT_PREFIX_1, /** 等待固定头第三字节 09。 */ MESH_WAIT_PREFIX_2, - /** 等待固定头第四字节 00。 */ - MESH_WAIT_PREFIX_3, - /** 等待固定头第五字节 0A。 */ - MESH_WAIT_PREFIX_4, - /** 接收外层目标地址高字节。 */ + /** 接收外层发送方地址高字节。 */ + MESH_READ_SOURCE_HIGH, + /** 接收外层发送方地址低字节。 */ + MESH_READ_SOURCE_LOW, + /** 接收外层接收方地址高字节。 */ MESH_READ_TARGET_HIGH, - /** 接收外层目标地址低字节。 */ + /** 接收外层接收方地址低字节。 */ MESH_READ_TARGET_LOW, /** 接收内层业务帧。 */ MESH_READ_BUSINESS, - /** 等待外层帧尾 0D。 */ - MESH_WAIT_TAIL_0, - /** 等待外层帧尾 0A。 */ - MESH_WAIT_TAIL_1 + /** 等待外层单字节帧尾 0A。 */ + MESH_WAIT_TAIL } MeshParserState; /** JDY-28M 外层接收状态机当前状态。 */ static MeshParserState mesh_state; /** 最近一帧合法外层数据携带的目标短地址,仅用于诊断。 */ static uint16_t last_target_address; +/** 当前正在接收帧携带的发送方短地址。 */ +static uint16_t current_source_address; /** 当前正在接收的业务帧缓冲区。 */ static uint8_t business_buffer[BUSINESS_MAX_DATA_LENGTH + 6u]; /** 当前已经接收的业务帧字节数。 */ @@ -96,13 +95,15 @@ static void MeshFrame_InputByte(uint8_t value) mesh_state = (value == RX_PREFIX_1) ? MESH_WAIT_PREFIX_2 : MESH_WAIT_PREFIX_0; break; case MESH_WAIT_PREFIX_2: - mesh_state = (value == RX_PREFIX_2) ? MESH_WAIT_PREFIX_3 : MESH_WAIT_PREFIX_0; + mesh_state = (value == RX_PREFIX_2) ? MESH_READ_SOURCE_HIGH : MESH_WAIT_PREFIX_0; break; - case MESH_WAIT_PREFIX_3: - mesh_state = (value == RX_PREFIX_3) ? MESH_WAIT_PREFIX_4 : MESH_WAIT_PREFIX_0; + case MESH_READ_SOURCE_HIGH: + current_source_address = (uint16_t)value << 8u; + mesh_state = MESH_READ_SOURCE_LOW; break; - case MESH_WAIT_PREFIX_4: - mesh_state = (value == RX_PREFIX_4) ? MESH_READ_TARGET_HIGH : MESH_WAIT_PREFIX_0; + case MESH_READ_SOURCE_LOW: + current_source_address |= value; + mesh_state = MESH_READ_TARGET_HIGH; break; case MESH_READ_TARGET_HIGH: last_target_address = (uint16_t)value << 8u; @@ -122,7 +123,7 @@ static void MeshFrame_InputByte(uint8_t value) business_buffer[business_length++] = value; if (business_length >= 4u) { - expected_length = (uint8_t)(business_buffer[3] + 6u); + expected_length = (uint8_t)(business_buffer[1] + 4u); business_expected_length = expected_length; if ((business_buffer[3] > BUSINESS_MAX_DATA_LENGTH) || (business_length > expected_length)) @@ -132,19 +133,16 @@ static void MeshFrame_InputByte(uint8_t value) } if (business_length == expected_length) { - /* 先确认外层 0D 0A,再提交内层业务帧。 */ - mesh_state = MESH_WAIT_TAIL_0; + mesh_state = MESH_WAIT_TAIL; } } break; - case MESH_WAIT_TAIL_0: - mesh_state = (value == MESH_TAIL_0) ? MESH_WAIT_TAIL_1 : MESH_WAIT_PREFIX_0; - break; - case MESH_WAIT_TAIL_1: - if (value == MESH_TAIL_1) + case MESH_WAIT_TAIL: + if (value == MESH_RX_TAIL) { uint8_t index; uint8_t frame_ready = 0u; + BusinessProtocol_SetSourceAddress(current_source_address); for (index = 0u; index < business_expected_length; ++index) { if (BusinessProtocol_InputByte(business_buffer[index]) != 0u) @@ -187,8 +185,7 @@ uint8_t MeshFrame_SendBusiness(uint16_t target_address, const uint8_t *data, uint8_t length) { - uint8_t crc_input[4u + BUSINESS_MAX_DATA_LENGTH]; - uint16_t source_address; + uint8_t crc_input[2u + BUSINESS_MAX_DATA_LENGTH]; uint16_t crc; uint8_t index; @@ -197,13 +194,10 @@ uint8_t MeshFrame_SendBusiness(uint16_t target_address, { return 0u; } - source_address = BluetoothManager_GetShortAddress(); - crc_input[0] = (uint8_t)(source_address >> 8u); - crc_input[1] = (uint8_t)source_address; - crc_input[2] = command; - crc_input[3] = length; - for (index = 0u; index < length; ++index) crc_input[4u + index] = data[index]; - crc = BusinessProtocol_Crc16Modbus(crc_input, (uint16_t)(4u + length)); + crc_input[0] = command; + crc_input[1] = length; + for (index = 0u; index < length; ++index) crc_input[2u + index] = data[index]; + crc = BusinessProtocol_Crc16Modbus(crc_input, (uint16_t)(2u + length)); Uart1_WriteByte(TX_PREFIX_0); Uart1_WriteByte(TX_PREFIX_1); Uart1_WriteByte(TX_PREFIX_2); Uart1_WriteByte(TX_PREFIX_3); @@ -211,11 +205,9 @@ uint8_t MeshFrame_SendBusiness(uint16_t target_address, Uart1_WriteByte((uint8_t)(target_address >> 8u)); Uart1_WriteByte((uint8_t)target_address); Uart1_WriteByte(MESH_DATA_FLAG); - Uart1_WriteByte((uint8_t)(source_address >> 8u)); - Uart1_WriteByte((uint8_t)source_address); Uart1_WriteByte(command); Uart1_WriteByte(length); for (index = 0u; index < length; ++index) Uart1_WriteByte(data[index]); Uart1_WriteByte((uint8_t)(crc >> 8u)); Uart1_WriteByte((uint8_t)crc); - Uart1_WriteByte(MESH_TAIL_0); Uart1_WriteByte(MESH_TAIL_1); + Uart1_WriteByte(MESH_TX_TAIL_0); Uart1_WriteByte(MESH_TX_TAIL_1); return 1u; } diff --git a/src/protocol/ota_manager.c b/src/protocol/ota_manager.c index daf2587..2eb7bd4 100644 --- a/src/protocol/ota_manager.c +++ b/src/protocol/ota_manager.c @@ -58,6 +58,31 @@ static uint8_t ota_verify_response_pending; /** CMD=0x0B请求是否来自广播,用于决定最终0x8B是否随机延迟。 */ static uint8_t ota_verify_broadcast_request; +/** + * 发送后台OTA校验结果。 + * + * 校验读取失败和整包校验完成原来分别实现了一套广播/单播发送分支, + * 这里统一封装,确保两条路径使用完全相同的应答规则。 + */ +static void OtaManager_SendVerifyResult(uint8_t result) +{ + if (ota_verify_response_pending == 0u) + { + return; + } + if (ota_verify_broadcast_request != 0u) + { + BusinessProtocol_QueueDelayedResponse(ota_verify_response_target, + 0x8Bu, &result, 1u); + } + else + { + (void)MeshFrame_SendBusiness(ota_verify_response_target, + 0x8Bu, &result, 1u); + } + ota_verify_response_pending = 0u; +} + /** * @brief 初始化 OTA 状态机。 */ @@ -75,7 +100,12 @@ void OtaManager_Init(void) ota_verify_response_target = 0u; ota_verify_response_pending = 0u; ota_verify_broadcast_request = 0u; - W25Q64App_Init(); + if (W25Q64App_Init() == 0u) + { + /* 外部Flash故障只关闭OTA和升级结果读取,不能影响继电器、 + * 电流检测、蓝牙通信等主程序基础功能。 */ + return; + } if (W25Q64App_ReadByte(OTA_METADATA_ADDRESS + 0x10ul, &ota_bootloader_result) != 0u && ota_bootloader_result != 0u) @@ -115,20 +145,7 @@ void OtaManager_Task(void) * 不能只退出状态机,否则主机将一直等待0x8B应答。 */ result = 0x01u; ota_state = OTA_STATE_IDLE; - if (ota_verify_response_pending != 0u) - { - if (ota_verify_broadcast_request != 0u) - { - BusinessProtocol_QueueDelayedResponse(ota_verify_response_target, - 0x8Bu, &result, 1u); - } - else - { - (void)MeshFrame_SendBusiness(ota_verify_response_target, - 0x8Bu, &result, 1u); - } - ota_verify_response_pending = 0u; - } + OtaManager_SendVerifyResult(result); } else { @@ -175,28 +192,12 @@ void OtaManager_Task(void) } } ota_state = OTA_STATE_IDLE; - if (ota_verify_response_pending != 0u) + /* 只有整包CRC正确且OTA元数据已经成功写入时,才在0x8B + * 应答发送完成后复位。失败分支必须继续留在当前程序。 */ + OtaManager_SendVerifyResult(result); + if ((result == 0x00u) && (ota_verify_broadcast_request == 0u)) { - if (ota_verify_broadcast_request != 0u) - { - /* 广播校验请求仍使用随机延时应答,但广播场景不立即 - * 复位,避免延时队列中的0x8B尚未发出就被复位丢失。 */ - BusinessProtocol_QueueDelayedResponse(ota_verify_response_target, - 0x8Bu, &result, 1u); - } - else - { - (void)MeshFrame_SendBusiness(ota_verify_response_target, - 0x8Bu, &result, 1u); - } - ota_verify_response_pending = 0u; - - /* 只有整包CRC正确且OTA元数据已经成功写入时,才在0x8B - * 应答发送完成后复位。失败分支必须继续留在当前程序。 */ - if ((result == 0x00u) && (ota_verify_broadcast_request == 0u)) - { - Watchdog_ResetNow(); - } + Watchdog_ResetNow(); } } } @@ -322,12 +323,6 @@ uint8_t OtaManager_GetExpectedOffset(uint16_t *offset) *offset=ota_expected_offset; return 1u; } -uint8_t OtaManager_GetBootloaderResult(void) -{ - /* W25Q80BV读取接口接入后,在此处加载result字段并置pending。 */ - return (ota_bootloader_result_pending != 0u) ? ota_bootloader_result : 0u; -} - void OtaManager_AcknowledgeBootloaderResult(void) { if (ota_bootloader_result_pending != 0u) diff --git a/tools/merge_intel_hex.py b/tools/merge_intel_hex.py new file mode 100644 index 0000000..81f4d08 --- /dev/null +++ b/tools/merge_intel_hex.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python3 +"""合并Bootloader和主程序的Intel HEX文件。""" + +from __future__ import annotations + +import argparse +from pathlib import Path + +EOF_RECORD = ":00000001FF" + + +def read_records(path: Path) -> list[str]: + """读取HEX记录,并去掉输入文件末尾的EOF记录。""" + records = [] + for raw_line in path.read_text(encoding="ascii").splitlines(): + line = raw_line.strip().upper() + if not line: + continue + if not line.startswith(":"): + raise ValueError(f"{path}包含非法HEX记录:{line}") + if line != EOF_RECORD: + records.append(line) + return records + + +def record_range(record: str) -> tuple[int, int] | None: + """返回数据记录地址范围,用于检查两个镜像是否重叠。""" + count = int(record[1:3], 16) + record_type = int(record[7:9], 16) + if record_type != 0x00 or count == 0: + return None + address = int(record[3:7], 16) + return address, address + count + + +def merge(output: Path, inputs: list[Path]) -> None: + """合并绝对地址HEX文件,并检查数据区间重叠。""" + occupied: list[tuple[int, int, Path]] = [] + records: list[str] = [] + for path in inputs: + for record in read_records(path): + current = record_range(record) + if current is not None: + start, end = current + for old_start, old_end, old_path in occupied: + if start < old_end and old_start < end: + raise ValueError( + f"HEX地址重叠:{path} 0x{start:04X}~0x{end - 1:04X}," + f"与 {old_path} 冲突" + ) + occupied.append((start, end, path)) + records.append(record) + output.write_text("\n".join(records + [EOF_RECORD]) + "\n", encoding="ascii") + + +def main() -> None: + """解析参数并执行HEX合并。""" + parser = argparse.ArgumentParser(description="合并STM8 Bootloader和主程序HEX") + parser.add_argument("output", type=Path, help="输出HEX文件") + parser.add_argument("inputs", nargs="+", type=Path, help="输入HEX文件") + args = parser.parse_args() + merge(args.output, args.inputs) + + +if __name__ == "__main__": + main() diff --git a/tools/ota_serial_test.py b/tools/ota_serial_test.py index 783c23f..d7d3e7b 100644 --- a/tools/ota_serial_test.py +++ b/tools/ota_serial_test.py @@ -10,7 +10,7 @@ 当前协议约定: - JDY-28M发送外层:41 54 2B AA A1 | DST_H DST_L | A0 | BUSINESS | 0D 0A - - 业务帧:SRC_H SRC_L | CMD | LEN | DATA | CRC16_H CRC16_L + - 业务帧:CMD | LEN | DATA | CRC16_H CRC16_L - CRC算法:CRC-16/MODBUS - DATA中的多字节数值和CRC均为高字节在前 - CMD=0x09每帧最多携带2字节固件数据 @@ -46,10 +46,10 @@ except ImportError as exc: # JDY-28M Mesh发送外层固定字段。 MESH_TX_PREFIX = bytes.fromhex("41 54 2B AA A1") MESH_DATA_FLAG = 0xA0 -MESH_TAIL = bytes.fromhex("0D 0A") +MESH_TAIL = bytes.fromhex("0A") # JDY-28M接收数据时,串口侧看到的固定前缀。 -MESH_RX_PREFIX = bytes.fromhex("F1 DD 09 00 0A") +MESH_RX_PREFIX = bytes.fromhex("F1 DD 09") # OTA命令定义。 CMD_OTA_START = 0x08 @@ -115,7 +115,8 @@ def make_business_frame(source: int, command: int, data: bytes) -> bytes: if len(data) > 4: raise ValueError("当前JDY-28M外层20字节限制下,业务DATA不能超过4字节") - body = struct.pack(">HBB", source, command, len(data)) + data + # JDY-28M外层已经携带发送方和接收方地址,业务帧不再重复嵌入source。 + body = struct.pack(">BB", command, len(data)) + data crc = crc16_modbus(body) return body + struct.pack(">H", crc) @@ -145,27 +146,28 @@ def parse_received_business(frame: bytes) -> tuple[int, int, bytes] | None: """从JDY-28M接收外层帧中提取业务命令并校验CRC。 接收格式为: - F1 DD 09 00 0A | 外层地址2字节 | BUSINESS | 0D 0A + F1 DD 09 | SRC地址2字节 | DST地址2字节 | BUSINESS | 0A - 外层地址字段由模块提供;真正的发送方地址仍取业务帧SRC字段。 + 发送方和接收方地址由JDY-28M外层提供,业务帧不再重复携带地址。 """ - if len(frame) < len(MESH_RX_PREFIX) + 2 + 6 + 2: + if len(frame) < len(MESH_RX_PREFIX) + 4 + 4 + 1: return None if not frame.startswith(MESH_RX_PREFIX) or not frame.endswith(MESH_TAIL): return None - business = frame[len(MESH_RX_PREFIX) + 2:-2] - if len(business) < 6: + source = struct.unpack(">H", frame[3:5])[0] + business = frame[len(MESH_RX_PREFIX) + 4:-1] + if len(business) < 4: return None - source, command, length = struct.unpack(">HBB", business[:4]) - if len(business) != 4 + length + 2 or length > 4: + command, length = struct.unpack(">BB", business[:2]) + if len(business) != 2 + length + 2 or length > 4: return None - body = business[:4 + length] - received_crc = struct.unpack(">H", business[4 + length:])[0] + body = business[:2 + length] + received_crc = struct.unpack(">H", business[2 + length:])[0] if crc16_modbus(body) != received_crc: return None - return source, command, business[4:4 + length] + return source, command, business[2:2 + length] def receive_response(ser: serial.Serial, timeout: float) -> tuple[int, int, bytes]: @@ -192,11 +194,11 @@ def receive_response(ser: serial.Serial, timeout: float) -> tuple[int, int, byte break if start: del buffer[:start] - if len(buffer) < len(MESH_RX_PREFIX) + 2 + 4 + 2 + 2: + if len(buffer) < len(MESH_RX_PREFIX) + 4 + 4 + 1: break - data_length = buffer[len(MESH_RX_PREFIX) + 2 + 3] - total = len(MESH_RX_PREFIX) + 2 + 4 + data_length + 2 + 2 + data_length = buffer[len(MESH_RX_PREFIX) + 4 + 1] + total = len(MESH_RX_PREFIX) + 4 + 2 + data_length + 2 + 1 if data_length > 4: del buffer[0] continue @@ -232,7 +234,7 @@ def wait_upgrade_result(ser: serial.Serial, timeout: float) -> tuple[int, int]: 0x8B只表示外部Flash固件校验成功。设备随后进入Bootloader搬运固件, 重启主程序并在蓝牙初始化完成后广播0x0D。因此这里除了结果码,还要 - 保留业务帧中的SRC地址,以便后续发送0x8D确认。 + 保留JDY-28M外层提供的SRC地址,以便后续发送0x8D确认。 @return (设备短地址, 升级结果码)。 @raises TimeoutError 在指定时间内未收到合法0x0D。