feat(FOC): 添加快速电压检测功能和PWM调试模式

新增基于跌幅检测的快速电压保护机制,在100us周期内响应电压异常,
当电压跌幅超过阈值或低于绝对下限时立即关机保护。同时添加PWM输出
调试模式便于硬件测试。

BREAKING CHANGE: 重构了电压检测逻辑,采用峰值跟踪算法替代固
定阈值检测,提高在动态供电环境下的可靠性
This commit is contained in:
cbb
2026-04-26 03:36:21 +08:00
parent bdf03517b3
commit f4fb23d640
9 changed files with 586 additions and 127 deletions
+39 -5
View File
@@ -16,8 +16,8 @@
// 版本号定义 - 可根据实际项目进行调整
constexpr u16 SOFTWARE_VERSION_MAJOR = 26;
constexpr u16 SOFTWARE_VERSION_MINOR = 3;
constexpr u16 SOFTWARE_VERSION_PATCH = 16;
constexpr u16 SOFTWARE_VERSION_MINOR = 4;
constexpr u16 SOFTWARE_VERSION_PATCH = 26;
constexpr u16 SOFTWARE_VERSION_BUILD = 2;
constexpr u16 HARDWARE_VERSION_MAJOR = 25;
@@ -46,10 +46,31 @@ constexpr u32 SLEEP_DISABLE = 0x00000000; // 睡眠禁止
// 节点 ID 配置 (用于多电机 CAN 总线隔离)
// 使用 CAN ID 高 16 位 (bits 31-16) 存储节点 ID,低 16 位 (bits 15-0) 存储基础 ID
// 格式:[节点 ID(16 位)][基础 ID(16 位)],例如:node_id=1, base_id=0x0200 → 0x00010200
constexpr u32 NODE_ID_CONFIG_ADDR = 0x0801E800; // 节点 ID 存储地址
//
// 节点 ID 存储方案:
// - 使用独立 Flash 页 (NODE_ID_PAGE_ADDR) 存储节点 ID,与EEPROM完全隔离
// - 采用双重备份机制 (主备份 + 备用备份)
// - 使用魔数 (NODE_ID_MAGIC) 验证数据有效性,防止位翻转导致错读
// - 使用写入计数器区分最新数据
constexpr u32 NODE_ID_PAGE_ADDR = 0x0801E800; // 节点 ID 存储页 (1KB,独立区域)
constexpr u32 NODE_ID_PRIMARY_ADDR = 0x0801E800; // 主存储地址
constexpr u32 NODE_ID_BACKUP_ADDR = 0x0801E840; // 备份存储地址 (偏移64字节)
constexpr u32 NODE_ID_MAGIC = 0x4E4F4445; // 魔数 'NODE' 用于验证数据有效性
constexpr u16 DEFAULT_NODE_ID = 1; // 默认节点 ID
constexpr u16 BROADCAST_NODE_ID = 0xFFFF; // 广播 ID (所有节点响应)
constexpr u16 MAX_NODE_ID = 0xFFFE; // 最大有效节点 ID (0~0xFFFE)
constexpr u16 BROADCAST_NODE_ID = 0x1FFF; // 广播 ID (所有节点响应)
constexpr u16 MAX_NODE_ID = 0x1FFE; // 最大有效节点 ID (0~0x1FFE)
// 节点 ID 存储结构 (存储在Flash中的格式)
// 地址偏移 0x00: 魔数 (0x4E4F4445 = 'NODE')
// 地址偏移 0x04: 节点 ID (u16)
// 地址偏移 0x06: 保留 (u16,填充为0xFFFF)
// 地址偏移 0x08: 写入计数器 (u32,用于区分最新数据)
// 地址偏移 0x0C: CRC32 校验 (u32)
constexpr u8 NODE_ID_STRUCT_SIZE = 0x10; // 结构体大小 16 字节
constexpr u8 NODE_ID_MAGIC_OFFSET = 0x00; // 魔数偏移
constexpr u8 NODE_ID_VALUE_OFFSET = 0x04; // 节点ID值偏移
constexpr u8 NODE_ID_COUNTER_OFFSET = 0x08; // 写入计数器偏移
constexpr u8 NODE_ID_CRC_OFFSET = 0x0C; // CRC偏移
// BOOT 相关
constexpr u32 OTA_CONFIG_NUM = 6;
@@ -215,4 +236,17 @@ constexpr int TIMER1_DEFAULT_PULSE = 0;
constexpr int TIMER1_CH3_COMPARE_VALUE =
(TIMER1_PERIOD + 1) * 0.41 - 1; // PWM 周期 50% 位置触发中断
// 电压检测参数(基于跌幅检测,不依赖固定额定电压)
// 快速检测层(fast_voltage_check,100us周期):
// - 使用峰值检测算法跟踪当前额定电压(vbus_reference)
// - 当电压相对于基准电压下跌超过 VBUS_DROP_THRESHOLD → 关机
// - 当电压低于绝对下限 VBUS_ABSOLUTE_MIN → 关机
// 慢速管理层(power_manage,~10ms周期):
// - 执行Flash保存
// - 状态恢复判断(电压恢复到基准电压的90%以上)
constexpr float VBUS_DROP_THRESHOLD = 5.0f; // 电压跌幅阈值 (V)
constexpr float VBUS_ABSOLUTE_MIN = 12.0f; // 绝对最低电压 (V)
constexpr u32 VBUS_DEBOUNCE_COUNT = 5; // 快速检测:防抖计数 (5次×100us = 500us)
constexpr float VBUS_RECOVERY_RATIO = 0.9f; // 恢复比例(基准电压的90%)
#endif
+222
View File
@@ -477,6 +477,228 @@ float FlashEEPROM::read_angle() {
return angle;
}
// ============================================================================
// 节点 ID 独立读写函数实现(使用魔数+双重备份+计数器机制)
// ============================================================================
/*!
* @brief 计算节点 ID 数据的 CRC32 校验值
* @param node_id: 节点 ID 值
* @param counter: 写入计数器值
* @retval u32: CRC32 校验值
*/
static u32 calculate_node_id_crc(u16 node_id, u32 counter) {
u32 crc_data[2];
crc_data[0] = (u32)node_id; // 节点 ID 值
crc_data[1] = counter; // 写入计数器
crc_data_register_reset();
return crc_block_data_calculate(crc_data, 2);
}
/*!
* @brief 从指定地址读取节点 ID 数据结构
* @param addr: Flash 地址
* @param node_id: 输出节点 ID 值
* @param counter: 输出写入计数器值
* @retval FlashEepromStatus: 操作状态
*/
static FlashEepromStatus read_node_id_struct(u32 addr, u16 *node_id, u32 *counter) {
// 检查地址是否对齐
if (addr & 0x3) {
return FLASH_EEPROM_ALIGNMENT_ERROR;
}
volatile u32 *flash_ptr = (volatile u32 *)addr;
// 读取魔数
u32 magic = flash_ptr[NODE_ID_MAGIC_OFFSET / sizeof(u32)];
if (magic != NODE_ID_MAGIC) {
EEP_DEBUG_PRINT("Node ID magic mismatch at 0x%08X: 0x%08X\r\n", addr, magic);
return FLASH_EEPROM_DATA_NOT_FOUND;
}
// 读取节点 ID 值
u16 id_value = (u16)flash_ptr[NODE_ID_VALUE_OFFSET / sizeof(u32)];
if (id_value > MAX_NODE_ID && id_value != 0xFFFF) {
EEP_DEBUG_PRINT("Invalid node ID value: %d\r\n", id_value);
return FLASH_EEPROM_READ_ERROR;
}
// 读取写入计数器
u32 write_counter = flash_ptr[NODE_ID_COUNTER_OFFSET / sizeof(u32)];
// 读取并验证 CRC
u32 stored_crc = flash_ptr[NODE_ID_CRC_OFFSET / sizeof(u32)];
u32 calculated_crc = calculate_node_id_crc(id_value, write_counter);
if (stored_crc != calculated_crc) {
EEP_DEBUG_PRINT("Node ID CRC mismatch at 0x%08X: stored=0x%08X, calc=0x%08X\r\n",
addr, stored_crc, calculated_crc);
return FLASH_EEPROM_CRC_ERROR;
}
*node_id = id_value;
*counter = write_counter;
return FLASH_EEPROM_OK;
}
/*!
* @brief 写入节点 ID 数据结构到指定地址
* @param addr: Flash 地址
* @param node_id: 节点 ID 值
* @param counter: 写入计数器值
* @retval FlashEepromStatus: 操作状态
*/
static FlashEepromStatus write_node_id_struct(u32 addr, u16 node_id, u32 counter) {
// 检查地址是否对齐
if (addr & 0x3) {
return FLASH_EEPROM_ALIGNMENT_ERROR;
}
// 计算 CRC
u32 crc = calculate_node_id_crc(node_id, counter);
// 准备写入数据
u32 write_data[4];
write_data[0] = NODE_ID_MAGIC; // 魔数
write_data[1] = (u32)node_id; // 节点 ID 值
write_data[2] = counter; // 写入计数器
write_data[3] = crc; // CRC 校验
// 写入 Flash(需要解锁)
fmc_unlock();
fmc_flag_clear(FMC_FLAG_PGERR | FMC_FLAG_PGAERR | FMC_FLAG_WPERR);
fmc_state_enum fmc_status;
for (u8 i = 0; i < 4; i++) {
fmc_status = fmc_word_program(addr + i * sizeof(u32), write_data[i]);
if (fmc_status != FMC_READY) {
fmc_lock();
EEP_DEBUG_PRINT("Flash write failed at offset %d, status=%d\r\n", i, fmc_status);
return FLASH_EEPROM_WRITE_ERROR;
}
}
fmc_lock();
return FLASH_EEPROM_OK;
}
/*!
* @brief 擦除节点 ID 存储页
* @retval FlashEepromStatus: 操作状态
*/
FlashEepromStatus FlashEEPROM::eraseNodeId() {
fmc_state_enum fmc_status;
EEP_DEBUG_PRINT("Erasing Node ID page at 0x%08X\r\n", NODE_ID_PAGE_ADDR);
fmc_unlock();
fmc_status = fmc_page_erase(NODE_ID_PAGE_ADDR);
if (fmc_status != FMC_READY) {
fmc_lock();
EEP_DEBUG_PRINT("Node ID page erase failed, status=%d\r\n", fmc_status);
return FLASH_EEPROM_ERASE_ERROR;
}
fmc_lock();
return FLASH_EEPROM_OK;
}
/*!
* @brief 从 Flash 读取节点 ID
* @param node_id: 输出节点 ID 值
* @retval FlashEepromStatus: 操作状态
*/
FlashEepromStatus FlashEEPROM::readNodeId(u16 *node_id) const {
if (node_id == nullptr) {
return FLASH_EEPROM_INVALID_ADDR;
}
u16 id_value = 0;
u32 counter = 0;
// 从主存储区读取
FlashEepromStatus status = read_node_id_struct(NODE_ID_PRIMARY_ADDR, &id_value, &counter);
if (status == FLASH_EEPROM_OK) {
*node_id = id_value;
EEP_DEBUG_PRINT("Node ID read from primary (counter=%u): %d\r\n", counter, *node_id);
return FLASH_EEPROM_OK;
} else {
EEP_DEBUG_PRINT("Node ID read failed from primary: status=%d\r\n", status);
return FLASH_EEPROM_DATA_NOT_FOUND;
}
}
/*!
* @brief 保存节点 ID 到 Flash(每次写入前先擦除整个页面,写入后校验)
* @param node_id: 要保存的节点 ID 值
* @retval FlashEepromStatus: 操作状态
* @note 由于Flash只能将1写为0,不能将0写为1,因此每次写入新数据前
* 需要先擦除整个页面。简化方案:不再使用备用备份区,
* 每次写入前擦除页面,然后写入主存储区。
* 写入后立即校验数据,如果不对则再次写入,最多3次。
*/
FlashEepromStatus FlashEEPROM::writeNodeId(u16 node_id) {
if (node_id > MAX_NODE_ID) {
EEP_DEBUG_PRINT("Invalid node ID: %d (max=%d)\r\n", node_id, MAX_NODE_ID);
return FLASH_EEPROM_INVALID_ADDR;
}
// 获取当前写入计数器(从主存储区读取)
u16 current_id = 0;
u32 current_counter = 0;
FlashEepromStatus primary_status = read_node_id_struct(NODE_ID_PRIMARY_ADDR, &current_id, &current_counter);
// 计数器 +1
u32 new_counter = current_counter + 1;
// 如果计数器溢出(0xFFFFFFFF + 1 = 0),则使用1
if (new_counter == 0) {
new_counter = 1;
}
// 先擦除整个页面
EEP_DEBUG_PRINT("Erasing Node ID page before write\r\n");
FlashEepromStatus status = eraseNodeId();
if (status != FLASH_EEPROM_OK) {
return status;
}
// 写入并校验,最多3次
#define MAX_WRITE_RETRY 3
for (u8 retry = 0; retry < MAX_WRITE_RETRY; retry++) {
// 写入主存储区
EEP_DEBUG_PRINT("Writing to primary (counter=%u, retry=%d)\r\n", new_counter, retry);
FlashEepromStatus write_status = write_node_id_struct(NODE_ID_PRIMARY_ADDR, node_id, new_counter);
if (write_status != FLASH_EEPROM_OK) {
EEP_DEBUG_PRINT("Node ID write failed: status=%d\r\n", write_status);
continue;
}
// 立即校验数据
u16 read_id = 0;
u32 read_counter = 0;
FlashEepromStatus read_status = read_node_id_struct(NODE_ID_PRIMARY_ADDR, &read_id, &read_counter);
if (read_status == FLASH_EEPROM_OK && read_id == node_id && read_counter == new_counter) {
EEP_DEBUG_PRINT("Node ID saved and verified successfully: %d (counter=%u)\r\n", node_id, new_counter);
return FLASH_EEPROM_OK;
} else {
EEP_DEBUG_PRINT("Node ID verification failed: read_id=%d (expected=%d), read_counter=%u (expected=%u)\r\n",
read_id, node_id, read_counter, new_counter);
// 校验失败,重新擦除并写入
if (retry < MAX_WRITE_RETRY - 1) {
EEP_DEBUG_PRINT("Retrying...\r\n");
eraseNodeId();
}
}
}
EEP_DEBUG_PRINT("Node ID save failed after %d retries\r\n", MAX_WRITE_RETRY);
return FLASH_EEPROM_WRITE_ERROR;
}
/**
* @brief 保存 CRC 数据到 Flash
* @param saved: 是否一次性保存
+9 -1
View File
@@ -24,7 +24,7 @@ class FlashEEPROM {
public:
// EEPROM emulation constants
static constexpr u32 EEPROM_START_ADDRESS = 0x0801F800; // 第一页(1KB)
static constexpr u32 EEPROM_DATA_PAGE2_ADDRESS = 0x801FC00; // 第二页(1KB)
static constexpr u32 EEPROM_DATA_PAGE2_ADDRESS = 0x0801FC00; // 第二页(1KB)
static constexpr u32 EEPROM_END_ADDRESS = 0x08020000; // Flash结束地址
static constexpr u32 EEPROM_MAX_SIZE = 0x800; // EEPROM最大容量
static constexpr u32 EEPROM_DATA_START_ADDRESS =
@@ -67,6 +67,14 @@ class FlashEEPROM {
u8 read_saved_system_config(void);
float read_angle();
// 节点 ID 独立读写函数(使用魔数+双重备份+计数器机制)
// 从 Flash 读取节点 ID(带魔数验证和双重备份回退)
FlashEepromStatus readNodeId(u16 *node_id) const;
// 保存节点 ID 到 Flash(写入主备份和备用备份)
FlashEepromStatus writeNodeId(u16 node_id);
// 擦除节点 ID 存储区(恢复默认值)
FlashEepromStatus eraseNodeId();
private:
FlashEepromStatus writeCRC(u32 CRC_NUM, bool saved);
};
+43 -8
View File
@@ -4,6 +4,7 @@
#include "FOC.h"
#include "adc.h"
#include "flash.h"
#include "systick.h"
extern sampler sampler;
@@ -15,6 +16,10 @@ ThermalController thermalController; // 热量控制器初始化
#define max(a, b, c) \
((a) > (b) ? ((a) > (c) ? (a) : (c)) : ((b) > (c) ? (b) : (c)))
constexpr u16 PROTECT_TIME = 6000; // 过热保护时间,单位*10ms
// 电压管理状态机
static VoltageState voltage_state = VoltageState::NORMAL;
static float last_vbus_slow = 0.0f; // 上次电压值(慢速任务用)
/**
** @brief 热能计算
** @param None
@@ -90,18 +95,48 @@ void angsens_MG_check(void) {
CAN0_transmiter.MagStatus = MGH or MGL;
}
/**
* @brief 电压管理(低频任务,约10ms调用一次)
*
* 负责:
* 1. 执行Flash保存(阻塞操作,不能在高频循环中执行)
* 2. 状态恢复判断(基于基准电压的比例恢复)
* 3. 更新FOC的电源电压参数
*/
void power_manage(void) {
float last_vbus = sampler.get_vbus_voltage();
sampler.update_vbus_voltage();
FOC.update_power(last_vbus);
float vbus = sampler.get_vbus_voltage();
if ((vbus < 22.0f) and (last_vbus > vbus) and vbus > 8.0f) {
FOC.shut_down();
// 更新FOC的电源电压参数
FOC.update_power(last_vbus_slow);
last_vbus_slow = vbus;
// 如果快速检测已触发紧急关机,执行保存
if (FOC.emergency_shutdown) {
flashEEP.save_system_config(true);
} else {
// 只有在没有过热关机时才恢复
if (!FOC.thermal_shutdown) {
FOC.resume();
FOC.emergency_shutdown = false; // 清除紧急关机标志
voltage_state = VoltageState::SHUTDOWN_SAVE;
return;
}
// 状态机处理
switch (voltage_state) {
case VoltageState::NORMAL:
// 正常状态:无需操作
break;
case VoltageState::SHUTDOWN_SAVE:
// 关机保存状态:检查是否可以恢复
// 恢复条件:电压恢复到基准电压的90%以上
if (FOC.vbus_reference > 0 && vbus > FOC.vbus_reference * VBUS_RECOVERY_RATIO) {
if (!FOC.thermal_shutdown) {
FOC.resume();
voltage_state = VoltageState::NORMAL;
}
}
break;
default:
break;
}
}
+7
View File
@@ -10,6 +10,13 @@ typedef int32_t q16_16_t; // Q16.16格式
#define Q16_16_MUL(a, b) ((q16_16_t)(((int64_t)(a) * (b)) >> 16))
#define Q16_16_SQUARE(x) Q16_16_MUL((x), (x))
// 电压监测状态机状态
enum class VoltageState : uint8_t {
NORMAL, // 正常运行
VOLTAGE_DROP, // 电压跌落检测中(防抖等待)
SHUTDOWN_SAVE // 已关机,等待保存
};
class ThermalController {
private:
u8 temperature;