FOC 磁场定向控制的 C 语言实现
笔记 / 嵌入式
**磁场定向控制(FOC)**把三相电机的定子电流从静止的 坐标系,经 Clarke、Park 变换搬到跟着转子一起转的 坐标系上——于是在旋转坐标系里,交流量被”拍平”成两个直流量 、,用两个 PI 控制器就能分别管住磁链和转矩。这里是它的原理框架、几个关键变换的数学模型,以及一份可以直接编译验证的 C 实现。PWM 侧的原理见 PWM脉冲宽度调制详解,代码里用到的定时器与中断见 单片机定时器与中断系统详解;SVPWM 矢量合成的专题展开见 电机控制的空间矢量调制 SVPWM,无刷电机本体与六步换相的背景见 无刷电机的工作原理,电流环/速度环背后的 PI 控制器数学见 PID 算法的基本原理和数学描述。
一、磁场定向控制(FOC)介绍
1.1 FOC 控制模型介绍
磁场定向控制(FOC),又称矢量控制,是一种控制方法,用于在多种电机类型(包括感应电机、永磁同步电机 PMSM 和无刷直流 BLDC 电机)的全转矩和转速范围内实现良好的控制能力。如果超出额定转速,则使用配合弱磁的磁场定向控制。
以下模块图显示了一个磁场定向控制架构,包括以下组件:

图:FOC 整体架构。控制算法的边界很清晰——只有两个电流 PI 在 dq 域,往右全是坐标变换与 PWM 生成,往左是可选的外环(速度环/位置环)。 被同时送给 SVPWM 发生器与保护逻辑,正是欠压/过流保护要盯着的那一路。
1.2 模型功能
设计磁场定向控制的电机控制工程师执行以下任务:
- 为电流回路开发具有两个 PI 控制器的控制器架构
- 为可选的转速外环和位置外环开发 PI 控制器
- 调节所有 PI 控制器的增益以满足性能要求
- 设计用于控制 PWM 的空间矢量调制器
- 如果使用无传感器控制,则设计观测器算法来估计转子位置和速度
- 设计每安培最大转矩或弱磁控制算法,以生成最佳
id_ref和iq_ref - 实现在计算上高效的帕克变换、克拉克变换和帕克逆变换
- 设计故障检测和保护逻辑
- 验证和确认控制器在不同工况下的性能
- 在微控制器或 FPGA 上实现采用定点或浮点的控制器
二、FOC 模型的几个重要转换关系
2.1 Clarke Transform
Clarke Transform 模块计算 参考系中平衡三相分量的克拉克变换,并输出静止 参考系中平衡两相正交分量。该模块也可以计算三相分量 、 和 的克拉克变换,并输出分量 、 和 。对于平衡系统,零分量等于零。使用输入数参数以使用两个或三个输入。
使用双输入配置时,该模块接受三相()中的两个信号,自动计算第三个信号,并输出 参考系中的对应分量。例如,该模块接受 和 输入值或多路复用输入值 ,其中相位 轴与 轴对齐。

图: 的几何含义。 轴与 轴重合, 轴与之正交——这就是”静止两相正交坐标系”里”正交”二字的来源(图中 、 分别与 轴成 120°)。
数学模型如下:
2.2 Inverse Clarke Transform
Inverse Clarke Transform 模块计算静止 参考系中平衡的两相正交分量的克拉克逆变换,并输出静止 参考系中平衡的三相分量。该模块也可以计算分量 、 和 的克拉克逆变换,以输出三相分量 、 和 。对于平衡系统,零分量等于零。使用输入数参数以使用两个或三个输入。
该模块接受 - 轴分量作为输入,并输出对应的三相信号,其中相位 轴与 轴对齐。

图:静止的 坐标系本身—— 轴水平、 轴竖直,是不随转子旋转的”地面参照系”。
数学模型:
2.3 Park Transform
Park Transform 模块计算静止 参考系中两相正交分量(、)或多路复用 分量的帕克变换。该模块接受以下输入:
静止参考系中的 - 轴分量或多路复用分量 。使用输入数参数以使用两个或三个输入。对应变换角度的正弦值和余弦值。
在使用双输入配置时,它会输出旋转 参考系中的正交直轴()和交轴()分量。在使用三输入配置时,它输出多路复用分量 。对于平衡系统,零分量等于零。
可以配置模块,使 轴或 轴在时间 处与 轴对齐。
下列各图显示在以下情形下 参考系和旋转 参考系中的 - 轴分量:

图:(黄色,静止)与 (黑色,旋转)两套坐标系的关系。 就是”电角度”——Park 变换的输入之一,也是为什么无传感器 FOC 的难点在于估准这个角。
数学模型:
当 轴与 轴对齐时:
其中: 和 是静止 参考系中的两相正交分量。 和 是旋转 参考系中的直轴和交轴正交分量。
2.4 Inverse Park Transform
Inverse Park Transform 模块计算正交直轴()和正交轴()分量或旋转 参考系中的多路复用 分量的帕克逆变换。可以对该模块进行配置,使 轴或 轴在时间 处与 轴对齐。
该模块接受以下输入:
旋转参考系中的 - 轴分量或多路复用分量 。使用输入数参数以使用两个或三个输入。对应变换角度的正弦值和余弦值。
在使用双输入配置时,它输出静止 参考系中的两相正交分量。在使用三输入配置时,它输出多路复用分量 。对于平衡系统,零分量等于零。
下列各图显示在以下情形下的旋转 参考系和 参考系中的 - 轴分量:
当 轴与 轴对齐时:
当 轴与 轴对齐时:
其中: 和 是旋转 参考系中的直轴和交轴正交分量。 和 是静止 参考系中的两相正交分量。
三、STM32 模拟实现 FOC
3.1 FOC 算法的 C 语言实现
1)Clarke 变换

2)Clarke 逆变换

3) Park 变换

4) park 逆变换

3.2 测试代码实现
代码 216 行:设置 d 轴的值为 0.2f 代码 217 行:设置 q 轴的值为 0 代码 221 行:设置电机 DQ 轴上运行的角度值范围(0~2π) 代码 224 行:实现 park 逆变换,将 D、Q 坐标转变至 α、β 坐标系上 代码 229 行:将经过逆 park 生成的 ia、ib、ic,生成 svpwm

图:测试代码把 从 扫到 ,每步算出 与三相电压并打印——这是开环的变换链路验证,不接电机也不需要 ADC 反馈,光靠串口输出就能画出后面那几张波形图。
四、波形验证
- D、Q 坐标系中的值经过逆 park 变换生成的 α、β 坐标系上的波形图,、 的波形相位相差 90°

- α、β 坐标系经过逆 Clark 生成的 ia、ib、ic 的波形图,、、 三个波形之间相位相差 120°

- 将 α、β 坐标系和 ia、ib、ic 的波形图放在同一个图像中进行参照

图:三相波形互差 120°、两相波形互差 90°——这正是逆 Clarke 矩阵里 与 这几个系数要达到的效果。三个波形在同一时间刻度下对齐,说明整条变换链路没有引入额外相移。
五、源代码文件
创建 foc.c 文件,编写如下代码:
/* USER CODE BEGIN Header */
/**
******************************************************************************
* File Name : foc_ctrl.h
* Description : foc driver base on stm32f446
******************************************************************************
* @attention
*
* COPYRIGHT: Copyright (c) 2024 tangminfei2013@126.com
* DATE: JUL 05th, 2024
******************************************************************************
*/
/* USER CODE END Header */
/* Includes ------------------------------------------------------------------*/
#include "foc_ctrl.h"
#define SQRT_3 1.7320508f
#define SQRT_3_DIV_2 0.8660254f
#define DIV_1 0.5f
FOC_T FOC;
/*****************************************************************************
Clarke变换 输入三相电流,输出alpha,bate电流
Iα = Ia
Iβ = (Ia + 2Ib) / sqrt(3)
******************************************************************************/
void clarkeTransform(Phase_T *abc, AlphaBeta_T *alphaBeta)
{
alphaBeta->alpha = abc->Ua;
alphaBeta->beta = (abc->Ua + 2 * abc->Ub) * SQRT_3;
}
/****************************************************************************
Park变换,输入电角度、Ialpha和Ibeta,经过Park变换得到Iq、Id
Id = Iα · cosθ + Iβ · sinθ
Iq = Iα · sinθ + Iβ · cosθ
*****************************************************************************/
void parkTransform(const AlphaBeta_T *alphaBeta, float angle, DQ_T *dq)
{
float sinAngle = sin(angle);
float cosAngle = cos(angle);
dq->d = cosAngle * alphaBeta->alpha + sinAngle * alphaBeta->beta;
dq->q = -sinAngle * alphaBeta->alpha + cosAngle * alphaBeta->beta;
}
/***************************************************************************
park逆变换,输入Uq、Ud得到Ualpha、Ubeta
Uα = Ud · cosθ - Uq · sinθ
Uβ = Ud · sinθ + Uq · cosθ
****************************************************************************/
void inverseParkTransform(DQ_T *dq, AlphaBeta_T *alphaBeta, float angle)
{
float cosAngle = cos(angle);
float sinAngle = sin(angle);
alphaBeta->alpha = dq->d * cosAngle - dq->q * sinAngle;
alphaBeta->beta = dq->d * sinAngle + dq->q * cosAngle;
}
/**********************************************************************************************************
Clarke逆变换,输入Ualpha、Ubeta,得到Ua,Ub,Uc
Ua = Uα
Ub = -1/2 * Uα + sqrt(3)/2 * Uβ
Ub = -1/2 * Uα - sqrt(3)/2 * Uβ
**********************************************************************************************************/
void inverseClarkeTransform(AlphaBeta_T *abVoltage, Phase_T *abc)
{
abc->Ua = abVoltage->alpha;
abc->Ub = -DIV_1 * abVoltage->alpha + SQRT_3_DIV_2 * abVoltage->beta;
abc->Uc = -DIV_1 * abVoltage->alpha - SQRT_3_DIV_2 * abVoltage->beta;
}
void SVPWM(SVPWM_T *svpwm, Phase_T *abc)
{
float sum;
float k_svpwm;
// step-1: 设置象限电压值
svpwm->Ts = 1.0f; // SVPWM的采样周期
svpwm->u1 = abc->Ua;
svpwm->u2 = abc->Ub;
svpwm->u3 = abc->Uc;
// step2:扇区判断
// 根据u1、u2和u3的正负情况确定所处的扇区
svpwm->sector = (svpwm->u1 > 0.0f) + ((svpwm->u2 > 0.0f) << 1) + ((svpwm->u3 > 0.0f) << 2); // N=4*C+2*B+A
// step3:计算基本矢量电压作用时间(占空比)
// 根据扇区的不同,计算对应的t_a、t_b和t_c的值,表示生成的三相电压的时间
switch (svpwm->sector)
{
case 5:
// 扇区5
svpwm->t4 = svpwm->u3;
svpwm->t6 = svpwm->u1;
sum = svpwm->t4 + svpwm->t6;
if (sum > svpwm->Ts)
{
k_svpwm = svpwm->Ts / sum; //
svpwm->t4 = k_svpwm * svpwm->t4;
svpwm->t6 = k_svpwm * svpwm->t6;
}
svpwm->t7 = (svpwm->Ts - svpwm->t4 - svpwm->t6) / 2;
svpwm->ta = svpwm->t4 + svpwm->t6 + svpwm->t7;
svpwm->tb = svpwm->t6 + svpwm->t7;
svpwm->tc = svpwm->t7;
break;
case 1:
// 扇区1
svpwm->t2 = -svpwm->u3;
svpwm->t6 = -svpwm->u2;
sum = svpwm->t2 + svpwm->t6;
if (sum > svpwm->Ts)
{
k_svpwm = svpwm->Ts / sum; // 计算缩放系数
svpwm->t2 = k_svpwm * svpwm->t2;
svpwm->t6 = k_svpwm * svpwm->t6;
}
svpwm->t7 = (svpwm->Ts - svpwm->t2 - svpwm->t6) / 2;
svpwm->ta = svpwm->t6 + svpwm->t7;
svpwm->tb = svpwm->t2 + svpwm->t6 + svpwm->t7;
svpwm->tc = svpwm->t7;
break;
case 3:
// 扇区3
svpwm->t2 = svpwm->u1;
svpwm->t3 = svpwm->u2;
sum = svpwm->t2 + svpwm->t3;
if (sum > svpwm->Ts)
{
k_svpwm = svpwm->Ts / sum; //
svpwm->t2 = k_svpwm * svpwm->t2;
svpwm->t3 = k_svpwm * svpwm->t3;
}
svpwm->t7 = (svpwm->Ts - svpwm->t2 - svpwm->t3) / 2;
svpwm->ta = svpwm->t7;
svpwm->tb = svpwm->t2 + svpwm->t3 + svpwm->t7;
svpwm->tc = svpwm->t3 + svpwm->t7;
break;
case 2:
// 扇区2
svpwm->t1 = -svpwm->u1;
svpwm->t3 = -svpwm->u3;
sum = svpwm->t1 + svpwm->t3;
if (sum > svpwm->Ts)
{
k_svpwm = svpwm->Ts / sum;
svpwm->t1 = k_svpwm * svpwm->t1;
svpwm->t3 = k_svpwm * svpwm->t3;
}
svpwm->t7 = (svpwm->Ts - svpwm->t1 - svpwm->t3) / 2;
svpwm->ta = svpwm->t7;
svpwm->tb = svpwm->t3 + svpwm->t7;
svpwm->tc = svpwm->t1 + svpwm->t3 + svpwm->t7;
break;
case 6:
// 扇区6
svpwm->t1 = svpwm->u2;
svpwm->t5 = svpwm->u3;
sum = svpwm->t1 + svpwm->t5;
if (sum > svpwm->Ts)
{
k_svpwm = svpwm->Ts / sum; //
svpwm->t1 = k_svpwm * svpwm->t1;
svpwm->t5 = k_svpwm * svpwm->t5;
}
svpwm->t7 = (svpwm->Ts - svpwm->t1 - svpwm->t5) / 2;
svpwm->ta = svpwm->t5 + svpwm->t7;
svpwm->tb = svpwm->t7;
svpwm->tc = svpwm->t1 + svpwm->t5 + svpwm->t7;
break;
case 4:
// 扇区4
svpwm->t4 = -svpwm->u2;
svpwm->t5 = -svpwm->u1;
sum = svpwm->t4 + svpwm->t5;
if (sum > svpwm->Ts)
{
k_svpwm = svpwm->Ts / sum; //
svpwm->t4 = k_svpwm * svpwm->t4;
svpwm->t5 = k_svpwm * svpwm->t5;
}
svpwm->t7 = (svpwm->Ts - svpwm->t4 - svpwm->t5) / 2;
svpwm->ta = svpwm->t4 + svpwm->t5 + svpwm->t7;
svpwm->tb = svpwm->t7;
svpwm->tc = svpwm->t5 + svpwm->t7;
break;
default:
break;
}
// step4:3路PWM输出
}
void foc_test(void)
{
int run_cnt = 10;
float theta = 0;
float ta,tb,tc;
DQ_T dq_t;
AlphaBeta_T alphaBeta_t;
SVPWM_T svpwm_out;
Phase_T phase_t;
dq_t.d = 0.2f;
dq_t.q = 0.0f;
while( run_cnt--)
{
for ( theta = 0; theta < 6.2831853f; theta += 0.275f )
{
// 逆Park变换
inverseParkTransform(&dq_t,&alphaBeta_t,theta);
// 逆Clark变换
inverseClarkeTransform(&alphaBeta_t, &phase_t);
// swpwm 转换
SVPWM( &svpwm_out,&phase_t );
ta = 100.0f*svpwm_out.ta;
tb = 100.0f*svpwm_out.tb;
tc = 100.0f*svpwm_out.tc;
// printf("%.4f,%.4f,%.4f,%.4f,%.4f\n", alphaBeta_t.alpha*100.0f ,
// alphaBeta_t.beta*100.0f ,ta,tb,tc);
printf("%.4f,%.4f,%.4f,%.4f,%.4f \n", alphaBeta_t.alpha,alphaBeta_t.beta,
phase_t.Ua,phase_t.Ub,phase_t.Uc );
}
}
}
/* End of this file */
创建 foc.h 文件,编写如下代码:
/* USER CODE BEGIN Header */
/**
******************************************************************************
* File Name : foc_ctrl.h
* Description : foc driver base on stm32f446
******************************************************************************
* @attention
*
* COPYRIGHT: Copyright (c) 2024 tangminfei2013@126.com
* DATE: JUL 05th, 2024
******************************************************************************
*/
/* USER CODE END Header */
/* Includes ------------------------------------------------------------------*/
#ifndef __FOC_CTRL_H
#define __FOC_CTRL_H
/*****************************************************************************/
/* Includes */
/*****************************************************************************/
#include "main.h"
#include "utils_types.h"
#ifdef _cplusplus
extern "C" {
#endif
typedef struct
{
float Ia; // Phase A current
float Ib; // Phase B current
float Ic; // Phase C current
float Ua; // Phase A Voltage
float Ub; // Phase B Voltage
float Uc; // Phase C Voltage
} Phase_T;
typedef struct
{
float alpha; // alpha-axis current
float beta; // beta-axis current
} AlphaBeta_T;
typedef struct
{
float d; // d-axis current
float q; // q-axis current
} DQ_T;
typedef struct
{
int sector;
float u1;
float u2;
float u3;
float ta;
float tb;
float tc;
float Ts;
float t0;
float t1;
float t2;
float t3;
float t4;
float t5;
float t6;
float t7;
} SVPWM_T;
typedef struct
{
float U_d;
float U_q;
float theta;
float U_alpha;
float U_bate;
Phase_T Phase_Curr;
AlphaBeta_T AlphaBeta;
DQ_T DQ;
} FOC_T;
extern FOC_T FOC;
void foc_test(void);
#ifdef _cplusplus
}
#endif
#endif /* __FOC_CTRL_H */
六、几点观察
- 代码与注释不一致:
clarkeTransform()上方的注释写的是 ,函数体却写成了(abc->Ua + 2 * abc->Ub) * SQRT_3——乘 而非除。两者相差 3 倍(),若要照抄这段代码,这一行需要按自己的标度约定核对。上面 2.1 节的矩阵是”除”的那个版本。 - Park/逆 Park 的对齐约定要与系数配套:本实现取 轴与 轴对齐( 时 、),与 2.4 节第一组矩阵一致;而 2.3 节给的矩阵是 轴与 轴对齐的版本。混用两套约定,表现是电机能转但转矩方向/力矩大小不对,不是不转——这类错误很难从现象倒推。
- SVPWM 只算到占空比:
SVPWM()的// step4:3路PWM输出是空的,ta/tb/tc也只是被foc_test()打印出来,没有写进TIMx->CCR。要在硬件上跑,最后一步得自己接上定时器——所以本文严格说是**“变换链路的开环验证”**,不是完整的电机驱动器。 - 文件头注释写的是 stm32f446:文章标题说 STM32G4,源码里的
Description却写着foc driver base on stm32f446。这份代码本身不依赖具体型号(纯浮点数学 +math.h),挪到哪个 Cortex-M4 上都能跑,但用 G4 的话可以换成 CORDIC 或 FPU 单精度指令加速。
素材来源:本文为微信公众号「STM32应用分析」2026-08-22 文章《磁场定向控制 (FOC)模型的C语言实现(STM32G4)》的整理存档,作者 mftang(原发于 CSDN,所有插图带 CSDN @mftang 水印)。原文中 5 张纯公式截图已改写为 KaTeX 排版,代码块由插图转写为可复制的文本;其余插图保留原图。原代码 Copyright (c) 2024 tangminfei2013@126.com。