芯驰 E3 - CAN 报文路由转发 Demo

1. 前言

本篇博文将介绍 CAN 报文路由转发的思路和实现方法,CAN 报文路由就是要把在一路 CAN 上获取接收到的报文,通过预先设定好的路由表将不同 CAN ID 报文通过其他的一路或多路的 CAN 转发出去。

2. 实验思路

想要完成实验,首先要准备好实验材料和仪器。

实验仪器:
① CAN 分析仪,一个 CAN 分析仪一般可以接两路 CAN,实验使用四路 CAN,因此需要两个 CAN 分析仪;
② 分析仪的上位机软件;
③ E3640 开发板。

实验材料:
① 路由表;
② 自发送报文表(与核心实验无法,通过自发报文可以提高 CAN 总线的负载率,用来测试高负载率的 CAN 路由延迟情况)。

路由表描述的是所有 CAN 接收到的报文的 CAN ID 的对应关系,存在一对一和一对多的关系,具体而言路由表记录的是发送和接收双方报文转发关系,发送一帧需要转发的报文时就要根据路由表确定路由的目的设备,因此在 CAN 要做路由转发之前要通过路由表解析出接收到的 ID 报文要通过那一路或几路 CAN 转发出去。

这里我们需要建立在程序中做路由查询的路由表,这里给出路由表的结构:

typedef struct {
uint32_t src_id;
uint32_t dst_id;
uint32_t src_ch;
uint32_t dst_ch;
}can_route_t;​

完成了单片机接收处理和转发处理,接下来就需要上位机端发出 CAN 报数据了,针对不同的 CAN 按照路由表,上位机需要发出路由表中给定的 CAN ID 报文。如下图是上位机通过列表发送的方法,发送指定周期的 CAN ID 报文给单片机。


可以把每一个 CAN 需要发送的 CAN ID 报文的 list 导出,防止关闭软件后需要重新输入 CAN ID 报文 list。

3. 路由功能实现

3.1 查表功能

查表就是根据收到的 CAN ID 确定接收方要转发给哪一个目的设备,查表函数 search_route_table 的实现思路要实现以下三点:

① 在查表的过程中根据表中的 src_ch 判断此条报文是否需要转发;
② 对 src_ch 符合的 CAN ID 报文,查找并记录 dst_ch,即要转发的 CAN设备为哪些。通过 pDstCh 数组进行保存。
uint32_t search_route_table(uint32_t src_ch,uint32_t id,uint32_t* pDstCh)
{
uint32_t dst_ch_num = 0;

for(int i=0;i < sizeof(can_route_table)/sizeof(can_route_table[0]) ; i++){
if(id == can_route_table[i].src_id && src_ch == can_route_table[i].src_ch){
pDstCh[dst_ch_num++] = can_route_table[i].dst_ch;
}
}
return dst_ch_num;
}

3.2 转发功能

转发就是接收到 CAN ID 报文后,查表得到 dst_ch can 设备,然后向 dst_ch can 发送此 CAN ID 报文的过程。转发功能需要报文转发具有及时的特点,如果接收到 CAN 报文后,程序不能及时的将 CAN 报文转发出去,就会出现路由延迟的问题,因此接收 CAN 报文和发送 CAN 报文都采用中断的方式进行。

确定了接收和发送都要在中断中处理,那可否只在接收中断中一旦接收到 CAN 报文数据,就立马把报文根据路由表转发到对应的 CAN 上去呢?答案是否定的,因为转发过程中,如果转发要求来了之后没有空闲邮箱可以放置 CAN 报文,则此报文将无法转发出去,即会出现报文丢失的情况,为了防止这种情况可以针对每一个 CAN 建立需要转发的报文队列,当 RX 中断的报文接收到后不能及时的转发出来的时候,可以把报文放到队列之中,通过在发送完成中断中将报文转发出去即可解决问题,这里给出报文队列的 CAN 报文结点的结构,以及队列的结构:

typedef struct {
uint32_t id; /**< CAN Frame Identifier. */
struct {
uint32_t length : 7; /**< CAN frame payload length in bytes(Range: 0~64). */
uint32_t type : 1; /**< CAN Frame Type(DATA or REMOTE). */
uint32_t format : 1; /**< CAN Frame Identifier(STD or EXT format). */
uint32_t isCANFDFrame : 1; /**< CAN FD or classic frame? */
uint32_t isCANFDBrsEn : 1; /**< CAN FD BRS enabled? */
uint32_t reserved1 : 5; /**< Reserved for placeholder. */
uint32_t idHit : 9; /**< CAN Rx FIFO filter hit id(This value is only
used in Rx FIFO receive mode). */
uint32_t reserved2 : 7; /**< Reserved for placeholder. */
};
uint8_t dataBuffer[8];
}CAN_Pdu_t;
 
typedef CAN_Pdu_t QuDataType;
// 链式结构:表示队列
typedef struct QListNode
{
struct QListNode* _next;
QuDataType _data;
}QueueNode;
通过以上描述可以清楚编程的步骤为:
① 在 RX 中断中接收来自其他设备的 CAN 报文;
② 在 RX 中断中经过查找路由表明确需要哪些 CAN 做转发;
③ 转发之前先对发送邮箱进行空闲状态判断,如果发送邮箱空闲则对 CAN 报文进行转发,如果邮箱不空闲或者转发失败则把需要转发的报文放到每一个要转发的 CAN 的队列里面;
④ 在 RX 的 CAN 报文转发完成后,将触发此 CAN 的发送完成中断;
⑤ 发送中断中将报文队列的数据发送出去。

3.2.1 转发接收中断
接收中断的使命是准确无误的把 CAN 报文接收到,尽可能快的把报文转发出去,如果不能及时把报文转发出去,将报文放到队列中交给发送完成中断处理。

编写接收中断函数:
① 把接收到的数据放到 can_pdu 结构体中;
② 通过 search_route_table 函数,查找当前 CAN 所接收 ID 需要路由到哪些 CAN 中;
③ 通过 for 循环把需要路由的 CAN 报文通过各自的 CAN 路由出去,如果路由失败,则将路由报文放入到各个 CAN 对应的队列之中。

case FLEXCAN_RX_IDLE:
//generate a squarer wave on S4, means the receive of the CAN_ID_TEST CAN Frame begin
if(buf->id == CAN_ID_TEST){
sdrv_gpio_toggle_pin_output_level(GPIO_S4);
sdrv_gpio_toggle_pin_output_level(GPIO_S4);
}
//save the CAN Frame into can_pdu struct
can_pdu.id = buf->id;
can_pdu.format = buf->format;
can_pdu.type = buf->type;
can_pdu.isCANFDFrame = buf->isCANFDFrame;
can_pdu.isCANFDBrsEn = buf->isCANFDBrsEn;
can_pdu.length = buf->length;
memcpy(can_pdu.dataBuffer,buf->dataBuffer,buf->length);

//toggle S5 to caculate the consumption of function search_route_table
sdrv_gpio_toggle_pin_output_level(GPIO_S5);
//search the route table to find the destination CAN channel to route the CAN data
//put the destination CAN Channel numbers to dest_ch_num
//put the destination CAN Channel to dest_ch
dest_ch_num = search_route_table(get_can_ch(handle),can_pdu.id, dest_ch);
sdrv_gpio_toggle_pin_output_level(GPIO_S5);

//try to send out the CAN frame data through each can channel
for(uint32_t ch_index = 0; ch_index < dest_ch_num; ch_index++)
{
if(flexcan_handle_array[dest_ch[ch_index]]->mbState[TX_MB_INDEX] == FLEXCAN_StateIdle){
if(flexcan_send(flexcan_handle_array[dest_ch[ch_index]], TX_MB_INDEX,&can_pdu)){
//if the send is not successful, push the can frame to the each quene of the destination CAN channel
QueuePush(queue_array[dest_ch[ch_index]], can_pdu);
}
}else{
//if the send mailbox is not idle, put the can frame data to each quene of the destination CAN channel
QueuePush(queue_array[dest_ch[ch_index]], can_pdu);
}
}

break;
3.2.2 转发发送中断
转发发送中断的主要目的是处理在接收中断报文由于转发邮箱不空闲而进入了各自 CAN 的队列中,在发送中断中,将以发送完成后邮箱空闲后最快响应 CAN ID 报文的转发。
在 TX 接收完成中断中:
① 获取完成中断进入的 CAN 通道是哪一个 CAN,将当前的 CAN 通道值保存到 cur_ch 里,方便后续的确定使用哪一个 queue 队列弹出路由报文;
② 根据进入发送完成中断的邮箱判断使用不同的发送邮箱发送 CAN 路由报文和 CAN 应用报文,对于来自 TX_MB_INDEX (即 0 邮箱)进入的发送完成中断,通过 0 邮箱发送出路由队列的报文数据;
③ 对于非 0 号邮箱,即自发报文队列里的数据,通过 1~7 号邮箱中的空闲邮箱发送。

case FLEXCAN_TX_IDLE:
cur_ch = get_can_ch(handle);
if(cur_ch == -1)
break;

if(result == TX_MB_INDEX){
uint32_t tx_canid = get_txmb_id(handle,TX_MB_INDEX);
if(tx_canid == CAN_ID_TEST){
sdrv_gpio_toggle_pin_output_level(GPIO_S6);
sdrv_gpio_toggle_pin_output_level(GPIO_S6);
}

if(!QueueEmpty(queue_array[cur_ch]))
{
can_pdu_tmp_can = QueueFront(queue_array[cur_ch]);
if(0 == flexcan_send(handle, TX_MB_INDEX, &can_pdu_tmp_can)){
QueuePop(queue_array[cur_ch]);
}
}
}else{
if(!QueueEmpty(queue_array_tx[cur_ch]))
{
can_pdu_tmp_can = QueueFront(queue_array_tx[cur_ch]);
if(0 == flexcan_send(handle, result, &can_pdu_tmp_can)){
QueuePop(queue_array_tx[cur_ch]);
}
}
}
break;​


4. 路由延迟检测

路由延迟检测就是要计算出当单片机接收到一帧 CAN 报文,到这帧 CAN 报文被对应的 CAN 路由发出所用的时间,对于没有 CANOE 这种上位机集成好的检测工具,通过在 MCU 端通过在接收和发送时进行 GPIO 翻转程序也可以实现简单的对路由时间的获取。
   
在接收中断中,如果接收到 CAN_ID_TEST 的 CAN ID 报文,则进行 GPIO_S4 的电平翻转,通过逻辑分析仪采集数据,即在 GPIO_S4 上会有一个短暂的方波存在,这个方波即代表路由接收的开始。

    if(buf->id == CAN_ID_TEST){
sdrv_gpio_toggle_pin_output_level(GPIO_S4);
sdrv_gpio_toggle_pin_output_level(GPIO_S4);
}

在发送完成中断中,根据发送完成中断中的 mailbox 对应的 CAN ID 为 CAN_ID_TEST 则表示发送完成,通过 GPIO_S6 的电平翻转进行时间标记。

    uint32_t tx_canid = get_txmb_id(handle,TX_MB_INDEX);
if(tx_canid == CAN_ID_TEST){
sdrv_gpio_toggle_pin_output_level(GPIO_S6);
sdrv_gpio_toggle_pin_output_level(GPIO_S6);
}

实际的 GPIO_S4 和 GPIO_S6 的电平抓取如下,可以看到对于需要转发 3 次的 CAN ID 报文起始转发时间由 GPIO_S4 的脉冲为开始标志,GPIO_S6 的 3 个脉冲为 3 次转发的结束标志。


5. 自发报文实现

自发报文就是要求某几路 CAN 不仅要对路由表中的 CAN ID 报文做路由转发,还要对一些指定的 CAN ID 报文做周期发送。这里我们采用 btm 定时器的方式完成,btm 定时器的配置不予介绍,在 10ms 一次的中断中,对需要自发报文的 CAN ID 报文根据周期间隔进行发送。


void timerCallback(void)
{
// sdrv_gpio_toggle_pin_output_level(GPIO_S5);
time_tick++;

for(int32_t table_index = 0; table_index < sizeof(bcan_table)/sizeof(bcan_table[0]); table_index++)
{
if(time_tick % bcan_table[table_index].time_peroid == 0)
{
flexcan_send_tx(handle_can3, bcan_table[table_index].tx_id,dataBuffer_tx);
}
}
for(int32_t table_index = 0; table_index < sizeof(ccan_table)/sizeof(ccan_table[0]); table_index++)
{
if(time_tick % ccan_table[table_index].time_peroid == 0)
{
flexcan_send_tx(handle_can4, ccan_table[table_index].tx_id,dataBuffer_tx);
}
}
for(int32_t table_index = 0; table_index < sizeof(pcan_table)/sizeof(pcan_table[0]); table_index++)
{
if(time_tick % pcan_table[table_index].time_peroid == 0)
{
flexcan_send_tx(handle_can5, pcan_table[table_index].tx_id,dataBuffer_tx);
}
}
}

自发报文的发送函数 flexcan_send_tx 如下,在调用 flexcan_send_tx 函数时,其中的执行逻辑如下:
① 将报文数据封装为 frame 格式数据,如果 TX_ROUTE_MAILBOX_NUM 到 TX_ROUTE_MAILBOX_NUM + TX_MAILBOX_NUM 号邮箱,即 1~8 号邮箱空闲,则通过其中的一个空闲的邮箱通过 flexcan_send_nonblocking 发送出去;
② 同时将报文数据存放为 temp 格式数据,如果 TX_ROUTE_MAILBOX_NUM 到 TX_ROUTE_MAILBOX_NUM + TX_MAILBOX_NUM 号邮箱,即 1~8 号邮箱不空闲,则把 temp 数据放入 CAN 队列中。

static void flexcan_send_tx(flexcan_handle_t *handle, uint32_t id_tx, uint8_t dataBuffer_tx[8])
{
flexcan_frame_t frame = {.id = id_tx,
.length = 8,
.type = FLEXCAN_FrameTypeData,
.format = FLEXCAN_EXTEND_FRAME,
.isCANFDFrame = false,
.isCANFDBrsEn = true,
.dataBuffer = &dataBuffer_tx[0]};
uint32_t tx_mb_index = 0;
flexcan_mb_transfer_t xfer = {&frame, tx_mb_index};
QuDataType temp;

temp.id = id_tx;
temp.format = FLEXCAN_EXTEND_FRAME;
temp.type = FLEXCAN_FrameTypeData;
temp.isCANFDFrame = false;
temp.length = 8;
memcpy(temp.dataBuffer,dataBuffer_tx,8);

for(int i= TX_ROUTE_MAILBOX_NUM;i< TX_ROUTE_MAILBOX_NUM + TX_MAILBOX_NUM;i++){
if(handle->mbState[i] == FLEXCAN_StateIdle){
tx_mb_index = i;
break;
}
}

if(tx_mb_index == 0){
QueuePush(queue_array_tx[get_can_ch(handle)],temp);
}

xfer.mbIdx = tx_mb_index;


if(flexcan_send_nonblocking(handle, &xfer, TX_PADDING_VAL)){
QueuePush(queue_array_tx[get_can_ch(handle)],temp);
}
}


6. 邮箱分配问题

实验过程中出现的最大的问题应该就是邮箱分配问题了,邮箱的发送和接收具有优先级,数字序号越小的邮箱的优先级越高,为了转发的及时,对于一路 CAN 而言,使用 0 号邮箱做路由转发的发送,且 0 号邮箱不用于自发报文的发送,将使得在发送阶段,路由转发的报文将具有最高的优先级发出数据。根据以上结论:
① 配置 0 号邮箱做路由报文的发送邮箱;
② 配置 1~7 号邮箱做自发报文的发送邮箱;
③ 配置 8~15 号邮箱做接收邮箱。

#define RX_MAILBOX_NUM 7
#define TX_ROUTE_MAILBOX_NUM 1
#define TX_MAILBOX_NUM 7

void flexcan_generic_init(flexcan_handle_t *handle_l)
{
/* initialize flexCAN. */
flexcan_init(handle_l, &g_flexcan_config);
/* unfreeze the flexCAN by demo. */
flexcan_freeze(handle_l, false);

/* enable busoff interrupt. */
// flexcan_enable_interrupts(handle, FLEXCAN_BusOffInterruptEnable);
flexcan_enable_interrupts(handle_l,FLEXCAN_ErrorFlag | FLEXCAN_BusOffIntFlag);

/* enable irq. */
irq_attach(handle_l->irq_num, flexcan_irq_handler, handle_l);
irq_enable(handle_l->irq_num);

// /* assign mailbox0-6 for rx. */
// for (uint8_t i = 0; i < RX_MAILBOX_NUM; i++) {
// flexcan_config_rx_mailbox(handle_l, i, &rxmbcfg[i]);
// }
//
// /* assign mailbox7-13 for tx. */
// for (uint8_t i = RX_MAILBOX_NUM; i < (RX_MAILBOX_NUM + TX_MAILBOX_NUM);
// i++) {
// flexcan_config_tx_mailbox(handle_l, i);
// }


/* assign mailbox0 for route tx. */
for (uint8_t i = 0; i < TX_ROUTE_MAILBOX_NUM; i++) {
flexcan_config_tx_mailbox(handle_l, i);
}

/* assign mailbox1~7 for tx. */
for (uint8_t i = TX_ROUTE_MAILBOX_NUM; i < TX_MAILBOX_NUM+TX_ROUTE_MAILBOX_NUM; i++) {
flexcan_config_tx_mailbox(handle_l, i);
}

/* assign mailbox8~15 for rx. */
for (uint8_t i = TX_MAILBOX_NUM+TX_ROUTE_MAILBOX_NUM; i < (RX_MAILBOX_NUM + TX_MAILBOX_NUM + TX_ROUTE_MAILBOX_NUM);i++) {
flexcan_config_rx_mailbox(handle_l, i, &rxmbcfg[i - TX_MAILBOX_NUM - TX_ROUTE_MAILBOX_NUM]);
}
}

在使用 flexcan_config_rx_mailbox 函数配置接收邮箱时出现了 Bug,在原本的 SDK 的库函数中,flexcan_config_rx_mailbox 函数的 index 形参直接作为了数组 g_frameBuf 和 g_rxDataBuf 的数组序号,而此时的 index 是 8~15 显然不能作为数组序号了。修改 flexcan_config_rx_mailbox,使用变量 id 作为 index 的偏移值,使用 id 作为两个数组的偏移值,则可以保证 rx mailbox 的正确配置。

#if 0
static void flexcan_config_rx_mailbox(flexcan_handle_t *handle, uint8_t index,
flexcan_rx_mb_config_t *rxmbcfg)
{
flexcan_frame_t *rx_frame = &g_frameBuf[index];
rx_frame->dataBuffer = &g_rxDataBuf[index][0];

flexcan_set_rx_mb_config(handle, index, rxmbcfg);
flexcan_set_rx_individual_mask(handle, index, rx_individual_mask[index]);
/* rx by interrupt */
flexcan_mb_transfer_t Xfer = {rx_frame, index};
flexcan_receive_nonblocking(handle, &Xfer);
}
#else
static void flexcan_config_rx_mailbox(flexcan_handle_t *handle, uint8_t index,
flexcan_rx_mb_config_t *rxmbcfg)
{
uint32_t id = index - TX_MAILBOX_NUM - TX_ROUTE_MAILBOX_NUM;
flexcan_frame_t *rx_frame = &g_frameBuf[id];
rx_frame->dataBuffer = &g_rxDataBuf[id][0];

flexcan_set_rx_mb_config(handle, index, rxmbcfg);
flexcan_set_rx_individual_mask(handle, index, rx_individual_mask[id]);
/* rx by interrupt */
flexcan_mb_transfer_t Xfer = {rx_frame, index};
flexcan_receive_nonblocking(handle, &Xfer);
}
#endif


7. 附录

主程序:

int main(void)
{
int ret;
/* initialize clock source. */
ret = sdrv_ckgen_init(&g_clock_config);
ASSERT(ret == 0);

/* reset needed modules. */
board_reset_init();

/* initialize VIC. */
irq_initialize(VIC1_BASE, IRQ_MAX_INTR_NUM);
/* config pinmux. */
sdrv_pinctrl_init(NUM_OF_CONFIGURED_PINS, g_pin_init_config);

/* add printf. */
board_debug_console_init();


/* initialize the FlexCAN handle. */
handle_can3 = &g_flexcan_handle_can3;
flexcan_create_handle(handle_can3, FLEXCAN3, (void *)APB_CANFD3_BASE,
CANFD3_CANFD_INTR_NUM, flexcan_transfer_callback,

NULL);
flexcan_generic_init(handle_can3);

handle_can4 = &g_flexcan_handle_can4;
flexcan_create_handle(handle_can4, FLEXCAN4, (void *)APB_CANFD4_BASE,
CANFD4_CANFD_INTR_NUM, flexcan_transfer_callback,
NULL);
flexcan_generic_init(handle_can4);


/* initialize the FlexCAN handle. */
handle_can5 = &g_flexcan_handle_can5;
flexcan_create_handle(handle_can5, FLEXCAN5, (void *)APB_CANFD5_BASE,
CANFD5_CANFD_INTR_NUM, flexcan_transfer_callback,
NULL);
flexcan_generic_init(handle_can5);

handle_can6 = &g_flexcan_handle_can6;
flexcan_create_handle(handle_can6, FLEXCAN6, (void *)APB_CANFD6_BASE,
CANFD6_CANFD_INTR_NUM, flexcan_transfer_callback,
NULL);
flexcan_generic_init(handle_can6);


btmTimerInit(timerCallback);
btmTimerStart();

printf("dst ch = %d\r\n",can_route_table[0].dst_ch);
printf("size of the route table is: %d\r\n",sizeof(can_route_table)/sizeof(can_route_table[0]));
printf("FLEXCAN_SUCCESS = %d\r\n",FLEXCAN_SUCCESS);
printf("FLEXCAN_FAIL = %d\r\n",FLEXCAN_FAIL);
printf("FLEXCAN_TX_BUSY = %d\r\n",FLEXCAN_TX_BUSY);

QueueInit(&q_can3);
QueueInit(&q_can4);
QueueInit(&q_can5);
QueueInit(&q_can6);

QueueInit(&q_can3_tx);
QueueInit(&q_can4_tx);
QueueInit(&q_can5_tx);
QueueInit(&q_can6_tx);

for (;;)
{

}
}


8. 参考资料

1.《E3400_E3600_MCU_TRM_Rev01.00.pdf》

★博文内容均由个人提供,与平台无关,如有违法或侵权,请与网站管理员联系。

★文明上网,请理性发言。内容一周内被举报5次,发文人进小黑屋喔~

评论