feat: 完善Mesh继电器OTA与地址协议
- 增加STM8 Bootloader工程及出厂固件恢复流程\n- 增加W25Q64外部Flash驱动和OTA分包接收、CRC16校验、断点续传状态管理\n- 增加固件版本查询、升级结果上报与主机确认机制\n- 将业务帧从55 AA帧头调整为发送方Mesh短地址+CMD+LEN+DATA+CRC16\n- 修正JDY-28M外层地址与业务发送方地址的职责划分,支持广播请求定向应答\n- 同步更新硬件引脚、存储分区、OTA流程和主机通信协议文档\n- 已使用SDCC重新编译,应用代码大小为5512字节,链接地址为0x8800
This commit is contained in:
@@ -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 状态指示。
|
||||
|
||||
### 阶段四:继电器业务
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
+11
-11
@@ -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 与当前待确认故障码一致后,停止该事件的周期重发;
|
||||
|
||||
@@ -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)
|
||||
@@ -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` 重新链接。
|
||||
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* @file bootloader.h
|
||||
* @brief Bootloader 顶层流程接口。
|
||||
*/
|
||||
#ifndef STM8_BOOTLOADER_H
|
||||
#define STM8_BOOTLOADER_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
/** 初始化 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
|
||||
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* @file bootloader_config.h
|
||||
* @brief Bootloader 的固定地址和安全策略配置。
|
||||
*/
|
||||
#ifndef STM8_BOOTLOADER_CONFIG_H
|
||||
#define STM8_BOOTLOADER_CONFIG_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
/** 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
|
||||
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* @file stm8_bootloader_registers.h
|
||||
* @brief Bootloader 使用的 STM8S003F3P6 最小寄存器定义。
|
||||
*/
|
||||
#ifndef STM8_BOOTLOADER_REGISTERS_H
|
||||
#define STM8_BOOTLOADER_REGISTERS_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#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
|
||||
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* @file w25q64.h
|
||||
* @brief W25Q64JV 硬件SPI访问接口。
|
||||
*/
|
||||
#ifndef STM8_BOOTLOADER_W25Q64_H
|
||||
#define STM8_BOOTLOADER_W25Q64_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
/** 初始化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
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* @file main.c
|
||||
* @brief Bootloader 独立工程入口。
|
||||
*/
|
||||
#include "bootloader.h"
|
||||
|
||||
/**
|
||||
* @brief Bootloader 入口函数。
|
||||
* @return 理论上不会返回。
|
||||
*/
|
||||
int main(void)
|
||||
{
|
||||
Bootloader_Init();
|
||||
Bootloader_Task();
|
||||
return 0;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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`。
|
||||
@@ -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 = 升级结果码
|
||||
```
|
||||
|
||||
发送给主机。主机确认后,主程序清除结果字段。
|
||||
@@ -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
|
||||
|
||||
+9
-2
@@ -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);
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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 业务数据。
|
||||
|
||||
@@ -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 <stdint.h>
|
||||
|
||||
/** 初始化 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
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
/** @file w25q64_app.h @brief 主程序侧W25Q64结果读取接口。 */
|
||||
#ifndef W25Q64_APP_H
|
||||
#define W25Q64_APP_H
|
||||
#include <stdint.h>
|
||||
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
|
||||
+71
-12
@@ -8,11 +8,18 @@
|
||||
#include <stdint.h>
|
||||
#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;
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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<length;index++)buffer[index]=Xfer(0xFFu);
|
||||
Deselect();
|
||||
return 1u;
|
||||
}
|
||||
uint8_t W25Q64App_EraseSector(uint32_t address)
|
||||
{
|
||||
WriteEnable(); Select(); Xfer(W25_CMD_ERASE_4K);
|
||||
Xfer((uint8_t)(address>>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<length;index++)Xfer(data[index]);
|
||||
Deselect();
|
||||
return Ready();
|
||||
}
|
||||
uint8_t W25Q64App_ClearUpgradeResult(void)
|
||||
{
|
||||
uint8_t zero=0u;
|
||||
WriteEnable(); Select(); Xfer(W25_CMD_PROGRAM); Xfer(0x03u); Xfer(0x00u); Xfer(0x10u); Xfer(zero); Deselect();
|
||||
return Ready();
|
||||
}
|
||||
+10
-3
@@ -18,6 +18,8 @@
|
||||
#include "nonvolatile_config.h"
|
||||
#include "bluetooth_manager.h"
|
||||
#include "watchdog.h"
|
||||
#include "interrupt_control.h"
|
||||
#include "ota_manager.h"
|
||||
|
||||
/**
|
||||
* @brief 固件主函数。
|
||||
@@ -32,15 +34,14 @@ int main(void)
|
||||
{
|
||||
Board_Init();
|
||||
SystemTick_Init();
|
||||
__asm
|
||||
rim
|
||||
__endasm;
|
||||
Interrupts_Enable();
|
||||
Uart1_Init();
|
||||
Current_Init();
|
||||
BusinessProtocol_Init();
|
||||
MeshFrame_Init();
|
||||
FaultManager_Init();
|
||||
BluetoothManager_Init();
|
||||
OtaManager_Init();
|
||||
Watchdog_Init();
|
||||
|
||||
while (1)
|
||||
@@ -57,6 +58,12 @@ int main(void)
|
||||
BluetoothManager_Task();
|
||||
}
|
||||
FaultManager_Task();
|
||||
/* 蓝牙完成初始化后再上报Bootloader升级结果,避免结果帧丢在AT阶段。 */
|
||||
if (BluetoothManager_IsReady() != 0u)
|
||||
{
|
||||
OtaManager_Task();
|
||||
}
|
||||
Board_StatusLedTask(OtaManager_IsActive());
|
||||
Watchdog_Refresh();
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
* 2. 若不是 ROLE2,发送 ROLE2 和 RESET;
|
||||
* 3. 再次确认 ROLE;
|
||||
* 4. 查询 MADDR,保存 2 字节短地址到 RAM;
|
||||
* 5. 初始化完成后由 PC3 决定 LED3 常亮或 1 秒闪烁。
|
||||
* 5. 初始化完成后由 PC3 决定 PB5 蓝牙状态灯常亮或 1 秒闪烁。
|
||||
*
|
||||
* 初始化过程不使用阻塞延时。UART 返回内容只识别关键字符和超时,具体
|
||||
* AT 返回格式仍应在首次联调时用串口工具记录并校准。
|
||||
|
||||
@@ -11,14 +11,15 @@
|
||||
#include "mesh_frame.h"
|
||||
#include "fault_manager.h"
|
||||
#include "bluetooth_manager.h"
|
||||
#include "ota_manager.h"
|
||||
|
||||
/** 业务帧解析状态。 */
|
||||
typedef enum
|
||||
{
|
||||
/** 等待业务帧头第一个字节 0x55。 */
|
||||
PARSER_WAIT_HEAD_0 = 0,
|
||||
/** 已收到 0x55,等待帧头第二个字节 0xAA。 */
|
||||
PARSER_WAIT_HEAD_1,
|
||||
/** 等待发送方短地址高字节。 */
|
||||
PARSER_WAIT_SOURCE_HIGH = 0,
|
||||
/** 等待发送方短地址低字节。 */
|
||||
PARSER_WAIT_SOURCE_LOW,
|
||||
/** 等待命令字节。 */
|
||||
PARSER_WAIT_CMD,
|
||||
/** 等待 DATA 长度字节。 */
|
||||
@@ -35,6 +36,8 @@ typedef enum
|
||||
static BusinessParserState parser_state;
|
||||
/** 当前正在接收的命令。 */
|
||||
static uint8_t parser_command;
|
||||
/** 当前正在接收的发送方 Mesh 短地址。 */
|
||||
static uint16_t parser_source_address;
|
||||
/** 当前正在接收的数据长度。 */
|
||||
static uint8_t parser_length;
|
||||
/** 当前已经接收的数据字节数。 */
|
||||
@@ -43,10 +46,12 @@ static uint8_t parser_data_index;
|
||||
static uint8_t parser_data[BUSINESS_MAX_DATA_LENGTH];
|
||||
/** 当前正在接收的 CRC。 */
|
||||
static uint16_t parser_received_crc;
|
||||
/** CRC 计算临时区,依次保存 CMD、LEN 和 DATA。 */
|
||||
static uint8_t parser_crc_input[2u + BUSINESS_MAX_DATA_LENGTH];
|
||||
/** CRC 计算临时区,依次保存 SRC、CMD、LEN 和 DATA。 */
|
||||
static uint8_t parser_crc_input[4u + BUSINESS_MAX_DATA_LENGTH];
|
||||
/** 最近一帧合法业务帧的内容。 */
|
||||
static uint8_t last_command;
|
||||
/** 最近一帧合法业务帧的发送方短地址。 */
|
||||
static uint16_t last_source_address;
|
||||
/** 最近一帧合法业务帧的 DATA 长度。 */
|
||||
static uint8_t last_length;
|
||||
/** 最近一帧合法业务帧的 DATA 内容。 */
|
||||
@@ -63,7 +68,7 @@ static void BusinessProtocol_SendByteResponse(uint16_t address,
|
||||
/** 初始化业务协议状态机;完整解析器在第二阶段实现。 */
|
||||
void BusinessProtocol_Init(void)
|
||||
{
|
||||
parser_state = PARSER_WAIT_HEAD_0;
|
||||
parser_state = PARSER_WAIT_SOURCE_HIGH;
|
||||
parser_command = 0u;
|
||||
parser_length = 0u;
|
||||
parser_data_index = 0u;
|
||||
@@ -88,9 +93,9 @@ void BusinessProtocol_Task(void)
|
||||
/**
|
||||
* 向业务帧状态机输入一个字节。
|
||||
*
|
||||
* 帧格式为 `55 AA CMD LEN DATA CRC16`。CRC 计算范围为 CMD、LEN 和
|
||||
* DATA;线上 CRC 低字节在前。CRC 错误会恢复到等待帧头状态,不执行
|
||||
* 任何控制动作。
|
||||
* 帧格式为 `SRC_H SRC_L CMD LEN DATA CRC16`。CRC 计算范围为 SRC、CMD、
|
||||
* LEN 和 DATA;线上 CRC 低字节在前。CRC 错误会恢复到等待地址状态,
|
||||
* 不执行任何控制动作。
|
||||
*/
|
||||
uint8_t BusinessProtocol_InputByte(uint8_t value)
|
||||
{
|
||||
@@ -99,16 +104,14 @@ uint8_t BusinessProtocol_InputByte(uint8_t value)
|
||||
|
||||
switch (parser_state)
|
||||
{
|
||||
case PARSER_WAIT_HEAD_0:
|
||||
if (value == BUSINESS_FRAME_HEAD_0)
|
||||
{
|
||||
parser_state = PARSER_WAIT_HEAD_1;
|
||||
}
|
||||
case PARSER_WAIT_SOURCE_HIGH:
|
||||
parser_source_address = (uint16_t)value << 8u;
|
||||
parser_state = PARSER_WAIT_SOURCE_LOW;
|
||||
break;
|
||||
|
||||
case PARSER_WAIT_HEAD_1:
|
||||
parser_state = (value == BUSINESS_FRAME_HEAD_1) ?
|
||||
PARSER_WAIT_CMD : PARSER_WAIT_HEAD_0;
|
||||
case PARSER_WAIT_SOURCE_LOW:
|
||||
parser_source_address |= value;
|
||||
parser_state = PARSER_WAIT_CMD;
|
||||
break;
|
||||
|
||||
case PARSER_WAIT_CMD:
|
||||
@@ -121,7 +124,7 @@ uint8_t BusinessProtocol_InputByte(uint8_t value)
|
||||
parser_data_index = 0u;
|
||||
if (parser_length > 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)
|
||||
{
|
||||
|
||||
+30
-28
@@ -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 <stdint.h>
|
||||
#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));
|
||||
|
||||
@@ -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(offset<ota_image_size){
|
||||
chunk=(uint16_t)(ota_image_size-offset); if(chunk>16u)chunk=16u;
|
||||
if(W25Q64App_Read(OTA_IMAGE_ADDRESS+offset,buffer,chunk)==0u)return 0u;
|
||||
{
|
||||
uint16_t index;
|
||||
for(index=0u;index<chunk;index++)
|
||||
{
|
||||
uint8_t bit; crc^=buffer[index];
|
||||
for(bit=0u;bit<8u;bit++)
|
||||
crc=(crc&1u)?(uint16_t)((crc>>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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user