一、导言
github 源码:
- https://github.com/simplefoc/Arduino-FOC/blob/v2.3.2/src/common/base_classes/CurrentSense.h
- https://github.com/simplefoc/Arduino-FOC/blob/v2.3.2/src/common/base_classes/CurrentSense.cpp
如果说前面分析的 Encoder、HallSensor 解决的是"转子在哪里",那么 CurrentSense 解决的是另一个更贴近力矩控制的问题:电机线圈里现在到底流过多少电流。
如果你在调 foc_current 模式时遇到电机一使能就发热、抖动,或者电流目标越给越不对劲,很多时候不要急着先调 PID。更值得优先确认的是:电流采样相序、方向、ADC 零点,以及三相电流到 id/iq 的变换链路有没有错。
先说结论:
CurrentSense的主线不是"怎么读 ADC",而是把子类读到的ia/ib/ic统一转换成电流环需要的id/iqInlineCurrentSense、LowsideCurrentSense、GenericCurrentSense负责处理硬件差异,基类负责处理通用数学变换getFOCCurrents()是foc_current模式真正使用的入口,核心就是 Clarke 变换 + Park 变换driverAlign()很重要,它检查的不是转子零点,而是电流采样通道和驱动相序/方向是否匹配- 低边采样比内联采样更依赖 PWM/ADC 同步,所以
linkDriver()的调用顺序不能随便省
FOC 里真正被控制的不是三相电流本身,而是旋转坐标系下的 d/q 电流:
- q 轴电流:主要产生转矩,通常就是力矩控制的目标量
- d 轴电流:磁链方向电流,普通表贴 BLDC/PMSM 一般希望它接近 0
CurrentSense.cpp 的核心任务,就是把底层 ADC 读到的 ia/ib/ic 三相电流,变成控制器能直接使用的 id/iq。
CurrentSense 是电流采样的抽象基类 。它并不继承 Sensor,但设计思路很像:都用一个基类把不同硬件的差异收敛到统一接口,再把通用逻辑放到基类里复用。它定义了所有电流传感器必须实现的接口,自身只保留少量和硬件无关的通用方法。
类层次结构如下:

二、CurrentSense在SimpleFOC架构中的位置
先不要急着看公式。CurrentSense 最重要的身份是:它把各种硬件电流采样方式统一成同一个上层接口。
SimpleFOC 里常见的电流采样实现有:
| 子类 | 采样方式 | 典型特点 |
|---|---|---|
| InlineCurrentSense | 相线上串采样电阻 | 采样直观,不强依赖 PWM 同步 |
| LowsideCurrentSense | 三相下桥臂低边采样 | 成本低,但必须和 PWM/ADC 时序同步 |
| GenericCurrentSense | 用户自定义回调 | 适合外部电流芯片或特殊硬件 |
这些子类负责"怎么读 ADC、怎么做硬件初始化"。而 CurrentSense 基类负责"读到三相电流以后,怎么变成 FOC 需要的电流"。

Park 变换公式 (代码直接实现的):
id=iαcosθ+iβsinθ
iq=iβcosθ−iαsinθ
iq 就是产生力矩的电流分量,PID 的闭环目标。id 通常被控制到 0(最大效率)。
三、CurrentSense.h:这个基类到底规定了什么?
先看类定义的主干:
cpp
class CurrentSense{
public:
virtual int init() = 0;
void linkDriver(BLDCDriver *driver);
bool skip_align = false;
BLDCDriver* driver = nullptr;
bool initialized = false;
void* params = 0;
virtual int driverAlign(float align_voltage) = 0;
virtual PhaseCurrent_s getPhaseCurrents() = 0;
virtual float getDCCurrent(float angle_el = 0);
DQCurrent_s getFOCCurrents(float angle_el);
};
这里可以分成三类成员看。
3.1、必须由子类实现的硬件相关接口
cpp
virtual int init() = 0;
virtual int driverAlign(float align_voltage) = 0;
virtual PhaseCurrent_s getPhaseCurrents() = 0;
它们都是纯虚函数,说明 CurrentSense 自己不能被直接实例化。
| 函数 | 谁实现 | 作用 |
|---|---|---|
| init() | 子类 | 初始化 ADC、校准零点、同步 PWM 等 |
| driverAlign() | 子类 | 检查采样相序和电流方向是否正确 |
| getPhaseCurrents() | 子类 | 返回 PhaseCurrent_s {a,b,c} |
这一层的核心思想是:硬件差异到 getPhaseCurrents() 为止。只要子类最终能给出 ia/ib/ic,后面的数学变换就可以由基类统一完成。
3.2、基类统一实现的算法接口
cpp
virtual float getDCCurrent(float angle_el = 0);
DQCurrent_s getFOCCurrents(float angle_el);
这两个函数都在 CurrentSense.cpp 中实现:
| 函数 | 返回值 | 用途 |
|---|---|---|
| getDCCurrent() | 一个电流幅值 | dc_current 力矩模式 |
| getFOCCurrents() | d/q 两个电流 | foc_current 力矩模式 |
| 注意一个细节:getFOCCurrents() 没有声明成 virtual。这意味着库作者希望所有电流采样方式共用这一套 Clarke/Park 变换逻辑,而不是让每个硬件子类各写一遍。 |
3.3、几个看似普通但很关键的变量
cpp
bool skip_align = false;
BLDCDriver* driver = nullptr;
bool initialized = false;
void* params = 0;
| 变量 | 含义 |
|---|---|
| skip_align | 是否跳过电流采样和驱动相序的自动对齐 |
| driver | 指向当前使用的 BLDCDriver |
| initialized | 电流采样是否初始化成功 |
| params | 指向平台相关硬件参数,类型用 void* 抹平差异 |
params 是一个很典型的嵌入式抽象写法。不同 MCU 的 ADC、定时器、DMA 配置结构完全不同,基类不可能知道具体类型,所以用 void* 保存,再交给 hardware_specific 层解释。
四、初始化链路:从用户代码到initFOC()
典型电流环例程里会看到这样的顺序:
cpp
motor.linkDriver(&driver);
current_sense.linkDriver(&driver);
current_sense.init();
motor.linkCurrentSense(¤t_sense);
motor.init();
motor.initFOC();
这里最容易忽略的是 linkDriver()。
cpp
void CurrentSense::linkDriver(BLDCDriver* _driver) {
driver = _driver;
}
它本身只有一行代码,但作用很重要:电流采样对象需要知道自己对应哪一个驱动器 。
特别是 LowsideCurrentSense,低边采样必须在合适的 PWM 时刻读取 ADC,否则采到的可能不是相电流,所以它的 init() 里会调用类似:
cpp
params = _configureADCLowSide(driver->params, pinA, pinB, pinC);
_driverSyncLowSide(driver->params, params);
这就是 CurrentSense 和 BLDCDriver 必须互相对齐的原因。
五、PhaseCurrent_s 和 DQCurrent_s:两种电流坐标
相关结构体定义在 foc_utils.h:
struct DQCurrent_s
{
float d;
float q;
};
struct PhaseCurrent_s
{
float a;
float b;
float c;
};
PhaseCurrent_s 是物理世界里的三相电流:
| 字段 | 含义 |
|---|---|
| a | A 相电流 |
| b | B 相电流 |
| c | C 相电流 |
DQCurrent_s 是 FOC 控制算法喜欢的旋转坐标系电流:
| 字段 | 含义 | 控制目标 |
|---|---|---|
| d | 磁链方向电流 | 通常控制到 0 |
| q | 转矩方向电流 | 控制到目标力矩电流 |
可以把整个变换理解成两次换坐标系:

这就是 FOC 中坐标变换的核心流程. 为什么要绕这一圈?因为三相正弦电流本身一直在变化,不适合直接用普通 PID 控制。经过 Park 变换后,如果电机稳定运行,理想情况下 d/q 电流会变成接近直流的量,PID 才更容易工作。
六、Clarke 变换:abc -> alpha/beta
CurrentSense.cpp 里这段分支在 getDCCurrent() 和 getFOCCurrents() 中各出现了一次:
cpp
float i_alpha, i_beta;
if(!current.c){
i_alpha = current.a;
i_beta = _1_SQRT3 * current.a + _2_SQRT3 * current.b;
}else if(!current.a){
float a = -current.c - current.b;
i_alpha = a;
i_beta = _1_SQRT3 * a + _2_SQRT3 * current.b;
}else if(!current.b){
float b = -current.a - current.c;
i_alpha = current.a;
i_beta = _1_SQRT3 * current.a + _2_SQRT3 * b;
}else{
float mid = (1.f/3) * (current.a + current.b + current.c);
float a = current.a - mid;
float b = current.b - mid;
i_alpha = a;
i_beta = _1_SQRT3 * a + _2_SQRT3 * b;
}
这段代码的物理前提是三相电流满足:ia + ib + ic = 0。所以理论上只测两相也够了,第三相可以推出来。
6.1、为什么两相采样也能工作?
假设只测了 A/B 两相,那么:
cpp
ic = -ia - ib
Clarke 变换在这份代码里使用的形式是:
cpp
i_alpha = ia;
i_beta = (ia + 2 * ib) / sqrt(3);
对应源码常量:
cpp
#define _1_SQRT3 0.57735026919f
#define _2_SQRT3 1.15470053838f
于是:
cpp
i_beta = _1_SQRT3 * ia + _2_SQRT3 * ib;
这就是源码里最常出现的那一行。
6.2、四种分支分别代表什么?
| 分支 | 源码判断 | 含义 | 处理方式 |
|---|---|---|---|
| C 相为 0 | if(!current.c) | 认为 C 相没测 | 直接用 A/B 算 alpha/beta |
| A 相为 0 | else if(!current.a) | 认为 A 相没测 | 用 a = -b - c 推回 A |
| B 相为 0 | else if(!current.b) | 认为 B 相没测 | 用 b = -a - c 推回 B |
| 三相都有 | else | A/B/C 都测到了 | 先去掉公共偏移 mid |
下面这张图把"缺一相时怎么补回来"画出来:

整体结构清晰呈现了 SimpleFOC 在 getPhaseCurrents() 之后的电流重构策略:
判断顺序是 c == 0 → a == 0 → b == 0,逐层筛掉「未测量的相」。三种分支对应硬件上常见的电流采样配置:
- 两相采样(c == 0):最常见的低成本方案,只用两个电流传感器测 A、B 相,直接当作两相用。
- A 相缺失(a == 0) :用基尔霍夫电流定律
a + b + c = 0反推a = -b - c。 - B 相缺失(b == 0) :同理反推
b = -a - c。 - 三相全测 :计算
mid = (a + b + c)/3作为公共误差(理论上三相和应为 0,实际偏差来自零点漂移),从每相中减去以提高精度。
四条分支最终都汇入 Clarke 变换,把三相 (a, b, c) 映射到两相静止坐标系 (i_alpha, i_beta),作为后续 Park 变换的输入。这个判断逻辑体现了 SimpleFOC 在实时路径里的一个工程取舍:用很少的分支适配不同硬件采样配置,把复杂度尽量收敛在 getPhaseCurrents() 之后。
这里有一个源码细节值得专门记住:SimpleFOC 用 0 作为"这一相没有测量"的标志。这很简洁,但也意味着如果某个自定义采样函数把"有效的零电流"也严格返回为 0,就可能被这段逻辑当成"没测这一相"。实际 ADC 浮点读数经过 offset 和 gain 后很少长期精确等于 0,所以通常问题不大;但写 GenericCurrentSense 回调时要知道这个约定。
6.3、三相都测到时为什么要减去mid?
源码:
cpp
float mid = (1.f/3) * (current.a + current.b + current.c);
float a = current.a - mid;
float b = current.b - mid;
理想情况下:
cpp
ia + ib + ic = 0
但真实 ADC 会有 offset、噪声和增益误差,可能测出来:
cpp
ia + ib + ic != 0
mid 可以看成三相共同带上的平均偏移。把它从每一相里减掉,相当于把三相电流重新投影回"总和为 0"的平面上。

- 第一阶段是原始测量------由于 ADC 偏置漂移、采样误差或电流传感器零点偏移,三相电流的瞬时和不为零,违反了基尔霍夫电流定律。
- 第二阶段计算中点 ------把这个非零的偏差均摊到三相上,
mid实际上就是共模偏置的估计值(common-mode offset)。 - 第三阶段是核心修正------从每一相中减去这个共模分量,相当于把三相信号重新"居中"。
- 最后修正后 的电流满足
a' + b' + c' = 0,可以安全地送入后续的 Clarke 变换(αβ → dq)进行 FOC 控制。
注意这个适用范围:
mid修正只发生在三相都测到 的else分支里。两相采样时没有第三路冗余测量值,源码会直接用ia + ib + ic = 0反推缺失相,不会再计算mid。所以它更适合理解成"三相测量时的共模偏移修正",而不是两相采样的补偿算法。
这不是复杂滤波器,而是利用三相电机的物理约束做了一次简单、便宜、实时的误差修正。
七、Park变换:alpha/beta -> d/q
getFOCCurrents() 在完成 Clarke 变换后,会继续做 Park 变换:
cpp
float ct;
float st;
_sincos(angle_el, &st, &ct);
DQCurrent_s return_current;
return_current.d = i_alpha * ct + i_beta * st;
return_current.q = i_beta * ct - i_alpha * st;
return return_current;
这里的 angle_el 是电角度,来自电机当前转子位置。可以这样理解:

Park 变换的本质这张图就讲清楚了------把静止参考系 里随电角速度旋转的交流量,通过一次坐标旋转"锁"到转子上,变成旋转参考系 里看似静止的直流量(d、q)。这里的频率由机械转速和极对数共同决定,不是固定的 50Hz 或 100Hz。
为什么要分成 d、q 两个轴? 这是 FOC(磁场定向控制)的核心思想------解耦磁场和转矩:
- d 轴对齐转子永磁体的磁链方向 :在这个轴上的电流
i_d只会增强或削弱磁场,不产生转矩。对于表贴式 PMSM,理想控制策略是i_d = 0(不浪费电流去干扰磁场)。 - q 轴垂直于 d 轴 :根据洛伦兹力
F = BIL,电流必须垂直于磁场 才能产生最大力矩。所以i_q就是真正的"油门",转矩公式简化为T ∝ i_q。
这种解耦让交流电机的控制变得像直流有刷电机一样简单------一个变量管磁场,一个变量管转矩,互不干扰。这正是 FOC 相比标量控制(V/f)的革命性优势。
公式写成矩阵就是:
cpp
id = i_alpha * cos(theta) + i_beta * sin(theta)
iq = -i_alpha * sin(theta) + i_beta * cos(theta)
源码里的q 写法:
cpp
return_current.q = i_beta * ct - i_alpha * st;
和上面的 iq 是同一个式子。
7.1、为什么电流环要控制d/q,而不是直接控制a/b/c?
三相电流在静止坐标系里是正弦量。电机一转,ia/ib/ic 就一直周期性变化。如果直接对三相电流做 PID,控制器面对的是高速变化的交流量。
Park 变换的价值是:把旋转的电流矢量放到和转子一起旋转的坐标系里观察 。在这个坐标系里,理想转矩电流会变成一个接近常值的 q,磁链方向电流会变成一个接近常值的 d。
于是电流环就可以很自然地写成:
cpp
voltage.q = PID_current_q(current_sp - current.q);
voltage.d = PID_current_d(-current.d);
也就是:
- q 轴:让实际转矩电流追上目标电流 current_sp
- d 轴:让磁链方向电流尽量回到 0
八、getFOCCurrents():FOC 电流模式真正使用的入口
完整函数结构可以简化成:
cpp
DQCurrent_s CurrentSense::getFOCCurrents(float angle_el){
PhaseCurrent_s current = getPhaseCurrents();
// Clarke: a/b/c -> alpha/beta
float i_alpha, i_beta;
...
// Park: alpha/beta -> d/q
float ct;
float st;
_sincos(angle_el, &st, &ct);
DQCurrent_s return_current;
return_current.d = i_alpha * ct + i_beta * st;
return_current.q = i_beta * ct - i_alpha * st;
return return_current;
}
它的调用点在 BLDCMotor.cpp 的电流控制分支中:
cpp
case TorqueControlType::foc_current:
if(!current_sense) return;
current = current_sense->getFOCCurrents(electrical_angle);
current.q = LPF_current_q(current.q);
current.d = LPF_current_d(current.d);
voltage.q = PID_current_q(current_sp - current.q);
voltage.d = PID_current_d(-current.d);
break;

这就是 FOC 的最内层环路------电流环(current loop),也是整个级联控制架构中速度最快、带宽最高的一环(通常运行在 5--20 kHz)。
闭环的精髓在那条从电机绕回起点的反馈路径 :电流采样 →
getFOCCurrents()→ id/iq → 重新进入误差节点。没有这条回路,PID 就只是个开环放大器,电机会失控发热或失步。
两个 PID 的分工,对应上一张图讲过的解耦原理:
- PID_current_q(转矩通道) :跟踪外部给定的
current_sp。这个目标值在级联架构中通常来自上层的速度环输出(PID_velocity的输出就是 q 轴电流目标)。q 轴电流直接决定电机产生多大转矩。 - PID_current_d(励磁通道) :目标值恒为 0(对于表贴式 PMSM)。它的作用是"抵消"------任何由于反电动势耦合、磁链扰动或测量误差导致的 d 轴电流,都会被这个 PID 拉回零位。这是 FOC 解耦能成立的必要条件。
信号流的几个关键节点:
- 第一处是误差节点 (求和点)------
current_sp − iq和0 − id是 FOC 的"标尺",PID 就是不停地缩小这两把标尺的读数。注意 q 轴误差节点有两个输入(目标 + 反馈),d 轴误差节点的"目标"是隐含的零。 - 第二处是 PID → 电压输出 ------这一步是 FOC 的一个常被忽略的细节:PID 输出的不是电流,而是电压 。因为 PMSM 的物理模型是
V = R·I + L·dI/dt + e,控制器只能直接控制电压(通过 PWM 占空比),通过电感的微分关系间接调节电流。 - 第三处是 setPhaseVoltage() ------把 d、q 轴电压通过逆 Park 变换回到 αβ,再根据
foc_modulation选择 SinePWM、SpaceVectorPWM 等调制方式生成三相 PWM,驱动 MOSFET 桥。这是控制信号离开"数学世界"进入"功率世界"的边界。
如果 foc_current 模式一开就发热或抖动,我建议优先按这条链路排查:
current_sense.linkDriver(&driver)是否在current_sense.init()之前调用。driverAlign()是否成功,返回值有没有提示相序或增益方向被修改。- 静止或轻载时
current.d是否长期偏大。 - 给一个很小的正向
current_sp时,current.q的正负是否符合预期。 - 如果使用低边采样,确认当前 MCU/驱动组合是否支持 PWM 与 ADC 同步采样。
九、getDCCurrent():只要一个电流幅值的简化模式
getDCCurrent() 和 getFOCCurrents() 前半段几乎一样:都先读三相电流,再做 Clarke 变换。
区别在最后:
cpp
return sign * _sqrt(i_alpha*i_alpha + i_beta*i_beta);
也就是说,它不返回 d/q,只返回电流矢量在 alpha/beta 平面里的长度:
cpp
I = sqrt(i_alpha^2 + i_beta^2)
如果没有提供电角度,返回值永远是正数。如果提供了电角度,它会尝试判断这个电流是正向还是反向:
cpp
if(motor_electrical_angle) {
float ct;
float st;
_sincos(motor_electrical_angle, &st, &ct);
sign = (i_beta*ct - i_alpha*st) > 0 ? 1 : -1;
}
注意这一项:
cpp
i_beta*ct - i_alpha*st
它其实就是 q 轴电流的表达式。所以源码用 q 电流的正负来决定电流幅值的符号。
这里还有一个边界细节:源码用 if(motor_electrical_angle) 判断是否进入符号计算,所以当传入的角度刚好是 0 时,这个分支不会执行,返回值仍然是正的幅值。实际闭环运行中角度长期精确等于 0 的概率很低,但读源码时最好知道这个判断方式。

它解决的是一个很实际的问题:当我们只需要"流过电机的电流有多大、是正是反"时,是否有必要做完整的 Park 变换?
完整 FOC 需要 d、q 两路解耦电流,但很多场景(电流限幅、过流保护、转矩观测、简化的力矩控制)只需要一个标量 ------电流的幅值和方向。这时一次
_sincosf+ 完整 Park 矩阵乘法就显得过于昂贵。
两条支路的几何含义:
第一条支路是幅值计算 ------√(α² + β²)。SimpleFOC 这里使用的是常见的幅值不变 Clarke 形式:对于平衡三相正弦电流,αβ 平面里的电流矢量模长可以作为相电流峰值意义下的合成电流幅值。它不依赖电角度 ------无论转子在哪个位置,只要三相电流的瞬时值确定,幅值就是确定的。这一步纯粹是平方和开方,没有三角函数。
第二条支路是符号判断 ------这是这个技巧最巧妙的地方。√ 永远输出正数,但电流可以反向流动(电机刹车、反向加速、反转)。怎么找回符号?投影到 q 轴:
cpp
i_q = -α·sin(angle_el) + β·cos(angle_el)
q 轴电流的正负 直接对应转矩的方向------i_q > 0 是正向转矩,i_q < 0 是反向转矩。所以只需要 sign(i_q),不需要 i_q 本身的精确数值。这里虽然还是要算一次 sincosf,但不需要做完整的 Park 矩阵乘法 (少了 d 轴那一行的两次乘法和加法)。最终合成 :signed_current = sign(i_q) · √(α² + β²)。
为什么不直接用 i_q 当作"电流幅值"? 因为在 q 轴上投影的电流
i_q,在 d 轴有非零分量时(例如电流没完全锁相、或者使用弱磁控制时),会小于真实的电流模长。用 √(α²+β²) 才能拿到真正的总电流大小,再用 i_q 的符号给它"贴标签"。这是幅值与方向的解耦。
十、driverAlign():为什么电流采样也要"对齐"?
CurrentSense.h 里声明:
cpp
virtual int driverAlign(float align_voltage) = 0;
这个函数的目的不是传感器零点对齐,而是检查:
- 当前 ADC 的 A/B/C 是否真的对应驱动器的 A/B/C
- 电流方向是否反了
- 必要时是否需要交换采样通道或反转增益符号
以 InlineCurrentSense 为例,它会让驱动器给某一相施加测试电压:
cpp
driver->setPwm(voltage, 0, 0);
然后读取三相电流,看哪一相响应最大、方向是否符合预期。这个过程可以理解成:

driverAlign() 返回值也带有状态含义。以源码注释为准:
| 返回值 | 含义 |
|---|---|
| 0 | 失败 |
| 1 | 成功,什么都没改 |
| 2 | 成功,但重新配置了引脚/相序 |
| 3 | 成功,但反转了增益方向 |
| 4 | 成功,同时改了相序和方向 |
如果你已经非常确定硬件接线和方向,也可以设置:
cpp
current_sense.skip_align = true;
这样 driverAlign() 会直接返回成功,不再做自动测试。调试早期不建议跳过,因为电流方向反了会让电流环表现得非常诡异:目标越大,系统越可能往错误方向补偿。
十一、InlineCurrentSense 与 LowsideCurrentSense 如何接到基类?
这一节不展开所有子类源码,只看它们和基类的接口关系。
11.1、InlineCurrentSense:读 ADC 电压,减 offset,乘 gain
cpp
PhaseCurrent_s InlineCurrentSense::getPhaseCurrents(){
PhaseCurrent_s current;
current.a = (!_isset(pinA)) ? 0 : (_readADCVoltageInline(pinA, params) - offset_ia)*gain_a;
current.b = (!_isset(pinB)) ? 0 : (_readADCVoltageInline(pinB, params) - offset_ib)*gain_b;
current.c = (!_isset(pinC)) ? 0 : (_readADCVoltageInline(pinC, params) - offset_ic)*gain_c;
return current;
}
它做了三件事:
- 读每一相 ADC 电压
- 减去零电流 offset
- 乘以 volts_to_amps_ratio,把电压换成电流
11.2、LowsideCurrentSense:多了 ADC 触发与 PWM 同步
cpp
PhaseCurrent_s LowsideCurrentSense::getPhaseCurrents(){
PhaseCurrent_s current;
_startADC3PinConversionLowSide();
current.a = (!_isset(pinA)) ? 0 : (_readADCVoltageLowSide(pinA, params) - offset_ia)*gain_a;
current.b = (!_isset(pinB)) ? 0 : (_readADCVoltageLowSide(pinB, params) - offset_ib)*gain_b;
current.c = (!_isset(pinC)) ? 0 : (_readADCVoltageLowSide(pinC, params) - offset_ic)*gain_c;
return current;
}
它和 Inline 的形式很像,但多了:
cpp
_startADC3PinConversionLowSide();
低边采样对采样时刻更敏感,因为只有某些 PWM 状态下,低边采样电阻上的电流才代表你想测的相电流。这也是为什么 LowsideCurrentSense::init() 必须要求 driver != nullptr。
11.3、继承关系图

十二、两种电流控制模式的区别:dc_current vs foc_current
BLDCMotor.cpp 里有两个电流相关的 torque controller。下面是核心节选,省略了部分限幅和补偿逻辑:
cpp
case TorqueControlType::dc_current:
current.q = current_sense->getDCCurrent(electrical_angle);
current.q = LPF_current_q(current.q);
voltage.q = PID_current_q(current_sp - current.q);
...
break;
case TorqueControlType::foc_current:
current = current_sense->getFOCCurrents(electrical_angle);
current.q = LPF_current_q(current.q);
current.d = LPF_current_d(current.d);
voltage.q = PID_current_q(current_sp - current.q);
voltage.d = PID_current_d(-current.d);
break;
它们最大的区别是:
| 模式 | 读取函数 | 控制内容 | 特点 |
|---|---|---|---|
| dc_current | getDCCurrent(angle) | 主要控制一个带符号电流幅值 | 简化,成本低 |
| foc_current | getFOCCurrents(angle) | 同时控制 q 和 d | 更完整的 FOC 电流闭环 |

如果你想真正理解 FOC 电流环,重点应该放在 foc_current,也就是 getFOCCurrents() 这条路径。
十三、这一篇可以记住的几个结论
- CurrentSense 是电流采样的抽象基类,它不直接读 ADC,而是规定子类必须提供 init()、driverAlign() 和 getPhaseCurrents()。
- 硬件差异被封装在 InlineCurrentSense、LowsideCurrentSense、GenericCurrentSense 等子类里;基类统一负责 Clarke/Park 变换。
- getPhaseCurrents() 返回的是物理三相电流 ia/ib/ic,而 getFOCCurrents() 返回的是 FOC 电流环真正使用的 id/iq。
- Clarke 变换利用了 ia + ib + ic = 0,因此两相采样也能推导出足够的信息。
- 三相都测量时,源码用 mid=(ia+ib+ic)/3 去掉公共偏移,这是利用三相电流总和为 0 的物理约束做的简单误差修正。
- Park 变换需要电角度 angle_el,它把静止坐标系下的 alpha/beta 电流转换成跟随转子旋转的 d/q 电流。
- q 轴电流主要对应转矩,d 轴电流通常希望控制到 0,这就是 PID_current_q(current_sp-current.q) 和 PID_current_d(-current.d) 的来源。
- getDCCurrent() 只返回电流矢量幅值;传入非零角度时,它通过 q 轴投影判断正负。
- driverAlign() 是为了检查电流采样通道和驱动相序/方向是否一致,电流环调不起来时,这一步比 PID 参数更值得优先排查。
- 低边采样比内联采样更依赖 PWM/ADC 同步,所以 LowsideCurrentSense 必须链接 driver 后才能初始化。
如果把前面几篇串起来看,传感器链路已经从"转子位置"走到了"电流反馈":
Encoder、HallSensor、MagneticSensorSPI解决的是电角度从哪里来CurrentSense解决的是三相电流怎么变成id/iqBLDCMotor里的电流环则把id/iq反馈重新变成voltage.d/q
你在调 SimpleFOC 电流环时,遇到过 driverAlign() 失败、id 压不住,还是 iq 符号反了?这类问题通常比速度环 PID 更难定位,欢迎把硬件采样方式和现象一起拿出来讨论。