feat(can): 添加跨通道CAN突发发送功能支持多电机同步驱动

- 新增 /can/send_burst HTTP接口,支持一次SPI事务批量发送多帧CAN消息
- 实现canFdSendBurst函数,通过0xC7 SPI类型实现背靠背装载各通道邮箱
- 添加canBurstCapacity函数计算单次突发最大帧数限制
- 突发发送期间暂停can_feedback_task避免SPI抢占冲突

refactor(can_feedback_task): 改造为周期查询+事件消费模式

- 0xC4/0xC5从GD32主动推送改为ESP32周期查询(250ms/1s)
- 0xC6/0xD2仍保留事件驱动推送模式,由任务消费处理
- 消除OTA期间GD32悬空按铃导致的超时刷屏问题
- 电机OTA期间也暂停任务避免与0xD1抢夺SPI资源

perf(ota_relay): 增加重发次数上限提升传输可靠性

- 将kMaxDataSends从3次增加到5次,提高数据包传输成功率

chore(version): 更新固件版本号至0.3.78

- 版本查询checkVersionWait移到tickInFlight释放后异步触发
- 避免/motor/version请求拖长tick导致tickStuck误报通道闪黄
This commit is contained in:
小龙虾
2026-08-10 00:54:12 +08:00
parent f9cbcc396b
commit ef6d3a4be3
23 changed files with 2524 additions and 157 deletions
+107
View File
@@ -20,10 +20,15 @@
namespace {
constexpr uint8_t kSpiCanFrameType = 0xC0U;
constexpr uint8_t kSpiCanBurstType = 0xC7U;
constexpr uint8_t kSpiCanSetModeType = 0xC2U;
constexpr uint8_t kFeedbackFixedHeader = 28U;
constexpr uint8_t kFeedbackMaxPage = 1U; /* GD32 仅 page 0/1 */
/* 突发 payload 上限 = V2_MAX_PAYLOAD(236);头 4B = count+dlc+suffix(2)。 */
constexpr uint8_t kBurstHeaderLen = 4U;
constexpr uint16_t kBurstPayloadCap = 236U;
/**
* @brief 经 SPI 发送一帧 CAN,并解析 ACK/ERROR。
*
@@ -169,6 +174,108 @@ int canFdSend(can_channel_t channel, uint32_t ext_id,
recv_buf, recv_max, timeout_ms, gd32_err_out);
}
/**
* @brief 给定统一 DLC,返回单次突发最多可容纳的帧数。
* payload = 4B 头 + count*(1+dlc),须 ≤ 236B;并受 GD32 硬上限约束。
*/
uint8_t canBurstCapacity(uint8_t dlc)
{
if (dlc > CAN_MAX_DLC) {
return 0U;
}
const uint16_t entry_len = static_cast<uint16_t>(1U + dlc);
const uint16_t avail = (kBurstPayloadCap > kBurstHeaderLen)
? static_cast<uint16_t>(kBurstPayloadCap - kBurstHeaderLen)
: 0U;
const uint16_t cap = static_cast<uint16_t>(avail / entry_len);
return static_cast<uint8_t>((cap < CAN_BURST_HARD_MAX) ? cap : CAN_BURST_HARD_MAX);
}
/**
* @brief 发送一次 CAN 突发(SPI 0xC7)。
*
* GD32 在 CS 释放 ISR 内对每帧调用非阻塞 transmitFrame(仅装载邮箱),
* 全部装载完成后才回 ACK,因此各通道帧近同时上线;ESP32 这里等待的只是
* 这一个 ACK,并非逐帧等待,不会破坏组内同时性。
*
* 返回值: count 成功, -1 参数错误, -2 SPI 通信失败, -3 GD32 拒绝。
*/
int canFdSendBurst(uint16_t id_suffix, uint8_t dlc,
const can_burst_entry_t *entries, uint8_t count,
uint8_t *status_out, uint8_t status_max,
uint32_t timeout_ms, uint8_t *gd32_err_out)
{
if (gd32_err_out) {
*gd32_err_out = 0U;
}
if (dlc > CAN_MAX_DLC || entries == nullptr || count == 0U) {
return -1;
}
if (count > canBurstCapacity(dlc)) {
return -1;
}
uint8_t tx_buf[240];
size_t tx_len = 0U;
tx_buf[tx_len++] = count;
tx_buf[tx_len++] = dlc;
tx_buf[tx_len++] = static_cast<uint8_t>((id_suffix >> 8) & 0xFFU);
tx_buf[tx_len++] = static_cast<uint8_t>(id_suffix & 0xFFU);
for (uint8_t i = 0U; i < count; ++i) {
tx_buf[tx_len++] = entries[i].chnode;
if (dlc > 0U) {
if (entries[i].data == nullptr) {
return -1;
}
memcpy(&tx_buf[tx_len], entries[i].data, dlc);
tx_len += dlc;
}
}
uint8_t resp[32];
uint8_t resp_type = 0U;
size_t resp_len = 0U;
esp_err_t ret = ESP_OK;
/* 与 0xC0 相同:排空可能残留的陈旧 WAIT,直到读到本次真正的 ACK/ERROR */
for (uint8_t attempt = 0U; attempt < 4U; ++attempt) {
resp_type = 0U;
resp_len = 0U;
ret = SpiMaster::instance().transact(
kSpiCanBurstType, tx_buf, tx_len,
&resp_type, resp, sizeof(resp), &resp_len, timeout_ms);
if (resp_type != SPI_OTA_RSP_WAIT) {
break;
}
vTaskDelay(pdMS_TO_TICKS(3));
}
if (ret != ESP_OK) {
DEBUG_WARN("CAN", "BURST SPI fail n=%u %s",
(unsigned)count, esp_err_to_name(ret));
return -2;
}
if (resp_type == SPI_OTA_RSP_ACK) {
/* resp[0]=count, resp[1..count]=每帧状态(CanTxResult 码) */
const uint8_t n = (resp_len > 0U) ? resp[0] : 0U;
if (status_out != nullptr && status_max > 0U) {
const uint8_t copy_n = (n < status_max) ? n : status_max;
for (uint8_t i = 0U; i < copy_n; ++i) {
status_out[i] = (static_cast<size_t>(1U + i) < resp_len)
? resp[1U + i] : 0xFFU;
}
}
return static_cast<int>(n);
}
const uint8_t gd32_err = (resp_len > 0U) ? resp[0] : 0xFFU;
if (gd32_err_out) {
*gd32_err_out = gd32_err;
}
DEBUG_WARN("CAN", "BURST拒 n=%u r=%02X e=%02X",
(unsigned)count, resp_type, gd32_err);
return -3;
}
/**
* @brief 解析 GD32 主动推送的一页 CAN 反馈 payload,累积到结果结构中。
*