【从零开始实现stm32无刷电机FOC】【实践】【7.2/7 完整代码编写】

news2025/1/8 19:06:41

目录

  • stm32cubemx配置
    • 芯片选择
    • 工程配置
    • stm32基础配置
    • SPI的配置
    • 定时器的配置
    • ADC的配置
    • 中断优先级的配置
    • 生成工程
  • 工程代码编写
    • FOC代码结构搭建
    • 电机编码器角度读取
    • PWM产生
    • FOC开环代码编写
    • 确定电机正负旋转方向
    • 电机旋转速度计算
    • 多圈逻辑角度
    • 电流采样
    • 极对数
    • 转子角度确定
  • 闭环控制
    • 控制函数接口定义
    • CMSIS-DSP提供的PID控制器
    • 位置控制
    • 速度控制
    • 力矩(电流)控制
    • 位置-速度-力矩控制
    • PID系数调节
  • 注意点

本节使用stm32cubemx配置外设,生成keil工程,代码适配本文的硬件电路板(可参考上节的硬件设计内容)。

  • 点击查看本文开源的完整FOC工程https://gitee.com/best_pureer/stm32_foc。

stm32cubemx配置

芯片选择

选择stm32f103c8t6后,点击Start Project。
在这里插入图片描述

工程配置

切换到Projeck Manager页面,设定好项目名称、项目路径,选择生成keil工程。
在这里插入图片描述

stm32基础配置

  • 配置时钟

这里选择晶振:
在这里插入图片描述
切换到Clock Configuration页面,首先选择PLLCLK,再HCLK输入72,再点击确认会自动生成72MHz的主频。
在这里插入图片描述

  • SWD接口配置

用于烧录和调试程序
在这里插入图片描述

  • 串口配置

用于打印一些调试信息,选择串口2,因为串口1会被定时器1的pwm通道占用
在这里插入图片描述

  • LED灯配置

在这里插入图片描述

  • CMSIS-DSP数学库配置

第一步:
在这里插入图片描述
第二步:
在这里插入图片描述

SPI的配置

用于读取MT6701,这里的SPI波特率不要设置太高,因为stm32f103c8t6的计算能力有限,角度读的太快算不过来。
从MT6701的数据手册中可以看到,应该在CLK的每2个脉冲时读取DO的数据,而且CLK空闲电平是低电平,因此设置CPOL为Low,CPHA为2个边沿。
由于D13先发送,因此是先高位传输,即MSB大端传输方式。
在这里插入图片描述
这里将SPI的模式设置为全双工Full-Duplex Master,而不设置为只读取模式Receive Only Master。虽然单片机的SPI只需要读取MT6701的数据,无需SPI写入数据,但是实测设置为只读取模式时,貌似是每读完1个字节进一次读取完成中断,而我们想要的是每读完3个字节进一次读取完成中断,全双工模式能做到这个效果。
注意不要使用硬件片选NSS(对应芯片CSN读取使能引脚),因为硬件片选NSS是单片机disable对应SPI外设后才会拉低。在这里插入图片描述
这里选择手动操作SPI的片选引脚,设置片选信号所在引脚为GPIO输出模式,操作逻辑是:每次在SPI读取MT6701前拉低该引脚,在刚进入读取完成中断时拉高该引脚。
在这里插入图片描述
后续使用DMA读取SPI数据,全双工模式下,接收和发送DMA均需要打开:
在这里插入图片描述

定时器的配置

  • 用于电机速度计算的定时器

首先勾选时钟来自内部时钟源,再创建一个宏定义,用于速度计算间隔。motor_speed_calc_freq是FOC代码里手动设置的宏,speed_calc_freq这个宏给cubemx用,这样就将cubemx的参数与FOC代码里的宏绑定到了一起,好处就是FOC代码与cubemx独立开,FOC代码用于其他厂商单片机不用改太多的代码。
在这里插入图片描述
首先设置分频为72-1,72MHz的时钟分频下来后每1us定时器计数加1。定时器计数容量为N时,定时器溢出中断频率是1秒/N微妙=1000000/N赫兹,我们想要定时器溢出中断频率等于motor_speed_calc_freq,那么需要1000000/N=motor_speed_calc_freq,因此这里的计数容量设置为N=1000000/motor_speed_calc_freq。
由于cubemx是不认识motor_speed_calc_freq的,因此这里选择No check不检查参数类型。
在这里插入图片描述
开启定时器中断函数,用于计算电机速度:
在这里插入图片描述

  • 用于产生电机PWM的高级定时器

高级定时器的使用非常关键,非常重要,对于外设配置项的说明请查看前文定时器章节。
stm32f103c8t6只有一个高级定时器:TIM1。
首先选择时钟源,然后开启3个PWM正通道,由于本文使用的集成驱动芯片DRV8313自带互补PWM功能,因此不开启PWM负通道。
这里注意不勾选Activate-Break-Input,这个只是帮你配置刹车引脚的GPIO参数,刹车功能依然是有效的。不勾选的原因是经过我的各种尝试,触发刹车后,依然进不去刹车中断函数TIM1 break interrupt,所以选择自己配置刹车引脚GPIO为外部中断。
在这里插入图片描述
手动设置刹车引脚为外部中断,这里不设置也没事,本文代码是因为想要触发刹车后串口打印一下告知刹车被触发了:
在这里插入图片描述
开启外部中断函数:
在这里插入图片描述
回到高级定时器的配置,再次提醒对于外设配置项的说明请查看前文定时器章节。

  • PWM的频率是非常高的,我设置的20KHz,因此不进行预分频。
  • 计数方式选择中心对齐方式3。
  • 计数容量定义了一个cubemx宏,与FOC代码里的宏进行绑定,这样做可以使得FOC代码独立在各种平台上使用,勾选No check不检查数值类型。数值计算过程:定时器计数不分频情况下,每 1 72 ∗ 1000000 \frac{1}{72*1000000} 7210000001秒计数加1,计数容量为N时,计数溢出时间为 N 72000000 \frac{N}{72000000} 72000000N,则计数溢出频率为 72000000 N \frac{72000000}{N} N72000000,而我们设定的计数溢出频率=motor_pwm_freq,即 72000000 N = m o t o r _ p w m _ f r e q \frac{72000000}{N}=motor\_pwm\_freq N72000000=motor_pwm_freq,因此计数容量 N = 72000000 m o t o r _ p w m _ f r e q N=\frac{72000000}{motor\_pwm\_freq} N=motor_pwm_freq72000000
  • 重复计数器设置为4,即每5个定时器溢出产生1个更新事件,更新事件会触发ADC采样,本文FOC代码是放在ADC采样完成中断里计算的,这样FOC代码的计算频率等于20KHz/5=4KHz,这个频率不要太快,因为stm32f103c8t6的算力有限,计算一次FOC代码大约需要120us。
  • 输出触发事件来源选择更新事件,自动触发ADC采样。
  • BRK State刹车功能可以开启,也可以关闭,建议验证FOC算法阶段先关闭,本文配套硬件有刹车引脚LED灯提示。对应DRV8313的Fault引脚信号,刹车电平选择Low低电平。
  • 3个PWM Generation Channel的配置项都相同,默认值刚好是我们想要配置的值。关键项是Mode选择PWM mode 1,因为DRV8313内部是NMOS,所以CH Polarity有效电平是高电平。
    在这里插入图片描述

ADC的配置

ADC的使用非常关键,非常重要,对于外设配置项的说明请查看前文ADC章节。
使用双ADC同步采样两条电机相线上采样电阻放大后的电压差,首先给ADC1和ADC2分配通道,IN0对应PA0引脚,IN0对应PA1引脚,ADC1是主ADC。
在这里插入图片描述
两个ADC都分配好通道后,主ADC的模式下拉框里才会出现同步采样的选项,由于只有注入式采样才能绑定到高级定时器的触发事件,因此选择注入式同步采样。
在这里插入图片描述
接下来配置ADC选项:

  • 同步采样模式下,一定要开启Continuous连续转换模式。
  • 关闭常规采样,打开注入采样。
  • 每个ADC只采集一个通道,因此Number Of Conversions设置为1,ADC1的Rank序列中的Channel通道选择通道0,ADC2的Rank序列中的Channel通道选择通道1,采样时间可以设置大一点,我们代码中限制PWM占空比最高为90%即可留出充足的时间给ADC采样。
  • 外部触发源选择定时器1的触发事件。
    在这里插入图片描述
    在这里插入图片描述
    开启ADC采样完成中断,FOC代码会放在这个中断函数里进行:
    在这里插入图片描述

中断优先级的配置

读取磁编码器SPI的DMA中断优先级设置到最优先,电机角度获取一定要及时准确。
接下来速度计算的定时器中断和运行FOC代码的ADC中断其次,速度计算和FOC代码必须尽量优先。
连到刹车引脚的外部中断优先级放到最低即可,刹车功能在刹车引脚有效时被硬件触发,和中断无关,这个外部中断只是为了串口打印几句消息告知触发刹车了。
在这里插入图片描述

生成工程

至此,stm32cubemx相关配置已经完毕,接下来生成keil工程,这里勾选了Generate peripheral initialization as a pair of '.c/.h' files per peripheral,方便功能模块分离。
在这里插入图片描述

此时keil工程编译是无法通过的,编译器可能没选到verison 6,而且两个宏motor_speed_calc_freqmotor_pwm_freq我们还没写到代码里。
首先选择编译器为version 6,如果没有出现version 6,你需要下载最新keil版本。
在这里插入图片描述
接下来我会使用vscode进行代码编辑,keil用来编译工程和烧录调试代码。
接下来定义motor_speed_calc_freqmotor_pwm_freq

  • 首先在Drivers文件夹下创建motor文件夹,这个文件夹专门用来放置电机FOC驱动代码。
  • 然后motor文件夹下创建conf.h,这个文件专门用来放置FOC代码相关的工程配置,将Drivers文件夹添加到头文件路径中。
  • 最后在main.h文件的用户代码区把conf.h文件include进来即可将工程编译通过。
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

Src/usart.c中定义fputc,方便后续使用printf函数:

//usart.c
/* USER CODE BEGIN 1 */
#include <stdio.h>
int fputc(int c, FILE *stream)
{
  uint8_t ch[] = {(uint8_t)c};
  HAL_UART_Transmit(&huart2, ch, 1, HAL_MAX_DELAY);
  return c;
}
/* USER CODE END 1 */

在Inc文件夹下创建一个全局宏定义文件global_def.h,方便调用:

//global_def.h
#pragma once
#ifndef PI
#define PI 3.14159265358979
#endif
#define deg2rad(a) (PI * (a) / 180)
#define rad2deg(a) (180 * (a) / PI)
#define max(a, b) ((a) > (b) ? (a) : (b))
#define min(a, b) ((a) < (b) ? (a) : (b))

由于本文使用的daplink是低成本版本,为了保证传输稳定性,下载程序的频率选择为1MHz。

工程代码编写

在stm32hal库中,应用逻辑的代码实现变得非常方便,基本规律就是:外设读取+读取完成中断。

FOC代码结构搭建

根据本文硬件情况补充FOC代码里的conf.h

#pragma once

// 电机物理参数:
#define POLE_PAIRS 7 // 极对数

// 电路参数:
#define R_SHUNT 0.02           // 电流采样电阻,欧姆
#define OP_GAIN 50             // 运放放大倍数
#define MAX_CURRENT 2          // 最大q轴电流,安培A
#define ADC_REFERENCE_VOLT 3.3 // 电流采样adc参考电压,伏
#define ADC_BITS 12            // ADC精度,bit

// 单片机配置参数:
#define motor_pwm_freq 20000      // 驱动桥pwm频率,Hz
#define motor_speed_calc_freq 930 // 电机速度计算频率,Hz

// 软件参数:
#define position_cycle 6 * 3.14159265358979 // 电机多圈周期,等于正半周期+负半周期

Drivers/motor文件夹下创建motor_runtime_param.cmotor_runtime_param.h,用于放置电机运行过程中的各种参数。
关于转子角度和电机角度的区别,前文位置控制说明了一部分,后面内容有代码上的讲解。

//motor_runtime_param.c
#include "motor_runtime_param.h"

float motor_i_u;
float motor_i_v;
float motor_i_d;
float motor_i_q;
float motor_speed;
float motor_logic_angle;
float encoder_angle;
float rotor_zero_angle;
//motor_runtime_param.h
#pragma once
#include "conf.h"
#define rotor_phy_angle (motor_logic_angle - rotor_zero_angle) // 转子物理角度
#define rotor_logic_angle rotor_phy_angle *POLE_PAIRS          // 转子多圈角度
extern float motor_i_u;
extern float motor_i_v;
extern float motor_i_d;
extern float motor_i_q;
extern float motor_speed;
extern float motor_logic_angle; // 电机多圈角度
extern float encoder_angle;     // 编码器直接读出的角度
extern float rotor_zero_angle;  // 转子d轴与线圈d轴重合时的编码器角度

Drivers/motor文件夹下创建foc.cfoc.h,准备用于放置FOC代码。
添加好头文件路径:
在这里插入图片描述
添加好C文件:
在这里插入图片描述

电机编码器角度读取

从MT6701数据手册可知,SPI数据一共有3个字节:
在这里插入图片描述采用DMA方式SPI读取3个字节,再取前14位组合成角度值,这里我不使用4bit的Status信息,也不进行crc校验,这不影响角度读取,你可自行运用这些数据提高程序健壮性。
Src/spi.c中的USER CODE区域实现读取成功的回调函数,还要实现读取失败的回调函数,每次进入读取完毕函数后立即关闭SPI片选引脚,并且在即将退出回调函数的时候打开SPI片选引脚并启动下一次读取。

//spi.c
/* USER CODE BEGIN 1 */
#include <stdio.h>
#include "motor/motor_runtime_param.h"
#include "motor/foc.h"	
#include "arm_math.h"
uint8_t mt6701_rx_data[3];
void HAL_SPI_TxRxCpltCallback(SPI_HandleTypeDef *hspi)
{
  if (hspi->Instance == SPI1)
  {
    HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET);
    int angle_raw = (mt6701_rx_data[1] >> 2) | (mt6701_rx_data[0] << 6);
    encoder_angle = 2 * 3.1415926 * angle_raw / ((1 << 14) - 1);
    HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET);

    HAL_SPI_TransmitReceive_DMA(&hspi1, mt6701_rx_data, mt6701_rx_data, 3);
    return;
  }
}

void HAL_SPI_ErrorCallback(SPI_HandleTypeDef *hspi)
{
  printf("HAL_SPI_ErrorCallback:%d\n", hspi->ErrorCode);
  if (hspi->Instance == SPI1)
  {
    HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET);
    HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET);
    HAL_SPI_TransmitReceive_DMA(&hspi1, mt6701_rx_data, mt6701_rx_data, 3);
  }
  return;
}
/* USER CODE END 1 */

在main函数的while(1)之前调用一次DMA方式的SPI读取,即可自动连续获取角度值,然后在while(1)里面打印一下角度验证一下代码,手动旋转一下电机,结果会在 0 0 0 2 π 2\pi 2π弧度值之间变化:

//main.c
//......
/* USER CODE BEGIN Includes */
#include <stdio.h>
#include "motor/motor_runtime_param.h"
#include "motor/foc.h"
#include "global_def.h"
/* USER CODE END Includes */
//......
/* USER CODE BEGIN 2 */
extern uint8_t mt6701_rx_data[3];
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET);
HAL_SPI_TransmitReceive_DMA(&hspi1, mt6701_rx_data, mt6701_rx_data, 3);
HAL_Delay(100);//延时一会,让角度变量被赋值,不然角度会是0
/* USER CODE END 2 */
//......
while(1)
{
//......
	/* USER CODE BEGIN 3 */
    printf("%f\n", encoder_angle);
    HAL_Delay(100);
//......

在此再提醒:读取角度的SPI中断优先级一定要最高,否则片选信号可能没有及时关闭,导致角度读取无法接续。

PWM产生

开启PWM输出:

//main.c
//......
  HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_1);
  HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_2);
  HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_3);
  /* USER CODE END 2 */
//......

自己定义一个函数set_pwm_duty,方便设置PWM占空比。htim1.Instance->ARR是定时器计数容量。
有两个细节:

  • 限制占空比最高为90%,留出一个电流稳定的时段,有利于减少电机抖动以及给后续ADC采样提供稳定电流时段。原因是:占空比接近100%时,会出现mos关闭后瞬间开启的情况,而且此时是电流通过,导致电流非常不稳定;不必要限制最低占空比,因为q轴为0时,由于SVPWM中的000矢量和111矢量两个零力矩矢量平分了一个PWM周期,因此FOC控制下的PWM最低占空比就是50%。
    在这里插入图片描述

  • 操作3个通道时关闭了中断,防止通道不同步。

//main.c
/* USER CODE BEGIN 0 */
void set_pwm_duty(float d_u, float d_v, float d_w)
{
  d_u = min(d_u, 0.9);
  d_v = min(d_v, 0.9);
  d_w = min(d_w, 0.9);
  __disable_irq();
  __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, d_u * htim1.Instance->ARR);
  __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_2, d_v * htim1.Instance->ARR);
  __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_3, d_w * htim1.Instance->ARR);
  __enable_irq();
}
/* USER CODE END 0 */

FOC开环代码编写

FOC开环就是实现一下SVPWM代码,输入目标d轴q轴强度和旋转的目标转子位置,输出电机相线pwm占空比,前文已经实现了SVPWM纯C语言代码,这里将其放到单片机中,也就是要将纯C的数学函数换做CMSIS-DSP里的函数:

//foc.c
#include "foc.h"
#include "arm_math.h"
#include "motor_runtime_param.h"
#include <stdbool.h>
#define rad60 deg2rad(60)
#define SQRT3 1.73205080756887729353
#define deg2rad(a) (PI * (a) / 180)
#define rad2deg(a) (180 * (a) / PI)
#define max(a, b) ((a) > (b) ? (a) : (b))
#define min(a, b) ((a) < (b) ? (a) : (b))

static void svpwm(float phi, float d, float q, float *d_u, float *d_v, float *d_w)
{
    d = min(d, 1);
    d = max(d, -1);
    q = min(q, 1);
    q = max(q, -1);
    const int v[6][3] = {{1, 0, 0}, {1, 1, 0}, {0, 1, 0}, {0, 1, 1}, {0, 0, 1}, {1, 0, 1}};
    const int K_to_sector[] = {4, 6, 5, 5, 3, 1, 2, 2};
    float sin_phi = arm_sin_f32(phi);
    float cos_phi = arm_cos_f32(phi);
    float alpha = 0;
    float beta = 0;
    arm_inv_park_f32(d, q, &alpha, &beta, sin_phi, cos_phi);

    bool A = beta > 0;
    bool B = fabs(beta) > SQRT3 * fabs(alpha);
    bool C = alpha > 0;

    int K = 4 * A + 2 * B + C;
    int sector = K_to_sector[K];

    float t_m = arm_sin_f32(sector * rad60) * alpha - arm_cos_f32(sector * rad60) * beta;
    float t_n = beta * arm_cos_f32(sector * rad60 - rad60) - alpha * arm_sin_f32(sector * rad60 - rad60);
    float t_0 = 1 - t_m - t_n;

    *d_u = t_m * v[sector - 1][0] + t_n * v[(sector) % 6][0] + t_0 / 2;
    *d_v = t_m * v[sector - 1][1] + t_n * v[(sector) % 6][1] + t_0 / 2;
    *d_w = t_m * v[sector - 1][2] + t_n * v[(sector) % 6][2] + t_0 / 2;
}

我将svpwm函数封装一层,命名为foc_forward,传入参数是d轴强度、q轴强度、转子角度,后续闭环计算完毕后也能输入到该函数,得到电机相线pwm占空比。这里创建了一个和set_pwm_duty同名的weak类型函数,这样做是为了提高FOC代码的独立解耦性,编译的时候如果有同名函数,带weak的不会被编译进去。

//foc.c
//......
__attribute__((weak)) void set_pwm_duty(float d_u, float d_v, float d_w)
{
    while (1)
        ;
}
void foc_forward(float d, float q, float rotor_rad)
{
    float d_u = 0;
    float d_v = 0;
    float d_w = 0;
    svpwm(rotor_rad, d, q, &d_u, &d_v, &d_w);
    set_pwm_duty(d_u, d_v, d_w);
}
//......
//foc.h
#pragma once

float cycle_diff(float diff, float cycle);
void foc_forward(float d, float q, float rotor_rad);

测试一下FOC开环控制,设定d轴强度为0、q轴强度为0.5(不要太高,开环发热)、目标转子在0~360度之间循环转动,此时电机应该就转起来了,由于是开环,电机转起来会抖,而且会发热,不要长时间转。

//main.c
//......
while(1)
{
//......
	/* USER CODE BEGIN 3 */
    for (size_t i = 0; i < 360; i += 20)
    {
      HAL_Delay(2);
      foc_forward(0, 0.5, deg2rad(i));
    }
//......

确定电机正负旋转方向

默认情况下,手动逆时针转动电机,MT6701磁编码器的角度是递增的,因此将逆时针旋转设定为角度增加的旋转正方向。有些编码器角度增加的旋转方向是逆时针,这并不唯一。
继续使用上述FOC开环测试代码,传入的q轴是正数,如果电机逆时针转,那么相线顺序是正确的,否则将电机相线取两相对调即可得到正确的相线顺序。
得到正确的相线顺序后,就可以认为UVW三相线分别对应PWM通道123。

电机旋转速度计算

众所周知, 旋转速度 = Δ θ Δ t = Δ θ ∗ 频率 旋转速度=\frac{\Delta \theta}{\Delta t}=\Delta \theta*频率 旋转速度=ΔtΔθ=Δθ频率,速度计算频率我们已经设定了宏定义motor_speed_calc_freq,所以在速度定时器中断里的计算过程是:
( 上次编码器角度 − 这次编码器角度 ) ∗ m o t o r _ s p e e d _ c a l c _ f r e q (上次编码器角度-这次编码器角度)*motor\_speed\_calc\_freq (上次编码器角度这次编码器角度)motor_speed_calc_freq
但是代码不能直接这么写,因为:

  • 编码器角度位于 0 0 0 2 π 2\pi 2π之间,359度到1度实际旋转了2度,但是差值是358度,因此计算差值的时候要把差值转换到 − π -\pi π π \pi π之间。
  • 算出来的速度需要进行滤波才能给外部用。

下面这个函数能将无符号数映射到一个周期内的有符号数。算角度差值的时候经常会用到,将其放在FOC代码中:

//foc.c
//......
float cycle_diff(float diff, float cycle)
{
    if (diff > (cycle / 2))
        diff -= cycle;
    else if (diff < (-cycle / 2))
        diff += cycle;
    return diff;
}
//......
//foc.h
#pragma once
//......
float cycle_diff(float diff, float cycle);
//......

再实现滤波函数,这里使用低通滤波,也可以自行更换为卡尔曼滤波等等。
先创建文件用于放置滤波代码,注意别忘了在keil里加入头文件路径和C文件,这里keil里就不演示了。
在这里插入图片描述
再实现低通滤波函数:

//filter.c
float low_pass_filter(float input, float last_output, float alpha)
{
    return alpha * input + (1.0 - alpha) * last_output;
}

Src/tim.c实现最终的速度计算代码:

//tim.c
#include "global_def.h"
#include "motor/motor_runtime_param.h"
#include "motor/foc.h"
#include "algorithm/filter.h"
#include "arm_math.h"
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
  if (htim->Instance == TIM3)
  {
    static float encoder_angle_last = 0;
    
    /******encoder_angle_last默认值是0,不能用于计算,需要赋初值*********/
    static int once = 1;
    if (once)
    {
      once = !once;
      encoder_angle_last = encoder_angle;
    }
    /***************/
    
    float diff_angle = cycle_diff(encoder_angle - encoder_angle_last, 2 * PI);
    encoder_angle_last = encoder_angle;
    float _motor_speed = diff_angle * motor_speed_calc_freq;
    float filter_alpha_speed = 0.1;//低通滤波参数,如果希望保留更多的细节,可以增加alpha的值
    motor_speed = low_pass_filter(_motor_speed, motor_speed, filter_alpha_speed);
  }
}

在main.c中开启速度计算定时器:

//......
HAL_TIM_Base_Start_IT(&htim3);
/* USER CODE END 2 */
//......

测试一下速度计算是否有效,继续使用FOC开环测试代码,不要转太长时间,开环控制会发热:

//main.c
//......
while(1)
{
//......
	/* USER CODE BEGIN 3 */
    for (size_t i = 0; i < 360; i += 20)
    {
      HAL_Delay(2);
      foc_forward(0, 0.5, deg2rad(i));
      printf("%f\n", motor_speed);
    }
//......

多圈逻辑角度

从MT6701编码器是 0 0 0 2 π 2\pi 2π周期变化的,只有单圈,但我们想要多圈角度时,需要自行将每次的角度差值累计起来,即累计(当前角度-上次角度)。
逻辑角度是手动累计的,因此更新及时性是取决与放在哪里累计的。如果放在SPI读取角度中断里,那么和编码器角度一样的更新及时;如果放在其他中断里,比如速度计算定时器中断、ADC采样完成中断,按照本文设置,则逻辑角度更新速度等于速度计算频率=930Hz,ADC中断=4KHz。更新及时性超过SPI读取角度中断频率是没有意义的。本文选择放在SPI读取角度中断里累计。
这里对逻辑角度进行了周期操作,半周期是position_cycle,比如按照度数表达,如果半周期设置为360度、当前逻辑角度是358度,再转3度后,逻辑角度等于-359度。如果你的逻辑角度是非常非常多圈,并且你自信电机实际旋转需求不会超出浮点数可表达的最大值,可以不需要周期操作。
补充SPI读取角度中断:

//spi.c
//......
void HAL_SPI_TxRxCpltCallback(SPI_HandleTypeDef *hspi)
{
  if (hspi->Instance == SPI1)
  {
    HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET);
    int angle_raw = (mt6701_rx_data[1] >> 2) | (mt6701_rx_data[0] << 6);
    encoder_angle = 2 * 3.1415926 * angle_raw / ((1 << 14) - 1);

    static float encoder_angle_last = 0;
    /****encoder_angle_last默认值是0,不能用于计算,要先赋值一次****/
    static int once = 1;
    if (once)
    {
      once = !once;
      encoder_angle_last = encoder_angle;
    }
    /*************/
    float _encoder_angle = encoder_angle;
    // 角度差值,用于累计多圈逻辑角度
    float diff_angle = cycle_diff(_encoder_angle - encoder_angle_last, 2 * PI);
    encoder_angle_last = _encoder_angle;
    // 实现周期操作,将motor_logic_angle转到周期内
    motor_logic_angle = cycle_diff(motor_logic_angle + diff_angle, position_cycle);

    HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET);
    HAL_SPI_TransmitReceive_DMA(&hspi1, mt6701_rx_data, mt6701_rx_data, 3);
    return;
  }
}
//......

在主函数里打印一下逻辑角度,用手转动电机几圈,看看逻辑角度是否有效:

//main.c
//......
while(1)
{
printf("%f\n", motor_logic_angle);
/* USER CODE END WHILE */
//......

从角度结果可以看到逻辑角度具有多圈周期性:
在这里插入图片描述

电流采样

电机相线上的电流采样电阻两端电压经过放大器放大后,输入单片机ADC口。
本文会将正式的FOC实时计算代码放在ADC采样完成中断里(大部分驱动代码都会这么做,当然放在角度读取完成中断里也是可行的),因此无论是否读取ADC值,这个中断函数必须要开放实现的。
进行ADC采样之前,先校准,然后开始采样。由于ADC采样绑定了高级定时器的输出事件,后续的ADC采样会自动被高级定时器触发。

//main.c
//......
  HAL_ADCEx_Calibration_Start(&hadc1);
  HAL_ADCEx_Calibration_Start(&hadc2);
  HAL_ADCEx_InjectedStart_IT(&hadc1);
  HAL_ADCEx_InjectedStart(&hadc2);
  /* USER CODE END 2 */
//......

在ADC采样完成中断函数里读取ADC值,INA199放大器系列的ADC值会在1.65V上下变动,1.65V代表采样电阻两端电压为0V,大于1.65V代表采样电阻两端电压为正数,小于1.65V代表采样电阻两端电压为负数,相线电流等于 A D C 值 − 1.65 V 放大倍数 ÷ 采样电阻值 \frac{ADC值-1.65V}{放大倍数}\div采样电阻值 放大倍数ADC1.65V÷采样电阻值
这里最好验证一下ADC值的正负以及是否正好对应PWM通道123,先假设PWM通道1对应ADC1,PWM通道2对应ADC2,则假设U相电流等于ADC1计算得到的电流,V相电流等于ADC2计算得到的电流,W相电流根据基尔霍夫电流总和为0的定律等于: − ( U 相电流 + V 相电流 ) -(U相电流+V相电流) (U相电流+V相电流)

//adc.c
//......
#include "motor/motor_runtime_param.h"
#include "motor/foc.h"
#include "algorithm/filter.h"
#include "global_def.h"
#include "arm_math.h"
void HAL_ADCEx_InjectedConvCpltCallback(ADC_HandleTypeDef *hadc)
{
  if (hadc->Instance == ADC1)
  {
    float u_1 = ADC_REFERENCE_VOLT * ((float)HAL_ADCEx_InjectedGetValue(&hadc1, ADC_INJECTED_RANK_1) / ((1 << ADC_BITS) - 1) - 0.5);
    float u_2 = ADC_REFERENCE_VOLT * ((float)HAL_ADCEx_InjectedGetValue(&hadc2, ADC_INJECTED_RANK_1) / ((1 << ADC_BITS) - 1) - 0.5);
    float i_1= u_1 / R_SHUNT / OP_GAIN;
    float i_2 = u_2 / R_SHUNT / OP_GAIN;
	motor_i_u = i_1;
    motor_i_v = i_2;
  }
}

如果3个PWM通道正对应ADC1和ADC2,那么打开PWM通道1,关闭PWM通道2、3的时候,ADC1这条路会得到一个对应的正电流值,如下图所示:

按照这个思想验证一下:

//main.c
//......
  /* USER CODE BEGIN WHILE */
  set_pwm_duty(0.5, 0, 0);
  HAL_Delay(300);
  printf("%f,%f,%f\n", motor_i_u, motor_i_v, -(motor_i_u + motor_i_v));

  set_pwm_duty(0, 0.5, 0);
  HAL_Delay(300);
  printf("%f,%f,%f\n", motor_i_u, motor_i_v, -(motor_i_u + motor_i_v));

  set_pwm_duty(0, 0, 0.5);
  HAL_Delay(300);
  printf("%f,%f,%f\n", motor_i_u, motor_i_v, -(motor_i_u + motor_i_v));
  set_pwm_duty(0, 0, 0);
  while (1)
  {
//......

如果得到类似下图的结果,三个电流正值都差不多,也是分别出现在第1、2、3个,那么说明ADC1和ADC2正对应PWM通道1和2,而且正负方向也是正确的,否则就需要调整motor_i_u = i_1;motor_i_v = i_2;代码,重新排列正确的对应关系。
在这里插入图片描述
再根据3个相电流换算到d轴和q轴电流,即clark变换和park变换,理论部分请参考前文电流控制,然后进行滤波操作。如果你不想使用力矩环,d轴和q轴电流无需计算。ADC中断代码补充为:

void HAL_ADCEx_InjectedConvCpltCallback(ADC_HandleTypeDef *hadc)
{
  if (hadc->Instance == ADC1)
  {
    float u_1 = ADC_REFERENCE_VOLT * ((float)HAL_ADCEx_InjectedGetValue(&hadc1, ADC_INJECTED_RANK_1) / ((1 << ADC_BITS) - 1) - 0.5);
    float u_2 = ADC_REFERENCE_VOLT * ((float)HAL_ADCEx_InjectedGetValue(&hadc2, ADC_INJECTED_RANK_1) / ((1 << ADC_BITS) - 1) - 0.5);
    float i_1 = u_1 / R_SHUNT / OP_GAIN;
    float i_2 = u_2 / R_SHUNT / OP_GAIN;
    motor_i_u = i_1;
    motor_i_v = i_2;

    float i_alpha = 0;
    float i_beta = 0;
    arm_clarke_f32(motor_i_u, motor_i_v, &i_alpha, &i_beta);
    float sin_value = arm_sin_f32(rotor_logic_angle);
    float cos_value = arm_cos_f32(rotor_logic_angle);
    float _motor_i_d = 0;
    float _motor_i_q = 0;
    arm_park_f32(i_alpha, i_beta, &_motor_i_d, &_motor_i_q, sin_value, cos_value);
    float filter_alpha_i_d = 0.1;
    float filter_alpha_i_q = 0.1;
    motor_i_d = low_pass_filter(_motor_i_d, motor_i_d, filter_alpha_i_d);
    motor_i_q = low_pass_filter(_motor_i_q, motor_i_q, filter_alpha_i_q);
  }
}

极对数

前文理论部分一直使用的是简单模型,即转子看成一根直的磁铁,实际电机的转子磁铁排布可能如下图,这是本文使用的电机示意图,一共有14个(7对)磁极,即极对数为7。
在这里插入图片描述
极对数为7的电机完成7次相线换相周期后,电机才会旋转一圈。也就是说用于FOC计算的转子逻辑角度=极对数*转子物理角度。

转子角度确定

注意这里称为【转子】角度。
FOC模型是建立在电机转子基础上的,d轴q轴相关计算是按照转子角度计算的,而我们能直接获取的是磁编码器角度,也就是电机外壳角度,因为磁铁粘在电机外壳上,磁编码器感应磁铁角度。
这就出现了一个问题,转子零度与磁编码器零度之间差了多少度?解决了这个问题,那么转子角度就可知了,即等于磁编码器角度减去这个偏差角度。
其中一个简单方法是:电机上电后,控制相线形成磁场,将转子d轴吸引到转子零度的位置,然后读取磁编码器数据,这个角度就是偏差角度,叫做d轴强拖。
用代码实现这个方法:

//main.c
//......
  set_pwm_duty(0.5, 0, 0);              // 生成SVPWM模型中的基础矢量1,即对应转子零度位置
  HAL_Delay(400);                       // 保持一会,转子吸引过来需要时间
  rotor_zero_angle = motor_logic_angle; // 读取磁编码器数据,刚上电,用逻辑角度也可以
  set_pwm_duty(0, 0, 0);                // 松开电机
  HAL_Delay(10);
  /* USER CODE END 2 */
//......

前面在motor_runtime_param.h里设置的宏定义帮我们完成了转子角度的计算:#define rotor_logic_angle rotor_phy_angle *POLE_PAIRS


闭环控制

这个部分编写位置环、速度环、电流环代码。FOC闭环控制代码将放在ADC采样中断中进行。

控制函数接口定义

控制函数接口要符合人的直觉,让人可以直观地调用,先定义函数接口,再逐步补充这些函数:

//foc.h
//......
void lib_position_control(float rad);
void lib_speed_control(float speed);
void lib_torque_control(float torque_norm_d, float torque_norm_q);
void lib_position_speed_torque_control(float position, float max_speed, float max_torque_norm);
//......

先这么放着,后面会补充内容:

//foc.c
//......
void lib_position_control(float rad)
{
}

void lib_speed_control(float speed)
{
}

void lib_torque_control(float torque_norm_d, float torque_norm_q)
{
}

void lib_speed_torque_control(float speed_rad, float max_torque_norm)
{
}

void lib_position_speed_torque_control(float position_rad, float max_speed_rad, float max_torque_norm)
{
}
//......

力矩控制包括d轴和q轴强度。
上述定义的接口意思是实时计算一次电机相线pwm输出占空比,这是FOC核心算法,因此需要不断调用上述接口以及时更新输出占空比。
为了让后续功能性的代码更加容易编写,再对这个接口封装一层。这里创建了一个结构体变量,用于方便后续程序上的使用,你也可以不封装上述接口,直接反复调用上述接口也是可以的。

//foc.h
//......
typedef enum
{
    control_type_null,                  // 不进行控制
    control_type_position,              // 位置控制
    control_type_speed,                 // 速度控制
    control_type_torque,                // 力矩控制
    control_type_position_speed_torque, // 位置-速度-力矩控制
} motor_control_type;

typedef struct
{
    motor_control_type type;
    float position;        // 目标角度,单位rad
    float speed;           // 目标速度,单位rad/s
    float torque_norm_d;   // 目标d轴强度,0~1
    float torque_norm_q;   // 目标q轴强度,0~1
    float max_speed;       // 串级控制时的最大速度,单位rad/s
    float max_torque_norm; // 串级控制时的最大q轴力矩,0~1
} motor_control_context_t;

extern motor_control_context_t motor_control_context;
//......

有了这个封装后的结构体变量后,就可以在ADC中断里不断扫描这个结构体变量从而不断调用对应的FOC控制函数接口,在主代码里对这个结构体变量赋值即可。

//adc.c
//......
void HAL_ADCEx_InjectedConvCpltCallback(ADC_HandleTypeDef *hadc)
{
  if (hadc->Instance == ADC1)
  {
    float u_1 = ADC_REFERENCE_VOLT * ((float)HAL_ADCEx_InjectedGetValue(&hadc1, ADC_INJECTED_RANK_1) / ((1 << ADC_BITS) - 1) - 0.5);
    float u_2 = ADC_REFERENCE_VOLT * ((float)HAL_ADCEx_InjectedGetValue(&hadc2, ADC_INJECTED_RANK_1) / ((1 << ADC_BITS) - 1) - 0.5);
    float i_1 = u_1 / R_SHUNT / OP_GAIN;
    float i_2 = u_2 / R_SHUNT / OP_GAIN;
    motor_i_u = i_1;
    motor_i_v = i_2;

    float i_alpha = 0;
    float i_beta = 0;
    arm_clarke_f32(motor_i_u, motor_i_v, &i_alpha, &i_beta);
    float sin_value = arm_sin_f32(rotor_logic_angle);
    float cos_value = arm_cos_f32(rotor_logic_angle);
    float _motor_i_d = 0;
    float _motor_i_q = 0;
    arm_park_f32(i_alpha, i_beta, &_motor_i_d, &_motor_i_q, sin_value, cos_value);
    float filter_alpha_i_d = 0.1;
    float filter_alpha_i_q = 0.1;
    motor_i_d = low_pass_filter(_motor_i_d, motor_i_d, filter_alpha_i_d);
    motor_i_q = low_pass_filter(_motor_i_q, motor_i_q, filter_alpha_i_q);

    switch (motor_control_context.type)
    {
    case control_type_position:
      lib_position_control(motor_control_context.position);
      break;
    case control_type_speed:
      lib_speed_control(motor_control_context.speed);
      break;
    case control_type_torque:
      lib_torque_control(motor_control_context.torque_norm_d, motor_control_context.torque_norm_q);
      break;
    case control_type_position_speed_torque:
      lib_position_speed_torque_control(motor_control_context.position, motor_control_context.max_speed, motor_control_context.max_torque_norm);
      break;
    default:
      break;
    }
  }
}
//......

以位置控制为例,目标位置是90度,封装接口后的调用方式就变为:

//main.c
//......
  /* USER CODE BEGIN WHILE */
  motor_control_context.position = deg2rad(90);
  motor_control_context.type = control_type_position;
  while (1)
  {
//......

CMSIS-DSP提供的PID控制器

CMSIS-DSP数学库中提供了一个PID控制器,变量类型是arm_pid_instance_f32,将其用在我们的闭环控制中。使用方法是先调用arm_pid_init_f32配置PID系数,在闭环控制时使用arm_pid_f32计算输出PID输出值。该部分可以参考前文CMSIS-DSP。
设计一个函数,给位置环、速度环、电流环分别创建pid控制器:

//foc.c
//......
static arm_pid_instance_f32 pid_position;
static arm_pid_instance_f32 pid_speed;
static arm_pid_instance_f32 pid_torque_d;
static arm_pid_instance_f32 pid_torque_q;
void set_motor_pid(
    float position_p, float position_i, float position_d,
    float speed_p, float speed_i, float speed_d,
    float torque_d_p, float torque_d_i, float torque_d_d,
    float torque_q_p, float torque_q_i, float torque_q_d)
{
    pid_position.Kp = position_p;
    pid_position.Ki = position_i;
    pid_position.Kd = position_d;

    pid_speed.Kp = speed_p;
    pid_speed.Ki = speed_i;
    pid_speed.Kd = speed_d;

    pid_torque_d.Kp = torque_d_p;
    pid_torque_d.Ki = torque_d_i;
    pid_torque_d.Kd = torque_d_d;

    pid_torque_q.Kp = torque_q_p;
    pid_torque_q.Ki = torque_q_i;
    pid_torque_q.Kd = torque_q_d;
    arm_pid_init_f32(&pid_position, false);//false代表清空内部增量数据
    arm_pid_init_f32(&pid_speed, false);
    arm_pid_init_f32(&pid_torque_d, false);
    arm_pid_init_f32(&pid_torque_q, false);
}
//......
//foc.h
//......
void set_motor_pid(
    float position_p, float position_i, float position_d,
    float speed_p, float speed_i, float speed_d,
    float torque_d_p, float torque_d_i, float torque_d_d,
    float torque_q_p, float torque_q_i, float torque_q_d);
//......

在main.c中配置pid系数,先设置为0,后续调参过程中会反复修改这里的参数。

//main.c
//......
  /* USER CODE BEGIN 2 */
  set_motor_pid(
      0, 0, 0,
      0, 0, 0,
      0, 0, 0,
      0, 0, 0);
//......

位置控制

位置环的角度差值注意要使用cycle_diff函数进行周期化,因为角度是有周期的,这里使用逻辑角度以便可以多圈控制。
差值到底是rad - motor_logic_angle还是motor_logic_angle - rad呢?你可以这样想,如果真实角度motor_logic_angle是100度,输入目标角度rad是150度,pid控制器需要输出正数才能让电机转向目标角度,因此pid控制器也需要输入正数,因此只有rad - motor_logic_angle才是得到正数。
position_loop函数是干嘛用的?它有输入和输出,可以和速度环、力矩环串起来,因此单独隔离出一个函数方便控制环之间的串联。
d轴是不提供转向力矩的,对位置环没有用,因此目标d轴强度设置为0。
foc_forward开环控制在这里又用起来了,这个函数的本质是将目标d轴q轴强度落实到电机相线pwm通道上。
后续的速度环和位置环同理。

//foc.c
//......
static float position_loop(float rad)
{
    float diff = cycle_diff(rad - motor_logic_angle, position_cycle);
    return arm_pid_f32(&pid_position, diff);
}

void lib_position_control(float rad)
{
    float d = 0;
    float q = position_loop(rad);
    foc_forward(d, q, rotor_logic_angle);
}
//......

速度控制

速度是没有周期的,所以不需要使用cycle_diff函数周期化差值。

//foc.c
//......
static float speed_loop(float speed_rad)
{
    float diff = speed_rad - motor_speed;
    return arm_pid_f32(&pid_speed, diff);
}
void lib_speed_control(float speed)
{
    float d = 0;
    float q = speed_loop(speed);
    foc_forward(d, q, rotor_logic_angle);
}
//......

力矩(电流)控制

d轴和q轴都有力矩环,将d轴和q轴电流与宏定义设定的最大电流的比例看作【力矩强度】,力矩环输入的是0到1之间的百分比强度数据。

//foc.c
//......
static float torque_d_loop(float d)
{
    float diff = d - motor_i_d / MAX_CURRENT;
    return arm_pid_f32(&pid_torque_d, diff);
}

static float torque_q_loop(float q)
{
    float diff = q - motor_i_q / MAX_CURRENT;
    return arm_pid_f32(&pid_torque_q, diff);
}

void lib_torque_control(float torque_norm_d, float torque_norm_q)
{
    float d = torque_d_loop(torque_norm_d);
    float q = torque_q_loop(torque_norm_q);
    foc_forward(d, q, rotor_logic_angle);
}
//......

位置-速度-力矩控制

将位置环、速度环、力矩环串联起来,就是【位置-速度-力矩控制】。可前往查看前文位置、速度、电流控制的控制框图。
位置-速度-力矩联合控制的时候,主角是还是位置环,速度、力矩是运行过程中的最大值,比如:输入的速度是30rad/s,意思是电机角度到位过程中的速度按照30rad/s进行控制,角度到位后,电机速度会按照0rad/s进行控制。
这里的代码抽出了速度-力矩控制作为一个单独的函数,这样既开放了【速度-力矩控制】的接口,也可以被【位置-速度-力矩控制】调用。

//foc.c
//......
void lib_speed_torque_control(float speed_rad, float max_torque_norm)
{
    float torque_norm = speed_loop(speed_rad);
    torque_norm = min(fabs(torque_norm), max_torque_norm) * (torque_norm > 0 ? 1 : -1);
    lib_torque_control(0, torque_norm);
}

void lib_position_speed_torque_control(float position_rad, float max_speed_rad, float max_torque_norm)
{
    float speed_rad = position_loop(position_rad);
    speed_rad = min(fabs(speed_rad), max_speed_rad) * (speed_rad > 0 ? 1 : -1);
    lib_speed_torque_control(speed_rad, max_torque_norm);
}
//......

PID系数调节

到目前位置,已经在ADC中断中放置了FOC代码,也在main.c中设置好了闭环控制类型(示例是位置控制),只要再设置好PID系数,电机就能进行闭环动作了。PID系数调节比较依靠经验,由于本人能力有限,我无法深入讲解,网上有大量的PID系数调节教程。

  • 位置环

本文选择的电机是云台电机,这里的位置环的PID系数只用到P系数和D系数,可以在主while(1)循环里打印电机实时角度辅助调节。
首先设定为位置控制模式,由于FOC代码里位置控制是按照逻辑角度进行控制的,因此这里设置的90度是逻辑角度:

//main.c
//......
  /* USER CODE BEGIN WHILE */
  motor_control_context.position = deg2rad(90);
  motor_control_context.type = control_type_position;
  while (1)
  {
  	printf("%f\n", motor_logic_angle);
  	/* USER CODE END WHILE */
//......

调节P系数建议从2开始,0.1为步距往上调整,直到电机到位过程中出现轻微位置弹簧感:
在这里插入图片描述
然后设定D系数,建议从5开始,1为步距往上调,直到弹簧感消失,这样一个归位迅速而回弹轻微的位置环就调节好了:
在这里插入图片描述

  • 速度环

调节转速环的时候,转速滤波系数filter_alpha_speed非常重要,决定了计算出来的速度是否平稳,本文使用的低通滤波,如果想要滤波平稳一点,那么就会有滞后,如果想要速度更新及时一点,那么滤波结果波动就大,这个转速滤波的参数也是需要自己调节测试的,我为了验证FOC算法,设定的0.07,滞后比较厉害,但是速度滤波出来比较平稳。
在主while(1)前设定FOC为速度控制模式:

//main.c
//......
  /* USER CODE BEGIN WHILE */
  motor_control_context.speed = 20 * 2 * PI;
  motor_control_context.type = control_type_position;
  while (1)
  {
  	printf("%f\n", motor_speed);
  	/* USER CODE END WHILE */
//......

速度环主要是使用P系数和I系数,速度波动比较剧烈,D系数干扰大。P系数要给的非常小,建议从0.01开始,步距0.001往上调节,I系数从0.001开始往上调节。
经过我简单的调节,本文选用的电机在空载时,在速度环模式下,速度大概能到每秒122.6弧度(每秒39转,每分钟1171转)左右。
在这里插入图片描述

  • 力矩环

力矩环同样有滤波系数filter_alpha_i_dfilter_alpha_i_q,分别对应d轴电流和q轴电流滤波,我为了验证FOC算法,这两个参数设置的0.1。
在主while(1)前设定FOC为力矩控制模式:

//main.c
//......
  /* USER CODE BEGIN WHILE */
  motor_control_context.torque_norm_d = 0;
  motor_control_context.torque_norm_q = 1;
  motor_control_context.type = control_type_torque;
  while (1)
  {
  	printf("%f\n", motor_speed);
  	/* USER CODE END WHILE */
//......

经过我简单的调节,本文选用的电机在空载时,在力矩环模式下,速度大概能到每秒145.6弧度(每秒23转,每分钟1391转)左右。
在这里插入图片描述

  • 位置-速度-力矩串级PID

三者串级控制的时候,不能直接使用单独控制时的PID系数,需要重新调节,要从最内环开始串级调节到最外环,即先调好力矩控制,再调好速度-力矩控制,再调好位置-速度-力矩控制。


注意点

  • 注意调试过程中打断点或者点击Stop Debug的时候,不要暂停时间过长,因为暂停的时候电机相线的pwm占空比也固定住了,如果某一相的占空比刚好固定在非常高的状态,会导致电机迅速发热。
  • stm32f103系列的算力有限,keil编译的时候请开启-O3优化,否则可能需要再降低SPI频率或者提高高级定时器重复计数器以降低FOC代码计算频率。

至此已经完成了从零开始实现stm32无刷电机FOC,本文源码开源,可前往查看:https://gitee.com/best_pureer/stm32_foc。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2185666.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

两个月冲刺软考——网络与信息安全知识

1. 什么是网络监听&#xff1f; 网络监听是主机的一种工作模式&#xff0c;在这种模式下&#xff0c;主机可以接收到本网段在同一条物理通道上传输的所有信息。 2. 客户端与服务器建立连接的步骤 域名解析 建立TCP连接 发送HTTP请求 服务器处理请求 发送HTTP响应 客户端…

uniapp 上了原生的 echarts 图表插件了 兼容性还行

插件地址&#xff1a;echarts - DCloud 插件市场 兼容性这块儿不知道后期会不会支持其他浏览器 H5 的话建议可以用原生的不用这个插件

【单片机】单片机map表详细解析

1、RO Size、RW Size、ROM Size分别是什么 首先将map文件翻到最下面&#xff0c;可以看到 1.1 RO Size&#xff1a;只读段 Code&#xff1a;程序的代码部分&#xff08;也就是 .text 段&#xff09;&#xff0c;它存放了程序的指令和可执行代码。 RO Data&#xff1a;只读…

二叉树进阶练习——根据二叉树创建字符串

1.题目解析 题目来源&#xff1a;606.根据二叉树创建字符串 测试用例 2.算法原理 根据上面的题目我们知道这里需要根据前序遍历来创建字符串&#xff0c;并且需要将每棵子树使用括号括起来&#xff0c;但是要根据实际情况省略括号&#xff0c;比如当右子树为空左子树为空就可…

基于opencv-C++dnn模块推理的yolov5 onnx模型

前言 由于最近手边的项目要求&#xff0c;本人需要在一块算力吃紧的板端上部署yolov5目标检测模型&#xff08;纯CPU推理&#xff09;&#xff0c;考虑到python在板端上的运行速率和其运行时所占内存过大&#xff0c;因此使用torch.hub.load对训练好后的pt模型进行加载和运行不…

rtmp协议转websocketflv的去队列积压

websocket server的优点 websocket server的好处&#xff1a;WebSocket 服务器能够实现实时的数据推送&#xff0c;服务器可以主动向客户端发送数据 1 不需要客户端不断轮询。 2 不需要实现httpserver跨域。 在需要修改协议的时候比较灵活&#xff0c;我们发送数据的时候比较…

使用默认不可变的Rust变量会踩什么坑

讲动人的故事&#xff0c;写懂人的代码 Rust的变量真的是名不副实。名字中明明有个“变”字&#xff0c;却默认不可变。还美其名曰“不可变变量”。要想让变量名副其实&#xff0c;还必须费心额外加个mut关键字&#xff0c;并必须称其为“可变变量”&#xff0c;才能与前者区分…

对比学习与图像去雾在24TIP和CVPR经典图像去雾论文中的良好结合,展示出模型良好的泛化能力(本期内容较详细 多)

今天主要内容是图像去雾&#xff0c;对比学习&#xff0c;无监督学习&#xff0c;要分享的三篇论文分别是&#xff1a; 21CVPR Contrastive Learning for Compact Single Image Dehazing 24 TIP UCL-Dehaze: Toward Real-World Image Dehazing via Unsupervised Contrastive Le…

stm32f103调试,程序与定时器同步设置

在调试定时器相关代码时&#xff0c;注意到定时器的中断位总是置1&#xff0c;怀疑代码有问题&#xff0c;经过增大定时器的中断时间&#xff0c;发现定时器与代码调试并不同步&#xff0c;这一点对于调试涉及定时器的代码是非常不利的&#xff0c;这里给出keil调试stm32使定时…

HTB:Vaccine[WriteUP]

目录 连接至HTB服务器并启动靶机 1.Besides SSH and HTTP, what other service is hosted on this box? 2.This service can be configured to allow login with any password for specific username. What is that username? 3.What is the name of the file downloaded…

Kafka和RabbitMQ区别

RabbitMQ的消息延迟是微秒级&#xff0c;Kafka是毫秒级&#xff08;1毫秒1000微秒&#xff09; 延迟消息是指生产者发送消息发送消息后&#xff0c;不能立刻被消费者消费&#xff0c;需要等待指定的时间后才可以被消费。 Kafka的单机呑吐量是十万级&#xff0c;RabbitMQ是万级…

10.4 Linux_并发_线程

概述 线程的共享资源&#xff1a; 可执行的指令、静态数据、文件描述符、当前工作目录、用户ID、用户组ID 线程的私有资源&#xff1a; 线程ID、程序计数器PC和相关寄存器、堆栈、错误号、优先级、执行状态和属性 线程编译&#xff1a; gcc <.c文件> -l pthread -o…

数据集-目标检测系列- 螃蟹 检测数据集 crab >> DataBall

数据集-目标检测系列- 螃蟹 检测数据集 crab >> DataBall 数据集-目标检测系列- 螃蟹 检测数据集 crab >> DataBall 数据量&#xff1a;3k 想要进一步了解&#xff0c;请联系。 DataBall 助力快速掌握数据集的信息和使用方式&#xff0c;会员享有 百种数据集&a…

加密与安全_TOTP 一次性密码生成算法

文章目录 PreTOTP是什么TOTP 算法工作原理TOTP 生成公式TOTP 与 HOTP 的对比Code生成TOTP验证 TOTP使用场景小结 TOTP 与 HOTP 的主要区别TOTP 与 HOTP应用场景比较TOTP 与 HOTP安全性分析 Pre 加密与安全_HTOP 一次性密码生成算法 https://github.com/samdjstevens/java-tot…

YOLO11改进|卷积篇|引入可变核卷积AKConv

目录 一、AKConv卷积1.1AKConv卷积介绍1.2MLCA核心代码 五、添加MLCA注意力机制5.1STEP15.2STEP25.3STEP35.4STEP4 六、yaml文件与运行6.1yaml文件6.2运行成功截图 一、AKConv卷积 1.1AKConv卷积介绍 AKConv允许卷积参数的数量以线性方式增加或减少&#xff0c;而不是传统的平…

C# 表达式与运算符

本课要点&#xff1a; 1、表达式的基本概念 2、常用的几种运算符 3、运算符的优先级 4、常见问题 一 表达式 表达式是由运算符和操作数组成的。、-、*和/等都是运算符&#xff0c;操作数包括文本、常量、变量和表达式等。 二 算术运算符 2.1 算术运算符的使用 三 常见错误 …

Cocotb 学习记录--V01

1. Windows 下安装cocotb pip install cocotb 其他参考&#xff1a; 1.Welcome to cocotb’s documentation! — cocotb 1.9.1 documentation

【Koa】文件上传

主要使用两个 koa 插件&#xff0c;koa-body 里面自带文件上传功能&#xff0c;还有一个 koa-staitc 用于配置静态资源目录&#xff08;可以通过路径直接访问图片&#xff09;。 router const Router require(koa/router); const {upload} require(../controller/user);con…

Spring之生成Bean

Bean的生命周期&#xff1a;实例化->属性填充->初始化->销毁 核心入口方法&#xff1a;finishBeanFactoryInitialization-->preInstantiateSingletons DefaultListableBeanFactory#preInstantiateSingletons用于实例化非懒加载的bean。 1.preInstantiateSinglet…

【RADARSAT Constellation Mission(RCM)卫星星座简介】

RADARSAT Constellation Mission&#xff08;RCM&#xff09;卫星星座是加拿大太空局&#xff08;CSA&#xff09;的下一代C波段合成孔径雷达&#xff08;SAR&#xff09;卫星星座&#xff0c;以下是对其的详细介绍&#xff1a; 一、基本信息 发射时间&#xff1a;2019年6月…