在之前发布的文章之中已经阐述了通过 MC Workbench 生成电机控制源代码和 Motor Pilot 检测、调节电机参数、收集运行参数曲线等功能,本文对这部分内容不再过多讲解。
ST电机控制开发套件快速上手
使用Motor Pilot Profiler工具识别未知参数电机
1.技术文档
-在 Motor Pilot 的 Help 菜单里面内置了 MC SDK 的技术文档入口,这个文档是位于 /MC_SDK_x.x.x(版本号)/Documentation/html/modules.html 目录下的html文件。

打开之后主页是这样的。点击 User Manual ,可以看到目录下内容非常齐全,涵盖了电机驱动中从底层硬件驱动到上层应用架构的方方面面,是一份极具参考价值的开发指南。

文档首先从基础理论与核心算法入手,详细讲解了PMSM电机结构、FOC驱动原理,以及PMSM的MTPA控制、弱磁控制和PID调节器的设计背景;
在硬件实现细节上,详细讲解了PWM生成策略(包括SVPWM、过调制等)以及多种电流采样拓扑(涵盖三电阻、单电阻及隔离传感器方案);
此外,针对反馈系统与系统架构,手册不仅包含了霍尔、编码器等有感及无感算法的处理,还延伸到了六步算法、FreeRTOS应用、上位机通讯数据包协议、位置控制以及MC状态机管理等设计。
总而言之,这份手册几乎囊括了电机控制开发中需要掌握的所有关键技术点,无论是对于理解控制原理还是工程落地,都有着非常重要的指导意义。
Component 页是对MCSDK软件接口即其函数实现的介绍,我们重点查看API,利用其提供的API来进行电机控制。其底层实现有机会再一起详细学习。

2. API介绍
MCSDK的API在mc_api.h/.c中提供。下面介绍的是比较常用的接口。
①启停接口:
启动电机1的启动程序。
启动电机1的停止程序。
②状态接口:
MCI_CommandState_t MC_GetCommandStateMotor1 (void)
返回电机1的最后一个缓冲命令的状态。
int16_t MC_GetMecSpeedReferenceMotor1 (<span class="hljs-keyword">void</span>)
返回电机1的当前机械转子转速参考,以SPEED_UNIT定义的单位表示。
关于SPEED_UNIT有如下解释:
转速单位用于与应用程序的接口。
这个符号定义了API函数用于其速度参数的单位中1赫兹的值。
例如,如果选择的单位是RPM,则SPEED_UNIT被定义为60,因为1 Hz等于60 RPM。默认的单位是U_01HZ,由Workbench在项目的初始生成中设置。由于此符号是在用户节中定义的,因此用户设置的自定义值将在整个项目再生过程中持续存在。
电机控制工作台计算的调速PID参数适用于01Hz的转速。电机控制子系统内部缩放它们以适应实际的速度单位。
此符号不应设置为文字数值。相反,应该将其设置为为此目的预定义的符号之一,例如U_RPM, U_01HZ,…有关详细信息,请参阅SpeedUnit。
int16_t MC_GetMecSpeedAverageMotor1 (void)
返回电机1最后一次计算的平均机械转子转速,用SPEED_UNIT定义的单位表示。
MCI_State_t MC_GetSTMStateMotor1 (void)
返回电机1状态机的当前状态。
uint16_t MC_GetCurrentFaultsMotor1 (void)
返回一个位字段,显示电机1上的所有当前故障。
span
uint16_t MC_GetCurrentFaultsMotor1 (void)
修改电机1的目标速度。
如果电机1的状态机处于RUN状态,则立即执行。可以使用MC_GetCommandStateMotor1() 检查命令的状态。如果应用程序使用无传感器电机控制技术,则不能反转旋转方向。如果hFinalSpeed参数的符号与当前速度的符号不同,则当转速即将达到0rpm时,将不会完成速度斜坡并发生速度反馈错误(MC_SPEED_FDBK)。
其他的函数接口可以自行查阅。
span
为了直观地看到电机运行的状态以及速度,我连接了一个1.28英寸,128x128像素的SPI屏幕,驱动芯片是ST7735S。
值得注意的是,在分配引脚的时候不能只根据CubeMX中的空闲引脚分配,而需要根据原理图进行分配。我最开始分配引脚的时候将MOSI放到了PB5引脚,这个引脚在CubeMX里是未初始化的,但是实际上在扩展板上连接到了STSPIN830芯片的STBY引脚。这个错误导致了STBY不稳定,同时也影响了SPI通信的稳定性,通信速率达到5MHz以上屏幕就花屏或白屏;同时电机完全无法正常启动。
同时,最开始分配背光BLK到引脚PB3、数据/指令D/C到引脚PC7,其实在扩展板上也连接到了霍尔传感器的接口上,通过两个0R电阻短接。这也导致了SPI通信失败。最开始一直以为是软件问题,直到用示波器测量的时候才发现了异常所在。所以使用扩展板的时候检查硬件连接是十分必要的。
最终确定的引脚分配如下:
span
其中I2C1是用于连接AS5600磁编码器的;最开始分配的是PC8和PC9,后面发现开发板上PC9用于反电动势测量,虽然当前FOC应用不需要使用PC9,但是I2C总线还是重新分配到I2C1的PB8和PB9两个引脚,避免后续的应用中发生冲突。
span
我们的程序主要新增的功能:
- 调用MCSDK接口,显示电机状态、观测器测量的速度到屏幕;
- 新增AS5600驱动,读取并显示编码器测量的速度到屏幕。
这边使用的是VSCode+CMake开发环境,新增的文件需要在CmakeList.txt中添加文件路径。
# Add sources to executable
target_sources(${CMAKE_PROJECT_NAME} PRIVATE
# Add user sources here
Hardware/AS5600_I2C.c
Hardware/BSP_LCD.c
Hardware/ST7735.c
)
# Add include paths
target_include_directories(${CMAKE_PROJECT_NAME} PRIVATE
# Add user defined include paths
Hardware
)
显示方面,我的软件封装层级是这样的:
BSP_LCD 用于描述LCD接口的宏定义,ST7735.h则封装了用于驱动ST7735的初始化、显示、绘图等等函数。修改硬件之后只需要修改BSP_LCD中的定义即可。
//BSP_LCD.h
#define RST_L HAL_GPIO_WritePin(LCD_RST_GPIO_Port, LCD_RST_Pin, GPIO_PIN_RESET)
#define RST_H HAL_GPIO_WritePin(LCD_RST_GPIO_Port, LCD_RST_Pin, GPIO_PIN_SET)
#define DC_CMD HAL_GPIO_WritePin(LCD_DC_GPIO_Port, LCD_DC_Pin, GPIO_PIN_RESET)
#define DC_DATA HAL_GPIO_WritePin(LCD_DC_GPIO_Port, LCD_DC_Pin, GPIO_PIN_SET)
#define CS_L HAL_GPIO_WritePin(SPI3_CS_GPIO_Port, SPI3_CS_Pin, GPIO_PIN_RESET)
#define CS_H HAL_GPIO_WritePin(SPI3_CS_GPIO_Port, SPI3_CS_Pin, GPIO_PIN_SET)
#ifndef LCD_BLK_ACTIVE_LOW
#define LCD_BLK_ACTIVE_LOW 0
#endif
#if LCD_BLK_ACTIVE_LOW
#define BLK_L HAL_GPIO_WritePin(LCD_BLK_GPIO_Port, LCD_BLK_Pin, GPIO_PIN_SET)
#define BLK_H HAL_GPIO_WritePin(LCD_BLK_GPIO_Port, LCD_BLK_Pin, GPIO_PIN_RESET)
#else
#define BLK_L HAL_GPIO_WritePin(LCD_BLK_GPIO_Port, LCD_BLK_Pin, GPIO_PIN_RESET)
#define BLK_H HAL_GPIO_WritePin(LCD_BLK_GPIO_Port, LCD_BLK_Pin, GPIO_PIN_SET)
#endif
这里只截取部分代码。
//ST7735.h中的接口
void ST7735_reset(void);
void ST7735_TurnON(void);
void ST7735_TurnOFF(void);
void ST7735_write_command(uint8_t command);
void ST7735_write_data(uint8_t data);
void ST7735_write_data_u16(uint16_t data);
void ST7735_init(void);
/* 地址控制 */
void ST7735_Set_Cursor(uint16_t x, uint16_t y);
void ST7735_Set_Region(uint16_t x_start, uint16_t y_start,
uint16_t x_end, uint16_t y_end);
void ST7735_SpinScreen(uint8_t locate);
/* 填充 */
void ST7735_Screen_Fill(uint16_t color);
void ST7735_Region_Fill(uint16_t x1,uint16_t y1,
uint16_t x2,uint16_t y2,uint16_t color);
/* 基础图形 */
void ST7735_DrawPoint(uint16_t x,uint16_t y,uint16_t Color);
void ST7735_DrawArc(uint16_t X, uint16_t Y, uint16_t R,
uint16_t start_angle, uint16_t end_angle,
uint16_t color);
void ST7735_DrawQuarterCircle(uint16_t X, uint16_t Y, uint16_t R,
uint8_t quadrant, uint16_t color);
void ST7735_DrawCircle(uint16_t X,uint16_t Y,uint16_t R,uint16_t fc);
void ST7735_DrawLine(uint16_t x0, uint16_t y0,
uint16_t x1, uint16_t y1,
uint16_t Color);
void ST7735_DrawRect(uint16_t x, uint16_t y,
uint16_t w, uint16_t h,
uint16_t color);
void ST7735_DrawRoundRect(uint16_t x, uint16_t y,
uint16_t w, uint16_t h,
uint16_t r, uint16_t color);
void ST7735_FillRoundRect(uint16_t x, uint16_t y,
uint16_t w, uint16_t h,
uint16_t r, uint16_t color);
/* 字符 / 字符串 / 数字 / 图片 / 中文 */
void ST7735_ShowChar(uint8_t x,uint8_t y,
uint16_t fc,uint16_t bc,char c);
void ST7735_ShowString(uint8_t x,uint8_t y,
uint16_t fc,uint16_t bc,char *c);
void ST7735_ShowNumber(uint8_t x,uint8_t y,
uint16_t fc,uint16_t bc,long long num);
void ST7735_ShowImage(uint16_t x, uint16_t y,
uint16_t length, uint16_t width,
const unsigned char *p);
void ST7735_ShowChinese(uint8_t x,uint8_t y,
uint16_t fc,uint16_t bc,char *c);
int map(char *c);
AS5600的驱动只封装了一个读取角度值的函数:
#ifndef __AS5600_I2C_H
#define __AS5600_I2C_H
#include "main.h"
#define AS5600_ADDR (0x36U << 1)
uint16_t AS5600_ReadRawAngle(void);
#endif
在main.c中声明以下变量:
//时间变量用于主循环中非阻塞定时
static uint32_t display_update_time = 0;
static const uint32_t DISPLAY_UPDATE_INTERVAL = 100; // 100ms
static uint32_t encoder_update_time = 0;
static const uint32_t ENCODER_UPDATE_INTERVAL = 10; // 10ms
//实现了仅变化才重绘的刷新逻辑
static bool lcd_init_done = false;
static MCI_State_t last_state = (MCI_State_t)0xFF;
static int16_t last_speed_rpm = INT16_MIN;
static uint16_t last_fault_now = 0xFFFF;
static uint16_t last_fault_occ = 0xFFFF;
static uint16_t last_enc_angle = 0xFFFF;
static int16_t last_enc_speed_rpm = INT16_MIN;
static bool last_enc_valid = false;
//编码器测速用
static uint16_t prev_enc_angle = 0U;
static uint32_t prev_enc_tick = 0U;
static bool enc_speed_ready = false;
static uint16_t enc_angle_latest = 0xFFFF;
static int16_t enc_speed_rpm_latest = 0;
static bool enc_valid_latest = false;
static int16_t last_pot_target_rpm = INT16_MIN;
此函数用于将电机状态转化为字符串,在显示任务中调用:
/* Motor state to string */
static const char* MC_GetStateString(MCI_State_t state)
{
switch (state) {
case ICLWAIT: return "ICLWAIT";
case IDLE: return "IDLE";
case ALIGNMENT: return "ALIGNMENT";
case CHARGE_BOOT_CAP: return "CHARGE_BOOT";
case OFFSET_CALIB: return "OFFSET_CALIB";
case START: return "START";
case SWITCH_OVER: return "SWITCH_OVER";
case RUN: return "RUN";
case STOP: return "STOP";
case FAULT_NOW: return "FAULTNOW";
case FAULT_OVER: return "FAULTOV";
case WAIT_STOP_MOTOR: return "WAIT_STOP";
case OTF_DETECTION: return "OTF_DET";
case OTF_BRAKE: return "OTF_BRAKE";
default: return "UNKNOWN";
}
}
更新电机参数显示的函数;主要使用
static void Display_UpdateMotorInfo(void)
{
MCI_State_t motor_state = MC_GetSTMStateMotor1();
int16_t speed_unit = MC_GetMecSpeedAverageMotor1();
int16_t speed_rpm = (int16_t)(((int32_t)speed_unit * U_RPM) / SPEED_UNIT);
uint16_t fault_now = MC_GetCurrentFaultsMotor1();
uint16_t enc_angle = enc_angle_latest;
int16_t enc_speed_rpm = enc_speed_rpm_latest;
bool enc_valid = enc_valid_latest;
char line_buf[24];
if (motor_state != last_state)
{
snprintf(line_buf, sizeof(line_buf), "%-8s", MC_GetStateString(motor_state));
ST7735_ShowString(56, 28, BLACK, WHITE, line_buf);
last_state = motor_state;
}
if (speed_rpm != last_speed_rpm)
{
snprintf(line_buf, sizeof(line_buf), "%-8d", speed_rpm);
ST7735_ShowString(56, 48, BLACK, WHITE, line_buf);
last_speed_rpm = speed_rpm;
}
if (fault_now != last_fault_now)
{
snprintf(line_buf, sizeof(line_buf), "0x%04X", fault_now);
ST7735_ShowString(56, 68, BLACK, WHITE, line_buf);
last_fault_now = fault_now;
}
if ((!enc_valid && last_enc_valid) ||
(enc_valid && (!last_enc_valid || enc_angle != last_enc_angle || enc_speed_rpm != last_enc_speed_rpm)))
{
if (enc_valid)
{
snprintf(line_buf, sizeof(line_buf), "A:%4u V:%4d", enc_angle, enc_speed_rpm);
}
else
{
snprintf(line_buf, sizeof(line_buf), "A:ERR V:ERR");
}
ST7735_ShowString(8, 108, BLACK, WHITE, " ");
ST7735_ShowString(8, 108, BLACK, WHITE, line_buf);
last_enc_valid = enc_valid;
last_enc_angle = enc_angle;
last_enc_speed_rpm = enc_speed_rpm;
}
}
AS5600编码器读到的数据是角度值,在主循环中每10ms测得一次角度数据,并换算得到速度值。这个方法最高能测到6000rpm的转速,如果转速更高的话需要减小计时值。为了方便主循环中的定时是读取SysTick值来计算的,如果要更精准的话最好启用定时器。
//编码器角度值到速度的转换
static void Encoder_UpdateFromAS5600(void)
{
uint16_t enc_angle = AS5600_ReadRawAngle();
if (enc_angle != 0xFFFFU)
{
uint32_t now = HAL_GetTick();
int16_t enc_speed_rpm = enc_speed_rpm_latest;
if (enc_speed_ready)
{
uint32_t dt_ms = now - prev_enc_tick;
if (dt_ms > 0U)
{
int32_t delta = (int32_t)enc_angle - (int32_t)prev_enc_angle;
if (delta > 2048)
{
delta -= 4096;
}
else if (delta < -2048)
{
delta += 4096;
}
/* rpm = delta_count * 60000 / (counts_per_rev * dt_ms) */
int32_t rpm32 = (delta * 60000) / (4096 * (int32_t)dt_ms);
if (rpm32 > INT16_MAX)
{
rpm32 = INT16_MAX;
}
else if (rpm32 < INT16_MIN)
{
rpm32 = INT16_MIN;
}
enc_speed_rpm = (int16_t)rpm32;
}
}
prev_enc_angle = enc_angle;
prev_enc_tick = now;
enc_speed_ready = true;
enc_angle_latest = enc_angle;
enc_speed_rpm_latest = enc_speed_rpm;
enc_valid_latest = true;
}
else
{
enc_speed_ready = false;
enc_valid_latest = false;
enc_angle_latest = 0xFFFF;
enc_speed_rpm_latest = 0;
}
}
主函数中初始化过程加入:
ST7735_init();
Display_InitLayout();
Display_UpdateMotorInfo();
display_update_time = HAL_GetTick();
encoder_update_time = display_update_time;
ST7735_TurnON();
完成ST7735的初始化以及亮屏,以及初始时间戳的记录;
while循环中加入以下测速、显示的任务:
while (1)
{
if (HAL_GetTick() - encoder_update_time >= ENCODER_UPDATE_INTERVAL)
{
Encoder_UpdateFromAS5600();
encoder_update_time = HAL_GetTick();
}
if (lcd_init_done && (HAL_GetTick() - display_update_time >= DISPLAY_UPDATE_INTERVAL))
{
Display_UpdateMotorInfo();
display_update_time = HAL_GetTick();
}
}
运行结果如下,可以看到观测器在中高速状态下和编码器测得的速度是相近的。缺点就是不能适应低速运行;在drive_parameters.h中也定义了观测器可观测的最低速度值,运行的话尽量不要低于这个转速:
/* Observer start-up output conditions */
#define OBS_MINIMUM_SPEED_RPM 486
依旧不能放视频,放一张图片。