吊哥FOC学习总结
本文基于硬石科技的STM32F407IGT6的主控板移植的,吊哥的主控是STM32F407VET6。这里需要大家注意!
1 开环FOC驱动
开环所需的部件是:
1、TIM定时器,产生PWM信号,驱动L6234芯片,这里使用定时器1,即M1_PWM1、M1_PWM2、M1_PWM3的引脚分别为:PE9、PE11、PE13。
2、L6234需要一个使能引脚,我们这里选择PE8作为M1_ENABLE。
3、编码器部分,用的是绝对模式接口的编码器,SPI通讯模式,因此,我们使用SPI1,即PA5、PA6、PA7分别作为:M1_SPI_SCK、M1_SPI_MISO、M1_SPI_MOSI引脚。
4、SPI通讯模式需要一个片选引脚,因此我们选择PA4引脚作为M1_SPI_CS。
? 有了SPI通讯可以获取绝对式编码器的初始角度值,方便校验,使其为零度角位置。剩下的就是需要产生一个PWM信号,去驱动电机芯片L6234运行即可做成开环FOC系统。
? 为了方便理解吊哥的FOC代码风格,首先吊哥是使用一个SYS_LED引入的,方便我们了解他的代码风格。接下来我也以先点亮SYS_LED为例子,来讲一下吊哥FOC的代码理解,方便大家看懂代码。
1.1 点亮SYS_LED
? 这里其实灯的引脚每个板子都不一样,我用的硬石科技的板子,他有3个LED灯(LED1:PH9、LED2:PE5、LED3:PE6),我这里就用LED1来移植吧,即定义PH9为SYS_LED,大家可根据自己的开发板进行移植~
? 具体配置引脚如下所示:
? 关于配置部分,这里介绍一下各个部分如何配置的:
1.1.1 定时器3路通道配置
关于定时器配置,主要是用到了定时器1的3路PWM信号,即PE9、PE11、PE13引脚,其模式配置如下图所示:
1.1.2 L6234使能引脚配置
? L6234的三路PWM信号的使能端口全部都连接在一个引脚上:即PE8引脚,将其配置成GPIO_Output模式,因为是高电平有效,因此在初始化时,我们设置成输出电平为低电平,普通推挽输出,上拉,输出速度因为是长期的变化,因此我们设置成高,其具体配置如下图所示:
1.1.3 SPI配置
? SPI通讯是用于跟编码器(TLE5012B E1000)进行通讯的,使用的是绝对式编码器接口,因此只需要上电校验一次即可。这里配置的引脚就是SPI1引脚,即PA5、PA6、PA7引脚,切由于SPI通讯需要一个片选引脚,我们这里选择了PA4作为片选引脚。SPI1的配置如下图所示:
1.1.4 SPI片选引脚配置
? 前面说了SPI通讯需要片选引脚拉低才行,因此我们设置的片选引脚是连接到PA4引脚的,PA4配置成:GPIO_Output模式即可,
最后值得注意的是:我们的定时器是设置了更新中断的,这是方便后面开环的时候,在定时器更新中断中调用FOC控制函数。
1.1.5 定时器更新中断配置
也就是设置定时器1的更新中断为最高等级即可,因为是唯一一个中断,就设置成最高等级默认即可。配置图如下所示:
最后配置一下时钟树就可以了,如下图所示:
1.1.6 时钟树配置
上面就完成了所有的开环必备的配置了,接下来我们就可以开启我们的开环FOC之旅了~
1.2 接口函数导入
? 我们在用STM32CubeMX生成的文件夹中,新建User文件夹,并在User文件夹中,导入相关函数,如下图所示:
? 然后我们打开我们的MDK工程,把接口函数加入到我们的KEIL文件中,如下图所示:
上面其实我们就是导入了能点亮系统LED所需的文件,到时候后期开环的时候还需要加入一些函数,我们讲解开环的时候再介绍。
? 加下来我们分析一下每一个函数接口的意义。
1.2.1 APP函数
APP:承接main.c文件和我们写的函数相连接的功能,也就是说在main.c中我们只需要在初始化中调用APPMain_Init()函数即可,在while(1)中调用APPMain_Loop()函数即可,我们改变函数仅在APP函数中进行修改,main.c中我们不修改,保持默认用STM32CubeMX产生的形势,如下图所示:
然后在APP函数中我们在调用我们的接口函数,所有APP函数就类似于一个中介,他起到连接main.c和我们写的接口函数的作用。
APP函数中只有二个函数:
void APPMain_Init(void)
{
MCUDriverMain_Init();
PerDriverMain_Init();
FunctionMain_Init();
}
void APPMain_Loop(void)
{
MCUDriverMain_Loop();
PerDriverMain_Loop();
FunctionMain_Loop();
}
一个是初始化的函数,一个是循环调用函数。与main.c中的init和while(1)功能一致!
1.2.2 Framework函数
? 总体而言,Framework中的函数,是一个完全脱离硬件,写的一个实现函数功能的函数,也就是说他不依赖任何硬件,如果是调用它,无论什么硬件,都可直接调用。
? 依照LEDControl函数为例,其代码为:
#include "LEDControl.h"
volatile uint32_t gLED_TimeCNT;
void SetLEDON(PLEDControl_Struct gLED)
{
gLED->state = LEDState_ON;
gLED->SetLEDLeave(gLED->onLeave);
}
void SetLEDOFF(PLEDControl_Struct gLED)
{
gLED->state = LEDState_OFF;
gLED->SetLEDLeave(!(gLED->onLeave));
}
void LEDToggle(PLEDControl_Struct gLED)
{
gLED->onoff = !gLED->onoff;
gLED->SetLEDLeave(gLED->onoff);
}
void LEDRunCycle(PLEDControl_Struct gLED)
{
if(LED_TIMEOUT(gLED->cycle * 1000,gLED->startTime)) {
gLED->startTime = LED_GETTIME();
if(gLED->state == LEDState_Toggle) {
LEDToggle(gLED);
}
}
}
void SetLEDFlashing(PLEDControl_Struct gLED,float cycle)
{
gLED->state = LEDState_Toggle;
gLED->cycle = cycle;
}
void SetLedStatus(PLEDControl_Struct gLED,uint8_t status,float cycle)
{
switch(status) {
case LEDState_OFF:
SetLEDOFF(gLED);
break;
case LEDState_ON:
SetLEDON(gLED);
break;
case LEDState_Toggle:
SetLEDFlashing(gLED,cycle);
break;
}
}
#ifndef LEDControl_h
#define LEDControl_h
#include "stdint.h"
enum {
LEDState_OFF = 0,
LEDState_ON,
LEDState_Toggle,
};
struct SLEDControl_Struct {
uint8_t state;
uint8_t onoff;
float cycle;
uint8_t onLeave;
uint32_t startTime;
void(*SetLEDLeave)(uint8_t leave);
};
typedef struct SLEDControl_Struct LEDControl_Struct;
typedef LEDControl_Struct* PLEDControl_Struct;
#define LED_EXPORT(x,xOnLeave,xSetLEDLeave) \
LEDControl_Struct x = { \
.state = LEDState_OFF, \
.onoff = 0, \
.cycle = 0.0, \
.onLeave = xOnLeave, \
.startTime = 0, \
.SetLEDLeave = xSetLEDLeave, \
};
extern volatile uint32_t gLED_TimeCNT;
#define LED_TIMEBASE(ms) \
gLED_TimeCNT+= ms
#define LED_GETTIME(void) \
gLED_TimeCNT
#define LED_TIMEOUT(timeOut,startTime) ((gLED_TimeCNT - startTime) >= timeOut ? 1 : 0)
void SetLedStatus(PLEDControl_Struct gLED,uint8_t status,float cycle);
void LEDRunCycle(PLEDControl_Struct gLED);
#endif
我们会发现其实这个函数中,有6个函数,分别作用为:
#include "LEDControl.h"
volatile uint32_t gLED_TimeCNT;
void SetLEDON(PLEDControl_Struct gLED); ->设置LED点亮
void SetLEDOFF(PLEDControl_Struct gLED); ->设置LED熄灭
void LEDToggle(PLEDControl_Struct gLED); ->设置LED翻转
void LEDRunCycle(PLEDControl_Struct gLED); ->设置LED周期性的执行函数
void SetLEDFlashing(PLEDControl_Struct gLED,float cycle); ->设置LED闪烁
void SetLedStatus(PLEDControl_Struct gLED,uint8_t status,float cycle); ->设置LED状态
在LEDControl.c函数中,其实我们正在需要调用的函数就只有两个函数:
void SetLedStatus(PLEDControl_Struct gLED,uint8_t status,float cycle); ->设置LED状态
void LEDRunCycle(PLEDControl_Struct gLED); ->设置LED周期性的执行函数
因为在这两个函数中已经包括了其他三个函数,并且实现了一切功能:比如点亮LED、熄灭LED、周期性闪烁LED灯;
有人会问,既然脱离了硬件层,那我们如何通过我们的硬件进行调用呢?
这就是吊哥牛逼的地方,他使用了一个函数叫做LED_EXPORT(x,xOnLeave,xSetLEDLeave),他将这个脱离硬件层的函数中所有未知量作为LED_EXPORT的形参,我们调用的时候,赋我们的硬件实参,就可以间接性的给这个Framework中的函数赋值!!!不得不佩服这个思路!!!
1.2.3 MCUDriver函数
? 这个函数主要是调用Hal库中的函数,实现我们所需要的功能,比如说点亮LED灯,我们就使用:
void SetLedLeave(uint8_t leave)
{
HAL_GPIO_WritePin(SYS_LED_GPIO_Port, SYS_LED_Pin, leave);
}
这个函数主要是硬件相关的调用。
好了,有了脱离硬件层的代码Framework中的函数,又有了硬件层的代码MCUDriver,那怎么结合在一起?这个时候就出来了一个函数叫:PeripheralsDriver,这个函数的主要目的就是:Framework中的函数+MCUDriver中的函数 == PeripheralsDriver中的函数!!!
1.2.4 PeripheralsDriver函数
首先比如是点亮LED灯,他就弄了一个文件专门结合Framework中的代码和MCUDriver中的代码:LEDConfig.c文件,其内容为:
#include "LEDConfig.h"
#include "LEDControl.h"
#include "LEDGPIO.h"
LED_EXPORT(gSysLed,1,SetLedLeave);
void LEDConfigSetSysLedStatus(uint8_t status, float cycle)
{
SetLedStatus(&gSysLed,status,cycle);
}
void LEDConfig_Init(void)
{
LEDConfigSetSysLedStatus(LEDConfig_Toggle,1);
}
void LEDConfig_Loop(void)
{
LEDRunCycle(&gSysLed);
}
其中初始化中的函数LEDConfigSetSysLedStatus(LEDConfig_Toggle,1)中的1是不是很熟悉,没错,他就是Framework中的cycle值,这里给的是1ms,如果好奇的同学可以试试改变这个1会怎么样?
大家可以试试将上面的代码改成下面的:
#include "LEDConfig.h"
#include "LEDControl.h"
#include "LEDGPIO.h"
LED_EXPORT(gSysLed,1,SetLedLeave);
void LEDConfigSetSysLedStatus(uint8_t status, float cycle)
{
SetLedStatus(&gSysLed,status,cycle);
}
void LEDConfig_Init(void)
{
LEDConfigSetSysLedStatus(LEDConfig_ON,1);
}
void LEDConfig_Loop(void)
{
}
#include "LEDConfig.h"
#include "LEDControl.h"
#include "LEDGPIO.h"
LED_EXPORT(gSysLed,1,SetLedLeave);
void LEDConfigSetSysLedStatus(uint8_t status, float cycle)
{
SetLedStatus(&gSysLed,status,cycle);
}
void LEDConfig_Init(void)
{
LEDConfigSetSysLedStatus(LEDConfig_OFF,1);
}
void LEDConfig_Loop(void)
{
}
#include "LEDConfig.h"
#include "LEDControl.h"
#include "LEDGPIO.h"
LED_EXPORT(gSysLed,1,SetLedLeave);
void LEDConfigSetSysLedStatus(uint8_t status, float cycle)
{
SetLedStatus(&gSysLed,status,cycle);
}
void LEDConfig_Init(void)
{
LEDConfigSetSysLedStatus(LEDConfig_Toggle,2);
}
void LEDConfig_Loop(void)
{
LEDRunCycle(&gSysLed);
}
试试上面的代码,你看看这三种情况的效果,针对你LED的情况,你也许就能领悟到这个调用的风格是多牛逼!!!在这里再说一句,吊哥牛逼!
在最后补充一句,当调用LED闪烁的时候,别忘记把Framework中LEDControl.h中的#define LED_TIMEBASE(ms) gLED_TimeCNT+= ms这个函数放在系统滴答定时器的中断服务函数中,并赋值ms=1,表示没1ms,这个gLED_TimeCNT的值+1,这里主要是调用LEDContrl.c中的
void LEDRunCycle(PLEDControl_Struct gLED) { if(LED_TIMEOUT(gLED->cycle * 1000,gLED->startTime)) { gLED->startTime = LED_GETTIME(); if(gLED->state == LEDState_Toggle) { LEDToggle(gLED); } } }
这个函数中的技数时间,LED_TIMEOUT(gLED->cycle * 1000,gLED->startTime)这个函数乘以1000,就是表明1ms时间gLED_TimeCNT加1,要+1000次1,那1ms+1,+1000个1是不是1s呢,所有就实现了1s闪烁LED灯的效果~~~大家慢慢领悟吧
下面把滴答定时器中断服务函数中的代码放出来,方便大家借鉴。
烧写完代码,你的LED灯就会闪烁了~~~第一步就成功了!
今天就到这里,希望对大家有帮助,我也是小菜鸡,初学者,之所以发出来,是希望能帮助到跟我一样的同学,少走弯路~~~
最后,谢谢吊哥的无私开源代码~~~谢谢大佬!!!
DRunCycle(PLEDControl_Struct gLED) { if(LED_TIMEOUT(gLED->cycle * 1000,gLED->startTime)) { gLED->startTime = LED_GETTIME(); if(gLED->state == LEDState_Toggle) { LEDToggle(gLED); } } }
这个函数中的技数时间,LED_TIMEOUT(gLED->cycle * 1000,gLED->startTime)这个函数乘以1000,就是表明1ms时间gLED_TimeCNT加1,要+1000次1,那1ms+1,+1000个1是不是1s呢,所有就实现了1s闪烁LED灯的效果~~~大家慢慢领悟吧
下面把滴答定时器中断服务函数中的代码放出来,方便大家借鉴。
烧写完代码,你的LED灯就会闪烁了~~~第一步就成功了!
今天就到这里,希望对大家有帮助,我也是小菜鸡,初学者,之所以发出来,是希望能帮助到跟我一样的同学,少走弯路~~~
最后,谢谢吊哥的无私开源代码~~~谢谢大佬!!!
|