我們知道,在寫裸機程序時,當我們完成硬件初始化后,就需要在主函數中進行調用。當我們使用RT-Thread后,完全不需要這樣做了,我們可以將硬件等自動初始化。RT-Thread自動初始化機制是指初始化函數不需要被顯式調用,只需要在函數定義處通過宏定義的方式進行申明,就會在系統啟動過程中被執行,非常的方便。
1 普通初始化
前面也講了,我們在寫單片機的程序時,需要對硬件進行初始化操作,我們這里還是以LED為例。需要對LED的GPIO進行初始化后才能進一步操作。
int main(void)
{
rt_err_t rst;
/* LED初始化 */
LED_GPIO_Config();
rst = rt_thread_init(&led_thread,
"ledshine",
led_thread_entry,
RT_NULL,
&led_thread_stack[0],
sizeof(led_thread_stack),
RT_THREAD_PRIORITY_MAX-2,
20);
if(rst == RT_EOK)
{
rt_thread_startup(&led_thread);
}
}
上述代碼很簡單,就是在main()函數中對LED的GPIO進行初始化,也就是調用了LED_GPIO_Config() 函數,而針對RT-Thread系統,我們在需要初始化的地方進行初始化即可,無需在main()函數或者board.c中初始化了。
2 RT-Thread初始化流程
要想搞清楚RT-Thread的自動初始化流程,那么必須的了解RT-Thread初始化流程,這一部分前文也就有講,官方也有,我們還是再來復習下。
RT-Thread支持多種平臺和多種編譯器。RT-Thread啟動代碼統一入口為 rtthread_startup(),芯片啟動文件在完成必要工作(如初始化時鐘、配置中斷向量表、初始化堆棧等)后,跳轉至 RT-Thread的啟動入口中,最后進入用戶入口 main()。RT-Thread的啟動流程如下:
1.全局關中斷,初始化與系統相關的硬件。
2.打印系統版本信息,初始化系統內核對象(如定時器、調度器)。
3.初始化用戶 main線程(同時會初始化線程棧),在 main線程中對各類模塊依次進行初始化。
4.初始化軟件定時器線程、初始化空閑線程。
5.啟動調度器,系統切換到第一個線程開始運行(如 main線程),并打開全局中斷。
在圖中標出顏色的部分需要用戶特別注意(黃色表示 libcpu移植相關的內容,綠色部分表示板級移植相關的內容)。
3 RT-Thread自動初始化原理
既然是初始化,我們這里中的重點關注初始化過程,重新整理RT-Thread初始化如下圖所示:
在系統啟動流程圖中,有兩個函數:rt_components_board_init() 與 rt_components_init(),其后的帶底色方框內部的函數表示被自動初始化的函數,其中:
“board init functions”為所有通過 INIT_BOARD_EXPORT(fn)申明的初始化函數。
“pre-initialization functions”為所有通過 INIT_PREV_EXPORT(fn)申明的初始化函數。
“device init functions”為所有通過 INIT_DEVICE_EXPORT(fn)申明的初始化函數。
“components init functions”為所有通過 INIT_COMPONENT_EXPORT(fn)申明的初始化函數。
“enviroment init functions”為所有通過 INIT_ENV_EXPORT(fn)申明的初始化函數。
“application init functions”為所有通過 INIT_APP_EXPORT(fn)申明的初始化函數。
rt_components_board_init() 函數執行的比較早,主要初始化相關硬件環境,執行這個函數時將會遍歷通過 INIT_BOARD_EXPORT(fn)申明的初始化函數表,并調用各個函數。
rt_components_init() 函數會在操作系統運行起來之后創建的 main線程里被調用執行,這個時候硬件環境和操作系統已經初始化完成,可以執行應用相關代碼。rt_components_init() 函數會遍歷通過剩下的其他幾個宏申明的初始化函數表。
RT-Thread的自動初始化機制使用了自定義 RTI符號段,將需要在啟動時進行初始化的函數指針放到了該段中,形成一張初始化函數表,在系統啟動過程中會遍歷該表,并調用表中的函數,達到自動初始化的目的。
自動初始化功能的宏接口定義詳細描述如下表所示:
初始化順序 | 宏接口 | 描述 |
1 | INIT_BOARD_EXPORT(fn) | 非常早期的初始化,此時調度器還未啟動 |
2 | INIT_PREV_EXPORT(fn) | 主要是用于純軟件的初始化、沒有太多依賴的函數 |
3 | INIT_DEVICE_EXPORT(fn) | 外設驅動初始化相關,比如網卡設備 |
4 | INIT_COMPONENT_EXPORT(fn) | 組件初始化,比如文件系統或者 LWIP |
5 | INIT_ENV_EXPORT(fn) | 系統環境初始化,比如掛載文件系統 |
6 | INIT_APP_EXPORT(fn) | 應用初始化,比如 GUI應用 |
初始化函數主動通過這些宏接口進行申明,如 INIT_BOARD_EXPORT(rt_hw_usart_init),鏈接器會自動收集所有被申明的初始化函數,放到 RTI符號段中,該符號段位于內存分布的 RO段中,該 RTI符號段中的所有函數在系統初始化時會被自動調用。
好了,介紹性文字我就不貼了,下面直接看源代碼進一步分析。前文說過,在RT-Thread的啟動流程中,調用了兩個函數 rt_components_board_init()與 rt_components_init()就完成了上述6個部分的初始化工作。從初始化啟動流程圖中我們可以看出: rt_components_board_init()完成了第 1部分工作, rt_components_init()完成了第2-6部分的工作。那么接下來我們先看這兩個函數源代碼。
/**
* RT-Thread Components Initialization for board
*/
void rt_components_board_init(void)
{
#if RT_DEBUG_INIT
int result;
const struct rt_init_desc *desc;
for (desc = &__rt_init_desc_rti_board_start; desc < &__rt_init_desc_rti_board_end; desc ++)
{
rt_kprintf("initialize %s", desc->fn_name);
result = desc->fn();
rt_kprintf(":%d done\n", result);
}
#else
const init_fn_t *fn_ptr;
for (fn_ptr = &__rt_init_rti_board_start; fn_ptr < &__rt_init_rti_board_end; fn_ptr++)
{
(*fn_ptr)();
}
#endif
}
/**
* RT-Thread Components Initialization
*/
void rt_components_init(void)
{
#if RT_DEBUG_INIT
int result;
const struct rt_init_desc *desc;
rt_kprintf("do components initialization.\n");
for (desc = &__rt_init_desc_rti_board_end; desc < &__rt_init_desc_rti_end; desc ++)
{
rt_kprintf("initialize %s", desc->fn_name);
result = desc->fn();
rt_kprintf(":%d done\n", result);
}
#else
const init_fn_t *fn_ptr;
for (fn_ptr = &__rt_init_rti_board_end; fn_ptr < &__rt_init_rti_end; fn_ptr ++)
{
(*fn_ptr)();
}
#endif
}
【注】rt_components_board_init() 與 rt_components_init()函數在components.c文件中實現。
可以看到兩個函數在非調試模式下都是通過for循環會遍歷位于__rt_init_rti_board_start 到 __rt_init_rti_board_end以及__rt_init_rti_board_end 到 __rt_init_rti_end之間保存的函數指針,然后依次執行這些函數。那么接下來我們看看上述函數指針。我們先編譯下,找到.map文件,我們在在文件中搜索上述的函數指針。
又問會問,找到又能怎樣呢?怎么和上述的6個宏定義對應起來呢?不急哈,慢慢來,我們先找到上述6個宏定義又是如何實現的,找到啦,在rtdef.h中。
宏定義又是通過INIT_EXPORT宏函數定義的,那么INIT_EXPORT又是如何實現的呢?
#ifdef _MSC_VER /* we do not support MS VC++ compiler */
#define INIT_EXPORT(fn, level)
#else
#if RT_DEBUG_INIT
struct rt_init_desc
{
const char* fn_name;
const init_fn_t fn;
};
#define INIT_EXPORT(fn, level) \
const char __rti_##fn##_name[] = #fn; \
RT_USED const struct rt_init_desc __rt_init_desc_##fn SECTION(".rti_fn."level) = \
{ __rti_##fn##_name, fn};
#else
#define INIT_EXPORT(fn, level) \
RT_USED const init_fn_t __rt_init_##fn SECTION(".rti_fn."level) = fn
#endif
#endif
【注】上述代碼在rtdef.h中實現。
上述代碼最關鍵的代碼是:
RT_USED const init_fn_t __rt_init_##fn SECTION(".rti_fn."level) = fn
這又是啥,看不懂啊,不急哈,我們先看看init_fn_t是啥?其定義如下:
#ifdef RT_USING_COMPONENTS_INIT
typedef int (*init_fn_t)(void);
這就是一個函數指針類型,其實就是個指針。那么SECTION又是啥呢?其定義如下:
__attribute__((used))表示這個標記這個東西是使用過的,避免出現如: warning: #177-D: variable "a" was declared but never referenced的警告。
在GCC的宏中,##后面跟變量名。__attribute__((section(x)))則表示fn被放置于指定段中。是不是還是一頭霧水,不急,我們再看看一些代碼或許你就明白了。
/*
* Components Initialization will initialize some driver and components as following
* order:
* rti_start --> 0
* BOARD_EXPORT --> 1
* rti_board_end --> 1.end
*
* DEVICE_EXPORT --> 2
* COMPONENT_EXPORT --> 3
* FS_EXPORT --> 4
* ENV_EXPORT --> 5
* APP_EXPORT --> 6
*
* rti_end --> 6.end
*
* These automatically initialization, the driver or component initial function must
* be defined with:
* INIT_BOARD_EXPORT(fn);
* INIT_DEVICE_EXPORT(fn);
* ...
* INIT_APP_EXPORT(fn);
* etc.
*/
static int rti_start(void)
{
return 0;
}
INIT_EXPORT(rti_start, "0");
static int rti_board_start(void)
{
return 0;
}
INIT_EXPORT(rti_board_start, "0.end");
static int rti_board_end(void)
{
return 0;
}
INIT_EXPORT(rti_board_end, "1.end");
static int rti_end(void)
{
return 0;
}
INIT_EXPORT(rti_end, "6.end");
【注】以上代碼再在components.c文件中實現。
好了,我們結合上述代碼和6個宏定義,以及.map文件。
上圖中框選的第一行是一個Section,叫做.rti_fn.0,這個內容實際是我們通過INIT_EXPORT(rti_start, "0");完成的,我們把函數rti_start改名為__rt_init_rti_start,存入.rti_fn.0這個地方。同樣的,INIT_EXPORT(rti_board_start, "0.end");、INIT_EXPORT(rti_board_end, "1.end");、INIT_EXPORT(rti_end, "6.end");也是這里插入的。下圖就是我們插入的位置。
這下是不是很明白,當然啦,多看幾遍,還是很好理解的。
4 RT-Thread自動初始化實例
前文理論講了很多,源代碼也分析,估計初學者還很蒙,沒關系,慢慢理解,我們先看個例子,先用起來,后面再慢慢理解。還會是LED的例子,前文已近給出了普通初始化方式,下面我們看看如何使用自動化初始化。
/**
* @brief 初始化LED的GPIO
* @param None
* @retval None
*/
int LED_GPIO_Config(void)
{
/*定義一個GPIO_InitTypeDef類型的結構體*/
GPIO_InitTypeDef GPIO_InitStructure;
/*開啟LED的外設時鐘*/
RCC_APB2PeriphClockCmd( RCC_APB2Periph_GPIOB|RCC_APB2Periph_GPIOG, ENABLE);
/*設置IO口*/
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //設置引腳模式為通用推挽輸出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; //設置引腳速率為50MHz
/*調用庫函數,初始化GPIOB0*/
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; //選擇要控制的GPIOB引腳
GPIO_Init(GPIOB, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6|GPIO_Pin_7;/*選擇要控制的引腳*/
GPIO_Init(GPIOG, &GPIO_InitStructure);
/* 開啟所有led燈 */
GPIO_SetBits(GPIOB, GPIO_Pin_0);
GPIO_SetBits(GPIOG, GPIO_Pin_6|GPIO_Pin_7);
return 0;
}
/* LED初始化 */
INIT_BOARD_EXPORT(LED_GPIO_Config);
我們注意最后一行代碼,這里使用INIT_BOARD_EXPORT宏定義進行初始化,其初始化時間最早,值得注意的是,上述6個宏定義修飾的函數返回值都是int,最好將返回值改為int。
如果LED配置正確,其實驗現象和普通初始化方式沒有任何區別,接下來我們再來看看.map文件有何變化,當然我說的是自動初始化部分。
我們可以看到多了LED初始化的信息,也說明我們前文分析的是合理的。
5 總結
好了,關于自動出初始化就講完了,我估計初學者很蒙,沒關系,我們再來總結下,還是先看看RT-Thread初始化過程,如下圖所示:
rt_components_board_init() 與 rt_components_init()負責初始化,其中帶底色方框內部的函數表示被自動初始化的函數。這部分應該沒是啥問題。關鍵是如何將初始化函數和6個自動初始化宏定義聯系起來這就有點燒腦子,我整理了一張關系圖,如下圖所示:
結合前文的講解,再結合上述兩張圖,我不想在贅述了,自行理解去吧。
參考地址:
https://www.rt-thread.org/document/site/#/rt-thread-version/rt-thread-standard/programming-manual/basic/basic
-
初始化
+關注
關注
0文章
50瀏覽量
11850 -
RT-Thread
+關注
關注
31文章
1285瀏覽量
40081
發布評論請先 登錄
相關推薦
評論