本次实验目的是通过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通讯参考:文章来源:https://www.toymoban.com/news/detail-400125.html
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文章来源地址https://www.toymoban.com/news/detail-400125.html
到了这里,关于STM32F103C8T6实现CAN通讯与直流编码电机转速闭环控制的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!