当前位置: 首页 > ops >正文

使用STM32CubeMX使用CAN驱动无刷电机DJI3508

简介

文章为笔记性质

硬件包括

大疆C板

电机调速器C620

DJI3508电机

CAN知识介绍

CAN的概念

CAN是控制器区域网络(Controller Area Network)的缩写。CAN总线是一种适用于工业设备的高性能总线网络。说白了就是也就是一种通讯方式而已。

把多个设备串在一根线上,再通过一定的协议约束,就能实现最方便的多设备互相通信。

而不用每个设备都两两之间有连线,大大节省了成本和布线复杂程度。

其次CAN的可靠性也比较高,采用差分线的形式,具有较强的抗干扰能力和噪声抑制能力。

CAN总线结构

CAN总线网络的结构有闭环和开环两种形式。

闭环

闭环结构的CAN总线网络,总线两端各接一个120Ω的终端电阻,两根信号线形成的回路。这种CAN总线网络由ISO11898标准定义,是高速短距离的CAN网络,通信速率为125kbit/s到1Mbit/s,在1Mbit/s通信速率时,总线长达40m。

开环

开环结构的CAN总线网络,两根信号线独立,各自串联一个2.2 kΩ的电阻。这中CAN总线网络由ISO11519-2标准定义,是低速远距离的CAN网络,最高速率为125kbit/s。在40kbit/速率时,总线最长举例为1000m。
  闭环结构适用于节点数量有限的小型网络、简单性要求高、可靠性要求高的场景。开环结构适用于大型网络,扩展性要求高,故障隔离要求高的场景。

一般情况下,(平时驱动个电机、进行简单的通信),我们只用得到 高速CAN 也就是 闭环CAN

所以这里也就主要说一说高速CAN(闭环CAN,1Mbit/s)

以下部分简称之为CAN

下图是CAN的结构示意图:

CAN总线上只有两根信号线,即CANHCANL,没有时钟同步信号。

所以CAN是一种异步通信方式,与UART的异步通信方式类似,而SPI、I2C是以时钟信号同步的同步通信方式

其中的120Ω电阻被称为终端电阻,作用是:阻抗匹配、减少信号反射、增强抗干扰等。

CAN的物理层

        CAN总线的两根信号线通常采用双绞线,传输的是差分信号,通过两根信号线的电压差CANH - CANL来表示CAN的总线电平。

        以差分信号传输信号具有抗干扰强,能有效抑制外部电子干扰等优点,这也是CAN总线在工业上应用广泛的一个原因。
  两根信号线的电压差CANH-CANL表示CAN总线的电平,与传输的逻辑信号1和逻辑0对应。对于逻辑1的称为隐形(Ressive)电平,对应与逻辑0的称为显性(Dominant)电平。

到这里应该对CAN有了一定的了解了,那么你也许会问:

单片机只能传输电平信号,怎么实现这种操作呢?

所以我们就需要另一个芯片作为CAN收发器 把单片机的高低电平信号转化为 CAN总线需要的信号

CAN收发器一般是单独的芯片,并且根据CAN总线的结构不同,需要使用不同的CAN收发器芯片,常见的CAN收发器芯片有TJA1050,TJA1042,SIT1050T,SIT1050T支持高速CAN,传输速率可达1Mbps。

我们的C板上已经有了CAN收发器芯片,所以直接用就可以。

CAN的数据传输特点

(1)CAN总线上的节点既可以发送数据又可以接收数据,没有主从之分。但是在同一个时刻,只能有一个节点在发送数据,其他节点只能接收数据


(2) CAN总线上的节点没有地址的概念。CAN总线上的数据是以为单位传输的。,帧又分为数据帧、遥控帧等多种帧类型,帧包含需要传输的数据或控制信息。


(3)CAN总线具有“线与”的特性,也就是当有两个字节同时向总线发送信号时,一个发送显性电平(逻辑0),另一个发送隐形电平(逻辑1),总线呈现为显性电平,这个特性被用于总线仲裁,也就是哪个节点优先占用总线进行发送操作。

        当没有数据发送时,终端电阻使总线保持在隐性电平。
(4)每个帧也有一个标识符(Indentifier,以下简称ID)。ID不是地址,它表示数据传输的类型,也可以用于总线仲裁时确定优先级。例如,在汽车的CAN总线上,假设用于碰撞检测的节点输出数据帧的ID为01,车内温度检测节点发送数据帧的ID为05等。


(5) 每个CAN节点都接收数据,但是可以对接收的帧ID进行过滤。只有节点需要的数据才会被接收并进一步处理,不需要的数据会被自动舍弃。例如,假设安全气囊控制器只接收碰撞检测节点发出的ID为01的帧,这种ID的过滤是由硬件完成的,以便安全气囊控制器在发生碰撞时能及时响应。


(6)CAN总线通信是半双工的,即总线不能同时发送和接收。在多个节点竞争总线进行发送时,通过ID的优先级进行仲裁,竞争胜出的节点继续发送,竞争失败的节点立刻转为接送状态。


(7)CAN总线没有用于同步的时钟信号,所以需要规定CAN总线通信的波特率,所有节点都必须使用相同的波特率进行通讯。

CAN协议层

CAN总线以“帧”形式进行通信。

CAN协议定义了5种类型的帧:数据帧、遥控帧、错误帧、过载帧、间隔帧,其中数据帧最为常用。


  其中,数据帧和遥控帧有ID,并且有标准格式和拓展格式两种格式,标准格式的ID是11位,拓展格式的ID是29位,下面仅详细介绍数据帧的结构,其他帧的结构可参考相关资料。

数据帧可以分为以下几段。
  (1)帧起始(Start Of Frame,SOF)。帧起始只有一个位,是一个显性电平(逻辑0),表示一个帧的开始。
  (2)仲裁段(Arbitration Field)。仲裁段包括11位的ID和RTR位,共12位。多个节点竞争总线发送数据时,根据仲裁段的数据决定哪个节点优先占用总线。哪个ID先出现显性电平(逻辑0),对应的节点就占用总线。所以,ID数值越小的优先级更高(CAN总线使用的是基于标识符的仲裁机制,标识符中的低位具有较高的权重,因此ID数值小的节点的标识符具有更高的优先级)。如果两个节点发送数据帧的ID相同,再根据仲裁段最后的RTR位进行裁决。
  RTR(Remote Transmit Request)是远程传输请求位,RTR位用于区分数据帧和遥控帧。数据帧的RTR位是显性电平(逻辑0),遥控帧的RTR位是隐形电平(逻辑1)。所以,具有相同ID的数据帧和遥控帧竞争总线时,数据帧优先级更高。
  (3)控制段。控制段包括IDE位、RB0位和4位的DLC,共6位。
IDE是标识符扩展位(Identifier Extension Bit),用于表示帧是扩展格式还是标准格式。标准格式的IDE是显性电平(逻辑0),拓展格式帧的IDE是隐形电平(逻辑1)。
  RB0是保留位,默认为显性电平。
  DLC是4个位的数据长度编码(Data Length Code),编码数值为0到8,表示后面数据段的字节,遥控帧的DLC编码数值为0,因为遥控帧不传输数据。
  (4)数据段。数据段里面是数据帧需要传输的数据,可以是0到8字节,数据的字节个数由DLC编码确定。遥控帧没有数据段。
  (5)CRC段。检查帧的传输错误的段。CRC共16位,其中前15位是CRC校验码,最后一位总是隐形电平,是CRC段的界定符(Delimiter)。
  (6)ACK段。ACK段包括一个ACK位(Acknowledge Bit)和一个ACK段界定符。发送节点发送的ACK位是隐形电平,接收节点接收的ACK位是显性电平。
  (7)帧结束(End Of Frame,EOF)。帧结束是帧结束段,由7个隐性位表示EOF。
  数据帧或遥控帧结束后,后面一般是帧空间或过载帧,用于分隔开数据帧和遥控帧。

具体参考下图:

到这里基本知识已经了解的差不多了。

更详细的内容请看中科大RM的培训视频。

接下来看CubeMX配置和相应代码

CubeMX配置

在SYS把DEBUG改成sw

在RCC开启外部时钟

配置时钟树

这里需要尤其关注 input frequence 一定一定要是12M 因为C板的硬件给的就是12M晶振,如果你这里配错了 接下来的到板子上的时钟就乱了 就不是你想要的频率了。

然后是开启CAN1

把波特率调整为1M

Normal模式

开NVIC

OK到这就配置完成。

代码

本代码基于中科大RM电控培训的开源代码改编而来

包括接收和发送

一些宏定义和声明

一些必要的宏定义以及声明// 滤波器编号
#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
#define CAN_REMOTE_TYPE 1/*** @brief CAN接收的信息结构体**/
typedef struct
{CAN_RxHeaderTypeDef Header;uint8_t Data[8];
} Struct_CAN_Rx_Buffer;/*** @brief CAN通信接收回调函数数据类型**/
typedef void (*CAN_Call_Back)(Struct_CAN_Rx_Buffer *);/*** @brief CAN通信处理结构体**/
typedef struct
{CAN_HandleTypeDef *CAN_Handler;Struct_CAN_Rx_Buffer Rx_Buffer;CAN_Call_Back Callback_Function;
} Struct_CAN_Manage_Object;// CAN通信发送缓冲区
uint8_t CAN1_0x0a0_Tx_Data[8];
uint8_t CAN1_0x1ff_Tx_Data[8];
uint8_t CAN1_0x200_Tx_Data[8];
uint8_t CAN1_0x2ff_Tx_Data[8];
uint8_t CAN1_0xxf1_Tx_Data[8];
uint8_t CAN1_0xxf2_Tx_Data[8];
uint8_t CAN1_0xxf3_Tx_Data[8];
uint8_t CAN1_0xxf4_Tx_Data[8];
uint8_t CAN1_0xxf5_Tx_Data[8];
uint8_t CAN1_0xxf6_Tx_Data[8];
uint8_t CAN1_0xxf7_Tx_Data[8];
uint8_t CAN1_0xxf8_Tx_Data[8];

这里创建一个两个CAN的结构体用于接收数据


Struct_CAN_Manage_Object CAN1_Manage_Object = {0};
Struct_CAN_Manage_Object CAN2_Manage_Object = {0};

CAN过滤器

/*** @brief 配置CAN的过滤器** @param hcan CAN编号* @param Object_Para 筛选器编号0-27 | FIFOx | ID类型 | 帧类型* @param 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)
{// 检测传参是否正确assert_param(hcan != NULL);// CAN过滤器初始化结构体CAN_FilterTypeDef can_filter_init_structure;// 滤波器序号, 0-27, 共28个滤波器can_filter_init_structure.FilterBank = Object_Para >> 3;// 滤波器模式,设置ID掩码模式can_filter_init_structure.FilterMode = CAN_FILTERMODE_IDMASK;if ((Object_Para & 0x02)){// 29位 拓展帧//  32位滤波can_filter_init_structure.FilterScale = CAN_FILTERSCALE_32BIT;// 验证码 高16bitcan_filter_init_structure.FilterIdHigh = (ID << 3) >> 16;// 验证码 低16bitcan_filter_init_structure.FilterIdLow = ID << 3 | (Object_Para & 0x03) << 1;// 屏蔽码 高16bitcan_filter_init_structure.FilterMaskIdHigh = (Mask_ID << 3) >> 16;// 屏蔽码 低16bitcan_filter_init_structure.FilterMaskIdLow = Mask_ID << 3 | (0x03) << 1;}else{// 11位 标准帧//  32位滤波can_filter_init_structure.FilterScale = CAN_FILTERSCALE_16BIT;// 标准帧验证码 高16bit不启用can_filter_init_structure.FilterIdHigh = 0x0000;// 验证码 低16bitcan_filter_init_structure.FilterIdLow = ID << 5 | (Object_Para & 0x02) << 4;// 标准帧屏蔽码 高16bit不启用can_filter_init_structure.FilterMaskIdHigh = 0x0000;// 屏蔽码 低16bitcan_filter_init_structure.FilterMaskIdLow = (Mask_ID << 5) | 0x01 << 4;}// 滤波器绑定FIFO0或FIFO1can_filter_init_structure.FilterFIFOAssignment = (Object_Para >> 2) & 0x01;// 从机模式选择开始单元 , 前14个在CAN1, 后14个在CAN2can_filter_init_structure.SlaveStartFilterBank = 14;// 使能滤波器can_filter_init_structure.FilterActivation = ENABLE;// 过滤器配置if (HAL_CAN_ConfigFilter(hcan, &can_filter_init_structure) != HAL_OK){Error_Handler();}
}

CAN初始化函数

/*** @brief 初始化CAN总线** @param hcan CAN编号* @param Callback_Function 处理回调函数*/
void CAN_Init(CAN_HandleTypeDef *hcan, CAN_Call_Back Callback_Function)
{if (hcan->Instance == CAN1){CAN1_Manage_Object.CAN_Handler = hcan;CAN1_Manage_Object.Callback_Function = Callback_Function;//         can_filter_mask_config(hcan, CAN_FILTER(0) | CAN_FIFO_0 | CAN_STDID | CAN_DATA_TYPE, 0x200 ,0x7F8);  //只接收0x200-0x207//         can_filter_mask_config(hcan, CAN_FILTER(1) | CAN_FIFO_1 | CAN_STDID | CAN_DATA_TYPE, 0x200, 0x7F8);
//        can_filter_mask_config(hcan, CAN_FILTER(0) | CAN_FIFO_0 | CAN_STDID | CAN_DATA_TYPE, 0x200, 0x7F8);
//        can_filter_mask_config(hcan, CAN_FILTER(1) | CAN_FIFO_1 | CAN_STDID | CAN_DATA_TYPE, 0x200, 0x7F8);can_filter_mask_config(hcan, CAN_FILTER(0) | CAN_FIFO_0 | CAN_STDID | CAN_DATA_TYPE, 0, 0); // 只接收can_filter_mask_config(hcan, CAN_FILTER(1) | CAN_FIFO_1 | CAN_STDID | CAN_DATA_TYPE, 0, 0);}else if (hcan->Instance == CAN2){CAN2_Manage_Object.CAN_Handler = hcan;CAN2_Manage_Object.Callback_Function = Callback_Function;can_filter_mask_config(hcan, CAN_FILTER(14) | CAN_FIFO_0 | CAN_STDID | CAN_DATA_TYPE, 0, 0); // 只接收can_filter_mask_config(hcan, CAN_FILTER(15) | CAN_FIFO_1 | CAN_STDID | CAN_DATA_TYPE, 0, 0);}/*离开初始模式*/HAL_CAN_Start(hcan);/*开中断*/HAL_CAN_ActivateNotification(hcan, CAN_IT_RX_FIFO0_MSG_PENDING); // can 接收fifo 0不为空中断HAL_CAN_ActivateNotification(hcan, CAN_IT_RX_FIFO1_MSG_PENDING); // can 接收fifo 1不为空中断
}

在这个函数里面利用我们的结构体到HAL的库的底层,分别初始化了CAN1 CAN2,

然后 用了滤波器 (一定要加上这个滤波器要不然收不到数据的)

这个滤波器里面写了起始段 仲裁段 控制段 的内容

CAN发送数据函数

/*** @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));
}

CAN报文回调函数

/*** @brief CAN报文回调函数** @param Rx_Buffer CAN接收的信息结构体*/
void CAN_Motor_Call_Back(Struct_CAN_Rx_Buffer *Rx_Buffer)
{uint8_t *Rx_Data = Rx_Buffer->Data;switch (Rx_Buffer->Header.StdId){//3508电机会传数据回来,之前发送的ID是0x200//所以接收的ID就分别是0x201,0x202,0x203,0x204,0x205,0x206,0x207,0x208//电机的ID是几对应的就是0x20几//要注意的是 电机ID1-4对应0x200 ID5-8对应0x1ffcase (0x202):{Rx_Encoder = (Rx_Data[0] << 8) | Rx_Data[1];Rx_Omega = (Rx_Data[2] << 8) | Rx_Data[3];Rx_Torque = (Rx_Data[4] << 8) | Rx_Data[5];Rx_Temperature = Rx_Data[6];}break;}
}

HAL库CAN接收FIFO中断

/*** @brief HAL库CAN接收FIFO0中断** @param hcan CAN编号*/
void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan)
{// CAN_RxHeaderTypeDef header;// uint8_t data;// HAL_CAN_GetRxMessage(hcan, CAN_FILTER_FIFO0, &header, &data);//选择回调函数if (hcan->Instance == CAN1){HAL_CAN_GetRxMessage(hcan, CAN_FILTER_FIFO0, &CAN1_Manage_Object.Rx_Buffer.Header, CAN1_Manage_Object.Rx_Buffer.Data);CAN1_Manage_Object.Callback_Function(&CAN1_Manage_Object.Rx_Buffer);}else if (hcan->Instance == CAN2){HAL_CAN_GetRxMessage(hcan, CAN_FILTER_FIFO0, &CAN2_Manage_Object.Rx_Buffer.Header, CAN2_Manage_Object.Rx_Buffer.Data);CAN2_Manage_Object.Callback_Function(&CAN2_Manage_Object.Rx_Buffer);}}/*** @brief HAL库CAN接收FIFO1中断** @param hcan CAN编号*/
void HAL_CAN_RxFifo1MsgPendingCallback(CAN_HandleTypeDef *hcan)
{// 选择回调函数if (hcan->Instance == CAN1){HAL_CAN_GetRxMessage(hcan, CAN_FILTER_FIFO1, &CAN1_Manage_Object.Rx_Buffer.Header, CAN1_Manage_Object.Rx_Buffer.Data);CAN1_Manage_Object.Callback_Function(&CAN1_Manage_Object.Rx_Buffer);}else if (hcan->Instance == CAN2){HAL_CAN_GetRxMessage(hcan, CAN_FILTER_FIFO1, &CAN2_Manage_Object.Rx_Buffer.Header, CAN2_Manage_Object.Rx_Buffer.Data);CAN2_Manage_Object.Callback_Function(&CAN2_Manage_Object.Rx_Buffer);}
}

以上就是所有的偏底层一些的代码了

接下来我们直接调用就可以了

主函数

#include "main.h"
#include "can.h"
#include "gpio.h"#include "drv_can.h"int16_t torque = 500;/* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM *//* USER CODE END PM *//* Private variables ---------------------------------------------------------*//* USER CODE BEGIN PV *//* USER CODE END PV *//* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
/* USER CODE BEGIN PFP *//* USER CODE END PFP *//* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 *//* USER CODE END 0 *//*** @brief  The application entry point.* @retval int*/
int main(void)
{/* USER CODE BEGIN 1 *//* USER CODE END 1 *//* MCU Configuration--------------------------------------------------------*//* Reset of all peripherals, Initializes the Flash interface and the Systick. */HAL_Init();/* USER CODE BEGIN Init *//* USER CODE END Init *//* Configure the system clock */SystemClock_Config();/* USER CODE BEGIN SysInit *//* USER CODE END SysInit *//* Initialize all configured peripherals */MX_GPIO_Init();MX_CAN1_Init();/* USER CODE BEGIN 2 */CAN_Init(&hcan1, CAN_Motor_Call_Back);/* USER CODE END 2 *//* Infinite loop *//* USER CODE BEGIN WHILE */while (1){CAN1_0x200_Tx_Data[2] = (torque >> 8) & 0xFF;CAN1_0x200_Tx_Data[3] = torque & 0xFF;CAN_Send_Data(&hcan1,0x200,CAN1_0x200_Tx_Data,8);/* USER CODE END WHILE *//* USER CODE BEGIN 3 */}/* USER CODE END 3 */
}

大疆电机的控速是直接给电流的

他有一个映射的范围

我们的传过去的数据是2字节的所以采用

CAN1_0x200_Tx_Data[2] = (torque >> 8) & 0xFF;
CAN1_0x200_Tx_Data[3] = torque & 0xFF;

的方法来写入。

根据3508电机的使用手册,要根据不同的ID来往不同的位里面写数据。

总结

总结起来,如果我们不看底层的话,实际上要我们调用的函数其实很少。

就跑一个初始化CAN_Init

把数据写入相对应的数组内

再用CAN_Send_Data发送出去就好了

然后我们也可以在CAN_Motor_Call_Back这个回调函数中接收电调发来的消息,进行一些其他的处理等等。



本文参考链接如下:

中科大RM电控合集

HAL库STM32常用外设—— CAN通信(一)_stm32 hal can-CSDN博客

趋近于完美的通讯 CAN总线!4分钟看懂!

http://www.xdnf.cn/news/19045.html

相关文章:

  • VisualStudio 将xlsx文件嵌入到资源中访问时变String?
  • HTML 和 JavaScript 关联的基础教程
  • LeetCode 刷题【56. 合并区间】
  • Linux - 中文显示乱码问题解决方法(编码查看及转换)- 学习/实践
  • 【Spring Cloud微服务】6.通信的利刃:深入浅出 Spring Cloud Feign 实战与原理
  • 智能体开发:学习与实验 ReAct
  • web端播放flv视频流demo(flv.js的使用)
  • API 月度更新汇总:ONLYOFFICE 协作空间文档
  • 【RAG Agent实战】告别“单线程”RAG:用查询理解与LangGraph构建能处理复杂意图的高级代理
  • WPF+IOC学习记录
  • 学习Java30天(tcp的多开客户端和bs架构以及java高级)
  • 群核科技--SpatialGen
  • 毕马威 —— 公众对人工智能的信任、态度及使用情况调查
  • OpenHarmony设备使用统计深度实战:从数据埋点到信息采集的全链路方案
  • matlab利用模糊算法控制PID参数实现模糊控制
  • echo、seq、{}、date、bc命令
  • Shell 秘典(卷二)——号令延展秘术 与 流程掌控心法・if 天机判语篇精解
  • SpringMvc下
  • log4jshell CVE-2021-44228 复现
  • 智能标签分类:新一代任务管理工具的进化方向
  • LangChain如何使用通义千问的向量模型
  • 【C语言入门级教学】sizeof和strlen的对⽐
  • Java使用apache.commons.math3的DBSCAN实现自动聚类
  • HTML 核心标签全解析:从文本排版到媒体嵌入
  • vue3中安装tailwindcss
  • C++函数继承
  • docker 搭建zookper集群,快照虚拟机多机模拟
  • 园区智慧水电管理系统:让能源管理从“成本黑洞”变“利润引擎”
  • 【实时Linux实战系列】实时数据可视化技术实现
  • 【机器学习】 12 Latent linear models