在实现FOC电机算法库模块化时,我思考了如何使库的代码在各个平台上都能引入直接编译,实现平台无关性。在一段时间的考虑后,我选择了使用weak
关键字。
具体需求
众所周知,FOC的电流采样方式有多种,既可以使用三个ADC进行三电阻采样,也可以使用霍尔电流传感器在相线上进行2路采样。
如果我希望算法库与硬件平台无关,就不能在库中兼容所有硬件平台(也不可能实现),因此我决定让算法库的使用者来实现这部分代码。
学过C++的同学应该很快能想到解决方案,没错,面向对象的高级语言中有一种函数叫做虚函数。
虚函数是面向对象编程中的一个概念,通常与多态相关。在许多面向对象的编程语言中,如C++和Java,都支持虚函数的概念。
虚函数在基类中声明为虚拟的(virtual
),并在派生类中进行重写。通过使用虚函数,可以实现运行时多态性,使得程序在运行时能够动态地选择调用哪个版本的函数,而不是在编译时确定。
具体而言,当一个类中的函数被声明为虚函数时,派生类可以通过重写(覆盖)这个函数来提供特定于派生类的实现。然后,通过基类指针或引用调用这个函数时,实际上会调用相应派生类中的函数,而不是基类中的函数。这种动态的函数调用称为运行时多态。
那么在嵌入式 C 语言中,我们如何实现这样的骚操作呢?
C语言中的强符号和弱符号的区别
在C语言中,函数和初始化的全局变量(包括显式初始化为0)被认为是强符号,而未初始化的全局变量则被视为弱符号。
这些符号有一些规则,让我们来看看:
① 如果有两个同名的强符号,那就会出错,编译器会说“这个定义重复了”。
② 你可以有一个强符号和多个弱符号,但是在定义时,系统会选择强符号。
③ 当存在多个相同名字的弱符号时,链接器会选择占用内存空间最大的那个。
在编程中,我们常常碰到一种情况,叫做“符号重复定义”。如果多个目标文件中都定义了一个名为global的全局整数变量并对其进行了初始化,链接这些目标文件时就会出现符号重复定义的错误。
比如:
main.c 文件中
int strong = 1;
int main()
{
return 0;
}
led.c 文件中
int strong = 0;
int led_on()
{
return 0;
}
在 MDK 的编译器中,会产生符号重复定义的错误,因为对于 strong 这个变量符号,存在两个强者。
当然由于编译器的差异,在 MDK 中即使我们把 strong 不进行显示初始化,编译器也可以检测出符号重复定义,除非我们使用 extern 来表明这是一个外部符号,或者用 weak 修饰来声明这是一个弱函数。
extern int extnum;
int weak1;
int strong = 1;
int __attribute__((weak)) weak2 = 2;
int main()
{
return 0;
}
上面这段程序中,"weak"和"weak2"是弱符号,"strong"和"main"是强符号,而"extnum"既非强符号也非弱符号,因为它是一个外部变量的引用。
对于C语言来说,编译器默认函数和初始化了的全局变量为强符号,未初始化的全局变量为弱符号(C++并没有将未初始化的全局符号视为弱符号)。我们也可以通过GCC的"__attribute__((weak))"来定义任何一个强符号为弱符号。
注意,在 MDK 中使用 weak 可以直接使用它定义好的“__weak”即可,可以看后续的案例。
换句话说,就是我们可以定义一个符号,而该符号在链接时可以不解析,注意这里和 C++ 中的虚函数的区别。
我们用函数来做个实验
int main(void)
{
led_on();
return 0;
}
很明显,这样写连编译都无法通过。因为编译器会报错,led_on 符号没有定义。
__weak void led_on();
int main(void)
{
if (f)
f();
return 0;
}
那么,我们声明了一个函数led_on(),属性为weak,但并不定义它,这样,链接器会将此未定义的weak symbol赋值为0,也就是说led_on()并没有真正被调用,试试看,去掉if条件后,它就崩了!
FOC 中封装,用户来实现
这里大家应该突然就明白为什么我要说这个 weak 关键字了吧,没错,这里的弱函数其实也可以叫做虚函数,就是比较务虚,他就是一个占座的,有强者来的时候,就乖乖的让座了。
下面看我代码中的实际例子:
//虚函数,获取相电流,用户应自行实现
__weak curr_t get_phase_current(void)
{
#warning pls define your get_phase_volt function
curr_t c_t = {0};
return c_t;
}
这里首先定义一个弱函数符号,让编译器可以编译通过,到任何平台,用户不实现这个函数,他也可以编译通过,只是认为采样电流为 0,同时我们可以使用 warning 的预编译指令提醒用户需要自己实现。
查看编译结果如下:
当用户引入我的 FOC 算法库后,他可以直接编译通过,同时可以自己实现一下从硬件获取电流的函数,只要保证跟我的弱函数一样的符号名和返回值即可。
curr_t get_phase_current(void)
{
s32 C1, C2, temp32 = 0;
curr_t Local_Stator_Currents;
adcData[2] = HAL_ADCEx_InjectedGetValue(&adcHandle, ADC_INJECTED_RANK_4);
adcData[3] = HAL_ADCEx_InjectedGetValue(&adcHandle, ADC_INJECTED_RANK_3);
temp32 = _CRT_A_1_75MR1;
switch(m_Sector)
{
case 1: //BC相电流
case 6:
C1 = (s16)(ADC->JDOR4) - m_ADCOffsetB;
C2 = (s16)(ADC->JDOR3) - m_ADCOffsetC;
C1 = (C1*temp32)>>10;
C2 = (C2*temp32)>>10;
Local_Stator_Currents.C1 = C1+C2;
Local_Stator_Currents.C2 = -C1;
break;
case 2: //AC相电流
case 3:
C1 = (s16)(ADC->JDOR4) - m_ADCOffsetA;
C2 = (s16)(ADC->JDOR3) - m_ADCOffsetC;
C1 = (C1*temp32)>>10;
C2 = (C2*temp32)>>10;
Local_Stator_Currents.C1 = -C1;
Local_Stator_Currents.C2 = C1+C2;
break;
case 4: //AB相电流
case 5:
C1 = (s16)(ADC->JDOR4) - m_ADCOffsetA;
C2 = (s16)(ADC->JDOR3) - m_ADCOffsetB;
C1 = (C1*temp32)>>10;
C2 = (C2*temp32)>>10;
Local_Stator_Currents.C1 = -C1;
Local_Stator_Currents.C2 = -C2;
break;
default:
break;
}
Local_Stator_Currents.C1 = Local_Stator_Currents.C1;
Local_Stator_Currents.C2 = Local_Stator_Currents.C2;
return(Local_Stator_Currents);
}