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