STM32基于can总线通信控制多个舵机/电机原理及代码

CAN(Controller Area Network)总线是一种广泛用于汽车和工业领域的串行通信协议,由德国博世公司在1986年首次提出。它最初设计用于汽车内部的各种电子控制单元(ECU)之间的通信,但由于其高可靠性、实时性和抗干扰能力,现在也被广泛应用于其他领域,如工业自动化、医疗设备、航空航天等。

此次实战演示效果为一个stm32通过按键给另一个stm32发送消息,另一个stm32接受到消息后显示到屏幕上并处理收到的消息控制舵机。演示视频在同名b站已发布,下面是视频链接:

【两个stm32的can通信及控制多个舵机-哔哩哔哩】 https://b23.tv/WHhrOWn

下图是几种主要通信方式的特点及对比:

1.主要特点

  1. 多主结构:CAN总线支持多主模式,任何节点都可以在总线空闲时开始传输消息,这通过非破坏性仲裁机制实现。

  2. 非破坏性仲裁:当多个节点同时发送消息时,CAN总线使用标识符(ID)进行仲裁。标识符数值越低,优先级越高。在仲裁过程中,优先级高的消息继续发送,而优先级低的消息自动退出发送,不会造成数据损坏。

  3. 高可靠性:CAN总线具有错误检测、错误通知和错误恢复机制。它包括循环冗余校验(CRC)、帧检查、应答错误和形式错误等。

  4. 通信速率:CAN总线的通信速率可达1 Mbps(在短距离内),距离较长时,速率会降低。例如,在40米内可达1 Mbps,而在1000米内可达50 kbps。

  5. 连接节点数:理论上,CAN总线可以连接多达110个节点,实际数量取决于总线驱动器和网络负载。

  6. 消息格式:CAN总线有两种消息格式:标准帧(11位标识符)和扩展帧(29位标识符)。

**2.CAN消息帧类型:**最常用的是数据帧。

3.数据帧结构:

标准数据帧(11位ID) 由以下字段组成:

  • 帧起始(Start of Frame, SOF):一个显性位(0),表示帧的开始。

  • 仲裁场(Arbitration Field):

    • 11位标识符(ID) + 远程传输请求位(RTR,0表示数据帧)。
  • 控制场(Control Field):包括标识符扩展位(IDE,0表示标准帧)、保留位和数据长度码(DLC,0-8字节)。

  • 数据场(Data Field):0-8字节的数据。

  • CRC场(CRC Field):15位CRC校验和 + 1位CRC界定符。

  • 应答场(ACK Field):包括应答间隙和应答界定符。

  • 帧结束(End of Frame, EOF):7个连续的隐性位(1)。

扩展数据帧(29位ID) 与标准帧类似,但仲裁场包括29位标识符。

4.错误处理:

5.stm32的can外设:

STM32的CAN外设称为**bxCAN(Basic Extended CAN)**其主要特点有:

  • 支持CAN协议:兼容CAN 2.0A和2.0B主动模式,支持标准帧(11位标识符)和扩展帧(29位标识符)。

  • 通信速率:最高可达1 Mbps。

  • 四种工作模式:正常模式、静默模式、环回模式和环回静默模式(用于自测试)。

  • 可配置的位时序:允许用户根据总线时钟和波特率要求设置位时序参数。

  • 发送和接收:具有3个发送邮箱和2个接收FIFO(每个FIFO可存储3个报文),以及可配置的过滤器组(最多28个,取决于型号)。

  • 高级错误管理:包括错误计数器和错误状态指示。

  • 时间触发通信:支持时间触发通信模式(TTCAN)级别1。

6.can收发器模块:TJA1050

理论部分介绍的差不多了接下来给大家介绍实战了,虽然理论看起来很复杂但实际上stm32的库函数已经帮大家都封装好了,我们只需进行简单的配置即可使用,所以说使用起来并不难,大家可以先入门学会基本使用,后面需要过滤,遥控等其它功能再着重的加强学习。

7.硬件介绍

为了方便就直接用了我之前项目的pcb板子,因为舵机和电机都是pwm控制的,这里就选用了舵机来演示,原理是一样的,能控制舵机自然就会电机了。(注:这里硬件连接有一个问题,我的两个TJA1050的电源和地线都接到了右边板子上,这样会导致只能右边32给左边的32发送消息,但在此次演示中不影响)

8.软件介绍

main.c

cs 复制代码
uint8_t KeyNum;
  
uint8_t yxy=0;
uint8_t yzc=0;

CanTxMsg TxMsgArray[] = {
/*   StdId     ExtId         IDE             RTR        DLC         Data[8]        */
	{0x555, 0x00000000, CAN_Id_Standard, CAN_RTR_Data,   4, {0x11, 0x22, 0x33, 0x44}},
	{0x000, 0x12345678, CAN_Id_Extended, CAN_RTR_Data,   4, {0xAA, 0xBB, 0xCC, 0xDD}},
	{0x666, 0x00000000, CAN_Id_Standard, CAN_RTR_Data,   4, {0x01, 0x02, 0x03, 0x04}},
	{0x000, 0x0789ABCD, CAN_Id_Extended, CAN_RTR_Data,   4, {0x05, 0x06, 0x07, 0x08}},
};//CAN_RTR_Remote,在普通模式下,遥控帧通信需要另一个节点响应数据
uint8_t pTxMsgArray = 0;

void RCC_Configuration(void) {
    RCC_DeInit();
    SystemInit();
}

int main(void)
{
	delay_init();	    	 		//延时函数初始化	  
	RCC_Configuration();            //系统时钟初始化
	KEY_Init();	                    //按键初始化
	EXTIX_Init();					//外部中断初始化	
	MyCAN_Init();                   //can初始化
	Servo_Init();                   //舵机初始化
	LED_Init();                     //LED初始化
	OLED_Init();                    //oled初始化
	OLED_Clear();                   //oled初始化后清屏
	
	OLED_ShowString(0, 0, " Rx :",16);
	OLED_ShowString(0, 2, "RxID:",16);
	OLED_ShowString(0, 4, "Leng:",16);
	OLED_ShowString(0, 6, "Data:",16);
	
	while (1)
	{	  
        //按键发送can消息		
		KeyNum = Key_GetNum();//按键扫描
		if(KeyNum == 1)//B3
		{
		  MyCAN_Transmit(&TxMsgArray[pTxMsgArray]);
			
			pTxMsgArray ++;
			if (pTxMsgArray >= sizeof(TxMsgArray) / sizeof(CanTxMsg))
			{
				pTxMsgArray = 0;
			}
		}
		else if(KeyNum == 2)//B4
		{
		  MyCAN_Transmit(&TxMsgArray[pTxMsgArray]);
			
			pTxMsgArray ++;
			if (pTxMsgArray >= sizeof(TxMsgArray) / sizeof(CanTxMsg))
			{
				pTxMsgArray = 0;
			}
		}		
		
	  //处理中断接收到的can消息
		if (MyCAN_RxFlag == 1)//中断标志位
		{
			MyCAN_RxFlag = 0;
			
			if (MyCAN_RxMsg.IDE == CAN_Id_Standard)//标准帧
			{
				OLED_ShowString(45, 0, "Std",16);
				
				OLED_ShowHexNum(45, 2, MyCAN_RxMsg.StdId, 8);
			}
			else if (MyCAN_RxMsg.IDE == CAN_Id_Extended)//拓展帧
			{
				OLED_ShowString(45, 0, "Ext",16);
				
				OLED_ShowHexNum(45, 2, MyCAN_RxMsg.ExtId, 8);
			}
			
			if (MyCAN_RxMsg.RTR == CAN_RTR_Data)//数据帧
			{
				OLED_ShowString(75, 0, "Data  ",16);
				
				OLED_ShowHexNum(45, 4, MyCAN_RxMsg.DLC, 1);
				
				OLED_ShowHexNum(45, 6, MyCAN_RxMsg.Data[0], 2);
				OLED_ShowHexNum(63, 6, MyCAN_RxMsg.Data[1], 2);
				OLED_ShowHexNum(81, 6, MyCAN_RxMsg.Data[2], 2);
				OLED_ShowHexNum(99, 6, MyCAN_RxMsg.Data[3], 2);
				//根据接受到的数据控制舵机(可自行diy)
				if(MyCAN_RxMsg.Data[0] == 0x11){GPIO_ResetBits(GPIOA, GPIO_Pin_11);Servo_SetAngle(0);}
				if(MyCAN_RxMsg.Data[0] == 0xAA){GPIO_ResetBits(GPIOA, GPIO_Pin_12);Servo_SetAngle1(0);}
				if(MyCAN_RxMsg.Data[0] == 0x01){GPIO_SetBits(GPIOA, GPIO_Pin_11);Servo_SetAngle(60);}
				if(MyCAN_RxMsg.Data[0] == 0x05){GPIO_SetBits(GPIOA, GPIO_Pin_12);Servo_SetAngle1(60);}
			}
			else if (MyCAN_RxMsg.RTR == CAN_RTR_Remote)//遥控帧
			{
				OLED_ShowString(75, 0, "Remote",16);
				
				OLED_ShowHexNum(45, 4, MyCAN_RxMsg.DLC, 1);
				
				OLED_ShowHexNum(45, 6, 0x00, 2);
				OLED_ShowHexNum(63, 6, 0x00, 2);
				OLED_ShowHexNum(81, 6, 0x00, 2);
				OLED_ShowHexNum(99, 6, 0x00, 2);
			}
		}		
		
	}
}
		

一般情况下都是一个帧id对应一个电机或舵机,然后通过帧数据来控制电机和舵机的运动,我这里就简单演示一下没搞那么复杂,有需要的小伙伴可自行diy代码。对于电机的代码,我之前也发布过步进电机和无刷电机的控制代码文章,大家可自行结合之前的电机代码自行diy。

can.c

cs 复制代码
#include "stm32f10x.h"                  // Device header
#include "MyCAN.h"
#include "delay.h"

CanRxMsg MyCAN_RxMsg;
uint8_t MyCAN_RxFlag;

void MyCAN_Init(void)
{
  CAN_DeInit(CAN1);// 先关闭CAN
	delay_ms(10);//延时一会
	
  RCC_APB1PeriphClockCmd(RCC_APB1Periph_CAN1, ENABLE);
  RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);  // 必须开启AFIO时钟
  RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); // PB8, PB9 时钟
	
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;//can_tx
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOB, &GPIO_InitStructure);
	
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8;//can_rx
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOB, &GPIO_InitStructure);
	
  GPIO_PinRemapConfig(GPIO_Remap1_CAN1, ENABLE); //PB8, PB9 重映射can功能
	
	//开启中断
	CAN_ITConfig(CAN1, CAN_IT_FMP0, ENABLE);
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
	NVIC_InitTypeDef NVIC_InitStructure;
	NVIC_InitStructure.NVIC_IRQChannel = USB_LP_CAN1_RX0_IRQn;
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
	NVIC_Init(&NVIC_InitStructure);

  delay_ms(5);//延时一会
  
	CAN_InitTypeDef CAN_InitStructure;
	CAN_StructInit(&CAN_InitStructure);
	CAN_InitStructure.CAN_Mode = CAN_Mode_Normal;//LoopBack    Normal
	CAN_InitStructure.CAN_Prescaler = 48;		//波特率 = 36M / 48 / (1 + 2 + 3) = 125K
	CAN_InitStructure.CAN_BS1 = CAN_BS1_2tq;
	CAN_InitStructure.CAN_BS2 = CAN_BS2_3tq;
	CAN_InitStructure.CAN_SJW = CAN_SJW_2tq;
	CAN_InitStructure.CAN_NART = ENABLE;// 禁用自动重传,避免阻塞(不堵塞但第一次通信所需同步时间较长)
	CAN_InitStructure.CAN_TXFP = DISABLE;
	CAN_InitStructure.CAN_RFLM = DISABLE;
	CAN_InitStructure.CAN_AWUM = ENABLE;// 自动唤醒
	CAN_InitStructure.CAN_TTCM = DISABLE;
	CAN_InitStructure.CAN_ABOM = ENABLE;// 自动总线恢复
	CAN_Init(CAN1, &CAN_InitStructure);
	
	//过滤器配置
	CAN_FilterInitTypeDef CAN_FilterInitStructure;
	CAN_FilterInitStructure.CAN_FilterNumber = 0;
	CAN_FilterInitStructure.CAN_FilterIdHigh = 0x0000;
	CAN_FilterInitStructure.CAN_FilterIdLow = 0x0000;
	CAN_FilterInitStructure.CAN_FilterMaskIdHigh = 0x0000;
	CAN_FilterInitStructure.CAN_FilterMaskIdLow = 0x0000;
	CAN_FilterInitStructure.CAN_FilterScale = CAN_FilterScale_32bit;
	CAN_FilterInitStructure.CAN_FilterMode = CAN_FilterMode_IdMask;
	CAN_FilterInitStructure.CAN_FilterFIFOAssignment = CAN_Filter_FIFO0;
	CAN_FilterInitStructure.CAN_FilterActivation = ENABLE;
	CAN_FilterInit(&CAN_FilterInitStructure);
}

void MyCAN_Transmit(CanTxMsg *TxMessage)
{
	uint8_t TransmitMailbox = CAN_Transmit(CAN1, TxMessage);
	
	uint32_t Timeout = 0;
	while (CAN_TransmitStatus(CAN1, TransmitMailbox) != CAN_TxStatus_Ok)
	{
		Timeout ++;
		if (Timeout > 100000)
		{
			break;
		}
	}
}

void USB_LP_CAN1_RX0_IRQHandler(void)
{
	if (CAN_GetITStatus(CAN1, CAN_IT_FMP0) == SET)
	{
		CAN_Receive(CAN1, CAN_FIFO0, &MyCAN_RxMsg);
		MyCAN_RxFlag = 1;
	}
}

因为我使用的板子A11和A12引脚连接了LED会影响can总线通信,故重映射了B8和B9的can引脚,一样的不影响使用,此外我在环回模式下单设备测试时没任何问题,但普通模式下两设备通信时会出现通信堵塞的现象,故将can的NART配置成了ENABLE后不堵塞了但是两个设备间第一次通信的同步所需要的时间会有点长,但不影响使用,如果有大佬能够解决欢迎评论区留言。

can.h

cs 复制代码
#ifndef __MYCAN_H
#define __MYCAN_H

void MyCAN_Init(void);
void MyCAN_Transmit(CanTxMsg *TxMessage);
void MyCAN_Init(void);
extern CanRxMsg MyCAN_RxMsg;
extern uint8_t MyCAN_RxFlag;


#endif

还有按键,oled,led,舵机等的代码这里就不一一列举了,有需要的朋友可一键三联后私聊获取完整源码。此次分享就到这里了,感谢支持!

相关推荐
武文斌773 小时前
项目学习总结:CAN总线、摄像头、STM32概述
linux·arm开发·stm32·单片机·嵌入式硬件·学习·c#
淘晶驰AK5 小时前
主流的 MCU 开发语言为什么是 C 而不是 C++?
c语言·开发语言·单片机
云山工作室11 小时前
2025年单片机毕业设计选题物联网计算机电气电子通信类
单片机·物联网·课程设计
Ching·13 小时前
STM32L4xx编译提示Keil MDK Warning: L6989W警告问题及其解决办法
stm32·单片机·嵌入式硬件
小莞尔13 小时前
【51单片机】【protues仿真】基于51单片机温度测量系统
c语言·单片机·嵌入式硬件·物联网·51单片机
晓风凌殇13 小时前
单片机按键检测与长短按识别实现
c语言·单片机
Zaki_gd15 小时前
GPIO 引脚速度(Speed)
单片机·嵌入式硬件
武文斌7715 小时前
复习总结最终版:单片机
linux·单片机·嵌入式硬件·学习
xiaofei55800816 小时前
CAN 波特率的几个参数说明和计算方式(以STM32为例)
单片机·嵌入式硬件·汽车