从入门小白成长为嵌入式高手(二)——CAN通信协议(大疆3508电机驱动)(RoboMater篇)(下)
上节我们提到STM32cubeMX配置和代码展示,这节来继续学习!
为了便于理解,本文尽量采用简单的轮询方式来执行CAN,后续如果有需要,再考虑出freerots篇。
本文我们使用大疆C板来控制(因为手头上仅有大疆C板,A板和C板板级支持包差不多(CAN格式一样),仅CAN配置的端口不一样)
目录
一、大疆C板CAN口简介
大疆C板的主控芯片是STM32F407IGH6TR (根据大疆C板用户手册查询),
搭载两路CAN总线接口,常用来控制Robomaster电调或与其他设备通信:

1、CAN1为2-pin接口,引脚定义为PD0(CAN1_Rx)和PD1(CAN1_Tx),线序是黑色对应CANL、红色对应CANH。

2、CAN2为4-pin接口,引脚定义为为PB5(CAN2_RX)和PB6(CAN2_TX) 。线序为灰色对应CANL、CANH、GND,红色对应5V。(大疆C板里面会包含2pinCAN线、4pinCAN线、XT30电源线和SWD下载线)两路接口最大均支持1Mbps传输速率,实际应用中也可按需调低速率保障稳定性。
二、STM32cubeMX配置
以CAN1配置为例(CAN2配置和CAN1差不多的)。
1、芯片选型
(1)新建一个工程,电机图片红色箭头,可能会进行下载,这是正常的。

(2)、芯片型号选择STM32F407IGH6,双击下方芯片打开
2、系统时钟配置
打开外部高速时钟

将Serial Wire打开,不开Serial Wire会导致无法通过SWD模式烧录程序、进行在线调试。

配置时钟树,如图使用12MHz外部时钟,选择HSE(外部时钟通道),选择PLLCLK(锁相环:可将低速外部/内部时钟提升至芯片支持的高频(如72MHz、168MHz),为CPU、外设(串口、SPI等)提供稳定高频时钟。),主频输入168MHz,按下回车系统会自动帮忙配置完成。

系统时钟到这基本简单配置完成了,接下来是CAN配置。
3、CAN1配置
(1)、点击PD0和PD1端口(芯片上的灰色小圆,没配置是灰色的,配置完后变成绿色),选择CAN_Rx和CAN_Tx。

配置完后会变成绿色,如图,接着配置CAN的位时序参数

1. Bit Timings Parameters(位时序参数)
Prescaler (for Time Quantum):时间量子预分频系数。Prescaler是STM32中用于降低时钟频率的硬件模块,“Time Quantum(时间量子)”是CAN总线位时序的基本时间单元,预分频系数用于对CAN外设的输入时钟(通常由系统时钟或外设时钟派生)进行分频,得到“时间量子”的时长。这里配置为3,意味着输入时钟会被分成 3 + 1 = 4 份(CAN的预分频通常是“配置值 + 1”的逻辑)。
Time Quantum:即“时间量子”,是CAN位时序的最小时间单位。
Time Quanta in Bit Segment 1:位段1的时间量子数。CAN的位时序分为多个段(Segment),位段1用于采样点前的时序补偿。
Time Quanta in Bit Segment 2:位段2的时间量子数。位段2用于采样点后的时序补偿。
Time for one Bit:一位的总时长。CAN总线传输一位的时间等于“位段1 + 位段2 + 同步段(通常隐含为1个时间量子)”的总时间量子数乘以“时间量子”时长。
Baud Rate:波特率。即CAN总线的传输速率,由“一位的总时长”的倒数计算而来,也就是Baud Rate = 1 / Time for one Bit ,通常波特率配置为1Mbps。
ReSynchronization Jump Width:重同步跳转宽度。用于CAN总线的同步机制,当总线上的节点检测到时钟偏差时,可通过调整位时序的“跳变宽度”来重同步,这里配置为1个“时间量子”。
2. Basic Parameters(基本参数)
Time Triggered Communication Mode:时间触发通信模式。禁用(Disable)时,CAN工作在常规的事件触发通信模式;启用时,可基于时间触发通信(多用于对实时性要求极高的场景)。
Automatic Bus-Off Management:自动总线离线管理。禁用时,CAN控制器进入“总线离线”状态后不会自动恢复;启用时,会自动尝试恢复总线通信。
Automatic Wake-Up Mode:自动唤醒模式。禁用时,需手动唤醒CAN外设;启用时,CAN可通过总线上的活动自动唤醒。
Automatic Retransmission:自动重传。启用(Enable)时,若CAN报文发送失败(如仲裁丢失),会自动重新发送;禁用时,需软件手动重传,建议打开。
Receive Fifo Locked Mode:接收FIFO锁定模式。禁用时,接收FIFO满后新报文会覆盖旧报文;启用时,满后不再接收新报文。
Transmit Fifo Priority:发送FIFO优先级。启用(Enable)时,发送报文按优先级排序;禁用时,按“先到先发送”的顺序,建议打开。
3. Advanced Parameters(高级参数)
Operating Mode:工作模式。配置为“Normal(正常模式)”,CAN正常收发报文;若为“Loopback(回环模式)”则用于自测试(发送的报文会回传给自身接收),“Silent(静默模式)”则只收不发。
这些参数共同决定了CAN总线的通信速率、时序特性和工作行为,需根据实际总线要求(如波特率、同步需求)进行配置,这里我先进行简单的配置,最终波特率达到1Mbps即可,有需要的可以考虑自行查阅。
4、工程位置已经生成工程小技巧

点击左边的“Project”,根据上图配置工程,“Project Name”是工程名字,“Project Location”是工程存放位置,“Toolchain/IDE”是选择编译文件类型,由于是简单工程配置,我们先选择MDK-ARM,可以用keil5来编译。

点击左边的“Code Generator”选择要生成的文件,我们选择第一栏的使用所有库,这个方便你在后续的工程使用到某些库不用再去STM32CubeMX再重新生成。但是同样的,工程体积也会大一点。第二栏是添加使用到的库。往下勾选.c和.h文件分开,方便我们查看和修改代码。
三、CAN发送,回调代码
这是CAN的板级支持包,因为我用的是C++开发,怕大家一下子看不懂,所以这个板级支持包是参考了别人的,具体是谁的我也母鸡(狗头保命)。
这是bsp_can.c文件,主要是对CAN1进行一个初始化,调用CAN_Send_Data()函数进行一个发送
#include "bsp_can.h"
struct Struct_CAN_Manage_Object CAN1_Manage_Object = {0};
// CAN通信发送缓冲区
uint8_t CAN1_0x1ff_Tx_Data[8];
uint8_t CAN1_0x200_Tx_Data[8];
uint8_t CAN1_0x2ff_Tx_Data[8];
uint8_t CAN1_0x220_Tx_Data[8];
/**
* @brief 初始化CAN总线
* @param hcan CAN编号
* @param Callback_Function 处理回调函数
*/
void CAN_Init(CAN_HandleTypeDef *hcan, CAN_Call_Back Callback_Function)
{
HAL_CAN_Start(hcan);
__HAL_CAN_ENABLE_IT(hcan, CAN_IT_RX_FIFO0_MSG_PENDING);
__HAL_CAN_ENABLE_IT(hcan, CAN_IT_RX_FIFO1_MSG_PENDING);
if (hcan->Instance == CAN1)
{
CAN1_Manage_Object.CAN_Handler = hcan;
CAN1_Manage_Object.Callback_Function = Callback_Function;
}
}
/**
* @brief 配置CAN的过滤器
*
* @param hcan CAN编号
* @param Object_Para 编号 | FIFOx | ID类型 | 帧类型
* @param ID ID
* @param Mask_ID 屏蔽位(0x3ff, 0x1fffffff)
*/
void CAN_Filter_Mask_Config(CAN_HandleTypeDef *hcan, uint8_t Object_Para, uint32_t ID, uint32_t Mask_ID)
{
CAN_FilterTypeDef can_filter_init_structure;
//检测传参是否正确
assert_param(hcan != NULL);
if ((Object_Para & 0x02))
{
//数据帧
//掩码后ID的高16bit
can_filter_init_structure.FilterIdHigh = ID << 3 << 16;
//掩码后ID的低16bit
can_filter_init_structure.FilterIdLow = ID << 3 | ((Object_Para & 0x03) << 1);
// ID掩码值高16bit
can_filter_init_structure.FilterMaskIdHigh = Mask_ID << 3 << 16;
// ID掩码值低16bit
can_filter_init_structure.FilterMaskIdLow = Mask_ID << 3 | ((Object_Para & 0x03) << 1);
}
else
{
//其他帧
//掩码后ID的高16bit
can_filter_init_structure.FilterIdHigh = ID << 5;
//掩码后ID的低16bit
can_filter_init_structure.FilterIdLow = ((Object_Para & 0x03) << 1);
// ID掩码值高16bit
can_filter_init_structure.FilterMaskIdHigh = Mask_ID << 5;
// ID掩码值低16bit
can_filter_init_structure.FilterMaskIdLow = ((Object_Para & 0x03) << 1);
}
//滤波器序号, 0-27, 共28个滤波器, 前14个在CAN1, 后14个在CAN2
can_filter_init_structure.FilterBank = Object_Para >> 3;
//滤波器绑定FIFO0
can_filter_init_structure.FilterFIFOAssignment = (Object_Para >> 2) & 0x01;
//使能滤波器
can_filter_init_structure.FilterActivation = ENABLE;
//滤波器模式,设置ID掩码模式
can_filter_init_structure.FilterMode = CAN_FILTERMODE_IDMASK;
// 32位滤波
can_filter_init_structure.FilterScale = CAN_FILTERSCALE_32BIT;
//从机模式选择开始单元
can_filter_init_structure.SlaveStartFilterBank = 14;
HAL_CAN_ConfigFilter(hcan, &can_filter_init_structure);
}
/**
* @brief 发送数据帧
*
* @param hcan CAN编号
* @param ID ID
* @param Data 被发送的数据指针
* @param Length 长度
* @return uint8_t 执行状态
*/
uint8_t CAN_Send_Data(CAN_HandleTypeDef *hcan, uint16_t ID, uint8_t *Data, uint16_t Length)
{
CAN_TxHeaderTypeDef tx_header;
uint32_t used_mailbox;
//检测传参是否正确
assert_param(hcan != NULL);
tx_header.StdId = ID;
tx_header.ExtId = 0;
tx_header.IDE = 0;
tx_header.RTR = 0;
tx_header.DLC = Length;
return (HAL_CAN_AddTxMessage(hcan, &tx_header, Data, &used_mailbox));
}
/**
* @brief HAL库CAN接收FIFO0中断
*
* @param hcan CAN编号
*/
void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan)
{
//接收缓冲区
static Struct_CAN_Rx_Buffer can_rx_buffer;
//选择回调函数
if (hcan->Instance == CAN1)
{
HAL_CAN_GetRxMessage(hcan, CAN_FILTER_FIFO0, &can_rx_buffer.Header, can_rx_buffer.Data);
CAN1_Manage_Object.Callback_Function(&can_rx_buffer);
}
}
/**
* @brief HAL库CAN接收FIFO1中断
*
* @param hcan CAN编号
*/
void HAL_CAN_RxFifo1MsgPendingCallback(CAN_HandleTypeDef *hcan)
{
//接收缓冲区
static Struct_CAN_Rx_Buffer can_rx_buffer;
//选择回调函数
if (hcan->Instance == CAN1)
{
HAL_CAN_GetRxMessage(hcan, CAN_FILTER_FIFO1, &can_rx_buffer.Header, can_rx_buffer.Data);
CAN1_Manage_Object.Callback_Function(&can_rx_buffer);
}
}
这是bsp_can.h文件,主要就是声明一些函数。板级支持包里仅配置了CAN1,需要CAN2可以照葫芦画瓢进行添加。
#ifndef BSP_CAN_H
#define BSP_CAN_H
#include "stm32f4xx_hal.h"
// 滤波器编号
#define CAN_FILTER(x) ((x) << 3)
// 接收队列
#define CAN_FIFO_0 (0 << 2)
#define CAN_FIFO_1 (1 << 2)
//标准帧或扩展帧
#define CAN_STDID (0 << 1)
#define CAN_EXTID (1 << 1)
// 数据帧或遥控帧
#define CAN_DATA_TYPE (0 << 0)
#define CAN_REMOTE_TYPE (1 << 0)
/**
* @brief CAN接收的信息结构体
*
*/
struct Struct_CAN_Rx_Buffer
{
CAN_RxHeaderTypeDef Header;
uint8_t Data[8];
};
/**
* @brief CAN通信接收回调函数数据类型
*
*/
typedef void (*CAN_Call_Back)(Struct_CAN_Rx_Buffer *);
/**
* @brief CAN通信处理结构体
*
*/
struct Struct_CAN_Manage_Object
{
CAN_HandleTypeDef *CAN_Handler;
CAN_Call_Back Callback_Function;
};
extern CAN_HandleTypeDef hcan1;
extern struct Struct_CAN_Manage_Object CAN1_Manage_Object;
extern uint8_t CAN1_0x1ff_Tx_Data[];
extern uint8_t CAN1_0x200_Tx_Data[];
extern uint8_t CAN1_0x2ff_Tx_Data[];
extern uint8_t CAN1_0x220_Tx_Data[];
void CAN_Init(CAN_HandleTypeDef *hcan, CAN_Call_Back Callback_Function);
void CAN_Filter_Mask_Config(CAN_HandleTypeDef *hcan, uint8_t Object_Para, uint32_t ID, uint32_t Mask_ID);
uint8_t CAN_Send_Data(CAN_HandleTypeDef *hcan, uint16_t ID, uint8_t *Data, uint16_t Length);
void TIM_CAN_PeriodElapsedCallback();
#endif
接着可以在主函数里调用
MX_CAN1_Init();
CAN_Init(&hcan1, CAN_Motor_Call_Back);
CAN_Filter_Mask_Config(&hcan1, CAN_FILTER(13) | CAN_FIFO_1 | CAN_STDID | CAN_DATA_TYPE, 0x204, 0x7ff);
对其进行一个初始化,再调用CAN_Send_Data(&hcan1, 0x204, CAN1_0x200_Tx_Data, 8);对其进行一个数据发送就可以驱动电机了,里面的数据bsp_can.c里面有解释了,我就不重复说明了。
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_CAN1_Init();
CAN_Init(&hcan1, CAN_Motor_Call_Back);
CAN_Filter_Mask_Config(&hcan1, CAN_FILTER(13) | CAN_FIFO_1 | CAN_STDID | CAN_DATA_TYPE, 0x204, 0x7ff);
while (1)
{
//发送函数
while(torque < 1000)
{
torque += 50;
CAN1_0x200_Tx_Data[0] = torque >> 8;
CAN1_0x200_Tx_Data[1] = torque;
CAN_Send_Data(&hcan1, 0x204, CAN1_0x200_Tx_Data, 8);
HAL_Delay(50);
}
while(torque > -1000)
{
torque -= 50;
CAN1_0x200_Tx_Data[0] = torque >> 8;
CAN1_0x200_Tx_Data[1] = torque;
CAN_Send_Data(&hcan1, 0x204, CAN1_0x200_Tx_Data, 8);
HAL_Delay(50);
}
HAL_Delay(0);
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}
四、CAN回调接收解析代码
这里我们主要来探讨大疆M3508接收的数据该怎么进行一个解析,参考上节,我们可以看到MCU接收电调的内容,这里基本上是对转子进行一个反馈,转子和输出轴还是有所区别的,输出轴是我们能看到的裸露在外的用于连接的,转子是电机内部的,转子经过了减速箱才到输出轴。
0-1位是转子角度,并非输出轴的角度,即输出轴角度=转子机械角度*减速比(3591/187约为19,这里姑且用19代替),还有速度,也是针对转子而言的。我们主要用到的还是这两个参数。

我们只需要调用CAN1的回调指针,指针就叫measure_,就可以将数据按位进行解析了。由于电调内部反馈的一圈转子编码器值是8192,我们需要对其进行量化处理,将其转化为转子一圈的角度,再将得到的转子角度*减速比就可以得到电机输出轴的实际角度了,大疆M2006电机的驱动方式同理,能驱动大疆M3508电机的代码也能驱动大疆M2006电机。只是机械属性不同,比如减速度是36。大疆的GM6020电机驱动也类似,详细的自行查阅手册。
measure_.last_encoder = measure_.encoder;
measure_.encoder = ((uint16_t)buf[0] << 8) | buf[1];
measure_.angle = (float)measure_.encoder / 8192.0f * 360.0f;
measure_.speed = (buf[2] << 8) | buf[3];
measure_.speed_dps =
(1.0f - 0.85) * measure_.speed_dps + 6.0f * 0.85 * (float)measure_.speed; //对电机转速进行平滑滤波
measure_.torque_current = (1.0f - 0.9) * measure_.torque_current +
0.9 * (float)((int16_t)(buf[4] << 8 | buf[5])); //读取转矩电流并进行平滑滤波
measure_.temperature = buf[6];
/**
* @brief 存储DJI电机控制数据的静态数组
*
* 由于DJI电机发送以4个为一组, 专门使用一个数组存储控制数据, 在DJIMotorControl()中统一管理并发送
* M2006/M3508: 0x200, 0x1FF
* GM6020: 0x1FE, 0x2FE 电流驱动模式
* GM6020: 0X1FF, 0X2FF 电压驱动模式
*
* 反馈(rx_id): GM6020: 0x204+id ; C610/C620: 0x200+id
*
* 按照CAN总线与发送ID分组
* CAN1: [0]: 0x200, [1]: 0x1FF, [2]: 0x1FE, [3]: 0x2FE
* CAN2: [4]: 0x200, [5]: 0x1FF, [6]: 0x1FE, [7]: 0x2FE
*/
如此,本节暂告一段落了。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐
所有评论(0)