2026-05-10 17:28:14 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* @file CANFD.cpp
|
|
|
|
|
|
* @brief CANFD 通信模块实现。
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
#include "main.h"
|
|
|
|
|
|
#include "device/canfd.h"
|
|
|
|
|
|
#include "sys/config.h"
|
2026-05-30 00:24:31 +08:00
|
|
|
|
#include "sys/task_manager.h"
|
2026-06-25 01:11:31 +08:00
|
|
|
|
#include "sys/systick.h"
|
2026-05-10 17:28:14 +08:00
|
|
|
|
|
|
|
|
|
|
namespace {
|
2026-05-10 21:27:51 +08:00
|
|
|
|
struct TxFrame {
|
2026-05-10 17:28:14 +08:00
|
|
|
|
can_trasnmit_message_struct tx_message;
|
|
|
|
|
|
uint8_t payload[64];
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-05-10 21:27:51 +08:00
|
|
|
|
struct TxQueue {
|
|
|
|
|
|
TxFrame frames[4];
|
|
|
|
|
|
uint8_t head;
|
|
|
|
|
|
uint8_t tail;
|
|
|
|
|
|
uint8_t count;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
struct CanChannelConfig {
|
|
|
|
|
|
uint32_t canPeriph;
|
|
|
|
|
|
rcu_periph_enum canRcu;
|
|
|
|
|
|
uint32_t txPort;
|
|
|
|
|
|
uint32_t txPin;
|
|
|
|
|
|
uint32_t rxPort;
|
|
|
|
|
|
uint32_t rxPin;
|
|
|
|
|
|
uint32_t slpPort;
|
|
|
|
|
|
uint32_t slpPin;
|
|
|
|
|
|
IRQn_Type rx0Irq;
|
|
|
|
|
|
uint8_t filterNumber;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
constexpr CanChannelConfig kChannels[] = {
|
|
|
|
|
|
{ CAN0, RCU_CAN0, GPIOA, GPIO_PIN_12, GPIOA, GPIO_PIN_11, GPIOA, GPIO_PIN_8, CAN0_RX0_IRQn, 0U },
|
|
|
|
|
|
{ CAN1, RCU_CAN1, GPIOB, GPIO_PIN_12, GPIOB, GPIO_PIN_13, GPIOB, GPIO_PIN_14, CAN1_RX0_IRQn, 1U },
|
|
|
|
|
|
{ CAN2, RCU_CAN2, GPIOE, GPIO_PIN_1, GPIOE, GPIO_PIN_0, GPIOB, GPIO_PIN_9, CAN2_RX0_IRQn, 2U },
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
constexpr size_t kChannelCount = sizeof(kChannels) / sizeof(kChannels[0]);
|
|
|
|
|
|
constexpr uint8_t kTxQueueDepth = 4U;
|
2026-05-30 00:24:31 +08:00
|
|
|
|
constexpr uint32_t kCanRxParseTaskMask = (1UL << TASK_CAN_RX_PARSE);
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @brief Enter a critical section and preserve the previous interrupt mask.
|
|
|
|
|
|
* @param[in] 无。
|
|
|
|
|
|
* @param[out] 无。
|
|
|
|
|
|
* @retval uint32_t Previous PRIMASK value.
|
|
|
|
|
|
*/
|
|
|
|
|
|
static uint32_t enterCriticalSection()
|
|
|
|
|
|
{
|
|
|
|
|
|
const uint32_t primask = __get_PRIMASK();
|
|
|
|
|
|
__disable_irq();
|
|
|
|
|
|
return primask;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @brief Exit a critical section by restoring the saved interrupt mask.
|
|
|
|
|
|
* @param[in] primask Previous PRIMASK value returned by `enterCriticalSection()`.
|
|
|
|
|
|
* @param[out] 无。
|
|
|
|
|
|
* @retval 无返回值。
|
|
|
|
|
|
*/
|
|
|
|
|
|
static void exitCriticalSection(uint32_t primask)
|
|
|
|
|
|
{
|
|
|
|
|
|
__set_PRIMASK(primask);
|
|
|
|
|
|
}
|
2026-05-10 17:28:14 +08:00
|
|
|
|
} // namespace
|
|
|
|
|
|
|
2026-05-10 21:27:51 +08:00
|
|
|
|
CanFd::Diag CanFd::s_diag{};
|
|
|
|
|
|
CanFd::RxQueue CanFd::s_rxQueues[3]{};
|
|
|
|
|
|
uint8_t CanFd::s_nextConsumeChannel = 0U;
|
|
|
|
|
|
static TxQueue s_txQueues[3]{};
|
2026-05-10 17:28:14 +08:00
|
|
|
|
|
2026-05-10 21:27:51 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* @brief 初始化三路 CANFD 相关 GPIO。
|
|
|
|
|
|
* @param[in] 无。
|
|
|
|
|
|
* @param[out] 无。
|
|
|
|
|
|
* @retval 无返回值。
|
|
|
|
|
|
* @note 为 CAN0/CAN1/CAN2 的 TX、RX 及收发器 SLP 控制脚统一完成 GPIO 时钟与模式配置。
|
|
|
|
|
|
*/
|
2026-05-10 17:28:14 +08:00
|
|
|
|
void CanFd::initGpio()
|
|
|
|
|
|
{
|
|
|
|
|
|
rcu_periph_clock_enable(RCU_GPIOA);
|
2026-05-10 21:27:51 +08:00
|
|
|
|
rcu_periph_clock_enable(RCU_GPIOB);
|
|
|
|
|
|
rcu_periph_clock_enable(RCU_GPIOE);
|
2026-05-10 17:28:14 +08:00
|
|
|
|
rcu_periph_clock_enable(RCU_AF);
|
|
|
|
|
|
|
2026-05-10 21:27:51 +08:00
|
|
|
|
for(size_t i = 0; i < kChannelCount; ++i) {
|
|
|
|
|
|
const auto &ch = kChannels[i];
|
|
|
|
|
|
gpio_init(ch.rxPort, GPIO_MODE_IPU, GPIO_OSPEED_50MHZ, ch.rxPin);
|
|
|
|
|
|
gpio_init(ch.txPort, GPIO_MODE_AF_PP, GPIO_OSPEED_50MHZ, ch.txPin);
|
|
|
|
|
|
gpio_init(ch.slpPort, GPIO_MODE_OUT_PP, GPIO_OSPEED_50MHZ, ch.slpPin);
|
|
|
|
|
|
}
|
2026-05-10 17:28:14 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-10 21:27:51 +08:00
|
|
|
|
/**
|
2026-05-30 00:24:31 +08:00
|
|
|
|
* @brief 为指定 CAN 控制器配置接收过滤器。
|
2026-05-10 21:27:51 +08:00
|
|
|
|
* @param[in] canPeriph 目标 CAN 外设实例。
|
|
|
|
|
|
* @param[in] filterNumber 过滤器编号。
|
|
|
|
|
|
* @param[out] 无。
|
|
|
|
|
|
* @retval 无返回值。
|
2026-05-30 00:24:31 +08:00
|
|
|
|
* @note 当前默认按 `CONFIG_CAN_RX_FILTER_ID`/`CONFIG_CAN_RX_FILTER_MASK` 对标准帧做掩码匹配,并统一进入 FIFO0。
|
2026-05-10 21:27:51 +08:00
|
|
|
|
*/
|
2026-05-30 00:24:31 +08:00
|
|
|
|
void CanFd::configureRxFilter(uint32_t canPeriph, uint8_t filterNumber)
|
2026-05-10 17:28:14 +08:00
|
|
|
|
{
|
|
|
|
|
|
can_filter_parameter_struct can_filter{};
|
2026-05-10 21:27:51 +08:00
|
|
|
|
can_filter.filter_number = filterNumber;
|
2026-05-10 17:28:14 +08:00
|
|
|
|
can_filter.filter_mode = CAN_FILTERMODE_MASK;
|
|
|
|
|
|
can_filter.filter_bits = CAN_FILTERBITS_32BIT;
|
2026-05-30 00:24:31 +08:00
|
|
|
|
can_filter.filter_list_high = static_cast<uint16_t>((CONFIG_CAN_RX_FILTER_ID & 0x7FFU) << 5U);
|
2026-05-10 17:28:14 +08:00
|
|
|
|
can_filter.filter_list_low = 0x0000U;
|
2026-05-30 00:24:31 +08:00
|
|
|
|
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);
|
2026-05-10 17:28:14 +08:00
|
|
|
|
can_filter.filter_fifo_number = CAN_FIFO0;
|
|
|
|
|
|
can_filter.filter_enable = ENABLE;
|
2026-05-10 21:27:51 +08:00
|
|
|
|
can_filter_init(canPeriph, &can_filter);
|
2026-05-10 17:28:14 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-10 21:27:51 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* @brief 配置指定 CAN 控制器的标称仲裁段位时序。
|
|
|
|
|
|
* @param[in] canPeriph 目标 CAN 外设实例。
|
|
|
|
|
|
* @param[out] 无。
|
|
|
|
|
|
* @retval 无返回值。
|
|
|
|
|
|
* @note 当前按 1 Mbps 仲裁段参数完成普通 CAN 初始化。
|
|
|
|
|
|
*/
|
|
|
|
|
|
void CanFd::initNominalTiming(uint32_t canPeriph)
|
2026-05-10 17:28:14 +08:00
|
|
|
|
{
|
|
|
|
|
|
can_parameter_struct can_parameter{};
|
|
|
|
|
|
|
|
|
|
|
|
can_struct_para_init(CAN_INIT_STRUCT, &can_parameter);
|
|
|
|
|
|
can_parameter.time_triggered = DISABLE;
|
|
|
|
|
|
can_parameter.auto_bus_off_recovery = ENABLE;
|
|
|
|
|
|
can_parameter.auto_wake_up = DISABLE;
|
|
|
|
|
|
can_parameter.auto_retrans = ENABLE;
|
|
|
|
|
|
can_parameter.rec_fifo_overwrite = ENABLE;
|
|
|
|
|
|
can_parameter.trans_fifo_order = ENABLE;
|
|
|
|
|
|
can_parameter.working_mode = CAN_NORMAL_MODE;
|
|
|
|
|
|
can_parameter.resync_jump_width = CAN_BT_SJW_1TQ;
|
|
|
|
|
|
can_parameter.time_segment_1 = CAN_BT_BS1_7TQ;
|
|
|
|
|
|
can_parameter.time_segment_2 = CAN_BT_BS2_2TQ;
|
|
|
|
|
|
can_parameter.prescaler = 6U;
|
2026-05-10 21:27:51 +08:00
|
|
|
|
can_init(canPeriph, &can_parameter);
|
2026-05-10 17:28:14 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-10 21:27:51 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* @brief 配置指定 CAN 控制器的 CAN FD 数据段参数。
|
|
|
|
|
|
* @param[in] canPeriph 目标 CAN 外设实例。
|
|
|
|
|
|
* @param[out] 无。
|
|
|
|
|
|
* @retval 无返回值。
|
|
|
|
|
|
* @note 在 GD32E508 平台下启用 FD、BRS 与 TDC;非该平台则仅使能 FD 功能。
|
|
|
|
|
|
*/
|
|
|
|
|
|
void CanFd::initFdMode(uint32_t canPeriph)
|
2026-05-10 17:28:14 +08:00
|
|
|
|
{
|
|
|
|
|
|
#ifdef GD32E508
|
|
|
|
|
|
can_fdframe_struct can_fd_parameter{};
|
|
|
|
|
|
can_fd_tdc_struct can_fd_tdc_parameter{};
|
|
|
|
|
|
|
|
|
|
|
|
can_struct_para_init(CAN_FD_FRAME_STRUCT, &can_fd_parameter);
|
|
|
|
|
|
can_fd_parameter.fd_frame = ENABLE;
|
|
|
|
|
|
can_fd_parameter.excp_event_detect = ENABLE;
|
|
|
|
|
|
can_fd_parameter.delay_compensation = ENABLE;
|
|
|
|
|
|
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;
|
|
|
|
|
|
can_fd_parameter.p_delay_compensation = &can_fd_tdc_parameter;
|
|
|
|
|
|
can_fd_parameter.iso_bosch = CAN_FDMOD_ISO;
|
|
|
|
|
|
can_fd_parameter.esi_mode = CAN_ESIMOD_HARDWARE;
|
|
|
|
|
|
can_fd_parameter.data_prescaler = 6U;
|
|
|
|
|
|
can_fd_parameter.data_resync_jump_width = CAN_BT_SJW_1TQ;
|
|
|
|
|
|
can_fd_parameter.data_time_segment_1 = CAN_BT_BS1_3TQ;
|
|
|
|
|
|
can_fd_parameter.data_time_segment_2 = CAN_BT_BS2_1TQ;
|
2026-05-10 21:27:51 +08:00
|
|
|
|
can_fd_init(canPeriph, &can_fd_parameter);
|
2026-05-10 17:28:14 +08:00
|
|
|
|
#else
|
2026-05-10 21:27:51 +08:00
|
|
|
|
can_fd_function_enable(canPeriph);
|
2026-05-10 17:28:14 +08:00
|
|
|
|
#endif
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-10 21:27:51 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* @brief 构造内部发送缓冲中的状态帧内容。
|
|
|
|
|
|
* @param[in] status 指向状态载荷的指针。
|
|
|
|
|
|
* @param[out] 无。
|
|
|
|
|
|
* @retval 无返回值。
|
|
|
|
|
|
* @note 当前实现中状态帧构造逻辑已内联至 `enqueueTxFrame()`,此函数仅保留接口兼容性。
|
|
|
|
|
|
*/
|
2026-05-10 17:28:14 +08:00
|
|
|
|
void CanFd::buildStatusFrame(const StatusPayload *status)
|
|
|
|
|
|
{
|
2026-05-10 21:27:51 +08:00
|
|
|
|
if(status == nullptr) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-10 17:28:14 +08:00
|
|
|
|
const int16_t temp_x100 = static_cast<int16_t>(status->temperature * 100.0f);
|
|
|
|
|
|
const uint16_t vdda_x1000 = static_cast<uint16_t>(status->vdda * 1000.0f);
|
|
|
|
|
|
|
2026-05-10 21:27:51 +08:00
|
|
|
|
auto &frame = s_txQueues[0].frames[0];
|
|
|
|
|
|
auto &p = frame.payload;
|
2026-05-10 17:28:14 +08:00
|
|
|
|
p[0] = static_cast<uint8_t>(status->temp_raw & 0xFFU);
|
|
|
|
|
|
p[1] = static_cast<uint8_t>((status->temp_raw >> 8) & 0xFFU);
|
|
|
|
|
|
p[2] = static_cast<uint8_t>(status->vref_raw & 0xFFU);
|
|
|
|
|
|
p[3] = static_cast<uint8_t>((status->vref_raw >> 8) & 0xFFU);
|
|
|
|
|
|
p[4] = static_cast<uint8_t>(temp_x100 & 0xFF);
|
|
|
|
|
|
p[5] = static_cast<uint8_t>((temp_x100 >> 8) & 0xFF);
|
|
|
|
|
|
p[6] = static_cast<uint8_t>(vdda_x1000 & 0xFFU);
|
|
|
|
|
|
p[7] = static_cast<uint8_t>((vdda_x1000 >> 8) & 0xFFU);
|
|
|
|
|
|
p[8] = CONFIG_CAN_STATUS_TAG0;
|
|
|
|
|
|
p[9] = CONFIG_CAN_STATUS_TAG1;
|
|
|
|
|
|
p[10] = CONFIG_CAN_STATUS_TAG2;
|
|
|
|
|
|
p[11] = CONFIG_CAN_STATUS_TAG3;
|
2026-05-10 21:27:51 +08:00
|
|
|
|
}
|
2026-05-10 17:28:14 +08:00
|
|
|
|
|
2026-05-10 21:27:51 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* @brief 向指定 CAN 通道的发送 FIFO 入队一帧状态报文。
|
|
|
|
|
|
* @param[in] channelIndex 目标 CAN 通道索引(0/1/2)。
|
|
|
|
|
|
* @param[in] status 指向状态载荷的指针。
|
|
|
|
|
|
* @param[out] 无。
|
|
|
|
|
|
* @retval bool true 表示成功入队,false 表示参数无效或该通道发送队列已满。
|
|
|
|
|
|
* @note 仅完成软件发送队列入队,不会立即触发硬件发送。
|
|
|
|
|
|
*/
|
|
|
|
|
|
bool CanFd::enqueueTxFrame(uint8_t channelIndex, const StatusPayload *status)
|
|
|
|
|
|
{
|
|
|
|
|
|
if(status == nullptr || channelIndex >= kChannelCount) {
|
|
|
|
|
|
return false;
|
2026-05-10 17:28:14 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-10 21:27:51 +08:00
|
|
|
|
auto &queue = s_txQueues[channelIndex];
|
|
|
|
|
|
if(queue.count >= kTxQueueDepth) {
|
|
|
|
|
|
s_diag.tx_drop_count++;
|
|
|
|
|
|
return false;
|
2026-05-10 17:28:14 +08:00
|
|
|
|
}
|
2026-05-10 21:27:51 +08:00
|
|
|
|
|
2026-05-10 17:28:14 +08:00
|
|
|
|
buildStatusFrame(status);
|
|
|
|
|
|
|
2026-05-10 21:27:51 +08:00
|
|
|
|
auto &frame = queue.frames[queue.tail];
|
|
|
|
|
|
can_struct_para_init(CAN_TX_MESSAGE_STRUCT, &frame.tx_message);
|
|
|
|
|
|
frame.tx_message.tx_sfid = CONFIG_CAN_STATUS_ID;
|
|
|
|
|
|
frame.tx_message.tx_efid = 0U;
|
|
|
|
|
|
frame.tx_message.tx_ff = CAN_FF_STANDARD;
|
|
|
|
|
|
frame.tx_message.tx_ft = CAN_FT_DATA;
|
|
|
|
|
|
frame.tx_message.tx_dlen = CANFD_STATUS_DLEN;
|
|
|
|
|
|
#ifdef GD32E508
|
|
|
|
|
|
frame.tx_message.fd_flag = ENABLE;
|
|
|
|
|
|
frame.tx_message.fd_brs = ENABLE;
|
|
|
|
|
|
frame.tx_message.fd_esi = CAN_ESI_DOMINANT;
|
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
|
|
const auto &src = s_txQueues[0].frames[0].payload;
|
|
|
|
|
|
auto &p = frame.payload;
|
|
|
|
|
|
for(uint8_t j = 0; j < CANFD_STATUS_DLEN; ++j) {
|
|
|
|
|
|
p[j] = src[j];
|
|
|
|
|
|
frame.tx_message.tx_data[j] = p[j];
|
2026-05-10 17:28:14 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-10 21:27:51 +08:00
|
|
|
|
queue.tail = static_cast<uint8_t>((queue.tail + 1U) % kTxQueueDepth);
|
|
|
|
|
|
queue.count++;
|
2026-05-10 17:28:14 +08:00
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-10 21:27:51 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* @brief 初始化三路 CANFD 控制器、过滤器、中断与软件队列。
|
|
|
|
|
|
* @param[in] 无。
|
|
|
|
|
|
* @param[out] 无。
|
|
|
|
|
|
* @retval 无返回值。
|
2026-05-30 00:24:31 +08:00
|
|
|
|
* @note 初始化后默认将三路 TCAN1057A 收发器置为 Normal,并使能 FIFO0 的非空/满/overfull 中断统计。
|
2026-05-10 21:27:51 +08:00
|
|
|
|
*/
|
|
|
|
|
|
void CanFd::init()
|
|
|
|
|
|
{
|
|
|
|
|
|
s_diag = {};
|
|
|
|
|
|
s_nextConsumeChannel = 0U;
|
|
|
|
|
|
|
|
|
|
|
|
initGpio();
|
|
|
|
|
|
|
|
|
|
|
|
for(size_t i = 0; i < kChannelCount; ++i) {
|
|
|
|
|
|
const auto &ch = kChannels[i];
|
|
|
|
|
|
rcu_periph_clock_enable(ch.canRcu);
|
|
|
|
|
|
gpio_bit_reset(ch.slpPort, ch.slpPin); /* STB=0 -> Normal */
|
|
|
|
|
|
can_deinit(ch.canPeriph);
|
|
|
|
|
|
initNominalTiming(ch.canPeriph);
|
|
|
|
|
|
initFdMode(ch.canPeriph);
|
2026-05-30 00:24:31 +08:00
|
|
|
|
configureRxFilter(ch.canPeriph, ch.filterNumber);
|
|
|
|
|
|
can_interrupt_enable(ch.canPeriph, CAN_INT_RFNE0 | CAN_INT_RFF0 | CAN_INT_RFO0);
|
2026-05-10 21:27:51 +08:00
|
|
|
|
nvic_irq_enable(ch.rx0Irq, 1U, 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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
s_nextConsumeChannel = 0U;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @brief 执行兼容保留的 CAN 轮询入口。
|
|
|
|
|
|
* @param[in] 无。
|
|
|
|
|
|
* @param[out] 无。
|
|
|
|
|
|
* @retval 无返回值。
|
|
|
|
|
|
* @note 当前三路接收均由 RX0 中断驱动,该函数仅保留旧调用兼容性。
|
|
|
|
|
|
*/
|
|
|
|
|
|
void CanFd::poll()
|
|
|
|
|
|
{
|
|
|
|
|
|
/* 兼容保留:三通道接收已由 RX0 中断驱动 */
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @brief 根据状态载荷为三路发送队列批量准备状态报文。
|
|
|
|
|
|
* @param[in] status 指向状态载荷的指针;为空时直接返回。
|
|
|
|
|
|
* @param[out] 无。
|
|
|
|
|
|
* @retval 无返回值。
|
|
|
|
|
|
* @note 当前默认不会周期性调用;仅在外部显式触发发送测试或业务发送时使用。
|
|
|
|
|
|
*/
|
|
|
|
|
|
void CanFd::prepareStatus(const StatusPayload *status)
|
|
|
|
|
|
{
|
|
|
|
|
|
if(status == nullptr) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
for(size_t i = 0; i < kChannelCount; ++i) {
|
|
|
|
|
|
(void)enqueueTxFrame(static_cast<uint8_t>(i), status);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @brief 根据状态载荷仅为指定通道准备一帧状态报文。
|
|
|
|
|
|
* @param[in] channelIndex 目标 CAN 通道索引(0/1/2)。
|
|
|
|
|
|
* @param[in] status 指向状态载荷的指针;为空时返回失败。
|
|
|
|
|
|
* @param[out] 无。
|
|
|
|
|
|
* @retval bool true 表示成功入队,false 表示参数非法或发送队列已满。
|
|
|
|
|
|
*/
|
|
|
|
|
|
bool CanFd::prepareStatusForChannel(uint8_t channelIndex, const StatusPayload *status)
|
|
|
|
|
|
{
|
|
|
|
|
|
return enqueueTxFrame(channelIndex, status);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @brief 将当前各通道已准备好的状态报文送入硬件邮箱发送。
|
|
|
|
|
|
* @param[in] 无。
|
|
|
|
|
|
* @param[out] 无。
|
|
|
|
|
|
* @retval 无返回值。
|
|
|
|
|
|
* @note 当前默认不会周期性调用;仅处理调用方显式准备过的发送队列内容。
|
|
|
|
|
|
*/
|
|
|
|
|
|
void CanFd::sendPrepared()
|
|
|
|
|
|
{
|
|
|
|
|
|
for(size_t i = 0; i < kChannelCount; ++i) {
|
|
|
|
|
|
auto &queue = s_txQueues[i];
|
|
|
|
|
|
if(queue.count == 0U) {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
auto &frame = queue.frames[queue.head];
|
|
|
|
|
|
const uint8_t mailbox = can_message_transmit(kChannels[i].canPeriph, &frame.tx_message);
|
|
|
|
|
|
if(mailbox != CAN_NOMAILBOX) {
|
|
|
|
|
|
s_diag.tx_count++;
|
|
|
|
|
|
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);
|
|
|
|
|
|
queue.head = static_cast<uint8_t>((queue.head + 1U) % kTxQueueDepth);
|
|
|
|
|
|
queue.count--;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
s_diag.error_count++;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @brief 查询三路 CAN 通道是否存在待消费接收报文。
|
|
|
|
|
|
* @param[in] 无。
|
|
|
|
|
|
* @param[out] 无。
|
|
|
|
|
|
* @retval bool true 表示至少一条接收队列非空,false 表示当前三路均无待消费报文。
|
|
|
|
|
|
*/
|
|
|
|
|
|
bool CanFd::hasRxFrame()
|
|
|
|
|
|
{
|
2026-05-30 00:24:31 +08:00
|
|
|
|
bool found = false;
|
|
|
|
|
|
const uint32_t primask = enterCriticalSection();
|
2026-05-10 21:27:51 +08:00
|
|
|
|
for(size_t i = 0; i < kChannelCount; ++i) {
|
|
|
|
|
|
if(s_rxQueues[i].count != 0U) {
|
2026-05-30 00:24:31 +08:00
|
|
|
|
found = true;
|
|
|
|
|
|
break;
|
2026-05-10 21:27:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-05-30 00:24:31 +08:00
|
|
|
|
exitCriticalSection(primask);
|
|
|
|
|
|
return found;
|
2026-05-10 21:27:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @brief 轮转读取并消费一帧接收报文。
|
|
|
|
|
|
* @param[in] 无。
|
|
|
|
|
|
* @param[out] id 输出报文标识符;允许为空。
|
|
|
|
|
|
* @param[out] data 输出报文数据缓存;允许为空。
|
|
|
|
|
|
* @param[out] length 输出报文长度;允许为空。
|
|
|
|
|
|
* @param[out] channel 输出报文来源通道号;允许为空。
|
|
|
|
|
|
* @retval bool true 表示成功消费一帧报文,false 表示当前三路接收队列均为空。
|
|
|
|
|
|
* @note 消费顺序采用从 `s_nextConsumeChannel` 开始的轮转扫描,避免长期偏向固定通道。
|
|
|
|
|
|
*/
|
|
|
|
|
|
bool CanFd::consumeRxFrame(uint32_t *id, uint8_t *data, uint8_t *length, uint8_t *channel)
|
|
|
|
|
|
{
|
|
|
|
|
|
RxFrame localFrame{};
|
|
|
|
|
|
bool found = false;
|
|
|
|
|
|
|
2026-05-30 00:24:31 +08:00
|
|
|
|
const uint32_t primask = enterCriticalSection();
|
2026-05-10 21:27:51 +08:00
|
|
|
|
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) {
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
2026-05-30 00:24:31 +08:00
|
|
|
|
exitCriticalSection(primask);
|
2026-05-10 21:27:51 +08:00
|
|
|
|
|
|
|
|
|
|
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];
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-13 17:07:25 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* @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 桥接等即时发送场景。
|
|
|
|
|
|
*/
|
|
|
|
|
|
bool CanFd::transmitFrame(uint8_t channelIndex, uint32_t ext_id,
|
|
|
|
|
|
const uint8_t *data, uint8_t dlc)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (channelIndex >= kChannelCount || data == nullptr || dlc == 0U || dlc > 64U) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
can_trasnmit_message_struct tx_msg{};
|
|
|
|
|
|
can_struct_para_init(CAN_TX_MESSAGE_STRUCT, &tx_msg);
|
|
|
|
|
|
tx_msg.tx_efid = ext_id;
|
|
|
|
|
|
tx_msg.tx_ff = CAN_FF_EXTENDED;
|
|
|
|
|
|
tx_msg.tx_ft = CAN_FT_DATA;
|
|
|
|
|
|
tx_msg.tx_dlen = dlc;
|
|
|
|
|
|
#ifdef GD32E508
|
|
|
|
|
|
tx_msg.fd_flag = (dlc > 8U) ? ENABLE : DISABLE;
|
|
|
|
|
|
tx_msg.fd_brs = (dlc > 8U) ? ENABLE : DISABLE;
|
|
|
|
|
|
tx_msg.fd_esi = CAN_ESI_DOMINANT;
|
|
|
|
|
|
#endif
|
|
|
|
|
|
for (uint8_t i = 0U; i < dlc; ++i) {
|
|
|
|
|
|
tx_msg.tx_data[i] = data[i];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-25 01:11:31 +08:00
|
|
|
|
const uint32_t canPeriph = kChannels[channelIndex].canPeriph;
|
|
|
|
|
|
uint8_t mailbox = can_message_transmit(canPeriph, &tx_msg);
|
|
|
|
|
|
|
|
|
|
|
|
/* 邮箱全占时的恢复处理:
|
|
|
|
|
|
* 当目标控制器初始化时 auto_retrans = ENABLE,且总线上没有其它节点回 ACK,
|
|
|
|
|
|
* 已入邮箱的帧会无限自动重传,TME 位永不置回,导致后续帧一律 CAN_NOMAILBOX。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 改良策略(两段式,尽量不丢帧):
|
|
|
|
|
|
* 1) 先短自旋 ~2 ms 轮询邮箱状态。健康总线上 1 Mbps + 8 字节帧的实际发送
|
|
|
|
|
|
* 仅约 130 µs,正常重传几百 µs 内即可 drain,绝大多数“偶发 NOMAILBOX”
|
|
|
|
|
|
* 场景到这一步就已经恢复,不会触发 abort,也就不会丢帧。
|
|
|
|
|
|
* 2) 若 2 ms 后仍全部 PENDING(典型:物理层不通、TCAN1057A 没焊或 CAN_L/H
|
|
|
|
|
|
* 断线导致 ACK Error 死循环),只 abort 第一个 PENDING 邮箱腾一个位置,
|
|
|
|
|
|
* 最坏丢 1 帧而不是 3 帧,并通过 s_diag.tx_abort_count 暴露给串口 diag,
|
|
|
|
|
|
* 上层(含 SPI 桥)仍能把后续帧正常下发。 */
|
|
|
|
|
|
if (mailbox == CAN_NOMAILBOX) {
|
|
|
|
|
|
constexpr uint8_t kDrainPolls = 2U;
|
|
|
|
|
|
for (uint8_t poll = 0U; poll < kDrainPolls && mailbox == CAN_NOMAILBOX; ++poll) {
|
|
|
|
|
|
SysTickTimer::delayMs(1U);
|
|
|
|
|
|
mailbox = can_message_transmit(canPeriph, &tx_msg);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (mailbox == CAN_NOMAILBOX) {
|
|
|
|
|
|
/* 只 abort 一个 PENDING 邮箱:找到首个 stop 成功的就停手,
|
|
|
|
|
|
* 不再扫描后续邮箱,把丢帧上限收敛到 1。stop 失败的邮箱
|
|
|
|
|
|
* (硬件正在物理发送)继续尝试下一个,避免一次 abort 都没成功。 */
|
|
|
|
|
|
for (uint8_t mb = CAN_MAILBOX0; mb <= CAN_MAILBOX2; ++mb) {
|
|
|
|
|
|
if (can_transmit_states(canPeriph, mb) != CAN_TRANSMIT_PENDING) {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (can_transmission_stop(canPeriph, mb) == SUCCESS) {
|
|
|
|
|
|
s_diag.tx_abort_count++;
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
mailbox = can_message_transmit(canPeriph, &tx_msg);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-13 17:07:25 +08:00
|
|
|
|
if (mailbox != CAN_NOMAILBOX) {
|
|
|
|
|
|
s_diag.tx_count++;
|
|
|
|
|
|
s_diag.last_tx_id = ext_id;
|
|
|
|
|
|
s_diag.last_tx_channel = channelIndex;
|
|
|
|
|
|
s_diag.last_payload_length = dlc;
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
s_diag.error_count++;
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-10 21:27:51 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* @brief 获取 CANFD 诊断统计信息只读指针。
|
|
|
|
|
|
* @param[in] 无。
|
|
|
|
|
|
* @param[out] 无。
|
|
|
|
|
|
* @retval const CanFd::Diag* 返回内部诊断统计结构体只读指针。
|
|
|
|
|
|
*/
|
2026-05-10 17:28:14 +08:00
|
|
|
|
const CanFd::Diag *CanFd::diag()
|
|
|
|
|
|
{
|
|
|
|
|
|
return &s_diag;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-10 21:27:51 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* @brief 将三路 TCAN1057A 收发器置为 Normal 模式。
|
|
|
|
|
|
* @param[in] 无。
|
|
|
|
|
|
* @param[out] 无。
|
|
|
|
|
|
* @retval 无返回值。
|
|
|
|
|
|
* @note 对所有通道统一将 SLP/STB 控制脚拉低。
|
|
|
|
|
|
*/
|
2026-05-10 17:28:14 +08:00
|
|
|
|
void CanFd::setNormalMode()
|
|
|
|
|
|
{
|
2026-05-10 21:27:51 +08:00
|
|
|
|
for(size_t i = 0; i < kChannelCount; ++i) {
|
|
|
|
|
|
gpio_bit_reset(kChannels[i].slpPort, kChannels[i].slpPin);
|
|
|
|
|
|
}
|
2026-05-10 17:28:14 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-10 21:27:51 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* @brief 将三路 TCAN1057A 收发器置为 Silent 模式。
|
|
|
|
|
|
* @param[in] 无。
|
|
|
|
|
|
* @param[out] 无。
|
|
|
|
|
|
* @retval 无返回值。
|
|
|
|
|
|
* @note 对所有通道统一将 SLP/STB 控制脚拉高。
|
|
|
|
|
|
*/
|
2026-05-10 17:28:14 +08:00
|
|
|
|
void CanFd::setSilentMode()
|
|
|
|
|
|
{
|
2026-05-10 21:27:51 +08:00
|
|
|
|
for(size_t i = 0; i < kChannelCount; ++i) {
|
|
|
|
|
|
gpio_bit_set(kChannels[i].slpPort, kChannels[i].slpPin);
|
|
|
|
|
|
}
|
2026-05-10 17:28:14 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-10 21:27:51 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* @brief 处理指定通道的 RX0 中断接收。
|
|
|
|
|
|
* @param[in] channelIndex 目标 CAN 通道索引(0/1/2)。
|
|
|
|
|
|
* @param[out] 无。
|
|
|
|
|
|
* @retval 无返回值。
|
2026-05-30 00:24:31 +08:00
|
|
|
|
* @note 从对应控制器 FIFO0 循环取空所有待处理报文,更新诊断统计,写入该通道软件接收队列,并置位解析任务。
|
2026-05-10 21:27:51 +08:00
|
|
|
|
*/
|
|
|
|
|
|
void CanFd::handleRxInterrupt(uint8_t channelIndex)
|
2026-05-10 17:28:14 +08:00
|
|
|
|
{
|
2026-05-10 21:27:51 +08:00
|
|
|
|
if(channelIndex >= kChannelCount) return;
|
|
|
|
|
|
const auto &ch = kChannels[channelIndex];
|
|
|
|
|
|
|
2026-05-30 00:24:31 +08:00
|
|
|
|
const bool fifoFull = (can_interrupt_flag_get(ch.canPeriph, CAN_INT_FLAG_RFF0) != RESET);
|
|
|
|
|
|
const bool fifoOverfull = (can_interrupt_flag_get(ch.canPeriph, CAN_INT_FLAG_RFO0) != RESET);
|
|
|
|
|
|
if(fifoFull) {
|
|
|
|
|
|
s_diag.rx_fifo_full_count++;
|
|
|
|
|
|
}
|
|
|
|
|
|
if(fifoOverfull) {
|
|
|
|
|
|
s_diag.rx_fifo_overfull_count++;
|
|
|
|
|
|
}
|
2026-05-10 17:28:14 +08:00
|
|
|
|
|
2026-05-30 00:24:31 +08:00
|
|
|
|
bool hadTraffic = false;
|
|
|
|
|
|
while(can_interrupt_flag_get(ch.canPeriph, CAN_INT_FLAG_RFL0) != RESET) {
|
|
|
|
|
|
hadTraffic = true;
|
2026-05-10 17:28:14 +08:00
|
|
|
|
|
2026-05-30 00:24:31 +08:00
|
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
|
|
s_diag.rx_count++;
|
|
|
|
|
|
s_diag.last_rx_channel = channelIndex;
|
|
|
|
|
|
s_diag.last_payload_length = rx_message.rx_dlen;
|
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
|
|
auto &queue = s_rxQueues[channelIndex];
|
|
|
|
|
|
if(queue.count >= kRxQueueDepth) {
|
|
|
|
|
|
s_diag.rx_drop_count++;
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
2026-05-10 17:28:14 +08:00
|
|
|
|
|
2026-05-10 21:27:51 +08:00
|
|
|
|
auto &frame = queue.frames[queue.tail];
|
|
|
|
|
|
frame.channel = channelIndex;
|
|
|
|
|
|
frame.id = s_diag.last_rx_id;
|
|
|
|
|
|
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];
|
|
|
|
|
|
}
|
2026-05-30 00:24:31 +08:00
|
|
|
|
|
2026-05-10 21:27:51 +08:00
|
|
|
|
queue.tail = static_cast<uint8_t>((queue.tail + 1U) % kRxQueueDepth);
|
|
|
|
|
|
queue.count++;
|
|
|
|
|
|
}
|
2026-05-10 17:28:14 +08:00
|
|
|
|
|
2026-05-30 00:24:31 +08:00
|
|
|
|
if(fifoFull) {
|
|
|
|
|
|
can_interrupt_flag_clear(ch.canPeriph, CAN_INT_FLAG_RFF0);
|
|
|
|
|
|
}
|
|
|
|
|
|
if(fifoOverfull) {
|
|
|
|
|
|
can_interrupt_flag_clear(ch.canPeriph, CAN_INT_FLAG_RFO0);
|
|
|
|
|
|
}
|
|
|
|
|
|
if(hadTraffic) {
|
|
|
|
|
|
TaskManager::set(kCanRxParseTaskMask);
|
|
|
|
|
|
}
|
2026-05-10 21:27:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @brief CAN0 兼容中断处理入口。
|
|
|
|
|
|
* @param[in] 无。
|
|
|
|
|
|
* @param[out] 无。
|
|
|
|
|
|
* @retval 无返回值。
|
|
|
|
|
|
* @note 兼容保留入口,内部统一转调 `handleRxInterrupt(0U)`。
|
|
|
|
|
|
*/
|
|
|
|
|
|
void CanFd::onRx0Interrupt()
|
|
|
|
|
|
{
|
|
|
|
|
|
handleRxInterrupt(0U);
|
2026-05-10 17:28:14 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* ----- 中断入口 C 包装 ------------------------------------------------- */
|
2026-05-10 21:27:51 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* @brief CAN0 RX0 中断向量入口。
|
|
|
|
|
|
* @param[in] 无。
|
|
|
|
|
|
* @param[out] 无。
|
|
|
|
|
|
* @retval 无返回值。
|
|
|
|
|
|
* @note C 向量包装函数,内部转调 `CanFd::handleRxInterrupt(0U)`。
|
|
|
|
|
|
*/
|
|
|
|
|
|
extern "C" void CAN0_RX0_IRQHandler(void) { CanFd::handleRxInterrupt(0U); }
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @brief CAN1 RX0 中断向量入口。
|
|
|
|
|
|
* @param[in] 无。
|
|
|
|
|
|
* @param[out] 无。
|
|
|
|
|
|
* @retval 无返回值。
|
|
|
|
|
|
* @note C 向量包装函数,内部转调 `CanFd::handleRxInterrupt(1U)`。
|
|
|
|
|
|
*/
|
|
|
|
|
|
extern "C" void CAN1_RX0_IRQHandler(void) { CanFd::handleRxInterrupt(1U); }
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @brief CAN2 RX0 中断向量入口。
|
|
|
|
|
|
* @param[in] 无。
|
|
|
|
|
|
* @param[out] 无。
|
|
|
|
|
|
* @retval 无返回值。
|
|
|
|
|
|
* @note C 向量包装函数,内部转调 `CanFd::handleRxInterrupt(2U)`。
|
|
|
|
|
|
*/
|
|
|
|
|
|
extern "C" void CAN2_RX0_IRQHandler(void) { CanFd::handleRxInterrupt(2U); }
|