在實現(xiàn)FOC電機算法庫模塊化時,我思考了如何使庫的代碼在各個平臺上都能引入直接編譯,實現(xiàn)平臺無關(guān)性。在一段時間的考慮后,我選擇了使用weak
關(guān)鍵字。
具體需求
眾所周知,F(xiàn)OC的電流采樣方式有多種,既可以使用三個ADC進行三電阻采樣,也可以使用霍爾電流傳感器在相線上進行2路采樣。
如果我希望算法庫與硬件平臺無關(guān),就不能在庫中兼容所有硬件平臺(也不可能實現(xiàn)),因此我決定讓算法庫的使用者來實現(xiàn)這部分代碼。
學過C++的同學應(yīng)該很快能想到解決方案,沒錯,面向?qū)ο蟮母呒壵Z言中有一種函數(shù)叫做虛函數(shù)。
虛函數(shù)是面向?qū)ο缶幊讨械囊粋€概念,通常與多態(tài)相關(guān)。在許多面向?qū)ο蟮木幊陶Z言中,如C++和Java,都支持虛函數(shù)的概念。
虛函數(shù)在基類中聲明為虛擬的(virtual
),并在派生類中進行重寫。通過使用虛函數(shù),可以實現(xiàn)運行時多態(tài)性,使得程序在運行時能夠動態(tài)地選擇調(diào)用哪個版本的函數(shù),而不是在編譯時確定。
具體而言,當一個類中的函數(shù)被聲明為虛函數(shù)時,派生類可以通過重寫(覆蓋)這個函數(shù)來提供特定于派生類的實現(xiàn)。然后,通過基類指針或引用調(diào)用這個函數(shù)時,實際上會調(diào)用相應(yīng)派生類中的函數(shù),而不是基類中的函數(shù)。這種動態(tài)的函數(shù)調(diào)用稱為運行時多態(tài)。
那么在嵌入式 C 語言中,我們?nèi)绾螌崿F(xiàn)這樣的騷操作呢?
C語言中的強符號和弱符號的區(qū)別
在C語言中,函數(shù)和初始化的全局變量(包括顯式初始化為0)被認為是強符號,而未初始化的全局變量則被視為弱符號。
這些符號有一些規(guī)則,讓我們來看看:
① 如果有兩個同名的強符號,那就會出錯,編譯器會說“這個定義重復(fù)了”。
② 你可以有一個強符號和多個弱符號,但是在定義時,系統(tǒng)會選擇強符號。
③ 當存在多個相同名字的弱符號時,鏈接器會選擇占用內(nèi)存空間最大的那個。
在編程中,我們常常碰到一種情況,叫做“符號重復(fù)定義”。如果多個目標文件中都定義了一個名為global的全局整數(shù)變量并對其進行了初始化,鏈接這些目標文件時就會出現(xiàn)符號重復(fù)定義的錯誤。
比如:
main.c 文件中
int strong = 1;
int main()
{
return 0;
}
led.c 文件中
int strong = 0;
int led_on()
{
return 0;
}
在 MDK 的編譯器中,會產(chǎn)生符號重復(fù)定義的錯誤,因為對于 strong 這個變量符號,存在兩個強者。
當然由于編譯器的差異,在 MDK 中即使我們把 strong 不進行顯示初始化,編譯器也可以檢測出符號重復(fù)定義,除非我們使用 extern 來表明這是一個外部符號,或者用 weak 修飾來聲明這是一個弱函數(shù)。
extern int extnum;
int weak1;
int strong = 1;
int __attribute__((weak)) weak2 = 2;
int main()
{
return 0;
}
上面這段程序中,"weak"和"weak2"是弱符號,"strong"和"main"是強符號,而"extnum"既非強符號也非弱符號,因為它是一個外部變量的引用。
對于C語言來說,編譯器默認函數(shù)和初始化了的全局變量為強符號,未初始化的全局變量為弱符號(C++并沒有將未初始化的全局符號視為弱符號)。我們也可以通過GCC的"__attribute__((weak))"來定義任何一個強符號為弱符號。
注意,在 MDK 中使用 weak 可以直接使用它定義好的“__weak”即可,可以看后續(xù)的案例。
換句話說,就是我們可以定義一個符號,而該符號在鏈接時可以不解析,注意這里和 C++ 中的虛函數(shù)的區(qū)別。
我們用函數(shù)來做個實驗
int main(void)
{
led_on();
return 0;
}
很明顯,這樣寫連編譯都無法通過。因為編譯器會報錯,led_on 符號沒有定義。
__weak void led_on();
int main(void)
{
if (f)
f();
return 0;
}
那么,我們聲明了一個函數(shù)led_on(),屬性為weak,但并不定義它,這樣,鏈接器會將此未定義的weak symbol賦值為0,也就是說led_on()并沒有真正被調(diào)用,試試看,去掉if條件后,它就崩了!
FOC 中封裝,用戶來實現(xiàn)
這里大家應(yīng)該突然就明白為什么我要說這個 weak 關(guān)鍵字了吧,沒錯,這里的弱函數(shù)其實也可以叫做虛函數(shù),就是比較務(wù)虛,他就是一個占座的,有強者來的時候,就乖乖的讓座了。
下面看我代碼中的實際例子:
//虛函數(shù),獲取相電流,用戶應(yīng)自行實現(xiàn)
__weak curr_t get_phase_current(void)
{
#warning pls define your get_phase_volt function
curr_t c_t = {0};
return c_t;
}
這里首先定義一個弱函數(shù)符號,讓編譯器可以編譯通過,到任何平臺,用戶不實現(xiàn)這個函數(shù),他也可以編譯通過,只是認為采樣電流為 0,同時我們可以使用 warning 的預(yù)編譯指令提醒用戶需要自己實現(xiàn)。
查看編譯結(jié)果如下:
當用戶引入我的 FOC 算法庫后,他可以直接編譯通過,同時可以自己實現(xiàn)一下從硬件獲取電流的函數(shù),只要保證跟我的弱函數(shù)一樣的符號名和返回值即可。
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);
}