SimpleFOC源码学习10(v2.3.2) - 电流传感器CurrentSense.cpp与CurrentSense.h

一、导言


github 源码:

如果说前面分析的 Encoder、HallSensor 解决的是"转子在哪里",那么 CurrentSense 解决的是另一个更贴近力矩控制的问题:电机线圈里现在到底流过多少电流

如果你在调 foc_current 模式时遇到电机一使能就发热、抖动,或者电流目标越给越不对劲,很多时候不要急着先调 PID。更值得优先确认的是:电流采样相序、方向、ADC 零点,以及三相电流到 id/iq 的变换链路有没有错。

先说结论:

  • CurrentSense 的主线不是"怎么读 ADC",而是把子类读到的 ia/ib/ic 统一转换成电流环需要的 id/iq
  • InlineCurrentSenseLowsideCurrentSenseGenericCurrentSense 负责处理硬件差异,基类负责处理通用数学变换
  • 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(&current_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 == 0a == 0b == 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 − iq0 − 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 模式一开就发热或抖动,我建议优先按这条链路排查:

  1. current_sense.linkDriver(&driver) 是否在 current_sense.init() 之前调用。
  2. driverAlign() 是否成功,返回值有没有提示相序或增益方向被修改。
  3. 静止或轻载时 current.d 是否长期偏大。
  4. 给一个很小的正向 current_sp 时,current.q 的正负是否符合预期。
  5. 如果使用低边采样,确认当前 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;

这个函数的目的不是传感器零点对齐,而是检查:

  1. 当前 ADC 的 A/B/C 是否真的对应驱动器的 A/B/C
  2. 电流方向是否反了
  3. 必要时是否需要交换采样通道或反转增益符号

以 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;
}

它做了三件事:

  1. 读每一相 ADC 电压
  2. 减去零电流 offset
  3. 乘以 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() 这条路径。

十三、这一篇可以记住的几个结论


  1. CurrentSense 是电流采样的抽象基类,它不直接读 ADC,而是规定子类必须提供 init()、driverAlign() 和 getPhaseCurrents()。
  2. 硬件差异被封装在 InlineCurrentSense、LowsideCurrentSense、GenericCurrentSense 等子类里;基类统一负责 Clarke/Park 变换。
  3. getPhaseCurrents() 返回的是物理三相电流 ia/ib/ic,而 getFOCCurrents() 返回的是 FOC 电流环真正使用的 id/iq。
  4. Clarke 变换利用了 ia + ib + ic = 0,因此两相采样也能推导出足够的信息。
  5. 三相都测量时,源码用 mid=(ia+ib+ic)/3 去掉公共偏移,这是利用三相电流总和为 0 的物理约束做的简单误差修正。
  6. Park 变换需要电角度 angle_el,它把静止坐标系下的 alpha/beta 电流转换成跟随转子旋转的 d/q 电流。
  7. q 轴电流主要对应转矩,d 轴电流通常希望控制到 0,这就是 PID_current_q(current_sp-current.q) 和 PID_current_d(-current.d) 的来源。
  8. getDCCurrent() 只返回电流矢量幅值;传入非零角度时,它通过 q 轴投影判断正负。
  9. driverAlign() 是为了检查电流采样通道和驱动相序/方向是否一致,电流环调不起来时,这一步比 PID 参数更值得优先排查。
  10. 低边采样比内联采样更依赖 PWM/ADC 同步,所以 LowsideCurrentSense 必须链接 driver 后才能初始化。

如果把前面几篇串起来看,传感器链路已经从"转子位置"走到了"电流反馈":

  • EncoderHallSensorMagneticSensorSPI 解决的是电角度从哪里来
  • CurrentSense 解决的是三相电流怎么变成 id/iq
  • BLDCMotor 里的电流环则把 id/iq 反馈重新变成 voltage.d/q

你在调 SimpleFOC 电流环时,遇到过 driverAlign() 失败、id 压不住,还是 iq 符号反了?这类问题通常比速度环 PID 更难定位,欢迎把硬件采样方式和现象一起拿出来讨论。

相关推荐
进击的小头2 小时前
第21篇:TI DSP 寄存器级开发与库函数开发对比
驱动开发·单片机·嵌入式硬件
小仙女的小稀罕2 小时前
适合销售从业者会议整理使用的销售录音转任务工具
大数据·人工智能·学习·自然语言处理·语音识别
普通网友3 小时前
HTML5新增了哪些重要标签?多多学习也是成长的一部分
前端·学习·html5
高翔·权衡之境3 小时前
差错控制——噪声中如何保真?
网络·驱动开发·嵌入式硬件·物联网·软件工程·信息与通信
南境十里·墨染春水3 小时前
linux学习进展 mysql数据库
linux·数据库·学习
深蓝海拓4 小时前
用HSL颜色系统改造qdarkstyle样式表库
前端·笔记·python·qt·学习
嵌入式小企鹅4 小时前
大模型算法工程师面试宝典
人工智能·学习·算法·面试·职场和发展·大模型·面经
-To be number.wan4 小时前
操作系统 | 关于时间片大小的确定问题
学习·操作系统
阿荻在肝了4 小时前
Agent学习八:LangGraph学习-小结
python·学习·agent