Bootstrap

|正点原子PMSM永磁同步电机开发套件|基于STM32F4手写HAL库SVPWM调制方法的PMSM开环控制实践

3.基于SVPWM调制方法的PMSM开环控制实践

3.1 硬件平台

以下都是参考必备硬件

电机开发板(单片机):正点原子电机开发板STM32F407IG;

直流无刷电机驱动板:正点原子ATK-PD6010B模块;

永磁电机:正点原子永磁同步电机;

电源:随便购买的100W/24V开关电源(只要27.3+8(线材))

示波器:正点原子手持示波器,用于测试;

万用表;

杜邦线、双头U插转U插连接线0.5平方线;

3.2软件平台

我这里也有踩坑,比如我最开始是先学习的UP主的HAL库开发,因此使用了UP主推荐的相关开发版本,但用那套开发软件就会有缺包的问题很麻烦,最终我还是使用的正点原子电机开发套件建议的版本。

1.MDK534(KEIL5)

2.CubeMX6.11(正点原子提供了很多版本但是还是要注意使用6.11)

3.VOFA+,串口调试用,可以用这个软件+串口实现马鞍波的观察

3.2 学习前提

首先要学会基于HAL库和CUBEMX的STM32开发,要把基础的外设例如GPIO、定时器、中断、串口、ADC等内容学会。有一点电路知识,比如电机驱动板的输入和输出到底是什么以及死区时间等。对SVPWM调制有一定了解。我本人有一定的STM32库函数开发基础,我一天的时间刷了一下HAL库和CUBEMX的开发流程,推荐B站UP:铁头山羊,他用的是F1最小系统板来讲解这些基础功能,理论加实践的教学方法。因为我有基础所以不需要做他的实验,我主要是看一下HAL库函数以及相关操作就好。如果零基础入门可以先买他的套件进行基础学习,把基础打牢才好!如果也买了正点原子的PMSM开发套装,建议可以刷一下正点原子关于FOC的教学视频,复现一下其使用ST的可视化编程软件进行的PMSM控制,对整体软件硬件有点了解,结合相关开发手册明确各个GPIO口用来干什么的,比如对于其套件,电机开发板需要通过PF10给电机驱动板使能信号才能驱动电机,还有电机驱动板硬件的死区时间大概是100ns,如果不设置软件就会导致电机驱动板过流警告,这都是我基于这套开发套件踩得坑吧。

3.3 开环控制实践

我其实很清楚理论建模和最终落到硬件上差距会很大,很多原理需要转换,也需要明确很多地方的输入输出是什么,有很多数学公式该如何转换到单片机代码上,该如何组织单片机代码等都是问题。鉴于我的目标也是做开发的工作,因此我觉得实践是非常重要的。作为一个刚进入这个领域的小白来讲,如何写这个项目的代码是不清楚的,尤其是开发框架是最重要的,比如该用到哪些外设?整体任务和中断是如何配合的,在没有资料的参考下我肯定是写不出来的,甚至只参考简单的博客资料都是写不出来的,因为开发上是有很多小细节存在的。我其实这段时间用了很多时间精力在搜寻资料上,最开始是参考博客[3]写,我写不出来,然后参考开源代码写,但他是库函数且没有注释的。最后我运气很好找到了B站视频[4]一步一步带着写我才写出来(但也有很多小Bug,要参考他的代码一点点改)。

3.3.1外设设置

1.PWM引脚:TIM1-CH1–PA8、TIM1-CH2–PA9、TIM1-CH3–PA10、TIM1-CH1N–PB13、TIM1-CH2N–PB14、TIM1-CH3N–PB15;

2.电机驱动板使能脚:SHUTDOWN-PF10;

3.定时器:定时器6用于定时执行任务;

4.串口:USART1用于电脑示波器查看马鞍波等串口任务

3.3.2时钟树设置

时钟树设置有一个小坑,我最开始参考别人的资料时Input frequency是25M,然后当我跟着别人设置生存固定频率的PWM波时,发现我的周期是他的1/3,在检查定时器设置没问题后我检查了时钟树,通过阅读开发手册发现这个板子上是一颗8M的晶振,破案了,8/25和1/3差不多就错在这了。因此大家在设置Input frequency的时候要根据自己板载晶振频率来设定。时钟树的设置主要在一APB1和APB2的频率就好,尤其是APB2我们这里生成的PWM波就受到APB2的影响。我这里是168MHZ。

3.3.3GPIO设置

首先设置电机驱动板使能脚、两颗板载LED(用于调试观察现象)的GPIO,设置如下图所示。

然后设置PWM引脚,这里要先定义PWM引脚再去定义定时器,因为F4本身的TIM1 PWM输出不是板子设计的接口,因此需要重映射来实现这个功能,我们按照前面的外设设置,将对应的引脚设置为对应的模式。

3.3.4 SYS与RCC设置

这里没什么好说的,我看一般都这样设置。

3.3.5 定时器Timers设置

首先是用于生成PWM波的高级定时器TIM1,我们主要是要用TIM1生成带有死区时间的三路互补PWM波。死区时间是指电机驱动板的开关管就如我们开关灯一样会有一个时间差,我们需要在这个时间内不能同时导通同一路开关管,从而导致过流,因此在了解硬件的电气性能后设置合适的死区时间,高级定时器会自动生成带有死区时间的互补PWM波。设置如下图所示

接下来设置频率为15K的PWM波。因此满占空比计数是5600;

设置死区时间100ns左右吧。死区时间的计算麻烦自己参考其他博客。

接下来是三路PWM设置,三路都是一样的参数,我就不赘述了。

然后是定时执行中断任务的TIM6的设置,我们希望1ms执行一次中断任务。这里中断优先级没有强调。在此建议后面的闭环功能如果没有成熟代码,很多中断不要设置,我们再次只想完成SVPWM算法的开发与电机开环控制,我当时把所有ADC什么的都设置了会导致莫名其妙的中断卡顿问题。

3.3.6最后是串口USART1的设置

这没什么好说的,使用它的原因是正点原子电机开发板的串口接口就是USART1。

3.3.7代码生成

如下设置能够将对应的初始化生成在对应的.c/.h文件中,不勾选Generated第一个选项就会将所有CubeMX生成的代码放在main函数当中很头疼。如果你后来发现这个问题,想要使用软件在源代码基础上进行此功能修改,请关闭keil在点击生成,否则会卡死!

3.3.8 PWM波与硬件测试

此步骤是为了测试PWM死区功能、互补输出功能、以及频率设置等功能是否设置成功,以及硬件链接是否正常。此步要求使用示波器。此步骤也是参考博客[3]进行测试,在这里我就直接引用其代码了。

参考下面的代码我们会发现在设置好CubeMX生成代码后,还需手动开启三路互补PWM波,然后手动设置占空比。将其下面的代码添加到main函数里就可以看到相关占空比以及死区时间的PWM波,这很重要。一定要严格对齐频率和死区时间。对了,对于正点原子这套开发套件,要记得要将电机驱动板使能脚:SHUTDOWN-PF10拉高,否则电机驱动板不会输出电压。

测试的死区时间大于100ns就好,关于如何测试可以咨询正点原子客服。

以下是我自己写的电机初始化的函数。

void motor_init(void)
{
	HAL_TIM_PWM_Start(&htim1,TIM_CHANNEL_1);
	HAL_TIM_PWM_Start(&htim1,TIM_CHANNEL_2);
	HAL_TIM_PWM_Start(&htim1,TIM_CHANNEL_3);
	HAL_TIMEx_PWMN_Start(&htim1,TIM_CHANNEL_1);
	HAL_TIMEx_PWMN_Start(&htim1,TIM_CHANNEL_2);
	HAL_TIMEx_PWMN_Start(&htim1,TIM_CHANNEL_3);
	__HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1,0);
	__HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_2,0);
	__HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_3,0);
	HAL_GPIO_WritePin(SHUTDOWN_GPIO_Port, SHUTDOWN_Pin, GPIO_PIN_SET);
}

3.3.9 SVPWM相关代码撰写

这部分强烈建议参考[4]的视频来一步一步做,他的视频讲的更加清晰,而且是一步一步带着做,我这里内容和他没区别,并且up本身是原理和代码结合会比我更加清晰和细节。我这里可能会强调一下软件结构和我自己踩得坑。

首先整个项目由以下文件组成。其中FOC.C和SVPWM.C及其对应的.h文件是我们自己制作。

3.3.9.1 FOC.C

这里面主要包含四个函数分别是限幅函数(用不到)、归一化函数、反park变换函数、电机初始化函数。

//############################################################
// FILE:  Foc.h
//############################################################
#ifndef __FOC_H__
#define __FOC_H__

#ifdef __cplusplus
extern "C"{
#endif
#include <math.h>
#define PI 3.1415926535
//限幅函数
float my_limit(float *limit,float limit_max,float limit_min);
//归一化函数
float normalizeAngle(float angle);
	//Park逆变换
void back_park(float Uq, float Ud,float eAngle);
void motor_init(void);
#ifdef __cplusplus
}
#endif
#endif /*__FOC_H__*/

反Park变化主要是依据反Park变换的公式,其输入时q-d坐标系下的两轴电压值,还有一个电角度。其中q轴电压值主要是为了提供磁链(使得转子发生旋转的力),d轴是什么磁场力一般为0,电角度则是转子旋转的角度。开环控制中q轴电压给定,d轴电压期望为0,使用定时器让电角度在[0,2PI]范围内自增。然后将生成的ualpah,ubeta输入给svpwm函数得到三相电压的PWM占空比输出。电机初始化就是初始化pwm输出和电机控制使能引脚拉高

//############################################################
// FILE:  Foc.c
//############################################################
#include "foc.h"
#include "math.h"
#include "Svpwm.h"
#include "stm32f4xx_hal.h"
#include "tim.h"
#include "main.h"
float Ualpha;
float Ubeta;
//限幅函数
float my_limit(float *limit,float limit_max,float limit_min)
{
	if(*limit > limit_max)
	{
		*limit = limit_max;
	}
	if(*limit < limit_min)
	{
		*limit = limit_min;
	}
	return *limit;

}
	
//归一化函数,为了将角度限制在[0,2PI]
float normalizeAngle(float angle)
{
	float a;
	a=fmod(angle,2*PI);
	return a>=0?a:(a+2*PI);
	
}

//Park逆变换
void back_park(float Uq, float Ud,float eAngle)
{
	normalizeAngle(eAngle);
	
	Ualpha = Ud*cos(eAngle)-Uq*sin(eAngle);
	Ubeta  = Ud*sin(eAngle)+Uq*cos(eAngle);
	
	svpwm(Ualpha,Ubeta);
	
	__HAL_TIM_SetCompare(&htim1, TIM_CHANNEL_1, Tc*5600);
	__HAL_TIM_SetCompare(&htim1, TIM_CHANNEL_2, Tb*5600);
	__HAL_TIM_SetCompare(&htim1, TIM_CHANNEL_3, Ta*5600);
	
	
}
void motor_init(void)
{
	HAL_TIM_PWM_Start(&htim1,TIM_CHANNEL_1);
	HAL_TIM_PWM_Start(&htim1,TIM_CHANNEL_2);
	HAL_TIM_PWM_Start(&htim1,TIM_CHANNEL_3);
	HAL_TIMEx_PWMN_Start(&htim1,TIM_CHANNEL_1);
	HAL_TIMEx_PWMN_Start(&htim1,TIM_CHANNEL_2);
	HAL_TIMEx_PWMN_Start(&htim1,TIM_CHANNEL_3);
	__HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1,0);
	__HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_2,0);
	__HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_3,0);
	HAL_GPIO_WritePin(SHUTDOWN_GPIO_Port, SHUTDOWN_Pin, GPIO_PIN_SET);

}
3.3.9.2 SVPWM.c

.h文件主要是先宏定义一些浮点数_sqrt3_2,_1_2都是对应svpwm三相电压计算里面的一些参数,提高运算速度。Ts则是svpwm公式里面的总时间。

//############################################################
// FILE:  Svpwm.h
//############################################################

#ifndef __SVPWM_H__
#define __SVPWM_H__

#ifdef __cplusplus
extern "C"{
#endif

#define _sqrt3_2        0.8660254037844f
#define _1_2			0.5f
#define Ts				1

void svpwm(float Ualpha,float Ubeta);

#ifdef __cplusplus
}
#endif
#endif /*__SVPWM_H__*/

.c文件主要是按照svpwm计算原理设计svpwm调制函数,其输入时期望Ualpha,Ubeta,输出则是三相PWM的占空比Ta,Tb,Tc.。首先按照转换公式计算u1,u2,u3。然后根据u1,u2,u3关系计算扇区号,对应扇区判断也根据原理可以参考up视频。还有一个限制幅度的判断 if(sum >Ts),当时间计算超出Ts时要进行一个缩放。

扇区判断其实就是来自上面这个算法,uint8_t sector = (u3 >0)+((u2>0) << 1)+((u1>0) << 2);这里的按位移动其实就是里面的*2,*4操作。

//############################################################
// FILE:  Svpwm.c
//############################################################
#include "Svpwm.h"
#include "stdio.h"
#include "tim.h"
#include "main.h"
float Ta;
float Tb;
float Tc;
void svpwm(float Ualpha,float Ubeta)
{
	//1.u1,u2,u3
	float u1 = Ubeta;
	float u2 = -_sqrt3_2*Ualpha - Ubeta*_1_2;
	float u3 = _sqrt3_2*Ualpha  - Ubeta*_1_2;
	
	//2.获取扇区号
	uint8_t sector = (u3 >0)+((u2>0) << 1)+((u1>0) << 2);
	
	//3.总结矢量作用时间表
	if(sector == 5) //扇区1
	{
		float T4 = u3;
		float T6 = u1;
		
		float sum = T4+T6;
		if(sum >Ts)
		{
			float k = Ts/sum;
			
			T4=k*T4;
			T6=k*T6;
		}
		float T0 = (Ts - T4 - T6)/2;
		
		Ta = T4+T6+T0;
		Tb = T6+T0;
		Tc = T0;
	}
	else if(sector == 4) //扇区2
	{
		float T2 = -u3;
		float T6 = -u2;
		
		float sum = T2+T6;
		if(sum >Ts)
		{
			float k = Ts/sum;
			
			T2=k*T2;
			T6=k*T6;
		}
		float T0 = (Ts - T2 - T6)/2;
		
		Ta = T6+T0;
		Tb = T2+T6+T0;
		Tc = T0;
	}
	else if(sector == 6) //扇区3
	{
		float T2 = u1;
		float T3 = u2;
		
		float sum = T2+T3;
		if(sum >Ts)
		{
			float k = Ts/sum;
			
			T2=k*T2;
			T3=k*T3;
		}
		float T0 = (Ts - T2 - T3)/2;
		
		Ta = T0;
		Tb = T2+T3+T0;
		Tc = T3+T0;
	}
	else if(sector == 2) //扇区4
	{
		float T1 = -u1;
		float T3 = -u3;
		
		float sum = T1+T3;
		if(sum >Ts)
		{
			float k = Ts/sum;
			
			T1=k*T1;
			T3=k*T3;
		}
		float T0 = (Ts - T1 - T3)/2;
		
		Ta = T0;
		Tb = T3+T0;
		Tc = T1+T3+T0;
	}
	else if(sector == 3) //扇区5
	{
		float T1 = u2;
		float T5 = u3;
		
		float sum = T1+T5;
		if(sum >Ts)
		{
			float k = Ts/sum;
			
			T1=k*T1;
			T5=k*T5;
		}
		float T0 = (Ts - T1 - T5)/2;
		
		Ta = T5+T0;
		Tb = T0;
		Tc = T1+T5+T0;
	}
	else if(sector == 1) //扇区6
	{
		float T4 = -u2;
		float T5 = -u1;
		
		float sum = T4+T5;
		if(sum >Ts)
		{
			float k = Ts/sum;
			
			T4=k*T4;
			T5=k*T5;
		}
		float T0 = (Ts - T4 - T5)/2;
		
		Ta = T4+T5+T0;
		Tb = T0;
		Tc = T5+T0;
	}
}



3.3.10 其他文件程序修改

想要完成开环控制,需要使用中断来控制电角度进行自增,然后使用反Park变换和Svpwm调制生成PWM波。同时还需要修改串口相关功能是的print函数重定向到串口1,这样就可以实验用VOFA+查看是否产生马鞍波来确定代码写的对不对。

3.3.10.1 tim.c定时器中断六任务编写

这就是hal库中断函数的标准写法,这函数的名字在hal库里也是弱定义的。记得在文件前面进行include和定义电角度。

/* USER CODE BEGIN 0 */
#include "foc.h"
float angle;    //开环电角度
/* USER CODE END 0 */


/* USER CODE BEGIN 1 */
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
	if(htim->Instance == TIM6)
	{
		angle += 0.1;
		back_park(0.3, 0, angle);
	}
}

/* USER CODE END 1 */

3.3.10.2main.c printf函数重定向

在stm32里也有像C语言中的printf函数,这样就免去我们自己写串口传输数据的代码了。但是一般都要进行一次重定向,以使得printf在我们想要的端口输出。这个CSDN也有很多教程可自行查询。在主函数里也只是加了定时器启动的语句,还有固定格式的printf函数。

//main.c

/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include "stdio.h"
#include "svpwm.h"
#include "foc.h"
/* USER CODE END Includes */

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_TIM1_Init();
  MX_TIM6_Init();
  MX_USART1_UART_Init();
  /* USER CODE BEGIN 2 */
	HAL_TIM_Base_Start_IT(&htim6);
	motor_init();
//	__HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1,2500);
  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
		printf("%f,%f,%f\r\n", Ta, Tb, Tc);
  }
  /* USER CODE END 3 */
}

/* USER CODE BEGIN 4 *自定义的函数/
int fputc(int ch, FILE *f)
{
	HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xFFFF);

	return ch;
}
/* USER CODE END 4 */

.h文件中进行了全局变量的定义

/* USER CODE BEGIN Header */
/**
  ******************************************************************************
  * @file           : main.h
  * @brief          : Header for main.c file.
  *                   This file contains the common defines of the application.
  ******************************************************************************
  * @attention
  *
  * <h2><center>&copy; Copyright (c) 2024 STMicroelectronics.
  * All rights reserved.</center></h2>
  *
  * This software component is licensed by ST under BSD 3-Clause license,
  * the "License"; You may not use this file except in compliance with the
  * License. You may obtain a copy of the License at:
  *                        opensource.org/licenses/BSD-3-Clause
  *
  ******************************************************************************
  */
/* USER CODE END Header */

/* Define to prevent recursive inclusion -------------------------------------*/
#ifndef __MAIN_H
#define __MAIN_H

#ifdef __cplusplus
extern "C" {
#endif

/* Includes ------------------------------------------------------------------*/
#include "stm32f4xx_hal.h"

/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */

/* USER CODE END Includes */

/* Exported types ------------------------------------------------------------*/
/* USER CODE BEGIN ET */
extern float Ta;
extern float Tb;
extern float Tc;
/* USER CODE END ET */

/* Exported constants --------------------------------------------------------*/
/* USER CODE BEGIN EC */

/* USER CODE END EC */

/* Exported macro ------------------------------------------------------------*/
/* USER CODE BEGIN EM */

/* USER CODE END EM */

/* Exported functions prototypes ---------------------------------------------*/
void Error_Handler(void);

/* USER CODE BEGIN EFP */

/* USER CODE END EFP */

/* Private defines -----------------------------------------------------------*/
#define SHUTDOWN_Pin GPIO_PIN_10
#define SHUTDOWN_GPIO_Port GPIOF
#define LED0_Pin GPIO_PIN_0
#define LED0_GPIO_Port GPIOE
#define LED1_Pin GPIO_PIN_1
#define LED1_GPIO_Port GPIOE
/* USER CODE BEGIN Private defines */

/* USER CODE END Private defines */

#ifdef __cplusplus
}
#endif

#endif /* __MAIN_H */

/************************ (C) COPYRIGHT STMicroelectronics *****END OF FILE****/

当使用VOFA+看到以下波形时就说明几个自定义函数写的没问题,当波形不对时,请检查自己写的代码。VOFA+的用法也请自己学习,注意下面绿色红色紫色点,一般调整他们会影响波形。

3.3.11 调试技巧

首先使用CubeMX生成代码后,先根据[3]测试PWM功能。如果PWM出问题要去查CubeMX初始化会不会出问题。

然后编辑好上面的代码,先测试是否生成马鞍波,这个功能很重要,我认为这这个功能可以帮助后续学习的调试。这里出问题有可能是1.VOFA+软件调整没好,如果右侧有数值,那就是示波器模块没调整好,大概率是下面的时间轴有问题。2.相关代码出错要好好查,还有printf函数重定向失败,可以先试着输出hello world!

最后上电,要调试好设备,用电表检测一些关键参数防止电路板燃烧!!!!
项目开源:https://github.com/TOMATOXX/STM32F4-HAL-SVPWM-FOC-OPENLOAD


引用
[1]《现代永磁同步电机控制原理及matlab仿真》袁雷
[2]《永磁同步电机矢量控制详细搭建过程》烟雨洲不冷言

[3]《【STM32-HAL库】一步步搭建出FOC矢量控制(附C代码)》 电气超^

[4] 《与FOC从零开始的缘分——SVPWM与开环强拖》 雨梦阳明

;