單片機C語言__模塊化思想__1.自動初始化
本人的分享均來自于實際設計過程中的感悟
不能保證分享成果的正確性,如有錯誤,請各路大神指出,我會虛心學習,感謝!??!
![]()
一、普遍的驅動初始化方法:
大家在編寫單片機程序的時候,不知道有沒有一種感覺,就是在寫驅動程序的時候,很多時候,每一個驅動的初始化函數,都需要通過頭文件extern導出。然后再到main.c文件中的main函數中去調用該初始化函數。如此一來,mian函數中全都是初始化函數,并且每個驅動程序都需要單獨導出初始化函數提供給mian函數調用。但是實際上mian函數其實并不關心初始化調用的是哪些函數,它只需要在程序運行時運行一遍驅動的初始化操作即可。


如圖所示的工程中,有三個文件:
dev_led.c驅動源文件
dev_led.h驅動頭文件
main.c主程序源文件
通常情況下如果想要初始化dev_led驅動,需要哪些步驟呢?下面我們來一一分解:
1、第一步:在dev_led.c文件中編寫好dev_led驅動初始化代碼。
2、第二步:在dev_led.h文件中通過extern 導出led_init函數,以便于其他文件使用。
3、第三步:在KEIL中設置dev_led.h的頭文件包含路徑,否則會找不到該頭文件。
4、第四步:在mian.c頭文件中包含dev_led.h頭文件。
5、第五步:在main函數中調用led_init函數,完成dev_led驅動的初始化。
這些操作,大部分驅動都是如此,這就引發了我的一些思考。有沒有什么辦法,不用通過頭文件的導出導入,就可以實現驅動程序的初始化嗎?如此一來,即解決了頭文件處理繁瑣的問題,也解決的驅動模塊和邏輯代碼間的耦合問題,這樣該多好啊。
二、自動初始化的原理
一頓google,百度之后,我發現早就有人想過這個問題(具體誰發明的不知),并且解決了這個問題,利用最廣泛的地方就是Linux的驅動初始化中。如果學過Linux底層原理的朋友應該知道,Linux驅動文件是通過moudle_init宏定義實現驅動的初始化和一系列的注冊操作的,module_exit宏定義實現驅動的卸載等操作。
在使用moudle_init宏加載初始化函數的時候,這個初始化函數,好像并沒有說通過頭文件導出,然后在main函數中調用,但是為什么在系統加載的過程中,這個初始化函數就被調用了呢,這里面的玄機就在moudle_init這個宏定義中,由于這篇文章篇幅有限,我們不討論Linux中是如何實現這個機制的,網上也有很多這個宏定義的解析,都寫得很詳細。我們直接看看如何在單片機環境中實現這個機制,以便于我們形成自己的程序組織架構。這里我們只討論在KEIL環境下,其他環境是否兼容,可能需要編譯器的支持,請大家自己嘗試。
根據Linux中實現的原理,其實就是通過編譯器特有的預編譯指令,使得初始化函數的指針包含在一個特定的程序段中,并且這個函數按照一定的規則排序,注意,這里函數的指針,是在預編譯階段就確定下來,要放到指定的地方去的。
三、KEIL中實現自動初始化知識點
首先我們要知道幾個預編譯指令的功能和使用場景.
1、__attribute__
我們只需要知道__attribute__ 能夠指定函數的特性即可。
參考文章:【C語言】__attribute__使用_叮咚咕嚕的博客-CSDN博客_attribute c語言
2、used
__attribute__((used))
這條指令可以使得指定的函數,在編譯時,不會被編譯器優化掉,因為,我們在使用初始化函數的時候,可能不會被任何地方顯示的調用,所以不加這個指令,可能編譯器會認為該函數沒有被使用,于是就自動優化掉了該函數,導致錯誤發生。
下面我們通過一個列子看看,是不是真的是這樣,代碼如下所示:
__attribute__((used)) void FUNC1(void)
{
}
void FUNC2(void)
{
}
int main(void)
{
while(1)
{
}
}
這段代碼中,FUNC1被添加了used修飾,而FUNC2沒有加used修飾,并且這兩個函數都沒有被任何地方顯示地調用,編譯之后,我們雙擊keil的工程,即可看到工程的map文件,參考下圖,其中就有函數的段分配情況,可以看到,FUNC1被編譯成功了,而FUNC2并沒有找到,這證明used修飾后的函數,不會被編譯器優化掉。

3、section
__attribute__ ((section ("abc")))
這條指令可以使得指定的函數,在編譯時,存放在指定名稱的段中。由于我們要隱式的調用初始化函數,所以一定要在編譯后就知道函數的位置,使用該特性就可以實現這個功能。
下面我們看看是不是真的能實現這些功能,代碼如下:
__attribute__ ((section ("abc"))) __attribute__((used)) void FUNC1(void)
{
}
void FUNC2(void)
{
}
int main(void)
{
while(1)
{
}
}
其中FUNC1被放入了代碼段abc中,我們雙擊工程查看map文件,可以找到FUNC1函數,確實就是在abc段中,由此可以知道該特性可以使得函數在編譯時放到用戶指定的段中。

四、KEIL中實現自動初始化
我們先給自動初始化一個定義:
在不用顯示的調用的情況下,可以由程序自動調用指定的初始化函數。
那么知道了上面的知識點之后,能不能實現自動初始化的功能呢。我們先做一個嘗試,
先看看程序編譯后的段信息能否按照特定的規則按順序排列。請看下面的代碼:
__attribute__ ((section ("3"))) __attribute__((used)) void F1(void)
{
}
__attribute__ ((section ("4"))) __attribute__((used)) void F2(void)
{
}
__attribute__ ((section ("2"))) __attribute__((used)) void F3(void)
{
}
__attribute__ ((section ("1"))) __attribute__((used)) void F4(void)
{
}
int main(void)
{
while(1)
{
}
}
F1,F2,F3,F4分別設置在不同的段中,他們在文件中的順序是隨機的,下面我們編譯該程序,看看map文件中是否有什么規律。
F4 0x08000287 Thumb Code 2 rxm.o(1)
F3 0x08000289 Thumb Code 2 rxm.o(2)
F1 0x0800028b Thumb Code 2 rxm.o(3)
F2 0x0800028d Thumb Code 2 rxm.o(4)
在map文件中,我們找到如上的信息,可以看出編譯器將我們的函數按照段名稱的數值大小進行了排列。
我們修改段名稱在看看:
__attribute__ ((section ("b3"))) __attribute__((used)) void F1(void)
{
}
__attribute__ ((section ("c4"))) __attribute__((used)) void F2(void)
{
}
__attribute__ ((section ("a2"))) __attribute__((used)) void F3(void)
{
}
__attribute__ ((section ("d1"))) __attribute__((used)) void F4(void)
{
}
int main(void)
{
while(1)
{
}
}
結果如下:
F3 0x08000287 Thumb Code 2 rxm.o(a2)
F1 0x08000289 Thumb Code 2 rxm.o(b3)
F2 0x0800028b Thumb Code 2 rxm.o(c4)
F4 0x0800028d Thumb Code 2 rxm.o(d1)
看樣子,編譯器是根據段的字符串名稱進行排序的,這樣的特性就可以使我們自動初始化的時候,可以輕易找到函數的地址了。
那么這樣就可以通過第一個函數的地址,每次加2就可以計算出每個初始化函數的地址嗎?
答案是不可以。
我們在函數中添加一些代碼:
__attribute__ ((section ("b3"))) __attribute__((used)) void F1(void)
{
int a=0;
a++;
}
__attribute__ ((section ("c4"))) __attribute__((used)) void F2(void)
{
int a=0;
a++;
a++;
}
__attribute__ ((section ("a2"))) __attribute__((used)) void F3(void)
{
int a=0;
a++;
a++;
a++;
}
__attribute__ ((section ("d1"))) __attribute__((used)) void F4(void)
{
int a=0;
a++;
a++;
a++;
a++;
}
int main(void)
{
while(1)
{
}
}
編譯后得到結果:
F3 0x08000287 Thumb Code 10 rxm.o(a2)
F1 0x08000291 Thumb Code 6 rxm.o(b3)
F2 0x08000297 Thumb Code 8 rxm.o(c4)
F4 0x0800029f Thumb Code 12 rxm.o(d1)
這下好了,每個函數通過只加2不能準確計算出函數的地址了。這個方法行不通的啊。
那有什么辦法能夠固定的計算出函數的地址位置呢?
我們可以通過一個函數指針類型的變量記錄函數的具體位置,函數指針的大小是固定的,那么這樣不就可以精確的計算出函數的具體位置了嗎?
下面我們再改造一下代碼:
typedef void (*init_func)(void);//?¨ò?ò???oˉêy????ààDí
void F1(void)
{
int a=0;
a++;
}
__attribute__ ((section ("b3"))) __attribute__((used)) init_func init_func_f1 = F1;
void F2(void)
{
int a=0;
a++;
a++;
}
__attribute__ ((section ("b3"))) __attribute__((used)) init_func init_func_f2 = F2;
void F3(void)
{
int a=0;
a++;
a++;
a++;
}
__attribute__ ((section ("b3"))) __attribute__((used)) init_func init_func_f3 = F3;
void F4(void)
{
int a=0;
a++;
a++;
a++;
a++;
}
__attribute__ ((section ("b3"))) __attribute__((used)) init_func init_func_f4 = F4;
int main(void)
{
while(1)
{
}
}
編譯后的結果是:
F1 0x080002a3 Thumb Code 6 rxm.o(i.F1)
F2 0x080002a9 Thumb Code 8 rxm.o(i.F2)
F3 0x080002b1 Thumb Code 10 rxm.o(i.F3)
F4 0x080002bb Thumb Code 12 rxm.o(i.F4)
init_func_f1 0x20000000 Data 4 rxm.o(b3)
init_func_f2 0x20000004 Data 4 rxm.o(b3)
init_func_f3 0x20000008 Data 4 rxm.o(b3)
init_func_f4 0x2000000c Data 4 rxm.o(b3)
這里的init_func_f1 就是函數F1的函數指針,通過init_func_f1就可以找到F1函數的位置了,并且函數init_func_f1 到 init_func_f4 函數的位置的步長都是固定的。這樣就好了。
那么最后我們如何調用這些初始化函數呢,總不能每次寫一個函數,都給它指定一個段名稱,還要自己取個名字吧,這樣太麻煩了。
可以這樣實現,通過兩個已知的段的函數指針,把所有初始化函數指針都放在這兩個函數指針中間,就可以調用這些函數了。
我們改造一下代碼:
typedef void (*init_func)(void);
#define DRV_INIT_S(func) __attribute__ ((section ("DRV.1"))) __attribute__((used)) init_func init_func_##func = func
#define DRV_INIT_E(func) __attribute__ ((section ("DRV.3"))) __attribute__((used))init_func init_func_##func = func
#define DRV_INIT(func) __attribute__ ((section ("DRV.2"))) __attribute__((used)) init_func init_func_##func = func
void DRV_INIT_S_FUNC(void){}
DRV_INIT_S(DRV_INIT_S_FUNC);
void DRV_INIT_E_FUNC(void){}
DRV_INIT_E(DRV_INIT_E_FUNC);
void F3(void)
{
int a=0;
a++;
}
DRV_INIT(F3);
void F2(void)
{
int a=0;
a++;
}
DRV_INIT(F2);
void F1(void)
{
int a=0;
a++;
}
DRV_INIT(F1);
int main(void)
{
while(1)
{
}
}
程序中DRV_INIT_S用于指定函數在DRV.1段中,DRV_INIT_E用于函數在DRV.3段中,DRV_INIT用于指定函數在DRV.2段中,由于編譯器的排序,所以通過DRV_INIT注冊的函數都會在DRV.1到DRV.2段之間,這樣的換,通過遍歷這兩個段中間的函數指針,即可調用所有初始化函數,下面是map文件結果:
init_func_DRV_INIT_S_FUNC 0x20000000 Data 4 rxm.o(DRV.1)
init_func_F3 0x20000004 Data 4 rxm.o(DRV.2)
init_func_F2 0x20000008 Data 4 rxm.o(DRV.2)
init_func_F1 0x2000000c Data 4 rxm.o(DRV.2)
init_func_DRV_INIT_E_FUNC 0x20000010 Data 4 rxm.o(DRV.3)
從結果來看,函數指針的排序順序和在代碼文件中的順序相同,這樣初始化順序也可以控制了。
那么如何調用這些初始化函數呢,再改造一下代碼:
typedef void (*init_func)(void);
#define DRV_INIT_S(func) __attribute__ ((section ("DRV.1"))) __attribute__((used)) init_func init_func_##func = func
#define DRV_INIT_E(func) __attribute__ ((section ("DRV.3"))) __attribute__((used))init_func init_func_##func = func
#define DRV_INIT(func) __attribute__ ((section ("DRV.2"))) __attribute__((used)) init_func init_func_##func = func
void DRV_INIT_S_FUNC(void){}
DRV_INIT_S(DRV_INIT_S_FUNC);
void DRV_INIT_E_FUNC(void){}
DRV_INIT_E(DRV_INIT_E_FUNC);
void F3(void)
{
int a=0;
a++;
}
DRV_INIT(F3);
void F2(void)
{
int a=0;
a++;
}
DRV_INIT(F2);
void F1(void)
{
int a=0;
a++;
}
DRV_INIT(F1);
void DO_DRV_INIT(void)
{
init_func* p=0;
for(p=&init_func_DRV_INIT_S_FUNC;p<=&init_func_DRV_INIT_E_FUNC;p++)
{
(*p)();
}
}
int main(void)
{
DO_DRV_INIT();
while(1)
{
}
}
由于宏定義已經為我們定義了函數指針init_func_DRV_INIT_S_FUNC 和 init_func_DRV_INIT_E_FUNC ,通過這兩個函數指針的地址,就可以計算出所有初始化函數的地址,再調用這些函數即可實現自動初始化函數的調用。
最后,很多人可能會問這樣做之后,函數的調用順序,會不會亂掉,我們再分析的時候,會不會找不到函數的初始化順序?
其實不用擔心,看看下面的工程:

其中,dev_test2.c和dev_test1.c是模擬的驅動文件,drv.h是自動初始化框架的公共頭文件。
代碼如下:
dev_test2.c
#include "drv.h"
void F4()
{
}
DRV_INIT(F4);
void F3()
{
}
DRV_INIT(F3);
dev_test1.c
#include "drv.h"
void F2()
{
}
DRV_INIT(F2);
void F1()
{
}
DRV_INIT(F1);
編譯后結果如下:
init_func_DRV_INIT_S_FUNC 0x20000000 Data 4 rxm.o(DRV.1)
init_func_F4 0x20000004 Data 4 dev_test2.o(DRV.2)
init_func_F3 0x20000008 Data 4 dev_test2.o(DRV.2)
init_func_F2 0x2000000c Data 4 dev_test1.o(DRV.2)
init_func_F1 0x20000010 Data 4 dev_test1.o(DRV.2)
init_func_DRV_INIT_E_FUNC 0x20000014 Data 4 rxm.o(DRV.3)
可以看出,初始化函數的順序是先按照文件的排序,再按照文件中代碼的順序排序的。
那么我們只要調整一下文件的順序:

那它的順序就是:
init_func_DRV_INIT_S_FUNC 0x20000000 Data 4 rxm.o(DRV.1)
init_func_F2 0x20000004 Data 4 dev_test1.o(DRV.2)
init_func_F1 0x20000008 Data 4 dev_test1.o(DRV.2)
init_func_F4 0x2000000c Data 4 dev_test2.o(DRV.2)
init_func_F3 0x20000010 Data 4 dev_test2.o(DRV.2)
init_func_DRV_INIT_E_FUNC 0x20000014 Data 4 rxm.o(DRV.3)
這樣的話,其實我們的自動初始化函數的初始化順序,一眼就能看出來了,還是非常方便的。
經過實驗之后,可以得出結論,KEIL中自動初始化的順序具體如下:
1、先按分組排序
2、再按文件排序
3、再按代碼排序
四、總結
通過研究之后,我們可以使用編譯器給出的特性,實現自動初始化的功能。
它的優點如下:

1、可以幫我們省去驅動頭文件管理的麻煩。
2、可以幫助我們實現程序的模塊化關聯,比如上圖中刪除dev_test2.c,其實對整個工程沒有影響.不需要修改main函數代碼。
3、初始化順序結構清晰,很容易實現初始化順序的修改。
4、使程序結果清晰明了,并強制使我們的代碼結構規范,易懂易維護。
它的缺點是:
1、程序結構復雜化了。
2、使用它需要一定學習成本。
3、需要更改代碼書寫習慣。
附上我測試使用的工程:單片機C語言騷操作__模塊化思想__1.自動初始化-Linux文檔類資源-CSDN文庫
這個特性是模塊化的一部分,后面還會寫別的,請關注

浙公網安備 33010602011771號