<output id="qn6qe"></output>

    1. <output id="qn6qe"><tt id="qn6qe"></tt></output>
    2. <strike id="qn6qe"></strike>

      亚洲 日本 欧洲 欧美 视频,日韩中文字幕有码av,一本一道av中文字幕无码,国产线播放免费人成视频播放,人妻少妇偷人无码视频,日夜啪啪一区二区三区,国产尤物精品自在拍视频首页,久热这里只有精品12
      嵌入式工具集

      嵌入式工具集

      Modbus協議生成器

      --
      暫無記錄

      Modbus CRC16 計算器

      等待計算...

      32位Bit位選擇器

      0

      16進制計算器

      等待計算...

      電阻計算器

      并聯: --
      串聯: --

      字節序轉換工具

      大端: --
      小端: --

      進制/編碼轉換工具

      --
      --
      --
      --

      單片機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文庫

      這個特性是模塊化的一部分,后面還會寫別的,請關注

      posted @ 2022-05-08 20:08  大高玩子  閱讀(1592)  評論(0)    收藏  舉報
      主站蜘蛛池模板: 亚洲 中文 欧美 日韩 在线| 好吊妞人成视频在线观看27du| 亚洲免费人成在线视频观看| 亚洲成人动漫av在线| 久久精品成人免费看| 玩弄放荡人妻少妇系列| 国产自产一区二区三区视频| 蜜桃av无码免费看永久| 国产欧美另类久久久精品不卡| 国产午夜亚洲精品福利| 99热久久这里只有精品| 国产毛片子一区二区三区| 日韩V欧美V中文在线| 亚洲免费观看视频| 99久久免费精品国产色| 亚洲一区二区精品动漫| 亚洲综合天堂av网站在线观看| 激情五月天一区二区三区| 亚洲国产成人久久精品软件 | 亚洲综合久久国产一区二区| xxxx丰满少妇高潮| 亚洲人成色99999在线观看| 国产精品亚洲二区在线播放 | 天堂久久天堂av色综合| 色成人亚洲| 国产强奷在线播放免费| 日韩av综合中文字幕| 国产精品久久精品国产| 国产av一区二区三区精品| 风流少妇树林打野战视频| 亚洲avav天堂av在线网爱情| 欧洲精品亚洲精品日韩专区| 美女把尿囗扒开让男人添| 日韩理伦片一区二区三区| 亚洲精品国产美女久久久| 久热这里只国产精品视频| 国产一区二区日韩经典| 色偷偷亚洲男人的天堂| 99久久精品国产亚洲精品| 无码中文字幕人妻在线一区| 夜夜躁日日躁狠狠久久av|