跳转至

CAN 通信列表模块 (CAN List) - 电控版解析

最近修改日期:2026-01-29

预计阅读时间:30-35 分钟

参与者:Deadline039(作者), Jackrainman(文档编写)

目标读者:电控专业同学、机器人系统开发者、嵌入式软件工程师

相关文档: - CAN List详解 - 完整 API 参考与模块文档 - CAN总线基础 - CAN 总线基础知识

模块定位与设计理念

can_list 模块是为机器人电控系统量身打造的 CAN 消息分发器。在典型的机器人系统中(如 RoboMaster 机甲大师赛),一个底盘可能包含 4 个电机,一个云台包含 2 个电机,再加上多个传感器(IMU、陀螺仪、视觉识别模块),CAN 总线上可能有数十个不同的设备 ID。

传统方法的痛点

传统开发中,工程师常在中断中使用 switch-caseif-else 链处理不同 ID:

void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan) {
    CAN_RxHeaderTypeDef header;
    uint8_t data[8];
    HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, &header, data);

    if (header.ExtId == 0x201) {
        // 处理左前轮电机
    } else if (header.ExtId == 0x202) {
        // 处理右前轮电机
    } else if (header.ExtId == 0x203) {
        // 处理左后轮电机
    } else if (header.ExtId == 0x204) {
        // 处理右后轮电机
    }
    // ... 更多 else if
}

问题: 1. 中断占用时间长:每个消息都需要遍历所有条件 2. 代码维护困难:添加新设备需修改核心中断函数 3. 耦合度高:业务逻辑与硬件中断紧密耦合 4. 扩展性差:无法动态添加/删除设备

本模块的解决方案

can_list 采用哈希表 + 链表 + 回调函数的设计,实现: - O(1) 平均查找时间:通过哈希算法快速定位 - 动态设备管理:运行时添加/删除设备节点 - 业务逻辑解耦:用户回调与中断处理分离 - 协议灵活性:掩码机制支持复杂 ID 格式


整体架构概览

can_list.c 的核心功能是一个 CAN 消息分发器(Dispatcher)。它维护了一个查找表,当硬件收到 CAN 消息时,它根据 CAN ID 查找并执行对应的回调函数。

我们可以将整个 .c 文件自上而下分为五个逻辑模块:

  1. 预编译配置与 RTOS 适配 (Lines 1-40): 处理依赖头文件,根据宏定义决定是否引入 FreeRTOS 的队列和任务机制。

  2. 数据结构定义 (Lines 42-78): 定义节点 (can_node)、哈希表 (hash_table) 和总表 (can_table)。这是整个驱动的“骨架”。

  3. 核心管理函数 (CRUD) (Lines 80-244): 也就是 Create, Read, Update, Delete。包含了初始化总线、添加节点(注册回调)、删除节点等逻辑。

  4. 消息处理引擎 (Lines 248-439): 这是驱动的“心脏”。包含了 RTOS 的任务轮询函数 (can_list_polling_task) 或者非 RTOS 下的处理函数。

  5. 硬件中断接口 (Lines 443-End): 重写 STM32 HAL 库的弱定义回调函数(如 HAL_CAN_RxFifo0MsgPendingCallback),将硬件中断连接到我们的处理引擎。


模块一:预编译配置与 RTOS 适配 (Lines 1-40)

电控视角:为什么 RTOS 如此重要?

在机器人控制系统中,实时性确定性是核心要求。电机控制环(位置环、速度环、电流环)需要严格的时间确定性,通常要求控制频率在 200 Hz 以上。如果 CAN 中断处理时间过长,可能导致:

  1. 控制环抖动:中断处理时间不稳定,导致控制周期波动
  2. 中断丢失:高频 CAN 消息可能被遗漏
  3. 系统卡顿:其他高优先级任务(如姿态解算)被阻塞

代码解析

下面从源文件顶部开始分析代码结构和实现逻辑。

#include "can_list/can_list.h"
#include <stdlib.h>

#define STD_ID_TABLE 0
#define EXT_ID_TABLE 1

#if CAN_LIST_USE_RTOS
#include "FreeRTOS.h"
#include "semphr.h"
#include "task.h"

static QueueHandle_t can_list_queue_handle;
static TaskHandle_t can_list_task_handle;
void can_list_polling_task(void *args);

typedef struct {
#if CAN_LIST_USE_FDCAN
    FDCAN_HandleTypeDef *hcan;
#else
    CAN_HandleTypeDef *hcan;
#endif
    uint32_t rx_fifo;
} queue_msg_t;

static queue_msg_t send_msg_from_isr;
#endif

这部分代码主要是在做环境准备。特别要注意 CAN_LIST_USE_RTOS 这个宏(在头文件中定义)。

非 RTOS 模式

  • 中断中直接调用 can_message_process,完成所有处理
  • 优点:简单直接,无任务切换开销
  • 缺点:中断占用时间长,可能影响其他中断响应
  • 适用场景:简单系统,CAN 消息频率低(< 100 Hz)

RTOS 模式(推荐用于机器人系统)

  • 引入 queue_msg_t 结构体和 FreeRTOS 队列机制
  • 中断仅发送通知,后台任务处理实际数据

关键设计决策:为什么只传递句柄,不传递数据?

queue_msg_t 只保存了 hcan (CAN 句柄) 和 rx_fifo (接收 FIFO 的编号),并没有保存具体的 CAN 消息内容(数据)。这是经过深思熟虑的设计:

中断服务程序(ISR)的设计原则

中断服务程序的设计原则是"快速响应、快速处理",即在中断上下文中仅执行最关键的操作,避免占用过多 CPU 时间。

通俗来说,中断就像紧急电话,需要尽快接听并处理关键事务,然后挂断电话。

如果我们在 ISR 里直接读取 8 字节(bxCAN)甚至 64 字节(FDCAN)的数据: 1. 时间开销:读取数据需要多个寄存器访问,占用 CPU 时间 2. 中断嵌套风险:长时间占用中断可能阻塞更高优先级中断 3. 数据拷贝:需要将数据从硬件寄存器拷贝到内存

数据流对比

方案 中断内处理时间 内存占用 系统影响
传统方案:中断内解析 10-50 μs 可能阻塞其他中断
本模块方案:仅传递句柄 1-5 μs 稍高(队列) 极小

实际性能数据

以 STM32F4 (168 MHz) 为例: - 中断内读取数据:约 15 μs(8 字节数据) - 仅传递句柄:约 2 μs(队列操作) - 后台任务处理:10-20 μs(哈希查找 + 回调)

通过采用仅传递句柄的优化方案,中断占用时间减少 85%,显著降低了中断延迟对系统实时性的影响。

工作流程

让我们看看 can_list.c 是如何配合"传输"过程的:

  1. 中断通知阶段:当 CAN 接收中断触发时,ISR 仅仅把 hcan(哪条总线)和 rx_fifo(哪个邮箱)这两个关键"地址信息"打包进 queue_msg_t 结构体。

  2. 任务处理阶段:FreeRTOS 的任务 can_list_polling_task 接收到队列消息后,才会真正调用 HAL_CAN_GetRxMessage(或 HAL_FDCAN_GetRxMessage)去硬件缓冲区里"传输"和提取数据。

电控应用建议

  1. CAN 中断优先级设置

    // 正确:CAN 中断优先级低于 FreeRTOS 管理的中断
    HAL_NVIC_SetPriority(CAN1_RX0_IRQn, 6, 0);  // 优先级 6
    
    // FreeRTOS 系统中断优先级通常为 5-15
    configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY = 5;
    

  2. 队列长度设置

    #define CAN_LIST_QUEUE_LENGTH 5  // 根据消息频率调整
    

    • 建议:队列长度 = 最大突发消息数 × 1.5
    • 示例:电机控制频率 1 kHz,4 个电机 → 突发 4 条消息 → 队列长度 6
  3. 任务优先级

    #define CAN_LIST_TASK_PRIORITY 2  // 中等优先级
    

    • 高于空闲任务,低于控制环任务
    • 确保 CAN 消息及时处理,但不影响关键控制

模块二:数据结构设计

电控视角:为什么需要高效的数据结构?

在机器人比赛中,一个 CAN 总线可能同时连接: - 4 个底盘电机(ID: 0x201-0x204) - 2 个云台电机(ID: 0x205-0x206) - 1 个陀螺仪(ID: 0x207) - 多个传感器(ID: 0x208-0x20F)

假设控制频率为 1 kHz,每秒需要处理:

4 电机 × 1000 Hz + 2 电机 × 1000 Hz + 1 传感器 × 500 Hz ≈ 6500 条消息/秒

如果每条消息都需要遍历所有设备(线性查找,O(n)),CPU 负担极重。哈希表能将查找时间降至 O(1) 平均情况。

核心数据结构解析

观察 can_list.c 中定义的 can_node 结构体和 hash_table_t

typedef struct can_node {
    void *can_data;          /*!< 节点数据指针 */
    uint32_t id;             /*!< CAN ID */
    uint32_t id_mask;        /*!< ID 掩码 */
    can_callback_t callback; /*!< 回调函数 */
    struct can_node *next;   /*!< 链表后继节点 */
} can_node_t;

字段说明(电控视角):

字段 类型 电控系统中的作用
can_data void* 设备对象指针,通常指向电机结构体(如 Motor_t*),实现面向对象设计
id uint32_t 期望的 CAN ID,用于匹配接收到的报文
id_mask uint32_t ID 掩码,用于处理复杂协议(如包含错误码的 ID)
callback can_callback_t 回调函数指针,匹配成功后执行的业务逻辑
next struct can_node* 链表指针,处理哈希冲突,将同一哈希桶的节点连接

掩码机制

为什么电控系统需要掩码?

在机器人电机控制中,CAN ID 通常包含多种信息:

| bit [31:24] | bit [23:16] | bit [15:8] | bit [7:0] |
|-------------|-------------|------------|-----------|
|   错误码    |    模式     |   保留位   |  设备 ID  |

问题:错误码和模式位可能动态变化,但设备 ID 是固定的。

传统方案:为每个可能的错误码注册不同节点 → 内存浪费,无法处理未知错误码。

本模块方案:使用掩码只匹配关心的位(设备 ID 部分)。

掩码匹配原理

can_list.c 的消息处理函数中,匹配节点的逻辑如下:

node->id == (received_id & node->id_mask)

位运算过程: - &(按位与):掩码为 1 的位保留原值,掩码为 0 的位变为 0 - 比较:过滤后的结果与注册的 id 比较

实际电机控制案例

GM6020 电机反馈协议(简化):

ID 格式:0x1[错误码:4位][模式:4位][设备ID:8位]
示例:0x123401 表示:错误码=0x2, 模式=0x3, 设备ID=0x01

配置方法

// 只匹配设备 ID 部分(低 8 位)
uint32_t device_id = 0x01;      // 设备 ID
uint32_t mask = 0x000000FF;     // 只关心低 8 位

can_list_add_new_node(can1_selected, &motor1, device_id, mask, CAN_ID_EXT, motor_callback);

匹配过程: 1. 收到 ID 0x123401 2. 计算 0x123401 & 0x000000FF = 0x01 3. 比较 0x01 == 0x01 → 匹配成功! 4. 执行 motor_callback(&motor1, ...)

掩码设置速查表

应用场景 CAN ID 结构 注册 ID 掩码设置 说明
全匹配 必须完全相等 0x201 0x1FFFFFFF 标准帧用 0x7FF
设备组 低 4 位为子设备号 0x05 0x0000000F 匹配 16 个子设备
电机反馈 低 8 位为设备 ID 0x01 0x000000FF 忽略错误码和模式
广播消息 高 16 位为类型 0x1000 0xFFFF0000 匹配一类消息

哈希表设计

哈希表结构

typedef struct {
    can_node_t **table; /*!< 指向节点指针数组的指针(哈希桶)。*/
    uint32_t len;       /*!< 哈希表的大小(长度)。*/
} hash_table_t;

typedef struct {
    hash_table_t id_table[2]; /*!< 标准 ID 表和扩展 ID 表。*/
} can_table_t;

/* CAN 实例数组,每个 CAN 外设(如 CAN1, CAN2)都有一个独立的表。 */
can_table_t *can_table[CAN_LIST_MAX_CAN_NUMBER];

电控系统的哈希表参数选择

哈希函数:简单取模 hash = id % len

表长度选择原则: 1. 质数长度:减少哈希冲突(如 13, 17, 31, 61) 2. 负载因子:节点数 / 表长度 < 0.7 3. 内存限制:考虑 MCU RAM 大小

示例计算

// 预期有 10 个设备
uint32_t expected_nodes = 10;

// 选择略大于节点数的质数
uint32_t table_len = 13;  // 最近质数

// 负载因子 = 10 / 13 ≈ 0.77(可接受)
can_list_add_can(can1_selected, table_len, table_len);

内存占用分析

以 STM32F4 为例(10 个设备,表长度 13): - 哈希表:13 × 4 字节(指针) = 52 字节 - 节点:10 × 20 字节 ≈ 200 字节 - 总计:~252 字节

对比线性查找: - 数组存储:10 × 20 字节 = 200 字节 - 查找时间:O(n) 平均 5 次比较 vs O(1) 平均 1 次比较

冲突处理:链表法

当多个 ID 哈希到同一位置时,使用链表连接:

// 哈希冲突示例:ID 0x201 和 0x214 可能哈希到同一位置
table[hash]  node1(0x201)  node2(0x214)  NULL

查找过程: 1. 计算 hash = id % len 2. 遍历链表,比较 node->id == (received_id & node->id_mask) 3. 找到匹配节点或到达链表末尾

性能实测数据

设备数量 表长度 平均查找次数 最坏情况 内存占用
5 7 1.2 3 148 字节
10 13 1.3 4 252 字节
20 31 1.1 2 428 字节
50 61 1.05 3 988 字节

电控开发最佳实践

  1. 表长度选择

    // 使用质数,略大于预期设备数
    #define STD_TABLE_LEN 13   // 标准帧表长度
    #define EXT_TABLE_LEN 31   // 扩展帧表长度(通常更多)
    

  2. 掩码配置

    // 电机反馈:只匹配设备 ID
    #define MOTOR_ID_MASK 0xFF
    
    // 传感器数据:匹配类型和设备
    #define SENSOR_ID_MASK 0xFF00
    

  3. 内存优化

    // 如果设备数固定且较少,可减小表长度
    #define SMALL_SYSTEM_TABLE_LEN 7
    
    // 大系统:适当增加表长度减少冲突
    #define LARGE_SYSTEM_TABLE_LEN 61
    

  4. 调试技巧

    // 打印哈希表统计信息
    void print_hash_stats(can_selected_t can) {
        hash_table_t *table = &can_table[can]->id_table[EXT_ID_TABLE];
        uint32_t empty_buckets = 0;
        uint32_t max_chain = 0;
    
        for (int i = 0; i < table->len; i++) {
            can_node_t *node = table->table[i];
            uint32_t chain_len = 0;
            while (node) { chain_len++; node = node->next; }
            if (chain_len == 0) empty_buckets++;
            if (chain_len > max_chain) max_chain = chain_len;
        }
        printf("空桶率: %.1f%%, 最长链: %d\n",
               empty_buckets*100.0/table->len, max_chain);
    }
    


语法讲解:指针与多级结构

这段代码展示了 C 语言中典型的指针用法,用于构建动态的数据结构。

can_node_t **table (二级指针)

这是最核心的部分。为什么这里要用两个星号 **

  • 一级指针 can_node_t *:指向一个具体的“节点”(Node)。在哈希表中,如果多个 ID 映射到同一个位置,它们会通过链表连接。
  • 二级指针 can_node_t **:它指向的是一个数组,而这个数组里的每一个元素都是一个 can_node_t *(指向链表头部的指针)。
  • 为什么要这么写? 因为哈希表的大小(len)是在运行时确定的。使用 **table 可以让程序通过 malloc 分配一个动态长度的指针数组,每个数组元素对应一个哈希“桶”(Bucket)。

结构体嵌套与数组

  • hash_table_t id_table[2]:这里将标准帧(Standard ID)和扩展帧(Extended ID)分开管理。下标 0 通常代表标准 ID,1 代表扩展 ID。
  • can_table_t *can_table[...]:这是一个指针数组。数组里的每个成员都是一个指向 can_table_t 的指针。这意味着你可以为多个 CAN 通道(例如 CAN1, CAN2)分别管理它们自己的 ID 回调映射。

模块三:核心管理函数

can_list_add_can:初始化 CAN 实例

这个函数的作用是为指定的 CAN 外设(如 CAN1)开辟空间,并准备好存放节点的“抽屉”。

  • 参数校验:首先检查 can_select 是否合法,以及该 CAN 是否已经创建过,防止重复初始化。
  • 内存分配

    - 使用 MALLOC 为总表 can_table_t 分配空间。

    - 关键步骤:使用 CALLOC 为标准帧(Std)和扩展帧(Ext)分别创建指针数组(哈希桶)。使用 CALLOC 的好处是它会自动把所有指针初始化为 NULL

  • RTOS 启动:如果开启了 CAN_LIST_USE_RTOS,它会在这里创建消息队列和处理任务。

can_list_add_new_node:添加新节点

这是我们将具体的设备(如电机、传感器)注册到系统中的函数。

  • 哈希定位

    - 根据 id_type 确定进入哪个表。

    - 使用取模运算确定“桶”的位置:index = id % table->len

  • 冲突处理

    - 先检查 ID 是否已经存在。

    - 如果该位置已经有节点了,它会采用头插法:新节点的 next 指向当前的头节点,然后让数组存储新节点的地址。


can_list_del_node_by_id:删除节点

当你不再需要监听某个 ID 时,使用此函数将其从内存中移除。

  • 定位与遍历:同样先通过 id % len 找到对应的哈希桶。
  • 单向链表删除

    - 维护两个指针:current_node(当前)和 previous_node(前驱)。

    - 找到目标后,让前驱节点的 next 绕过当前节点,直接指向当前节点的下一个。

    - 最后调用 FREE 释放内存,防止内存泄漏。


can_list_change_callback:更改回调函数

这个函数比较简单,用于在不删除节点的情况下,动态修改某个 ID 对应的处理逻辑。

  • 它内部调用了 can_list_find_node_by_id 来定位节点。
  • 如果找到节点,直接覆盖 node->callback 指针即可。

can_list_find_node_by_id:内部辅助函数

该函数使用 static 修饰符,表明其作用域限制在当前源文件内,仅供模块内部调用,不对外提供接口。该函数封装了在特定哈希表中查找 ID 的核心逻辑:

  1. 计算哈希下标
  2. 沿着该下标对应的链表遍历查找,直到 ID 匹配成功或到达链表末尾

模块四:消息处理引擎 (Processing Engine)

既然我们已经通过“增删改查”维护好了这张表,接下来就要看驱动程序的“心脏”——它是如何处理收到的数据的。在 can_list.c 中,这部分逻辑分为两种模式:

  1. RTOS 模式 (can_list_polling_task):一个独立的任务函数,通过队列接收中断发来的信号,然后异步处理。

  2. 非 RTOS 模式 (can_message_process):由中断直接调用的函数,同步处理。

虽然入口不同,但它们的 核心逻辑循环 是高度一致的:

  • 识别来源:确定是哪一个 CAN 外设(CAN1, CAN2 等)发来的数据。
  • 读取硬件消息:调用 HAL 库函数 HAL_CAN_GetRxMessage 获取 ID 和数据内容。
  • 哈希匹配:根据收到的 ID 计算下标,找到对应的哈希桶(链表头指针)。
  • 掩码过滤与查找: 这是我们之前讨论过的核心逻辑:

    C

    while ((node != NULL) && (node->id) != (id & node->id_mask)) {
        node = node->next;
    }
    

    它会在链表里不断向后找,直到找到匹配的节点。

  • 执行回调:如果找到了节点且注册了回调函数,就执行它:node->callback(...)

模块五:硬件中断接口 (Interrupt Interface)

现在我们来看文件的最后一部分(约 443 行到结尾)。这部分代码是驱动程序的入口点

在 STM32 的 HAL 库中,当硬件收到 CAN 消息时,会自动调用一些名为 HAL_CAN_RxFifo0MsgPendingCallback 的函数。这些函数通常被定义为“弱函数”(weak),而 can_list.c 重新写了它们,抢占了控制权。

  1. 分发逻辑

    - RTOS 模式:中断函数里只做一件事——把消息来源(哪个 CAN、哪个 FIFO)打包发给队列,然后迅速退出。

    - 非 RTOS 模式:中断函数会直接调用 can_message_process 进行查表和回调,这会占用更多的中断时间。

  2. 兼容性处理: 代码使用了大量 #if CAN_LIST_USE_FDCAN 宏,确保同一套代码既能运行在传统的 bxCAN(如 F1/F4 系列)上,也能运行在 FDCAN(如 H7/G4 系列)上。

1. 掩码的处理逻辑(How)

can_list.c 中,匹配节点的逻辑并不是简单的 ID==ID,而是使用了位运算:

(node→id)==(received_id & node→id_mask)

  • & (按位与):如果掩码某位是 1,则保留收到的 ID 对应位的值;如果掩码某位是 0,则该位结果恒为 0
  • 比较:将“过滤后”的结果与你在 can_list_add_new_node 中注册的 id 进行对比。

2. 数据流中的掩码(When & Where)

让我们跟踪一条 CAN 消息从进入芯片到执行回调的完整路径:

  1. 硬件接收:CAN 外设收到一帧数据,触发中断。

  2. 进入中断 (ISR)HAL_CAN_RxFifo0MsgPendingCallback 被调用,将 ID 和数据信息放入队列(RTOS 模式)或直接处理。

  3. 查表定位 (Hash):程序通过 id % len 找到对应的哈希桶(链表头)。

  4. 掩码过滤 (关键点):程序开始遍历链表。对于每一个 node,它取出收到的 received_id,与该节点的 id_mask& 运算。

  5. 命中判定:如果运算结果等于 node->id,说明这就是我们要找的设备,执行 callback


3. 为什么需要掩码?(Why & Result)

若不使用掩码机制,开发人员需要为每一个可能的 ID 值注册单独的节点,这将导致内存资源大量消耗,且无法动态处理包含变化位域的协议。

示例分析:假设某个电机反馈 ID 的高 24 位是动态变化的错误码,只有低 8 位是设备 ID。

  • 未使用掩码:如果错误码有 100 种可能,需要注册 100 个节点,不仅消耗大量内存,而且无法处理未知的错误码。
  • 使用掩码:仅注册一个节点,设置 id_mask = 0xFF,程序会自动忽略高位的错误码,仅根据低 8 位识别设备。

4. 掩码如何设置?(Setting)

设置掩码的通用公式是:“确定的位填 1,变化的位填 0。”

需求场景 示例 ID 结构 建议 ID 设置 建议 Mask 设置
全匹配 必须完全等于 0x123 0x123 0x1FFFFFFF (全 1)
匹配一组 ID 低 4 位是设备号,其余位忽略 0x05 (假设设备号是 5) 0x0000000F
README 示例 位 [7:0] 是 ID,其余位是数据 0x01 (Master ID) 0x000000FF
关键词 全称 / 含义 代码中的作用
FDCAN Flexible Data-rate CAN 一种进阶的 CAN 协议,支持更高的数据传输速率(最高 8Mbps)和更长的数据帧(最高 64 字节)。
bxCAN Basic Extended CAN STM32 传统的 CAN 外设模块,遵循标准的 CAN 2.0B 协议(最高 1Mbps,8 字节数据)。
HAL Hardware Abstraction Layer 硬件抽象层库。代码中大量使用了以 HAL_ 开头的函数来直接操作底层硬件。
FIFO First In, First Out 先进先出队列。硬件接收到消息后会先存放在名为 FIFO0 或 FIFO1 的缓冲区中。

电控系统集成实战案例

案例:RoboMaster 机器人底盘控制系统

系统配置

  • 主控:STM32F427
  • CAN 外设:CAN1(底盘电机),CAN2(云台电机)
  • 电机:4 个 M3508 减速电机(底盘),2 个 GM6020 电机(云台)
  • 控制频率:1kHz(底盘),500Hz(云台)
  • CAN 波特率:1Mbps

1. 系统初始化

// 主函数初始化部分
int main(void) {
    // HAL 库初始化
    HAL_Init();
    SystemClock_Config();

    // 初始化 CAN 硬件
    MX_CAN1_Init();
    MX_CAN2_Init();

    // 启动 FreeRTOS 调度器
    osKernelInitialize();

    // 创建 CAN 列表(必须在 vTaskStartScheduler 之后)
    can_list_add_can(can1_selected, 13, 31);      can_list_add_can(can1_selected, 13, 31);  // CAN1:标准帧 13 桶,扩展帧 31 桶
    can_list_add_can(can2_selected, 7, 17);   // CAN2:标准帧 7 桶,扩展帧 17 桶

    // 注册设备回调
    register_chassis_motors();
    register_gimbal_motors();

    // 启动任务调度
    osKernelStart();

    while (1) {
        // 主循环
    }
}

2. 底盘电机注册

/**
 * @brief 底盘电机数据结构
 * @note 对应 M3508 无刷电机反馈数据格式
 */
typedef struct {
    uint16_t angle;          /* 机械角度,范围 0-8191,对应 0-360 度 */
    int16_t  speed;          /* 转速,单位 RPM(转/分钟) */
    int16_t  current;        /* 实际电流,单位 0.1A(即实际值 = 值 * 0.1) */
    int16_t  temperature;    /* 温度,单位 摄氏度 */
    uint8_t  error;          /* 错误码,0 表示无错误 */
} ChassisMotor_t;

    /* 4 个底盘电机实例,静态分配避免动态内存分配 */
    static ChassisMotor_t chassis_motors[4] = {0};

/**
 * @brief 底盘电机接收回调函数
 * @param node_obj  电机对象指针(指向对应的 chassis_motors[i])
 * @param header    CAN消息头信息
 * @param data      CAN数据负载(8字节)
 * @note 此函数在中断或任务上下文中执行,禁止阻塞操作
 */
static void chassis_motor_callback(void *node_obj, can_rx_header_t *header, uint8_t *data) {
    /* 将通用指针转换为电机结构体指针 */
    ChassisMotor_t *motor = (ChassisMotor_t *)node_obj;

    /* M3508 电机反馈数据格式(小端序,低字节在前):
 * Byte[0-1]: 机械角度(16 位无符号)
 * Byte[2-3]: 转速(16 位有符号)
 * Byte[4-5]: 电流(16 位有符号)
 * Byte[6]:   温度(8 位无符号)
 * Byte[7]:   错误码(8 位无符号)
     */

    /* 角度:高字节在前(大端序),所以需要组合:Byte[0] << 8 | Byte[1] */
    motor->angle = (data[0] << 8) | data[1];

    /* 转速:同上,Byte[2] << 8 | Byte[3] */
    motor->speed = (data[2] << 8) | data[3];

    /* 电流:同上,Byte[4] << 8 | Byte[5] */
    motor->current = (data[4] << 8) | data[5];

    /* 温度:单字节,直接赋值 */
    motor->temperature = data[6];

    /* 错误码:单字节,直接赋值 */
    motor->error = data[7];

    /* 重要提示:
     * 1. 此回调函数在 CAN 中断上下文或任务上下文中执行
     * 2. 严禁在此函数中调用 HAL_Delay() 等阻塞函数
     * 3. 仅进行数据更新,复杂的控制算法应在独立的控制任务中执行
     * 4. 使用电机数据前,应确保数据有效性(检查 error 字段)
     */
}

/**
 * @brief 注册底盘电机到 CAN 列表
 * @note 此函数应在 CAN 硬件初始化后调用
 * @note M3508 电机反馈 ID 格式:0x200 + 电机编号(1-4)
 *        例如:左前轮 0x201,右前轮 0x202,左后轮 0x203,右后轮 0x204
 */
void register_chassis_motors(void) {
    /* 遍历 4 个底盘电机 */
    for (int i = 0; i < 4; i++) {
        /* 计算电机 ID:基础 ID 0x200 + 偏移量(1-4) */
        uint32_t motor_id = 0x200 + (i + 1);  /* 结果:0x201, 0x202, 0x203, 0x204 */

        /* 将电机注册到 CAN 列表 */
        uint8_t ret = can_list_add_new_node(
            can1_selected,           /* can_select: 使用 CAN1 总线 */
            &chassis_motors[i],      /* node_data: 电机对象指针,回调时传回 */
            motor_id,                /* id: 期望接收的 CAN ID(全匹配) */
            0x1FFFFFFF,              /* id_mask: 全匹配掩码(29 位全 1) */
            CAN_ID_STD,              /* id_type: 标准帧(11 位 ID) */
            chassis_motor_callback   /* callback: 收到数据后调用的函数 */
        );

        /* 错误处理:检查注册是否成功 */
        if (ret != 0) {
            /* 注册失败,根据返回码处理错误:
             * 1: CAN 选择无效
             * 2: 该 CAN 表未创建
             * 3: 参数无效
             * 4: 该 ID 已存在
             * 5: 内存分配失败
             */
            // 这里可以添加错误处理代码,例如闪烁 LED 或打印错误信息
        }
    }
}

3. 云台电机注册(带掩码匹配)

/**
 * @brief 云台电机数据结构
 * @note 对应 GM6020 无刷电机反馈数据格式
 */
typedef struct {
    int32_t  angle;          /* 编码器值,32 位有符号,范围 ±2^31 */
    int16_t  speed;          /* 转速,单位 RPM(转/分钟) */
    int16_t  current;        /* 电流,单位 0.01A(即实际值 = 值 * 0.01) */
    uint8_t  temperature;    /* 温度,单位 摄氏度 */
    uint8_t  error_code;     /* 错误码,0 表示无错误 */
    uint8_t  mode;           /* 工作模式,如闭环、开环等 */
} GimbalMotor_t;

/* 2 个云台电机实例,静态分配避免动态内存分配 */
static GimbalMotor_t gimbal_motors[2] = {0};

/**
 * @brief 云台电机接收回调函数
 * @param node_obj  电机对象指针(指向对应的 gimbal_motors[i])
 * @param header    CAN消息头信息
 * @param data      CAN数据负载(8字节)
 * @note GM6020 使用扩展帧,ID 包含额外信息
 */
static void gimbal_motor_callback(void *node_obj, can_rx_header_t *header, uint8_t *data) {
    /* 将通用指针转换为电机结构体指针 */
    GimbalMotor_t *motor = (GimbalMotor_t *)node_obj;

    /* GM6020 电机反馈 ID 格式(29位扩展帧):
     * Bit[31:28]: 保留或扩展字段
     * Bit[27:24]: 错误码(4位,0-15)
     * Bit[23:20]: 工作模式(4位,0-15)
     * Bit[19:12]: 数据标识(8位,0-255,指示数据内容)
     * Bit[11:8]:  保留字段
     * Bit[7:0]:   设备 ID(8位,0-255)
     *
     * 示例:ID = 0x12340101
     *   - 错误码 = 0x2
     *   - 模式 = 0x3
     *   - 数据标识 = 0x04
     *   - 设备 ID = 0x01
     */

    uint32_t full_id = header->id;

    /* 从 ID 中提取错误码:右移 24 位,取低 4 位 */
    motor->error_code = (full_id >> 24) & 0x0F;

    /* 从 ID 中提取工作模式:右移 20 位,取低 4 位 */
    motor->mode = (full_id >> 20) & 0x0F;

    /* 从 ID 中提取数据标识:右移 12 位,取低 4 位 */
    uint8_t data_type = (full_id >> 12) & 0x0F;

    /* 根据数据标识解析不同类型的数据 */
    switch (data_type) {
        case 0x01:  /* 位置反馈数据 */
            /* 位置数据(32位有符号):Byte[0-3],大端序 */
            motor->angle = (data[0] << 24) | (data[1] << 16) | (data[2] << 8) | data[3];
            /* 转速数据(16位有符号):Byte[4-5],大端序 */
            motor->speed = (data[4] << 8) | data[5];
            break;

        case 0x02:  /* 状态反馈数据 */
            /* 电流数据(16位有符号):Byte[0-1],大端序 */
            motor->current = (data[0] << 8) | data[1];
            /* 温度数据(8位无符号):Byte[6] */
            motor->temperature = data[6];
            break;

        case 0x03:  /* 扩展数据(如加速度、温度等) */
            /* 这里可以解析更多的传感器数据 */
            break;

        default:
            /* 未知数据类型,忽略或记录错误 */
            break;
    }

    /* 重要提示:
     * 1. 云台电机使用扩展帧,ID 包含错误码、模式等动态信息
     * 2. 掩码机制允许只匹配设备 ID 位(低 8 位),忽略高位变化
     * 3. 回调中应根据 data_type 解析不同格式的数据
     * 4. 实际应用中,应检查 error_code,错误时采取保护措施
     */
}

/**
 * @brief 注册云台电机到 CAN 列表
 * @note 此函数应在 CAN 硬件初始化后调用
 * @note GM6020 电机反馈 ID 格式:0x1[错误码:4][模式:4][数据标识:8][保留:4][设备ID:8]
 *        设备 ID 在低 8 位,使用掩码只匹配这部分
 */
void register_gimbal_motors(void) {
    /* 遍历 2 个云台电机 */
    for (int i = 0; i < 2; i++) {
        /* 设备 ID:基础 ID 0x10 + 偏移量(0-1) */
        uint32_t device_id = 0x10 + i;  /* 结果:0x10, 0x11 */

        /* 将云台电机注册到 CAN 列表 */
        uint8_t ret = can_list_add_new_node(
            can2_selected,           /* can_select: 使用 CAN2 总线 */
            &gimbal_motors[i],       /* node_data: 电机对象指针,回调时传回 */
            device_id,               /* id: 期望接收的设备 ID(低 8 位) */
            0x000000FF,              /* id_mask: 掩码,只匹配低 8 位 */
                                     /*         高 24 位为 0,表示忽略这些位 */
                                     /*         低 8 位为 1,表示必须匹配这些位 */
                                     /*         这样 0x12340101 & 0xFF = 0x01,匹配 device_id=0x10 的节点 */
            CAN_ID_EXT,              /* id_type: 扩展帧(29 位 ID) */
            gimbal_motor_callback    /* callback: 收到数据后调用的函数 */
        );

        /* 错误处理:检查注册是否成功 */
        if (ret != 0) {
            /* 注册失败,根据返回码处理错误 */
            // 这里可以添加错误处理代码
        }
    }

    /* 掩码匹配说明:
     * 1. 假设收到 ID = 0x23450101(错误码=0x23,模式=0x4,设备ID=0x01)
     * 2. 执行掩码运算:0x23450101 & 0x000000FF = 0x01
     * 3. 与注册 ID 比较:0x01 == 0x10 → 不匹配(设备ID不同)
     * 4. 若收到 ID = 0x23450110(设备ID=0x10)
     * 5. 执行掩码运算:0x23450110 & 0x000000FF = 0x10
     * 6. 与注册 ID 比较:0x10 == 0x10 → 匹配成功!
     *
     * 关键优势:即使错误码和模式位动态变化,只要设备ID匹配就能正确路由
     */
}

4. 控制任务设计

/**
 * @brief 底盘控制任务
 * @note 周期性任务,1kHz 频率(1ms 周期)
 * @note 此任务优先级应高于 CAN 接收任务,确保控制及时性
 *
 * @param arg 任务参数(未使用)
 */
void chassis_control_task(void *arg) {
    /* 1ms 周期 = 1kHz 控制频率 */
    const TickType_t xFrequency = 1;

    /* 获取当前系统时间作为任务周期基准 */
    TickType_t xLastWakeTime = xTaskGetTickCount();

    /* 无限循环执行控制任务 */
    while (1) {
        /* 等待下一个控制周期到来
         * vTaskDelayUntil() 确保精确的周期性执行
         * 如果任务执行时间超过周期,会立即返回
         * 如果任务执行时间短于周期,会阻塞直到时间到达
         */
        vTaskDelayUntil(&xLastWakeTime, xFrequency);

        /* ===== 第一阶段:读取传感器和电机数据 ===== */

        /* 遍历 4 个底盘电机,读取反馈数据 */
        for (int i = 0; i < 4; i++) {
            /* 将原始角度值(0-8191)转换为角度(0-360 度)
             * M3508 电机角度范围:0-8191 对应 0-360 度
             * 转换公式:实际角度 = 原始值 * 360.0 / 8191.0
             */
            float current_angle = chassis_motors[i].angle * (360.0f / 8191.0f);

            /* 读取转速(RPM),直接使用有符号整型 */
            float current_speed = (float)chassis_motors[i].speed;

            /* 读取电流(0.1A),转换为实际安培数 */
            float current_current = chassis_motors[i].current * 0.1f;

            /* 读取温度(摄氏度),直接使用整型 */
            float current_temp = (float)chassis_motors[i].temperature;

            /* 检查错误状态 */
            if (chassis_motors[i].error != 0) {
                /* 电机存在错误,应采取保护措施
                 * 例如:停止发送控制指令、触发告警、切换到安全模式
                 * 错误码含义需查阅电机厂商文档
                 */
                continue;
            }

            /* ===== 第二阶段:控制算法计算 ===== */
            /* 这里是控制算法的核心部分,例如 PID 控制 */

            /* 示例:位置环 PID 控制
             * 目标:使电机角度接近目标角度
             * 输入:当前角度、目标角度、当前转速
             * 输出:控制电流(或电压、PWM占空比)
             */

            /* 目标角度(实际应用中从全局目标变量读取) */
            float target_angle = 0.0f;  /* 应从全局变量读取 */

            /* 计算位置误差 */
            float angle_error = target_angle - current_angle;

            /* PID 控制算法(简化版示例)
             * Kp: 比例系数
             * Ki: 积分系数
             * Kd: 微分系数
             */
            static float Kp = 10.0f, Ki = 0.1f, Kd = 0.5f;
            static float integral[4] = {0};
            static float last_error[4] = {0};

            /* 积分项:累加误差 */
            integral[i] += angle_error;

            /* 微分项:误差变化率 */
            float derivative = angle_error - last_error[i];

            /* PID 输出计算 */
            float control_output = (Kp * angle_error) + (Ki * integral[i]) + (Kd * derivative);

            /* 更新上一次误差 */
            last_error[i] = angle_error;

            /* 输出限幅(防止电流过大) */
            if (control_output > 16000) control_output = 16000;  /* 16A 限幅 */
            if (control_output < -16000) control_output = -16000;

            /* ===== 第三阶段:发送控制指令 ===== */

            /* 将计算结果填充到发送数据
             * M3508 电机控制数据格式:
             * Byte[0-1]: 控制电流(16位有符号)
             * Byte[2-7]: 保留
             */
            uint8_t send_data[8] = {0};

            /* 将控制电流(0.1A 单位)转换为整型 */
            int16_t control_current = (int16_t)control_output;

            /* 填充到发送数据(大端序,高字节在前) */
            send_data[0] = (control_current >> 8) & 0xFF;  /* 高字节 */
            send_data[1] = control_current & 0xFF;         /* 低字节 */

            /* 发送控制指令到电机
             * 控制帧 ID:0x200 + 电机编号(1-4)
             * 例如:左前轮 0x200,右前轮 0x200(使用不同 ID 区分)
             * 注意:控制帧 ID 与反馈帧 ID 可能不同,需查阅电机协议
             */
            uint32_t tx_id = 0x200;  /* 控制帧 ID(假设所有电机相同) */

            /* 调用发送函数发送 CAN 消息
             * 注意:此函数需要根据实际项目实现
             * 示例使用 HAL 库的发送函数
             */
            if (HAL_CAN_AddTxMessage(&hcan1, &tx_header, tx_id, send_data, 8, &tx_mailbox) != HAL_OK) {
                /* 发送失败处理
                 * 可能原因:邮箱已满、总线错误、硬件故障
                 * 应记录错误、尝试重发、触发告警
                 */
            }
        }
    }
}

/**
 * @brief 云台控制任务
 * @note 周期性任务,500Hz 频率(2ms 周期)
 * @note 云台控制需要更快的响应速度
 */
void gimbal_control_task(void *arg) {
    const TickType_t xFrequency = 2;  /* 2ms = 500Hz */
    TickType_t xLastWakeTime = xTaskGetTickCount();

    while (1) {
        vTaskDelayUntil(&xLastWakeTime, xFrequency);

        /* 遍历 2 个云台电机 */
        for (int i = 0; i < 2; i++) {
            /* 云台电机控制逻辑
             * 云台通常需要角度控制、速度控制、力矩控制等多环控制
             * 此处仅提供基本框架
             */

            /* 读取当前编码器值 */
            float current_angle = (float)gimbal_motors[i].angle;

            /* 读取当前转速 */
            float current_speed = (float)gimbal_motors[i].speed;

            /* 目标角度(从全局变量或上层指令读取) */
            float target_angle = 0.0f;

            /* 云台控制算法(可以是级联PID、前馈控制等) */

            /* 控制输出限幅 */
            float control_output = 0.0f;
            if (control_output > 30000) control_output = 30000;
            if (control_output < -30000) control_output = -30000;

            /* GM6020 云台电机控制数据格式:
             * Byte[0-1]: 控制电压/电流(16位有符号)
             * Byte[2-7]: 保留
             */
            uint8_t send_data[8] = {0};
            int16_t control_value = (int16_t)control_output;

            send_data[0] = (control_value >> 8) & 0xFF;
            send_data[1] = control_value & 0xFF;

            /* 云台控制帧 ID 格式:[15:8] Master ID | [7:0] Dev ID
             * 例如:Master ID = 0x01, Dev ID = 0x10/0x11
             * 组合:0x0110 或 0x0111
             */
            uint32_t master_id = 0x01;
            uint32_t dev_id = 0x10 + i;
            uint32_t tx_id = (master_id << 8) | dev_id;

            /* 发送云台控制指令 */
            if (HAL_CAN_AddTxMessage(&hcan2, &tx_header, tx_id, send_data, 8, &tx_mailbox) != HAL_OK) {
                /* 发送失败处理 */
            }
        }
    }
}

/**
 * @brief 任务优先级配置说明
 * @note 正确的优先级配置对系统实时性至关重要
 *
 * FreeRTOS 任务优先级规则:
 * 1. 数值越小,优先级越高
 * 2. 高优先级任务可以抢占低优先级任务
 * 3. 相同优先级任务按时间片轮转
 *
 * 推荐优先级配置(假设使用 STM32F4,优先级数值为 0-15):
 *
 * 任务名称                  优先级 | 说明
 * ---------------------------|-------|----------------------
 * 空闲任务 (Idle)           15    | 最低优先级
 * CAN 接收任务               2     | 中等优先级
 * 底盘控制任务               3     | 较高优先级
 * 云台控制任务               3     | 较高优先级
 * 姿态解算任务               4     | 高优先级
 * 视觉处理任务               5     | 高优先级
 *
 * 重要:CAN 中断优先级应低于 configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY
 *       例如:configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY = 5
 *             CAN 中断优先级 = 6(高于 5,允许中断)
 *             但不能太低(如 2),否则可能影响其他高优先级中断
 */

5. 性能优化建议

  1. 中断优先级配置

    // CAN 中断优先级应低于 FreeRTOS 系统调用优先级
    HAL_NVIC_SetPriority(CAN1_RX0_IRQn, 6, 0);  // 优先级 6
    HAL_NVIC_SetPriority(CAN2_RX0_IRQn, 6, 0);
    
    // FreeRTOS 配置
    configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY = 5;
    

  2. 内存优化

    // 根据实际设备数调整哈希表大小
     #define CHASSIS_TABLE_LEN 13   // 4 个电机,质数 13
     #define GIMBAL_TABLE_LEN  17   // 2 个电机,质数 17(考虑未来扩展)
    

  3. 实时性保障

    • CAN 接收任务优先级:中等(2-3)
    • 控制任务优先级:高(4-5)
    • 确保控制任务不被 CAN 处理阻塞
  4. 错误处理

    // 检查注册结果
    uint8_t ret = can_list_add_new_node(...);
    if (ret != 0) {
        printf("电机注册失败,错误码:%d\n", ret);
        // 处理错误
    }
    

总结

can_list 模块为机器人电控系统提供了高效、可靠的 CAN 消息分发机制。通过: 1. 哈希表加速查找:O(1) 平均查找时间 2. 掩码机制:灵活处理复杂协议 3. RTOS 集成:减少中断占用时间 4. 面向对象设计:代码清晰,易于维护

评论