STM32F103C8T6实现CAN通讯与直流编码电机转速闭环控制:实践与应用

本次实验目的是通过CAN发送目标转速与转向信息,接收方在接到CAN数据流后开始对直流编码电机进行转速闭环控制。我会尽量说清每个函数,注释每一句代码,希望能对大家有所帮助。

CAN通讯基于STM32自带CAN通讯模块,配合库函数使用十分方便。关于CAN通讯可以参考站内大佬的文章,讲解的十分透彻,末尾会提供链接。 

电机驱动基于定时器1和TB6612,转速测量基于定时器2和直流电机自带编码器。

另外,可通过三个LED来显示电机状态(正转,反转和停止);通过OLED来显示转速和其他信息(如PI输出)。

目录

1.CAN通讯驱动

2.直流电机驱动(PWM)

3.直流电机驱动(转向和转速控制)

4.编码器驱动

5.PI转速闭环控制

1.CAN通讯驱动

因为目前手上只有一个STM32的最小系统板,所以采用CAN通讯的回传模式。这部分的函数包括:CAN配置函数,CAN接收中断服务函数和CAN发送函数。为了简便,我将CAN接收的数据设为外部可调用变量。具体代码如下:

#include "stm32f10x.h"                  // Device header

uint32_t get_STID;//存储标准ID
uint32_t get_EXID;//存储拓展ID
uint8_t get_IDE;//标准/拓展ID识别
uint8_t get_RTR;//数据/遥控帧识别
uint8_t get_DLC;//数据长度识别
uint8_t get_DATA[8];//存储数据
uint8_t get_FMI;//识别所经过的筛选器

void CAN_INIT(void)//CAN初始化函数
{
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);//开GPIOA时钟
	GPIO_InitTypeDef GPIO_InitStructure;//GPIO配置
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;//配置为复用推挽
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_11|GPIO_Pin_12;//打开11,12引脚
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA,&GPIO_InitStructure);
	
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_CAN1, ENABLE);//开CAN时钟
	CAN_InitTypeDef CAN_InitStructure;//配置CAN
	CAN_InitStructure.CAN_ABOM = DISABLE;//关动休眠
	CAN_InitStructure.CAN_AWUM = DISABLE;//关动唤醒
	CAN_InitStructure.CAN_BS1 = CAN_BS1_5tq;//TBS1长度5Tq
	CAN_InitStructure.CAN_BS2 = CAN_BS2_3tq;//TBS2长度5Tq
	CAN_InitStructure.CAN_Mode = CAN_Mode_LoopBack;//工作模式选择(单机实验下位回传模式)
	CAN_InitStructure.CAN_NART = ENABLE;//禁止重发
	CAN_InitStructure.CAN_Prescaler = 80;//80分频(最终得到10kB速率)
	CAN_InitStructure.CAN_RFLM = DISABLE;//不锁定FIFO
	CAN_InitStructure.CAN_SJW = CAN_SJW_2tq;//最大调整步长2Tq
	CAN_InitStructure.CAN_TTCM = DISABLE;//关闭时间触发
	CAN_InitStructure.CAN_TXFP = DISABLE;//发送按ID优先级,不按邮箱顺序
	CAN_Init(CAN1, &CAN_InitStructure);
	
	CAN_FilterInitTypeDef CAN_FilterInitStructure;//CAN筛选器配置
	CAN_FilterInitStructure.CAN_FilterActivation = ENABLE;//使能筛选器
	CAN_FilterInitStructure.CAN_FilterFIFOAssignment = CAN_Filter_FIFO0;//启用FIFO0
	CAN_FilterInitStructure.CAN_FilterIdHigh = 0x00;
	CAN_FilterInitStructure.CAN_FilterIdLow = 0x00;
	CAN_FilterInitStructure.CAN_FilterMaskIdHigh = 0x00;
	CAN_FilterInitStructure.CAN_FilterMaskIdLow = 0x00;//实际上可通过任意ID
	CAN_FilterInitStructure.CAN_FilterMode = CAN_FilterMode_IdMask;//掩码模式
	CAN_FilterInitStructure.CAN_FilterNumber = 0;//选择筛选器组0
	CAN_FilterInitStructure.CAN_FilterScale = CAN_FilterScale_32bit;//32位长度	
	CAN_FilterInit(&CAN_FilterInitStructure);
	
//	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//2个抢占2个响应,这句代码已在定时器中短通道配置部分给出,这里不再需要
	
	NVIC_InitTypeDef NVIC_InitStructure;
	NVIC_InitStructure.NVIC_IRQChannel = USB_LP_CAN1_RX0_IRQn;//指向CAN接收中断,定义在中容量量产非互联型,需要注意一下
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;//通道使能
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;//最高抢占等级
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;//第2响应等级
	NVIC_Init(&NVIC_InitStructure);
	
	CAN_ITConfig(CAN1, CAN_IT_FMP0, ENABLE);//使能接收中断
}

void USB_LP_CAN1_RX0_IRQHandler()//CAN接收中断服务函数。非互联型使用PA11、12引脚时,使用该中断函数名
{
	CanRxMsg RxMessage;//接收数据结构体
	CAN_Receive(CAN1, CAN_FIFO0, &RxMessage);//接收函数
	
	get_DLC = RxMessage.DLC;//接下数据长度
	get_EXID = RxMessage.ExtId;//接下拓展ID
	get_FMI = RxMessage.FMI;//接下所经过的筛选器
	get_IDE = RxMessage.IDE;//标准/拓展ID识别
	get_RTR = RxMessage.RTR;//数据/遥控帧识别
	get_STID = RxMessage.StdId;//接下标准ID
	
	get_DATA[0] = RxMessage.Data[0];
	get_DATA[1] = RxMessage.Data[1];
	get_DATA[2] = RxMessage.Data[2];
	get_DATA[3] = RxMessage.Data[3];
	get_DATA[4] = RxMessage.Data[4];
	get_DATA[5] = RxMessage.Data[5];
	get_DATA[6] = RxMessage.Data[6];
	get_DATA[7] = RxMessage.Data[7];
}

void send_CAN(uint32_t STID, uint32_t EXID, uint8_t IDE, uint8_t RTR, uint8_t DLC, uint8_t DATA[8])//CAN发送函数
{
	CanTxMsg TxMessage;
	TxMessage.DLC = DLC;
	TxMessage.StdId = STID;
	TxMessage.ExtId = EXID;
	TxMessage.IDE = IDE;
	TxMessage.RTR = RTR;	
	
	TxMessage.Data[0] = DATA[0];
	TxMessage.Data[1] = DATA[1];
	TxMessage.Data[2] = DATA[2];
	TxMessage.Data[3] = DATA[3];
	TxMessage.Data[4] = DATA[4];
	TxMessage.Data[5] = DATA[5];
	TxMessage.Data[6] = DATA[6];
	TxMessage.Data[7] = DATA[7];
	
	CAN_Transmit(CAN1, &TxMessage);
}

再将各个函数以及变量在头文件中声明一下:

#ifndef __CAN_H
#define __CAN_H

extern uint32_t get_STID;//存储标准ID
extern uint32_t get_EXID;//存储拓展ID
extern uint8_t get_IDE;//标准/拓展ID识别
extern uint8_t get_RTR;//数据/遥控帧识别
extern uint8_t get_DLC;//数据长度识别
extern uint8_t get_DATA[8];//存储数据
extern uint8_t get_FMI;//识别所经过的筛选器

void CAN_INIT(void);
void send_CAN(uint32_t STID, 
			  uint32_t EXID, 
			  uint8_t IDE, 
			  uint8_t RTR, 
			  uint8_t DLC, 
			  uint8_t DATA[8]);//CAN发送函数

#endif

以上为CAN驱动部分,CAN发送函数可以在主函数中直接调用,通过CAN接收中断服务函数来读数据,并转移至内存。

2.直流电机驱动(PWM)

这部分包括PWM配置函数和PWM占空比调节函数。PWM基于定时器1,具体代码如下:

#include "stm32f10x.h"                  // Device header


void TIMER1_INIT(void)//配置带有中断和PWM功能的定时器
{
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);//开GPIOA时钟
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;//复用推挽
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;//10引脚
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA,&GPIO_InitStructure);
	
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1, ENABLE);//开定时器1时钟
	TIM_InternalClockConfig(TIM1);//定时器1采用内部时钟
	TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;//时基单元配置
	TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;//不分频
	TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;//上升计数模式
	TIM_TimeBaseInitStructure.TIM_Period = 100-1;
	TIM_TimeBaseInitStructure.TIM_Prescaler = 72-1;//72分频配合100的计数周期,则每秒进10k次中断,即载频为10kHz
	TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;
	TIM_TimeBaseInit(TIM1, &TIM_TimeBaseInitStructure);
	
	TIM_OCInitTypeDef TIM_OCInitStructure;//比较器配置
	TIM_OCStructInit(&TIM_OCInitStructure);//初始化其他未设置的变量
	TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;//高电平有效
	TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;//极性不翻转
	TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;//使能比较输出
	TIM_OCInitStructure.TIM_Pulse = 0;//比较器装载值
	TIM_OC3Init(TIM1, &TIM_OCInitStructure);//PA10引脚对应第三通道(OC3)
	TIM_ITConfig(TIM1, TIM_IT_Update, ENABLE);//开定时器中断
	
//	TIM_BDTRInitTypeDef TIM_BDTRInitStructure;//配置死区,直流电机不需要
//	TIM_BDTRInitStructure.TIM_AutomaticOutput = TIM_AutomaticOutput_Enable;
//	TIM_BDTRInitStructure.TIM_Break = TIM_Break_Enable;
//	TIM_BDTRInitStructure.TIM_BreakPolarity = TIM_BreakPolarity_High;
//	TIM_BDTRInitStructure.TIM_DeadTime = 11;
//	TIM_BDTRInitStructure.TIM_LOCKLevel = TIM_LOCKLevel_1;
//	TIM_BDTRInitStructure.TIM_OSSIState = TIM_OSSIState_Enable;
//	TIM_BDTRInitStructure.TIM_OSSRState = TIM_OSSRState_Enable;
//	TIM_BDTRConfig(TIM1, &TIM_BDTRInitStructure);
	TIM_ClearITPendingBit(TIM1, TIM_IT_Update);清中断标志位,防止上电/复位进中断
	TIM_Cmd(TIM1, ENABLE);//开定时器
	TIM_CtrlPWMOutputs(TIM1, ENABLE);//pwm主使能(高级定时器独有)
	
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//中断优先级配置
	
	NVIC_InitTypeDef NVIC_InitStructure;
	NVIC_InitStructure.NVIC_IRQChannel = TIM1_UP_IRQn;//指定到定时器1更新中断
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;//通道使能
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;//最高抢占优先级
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;//最高响应优先级
	NVIC_Init(&NVIC_InitStructure);//配置中断通道
	
	
}

void set_speed(uint8_t speed)//通过调用比较器赋值函数实现调节PWM占空比,从而实现调速
{
	TIM_SetCompare3(TIM1, speed);
}

对于定时器1(高级定时器),别忘了“TIM_CtrlPWMOutputs(TIM1, ENABLE);”。需要将PWM信号送入6612的PWMA引脚。最后将函数和变量在头文件中声明一下:

#ifndef __TIMER1_H
#define __TIMER1_H

void TIMER1_INIT(void);
void set_speed(uint8_t speed);

#endif

3.直流电机驱动(转向和转速控制)

直流电机的转向控制是通过STM32的两个引脚对6612的AIN1和AIN2引脚发送高低电平来控制,转速是通过STM32的PA10引脚送入6612的PWMA引脚的PWM信号来控制。接线如下图所示:

 AIN1和AIN2真值表如下,这里的正反转是相对的,大家可根据需要搭配。

 这部分包括PA6,PA7(转向控制)引脚配置函数,CAN接收数据流解码函数和转速转速转向指定控制。特别的,CAN发送和接收最多8个元素的8位无符号整形数组,本实验用数组的第一个元素指定正反转和停止,0代表停止,1代表正转,2代表反转;用数组的第二个元素来表示转速大小。大家也可采用其他方法,我这里只是为了简单。

在开环状态下,PWM输出饱和时(输出100),直流电机转速rpm=126,所以在转速控制部分需要进行转换:spd=spd*100/126。具体代码如下:

#include "stm32f10x.h"                  // Device header
#include "Timer1.h"
#include "CAN.h"

void DCmotor_INIT(void)//转向控制引脚配置
{
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);//配置H桥控制引脚(旋转方向)
	
	GPIO_InitTypeDef GPIO_InitStructure;
 	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;//推挽输出
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2|GPIO_Pin_3|GPIO_Pin_4|GPIO_Pin_6| GPIO_Pin_7;//2,3,4脚控制三个LED,6,7脚进6612
 	GPIO_Init(GPIOA, &GPIO_InitStructure);	
}

int8_t speed_trans(uint8_t direction, uint8_t speed)//CAN接收数据流解码
{
	int8_t spd;
	direction = get_DATA[0];//转向/停止
	speed = get_DATA[1];//转速大小
	if(direction==1)
	{
		spd = speed;
	}
	else if(direction==2)
	{
		spd = -speed;
	}
	else
	{
		spd = 0;
	}
	return spd;
}

void speed_CTL(int8_t spd)//调速/转向
{
	spd = spd*100/126;
	if(spd>0)//正向旋转
	{
		GPIO_WriteBit(GPIOA, GPIO_Pin_6, Bit_SET);//转向控制
		GPIO_WriteBit(GPIOA, GPIO_Pin_7, Bit_RESET);
		set_speed(spd);
		GPIO_SetBits(GPIOA, GPIO_Pin_2);//LED指示灯
		GPIO_ResetBits(GPIOA, GPIO_Pin_3);
		GPIO_ResetBits(GPIOA, GPIO_Pin_4);
	}
	else if(spd<0)//反向旋转
	{
		GPIO_WriteBit(GPIOA, GPIO_Pin_7, Bit_SET);
		GPIO_WriteBit(GPIOA, GPIO_Pin_6, Bit_RESET);
		set_speed(-spd);
		GPIO_SetBits(GPIOA, GPIO_Pin_4);
		GPIO_ResetBits(GPIOA, GPIO_Pin_3);
		GPIO_ResetBits(GPIOA, GPIO_Pin_2);
	}
	else//停
	{
		GPIO_WriteBit(GPIOA, GPIO_Pin_6, Bit_SET);
		GPIO_WriteBit(GPIOA, GPIO_Pin_7, Bit_SET);
		GPIO_SetBits(GPIOA, GPIO_Pin_3);
		GPIO_ResetBits(GPIOA, GPIO_Pin_4);
		GPIO_ResetBits(GPIOA, GPIO_Pin_2);
	}
}

 最后将函数与变量在头文件声明一下:

#ifndef __DCMOTOR_H
#define __DCMOTOR_H

void DCmotor_INIT(void);
void speed_CTL(int8_t spd);
int8_t speed_trans(uint8_t direction, uint8_t speed);
	
#endif

4.编码器驱动

本次实验直流电机转速测量基于电机自带两相霍尔编码器和定时器2。AB两相编码器脉冲信号需接入定时器2的1,2通道(PA0和PA1)。特别的,编码器电源需接3.3V。

直流电机减速比位1:48,输出轴旋转1周,编码器AB两相一共输出2496个上升和下降沿,因为载频较高,所以每进100次中断(10ms)读取一次定时器2的计数值。当电机正向旋转(从零上升计数)时,输出轴的转速为rpm=计数值*100*60/2496。

当电机反向旋转(从5000向下计数)时,因为电机最高转速为rpm=126,所以计数器在10ms内,得到的数值不会小于4947(正转时不会大于53),所以可以通过判断计数值确定是否反转,反转时,转速为rpm=(计数值-5000)*100*60/2496。具体代码如下:

#include "stm32f10x.h"                  // Device header
#include "CAN.h"

void encoder1_INIT(void)
{
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);//开GPIOA时钟
		
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;//浮空输入
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0|GPIO_Pin_1;//开0,1引脚(定时器2的1,2通道)
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA,&GPIO_InitStructure);//GPIOA初始化
	
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE);//开定时器2时钟
	TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
	TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;//不分频
	TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;//向上计数
	TIM_TimeBaseStructure.TIM_Period = 5000;//计数到5000
	TIM_TimeBaseStructure.TIM_Prescaler = 0;
	TIM_TimeBaseStructure.TIM_RepetitionCounter = 0;
	TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);
	
	TIM_EncoderInterfaceConfig(TIM2, TIM_EncoderMode_TI12,
                                TIM_ICPolarity_Rising, TIM_ICPolarity_Rising);//定时器2编码计数配置,1,2通道同时计数
	TIM_ICInitTypeDef TIM_ICInitStructure;
	TIM_ICStructInit(&TIM_ICInitStructure);
	TIM_ICInitStructure.TIM_ICFilter = 0x00;//不使用滤波
	TIM_ICInit(TIM2, &TIM_ICInitStructure);
	TIM_SetCounter(TIM2,0);//清零定时器计数值
}

int16_t get_rpm(void)//转速计算
{
	int16_t rpm;
	uint16_t count;
	count = TIM_GetCounter(TIM2);//接定时器2编码计数值
	if(count>2500)//如果反转
	{
		rpm = (count-5000)*100*60/2496;
	}
	else
	{
		rpm = (count)*100*60/2496;
	}
	
	return rpm;
}

 最后将函数与变量在头文件声明一下:

#ifndef __ENCODER_H
#define __ENCODER_H

void encoder1_INIT(void);
int16_t get_rpm(void);

#endif

5.PI转速闭环控制

PI转速闭环控制可表示为:PI_out=Kp*err+Ki*err_integral,PI调节的输出量直接送给转速控制。这部分与定时器1中断服务函数以及主函数放在了一页,具体代码如下:

#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "CAN.h"
#include "encoder.h"
#include "DCmotor.h"
#include "Timer1.h"

uint16_t i;
int16_t rpm;
uint16_t count;
int8_t spd_tgt,PI_out;
float RPM,err,err_old_intg,err_intg;//PI调节相关参数
float kp = 0.3;
float ki = 0.015;
float kp_out = 0;
float PI_value = 0;

int main()
{	
	uint8_t DATA[8] = {1,55,2,3,4,5,6,7};//第一个参数控制转向/停止,第二个参数指定转速,限制100
	
	OLED_Init();
	CAN_INIT();
	ADC_INIT();
	TIMER1_INIT();
	DCmotor_INIT();
	encoder1_INIT();
		
	OLED_ShowString(1,1,"CAN Data:");
	OLED_ShowString(3,1,"Speed rpm:");
		
	send_CAN(0x00,               //标准帧ID(uint32)
			 0xFE,               //扩展帧ID(uint32,但只有29位,0 to 0x1FFFFFFF)
	         CAN_Id_Extended,    //标准/拓展ID识别
		     CAN_RTR_Data,       //数据/遥控帧识别
		     8,                  //数据长度识别
			 DATA);              //8个字节数据

	while(1)
	{
		OLED_ShowSignedNum(2, 1, PI_out, 5);
		OLED_ShowSignedNum(4, 1, rpm, 5);
	}
}

int8_t PI(int8_t target_value)//PI调节函数
{
	RPM = get_rpm();//获取测量转速
	err = target_value - RPM;//得到转速偏差
	kp_out = err*kp;
	err_intg = err_old_intg + err;//计算偏差的积累量
	err_old_intg = err_intg;
	PI_value = kp_out + ki*err_intg;//PI输出
	
	if(PI_value>125)//限幅
	{
		PI_value = 125;
	}
	if(PI_value< (-125))
	{
		PI_value= -125;
	}
    PI_out = PI_value;
	return PI_out;
}

void TIM1_UP_IRQHandler(void)//定时器1中断服务函数,执行转速闭环控制
{
	if(TIM_GetITStatus(TIM1, TIM_IT_Update)==SET)//查定时器1中断标志位
	{	
		i++;
		if(i>=100)//每进100次中断,计算一次转速
		{
			rpm = get_rpm();//调用转速计算
			spd_tgt = speed_trans(get_DATA[0], get_DATA[1]);//CAN数据解码为转速(大小,方向)
			PI_out = PI(spd_tgt);//调用PI调节
			speed_CTL(PI_out); //赋予转向/转速			
			TIM_SetCounter(TIM2,0);//定时器重载
			i = 0;
		}
		TIM_ClearITPendingBit(TIM1, TIM_IT_Update);//清定时器1中断标志位
	}
}

实验效果如下图所示,中间的6050模块大家请忽略,与本实验无关。

 目标转速为0

 

  目标转速为36

 

   目标转速为-36

介于水平有限,文中的错误和不足还望大家批评指正,谢谢!

CAN通讯参考:

STM32 CAN通信详解_sgh0609的博客-CSDN博客_stm32can通信转:https://blog.csdn.net/CSDN_Yoa/article/details/81384924 并结合自己项目上CAN的配置觉得该文章很好希望帮助想了解CAN的网友。首先是自己工程中自己的东西分享给大家,后面内筒是转载他人的优秀文档。由于STM32中我使用的扩展标识符(ID)是29位(28~0),STM32的过滤器和掩码是32位分别映射到10~0、28~0、IDE、RTR、0,上;那么我们就可以根据这些内容建立自己的过滤和掩码。其中不建议使用CAN接收中的EXtID,因为掩码中为零.https://blog.csdn.net/sgh69/article/details/121243632?spm=1001.2014.3001.5506

物联沃分享整理
物联沃-IOTWORD物联网 » STM32F103C8T6实现CAN通讯与直流编码电机转速闭环控制:实践与应用

发表评论