feat: 完善Mesh继电器OTA升级闭环

This commit is contained in:
十七
2026-11-04 15:41:29 +08:00
parent be8bd7eaec
commit df1aad3d16
27 changed files with 1372 additions and 243 deletions
+2
View File
@@ -9,6 +9,8 @@
/** 初始化 Bootloader 所需的最小硬件资源。 */
void Bootloader_Init(void);
/** 刷新独立看门狗,供W25Q64和Flash等待循环调用。 */
void Bootloader_WatchdogRefresh(void);
/** 判断应用程序区域是否包含可执行程序。 */
uint8_t Bootloader_IsApplicationValid(void);
/** 跳转到应用程序入口地址,函数成功后不返回。 */
+9 -7
View File
@@ -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
+2 -2
View File
@@ -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);
+142 -17
View File
@@ -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)
{
}
}
}
}
+69 -15
View File
@@ -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 +