can_list
更新日期:2026/2/2
参与者:Jackrainman,PickingChip
源码地址:can_list
解决什么问题
在机器人电控系统中,一个 CAN 总线挂载多个电机和传感器。对于不同的电机回传的消息格式不同,同时也有着不同的处理方式,传统开方式在中断中使用 switch-case 处理不同 CAN ID:
void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan) {
if (header.ExtId == 0x201) { /* 处理左前轮 */ }
else if (header.ExtId == 0x202) { /* 处理右前轮 */ }
// ... 更多 else if
}
这种方式在机构复杂之后有着明显的痛点:
- 中断占用时间长
- 代码维护困难
- 业务逻辑与硬件中断耦合
因此本模块使用哈希表 + 链表的方式组织起总线上的所有节点,通过在节点中注册的回调函数处理不同电机的处理需求,从而实现消息接收与电机处理的解耦。
快速使用
- 将该文件夹复制到
/Drivers/bsp/下。 - 在
bsp.h中使用相对路径包含:#include "can_list/can_list.h"。 - 初始化 CAN 外设。
- 使用
can_list_add_can添加一个 CAN 外设。 - 使用
can_list_add_new_node添加 CAN 外设对应的设备。
数据结构
can_table
CAN 表指针数组,数组长度为主控 CAN 外设的个数,每一个 CAN 外设有单独的 CAN 表。
can_table_t
CAN 表类型,每一个包含两个哈希表分别用于标准帧和拓展帧节点的查询。
hash_table_t
哈希表类型,包含表长和一个指向can_node_t类型的二级指针(可视为CAN 节点指针数组),该数组每一位都挂载由哈希值相等的 CAN 节点组成的链表。
can_node_t
CAN 节点类型,总线上所有电机在初始化的时候都通过can_list_add_new_node分配一个该类型的 CAN 节点,这个节点有以下成员。当 CAN 外设接收到电机消息后,根据消息的ID查表找到电机对应的节点,从而触发回调函数执行数据处理。
typedef struct can_node {
void *can_data; // 设备对象指针(如 Motor_t*)
uint32_t id; // 期望的 CAN ID
uint32_t id_mask; // ID 掩码
can_callback_t callback; // 回调函数
struct can_node *next; // 链表后继
} can_node_t;
queue_msg_t
消息队列数据类型,在启用 RTOS 时触发中断后会在中断中发送
typedef struct {
#if CAN_LIST_USE_FDCAN
FDCAN_HandleTypeDef *hcan; /*!< The handle of FDCAN. */
#else /* CAN_LIST_USE_FDCAN */
CAN_HandleTypeDef *hcan; /*!< The handle of CAN. */
#endif /* CAN_LIST_USE_FDCAN */
uint32_t rx_fifo; /*!< The FIFO which received. */
} queue_msg_t;
工作流程
大致工作流程如下:

对于整个流程的实现,是否启用 RTOS 有些区别。当启用 RTOS 之后调用can_list_add_can函数会创建can_list_polling_task任务,该任务会一直阻塞式轮询消息队列。
直到 CAN 外设接到消息触发中断,中断服务函数中向队列消息写入消息,此时该任务才会根据消息队列中消息的去读取 CAN 接收队列中的电机消息,再去执行查表,进而执行对应电机的回调函数。这样设计的目的是为了让中断执行的时间尽可能短,如果为启用 RTOS 上述操作将在中断服务函数中调用can_message_process执行。
void HAL_FDCAN_RxFifo0Callback(FDCAN_HandleTypeDef *hfdcan,
uint32_t RxFifo0ITs) {
if ((RxFifo0ITs & FDCAN_IT_RX_FIFO0_NEW_MESSAGE) == RESET) {
return;
}
#if CAN_LIST_USE_RTOS
if (can_list_queue_handle == NULL) {
return;
}
send_msg_from_isr.hcan = hfdcan;
send_msg_from_isr.rx_fifo = FDCAN_RX_FIFO0;
xQueueSendFromISR(can_list_queue_handle, &send_msg_from_isr, NULL);
#else /* CAN_LIST_USE_RTOS */
can_message_process(hfdcan, FDCAN_RX_FIFO0);
#endif /* CAN_LIST_USE_RTOS */
}
配置宏
该模块支持的许多功能通过下述宏来进行配置。
| 宏定义 | 默认值 | 说明 |
|---|---|---|
CAN_LIST_USE_FDCAN | 0 | 1=启用 FDCAN,0=启用 bxCAN。STM32 HAL 库的 bxCAN 与 FDCAN 互不兼容,但本模块代码兼容两者 |
CAN_LIST_MAX_CAN_NUMBER | - | 最大支持的 CAN 外设数量,根据MCU的CAN控制器数量确认 |
CAN_LIST_USE_RTOS | 1 | 1=使用 FreeRTOS 任务处理 CAN 消息,加快中断退出时间;0=中断内直接处理。 启用后需要注意 CAN 中断的优先级不能高于 FreeRTOS 可管理的优先级! 启用后使用 can_list_add_can时,一定要在vTaskStartScheduler()后使用! |
CAN_LIST_QUEUE_LENGTH | 5 | FreeRTOS 消息队列长度 |
CAN_LIST_TASK_PRIORITY | 2 | FreeRTOS 任务优先级 |
CAN_LIST_MALLOC | malloc | 内存分配函数 |
CAN_LIST_FREE | free | 内存释放函数 |
API
| 函数 | 说明 |
|---|---|
can_list_add_can(can, std_len, ext_len) | 初始化 CAN 实例 |
can_list_add_new_node(...) | 注册设备节点 |
can_list_del_node_by_id(...) | 删除节点 |
can_list_change_callback(...) | 更改回调 |
can_list_add_can
添加一个 CAN 实例,初始化其哈希表。
| 参数 | 说明 |
|---|---|
can_select | CAN 外设选择(如 can1_selected) |
std_len | 标准帧哈希表桶数,根据 ID 分布设置(设为 1 退化为链表) |
ext_len | 扩展帧哈希表桶数,根据 ID 分布设置(设为 1 退化为链表) |
can_list_add_new_node
注册设备节点,绑定 ID、掩码、设备指针和回调函数。
uint8_t can_list_add_new_node(
can_selected_t can_select,
void *node_ptr, // 设备指针,收到数据后传入回调
uint32_t id, // 设备反馈 CAN ID
uint32_t id_mask, // ID 掩码
uint32_t id_type, // CAN_ID_STD 或 CAN_ID_EXT
can_callback_t callback // 回调函数(不能为空!)
);
can_list_del_node_by_id
通过 ID 删除已注册的设备节点。
can_list_change_callback
通过 ID 动态更改回调函数。
uint8_t can_list_change_callback(can_selected_t can_select, uint32_t id_type, uint32_t id, can_callback_t new_callback);
can_callback_t
回调函数指针类型,电机的回调函数必须满足该格式。
| 参数 | 说明 |
|---|---|
node_obj | 注册时传入的设备指针 |
header | CAN 消息头(包含 ID、ID 类型、数据长度等) |
data | CAN 数据负载(8 字节) |