feat: 完善Mesh继电器OTA升级闭环
This commit is contained in:
@@ -9,6 +9,8 @@
|
||||
|
||||
/** 初始化 Bootloader 所需的最小硬件资源。 */
|
||||
void Bootloader_Init(void);
|
||||
/** 刷新独立看门狗,供W25Q64和Flash等待循环调用。 */
|
||||
void Bootloader_WatchdogRefresh(void);
|
||||
/** 判断应用程序区域是否包含可执行程序。 */
|
||||
uint8_t Bootloader_IsApplicationValid(void);
|
||||
/** 跳转到应用程序入口地址,函数成功后不返回。 */
|
||||
|
||||
@@ -19,14 +19,14 @@
|
||||
#define APPLICATION_MAX_SIZE_BYTES \
|
||||
(APPLICATION_END_ADDRESS - APPLICATION_START_ADDRESS + 1u)
|
||||
|
||||
/** W25Q64 中保存出厂固件镜像的固定地址,容量64KiB。 */
|
||||
/** W25Q80BV中保存出厂固件镜像的固定地址,预留8KiB。 */
|
||||
#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
|
||||
/** W25Q80BV中保存出厂固件元数据的固定地址,独占4KiB扇区。 */
|
||||
#define FACTORY_METADATA_ADDRESS 0x002000ul
|
||||
/** W25Q80BV中保存OTA升级固件的固定地址,预留8KiB。 */
|
||||
#define OTA_IMAGE_ADDRESS 0x003000ul
|
||||
/** W25Q80BV中保存OTA元数据的固定地址,独占4KiB扇区。 */
|
||||
#define OTA_METADATA_ADDRESS 0x005000ul
|
||||
/** 升级元数据魔数,用于避免误把普通数据当成升级请求。 */
|
||||
#define OTA_METADATA_MAGIC 0x4F544131ul
|
||||
/** 出厂恢复镜像元数据魔数。 */
|
||||
@@ -53,5 +53,7 @@
|
||||
#define FACTORY_BUTTON_HOLD_TIME_MS 5000u
|
||||
/** Bootloader忙等待1ms的循环次数,需结合实际系统时钟校准。 */
|
||||
#define BOOTLOADER_DELAY_1MS_LOOPS 1200u
|
||||
/** 内部Flash单次擦写等待的最大循环次数,超时即判定操作失败。 */
|
||||
#define BOOTLOADER_FLASH_WAIT_TIMEOUT 60000ul
|
||||
|
||||
#endif
|
||||
|
||||
@@ -54,6 +54,13 @@
|
||||
#define GPIOC_DDR REG8(0x500Cu)
|
||||
#define GPIOC_CR1 REG8(0x500Du)
|
||||
|
||||
/* GPIO Port D寄存器;PD4用于W25Q64片选CS#。 */
|
||||
#define GPIOD_ODR REG8(0x500Fu)
|
||||
#define GPIOD_IDR REG8(0x5010u)
|
||||
#define GPIOD_DDR REG8(0x5011u)
|
||||
#define GPIOD_CR1 REG8(0x5012u)
|
||||
#define GPIOD_CR2 REG8(0x5013u)
|
||||
|
||||
#define SPI_CR1_SPE 0x40u
|
||||
#define SPI_CR1_MSTR 0x04u
|
||||
#define SPI_CR1_BR0 0x08u
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
/**
|
||||
* @file w25q64.h
|
||||
* @brief W25Q64JV 硬件SPI访问接口。
|
||||
* @brief W25Q80BV 硬件SPI访问接口。
|
||||
*/
|
||||
#ifndef STM8_BOOTLOADER_W25Q64_H
|
||||
#define STM8_BOOTLOADER_W25Q64_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
/** 初始化STM8硬件SPI和W25Q64片选。 */
|
||||
/** 初始化STM8硬件SPI和W25Q64片选;当前片选引脚为PD4。 */
|
||||
void W25Q64_Init(void);
|
||||
/** 读取W25Q64 JEDEC ID,便于启动时确认芯片存在。 */
|
||||
uint32_t W25Q64_ReadJedecId(void);
|
||||
|
||||
@@ -14,14 +14,14 @@
|
||||
|
||||
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;
|
||||
uint32_t magic; /* 用于识别元数据类型的固定魔数。 */
|
||||
uint32_t image_size; /* 固件裸二进制长度,业务格式为高字节在前。 */
|
||||
uint16_t version; /* 固件版本号,业务格式为高字节在前。 */
|
||||
uint16_t image_crc16; /* 固件完整镜像CRC16,业务格式为高字节在前。 */
|
||||
uint8_t image_valid; /* 镜像已经完整写入并通过校验的标志。 */
|
||||
uint8_t request; /* 非零表示Bootloader需要处理该镜像。 */
|
||||
uint8_t result; /* 最近一次搬运结果码。 */
|
||||
uint8_t reserved; /* 预留字段,保持元数据固定长度。 */
|
||||
} BootloaderMetadata;
|
||||
|
||||
/** 出厂镜像元数据与普通升级元数据使用相同布局,便于复用校验流程。 */
|
||||
@@ -36,6 +36,43 @@ typedef BootloaderMetadata FactoryMetadata;
|
||||
#define OTA_RESULT_W25Q64_ERROR 0x06u
|
||||
#define FACTORY_BUTTON_MASK 0x08u
|
||||
|
||||
/**
|
||||
* @brief 从W25Q64读取并解析一份镜像元数据。
|
||||
*
|
||||
* 元数据中的固件长度、版本和CRC16属于跨程序边界的数据,统一采用
|
||||
* 高字节在前的存储格式,不能直接把Flash字节流强制转换成C结构体。
|
||||
* 魔数保留现有出厂布局的字节顺序,以兼容已经生成的镜像元数据;其余
|
||||
* 数值字段严格按照 bootloader_config.h 中定义的固定偏移显式解析。
|
||||
*
|
||||
* @param address 元数据所在的W25Q64起始地址。
|
||||
* @param metadata 输出的解析结果,不能为NULL。
|
||||
*/
|
||||
static void Bootloader_ReadMetadata(uint32_t address,
|
||||
BootloaderMetadata *metadata)
|
||||
{
|
||||
uint8_t raw[18];
|
||||
|
||||
W25Q64_Read(address, raw, (uint16_t)sizeof(raw));
|
||||
|
||||
/* 魔数沿用当前镜像生成格式:Flash中按低字节到高字节保存。 */
|
||||
metadata->magic = (uint32_t)raw[0] |
|
||||
((uint32_t)raw[1] << 8u) |
|
||||
((uint32_t)raw[2] << 16u) |
|
||||
((uint32_t)raw[3] << 24u);
|
||||
|
||||
/* 长度、版本和CRC16统一按协议规定的高字节在前解析。 */
|
||||
metadata->image_size = ((uint32_t)raw[4] << 24u) |
|
||||
((uint32_t)raw[5] << 16u) |
|
||||
((uint32_t)raw[6] << 8u) |
|
||||
(uint32_t)raw[7];
|
||||
metadata->version = (uint16_t)(((uint16_t)raw[8] << 8u) | raw[9]);
|
||||
metadata->image_crc16 = (uint16_t)(((uint16_t)raw[10] << 8u) | raw[11]);
|
||||
metadata->image_valid = raw[14];
|
||||
metadata->request = raw[15];
|
||||
metadata->result = raw[16];
|
||||
metadata->reserved = raw[17];
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 初始化独立看门狗。
|
||||
*
|
||||
@@ -55,7 +92,7 @@ static void Bootloader_WatchdogInit(void)
|
||||
/**
|
||||
* @brief 刷新独立看门狗。
|
||||
*/
|
||||
static void Bootloader_WatchdogRefresh(void)
|
||||
void Bootloader_WatchdogRefresh(void)
|
||||
{
|
||||
IWDG_KR = 0xAAu;
|
||||
}
|
||||
@@ -74,6 +111,7 @@ static uint8_t Bootloader_ReadFlashByte(uint16_t address)
|
||||
* @brief 初始化上电恢复按键PA3。
|
||||
*
|
||||
* 按键另一端接地,因此PA3采用内部上拉:高电平表示释放,低电平表示按下。
|
||||
* W25Q64片选使用PD4,与本按键互不复用。
|
||||
*/
|
||||
static void Bootloader_ButtonInit(void)
|
||||
{
|
||||
@@ -120,6 +158,17 @@ static uint8_t Bootloader_IsFactoryButtonHeld(void)
|
||||
return 1u;
|
||||
}
|
||||
|
||||
/**
|
||||
* 按CRC-16/MODBUS规则处理一段固件数据。
|
||||
*
|
||||
* 该函数支持分段计算:调用者可以把上一次返回的CRC作为下一次调用的
|
||||
* 初始值,从而不需要把整个固件一次性装入RAM。
|
||||
*
|
||||
* @param data 当前数据片段。
|
||||
* @param length 当前数据片段长度。
|
||||
* @param crc 当前累计CRC;第一段应传入0xFFFF。
|
||||
* @return 加入当前数据片段后的累计CRC值。
|
||||
*/
|
||||
static uint16_t Bootloader_Crc16(const uint8_t *data, uint16_t length,
|
||||
uint16_t crc)
|
||||
{
|
||||
@@ -137,9 +186,20 @@ static uint16_t Bootloader_Crc16(const uint8_t *data, uint16_t length,
|
||||
return crc;
|
||||
}
|
||||
|
||||
/**
|
||||
* 向STM8内部程序Flash写入一个字节并回读校验。
|
||||
*
|
||||
* 函数首次调用时解锁程序Flash;每次写入都等待EOP完成标志,并在等待
|
||||
* 期间刷新看门狗。超过超时阈值后返回失败,由上层决定是否恢复或复位。
|
||||
*
|
||||
* @param address 内部Flash写入地址。
|
||||
* @param value 要写入的字节。
|
||||
* @return 1表示写入并回读一致,0表示超时或回读不一致。
|
||||
*/
|
||||
static uint8_t Bootloader_ApplicationWriteByte(uint16_t address,
|
||||
uint8_t value)
|
||||
{
|
||||
uint32_t timeout = BOOTLOADER_FLASH_WAIT_TIMEOUT;
|
||||
/* 程序Flash解锁序列必须在第一次擦写前完成。 */
|
||||
static uint8_t unlocked;
|
||||
if (unlocked == 0u)
|
||||
@@ -151,10 +211,25 @@ static uint8_t Bootloader_ApplicationWriteByte(uint16_t address,
|
||||
FLASH_CR2 = FLASH_CR2_PRG;
|
||||
FLASH_NCR2 = (uint8_t)~FLASH_CR2_PRG;
|
||||
*((volatile uint8_t *)address) = value;
|
||||
while ((FLASH_IAPSR & FLASH_IAPSR_EOP) == 0u) {}
|
||||
while ((FLASH_IAPSR & FLASH_IAPSR_EOP) == 0u)
|
||||
{
|
||||
Bootloader_WatchdogRefresh();
|
||||
if (timeout-- == 0ul)
|
||||
{
|
||||
return 0u;
|
||||
}
|
||||
}
|
||||
return (*((volatile uint8_t *)address) == value) ? 1u : 0u;
|
||||
}
|
||||
|
||||
/**
|
||||
* 擦除整个应用程序区域。
|
||||
*
|
||||
* STM8S003F3P6程序Flash按64字节块擦除。每个块都单独等待完成并设置
|
||||
* 超时,任意一块失败都会立即终止,防止把不完整擦除状态当作成功升级。
|
||||
*
|
||||
* @return 1表示应用区全部擦除完成,0表示某个擦除块超时。
|
||||
*/
|
||||
static uint8_t Bootloader_EraseApplication(void)
|
||||
{
|
||||
uint16_t address;
|
||||
@@ -171,15 +246,34 @@ static uint8_t Bootloader_EraseApplication(void)
|
||||
/* STM8S003程序Flash按64字节块擦除;擦除前先把整片应用区设为0xFF。 */
|
||||
for (page = 0u; page < APPLICATION_MAX_SIZE_BYTES; page += 64u)
|
||||
{
|
||||
uint32_t timeout = BOOTLOADER_FLASH_WAIT_TIMEOUT;
|
||||
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) {}
|
||||
while ((FLASH_IAPSR & FLASH_IAPSR_EOP) == 0u)
|
||||
{
|
||||
Bootloader_WatchdogRefresh();
|
||||
if (timeout-- == 0ul)
|
||||
{
|
||||
return 0u;
|
||||
}
|
||||
}
|
||||
}
|
||||
return 1u;
|
||||
}
|
||||
|
||||
/**
|
||||
* 分段读取并校验外部Flash中的完整固件镜像。
|
||||
*
|
||||
* 每次只读取16字节,避免占用超过STM8 RAM能力的连续缓冲区;校验通过后
|
||||
* 才允许擦除内部应用区。
|
||||
*
|
||||
* @param image_address 外部Flash镜像起始地址。
|
||||
* @param image_size 镜像长度,不能超过内部应用区容量。
|
||||
* @param expected_crc16 元数据中保存的期望CRC16。
|
||||
* @return 1表示镜像长度合法且CRC匹配,0表示校验失败。
|
||||
*/
|
||||
static uint8_t Bootloader_ValidateImage(uint32_t image_address,
|
||||
uint32_t image_size,
|
||||
uint16_t expected_crc16)
|
||||
@@ -204,6 +298,16 @@ static uint8_t Bootloader_ValidateImage(uint32_t image_address,
|
||||
return (crc == expected_crc16) ? 1u : 0u;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将外部Flash中的镜像搬运到STM8内部应用区。
|
||||
*
|
||||
* 搬运前先擦除应用区,随后按16字节读取外部Flash并逐字节写入内部
|
||||
* Flash。单字节写入函数负责等待、超时和回读校验。
|
||||
*
|
||||
* @param image_address 外部Flash镜像起始地址。
|
||||
* @param image_size 镜像长度。
|
||||
* @return 1表示全部写入成功,0表示擦除或写入失败。
|
||||
*/
|
||||
static uint8_t Bootloader_WriteImage(uint32_t image_address,
|
||||
uint32_t image_size)
|
||||
{
|
||||
@@ -234,6 +338,18 @@ static uint8_t Bootloader_WriteImage(uint32_t image_address,
|
||||
return 1u;
|
||||
}
|
||||
|
||||
/**
|
||||
* 完成一次完整的镜像校验、搬运和结果记录流程。
|
||||
*
|
||||
* 先校验、后擦除是安全关键:如果外部镜像损坏,必须保留当前内部应用,
|
||||
* 不能因为升级请求而先把可运行程序擦掉。
|
||||
*
|
||||
* @param image_address 外部Flash镜像起始地址。
|
||||
* @param image_size 镜像长度。
|
||||
* @param expected_crc16 期望的完整镜像CRC16。
|
||||
* @param write_result 非零时把升级结果写回OTA元数据。
|
||||
* @return 1表示流程成功,0表示校验、搬运或结果记录失败。
|
||||
*/
|
||||
static uint8_t Bootloader_CopyImage(uint32_t image_address,
|
||||
uint32_t image_size,
|
||||
uint16_t expected_crc16,
|
||||
@@ -278,11 +394,18 @@ void Bootloader_Init(void)
|
||||
W25Q64_Init();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查OTA元数据并处理一次普通升级请求。
|
||||
*
|
||||
* 只有魔数、镜像有效标志和升级请求标志同时正确时才会进入搬运流程;
|
||||
* 长度非法时直接记录尺寸错误,不擦除内部应用区。
|
||||
*
|
||||
* @return 1表示升级镜像已经成功搬运并清除请求,0表示没有请求或处理失败。
|
||||
*/
|
||||
uint8_t Bootloader_ProcessUpgrade(void)
|
||||
{
|
||||
BootloaderMetadata metadata;
|
||||
W25Q64_Read(OTA_METADATA_ADDRESS, (uint8_t *)&metadata,
|
||||
(uint16_t)sizeof(metadata));
|
||||
Bootloader_ReadMetadata(OTA_METADATA_ADDRESS, &metadata);
|
||||
if (metadata.magic != OTA_METADATA_MAGIC ||
|
||||
metadata.image_valid != METADATA_IMAGE_VALID_VALUE ||
|
||||
metadata.request == 0u)
|
||||
@@ -306,8 +429,7 @@ uint8_t Bootloader_ProcessUpgrade(void)
|
||||
static uint8_t Bootloader_ProcessFactoryRecovery(void)
|
||||
{
|
||||
FactoryMetadata metadata;
|
||||
W25Q64_Read(FACTORY_METADATA_ADDRESS, (uint8_t *)&metadata,
|
||||
(uint16_t)sizeof(metadata));
|
||||
Bootloader_ReadMetadata(FACTORY_METADATA_ADDRESS, &metadata);
|
||||
if (metadata.magic != FACTORY_METADATA_MAGIC ||
|
||||
metadata.image_valid != METADATA_IMAGE_VALID_VALUE ||
|
||||
metadata.request == 0u ||
|
||||
@@ -389,8 +511,7 @@ void Bootloader_Task(void)
|
||||
upgrade_processed = 1u;
|
||||
{
|
||||
BootloaderMetadata metadata;
|
||||
W25Q64_Read(OTA_METADATA_ADDRESS, (uint8_t *)&metadata,
|
||||
(uint16_t)sizeof(metadata));
|
||||
Bootloader_ReadMetadata(OTA_METADATA_ADDRESS, &metadata);
|
||||
upgrade_requested = (metadata.magic == OTA_METADATA_MAGIC &&
|
||||
metadata.image_valid == METADATA_IMAGE_VALID_VALUE &&
|
||||
metadata.request != 0u) ? 1u : 0u;
|
||||
@@ -406,6 +527,10 @@ void Bootloader_Task(void)
|
||||
{
|
||||
Bootloader_JumpToApplication();
|
||||
}
|
||||
/* 升级和出厂恢复都失败时,不再喂狗,交给看门狗复位重新尝试。 */
|
||||
while (1)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,34 +1,44 @@
|
||||
/**
|
||||
* @file w25q64.c
|
||||
* @brief W25Q64JV 最小硬件SPI驱动。
|
||||
* @brief W25Q80BV 最小硬件SPI驱动。
|
||||
*
|
||||
* 引脚固定为:PC5=SCK、PC6=MOSI、PC7=MISO、PA3=CS#。
|
||||
* 引脚固定为:PC5=SCK、PC6=MOSI、PC7=MISO、PD4=CS#。
|
||||
* 本驱动只使用读取路径,因为Bootloader只读取主程序已经下载并校验好的
|
||||
* 固件;擦除和写入W25Q64属于Application职责。
|
||||
*/
|
||||
#include "w25q64.h"
|
||||
#include "bootloader.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
|
||||
#define FLASH_CS_MASK 0x10u /* PD4,低电平选中W25Q64。 */
|
||||
#define W25Q64_CMD_JEDEC_ID 0x9Fu /* 读取厂商、存储器类型和容量标识。 */
|
||||
#define W25Q64_CMD_READ_DATA 0x03u /* 普通连续读取命令。 */
|
||||
#define W25Q64_CMD_WRITE_ENABLE 0x06u /* 允许后续擦除或写入。 */
|
||||
#define W25Q64_CMD_READ_STATUS1 0x05u /* 读取状态寄存器1。 */
|
||||
#define W25Q64_CMD_PAGE_PROGRAM 0x02u /* 页编程命令。 */
|
||||
#define W25Q64_CMD_SECTOR_ERASE 0x20u /* 擦除4KB扇区。 */
|
||||
#define W25Q64_STATUS_BUSY 0x01u /* 状态寄存器BUSY位。 */
|
||||
|
||||
/** 将W25Q64片选拉低,开始一次SPI事务。 */
|
||||
static void W25Q64_Select(void)
|
||||
{
|
||||
GPIOA_ODR &= (uint8_t)~FLASH_CS_MASK;
|
||||
GPIOD_ODR &= (uint8_t)~FLASH_CS_MASK;
|
||||
}
|
||||
|
||||
/** 将W25Q64片选拉高,结束一次SPI事务并释放总线。 */
|
||||
static void W25Q64_Deselect(void)
|
||||
{
|
||||
GPIOA_ODR |= FLASH_CS_MASK;
|
||||
GPIOD_ODR |= FLASH_CS_MASK;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过SPI发送一个字节并同时读取返回字节。
|
||||
*
|
||||
* SPI是全双工接口,发送期间必须等待发送缓冲区可写,发送完成后还要
|
||||
* 等待接收缓冲区出现一个字节。对于只写命令,返回值会被忽略;对于读取
|
||||
* 命令,返回值就是W25Q64输出的数据。
|
||||
*/
|
||||
static uint8_t W25Q64_Transfer(uint8_t value)
|
||||
{
|
||||
while ((SPI1_SR & SPI_SR_TXE) == 0u) {}
|
||||
@@ -37,6 +47,7 @@ static uint8_t W25Q64_Transfer(uint8_t value)
|
||||
return SPI1_DR;
|
||||
}
|
||||
|
||||
/** 向W25Q64发送写使能命令,为擦除或页编程准备写入许可。 */
|
||||
static void W25Q64_WriteEnable(void)
|
||||
{
|
||||
W25Q64_Select();
|
||||
@@ -44,6 +55,7 @@ static void W25Q64_WriteEnable(void)
|
||||
W25Q64_Deselect();
|
||||
}
|
||||
|
||||
/** 读取状态寄存器1,用于判断W25Q64是否仍处于擦写忙状态。 */
|
||||
static uint8_t W25Q64_ReadStatus1(void)
|
||||
{
|
||||
uint8_t status;
|
||||
@@ -54,11 +66,19 @@ static uint8_t W25Q64_ReadStatus1(void)
|
||||
return status;
|
||||
}
|
||||
|
||||
/**
|
||||
* 等待W25Q64完成内部擦除或编程。
|
||||
*
|
||||
* 正常情况下持续刷新Bootloader看门狗;超过最大轮询次数则认为外部
|
||||
* Flash或SPI通信异常,返回失败,避免无限等待。
|
||||
*/
|
||||
static uint8_t W25Q64_WaitReady(void)
|
||||
{
|
||||
uint32_t timeout = 200000ul;
|
||||
while ((W25Q64_ReadStatus1() & W25Q64_STATUS_BUSY) != 0u)
|
||||
{
|
||||
/* W25Q64正常擦写可能持续较长时间,但异常时必须由超时退出。 */
|
||||
Bootloader_WatchdogRefresh();
|
||||
if (timeout-- == 0ul)
|
||||
{
|
||||
return 0u;
|
||||
@@ -67,13 +87,19 @@ static uint8_t W25Q64_WaitReady(void)
|
||||
return 1u;
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化Bootloader使用的硬件SPI和W25Q64片选GPIO。
|
||||
*
|
||||
* SPI采用主机模式、模式0、MSB first,并使用较低初始时钟,降低上电阶段
|
||||
* 外部Flash尚未稳定时出现通信错误的概率。
|
||||
*/
|
||||
void W25Q64_Init(void)
|
||||
{
|
||||
/* PC5、PC6配置为推挽输出;PC7配置为输入;PA3配置为CS#输出。 */
|
||||
/* PC5、PC6配置为推挽输出;PC7配置为输入;PD4配置为CS#输出。 */
|
||||
GPIOC_DDR |= 0x60u;
|
||||
GPIOC_CR1 |= 0x60u;
|
||||
GPIOA_DDR |= FLASH_CS_MASK;
|
||||
GPIOA_CR1 |= FLASH_CS_MASK;
|
||||
GPIOD_DDR |= FLASH_CS_MASK;
|
||||
GPIOD_CR1 |= FLASH_CS_MASK;
|
||||
W25Q64_Deselect();
|
||||
|
||||
/* SPI主机、MSB first、模式0、低速启动,避免上电阶段时序过快。 */
|
||||
@@ -82,6 +108,10 @@ void W25Q64_Init(void)
|
||||
SPI1_CR2 = (uint8_t)(SPI_CR2_SSM | SPI_CR2_SSI);
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取W25Q64 JEDEC ID。
|
||||
* @return 由厂商ID、存储器类型和容量ID组成的24位标识;当前仅供诊断使用。
|
||||
*/
|
||||
uint32_t W25Q64_ReadJedecId(void)
|
||||
{
|
||||
uint32_t id;
|
||||
@@ -94,6 +124,12 @@ uint32_t W25Q64_ReadJedecId(void)
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从W25Q64连续读取数据。
|
||||
* @param address 24位Flash起始地址,SPI线上按高字节到低字节发送。
|
||||
* @param buffer 接收数据的RAM缓冲区。
|
||||
* @param length 需要读取的字节数。
|
||||
*/
|
||||
void W25Q64_Read(uint32_t address, uint8_t *buffer, uint16_t length)
|
||||
{
|
||||
uint16_t index;
|
||||
@@ -109,6 +145,11 @@ void W25Q64_Read(uint32_t address, uint8_t *buffer, uint16_t length)
|
||||
W25Q64_Deselect();
|
||||
}
|
||||
|
||||
/**
|
||||
* 擦除包含指定地址的4KB扇区。
|
||||
* @param address 扇区内任意地址,W25Q64会按扇区边界执行擦除。
|
||||
* @return 1表示擦除完成,0表示等待超时。
|
||||
*/
|
||||
uint8_t W25Q64_EraseSector(uint32_t address)
|
||||
{
|
||||
W25Q64_WriteEnable();
|
||||
@@ -121,6 +162,17 @@ uint8_t W25Q64_EraseSector(uint32_t address)
|
||||
return W25Q64_WaitReady();
|
||||
}
|
||||
|
||||
/**
|
||||
* 在单个256字节页内编程数据。
|
||||
*
|
||||
* 调用者必须保证数据不跨页;跨页写入会被拒绝,避免W25Q64在页边界
|
||||
* 回卷覆盖页首数据。
|
||||
*
|
||||
* @param address 页内起始地址。
|
||||
* @param data 待写入数据。
|
||||
* @param length 写入长度,范围为1至256字节。
|
||||
* @return 1表示写入完成,0表示参数非法或W25Q64等待超时。
|
||||
*/
|
||||
uint8_t W25Q64_PageProgram(uint32_t address, const uint8_t *data,
|
||||
uint16_t length)
|
||||
{
|
||||
@@ -144,6 +196,7 @@ uint8_t W25Q64_PageProgram(uint32_t address, const uint8_t *data,
|
||||
return W25Q64_WaitReady();
|
||||
}
|
||||
|
||||
/** 清零OTA元数据中的升级请求标志,防止同一镜像重复搬运。 */
|
||||
uint8_t W25Q64_ClearUpgradeRequest(void)
|
||||
{
|
||||
uint8_t cleared = 0u;
|
||||
@@ -152,6 +205,7 @@ uint8_t W25Q64_ClearUpgradeRequest(void)
|
||||
&cleared, 1u);
|
||||
}
|
||||
|
||||
/** 写入OTA升级结果码,供主程序上电后读取并向主机上报。 */
|
||||
uint8_t W25Q64_WriteUpgradeResult(uint8_t result)
|
||||
{
|
||||
return W25Q64_PageProgram(OTA_METADATA_ADDRESS +
|
||||
|
||||
+99
-31
@@ -54,7 +54,7 @@ F1 DD 09 00 0A | SRC_H SRC_L | BUSINESS | 0D 0A
|
||||
JDY-28M完整Mesh帧最大20字节。当前业务帧最大DATA长度为4字节:
|
||||
|
||||
```text
|
||||
BUSINESS = SRC_H SRC_L CMD LEN DATA CRC16_L CRC16_H
|
||||
BUSINESS = SRC_H SRC_L CMD LEN DATA CRC16_H CRC16_L
|
||||
```
|
||||
|
||||
业务帧长度为`6 + LEN`,完整外层发送帧长度为`16 + LEN`,因此`LEN`最大为4。
|
||||
@@ -62,7 +62,7 @@ BUSINESS = SRC_H SRC_L CMD LEN DATA CRC16_L CRC16_H
|
||||
## 2. 业务帧格式
|
||||
|
||||
```text
|
||||
SRC_H SRC_L | CMD | LEN | DATA[0..LEN-1] | CRC16_L CRC16_H
|
||||
SRC_H SRC_L | CMD | LEN | DATA[0..LEN-1] | CRC16_H CRC16_L
|
||||
```
|
||||
|
||||
### 2.1 CRC16规则
|
||||
@@ -74,7 +74,7 @@ SRC_H SRC_L | CMD | LEN | DATA[0..LEN-1] | CRC16_L CRC16_H
|
||||
输入反射:是
|
||||
输出反射:是
|
||||
计算范围:SRC_H、SRC_L、CMD、LEN和DATA
|
||||
发送顺序:CRC低字节在前,高字节在后
|
||||
发送顺序:CRC高字节在前,低字节在后
|
||||
```
|
||||
|
||||
CRC错误时设备直接丢弃整帧,不执行命令,也不返回错误应答。
|
||||
@@ -87,6 +87,11 @@ CRC错误时设备直接丢弃整帧,不执行命令,也不返回错误应
|
||||
应答CMD = 请求CMD | 0x80
|
||||
```
|
||||
|
||||
当请求通过 Mesh 广播地址 `FFFF` 发送时,设备不会立即同时回复。设备会
|
||||
根据本机短地址和系统时间生成约20~170ms、按10ms分档的随机延时,然后向业务帧中的
|
||||
发送方 `SRC_H SRC_L` 单播回复,以降低多个设备同时应答造成的网络拥塞。
|
||||
单播请求仍然直接回复。
|
||||
|
||||
错误应答:
|
||||
|
||||
```text
|
||||
@@ -104,6 +109,54 @@ DATA[0] = 错误码
|
||||
0x06:未知命令
|
||||
```
|
||||
|
||||
## 3.1 `0x07` 设备上线广播
|
||||
|
||||
设备在本次上电完成基础初始化、蓝牙进入可通信状态后,向 Mesh 广播一次:
|
||||
|
||||
```text
|
||||
CMD = 07
|
||||
LEN = 00
|
||||
```
|
||||
|
||||
发送方短地址位于业务帧的 `SRC_H SRC_L` 字段中,因此该命令不需要 DATA,
|
||||
其他设备收到后不回复。主机或其他设备可据此更新在线状态;如果后续长期
|
||||
未收到该设备的上线或业务消息,可以结合应用层超时判断设备是否离线。
|
||||
|
||||
## 3.2 `0x10` 固件版本发现广播
|
||||
|
||||
主机发现服务器存在新固件后,向Mesh广播最新版本和固件长度。该命令只用于
|
||||
设备发现,不会直接启动OTA。
|
||||
|
||||
```text
|
||||
CMD = 10
|
||||
LEN = 04
|
||||
DATA[0] = 最新版本高字节
|
||||
DATA[1] = 最新版本低字节
|
||||
DATA[2] = 固件长度高字节
|
||||
DATA[3] = 固件长度低字节
|
||||
```
|
||||
|
||||
设备收到后比较:
|
||||
|
||||
```text
|
||||
最新版本 > 当前版本:随机延时后回复
|
||||
最新版本 <= 当前版本:静默丢弃,不回复
|
||||
```
|
||||
|
||||
需要升级的设备返回:
|
||||
|
||||
```text
|
||||
CMD = 90
|
||||
LEN = 04
|
||||
DATA[0] = 当前版本高字节
|
||||
DATA[1] = 当前版本低字节
|
||||
DATA[2] = 需要升级的版本高字节
|
||||
DATA[3] = 需要升级的版本低字节
|
||||
```
|
||||
|
||||
设备短地址位于响应业务帧的`SRC_H SRC_L`字段中。主机收到多个`0x90`后,
|
||||
生成待升级设备列表,由人工选择设备,再对选中的短地址执行后续OTA流程。
|
||||
|
||||
## 4. 主机下发的普通命令
|
||||
|
||||
### 4.1 `0x01` 继电器控制
|
||||
@@ -120,7 +173,7 @@ DATA[0] = 01:继电器吸合
|
||||
业务帧结构示例:
|
||||
|
||||
```text
|
||||
SRC_H SRC_L 01 01 01 CRC16_L CRC16_H
|
||||
SRC_H SRC_L 01 01 01 CRC16_H CRC16_L
|
||||
```
|
||||
|
||||
设备执行后返回:
|
||||
@@ -170,14 +223,14 @@ LEN = 00
|
||||
```text
|
||||
CMD = 83
|
||||
LEN = 02
|
||||
DATA[0] = 电流低字节
|
||||
DATA[1] = 电流高字节
|
||||
DATA[0] = 电流高字节
|
||||
DATA[1] = 电流低字节
|
||||
```
|
||||
|
||||
单位为mA,无符号整数,低字节在前。例如实际电流为`1000mA = 0x03E8`:
|
||||
单位为mA,无符号整数,高字节在前。例如实际电流为`1000mA = 0x03E8`:
|
||||
|
||||
```text
|
||||
DATA = E8 03
|
||||
DATA = 03 E8
|
||||
```
|
||||
|
||||
### 4.4 `0x05` 查询设备短地址
|
||||
@@ -211,14 +264,14 @@ DATA = 00 0C
|
||||
```text
|
||||
CMD = 06
|
||||
LEN = 02
|
||||
DATA[0] = 阈值低字节
|
||||
DATA[1] = 阈值高字节
|
||||
DATA[0] = 阈值高字节
|
||||
DATA[1] = 阈值低字节
|
||||
```
|
||||
|
||||
单位为mA,默认值为100mA,设备保存到内部EEPROM。例如设置100mA:
|
||||
|
||||
```text
|
||||
DATA = 64 00
|
||||
DATA = 00 64
|
||||
```
|
||||
|
||||
设备成功返回:
|
||||
@@ -245,8 +298,8 @@ LEN = 00
|
||||
```text
|
||||
CMD = 8E
|
||||
LEN = 02
|
||||
DATA[0] = 版本低字节
|
||||
DATA[1] = 版本高字节
|
||||
DATA[0] = 版本高字节
|
||||
DATA[1] = 版本低字节
|
||||
```
|
||||
|
||||
当前版本在主程序`include/app_config.h`中定义:
|
||||
@@ -303,17 +356,17 @@ DATA[0] = 被确认的故障码或恢复码
|
||||
```text
|
||||
CMD = 08
|
||||
LEN = 04
|
||||
DATA[0] = 固件长度低字节
|
||||
DATA[1] = 固件长度高字节
|
||||
DATA[2] = 版本号低字节
|
||||
DATA[3] = 版本号高字节
|
||||
DATA[0] = 固件长度高字节
|
||||
DATA[1] = 固件长度低字节
|
||||
DATA[2] = 版本号高字节
|
||||
DATA[3] = 版本号低字节
|
||||
```
|
||||
|
||||
设备收到后擦除:
|
||||
|
||||
```text
|
||||
OTA固件区:0x020000~0x02FFFF
|
||||
OTA元数据:0x030000~0x030FFF
|
||||
OTA固件区:0x003000~0x004FFF
|
||||
OTA元数据:0x005000~0x005FFF
|
||||
```
|
||||
|
||||
成功应答:
|
||||
@@ -364,8 +417,8 @@ DATA[2] = 下一期待偏移低字节
|
||||
```text
|
||||
CMD = 0A
|
||||
LEN = 02
|
||||
DATA[0] = 完整固件CRC16低字节
|
||||
DATA[1] = 完整固件CRC16高字节
|
||||
DATA[0] = 完整固件CRC16高字节
|
||||
DATA[1] = 完整固件CRC16低字节
|
||||
```
|
||||
|
||||
CRC16使用CRC-16/MODBUS,计算范围是整个裸二进制固件文件。
|
||||
@@ -390,12 +443,12 @@ LEN = 00
|
||||
设备从W25Q64重新读取完整固件并计算CRC16。校验成功后写入OTA元数据:
|
||||
|
||||
```text
|
||||
0x030000:魔数
|
||||
0x030004:固件长度
|
||||
0x030008:固件版本
|
||||
0x03000A:CRC16
|
||||
0x03000E:镜像有效标志0xA5
|
||||
0x03000F:升级请求0xA5
|
||||
0x005000:魔数
|
||||
0x005004:固件长度
|
||||
0x005008:固件版本
|
||||
0x00500A:CRC16
|
||||
0x00500E:镜像有效标志0xA5
|
||||
0x00500F:升级请求0xA5
|
||||
```
|
||||
|
||||
设备返回:
|
||||
@@ -407,7 +460,10 @@ DATA[0] = 00:校验成功,已提交升级
|
||||
DATA[0] = 01:校验失败
|
||||
```
|
||||
|
||||
之后设备复位,由Bootloader把OTA固件搬运到内部Flash的`0x8800`区域。
|
||||
当`DATA[0]=00`时,设备已经完成外部Flash固件校验并写入升级元数据。
|
||||
设备会先发送`0x8B`应答,随后自动触发MCU复位,由Bootloader把OTA固件
|
||||
搬运到内部Flash的`0x8800`区域。`0x8B`不代表Bootloader已经搬运完成,
|
||||
主机必须继续等待设备重新启动后主动上报的`0x0D`。
|
||||
|
||||
### 7.5 `0x0C` 取消OTA
|
||||
|
||||
@@ -437,14 +493,24 @@ DATA[0] = 00:取消成功
|
||||
发送完整CRC16:0A
|
||||
请求校验提交:0B
|
||||
等待校验结果:8B
|
||||
设备复位并升级
|
||||
设备发送8B后自动复位并升级
|
||||
主程序启动后主动上报升级结果:0D
|
||||
主机确认升级结果:8D
|
||||
```
|
||||
|
||||
主机等待最终升级结果的建议规则:
|
||||
|
||||
```text
|
||||
收到 0x8B=00 后,继续等待 0x0D,默认超时时间为20秒;
|
||||
收到 0x0D 且 DATA[0]=0x01:升级成功;
|
||||
收到 0x0D 且 DATA[0]不是0x01:升级失败;
|
||||
超过20秒没有收到0x0D:升级失败;
|
||||
收到升级结果后,主机向该设备短地址发送0x8D确认。
|
||||
```
|
||||
|
||||
## 8. OTA升级结果上报
|
||||
|
||||
设备启动后从`0x030010`读取升级结果,蓝牙初始化成功后主动广播:
|
||||
设备启动后从`0x005010`读取升级结果,蓝牙初始化成功后主动广播:
|
||||
|
||||
```text
|
||||
CMD = 0D
|
||||
@@ -472,4 +538,6 @@ LEN = 01
|
||||
DATA[0] = 对应的升级结果码
|
||||
```
|
||||
|
||||
设备收到确认后清除`0x030010`。
|
||||
设备收到确认后清除`0x005010`。
|
||||
如果主机没有发送`0x8D`,设备不会清除结果记录,后续启动时可能再次上报,
|
||||
便于主机发现升级结果确认丢失。
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# STM8 Mesh 继电器 OTA、Flash 分区与业务协议设计
|
||||
|
||||
本文档记录当前已经确认的外部 W25Q64 存储规划、Bootloader 启动策略、主程序 OTA 职责以及业务命令规则。
|
||||
本文档记录当前已经确认的外部 W25Q80BV存储规划、Bootloader启动策略、主程序OTA职责以及业务命令规则。
|
||||
|
||||
## 1. 硬件资源与程序分区
|
||||
|
||||
@@ -17,23 +17,23 @@ STM8S003F3P6TR 内部程序 Flash 总容量为 8KiB,地址范围为 `0x8000~0x
|
||||
|
||||
应用程序必须按照 `0x8800` 重新链接,不能使用默认的 `0x8000` 起始地址。
|
||||
|
||||
### 1.2 W25Q64JVSIQ 外部 Flash
|
||||
### 1.2 W25Q80BV 外部 Flash
|
||||
|
||||
W25Q64 总容量为 8MiB。当前固定分区如下:
|
||||
W25Q80BV总容量为128KiB。当前固定分区如下:
|
||||
|
||||
| 地址范围 | 容量 | 用途 |
|
||||
|---|---:|---|
|
||||
| `0x000000~0x00FFFF` | 64KiB | 出厂固件镜像 |
|
||||
| `0x010000~0x010FFF` | 4KiB | 出厂固件元数据 |
|
||||
| `0x020000~0x02FFFF` | 64KiB | OTA升级固件镜像 |
|
||||
| `0x030000~0x030FFF` | 4KiB | OTA升级元数据 |
|
||||
| 其余空间 | 约8056KiB | 后续日志、运行记录、故障记录等 |
|
||||
| `0x000000~0x001FFF` | 8KiB | 出厂固件镜像 |
|
||||
| `0x002000~0x002FFF` | 4KiB | 出厂固件元数据 |
|
||||
| `0x003000~0x004FFF` | 8KiB | OTA升级固件镜像 |
|
||||
| `0x005000~0x005FFF` | 4KiB | OTA升级元数据 |
|
||||
| `0x006000~0x01FFFF` | 104KiB | 后续日志、运行记录、故障记录等 |
|
||||
|
||||
元数据区域均独占一个4KiB扇区,避免擦除元数据时影响固件镜像。
|
||||
|
||||
### 1.3 SPI连接
|
||||
|
||||
| STM8引脚 | W25Q64信号 |
|
||||
| STM8引脚 | W25Q80BV信号 |
|
||||
|---|---|
|
||||
| `PC5` | `SCK` |
|
||||
| `PC6` | `MOSI/DI` |
|
||||
@@ -76,19 +76,19 @@ JDY-28M 控制脚当前分配为:`PA2=PWRC`、`PD2=RESET`。`PA2` 不再作为
|
||||
|
||||
### 2.2 元数据绝对地址
|
||||
|
||||
OTA元数据区域起始地址为 `0x030000`,因此:
|
||||
OTA元数据区域起始地址为 `0x005000`,因此:
|
||||
|
||||
```text
|
||||
魔数: 0x030000~0x030003
|
||||
固件长度: 0x030004~0x030007
|
||||
固件版本: 0x030008~0x030009
|
||||
CRC16: 0x03000A~0x03000B
|
||||
有效标志: 0x03000E
|
||||
升级请求: 0x03000F
|
||||
升级结果: 0x030010
|
||||
魔数: 0x005000~0x005003
|
||||
固件长度: 0x005004~0x005007
|
||||
固件版本: 0x005008~0x005009
|
||||
CRC16: 0x00500A~0x00500B
|
||||
有效标志: 0x00500E
|
||||
升级请求: 0x00500F
|
||||
升级结果: 0x005010
|
||||
```
|
||||
|
||||
出厂元数据区域起始地址为 `0x010000`,字段偏移相同,因此出厂镜像升级结果地址为 `0x010010`。
|
||||
出厂元数据区域起始地址为 `0x002000`,字段偏移相同,因此出厂镜像升级结果地址为 `0x002010`。
|
||||
|
||||
### 2.3 魔数
|
||||
|
||||
@@ -128,7 +128,7 @@ Bootloader负责:
|
||||
- 读取OTA元数据;
|
||||
- 校验OTA固件CRC16/MODBUS;
|
||||
- 擦除STM8内部应用区;
|
||||
- 从W25Q64搬运固件到内部Flash;
|
||||
- 从W25Q80BV搬运固件到内部Flash;
|
||||
- 回读校验内部Flash;
|
||||
- 写入升级结果;
|
||||
- 清除升级请求;
|
||||
@@ -139,7 +139,7 @@ Bootloader负责:
|
||||
升级安全顺序必须是:
|
||||
|
||||
```text
|
||||
先完整校验W25Q64固件
|
||||
先完整校验W25Q80BV固件
|
||||
↓
|
||||
CRC正确
|
||||
↓
|
||||
@@ -165,7 +165,7 @@ CRC正确
|
||||
- 内部Flash写入后校验失败;
|
||||
- 应用程序损坏导致无法正常启动。
|
||||
|
||||
出厂镜像必须预先写入W25Q64,并且出厂元数据中的镜像有效标志和CRC16必须正确。
|
||||
出厂镜像必须预先写入W25Q80BV,并且出厂元数据中的镜像有效标志和CRC16必须正确。
|
||||
|
||||
## 4. PA3上电恢复按键
|
||||
|
||||
@@ -199,7 +199,7 @@ CRC正确
|
||||
- 通过JDY-28M接收OTA业务帧;
|
||||
- 校验业务帧CRC16/MODBUS;
|
||||
- 擦除OTA固件区域;
|
||||
- 将固件分包写入 `0x020000`;
|
||||
- 将固件分包写入 `0x003000`;
|
||||
- 计算完整固件CRC16/MODBUS;
|
||||
- 写入OTA元数据;
|
||||
- 设置有效标志和升级请求;
|
||||
@@ -288,7 +288,7 @@ OTA内部控制命令建议保留:
|
||||
0x03:内部Flash写入失败
|
||||
0x04:写入后校验失败
|
||||
0x05:固件长度非法
|
||||
0x06:W25Q64读取失败
|
||||
0x06:W25Q80BV读取失败
|
||||
```
|
||||
|
||||
主程序启动后读取升级结果,并使用:
|
||||
@@ -300,3 +300,29 @@ DATA = 升级结果码
|
||||
```
|
||||
|
||||
发送给主机。主机确认后,主程序清除结果字段。
|
||||
|
||||
### 8.3 主机侧最终确认流程
|
||||
|
||||
`CMD=0x8B`仅表示应用程序已经完成外部Flash固件的整包CRC16校验,并且
|
||||
已经成功写入OTA元数据。校验成功后,设备先发送`0x8B=00`,再主动触发
|
||||
MCU复位;Bootloader随后负责把OTA镜像搬运到内部Flash。
|
||||
|
||||
主机不能在收到`0x8B=00`后立即认为升级完成,而应继续等待设备重新启动并
|
||||
主动上报:
|
||||
|
||||
```text
|
||||
CMD = 0x0D
|
||||
LEN = 0x01
|
||||
DATA[0] = 0x01:Bootloader搬运并校验成功
|
||||
```
|
||||
|
||||
主机默认等待20秒。超时未收到`0x0D`,或者收到的结果码不是`0x01`,均应
|
||||
判定本次升级失败。收到结果后,主机向上报帧的设备短地址发送:
|
||||
|
||||
```text
|
||||
CMD = 0x8D
|
||||
LEN = 0x01
|
||||
DATA[0] = 原样返回设备上报的升级结果码
|
||||
```
|
||||
|
||||
设备收到`0x8D`后清除待上报结果记录。
|
||||
|
||||
Binary file not shown.
@@ -16,9 +16,6 @@ void BluetoothManager_Task(void);
|
||||
/** 返回蓝牙初始化是否成功。 */
|
||||
uint8_t BluetoothManager_IsReady(void);
|
||||
|
||||
/** 返回蓝牙初始化故障状态。 */
|
||||
uint8_t BluetoothManager_IsFault(void);
|
||||
|
||||
/** 返回最近一次查询到的短地址。 */
|
||||
uint16_t BluetoothManager_GetShortAddress(void);
|
||||
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
* @file business_protocol.h
|
||||
* @brief STM8 业务帧协议接口。
|
||||
*
|
||||
* 业务帧格式:SRC_H SRC_L | CMD | LEN | DATA | CRC16_L CRC16_H。
|
||||
* 业务帧格式:SRC_H SRC_L | CMD | LEN | DATA | CRC16_H CRC16_L。
|
||||
* SRC 为发送方的 Mesh 短地址;CRC 使用 CRC-16/MODBUS,计算范围为
|
||||
* SRC、CMD、LEN 和 DATA,线上低字节在前。
|
||||
* SRC、CMD、LEN 和 DATA,线上高字节在前。
|
||||
*/
|
||||
#ifndef BUSINESS_PROTOCOL_H
|
||||
#define BUSINESS_PROTOCOL_H
|
||||
@@ -35,6 +35,16 @@
|
||||
/** 业务命令:电流阈值配置。 */
|
||||
#define CMD_THRESHOLD_CONFIG 0x06u
|
||||
|
||||
/** 设备上线广播通知;DATA长度为0,不需要应答。 */
|
||||
#define CMD_DEVICE_ONLINE 0x07u
|
||||
|
||||
/** 主机广播可用固件版本和长度;DATA长度为4,不直接启动OTA。 */
|
||||
#define CMD_FIRMWARE_ANNOUNCE 0x10u
|
||||
|
||||
/** 设备发现自己需要升级后的响应命令;DATA返回当前版本和目标版本。 */
|
||||
#define CMD_FIRMWARE_ANNOUNCE_RESPONSE \
|
||||
(CMD_FIRMWARE_ANNOUNCE | CMD_RESPONSE_FLAG)
|
||||
|
||||
/** 查询当前主程序固件版本。 */
|
||||
#define CMD_FIRMWARE_VERSION_QUERY 0x0Eu
|
||||
|
||||
@@ -69,6 +79,12 @@ void BusinessProtocol_Init(void);
|
||||
/** 处理 UART 输入和待发送业务帧。 */
|
||||
void BusinessProtocol_Task(void);
|
||||
|
||||
/** 强制将业务应答放入随机延时队列,供异步广播应答使用。 */
|
||||
void BusinessProtocol_QueueDelayedResponse(uint16_t address,
|
||||
uint8_t command,
|
||||
const uint8_t *data,
|
||||
uint8_t length);
|
||||
|
||||
/**
|
||||
* 向解析器输入一个字节。
|
||||
*
|
||||
@@ -87,9 +103,6 @@ uint8_t BusinessProtocol_GetCommand(void);
|
||||
/** 读取最近一帧合法业务帧的数据长度。 */
|
||||
uint8_t BusinessProtocol_GetDataLength(void);
|
||||
|
||||
/** 读取最近一帧合法业务帧的数据区;返回 0 表示下标越界。 */
|
||||
uint8_t BusinessProtocol_GetDataByte(uint8_t index, uint8_t *value);
|
||||
|
||||
/** 读取最近一帧合法业务帧携带的发送方 Mesh 短地址。 */
|
||||
uint16_t BusinessProtocol_GetSourceAddress(void);
|
||||
|
||||
|
||||
@@ -18,7 +18,8 @@ void OtaManager_Task(void);
|
||||
/** 处理一条已经通过 Mesh 外层解析的 OTA 业务命令。 */
|
||||
uint8_t OtaManager_HandleCommand(uint8_t command,
|
||||
const uint8_t *data,
|
||||
uint8_t length);
|
||||
uint8_t length,
|
||||
uint16_t response_target);
|
||||
/** 返回当前是否处于 OTA 下载模式。 */
|
||||
uint8_t OtaManager_IsActive(void);
|
||||
uint8_t OtaManager_GetExpectedOffset(uint16_t *offset);
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
/** 初始化系统节拍定时器。 */
|
||||
void SystemTick_Init(void);
|
||||
|
||||
/** 返回系统启动后经过的毫秒数。 */
|
||||
/** 返回系统启动后经过的毫秒数;约49.7天回绕一次。 */
|
||||
uint32_t SystemTick_NowMs(void);
|
||||
|
||||
/** 在定时器中断服务程序中递增系统节拍。 */
|
||||
|
||||
+2
-2
@@ -7,10 +7,10 @@
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
/** 初始化 UART1。具体波特率和寄存器配置在底层阶段接入。 */
|
||||
/** 初始化 UART1,并开启接收中断和环形缓冲区。 */
|
||||
void Uart1_Init(void);
|
||||
|
||||
/** 轮询 UART1 硬件状态并把收到的字节放入环形缓冲区。 */
|
||||
/** UART任务入口;接收硬件由中断处理,此接口保留为空操作。 */
|
||||
void Uart1_Task(void);
|
||||
|
||||
/** 向 UART1 发送一个字节。 */
|
||||
|
||||
@@ -8,4 +8,12 @@ void Watchdog_Init(void);
|
||||
/** 在主循环中刷新看门狗计数器。 */
|
||||
void Watchdog_Refresh(void);
|
||||
|
||||
/**
|
||||
* 立即触发一次 MCU 看门狗复位。
|
||||
*
|
||||
* 该接口用于 OTA 校验成功后的受控重启:调用方必须先完成升级结果应答,
|
||||
* 然后本函数把看门狗重装值缩短并停止喂狗,交由硬件自动复位进入Bootloader。
|
||||
*/
|
||||
void Watchdog_ResetNow(void);
|
||||
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
#!/usr/bin/env python3
|
||||
"""生成写入W25Q80BV的出厂固件合并镜像。
|
||||
|
||||
合并镜像布局如下:
|
||||
0x000000:主程序裸二进制固件
|
||||
0x002000:出厂固件元数据
|
||||
|
||||
脚本会自动读取主程序BIN、计算CRC-16/MODBUS,并把版本号写入输出文件名
|
||||
和出厂元数据。CH341A打开生成的文件后,从W25Q80BV的0x000000地址开始烧录。
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
import re
|
||||
|
||||
|
||||
# 主程序Makefile生成的裸二进制文件,已经去除了Bootloader地址空间。
|
||||
FIRMWARE_PATH = Path("build/mesh_relay.bin")
|
||||
|
||||
# 主程序版本定义文件,脚本会自动读取其中的FIRMWARE_VERSION宏。
|
||||
VERSION_HEADER_PATH = Path("include/app_config.h")
|
||||
|
||||
# 出厂固件元数据在W25Q80BV中的固定地址。
|
||||
FACTORY_METADATA_ADDRESS = 0x002000
|
||||
|
||||
|
||||
def read_firmware_version() -> int:
|
||||
"""从主程序配置头文件读取FIRMWARE_VERSION宏。"""
|
||||
|
||||
if not VERSION_HEADER_PATH.is_file():
|
||||
raise SystemExit(f"找不到版本配置文件:{VERSION_HEADER_PATH}")
|
||||
|
||||
content = VERSION_HEADER_PATH.read_text(encoding="utf-8")
|
||||
match = re.search(
|
||||
r"^\s*#define\s+FIRMWARE_VERSION\s+((?:0[xX][0-9A-Fa-f]+)|(?:[0-9]+))",
|
||||
content,
|
||||
flags=re.MULTILINE,
|
||||
)
|
||||
if match is None:
|
||||
raise SystemExit(
|
||||
f"在{VERSION_HEADER_PATH}中找不到FIRMWARE_VERSION宏定义"
|
||||
)
|
||||
|
||||
version = int(match.group(1), 0)
|
||||
if not 0 <= version <= 0xFFFF:
|
||||
raise SystemExit(f"FIRMWARE_VERSION超出16位范围:{version}")
|
||||
return version
|
||||
|
||||
|
||||
def calculate_crc16_modbus(data: bytes) -> int:
|
||||
"""计算一段数据的CRC-16/MODBUS校验值。
|
||||
|
||||
Args:
|
||||
data: 需要计算CRC的完整固件数据。
|
||||
|
||||
Returns:
|
||||
16位CRC结果。发送和存储时由调用方按高字节、低字节写入。
|
||||
"""
|
||||
|
||||
crc = 0xFFFF
|
||||
for value in data:
|
||||
crc ^= value
|
||||
for _ in range(8):
|
||||
if crc & 1:
|
||||
crc = (crc >> 1) ^ 0xA001
|
||||
else:
|
||||
crc >>= 1
|
||||
return crc & 0xFFFF
|
||||
|
||||
|
||||
def build_factory_metadata(firmware: bytes, crc16: int, version: int) -> bytes:
|
||||
"""按照Bootloader固定布局生成18字节出厂元数据。"""
|
||||
|
||||
metadata = bytearray(18)
|
||||
|
||||
# FACTORY_METADATA_MAGIC=0x46414331。
|
||||
# 当前Bootloader兼容的魔数字节布局为31 43 41 46。
|
||||
metadata[0:4] = bytes([0x31, 0x43, 0x41, 0x46])
|
||||
|
||||
# 固件长度、版本号和CRC16统一使用高字节在前。
|
||||
metadata[4:8] = len(firmware).to_bytes(4, "big")
|
||||
metadata[8:10] = version.to_bytes(2, "big")
|
||||
metadata[10:12] = crc16.to_bytes(2, "big")
|
||||
|
||||
# 0x0C~0x0D为保留字段,保持0x00。
|
||||
metadata[14] = 0xA5 # 镜像有效标志。
|
||||
metadata[15] = 0x01 # 请求Bootloader处理出厂镜像。
|
||||
metadata[16] = 0x00 # 初始升级结果。
|
||||
metadata[17] = 0x00 # 保留字段。
|
||||
return bytes(metadata)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""读取固件、生成出厂镜像并输出烧录文件信息。"""
|
||||
|
||||
if not FIRMWARE_PATH.is_file():
|
||||
raise SystemExit(
|
||||
f"找不到固件文件:{FIRMWARE_PATH}\n"
|
||||
"请先在项目根目录执行 make。"
|
||||
)
|
||||
|
||||
firmware = FIRMWARE_PATH.read_bytes()
|
||||
firmware_version = read_firmware_version()
|
||||
crc16 = calculate_crc16_modbus(firmware)
|
||||
metadata = build_factory_metadata(firmware, crc16, firmware_version)
|
||||
|
||||
# 输出文件名包含4位十六进制版本号,例如factory_flash_v0100.bin。
|
||||
output_path = Path(f"factory_flash_v{firmware_version:04X}.bin")
|
||||
|
||||
# 文件覆盖到元数据末尾,空白区域填充0xFF,符合NOR Flash擦除状态。
|
||||
image = bytearray([0xFF] * (FACTORY_METADATA_ADDRESS + len(metadata)))
|
||||
image[0:len(firmware)] = firmware
|
||||
image[FACTORY_METADATA_ADDRESS:] = metadata
|
||||
output_path.write_bytes(image)
|
||||
|
||||
print(f"固件长度:{len(firmware)} 字节")
|
||||
print(f"固件版本:0x{firmware_version:04X}")
|
||||
print(f"固件CRC16:0x{crc16:04X}")
|
||||
print(f"输出文件:{output_path}")
|
||||
print(f"输出大小:{len(image)} 字节")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -12,12 +12,14 @@ static uint16_t current_milliamperes;
|
||||
static uint8_t current_ready;
|
||||
/** 启动校准得到的 ADC 零点码值。 */
|
||||
static uint16_t zero_code;
|
||||
/** 启动校准期间用于累加 ADC 码值的 32 位累加器。 */
|
||||
static uint32_t zero_accumulator;
|
||||
/** 启动校准期间用于累加 ADC 码值的16位累加器。
|
||||
* 64个样本的最大和为64×1023=65472,不超过uint16_t范围。 */
|
||||
static uint16_t zero_accumulator;
|
||||
/** 已经完成的零点校准样本数。 */
|
||||
static uint8_t zero_count;
|
||||
/** 正常运行期间用于累加偏差样本的累加器。 */
|
||||
static uint32_t signal_accumulator;
|
||||
/** 正常运行期间用于累加偏差样本的16位累加器。
|
||||
* 16个样本的最大和为16×1023=16368,不超过uint16_t范围。 */
|
||||
static uint16_t signal_accumulator;
|
||||
/** 当前平均窗口已经积累的偏差样本数。 */
|
||||
static uint8_t signal_count;
|
||||
|
||||
@@ -57,9 +59,9 @@ void Current_Init(void)
|
||||
current_milliamperes = 0u;
|
||||
current_ready = 0u;
|
||||
zero_code = 512u;
|
||||
zero_accumulator = 0ul;
|
||||
zero_accumulator = 0u;
|
||||
zero_count = 0u;
|
||||
signal_accumulator = 0ul;
|
||||
signal_accumulator = 0u;
|
||||
signal_count = 0u;
|
||||
|
||||
/* ADC1 单次转换,右对齐,选择 PC4 对应的 ADC 通道由后续封装核对。 */
|
||||
@@ -92,7 +94,7 @@ void Current_Task(void)
|
||||
++zero_count;
|
||||
if (zero_count == 64u)
|
||||
{
|
||||
zero_code = (uint16_t)(zero_accumulator / 64ul);
|
||||
zero_code = (uint16_t)(zero_accumulator / 64u);
|
||||
current_ready = 1u;
|
||||
}
|
||||
return;
|
||||
@@ -104,11 +106,11 @@ void Current_Task(void)
|
||||
++signal_count;
|
||||
if (signal_count >= 16u)
|
||||
{
|
||||
difference = (uint16_t)(signal_accumulator / 16ul);
|
||||
difference = (uint16_t)(signal_accumulator / 16u);
|
||||
milliamperes = ((uint32_t)difference * 977ul + 5ul) / 10ul;
|
||||
current_milliamperes = (milliamperes > 65535ul) ? 65535u :
|
||||
(uint16_t)milliamperes;
|
||||
signal_accumulator = 0ul;
|
||||
signal_accumulator = 0u;
|
||||
signal_count = 0u;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
#include "system_tick.h"
|
||||
#include "stm8s003_registers.h"
|
||||
|
||||
/** TIM4 中断递增的全局毫秒计数,溢出后按无符号差值继续工作。 */
|
||||
/** TIM4 中断递增的32位毫秒计数,溢出后按无符号差值继续工作。 */
|
||||
static volatile uint32_t system_tick_ms;
|
||||
|
||||
/**
|
||||
|
||||
+50
-23
@@ -16,7 +16,24 @@ static volatile uint8_t rx_write_index;
|
||||
/** 环形缓存读取位置,由上层协议任务推进。 */
|
||||
static volatile uint8_t rx_read_index;
|
||||
|
||||
/** 初始化 UART1;后续接入 JDY-28M 的 9600 baud 配置。 */
|
||||
/**
|
||||
* 将一个字节写入接收环形缓冲区。
|
||||
*
|
||||
* 该函数只由 UART1 接收中断调用。缓冲区满时丢弃新字节,绝不覆盖尚未
|
||||
* 被主循环读取的数据,避免破坏正在解析的 Mesh 帧。
|
||||
*/
|
||||
static void Uart1_RxPush(uint8_t value)
|
||||
{
|
||||
uint8_t next_index = (uint8_t)((rx_write_index + 1u) &
|
||||
(UART1_RX_BUFFER_SIZE - 1u));
|
||||
if (next_index != rx_read_index)
|
||||
{
|
||||
rx_buffer[rx_write_index] = value;
|
||||
rx_write_index = next_index;
|
||||
}
|
||||
}
|
||||
|
||||
/** 初始化 UART1,并开启 UART1 接收寄存器非空中断。 */
|
||||
void Uart1_Init(void)
|
||||
{
|
||||
/* 先关闭 UART,再配置 9600 baud@16 MHz:BRR=0x0683。 */
|
||||
@@ -25,36 +42,21 @@ void Uart1_Init(void)
|
||||
REG8(UART1_BRR1_ADDR) = 0x68u;
|
||||
REG8(UART1_CR1_ADDR) = 0x00u;
|
||||
REG8(UART1_CR3_ADDR) = 0x00u;
|
||||
/* RE=1、TE=1、接收中断暂不打开,环形缓冲区接入后再开启。 */
|
||||
REG8(UART1_CR2_ADDR) = 0x0Cu;
|
||||
rx_write_index = 0u;
|
||||
rx_read_index = 0u;
|
||||
/* RE=1、TE=1、RIEN=1:接收数据由UART1中断保存到环形缓冲区。 */
|
||||
REG8(UART1_CR2_ADDR) = 0x2Cu;
|
||||
}
|
||||
|
||||
/**
|
||||
* 轮询 UART1 接收状态并保存字节。
|
||||
* UART1接收任务入口。
|
||||
*
|
||||
* 本阶段先采用轮询方式,避免在尚未建立中断向量表前引入中断链接问题。
|
||||
* 后续如果业务帧速率提高,可将同一环形缓冲区写入逻辑迁移到 UART RX 中断。
|
||||
* 接收字节已经由 UART1 接收中断写入环形缓冲区,因此主循环不再轮询UART
|
||||
* 硬件寄存器。保留该接口是为了维持主循环结构和后续扩展接口兼容性。
|
||||
*/
|
||||
void Uart1_Task(void)
|
||||
{
|
||||
uint8_t next_index;
|
||||
|
||||
while ((REG8(UART1_SR_ADDR) & 0x20u) != 0u)
|
||||
{
|
||||
next_index = (uint8_t)((rx_write_index + 1u) & (UART1_RX_BUFFER_SIZE - 1u));
|
||||
|
||||
/* 缓冲区满时丢弃当前字节,避免覆盖尚未处理的完整帧。 */
|
||||
if (next_index == rx_read_index)
|
||||
{
|
||||
(void)REG8(UART1_DR_ADDR);
|
||||
continue;
|
||||
}
|
||||
|
||||
rx_buffer[rx_write_index] = REG8(UART1_DR_ADDR);
|
||||
rx_write_index = next_index;
|
||||
}
|
||||
/* UART硬件接收由Uart1_InterruptHandler()完成,此处无需执行任何操作。 */
|
||||
}
|
||||
|
||||
/** 发送一个字节;当前阶段不连接真实 UART 寄存器。 */
|
||||
@@ -85,7 +87,32 @@ uint8_t Uart1_Available(void)
|
||||
return (uint8_t)((rx_write_index - rx_read_index) & (UART1_RX_BUFFER_SIZE - 1u));
|
||||
}
|
||||
|
||||
/** UART1 接收中断入口占位。 */
|
||||
/**
|
||||
* UART1接收中断处理函数。
|
||||
*
|
||||
* STM8S003F3 UART1接收中断向量为19。中断中只读取状态和数据寄存器,
|
||||
* 将无错误字节快速放入环形缓冲区;协议解析、CRC和业务控制全部放在主循环。
|
||||
*/
|
||||
void Uart1_InterruptHandler(void)
|
||||
{
|
||||
uint8_t status = REG8(UART1_SR_ADDR);
|
||||
uint8_t value;
|
||||
|
||||
if ((status & 0x0Fu) != 0u)
|
||||
{
|
||||
/* STM8 UART错误标志需要先读SR再读DR清除;错误字节不送入协议层。 */
|
||||
value = REG8(UART1_DR_ADDR);
|
||||
(void)value;
|
||||
}
|
||||
else if ((status & 0x20u) != 0u)
|
||||
{
|
||||
value = REG8(UART1_DR_ADDR);
|
||||
Uart1_RxPush(value);
|
||||
}
|
||||
}
|
||||
|
||||
/** UART1接收寄存器非空中断向量,IRQ号19。 */
|
||||
void Uart1_RX_IRQHandler(void) __interrupt(19)
|
||||
{
|
||||
Uart1_InterruptHandler();
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/** @file w25q64_app.c @brief 主程序侧W25Q64只读结果访问驱动。 */
|
||||
/** @file w25q64_app.c @brief 主程序侧W25Q80BV访问驱动。 */
|
||||
#include "w25q64_app.h"
|
||||
#include "stm8s003_registers.h"
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
#define W25_CMD_RDSR 0x05u
|
||||
#define W25_CMD_ERASE_4K 0x20u
|
||||
#define W25_BUSY 0x01u
|
||||
#define W25_RESULT_ADDR 0x030010ul
|
||||
#define W25_RESULT_ADDR 0x005010ul
|
||||
|
||||
static uint8_t Xfer(uint8_t value)
|
||||
{
|
||||
@@ -82,6 +82,10 @@ uint8_t W25Q64App_PageProgram(uint32_t address,const uint8_t *data,
|
||||
uint8_t W25Q64App_ClearUpgradeResult(void)
|
||||
{
|
||||
uint8_t zero=0u;
|
||||
WriteEnable(); Select(); Xfer(W25_CMD_PROGRAM); Xfer(0x03u); Xfer(0x00u); Xfer(0x10u); Xfer(zero); Deselect();
|
||||
WriteEnable(); Select(); Xfer(W25_CMD_PROGRAM);
|
||||
Xfer((uint8_t)(W25_RESULT_ADDR >> 16));
|
||||
Xfer((uint8_t)(W25_RESULT_ADDR >> 8));
|
||||
Xfer((uint8_t)W25_RESULT_ADDR);
|
||||
Xfer(zero); Deselect();
|
||||
return Ready();
|
||||
}
|
||||
|
||||
@@ -22,3 +22,21 @@ void Watchdog_Refresh(void)
|
||||
{
|
||||
REG8(IWDG_KR_ADDR) = 0xAAu;
|
||||
}
|
||||
|
||||
/**
|
||||
* 立即触发看门狗复位。
|
||||
*
|
||||
* UART发送函数在返回前会等待最后一个字节写入发送寄存器,因此OTA校验
|
||||
* 成功分支可以先发送0x8B,再调用本函数。这里把看门狗重装值改为最小值,
|
||||
* 随后不再刷新计数器,避免等待原先约5秒的完整超时时间。
|
||||
*/
|
||||
void Watchdog_ResetNow(void)
|
||||
{
|
||||
REG8(IWDG_KR_ADDR) = 0x55u;
|
||||
REG8(IWDG_RLR_ADDR) = 0x01u;
|
||||
REG8(IWDG_KR_ADDR) = 0xAAu;
|
||||
while (1)
|
||||
{
|
||||
/* 等待硬件看门狗复位;故意不刷新计数器。 */
|
||||
}
|
||||
}
|
||||
|
||||
+19
@@ -21,6 +21,9 @@
|
||||
#include "interrupt_control.h"
|
||||
#include "ota_manager.h"
|
||||
|
||||
/** 标记本次上电是否已经完成设备上线广播,避免主循环重复发送。 */
|
||||
static uint8_t startup_broadcast_sent;
|
||||
|
||||
/**
|
||||
* @brief 固件主函数。
|
||||
*
|
||||
@@ -43,6 +46,7 @@ int main(void)
|
||||
BluetoothManager_Init();
|
||||
OtaManager_Init();
|
||||
Watchdog_Init();
|
||||
startup_broadcast_sent = 0u;
|
||||
|
||||
while (1)
|
||||
{
|
||||
@@ -56,8 +60,23 @@ int main(void)
|
||||
{
|
||||
MeshFrame_Task();
|
||||
BluetoothManager_Task();
|
||||
/* 所有基础模块已完成初始化且蓝牙进入可通信状态后,向Mesh
|
||||
* 广播一次上线通知。业务帧SRC字段会自动携带本机短地址,
|
||||
* 其他设备无需回复,避免广播上线时形成应答风暴。 */
|
||||
if ((startup_broadcast_sent == 0u) &&
|
||||
(BluetoothManager_IsReady() != 0u))
|
||||
{
|
||||
if (MeshFrame_SendBusiness(0xFFFFu,
|
||||
CMD_DEVICE_ONLINE,
|
||||
(const uint8_t *)0,
|
||||
0u) != 0u)
|
||||
{
|
||||
startup_broadcast_sent = 1u;
|
||||
}
|
||||
}
|
||||
}
|
||||
FaultManager_Task();
|
||||
BusinessProtocol_Task();
|
||||
/* 蓝牙完成初始化后再上报Bootloader升级结果,避免结果帧丢在AT阶段。 */
|
||||
if (BluetoothManager_IsReady() != 0u)
|
||||
{
|
||||
|
||||
@@ -286,8 +286,5 @@ void BluetoothManager_Task(void)
|
||||
/** 返回蓝牙初始化完成状态。 */
|
||||
uint8_t BluetoothManager_IsReady(void) { return (bluetooth_state == BT_READY) ? 1u : 0u; }
|
||||
|
||||
/** 返回蓝牙初始化故障状态。 */
|
||||
uint8_t BluetoothManager_IsFault(void) { return (bluetooth_state == BT_FAULT) ? 1u : 0u; }
|
||||
|
||||
/** 返回本次上电从模块查询到的短地址。 */
|
||||
uint16_t BluetoothManager_GetShortAddress(void) { return short_address; }
|
||||
|
||||
@@ -12,6 +12,59 @@
|
||||
#include "fault_manager.h"
|
||||
#include "bluetooth_manager.h"
|
||||
#include "ota_manager.h"
|
||||
#include "system_tick.h"
|
||||
|
||||
/** 广播应答的最小随机延时,避免多个设备同时发送。 */
|
||||
#define BROADCAST_RESPONSE_DELAY_MIN_MS 20u
|
||||
/** 广播应答随机延时的时间片,采用10ms量化以减少计算开销。 */
|
||||
#define BROADCAST_RESPONSE_DELAY_STEP_MS 10u
|
||||
/** 广播应答随机延时的时间片数量,实际范围为20ms至170ms。 */
|
||||
#define BROADCAST_RESPONSE_DELAY_SLOTS 16u
|
||||
|
||||
/** 待延迟发送的广播应答。当前设备一次只处理一条业务请求,因此单条队列足够。 */
|
||||
static uint8_t delayed_response_pending;
|
||||
static uint16_t delayed_response_target;
|
||||
static uint8_t delayed_response_command;
|
||||
static uint8_t delayed_response_length;
|
||||
static uint8_t delayed_response_data[BUSINESS_MAX_DATA_LENGTH];
|
||||
static uint32_t delayed_response_due_ms;
|
||||
static uint8_t response_random_state;
|
||||
|
||||
/** 生成一个轻量伪随机数,用于把广播应答分散到不同时间片。 */
|
||||
static uint8_t BusinessProtocol_NextRandom(void);
|
||||
|
||||
/** 发送或排队发送业务应答;只有广播请求才进行随机延时。 */
|
||||
static void BusinessProtocol_SendResponse(uint16_t address,
|
||||
uint8_t command,
|
||||
const uint8_t *data,
|
||||
uint8_t length);
|
||||
|
||||
/** 将异步应答写入随机延迟队列,不立即发送。 */
|
||||
void BusinessProtocol_QueueDelayedResponse(uint16_t address,
|
||||
uint8_t command,
|
||||
const uint8_t *data,
|
||||
uint8_t length)
|
||||
{
|
||||
uint8_t index;
|
||||
if ((data == (const uint8_t *)0 && length != 0u) ||
|
||||
(length > BUSINESS_MAX_DATA_LENGTH))
|
||||
{
|
||||
return;
|
||||
}
|
||||
delayed_response_target = address;
|
||||
delayed_response_command = command;
|
||||
delayed_response_length = length;
|
||||
for (index = 0u; index < length; ++index)
|
||||
{
|
||||
delayed_response_data[index] = data[index];
|
||||
}
|
||||
delayed_response_due_ms = SystemTick_NowMs() +
|
||||
BROADCAST_RESPONSE_DELAY_MIN_MS +
|
||||
(uint32_t)((BusinessProtocol_NextRandom() &
|
||||
(BROADCAST_RESPONSE_DELAY_SLOTS - 1u)) *
|
||||
BROADCAST_RESPONSE_DELAY_STEP_MS);
|
||||
delayed_response_pending = 1u;
|
||||
}
|
||||
|
||||
/** 业务帧解析状态。 */
|
||||
typedef enum
|
||||
@@ -26,10 +79,10 @@ typedef enum
|
||||
PARSER_WAIT_LENGTH,
|
||||
/** 按 LEN 接收 DATA 区。 */
|
||||
PARSER_WAIT_DATA,
|
||||
/** 等待 CRC 低字节。 */
|
||||
PARSER_WAIT_CRC_LOW,
|
||||
/** 等待 CRC 高字节并完成校验。 */
|
||||
PARSER_WAIT_CRC_HIGH
|
||||
/** 等待 CRC 高字节。 */
|
||||
PARSER_WAIT_CRC_HIGH,
|
||||
/** 等待 CRC 低字节并完成校验。 */
|
||||
PARSER_WAIT_CRC_LOW
|
||||
} BusinessParserState;
|
||||
|
||||
/** 解析器当前状态。 */
|
||||
@@ -62,7 +115,45 @@ static void BusinessProtocol_SendByteResponse(uint16_t address,
|
||||
uint8_t command,
|
||||
uint8_t value)
|
||||
{
|
||||
(void)MeshFrame_SendBusiness(address, command, &value, 1u);
|
||||
BusinessProtocol_SendResponse(address, command, &value, 1u);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成一个8位线性反馈移位寄存器伪随机数。
|
||||
*
|
||||
* 这里只需要把多个设备的广播应答时间打散,不需要密码学随机性。
|
||||
* 使用8位状态和按位与取时间片,避免16位除法和取模代码占用Flash。
|
||||
*/
|
||||
static uint8_t BusinessProtocol_NextRandom(void)
|
||||
{
|
||||
uint8_t feedback = (uint8_t)(((response_random_state >> 7u) ^
|
||||
(response_random_state >> 5u) ^
|
||||
(response_random_state >> 4u) ^
|
||||
(response_random_state >> 3u)) & 1u);
|
||||
response_random_state = (uint8_t)((response_random_state << 1u) | feedback);
|
||||
if (response_random_state == 0u)
|
||||
{
|
||||
response_random_state = 0xA5u;
|
||||
}
|
||||
return response_random_state;
|
||||
}
|
||||
|
||||
/** 发送或排队发送业务应答;只有广播请求才进行随机延时。 */
|
||||
static void BusinessProtocol_SendResponse(uint16_t address,
|
||||
uint8_t command,
|
||||
const uint8_t *data,
|
||||
uint8_t length)
|
||||
{
|
||||
if (MeshFrame_GetLastTargetAddress() != 0xFFFFu)
|
||||
{
|
||||
(void)MeshFrame_SendBusiness(address, command, data, length);
|
||||
return;
|
||||
}
|
||||
if (length > BUSINESS_MAX_DATA_LENGTH)
|
||||
{
|
||||
return;
|
||||
}
|
||||
BusinessProtocol_QueueDelayedResponse(address, command, data, length);
|
||||
}
|
||||
|
||||
/** 初始化业务协议状态机;完整解析器在第二阶段实现。 */
|
||||
@@ -75,18 +166,21 @@ void BusinessProtocol_Init(void)
|
||||
parser_received_crc = 0u;
|
||||
last_command = 0u;
|
||||
last_length = 0u;
|
||||
delayed_response_pending = 0u;
|
||||
response_random_state = 0xA5u;
|
||||
}
|
||||
|
||||
/** 处理 UART 数据和待发送帧;当前阶段仅保留任务入口。 */
|
||||
void BusinessProtocol_Task(void)
|
||||
{
|
||||
uint8_t value;
|
||||
|
||||
while (Uart1_ReadByte(&value) != 0u)
|
||||
if ((delayed_response_pending != 0u) &&
|
||||
((uint32_t)(SystemTick_NowMs() - delayed_response_due_ms) < 0x80000000ul))
|
||||
{
|
||||
/* 当前阶段仅消费 UART 字节,正式接入时由 Mesh 外层解析器调用
|
||||
* BusinessProtocol_InputByte(),避免把外层地址和帧尾误当业务数据。 */
|
||||
(void)BusinessProtocol_InputByte(value);
|
||||
(void)MeshFrame_SendBusiness(delayed_response_target,
|
||||
delayed_response_command,
|
||||
delayed_response_data,
|
||||
delayed_response_length);
|
||||
delayed_response_pending = 0u;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,7 +188,7 @@ void BusinessProtocol_Task(void)
|
||||
* 向业务帧状态机输入一个字节。
|
||||
*
|
||||
* 帧格式为 `SRC_H SRC_L CMD LEN DATA CRC16`。CRC 计算范围为 SRC、CMD、
|
||||
* LEN 和 DATA;线上 CRC 低字节在前。CRC 错误会恢复到等待地址状态,
|
||||
* LEN 和 DATA;线上 CRC 高字节在前。CRC 错误会恢复到等待地址状态,
|
||||
* 不执行任何控制动作。
|
||||
*/
|
||||
uint8_t BusinessProtocol_InputByte(uint8_t value)
|
||||
@@ -128,7 +222,7 @@ uint8_t BusinessProtocol_InputByte(uint8_t value)
|
||||
}
|
||||
else if (parser_length == 0u)
|
||||
{
|
||||
parser_state = PARSER_WAIT_CRC_LOW;
|
||||
parser_state = PARSER_WAIT_CRC_HIGH;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -144,13 +238,13 @@ uint8_t BusinessProtocol_InputByte(uint8_t value)
|
||||
}
|
||||
break;
|
||||
|
||||
case PARSER_WAIT_CRC_LOW:
|
||||
parser_received_crc = value;
|
||||
parser_state = PARSER_WAIT_CRC_HIGH;
|
||||
case PARSER_WAIT_CRC_HIGH:
|
||||
parser_received_crc = (uint16_t)value << 8u;
|
||||
parser_state = PARSER_WAIT_CRC_LOW;
|
||||
break;
|
||||
|
||||
case PARSER_WAIT_CRC_HIGH:
|
||||
parser_received_crc |= (uint16_t)value << 8u;
|
||||
case PARSER_WAIT_CRC_LOW:
|
||||
parser_received_crc |= value;
|
||||
parser_crc_input[0] = (uint8_t)(parser_source_address >> 8u);
|
||||
parser_crc_input[1] = (uint8_t)parser_source_address;
|
||||
parser_crc_input[2] = parser_command;
|
||||
@@ -196,17 +290,6 @@ uint8_t BusinessProtocol_GetDataLength(void)
|
||||
return last_length;
|
||||
}
|
||||
|
||||
/** 读取最近一帧合法业务帧的数据区字节。 */
|
||||
uint8_t BusinessProtocol_GetDataByte(uint8_t index, uint8_t *value)
|
||||
{
|
||||
if ((value == (uint8_t *)0) || (index >= last_length))
|
||||
{
|
||||
return 0u;
|
||||
}
|
||||
*value = last_data[index];
|
||||
return 1u;
|
||||
}
|
||||
|
||||
/** 读取最近一帧合法业务帧携带的发送方 Mesh 短地址。 */
|
||||
uint16_t BusinessProtocol_GetSourceAddress(void)
|
||||
{
|
||||
@@ -264,9 +347,9 @@ void BusinessProtocol_HandleFrame(void)
|
||||
break;
|
||||
}
|
||||
current = Current_GetMilliAmpere();
|
||||
current_data[0] = (uint8_t)current;
|
||||
current_data[1] = (uint8_t)(current >> 8u);
|
||||
(void)MeshFrame_SendBusiness(source_address,
|
||||
current_data[0] = (uint8_t)(current >> 8u);
|
||||
current_data[1] = (uint8_t)current;
|
||||
BusinessProtocol_SendResponse(source_address,
|
||||
CMD_CURRENT_QUERY | CMD_RESPONSE_FLAG,
|
||||
current_data, 2u);
|
||||
break;
|
||||
@@ -279,9 +362,9 @@ void BusinessProtocol_HandleFrame(void)
|
||||
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,
|
||||
current_data[0] = (uint8_t)(FIRMWARE_VERSION >> 8u);
|
||||
current_data[1] = (uint8_t)FIRMWARE_VERSION;
|
||||
BusinessProtocol_SendResponse(source_address,
|
||||
CMD_FIRMWARE_VERSION_RESPONSE,
|
||||
current_data, 2u);
|
||||
break;
|
||||
@@ -308,7 +391,8 @@ void BusinessProtocol_HandleFrame(void)
|
||||
{
|
||||
uint8_t ota_status = OtaManager_HandleCommand(last_command,
|
||||
last_data,
|
||||
last_length) ? 0x00u : 0x01u;
|
||||
last_length,
|
||||
source_address) ? 0x00u : 0x01u;
|
||||
uint8_t ota_response[3u];
|
||||
uint16_t next_offset = 0u;
|
||||
uint8_t response_command = (uint8_t)(last_command | CMD_RESPONSE_FLAG);
|
||||
@@ -318,14 +402,17 @@ void BusinessProtocol_HandleFrame(void)
|
||||
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,
|
||||
BusinessProtocol_SendResponse(source_address, response_command,
|
||||
ota_response, 3u);
|
||||
}
|
||||
else
|
||||
else if (last_command != 0x0Bu)
|
||||
{
|
||||
BusinessProtocol_SendByteResponse(source_address,
|
||||
response_command, ota_status);
|
||||
}
|
||||
/* 0x0B 的完整固件校验已经改为后台分片执行,最终的 0x8B
|
||||
* 应答由 OtaManager_Task() 在校验完成后发送,避免这里提前
|
||||
* 返回“成功”而实际校验尚未完成。 */
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -336,7 +423,7 @@ void BusinessProtocol_HandleFrame(void)
|
||||
BusinessProtocol_SendByteResponse(source_address, CMD_ERROR_RESPONSE, value);
|
||||
break;
|
||||
}
|
||||
threshold = (uint16_t)last_data[0] | ((uint16_t)last_data[1] << 8u);
|
||||
threshold = ((uint16_t)last_data[0] << 8u) | last_data[1];
|
||||
if ((threshold == 0u) || (threshold > 30000u))
|
||||
{
|
||||
value = 0x03u;
|
||||
@@ -349,11 +436,51 @@ void BusinessProtocol_HandleFrame(void)
|
||||
BusinessProtocol_SendByteResponse(source_address, CMD_ERROR_RESPONSE, value);
|
||||
break;
|
||||
}
|
||||
(void)MeshFrame_SendBusiness(source_address,
|
||||
BusinessProtocol_SendResponse(source_address,
|
||||
CMD_THRESHOLD_CONFIG | CMD_RESPONSE_FLAG,
|
||||
last_data, 2u);
|
||||
break;
|
||||
|
||||
case CMD_DEVICE_ONLINE:
|
||||
/* 上线通知是设备间广播事件。发送方短地址已经位于业务帧SRC
|
||||
* 字段中,因此不需要DATA,也不返回应答,避免多个设备同时
|
||||
* 上线时产生广播应答风暴。 */
|
||||
if (last_length != 0u)
|
||||
{
|
||||
value = 0x02u;
|
||||
BusinessProtocol_SendByteResponse(source_address,
|
||||
CMD_ERROR_RESPONSE, value);
|
||||
}
|
||||
break;
|
||||
|
||||
case CMD_FIRMWARE_ANNOUNCE:
|
||||
/* DATA依次为新固件版本和固件长度,均为高字节在前。设备只
|
||||
* 在新版本高于当前版本时回复,避免全网无意义应答。 */
|
||||
if (last_length != 4u)
|
||||
{
|
||||
value = 0x02u;
|
||||
BusinessProtocol_SendByteResponse(source_address,
|
||||
CMD_ERROR_RESPONSE, value);
|
||||
break;
|
||||
}
|
||||
/* 这里直接复用最近一帧的DATA缓冲区生成应答,避免额外申请
|
||||
* 4字节栈空间。先保存主机宣布的目标版本,再覆盖为当前版本。 */
|
||||
if ((last_data[0] > (uint8_t)(FIRMWARE_VERSION >> 8u)) ||
|
||||
((last_data[0] == (uint8_t)(FIRMWARE_VERSION >> 8u)) &&
|
||||
(last_data[1] > (uint8_t)FIRMWARE_VERSION)))
|
||||
{
|
||||
last_data[2] = last_data[0];
|
||||
last_data[3] = last_data[1];
|
||||
last_data[0] = (uint8_t)(FIRMWARE_VERSION >> 8u);
|
||||
last_data[1] = (uint8_t)FIRMWARE_VERSION;
|
||||
/* 当前请求来自广播时,统一通过应答延时队列发送,
|
||||
* 防止多个设备同时回复造成Mesh拥塞。 */
|
||||
BusinessProtocol_SendResponse(source_address,
|
||||
CMD_FIRMWARE_ANNOUNCE_RESPONSE,
|
||||
last_data, 4u);
|
||||
}
|
||||
break;
|
||||
|
||||
case CMD_ADDRESS_QUERY:
|
||||
if (last_length != 0u)
|
||||
{
|
||||
@@ -363,7 +490,7 @@ void BusinessProtocol_HandleFrame(void)
|
||||
}
|
||||
current_data[0] = (uint8_t)(BluetoothManager_GetShortAddress() >> 8u);
|
||||
current_data[1] = (uint8_t)BluetoothManager_GetShortAddress();
|
||||
(void)MeshFrame_SendBusiness(source_address,
|
||||
BusinessProtocol_SendResponse(source_address,
|
||||
CMD_ADDRESS_QUERY | CMD_RESPONSE_FLAG,
|
||||
current_data, 2u);
|
||||
break;
|
||||
@@ -380,7 +507,7 @@ void BusinessProtocol_HandleFrame(void)
|
||||
*
|
||||
* @param data 待计算数据指针,不能为 NULL。
|
||||
* @param length 数据长度。
|
||||
* @return CRC 结果;发送时低字节在前。
|
||||
* @return CRC 结果;发送时高字节在前。
|
||||
*/
|
||||
uint16_t BusinessProtocol_Crc16Modbus(const uint8_t *data, uint16_t length)
|
||||
{
|
||||
|
||||
@@ -175,7 +175,7 @@ void MeshFrame_Task(void)
|
||||
}
|
||||
}
|
||||
|
||||
/** 返回最近一帧合法外层数据的源地址。 */
|
||||
/** 返回最近一帧合法外层数据携带的目标短地址。 */
|
||||
uint16_t MeshFrame_GetLastTargetAddress(void)
|
||||
{
|
||||
return last_target_address;
|
||||
@@ -215,7 +215,7 @@ uint8_t MeshFrame_SendBusiness(uint16_t target_address,
|
||||
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));
|
||||
Uart1_WriteByte((uint8_t)(crc >> 8u)); Uart1_WriteByte((uint8_t)crc);
|
||||
Uart1_WriteByte(MESH_TAIL_0); Uart1_WriteByte(MESH_TAIL_1);
|
||||
return 1u;
|
||||
}
|
||||
|
||||
+155
-53
@@ -4,21 +4,24 @@
|
||||
*
|
||||
* 重要职责边界:
|
||||
* - 本模块属于 Application,不属于 Bootloader;
|
||||
* - 本模块接收 Mesh OTA 数据并写入外部 W25Q64;
|
||||
* - 本模块接收 Mesh OTA 数据并写入外部 W25Q80BV;
|
||||
* - 本模块不擦写 STM8 内部程序 Flash;
|
||||
* - 下载完成并完成整包 CRC16/MODBUS 校验后,才设置升级请求;
|
||||
* - 复位后由 Bootloader 读取升级请求并执行最终搬运。
|
||||
*
|
||||
* 当前代码先建立稳定的状态机和接口边界。W25Q64 的 SPI 底层驱动需要
|
||||
* 当前代码先建立稳定的状态机和接口边界。W25Q80BV 的 SPI 底层驱动需要
|
||||
* 在实际硬件确认片选、SPI模式、时钟和读写时序后接入。
|
||||
*/
|
||||
#include "ota_manager.h"
|
||||
#include "w25q64_app.h"
|
||||
#include "mesh_frame.h"
|
||||
#include "business_protocol.h"
|
||||
#include "watchdog.h"
|
||||
|
||||
#define OTA_IMAGE_ADDRESS 0x020000ul
|
||||
#define OTA_METADATA_ADDRESS 0x030000ul
|
||||
#define OTA_IMAGE_ADDRESS 0x003000ul
|
||||
#define OTA_METADATA_ADDRESS 0x005000ul
|
||||
#define OTA_IMAGE_SECTOR_COUNT 2u
|
||||
#define OTA_IMAGE_MAX_SIZE 0x2000u
|
||||
|
||||
#define OTA_COMMAND_START 0x08u
|
||||
#define OTA_COMMAND_DATA 0x09u
|
||||
@@ -28,25 +31,32 @@
|
||||
#define OTA_STATE_IDLE 0u
|
||||
#define OTA_STATE_RECEIVING 1u
|
||||
#define OTA_STATE_READY 2u
|
||||
#define OTA_STATE_VERIFYING 3u
|
||||
|
||||
/** 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驱动读取后填充。 */
|
||||
/** Bootloader升级结果缓存;实际值由W25Q80BV驱动读取后填充。 */
|
||||
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;
|
||||
/** 当前整包校验已经读取到的外部Flash偏移。 */
|
||||
static uint16_t ota_verify_offset;
|
||||
/** 当前整包校验过程中的连续CRC值。 */
|
||||
static uint16_t ota_verify_crc;
|
||||
/** 整包校验完成后需要回复的主机短地址。 */
|
||||
static uint16_t ota_verify_response_target;
|
||||
/** 标记校验完成后是否需要发送0x8B应答。 */
|
||||
static uint8_t ota_verify_response_pending;
|
||||
/** CMD=0x0B请求是否来自广播,用于决定最终0x8B是否随机延迟。 */
|
||||
static uint8_t ota_verify_broadcast_request;
|
||||
|
||||
/**
|
||||
* @brief 初始化 OTA 状态机。
|
||||
@@ -54,16 +64,20 @@ static uint16_t ota_expected_crc16;
|
||||
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;
|
||||
ota_verify_offset = 0u;
|
||||
ota_verify_crc = 0xFFFFu;
|
||||
ota_verify_response_target = 0u;
|
||||
ota_verify_response_pending = 0u;
|
||||
ota_verify_broadcast_request = 0u;
|
||||
W25Q64App_Init();
|
||||
if (W25Q64App_ReadByte(0x030010ul, &ota_bootloader_result) != 0u &&
|
||||
if (W25Q64App_ReadByte(OTA_METADATA_ADDRESS + 0x10ul,
|
||||
&ota_bootloader_result) != 0u &&
|
||||
ota_bootloader_result != 0u)
|
||||
{
|
||||
ota_bootloader_result_pending = 1u;
|
||||
@@ -78,6 +92,116 @@ void OtaManager_Init(void)
|
||||
void OtaManager_Task(void)
|
||||
{
|
||||
uint8_t result;
|
||||
uint8_t buffer[16];
|
||||
uint16_t chunk;
|
||||
uint16_t index;
|
||||
uint8_t bit;
|
||||
uint8_t metadata[18];
|
||||
|
||||
/* 每次任务只读取并计算最多16字节,计算完成后立即返回主循环。
|
||||
* 这样主循环可以继续刷新看门狗,不会因为大固件整包校验阻塞。 */
|
||||
if (ota_state == OTA_STATE_VERIFYING)
|
||||
{
|
||||
chunk = (uint16_t)(ota_expected_size - ota_verify_offset);
|
||||
if (chunk > 16u)
|
||||
{
|
||||
chunk = 16u;
|
||||
}
|
||||
|
||||
if (W25Q64App_Read(OTA_IMAGE_ADDRESS + ota_verify_offset,
|
||||
buffer, chunk) == 0u)
|
||||
{
|
||||
/* 外部 Flash 读取失败时,必须向发起校验的主机返回失败,
|
||||
* 不能只退出状态机,否则主机将一直等待0x8B应答。 */
|
||||
result = 0x01u;
|
||||
ota_state = OTA_STATE_IDLE;
|
||||
if (ota_verify_response_pending != 0u)
|
||||
{
|
||||
if (ota_verify_broadcast_request != 0u)
|
||||
{
|
||||
BusinessProtocol_QueueDelayedResponse(ota_verify_response_target,
|
||||
0x8Bu, &result, 1u);
|
||||
}
|
||||
else
|
||||
{
|
||||
(void)MeshFrame_SendBusiness(ota_verify_response_target,
|
||||
0x8Bu, &result, 1u);
|
||||
}
|
||||
ota_verify_response_pending = 0u;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
for (index = 0u; index < chunk; ++index)
|
||||
{
|
||||
ota_verify_crc ^= buffer[index];
|
||||
for (bit = 0u; bit < 8u; ++bit)
|
||||
{
|
||||
if ((ota_verify_crc & 1u) != 0u)
|
||||
{
|
||||
ota_verify_crc = (uint16_t)((ota_verify_crc >> 1u) ^ 0xA001u);
|
||||
}
|
||||
else
|
||||
{
|
||||
ota_verify_crc >>= 1u;
|
||||
}
|
||||
}
|
||||
}
|
||||
ota_verify_offset = (uint16_t)(ota_verify_offset + chunk);
|
||||
|
||||
if (ota_verify_offset >= ota_expected_size)
|
||||
{
|
||||
result = (ota_verify_crc == ota_expected_crc16) ? 0x00u : 0x01u;
|
||||
if (result == 0x00u)
|
||||
{
|
||||
metadata[0] = 0x31u; metadata[1] = 0x41u;
|
||||
metadata[2] = 0x54u; metadata[3] = 0x4Fu;
|
||||
metadata[4] = (uint8_t)(ota_expected_size >> 8u);
|
||||
metadata[5] = (uint8_t)ota_expected_size;
|
||||
metadata[6] = 0u; metadata[7] = 0u;
|
||||
metadata[8] = (uint8_t)(ota_image_version >> 8u);
|
||||
metadata[9] = (uint8_t)ota_image_version;
|
||||
metadata[10] = (uint8_t)(ota_verify_crc >> 8u);
|
||||
metadata[11] = (uint8_t)ota_verify_crc;
|
||||
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 ||
|
||||
W25Q64App_PageProgram(OTA_METADATA_ADDRESS + 16ul,
|
||||
&metadata[16], 2u) == 0u)
|
||||
{
|
||||
result = 0x01u;
|
||||
}
|
||||
}
|
||||
ota_state = OTA_STATE_IDLE;
|
||||
if (ota_verify_response_pending != 0u)
|
||||
{
|
||||
if (ota_verify_broadcast_request != 0u)
|
||||
{
|
||||
/* 广播校验请求仍使用随机延时应答,但广播场景不立即
|
||||
* 复位,避免延时队列中的0x8B尚未发出就被复位丢失。 */
|
||||
BusinessProtocol_QueueDelayedResponse(ota_verify_response_target,
|
||||
0x8Bu, &result, 1u);
|
||||
}
|
||||
else
|
||||
{
|
||||
(void)MeshFrame_SendBusiness(ota_verify_response_target,
|
||||
0x8Bu, &result, 1u);
|
||||
}
|
||||
ota_verify_response_pending = 0u;
|
||||
|
||||
/* 只有整包CRC正确且OTA元数据已经成功写入时,才在0x8B
|
||||
* 应答发送完成后复位。失败分支必须继续留在当前程序。 */
|
||||
if ((result == 0x00u) && (ota_verify_broadcast_request == 0u))
|
||||
{
|
||||
Watchdog_ResetNow();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (ota_bootloader_result_pending != 0u &&
|
||||
ota_bootloader_result_reported == 0u)
|
||||
{
|
||||
@@ -99,7 +223,8 @@ void OtaManager_Task(void)
|
||||
*/
|
||||
uint8_t OtaManager_HandleCommand(uint8_t command,
|
||||
const uint8_t *data,
|
||||
uint8_t length)
|
||||
uint8_t length,
|
||||
uint16_t response_target)
|
||||
{
|
||||
if (command == OTA_COMMAND_START)
|
||||
{
|
||||
@@ -108,18 +233,17 @@ uint8_t OtaManager_HandleCommand(uint8_t command,
|
||||
{
|
||||
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)
|
||||
ota_expected_size=((uint16_t)data[0]<<8u)|data[1];
|
||||
ota_image_version=((uint16_t)data[2]<<8u)|data[3];
|
||||
if(ota_expected_size==0u || ota_expected_size > OTA_IMAGE_MAX_SIZE)
|
||||
return 0u;
|
||||
/* 先清除OTA元数据,避免上一次升级请求残留。 */
|
||||
if(W25Q64App_EraseSector(OTA_METADATA_ADDRESS)==0u)
|
||||
return 0u;
|
||||
for(sector=0u;sector<16u;sector++)
|
||||
for(sector=0u;sector<OTA_IMAGE_SECTOR_COUNT;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_offset=0u;
|
||||
ota_expected_crc16=0u;
|
||||
ota_state = OTA_STATE_RECEIVING;
|
||||
return 1u;
|
||||
@@ -141,64 +265,42 @@ uint8_t OtaManager_HandleCommand(uint8_t command,
|
||||
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低字节、高字节。 */
|
||||
/* 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_expected_crc16 = ((uint16_t)data[0] << 8u) | data[1];
|
||||
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)
|
||||
if(ota_state!=OTA_STATE_READY||ota_expected_offset!=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;
|
||||
ota_verify_offset = 0u;
|
||||
ota_verify_crc = 0xFFFFu;
|
||||
ota_verify_response_target = response_target;
|
||||
ota_verify_broadcast_request =
|
||||
(MeshFrame_GetLastTargetAddress() == 0xFFFFu) ? 1u : 0u;
|
||||
ota_verify_response_pending = 1u;
|
||||
ota_state = OTA_STATE_VERIFYING;
|
||||
return 1u;
|
||||
}
|
||||
|
||||
if (command == OTA_COMMAND_CANCEL)
|
||||
{
|
||||
ota_state = OTA_STATE_IDLE;
|
||||
ota_received_size = 0u;
|
||||
ota_expected_offset = 0u;
|
||||
return 1u;
|
||||
}
|
||||
|
||||
@@ -222,7 +324,7 @@ uint8_t OtaManager_GetExpectedOffset(uint16_t *offset)
|
||||
|
||||
uint8_t OtaManager_GetBootloaderResult(void)
|
||||
{
|
||||
/* W25Q64读取接口接入后,在此处加载result字段并置pending。 */
|
||||
/* W25Q80BV读取接口接入后,在此处加载result字段并置pending。 */
|
||||
return (ota_bootloader_result_pending != 0u) ? ota_bootloader_result : 0u;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,407 @@
|
||||
#!/usr/bin/env python3
|
||||
"""通过电脑串口测试STM8 Mesh继电器的主程序OTA功能。
|
||||
|
||||
发送流程:
|
||||
0x08:发送固件长度和版本
|
||||
0x09:逐包发送固件数据,并等待设备返回下一期待偏移
|
||||
0x0A:发送完整固件CRC16
|
||||
0x0B:请求设备分片校验W25Q80BV中的固件
|
||||
0x0D:设备复位并升级后主动上报升级结果
|
||||
|
||||
当前协议约定:
|
||||
- JDY-28M发送外层:41 54 2B AA A1 | DST_H DST_L | A0 | BUSINESS | 0D 0A
|
||||
- 业务帧:SRC_H SRC_L | CMD | LEN | DATA | CRC16_H CRC16_L
|
||||
- CRC算法:CRC-16/MODBUS
|
||||
- DATA中的多字节数值和CRC均为高字节在前
|
||||
- CMD=0x09每帧最多携带2字节固件数据
|
||||
|
||||
依赖:
|
||||
python3 -m pip install pyserial
|
||||
|
||||
示例:
|
||||
python3 tools/ota_serial_test.py \
|
||||
--port /dev/cu.usbserial-0001 \
|
||||
--firmware build/mesh_relay.bin \
|
||||
--source 0001 \
|
||||
--target 000C
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import re
|
||||
import struct
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
import serial
|
||||
except ImportError as exc:
|
||||
raise SystemExit(
|
||||
"缺少pyserial,请先执行:python3 -m pip install pyserial"
|
||||
) from exc
|
||||
|
||||
|
||||
# JDY-28M Mesh发送外层固定字段。
|
||||
MESH_TX_PREFIX = bytes.fromhex("41 54 2B AA A1")
|
||||
MESH_DATA_FLAG = 0xA0
|
||||
MESH_TAIL = bytes.fromhex("0D 0A")
|
||||
|
||||
# JDY-28M接收数据时,串口侧看到的固定前缀。
|
||||
MESH_RX_PREFIX = bytes.fromhex("F1 DD 09 00 0A")
|
||||
|
||||
# OTA命令定义。
|
||||
CMD_OTA_START = 0x08
|
||||
CMD_OTA_DATA = 0x09
|
||||
CMD_OTA_FINISH = 0x0A
|
||||
CMD_OTA_VERIFY = 0x0B
|
||||
CMD_OTA_RESULT_REPORT = 0x0D
|
||||
CMD_OTA_RESULT_ACK = 0x8D
|
||||
CMD_FIRMWARE_ANNOUNCE = 0x10
|
||||
CMD_FIRMWARE_ANNOUNCE_RESPONSE = 0x90
|
||||
|
||||
# 协议和目标MCU的限制。
|
||||
MAX_FIRMWARE_SIZE = 0x2000
|
||||
OTA_CHUNK_SIZE = 2
|
||||
|
||||
|
||||
def crc16_modbus(data: bytes) -> int:
|
||||
"""计算CRC-16/MODBUS。
|
||||
|
||||
CRC计算范围不包含Mesh外层帧头、目标地址、A0和帧尾,只包含业务帧:
|
||||
SRC、CMD、LEN和DATA。
|
||||
"""
|
||||
|
||||
crc = 0xFFFF
|
||||
for value in data:
|
||||
crc ^= value
|
||||
for _ in range(8):
|
||||
crc = (crc >> 1) ^ 0xA001 if crc & 1 else crc >> 1
|
||||
return crc & 0xFFFF
|
||||
|
||||
|
||||
def parse_hex_address(value: str, name: str) -> int:
|
||||
"""解析两字节十六进制Mesh短地址。"""
|
||||
|
||||
try:
|
||||
address = int(value, 16)
|
||||
except ValueError as exc:
|
||||
raise argparse.ArgumentTypeError(f"{name}不是有效的十六进制地址:{value}") from exc
|
||||
if not 0 <= address <= 0xFFFF:
|
||||
raise argparse.ArgumentTypeError(f"{name}必须在0000~FFFF范围内")
|
||||
return address
|
||||
|
||||
|
||||
def read_firmware_version(header_path: Path) -> int:
|
||||
"""从主程序app_config.h读取FIRMWARE_VERSION。"""
|
||||
|
||||
content = header_path.read_text(encoding="utf-8")
|
||||
match = re.search(
|
||||
r"^\s*#define\s+FIRMWARE_VERSION\s+((?:0[xX][0-9A-Fa-f]+)|(?:[0-9]+))",
|
||||
content,
|
||||
flags=re.MULTILINE,
|
||||
)
|
||||
if match is None:
|
||||
raise ValueError(f"在{header_path}中找不到FIRMWARE_VERSION")
|
||||
version = int(match.group(1), 0)
|
||||
if not 0 <= version <= 0xFFFF:
|
||||
raise ValueError(f"固件版本超出16位范围:{version}")
|
||||
return version
|
||||
|
||||
|
||||
def make_business_frame(source: int, command: int, data: bytes) -> bytes:
|
||||
"""生成业务帧,末尾CRC16按高字节在前发送。"""
|
||||
|
||||
if len(data) > 4:
|
||||
raise ValueError("当前JDY-28M外层20字节限制下,业务DATA不能超过4字节")
|
||||
body = struct.pack(">HBB", source, command, len(data)) + data
|
||||
crc = crc16_modbus(body)
|
||||
return body + struct.pack(">H", crc)
|
||||
|
||||
|
||||
def make_mesh_frame(target: int, business: bytes) -> bytes:
|
||||
"""封装JDY-28M Mesh发送帧。"""
|
||||
|
||||
return (
|
||||
MESH_TX_PREFIX
|
||||
+ struct.pack(">H", target)
|
||||
+ bytes([MESH_DATA_FLAG])
|
||||
+ business
|
||||
+ MESH_TAIL
|
||||
)
|
||||
|
||||
|
||||
def send_command(ser: serial.Serial, source: int, target: int,
|
||||
command: int, data: bytes) -> None:
|
||||
"""通过串口发送一条完整Mesh业务命令。"""
|
||||
|
||||
frame = make_mesh_frame(target, make_business_frame(source, command, data))
|
||||
ser.write(frame)
|
||||
ser.flush()
|
||||
|
||||
|
||||
def parse_received_business(frame: bytes) -> tuple[int, int, bytes] | None:
|
||||
"""从JDY-28M接收外层帧中提取业务命令并校验CRC。
|
||||
|
||||
接收格式为:
|
||||
F1 DD 09 00 0A | 外层地址2字节 | BUSINESS | 0D 0A
|
||||
|
||||
外层地址字段由模块提供;真正的发送方地址仍取业务帧SRC字段。
|
||||
"""
|
||||
|
||||
if len(frame) < len(MESH_RX_PREFIX) + 2 + 6 + 2:
|
||||
return None
|
||||
if not frame.startswith(MESH_RX_PREFIX) or not frame.endswith(MESH_TAIL):
|
||||
return None
|
||||
|
||||
business = frame[len(MESH_RX_PREFIX) + 2:-2]
|
||||
if len(business) < 6:
|
||||
return None
|
||||
source, command, length = struct.unpack(">HBB", business[:4])
|
||||
if len(business) != 4 + length + 2 or length > 4:
|
||||
return None
|
||||
body = business[:4 + length]
|
||||
received_crc = struct.unpack(">H", business[4 + length:])[0]
|
||||
if crc16_modbus(body) != received_crc:
|
||||
return None
|
||||
return source, command, business[4:4 + length]
|
||||
|
||||
|
||||
def receive_response(ser: serial.Serial, timeout: float) -> tuple[int, int, bytes]:
|
||||
"""等待并解析一条合法的JDY-28M业务响应。"""
|
||||
|
||||
deadline = time.monotonic() + timeout
|
||||
buffer = bytearray()
|
||||
while time.monotonic() < deadline:
|
||||
waiting = ser.in_waiting
|
||||
if waiting:
|
||||
buffer.extend(ser.read(waiting))
|
||||
else:
|
||||
time.sleep(0.005)
|
||||
|
||||
while True:
|
||||
start = buffer.find(MESH_RX_PREFIX)
|
||||
if start < 0:
|
||||
# 保留可能是固定前缀开头的最后几个字节,避免跨次读取丢帧。
|
||||
keep = min(len(buffer), len(MESH_RX_PREFIX) - 1)
|
||||
if keep:
|
||||
del buffer[:-keep]
|
||||
else:
|
||||
buffer.clear()
|
||||
break
|
||||
if start:
|
||||
del buffer[:start]
|
||||
if len(buffer) < len(MESH_RX_PREFIX) + 2 + 4 + 2 + 2:
|
||||
break
|
||||
|
||||
data_length = buffer[len(MESH_RX_PREFIX) + 2 + 3]
|
||||
total = len(MESH_RX_PREFIX) + 2 + 4 + data_length + 2 + 2
|
||||
if data_length > 4:
|
||||
del buffer[0]
|
||||
continue
|
||||
if len(buffer) < total:
|
||||
break
|
||||
|
||||
candidate = bytes(buffer[:total])
|
||||
del buffer[:total]
|
||||
parsed = parse_received_business(candidate)
|
||||
if parsed is not None:
|
||||
return parsed
|
||||
|
||||
raise TimeoutError(f"等待设备响应超时({timeout:.1f}秒)")
|
||||
|
||||
|
||||
def wait_command_response(ser: serial.Serial, expected_command: int,
|
||||
timeout: float) -> bytes:
|
||||
"""等待指定应答命令,忽略上线通知等无关业务帧。"""
|
||||
|
||||
deadline = time.monotonic() + timeout
|
||||
while time.monotonic() < deadline:
|
||||
source, command, data = receive_response(
|
||||
ser, max(0.1, deadline - time.monotonic())
|
||||
)
|
||||
print(f"收到响应:SRC={source:04X} CMD={command:02X} DATA={data.hex(' ').upper()}")
|
||||
if command == expected_command:
|
||||
return data
|
||||
raise TimeoutError(f"未收到期望响应CMD={expected_command:02X}")
|
||||
|
||||
|
||||
def wait_upgrade_result(ser: serial.Serial, timeout: float) -> tuple[int, int]:
|
||||
"""等待设备复位后主动上报CMD=0x0D升级结果。
|
||||
|
||||
0x8B只表示外部Flash固件校验成功。设备随后进入Bootloader搬运固件,
|
||||
重启主程序并在蓝牙初始化完成后广播0x0D。因此这里除了结果码,还要
|
||||
保留业务帧中的SRC地址,以便后续发送0x8D确认。
|
||||
|
||||
@return (设备短地址, 升级结果码)。
|
||||
@raises TimeoutError 在指定时间内未收到合法0x0D。
|
||||
"""
|
||||
deadline = time.monotonic() + timeout
|
||||
while time.monotonic() < deadline:
|
||||
try:
|
||||
source, command, data = receive_response(
|
||||
ser, max(0.1, deadline - time.monotonic())
|
||||
)
|
||||
except TimeoutError:
|
||||
break
|
||||
print(f"收到升级结果:SRC={source:04X} CMD={command:02X} "
|
||||
f"DATA={data.hex(' ').upper()}")
|
||||
if command == CMD_OTA_RESULT_REPORT and len(data) == 1:
|
||||
return source, data[0]
|
||||
raise TimeoutError(f"等待设备升级结果超时({timeout:.1f}秒)")
|
||||
|
||||
|
||||
def send_ota(args: argparse.Namespace) -> None:
|
||||
"""执行完整OTA升级测试。"""
|
||||
|
||||
firmware = Path(args.firmware).read_bytes()
|
||||
if not firmware:
|
||||
raise ValueError("固件文件为空")
|
||||
if len(firmware) > MAX_FIRMWARE_SIZE:
|
||||
raise ValueError(f"固件大小{len(firmware)}超过8KB限制")
|
||||
|
||||
version_header = Path(args.version_header)
|
||||
version = args.version if args.version is not None else read_firmware_version(version_header)
|
||||
crc = crc16_modbus(firmware)
|
||||
print(f"固件:{args.firmware}")
|
||||
print(f"长度:{len(firmware)} 字节,版本:0x{version:04X},CRC16:0x{crc:04X}")
|
||||
|
||||
with serial.Serial(args.port, args.baudrate, timeout=0) as ser:
|
||||
# 进入OTA:长度和版本均为高字节在前。
|
||||
send_command(ser, args.source, args.target, CMD_OTA_START,
|
||||
struct.pack(">HH", len(firmware), version))
|
||||
status = wait_command_response(ser, 0x88, args.timeout)
|
||||
if status != b"\x00":
|
||||
raise RuntimeError(f"设备拒绝进入OTA,状态={status.hex(' ').upper()}")
|
||||
|
||||
offset = 0
|
||||
while offset < len(firmware):
|
||||
payload = firmware[offset:offset + OTA_CHUNK_SIZE]
|
||||
data = struct.pack(">H", offset) + payload
|
||||
send_command(ser, args.source, args.target, CMD_OTA_DATA, data)
|
||||
response = wait_command_response(ser, 0x89, args.timeout)
|
||||
if len(response) != 3 or response[0] != 0:
|
||||
raise RuntimeError(f"分包写入失败,响应={response.hex(' ').upper()}")
|
||||
expected_offset = struct.unpack(">H", response[1:3])[0]
|
||||
expected_local = offset + len(payload)
|
||||
if expected_offset != expected_local:
|
||||
raise RuntimeError(
|
||||
f"设备期待偏移0x{expected_offset:04X},本地下一偏移为0x{expected_local:04X}"
|
||||
)
|
||||
offset = expected_offset
|
||||
print(f"OTA分包进度:{offset}/{len(firmware)}", end="\r", flush=True)
|
||||
print()
|
||||
|
||||
# 所有分包均已确认后,再发送完整固件CRC16。
|
||||
send_command(ser, args.source, args.target, CMD_OTA_FINISH,
|
||||
struct.pack(">H", crc))
|
||||
status = wait_command_response(ser, 0x8A, args.timeout)
|
||||
if status != b"\x00":
|
||||
raise RuntimeError(f"设备拒绝固件CRC,状态={status.hex(' ').upper()}")
|
||||
|
||||
# 请求设备后台分片读取W25Q80BV并计算完整CRC。
|
||||
send_command(ser, args.source, args.target, CMD_OTA_VERIFY, b"")
|
||||
status = wait_command_response(ser, 0x8B, args.verify_timeout)
|
||||
if status != b"\x00":
|
||||
raise RuntimeError(f"设备固件校验失败,状态={status.hex(' ').upper()}")
|
||||
|
||||
# 0x8B只表示外部Flash中的固件校验成功。设备随后自动复位,
|
||||
# 由Bootloader搬运主程序;这里继续等待主程序启动后的0x0D上报。
|
||||
device_address, upgrade_result = wait_upgrade_result(
|
||||
ser, args.result_timeout
|
||||
)
|
||||
if upgrade_result != 0x01:
|
||||
raise RuntimeError(
|
||||
f"设备升级失败:设备地址={device_address:04X},"
|
||||
f"结果码=0x{upgrade_result:02X}"
|
||||
)
|
||||
|
||||
# 0x01表示Bootloader搬运并校验成功。回复0x8D后,设备清除待上报
|
||||
# 结果,避免后续每次启动都重复上报同一条升级结果。
|
||||
send_command(ser, args.source, device_address,
|
||||
CMD_OTA_RESULT_ACK, bytes([upgrade_result]))
|
||||
|
||||
print("OTA升级成功:设备已复位、Bootloader已搬运主程序,并完成结果确认。")
|
||||
|
||||
|
||||
def discover_upgrade_devices(args: argparse.Namespace) -> None:
|
||||
"""广播新固件信息并收集需要升级的设备列表。"""
|
||||
|
||||
firmware = Path(args.firmware).read_bytes()
|
||||
if not firmware or len(firmware) > MAX_FIRMWARE_SIZE:
|
||||
raise ValueError("固件为空或超过8KB限制")
|
||||
version = args.version if args.version is not None else read_firmware_version(
|
||||
Path(args.version_header)
|
||||
)
|
||||
|
||||
print(f"广播固件版本:0x{version:04X},长度:{len(firmware)}字节")
|
||||
devices: dict[int, tuple[int, int]] = {}
|
||||
with serial.Serial(args.port, args.baudrate, timeout=0) as ser:
|
||||
# 0x10使用广播地址FFFF,仅通知版本和长度,不会直接启动OTA。
|
||||
announce = struct.pack(">HH", version, len(firmware))
|
||||
send_command(ser, args.source, 0xFFFF,
|
||||
CMD_FIRMWARE_ANNOUNCE, announce)
|
||||
|
||||
deadline = time.monotonic() + args.discovery_timeout
|
||||
while time.monotonic() < deadline:
|
||||
try:
|
||||
source, command, data = receive_response(
|
||||
ser, max(0.1, deadline - time.monotonic())
|
||||
)
|
||||
except TimeoutError:
|
||||
break
|
||||
if command != CMD_FIRMWARE_ANNOUNCE_RESPONSE:
|
||||
continue
|
||||
if len(data) != 4:
|
||||
continue
|
||||
current_version, announced_version = struct.unpack(">HH", data)
|
||||
devices[source] = (current_version, announced_version)
|
||||
print(
|
||||
f"发现待升级设备:地址={source:04X},"
|
||||
f"当前版本=0x{current_version:04X},"
|
||||
f"目标版本=0x{announced_version:04X}"
|
||||
)
|
||||
|
||||
print(f"共发现{len(devices)}台需要升级的设备。")
|
||||
print("人工确认后,请使用--target 设备短地址执行正式OTA。")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
"""解析命令行参数并启动OTA测试。"""
|
||||
|
||||
parser = argparse.ArgumentParser(description="STM8 Mesh继电器串口OTA测试工具")
|
||||
parser.add_argument("--port", required=True, help="电脑串口,例如COM3或/dev/cu.usbserial-xxx")
|
||||
parser.add_argument("--firmware", required=True, help="主程序裸BIN文件")
|
||||
parser.add_argument("--source", default="0001", type=lambda value: parse_hex_address(value, "source"),
|
||||
help="主机Mesh短地址,默认0001")
|
||||
parser.add_argument("--target", default="FFFF", type=lambda value: parse_hex_address(value, "target"),
|
||||
help="设备短地址,FFFF表示广播,默认FFFF")
|
||||
parser.add_argument("--baudrate", type=int, default=9600, help="UART波特率,默认9600")
|
||||
parser.add_argument("--timeout", type=float, default=3.0, help="普通应答超时秒数,默认3秒")
|
||||
parser.add_argument("--verify-timeout", type=float, default=30.0, help="CRC校验应答超时秒数,默认30秒")
|
||||
parser.add_argument("--result-timeout", type=float, default=20.0,
|
||||
help="等待设备复位后上报升级结果的秒数,默认20秒")
|
||||
parser.add_argument("--discover", action="store_true",
|
||||
help="只广播固件信息并收集待升级设备,不执行OTA")
|
||||
parser.add_argument("--discovery-timeout", type=float, default=3.0,
|
||||
help="设备发现等待时间,默认3秒")
|
||||
parser.add_argument("--version-header", default="include/app_config.h",
|
||||
help="版本配置头文件,默认include/app_config.h")
|
||||
parser.add_argument("--version", type=lambda value: int(value, 0), default=None,
|
||||
help="可选,手工指定版本;不指定时自动读取FIRMWARE_VERSION")
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
if args.discover:
|
||||
discover_upgrade_devices(args)
|
||||
else:
|
||||
send_ota(args)
|
||||
except (OSError, serial.SerialException, TimeoutError, ValueError, RuntimeError) as exc:
|
||||
print(f"OTA失败:{exc}", file=sys.stderr)
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user