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:
@@ -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,累积到结果结构中。
|
||||
*
|
||||
|
||||
@@ -53,6 +53,44 @@ int canFdSend(can_channel_t channel, uint32_t ext_id,
|
||||
uint32_t timeout_ms,
|
||||
uint8_t *gd32_err_out = nullptr);
|
||||
|
||||
// ============================================================
|
||||
// CAN 跨通道突发发送(SPI 0xC7)
|
||||
// 一次 SPI 事务携带多帧,GD32 背靠背装载各通道 CAN 邮箱,
|
||||
// 组内近同时上线(偏差 <1ms),用于多电机同步驱动。
|
||||
// ============================================================
|
||||
#define CAN_BURST_HARD_MAX (16) ///< GD32 单次突发硬上限(帧数)
|
||||
|
||||
/** @brief 突发条目:chnode=(ch<<4)|node,data 指向 dlc 字节数据。 */
|
||||
typedef struct {
|
||||
uint8_t chnode; ///< (channel<<4) | node
|
||||
const uint8_t *data; ///< dlc 字节数据
|
||||
} can_burst_entry_t;
|
||||
|
||||
/**
|
||||
* @brief 给定统一 DLC,返回单次突发最多可容纳的帧数。
|
||||
* 受 236B SPI payload(4B 头 + count*(1+dlc))与 GD32 硬上限约束。
|
||||
*/
|
||||
uint8_t canBurstCapacity(uint8_t dlc);
|
||||
|
||||
/**
|
||||
* @brief 发送一次 CAN 突发(SPI 0xC7)。count 必须 <= canBurstCapacity(dlc)。
|
||||
* GD32 在 ISR 内对每帧调用非阻塞 transmitFrame,各通道邮箱背靠背装载。
|
||||
* @param id_suffix CAN ID 低 16 位(CMD=0x0200);id=(node<<16)|suffix。
|
||||
* @param dlc 所有条目统一数据长度(0~64)。
|
||||
* @param entries count 个条目(chnode + data)。
|
||||
* @param count 条目数(1~canBurstCapacity(dlc))。
|
||||
* @param status_out 输出每帧状态(GD32 CanTxResult 码,0x00=成功),可为 nullptr。
|
||||
* @param status_max status_out 容量。
|
||||
* @param timeout_ms SPI 事务超时。
|
||||
* @param gd32_err_out 返回 -3 时的 GD32 细分错误码;可为 nullptr。
|
||||
* @return 成功返回 count;-1 参数错误;-2 SPI 通信失败;-3 GD32 ERROR。
|
||||
*/
|
||||
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 = nullptr);
|
||||
|
||||
// ============================================================
|
||||
// 不重复 ID 快照表(SPI 0xC3 协议配套)
|
||||
// ============================================================
|
||||
|
||||
@@ -70,15 +70,17 @@ static size_t spiRespReadBudget(void)
|
||||
|
||||
/**
|
||||
* @brief 已知"短响应"的主动请求类型:应答帧 ≤ ~70B(实测 0xA1 PING ~60-70B、
|
||||
* DATA/REQ/FINISH ACK ~30-40B、0xB5/0xC0/0xC2/0xD1 ACK 均短)。
|
||||
* DATA/REQ/FINISH ACK ~30-40B、0xB5/0xC0/0xC2/0xD1 ACK 均短、0xC4 ADC 20B)。
|
||||
* 对这些类型的主动 poll 可把读预算从 256 收紧到 96B,省 ~160B 0xFF 填充时钟
|
||||
* (@13.3MHz ≈ 96µs/帧)。排除 0xC3(ID 快照响应可能很长)与被动 0xC1 事件读。
|
||||
* (@13.3MHz ≈ 96µs/帧)。排除 0xC3(ID 快照响应可能很长)、0xC1/0xC5
|
||||
* (分页响应 ~236B)与被动 0xC1 事件读。
|
||||
*/
|
||||
static bool isShortRespType(uint8_t type)
|
||||
{
|
||||
switch (type) {
|
||||
case 0xA1U: case 0x02U: case 0x01U: case 0xF7U:
|
||||
case 0xB0U: case 0xC0U: case 0xC2U: case 0xD1U:
|
||||
case 0xC4U: case 0xC7U:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
@@ -87,6 +89,24 @@ static bool isShortRespType(uint8_t type)
|
||||
/** 短响应读预算(覆盖最长 ~70B 应答 + 余量)。 */
|
||||
static constexpr size_t kShortRespReadBudget = 96U;
|
||||
|
||||
/**
|
||||
* @brief 已知 GD32 APP 在 onCsRelease ISR 内 µs 级装载响应的主动请求类型。
|
||||
* 这些类型 CS 释放后无需 5ms 静默:waitSpiDataReady 由下降沿 ISR 精确唤醒
|
||||
* (与 0xD1/0xA1 同机制)。仅 BOOT 主循环处理的 OTA 命令(0x01/0x02/0x03)
|
||||
* 保留 5ms——过早盲读会打断 BOOT 的 EXTI/DMA 时序(实测 CHALLENGE 超时)。
|
||||
*/
|
||||
static bool isAppIsrFastRespType(uint8_t type)
|
||||
{
|
||||
switch (type) {
|
||||
case 0xA1U: case 0xC0U: case 0xC1U: case 0xC2U:
|
||||
case 0xC3U: case 0xC4U: case 0xC5U: case 0xC7U: case 0xD1U:
|
||||
case 0xF7U:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** APP 独有业务 TYPE:见到即可纠正 m_gd32Role(BOOT 不会发这些)。 */
|
||||
bool SpiMaster::isAppOnlySpiType(uint8_t type)
|
||||
{
|
||||
@@ -646,6 +666,19 @@ void SpiMaster::workerLoop()
|
||||
continue;
|
||||
}
|
||||
|
||||
/* 无 job:先处理 GD32 重启重同步(session=0 响应置位;5s 冷却防空转)。
|
||||
* OTA 中继/暂停期间不 HELLO,避免干扰在途 OTA 事务。 */
|
||||
if (m_resyncPending && !m_suppressRingService && !m_spiPaused) {
|
||||
const TickType_t now_tick = xTaskGetTickCount();
|
||||
if ((uint32_t)(now_tick - m_lastResyncTick) >= pdMS_TO_TICKS(5000)) {
|
||||
m_lastResyncTick = now_tick;
|
||||
m_resyncPending = false;
|
||||
DEBUG_INFO("SPI", "GD32 session=0,HELLO 重同步...");
|
||||
(void)hello(1500U, 0U);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
/* 无 job:等 GPIO / 提交唤醒。
|
||||
* 超时只是无事件时的兜底回扫周期;ISR 的 xTaskNotifyFromISR 会立即唤醒
|
||||
* 本任务,响应延迟不由这里决定。过短会令 spi_worker(优先级6) 忙转压住
|
||||
@@ -981,6 +1014,9 @@ bool SpiMaster::otaFindFrame(const uint8_t *buf, size_t buf_len,
|
||||
DEBUG_WARN("SPI", "GD32 session=0 (对端已重启?) 放行 RESPONSE t=0x%02X #%lu",
|
||||
hdr.type, (unsigned long)s_sess0_logs);
|
||||
}
|
||||
/* GD32 重启后 v2 未激活:置重同步标志,worker 空闲时 HELLO 重建 session。
|
||||
* 功能由 session=0 放行兜底,重同步只是把链路拉回校验态。 */
|
||||
self.m_resyncPending = true;
|
||||
}
|
||||
static uint32_t s_evt_sess0_logs = 0U;
|
||||
if (is_event && (++s_evt_sess0_logs % 64U) == 1U) {
|
||||
@@ -1641,13 +1677,13 @@ esp_err_t SpiMaster::otaSendAndPoll(uint8_t type, const uint8_t *payload, size_t
|
||||
}
|
||||
|
||||
/* CS 释放后的静默期分型处理:
|
||||
* - 0xD1(电机 OTA 数据)/ 0xA1(APP PING):GD32 APP 在 onCsRelease ISR
|
||||
* 内 µs 级装好响应并拉低 PB6 → 零盲等,waitSpiDataReady 由下降沿 ISR
|
||||
* 精确唤醒,旧 1ms 固定延时压制喂速~47KB/s,现由事件驱动提速。
|
||||
* - 其他类型(GD32 relay/BOOT AES/HELLO 等):保留 5ms——BOOT 在主循环
|
||||
* - APP ISR 响应类型(0xC1/0xC4/0xC5 查询、0xD1 电机 OTA、0xA1 PING 等):
|
||||
* GD32 APP 在 onCsRelease ISR 内 µs 级装好响应并拉低 PB6 → 零盲等,
|
||||
* waitSpiDataReady 由下降沿 ISR 精确唤醒(见 isAppIsrFastRespType)。
|
||||
* - 其他类型(BOOT OTA 命令 0x01/0x02/0x03 等):保留 5ms——BOOT 在主循环
|
||||
* 处理 CHALLENGE/flash,poll 前 2 轮盲读 CS 过早打进去会打断其
|
||||
* EXTI/DMA 时序(实测去掉后 CHALLENGE 超时)。 */
|
||||
if (type != 0xD1U && type != 0xA1U) {
|
||||
if (!isAppIsrFastRespType(type)) {
|
||||
vTaskDelay(pdMS_TO_TICKS(5));
|
||||
}
|
||||
|
||||
|
||||
@@ -322,6 +322,10 @@ private:
|
||||
volatile bool m_suppressRingService = false;
|
||||
/** ESP32 自我 OTA 期间为 true:worker 空转、submitJob 快速拒绝。 */
|
||||
volatile bool m_spiPaused = false;
|
||||
/** GD32 重启检测(响应帧 session=0)置位:worker 空闲时 HELLO 重同步。 */
|
||||
volatile bool m_resyncPending = false;
|
||||
/** 上次 HELLO 重同步时刻(冷却 5s,防 GD32 不响应时反复 HELLO)。 */
|
||||
volatile uint32_t m_lastResyncTick = 0;
|
||||
/** 最近一次主动请求类型(otaTransferUnlocked 发送时记录;poll 读预算按它收紧)。 */
|
||||
volatile uint8_t m_lastReqType = 0xFFU;
|
||||
/** SPI worker 任务句柄(ISR 唯一通知目标)。 */
|
||||
|
||||
Reference in New Issue
Block a user