Compare commits
18
Commits
f9ef1b8da2
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
662dcccbf2 | ||
|
|
d8b3c9767c | ||
|
|
f3d7fc0c30 | ||
|
|
a777be1cb4 | ||
|
|
28d8426052 | ||
|
|
5fd986299c | ||
|
|
63af85e7be | ||
|
|
7d9d9c70a1 | ||
|
|
6136c2363f | ||
|
|
6afaacd03a | ||
|
|
9ca3db26f8 | ||
|
|
1742d9a759 | ||
|
|
959e2aef6e | ||
|
|
0cd3d9bed2 | ||
|
|
ace5a1671b | ||
|
|
9f1c2d25dd | ||
|
|
70daa4b0c6 | ||
|
|
2ebd4ff1d2 |
+20
@@ -85,3 +85,23 @@ uv2csolution.log
|
||||
|
||||
# legacy intermediate project file
|
||||
*.cprj
|
||||
|
||||
# Verification & Test suite process artifacts
|
||||
/tests/
|
||||
/.agents/
|
||||
TEST_READY.md
|
||||
TEST_READY.html
|
||||
convert_md_to_html.py
|
||||
*.html
|
||||
|
||||
# IDE / 多智能体过程产物
|
||||
.trae/
|
||||
.worktrees/
|
||||
.qoder/
|
||||
.zcode/
|
||||
docs/traework-import/
|
||||
|
||||
# 本机固件 keep / 安全评审过程稿
|
||||
firmware_keep/
|
||||
SECURITY_AUDIT_REPORT.*
|
||||
ORIGINAL_REQUEST.md
|
||||
|
||||
@@ -30,7 +30,7 @@ solution:
|
||||
target-set:
|
||||
- set:
|
||||
images:
|
||||
- project-context: GD32E508_DCU_APP.Debug
|
||||
- project-context: GD32E508_DCU_APP.Release
|
||||
debugger:
|
||||
name: ST-Link@pyOCD
|
||||
clock: 4000000
|
||||
|
||||
@@ -16,6 +16,8 @@ project:
|
||||
- file: ./sys/task_manager.cpp
|
||||
- file: ./sys/task_runner.h
|
||||
- file: ./sys/task_runner.cpp
|
||||
- file: ./sys/cpu_load.h
|
||||
- file: ./sys/cpu_load.cpp
|
||||
- file: ./sys/ota_boot_request.h
|
||||
- file: ./sys/ota_boot_request.cpp
|
||||
- file: ./sys/aes128.h
|
||||
@@ -25,6 +27,8 @@ project:
|
||||
- file: ./sys/hardfault.cpp
|
||||
- file: ./sys/cpp_stubs.cpp
|
||||
- file: ./sys/config.h
|
||||
- file: ./sys/esp32_en.h
|
||||
- file: ./sys/esp32_en.cpp
|
||||
- group: device
|
||||
files:
|
||||
- file: ./device/adc.h
|
||||
@@ -66,6 +70,8 @@ project:
|
||||
- file: ./device/spi/app_spi_cmd_motor_version.cpp
|
||||
- file: ./device/spi/app_spi_cmd_motor_ota.cpp
|
||||
- file: ./device/spi/app_spi_cmd_exmc_diag.cpp
|
||||
- file: ./device/spi/app_spi_diag.h
|
||||
- file: ./device/spi/app_spi_cmd_spi_diag.cpp
|
||||
- group: spi_link
|
||||
files:
|
||||
- file: ../../spi_link/spi_link_v2.cpp
|
||||
@@ -81,8 +87,14 @@ project:
|
||||
- file: ./comm/ethercat_sync.cpp
|
||||
- file: ./comm/ethercat_can_bridge.h
|
||||
- file: ./comm/ethercat_can_bridge.cpp
|
||||
# 电机 OTA 的 EtherCAT 邮箱进料通路(CoE 0x2510/0x2511 → MotorOtaRelay 环缓)
|
||||
- file: ./comm/ecat_motor_ota.h
|
||||
- file: ./comm/ecat_motor_ota.cpp
|
||||
- file: ./comm/ethercat_pdo.h
|
||||
- file: ./comm/ethercat_pdo.cpp
|
||||
# [DIFF-STATS] 临时仪表:差分写取证,删除时连同本两行与 comm/ethercat_diff_stats.* 一并移除
|
||||
- file: ./comm/ethercat_diff_stats.h
|
||||
- file: ./comm/ethercat_diff_stats.cpp
|
||||
- group: comm/ssc
|
||||
files:
|
||||
- file: ./comm/ssc/inc/ecatslv.h
|
||||
@@ -163,6 +175,9 @@ project:
|
||||
- -DHXTAL_VALUE=8000000U
|
||||
- -gdwarf-5
|
||||
- -D__MICROLIB
|
||||
# 启动文件经 legacy armasm 汇编:消 armclang 驱动"参数未使用"告警 + armasm 弃用告警(A1950W)
|
||||
- -Wno-unused-command-line-argument
|
||||
- -Wa,armasm,--diag_suppress=A1950W
|
||||
Link:
|
||||
- --library_type=microlib
|
||||
- --entry=Reset_Handler
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
@echo off
|
||||
call "C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Auxiliary\Build\vcvars64.bat" >nul 2>&1
|
||||
cd /d "G:\Robot_v2\DCU\DCU_APP\GD32E508_DCU_APP"
|
||||
set PACKED=/D"STRUCT_PACKED_START=" /D"STRUCT_PACKED_END=" /D"MBX_STRUCT_PACKED_START=" /D"MBX_STRUCT_PACKED_END=" /D"OBJ_STRUCT_PACKED_START=" /D"OBJ_STRUCT_PACKED_END="
|
||||
set INC=/I. /Icomm\ssc\inc /Icomm\ssc\port /IRTE\_Debug_GD32E508VE /I_syntaxcheck_stubs
|
||||
echo === mailbox.c ===
|
||||
cl /nologo /Zs /W0 /TC /std:c11 /utf-8 /Zp1 %PACKED% %INC% comm\ssc\src\mailbox.c
|
||||
if errorlevel 1 (echo MAILBOX_FAIL) else (echo MAILBOX_OK)
|
||||
echo === dc_app.c ===
|
||||
cl /nologo /Zs /W0 /TC /std:c11 /utf-8 /Zp1 %PACKED% %INC% comm\ssc\port\dc_app.c
|
||||
if errorlevel 1 (echo DC_APP_FAIL) else (echo DC_APP_OK)
|
||||
@@ -0,0 +1 @@
|
||||
/* syntax-check stub only: SSC C files never touch device registers directly */
|
||||
@@ -0,0 +1,383 @@
|
||||
/**
|
||||
* @file ecat_motor_ota.cpp
|
||||
* @brief EtherCAT 邮箱进料通路实现(职责与时序前提见 ecat_motor_ota.h)。
|
||||
*
|
||||
* 上下文:三个 SDO 钩子只在 MainLoop(ECAT_Application 调用链)里被调,泵 process()
|
||||
* 也在主循环,因此 48KB 暂存区没有并发写者。真正的硬保证仍是通道锁:即便主站循环
|
||||
* 被别的路径(页面自愈 / 守护重连)拉回来,运动帧也进不了正在升级的通道。
|
||||
*/
|
||||
#include "comm/ecat_motor_ota.h"
|
||||
|
||||
#include "sys/config.h"
|
||||
#include "sys/motor_ota_relay.h"
|
||||
#include "sys/ota_boot_request.h"
|
||||
#include "sys/systick.h"
|
||||
|
||||
#include "spi_link_v2.h" /* v2_crc32:CRC32 MPEG-2(poly 0x04C11DB7,初值 0xFFFFFFFF,无反射、无末尾异或) */
|
||||
|
||||
#include <cstring>
|
||||
#include <cstdio> /* LOG_ERR 展开为 printf(sys/config.h) */
|
||||
|
||||
namespace {
|
||||
|
||||
/*============================================================================
|
||||
* 尺寸常量与守卫
|
||||
*==========================================================================*/
|
||||
constexpr uint32_t kChunkObjBytes = ECAT_MOTA_CHUNK_OBJ_BYTES;
|
||||
constexpr uint32_t kChunkHdrBytes = ECAT_MOTA_CHUNK_HDR_BYTES;
|
||||
constexpr uint32_t kImgMaxBytes = ECAT_MOTA_IMG_MAX_BYTES;
|
||||
constexpr uint8_t kCtlSubs = ECAT_MOTA_CTL_SUBS;
|
||||
|
||||
static_assert(kImgMaxBytes == MotorOtaRelay::kMaxFwSize,
|
||||
"暂存区上限必须与 MotorOtaRelay::kMaxFwSize 一致");
|
||||
static_assert(kChunkObjBytes * 8U <= 0xFFFFU,
|
||||
"0x2511 块长超出 SDO 单条目 BitSize(UINT16)上限");
|
||||
static_assert(kChunkObjBytes > kChunkHdrBytes, "数据窗要留出净载荷");
|
||||
|
||||
/** APP 的堆容量(RTE/Device/GD32E508VE/regions_GD32E508VE.h 的 __HEAP_SIZE)。
|
||||
* SSC 每笔分段下载都要 malloc(块长)(sdoserv.c:751),堆不够就从站直接回
|
||||
* abort 0x05040005「Out of memory」——2026-10-11 就是拿 4096 撞出来的。
|
||||
* 这里留 3 倍余量:谁要把块长往上抬,先抬 __HEAP_SIZE 再来改这一行。 */
|
||||
constexpr uint32_t kHeapBytes = 3072U;
|
||||
static_assert(kChunkObjBytes * 3U <= kHeapBytes,
|
||||
"块长受 APP 堆限制:malloc(块长) 必须留足余量,否则从站 OOM 拒收");
|
||||
|
||||
/** 单次 process() 最多搬运的字节数:把一圈的占用钳住,别在 MainLoop 里长时间拷 48KB。 */
|
||||
constexpr uint16_t kPumpSlice = 512U;
|
||||
|
||||
/*============================================================================
|
||||
* 会话状态
|
||||
*==========================================================================*/
|
||||
uint32_t s_cmd = 0U; /**< sub1: 最近一次执行的命令 */
|
||||
uint32_t s_total = 0U; /**< sub2: 声明的镜像总字节数 */
|
||||
uint32_t s_channel = 0U; /**< sub3: 目标 CAN 通道 */
|
||||
uint32_t s_node = 0U; /**< sub4: 目标节点 1..9 */
|
||||
uint32_t s_img_crc = 0U; /**< sub5: 整镜像期望 CRC32 */
|
||||
|
||||
uint32_t s_staged = 0U; /**< 已连续暂存字节数(水位,写块时前移) */
|
||||
uint8_t s_sess_err = ECAT_MOTA_OK;
|
||||
|
||||
bool s_armed = false; /**< 已声明总长,可收块 */
|
||||
bool s_started = false; /**< 已交给 relay(暂存区冻结,只剩泵) */
|
||||
bool s_crc_ok = false; /**< 整镜像 CRC 已核对通过 */
|
||||
bool s_last_chunk_ok = false;
|
||||
bool s_end_logged = false;/**< 终态日志只打一次 */
|
||||
|
||||
/* 终态锁存:relay 会话结束后可能很快回到 kIdle(主站下一拍才来读),
|
||||
* 不做锁存就会把「刷成功了」读成 kIdle,主站据此报的结论就成了猜的。 */
|
||||
bool s_final_valid = false;
|
||||
uint8_t s_final_state = 0U;
|
||||
uint8_t s_final_err = 0U;
|
||||
|
||||
uint32_t s_pump_pos = 0U; /**< 泵在暂存区里的读游标 */
|
||||
|
||||
/** 48KB 暂存区(.bss:启动零耗时;APP 当前 RAM 用量 35KB/128KB,放得下)。 */
|
||||
uint8_t s_img[kImgMaxBytes];
|
||||
|
||||
inline uint32_t loadU32(const uint8_t *p) {
|
||||
return static_cast<uint32_t>(p[0])
|
||||
| (static_cast<uint32_t>(p[1]) << 8)
|
||||
| (static_cast<uint32_t>(p[2]) << 16)
|
||||
| (static_cast<uint32_t>(p[3]) << 24);
|
||||
}
|
||||
|
||||
void resetSession()
|
||||
{
|
||||
s_armed = false;
|
||||
s_started = false;
|
||||
s_crc_ok = false;
|
||||
s_staged = 0U;
|
||||
s_pump_pos = 0U;
|
||||
s_total = 0U;
|
||||
s_last_chunk_ok = false;
|
||||
s_end_logged = false;
|
||||
s_final_valid = false;
|
||||
s_final_state = 0U;
|
||||
s_final_err = 0U;
|
||||
}
|
||||
|
||||
/** 只认「已连续暂存水位之内」的偏移:留洞会让泵卡住,而且说明主站发料顺序错了。 */
|
||||
uint8_t acceptChunk(uint32_t offset, uint32_t crc, const uint8_t *data, uint32_t len)
|
||||
{
|
||||
if (!s_armed || s_total == 0U) {
|
||||
s_sess_err = ECAT_MOTA_E_NOTARMED;
|
||||
return s_sess_err;
|
||||
}
|
||||
if (s_started) {
|
||||
s_sess_err = ECAT_MOTA_E_STARTED;
|
||||
return s_sess_err;
|
||||
}
|
||||
if (offset > s_staged || offset + len > s_total) {
|
||||
s_sess_err = ECAT_MOTA_E_GAP;
|
||||
return s_sess_err;
|
||||
}
|
||||
if (len > 0U) {
|
||||
/* 块级校验:EtherCAT 数据报**头**有 CRC,载荷没有;邮箱分段拼装还是软件路径。
|
||||
* 这里挡一次,坏块就地重发,不会等到电机回 0x06 才发现(那时 flash 已擦)。 */
|
||||
if (v2_crc32(data, len) != crc) {
|
||||
s_sess_err = ECAT_MOTA_E_CHUNKCRC;
|
||||
s_last_chunk_ok = false;
|
||||
return s_sess_err;
|
||||
}
|
||||
std::memcpy(&s_img[offset], data, len);
|
||||
if (offset + len > s_staged) {
|
||||
s_staged = offset + len;
|
||||
}
|
||||
}
|
||||
s_last_chunk_ok = true;
|
||||
s_sess_err = ECAT_MOTA_OK;
|
||||
return ECAT_MOTA_OK;
|
||||
}
|
||||
|
||||
/** 整镜像核对 + 交给 relay。所有失败路径都必须在电机进 BOOT **之前**。 */
|
||||
bool beginTransfer()
|
||||
{
|
||||
if (s_total == 0U || s_total > kImgMaxBytes) {
|
||||
s_sess_err = ECAT_MOTA_E_BADLEN;
|
||||
return false;
|
||||
}
|
||||
if (s_staged != s_total) {
|
||||
s_sess_err = ECAT_MOTA_E_GAP;
|
||||
return false;
|
||||
}
|
||||
if (s_channel >= 3U || s_node < 1U || s_node > 9U) {
|
||||
s_sess_err = ECAT_MOTA_E_PARAM;
|
||||
return false;
|
||||
}
|
||||
/* DCU 自己正在回 BOOT(配置页写 + 复位在路上)时绝不能起电机会话:
|
||||
* 复位会把半程的刷写腰斩在 erase 之后。 */
|
||||
if (OtaBootRequest::isRebootPending()) {
|
||||
s_sess_err = ECAT_MOTA_E_BOOTPEND;
|
||||
return false;
|
||||
}
|
||||
/* 整镜像 CRC 先行:这一刻电机还在 APP 正常跑、擦除没开始,校验不过就是零代价失败。
|
||||
* 反过来「先进 BOOT 再验」才是变砖路径。 */
|
||||
if (v2_crc32(s_img, s_total) != s_img_crc) {
|
||||
s_sess_err = ECAT_MOTA_E_IMGCRC;
|
||||
return false;
|
||||
}
|
||||
s_crc_ok = true;
|
||||
|
||||
if (!MotorOtaRelay::start(static_cast<uint8_t>(s_channel), s_total,
|
||||
MotorOtaRelay::FeedSource::kEcat,
|
||||
static_cast<uint8_t>(s_node))) {
|
||||
s_sess_err = ECAT_MOTA_E_BUSY;
|
||||
s_crc_ok = false;
|
||||
return false;
|
||||
}
|
||||
s_started = true;
|
||||
s_end_logged = false;
|
||||
s_pump_pos = 0U;
|
||||
s_final_valid = false;
|
||||
s_sess_err = ECAT_MOTA_OK;
|
||||
LOG_ERR("[ECAT-MOTA] start ch=%lu node=%lu size=%lu crc=%08lX\r\n",
|
||||
(unsigned long)s_channel, (unsigned long)s_node,
|
||||
(unsigned long)s_total, (unsigned long)s_img_crc);
|
||||
return true;
|
||||
}
|
||||
|
||||
} /* namespace */
|
||||
|
||||
/*============================================================================
|
||||
* EcatMotorOta(C++ 侧:主循环钩子与查询)
|
||||
*==========================================================================*/
|
||||
namespace EcatMotorOta {
|
||||
|
||||
void process()
|
||||
{
|
||||
if (!s_started) {
|
||||
return;
|
||||
}
|
||||
|
||||
const MotorOtaRelay::Status st = MotorOtaRelay::getStatus();
|
||||
|
||||
/* 电机侧自动重试会清环缓并把 fw_fed 归零:泵必须跟着回退,否则会把
|
||||
* 「后半段」当成已喂过,喂出一个前段缺失的镜像。 */
|
||||
if (st.fw_fed < s_pump_pos) {
|
||||
s_pump_pos = st.fw_fed;
|
||||
}
|
||||
|
||||
/* 泵:按环缓空位切片,搬不动就下一圈再来(不阻塞、不忙等、不碰 CAN)。 */
|
||||
while (s_pump_pos < s_total) {
|
||||
const uint16_t freeSpace = MotorOtaRelay::feedFreeSpace();
|
||||
if (freeSpace == 0U) {
|
||||
break;
|
||||
}
|
||||
const uint32_t remain = s_total - s_pump_pos;
|
||||
uint16_t n = static_cast<uint16_t>(remain < kPumpSlice ? remain : kPumpSlice);
|
||||
if (n > freeSpace) {
|
||||
n = freeSpace;
|
||||
}
|
||||
if (!MotorOtaRelay::feedAt(s_pump_pos, &s_img[s_pump_pos], n)) {
|
||||
break;
|
||||
}
|
||||
s_pump_pos += n;
|
||||
}
|
||||
|
||||
if (!MotorOtaRelay::isActive()) {
|
||||
if (!s_end_logged) {
|
||||
s_end_logged = true;
|
||||
s_final_valid = true;
|
||||
s_final_state = static_cast<uint8_t>(st.state);
|
||||
s_final_err = st.error_code;
|
||||
LOG_ERR("[ECAT-MOTA] end state=%u err=0x%02X acked=%u/%u fed=%lu/%lu\r\n",
|
||||
static_cast<unsigned>(st.state), static_cast<unsigned>(st.error_code),
|
||||
static_cast<unsigned>(st.packets_acked),
|
||||
static_cast<unsigned>(st.total_packets),
|
||||
(unsigned long)st.fw_fed, (unsigned long)st.fw_size);
|
||||
}
|
||||
s_started = false;
|
||||
s_crc_ok = false;
|
||||
/* 会话已结束:不再收块。主站要开下一轮必须重新写 0x2510:02 声明总长,
|
||||
* 那一步复位全部会话状态(包括上面的终态锁存)。 */
|
||||
s_armed = false;
|
||||
s_last_chunk_ok = false;
|
||||
}
|
||||
}
|
||||
|
||||
bool sessionActive()
|
||||
{
|
||||
return s_started;
|
||||
}
|
||||
|
||||
} /* namespace EcatMotorOta */
|
||||
|
||||
/*============================================================================
|
||||
* SDO 钩子(extern "C":dc_app.c 的对象表直接引用)
|
||||
*==========================================================================*/
|
||||
extern "C" {
|
||||
|
||||
uint8_t EcatMotorOtaChunkWrite(uint32_t dataSize, const uint8_t *bytes)
|
||||
{
|
||||
if (bytes == nullptr) {
|
||||
return ECAT_MOTA_E_BADLEN;
|
||||
}
|
||||
if (dataSize < kChunkHdrBytes) {
|
||||
s_sess_err = ECAT_MOTA_E_BADLEN;
|
||||
return s_sess_err;
|
||||
}
|
||||
if (dataSize > kChunkObjBytes) {
|
||||
s_sess_err = ECAT_MOTA_E_BADLEN;
|
||||
return s_sess_err;
|
||||
}
|
||||
|
||||
const uint32_t offset = loadU32(&bytes[0]);
|
||||
const uint32_t crc = loadU32(&bytes[4]);
|
||||
const uint32_t len = dataSize - kChunkHdrBytes;
|
||||
|
||||
const uint8_t rc = acceptChunk(offset, crc, &bytes[kChunkHdrBytes], len);
|
||||
if (rc != ECAT_MOTA_OK) {
|
||||
/* 一次失败只打一行:主站会重发同一块,串口刷屏会拖慢 MainLoop。 */
|
||||
LOG_ERR("[ECAT-MOTA] chunk reject off=%lu len=%lu err=0x%02X\r\n",
|
||||
(unsigned long)offset, (unsigned long)len, (unsigned)rc);
|
||||
}
|
||||
return rc;
|
||||
}
|
||||
|
||||
uint8_t EcatMotorOtaCtlWrite(uint8_t subindex, uint32_t dataSize, const uint8_t *bytes)
|
||||
{
|
||||
if (bytes == nullptr || dataSize < 4U) {
|
||||
return ECAT_MOTA_E_BADLEN;
|
||||
}
|
||||
const uint32_t v = loadU32(bytes);
|
||||
|
||||
switch (subindex) {
|
||||
case 1U: {
|
||||
/* sub1 cmd:1 = 校验并启动,3 = 中止会话 */
|
||||
s_cmd = v;
|
||||
if (v == 1U) {
|
||||
if (!beginTransfer()) {
|
||||
return s_sess_err;
|
||||
}
|
||||
} else if (v == 3U) {
|
||||
if (s_started) {
|
||||
MotorOtaRelay::cancel();
|
||||
LOG_ERR("[ECAT-MOTA] cancel by master\r\n");
|
||||
}
|
||||
resetSession();
|
||||
s_sess_err = ECAT_MOTA_OK;
|
||||
} else {
|
||||
return ECAT_MOTA_E_PARAM;
|
||||
}
|
||||
return ECAT_MOTA_OK;
|
||||
}
|
||||
case 2U: {
|
||||
/* sub2 总长:声明即复位会话(主站重开一轮不需要先 abort) */
|
||||
if (v == 0U || v > kImgMaxBytes) {
|
||||
s_sess_err = ECAT_MOTA_E_BADLEN;
|
||||
return s_sess_err;
|
||||
}
|
||||
if (s_started) {
|
||||
s_sess_err = ECAT_MOTA_E_STARTED;
|
||||
return s_sess_err;
|
||||
}
|
||||
resetSession();
|
||||
s_total = v;
|
||||
s_armed = true;
|
||||
s_sess_err = ECAT_MOTA_OK;
|
||||
return ECAT_MOTA_OK;
|
||||
}
|
||||
case 3U:
|
||||
if (v >= 3U) {
|
||||
s_sess_err = ECAT_MOTA_E_PARAM;
|
||||
return s_sess_err;
|
||||
}
|
||||
s_channel = v;
|
||||
return ECAT_MOTA_OK;
|
||||
case 4U:
|
||||
if (v < 1U || v > 9U) {
|
||||
s_sess_err = ECAT_MOTA_E_PARAM;
|
||||
return s_sess_err;
|
||||
}
|
||||
s_node = v;
|
||||
return ECAT_MOTA_OK;
|
||||
case 5U:
|
||||
s_img_crc = v;
|
||||
return ECAT_MOTA_OK;
|
||||
default:
|
||||
/* sub0 是条目数;sub6..sub11 只读 */
|
||||
return ECAT_MOTA_E_PARAM;
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t EcatMotorOtaCtlRead(uint8_t subindex)
|
||||
{
|
||||
const MotorOtaRelay::Status st = MotorOtaRelay::getStatus();
|
||||
/* live = 会话还在跑,或从没跑过(那就照实反映 relay 当前状态);
|
||||
* 会话刚结束时返回锁存值,避免主站把「成功」读成 kIdle。 */
|
||||
const bool live = (s_started || !s_final_valid);
|
||||
|
||||
switch (subindex) {
|
||||
case 0U: return kCtlSubs;
|
||||
case 1U: return s_cmd;
|
||||
case 2U: return s_total;
|
||||
case 3U: return s_channel;
|
||||
case 4U: return s_node;
|
||||
case 5U: return s_img_crc;
|
||||
case 6U: return s_staged;
|
||||
case 7U: return live ? static_cast<uint32_t>(st.state)
|
||||
: static_cast<uint32_t>(s_final_state);
|
||||
case 8U: return st.packets_acked;
|
||||
case 9U: return st.total_packets;
|
||||
case 10U: return (static_cast<uint32_t>(s_sess_err) << 8)
|
||||
| (live ? static_cast<uint32_t>(st.error_code)
|
||||
: static_cast<uint32_t>(s_final_err));
|
||||
case 11U: {
|
||||
/* phase 位图:只报能被板子真实触发的事实,不做任何推测性状态。
|
||||
* bit0 已声明总长 · bit1 暂存齐 · bit2 整镜像 CRC 过 · bit3 会话在跑
|
||||
* bit4 上一块被接受 · bit5 本会话目标通道当前持锁 · bit6 relay 活跃 */
|
||||
uint32_t phase = 0U;
|
||||
if (s_armed) { phase |= 0x0001U; }
|
||||
if (s_total != 0U && s_staged == s_total) { phase |= 0x0002U; }
|
||||
if (s_crc_ok) { phase |= 0x0004U; }
|
||||
if (s_started) { phase |= 0x0008U; }
|
||||
if (s_last_chunk_ok) { phase |= 0x0010U; }
|
||||
if (MotorOtaRelay::isChannelLocked(static_cast<uint8_t>(s_channel))) { phase |= 0x0020U; }
|
||||
if (MotorOtaRelay::isActive()) { phase |= 0x0040U; }
|
||||
return phase;
|
||||
}
|
||||
default: return 0U;
|
||||
}
|
||||
}
|
||||
|
||||
} /* extern "C" */
|
||||
@@ -0,0 +1,97 @@
|
||||
/**
|
||||
* @file ecat_motor_ota.h
|
||||
* @brief EtherCAT(CoE 邮箱)电机 OTA 进料通路 —— 主站把电机固件灌进 DCU 本地暂存区,
|
||||
* 再由本模块泵进 MotorOtaRelay 现有的 4KB 环缓。
|
||||
*
|
||||
* 为什么要有它(不是重复造轮子):电机 OTA 的引擎一直是 MotorOtaRelay
|
||||
* (AES 握手 + CAN 停等 + 单发模式),只有一条进料路:ESP32 经 SPI 0xD1 喂。
|
||||
* 纯 EtherCAT 场合(只插网线、ESP32 WiFi 关断)那条 HTTP 打不通,
|
||||
* 于是前端把能力标成 supportsOta=false。本模块只换**进料口**,不换引擎:
|
||||
* - 分块写入仍走 MotorOtaRelay::feedAt(),offset/幂等/环缓满的判据只有一份;
|
||||
* - 加密、停等、重发、终态判定全部沿用原状态机;
|
||||
* - 通道独占锁同样沿用(EtherCAT 自己的 PDO 运动帧也被它挡在 OTA 之外)。
|
||||
*
|
||||
* 与 BOOT 的 0x2500/0x2501(刷 DCU 自身)的区别:那边落 flash,这边只落 SRAM 暂存区,
|
||||
* 全程不碰 fmc_*,因此不需要关中断擦写那套优雅关停。
|
||||
*
|
||||
* 对象(APP 字典,表项与钩子包装在 comm/ssc/port/dc_app.c):
|
||||
* 0x2510 会话控制(REC of UINT32,读写钩子返回实时值)
|
||||
* 0x2511 数据窗(VAR OCTETSTRING,4096B,载荷 [offset:u32 LE][crc32:u32 LE][data...])
|
||||
*
|
||||
* 本头刻意只用 <stdint.h> 类型:dc_app.c 是 C 且靠相对路径取 ecat_def.h,
|
||||
* 而 comm/ 下的 C++ 拿不到那套 SSC 类型。SDO abort 码的映射留在 dc_app.c 那一层。
|
||||
*/
|
||||
#ifndef COMM_ECAT_MOTOR_OTA_H
|
||||
#define COMM_ECAT_MOTOR_OTA_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
/**
|
||||
* ── 尺寸常量(宏:C 与 C++ 两侧共用同一真源) ──
|
||||
* 0x2511 对象字节数上限,含 8 字节自描述头。
|
||||
* 单条目 BitSize 是 UINT16 ⇒ 8191 字节是天花板,但**真正的上限是 APP 的堆**:
|
||||
* SSC 的分段下载缓冲是 `ALLOCMEM(ROUNDUPBYTE2WORD(size))` 即 malloc(size)
|
||||
* (comm/ssc/src/sdoserv.c:751,池不够就回 abort 0x05040005 Out of memory),
|
||||
* 而 APP 的 `__HEAP_SIZE` 只有 0xC00=3072B(BOOT 是 0x4000,所以 BOOT 的
|
||||
* 0x2501 能用 4096 —— 照抄块长不照抄堆就会当场失败,2026-10-11 实测)。
|
||||
* 取 1024:48KB 镜像 = 47 块,每块 1 initiate + 4 段 ≈ 5 个邮箱帧,
|
||||
* 总邮箱流量仍在亚秒级,且给 malloc 留出 3 倍余量。
|
||||
* 改这里必须同步 tools/ecat/ecat_native.c 的 MFO_CHUNK_OBJ(两边各自断言不了对方)。
|
||||
*/
|
||||
#define ECAT_MOTA_CHUNK_OBJ_BYTES 1024U
|
||||
#define ECAT_MOTA_CHUNK_HDR_BYTES 8U
|
||||
/** 电机固件上限:必须与 MotorOtaRelay::kMaxFwSize(48KB)一致,C++ 侧有 static_assert 守着。 */
|
||||
#define ECAT_MOTA_IMG_MAX_BYTES 0xC000U
|
||||
/** 0x2510 的子索引个数(sub1..sub11)。 */
|
||||
#define ECAT_MOTA_CTL_SUBS 11U
|
||||
|
||||
/**
|
||||
* ── 会话结果码 ──
|
||||
* 与 MotorOtaRelay 的协议错误码(0x01..0x3x,来自电机 BOOT)刻意分段:
|
||||
* 0x8x 永远是「DCU 本端把料挡了」,不是电机报错。看到 0x8x 查链路与参数,
|
||||
* 看到 0x0x/0x1x/0x3x 查电机与 CAN。同一份码既写进 0x2510:10 高字节(主站可读),
|
||||
* 也作为钩子的返回值(dc_app.c 再换成 SDO abort 码)。
|
||||
*/
|
||||
#define ECAT_MOTA_OK 0x00U
|
||||
#define ECAT_MOTA_E_BADLEN 0x81U /* 声明总长或块长非法 */
|
||||
#define ECAT_MOTA_E_CHUNKCRC 0x82U /* 块 CRC32 不符:邮箱搬坏了,主站重发该块 */
|
||||
#define ECAT_MOTA_E_GAP 0x83U /* 块偏移越界/留洞:主站发料顺序错 */
|
||||
#define ECAT_MOTA_E_IMGCRC 0x84U /* 整镜像 CRC32 不符:拒绝进 BOOT,一个字节都没发 */
|
||||
#define ECAT_MOTA_E_BUSY 0x85U /* relay 已在跑(另一条链路或上一会话未结束) */
|
||||
#define ECAT_MOTA_E_NOTARMED 0x86U /* 没声明总长就写数据窗 */
|
||||
#define ECAT_MOTA_E_STARTED 0x87U /* 已 start,暂存区冻结,不再收块 */
|
||||
#define ECAT_MOTA_E_PARAM 0x88U /* 通道/节点参数非法 */
|
||||
#define ECAT_MOTA_E_BOOTPEND 0x89U /* DCU 自己正在交接回 BOOT,此时不许刷电机 */
|
||||
|
||||
#ifdef __cplusplus
|
||||
namespace EcatMotorOta {
|
||||
|
||||
/** 主循环每圈调用:把暂存区泵进环缓,并在会话结束时收标志。 */
|
||||
void process(void);
|
||||
/** 本模块的会话是否活跃(已 start 且 relay 未进终态)。 */
|
||||
bool sessionActive(void);
|
||||
|
||||
} /* namespace EcatMotorOta */
|
||||
|
||||
#define ECAT_MOTA_LINKAGE extern "C"
|
||||
#else
|
||||
#define ECAT_MOTA_LINKAGE extern
|
||||
#endif /* __cplusplus */
|
||||
|
||||
/*---------------------------------------------------------------------------
|
||||
* SDO 钩子(dc_app.c 的对象表引用这三个,实现在 ecat_motor_ota.cpp)
|
||||
*
|
||||
* 返回 ECAT_MOTA_OK 或上面的 ECAT_MOTA_E_*;dc_app.c 把非 0 换成 SDO abort 码,
|
||||
* 主站既拿得到 abort,也能从 0x2510:10 读到具体是哪一条,不用猜。
|
||||
*---------------------------------------------------------------------------*/
|
||||
/** @brief 0x2510 写入:sub1=cmd(1=start / 3=abort)、sub2=总长、sub3=通道、sub4=node、sub5=整镜像 CRC32。 */
|
||||
ECAT_MOTA_LINKAGE uint8_t EcatMotorOtaCtlWrite(uint8_t subindex, uint32_t dataSize,
|
||||
const uint8_t *bytes);
|
||||
|
||||
/** @brief 0x2510 读取:返回该 sub 的实时值(进度不落地缓存,避免读到旧数)。 */
|
||||
ECAT_MOTA_LINKAGE uint32_t EcatMotorOtaCtlRead(uint8_t subindex);
|
||||
|
||||
/** @brief 0x2511 写入:一块镜像(自描述 offset + 块 CRC32),失败即 SDO abort。 */
|
||||
ECAT_MOTA_LINKAGE uint8_t EcatMotorOtaChunkWrite(uint32_t dataSize, const uint8_t *bytes);
|
||||
|
||||
#endif /* COMM_ECAT_MOTOR_OTA_H */
|
||||
@@ -114,6 +114,10 @@ bool EscExmcBus::init() {
|
||||
rcu_periph_clock_enable(RCU_GPIOE);
|
||||
rcu_periph_clock_enable(RCU_AF);
|
||||
rcu_periph_clock_enable(RCU_EXMC);
|
||||
/* 先 deinit 再 init。BOOT 跳转前 exmc_norsram_disable + 关时钟,
|
||||
* SNCTL/SNTCFG 仍留着 BOOT 的配置;APP 若只 enable+init 可能叠在残留位上。
|
||||
* deinit 把 Region0 打回复位默认,再按下面参数重铺。 */
|
||||
exmc_norsram_deinit(CONFIG_ESC_EXMC_BANK_NOR_SRAM_REGION0);
|
||||
/* 配置 16-bit 数据线 D0-D15 为 AF 推挽 */
|
||||
gpio_init(CONFIG_EXMC_D0_PORT, GPIO_MODE_AF_PP, GPIO_OSPEED_50MHZ,
|
||||
CONFIG_EXMC_D0_PIN);
|
||||
@@ -196,19 +200,15 @@ bool EscExmcBus::init() {
|
||||
norsram_init.memory_type = EXMC_MEMORY_TYPE_SRAM;
|
||||
norsram_init.address_data_mux = DISABLE;
|
||||
|
||||
/* 读/写共用时序(非 extended mode)
|
||||
* 2026-08-20 慢档测试(16/16/256 HCLK)无效,已还原:慢档时序未带来任何
|
||||
* 读数变化,速度因素彻底排除(与 bit-bang 40-80us 仍失败一致)。
|
||||
* 注:那轮测试的失败根因是访问模型错误(直读核心 CSR 索引空间,1.5.39 修复),
|
||||
* 与时序无关;时序余量本身仍需按 DS 表 9-4/9-5 独立核算,见下方 BUSLAT。 */
|
||||
/* 读/写共用时序(非 extended mode)。时序余量按 DS 表 9-4/9-5 独立核算,见下方 BUSLAT。 */
|
||||
rw_timing.asyn_access_mode = EXMC_ACCESS_MODE_A;
|
||||
rw_timing.syn_data_latency = EXMC_DATALAT_2_CLK;
|
||||
rw_timing.syn_clk_division = EXMC_SYN_CLOCK_RATIO_DISABLE;
|
||||
/* BUSLAT: 1→4 (1.5.57)。DS 表 9-4/9-5 要求 trdrd/twrwr/twrrd ≥ 13ns,
|
||||
/* BUSLAT: 4。DS 表 9-4/9-5 要求 trdrd/twrwr/twrrd ≥ 13ns,
|
||||
* 该间隔由 EXMC 在两次访问之间插入的总线空闲期承担 = (1 固有 + BUSLAT) HCLK。
|
||||
* BUSLAT=1 → 2 HCLK ≈ 11.1ns,低于 13ns 下限,仅靠 AHB 取指间隙偶然兜底:
|
||||
* 常温刚好可用,温度升高(传播延迟增大)/电压下降(驱动减弱)即越界,表现为
|
||||
* 偶发读到 0xFFFF 或错值——与实测的温漂偶发读错吻合。
|
||||
* 偶发读到 0xFFFF 或错值。
|
||||
* BUSLAT=4 → 5 HCLK ≈ 27.8ns,余量 2.1 倍,覆盖全温区与电压波动。
|
||||
* 代价:每次 HBI 访问多 3 HCLK(16.7ns)。满载 1024B 过程数据按每 DWORD
|
||||
* 两次 16-bit 访问计,增加约 256×2×16.7ns ≈ 8.6us,相对原 218us 增幅 3.9%,
|
||||
@@ -243,7 +243,7 @@ bool EscExmcBus::init() {
|
||||
ECAT_LOGI("[ECAT][EXMC] init: SNCTL0 fix: before=0x%08lX after=0x%08lX (bit7 reserved cleared)\r\n",
|
||||
(unsigned long)snctl_before, (unsigned long)snctl_after);
|
||||
|
||||
/* 时序寄存器实测回读:确认写入的 BUSLAT 真正落到硬件(1.5.57)。
|
||||
/* 时序寄存器回读:确认写入的 BUSLAT 真正落到硬件。
|
||||
* SNTCFG0 位域: [3:0]=ASET-1 [7:4]=AHLD-1 [15:8]=DSET-1 [19:16]=BUSLAT-1。
|
||||
* 期望 ASET=4/AHLD=2/DSET=20/BUSLAT=4 → 0x0313_0000|... 逐字段解码打印,
|
||||
* 避免只看整字导致的位域误读;BUSLAT 实际值 <4 说明库或硬件截断,须立即排查。 */
|
||||
@@ -271,8 +271,8 @@ bool EscExmcBus::init() {
|
||||
"ESC driver\r\n");
|
||||
|
||||
/* ---- 硬件诊断: 转储关键寄存器 + GPIO 手动输出测试 ----
|
||||
* 1.5.33: 默认关闭。硬件层已确认全通,探针仅换板/补焊排查浮空时经
|
||||
* CONFIG_ESC_EXMC_DIAG 临时开启(会拆 EXMC 引脚成 GPIO,正常运行不该跑)。 */
|
||||
* 默认关闭。硬件层已确认全通,探针仅换板/补焊排查浮空时经
|
||||
* CONFIG_ESC_EXMC_DIAG 临时开启(会拆 EXMC 引脚成 GPIO,正常运行不该跑)。 */
|
||||
#if CONFIG_ESC_EXMC_DIAG
|
||||
diagDump();
|
||||
#endif
|
||||
@@ -284,8 +284,8 @@ bool EscExmcBus::init() {
|
||||
|
||||
/* ========================================================================
|
||||
* 硬件诊断: 精简版 —— 只保留核心探活 + bit-bang + 位级回显
|
||||
* 1.5.33: 整段由 CONFIG_ESC_EXMC_DIAG 门控,默认不编译。bbDelay/bbReadData/
|
||||
* bbDriveData 等位拍辅助函数在上方匿名 namespace 中,continuityDrive 仍复用,保留。
|
||||
* 整段由 CONFIG_ESC_EXMC_DIAG 门控,默认不编译。bbDelay/bbReadData/
|
||||
* bbDriveData 等位拍辅助函数在上方匿名 namespace 中,continuityDrive 仍复用,保留。
|
||||
* ======================================================================== */
|
||||
#if CONFIG_ESC_EXMC_DIAG
|
||||
void EscExmcBus::diagDump() {
|
||||
@@ -301,7 +301,7 @@ void EscExmcBus::diagDump() {
|
||||
return;
|
||||
}
|
||||
printf("[EXMC-DIAG] --- bit-bang probe (bypass EXMC) ---\r\n");
|
||||
/* NRST 实际电平回读(1.5.26):输入通路常连引脚,输出态亦读真实电压。
|
||||
/* NRST 实际电平回读:输入通路常连引脚,输出态亦读真实电压。
|
||||
* 驱动高却 LOW => ECAT_NRST 被外部按低 => LAN9252 一直在复位态。 */
|
||||
printf("[EXMC-DIAG] NRST(PB10) reads %s\r\n",
|
||||
(gpio_input_bit_get(CONFIG_ESC_NRST_PORT, CONFIG_ESC_NRST_PIN) == SET)
|
||||
@@ -459,7 +459,7 @@ static void continuityEnterTestMode() {
|
||||
s_save_ctl0_e = GPIO_CTL0(GPIOE);
|
||||
s_save_ctl1_e = GPIO_CTL1(GPIOE);
|
||||
/* 测试期屏蔽 SYNC0/ESC IRQ/SYNC1:ISR 的 HBI 访问落在已被切成 GPIO 态的
|
||||
* 总线上只会读回垃圾并错写 IDX,SSC 状态可能被污染(1.5.29)。 */
|
||||
* 总线上只会读回垃圾并错写 IDX,SSC 状态可能被污染。 */
|
||||
s_maskPrev = EscExmcBus::maskEnter();
|
||||
/* 全部数据线 + 控制线设为 OUT_PP */
|
||||
gpio_init(GPIOD, GPIO_MODE_OUT_PP, GPIO_OSPEED_50MHZ,
|
||||
@@ -571,7 +571,7 @@ uint32_t EscExmcBus::maskEnter() {
|
||||
/* EXTI0=SYNC0(preempt=0) 无法被 BASEPRI 屏蔽,只能关 NVIC;
|
||||
* EXTI1=ESC IRQ / EXTI2=SYNC1 (均 preempt=1) 的 PDI_Isr/Sync1_Isr 同样做
|
||||
* HBI 两步事务访问,一并屏蔽——否则主循环事务会被 Sync1_Isr 的
|
||||
* PDO_InputMapping→HW_EscWriteIsr 打断而错址(1.5.29 修复)。 */
|
||||
* PDO_InputMapping→HW_EscWriteIsr 打断而错址。 */
|
||||
if (NVIC_GetEnableIRQ(EXTI0_IRQn) != 0U) {
|
||||
prev |= 1U;
|
||||
NVIC_DisableIRQ(EXTI0_IRQn);
|
||||
|
||||
@@ -7,19 +7,17 @@
|
||||
* - A2=1,A1=0 (偏移 0x40000) -> HBI_DATA_0 低 16 位
|
||||
* - A2=1,A1=1 (偏移 0x60000) -> HBI_DATA_0 高 16 位
|
||||
*
|
||||
* 地址线换算(1.5.27 修正,此前 0x20000/0x30000 是按 8-bit 规则算的,DATA 访问全部
|
||||
* 落在 HBI_IDX_0 高半字,IDX_0=0x1234 探针假通过而 BYTE_TEST 永远失败):
|
||||
* 地址线换算(16-bit 数据宽度时 EXMC 内部将 HADDR[25:1] 接到 EXMC_A[24:0]):
|
||||
* 板线:LAN9252 A1(Pin25)→PD11=EXMC_A16,A2(Pin29)→PD12=EXMC_A17。
|
||||
* GD32E50x 用户手册 p869:16-bit 数据宽度时 EXMC 内部将 HADDR[25:1] 接到
|
||||
* EXMC_A[24:0],即 EXMC_A16=CPU 偏移 bit17(0x20000)、EXMC_A17=bit18(0x40000)。
|
||||
* LAN9252 手册 Table 9-3:A2=1 选 HBI_DATA_0,A1 选低/高半字。
|
||||
*
|
||||
* 访问模型(1.5.39 按 DS 12.11/表 12-13 修正):
|
||||
* 访问模型(DS 12.11/表 12-13):
|
||||
* HBI 索引直接寻址的只有 LAN9252 系统 CSR(BYTE_TEST 0x064 等)与访问端口;
|
||||
* EtherCAT 核心 CSR(0x0000-0x0FFF)经 ECAT_CSR_CMD(0x304)/ECAT_CSR_DATA(0x300)
|
||||
* 间接访问,过程 RAM(0x1000-0x1FFF)经 PRAM FIFO(0x000-0x03C + 0x308-0x314) 访问。
|
||||
* 1.5.37 及之前把索引空间当统一寻址,导致全部核心 CSR 读 0/写丢弃——
|
||||
* 主站恒读 AL=INIT 的真正根因(网络侧寄存器诊断证实 EEPROM/核心一直正常)。
|
||||
* 核心 CSR 不在 HBI 直接空间,直读恒 0、写丢弃。
|
||||
*/
|
||||
#include "comm/esc_lan9252.h"
|
||||
#include "comm/esc_exmc_bus.h"
|
||||
@@ -59,7 +57,7 @@ constexpr uint32_t IRQ_CFG_IRQ_TYPE = 1UL << 0; /**< 1=推挽,0=开漏(开漏
|
||||
constexpr uint32_t INT_EN_ECAT_INT = 1UL << 0; /**< 允许 EtherCAT 事件驱动 IRQ 引脚。 */
|
||||
|
||||
/* ESC 过程数据看门狗(DS 12.14.41-43)。SII 不携带看门狗字段,芯片复位默认
|
||||
* divider=0x09C2(100µs)/WD Time PD=0x03E8(1000) 即 100ms——低于实测可用的
|
||||
* divider=0x09C2(100µs)/WD Time PD=0x03E8(1000) 即 100ms——等于
|
||||
* 100ms 手动调试周期,SM2 每 100ms 被判定超时,SSC 以 0x001B 拖回 SAFEOP。 */
|
||||
constexpr uint16_t ESC_REG_WD_DIVIDER = 0x0400U; /**< 看门狗分频(100µs 基准,复位默认 0x09C2)。 */
|
||||
constexpr uint16_t ESC_REG_WD_TIME_PD = 0x0420U; /**< 过程数据看门狗步数(复位默认 0x03E8=100ms)。 */
|
||||
@@ -80,6 +78,28 @@ constexpr uint32_t PRAM_BUSY = 1UL << 31; /**< PRAM_*_CMD:写 1 启动,完成
|
||||
constexpr uint32_t PRAM_AVAIL = 1UL << 0; /**< PRAM_*_CMD:数据可用(读)/空间可用(写) */
|
||||
constexpr uint32_t POLL_MAX = 2000UL; /**< busy/avail 轮询上限(~ms 级,防芯片异常时死等) */
|
||||
|
||||
/* 电源管理(DS 6.3/表 5-1):芯片级睡眠 D1-D3。
|
||||
* READY=0 期间仅 HW_CFG/PMT_CTRL/BYTE_TEST/RESET_CTL 四个寄存器可访问(DS 6.2.3),
|
||||
* 恰好覆盖睡眠进入与唤醒流程所需;PMT_CTRL 可在睡眠态安全读(DS 6.3.5.1 注)。 */
|
||||
constexpr uint16_t LAN9252_REG_HW_CFG = 0x0074U; /**< 硬件配置:bit27=READY(与 PMT_CTRL bit0 同源,DS 6.3.5.1)。 */
|
||||
constexpr uint16_t LAN9252_REG_PMT_CTRL = 0x0084U; /**< 功耗管理控制:bit0=READY,bit28=PM_SLEEP_EN,bit31:29=PM_MODE。 */
|
||||
constexpr uint32_t PMT_CTRL_READY = 1UL << 0;
|
||||
constexpr uint32_t PMT_CTRL_PM_MODE_MSK = 7UL << 29; /**< 001=D1 010=D2 011=D3。 */
|
||||
constexpr uint32_t PMT_CTRL_PM_SLEEP_EN = 1UL << 28; /**< 置 1 进入 PM_MODE 指定的睡眠档(须与 PM_MODE 分两次写)。 */
|
||||
|
||||
/* ESC 核心 MII 管理(ETG.1000.4 ESC 寄存器 0x0510-0x0514,经 csrRead/csrWrite 间接访问;
|
||||
* 表 5-1 证实 PHY 寄存器不在 HBI 直接空间)。用于 D3 前置的 PHY General Power Down
|
||||
* (DS 6.3.4:D3 要求两 PHY 先 General Power Down)与唤醒后恢复。 */
|
||||
constexpr uint16_t ESC_REG_MII_CTRL = 0x0510U; /**< bit0=CMD(写 1 启动自清) bit1=WRITE bit8=BUSY。 */
|
||||
constexpr uint16_t ESC_REG_MII_ADDR = 0x0512U; /**< bit4:0=PHY_ADDR bit15:5=REG_ADDR。 */
|
||||
constexpr uint16_t ESC_REG_MII_DATA = 0x0514U; /**< bit15:0=MII 数据。 */
|
||||
constexpr uint16_t MII_CTRL_CMD = 1U << 0;
|
||||
constexpr uint16_t MII_CTRL_WRITE = 1U << 1;
|
||||
constexpr uint16_t MII_CTRL_BUSY = 1U << 8;
|
||||
constexpr uint8_t MII_REG_BMCR = 0U; /**< IEEE 802.3 Basic Control 寄存器 0。 */
|
||||
constexpr uint16_t MII_BMCR_POWER_DOWN = 1U << 11; /**< PHY_PWR_DWN(DS 11.2.8)。 */
|
||||
constexpr uint8_t MII_PHY_ADDR_B = 1U; /**< PHY B 地址恒 1(DS 11.1.1);A 模式相关 0/2,运行期判定。 */
|
||||
|
||||
__attribute__((section(".ramfunc"), noinline))
|
||||
void setHbiIndex(uint16_t addr)
|
||||
{
|
||||
@@ -126,18 +146,93 @@ void writeReg32(uint16_t addr, uint32_t v)
|
||||
writeDataLow((uint16_t)(v & 0xFFFFU));
|
||||
writeDataHigh((uint16_t)(v >> 16));
|
||||
}
|
||||
|
||||
/* ---- 电源管理 helper(仅 pmDrive 诊断路径使用) ---- */
|
||||
|
||||
/** @brief DWT CYCCNT 忙等微秒。
|
||||
* @note EXTI ISR 上下文(0xA2 在 AppSpiCommand::onCsRelease 内同步执行)禁止用
|
||||
* SysTickTimer::delayMs:其基于 SysTick 中断递增 s_ms,中断被 CS EXTI
|
||||
* 压住时计数冻结会死等。DWT CYCCNT 已由 init() 使能,纯寄存器读,ISR 安全。 */
|
||||
__attribute__((section(".ramfunc"), noinline))
|
||||
void pmBusyWaitUs(uint32_t us)
|
||||
{
|
||||
const uint32_t start = DWT->CYCCNT;
|
||||
const uint32_t cycles = us * (SystemCoreClock / 1000000U);
|
||||
while ((DWT->CYCCNT - start) < cycles) { }
|
||||
}
|
||||
|
||||
/** @brief ESC 核心 MII 管理事务(读/写一个 PHY 寄存器)。
|
||||
* @note 调用方必须已持 HBI 互斥(pmDrive 的 maskEnter 段内)。MII 事务典型 ~25µs
|
||||
* (2.5MHz MDIO × 32bit 帧结构),BUSY 轮询上限 POLL_MAX 防芯片异常死等。 */
|
||||
bool miiXfer(uint8_t phyAddr, uint8_t regAddr, bool write, uint16_t &data)
|
||||
{
|
||||
EscLan9252::csrWrite(ESC_REG_MII_ADDR, ((uint32_t)regAddr << 5) | phyAddr, 2U);
|
||||
if (write) {
|
||||
EscLan9252::csrWrite(ESC_REG_MII_DATA, data, 2U);
|
||||
}
|
||||
EscLan9252::csrWrite(ESC_REG_MII_CTRL,
|
||||
write ? (MII_CTRL_CMD | MII_CTRL_WRITE) : MII_CTRL_CMD, 2U);
|
||||
for (uint32_t i = 0U; i < POLL_MAX; i++) {
|
||||
if ((EscLan9252::csrRead(ESC_REG_MII_CTRL, 2U, false) & MII_CTRL_BUSY) == 0U) {
|
||||
if (!write) {
|
||||
data = (uint16_t)EscLan9252::csrRead(ESC_REG_MII_DATA, 2U, false);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** @brief 置/清两 PHY 的 BMCR.PowerDown(D3 进入前置要求/唤醒后恢复)。
|
||||
* @return 全部事务成功 true。PHY A 地址模式相关(DS 11.1.1:0 或 2,随 chip_mode_strap),
|
||||
* 以 BMCR 读回有效性判定(0x0000/0xFFFF=无响应),结果缓存在静态变量。 */
|
||||
bool phySetPowerDown(bool down)
|
||||
{
|
||||
static uint8_t addrA = 0xFFU; /* 0xFF=未判定 */
|
||||
if (addrA == 0xFFU) {
|
||||
addrA = 0U;
|
||||
uint16_t probe = 0xFFFFU;
|
||||
if (!miiXfer(0U, MII_REG_BMCR, false, probe) ||
|
||||
probe == 0x0000U || probe == 0xFFFFU) {
|
||||
addrA = 2U;
|
||||
}
|
||||
}
|
||||
|
||||
bool ok = true;
|
||||
const uint8_t addrs[2] = { addrA, MII_PHY_ADDR_B };
|
||||
for (uint8_t i = 0U; i < 2U; i++) {
|
||||
uint16_t bmcr = 0U;
|
||||
ok = miiXfer(addrs[i], MII_REG_BMCR, false, bmcr) && ok;
|
||||
if (down) {
|
||||
bmcr |= MII_BMCR_POWER_DOWN;
|
||||
} else {
|
||||
bmcr &= (uint16_t)~MII_BMCR_POWER_DOWN;
|
||||
}
|
||||
ok = miiXfer(addrs[i], MII_REG_BMCR, true, bmcr) && ok;
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
/** @brief 把 PMT_CTRL 值打包成单字节状态:bit0=READY bit1=PM_SLEEP_EN bit3:2=PM_MODE。 */
|
||||
uint8_t pmPackStatus(uint32_t pmt)
|
||||
{
|
||||
return (uint8_t)(((pmt & PMT_CTRL_READY) ? 0x01U : 0U) |
|
||||
((pmt & PMT_CTRL_PM_SLEEP_EN) ? 0x02U : 0U) |
|
||||
(((pmt >> 29) & 3U) << 2));
|
||||
}
|
||||
} /* anonymous namespace */
|
||||
|
||||
bool EscLan9252::s_busReady = false;
|
||||
bool EscLan9252::s_coreReady = false;
|
||||
bool EscLan9252::s_resetDone = false;
|
||||
bool EscLan9252::s_pmPhyDown = false;
|
||||
bool EscLan9252::s_pmSuspended = false;
|
||||
|
||||
void EscLan9252::hardwareReset()
|
||||
{
|
||||
/* 受控复位 ECAT_NRST(PB10 → LAN9252 Pin11 RST#,SCH 第1页 Pin47)。
|
||||
* 2026-08-22(1.5.25) 前固件从未初始化 PB10,RST# 仅靠 R44 10k 上拉自然释放,
|
||||
* 不满足手册 18.6.3:tpurstd(电源稳定→RST#释放)≥25ms、trstia(低脉宽)≥200µs,
|
||||
* 且要求释放前时钟输入已稳定。固件执行到此点时上电早已完成、晶振已稳定,
|
||||
* 手册 18.6.3 要求 tpurstd(电源稳定→RST#释放)≥25ms、trstia(低脉宽)≥200µs,
|
||||
* 且释放前时钟输入已稳定。固件执行到此点时上电早已完成、晶振已稳定,
|
||||
* 统一 30ms 低脉宽同时覆盖两个下限。RST# 释放瞬间重锁 CHIP_MODE0/1 strap
|
||||
* (LED_LINK1/2 经 R35/R36 10k 下拉 → 00b = 2 端口 HBI 模式)。
|
||||
* 释放后保持推挽输出高:空闲电平确定,并保留运行期软件复位手段;
|
||||
@@ -148,7 +243,10 @@ void EscLan9252::hardwareReset()
|
||||
gpio_bit_reset(CONFIG_ESC_NRST_PORT, CONFIG_ESC_NRST_PIN); /* RST# 拉低 */
|
||||
SysTickTimer::delayMs(CONFIG_ESC_NRST_ASSERT_MS);
|
||||
gpio_bit_set(CONFIG_ESC_NRST_PORT, CONFIG_ESC_NRST_PIN); /* 释放复位 */
|
||||
SysTickTimer::delayMs(CONFIG_ESC_HBI_POLL_STEP_MS);
|
||||
/* 释放后等 50ms:warm jump 后芯片不是冷上电,内部状态机/PHY
|
||||
* 重锁比 POR 慢;等太短时随后的 HBI 探针会读到启动中垃圾值。
|
||||
* 50ms 仍远小于 300ms 轮询窗,且 init 单次成本可接受。 */
|
||||
SysTickTimer::delayMs(50U);
|
||||
/* 回读实际电平:GD32 输入通路常连引脚,输出态也能读到真实电压。
|
||||
* 驱动高却读回低 => ECAT_NRST 网络被外部短路到地(或引脚损伤),
|
||||
* LAN9252 被按死在复位态 —— 直接解释 HBI 全浮空 + 晶振照振。 */
|
||||
@@ -157,6 +255,8 @@ void EscLan9252::hardwareReset()
|
||||
printf("[ECAT][ESC] hardwareReset: RST# low %ums -> released, pin reads %s\r\n",
|
||||
(unsigned)CONFIG_ESC_NRST_ASSERT_MS,
|
||||
pinHigh ? "HIGH" : "*** LOW (net shorted to GND / pin damaged) ***");
|
||||
/* RST# 复位清掉 PM 睡眠态(芯片回 D0),挂起门控随之解除 */
|
||||
s_pmSuspended = false;
|
||||
}
|
||||
|
||||
uint8_t EscLan9252::nrstDrive(uint8_t action)
|
||||
@@ -169,6 +269,8 @@ uint8_t EscLan9252::nrstDrive(uint8_t action)
|
||||
gpio_init(CONFIG_ESC_NRST_PORT, GPIO_MODE_OUT_PP, GPIO_OSPEED_50MHZ,
|
||||
CONFIG_ESC_NRST_PIN);
|
||||
gpio_bit_set(CONFIG_ESC_NRST_PORT, CONFIG_ESC_NRST_PIN);
|
||||
/* 复位释放后芯片重走上电流程回 D0,睡眠挂起随之解除 */
|
||||
s_pmSuspended = false;
|
||||
break;
|
||||
case 1: /* 持续拉低(手动保持复位) */
|
||||
gpio_init(CONFIG_ESC_NRST_PORT, GPIO_MODE_OUT_PP, GPIO_OSPEED_50MHZ,
|
||||
@@ -178,6 +280,7 @@ uint8_t EscLan9252::nrstDrive(uint8_t action)
|
||||
case 3: /* 释放:浮空输入,网络交给 R44 10k 上拉 */
|
||||
gpio_init(CONFIG_ESC_NRST_PORT, GPIO_MODE_IN_FLOATING, 0,
|
||||
CONFIG_ESC_NRST_PIN);
|
||||
s_pmSuspended = false;
|
||||
break;
|
||||
case 4: /* 只读回,不改驱动 */
|
||||
break;
|
||||
@@ -193,71 +296,231 @@ uint8_t EscLan9252::nrstDrive(uint8_t action)
|
||||
return level ? 1U : 0U;
|
||||
}
|
||||
|
||||
uint8_t EscLan9252::pmDrive(uint8_t op)
|
||||
{
|
||||
if (op > 4U) {
|
||||
return 0xEEU;
|
||||
}
|
||||
if (!s_busReady) {
|
||||
printf("[ECAT][PM] op%u rejected: HBI bus not ready\r\n", op);
|
||||
return 0xE1U;
|
||||
}
|
||||
|
||||
/* 0xA2 在 CS EXTI ISR 上下文执行(AppSpiCommand::onCsRelease 同步调用)。
|
||||
* HBI Indexed 是两步事务(写 IDX→读写 DATA),而 SYNC0(EXTI0,preempt=0)是
|
||||
* 全系统最高优先级、BASEPRI 挡不住,与主循环 HW_* 路径相同必须关 NVIC:
|
||||
* maskEnter 屏蔽 EXTI0/EXTI1 并返回先前状态,ISR 里调用同样正确(纯 NVIC
|
||||
* 寄存器保存/恢复,挂起的中断在 maskExit 后补执行)。
|
||||
* 残余竞态窗口:CS EXTI 优先级更高,可抢占主循环持锁中的 HBI 事务半截——
|
||||
* 人工低频诊断可接受(continuityDrive 直接切走 EXMC 引脚破坏性更大)。 */
|
||||
const uint32_t mask = EscExmcBus::maskEnter();
|
||||
|
||||
uint8_t status;
|
||||
if (op == 0U) {
|
||||
/* 状态查询。注意:HBI 读事务本身就会唤醒睡眠中的芯片,
|
||||
* 本操作的语义 = "读状态(若在睡眠则主动打破睡眠)",非无侵入观测。 */
|
||||
const uint32_t pmt = readReg32(LAN9252_REG_PMT_CTRL);
|
||||
status = pmPackStatus(pmt);
|
||||
/* 读到 D0(SLEEP_EN=0):芯片不在睡眠,若门控仍挂起(如 op4 唤醒超时后
|
||||
* 芯片延迟自醒)则自愈解除,恢复主循环/ISR 的 HBI 访问。 */
|
||||
if ((status & 0x02U) == 0U && s_pmSuspended) {
|
||||
s_pmSuspended = false;
|
||||
printf("[ECAT][PM] status: chip in D0, pm suspend gate released\r\n");
|
||||
}
|
||||
printf("[ECAT][PM] status: PMT_CTRL=0x%08lX READY=%u SLEEP_EN=%u MODE=D%u\r\n",
|
||||
(unsigned long)pmt,
|
||||
(unsigned)((status >> 0) & 1U), (unsigned)((status >> 1) & 1U),
|
||||
(unsigned)((status >> 2) & 3U));
|
||||
} else if (op <= 3U) {
|
||||
/* 进入 D1/D2/D3。D3 最深(晶振/PLL 全关,仅手动唤醒),手册要求先把两个 PHY
|
||||
* 置 General Power Down,否则 PLL 在 D2 不真关/D3 直接不满足前置。 */
|
||||
if (op == 3U) {
|
||||
s_pmPhyDown = phySetPowerDown(true);
|
||||
printf("[ECAT][PM] D3 pre: both PHY power-down %s\r\n",
|
||||
s_pmPhyDown ? "OK" : "FAIL (entering anyway)");
|
||||
}
|
||||
uint32_t v = readReg32(LAN9252_REG_PMT_CTRL);
|
||||
v = (v & ~PMT_CTRL_PM_MODE_MSK) | ((uint32_t)op << 29);
|
||||
/* PM_MODE 与 PM_SLEEP_EN 必须分两次写(DS 6.3.5.1 注:同一写中改 MODE 置 EN 无效)。 */
|
||||
writeReg32(LAN9252_REG_PMT_CTRL, v);
|
||||
writeReg32(LAN9252_REG_PMT_CTRL, v | PMT_CTRL_PM_SLEEP_EN);
|
||||
/* 立即挂起全部 HBI 消费者(task_runner 停 MainLoop/守护/心跳,
|
||||
* ethercat_sync 三 ISR 清 pending 后直接返回)——本函数运行于 CS EXTI ISR,
|
||||
* 退出后主循环第一圈 ECAT_Main 读 0x220 就会把刚睡着的芯片叫醒;
|
||||
* 此前置位窗口内本 ISR 是唯一运行者(maskEnter 已屏 EXTI0/1),无竞态。 */
|
||||
s_pmSuspended = true;
|
||||
/* 进入睡眠后任何 HBI 访问(含读!)都会立即触发唤醒——
|
||||
* 手册 6.3.4.2 只说 HBI "write" 唤醒,但芯片实际按主机 CS 活动判定,
|
||||
* 写入后的读回验证会把芯片叫回 D0。
|
||||
* 故此处置信构造返回值,不再读回;睡眠态观测交给"读状态"(读即唤醒,
|
||||
* 语义=主动打破睡眠),页面轮询须在睡眠态暂停。 */
|
||||
status = (uint8_t)(0x02U | ((uint32_t)op << 2)); /* READY=0 | SLEEP_EN=1 | MODE=op */
|
||||
static const char *const kMode[4] = {"D0", "D1", "D2", "D3"};
|
||||
printf("[ECAT][PM] enter %s: entered, no read-back (HBI read wakes chip)\r\n",
|
||||
kMode[op]);
|
||||
printf("[ECAT][PM] WARNING: EtherCAT comms halted, wake with 0xA2 action 34\r\n");
|
||||
} else {
|
||||
/* 手动唤醒(DS 6.3.4.2):对 BYTE_TEST 做一次 HBI 写触发,随后轮询
|
||||
* BYTE_TEST=0x87654321(接口恢复)→ PMT_CTRL.READY=1(回 D0 且 PLL 稳定)。
|
||||
* 手册标称 ≤2ms,轮询 50µs 步进、上限 10ms——EXTI ISR 内不做长阻塞。 */
|
||||
writeReg32(LAN9252_REG_BYTE_TEST, LAN9252_BYTE_TEST_VALUE);
|
||||
bool awake = false;
|
||||
for (uint32_t i = 0U; i < 200U; i++) {
|
||||
pmBusyWaitUs(50U);
|
||||
if (readReg32(LAN9252_REG_BYTE_TEST) == LAN9252_BYTE_TEST_VALUE) {
|
||||
awake = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (awake) {
|
||||
for (uint32_t i = 0U; i < 200U; i++) {
|
||||
pmBusyWaitUs(50U);
|
||||
if ((readReg32(LAN9252_REG_PMT_CTRL) & PMT_CTRL_READY) != 0U) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
awake = (readReg32(LAN9252_REG_PMT_CTRL) & PMT_CTRL_READY) != 0U;
|
||||
}
|
||||
if (awake && s_pmPhyDown) {
|
||||
/* D3 唤醒后恢复 PHY 上电,链路重新自协商(数秒后 LINK 恢复)。 */
|
||||
const bool restored = phySetPowerDown(false);
|
||||
s_pmPhyDown = !restored;
|
||||
printf("[ECAT][PM] wake: PHY power-up %s (autoneg re-negotiating)\r\n",
|
||||
restored ? "OK" : "FAIL (retry action 34)");
|
||||
}
|
||||
if (awake) {
|
||||
/* 唤醒成功:解除 HBI 挂起,主循环/ISR 恢复运行。
|
||||
* 超时则保持挂起——芯片大概率仍睡着;若实际已醒,下一次 op0
|
||||
* 状态查询读到 D0 会自愈解除。 */
|
||||
s_pmSuspended = false;
|
||||
}
|
||||
status = awake ? pmPackStatus(readReg32(LAN9252_REG_PMT_CTRL)) : 0U;
|
||||
printf("[ECAT][PM] wake: %s -> status=0x%02X (READY=%u SLEEP_EN=%u)\r\n",
|
||||
awake ? "OK" : "TIMEOUT (10ms, poll action 30 / retry 34)",
|
||||
status, (unsigned)(status & 1U), (unsigned)((status >> 1) & 1U));
|
||||
}
|
||||
|
||||
EscExmcBus::maskExit(mask);
|
||||
return status;
|
||||
}
|
||||
|
||||
bool EscLan9252::init()
|
||||
{
|
||||
s_busReady = false; /* 1.5.27:s_busReady 语义 = init 完整成功(EXMC+探针+BYTE_TEST) */
|
||||
s_coreReady = false; /* 1.5.39:s_coreReady 语义 = EEPROM 加载完成、EtherCAT 核心使能 */
|
||||
s_busReady = false; /* EXMC+BYTE_TEST; warm 先探再定是否 RST# */
|
||||
s_coreReady = false; /* s_coreReady 语义 = EEPROM 加载完成、EtherCAT 核心使能 */
|
||||
|
||||
/* 使能 DWT 周期计数器,供 hw_gd32exmc/SSC 执行时间测量 */
|
||||
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
|
||||
DWT->CYCCNT = 0U;
|
||||
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
|
||||
|
||||
/* 受控 RST# 复位(仅首次;重复调用 init 不得二次复位,否则清掉 SSC 配置) */
|
||||
if (!s_resetDone) {
|
||||
hardwareReset();
|
||||
s_resetDone = true;
|
||||
}
|
||||
|
||||
/* 先起 EXMC 再谈复位。
|
||||
* warm jump:BOOT 刚用过 HBI,芯片活着;此时若先 RST# 再起 EXMC,等于
|
||||
* 把一条本来能通的总线按进复位重协商,一旦 RST# 释放时序不稳就整段假死。
|
||||
* cold:POR 后芯片也需要 EXMC 才谈得上探活。
|
||||
* 顺序固定为:EXMC → 无复位探 BYTE_TEST → 不通才 RST# 重来。 */
|
||||
if (!EscExmcBus::init()) {
|
||||
printf("[ECAT][ESC] init: FAIL, EXMC bus not ready (see [ECAT][EXMC] log)\r\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
/* HBI 存活探针(数据手册 9.5.4.2):先直接读 HBI_IDX_0(offset 0,不写任何东西),
|
||||
* 其复位默认值 = 0x1234。RST# 释放后 ESC 需完成内部启动与 EEPROM 加载,
|
||||
* 故按手册建议轮询(此前为单次读,复位后立即读可能仍在启动中)。
|
||||
* 若超时仍 != 0x1234(通常是 0x0000/0xFFFF 或上次写入的回显),
|
||||
* 说明 LAN9252 完全没有驱动总线——CS 未拉低、CHIP_MODE strap 非 00(三端口=SPI only)
|
||||
* 或芯片未出复位/未上电,而不是 EEPROM 问题(HBI 在 EEPROM 空白时也工作)。 */
|
||||
uint16_t idx0 = EscExmcBus::read16(HBI_IDX0_OFFSET);
|
||||
for (uint32_t waited = 0U;
|
||||
idx0 != 0x1234U && waited < CONFIG_ESC_HBI_POLL_TIMEOUT_MS;
|
||||
waited += CONFIG_ESC_HBI_POLL_STEP_MS) {
|
||||
SysTickTimer::delayMs(CONFIG_ESC_HBI_POLL_STEP_MS);
|
||||
idx0 = EscExmcBus::read16(HBI_IDX0_OFFSET);
|
||||
}
|
||||
if (idx0 != 0x1234U) {
|
||||
printf("[ECAT][ESC] init: FAIL, HBI_IDX_0=0x%04X (expect 0x1234) -> LAN not driving bus\r\n",
|
||||
uint16_t idx0 = 0U;
|
||||
uint32_t byteTest = 0U;
|
||||
|
||||
/* 第 0 轮:不复位直接探。warm 残值下 BOOT 刚写过 HBI,BYTE_TEST 应立刻可读。
|
||||
* BYTE_TEST 只证明通路;idx0==0x1234 才证明「芯片处于 POR 默认态」。
|
||||
* warm jump 后 idx0 常为 0x0054(BOOT 索引事务残值)——与刚上电不同。
|
||||
* 为了「跳转 = 上电」,残值时补一发 RST# 回到 POR,再走与 cold 相同的探活。 */
|
||||
idx0 = EscExmcBus::read16(HBI_IDX0_OFFSET);
|
||||
byteTest = readReg32(LAN9252_REG_BYTE_TEST);
|
||||
const bool porLike = (byteTest == LAN9252_BYTE_TEST_VALUE) && (idx0 == 0x1234U);
|
||||
if (porLike) {
|
||||
printf("[ECAT][ESC] probe: BYTE_TEST ok without RST# (idx0=0x%04X) — POR 态,跳过复位\r\n",
|
||||
(unsigned)idx0);
|
||||
printf("[ECAT][ESC] hint: check CS(PD7) pulse, CHIP_MODE0/1 straps=00, RST#/power\r\n");
|
||||
return false;
|
||||
s_resetDone = true;
|
||||
} else if (byteTest == LAN9252_BYTE_TEST_VALUE) {
|
||||
/* 通路活但不是 POR(典型 warm jump:idx0=0x0054)。补 RST# 对齐上电。 */
|
||||
printf("[ECAT][ESC] probe: BYTE_TEST ok but idx0=0x%04X != 0x1234 — warm 残值,"
|
||||
"补 RST# 对齐上电 POR\r\n", (unsigned)idx0);
|
||||
hardwareReset();
|
||||
s_resetDone = true;
|
||||
if (!EscExmcBus::init()) {
|
||||
printf("[ECAT][ESC] init: FAIL, EXMC re-init after RST# (warm residual)\r\n");
|
||||
return false;
|
||||
}
|
||||
idx0 = EscExmcBus::read16(HBI_IDX0_OFFSET);
|
||||
byteTest = readReg32(LAN9252_REG_BYTE_TEST);
|
||||
if (byteTest != LAN9252_BYTE_TEST_VALUE) {
|
||||
printf("[ECAT][ESC] probe: BYTE_TEST=0x%08lX after warm RST#, fall back to multi-round\r\n",
|
||||
(unsigned long)byteTest);
|
||||
} else {
|
||||
printf("[ECAT][ESC] probe: warm RST# ok (idx0=0x%04X, BYTE_TEST=0x87654321) — 与上电同态\r\n",
|
||||
(unsigned)idx0);
|
||||
}
|
||||
}
|
||||
|
||||
/* 再读 BYTE_TEST(0x064)确认接口完全就绪,默认 0x87654321(小端)。
|
||||
* 该值只有 HBI_DATA_0 偏移正确(EXMC 16-bit 地址左移 1 位)才可能读到。 */
|
||||
const uint32_t byteTest = readReg32(LAN9252_REG_BYTE_TEST);
|
||||
if (byteTest != LAN9252_BYTE_TEST_VALUE) {
|
||||
printf("[ECAT][ESC] init: FAIL, BYTE_TEST=0x%08lX (expect 0x87654321)\r\n",
|
||||
(unsigned long)byteTest);
|
||||
printf("[ECAT][ESC] hint: IDX probe passed but DATA access failed -> check HBI_DATA_0 offsets (0x40000/0x60000)\r\n");
|
||||
return false;
|
||||
printf("[ECAT][ESC] probe: BYTE_TEST=0x%08lX idx0=0x%04X, 补 RST# 最多 3 轮\r\n",
|
||||
(unsigned long)byteTest, (unsigned)idx0);
|
||||
s_resetDone = false; /* 失败路径首轮强制 RST#,与上电后探不通时一致 */
|
||||
bool alive = false;
|
||||
for (uint8_t attempt = 0U; attempt < 3U; ++attempt) {
|
||||
if (!s_resetDone || attempt > 0U) {
|
||||
hardwareReset();
|
||||
s_resetDone = true;
|
||||
}
|
||||
/* 复位后重配 EXMC:BOOT 关时钟时 PD/PE 停在 AF,MCU 侧寄存器也可能残留 */
|
||||
if (!EscExmcBus::init()) {
|
||||
continue;
|
||||
}
|
||||
idx0 = EscExmcBus::read16(HBI_IDX0_OFFSET);
|
||||
/* 仅在「像复位中/未驱动」时轮询 0x1234;warm 残值直接测 BYTE_TEST */
|
||||
const bool idxLooksIdle = (idx0 == 0x0000U) || (idx0 == 0xFFFFU);
|
||||
if (idxLooksIdle || idx0 == 0x1234U) {
|
||||
for (uint32_t waited = 0U;
|
||||
idx0 != 0x1234U && waited < CONFIG_ESC_HBI_POLL_TIMEOUT_MS;
|
||||
waited += CONFIG_ESC_HBI_POLL_STEP_MS) {
|
||||
SysTickTimer::delayMs(CONFIG_ESC_HBI_POLL_STEP_MS);
|
||||
idx0 = EscExmcBus::read16(HBI_IDX0_OFFSET);
|
||||
}
|
||||
}
|
||||
byteTest = readReg32(LAN9252_REG_BYTE_TEST);
|
||||
if (byteTest == LAN9252_BYTE_TEST_VALUE) {
|
||||
alive = true;
|
||||
printf("[ECAT][ESC] probe#%u: BYTE_TEST ok (idx0=0x%04X)\r\n",
|
||||
(unsigned)(attempt + 1U), (unsigned)idx0);
|
||||
break;
|
||||
}
|
||||
printf("[ECAT][ESC] probe#%u fail (idx0=0x%04X byte_test=0x%08lX)\r\n",
|
||||
(unsigned)(attempt + 1U), (unsigned)idx0, (unsigned long)byteTest);
|
||||
}
|
||||
if (!alive) {
|
||||
printf("[ECAT][ESC] init: FAIL, BYTE_TEST=0x%08lX (expect 0x87654321), idx0=0x%04X -> LAN not driving bus\r\n",
|
||||
(unsigned long)byteTest, (unsigned)idx0);
|
||||
printf("[ECAT][ESC] hint: check CS(PD7) pulse, CHIP_MODE0/1 straps=00, RST#/power\r\n");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
printf("[ECAT][ESC] init: OK, HBI alive (IDX=0x1234, BYTE_TEST=0x87654321)\r\n");
|
||||
|
||||
if (idx0 != 0x1234U) {
|
||||
printf("[ECAT][ESC] init: WARN idx0=0x%04X != 0x1234, BYTE_TEST ok -> 复位态未知但通路可用,放行\r\n",
|
||||
(unsigned)idx0);
|
||||
}
|
||||
printf("[ECAT][ESC] init: OK, HBI alive (idx0=0x%04X, BYTE_TEST=0x87654321)\r\n",
|
||||
(unsigned)idx0);
|
||||
s_busReady = true;
|
||||
|
||||
/* 等待 PDI 就绪、EtherCAT 核心可交互(1.5.37 新增,1.5.39 改经 CSR 间接端口读——
|
||||
* 此前直读 HBI 索引,DL_STATUS 属 EtherCAT 核心寄存器,根本不在直接地址空间)。
|
||||
/* 等待 PDI 就绪、EtherCAT 核心可交互。
|
||||
* DL_STATUS 属 EtherCAT 核心寄存器,不在 HBI 直接空间,必须经 CSR 间接端口读。
|
||||
* 判据:DL_STATUS(0x0110) bit0 = PDI operational / EEPROM loaded。
|
||||
* 网络侧诊断已证实本板 EEPROM 加载与核心均正常,此等待应几乎立即通过;
|
||||
* 超时意味着 PDI 真的未就绪(EEPROM 内容/I2C/strap 问题),此时 SSC 的
|
||||
* AL Event Mask/邮箱 SM 配置会落空,由 initial.cpp 按 isCoreReady() 门控跳过。 */
|
||||
uint16_t dlStatus = (uint16_t)csrRead(ESC_REG_DL_STATUS, 2U);
|
||||
uint16_t dlStatus = (uint16_t)csrRead(ESC_REG_DL_STATUS, 2U, false);
|
||||
uint32_t waited = 0U;
|
||||
while ((dlStatus & 0x0001U) == 0U && waited < CONFIG_ESC_HBI_POLL_TIMEOUT_MS) {
|
||||
SysTickTimer::delayMs(CONFIG_ESC_HBI_POLL_STEP_MS);
|
||||
waited += CONFIG_ESC_HBI_POLL_STEP_MS;
|
||||
dlStatus = (uint16_t)csrRead(ESC_REG_DL_STATUS, 2U);
|
||||
dlStatus = (uint16_t)csrRead(ESC_REG_DL_STATUS, 2U, false);
|
||||
}
|
||||
if ((dlStatus & 0x0001U) != 0U) {
|
||||
s_coreReady = true;
|
||||
@@ -269,7 +532,7 @@ bool EscLan9252::init()
|
||||
printf("[ECAT][ESC] init: FAIL, PDI not operational after %ums "
|
||||
"(DL_STATUS=0x%04X EEP_CSR=0x%04X)\r\n",
|
||||
(unsigned)CONFIG_ESC_HBI_POLL_TIMEOUT_MS, (unsigned)dlStatus,
|
||||
(unsigned)csrRead(ESC_REG_EEP_CSR, 2U));
|
||||
(unsigned)csrRead(ESC_REG_EEP_CSR, 2U, false));
|
||||
printf("[ECAT][ESC] hint: 用 tools/ecat/ecat_esc_regs.py 从网络侧读 0x0502 错误位区分 EEPROM/I2C/内容问题\r\n");
|
||||
}
|
||||
return true;
|
||||
@@ -277,11 +540,10 @@ bool EscLan9252::init()
|
||||
|
||||
/**
|
||||
* @brief 使能 LAN9252 的 IRQ 引脚输出,让 EtherCAT 事件真正到达 GD32 的 PB1。
|
||||
* @note 1.5.43 新增。IRQ_CFG(0x054)/INT_EN(0x05C) 都是 LAN9252 系统 CSR
|
||||
* @note IRQ_CFG(0x054)/INT_EN(0x05C) 都是 LAN9252 系统 CSR
|
||||
* (HBI 索引直接可达,不走 CSR 间接端口),复位默认值均为 0,即
|
||||
* IRQ_EN=0(引脚永久去断言) 且 ECAT_INT_EN=0(EtherCAT 事件不上报)。
|
||||
* 此前固件从未配置它们,导致 PDI_Isr 一次都没被调用过:SSC 因此
|
||||
* 永远等不到 PROCESS_OUTPUT_EVENT、bEcatFirstOutputsReceived 恒 FALSE,
|
||||
* 不配这两个寄存器,PDI_Isr 不会触发,SSC 永远等不到 PROCESS_OUTPUT_EVENT,
|
||||
* SAFEOP->OP 被无限挂起(表现为主站 "进 OP 失败 code=0x0000")。
|
||||
* 极性必须与 ethercat_sync.cpp 的 EXTI 配置一致:那里是上拉输入 +
|
||||
* 下降沿触发,故此处选推挽(IRQ_TYPE=1) + 低有效(IRQ_POL=0)。
|
||||
@@ -289,13 +551,13 @@ bool EscLan9252::init()
|
||||
*/
|
||||
void EscLan9252::configPdWatchdog()
|
||||
{
|
||||
/* 1.5.45:过程数据看门狗 100ms(复位默认)→1s。
|
||||
/* 过程数据看门狗 100ms(复位默认)→1s。
|
||||
* 主站侧手动单步/100ms 调试周期会触发 SM2 看门狗:SSC 在 CheckIfEcatError()
|
||||
* 读 0x0440 发现已过期,调 AL_ControlInd(STATE_SAFEOP, 0x001B) 把 OP 拖回 SAFEOP,
|
||||
* 表现为"100ms 周期全 WKC 错、10/50ms 正常"。分频保持默认 100µs,只延长步数;
|
||||
* 正常周期(≤50ms)远小于 1s,行为不变。 */
|
||||
csrWrite(ESC_REG_WD_TIME_PD, ESC_WD_TIME_PD_STEPS, 2U);
|
||||
const uint32_t rd = csrRead(ESC_REG_WD_TIME_PD, 2U);
|
||||
const uint32_t rd = csrRead(ESC_REG_WD_TIME_PD, 2U, false);
|
||||
if ((rd & 0xFFFFU) == ESC_WD_TIME_PD_STEPS) {
|
||||
printf("[ECAT][ESC] PD watchdog = 1s (0x0420=%u x 100us, default was 100ms)\r\n",
|
||||
(unsigned)ESC_WD_TIME_PD_STEPS);
|
||||
@@ -326,15 +588,39 @@ void EscLan9252::enableEcatIrqOutput()
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t EscLan9252::csrRead(uint16_t addr, uint8_t size)
|
||||
uint32_t EscLan9252::csrRead(uint16_t addr, uint8_t size, bool verify)
|
||||
{
|
||||
/* DS 12.11.1:写 CSR_CMD(BUSY|R_nW|SIZE|ADDR) 启动,busy 自清后读 CSR_DATA;
|
||||
* 有效数据总在 CSR_DATA 低位,硬件处理字节对齐。 */
|
||||
uint32_t n = POLL_MAX;
|
||||
while ((readReg32(ECAT_CSR_CMD_REG) & CSR_BUSY) != 0U && --n != 0U) {}
|
||||
writeReg32(ECAT_CSR_CMD_REG, CSR_BUSY | CSR_RNW | ((uint32_t)size << 16) | addr);
|
||||
n = POLL_MAX;
|
||||
while ((readReg32(ECAT_CSR_CMD_REG) & CSR_BUSY) != 0U && --n != 0U) {}
|
||||
* 有效数据总在 CSR_DATA 低位,硬件处理字节对齐。
|
||||
* verify=true(默认):CSR_DATA 是锁存寄存器(非 FIFO),命令不变时连读两次必一致;
|
||||
* 不一致即本拍 EXMC 毛刺——错读可能让 GetSyncMan 把错误值缓存成邮箱几何
|
||||
* (→SSC 邮箱楔死,邮箱几何另有 mailbox.c 构建期常量兜底),或让
|
||||
* CheckSmSettings 以瞬态值打断状态转换。对错读整事务重跑一次;
|
||||
* 两轮仍不一致则交出末拍。
|
||||
* verify=false:单读路径,供诊断回读/MII busy 轮询/init 轮询等低风险
|
||||
* 调用方使用;SM/AL/邮箱等状态机关键寄存器必须保持默认双读。 */
|
||||
if (!verify) {
|
||||
uint32_t n = POLL_MAX;
|
||||
while ((readReg32(ECAT_CSR_CMD_REG) & CSR_BUSY) != 0U && --n != 0U) {}
|
||||
writeReg32(ECAT_CSR_CMD_REG, CSR_BUSY | CSR_RNW | ((uint32_t)size << 16) | addr);
|
||||
n = POLL_MAX;
|
||||
while ((readReg32(ECAT_CSR_CMD_REG) & CSR_BUSY) != 0U && --n != 0U) {}
|
||||
return readReg32(ECAT_CSR_DATA_REG);
|
||||
}
|
||||
for (uint8_t attempt = 0U; attempt < 2U; ++attempt)
|
||||
{
|
||||
uint32_t n = POLL_MAX;
|
||||
while ((readReg32(ECAT_CSR_CMD_REG) & CSR_BUSY) != 0U && --n != 0U) {}
|
||||
writeReg32(ECAT_CSR_CMD_REG, CSR_BUSY | CSR_RNW | ((uint32_t)size << 16) | addr);
|
||||
n = POLL_MAX;
|
||||
while ((readReg32(ECAT_CSR_CMD_REG) & CSR_BUSY) != 0U && --n != 0U) {}
|
||||
const uint32_t d1 = readReg32(ECAT_CSR_DATA_REG);
|
||||
const uint32_t d2 = readReg32(ECAT_CSR_DATA_REG);
|
||||
if (d1 == d2)
|
||||
{
|
||||
return d1;
|
||||
}
|
||||
}
|
||||
return readReg32(ECAT_CSR_DATA_REG);
|
||||
}
|
||||
|
||||
@@ -353,7 +639,11 @@ void EscLan9252::pramRead(uint16_t addr, uint8_t *buf, uint16_t len)
|
||||
{
|
||||
/* DS 12.11.3:写 ADDR_LEN 再置 BUSY 启动;引擎按 DWORD 向下对齐填充读 FIFO,
|
||||
* 首 DWORD 有效字节从 (addr&3) 起(表 12-7),末 DWORD 尾部无效(表 12-8)。
|
||||
* 别名口 0x00-0x1C 功能相同,轮换寻址即官方突发用法;每读满一个 DWORD 弹一项。 */
|
||||
* 别名口 0x00-0x1C 功能相同,轮换寻址即官方突发用法;每读满一个 DWORD 弹一项。
|
||||
*
|
||||
* 必须逐 DWORD 轮询并弹前查 AVAIL 位。CMD 的 AVAIL_CNT(bits[12:8])批量弹模式
|
||||
* 对短读不可靠:首 DWORD 新鲜、后续 DWORD 可能取到 FIFO 陈旧项。邮箱短读/探针
|
||||
* 类小传输只验证过"逐 DWORD 查 AVAIL"模式,不要改回批量弹 DWORD。 */
|
||||
if (len == 0U) {
|
||||
return;
|
||||
}
|
||||
@@ -381,7 +671,8 @@ void EscLan9252::pramRead(uint16_t addr, uint8_t *buf, uint16_t len)
|
||||
void EscLan9252::pramWrite(uint16_t addr, const uint8_t *buf, uint16_t len)
|
||||
{
|
||||
/* DS 12.11.4:写 ADDR_LEN 再置 BUSY 启动;首 DWORD 有效字节从 (addr&3) 起,
|
||||
* 引擎只取有效字节;别名口 0x20-0x3C 轮换;写满一个 DWORD 推一项。 */
|
||||
* 引擎只取有效字节;别名口 0x20-0x3C 轮换;写满一个 DWORD 推一项。
|
||||
* 与 pramRead 同样逐 DWORD 轮询(推前必查 AVAIL 位),邮箱小 PDU 写同类风险。 */
|
||||
if (len == 0U) {
|
||||
return;
|
||||
}
|
||||
@@ -417,7 +708,7 @@ uint32_t EscLan9252::readChipId()
|
||||
|
||||
uint16_t EscLan9252::initOnlyReadReg(uint16_t addr)
|
||||
{
|
||||
/* 1.5.39:ESC 核心 CSR 经间接端口读(此前直读 HBI 索引,核心寄存器不在直接空间,恒 0) */
|
||||
/* ESC 核心 CSR 经间接端口读(核心寄存器不在 HBI 直接空间,直读恒 0) */
|
||||
return (uint16_t)csrRead(addr, 2U);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* @file esc_lan9252.h
|
||||
* @brief LAN9252 EtherCAT 从站控制器(ESC)驱动接口。
|
||||
* @note 通过 EscExmcBus 16-bit 并行总线,以 HBI Indexed Address Mode 访问 LAN9252。
|
||||
* 访问模型(1.5.39 按 DS 12.11 修正,此前误以为整个 16 位索引空间统一可寻址):
|
||||
* 访问模型(DS 12.11):
|
||||
* - HBI 直接地址空间:仅 LAN9252 系统 CSR(BYTE_TEST 0x064 等)+ 访问端口
|
||||
* (PRAM FIFO 0x000-0x03C、ECAT_CSR_DATA/CMD 0x300/0x304、PRAM_ADDR_LEN/CMD
|
||||
* 0x308-0x314)——经 HBI_IDX_0/HBI_DATA_0 直接读写;
|
||||
@@ -29,7 +29,7 @@
|
||||
class EscLan9252 {
|
||||
public:
|
||||
/** @brief 初始化 EXMC 总线与 ESC 基本寄存器。
|
||||
* @note 1.5.25 起内部先执行一次 hardwareReset() 受控复位(s_resetDone 门控,
|
||||
* @note 内部先执行一次 hardwareReset() 受控复位(s_resetDone 门控,
|
||||
* 重复调用不会二次复位 SSC 已配置好的芯片),再探活 EXMC/HBI。
|
||||
* @return bool true 表示 EXMC 总线探活通过(可继续读取芯片 ID)。 */
|
||||
static bool init();
|
||||
@@ -46,6 +46,18 @@ public:
|
||||
* 驱动高读回低 => ECAT_NRST 被按死在复位,LAS9252 永在复位态。 */
|
||||
static uint8_t nrstDrive(uint8_t action);
|
||||
|
||||
/** @brief 诊断:LAN9252 芯片级电源管理(休眠/唤醒,SPI 0xA2 action 30+ 转发到此)。
|
||||
* @param op 0=读状态 1=进 D1 2=进 D2 3=进 D3(先两 PHY General Power Down) 4=手动唤醒
|
||||
* @return 打包状态字节 bit0=READY bit1=PM_SLEEP_EN bit3:2=PM_MODE;
|
||||
* 唤醒 op 另有 1=成功 0=超时;0xEE=无效 op;0xE1=HBI 总线未就绪。
|
||||
* @note DS 6.3:PMT_CTRL(0x084) PM_MODE[31:29] 选档 + PM_SLEEP_EN(bit28) 进入,
|
||||
* 两字段必须分两次写。D1/D2/D3 下主机接口停摆、EtherCAT 通信中断(主站
|
||||
* wkc 报错),唤醒后 PM_MODE/PM_SLEEP_EN 自清、寄存器配置保留、READY 回高
|
||||
* (正常 ≤2ms)。D3 最深(晶振/PLL 全关)且手册要求先置两 PHY BMCR.PowerDown
|
||||
* (经 ESC 核心 MII 管理 0x0510 间接访问,PHY A 地址模式相关 0/2 运行期判定),
|
||||
* 唤醒后本函数自动恢复 PHY 上电,链路重新自协商需数秒。 */
|
||||
static uint8_t pmDrive(uint8_t op);
|
||||
|
||||
/**
|
||||
* @brief 读取 ESC 芯片 ID(INIT-ONLY,供 init 打印诊断用)。
|
||||
* @return uint32_t 32-bit 芯片 ID(期望值 0x87654321)。
|
||||
@@ -53,20 +65,23 @@ public:
|
||||
static uint32_t readChipId();
|
||||
|
||||
/** @brief INIT-ONLY:仅允许在 EthercatSync::init() 使能 EXTI NVIC 之前调用;运行期禁止调用,需 ESC 寄存器访问须通过 SSC 移植层(HW_*)。
|
||||
* @note 1.5.39 起经 CSR_CMD/CSR_DATA 间接端口读 ESC 核心 CSR(此前直读 HBI 索引,
|
||||
* 而 EtherCAT 核心寄存器根本不在 HBI 直接地址空间,恒读 0)。 */
|
||||
* @note 经 CSR_CMD/CSR_DATA 间接端口读 ESC 核心 CSR
|
||||
* (EtherCAT 核心寄存器不在 HBI 直接地址空间,直读恒 0)。 */
|
||||
static uint16_t initOnlyReadReg(uint16_t addr);
|
||||
|
||||
/** @brief INIT-ONLY:仅允许在 EthercatSync::init() 使能 EXTI NVIC 之前调用;运行期禁止调用。 */
|
||||
static void initOnlyWriteReg(uint16_t addr, uint16_t v);
|
||||
|
||||
/** @brief ESC 核心 CSR 间接读(ECAT_CSR_CMD/DATA 端口,0x0000-0x0FFF 地址空间)。
|
||||
* @note 1.5.39:LAN9252 的 HBI/SPI 直接地址空间只有 LAN9252 系统 CSR 与访问端口
|
||||
* @note LAN9252 的 HBI/SPI 直接地址空间只有 LAN9252 系统 CSR 与访问端口
|
||||
* (DS 12.11/表 12-13),EtherCAT 核心寄存器(0x0000-0x0FFF)必须经
|
||||
* CSR_CMD(0x304)/CSR_DATA(0x300) 间接访问;过程 RAM(0x1000-0x1FFF)
|
||||
* 必须经 PRAM FIFO。size=1/2/4(size2 需偶地址,size4 需 4 字节对齐)。
|
||||
* 调用方负责 HBI 互斥(主循环 EscExmcBus::maskEnter / 已在 ISR 上下文)。 */
|
||||
static uint32_t csrRead(uint16_t addr, uint8_t size);
|
||||
* 调用方负责 HBI 互斥(主循环 EscExmcBus::maskEnter / 已在 ISR 上下文)。
|
||||
* @param verify true(默认)=双读比对+不一致重试(防 EXMC 毛刺,SM/AL/邮箱
|
||||
* 等关键路径必须开);false=单读(诊断回读/MII busy 轮询/init 等低风险路径,
|
||||
* 省约一半 CSR 间接访问)。 */
|
||||
static uint32_t csrRead(uint16_t addr, uint8_t size, bool verify = true);
|
||||
|
||||
/** @brief ESC 核心 CSR 间接写(ECAT_CSR_CMD/DATA 端口)。约束同 csrRead。 */
|
||||
static void csrWrite(uint16_t addr, uint32_t data, uint8_t size);
|
||||
@@ -77,31 +92,44 @@ public:
|
||||
/** @brief 过程 RAM(0x1000-0x1FFF)经 PRAM 写 FIFO 写入 len 字节。 */
|
||||
static void pramWrite(uint16_t addr, const uint8_t *buf, uint16_t len);
|
||||
|
||||
/** @brief EXMC 总线 + HBI_IDX_0 探针 + BYTE_TEST 全部通过(init 成功)后为 true。
|
||||
* @note 与 init() 返回值一致,可作运行期查询。 */
|
||||
/** @brief EXMC 总线 + BYTE_TEST 往返探针通过(init 成功)后为 true。
|
||||
* @note HBI_IDX_0 复位默认值仅作诊断读数,不参与放行
|
||||
* (它只证明"芯片刚出过复位",证明不了 HBI 通路)。
|
||||
* 与 init() 返回值一致,可作运行期查询。 */
|
||||
static bool isBusReady() { return s_busReady; }
|
||||
|
||||
/** @brief EEPROM(SII) 加载完成、EtherCAT 核心使能(1.5.39)。
|
||||
/** @brief EEPROM(SII) 加载完成、EtherCAT 核心使能。
|
||||
* @note false 时核心寄存器对 PDI 读全 0、写静默丢弃,SSC MainInit 会全部
|
||||
* 落空(AL Event Mask/邮箱 SM 配置写不进),调用方必须跳过 SSC 初始化。 */
|
||||
static bool isCoreReady() { return s_coreReady; }
|
||||
|
||||
/** @brief ESC 已被 pmDrive 置入 D1-D3 睡眠、所有 HBI 消费者必须静默。
|
||||
* @note 睡眠中任何 HBI 访问(含读)都会把芯片打回 D0。置位期间
|
||||
* task_runner 停调 MainLoop()/AL 守护/AL 心跳,ethercat_sync 三个 EXTI
|
||||
* ISR 清 pending 后直接返回——否则芯片进入睡眠后毫秒级就被主循环的
|
||||
* ECAT_Main(读 0x220)叫醒,根本睡不住。
|
||||
* 解除:pmDrive 唤醒/读到 D0、nrstDrive 释放复位、hardwareReset。
|
||||
* ISR 上下文安全:读一个 bool 静态成员,无锁。 */
|
||||
static bool pmSuspended() { return s_pmSuspended; }
|
||||
|
||||
private:
|
||||
/** @brief 使能 LAN9252 的 IRQ 引脚输出(IRQ_CFG 0x054 / INT_EN 0x05C)。
|
||||
* @note 1.5.43 新增,由 init() 在 PDI/核心就绪后调用。两寄存器复位默认全 0,
|
||||
* @note 由 init() 在 PDI/核心就绪后调用。两寄存器复位默认全 0,
|
||||
* 不配则 EtherCAT 事件永不到达 PB1、SSC 的 PDI_Isr 从不执行,
|
||||
* bEcatFirstOutputsReceived 恒 FALSE 使 SAFEOP->OP 被无限挂起。 */
|
||||
* SAFEOP->OP 被无限挂起。 */
|
||||
static void enableEcatIrqOutput();
|
||||
|
||||
/** @brief 过程数据看门狗 100ms(复位默认)→1s(1.5.45)。
|
||||
/** @brief 过程数据看门狗 100ms(复位默认)→1s。
|
||||
* @note 由 init() 在 PDI/核心就绪后调用。写 ESC_REG_WD_TIME_PD(0x0420)=10000
|
||||
* (100µs 分频保持默认)。不写则主站 100ms 手动调试周期触发 SM2 看门狗,
|
||||
* SSC 以 0x001B 拖回 SAFEOP。 */
|
||||
static void configPdWatchdog();
|
||||
|
||||
static bool s_busReady; /**< init 完整成功(EXMC+IDX 探针+BYTE_TEST)后置位。 */
|
||||
static bool s_coreReady; /**< EEPROM 加载完成、EtherCAT 核心使能后置位(1.5.39)。 */
|
||||
static bool s_busReady; /**< init 成功(EXMC+BYTE_TEST 往返;IDX 残值不挡)后置位。 */
|
||||
static bool s_coreReady; /**< EEPROM 加载完成、EtherCAT 核心使能后置位。 */
|
||||
static bool s_resetDone; /**< hardwareReset 已执行门控(防二次复位清掉 SSC 配置)。 */
|
||||
static bool s_pmPhyDown; /**< pmDrive 进 D3 时两 PHY 已被置 PowerDown,唤醒后需恢复。 */
|
||||
static bool s_pmSuspended; /**< ESC 睡眠挂起:置位期间全固件 HBI 消费者静默(见 pmSuspended)。 */
|
||||
};
|
||||
|
||||
#endif /* COMM_ESC_LAN9252_H */
|
||||
@@ -1,10 +1,12 @@
|
||||
/**
|
||||
* @file ethercat_can_bridge.cpp
|
||||
* @brief EtherCAT PDO ↔ CAN 转发桥实现(1.5.56)。
|
||||
* @brief EtherCAT PDO ↔ CAN 转发桥实现。
|
||||
*/
|
||||
#include "comm/ethercat_can_bridge.h"
|
||||
#include "device/canfd.h"
|
||||
#include "device/motor_state.h"
|
||||
#include "sys/motor_ota_relay.h"
|
||||
#include "sys/systick.h"
|
||||
|
||||
#include "RTE_Components.h"
|
||||
#include CMSIS_device_header /* __DMB():seqlock 的编译器/总线屏障 */
|
||||
@@ -13,12 +15,16 @@
|
||||
|
||||
volatile uint8_t EthercatCanBridge::s_lastSeq = 0U;
|
||||
volatile bool EthercatCanBridge::s_seqPrimed = false;
|
||||
volatile bool EthercatCanBridge::s_coldStartPending = 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 uint32_t EthercatCanBridge::s_lastTxId = 0U;
|
||||
volatile uint16_t EthercatCanBridge::s_rx00ffCount = 0U;
|
||||
volatile uint8_t EthercatCanBridge::s_outSeenSeq = 0U;
|
||||
|
||||
volatile bool EthercatCanBridge::s_rxValid = false;
|
||||
volatile bool EthercatCanBridge::s_rxPublished = false;
|
||||
volatile uint32_t EthercatCanBridge::s_xrxSeq = 0U;
|
||||
volatile uint8_t EthercatCanBridge::s_rxCh = 0U;
|
||||
volatile uint8_t EthercatCanBridge::s_rxDlc = 0U;
|
||||
@@ -27,14 +33,29 @@ 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;
|
||||
volatile bool EthercatCanBridge::s_verValid = false;
|
||||
volatile uint32_t EthercatCanBridge::s_verHoldUntilMs = 0U;
|
||||
uint8_t EthercatCanBridge::s_verCh = 0U;
|
||||
uint8_t EthercatCanBridge::s_verDlc = 0U;
|
||||
uint32_t EthercatCanBridge::s_verId = 0U;
|
||||
uint8_t EthercatCanBridge::s_verData[kEcatCanXRxDataMax] = {0};
|
||||
|
||||
namespace {
|
||||
/** 小端读写:PDO 字节序按 EtherCAT 惯例固定小端,不依赖编译器结构体布局。 */
|
||||
/** 版本应答在 PDO 段3 的保持时长(ms)。主站 1ms 周期下足够被采到。 */
|
||||
constexpr uint32_t kVerHoldMs = 300U;
|
||||
|
||||
inline uint16_t canSubId(uint32_t id) { return static_cast<uint16_t>(id & 0xFFFFU); }
|
||||
} // namespace
|
||||
|
||||
uint8_t EthercatCanBridge::s_motShadow[kEcatMotRegionBytes] = {0};
|
||||
uint32_t EthercatCanBridge::s_motPubSeq[kMotorMaxNodes + 1U] = {0};
|
||||
uint8_t EthercatCanBridge::s_motPubOnline[kMotorMaxNodes + 1U] = {0};
|
||||
uint16_t EthercatCanBridge::s_motPubMask = 0U;
|
||||
uint8_t EthercatCanBridge::s_motPubCount = 0U;
|
||||
bool EthercatCanBridge::s_motShadowValid = false;
|
||||
|
||||
namespace {
|
||||
/** 小端读写:PDO 字节序按 EtherCAT 惯例固定小端。 */
|
||||
inline uint32_t rd32le(const uint8_t *p)
|
||||
{
|
||||
return (uint32_t)p[0] | ((uint32_t)p[1] << 8) |
|
||||
@@ -49,13 +70,19 @@ inline void wr16le(uint8_t *p, uint16_t v)
|
||||
{
|
||||
p[0] = (uint8_t)(v); p[1] = (uint8_t)(v >> 8);
|
||||
}
|
||||
|
||||
/** 段 2 头 + count 槽在影子区中的总长度。 */
|
||||
inline uint16_t motRegionLen(uint8_t count)
|
||||
{
|
||||
return (uint16_t)(kEcatMotHdrSize + (uint16_t)count * kEcatMotSlotSize);
|
||||
}
|
||||
} // 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 完全一致 */
|
||||
return 0U;
|
||||
}
|
||||
if (want > kEcatTxBatchMax) {
|
||||
want = kEcatTxBatchMax;
|
||||
@@ -66,8 +93,6 @@ uint8_t EthercatCanBridge::sendBatch(const uint8_t *out)
|
||||
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;
|
||||
}
|
||||
@@ -84,10 +109,13 @@ uint8_t EthercatCanBridge::sendBatch(const uint8_t *out)
|
||||
CanFd::CanTxResult r;
|
||||
if (!CanFd::isChannelEnabled(ch)) {
|
||||
r = CanFd::CanTxResult::kErrInvalid;
|
||||
} else if (MotorOtaRelay::isChannelLocked(ch)) {
|
||||
/* 该通道正被电机 OTA 独占:PDO 运动帧一律不投递。
|
||||
* OTA 用单发模式(ARD=1),一旦挤进同一个邮箱就会 abort 掉在途的
|
||||
* 0x101 固件包,或让电机 BOOT 收到非预期长度直接回 0x05/0x06。
|
||||
* 计数与 lastTxResult 走 0xE7,主站侧能看到「被 OTA 挡了」而不是「邮箱满」。 */
|
||||
r = CanFd::CanTxResult::kErrOtaLocked;
|
||||
} else {
|
||||
/* 与单帧路径同样用 enqueueTx:批量下发必然连续打同一通道,
|
||||
* transmitFrame 的 allow_abort 会让后帧顶掉前帧(历史上 7 帧
|
||||
* 只发出 3 帧的根因),队列语义才不静默丢帧。 */
|
||||
r = CanFd::enqueueTx(ch, rd32le(&f[ECAT_FRM_OFF_ID]),
|
||||
&f[ECAT_FRM_HDR_SIZE], dlc);
|
||||
}
|
||||
@@ -95,6 +123,7 @@ uint8_t EthercatCanBridge::sendBatch(const uint8_t *out)
|
||||
if (r == CanFd::CanTxResult::kOk) {
|
||||
++s_txCount;
|
||||
++sent;
|
||||
s_lastTxId = rd32le(&f[ECAT_FRM_OFF_ID]);
|
||||
} else {
|
||||
s_lastTxResult = (uint8_t)r;
|
||||
if (stopOnError) {
|
||||
@@ -106,43 +135,141 @@ uint8_t EthercatCanBridge::sendBatch(const uint8_t *out)
|
||||
return sent;
|
||||
}
|
||||
|
||||
void EthercatCanBridge::fillRxBatch(uint8_t *in)
|
||||
/**
|
||||
* 段 2:电机整轮密排。
|
||||
*
|
||||
* 1. 扫 MotorState,每个节点号 1..9 在 3 个通道里取 seq 最新的一条;
|
||||
* 2. 若节点集合与各节点 seq 均与上一拍发布相同 → 整段复用影子区(跳过重读);
|
||||
* 3. 否则按 node_id 升序密排写入,空节点不占位;seqlock 奇数(写入中)的槽
|
||||
* 本拍省略,下拍自愈 —— 不发半新半旧的 48B。
|
||||
*/
|
||||
uint8_t EthercatCanBridge::fillMotorRound(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; /* 快照一次:生产者可能边填边推进 */
|
||||
const MotorStateTable &mt = MotorState::table();
|
||||
|
||||
while (tail != head && count < kEcatRxBatchMax) {
|
||||
const RxRingItem &it = s_rxRing[tail];
|
||||
uint8_t dlc = it.dlc;
|
||||
if (dlc > kEcatCanFdDataMax) {
|
||||
dlc = kEcatCanFdDataMax;
|
||||
/* ── 1. 每节点选一条最新槽 ── */
|
||||
const MotorSlot *picks[kMotorMaxNodes];
|
||||
uint8_t nodes[kMotorMaxNodes];
|
||||
uint8_t n = 0U;
|
||||
|
||||
for (uint8_t node = 1U; node <= kMotorMaxNodes; ++node) {
|
||||
const MotorSlot *best = nullptr;
|
||||
for (uint8_t ch = 0U; ch < kMotorChannels; ++ch) {
|
||||
const MotorSlot &s =
|
||||
mt.slots[(uint8_t)((node - 1U) * kMotorChannels + ch)];
|
||||
if (s.node_id == 0U) {
|
||||
continue;
|
||||
}
|
||||
if (best == nullptr || s.seq >= best->seq) {
|
||||
best = &s;
|
||||
}
|
||||
}
|
||||
/* 装不下就停,**且不推进 tail** —— 该帧留到下个周期,不丢。 */
|
||||
if ((uint16_t)(off + ECAT_FRM_HDR_SIZE + dlc) > kEcatPdoInBytes) {
|
||||
break;
|
||||
if (best != nullptr) {
|
||||
picks[n] = best;
|
||||
nodes[n] = node;
|
||||
++n;
|
||||
}
|
||||
|
||||
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 是"自上次回填以来"的增量 */
|
||||
/* ── 2. 判「上一轮已填充且未变」──
|
||||
* 节点集合一致 + 每节点 seq/online 一致 + 头字段(active/boot)一致 + 写侧
|
||||
* seqlock 偶数 → 复用影子。online 必须比:checkOffline 超时置 0 时不 bump
|
||||
* seq,只比 seq 会把「已离线」永远报成在线。 */
|
||||
bool unchanged = s_motShadowValid && (n == s_motPubCount)
|
||||
&& mt.active_count == s_motShadow[ECAT_MOTW_OFF_ACTIVE - ECAT_MOTW_OFF_COUNT]
|
||||
&& mt.boot_active == s_motShadow[ECAT_MOTW_OFF_BOOT - ECAT_MOTW_OFF_COUNT]
|
||||
&& s_motShadow[ECAT_MOTW_OFF_MAGIC - ECAT_MOTW_OFF_COUNT]
|
||||
== kEcatMotLayoutMagic;
|
||||
if (unchanged) {
|
||||
for (uint8_t i = 0U; i < n; ++i) {
|
||||
const uint8_t node = nodes[i];
|
||||
const MotorSlot &s = *picks[i];
|
||||
if ((s.wr_seq & 1U) != 0U
|
||||
|| s.seq != s_motPubSeq[node]
|
||||
|| s.online != s_motPubOnline[node]
|
||||
|| (s_motPubMask & (uint16_t)(1U << node)) == 0U) {
|
||||
unchanged = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const uint16_t region = motRegionLen(unchanged ? s_motPubCount : n);
|
||||
|
||||
/* 集合+seq+online 未变:整段复用影子,避免 8kHz 下每拍重建密排区。
|
||||
* 头字段 ACTIVE/BOOT/MAGIC/GSEQ 在影子起点 COUNT 之后,一并复用。 */
|
||||
if (unchanged && s_motShadowValid && s_motPubCount > 0U) {
|
||||
memcpy(&in[ECAT_MOTW_OFF_COUNT], s_motShadow, region);
|
||||
return s_motPubCount;
|
||||
}
|
||||
|
||||
/* ── 3. 重建密排区 ── */
|
||||
in[ECAT_MOTW_OFF_ACTIVE] = mt.active_count;
|
||||
in[ECAT_MOTW_OFF_BOOT] = mt.boot_active;
|
||||
in[ECAT_MOTW_OFF_MAGIC] = kEcatMotLayoutMagic;
|
||||
wr32le(&in[ECAT_MOTW_OFF_GLOBAL_SEQ], mt.global_seq);
|
||||
|
||||
uint8_t w = 0U;
|
||||
uint16_t newMask = 0U;
|
||||
for (uint8_t i = 0U; i < n; ++i) {
|
||||
const MotorSlot &s = *picks[i];
|
||||
if ((s.wr_seq & 1U) != 0U) {
|
||||
continue; /* 写入中:本拍省略,下拍自愈 */
|
||||
}
|
||||
__DMB();
|
||||
|
||||
uint8_t *p = &in[ECAT_MOTW_OFF_SLOTS + (uint16_t)w * kEcatMotSlotSize];
|
||||
p[ECAT_MOT_OFF_NODE] = nodes[i];
|
||||
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);
|
||||
|
||||
s_motPubSeq[nodes[i]] = s.seq;
|
||||
s_motPubOnline[nodes[i]] = s.online;
|
||||
newMask |= (uint16_t)(1U << nodes[i]);
|
||||
++w;
|
||||
}
|
||||
|
||||
in[ECAT_MOTW_OFF_COUNT] = w;
|
||||
|
||||
/* 更新影子:下次集合+seq 未变时整段复用 */
|
||||
memcpy(s_motShadow, &in[ECAT_MOTW_OFF_COUNT], motRegionLen(w));
|
||||
s_motPubMask = newMask;
|
||||
s_motPubCount = w;
|
||||
s_motShadowValid = true;
|
||||
return w;
|
||||
}
|
||||
|
||||
bool EthercatCanBridge::peekOutput(const uint8_t *head)
|
||||
{
|
||||
/* 输出探针:12B 头里 SEQ@0 / FLAGS。seq 无变化沿且无清统计请求时返回 false,
|
||||
* PDO_OutputMapping 只补读 SM2 末字,跳过 384B 全量读。
|
||||
* 恒 true 会让 8kHz 每拍都全量读,单拍被 PRAM 打爆(6kHz 即掉速)。 */
|
||||
if (head == nullptr) {
|
||||
return false;
|
||||
}
|
||||
const uint8_t seq = head[ECAT_CAN_TX_OFF_SEQ];
|
||||
|
||||
if (!s_seqPrimed) {
|
||||
s_seqPrimed = true;
|
||||
s_lastSeq = seq;
|
||||
s_coldStartPending = true;
|
||||
return true; /* 冷启动首包强制全量读,避免漏掉首帧命令 */
|
||||
}
|
||||
|
||||
if (seq != s_lastSeq) {
|
||||
return true;
|
||||
}
|
||||
return (head[ECAT_CAN_TX_OFF_FLAGS] & 0x01U) != 0U;
|
||||
}
|
||||
|
||||
void EthercatCanBridge::resetSequenceState()
|
||||
{
|
||||
s_seqPrimed = false;
|
||||
s_lastSeq = 0U;
|
||||
s_coldStartPending = false;
|
||||
}
|
||||
|
||||
void EthercatCanBridge::processOutput(const uint8_t *out)
|
||||
@@ -152,60 +279,71 @@ void EthercatCanBridge::processOutput(const uint8_t *out)
|
||||
}
|
||||
|
||||
const uint8_t seq = out[ECAT_CAN_TX_OFF_SEQ];
|
||||
s_outSeenSeq = seq; /* 诊断:processOutput 本拍看到的输出 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;
|
||||
s_seqPrimed = true;
|
||||
s_coldStartPending = true;
|
||||
}
|
||||
|
||||
/* flags bit0:清零统计(一次性,不依赖 seq 沿,便于主站随时归零) */
|
||||
/* 无论 seq 是否变化,优先处理非 seq 驱动的命令(FLAGS bit0 清统计) */
|
||||
if ((out[ECAT_CAN_TX_OFF_FLAGS] & 0x01U) != 0U) {
|
||||
s_txCount = 0U;
|
||||
s_dropCount = 0U;
|
||||
s_rxCount = 0U;
|
||||
s_txCount = 0U;
|
||||
s_dropCount = 0U;
|
||||
s_rxCount = 0U;
|
||||
s_rx00ffCount = 0U;
|
||||
s_rxPublished = true;
|
||||
}
|
||||
|
||||
if (seq == s_lastSeq) {
|
||||
return; /* 没有新的发送请求 */
|
||||
const uint8_t dlcRaw = out[ECAT_CAN_TX_OFF_DLC];
|
||||
const uint8_t batchCount = out[ECAT_TXB_OFF_COUNT];
|
||||
const bool hasPayload = (dlcRaw > 0U) || (batchCount > 0U);
|
||||
|
||||
bool executeCommand = false;
|
||||
if (s_coldStartPending) {
|
||||
s_coldStartPending = false;
|
||||
/* 冷启动:有载荷就发(不依赖 s_lastSeq 是否已被探针碰过) */
|
||||
if (hasPayload) {
|
||||
executeCommand = true;
|
||||
} else {
|
||||
s_lastSeq = seq;
|
||||
return;
|
||||
}
|
||||
} else if (seq != s_lastSeq) {
|
||||
executeCommand = true;
|
||||
} else if (hasPayload && seq != s_lastSeq) {
|
||||
executeCommand = true;
|
||||
}
|
||||
|
||||
if (!executeCommand) {
|
||||
return;
|
||||
}
|
||||
s_lastSeq = seq;
|
||||
|
||||
const uint8_t ch = out[ECAT_CAN_TX_OFF_CH];
|
||||
uint8_t dlc = out[ECAT_CAN_TX_OFF_DLC];
|
||||
uint8_t dlc = dlcRaw;
|
||||
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 if (MotorOtaRelay::isChannelLocked(ch)) {
|
||||
/* 同 sendBatch:OTA 独占通道,周期运动帧不投邮箱(0xE7 上报主站)。 */
|
||||
s_lastTxResult = (uint8_t)CanFd::CanTxResult::kErrOtaLocked;
|
||||
} 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;
|
||||
s_lastTxId = rd32le(&out[ECAT_CAN_TX_OFF_ID]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 批量区:同一个 seq 沿里一次性发 N 帧,供多轴同步下发。 */
|
||||
(void)sendBatch(out);
|
||||
}
|
||||
|
||||
@@ -214,20 +352,19 @@ void EthercatCanBridge::buildInput(uint8_t *in)
|
||||
if (in == nullptr) {
|
||||
return;
|
||||
}
|
||||
/* 必须按新的全长清零。旧代码写死 memset(in,0,32),扩容后会把 32 字节以后
|
||||
* 的槽区留成上一周期的残留数据,主站读到的是"看起来合法但已过期"的电机状态。 */
|
||||
memset(in, 0, kEcatPdoInBytes);
|
||||
/* 不整包 memset(600):8kHz 下每拍 Process 都会走到这里,600B 清零
|
||||
* 与后续覆盖写重复。各段自己写满有效域;未用槽由 fillMotorRound
|
||||
* 按 count 截断,主站只迭代 count,空洞区可为残值。 */
|
||||
|
||||
/* ── 段 1(0..31):桥状态 + 短快照,布局与 1.5.53 逐字节兼容 ── */
|
||||
/* ── 段 1(0..31):桥状态 + 短快照 ── */
|
||||
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);
|
||||
wr32le(&in[ECAT_CAN_RX_OFF_TXTXID], s_lastTxId);
|
||||
wr16le(&in[ECAT_CAN_RX_OFF_RX00FF], s_rx00ffCount);
|
||||
|
||||
/* seqlock 判稳(见 onCanFrame):奇数=生产者正在写这组快照。单核下 ISR
|
||||
* 执行期间生产者冻结,读一次即可定性;本周期跳过段 1/段 3(开头已清零,
|
||||
* 语义=无有效快照),下周期自愈。批量区(段 4)走环形队列,不受影响。 */
|
||||
const bool xrxStable = ((s_xrxSeq & 1U) == 0U);
|
||||
|
||||
if (xrxStable && s_rxValid) {
|
||||
@@ -239,65 +376,47 @@ void EthercatCanBridge::buildInput(uint8_t *in)
|
||||
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);
|
||||
s_rxPublished = true;
|
||||
} else {
|
||||
in[ECAT_CAN_RX_OFF_FLAGS] = 0U;
|
||||
in[ECAT_CAN_RX_OFF_CH] = 0U;
|
||||
in[ECAT_CAN_RX_OFF_DLC] = 0U;
|
||||
}
|
||||
|
||||
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;
|
||||
/* ── 段 2(32..543):电机整轮密排 ── */
|
||||
(void)fillMotorRound(in);
|
||||
|
||||
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 判断 */
|
||||
/* ── 段 3(544..583):长帧快照(通用,不专宠版本) ──
|
||||
* 语义 =「最近一帧非 STATUS 长帧」:CONFIG/版本/节点ID 谁后到谁上。
|
||||
* STATUS(0x0100) 由段2 密排承载,不进本槽(否则 48B STATUS 会淹没一切)。
|
||||
* 例外:仅当刚收到 0x00FF 的短粘滞窗口内,优先摆版本应答,
|
||||
* 给主站一个稳定采样窗;窗口结束后立刻交还给 CONFIG 等,避免其它长帧失效。 */
|
||||
if (xrxStable) {
|
||||
const uint32_t nowMs = SysTickTimer::getMs();
|
||||
const bool stickyLive = s_verValid && static_cast<int32_t>(nowMs - s_verHoldUntilMs) < 0;
|
||||
/* 段3 RSVD@547:processOutput 看到的输出 seq(诊断用,不参与解析) */
|
||||
in[ECAT_XRX_OFF_RSVD] = s_outSeenSeq;
|
||||
if (stickyLive) {
|
||||
in[ECAT_XRX_OFF_CH] = s_verCh;
|
||||
in[ECAT_XRX_OFF_DLC] = s_verDlc;
|
||||
in[ECAT_XRX_OFF_FLAGS] = 0x01U;
|
||||
wr32le(&in[ECAT_XRX_OFF_ID], s_verId);
|
||||
memcpy(&in[ECAT_XRX_OFF_DATA], s_verData, kEcatCanXRxDataMax);
|
||||
} else if (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);
|
||||
}
|
||||
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);
|
||||
/* 段 5 由 EthercatPdo::fillDiagTail 写。 */
|
||||
}
|
||||
|
||||
void EthercatCanBridge::onCanFrame(uint8_t channel, uint32_t id,
|
||||
@@ -307,34 +426,25 @@ void EthercatCanBridge::onCanFrame(uint8_t channel, uint32_t id,
|
||||
return;
|
||||
}
|
||||
|
||||
/* 上一帧还没被 PDO 周期取走就来了新帧:记一次丢弃(覆盖旧帧,
|
||||
* 保证主站看到的永远是最新状态,而不是卡在一帧陈旧数据上)。 */
|
||||
if (s_rxValid) {
|
||||
++s_dropCount;
|
||||
/* STATUS(0x0100) 由 MotorState 段2 密排承载,不写入 xrx。
|
||||
* 原注释本就如此约定;实现曾漏过滤,导致高频 STATUS 盖掉 CONFIG/版本。 */
|
||||
if (canSubId(id) == 0x0100U) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* 缓存区已扩到 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; /* → 奇数:快照进入"正在写" */
|
||||
/* ── 快照 seqlock:奇数=正在写,消费者本拍跳过段 1/段 3 ── */
|
||||
++s_xrxSeq;
|
||||
__DMB();
|
||||
|
||||
if (s_rxValid && !s_rxPublished) {
|
||||
++s_dropCount;
|
||||
}
|
||||
|
||||
memcpy(s_rxData, data, copyLen);
|
||||
if (copyLen < kEcatCanXRxDataMax) {
|
||||
memset(&s_rxData[copyLen], 0, (size_t)(kEcatCanXRxDataMax - copyLen));
|
||||
@@ -342,37 +452,33 @@ void EthercatCanBridge::onCanFrame(uint8_t channel, uint32_t id,
|
||||
|
||||
s_rxCh = channel;
|
||||
s_rxId = id;
|
||||
s_rxDlc = length; /* 报告原始长度,便于主站发现被截断 */
|
||||
s_rxDlc = length;
|
||||
s_rxTruncated = (length > kEcatCanRxDataMax);
|
||||
++s_rxCount;
|
||||
s_rxValid = true;
|
||||
s_rxPublished = false;
|
||||
|
||||
if (canSubId(id) == 0x00FFU) {
|
||||
if (s_rx00ffCount < 0xFFFFU) {
|
||||
++s_rx00ffCount;
|
||||
}
|
||||
}
|
||||
|
||||
/* 版本应答:只做短粘滞,供主站在窗口内采到;不长期霸占 xrx */
|
||||
if (canSubId(id) == 0x00FFU) {
|
||||
s_verCh = channel;
|
||||
s_verDlc = length;
|
||||
s_verId = id;
|
||||
memcpy(s_verData, data, copyLen);
|
||||
if (copyLen < kEcatCanXRxDataMax) {
|
||||
memset(&s_verData[copyLen], 0, (size_t)(kEcatCanXRxDataMax - copyLen));
|
||||
}
|
||||
s_verHoldUntilMs = SysTickTimer::getMs() + kVerHoldMs;
|
||||
s_verValid = 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:消费者读到时数据必已写完 */
|
||||
}
|
||||
++s_xrxSeq;
|
||||
}
|
||||
|
||||
extern "C" {
|
||||
@@ -387,4 +493,9 @@ void EcatCan_BuildInput(uint8_t *in)
|
||||
EthercatCanBridge::buildInput(in);
|
||||
}
|
||||
|
||||
void EcatCan_ResetSequenceState(void)
|
||||
{
|
||||
EthercatCanBridge::resetSequenceState();
|
||||
}
|
||||
|
||||
} /* extern "C" */
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
/**
|
||||
* @file ethercat_can_bridge.h
|
||||
* @brief EtherCAT PDO ↔ CAN 转发桥(1.5.55)。
|
||||
* @brief EtherCAT PDO ↔ CAN 转发桥。
|
||||
*
|
||||
* 输出 PDO(0x7000, 主站→从站, 64B) = 待发送的 CAN 帧(数据区 32B)
|
||||
* 输入 PDO(0x6000, 从站→主站, 384B) = 桥状态 + 电机状态窗口 + 长帧快照
|
||||
* 输出 PDO(0x7000, 主站→从站, 384B) = 待发送的 CAN 帧(数据区 32B)
|
||||
* 输入 PDO(0x6000, 从站→主站, 600B) = 桥状态 + 电机整轮状态 + 长帧快照 + 诊断
|
||||
*
|
||||
* 发送用**序号沿触发**:仅当 seq 字节变化时才真正发一帧。PDO 是周期性刷新的,
|
||||
* 若按「内容非空即发」会把同一帧每个周期重发一次;seq 变化沿是 PDO 里表达
|
||||
@@ -13,38 +13,17 @@
|
||||
* TaskRunner::taskCanRxParse(),两个消费者会互相抢帧,OTA relay / 电机状态
|
||||
* 会随机丢帧。改为由 taskCanRxParse() 的分发链回调 onCanFrame() 投递。
|
||||
*
|
||||
* ── 1.5.55 为电机控制台扩容(32B/32B → 64B/384B)──
|
||||
* 段 2 语义:一帧 = 总线电机整轮状态。
|
||||
*
|
||||
* 三个动机:
|
||||
* 1. TX 数据区 24B→32B。电机 CMD 是 DLC 32,旧 processOutput 里
|
||||
* `dlc > kEcatCanTxDataMax` 直接钳位到 24,后 8 字节被**静默丢弃**且不报错。
|
||||
* 2. 输入侧加电机状态窗口。单帧快照对电机控制台不够用:9 节点各 32B STATUS,
|
||||
* 一周期只带回一帧、新帧还覆盖旧帧,永远取不齐。改为直接从 MotorState
|
||||
* 状态表(已按 node×channel 聚合,见 device/motor_state.h)取一个窗口,
|
||||
* 主站移动 win_base 即可 4 个周期扫完全部 27 槽。
|
||||
* 3. 加长帧快照区。旧 RX 数据区只有 8B,版本帧(DLC24)/CONFIG 帧(DLC24) 会被截断。
|
||||
* · 每一拍输入 PDO 携带**总线上已识别电机的完整一轮状态**(最多 9 个);
|
||||
* · **空槽不发送**:密排,count = 本拍实际打包的电机数;
|
||||
* · 主站按 count 迭代,每槽自带 node_id,自动识别,无需 win_base 滑窗;
|
||||
* · 版本/CONFIG 走段 1/段 3「最近一帧」快照;
|
||||
* · 上一轮已打包且 seq 未变的电机,本轮跳过 MotorState 重读与 48B 拷贝,
|
||||
* 直接复用影子区(s_motShadow)。
|
||||
*
|
||||
* **输入 PDO 前 32 字节与 1.5.53 逐字节兼容**,偏移与语义全部不变。这样
|
||||
* tools/ecat 的 CAN 调试页(ecat.html)与 ecat_server.py 的 _can_decode_input
|
||||
* 不改一行也照常工作,扩容只是在后面追加新区段。
|
||||
*
|
||||
* ── 1.5.56 扩到 DPRAM 硬上限并加 CANFD 批量帧区(64/384 → 384/640)──
|
||||
*
|
||||
* 动机:需要一个周期里合并下发/回收**多条 CANFD 帧**。1.5.55 的单帧槽
|
||||
* (一个 seq 一帧)每周期最多一帧,4ms 周期就是 250 帧/s 上限,
|
||||
* 而一次多轴同步下发就要 9 帧,靠排队要 36ms 才发完 —— 各轴指令不同拍,
|
||||
* 失去"同步下发"的意义。
|
||||
*
|
||||
* 新增两个批量区,每条帧带 8B 头(ch/dlc/flags/rsvd + id)+ 数据:
|
||||
* 输出 48..383(336B):批量发送,一个 seq 沿一次性发 N 帧
|
||||
* 输入 360..639(280B):批量接收,一周期回收多帧而不是只带最近一帧
|
||||
*
|
||||
* 三条兼容性铁律(改动只做追加,不动既有偏移):
|
||||
* 1. 输出 0..47 与 1.5.55 单帧布局完全一致(seq/ch/dlc/flags/id/win/data32);
|
||||
* 2. 输入 0..359 与 1.5.55 完全一致(段1 桥状态 + 段2 电机窗口 + 段3 长帧);
|
||||
* 3. 批量区**为空时**(count=0)行为与 1.5.55 完全相同,老主站照旧可用。
|
||||
*
|
||||
* 上限推导(为何是 384+640 而不是更大)见 comm/ethercat_pdo_size.h。
|
||||
* 输入 PDO 总长 600B;主站解码必须与段 2/3 偏移一致
|
||||
* (tools/ecat 的 decode_input / transport-ecat.js 等)。
|
||||
*/
|
||||
#ifndef COMM_ETHERCAT_CAN_BRIDGE_H
|
||||
#define COMM_ETHERCAT_CAN_BRIDGE_H
|
||||
@@ -52,20 +31,21 @@
|
||||
#include <cstdint>
|
||||
|
||||
#include "comm/ethercat_pdo_size.h"
|
||||
#include "device/motor_state.h" /* kMotorMaxNodes / kMotorChannels */
|
||||
|
||||
/** 输出 PDO(主站→从站, 384B)段 1(0..47):单帧发送。**与 1.5.55 完全兼容**。 */
|
||||
/** 输出 PDO(主站→从站, 384B)段 1(0..47):单帧发送。 */
|
||||
enum : uint8_t {
|
||||
ECAT_CAN_TX_OFF_SEQ = 0U, /**< 发送序号:变化沿触发一次发送(单帧+批量一起) */
|
||||
ECAT_CAN_TX_OFF_CH = 1U, /**< 目标通道 0/1/2 */
|
||||
ECAT_CAN_TX_OFF_DLC = 2U, /**< 数据长度 0..32;0 表示本次不发单帧(只发批量) */
|
||||
ECAT_CAN_TX_OFF_FLAGS = 3U, /**< bit0=1 请求清零统计;其余保留 */
|
||||
ECAT_CAN_TX_OFF_ID = 4U, /**< CAN ID,小端 uint32(29 位扩展帧) */
|
||||
ECAT_CAN_TX_OFF_WIN = 8U, /**< 请求回传的电机槽窗口起始下标 0..26 */
|
||||
/* 9..15 保留:让数据区落在 16 字节边界,hexdump 排查时对齐好读 */
|
||||
ECAT_CAN_TX_OFF_WIN = 8U, /**< 当前忽略(电机整轮密排,无滑窗);保留占位 */
|
||||
/* 9..15 保留:让数据区落在 16 字节边界 */
|
||||
ECAT_CAN_TX_OFF_DATA = 16U, /**< 数据区起点,32 字节(一帧电机 CMD) */
|
||||
};
|
||||
|
||||
/** 输出 PDO 段 2(48..383):CANFD 批量发送区(1.5.56 新增)。 */
|
||||
/** 输出 PDO 段 2(48..383):CANFD 批量发送区。 */
|
||||
enum : uint16_t {
|
||||
ECAT_TXB_OFF_COUNT = 48U, /**< 本次要发的帧数 0..kEcatTxBatchMax;0=不发批量 */
|
||||
ECAT_TXB_OFF_FLAGS = 49U, /**< bit0=1 遇错即停(默认继续发完并各自记结果) */
|
||||
@@ -83,7 +63,7 @@ enum : uint8_t {
|
||||
ECAT_FRM_HDR_SIZE = 8U, /**< 头长度,数据紧跟其后 */
|
||||
};
|
||||
|
||||
/** 输入 PDO 段 1(0..31):桥状态 + 最近一帧短快照。**与 1.5.53 完全兼容**。 */
|
||||
/** 输入 PDO 段 1(0..31):桥状态 + 最近一帧短快照 + 收发诊断。 */
|
||||
enum : uint8_t {
|
||||
ECAT_CAN_RX_OFF_ACK = 0U, /**< 回显已执行的发送序号,主站据此确认送达 */
|
||||
ECAT_CAN_RX_OFF_CH = 1U, /**< 该帧来自哪个通道 */
|
||||
@@ -95,65 +75,86 @@ enum : uint8_t {
|
||||
ECAT_CAN_RX_OFF_TXRES = 20U, /**< 最近一次发送结果(CanTxResult 原值,0=OK) */
|
||||
ECAT_CAN_RX_OFF_BACKLOG = 21U, /**< 该通道软件 TX 队列积压 */
|
||||
ECAT_CAN_RX_OFF_TXCNT = 22U, /**< 累计发送受理帧数,小端 uint16 */
|
||||
ECAT_CAN_RX_OFF_DROP = 24U, /**< 桥接层丢帧数(投递时上一帧还没被取走),小端 uint16 */
|
||||
ECAT_CAN_RX_OFF_DROP = 24U, /**< 段 1/段 3 快照「未取先覆」次数(u16 累计回绕) */
|
||||
ECAT_CAN_RX_OFF_TXTXID = 26U, /**< 最近 enqueueTx 成功的 CAN ID,小端 uint32 */
|
||||
ECAT_CAN_RX_OFF_RX00FF = 30U, /**< CAN RX 收到 0x00FF 次数,小端 uint16 */
|
||||
};
|
||||
|
||||
/** 输入 PDO 段 2(32..319):电机状态窗口头 + 槽数组。 */
|
||||
/**
|
||||
* 输入 PDO 段 2(32..543):电机整轮状态(密排)。
|
||||
*
|
||||
* 主站解析:
|
||||
* count = in[32] // 0..9,本拍实际打包的电机数
|
||||
* for (i = 0; i < count; i++)
|
||||
* slot = in[40 + i*56] // node_id/online/dlc/seq/status
|
||||
*
|
||||
* 空节点不占位;槽序按 node_id 升序;同号多通道取 seq 最新的一条。
|
||||
*/
|
||||
enum : uint16_t {
|
||||
ECAT_MOTW_OFF_ACTIVE = 32U, /**< 在线电机数 */
|
||||
ECAT_MOTW_OFF_BOOT = 33U, /**< 1=近 3s 收到过 0x103 BOOT 心跳 */
|
||||
ECAT_MOTW_OFF_WIN_BASE = 34U, /**< 本帧携带的槽窗口起始下标(回显主站请求) */
|
||||
ECAT_MOTW_OFF_WIN_COUNT = 35U, /**< 本帧携带的有效槽数(≤7) */
|
||||
ECAT_MOTW_OFF_GLOBAL_SEQ = 36U,/**< 状态表全局刷新序号,小端 uint32 */
|
||||
ECAT_MOTW_OFF_SLOTS = 40U, /**< 槽数组起点,7×40B */
|
||||
ECAT_MOTW_OFF_COUNT = 32U, /**< 本拍密排电机数 0..9(主站按此迭代) */
|
||||
ECAT_MOTW_OFF_ACTIVE = 33U, /**< 在线电机数(与 count 可不同:离线但仍报最后一轮) */
|
||||
ECAT_MOTW_OFF_BOOT = 34U, /**< 1=近 3s 收到过 0x103 BOOT 心跳 */
|
||||
ECAT_MOTW_OFF_MAGIC = 35U, /**< 恒 kEcatMotLayoutMagic(0xA5):密排布局标识 */
|
||||
ECAT_MOTW_OFF_GLOBAL_SEQ = 36U, /**< 状态表全局刷新序号,小端 uint32 */
|
||||
ECAT_MOTW_OFF_SLOTS = 40U, /**< 密排槽数组起点,count×56B */
|
||||
};
|
||||
|
||||
/** 电机状态槽内部布局(40 字节)。 */
|
||||
/** 电机状态槽内部布局(56 字节)。 */
|
||||
enum : uint8_t {
|
||||
ECAT_MOT_OFF_NODE = 0U, /**< 节点号 1..9,0=空槽 */
|
||||
ECAT_MOT_OFF_NODE = 0U, /**< 节点号 1..9(密排下不会出现 0) */
|
||||
ECAT_MOT_OFF_CH = 1U, /**< CAN 通道 0/1/2 */
|
||||
ECAT_MOT_OFF_ONLINE = 2U, /**< 1=在线 */
|
||||
ECAT_MOT_OFF_DLC = 3U, /**< 最后一帧 STATUS 的 DLC(正常 32) */
|
||||
ECAT_MOT_OFF_DLC = 3U, /**< 最后一帧 STATUS 的 DLC(正常 48) */
|
||||
ECAT_MOT_OFF_SEQ = 4U, /**< STATUS 刷新序号,小端 uint32 */
|
||||
ECAT_MOT_OFF_STATUS = 8U, /**< 原始 STATUS payload,32 字节 */
|
||||
ECAT_MOT_OFF_STATUS = 8U, /**< 原始 STATUS payload,48 字节 */
|
||||
};
|
||||
|
||||
/** 输入 PDO 段 3(320..359):最近一帧**完整**快照(版本/CONFIG 帧要 24B)。 */
|
||||
/** 输入 PDO 段 3(544..583):最近一帧完整快照(版本/CONFIG 帧要 24B+)。 */
|
||||
enum : uint16_t {
|
||||
ECAT_XRX_OFF_CH = 320U, /**< 通道 */
|
||||
ECAT_XRX_OFF_DLC = 321U, /**< 原始 DLC(可达 32) */
|
||||
ECAT_XRX_OFF_FLAGS = 322U, /**< bit0=有效 */
|
||||
ECAT_XRX_OFF_RSVD = 323U,
|
||||
ECAT_XRX_OFF_ID = 324U, /**< CAN ID,小端 uint32 */
|
||||
ECAT_XRX_OFF_DATA = 328U, /**< 数据区,32 字节完整 payload */
|
||||
ECAT_XRX_OFF_CH = 544U, /**< 通道 */
|
||||
ECAT_XRX_OFF_DLC = 545U, /**< 原始 DLC(可达 32) */
|
||||
ECAT_XRX_OFF_FLAGS = 546U, /**< bit0=有效 */
|
||||
ECAT_XRX_OFF_RSVD = 547U,
|
||||
ECAT_XRX_OFF_ID = 548U, /**< CAN ID,小端 uint32 */
|
||||
ECAT_XRX_OFF_DATA = 552U, /**< 数据区,32 字节完整 payload */
|
||||
};
|
||||
|
||||
/** 输入 PDO 段 4(360..639):CANFD 批量接收区(1.5.56 新增)。 */
|
||||
/** 输入 PDO 段 5(584..595):设备诊断区(位置未变)。 */
|
||||
enum : uint16_t {
|
||||
ECAT_RXB_OFF_COUNT = 360U, /**< 本周期回收的帧数 0..kEcatRxBatchMax */
|
||||
ECAT_RXB_OFF_LOST = 361U, /**< 本周期因批量区放不下而丢弃的帧数 */
|
||||
ECAT_RXB_OFF_PENDING = 362U, /**< 环形队列中仍待回收的帧数(主站据此提速) */
|
||||
ECAT_RXB_OFF_RSVD = 363U,
|
||||
ECAT_RXB_OFF_ITEMS = 364U, /**< 帧数组起点,项布局同 ECAT_FRM_OFF_* */
|
||||
ECAT_DIAG_OFF_LOAD = 584U, /**< 负载率,小端 uint16,单位 0.1%(1000=100%) */
|
||||
ECAT_DIAG_OFF_TORN = 586U, /**< CAN 取帧撕裂丢帧数(低 16 位,饱和) */
|
||||
ECAT_DIAG_OFF_SEQ = 588U, /**< 诊断区序号,小端 uint16,每完成 1s 窗口 +1 */
|
||||
ECAT_DIAG_OFF_OVERFULL = 590U, /**< CAN RX FIFO 满溢计数 RFO0(u16 饱和) */
|
||||
ECAT_DIAG_OFF_RELTIMEOUT = 592U, /**< RFD0 释放握手超时计数(u16 饱和) */
|
||||
ECAT_DIAG_OFF_REJECT = 594U, /**< CAN 内容级拼接帧命中计数(u16 饱和) */
|
||||
};
|
||||
|
||||
/** 单帧数据在输出 PDO / 长帧快照区中的最大字节数(电机 CMD 是 DLC 32)。 */
|
||||
constexpr uint8_t kEcatCanTxDataMax = 32U;
|
||||
/** 段 1 短快照数据区字节数(保持 1.5.53 兼容,勿改)。 */
|
||||
/** 段 1 短快照数据区字节数。 */
|
||||
constexpr uint8_t kEcatCanRxDataMax = 8U;
|
||||
/** 段 3 长快照数据区字节数。 */
|
||||
constexpr uint8_t kEcatCanXRxDataMax = 32U;
|
||||
|
||||
/** 单个电机状态槽字节数。 */
|
||||
constexpr uint8_t kEcatMotSlotSize = 40U;
|
||||
/** 一份输入 PDO 携带的电机状态槽数。 */
|
||||
constexpr uint8_t kEcatMotSlotsPerPdo = 7U;
|
||||
/** 单个电机状态槽字节数:8B 头 + 48B STATUS。 */
|
||||
constexpr uint8_t kEcatMotSlotSize = 56U;
|
||||
/** 一拍最多打包的电机数 = 节点号 1..9 各一槽(同号多通道取最新)。 */
|
||||
constexpr uint8_t kEcatMotSlotsPerPdo = kMotorMaxNodes; /* 9 */
|
||||
/** 段 2 头长度(COUNT..GLOBAL_SEQ,8B)。 */
|
||||
constexpr uint8_t kEcatMotHdrSize = 8U;
|
||||
/** 段 2 密排区最大字节:头 8B + 9×56B = 512B,落在 32..543。 */
|
||||
constexpr uint16_t kEcatMotRegionBytes =
|
||||
(uint16_t)(kEcatMotHdrSize
|
||||
+ (uint16_t)kEcatMotSlotsPerPdo * kEcatMotSlotSize);
|
||||
/** 布局魔数,写在 offset 35。主站据此区分密排布局与旧滑窗布局(两者
|
||||
* Ibytes 同为 600,不能只靠长度分支)。0=非密排布局。 */
|
||||
constexpr uint8_t kEcatMotLayoutMagic = 0xA5U;
|
||||
|
||||
/** 过程数据长度,唯一真源见 comm/ethercat_pdo_size.h(C/C++ 共用)。 */
|
||||
constexpr uint16_t kEcatPdoOutBytes = ECAT_PDO_OUT_BYTES; /* 48×64bit = 384B */
|
||||
constexpr uint16_t kEcatPdoInBytes = ECAT_PDO_IN_BYTES; /* 80×64bit = 640B */
|
||||
/** 过程数据长度,唯一真源见 comm/ethercat_pdo_size.h。 */
|
||||
constexpr uint16_t kEcatPdoOutBytes = ECAT_PDO_OUT_BYTES; /* 384B */
|
||||
constexpr uint16_t kEcatPdoInBytes = ECAT_PDO_IN_BYTES; /* 600B */
|
||||
|
||||
/** CANFD 单帧数据上限(DLC 64)—— 批量区按此上限做容量推导。 */
|
||||
/** CANFD 单帧数据上限。 */
|
||||
constexpr uint8_t kEcatCanFdDataMax = 64U;
|
||||
|
||||
/** 批量发送区可用字节数与最坏情况(全 DLC64)帧数上限。 */
|
||||
@@ -161,57 +162,52 @@ constexpr uint16_t kEcatTxBatchBytes = (uint16_t)(kEcatPdoOutBytes - ECAT_TXB_OF
|
||||
constexpr uint8_t kEcatTxBatchMax =
|
||||
(uint8_t)(kEcatTxBatchBytes / (ECAT_FRM_HDR_SIZE + kEcatCanFdDataMax));
|
||||
|
||||
/** 批量接收区可用字节数与最坏情况帧数上限。 */
|
||||
constexpr uint16_t kEcatRxBatchBytes = (uint16_t)(kEcatPdoInBytes - ECAT_RXB_OFF_ITEMS);
|
||||
constexpr uint8_t kEcatRxBatchMax =
|
||||
(uint8_t)(kEcatRxBatchBytes / (ECAT_FRM_HDR_SIZE + kEcatCanFdDataMax));
|
||||
|
||||
/** 批量接收环形队列槽数。取 kEcatRxBatchMax 的 4 倍,让主站慢一两拍也不丢帧。
|
||||
* 注意这是 RAM 成本(槽数 × 72B),不是 PDO 成本。 */
|
||||
constexpr uint8_t kEcatRxRingSlots = (uint8_t)(kEcatRxBatchMax * 4U);
|
||||
|
||||
/* 布局守卫:段 2 的槽数组必须完整落在输入 PDO 内,段 3 也不能越界。
|
||||
* 改槽数/槽大小时忘了同步 PDO 长度,会在这里编译期失败。 */
|
||||
static_assert(ECAT_MOTW_OFF_SLOTS + kEcatMotSlotsPerPdo * kEcatMotSlotSize
|
||||
<= ECAT_XRX_OFF_CH, "电机槽窗口与长帧快照区重叠");
|
||||
static_assert(ECAT_XRX_OFF_DATA + kEcatCanXRxDataMax <= ECAT_RXB_OFF_COUNT,
|
||||
"长帧快照区与批量接收区重叠");
|
||||
/* ── 布局守卫 ── */
|
||||
static_assert(ECAT_MOTW_OFF_SLOTS
|
||||
+ (uint16_t)kEcatMotSlotsPerPdo * kEcatMotSlotSize
|
||||
== ECAT_XRX_OFF_CH,
|
||||
"段2密排区必须与段3首尾相接");
|
||||
static_assert(ECAT_XRX_OFF_DATA + kEcatCanXRxDataMax == ECAT_DIAG_OFF_LOAD,
|
||||
"段3必须与段5首尾相接");
|
||||
static_assert(ECAT_CAN_TX_OFF_DATA + kEcatCanTxDataMax <= ECAT_TXB_OFF_COUNT,
|
||||
"单帧发送数据区与批量发送区重叠");
|
||||
static_assert(ECAT_RXB_OFF_ITEMS < kEcatPdoInBytes, "批量接收区起点越界");
|
||||
static_assert(ECAT_TXB_OFF_ITEMS < kEcatPdoOutBytes, "批量发送区起点越界");
|
||||
/* 批量区至少要装得下一帧满长 CANFD,否则"批量"没有意义 */
|
||||
static_assert(kEcatTxBatchMax >= 1U, "批量发送区装不下一帧 DLC64");
|
||||
static_assert(kEcatRxBatchMax >= 1U, "批量接收区装不下一帧 DLC64");
|
||||
static_assert(ECAT_DIAG_OFF_REJECT + 2U <= kEcatPdoInBytes, "段5诊断区越界");
|
||||
static_assert(ECAT_MOTW_OFF_COUNT + kEcatMotRegionBytes <= ECAT_XRX_OFF_CH,
|
||||
"段2密排区越界");
|
||||
|
||||
/**
|
||||
* @brief EtherCAT ↔ CAN 转发桥。
|
||||
* @note 全静态;PDO 侧函数在 Sync0_Isr(preempt=0) 或主循环 MainLoop 上下文执行,
|
||||
* onCanFrame() 在主循环 taskCanRxParse() 上下文执行。共享状态用 volatile +
|
||||
* 单生产者/单消费者语义,不需要临界区(投递只写、消费只读+清标志)。
|
||||
* 例外:段 1/段 3 的"最近一帧快照"是多字段一组(id/dlc/data 必须配套),
|
||||
* 用 s_xrxSeq seqlock 保证消费者不会读到"新数据+旧 ID"的混搭——
|
||||
* 单生产者/单消费者语义。多字段一组用 seqlock:
|
||||
* - 段 1/段 3 的「最近一帧快照」→ s_xrxSeq;
|
||||
* - 电机槽 48B payload → MotorSlot.wr_seq。
|
||||
* Sync0 是 preempt=0,critical.h 的 BASEPRI 方案挡不住它,故只能无锁协议。
|
||||
*/
|
||||
class EthercatCanBridge {
|
||||
public:
|
||||
/**
|
||||
* @brief 处理一份输出 PDO:seq 变化时向 CAN 发一帧。
|
||||
* @param[in] out kEcatPdoOutBytes 字节输出过程数据。
|
||||
* @note 由 APPL_Application() 每周期调用。
|
||||
* @brief 输出探针:seq 变化时请求全量读。
|
||||
* @note 不再刷新 win_base(电机整轮密排,无滑窗);仍处理 seq 沿
|
||||
* 与 FLAGS bit0 清统计。
|
||||
*/
|
||||
static bool peekOutput(const uint8_t *head);
|
||||
|
||||
/** @brief 复位输出 sequence 与冷启动同步状态。 */
|
||||
static void resetSequenceState();
|
||||
|
||||
static void processOutput(const uint8_t *out);
|
||||
|
||||
/**
|
||||
* @brief 填充输入 PDO:桥状态 + 电机状态窗口 + 长帧快照。
|
||||
* @param[out] in kEcatPdoInBytes 字节输入过程数据(本函数负责全部写满)。
|
||||
* @brief 填充输入 PDO:桥状态 + 电机整轮密排 + 长帧快照。
|
||||
* @param[out] in kEcatPdoInBytes 字节输入过程数据。
|
||||
*/
|
||||
static void buildInput(uint8_t *in);
|
||||
|
||||
/**
|
||||
* @brief CAN 收帧投递入口。
|
||||
* @note 由 TaskRunner::taskCanRxParse() 在分发链里调用,不消费帧(不返回
|
||||
* "已处理"),只做旁路复制,因此不影响 OTA relay / 电机状态。
|
||||
* @brief CAN 收帧投递入口(旁路复制,不消费帧)。
|
||||
*/
|
||||
static void onCanFrame(uint8_t channel, uint32_t id,
|
||||
const uint8_t *data, uint8_t length);
|
||||
@@ -221,29 +217,31 @@ public:
|
||||
|
||||
private:
|
||||
/**
|
||||
* @brief 处理输出 PDO 的批量发送区(seq 沿内调用一次)。
|
||||
* @return 实际受理的帧数。
|
||||
* @note 逐帧解析 (8B 头 + dlc),边界不足即停 —— 不信任主站给的 count,
|
||||
* 越界的 count 只会让循环提前结束,不会读到 PDO 之外。
|
||||
* @brief 填充段 2 电机整轮密排区(含差分跳过)。
|
||||
* @return 本拍打包的电机数(写入 count 字段)。
|
||||
*/
|
||||
static uint8_t fillMotorRound(uint8_t *in);
|
||||
|
||||
static uint8_t sendBatch(const uint8_t *out);
|
||||
|
||||
/** @brief 把接收环形队列里的帧尽量装进输入 PDO 批量区。 */
|
||||
static void fillRxBatch(uint8_t *in);
|
||||
|
||||
static volatile uint8_t s_lastSeq; /**< 上一次已执行的发送序号 */
|
||||
static volatile bool s_seqPrimed; /**< 是否已记录过基准 seq(防上电首帧误发) */
|
||||
static volatile uint8_t s_lastSeq;
|
||||
static volatile bool s_seqPrimed;
|
||||
static volatile bool s_coldStartPending;
|
||||
static volatile uint8_t s_lastTxResult;
|
||||
static volatile uint16_t s_txCount;
|
||||
static volatile uint16_t s_dropCount;
|
||||
/** 主站请求的电机槽窗口起始下标(输出 PDO 的 WIN 字节,每周期刷新)。 */
|
||||
static volatile uint8_t s_winBase;
|
||||
/** 最近一次 enqueueTx 成功的 CAN ID(诊断:ECAT 是否真的发出去) */
|
||||
static volatile uint32_t s_lastTxId;
|
||||
/** CAN RX 收到 0x00FF(版本应答)次数(诊断:电机是否回帧) */
|
||||
static volatile uint16_t s_rx00ffCount;
|
||||
/** processOutput 本拍读到的输出 PDO seq(诊断:ESC 输出是否到达应用层) */
|
||||
static volatile uint8_t s_outSeenSeq;
|
||||
|
||||
/* 最近一帧接收快照:taskCanRxParse 写、PDO 读。
|
||||
* s_xrxSeq 为 seqlock:奇数=正在写,buildInput 本周期跳过段 1/段 3。
|
||||
* 没有它,Sync0_Isr 抢占在"数据已写完、id 未更新"之间会把新帧数据
|
||||
* 配旧帧 ID 发出去(STATUS 被标成 CONFIG → 前端解出离谱目标值)。 */
|
||||
/* 最近一帧接收快照:taskCanRxParse 写、PDO 读。s_xrxSeq 为 seqlock。
|
||||
* STATUS(0x0100) 禁止写入本快照 —— 它由 MotorState 段2 密排承载;
|
||||
* 否则高频 STATUS 会持续冲掉版本应答(0x00FF)等低频长帧。 */
|
||||
static volatile bool s_rxValid;
|
||||
static volatile bool s_rxPublished;
|
||||
static volatile uint32_t s_xrxSeq;
|
||||
static volatile uint8_t s_rxCh;
|
||||
static volatile uint8_t s_rxDlc;
|
||||
@@ -252,20 +250,25 @@ private:
|
||||
static volatile uint32_t s_rxCount;
|
||||
static uint8_t s_rxData[kEcatCanXRxDataMax];
|
||||
|
||||
/* ── 批量接收环形队列(1.5.56)──
|
||||
* 单生产者(taskCanRxParse 的 onCanFrame) / 单消费者(PDO 的 fillRxBatch)。
|
||||
* head/tail 各由一侧独占写入,故无需临界区:生产者只推进 head、
|
||||
* 消费者只推进 tail,最坏情况是某周期少读一帧,下周期即取到。 */
|
||||
struct RxRingItem {
|
||||
uint32_t id;
|
||||
uint8_t ch;
|
||||
uint8_t dlc;
|
||||
uint8_t data[kEcatCanFdDataMax];
|
||||
};
|
||||
static RxRingItem s_rxRing[kEcatRxRingSlots];
|
||||
static volatile uint8_t s_rxRingHead; /**< 生产者写入位置 */
|
||||
static volatile uint8_t s_rxRingTail; /**< 消费者读取位置 */
|
||||
static volatile uint8_t s_rxRingLost; /**< 队列满导致的丢帧数(累计,回填后清零) */
|
||||
/* 版本应答粘滞快照:0x00FF 到达后在 PDO 段3 保持发布一段时间,
|
||||
* 避免被随后的 CONFIG 等帧在主站采样前覆盖。 */
|
||||
static volatile bool s_verValid;
|
||||
static volatile uint32_t s_verHoldUntilMs;
|
||||
static uint8_t s_verCh;
|
||||
static uint8_t s_verDlc;
|
||||
static uint32_t s_verId;
|
||||
static uint8_t s_verData[kEcatCanXRxDataMax];
|
||||
|
||||
/* ── 段 2 差分填充影子 ──
|
||||
* 上一拍已发布的密排区副本 + 各节点上次发布的 seq/online + 头字段。
|
||||
* 节点集合、seq、online、active/boot 全未变时整段复用,跳过 MotorState
|
||||
* 重读与逐槽 48B 拷贝。掩码必须 u16:节点号 1..9,bit9 超出 uint8_t。 */
|
||||
static uint8_t s_motShadow[kEcatMotRegionBytes];
|
||||
static uint32_t s_motPubSeq[kMotorMaxNodes + 1U]; /* 下标=节点号 */
|
||||
static uint8_t s_motPubOnline[kMotorMaxNodes + 1U]; /* checkOffline 不 bump seq,须单独比 */
|
||||
static uint16_t s_motPubMask; /**< bit n=1:节点 n 在影子区中 */
|
||||
static uint8_t s_motPubCount;
|
||||
static bool s_motShadowValid;
|
||||
};
|
||||
|
||||
extern "C" {
|
||||
@@ -273,6 +276,8 @@ extern "C" {
|
||||
void EcatCan_ProcessOutput(const uint8_t *out);
|
||||
/** @brief 供 dc_app.c 调用的 shim:填充输入 PDO。 */
|
||||
void EcatCan_BuildInput(uint8_t *in);
|
||||
/** @brief 供 dc_app.c 调用的 shim:复位输出 sequence 状态机。 */
|
||||
void EcatCan_ResetSequenceState(void);
|
||||
}
|
||||
|
||||
#endif /* COMM_ETHERCAT_CAN_BRIDGE_H */
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
/**
|
||||
* @file ethercat_diff_stats.cpp
|
||||
* @brief [DIFF-STATS] 输入镜像差分统计实现(度量定义与存在理由见 ethercat_diff_stats.h)。
|
||||
*
|
||||
* 整文件由 CONFIG_ECAT_DIFF_STATS 门控:置 0 时不参与编译,挂接点各自也有 #if,
|
||||
* 因此关掉之后固件与改造前逐字节等价。
|
||||
*/
|
||||
#include "comm/ethercat_diff_stats.h"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
#include "RTE_Components.h"
|
||||
#include CMSIS_device_header
|
||||
|
||||
#include "comm/ethercat_pdo_size.h"
|
||||
#include "sys/config.h"
|
||||
|
||||
#if CONFIG_ECAT_DIFF_STATS
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr uint16_t kInBytes = (uint16_t)ECAT_PDO_IN_BYTES;
|
||||
constexpr uint32_t kInWords = (uint32_t)kInBytes / 4U; /* 600/4=150,无尾巴字节 */
|
||||
constexpr uint8_t kSegCount = 5U;
|
||||
|
||||
/**
|
||||
* @brief 各段字节上界(开区间),用于把变化量归到布局段落。
|
||||
* @note 1.6.21 布局:段1(0..31) 段2(32..543) 段3(544..583) 段4 已删 段5(584..599)。
|
||||
* 段4 上界=段5 起点,区间为空、恒 0,子索引保留以免动对象字典。
|
||||
*/
|
||||
constexpr uint16_t kSegEnd[kSegCount] = {32U, 544U, 584U, 584U, kInBytes};
|
||||
|
||||
/** @brief 上一份已发布的镜像。alignas(4) 是让优化器能把 4 字节 memcpy 折成单条
|
||||
* LDR 的前提——丢了它,比对会从 ~150 次字访问退化成 600 次字节访问。 */
|
||||
alignas(4) uint8_t s_prev[kInBytes];
|
||||
bool s_havePrev = false;
|
||||
|
||||
/* ── 正在累积的窗口 ── */
|
||||
uint32_t s_curCycles = 0U;
|
||||
uint32_t s_curChangedMax = 0U;
|
||||
uint32_t s_curRunsTotal = 0U;
|
||||
uint32_t s_curRunsMax = 0U;
|
||||
uint32_t s_curCompareCyc = 0U;
|
||||
uint32_t s_curSeg[kSegCount] = {0U, 0U, 0U, 0U, 0U};
|
||||
uint32_t s_winStart = 0U;
|
||||
|
||||
/* ── 上一个已完成的窗口(getter 只读这份,保证主站拿到的是稳定值)── */
|
||||
uint32_t s_winCycles = 0U;
|
||||
uint32_t s_winChangedMax = 0U;
|
||||
uint32_t s_winRunsTotal = 0U;
|
||||
uint32_t s_winRunsMax = 0U;
|
||||
uint32_t s_winCompareCyc = 0U;
|
||||
uint32_t s_winSeg[kSegCount] = {0U, 0U, 0U, 0U, 0U};
|
||||
uint32_t s_windowSeq = 0U;
|
||||
|
||||
/** @brief 字节偏移所属段号。只对"确实变了"的字调用,线性 5 次比较足够。 */
|
||||
uint8_t segOf(uint32_t byteOff)
|
||||
{
|
||||
for (uint8_t i = 0U; i < kSegCount; ++i) {
|
||||
if (byteOff < (uint32_t)kSegEnd[i]) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return (uint8_t)(kSegCount - 1U); /* 理论上到不了:kSegEnd[4] == kInBytes */
|
||||
}
|
||||
|
||||
/** @brief 结算窗口:把累加值搬成"上一个完整窗口",再清零重新计时。 */
|
||||
void rollWindow(uint32_t now)
|
||||
{
|
||||
s_winCycles = s_curCycles;
|
||||
s_winChangedMax = s_curChangedMax;
|
||||
s_winRunsTotal = s_curRunsTotal;
|
||||
s_winRunsMax = s_curRunsMax;
|
||||
s_winCompareCyc = s_curCompareCyc;
|
||||
for (uint8_t i = 0U; i < kSegCount; ++i) {
|
||||
s_winSeg[i] = s_curSeg[i];
|
||||
s_curSeg[i] = 0U;
|
||||
}
|
||||
s_curCycles = 0U;
|
||||
s_curChangedMax = 0U;
|
||||
s_curRunsTotal = 0U;
|
||||
s_curRunsMax = 0U;
|
||||
s_curCompareCyc = 0U;
|
||||
s_winStart = now;
|
||||
s_windowSeq++;
|
||||
}
|
||||
|
||||
} /* namespace */
|
||||
|
||||
/**
|
||||
* @brief 与上一份已发布镜像比对并累计统计。
|
||||
* @note .ramfunc:每拍在 PDI_Isr 里跑。放 flash 会带来等待态,既污染本仪表
|
||||
* 自己要测的 compareCyc,也给被测的写侧窗口添无谓的额外占用。
|
||||
*/
|
||||
__attribute__((section(".ramfunc"), noinline))
|
||||
void EthercatDiffStats::observe(const uint8_t *cur) noexcept
|
||||
{
|
||||
const uint32_t t0 = DWT->CYCCNT;
|
||||
|
||||
if (!s_havePrev) {
|
||||
/* 首拍只建基准:否则第一拍会把"整 600B 都变了"记成样本,把 changedMax
|
||||
* 和段计数全钉在无意义的上限上。 */
|
||||
std::memcpy(s_prev, __builtin_assume_aligned(cur, 4), kInBytes);
|
||||
s_havePrev = true;
|
||||
s_winStart = DWT->CYCCNT;
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t changed = 0U;
|
||||
uint32_t runs = 0U;
|
||||
bool inRun = false;
|
||||
|
||||
/* 对齐必须在调用点再声明一次:alignas(4) 只到形参为止,函数签名是
|
||||
* const uint8_t*,优化器看不见对齐就无从折字访问。实测两版都因此慢到不能用——
|
||||
* 移位版反汇编出 8 条 ldrb.w/字(41µs/拍),memcpy 版 21µs/拍 —— 而这个数会被
|
||||
* 直接读成"差分判断很贵",是仪表自己造的假象,不是差分的成本。 */
|
||||
const uint8_t *prevW = static_cast<const uint8_t *>(__builtin_assume_aligned(s_prev, 4));
|
||||
const uint8_t *curW = static_cast<const uint8_t *>(__builtin_assume_aligned(cur, 4));
|
||||
|
||||
for (uint32_t w = 0U; w < kInWords; ++w) {
|
||||
const uint32_t o = w * 4U;
|
||||
uint32_t a;
|
||||
uint32_t b;
|
||||
/* assume_aligned + memcpy:不碰严格别名,又能让 -Oz 折成单条 LDR。 */
|
||||
std::memcpy(&a, prevW + o, 4U);
|
||||
std::memcpy(&b, curW + o, 4U);
|
||||
if (a == b) {
|
||||
inRun = false;
|
||||
continue;
|
||||
}
|
||||
changed += 4U;
|
||||
if (!inRun) {
|
||||
runs++;
|
||||
inRun = true;
|
||||
}
|
||||
s_curSeg[segOf(o)] += 4U;
|
||||
}
|
||||
|
||||
s_curCycles++;
|
||||
s_curRunsTotal += runs;
|
||||
if (changed > s_curChangedMax) { s_curChangedMax = changed; }
|
||||
if (runs > s_curRunsMax) { s_curRunsMax = runs; }
|
||||
|
||||
std::memcpy(s_prev, curW, kInBytes);
|
||||
|
||||
const uint32_t t1 = DWT->CYCCNT;
|
||||
/* 比对自身开销:不含上面那次 600B 快照搬运(那是差分写真要做时要付的成本之一,
|
||||
* 单列出来才看得清"光判断"值多少钱)。 */
|
||||
s_curCompareCyc += (uint32_t)(t1 - t0);
|
||||
|
||||
/* SystemCoreClock 个计数 = 1s(与 sys/cpu_load.cpp 同法;DWT 32 位约 23.8s
|
||||
* 绕回,窗口远短于此,无绕回问题)。 */
|
||||
if ((uint32_t)(t1 - s_winStart) >= SystemCoreClock) {
|
||||
rollWindow(t1);
|
||||
}
|
||||
}
|
||||
|
||||
void EthercatDiffStats::reset() noexcept
|
||||
{
|
||||
s_havePrev = false;
|
||||
}
|
||||
|
||||
uint32_t EthercatDiffStats::windowSeq() noexcept { return s_windowSeq; }
|
||||
uint32_t EthercatDiffStats::cycles() noexcept { return s_winCycles; }
|
||||
uint32_t EthercatDiffStats::changedMax() noexcept { return s_winChangedMax; }
|
||||
uint32_t EthercatDiffStats::runsTotal() noexcept { return s_winRunsTotal; }
|
||||
uint32_t EthercatDiffStats::runsMax() noexcept { return s_winRunsMax; }
|
||||
uint32_t EthercatDiffStats::seg1Bytes() noexcept { return s_winSeg[0]; }
|
||||
uint32_t EthercatDiffStats::seg2Bytes() noexcept { return s_winSeg[1]; }
|
||||
uint32_t EthercatDiffStats::seg3Bytes() noexcept { return s_winSeg[2]; }
|
||||
uint32_t EthercatDiffStats::seg4Bytes() noexcept { return s_winSeg[3]; }
|
||||
uint32_t EthercatDiffStats::seg5Bytes() noexcept { return s_winSeg[4]; }
|
||||
uint32_t EthercatDiffStats::compareCyc() noexcept { return s_winCompareCyc; }
|
||||
|
||||
/* ── 供 dc_app.c(C 侧对象字典) 取值:一次拿全 11 个窗口值,避免 11 次跨语言调用 ── */
|
||||
extern "C" {
|
||||
|
||||
/**
|
||||
* @brief [DIFF-STATS] 读上一个完整 1s 窗口的统计值。
|
||||
* @param[out] out 依次对应 0x2000 sub13..sub23。
|
||||
* 形参用 uint32_t 而非 SSC 的 UINT32 别名:本翻译单元不引 SSC 头,
|
||||
* 两者同为 32 位无符号,C 侧声明仍按 UINT32 写。
|
||||
*/
|
||||
void EcatDiffStats_GetWindow(uint32_t *out)
|
||||
{
|
||||
if (out == nullptr) {
|
||||
return;
|
||||
}
|
||||
out[0] = EthercatDiffStats::windowSeq();
|
||||
out[1] = EthercatDiffStats::cycles();
|
||||
out[2] = EthercatDiffStats::changedMax();
|
||||
out[3] = EthercatDiffStats::runsTotal();
|
||||
out[4] = EthercatDiffStats::runsMax();
|
||||
out[5] = EthercatDiffStats::seg1Bytes();
|
||||
out[6] = EthercatDiffStats::seg2Bytes();
|
||||
out[7] = EthercatDiffStats::seg3Bytes();
|
||||
out[8] = EthercatDiffStats::seg4Bytes();
|
||||
out[9] = EthercatDiffStats::seg5Bytes();
|
||||
out[10] = EthercatDiffStats::compareCyc();
|
||||
}
|
||||
|
||||
} /* extern "C" */
|
||||
|
||||
#else /* !CONFIG_ECAT_DIFF_STATS */
|
||||
/* 关掉时仍导出同名取值函数并填 0:这样 dc_app.c 的挂接点不需要再套一层
|
||||
* #if,子索引数量也保持与开启版一致(主站读到的恒 0,脚本按 winseq==0 判'未启用')。
|
||||
* 留一个空函数体反而会漏掉符号——1.6.20 首编就是栽在这里(L6218E)。 */
|
||||
extern "C" void EcatDiffStats_GetWindow(uint32_t *out)
|
||||
{
|
||||
if (out == nullptr) {
|
||||
return;
|
||||
}
|
||||
for (uint32_t i = 0U; i < 11U; ++i) {
|
||||
out[i] = 0U;
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* CONFIG_ECAT_DIFF_STATS */
|
||||
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* @file ethercat_diff_stats.h
|
||||
* @brief [DIFF-STATS] 临时仪表:量化"若把输入 PDO 改成差分写,每拍真正需要写多少字节、
|
||||
* 变化点碎成几个连续段(run)"。只测量,不改变任何现有写行为。
|
||||
*
|
||||
* 存在的理由:差分写(方案 A)的收益完全取决于"每拍有多少字节真的变了",而 run 碎片
|
||||
* 数量决定它会不会把一次 600B 整块写退化成几百次带 AVAIL 轮询的小写。这两个数今天
|
||||
* 都没有实测依据,所以先只上仪表,拿分布再决定 A1(语义分区条件写) 值不值得做、
|
||||
* A2(块粒度差分) 的碎片上限 N 该取几。
|
||||
*
|
||||
* 顺带把"比较本身要多久"也测出来(sub23 累计 DWT 周期)——这是差分方案最容易被
|
||||
* 质疑的成本项,用实测代替拍脑袋。
|
||||
*
|
||||
* @note 结果按 1s 窗口滚动(与 sys/cpu_load.cpp 同法,用 DWT 周期判窗口边界),
|
||||
* getter 返回**上一个已完成的完整窗口**,因此主站任何时刻 SDO 读到的都是
|
||||
* 稳定值,不会拿到半累积的窗口。窗口未满时 windowSeq()==0,读侧应显示
|
||||
* "等待首个完整窗口"而不是当 0 解读。
|
||||
*
|
||||
* @note 删除方法:删本文件与 ethercat_diff_stats.cpp + 删全工程 [DIFF-STATS] 标记行
|
||||
* + 把 sys/config.h 的 CONFIG_ECAT_DIFF_STATS 置 0。对象字典 0x2000 的
|
||||
* sub13..23 字段建议保留(恒 0,不影响任何既有子索引)。
|
||||
*/
|
||||
#ifndef COMM_ETHERCAT_DIFF_STATS_H
|
||||
#define COMM_ETHERCAT_DIFF_STATS_H
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
/**
|
||||
* @brief 输入镜像差分统计。
|
||||
* @note 全静态;observe() 在 PDI_Isr 上下文每拍调用一次,故内部状态无需临界区
|
||||
* (唯一写者),getter 由 APPL_InputMapping 同上下文读取。
|
||||
*/
|
||||
class EthercatDiffStats {
|
||||
public:
|
||||
/**
|
||||
* @brief 与上一份已发布的输入镜像比对并累计统计。
|
||||
* @param[in] cur 刚发布完成的完整输入 PDO 镜像(kEcatPdoInBytes 字节)。
|
||||
* @note 内部自持上一份副本,调用方无需额外维护缓冲、也不必自己 memcpy。
|
||||
* 首次调用只建立基准,不产生样本(否则会把"全 600B 都变了"记成第一拍)。
|
||||
*/
|
||||
static void observe(const uint8_t *cur) noexcept;
|
||||
|
||||
/** @brief 丢弃基准,下一次调用重新起算(AL 状态跳变、布局变更时用)。 */
|
||||
static void reset() noexcept;
|
||||
|
||||
/* ── 对象字典 0x2000 sub13..23 的取值接口,均为"上一个完整 1s 窗口" ── */
|
||||
static uint32_t windowSeq() noexcept; /**< sub13 已完成窗口数,0=还没有完整窗口 */
|
||||
static uint32_t cycles() noexcept; /**< sub14 窗口内参与比对的拍数 */
|
||||
static uint32_t changedMax() noexcept; /**< sub15 单拍变化字节峰值 */
|
||||
static uint32_t runsTotal() noexcept; /**< sub16 窗口内 run 总数(配 cycles 求均值) */
|
||||
static uint32_t runsMax() noexcept; /**< sub17 单拍 run 数峰值,A2 的 N 由它定 */
|
||||
static uint32_t seg1Bytes() noexcept; /**< sub18 段1(0..31) 桥状态+短快照 */
|
||||
static uint32_t seg2Bytes() noexcept; /**< sub19 段2(32..543) 电机整轮密排 */
|
||||
static uint32_t seg3Bytes() noexcept; /**< sub20 段3(544..583) 长帧快照 */
|
||||
static uint32_t seg4Bytes() noexcept; /**< sub21 已删(1.6.21 恒 0) */
|
||||
static uint32_t seg5Bytes() noexcept; /**< sub22 段5(584..599) 诊断区+保留 */
|
||||
static uint32_t compareCyc() noexcept; /**< sub23 比对自身累计 DWT 周期(本征开销) */
|
||||
};
|
||||
|
||||
#endif /* COMM_ETHERCAT_DIFF_STATS_H */
|
||||
@@ -1,17 +1,60 @@
|
||||
/**
|
||||
* @file ethercat_pdo.cpp
|
||||
* @brief EtherCAT 过程数据(PDO)收发模块实现(1.5.55)。
|
||||
* @brief EtherCAT 过程数据(PDO)收发模块实现。
|
||||
*/
|
||||
#include "comm/ethercat_pdo.h"
|
||||
#include "comm/ethercat_can_bridge.h"
|
||||
#include "comm/esc_exmc_bus.h"
|
||||
#include "device/canfd.h"
|
||||
#include "sys/config.h"
|
||||
#include "sys/cpu_load.h"
|
||||
#include "RTE_Components.h"
|
||||
#include CMSIS_device_header /* __DMB() */
|
||||
/* [DIFF-STATS] 临时仪表挂接(删除:去掉本块与 process() 内的 observe 块)。
|
||||
* 上一行的 sys/config.h 必须在此之前:宏未定义时 #if 会静默按 0 求值,
|
||||
* 开关就会被悄悄当成"关掉"。 */
|
||||
#if CONFIG_ECAT_DIFF_STATS
|
||||
#include "comm/ethercat_diff_stats.h"
|
||||
#endif
|
||||
|
||||
#include <cstring>
|
||||
|
||||
uint8_t EthercatPdo::s_out[EthercatPdo::kPdoOutBytes] = {0};
|
||||
uint8_t EthercatPdo::s_in[EthercatPdo::kPdoInBytes] = {0};
|
||||
extern "C" {
|
||||
/* SSC 输出过程数据:SM2 地址与长度(ecatslv.c,类型为 SSC UINT16=uint16) */
|
||||
extern uint16_t nEscAddrOutputData;
|
||||
extern uint16_t nPdOutputSize;
|
||||
void HW_EscRead(void *pData, uint16_t Address, uint16_t Len);
|
||||
}
|
||||
|
||||
/* alignas(4) 必须在类内声明与出类定义上同时标注:AC6(Clang) 否则报
|
||||
* "'alignas' must be specified on definition"。用途见 ethercat_pdo.h。 */
|
||||
alignas(4) uint8_t EthercatPdo::s_out[EthercatPdo::kPdoOutBytes] = {0};
|
||||
alignas(4) uint8_t EthercatPdo::s_in[EthercatPdo::kPdoInBytes] = {0};
|
||||
/* 输入 PDO 的工作缓冲:buildInput 填这里,填完整块发布到 s_in(见 process())。
|
||||
* 读侧 writeInputs 只读 s_in,因此永远看不到"填到一半"的镜像。 */
|
||||
alignas(4) uint8_t EthercatPdo::s_build[EthercatPdo::kPdoInBytes] = {0};
|
||||
|
||||
namespace {
|
||||
/* 待交回主站的输入数据双缓冲(ping-pong,配合 __DMB() 保证原子发布与 tear-free 读写) */
|
||||
alignas(4) uint8_t s_inBuf[2][EthercatPdo::kPdoInBytes] = {{0}, {0}};
|
||||
volatile uint8_t s_inActiveIdx = 0U;
|
||||
}
|
||||
alignas(4) uint8_t EthercatPdo::s_escSh1[32] = {0};
|
||||
alignas(4) uint8_t EthercatPdo::s_escSh2[ECAT_XRX_OFF_CH - 32U] = {0};
|
||||
alignas(4) uint8_t EthercatPdo::s_escSh3[kEcatPdoInBytes - ECAT_XRX_OFF_CH] = {0};
|
||||
uint8_t EthercatPdo::s_escFlush1 = 0U;
|
||||
uint8_t EthercatPdo::s_escFlush2 = 0U;
|
||||
uint8_t EthercatPdo::s_escFlush3 = 0U;
|
||||
bool EthercatPdo::s_escSh1Valid = false;
|
||||
bool EthercatPdo::s_escSh2Valid = false;
|
||||
bool EthercatPdo::s_escSh3Valid = false;
|
||||
|
||||
volatile uint32_t EthercatPdo::s_outputCycles = 0U;
|
||||
volatile uint32_t EthercatPdo::s_inputCycles = 0U;
|
||||
volatile uint32_t EthercatPdo::s_reentryCount = 0U;
|
||||
/** OutputMapping 本拍是否已刷新 s_out(process 兜底读的新鲜度门) */
|
||||
static volatile bool s_outMappedThisCycle = false;
|
||||
volatile uint32_t EthercatPdo::s_processSeq = 0U;
|
||||
|
||||
void EthercatPdo::readOutputs(const uint8_t *esc)
|
||||
{
|
||||
@@ -22,16 +65,144 @@ void EthercatPdo::readOutputs(const uint8_t *esc)
|
||||
++s_outputCycles;
|
||||
}
|
||||
|
||||
bool EthercatPdo::outputProbe(const uint8_t *head)
|
||||
{
|
||||
return EthercatCanBridge::peekOutput(head);
|
||||
}
|
||||
|
||||
void EthercatPdo::process()
|
||||
{
|
||||
/* 重入防护:SSC 的 ECAT_Application()→本函数 在**四个上下文**都会被
|
||||
* 调用 —— PDI_Isr / Sync0_Isr / Sync1_Isr / MainLoop(见 ssc/src/ecatappl.c
|
||||
* 388、499、527、641 行)。而 MainLoop 那条路只用 DISABLE_ESC_INT() 关了
|
||||
* EXTI1(ESC IRQ),**挡不住 EXTI0(Sync0,preempt=0) 和 EXTI2(Sync1,preempt=1)**。
|
||||
*
|
||||
* 竞态:MainLoop 把 s_in 填到一半时被 Sync0/Sync1
|
||||
* 抢占 → 内层重入本函数,memset 清空后重填整个输入 PDO,随后 PDO_InputMapping()
|
||||
* 把这一份发给 ESC;外层回来又按自己那轮的进度接着往下写。于是发出去的镜像里
|
||||
* 批量区相邻两条来自**不同轮次**。
|
||||
*
|
||||
* 处理:已在填充中就直接返回。输入 PDO 是周期量,跳过一次下一拍即补,
|
||||
* 远好过发出一份跨轮次拼接的镜像。 */
|
||||
static volatile bool s_building = false;
|
||||
if (s_building) {
|
||||
++s_reentryCount;
|
||||
return;
|
||||
}
|
||||
s_building = true;
|
||||
|
||||
/* PDO ↔ CAN 转发:输出 PDO 的 seq 字节变化沿触发一次 CAN 发送,
|
||||
* 输入 PDO 回填桥状态 + 电机状态窗口(布局见 ethercat_can_bridge.h)。
|
||||
*
|
||||
* 顺序必须是先 processOutput 后 buildInput:buildInput 会把本周期刚执行的
|
||||
* seq 回显成 ack,主站因此能在下一周期立刻确认送达;反过来则 ack 恒滞后一拍。
|
||||
* processOutput 还负责取走主站请求的状态窗口下标,也必须在 buildInput 之前。 */
|
||||
* processOutput 还负责取走主站请求的状态窗口下标,也必须在 buildInput 之前。
|
||||
*
|
||||
* 填的是**工作缓冲** s_build,填完整体发布到 s_in:只有重入门还不够 ——
|
||||
* 内层被门挡住后,它随后的 PDO_InputMapping() 仍会去读 s_in,若 buildInput
|
||||
* 直接写 s_in,读到的就是外层写到一半的半成品。填在别处、整块发布之后,
|
||||
* 读侧要么拿到上一份完整镜像、要么拿到这一份完整镜像,不存在中间态。 */
|
||||
/* 双缓冲发布:填入非活跃槽 nextIdx,完成后通过 __DMB() 与 s_inActiveIdx 切换
|
||||
* 实现原子发布,彻底避免中断读侧(PDI_Isr / Sync0_Isr)与主循环之间的读写撕裂。 */
|
||||
const uint8_t nextIdx = 1U - s_inActiveIdx;
|
||||
uint8_t *build = s_inBuf[nextIdx];
|
||||
|
||||
/* 输出兜底全量读:仅当 OutputMapping 本拍未刷新 s_out,且
|
||||
* (s_out 看起来是死的 或 距上次强制读已跑过一段 process) 才读。
|
||||
* 每拍 process() 都 HW_EscRead 384B 会与 PDO_OutputMapping 叠成双份 HBI,
|
||||
* PDO 2kHz 即把 ISR 成本打满(历史探针版 4kHz load≈80%)。 */
|
||||
static uint32_t s_lastForceReadSeq = 0U;
|
||||
if (nPdOutputSize > 0U && nEscAddrOutputData != 0U && !s_outMappedThisCycle) {
|
||||
const bool outDead = (s_out[0] == 0U && s_out[1] == 0U && s_out[8] == 0U);
|
||||
const uint32_t seqNow = s_processSeq;
|
||||
/* 仅 s_out 疑似死数据时强制全量读;探针已处理 seq 沿。
|
||||
* 按拍数兜底间隔拉到 256,避免 8kHz 下周期性 384B HBI。 */
|
||||
if (outDead || (uint32_t)(seqNow - s_lastForceReadSeq) >= 256U) {
|
||||
const uint16_t nread = (nPdOutputSize > kPdoOutBytes)
|
||||
? kPdoOutBytes
|
||||
: nPdOutputSize;
|
||||
uint8_t escOut[kEcatPdoOutBytes];
|
||||
const uint16_t prefix = (nread < 52U) ? nread : 52U;
|
||||
HW_EscRead(escOut, nEscAddrOutputData, prefix);
|
||||
const uint8_t batchCount = (prefix > 48U) ? escOut[48] : 0U;
|
||||
if (batchCount > 0U && nread > prefix) {
|
||||
HW_EscRead(&escOut[prefix],
|
||||
(uint16_t)(nEscAddrOutputData + prefix),
|
||||
(uint16_t)(nread - prefix));
|
||||
}
|
||||
readOutputs(escOut);
|
||||
s_lastForceReadSeq = seqNow;
|
||||
}
|
||||
}
|
||||
s_outMappedThisCycle = false;
|
||||
|
||||
EthercatCanBridge::processOutput(s_out);
|
||||
EthercatCanBridge::buildInput(s_in);
|
||||
EthercatCanBridge::buildInput(build);
|
||||
fillDiagTail(build);
|
||||
|
||||
__DMB();
|
||||
s_inActiveIdx = nextIdx;
|
||||
/* 不再 memcpy 到 s_in:writeInputs/稀疏写/差分写都读 s_inBuf[activeIdx],
|
||||
* 每拍多拷 600B 是纯浪费(8kHz 下不可接受)。 */
|
||||
++s_processSeq;
|
||||
|
||||
#if CONFIG_ECAT_DIFF_STATS
|
||||
/* [DIFF-STATS] 只测量:与上一份已发布镜像比对并累计到 1s 窗口,经 0x2000
|
||||
* sub13..23 上报。不改变上面的整块写行为。仪表自持上一份副本,这里不需要额外缓冲。 */
|
||||
EthercatDiffStats::observe(s_inBuf[nextIdx]);
|
||||
#endif
|
||||
|
||||
s_building = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 填充输入 PDO 段 5 设备诊断区(632..639)。
|
||||
* @param[out] in 全长输入过程数据缓冲(buildInput 刚清零+填完段 1~4)。
|
||||
* @note 负载率 1s 滚动一个窗口,seq 随窗口递增——主站只认"seq 在变"的负载值。
|
||||
* 每周期重写 6 字节比"值变化才写"的分支判断更便宜,也不引入隐藏状态。
|
||||
*/
|
||||
void EthercatPdo::fillDiagTail(uint8_t *in)
|
||||
{
|
||||
static uint16_t s_diagTick = 0U;
|
||||
const uint16_t load = CpuLoad::loadPermille();
|
||||
in[ECAT_DIAG_OFF_LOAD] = (uint8_t)(load & 0xFFU);
|
||||
in[ECAT_DIAG_OFF_LOAD + 1] = (uint8_t)(load >> 8);
|
||||
|
||||
/* CAN 取帧撕裂丢帧数。只有 16 位,饱和不回绕 —— 主站看的是
|
||||
* "有没有涨、涨多快",回绕会被误读成计数清零。 */
|
||||
const CanFd::Diag *cd = CanFd::diag();
|
||||
const uint32_t torn = (cd != nullptr) ? cd->rx_torn_count : 0U;
|
||||
const uint16_t torn16 = (torn > 0xFFFFU) ? 0xFFFFU : (uint16_t)torn;
|
||||
in[ECAT_DIAG_OFF_TORN] = (uint8_t)(torn16 & 0xFFU);
|
||||
in[ECAT_DIAG_OFF_TORN + 1] = (uint8_t)(torn16 >> 8);
|
||||
|
||||
/* seq 写「本函数被调用次数」而不是 windowSeq。
|
||||
* windowSeq 依赖 CpuLoad::tick() 滚窗;Process 若在 tick 前停摆,
|
||||
* 主站会长期读到 0 且分不清「没跑」和「窗口未满」。自由计数每拍+1,
|
||||
* 冻结看门狗与 app/load 的「seq 在涨」语义都更干净。 */
|
||||
++s_diagTick;
|
||||
in[ECAT_DIAG_OFF_SEQ] = (uint8_t)(s_diagTick & 0xFFU);
|
||||
in[ECAT_DIAG_OFF_SEQ + 1] = (uint8_t)(s_diagTick >> 8);
|
||||
|
||||
/* CAN RX FIFO 满溢计数 RFO0:与内容级 reject 计数是否同涨,
|
||||
* 是"残余拼接是否仍源自溢出"的观测指标。 */
|
||||
const uint32_t of = (cd != nullptr) ? cd->rx_fifo_overfull_count : 0U;
|
||||
const uint16_t of16 = (of > 0xFFFFU) ? 0xFFFFU : (uint16_t)of;
|
||||
in[ECAT_DIAG_OFF_OVERFULL] = (uint8_t)(of16 & 0xFFU);
|
||||
in[ECAT_DIAG_OFF_OVERFULL + 1] = (uint8_t)(of16 >> 8);
|
||||
|
||||
/* RFD0 释放握手超时计数。dequeue 握手没完成 = 窗口/读指针失同步前兆。 */
|
||||
const uint32_t rt = (cd != nullptr) ? cd->rx_release_timeout_count : 0U;
|
||||
const uint16_t rt16 = (rt > 0xFFFFU) ? 0xFFFFU : (uint16_t)rt;
|
||||
in[ECAT_DIAG_OFF_RELTIMEOUT] = (uint8_t)(rt16 & 0xFFU);
|
||||
in[ECAT_DIAG_OFF_RELTIMEOUT + 1] = (uint8_t)(rt16 >> 8);
|
||||
|
||||
/* 内容级拼接帧拦截次数:MI/MP 校验挡不住的指针错位拼接由入队前
|
||||
* 结构自洽校验兜底,语义同上 —— 主站看"涨不涨"判断拼接是否仍在发生。 */
|
||||
const uint32_t rj = (cd != nullptr) ? cd->rx_content_reject_count : 0U;
|
||||
const uint16_t rj16 = (rj > 0xFFFFU) ? 0xFFFFU : (uint16_t)rj;
|
||||
in[ECAT_DIAG_OFF_REJECT] = (uint8_t)(rj16 & 0xFFU);
|
||||
in[ECAT_DIAG_OFF_REJECT + 1] = (uint8_t)(rj16 >> 8);
|
||||
}
|
||||
|
||||
void EthercatPdo::writeInputs(uint8_t *esc)
|
||||
@@ -39,10 +210,149 @@ void EthercatPdo::writeInputs(uint8_t *esc)
|
||||
if (esc == nullptr) {
|
||||
return;
|
||||
}
|
||||
memcpy(esc, s_in, kPdoInBytes);
|
||||
const uint8_t activeIdx = s_inActiveIdx;
|
||||
__DMB();
|
||||
memcpy(esc, s_inBuf[activeIdx], kPdoInBytes);
|
||||
++s_inputCycles;
|
||||
}
|
||||
|
||||
bool EthercatPdo::sparseEscWritePlan(uint16_t *off0, uint16_t *len0,
|
||||
uint16_t *off1, uint16_t *len1)
|
||||
{
|
||||
if (off0 == nullptr || len0 == nullptr || off1 == nullptr || len1 == nullptr) {
|
||||
return false;
|
||||
}
|
||||
#if !CONFIG_ECAT_SPARSE_ESC_WRITE
|
||||
return false;
|
||||
#else
|
||||
/* 仅密排布局:count 有效且两段之间确有空洞才分段。
|
||||
* magic 不对 / count 越界 / count 撑满到与段3相接 → 整包,避免陈旧空洞被误读。 */
|
||||
const uint8_t activeIdx = s_inActiveIdx;
|
||||
__DMB();
|
||||
const uint8_t *cur = s_inBuf[activeIdx];
|
||||
if (cur[ECAT_MOTW_OFF_MAGIC] != kEcatMotLayoutMagic) {
|
||||
return false;
|
||||
}
|
||||
const uint8_t count = cur[ECAT_MOTW_OFF_COUNT];
|
||||
if (count > kEcatMotSlotsPerPdo) {
|
||||
return false;
|
||||
}
|
||||
const uint16_t headEnd =
|
||||
(uint16_t)(ECAT_MOTW_OFF_SLOTS + (uint16_t)count * kEcatMotSlotSize);
|
||||
if (headEnd >= ECAT_XRX_OFF_CH) {
|
||||
return false; /* 9 电机满配:A+B 无缝,整包更省一次 EXMC 事务 */
|
||||
}
|
||||
*off0 = 0U;
|
||||
*len0 = headEnd;
|
||||
*off1 = ECAT_XRX_OFF_CH;
|
||||
*len1 = (uint16_t)(kPdoInBytes - ECAT_XRX_OFF_CH);
|
||||
return true;
|
||||
#endif
|
||||
}
|
||||
|
||||
namespace {
|
||||
/** 段2 有效长度(稀疏:到 count*56 为止;否则整段)。 */
|
||||
uint16_t escSeg2WriteLen(const uint8_t *in)
|
||||
{
|
||||
#if CONFIG_ECAT_SPARSE_ESC_WRITE
|
||||
if (in[ECAT_MOTW_OFF_MAGIC] == kEcatMotLayoutMagic) {
|
||||
const uint8_t count = in[ECAT_MOTW_OFF_COUNT];
|
||||
if (count <= kEcatMotSlotsPerPdo) {
|
||||
const uint16_t headEnd =
|
||||
(uint16_t)(ECAT_MOTW_OFF_SLOTS + (uint16_t)count * kEcatMotSlotSize);
|
||||
if (headEnd < ECAT_XRX_OFF_CH) {
|
||||
return headEnd;
|
||||
}
|
||||
}
|
||||
}
|
||||
#else
|
||||
(void)in;
|
||||
#endif
|
||||
return ECAT_XRX_OFF_CH;
|
||||
}
|
||||
|
||||
/** 判定是否需要写某区:无影子 / 冲刷中 / 内容变化。变化时刷新影子并重置冲刷=2。 */
|
||||
bool diffNeedWrite(const uint8_t *cur, uint16_t off, uint16_t cmpLen,
|
||||
uint8_t *shadow, bool &valid, uint8_t &flush)
|
||||
{
|
||||
const bool changed = !valid || memcmp(cur + off, shadow, cmpLen) != 0;
|
||||
if (changed) {
|
||||
memcpy(shadow, cur + off, cmpLen);
|
||||
valid = true;
|
||||
flush = 2U; /* 本拍 + 再 2 拍,冲完 3-buffer */
|
||||
return true;
|
||||
}
|
||||
if (flush > 0U) {
|
||||
--flush;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
} /* anonymous namespace */
|
||||
|
||||
uint8_t EthercatPdo::escWritePlan(uint16_t *offs, uint16_t *lens)
|
||||
{
|
||||
if (offs == nullptr || lens == nullptr) {
|
||||
return 0U;
|
||||
}
|
||||
|
||||
#if !CONFIG_ECAT_DIFF_ESC_WRITE
|
||||
/* 回退:稀疏两段或整包 */
|
||||
uint8_t n = 0U;
|
||||
uint16_t o0, l0, o1, l1;
|
||||
if (sparseEscWritePlan(&o0, &l0, &o1, &l1)) {
|
||||
offs[n] = o0; lens[n] = l0; ++n;
|
||||
offs[n] = o1; lens[n] = l1; ++n;
|
||||
} else {
|
||||
offs[0] = 0U; lens[0] = kPdoInBytes; n = 1U;
|
||||
}
|
||||
return n;
|
||||
#else
|
||||
/* 三区差分:段1 桥状态 / 段2 电机密排(稀疏写有效长) / 段3+5 快照诊断。
|
||||
* 段1 计数器几乎每拍变,段2 稳定时省最大头,段3+5 多数拍稳定。
|
||||
* 无论各段是否变化,必须保证每周期必定写入 SM3 结束地址 (kPdoInBytes - 1),
|
||||
* 以触发 LAN9252 3-buffer 提交换冲,否则主站将读到冻结的旧缓冲。 */
|
||||
const uint8_t activeIdx = s_inActiveIdx;
|
||||
__DMB();
|
||||
const uint8_t *cur = s_inBuf[activeIdx];
|
||||
|
||||
uint8_t n = 0U;
|
||||
if (diffNeedWrite(cur, 0U, 32U, s_escSh1, s_escSh1Valid, s_escFlush1)) {
|
||||
offs[n] = 0U;
|
||||
lens[n] = 32U;
|
||||
++n;
|
||||
}
|
||||
if (diffNeedWrite(cur, 32U, (uint16_t)(ECAT_XRX_OFF_CH - 32U),
|
||||
s_escSh2, s_escSh2Valid, s_escFlush2)) {
|
||||
offs[n] = 32U;
|
||||
/* 写长:稀疏时只写到 count*56,但影子比对的是完整 32..543(含清零空洞)。
|
||||
* 空洞不写入 ESC 无妨:主站只按 count 迭代。 */
|
||||
const uint16_t wlen = escSeg2WriteLen(cur);
|
||||
lens[n] = (wlen > 32U) ? (uint16_t)(wlen - 32U) : 0U;
|
||||
if (lens[n] == 0U) {
|
||||
/* count=0:段2 头 8 字节仍要写(count/active/magic) */
|
||||
lens[n] = 8U;
|
||||
}
|
||||
++n;
|
||||
}
|
||||
bool seg3Included = false;
|
||||
if (diffNeedWrite(cur, ECAT_XRX_OFF_CH, (uint16_t)(kPdoInBytes - ECAT_XRX_OFF_CH),
|
||||
s_escSh3, s_escSh3Valid, s_escFlush3)) {
|
||||
offs[n] = ECAT_XRX_OFF_CH;
|
||||
lens[n] = (uint16_t)(kPdoInBytes - ECAT_XRX_OFF_CH);
|
||||
++n;
|
||||
seg3Included = true;
|
||||
}
|
||||
if (!seg3Included) {
|
||||
/* 段 3 未包含时,补写末 2 字节以保证触碰 SM3 结束地址触发硬件提交 */
|
||||
offs[n] = (uint16_t)(kPdoInBytes - 2U);
|
||||
lens[n] = 2U;
|
||||
++n;
|
||||
}
|
||||
return n;
|
||||
#endif
|
||||
}
|
||||
|
||||
void EthercatPdo::snapshotOutput(uint8_t *dst)
|
||||
{
|
||||
if (dst == nullptr) {
|
||||
@@ -53,11 +363,23 @@ void EthercatPdo::snapshotOutput(uint8_t *dst)
|
||||
|
||||
extern "C" {
|
||||
|
||||
/* 输出探针,SSC PDI_Isr 的 PDO_OutputMapping 每周期调用(C 链接)。
|
||||
* 返回 0/1;SSC 侧声明为 UINT8,小返回值零扩展,调用约定兼容。 */
|
||||
uint8_t EcatPdo_OutputProbe(const uint8_t *head)
|
||||
{
|
||||
return EthercatPdo::outputProbe(head) ? 1U : 0U;
|
||||
}
|
||||
|
||||
void EcatPdo_ReadOutputs(const uint8_t *esc)
|
||||
{
|
||||
EthercatPdo::readOutputs(esc);
|
||||
}
|
||||
|
||||
void EcatPdo_NoteOutputMapped(void)
|
||||
{
|
||||
s_outMappedThisCycle = true;
|
||||
}
|
||||
|
||||
void EcatPdo_Process(void)
|
||||
{
|
||||
EthercatPdo::process();
|
||||
@@ -68,4 +390,58 @@ void EcatPdo_WriteInputs(uint8_t *esc)
|
||||
EthercatPdo::writeInputs(esc);
|
||||
}
|
||||
|
||||
/* 稀疏写计划:SSC PDO_InputMapping 据此决定 HW_EscWriteIsr 写几段。
|
||||
* 返回 1=两段有效;0=回退整包。 */
|
||||
uint8_t EcatPdo_SparseEscPlan(uint16_t *off0, uint16_t *len0,
|
||||
uint16_t *off1, uint16_t *len1)
|
||||
{
|
||||
return EthercatPdo::sparseEscWritePlan(off0, len0, off1, len1) ? 1U : 0U;
|
||||
}
|
||||
|
||||
/* 差分写计划:SSC PDO_InputMapping 逐段 HW_EscWriteIsr。 */
|
||||
uint8_t EcatPdo_EscWritePlan(uint16_t *offs, uint16_t *lens, uint8_t maxSegs)
|
||||
{
|
||||
if (offs == nullptr || lens == nullptr || maxSegs == 0U
|
||||
|| maxSegs > EthercatPdo::kEscPlanMaxSegs) {
|
||||
return 0U;
|
||||
}
|
||||
uint16_t o[EthercatPdo::kEscPlanMaxSegs];
|
||||
uint16_t l[EthercatPdo::kEscPlanMaxSegs];
|
||||
const uint8_t n = EthercatPdo::escWritePlan(o, l);
|
||||
if (n == 0U || n > maxSegs) {
|
||||
return 0U;
|
||||
}
|
||||
for (uint8_t i = 0U; i < n; ++i) {
|
||||
offs[i] = o[i];
|
||||
lens[i] = l[i];
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
/* HBI/EXMC 事务互斥(仅屏蔽 EXTI0/1/2,不关全局中断) */
|
||||
uint32_t EscExmcBus_MaskEnter(void)
|
||||
{
|
||||
return EscExmcBus::maskEnter();
|
||||
}
|
||||
|
||||
void EscExmcBus_MaskExit(uint32_t prev)
|
||||
{
|
||||
EscExmcBus::maskExit(prev);
|
||||
}
|
||||
|
||||
uint32_t HW_EscExmcMaskEnter(void)
|
||||
{
|
||||
return EscExmcBus::maskEnter();
|
||||
}
|
||||
|
||||
void HW_EscExmcMaskExit(uint32_t prev)
|
||||
{
|
||||
EscExmcBus::maskExit(prev);
|
||||
}
|
||||
|
||||
uint32_t EcatPdo_ProcessSeq(void)
|
||||
{
|
||||
return EthercatPdo::processSeq();
|
||||
}
|
||||
|
||||
} /* extern "C" */
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* @file ethercat_pdo.h
|
||||
* @brief EtherCAT 过程数据(PDO)收发模块(1.5.54 由 dc_app.c 的散装 C 代码类化)。
|
||||
* @brief EtherCAT 过程数据(PDO)收发模块。
|
||||
*
|
||||
* 一个 EtherCAT 周期在 SSC 里被拆成三个回调,本类按这三步各提供一个入口:
|
||||
* 1. APPL_OutputMapping() → readOutputs() 从 ESC 取走主站下发的输出数据
|
||||
@@ -11,7 +11,7 @@
|
||||
* 是 SDO 存储,必须留在 C 侧)与三行 extern "C" 转发。这样"收发"这件事只有一个
|
||||
* 归属:想加新的过程数据语义只改本类,不必再动 SSC 移植层。
|
||||
*
|
||||
* @note 1.5.55 起输出/输入**长度不再相等**(64B / 384B),为电机控制台腾出
|
||||
* @note 输出/输入**长度不相等**(64B / 384B),为电机控制台腾出
|
||||
* 状态窗口空间。长度常量统一由 ethercat_can_bridge.h 提供,避免三处各写
|
||||
* 一个魔数后失配。
|
||||
*
|
||||
@@ -48,6 +48,16 @@ public:
|
||||
*/
|
||||
static void readOutputs(const uint8_t *esc);
|
||||
|
||||
/**
|
||||
* @brief 输出探针(输出按需全量读,配合 SSC PDO_OutputMapping)。
|
||||
* @param[in] head 输出 PDO 头 12B(覆盖 SEQ@0 / WIN@8 / FLAGS@3)。
|
||||
* @return true=需要全量读(seq 沿或清统计请求);false=本周期跳过全量读。
|
||||
* @note 每周期由 PDI_Isr 在读 ESC 前调用:做 seq 基准 priming 与沿判定;
|
||||
* s_lastSeq 的推进仍留在全量路径(processOutput)。不再
|
||||
* 维护 win_base(段 2 电机整轮密排,无滑窗)。
|
||||
*/
|
||||
static bool outputProbe(const uint8_t *head);
|
||||
|
||||
/**
|
||||
* @brief 跑一个应用周期:消费输出、生成输入。
|
||||
* @note 由 APPL_Application() 在主循环调用。
|
||||
@@ -57,15 +67,48 @@ public:
|
||||
/**
|
||||
* @brief 交回输入过程数据(从站→主站)。
|
||||
* @param[out] esc 指向 ESC 输入缓冲的 kPdoInBytes 字节。
|
||||
* @note 由 APPL_InputMapping() 每周期调用。
|
||||
* @note 由 APPL_InputMapping() 每周期调用。只填 RAM/对象存储;
|
||||
* 真正写 ESC 的字节量由 PDO_InputMapping 经 sparseEscWritePlan 决定。
|
||||
*/
|
||||
static void writeInputs(uint8_t *esc);
|
||||
|
||||
/**
|
||||
* @brief 稀疏写 ESC 的两段计划:段1+段2有效区、段3+段5;跳过空槽区。
|
||||
* @param[out] off0/len0 第一段相对 PDO 起点的偏移与长度
|
||||
* @param[out] off1/len1 第二段
|
||||
* @return true=可按两段写;false=应回退整包 600B(开关关/非密排/count 越界/两段相接)
|
||||
* @note 必须在 writeInputs() 之后调用,读的是已发布的 s_in。
|
||||
*/
|
||||
static bool sparseEscWritePlan(uint16_t *off0, uint16_t *len0,
|
||||
uint16_t *off1, uint16_t *len1);
|
||||
|
||||
/**
|
||||
* @brief 输入 PDO 写 ESC 的多段差分计划(未启用时回退稀疏/整包)。
|
||||
* @param[out] offs 段偏移数组,至少 kEscPlanMaxSegs 项
|
||||
* @param[out] lens 段长度数组,至少 kEscPlanMaxSegs 项
|
||||
* @return 段数 1..kEscPlanMaxSegs;调用方逐段 HW_EscWriteIsr。
|
||||
* @note 必须在 writeInputs()/s_in 发布之后、每拍一次调用。按三区差分:
|
||||
* 段1(0..31 桥状态)、段2(32..543 电机密排,写时用稀疏有效长)、
|
||||
* 段3+5(544..599)。变化后连续写 3 拍冲完 3-buffer 才允许跳过。
|
||||
* 全段跳过时返回「末 2 字节」一段以完成 SM 事件。有副作用。
|
||||
*/
|
||||
static uint8_t escWritePlan(uint16_t *offs, uint16_t *lens);
|
||||
|
||||
/** escWritePlan 最多段数。 */
|
||||
static constexpr uint8_t kEscPlanMaxSegs = 4U;
|
||||
|
||||
/** @brief 累计取走输出的周期数(主站在发过程数据的证据)。 */
|
||||
static uint32_t outputCycles() { return s_outputCycles; }
|
||||
|
||||
/** @brief 累计交回输入的周期数。 */
|
||||
static uint32_t inputCycles() { return s_inputCycles; }
|
||||
/** @brief process() 完整填完一拍输入的序号。新鲜度救援看这个,而不是
|
||||
* InputMapping 的 s_inMapSeq——后者只证明「在拷陈旧 s_in」。 */
|
||||
static uint32_t processSeq() { return s_processSeq; }
|
||||
/** @brief process() 被重入拦下的次数。持续增长 = 跨上下文抢占确实在发生
|
||||
* (PDI_Isr / Sync0_Isr / Sync1_Isr / MainLoop 互相打断),也就证明了
|
||||
* 批量区跨轮次拼接的成因;恒为 0 则说明该机制推断有误。 */
|
||||
static uint32_t reentryCount() { return s_reentryCount; }
|
||||
|
||||
/** @brief 是否已开始过程数据交换(收到过至少一个输出周期)。 */
|
||||
static bool isExchanging() { return s_outputCycles != 0U; }
|
||||
@@ -77,13 +120,41 @@ public:
|
||||
static void snapshotOutput(uint8_t *dst);
|
||||
|
||||
private:
|
||||
/** @brief 最近一份主站下发的输出数据。 */
|
||||
static uint8_t s_out[kPdoOutBytes];
|
||||
/** @brief 待交回主站的输入数据。 */
|
||||
static uint8_t s_in[kPdoInBytes];
|
||||
/**
|
||||
* @brief 填充输入 PDO 段 5 设备诊断区(尾部 8 字节)。
|
||||
* @param[out] in 全长输入过程数据缓冲。
|
||||
* @note 布局常量 ECAT_DIAG_OFF_* 见 ethercat_can_bridge.h;当前携带
|
||||
* CPU 负载率(0.1% 单位) + 窗口序号,取自 sys/cpu_load.h。
|
||||
*/
|
||||
static void fillDiagTail(uint8_t *in);
|
||||
|
||||
/** @brief 最近一份主站下发的输出数据。
|
||||
* @note alignas(4):让整块 memcpy(writeInputs / [DIFF-STATS] 的快照拷贝)按
|
||||
* 字宽走而非逐字节。比对主循环本身不依赖它——已改成手拼 4 字节移位,
|
||||
* 因为 -Oz 下 memcpy(&a,p,4) 会编成函数调用,见 ethercat_diff_stats.cpp。 */
|
||||
alignas(4) static uint8_t s_out[kPdoOutBytes];
|
||||
/** @brief 待交回主站的输入数据(已发布的完整镜像,writeInputs 只读它)。 */
|
||||
alignas(4) static uint8_t s_in[kPdoInBytes];
|
||||
/** @brief 输入 PDO 工作缓冲:buildInput 填这里,填完整块发布到 s_in。
|
||||
* ECAT_Application() 会在 PDI_Isr / Sync0_Isr / Sync1_Isr / MainLoop 四个
|
||||
* 上下文被调用并互相抢占,直接写 s_in 会让读侧取到跨轮次的半成品。 */
|
||||
alignas(4) static uint8_t s_build[kPdoInBytes];
|
||||
/** @brief 写 ESC 差分影子:段1 / 段2 / 段3+5 三区。 */
|
||||
alignas(4) static uint8_t s_escSh1[32];
|
||||
alignas(4) static uint8_t s_escSh2[ECAT_XRX_OFF_CH - 32U];
|
||||
alignas(4) static uint8_t s_escSh3[kEcatPdoInBytes - ECAT_XRX_OFF_CH];
|
||||
static uint8_t s_escFlush1;
|
||||
static uint8_t s_escFlush2;
|
||||
static uint8_t s_escFlush3;
|
||||
static bool s_escSh1Valid;
|
||||
static bool s_escSh2Valid;
|
||||
static bool s_escSh3Valid;
|
||||
/** @brief process() 被重入拦下的次数(诊断用,见 reentryCount())。 */
|
||||
static volatile uint32_t s_reentryCount;
|
||||
|
||||
static volatile uint32_t s_outputCycles;
|
||||
static volatile uint32_t s_inputCycles;
|
||||
static volatile uint32_t s_processSeq;
|
||||
};
|
||||
|
||||
extern "C" {
|
||||
@@ -93,6 +164,10 @@ void EcatPdo_ReadOutputs(const uint8_t *esc);
|
||||
void EcatPdo_Process(void);
|
||||
/** @brief 供 dc_app.c 调用:交回输入过程数据。 */
|
||||
void EcatPdo_WriteInputs(uint8_t *esc);
|
||||
/** @brief 供 dc_app.c 新鲜度救援:process() 是否还在推进。 */
|
||||
uint32_t EcatPdo_ProcessSeq(void);
|
||||
/** @brief PDO_OutputMapping 探针命中并完成全量读后调用(本拍 s_out 已新鲜)。 */
|
||||
void EcatPdo_NoteOutputMapped(void);
|
||||
}
|
||||
|
||||
#endif /* COMM_ETHERCAT_PDO_H */
|
||||
|
||||
@@ -50,8 +50,15 @@
|
||||
/** 输出过程数据字节数(主站→从站,SM2):48×64bit。 */
|
||||
#define ECAT_PDO_OUT_BYTES 384
|
||||
|
||||
/** 输入过程数据字节数(从站→主站,SM3):80×64bit。 */
|
||||
#define ECAT_PDO_IN_BYTES 640
|
||||
/**
|
||||
* 输入过程数据字节数(从站→主站,SM3):75×64bit = 600B。
|
||||
* 布局(1.6.21 密排):段1..2 0..543 · 段3 544..583 · 段5 584..595。
|
||||
* 改长度须同步 bridge/dc_app/gen_eeprom/ESI/主站 decode,并烧 EEPROM 冷启动。
|
||||
*
|
||||
* 注意:DWT sub10 量的是从站 PDI 写 ESC 耗时,**不是**主站 rx 段。
|
||||
* 主站 rx 主要由线速往返决定(约 1KB @100Mbit ≈ 80µs 量级),两边 µs 不要对拍。
|
||||
*/
|
||||
#define ECAT_PDO_IN_BYTES 600
|
||||
|
||||
/** DPRAM 中过程数据区起址与 SM 缓冲数 —— 上限推导的依据,供静态断言使用。 */
|
||||
#define ECAT_PD_AREA_START 0x1400
|
||||
|
||||
@@ -64,13 +64,12 @@ void EthercatSlave::init()
|
||||
* (如 SM Watchdog=0x1B、FMMU 配置错、邮箱协议错),需主站介入。 */
|
||||
|
||||
/* ============================================================
|
||||
* bring-up 诊断(1.5.35 引入,1.5.39 重新定性):经 CSR 间接端口读三个关键寄存器
|
||||
* bring-up 诊断:经 CSR 间接端口读三个关键寄存器
|
||||
* + 一次写回环,确认 EtherCAT 核心侧状态。
|
||||
* 注意主站读身份/AL 走 ESC 以太网侧 + EEPROM(SII),完全不经过 GD32 的 PDI,
|
||||
* 所以"主站能读到身份"不能证明下面任何一项成立。
|
||||
* 1.5.39 起访问已改经 ECAT_CSR_CMD/CSR_DATA(EtherCAT 核心寄存器不在 HBI 直接
|
||||
* 地址空间,DS 12.11);此前直读索引使本段恒读全 0,曾被误判为"EEPROM 加载竞态"
|
||||
* 与"HBI 写路径损坏"两个错误结论。现在读到全 0 才真正指向 EEPROM/PDI 故障,
|
||||
* 访问必须经 ECAT_CSR_CMD/CSR_DATA(EtherCAT 核心寄存器不在 HBI 直接
|
||||
* 地址空间,DS 12.11);直读索引恒读全 0。
|
||||
* 交叉验证用 tools/ecat/ecat_esc_regs.py 从网络侧读同样的寄存器。
|
||||
* 本段在 EthercatSync::init() 使能 EXTI 之前执行,满足 initOnly* 的 INIT-ONLY 约束。
|
||||
* ============================================================ */
|
||||
@@ -87,9 +86,7 @@ void EthercatSlave::init()
|
||||
/* (3) EEPROM 接口状态 0x0502-0x0503(16 位字)。位定义按 ETG.1000.4 表 49
|
||||
* 与 SSC esc.h:bit11=Checksum Error、bit12=EEPROM loading status(0=OK)、
|
||||
* bit13=Missing EEPROM Acknowledge、bit14=Write Error、bit15=Busy。
|
||||
* 注意 1.5.40 及之前本行把 bit11 打成 "loaded",语义还正好反了
|
||||
* (bit11=1 才是校验错),于是正常板子恒显示 "loaded=0",字面像"EEPROM 未加载",
|
||||
* 正是历史上 1.5.37 那个已撤回的错误结论的诱因;现按位分别命名,0 全部代表正常。
|
||||
* 0 全部代表正常。
|
||||
* 真正的"EEPROM 已加载/PDI 可用"判据是 DL_STATUS(0x0110) bit0,见上方 (1)。 */
|
||||
const uint16_t eepCsr = EscLan9252::initOnlyReadReg(0x0502U);
|
||||
LOG_ERR("[ECAT][DIAG] DL_STATUS(0x110)=0x%04X pdi_oper=%u | "
|
||||
@@ -109,10 +106,7 @@ void EthercatSlave::init()
|
||||
* 就是 PDI 侧可写(PDI 用它选择哪些 AL Event 产生 IRQ),不像 0x0012 Station
|
||||
* Alias 在部分 ESC 上对 PDI 只读(会造成假 FAIL);本段在 MainInit/HW_Init
|
||||
* 之前执行,还原后不影响 SSC 后续建立的 mask 影子。
|
||||
* 1.5.39 起本回环经 ECAT_CSR_CMD/CSR_DATA 间接端口执行。历史注意:1.5.35
|
||||
* 该回环报 FAIL 曾被误判为"HBI 写路径损坏",真因是当时直读直写 HBI 索引——
|
||||
* EtherCAT 核心寄存器不在 HBI 直接地址空间(DS 12.11),读恒 0、写被丢弃。
|
||||
* 网络侧诊断(tools/ecat/ecat_esc_regs.py)已证实 ESC 本体与 EEPROM 一直正常。 */
|
||||
* 回环必须经 ECAT_CSR_CMD/CSR_DATA 间接端口执行。 */
|
||||
const uint16_t maskSaved = EscLan9252::initOnlyReadReg(0x0204U);
|
||||
EscLan9252::initOnlyWriteReg(0x0204U, 0x0A50U);
|
||||
const uint16_t maskRead = EscLan9252::initOnlyReadReg(0x0204U);
|
||||
@@ -129,7 +123,7 @@ void EthercatSlave::init()
|
||||
/* 是否有主站在线:DL_STATUS(0x0110) 的 Physical Link 位 bit4/5/6 对应 Port0/1/2
|
||||
* (DS 12.14.19)。三个端口都无 link = 网线未插/无主站,此时 AL 状态机根本没人驱动
|
||||
* (AL 迁移由主站写 AL Control 0x0120 触发,从站不自跳),停在 INIT 是唯一正确行为。
|
||||
* 参考:接主站时实测 DL_STATUS=0x5611(Port0 有 link 有 comm);
|
||||
* 参考:接主站时 DL_STATUS 通常为 0x5611(Port0 有 link 有 comm);
|
||||
* 空载时为 0x5503(三口 loop closed、link/comm 全 0)。 */
|
||||
const bool anyPhyLink = chipIdOk && ((dlStatus & 0x0070U) != 0U);
|
||||
|
||||
@@ -137,8 +131,7 @@ void EthercatSlave::init()
|
||||
LOG_ERR("[ECAT][SLAVE] ESC NOT ready, sync ISR disabled (check EXMC wiring/timing)\r\n");
|
||||
} else if (s_alState < AL_STATE_SAFEOP) {
|
||||
/* 区分"等主站"与"真故障":两者 AL 都 < SAFEOP,但只有后者需要排查。
|
||||
* 1.5.40 及之前一律打 FAIL(partial),而冷启动无主站时必然走到这里,
|
||||
* 与紧随其后的 "SSC MainInit OK" 自相矛盾,极易被误读为初始化失败。 */
|
||||
* 冷启动无主站时必然走到这里,报 WAIT_MASTER 而非 FAIL。 */
|
||||
if (!anyPhyLink) {
|
||||
LOG_ERR("[ECAT][SLAVE] WAIT_MASTER: no PHY link (DL_STATUS=0x%04X), "
|
||||
"AL=0x%02X(%s) 由主站驱动,空载停在 INIT 属预期\r\n",
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
/**
|
||||
* @file ethercat_slave.h
|
||||
* @brief EtherCAT 从站 init 探针与运行状态查询接口。
|
||||
* @note 1.5.27:原先的 periodicIsr()/三缓冲 PDO 桥是死代码(无任何调用点),
|
||||
* 运行期 PDO 交换实际由 SSC 协议栈完成 —— 周期路径为 Sync0_Isr →
|
||||
* @note 运行期 PDO 交换由 SSC 协议栈完成 —— 周期路径为 Sync0_Isr →
|
||||
* ECAT_Application → APPL_InputMapping/OutputMapping(comm/ssc/port/dc_app.c),
|
||||
* 主循环路径为 MainLoop → ECAT_Main。本类仅保留 init 探针与诊断查询。
|
||||
*/
|
||||
|
||||
@@ -7,8 +7,10 @@
|
||||
* SYNC1 (EXTI2, preempt=1) → Sync1_Isr() : 预留
|
||||
*/
|
||||
#include "comm/ethercat_sync.h"
|
||||
#include "comm/esc_lan9252.h" /* pmSuspended:ESC 睡眠挂起期 ISR 拒碰 HBI */
|
||||
#include "sys/config.h"
|
||||
#include "main.h" /* g_app_uptime_sec */
|
||||
#include "sys/cpu_load.h"
|
||||
#include <cstdio>
|
||||
|
||||
extern "C" {
|
||||
@@ -113,6 +115,12 @@ void EthercatSync::onSync0Exti()
|
||||
/* 直接寄存器访问,绕过 GD32 库函数以消除可变抖动 */
|
||||
if ((EXTI_PD & (uint32_t)CONFIG_ESC_SYNC0_EXTI) != RESET) {
|
||||
EXTI_PD = (uint32_t)CONFIG_ESC_SYNC0_EXTI;
|
||||
/* ESC 睡眠挂起期,清了 pending 就走——Sync0_Isr 内的 PRAM/CSR
|
||||
* 访问全是 HBI,读即唤醒。睡眠中 SYNC0 引脚本应无沿,此分支只兜
|
||||
* 进入睡眠瞬间的残留 pending。 */
|
||||
if (EscLan9252::pmSuspended()) {
|
||||
return;
|
||||
}
|
||||
++s_sync0Count;
|
||||
Sync0_Isr();
|
||||
}
|
||||
@@ -122,6 +130,10 @@ void EthercatSync::onEscIrqExti()
|
||||
{
|
||||
if (exti_interrupt_flag_get(CONFIG_ESC_IRQ_EXTI) != RESET) {
|
||||
exti_interrupt_flag_clear(CONFIG_ESC_IRQ_EXTI);
|
||||
/* ESC 睡眠挂起期直接返回——PDI_Isr 会读 0x220/PRAM(HBI),读即唤醒 */
|
||||
if (EscLan9252::pmSuspended()) {
|
||||
return;
|
||||
}
|
||||
++s_escIrqCount;
|
||||
/* PDI_Isr(preempt=1) 可被 Sync0_Isr(preempt=0) 抢占;HBI Indexed 是两步
|
||||
* 事务(IDX 写 + DATA 读),被抢占即错址。此处屏蔽 EXTI0 实现互斥:
|
||||
@@ -140,6 +152,10 @@ void EthercatSync::onSync1Exti()
|
||||
{
|
||||
if (exti_interrupt_flag_get(CONFIG_ESC_SYNC1_EXTI) != RESET) {
|
||||
exti_interrupt_flag_clear(CONFIG_ESC_SYNC1_EXTI);
|
||||
/* ESC 睡眠挂起期直接返回——Sync1_Isr 内为 HBI 访问 */
|
||||
if (EscLan9252::pmSuspended()) {
|
||||
return;
|
||||
}
|
||||
++s_sync1Count;
|
||||
/* 同 onEscIrqExti:Sync1_Isr(preempt=1) 内的 HBI 访问需与 SYNC0 互斥 */
|
||||
const uint32_t prevSync0 = NVIC_GetEnableIRQ(CONFIG_ESC_SYNC0_IRQn);
|
||||
@@ -156,7 +172,10 @@ void EthercatSync::onTimerIrq()
|
||||
#if (CONFIG_ETHERCAT_SYNC_MODE == 1)
|
||||
if (timer_interrupt_flag_get(TIMER0, TIMER_INT_FLAG_UP) != RESET) {
|
||||
timer_interrupt_flag_clear(TIMER0, TIMER_INT_FLAG_UP);
|
||||
Sync0_Isr();
|
||||
/* ESC 睡眠挂起期跳过——Sync0_Isr 内为 HBI 访问 */
|
||||
if (!EscLan9252::pmSuspended()) {
|
||||
Sync0_Isr();
|
||||
}
|
||||
}
|
||||
#else
|
||||
/* EXTI 模式下 TIMER0 不使用,本函数为占位;CONFIG_ETHERCAT_SYNC_MODE=1 时激活。 */
|
||||
@@ -168,13 +187,23 @@ extern "C" {
|
||||
|
||||
#if (CONFIG_ETHERCAT_SYNC_MODE == 0)
|
||||
__attribute__((section(".ramfunc"), noinline))
|
||||
void EXTI0_IRQHandler(void) { EthercatSync::onSync0Exti(); }
|
||||
void EXTI1_IRQHandler(void) { EthercatSync::onEscIrqExti(); }
|
||||
void EXTI2_IRQHandler(void) { EthercatSync::onSync1Exti(); }
|
||||
void EXTI0_IRQHandler(void)
|
||||
{
|
||||
CpuLoad::isrEnter();
|
||||
EthercatSync::onSync0Exti();
|
||||
CpuLoad::isrExit();
|
||||
}
|
||||
void EXTI1_IRQHandler(void) { CpuLoad::isrEnter(); EthercatSync::onEscIrqExti(); CpuLoad::isrExit(); }
|
||||
void EXTI2_IRQHandler(void) { CpuLoad::isrEnter(); EthercatSync::onSync1Exti(); CpuLoad::isrExit(); }
|
||||
#else
|
||||
void TIMER0_UP_TIMER9_IRQHandler(void) { EthercatSync::onTimerIrq(); }
|
||||
void EXTI1_IRQHandler(void) { EthercatSync::onEscIrqExti(); }
|
||||
void EXTI2_IRQHandler(void) { EthercatSync::onSync1Exti(); }
|
||||
void TIMER0_UP_TIMER9_IRQHandler(void)
|
||||
{
|
||||
CpuLoad::isrEnter();
|
||||
EthercatSync::onTimerIrq();
|
||||
CpuLoad::isrExit();
|
||||
}
|
||||
void EXTI1_IRQHandler(void) { CpuLoad::isrEnter(); EthercatSync::onEscIrqExti(); CpuLoad::isrExit(); }
|
||||
void EXTI2_IRQHandler(void) { CpuLoad::isrEnter(); EthercatSync::onSync1Exti(); CpuLoad::isrExit(); }
|
||||
#endif
|
||||
|
||||
/* ---- SSC 诊断对象 0x2000 的 C 侧访问 (dc_app.c APPL_InputMapping 每周期拉取) ---- */
|
||||
@@ -182,5 +211,9 @@ uint32_t EcatDbg_GetSync0(void) { return EthercatSync::getSync0Count(); }
|
||||
uint32_t EcatDbg_GetSync1(void) { return EthercatSync::getSync1Count(); }
|
||||
uint32_t EcatDbg_GetEscIrq(void) { return EthercatSync::getEscIrqCount(); }
|
||||
uint32_t EcatDbg_GetUptimeSec(void){ return g_app_uptime_sec; }
|
||||
uint32_t EcatDbg_GetTick1ms(void) { return g_scheduler.tick_1ms; }
|
||||
|
||||
/* 剖分埋点:供 ecatappl.c (C) 读取 DWT 周期计数 */
|
||||
uint32_t EcatDbg_Dwt(void) { return DWT->CYCCNT; }
|
||||
|
||||
} /* extern "C" */
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
<!-- SM2: Process Data Output (RxPDO, Master -> Slave) -->
|
||||
<Sm DefaultSize="384" StartAddress="#x1400" ControlByte="#x64" Enable="1">Outputs</Sm>
|
||||
<!-- SM3: Process Data Input (TxPDO, Slave -> Master) -->
|
||||
<Sm DefaultSize="640" StartAddress="#x1880" ControlByte="#x20" Enable="1">Inputs</Sm>
|
||||
<Sm DefaultSize="600" StartAddress="#x1880" ControlByte="#x20" Enable="1">Inputs</Sm>
|
||||
<!-- RxPDO (Output, Master -> Slave): 索引 0x1600-0x17FF,映射 0x7000:01..48
|
||||
1.5.56 扩到 48 条目(384B):兼容单帧区 48B + CANFD 批量发送区 336B。
|
||||
与 dc_app.c 的 PDO_OUT_ENTRIES / gen_eeprom.py 的 PD_OUT_SIZE 必须一致,
|
||||
@@ -370,10 +370,13 @@
|
||||
<DataType>ULINT</DataType>
|
||||
</Entry>
|
||||
</RxPdo>
|
||||
<!-- TxPDO (Input, Slave -> Master): 索引 0x1A00-0x1BFF,映射 0x6000:01..80
|
||||
<!-- TxPDO (Input, Slave -> Master): 索引 0x1A00-0x1BFF,映射 0x6000:01..75
|
||||
1.5.56 扩到 80 条目(640B):电机状态窗口 + CANFD 批量接收区 280B。
|
||||
SM3 起址随之挪到 #x1880 —— SM2 是 3-buffer 模式,384B×3 占到 #x187F。
|
||||
(384+640)×3 = 3072 = 过程数据区 0x1400-0x1FFF 全长,恰好用满。
|
||||
1.6.1 输入瘦身 640→496: 批量接收条目数据 64→32B, 条目 80→62。
|
||||
1.6.15 496→600: 批量接收改定长覆盖窗口 5 槽(每槽 12B 头含 u32 seqlock
|
||||
+ 32B 数据), 条目 62→75, SM3 实占 3×600=1800B (0x1880-0x1F67),
|
||||
out+in=984 <= 1024 硬上限。推导见 comm/ethercat_pdo_size.h。
|
||||
撞址会被 SSC ecatslv.c 重叠检查拦下,报 INVALIDSMOUTCFG。 -->
|
||||
<TxPdo Fixed="1" Mandatory="1" Sm="3">
|
||||
<Index>#x1A00</Index>
|
||||
@@ -903,41 +906,6 @@
|
||||
<Name>Status Data 75</Name>
|
||||
<DataType>ULINT</DataType>
|
||||
</Entry>
|
||||
<Entry>
|
||||
<Index>#x6000</Index>
|
||||
<SubIndex>76</SubIndex>
|
||||
<BitLen>64</BitLen>
|
||||
<Name>Status Data 76</Name>
|
||||
<DataType>ULINT</DataType>
|
||||
</Entry>
|
||||
<Entry>
|
||||
<Index>#x6000</Index>
|
||||
<SubIndex>77</SubIndex>
|
||||
<BitLen>64</BitLen>
|
||||
<Name>Status Data 77</Name>
|
||||
<DataType>ULINT</DataType>
|
||||
</Entry>
|
||||
<Entry>
|
||||
<Index>#x6000</Index>
|
||||
<SubIndex>78</SubIndex>
|
||||
<BitLen>64</BitLen>
|
||||
<Name>Status Data 78</Name>
|
||||
<DataType>ULINT</DataType>
|
||||
</Entry>
|
||||
<Entry>
|
||||
<Index>#x6000</Index>
|
||||
<SubIndex>79</SubIndex>
|
||||
<BitLen>64</BitLen>
|
||||
<Name>Status Data 79</Name>
|
||||
<DataType>ULINT</DataType>
|
||||
</Entry>
|
||||
<Entry>
|
||||
<Index>#x6000</Index>
|
||||
<SubIndex>80</SubIndex>
|
||||
<BitLen>64</BitLen>
|
||||
<Name>Status Data 80</Name>
|
||||
<DataType>ULINT</DataType>
|
||||
</Entry>
|
||||
</TxPdo>
|
||||
<Mailbox DataLinkLayer="true">
|
||||
<CoE SdoInfo="true" CompleteAccess="true" SegmentedSdo="true"/>
|
||||
|
||||
Binary file not shown.
@@ -11,10 +11,7 @@
|
||||
* 0x6000 Input Data (从站→主站, 384 bytes)
|
||||
* 0x2000 SYNC Diag (只读: sync0/sync1/escirq/uptime, 主站 SDO 轮询看 DC 通路)
|
||||
*
|
||||
* 1.5.55:为电机控制台把过程数据从 32B/32B 扩到 64B/384B。
|
||||
* 0x7000/0x6000/0x1600/0x1A00 由 OBJCODE_REC 改为 OBJCODE_ARR —— 关键原因见
|
||||
* 下方 Entry Descriptions 注释:REC 会逐下标索引 pEntryDesc[Subindex],48 个
|
||||
* 条目就得写 49 项描述符;ARR 由 OBJ_GetEntryDesc 复用 pEntryDesc[1],只需 2 项。
|
||||
* 0x7000/0x6000/0x1600/0x1A00 为 OBJCODE_ARR(原因见下方 Entry Descriptions 注释)。
|
||||
*/
|
||||
|
||||
#include "ecat_def.h"
|
||||
@@ -24,10 +21,15 @@
|
||||
#include "objdef.h"
|
||||
#include "dc_app.h"
|
||||
#include "../../ethercat_pdo_size.h" /* ECAT_PDO_OUT_BYTES / ECAT_PDO_IN_BYTES */
|
||||
#include "../../ecat_motor_ota.h" /* 0x2510/0x2511:电机 OTA 邮箱进料的尺寸、会话码与钩子 */
|
||||
|
||||
#include <string.h>
|
||||
#include <stddef.h> /* offsetof:下方布局守卫断言使用 */
|
||||
#include <stdio.h> /* printf:0x204 守护 WARN 日志(1.5.48) */
|
||||
#include <stdio.h> /* printf:0x204 守护 WARN 日志 */
|
||||
|
||||
/* CPU 负载挂钩(sys/cpu_load.cpp,与 /api/app/load 同口径) */
|
||||
extern void CpuLoad_Enter(void);
|
||||
extern void CpuLoad_Leave(void);
|
||||
|
||||
/*---------------------------------------------------------------------------
|
||||
* el9800appl.h 桩 include (SSC 内部需要)
|
||||
@@ -39,14 +41,13 @@
|
||||
/*---------------------------------------------------------------------------
|
||||
* PDO 数据缓冲
|
||||
*
|
||||
* 1.5.56 扩到 DPRAM 硬上限:输出 48×64bit=384B,输入 80×64bit=640B(非对称)。
|
||||
* 长度的唯一真源是 comm/ethercat_pdo_size.h;本文件用 _Static_assert 与之绑定,
|
||||
* 改一处漏一处会编译失败。为何硬上限是 out+in≤1024B 见该头文件的推导。
|
||||
*---------------------------------------------------------------------------*/
|
||||
|
||||
/** 输出/输入 PDO 的 64bit 条目数。 */
|
||||
#define PDO_OUT_ENTRIES 48u /* 48×8 = 384 B */
|
||||
#define PDO_IN_ENTRIES 80u /* 80×8 = 640 B */
|
||||
#define PDO_IN_ENTRIES 75u /* 75×8 = 600 B (覆盖窗口) */
|
||||
|
||||
/** 输出数据对象 0x7000 (主站→从站, RxPDO) */
|
||||
typedef struct OBJ_STRUCT_PACKED_START {
|
||||
@@ -65,13 +66,13 @@ TOBJ6000;
|
||||
static TOBJ7000 s_outputObj = {PDO_OUT_ENTRIES, {0}};
|
||||
static TOBJ6000 s_inputObj = {PDO_IN_ENTRIES, {0}};
|
||||
|
||||
/* 布局守卫(1.5.42):SSC 的 SDO 单条目访问按"si0 占 16bit,其后按 BitLength 紧排"
|
||||
/* 布局守卫:SSC 的 SDO 单条目访问按"si0 占 16bit,其后按 BitLength 紧排"
|
||||
* 计算偏移(OBJ_GetEntryOffset),故 UINT64 数组必须紧跟在 UINT16 之后落在字节 2。
|
||||
* OBJ_STRUCT_PACKED_START 曾被留空导致其落在字节 8、SDO 读写错位,加断言防回归。 */
|
||||
_Static_assert(offsetof(TOBJ7000, u64Data) == 2, "TOBJ7000 未紧排:检查 OBJ_STRUCT_PACKED_START");
|
||||
_Static_assert(offsetof(TOBJ6000, u64Data) == 2, "TOBJ6000 未紧排:检查 OBJ_STRUCT_PACKED_START");
|
||||
_Static_assert(sizeof(TOBJ7000) == 2 + 8 * PDO_OUT_ENTRIES, "TOBJ7000 尺寸应为 2+384");
|
||||
_Static_assert(sizeof(TOBJ6000) == 2 + 8 * PDO_IN_ENTRIES, "TOBJ6000 尺寸应为 2+640");
|
||||
_Static_assert(sizeof(TOBJ6000) == 2 + 8 * PDO_IN_ENTRIES, "TOBJ6000 尺寸应为 2+600");
|
||||
|
||||
/* 与 C++ 侧桥接层的长度约定绑定:任一处改了尺寸而忘了另一处,这里直接编译失败,
|
||||
* 而不是等到运行期 SAFEOP 报 AL 0x001A/0x0011 再回头查。 */
|
||||
@@ -94,10 +95,35 @@ typedef struct OBJ_STRUCT_PACKED_START {
|
||||
UINT32 u32Sync1; /* sub2 */
|
||||
UINT32 u32EscIrq; /* sub3 */
|
||||
UINT32 u32UptimeS; /* sub4 */
|
||||
/* 剖分埋点(sub5..12,DWT 周期/计数累加,主站 1s 差分) */
|
||||
UINT32 u32PdiCyc; /* sub5: PDI_Isr 全函数累计 */
|
||||
UINT32 u32OutCyc; /* sub6: PDO_OutputMapping(输出探针/全量读) */
|
||||
UINT32 u32FreeRunCyc; /* sub7: MainLoop free-run 块(SM 同步下恒 0) */
|
||||
UINT32 u32PdiCnt; /* sub8: PDI_Isr 触发次数 */
|
||||
UINT32 u32InCyc; /* sub9: PDO_InputMapping 全程(APPL+输入PDO写) */
|
||||
UINT32 u32InWriteCyc; /* sub10: 其中 HW_EscWriteIsr 输入PDO 过程数据写 */
|
||||
UINT32 u32AlCyc; /* sub11: PDI_Isr 内 AL 事件读×2+超限应答 */
|
||||
UINT32 u32AppCyc; /* sub12: ECAT_Application(桥 processOutput) */
|
||||
/* [DIFF-STATS] 差分写取证仪表(sub13..23):上一个完整 1s 窗口的值,
|
||||
* 取自 comm/ethercat_diff_stats.cpp。CONFIG_ECAT_DIFF_STATS 置 0 时恒 0,
|
||||
* 字段保留(不影响任何既有子索引,也不动 PDO/映射,故无需重刷 SII)。 */
|
||||
UINT32 u32DsWinSeq; /* sub13: 已完成窗口数,0=还没有完整窗口(勿把 0 当"无变化") */
|
||||
UINT32 u32DsCycles; /* sub14: 窗口内参与比对的拍数 */
|
||||
UINT32 u32DsChangedMax; /* sub15: 单拍变化字节峰值(差分写的理论上限=600×拍数) */
|
||||
UINT32 u32DsRunsTotal; /* sub16: 窗口内 run(连续变化段)总数,配 sub14 求均值 */
|
||||
UINT32 u32DsRunsMax; /* sub17: 单拍 run 数峰值 —— 块粒度差分的碎片上限由它定 */
|
||||
UINT32 u32DsSeg1; /* sub18: 段1(0..31) 桥状态+短快照 变化字节合计 */
|
||||
UINT32 u32DsSeg2; /* sub19: 段2(32..543) 电机整轮密排 */
|
||||
UINT32 u32DsSeg3; /* sub20: 段3(544..583) 长帧快照 */
|
||||
UINT32 u32DsSeg4; /* sub21: 已删(恒 0) */
|
||||
UINT32 u32DsSeg5; /* sub22: 段5(584..599) 诊断区+保留 */
|
||||
UINT32 u32DsCompareCyc; /* sub23: 比对自身累计 DWT 周期(u32 粒度,不含快照拷贝) */
|
||||
UINT32 u32OpFreshRescues; /* sub24: OP 输入新鲜度救援次数(主站网页看,不打串口) */
|
||||
} OBJ_STRUCT_PACKED_END
|
||||
TOBJ2000;
|
||||
|
||||
static TOBJ2000 s_syncDiag = {4, 0, 0, 0, 0};
|
||||
static TOBJ2000 s_syncDiag = {24, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
|
||||
|
||||
/* 同上:UINT32 首条目必须落在字节 2(未紧排时为 4)。 */
|
||||
_Static_assert(offsetof(TOBJ2000, u32Sync0) == 2, "TOBJ2000 未紧排:检查 OBJ_STRUCT_PACKED_START");
|
||||
@@ -107,14 +133,45 @@ extern UINT32 EcatDbg_GetSync0(void);
|
||||
extern UINT32 EcatDbg_GetSync1(void);
|
||||
extern UINT32 EcatDbg_GetEscIrq(void);
|
||||
extern UINT32 EcatDbg_GetUptimeSec(void);
|
||||
/* 剖分埋点:ecatappl.c 累加器 + ethercat_sync.cpp 的 DWT 读 */
|
||||
extern UINT32 EcatDbg_Dwt(void);
|
||||
extern volatile UINT32 g_dbg_pdiCyc;
|
||||
extern volatile UINT32 g_dbg_outCyc;
|
||||
extern volatile UINT32 g_dbg_freerunCyc;
|
||||
extern volatile UINT32 g_dbg_pdiCnt;
|
||||
extern volatile UINT32 g_dbg_inCyc;
|
||||
extern volatile UINT32 g_dbg_inWriteCyc;
|
||||
extern volatile UINT32 g_dbg_alCyc;
|
||||
extern volatile UINT32 g_dbg_appCyc;
|
||||
|
||||
/* 过程数据收发模块 (comm/ethercat_pdo.cpp 的 extern "C" shim, 1.5.54)。
|
||||
* 1.5.53 时这里直接调 EcatCan_* 转发桥;1.5.54 把"一个 EtherCAT 周期怎么收发"
|
||||
* 收拢进 EthercatPdo 类,本文件退回纯 SSC 移植层:只留对象字典 + 三行转发。 */
|
||||
/* [DIFF-STATS] 差分写统计窗口值(comm/ethercat_diff_stats.cpp),out[0..10] 对应 sub13..23 */
|
||||
void EcatDiffStats_GetWindow(UINT32 *out);
|
||||
|
||||
/* OP 输入新鲜度 — APPL_InputMapping 每次成功刷新就自增 */
|
||||
static volatile UINT32 s_inMapSeq = 0U;
|
||||
static UINT32 s_inMapSeqSeen = 0U;
|
||||
static UINT32 s_inStaleBaseCyc = 0U;
|
||||
static UINT32 s_opFreshRescues = 0U;
|
||||
#include <stdint.h>
|
||||
/* DFP 的 SystemCoreClock 是 uint32_t(=unsigned int);SSC 的 UINT32 是 unsigned long,
|
||||
* 不能用 UINT32 重复声明。 */
|
||||
extern uint32_t SystemCoreClock;
|
||||
|
||||
/* 过程数据收发模块 (comm/ethercat_pdo.cpp 的 extern "C" shim)。
|
||||
* 本文件为纯 SSC 移植层:只留对象字典 + 三行转发。 */
|
||||
extern void EcatPdo_ReadOutputs(const UINT8 *esc);
|
||||
extern void EcatPdo_Process(void);
|
||||
extern uint32_t EcatPdo_ProcessSeq(void);
|
||||
extern void EcatPdo_WriteInputs(UINT8 *esc);
|
||||
|
||||
/* 0x2502 用:进 BOOT 的异步请求(sys/ota_boot_request.cpp 的 extern "C" shim)。
|
||||
* 只置 pending 标志,真正的配置页写入 + 复位在主循环下一圈,见 ObjBootReqWrite 注。 */
|
||||
extern void EcatEnterBootRequestAsync(void);
|
||||
|
||||
/* 0x2503 用:ESP32 EN(PC9) 控制(sys/esp32_en.cpp 的 extern "C" shim)。
|
||||
* 经 EtherCAT SDO 下发,**不走 ESP32 WiFi**——WiFi 关断后主站仍能拉回使能。 */
|
||||
extern UINT8 EcatEsp32EnDrive(UINT8 action);
|
||||
|
||||
/*---------------------------------------------------------------------------
|
||||
* PDO Assign 对象 (0x1C12 / 0x1C13)
|
||||
*---------------------------------------------------------------------------*/
|
||||
@@ -161,12 +218,12 @@ static TOBJ_RXPDO_MAPPING sRxPDOMap = {PDO_OUT_ENTRIES, {
|
||||
PDO_MAP_8(0x7000, 24), PDO_MAP_8(0x7000, 32), PDO_MAP_8(0x7000, 40)
|
||||
}};
|
||||
|
||||
/* 0x1A00: 映射 0x6000:01..80, 每个 64 bits */
|
||||
/* 0x1A00: 映射 0x6000:01..75, 每个 64 bits(9×8 + 3) */
|
||||
static TOBJ_TXPDO_MAPPING sTxPDOMap = {PDO_IN_ENTRIES, {
|
||||
PDO_MAP_8(0x6000, 0), PDO_MAP_8(0x6000, 8), PDO_MAP_8(0x6000, 16),
|
||||
PDO_MAP_8(0x6000, 24), PDO_MAP_8(0x6000, 32), PDO_MAP_8(0x6000, 40),
|
||||
PDO_MAP_8(0x6000, 48), PDO_MAP_8(0x6000, 56), PDO_MAP_8(0x6000, 64),
|
||||
PDO_MAP_8(0x6000, 72)
|
||||
PDO_MAP_ENTRY(0x6000, 73), PDO_MAP_ENTRY(0x6000, 74), PDO_MAP_ENTRY(0x6000, 75)
|
||||
}};
|
||||
|
||||
_Static_assert(sizeof(sRxPDOMap.aEntries) / 4 == PDO_OUT_ENTRIES, "0x1600 条目数不符");
|
||||
@@ -176,16 +233,11 @@ _Static_assert(sizeof(sTxPDOMap.aEntries) / 4 == PDO_IN_ENTRIES, "0x1A00 条目
|
||||
/*---------------------------------------------------------------------------
|
||||
* Entry Descriptions
|
||||
*
|
||||
* 重要(1.5.42 修正): OBJ_GetEntryDesc()(objdef.c:554) 只对 OBJCODE_ARR 复用
|
||||
* OBJ_GetEntryDesc()(objdef.c:554) 只对 OBJCODE_ARR 复用
|
||||
* pEntryDesc[1] 描述所有子索引; 对 OBJCODE_REC 是逐下标索引 pEntryDesc[Subindex]。
|
||||
* 因此 REC 对象的描述数组必须给满(Sub0..SubN 共 N+1 项), 否则 SDO Info 越界读到
|
||||
* 相邻常量数组的垃圾, 主站 config_map 报 "Unsupported access to an object" /
|
||||
* "Attempt to read to a write only object"(实测 0x1600/0x1A00 的 sub0/3/4 报错
|
||||
* 而 sub1/2 侥幸通过, 正是越界特征)。
|
||||
*
|
||||
* 1.5.55: 输入 PDO 扩到 48 条目后, 继续用 REC 就得写 49 项描述符(且 0x1600/0x1A00
|
||||
* 与 0x7000/0x6000 条目数不同, 不能再共用一个数组)。这些条目类型完全一致, 正是
|
||||
* ARR 的语义, 故统一改为 OBJCODE_ARR —— 描述数组固定 2 项, 由 SSC 复用 [1]。
|
||||
* 相邻常量数组的垃圾。
|
||||
* 条目类型完全一致时用 OBJCODE_ARR —— 描述数组固定 2 项, 由 SSC 复用 [1]。
|
||||
* 注意: 可被 PDO 映射的数据子条目必须带 OBJACCESS_RxPDOMAPPING/TxPDOMAPPING,
|
||||
* 否则主站校验 0x1600/0x1A00 的映射目标时会拒绝(官方 sampleappl.h 同此)。
|
||||
*---------------------------------------------------------------------------*/
|
||||
@@ -215,15 +267,266 @@ OBJCONST TSDOINFOENTRYDESC OBJMEM asInputEntryDesc[] = {
|
||||
{DEFTYPE_UNSIGNED64, 0x40, ACCESS_READ | OBJACCESS_TXPDOMAPPING}
|
||||
};
|
||||
|
||||
/* SYNC 诊断 0x2000: REC, Sub0..Sub4 共 5 项, 纯只读不参与 PDO 映射。 */
|
||||
/* SYNC 诊断 0x2000: REC, sub0..sub24 共 25 项, 纯只读不参与 PDO 映射,
|
||||
* 改子索引数无需重刷 SII/EEPROM。sub5..12 为剖分埋点(DWT 周期/计数累加);sub13..23 为 [DIFF-STATS] 差分写取证仪表;
|
||||
* sub24 = OP 输入新鲜度救援次数(不打串口,网页 /api/sync_diag 读)。
|
||||
* 行数必须与 TOBJ2000 字段一一对应:少一行不会编译报错,只会让 SDO 从错误的
|
||||
* 偏移取值 —— 故逐行标号便于核对。 */
|
||||
OBJCONST TSDOINFOENTRYDESC OBJMEM asSyncDiagEntryDesc[] = {
|
||||
{DEFTYPE_UNSIGNED8, 0x08, ACCESS_READ},
|
||||
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ},
|
||||
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ},
|
||||
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ},
|
||||
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}
|
||||
{DEFTYPE_UNSIGNED8, 0x08, ACCESS_READ}, /* sub0 : 子索引个数 = 24 */
|
||||
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* sub1 : SYNC0 计数 */
|
||||
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* sub2 : SYNC1 计数 */
|
||||
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* sub3 : ESC IRQ 计数 */
|
||||
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* sub4 : uptime 秒 */
|
||||
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* sub5 : PDI_Isr 累计周期 */
|
||||
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* sub6 : PDO_OutputMapping */
|
||||
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* sub7 : MainLoop free-run 块 */
|
||||
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* sub8 : PDI_Isr 次数 */
|
||||
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* sub9 : PDO_InputMapping 全程 */
|
||||
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* sub10 : 其中输入 PDO 过程数据写 */
|
||||
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* sub11 : AL 事件读×2+超限应答 */
|
||||
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* sub12 : ECAT_Application */
|
||||
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* sub13 : [DIFF-STATS] 已完成窗口数 */
|
||||
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* sub14 : [DIFF-STATS] 窗口内拍数 */
|
||||
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* sub15 : [DIFF-STATS] 单拍变化字节峰值 */
|
||||
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* sub16 : [DIFF-STATS] run 总数 */
|
||||
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* sub17 : [DIFF-STATS] 单拍 run 峰值 */
|
||||
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* sub18 : [DIFF-STATS] 段1 变化字节 */
|
||||
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* sub19 : [DIFF-STATS] 段2 变化字节 */
|
||||
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* sub20 : [DIFF-STATS] 段3 变化字节 */
|
||||
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* sub21 : [DIFF-STATS] 段4 变化字节 */
|
||||
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* sub22 : [DIFF-STATS] 段5 变化字节 */
|
||||
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* sub23 : [DIFF-STATS] 比对自身 DWT 周期 */
|
||||
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* sub24 : OP 输入新鲜度救援次数 */
|
||||
};
|
||||
|
||||
/*---------------------------------------------------------------------------
|
||||
* 0x2502 引导程序交接请求(主站 → APP:「让你回 BOOT」)
|
||||
*
|
||||
* 为什么要有它:EtherCAT 直灌的会话对象在 **BOOT** 里(0x2500/0x2501),APP 的字典
|
||||
* 根本没有这两个对象 —— 实测对着 APP 写 0x2500 回 abort=0x06020000「对象不存在」。
|
||||
* 所以一台正在跑 APP 的机器必须先交接,才能走总线刷写。量产那条交接只有
|
||||
* SPI 0xB0 和 CAN 0x320+"OTA!",没有总线内的入口;常驻刷写必须补上。
|
||||
*
|
||||
* 语义:写 1 = 请求进 BOOT;写 0 是无操作(不做"取消",避免半路误清)。
|
||||
* 动作复用现成的 OtaBootRequest 异步链:这里**只置 pending 标志**,配置页三字的
|
||||
* Flash 写入 + NVIC_SystemReset 由主循环 OtaBootRequest::process() 下一圈执行
|
||||
* (sys/initial.cpp 每圈调;与 SPI 0xB0 在 ISR 里只置标志同一个形状),
|
||||
* 所以 SDO 响应先发出去、复位后到,不会把 SM1 半包留在总线上。
|
||||
*
|
||||
* 交接后板子会稳定停在 BOOT:APP 写的是 EN=NOT_AVAL,而 BOOT 的救砖门
|
||||
* ecatArmed() = !(CHECK==AVAL && EN==AVAL) ⇒ 顺手就把 0x2500 直灌解锁了,
|
||||
* 不需要 word8 令牌(也别依赖它:APP 的 otaConfigSet 只保全 8 个字,一擦页就没了)。
|
||||
*---------------------------------------------------------------------------*/
|
||||
OBJCONST TSDOINFOENTRYDESC OBJMEM sEntryDesc0x2502[] = {
|
||||
/* VAR 只有 :00 一个条目,且必须带写权限位(SSC 的通用写权限门在 objdef.c:1325,
|
||||
* 早于 1352 的回调分发 —— BOOT 的 0x2500 就是栽在只给 ACCESS_READ 上) */
|
||||
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READWRITE},
|
||||
};
|
||||
static OBJCONST UCHAR aName0x2502[] = "GD32 boot request";
|
||||
|
||||
static UINT32 s_bootReq = 0U; /**< 最近一次写入值,读回用于主站自证 */
|
||||
|
||||
static UINT8 ObjBootReqWrite(UINT16 index, UINT8 subindex, UINT32 dataSize,
|
||||
UINT16 MBXMEM *pData, UINT8 bCompleteAccess)
|
||||
{
|
||||
const UINT8 *p8 = (const UINT8 *)pData;
|
||||
UINT32 v;
|
||||
|
||||
(void)index;
|
||||
(void)bCompleteAccess;
|
||||
|
||||
if (subindex != 0U) {
|
||||
return ABORTIDX_SUBINDEX_NOT_EXISTING;
|
||||
}
|
||||
if (pData == NULL || dataSize < 4U) {
|
||||
return ABORTIDX_PARAM_LENGTH_TOO_SHORT;
|
||||
}
|
||||
v = (UINT32)p8[0] | ((UINT32)p8[1] << 8) | ((UINT32)p8[2] << 16) | ((UINT32)p8[3] << 24);
|
||||
s_bootReq = v;
|
||||
|
||||
if (v == 1U) {
|
||||
EcatEnterBootRequestAsync();
|
||||
}
|
||||
return 0U;
|
||||
}
|
||||
|
||||
/*---------------------------------------------------------------------------
|
||||
* 0x2503 ESP32 WiFi EN(PC9 / WLESS_EN)控制(主站 → APP)
|
||||
*
|
||||
* 为什么走 EtherCAT 而不是 SPI 0xA2 / WiFi HTTP:WiFi 使能被拉低后 ESP32
|
||||
* 当场掉线,经 /spi/send 的控制路径跟被控对象同生共死——关得掉、开不回来。
|
||||
* EtherCAT 主站在 PC 侧,与 ESP32 完全解耦,关断/开启都能下发。
|
||||
*
|
||||
* 语义(UINT32 bit[7:0]=action,与 sys/esp32_en.h Esp32En::drive 对齐):
|
||||
* 0=拉高(开) 1=拉低保持(关) 2=拉高 3=浮空 4=只读
|
||||
* 读回:bit[7:0] = 实际引脚电平 0/1(0xEE=无效 action)。
|
||||
*---------------------------------------------------------------------------*/
|
||||
OBJCONST TSDOINFOENTRYDESC OBJMEM sEntryDesc0x2503[] = {
|
||||
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READWRITE},
|
||||
};
|
||||
static OBJCONST UCHAR aName0x2503[] = "ESP32 WiFi EN";
|
||||
|
||||
static UINT32 s_esp32EnVal = 1U; /**< 读回缓存:默认 1=上电已释放 EN */
|
||||
|
||||
static UINT8 ObjEsp32EnWrite(UINT16 index, UINT8 subindex, UINT32 dataSize,
|
||||
UINT16 MBXMEM *pData, UINT8 bCompleteAccess)
|
||||
{
|
||||
const UINT8 *p8 = (const UINT8 *)pData;
|
||||
UINT32 v;
|
||||
UINT8 action, st;
|
||||
|
||||
(void)index;
|
||||
(void)bCompleteAccess;
|
||||
|
||||
if (subindex != 0U) {
|
||||
return ABORTIDX_SUBINDEX_NOT_EXISTING;
|
||||
}
|
||||
if (pData == NULL || dataSize < 4U) {
|
||||
return ABORTIDX_PARAM_LENGTH_TOO_SHORT;
|
||||
}
|
||||
v = (UINT32)p8[0] | ((UINT32)p8[1] << 8) | ((UINT32)p8[2] << 16) | ((UINT32)p8[3] << 24);
|
||||
action = (UINT8)(v & 0xFFU);
|
||||
st = EcatEsp32EnDrive(action);
|
||||
s_esp32EnVal = (UINT32)st;
|
||||
return 0U;
|
||||
}
|
||||
|
||||
/*---------------------------------------------------------------------------
|
||||
* 0x2510 / 0x2511 电机 OTA 邮箱进料通路(主站 → APP)
|
||||
*
|
||||
* 为什么要有它:电机 OTA 的引擎在 GD32(MotorOtaRelay:AES 握手 + CAN 停等),
|
||||
* 但固件明文只有一条进料路 —— ESP32 经 SPI 0xD1 喂。只插网线的场合那条 HTTP
|
||||
* 打不通,于是前端把电机 OTA 标成「本链路不支持」。这两个对象把进料口换成
|
||||
* EtherCAT 邮箱,引擎一行没动:数据窗的写回调最终调的还是 MotorOtaRelay::feedAt()。
|
||||
*
|
||||
* 形状完全照 BOOT 已验证的 0x2500/0x2501(comm/ecat/boot_ecat_app.cpp):
|
||||
* - REC 的条目统一 UINT32,偏移才和 SSC「si0 占 2 字节 + 按 BitLength 紧排」一致;
|
||||
* - 数据窗建成 VAR(不是 ARR):VAR 没有 sub0 计数字段,主站按 `:00` 整块写;
|
||||
* VAR 的 maxSubindex 恒为 0 ⇒ 写 `:1` 会直接 ABORTIDX_SUBINDEX_NOT_EXISTING;
|
||||
* complete access 对 VAR 一律 UNSUPPORTED ⇒ 主站必须传 CA=FALSE;
|
||||
* - 条目描述必须带写权限位,objdef.c:1325 的通用写权限门早于 1352 的回调分发,
|
||||
* 少了它回调一次都不会进(BOOT 的 0x2500 当初就这么静默躺平过)。
|
||||
* 业务逻辑(暂存、CRC、泵、终态锁存)在 comm/ecat_motor_ota.cpp,本层只做
|
||||
* SDO 字节 ↔ 纯整型参数的翻译。
|
||||
*---------------------------------------------------------------------------*/
|
||||
|
||||
typedef struct OBJ_STRUCT_PACKED_START {
|
||||
UINT16 bSubIndex; /**< sub0:条目数,SSC 按 UINT16 读 */
|
||||
UINT32 u32Cmd; /**< sub1:W 1=start 3=abort;R 最近一次命令 */
|
||||
UINT32 u32Total; /**< sub2:镜像总字节数 */
|
||||
UINT32 u32Channel; /**< sub3:目标 CAN 通道 0..2 */
|
||||
UINT32 u32Node; /**< sub4:目标节点 1..9 */
|
||||
UINT32 u32ImgCrc; /**< sub5:整镜像 CRC32(MPEG-2,与 SPI 链路同款) */
|
||||
UINT32 u32Staged; /**< sub6:已连续暂存字节数(只读) */
|
||||
UINT32 u32RelayState; /**< sub7:MotorOtaRelay 状态(只读) */
|
||||
UINT32 u32PkAcked; /**< sub8:已确认包数(只读) */
|
||||
UINT32 u32PkTotal; /**< sub9:总包数(只读) */
|
||||
UINT32 u32Err; /**< sub10:高字节=本模块会话码 0x8x,低字节=电机协议错误码 */
|
||||
UINT32 u32Phase; /**< sub11:位图,见 ecat_motor_ota.cpp */
|
||||
}
|
||||
OBJ_STRUCT_PACKED_END
|
||||
TOBJ2510;
|
||||
|
||||
_Static_assert(offsetof(TOBJ2510, u32Cmd) == 2, "TOBJ2510 未紧排:检查 OBJ_STRUCT_PACKED_START");
|
||||
_Static_assert(sizeof(TOBJ2510) == 2 + 4 * 11, "TOBJ2510 尺寸应为 2+44");
|
||||
|
||||
static TOBJ2510 s_motorOtaCtl = {ECAT_MOTA_CTL_SUBS, 0U, 0U, 0U, 0U, 0U,
|
||||
0U, 0U, 0U, 0U, 0U, 0U};
|
||||
|
||||
OBJCONST TSDOINFOENTRYDESC OBJMEM asMotorOtaCtlEntryDesc[] = {
|
||||
{DEFTYPE_UNSIGNED8, 0x08, ACCESS_READ}, /* sub0 : 子索引个数 = 11 */
|
||||
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READWRITE}, /* sub1 : cmd(必须带写位,否则回调进不来) */
|
||||
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READWRITE}, /* sub2 : 镜像总长 */
|
||||
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READWRITE}, /* sub3 : CAN 通道 */
|
||||
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READWRITE}, /* sub4 : 节点号 */
|
||||
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READWRITE}, /* sub5 : 整镜像 CRC32 */
|
||||
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* sub6 : 已暂存字节 */
|
||||
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* sub7 : relay 状态 */
|
||||
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* sub8 : 已 ACK 包数 */
|
||||
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* sub9 : 总包数 */
|
||||
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* sub10 : 会话码<<8 | 协议错误码 */
|
||||
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* sub11 : phase 位图 */
|
||||
};
|
||||
static OBJCONST UCHAR aName0x2510[] = "Motor OTA control";
|
||||
|
||||
OBJCONST TSDOINFOENTRYDESC OBJMEM asMotorOtaChunkEntryDesc[] = {
|
||||
/* 唯一条目 = 一整块 [offset:u32][crc32:u32][data...]。
|
||||
* 写回调在 objdef.c:1352 早分发,SSC 的默认拷贝路径不会执行。 */
|
||||
{DEFTYPE_OCTETSTRING, (UINT16)(ECAT_MOTA_CHUNK_OBJ_BYTES * 8U), ACCESS_READWRITE},
|
||||
};
|
||||
static OBJCONST UCHAR aName0x2511[] = "Motor firmware chunk";
|
||||
|
||||
/** 数据窗的落地缓冲:写路径不依赖它(回调自己取字节),保留是给默认读路径的落点。 */
|
||||
static UINT8 s_motorOtaChunk[ECAT_MOTA_CHUNK_OBJ_BYTES];
|
||||
|
||||
static UINT8 ObjMotorOtaCtlWrite(UINT16 index, UINT8 subindex, UINT32 dataSize,
|
||||
UINT16 MBXMEM *pData, UINT8 bCompleteAccess)
|
||||
{
|
||||
const UINT8 rc = EcatMotorOtaCtlWrite(subindex, dataSize, (const UINT8 *)pData);
|
||||
|
||||
(void)index;
|
||||
(void)bCompleteAccess;
|
||||
|
||||
if (subindex == 0U) {
|
||||
return ABORTIDX_SUBINDEX_NOT_EXISTING; /* sub0 是条目数,不可写 */
|
||||
}
|
||||
if (subindex >= 2U + ECAT_MOTA_CTL_SUBS) {
|
||||
return ABORTIDX_SUBINDEX_NOT_EXISTING;
|
||||
}
|
||||
if (rc != ECAT_MOTA_OK) {
|
||||
/* 具体原因不靠 abort 码分类:主站读 0x2510:10 的高字节拿真码。 */
|
||||
return ABORTIDX_DATA_CANNOT_BE_READ_OR_STORED;
|
||||
}
|
||||
return 0U;
|
||||
}
|
||||
|
||||
/** 0x2510 读取:逐 subindex 从 ecat_motor_ota 取**实时**值,不读 s_motorOtaCtl 的镜像。 */
|
||||
static UINT8 ObjMotorOtaCtlRead(UINT16 index, UINT8 subindex, UINT32 objSize,
|
||||
UINT16 MBXMEM *pData, UINT8 bCompleteAccess)
|
||||
{
|
||||
const UINT32 v = EcatMotorOtaCtlRead(subindex);
|
||||
UINT8 *p8 = (UINT8 *)pData;
|
||||
|
||||
(void)index;
|
||||
(void)objSize;
|
||||
(void)bCompleteAccess;
|
||||
|
||||
if (pData == NULL) {
|
||||
return ABORTIDX_PARAM_LENGTH_TOO_SHORT;
|
||||
}
|
||||
if (subindex == 0U) {
|
||||
/* sub0 是 UINT8(条目数),只占载荷一个字节 */
|
||||
p8[0] = (UINT8)v;
|
||||
} else {
|
||||
p8[0] = (UINT8)(v & 0xFFU);
|
||||
p8[1] = (UINT8)((v >> 8) & 0xFFU);
|
||||
p8[2] = (UINT8)((v >> 16) & 0xFFU);
|
||||
p8[3] = (UINT8)((v >> 24) & 0xFFU);
|
||||
}
|
||||
return 0U;
|
||||
}
|
||||
|
||||
static UINT8 ObjMotorOtaChunkWrite(UINT16 index, UINT8 subindex, UINT32 dataSize,
|
||||
UINT16 MBXMEM *pData, UINT8 bCompleteAccess)
|
||||
{
|
||||
const UINT8 rc = EcatMotorOtaChunkWrite(dataSize, (const UINT8 *)pData);
|
||||
|
||||
(void)index;
|
||||
(void)bCompleteAccess;
|
||||
|
||||
if (subindex != 0U) {
|
||||
return ABORTIDX_SUBINDEX_NOT_EXISTING; /* VAR 只有 :00 */
|
||||
}
|
||||
if (rc == ECAT_MOTA_E_GAP || rc == ECAT_MOTA_E_PARAM || rc == ECAT_MOTA_E_BADLEN) {
|
||||
return ABORTIDX_VALUE_EXCEEDED;
|
||||
}
|
||||
if (rc != ECAT_MOTA_OK) {
|
||||
return ABORTIDX_DATA_CANNOT_BE_READ_OR_STORED;
|
||||
}
|
||||
return 0U;
|
||||
}
|
||||
|
||||
/*---------------------------------------------------------------------------
|
||||
* Object Names
|
||||
*---------------------------------------------------------------------------*/
|
||||
@@ -266,9 +569,27 @@ TOBJECT OBJMEM ApplicationObjDicList[] = {
|
||||
asInputEntryDesc, aName0x6000, &s_inputObj, NULL, NULL, 0x0000},
|
||||
|
||||
/* 0x2000: SYNC 诊断 (只读, 主站 SDO 轮询验证 DC 通路) */
|
||||
{NULL, NULL, 0x2000, {DEFTYPE_RECORD, 4 | (OBJCODE_REC << 8)},
|
||||
{NULL, NULL, 0x2000, {DEFTYPE_RECORD, 24 | (OBJCODE_REC << 8)},
|
||||
asSyncDiagEntryDesc, aName0x2000, &s_syncDiag, NULL, NULL, 0x0000},
|
||||
|
||||
/* 0x2502: 引导程序交接请求(写 1 = 回 BOOT 等灌写;见 ObjBootReqWrite 注) */
|
||||
{NULL, NULL, 0x2502, {DEFTYPE_UNSIGNED32, 0 | (OBJCODE_VAR << 8)},
|
||||
sEntryDesc0x2502, aName0x2502, &s_bootReq, NULL, ObjBootReqWrite, 0x0000},
|
||||
|
||||
/* 0x2503: ESP32 WiFi EN(PC9) —— 经 EtherCAT 控制,WiFi 关断后仍可拉回 */
|
||||
{NULL, NULL, 0x2503, {DEFTYPE_UNSIGNED32, 0 | (OBJCODE_VAR << 8)},
|
||||
sEntryDesc0x2503, aName0x2503, &s_esp32EnVal, NULL, ObjEsp32EnWrite, 0x0000},
|
||||
|
||||
/* 0x2510: 电机 OTA 会话控制(写 sub2 声明总长 → 反复写 0x2511 灌块 → 写 sub1=1 启动) */
|
||||
{NULL, NULL, 0x2510, {DEFTYPE_RECORD, ECAT_MOTA_CTL_SUBS | (OBJCODE_REC << 8)},
|
||||
asMotorOtaCtlEntryDesc, aName0x2510, &s_motorOtaCtl,
|
||||
ObjMotorOtaCtlRead, ObjMotorOtaCtlWrite, 0x0000},
|
||||
|
||||
/* 0x2511: 电机固件数据窗(VAR OCTETSTRING,主站分段下载攒完才回调一次) */
|
||||
{NULL, NULL, 0x2511, {DEFTYPE_OCTETSTRING, 0 | (OBJCODE_VAR << 8)},
|
||||
asMotorOtaChunkEntryDesc, aName0x2511, &s_motorOtaChunk,
|
||||
NULL, ObjMotorOtaChunkWrite, 0x0000},
|
||||
|
||||
/* 终止条目 */
|
||||
{NULL, NULL, 0xFFFF, {0, 0}, NULL, NULL, NULL, NULL, NULL, 0x000}
|
||||
};
|
||||
@@ -280,7 +601,7 @@ TOBJECT OBJMEM * VARMEM ApplicationObjDic = ApplicationObjDicList;
|
||||
*---------------------------------------------------------------------------*/
|
||||
UINT16 APPL_GenerateMapping(UINT16 *pInputSize, UINT16 *pOutputSize)
|
||||
{
|
||||
*pInputSize = ECAT_PDO_IN_BYTES; /* 80×UINT64 = 640 bytes */
|
||||
*pInputSize = ECAT_PDO_IN_BYTES; /* 75×UINT64 = 600 bytes */
|
||||
*pOutputSize = ECAT_PDO_OUT_BYTES; /* 48×UINT64 = 384 bytes */
|
||||
return 0;
|
||||
}
|
||||
@@ -296,12 +617,12 @@ UINT16 APPL_StopMailboxHandler(void) { return 0; }
|
||||
*---------------------------------------------------------------------------*/
|
||||
UINT16 APPL_StartInputHandler(UINT16 *pIntMask)
|
||||
{
|
||||
/* 1.5.44:此处**不能**覆写 *pIntMask。
|
||||
/* 此处**不能**覆写 *pIntMask。
|
||||
*
|
||||
* pIntMask 指向 SSC 全局 u16ALEventMask,StartInputHandler() 已按同步模式算好:
|
||||
* free-run + 有输出 PDO 时它是 PROCESS_OUTPUT_EVENT(0x0400)(ecatslv.c:959/1090),
|
||||
* 即 SM2 收到输出这一事件才是 SM-Synchron 模式的同步节拍。
|
||||
* 旧代码写死 0x0800(PROCESS_INPUT_EVENT) 把 0x0400 顶掉了,而随后的
|
||||
* 若写死 0x0800(PROCESS_INPUT_EVENT) 会把 0x0400 顶掉,而随后的
|
||||
* SetALEventMask() 只做“或”,于是寄存器 0x204 永远缺 SM2 位:
|
||||
* 输出到达不再产生 IRQ,PDI_Isr 只能被主站“读输入”事件带起来,
|
||||
* 输入刷新因此永远落在主站读之后 —— SM3 是 3-buffer 缓冲模式,
|
||||
@@ -317,8 +638,17 @@ UINT16 APPL_StopInputHandler(void) { return 0; }
|
||||
/*---------------------------------------------------------------------------
|
||||
* Output Handler (SAFEOP→OP)
|
||||
*---------------------------------------------------------------------------*/
|
||||
UINT16 APPL_StartOutputHandler(void) { return 0; }
|
||||
UINT16 APPL_StopOutputHandler(void) { return 0; }
|
||||
UINT16 APPL_StartOutputHandler(void)
|
||||
{
|
||||
EcatCan_ResetSequenceState();
|
||||
return 0;
|
||||
}
|
||||
|
||||
UINT16 APPL_StopOutputHandler(void)
|
||||
{
|
||||
EcatCan_ResetSequenceState();
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*---------------------------------------------------------------------------
|
||||
* APPL_AckErrorInd
|
||||
@@ -340,12 +670,43 @@ void APPL_InputMapping(UINT16 *pInputData)
|
||||
s_syncDiag.u32Sync1 = EcatDbg_GetSync1();
|
||||
s_syncDiag.u32EscIrq = EcatDbg_GetEscIrq();
|
||||
s_syncDiag.u32UptimeS = EcatDbg_GetUptimeSec();
|
||||
/* 剖分埋点快照(sub5..12) */
|
||||
s_syncDiag.u32PdiCyc = g_dbg_pdiCyc;
|
||||
s_syncDiag.u32OutCyc = g_dbg_outCyc;
|
||||
s_syncDiag.u32FreeRunCyc = g_dbg_freerunCyc;
|
||||
s_syncDiag.u32PdiCnt = g_dbg_pdiCnt;
|
||||
s_syncDiag.u32InCyc = g_dbg_inCyc;
|
||||
s_syncDiag.u32InWriteCyc = g_dbg_inWriteCyc;
|
||||
s_syncDiag.u32AlCyc = g_dbg_alCyc;
|
||||
s_syncDiag.u32AppCyc = g_dbg_appCyc;
|
||||
s_syncDiag.u32OpFreshRescues = s_opFreshRescues;
|
||||
|
||||
/* 1.5.54:先让 EthercatPdo 把本周期的输入数据写进 0x6000 的对象存储,
|
||||
/* [DIFF-STATS] 差分写统计快照(sub13..23):一次取全 11 个窗口值,
|
||||
* 取的都是"上一个已完成的 1s 窗口",故 SDO 读不会拿到半累积值。 */
|
||||
{
|
||||
UINT32 ds[11];
|
||||
EcatDiffStats_GetWindow(ds);
|
||||
s_syncDiag.u32DsWinSeq = ds[0];
|
||||
s_syncDiag.u32DsCycles = ds[1];
|
||||
s_syncDiag.u32DsChangedMax = ds[2];
|
||||
s_syncDiag.u32DsRunsTotal = ds[3];
|
||||
s_syncDiag.u32DsRunsMax = ds[4];
|
||||
s_syncDiag.u32DsSeg1 = ds[5];
|
||||
s_syncDiag.u32DsSeg2 = ds[6];
|
||||
s_syncDiag.u32DsSeg3 = ds[7];
|
||||
s_syncDiag.u32DsSeg4 = ds[8];
|
||||
s_syncDiag.u32DsSeg5 = ds[9];
|
||||
s_syncDiag.u32DsCompareCyc = ds[10];
|
||||
}
|
||||
|
||||
/* 先让 EthercatPdo 把本周期的输入数据写进 0x6000 的对象存储,
|
||||
* 再交给 ESC。经由对象存储中转(而非直接写 pInputData)是为了让主站
|
||||
* SDO 读 0x6000 时看到与过程数据一致的内容。 */
|
||||
EcatPdo_WriteInputs((UINT8 *)&s_inputObj.u64Data[0]);
|
||||
memcpy(pInputData, &s_inputObj.u64Data[0], ECAT_PDO_IN_BYTES);
|
||||
/* OP 输入新鲜度不变量的活性计数。每次真正走到输入映射就自增,
|
||||
* 供 EcatDbg_OpInputFreshness 判断 PDI 路径是否还活着。 */
|
||||
++s_inMapSeq;
|
||||
}
|
||||
|
||||
/*---------------------------------------------------------------------------
|
||||
@@ -362,11 +723,9 @@ void APPL_OutputMapping(UINT16 *pOutputData)
|
||||
*---------------------------------------------------------------------------*/
|
||||
void APPL_Application(void)
|
||||
{
|
||||
/* 1.5.54:应用逻辑全部搬进 EthercatPdo::process()(当前实现是 PDO↔CAN 转发)。
|
||||
*
|
||||
* 原 Phase 2 的 32B 回环已被取代:回环的验证价值由输入 PDO 的 seq 回显
|
||||
* (ECAT_CAN_RX_OFF_ACK)承担——主站写 seq=N 后应在下一周期读回 ack=N,
|
||||
* 同样能证明 输出→应用→输入 整条过程数据链路通。 */
|
||||
/* 应用逻辑在 EthercatPdo::process()(当前实现是 PDO↔CAN 转发)。
|
||||
* 不 CpuLoad 包裹:SM/DC 下本函数在 PDI/Sync0 ISR 内已被 isrEnter/Exit
|
||||
* 计入;free-run 下若主循环每圈调用,再包会抬高 load_perm。 */
|
||||
EcatPdo_Process();
|
||||
}
|
||||
|
||||
@@ -374,35 +733,42 @@ void APPL_Application(void)
|
||||
* AL 诊断快照 — task_runner 5s 心跳用 (C 链接, C++ 侧 extern "C" 声明)
|
||||
* [19:16]=AL 状态(0x0130 低 4 位) [31:24]=AL 错误码(0x0130 高 8 位)
|
||||
* [15:0] =AL 事件寄存器(0x0220, bit0=主站 AL 控制请求待处理)
|
||||
*
|
||||
* ESC 寄存器经 PDI/CSR 读出即主机序(LAN9252 CSR 引擎按
|
||||
* 16-bit 主机总线给出寄存器"值";线上大端只存在于网络侧与 PRAM 原始帧数据),
|
||||
* 与 SSC 自身 GetSyncMan/mailbox 的裸读约定一致,勿加 SWAPWORD。
|
||||
*---------------------------------------------------------------------------*/
|
||||
UINT32 EcatDbg_ReadAlDiag(void)
|
||||
{
|
||||
UINT16 alStatus = 0U;
|
||||
HW_EscReadWord(alStatus, ESC_AL_STATUS_OFFSET);
|
||||
alStatus = SWAPWORD(alStatus);
|
||||
return ((UINT32)alStatus << 16) | (UINT32)SWAPWORD(HW_GetALEventRegister());
|
||||
return ((UINT32)alStatus << 16) | (UINT32)HW_GetALEventRegister();
|
||||
}
|
||||
|
||||
/*---------------------------------------------------------------------------
|
||||
* AL Event Mask (0x204) 守护 + SM2 事件楔死自愈 — 1.5.48 新增, 1.5.49 扩展
|
||||
* (C 链接, task_runner 1s 节流调用)
|
||||
*
|
||||
* 背景:实测"停止回 INIT 后重进 OP"概率性失败——主站 LRW WKC=3/3(输出
|
||||
* 确实写进 SM2),但从站 9s ESM 超时报 0x001B:bEcatFirstOutputsReceived 一直
|
||||
* 没被置位。该标志只在 PDI_Isr 收到 SM2 事件(AL Event 0x220 bit10)时置位。
|
||||
* 01:03 串口日志钉死现场:evt=0x0400 在 0x220 挂起 10s 无人处理(PDI_Isr 的
|
||||
* SM2 分支从未执行),escirq 2.6/s 证明 EXTI 只在 AL Control 事件时跳动——
|
||||
* 即 PDI_Isr 在跑但 SM2 分支被某个内部旗标挡住(bEscIntEnabled 门控)。
|
||||
* AL Event Mask (0x204) 守护 + SM2 事件楔死自愈
|
||||
* (C 链接, task_runner 250ms 节流调用)
|
||||
*
|
||||
* 守护(1)0x204 缺位自愈:从站 AL>=SAFEOP 且有输出 PDO 时,SSC 不变式是
|
||||
* "0x204 ⊇ u16ALEventMask"。读实际值比对,缺位则补写并打 WARN。
|
||||
* 先补期望值再比对——下行转换路径(ecatslv.c AL_ControlInd 尾部)同时清
|
||||
* 0x204 的 PDO 事件位、bEscIntEnabled、bEcatFirstOutputsReceived;若期望值
|
||||
* 本身缺 SM2 位,旧判定恒通过、缺位永远补不回。SM-Synchron+有输出 PDO 的
|
||||
* 不变式是 u16ALEventMask ⊇ PROCESS_OUTPUT_EVENT(ecatslv.c:959/1086)。
|
||||
*
|
||||
* 自愈(2)SM2 事件楔死(1.5.49):SAFEOP 下 0x220 bit10 挂起 且 标志未置位
|
||||
* 自愈(2)SM2 事件楔死:SAFEOP 下 0x220 bit10 挂起 且 标志未置位
|
||||
* = PDI_Isr 通路楔死。从主循环直接完成 PDI_Isr 该做的事:置位
|
||||
* bEcatFirstOutputsReceived + 读 SM2 首/末字应答事件(ecatappl.c:317-320 同款)。
|
||||
* 协议等价:bit10 挂起 = 主站已写过 SM2 = "收到输出"成立;AL_ControlRes 下一个
|
||||
* MainLoop 周期即完成 SAFEOP→OP。WARN 附带全部内部旗标(bEscInt/bDcSync/
|
||||
* 期望与实际 mask/看门狗状态),下次复现一行日志即可定位楔死路径。
|
||||
* 协议等价:bit10 挂起 = 主站已写过 SM2 = "收到输出"成立。
|
||||
* WARN 附带全部内部旗标(bEscInt/bDcSync/期望与实际 mask/看门狗状态),
|
||||
* 下次复现一行日志即可定位楔死路径。
|
||||
*
|
||||
* 自愈(3)OP 态 PDI_Isr 门控:AL==OP 且 bEscIntEnabled==FALSE 不可
|
||||
* 合法出现——门控只在下行转换尾部被清(ecatslv.c:1273),而下行必改 AL 状态;
|
||||
* 守护与 ESM 同在主循环上下文串行,看到"OP 且门关着"即重入路径漏了恢复段。
|
||||
* 此时 PDI_Isr 整体被门控(ecatappl.c:282),PDO_OutputMapping 停调、输入冻结。
|
||||
* 修法:重开门 + 应答挂起的 SM2 事件(掩码补位由守护(1)在同次调用完成),
|
||||
* 下一帧 LRW 的 SM2 中断即恢复整条过程数据链路。
|
||||
*---------------------------------------------------------------------------*/
|
||||
UINT32 EcatDbg_GuardAleventMask(void)
|
||||
{
|
||||
@@ -420,30 +786,17 @@ UINT32 EcatDbg_GuardAleventMask(void)
|
||||
return 0xFFFFU; /* 状态不适用:返回哨兵值,调用方不打印 */
|
||||
}
|
||||
|
||||
/* (0) 邮箱 SM 地址/长度失配自愈(2026-09-03 03:00 根治,1.5.52):
|
||||
*
|
||||
* 主站侧探针(楔死态)实锤的证据链:
|
||||
* - ESC/SSC 邮箱处理全程活着(注入合法 CoE 请求后 200ms 内 SM1 出响应);
|
||||
* - 但响应帧头出现在 SM1 start+8 而非 start(且逐会话累积:+4→+8,断电才清零);
|
||||
* - 网络侧(FPRD)读 SM0/SM1 寄存器 start 恒正确(0x1000/0x1200);
|
||||
* => SSC 的 u16EscAddrSendMbx(Init→PreOP 时经 GetSyncMan/CSR 间接读)拿到了错值,
|
||||
* 主站按协议从 0x1200 读 → 帧头永远错位 → SOEM 报 WkcError/MailboxError,
|
||||
* 且预清理重枚举(重跑 StartMailboxHandler)重读仍错 → 死锁,只有断电恢复。
|
||||
* 自愈:AL>=PREOP 时用寄存器真值(0x0800/0x0808)校验 SSC 缓存的 start/len,
|
||||
* 失配即写回 + WARN(旧值/新值一并打印,下次串口复现可确凿定位 CSR 读错还是
|
||||
* 变量污染——两种成因此写回均有效,写回后下一个邮箱事务即恢复)。
|
||||
* 变量仅在 Stop/StartMailboxHandler(INIT<->PREOP 迁移,主循环上下文)被写,
|
||||
* 守护同样在主循环——无并发风险。 */
|
||||
/* (0) 邮箱 SM 地址/长度失配兜底:
|
||||
* 邮箱几何构建期常量化(mailbox.c 直达 ecat_def.h:MBX_SM0/1_START/SIZE),
|
||||
* CSR 错值无进入缓存的路径;本守护为运行期内存踩踏的最后兜底:
|
||||
* AL>=PREOP 时用寄存器真值校验 SSC 缓存的 start/len,失配即写回 + WARN。
|
||||
* ESC 寄存器经 PDI/CSR 读出即主机序,勿加 SWAPWORD。 */
|
||||
if (al >= 2U && bMbxRunning) {
|
||||
UINT16 sm0Start = 0U, sm0Len = 0U, sm1Start = 0U, sm1Len = 0U;
|
||||
HW_EscReadWord(sm0Start, ESC_SYNCMAN_REG_OFFSET);
|
||||
HW_EscReadWord(sm0Len, ESC_SYNCMAN_REG_OFFSET + 2U);
|
||||
HW_EscReadWord(sm1Start, ESC_SYNCMAN_REG_OFFSET + SIZEOF_SM_REGISTER);
|
||||
HW_EscReadWord(sm1Len, ESC_SYNCMAN_REG_OFFSET + SIZEOF_SM_REGISTER + 2U);
|
||||
sm0Start = SWAPWORD(sm0Start);
|
||||
sm0Len = SWAPWORD(sm0Len);
|
||||
sm1Start = SWAPWORD(sm1Start);
|
||||
sm1Len = SWAPWORD(sm1Len);
|
||||
if (sm0Len >= 8U && sm1Len >= 8U
|
||||
&& (u16EscAddrReceiveMbx != sm0Start || u16EscAddrSendMbx != sm1Start
|
||||
|| u16ReceiveMbxSize != sm0Len || u16SendMbxSize != sm1Len)) {
|
||||
@@ -464,16 +817,20 @@ UINT32 EcatDbg_GuardAleventMask(void)
|
||||
return 0xFFFFU; /* 以下仅 SAFEOP+ 适用 */
|
||||
}
|
||||
|
||||
evt = SWAPWORD(HW_GetALEventRegister());
|
||||
/* PDI 寄存器读即主机序,勿加 SWAPWORD。 */
|
||||
evt = HW_GetALEventRegister();
|
||||
HW_EscReadWord(actual, ESC_AL_EVENTMASK_OFFSET);
|
||||
actual = SWAPWORD(actual);
|
||||
|
||||
/* (1) 0x204 缺位自愈 */
|
||||
/* (1) 0x204 缺位自愈:先补期望值,再比对补实际值 */
|
||||
if ((u16ALEventMask & PROCESS_OUTPUT_EVENT) == 0U) {
|
||||
u16ALEventMask |= PROCESS_OUTPUT_EVENT;
|
||||
printf("[ECAT][AEM] WARN: expected AL mask lacked SM2 bit -> 0x%04X\r\n",
|
||||
(unsigned)u16ALEventMask);
|
||||
}
|
||||
if ((UINT16)(actual & u16ALEventMask) != u16ALEventMask) {
|
||||
const UINT16 before = actual;
|
||||
HW_SetALEventMask(u16ALEventMask);
|
||||
HW_EscReadWord(actual, ESC_AL_EVENTMASK_OFFSET);
|
||||
actual = SWAPWORD(actual);
|
||||
printf("[ECAT][AEM] WARN: AL Event Mask lost bits, actual=0x%04X expect=0x%04X -> restored 0x%04X\r\n",
|
||||
(unsigned)before, (unsigned)u16ALEventMask, (unsigned)actual);
|
||||
}
|
||||
@@ -482,7 +839,6 @@ UINT32 EcatDbg_GuardAleventMask(void)
|
||||
* (OP 态下 LRW 每周期置位/清除 bit10 属正常,不能误报) */
|
||||
if (al == 4U /* SAFEOP */ && (evt & 0x0400U) != 0U && bEcatFirstOutputsReceived == FALSE) {
|
||||
HW_EscReadWord(wdState, ESC_PD_WD_STATE);
|
||||
wdState = SWAPWORD(wdState);
|
||||
bEcatFirstOutputsReceived = TRUE;
|
||||
HW_EscReadWord(dummy, nEscAddrOutputData);
|
||||
HW_EscReadWord(dummy, (UINT16)(nEscAddrOutputData + nPdOutputSize - 2U));
|
||||
@@ -494,5 +850,104 @@ UINT32 EcatDbg_GuardAleventMask(void)
|
||||
(unsigned)(bDcSyncActive ? 1U : 0U),
|
||||
(unsigned)u16ALEventMask, (unsigned)actual, (unsigned)wdState);
|
||||
}
|
||||
|
||||
/* (3) OP 态 PDI_Isr 门控自愈:OP 下 bEscIntEnabled==FALSE
|
||||
* 不可合法出现——门控只在下行转换尾部被清(ecatslv.c:1273),而下行必改
|
||||
* AL 状态;守护与 ESM 同在主循环上下文串行,看到"OP 且门关着"即 SAFEOP
|
||||
* 重入路径漏了恢复段。此时 PDI_Isr 整体被门控(ecatappl.c:282),
|
||||
* PDO_OutputMapping 停调、输入冻结。
|
||||
* 修法:重开门 + 应答挂起的 SM2 事件(ISR 非 OP 分支同款读首/末字);
|
||||
* 0x204 掩码补位已由上面(1)在同次调用完成,下一帧 LRW 即恢复。 */
|
||||
if (al == 8U /* OP */ && nPdOutputSize > 0U && bEscIntEnabled == FALSE) {
|
||||
HW_EscReadWord(wdState, ESC_PD_WD_STATE);
|
||||
bEscIntEnabled = TRUE;
|
||||
HW_EscReadWord(dummy, nEscAddrOutputData);
|
||||
HW_EscReadWord(dummy, (UINT16)(nEscAddrOutputData + nPdOutputSize - 2U));
|
||||
printf("[ECAT][AEM] WARN: OP with PDI_Isr gated, evt=0x%04X "
|
||||
"(bDcSync=%u mask_exp=0x%04X mask_act=0x%04X wd=0x%04X) "
|
||||
"-> gate reopened + event acked\r\n",
|
||||
(unsigned)evt,
|
||||
(unsigned)(bDcSyncActive ? 1U : 0U),
|
||||
(unsigned)u16ALEventMask, (unsigned)actual, (unsigned)wdState);
|
||||
}
|
||||
return actual;
|
||||
}
|
||||
|
||||
/*---------------------------------------------------------------------------
|
||||
* OP 输入新鲜度不变量
|
||||
*
|
||||
* SM-Synchron 稳态下 MainLoop 的 free-run 分支被
|
||||
* (!bEscIntEnabled || !bEcatFirstOutputsReceived) && !bDcSyncActive
|
||||
* 整段跳过,输入 PDO **只**由 PDI_Isr 的 PDO_InputMapping 刷新。
|
||||
* PDI 通路一停(门控/掩码/EXTI),ESC 仍答 WKC、AL 仍 OP,主站读到的是
|
||||
* 冻住的旧缓冲。
|
||||
*
|
||||
* 不变量:「OP ⇒ 输入必须在 T ms 内被重写」。
|
||||
* - 正常路径:Process 每拍自增 seq
|
||||
* - 异常路径:seq 停涨超过阈值 → 主循环强制 PDO_InputMapping
|
||||
* (屏中断防双写)+ 重开门/应答 SM2,主站侧 gseq/rx 立刻恢复前进
|
||||
* 不依赖主站重拉、不依赖人工复位。MainLoop 若本身死锁则本函数也停 ——
|
||||
* 那是另一类故障(心跳会停),不在本不变量覆盖范围。
|
||||
*---------------------------------------------------------------------------*/
|
||||
void EcatDbg_OpInputFreshness(void)
|
||||
{
|
||||
const UINT8 al = (UINT8)(nAlStatus & 0x0FU);
|
||||
const UINT32 now = EcatDbg_Dwt();
|
||||
/* 活性看 processSeq(Process 真填过输入),不能看 s_inMapSeq。
|
||||
* InputMapping 可在 Process 停摆时继续拷陈旧 s_in,seq 照涨 → 救援永不触发。 */
|
||||
const UINT32 seq = EcatPdo_ProcessSeq();
|
||||
|
||||
if (al != 8U || nPdInputSize == 0U) {
|
||||
s_inMapSeqSeen = seq;
|
||||
s_inStaleBaseCyc = now;
|
||||
return;
|
||||
}
|
||||
if (seq != s_inMapSeqSeen) {
|
||||
s_inMapSeqSeen = seq;
|
||||
s_inStaleBaseCyc = now;
|
||||
return;
|
||||
}
|
||||
|
||||
/* seq 未推进:等满阈值再介入,避开两拍之间的正常空隙。
|
||||
* 5ms @1ms 周期 = 连丢 ≥5 拍;@4ms 周期 = 连丢 1~2 拍,仍足够判死。 */
|
||||
{
|
||||
const UINT32 thresh = (SystemCoreClock / 1000U) * 5U;
|
||||
if ((UINT32)(now - s_inStaleBaseCyc) < thresh) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/* 强制刷新:先重开门/首输出标志(门关着时 PDI 永远不会回来),
|
||||
* 再在 EXMC 互斥窗口(仅屏蔽 EXTI0/1/2)内做一次输入映射,保证不关全局中断(Preempt 0 CAN RX 不受影响)。
|
||||
* 必须先 EcatPdo_Process() 再 PDO_InputMapping()。
|
||||
* Process 先填好 buildInput+fillDiagTail,InputMapping 再发布,救援才有内容可写。 */
|
||||
{
|
||||
UINT16 dummy = 0U;
|
||||
const UINT16 evt = HW_GetALEventRegister();
|
||||
if (!bEscIntEnabled) {
|
||||
bEscIntEnabled = TRUE;
|
||||
}
|
||||
if (!bEcatFirstOutputsReceived) {
|
||||
bEcatFirstOutputsReceived = TRUE;
|
||||
HW_EscReadWord(dummy, nEscAddrOutputData);
|
||||
HW_EscReadWord(dummy, (UINT16)(nEscAddrOutputData + nPdOutputSize - 2U));
|
||||
}
|
||||
/* 救援路径计入 busy(低频;与 ISR/任务派发同一口径)。
|
||||
* 不要把 MainLoop 整段包进 CpuLoad——见 task_runner / cpu_load.h。 */
|
||||
CpuLoad_Enter();
|
||||
const uint32_t prevMask = EscExmcBus_MaskEnter();
|
||||
EcatPdo_Process();
|
||||
PDO_InputMapping();
|
||||
EscExmcBus_MaskExit(prevMask);
|
||||
CpuLoad_Leave();
|
||||
++s_opFreshRescues;
|
||||
s_inMapSeqSeen = EcatPdo_ProcessSeq();
|
||||
s_inStaleBaseCyc = EcatDbg_Dwt();
|
||||
/* 不打串口:计数经 0x2000:24 / /api/sync_diag 上到网页,避免 5ms 刷屏。 */
|
||||
}
|
||||
}
|
||||
|
||||
UINT32 EcatDbg_OpFreshRescues(void)
|
||||
{
|
||||
return s_opFreshRescues;
|
||||
}
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
* @brief DCU EtherCAT 应用层接口声明
|
||||
*
|
||||
* 提供 SSC 所需的 APPL_ 回调函数声明。
|
||||
* Phase 1: 桩实现,仅保证编译通过。
|
||||
* Phase 3: 接入实际 PDO 映射(DCU 传感器输入 / 控制输出)。
|
||||
*/
|
||||
#ifndef COMM_SSC_PORT_DC_APP_H
|
||||
#define COMM_SSC_PORT_DC_APP_H
|
||||
@@ -12,6 +10,7 @@
|
||||
#include "ecat_def.h"
|
||||
#include "esc.h"
|
||||
#include "ecatslv.h"
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
@@ -32,6 +31,19 @@ void APPL_InputMapping(UINT16 *pInputData);
|
||||
void APPL_OutputMapping(UINT16 *pOutputData);
|
||||
void APPL_Application(void);
|
||||
|
||||
/* OP 输入新鲜度不变量(task_runner 每圈调用;内部廉价,仅冻结时强制重写) */
|
||||
void EcatDbg_OpInputFreshness(void);
|
||||
UINT32 EcatDbg_OpFreshRescues(void);
|
||||
|
||||
/* HBI/EXMC 事务互斥(仅屏蔽 EXTI0/1/2,不关全局中断) */
|
||||
uint32_t EscExmcBus_MaskEnter(void);
|
||||
void EscExmcBus_MaskExit(uint32_t prev);
|
||||
uint32_t HW_EscExmcMaskEnter(void);
|
||||
void HW_EscExmcMaskExit(uint32_t prev);
|
||||
|
||||
/* 复位输出 sequence 状态机 */
|
||||
void EcatCan_ResetSequenceState(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -144,22 +144,17 @@
|
||||
#endif
|
||||
|
||||
/** 看门狗由定时器中断检查。
|
||||
* 1.5.47:1→0。ECAT_TIMER_INT=1 要求端口提供调用 ECAT_CheckTimer() 的 1ms 硬件
|
||||
* 定时器 ISR——本工程从未实现,导致 ECAT_CheckTimer 在整个固件里零调用者:
|
||||
* (a) EsmTimeoutCounter 永不递减 → 任何 pending 状态迁移(SAFEOP→OP 等)永不
|
||||
* 超时,从站会无限期忽略一切 AL Control(实测"进 PREOP 失败: AL=SAFEOP
|
||||
* code=0x0000"的卡死就是它);
|
||||
* (b) DC_CheckWatchdog/PLL 计数器(i16WaitForPllRunningCnt)不前进 → 若 ESC
|
||||
* 残留 DC 激活位(0x0981,上个 DC 会话遗留、跨重连不清),从站按 DC 同步
|
||||
* 模式进 SAFEOP→OP,OP 等待 PLL 序列永远不满足 → SAFEOP+0x0000 永久挂起
|
||||
* (00:27 复测日志: 等待期轨迹恒 SAFEOP+0x0000,LRW 每 7ms 也救不回)。
|
||||
* 改 0 后 MainLoop 按 DWT CYCCNT 轮询(hw_gd32exmc.cpp 的 HW_GetTimerValue/
|
||||
* HW_ClearTimerValue 本就是为此写的死代码),每 1ms 调一次 ECAT_CheckTimer。 */
|
||||
* ECAT_TIMER_INT=0:MainLoop 按 DWT CYCCNT 轮询(hw_gd32exmc.cpp 的
|
||||
* HW_GetTimerValue/HW_ClearTimerValue),每 1ms 调一次 ECAT_CheckTimer。
|
||||
* 若置 1 则要求端口提供调用 ECAT_CheckTimer() 的 1ms 硬件定时器 ISR——
|
||||
* 本工程无该 ISR,ECAT_CheckTimer 零调用者会导致 EsmTimeoutCounter 永不递减
|
||||
* (状态迁移永不超时)及 DC PLL 计数器不前进(残留 DC 激活位时 SAFEOP+0x0000
|
||||
* 永久挂起)。 */
|
||||
#ifndef ECAT_TIMER_INT
|
||||
#define ECAT_TIMER_INT 0
|
||||
#endif
|
||||
|
||||
/** 1.5.47:DWT CYCCNT @180MHz 的每毫秒 tick 数(ECAT_TIMER_INT=0 时 MainLoop 用)。 */
|
||||
/** DWT CYCCNT @180MHz 的每毫秒 tick 数(ECAT_TIMER_INT=0 时 MainLoop 用)。 */
|
||||
#ifndef ECAT_TIMER_INC_P_MS
|
||||
#define ECAT_TIMER_INC_P_MS 180000U
|
||||
#endif
|
||||
@@ -535,13 +530,10 @@
|
||||
------
|
||||
------ Struct Packing
|
||||
------
|
||||
------ 1.5.42 修正:此前三对宏全部留空,注释误以为"AC6 不需要"。实际这些宏就是 SSC
|
||||
------ 用来紧排结构体的唯一手段——SSC 的 SDO 读写按"si0 占 16bit,其后各条目按
|
||||
------ BitLength 紧排"计算字节偏移(OBJ_GetEntryOffset, objdef.c:609),留空则结构体
|
||||
------ 按编译器自然对齐,与该假设不符。armclang 实测:{UINT16; UINT64[4]} 的 UINT64
|
||||
------ 落在字节 8 而非 2(编译期断言验证),{UINT16; UINT32 x4} 落在字节 4 而非 2,
|
||||
------ 于是 0x7000/0x6000/0x2000 的 SDO 单条目访问全部读写错位内存;SSC 内部
|
||||
------ 0x1C3x(TSYNCMANPAR)/0x10F1 等同样受影响。
|
||||
------ 这些宏是 SSC 紧排结构体的唯一手段——SSC 的 SDO 读写按"si0 占 16bit,
|
||||
------ 其后各条目按 BitLength 紧排"计算字节偏移(OBJ_GetEntryOffset, objdef.c:609),
|
||||
------ 留空则结构体按编译器自然对齐,与该假设不符,导致 SDO 单条目访问
|
||||
------ 全部读写错位内存;SSC 内部 0x1C3x/0x10F1 等同样受影响。
|
||||
------ AC6/armclang 支持 GCC 风格 __attribute__((packed)),置于 struct 关键字之后
|
||||
------ (即 START 位置)对整个结构体生效。
|
||||
-----------------------------------------------------------------------------------------*/
|
||||
@@ -590,11 +582,8 @@
|
||||
------
|
||||
------ PDO Size (需与 DCU_ESI.xml / gen_eeprom.py / dc_app.c 三处一致)
|
||||
------
|
||||
------ 1.5.56 扩到 DPRAM 硬上限:64B/384B → 384B/640B。
|
||||
------ 输出 384B = 48×64bit:兼容单帧区 48B + CANFD 批量发送区 336B
|
||||
------ 输入 640B = 80×64bit:桥状态 32B + 电机窗口 288B + 长帧 40B + 批量接收区 280B
|
||||
------ 上限依据:邮箱占 0x1000-0x13FF,过程数据区仅 0x1400-0x1FFF=3072B,
|
||||
------ SM2/SM3 均 3-buffer 实占 3×size ⇒ out+in ≤ 1024B。384+640 恰好用满。
|
||||
------ 上限依据:邮箱占 0x1000-0x13FF,过程数据区仅 0x1400-0x1FFF=3072B,
|
||||
------ SM2/SM3 均 3-buffer 实占 3×size ⇒ out+in ≤ 1024B。
|
||||
------ 推导与实测(SII 余量/EXMC 耗时)见 comm/ethercat_pdo_size.h。
|
||||
------
|
||||
------ 这两个宏是**编译期硬门限**,必须先于其它改动生效:
|
||||
@@ -604,12 +593,12 @@
|
||||
------ 忘改这里会在进 SAFEOP 时报 AL 错误,而不是静默截断。
|
||||
-----------------------------------------------------------------------------------------*/
|
||||
|
||||
/** 最大输出 PDO 字节数(RxPDO,主站→从站)。1.5.56: 0x40 → 0x180 = 384B */
|
||||
/** 最大输出 PDO 字节数(RxPDO,主站→从站)。0x180 = 384B */
|
||||
#ifndef MAX_PD_OUTPUT_SIZE
|
||||
#define MAX_PD_OUTPUT_SIZE 0x180
|
||||
#endif
|
||||
|
||||
/** 最大输入 PDO 字节数(TxPDO,从站→主站)。1.5.56: 0x140 → 0x280 = 640B */
|
||||
/** 最大输入 PDO 字节数(TxPDO,从站→主站)。0x280 = 640B */
|
||||
#ifndef MAX_PD_INPUT_SIZE
|
||||
#define MAX_PD_INPUT_SIZE 0x280
|
||||
#endif
|
||||
@@ -666,8 +655,10 @@
|
||||
#define DEVICE_SW_VERSION "1.0"
|
||||
#endif
|
||||
|
||||
/* 0x100A 的位长。SSC 默认 3 字符只装得下占位 "1.0";这里抬到 16,真实值由
|
||||
* sys/initial.cpp 起板时写进 acSoftwareversion(与 BOOT 侧同款改动)。 */
|
||||
#ifndef DEVICE_SW_VERSION_LEN
|
||||
#define DEVICE_SW_VERSION_LEN 0x3
|
||||
#define DEVICE_SW_VERSION_LEN 0x10
|
||||
#endif
|
||||
|
||||
/*-----------------------------------------------------------------------------------------
|
||||
@@ -708,6 +699,28 @@
|
||||
#define MAX_MBX_READ_ADDRESS 0x1FFF
|
||||
#endif
|
||||
|
||||
/* 本板邮箱几何构建期常量。
|
||||
* 取值 = gen_eeprom.py 的 MBX_RX_OFFSET/SIZE(0x1000/256) 与 MBX_TX_OFFSET/SIZE
|
||||
* (0x1200/256),即 DCU_ESI.xml <Sm> 与 ESC 上电自 SII 加载的寄存器真值;主站
|
||||
* config_init 也只是照抄同一份 SII。MBX_StartMailboxHandler 以此直达缓存,
|
||||
* 不信任单次 CSR 间接读,CSR 读仅作交叉校验(mailbox.c 打 WARN)。
|
||||
* ★ 改 SII 邮箱几何必须同步改这里(gen_eeprom.py 顶部注释同样有约束)。★ */
|
||||
#ifndef MBX_SM0_START
|
||||
#define MBX_SM0_START 0x1000
|
||||
#endif
|
||||
|
||||
#ifndef MBX_SM0_SIZE
|
||||
#define MBX_SM0_SIZE 0x0100
|
||||
#endif
|
||||
|
||||
#ifndef MBX_SM1_START
|
||||
#define MBX_SM1_START 0x1200
|
||||
#endif
|
||||
|
||||
#ifndef MBX_SM1_SIZE
|
||||
#define MBX_SM1_SIZE 0x0100
|
||||
#endif
|
||||
|
||||
/*-----------------------------------------------------------------------------------------
|
||||
------
|
||||
------ PDO Timing (DC sync parameters, unit: ns)
|
||||
|
||||
@@ -4,8 +4,7 @@ Generate LAN9252 EEPROM binary image (ESI/SII) for GD32E508 DCU.
|
||||
Outputs a C header file with the EEPROM content as a byte array.
|
||||
|
||||
Layout per ETG.1000.6 section 5.4 "SII coding" (Table 16/17, verified against the
|
||||
local spec copy ETG1000_6_V1i0i3; 1.5.27 rewrite of the previous generator whose
|
||||
layout was shifted by 2 words from word 4 onwards and used XOR instead of CRC-8):
|
||||
local spec copy ETG1000_6_V1i0i3):
|
||||
|
||||
Word 0x00 PDI Control (byte0=PDI type 0x8D HBI 16-bit indexed,
|
||||
byte1=ESC Configuration 0x00)
|
||||
@@ -62,7 +61,6 @@ MBX_TX_SIZE = 256
|
||||
|
||||
# Process Data configuration
|
||||
#
|
||||
# 1.5.56 扩到 DPRAM 硬上限 64/384 -> 384/640(需要一周期合并多条 CANFD 帧)。
|
||||
# 必须与 comm/ethercat_pdo_size.h 的 ECAT_PDO_OUT_BYTES / ECAT_PDO_IN_BYTES 一致
|
||||
# ——固件侧有 _Static_assert 守着,但 EEPROM 这一侧无法编译期校验,只能靠这里对齐。
|
||||
#
|
||||
@@ -70,8 +68,8 @@ MBX_TX_SIZE = 256
|
||||
# 邮箱 SM0/SM1 各 256B 固定占 0x1000-0x13FF,过程数据区只剩 0x1400-0x1FFF=3072B。
|
||||
# SM2/SM3 都是 3-buffer 模式(control 0x64 / 0x20,bit1=0),实际占用 3×size:
|
||||
# SM2 384B -> 0x1400-0x187F (384×3 = 1152B)
|
||||
# SM3 640B -> 0x1880-0x1FFF (640×3 = 1920B) ← 恰好用满最后一字节
|
||||
# (384+640)×3 = 3072 = 过程数据区全长,一字节不剩也一字节不越界。
|
||||
# SM3 600B -> 0x1880-0x1F67 (600×3 = 1800B), DPRAM 留 120B 余量
|
||||
# (384+600)×3 = 2952 ≤ 3072 = 过程数据区全长。
|
||||
# SSC 在 ecatslv.c:708-721 按 (start + size*buffer) 做重叠检查,撞了会在
|
||||
# PREOP->SAFEOP 返回 ALSTATUSCODE_INVALIDSMOUTCFG,不是静默出错。
|
||||
# 要再更大只能压邮箱(拖慢 SDO/FoE)或改 1-buffer(失去读写并发保护),见
|
||||
@@ -79,7 +77,13 @@ MBX_TX_SIZE = 256
|
||||
PD_OUT_OFFSET = 0x1400 # SM2: RxPDO (Master -> Slave outputs)
|
||||
PD_OUT_SIZE = 384 # 48×64bit
|
||||
PD_IN_OFFSET = 0x1880 # SM3: TxPDO (Slave -> Master inputs)
|
||||
PD_IN_SIZE = 640 # 80×64bit
|
||||
PD_IN_SIZE = 600 # 75×64bit
|
||||
# 批量接收为**定长覆盖窗口** 5 槽(每槽 12B 头含 u32 seqlock
|
||||
# + 32B 数据 = 44B,共 220B,落在 364..583),段5诊断区 584。
|
||||
# 5 槽 = 5000 帧/s@1ms。
|
||||
# SM3 实占 3×600=1800B (0x1880-0x1F67),out+in=984 ≤ 1024 硬上限,留 120B 余量;
|
||||
# 依据见 comm/ethercat_pdo_size.h。
|
||||
# 改后必须重烧 EEPROM 并断电冷启动。
|
||||
|
||||
# 3-buffer 占用与 DPRAM 上限自检:改尺寸/偏移时立刻在生成阶段拦住,
|
||||
# 而不是等烧进 EEPROM、冷启动后再看 AL 错误码。
|
||||
@@ -97,7 +101,7 @@ assert PD_OUT_SIZE % 8 == 0 and PD_IN_SIZE % 8 == 0, \
|
||||
# 0x8D=HBI 16-bit). 0x05 is a reserved value on LAN9252 and disables the HBI PDI.
|
||||
PDI_CONTROL = 0x8D
|
||||
# Word 0 high byte = ESC Configuration register (0x0141): all zero
|
||||
# (the old 0x0C hit "NOT set by EEPROM" DC bits - removed in 1.5.27).
|
||||
# (0x0C would hit "NOT set by EEPROM" DC bits).
|
||||
ESC_CONFIG = 0x00
|
||||
# Word 1 low byte = PDI Configuration register (0x0150) HBI-mode bits: ALE
|
||||
# qual / RD_WR mode / CS / RD / WR / ALE polarities. 0x00 = normal active-low
|
||||
@@ -109,7 +113,7 @@ PDI_CONFIG = 0x00
|
||||
# SYNC1 Output=1, SYNC0 Map=1, SYNC0 Output=1, both Driver/Polarity=00
|
||||
# (push-pull ACTIVE LOW, DS p247). Active-low pulses pair with the firmware's
|
||||
# falling-edge EXTI; with 0x00 the SYNC0/1 pins stay LATCH inputs and a DC
|
||||
# SYNC0 event would never reach PB0 -> process data dead in DC mode (1.5.28).
|
||||
# SYNC0 event would never reach PB0 -> process data dead in DC mode.
|
||||
# Same value as the official LAN9252-EVB-HBIXML.xml ConfigData 890E80CC...
|
||||
SYNC_LATCH_CFG = 0xCC
|
||||
# Word 2 = Pulse Length of SyncSignals register (0x0982), units of 10ns
|
||||
@@ -343,7 +347,7 @@ for row in range(0, EEPROM_SIZE, 16):
|
||||
chunk = eeprom[row:row+16]
|
||||
hex_str = ", ".join(f"0x{b:02X}" for b in chunk)
|
||||
if all(b == 0xFF for b in chunk):
|
||||
# 1.5.28: emit the real 0xFF bytes. A comment-only padding row leaves
|
||||
# Emit the real 0xFF bytes. A comment-only padding row leaves
|
||||
# those array elements zero-initialized (C static init) instead of
|
||||
# the erased-0xFF state, and the firmware burns the whole array.
|
||||
if row % 256 == 0:
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
* @brief SSC V5.11 硬件 port 层 — GD32E508 + LAN9252 (EXMC 16-bit HBI Indexed Mode)
|
||||
*
|
||||
* 实现 el9800hw.h 中声明的全部 HW_* 函数,将 SSC 的 ESC 访问请求
|
||||
* 转发到 EscLan9252 的间接访问端口(1.5.39 按 DS 12.11 修正访问模型):
|
||||
* 转发到 EscLan9252 的间接访问端口:
|
||||
* - EtherCAT 核心 CSR(0x0000-0x0FFF:AL Status/Event、SyncManager、LED 等)
|
||||
* → ECAT_CSR_CMD(0x304)/ECAT_CSR_DATA(0x300) 间接端口;
|
||||
* - 过程 RAM(0x1000-0x1FFF:邮箱 0x1000+、PDO 0x1400+)
|
||||
* → PRAM FIFO(0x000-0x03C 数据口 + 0x308-0x314 地址/命令)。
|
||||
* 1.5.37 及之前直读/直写 HBI 索引(误以为索引空间统一可寻址),核心 CSR 全部
|
||||
* 读 0/写丢弃——SSC 对主站 AL Control 完全失聪,主站恒读 AL=INIT。
|
||||
* 核心 CSR 不在 HBI 直接空间,直读/直写会使核心 CSR 全部
|
||||
* 读 0/写丢弃,SSC 对主站 AL Control 完全失聪。
|
||||
*
|
||||
* 并发保护(不变):
|
||||
* - HW_EscRead/HW_EscWrite 等主循环函数:内部经 EscExmcBus::maskEnter/Exit
|
||||
@@ -229,8 +229,7 @@ void HW_EscWriteIsr(MEM_ADDR *pData, UINT16 Address, UINT16 Len)
|
||||
/**
|
||||
* @brief 禁用指定 SM 通道 (清 SM Activate 寄存器 0x0806+8n bit0)。
|
||||
* @param channel SM 通道号 (0-15)
|
||||
* @note 1.5.39:偏移由 +4 改为 +6——+4 是 SM Control(操作模式字段),
|
||||
* Activate(bit0=SM Enable)在 +6;旧值从未真正生效过(访问层全空)。
|
||||
* @note SM Activate 在偏移 +6(bit0=SM Enable);+4 是 SM Control(操作模式字段)。
|
||||
*/
|
||||
void HW_DisableSyncManChannel(UINT8 channel)
|
||||
{
|
||||
|
||||
@@ -209,8 +209,27 @@ OBJCONST UCHAR OBJMEM aName0x1009[] = "Hardware version";
|
||||
-----------------------------------------------*/
|
||||
/**
|
||||
* \brief 0x100A (Software version) variable to handle the object data
|
||||
*
|
||||
* 必须显式给到 DEVICE_SW_VERSION_LEN+1:条目描述符按 LEN 声明位长,SSC 会照长度
|
||||
* 整块回给主站;用 =DEVICE_SW_VERSION 推出来只有 4 字节,读到的是越界内容。
|
||||
* 真实版本号由 sys/initial.cpp 起板时调 AppSetSoftwareVersion() 覆盖。
|
||||
*/
|
||||
CHAR acSoftwareversion[] = DEVICE_SW_VERSION;
|
||||
CHAR acSoftwareversion[DEVICE_SW_VERSION_LEN + 1] = DEVICE_SW_VERSION;
|
||||
|
||||
/** @brief 覆盖 0x100A 上报的软件版本串(容量只有本文件知道,故 setter 放这儿)。 */
|
||||
void AppSetSoftwareVersion(const char *version)
|
||||
{
|
||||
unsigned int i = 0U;
|
||||
|
||||
if (version == NULL) {
|
||||
return;
|
||||
}
|
||||
/* 本文件没 include stdio,手写有界拷贝:超长就截断,末尾必带 NUL。 */
|
||||
for (; i + 1U < sizeof(acSoftwareversion) && version[i] != '\0'; i++) {
|
||||
acSoftwareversion[i] = version[i];
|
||||
}
|
||||
acSoftwareversion[i] = '\0';
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief 0x1009 (Hardware version) entry description
|
||||
|
||||
@@ -11,94 +11,6 @@
|
||||
This file contains the Process Data interface
|
||||
|
||||
\version 5.11
|
||||
|
||||
<br>Changes to version V5.10.1:<br>
|
||||
V5.11 COE3: change 0x10F3.2 (Sync Error limit) from UINT32 to UINT16 (according to the ETG.1020)<br>
|
||||
V5.11 ECAT1: update EEPROM access reset operation<br>
|
||||
V5.11 ECAT10: change PROTO handling to prevent compiler errors<br>
|
||||
V5.11 ECAT11: create application interface function pointer, add eeprom emulation interface functions<br>
|
||||
V5.11 ECAT2: update EEPROM access retry cycle (add 10ms delay between two retry cycles)<br>
|
||||
V5.11 ECAT3: handle bus cycle calculation for input/output only devices and create warning diag message only if calculation failed<br>
|
||||
V5.11 ECAT4: enhance SM/Sync monitoring for input/output only slaves<br>
|
||||
V5.11 ECAT6: add function to calculate bus cycle time<br>
|
||||
V5.11 ECAT8: call PDO_InputMapping only once if DC is enabled and COE is not supported<br>
|
||||
V5.11 EEPROM1: fix compiler error during pEEPROM pointer initialization<br>
|
||||
V5.11 EEPROM2: write Station alias value to EEPROM data register on EEPROM reload command<br>
|
||||
V5.11 EEPROM3: clear EEPROM error bits<br>
|
||||
V5.11 EEPROM4: prevent the variable in the EEPROM busy loop to be removed by the compiler<br>
|
||||
V5.11 ESM7: "add Sync define for 0x22 (""SYNCTYPE_SM2_SYNCHRON""), support value 0x22 for 0x1C33.1 (SM2 sync)"<br>
|
||||
<br>Changes to version V5.01:<br>
|
||||
V5.10 COE1: Define one entry description for all 0x1C3x objects and change data type of SI11,12,13 to UINT16 (according ETG.1020)<br>
|
||||
V5.10 ECAT1: Correct calculation of blinking and flashing sequence<br>
|
||||
V5.10 ECAT13: Update Synchronisation handling (FreeRun,SM Sync, Sync0, Sync1)<br>
|
||||
Compare DC UINT configuration (by ESC Config data) vs. DC activation register settings<br>
|
||||
Update 0x1C3x entries<br>
|
||||
V5.10 ECAT2: Prevent EEPROM data null pointer access (if the pointer is null an command error is set)<br>
|
||||
EEPROM emulation return command error if unknown command was received<br>
|
||||
V5.10 ECAT4: Update alignment marco for 8 to 15 bit alignments (16 and 32 Bit controllers)<br>
|
||||
Bugfix calculate LED blink frequency<br>
|
||||
V5.10 ECAT7: Add "bInitFinished" to indicate if the initialization is complete<br>
|
||||
V5.10 HW2: Change HW_GetTimer return value to UINT32<br>
|
||||
<br>Changes to version V5.0:<br>
|
||||
V5.01 APPL3: Include library demo application<br>
|
||||
V5.01 ESC1: Change ESC access function (if EEPROM Emulation is active)<br>
|
||||
V5.01 ESC2: Add missed value swapping<br>
|
||||
<br>Changes to version V4.40:<br>
|
||||
V5.0 TEST1: Add test application. See Application Note ET9300 for more details.<br>
|
||||
V5.0 ECAT2: Application specific functions are moved to application files.<br>
|
||||
V5.0 ECAT3: Global dummy variables used for dummy ESC operations.<br>
|
||||
V5.0 ESC1: ESC 32Bit Access added.<br>
|
||||
V5.0 ESC3: Add EEPROM emulation support.<br>
|
||||
V5.0 ESM3: Handling pending ESM transitions.<br>
|
||||
V5.0 ESC5: Enhance EEPROM access handling.<br>
|
||||
V5.0 PDO1: AL Event flags are not rechecked in PDO_OutputMappping(). (Already checked before call function)<br>
|
||||
V5.0 SYNC1: Add missed SM event indication (0x1C32/0x1C33 SI11).<br>
|
||||
<br>Changes to version V4.30:<br>
|
||||
V4.40 DIAG1: add diagnosis message support<br>
|
||||
V4.40 PDO1: merge content of HW_InputMapping (spihw.c/mcihw.c) to PDO_InputMapping. merge content of HW_OutputMapping (spihw.c/mcihw.c) to PDO_OutputMapping.<br>
|
||||
V4.40 PDO2: Generic process data length calculation<br>
|
||||
V4.40 ECAT2: call cyclic CheckIfLocalError() to check the local flags<br>
|
||||
V4.40 HW0: Generic hardware access functions. Add Function (PDI_Isr()), content merged from spihw.c and mcihw.c.<br>
|
||||
V4.40 WD1: define (ESC_SM_WD_SUPPORTED) to choose ESC SyncManager watchdog or local watchdog<br>
|
||||
V4.40 ESM2: Change state transition behaviour from SafeOP to OP<br>
|
||||
V4.40 TIMER1: Change bus cycle time calculation and trigger of ECAT_CheckTimer() if ECAT_TIMER_INT is reset<br>
|
||||
V4.40 HW1: Add support for fc1100 hardware<br>
|
||||
<br>Changes to version V4.20:<br>
|
||||
V4.30 EL9800: EL9800_x cyclic application is moved to el9800.c<br>
|
||||
V4.30 OBJ 3: add object dictionary initialization<br>
|
||||
V4.30 SYNC: add CalcSMCycleTime() (calculation of bus cycle time); change synchronisation control functions<br>
|
||||
V4.30 PDO: include PDO specific functions (moved from coeappl.c).<br>
|
||||
xxx_InputMapping(); xxx_OutputMapping(); xxx_ReadInputs(); xxx_ResetOutputs(); xxx_Application()<br>
|
||||
V4.30 CiA402: Add CiA402_StateMachine() and CiA402_Application() call<br>
|
||||
V4.20 DC 1: Add DC pending Statemachine handling<br>
|
||||
V4.20 PIC24: Add EL9800_4 (PIC24) required source code<br>
|
||||
V4.20 LED 1: Modified LED Handling<br>
|
||||
V4.11 APPL 1: The checkWatchdog() function should not called in checkTimer() if this function is triggered by an Interrupt<br>
|
||||
<br>Changes to version V4.08:<br>
|
||||
V4.10 LED 1: The handling of the EtherCAT-Error-LED was added<br>
|
||||
V4.10 AOE 3: The AoE fragment size has to be initialized during the state transition<br>
|
||||
from INIT to PREOP<br>
|
||||
<br>Changes to version V4.07:<br>
|
||||
V4.08 LED 1: The handling of the EtherCAT-LED can be (de-)selected by the switch LEDS_SUPPORTED<br>
|
||||
because the ET1100 and ET1200 have an output port which could be connected directly.<br>
|
||||
<br>Changes to version V4.01:<br>
|
||||
V4.02 ECAT 1: The watchdog timer variables shall be initialized.<br>
|
||||
<br>Changes to version V4.00:<br>
|
||||
V4.01 APPL 1: If the application is running in synchron mode and no SM event<br>
|
||||
is received, the application should be called from the main loop<br>
|
||||
V4.01 APPL 2: In FreeRun mode the output should only be copied if the slave is in OP<br>
|
||||
<br>Changes to version V3.20:<br>
|
||||
V4.00 APPL 1: The watchdog checking should be done by a microcontroller<br>
|
||||
timer because the watchdog trigger of the ESC will be reset too<br>
|
||||
if only a part of the sync manager data is written<br>
|
||||
V4.00 APPL 2: The setting of EtherCAT state LEDs were included<br>
|
||||
V4.00 APPL 3: The outputs should be reset to a safe state,<br>
|
||||
when the state OP is left<br>
|
||||
V4.00 APPL 4: An example for the EEPROM access through the ESC is shown in<br>
|
||||
the function APPL_StartMailboxHandler<br>
|
||||
V4.00 APPL 5: The inputs should be read once when the state transition<br>
|
||||
from PREOP to SAFEOP is made<br>
|
||||
V4.00 APPL 6: The main function was split in MainInit and MainLoop
|
||||
*/
|
||||
|
||||
|
||||
@@ -157,6 +69,33 @@ BOOL bCycleTimeMeasurementStarted; // indicates if the bus cycle measurement is
|
||||
UINT16 aPdOutputData[(MAX_PD_OUTPUT_SIZE>>1)];
|
||||
UINT16 aPdInputData[(MAX_PD_INPUT_SIZE>>1)];
|
||||
|
||||
/*ECAT_DBG_START(剖分埋点)*/
|
||||
/* 负载归因埋点:DWT 周期累加器,经 0x2000 sub5..8 上报,主站 1s 差分出速率。
|
||||
* EcatDbg_Dwt() 定义在 comm/ethercat_sync.cpp (extern "C")。 */
|
||||
extern UINT32 EcatDbg_Dwt(void);
|
||||
#define ecatdbg_dwt() EcatDbg_Dwt()
|
||||
/* CPU 负载挂钩(sys/cpu_load.cpp) */
|
||||
extern void CpuLoad_Enter(void);
|
||||
extern void CpuLoad_Leave(void);
|
||||
/* 1ms 调度节拍(ethercat_sync.cpp extern "C") */
|
||||
extern UINT32 EcatDbg_GetTick1ms(void);
|
||||
/* 输出探针(实现见 comm/ethercat_pdo.cpp,C 链接) */
|
||||
extern UINT8 EcatPdo_OutputProbe(UINT8 const *head);
|
||||
extern void EcatPdo_NoteOutputMapped(void);
|
||||
/* 稀疏写 ESC 计划(密排 count 只写有效段; 实现见 ethercat_pdo.cpp) */
|
||||
extern UINT8 EcatPdo_SparseEscPlan(UINT16 *off0, UINT16 *len0, UINT16 *off1, UINT16 *len1);
|
||||
/* 差分写 ESC 多段计划(段头/段尾 + 3-buffer 冲刷; 实现见 ethercat_pdo.cpp) */
|
||||
extern UINT8 EcatPdo_EscWritePlan(UINT16 *offs, UINT16 *lens, UINT8 maxSegs);
|
||||
volatile UINT32 g_dbg_pdiCyc = 0; /* sub5: PDI_Isr 全函数累计周期 */
|
||||
volatile UINT32 g_dbg_outCyc = 0; /* sub6: PDO_OutputMapping(输出探针/全量读) */
|
||||
volatile UINT32 g_dbg_freerunCyc = 0; /* sub7: MainLoop free-run 分支(SM 同步下恒 0) */
|
||||
volatile UINT32 g_dbg_pdiCnt = 0; /* sub8: PDI_Isr 触发次数(=每周期 1 次) */
|
||||
volatile UINT32 g_dbg_inCyc = 0; /* sub9: PDO_InputMapping 全程(APPL+过程数据写) */
|
||||
volatile UINT32 g_dbg_inWriteCyc = 0; /* sub10: 其中 HW_EscWriteIsr 640B 过程数据写 */
|
||||
volatile UINT32 g_dbg_alCyc = 0; /* sub11: PDI_Isr 内 AL 事件读×2+超限应答 */
|
||||
volatile UINT32 g_dbg_appCyc = 0; /* sub12: ECAT_Application(桥 processOutput) */
|
||||
/*ECAT_DBG_END*/
|
||||
|
||||
/*variables are declared in ecatslv.c*/
|
||||
extern VARVOLATILE UINT16 u16dummy;
|
||||
BOOL bInitFinished = FALSE; /** < \brief indicates if the initialization is finished*/
|
||||
@@ -178,21 +117,76 @@ BOOL bInitFinished = FALSE; /** < \brief indicates if the initialization is fini
|
||||
*////////////////////////////////////////////////////////////////////////////////////////
|
||||
void PDO_InputMapping(void)
|
||||
{
|
||||
/*ECAT_DBG(剖分:输入映射全程 + 过程数据写细分)*/
|
||||
/* 稀疏写:APPL 仍整填 aPdInputData(对齐 SDO 0x6000),但 HW_EscWriteIsr
|
||||
* 只写密排有效段+段3/5,跳过空槽区 —— 省的是 PRAM 字节时间,不是 RAM 拷贝。
|
||||
* 差分写:在稀疏之上再跳过「与影子一致且 3-buffer 冲刷完成」的段。 */
|
||||
const UINT32 dbgT0 = ecatdbg_dwt();
|
||||
APPL_InputMapping((UINT16*)aPdInputData);
|
||||
HW_EscWriteIsr(((MEM_ADDR *) aPdInputData), nEscAddrInputData, nPdInputSize );
|
||||
const UINT32 dbgT1 = ecatdbg_dwt();
|
||||
{
|
||||
UINT16 offs[4], lens[4];
|
||||
const UINT8 nseg = EcatPdo_EscWritePlan(offs, lens, 4U);
|
||||
if (nseg == 0U) {
|
||||
HW_EscWriteIsr(((MEM_ADDR *) aPdInputData), nEscAddrInputData, nPdInputSize );
|
||||
} else {
|
||||
for (UINT8 i = 0U; i < nseg; ++i) {
|
||||
HW_EscWriteIsr(((MEM_ADDR *)((UINT8 *)aPdInputData + offs[i])),
|
||||
(UINT16)(nEscAddrInputData + offs[i]), lens[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
g_dbg_inWriteCyc += ecatdbg_dwt() - dbgT1;
|
||||
g_dbg_inCyc += ecatdbg_dwt() - dbgT0;
|
||||
}
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
/**
|
||||
\brief This function will copies the outputs from the ESC memory to the local memory
|
||||
to the hardware. This function is only called in case of an SM2
|
||||
(output process data) event.
|
||||
|
||||
输出按需全量读:每周期先读输出 PDO 头 12B 探针(覆盖 SEQ@0
|
||||
与 WIN@8),经 EcatPdo_OutputProbe 判定——seq 无变化沿且无清统计请求时,
|
||||
跳过全量读(只补读 SM2 末字完成事件应答)。主站滚动窗口扫槽依赖的
|
||||
WIN 字节在探针内每周期刷新,窗口扫描不受影响。
|
||||
SM2 应答语义不变:首字(探针内)+末字(两分支各自覆盖)。
|
||||
*////////////////////////////////////////////////////////////////////////////////////////
|
||||
void PDO_OutputMapping(void)
|
||||
{
|
||||
/*ECAT_DBG(剖分:输出映射全程)*/
|
||||
const UINT32 dbgT0 = ecatdbg_dwt();
|
||||
|
||||
HW_EscReadIsr(((MEM_ADDR *)aPdOutputData), nEscAddrOutputData, nPdOutputSize );
|
||||
/* 输出映射:12B 探针判定是否有新输出。
|
||||
* 「每拍始终全量读 384B」会在 PDO 提速后把 PDI 成本抬高一个数量级
|
||||
* (历史:探针版 4kHz load≈80%;全量+process 双读时 2kHz 即饱和)。
|
||||
* 探针命中才全量读;否则只补读 SM2 末字。s_out 长期不更新由
|
||||
* EthercatPdo::process() 按新鲜度门兜底全量读(不是每拍)。 */
|
||||
UINT8 head[12];
|
||||
HW_EscReadIsr(((MEM_ADDR *) head), nEscAddrOutputData, 12U );
|
||||
|
||||
APPL_OutputMapping((UINT16*) aPdOutputData);
|
||||
if ( EcatPdo_OutputProbe(head) )
|
||||
{
|
||||
/* 有新 seq 时不必每次拉满 384B:先读命令前缀(含批量 count@48)。
|
||||
* 8kHz 主站每拍都递增 seq 时,若 count==0 仍全量读会每拍吃满 PRAM,
|
||||
* 把 CAN 解析饿死(gseq/s 从 ~3750 掉到数百)。 */
|
||||
const UINT16 prefix = (nPdOutputSize < 52U) ? nPdOutputSize : 52U;
|
||||
HW_EscReadIsr(((MEM_ADDR *)aPdOutputData), nEscAddrOutputData, prefix);
|
||||
const UINT8 batchCount = (prefix > 48U) ? ((const UINT8 *)aPdOutputData)[48] : 0U;
|
||||
if (batchCount > 0U && nPdOutputSize > prefix) {
|
||||
HW_EscReadIsr(((MEM_ADDR *)((UINT8 *)aPdOutputData + prefix)),
|
||||
(UINT16)(nEscAddrOutputData + prefix),
|
||||
(UINT16)(nPdOutputSize - prefix));
|
||||
}
|
||||
APPL_OutputMapping((UINT16*) aPdOutputData);
|
||||
EcatPdo_NoteOutputMapped();
|
||||
}
|
||||
else
|
||||
{
|
||||
if (nPdOutputSize >= 2U) {
|
||||
HW_EscReadWordIsr(u16dummy,(nEscAddrOutputData+nPdOutputSize-2));
|
||||
}
|
||||
}
|
||||
g_dbg_outCyc += ecatdbg_dwt() - dbgT0;
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
@@ -279,10 +273,14 @@ void HandleBusCycleCalculation(void)
|
||||
|
||||
void PDI_Isr(void)
|
||||
{
|
||||
/*ECAT_DBG(剖分:ISR 全程)*/
|
||||
const UINT32 dbgT0 = ecatdbg_dwt();
|
||||
if(bEscIntEnabled)
|
||||
{
|
||||
/* get the AL event register */
|
||||
const UINT32 dbgTa = ecatdbg_dwt();
|
||||
UINT16 ALEvent = HW_GetALEventRegister_Isr();
|
||||
g_dbg_alCyc += ecatdbg_dwt() - dbgTa;
|
||||
ALEvent = SWAPWORD(ALEvent);
|
||||
|
||||
if ( ALEvent & PROCESS_OUTPUT_EVENT )
|
||||
@@ -330,12 +328,27 @@ void PDI_Isr(void)
|
||||
/*ECATCHANGE_END(V5.11) ECAT4*/
|
||||
|
||||
/*
|
||||
Call ECAT_Application() in SM Sync mode
|
||||
Call ECAT_Application() in SM Sync mode (non-DC only).
|
||||
|
||||
DC:应用节拍已由 Sync0_Isr 拥有(bDcSyncActive),PDI 再跑一次
|
||||
会把 Process/buildInput 算两遍 load_perm。
|
||||
|
||||
非 DC:仍只在 SM/SM2 类型时在 PDI 内跑应用。
|
||||
**不要**「凡有 AL 过程事件就跑」——8kHz 时 PDI 每拍都带事件,
|
||||
Process+InputMapping 热路径会被放大,单拍超过 ~120µs 墙,
|
||||
实测 6kHz 即掉出(曾可 8kHz)。
|
||||
负载随 PDO 上升由 ISR 包装 + free-run 1ms 节流路径体现,
|
||||
见 sys/cpu_load.h;输出侧保持 12B 探针,避免每拍全量读。
|
||||
*/
|
||||
if (sSyncManOutPar.u16SyncType == SYNCTYPE_SM_SYNCHRON)
|
||||
if (!bDcSyncActive
|
||||
&& (sSyncManOutPar.u16SyncType == SYNCTYPE_SM_SYNCHRON
|
||||
|| sSyncManOutPar.u16SyncType == SYNCTYPE_SM2_SYNCHRON
|
||||
|| sSyncManInPar.u16SyncType == SYNCTYPE_SM_SYNCHRON
|
||||
|| sSyncManInPar.u16SyncType == SYNCTYPE_SM2_SYNCHRON))
|
||||
{
|
||||
/* The Application is synchronized to process data Sync Manager event*/
|
||||
const UINT32 dbgTb = ecatdbg_dwt();
|
||||
ECAT_Application();
|
||||
g_dbg_appCyc += ecatdbg_dwt() - dbgTb;
|
||||
}
|
||||
|
||||
if ( bEcatInputUpdateRunning
|
||||
@@ -345,14 +358,18 @@ void PDI_Isr(void)
|
||||
)
|
||||
{
|
||||
/* EtherCAT slave is at least in SAFE-OPERATIONAL, update inputs */
|
||||
const UINT32 dbgTc = ecatdbg_dwt();
|
||||
PDO_InputMapping();
|
||||
g_dbg_inCyc += ecatdbg_dwt() - dbgTc;
|
||||
}
|
||||
|
||||
/*
|
||||
Check if cycle exceed
|
||||
*/
|
||||
/*if next SM event was triggered during runtime increment cycle exceed counter*/
|
||||
const UINT32 dbgTd = ecatdbg_dwt();
|
||||
ALEvent = HW_GetALEventRegister_Isr();
|
||||
g_dbg_alCyc += ecatdbg_dwt() - dbgTd;
|
||||
ALEvent = SWAPWORD(ALEvent);
|
||||
|
||||
if ( ALEvent & PROCESS_OUTPUT_EVENT )
|
||||
@@ -365,6 +382,9 @@ void PDI_Isr(void)
|
||||
HW_EscReadWordIsr(u16dummy,(nEscAddrOutputData+nPdOutputSize-2));
|
||||
}
|
||||
} //if(bEscIntEnabled)
|
||||
/*ECAT_DBG(剖分:ISR 全程 收尾)*/
|
||||
g_dbg_pdiCyc += ecatdbg_dwt() - dbgT0;
|
||||
g_dbg_pdiCnt++;
|
||||
}
|
||||
|
||||
void Sync0_Isr(void)
|
||||
@@ -577,12 +597,30 @@ void MainLoop(void)
|
||||
}
|
||||
|
||||
DISABLE_ESC_INT();
|
||||
ECAT_Application();
|
||||
|
||||
if ( bEcatInputUpdateRunning )
|
||||
/*ECAT_DBG(剖分:free-run 应用+输入映射块)*/
|
||||
{
|
||||
/* EtherCAT slave is at least in SAFE-OPERATIONAL, update inputs */
|
||||
PDO_InputMapping();
|
||||
const UINT32 dbgT1 = ecatdbg_dwt();
|
||||
/*
|
||||
* free-run 主循环:**严格 1ms 节流**再跑应用并计入 busy。
|
||||
* 不要「AL 有过程事件就跑」——事件位可能连续置位,退化成
|
||||
* 每圈 MainLoop 都 Process,热路径被抬爆(8kHz 掉到 6kHz)。
|
||||
* SM 模式(bEscIntEnabled)本分支本就不进;PDO 高速走 PDI ISR,
|
||||
* 负载由 EXTI1 挂钩计入。
|
||||
*/
|
||||
static UINT32 s_freeRunAppMs = 0xFFFFFFFFu;
|
||||
const UINT32 nowMs = EcatDbg_GetTick1ms();
|
||||
if (nowMs != s_freeRunAppMs) {
|
||||
s_freeRunAppMs = nowMs;
|
||||
CpuLoad_Enter();
|
||||
ECAT_Application();
|
||||
|
||||
if ( bEcatInputUpdateRunning )
|
||||
{
|
||||
PDO_InputMapping();
|
||||
}
|
||||
CpuLoad_Leave();
|
||||
}
|
||||
g_dbg_freerunCyc += ecatdbg_dwt() - dbgT1;
|
||||
}
|
||||
ENABLE_ESC_INT();
|
||||
}
|
||||
|
||||
@@ -67,48 +67,6 @@ When the repeated mailbox service was sent (call of MBX_MailboxReadInd), psReadM
|
||||
and psStoreMbx will be sent (in MBX_CopyToSendMailbox) and stored in psReadMbx, psStoreMbx will be set to 0.
|
||||
|
||||
\version 5.11
|
||||
|
||||
<br>Changes to version V5.10:<br>
|
||||
V5.11 ECAT10: change PROTO handling to prevent compiler errors<br>
|
||||
V5.11 ECAT7: add missing big endian swapping<br>
|
||||
V5.11 HW1: "move hardware independent functions ""HW_DisableSyncManChannel()"", ""HW_EnableSyncManChannel()"", ""HW_GetSyncMan()"", ""HW_ResetALEventMask()"", ""HW_SetALEventMask()"" to ecatalv.c"<br>
|
||||
V5.11 MBX2: "check in IP if enough dynamic memory is available to handle mailbox communication, if it is not the case the mbx error ""no memory"" is returned on any mbx request"<br>
|
||||
V5.11 MBX3: set application triggered emergency and EoE data to pending if no mailbox queue is supported and another mailbox request is currently handled, Handle only one mailbox request at a time (in case that MAILBPX_QUEUE is disabled)<br>
|
||||
V5.11 TEST4: add new mailbox test behaviour (the master mailbox cnt shall be incremented by 1 and the slave mailbox cnt is alternating)<br>
|
||||
<br>Changes to version V5.01:<br>
|
||||
V5.10 EOE3: Change local send frame pending indication variable to a global variable (it need to be resetted if the mailbox is stopped and a frame is pending)<br>
|
||||
V5.10 ESC6: Update SyncManager register size (only for 8Bit ESC access)<br>
|
||||
V5.10 MBX2: Allocate only memory for current configured mailbox size (max supported size was allocated before)<br>
|
||||
<br>Changes to version V5.0:<br>
|
||||
V5.01 ESC2: Add missed value swapping<br>
|
||||
V5.01 MBX2: Return an error if length specified in mailbnox header is too large.<br>
|
||||
<br>Changes to version V4.40:<br>
|
||||
V5.0 ECAT3: Global dummy variables used for dummy ESC operations.<br>
|
||||
V5.0 ESC1: ESC 32Bit Access added.<br>
|
||||
V5.0 MBX2: Prevent double swapping (MBX repeat service) of MBX length field.<br>
|
||||
V5.0 MBX4: Unlock SM buffer based on SM state.<br>
|
||||
<br>Changes to version V4.11:<br>
|
||||
V4.40 MBX8: Handle pending mailbox data even if mailbox queue is enabled<br>
|
||||
V4.40 MBX7: Reset SyncManger 1 Repeat Acknowledge bit if local variable is reset<br>
|
||||
V4.40 EMCY2: Send queued emergency messages<br>
|
||||
V4.40 BOOT2: Support only FoE in Bootstrap state<br>
|
||||
V4.40 MBX5: Prevent multiple free buffer<br>
|
||||
V4.40 MBX6: change return value if no mailbox buffer is available<br>
|
||||
<br>Changes to version V4.08:<br>
|
||||
V4.11 MBX 1: free some dynamic memory to avoid memory leaks in the mailbox queue<br>
|
||||
<br>Changes to version V4.07:<br>
|
||||
V4.10 MBX 1: check Mailbox message buffer and print emergency messages<br>
|
||||
during state change Init -> PreOP<br>
|
||||
<br>Changes to version V4.07:<br>
|
||||
V4.08 MBX 1: The mailbox queue was added (switch MAILBOX_QUEUE), if the<br>
|
||||
mailbox should be handled in the ESC-Interrupt Service Routine<br>
|
||||
the defines ENTER_MBX_CRITICAL and LEAVE_MBX_CRITICAL could be used<br>
|
||||
to disable the interrupts or change the priority<br>
|
||||
V4.08 AOE 1: AoE was added<br>
|
||||
<br>Changes to version V4.06:<br>
|
||||
V4.07 ECAT 1: The sources for SPI and MCI were merged (in ecat_def.h<br>
|
||||
set the switch MCI_HW to 1 when using the MCI,<br>
|
||||
set the switch SPI_HW to 1 when using the SPI
|
||||
*/
|
||||
|
||||
|
||||
@@ -133,6 +91,8 @@ V4.07 ECAT 1: The sources for SPI and MCI were merged (in ecat_def.h<br>
|
||||
|
||||
#include "ecatcoe.h"
|
||||
|
||||
#include <stdio.h> /* printf: SM 几何交叉校验 WARN(见 MBX_StartMailboxHandler) */
|
||||
|
||||
/*--------------------------------------------------------------------------------------
|
||||
------
|
||||
------ internal Types and Defines
|
||||
@@ -282,24 +242,41 @@ void MBX_Init(void)
|
||||
UINT16 MBX_StartMailboxHandler(void)
|
||||
{
|
||||
UINT16 result = 0;
|
||||
/* get address of the receive mailbox sync manager (SM0) */
|
||||
/* 邮箱几何构建期常量(ecat_def.h:MBX_SM0/1_START/SIZE)直达缓存,
|
||||
* 不信任 GetSyncMan/CSR 单次间接读——CSR 读降级为交叉校验:
|
||||
* 失配打 WARN(说明 ESC 寄存器被非常规改写,需查)但缓存仍取常量。
|
||||
* 运行期内存踩踏也会在每次 INIT→PREOP 被常量覆写。
|
||||
* 注意:ESC 寄存器经 PDI/CSR 读出即主机序(与 GetSyncMan 既有路径一致,勿加
|
||||
* SWAPWORD;PRAM 帧数据才是线上大端,那边交换是对的)。 */
|
||||
/*ECATCHANGE_START(V5.11) HW1*/
|
||||
TSYNCMAN ESCMEM * pSyncMan = (TSYNCMAN ESCMEM *)GetSyncMan(MAILBOX_WRITE);
|
||||
/*ECATCHANGE_END(V5.11) HW1*/
|
||||
if (pSyncMan->PhysicalStartAddress != MBX_SM0_START || pSyncMan->Length != MBX_SM0_SIZE)
|
||||
{
|
||||
printf("[ECAT][MBX] WARN: SM0 reg 0x%04X/%u != build-time 0x%04X/%u -> keep build-time\r\n",
|
||||
(unsigned)pSyncMan->PhysicalStartAddress, (unsigned)pSyncMan->Length,
|
||||
(unsigned)MBX_SM0_START, (unsigned)MBX_SM0_SIZE);
|
||||
}
|
||||
/* store size of the receive mailbox */
|
||||
u16ReceiveMbxSize = pSyncMan->Length;
|
||||
u16ReceiveMbxSize = MBX_SM0_SIZE;
|
||||
/* store the address of the receive mailbox */
|
||||
u16EscAddrReceiveMbx = pSyncMan->PhysicalStartAddress;
|
||||
u16EscAddrReceiveMbx = MBX_SM0_START;
|
||||
|
||||
/* get address of the send mailbox sync manager (SM1) */
|
||||
/*ECATCHANGE_START(V5.11) HW1*/
|
||||
pSyncMan =(TSYNCMAN ESCMEM *) GetSyncMan(MAILBOX_READ);
|
||||
/*ECATCHANGE_END(V5.11) HW1*/
|
||||
if (pSyncMan->PhysicalStartAddress != MBX_SM1_START || pSyncMan->Length != MBX_SM1_SIZE)
|
||||
{
|
||||
printf("[ECAT][MBX] WARN: SM1 reg 0x%04X/%u != build-time 0x%04X/%u -> keep build-time\r\n",
|
||||
(unsigned)pSyncMan->PhysicalStartAddress, (unsigned)pSyncMan->Length,
|
||||
(unsigned)MBX_SM1_START, (unsigned)MBX_SM1_SIZE);
|
||||
}
|
||||
|
||||
/* store the size of the send mailbox */
|
||||
u16SendMbxSize = pSyncMan->Length;
|
||||
u16SendMbxSize = MBX_SM1_SIZE;
|
||||
/* store the address of the send mailbox */
|
||||
u16EscAddrSendMbx = pSyncMan->PhysicalStartAddress;
|
||||
u16EscAddrSendMbx = MBX_SM1_START;
|
||||
|
||||
// HBu 02.05.06: it should be checked if there are overlaps in the sync manager areas
|
||||
if ((u16EscAddrReceiveMbx + u16ReceiveMbxSize) > u16EscAddrSendMbx && (u16EscAddrReceiveMbx < (u16EscAddrSendMbx + u16SendMbxSize)))
|
||||
|
||||
@@ -887,14 +887,15 @@ UINT8 OBJ_Read( UINT16 index, UINT8 subindex, UINT32 objSize, OBJCONST TOBJECT O
|
||||
p = (CHAR **) pVarPtr;
|
||||
pVarPtr = (UINT16 MBXMEM *)p[subindex-1];
|
||||
|
||||
if((((UINT16)pVarPtr) & 0x1) == 0x1)
|
||||
if((((UINT32)pVarPtr) & 0x1) == 0x1)
|
||||
{
|
||||
/*enum is stored at an odd address*/
|
||||
UINT16 cnt = 0;
|
||||
|
||||
//get last even WORD address
|
||||
/*ECATCHANGE_START(V5.11) SDO3*/
|
||||
pVarPtr = (UINT16 MBXMEM *)(((MEM_ADDR)pVarPtr)& ~(MEM_ADDR)0x1);
|
||||
/*MEM_ADDR=UINT16 会把 32 位指针截成 16 位,必须用完整位宽清 bit0*/
|
||||
pVarPtr = (UINT16 MBXMEM *)(((UINT32)pVarPtr) & ~(UINT32)0x1);
|
||||
/*ECATCHANGE_END(V5.11) SDO3*/
|
||||
|
||||
for(cnt = 0; cnt < (size / 2);cnt++)
|
||||
|
||||
@@ -1025,7 +1025,8 @@ UINT8 SDOS_SdoInfoInd( TSDOINFORMATION MBXMEM *pSdoInfoInd )
|
||||
UINT16 n = OBJ_GetNoOfObjects(i);
|
||||
|
||||
/* copy the number of objects of the list type in the SDO Information response */
|
||||
((UINT16 MBXMEM *) &pSdoInfoInd->CoeHeader)[(SIZEOF_SDOINFOLISTSTRUCT>>1)+i] = SWAPWORD(n);
|
||||
/*(UINT32) 中转:对 packed 成员直接取址强转 UINT16 指针会报 -Waddress-of-packed-member(邮箱缓冲实际偶对齐)*/
|
||||
((UINT16 MBXMEM *)(UINT32)&pSdoInfoInd->CoeHeader)[(SIZEOF_SDOINFOLISTSTRUCT>>1)+i] = SWAPWORD(n);
|
||||
}
|
||||
|
||||
/* size of the mailbox service response */
|
||||
@@ -1045,7 +1046,7 @@ UINT8 SDOS_SdoInfoInd( TSDOINFORMATION MBXMEM *pSdoInfoInd )
|
||||
size = u16SendMbxSize - SIZEOF_SDOINFO - MBX_HEADER_SIZE;
|
||||
}
|
||||
/* initialize pData with the pointer where the fragment has to be copied */
|
||||
pData = &((UINT16 MBXMEM *) &pSdoInfoInd->CoeHeader)[SIZEOF_SDOINFO>>1];
|
||||
pData = &((UINT16 MBXMEM *)(UINT32)&pSdoInfoInd->CoeHeader)[SIZEOF_SDOINFO>>1];
|
||||
/* initialize index with the next index to be sent */
|
||||
index = nSdoInfoIndex;
|
||||
/* decrement the number of fragments to be sent */
|
||||
@@ -1063,7 +1064,7 @@ UINT8 SDOS_SdoInfoInd( TSDOINFORMATION MBXMEM *pSdoInfoInd )
|
||||
size = u16SendMbxSize - SIZEOF_SDOINFOLISTSTRUCT - MBX_HEADER_SIZE;
|
||||
}
|
||||
/* initialize pData with the pointer where the fragment has to be copied */
|
||||
pData = &((UINT16 MBXMEM *) &pSdoInfoInd->CoeHeader)[SIZEOF_SDOINFOLISTSTRUCT>>1];
|
||||
pData = &((UINT16 MBXMEM *)(UINT32)&pSdoInfoInd->CoeHeader)[SIZEOF_SDOINFOLISTSTRUCT>>1];
|
||||
/*Check if List need to be send in fragments*/
|
||||
if( (n<<1) > size)
|
||||
{
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include "device/canfd.h"
|
||||
#include "sys/config.h"
|
||||
#include "sys/critical.h"
|
||||
#include "sys/cpu_load.h"
|
||||
#include "sys/systick.h"
|
||||
#include "sys/task_manager.h"
|
||||
#include <cstring>
|
||||
@@ -18,6 +19,125 @@ static inline void spinDelay(uint32_t loops) {
|
||||
for (volatile uint32_t d = 0; d < loops; ++d) { }
|
||||
}
|
||||
|
||||
/** @brief DWT CYCCNT 忙等微秒(ISR 安全)。
|
||||
* @note setTxSilent 退 Silent 时要等收发器 tMODE=20µs;本函数可能在 SPI CS
|
||||
* EXTI ISR 内被 0xC8 handler 调用,而 SysTickTimer::delayMs 依赖 SysTick
|
||||
* 中断计数,在该上下文会被冻死(同 esc_lan9252.cpp 的 pmBusyWaitUs)。
|
||||
* **不得复位 CYCCNT**:cpu_load / ethercat_diff_stats / SSC 软件定时器都拿它
|
||||
* 当时间基,清零会让它们的窗口计算瞬时报错。只在未使能时补使能位。 */
|
||||
static inline void canBusyWaitUs(uint32_t us)
|
||||
{
|
||||
if ((DWT->CTRL & DWT_CTRL_CYCCNTENA_Msk) == 0U) {
|
||||
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
|
||||
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
|
||||
}
|
||||
const uint32_t start = DWT->CYCCNT;
|
||||
const uint32_t cycles = us * (SystemCoreClock / 1000000U);
|
||||
/* 有界自旋:DWT 因故没在跑时不能变成无限等(最多多转 ~1ms 的圈数) */
|
||||
const uint32_t spin_cap = cycles + (SystemCoreClock / 1000U);
|
||||
for (uint32_t i = 0U; i < spin_cap; ++i) {
|
||||
if ((DWT->CYCCNT - start) >= cycles) break;
|
||||
}
|
||||
}
|
||||
|
||||
/* DLENC>8 的长度映射,与厂商库 g_can_fdlength_table 一致 */
|
||||
constexpr uint8_t kFdLenTable[7] = { 12U, 16U, 20U, 24U, 32U, 48U, 64U };
|
||||
|
||||
/** 释放 FIFO0 邮箱并等待 RFD0 由硬件自动清 0(手册 26.3.4 第 3 步)。
|
||||
*
|
||||
* 两处与厂商库 can_message_receive() 的差别,都是它漏掉的:
|
||||
* 1) 用**直接赋值**而不是 `|=`。CAN_RFIFO0 里 RFF0(bit3)/RFO0(bit4) 是 rc_w1
|
||||
* (写 1 清除),读-改-写会把这期间硬件刚置起来的"FIFO 满/溢出"标志顺手
|
||||
* 清掉 —— 溢出证据就是这么丢的。写 0 对 rc_w1 位无副作用。
|
||||
* 2) 置位后**等它自清**。手册原文:"将 RFD 置 1 释放邮箱,并且等待其由硬件
|
||||
* 自动清 0"。不等就接着读下一条,会撞上 dequeue 尚未完成的邮箱窗口。
|
||||
* @return true=已确认释放完成;false=超时(异常,计数后照常继续,不死等)。 */
|
||||
static inline bool releaseFifo0(uint32_t canPeriph)
|
||||
{
|
||||
CAN_RFIFO0(canPeriph) = CAN_RFIFO0_RFD0;
|
||||
for (uint32_t n = 0U; n < 2000U; ++n) {
|
||||
if ((CAN_RFIFO0(canPeriph) & CAN_RFIFO0_RFD0) == 0U) {
|
||||
/* 节点指纹实验表明拼接是 FIFO 缓冲指针与帧边界失同步
|
||||
* (STATUS 帧头 24B = 完整 CONFIG,错位恰为 CONFIG 帧长 6 字),
|
||||
* 不是延迟能挡的;结构矛盾的帧由 handleRxInterrupt 的内容校验计数
|
||||
* (只计不丢 —— 管道必须诚实;源头修复见 CONFIG_IRQ_PREEMPT_CAN_RX)。 */
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** 取一帧 FIFO0 报文,并校验取数期间硬件没有换帧。
|
||||
*
|
||||
* 为什么自己实现(不用厂商库 can_message_receive):一条 CANFD 报文要对同一个
|
||||
* 地址 CAN_RFIFOMDATA0 连读 dlen/4 次才取完(48B = 12 次),加上头部两次,一条
|
||||
* 帧就是十几次有先后依赖的总线访问,整个过程**不是原子的**。若这期间硬件把下
|
||||
* 一帧搬进邮箱窗口,就会读出"前几个字属于上一帧、后几个字属于这一帧"的拼接
|
||||
* 体,而 id/dlc 取自最开始那次读 —— 表现为 STATUS 帧的 ID 配上 CONFIG 帧
|
||||
* 的前 12~24 字节,且混合边界 100% 落在 4 字节边界上。
|
||||
*
|
||||
* 判据用 MP 寄存器:TS[31:16] 是硬件接收时间戳、DLENC[3:0] 是长度,同一帧内两
|
||||
* 者恒定。取数前后各读一次,只要有一个变了就说明窗口内容已不是原来那帧,整帧
|
||||
* 作废(计入 rx_torn_count)—— 宁可丢一帧,也不把拼接数据递上去。
|
||||
*
|
||||
* @return true=取到一帧完整报文;false=本帧判为撕裂、应丢弃(邮箱仍需释放)。 */
|
||||
static bool receiveFrameChecked(uint32_t canPeriph, can_receive_message_struct *msg)
|
||||
{
|
||||
/* 整段取数放进 BASEPRI 临界区:屏蔽 preempt>=1,把 EtherCAT 的 ESC(1)/SYNC1(1)
|
||||
* 挡在外面。取数是十几次有先后依赖的寄存器读,被抢占就会读出跨帧拼接体。
|
||||
* 本 ISR 自身是 preempt=0(CONFIG_IRQ_PREEMPT_CAN_RX),与 SYNC0
|
||||
* 同级互不抢占,取数真正原子;MI/MP 回读校验保留,作为对硬件自发换帧
|
||||
* (dequeue 异步完成等)的兜底观测 —— rx_torn 计数即"该病理是否还在发生"。 */
|
||||
const uint32_t basepri = RT_CRITICAL_SAVE();
|
||||
|
||||
const uint32_t mi0 = CAN_RFIFOMI(canPeriph, CAN_FIFO0);
|
||||
const uint32_t mp0 = CAN_RFIFOMP(canPeriph, CAN_FIFO0);
|
||||
|
||||
msg->rx_ff = static_cast<uint8_t>(CAN_RFIFOMI_FF & mi0);
|
||||
if (CAN_FF_STANDARD == msg->rx_ff) {
|
||||
msg->rx_sfid = static_cast<uint32_t>(GET_RFIFOMI_SFID(mi0));
|
||||
} else {
|
||||
msg->rx_efid = static_cast<uint32_t>(GET_RFIFOMI_EFID(mi0));
|
||||
}
|
||||
msg->rx_ft = static_cast<uint8_t>(CAN_RFIFOMI_FT & mi0);
|
||||
msg->rx_fi = static_cast<uint8_t>(GET_RFIFOMP_FI(mp0));
|
||||
msg->fd_flag = static_cast<uint8_t>((CAN_RFIFOMP_FDF & mp0) >> 7);
|
||||
msg->fd_brs = static_cast<uint8_t>((mp0 & CAN_RFIFOMP_BRS) >> 5);
|
||||
msg->fd_esi = static_cast<uint8_t>((mp0 & CAN_RFIFOMP_ESI) >> 4);
|
||||
|
||||
const uint8_t dlenc = static_cast<uint8_t>(GET_RFIFOMP_DLENC(mp0));
|
||||
const uint8_t dlen = (dlenc <= 8U) ? dlenc
|
||||
: ((dlenc <= 15U) ? kFdLenTable[dlenc - 9U] : 64U);
|
||||
msg->rx_dlen = dlen;
|
||||
|
||||
/* 按字取数:与厂商库同样是对同一地址重复读(硬件读指针自动前进)。
|
||||
* 5~7 字节要多读一个字才取全,与厂商库口径一致。 */
|
||||
uint32_t words = static_cast<uint32_t>(dlen) / 4U;
|
||||
if ((1U == words) && (4U != dlen)) {
|
||||
words++;
|
||||
}
|
||||
if (0U == words) {
|
||||
words = 1U; /* dlen<4 也要读走一个字 */
|
||||
}
|
||||
for (uint32_t i = 0U; i < words; ++i) {
|
||||
const uint32_t w = CAN_RFIFOMDATA0(canPeriph, CAN_FIFO0);
|
||||
std::memcpy(&msg->rx_data[i * 4U], &w, sizeof(w));
|
||||
}
|
||||
|
||||
/* 取数后回读 MI+MP:任一变化 ⇒ 邮箱窗口在取数期间被换帧,这一帧是拼接体。
|
||||
* 必须连 MI(完整 ID)一起比 —— 只比 MP 的 TS/DLENC 会漏:TS 是 16 位计数器,
|
||||
* 相邻两帧可能同值,DLENC 也常常相同(都是 48),于是"ID 报 STATUS、数据却是
|
||||
* CONFIG"这种错配能从校验里溜过去。ID 变了就一定是
|
||||
* 换了帧,这条判据没有巧合空间。 */
|
||||
const uint32_t keep = CAN_RFIFOMP_TS | CAN_RFIFOMP_DLENC;
|
||||
const uint32_t mp1 = CAN_RFIFOMP(canPeriph, CAN_FIFO0);
|
||||
const uint32_t mi1 = CAN_RFIFOMI(canPeriph, CAN_FIFO0);
|
||||
const bool intact = ((mp1 & keep) == (mp0 & keep)) && (mi1 == mi0);
|
||||
|
||||
RT_CRITICAL_RESTORE(basepri);
|
||||
return intact;
|
||||
}
|
||||
|
||||
struct CanChannelConfig {
|
||||
uint32_t canPeriph;
|
||||
rcu_periph_enum canRcu;
|
||||
@@ -153,6 +273,49 @@ static void hardResetCan(uint32_t canPeriph, rcu_periph_enum canRcu)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief CAN TX 并发保护 RAII 守卫。
|
||||
*
|
||||
* 屏蔽 EXTI10_15_IRQn(SPI CS Assert/Release, Preempt 0)与该通道的 txIrq(Preempt 2),
|
||||
* 防止 EtherCAT PDI_Isr(Preempt 1)与 SPI CS EXTI ISR(Preempt 0)在操作 s_txQueues
|
||||
* 或硬件发送邮箱寄存器时产生并发撕裂与重入竞争。
|
||||
* 具备重入安全性:若某中断当前未使能(例如已被外层守卫屏蔽),退出时不会误使能。
|
||||
*/
|
||||
class CanTxConcurrencyGuard {
|
||||
public:
|
||||
explicit CanTxConcurrencyGuard(uint8_t channelIndex)
|
||||
: m_txIrq(kChannels[channelIndex].txIrq)
|
||||
{
|
||||
m_exti15_enabled = (NVIC_GetEnableIRQ(EXTI10_15_IRQn) != 0U);
|
||||
m_tx_enabled = (NVIC_GetEnableIRQ(m_txIrq) != 0U);
|
||||
|
||||
if (m_exti15_enabled) {
|
||||
NVIC_DisableIRQ(EXTI10_15_IRQn);
|
||||
}
|
||||
if (m_tx_enabled) {
|
||||
NVIC_DisableIRQ(m_txIrq);
|
||||
}
|
||||
__DSB();
|
||||
__ISB();
|
||||
}
|
||||
|
||||
~CanTxConcurrencyGuard() {
|
||||
if (m_tx_enabled) {
|
||||
NVIC_EnableIRQ(m_txIrq);
|
||||
}
|
||||
if (m_exti15_enabled) {
|
||||
NVIC_EnableIRQ(EXTI10_15_IRQn);
|
||||
}
|
||||
__DSB();
|
||||
__ISB();
|
||||
}
|
||||
|
||||
private:
|
||||
IRQn_Type m_txIrq;
|
||||
bool m_exti15_enabled;
|
||||
bool m_tx_enabled;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
CanFd::Diag CanFd::s_diag{};
|
||||
@@ -165,6 +328,8 @@ CanFd::ChannelFeedback CanFd::s_feedback[3]{};
|
||||
uint32_t CanFd::s_feedbackSeq = 0U;
|
||||
CanFd::IdSnapshotTable CanFd::s_idSnapshot[3]{};
|
||||
bool CanFd::s_channelHwOk[3]{};
|
||||
bool CanFd::s_brsSuppress[3]{};
|
||||
volatile uint8_t CanFd::s_txSilent[3]{};
|
||||
|
||||
/* TX 错误事件标志(ARD=1 单发丢帧通知):TX 完成 ISR 置位,主循环
|
||||
* consumeTxError() 读清;单写单读,无锁。 */
|
||||
@@ -273,7 +438,14 @@ bool CanFd::initNominalTiming(uint32_t canPeriph)
|
||||
can_parameter.auto_bus_off_recovery = ENABLE;
|
||||
can_parameter.auto_wake_up = DISABLE;
|
||||
can_parameter.auto_retrans = ENABLE; /* 默认自动重发兜底偶发 ACK 丢失;OTA 停等协议期间由 setChannelAutoRetrans(ch,false) 临时关闭,避免重发打穿电机单缓冲 RX */
|
||||
can_parameter.rec_fifo_overwrite = ENABLE;
|
||||
/* rec_fifo_overwrite = DISABLE:FIFO 满时丢新帧而非覆盖最旧邮箱。
|
||||
* 覆盖最旧会伤及 CPU 正在读取的输出窗口。buildInput 在 PDI_Isr(preempt1)
|
||||
* 占 ~357µs 会压住 CAN ISR(preempt2),发指令时电机 CONFIG 翻倍到 2kHz/节点、
|
||||
* 总线 ~8kHz,阻塞窗口内到达 ~3 帧恰好打满 FIFO 深度 → 覆盖进行到一半被读走
|
||||
* = 跨节点/跨类型拼接帧。DISABLE 后溢出丢新帧并置 RFO0
|
||||
* (rx_fifo_overfull_count 已在统计):STATUS/CONFIG 均周期量,丢一拍下拍
|
||||
* 即补;覆盖递上来的才是网页乱码。与本文件软件队列"满丢新不覆旧"同则。 */
|
||||
can_parameter.rec_fifo_overwrite = DISABLE;
|
||||
can_parameter.trans_fifo_order = ENABLE;
|
||||
can_parameter.working_mode = CAN_NORMAL_MODE;
|
||||
can_parameter.resync_jump_width = CONFIG_CAN_NOMINAL_SJW;
|
||||
@@ -288,7 +460,7 @@ bool CanFd::initNominalTiming(uint32_t canPeriph)
|
||||
* @param[in] canPeriph 目标 CAN 外设实例。
|
||||
* @param[out] 无。
|
||||
* @retval 无返回值。
|
||||
* @note 时钟源为 APB1=90MHz;按 CONFIG_CAN_DATA_* 宏配置,目标 2.0 Mbps。
|
||||
* @note 时钟源为 APB1=90MHz;按 CONFIG_CAN_DATA_* 宏配置,目标 5.0 Mbps。
|
||||
* 实际数据段波特率 = 90MHz / (CONFIG_CAN_DATA_PRESCALER × (1+BS1+BS2))。
|
||||
* BRS 受 CONFIG_CAN_FD_BRS_ENABLE 控制。
|
||||
*/
|
||||
@@ -302,6 +474,9 @@ bool CanFd::initFdMode(uint32_t canPeriph)
|
||||
can_fd_parameter.fd_frame = ENABLE;
|
||||
can_fd_parameter.excp_event_detect = ENABLE;
|
||||
can_fd_parameter.delay_compensation = ENABLE;
|
||||
/* TDCF/TDCO 不随数据波特率等比缩放:TDCMOD=CALC_AND_OFFSET 下硬件已自动测得
|
||||
* 回路延时(TDCV),这里只是微调,经验取 TDCO ≈ 数据段 BS1 的 tq 数(4TQ→4)。
|
||||
* 放到 12 会让本节点回采时错开约 6 个数据位,自己发出的 FD+BRS 帧被判成错误帧。 */
|
||||
can_fd_tdc_parameter.tdc_filter = 0x04U;
|
||||
can_fd_tdc_parameter.tdc_mode = CAN_TDCMOD_CALC_AND_OFFSET;
|
||||
can_fd_tdc_parameter.tdc_offset = 0x04U;
|
||||
@@ -339,7 +514,8 @@ void CanFd::initChannelHardware(uint8_t i)
|
||||
}
|
||||
s_channelHwOk[i] = false;
|
||||
rcu_periph_clock_enable(ch.canRcu);
|
||||
gpio_bit_reset(ch.slpPort, ch.slpPin); /* STB=0 -> Normal */
|
||||
gpio_bit_reset(ch.slpPort, ch.slpPin); /* S=0 → Normal(静默不跨重启,上电一律放开发送) */
|
||||
s_txSilent[i] = 0U;
|
||||
const uint8_t rxLevelBefore = (gpio_input_bit_get(ch.rxPort, ch.rxPin) != RESET) ? 1U : 0U;
|
||||
const uint8_t stbLevel = (gpio_output_bit_get(ch.slpPort, ch.slpPin) != RESET) ? 1U : 0U;
|
||||
|
||||
@@ -421,11 +597,11 @@ void CanFd::initChannelHardware(uint8_t i)
|
||||
CAN_TSTAT(ch.canPeriph) = CAN_TSTAT_MTF0 | CAN_TSTAT_MTF1 | CAN_TSTAT_MTF2;
|
||||
can_interrupt_enable(ch.canPeriph, CAN_INT_TME | CAN_INT_RFNE0 | CAN_INT_RFF0 | CAN_INT_RFO0);
|
||||
nvic_irq_enable(ch.txIrq, CONFIG_IRQ_PREEMPT_CAN, static_cast<uint8_t>(i));
|
||||
nvic_irq_enable(ch.rx0Irq, CONFIG_IRQ_PREEMPT_CAN, static_cast<uint8_t>(i));
|
||||
/* RX 提到 preempt=0 —— 见 sys/config.h CONFIG_IRQ_PREEMPT_CAN_RX 注释。 */
|
||||
nvic_irq_enable(ch.rx0Irq, CONFIG_IRQ_PREEMPT_CAN_RX, static_cast<uint8_t>(i));
|
||||
|
||||
s_rxQueues[i].head = 0U;
|
||||
s_rxQueues[i].tail = 0U;
|
||||
s_rxQueues[i].count = 0U;
|
||||
s_txQueues[i].head = 0U;
|
||||
s_txQueues[i].tail = 0U;
|
||||
s_txQueues[i].count = 0U;
|
||||
@@ -788,7 +964,7 @@ void CanFd::init()
|
||||
|
||||
/**
|
||||
* @brief L0 轻量自检:无总线发帧,仅校验使能通道控制器自身状态(GD32 侧故障)。
|
||||
* @note 1.5.35:不再检查 BUS_OFF/TEC/REC——未接从站的通道 RX 浮空噪声会累积 REC,
|
||||
* @note 不检查 BUS_OFF/TEC/REC——未接从站的通道 RX 浮空噪声会累积 REC,
|
||||
* 「没接设备」不等于错误;总线侧健康由 0xC1 反馈帧运行期上报。
|
||||
*/
|
||||
bool CanFd::selfTestL0(uint8_t *fault_code, uint8_t *fault_channel)
|
||||
@@ -866,23 +1042,20 @@ CanFd::Mode CanFd::mode()
|
||||
* @param[in] 无。
|
||||
* @param[out] 无。
|
||||
* @retval bool true 表示至少一条接收队列非空,false 表示当前三路均无待消费报文。
|
||||
* @note 无锁检查 head != tail,免关中断。
|
||||
*/
|
||||
bool CanFd::hasRxFrame()
|
||||
{
|
||||
bool found = false;
|
||||
uint32_t save = RT_CRITICAL_SAVE();
|
||||
for(size_t i = 0; i < kChannelCount; ++i) {
|
||||
if(s_rxQueues[i].count != 0U) {
|
||||
found = true;
|
||||
break;
|
||||
if(s_rxQueues[i].head != s_rxQueues[i].tail) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
RT_CRITICAL_RESTORE(save);
|
||||
return found;
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 轮转读取并消费一帧接收报文。
|
||||
* @brief 轮转读取并消费一帧接收报文(Lock-Free SPSC 消费者端)。
|
||||
* @param[in] 无。
|
||||
* @param[out] id 输出报文标识符;允许为空。
|
||||
* @param[out] data 输出报文数据缓存;允许为空。
|
||||
@@ -890,42 +1063,37 @@ bool CanFd::hasRxFrame()
|
||||
* @param[out] channel 输出报文来源通道号;允许为空。
|
||||
* @retval bool true 表示成功消费一帧报文,false 表示当前三路接收队列均为空。
|
||||
* @note 消费顺序采用从 `s_nextConsumeChannel` 开始的轮转扫描,避免长期偏向固定通道。
|
||||
* 基于 SPSC 无锁环形缓冲区,消费端读取 payload 并执行 __DMB() 后推进 head,
|
||||
* 无需关中断或依赖 BASEPRI,彻底消除与 Preempt 0 RX ISR 的并发撕裂。
|
||||
*/
|
||||
bool CanFd::consumeRxFrame(uint32_t *id, uint8_t *data, uint8_t *length, uint8_t *channel)
|
||||
{
|
||||
RxFrame localFrame{};
|
||||
bool found = false;
|
||||
|
||||
uint32_t save = RT_CRITICAL_SAVE();
|
||||
for(size_t n = 0; n < kChannelCount; ++n) {
|
||||
const uint8_t idx = static_cast<uint8_t>((s_nextConsumeChannel + n) % kChannelCount);
|
||||
auto &queue = s_rxQueues[idx];
|
||||
if(queue.count == 0U) {
|
||||
const uint8_t head = queue.head;
|
||||
const uint8_t tail = queue.tail;
|
||||
|
||||
if(head == tail) {
|
||||
continue;
|
||||
}
|
||||
|
||||
localFrame = queue.frames[queue.head];
|
||||
queue.head = static_cast<uint8_t>((queue.head + 1U) % kRxQueueDepth);
|
||||
queue.count--;
|
||||
s_nextConsumeChannel = static_cast<uint8_t>((idx + 1U) % kChannelCount);
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
RT_CRITICAL_RESTORE(save);
|
||||
|
||||
if(!found) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if(channel) *channel = localFrame.channel;
|
||||
if(id) *id = localFrame.id;
|
||||
if(length) *length = localFrame.length;
|
||||
if(data) {
|
||||
for(uint8_t i = 0; i < localFrame.length; ++i) {
|
||||
data[i] = localFrame.data[i];
|
||||
const auto &src = queue.frames[head];
|
||||
if(channel) *channel = src.channel;
|
||||
if(id) *id = src.id;
|
||||
if(length) *length = src.length;
|
||||
if(data && src.length > 0U) {
|
||||
for(uint8_t i = 0; i < src.length; ++i) {
|
||||
data[i] = src.data[i];
|
||||
}
|
||||
}
|
||||
|
||||
__DMB();
|
||||
queue.head = static_cast<uint8_t>((head + 1U) & kRxQueueMask);
|
||||
s_nextConsumeChannel = static_cast<uint8_t>((idx + 1U) % kChannelCount);
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -951,6 +1119,15 @@ CanFd::CanTxResult CanFd::transmitFrame(uint8_t channelIndex, uint32_t ext_id,
|
||||
if (dlc > 0U && data == nullptr) {
|
||||
return CanTxResult::kErrInvalid;
|
||||
}
|
||||
/* 收发器 Silent(只听不发):本端主动配置的发送禁能,不是总线状态。
|
||||
* 与下面「BUS_OFF/TEC 不做软件拦截」的约定不冲突——那两条是粘性总线侧标志会误判,
|
||||
* 这条是 s_txSilent 单一真源,且驱动器已关断:硬发只会让控制器白涨 TEC。
|
||||
* 拒绝路径不碰任何计数/诊断,静默期间的拒发不是错误。 */
|
||||
if (s_txSilent[channelIndex] != 0U) {
|
||||
return CanTxResult::kErrSilent;
|
||||
}
|
||||
|
||||
CanTxConcurrencyGuard guard(channelIndex);
|
||||
|
||||
/* 诊断:捕获任何通过 transmitFrame 发 0x100 DLC=16 的帧(排查幽灵帧来源) */
|
||||
if (ext_id == 0x00000100U && dlc == 16U) {
|
||||
@@ -984,9 +1161,13 @@ CanFd::CanTxResult CanFd::transmitFrame(uint8_t channelIndex, uint32_t ext_id,
|
||||
tx_msg.tx_ft = CAN_FT_DATA;
|
||||
tx_msg.tx_dlen = dlc;
|
||||
#ifdef GD32E508
|
||||
tx_msg.fd_flag = (dlc > 8U) ? ENABLE : DISABLE;
|
||||
/* 只要通道硬件已启用 CAN-FD(3路均由 initFdMode 开启),统一发 FD 帧;
|
||||
* 避免 DLC<=8 时回退经典 CAN 导致对端电机 DMA / 报文解析异常。 */
|
||||
tx_msg.fd_flag = ENABLE;
|
||||
#if (CONFIG_CAN_FD_BRS_ENABLE != 0)
|
||||
tx_msg.fd_brs = (dlc > 8U) ? ENABLE : DISABLE;
|
||||
/* 电机 BOOT 收不了 BRS 数据段,OTA 期间 relay 对该通道压制(setChannelBrsSuppress),
|
||||
* 压制中回退仲裁段速率(FD-no-BRS,OTA 长期验证过的组合)。日常发送统一启用 BRS 高速段。 */
|
||||
tx_msg.fd_brs = (!s_brsSuppress[channelIndex]) ? ENABLE : DISABLE;
|
||||
#else
|
||||
tx_msg.fd_brs = DISABLE;
|
||||
#endif
|
||||
@@ -1060,9 +1241,19 @@ CanFd::CanTxResult CanFd::enqueueTx(uint8_t channelIndex, uint32_t ext_id,
|
||||
if (dlc > 0U && data == nullptr) {
|
||||
return CanTxResult::kErrInvalid;
|
||||
}
|
||||
/* 静默通道在入队口就拒掉:否则帧会进软件队列,等 TX 完成中断 drainTxQueueIsr
|
||||
* 调 transmitFrame 时才被拒,那一轮会把 head/count 推进并静默丢帧,
|
||||
* 表面上"已受理"实则必丢。 */
|
||||
if (s_txSilent[channelIndex] != 0U) {
|
||||
return CanTxResult::kErrSilent;
|
||||
}
|
||||
|
||||
TxQueue &q = s_txQueues[channelIndex];
|
||||
|
||||
/* 关中断并发保护:屏蔽 EXTI10_15_IRQn(SPI burst, Preempt 0)与 txIrq(Preempt 2),
|
||||
* 解决 PDI_Isr(Preempt 1)与 SPI CS release(Preempt 0)并发访问 s_txQueues 的竞态。 */
|
||||
CanTxConcurrencyGuard guard(channelIndex);
|
||||
|
||||
/* 快速路径:队列空 → 直通硬件邮箱。装得下立即返回,不用等待 TX 完成中断。
|
||||
* ★ allow_abort=false:邮箱满时禁止 abort 旧帧顶替(那会把同通道已装载的
|
||||
* 旧帧静默丢弃,>3 帧同通道突发只剩最后 3 帧上线),一律落入软件队列,
|
||||
@@ -1075,16 +1266,9 @@ CanFd::CanTxResult CanFd::enqueueTx(uint8_t channelIndex, uint32_t ext_id,
|
||||
/* 邮箱满:落入下面的入队路径 */
|
||||
}
|
||||
|
||||
/* 关中段:EXTI15(SPI burst) 与 CANx TX ISR 并发,保护 head/tail/count。
|
||||
* 用 CMSIS NVIC 接口(SPL 的 nvic_irq_enable 是 3 参版)。
|
||||
* 关的是该通道自身的 txIrq,避免关全局影响其他通道的实时性。 */
|
||||
const IRQn_Type irqn = kChannels[channelIndex].txIrq;
|
||||
NVIC_DisableIRQ(irqn);
|
||||
|
||||
if (q.count >= kTxQueueDepth) {
|
||||
/* 队列也满了:彻底装不下。此时不能再返回 kOk,
|
||||
* 否则前端会误判成功;只能丢弃并返回 kErrMailbox。 */
|
||||
NVIC_EnableIRQ(irqn);
|
||||
s_diag.error_count++;
|
||||
s_channelStats[channelIndex].error_count++;
|
||||
return CanTxResult::kErrMailbox;
|
||||
@@ -1099,14 +1283,13 @@ CanFd::CanTxResult CanFd::enqueueTx(uint8_t channelIndex, uint32_t ext_id,
|
||||
q.tail = static_cast<uint8_t>((q.tail + 1U) % kTxQueueDepth);
|
||||
q.count++;
|
||||
|
||||
NVIC_EnableIRQ(irqn);
|
||||
return CanTxResult::kOk;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief CAN TX 完成中断末尾调用:用硬件邮箱空闲出的位置,把队列里的帧装回去。
|
||||
* @note 只在 ISR 内调用;关中断保护已在 transmitFrame 返回值判定外层完成,
|
||||
* 本函数本身不关中(邮箱空位是"单调减少再单调增加",多读无害)。
|
||||
* @note 由 CAN TX 完成 ISR 调用;使用 CanTxConcurrencyGuard 屏蔽 EXTI10_15 与 txIrq,
|
||||
* 防止出队期间被高优先级的 SPI CS release 抢占导致 head/count 撕裂。
|
||||
*/
|
||||
void CanFd::drainTxQueueIsr(uint8_t channelIndex)
|
||||
{
|
||||
@@ -1114,6 +1297,8 @@ void CanFd::drainTxQueueIsr(uint8_t channelIndex)
|
||||
return;
|
||||
}
|
||||
TxQueue &q = s_txQueues[channelIndex];
|
||||
CanTxConcurrencyGuard guard(channelIndex);
|
||||
|
||||
while (q.count > 0U) {
|
||||
TxQueuedFrame &f = q.frames[q.head];
|
||||
/* 复用 transmitFrame 的装载逻辑;allow_abort=false:这里邮箱刚空出来,
|
||||
@@ -1143,6 +1328,16 @@ uint32_t CanFd::txQueueBacklog(uint8_t channelIndex)
|
||||
return s_txQueues[channelIndex].count;
|
||||
}
|
||||
|
||||
uint32_t CanFd::rxQueueCount(uint8_t channelIndex)
|
||||
{
|
||||
if (channelIndex >= kChannelCount) {
|
||||
return 0U;
|
||||
}
|
||||
const uint8_t head = s_rxQueues[channelIndex].head;
|
||||
const uint8_t tail = s_rxQueues[channelIndex].tail;
|
||||
return static_cast<uint32_t>((tail - head) & kRxQueueMask);
|
||||
}
|
||||
|
||||
void CanFd::abortPendingTx(uint8_t channelIndex)
|
||||
{
|
||||
if (channelIndex >= kChannelCount || !kChannelEnabled[channelIndex]) {
|
||||
@@ -1181,6 +1376,60 @@ void CanFd::setChannelAutoRetrans(uint8_t channelIndex, bool enable)
|
||||
(CAN_CTL(canPeriph) & CAN_CTL_ARD) ? 1U : 0U);
|
||||
}
|
||||
|
||||
void CanFd::setChannelBrsSuppress(uint8_t channelIndex, bool suppress)
|
||||
{
|
||||
if (channelIndex >= kChannelCount || !kChannelEnabled[channelIndex]) {
|
||||
return;
|
||||
}
|
||||
/* 仅置运行时标志,打点在 transmitFrame:BRS 是逐帧属性(fd_brs 位),无需动寄存器。
|
||||
* 写方(relay enterState 钩子)与读方(主循环/CS 中断的 transmitFrame)间 bool 单写单读,原子。 */
|
||||
s_brsSuppress[channelIndex] = suppress;
|
||||
CAN_LOGI("[CAN] CH%u set brs_suppress=%u\r\n", (unsigned)channelIndex, (unsigned)suppress);
|
||||
}
|
||||
|
||||
bool CanFd::setTxSilent(uint8_t channelIndex, bool silent)
|
||||
{
|
||||
if (channelIndex >= kChannelCount || !kChannelEnabled[channelIndex]) {
|
||||
return false;
|
||||
}
|
||||
const auto &ch = kChannels[channelIndex];
|
||||
|
||||
if (silent) {
|
||||
/* 先立旗再动脚:并发路径(主循环/其它 ISR)调 transmitFrame 立刻被拒,
|
||||
* 不会在关驱动器的当口又塞一帧进邮箱。 */
|
||||
s_txSilent[channelIndex] = 1U;
|
||||
/* 驱动器即将关断:残留 PENDING 邮箱发不出也等不到 ACK,只会白涨 TEC,
|
||||
* 进静默前先把它们停掉。 */
|
||||
abortPendingTx(channelIndex);
|
||||
gpio_bit_set(ch.slpPort, ch.slpPin); /* S=1 → TCAN1057 Silent(驱动关断,接收照常) */
|
||||
} else {
|
||||
gpio_bit_reset(ch.slpPort, ch.slpPin); /* S=0 → Normal */
|
||||
/* 收发器模式建立时间 tMODE=20µs(手册 6.9):等够再放开发送门,
|
||||
* 否则紧随其后的第一帧会被驱动器忽略。 */
|
||||
canBusyWaitUs(20U);
|
||||
s_txSilent[channelIndex] = 0U;
|
||||
}
|
||||
|
||||
const uint8_t pinHigh = (gpio_output_bit_get(ch.slpPort, ch.slpPin) != RESET) ? 1U : 0U;
|
||||
CAN_LOGI("[CAN] CH%u tx_silent=%u (S pin=%u)\r\n", (unsigned)channelIndex,
|
||||
(unsigned)silent, (unsigned)pinHigh);
|
||||
/* 回读确认脚电平与意图一致;不一致就把标志回滚成原状态再报错,
|
||||
* 否则 isTxSilent()(发送门控 + 0xC1 bit5)会与物理脚电平各说一套。 */
|
||||
if (pinHigh != (silent ? 1U : 0U)) {
|
||||
s_txSilent[channelIndex] = silent ? 0U : 1U;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CanFd::isTxSilent(uint8_t channelIndex)
|
||||
{
|
||||
if (channelIndex >= kChannelCount) {
|
||||
return false;
|
||||
}
|
||||
return s_txSilent[channelIndex] != 0U;
|
||||
}
|
||||
|
||||
uint32_t CanFd::readChannelErrorReg(uint8_t channelIndex)
|
||||
{
|
||||
if (channelIndex >= kChannelCount || !kChannelEnabled[channelIndex]) {
|
||||
@@ -1291,7 +1540,104 @@ void CanFd::handleRxInterrupt(uint8_t channelIndex)
|
||||
|
||||
can_receive_message_struct rx_message{};
|
||||
can_struct_para_init(CAN_RX_MESSAGE_STRUCT, &rx_message);
|
||||
can_message_receive(ch.canPeriph, CAN_FIFO0, &rx_message);
|
||||
/* 自己取帧并校验(见 receiveFrameChecked):厂商库不校验也不等 RFD 自清,
|
||||
* 会把"半条上一帧 + 半条本帧"的拼接体当正常帧交上来。
|
||||
*
|
||||
* **校验失败只丢不重取**。RFIFOMDATA0 是"重复读同一地址、硬件读
|
||||
* 指针自动前进",取数失败时读指针已随取数前进若干字,
|
||||
* 重取会从错位指针开读 —— MI/MP 回读比对的是"窗口身份",
|
||||
* 管不住"指针相对窗口起点的错位",于是校验通过、内容却是"上一帧的尾巴
|
||||
* 拼下一帧的开头"。
|
||||
* 丢帧无害:STATUS/CONFIG 都是 1kHz 周期量,下拍即补;递上去才会变乱码。 */
|
||||
const bool intact = receiveFrameChecked(ch.canPeriph, &rx_message);
|
||||
if (!releaseFifo0(ch.canPeriph)) {
|
||||
s_diag.rx_release_timeout_count++;
|
||||
}
|
||||
if (!intact) {
|
||||
/* 校验判为拼接:整帧作废,不计 rx_count、不入队、不转发。 */
|
||||
s_diag.rx_torn_count++;
|
||||
continue;
|
||||
}
|
||||
|
||||
/* 本帧的 ID 用局部量,不要读 s_diag.last_rx_id ——三路通道共用那个全局,
|
||||
* 嵌套/交错时会串出"A 通道的 id 配 B 通道的数据"。 */
|
||||
const uint32_t rxId = (rx_message.rx_ff == CAN_FF_STANDARD)
|
||||
? rx_message.rx_sfid : rx_message.rx_efid;
|
||||
|
||||
/* ── 内容级拼接帧拦截 ──
|
||||
* MI/MP 回读校验管不住 FIFO 缓冲指针与帧边界的失同步
|
||||
* (指针病理,软件无法从取数时序上根除)。
|
||||
* 在入队前做结构自洽校验:拼接帧的 (id, data) 必矛盾,而真帧必自洽。
|
||||
* STATUS(0x000N0100, dlc48):CONFIG 头灌入必然把 MCU/Motor 温度推出
|
||||
* [-40,125]℃ 物理量程(CONFIG B0-1 = 03+限压低字节 → 123~250℃ 或
|
||||
* 负值),母线电压同步失序;
|
||||
* CONFIG(0x000N0101, dlc24):DBC v0.74 规定 B15-23 未使用恒 0,
|
||||
* STATUS 体灌入几乎必非 0;模式字节限 0..7(电机模式表最大 7)。
|
||||
* 命中即计数(不丢帧——管道必须诚实,计数仅做"源头是否
|
||||
* 还在产拼接帧"的测量):源头修复生效后本计数应恒 0,非 0 即失效告警。
|
||||
* 计数经段5@494 上报。
|
||||
* 残余风险:限压字节恰使温度落回量程内的理论组合
|
||||
* (24V 台架实测限压 ≥15V → 对应"温度"≥153℃,仍在量程外),可接受。 */
|
||||
{
|
||||
const uint16_t idLo = (uint16_t)(rxId & 0xFFFFU);
|
||||
bool reject = false;
|
||||
/* 上一帧已接受的温度/电压(按节点):拼接帧的 CONFIG 字节可能恰好落进
|
||||
* 静态量程,但绝不可能在 1ms 帧间隔里物理地跳过去 —— 斜率判据
|
||||
* 是静态量程补不上的洞。阈值留 3 个数量级余量:温度 20℃/帧(实际
|
||||
* ≤0.005℃),电压 5V/帧(实际 ≤0.05V)。 */
|
||||
static int16_t s_lastMcu[16];
|
||||
static int16_t s_lastMot[16];
|
||||
static uint16_t s_lastPwr[16];
|
||||
static uint8_t s_lastOk[16];
|
||||
if (idLo == 0x0100U && rx_message.rx_dlen == 48U) {
|
||||
const int16_t mcu = (int16_t)((uint16_t)rx_message.rx_data[0] |
|
||||
((uint16_t)rx_message.rx_data[1] << 8));
|
||||
const int16_t mot = (int16_t)((uint16_t)rx_message.rx_data[2] |
|
||||
((uint16_t)rx_message.rx_data[3] << 8));
|
||||
const uint16_t pwr = (uint16_t)rx_message.rx_data[11] |
|
||||
((uint16_t)rx_message.rx_data[12] << 8);
|
||||
if (mcu < -4000 || mcu > 12500 || mot < -4000 || mot > 12500 ||
|
||||
pwr < 8000 || pwr > 60000) {
|
||||
reject = true;
|
||||
}
|
||||
const uint8_t nd = (uint8_t)((rxId >> 16) & 0x0FU);
|
||||
if (!reject && nd >= 1U && nd < 16U && (s_lastOk[nd] & 1U)) {
|
||||
int16_t dmcu = (int16_t)(mcu - s_lastMcu[nd]);
|
||||
int16_t dmot = (int16_t)(mot - s_lastMot[nd]);
|
||||
if (dmcu < 0) { dmcu = (int16_t)-dmcu; }
|
||||
if (dmot < 0) { dmot = (int16_t)-dmot; }
|
||||
/* 只比温度斜率:拼接帧的 CONFIG 头必然把 mcu/mot 推出 20℃/帧
|
||||
* (实际温度 ≤0.005℃/帧,4 个数量级余量)。
|
||||
* 电压不比斜率 —— 正弦换向/堵转时母线真实跌落可超 5V/帧,
|
||||
* 误杀会让显示冻结;拼接的电压特征(≈48V)已由量程+温度斜率兜住。 */
|
||||
if (dmcu > 2000 || dmot > 2000) {
|
||||
reject = true;
|
||||
}
|
||||
}
|
||||
if (!reject && nd >= 1U && nd < 16U) {
|
||||
s_lastMcu[nd] = mcu;
|
||||
s_lastMot[nd] = mot;
|
||||
s_lastPwr[nd] = pwr;
|
||||
s_lastOk[nd] = 1U;
|
||||
}
|
||||
} else if (idLo == 0x0101U && rx_message.rx_dlen == 24U) {
|
||||
if (rx_message.rx_data[0] > 7U) {
|
||||
reject = true;
|
||||
}
|
||||
for (uint8_t i = 15U; !reject && i < 24U; ++i) {
|
||||
if (rx_message.rx_data[i] != 0U) {
|
||||
reject = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (reject) {
|
||||
/* 只计不丢 —— 数据管道必须诚实。
|
||||
* 网页/ESP32/主站看到的就是源头产出的原始字节,
|
||||
* 本计数退化为纯测量仪表:源头修好 = 恒 0;还在涨 = 源头问题仍在,
|
||||
* 且网页如实显示着,不存在"看着干净其实坏了"。 */
|
||||
s_diag.rx_content_reject_count++;
|
||||
}
|
||||
}
|
||||
|
||||
s_diag.rx_count++;
|
||||
s_diag.last_rx_channel = channelIndex;
|
||||
@@ -1299,14 +1645,14 @@ void CanFd::handleRxInterrupt(uint8_t channelIndex)
|
||||
s_diag.last_rx_fd_flag = rx_message.fd_flag;
|
||||
s_diag.last_rx_brs = rx_message.fd_brs;
|
||||
s_diag.last_rx_esi = rx_message.fd_esi;
|
||||
s_diag.last_rx_id = (rx_message.rx_ff == CAN_FF_STANDARD) ? rx_message.rx_sfid : rx_message.rx_efid;
|
||||
s_diag.last_rx_id = rxId;
|
||||
/* CAN0 桥接模式:将刚收到的原帧透传到 CAN1 与 CAN2,保留 11/29 位、FD、BRS、ESI。 */
|
||||
if (channelIndex == 0U && s_mode == Mode::kModeBridge0To12) {
|
||||
for (uint8_t bridgeCh = 1U; bridgeCh <= 2U; ++bridgeCh) {
|
||||
const uint32_t bridgePeriph = kChannels[bridgeCh].canPeriph;
|
||||
const bool ok = bridgeTransmitRaw(bridgePeriph, rx_message);
|
||||
if (ok) {
|
||||
s_diag.last_tx_id = s_diag.last_rx_id;
|
||||
s_diag.last_tx_id = rxId;
|
||||
s_diag.last_tx_channel = bridgeCh;
|
||||
s_diag.last_payload_length = rx_message.rx_dlen;
|
||||
} else {
|
||||
@@ -1319,7 +1665,7 @@ void CanFd::handleRxInterrupt(uint8_t channelIndex)
|
||||
/* 维护每通道独立计数与最近一帧快照,用于 1Hz 反馈给 ESP32。 */
|
||||
auto &stats = s_channelStats[channelIndex];
|
||||
stats.rx_count++;
|
||||
stats.last_rx_id = s_diag.last_rx_id;
|
||||
stats.last_rx_id = rxId;
|
||||
stats.last_rx_dlc = rx_message.rx_dlen;
|
||||
stats.last_rx_fd_flag = rx_message.fd_flag;
|
||||
stats.last_rx_brs = rx_message.fd_brs;
|
||||
@@ -1335,7 +1681,6 @@ void CanFd::handleRxInterrupt(uint8_t channelIndex)
|
||||
/* 维护每通道不重复 ID 快照表,用于 1Hz 分页拉取给 ESP32。 */
|
||||
{
|
||||
auto &table = s_idSnapshot[channelIndex];
|
||||
const uint32_t rxId = s_diag.last_rx_id;
|
||||
/* 查找已有条目 */
|
||||
uint8_t found = CANFD_ID_SNAPSHOT_MAX;
|
||||
for (uint8_t si = 0U; si < table.count; ++si) {
|
||||
@@ -1378,22 +1723,26 @@ void CanFd::handleRxInterrupt(uint8_t channelIndex)
|
||||
}
|
||||
|
||||
auto &queue = s_rxQueues[channelIndex];
|
||||
if(queue.count >= kRxQueueDepth) {
|
||||
const uint8_t head = queue.head;
|
||||
const uint8_t tail = queue.tail;
|
||||
const uint8_t next_tail = static_cast<uint8_t>((tail + 1U) & kRxQueueMask);
|
||||
|
||||
if(next_tail == head) {
|
||||
s_diag.rx_drop_count++;
|
||||
stats.rx_drop_count++;
|
||||
continue;
|
||||
}
|
||||
|
||||
auto &frame = queue.frames[queue.tail];
|
||||
auto &frame = queue.frames[tail];
|
||||
frame.channel = channelIndex;
|
||||
frame.id = s_diag.last_rx_id;
|
||||
frame.id = rxId;
|
||||
frame.length = rx_message.rx_dlen;
|
||||
for(uint8_t i = 0; i < rx_message.rx_dlen; ++i) {
|
||||
frame.data[i] = rx_message.rx_data[i];
|
||||
}
|
||||
|
||||
queue.tail = static_cast<uint8_t>((queue.tail + 1U) % kRxQueueDepth);
|
||||
queue.count++;
|
||||
__DMB();
|
||||
queue.tail = next_tail;
|
||||
}
|
||||
|
||||
if(fifoFull) {
|
||||
@@ -1455,6 +1804,15 @@ void CanFd::handleTxInterrupt(uint8_t channelIndex)
|
||||
drainTxQueueIsr(channelIndex);
|
||||
}
|
||||
|
||||
/* ── 空通道自动识别状态(2026-09-18)──
|
||||
* 无对端的通道 RX 浮空 → 持续 STUFF、REC 冲 ~130 停在 ERR_PASSIVE,每 5s 刷一行
|
||||
* [CAN-HW-ERR](config.h 老注释"CH1/CH2 未接设备,关闭避免持续 CAN 错误 + 占 SPI
|
||||
* 带宽")。改为运行时自动判定:连续 CONFIG_CAN_QUIET_WINDOW 个窗口无有效帧增量
|
||||
* 且有错误 → 静默上报;出现帧增量立刻恢复。只抑制上报,硬件保持 NORMAL 接收。 */
|
||||
static uint8_t s_chanQuiet[3] = {0U, 0U, 0U}; /* 1=该通道已自动静默上报 */
|
||||
static uint32_t s_rxCountPrev[3] = {0U, 0U, 0U}; /* 上一拍末尾的 rx_count */
|
||||
static uint8_t s_quietMiss[3] = {0U, 0U, 0U}; /* 连续"无帧且有错"的窗口数 */
|
||||
|
||||
/**
|
||||
* @brief 刷新三路 CAN 通道的 1Hz 反馈快照。
|
||||
* @note 在临界区内拷贝实时计数与最近一帧信息到 s_feedback;保证 SPI 命令读到一致快照。
|
||||
@@ -1484,6 +1842,37 @@ void CanFd::refreshFeedbackSnapshot()
|
||||
}
|
||||
|
||||
s_feedbackSeq++;
|
||||
/* 空通道自动识别:本拍无有效帧增量且有错误 → 记一次 miss,连续达到窗口长度才静默
|
||||
* (加滞回,避免偶发丢拍/慢流量误判);一旦有帧增量立刻恢复上报。
|
||||
* 时间基 = s_feedbackSeq(本函数 1Hz,见下面 %5 的 5s 打印节拍)。 */
|
||||
for (size_t i = 0; i < kChannelCount; ++i) {
|
||||
const uint32_t rxNow = s_channelStats[i].rx_count;
|
||||
const uint32_t rxDelta = rxNow - s_rxCountPrev[i];
|
||||
s_rxCountPrev[i] = rxNow;
|
||||
const bool hasErr = (hw_tec[i] != 0U || hw_rec[i] != 0U
|
||||
|| (hw_err[i] & (CAN_ERR_WERR | CAN_ERR_PERR | CAN_ERR_BOERR)) != 0U);
|
||||
if (rxDelta > 0U) {
|
||||
s_quietMiss[i] = 0U;
|
||||
if (s_chanQuiet[i] != 0U) {
|
||||
s_chanQuiet[i] = 0U;
|
||||
CAN_LOGI("[CAN] CH%u 恢复上报:检测到有效帧 (rx=%lu)\r\n",
|
||||
static_cast<unsigned>(i), static_cast<unsigned long>(rxNow));
|
||||
}
|
||||
} else if (hasErr) {
|
||||
if (s_quietMiss[i] < 0xFFU) s_quietMiss[i]++;
|
||||
if (s_chanQuiet[i] == 0U
|
||||
&& s_feedbackSeq >= CONFIG_CAN_QUIET_AFTER_SEC
|
||||
&& s_quietMiss[i] >= CONFIG_CAN_QUIET_WINDOW_SEC) {
|
||||
s_chanQuiet[i] = 1U;
|
||||
LOG_ERR("[CAN] CH%u 无有效帧且错误累积 → 自动静默上报(本通道无设备/无对端;"
|
||||
"硬件保持接收,插上设备有帧即自动恢复;TEC=%lu REC=%lu)\r\n",
|
||||
static_cast<unsigned>(i),
|
||||
static_cast<unsigned long>(hw_tec[i]), static_cast<unsigned long>(hw_rec[i]));
|
||||
}
|
||||
} else {
|
||||
s_quietMiss[i] = 0U;
|
||||
}
|
||||
}
|
||||
/* 每 5 秒采样硬件错误态;正常心跳不打印(统计走 SPI 0xC1)。
|
||||
* 仅当 TEC/REC 或告警位非零时输出 [CAN-HW-ERR];全量 seq 心跳见 CONFIG_UART_DIAG_LEVEL。 */
|
||||
if (s_feedbackSeq % 5U == 0U) {
|
||||
@@ -1499,6 +1888,7 @@ void CanFd::refreshFeedbackSnapshot()
|
||||
&& (hw_err[i] & (CAN_ERR_WERR | CAN_ERR_PERR | CAN_ERR_BOERR)) == 0U) {
|
||||
continue;
|
||||
}
|
||||
if (s_chanQuiet[i] != 0U) continue; /* 已自动静默的空通道:不再刷错误 */
|
||||
LOG_ERR("[CAN-HW-ERR] CH%u ERR=0x%08lX ERRN=%lu(%s) TEC=%lu REC=%lu TSTAT=0x%08lX\r\n",
|
||||
static_cast<unsigned>(i),
|
||||
static_cast<unsigned long>(hw_err[i]),
|
||||
@@ -1515,14 +1905,17 @@ void CanFd::refreshFeedbackSnapshot()
|
||||
/* 写入硬件层错误状态供 SPI 反馈使用 */
|
||||
s_feedback[i].stats.tec = static_cast<uint8_t>(hw_tec[i]);
|
||||
s_feedback[i].stats.rec = static_cast<uint8_t>(hw_rec[i]);
|
||||
/* 总线状态位图:bit0=init, bit1=sleep, bit2=error_passive, bit3=bus_off, bit4=error_warning
|
||||
* init/sleep 来自 CAN_STAT,error_passive/bus_off/error_warning 来自 CAN_ERR */
|
||||
/* 总线状态位图:bit0=init, bit1=控制器sleep, bit2=error_passive, bit3=bus_off,
|
||||
* bit4=error_warning, bit5=本端收发器 Silent(只听不发)
|
||||
* bit0~1 来自 CAN_STAT,bit2~4 来自 CAN_ERR;bit5 来自 s_txSilent(唯一真源,
|
||||
* 与发送门控同一个变量),别拿 bit1 当收发器状态——两者不是一回事。 */
|
||||
uint8_t busState = 0U;
|
||||
if (hw_stat[i] & CAN_STAT_IWS) busState |= (1U << 0); /* 初始化模式 */
|
||||
if (hw_stat[i] & CAN_STAT_SLPWS) busState |= (1U << 1); /* 睡眠模式 */
|
||||
if (hw_stat[i] & CAN_STAT_SLPWS) busState |= (1U << 1); /* 控制器睡眠模式 */
|
||||
if (hw_err[i] & CAN_ERR_PERR) busState |= (1U << 2); /* 错误被动 */
|
||||
if (hw_err[i] & CAN_ERR_BOERR) busState |= (1U << 3); /* 总线关闭 */
|
||||
if (hw_err[i] & CAN_ERR_WERR) busState |= (1U << 4); /* 错误警告 */
|
||||
if (s_txSilent[i] != 0U) busState |= (1U << 5); /* 收发器 Silent */
|
||||
s_feedback[i].stats.bus_state = busState;
|
||||
s_idSnapshot[i].seq = s_feedbackSeq;
|
||||
}
|
||||
@@ -1532,6 +1925,10 @@ void CanFd::refreshFeedbackSnapshot()
|
||||
for (size_t i = 0; i < kChannelCount; ++i) {
|
||||
uint8_t cur = s_feedback[i].stats.bus_state;
|
||||
if (cur != s_prevBusState[i]) {
|
||||
if (s_chanQuiet[i] != 0U) { /* 静默通道的状态变化只记账不打印 */
|
||||
s_prevBusState[i] = cur;
|
||||
continue;
|
||||
}
|
||||
const char *stateStr = "NORMAL";
|
||||
if (cur & (1U << 0)) stateStr = "INIT_MODE";
|
||||
else if (cur & (1U << 3)) stateStr = "BUS_OFF";
|
||||
@@ -1593,10 +1990,12 @@ bool CanFd::copyFeedbackSnapshot(uint8_t channelIndex, ChannelFeedback *out)
|
||||
* @retval 无返回值。
|
||||
* @note C 向量包装函数,内部转调 `CanFd::handleRxInterrupt(0U)`。
|
||||
*/
|
||||
extern "C" void CAN0_RX0_IRQHandler(void) { CanFd::handleRxInterrupt(0U); }
|
||||
extern "C" void CAN0_RX0_IRQHandler(void)
|
||||
{ CpuLoad::isrEnter(); CanFd::handleRxInterrupt(0U); CpuLoad::isrExit(); }
|
||||
|
||||
/** @brief CAN0 TX 完成中断向量入口。 */
|
||||
extern "C" void CAN0_TX_IRQHandler(void) { CanFd::handleTxInterrupt(0U); }
|
||||
extern "C" void CAN0_TX_IRQHandler(void)
|
||||
{ CpuLoad::isrEnter(); CanFd::handleTxInterrupt(0U); CpuLoad::isrExit(); }
|
||||
|
||||
/**
|
||||
* @brief CAN1 RX0 中断向量入口。
|
||||
@@ -1605,10 +2004,12 @@ extern "C" void CAN0_TX_IRQHandler(void) { CanFd::handleTxInterrupt(0U); }
|
||||
* @retval 无返回值。
|
||||
* @note C 向量包装函数,内部转调 `CanFd::handleRxInterrupt(1U)`。
|
||||
*/
|
||||
extern "C" void CAN1_RX0_IRQHandler(void) { CanFd::handleRxInterrupt(1U); }
|
||||
extern "C" void CAN1_RX0_IRQHandler(void)
|
||||
{ CpuLoad::isrEnter(); CanFd::handleRxInterrupt(1U); CpuLoad::isrExit(); }
|
||||
|
||||
/** @brief CAN1 TX 完成中断向量入口。 */
|
||||
extern "C" void CAN1_TX_IRQHandler(void) { CanFd::handleTxInterrupt(1U); }
|
||||
extern "C" void CAN1_TX_IRQHandler(void)
|
||||
{ CpuLoad::isrEnter(); CanFd::handleTxInterrupt(1U); CpuLoad::isrExit(); }
|
||||
|
||||
/**
|
||||
* @brief CAN2 RX0 中断向量入口。
|
||||
@@ -1617,7 +2018,9 @@ extern "C" void CAN1_TX_IRQHandler(void) { CanFd::handleTxInterrupt(1U); }
|
||||
* @retval 无返回值。
|
||||
* @note C 向量包装函数,内部转调 `CanFd::handleRxInterrupt(2U)`。
|
||||
*/
|
||||
extern "C" void CAN2_RX0_IRQHandler(void) { CanFd::handleRxInterrupt(2U); }
|
||||
extern "C" void CAN2_RX0_IRQHandler(void)
|
||||
{ CpuLoad::isrEnter(); CanFd::handleRxInterrupt(2U); CpuLoad::isrExit(); }
|
||||
|
||||
/** @brief CAN2 TX 完成中断向量入口。 */
|
||||
extern "C" void CAN2_TX_IRQHandler(void) { CanFd::handleTxInterrupt(2U); }
|
||||
extern "C" void CAN2_TX_IRQHandler(void)
|
||||
{ CpuLoad::isrEnter(); CanFd::handleTxInterrupt(2U); CpuLoad::isrExit(); }
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include "device/timer.h"
|
||||
#include "sys/task_manager.h"
|
||||
#include "sys/systick.h"
|
||||
#include "sys/cpu_load.h"
|
||||
#include "sys/config.h"
|
||||
|
||||
/**
|
||||
@@ -105,4 +106,9 @@ void SchedulerTimer::onInterrupt()
|
||||
* @retval 无返回值。
|
||||
* @note C 向量包装函数,内部转调 `SchedulerTimer::onInterrupt()`。
|
||||
*/
|
||||
extern "C" void TIMER2_IRQHandler(void) { SchedulerTimer::onInterrupt(); }
|
||||
extern "C" void TIMER2_IRQHandler(void)
|
||||
{
|
||||
CpuLoad::isrEnter();
|
||||
SchedulerTimer::onInterrupt();
|
||||
CpuLoad::isrExit();
|
||||
}
|
||||
|
||||
@@ -33,7 +33,9 @@ public:
|
||||
/**
|
||||
* @brief CAN 发送结果枚举,枚举值即 SPI 错误响应码,便于 handler 直接透传。
|
||||
* @note 0xE0/0xE1 保留给 handler 入口校验(payload 过短/通道非法),
|
||||
* 不经 transmitFrame 返回;0xE2~0xE5 由 transmitFrame 区分四种失败原因。
|
||||
* 不经 transmitFrame 返回;0xE2~0xE5 由 transmitFrame 区分四种失败原因;
|
||||
* 0xE6=通道处于收发器 Silent(只听不发),在入口即拒绝,不计任何错误计数。
|
||||
* 0xE7=通道被电机 OTA 独占锁定(EtherCAT/SPI 跨总线排他),同样不计错误计数。
|
||||
*/
|
||||
enum class CanTxResult : uint8_t {
|
||||
kOk = 0x00U, /**< 发送成功,已进入硬件邮箱。 */
|
||||
@@ -41,6 +43,8 @@ public:
|
||||
kErrTecHigh = 0xE3U, /**< TEC > 96,暂停发送保护通道。 */
|
||||
kErrMailbox = 0xE4U, /**< 3 个硬件邮箱全 PENDING,abort+重投仍失败。 */
|
||||
kErrInvalid = 0xE5U, /**< 入口参数无效(channelIndex/dlc/data 等)。 */
|
||||
kErrSilent = 0xE6U, /**< 通道收发器处于 Silent(驱动器关断),拒绝发送。 */
|
||||
kErrOtaLocked = 0xE7U, /**< 通道被电机 OTA 独占(跨总线排他),拒绝发送,不计错误计数。 */
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -53,6 +57,15 @@ public:
|
||||
uint32_t rx_drop_count;
|
||||
uint32_t rx_fifo_full_count;
|
||||
uint32_t rx_fifo_overfull_count;
|
||||
/** 取帧期间被硬件换帧、整帧丢弃的次数(1.6.6,TS/DLC 前后不一致即判定)。
|
||||
* 这是"读到半条帧"的唯一可观测量,稳态下应当只在高负载时缓慢增长。 */
|
||||
uint32_t rx_torn_count;
|
||||
/** 释放邮箱后等待 RFD 硬件自清超时的次数(手册 26.3.4 第 3 步要求等待)。 */
|
||||
uint32_t rx_release_timeout_count;
|
||||
/** 内容级拼接帧拦截次数(1.6.11):(id, data) 结构自相矛盾(STATUS 的
|
||||
* 温度/电压出量程、CONFIG 的未使用区非 0)⇒ 整帧作废。与 rx_torn 互补:
|
||||
* torn 挡"取数期间窗口被换",这个挡"窗口没换但 FIFO 缓冲指针错位"。 */
|
||||
uint32_t rx_content_reject_count;
|
||||
uint32_t tx_abort_count;
|
||||
uint32_t last_rx_id;
|
||||
uint32_t last_tx_id;
|
||||
@@ -80,7 +93,7 @@ public:
|
||||
uint8_t last_rx_data[CANFD_FEEDBACK_LAST_DATA_MAX];
|
||||
uint8_t tec; /**< 最近一次采样时硬件发送错误计数器 (0~255)。 */
|
||||
uint8_t rec; /**< 最近一次采样时硬件接收错误计数器 (0~255)。 */
|
||||
uint8_t bus_state; /**< 总线状态位图:bit0=init_mode, bit1=sleep, bit2=error_passive, bit3=bus_off, bit4=error_warning。 */
|
||||
uint8_t bus_state; /**< 总线状态位图:bit0=init_mode, bit1=sleep, bit2=error_passive, bit3=bus_off, bit4=error_warning, bit5=tx_silent(收发器 Silent,只听不发)。bit0~4 是控制器/总线侧状态,bit5 是本端 S 脚状态。 */
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -158,7 +171,7 @@ public:
|
||||
/**
|
||||
* @brief 直接向指定 CAN 通道发送一帧(绕过软件队列)。
|
||||
* @param allow_abort 邮箱全占时是否允许 abort 一个 PENDING 邮箱并重投一次。
|
||||
* - true(0xC0 单帧路径默认):非阻塞顶替,适合"最新命令优先"的单帧发送;
|
||||
* - true(底层非阻塞顶替):适合最新单发覆盖场景;高总线负载下慎用(可能 abort 掉正在等仲裁的帧);
|
||||
* - false(突发/队列路径):邮箱满直接返回 kErrMailbox,交由 enqueueTx
|
||||
* 的软件队列缓存,由 TX 完成中断回灌——避免 abort 把同通道已装载的
|
||||
* 旧帧静默丢弃(>3 帧同通道突发只剩最后 3 帧上线的根因)。
|
||||
@@ -173,6 +186,28 @@ public:
|
||||
/** @brief 运行时切换指定通道 CAN 自动重发(ARD 位,需 init-mode);停等式 OTA 期间关闭、结束后恢复。 */
|
||||
static void setChannelAutoRetrans(uint8_t channelIndex, bool enable);
|
||||
|
||||
/** @brief 运行时压制指定通道 TX 帧的 BRS:压制中 FD 帧数据段回退仲裁段速率。
|
||||
* 电机 BOOT 无法正确接收 BRS 高速段(见 sys/config.h CONFIG_CAN_FD_BRS_ENABLE 注释),
|
||||
* 电机 OTA relay 经 enterState 钩子与 setChannelAutoRetrans 同点切换,OTA 结束必恢复。 */
|
||||
static void setChannelBrsSuppress(uint8_t channelIndex, bool suppress);
|
||||
|
||||
/** @brief 切换指定通道收发器(TCAN1057AV)的 Silent 模式:只听不发。
|
||||
* S 脚(CAN0=PA8/CAN1=PB14/CAN2=PB9)置高 → 驱动器关断、**接收机照常工作**
|
||||
* (手册 Table 8-4:RXD 仍镜像总线),所以静默期间接收与上报完全不受影响;
|
||||
* 置低 → 回 Normal。这是硬件级禁止发送,S 脚内部上拉,MCU 复位/浮空即为 Silent。
|
||||
* @note 进 Silent 前先 abort 本通道 PENDING 邮箱:驱动器已关,残留邮箱只会让
|
||||
* 控制器因收不到 ACK 而白涨 TEC。
|
||||
* 退 Silent 时忙等收发器模式建立时间 tMODE=20µs 再返回,否则紧跟的第一帧
|
||||
* 驱动不上线。用 DWT CYCCNT 忙等,可在 CS EXTI ISR 上下文调用
|
||||
* (SysTickTimer::delayMs 在该上下文会死等)。
|
||||
* 与 canfd.cpp 内 s_chanQuiet(空通道上报节流)无关,也别混为一谈。
|
||||
* 静默不持久化:上电 init 一律回 Normal。
|
||||
* @retval true 通道合法且已生效;false 通道非法。 */
|
||||
static bool setTxSilent(uint8_t channelIndex, bool silent);
|
||||
|
||||
/** @brief 查询指定通道是否处于收发器 Silent(唯一真源 s_txSilent,0xC1 bus_state bit5 用它)。 */
|
||||
static bool isTxSilent(uint8_t channelIndex);
|
||||
|
||||
/** @brief 读取指定通道 CAN_ERR 寄存器原始值(事件时刻只读快照,不依赖 1Hz 采样)。
|
||||
* 解码:TEC=bit16-23,REC=bit24-31,ERRN=bit4-6;无效通道返回 0。仅供诊断日志,禁用于发送门控。 */
|
||||
static uint32_t readChannelErrorReg(uint8_t channelIndex);
|
||||
@@ -232,8 +267,15 @@ public:
|
||||
/** @brief 软件 TX 队列当前积压帧数(统计/诊断用)。 */
|
||||
static uint32_t txQueueBacklog(uint8_t channelIndex);
|
||||
|
||||
/** @brief 接收队列容量深度(2 的幂;8kHz PDO 下主循环间隙短,加深缓冲防 STATUS 溢出丢帧)。 */
|
||||
static constexpr uint8_t rxQueueDepth() { return kRxQueueDepth; }
|
||||
|
||||
/** @brief 查询指定通道软件接收队列当前积压帧数(无锁 SPSC,由 tail 与 head 计算)。 */
|
||||
static uint32_t rxQueueCount(uint8_t channelIndex);
|
||||
|
||||
private:
|
||||
static constexpr uint8_t kRxQueueDepth = 16U;
|
||||
static constexpr uint8_t kRxQueueDepth = 64U; /* 16→64:1kHz STATUS×3 在 PDI 风暴下不再轻易打满 */
|
||||
static constexpr uint8_t kRxQueueMask = kRxQueueDepth - 1U;
|
||||
/** 每通道软件突发队列深度:覆盖整块突发(CAN_BURST_HARD_MAX=16 帧)中同一通道的
|
||||
* 全部超载帧(3 邮箱之外最多 13 帧 = 16-3),保证任意 DLC 的整块突发不因队列满丢帧。 */
|
||||
static constexpr uint8_t kTxQueueDepth = 13U;
|
||||
@@ -245,11 +287,15 @@ private:
|
||||
uint8_t length;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Lock-Free 单生产者单消费者 (SPSC) 环形接收队列。
|
||||
* @note 生产者为 CAN RX ISR (Preempt 0) 写入 tail,消费者为 consumeRxFrame (Thread) 推进 head。
|
||||
* 移除共享 count 变量,消除 BASEPRI=0x10 临界区对 Preempt 0 无效导致的并发撕裂与计数下溢风险。
|
||||
*/
|
||||
struct RxQueue {
|
||||
RxFrame frames[kRxQueueDepth];
|
||||
volatile uint8_t head;
|
||||
volatile uint8_t tail;
|
||||
volatile uint8_t count;
|
||||
};
|
||||
|
||||
/** 排队等待硬件邮箱空位的 CAN 帧(仅 SDO 突发用,payload ≤ 64B)。 */
|
||||
@@ -294,6 +340,8 @@ private:
|
||||
static uint8_t s_nextConsumeChannel;
|
||||
static volatile Mode s_mode;
|
||||
static ChannelStats s_channelStats[3];
|
||||
static bool s_brsSuppress[3]; /**< TX BRS 压制(电机 OTA 期间该通道 DLC>8 帧禁 BRS) */
|
||||
static volatile uint8_t s_txSilent[3]; /**< 收发器 Silent(只听不发):1=S 脚已置高,TX 入口拒绝 */
|
||||
static ChannelFeedback s_feedback[3];
|
||||
/** 各通道硬件 init(时序+退出 init mode)是否成功 */
|
||||
static bool s_channelHwOk[3];
|
||||
|
||||
@@ -2,8 +2,10 @@
|
||||
* @file motor_state.cpp
|
||||
* @brief 多电机状态表实现 — CAN STATUS 帧识别、内存缓存、SPI 推送序列化。
|
||||
*
|
||||
* 所有函数仅在主循环上下文调用(taskCanRxParse / taskHeartbeat / pushStep),
|
||||
* 无 ISR 竞态,不需要临界区保护。
|
||||
* 本模块的函数只在主循环上下文调用(taskCanRxParse / taskHeartbeat / pushStep),
|
||||
* 但消费者不止主循环:EtherCAT 的 buildInput 跑在 PDI_Isr,会抢占 STATUS 写侧,
|
||||
* 故 STATUS 的 seq/data 由 MotorSlot.wr_seq(seqlock)保护,见 onCanFrame。
|
||||
* SPI 侧的 buildPayload 与写侧同上下文,天然无此问题。
|
||||
*/
|
||||
|
||||
#include "device/motor_state.h"
|
||||
@@ -36,7 +38,7 @@ void onCanFrame(uint8_t channel, uint32_t id, const uint8_t *data, uint8_t len)
|
||||
|
||||
const uint32_t masked = id & kMotorStatusIdMask;
|
||||
|
||||
/* STATUS 帧 0x000N0100 (DLC 32, 实时值) */
|
||||
/* STATUS 帧 0x000N0100 (DLC 48, 实时值,字节 32~35 = InitStatus) */
|
||||
if (masked == kMotorStatusIdMatch) {
|
||||
const uint8_t node = static_cast<uint8_t>((id >> 16) & 0x0FU);
|
||||
if (node < 1U || node > kMotorMaxNodes || channel >= kMotorChannels) {
|
||||
@@ -45,6 +47,14 @@ void onCanFrame(uint8_t channel, uint32_t id, const uint8_t *data, uint8_t len)
|
||||
|
||||
MotorSlot &slot = s_motorTable.slots[(node - 1U) * kMotorChannels + channel];
|
||||
|
||||
/* seqlock 写侧(1.6.5):奇数=本槽 seq/data 正在改。EtherCAT 的 buildInput
|
||||
* 跑在 PDI_Isr,会抢占这里;抢占落在下面那次 memcpy 中途就会发布
|
||||
* "新 seq + 半新半旧 48B"——两帧字节拼成一个 STATUS,边界若落在 float 的
|
||||
* 符号/指数字节上就是网页上的离谱值。缺的不是顺序而是让消费者能认出
|
||||
* "这一段没写完"的标记。 */
|
||||
++slot.wr_seq; /* → 奇数 */
|
||||
__DMB(); /* 普通写不得重排到奇/偶两次自增之外 */
|
||||
|
||||
slot.node_id = node;
|
||||
slot.channel = channel;
|
||||
slot.online = 1U;
|
||||
@@ -57,6 +67,9 @@ void onCanFrame(uint8_t channel, uint32_t id, const uint8_t *data, uint8_t len)
|
||||
if (slot.dlc < kMotorStatusDlc) {
|
||||
std::memset(&slot.data[slot.dlc], 0, kMotorStatusDlc - slot.dlc);
|
||||
}
|
||||
|
||||
__DMB();
|
||||
++slot.wr_seq; /* → 偶数:本槽整体一致 */
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -159,9 +172,10 @@ uint16_t buildPayload(uint8_t page, uint8_t *buf, uint16_t maxLen)
|
||||
writeU32Le(&buf[offset + 4U], slot.rx_count);
|
||||
writeU32Le(&buf[offset + 8U], slot.seq);
|
||||
std::memcpy(&buf[offset + 12U], slot.data, kMotorStatusDlc);
|
||||
writeU32Le(&buf[offset + 44U], slot.config_rx_count);
|
||||
writeU32Le(&buf[offset + 48U], slot.config_seq);
|
||||
std::memcpy(&buf[offset + 52U], slot.config_data, kMotorConfigDlc);
|
||||
/* STATUS DLC 48 后,CONFIG 字段整体后移 16B */
|
||||
writeU32Le(&buf[offset + 60U], slot.config_rx_count);
|
||||
writeU32Le(&buf[offset + 64U], slot.config_seq);
|
||||
std::memcpy(&buf[offset + 68U], slot.config_data, kMotorConfigDlc);
|
||||
|
||||
offset += kMotorEntrySize;
|
||||
count++;
|
||||
|
||||
@@ -32,7 +32,7 @@ static constexpr uint8_t kMotorMaxNodes = CONFIG_MOTOR_MAX_NODES;
|
||||
* 状态表按 node×channel 双键存储,各通道独立槽位互不覆盖。 */
|
||||
static constexpr uint8_t kMotorChannels = 3U;
|
||||
static constexpr uint8_t kMotorSlotCount = kMotorMaxNodes * kMotorChannels; /* 9×3 = 27 */
|
||||
static constexpr uint8_t kMotorStatusDlc = 32U;
|
||||
static constexpr uint8_t kMotorStatusDlc = 48U; /* v0.74:STATUS 帧 DLC 48(字节 32~35 = InitStatus) */
|
||||
static constexpr uint8_t kMotorConfigDlc = 24U; /* CONFIG 帧 DLC */
|
||||
static constexpr uint16_t kMotorOfflineSec = CONFIG_MOTOR_OFFLINE_MS / 1000U;
|
||||
|
||||
@@ -50,18 +50,18 @@ static constexpr uint32_t kBootHeartbeatTimeoutSec = 3U; /* >3s 无 0x103 →
|
||||
static constexpr uint8_t kMotorActiveBootBit = 0x80U; /* 0xC5 page0 header active 字节 bit7 = boot_active */
|
||||
|
||||
/* ── 序列化常量 (0xC5 payload) ── */
|
||||
static constexpr uint8_t kMotorEntrySize = 76U; /* 1+1+1+1+4+4+32+4+4+24 (STATUS+CONFIG) */
|
||||
static constexpr uint8_t kMotorEntrySize = 92U; /* 1+1+1+1+4+4+48+4+4+24 (STATUS+CONFIG) */
|
||||
static constexpr uint8_t kMotorPageHeaderLen = 4U; /* page+total+count+active */
|
||||
static constexpr uint8_t kMotorEntriesPerPage = 3U; /* (236-4)/76 = 3.05 → 3 */
|
||||
static constexpr uint8_t kMotorEntriesPerPage = 2U; /* (236-4)/92 = 2.52 → 2 */
|
||||
static constexpr uint8_t kMotorTotalPages =
|
||||
(kMotorSlotCount + kMotorEntriesPerPage - 1U) / kMotorEntriesPerPage; /* ceil(27/3) = 9 */
|
||||
(kMotorSlotCount + kMotorEntriesPerPage - 1U) / kMotorEntriesPerPage; /* ceil(27/2) = 14 */
|
||||
|
||||
/// 单电机状态槽
|
||||
struct MotorSlot {
|
||||
uint8_t node_id; ///< 1-9, 0=空槽
|
||||
uint8_t channel; ///< 最后收到帧的 CAN 通道 (0/1/2)
|
||||
uint8_t online; ///< 1=在线, 0=离线
|
||||
uint8_t dlc; ///< 最后帧 DLC (正常=32)
|
||||
uint8_t dlc; ///< 最后帧 DLC (正常=48)
|
||||
uint32_t rx_count; ///< 累计收到 STATUS 帧数
|
||||
uint32_t last_rx_sec; ///< 最后收到帧时的 g_app_uptime_sec
|
||||
uint32_t seq; ///< 更新序号 (每次刷新 +1)
|
||||
@@ -69,6 +69,11 @@ struct MotorSlot {
|
||||
uint32_t config_rx_count; ///< 累计收到 CONFIG 帧数
|
||||
uint32_t config_seq; ///< CONFIG 更新序号
|
||||
uint8_t config_data[kMotorConfigDlc]; ///< 原始 CONFIG payload (目标值回报)
|
||||
/* STATUS 写侧 seqlock(1.6.5):奇数=正在写本槽的 seq/data。
|
||||
* 只有主循环(onCanFrame 的 STATUS 分支)自增,ISR 只读 —— 单核下消费者是
|
||||
* ISR,它执行期间生产者被冻结,故"读到偶数 ⇒ 生产者不在这段里"成立。
|
||||
* 放结构体末尾,已有字段偏移全不变(SPI 序列化/PDO 布局照旧)。 */
|
||||
volatile uint8_t wr_seq;
|
||||
};
|
||||
|
||||
/// 全局电机状态表
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "device/spi/app_spi_command_defs.h"
|
||||
#include "device/canfd.h"
|
||||
#include "sys/config.h"
|
||||
#include "sys/motor_ota_relay.h"
|
||||
#include <cstdio>
|
||||
|
||||
namespace {
|
||||
@@ -76,7 +77,22 @@ public:
|
||||
return;
|
||||
}
|
||||
|
||||
const CanFd::CanTxResult result = CanFd::transmitFrame(can_ch, can_id, can_data, can_dlc);
|
||||
/* 通道正被电机 OTA 独占(WiFi 与 EtherCAT 两条链路可能同时在跑,锁是共用的):
|
||||
* 这一帧挤进同一通道会 abort 掉在途的 0x101 固件包,单发模式下永不重传。
|
||||
* 回 0xE7,上位机据此知道「是被 OTA 挡了」而不是「邮箱满」。 */
|
||||
if (MotorOtaRelay::isChannelLocked(can_ch)) {
|
||||
postCanSendLog(bus, "LOCKED(motor-OTA)", can_ch, can_id, can_data, can_dlc);
|
||||
const uint8_t err = static_cast<uint8_t>(CanFd::CanTxResult::kErrOtaLocked);
|
||||
bus.buildResponse(ctx.tx_buf, ctx.tx_buf_size, ctx.tx_len,
|
||||
SpiDefs::kRspError, &err, 1U);
|
||||
return;
|
||||
}
|
||||
|
||||
/* 0xC0 单帧发送改走 enqueueTx:
|
||||
* 1) allow_abort=false 杜绝硬件邮箱被后续发送主动 abort 抹杀;
|
||||
* 2) 电机高频上报抢占仲裁导致邮箱全占时,帧自动进入 13 深度软件队列,
|
||||
* 由 TX 中断 drainTxQueueIsr 持续回灌空闲邮箱发送,彻底消除丢帧。 */
|
||||
const CanFd::CanTxResult result = CanFd::enqueueTx(can_ch, can_id, can_data, can_dlc);
|
||||
if (result == CanFd::CanTxResult::kOk) {
|
||||
postCanSendLog(bus, "OK", can_ch, can_id, can_data, can_dlc);
|
||||
const uint8_t ack = 0x00U;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "device/spi/app_spi_command_defs.h"
|
||||
#include "device/canfd.h"
|
||||
#include "sys/config.h"
|
||||
#include "sys/motor_ota_relay.h"
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -81,6 +82,11 @@ public:
|
||||
resp[1U + i] = 0xE1U; /* 通道非法:跳过该帧,其余照常发送 */
|
||||
continue;
|
||||
}
|
||||
if (MotorOtaRelay::isChannelLocked(ch)) {
|
||||
/* 电机 OTA 独占该通道:整帧拒绝,不挤邮箱(0xE7)。 */
|
||||
resp[1U + i] = static_cast<uint8_t>(CanFd::CanTxResult::kErrOtaLocked);
|
||||
continue;
|
||||
}
|
||||
const CanFd::CanTxResult r =
|
||||
CanFd::enqueueTx(ch, id, (dlc > 0U) ? &p[1] : nullptr, dlc);
|
||||
resp[1U + i] = static_cast<uint8_t>(r);
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
* [21..24] error_count (LE)
|
||||
* [25] tec 硬件发送错误计数器 (0~255)
|
||||
* [26] rec 硬件接收错误计数器 (0~255)
|
||||
* [27] bus_state 总线状态位图:bit0=init,bit1=sleep,bit2=error_passive,bit3=bus_off,bit4=error_warning
|
||||
* [27] bus_state 总线状态位图:bit0=init,bit1=控制器sleep,bit2=error_passive,bit3=bus_off,bit4=error_warning,bit5=tx_silent(收发器Silent)
|
||||
* [28..] 条目逐条拼接:
|
||||
* id(4LE) + dlc(1) + flags(1) + reserved(2) + data(dlc)
|
||||
* 每条 = 8 + dlc 字节
|
||||
@@ -31,6 +31,16 @@
|
||||
* [0] mode 0=全部接收, 1=按 ID 过滤;其它值返回 kRspError。
|
||||
* 应答 payload:
|
||||
* [0] 实际生效的 mode。
|
||||
*
|
||||
* 3) kTypeCanSetSilent (0xC8):ESP32 切换指定通道收发器 Silent(只听不发)。
|
||||
* 请求 payload:
|
||||
* [0] channel 0/1/2
|
||||
* [1] silent 1=进 Silent(S 脚置高:驱动关断、接收机照常), 0=回 Normal
|
||||
* 应答 payload:
|
||||
* [0] channel [1] 实际生效的 silent(与发送门控同一个值) [2] 该通道使能位
|
||||
* ERROR:0xE0=payload 过短, 0xE1=通道非法/未使能, 0xE2=S 脚回读与意图不符(标志已回滚)。
|
||||
* 生效期间该通道所有发送入口(0xC0/0xC7/桥接/relay)回 0xE6=kErrSilent,
|
||||
* 不计错误计数;静默不持久化,上电 init 一律回 Normal。
|
||||
*/
|
||||
|
||||
#include "device/spi/spi_command_handler.h"
|
||||
@@ -138,18 +148,19 @@ static uint16_t buildFeedbackPayload(uint8_t channel, uint8_t page,
|
||||
|
||||
namespace {
|
||||
|
||||
/** @brief SPI 0xC1 反馈查询 + 0xC2 模式切换处理器。 */
|
||||
/** @brief SPI 0xC1 反馈查询 + 0xC2 模式切换 + 0xC8 收发器 Silent 开关处理器。 */
|
||||
class SpiCanFeedbackHandler final : public ISpiCommandHandler {
|
||||
public:
|
||||
/** @brief 是否处理 0xC1 / 0xC2。 */
|
||||
/** @brief 是否处理 0xC1 / 0xC2 / 0xC8。 */
|
||||
bool canHandle(uint8_t frame_type) const override
|
||||
{
|
||||
return (frame_type == SpiDefs::kTypeCanFeedback)
|
||||
|| (frame_type == SpiDefs::kTypeCanSetMode);
|
||||
|| (frame_type == SpiDefs::kTypeCanSetMode)
|
||||
|| (frame_type == SpiDefs::kTypeCanSetSilent);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 分发到 handleFeedback / handleSetMode。
|
||||
* @brief 分发到 handleFeedback / handleSetMode / handleSetSilent。
|
||||
* @param bus 命令总线
|
||||
* @param ctx 请求上下文
|
||||
*/
|
||||
@@ -157,6 +168,8 @@ public:
|
||||
{
|
||||
if (ctx.frame_type == SpiDefs::kTypeCanFeedback) {
|
||||
handleFeedback(bus, ctx);
|
||||
} else if (ctx.frame_type == SpiDefs::kTypeCanSetSilent) {
|
||||
handleSetSilent(bus, ctx);
|
||||
} else {
|
||||
handleSetMode(bus, ctx);
|
||||
}
|
||||
@@ -237,6 +250,58 @@ private:
|
||||
bus.buildResponse(ctx.tx_buf, ctx.tx_buf_size, ctx.tx_len,
|
||||
SpiDefs::kRspAck, &actual, 1U);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 0xC8:切换指定通道收发器 Silent(只听不发,S 脚控制)。
|
||||
* @param bus 命令总线
|
||||
* @param ctx 请求上下文(payload[0]=ch, [1]=silent 1/0)
|
||||
* @note 运行在 SPI CS EXTI ISR 上下文:只做 GPIO + 标志位;退 Silent 的 20µs
|
||||
* 收发器建立时间由 CanFd::setTxSilent 内部用 DWT 忙等,禁止 SysTick delayMs。
|
||||
*/
|
||||
void handleSetSilent(AppSpiCommand &bus, const SpiCommandContext &ctx)
|
||||
{
|
||||
if (ctx.payload_len < 2U) {
|
||||
uint8_t err = 0xE0U;
|
||||
bus.buildResponse(ctx.tx_buf, ctx.tx_buf_size, ctx.tx_len,
|
||||
SpiDefs::kRspError, &err, 1U);
|
||||
return;
|
||||
}
|
||||
|
||||
const uint8_t channel = ctx.payload[0];
|
||||
const bool silent = (ctx.payload[1] != 0U);
|
||||
|
||||
if (!CanFd::isChannelEnabled(channel)) {
|
||||
uint8_t err = 0xE1U;
|
||||
bus.buildResponse(ctx.tx_buf, ctx.tx_buf_size, ctx.tx_len,
|
||||
SpiDefs::kRspError, &err, 1U);
|
||||
return;
|
||||
}
|
||||
|
||||
const bool ok = CanFd::setTxSilent(channel, silent);
|
||||
const uint8_t actual = CanFd::isTxSilent(channel) ? 1U : 0U;
|
||||
|
||||
if (!ok) {
|
||||
/* S 脚回读与意图不符(被别处抢改 / GPIO 配置未生效):如实报错,不报成功 */
|
||||
uint8_t err = 0xE2U;
|
||||
bus.buildResponse(ctx.tx_buf, ctx.tx_buf_size, ctx.tx_len,
|
||||
SpiDefs::kRspError, &err, 1U);
|
||||
return;
|
||||
}
|
||||
|
||||
#if (CONFIG_SPI_APP_LOG_ENABLE != 0)
|
||||
char log_msg[AppSpiCommand::kLogBufSize] = {};
|
||||
(void)snprintf(log_msg, sizeof(log_msg),
|
||||
"CAN_SILENT ch=%u set=%u -> %u",
|
||||
static_cast<unsigned>(channel),
|
||||
static_cast<unsigned>(silent ? 1U : 0U),
|
||||
static_cast<unsigned>(actual));
|
||||
bus.postLog(log_msg);
|
||||
#endif
|
||||
|
||||
const uint8_t rsp[3] = { channel, actual, 1U };
|
||||
bus.buildResponse(ctx.tx_buf, ctx.tx_buf_size, ctx.tx_len,
|
||||
SpiDefs::kRspAck, rsp, 3U);
|
||||
}
|
||||
};
|
||||
|
||||
static SpiCanFeedbackHandler s_handler;
|
||||
|
||||
@@ -1,18 +1,23 @@
|
||||
/**
|
||||
* @file app_spi_cmd_exmc_diag.cpp
|
||||
* @brief SPI 0xA2 EXMC/HBI 诊断命令处理器:驱动 LAN9252 通断测试 pattern / NRST 手动诊断。
|
||||
* @brief SPI 0xA2 EXMC/HBI 诊断命令处理器:驱动 LAN9252 通断测试 pattern / NRST 手动诊断 / 电源管理 / ESP32 EN。
|
||||
*
|
||||
* 网页按钮 → ESP32 /spi/send → SPI 0xA2 → 本 handler → 分流:
|
||||
* action 0~10 → EscExmcBus::continuityDrive() (EXMC 数据/控制线 pattern)
|
||||
* action 20~24 → EscLan9252::nrstDrive() (ECAT_NRST/PB10 拉低/拉高/浮空/回读)
|
||||
* action 30~34 → EscLan9252::pmDrive() (电源管理:读状态/进D1/D2/D3/手动唤醒)
|
||||
* action 40~44 → Esp32En::drive() (ESP32 EN/PC9 诊断备援;开关请走 EtherCAT 0x2503)
|
||||
* 立即返回 ACK,不阻塞 SPI 总线。电平保持到下次调用或 action=0(stop)。
|
||||
* NRST action 返回的 status 字节 = 实际引脚电平(0/1),非 OK 码。
|
||||
* NRST/ESP32_EN action 返回的 status 字节 = 实际引脚电平(0/1),非 OK 码。
|
||||
* PM action 返回的 status 字节 = bit0=READY bit1=PM_SLEEP_EN bit3:2=PM_MODE;
|
||||
* 唤醒(34)另有 1=成功 0=超时;0xEE=无效 action,0xE1=HBI 未就绪。
|
||||
*/
|
||||
#include "device/spi/spi_command_handler.h"
|
||||
#include "device/spi/app_spi_command.h"
|
||||
#include "device/spi/app_spi_command_defs.h"
|
||||
#include "comm/esc_exmc_bus.h"
|
||||
#include "comm/esc_lan9252.h"
|
||||
#include "sys/esp32_en.h"
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -29,7 +34,11 @@ public:
|
||||
uint8_t status = 0xEE;
|
||||
if (ctx.payload_len >= 1) {
|
||||
const uint8_t action = ctx.payload[0];
|
||||
if (action >= 20U) {
|
||||
if (action >= 40U) {
|
||||
status = Esp32En::drive(action - 40U);
|
||||
} else if (action >= 30U) {
|
||||
status = EscLan9252::pmDrive(action - 30U);
|
||||
} else if (action >= 20U) {
|
||||
status = EscLan9252::nrstDrive(action - 20U);
|
||||
} else {
|
||||
status = EscExmcBus::continuityDrive(action);
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
/**
|
||||
* @file app_spi_cmd_spi_diag.cpp
|
||||
* @brief SPI 0xA4 链路全量诊断命令处理器。
|
||||
*
|
||||
* 供主机 (ESP32 / 调试脚本) 查询 GD32 侧 SPI0 从机实时运行诊断指标与异常现场快照:
|
||||
* payload[0]=sub_cmd:
|
||||
* 0 (或空载荷) -> 读取 ~88 字节全量诊断快照 (ACK)
|
||||
* 1 -> 复位清零所有诊断计数器 (ACK 1B: 0x00)
|
||||
* 2 -> 立即输出完整 ASCII 格式化报告到 GD32 USART0 串口 (ACK 1B: 0x00)
|
||||
*/
|
||||
#include "device/spi/spi_command_handler.h"
|
||||
#include "device/spi/app_spi_command.h"
|
||||
#include "device/spi/app_spi_command_defs.h"
|
||||
#include "device/spi/app_spi_diag.h"
|
||||
#include "gd32e50x_gpio.h"
|
||||
#include "sys/config.h"
|
||||
#include <cstring>
|
||||
#if (CONFIG_SPI_DIAG_ENABLE != 0)
|
||||
|
||||
namespace {
|
||||
|
||||
class SpiDiagHandler final : public ISpiCommandHandler {
|
||||
public:
|
||||
bool canHandle(uint8_t frame_type) const override
|
||||
{
|
||||
return frame_type == SpiDefs::kTypeSpiDiag;
|
||||
}
|
||||
|
||||
void handle(AppSpiCommand &bus, const SpiCommandContext &ctx) override
|
||||
{
|
||||
uint8_t sub_cmd = 0U;
|
||||
if (ctx.payload_len >= 1U && ctx.payload != nullptr) {
|
||||
sub_cmd = ctx.payload[0];
|
||||
}
|
||||
|
||||
if (sub_cmd == 1U) {
|
||||
// 复位清零诊断计数器
|
||||
resetSpiDiagStats();
|
||||
uint8_t status = 0x00U;
|
||||
bus.buildResponse(ctx.tx_buf, ctx.tx_buf_size, ctx.tx_len,
|
||||
SpiDefs::kRspAck, &status, 1U);
|
||||
return;
|
||||
}
|
||||
|
||||
if (sub_cmd == 2U) {
|
||||
// 立即向串口输出全量 ASCII 诊断报告
|
||||
dumpSpiDiag();
|
||||
uint8_t status = 0x00U;
|
||||
bus.buildResponse(ctx.tx_buf, ctx.tx_buf_size, ctx.tx_len,
|
||||
SpiDefs::kRspAck, &status, 1U);
|
||||
return;
|
||||
}
|
||||
|
||||
// 默认 / sub_cmd == 0: 返回二进制全量指标快照
|
||||
const SpiDiagStats &st = getSpiDiagStats();
|
||||
uint8_t buf[88] = {};
|
||||
|
||||
buf[0] = 0x00U; // status OK
|
||||
buf[1] = bus.isV2Active() ? 1U : 0U;
|
||||
writeU32Le(&buf[2], bus.getSessionId());
|
||||
buf[6] = bus.getLocalCredit();
|
||||
buf[7] = (gpio_input_bit_get(GPIOA, GPIO_PIN_15) == SET) ? 1U : 0U;
|
||||
buf[8] = (gpio_input_bit_get(CONFIG_SPI_READY_PORT, CONFIG_SPI_READY_PIN) == SET) ? 1U : 0U;
|
||||
buf[9] = bus.isCsActive() ? 1U : 0U;
|
||||
buf[10] = bus.isRingPending() ? 1U : 0U;
|
||||
writeU16Le(&buf[11], st.last_spi_stat);
|
||||
|
||||
writeU32Le(&buf[13], st.total_cs_assert);
|
||||
writeU32Le(&buf[17], st.total_cs_release);
|
||||
writeU32Le(&buf[21], st.valid_v2_frames);
|
||||
writeU32Le(&buf[25], st.parse_fail_count);
|
||||
writeU32Le(&buf[29], st.err_crc_mismatch);
|
||||
|
||||
writeU16Le(&buf[33], (uint16_t)st.err_no_sof);
|
||||
writeU16Le(&buf[35], (uint16_t)st.err_sof_drift);
|
||||
writeU16Le(&buf[37], (uint16_t)st.err_bad_len);
|
||||
writeU16Le(&buf[39], (uint16_t)st.rx_overrun_count);
|
||||
writeU16Le(&buf[41], (uint16_t)st.short_rx_len_count);
|
||||
writeU16Le(&buf[43], (uint16_t)st.zero_rx_len_count);
|
||||
writeU16Le(&buf[45], (uint16_t)st.all_ff_count);
|
||||
writeU16Le(&buf[47], (uint16_t)st.all_00_count);
|
||||
writeU16Le(&buf[49], (uint16_t)st.session_mismatch);
|
||||
writeU16Le(&buf[51], (uint16_t)st.seq_jump_count);
|
||||
writeU16Le(&buf[53], (uint16_t)st.ring_timeout_count);
|
||||
writeU16Le(&buf[55], (uint16_t)st.unknown_type_count);
|
||||
writeU16Le(&buf[57], (uint16_t)st.handler_empty_count);
|
||||
|
||||
buf[59] = st.last_rx_type;
|
||||
buf[60] = st.last_tx_type;
|
||||
buf[61] = st.last_error_code;
|
||||
buf[62] = st.last_error_reason;
|
||||
writeU16Le(&buf[63], (uint16_t)st.last_rx_len);
|
||||
writeU16Le(&buf[65], (uint16_t)st.last_error_rx_len);
|
||||
memcpy(&buf[67], st.last_error_bytes, 8);
|
||||
writeU32Le(&buf[75], st.last_error_calc_crc);
|
||||
writeU32Le(&buf[79], st.last_error_expected_crc);
|
||||
writeU32Le(&buf[83], st.last_error_uptime_ms);
|
||||
buf[87] = st.last_error_sof_offset;
|
||||
|
||||
bus.buildResponse(ctx.tx_buf, ctx.tx_buf_size, ctx.tx_len,
|
||||
SpiDefs::kRspAck, buf, sizeof(buf));
|
||||
}
|
||||
};
|
||||
|
||||
static SpiDiagHandler s_handler;
|
||||
|
||||
} // namespace
|
||||
|
||||
void registerSpiDiagCommand(AppSpiCommand &bus)
|
||||
{
|
||||
bus.registerHandler(&s_handler);
|
||||
}
|
||||
|
||||
#else /* CONFIG_SPI_DIAG_ENABLE == 0 */
|
||||
|
||||
void registerSpiDiagCommand(AppSpiCommand & /*bus*/) {}
|
||||
|
||||
#endif
|
||||
@@ -23,6 +23,7 @@
|
||||
#include "device/spi/app_spi_command.h"
|
||||
#include "device/spi/spi_command_handler.h"
|
||||
#include "device/spi/app_spi_command_defs.h"
|
||||
#include "device/spi/app_spi_diag.h"
|
||||
#include "spi_link_v2.h"
|
||||
#include "sys/ota_boot_request.h"
|
||||
|
||||
@@ -37,6 +38,8 @@ extern "C" {
|
||||
#include "sys/config.h"
|
||||
#include "sys/critical.h"
|
||||
#include "sys/systick.h"
|
||||
#include "sys/cpu_load.h"
|
||||
#include "sys/esp32_en.h"
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
@@ -166,7 +169,113 @@ static uint16_t s_lastRespLen = 0;
|
||||
static bool s_hasCachedResp = false;
|
||||
|
||||
/* ============================================================
|
||||
* v2 帧构建 / 帧解析(委托 spi_link_v2.h,三工程共用)
|
||||
* SPI 诊断统计与现场快照
|
||||
* ============================================================ */
|
||||
#if (CONFIG_SPI_DIAG_ENABLE != 0)
|
||||
#define SPI_DIAG_RECORD(...) do { __VA_ARGS__; } while (0)
|
||||
|
||||
static SpiDiagStats s_diag = {};
|
||||
|
||||
const SpiDiagStats& getSpiDiagStats()
|
||||
{
|
||||
s_diag.current_local_credit = (uint8_t)g_localCredit;
|
||||
s_diag.expected_rx_seq = g_expectedRxSeq;
|
||||
return s_diag;
|
||||
}
|
||||
|
||||
void resetSpiDiagStats()
|
||||
{
|
||||
memset(&s_diag, 0, sizeof(s_diag));
|
||||
}
|
||||
|
||||
const char* spiParseResultToString(SpiParseResult res)
|
||||
{
|
||||
switch (res) {
|
||||
case SPI_PARSE_OK: return "OK";
|
||||
case SPI_PARSE_ERR_TOO_SHORT: return "TOO_SHORT";
|
||||
case SPI_PARSE_ERR_ALL_FF: return "ALL_FF";
|
||||
case SPI_PARSE_ERR_ALL_00: return "ALL_00";
|
||||
case SPI_PARSE_ERR_NO_SOF: return "NO_SOF";
|
||||
case SPI_PARSE_ERR_BAD_VERSION: return "BAD_VERSION";
|
||||
case SPI_PARSE_ERR_BAD_LEN: return "BAD_LEN";
|
||||
case SPI_PARSE_ERR_CRC_MISMATCH: return "CRC_MISMATCH";
|
||||
default: return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
#else
|
||||
#define SPI_DIAG_RECORD(...) ((void)0)
|
||||
#endif
|
||||
|
||||
bool AppSpiCommand::isV2Active() const { return g_v2Active; }
|
||||
uint32_t AppSpiCommand::getSessionId() const { return g_sessionId; }
|
||||
uint8_t AppSpiCommand::getLocalCredit() const { return (uint8_t)g_localCredit; }
|
||||
void AppSpiCommand::dumpDiag() const { dumpSpiDiag(); }
|
||||
|
||||
#if (CONFIG_SPI_DIAG_ENABLE != 0)
|
||||
void dumpSpiDiag()
|
||||
{
|
||||
const SpiDiagStats &st = getSpiDiagStats();
|
||||
const uint32_t now = SysTickTimer::getMs();
|
||||
const uint8_t cs_pin = (gpio_input_bit_get(GPIOA, GPIO_PIN_15) == SET) ? 1U : 0U;
|
||||
const uint8_t rdy_pin = (gpio_input_bit_get(CONFIG_SPI_READY_PORT, CONFIG_SPI_READY_PIN) == SET) ? 1U : 0U;
|
||||
const uint16_t stat = (uint16_t)SPI_STAT(SPI0);
|
||||
|
||||
printf("\r\n==================== [GD32 SPI0 LINK DIAGNOSTICS] ====================\r\n");
|
||||
printf("Status : v2_active=%d session_id=0x%08lX local_credit=%u uptime=%lums\r\n",
|
||||
AppSpiCommandBus.isV2Active() ? 1 : 0, (unsigned long)AppSpiCommandBus.getSessionId(),
|
||||
(unsigned)AppSpiCommandBus.getLocalCredit(), (unsigned long)now);
|
||||
printf("Pins & HW: CS(PA15)=%u DATA_READY(PB6)=%u ring_pending=%d cs_active=%d\r\n",
|
||||
cs_pin, rdy_pin, AppSpiCommandBus.isRingPending() ? 1 : 0, AppSpiCommandBus.isCsActive() ? 1 : 0);
|
||||
printf("SPI_STAT : 0x%04X (TRANS:%d TBE:%d RBNE:%d OVR:%d FERR:%d)\r\n",
|
||||
stat, (stat & SPI_FLAG_TRANS) ? 1 : 0, (stat & SPI_FLAG_TBE) ? 1 : 0,
|
||||
(stat & SPI_FLAG_RBNE) ? 1 : 0, (stat & SPI_FLAG_RXORERR) ? 1 : 0,
|
||||
(stat & SPI_FLAG_FERR) ? 1 : 0);
|
||||
printf("CS Stats : assert=%lu release=%lu missed=%lu short(<20B)=%lu zero_len=%lu\r\n",
|
||||
(unsigned long)st.total_cs_assert, (unsigned long)st.total_cs_release,
|
||||
(unsigned long)st.missed_cs_assert, (unsigned long)st.short_rx_len_count,
|
||||
(unsigned long)st.zero_rx_len_count);
|
||||
printf("Frames : valid_v2=%lu parse_fail=%lu all_FF=%lu all_00=%lu\r\n",
|
||||
(unsigned long)st.valid_v2_frames, (unsigned long)st.parse_fail_count,
|
||||
(unsigned long)st.all_ff_count, (unsigned long)st.all_00_count);
|
||||
printf("ParseErr : CRC_err=%lu no_SOF=%lu SOF_drift=%lu bad_ver=%lu bad_len=%lu\r\n",
|
||||
(unsigned long)st.err_crc_mismatch, (unsigned long)st.err_no_sof,
|
||||
(unsigned long)st.err_sof_drift, (unsigned long)st.err_bad_version,
|
||||
(unsigned long)st.err_bad_len);
|
||||
printf("Hw Errors: RXORERR(ovr)=%lu FERR(fmt)=%lu\r\n",
|
||||
(unsigned long)st.rx_overrun_count, (unsigned long)st.format_error_count);
|
||||
printf("Session : HELLO_cnt=%lu mismatch=%lu seq_ok=%lu seq_jump=%lu seq_dup=%lu\r\n",
|
||||
(unsigned long)st.hello_count, (unsigned long)st.session_mismatch,
|
||||
(unsigned long)st.seq_ok_count, (unsigned long)st.seq_jump_count,
|
||||
(unsigned long)st.seq_dup_count);
|
||||
printf("Sequences: last_rx_seq=0x%02X exp_rx_seq=0x%02X last_tx_seq=0x%02X\r\n",
|
||||
st.last_rx_seq, st.expected_rx_seq, st.last_tx_seq);
|
||||
printf("Commands : last_rx_type=0x%02X last_tx_type=0x%02X error_rsp(0x82)=%lu (last_code=0x%02X)\r\n",
|
||||
st.last_rx_type, st.last_tx_type, (unsigned long)st.error_rsp_count, st.last_error_code);
|
||||
printf("Routing : unknown_type=%lu (last=0x%02X) handler_empty=%lu (last=0x%02X)\r\n",
|
||||
(unsigned long)st.unknown_type_count, st.last_unknown_type,
|
||||
(unsigned long)st.handler_empty_count, st.last_empty_type);
|
||||
printf("Ring Push: req=%lu timeout=%lu collision=%lu\r\n",
|
||||
(unsigned long)st.ring_request_count, (unsigned long)st.ring_timeout_count,
|
||||
(unsigned long)st.ring_collision_count);
|
||||
if (st.last_error_uptime_ms > 0U) {
|
||||
printf("LastErr : at %lums, rxLen=%u, reason=%s(0x%02X), SOF_off=%u\r\n",
|
||||
(unsigned long)st.last_error_uptime_ms, (unsigned)st.last_error_rx_len,
|
||||
spiParseResultToString((SpiParseResult)st.last_error_reason),
|
||||
st.last_error_code, (unsigned)st.last_error_sof_offset);
|
||||
printf("LastBytes: [%02X %02X %02X %02X %02X %02X %02X %02X]\r\n",
|
||||
st.last_error_bytes[0], st.last_error_bytes[1], st.last_error_bytes[2], st.last_error_bytes[3],
|
||||
st.last_error_bytes[4], st.last_error_bytes[5], st.last_error_bytes[6], st.last_error_bytes[7]);
|
||||
if (st.last_error_reason == SPI_PARSE_ERR_CRC_MISMATCH) {
|
||||
printf("LastCRC : calc=0x%08lX exp=0x%08lX\r\n",
|
||||
(unsigned long)st.last_error_calc_crc, (unsigned long)st.last_error_expected_crc);
|
||||
}
|
||||
}
|
||||
printf("======================================================================\r\n\r\n");
|
||||
}
|
||||
#endif
|
||||
|
||||
/* ============================================================
|
||||
* v2 帧构建 / 诊断帧解析(委托 spi_link_v2.h,三工程共用)
|
||||
* ============================================================ */
|
||||
|
||||
/* 构建响应帧;上层接口(type/payload)保持不变,handler 无需改动。
|
||||
@@ -190,36 +299,120 @@ static bool spi_build_frame(uint8_t type, const uint8_t *payload, uint16_t paylo
|
||||
return true;
|
||||
}
|
||||
|
||||
/* 解析 v2 帧;委托 v2_parse_frame,扫描 SOF 并校验 VERSION/CRC32。
|
||||
* 仅 v2:v2_parse_frame 扫 SOF0/SOF1 + CRC32;禁止再加 v1 CRC8 分支。 */
|
||||
static bool spi_try_parse_frame(const uint8_t *frame, uint16_t frame_len,
|
||||
uint8_t *type, const uint8_t **payload, uint16_t *payload_len,
|
||||
v2_header_t *out_hdr = nullptr)
|
||||
/* 细粒度诊断帧解析:逐项分析失败原因(无SOF/相位漂移/版本不符/长度截断/CRC位错)。 */
|
||||
static bool spi_diagnose_and_parse_frame(
|
||||
const uint8_t *buf, size_t buf_len,
|
||||
uint8_t *out_type, const uint8_t **out_payload, uint16_t *out_payload_len,
|
||||
v2_header_t *out_hdr,
|
||||
SpiParseResult *out_res,
|
||||
uint32_t *out_calc_crc, uint32_t *out_exp_crc,
|
||||
uint8_t *out_sof_offset)
|
||||
{
|
||||
if (!frame || !type || !payload || !payload_len) { return false; }
|
||||
if (!buf || buf_len == 0U) {
|
||||
if (out_res) *out_res = SPI_PARSE_ERR_TOO_SHORT;
|
||||
if (out_sof_offset) *out_sof_offset = 0xFFU;
|
||||
return false;
|
||||
}
|
||||
|
||||
v2_header_t hdr;
|
||||
const uint8_t *pload = nullptr;
|
||||
const bool ok = v2_parse_frame(frame, frame_len, &hdr, &pload);
|
||||
if (!ok) { return false; }
|
||||
if (isAllBytesValue(buf, (uint16_t)buf_len, 0xFFU)) {
|
||||
if (out_res) *out_res = SPI_PARSE_ERR_ALL_FF;
|
||||
if (out_sof_offset) *out_sof_offset = 0xFFU;
|
||||
return false;
|
||||
}
|
||||
|
||||
*type = hdr.type;
|
||||
*payload = pload;
|
||||
*payload_len = hdr.payload_len;
|
||||
if (out_hdr) { *out_hdr = hdr; }
|
||||
return true;
|
||||
if (isAllBytesValue(buf, (uint16_t)buf_len, 0x00U)) {
|
||||
if (out_res) *out_res = SPI_PARSE_ERR_ALL_00;
|
||||
if (out_sof_offset) *out_sof_offset = 0xFFU;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (buf_len < V2_OVERHEAD) {
|
||||
if (out_res) *out_res = SPI_PARSE_ERR_TOO_SHORT;
|
||||
if (out_sof_offset) *out_sof_offset = 0xFFU;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool found_sof = false;
|
||||
size_t first_sof_offset = 0xFFU;
|
||||
SpiParseResult best_err = SPI_PARSE_ERR_NO_SOF;
|
||||
|
||||
for (size_t i = 0; i + V2_OVERHEAD <= buf_len; ++i) {
|
||||
if (buf[i + V2_OFF_SOF0] != V2_SOF0 || buf[i + V2_OFF_SOF1] != V2_SOF1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!found_sof) {
|
||||
found_sof = true;
|
||||
first_sof_offset = i;
|
||||
}
|
||||
|
||||
const uint8_t version = buf[i + V2_OFF_VERSION];
|
||||
if (version != V2_VERSION) {
|
||||
best_err = SPI_PARSE_ERR_BAD_VERSION;
|
||||
continue;
|
||||
}
|
||||
|
||||
const uint16_t plen = v2_get_u16le(&buf[i + V2_OFF_LEN]);
|
||||
if (plen > V2_MAX_PAYLOAD) {
|
||||
best_err = SPI_PARSE_ERR_BAD_LEN;
|
||||
continue;
|
||||
}
|
||||
|
||||
const size_t total = V2_OVERHEAD + plen;
|
||||
if (i + total > buf_len) {
|
||||
best_err = SPI_PARSE_ERR_BAD_LEN;
|
||||
continue;
|
||||
}
|
||||
|
||||
const uint32_t expect = v2_get_u32le(&buf[i + V2_HEADER_LEN + plen]);
|
||||
const uint32_t calc = v2_crc32(&buf[i], V2_HEADER_LEN + plen);
|
||||
if (expect != calc) {
|
||||
best_err = SPI_PARSE_ERR_CRC_MISMATCH;
|
||||
if (out_calc_crc) *out_calc_crc = calc;
|
||||
if (out_exp_crc) *out_exp_crc = expect;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (out_hdr) {
|
||||
out_hdr->version = version;
|
||||
out_hdr->flags = buf[i + V2_OFF_FLAGS];
|
||||
out_hdr->session_id = v2_get_u32le(&buf[i + V2_OFF_SESSION]);
|
||||
out_hdr->tx_seq = buf[i + V2_OFF_TX_SEQ];
|
||||
out_hdr->ack_seq = buf[i + V2_OFF_ACK_SEQ];
|
||||
out_hdr->credit = buf[i + V2_OFF_CREDIT];
|
||||
out_hdr->type = buf[i + V2_OFF_TYPE];
|
||||
out_hdr->payload_len = plen;
|
||||
out_hdr->frame_offset = i;
|
||||
}
|
||||
if (out_type) *out_type = buf[i + V2_OFF_TYPE];
|
||||
if (out_payload) *out_payload = (plen > 0U) ? &buf[i + V2_HEADER_LEN] : nullptr;
|
||||
if (out_payload_len) *out_payload_len = plen;
|
||||
if (out_res) *out_res = SPI_PARSE_OK;
|
||||
if (out_sof_offset) *out_sof_offset = (uint8_t)i;
|
||||
if (out_calc_crc) *out_calc_crc = calc;
|
||||
if (out_exp_crc) *out_exp_crc = expect;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (out_res) *out_res = best_err;
|
||||
if (out_sof_offset) *out_sof_offset = (uint8_t)first_sof_offset;
|
||||
return false;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* 调试日志辅助
|
||||
* 调试日志辅助(环形队列,避免事件并发时丢日志)
|
||||
* ============================================================ */
|
||||
|
||||
void AppSpiCommand::postLog(const char *msg)
|
||||
{
|
||||
#if (CONFIG_SPI_APP_LOG_ENABLE != 0)
|
||||
if (!msg) return;
|
||||
snprintf(m_logBuf, kLogBufSize, "[SPI-APP] %s", msg);
|
||||
m_logPending = true;
|
||||
uint8_t next = (uint8_t)((m_logHead + 1U) % kLogSlotCount);
|
||||
if (next != m_logTail) {
|
||||
strncpy(m_logQueue[m_logHead], msg, kLogSlotLen - 1U);
|
||||
m_logQueue[m_logHead][kLogSlotLen - 1U] = '\0';
|
||||
m_logHead = next;
|
||||
}
|
||||
#else
|
||||
(void)msg;
|
||||
#endif
|
||||
@@ -229,14 +422,29 @@ void AppSpiCommand::postLogFmt(const char *fmt, uint8_t val)
|
||||
{
|
||||
#if (CONFIG_SPI_APP_LOG_ENABLE != 0)
|
||||
if (!fmt) return;
|
||||
snprintf(m_logBuf, kLogBufSize, fmt, val);
|
||||
m_logPending = true;
|
||||
char tmp[kLogSlotLen];
|
||||
snprintf(tmp, sizeof(tmp), fmt, val);
|
||||
postLog(tmp);
|
||||
#else
|
||||
(void)fmt;
|
||||
(void)val;
|
||||
#endif
|
||||
}
|
||||
|
||||
void AppSpiCommand::postLogFmt32(const char *fmt, uint32_t v1, uint32_t v2)
|
||||
{
|
||||
#if (CONFIG_SPI_APP_LOG_ENABLE != 0)
|
||||
if (!fmt) return;
|
||||
char tmp[kLogSlotLen];
|
||||
snprintf(tmp, sizeof(tmp), fmt, (unsigned long)v1, (unsigned long)v2);
|
||||
postLog(tmp);
|
||||
#else
|
||||
(void)fmt;
|
||||
(void)v1;
|
||||
(void)v2;
|
||||
#endif
|
||||
}
|
||||
|
||||
void AppSpiCommand::buildResponse(uint8_t *tx_buf, uint16_t tx_buf_size,
|
||||
uint16_t *tx_len, uint8_t rsp_type,
|
||||
const uint8_t *payload, uint16_t payload_len)
|
||||
@@ -369,9 +577,18 @@ void AppSpiCommand::configureExti()
|
||||
{
|
||||
gpio_exti_source_select(GPIO_PORT_SOURCE_GPIOA, GPIO_PIN_SOURCE_15);
|
||||
exti_init(EXTI_15, EXTI_INTERRUPT, EXTI_TRIG_BOTH);
|
||||
exti_interrupt_enable(EXTI_15);
|
||||
/* 先清 STAT 再开 IMR。warm jump 下 BOOT 关中断跳转前 ESP32 持续拉 CS,
|
||||
* EXTI15 STAT 大概率残留;若先 enable IMR,残留位会立即回放成一次假 CS
|
||||
* 沿进 NVIC。 */
|
||||
exti_interrupt_flag_clear(EXTI_15);
|
||||
nvic_irq_enable(EXTI10_15_IRQn, 1U, 1U);
|
||||
exti_interrupt_enable(EXTI_15);
|
||||
/* SPI CS EXTI 抢占级 0。抢占级 1 与 PDI/SYNC1/SCHED 同级不抢占
|
||||
* (PDI 窗口可达数百 µs),且被 RT_CRITICAL(BASEPRI≥1) 完全屏蔽;
|
||||
* release ISR 一旦拖过 ESP32 的 CS 间隔(1ms),re-arm 落空,
|
||||
* 下一笔查询帧头部丢失。提到 0 后仅与 SYNC0/CAN-RX
|
||||
* (µs 级)同级排队,release 处理确定性 µs 级完成。代价:0xA2 pmDrive 的
|
||||
* 忙等(≤10ms,手动 PM 诊断才用)会阻塞 SYNC0——诊断窗口,可接受。 */
|
||||
nvic_irq_enable(EXTI10_15_IRQn, 0U, 1U);
|
||||
}
|
||||
|
||||
|
||||
@@ -391,18 +608,20 @@ extern void registerSpiAdcFeedbackCommand(AppSpiCommand &bus);
|
||||
extern void registerSpiMotorStateCommand(AppSpiCommand &bus);
|
||||
extern void registerSpiMotorOtaCommand(AppSpiCommand &bus);
|
||||
extern void registerSpiExmcDiagCommand(AppSpiCommand &bus);
|
||||
extern void registerSpiDiagCommand(AppSpiCommand &bus);
|
||||
AppSpiCommand AppSpiCommandBus;
|
||||
void AppSpiCommand::init()
|
||||
{
|
||||
m_csActive = false;
|
||||
m_rxLen = 0U;
|
||||
m_txLen = 0U;
|
||||
m_logPending = false;
|
||||
m_logHead = 0U;
|
||||
m_logTail = 0U;
|
||||
m_commandCount = 0U;
|
||||
m_ringPending = false;
|
||||
m_ringDeadline = 0U;
|
||||
memset(m_txBuf, 0xFF, sizeof(m_txBuf));
|
||||
memset(m_logBuf, 0, sizeof(m_logBuf));
|
||||
memset(m_logQueue, 0, sizeof(m_logQueue));
|
||||
memset(m_commands, 0, sizeof(m_commands));
|
||||
|
||||
registerSpiBootRequestCommand(AppSpiCommandBus);
|
||||
@@ -417,6 +636,7 @@ void AppSpiCommand::init()
|
||||
registerSpiMotorStateCommand(AppSpiCommandBus); /* CONFIG 关时为空注册 */
|
||||
registerSpiMotorOtaCommand(AppSpiCommandBus);
|
||||
registerSpiExmcDiagCommand(AppSpiCommandBus);
|
||||
registerSpiDiagCommand(AppSpiCommandBus);
|
||||
|
||||
configureGpio();
|
||||
configureDma();
|
||||
@@ -519,34 +739,122 @@ void AppSpiCommand::processCommand()
|
||||
const uint8_t *payload = nullptr;
|
||||
uint16_t payload_len = 0U;
|
||||
v2_header_t hdr;
|
||||
SpiParseResult parse_res = SPI_PARSE_OK;
|
||||
uint32_t calc_crc = 0U;
|
||||
uint32_t exp_crc = 0U;
|
||||
uint8_t sof_offset = 0xFFU;
|
||||
|
||||
if (!spi_try_parse_frame(m_rxBuf, m_rxLen, &frame_type, &payload, &payload_len, &hdr)) {
|
||||
postLogFmt("[SPI-APP] 非v2帧 0x%02X -> ERROR", m_rxBuf[0]);
|
||||
const bool parse_ok = spi_diagnose_and_parse_frame(
|
||||
m_rxBuf, m_rxLen, &frame_type, &payload, &payload_len, &hdr,
|
||||
&parse_res, &calc_crc, &exp_crc, &sof_offset);
|
||||
|
||||
if (!parse_ok) {
|
||||
SPI_DIAG_RECORD(
|
||||
s_diag.parse_fail_count++;
|
||||
s_diag.last_error_uptime_ms = SysTickTimer::getMs();
|
||||
s_diag.last_error_rx_len = m_rxLen;
|
||||
s_diag.last_error_calc_crc = calc_crc;
|
||||
s_diag.last_error_expected_crc = exp_crc;
|
||||
s_diag.last_error_sof_offset = sof_offset;
|
||||
s_diag.last_error_reason = (uint8_t)parse_res;
|
||||
s_diag.last_error_code = 0xE8U;
|
||||
memcpy(s_diag.last_error_bytes, m_rxBuf, (m_rxLen < 8U) ? m_rxLen : 8U);
|
||||
);
|
||||
|
||||
#if (CONFIG_SPI_APP_LOG_ENABLE != 0 && CONFIG_SPI_DIAG_ENABLE != 0)
|
||||
switch (parse_res) {
|
||||
case SPI_PARSE_ERR_CRC_MISMATCH: {
|
||||
SPI_DIAG_RECORD(s_diag.err_crc_mismatch++);
|
||||
char log_msg[128];
|
||||
snprintf(log_msg, sizeof(log_msg),
|
||||
"[SPI-DIAG][ERR] CRC mismatch: calc=0x%08lX exp=0x%08lX rxLen=%u",
|
||||
(unsigned long)calc_crc, (unsigned long)exp_crc, (unsigned)m_rxLen);
|
||||
postLog(log_msg);
|
||||
break;
|
||||
}
|
||||
case SPI_PARSE_ERR_NO_SOF: {
|
||||
SPI_DIAG_RECORD(s_diag.err_no_sof++);
|
||||
char log_msg[128];
|
||||
snprintf(log_msg, sizeof(log_msg),
|
||||
"[SPI-DIAG][ERR] No SOF: rxLen=%u raw=[%02X %02X %02X %02X]",
|
||||
(unsigned)m_rxLen, m_rxBuf[0], m_rxBuf[1], m_rxBuf[2], m_rxBuf[3]);
|
||||
postLog(log_msg);
|
||||
break;
|
||||
}
|
||||
case SPI_PARSE_ERR_BAD_VERSION: {
|
||||
SPI_DIAG_RECORD(s_diag.err_bad_version++);
|
||||
char log_msg[128];
|
||||
snprintf(log_msg, sizeof(log_msg),
|
||||
"[SPI-DIAG][ERR] Bad ver=0x%02X (exp 0x02) SOF_off=%u rxLen=%u",
|
||||
(sof_offset < m_rxLen - 2) ? m_rxBuf[sof_offset + 2] : 0xFFU,
|
||||
(unsigned)sof_offset, (unsigned)m_rxLen);
|
||||
postLog(log_msg);
|
||||
break;
|
||||
}
|
||||
case SPI_PARSE_ERR_BAD_LEN: {
|
||||
SPI_DIAG_RECORD(s_diag.err_bad_len++);
|
||||
char log_msg[128];
|
||||
snprintf(log_msg, sizeof(log_msg),
|
||||
"[SPI-DIAG][ERR] Bad len at SOF_off=%u rxLen=%u",
|
||||
(unsigned)sof_offset, (unsigned)m_rxLen);
|
||||
postLog(log_msg);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
char log_msg[128];
|
||||
snprintf(log_msg, sizeof(log_msg),
|
||||
"[SPI-DIAG][ERR] Parse fail res=%d rxLen=%u",
|
||||
(int)parse_res, (unsigned)m_rxLen);
|
||||
postLog(log_msg);
|
||||
break;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
if (sof_offset > 0U && sof_offset != 0xFFU) {
|
||||
SPI_DIAG_RECORD(s_diag.err_sof_drift++);
|
||||
}
|
||||
|
||||
/* 0xE8 payload 携带接收诊断——[0]=0xE8, [1]=rxLen, [2..3]=rxBuf 前 2 字节, [4]=parse_res, [5]=sof_offset */
|
||||
uint16_t tx_len = 0U;
|
||||
uint8_t err = 0xEEU;
|
||||
spi_build_frame(kRspError, &err, 1U, m_txBuf, &tx_len);
|
||||
uint8_t diag[8];
|
||||
diag[0] = 0xE8U;
|
||||
diag[1] = (m_rxLen > 255U) ? 255U : (uint8_t)m_rxLen;
|
||||
diag[2] = (m_rxLen > 0U) ? m_rxBuf[0] : 0xFFU;
|
||||
diag[3] = (m_rxLen > 1U) ? m_rxBuf[1] : 0xFFU;
|
||||
diag[4] = (uint8_t)parse_res;
|
||||
diag[5] = sof_offset;
|
||||
diag[6] = 0x5AU;
|
||||
diag[7] = 0xA5U;
|
||||
SPI_DIAG_RECORD(s_diag.error_rsp_count++);
|
||||
spi_build_frame(kRspError, diag, sizeof(diag), m_txBuf, &tx_len);
|
||||
m_txLen = tx_len;
|
||||
finishResponseAndReady();
|
||||
return;
|
||||
}
|
||||
|
||||
if (CONFIG_SPI_APP_LOG_ENABLE != 0 && !isFlashRelatedFrame(frame_type)) {
|
||||
postLogFmt("[SPI-APP] 收到非刷写 SPI type=0x%02X", frame_type);
|
||||
// 解析合法 v2 帧成功
|
||||
SPI_DIAG_RECORD(
|
||||
s_diag.valid_v2_frames++;
|
||||
s_diag.last_rx_type = frame_type;
|
||||
s_diag.last_rx_seq = hdr.tx_seq;
|
||||
);
|
||||
if (sof_offset > 0U && sof_offset != 0xFFU) {
|
||||
SPI_DIAG_RECORD(s_diag.err_sof_drift++);
|
||||
}
|
||||
|
||||
g_pendingAckSeq = hdr.tx_seq;
|
||||
|
||||
if (frame_type == V2_TYPE_LINK_HELLO) {
|
||||
SPI_DIAG_RECORD(s_diag.hello_count++);
|
||||
Esp32En::cancelRelinkFromSpi();
|
||||
g_sessionId = hdr.session_id;
|
||||
g_txSeq = 0;
|
||||
g_expectedRxSeq = 0;
|
||||
g_v2Active = true;
|
||||
s_hasCachedResp = false;
|
||||
|
||||
/* 与 requestEsp32Read() 一致(L5 8bc01dd):建帧前 credit=1,
|
||||
* 避免 TX 占用时 g_localCredit=0 写入 HELLO_ACK,导致主机 remoteCredit=0 拒发 PING。 */
|
||||
g_localCredit = 1U;
|
||||
/* 建连 + 开机身份一次完成:HELLO_ACK = 6B 链路 + 16B 身份(版本/开关) */
|
||||
|
||||
uint8_t identity[V2_APP_IDENTITY_LEN];
|
||||
fillGd32AppIdentityPayload(identity);
|
||||
size_t built_len = 0U;
|
||||
@@ -559,6 +867,10 @@ void AppSpiCommand::processCommand()
|
||||
memcpy(s_lastRespBuf, m_txBuf, m_txLen);
|
||||
s_lastRespLen = m_txLen;
|
||||
s_hasCachedResp = true;
|
||||
SPI_DIAG_RECORD(
|
||||
s_diag.last_tx_type = V2_TYPE_LINK_HELLO;
|
||||
s_diag.last_tx_seq = 0;
|
||||
);
|
||||
|
||||
finishResponseAndReady();
|
||||
return;
|
||||
@@ -566,42 +878,77 @@ void AppSpiCommand::processCommand()
|
||||
|
||||
if (g_v2Active) {
|
||||
if (hdr.session_id != g_sessionId) {
|
||||
postLogFmt("[SPI-APP] session 不匹配 0x%02X -> LINK_ERROR",
|
||||
(uint8_t)(hdr.session_id & 0xFFU));
|
||||
SPI_DIAG_RECORD(
|
||||
s_diag.session_mismatch++;
|
||||
s_diag.last_error_code = V2_ERR_SESSION;
|
||||
s_diag.last_error_uptime_ms = SysTickTimer::getMs();
|
||||
s_diag.error_rsp_count++;
|
||||
);
|
||||
#if (CONFIG_SPI_APP_LOG_ENABLE != 0)
|
||||
char log_msg[128];
|
||||
snprintf(log_msg, sizeof(log_msg),
|
||||
"[SPI-DIAG][ERR] Session mismatch: rx=0x%08lX != cur=0x%08lX",
|
||||
(unsigned long)hdr.session_id, (unsigned long)g_sessionId);
|
||||
postLog(log_msg);
|
||||
#endif
|
||||
|
||||
size_t built_len = 0U;
|
||||
v2_build_error_frame(g_sessionId, g_txSeq, hdr.tx_seq,
|
||||
(uint8_t)g_localCredit, V2_ERR_SESSION,
|
||||
(uint8_t)g_expectedRxSeq, m_txBuf, &built_len);
|
||||
m_txLen = (uint16_t)built_len;
|
||||
SPI_DIAG_RECORD(
|
||||
s_diag.last_tx_type = V2_TYPE_LINK_ERROR;
|
||||
s_diag.last_tx_seq = g_txSeq;
|
||||
);
|
||||
finishResponseAndReady();
|
||||
return;
|
||||
}
|
||||
|
||||
const uint8_t dist = v2_seq_distance(hdr.tx_seq, g_expectedRxSeq);
|
||||
if (dist == 0U) {
|
||||
SPI_DIAG_RECORD(s_diag.seq_ok_count++);
|
||||
g_expectedRxSeq = (uint8_t)(g_expectedRxSeq + 1U);
|
||||
} else if (dist >= 128U) {
|
||||
SPI_DIAG_RECORD(s_diag.seq_dup_count++);
|
||||
if (s_hasCachedResp && hdr.tx_seq == s_lastReqSeq) {
|
||||
postLogFmt("[SPI-APP] 重复请求 seq=0x%02X -> 重发缓存", hdr.tx_seq);
|
||||
postLogFmt("[SPI-DIAG][INFO] 重复请求 seq=0x%02X -> 重发缓存", hdr.tx_seq);
|
||||
memcpy(m_txBuf, s_lastRespBuf, s_lastRespLen);
|
||||
m_txLen = s_lastRespLen;
|
||||
finishResponseAndReady();
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
postLogFmt("[SPI-APP] SEQ jump rx=0x%02X", hdr.tx_seq);
|
||||
SPI_DIAG_RECORD(
|
||||
s_diag.seq_jump_count++;
|
||||
s_diag.last_error_code = V2_ERR_SEQ_JUMP;
|
||||
s_diag.last_error_uptime_ms = SysTickTimer::getMs();
|
||||
);
|
||||
#if (CONFIG_SPI_APP_LOG_ENABLE != 0)
|
||||
char log_msg[128];
|
||||
snprintf(log_msg, sizeof(log_msg),
|
||||
"[SPI-DIAG][WARN] SEQ jump: rx=0x%02X exp=0x%02X (gap=%u)",
|
||||
hdr.tx_seq, g_expectedRxSeq, (unsigned)dist);
|
||||
postLog(log_msg);
|
||||
#endif
|
||||
|
||||
g_expectedRxSeq = (uint8_t)(hdr.tx_seq + 1U);
|
||||
|
||||
if (frame_type != kTypeBootReq) {
|
||||
SPI_DIAG_RECORD(s_diag.error_rsp_count++);
|
||||
size_t built_len = 0U;
|
||||
v2_build_error_frame(g_sessionId, g_txSeq, hdr.tx_seq,
|
||||
(uint8_t)g_localCredit, V2_ERR_SEQ_JUMP,
|
||||
(uint8_t)g_expectedRxSeq, m_txBuf, &built_len);
|
||||
m_txLen = (uint16_t)built_len;
|
||||
SPI_DIAG_RECORD(
|
||||
s_diag.last_tx_type = V2_TYPE_LINK_ERROR;
|
||||
s_diag.last_tx_seq = g_txSeq;
|
||||
);
|
||||
finishResponseAndReady();
|
||||
return;
|
||||
}
|
||||
postLog("SEQ jump but BOOT_REQ still handled");
|
||||
postLog("[SPI-DIAG] SEQ jump but BOOT_REQ still handled");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -626,17 +973,46 @@ void AppSpiCommand::processCommand()
|
||||
}
|
||||
|
||||
if (!handled) {
|
||||
postLogFmt("[SPI-APP] 未知 type=0x%02X -> ERROR", frame_type);
|
||||
uint8_t err = 0xEEU;
|
||||
SPI_DIAG_RECORD(
|
||||
s_diag.unknown_type_count++;
|
||||
s_diag.last_unknown_type = frame_type;
|
||||
s_diag.last_error_code = 0xE9U;
|
||||
s_diag.last_error_uptime_ms = SysTickTimer::getMs();
|
||||
s_diag.error_rsp_count++;
|
||||
);
|
||||
#if (CONFIG_SPI_APP_LOG_ENABLE != 0)
|
||||
char log_msg[128];
|
||||
snprintf(log_msg, sizeof(log_msg),
|
||||
"[SPI-DIAG][ERR] 未知 type=0x%02X -> ERROR 0xE9", frame_type);
|
||||
postLog(log_msg);
|
||||
#endif
|
||||
uint8_t err = 0xE9U; /* E9=无 handler 认领 */
|
||||
spi_build_frame(kRspError, &err, 1U, m_txBuf, &tx_len);
|
||||
}
|
||||
|
||||
if (tx_len == 0U) {
|
||||
uint8_t err = 0xEEU;
|
||||
SPI_DIAG_RECORD(
|
||||
s_diag.handler_empty_count++;
|
||||
s_diag.last_empty_type = frame_type;
|
||||
s_diag.last_error_code = 0xEAU;
|
||||
s_diag.last_error_uptime_ms = SysTickTimer::getMs();
|
||||
s_diag.error_rsp_count++;
|
||||
);
|
||||
#if (CONFIG_SPI_APP_LOG_ENABLE != 0)
|
||||
char log_msg[128];
|
||||
snprintf(log_msg, sizeof(log_msg),
|
||||
"[SPI-DIAG][ERR] handler t=0x%02X tx_len=0 -> ERROR 0xEA", frame_type);
|
||||
postLog(log_msg);
|
||||
#endif
|
||||
uint8_t err = 0xEAU; /* EA=handler 命中但未产出响应 */
|
||||
spi_build_frame(kRspError, &err, 1U, m_txBuf, &tx_len);
|
||||
}
|
||||
|
||||
m_txLen = tx_len;
|
||||
SPI_DIAG_RECORD(
|
||||
s_diag.last_tx_type = m_txBuf[V2_OFF_TYPE];
|
||||
s_diag.last_tx_seq = g_txSeq;
|
||||
);
|
||||
|
||||
if (g_v2Active) {
|
||||
s_lastReqSeq = hdr.tx_seq;
|
||||
@@ -670,8 +1046,47 @@ void AppSpiCommand::cancelRing()
|
||||
updateDataReady();
|
||||
}
|
||||
|
||||
void AppSpiCommand::resetEsp32Link()
|
||||
{
|
||||
/* ESP32 被 PC9 掉电可能死在半笔 CS 里,SPI0/DMA/EXTI 都可能卡死。
|
||||
* 做一次完整外设重初始化(对齐 init 的硬件序列),而不是只清软件状态。
|
||||
* 仅在 PC9 翻转、对端确定不在发帧时调用。 */
|
||||
nvic_irq_disable(EXTI10_15_IRQn);
|
||||
exti_interrupt_disable(EXTI_15);
|
||||
|
||||
g_v2Active = false;
|
||||
g_sessionId = 0U;
|
||||
g_txSeq = 0U;
|
||||
g_expectedRxSeq = 0U;
|
||||
g_pendingAckSeq = 0U;
|
||||
g_localCredit = 1U;
|
||||
s_lastReqSeq = 0xFFU;
|
||||
s_lastRespLen = 0U;
|
||||
s_hasCachedResp = false;
|
||||
|
||||
m_csActive = false;
|
||||
m_rxLen = 0U;
|
||||
m_ringPending = false;
|
||||
m_ringDeadline = 0U;
|
||||
m_txLen = 0U;
|
||||
memset(m_txBuf, 0xFF, sizeof(m_txBuf));
|
||||
memset(m_rxBuf, 0x00, sizeof(m_rxBuf));
|
||||
|
||||
/* 硬件重初始化:SPI0 + DMA + CS EXTI(与 init() 同序列) */
|
||||
configureDma();
|
||||
configureSpi0();
|
||||
armSpiDmasForNextCs();
|
||||
updateDataReady();
|
||||
configureExti();
|
||||
#if (CONFIG_SPI_APP_LOG_ENABLE != 0)
|
||||
printf("[SPI-APP] resetEsp32Link: SPI0/DMA/EXTI re-init done\r\n");
|
||||
#endif
|
||||
}
|
||||
|
||||
bool AppSpiCommand::requestEsp32Read(uint8_t frame_type, const uint8_t *payload, uint16_t payload_len)
|
||||
{
|
||||
SPI_DIAG_RECORD(s_diag.ring_request_count++);
|
||||
|
||||
/* 若上一次按铃尚未被 ESP32 读走,拒绝重复按铃 */
|
||||
if (m_ringPending) {
|
||||
return false;
|
||||
@@ -704,39 +1119,61 @@ bool AppSpiCommand::requestEsp32Read(uint8_t frame_type, const uint8_t *payload,
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* 主循环轮询:打印延迟日志 + 按铃超时看护
|
||||
* 主循环轮询:打印延迟日志 + 按铃超时看护 + 周期/异常诊断报告
|
||||
* ============================================================ */
|
||||
|
||||
void AppSpiCommand::poll()
|
||||
{
|
||||
/* 打印 ISR 推迟的日志 */
|
||||
char log_copy[kLogBufSize] = {};
|
||||
bool has_log = false;
|
||||
|
||||
uint32_t save = RT_CRITICAL_SAVE();
|
||||
if (m_logPending) {
|
||||
memcpy(log_copy, m_logBuf, sizeof(log_copy));
|
||||
m_logPending = false;
|
||||
has_log = true;
|
||||
}
|
||||
RT_CRITICAL_RESTORE(save);
|
||||
|
||||
if (has_log) {
|
||||
printf("%s\r\n", log_copy);
|
||||
/* 1. 打印 ISR 推迟的日志队列(环形缓冲,避免丢弃) */
|
||||
while (m_logTail != m_logHead) {
|
||||
printf("%s\r\n", m_logQueue[m_logTail]);
|
||||
m_logTail = (uint8_t)((m_logTail + 1U) % kLogSlotCount);
|
||||
}
|
||||
|
||||
/* 按铃超时看护:ESP32 未在 kRingTimeoutMs 内来读则放弃本次按铃。
|
||||
/* 2. 按铃超时看护:ESP32 未在 kRingTimeoutMs 内来读则放弃本次按铃。
|
||||
* 防止 ESP32 不响应时永久占用 SPIready 线导致双方死锁。 */
|
||||
if (m_ringPending) {
|
||||
uint32_t save2 = RT_CRITICAL_SAVE();
|
||||
if (m_ringPending) {
|
||||
if ((int32_t)(SysTickTimer::getMs() - m_ringDeadline) >= 0) {
|
||||
printf("[SPI-APP] 按铃超时取消 (ESP32 未响应, 释放 SPIready 重试)\r\n");
|
||||
SPI_DIAG_RECORD(s_diag.ring_timeout_count++);
|
||||
#if (CONFIG_SPI_DIAG_PERIODIC_PRINT != 0)
|
||||
printf("[SPI-DIAG][WARN] 按铃超时取消 (ESP32 未在 %lums 内响应, 释放 SPIready 重试)\r\n",
|
||||
(unsigned long)kRingTimeoutMs);
|
||||
#endif
|
||||
cancelRing();
|
||||
}
|
||||
}
|
||||
RT_CRITICAL_RESTORE(save2);
|
||||
}
|
||||
|
||||
#if (CONFIG_SPI_DIAG_ENABLE != 0 && CONFIG_SPI_DIAG_PERIODIC_PRINT != 0)
|
||||
/* 3. 定期 / 增量打印 SPI 诊断摘要 */
|
||||
static uint32_t s_lastDiagPrintMs = 0;
|
||||
static uint32_t s_lastDiagErrCount = 0;
|
||||
const uint32_t now = SysTickTimer::getMs();
|
||||
const uint32_t totalErrs = s_diag.parse_fail_count + s_diag.rx_overrun_count +
|
||||
s_diag.short_rx_len_count + s_diag.session_mismatch +
|
||||
s_diag.seq_jump_count + s_diag.ring_timeout_count +
|
||||
s_diag.unknown_type_count + s_diag.handler_empty_count;
|
||||
|
||||
const bool hasNewError = (totalErrs > s_lastDiagErrCount && (now - s_lastDiagPrintMs >= 2000U));
|
||||
const bool periodicCheck = (s_diag.total_cs_assert > 0U && (now - s_lastDiagPrintMs >= 5000U));
|
||||
|
||||
if (hasNewError || periodicCheck) {
|
||||
s_lastDiagPrintMs = now;
|
||||
s_lastDiagErrCount = totalErrs;
|
||||
const uint8_t cs_pin = (gpio_input_bit_get(GPIOA, GPIO_PIN_15) == SET) ? 1U : 0U;
|
||||
const uint8_t rdy_pin = (gpio_input_bit_get(CONFIG_SPI_READY_PORT, CONFIG_SPI_READY_PIN) == SET) ? 1U : 0U;
|
||||
printf("[SPI-DIAG] CS:assert=%lu rls=%lu ok_v2=%lu | err[CRC:%lu noSOF:%lu short:%lu ovr:%lu sessMis:%lu jmp:%lu tmo:%lu] pins[CS:%d RDY:%d STAT:0x%02X]\r\n",
|
||||
(unsigned long)s_diag.total_cs_assert, (unsigned long)s_diag.total_cs_release,
|
||||
(unsigned long)s_diag.valid_v2_frames,
|
||||
(unsigned long)s_diag.err_crc_mismatch, (unsigned long)s_diag.err_no_sof,
|
||||
(unsigned long)s_diag.short_rx_len_count, (unsigned long)s_diag.rx_overrun_count,
|
||||
(unsigned long)s_diag.session_mismatch, (unsigned long)s_diag.seq_jump_count,
|
||||
(unsigned long)s_diag.ring_timeout_count, cs_pin, rdy_pin, (unsigned)(s_diag.last_spi_stat & 0xFFU));
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
@@ -745,6 +1182,7 @@ void AppSpiCommand::poll()
|
||||
|
||||
void AppSpiCommand::onCsAssert()
|
||||
{
|
||||
SPI_DIAG_RECORD(s_diag.total_cs_assert++);
|
||||
if (m_csActive) {
|
||||
return;
|
||||
}
|
||||
@@ -756,9 +1194,16 @@ void AppSpiCommand::onCsAssert()
|
||||
|
||||
void AppSpiCommand::onCsRelease()
|
||||
{
|
||||
SPI_DIAG_RECORD(s_diag.total_cs_release++);
|
||||
if (!m_csActive) {
|
||||
/* EXTI 丢失下降沿时:仍尝试收尾并 re-arm,避免永久失步(对齐 BOOT) */
|
||||
m_rxLen = stopSpiDmas(kRxBufSize);
|
||||
SPI_DIAG_RECORD(
|
||||
s_diag.missed_cs_assert++;
|
||||
s_diag.last_rx_len = m_rxLen;
|
||||
s_diag.last_spi_stat = (uint16_t)SPI_STAT(SPI0);
|
||||
);
|
||||
postLogFmt("[SPI-DIAG][WARN] CS release without assert (got %u B)", (uint8_t)m_rxLen);
|
||||
drainSpiRxHw();
|
||||
if (m_txLen > 0U) {
|
||||
m_txLen = 0U;
|
||||
@@ -766,6 +1211,9 @@ void AppSpiCommand::onCsRelease()
|
||||
}
|
||||
if (m_rxLen >= V2_OVERHEAD) {
|
||||
processCommand();
|
||||
} else {
|
||||
/* 残包:SPI 可能已乱,重配外设 */
|
||||
configureSpi0();
|
||||
}
|
||||
armSpiDmasForNextCs();
|
||||
updateDataReady();
|
||||
@@ -775,18 +1223,15 @@ void AppSpiCommand::onCsRelease()
|
||||
|
||||
/* 停 DMA 并取本笔 RX 长度(先读 CHCNT 再 disable,否则计数器被清) */
|
||||
m_rxLen = stopSpiDmas(kRxBufSize);
|
||||
SPI_DIAG_RECORD(
|
||||
s_diag.last_rx_len = m_rxLen;
|
||||
s_diag.last_spi_stat = (uint16_t)SPI_STAT(SPI0);
|
||||
);
|
||||
drainSpiRxHw();
|
||||
|
||||
/* 若本次 CS 事务是 ESP32 响应 GD32 按铃来读请求帧,
|
||||
* 则 GD32 的请求帧已通过 DMA 输出到 MISO,取消按铃状态。
|
||||
*
|
||||
* 但需区分:ESP32 发的是 dummy 读(按铃响应)还是 v2 协议帧(如 PING/OTA/0xB0)。
|
||||
* 若 GD32 正在按铃时 ESP32 发协议帧,会因 m_ringPending=true 被误吞,
|
||||
* 导致命令无法处理。检测到 v2 SOF 时取消按铃并走正常命令处理。 */
|
||||
* 则 GD32 的请求帧已通过 DMA 输出到 MISO,取消按铃状态。 */
|
||||
if (m_ringPending) {
|
||||
/* 判断本次收到的数据是否为 v2 协议帧(扫描 SOF0 SOF1)。
|
||||
* 取代旧 containsSyncSof,直接用 V2_SOF0/V2_SOF1。
|
||||
* 0xB0 进 BOOT 现已走标准 v2 帧 handler,不再单独检测裸字节。 */
|
||||
bool is_v2_frame = false;
|
||||
if (m_rxLen >= 2U) {
|
||||
const uint16_t last = (m_rxLen < 16U) ? (uint16_t)(m_rxLen - 1U) : 16U;
|
||||
@@ -798,9 +1243,8 @@ void AppSpiCommand::onCsRelease()
|
||||
}
|
||||
}
|
||||
if (is_v2_frame) {
|
||||
/* ESP32 发的是 v2 协议帧(如 PING/OTA/0xB0 等),不是按铃响应。
|
||||
* 取消按铃,清空 TX 让 processCommand() 重新装载对应应答。
|
||||
* processCommand() 末尾由 updateDataReady() 依据新 m_txLen 驱动 PB6。 */
|
||||
SPI_DIAG_RECORD(s_diag.ring_collision_count++);
|
||||
postLog("[SPI-DIAG][INFO] Ring collision: host sent v2 frame while ring pending");
|
||||
m_ringPending = false;
|
||||
m_txLen = 0U;
|
||||
memset(m_txBuf, 0xFF, sizeof(m_txBuf));
|
||||
@@ -819,8 +1263,46 @@ void AppSpiCommand::onCsRelease()
|
||||
}
|
||||
|
||||
if (shouldIgnoreTransaction(m_rxBuf, m_rxLen)) {
|
||||
if (m_rxLen == 0U) {
|
||||
SPI_DIAG_RECORD(
|
||||
s_diag.zero_rx_len_count++;
|
||||
s_diag.last_error_reason = (uint8_t)SPI_PARSE_ERR_TOO_SHORT;
|
||||
s_diag.last_error_rx_len = 0U;
|
||||
s_diag.last_error_uptime_ms = SysTickTimer::getMs();
|
||||
);
|
||||
postLog("[SPI-DIAG][WARN] Ignored 0-byte transaction (SCK did not toggle)");
|
||||
} else if (isAllBytesValue(m_rxBuf, m_rxLen, 0xFFU)) {
|
||||
SPI_DIAG_RECORD(
|
||||
s_diag.all_ff_count++;
|
||||
s_diag.last_error_reason = (uint8_t)SPI_PARSE_ERR_ALL_FF;
|
||||
s_diag.last_error_rx_len = m_rxLen;
|
||||
memcpy(s_diag.last_error_bytes, m_rxBuf, (m_rxLen < 8U) ? m_rxLen : 8U);
|
||||
s_diag.last_error_uptime_ms = SysTickTimer::getMs();
|
||||
);
|
||||
postLogFmt("[SPI-DIAG][WARN] Ignored all-0xFF transaction (%u bytes)", (uint8_t)m_rxLen);
|
||||
} else if (isAllBytesValue(m_rxBuf, m_rxLen, 0x00U)) {
|
||||
SPI_DIAG_RECORD(
|
||||
s_diag.all_00_count++;
|
||||
s_diag.last_error_reason = (uint8_t)SPI_PARSE_ERR_ALL_00;
|
||||
s_diag.last_error_rx_len = m_rxLen;
|
||||
memcpy(s_diag.last_error_bytes, m_rxBuf, (m_rxLen < 8U) ? m_rxLen : 8U);
|
||||
s_diag.last_error_uptime_ms = SysTickTimer::getMs();
|
||||
);
|
||||
postLogFmt("[SPI-DIAG][WARN] Ignored all-0x00 transaction (%u bytes)", (uint8_t)m_rxLen);
|
||||
} else {
|
||||
SPI_DIAG_RECORD(
|
||||
s_diag.short_rx_len_count++;
|
||||
s_diag.last_error_reason = (uint8_t)SPI_PARSE_ERR_TOO_SHORT;
|
||||
s_diag.last_error_rx_len = m_rxLen;
|
||||
memcpy(s_diag.last_error_bytes, m_rxBuf, (m_rxLen < 8U) ? m_rxLen : 8U);
|
||||
s_diag.last_error_uptime_ms = SysTickTimer::getMs();
|
||||
);
|
||||
postLogFmt("[SPI-DIAG][WARN] Ignored short transaction (%u bytes < 20B)", (uint8_t)m_rxLen);
|
||||
}
|
||||
m_rxLen = 0U;
|
||||
/* v2 DATA_READY: 无待处理帧时 updateDataReady() 释放 PB6。 */
|
||||
/* 起机毛刺/相位漂移后仅 re-arm DMA 不够:SPI0 可能已 OVR 卡死。
|
||||
* 顺手重配 SPI0+DMA(不动 EXTI/session),下一笔 HELLO 才能收进来。 */
|
||||
configureSpi0();
|
||||
armSpiDmasForNextCs();
|
||||
updateDataReady();
|
||||
return;
|
||||
@@ -828,12 +1310,6 @@ void AppSpiCommand::onCsRelease()
|
||||
|
||||
m_txLen = 0U;
|
||||
memset(m_txBuf, 0xFF, sizeof(m_txBuf));
|
||||
|
||||
/* ★ 关键:在 processCommand 之前同步 g_localCredit。
|
||||
* 此时 m_txLen=0, m_ringPending=false → g_localCredit=1。
|
||||
* 若不做,buildResponse 会读到上一次 pushStatus/requestEsp32Read 留下的
|
||||
* g_localCredit=0 → 响应帧携带 credit=0 → ESP32 m_remoteCredit=0 → 后续发送死锁。
|
||||
* 这是电机 OTA 吐量只有 1 chunk/2s 的根因。 */
|
||||
updateDataReady();
|
||||
|
||||
/* 命令处理在 EXTI ISR 内同步完成(对齐 BOOT 的 isrProcessFrame 模式),
|
||||
@@ -854,12 +1330,20 @@ void AppSpiCommand::onInterrupt()
|
||||
/* RX 已改走 DMA0_CH1:RBNE 逐字节中断已禁用,本 ISR 只保留错误标志清理。
|
||||
* CS 边沿处理全部在 onCsExtiInterrupt 的 EXTI15 ISR 里完成。 */
|
||||
if (spi_i2s_interrupt_flag_get(SPI0, SPI_I2S_INT_FLAG_RXORERR) != RESET) {
|
||||
/* RXORERR 理论上不应再出现(RX 走 DMA),但兜底清理避免标志卡死。
|
||||
* 读 SPI_DATA + SPI_STAT 清除 OVR 标志。 */
|
||||
SPI_DIAG_RECORD(
|
||||
s_diag.rx_overrun_count++;
|
||||
s_diag.last_spi_stat = (uint16_t)SPI_STAT(SPI0);
|
||||
);
|
||||
postLog("[SPI-DIAG][ERR] SPI0 RXORERR (RX Overrun Error)!");
|
||||
(void)spi_i2s_data_receive(SPI0);
|
||||
(void)SPI_STAT(SPI0);
|
||||
}
|
||||
if (spi_i2s_interrupt_flag_get(SPI0, SPI_I2S_INT_FLAG_FERR) != RESET) {
|
||||
SPI_DIAG_RECORD(
|
||||
s_diag.format_error_count++;
|
||||
s_diag.last_spi_stat = (uint16_t)SPI_STAT(SPI0);
|
||||
);
|
||||
postLog("[SPI-DIAG][ERR] SPI0 FERR (Frame Format Error)!");
|
||||
spi_i2s_format_error_clear(SPI0, SPI_FLAG_FERR);
|
||||
}
|
||||
}
|
||||
@@ -878,10 +1362,14 @@ void AppSpiCommand::onCsExtiInterrupt()
|
||||
|
||||
extern "C" void SPI0_IRQHandler(void)
|
||||
{
|
||||
CpuLoad::isrEnter();
|
||||
AppSpiCommandBus.onInterrupt();
|
||||
CpuLoad::isrExit();
|
||||
}
|
||||
|
||||
extern "C" void EXTI10_15_IRQHandler(void)
|
||||
{
|
||||
CpuLoad::isrEnter();
|
||||
AppSpiCommandBus.onCsExtiInterrupt();
|
||||
CpuLoad::isrExit();
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
#include <cstdint>
|
||||
#include "sys/config.h"
|
||||
#include "device/spi/app_spi_diag.h"
|
||||
|
||||
class ISpiCommandHandler;
|
||||
|
||||
@@ -51,6 +52,9 @@ public:
|
||||
/** @brief 记录一条带 1 字节参数的待输出日志。 */
|
||||
void postLogFmt(const char *fmt, uint8_t val);
|
||||
|
||||
/** @brief 记录一条带双 32 位参数的待输出日志。 */
|
||||
void postLogFmt32(const char *fmt, uint32_t v1, uint32_t v2);
|
||||
|
||||
/**
|
||||
* @brief 将应答帧构建到指定发送缓冲区。
|
||||
* @param[out] tx_buf 发送缓冲区。
|
||||
@@ -85,10 +89,34 @@ public:
|
||||
/** @brief 查询当前是否有未读按铃(ESP32 尚未来读)。 */
|
||||
bool isRingPending() const { return m_ringPending; }
|
||||
|
||||
/** @brief 查询 v2 会话是否已建立。 */
|
||||
bool isV2Active() const;
|
||||
|
||||
/** @brief 查询当前 v2 会话 ID。 */
|
||||
uint32_t getSessionId() const;
|
||||
|
||||
/** @brief 查询当前 local credit。 */
|
||||
uint8_t getLocalCredit() const;
|
||||
|
||||
/** @brief 查询 CS 是否当前处于激活(低电平)状态。 */
|
||||
bool isCsActive() const { return m_csActive; }
|
||||
|
||||
/** @brief 打印完整 ASCII SPI 链路诊断报告。 */
|
||||
void dumpDiag() const;
|
||||
|
||||
/** @brief 取消未完成的按铃(释放 SPIready、清 m_ringPending)。
|
||||
* 0xB0 进 BOOT 等路径需公开调用,避免 0xC1 残留盖住 ACK。 */
|
||||
void cancelRing();
|
||||
|
||||
/**
|
||||
* @brief ESP32 掉电/重上电后复位 SPI 链路(PC9 翻转时调用)。
|
||||
*
|
||||
* ESP32 被 EN 拉低时可能死在半笔 CS 事务里:session/序号/按铃/DMA 都会脏。
|
||||
* 重上电 HELLO 前必须清干净,否则首帧对不齐 → 永远建不起 v2 会话。
|
||||
* 清 session(HELLO 会重建)、取消按铃、re-arm DMA、释放 DATA_READY。
|
||||
*/
|
||||
void resetEsp32Link();
|
||||
|
||||
static constexpr uint16_t kRxBufSize = CONFIG_SPI_RX_BUF_SIZE;
|
||||
static constexpr uint16_t kTxBufSize = CONFIG_SPI_TX_BUF_SIZE;
|
||||
static constexpr uint16_t kLogBufSize = CONFIG_SPI_LOG_BUF_SIZE;
|
||||
@@ -102,7 +130,12 @@ private:
|
||||
/** 本笔 CS 实际收到的字节数(CS 上升沿时由 RX DMA CHCNT 反算得到)。 */
|
||||
volatile uint16_t m_rxLen = 0U;
|
||||
volatile uint16_t m_txLen = 0U;
|
||||
volatile bool m_logPending = false;
|
||||
/** 日志环形缓冲区:避免高频/多事件并发时单缓冲覆盖丢日志。 */
|
||||
static constexpr uint8_t kLogSlotCount = 8U;
|
||||
static constexpr uint8_t kLogSlotLen = 128U;
|
||||
char m_logQueue[kLogSlotCount][kLogSlotLen] = {};
|
||||
volatile uint8_t m_logHead = 0U;
|
||||
volatile uint8_t m_logTail = 0U;
|
||||
/** RX DMA 接收缓冲(替代旧 RBNE 逐字节累积的 m_rxBuf)。
|
||||
* CS 上升沿时 m_rxLen 为本笔实际收到的字节数,本缓冲前 m_rxLen 字节有效。 */
|
||||
uint8_t m_rxBuf[kRxBufSize] = {};
|
||||
|
||||
@@ -21,7 +21,10 @@ static constexpr uint8_t kTypeOtaCancel = 0x03U;
|
||||
static constexpr uint8_t kTypeAppPing = 0xA1U;
|
||||
/** @brief EXMC/HBI 诊断命令(ESP32→GD32):payload[0]=action。
|
||||
* 0=stop(恢复AF_PP), 1-4=数据pattern(0xFFFF/0x0000/0xAAAA/0x5555),
|
||||
* 5=CS低, 6=RD低, 7=WR低, 8=A1高, 9=A2高。用于 LAN9252 通断测试。 */
|
||||
* 5=CS低, 6=RD低, 7=WR低, 8=A1高, 9=A2高。用于 LAN9252 通断测试。
|
||||
* 20~24=ECAT_NRST(PB10) 30~34=LAN9252 电源管理
|
||||
* 40~44=ESP32 EN(PC9):40/42=拉高 41=拉低保持 43=浮空 44=回读
|
||||
* (运行期开关请走 EtherCAT SDO 0x2503,本组仅 SPI 诊断备援) */
|
||||
static constexpr uint8_t kTypeExmcDiag = 0xA2U;
|
||||
/** @brief APP 初始化状态查询(ESP32→GD32,1.5.34 新增):无 payload。
|
||||
* 响应 ACK 25B(1.5.35 起;1.5.34 仅前 12B):
|
||||
@@ -32,6 +35,10 @@ static constexpr uint8_t kTypeExmcDiag = 0xA2U;
|
||||
* [12..24] fault_codes[13](每模块 1B 故障码,0=成功;编码见 sys/initial.h,
|
||||
* canfd = 原因码|(首个失败通道<<4))。位定义与 sys/initial.h 一致。 */
|
||||
static constexpr uint8_t kTypeInitStatus = 0xA3U;
|
||||
/** @brief SPI 链路全量诊断查询与控制(ESP32→GD32):
|
||||
* payload[0]=sub_cmd: 0=读取全量诊断快照 (ACK ~88B), 1=复位诊断计数器, 2=打印完整报告到 GD32 串口。
|
||||
* 若 payload 为空,默认执行 sub_cmd=0。 */
|
||||
static constexpr uint8_t kTypeSpiDiag = 0xA4U;
|
||||
static constexpr uint8_t kTypeBootReq = 0xB0U;
|
||||
static constexpr uint8_t kTypeCanSend = 0xC0U;
|
||||
static constexpr uint8_t kTypeCanFeedback = 0xC1U; /**< ESP32 -> GD32 轮询 CAN 反馈快照 */
|
||||
@@ -55,6 +62,14 @@ static constexpr uint8_t kTypeMotorVersion = 0xC6U;
|
||||
* 响应 ACK: count(1) + 每条目 1 字节状态(CanTxResult 码)。 */
|
||||
static constexpr uint8_t kTypeCanBurstSend = 0xC7U;
|
||||
|
||||
/** @brief CAN 通道收发器 Silent 开关(ESP32→GD32):只听不发(驱动关断、接收照常)。
|
||||
* 请求 payload: [0] channel 0/1/2, [1] silent 1=进 Silent / 0=回 Normal。
|
||||
* 应答 ACK: [0] channel, [1] 实际生效的 silent(与发送门控同一个值),[2] 该通道是否使能。
|
||||
* ERROR: 0xE0=payload 过短, 0xE1=通道未使能/非法, 0xE2=S 脚回读与意图不符(标志已回滚)。
|
||||
* 生效期间该通道所有发送入口(0xC0/0xC7/桥接/relay)返回 0xE6=kErrSilent,
|
||||
* 不计错误计数;静默不持久化,上电 init 一律回 Normal。 */
|
||||
static constexpr uint8_t kTypeCanSetSilent = 0xC8U;
|
||||
|
||||
/** @brief 电机 OTA relay 命令(ESP32→GD32):start / cancel / query。
|
||||
* payload[0]=cmd(0=start,1=cancel,2=query), start 时 payload[1]=ch, payload[2..5]=fw_size LE */
|
||||
static constexpr uint8_t kTypeMotorOtaCmd = 0xD0U;
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
#ifndef DEVICE_APP_SPI_DIAG_H
|
||||
#define DEVICE_APP_SPI_DIAG_H
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
#include "sys/config.h"
|
||||
|
||||
/**
|
||||
* @file app_spi_diag.h
|
||||
* @brief SPI 链路全量硬件与协议诊断模块声明。
|
||||
*
|
||||
* 用于快速量化与定位 SPI 通信故障根因:
|
||||
* - 物理/时序层:CS 边沿跳变、丢失下降沿、0 字节事务(SCK未走/DMA落后)、短包(<20B)、全FF/全00
|
||||
* - 硬件状态:SPI0 RXORERR (接收FIFO溢出)、FERR (格式错误)、SPI_STAT 寄存器值
|
||||
* - 协议/帧层:SOF丢失、相位偏移(SOF drift)、版本错误、超长/截断、CRC32位跳变(附带期望值与计算值)
|
||||
* - 状态机/会话:Session ID脱节、序号跳变(丢包)、序号重复(重发)、未处理命令TYPE、空响应
|
||||
* - 握手/流控:PB6 DATA_READY 引脚电平、PA15 CS 引脚电平、主动按铃超时、按铃竞争碰撞
|
||||
* - 现场快照:记录最近一次异常发生时刻、实际接收长度、前导原始字节及 CRC 对比
|
||||
*/
|
||||
|
||||
/**
|
||||
* @brief SPI 帧解析失败原因细分
|
||||
*/
|
||||
enum SpiParseResult : uint8_t {
|
||||
SPI_PARSE_OK = 0U, ///< 成功解析合法 v2 帧
|
||||
SPI_PARSE_ERR_TOO_SHORT = 1U, ///< 事务长度小于 v2 最小帧头 (rxLen < 20)
|
||||
SPI_PARSE_ERR_ALL_FF = 2U, ///< 接收数据全为 0xFF (从机浮空/DMA未就绪/MISO未连接)
|
||||
SPI_PARSE_ERR_ALL_00 = 3U, ///< 接收数据全为 0x00 (地电平短路或总线死锁)
|
||||
SPI_PARSE_ERR_NO_SOF = 4U, ///< 未扫描到 SOF 同步字 (0xA5 0x5A)
|
||||
SPI_PARSE_ERR_BAD_VERSION = 5U, ///< 找到 SOF,但 VERSION != 0x02
|
||||
SPI_PARSE_ERR_BAD_LEN = 6U, ///< 长度异常 (payload_len > 236 或帧总长超出实际收到的 rxLen)
|
||||
SPI_PARSE_ERR_CRC_MISMATCH = 7U, ///< CRC32 校验失败 (物理信号干扰/位跳变)
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief SPI 链路全量诊断计数器与状态快照
|
||||
*/
|
||||
struct SpiDiagStats {
|
||||
/* 1. CS 边沿与事务统计 */
|
||||
uint32_t total_cs_assert; ///< CS 下降沿次数
|
||||
uint32_t total_cs_release; ///< CS 上升沿次数
|
||||
uint32_t missed_cs_assert; ///< 丢失 CS 下降沿异常次数
|
||||
uint32_t zero_rx_len_count; ///< RX 收到 0 字节 (SCK 未时钟化或 DMA 启动严重落后)
|
||||
uint32_t short_rx_len_count; ///< RX 收到 1~19 字节 (<20B 残包)
|
||||
uint32_t all_ff_count; ///< 全 0xFF 事务数
|
||||
uint32_t all_00_count; ///< 全 0x00 事务数
|
||||
|
||||
/* 2. 硬件外设与状态 */
|
||||
uint32_t rx_overrun_count; ///< SPI0 RXORERR (接收 FIFO 溢出)
|
||||
uint32_t format_error_count; ///< SPI0 FERR (帧格式错误)
|
||||
uint16_t last_spi_stat; ///< 最近一笔事务结束时的 SPI_STAT 寄存器值
|
||||
|
||||
/* 3. 帧解析统计 */
|
||||
uint32_t valid_v2_frames; ///< 成功解析的合法 v2 帧总数
|
||||
uint32_t parse_fail_count; ///< 帧解析失败总数
|
||||
uint32_t err_no_sof; ///< 未找到 SOF0(0xA5)+SOF1(0x5A)
|
||||
uint32_t err_sof_drift; ///< SOF 偏移不在索引 0 (前导毛刺或字节时序偏移)
|
||||
uint32_t err_bad_version; ///< 版本不匹配
|
||||
uint32_t err_bad_len; ///< 长度字段错误或事务截断
|
||||
uint32_t err_crc_mismatch; ///< CRC32 校验失败
|
||||
|
||||
/* 4. 会话与序号流控 */
|
||||
uint32_t hello_count; ///< 收到 LINK_HELLO 次数
|
||||
uint32_t session_mismatch; ///< Session ID 不匹配 (对端重置或会话脱节)
|
||||
uint32_t seq_ok_count; ///< 序号严格递增正常次数
|
||||
uint32_t seq_jump_count; ///< 序号跳变 (丢帧)
|
||||
uint32_t seq_dup_count; ///< 序号重复 (重发包)
|
||||
uint16_t last_rx_len; ///< 最近收到的 rx_len (字节数)
|
||||
uint8_t last_rx_seq; ///< 最近收到的 rx_seq
|
||||
uint8_t expected_rx_seq; ///< 当前期望的 rx_seq
|
||||
uint8_t last_tx_seq; ///< 最近发送的 tx_seq
|
||||
|
||||
/* 5. 命令路由与处理 */
|
||||
uint8_t last_rx_type; ///< 最近收到的命令 TYPE
|
||||
uint8_t last_tx_type; ///< 最近构建的响应 TYPE
|
||||
uint32_t unknown_type_count; ///< 未知命令 TYPE (无 handler 注册)
|
||||
uint8_t last_unknown_type; ///< 最近未知的 TYPE 值
|
||||
uint32_t handler_empty_count; ///< Handler 执行但返回 tx_len == 0
|
||||
uint8_t last_empty_type; ///< 最近产出 0 字节的 TYPE 值
|
||||
uint32_t error_rsp_count; ///< GD32 主动返回 0x82 ERROR 响应次数
|
||||
uint8_t last_error_code; ///< 最近一次返回 0x82 的错误码 (0xE8/0xE9/0xEA/V2_ERR_*)
|
||||
|
||||
/* 6. 握手引脚与流控 */
|
||||
uint32_t ring_request_count; ///< 主动按铃请求次数
|
||||
uint32_t ring_timeout_count; ///< 按铃超时 (ESP32 未在期限内读走)
|
||||
uint32_t ring_collision_count; ///< 按铃与 ESP32 主动发帧竞争碰撞次数
|
||||
uint8_t current_local_credit; ///< 当前 local credit (0 或 1)
|
||||
|
||||
/* 7. 最近异常快照 (用于定位具体错误现场) */
|
||||
uint32_t last_error_uptime_ms; ///< 异常发生时刻 SysTick 毫秒数
|
||||
uint16_t last_error_rx_len; ///< 异常事务收到的字节数
|
||||
uint8_t last_error_bytes[8]; ///< 异常事务前 8 字节原始数据
|
||||
uint32_t last_error_calc_crc; ///< CRC 错误时: 计算出的 CRC
|
||||
uint32_t last_error_expected_crc;///< CRC 错误时: 帧内携带的 CRC
|
||||
uint8_t last_error_sof_offset; ///< SOF 所在的偏移位置 (255 表示未找到)
|
||||
uint8_t last_error_reason; ///< SpiParseResult 错误原因枚举
|
||||
};
|
||||
|
||||
#if (CONFIG_SPI_DIAG_ENABLE != 0)
|
||||
/** @brief 获取当前全局 SPI 诊断统计快照引用 */
|
||||
const SpiDiagStats& getSpiDiagStats();
|
||||
|
||||
/** @brief 复位清零所有 SPI 诊断统计计数器 */
|
||||
void resetSpiDiagStats();
|
||||
|
||||
/** @brief 将解析结果枚举转换为可读字符串 */
|
||||
const char* spiParseResultToString(SpiParseResult res);
|
||||
|
||||
/** @brief 打印完整 SPI 链路诊断报告到串口 (ASCII 表格) */
|
||||
void dumpSpiDiag();
|
||||
#else
|
||||
inline const SpiDiagStats& getSpiDiagStats()
|
||||
{
|
||||
static const SpiDiagStats s_empty{};
|
||||
return s_empty;
|
||||
}
|
||||
inline void resetSpiDiagStats() {}
|
||||
inline const char* spiParseResultToString(SpiParseResult /*res*/) { return "DISABLED"; }
|
||||
inline void dumpSpiDiag() {}
|
||||
#endif
|
||||
|
||||
#endif /* DEVICE_APP_SPI_DIAG_H */
|
||||
@@ -7,6 +7,7 @@
|
||||
#include "device/usart0.h"
|
||||
#include "sys/config.h"
|
||||
#include "sys/critical.h"
|
||||
#include "sys/cpu_load.h"
|
||||
|
||||
uint8_t Usart0::s_txBuf[Usart0::kTxBufSize];
|
||||
volatile uint16_t Usart0::s_txHead = 0U;
|
||||
@@ -182,4 +183,9 @@ extern "C" int fputc(int ch, FILE *f) { return Usart0::putChar(ch, f); }
|
||||
* @retval 无返回值。
|
||||
* @note 该 C 包装函数内部转调 `Usart0::onInterrupt()`。
|
||||
*/
|
||||
extern "C" void USART0_IRQHandler(void) { Usart0::onInterrupt(); }
|
||||
extern "C" void USART0_IRQHandler(void)
|
||||
{
|
||||
CpuLoad::isrEnter();
|
||||
Usart0::onInterrupt();
|
||||
CpuLoad::isrExit();
|
||||
}
|
||||
|
||||
@@ -15,8 +15,7 @@ uint8_t Watchdog::init()
|
||||
{
|
||||
/* FWDGT 的 PSC/RLD 写入经 LSI(IRC40K) 时钟域同步(PUD/RUD 由硬件清零)。
|
||||
* BOOT 只检测 FWDGT 复位标志、从不启动看门狗,冷启动时 LSI 未开——
|
||||
* 直接配置会让 PUD/RUD 永远等不到清零(库超时返回 ERROR,1.5.34 起暴露为 init 失败;
|
||||
* 此前静默:FWDGT 实际按复位默认值 DIV4/0xFFF 运行,超时就与配置不符)。
|
||||
* 直接配置会让 PUD/RUD 永远等不到清零(库超时返回 ERROR)。
|
||||
* 必须先手动开 LSI 并等就绪,再配置。 */
|
||||
rcu_osci_on(RCU_IRC40K);
|
||||
if (rcu_osci_stab_wait(RCU_IRC40K) != SUCCESS) {
|
||||
@@ -24,7 +23,7 @@ uint8_t Watchdog::init()
|
||||
}
|
||||
|
||||
/* PSC/RLD 配置函数自带 ErrStatus(等待 PUD/RUD 清零带超时);
|
||||
* 此前返回值被忽略,写入失败会无声变成"看门狗实际未按预期运行"。 */
|
||||
* 必须检查返回值,否则写入失败会让看门狗实际未按预期运行。 */
|
||||
fwdgt_write_enable();
|
||||
uint8_t fault = kFaultOk;
|
||||
if (fwdgt_prescaler_value_config((uint16_t)CONFIG_FWDGT_PSC) != SUCCESS) {
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
#include "main.h"
|
||||
#include "sys/initial.h"
|
||||
#include "sys/cpu_load.h"
|
||||
#include "sys/task_manager.h"
|
||||
#include "sys/task_runner.h"
|
||||
#include "sys/config.h"
|
||||
@@ -45,8 +46,15 @@ int App::run()
|
||||
APP_LOGI("[APP] >>> TaskRunner ready, entering main loop\r\n");
|
||||
printBanner();
|
||||
|
||||
CpuLoad::init();
|
||||
|
||||
while(1) {
|
||||
/* 负载率测量(1.5.63 定义收窄):runPending 内部按"非延迟工作"分段挂钩
|
||||
* (ISR 全量 + 任务派发 + 电机 OTA relay,见 sys/cpu_load.h);
|
||||
* 常开轮询(SSC MainLoop / 各 poll)与 NOP 空转不计入。
|
||||
* tick() 每圈无条件做 1s 窗口滚动检查——滚动不依赖本圈是否有挂钩段。 */
|
||||
TaskRunner::runPending();
|
||||
CpuLoad::tick();
|
||||
#if (CONFIG_MAIN_LOOP_WFI_ENABLE != 0)
|
||||
uint32_t save = RT_CRITICAL_SAVE();
|
||||
if(TaskManager::state().task_flags == 0U) {
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* @file ota_layout.h
|
||||
* @brief APP/BOOT 共用 OTA Flash 布局(唯一真源)。
|
||||
* @note 两侧 sys/config.h 都必须 include 本头,禁止再抄地址常量。
|
||||
* 相对路径:BOOT/APP 的 sys/config.h → ../../../shared/ota_layout.h
|
||||
*
|
||||
* 约定:0x00000000=AVAL(有效),0xFFFFFFFF=NOT_AVAL(擦除态/无效)。
|
||||
*/
|
||||
#ifndef SHARED_OTA_LAYOUT_H
|
||||
#define SHARED_OTA_LAYOUT_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
/* ---- 整片 Flash 区段(GD32E508VE 512KB) ---- */
|
||||
#define OTA_LAYOUT_BOOT_START 0x08000000U
|
||||
#define OTA_LAYOUT_BOOT_SIZE 0x00012000U /* 72KB */
|
||||
#define OTA_LAYOUT_APP_BACK_START 0x08012000U /* 216KB 备份区 */
|
||||
#define OTA_LAYOUT_APP_MAIN_START 0x08048000U /* 216KB 主应用区 = APP VTOR */
|
||||
#define OTA_LAYOUT_APP_MAX_SIZE 0x00035FFCU /* 216KB-4B,末尾 CRC32 */
|
||||
#define OTA_LAYOUT_CONFIG_PAGE 0x0807E000U /* OTA 状态配置页 2KB */
|
||||
#define OTA_LAYOUT_SLEEP_PAGE 0x0807E800U /* 睡眠配置页 2KB */
|
||||
#define OTA_LAYOUT_PAGE_SIZE 0x00000800U /* FMC 单页 2KB */
|
||||
|
||||
/* ---- OTA 配置页 word 布局(word N = PAGE + 4*N) ---- */
|
||||
#define OTA_LAYOUT_WORD_LATEST_AVAL 0U
|
||||
#define OTA_LAYOUT_WORD_BACKUP_AVAL 1U
|
||||
#define OTA_LAYOUT_WORD_LATEST_EN 2U
|
||||
#define OTA_LAYOUT_WORD_BACKUP_EN 3U
|
||||
#define OTA_LAYOUT_WORD_LATEST_CHECK 4U
|
||||
#define OTA_LAYOUT_WORD_BACKUP_CHECK 5U
|
||||
#define OTA_LAYOUT_WORD_BOOT_VER0 6U /* major|minor */
|
||||
#define OTA_LAYOUT_WORD_BOOT_VER1 7U /* patch|build */
|
||||
#define OTA_LAYOUT_WORD_ECAT_ARM 8U /* 0xACA70001 才允许 ECAT SDO 刷 APP */
|
||||
#define OTA_LAYOUT_WORD_CRASH_COUNT 9U /* 连续 FWDGT 崩溃次数;APP 起板成功清 0 */
|
||||
|
||||
#define OTA_LAYOUT_ADDR_LATEST_AVAL (OTA_LAYOUT_CONFIG_PAGE + 4U * OTA_LAYOUT_WORD_LATEST_AVAL)
|
||||
#define OTA_LAYOUT_ADDR_BACKUP_AVAL (OTA_LAYOUT_CONFIG_PAGE + 4U * OTA_LAYOUT_WORD_BACKUP_AVAL)
|
||||
#define OTA_LAYOUT_ADDR_LATEST_EN (OTA_LAYOUT_CONFIG_PAGE + 4U * OTA_LAYOUT_WORD_LATEST_EN)
|
||||
#define OTA_LAYOUT_ADDR_BACKUP_EN (OTA_LAYOUT_CONFIG_PAGE + 4U * OTA_LAYOUT_WORD_BACKUP_EN)
|
||||
#define OTA_LAYOUT_ADDR_LATEST_CHECK (OTA_LAYOUT_CONFIG_PAGE + 4U * OTA_LAYOUT_WORD_LATEST_CHECK)
|
||||
#define OTA_LAYOUT_ADDR_BACKUP_CHECK (OTA_LAYOUT_CONFIG_PAGE + 4U * OTA_LAYOUT_WORD_BACKUP_CHECK)
|
||||
#define OTA_LAYOUT_ADDR_BOOT_VER0 (OTA_LAYOUT_CONFIG_PAGE + 4U * OTA_LAYOUT_WORD_BOOT_VER0)
|
||||
#define OTA_LAYOUT_ADDR_BOOT_VER1 (OTA_LAYOUT_CONFIG_PAGE + 4U * OTA_LAYOUT_WORD_BOOT_VER1)
|
||||
#define OTA_LAYOUT_ADDR_ECAT_ARM (OTA_LAYOUT_CONFIG_PAGE + 4U * OTA_LAYOUT_WORD_ECAT_ARM)
|
||||
#define OTA_LAYOUT_ADDR_CRASH_COUNT (OTA_LAYOUT_CONFIG_PAGE + 4U * OTA_LAYOUT_WORD_CRASH_COUNT)
|
||||
|
||||
/** 配置页有效 word 数(擦页回写必须覆盖到此);页 2KB,余量充足。 */
|
||||
#define OTA_LAYOUT_WORD_COUNT 10U
|
||||
|
||||
/** ECAT 下载 arm 令牌。避开擦除态与 AVAL。 */
|
||||
#define OTA_LAYOUT_ECAT_ARM_KEY 0xACA70001U
|
||||
|
||||
/** 连续 FWDGT 达到该次数后 BOOT 停止自动跳 APP,只接受外部灌写。 */
|
||||
#define OTA_LAYOUT_CRASH_STAY_MAX 3U
|
||||
|
||||
#define OTA_LAYOUT_AVAL 0x00000000U
|
||||
#define OTA_LAYOUT_NOT_AVAL 0xFFFFFFFFU
|
||||
|
||||
#endif /* SHARED_OTA_LAYOUT_H */
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,122 @@
|
||||
/**
|
||||
* @file cpu_load.cpp
|
||||
* @brief CPU 负载率测量实现(1.5.61,度量定义与并发协议见 cpu_load.h)。
|
||||
*
|
||||
* 热路径(beginWork/endWork/isrEnter/isrExit/tick)放进 .ramfunc——它们会被包进
|
||||
* EXTI0(Sync0, preempt=0) 等延迟敏感的 ISR 包装,与 EthercatSync::onSync0Exti
|
||||
* 同款 section 属性,避免 flash 等待态给 Sync0 入口添抖动。1s 一次的窗口
|
||||
* 结算(含 64 位除法)留在普通 flash 函数里。
|
||||
*/
|
||||
#include "sys/cpu_load.h"
|
||||
|
||||
#include "RTE_Components.h"
|
||||
#include CMSIS_device_header
|
||||
|
||||
volatile uint32_t CpuLoad::s_depth = 0U;
|
||||
volatile uint32_t CpuLoad::s_stamp = 0U;
|
||||
volatile uint32_t CpuLoad::s_busy = 0U;
|
||||
volatile uint32_t CpuLoad::s_winStart = 0U;
|
||||
volatile uint16_t CpuLoad::s_loadPerm = 0U;
|
||||
volatile uint32_t CpuLoad::s_seq = 0U;
|
||||
|
||||
/** @brief 区间并集的共享进入点。0→1 者负责盖时间戳;STREX 失败方重试时深度
|
||||
* 已被抢占者修正,不会再重复盖戳。 */
|
||||
__attribute__((always_inline)) static inline void cpuLoadEnter()
|
||||
{
|
||||
uint32_t d;
|
||||
do {
|
||||
d = __LDREXW(&CpuLoad::s_depth);
|
||||
if (d == 0U) {
|
||||
CpuLoad::s_stamp = DWT->CYCCNT;
|
||||
__DMB();
|
||||
}
|
||||
} while (__STREXW(d + 1U, &CpuLoad::s_depth) != 0U);
|
||||
__DMB();
|
||||
}
|
||||
|
||||
/** @brief 区间并集的共享退出点。1→0 者把区间累进 s_busy(同样走独占访问,
|
||||
* 防"ISR 恰好插在主循环退出的读-改-写之间"丢累加)。 */
|
||||
__attribute__((always_inline)) static inline void cpuLoadLeave()
|
||||
{
|
||||
const uint32_t end = DWT->CYCCNT;
|
||||
const uint32_t start = CpuLoad::s_stamp;
|
||||
uint32_t d;
|
||||
do {
|
||||
d = __LDREXW(&CpuLoad::s_depth);
|
||||
} while (__STREXW(d - 1U, &CpuLoad::s_depth) != 0U);
|
||||
if (d == 1U) {
|
||||
const uint32_t delta = end - start;
|
||||
uint32_t b;
|
||||
do {
|
||||
b = __LDREXW(&CpuLoad::s_busy);
|
||||
} while (__STREXW(b + delta, &CpuLoad::s_busy) != 0U);
|
||||
}
|
||||
}
|
||||
|
||||
void CpuLoad::init()
|
||||
{
|
||||
/* DWT 已由 EscLan9252::init 使能;这里幂等兜底(万一 EtherCAT 初始化被裁剪)。 */
|
||||
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
|
||||
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
|
||||
|
||||
s_depth = 0U;
|
||||
s_busy = 0U;
|
||||
s_winStart = DWT->CYCCNT;
|
||||
s_loadPerm = 0U;
|
||||
s_seq = 0U;
|
||||
}
|
||||
|
||||
__attribute__((section(".ramfunc"), noinline)) void CpuLoad::beginWork()
|
||||
{
|
||||
cpuLoadEnter();
|
||||
}
|
||||
|
||||
__attribute__((section(".ramfunc"), noinline)) void CpuLoad::isrEnter()
|
||||
{
|
||||
cpuLoadEnter();
|
||||
}
|
||||
|
||||
__attribute__((section(".ramfunc"), noinline)) void CpuLoad::isrExit()
|
||||
{
|
||||
cpuLoadLeave();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 窗口结算(普通 flash 函数,1s 才进一次)。
|
||||
* @note s_busy 用独占访问换出:窗口边界上 ISR 的在途累加要么算进本窗口、
|
||||
* 要么算进下窗口,不丢。
|
||||
*/
|
||||
static void cpuLoadRollWindow(uint32_t now)
|
||||
{
|
||||
const uint32_t elapsed = now - CpuLoad::s_winStart; /* 调用方已保证 >= 1s */
|
||||
uint32_t busy;
|
||||
do {
|
||||
busy = __LDREXW(&CpuLoad::s_busy);
|
||||
} while (__STREXW(0U, &CpuLoad::s_busy) != 0U);
|
||||
CpuLoad::s_winStart = now;
|
||||
|
||||
uint32_t perm = (uint32_t)(((uint64_t)busy * 1000ULL) / (uint64_t)elapsed);
|
||||
if (perm > 1000U) {
|
||||
perm = 1000U;
|
||||
}
|
||||
CpuLoad::s_loadPerm = (uint16_t)perm;
|
||||
CpuLoad::s_seq++;
|
||||
}
|
||||
|
||||
__attribute__((section(".ramfunc"), noinline)) void CpuLoad::endWork()
|
||||
{
|
||||
cpuLoadLeave();
|
||||
}
|
||||
|
||||
__attribute__((section(".ramfunc"), noinline)) void CpuLoad::tick()
|
||||
{
|
||||
const uint32_t now = DWT->CYCCNT;
|
||||
if ((uint32_t)(now - s_winStart) >= SystemCoreClock) {
|
||||
cpuLoadRollWindow(now);
|
||||
}
|
||||
}
|
||||
|
||||
/* C 侧挂钩:供 ecatappl.c / dc_app.c 计入 EtherCAT 过程数据路径。
|
||||
* 与 beginWork/endWork 共用深度计数器,嵌套不重不漏。 */
|
||||
extern "C" void CpuLoad_Enter(void) { CpuLoad::beginWork(); }
|
||||
extern "C" void CpuLoad_Leave(void) { CpuLoad::endWork(); }
|
||||
@@ -0,0 +1,110 @@
|
||||
/**
|
||||
* @file cpu_load.h
|
||||
* @brief CPU 负载率测量(1.5.63 定义收窄,机制沿用 1.5.61)。
|
||||
*
|
||||
* 度量定义:**中断 + 非延迟处理段(任务派发 / OTA 中继等请求驱动工作)占墙钟
|
||||
* 时间的比值**,1s 滚动窗口,分辨率 0.1%(permille)。
|
||||
*
|
||||
* 为什么不计入主循环常开轮询:本架构在 EtherCAT 实时模式(CONFIG_MAIN_LOOP_WFI_ENABLE=0)
|
||||
* 下主循环空闲时是 NOP 忙等,且 runPending() 每圈无条件跑 SSC MainLoop 轮询与各
|
||||
* poll() 检查——把这部分计入"busy"的话负载率恒 ≈99%,失去参考价值(1.5.61 实机
|
||||
* 实测 99.4%,其中绝大部分是自由运转的轮询而非在处理事件)。收窄后的定义回答
|
||||
* 的问题变成"CPU 有多少时间在响应事件",随真实负载(CAN 流量、OTA 中继、
|
||||
* DC 模式 Sync0 过程数据搬运)增长;常开轮询成本与空转时间不计入。
|
||||
*
|
||||
* 计入项(全走同一深度计数器求**区间并集**,嵌套不重不漏):
|
||||
* - 全部挂钩 ISR:TIMER2(100µs 调度)、SysTick、EXTI0/1/2(EtherCAT Sync0/PDI/
|
||||
* Sync1)、TIMER0(DC 定时档)、CAN0-2 RX+TX、SPI0+EXTI10_15、USART0。
|
||||
* DC 模式下 Sync0_Isr 的 EXMC 过程数据搬运由此计入——这是大头,不能漏。
|
||||
* - 主循环非延迟段:TaskManager 派发任务、BOOT、OTA relay、新鲜度救援、
|
||||
* free-run 下 **1ms 节流** 的过程数据应用(有 CpuLoad 挂钩)。
|
||||
* - PDI/Sync0 ISR 全程(含 SM 类型下每拍 Process/映射)——负载随 PDO Hz 升。
|
||||
* 不计入:MainLoop/SSC/poll 空转。
|
||||
* 性能约束:PDI 热路径不得「凡 AL 事件就 Process/InputMapping」——8kHz 时
|
||||
* 单拍超 ~120µs 墙会掉速;输出用 12B 探针,process 兜底读带新鲜度门。
|
||||
*
|
||||
* 并发协议(无关中断、无临界区):enter/exit 用 LDREX/STREX 原子改深度,
|
||||
* 任一优先级可抢占任意上下文而不破坏计数——STREX 失败方重试时会看到已修正的
|
||||
* 深度并补上时间戳。已知残余误差:exit 在读时间戳与递减深度之间被高优先级
|
||||
* 中断插入时,该次中断时长漏计(几 cycle 窗口,对 0.1% 分辨率可忽略)。
|
||||
*
|
||||
* @note DWT 已由 EscLan9252::init 使能;init() 幂等兜底,在主循环启动前调用。
|
||||
*/
|
||||
#ifndef SYS_CPU_LOAD_H
|
||||
#define SYS_CPU_LOAD_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
/** C 侧挂钩(ecatappl.c / dc_app.c)。与 beginWork/endWork 同一深度计数器。 */
|
||||
void CpuLoad_Enter(void);
|
||||
void CpuLoad_Leave(void);
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef __cplusplus
|
||||
/**
|
||||
* @brief CPU 负载率测量。
|
||||
* @note 全静态类,与 SysTickTimer 同风格。
|
||||
*/
|
||||
class CpuLoad {
|
||||
public:
|
||||
/**
|
||||
* @brief 使能 DWT CYCCNT 并复位统计状态。
|
||||
* @note 幂等;须在主循环启动前调用一次。
|
||||
*/
|
||||
static void init();
|
||||
|
||||
/** @brief 主循环圈起点:进入"busy"区间。与 endWork() 成对,每圈各一次。 */
|
||||
static void beginWork();
|
||||
|
||||
/**
|
||||
* @brief 主循环圈终点:退出"busy"区间。
|
||||
* @note 与 beginWork() 成对;窗口滚动不在这里(begin/end 可能整圈不执行),
|
||||
* 见 tick()。
|
||||
*/
|
||||
static void endWork();
|
||||
|
||||
/**
|
||||
* @brief 主循环每圈无条件调用:检查 1s 窗口是否滚动。
|
||||
* @note 只做一次 CYCCNT 读 + 比较(命中才进 flash 侧结算函数),滚动不再
|
||||
* 依赖"本圈恰好有挂钩段执行",空闲系统 seq 也会按秒推进。
|
||||
*/
|
||||
static void tick();
|
||||
|
||||
/** @brief 中断入口挂钩:与 isrExit() 成对,包住 ISR 主体。 */
|
||||
static void isrEnter();
|
||||
|
||||
/** @brief 中断出口挂钩。 */
|
||||
static void isrExit();
|
||||
|
||||
/**
|
||||
* @brief 最近一个完整窗口的负载率。
|
||||
* @return 0..1000,单位 0.1%(1000=满载);窗口未滚动过时为 0。
|
||||
*/
|
||||
static uint16_t loadPermille() { return s_loadPerm; }
|
||||
|
||||
/**
|
||||
* @brief 已完成的统计窗口数(自 init 起,单调递增回绕)。
|
||||
* @note 随负载率一起进 EtherCAT 输入 PDO 段 5,主站据此判断数据是否仍在刷新。
|
||||
*/
|
||||
static uint32_t windowSeq() { return s_seq; }
|
||||
|
||||
public:
|
||||
/* 静态成员开成 public:cpu_load.cpp 里 .ramfunc 的文件级内联热路径
|
||||
* (cpuLoadEnter/cpuLoadLeave/cpuLoadRollWindow) 直接触碰它们,
|
||||
* 经成员函数绕一道反而多一层调用。勿在模块外写这些状态。 */
|
||||
|
||||
static volatile uint32_t s_depth; /**< busy 区间嵌套深度 */
|
||||
static volatile uint32_t s_stamp; /**< 深度 0→1 时刻的 CYCCNT 快照 */
|
||||
static volatile uint32_t s_busy; /**< 当前窗口内累计 busy 周期数 */
|
||||
static volatile uint32_t s_winStart; /**< 当前窗口起点 CYCCNT */
|
||||
static volatile uint16_t s_loadPerm; /**< 最近一个完整窗口的负载率(0.1%) */
|
||||
static volatile uint32_t s_seq; /**< 完成滚动的窗口计数 */
|
||||
};
|
||||
#endif /* __cplusplus */
|
||||
|
||||
#endif /* SYS_CPU_LOAD_H */
|
||||
@@ -59,4 +59,19 @@
|
||||
*/
|
||||
#define RT_CRITICAL_RESTORE(val) __set_BASEPRI(val)
|
||||
|
||||
/**
|
||||
* @brief Preempt-0 专用临界区:PRIMASK 全局屏蔽,用于与 preempt=0 中断共享的
|
||||
* 单字标志(读-改-写)。
|
||||
* @note 严格限定 O(1) 状态操作(置位/清位/单字赋值)。**严禁**在其中调用
|
||||
* printf/LOG_*:串口入队实测 36us,TX FIFO 满时可达 123us,会把 EtherCAT
|
||||
* SYNC0 抖动顶出 DC 容差。日志一律放临界区外。
|
||||
*/
|
||||
#define RT_CRITICAL_PREEMPT0_SAVE() \
|
||||
({ \
|
||||
uint32_t __primask_save = __get_PRIMASK(); \
|
||||
__disable_irq(); \
|
||||
__primask_save; \
|
||||
})
|
||||
#define RT_CRITICAL_PREEMPT0_RESTORE(val) __set_PRIMASK(val)
|
||||
|
||||
#endif /* SYS_CRITICAL_H */
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* @file esp32_en.cpp
|
||||
* @brief ESP32 EN (PC9) 驱动。PC9 拉高后延时 1.2s 复位 SPI,避开起机毛刺。
|
||||
*/
|
||||
#include "sys/esp32_en.h"
|
||||
#include "sys/config.h"
|
||||
#include "sys/task_manager.h"
|
||||
#include "device/spi/app_spi_command.h"
|
||||
#include <stdio.h>
|
||||
|
||||
namespace {
|
||||
|
||||
/** @brief PC9 拉高后延时多久再清 SPI(等 ESP32 ROM/APP 引脚初始化毛刺结束)。 */
|
||||
constexpr uint32_t kPostBootRelinkDelayMs = 1200U;
|
||||
|
||||
/** @brief 延时复位排期;0=无。 */
|
||||
volatile uint32_t s_relinkAtMs = 0U;
|
||||
|
||||
void drivePinLow()
|
||||
{
|
||||
gpio_init(CONFIG_ESP32_RESET_PORT, GPIO_MODE_OUT_PP, GPIO_OSPEED_50MHZ,
|
||||
CONFIG_ESP32_RESET_PIN);
|
||||
gpio_bit_reset(CONFIG_ESP32_RESET_PORT, CONFIG_ESP32_RESET_PIN);
|
||||
}
|
||||
|
||||
void drivePinHigh()
|
||||
{
|
||||
gpio_init(CONFIG_ESP32_RESET_PORT, GPIO_MODE_OUT_PP, GPIO_OSPEED_50MHZ,
|
||||
CONFIG_ESP32_RESET_PIN);
|
||||
gpio_bit_set(CONFIG_ESP32_RESET_PORT, CONFIG_ESP32_RESET_PIN);
|
||||
}
|
||||
|
||||
void floatPin()
|
||||
{
|
||||
gpio_init(CONFIG_ESP32_RESET_PORT, GPIO_MODE_IN_FLOATING, 0,
|
||||
CONFIG_ESP32_RESET_PIN);
|
||||
}
|
||||
|
||||
bool readPin()
|
||||
{
|
||||
return gpio_input_bit_get(CONFIG_ESP32_RESET_PORT, CONFIG_ESP32_RESET_PIN) == SET;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
uint8_t Esp32En::drive(uint8_t action)
|
||||
{
|
||||
switch (action) {
|
||||
case 0: /* 拉高(开 WiFi) */
|
||||
case 2:
|
||||
/* ESP32 仍死着:先清一次 SPI;起机毛刺会在 1.2s 后再清一次 */
|
||||
s_relinkAtMs = 0U;
|
||||
AppSpiCommandBus.resetEsp32Link();
|
||||
drivePinHigh();
|
||||
s_relinkAtMs = TaskManager::state().tick_1ms + kPostBootRelinkDelayMs;
|
||||
break;
|
||||
case 1: /* 拉低保持(关 WiFi) */
|
||||
s_relinkAtMs = 0U;
|
||||
drivePinLow();
|
||||
AppSpiCommandBus.resetEsp32Link();
|
||||
break;
|
||||
case 3: /* 浮空 */
|
||||
s_relinkAtMs = 0U;
|
||||
floatPin();
|
||||
AppSpiCommandBus.resetEsp32Link();
|
||||
break;
|
||||
case 4: /* 只读 */
|
||||
break;
|
||||
case 5: /* 仅复位 SPI(诊断;ESP32 空闲时调用,再让它 HELLO) */
|
||||
s_relinkAtMs = 0U;
|
||||
AppSpiCommandBus.resetEsp32Link();
|
||||
break;
|
||||
default:
|
||||
return 0xEEU;
|
||||
}
|
||||
|
||||
const bool level = readPin();
|
||||
static const char *const kName[6] = {
|
||||
"restore HIGH", "drive LOW (hold)", "drive HIGH", "float", "read-only", "spi-relink"};
|
||||
printf("[APP][ESP32_EN] %s -> pin reads %s\r\n", kName[action],
|
||||
level ? "HIGH" : "LOW");
|
||||
return level ? 1U : 0U;
|
||||
}
|
||||
|
||||
void Esp32En::poll()
|
||||
{
|
||||
if (s_relinkAtMs == 0U) {
|
||||
return;
|
||||
}
|
||||
const uint32_t now = TaskManager::state().tick_1ms;
|
||||
if ((int32_t)(now - s_relinkAtMs) < 0) {
|
||||
return;
|
||||
}
|
||||
s_relinkAtMs = 0U;
|
||||
AppSpiCommandBus.resetEsp32Link();
|
||||
printf("[APP][ESP32_EN] post-boot SPI relink (glitch window over)\r\n");
|
||||
}
|
||||
|
||||
void Esp32En::cancelRelinkFromSpi()
|
||||
{
|
||||
/* HELLO/业务帧已进来:不必再延时复位(会打断建连) */
|
||||
s_relinkAtMs = 0U;
|
||||
}
|
||||
|
||||
/* EtherCAT SDO 0x2503 回调入口(dc_app.c 为 C 侧)。 */
|
||||
extern "C" uint8_t EcatEsp32EnDrive(uint8_t action)
|
||||
{
|
||||
return Esp32En::drive(action);
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* @file esp32_en.h
|
||||
* @brief ESP32 EN/RESET (PC9 / WLESS_EN) 手动控制 —— 网页开关经 EtherCAT SDO 0x2503 下发。
|
||||
*
|
||||
* 硬件语义(见 sys/config.h CONFIG_ESP32_RESET_*):
|
||||
* - PC9 推挽高 = 释放 ESP_CHIP_EN,ESP32 运行、WiFi 可用
|
||||
* - PC9 推挽低 = 按死 EN,ESP32 硬复位/掉使能,WiFi 与 HTTP 全断
|
||||
* - PC9 浮空 = 外部 C41/U11 把 ESP_CHIP_EN 拉低,同样关断(与复位后一致)
|
||||
*
|
||||
* 控制必须走 EtherCAT(与 ESP32 解耦):WiFi 关断后 ESP32 已死,
|
||||
* 经 /spi/send 的路径关得掉、开不回来。
|
||||
* 恢复:再次 SDO 写 action 0/2 拉高;或 GD32 复位/重新上电(initial.cpp 无条件拉高)。
|
||||
*
|
||||
* SPI 重建:ESP32 冷启动的 GPIO 毛刺会再次弄脏 SPI 链路,故拉高 PC9 后
|
||||
* 在起机窗口内多次 resetEsp32Link(),赶在 HELLO 前把 session/DMA 清干净。
|
||||
*/
|
||||
#ifndef SYS_ESP32_EN_H
|
||||
#define SYS_ESP32_EN_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
class Esp32En {
|
||||
public:
|
||||
/**
|
||||
* @brief 驱动/回读 ESP32 EN(PC9)。EtherCAT SDO 0x2503 转发到此。
|
||||
* @param action 0=拉高(开) 1=拉低保持(关) 2=拉高 3=浮空 4=只读 5=仅复位 SPI
|
||||
* @return 实际引脚电平 0/1(与 nrstDrive 同口径);0xEE=无效 action。
|
||||
*/
|
||||
static uint8_t drive(uint8_t action);
|
||||
|
||||
/**
|
||||
* @brief 起机窗口内补清 SPI 链路(TaskRunner::runPending 每圈调用)。
|
||||
* @note 无排期时立即返回。PC9 拉高后早期少量补清;一旦收到 CS 事务即停,
|
||||
* 避免打断正在建连的 HELLO。
|
||||
*/
|
||||
static void poll();
|
||||
|
||||
/** @brief SPI 收到任意 CS 事务时取消剩余补清(由 processCommand 调用)。 */
|
||||
static void cancelRelinkFromSpi();
|
||||
};
|
||||
|
||||
#endif /* SYS_ESP32_EN_H */
|
||||
@@ -7,7 +7,7 @@
|
||||
* ADC → CANFD → OtaBootRequest → TaskManager → Scheduler →
|
||||
* EtherCAT(+Sync 可选) → Watchdog → report
|
||||
*
|
||||
* 1.5.34:各模块 init 从"盲配置"改为带检查(寄存器回读 / 节拍验证 / 校准超时),
|
||||
* 各模块 init 带检查(寄存器回读 / 节拍验证 / 校准超时),
|
||||
* 失败不置位;复位源/模块位图/critical 失败位图经 getter 导出供 SPI 0xA3 上报。
|
||||
*/
|
||||
|
||||
@@ -37,6 +37,10 @@ uint8_t AppInitializer::s_initState = AppInitializer::kInitOk;
|
||||
uint8_t AppInitializer::s_resetCause = 0U;
|
||||
uint8_t AppInitializer::s_faultCodes[AppInitializer::kModuleCount] = {0U};
|
||||
|
||||
/* CoE 0x100A「Software version」写入口,实现在 comm/ssc/src/coeappl.c(C 链接)。
|
||||
* 主站总线读此对象可自证「新 APP 真起来了」。 */
|
||||
extern "C" void AppSetSoftwareVersion(const char *version);
|
||||
|
||||
namespace {
|
||||
|
||||
struct InitEntry {
|
||||
@@ -195,9 +199,8 @@ void AppInitializer::report()
|
||||
fail_list[0] != '\0' ? fail_list : "?");
|
||||
}
|
||||
|
||||
/* 日志完整性自证:串口 TX 缓冲溢出会静默吞字节,1.5.40 及之前实测使 [ECAT] 诊断
|
||||
* 出现 "bus eaabled"/"pdi_type=0x8Dexect" 等掉字,并两次导致 LAN9252 故障误定性。
|
||||
* 1.5.41 起缓冲满时自旋腾位,仅 USART 外设异常才丢弃;非 0 即代表上面的日志不可全信。 */
|
||||
/* 日志完整性自证:串口 TX 缓冲溢出会静默吞字节。
|
||||
* 缓冲满时自旋腾位,仅 USART 外设异常才丢弃;非 0 即代表上面的日志不可全信。 */
|
||||
const uint32_t dropped = Usart0::droppedBytes();
|
||||
if (dropped != 0U) {
|
||||
printf("[APP] WARN: uart dropped %u bytes, 以上日志可能不完整\r\n",
|
||||
@@ -279,6 +282,16 @@ void AppInitializer::init()
|
||||
OtaBootRequest::init();
|
||||
mark(kOtaBootReq);
|
||||
|
||||
/* 版本号写进 CoE 0x100A:唯一真源是 sys/config.h 的 APP_FW_VERSION_*,
|
||||
* 不在别处再抄一遍字符串。 */
|
||||
{
|
||||
char ver[20];
|
||||
snprintf(ver, sizeof ver, "%u.%u.%u",
|
||||
(unsigned)APP_FW_VERSION_MAJOR, (unsigned)APP_FW_VERSION_MINOR,
|
||||
(unsigned)APP_FW_VERSION_PATCH);
|
||||
AppSetSoftwareVersion(ver);
|
||||
}
|
||||
|
||||
TaskManager::init();
|
||||
mark(kTaskManager);
|
||||
|
||||
@@ -293,13 +306,11 @@ void AppInitializer::init()
|
||||
}
|
||||
|
||||
/* ---- EtherCAT: EXMC 总线探活 → SSC 协议栈初始化 → SYNC0/IRQ 中断使能 ----
|
||||
* 1.5.27:改经 EthercatSlave::init()(内部调用 EscLan9252::init 并记录
|
||||
* chipId/AL 状态诊断,此前该函数无调用点、s_escReady 恒为 false)。
|
||||
* 门控用 EscLan9252::isBusReady()(BYTE_TEST 通过)而非 isEscReady():
|
||||
* 冷启动时 AL=INIT,SSC 必须照常启动并等待主站配置。
|
||||
* 1.5.39:追加 isCoreReady() 门控——EscLan9252::init() 已等待 EEPROM(SII)
|
||||
* 另需 isCoreReady()——EscLan9252::init() 已等待 EEPROM(SII)
|
||||
* 加载完成(DL_STATUS bit0);未加载时 EtherCAT 核心寄存器写静默丢弃,
|
||||
* SSC 的 AL Event Mask/邮箱 SM 配置会全部落空(主站恒读 AL=INIT 的根因),
|
||||
* SSC 的 AL Event Mask/邮箱 SM 配置会全部落空(主站恒读 AL=INIT),
|
||||
* 此时跳过 SSC 并以独立故障码上报。
|
||||
* EEPROM 校验/烧录延迟到主循环执行,避免阻塞 init(烧录需 ~20s)。 */
|
||||
EthercatSlave::init();
|
||||
@@ -322,7 +333,9 @@ void AppInitializer::init()
|
||||
printf("[ECAT] EXMC bus not ready, SSC skipped\r\n");
|
||||
}
|
||||
|
||||
/* 看门狗放最后:PSC/RLD 写入经硬件确认才置位 */
|
||||
/* 看门狗放最后:PSC/RLD 写入经硬件确认才置位。
|
||||
* 崩溃计数不在这里清:须喂狗稳定满 CONFIG_CRASH_CLEAR_AFTER_SEC
|
||||
* (task_runner 每圈 maybeClearCrashCount),否则主循环崩溃攒不到阈值。 */
|
||||
const uint8_t wdgFault = Watchdog::init();
|
||||
if (wdgFault == Watchdog::kFaultOk) {
|
||||
mark(kWatchdog);
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "sys/motor_ota_relay.h"
|
||||
#include "sys/aes128.h"
|
||||
#include "sys/config.h"
|
||||
#include "sys/critical.h"
|
||||
#include "sys/systick.h"
|
||||
#include "sys/task_manager.h"
|
||||
#include "main.h"
|
||||
@@ -29,11 +30,23 @@ static const uint8_t K_MASTER[16] = {
|
||||
* 提到 4KB≈85 包(~420ms)给 WiFi/HTTP 抖动更大缓冲。 */
|
||||
static constexpr uint16_t kBufSize = 4096U;
|
||||
|
||||
/* ═══════════════════ 通道独占锁 ═══════════════════ */
|
||||
/* 位 0..2 = CAN 通道 0..2 正被 OTA 独占。置位于 start() 第一条语句(早于任何
|
||||
* 状态清零与串口日志),释位于 enterState() 进终态时。
|
||||
* EtherCAT 的 PDO→CAN 在 Sync0_Isr(preempt 0) 里发,OTA 的 START 可能来自主循环
|
||||
* 也可能来自 EXTI10_15(preempt 0),因此置位/判忙必须原子完成——否则两者之间
|
||||
* 的窗口(含 LOG 的百微秒级耗时)足够让一拍运动帧灌进正在升级的通道。
|
||||
* 同一时刻只跑一个会话,故写入用整字赋值/在 PRIMASK 里做单次 RMW,不做 RMW 于中断。 */
|
||||
static volatile uint32_t s_channel_lock_mask = 0U;
|
||||
|
||||
/* 本次会话的进料来源:环缓只认一路,交叉写入会拼出「一半 WiFi 一半 ECAT」的镜像。 */
|
||||
static FeedSource s_feed_src = FeedSource::kSpi;
|
||||
|
||||
/* ═══════════════════ 内部状态 ═══════════════════ */
|
||||
|
||||
static State s_state = State::kIdle;
|
||||
static volatile State s_state = State::kIdle;
|
||||
static uint8_t s_error = 0;
|
||||
static uint8_t s_channel = 0;
|
||||
static volatile uint8_t s_channel = 0;
|
||||
static uint32_t s_fw_size = 0;
|
||||
static uint32_t s_fw_fed = 0; /* 已接受的固件字节数(绝对流位置,防 0xD1 重传重复追加) */
|
||||
static uint16_t s_total_packets = 0;
|
||||
@@ -214,6 +227,12 @@ static void enterState(State st) {
|
||||
s_last_send_ms = 0;
|
||||
s_crc_resend_pending = false;
|
||||
|
||||
/* 通道锁释放:进终态(含 kIdle/kFinished/kError)即解锁,让 EtherCAT/SPI 恢复
|
||||
* 对该通道发帧。整字清零——同一时刻只可能有一个会话,无需 RMW。 */
|
||||
if (st == State::kIdle || st == State::kFinished || st == State::kError) {
|
||||
s_channel_lock_mask = 0U;
|
||||
}
|
||||
|
||||
/* auto_retrans 切换:OTA 是应用层停等协议,硬件自动重发会打穿电机单缓冲 RX
|
||||
* (重发的前一帧被当成新帧处理 → receive_packet_num 错位 → CRC 错)。
|
||||
* 进入 kHandshakeReq(start() 唯一入口)即关 ARD 切单发;
|
||||
@@ -229,8 +248,12 @@ static void enterState(State st) {
|
||||
|| prev == State::kFinishOta);
|
||||
if (otaActive && !wasActive) {
|
||||
CanFd::setChannelAutoRetrans(s_channel, false); /* OTA 开始:单发 */
|
||||
CanFd::setChannelBrsSuppress(s_channel, true); /* OTA 开始:压 BRS——电机 BOOT 收不了
|
||||
BRS 数据段,nonce/confirm/64B 数据包
|
||||
(DLC>8) 会全部静默丢失 */
|
||||
} else if (!otaActive && wasActive) {
|
||||
CanFd::setChannelAutoRetrans(s_channel, true); /* OTA 结束:恢复重发 */
|
||||
CanFd::setChannelBrsSuppress(s_channel, false); /* OTA 结束:恢复 BRS(EtherCAT 桥接吞吐) */
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1063,18 +1086,33 @@ bool onCanFrame(uint8_t channel, uint32_t id, const uint8_t *data, uint8_t len)
|
||||
return true; /* 已消费,不再传递给其他 handler */
|
||||
}
|
||||
|
||||
bool start(uint8_t can_channel, uint32_t fw_size) {
|
||||
if (s_state != State::kIdle && s_state != State::kFinished && s_state != State::kError) {
|
||||
return false; /* 已在进行中 */
|
||||
}
|
||||
bool start(uint8_t can_channel, uint32_t fw_size, FeedSource src, uint8_t node_id) {
|
||||
if (fw_size == 0 || fw_size > kMaxFwSize) return false;
|
||||
if (can_channel >= 3) return false;
|
||||
|
||||
s_channel = can_channel;
|
||||
s_fw_size = fw_size;
|
||||
s_fw_fed = 0U;
|
||||
s_total_packets = static_cast<uint16_t>((fw_size + kPacketPayload - 1U) / kPacketPayload);
|
||||
s_packets_acked = 0;
|
||||
/* ★ 第一条触碰共享状态的语句就完成「判忙 + 上锁」,且两者原子。
|
||||
* 下面的二十余项清零和 START 日志(UART 格式化,百微秒级)都在锁之后;
|
||||
* EtherCAT 的 PDO→CAN 由 preempt 0 的 Sync0_Isr 驱动,随时能插进这些空隙,
|
||||
* 只要漏一拍运动帧进这条通道,单发模式(ARD=1)的 0x101 固件包就被硬件 abort,
|
||||
* 电机会回 0x05/0x06 并在擦除后半路变砖。 */
|
||||
{
|
||||
const uint32_t save = RT_CRITICAL_PREEMPT0_SAVE();
|
||||
const bool busy = (s_channel_lock_mask != 0U) ||
|
||||
(s_state != State::kIdle && s_state != State::kFinished &&
|
||||
s_state != State::kError);
|
||||
if (!busy) {
|
||||
s_channel_lock_mask = (1U << can_channel);
|
||||
}
|
||||
RT_CRITICAL_PREEMPT0_RESTORE(save);
|
||||
if (busy) return false; /* 已有会话在跑(WiFi 或 EtherCAT,两者共用这一道门) */
|
||||
}
|
||||
|
||||
s_channel = can_channel;
|
||||
s_feed_src = src;
|
||||
s_fw_size = fw_size;
|
||||
s_fw_fed = 0U;
|
||||
s_total_packets = static_cast<uint16_t>((fw_size + kPacketPayload - 1U) / kPacketPayload);
|
||||
s_packets_acked = 0;
|
||||
s_error = 0;
|
||||
s_session_num = 0; /* 由 processNonceAck 根据 s_nonce 前4字节设置 */
|
||||
s_buf_head = 0;
|
||||
@@ -1089,81 +1127,102 @@ s_channel = can_channel;
|
||||
s_ack_resend_total = 0;
|
||||
s_fast_resend_total = 0;
|
||||
s_last_pad_valid = 0xFFFFU;
|
||||
s_last_feed_ms = SysTickTimer::getMs(); /* 从 start 起算 SPI 喂数超时 */
|
||||
s_last_feed_ms = SysTickTimer::getMs(); /* 从 start 起算喂数超时 */
|
||||
s_auto_retry = 0;
|
||||
|
||||
/* 不在 SPI 中断里同步发握手帧(电机跳 BOOT 期间 CAN 可能短暂断开)。
|
||||
* 先进 kPreCancel 等待电机 0x103 心跳确认已进 BOOT,再由 process() 异步推进到握手。 */
|
||||
LOG_ERR("[MOTOR-OTA] START ch=%u fw=%lu total=%u\r\n",
|
||||
(unsigned)can_channel, (unsigned long)fw_size, (unsigned)s_total_packets);
|
||||
LOG_ERR("[MOTOR-OTA] START ch=%u fw=%lu total=%u src=%s\r\n",
|
||||
(unsigned)can_channel, (unsigned long)fw_size, (unsigned)s_total_packets,
|
||||
(src == FeedSource::kEcat) ? "ECAT" : "SPI");
|
||||
s_boot_heartbeat_seen = false;
|
||||
enterState(State::kPreCancel);
|
||||
|
||||
/* 进 BOOT 帧(0x000N0000, DLC=0)由本机在**已加锁**的通道上发:EtherCAT 链路必须
|
||||
* 走这里——那条链路的运动帧就是同一批 CAN 邮箱发出去的,先解锁再让上位机发帧
|
||||
* 等于把窗口留给运动帧。WiFi 链路保持 node_id=0(上位机先发帧的既有顺序),
|
||||
* 不动那条已验证的通路。 */
|
||||
if (node_id >= 1U && node_id <= 9U) {
|
||||
const uint32_t bootId = static_cast<uint32_t>(node_id) << 16;
|
||||
const bool sent = canSend(bootId, nullptr, 0U);
|
||||
LOG_ERR("[MOTOR-OTA] ENTER-BOOT id=0x%08lX ch=%u %s\r\n",
|
||||
(unsigned long)bootId, (unsigned)can_channel, sent ? "queued" : "retry-by-state");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool feedData(const uint8_t *data, uint16_t len) {
|
||||
/* 接受任何活跃状态的数据(包括握手期间)——让 ESP32 在 kPreCancel/握手
|
||||
* 期间就能预填充缓冲,进入 kTransfer 后立即有数据可发。
|
||||
*
|
||||
* payload: [offset:u32 LE][chunk...]。offset 为固件绝对字节偏移。
|
||||
* ★ BLF 根因:无 offset 纯流时,SPI 超时重传同一 chunk,GD32 再次追加,
|
||||
* CAN 上出现「前 236B 循环 185 次」→ CRC 全过但固件内容全错。 */
|
||||
if (s_state == State::kIdle || s_state == State::kError ||
|
||||
s_state == State::kFinished) {
|
||||
return false;
|
||||
}
|
||||
if (data == nullptr || len < 4U) {
|
||||
return false;
|
||||
}
|
||||
/* payload: [offset:u32 LE][chunk...]。offset 为固件绝对字节偏移。
|
||||
* ★ BLF 根因:无 offset 纯流时,SPI 超时重传同一 chunk,GD32 再次追加,
|
||||
* CAN 上出现「前 236B 循环 185 次」→ CRC 全过但固件内容全错。 */
|
||||
if (s_feed_src != FeedSource::kSpi) {
|
||||
/* 本次会话由 ECAT 暂存泵进料:SPI 侧(ESP32 还在线时)的喂数一律拒收,
|
||||
* 两路交叉写环缓会拼出「半份 WiFi、半份 EtherCAT」的镜像。 */
|
||||
return false;
|
||||
}
|
||||
if (data == nullptr || len < 4U) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const uint32_t offset = static_cast<uint32_t>(data[0])
|
||||
| (static_cast<uint32_t>(data[1]) << 8)
|
||||
| (static_cast<uint32_t>(data[2]) << 16)
|
||||
| (static_cast<uint32_t>(data[3]) << 24);
|
||||
const uint8_t *chunk = data + 4U;
|
||||
uint16_t chunk_len = static_cast<uint16_t>(len - 4U);
|
||||
if (chunk_len == 0U) {
|
||||
return true;
|
||||
}
|
||||
if (s_fw_size == 0U) {
|
||||
return false;
|
||||
}
|
||||
if (offset >= s_fw_size) {
|
||||
return true; /* 越界尾:幂等 ACK */
|
||||
}
|
||||
/* 截到固件末尾 */
|
||||
if (offset + chunk_len > s_fw_size) {
|
||||
chunk_len = static_cast<uint16_t>(s_fw_size - offset);
|
||||
}
|
||||
const uint32_t offset = static_cast<uint32_t>(data[0])
|
||||
| (static_cast<uint32_t>(data[1]) << 8)
|
||||
| (static_cast<uint32_t>(data[2]) << 16)
|
||||
| (static_cast<uint32_t>(data[3]) << 24);
|
||||
return feedAt(offset, data + 4U, static_cast<uint16_t>(len - 4U));
|
||||
}
|
||||
|
||||
if (offset + chunk_len <= s_fw_fed) {
|
||||
/* 完全已收过:幂等 ACK,不写环缓 */
|
||||
return true;
|
||||
}
|
||||
if (offset > s_fw_fed) {
|
||||
/* 缺口:拒收,ESP32 按 s_fw_offset 重发正确位置 */
|
||||
static uint32_t s_last_gap_ms = 0;
|
||||
const uint32_t now = SysTickTimer::getMs();
|
||||
if (now - s_last_gap_ms >= 200U) {
|
||||
LOG_ERR("[MOTOR-OTA] feed gap off=%lu fed=%lu len=%u\r\n",
|
||||
(unsigned long)offset, (unsigned long)s_fw_fed,
|
||||
(unsigned)chunk_len);
|
||||
s_last_gap_ms = now;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
bool feedAt(uint32_t offset, const uint8_t *chunk, uint16_t chunk_len) {
|
||||
/* 接受任何活跃状态的数据(包括握手期间)——让上位机在 kPreCancel/握手
|
||||
* 期间就能预填充缓冲,进入 kTransfer 后立即有数据可发。 */
|
||||
if (s_state == State::kIdle || s_state == State::kError ||
|
||||
s_state == State::kFinished) {
|
||||
return false;
|
||||
}
|
||||
if (chunk == nullptr) {
|
||||
return false;
|
||||
}
|
||||
if (chunk_len == 0U) {
|
||||
return true;
|
||||
}
|
||||
if (s_fw_size == 0U) {
|
||||
return false;
|
||||
}
|
||||
if (offset >= s_fw_size) {
|
||||
return true; /* 越界尾:幂等 ACK */
|
||||
}
|
||||
/* 截到固件末尾 */
|
||||
if (offset + chunk_len > s_fw_size) {
|
||||
chunk_len = static_cast<uint16_t>(s_fw_size - offset);
|
||||
}
|
||||
|
||||
/* 部分重叠:只追加 [s_fw_fed, offset+chunk_len) */
|
||||
const uint32_t skip = s_fw_fed - offset;
|
||||
const uint16_t add_len = static_cast<uint16_t>(chunk_len - skip);
|
||||
if (bufFree() < add_len) {
|
||||
return false; /* 环缓满 → SPI WAIT */
|
||||
}
|
||||
bufPush(chunk + skip, add_len);
|
||||
s_fw_fed += add_len;
|
||||
s_last_feed_ms = SysTickTimer::getMs(); /* SPI 断流检测:记录有效喂数时刻 */
|
||||
return true;
|
||||
}
|
||||
if (offset + chunk_len <= s_fw_fed) {
|
||||
/* 完全已收过:幂等 ACK,不写环缓 */
|
||||
return true;
|
||||
}
|
||||
if (offset > s_fw_fed) {
|
||||
/* 缺口:拒收,上位机按 s_fw_fed 重发正确位置 */
|
||||
static uint32_t s_last_gap_ms = 0;
|
||||
const uint32_t now = SysTickTimer::getMs();
|
||||
if (now - s_last_gap_ms >= 200U) {
|
||||
LOG_ERR("[MOTOR-OTA] feed gap off=%lu fed=%lu len=%u\r\n",
|
||||
(unsigned long)offset, (unsigned long)s_fw_fed,
|
||||
(unsigned)chunk_len);
|
||||
s_last_gap_ms = now;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/* 部分重叠:只追加 [s_fw_fed, offset+chunk_len) */
|
||||
const uint32_t skip = s_fw_fed - offset;
|
||||
const uint16_t add_len = static_cast<uint16_t>(chunk_len - skip);
|
||||
if (bufFree() < add_len) {
|
||||
return false; /* 环缓满 → SPI WAIT / ECAT 下拍再泵 */
|
||||
}
|
||||
bufPush(chunk + skip, add_len);
|
||||
s_fw_fed += add_len;
|
||||
s_last_feed_ms = SysTickTimer::getMs(); /* 喂数断流检测:记录有效喂数时刻 */
|
||||
return true;
|
||||
}
|
||||
|
||||
void cancel() {
|
||||
if (s_state == State::kIdle) return;
|
||||
@@ -1194,11 +1253,32 @@ Status getStatus() {
|
||||
st.packets_acked = s_packets_acked;
|
||||
st.total_packets = s_total_packets;
|
||||
st.fw_size = s_fw_size;
|
||||
st.fw_fed = s_fw_fed;
|
||||
return st;
|
||||
}
|
||||
|
||||
uint16_t feedFreeSpace() {
|
||||
return bufFree();
|
||||
}
|
||||
|
||||
bool isActive() {
|
||||
return (s_state != State::kIdle && s_state != State::kFinished && s_state != State::kError);
|
||||
}
|
||||
|
||||
bool isChannelLocked(uint8_t channel) {
|
||||
if (channel >= 3U) return false;
|
||||
/* 单次 32 位读:位与本身就是原子的,不需要临界区。
|
||||
* 判据只看锁位,不看状态机——状态机那几个变量是在锁之后逐步赋的,
|
||||
* 拿它当门会让出几十微秒的空窗(审计报告里的 88.53% 漏包就是这么来的)。 */
|
||||
return ((s_channel_lock_mask & (1U << channel)) != 0U);
|
||||
}
|
||||
|
||||
int8_t lockedChannel() {
|
||||
const uint32_t mask = s_channel_lock_mask;
|
||||
for (uint8_t ch = 0U; ch < 3U; ++ch) {
|
||||
if ((mask & (1U << ch)) != 0U) return static_cast<int8_t>(ch);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
} /* namespace MotorOtaRelay */
|
||||
|
||||
@@ -51,6 +51,16 @@ struct Status {
|
||||
uint16_t packets_acked;
|
||||
uint16_t total_packets;
|
||||
uint32_t fw_size;
|
||||
/** 已被环缓接受的明文字节数(流位置)。ECAT 暂存泵用它对账:
|
||||
* 电机侧自动重试会把环缓与这个位置一起清零,泵必须跟着回退。
|
||||
* 注意:只是内部 C++ 结构,SPI 0xD0 QUERY 的线格式没有改动。 */
|
||||
uint32_t fw_fed;
|
||||
};
|
||||
|
||||
/** 固件明文进料来源。同一时刻只允许一个来源,环缓不接受两路交叉写入。 */
|
||||
enum class FeedSource : uint8_t {
|
||||
kSpi = 0U, /* ESP32 经 SPI 0xD1 喂(WiFi/BLE 链路的既有通路) */
|
||||
kEcat = 1U, /* EtherCAT 主站经 CoE 0x2511 灌进暂存区,再由 ecat_motor_ota 泵入环缓 */
|
||||
};
|
||||
|
||||
/* 初始化(注册 CAN RX 回调等) */
|
||||
@@ -67,19 +77,51 @@ void init();
|
||||
bool onCanFrame(uint8_t channel, uint32_t id, const uint8_t *data, uint8_t len);
|
||||
|
||||
/* SPI 命令接口 */
|
||||
bool start(uint8_t can_channel, uint32_t fw_size);
|
||||
/**
|
||||
* @brief 启动一次电机 OTA。
|
||||
* @param can_channel 目标 CAN 通道 0..2。
|
||||
* @param fw_size 固件明文字节数。
|
||||
* @param src 进料来源(SPI / ECAT 暂存泵)。
|
||||
* @param node_id 1..9:由 GD32 自己在**已加锁**的通道上发 0x000N0000 进 BOOT 帧;
|
||||
* 0:不进 BOOT(WiFi 链路沿用上位机先发帧的老顺序)。
|
||||
* @note 入口第一条语句即置通道锁,早于任何状态清零与串口日志。
|
||||
*/
|
||||
bool start(uint8_t can_channel, uint32_t fw_size,
|
||||
FeedSource src = FeedSource::kSpi, uint8_t node_id = 0U);
|
||||
/**
|
||||
* @brief 喂入固件明文。payload 布局:[offset:u32 LE][data...]
|
||||
* offset 为固件绝对字节偏移;重复/重叠区间幂等 ACK,缺口拒绝(→SPI WAIT)。
|
||||
* 无 offset 的旧纯流会在 SPI 超时重传时把同一 chunk 追加进环缓(BLF 见前 236B 循环)。
|
||||
*/
|
||||
bool feedData(const uint8_t *data, uint16_t len);
|
||||
/**
|
||||
* @brief 已解析过的进料写入(feedData 的核心,偏移/幂等/环缓满判据只有一份)。
|
||||
* @note 供 ECAT 暂存泵调用:那边已按 offset 寻址,无需再拼 4 字节头,
|
||||
* 也避免在仅 512B 的主栈上放临时头缓冲。
|
||||
*/
|
||||
bool feedAt(uint32_t offset, const uint8_t *chunk, uint16_t chunk_len);
|
||||
/**
|
||||
* @brief 环缓当前还能接受多少明文字节。
|
||||
* @note 给进料方按空位切片用(否则一次投过大永远排不进队首)。不改变任何判据:
|
||||
* feedAt 内部仍会自己再查一次空位。
|
||||
*/
|
||||
uint16_t feedFreeSpace(void);
|
||||
void cancel();
|
||||
|
||||
/* 状态查询 */
|
||||
Status getStatus();
|
||||
bool isActive();
|
||||
|
||||
/**
|
||||
* @brief 指定 CAN 通道当前是否被电机 OTA 独占。
|
||||
* @note 单条 32 位位与,可在 preempt=0(EXTI0/SYNC0、EXTI10_15)里直接调用。
|
||||
* EtherCAT 桥接与 SPI 桥接/突发在发送前必须查它,否则周期运动帧会
|
||||
* abort 掉 OTA 的 0x101 包(单发模式 ARD=1 不重传),电机会回 0x05/0x06。
|
||||
*/
|
||||
bool isChannelLocked(uint8_t channel);
|
||||
/** @brief 当前被锁定的通道号;-1 表示无锁。 */
|
||||
int8_t lockedChannel();
|
||||
|
||||
} /* namespace MotorOtaRelay */
|
||||
|
||||
#endif /* SYS_MOTOR_OTA_RELAY_H */
|
||||
|
||||
@@ -7,21 +7,23 @@
|
||||
|
||||
#include "gd32e50x.h"
|
||||
#include "sys/config.h"
|
||||
#include "sys/motor_ota_relay.h"
|
||||
#include "sys/systick.h"
|
||||
#include <cstdio> /* LOG_ERR 展开为 printf(sys/config.h),本文件原先没有 stdio */
|
||||
|
||||
namespace {
|
||||
/** Bootloader OTA 配置页地址,与 BOOT 工程 sys/config.h 保持一致。 */
|
||||
constexpr uint32_t kOtaConfigPageAddr = 0x0807E000U;
|
||||
constexpr uint32_t kAppLatestAvalAddr = kOtaConfigPageAddr + 0x00U;
|
||||
constexpr uint32_t kAppLatestEnAddr = kOtaConfigPageAddr + 0x08U;
|
||||
constexpr uint32_t kAppLatestCheckAddr = kOtaConfigPageAddr + 0x10U;
|
||||
/** BOOT 版本号在 OTA 配置页中的存储地址:word6=major|minor, word7=patch|build。 */
|
||||
constexpr uint32_t kBootVersionAddr0 = kOtaConfigPageAddr + 0x18U;
|
||||
constexpr uint32_t kBootVersionAddr1 = kOtaConfigPageAddr + 0x1CU;
|
||||
constexpr uint32_t kOtaAval = 0x00000000U;
|
||||
constexpr uint32_t kOtaNotAval = 0xFFFFFFFFU;
|
||||
constexpr uint32_t kFmcPageSize = 0x00000800U;
|
||||
constexpr uint32_t kRebootDelayMs = 20U;
|
||||
/** Bootloader OTA 配置页地址与 word 布局:唯一真源 shared/ota_layout.h。 */
|
||||
constexpr uint32_t kOtaConfigPageAddr = OTA_LAYOUT_CONFIG_PAGE;
|
||||
constexpr uint32_t kAppLatestAvalAddr = OTA_LAYOUT_ADDR_LATEST_AVAL;
|
||||
constexpr uint32_t kAppLatestEnAddr = OTA_LAYOUT_ADDR_LATEST_EN;
|
||||
constexpr uint32_t kAppLatestCheckAddr = OTA_LAYOUT_ADDR_LATEST_CHECK;
|
||||
constexpr uint32_t kBootVersionAddr0 = OTA_LAYOUT_ADDR_BOOT_VER0;
|
||||
constexpr uint32_t kBootVersionAddr1 = OTA_LAYOUT_ADDR_BOOT_VER1;
|
||||
constexpr uint32_t kBootCrashCountAddr = OTA_LAYOUT_ADDR_CRASH_COUNT;
|
||||
constexpr uint32_t kOtaAval = OTA_LAYOUT_AVAL;
|
||||
constexpr uint32_t kOtaNotAval = OTA_LAYOUT_NOT_AVAL;
|
||||
constexpr uint32_t kFmcPageSize = OTA_LAYOUT_PAGE_SIZE;
|
||||
constexpr uint32_t kRebootDelayMs = 20U;
|
||||
|
||||
static bool writeWord(uint32_t addr, uint32_t value)
|
||||
{
|
||||
@@ -122,12 +124,45 @@ BootVersion OtaBootRequest::readBootVersion()
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 清零连续 FWDGT 崩溃计数。
|
||||
* @note 与 BOOT OTA_CRASH_STAY_MAX 门配套。写 0(AVAL)可对 1→0 直接编程。
|
||||
*/
|
||||
bool OtaBootRequest::clearCrashCount()
|
||||
{
|
||||
return otaConfigSet(kBootCrashCountAddr, kOtaAval);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 喂狗运行满 CONFIG_CRASH_CLEAR_AFTER_SEC 后清崩溃计数(每上电至多一次)。
|
||||
*/
|
||||
void OtaBootRequest::maybeClearCrashCount(uint32_t uptime_sec)
|
||||
{
|
||||
static bool s_cleared = false;
|
||||
if (s_cleared || uptime_sec < CONFIG_CRASH_CLEAR_AFTER_SEC) {
|
||||
return;
|
||||
}
|
||||
if (clearCrashCount()) {
|
||||
s_cleared = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 写入 Bootloader OTA 等待标志,并安排稍后软复位。
|
||||
* @return true 表示标志写入成功;false 表示 Flash 配置页写入失败。
|
||||
*/
|
||||
bool OtaBootRequest::requestEnterBoot()
|
||||
{
|
||||
/* 电机 OTA 正在跑(WiFi 与 EtherCAT 两条进料路都算)时绝不交回 BOOT:
|
||||
* 复位会把电机的刷写腰斩在 erase 之后——旧 APP 已抹、新 APP 没收完,直接变砖。
|
||||
* 判据只放这一个执行点:SPI 0xB0、EtherCAT CoE 0x2502、CAN 0x320 三条入口
|
||||
* 最终都要过这里,不在调用方各抄一份。 */
|
||||
if (MotorOtaRelay::isActive()) {
|
||||
LOG_ERR("[OTA-BOOT] enter-boot refused: motor OTA active ch=%d\r\n",
|
||||
(int)MotorOtaRelay::lockedChannel());
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ok = true;
|
||||
ok = otaConfigSet(kAppLatestCheckAddr, kOtaAval) && ok;
|
||||
ok = otaConfigSet(kAppLatestAvalAddr, kOtaAval) && ok;
|
||||
@@ -197,7 +232,7 @@ void OtaBootRequest::process()
|
||||
if(!ok) {
|
||||
s_spiFrozenForBoot = false;
|
||||
nvic_irq_enable(SPI0_IRQn, 2U, 0U);
|
||||
nvic_irq_enable(EXTI10_15_IRQn, 1U, 1U);
|
||||
nvic_irq_enable(EXTI10_15_IRQn, 0U, 1U); /* 与 configureExti 一致:抢占级 0 */
|
||||
/* requestEnterBoot 失败时 s_rebootPending 仍为 false */
|
||||
return;
|
||||
}
|
||||
@@ -223,3 +258,15 @@ void OtaBootRequest::process()
|
||||
while(1) {
|
||||
}
|
||||
}
|
||||
|
||||
/*--------------------------------------------------------------------------
|
||||
* EtherCAT CoE 0x2502 的入口(comm/ssc/port/dc_app.c 是 C 文件,调不到类静态方法,
|
||||
* 与 EcatPdo_* 同一形状)。写 1 走的是和 SPI 0xB0 完全相同的那条异步链:
|
||||
* 这里只置标志,配置页三字的 Flash 写入 + NVIC_SystemReset 在主循环
|
||||
* OtaBootRequest::process() 下一圈做 —— SDO 响应先发、复位后到,
|
||||
* 不会把 SM1 半包留在总线上。
|
||||
*-------------------------------------------------------------------------*/
|
||||
extern "C" void EcatEnterBootRequestAsync(void)
|
||||
{
|
||||
OtaBootRequest::requestEnterBootAsync();
|
||||
}
|
||||
|
||||
@@ -40,6 +40,19 @@ public:
|
||||
*/
|
||||
static BootVersion readBootVersion();
|
||||
|
||||
/**
|
||||
* @brief 清零 BOOT 侧连续 FWDGT 崩溃计数(配置页 word9)。
|
||||
* @note 不要在 init 里立刻调用;见 maybeClearCrashCount()。
|
||||
*/
|
||||
static bool clearCrashCount();
|
||||
|
||||
/**
|
||||
* @brief APP 持续喂狗满 CONFIG_CRASH_CLEAR_AFTER_SEC 后清一次崩溃计数。
|
||||
* @param[in] uptime_sec 当前 APP 运行秒数(g_app_uptime_sec)。
|
||||
* @note 挂在主循环;init 未走完或 30s 内崩溃时计数保留,BOOT 才能攒到阈值。
|
||||
*/
|
||||
static void maybeClearCrashCount(uint32_t uptime_sec);
|
||||
|
||||
/**
|
||||
* @brief 尝试处理一帧 CAN 接收报文。
|
||||
* @param[in] channel 报文来源 CAN 通道。
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
#include "main.h"
|
||||
#include "sys/systick.h"
|
||||
#include "sys/cpu_load.h"
|
||||
|
||||
volatile uint32_t SysTickTimer::s_ms = 0U;
|
||||
|
||||
@@ -93,4 +94,9 @@ bool SysTickTimer::initMs()
|
||||
* @retval 无返回值。
|
||||
* @note C 向量包装函数,内部转调 `SysTickTimer::onInterrupt()`。
|
||||
*/
|
||||
extern "C" void SysTick_Handler(void) { SysTickTimer::onInterrupt(); }
|
||||
extern "C" void SysTick_Handler(void)
|
||||
{
|
||||
CpuLoad::isrEnter();
|
||||
SysTickTimer::onInterrupt();
|
||||
CpuLoad::isrExit();
|
||||
}
|
||||
|
||||
@@ -40,13 +40,15 @@ void TaskManager::init()
|
||||
* @param[in] taskMask 待置位的任务位图掩码。
|
||||
* @param[out] 无。
|
||||
* @retval 无返回值。
|
||||
* @note 该操作不清除其他已存在的任务位。
|
||||
* @note 该操作不清除其他已存在的任务位;通过 __LDREXW/__STREXW 独占访问循环实现无锁原子置位。
|
||||
*/
|
||||
void TaskManager::set(uint32_t taskMask)
|
||||
{
|
||||
uint32_t save = RT_CRITICAL_SAVE();
|
||||
g_scheduler.task_flags |= taskMask;
|
||||
RT_CRITICAL_RESTORE(save);
|
||||
uint32_t val;
|
||||
do {
|
||||
val = __LDREXW(&g_scheduler.task_flags);
|
||||
} while (__STREXW(val | taskMask, &g_scheduler.task_flags) != 0U);
|
||||
__DMB();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -54,15 +56,15 @@ void TaskManager::set(uint32_t taskMask)
|
||||
* @param[in] 无。
|
||||
* @param[out] 无。
|
||||
* @retval uint32_t 返回当前待执行任务位图。
|
||||
* @note 内部通过关中断实现对 `task_flags` 的原子交换。
|
||||
* @note 通过 __LDREXW/__STREXW 独占访问循环实现对 `task_flags` 的原子读取与清零,彻底避免被 Preempt 0 ISR 抢占丢失标志。
|
||||
*/
|
||||
uint32_t TaskManager::takePending()
|
||||
{
|
||||
uint32_t pending;
|
||||
uint32_t save = RT_CRITICAL_SAVE();
|
||||
pending = g_scheduler.task_flags;
|
||||
g_scheduler.task_flags &= ~pending;
|
||||
RT_CRITICAL_RESTORE(save);
|
||||
do {
|
||||
pending = __LDREXW(&g_scheduler.task_flags);
|
||||
} while (__STREXW(0U, &g_scheduler.task_flags) != 0U);
|
||||
__DMB();
|
||||
return pending;
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ public:
|
||||
* @param[in] 无。
|
||||
* @param[out] 无。
|
||||
* @retval uint32_t 返回当前待执行任务位图。
|
||||
* @note 内部通过关中断保证原子性,供 TaskRunner 消费。
|
||||
* @note 内部通过 Cortex-M 独占访问循环(__LDREXW/__STREXW)保证原子性,供 TaskRunner 消费。
|
||||
*/
|
||||
static uint32_t takePending();
|
||||
|
||||
@@ -42,7 +42,7 @@ public:
|
||||
* @param[in] taskMask 待置位的任务位图掩码。
|
||||
* @param[out] 无。
|
||||
* @retval 无返回值。
|
||||
* @note 内部通过 BASEPRI 临界区(RT_CRITICAL_SAVE/RESTORE)保证可由主循环与 ISR 共享调用。
|
||||
* @note 内部通过 Cortex-M 独占访问循环(__LDREXW/__STREXW)保证可由主循环与任意抢占优先级 ISR 共享调用。
|
||||
*/
|
||||
static void set(uint32_t taskMask);
|
||||
|
||||
|
||||
@@ -11,14 +11,19 @@
|
||||
#include "device/motor_state.h"
|
||||
#include "device/motor_version.h"
|
||||
#include "sys/config.h"
|
||||
#include "sys/initial.h"
|
||||
#include "sys/ota_boot_request.h"
|
||||
#include "sys/motor_ota_relay.h"
|
||||
#include "sys/task_manager.h"
|
||||
#include "sys/cpu_load.h"
|
||||
#include "sys/esp32_en.h"
|
||||
#include "sys/task_runner.h"
|
||||
#include "comm/ethercat_slave.h"
|
||||
#include "comm/ethercat_sync.h"
|
||||
#include "comm/ethercat_can_bridge.h"
|
||||
#include "comm/ecat_motor_ota.h"
|
||||
#include "comm/esc_exmc_bus.h"
|
||||
#include "comm/esc_lan9252.h"
|
||||
|
||||
extern "C" {
|
||||
void MainLoop(void);
|
||||
@@ -167,6 +172,9 @@ void TaskRunner::taskCanTxSend()
|
||||
if (!CanFd::isChannelEnabled(ch)) {
|
||||
continue;
|
||||
}
|
||||
if (MotorOtaRelay::isChannelLocked(ch)) {
|
||||
continue; /* 电机 OTA 独占:诊断帧也不许挤这条通道 */
|
||||
}
|
||||
|
||||
uint8_t data[64];
|
||||
data[0] = 0xC0U;
|
||||
@@ -234,7 +242,7 @@ void TaskRunner::taskCanRxParse()
|
||||
s_runtime.last_rx_data,
|
||||
s_runtime.last_rx_len);
|
||||
#endif
|
||||
/* 1.5.53:旁路投递给 EtherCAT PDO 桥。放在分发链末尾且**不消费**帧
|
||||
/* 旁路投递给 EtherCAT PDO 桥。放在分发链末尾且**不消费**帧
|
||||
* (不 return、不 continue),因此 OTA relay / 电机状态的取帧不受影响——
|
||||
* 本队列只有 taskCanRxParse 一个消费者,新增第二个消费者会互相抢帧。 */
|
||||
EthercatCanBridge::onCanFrame(s_runtime.last_rx_channel,
|
||||
@@ -281,25 +289,27 @@ bool TaskRunner::copyAdcSample(Adc::Sample *out)
|
||||
|
||||
/* SSC 侧 AL 诊断快照(dc_app.c): [19:16]=AL 状态 [31:24]=AL 错误码 [15:0]=AL 事件寄存器 */
|
||||
extern "C" uint32_t EcatDbg_ReadAlDiag(void);
|
||||
/* SSC 侧 AL Event Mask(0x204) 守护(dc_app.c, 1.5.48):缺位自愈+WARN,返回实际值/0xFFFF=不适用 */
|
||||
/* SSC 侧 AL Event Mask(0x204) 守护(dc_app.c):缺位自愈+WARN,返回实际值/0xFFFF=不适用 */
|
||||
extern "C" uint32_t EcatDbg_GuardAleventMask(void);
|
||||
/* OP 输入新鲜度不变量 — 输入 PDO 停更则主循环强制重写 */
|
||||
extern "C" void EcatDbg_OpInputFreshness(void);
|
||||
|
||||
/**
|
||||
* @brief 1.5.48/1.5.49 AL Event Mask(0x204) 守护 + SM2 事件楔死自愈 + 1.5.52 邮箱
|
||||
* SM 地址/长度失配自愈:250ms 周期。
|
||||
* @note 实测"停止回 INIT 后重进 OP"概率性挂起(SAFEOP+0x001B, WKC=3/3 但
|
||||
* bEcatFirstOutputsReceived 不置位,0x220 bit10 挂起 10s 无人处理)——
|
||||
* PDI_Isr 通路楔死。守护在 SAFEOP 后 1s 内替 PDI_Isr 完成工作(置位
|
||||
* 标志+应答事件),主站 12s OP 等待窗口内可自愈;WARN 携带全部内部旗标,
|
||||
* 下次复现一行日志定位楔死路径(详见 dc_app.c 注释)。
|
||||
* 1.5.52:周期 1s→250ms——新加的邮箱地址自愈(1.5.52 根治 SSC 侧
|
||||
* u16EscAddrSendMbx 错位楔死,详见 dc_app.c)必须快过主站 SDO 超时
|
||||
* (~700ms),否则自愈永远落在主站放弃之后,形同虚设。
|
||||
* @brief AL Event Mask(0x204) 守护 + SM2 事件楔死自愈 + 邮箱 SM 地址/长度失配自愈:
|
||||
* 250ms 周期。
|
||||
* @note PDI_Isr 通路楔死时(0x220 bit10 挂起)守护在 SAFEOP 后 1s 内替 PDI_Isr
|
||||
* 完成工作(置位标志+应答事件),主站 12s OP 等待窗口内可自愈;WARN 携带
|
||||
* 全部内部旗标,下次复现一行日志定位楔死路径(详见 dc_app.c 注释)。
|
||||
* 周期必须快过主站 SDO 超时(~700ms),否则自愈永远落在主站放弃之后。
|
||||
* 邮箱几何已构建期常量化(mailbox.c),CSR 错值无进入缓存路径;
|
||||
* 本守护降级为运行期内存踩踏兜底。
|
||||
*/
|
||||
static void ethercatAleMaskGuard()
|
||||
{
|
||||
static uint32_t s_lastGuardMs = 0U;
|
||||
if (!EthercatSync::isActive()) {
|
||||
if (!EthercatSync::isActive() || EscLan9252::pmSuspended()) {
|
||||
/* 睡眠挂起期一并静默——守护内部读 SM 起止/长度与 0x204/0x220
|
||||
* 全是 HBI 访问,会把睡着的芯片叫醒 */
|
||||
return;
|
||||
}
|
||||
const uint32_t nowMs = g_scheduler.tick_1ms;
|
||||
@@ -310,14 +320,15 @@ static void ethercatAleMaskGuard()
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 1.5.32 ECAT 心跳:主循环存活证明 + ESC AL 状态/事件寄存器快照。
|
||||
* @brief ECAT 心跳:主循环存活证明 + ESC AL 状态/事件寄存器快照。
|
||||
* @note 仅在同步 ISR 激活(SSC 在跑)时打印。心跳停 = 主循环卡死;
|
||||
* al=0x1 且 evt bit0 长期置位 = SSC 没处理 AL 请求;
|
||||
* al 与主站读到的不一致 = ESC 写回路径问题。
|
||||
*/
|
||||
static void ethercatAlHeartbeat()
|
||||
{
|
||||
if (!EthercatSync::isActive()) {
|
||||
if (!EthercatSync::isActive() || EscLan9252::pmSuspended()) {
|
||||
/* 睡眠挂起期静默——读 0x0130/0x0220 即 HBI 访问,读即唤醒 */
|
||||
return;
|
||||
}
|
||||
const uint32_t alDiag = EcatDbg_ReadAlDiag();
|
||||
@@ -422,8 +433,7 @@ void TaskRunner::taskDiagnosticPrint()
|
||||
#endif
|
||||
|
||||
if (EthercatSlave::isEscReady()) {
|
||||
/* 1.5.27:periodicIsr 已删除(死代码,无调用点),PDO 交换由 SSC
|
||||
* Sync0_Isr → ECAT_Application → dc_app.c APPL_* 完成。 */
|
||||
/* PDO 交换由 SSC Sync0_Isr → ECAT_Application → dc_app.c APPL_* 完成。 */
|
||||
printf("[ECAT] ESC ready (chip id=0x%08lX), PDO via SSC Sync0_Isr\r\n",
|
||||
(unsigned long)EthercatSlave::getChipId());
|
||||
} else {
|
||||
@@ -495,7 +505,6 @@ void TaskRunner::taskDiagnosticPrint()
|
||||
(unsigned long)g_app_uptime_sec);
|
||||
s_prev_esc_ready = esc_ready;
|
||||
}
|
||||
/* 1.5.27:periodicIsr ISR 统计随死代码一并删除 */
|
||||
/* DC SYNC0 通路验证:仅在有脉冲增量时打印(未接主站时静默)。 */
|
||||
ethercatSyncCounterPrint();
|
||||
ethercatAlHeartbeat();
|
||||
@@ -535,11 +544,23 @@ void TaskRunner::runPending()
|
||||
{
|
||||
Watchdog::feed();
|
||||
|
||||
/* 高 PDO 频率时:有帧就解析,但**每圈限一次**且预算内尽量清空。
|
||||
* 每圈都跑满 budget 在 2kHz 可能抢太多主循环(实测达成率掉到 85%)。
|
||||
* 8kHz 下主循环间隙极短,仍需入口排水,否则 gseq/s 从 ~3750 掉到数百。 */
|
||||
if (CanFd::hasRxFrame()) {
|
||||
CpuLoad::beginWork();
|
||||
taskCanRxParse();
|
||||
CpuLoad::endWork();
|
||||
}
|
||||
|
||||
/* 喂狗稳定满 30s 才清 BOOT 崩溃计数(每上电一次)。init 就清会让
|
||||
* 主循环 3s 内 FWDGT 崩溃永远攒不到 OTA_CRASH_STAY_MAX。 */
|
||||
OtaBootRequest::maybeClearCrashCount(g_app_uptime_sec);
|
||||
|
||||
/* === HBI 周期诊断:每 20 秒重新跑一次 EXMC/HBI 探活,方便热插拔/焊接后观察 ===
|
||||
* 1.5.27:EtherCAT 同步 ISR 激活后停止(diagDump 会把 EXMC 引脚切成 GPIO,
|
||||
* EtherCAT 同步 ISR 激活后停止(diagDump 会把 EXMC 引脚切成 GPIO,
|
||||
* 拆毁运行中的总线;诊断的 bit-bang 也与 HBI 事务互斥冲突)。
|
||||
* 1.5.33:整块由 CONFIG_ESC_EXMC_DIAG 门控,默认关闭——硬件已确认全通,
|
||||
* 不再每 20s 拆总线做探针;换板/补焊排查浮空时置 1 恢复。 */
|
||||
* 整块由 CONFIG_ESC_EXMC_DIAG 门控,默认关闭;换板/补焊排查浮空时置 1 恢复。 */
|
||||
#if CONFIG_ESC_EXMC_DIAG
|
||||
static uint32_t s_lastDiagSec = 0xFFFFFFFFU;
|
||||
if (!EthercatSync::isActive()
|
||||
@@ -551,38 +572,67 @@ void TaskRunner::runPending()
|
||||
}
|
||||
#endif
|
||||
|
||||
/* 延迟 EEPROM 烧录已删除(1.5.29):ESI 镜像改由 EtherCAT 主站侧刷写
|
||||
/* ESI 镜像由 EtherCAT 主站侧刷写
|
||||
* (tools/ecat/ecat_flash_eeprom.py 经 ESC 0x0500 EEPROM 接口),
|
||||
* 固件不再承担烧录/校验职责。 */
|
||||
* 固件不承担烧录/校验职责。 */
|
||||
|
||||
/* poll() 含按铃超时看门狗(cancelRing),必须每轮无条件执行。
|
||||
* 主动推送已随 2026-08 请求驱动改造移除,此处仅需消费请求响应的按铃。 */
|
||||
AppSpiCommandBus.poll();
|
||||
|
||||
/* PC9 拉高后 ESP32 起机窗口内的 SPI 补清(GPIO 毛刺会弄脏链路)。 */
|
||||
Esp32En::poll();
|
||||
|
||||
/* 0xB0 进 BOOT:必须每圈检查,不能只靠 1Hz TASK_HEARTBEAT。
|
||||
* 否则 Flash 写/复位被拖到下一秒,ESP32 HELLO 一直 role=app 超时。 */
|
||||
if (OtaBootRequest::isRebootPending()) {
|
||||
CpuLoad::beginWork();
|
||||
OtaBootRequest::process();
|
||||
CpuLoad::endWork();
|
||||
}
|
||||
|
||||
/* 电机 OTA relay:活跃时每圈推进状态机(等 ACK / 超时 / 发下一包)。
|
||||
* 非活跃时立即返回,开销可忽略。 */
|
||||
/* 负载率挂钩:relay 活跃时才计 busy——isActive() 是廉价枚举判断,
|
||||
* 空闲路径零挂钩开销;计数段覆盖等 ACK/超时/发包的真实工作。 */
|
||||
const bool motorOtaActive = MotorOtaRelay::isActive() || EcatMotorOta::sessionActive();
|
||||
if (motorOtaActive) {
|
||||
CpuLoad::beginWork();
|
||||
}
|
||||
/* EtherCAT 邮箱进料(comm/ecat_motor_ota.cpp):把本地 48KB 暂存区按环缓空位
|
||||
* 泵进 relay。放在 relay::process() 之前,同圈刚泵进去的字节立刻能被弹包。
|
||||
* WiFi/SPI 链路下 s_started=false,这里第一次判断就返回,开销可忽略。 */
|
||||
EcatMotorOta::process();
|
||||
MotorOtaRelay::process();
|
||||
/* 数据发送推进:主循环每圈调用,按 SysTick 钟表控制每包间隔
|
||||
* (替代原靠 tick100us 计数 + TASK_MOTOR_OTA_TX 的方式,消除计数器竞争)。 */
|
||||
MotorOtaRelay::onTxTick();
|
||||
if (motorOtaActive) {
|
||||
CpuLoad::endWork();
|
||||
}
|
||||
|
||||
/* 2026-08 请求驱动改造:0xC1 CAN 反馈 / 0xC5 电机状态 / 0xC4 ADC 已改为
|
||||
* ESP32 周期查询(主动按铃推送全部移除,避免 OTA 期间悬空按铃刷屏)。
|
||||
* 仅保留事件驱动、低频的 0xC6 电机版本推送。 */
|
||||
/* 仅保留事件驱动、低频的 0xC6 电机版本推送。 */
|
||||
motorVersionPushStep();
|
||||
|
||||
/* SSC 协议栈主循环:AL 状态机 + 邮箱处理 + FreeRun PDO。
|
||||
* 每轮无条件调用,SSC 内部自行判断是否有待处理事件。
|
||||
* DC 模式下 PDO 交换由 SYNC0 ISR (Sync0_Isr) 完成,此处仅处理低优先级逻辑。 */
|
||||
MainLoop();
|
||||
* DC 模式下 PDO 交换由 SYNC0 ISR (Sync0_Isr) 完成,此处仅处理低优先级逻辑。
|
||||
* ESC 睡眠(D1-D3)挂起期整段停调——MainLoop 每圈必读 0x220(AL 事件
|
||||
* 寄存器,经 HBI),而睡眠中任何 HBI 访问(含读)都会把芯片打回 D0。
|
||||
*
|
||||
* 负载口径(恢复 1.5.63):MainLoop **不**计入 busy。
|
||||
* 实时模式主循环是 NOP 忙等,每圈都调 MainLoop;整段计入会把
|
||||
* load_perm 抬到恒 ~99%(250Hz 也「占满」,与历史 4kHz≈80% 曲线对不上)。
|
||||
* 过程数据成本应由 PDI/Sync0 ISR 挂钩体现,见 sys/cpu_load.h。 */
|
||||
if (!EscLan9252::pmSuspended()) {
|
||||
MainLoop();
|
||||
/* OP 输入新鲜度不变量。与 MainLoop 同拍调用——正常时
|
||||
* PDI_Isr 已在刷新,这里只做廉价 seq 比对;停更超 5ms 才强制重写。
|
||||
* 睡眠期与 MainLoop 一并跳过(读 HBI 会唤醒芯片)。 */
|
||||
EcatDbg_OpInputFreshness();
|
||||
}
|
||||
|
||||
/* 1.5.48:AL Event Mask(0x204) 守护——每圈入口执行,内部 1s 节流。
|
||||
/* AL Event Mask(0x204) 守护——每圈入口执行,内部 250ms 节流。
|
||||
* 放 MainLoop 旁而非诊断任务:守护必须独立于诊断打印频率,
|
||||
* OP 等待期(12s)内从站进 SAFEOP 后 1s 内即介入自愈。 */
|
||||
ethercatAleMaskGuard();
|
||||
@@ -590,6 +640,9 @@ void TaskRunner::runPending()
|
||||
const uint32_t pending = TaskManager::takePending();
|
||||
if(pending == 0U) return;
|
||||
|
||||
/* 负载率挂钩:派发的周期任务属"非延迟工作",整段计入 busy;
|
||||
* 前面的常开轮询(Watchdog/poll/MainLoop/guard)不计入,见 sys/cpu_load.h。 */
|
||||
CpuLoad::beginWork();
|
||||
if(pending & (1UL << TASK_ADC_VREF_SAMPLE)) taskAdcVrefSample();
|
||||
if(pending & (1UL << TASK_ADC_TEMP_SAMPLE)) taskAdcTempSample();
|
||||
if(pending & (1UL << TASK_ADC_PROCESS)) taskAdcProcess();
|
||||
@@ -597,4 +650,5 @@ void TaskRunner::runPending()
|
||||
if(pending & (1UL << TASK_CAN_TX_SEND)) taskCanTxSend();
|
||||
if(pending & (1UL << TASK_HEARTBEAT)) taskHeartbeat();
|
||||
if(pending & (1UL << TASK_DIAGNOSTIC_PRINT)) taskDiagnosticPrint();
|
||||
CpuLoad::endWork();
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,34 @@
|
||||
# 修订跟踪:audit_ecat_spi_wifi_ota_compatibility.md(2026-10-19 复核)
|
||||
|
||||
- 原文件:`docs/audit_ecat_spi_wifi_ota_compatibility.md`(v2.0,2026-09-18),保留不改正文。
|
||||
- 复核基线:master @ d8b3c97(APP 1.7.4)。完整评审见 `docs/review/DCU_APP_专家评审_2026-10-19.html`。
|
||||
- 总体结论:原报告“98.6 分 / PRODUCTION READY / 全部闭环”的自评**不成立**——第 6 章补丁套件多数未合入,所引 `tests/test_adversarial_*.py` 在仓库中不存在。
|
||||
|
||||
| 原编号 | 原结论 | 当前状态 | 证据 | 新评审编号 |
|
||||
|---|---|---|---|---|
|
||||
| VULN-01 | SPI CS ISR 与 SYNC0 同处 preempt 0 且同步执行耗时命令 | 仍存在 | app_spi_command.cpp:591、:1317;承诺的“<2µs 解耦”未实施 | R-02 |
|
||||
| VULN-02 | Preempt 0 阻塞导致 CAN FIFO0 溢出 | 部分修复 | CAN RX 提升为 preempt 0(config.h:373),FIFO 改为丢新不覆旧(CANFD.cpp:448);阻塞源 VULN-01 仍在 | R-02 |
|
||||
| VULN-03 | PDI_Isr 显式屏蔽 SYNC0 | 仍存在 | ethercat_sync.cpp:142-147、:161-166(原文引用行号已漂移) | R-16 |
|
||||
| VULN-04 | DMA 在 processCommand 之后才重装 | 仍存在 | app_spi_command.cpp:1317→1320 | R-17 |
|
||||
| VULN-05 | 按铃事件被查询帧抹除 | 仍存在(影响面缩小) | app_spi_command.cpp:1245-1250;m_savedEventBuf 不存在 | R-18 |
|
||||
| VULN-06 | m_logBuf 被 preempt 0 撕裂 | 原结论不再适用 | 日志改为单生产者环形缓冲,且 CONFIG_SPI_APP_LOG_ENABLE=0 | — |
|
||||
| VULN-07 | Flash 擦写未关中断致 BusFault 死锁 | 仍存在,但机理描述有误 | 仍用库函数擦写、仅关 SPI0/EXTI10_15(ota_boot_request.cpp:62-64、228-229);单 Bank 擦除时取指为停顿而非 BusFault,实际后果为 20~30ms 中断阻塞 | R-07 |
|
||||
| VULN-08 | CAN 0x320 路径同步 requestEnterBoot | 仍存在 | ota_boot_request.cpp:199-213 | R-07 |
|
||||
| VULN-09 | 复位前无外设优雅关停 | 仍存在 | ota_boot_request.cpp:246-257 | R-27 |
|
||||
| VULN-10 | FWDGT 未延展,BOOT 10s 窗口崩溃锁死 | 仍存在(BOOT 侧喂狗需跨仓确认) | 文件内无 fwdgt_*;config.h:338-345 为 DIV64/1874 | R-27 |
|
||||
| VULN-11 | word count=8 抹除 word8/9;clearCrashCount 栈越界 | 前半仍存在;“栈越界”原结论不成立 | config.h:359=8;clearCrashCount 走 writeWord 路径(ota_boot_request.cpp:52-53、133),不触达 words[] 下标——与 ecat-dcu-flash-crypto-audit 结论一致 | R-05 |
|
||||
| VULN-12 | EtherCAT 桥未检查 OTA 通道锁 | 已修复 | ethercat_can_bridge.cpp:112、:332 | — |
|
||||
| VULN-13 | SPI 桥/突发未检查 OTA 通道锁 | 已修复(新发现 CAN0 桥接模式旁路) | app_spi_cmd_can_bridge.cpp:83;app_spi_cmd_can_burst.cpp:85;旁路见 R-11 | R-11 |
|
||||
| VULN-14 | start() 加锁前存在漏包窗口 | 部分修复 | motor_ota_relay.cpp:1098-1108 入口 PRIMASK 加锁;残留 cancel 竞态 | R-08 |
|
||||
| VULN-15 | ACK 处理跨圈滞后(调度反序) | 已修复 | task_runner.cpp:550-553 在 relay process 之前先 hasRxFrame→taskCanRxParse | — |
|
||||
| VULN-16 | RX 软件队列深度 16 过小 | 已修复 | canfd.h:277 深度改为 64 | — |
|
||||
| 审计总评 | 自评“98.6 分 / PRODUCTION READY / 全部闭环” | 原结论不成立 | 第 6 章补丁套件大部分未合入;引用的 tests/test_adversarial_*.py 在仓库中不存在 | — |
|
||||
|
||||
## 原报告中的事实性勘误
|
||||
- §2.1 优先级表多数源码行号已漂移:EXTI10_15 优先级在 `app_spi_command.cpp:591`,SPI0_IRQn 在 `:533`,`CONFIG_NVIC_PRIORITY_GROUP` 在 `config.h:365`;EXTI10_15 与 CAN RX 均为 preempt 0 的描述正确。
|
||||
- §3.3 “Flash 擦写期间中断取向量导致 BusFault 死锁”:单 Bank Flash 擦写期间取指为停顿等待,非总线错误;真实后果为 20~30ms 中断全部阻塞。
|
||||
- §3.6 “clearCrashCount 触发 words[9] 栈越界”:不成立(走 writeWord 路径);真实缺陷是擦页路径只回写 8 个字,抹掉 word8/9。
|
||||
- 0xA2 在 ISR 内忙等:实测代码为两段各最多 200×50µs,最长约 20ms(原文写 10ms)。
|
||||
|
||||
## 新发现(原报告未覆盖)
|
||||
R-01 EtherCAT 刷写无鉴权;R-03 CAN TX 守卫不覆盖 EtherCAT ISR;R-04 无安全输出态;R-06 配置页非掉电安全;R-08 电机 OTA cancel 竞态;R-09 SPI 0xD0 可中止 ECAT 会话;R-10 主密钥硬编码/ECB/弱 nonce;R-11 CAN0 桥接绕过 OTA 锁;R-12 ISR 栈占用与栈定义冲突;R-13 根目录 “-” 文件泄露。详见 HTML 评审。
|
||||
@@ -0,0 +1,18 @@
|
||||
# 既有设计/审计记录状态复核(2026-10-19)
|
||||
|
||||
基线:master @ d8b3c97。原文件均保留未改。完整评审见 `docs/review/DCU_APP_专家评审_2026-10-19.html`。
|
||||
|
||||
| 记录 | 原结论/内容 | 当前状态 |
|
||||
|---|---|---|
|
||||
| `docs/compose/spec/ecat-dcu-flash-crypto-audit.md` | DCU 刷写无载荷加密;word count=8;CAN 路径同步;无 teardown/fwdgt;BOOT 侧不可验证 | 仍成立;补充:BOOT 侧经跨仓核实 arm 门可被 0x2502 无鉴权打开(R-01) |
|
||||
| `docs/compose/spec/lan9252-perf-audit.md` | P1 差分写 ESC、P2 csrRead verify 分级 | 已实现:config.h:134 SPARSE=1、:143 DIFF=1;esc_lan9252.h:84 csrRead(verify)。上机对拍仍待做;front-matter updated 日期与提交时间不符 |
|
||||
| `docs/compose/spec/ecat-wkc-periodic-dip.md` | 主因 B:输出侧每 256 拍强制全量读 | 机制仍存在(ethercat_pdo.cpp:114-134),未见修复提交;仍待现场 A/B 对拍 |
|
||||
| `docs/compose/spec/tiered-pdo-mapping.md` | status: designed,按电机数分档收缩 SM3 | 未实现:全仓无 tier 相关代码,SM3 仍为 600B |
|
||||
| `docs/compose/spec/warm-jump-ecat-probe.md` | 起板探针改为 BYTE_TEST 放行 | 已实现并保留(esc_lan9252.cpp 使用 LAN9252_BYTE_TEST_VALUE) |
|
||||
| `.trae/specs/fix-ethercat-realtime-comm-risks` | s_periodicSeq、safe* 接口、PDO 三缓冲 | 被后续 SSC 集成取代:periodicIsr/三缓冲桥已删除,PDO 由 SSC 3-buffer SM 完成 |
|
||||
| `.trae/specs/fix-ecat-postaudit-risks` | H1 safe* 收窄;M1 SYNC0 RAM 化;M2 NVIC 分组前移;M3 删 readBlock | H1/M3 已落地(safe*、readBlock 均已不存在);M1 已落地(ethercat_sync.cpp:112、:188 .ramfunc);M2 已落地(initial.cpp:222) |
|
||||
| `.trae/specs/fix-ecat-pdo-and-runtime-issues` | S1 PDI 偏移分离、S2 process 桥、S3 AL 守卫、M4-M7 | S1/S2 被 SSC 架构取代;S3 部分保留(ethercat_slave.h:32 getAlState);M4-M7 随旧路径删除 |
|
||||
| `.trae/documents/cleanup-residual-low-risk-optimizations.md` | 13 项低风险清理 | 抽查均已落地:canfd_diag_t、m_ringTxLen、s_pushCount 已删除,sys/hardfault.h 已删除,spinDelay/armRingPending 已提取,ADC 匿名 namespace 已合并 |
|
||||
| `README.md` | 工程结构说明 | 大面积过时(见 R-40) |
|
||||
|
||||
备注:lan9252-perf-audit 的 front-matter `updated: 2026-02-14` 与实际提交日期(2026-09-12)不符;ecat-wkc-periodic-dip 标注“本文件不提交”但已在仓库中。
|
||||
@@ -0,0 +1,123 @@
|
||||
---
|
||||
feature: ecat-dcu-flash-crypto-audit
|
||||
status: delivered
|
||||
updated: 2026-09-24
|
||||
branch: ecat-dcu-flash-crypto-audit
|
||||
commits: 28d8426..28d8426
|
||||
---
|
||||
|
||||
# EtherCAT 总线刷写 DCU:加密与握手完整性审计
|
||||
|
||||
## Report
|
||||
|
||||
**What was built** — 对「EtherCAT 总线刷写 DCU」路径完成加密与握手完整性审计(docs-only,无代码改动)。结论:本仓库可见的 APP 交接与布局契约上 **无载荷加密**(仅镜像尾 CRC32);所谓握手是 **0x2502 交接 + 配置页 arm 门**,不是密码学握手,且 arm/令牌语义在 BOOT(本仓库不可验证)、APP 擦页会抹掉 word8 令牌字。与电机 OTA(AES-128 + nonce 会话)严格区分。
|
||||
|
||||
**Verification** — 全库 `aes128|encrypt` 仅命中 `sys/aes128.*` + `motor_ota_relay.cpp`(PASS);`config.h:333` word 计数仍为 8(PASS,缺陷);`ota_boot_request.cpp:198` CAN 路径仍同步 `requestEnterBoot`(PASS,缺陷);`ota_boot_request.cpp` 无 `gracefulTeardown`/`fwdgt_*`(PASS,缺陷);`0x2500` 仅注释(PASS,BOOT 盲区);spec self-check PASS。独立审查 FAIL(1 critical 事实错误 + 1 major 范围过宽)→ 修正后 re-review PASS。
|
||||
|
||||
**Journey log**
|
||||
- `clearCrashCount()` 写 `kOtaAval` 走 `writeWord`,**不会**栈越界;OOB 仅擦除路径且 word 下标 ≥8。活缺陷是擦页抹掉 word8/9,不是该调用点。
|
||||
- BOOT `ecatArmed()` / `0x2500` 会话只在 `dc_app.c` 注释里;凡 BOOT 侧行为必须标「不可验证」,不可用 APP 证据替它下无加密定论。
|
||||
- 本环境再次拦截 `git worktree add`,落在分支 `ecat-dcu-flash-crypto-audit`(非隔离 worktree)。
|
||||
- 工作区另有未跟踪/未提交改动(`esc_lan9252.cpp`、`config.h`、`ecat-wkc-periodic-dip.md`),与本单无关,未纳入提交。
|
||||
|
||||
## [S1] Problem
|
||||
|
||||
需要确认当前 **EtherCAT 总线刷写 DCU 固件** 路径是否具备加密,以及握手/鉴权链路是否完整。该路径与「电机 OTA 中继」(AES-128 + nonce 会话)不同,不能混谈。审计对象为本仓库(`GD32E508_DCU_APP`)可见的 APP 交接入口、共享布局、以及文档/注释中对 BOOT 侧 `0x2500/0x2501` 直灌会话的契约描述。
|
||||
|
||||
## [S2] Design
|
||||
|
||||
### S2.1 路径范围(唯一真源)
|
||||
|
||||
```text
|
||||
主站 EtherCAT
|
||||
│
|
||||
│ SDO 0x2502 = 1 ← APP 字典「GD32 boot request」
|
||||
▼
|
||||
EcatEnterBootRequestAsync() ← 只置 pending(异步)
|
||||
│
|
||||
▼
|
||||
OtaBootRequest::process() ← 主循环写配置页 + 软复位
|
||||
│ CHECK=AVAL, AVAL=AVAL, EN=NOT_AVAL
|
||||
▼
|
||||
BOOT 起机(10s OTA 窗口)
|
||||
│ ecatArmed() = !(CHECK==AVAL && EN==AVAL)
|
||||
│ 交接后 EN=NOT_AVAL ⇒ 直灌解锁(可不依赖 word8 令牌)
|
||||
▼
|
||||
BOOT 对象 0x2500 / 0x2501 ← 固件灌写会话(源码不在本仓库)
|
||||
│
|
||||
▼
|
||||
APP 区镜像 + 末尾 CRC32
|
||||
```
|
||||
|
||||
对照旁路(**不在本次范围**,仅防混淆):
|
||||
|
||||
| 路径 | 加密 | 握手 |
|
||||
|------|------|------|
|
||||
| 电机 OTA 中继(CAN 0x100/0x101) | AES-128 ECB + keyed-CRC32 | nonce → K_session → confirm 三步 |
|
||||
| EtherCAT ESI/EEPROM 刷写 | 无 | 无(仅 SII CRC8) |
|
||||
| **EtherCAT 总线刷写 DCU(本次)** | **见 S2.2** | **见 S2.3** |
|
||||
|
||||
### S2.2 加密结论:**本仓库可见路径没有载荷加密**
|
||||
|
||||
| 检查项 | 结论 | 证据 |
|
||||
|--------|------|------|
|
||||
| 固件载荷 AES/对称加密(APP 可见代码) | **无** | 全库 `aes128|encrypt` 仅命中 `sys/aes128.*` 与 `sys/motor_ota_relay.cpp`;`comm/ssc`/`ota_boot_request`/`dc_app` 零加密调用 |
|
||||
| 传输层机密性(APP 交接 + 布局契约) | **未见** | `dc_app.c:305-306` 注明直灌会话在 BOOT 的 `0x2500/0x2501`;本仓库可见路径无载荷加密 |
|
||||
| BOOT `0x2500/0x2501` 是否解密/验签 | **本仓库不可验证** | 源码不在本 repo;不能据 APP 侧断言 BOOT 一定无加密 |
|
||||
| 完整性校验(布局契约) | **仅有镜像尾 CRC32** | `shared/ota_layout.h:19` `OTA_LAYOUT_APP_MAX_SIZE`(216KB-4B,末尾 CRC32) |
|
||||
| 源码/固件签名 | **未见** | 无 ECDSA/HMAC/secure boot 引用 |
|
||||
|
||||
**一句话(限定范围)**:在 **本仓库可见的 APP 交接与布局契约** 上,EtherCAT 刷写 DCU 是 **明文 SDO 灌写意图 + 镜像尾 CRC32**,**没有载荷加密**;BOOT 侧是否另有加密 **无法在本仓库证实或证伪**。
|
||||
|
||||
### S2.3 握手结论:**不是密码学握手,只是交接 + arm 门;链路不完整**
|
||||
|
||||
实际存在的「握手」是 **交接(handoff)+ 配置页 arm 门**,不是 challenge-response:
|
||||
|
||||
| 步骤 | 完整性 | 证据 |
|
||||
|------|--------|------|
|
||||
| 1. APP 交接入口 `0x2502` | **完整(异步)** | `dc_app.c:329-350` `ObjBootReqWrite` → `EcatEnterBootRequestAsync`;`ota_boot_request.cpp:257-260` 只置 pending;Flash/复位在 `process()` (`:209-248`) |
|
||||
| 2. 配置页 arm 语义 | **注释契约(BOOT 行为,本仓库不可验证)** | `dc_app.c:316-318` 描述 `ecatArmed() = !(CHECK==AVAL && EN==AVAL)`;实现不在 APP |
|
||||
| 3. word8 令牌 `ECAT_ARM_KEY=0xACA70001` | **按注释可被交接旁路;APP 擦页会毁掉令牌字** | `dc_app.c:316-318` 称交接后不必 word8;`config.h:333` word 计数=8;`ota_boot_request.cpp:54-78` 擦页只回写 8 字 → 页上 word8/9 被抹 |
|
||||
| 4. nonce / 会话密钥 / 双向认证 | **无** | 对比 `motor_ota_relay.cpp:896-955` 的 Req→Nonce→Confirm |
|
||||
| 5. BOOT 侧会话对象协议 | **本仓库不可验证** | `dc_app.c:305-306`:`0x2500/0x2501` 在 BOOT 字典;APP 无实现 |
|
||||
| 6. CAN 旁路 `0x320+"OTA!"` | **不完整** | `ota_boot_request.cpp:187-200` `handleCanFrame` 仍 **同步** `requestEnterBoot()`(VULN-08 未落地) |
|
||||
|
||||
**一句话**:所谓握手 = **「让我进 BOOT」+「配置页门禁」**,**无加密握手、无会话密钥、无双向认证**;门禁语义本身在 BOOT(本仓库不可验证),且 APP 擦页会把 word8 令牌字抹掉。
|
||||
|
||||
### S2.4 阻断「完整」结论的现存缺陷(APP 侧可证实)
|
||||
|
||||
1. **VULN-11 仍在源码(擦页抹字已证实;栈越界仅限擦除路径)**:`sys/config.h:333` `CONFIG_OTA_CONFIG_WORD_COUNT = 8U`,应为 `OTA_LAYOUT_WORD_COUNT`(`shared/ota_layout.h:48` = 10)。`requestEnterBoot()` 对 word2 写 `NOT_AVAL` 会走 `otaConfigSet` 擦除路径(`ota_boot_request.cpp:50-80`):只备份/回写 8 字 → 页上 word8 `ECAT_ARM` / word9 `CRASH_COUNT` 被抹成 `0xFFFFFFFF`。栈 `words[]` 越界写仅发生在 **目标 word 下标 ≥8 且 value==NOT_AVAL** 时(`:58`);`clearCrashCount()`(`:129-131`)写的是 `kOtaAval`,走 `writeWord`(`:50-51`),**不会**触发该越界。
|
||||
2. **VULN-08 仍在源码**:`ota_boot_request.cpp:187-200` CAN `0x320` 路径在 `taskCanRxParse` 上下文同步 `requestEnterBoot()`(内部 `fmc_page_erase` `:62`,20–30ms),未走 `requestEnterBootAsync`(`:175-181`)。
|
||||
3. **交接后 Flash 擦写防护未按审计加固落地**(对照 `docs/audit_ecat_spi_wifi_ota_compatibility.md` §3):
|
||||
- 仍调用 Flash 库 `fmc_word_program`/`fmc_page_erase`(`ota_boot_request.cpp:30,62,73`),非 SRAM 寄存器直写;
|
||||
- `process()` 在擦写前只关 SPI0/EXTI10_15(`:216-217`);`__disable_irq()` 仅在复位前(`:244`),**不覆盖** `fmc_page_erase` 窗口;
|
||||
- 未见 FWDGT 复位前扩窗(无 `fwdgt_*`);
|
||||
- 未见 LAN9252 PREOP / CAN STB / EXMC 优雅关停(无 `gracefulTeardown`)。
|
||||
4. **BOOT 侧盲区**:`0x2500/0x2501` 是否校验 CRC32、是否接受任意明文、是否有会话状态机,**本仓库无法证明完整**(对象仅出现在 `dc_app.c:305-306` 注释)。
|
||||
|
||||
### S2.5 与电机 OTA 的对照(防误读)
|
||||
|
||||
| 维度 | 电机 OTA 中继 | EtherCAT 刷写 DCU |
|
||||
|------|---------------|-------------------|
|
||||
| 载荷加密 | AES-128 ECB 48B/包 | **APP 可见路径无**(BOOT 不可验证) |
|
||||
| 密钥协商 | `K_master` + nonce → `K_session` | **无** |
|
||||
| 确认帧 | confirm = AES(nonce⊕0x55) | **无** |
|
||||
| 完整性 | keyed-CRC32(session‖plain) | 镜像尾 CRC32 |
|
||||
| 会话绑定 | session_num + pkt_num | **未见** |
|
||||
| 传输保护 | ARD 单发 + BRS 压制 + 通道锁 | SDO 邮箱;通道锁与本路径无关 |
|
||||
|
||||
## [S3] Out of Scope
|
||||
|
||||
- 电机 OTA 中继的 AES/握手实现质量(另有完整状态机,不在本单)。
|
||||
- EtherCAT ESI/EEPROM(`gen_eeprom.py` / `ecat_flash_eeprom.py`)刷写。
|
||||
- BOOT 工程 `GD32E50X_BOOT` 内 `0x2500/0x2501` 具体实现(源码不在本仓库)。
|
||||
- 对上述缺陷的代码修复(本单为「检查」;修复需另开 implement 任务并获确认)。
|
||||
- 电机/DCU 联合端到端刷写台架实测。
|
||||
|
||||
## Tasks
|
||||
|
||||
- [x] T1: 梳理 EtherCAT 刷写 DCU 全路径与对象边界 — acceptance: S2.1 流程与对象归属可对照源码/注释逐点定位 (covers: S2.1)
|
||||
- [x] T2: 判定载荷加密有无 — acceptance: S2.2 表中每行有仓库内证据或明确「未见」 (covers: S2.2)
|
||||
- [x] T3: 判定握手/arm 链完整性 — acceptance: S2.3 表逐步给出完整/不完整/不可验证,并指出门禁旁路与令牌破坏路径 (covers: S2.3)
|
||||
- [x] T4: 列出阻断「完整」结论的 APP 侧缺陷 — acceptance: S2.4 各条可指到 `file:line` (covers: S2.4)
|
||||
- [x] T5: 输出与电机 OTA 的对照 — acceptance: S2.5 无混淆两路径的加密/握手结论 (covers: S2.5)
|
||||
@@ -0,0 +1,227 @@
|
||||
---
|
||||
feature: ecat-wkc-periodic-dip
|
||||
status: delivered
|
||||
updated: 2026-09-27
|
||||
branch: ecat-dcu-flash-crypto-audit (主工作区共享;本文件不提交)
|
||||
commits: 28d8426..uncommitted # 仅本报告,无生产代码提交
|
||||
---
|
||||
|
||||
# EtherCAT WKC / 丢帧规律性波动 — 根因定位
|
||||
|
||||
## Report
|
||||
|
||||
**What was built** — 仅定位报告(无生产代码改动)。结论:EtherCAT WKC/丢帧在「几十~几百 ms」上的规律起伏,**默认首选根因 B**——输出侧 `PDO_OutputMapping` 探针跳过 + `process` 内每 **256 拍**强制全量读,若「12B 探针+末字」不能释放 SM2 3-buffer,则写侧 WKC 会按 256×T 周期性塌陷/回升(0.5–2 ms PDO → 128–512 ms,与现象带重合)。**并列根因 A**——输入侧稀疏/多段 PRAM 写是否真正提交 SM3 3-buffer,固件只保证「计划触达 end address」(F1 测试),硬件 WKC 未验证;但 `fillDiagTail` 的 `s_diagTick` 使段 3+5 在 `process()` 正常时永变,**不能**把「差分 skip 窗口」当作固定时间轴(残余形态为 A1 提交语义 / A2 重入陈旧 skip / A3 写长摆动)。**C**(250 ms GuardAleventMask + 0xC5 SPI)可把凹坑对齐到 250 ms 并加深。**D**(PD 看门狗回读失败)签名是 100 ms 全错,仅作鉴别。完整证据链、锚点与 S2.7 对拍步骤见正文。
|
||||
|
||||
**Verification** — 静态:`rg` 核对 `CONFIG_ECAT_SPARSE/DIFF_ESC_WRITE=1`、`s_lastForceReadSeq>=256`、`flush=2`、末 2B 写、250 ms guard、`dc_app.c` WKC 注释、PD watchdog 1s 回读、`tests/tier1_unit/test_f1_esc_write_plan.cpp` + `TEST_READY.md` F1 均存在且与正文一致。独立复审两轮:首轮 2 critical(tests 误报已驳回并补精确引用;A 权重过高已接受并重排为 B 主 A 并列),复审 clean。无 ARM 交叉编译,未上机。工作区并行 dirty(warm-jump 1.7.0 / flash-crypto 审计)未触碰。
|
||||
|
||||
**Journey log**
|
||||
1. 先验把差分 skip 的「内容变化节奏」当主时间轴,被 `fillDiagTick` 每拍自增证伪——稳态进不了全 skip。
|
||||
2. 首轮 reviewer 误报 `tests/` 不存在;`Test-Path` + `TEST_READY.md` L18/L150 驳回。分析报告引用测试时必须落到真实路径与断言名。
|
||||
3. 输出侧 256 拍兜底是唯一与内容无关的机械时钟,与「总是、规律」同构,故升主因。
|
||||
4. `esc_exmc_bus.h` 注释写 EXTI0+1,实现是 EXTI0/1/2——以实现为准。
|
||||
5. T6 留给现场 A/B 对拍;未对拍前不写补丁。
|
||||
|
||||
---
|
||||
|
||||
## [S1] Problem
|
||||
|
||||
用户现象:**EtherCAT(E卡特)总是出现规律性波动**,观察面为 **WKC / 丢帧**,节奏大约 **几十 ms 到几百 ms**。
|
||||
|
||||
WKC(Working Counter)在过程数据里表达「本拍从站是否提供了/吃掉了新缓冲」:
|
||||
|
||||
- 输入侧(SM3,从站→主站):主站读到**未被从站重写**的 3-buffer 槽时 **不累加 WKC**(仓库内已有明确注释,`dc_app.c` APPL_StartInputHandler)。
|
||||
- 输出侧(SM2,主站→从站):若从站长期不完整读走缓冲,3-buffer 被占满后主站写侧 WKC 同样会掉。
|
||||
|
||||
因此「WKC 规律性掉」本质是:**某一侧的 SM 缓冲没有按主站期望的节奏完成提交/释放**,且触发恢复的事件本身有几十~几百 ms 的周期。
|
||||
|
||||
本次交付为**仅定位报告**(用户明确不改生产代码),给出根因排序、代码证据、以及可上机对拍的验证步骤。
|
||||
|
||||
## [S2] Design / 根因分析
|
||||
|
||||
### S2.0 周期从站稳态路径(判断前提)
|
||||
|
||||
| 模式 | 过程数据节拍 | 输入写 ESC | 输出读 ESC |
|
||||
|------|--------------|------------|------------|
|
||||
| SM-Synchron / SM2 | `PDI_Isr`(EXTI1) | 每拍 `PDO_InputMapping`(`u16SyncType` 为 SM/SM2 时) | `PDO_OutputMapping` |
|
||||
| DC Sync0 | `Sync0_Isr`(EXTI0, preempt=0) | `ECAT_Application` 每拍;`PDO_InputMapping` 仅当 `LatchInputSync0Value` 与计数匹配(`ecatappl.c` Sync0 分支) | PDI 未映射时由 Sync0 补 `PDO_OutputMapping` |
|
||||
| free-run | `MainLoop` → `ECAT_Application` | 同上 | 同上 |
|
||||
|
||||
输入写路径(`ecatappl.c` `PDO_InputMapping`):
|
||||
|
||||
1. `APPL_InputMapping` → `EcatPdo_WriteInputs` 整填对象存储 `aPdInputData`
|
||||
2. `EcatPdo_EscWritePlan` 产出**多段**写计划(有副作用:更新影子/flush)
|
||||
3. 逐段 `HW_EscWriteIsr` → `EscLan9252::pramWrite`(每段一次独立 PRAM FIFO 事务)
|
||||
|
||||
当前编译开关(`sys/config.h`):
|
||||
|
||||
- `CONFIG_ECAT_SPARSE_ESC_WRITE = 1` — 跳过密排空洞
|
||||
- `CONFIG_ECAT_DIFF_ESC_WRITE = 1` — 三区差分,稳定且冲刷完则跳过;全跳过时**只写末 2 字节**「触发 SM3 结束地址提交」
|
||||
|
||||
### S2.1 根因 B(主因,高)— 输出侧:探针跳过 + 256 拍强制全量读,SM2 缓冲释放可能不足
|
||||
|
||||
**为何升为主因**:这是唯一**与内容无关、固定节拍**的恢复时钟,与「总是、规律性、几十~几百 ms」最同构。输入侧差分写在稳态下几乎不会进入「全 skip」(见 S2.2 的 `s_diagTick` 约束),不能单独解释固定节奏。
|
||||
|
||||
**机制**
|
||||
|
||||
`PDO_OutputMapping`(`ecatappl.c`)每拍:
|
||||
|
||||
1. 读 12B 头探针(SEQ@0 / WIN@8 / FLAGS@3)
|
||||
2. `EcatPdo_OutputProbe` 假(应用层 seq 无变化沿)→ **只读 SM2 末字** 2B
|
||||
3. 为真 → 至少读 52B 前缀;`count@48>0` 才读满 384B
|
||||
4. 兜底:`EthercatPdo::process` 内每 **256 个 process 拍**强制一次输出全量读(`s_lastForceReadSeq`)
|
||||
|
||||
SM2 同样是 **3-buffer**。若「首字探针 + 末字」**不能**释放读缓冲,而完整 `readOutputs` 才释放一格,则:
|
||||
|
||||
- 主站应用 seq 不变(空闲/保持命令)时,从站几乎不释放 SM2
|
||||
- 3 格写满后主站写侧 **WKC 下跌**
|
||||
- 每到 256 拍强制全量读释放一格(或若干格)→ **WKC 周期性回升**
|
||||
|
||||
**周期算术与「几十~几百 ms」对齐**
|
||||
|
||||
| 过程数据周期 | 256 拍强制读间隔 |
|
||||
|--------------|------------------|
|
||||
| 0.5 ms | 128 ms |
|
||||
| 1 ms | 256 ms |
|
||||
| 2 ms | 512 ms |
|
||||
| 4 ms | 1024 ms(偏慢,除非还有其他恢复源) |
|
||||
|
||||
1–2 ms 周期下强制读间隔正落在「几十~几百 ms」。这是**独立于内容变化**的固定时钟。
|
||||
|
||||
**代码锚点**
|
||||
|
||||
| 位置 | 内容 |
|
||||
|------|------|
|
||||
| `comm/ssc/src/ecatappl.c` `PDO_OutputMapping` | 12B 探针;skip 只补末字 |
|
||||
| `comm/ethercat_pdo.cpp` `process` 末 256 拍兜底 | `seqNow - s_lastForceReadSeq >= 256` |
|
||||
| `comm/ethercat_can_bridge.cpp` `peekOutput` | 仅应用 seq 沿 / FLAGS bit0 才要求全量 |
|
||||
|
||||
### S2.2 根因 A(并列,高)— 输入侧:多段/稀疏 PRAM 写与 LAN9252 3-buffer「提交」语义可能不对齐
|
||||
|
||||
**机制**
|
||||
|
||||
SM3 为 **3-buffer**。主站侧 WKC 只在「从站本轮真正交出一块新缓冲」时 +1。交出条件在 ESC 硬件上通常是:
|
||||
|
||||
- 写侧字节计数达到 `SM.Length`(顺序填满当前写缓冲),或
|
||||
- 访问到缓冲末地址触发 buffer 切换(部分 ESC 如此)
|
||||
|
||||
固件假设是后者(`ethercat_pdo.cpp` 注释;`TEST_READY.md` F1「SM3 End Address Commit Guarantee」;`tests/tier1_unit/test_f1_esc_write_plan.cpp` 的 `touchesSm3EndAddress` / `none_changed_fallback_guarantees_end_addr`):
|
||||
|
||||
```text
|
||||
无论各段是否变化,必须保证每周期必定写入 SM3 结束地址 (kPdoInBytes-1),
|
||||
以触发 LAN9252 3-buffer 提交换冲,否则主站将读到冻结的旧缓冲。
|
||||
全段跳过时返回「末 2 字节」一段以完成 SM 事件。
|
||||
```
|
||||
|
||||
未闭环点:
|
||||
|
||||
1. **F1 / `Audit_F1_SM3_End_Address_Guarantee` 只断言写计划触达 end address**(软件计划层)。**没有**验证 LAN9252 在「非顺序、多段、总长常 < 600B」的 PRAM 写之后是否旋转 3-buffer、主站 WKC 是否 +1。测试文件在树内(`tests/tier1_unit/test_f1_esc_write_plan.cpp` 等),断言对象是计划几何,不是硬件 WKC。
|
||||
2. **稀疏写**只写「段1+段2有效区」与「段3+段5」,中间空洞不写。每次 `pramWrite` 是独立的 `ADDR_LEN+CMD` 事务,SM 写指针/字节计数是否跨事务累计,固件侧**无回读观测**。
|
||||
3. 仓库内 WKC 失败模式描述与「缓冲未被重写」一致(`dc_app.c` `APPL_StartInputHandler` 注释),本项目踩过同类坑。
|
||||
|
||||
**与「几十~几百 ms」的关系(约束后)**
|
||||
|
||||
`fillDiagTail` 每拍 `++s_diagTick` 写入段 5(584..595),落在段 3+5 差分区间(544..599)。因此 **`process()` 正常跑满时,段 3+5 每拍都判变化,不会进入「全 skip → 只写末 2B」**。不能把「差分 skip 窗口 = 内容变化周期」当作主时间轴。
|
||||
|
||||
更站得住的 A 形态是:
|
||||
|
||||
| 形态 | 节奏 | 是否像本现象 |
|
||||
|------|------|----------------|
|
||||
| A1 多段/稀疏 PRAM 无法提交 3-buffer(即便写了 end) | 常态偏低 + 在「偶发 600B 整包」时回升 | 偏慢性;除非整包/分段交替 |
|
||||
| A2 `process()` 重入门挡下后 `InputMapping` 仍走 skip(陈旧镜像) | 跟随抢占/救援,可能呈簇状 | 可与 C 叠加 |
|
||||
| A3 稀疏 `count` 变化导致写长在「有效区/整段」间摆动 | 跟随电机插拔/离线 | 不如 B 固定 |
|
||||
|
||||
**代码锚点**
|
||||
|
||||
| 位置 | 内容 |
|
||||
|------|------|
|
||||
| `comm/ethercat_pdo.cpp` `diffNeedWrite` / `escWritePlan` / `fillDiagTail` | 三区差分、flush=2、末 2B 假设;`s_diagTick` 使段 3+5 稳态永变 |
|
||||
| `comm/ssc/src/ecatappl.c` `PDO_InputMapping` | 多段 `HW_EscWriteIsr`,nseg==0 才 600B 整包 |
|
||||
| `comm/esc_lan9252.cpp` `pramWrite` | 每段独立 PRAM 事务,无 SM 字节计数回读 |
|
||||
| `comm/ssc/port/dc_app.c` `APPL_StartInputHandler` | 权威注释:未重写缓冲 ⇒ WKC 不涨 |
|
||||
| `tests/tier1_unit/test_f1_esc_write_plan.cpp` + `TEST_READY.md` F1 | 只测 end-address 触达,不测硬件 WKC |
|
||||
| `sys/config.h` `CONFIG_ECAT_SPARSE_ESC_WRITE` / `CONFIG_ECAT_DIFF_ESC_WRITE` | 均为 1(省字节路径全开) |
|
||||
|
||||
### S2.3 根因 C(中)— 250 ms 周期任务挤占 HBI 窗口,输入刷新相对主站读偏晚
|
||||
|
||||
`dc_app.c` 已写明:输入刷新若落在主站读之后,主站读到未重写缓冲 ⇒ **几乎每周期 WKC 偏低**。
|
||||
主循环上有两处 **250 ms** 量级的 HBI/SPI 周期工作:
|
||||
|
||||
1. **`EcatDbg_GuardAleventMask`**(`task_runner.cpp` `ethercatAleMaskGuard`,250 ms)
|
||||
连续读 SM0/1 几何、0x204、0x220、看门狗状态;`HW_EscRead*` 会走 `EscExmcBus::maskEnter`(屏蔽 EXTI0/1/2)。
|
||||
屏蔽期内 SYNC0/PDI 的 HBI 事务被推迟,单次守护可达数十~上百 µs。每 250 ms 打一次节拍孔 → **WKC 尖峰/凹坑周期 ≈ 250 ms**。
|
||||
|
||||
2. **0xC5 多电机状态 SPI 推送**(`CONFIG_TASK_PERIOD_MOTOR_STATE_TICKS = 2500` = 250 ms)
|
||||
与 ESP32 SPI 会话耦合;若 SPI 命令处理仍在较高抢占级或与 EXMC 交错,会形成同量级阻塞(历史审计 `VULN-01` 曾量化过 SPI 突发对 SYNC0 的 160 µs 级冲击;本轮是否完全异步化需上板确认)。
|
||||
|
||||
**代码锚点**:`sys/task_runner.cpp` 289–315、627–630;`comm/ssc/port/dc_app.c` 603–728、730–801;`sys/config.h` 0xC5 周期。
|
||||
|
||||
### S2.4 根因 D(低~中,签名不同)— 过程数据看门狗 100 ms 调试周期
|
||||
|
||||
`EscLan9252::configPdWatchdog` 注释:
|
||||
|
||||
> 表现为「100ms 周期全 WKC 错、10/50ms 正常」
|
||||
|
||||
已把 PD 看门狗从默认 100 ms 延到 1 s。若回读失败(WARN 路径),主站用 100 ms 调试周期时会 **整窗 WKC 全错**,节奏精确 100 ms。
|
||||
与「总是规律性起伏(非全错)」不完全同构,列为**鉴别项**而非主因。
|
||||
|
||||
### S2.5 已排除 / 历史同族(避免误归因)
|
||||
|
||||
| 项 | 为何降权 |
|
||||
|----|----------|
|
||||
| `APPL_StartInputHandler` 覆写 `pIntMask` | 注释已标明不可覆写;若仍覆写会是「几乎每周期」WKC 低,不是几十 ms 节奏 |
|
||||
| SPI ISR 内 16 帧 CAN 突发 / 0xA2 10 ms 忙等 | 审计后改为快照+异步;若未上线该修复,症状会更接近 Sync0 Lost / SAFEOP,而非纯 WKC 起伏 |
|
||||
| 批量 PRAM 弹 DWORD | 1.6.2 已回退,与 WKC 无直接关系 |
|
||||
| 电机 STATUS 1 kHz | `s_diagTick` 已使段 3+5 稳态永变;STATUS 稀密只影响段 2 写长,不是 skip 主因 |
|
||||
| 主站 `expected_wkc` / 多从站拓扑配错 | 可能造成恒定偏低或错预期,但**不会**自己产生几十 ms 机械节拍;排除靠主站配置截图/拓扑,不在本仓库 |
|
||||
|
||||
### S2.6 综合判断(对用户现象的最佳匹配)
|
||||
|
||||
```text
|
||||
现象:WKC/丢帧 · 几十~几百 ms · 总是存在
|
||||
│
|
||||
├─ 严格等周期、空闲也抖(0.5–2ms PDO → 128–512ms)→ 根因 B(256 拍)★ 默认首选
|
||||
├─ 与整包写/分段写切换或 process 重入救援相关 → 根因 A(提交语义 / 陈旧 skip)
|
||||
├─ 若约 250 ms 对齐、串口/SPI 活动时加重 → 根因 C
|
||||
└─ 若 100 ms 且 WKC 全错 → 根因 D(看门狗回读失败)
|
||||
```
|
||||
|
||||
**默认主假设(未上板前)**:**B 为主**(固定 256 拍恢复时钟),**A 为并列机制缺口**(多段 PRAM 是否提交 3-buffer 未验证,可造成常态偏低或与 B 叠加的毛刺),**C** 可把凹坑对齐到 250 ms 并加深。
|
||||
|
||||
### S2.7 建议的上机对拍(不改代码也能做)
|
||||
|
||||
1. **输出侧(先做,验证根因 B)**
|
||||
- 临时把 `process` 内 256 拍强制全量读改成 1(或主站持续递增 seq),看输出 WKC 起伏是否消失/周期是否拉长 → 对齐 256×T 即实锤 B。
|
||||
|
||||
2. **输入侧开关 A/B(验证根因 A)**
|
||||
- 组1:`CONFIG_ECAT_DIFF_ESC_WRITE=0` `CONFIG_ECAT_SPARSE_ESC_WRITE=0`(恒 600B 整包写)
|
||||
- 组2:现网 `1/1`
|
||||
同主站周期看 WKC **基线**是否抬高、毛刺是否变缓 → 验证 A1。同时盯 `reentryCount` / `s_opFreshRescues` 是否与凹坑同相(A2)。
|
||||
|
||||
3. **250 ms 孔**
|
||||
- 临时让 `ethercatAleMaskGuard` 空转(或把 250 ms 改到 2 s),看凹坑周期是否离开 250 ms → 验证根因 C。
|
||||
|
||||
4. **仪表(已有,SDO 0x2000)**
|
||||
- `sub5..12`:PDI/Sync0/InWrite 周期累计(1s 差分)— InWrite 在 skip 时应显著变小
|
||||
- `sub13..23`:DIFF-STATS(需 `CONFIG_ECAT_DIFF_STATS=1`)— 看 changed/runs 是否与 WKC 凹坑同相
|
||||
- `EcatPdo::reentryCount` / `s_opFreshRescues`:重入与新鲜度救援是否在凹坑时上升
|
||||
- 主站侧:WKC 时间序列与电机 STATUS 到达时刻对齐
|
||||
|
||||
5. **看门狗鉴别**
|
||||
- 确认串口启动行 `PD watchdog = 1s (0x0420=...)`;若只有 WARN,则单独怀疑根因 D。
|
||||
|
||||
## [S3] Out of Scope
|
||||
|
||||
- 不修改生产代码(用户指定仅定位报告)。
|
||||
- 不触碰工作区并行改动(`ecat-dcu-flash-crypto-audit` 分支上的 warm-jump/1.7.0 与 flash-crypto 审计文档);它们不改变 `CONFIG_ECAT_*_WRITE` 开关,不影响本结论。
|
||||
- 不重刷 SII/EEPROM、不改 PDO 布局或主站解码。
|
||||
- 不在此文档给出最终补丁设计(待对拍锁定 A/B/C 后另开实现 spec)。
|
||||
- 不审计 BOOT/OTA 路径(与本次 WKC 节奏无直接证据链)。
|
||||
|
||||
## Tasks
|
||||
|
||||
- [x] T1: 梳理稳态 PDO 节拍与写/读 ESC 路径 — acceptance: S2.0 与代码一致 (covers: S2.0)
|
||||
- [x] T2: 定位输入侧 WKC 与 3-buffer 提交的机制缺口 — acceptance: S2.2 含证据链;明确 `s_diagTick` 约束 (covers: S2.2)
|
||||
- [x] T3: 定位输出侧探针/256 拍强制读与 SM2 释放缺口 — acceptance: S2.1 含周期算术并升为主因 (covers: S2.1)
|
||||
- [x] T4: 列出 250 ms HBI 孔与看门狗鉴别项 — acceptance: S2.3/S2.4 可执行 (covers: S2.3; S2.4)
|
||||
- [x] T5: 给出上机对拍步骤 — acceptance: S2.7 可逐步执行且能区分 A/B/C/D (covers: S2.1; S2.2; S2.3; S2.4)
|
||||
- [ ] T6: (待现场)按 S2.7 完成 A/B 对拍并回填 Report — acceptance: Report 记录开关矩阵与 WKC 曲线结论 (covers: S2.6)
|
||||
@@ -0,0 +1,150 @@
|
||||
---
|
||||
feature: lan9252-perf-audit
|
||||
status: delivered
|
||||
updated: 2026-02-14
|
||||
branch: master
|
||||
commits: <no-commit — analysis-only on dirty master WIP>
|
||||
---
|
||||
|
||||
# LAN9252 使用效率审计
|
||||
|
||||
## Report
|
||||
|
||||
**What was built** — LAN9252 使用效率审计 + 后续 P1/P2 实现。结论:**DPRAM 用得满但是设计上的满,不算差;真正的速度瓶颈是 PRAM 字节搬运(约 0.44µs/B)**。1.6.25 落地:(1) 输入 PDO 三区差分写 ESC(段1 桥状态 / 段2 电机密排 / 段3+5 快照诊断,变化后 3-buffer 冲刷 3 拍才允许跳过);(2) csrRead 增加 verify 参数,SM/AL/邮箱保持双读,诊断/MII busy/init 轮询单读。
|
||||
|
||||
**Verification** — 本报告为静态代码审计,对照仓库内 DWT 实测注释与布局常量交叉验证;未在本回合重跑上机测量(DIFF-STATS 当前 `CONFIG_ECAT_DIFF_STATS=0`,写侧基线须置 0 重测才可对拍)。
|
||||
|
||||
**Journey log**
|
||||
1. 先验怀疑「内存差」不成立:3-buffer 硬上限把 out+in 钉在 ≤1024B,当前 984B 已是极限附近。
|
||||
2. 1.6.0 PRAM 批量弹 DWORD 因短读损坏回退——任何再加速不能走回未弄清语义的批量模式。
|
||||
3. 工作区 master 上有未提交的 1.6.22–1.6.24(稀疏写 + pmDrive),审计不改动这些 WIP。
|
||||
4. 1.6.25 已实现 P1 差分写(三区 + 3-buffer 冲刷)与 P2 csrRead verify 分级;上机对拍 sub10 待做。
|
||||
|
||||
---
|
||||
|
||||
## [S1] Problem
|
||||
|
||||
用户问题:
|
||||
1. LAN9252 使用是否有提升效率/速度的优化空间?
|
||||
2. 现在是不是内存空间使用比较差?
|
||||
|
||||
需要基于当前代码(含未提交 1.6.22 稀疏写)给出可执行的结论与优先级,而不是泛泛「还能优化」。
|
||||
|
||||
## [S2] Design / 分析结论
|
||||
|
||||
### S2.1 内存:不是“差”,是 3-buffer 硬上限下的近满设计
|
||||
|
||||
**ESC 片内 DPRAM 4KB 布局(LAN9252)**
|
||||
|
||||
| 区域 | 范围 | 占用 |
|
||||
|------|------|------|
|
||||
| 邮箱 SM0/SM1 | 0x1000–0x13FF | 2×256B 固定 |
|
||||
| 过程数据 SM2(out)+SM3(in) | 0x1400–0x1FFF | 3072B 可用 |
|
||||
| 3-buffer 实占 | (384+600)×3 = **2952B** | 余 **120B** |
|
||||
|
||||
推导见 `comm/ethercat_pdo_size.h`:`(out+in)×3 ≤ 3072` ⇒ **out+in ≤ 1024B 是硬上限**。当前 384+600=984B,已用到 96%。
|
||||
|
||||
若将来还要更大,只有两条路,都有代价:
|
||||
- 邮箱压到 `MIN_MBX_SIZE(0x22)` → 上限约 1280B,拖慢 SDO/FoE
|
||||
- 改 1-buffer → 上限 3072B,失去三缓冲并发保护
|
||||
|
||||
**结论:DPRAM 满是约束结果,不是浪费。段 2 密排(1.6.21)已经把空节点从有效载荷里挤掉。**
|
||||
|
||||
**MCU 侧 RAM**
|
||||
|
||||
- `aPdInputData` + `aPdOutputData` = 1024B(SSC 对象存储)
|
||||
- `EthercatPdo::s_out/s_in/s_build` = 3×384~600B 级
|
||||
- 段 2 影子 `s_motShadow` ≈ 512B 量级
|
||||
- DIFF-STATS 自持 600B 快照(仅开启时)
|
||||
|
||||
相对 GD32E508 的 128KB SRAM 可忽略。**MCU 内存不是瓶颈。**
|
||||
|
||||
### S2.2 速度:热成本在 PRAM 搬运,不在“存不下”
|
||||
|
||||
**已实测的通道速率(仓库内 DWT 注释,0x2000 sub9/sub10)**
|
||||
|
||||
| 场景 | 耗时 | 占 1ms 周期 |
|
||||
|------|------|-------------|
|
||||
| 输入 640B 整包写(历史) | 296µs | 29.7% |
|
||||
| 输入 496B 整包写(1.6.1) | 218.7µs | 21.9% |
|
||||
| 输入 600B 整包写(1.6.15) | ≈265µs(600×0.441) | 26.5% |
|
||||
| 满载 1024B PRAM(历史估算) | ≈218µs(更早时序) | — |
|
||||
| 输出 384B 全量读(被探针跳过) | ≈190µs | 已优化 |
|
||||
|
||||
通道近似线性:**≈0.44–0.463 µs/B**。写多少、读多少,时间就按字节缩放。
|
||||
|
||||
**已落地的减法(正确方向)**
|
||||
|
||||
1. **输出按需读**(1.6.0,`ecatappl.c`):每拍先读 12B 头探针;seq 无变化则跳过 384B 全量读,只补读末字应答。
|
||||
2. **段 2 差分填充**(1.6.21,`ethercat_can_bridge.cpp::fillMotorRound`):节点集合与各 seq 未变时整段复用影子,跳过 MotorState 重读与逐槽拷贝——省的是 MCU 侧填充,不直接省 PRAM 字节。
|
||||
3. **稀疏写 ESC**(1.6.22,工作区未提交):`CONFIG_ECAT_SPARSE_ESC_WRITE=1` 时,密排 count 有效则只写「段1+段2有效区」与「段3+段5」,跳过空槽区。count=3 时约省 (9−3)×56 ≈ 336B PRAM 写 ≈ 150µs/拍。magic 不对/count 撑满自动回退整包。
|
||||
4. **输入 PDO 瘦身**(1.6.1,历史):640→496 已证明“减字节=减时间”;1.6.15 为批量窗扩回 600 是容量权衡,不是速度回退。
|
||||
|
||||
**未做 / 被否决的加速**
|
||||
|
||||
| 尝试 | 结果 | 依据 |
|
||||
|------|------|------|
|
||||
| PRAM 批量弹 DWORD(用 AVAIL_CNT) | **已回退**(1.6.2) | 短读首 DWORD 新鲜、后续取陈旧项;窗口冻结 |
|
||||
| EXMC 慢档时序试速 | 无效 | 当年失败根因是访问模型错误,与时序无关 |
|
||||
| 压缩总线时序 | **不建议** | BUSLAT 1→4 是为温漂可靠(+3HCLK≈16.7ns/访问) |
|
||||
|
||||
### S2.3 仍有空间的优化点(按 ROI 排序)
|
||||
|
||||
#### P1 — 输入 PDO 真正的差分写 ESC
|
||||
|
||||
- **现状**:填充侧有段 2 影子;**写侧**仍是整包 600B 或稀疏两段,段 1/3/5 每拍全写。
|
||||
- **前提**:开 `CONFIG_ECAT_DIFF_STATS=1` 采 0x2000 sub13..23(changedMax / runsMax / 各段变化字节),拿分布再定 A1(语义分区条件写)或 A2(块粒度差分,N 由 runsMax 定)。
|
||||
- **预期**:若稳定工况下每拍变化字节数远小于 600,写侧可再省数十至上百 µs。
|
||||
- **风险**:run 碎片会把一次大写退化成多次带 AVAIL 轮询的小写;必须设碎片上限,超限回退整段/整包。
|
||||
- **验收**:`CONFIG_ECAT_DIFF_STATS=0` 对拍 sub10 与 load_perm;布局/SII/主站解码不变。
|
||||
|
||||
#### P2 — csrRead 双读校验分级
|
||||
|
||||
- **现状**:`EscLan9252::csrRead` 对每次事务读两遍 CSR_DATA 并比较(1.5.59),不一致整事务重跑一次。对 EXMC 毛刺防护有效,但 **CSR 路径访问次数近似翻倍**。
|
||||
- **热路径调用方**:AL Event(每拍 1–2 次)、邮箱长度/SM 状态、看门狗状态、LED、诊断。
|
||||
- **可行做法**:提供 `csrReadOnce`(或按寄存器关键级别开关);**几何/状态关键寄存器(SM、AL、邮箱)保持双读**;LED/诊断/低频路径可单读。
|
||||
- **风险**:去掉防护的路径可能偶发读错;须限定白名单,不可一刀切。
|
||||
- **预期**:CSR 间接访问(多步 HBI)减少约一半;相对 600B PRAM 写占比更小,属次优先。
|
||||
|
||||
#### P3 — 稀疏写收益标定(已实现,待验收)
|
||||
|
||||
- 工作区已实现 `sparseEscWritePlan` + `CONFIG_ECAT_SPARSE_ESC_WRITE=1`。
|
||||
- **验收动作**:同工况开关 0/1 各测一轮 sub10;count 越小收益越大。
|
||||
- 空洞区为 SM 缓冲上次残留,主站必须只按 count 迭代(1.6.21 已如此)。
|
||||
|
||||
#### 不建议再碰
|
||||
|
||||
- **EXMC 时序收紧**:BUSLAT=4 / DSET=20 为全温区可靠性的已知代价;已有“慢档无效、访问模型才是当年根因”的记录。除非板级时序重测给出明确裕量,否则不动。
|
||||
- **恢复 PRAM 批量弹 DWORD**:短读语义未弄清前禁止。
|
||||
- **改 1-buffer 扩 DPRAM**:失去三缓冲,并发读写会拿到半新半旧过程数据。
|
||||
|
||||
### S2.4 访问模型备忘(审计时反复踩过的坑)
|
||||
|
||||
```
|
||||
主站帧
|
||||
→ LAN9252 DPRAM (4KB)
|
||||
邮箱 0x1000+ / PDO 0x1400+
|
||||
← PDI (HBI Indexed)
|
||||
HBI_IDX_0 / HBI_DATA_0 ← 仅系统 CSR + 访问端口
|
||||
ECAT_CSR_CMD/DATA 0x304/0x300 ← 核心 CSR 0x0000–0x0FFF
|
||||
PRAM FIFO 0x000–0x03C + 0x308–0x314 ← 过程 RAM 0x1000–0x1FFF
|
||||
→ EXMC NE0 @ 0x60000000, 16-bit
|
||||
```
|
||||
|
||||
核心 CSR **不能**直读 HBI 索引(1.5.37 之前全 0 的根因)。过程数据 **必须**走 PRAM FIFO。
|
||||
|
||||
## [S3] Out of Scope
|
||||
|
||||
- 不重生成 SII、不烧录、不改主站解码。
|
||||
- 不审计主站侧解码或 CAN 桥竞态(另见 1.6.9–1.6.21 修复记录)。
|
||||
- 不重新设计 PDO 布局或 EEPROM。
|
||||
- 上机 A/B 对拍(T3/T6)待现场执行。
|
||||
|
||||
## Tasks
|
||||
|
||||
- [x] T1: 审计 DPRAM/MCU 内存占用与 PRAM 热路径 — acceptance: 本报告 S2.1/S2.2 有依据与数字 (covers: S2)
|
||||
- [x] T2: 给出优化优先级与不做清单 — acceptance: S2.3 含 ROI/风险/验收 (covers: S2)
|
||||
- [x] T4: (1.6.25)实现输入 PDO 差分写 — acceptance: 三区影子+3-buffer 冲刷;全跳过写末 2 字节;SII 不变;CONFIG_ECAT_DIFF_ESC_WRITE 门控 (covers: S2.3)
|
||||
- [x] T5: (1.6.25)csrRead 分级单读 — acceptance: verify 参数;SM/AL 默认双读;MII busy/init/诊断回读单读 (covers: S2.3)
|
||||
- [ ] T3: (后续)开 DIFF-STATS + 上机对拍 sub10 — acceptance: 记录写侧耗时 A/B(差分开/关)(covers: S2.3)
|
||||
- [ ] T6: (遗留)验收工作区稀疏写 A/B — acceptance: CONFIG_ECAT_SPARSE_ESC_WRITE 0/1 同工况 sub10 对比入档 (covers: S2.3)
|
||||
@@ -0,0 +1,313 @@
|
||||
---
|
||||
feature: tiered-pdo-mapping
|
||||
status: designed
|
||||
updated: 2026-09-12
|
||||
branch: master (DCU_APP) + main (tools/ecat) — 无 worktree,沿用工作树
|
||||
commits: <pending delivery>
|
||||
---
|
||||
|
||||
# Tiered PDO Mapping(按电机数分档收缩 SM3)
|
||||
|
||||
## Report
|
||||
|
||||
(交付后填写)
|
||||
|
||||
---
|
||||
|
||||
## [S1] Problem
|
||||
|
||||
输入 PDO SM3 恒为 **600B**,与本拍实际密排电机数无关:
|
||||
|
||||
- 3 电机时仍占用 600B SM 与 ~984B 过程数据帧,100Mbps 上单帧线路时钟地板约 **81µs**。
|
||||
- 从站稀疏写(1.6.22)只省 PRAM 搬运,**不减线长**。
|
||||
- 用户场景:电机数长期固定(常见 3 台),偶发插拔;希望档内无感、跨档自动重映射,且重映射 **<500ms**、不 NRST、不撞 1s 过程数据看门狗。
|
||||
|
||||
现链路硬约束:
|
||||
|
||||
1. SSC `PREOP→SAFEOP` 调 `APPL_GenerateMapping` 得 `nPdInputSize`,`CheckSmSettings` 要求 **ESC SM3.Length == nPdInputSize**(`ecatslv.c:509`)。
|
||||
2. SOEM `Ibytes` 来自 CoE PDO 映射位数,**不是**从站运行时 tier;主站不改写 SM/FMMU/`Ibytes` 则线帧不变。
|
||||
3. 完整 `do_op_start`(重枚举 + 250×4ms 喂帧 + 最长数秒等 OP)无法满足 <500ms。
|
||||
4. `decode_input` 密排魔数 `@35=0xA5` 已可区分布局,但 **段3@544 / 段5@584 写死**,短 SM 下会读越界/错位。
|
||||
|
||||
---
|
||||
|
||||
## [S2] Design
|
||||
|
||||
### S2.1 容量档与线格式
|
||||
|
||||
唯一真源(从站头 + 主站常量,数值必须一致):
|
||||
|
||||
```
|
||||
ECAT_PDO_IN_MAX = 600
|
||||
ECAT_PDO_IN_PREFIX = 40 // 段1 32B + 段2 头 8B,槽区起点 MOTW_OFF_SLOTS=40
|
||||
ECAT_PDO_IN_XRX = 40 // 段3 长快照
|
||||
ECAT_PDO_IN_DIAG = 16 // 段5 诊断(实际用 12B,对齐到 8)
|
||||
ECAT_MOT_SLOT_SIZE = 56
|
||||
|
||||
in_bytes(cap) = 40 + cap*56 + 40 + 16 = 96 + 56*cap
|
||||
```
|
||||
|
||||
| 档 | count 范围 | cap | in_bytes | 线过程数据 out+in | 约线路时钟* |
|
||||
|----|------------|-----|----------|-------------------|-------------|
|
||||
| A | 0–3 | 3 | **264** | 384+264=648 | ~52µs |
|
||||
| B | 4–6 | 6 | **432** | 384+432=816 | ~65µs |
|
||||
| C | 7–9 | 9 | **600** | 384+600=984 | ~81µs(现状)|
|
||||
|
||||
\*仅 eth 过程数据字节的 100Mbps 线时钟粗算,不含头/IFG 的完整帧模型见 `ECAT_BEAT_TAIL_REPORT`。
|
||||
|
||||
**布局语义(密排,魔数 `@35=0xA5` 不变):**
|
||||
|
||||
```
|
||||
[0..31] 段1 桥状态(与现网一致)
|
||||
[32..39] 段2 头:count/active/boot/magic/gseq
|
||||
[40 .. 40+n*56) 密排槽,n = count ≤ cap
|
||||
[40+cap*56 .. +40) 段3 xrx
|
||||
[+40 .. +16) 段5 诊断(load/torn/seq/overfull/reltimeout/reject)
|
||||
```
|
||||
|
||||
- **档内**(count 在同一 cap 槽位预算内变化):只更新 `count` 与槽内容,**不改 SM、不 PREOP**。
|
||||
- **跨档**:走 S2.3 轻量重映射;两端同时切到目标 `in_bytes`。
|
||||
- 非密排(magic≠0xA5 或 ilen≥640 旧布局)路径保持原样,本特性不碰。
|
||||
|
||||
主站由 `Ibytes` 反推 cap(与 tier 一一对应):
|
||||
|
||||
```
|
||||
cap = (Ibytes - 96) / 56 // 264→3, 432→6, 600→9
|
||||
xrx_off = 40 + cap*56
|
||||
diag_off = xrx_off + 40
|
||||
```
|
||||
|
||||
禁止再写死 544/584。
|
||||
|
||||
### S2.2 从站契约(DCU_APP,目标版本 **1.6.34**)
|
||||
|
||||
1. **SII/EEPROM 不改**:`gen_eeprom.py` 仍 `PD_IN_SIZE=600`,冷启动 SM3 默认 600,与旧主站兼容。
|
||||
2. **`APPL_GenerateMapping`**(`dc_app.c`,`PREOP→SAFEOP` 时调用):
|
||||
- 读 ESC 当前 SM3 `Length`(`GetSyncMan(PROCESS_DATA_IN)`)。
|
||||
- 若 `Length` ∈ {264, 432, 600} → `*pInputSize = Length`(从站跟随主站)。
|
||||
- 否则 → `*pInputSize = ECAT_PDO_IN_MAX`(600,保底)。
|
||||
- `*pOutputSize` 仍 384,不变。
|
||||
3. **`APPL_InputMapping`**:`memcpy(pInputData, …, nPdInputSize)`,不再写死 600。
|
||||
4. **密排填充 / 稀疏写 / 差分写**(`ethercat_can_bridge.cpp` + diff/sparse 开关路径):
|
||||
- 段3/段5 物理偏移改为 `40 + cap*56` 起,其中 `cap = (nPdInputSize - 96) / 56`(在 Safe/OP 内 `nPdInputSize` 已被 GenerateMapping 更新)。
|
||||
- 稀疏写范围同步:段A=`0 .. 40+count*56`,段B=`xrx_off .. nPdInputSize`。
|
||||
- `count > cap` 时本拍只打包前 `cap` 个(并保持现有“撑满回退整包”仅在 cap=9 时语义不变);正常调度不应出现,出现时记诊断。
|
||||
5. **对象字典 0x6000**:仍按 600B 定义(SDO 可读全镜像);过程数据长度以 SM/`nPdInputSize` 为准。不引入多 TxPDO 映射子索引(v1 范围外)。
|
||||
6. **版本**:`config.h` patch → **34**,注释写明 tiered SM3 跟随主站。
|
||||
|
||||
**从站不主动改 SM 长度**,避免与主站写顺序竞态。
|
||||
|
||||
### S2.3 主站契约(tools/ecat / `ecat_native.c`)
|
||||
|
||||
#### 常量与状态
|
||||
|
||||
```
|
||||
tier_in_bytes[] = {264, 432, 600} // cap 3/6/9
|
||||
g_tier_cap // 当前会话生效 cap,默认 9
|
||||
g_tier_seen_count / g_tier_since // 滞回:观察到的新档 count 与起始时刻
|
||||
g_tier_last_remap / g_tier_remaps // 诊断
|
||||
```
|
||||
|
||||
`count` 取密排头 `g_head.wcount`(或 `inp[32]`),**不用** `active`(离线电机仍占槽报最后一轮,属于“槽预算”)。
|
||||
|
||||
#### 首次进 OP
|
||||
|
||||
- `do_op_start` 仍走完整 `config_init` + `config_map`(CoE 得 Ibytes=600)→ **首拍恒 Tier C**。
|
||||
- 首拍 decode 得到 `count` 后进入滞回观察;不与 SAFEOP→OP 竞态。
|
||||
|
||||
#### 跨档轻量重映射 `do_tier_remap(target_cap)`
|
||||
|
||||
前置:`g_bus_open && g_op && 循环已 loop_stop(调用方持 g_bus_lock)`。
|
||||
|
||||
```
|
||||
1. 记录 DC 形态 (dc_cycle_us, dc_shift_us) —— 与 wedge 恢复同源
|
||||
2. OP → PREOP|ACK(不 INIT、不 config_init 重枚举)
|
||||
- statecheck PREOP ≤400ms;失败则回退完整 do_op_start 一次
|
||||
3. 写 0x0420 = 10000(1s WD,与 do_op_start 同款)
|
||||
4. 改写 SM3:
|
||||
- ec_slave[1].SM[3].SMlength = htoes(target_in_bytes) // StartAddr 仍 0x1880,flags 不动(3-buffer)
|
||||
- ecx_FPWR / ec_FPWRw 写 ESC SM3 寄存器(与 config_map 写 SM 同基址)
|
||||
5. 改写输入 FMMU 逻辑长度(仅输入 FMMU,LogStart 不变,LogLength = target_in_bytes)
|
||||
6. 同步 SOEM 进程数据尺寸:
|
||||
- ec_slave[1].Ibytes / Ibits = target_in_bytes / *8
|
||||
- ec_group[0].Ibytes 调整,保证 send/receive_processdata 帧长 = O+new_I
|
||||
- g_expected_wkc 重算(inputsWKC 对短映射通常仍 1,需实读确认)
|
||||
7. PREOP → SAFEOP|ACK(≤400ms)—— 此时从站 GenerateMapping 读回新 SM3 长度
|
||||
8. 短喂帧:1ms 节拍 pump ~150 次(≈150ms,≪1s WD),再请求 OP
|
||||
9. OP|ACK,继续 pump 直到 AL=OP,总预算 ≤400ms;失败一次则完整 do_op_start
|
||||
10. g_tier_cap = target_cap;g_rxb_layout 保持 dense(3);清空槽表按 do_op_start 会话语义
|
||||
(可选:保留 g_slots,避免 remap 窗口 UI 闪空——v1 与 do_op_start 一致选择清空+重建)
|
||||
```
|
||||
|
||||
**目标耗时:** PREOP/SAFEOP/OP 状态检查各一次成功路径通常 10–50ms,加 150ms 喂帧,合计 **远低于 500ms**;全失败回退完整路径仍可恢复(不保证 <500ms,记日志)。
|
||||
|
||||
**禁止:** NRST、`ec_close`、改邮箱 SM、改输出 SM2 长度。
|
||||
|
||||
#### 滞回与触发点
|
||||
|
||||
- 仅在 **循环运行且 OP** 时评估;放在 `wedge_worker` 100ms 节拍(或 pdo_worker 每 N 拍采样后置标志,由 wedge 线程统一处理——**不得在 pdo_worker 内 loop_stop 自己**)。
|
||||
- 规则:
|
||||
- `need_cap = cap_for_count(count)`
|
||||
- 若 `need_cap == g_tier_cap` → 清观察状态
|
||||
- 若 `need_cap != g_tier_cap`:
|
||||
- 首次记下 `g_tier_seen_count` 与 `g_tier_since`
|
||||
- **同一 need_cap 连续 ≥500ms** 才执行 remap
|
||||
- 期间 count 又回到原档 → 清观察
|
||||
- `pm/hold` 置位期间不 remap(与楔死看门狗一致)。
|
||||
- remap 进行中 `g_tier_busy`:wedge 楔死恢复、`/api/motor/start`、SDO 独占路由互斥(复用 `g_bus_lock`)。
|
||||
|
||||
#### `decode_input` 密排分支
|
||||
|
||||
- 魔数判定不变。
|
||||
- `cap = (ilen - 96) / 56`,非法(不能整除或 cap∉{3,6,9})时:按 cap=9 且 `off+size>ilen` 则 break 的现有保护,**不崩溃**;记 layout 异常。
|
||||
- 段3/段5 偏移用 `xrx_off/diag_off`,替换 `XRX_DENSE_OFF_*` / `DIAG_OFF_*_WIN` 常量中的 544/584(常量改为宏函数或运行时变量)。
|
||||
|
||||
#### API / 观测
|
||||
|
||||
- `/api/motor/state` 或 busKv 增加:`tier_cap`、`tier_in_bytes`、`tier_remaps`、`tier_since`(可选)。
|
||||
- `/api/pdo/tier`:`{"cap":3|6|9}` 手动钉档(立即 remap,不走 500ms 滞回);`{"auto":true}` 恢复自动。v1 至少实现手动钉档读写。
|
||||
- `/api/open` 启动日志:`输入 %dB` 打印真实 `Ibytes`(remap 后),去掉写死「600B」文案。
|
||||
|
||||
#### 楔死恢复 / start / sdo
|
||||
|
||||
- `wedge_worker`、`/api/motor/state` 快路径、`/api/motor/start`:重拉 `do_op_start` 后 **重置 `g_tier_cap=9`**,再允许滞回把档缩回去(简单、可预期)。
|
||||
- `/api/sdo` 仍 `loop_stop` 不恢复——tier 状态在下次 start 时回到 9,可接受。
|
||||
|
||||
### S2.4 前端 / 文档(次要)
|
||||
|
||||
- `lab/spi_test.html`:bus 总线卡展示 `tier in_bytes`(若 API 提供);layout=dense 分支的段5 注释改为“偏移随 cap”。
|
||||
- `lab/pdo_layout.html`:统计卡从「600 B SM3」改为「264/432/600 分档」;**不阻塞固件交付**,可同 PR 或 follow-up。
|
||||
- 注释清理:避免再写“线速两布局同为 600B”这类在 tier 生效后错误的句子。
|
||||
|
||||
### S2.5 错误与回退
|
||||
|
||||
| 情况 | 行为 |
|
||||
|------|------|
|
||||
| SM3 改写 FPWR 失败 | abort remap,保持旧 Ibytes,日志 WARN;下一滞回窗重试 |
|
||||
| SAFEOP 失败(SSC 拒 SM) | 读 ALstatuscode;完整 `do_op_start`(回到 600)并记 `tier_remap_fail` |
|
||||
| OP 失败 | 同上 |
|
||||
| count 突然 > cap(从站 bug/撕裂) | decode 已有越界 break;从站侧 count>cap 截断;**不**立即 remap,等滞回 |
|
||||
| 首拍 Ibytes 仍 600 且 count=3 | 正常进入滞回,500ms 后缩到 264 |
|
||||
| 主站新、从站旧(GenerateMapping 恒 600) | 主站写 SM=264 后 SSC CheckSm 失败 → 回退 600,功能与今日等价 |
|
||||
| 从站新、主站旧 | SM 仍被旧主站写 600,从站跟随 600,与今日等价 |
|
||||
|
||||
### S2.6 不做(Out of Scope 见 S3)但需预留
|
||||
|
||||
- 输出 SM2 分档
|
||||
- 多 TxPDO 赋值(0x1C13 子索引选映射)
|
||||
- EEPROM/ESI 为每档发一份 SII
|
||||
- 改 1-buffer 换更大窗口
|
||||
|
||||
### S2.7 文件级改动清单(Implement 对照表)
|
||||
|
||||
> 行号以 2026-09-12 工作树为准,实现时以符号为准、行号仅作导航。
|
||||
|
||||
#### A. 从站 DCU_APP
|
||||
|
||||
| # | 文件 | 改什么 |
|
||||
|---|------|--------|
|
||||
| A1 | `GD32E508_DCU_APP/comm/ethercat_pdo_size.h` | 新增分档纯宏(C/C++ 共用):`ECAT_PDO_IN_TIERS {264,432,600}`、`ECAT_PDO_TIER_CAPS {3,6,9}`、`ECAT_PDO_IN_PREFIX=40`、`ECAT_PDO_IN_XRX=40`、`ECAT_PDO_IN_DIAG=16`、`ECAT_PDO_IN_BYTES_FOR_CAP(cap)=(96+56*(cap))`、`ECAT_PDO_CAP_FOR_IN_BYTES(ib)`。静态断言:三档均 ≤600 且 `%8==0`,cap=9 时等于现行 `ECAT_PDO_IN_BYTES`。`ECAT_PDO_IN_BYTES` 保留为 **max=600**(数组/EEPROM 上限),语义注释改为「上限,运行期可小于」。 |
|
||||
| A2 | `GD32E508_DCU_APP/comm/ethercat_can_bridge.h` | 段3/段5 **枚举保持 cap=9 数值**(544/584…)作默认/静态断言基准;新增运行时偏移接口:`uint16_t ecatMotXrxOff(uint8_t cap)` / `ecatMotDiagOff(uint8_t cap)` / `ecatPdoInBytesForCap(uint8_t cap)`(inline 或声明+cpp)。`static_assert` 改为只约束 cap=9 与 max 的首尾相接。注释标明:短档时段3/段5 前移,主站必须按 Ibytes 解。 |
|
||||
| A3 | `GD32E508_DCU_APP/comm/ethercat_can_bridge.cpp` `buildInput` @286–333 | 段3 填充:`ECAT_XRX_OFF_*` → `xrx = ecatMotXrxOff(cap)` 相对写。`memset(in,0,kEcatPdoInBytes)` 可改为只清 `in_bytes`(或仍清 600,多清无害但稀疏/diff 会多比——**建议只清 in_bytes**)。`cap` 来源见 A5。段1/段2 偏移不变(0–39 + slots)。 |
|
||||
| A4 | `GD32E508_DCU_APP/comm/ethercat_can_bridge.cpp` `fillMotorRound` @119+ | 逻辑不变;若 `count > cap` 截断到 cap(防越界写到段3)。影子区仍按 max 分配。 |
|
||||
| A5 | `GD32E508_DCU_APP/comm/ssc/port/dc_app.c` `APPL_GenerateMapping` @355–360 | 读 `GetSyncMan(PROCESS_DATA_IN)`(channel=3)`Length`;∈{264,432,600} 则 `*pInputSize=Length`,否则 600。随后调用新增 `EcatPdo_OnPdoInBytes(*pInputSize)`(或写 `EthercatPdo` 静态),通知填充层当前 in_bytes/cap。输出仍 384。 |
|
||||
| A6 | `GD32E508_DCU_APP/comm/ssc/port/dc_app.c` `APPL_InputMapping` @408–453 | `EcatPdo_WriteInputs` 之后:`memcpy(pInputData, …, nPdInputSize)`(替换写死 `ECAT_PDO_IN_BYTES`)。 |
|
||||
| A7 | `GD32E508_DCU_APP/comm/ethercat_pdo.h` / `.cpp` | 新增静态 `s_inBytes`/`s_cap`(默认 600/9)+ `OnPdoInBytes(uint16)`。`fillDiagTail` @114–155:`ECAT_DIAG_OFF_*` → `diag=ecatMotDiagOff(cap)` 相对。`sparseEscWritePlan` @166–194:`ECAT_XRX_OFF_CH`/`kPdoInBytes` → 运行时 `xrx`/`in_bytes`;「撑满」条件改为 `headEnd >= xrx`。`escSeg2WriteLen` @199–216、diff 三区 @258–279:同改。影子数组 `s_escSh2/s_escSh3` **保持按 600 上限分配**,只比较/写 `in_bytes` 内长度。 |
|
||||
| A8 | `GD32E508_DCU_APP/sys/config.h` @152 | `APP_FW_VERSION_PATCH` 33→**34**,追加 1.6.34 注释:SM3 分档跟随主站(GenerateMapping 读回 SM3.Length)。 |
|
||||
| A9 | `GD32E508_DCU_APP/comm/ssc/port/gen_eeprom.py` | **不改** PD_IN_SIZE=600(v1 不刷 EEPROM)。可在文件头加一行注释:「运行期主站可把 SM3 改为 264/432,SII 仍 600」。 |
|
||||
| A10 | `ethercat_diff_stats.cpp` | 若 `kInBytes` 宏绑死 600:改为读 `EthercatPdo::inBytes()` 或仍按 max 快照——**差分仪表在短档下勿拿 600 去比 in_bytes 内容**;实现时优先 `observe` 用当前 in_bytes。 |
|
||||
|
||||
#### B. 主站 tools/ecat
|
||||
|
||||
| # | 文件 | 改什么 |
|
||||
|---|------|--------|
|
||||
| B1 | `ecat_native.c` 顶部常量 @70–133 | 新增:`TIER_IN_BYTES[]`、`tier_cap_for_count(n)`、`tier_in_bytes(cap)`、`dense_xrx_off(ilen)`、`dense_diag_off(ilen)`(`cap=(ilen-96)/56`)。**保留** `XRX_DENSE_OFF_*`/`DIAG_OFF_*_WIN` 仅作文档/默认 cap=9;decode 路径改走函数。 |
|
||||
| B2 | `ecat_native.c` 状态 @190–226 附近 | 新增:`g_tier_cap`(默认9)、`g_tier_auto`(默认1)、`g_tier_busy`、`g_tier_need_cap`、`g_tier_since`、`g_tier_remaps`、`g_tier_remap_fail`。 |
|
||||
| B3 | `ecat_native.c` `decode_input` @450–530 | 密排分支:`xch/xda/diag` 用 `dense_*_off(ilen)`。若 `(ilen-96)%56!=0` 或 cap∉{3,6,9}:仍按魔数密排处理但 `off+size>ilen` 则 break(已有),并置 layout 异常标志。段5 读取位置同步改(@559–560 现用 `DIAG_OFF_LOAD_WIN`)。 |
|
||||
| B4 | `ecat_native.c` 新函数 `do_tier_remap(int target_cap, …)` | 放在 `do_op_start`/`do_op_stop` 附近。实现 S2.3 十步;内部用 `ecx_FPWR`/`ec_FPWRw`:SM3 长度字 @ ESC `0x081A`(`ECT_REG_SM0+3*8+2`),整段 SM 重写可抄 `ethercatconfig.c` `ecx_map_sm` 的 FPWR 模式;输入 FMMU:读当前 FMMU 表,只改输入那条的 `LogLength=target_in`,FPWR 回去;`ec_slave[1].Ibytes=Ibits/8`、`ec_group[0].Ibytes` 收缩/回扩;`g_expected_wkc` 重算。成功:`g_tier_cap=target`;失败:`do_op_start` 回落并 `g_tier_cap=9`。 |
|
||||
| B5 | `ecat_native.c` `wedge_worker` @3057–3100 | 在 `need` 楔死分支**之前**增加 tier 滞回评估(仅当 `g_loop_running && g_op && !g_tier_busy && !pm_hold && g_tier_auto`):用最新 `g_head.wcount`;同一 `need_cap≠g_tier_cap` 持续 ≥0.5s → `loop_stop` + `do_tier_remap` + `loop_start`。楔死恢复成功路径:`g_tier_cap=9`、清观察。 |
|
||||
| B6 | `ecat_native.c` `do_op_start` 成功出口 @1178–1207 | `g_tier_cap=9`、清滞回观察。日志 `输入 %dB` 已打真实 Ibytes(@1180)——确认 remap 后再 start 也正确。 |
|
||||
| B7 | `ecat_native.c` `/api/motor/start` @1987–2046 | 成功 `loop_start` 后同样 `g_tier_cap=9`(若走的是未 remap 的 do_op_start)。日志去掉写死「输入 600B」文案 @2039。 |
|
||||
| B8 | `ecat_native.c` `/api/motor/state` @2365–2403 | 快路径楔死恢复成功后 `g_tier_cap=9`。`build_state_json` 增加 `tier_cap`/`tier_in_bytes`/`tier_remaps`(函数体搜 `build_state_json`)。 |
|
||||
| B9 | `ecat_native.c` 新路由 `/api/pdo/tier` | GET:返回 `{ok, auto, tier_cap, tier_in_bytes, remaps, last_ms}`。POST body `{"cap":3\|6\|9}` 或 `{"auto":false\|true}`:钉档时持 `g_bus_lock`、`loop_stop`、`do_tier_remap`、`loop_start`;`auto` 只改标志。 |
|
||||
| B10 | `ecat_native.c` `pdo_worker` @848–852 | 无需改 Ibytes 来源(已读 `ec_slave[1].Ibytes`)。**禁止**在此 remap。可选:每 N 拍把 `g_head.wcount` 时间戳写给 wedge(直接读 `g_head` 已无锁跨线程——与现状一致,保持)。 |
|
||||
| B11 | `build_new.bat` / 工程 | 无新源文件则不用改;若 `do_tier_remap` 拆到新 `.c` 再加入 bat。 |
|
||||
|
||||
#### C. 前端 tools/ESP32(不阻塞固件)
|
||||
|
||||
| # | 文件 | 改什么 |
|
||||
|---|------|--------|
|
||||
| C1 | `lab/spi_test.html` | 总线/KV 区:若 API 返回 `tier_in_bytes` 则显示「SM3 264/432/600 · 档」。dense 段5 注释 @1198 改为「偏移随 cap」。CSS/JS 引用若外置需 `?v=`(本页自包含则内联即可)。 |
|
||||
| C2 | `lab/pdo_layout.html` | 统计卡 @243–244、图例 @819/@841、说明 @264:由「600 B SM3」改为分档说明;图可标三档长度。 |
|
||||
| C3 | `ECAT_BEAT_TAIL_REPORT.html` 等历史报告 | **不改**(已归档结论);避免改写历史数字。 |
|
||||
|
||||
#### D. 明确不改
|
||||
|
||||
- `ESP32_COMM/**`
|
||||
- `gen_eeprom.py` 的 PD_IN_SIZE、`DCU_ESI.xml`
|
||||
- `soem-src/**`(只调用,不改库;FMMU/SM 用现成 FPWR)
|
||||
- 输出 SM2、邮箱 SM0/1
|
||||
|
||||
#### E. 实现顺序(文件依赖)
|
||||
|
||||
```
|
||||
A1 → A2 → A5/A6(GenerateMapping+InputMapping)
|
||||
→ A3/A4/A7(填充与稀疏/差分,依赖 A5 通知 in_bytes)
|
||||
→ A8 版本
|
||||
B1 → B3 decode → B4 remap → B5/B6/B7 滞回与重置 → B8/B9 API
|
||||
C1/C2 可后置
|
||||
```
|
||||
|
||||
双端可并行:A 与 B 仅共享数值表;联调以 V3–V8 为准。
|
||||
|
||||
---
|
||||
|
||||
## [S3] Out of Scope
|
||||
|
||||
- ESP32_COMM 固件(不解析 EtherCAT PDO)
|
||||
- 输出 PDO 384B 收缩
|
||||
- 刷 EEPROM / 冷启动依赖的 SII 多映射
|
||||
- 电机 10+ 节点
|
||||
- 用 tier 直接调 PDO 周期(0.25ms 等)
|
||||
- 提交/合并(用户明确不自动 commit)
|
||||
|
||||
---
|
||||
|
||||
## Tasks
|
||||
|
||||
- [ ] T1: A1+A2+A5+A6 从站长度宏 + GenerateMapping 跟随 SM3 + InputMapping 按 nPdInputSize —— acceptance: 静态断言 264/432/600;主站手写 SM3=264 后 PREOP→SAFEOP 不报 0x001A/INVALIDSMINCFG;`nPdInputSize` 变 264 (covers: S2.2 S2.7-A)
|
||||
- [ ] T2: A3+A4+A7 从站密排/稀疏/差分偏移 cap 相对 —— acceptance: 264B SM 下段3/段5 不越界,主站能读 load/torn;count≤3 稀疏写两段正确 (covers: S2.2 S2.7-A; depends: T1)
|
||||
- [ ] T3: A8 版本 1.6.34 —— acceptance: PATCH=34 + 注释 (covers: S2.2; depends: T1)
|
||||
- [ ] T4: B1+B3 主站 decode cap 相对 —— acceptance: ilen=264+magic 槽/段3/段5 正确;ilen=600 回归一致 (covers: S2.1 S2.3 S2.7-B)
|
||||
- [ ] T5: B4 do_tier_remap + Ibytes/FMMU/group —— acceptance: 600→264→600 各一次 AL=OP、wkc 正、成功路径日志 <500ms (covers: S2.3; depends: T4)
|
||||
- [ ] T6: B5+B6+B7 滞回 + start/wedge 重置 —— acceptance: 稳定 3 电机 0.5–1.0s 进 264;4 电机进 432;楔死后回 9 再缩回 (covers: S2.3; depends: T5)
|
||||
- [ ] T7: B8+B9 `/api/pdo/tier` 与 state 字段 —— acceptance: POST cap 立即 remap;GET 含 tier_* (covers: S2.3; depends: T5)
|
||||
- [ ] T8: V1–V10 双端编译+台架 —— acceptance: cbuild/build_native 通过;见 Verification (covers: S2.2 S2.3; depends: T1–T7)
|
||||
- [ ] T9: C1+C2 前端分档展示 —— acceptance: 页面显示当前档;不阻塞 T8 (covers: S2.4; depends: T7)
|
||||
|
||||
---
|
||||
|
||||
## Verification(交付证据)
|
||||
|
||||
| 步骤 | 命令/操作 | 期望 |
|
||||
|------|-----------|------|
|
||||
| V1 | DCU cbuild(既有 Release+GD32E508VE) | 0 error |
|
||||
| V2 | `build_new.bat` / VS2022 cl 原生主站 | 产出 `ecat_native.new.exe` |
|
||||
| V3 | 冷启动 600 进 OP(3 电机) | 与今日一致,layout=dense |
|
||||
| V4 | 等待 ≥1s | 自动 remap 到 264,日志含耗时 |
|
||||
| V5 | `/api/motor/state` + spi_test | Ibytes/tier=264,count=3,wkc 稳定 |
|
||||
| V6 | rx 线时(粗) | 过程数据字节 648 vs 984,rx 段应下降(允许 ±15µs 噪声) |
|
||||
| V7 | 手动 `/api/pdo/tier` cap=9 | 回到 600 |
|
||||
| V8 | 拔/加第 4 台(或模拟 count) | 滞回后 432 |
|
||||
| V9 | 旧从站 + 新主站 | SAFEOP 失败路径回 600,总线仍可用 |
|
||||
| V10 | 楔死/PM hold | hold 中不 remap;恢复后 tier 策略符合 S2.3 |
|
||||
|
||||
---
|
||||
|
||||
## Journey log(设计期)
|
||||
|
||||
1. 线时收益来自 **SM3 变短**,稀疏写不够——与 1.6.22/1.6.25 结论一致。
|
||||
2. SSC 长度双源校验 ⇒ 必须主从同数;v1 选「主站写 SM,从站 GenerateMapping 读回」,避免 EEPROM 与多 PDO 映射。
|
||||
3. 首拍恒 600 是为了兼容 config_map/CoE,用 500ms 滞回消化一次 remap,比改 CoE 映射表风险低。
|
||||
4. 重映射不能走完整 `do_op_start`(重枚举+长喂帧)——独立轻量路径 + 失败回退完整路径。
|
||||
5. 段3/段5 偏移写死是主站解码的隐雷;Ibytes 分支(ilen<600→slim)会把 264 误判成瘦身布局——必须先用魔数 0xA5 拦下,再按 cap 排偏移。
|
||||
@@ -0,0 +1,81 @@
|
||||
---
|
||||
feature: warm-jump-ecat-probe
|
||||
status: delivered
|
||||
updated: 2026-09-16
|
||||
branch: master
|
||||
commits: 7d9d9c7..uncommitted
|
||||
---
|
||||
|
||||
# Warm Jump 后 EtherCAT 起板探针修复
|
||||
|
||||
## Report
|
||||
|
||||
**What was built** — APP 1.6.40:`EscLan9252::init` 起板探针放行门从
|
||||
`HBI_IDX_0==0x1234` 改为 `BYTE_TEST==0x87654321`。warm jump 残留的
|
||||
`HBI_IDX_0=0x0054` 只打 WARN 并放行;BYTE_TEST 不过才补一发 RST# 重探一轮。
|
||||
cold 路径行为不变(仍等芯片出复位到 0x1234 再测 BYTE_TEST)。
|
||||
|
||||
**Verification** — 无 ARM 交叉编译环境;静态审查通过(spec 合规 / 正确性 /
|
||||
代码风格),reviewer 无 critical。上板验收:ECAT 刷完 warm jump 后串口应见
|
||||
`[ECAT][ESC] init: OK`(可带 `WARN idx0=0x0054`),`[APP] init` 不再报
|
||||
`skipped/failed: ethercat`,主站可建连;reset 路径回归不破坏。
|
||||
|
||||
**Journey log**
|
||||
- BOOT 1.1.0.18 短路 `escResetForJump` 是刻意实验;APP 侧旧探针把「复位默认值」
|
||||
当成通路判据,在 warm 残值上必然假阴性。
|
||||
- 主工作区曾有一份未完成的 1.6.40 草稿;本任务收口并跳过 warm 残值的 300ms 空等。
|
||||
- 用户选了隔离 worktree,但本环境拦截 `git worktree add`,改动落在主工作区未提交 diff。
|
||||
|
||||
## [S1] Problem
|
||||
|
||||
刷完固件后 BOOT(1.1.0.18,跳转前 `escResetForJump()` 已短路)warm jump 进 APP:
|
||||
APP banner 正常,但 EtherCAT 不可用(自检降级 / 主站无法建连)。按复位键走 cold
|
||||
jump 后 EtherCAT 恢复。同一镜像、同一 APP 启动链,差异只在进入 APP 前 ESC 的
|
||||
HBI 索引寄存器状态。
|
||||
|
||||
## [S2] Design
|
||||
|
||||
### 根因
|
||||
|
||||
1. BOOT `bootEcatDeinit()` 最后一条 `writeReg32(IRQ_CFG)` 把 LAN9252 的
|
||||
`HBI_IDX_0` 留在 **0x0054**(索引模式残值,DS 9.5.4.2 复位默认才是 0x1234)。
|
||||
2. 跳转前硬复位在 BOOT 1.1.0.18 被刻意短路(1.1.0.13/14 实测那发复位会打死
|
||||
warm jump),warm 路径芯片 **不会** 回到 POR 默认态。
|
||||
3. APP `EscLan9252::init()` 旧放行门是 `HBI_IDX_0==0x1234`。该值只证明
|
||||
「芯片刚出过复位」,证明不了 HBI 通路。warm 路径上探针必然假阴性 →
|
||||
`s_busReady=false` → SSC 跳过 → 降级 ethercat。
|
||||
4. cold 路径芯片自带 POR,`IDX_0` 天然 0x1234,与 PB10 那发复位无关 ——
|
||||
这正是「reset 能好、刷完不行」的分界。
|
||||
|
||||
### 修复契约
|
||||
|
||||
- **BYTE_TEST(0x064==0x87654321)是唯一物理通路门**。`readReg32` 每次访问前
|
||||
都 `setHbiIndex(addr)`,完整「索引写 + 数据低/高字读」往返,与复位态无关。
|
||||
- `HBI_IDX_0` 降级为诊断读数:异常时打 WARN 并放行,不再导致 `return false`。
|
||||
- 任一判据不过 → 补一发 `hardwareReset()` 再探一轮(此刻 SSC 未 MainInit,
|
||||
二次复位零配置代价)。两轮仍不通 = 总线物理不通,维持降级。
|
||||
- `s_busReady` 语义同步:EXMC + BYTE_TEST 通过即 true(与 `initial.cpp` 的
|
||||
`isBusReady()&&isCoreReady()` 门控一致)。
|
||||
|
||||
### 不做的事
|
||||
|
||||
- 不改 BOOT(1.1.0.18 保持 RST# 短路);APP 侧已能兜住 warm 残值。
|
||||
- 不把 `HBI_IDX_0==0x1234` 恢复为硬门。
|
||||
- 不动 PDO/SII/主站解码。
|
||||
|
||||
## [S3] Out of Scope
|
||||
|
||||
- BOOT `bootEcatDeinit` 的 PD/PE 浮空注释错误、`escResetForJump` 时序重设计。
|
||||
- PHY 链路重协商窗口与主站 probe 重试(另见 `ecat_boot_handoff_link_issue.md`)。
|
||||
- 1.6.35~38 启动链实验回退项(已在 1.6.39 完成)。
|
||||
|
||||
## Tasks
|
||||
|
||||
- [x] T1: 改 `EscLan9252::init` 探针 — 放行门改为 BYTE_TEST,IDX 残值 WARN 放行,失败补一发 RST# 重探 — acceptance: 代码与 [S2] 契约一致 (covers: S2)
|
||||
- [x] T2: 同步 `esc_lan9252.h` 的 `isBusReady` 文档语义 — acceptance: 注释与实现一致 (covers: S2; depends: T1)
|
||||
- [x] T3: `config.h` 版本 bump 1.6.40 + 来由注释 — acceptance: PATCH=40 且注释写明 warm-jump 根因 (covers: S2)
|
||||
- [x] T4: 逻辑核对探针与调用方 — acceptance: 无 ARM 交叉编译环境,完成静态审查;`initial.cpp` 仍用 `isBusReady()&&isCoreReady()` 门控,语义兼容 (covers: S2; depends: T1)
|
||||
|
||||
### 工作区说明
|
||||
|
||||
用户选择隔离 worktree,但本环境拦截了 `git worktree add`(共享 ref store 保护)。改动落在主工作区未提交 diff,分支意图为 `fix/warm-jump-ecat-probe`。验收通过后需用户确认是否提交到 master。
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user