Files
DCU_APP/GD32E508_DCU_APP/comm/ethercat_can_bridge.cpp
T
cbb f9ef1b8da2 feat(comm): 添加EtherCAT CAN桥接和PDO功能模块
添加了ethercat_can_bridge.h/cpp和ethercat_pdo.h/cpp四个文件,
扩展通信模块功能,支持CAN桥接和PDO相关操作。

fix(comm): 修复LAN9252 EXMC总线时序配置问题

将BUSLAT从1调整为4,确保满足数据手册要求的最小13ns间隔,
解决因时序余量不足导致的温漂偶发读错问题。

feat(comm): 配置ESC过程数据看门狗时间为1秒

修改ESC_REG_WD_TIME_PD寄存器值从默认100ms到1s,
防止主站调试周期触发SM2看门狗导致状态被拖回SAFEOP。
2026-09-05 16:54:38 +08:00

391 lines
16 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* @file ethercat_can_bridge.cpp
* @brief EtherCAT PDO ↔ CAN 转发桥实现(1.5.56)。
*/
#include "comm/ethercat_can_bridge.h"
#include "device/canfd.h"
#include "device/motor_state.h"
#include "RTE_Components.h"
#include CMSIS_device_header /* __DMB():seqlock 的编译器/总线屏障 */
#include <cstring>
volatile uint8_t EthercatCanBridge::s_lastSeq = 0U;
volatile bool EthercatCanBridge::s_seqPrimed = false;
volatile uint8_t EthercatCanBridge::s_lastTxResult = 0U;
volatile uint16_t EthercatCanBridge::s_txCount = 0U;
volatile uint16_t EthercatCanBridge::s_dropCount = 0U;
volatile uint8_t EthercatCanBridge::s_winBase = 0U;
volatile bool EthercatCanBridge::s_rxValid = false;
volatile uint32_t EthercatCanBridge::s_xrxSeq = 0U;
volatile uint8_t EthercatCanBridge::s_rxCh = 0U;
volatile uint8_t EthercatCanBridge::s_rxDlc = 0U;
volatile bool EthercatCanBridge::s_rxTruncated = false;
volatile uint32_t EthercatCanBridge::s_rxId = 0U;
volatile uint32_t EthercatCanBridge::s_rxCount = 0U;
uint8_t EthercatCanBridge::s_rxData[kEcatCanXRxDataMax] = {0};
EthercatCanBridge::RxRingItem
EthercatCanBridge::s_rxRing[kEcatRxRingSlots] = {};
volatile uint8_t EthercatCanBridge::s_rxRingHead = 0U;
volatile uint8_t EthercatCanBridge::s_rxRingTail = 0U;
volatile uint8_t EthercatCanBridge::s_rxRingLost = 0U;
namespace {
/** 小端读写:PDO 字节序按 EtherCAT 惯例固定小端,不依赖编译器结构体布局。 */
inline uint32_t rd32le(const uint8_t *p)
{
return (uint32_t)p[0] | ((uint32_t)p[1] << 8) |
((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24);
}
inline void wr32le(uint8_t *p, uint32_t v)
{
p[0] = (uint8_t)(v); p[1] = (uint8_t)(v >> 8);
p[2] = (uint8_t)(v >> 16); p[3] = (uint8_t)(v >> 24);
}
inline void wr16le(uint8_t *p, uint16_t v)
{
p[0] = (uint8_t)(v); p[1] = (uint8_t)(v >> 8);
}
} // namespace
uint8_t EthercatCanBridge::sendBatch(const uint8_t *out)
{
uint8_t want = out[ECAT_TXB_OFF_COUNT];
if (want == 0U) {
return 0U; /* 未使用批量区:行为与 1.5.55 完全一致 */
}
if (want > kEcatTxBatchMax) {
want = kEcatTxBatchMax;
}
const bool stopOnError = (out[ECAT_TXB_OFF_FLAGS] & 0x01U) != 0U;
uint8_t sent = 0U;
uint16_t off = ECAT_TXB_OFF_ITEMS;
for (uint8_t i = 0U; i < want; ++i) {
/* 不信任主站给的 count/dlc:每步都验证头与数据都完整落在 PDO 内。
* 越界即停 —— 这是唯一的边界判据,count 只是上限提示。 */
if ((uint16_t)(off + ECAT_FRM_HDR_SIZE) > kEcatPdoOutBytes) {
break;
}
const uint8_t *f = &out[off];
uint8_t dlc = f[ECAT_FRM_OFF_DLC];
if (dlc > kEcatCanFdDataMax) {
dlc = kEcatCanFdDataMax;
}
if ((uint16_t)(off + ECAT_FRM_HDR_SIZE + dlc) > kEcatPdoOutBytes) {
break;
}
const uint8_t ch = f[ECAT_FRM_OFF_CH];
CanFd::CanTxResult r;
if (!CanFd::isChannelEnabled(ch)) {
r = CanFd::CanTxResult::kErrInvalid;
} else {
/* 与单帧路径同样用 enqueueTx:批量下发必然连续打同一通道,
* transmitFrame 的 allow_abort 会让后帧顶掉前帧(历史上 7 帧
* 只发出 3 帧的根因),队列语义才不静默丢帧。 */
r = CanFd::enqueueTx(ch, rd32le(&f[ECAT_FRM_OFF_ID]),
&f[ECAT_FRM_HDR_SIZE], dlc);
}
if (r == CanFd::CanTxResult::kOk) {
++s_txCount;
++sent;
} else {
s_lastTxResult = (uint8_t)r;
if (stopOnError) {
break;
}
}
off = (uint16_t)(off + ECAT_FRM_HDR_SIZE + dlc);
}
return sent;
}
void EthercatCanBridge::fillRxBatch(uint8_t *in)
{
uint8_t count = 0U;
uint16_t off = ECAT_RXB_OFF_ITEMS;
uint8_t tail = s_rxRingTail;
const uint8_t head = s_rxRingHead; /* 快照一次:生产者可能边填边推进 */
while (tail != head && count < kEcatRxBatchMax) {
const RxRingItem &it = s_rxRing[tail];
uint8_t dlc = it.dlc;
if (dlc > kEcatCanFdDataMax) {
dlc = kEcatCanFdDataMax;
}
/* 装不下就停,**且不推进 tail** —— 该帧留到下个周期,不丢。 */
if ((uint16_t)(off + ECAT_FRM_HDR_SIZE + dlc) > kEcatPdoInBytes) {
break;
}
uint8_t *p = &in[off];
p[ECAT_FRM_OFF_CH] = it.ch;
p[ECAT_FRM_OFF_DLC] = dlc;
p[ECAT_FRM_OFF_FLAGS] = 0x01U;
wr32le(&p[ECAT_FRM_OFF_ID], it.id);
memcpy(&p[ECAT_FRM_HDR_SIZE], it.data, dlc);
off = (uint16_t)(off + ECAT_FRM_HDR_SIZE + dlc);
tail = (uint8_t)((tail + 1U) % kEcatRxRingSlots);
++count;
}
__DMB(); /* 槽位读取先于 tail 发布落地:否则生产者可能提前覆写还没读完的槽 */
s_rxRingTail = tail; /* 只在这里推进:消费者独占写 tail */
const uint8_t pending = (uint8_t)((head + kEcatRxRingSlots - tail) % kEcatRxRingSlots);
in[ECAT_RXB_OFF_COUNT] = count;
in[ECAT_RXB_OFF_LOST] = s_rxRingLost;
in[ECAT_RXB_OFF_PENDING] = pending;
s_rxRingLost = 0U; /* 回填后清零:lost 是"自上次回填以来"的增量 */
}
void EthercatCanBridge::processOutput(const uint8_t *out)
{
if (out == nullptr) {
return;
}
const uint8_t seq = out[ECAT_CAN_TX_OFF_SEQ];
/* 电机状态窗口下标每周期都取(与 seq 沿无关):主站靠移动这个值滚动扫表,
* 不该被「有没有新 CAN 帧要发」耦合。越界值钳到 0,避免读到槽数组外。 */
const uint8_t win = out[ECAT_CAN_TX_OFF_WIN];
s_winBase = (win < kMotorSlotCount) ? win : 0U;
/* 上电/重连后第一次看到输出数据时只记录基准,不发送:否则主站残留的
* 非零 seq 会被当成一次新请求,在进 OP 的瞬间凭空发出一帧。 */
if (!s_seqPrimed) {
s_seqPrimed = true;
s_lastSeq = seq;
return;
}
/* flags bit0:清零统计(一次性,不依赖 seq 沿,便于主站随时归零) */
if ((out[ECAT_CAN_TX_OFF_FLAGS] & 0x01U) != 0U) {
s_txCount = 0U;
s_dropCount = 0U;
s_rxCount = 0U;
}
if (seq == s_lastSeq) {
return; /* 没有新的发送请求 */
}
s_lastSeq = seq;
const uint8_t ch = out[ECAT_CAN_TX_OFF_CH];
uint8_t dlc = out[ECAT_CAN_TX_OFF_DLC];
if (dlc > kEcatCanTxDataMax) {
dlc = kEcatCanTxDataMax;
}
/* 单帧区先发(保持 1.5.55 语义),再发批量区。
* dlc==0 表示本次只用批量区,不发单帧 —— 否则会凭空发出一帧 0 长度报文。 */
if (dlc > 0U) {
/* isChannelEnabled 内含越界检查,是"通道号合法且可用"的唯一判据 */
if (!CanFd::isChannelEnabled(ch)) {
s_lastTxResult = (uint8_t)CanFd::CanTxResult::kErrInvalid;
} else {
/* 用 enqueueTx 而非 transmitFrame:PDO 是周期性场景,可能连续多帧打到
* 同一通道。transmitFrame 默认 allow_abort=true 会在邮箱满时顶掉已装载
* 的旧帧(历史上 7 帧突发只发出 3 帧的根因);enqueueTx 队列空时直通、
* 满时排队,不静默丢帧。 */
const CanFd::CanTxResult r =
CanFd::enqueueTx(ch, rd32le(&out[ECAT_CAN_TX_OFF_ID]),
&out[ECAT_CAN_TX_OFF_DATA], dlc);
s_lastTxResult = (uint8_t)r;
if (r == CanFd::CanTxResult::kOk) {
++s_txCount;
}
}
}
/* 批量区:同一个 seq 沿里一次性发 N 帧,供多轴同步下发。 */
(void)sendBatch(out);
}
void EthercatCanBridge::buildInput(uint8_t *in)
{
if (in == nullptr) {
return;
}
/* 必须按新的全长清零。旧代码写死 memset(in,0,32),扩容后会把 32 字节以后
* 的槽区留成上一周期的残留数据,主站读到的是"看起来合法但已过期"的电机状态。 */
memset(in, 0, kEcatPdoInBytes);
/* ── 段 1(0..31):桥状态 + 短快照,布局与 1.5.53 逐字节兼容 ── */
in[ECAT_CAN_RX_OFF_ACK] = s_lastSeq;
in[ECAT_CAN_RX_OFF_TXRES] = s_lastTxResult;
wr16le(&in[ECAT_CAN_RX_OFF_TXCNT], s_txCount);
wr16le(&in[ECAT_CAN_RX_OFF_DROP], s_dropCount);
wr32le(&in[ECAT_CAN_RX_OFF_RXCNT], s_rxCount);
/* seqlock 判稳(见 onCanFrame):奇数=生产者正在写这组快照。单核下 ISR
* 执行期间生产者冻结,读一次即可定性;本周期跳过段 1/段 3(开头已清零,
* 语义=无有效快照),下周期自愈。批量区(段 4)走环形队列,不受影响。 */
const bool xrxStable = ((s_xrxSeq & 1U) == 0U);
if (xrxStable && s_rxValid) {
uint8_t flags = 0x01U;
if (s_rxTruncated) {
flags |= 0x02U;
}
in[ECAT_CAN_RX_OFF_CH] = s_rxCh;
in[ECAT_CAN_RX_OFF_DLC] = s_rxDlc;
in[ECAT_CAN_RX_OFF_FLAGS] = flags;
wr32le(&in[ECAT_CAN_RX_OFF_ID], s_rxId);
/* 段 1 只放前 8 字节(兼容),完整 payload 见段 3 */
memcpy(&in[ECAT_CAN_RX_OFF_DATA], s_rxData, kEcatCanRxDataMax);
}
const uint8_t ch = s_rxValid ? s_rxCh : 0U;
const uint32_t backlog = CanFd::isChannelEnabled(ch) ? CanFd::txQueueBacklog(ch) : 0U;
in[ECAT_CAN_RX_OFF_BACKLOG] = (backlog > 255U) ? 255U : (uint8_t)backlog;
/* ── 段 2(32..319):电机状态窗口 ──
* 直接读 MotorState 聚合表,不再依赖"最近一帧 CAN"。窗口起点由主站通过
* 输出 PDO 的 WIN 字节指定,主站每周期 +7 即可 4 周期扫完 27 个槽。
*
* 上下文安全:buildInput 在主循环(free-run)或 Sync0_Isr 里跑,而状态表由
* 主循环的 taskCanRxParse 写。单字段撕裂最坏是某槽的 seq 与 data 不同步一拍,
* 主站按 seq 变化判新旧、下一周期即自愈,故不加临界区(与本文件既有约定一致)。 */
const MotorStateTable &mt = MotorState::table();
const uint8_t base = (s_winBase < kMotorSlotCount) ? s_winBase : 0U;
in[ECAT_MOTW_OFF_ACTIVE] = mt.active_count;
in[ECAT_MOTW_OFF_BOOT] = mt.boot_active;
in[ECAT_MOTW_OFF_WIN_BASE] = base;
wr32le(&in[ECAT_MOTW_OFF_GLOBAL_SEQ], mt.global_seq);
uint8_t count = 0U;
for (uint8_t i = 0U; i < kEcatMotSlotsPerPdo; ++i) {
const uint8_t idx = (uint8_t)(base + i);
if (idx >= kMotorSlotCount) {
break; /* 窗口尾部越界即停,不回绕:主站据 win_count 判断 */
}
const MotorSlot &s = mt.slots[idx];
uint8_t *p = &in[ECAT_MOTW_OFF_SLOTS + (uint16_t)i * kEcatMotSlotSize];
p[ECAT_MOT_OFF_NODE] = s.node_id;
p[ECAT_MOT_OFF_CH] = s.channel;
p[ECAT_MOT_OFF_ONLINE] = s.online;
p[ECAT_MOT_OFF_DLC] = s.dlc;
wr32le(&p[ECAT_MOT_OFF_SEQ], s.seq);
memcpy(&p[ECAT_MOT_OFF_STATUS], s.data, kMotorStatusDlc);
++count;
}
in[ECAT_MOTW_OFF_WIN_COUNT] = count;
/* ── 段 3(320..359):最近一帧完整快照 ──
* 版本帧(0x00FF)/CONFIG 帧(0x0101) 都是 DLC 24,段 1 的 8 字节装不下。
* 与段 1 共用 xrxStable 门控:两段必须来自同一帧,否则 ecat.html 的短快照
* 和电机页的长快照会各自解出半新半旧的 ID/数据。 */
if (xrxStable && s_rxValid) {
in[ECAT_XRX_OFF_CH] = s_rxCh;
in[ECAT_XRX_OFF_DLC] = s_rxDlc;
in[ECAT_XRX_OFF_FLAGS] = 0x01U;
wr32le(&in[ECAT_XRX_OFF_ID], s_rxId);
memcpy(&in[ECAT_XRX_OFF_DATA], s_rxData, kEcatCanXRxDataMax);
}
/* ── 段 4(360..639):CANFD 批量接收区 ──
* 段 1/段 3 只带"最近一帧",高速多帧场景下周期间到达的帧会互相覆盖
* (s_dropCount 就是在数这个)。批量区从环形队列里一次回收多帧,
* 让主站在一个周期内拿到完整的帧序列而不是采样点。 */
fillRxBatch(in);
}
void EthercatCanBridge::onCanFrame(uint8_t channel, uint32_t id,
const uint8_t *data, uint8_t length)
{
if (data == nullptr) {
return;
}
/* 上一帧还没被 PDO 周期取走就来了新帧:记一次丢弃(覆盖旧帧,
* 保证主站看到的永远是最新状态,而不是卡在一帧陈旧数据上)。 */
if (s_rxValid) {
++s_dropCount;
}
/* 缓存区已扩到 32B(段 3 用),truncated 仍按段 1 的 8B 口径上报,
* 这样 ecat.html 的 CAN 调试页语义不变。 */
uint8_t copyLen = length;
if (copyLen > kEcatCanXRxDataMax) {
copyLen = kEcatCanXRxDataMax;
}
/* ── 快照 seqlock(1.5.58)──
* s_rxData/s_rxCh/s_rxId/s_rxDlc/s_rxTruncated/s_rxValid 是一组,必须整体
* 被 buildInput 读到。DC 模式下 buildInput 跑在 Sync0_Isr(preempt=0),会
* 抢占本函数(主循环 taskCanRxParse)。原注释声称"valid 最后置位保证一致"
* 是错的:valid 一旦置位就永不清零,ISR 落在"新 payload 已写完、id 还没
* 更新"的窗口里,会把新帧数据配旧帧 ID 发出去——STATUS 字节被标成 CONFIG
* (0x000N0101),前端按 CONFIG 布局解码出 -3e26 这类离谱目标值。
* 协议:seq 奇数=正在写。消费者在单核 ISR 里生产者必然冻结,判一次奇偶
* 即可,奇数则本周期跳过(段 1/段 3 已清零=无效),下周期自愈。
* 不关中断:critical.h 的 BASEPRI 方案放行 preempt=0,挡不住 Sync0;
* 而 seqlock 零抖动,与本文件既有无锁约定一致。
* __DMB 防编译器把非 volatile 的 memcpy 挪出两次 seq 自增之间。 */
++s_xrxSeq; /* → 奇数:快照进入"正在写" */
__DMB();
memcpy(s_rxData, data, copyLen);
if (copyLen < kEcatCanXRxDataMax) {
memset(&s_rxData[copyLen], 0, (size_t)(kEcatCanXRxDataMax - copyLen));
}
s_rxCh = channel;
s_rxId = id;
s_rxDlc = length; /* 报告原始长度,便于主站发现被截断 */
s_rxTruncated = (length > kEcatCanRxDataMax);
++s_rxCount;
s_rxValid = true;
__DMB();
++s_xrxSeq; /* → 偶数:快照整体一致 */
/* ── 推入批量接收环形队列(1.5.56)──
* 与上面的"最近一帧快照"并行维护:快照会被后帧覆盖(这是它的设计语义),
* 队列则保序累积,供 fillRxBatch 一次回收多帧。
* 队列满时丢**新**帧并计数,而不是覆盖旧帧 —— 批量区的价值在于保序完整,
* 覆盖会在序列中间留下空洞,比明确丢尾更难排查。 */
const uint8_t next = (uint8_t)((s_rxRingHead + 1U) % kEcatRxRingSlots);
if (next == s_rxRingTail) {
if (s_rxRingLost < 0xFFU) {
++s_rxRingLost;
}
} else {
RxRingItem &it = s_rxRing[s_rxRingHead];
uint8_t n = length;
if (n > kEcatCanFdDataMax) {
n = kEcatCanFdDataMax;
}
it.id = id;
it.ch = channel;
it.dlc = n;
memcpy(it.data, data, n);
__DMB(); /* 数据写入先于 head 发布落地(volatile 不约束非 volatile 的 memcpy) */
s_rxRingHead = next; /* 最后推进 head:消费者读到时数据必已写完 */
}
}
extern "C" {
void EcatCan_ProcessOutput(const uint8_t *out)
{
EthercatCanBridge::processOutput(out);
}
void EcatCan_BuildInput(uint8_t *in)
{
EthercatCanBridge::buildInput(in);
}
} /* extern "C" */