From 40fea328b01845c72feed11aaa92ab84475b4572 Mon Sep 17 00:00:00 2001 From: cbb <2625274797@qq.com> Date: Sat, 12 Sep 2026 19:00:06 +0800 Subject: [PATCH] =?UTF-8?q?feat(can):=20=E5=8F=8D=E9=A6=88=E6=8A=A5?= =?UTF-8?q?=E6=96=87=E5=88=B7=E6=96=B0=E7=8E=87=E9=80=90=E5=B8=A7=E5=8F=AF?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E5=B9=B6=E6=8C=81=E4=B9=85=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit STATUS/CONFIG 反馈帧各自独立按 period_ms(1ms 节拍分频,0=停发)发送, 默认 STATUS 1kHz、CONFIG 250Hz,可运行时修改: - 0x0400 扩展格式(DLC=4):[选择器 u16][值 u16],选择器 0x0000=节点 ID (仅单播),0x0100/0x0101=对应帧刷新周期;回 5B ACK,广播只应用不应答 - 0x0401 扩展格式(DLC=2):查询选择器对应值,回 [sel][val](4B); DLC<=1 及 SET DLC 1~2 保持旧格式行为完全不变 - 刷新率经 FlashEEPROM 独立 u16 记录(虚拟地址 0x08/0x09)持久化, 不并入 6 参数 CRC 组,避免老设备升级后整组参数回退默认值 - save_system_config 追加从 RAM 副本回写两条记录,页满 format() 重建后配置不丢失;read() 增加保留字节校验防 CRC 头部字误匹配 - CANFD_all_Send 拆分为 send_status_frame/send_config_frame, 初始化失败上报路径直调 STATUS 帧不受分频影响 GATE_DRIVER_DRV8323 0/1 两种板型均编译通过 --- Robot_V2/device/Can.cpp | 206 +++++++++++++++++++++++++++++++++------- Robot_V2/device/Can.h | 48 ++++++++-- Robot_V2/main.cpp | 2 +- Robot_V2/sys/config.h | 9 ++ Robot_V2/sys/flash.cpp | 52 +++++++++- Robot_V2/sys/flash.h | 15 +++ 6 files changed, 283 insertions(+), 49 deletions(-) diff --git a/Robot_V2/device/Can.cpp b/Robot_V2/device/Can.cpp index ac08ef4..465f36e 100644 --- a/Robot_V2/device/Can.cpp +++ b/Robot_V2/device/Can.cpp @@ -448,6 +448,13 @@ u8 CAN_transmiter::init() { // 从 Flash 加载节点 ID (如果 Flash 中有有效值则覆盖默认值) load_node_id_from_flash(); + + // 加载周期反馈帧刷新率(flashEEP.init() 已从 Flash 读入 RAM 副本, + // 无记录时为其默认值),注意 hardware_init 保证 flashEEP.init() 先执行 + status_period_ms = flashEEP.tx_period_status_ms; + config_period_ms = flashEEP.tx_period_config_ms; + status_period_counter = 0; + config_period_counter = 0; return 0; } @@ -772,19 +779,13 @@ void CAN_transmiter::update(void) { } /** - * @brief CANFD 发送函数 - * @retval 0: 发送失败(或处于静默模式) + * @brief 打包并发送 STATUS 反馈帧(实时状态 + 初始化错误位图) + * @note 不做分频与静默检查,由周期任务按 status_period_ms 分频调用; + * 初始化失败上报路径(main.cpp)也直接调用 + * @retval 0: 发送失败 * 1: 发送成功 */ -u8 CAN_transmiter::CANFD_all_Send() { - // 检查是否在静默模式中,如果是则不发送 - if (is_in_silent_mode()) { - check_silent_mode(); // 检查是否已退出静默 - return 0; - } - - u32 success_flag = 0; - +u8 CAN_transmiter::send_status_frame() { // 打包 status 数据 - 只清零实际使用的 48 字节 // v0.74:字节 32~35 新增 InitStatus(初始化错误码位图,0=全部正常, // 定义见 config.h INIT_ERR_*,含电流检测配平超限/无采样帧),36~47 预留 @@ -808,9 +809,14 @@ u8 CAN_transmiter::CANFD_all_Send() { transmit_data[31] = cpu_usage; pack_u32_to_bytes(transmit_data, 32, (u32)init_error_flags); // 使用节点 ID 构建发送 ID (高 16 位为节点 ID) - success_flag += - CANFD_AsyncSend(build_can_id(CANFD_STATUS_ID), 48, transmit_data); + return CANFD_AsyncSend(build_can_id(CANFD_STATUS_ID), 48, transmit_data); +} +/** + * @brief 打包并发送 CONFIG 反馈帧(控制模式与目标设定回显) + * @note 不做分频与静默检查,由周期任务按 config_period_ms 分频调用 + */ +void CAN_transmiter::send_config_frame() { // 打包 config 数据 - 只清零实际使用的 24 字节 memset(transmit_data, 0, 24); transmit_data[0] = controller_Mode; @@ -819,10 +825,7 @@ u8 CAN_transmiter::CANFD_all_Send() { pack_float_to_bytes(transmit_data, 7, target_velocity_union); pack_float_to_bytes(transmit_data, 11, target_angle_union); // 使用节点 ID 构建发送 ID (高 16 位为节点 ID) - success_flag += - CANFD_AsyncSend(build_can_id(CANFD_CONFIG_ID), 24, transmit_data); - - return success_flag; + CANFD_AsyncSend(build_can_id(CANFD_CONFIG_ID), 24, transmit_data); } /** @@ -846,7 +849,19 @@ void CAN_transmiter::handle_periodic_send_tick() { if (!is_in_silent_mode()) { update(); - CANFD_all_Send(); + // STATUS 与 CONFIG 各自独立分频(period_ms=0 停发),节拍仍为 + // 1ms(Timer.cpp task_schedule_100us[9] = TASK_CAN_TRANSMIT), + // 修改周期经 SET 命令(0x0400 扩展格式)即时生效并清零计数器 + if (status_period_ms != 0U && + ++status_period_counter >= status_period_ms) { + status_period_counter = 0; + send_status_frame(); + } + if (config_period_ms != 0U && + ++config_period_counter >= config_period_ms) { + config_period_counter = 0; + send_config_frame(); + } } else { check_silent_mode(); } @@ -903,6 +918,27 @@ void CAN_transmiter::handle_boot_message(u16 sender_node_id) { jumpToBOOT(); } +/** + * @brief 校验并保存新节点 ID 到 Flash(不组应答帧) + * @param new_id: 新节点 ID + * @retval 1: 成功(save_node_id_to_flash 内部已更新 node_id) + * 0: 非法 ID 或 Flash 写入失败(node_id 保持不变) + */ +u8 CAN_transmiter::apply_new_node_id(u16 new_id) { + if (new_id < 1 || new_id > SETTABLE_NODE_ID_MAX) { + CAN_DEBUG_PRINT("SET_NODE_ID: invalid new_id=%d (range 1~%d)\r\n", + new_id, SETTABLE_NODE_ID_MAX); + return 0; + } + + // 先写 Flash,成功后再由调用方回确认,避免 Flash 失败时上位机已收到 OK。 + u8 save_status = save_node_id_to_flash(new_id); + + CAN_DEBUG_PRINT("SET_NODE_ID: save_status=%d\r\n", save_status); + + return (save_status == FLASH_EEPROM_OK) ? 1U : 0U; +} + void CAN_transmiter::handle_set_node_id_message(can_receive_message_struct ¤t_message) { // ── 防护:只处理发给本节点(或广播)的 SET_NODE_ID ── u16 target_node = (u16)((current_message.rx_efid >> 16) & 0xFFFF); @@ -910,6 +946,13 @@ void CAN_transmiter::handle_set_node_id_message(can_receive_message_struct &curr return; } + // DLC=4 为扩展格式参数设置:[选择器 u16][值 u16], + // 选择器 0x0000=节点 ID,反馈帧基础 ID=刷新周期(ms) + if (current_message.rx_dlen == 4) { + handle_set_param_message(current_message); + return; + } + // 支持 1 字节和 2 字节节点 ID 命令,兼容旧上位机。 CAN_DEBUG_PRINT( "SET_NODE_ID: rx_dlen=%d, data[0]=%02X data[1]=%02X\r\n", @@ -928,20 +971,7 @@ void CAN_transmiter::handle_set_node_id_message(can_receive_message_struct &curr // 响应帧使用旧节点 ID 构造,确保上位机能收到确认。 node_id = old_node_id; - if (new_node_id < 1 || new_node_id > SETTABLE_NODE_ID_MAX) { - CAN_DEBUG_PRINT("SET_NODE_ID: invalid new_id=%d (range 1~%d)\r\n", - new_node_id, SETTABLE_NODE_ID_MAX); - transmit_data[0] = 0x00; - CANFD_AsyncSend(build_can_id(CANFD_SET_NODE_ID), 3, transmit_data); - return; - } - - // 先写 Flash,成功后再回确认,避免 Flash 失败时上位机已收到 OK。 - u8 save_status = save_node_id_to_flash(new_node_id); - - CAN_DEBUG_PRINT("SET_NODE_ID: save_status=%d\r\n", save_status); - - if (save_status == FLASH_EEPROM_OK) { + if (apply_new_node_id(new_node_id)) { transmit_data[0] = 0x01; CANFD_AsyncSend(build_can_id(CANFD_SET_NODE_ID), 3, transmit_data); CAN_DEBUG_PRINT("SET_NODE_ID: SUCCESS, node_id updated from %d to %d\r\n", @@ -949,12 +979,91 @@ void CAN_transmiter::handle_set_node_id_message(can_receive_message_struct &curr return; } - CAN_DEBUG_PRINT("SET_NODE_ID: Flash write FAILED, status=%d\r\n", save_status); + // 非法 ID 或 Flash 写入失败:node_id 保持旧值,NAK transmit_data[0] = 0x00; - node_id = old_node_id; CANFD_AsyncSend(build_can_id(CANFD_SET_NODE_ID), 3, transmit_data); } +/** + * @brief 处理 0x0400 扩展格式参数设置(DLC=4) + * @param current_message: [选择器 u16 LE][值 u16 LE] + * 选择器 0x0000=节点 ID(仅单播),反馈帧基础 ID=该帧刷新周期 ms(0=停发) + * @note 写 Flash 成功后立即生效;广播目标只应用不应答 + * (多节点同时应答会占总线冲突),单播回 5 字节应答 + * [status, sel_lo, sel_hi, val_lo, val_hi] + */ +void CAN_transmiter::handle_set_param_message(can_receive_message_struct ¤t_message) { + u16 selector = unpack_bytes_to_u16(current_message.rx_data, 0); + u16 value = unpack_bytes_to_u16(current_message.rx_data, 2); + // 目标已由 handle_set_node_id_message 过滤为本节点或广播 + u16 target_node = (u16)((current_message.rx_efid >> 16) & 0xFFFF); + bool is_broadcast = (target_node == BROADCAST_NODE_ID); + u8 ok = 0; + + if (selector == CANFD_PARAM_NODE_ID) { + // 广播改节点 ID 会使所有节点同 ID,仅允许单播,广播时直接忽略 + if (is_broadcast) return; + ok = apply_new_node_id(value); + } else if (selector == CANFD_STATUS_ID || selector == CANFD_CONFIG_ID) { + ok = set_tx_period(selector, value); + } else { + CAN_DEBUG_PRINT("SET_PARAM: unknown selector 0x%04X\r\n", selector); + } + + if (is_broadcast) return; + + transmit_data[0] = ok ? 0x01 : 0x00; + pack_u16_to_bytes(transmit_data, 1, selector); + pack_u16_to_bytes(transmit_data, 3, value); + // 节点 ID 修改成功时 node_id 已更新,应答以新节点 ID 发出(与旧格式一致) + CANFD_AsyncSend(build_can_id(CANFD_SET_NODE_ID), 5, transmit_data); +} + +/** + * @brief 设置反馈帧刷新周期并持久化到 Flash + * @param base_id: 反馈帧基础 ID(CANFD_STATUS_ID/CANFD_CONFIG_ID) + * @param period_ms: 刷新周期 ms(0=停发,上限 1kHz 由 1ms 节拍决定) + * @retval 1: 成功且已立即生效 + * 0: 未知帧 ID 或 Flash 写入失败(不生效) + */ +u8 CAN_transmiter::set_tx_period(u16 base_id, u16 period_ms) { + u16 old_period; + if (base_id == CANFD_STATUS_ID) { + old_period = flashEEP.tx_period_status_ms; + flashEEP.tx_period_status_ms = period_ms; + } else if (base_id == CANFD_CONFIG_ID) { + old_period = flashEEP.tx_period_config_ms; + flashEEP.tx_period_config_ms = period_ms; + } else { + return 0; + } + + // 先写 Flash 成功后再生效,与 SET_NODE_ID 的"写即存"握手一致; + // 页满 format() 重建时 save_system_config 会从 RAM 副本恢复本次修改 + if (flashEEP.write_tx_periods() != FLASH_EEPROM_OK) { + // 写入失败回滚 RAM 副本,保证失败不残留(否则关机保存路径 + // 会把未确认的值持久化) + if (base_id == CANFD_STATUS_ID) { + flashEEP.tx_period_status_ms = old_period; + } else { + flashEEP.tx_period_config_ms = old_period; + } + CAN_DEBUG_PRINT("SET_PARAM: tx period 0x%04X flash write failed\r\n", + base_id); + return 0; + } + + if (base_id == CANFD_STATUS_ID) { + status_period_ms = period_ms; + status_period_counter = 0; + } else { + config_period_ms = period_ms; + config_period_counter = 0; + } + CAN_DEBUG_PRINT("SET_PARAM: base 0x%04X period %d ms\r\n", base_id, period_ms); + return 1; +} + void CAN_transmiter::handle_node_message(can_receive_message_struct ¤t_message, u32 base_id) { // 这里只处理已经确认发给本节点的普通协议消息。 @@ -967,8 +1076,33 @@ void CAN_transmiter::handle_node_message(can_receive_message_struct ¤t_mes } else if (base_id == CANFD_OTA_ACK_ID) { handle_ota_ack_message(); } else if (base_id == CANFD_GET_NODE_ID) { - pack_u16_to_bytes(transmit_data, 0, get_node_id()); - CANFD_AsyncSend(build_can_id(CANFD_GET_NODE_ID), 2, transmit_data); + // 仅 DLC=2 判为扩展格式 [选择器 u16 LE],避免旧上位机带填充的 + // 长 DLC 查询被误解析;其余长度按旧格式回节点 ID(2 字节) + if (current_message.rx_dlen == 2) { + // 扩展格式:查询参数 [选择器 u16 LE] + // 应答 [sel_lo, sel_hi, val_lo, val_hi](4 字节) + u16 selector = unpack_bytes_to_u16(current_message.rx_data, 0); + u16 value = 0; + if (selector == CANFD_PARAM_NODE_ID) { + value = get_node_id(); + } else if (selector == CANFD_STATUS_ID) { + value = status_period_ms; + } else if (selector == CANFD_CONFIG_ID) { + value = config_period_ms; + } else { + // 未知选择器回 0(0 对周期含义为停发,注意与真实配置区分; + // 权威写路径 0x0400 有显式状态字节) + CAN_DEBUG_PRINT("GET_PARAM: unknown selector 0x%04X\r\n", + selector); + } + pack_u16_to_bytes(transmit_data, 0, selector); + pack_u16_to_bytes(transmit_data, 2, value); + CANFD_AsyncSend(build_can_id(CANFD_GET_NODE_ID), 4, transmit_data); + } else { + // 旧格式:回节点 ID(2 字节),兼容旧上位机 + pack_u16_to_bytes(transmit_data, 0, get_node_id()); + CANFD_AsyncSend(build_can_id(CANFD_GET_NODE_ID), 2, transmit_data); + } } else { CAN_DEBUG_PRINT("Unknown CAN ID: %X (node_id=%d)\n", current_message.rx_efid, node_id); diff --git a/Robot_V2/device/Can.h b/Robot_V2/device/Can.h index d53606d..65c6bd8 100644 --- a/Robot_V2/device/Can.h +++ b/Robot_V2/device/Can.h @@ -214,6 +214,13 @@ class CAN_transmiter { u8 transmit_mailbox = CAN_NOMAILBOX; // 初始化为无 mailbox 状态 u8 transmit_data[64]; // CANFD 最大 64 字节 + // 周期反馈帧独立刷新率(ms,0=停发)与 1ms 节拍分频计数器。 + // init() 从 flashEEP 的 RAM 副本加载,SET 命令(0x0400 扩展格式)运行时修改 + u16 status_period_ms = CANFD_TX_PERIOD_STATUS_DEFAULT_MS; + u16 status_period_counter = 0; + u16 config_period_ms = CANFD_TX_PERIOD_CONFIG_DEFAULT_MS; + u16 config_period_counter = 0; + // CAN 接收消息队列 can_receive_message_struct rx_message_buffer[CAN_RX_BUFFER_SIZE]; volatile uint8_t rx_buffer_head = 0; @@ -224,6 +231,11 @@ class CAN_transmiter { * @return void */ void update(); // 更新数据 + /** + * @brief 打包并发送 CONFIG 反馈帧(0x0101,控制模式/目标设定回显) + * @return void + */ + void send_config_frame(); // CONFIG 反馈帧发送 /** * @brief CAN_Receive 函数 * @return u8 返回值 @@ -289,6 +301,26 @@ class CAN_transmiter { * @return void */ void handle_set_node_id_message(can_receive_message_struct ¤t_message); + /** + * @brief 处理 0x0400 扩展格式参数设置(DLC=4:[选择器 u16][值 u16]), + * 选择器 0x0000=节点 ID、反馈帧基础 ID=对应帧刷新周期(ms) + * @param[in,out] current_message 当前 CAN 消息 + * @return void + */ + void handle_set_param_message(can_receive_message_struct ¤t_message); + /** + * @brief 校验并保存新节点 ID 到 Flash(不修改应答帧) + * @param[in] new_id 新节点 ID + * @return u8 1=成功,0=非法 ID 或 Flash 写入失败 + */ + u8 apply_new_node_id(u16 new_id); + /** + * @brief 设置反馈帧刷新周期并持久化到 Flash + * @param[in] base_id 反馈帧基础 ID(CANFD_STATUS_ID/CANFD_CONFIG_ID) + * @param[in] period_ms 刷新周期 ms(0=停发) + * @return u8 1=成功,0=未知帧或 Flash 写入失败 + */ + u8 set_tx_period(u16 base_id, u16 period_ms); /** * @brief 处理已经确认发给本节点的普通协议消息 * @param[in,out] current_message 当前 CAN 消息 @@ -350,6 +382,12 @@ class CAN_transmiter { * @return void */ void communication_check(); // 通信检查 + /** + * @brief 打包并发送 STATUS 反馈帧(0x0100,实时状态与错误位图)。 + * 不做分频与静默检查,初始化失败上报路径直接调用 + * @return u8 1=已提交发送,0=发送失败 + */ + u8 send_status_frame(); // STATUS 反馈帧发送 /** * @brief CAN_Sleep 函数 * @return u8 返回值 @@ -376,16 +414,6 @@ class CAN_transmiter { * @return u8 返回值 */ u8 CANFD_AsyncSend(u32 ID, u8 length, u8 *data); // CANFD 异步发送 (使用 DMA) - /** - * @brief CAN_all_Send 函数 - * @return u8 返回值 - */ - u8 CAN_all_Send(); // CAN 发送 - /** - * @brief CANFD_all_Send 函数 - * @return u8 返回值 - */ - u8 CANFD_all_Send(); // CANFD 发送 /** * @brief enter_silent_mode 函数 * @return void diff --git a/Robot_V2/main.cpp b/Robot_V2/main.cpp index 537efc3..d5f5ae3 100644 --- a/Robot_V2/main.cpp +++ b/Robot_V2/main.cpp @@ -56,7 +56,7 @@ int main(void) { // 初始化失败:持续通过 CAN STATUS 帧上报 InitStatus 错误码 // (含电流检测配平超限/无采样帧),上位机无需调试串口即可识别故障模块 printf("初始化失败 InitStatus=0x%08X\r\n", init_error_flags); - CAN0_transmiter.CANFD_all_Send(); // STATUS 帧字节 32~35 携带错误位图 + CAN0_transmiter.send_status_frame(); // STATUS 帧字节 32~35 携带错误位图 delay_1ms(100); } OTA_initial(); // OTA 初始化,A/B 分区处理 diff --git a/Robot_V2/sys/config.h b/Robot_V2/sys/config.h index 3f84fba..052126d 100644 --- a/Robot_V2/sys/config.h +++ b/Robot_V2/sys/config.h @@ -331,6 +331,15 @@ constexpr u8 CONFIG_SAVED_NUM = 6; constexpr u16 CANFD_STATUS_ID = 0x0100; // 状态反馈基础 ID constexpr u16 CANFD_CONFIG_ID = 0x0101; // 配置信息基础 ID constexpr u16 CANFD_VERSION_ID = 0x00FF; // 版本信息基础 ID +// 周期反馈帧刷新周期(单位 ms,基于 1ms CAN 任务节拍分频;0=停发), +// 可通过 SET_NODE_ID(0x0400) 扩展格式运行时修改并持久化, +// 未配置时使用以下默认值(CONFIG 帧内容为控制模式/目标设定回显, +// 不实时依赖,默认 250Hz 降低总线占用) +constexpr u16 CANFD_TX_PERIOD_STATUS_DEFAULT_MS = 1; // STATUS 帧默认 1kHz +constexpr u16 CANFD_TX_PERIOD_CONFIG_DEFAULT_MS = 4; // CONFIG 帧默认 250Hz +// 0x0400/0x0401 扩展格式(DLC>=2/4)的参数选择器:0x0000=节点 ID, +// 其余选择器直接使用目标反馈帧的基础 ID(如 0x0100/0x0101) +constexpr u16 CANFD_PARAM_NODE_ID = 0x0000; // 命令基础 ID constexpr u16 CANFD_CMD_ID = 0x0200; // 控制命令基础 ID constexpr u16 CANFD_RESET_ID = 0x0301; // 重置命令基础 ID diff --git a/Robot_V2/sys/flash.cpp b/Robot_V2/sys/flash.cpp index 5e55295..13d6bf9 100644 --- a/Robot_V2/sys/flash.cpp +++ b/Robot_V2/sys/flash.cpp @@ -83,6 +83,18 @@ u8 FlashEEPROM::init() { } KTH7823.set_calibration(calib); + // 加载周期反馈帧刷新率(独立 u16 记录,无记录/读取失败回退默认值, + // 不影响 EEPROMStatus 的 CRC 参数组判定) + u16 tx_period = 0; + tx_period_status_ms = + (read(ADDR_TX_PERIOD_STATUS, &tx_period) == FLASH_EEPROM_OK) + ? tx_period + : CANFD_TX_PERIOD_STATUS_DEFAULT_MS; + tx_period_config_ms = + (read(ADDR_TX_PERIOD_CONFIG, &tx_period) == FLASH_EEPROM_OK) + ? tx_period + : CANFD_TX_PERIOD_CONFIG_DEFAULT_MS; + if (EEPROMStatus == FLASH_EEPROM_OK) { return FLASH_EEPROM_OK; } else { @@ -189,8 +201,9 @@ FlashEepromStatus FlashEEPROM::read(u8 virtualAddr, u16 *data) const { // 检查是否是有效数据(非擦除状态) if (data_word != 0xFFFFFFFF) { u8 addr = (u8)(data_word & DATA_ADDR_MASK); - // 找到匹配的虚拟地址 - if (addr == virtualAddr) { + // u16 记录保留字节恒为 0(write() 写入格式 [data:16][rsvd:8][addr:8]), + // 借此排除 EEPROM 头部区 CRC 字(低 8 位恰为虚拟地址)的误匹配 + if (addr == virtualAddr && (data_word & 0x0000FF00) == 0) { // 右移 16 位以提取高 16 位的 u16 数据 *data = (u16)((data_word & DATA_U16_MASK) >> 16); EEP_DEBUG_PRINT("read u16: addr=0x%x, val=%d\r\n", address, *data); @@ -487,10 +500,45 @@ FlashEepromStatus FlashEEPROM::save_system_config(bool saved) { CRC_NUM = crc_block_data_calculate(crc_data_words, CONFIG_SAVED_NUM); writeCRC(CRC_NUM, saved); + // 周期反馈帧刷新率(独立记录,非 CRC 组):从 RAM 副本追加写入。 + // 放在 CRC 组保存之后,写入失败不影响参数组状态(下次上电回退默认值); + // 页满 format() 重建时也经此路径从 RAM 副本恢复,刷新率配置不丢失 + status = write(ADDR_TX_PERIOD_STATUS, tx_period_status_ms); + if (status != FLASH_EEPROM_OK) { + EEP_DEBUG_PRINT("save tx period STATUS failed (%d)\r\n", status); + } + status = write(ADDR_TX_PERIOD_CONFIG, tx_period_config_ms); + if (status != FLASH_EEPROM_OK) { + EEP_DEBUG_PRINT("save tx period CONFIG failed (%d)\r\n", status); + } + EEPROMStatus = FLASH_EEPROM_OK; return FLASH_EEPROM_OK; } +/** + * @brief 将 RAM 副本中的两帧刷新周期写入 Flash + * @note 独立 u16 记录(非 CRC 参数组),值不变时 write() 自动跳过以减少 + * 磨损。运行时 SET 命令修改 RAM 副本后调用本函数持久化;若触发 + * 页满 format() 重建,save_system_config() 会从 RAM 副本重写这两 + * 条记录,本次修改不会丢失。 + * @return FlashEepromStatus 操作状态 + */ +FlashEepromStatus FlashEEPROM::write_tx_periods() { + FlashEepromStatus status = write(ADDR_TX_PERIOD_STATUS, tx_period_status_ms); + if (status != FLASH_EEPROM_OK) { + EEP_DEBUG_PRINT("write_tx_periods: STATUS period write failed (%d)\r\n", + status); + return status; + } + status = write(ADDR_TX_PERIOD_CONFIG, tx_period_config_ms); + if (status != FLASH_EEPROM_OK) { + EEP_DEBUG_PRINT("write_tx_periods: CONFIG period write failed (%d)\r\n", + status); + } + return status; +} + /** * @brief 保存电角度零位校准角到 Flash diff --git a/Robot_V2/sys/flash.h b/Robot_V2/sys/flash.h index 409897d..37ef7bf 100644 --- a/Robot_V2/sys/flash.h +++ b/Robot_V2/sys/flash.h @@ -57,9 +57,16 @@ class FlashEEPROM { static constexpr u8 ADDR_VEL_LIMIT = 0x05; // 角度数据存储地址 static constexpr u8 ADDR_NODE_ID = 0x06; // 节点 ID 存储地址 static constexpr u8 ADDR_CALIBRATION_ANGLE = 0x07; // 电角度零位校准角存储地址 + static constexpr u8 ADDR_TX_PERIOD_STATUS = 0x08; // STATUS 帧刷新周期(ms)存储地址 + static constexpr u8 ADDR_TX_PERIOD_CONFIG = 0x09; // CONFIG 帧刷新周期(ms)存储地址 bool system_config_saved = false; u8 EEPROMStatus; + // 周期反馈帧刷新周期 RAM 副本(ms,0=停发)。init() 从 Flash 加载, + // 无记录时回退 config.h 默认值;CAN_transmiter::init() 读取应用, + // SET 命令修改后经 write_tx_periods() 持久化 + u16 tx_period_status_ms = CANFD_TX_PERIOD_STATUS_DEFAULT_MS; + u16 tx_period_config_ms = CANFD_TX_PERIOD_CONFIG_DEFAULT_MS; // 构造函数 FlashEEPROM(); @@ -128,6 +135,14 @@ class FlashEEPROM { */ FlashEepromStatus save_calibration_angle(float angle); + // 周期反馈帧刷新率持久化(独立 u16 记录,不属于 CRC 参数组, + // 读取失败/无记录由调用方回退默认值,避免老设备升级后 CRC 全组失效) + /** + * @brief 将 RAM 副本中的两帧刷新周期写入 Flash(值不变自动跳过) + * @return FlashEepromStatus 操作状态 + */ + FlashEepromStatus write_tx_periods(); + // 节点 ID 独立读写函数(主/备双备份分属独立 Flash 页 + 魔数 + 计数器 + CRC) // 从 Flash 读取节点 ID(双备份均有效取计数器最新,单份有效则回退) /**