FOC 电流采样

采样点

在基尔霍夫定律下,三相电流的合应该等于 0,因此只需要获取亮相电流就可以重构出完整的三相电流。

对于 STM32 一般通过高级定时器的 channel4 作为 ADC 的触发源,对于下桥臂电流采样,需要在下桥臂 MOS 管导筒的时候才能去采样,而且需要在 MOS 管导通时间以及电流稳定后才能采样到比较可靠的电流。

  • \(\Delta_1\) 大于两倍的 $T_{dead} + T_{on} + T_{ring} + T_{conv} $ 那么在 PWM Freq Middle 时刻出发 ADC 转换也是足够转换 3 相中任意的一相。
  • \(\Delta_1\) 大于$T_{dead} + T_{on} + T_{ring} + T_{conv} 那么从 CCRmax 之后开始转换也是可以转换 3 相中任意的一相。
  • \(\Delta_2\) > \(\Delta_0\) > \(\Delta_1\) 那么导通最短的那一相就没有足够的转换时间了,那么只能转换导通时间长的那两相。
  • \(\Delta_1\) < \(\Delta_0\)\(\Delta_2\) < \(\Delta_0\) 没有足够的转换时间,有两相电流不可获取将无法重建三相电流。

因此需要根据 u、v、w 三相 pwm 的占空比来调整 timer 的 channel 4 比较值来调整触发时刻完成三相电流的重建工作。

相电流重构

  • 对于 Sector4 和 5,C 相导通时间最长,相应的就是下桥臂导通时间最短,这个时候应该采集 A 和 B 相电流。
  • 对于 Sector2 和 3,B 相导通时间最长,相应的就是下桥臂导通时间最短,这个时候应该采集 A 和 C 相电流。
  • 对于 Sector1 和 6,A 相导通时间最长,相应的就是下桥臂导通时间最短,这个时候应该采集 B 和 C 相电流。

常用芯片

MP6540 电流采样

MP6540H 的内部电流采样电路。其输出可通过外部电阻器 (RTERM) 和参考电压 (VREF) 来设置。两个等值电阻连接到 ADC 电源和地,接地用于终止输出。

MP6540 的电流采样计算比较简单,这里就不再详细说明了。

DRV8301/DRV8302 电流采样

首先将 DRV8301 的框图列出来,大致了解下结构,主要就是通过 SNx 和 SPx 四个引脚之间的电阻来采样电流。

如果我们需要采样一个桥臂的电流,那传统的方式自然是直接在电路上串联一个采样电阻,然后采样两端电压即可。由于电阻是串联进电路的,所以阻值当然是要越低越好。但是这又会带来另一个问题,低阻值导致的采样精度不高,因此需要运放来实现电流的采样。

我们以 odriver 原理图为例进行计算,如下:

这里 R27 和 R28 电阻就是采样电阻,\(500u\Omega(500 * 10^{-6})\Omega\).

下面是官方手册给出的电流计算方式。

这里做如下说明: \(SN_x - SN_x\) 是采样电阻两端的电压值,也是我们需要测量的值,得到该值后除以电阻就得到对应的电流值。Vo 是我们 ADC 得到的值。针对官方给出的公式,我们做如下计算:

\[ V_o = \frac{V_{REF}}{2} - G \times (SN_x - SP_x) \]

化简如下:

\[ (SN_x - SP_x) = \frac{(\frac{V_{REF}}{2} - V_o )}{G} \]

这里 G 是 10、20、40、80. \(V_{REF}\)一般是 3.3v,那么如下:

\[ (\frac{V_{REF}}{2} - V_o ) = (\frac{3.3}{2} - \frac{ADC}{2^{12}} \times 3.3 ) = \frac{2^{11} - ADC}{2^{12}} \times 3.3 \]

得到电流值如下:

\[ (SN_x - SP_x) = I_{measure} \times R_{Isensor} = \frac{2^{11} - ADC}{2^{12}} \times \frac{3.3}{G} \]

得到最终结果为:

\[ I_{measure} = \frac{2^{11} - ADC}{2^{12}} \times \frac{3.3}{G} \times \frac{1}{R_{Isensor}} \]

死区时间的计算

我们用下列公式计算控制死区时间:

\[ t_{dead} = [(td_{offmax} - td_{onmin}) + (tpdd_{max} - tpdd_{min})] * 1.2 \]

  • \(td_{offmax}\) :MOS 管最大关断延迟时间。
  • \(td_{onmin}\):MOS 管最小开通延迟时间。
  • \(tpdd_{max}\):驱动器最大传输延迟时间。
  • \(tpdd_{min}\):驱动器最小传输延迟时间。

在该公式中,第一项\(td_{offmax}-td_{onmin}\)为最大关断延迟时间和最小开通延迟时间之差。这一项主要描述 MOS 管器件结合所用的门极电阻的特性。由于上升和下降时间通常比延迟时间短很多,这里就不考虑它们。另一项\(tpdd_{max} - tpdd_{min}\)为由驱动器决定的传输延迟时间之差(延迟时间不匹配)。该参数通常可在驱动器制造商提供的驱动器数据表中查找到。对于基于光耦合器的驱动器,该参数值通常很大。

对于 csd18540q:

\(td_{offmax}\) = 20ns \(td_{onmin}\) = 6ns

STM32 实现双 ADC 采样三电阻电流

重构采样点

STM32 的高级定时器有 6 个 pwm channel,我们可以使用前三个 channel 输出互补的 pwm,第四个 channel 用于触发 adc 的采样时刻。这里主要是根据扇区以及三通道的 pwm 占空比来判断电流采样点时刻。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/**
* @brief Configure the ADC for the current sampling related to sector 1.
* It means set the sampling point via TIMx_Ch4 value and polarity
* ADC sequence length and channels.
* And call the WriteTIMRegisters method.
* @retval none
*/
uint16_t R3_2_SetADCSampPointSectX(void)
{
uint16_t sampling_point;
uint16_t DeltaDuty;

/* 通过检查最小占空比验证是否可以在 PWM 中间进行采样 */
if ((uint16_t)(kMCLowLevelInstance.half_pwm_period - kMCLowLevelInstance.low_duty) > kMCLowLevelInstance.t_after) {
/* 当可以在 PWM 周期的中间进行采样时,始终对所有扇区的相同相位(选择 AB)进行采样,以便在偏移量之间存在差异时不会引起电流不连续 */

/* GetPhaseCurrent 需要的扇区号,采样 A 相和 B 相对应扇区 4 或 5 */
kMCLowLevelInstance.sector = SECTOR_5;

/* 在 PWM 周期中间设置采样点触发器 */
sampling_point = kMCLowLevelInstance.half_pwm_period - (uint16_t)1;
} else {
/* 在这种情况下,有必要转换具有最大和可变互补占空比的相位。*/
DeltaDuty = (uint16_t)(kMCLowLevelInstance.low_duty - kMCLowLevelInstance.mid_duty);

/* Definition of crossing point */
if (DeltaDuty > (uint16_t)(kMCLowLevelInstance.half_pwm_period - kMCLowLevelInstance.low_duty) * 2u) {
sampling_point = kMCLowLevelInstance.low_duty - kMCLowLevelInstance.t_before;
} else {
sampling_point = kMCLowLevelInstance.low_duty + kMCLowLevelInstance.t_after;

if (sampling_point >= kMCLowLevelInstance.half_pwm_period) {
/* ADC trigger edge must be changed from positive to negative */
kMCLowLevelInstance.adc_external_polarity_injected = (uint16_t)LL_ADC_INJ_TRIG_EXT_FALLING;
sampling_point = (2u * kMCLowLevelInstance.half_pwm_period) - sampling_point - (uint16_t)1;
}
}
}
return R3_2_WriteTIMRegisters(sampling_point);
}

重构电流采样

我们可以根据当前扇区以及三通道的 pwm 占空比来选择采样哪两相电流,通常不采样占空比最小的那一通道,而通过基尔霍夫定律来算第三通道的电流,避免采样区间太小造成的采样电流不稳定。具体算法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
/**
* @brief It computes and return latest converted motor phase currents motor
* @param pHdl: handler of the current instance of the PWM component
* @retval Ia and Ib current in Curr_Components format
*/
void R3_2_PhaseCurrentsUpdate(void)
{
int16_t i_a,i_b;
int32_t aux;
uint32_t adc_data_reg1, adc_data_reg2;

uint8_t sector = (uint8_t)kMCLowLevelInstance.sector;
TIM_TypeDef* TIMx = kMCLowLevelInstance.TIMx;
adc_data_reg1 = *kMCLowLevelInstance.adc_data_reg1[sector];
adc_data_reg2 = *kMCLowLevelInstance.adc_data_reg2[sector];

/* disable ADC trigger source */
// LL_TIM_CC_DisableChannel(TIMx, LL_TIM_CHANNEL_CH4);
LL_TIM_SetTriggerOutput(TIMx, TIM_TRGO_RESET);

switch (sector) {
case SECTOR_4:
case SECTOR_5:
/* Current on Phase C is not accessible */
/* Ia = phase_a_offset - ADC converted value) */
aux = (int32_t)(kMCLowLevelInstance.phase_a_offset) - (int32_t)(adc_data_reg1);

/* Saturation of Ia */
if (aux < -INT16_MAX) {
i_a = -INT16_MAX;
} else if (aux > INT16_MAX) {
i_a = INT16_MAX;
} else {
i_a = (int16_t)aux;
}

/* Ib = phase_b_offset - ADC converted value) */
aux = (int32_t)(kMCLowLevelInstance.phase_b_offset) - (int32_t)(adc_data_reg2);

/* Saturation of Ib */
if (aux < -INT16_MAX) {
i_b = -INT16_MAX;
} else if (aux > INT16_MAX) {
i_b = INT16_MAX;
} else {
i_b = (int16_t)aux;
}
break;

case SECTOR_6:
case SECTOR_1:
/* Current on Phase A is not accessible */
/* Ib = phase_b_offset - ADC converted value) */
aux = (int32_t)(kMCLowLevelInstance.phase_b_offset) - (int32_t)(adc_data_reg1);

/* Saturation of Ib */
if (aux < -INT16_MAX) {
i_b = -INT16_MAX;
} else if (aux > INT16_MAX) {
i_b = INT16_MAX;
} else {
i_b = (int16_t)aux;
}

/* Ia = -Ic -Ib */
aux = (int32_t)(adc_data_reg2) - (int32_t)(kMCLowLevelInstance.phase_c_offset); /* -Ic */
aux -= (int32_t)i_b; /* Ia */

/* Saturation of Ia */
if (aux > INT16_MAX) {
i_a = INT16_MAX;
} else if (aux < -INT16_MAX) {
i_a = -INT16_MAX;
} else {
i_a = (int16_t)aux;
}
break;

case SECTOR_2:
case SECTOR_3:
/* Current on Phase B is not accessible */
/* Ia = phase_a_offset - ADC converted value) */
aux = (int32_t)(kMCLowLevelInstance.phase_a_offset) - (int32_t)(adc_data_reg1);

/* Saturation of Ia */
if (aux < -INT16_MAX) {
i_a = -INT16_MAX;
} else if (aux > INT16_MAX) {
i_a = INT16_MAX;
} else {
i_a = (int16_t)aux;
}

/* Ib = -Ic -Ia */
aux = (int32_t)(adc_data_reg2) - (int32_t)(kMCLowLevelInstance.phase_c_offset); /* -Ic */
aux -= (int32_t)i_a; /* Ib */

/* Saturation of Ib */
if (aux > INT16_MAX) {
i_b = INT16_MAX;
} else if (aux < -INT16_MAX) {
i_b = -INT16_MAX;
} else {
i_b = (int16_t)aux;
}
break;

default:
break;
}
kMCLowLevelInstance.current_a = i_a;
kMCLowLevelInstance.current_b = i_b;
}

...
static MotorControlLowLevel_t kMCLowLevelInstance = {
.adc_config1 = {MC_ADC_CHANNEL_2 << ADC_JSQR_JSQ1_Pos | (LL_ADC_INJ_TRIG_EXT_TIM1_TRGO & ~ADC_INJ_TRIG_EXT_EDGE_DEFAULT),
MC_ADC_CHANNEL_1 << ADC_JSQR_JSQ1_Pos | (LL_ADC_INJ_TRIG_EXT_TIM1_TRGO & ~ADC_INJ_TRIG_EXT_EDGE_DEFAULT),
MC_ADC_CHANNEL_2 << ADC_JSQR_JSQ1_Pos | (LL_ADC_INJ_TRIG_EXT_TIM1_TRGO & ~ADC_INJ_TRIG_EXT_EDGE_DEFAULT)},
.adc_config2 = {MC_ADC_CHANNEL_12 << ADC_JSQR_JSQ1_Pos | (LL_ADC_INJ_TRIG_EXT_TIM1_TRGO & ~ADC_INJ_TRIG_EXT_EDGE_DEFAULT),
MC_ADC_CHANNEL_12 << ADC_JSQR_JSQ1_Pos | (LL_ADC_INJ_TRIG_EXT_TIM1_TRGO & ~ADC_INJ_TRIG_EXT_EDGE_DEFAULT),
MC_ADC_CHANNEL_2 << ADC_JSQR_JSQ1_Pos | (LL_ADC_INJ_TRIG_EXT_TIM1_TRGO & ~ADC_INJ_TRIG_EXT_EDGE_DEFAULT),
MC_ADC_CHANNEL_12 << ADC_JSQR_JSQ1_Pos | (LL_ADC_INJ_TRIG_EXT_TIM1_TRGO & ~ADC_INJ_TRIG_EXT_EDGE_DEFAULT)},
...
};

注意这里 sector 1 的采样通道是 ADC1 的 MC_ADC_CHANNEL_2 和 ADC2 的 MC_ADC_CHANNEL_12,sector2 的采样通道是 ADC1 的 MC_ADC_CHANNEL_1 和 ADC2 的 MC_ADC_CHANNEL_12。这个表格是根据前面的理论部分以及原理图来计算得到的通道。

在得到原始的 ADC 值之后可以通过电路原理将 ADC 转换为对应的电流值,这个就跟具体的设计有关了,这里不过多赘述。

参考文献

https://www.monolithicpower.cn/cn/designing-a-brushless-motor-driver-circuit-with-the-mp6540
https://www.ti.com/lit/ds/symlink/drv8301.pdf?ts=1664978601316&ref_url=https%253A%252F%252Fwww.ti.com%252Fproduct%252FDRV8301