feat(ethercat): 添加EtherCAT从站功能支持并优化同步机制

- 添加CAN反馈和ID快照SPI命令文件
- 移除EXMC组件依赖,精简项目配置
- 添加RAM函数段链接脚本,优化Flash等待周期抖动
- 实现EscExmcBus类,提供16位并行总线访问LAN9252
- 实现EscLan9252驱动,支持寄存器和PDI内存访问
- 实现EthercatSlave类,采用三缓冲机制进行PDO数据交换
- 实现EthercatSync同步管理,支持SYNC0/SYNC1中断和定时器模式
- 添加ISR执行时间统计和诊断功能
- 分离周期路径和事件路径通知机制,避免RMW竞态
This commit is contained in:
cbb
2026-06-29 01:51:02 +08:00
parent 1612e326ee
commit b171767023
31 changed files with 2177 additions and 290 deletions
@@ -1,4 +1,4 @@
# A project translates into one executable or library.
# A project translates into one executable or library.
project:
groups:
@@ -44,6 +44,8 @@ project:
- file: ./device/spi/app_spi_cmd_ota_wait.cpp
- file: ./device/spi/app_spi_cmd_ping.cpp
- file: ./device/spi/app_spi_cmd_can_bridge.cpp
- file: ./device/spi/app_spi_cmd_can_feedback.cpp
- file: ./device/spi/app_spi_cmd_id_snapshot.cpp
- group: comm
files:
- file: ./comm/esc_exmc_bus.h
@@ -65,7 +67,6 @@ project:
- component: Device:GD32E50x_StdPeripherals:CRC
- component: Device:GD32E50x_StdPeripherals:DBG
- component: Device:GD32E50x_StdPeripherals:DMA
- component: Device:GD32E50x_StdPeripherals:EXMC
- component: Device:GD32E50x_StdPeripherals:EXTI
- component: Device:GD32E50x_StdPeripherals:FMC
- component: Device:GD32E50x_StdPeripherals:GPIO
@@ -34,7 +34,11 @@ LR_ROM0 __ROM0_BASE __ROM0_SIZE {
ER_ROM0 __ROM0_BASE __ROM0_SIZE {
*.o (RESET, +First)
*(InRoot$$Sections)
*(+RO +XO)
; 用 .ANY (+RO +XO) 兜底所有 RO 段,而非 *(+RO +XO) 属性通配符。
; 原因:*(+RO) 与 RW_RAM0 的 *(.ramfunc*) 跨执行区会产生 L6223E 歧义;
; 而 .ANY 优先级低于 *(.ramfunc*),armlink 先把 .ramfunc 分配给 RW_RAM0,
; 剩余 RO 段再由 .ANY 兜底到 ER_ROM0,既无歧义又零丢弃(不依赖段名枚举)。
.ANY (+RO +XO)
}
#if defined (__ARM_FEATURE_CMSE) && (__ARM_FEATURE_CMSE == 3U)
@@ -49,6 +53,9 @@ LR_ROM0 __ROM0_BASE __ROM0_SIZE {
}
RW_RAM0 AlignExpr(+0, 8) (__RAM0_SIZE - __HEAP_SIZE - __STACK_SIZE - __STACKSEAL_SIZE - AlignExpr(ImageLength(RW_NOINIT), 8)) {
; RAM 函数段:VMA 在 SRAM、LMA 在 Flash,由 C 库 __main scatter-load 自动从 Flash 拷贝到 RAM。
; 用于 EtherCAT ISR 调用链(periodicIsr/readPdi/writePdi/read16/write16),消除 Flash 等待周期抖动。
*(.ramfunc*)
*(+RW +ZI)
}
+3 -19
View File
@@ -5,8 +5,6 @@
#include "comm/esc_exmc_bus.h"
#include "sys/config.h"
#if CONFIG_ETHERCAT_ENABLE
extern "C" {
#include "gd32e50x_rcu.h"
#include "gd32e50x_gpio.h"
@@ -68,32 +66,18 @@ void EscExmcBus::init()
exmc_norsram_init(&norsram_init);
exmc_norsram_enable(CONFIG_ESC_EXMC_BANK_NOR_SRAM_REGION1);
#else
/* EXMC 外设库未安装:仅占位,CONFIG_ETHERCAT_ENABLE=1 时需先安装 EXMC 组件。 */
/* EXMC 外设库未安装:仅占位,启用 EtherCAT 时需先安装 EXMC 组件。 */
#endif
}
__attribute__((section(".ramfunc"), noinline))
uint16_t EscExmcBus::read16(uint32_t offset)
{
return ESC_EXMC_REG16(offset);
}
__attribute__((section(".ramfunc"), noinline))
void EscExmcBus::write16(uint32_t offset, uint16_t data)
{
ESC_EXMC_REG16(offset) = data;
}
void EscExmcBus::readBlock(uint32_t offset, uint16_t *buf, size_t count)
{
for (size_t i = 0U; i < count; ++i) {
buf[i] = ESC_EXMC_REG16(offset + (uint32_t)(i * 2U));
}
}
void EscExmcBus::writeBlock(uint32_t offset, const uint16_t *buf, size_t count)
{
for (size_t i = 0U; i < count; ++i) {
ESC_EXMC_REG16(offset + (uint32_t)(i * 2U)) = buf[i];
}
}
#endif /* CONFIG_ETHERCAT_ENABLE */
+2 -26
View File
@@ -19,35 +19,11 @@ public:
/** @brief 初始化 EXMC NOR/SRAM Region1 与全部 GPIO(AF 复用)。 */
static void init();
/**
* @brief 从指定偏移读取 16-bit 数据。
* @param[in] offset 地址偏移(字节)。
* @return uint16_t 读取的 16-bit 数据。
*/
/** @brief ISR-only:仅供 preempt=0 调用,无锁。 */
static uint16_t read16(uint32_t offset);
/**
* @brief 向指定偏移写入 16-bit 数据。
* @param[in] offset 地址偏移(字节)。
* @param[in] data 待写入的 16-bit 数据。
*/
/** @brief ISR-only:仅供 preempt=0 调用,无锁。 */
static void write16(uint32_t offset, uint16_t data);
/**
* @brief 从指定偏移连续读取一块 16-bit 数据。
* @param[in] offset 地址偏移(字节)。
* @param[out] buf 输出缓冲(以 uint16_t 为单位)。
* @param[in] count 读取的 16-bit 半字数量。
*/
static void readBlock(uint32_t offset, uint16_t *buf, size_t count);
/**
* @brief 向指定偏移连续写入一块 16-bit 数据。
* @param[in] offset 地址偏移(字节)。
* @param[in] buf 数据缓冲(以 uint16_t 为单位)。
* @param[in] count 写入的 16-bit 半字数量。
*/
static void writeBlock(uint32_t offset, const uint16_t *buf, size_t count);
};
#endif /* COMM_ESC_EXMC_BUS_H */
+70 -8
View File
@@ -5,8 +5,8 @@
#include "comm/esc_lan9252.h"
#include "comm/esc_exmc_bus.h"
#include "sys/config.h"
#if CONFIG_ETHERCAT_ENABLE
#include "RTE_Components.h"
#include CMSIS_device_header
namespace {
/* LAN9252 ESC 关键寄存器地址(字节偏移,EXMC 访问) */
@@ -20,16 +20,55 @@ constexpr uint16_t LAN9252_PDI_BASE_OFFSET = 0x1000U;
void EscLan9252::init()
{
/* 使能 DWT 周期计数器,供 periodicIsr 执行时间测量 */
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
DWT->CYCCNT = 0U;
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
EscExmcBus::init();
/* TODO: 配置 ESC 中断使能、AL 控制等基本寄存器(留待协议栈集成阶段实现) */
/* ============================================================
* TODO: 完整 ESC 寄存器配置(需 SSC 或等价协议栈代码,本 spec 不实现)
* ============================================================
* 当前仅完成 EXMC 总线初始化与芯片 ID 读取。要使 SYNC0 实际触发,还需配置:
*
* 1. SyncManager(SM)初始化:
* - SM0/SM1:邮箱输入/输出(IN/OUT),地址 0x0520-0x0530
* - SM2:TxPDO(输入),地址 0x0530,配置为 3-buffer 模式
* - SM3:RxPDO(输出),地址 0x0540,配置为 3-buffer 模式
* - 各 SM 的 StartAddr/Length/Control/Status/Activate 寄存器
*
* 2. FMMU(Fieldbus Memory Mapping Unit)配置:
* - FMMU0:TxPDO 映射(逻辑地址 → 物理地址,读)
* - FMMU1:RxPDO 映射(逻辑地址 → 物理地址,写)
* - 地址 0x0580-0x0590
*
* 3. AL Control(0x0120):写入 0x08 请求 OP 状态(需主站配合完成状态迁移)
*
* 4. SYNC0 生成寄存器:
* - 0x0981-0x0982:SYNC0 周期(cycle time,单位 ns)
* - 0x0983:SYNC0 脉冲长度
* - 0x0984:SYNC0 控制(使能/触发模式)
* - 需主站 DC 同步后由 ESC 自动生成 SYNC0 脉冲
*
* 这些步骤通常由 EtherCAT SSC(Slave Stack Code)或等价协议栈代码完成,
* 涉及 AL 状态机管理、邮箱通信、PDO 映射协商等复杂逻辑。
* 本 spec 仅通过 EthercatSlave::isEscReady() 的 AL 状态守卫使
* "SYNC0 不会触发"在运行期可观测,完整配置留待 SSC 集成 spec。
* ============================================================ */
}
bool EscLan9252::selfTest()
{
const uint16_t id_low = readReg(LAN9252_REG_CHIP_ID_L);
const uint16_t id_high = readReg(LAN9252_REG_CHIP_ID_H);
const uint32_t chip_id = ((uint32_t)id_high << 16) | id_low;
return (chip_id == LAN9252_CHIP_ID_VALUE);
return (readChipId() == LAN9252_CHIP_ID_VALUE);
}
uint32_t EscLan9252::readChipId()
{
/* INIT-ONLY:init 阶段中断未使能,但仍走 initOnly 路径保持一致性 */
const uint16_t id_low = initOnlyReadReg(LAN9252_REG_CHIP_ID_L);
const uint16_t id_high = initOnlyReadReg(LAN9252_REG_CHIP_ID_H);
return ((uint32_t)id_high << 16) | id_low;
}
uint16_t EscLan9252::readReg(uint16_t addr)
@@ -43,6 +82,7 @@ void EscLan9252::writeReg(uint16_t addr, uint16_t val)
EscExmcBus::write16((uint32_t)addr << 1U, val);
}
__attribute__((section(".ramfunc"), noinline))
void EscLan9252::readPdi(uint16_t offset, uint8_t *buf, size_t len)
{
if (buf == nullptr || len == 0U) {
@@ -63,6 +103,7 @@ void EscLan9252::readPdi(uint16_t offset, uint8_t *buf, size_t len)
}
}
__attribute__((section(".ramfunc"), noinline))
void EscLan9252::writePdi(uint16_t offset, const uint8_t *buf, size_t len)
{
if (buf == nullptr || len == 0U) {
@@ -79,4 +120,25 @@ void EscLan9252::writePdi(uint16_t offset, const uint8_t *buf, size_t len)
}
}
#endif /* CONFIG_ETHERCAT_ENABLE */
/* ---- initOnly* 接口:INIT-ONLY,仅允许在 EthercatSync::init() 使能 EXTI NVIC 之前调用 ----
* 运行期禁止调用;需 ESC 寄存器访问须通过 SYNC0 ISR 代访队列(本 spec 不涵盖)。
* 注:不再使用 PRIMASK 关中断(违反项目硬约束"Critical sections MUST use BASEPRI")。 */
uint16_t EscLan9252::initOnlyReadReg(uint16_t addr)
{
return readReg(addr);
}
void EscLan9252::initOnlyWriteReg(uint16_t addr, uint16_t val)
{
writeReg(addr, val);
}
void EscLan9252::initOnlyReadPdi(uint16_t offset, uint8_t *buf, size_t len)
{
readPdi(offset, buf, len);
}
void EscLan9252::initOnlyWritePdi(uint16_t offset, const uint8_t *buf, size_t len)
{
writePdi(offset, buf, len);
}
+26 -20
View File
@@ -2,6 +2,12 @@
* @file esc_lan9252.h
* @brief LAN9252 EtherCAT 从站控制器(ESC)驱动接口。
* @note 通过 EscExmcBus 16-bit 并行总线访问 LAN9252 寄存器与 PDI 内存。
*
* 接口调用约束(避免 EXMC 总线争用):
* - readReg/writeReg/readPdi/writePdi:ISR-only,仅供 preempt=0 的 periodicIsr() 调用,无锁。
* - initOnlyReadReg/initOnlyWriteReg/initOnlyReadPdi/initOnlyWritePdi:INIT-ONLY,仅允许在 EthercatSync::init() 使能 EXTI NVIC 之前调用;运行期禁止调用,需 ESC 寄存器访问须通过 SYNC0 ISR 代访队列(本 spec 不涵盖)。
*
* PDI offset 语义:offset 参数为 SyncManager 内存区相对偏移,输入 PDO(TxPDO)与输出 PDO(RxPDO)必须使用不同 offset,参见 CONFIG_ESC_PDI_INPUT_OFFSET / CONFIG_ESC_PDI_OUTPUT_OFFSET。
*/
#ifndef COMM_ESC_LAN9252_H
#define COMM_ESC_LAN9252_H
@@ -25,34 +31,34 @@ public:
static bool selfTest();
/**
* @brief 读取 ESC 寄存器(16-bit)。
* @param[in] addr 寄存器地址(ESC 寄存器空间偏移)。
* @return uint16_t 寄存器值。
* @brief 读取 ESC 芯片 ID(INIT-ONLY,供 init 打印诊断用)。
* @return uint32_t 32-bit 芯片 ID(期望值 0x87654321)。
*/
static uint32_t readChipId();
/** @brief ISR-only:仅供 preempt=0 的 periodicIsr() 调用,无锁。主循环必须用 initOnly 版本。 */
static uint16_t readReg(uint16_t addr);
/**
* @brief 写入 ESC 寄存器(16-bit)。
* @param[in] addr 寄存器地址。
* @param[in] val 待写入值。
*/
/** @brief ISR-only:仅供 preempt=0 的 periodicIsr() 调用,无锁。主循环必须用 initOnly 版本。 */
static void writeReg(uint16_t addr, uint16_t val);
/**
* @brief 从 PDI 内存读取一块数据。
* @param[in] offset PDI 偏移(字节)。
* @param[out] buf 输出缓冲。
* @param[in] len 读取字节数。
*/
/** @brief ISR-only:仅供 preempt=0 的 periodicIsr() 调用,无锁。主循环必须用 initOnly 版本。 */
static void readPdi(uint16_t offset, uint8_t *buf, size_t len);
/**
* @brief 向 PDI 内存写入一块数据。
* @param[in] offset PDI 偏移(字节)。
* @param[in] buf 数据缓冲。
* @param[in] len 写入字节数。
*/
/** @brief ISR-only:仅供 preempt=0 的 periodicIsr() 调用,无锁。主循环必须用 initOnly 版本。 */
static void writePdi(uint16_t offset, const uint8_t *buf, size_t len);
/** @brief INIT-ONLY:仅允许在 EthercatSync::init() 使能 EXTI NVIC 之前调用;运行期禁止调用,需 ESC 寄存器访问须通过 SYNC0 ISR 代访队列(本 spec 不涵盖)。 */
static uint16_t initOnlyReadReg(uint16_t addr);
/** @brief INIT-ONLY:仅允许在 EthercatSync::init() 使能 EXTI NVIC 之前调用;运行期禁止调用,需 ESC 寄存器访问须通过 SYNC0 ISR 代访队列(本 spec 不涵盖)。 */
static void initOnlyWriteReg(uint16_t addr, uint16_t val);
/** @brief INIT-ONLY:仅允许在 EthercatSync::init() 使能 EXTI NVIC 之前调用;运行期禁止调用,需 ESC 寄存器访问须通过 SYNC0 ISR 代访队列(本 spec 不涵盖)。 */
static void initOnlyReadPdi(uint16_t offset, uint8_t *buf, size_t len);
/** @brief INIT-ONLY:仅允许在 EthercatSync::init() 使能 EXTI NVIC 之前调用;运行期禁止调用,需 ESC 寄存器访问须通过 SYNC0 ISR 代访队列(本 spec 不涵盖)。 */
static void initOnlyWritePdi(uint16_t offset, const uint8_t *buf, size_t len);
};
#endif /* COMM_ESC_LAN9252_H */
+112 -29
View File
@@ -8,46 +8,92 @@
#include "sys/task_manager.h"
#include "main.h"
#if CONFIG_ETHERCAT_ENABLE
PdoBuffer EthercatSlave::s_inputs[3] = {};
PdoBuffer EthercatSlave::s_outputs[3] = {};
PdoBuffer EthercatSlave::s_appInputs = {};
PdoBuffer EthercatSlave::s_appOutputs = {};
volatile uint8_t EthercatSlave::s_inputWriteIdx = 0U;
volatile uint8_t EthercatSlave::s_outputReadIdx = 0U;
volatile uint16_t EthercatSlave::s_periodicSeq = 0U;
uint16_t EthercatSlave::s_processedSeq = 0U;
volatile uint32_t EthercatSlave::s_isrMaxCycles = 0U;
volatile uint32_t EthercatSlave::s_isrAvgCycles = 0U;
volatile uint32_t EthercatSlave::s_isrCount = 0U;
bool EthercatSlave::s_escReady = false;
uint32_t EthercatSlave::s_chipId = 0U;
uint8_t EthercatSlave::s_alState = 0U;
PdoBuffer EthercatSlave::s_inputs[2] = {};
PdoBuffer EthercatSlave::s_outputs[2] = {};
volatile uint8_t EthercatSlave::s_inputReadIndex = 0U;
volatile uint8_t EthercatSlave::s_outputWriteIndex = 0U;
/* ISR 执行时间评估:
* - 若 DWT 实测 max > CONFIG_ETHERCAT_ISR_MAX_US_THRESHOLD(5µs),需评估:
* a) 减小 CONFIG_ETHERCAT_PDO_SIZE(默认 32 字节)
* b) 优化 EXMC 时序参数(esc_exmc_bus.cpp 中 read_latency/write_latency)
* - 若 max ≤ 5µs,接受现状;preempt=0 阻塞窗口 ≤ 5µs,CAN/SPI 中断需容忍此窗口。
* SPI0 从机在此窗口内不会 RX 溢出(假设 SPI 时钟 ≤ 10MHz,5µs ≥ 50 bit,远小于一帧)。 */
void EthercatSlave::init()
{
EscLan9252::init();
if (EscLan9252::selfTest()) {
/* ESC 自检通过:TODO 启用周期同步 */
/* 第 1 层调试可观测性:读取芯片 ID 并打印 selfTest 结果 */
s_chipId = EscLan9252::readChipId();
const bool chipIdOk = (s_chipId == 0x87654321U);
/* AL 状态守卫:仅在芯片 ID 匹配时读取 AL 状态(ID 不匹配时 EXMC 可能读回 0/0xFFFF,无意义)。
* AL ≥ SAFEOP(0x04) 才认为 SYNC0 可触发;完整 ESC 配置(SM/FMMU/AL Control/SYNC0)需 SSC 集成。 */
s_alState = 0U;
if (chipIdOk) {
s_alState = (uint8_t)(EscLan9252::initOnlyReadReg(0x0130U) & 0x000FU);
}
s_inputReadIndex = 0U;
s_outputWriteIndex = 0U;
s_escReady = chipIdOk && (s_alState >= AL_STATE_SAFEOP);
if (!chipIdOk) {
printf("[ECAT] ESC chip id=0x%08lX selfTest=FAIL\r\n", (unsigned long)s_chipId);
printf("[ECAT] ESC NOT ready, sync ISR disabled (check EXMC wiring/timing)\r\n");
} else if (s_alState < AL_STATE_SAFEOP) {
printf("[ECAT] ESC chip id=0x%08lX selfTest=PASS, but AL state=0x%02X < SAFEOP(0x%02X)\r\n",
(unsigned long)s_chipId, (unsigned)s_alState, (unsigned)AL_STATE_SAFEOP);
printf("[ECAT] SYNC0 will not fire, full ESC config requires SSC integration\r\n");
} else {
printf("[ECAT] ESC ready, chip id=0x%08lX, AL state=0x%02X\r\n",
(unsigned long)s_chipId, (unsigned)s_alState);
}
s_inputWriteIdx = 0U;
s_outputReadIdx = 0U;
s_periodicSeq = 0U;
s_processedSeq = 0U;
}
__attribute__((section(".ramfunc")))
__attribute__((section(".ramfunc"), noinline))
void EthercatSlave::periodicIsr()
{
/* 1. 读取输入 PDO 到另一块缓冲(当前可读缓冲的配对) */
const uint8_t write_idx = s_inputReadIndex ^ 1U;
EscLan9252::readPdi(0x0000U, s_inputs[write_idx].data, CONFIG_ETHERCAT_PDO_SIZE);
s_inputs[write_idx].len = CONFIG_ETHERCAT_PDO_SIZE;
const uint32_t cycStart = DWT->CYCCNT;
/* 2. 写入输出 PDO(从应用层未写入的那个缓冲读取) */
const uint8_t out_read_idx = s_outputWriteIndex ^ 1U;
EscLan9252::writePdi(0x0000U, s_outputs[out_read_idx].data, s_outputs[out_read_idx].len);
/* 1. 读取输入 PDO 到当前写入缓冲 */
EscLan9252::readPdi(CONFIG_ESC_PDI_INPUT_OFFSET, s_inputs[s_inputWriteIdx].data, CONFIG_ETHERCAT_PDO_SIZE);
s_inputs[s_inputWriteIdx].len = CONFIG_ETHERCAT_PDO_SIZE;
s_inputWriteIdx = (s_inputWriteIdx + 1U) % 3U;
/* 3. 翻转输入缓冲指针,使新完成的缓冲成为可读缓冲 */
s_inputReadIndex = write_idx;
/* 2. 写入输出 PDO(从当前读取缓冲) */
EscLan9252::writePdi(CONFIG_ESC_PDI_OUTPUT_OFFSET, s_outputs[s_outputReadIdx].data, s_outputs[s_outputReadIdx].len);
s_outputReadIdx = (s_outputReadIdx + 1U) % 3U;
/* 4. 置位非实时任务,通知主循环处理应用层 PDO 映射 */
TaskManager::set(1UL << TASK_ETHERCAT_PROCESS);
/* 3. 递增周期序列号通知主循环(替代 TaskManager::set,避免 BASEPRI 竞态) */
s_periodicSeq++;
const uint32_t elapsed = DWT->CYCCNT - cycStart;
s_isrCount++;
if (elapsed > s_isrMaxCycles) {
s_isrMaxCycles = elapsed;
}
/* 滑动平均:avg = avg + (elapsed - avg) / 16 (近似 EMA,避免除法精度问题) */
s_isrAvgCycles = s_isrAvgCycles + ((int32_t)elapsed - (int32_t)s_isrAvgCycles) / 16;
}
const PdoBuffer* EthercatSlave::getLatestInputs()
{
/* s_inputReadIndex 始终指向最新完成的输入缓冲 */
return &s_inputs[s_inputReadIndex];
/* 返回最新完成的缓冲 = (writeIdx + 2) % 3,与 ISR 当前写入位置隔一个 */
return &s_inputs[(s_inputWriteIdx + 2U) % 3U];
}
void EthercatSlave::setOutputs(const PdoBuffer *outputs)
@@ -55,16 +101,53 @@ void EthercatSlave::setOutputs(const PdoBuffer *outputs)
if (outputs == nullptr) {
return;
}
/* 写入应用层缓冲(ISR 当前不读取的那个) */
const uint8_t write_idx = s_outputWriteIndex ^ 1U;
/* 写入与 ISR 当前读取位置隔 2 个缓冲,提供 2 周期宽限。
* 2 周期宽限下拷贝竞态窗口消除,前提是主循环处理时间 < 2 × 周期时间。 */
const uint8_t write_idx = (s_outputReadIdx + 2U) % 3U;
s_outputs[write_idx] = *outputs;
s_outputWriteIndex = write_idx;
}
void EthercatSlave::process()
{
/* 应用层 PDO 映射入口(非实时,由主循环 TASK_ETHERCAT_PROCESS 调用)。
* TODO: 将 getLatestInputs() 数据映射到应用对象字典;将应用输出填入 setOutputs()。 */
/* 应用层 PDO 桥接(非实时,由主循环周期序列号或 ESC IRQ 触发)。
* 运行期 PDO 交换仅通过 getLatestInputs()/setOutputs() 三缓冲进行;ESC 寄存器访问仅限 init 阶段。
*
* 框架级桥接:从三缓冲取最新输入到 s_appInputs,把 s_appOutputs 推回三缓冲。
* TODO: 应用对象字典映射(具体输入字节对应哪个应用变量,如字节 0-3=温度浮点),
* 由后续应用层 spec 定义;应用层通过 getApplicationInputs()/setApplicationOutputs() 访问快照。 */
const PdoBuffer *latest = getLatestInputs();
if (latest != nullptr) {
s_appInputs = *latest;
}
setOutputs(&s_appOutputs);
}
#endif /* CONFIG_ETHERCAT_ENABLE */
const PdoBuffer* EthercatSlave::getApplicationInputs()
{
return &s_appInputs;
}
void EthercatSlave::setApplicationOutputs(const PdoBuffer *outputs)
{
if (outputs == nullptr) {
return;
}
s_appOutputs = *outputs;
}
bool EthercatSlave::hasPendingPeriodic()
{
return s_periodicSeq != s_processedSeq;
}
void EthercatSlave::markPeriodicProcessed()
{
s_processedSeq = s_periodicSeq;
}
void EthercatSlave::getIsrStats(uint32_t *maxCycles, uint32_t *avgCycles, uint32_t *count)
{
if (maxCycles) *maxCycles = s_isrMaxCycles;
if (avgCycles) *avgCycles = s_isrAvgCycles;
if (count) *count = s_isrCount;
}
+91 -12
View File
@@ -1,8 +1,13 @@
/**
* @file ethercat_slave.h
* @brief EtherCAT 从站周期处理接口与 PDO 双缓冲(ping-pong)结构。
* @brief EtherCAT 从站周期处理接口与 PDO 三缓冲(triple buffer)结构。
* @note 周期 ISR 调用 periodicIsr() 完成输入 PDO 读取 + 输出 PDO 写入;
* 应用层通过 getLatestInputs()/setOutputs() 与周期路径无锁交换数据。
*
* 通知路径分离:
* 周期路径(periodicIsr)用 s_periodicSeq 序列号通知主循环;
* 事件路径(ESC IRQ)用 TASK_ETHERCAT_PROCESS 标志。
* 两者独立,避免 preempt=0 ISR 与 BASEPRI 临界区的 RMW 竞态。
*/
#ifndef COMM_ETHERCAT_SLAVE_H
#define COMM_ETHERCAT_SLAVE_H
@@ -13,9 +18,16 @@
/** @brief 默认 PDO 大小(可按需调整)。 */
constexpr size_t CONFIG_ETHERCAT_PDO_SIZE = 32U;
/** @brief EtherCAT AL 状态码(EtherCAT 协议定义,ESC AL Status 寄存器低 4 位)。 */
constexpr uint8_t AL_STATE_INIT = 0x01U; /**< INIT 状态。 */
constexpr uint8_t AL_STATE_PREOP = 0x02U; /**< PREOP 状态。 */
constexpr uint8_t AL_STATE_BOOT = 0x03U; /**< BOOT 状态。 */
constexpr uint8_t AL_STATE_SAFEOP = 0x04U; /**< SAFEOP 状态(SYNC0 可触发下限)。 */
constexpr uint8_t AL_STATE_OP = 0x08U; /**< OP 状态。 */
/**
* @brief PDO 数据缓冲(输入/输出共用结构)。
* @note 周期 ISR 与应用层通过双缓冲无锁交换。
* @note 周期 ISR 与应用层通过三缓冲无锁交换。
*/
struct PdoBuffer {
uint8_t data[CONFIG_ETHERCAT_PDO_SIZE] = {0}; /**< 数据载荷。 */
@@ -24,44 +36,111 @@ struct PdoBuffer {
/**
* @brief EtherCAT 从站周期处理。
* @note 周期路径(periodicIsr)必须极简:读输入→写输出→翻转指针→置位任务。
* @note 周期路径(periodicIsr)必须极简:读输入→写输出→模 3 轮转指针→递增序列号。
*/
class EthercatSlave {
public:
/** @brief 初始化 ESC 与双缓冲。 */
/** @brief 初始化 ESC 与三缓冲。 */
static void init();
/**
* @brief 周期中断处理(由 SYNC0 EXTI 触发)。
* @note 放入 RAM 执行段(.ramfunc)以降低 Flash 等待带来的抖动。
* 流程:读输入 PDO → 写输出 PDO → 翻转输入缓冲指针 → 置位 TASK_ETHERCAT_PROCESS。
* 流程:读输入 PDO → 写输出 PDO → 模 3 轮转索引 → 递增 s_periodicSeq。
* 不再调用 TaskManager::set,避免 preempt=0 与 BASEPRI 临界区的 RMW 竞态。
*/
static void periodicIsr();
/**
* @brief 获取最新输入 PDO(主循环调用,无锁)。
* @return const PdoBuffer* 最新输入缓冲只读指针;周期 ISR 不会修改该缓冲。
* @return const PdoBuffer* 最新输入缓冲只读指针;返回与 ISR 当前写入位置隔一个的缓冲。
* @note 三缓冲容忍主循环跨 1 个周期,主循环处理必须 < 2 × 周期时间。
*/
static const PdoBuffer* getLatestInputs();
/**
* @brief 设置下次周期要发送的输出 PDO(主循环调用)。
* @param[in] outputs 输出数据指针(内容会被拷贝到内部输出缓冲)。
* @note 调用方在 ISR 不持有期间调用即可,内部使用乒乓缓冲避免竞争。
* @note 写入与 ISR 当前读取位置隔一个的缓冲,无需任何临界区。
*/
static void setOutputs(const PdoBuffer *outputs);
/**
* @brief 应用层 PDO 映射入口(非实时,由 TASK_ETHERCAT_PROCESS 调用)。
* @brief 应用层 PDO 映射入口(非实时,由 TASK_ETHERCAT_PROCESS 或周期序列号触发)。
* @note 留接口,具体映射逻辑由协议栈后续实现。
*/
static void process();
/**
* @brief 获取应用层输入 PDO 快照(主循环调用)。
* @return const PdoBuffer* 最近一次 process() 拷贝的输入快照只读指针。
* @note 返回的指针在下次 process() 调用前有效;应用层应在 process() 后立即读取。
*/
static const PdoBuffer* getApplicationInputs();
/**
* @brief 设置应用层输出 PDO 快照(主循环调用)。
* @param[in] outputs 输出数据指针(内容会被拷贝到内部快照)。
* @note 下次 process() 调用时,内部快照通过 setOutputs() 推入三缓冲。
*/
static void setApplicationOutputs(const PdoBuffer *outputs);
/**
* @brief 检查是否有新的周期数据待主循环处理。
* @return bool true 表示 s_periodicSeq 与已处理序号不同。
* @note 主循环 runPending() 优先调用此接口,替代原 TASK_ETHERCAT_PROCESS 周期路径。
*/
static bool hasPendingPeriodic();
/**
* @brief 标记当前周期数据已被主循环处理。
* @note 调用此接口前应已调用 taskEthercatProcess() 完成本次 PDO 映射。
*/
static void markPeriodicProcessed();
/** @brief 读取 ISR 执行时间诊断(max/avg 单位:CPU 周期数,除以 180 得 µs)。 */
static void getIsrStats(uint32_t *maxCycles, uint32_t *avgCycles, uint32_t *count);
/** @brief ESC 自检是否通过(init 后可查询,用于决定是否使能同步中断)。 */
static bool isEscReady() { return s_escReady; }
/** @brief 读取 ESC 芯片 ID(供诊断打印;未就绪时为上次读到的值)。 */
static uint32_t getChipId() { return s_chipId; }
/** @brief 读取 ESC AL 状态码(供诊断打印与 SSC 集成;未就绪时为上次读到的值)。 */
static uint8_t getAlState() { return s_alState; }
private:
static PdoBuffer s_inputs[2]; /**< 输入双缓冲(pong-pong)。 */
static PdoBuffer s_outputs[2]; /**< 输出双缓冲(ping-pong)。 */
static volatile uint8_t s_inputReadIndex; /**< ISR 当前写入索引;getLatestInputs 读另一个。 */
static volatile uint8_t s_outputWriteIndex; /**< 应用层写入索引;ISR 读另一个。 */
/**
* @brief 输入三缓冲。
* ISR 按 s_inputWriteIdx 模 3 轮转写入;getLatestInputs() 读取 (writeIdx + 2) % 3。
*/
static PdoBuffer s_inputs[3];
/**
* @brief 输出三缓冲。
* ISR 按 s_outputReadIdx 模 3 轮转读取;setOutputs() 写入 (readIdx + 2) % 3,
* 与 ISR 当前读取位置隔 2 个缓冲,提供 2 周期宽限消除拷贝竞态。
*/
static PdoBuffer s_outputs[3];
/** @brief 应用层输入快照(主循环访问,process() 从三缓冲拷贝;无需三缓冲)。 */
static PdoBuffer s_appInputs;
/** @brief 应用层输出快照(主循环访问,process() 推入三缓冲;无需三缓冲)。 */
static PdoBuffer s_appOutputs;
static volatile uint8_t s_inputWriteIdx; /**< ISR 当前写入索引;模 3 轮转。 */
static volatile uint8_t s_outputReadIdx; /**< ISR 当前读取索引;模 3 轮转。 */
/** @brief 周期路径通知序列号(ISR 单字原子递增,主循环比对;uint16_t 提供 6.55s 回绕周期)。 */
static volatile uint16_t s_periodicSeq;
/** @brief 主循环已处理序号(由 hasPendingPeriodic/markPeriodicProcessed 维护)。 */
static uint16_t s_processedSeq;
static volatile uint32_t s_isrMaxCycles; /**< ISR 最大执行周期数(180MHz 基准)。 */
static volatile uint32_t s_isrAvgCycles; /**< ISR 平均执行周期数(滑动累计)。 */
static volatile uint32_t s_isrCount; /**< ISR 执行次数计数。 */
static bool s_escReady; /**< ESC 自检结果(init 后置位)。 */
static uint32_t s_chipId; /**< ESC 芯片 ID(诊断用)。 */
static uint8_t s_alState; /**< ESC AL 状态码(init 后可查询,供 isEscReady 守卫)。 */
};
#endif /* COMM_ETHERCAT_SLAVE_H */
+21 -6
View File
@@ -8,8 +8,6 @@
#include "sys/task_manager.h"
#include "main.h"
#if CONFIG_ETHERCAT_ENABLE
extern "C" {
#include "gd32e50x_rcu.h"
#include "gd32e50x_gpio.h"
@@ -68,13 +66,29 @@ void EthercatSync::init()
timer_interrupt_enable(TIMER0, TIMER_INT_UP);
nvic_irq_enable(TIMER0_IRQn, CONFIG_IRQ_PREEMPT_ETHERCAT_SYNC, 0U);
timer_enable(TIMER0);
/* ESC IRQ:PB1 -> EXTI1,下降沿,preempt=1 */
gpio_exti_source_select(CONFIG_ESC_IRQ_EXTI_SOURCE_PORT, CONFIG_ESC_IRQ_EXTI_SOURCE_PIN);
exti_init(CONFIG_ESC_IRQ_EXTI, EXTI_INTERRUPT, EXTI_TRIG_FALLING);
exti_interrupt_enable(CONFIG_ESC_IRQ_EXTI);
exti_interrupt_flag_clear(CONFIG_ESC_IRQ_EXTI);
nvic_irq_enable(CONFIG_ESC_IRQ_IRQn, CONFIG_IRQ_PREEMPT_ETHERCAT_ESC, 0U);
/* SYNC1:PB2 -> EXTI2,下降沿,preempt=1(备用) */
gpio_exti_source_select(CONFIG_ESC_SYNC1_EXTI_SOURCE_PORT, CONFIG_ESC_SYNC1_EXTI_SOURCE_PIN);
exti_init(CONFIG_ESC_SYNC1_EXTI, EXTI_INTERRUPT, EXTI_TRIG_FALLING);
exti_interrupt_enable(CONFIG_ESC_SYNC1_EXTI);
exti_interrupt_flag_clear(CONFIG_ESC_SYNC1_EXTI);
nvic_irq_enable(CONFIG_ESC_SYNC1_IRQn, CONFIG_IRQ_PREEMPT_ETHERCAT_SYNC1, 0U);
#endif
}
__attribute__((section(".ramfunc"), noinline))
void EthercatSync::onSync0Exti()
{
if (exti_interrupt_flag_get(CONFIG_ESC_SYNC0_EXTI) != RESET) {
exti_interrupt_flag_clear(CONFIG_ESC_SYNC0_EXTI);
/* 直接寄存器访问,绕过 GD32 库函数以消除可变抖动 */
if ((EXTI_PD & (uint32_t)CONFIG_ESC_SYNC0_EXTI) != RESET) {
EXTI_PD = (uint32_t)CONFIG_ESC_SYNC0_EXTI;
EthercatSlave::periodicIsr();
}
}
@@ -110,13 +124,14 @@ void EthercatSync::onTimerIrq()
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(); }
#else
void TIMER0_IRQHandler(void) { EthercatSync::onTimerIrq(); }
void EXTI1_IRQHandler(void) { EthercatSync::onEscIrqExti(); }
void EXTI2_IRQHandler(void) { EthercatSync::onSync1Exti(); }
#endif
} /* extern "C" */
#endif /* CONFIG_ETHERCAT_ENABLE */
+167 -8
View File
@@ -1,4 +1,4 @@
/**
/**
* @file canfd.cpp
* @brief CANFD 通信模块实现。
*/
@@ -8,6 +8,7 @@
#include "sys/config.h"
#include "sys/critical.h"
#include "sys/task_manager.h"
#include <cstring>
namespace {
struct TxFrame {
@@ -49,6 +50,11 @@ constexpr uint32_t kCanRxParseTaskMask = (1UL << TASK_CAN_RX_PARSE);
CanFd::Diag CanFd::s_diag{};
CanFd::RxQueue CanFd::s_rxQueues[3]{};
uint8_t CanFd::s_nextConsumeChannel = 0U;
volatile CanFd::Mode CanFd::s_mode = CanFd::Mode::kModeAllPass;
CanFd::ChannelStats CanFd::s_channelStats[3]{};
CanFd::ChannelFeedback CanFd::s_feedback[3]{};
uint32_t CanFd::s_feedbackSeq = 0U;
CanFd::IdSnapshotTable CanFd::s_idSnapshot[3]{};
static TxQueue s_txQueues[3]{};
/**
@@ -77,20 +83,30 @@ void CanFd::initGpio()
* @brief 为指定 CAN 控制器配置接收过滤器。
* @param[in] canPeriph 目标 CAN 外设实例。
* @param[in] filterNumber 过滤器编号。
* @param[in] mode 目标接收过滤模式。
* @param[out] 无。
* @retval 无返回值。
* @note 当前默认按 `CONFIG_CAN_RX_FILTER_ID`/`CONFIG_CAN_RX_FILTER_MASK` 对标准帧做掩码匹配,并统一进入 FIFO0。
* @note kModeAllPass 模式下使用 32 位掩码模式且 mask=0/list=0,硬件直接放行所有标准/扩展帧到 FIFO0;
* kModeFilterId 模式下按 CONFIG_CAN_RX_FILTER_ID/MASK 做标准帧掩码匹配。
*/
void CanFd::configureRxFilter(uint32_t canPeriph, uint8_t filterNumber)
void CanFd::configureRxFilter(uint32_t canPeriph, uint8_t filterNumber, Mode mode)
{
can_filter_parameter_struct can_filter{};
can_filter.filter_number = filterNumber;
can_filter.filter_mode = CAN_FILTERMODE_MASK;
can_filter.filter_bits = CAN_FILTERBITS_32BIT;
can_filter.filter_list_high = static_cast<uint16_t>((CONFIG_CAN_RX_FILTER_ID & 0x7FFU) << 5U);
can_filter.filter_list_low = 0x0000U;
can_filter.filter_mask_high = static_cast<uint16_t>((CONFIG_CAN_RX_FILTER_MASK & 0x7FFU) << 5U);
can_filter.filter_mask_low = static_cast<uint16_t>(1U << 2U);
if (mode == Mode::kModeAllPass) {
/* 全部接收:列表与掩码都为 0,硬件不再对 ID 做比较。 */
can_filter.filter_list_high = 0x0000U;
can_filter.filter_list_low = 0x0000U;
can_filter.filter_mask_high = 0x0000U;
can_filter.filter_mask_low = 0x0000U;
} else {
can_filter.filter_list_high = static_cast<uint16_t>((CONFIG_CAN_RX_FILTER_ID & 0x7FFU) << 5U);
can_filter.filter_list_low = 0x0000U;
can_filter.filter_mask_high = static_cast<uint16_t>((CONFIG_CAN_RX_FILTER_MASK & 0x7FFU) << 5U);
can_filter.filter_mask_low = static_cast<uint16_t>(1U << 2U);
}
can_filter.filter_fifo_number = CAN_FIFO0;
can_filter.filter_enable = ENABLE;
can_filter_init(canPeriph, &can_filter);
@@ -204,6 +220,7 @@ bool CanFd::enqueueTxFrame(uint8_t channelIndex, const StatusPayload *status)
auto &queue = s_txQueues[channelIndex];
if(queue.count >= kTxQueueDepth) {
s_diag.tx_drop_count++;
s_channelStats[channelIndex].error_count++;
return false;
}
@@ -245,6 +262,12 @@ void CanFd::init()
{
s_diag = {};
s_nextConsumeChannel = 0U;
s_mode = Mode::kModeAllPass;
for (size_t i = 0; i < kChannelCount; ++i) {
s_channelStats[i] = {};
s_feedback[i] = {};
}
s_feedbackSeq = 0U;
initGpio();
@@ -255,7 +278,7 @@ void CanFd::init()
can_deinit(ch.canPeriph);
initNominalTiming(ch.canPeriph);
initFdMode(ch.canPeriph);
configureRxFilter(ch.canPeriph, ch.filterNumber);
configureRxFilter(ch.canPeriph, ch.filterNumber, s_mode);
can_interrupt_enable(ch.canPeriph, CAN_INT_RFNE0 | CAN_INT_RFF0 | CAN_INT_RFO0);
nvic_irq_enable(ch.rx0Irq, CONFIG_IRQ_PREEMPT_CAN, static_cast<uint8_t>(i));
@@ -270,6 +293,29 @@ void CanFd::init()
s_nextConsumeChannel = 0U;
}
/**
* @brief 切换 CANFD 接收过滤模式。
* @param[in] mode 目标过滤模式。
* @note 切换时逐路重新配置过滤器;can_filter_init 内部会清 FCTL_FLD,无需手动进入 INITIAL 模式。
*/
void CanFd::setMode(Mode mode)
{
uint32_t save = RT_CRITICAL_SAVE();
s_mode = mode;
for (size_t i = 0; i < kChannelCount; ++i) {
configureRxFilter(kChannels[i].canPeriph, kChannels[i].filterNumber, mode);
}
RT_CRITICAL_RESTORE(save);
}
/**
* @brief 查询当前 CANFD 接收过滤模式。
*/
CanFd::Mode CanFd::mode()
{
return s_mode;
}
/**
* @brief 执行兼容保留的 CAN 轮询入口。
* @param[in] 无。
@@ -334,10 +380,12 @@ void CanFd::sendPrepared()
s_diag.last_payload_length = frame.tx_message.tx_dlen;
s_diag.last_tx_id = frame.tx_message.tx_sfid;
s_diag.last_tx_channel = static_cast<uint8_t>(i);
s_channelStats[i].tx_count++;
queue.head = static_cast<uint8_t>((queue.head + 1U) % kTxQueueDepth);
queue.count--;
} else {
s_diag.error_count++;
s_channelStats[i].error_count++;
}
}
}
@@ -464,9 +512,11 @@ bool CanFd::transmitFrame(uint8_t channelIndex, uint32_t ext_id,
s_diag.last_tx_id = ext_id;
s_diag.last_tx_channel = channelIndex;
s_diag.last_payload_length = dlc;
s_channelStats[channelIndex].tx_count++;
return true;
}
s_diag.error_count++;
s_channelStats[channelIndex].error_count++;
return false;
}
@@ -546,9 +596,71 @@ void CanFd::handleRxInterrupt(uint8_t channelIndex)
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;
/* 维护每通道独立计数与最近一帧快照,用于 1Hz 反馈给 ESP32。 */
auto &stats = s_channelStats[channelIndex];
stats.rx_count++;
stats.last_rx_id = s_diag.last_rx_id;
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;
stats.last_rx_esi = rx_message.fd_esi;
const uint8_t copyLen = (rx_message.rx_dlen < CANFD_FEEDBACK_LAST_DATA_MAX)
? rx_message.rx_dlen : static_cast<uint8_t>(CANFD_FEEDBACK_LAST_DATA_MAX);
for (uint8_t i = 0U; i < copyLen; ++i) {
stats.last_rx_data[i] = rx_message.rx_data[i];
}
for (uint8_t i = copyLen; i < CANFD_FEEDBACK_LAST_DATA_MAX; ++i) {
stats.last_rx_data[i] = 0U;
}
/* 维护每通道不重复 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) {
if (table.entries[si].occupied && table.entries[si].id == rxId) {
found = si;
break;
}
}
if (found >= CANFD_ID_SNAPSHOT_MAX) {
/* 未找到:表未满则新建,满了淘汰最旧(index 0) */
if (table.count < CANFD_ID_SNAPSHOT_MAX) {
found = table.count;
table.count++;
} else {
found = 0U;
/* 淘汰后整体左移一格让新条目在末尾 */
for (uint8_t si = 0U; si < CANFD_ID_SNAPSHOT_MAX - 1U; ++si) {
table.entries[si] = table.entries[si + 1U];
}
found = CANFD_ID_SNAPSHOT_MAX - 1U;
}
table.entries[found] = {};
table.entries[found].id = rxId;
table.entries[found].occupied = true;
}
auto &entry = table.entries[found];
entry.rx_count++;
entry.dlc = rx_message.rx_dlen;
entry.fd_flag = rx_message.fd_flag;
entry.brs = rx_message.fd_brs;
entry.esi = rx_message.fd_esi;
const uint8_t snapCopy = (rx_message.rx_dlen < CANFD_ID_SNAPSHOT_DATA_MAX)
? rx_message.rx_dlen : static_cast<uint8_t>(CANFD_ID_SNAPSHOT_DATA_MAX);
for (uint8_t si = 0U; si < snapCopy; ++si) {
entry.data[si] = rx_message.rx_data[si];
}
for (uint8_t si = snapCopy; si < CANFD_ID_SNAPSHOT_DATA_MAX; ++si) {
entry.data[si] = 0U;
}
}
auto &queue = s_rxQueues[channelIndex];
if(queue.count >= kRxQueueDepth) {
s_diag.rx_drop_count++;
stats.rx_drop_count++;
continue;
}
@@ -587,6 +699,53 @@ void CanFd::onRx0Interrupt()
handleRxInterrupt(0U);
}
/**
* @brief 刷新三路 CAN 通道的 1Hz 反馈快照。
* @note 在临界区内拷贝实时计数与最近一帧信息到 s_feedback;保证 SPI 命令读到一致快照。
* 典型调用频率 1Hz,由任务调度器在心跳节拍触发。
*/
void CanFd::refreshFeedbackSnapshot()
{
uint32_t save = RT_CRITICAL_SAVE();
s_feedbackSeq++;
for (size_t i = 0; i < kChannelCount; ++i) {
s_feedback[i].snapshot_seq = s_feedbackSeq;
s_feedback[i].stats = s_channelStats[i];
s_idSnapshot[i].seq = s_feedbackSeq;
}
RT_CRITICAL_RESTORE(save);
}
/**
* @brief 拷贝指定通道的最新反馈快照。
* @param[in] channelIndex 目标 CAN 通道号(0/1/2)。
* @param[out] out 目标快照缓冲;为空或通道非法时返回 false。
* @retval bool true 表示成功拷贝。
*/
/**
* @brief 拷贝指定通道的不重复 ID 快照表。
*/
bool CanFd::copyIdSnapshotTable(uint8_t channelIndex, IdSnapshotTable *out)
{
if (out == nullptr || channelIndex >= kChannelCount) {
return false;
}
uint32_t save = RT_CRITICAL_SAVE();
*out = s_idSnapshot[channelIndex];
RT_CRITICAL_RESTORE(save);
return true;
}
bool CanFd::copyFeedbackSnapshot(uint8_t channelIndex, ChannelFeedback *out)
{
if (out == nullptr || channelIndex >= kChannelCount) {
return false;
}
uint32_t save = RT_CRITICAL_SAVE();
*out = s_feedback[channelIndex];
RT_CRITICAL_RESTORE(save);
return true;
}
/* ----- 中断入口 C 包装 ------------------------------------------------- */
/**
* @brief CAN0 RX0 中断向量入口。
+1 -2
View File
@@ -40,11 +40,10 @@ void SchedulerTimer::configureTimer()
* @param[in] 无。
* @param[out] 无。
* @retval 无返回值。
* @note 设置优先级分组,并为调度定时器中断向量配置抢占优先级与子优先级。
* @note 优先级分组已前移至 AppInitializer::init() 最开头统一设置;此处仅为调度定时器中断向量配置抢占优先级与子优先级。
*/
void SchedulerTimer::configureNvic()
{
nvic_priority_group_set(CONFIG_NVIC_PRIORITY_GROUP);
nvic_irq_enable(CONFIG_SCHED_TIMER_IRQn, CONFIG_SCHED_TIMER_IRQ_PREEMPT_PRIO, CONFIG_SCHED_TIMER_IRQ_SUB_PRIO);
}
+129 -134
View File
@@ -1,4 +1,4 @@
#ifndef DEVICE_CANFD_H
#ifndef DEVICE_CANFD_H
#define DEVICE_CANFD_H
#include <stdint.h>
@@ -6,165 +6,155 @@
/** @brief CANFD 状态报文的数据长度,单位字节。 */
#define CANFD_STATUS_DLEN 12U
/** @brief 每通道反馈快照中保存的最近一帧最大数据长度,受 SPI 应答帧总长度约束。 */
#define CANFD_FEEDBACK_LAST_DATA_MAX 32U
/** @brief 每通道不重复 ID 快照表最大条目数。 */
#define CANFD_ID_SNAPSHOT_MAX 16U
/** @brief 快照条目中最大数据字节数(等于 CANFD DLC 上限)。 */
#define CANFD_ID_SNAPSHOT_DATA_MAX 64U
/**
* @brief CANFD 通信模块。
* @note 负责 GPIO/模式控制、波特率配置、异步接收缓存与按需发送能力。
*/
class CanFd {
public:
/**
* @brief CANFD 接收过滤模式枚举。
* @note 默认模式为 kModeAllPass,即三路控制器关闭硬件 ID 过滤,全部接收。
*/
enum class Mode : uint8_t {
kModeAllPass = 0U, /**< 全部接收模式:禁用硬件 ID/掩码匹配,所有 11/29 位帧都进入 FIFO0。 */
kModeFilterId = 1U, /**< 过滤模式:按 CONFIG_CAN_RX_FILTER_ID/MASK 做标准帧掩码匹配。 */
};
/**
* @brief CANFD 运行诊断统计信息。
* @note 记录发送、接收、错误计数以及最近一次收发报文的关键信息。
*/
struct Diag {
uint32_t tx_count; /**< 成功发送的报文计数。 */
uint32_t rx_count; /**< 成功接收的报文计数。 */
uint32_t error_count; /**< 发送失败或检测到的错误计数。 */
uint32_t rx_drop_count; /**< 软件接收队列满导致丢弃的报文计数。 */
uint32_t rx_fifo_full_count; /**< 硬件 FIFO0 进入满状态的次数。 */
uint32_t rx_fifo_overfull_count; /**< 硬件 FIFO0 发生 overfull 的次数。 */
uint32_t tx_drop_count; /**< 发送 FIFO 满导致丢弃的报文计数。 */
uint32_t tx_abort_count; /**< 因邮箱全占主动中止占用邮箱的次数(典型:总线无 ACK 导致自动重传卡死)。 */
uint32_t last_rx_id; /**< 最近一次接收报文的标识符。 */
uint32_t last_tx_id; /**< 最近一次发送报文的标识符。 */
uint8_t last_rx_channel; /**< 最近一次接收报文所在的 CAN 通道号(0/1/2)。 */
uint8_t last_tx_channel; /**< 最近一次发送报文所在的 CAN 通道号(0/1/2)。 */
uint8_t last_payload_length; /**< 最近一次收发报文的数据长度。 */
uint8_t last_rx_fd_flag; /**< 最近一次接收报文的 FD 帧标志。 */
uint8_t last_rx_brs; /**< 最近一次接收报文的波特率切换标志。 */
uint8_t last_rx_esi; /**< 最近一次接收报文的错误状态指示位。 */
uint32_t tx_count;
uint32_t rx_count;
uint32_t error_count;
uint32_t rx_drop_count;
uint32_t rx_fifo_full_count;
uint32_t rx_fifo_overfull_count;
uint32_t tx_drop_count;
uint32_t tx_abort_count;
uint32_t last_rx_id;
uint32_t last_tx_id;
uint8_t last_rx_channel;
uint8_t last_tx_channel;
uint8_t last_payload_length;
uint8_t last_rx_fd_flag;
uint8_t last_rx_brs;
uint8_t last_rx_esi;
};
/**
* @brief 单通道运行计数(每路独立累计)。
*/
struct ChannelStats {
uint32_t rx_count; /**< 该通道累计成功接收帧数。 */
uint32_t tx_count; /**< 该通道累计成功发送帧数。 */
uint32_t rx_drop_count; /**< 该通道累计软件丢帧数。 */
uint32_t error_count; /**< 该通道累计错误计数(与发送邮箱失败合并)。 */
uint32_t last_rx_id; /**< 该通道最近一帧报文标识符。 */
uint8_t last_rx_dlc; /**< 该通道最近一帧报文数据长度。 */
uint8_t last_rx_fd_flag; /**< 该通道最近一帧 FD 帧标志。 */
uint8_t last_rx_brs; /**< 该通道最近一帧 BRS 标志。 */
uint8_t last_rx_esi; /**< 该通道最近一帧 ESI 标志。 */
uint8_t last_rx_data[CANFD_FEEDBACK_LAST_DATA_MAX];
};
/**
* @brief 单通道 1Hz 反馈快照(每秒刷新一次)。
*/
struct ChannelFeedback {
uint32_t snapshot_seq; /**< 第几次刷新(自启动起递增)。 */
ChannelStats stats; /**< 抓拍时刻的该通道统计与最近一帧信息。 */
};
/**
* @brief 单个不重复 ID 的最新帧快照。
*/
struct IdSnapshotEntry {
uint32_t id; /**< CAN 报文标识符。 */
uint32_t seq; /**< 该条目被更新的序号(自启动起递增)。 */
uint32_t rx_count; /**< 该 ID 累计接收帧数。 */
uint8_t dlc; /**< 最新一帧数据长度。 */
uint8_t fd_flag; /**< 最新一帧 FD 标志。 */
uint8_t brs; /**< 最新一帧 BRS 标志。 */
uint8_t esi; /**< 最新一帧 ESI 标志。 */
uint8_t data[CANFD_ID_SNAPSHOT_DATA_MAX]; /**< 最新一帧数据。 */
bool occupied; /**< 条目是否已被占用。 */
};
/**
* @brief 每通道不重复 ID 快照表(1Hz 刷新,SPI 命令分页拉取)。
*/
struct IdSnapshotTable {
IdSnapshotEntry entries[CANFD_ID_SNAPSHOT_MAX];
uint8_t count; /**< 当前占用条目数。 */
uint32_t seq; /**< 全局更新序号(每次 refresh 时递增)。 */
};
/**
* @brief CANFD 状态发送载荷。
* @note 当前保持协议兼容,仅包含温度与供电相关字段;VIN_ADC 单独在诊断打印中输出。
*/
struct StatusPayload {
uint16_t temp_raw; /**< 内部温度通道原始 ADC 数值。 */
uint16_t vref_raw; /**< 内部参考电压通道原始 ADC 数值。 */
float vdda; /**< 根据内部参考电压换算得到的 MCU 供电电压,单位 V。 */
float temperature; /**< 根据温度通道换算得到的芯片温度,单位 ℃。 */
uint16_t temp_raw;
uint16_t vref_raw;
float vdda;
float temperature;
};
/**
* @brief 初始化 CANFD 外设、GPIO、模式控制、过滤器与中断。
* @param[in] 无。
* @param[out] 无。
* @retval 无返回值。
* @note 初始化后默认进入 Normal 模式,并使能 3 路 CAN 控制器的 RX0 中断用于异步接收。
*/
static void init();
/**
* @brief 兼容保留的轮询接口,中断模式下为空壳。
* @param[in] 无。
* @param[out] 无。
* @retval 无返回值。
* @note 实际接收已由 CAN0/CAN1/CAN2 的 RX0 中断驱动。
* @brief 切换 CANFD 接收过滤模式。
*/
static void setMode(Mode mode);
/** @brief 查询当前 CANFD 接收过滤模式。 */
static Mode mode();
static void poll();
/**
* @brief 根据状态载荷准备一帧待发送的 CANFD 状态报文,并广播入队到 3 路发送 FIFO。
* @param[in] status 指向 StatusPayload 的指针;为空时不入队。
* @param[out] 无。
* @retval 无返回值。
* @note 当前模块默认不会周期性调用该接口;仅在外部显式触发发送测试或业务发送时使用。
*/
static void prepareStatus(const StatusPayload *status);
/**
* @brief 根据状态载荷仅向指定 CAN 通道的发送 FIFO 入队一帧状态报文。
* @param[in] channelIndex 目标 CAN 通道号(0/1/2)。
* @param[in] status 指向 StatusPayload 的指针;为空时不入队。
* @param[out] 无。
* @retval bool true 表示成功入队,false 表示通道非法或该通道发送 FIFO 已满。
* @note 当前模块默认不会周期性调用该接口;仅在外部显式触发指定通道发送时使用。
*/
static bool prepareStatusForChannel(uint8_t channelIndex, const StatusPayload *status);
/**
* @brief 发送当前已准备好的 CANFD 状态报文。
* @param[in] 无。
* @param[out] 无。
* @retval 无返回值。
* @note 当前模块默认不会周期性调用该接口;仅发送调用方显式准备过的报文;若队列为空则直接返回。
*/
static void sendPrepared();
/**
* @brief 查询 3 个 CAN 通道中是否存在尚未消费的接收报文。
* @param[in] 无。
* @param[out] 无。
* @retval bool true 表示至少有一路存在待消费报文,false 表示当前所有通道缓存为空。
*/
static bool hasRxFrame();
/**
* @brief 读取并消费最近一轮扫描到的某一路缓存接收帧。
* @param[in] 无。
* @param[out] id 输出报文标识符的指针;允许为空。
* @param[out] data 输出报文数据缓存区指针;允许为空。
* @param[out] length 输出报文数据长度的指针;允许为空。
* @param[out] channel 输出报文来源的 CAN 通道号指针;允许为空。
* @retval bool true 表示成功消费一帧报文,false 表示当前 3 路缓存均为空。
* @note 每路通道使用轻量环形缓冲保留多帧;消费顺序采用轮转扫描,避免长期偏向固定通道。
*/
static bool consumeRxFrame(uint32_t *id, uint8_t *data, uint8_t *length, uint8_t *channel = nullptr);
/**
* @brief 直接向指定 CAN 通道发送一帧任意数据报文(扩展帧)。
* @param[in] channelIndex 目标 CAN 通道号(0/1/2)。
* @param[in] ext_id 扩展帧 ID(29 位)。
* @param[in] data 数据负载指针。
* @param[in] dlc 数据长度(1~64)。
* @retval bool true 表示成功入队硬件邮箱,false 表示参数无效或邮箱已满。
* @note 绕过软件发送队列,直接调用硬件发送;适用于 SPI 桥接等即时发送场景。
*/
static bool transmitFrame(uint8_t channelIndex, uint32_t ext_id,
const uint8_t *data, uint8_t dlc);
/**
* @brief 获取 CANFD 诊断统计信息只读指针。
* @param[in] 无。
* @param[out] 无。
* @retval const Diag* 指向内部诊断统计结构体的只读指针。
*/
static const Diag *diag();
/**
* @brief 将 TCAN1057A 收发器置为正常工作模式。
* @param[in] 无。
* @param[out] 无。
* @retval 无返回值。
* @note 模式控制脚按 STB 语义处理:低电平为 Normal,高电平为 Silent。
* @brief 刷新三路 1Hz 反馈快照。
* @note 通常由任务调度器在 1 秒节拍调用,将各通道实时计数与最近一帧信息抓拍到 s_feedback。
*/
static void refreshFeedbackSnapshot();
/**
* @brief 拷贝指定通道的最新反馈快照到调用方缓冲。
* @param[in] channelIndex 目标 CAN 通道号(0/1/2)。
* @param[out] out 目标快照缓冲;为空或通道非法时返回 false。
* @retval bool true 表示成功拷贝,false 表示参数非法。
*/
static bool copyFeedbackSnapshot(uint8_t channelIndex, ChannelFeedback *out);
/**
* @brief 拷贝指定通道的不重复 ID 快照表。
* @param[in] channelIndex 目标通道 0/1/2。
* @param[out] out 输出缓冲。
* @retval true 成功。
*/
static bool copyIdSnapshotTable(uint8_t channelIndex, IdSnapshotTable *out);
static void setNormalMode();
/**
* @brief 将 TCAN1057A 收发器置为静默模式。
* @param[in] 无。
* @param[out] 无。
* @retval 无返回值。
* @note 静默模式下收发器不驱动总线显性位,可用于总线监听或受控隔离。
*/
static void setSilentMode();
/**
* @brief CAN0 兼容中断处理入口。
* @param[in] 无。
* @param[out] 无。
* @retval 无返回值。
* @note 兼容保留给 CAN0 中断包装使用;三路统一接收逻辑已收口到 handleRxInterrupt()。
*/
static void onRx0Interrupt();
/**
* @brief 处理指定 CAN 通道的 RX0 中断接收。
* @param[in] channelIndex CAN 通道索引(0/1/2)。
* @param[out] 无。
* @retval 无返回值。
*/
static void handleRxInterrupt(uint8_t channelIndex);
private:
@@ -184,25 +174,30 @@ private:
volatile uint8_t count;
};
/** @brief 初始化 CAN0/CAN1/CAN2 相关 GPIO。 */
static void initGpio();
/** @brief 为指定控制器配置接收过滤器。 */
static void configureRxFilter(uint32_t canPeriph, uint8_t filterNumber);
/** @brief 配置指定控制器的 CAN 仲裁段标称位时序。 */
/**
* @brief 根据当前模式为指定控制器配置接收过滤器。
* @param[in] canPeriph 目标 CAN 外设实例。
* @param[in] filterNumber 过滤器编号。
* @param[in] mode 目标接收过滤模式。
*/
static void configureRxFilter(uint32_t canPeriph, uint8_t filterNumber, Mode mode);
static void initNominalTiming(uint32_t canPeriph);
/** @brief 配置指定控制器的 CAN FD 数据段位时序与 TDC。 */
static void initFdMode(uint32_t canPeriph);
/** @brief 构造内部发送缓冲中的状态帧内容。 */
static void buildStatusFrame(const StatusPayload *status);
/** @brief 向指定 CAN 通道的发送 FIFO 写入一帧状态报文。 */
static bool enqueueTxFrame(uint8_t channelIndex, const StatusPayload *status);
static Diag s_diag;
static RxQueue s_rxQueues[3];
static uint8_t s_nextConsumeChannel;
static Diag s_diag;
static RxQueue s_rxQueues[3];
static uint8_t s_nextConsumeChannel;
static volatile Mode s_mode;
static ChannelStats s_channelStats[3];
static ChannelFeedback s_feedback[3];
static uint32_t s_feedbackSeq;
static IdSnapshotTable s_idSnapshot[3];
};
using canfd_diag_t = CanFd::Diag;
using canfd_status_payload_t = CanFd::StatusPayload;
#endif /* DEVICE_CANFD_H */
#endif /* DEVICE_CANFD_H */
@@ -0,0 +1,206 @@
/**
* @file app_spi_cmd_can_feedback.cpp
* @brief CAN 不重复 ID 快照表轮询与接收模式切换 SPI 命令处理器。
*
* 协议说明(与 ESP32 端 canfd_bridge 对接):
*
* 1) kTypeCanFeedback (0xC1):ESP32 主动轮询,分页返回不重复 ID 快照表。
* 请求 payload:
* [0] channel 0/1/2
* [1] page 页号(0 = 条目 0~7,1 = 条目 8~15)
* 应答 payload(变长,最大 kTxBufSize - overhead = 123 字节):
* [0] channel
* [1] page
* [2] total_count 该通道当前条目总数
* [3] entry_count 本页实际条目数
* [4] mode 当前 CAN 接收模式
* [5..8] snapshot_seq (LE)
* [9..12] rx_count (LE) 通道累计接收
* [13..16] tx_count (LE) 通道累计发送
* [17..20] drop_count (LE)
* [21..24] error_count (LE)
* [25..] 条目逐条拼接:
* id(4LE) + dlc(1) + flags(1) + reserved(2) + data(dlc)
* 每条 = 8 + dlc 字节
*
* 2) kTypeCanSetMode (0xC2):ESP32 主动切换三路 CAN 过滤模式。
* 请求 payload:
* [0] mode 0=全部接收, 1=按 ID 过滤;其它值返回 kRspError。
* 应答 payload:
* [0] 实际生效的 mode。
*/
#include "device/spi/spi_command_handler.h"
#include "device/spi/app_spi_command.h"
#include "device/spi/app_spi_command_defs.h"
#include "device/canfd.h"
#include "sys/config.h"
#include <cstdio>
#include <cstring>
namespace {
constexpr uint8_t kFeedbackChannelCount = 3U;
constexpr uint16_t kFeedbackPayloadLen = 123U; /* kTxBufSize=128 - overhead=5 */
constexpr uint8_t kPageEntries = 8U;
constexpr uint16_t kFixedHeaderLen = 25U; /* ch(1)+page(1)+total(1)+cnt(1)+mode(1)+seq(4)+rx(4)+tx(4)+drop(4)+err(4) */
void writeU32Le(uint8_t *dst, uint32_t value)
{
dst[0] = static_cast<uint8_t>(value & 0xFFU);
dst[1] = static_cast<uint8_t>((value >> 8) & 0xFFU);
dst[2] = static_cast<uint8_t>((value >> 16) & 0xFFU);
dst[3] = static_cast<uint8_t>((value >> 24) & 0xFFU);
}
class SpiCanFeedbackHandler final : public ISpiCommandHandler {
public:
bool canHandle(uint8_t frame_type) const override
{
return (frame_type == SpiDefs::kTypeCanFeedback)
|| (frame_type == SpiDefs::kTypeCanSetMode);
}
void handle(AppSpiCommand &bus, const SpiCommandContext &ctx) override
{
if (ctx.frame_type == SpiDefs::kTypeCanFeedback) {
handleFeedback(bus, ctx);
} else {
handleSetMode(bus, ctx);
}
}
private:
void handleFeedback(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 uint8_t page = ctx.payload[1];
if (channel >= kFeedbackChannelCount) {
uint8_t err = 0xE1U;
bus.buildResponse(ctx.tx_buf, ctx.tx_buf_size, ctx.tx_len,
SpiDefs::kRspError, &err, 1U);
return;
}
/* 拷贝统计快照 */
CanFd::ChannelFeedback snap{};
CanFd::copyFeedbackSnapshot(channel, &snap);
/* 拷贝 ID 快照表 */
CanFd::IdSnapshotTable table{};
CanFd::copyIdSnapshotTable(channel, &table);
const uint8_t start = static_cast<uint8_t>(page * kPageEntries);
if (start >= CANFD_ID_SNAPSHOT_MAX) {
uint8_t err = 0xE3U;
bus.buildResponse(ctx.tx_buf, ctx.tx_buf_size, ctx.tx_len,
SpiDefs::kRspError, &err, 1U);
return;
}
uint8_t payload[kFeedbackPayloadLen] = {};
/* 固定头 */
payload[0] = channel;
payload[1] = page;
payload[2] = table.count;
/* payload[3] = entry_count 稍后填 */
payload[4] = static_cast<uint8_t>(CanFd::mode());
writeU32Le(&payload[5], snap.snapshot_seq);
writeU32Le(&payload[9], snap.stats.rx_count);
writeU32Le(&payload[13], snap.stats.tx_count);
writeU32Le(&payload[17], snap.stats.rx_drop_count);
writeU32Le(&payload[21], snap.stats.error_count);
/* 打包 ID 条目 */
uint16_t offset = kFixedHeaderLen;
uint8_t packed = 0U;
const uint8_t end = static_cast<uint8_t>(
(start + kPageEntries < CANFD_ID_SNAPSHOT_MAX)
? (start + kPageEntries) : CANFD_ID_SNAPSHOT_MAX);
for (uint8_t i = start; i < end; ++i) {
const auto &e = table.entries[i];
if (!e.occupied) continue;
const uint8_t dataLen = (e.dlc < CANFD_ID_SNAPSHOT_DATA_MAX)
? e.dlc : static_cast<uint8_t>(CANFD_ID_SNAPSHOT_DATA_MAX);
const uint16_t entrySize = static_cast<uint16_t>(8U + dataLen);
if (offset + entrySize > kFeedbackPayloadLen) break;
writeU32Le(&payload[offset], e.id);
payload[offset + 4U] = e.dlc;
payload[offset + 5U] = static_cast<uint8_t>(
(e.fd_flag ? 1U : 0U) | (e.brs ? 2U : 0U) | (e.esi ? 4U : 0U));
payload[offset + 6U] = 0U; /* reserved */
payload[offset + 7U] = 0U; /* reserved */
memcpy(&payload[offset + 8U], e.data, dataLen);
offset += entrySize;
packed++;
}
payload[3] = packed;
bus.buildResponse(ctx.tx_buf, ctx.tx_buf_size, ctx.tx_len,
SpiDefs::kRspAck, payload, offset);
}
void handleSetMode(AppSpiCommand &bus, const SpiCommandContext &ctx)
{
if (ctx.payload_len < 1U) {
uint8_t err = 0xE0U;
bus.buildResponse(ctx.tx_buf, ctx.tx_buf_size, ctx.tx_len,
SpiDefs::kRspError, &err, 1U);
return;
}
const uint8_t requested = ctx.payload[0];
CanFd::Mode target;
switch (requested) {
case 0U: target = CanFd::Mode::kModeAllPass; break;
case 1U: target = CanFd::Mode::kModeFilterId; break;
default: {
uint8_t err = 0xE1U;
bus.buildResponse(ctx.tx_buf, ctx.tx_buf_size, ctx.tx_len,
SpiDefs::kRspError, &err, 1U);
return;
}
}
CanFd::setMode(target);
const uint8_t actual = static_cast<uint8_t>(CanFd::mode());
#if (CONFIG_SPI_APP_LOG_ENABLE != 0)
char log_msg[AppSpiCommand::kLogBufSize] = {};
(void)snprintf(log_msg, sizeof(log_msg),
"CAN_MODE set=%u -> actual=%u",
static_cast<unsigned>(requested),
static_cast<unsigned>(actual));
bus.postLog(log_msg);
#endif
bus.buildResponse(ctx.tx_buf, ctx.tx_buf_size, ctx.tx_len,
SpiDefs::kRspAck, &actual, 1U);
}
};
static SpiCanFeedbackHandler s_handler;
} // namespace
void registerSpiCanFeedbackCommand(AppSpiCommand &bus)
{
bus.registerHandler(&s_handler);
}
@@ -0,0 +1,135 @@
/**
* @file app_spi_cmd_id_snapshot.cpp
* @brief SPI 命令处理器:kTypeIdSnapshot (0xC3)。
*
* ESP32 分页拉取指定 CAN 通道的不重复 ID 快照表。
*
* 请求 payload:
* [0] channel 0/1/2
* [1] page 页号(0 = 条目 0~7,1 = 条目 8~15)
*
* 应答 payload(变长,最大 kTxBufSize - 5 = 123 字节):
* [0] channel
* [1] page
* [2] total_count 该通道当前条目总数
* [3] entry_count 本页实际条目数
* [4..] 条目逐条拼接:
* id(4LE) + seq(4LE) + rx_count(4LE) + dlc(1) + flags(1) + data(dlc)
* 每条 = 16 + dlc 字节
*/
#include "device/spi/spi_command_handler.h"
#include "device/spi/app_spi_command.h"
#include "device/spi/app_spi_command_defs.h"
#include "device/canfd.h"
#include <cstdio>
#include <cstring>
namespace {
constexpr uint8_t kPageEntries = 8U;
constexpr uint16_t kMaxPayloadLen = 123U; /* kTxBufSize=128 - overhead=5 */
inline void writeU32Le(uint8_t *dst, uint32_t val)
{
dst[0] = static_cast<uint8_t>(val & 0xFFU);
dst[1] = static_cast<uint8_t>((val >> 8) & 0xFFU);
dst[2] = static_cast<uint8_t>((val >> 16) & 0xFFU);
dst[3] = static_cast<uint8_t>((val >> 24) & 0xFFU);
}
class SpiIdSnapshotHandler final : public ISpiCommandHandler {
public:
bool canHandle(uint8_t frame_type) const override
{
return frame_type == SpiDefs::kTypeIdSnapshot;
}
void handle(AppSpiCommand &bus, const SpiCommandContext &ctx) override
{
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 uint8_t page = ctx.payload[1];
if (channel >= 3U) {
uint8_t err = 0xE1U;
bus.buildResponse(ctx.tx_buf, ctx.tx_buf_size, ctx.tx_len,
SpiDefs::kRspError, &err, 1U);
return;
}
CanFd::IdSnapshotTable table{};
if (!CanFd::copyIdSnapshotTable(channel, &table)) {
uint8_t err = 0xE2U;
bus.buildResponse(ctx.tx_buf, ctx.tx_buf_size, ctx.tx_len,
SpiDefs::kRspError, &err, 1U);
return;
}
const uint8_t start = static_cast<uint8_t>(page * kPageEntries);
if (start >= CANFD_ID_SNAPSHOT_MAX) {
uint8_t err = 0xE3U;
bus.buildResponse(ctx.tx_buf, ctx.tx_buf_size, ctx.tx_len,
SpiDefs::kRspError, &err, 1U);
return;
}
uint8_t payload[kMaxPayloadLen] = {};
payload[0] = channel;
payload[1] = page;
payload[2] = table.count;
uint16_t offset = 4U; /* [0]=ch [1]=page [2]=total [3]=entry_count(后填) */
uint8_t packed = 0U;
const uint8_t end = static_cast<uint8_t>(
(start + kPageEntries < CANFD_ID_SNAPSHOT_MAX)
? (start + kPageEntries) : CANFD_ID_SNAPSHOT_MAX);
for (uint8_t i = start; i < end; ++i) {
const auto &e = table.entries[i];
if (!e.occupied) continue;
const uint8_t dataLen = (e.dlc < CANFD_ID_SNAPSHOT_DATA_MAX)
? e.dlc : static_cast<uint8_t>(CANFD_ID_SNAPSHOT_DATA_MAX);
const uint16_t entrySize = 16U + dataLen; /* 4+4+4+1+1+1+1 + data */
if (offset + entrySize > kMaxPayloadLen) break; /* 装不下就截断 */
writeU32Le(&payload[offset], e.id);
writeU32Le(&payload[offset + 4U], e.seq);
writeU32Le(&payload[offset + 8U], e.rx_count);
payload[offset + 12U] = e.dlc;
/* flags 字节: bit0=fd, bit1=brs, bit2=esi */
payload[offset + 13U] = static_cast<uint8_t>(
(e.fd_flag ? 1U : 0U) | (e.brs ? 2U : 0U) | (e.esi ? 4U : 0U));
/* 保留 2 字节对齐 */
payload[offset + 14U] = 0U;
payload[offset + 15U] = 0U;
memcpy(&payload[offset + 16U], e.data, dataLen);
offset += entrySize;
packed++;
}
payload[3] = packed;
bus.buildResponse(ctx.tx_buf, ctx.tx_buf_size, ctx.tx_len,
SpiDefs::kRspAck, payload, offset);
}
};
static SpiIdSnapshotHandler s_handler;
} // namespace
void registerSpiIdSnapshotCommand(AppSpiCommand &bus)
{
bus.registerHandler(&s_handler);
}
@@ -1,4 +1,4 @@
/**
/**
* @file app_spi_command.cpp
* @brief APP 运行期 SPI 链路与命令路由实现。
*
@@ -303,6 +303,8 @@ extern void registerSpiBootRequestCommand(AppSpiCommand &bus);
extern void registerSpiOtaWaitCommand(AppSpiCommand &bus);
extern void registerSpiAppPingCommand(AppSpiCommand &bus);
extern void registerSpiCanBridgeCommand(AppSpiCommand &bus);
extern void registerSpiCanFeedbackCommand(AppSpiCommand &bus);
extern void registerSpiIdSnapshotCommand(AppSpiCommand &bus);
AppSpiCommand AppSpiCommandBus;
void AppSpiCommand::init()
{
@@ -320,6 +322,8 @@ void AppSpiCommand::init()
registerSpiOtaWaitCommand(AppSpiCommandBus);
registerSpiAppPingCommand(AppSpiCommandBus);
registerSpiCanBridgeCommand(AppSpiCommandBus);
registerSpiCanFeedbackCommand(AppSpiCommandBus);
registerSpiIdSnapshotCommand(AppSpiCommandBus);
configureGpio();
configureSpi0();
@@ -58,7 +58,7 @@ public:
const uint8_t *payload, uint16_t payload_len);
static constexpr uint16_t kRxBufSize = 128U;
static constexpr uint16_t kTxBufSize = 64U;
static constexpr uint16_t kTxBufSize = 128U;
static constexpr uint16_t kLogBufSize = 256U;
private:
@@ -23,6 +23,9 @@ static constexpr uint8_t kTypeOtaCancel = 0x03U;
static constexpr uint8_t kTypeAppPing = 0xA1U;
static constexpr uint8_t kTypeBootReq = 0xB0U;
static constexpr uint8_t kTypeCanSend = 0xC0U;
static constexpr uint8_t kTypeCanFeedback = 0xC1U; /**< ESP32 -> GD32 轮询 CAN 反馈快照 */
static constexpr uint8_t kTypeCanSetMode = 0xC2U; /**< ESP32 -> GD32 切换 CAN 接收过滤模式 */
static constexpr uint8_t kTypeIdSnapshot = 0xC3U; /**< ESP32 -> GD32 拉取不重复 ID 快照表 */
/* 响应类型 */
static constexpr uint8_t kRspAck = 0x80U;
+1 -1
View File
@@ -74,7 +74,7 @@ private:
static bool putCharNonBlocking(int ch);
static constexpr uint16_t kTxBufSize = 256U;
static constexpr uint16_t kTxBufSize = 1024U;
static volatile uint32_t s_dropCount;
static uint8_t s_txBuf[kTxBufSize];
static volatile uint16_t s_txHead;
+17 -6
View File
@@ -102,7 +102,7 @@
#define CONFIG_TASK_PERIOD_HEARTBEAT_TICKS 10000U
/** @brief 周期温度/电压 UART 打印开关:0=关闭,1=开启。 */
#define CONFIG_UART_TEMP_PERIODIC_PRINT 0
#define CONFIG_UART_TEMP_PERIODIC_PRINT 1
/** @brief UART 诊断打印级别:0=仅温度/电压,1=温度/电压+CAN 统计。 */
#define CONFIG_UART_DIAG_LEVEL 1
/** @brief SPI 命令逐帧日志开关:0=关闭,1=开启。高速 CAN 桥接时应关闭。 */
@@ -129,7 +129,7 @@
/** @brief APP 次版本号。 */
#define APP_FW_VERSION_MINOR 1U
/** @brief APP 补丁版本号。 */
#define APP_FW_VERSION_PATCH 0U
#define APP_FW_VERSION_PATCH 2U
/** @brief APP 构建编号。 */
#define APP_FW_VERSION_BUILD 0U
@@ -175,13 +175,24 @@
/* ============================================================
* EtherCAT LAN9252 使能与配置
* ============================================================ */
/** @brief EtherCAT 使能开关:0=禁用(未焊接 LAN9252),1=启用(焊接后)。
* 禁用时 comm/ 全部代码不编译,初始化不调用 EtherCAT,EXMC/EXTI0-2 不配置。 */
#define CONFIG_ETHERCAT_ENABLE 0
/** @brief SYNC0 同步模式:0=EXTI 外部触发(ESC 硬件同步,默认),1=TIMER0 降级(DC 未同步时)。 */
#define CONFIG_ETHERCAT_SYNC_MODE 0
/** @brief periodicIsr 最大执行时间告警阈值(µs)。实测超过此值需评估减小 PDO 大小或优化 EXMC 时序。
* 当前阈值 5µs:基于 32 字节 PDO + EXMC 6 HCLK 时序的估算上限。
* preempt=0 ISR 阻塞窗口 ≤ 此阈值时,CAN/SPI 中断需容忍。 */
#define CONFIG_ETHERCAT_ISR_MAX_US_THRESHOLD 5U
/** @brief 输入 PDO(TxPDO)在 PDI 内存区的起始偏移。
* 对应 LAN9252 SyncManager 内存映射,需与主站 ESI 配置匹配。
* @note 占位值,实际数值待主站配置确定后在集成阶段填入。 */
#define CONFIG_ESC_PDI_INPUT_OFFSET 0x1000U
/** @brief 输出 PDO(RxPDO)在 PDI 内存区的起始偏移。
* 对应 LAN9252 SyncManager 内存映射,需与主站 ESI 配置匹配。
* @note 占位值,实际数值待主站配置确定后在集成阶段填入。 */
#define CONFIG_ESC_PDI_OUTPUT_OFFSET 0x1200U
/* ---------- EXMC 引脚配置(GD32E508 EXMC 标准映射) ---------- */
/** @brief EXMC NOR/SRAM Region1 基址(NE1 片选)。 */
#define CONFIG_ESC_EXMC_BASE_ADDR 0x60000000U
+10 -6
View File
@@ -16,10 +16,8 @@
#include "sys/ota_boot_request.h"
#include "sys/task_manager.h"
#include "sys/config.h"
#if CONFIG_ETHERCAT_ENABLE
#include "comm/ethercat_slave.h"
#include "comm/ethercat_sync.h"
#endif
/**
* @brief 执行应用级外设与调度初始化。
@@ -30,6 +28,10 @@
*/
void AppInitializer::init()
{
/* 必须最先设置 NVIC 优先级分组:确保后续 CanFd/SPI0 等外设 init 中
* 编码抢占/子优先级字节时使用正确的分组(PRE4_SUB0),避免在错误分组
* 下编码后被重新解释。EtherCAT 实时架构整改 Task3。 */
nvic_priority_group_set(CONFIG_NVIC_PRIORITY_GROUP);
printf("[APP] init: SysTick...\r\n");
SysTickTimer::initMs();
printf("[APP] init: USART0...\r\n");
@@ -47,12 +49,14 @@ void AppInitializer::init()
TaskManager::init();
printf("[APP] init: SchedulerTimer...\r\n");
SchedulerTimer::init();
#if CONFIG_ETHERCAT_ENABLE
printf("[APP] init: EthercatSlave...\r\n");
EthercatSlave::init();
printf("[APP] init: EthercatSync...\r\n");
EthercatSync::init();
#endif
if (EthercatSlave::isEscReady()) {
printf("[APP] init: EthercatSync...\r\n");
EthercatSync::init();
} else {
printf("[APP] init: EthercatSync SKIPPED (ESC not ready)\r\n");
}
printf("[APP] init: Watchdog...\r\n");
Watchdog::init();
printf("[APP] init: all done\r\n");
+29 -8
View File
@@ -1,4 +1,4 @@
/**
/**
* @file task_runner.cpp
* @brief 任务执行器实现。
*/
@@ -13,9 +13,7 @@
#include "sys/ota_boot_request.h"
#include "sys/task_manager.h"
#include "sys/task_runner.h"
#if CONFIG_ETHERCAT_ENABLE
#include "comm/ethercat_slave.h"
#endif
namespace {
constexpr uint32_t kAdcFilterWindow = CONFIG_ADC_FILTER_SIZE + 1U;
@@ -214,7 +212,7 @@ void TaskRunner::taskStatusLed() { /* 预留 */ }
* @retval 无返回值。
* @note 当前仅保留任务入口。
*/
void TaskRunner::taskHeartbeat() { OtaBootRequest::process(); }
void TaskRunner::taskHeartbeat() { OtaBootRequest::process(); CanFd::refreshFeedbackSnapshot(); }
/**
* @brief 输出当前温度、电压与 CAN 诊断信息。
@@ -266,6 +264,23 @@ void TaskRunner::taskDiagnosticPrint()
static_cast<unsigned long>(diag->tx_abort_count),
static_cast<unsigned long>(diag->error_count));
#endif
{
if (EthercatSlave::isEscReady()) {
uint32_t maxCyc = 0U, avgCyc = 0U, cnt = 0U;
EthercatSlave::getIsrStats(&maxCyc, &avgCyc, &cnt);
/* 180MHz: cycles / 180 = µs;用 *1000/180 获得毫微秒精度避免浮点 */
const uint32_t maxUs_x1000 = maxCyc * 1000U / 180U;
const uint32_t avgUs_x1000 = avgCyc * 1000U / 180U;
printf("[ECAT] periodicIsr cnt=%lu max=%lu.%03luus avg=%lu.%03luus\r\n",
(unsigned long)cnt,
(unsigned long)(maxUs_x1000 / 1000U), (unsigned long)(maxUs_x1000 % 1000U),
(unsigned long)(avgUs_x1000 / 1000U), (unsigned long)(avgUs_x1000 % 1000U));
} else {
printf("[ECAT] ESC not detected (chip id=0x%08lX), EtherCAT disabled\r\n",
(unsigned long)EthercatSlave::getChipId());
}
}
}
/**
@@ -289,11 +304,7 @@ void TaskRunner::taskSpiCmdProcess()
*/
void TaskRunner::taskEthercatProcess()
{
#if CONFIG_ETHERCAT_ENABLE
EthercatSlave::process();
#else
/* 未使能 EtherCAT 时此分支不应被置位;保留空操作以避免链接错误。 */
#endif
}
/**
@@ -328,6 +339,16 @@ void TaskRunner::runPending()
{
Watchdog::feed();
/* 周期路径( SYNC0 ISR preempt=0 )用 s_periodicSeq 序列号通知主循环,
* 不再走 task_flags,避免 BASEPRI 临界区与 preempt=0 的 RMW 竞态。
* 优先于 takePending() 处理,确保周期数据及时消费。
* ESC 未焊接时 selfTest 失败,isEscReady()=false,中断未使能,
* s_periodicSeq 不会递增,此分支自然不触发——双重守卫仅为防御性编程。 */
if (EthercatSlave::isEscReady() && EthercatSlave::hasPendingPeriodic()) {
EthercatSlave::markPeriodicProcessed();
taskEthercatProcess();
}
const uint32_t pending = TaskManager::takePending();
if(pending == 0U) return;