diff --git a/CODEX_FIRMWARE_HANDOFF.md b/CODEX_FIRMWARE_HANDOFF.md index 4f65d4e..2a5b75d 100644 --- a/CODEX_FIRMWARE_HANDOFF.md +++ b/CODEX_FIRMWARE_HANDOFF.md @@ -25,7 +25,8 @@ | 继电器控制 | `PA1_relay` | `PA1=1` 吸合,`PA1=0` 断开 | | ACS712 电流 ADC | `PC4` | 模拟输入,计算实际电流,不使用 ADC 原始值直接上报 | | JDY-28M Mesh 状态 | `PC3_JDY-28M-STAT` | 高电平表示已入网,低电平表示未入网 | -| LED3 | `PD3` | 高电平点亮 | +| 蓝牙状态/故障灯 | `PB5` | 低电平点亮 | +| 继电器状态灯 | `PD3` | 高电平点亮,吸合常亮、断开熄灭 | | JDY-28M UART TX | `PD5_JDY-28M-RXD` | MCU TX 连接模块 RXD | | JDY-28M UART RX | `PD6_JDY-28M-TXD` | MCU RX 连接模块 TXD | | 下载调试 | `SWIM/NRST` | 用于 SWIM 下载和调试 | @@ -57,11 +58,11 @@ JDY-28M 运行模式必须是 `AT+ROLE2`,即 MESH 指令通信模式。 - `AT+ROLE2`、`AT+MADDR` 必须有超时和有限次数重试; - 初始化失败时继电器保持断开; - 初始化失败不能通过 Mesh 上报,因为 Mesh 通信本身不可用; -- 初始化失败由 LED3 以亮灭各 300 ms 指示; +- 初始化失败由 PB5 蓝牙状态/故障灯以亮灭各 300 ms 指示; - Mesh 未组网不是初始化故障,不因为 PC3 为低电平而强制断开继电器; - PC3 必须连续稳定确认至少 3 秒后,才认为组网状态发生变化。 -## 5. LED3 指示规则 +## 5. 蓝牙状态/故障灯指示规则 状态优先级: @@ -69,7 +70,7 @@ JDY-28M 运行模式必须是 `AT+ROLE2`,即 MESH 指令通信模式。 蓝牙初始化故障 > 未组网 > 已组网 ``` -| 状态 | LED3 | +| 状态 | PB5蓝牙状态/故障灯 | |---|---| | 蓝牙初始化故障 | 亮 300 ms、灭 300 ms | | 初始化完成但未组网 | 亮 1 s、灭 1 s | @@ -271,9 +272,9 @@ current_mA = adc_rms * 6600UL * 2UL / (1023UL * 66UL); - 未收到确认时,每隔 10 秒重发相同的故障或恢复上报帧; - 收到匹配的 `0x84` 确认后,停止该事件的周期重发; - 重复收到同一故障上报时,不重复改变继电器状态,只返回 `0x84`; -- 蓝牙初始化故障不能通过 Mesh 上报,只通过 LED3 指示。 +- 蓝牙初始化故障不能通过 Mesh 上报,只通过 PB5 指示。 -蓝牙初始化异常使用 LED3 指示,不通过 Mesh 上报。 +蓝牙初始化异常使用 PB5 指示,不通过 Mesh 上报。 ## 11. 错误处理 @@ -322,7 +323,7 @@ services/ ### 阶段三:蓝牙初始化 -完成 `AT+ROLE`、`AT+ROLE2`、`AT+RESET`、`AT+MADDR` 状态机以及 LED3 状态指示。 +完成 `AT+ROLE`、`AT+ROLE2`、`AT+RESET`、`AT+MADDR` 状态机以及 PB5 状态指示。 ### 阶段四:继电器业务 diff --git a/Makefile b/Makefile index 8a19154..02da4c1 100644 --- a/Makefile +++ b/Makefile @@ -8,23 +8,33 @@ INC_DIR := include CC := sdcc OBJCOPY := packihx +MAKEBIN := makebin CFLAGS := -mstm8 --std-c99 --opt-code-size -I$(INC_DIR) -LDFLAGS := -mstm8 --out-fmt-ihx +# Bootloader占用0x8000~0x87FF,主程序必须从0x8800开始链接。 +LDFLAGS := -mstm8 --out-fmt-ihx --code-loc 0x8800 SOURCES := $(shell find $(SRC_DIR) -name '*.c' -print) 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 -all: $(HEX) +all: $(HEX) $(BIN) $(HEX): $(IHX) @mkdir -p $(dir $@) $(OBJCOPY) $< > $@ +# 生成OTA使用的裸二进制:跳过0x0000~0x87FF,只保留0x8800之后的应用程序数据。 +# -s 0x10000确保按STM8完整地址空间解析IHX;-o 0x8800去除Bootloader区域; +# -p再截断到应用程序最后一个实际占用地址,避免生成无意义的尾部空洞。 +$(BIN): $(IHX) + @mkdir -p $(dir $@) + $(MAKEBIN) -p -s 0x10000 -o 0x8800 $< $@ + $(IHX): $(OBJECTS) @mkdir -p $(dir $@) $(CC) $(LDFLAGS) -o $@ $(OBJECTS) @@ -34,7 +44,7 @@ $(BUILD_DIR)/%.rel: $(SRC_DIR)/%.c $(CC) $(CFLAGS) -c $< -o $@ size: $(IHX) - @ls -lh $(IHX) $(HEX) + @ls -lh $(IHX) $(HEX) $(BIN) clean: rm -rf $(BUILD_DIR) diff --git a/PROGRAM_DESIGN.md b/PROGRAM_DESIGN.md index 20e003e..3bbbc8c 100644 --- a/PROGRAM_DESIGN.md +++ b/PROGRAM_DESIGN.md @@ -79,7 +79,7 @@ Mesh 初始化完成,允许继电器业务运行 - 如果连续重试仍无法确认模式或短地址,进入 `BT_INIT_FAULT`,继电器保持断开,并通过 LED/故障状态报告蓝牙初始化故障;Mesh 尚未组网本身不属于初始化故障,不强制断开继电器; - 短地址不写入 EEPROM,设备每次上电都从 JDY-28M 重新查询。 -JDY-28M 手册说明,MESH 指令模式相关配置需要注意模块的指令/透传状态和 PWRC 管脚条件。实际硬件上应确认 `PC5_JDY-28M-PWRC` 的上电电平;如果模块未进入指令窗口,STM8 应先按手册要求控制 PWRC,再发送 `AT+ROLE`。 +JDY-28M 手册说明,MESH 指令模式相关配置需要注意模块的指令/透传状态和 PWRC 管脚条件。当前硬件使用 `PA2_JDY-28M-PWRC`,RESET 使用 `PD2_JDY-28M-RESET`;如果模块未进入指令窗口,STM8 应先按手册要求控制 PWRC,再发送 `AT+ROLE`。 建议的初始化状态枚举如下: @@ -109,13 +109,13 @@ void Bluetooth_InitTask(void) 短地址只保存在运行期 RAM 中。上电时必须重新执行 `AT+MADDR` 查询并校验,查询失败不能使用旧地址替代。 -### 4.2 LED3 蓝牙状态指示 +### 4.2 PB5 蓝牙状态指示 -LED3 由 STM8 的 `PD3` 控制,`PC3_JDY-28M-STAT` 用于读取 JDY-28M 的 Mesh 状态。蓝牙初始化完成后,程序需要区分“模块初始化成功”和“已经完成组网”两个状态,不能仅凭 `AT+ROLE2` 成功就认为已经入网。 +蓝牙状态/故障指示灯由 STM8 的 `PB5` 控制,低电平点亮;`PD3` 改作继电器状态指示灯,高电平点亮。`PC3_JDY-28M-STAT` 用于读取 JDY-28M 的 Mesh 状态。蓝牙初始化完成后,程序需要区分“模块初始化成功”和“已经完成组网”两个状态,不能仅凭 `AT+ROLE2` 成功就认为已经入网。 -LED3 显示规则如下: +PB5蓝牙状态/故障灯显示规则如下: -| 条件 | LED3 状态 | 说明 | +| 条件 | PB5 状态 | 说明 | |---|---|---| | 蓝牙模块初始化故障 | 300 ms 亮、300 ms 灭 | 最高优先级,表示 ROLE/RESET/MADDR 初始化失败 | | 初始化成功但尚未完成 Mesh 组网 | 1 s 亮、1 s 灭 | 表示模块正常,但当前没有完成组网 | @@ -134,7 +134,7 @@ JDY-28M 手册第 7 页对 STAT(第 6 脚)给出了明确规定:在 MESH #define JDY_MESH_JOINED_LEVEL 1 /* - * 更新 LED3 显示状态。 + * 更新 PB5 蓝牙状态/故障灯显示状态。 * - 初始化故障时以 300 ms 周期闪烁; * - 初始化成功但 PC3 未确认组网时以 1 s 周期闪烁; * - PC3 连续稳定确认已组网时保持常亮。 @@ -146,7 +146,7 @@ void BluetoothLed_Update(uint8_t init_fault, uint8_t init_ready, } ``` -PC3 状态必须连续稳定确认至少 3 秒:连续 3 秒读取到高电平才切换为常亮;连续 3 秒读取到低电平才切换为 1 秒闪烁。模块的 ALED(第 17 脚)是 JDY-28M 自带的广播 LED 指示脚,不应与本项目由 STM8 PD3 控制的 LED3 混淆。 +PC3 状态必须连续稳定确认至少 3 秒:连续 3 秒读取到高电平才切换为常亮;连续 3 秒读取到低电平才切换为 1 秒闪烁。模块的 ALED(第 17 脚)是 JDY-28M 自带的广播 LED 指示脚,不应与本项目由 STM8 PB5 控制的蓝牙状态灯混淆;PD3只用于继电器状态指示。 ### 4.3 量产配置 @@ -305,7 +305,7 @@ CMD=0x05, LEN=2, DATA=[ADDR_L, ADDR_H] `0x04` 故障上报建议定义为: ```text -DATA[0]:故障码,0x00=继电器已吸合但没有电流输出,0x01=继电器已断开但仍存在异常电流,0x02=ACS712采样异常,0x04=电流故障恢复,0x06=采样异常恢复。蓝牙初始化异常不通过 Mesh 上报,只通过 LED3 指示。 +DATA[0]:故障码,0x00=继电器已吸合但没有电流输出,0x01=继电器已断开但仍存在异常电流,0x02=ACS712采样异常,0x04=电流故障恢复,0x06=采样异常恢复。蓝牙初始化异常不通过 Mesh 上报,只通过 PB5 指示。 ``` `CMD=0x03` 的电流数据是 STM8 根据 ACS712 采样值、零点偏置和校准系数计算出的实际电流,不是 ADC 原始值。数据类型为 16 位无符号整数,单位为 mA,低字节在前,并采用四舍五入: @@ -338,7 +338,7 @@ JDY-28M 先按 MESH 外层帧完成目标地址处理和来源地址携带,STM 1. JDY-28M 已确认 `AT+ROLE=2`; 2. 已成功查询并校验 Mesh 短地址; -3. 已读取并保存 Mesh 状态;组网状态由 LED3 单独指示; +3. 已读取并保存 Mesh 状态;组网状态由 PB5 蓝牙状态灯单独指示;PD3指示继电器状态; 4. STM8 已完成 ACS712 零点初始化; 5. 继电器输出已经处于安全的断开状态。 @@ -409,8 +409,8 @@ CMD=0x06, LEN=2, DATA=[THRESHOLD_L, THRESHOLD_H] - 吸合后持续检测至少 10 s,电流始终低于 `I_ON_THRESHOLD`,主动上报 `0x00`; - 断开后持续检测至少 10 s,电流仍持续高于 `I_OFF_THRESHOLD`,主动上报 `0x01`; -- ACS712 采样异常上报 `0x02`;蓝牙初始化异常无法通过 Mesh 上报,只通过 LED3 指示; -- 后续连续检测恢复正常至少 10 s 时,清除对应故障并主动上报恢复码 `0x04` 或 `0x06`;蓝牙初始化故障无法通过 Mesh 上报,恢复后仅结束 LED3 故障闪烁; +- ACS712 采样异常上报 `0x02`;蓝牙初始化异常无法通过 Mesh 上报,只通过 PB5 指示; +- 后续连续检测恢复正常至少 10 s 时,清除对应故障并主动上报恢复码 `0x04` 或 `0x06`;蓝牙初始化故障无法通过 Mesh 上报,恢复后仅结束 PB5 故障闪烁; - 故障或恢复上报发送后,等待主机返回 `CMD=0x84` 确认; - 未收到确认时,每隔 10 s 重发一次相同的 `CMD=0x04` 上报帧; - 收到 `CMD=0x84` 且 DATA 与当前待确认故障码一致后,停止该事件的周期重发; diff --git a/STM8_Bootloader/Makefile b/STM8_Bootloader/Makefile new file mode 100644 index 0000000..27e02bf --- /dev/null +++ b/STM8_Bootloader/Makefile @@ -0,0 +1,38 @@ +PROJECT := stm8_bootloader +BUILD_DIR := build +SRC_DIR := src +INC_DIR := include + +CC := sdcc +OBJCOPY := packihx + +# Bootloader 固定放在 STM8S003F3P6 的 Flash 起始地址 0x8000。 +CFLAGS := -mstm8 --std-c99 --opt-code-size -I$(INC_DIR) +LDFLAGS := -mstm8 --out-fmt-ihx --code-loc 0x8000 --data-loc 0x0000 + +SOURCES := $(shell find $(SRC_DIR) -name '*.c' -print) +OBJECTS := $(patsubst $(SRC_DIR)/%.c,$(BUILD_DIR)/%.rel,$(SOURCES)) +IHX := $(BUILD_DIR)/$(PROJECT).ihx +HEX := $(BUILD_DIR)/$(PROJECT).hex + +.PHONY: all clean size + +all: $(HEX) + +$(HEX): $(IHX) + @mkdir -p $(dir $@) + $(OBJCOPY) $< > $@ + +$(IHX): $(OBJECTS) + @mkdir -p $(dir $@) + $(CC) $(LDFLAGS) -o $@ $(OBJECTS) + +$(BUILD_DIR)/%.rel: $(SRC_DIR)/%.c + @mkdir -p $(dir $@) + $(CC) $(CFLAGS) -c $< -o $@ + +size: $(IHX) + @ls -lh $(IHX) $(HEX) + +clean: + rm -rf $(BUILD_DIR) diff --git a/STM8_Bootloader/README.md b/STM8_Bootloader/README.md new file mode 100644 index 0000000..7050b47 --- /dev/null +++ b/STM8_Bootloader/README.md @@ -0,0 +1,22 @@ +# STM8S003F3P6TR Bootloader + +这是独立于应用程序的 STM8S003F3P6TR Bootloader 工程。 + +当前地址规划: + +```text +0x8000 ~ 0x83FF Bootloader,1KB +0x8400 ~ 0x9FFF Application,7KB +``` + +当前版本完成的基础功能: + +- 复位后初始化最小硬件环境; +- 读取应用程序首字节,判断应用程序是否存在; +- 通过跳转入口启动应用程序; +- 看门狗保护; +- 仅为后续 W25Q64 升级标志读取、CRC16/MODBUS 校验和应用程序写入预留接口。 + +OTA 分包接收、Mesh 通信、W25Q64 下载写入属于主程序,不属于本工程。 + +注意:当前版本是 Bootloader 基础框架,尚未接入完整的 JDY-28M OTA 下载协议和 W25Q64 驱动。实物烧录前必须先确认应用程序已经按照 `0x8400` 重新链接。 diff --git a/STM8_Bootloader/include/bootloader.h b/STM8_Bootloader/include/bootloader.h new file mode 100644 index 0000000..c90bba1 --- /dev/null +++ b/STM8_Bootloader/include/bootloader.h @@ -0,0 +1,21 @@ +/** + * @file bootloader.h + * @brief Bootloader 顶层流程接口。 + */ +#ifndef STM8_BOOTLOADER_H +#define STM8_BOOTLOADER_H + +#include + +/** 初始化 Bootloader 所需的最小硬件资源。 */ +void Bootloader_Init(void); +/** 判断应用程序区域是否包含可执行程序。 */ +uint8_t Bootloader_IsApplicationValid(void); +/** 跳转到应用程序入口地址,函数成功后不返回。 */ +void Bootloader_JumpToApplication(void); +/** Bootloader 主任务,后续OTA接收和搬运流程在此扩展。 */ +void Bootloader_Task(void); +/** 检查W25Q64中的升级请求并完成固件搬运。 */ +uint8_t Bootloader_ProcessUpgrade(void); + +#endif diff --git a/STM8_Bootloader/include/bootloader_config.h b/STM8_Bootloader/include/bootloader_config.h new file mode 100644 index 0000000..2cd9dfd --- /dev/null +++ b/STM8_Bootloader/include/bootloader_config.h @@ -0,0 +1,57 @@ +/** + * @file bootloader_config.h + * @brief Bootloader 的固定地址和安全策略配置。 + */ +#ifndef STM8_BOOTLOADER_CONFIG_H +#define STM8_BOOTLOADER_CONFIG_H + +#include + +/** Bootloader 在 STM8 程序 Flash 中的起始地址。 */ +#define BOOTLOADER_START_ADDRESS 0x8000u +/** Bootloader 预留空间,当前规划为 1KB。 */ +#define BOOTLOADER_SIZE_BYTES 0x0800u +/** 应用程序固定入口地址,必须与应用工程链接地址一致。 */ +#define APPLICATION_START_ADDRESS 0x8800u +/** STM8S003F3P6 程序 Flash 的最后一个地址。 */ +#define APPLICATION_END_ADDRESS 0x9FFFu +/** 应用程序允许使用的最大空间。 */ +#define APPLICATION_MAX_SIZE_BYTES \ + (APPLICATION_END_ADDRESS - APPLICATION_START_ADDRESS + 1u) + +/** W25Q64 中保存出厂固件镜像的固定地址,容量64KiB。 */ +#define FACTORY_IMAGE_ADDRESS 0x000000ul +/** W25Q64 中保存出厂固件元数据的固定地址,独占4KiB扇区。 */ +#define FACTORY_METADATA_ADDRESS 0x010000ul +/** W25Q64 中保存OTA升级固件的固定地址,容量64KiB。 */ +#define OTA_IMAGE_ADDRESS 0x020000ul +/** W25Q64 中保存OTA元数据的固定地址,独占4KiB扇区。 */ +#define OTA_METADATA_ADDRESS 0x030000ul +/** 升级元数据魔数,用于避免误把普通数据当成升级请求。 */ +#define OTA_METADATA_MAGIC 0x4F544131ul +/** 出厂恢复镜像元数据魔数。 */ +#define FACTORY_METADATA_MAGIC 0x46414331ul + +/* 元数据字段相对于各自元数据扇区起始地址的固定偏移。 */ +#define METADATA_MAGIC_OFFSET 0x00u +#define METADATA_IMAGE_SIZE_OFFSET 0x04u +#define METADATA_VERSION_OFFSET 0x08u +#define METADATA_CRC16_OFFSET 0x0Au +#define METADATA_IMAGE_VALID_OFFSET 0x0Eu +#define METADATA_UPGRADE_REQUEST_OFFSET 0x0Fu +#define METADATA_UPGRADE_RESULT_OFFSET 0x10u +/** 镜像已经完整写入并通过校验时使用的有效标志。 */ +#define METADATA_IMAGE_VALID_VALUE 0xA5u + +/** 应用程序有效标记,避免把空白 Flash 当作可执行程序。 */ +#define APPLICATION_FIRST_BYTE_INVALID 0x00u +#define APPLICATION_FIRST_BYTE_ERASED 0xFFu + +/** Bootloader 看门狗刷新周期,必须小于硬件看门狗超时时间。 */ +#define BOOTLOADER_WATCHDOG_REFRESH_LOOP 20000u +/** 上电恢复按键判定时间,单位为毫秒。 */ +#define FACTORY_BUTTON_HOLD_TIME_MS 5000u +/** Bootloader忙等待1ms的循环次数,需结合实际系统时钟校准。 */ +#define BOOTLOADER_DELAY_1MS_LOOPS 1200u + +#endif diff --git a/STM8_Bootloader/include/stm8_bootloader_registers.h b/STM8_Bootloader/include/stm8_bootloader_registers.h new file mode 100644 index 0000000..f6237d4 --- /dev/null +++ b/STM8_Bootloader/include/stm8_bootloader_registers.h @@ -0,0 +1,72 @@ +/** + * @file stm8_bootloader_registers.h + * @brief Bootloader 使用的 STM8S003F3P6 最小寄存器定义。 + */ +#ifndef STM8_BOOTLOADER_REGISTERS_H +#define STM8_BOOTLOADER_REGISTERS_H + +#include + +#define REG8(address) (*((volatile uint8_t *)(address))) + +/* FLASH 外设寄存器地址,地址来自 STM8S003 数据手册。 */ +#define FLASH_CR1_ADDR 0x505Au +#define FLASH_CR2_ADDR 0x505Bu +#define FLASH_NCR2_ADDR 0x505Cu +#define FLASH_IAPSR_ADDR 0x505Fu +#define FLASH_PUKR_ADDR 0x5062u + +#define FLASH_CR1 REG8(FLASH_CR1_ADDR) +#define FLASH_CR2 REG8(FLASH_CR2_ADDR) +#define FLASH_NCR2 REG8(FLASH_NCR2_ADDR) +#define FLASH_IAPSR REG8(FLASH_IAPSR_ADDR) +#define FLASH_PUKR REG8(FLASH_PUKR_ADDR) + +/* Flash 控制位。 */ +#define FLASH_CR2_PRG 0x01u +#define FLASH_CR2_WPRG 0x10u +#define FLASH_IAPSR_HVOFF 0x08u + +/* IWDG 寄存器。 */ +#define IWDG_KR_ADDR 0x50E0u +#define IWDG_PR_ADDR 0x50E1u +#define IWDG_RLR_ADDR 0x50E2u +#define IWDG_KR REG8(IWDG_KR_ADDR) +#define IWDG_PR REG8(IWDG_PR_ADDR) +#define IWDG_RLR REG8(IWDG_RLR_ADDR) + +/* SPI1 外设寄存器。 */ +#define SPI1_CR1_ADDR 0x5200u +#define SPI1_CR2_ADDR 0x5201u +#define SPI1_ICR_ADDR 0x5202u +#define SPI1_SR_ADDR 0x5203u +#define SPI1_DR_ADDR 0x5204u +#define SPI1_CR1 REG8(SPI1_CR1_ADDR) +#define SPI1_CR2 REG8(SPI1_CR2_ADDR) +#define SPI1_SR REG8(SPI1_SR_ADDR) +#define SPI1_DR REG8(SPI1_DR_ADDR) + +#define GPIOA_ODR REG8(0x5000u) +#define GPIOA_IDR REG8(0x5001u) +#define GPIOA_DDR REG8(0x5002u) +#define GPIOA_CR1 REG8(0x5003u) +#define GPIOC_ODR REG8(0x500Au) +#define GPIOC_DDR REG8(0x500Cu) +#define GPIOC_CR1 REG8(0x500Du) + +#define SPI_CR1_SPE 0x40u +#define SPI_CR1_MSTR 0x04u +#define SPI_CR1_BR0 0x08u +#define SPI_CR1_BR1 0x10u +#define SPI_CR1_BR2 0x20u +#define SPI_CR2_SSI 0x01u +#define SPI_CR2_SSM 0x02u +#define SPI_SR_RXNE 0x01u +#define SPI_SR_TXE 0x02u +#define SPI_SR_BSY 0x80u + +#define FLASH_IAPSR_EOP 0x04u +#define FLASH_CR2_PRG 0x01u +#define FLASH_CR2_ERASE 0x20u + +#endif diff --git a/STM8_Bootloader/include/w25q64.h b/STM8_Bootloader/include/w25q64.h new file mode 100644 index 0000000..d6612fb --- /dev/null +++ b/STM8_Bootloader/include/w25q64.h @@ -0,0 +1,26 @@ +/** + * @file w25q64.h + * @brief W25Q64JV 硬件SPI访问接口。 + */ +#ifndef STM8_BOOTLOADER_W25Q64_H +#define STM8_BOOTLOADER_W25Q64_H + +#include + +/** 初始化STM8硬件SPI和W25Q64片选。 */ +void W25Q64_Init(void); +/** 读取W25Q64 JEDEC ID,便于启动时确认芯片存在。 */ +uint32_t W25Q64_ReadJedecId(void); +/** 从W25Q64连续读取数据。 */ +void W25Q64_Read(uint32_t address, uint8_t *buffer, uint16_t length); +/** 擦除包含指定地址的4KB扇区。 */ +uint8_t W25Q64_EraseSector(uint32_t address); +/** 向W25Q64页内写入数据,长度不能跨越256字节页边界。 */ +uint8_t W25Q64_PageProgram(uint32_t address, const uint8_t *data, + uint16_t length); +/** 将升级元数据中的request字节清零,表示升级已处理。 */ +uint8_t W25Q64_ClearUpgradeRequest(void); +/** 写入Bootloader升级结果码,供主程序启动后上报。 */ +uint8_t W25Q64_WriteUpgradeResult(uint8_t result); + +#endif diff --git a/STM8_Bootloader/src/bootloader.c b/STM8_Bootloader/src/bootloader.c new file mode 100644 index 0000000..e4ef6a2 --- /dev/null +++ b/STM8_Bootloader/src/bootloader.c @@ -0,0 +1,411 @@ +/** + * @file bootloader.c + * @brief STM8S003F3P6 Bootloader 基础实现。 + * + * Bootloader 不处理 JDY-28M Mesh 通信,也不负责接收OTA分包。OTA通信和 + * W25Q64下载写入由 Application 完成;本程序只在复位后检查升级请求, + * 将已经校验完成的固件从W25Q64搬运到STM8片内应用区,然后跳转应用程序。 + */ +#include "bootloader.h" +#include "bootloader_config.h" +#include "stm8_bootloader_registers.h" +#include "w25q64.h" +#include "bootloader_config.h" + +typedef struct +{ + uint32_t magic; + uint32_t image_size; + uint16_t version; + uint16_t image_crc16; + uint8_t image_valid; + uint8_t request; + uint8_t result; + uint8_t reserved; +} BootloaderMetadata; + +/** 出厂镜像元数据与普通升级元数据使用相同布局,便于复用校验流程。 */ +typedef BootloaderMetadata FactoryMetadata; + +#define OTA_RESULT_NONE 0x00u +#define OTA_RESULT_SUCCESS 0x01u +#define OTA_RESULT_CRC_ERROR 0x02u +#define OTA_RESULT_FLASH_WRITE_ERROR 0x03u +#define OTA_RESULT_VERIFY_ERROR 0x04u +#define OTA_RESULT_SIZE_ERROR 0x05u +#define OTA_RESULT_W25Q64_ERROR 0x06u +#define FACTORY_BUTTON_MASK 0x08u + +/** + * @brief 初始化独立看门狗。 + * + * Bootloader不能依赖应用程序的初始化结果,因此在自身入口中重新配置 + * 看门狗。这里采用与应用程序一致的约5秒量级策略,具体超时时间仍需结合 + * STM8时钟分频和实物测量确认。 + */ +static void Bootloader_WatchdogInit(void) +{ + IWDG_KR = 0xCCu; + IWDG_KR = 0x55u; + IWDG_PR = 0x06u; + IWDG_RLR = 0xFFu; + IWDG_KR = 0xAAu; +} + +/** + * @brief 刷新独立看门狗。 + */ +static void Bootloader_WatchdogRefresh(void) +{ + IWDG_KR = 0xAAu; +} + +/** + * @brief 读取程序 Flash 中的一个字节。 + * @param address 需要读取的 16 位 Flash 地址。 + * @return 该地址中的 Flash 数据。 + */ +static uint8_t Bootloader_ReadFlashByte(uint16_t address) +{ + return *((const uint8_t *)address); +} + +/** + * @brief 初始化上电恢复按键PA3。 + * + * 按键另一端接地,因此PA3采用内部上拉:高电平表示释放,低电平表示按下。 + */ +static void Bootloader_ButtonInit(void) +{ + GPIOA_DDR &= (uint8_t)~FACTORY_BUTTON_MASK; + GPIOA_CR1 |= FACTORY_BUTTON_MASK; +} + +/** + * @brief 产生约1ms忙等待。 + * + * 该延时只用于Bootloader上电按键判定,最终5秒精度需要在目标时钟下实测校准。 + */ +static void Bootloader_Delay1ms(void) +{ + volatile uint16_t loop; + for (loop = 0u; loop < BOOTLOADER_DELAY_1MS_LOOPS; loop++) + { + __asm__("nop"); + } +} + +/** + * @brief 判断上电时是否长按恢复按键。 + * @return 1表示PA3持续按下达到5秒,0表示未按下或5秒内释放。 + */ +static uint8_t Bootloader_IsFactoryButtonHeld(void) +{ + uint16_t elapsed; + + if ((GPIOA_IDR & FACTORY_BUTTON_MASK) != 0u) + { + return 0u; + } + + for (elapsed = 0u; elapsed < FACTORY_BUTTON_HOLD_TIME_MS; elapsed++) + { + Bootloader_WatchdogRefresh(); + if ((GPIOA_IDR & FACTORY_BUTTON_MASK) != 0u) + { + return 0u; + } + Bootloader_Delay1ms(); + } + return 1u; +} + +static uint16_t Bootloader_Crc16(const uint8_t *data, uint16_t length, + uint16_t crc) +{ + uint8_t bit; + while (length-- != 0u) + { + crc ^= data[0]; + data++; + for (bit = 0u; bit < 8u; bit++) + { + crc = (crc & 1u) ? (uint16_t)((crc >> 1) ^ 0xA001u) : + (uint16_t)(crc >> 1); + } + } + return crc; +} + +static uint8_t Bootloader_ApplicationWriteByte(uint16_t address, + uint8_t value) +{ + /* 程序Flash解锁序列必须在第一次擦写前完成。 */ + static uint8_t unlocked; + if (unlocked == 0u) + { + FLASH_PUKR = 0x56u; + FLASH_PUKR = 0xAEu; + unlocked = 1u; + } + FLASH_CR2 = FLASH_CR2_PRG; + FLASH_NCR2 = (uint8_t)~FLASH_CR2_PRG; + *((volatile uint8_t *)address) = value; + while ((FLASH_IAPSR & FLASH_IAPSR_EOP) == 0u) {} + return (*((volatile uint8_t *)address) == value) ? 1u : 0u; +} + +static uint8_t Bootloader_EraseApplication(void) +{ + uint16_t address; + uint16_t page; + static uint8_t unlocked; + + if (unlocked == 0u) + { + FLASH_PUKR = 0x56u; + FLASH_PUKR = 0xAEu; + unlocked = 1u; + } + + /* STM8S003程序Flash按64字节块擦除;擦除前先把整片应用区设为0xFF。 */ + for (page = 0u; page < APPLICATION_MAX_SIZE_BYTES; page += 64u) + { + address = (uint16_t)(APPLICATION_START_ADDRESS + page); + FLASH_CR2 = FLASH_CR2_ERASE; + FLASH_NCR2 = (uint8_t)~FLASH_CR2_ERASE; + *((volatile uint8_t *)address) = 0x00u; + while ((FLASH_IAPSR & FLASH_IAPSR_EOP) == 0u) {} + } + return 1u; +} + +static uint8_t Bootloader_ValidateImage(uint32_t image_address, + uint32_t image_size, + uint16_t expected_crc16) +{ + uint8_t buffer[16]; + uint32_t source = image_address; + uint16_t crc = 0xFFFFu; + + if (image_size == 0ul || image_size > APPLICATION_MAX_SIZE_BYTES) + { + return 0u; + } + + while (image_size != 0ul) + { + uint16_t chunk = (image_size > 16ul) ? 16u : (uint16_t)image_size; + W25Q64_Read(source, buffer, chunk); + crc = Bootloader_Crc16(buffer, chunk, crc); + source += chunk; + image_size -= chunk; + } + return (crc == expected_crc16) ? 1u : 0u; +} + +static uint8_t Bootloader_WriteImage(uint32_t image_address, + uint32_t image_size) +{ + uint8_t buffer[16]; + uint16_t index; + uint16_t destination = (uint16_t)APPLICATION_START_ADDRESS; + uint32_t source = image_address; + + if (Bootloader_EraseApplication() == 0u) + { + return 0u; + } + while (image_size != 0ul) + { + uint16_t chunk = (image_size > 16ul) ? 16u : (uint16_t)image_size; + W25Q64_Read(source, buffer, chunk); + for (index = 0u; index < chunk; index++) + { + if (Bootloader_ApplicationWriteByte(destination, buffer[index]) == 0u) + { + return 0u; + } + destination++; + } + source += chunk; + image_size -= chunk; + } + return 1u; +} + +static uint8_t Bootloader_CopyImage(uint32_t image_address, + uint32_t image_size, + uint16_t expected_crc16, + uint8_t write_result) +{ + /* 先完整校验,再擦除应用区,避免坏固件导致原程序被提前破坏。 */ + if (Bootloader_ValidateImage(image_address, image_size, + expected_crc16) == 0u) + { + if (write_result != 0u) + { + (void)W25Q64_WriteUpgradeResult(OTA_RESULT_CRC_ERROR); + } + return 0u; + } + if (Bootloader_WriteImage(image_address, image_size) == 0u) + { + if (write_result != 0u) + { + (void)W25Q64_WriteUpgradeResult(OTA_RESULT_FLASH_WRITE_ERROR); + } + return 0u; + } + if (write_result == 0u) + { + return 1u; + } + if (W25Q64_WriteUpgradeResult(OTA_RESULT_SUCCESS) == 0u) + { + return 0u; + } + return W25Q64_ClearUpgradeRequest(); +} + +/** + * @brief 初始化 Bootloader 所需的最小硬件资源。 + */ +void Bootloader_Init(void) +{ + Bootloader_WatchdogInit(); + Bootloader_ButtonInit(); + W25Q64_Init(); +} + +uint8_t Bootloader_ProcessUpgrade(void) +{ + BootloaderMetadata metadata; + W25Q64_Read(OTA_METADATA_ADDRESS, (uint8_t *)&metadata, + (uint16_t)sizeof(metadata)); + if (metadata.magic != OTA_METADATA_MAGIC || + metadata.image_valid != METADATA_IMAGE_VALID_VALUE || + metadata.request == 0u) + { + return 0u; + } + if (metadata.image_size == 0ul || + metadata.image_size > APPLICATION_MAX_SIZE_BYTES) + { + (void)W25Q64_WriteUpgradeResult(OTA_RESULT_SIZE_ERROR); + return 0u; + } + return Bootloader_CopyImage(OTA_IMAGE_ADDRESS, metadata.image_size, + metadata.image_crc16, 1u); +} + +/** + * @brief 从W25Q64固定出厂区域恢复应用程序。 + * @return 1表示恢复成功,0表示出厂镜像不存在或校验失败。 + */ +static uint8_t Bootloader_ProcessFactoryRecovery(void) +{ + FactoryMetadata metadata; + W25Q64_Read(FACTORY_METADATA_ADDRESS, (uint8_t *)&metadata, + (uint16_t)sizeof(metadata)); + if (metadata.magic != FACTORY_METADATA_MAGIC || + metadata.image_valid != METADATA_IMAGE_VALID_VALUE || + metadata.request == 0u || + metadata.image_size == 0ul || + metadata.image_size > APPLICATION_MAX_SIZE_BYTES) + { + return 0u; + } + return Bootloader_CopyImage(FACTORY_IMAGE_ADDRESS, metadata.image_size, + metadata.image_crc16, 0u); +} + +/** + * @brief 判断应用程序区域是否存在有效入口。 + * + * 这里只进行最小判断:应用区首字节不能是空白态 0xFF,也不能是明显无效 + * 的0x00。正式版本还必须增加应用程序长度、版本和CRC16校验,只有整体校验 + * 成功后才允许跳转。 + * + * @return 1表示首字节具备基本有效性,0表示应用区为空或明显无效。 + */ +uint8_t Bootloader_IsApplicationValid(void) +{ + uint8_t first_byte = Bootloader_ReadFlashByte( + (uint16_t)APPLICATION_START_ADDRESS); + + return (first_byte != APPLICATION_FIRST_BYTE_ERASED && + first_byte != APPLICATION_FIRST_BYTE_INVALID) ? 1u : 0u; +} + +/** + * @brief 跳转到应用程序入口地址。 + * + * 应用程序必须按照 APPLICATION_START_ADDRESS 重新链接。跳转前不再执行 + * 复杂外设关闭动作,避免增加Bootloader体积;应用程序进入后应重新初始化 + * GPIO、UART、SPI、定时器、ADC和看门狗。 + */ +void Bootloader_JumpToApplication(void) +{ + void (*application_entry)(void); + + application_entry = (void (*)(void))APPLICATION_START_ADDRESS; + application_entry(); +} + +/** + * @brief Bootloader 主任务。 + * + * 当前基础版本暂不接收OTA数据,后续应在这里加入: + * 1. 读取W25Q64升级标志; + * 2. 校验完整固件CRC16/MODBUS; + * 3. 擦除应用程序区并按页写入; + * 4. 回读校验并清除升级标志; + * 5. 没有升级任务时启动现有应用程序。 + */ +void Bootloader_Task(void) +{ + uint8_t upgrade_processed = 0u; + uint8_t upgrade_requested = 0u; + uint8_t upgrade_success = 0u; + uint8_t factory_button_held; + + factory_button_held = Bootloader_IsFactoryButtonHeld(); + if (factory_button_held != 0u) + { + /* 上电长按优先级最高:不处理普通OTA请求,直接恢复出厂镜像。 */ + (void)Bootloader_ProcessFactoryRecovery(); + if (Bootloader_IsApplicationValid() != 0u) + { + Bootloader_JumpToApplication(); + } + } + + while (1) + { + Bootloader_WatchdogRefresh(); + if (upgrade_processed == 0u) + { + upgrade_processed = 1u; + { + BootloaderMetadata metadata; + W25Q64_Read(OTA_METADATA_ADDRESS, (uint8_t *)&metadata, + (uint16_t)sizeof(metadata)); + upgrade_requested = (metadata.magic == OTA_METADATA_MAGIC && + metadata.image_valid == METADATA_IMAGE_VALID_VALUE && + metadata.request != 0u) ? 1u : 0u; + } + upgrade_success = Bootloader_ProcessUpgrade(); + if ((upgrade_requested != 0u && upgrade_success == 0u) || + (upgrade_requested == 0u && + Bootloader_IsApplicationValid() == 0u)) + { + (void)Bootloader_ProcessFactoryRecovery(); + } + if (Bootloader_IsApplicationValid() != 0u) + { + Bootloader_JumpToApplication(); + } + } + } +} diff --git a/STM8_Bootloader/src/main.c b/STM8_Bootloader/src/main.c new file mode 100644 index 0000000..5d36cf6 --- /dev/null +++ b/STM8_Bootloader/src/main.c @@ -0,0 +1,16 @@ +/** + * @file main.c + * @brief Bootloader 独立工程入口。 + */ +#include "bootloader.h" + +/** + * @brief Bootloader 入口函数。 + * @return 理论上不会返回。 + */ +int main(void) +{ + Bootloader_Init(); + Bootloader_Task(); + return 0; +} diff --git a/STM8_Bootloader/src/w25q64.c b/STM8_Bootloader/src/w25q64.c new file mode 100644 index 0000000..127f743 --- /dev/null +++ b/STM8_Bootloader/src/w25q64.c @@ -0,0 +1,160 @@ +/** + * @file w25q64.c + * @brief W25Q64JV 最小硬件SPI驱动。 + * + * 引脚固定为:PC5=SCK、PC6=MOSI、PC7=MISO、PA3=CS#。 + * 本驱动只使用读取路径,因为Bootloader只读取主程序已经下载并校验好的 + * 固件;擦除和写入W25Q64属于Application职责。 + */ +#include "w25q64.h" +#include "stm8_bootloader_registers.h" +#include "bootloader_config.h" + +#define FLASH_CS_MASK 0x08u +#define W25Q64_CMD_JEDEC_ID 0x9Fu +#define W25Q64_CMD_READ_DATA 0x03u +#define W25Q64_CMD_WRITE_ENABLE 0x06u +#define W25Q64_CMD_READ_STATUS1 0x05u +#define W25Q64_CMD_PAGE_PROGRAM 0x02u +#define W25Q64_CMD_SECTOR_ERASE 0x20u +#define W25Q64_STATUS_BUSY 0x01u + +static void W25Q64_Select(void) +{ + GPIOA_ODR &= (uint8_t)~FLASH_CS_MASK; +} + +static void W25Q64_Deselect(void) +{ + GPIOA_ODR |= FLASH_CS_MASK; +} + +static uint8_t W25Q64_Transfer(uint8_t value) +{ + while ((SPI1_SR & SPI_SR_TXE) == 0u) {} + SPI1_DR = value; + while ((SPI1_SR & SPI_SR_RXNE) == 0u) {} + return SPI1_DR; +} + +static void W25Q64_WriteEnable(void) +{ + W25Q64_Select(); + W25Q64_Transfer(W25Q64_CMD_WRITE_ENABLE); + W25Q64_Deselect(); +} + +static uint8_t W25Q64_ReadStatus1(void) +{ + uint8_t status; + W25Q64_Select(); + W25Q64_Transfer(W25Q64_CMD_READ_STATUS1); + status = W25Q64_Transfer(0xFFu); + W25Q64_Deselect(); + return status; +} + +static uint8_t W25Q64_WaitReady(void) +{ + uint32_t timeout = 200000ul; + while ((W25Q64_ReadStatus1() & W25Q64_STATUS_BUSY) != 0u) + { + if (timeout-- == 0ul) + { + return 0u; + } + } + return 1u; +} + +void W25Q64_Init(void) +{ + /* PC5、PC6配置为推挽输出;PC7配置为输入;PA3配置为CS#输出。 */ + GPIOC_DDR |= 0x60u; + GPIOC_CR1 |= 0x60u; + GPIOA_DDR |= FLASH_CS_MASK; + GPIOA_CR1 |= FLASH_CS_MASK; + W25Q64_Deselect(); + + /* SPI主机、MSB first、模式0、低速启动,避免上电阶段时序过快。 */ + 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); +} + +uint32_t W25Q64_ReadJedecId(void) +{ + uint32_t id; + W25Q64_Select(); + W25Q64_Transfer(W25Q64_CMD_JEDEC_ID); + id = (uint32_t)W25Q64_Transfer(0xFFu) << 16; + id |= (uint32_t)W25Q64_Transfer(0xFFu) << 8; + id |= (uint32_t)W25Q64_Transfer(0xFFu); + W25Q64_Deselect(); + return id; +} + +void W25Q64_Read(uint32_t address, uint8_t *buffer, uint16_t length) +{ + uint16_t index; + W25Q64_Select(); + W25Q64_Transfer(W25Q64_CMD_READ_DATA); + W25Q64_Transfer((uint8_t)(address >> 16)); + W25Q64_Transfer((uint8_t)(address >> 8)); + W25Q64_Transfer((uint8_t)address); + for (index = 0u; index < length; index++) + { + buffer[index] = W25Q64_Transfer(0xFFu); + } + W25Q64_Deselect(); +} + +uint8_t W25Q64_EraseSector(uint32_t address) +{ + W25Q64_WriteEnable(); + W25Q64_Select(); + W25Q64_Transfer(W25Q64_CMD_SECTOR_ERASE); + W25Q64_Transfer((uint8_t)(address >> 16)); + W25Q64_Transfer((uint8_t)(address >> 8)); + W25Q64_Transfer((uint8_t)address); + W25Q64_Deselect(); + return W25Q64_WaitReady(); +} + +uint8_t W25Q64_PageProgram(uint32_t address, const uint8_t *data, + uint16_t length) +{ + uint16_t index; + if (length == 0u || length > 256u || + (((uint16_t)address & 0x00FFu) + length) > 256u) + { + return 0u; + } + W25Q64_WriteEnable(); + W25Q64_Select(); + W25Q64_Transfer(W25Q64_CMD_PAGE_PROGRAM); + W25Q64_Transfer((uint8_t)(address >> 16)); + W25Q64_Transfer((uint8_t)(address >> 8)); + W25Q64_Transfer((uint8_t)address); + for (index = 0u; index < length; index++) + { + W25Q64_Transfer(data[index]); + } + W25Q64_Deselect(); + return W25Q64_WaitReady(); +} + +uint8_t W25Q64_ClearUpgradeRequest(void) +{ + uint8_t cleared = 0u; + return W25Q64_PageProgram(OTA_METADATA_ADDRESS + + METADATA_UPGRADE_REQUEST_OFFSET, + &cleared, 1u); +} + +uint8_t W25Q64_WriteUpgradeResult(uint8_t result) +{ + return W25Q64_PageProgram(OTA_METADATA_ADDRESS + + METADATA_UPGRADE_RESULT_OFFSET, + &result, 1u); +} diff --git a/docs/HOST_PROTOCOL_SPEC.md b/docs/HOST_PROTOCOL_SPEC.md new file mode 100644 index 0000000..50d0340 --- /dev/null +++ b/docs/HOST_PROTOCOL_SPEC.md @@ -0,0 +1,468 @@ +# STM8 Mesh 继电器主机通信协议 + +本文档供主机、网关和上位机开发人员使用。通信分为两层:JDY-28M Mesh外层和设备业务内层。 + +## 1. Mesh外层帧 + +### 1.1 主机发送给设备 + +```text +41 54 2B AA A1 | DST_H DST_L | A0 | BUSINESS | 0D 0A +``` + +字段说明: + +| 字段 | 长度 | 说明 | +|---|---:|---| +| `41 54 2B AA A1` | 5 | JDY-28M Mesh发送固定帧头 | +| `DST_H DST_L` | 2 | 目标短地址,高字节在前 | +| `A0` | 1 | Mesh数据通信标志,固定值 | +| `BUSINESS` | 变长 | 本文档第2节定义的业务帧 | +| `0D 0A` | 2 | Mesh帧尾,固定值 | + +广播地址: + +```text +FFFF +``` + +单播到短地址`000C`: + +```text +DST_H = 00 +DST_L = 0C +``` + +### 1.2 设备发送给主机 + +```text +F1 DD 09 00 0A | SRC_H SRC_L | BUSINESS | 0D 0A +``` + +这里的两字节是 JDY-28M 外层携带的目标地址字段;设备发送方短地址不由该 +字段提供,而是放在业务帧 `BUSINESS` 的 `SRC_H SRC_L` 字段中。 + +### 1.3 长度限制 + +JDY-28M完整Mesh帧最大20字节。当前业务帧最大DATA长度为4字节: + +```text +BUSINESS = SRC_H SRC_L CMD LEN DATA CRC16_L CRC16_H +``` + +业务帧长度为`6 + LEN`,完整外层发送帧长度为`16 + LEN`,因此`LEN`最大为4。 + +## 2. 业务帧格式 + +```text +SRC_H SRC_L | CMD | LEN | DATA[0..LEN-1] | CRC16_L CRC16_H +``` + +### 2.1 CRC16规则 + +```text +算法:CRC-16/MODBUS +多项式:0xA001 +初始值:0xFFFF +输入反射:是 +输出反射:是 +计算范围:SRC_H、SRC_L、CMD、LEN和DATA +发送顺序:CRC低字节在前,高字节在后 +``` + +CRC错误时设备直接丢弃整帧,不执行命令,也不返回错误应答。 + +## 3. 通用应答规则 + +普通请求的应答命令通常为: + +```text +应答CMD = 请求CMD | 0x80 +``` + +错误应答: + +```text +CMD = E0 +LEN = 01 +DATA[0] = 错误码 +``` + +当前常用错误码: + +```text +0x02:DATA长度错误 +0x03:DATA参数错误 +0x05:EEPROM或配置写入失败 +0x06:未知命令 +``` + +## 4. 主机下发的普通命令 + +### 4.1 `0x01` 继电器控制 + +主机发送: + +```text +CMD = 01 +LEN = 01 +DATA[0] = 00:继电器断开 +DATA[0] = 01:继电器吸合 +``` + +业务帧结构示例: + +```text +SRC_H SRC_L 01 01 01 CRC16_L CRC16_H +``` + +设备执行后返回: + +```text +CMD = 81 +LEN = 01 +DATA[0] = 当前继电器控制状态 +``` + +```text +00:断开 +01:吸合 +``` + +注意:当前没有继电器触点反馈,设备只能确认PA1控制输出已经设置,不能确认触点机械动作是否真实成功。电流异常由故障检测功能另行上报。 + +### 4.2 `0x02` 查询继电器状态 + +主机发送: + +```text +CMD = 02 +LEN = 00 +``` + +设备返回: + +```text +CMD = 82 +LEN = 01 +DATA[0] = 00:断开 +DATA[0] = 01:吸合 +``` + +### 4.3 `0x03` 查询实际电流 + +主机发送: + +```text +CMD = 03 +LEN = 00 +``` + +设备返回: + +```text +CMD = 83 +LEN = 02 +DATA[0] = 电流低字节 +DATA[1] = 电流高字节 +``` + +单位为mA,无符号整数,低字节在前。例如实际电流为`1000mA = 0x03E8`: + +```text +DATA = E8 03 +``` + +### 4.4 `0x05` 查询设备短地址 + +主机发送: + +```text +CMD = 05 +LEN = 00 +``` + +设备返回: + +```text +CMD = 85 +LEN = 02 +DATA[0] = 短地址高字节 +DATA[1] = 短地址低字节 +``` + +例如设备短地址为`000C`: + +```text +DATA = 00 0C +``` + +### 4.5 `0x06` 配置电流阈值 + +主机发送: + +```text +CMD = 06 +LEN = 02 +DATA[0] = 阈值低字节 +DATA[1] = 阈值高字节 +``` + +单位为mA,默认值为100mA,设备保存到内部EEPROM。例如设置100mA: + +```text +DATA = 64 00 +``` + +设备成功返回: + +```text +CMD = 86 +LEN = 02 +DATA = 原样返回设置的阈值 +``` + +## 5. 固件版本查询 + +### 5.1 `0x0E` 查询当前固件版本 + +主机发送: + +```text +CMD = 0E +LEN = 00 +``` + +设备返回: + +```text +CMD = 8E +LEN = 02 +DATA[0] = 版本低字节 +DATA[1] = 版本高字节 +``` + +当前版本在主程序`include/app_config.h`中定义: + +```c +#define FIRMWARE_VERSION 0x0001u +``` + +例如版本`0x0001`返回: + +```text +DATA = 01 00 +``` + +## 6. 故障与恢复上报 + +故障上报由设备主动发送: + +```text +CMD = 04 +LEN = 01 +DATA[0] = 故障码或恢复码 +``` + +故障码: + +```text +0x00:继电器吸合但持续无有效电流 +0x01:继电器断开但仍存在持续电流 +0x02:ACS712采样异常 +0x03:蓝牙初始化异常,仅LED指示,不通过Mesh上报 +0x04:电流故障恢复 +0x05:蓝牙故障恢复 +0x06:采样异常恢复 +0x07:继电器控制失败预留码 +``` + +主机确认故障或恢复上报: + +```text +CMD = 84 +LEN = 01 +DATA[0] = 被确认的故障码或恢复码 +``` + +故障产生和恢复都需要持续至少10秒确认。故障恢复上报未收到主机确认时,设备每10秒重复上报,直到收到确认。 + +## 7. OTA命令 + +主机发送的OTA固件是`mesh_relay.bin`裸二进制文件,不是HEX文本文件,也不包含Bootloader。 + +### 7.1 `0x08` 进入OTA + +```text +CMD = 08 +LEN = 04 +DATA[0] = 固件长度低字节 +DATA[1] = 固件长度高字节 +DATA[2] = 版本号低字节 +DATA[3] = 版本号高字节 +``` + +设备收到后擦除: + +```text +OTA固件区:0x020000~0x02FFFF +OTA元数据:0x030000~0x030FFF +``` + +成功应答: + +```text +CMD = 88 +LEN = 01 +DATA[0] = 00 +``` + +### 7.2 `0x09` 固件分包 + +正常数据包: + +```text +CMD = 09 +LEN = 04 +DATA[0] = OFFSET高字节 +DATA[1] = OFFSET低字节 +DATA[2] = 固件数据1 +DATA[3] = 固件数据2 +``` + +最后只有1字节固件数据时: + +```text +CMD = 09 +LEN = 03 +DATA[0] = OFFSET高字节 +DATA[1] = OFFSET低字节 +DATA[2] = 固件数据1 +``` + +设备只接受等于当前期待偏移的分包。成功后返回: + +```text +CMD = 89 +LEN = 03 +DATA[0] = 00:写入成功 +DATA[1] = 下一期待偏移高字节 +DATA[2] = 下一期待偏移低字节 +``` + +如果偏移错误、Flash写入失败或OTA状态错误,返回非0状态码。主机应根据返回的期待偏移重传。 + +### 7.3 `0x0A` 发送完整固件CRC16 + +```text +CMD = 0A +LEN = 02 +DATA[0] = 完整固件CRC16低字节 +DATA[1] = 完整固件CRC16高字节 +``` + +CRC16使用CRC-16/MODBUS,计算范围是整个裸二进制固件文件。 + +设备接收后返回: + +```text +CMD = 8A +LEN = 01 +DATA[0] = 00:接收成功 +``` + +### 7.4 `0x0B` 请求校验并提交升级 + +主机发送: + +```text +CMD = 0B +LEN = 00 +``` + +设备从W25Q64重新读取完整固件并计算CRC16。校验成功后写入OTA元数据: + +```text +0x030000:魔数 +0x030004:固件长度 +0x030008:固件版本 +0x03000A:CRC16 +0x03000E:镜像有效标志0xA5 +0x03000F:升级请求0xA5 +``` + +设备返回: + +```text +CMD = 8B +LEN = 01 +DATA[0] = 00:校验成功,已提交升级 +DATA[0] = 01:校验失败 +``` + +之后设备复位,由Bootloader把OTA固件搬运到内部Flash的`0x8800`区域。 + +### 7.5 `0x0C` 取消OTA + +主机发送: + +```text +CMD = 0C +LEN = 00 +``` + +设备返回: + +```text +CMD = 8C +LEN = 01 +DATA[0] = 00:取消成功 +``` + +### 7.6 OTA传输顺序 + +```text +查询版本:0E +进入OTA:08 +等待应答:88 +发送分包:09 +每包等待应答:89 +发送完整CRC16:0A +请求校验提交:0B +等待校验结果:8B +设备复位并升级 +主程序启动后主动上报升级结果:0D +主机确认升级结果:8D +``` + +## 8. OTA升级结果上报 + +设备启动后从`0x030010`读取升级结果,蓝牙初始化成功后主动广播: + +```text +CMD = 0D +LEN = 01 +DATA[0] = 升级结果码 +``` + +结果码: + +```text +0x00:无结果 +0x01:升级成功 +0x02:固件CRC错误 +0x03:内部Flash写入失败 +0x04:写入后校验失败 +0x05:固件长度非法 +0x06:W25Q64读取失败 +``` + +主机确认: + +```text +CMD = 8D +LEN = 01 +DATA[0] = 对应的升级结果码 +``` + +设备收到确认后清除`0x030010`。 diff --git a/docs/OTA_STORAGE_AND_PROTOCOL.md b/docs/OTA_STORAGE_AND_PROTOCOL.md new file mode 100644 index 0000000..6b63059 --- /dev/null +++ b/docs/OTA_STORAGE_AND_PROTOCOL.md @@ -0,0 +1,302 @@ +# STM8 Mesh 继电器 OTA、Flash 分区与业务协议设计 + +本文档记录当前已经确认的外部 W25Q64 存储规划、Bootloader 启动策略、主程序 OTA 职责以及业务命令规则。 + +## 1. 硬件资源与程序分区 + +### 1.1 STM8S003F3P6TR 内部 Flash + +STM8S003F3P6TR 内部程序 Flash 总容量为 8KiB,地址范围为 `0x8000~0x9FFF`。 + +当前 Bootloader 规划如下: + +| 地址范围 | 容量 | 用途 | +|---|---:|---| +| `0x8000~0x87FF` | 2KiB | Bootloader | +| `0x8800~0x9FFF` | 6KiB | Application 主程序 | + +应用程序必须按照 `0x8800` 重新链接,不能使用默认的 `0x8000` 起始地址。 + +### 1.2 W25Q64JVSIQ 外部 Flash + +W25Q64 总容量为 8MiB。当前固定分区如下: + +| 地址范围 | 容量 | 用途 | +|---|---:|---| +| `0x000000~0x00FFFF` | 64KiB | 出厂固件镜像 | +| `0x010000~0x010FFF` | 4KiB | 出厂固件元数据 | +| `0x020000~0x02FFFF` | 64KiB | OTA升级固件镜像 | +| `0x030000~0x030FFF` | 4KiB | OTA升级元数据 | +| 其余空间 | 约8056KiB | 后续日志、运行记录、故障记录等 | + +元数据区域均独占一个4KiB扇区,避免擦除元数据时影响固件镜像。 + +### 1.3 SPI连接 + +| STM8引脚 | W25Q64信号 | +|---|---| +| `PC5` | `SCK` | +| `PC6` | `MOSI/DI` | +| `PC7` | `MISO/DO` | +| `PD4` | `CS#` | +| `3.3V` | `VCC` | +| `GND` | `GND` | + +恢复/继电器按键连接到 `PA3`,按键另一端接地,使用内部上拉: + +```text +PA3 = 1:按键释放 +PA3 = 0:按键按下 +``` + +JDY-28M 控制脚当前分配为:`PA2=PWRC`、`PD2=RESET`。`PA2` 不再作为Flash片选。 + +蓝牙状态/故障指示灯连接到 `PB5`,低电平点亮;继电器状态指示灯连接到 `PD3`,高电平点亮。 + +程序运行/OTA状态灯连接到 `PB4`,低电平点亮:正常运行时按500ms节奏闪烁,OTA接收或校验时按约100ms节奏快速闪烁。 + +## 2. 元数据格式 + +出厂元数据和OTA元数据使用相同字段布局,只是存储区域不同。 + +### 2.1 元数据字段 + +相对于元数据区域起始地址的固定偏移如下: + +| 偏移 | 字段 | 长度 | 说明 | +|---:|---|---:|---| +| `0x00` | 魔数 | 4字节 | 识别元数据类型 | +| `0x04` | 固件长度 | 4字节 | 实际固件字节数 | +| `0x08` | 固件版本 | 2字节 | 版本号,高低字节规则由打包工具固定 | +| `0x0A` | CRC16 | 2字节 | 完整固件镜像CRC-16/MODBUS | +| `0x0E` | 镜像有效标志 | 1字节 | `0xA5`表示有效 | +| `0x0F` | 升级请求 | 1字节 | 非0表示请求Bootloader处理 | +| `0x10` | 升级结果 | 1字节 | Bootloader执行结果 | +| `0x11` | 保留 | 1字节 | 后续扩展 | + +### 2.2 元数据绝对地址 + +OTA元数据区域起始地址为 `0x030000`,因此: + +```text +魔数: 0x030000~0x030003 +固件长度: 0x030004~0x030007 +固件版本: 0x030008~0x030009 +CRC16: 0x03000A~0x03000B +有效标志: 0x03000E +升级请求: 0x03000F +升级结果: 0x030010 +``` + +出厂元数据区域起始地址为 `0x010000`,字段偏移相同,因此出厂镜像升级结果地址为 `0x010010`。 + +### 2.3 魔数 + +魔数是固定识别值,不是密码。当前约定: + +```c +FACTORY_METADATA_MAGIC = 0x46414331 /* FAC1,出厂镜像 */ +OTA_METADATA_MAGIC = 0x4F544131 /* OTA1,升级镜像 */ +``` + +Bootloader只有在魔数正确、有效标志正确、升级请求有效、固件长度合法且CRC16正确时,才允许使用镜像。 + +### 2.4 元数据写入规则 + +下载过程中不能提前写入有效标志。正确顺序为: + +```text +1. 擦除OTA固件区域; +2. 接收并写入固件分包; +3. 计算完整固件CRC16/MODBUS; +4. 写入魔数、长度、版本和CRC16; +5. 校验元数据; +6. 最后写入image_valid = 0xA5; +7. 设置upgrade_request; +8. 软件复位进入Bootloader。 +``` + +这样可以避免断电时把不完整固件误判为有效镜像。 + +## 3. Bootloader职责 + +Bootloader不处理JDY-28M Mesh通信,也不接收OTA分包。 + +Bootloader负责: + +- 上电检测PA3上电恢复按键; +- 读取OTA元数据; +- 校验OTA固件CRC16/MODBUS; +- 擦除STM8内部应用区; +- 从W25Q64搬运固件到内部Flash; +- 回读校验内部Flash; +- 写入升级结果; +- 清除升级请求; +- 检查内部应用程序是否有效; +- 应用损坏时恢复出厂固件; +- 跳转到 `0x8800` 启动主程序。 + +升级安全顺序必须是: + +```text +先完整校验W25Q64固件 + ↓ +CRC正确 + ↓ +擦除内部应用区 + ↓ +写入应用程序 + ↓ +回读校验 + ↓ +记录升级成功 + ↓ +清除升级请求 +``` + +### 3.1 出厂固件恢复 + +以下任一情况出现时,Bootloader使用 `0x000000` 开始的出厂镜像: + +- 内部应用程序为空; +- 内部应用程序入口无效; +- OTA固件CRC16错误; +- 内部Flash写入失败; +- 内部Flash写入后校验失败; +- 应用程序损坏导致无法正常启动。 + +出厂镜像必须预先写入W25Q64,并且出厂元数据中的镜像有效标志和CRC16必须正确。 + +## 4. PA3上电恢复按键 + +### 4.1 Bootloader阶段 + +上电后首先运行Bootloader: + +```text +检测PA3 +├── 未按下:执行正常Bootloader流程 +└── 已按下:开始计时,此期间不跳转主程序 + ├── 5秒内释放:继续正常Bootloader流程 + └── 持续按住超过5秒:恢复出厂固件 +``` + +主程序不会参与本次上电按键判断,因此不存在Bootloader长按和主程序恢复参数重复触发的问题。 + +### 4.2 主程序阶段 + +主程序只处理正常运行期间的新按键: + +```text +短按:按下时间小于5秒,控制继电器 +长按:按下时间大于5秒,恢复主程序参数 +``` + +## 5. 主程序OTA职责 + +主程序负责: + +- 通过JDY-28M接收OTA业务帧; +- 校验业务帧CRC16/MODBUS; +- 擦除OTA固件区域; +- 将固件分包写入 `0x020000`; +- 计算完整固件CRC16/MODBUS; +- 写入OTA元数据; +- 设置有效标志和升级请求; +- 软件复位进入Bootloader; +- 启动后读取Bootloader升级结果; +- 通过Mesh主动上报升级结果; +- 收到主机确认后清除升级结果。 + +## 6. 主程序业务帧格式 + +业务数据帧格式: + +```text +SRC_H | SRC_L | CMD | LEN | DATA | CRC16_L | CRC16_H +``` + +说明: + +- `SRC_H SRC_L`:发送方 Mesh 短地址,高字节在前;设备应答时将该地址作为 + JDY-28M 外层目标地址,业务帧中的 SRC 改为设备自身短地址; +- `CMD`:命令; +- `LEN`:DATA长度; +- `DATA`:命令数据; +- `CRC16`:CRC-16/MODBUS,计算范围为 SRC、CMD、LEN、DATA,低字节在前; +- CRC错误时静默丢弃,不执行继电器动作,不返回错误帧。 + +JDY-28M的Mesh外层帧负责目标地址、源地址、Mesh头和帧尾,业务帧本身不重复添加地址。 + +## 7. 已确定的业务命令 + +| CMD | 名称 | 方向 | DATA说明 | +|---:|---|---|---| +| `0x01` | 继电器控制 | 主机→设备 | `0x00`断开,`0x01`吸合 | +| `0x02` | 查询继电器状态 | 主机→设备 | 返回`0x00`断开或`0x01`吸合 | +| `0x03` | 查询实际电流 | 主机→设备 | 返回无符号mA,高字节在前 | +| `0x04` | 故障/恢复上报 | 设备→主机 | DATA为故障码或恢复码 | +| `0x05` | 查询设备短地址 | 主机→设备 | 返回2字节短地址 | +| `0x06` | 配置电流阈值 | 主机→设备 | 2字节,单位mA,低字节在前,保存到EEPROM | +| `0x0E` | 查询当前固件版本 | 主机→设备 | 无DATA | +| `0x0D` | OTA升级结果上报 | 设备→主机 | DATA为升级结果码 | + +应答命令使用请求命令加 `0x80`: + +```text +0x81:继电器控制应答 +0x82:继电器状态应答 +0x83:电流查询应答 +0x85:短地址查询应答 +0x86:阈值配置应答 +``` + +OTA内部控制命令建议保留: + +```text +0x08:进入OTA模式 +0x09:固件数据分包 +0x0A:固件发送完成并校验 +0x0B:查询OTA状态 +0x0C:取消OTA +0x0E:查询当前固件版本 +``` + +## 8. 故障码和升级结果码 + +### 8.1 故障码 + +```text +0x02:ACS712采样异常 +0x03:蓝牙初始化异常 +0x04:电流故障恢复 +0x05:蓝牙故障恢复 +0x06:采样异常恢复 +0x07:继电器控制失败预留码 +``` + +电流故障需要持续至少10秒确认,恢复也需要持续至少10秒确认。默认电流阈值为100mA,支持动态配置并保存到EEPROM。 + +故障恢复上报需要等待主机确认;未收到确认时每10秒重复上报,直到收到确认。 + +### 8.2 OTA升级结果码 + +```text +0x00:无结果 +0x01:升级成功 +0x02:固件CRC错误 +0x03:内部Flash写入失败 +0x04:写入后校验失败 +0x05:固件长度非法 +0x06:W25Q64读取失败 +``` + +主程序启动后读取升级结果,并使用: + +```text +CMD = 0x0D +LEN = 0x01 +DATA = 升级结果码 +``` + +发送给主机。主机确认后,主程序清除结果字段。 diff --git a/include/app_config.h b/include/app_config.h index f3542af..c1b372a 100644 --- a/include/app_config.h +++ b/include/app_config.h @@ -20,6 +20,12 @@ /** 释放 RESET 后等待模块启动完成的时间。 */ #define JDY28M_RESET_STARTUP_WAIT_MS 1500ul +/** 当前主程序固件版本,高字节为主版本、低字节为次版本。发布新版本时必须增加版本号。 + * 0x0001:版本0.1 + * 0x0100:版本1.0 + * 0x0102:版本1.2 */ +#define FIRMWARE_VERSION 0x0100u + /** 默认电流阈值,单位为 mA。 */ #define DEFAULT_CURRENT_THRESHOLD_MA 100u @@ -38,10 +44,4 @@ /** ACS712 采样建议使用的 RMS 窗口,单位为毫秒。 */ #define CURRENT_RMS_WINDOW_MS 100ul -/** 业务协议帧头第一个字节。 */ -#define BUSINESS_FRAME_HEAD_0 0x55u - -/** 业务协议帧头第二个字节。 */ -#define BUSINESS_FRAME_HEAD_1 0xAAu - #endif diff --git a/include/board.h b/include/board.h index dd4cd8b..a9a5815 100644 --- a/include/board.h +++ b/include/board.h @@ -3,7 +3,8 @@ * @brief 蓝牙 Mesh 继电器硬件板级接口定义。 * * 本阶段先提供稳定的硬件抽象接口,不在业务层直接操作 GPIO 寄存器。 - * PA1 的有效电平已经确认:高电平吸合、低电平断开;PD3 高电平点亮 LED3。 + * PA1 的有效电平已经确认:高电平吸合、低电平断开;PB5 为低电平点亮的 + * 蓝牙组网/故障指示灯,PD3 为高电平点亮的继电器状态灯。 */ #ifndef BOARD_H #define BOARD_H @@ -22,9 +23,15 @@ uint8_t Board_GetRelay(void); /** 读取 JDY-28M STAT 状态,1 表示模块报告已组网。 */ uint8_t Board_ReadMeshJoined(void); -/** 设置 LED3 点亮状态,1 表示点亮,0 表示熄灭。 */ +/** 设置蓝牙组网/故障指示灯,1表示点亮,0表示熄灭;硬件低电平点亮。 */ void Board_SetLed3(uint8_t on); +/** 设置继电器状态灯,1表示点亮,0表示熄灭;硬件高电平点亮。 */ +void Board_SetRelayLed(uint8_t on); + +/** 周期驱动PB4程序状态灯;正常运行500ms翻转,OTA期间100ms翻转。 */ +void Board_StatusLedTask(uint8_t ota_active); + /** 设置 PWRC:1 表示拉低进入 AT 窗口,0 表示恢复高电平通信。 */ void Board_SetBluetoothAtWindow(uint8_t enabled); diff --git a/include/business_protocol.h b/include/business_protocol.h index ebb3a0c..0a5ea34 100644 --- a/include/business_protocol.h +++ b/include/business_protocol.h @@ -2,8 +2,9 @@ * @file business_protocol.h * @brief STM8 业务帧协议接口。 * - * 业务帧格式:0x55 0xAA | CMD | LEN | DATA | CRC16。 - * CRC 使用 CRC-16/MODBUS,计算范围为 CMD 至 DATA,低字节在前。 + * 业务帧格式:SRC_H SRC_L | CMD | LEN | DATA | CRC16_L CRC16_H。 + * SRC 为发送方的 Mesh 短地址;CRC 使用 CRC-16/MODBUS,计算范围为 + * SRC、CMD、LEN 和 DATA,线上低字节在前。 */ #ifndef BUSINESS_PROTOCOL_H #define BUSINESS_PROTOCOL_H @@ -13,8 +14,8 @@ /** 业务帧最大 DATA 长度,需为 JDY-28M 外层 20 字节限制预留空间。 */ #define BUSINESS_MAX_DATA_LENGTH 4u -/** 业务帧头长度。 */ -#define BUSINESS_FRAME_HEADER_LENGTH 2u +/** 发送方短地址字段长度。 */ +#define BUSINESS_SOURCE_ADDRESS_LENGTH 2u /** 业务命令:继电器控制。 */ #define CMD_RELAY_CONTROL 0x01u @@ -34,9 +35,22 @@ /** 业务命令:电流阈值配置。 */ #define CMD_THRESHOLD_CONFIG 0x06u +/** 查询当前主程序固件版本。 */ +#define CMD_FIRMWARE_VERSION_QUERY 0x0Eu + +/** 查询固件版本应答。 */ +#define CMD_FIRMWARE_VERSION_RESPONSE \ + (CMD_FIRMWARE_VERSION_QUERY | CMD_RESPONSE_FLAG) + +/** Bootloader升级结果主动上报。 */ +#define CMD_OTA_RESULT_REPORT 0x0Du + /** 应答命令基值,具体应答为请求命令加 0x80。 */ #define CMD_RESPONSE_FLAG 0x80u +/** OTA升级结果上报应答命令。 */ +#define CMD_OTA_RESULT_ACK (CMD_OTA_RESULT_REPORT | CMD_RESPONSE_FLAG) + /** 通用错误应答命令。 */ #define CMD_ERROR_RESPONSE 0xE0u @@ -76,10 +90,13 @@ uint8_t BusinessProtocol_GetDataLength(void); /** 读取最近一帧合法业务帧的数据区;返回 0 表示下标越界。 */ uint8_t BusinessProtocol_GetDataByte(uint8_t index, uint8_t *value); -/** 执行最近一帧已经通过校验的业务命令,并向源地址返回应答。 */ -void BusinessProtocol_HandleFrame(uint16_t source_address); +/** 读取最近一帧合法业务帧携带的发送方 Mesh 短地址。 */ +uint16_t BusinessProtocol_GetSourceAddress(void); -/** CRC-16/MODBUS 计算函数,输入范围不包含业务帧头。 */ +/** 执行最近一帧已经通过校验的业务命令,并向源地址返回应答。 */ +void BusinessProtocol_HandleFrame(void); + +/** CRC-16/MODBUS 计算函数,输入范围包含发送方地址字段。 */ uint16_t BusinessProtocol_Crc16Modbus(const uint8_t *data, uint16_t length); #endif diff --git a/include/interrupt_control.h b/include/interrupt_control.h new file mode 100644 index 0000000..040d8f3 --- /dev/null +++ b/include/interrupt_control.h @@ -0,0 +1,17 @@ +/** + * @file interrupt_control.h + * @brief STM8 全局中断控制接口。 + * + * STM8 的全局中断允许位位于 CPU 条件代码寄存器中,不能像 GPIO 或定时器 + * 那样通过普通 C 变量直接访问,因此由本模块统一封装 SDCC 内联汇编。 + */ +#ifndef INTERRUPT_CONTROL_H +#define INTERRUPT_CONTROL_H + +/** 开启 STM8 全局中断,使 TIM4、UART 等中断可以进入。 */ +void Interrupts_Enable(void); + +/** 关闭 STM8 全局中断,用于极短的不可打断临界区。 */ +void Interrupts_Disable(void); + +#endif diff --git a/include/mesh_frame.h b/include/mesh_frame.h index f281935..e4da897 100644 --- a/include/mesh_frame.h +++ b/include/mesh_frame.h @@ -16,8 +16,8 @@ void MeshFrame_Init(void); /** 从 UART 接收并解析 JDY-28M 外层帧。 */ void MeshFrame_Task(void); -/** 返回最近一帧合法数据的源短地址。 */ -uint16_t MeshFrame_GetLastSourceAddress(void); +/** 返回最近一帧合法外层数据携带的目标短地址,仅用于诊断。 */ +uint16_t MeshFrame_GetLastTargetAddress(void); /** * 封装并发送一帧 Mesh 业务数据。 diff --git a/include/ota_manager.h b/include/ota_manager.h new file mode 100644 index 0000000..8085786 --- /dev/null +++ b/include/ota_manager.h @@ -0,0 +1,33 @@ +/** + * @file ota_manager.h + * @brief 应用程序侧 OTA 管理接口。 + * + * 应用程序负责通过 JDY-28M 接收 OTA 分包、校验业务帧并写入 W25Q64。 + * 下载完成后,应用程序只设置升级请求标志并复位;真正擦写片内程序 + * Flash 的工作由独立 Bootloader 完成。 + */ +#ifndef OTA_MANAGER_H +#define OTA_MANAGER_H + +#include + +/** 初始化 OTA 状态机和外部 Flash 接口。 */ +void OtaManager_Init(void); +/** 周期性处理 OTA 状态机,不得长时间阻塞主循环。 */ +void OtaManager_Task(void); +/** 处理一条已经通过 Mesh 外层解析的 OTA 业务命令。 */ +uint8_t OtaManager_HandleCommand(uint8_t command, + const uint8_t *data, + uint8_t length); +/** 返回当前是否处于 OTA 下载模式。 */ +uint8_t OtaManager_IsActive(void); +uint8_t OtaManager_GetExpectedOffset(uint16_t *offset); +/** + * 读取Bootloader写入的升级结果并准备由主程序主动上报。 + * @return 0表示没有待上报结果,非0表示升级结果码。 + */ +uint8_t OtaManager_GetBootloaderResult(void); +/** 主机确认升级结果后清除待上报状态。 */ +void OtaManager_AcknowledgeBootloaderResult(void); + +#endif diff --git a/include/stm8s003_registers.h b/include/stm8s003_registers.h index 6c44fdc..53ac7f2 100644 --- a/include/stm8s003_registers.h +++ b/include/stm8s003_registers.h @@ -16,6 +16,11 @@ #define GPIOA_DDR_ADDR 0x5002u #define GPIOA_CR1_ADDR 0x5003u #define GPIOA_CR2_ADDR 0x5004u +#define GPIOB_ODR_ADDR 0x5005u +#define GPIOB_IDR_ADDR 0x5006u +#define GPIOB_DDR_ADDR 0x5007u +#define GPIOB_CR1_ADDR 0x5008u +#define GPIOB_CR2_ADDR 0x5009u #define GPIOC_ODR_ADDR 0x500Au #define GPIOC_IDR_ADDR 0x500Bu #define GPIOC_DDR_ADDR 0x500Cu @@ -26,6 +31,10 @@ #define GPIOD_DDR_ADDR 0x5011u #define GPIOD_CR1_ADDR 0x5012u #define GPIOD_CR2_ADDR 0x5013u +#define SPI1_CR1_ADDR 0x5200u +#define SPI1_CR2_ADDR 0x5201u +#define SPI1_SR_ADDR 0x5203u +#define SPI1_DR_ADDR 0x5204u /* UART1 寄存器地址。 */ #define UART1_SR_ADDR 0x5230u @@ -63,6 +72,7 @@ #define REG8(address) (*((volatile uint8_t *)(address))) #define PA1_MASK 0x02u +#define PA2_MASK 0x04u #define PC3_MASK 0x08u #define PC4_MASK 0x10u /** PC4 在 STM8S003F3P6 上对应 ADC1 的 AIN2 通道。 */ @@ -72,5 +82,24 @@ #define PD6_MASK 0x40u #define PC5_MASK 0x20u #define PC6_MASK 0x40u +#define PC7_MASK 0x80u +#define PD2_MASK 0x04u +#define PD4_MASK 0x10u +#define PB5_MASK 0x20u +#define PB4_MASK 0x10u + +#define SPI1_CR1 REG8(SPI1_CR1_ADDR) +#define SPI1_CR2 REG8(SPI1_CR2_ADDR) +#define SPI1_SR REG8(SPI1_SR_ADDR) +#define SPI1_DR REG8(SPI1_DR_ADDR) +#define SPI_CR1_SPE 0x40u +#define SPI_CR1_MSTR 0x04u +#define SPI_CR1_BR0 0x08u +#define SPI_CR1_BR1 0x10u +#define SPI_CR1_BR2 0x20u +#define SPI_CR2_SSI 0x01u +#define SPI_CR2_SSM 0x02u +#define SPI_SR_RXNE 0x01u +#define SPI_SR_TXE 0x02u #endif diff --git a/include/w25q64_app.h b/include/w25q64_app.h new file mode 100644 index 0000000..3511255 --- /dev/null +++ b/include/w25q64_app.h @@ -0,0 +1,12 @@ +/** @file w25q64_app.h @brief 主程序侧W25Q64结果读取接口。 */ +#ifndef W25Q64_APP_H +#define W25Q64_APP_H +#include +void 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); +uint8_t W25Q64App_PageProgram(uint32_t address, const uint8_t *data, + uint16_t length); +uint8_t W25Q64App_ClearUpgradeResult(void); +#endif diff --git a/src/drivers/board.c b/src/drivers/board.c index 77655e7..d08476f 100644 --- a/src/drivers/board.c +++ b/src/drivers/board.c @@ -8,11 +8,18 @@ #include #include "board.h" #include "stm8s003_registers.h" +#include "system_tick.h" /** 当前 PA1 继电器控制输出的逻辑状态。 */ static uint8_t relay_state; -/** 当前 PD3 LED3 输出的逻辑状态。 */ +/** 当前 PB5 蓝牙组网/故障指示灯的逻辑状态。 */ static uint8_t led3_state; +/** 当前 PD3 继电器状态灯的逻辑状态。 */ +static uint8_t relay_led_state; +/** PB4程序状态灯当前逻辑状态,1表示逻辑点亮。 */ +static uint8_t status_led_state; +/** 上次翻转PB4状态灯的系统时间。 */ +static uint32_t status_led_last_ms; /** 初始化板级资源,并将继电器置于安全断开状态。 */ void Board_Init(void) @@ -28,13 +35,23 @@ void Board_Init(void) REG8(GPIOC_CR1_ADDR) &= (uint8_t)~(PC3_MASK | PC4_MASK); REG8(GPIOC_CR2_ADDR) &= (uint8_t)~(PC3_MASK | PC4_MASK); - /* PC5/PWRC 和 PC6/RESET 上电默认高电平,避免模块误进入 AT 或复位。 */ - REG8(GPIOC_ODR_ADDR) |= (uint8_t)(PC5_MASK | PC6_MASK); - REG8(GPIOC_DDR_ADDR) |= (uint8_t)(PC5_MASK | PC6_MASK); - REG8(GPIOC_CR1_ADDR) |= (uint8_t)(PC5_MASK | PC6_MASK); - REG8(GPIOC_CR2_ADDR) &= (uint8_t)~(PC5_MASK | PC6_MASK); + /* PA2/PWRC 和 PD2/RESET 上电默认高电平。PC5~PC7释放给硬件SPI。 */ + REG8(GPIOA_ODR_ADDR) |= PA2_MASK; + REG8(GPIOA_DDR_ADDR) |= PA2_MASK; + REG8(GPIOA_CR1_ADDR) |= PA2_MASK; + REG8(GPIOA_CR2_ADDR) &= (uint8_t)~PA2_MASK; + REG8(GPIOD_ODR_ADDR) |= PD2_MASK; + REG8(GPIOD_DDR_ADDR) |= PD2_MASK; + REG8(GPIOD_CR1_ADDR) |= PD2_MASK; + REG8(GPIOD_CR2_ADDR) &= (uint8_t)~PD2_MASK; - /* PD3:推挽输出,默认熄灭 LED3。 */ + /* 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; + + /* PD3:高电平点亮,默认熄灭继电器状态灯。 */ REG8(GPIOD_DDR_ADDR) |= PD3_MASK; REG8(GPIOD_CR1_ADDR) |= PD3_MASK; REG8(GPIOD_CR2_ADDR) &= (uint8_t)~PD3_MASK; @@ -42,6 +59,15 @@ void Board_Init(void) relay_state = 0u; led3_state = 0u; + relay_led_state = 0u; + 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; } /** 设置继电器状态,后续接入 PA1 寄存器时保持高电平吸合约定。 */ @@ -70,11 +96,25 @@ uint8_t Board_ReadMeshJoined(void) return ((REG8(GPIOC_IDR_ADDR) & PC3_MASK) != 0u) ? 1u : 0u; } -/** 设置 LED3 状态,后续接入 PD3 寄存器时保持高电平点亮约定。 */ +/** 设置PB5蓝牙组网/故障指示灯;由于低电平点亮,逻辑值需要反相输出。 */ void Board_SetLed3(uint8_t on) { led3_state = (on != 0u) ? 1u : 0u; if (led3_state != 0u) + { + REG8(GPIOB_ODR_ADDR) &= (uint8_t)~PB5_MASK; + } + else + { + REG8(GPIOB_ODR_ADDR) |= PB5_MASK; + } +} + +/** 设置PD3继电器状态灯;高电平点亮,与逻辑值保持同相。 */ +void Board_SetRelayLed(uint8_t on) +{ + relay_led_state = (on != 0u) ? 1u : 0u; + if (relay_led_state != 0u) { REG8(GPIOD_ODR_ADDR) |= PD3_MASK; } @@ -84,16 +124,35 @@ void Board_SetLed3(uint8_t on) } } +/** + * @brief 周期驱动PB4程序状态灯。 + * @param ota_active 1表示OTA接收/校验中,0表示正常运行。 + */ +void Board_StatusLedTask(uint8_t ota_active) +{ + uint32_t now = SystemTick_NowMs(); + uint16_t period = (ota_active != 0u) ? 100u : 500u; + if ((uint32_t)(now - status_led_last_ms) >= period) + { + 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; + else + REG8(GPIOB_ODR_ADDR) |= PB4_MASK; + } +} + /** 控制 PWRC;低电平使能 AT 指令窗口。 */ void Board_SetBluetoothAtWindow(uint8_t enabled) { - if (enabled != 0u) REG8(GPIOC_ODR_ADDR) &= (uint8_t)~PC5_MASK; - else REG8(GPIOC_ODR_ADDR) |= PC5_MASK; + if (enabled != 0u) REG8(GPIOA_ODR_ADDR) &= (uint8_t)~PA2_MASK; + else REG8(GPIOA_ODR_ADDR) |= PA2_MASK; } /** 控制 RESET;低电平复位,高电平正常工作。 */ void Board_SetBluetoothReset(uint8_t reset_active) { - if (reset_active != 0u) REG8(GPIOC_ODR_ADDR) &= (uint8_t)~PC6_MASK; - else REG8(GPIOC_ODR_ADDR) |= PC6_MASK; + if (reset_active != 0u) REG8(GPIOD_ODR_ADDR) &= (uint8_t)~PD2_MASK; + else REG8(GPIOD_ODR_ADDR) |= PD2_MASK; } diff --git a/src/drivers/interrupt_control.c b/src/drivers/interrupt_control.c new file mode 100644 index 0000000..1ecb7a3 --- /dev/null +++ b/src/drivers/interrupt_control.c @@ -0,0 +1,31 @@ +/** + * @file interrupt_control.c + * @brief STM8 全局中断控制封装实现。 + * + * 这里保留极少量与 CPU 架构相关的汇编指令,业务代码不再直接出现 + * `__asm`、`rim` 或 `sim`,从而提高主程序可读性,也方便以后更换编译器 + * 时集中修改底层适配代码。 + */ +#include "interrupt_control.h" + +/** + * 开启全局中断。 + * + * STM8 `rim` 指令会清除 CPU 的中断屏蔽状态。该函数应在 TIM4、UART 等 + * 外设完成初始化后调用,避免外设尚未配置完整时提前进入中断服务程序。 + */ +void Interrupts_Enable(void) +{ + __asm__("rim"); +} + +/** + * 关闭全局中断。 + * + * STM8 `sim` 指令会屏蔽可屏蔽中断。临界区应尽可能短,不能在关闭中断后 + * 执行 UART 等待、EEPROM 写入或长时间循环,否则可能导致系统节拍丢失。 + */ +void Interrupts_Disable(void) +{ + __asm__("sim"); +} diff --git a/src/drivers/w25q64_app.c b/src/drivers/w25q64_app.c new file mode 100644 index 0000000..6dd6512 --- /dev/null +++ b/src/drivers/w25q64_app.c @@ -0,0 +1,87 @@ +/** @file w25q64_app.c @brief 主程序侧W25Q64只读结果访问驱动。 */ +#include "w25q64_app.h" +#include "stm8s003_registers.h" + +#define W25_CS_MASK PD4_MASK +#define W25_CMD_READ 0x03u +#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 0x030010ul + +static uint8_t Xfer(uint8_t value) +{ + while ((SPI1_SR & SPI_SR_TXE) == 0u) {} + SPI1_DR = value; + while ((SPI1_SR & SPI_SR_RXNE) == 0u) {} + return SPI1_DR; +} +static void Select(void){REG8(GPIOD_ODR_ADDR)&=(uint8_t)~W25_CS_MASK;} +static void Deselect(void){REG8(GPIOD_ODR_ADDR)|=W25_CS_MASK;} +static uint8_t Ready(void) +{ + uint32_t timeout=200000ul; + uint8_t value; + do { Select(); Xfer(W25_CMD_RDSR); value=Xfer(0xFFu); Deselect(); } + while ((value&W25_BUSY)!=0u && timeout--!=0ul); + return (value&W25_BUSY)==0u ? 1u : 0u; +} +static void WriteEnable(void){Select();Xfer(W25_CMD_WREN);Deselect();} + +void W25Q64App_Init(void) +{ + REG8(GPIOC_DDR_ADDR)|=(uint8_t)(PC5_MASK|PC6_MASK); + REG8(GPIOC_CR1_ADDR)|=(uint8_t)(PC5_MASK|PC6_MASK); + REG8(GPIOC_DDR_ADDR)&=(uint8_t)~PC7_MASK; + REG8(GPIOC_CR1_ADDR)&=(uint8_t)~PC7_MASK; + REG8(GPIOD_DDR_ADDR)|=W25_CS_MASK; + REG8(GPIOD_CR1_ADDR)|=W25_CS_MASK; + 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); +} +uint8_t W25Q64App_ReadByte(uint32_t address,uint8_t *value) +{ + if(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; +} +uint8_t W25Q64App_Read(uint32_t address,uint8_t *buffer,uint16_t length) +{ + uint16_t index; + if(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(); + return Ready(); +} +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 || + (((uint16_t)address&0xFFu)+length)>256u)return 0u; + WriteEnable(); Select(); Xfer(W25_CMD_PROGRAM); + Xfer((uint8_t)(address>>16)); Xfer((uint8_t)(address>>8)); + Xfer((uint8_t)address); + for(index=0u;index BUSINESS_MAX_DATA_LENGTH) { - parser_state = PARSER_WAIT_HEAD_0; + parser_state = PARSER_WAIT_SOURCE_HIGH; } else if (parser_length == 0u) { @@ -148,21 +151,24 @@ uint8_t BusinessProtocol_InputByte(uint8_t value) case PARSER_WAIT_CRC_HIGH: parser_received_crc |= (uint16_t)value << 8u; - parser_crc_input[0] = parser_command; - parser_crc_input[1] = parser_length; + 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; for (index = 0u; index < parser_length; ++index) { - parser_crc_input[2u + index] = parser_data[index]; + parser_crc_input[4u + index] = parser_data[index]; } expected_crc = BusinessProtocol_Crc16Modbus(parser_crc_input, - (uint16_t)(2u + parser_length)); - parser_state = PARSER_WAIT_HEAD_0; + (uint16_t)(4u + parser_length)); + parser_state = PARSER_WAIT_SOURCE_HIGH; if (expected_crc != parser_received_crc) { return 0u; } last_command = parser_command; + last_source_address = parser_source_address; last_length = parser_length; for (index = 0u; index < last_length; ++index) { @@ -171,7 +177,7 @@ uint8_t BusinessProtocol_InputByte(uint8_t value) return 1u; default: - parser_state = PARSER_WAIT_HEAD_0; + parser_state = PARSER_WAIT_SOURCE_HIGH; break; } @@ -201,6 +207,12 @@ uint8_t BusinessProtocol_GetDataByte(uint8_t index, uint8_t *value) return 1u; } +/** 读取最近一帧合法业务帧携带的发送方 Mesh 短地址。 */ +uint16_t BusinessProtocol_GetSourceAddress(void) +{ + return last_source_address; +} + /** * 执行当前合法业务帧。 * @@ -208,8 +220,9 @@ uint8_t BusinessProtocol_GetDataByte(uint8_t index, uint8_t *value) * 和实际电流查询。控制命令只有 DATA=00/01 才会改变 PA1;其他参数只返 * 回错误应答,不会误动作。应答目标使用接收帧中的源短地址。 */ -void BusinessProtocol_HandleFrame(uint16_t source_address) +void BusinessProtocol_HandleFrame(void) { + uint16_t source_address = last_source_address; uint8_t value; uint8_t current_data[2u]; uint16_t current; @@ -225,6 +238,7 @@ void BusinessProtocol_HandleFrame(uint16_t source_address) break; } Board_SetRelay(last_data[0]); + Board_SetRelayLed(Board_GetRelay()); BusinessProtocol_SendByteResponse(source_address, CMD_RELAY_CONTROL | CMD_RESPONSE_FLAG, Board_GetRelay()); @@ -257,6 +271,21 @@ void BusinessProtocol_HandleFrame(uint16_t source_address) current_data, 2u); break; + case CMD_FIRMWARE_VERSION_QUERY: + if (last_length != 0u) + { + value = 0x02u; + BusinessProtocol_SendByteResponse(source_address, + CMD_ERROR_RESPONSE, value); + break; + } + current_data[0] = (uint8_t)FIRMWARE_VERSION; + current_data[1] = (uint8_t)(FIRMWARE_VERSION >> 8u); + (void)MeshFrame_SendBusiness(source_address, + CMD_FIRMWARE_VERSION_RESPONSE, + current_data, 2u); + break; + case (CMD_FAULT_REPORT | CMD_RESPONSE_FLAG): if (last_length == 1u) { @@ -264,6 +293,42 @@ void BusinessProtocol_HandleFrame(uint16_t source_address) } break; + case CMD_OTA_RESULT_ACK: + if (last_length == 1u) + { + OtaManager_AcknowledgeBootloaderResult(); + } + break; + + case 0x08u: + case 0x09u: + case 0x0Au: + case 0x0Bu: + case 0x0Cu: + { + uint8_t ota_status = OtaManager_HandleCommand(last_command, + last_data, + last_length) ? 0x00u : 0x01u; + uint8_t ota_response[3u]; + uint16_t next_offset = 0u; + uint8_t response_command = (uint8_t)(last_command | CMD_RESPONSE_FLAG); + if (last_command == 0x09u) + { + (void)OtaManager_GetExpectedOffset(&next_offset); + ota_response[0] = ota_status; + ota_response[1] = (uint8_t)(next_offset >> 8u); + ota_response[2] = (uint8_t)next_offset; + (void)MeshFrame_SendBusiness(source_address, response_command, + ota_response, 3u); + } + else + { + BusinessProtocol_SendByteResponse(source_address, + response_command, ota_status); + } + break; + } + case CMD_THRESHOLD_CONFIG: if (last_length != 2u) { diff --git a/src/protocol/mesh_frame.c b/src/protocol/mesh_frame.c index ef69a0a..d5923ea 100644 --- a/src/protocol/mesh_frame.c +++ b/src/protocol/mesh_frame.c @@ -3,10 +3,12 @@ * @brief JDY-28M Mesh 外层帧实现。 * * 发送格式:41 54 2B AA A1 | 目标地址高低字节 | A0 | 业务帧 | 0D 0A。 - * 接收格式:F1 DD 09 00 0A | 源地址高低字节 | 业务帧 | 0D 0A。 + * 接收格式:F1 DD 09 00 0A | 目标地址高低字节 | 业务帧 | 0D 0A。 + * 发送方地址由业务帧 SRC_H/SRC_L 携带;JDY-28M 外层接收地址不是发送方地址。 */ #include #include "business_protocol.h" +#include "bluetooth_manager.h" #include "mesh_frame.h" #include "uart1.h" @@ -37,10 +39,10 @@ typedef enum 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。 */ @@ -51,8 +53,8 @@ typedef enum /** JDY-28M 外层接收状态机当前状态。 */ static MeshParserState mesh_state; -/** 最近一帧合法外层数据携带的源短地址。 */ -static uint16_t last_source_address; +/** 最近一帧合法外层数据携带的目标短地址,仅用于诊断。 */ +static uint16_t last_target_address; /** 当前正在接收的业务帧缓冲区。 */ static uint8_t business_buffer[BUSINESS_MAX_DATA_LENGTH + 6u]; /** 当前已经接收的业务帧字节数。 */ @@ -71,7 +73,7 @@ static void MeshFrame_ResetParser(void) /** 初始化外层帧解析器。 */ void MeshFrame_Init(void) { - last_source_address = 0u; + last_target_address = 0u; MeshFrame_ResetParser(); } @@ -100,14 +102,14 @@ static void MeshFrame_InputByte(uint8_t value) mesh_state = (value == RX_PREFIX_3) ? MESH_WAIT_PREFIX_4 : MESH_WAIT_PREFIX_0; break; case MESH_WAIT_PREFIX_4: - mesh_state = (value == RX_PREFIX_4) ? MESH_READ_SOURCE_HIGH : MESH_WAIT_PREFIX_0; + mesh_state = (value == RX_PREFIX_4) ? MESH_READ_TARGET_HIGH : MESH_WAIT_PREFIX_0; break; - case MESH_READ_SOURCE_HIGH: - last_source_address = (uint16_t)value << 8u; - mesh_state = MESH_READ_SOURCE_LOW; + case MESH_READ_TARGET_HIGH: + last_target_address = (uint16_t)value << 8u; + mesh_state = MESH_READ_TARGET_LOW; break; - case MESH_READ_SOURCE_LOW: - last_source_address |= value; + case MESH_READ_TARGET_LOW: + last_target_address |= value; business_length = 0u; mesh_state = MESH_READ_BUSINESS; break; @@ -120,11 +122,6 @@ static void MeshFrame_InputByte(uint8_t value) business_buffer[business_length++] = value; if (business_length >= 4u) { - if ((business_buffer[0] != 0x55u) || (business_buffer[1] != 0xAAu)) - { - MeshFrame_ResetParser(); - break; - } expected_length = (uint8_t)(business_buffer[3] + 6u); business_expected_length = expected_length; if ((business_buffer[3] > BUSINESS_MAX_DATA_LENGTH) || @@ -157,7 +154,7 @@ static void MeshFrame_InputByte(uint8_t value) } if (frame_ready != 0u) { - BusinessProtocol_HandleFrame(last_source_address); + BusinessProtocol_HandleFrame(); } } MeshFrame_ResetParser(); @@ -179,9 +176,9 @@ void MeshFrame_Task(void) } /** 返回最近一帧合法外层数据的源地址。 */ -uint16_t MeshFrame_GetLastSourceAddress(void) +uint16_t MeshFrame_GetLastTargetAddress(void) { - return last_source_address; + return last_target_address; } /** 封装并发送一帧业务数据。 */ @@ -190,7 +187,8 @@ uint8_t MeshFrame_SendBusiness(uint16_t target_address, const uint8_t *data, uint8_t length) { - uint8_t crc_input[2u + BUSINESS_MAX_DATA_LENGTH]; + uint8_t crc_input[4u + BUSINESS_MAX_DATA_LENGTH]; + uint16_t source_address; uint16_t crc; uint8_t index; @@ -199,10 +197,13 @@ uint8_t MeshFrame_SendBusiness(uint16_t target_address, { return 0u; } - 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)); + 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)); Uart1_WriteByte(TX_PREFIX_0); Uart1_WriteByte(TX_PREFIX_1); Uart1_WriteByte(TX_PREFIX_2); Uart1_WriteByte(TX_PREFIX_3); @@ -210,7 +211,8 @@ 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(0x55u); Uart1_WriteByte(0xAAu); + 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); Uart1_WriteByte((uint8_t)(crc >> 8u)); diff --git a/src/protocol/ota_manager.c b/src/protocol/ota_manager.c new file mode 100644 index 0000000..1fcd325 --- /dev/null +++ b/src/protocol/ota_manager.c @@ -0,0 +1,240 @@ +/** + * @file ota_manager.c + * @brief 应用程序侧 OTA 下载状态机。 + * + * 重要职责边界: + * - 本模块属于 Application,不属于 Bootloader; + * - 本模块接收 Mesh OTA 数据并写入外部 W25Q64; + * - 本模块不擦写 STM8 内部程序 Flash; + * - 下载完成并完成整包 CRC16/MODBUS 校验后,才设置升级请求; + * - 复位后由 Bootloader 读取升级请求并执行最终搬运。 + * + * 当前代码先建立稳定的状态机和接口边界。W25Q64 的 SPI 底层驱动需要 + * 在实际硬件确认片选、SPI模式、时钟和读写时序后接入。 + */ +#include "ota_manager.h" +#include "w25q64_app.h" +#include "mesh_frame.h" +#include "business_protocol.h" + +#define OTA_IMAGE_ADDRESS 0x020000ul +#define OTA_METADATA_ADDRESS 0x030000ul + +#define OTA_COMMAND_START 0x08u +#define OTA_COMMAND_DATA 0x09u +#define OTA_COMMAND_FINISH 0x0Au +#define OTA_COMMAND_CANCEL 0x0Cu + +#define OTA_STATE_IDLE 0u +#define OTA_STATE_RECEIVING 1u +#define OTA_STATE_READY 2u + +/** OTA 状态机当前状态。 */ +static uint8_t ota_state; +/** 已经写入外部 Flash 的固件字节数。 */ +static uint16_t ota_received_size; +/** 主机声明的固件总长度。 */ +static uint16_t ota_expected_size; +/** 主机声明的完整固件 CRC16/MODBUS。 */ +static uint16_t ota_expected_crc16; +/** Bootloader升级结果缓存;实际值由W25Q64驱动读取后填充。 */ +static uint8_t ota_bootloader_result; +/** 升级结果是否已经等待主程序上报。 */ +static uint8_t ota_bootloader_result_pending; +/** 防止主循环每次重复发送同一条启动结果。 */ +static uint8_t ota_bootloader_result_reported; +static uint16_t ota_expected_offset; +static uint16_t ota_image_size; +static uint16_t ota_image_version; +static uint16_t ota_expected_crc16; + +/** + * @brief 初始化 OTA 状态机。 + */ +void OtaManager_Init(void) +{ + ota_state = OTA_STATE_IDLE; + ota_received_size = 0u; + ota_expected_size = 0u; + ota_bootloader_result = 0u; + ota_bootloader_result_pending = 0u; + ota_bootloader_result_reported = 0u; + ota_expected_offset = 0u; + ota_image_size = 0u; + ota_image_version = 0u; + W25Q64App_Init(); + if (W25Q64App_ReadByte(0x030010ul, &ota_bootloader_result) != 0u && + ota_bootloader_result != 0u) + { + ota_bootloader_result_pending = 1u; + } +} + +/** + * @brief 周期性执行 OTA 状态机。 + * + * 擦除、页编程和状态轮询都应拆成小步骤,以免触发5秒看门狗复位。 + */ +void OtaManager_Task(void) +{ + uint8_t result; + if (ota_bootloader_result_pending != 0u && + ota_bootloader_result_reported == 0u) + { + result = ota_bootloader_result; + if (MeshFrame_SendBusiness(0xFFFFu, CMD_OTA_RESULT_REPORT, + &result, 1u) != 0u) + { + ota_bootloader_result_reported = 1u; + } + } +} + +/** + * @brief 处理应用程序侧 OTA 命令。 + * @param command OTA命令号。 + * @param data 命令数据指针。 + * @param length 命令数据长度。 + * @return 1表示命令格式被接受,0表示命令非法或当前状态不允许。 + */ +uint8_t OtaManager_HandleCommand(uint8_t command, + const uint8_t *data, + uint8_t length) +{ + if (command == OTA_COMMAND_START) + { + uint8_t sector; + if (length != 4u) + { + return 0u; + } + ota_expected_size=(uint16_t)data[0]|((uint16_t)data[1]<<8); + ota_image_version=(uint16_t)data[2]|((uint16_t)data[3]<<8); + if(ota_expected_size==0u) + return 0u; + /* 先清除OTA元数据,避免上一次升级请求残留。 */ + if(W25Q64App_EraseSector(OTA_METADATA_ADDRESS)==0u) + return 0u; + for(sector=0u;sector<16u;sector++) + if(W25Q64App_EraseSector(OTA_IMAGE_ADDRESS+((uint32_t)sector*0x1000ul))==0u) + return 0u; + ota_image_size=(uint16_t)ota_expected_size; + ota_received_size=0u; ota_expected_offset=0u; + ota_expected_crc16=0u; + ota_state = OTA_STATE_RECEIVING; + return 1u; + } + + if (command == OTA_COMMAND_DATA) + { + if (ota_state != OTA_STATE_RECEIVING || data == (const uint8_t *)0 || + (length != 3u && length != 4u)) + { + return 0u; + } + { + uint16_t offset=(uint16_t)((uint16_t)data[0]<<8)|data[1]; + uint8_t payload_length=(uint8_t)(length-2u); + if(offset!=ota_expected_offset || + (uint16_t)(offset+payload_length)>ota_expected_size)return 0u; + if(W25Q64App_PageProgram(OTA_IMAGE_ADDRESS+offset,&data[2], + payload_length)==0u) + return 0u; + ota_expected_offset=(uint16_t)(offset+payload_length); + ota_received_size=(uint16_t)(ota_received_size+payload_length); + } + return 1u; + } + + if (command == OTA_COMMAND_FINISH) + { + /* CMD=0x0A的DATA为完整固件CRC16低字节、高字节。 */ + if (ota_state != OTA_STATE_RECEIVING || data == (const uint8_t *)0 || + length != 2u) + { + return 0u; + } + + ota_expected_crc16 = (uint16_t)data[0] | ((uint16_t)data[1] << 8); + ota_state = OTA_STATE_READY; + return 1u; + } + + if (command == 0x0Bu) + { + uint8_t buffer[16]; uint16_t offset=0u; uint16_t crc=0xFFFFu; + uint16_t chunk; + uint8_t metadata[18]; + if(ota_state!=OTA_STATE_READY||ota_received_size!=ota_expected_size) + return 0u; + while(offset16u)chunk=16u; + if(W25Q64App_Read(OTA_IMAGE_ADDRESS+offset,buffer,chunk)==0u)return 0u; + { + uint16_t index; + for(index=0u;index>1)^0xA001u):(uint16_t)(crc>>1); + } + } + offset=(uint16_t)(offset+chunk); + } + if(crc!=ota_expected_crc16)return 0u; + metadata[0]=0x31u; metadata[1]=0x41u; metadata[2]=0x54u; metadata[3]=0x4Fu; + metadata[4]=(uint8_t)ota_image_size; metadata[5]=(uint8_t)(ota_image_size>>8); + metadata[6]=0u; metadata[7]=0u; + metadata[8]=(uint8_t)ota_image_version; metadata[9]=(uint8_t)(ota_image_version>>8); + metadata[10]=(uint8_t)crc; metadata[11]=(uint8_t)(crc>>8); + metadata[12]=0u; metadata[13]=0u; metadata[14]=0xA5u; + metadata[15]=0xA5u; metadata[16]=0u; metadata[17]=0u; + if(W25Q64App_PageProgram(OTA_METADATA_ADDRESS,metadata,16u)==0u)return 0u; + if(W25Q64App_PageProgram(OTA_METADATA_ADDRESS+16ul,&metadata[16],2u)==0u)return 0u; + ota_state=OTA_STATE_IDLE; + return 1u; + } + + if (command == OTA_COMMAND_CANCEL) + { + ota_state = OTA_STATE_IDLE; + ota_received_size = 0u; + return 1u; + } + + return 0u; +} + +/** + * @brief 查询OTA是否正在进行。 + * @return 1表示已进入OTA下载或完成等待状态,0表示普通运行状态。 + */ +uint8_t OtaManager_IsActive(void) +{ + return (ota_state != OTA_STATE_IDLE) ? 1u : 0u; +} + +uint8_t OtaManager_GetExpectedOffset(uint16_t *offset) +{ + if(offset==(uint16_t *)0)return 0u; + *offset=ota_expected_offset; return 1u; +} + +uint8_t OtaManager_GetBootloaderResult(void) +{ + /* W25Q64读取接口接入后,在此处加载result字段并置pending。 */ + return (ota_bootloader_result_pending != 0u) ? ota_bootloader_result : 0u; +} + +void OtaManager_AcknowledgeBootloaderResult(void) +{ + if (ota_bootloader_result_pending != 0u) + { + if (W25Q64App_ClearUpgradeResult() != 0u) + { + ota_bootloader_result = 0u; + ota_bootloader_result_pending = 0u; + ota_bootloader_result_reported = 0u; + } + } +}