feat(canfd): 添加TX错误事件驱动的快速重发机制

添加TX错误事件标志和相关API以支持ARD模式下单发帧丢失的快速检测和重发,
实现停等式OTA在丢帧后的立即重发,避免等待ACK超时的延迟。

- 新增s_txErrorEvent数组跟踪各通道TX错误事件
- 添加readChannelErrorReg()读取CAN_ERR寄存器原始值
- 添加consumeTxError()消费TX错误事件并返回状态
- 在TX中断中检测MTE位设置错误事件标志

fix(motor-ota): 优化OTA重发策略和错误处理

调整OTA重发参数和逻辑,提升传输可靠性和响应速度。

- 将ACK超时重发间隔从500ms调整为100ms,提高响应速度
- 增加每包补发上限从2次到4次,保持总容错时间窗
- 实现TX错误驱动的快速重发,丢帧后立即按间隔补发
- 增加错误统计信息输出,包含重发和快速重发计数
- 提高仲裁段再同步跳跃宽度从1TQ到2TQ,增强总线容错能力
This commit is contained in:
cbb
2026-07-27 00:02:14 +08:00
parent 079c6442d4
commit edebde4a02
4 changed files with 91 additions and 11 deletions
+29
View File
@@ -165,6 +165,10 @@ uint32_t CanFd::s_feedbackSeq = 0U;
CanFd::IdSnapshotTable CanFd::s_idSnapshot[3]{}; CanFd::IdSnapshotTable CanFd::s_idSnapshot[3]{};
bool CanFd::s_channelHwOk[3]{}; bool CanFd::s_channelHwOk[3]{};
/* TX 错误事件标志(ARD=1 单发丢帧通知):TX 完成 ISR 置位,主循环
* consumeTxError() 读清;单写单读,无锁。 */
static volatile bool s_txErrorEvent[3]{};
/** /**
* @brief 查询指定通道是否已使能。 * @brief 查询指定通道是否已使能。
*/ */
@@ -1062,6 +1066,28 @@ void CanFd::setChannelAutoRetrans(uint8_t channelIndex, bool enable)
(CAN_CTL(canPeriph) & CAN_CTL_ARD) ? 1U : 0U); (CAN_CTL(canPeriph) & CAN_CTL_ARD) ? 1U : 0U);
} }
uint32_t CanFd::readChannelErrorReg(uint8_t channelIndex)
{
if (channelIndex >= kChannelCount || !kChannelEnabled[channelIndex]) {
return 0U;
}
/* 单次寄存器读,供事件时刻(如 OTA ACK 超时)抓取总线错误状态;
* 不走 1Hz refreshFeedbackSnapshot,避免采样混叠。 */
return CAN_ERR(kChannels[channelIndex].canPeriph);
}
bool CanFd::consumeTxError(uint8_t channelIndex)
{
if (channelIndex >= kChannelCount || !kChannelEnabled[channelIndex]) {
return false;
}
if (!s_txErrorEvent[channelIndex]) {
return false;
}
s_txErrorEvent[channelIndex] = false;
return true;
}
/** /**
* @brief 获取 CANFD 诊断统计信息只读指针。 * @brief 获取 CANFD 诊断统计信息只读指针。
* @param[in] 无。 * @param[in] 无。
@@ -1301,6 +1327,9 @@ void CanFd::handleTxInterrupt(uint8_t channelIndex)
} else if ((tstat & errorBits[mailbox]) != 0U) { } else if ((tstat & errorBits[mailbox]) != 0U) {
s_diag.error_count++; s_diag.error_count++;
s_channelStats[channelIndex].error_count++; s_channelStats[channelIndex].error_count++;
/* ARD=1 单发模式下 MTE 置位 = 该帧已确认丢失;置事件标志供停等式
* OTA 快速重发(免等 ACK 超时)。单写(ISR)单读清(主循环),无锁。 */
s_txErrorEvent[channelIndex] = true;
} }
} }
+8
View File
@@ -150,6 +150,14 @@ public:
/** @brief 运行时切换指定通道 CAN 自动重发(ARD 位,需 init-mode);停等式 OTA 期间关闭、结束后恢复。 */ /** @brief 运行时切换指定通道 CAN 自动重发(ARD 位,需 init-mode);停等式 OTA 期间关闭、结束后恢复。 */
static void setChannelAutoRetrans(uint8_t channelIndex, bool enable); static void setChannelAutoRetrans(uint8_t channelIndex, bool enable);
/** @brief 读取指定通道 CAN_ERR 寄存器原始值(事件时刻只读快照,不依赖 1Hz 采样)。
* 解码:TEC=bit16-23,REC=bit24-31,ERRN=bit4-6;无效通道返回 0。仅供诊断日志,禁用于发送门控。 */
static uint32_t readChannelErrorReg(uint8_t channelIndex);
/** @brief 读取并清除指定通道的 TX 错误事件(ARD=1 单发下 MTE 置位 = 帧已确认丢失)。
* 供停等式 OTA 在丢帧后立即重发,免等 ACK 超时;仅主循环调用。 */
static bool consumeTxError(uint8_t channelIndex);
static const Diag *diag(); static const Diag *diag();
/** /**
+5 -3
View File
@@ -80,7 +80,7 @@ constexpr bool CONFIG_CAN_CH2_ENABLE = false;
* ============================================================ */ * ============================================================ */
constexpr uint32_t APP_FW_VERSION_MAJOR = 1U; constexpr uint32_t APP_FW_VERSION_MAJOR = 1U;
constexpr uint32_t APP_FW_VERSION_MINOR = 4U; constexpr uint32_t APP_FW_VERSION_MINOR = 4U;
constexpr uint32_t APP_FW_VERSION_PATCH = 75U; /* 1.4.75:尾段fed>=fw_size旁路水位门控,修复末6包死锁 */ constexpr uint32_t APP_FW_VERSION_PATCH = 78U; /* 1.4.78:TX错误(MTE)事件驱动快速重发:丢帧后按gap立即补发免等100ms超时,日志加fast=计数 */
constexpr uint32_t APP_FW_VERSION_BUILD = 0U; constexpr uint32_t APP_FW_VERSION_BUILD = 0U;
@@ -163,8 +163,10 @@ constexpr uint32_t CONFIG_ESP32_RESET_PIN = GPIO_PIN_9;
/** @brief 仲裁段预分频值。 */ /** @brief 仲裁段预分频值。 */
constexpr uint32_t CONFIG_CAN_NOMINAL_PRESCALER = 9U; constexpr uint32_t CONFIG_CAN_NOMINAL_PRESCALER = 9U;
/** @brief 仲裁段再同步跳跃宽度。 */ /** @brief 仲裁段再同步跳跃宽度。
constexpr uint32_t CONFIG_CAN_NOMINAL_SJW = CAN_BT_SJW_1TQ; * 1TQ→2TQ:总线空闲期即持续 STUFF 接收错误(REC 冲 120+),OTA 数据帧被错误帧
* 击落触发应用层重发;放宽接收再同步容差(不改波特率/采样点,上限受 BS2=2TQ 限制)。 */
constexpr uint32_t CONFIG_CAN_NOMINAL_SJW = CAN_BT_SJW_2TQ;
/** @brief 仲裁段时间段 1(PHSEG1)。 */ /** @brief 仲裁段时间段 1(PHSEG1)。 */
constexpr uint32_t CONFIG_CAN_NOMINAL_BS1 = CAN_BT_BS1_7TQ; constexpr uint32_t CONFIG_CAN_NOMINAL_BS1 = CAN_BT_BS1_7TQ;
/** @brief 仲裁段时间段 2(PHSEG2)。 */ /** @brief 仲裁段时间段 2(PHSEG2)。 */
+49 -8
View File
@@ -273,6 +273,14 @@ static void setError(uint8_t code) {
static constexpr uint32_t kStepTimeoutMs = 100U; static constexpr uint32_t kStepTimeoutMs = 100U;
/* 补发次数上限:首发后最多再补发 kMaxResends 次;仍无反馈则判该步失败并打印。 */ /* 补发次数上限:首发后最多再补发 kMaxResends 次;仍无反馈则判该步失败并打印。 */
static constexpr uint8_t kMaxResends = 2U; static constexpr uint8_t kMaxResends = 2U;
/* 数据阶段 ACK 超时重发间隔(ms):正常 ACK 往返 ~11ms(电机 BOOT 主循环 ~10ms),
* 100ms 留 10 倍余量且避开电机偶发 flash 写慢。原 500ms 硬编码:11 次丢帧盲等
* 共 ~5.5s,占整场传输过半时间。电机对同包号幂等(只 ACK 不重复写 flash),
* 早发导致的双 ACK 由 processTransferAck 的重复 ACK 过滤兜底。 */
static constexpr uint32_t kAckResendMs = 100U;
/* 数据阶段每包补发上限:随 kAckResendMs 500→100 同步 2→4,保持总容错时间窗;
* 握手阶段仍用 kMaxResends,不动 v1.4.51 抗干扰逻辑。超限后由 process() 整体超时兜底。 */
static constexpr uint8_t kAckMaxResends = 4U;
/* 数据传输每包间隔(ms):正式发固件时每包也留间隔,防电机单缓冲 RX 高速下丢包/CRC 错。 /* 数据传输每包间隔(ms):正式发固件时每包也留间隔,防电机单缓冲 RX 高速下丢包/CRC 错。
* 现行:5ms/包(用户指定)。SPI 喂数必须能跟上;跟不上时靠水位门控暂停 CAN。 */ * 现行:5ms/包(用户指定)。SPI 喂数必须能跟上;跟不上时靠水位门控暂停 CAN。 */
static constexpr uint32_t kTransferGapMs = 5U; static constexpr uint32_t kTransferGapMs = 5U;
@@ -474,6 +482,8 @@ static void sendNextPacket() {
s_pkt_pending = true; s_pkt_pending = true;
s_retry_count = 0; /* 新数据包:重置补发计数 */ s_retry_count = 0; /* 新数据包:重置补发计数 */
s_state_enter_ms = SysTickTimer::getMs(); /* 新数据包:重置整体超时 backstop */ s_state_enter_ms = SysTickTimer::getMs(); /* 新数据包:重置整体超时 backstop */
/* 丢弃上一包/握手阶段残留的 TX 错误事件,避免新包刚发就被陈旧标志误触快速重发。 */
(void)CanFd::consumeTxError(s_channel);
/* 末包 padding 日志在 sendCurrentPacket 的 TX 行之前打印,且用最短格式, /* 末包 padding 日志在 sendCurrentPacket 的 TX 行之前打印,且用最短格式,
* 避免 10ms 高频 TX 期间被 UART 吞掉(之前放在这里会被紧接的 TX pkt= 行挤掉)。 */ * 避免 10ms 高频 TX 期间被 UART 吞掉(之前放在这里会被紧接的 TX pkt= 行挤掉)。 */
sendCurrentPacket(); sendCurrentPacket();
@@ -492,6 +502,8 @@ static uint8_t s_crc_retry_count = 0;
static uint16_t s_last_acked_pkt = 0xFFFFU; /* 上次确认的包号,用于识别重复 ACK */ static uint16_t s_last_acked_pkt = 0xFFFFU; /* 上次确认的包号,用于识别重复 ACK */
static uint32_t s_dup_ack_count = 0; /* 累计忽略的重复/未前进 ACK 帧数(诊断重传风暴) */ static uint32_t s_dup_ack_count = 0; /* 累计忽略的重复/未前进 ACK 帧数(诊断重传风暴) */
static uint32_t s_crc_err_count = 0; /* 累计收到的 0x06 CRC 错误帧数(含电机重传) */ static uint32_t s_crc_err_count = 0; /* 累计收到的 0x06 CRC 错误帧数(含电机重传) */
static uint32_t s_ack_resend_total = 0; /* 累计 ACK 超时重发次数(整场不清零,丢帧率基线) */
static uint32_t s_fast_resend_total = 0; /* 累计 TX 错误快速重发次数(MTE 事件驱动,免等超时) */
static void processTransferAck(const uint8_t *data, uint8_t len) { static void processTransferAck(const uint8_t *data, uint8_t len) {
/* 擦除等待窗口(首包尚未发出:s_packets_acked==0 且 !s_pkt_pending):电机重传的旧 /* 擦除等待窗口(首包尚未发出:s_packets_acked==0 且 !s_pkt_pending):电机重传的旧
@@ -567,9 +579,10 @@ if (next_pkt == 0xFFFFFFFFU) {
s_packets_acked = s_total_packets; s_packets_acked = s_total_packets;
s_last_acked_pkt = s_total_packets; s_last_acked_pkt = s_total_packets;
s_pkt_pending = false; s_pkt_pending = false;
printf("[MOTOR-OTA] FINISHED pkt=%u/%u dup=%lu crc=%lu t=%lu\r\n", printf("[MOTOR-OTA] FINISHED pkt=%u/%u dup=%lu crc=%lu resend=%lu fast=%lu t=%lu\r\n",
(unsigned)s_packets_acked, (unsigned)s_total_packets, (unsigned)s_packets_acked, (unsigned)s_total_packets,
(unsigned long)s_dup_ack_count, (unsigned long)s_crc_err_count, (unsigned long)s_dup_ack_count, (unsigned long)s_crc_err_count,
(unsigned long)s_ack_resend_total, (unsigned long)s_fast_resend_total,
(unsigned long)SysTickTimer::getMs()); (unsigned long)SysTickTimer::getMs());
enterState(State::kFinished); enterState(State::kFinished);
pushStatus(); pushStatus();
@@ -611,9 +624,10 @@ if (next_pkt == 0xFFFFFFFFU) {
* 与 0xD1 喂数抢总线,加重缓冲欠载(CANlog 可见周期性 ~500ms 空洞)。 * 与 0xD1 喂数抢总线,加重缓冲欠载(CANlog 可见周期性 ~500ms 空洞)。
* 进度由 ESP32 feedTask 按已喂字节估算;终态再 push。 */ * 进度由 ESP32 feedTask 按已喂字节估算;终态再 push。 */
if ((s_packets_acked & 0x1F) == 0) { if ((s_packets_acked & 0x1F) == 0) {
printf("[MOTOR-OTA] 进度 pkt=%u/%u dup=%lu crc=%lu t=%lu\r\n", printf("[MOTOR-OTA] 进度 pkt=%u/%u dup=%lu crc=%lu resend=%lu fast=%lu t=%lu\r\n",
(unsigned)s_packets_acked, (unsigned)s_total_packets, (unsigned)s_packets_acked, (unsigned)s_total_packets,
(unsigned long)s_dup_ack_count, (unsigned long)s_crc_err_count, (unsigned long)s_dup_ack_count, (unsigned long)s_crc_err_count,
(unsigned long)s_ack_resend_total, (unsigned long)s_fast_resend_total,
(unsigned long)SysTickTimer::getMs()); (unsigned long)SysTickTimer::getMs());
} }
} }
@@ -633,17 +647,42 @@ void onTxTick() {
const uint32_t now = SysTickTimer::getMs(); const uint32_t now = SysTickTimer::getMs();
/* ACK 超时重发:包已发出(s_pkt_pending=true)但超过 500ms 未收到 ACK,重发。 /* ACK 超时重发:包已发出(s_pkt_pending=true)但超过 kAckResendMs 未收到 ACK,重发。
* 场景:CAN 总线 ERR_PASSIVE(REC>128) 导致电机 ACK 丢失, * 场景:OTA 期间 ARD=1 单发,总线错误(如 STUFF)直接击落数据帧/ACK 丢失,
* GD32 等 ACK、电机等下一包 → 双方死锁。 * GD32 等 ACK、电机等下一包 → 双方死锁。
* ★ 注意:检查 s_pkt_pending(包已发等ACK),不是 s_ack_pending(ACK已到等处理)! * ★ 注意:检查 s_pkt_pending(包已发等ACK),不是 s_ack_pending(ACK已到等处理)!
* 重发后电机幂等处理(同包号只 ACK 不重复写 flash)。 */ * 重发后电机幂等处理(同包号只 ACK 不重复写 flash)。 */
if (s_pkt_pending && !s_ack_pending && !s_crc_resend_pending) { if (s_pkt_pending && !s_ack_pending && !s_crc_resend_pending) {
if (now - s_last_send_ms >= 500U) { /* 快速路径:TX 完成中断已确认当前帧被总线错误击落(MTE),免等
if (s_retry_count < kMaxResends) { * kAckResendMs 盲等,按 kTransferGapMs 节流立即重发。先判 gap 再 consume,
* 避免过早清标志丢事件;重发次数与超时路径共用 kAckMaxResends 上限。 */
if ((now - s_last_send_ms) >= kTransferGapMs
&& s_retry_count < kAckMaxResends
&& CanFd::consumeTxError(s_channel)) {
s_retry_count++; s_retry_count++;
printf("[MOTOR-OTA] ACK 超时重发 pkt=%u retry=%u\r\n", s_fast_resend_total++;
(unsigned)s_packets_acked, (unsigned)s_retry_count); const uint32_t err = CanFd::readChannelErrorReg(s_channel);
printf("[MOTOR-OTA] TX错快重发 pkt=%u retry=%u tec=%lu rec=%lu errn=%lu\r\n",
(unsigned)s_packets_acked, (unsigned)s_retry_count,
(unsigned long)((err >> 16) & 0xFFU),
(unsigned long)((err >> 24) & 0xFFU),
(unsigned long)((err >> 4) & 0x07U));
sendCurrentPacket();
s_last_send_ms = SysTickTimer::getMs();
return;
}
if (now - s_last_send_ms >= kAckResendMs) {
if (s_retry_count < kAckMaxResends) {
s_retry_count++;
s_ack_resend_total++;
/* 事件时刻 CAN_ERR 快照:钉死丢帧时总线状态(TEC 涨=数据帧发丢,
* REC 高=接收方向损坏),摆脱 1Hz [CAN-BUS] 采样混叠。仅日志,不做发送门控。 */
const uint32_t err = CanFd::readChannelErrorReg(s_channel);
printf("[MOTOR-OTA] ACK 超时重发 pkt=%u retry=%u tec=%lu rec=%lu errn=%lu\r\n",
(unsigned)s_packets_acked, (unsigned)s_retry_count,
(unsigned long)((err >> 16) & 0xFFU),
(unsigned long)((err >> 24) & 0xFFU),
(unsigned long)((err >> 4) & 0x07U));
sendCurrentPacket(); sendCurrentPacket();
s_last_send_ms = SysTickTimer::getMs(); s_last_send_ms = SysTickTimer::getMs();
} }
@@ -944,6 +983,8 @@ s_channel = can_channel;
s_pkt_pending = false; s_pkt_pending = false;
s_dup_ack_count = 0; s_dup_ack_count = 0;
s_crc_err_count = 0; s_crc_err_count = 0;
s_ack_resend_total = 0;
s_fast_resend_total = 0;
s_last_pad_valid = 0xFFFFU; s_last_pad_valid = 0xFFFFU;
/* 不在 SPI 中断里同步发握手帧(电机跳 BOOT 期间 CAN 可能短暂断开)。 /* 不在 SPI 中断里同步发握手帧(电机跳 BOOT 期间 CAN 可能短暂断开)。