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-case 或 if-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 文件自上而下分为五个逻辑模块:
预编译配置与 RTOS 适配 (Lines 1-40): 处理依赖头文件,根据宏定义决定是否引入 FreeRTOS 的队列和任务机制。
数据结构定义 (Lines 42-78): 定义节点 (can_node)、哈希表 (hash_table) 和总表 (can_table)。这是整个驱动的“骨架”。
核心管理函数 (CRUD) (Lines 80-244): 也就是 Create, Read, Update, Delete。包含了初始化总线、添加节点(注册回调)、删除节点等逻辑。
消息处理引擎 (Lines 248-439): 这是驱动的“心脏”。包含了 RTOS 的任务轮询函数 (can_list_polling_task) 或者非 RTOS 下的处理函数。
硬件中断接口 (Lines 443-End): 重写 STM32 HAL 库的弱定义回调函数(如 HAL_CAN_RxFifo0MsgPendingCallback),将硬件中断连接到我们的处理引擎。
模块一:预编译配置与 RTOS 适配 (Lines 1-40) 电控视角:为什么 RTOS 如此重要? 在机器人控制系统中,实时性 和确定性 是核心要求。电机控制环(位置环、速度环、电流环)需要严格的时间确定性,通常要求控制频率在 200 Hz 以上。如果 CAN 中断处理时间过长,可能导致:
控制环抖动 :中断处理时间不稳定,导致控制周期波动 中断丢失 :高频 CAN 消息可能被遗漏 系统卡顿 :其他高优先级任务(如姿态解算)被阻塞 代码解析 下面从源文件顶部开始分析代码结构和实现逻辑。
#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 是如何配合"传输"过程的:
中断通知阶段 :当 CAN 接收中断触发时,ISR 仅仅把 hcan(哪条总线)和 rx_fifo(哪个邮箱)这两个关键"地址信息"打包进 queue_msg_t 结构体。
任务处理阶段 :FreeRTOS 的任务 can_list_polling_task 接收到队列消息后,才会真正调用 HAL_CAN_GetRxMessage(或 HAL_FDCAN_GetRxMessage)去硬件缓冲区里"传输"和提取数据。
电控应用建议 CAN 中断优先级设置 :
// 正确:CAN 中断优先级低于 FreeRTOS 管理的中断
HAL_NVIC_SetPriority ( CAN1_RX0_IRQn , 6 , 0 ); // 优先级 6
// FreeRTOS 系统中断优先级通常为 5-15
configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY = 5 ;
队列长度设置 :
#define CAN_LIST_QUEUE_LENGTH 5 // 根据消息频率调整
建议 :队列长度 = 最大突发消息数 × 1.5 示例 :电机控制频率 1 kHz,4 个电机 → 突发 4 条消息 → 队列长度 6 任务优先级 :
#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 字节
电控开发最佳实践 表长度选择 :
// 使用质数,略大于预期设备数
#define STD_TABLE_LEN 13 // 标准帧表长度
#define EXT_TABLE_LEN 31 // 扩展帧表长度(通常更多)
掩码配置 :
// 电机反馈:只匹配设备 ID
#define MOTOR_ID_MASK 0xFF
// 传感器数据:匹配类型和设备
#define SENSOR_ID_MASK 0xFF00
内存优化 :
// 如果设备数固定且较少,可减小表长度
#define SMALL_SYSTEM_TABLE_LEN 7
// 大系统:适当增加表长度减少冲突
#define LARGE_SYSTEM_TABLE_LEN 61
调试技巧 :
// 打印哈希表统计信息
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 是否已经创建过,防止重复初始化。 RTOS 启动 :如果开启了 CAN_LIST_USE_RTOS,它会在这里创建消息队列和处理任务。 can_list_add_new_node:添加新节点 这是我们将具体的设备(如电机、传感器)注册到系统中的函数。
can_list_del_node_by_id:删除节点 当你不再需要监听某个 ID 时,使用此函数将其从内存中移除。
定位与遍历 :同样先通过 id % len 找到对应的哈希桶。 can_list_change_callback:更改回调函数 这个函数比较简单,用于在不删除节点的情况下,动态修改某个 ID 对应的处理逻辑。
它内部调用了 can_list_find_node_by_id 来定位节点。 如果找到节点,直接覆盖 node->callback 指针即可。 can_list_find_node_by_id:内部辅助函数 该函数使用 static 修饰符,表明其作用域限制在当前源文件内,仅供模块内部调用,不对外提供接口。该函数封装了在特定哈希表中查找 ID 的核心逻辑:
计算哈希下标 沿着该下标对应的链表遍历查找,直到 ID 匹配成功或到达链表末尾 模块四:消息处理引擎 (Processing Engine) 既然我们已经通过“增删改查”维护好了这张表,接下来就要看驱动程序的“心脏”——它是如何处理收到的数据的。在 can_list.c 中,这部分逻辑分为两种模式:
RTOS 模式 (can_list_polling_task) :一个独立的任务函数,通过队列接收中断发来的信号,然后异步处理。
非 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 重新写了它们,抢占了控制权。
分发逻辑 :
- RTOS 模式 :中断函数里只做一件事——把消息来源(哪个 CAN、哪个 FIFO)打包发给队列,然后迅速退出。
- 非 RTOS 模式 :中断函数会直接调用 can_message_process 进行查表和回调,这会占用更多的中断时间。
兼容性处理 : 代码使用了大量 #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 消息从进入芯片到执行回调的完整路径:
硬件接收 :CAN 外设收到一帧数据,触发中断。
进入中断 (ISR) :HAL_CAN_RxFifo0MsgPendingCallback 被调用,将 ID 和数据信息放入队列(RTOS 模式)或直接处理。
查表定位 (Hash) :程序通过 id % len 找到对应的哈希桶(链表头)。
掩码过滤 (关键点) :程序开始遍历链表。对于每一个 node,它取出收到的 received_id,与该节点的 id_mask 做 & 运算。
命中判定 :如果运算结果等于 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. 性能优化建议 中断优先级配置 :
// 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 ;
内存优化 :
// 根据实际设备数调整哈希表大小
#define CHASSIS_TABLE_LEN 13 // 4 个电机,质数 13
#define GIMBAL_TABLE_LEN 17 // 2 个电机,质数 17(考虑未来扩展)
实时性保障 :
CAN 接收任务优先级:中等(2-3) 控制任务优先级:高(4-5) 确保控制任务不被 CAN 处理阻塞 错误处理 :
// 检查注册结果
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. 面向对象设计 :代码清晰,易于维护