STM32F103+RTT從零開始(二)——RTT系統中點亮LED
上一篇部落格簡單說了下如何使用Keil建立STM32F103的工程,並且完成了LED點亮,及讓LED等閃爍的功能,那是諸多同學學習微控制器的起手式。本篇部落格繼續上一篇部落格的內容,依舊是點亮LED,不同的是,這次點亮LED等,是在RT-Thread作業系統中進行的。
建立工程
建立一個Keil工程,晶片依舊選擇STM32F103C8T6,然後在Manage Run-Time Environment對話方塊中選擇需要用的的軟體元件,與上文不同的是,我們需要把RTT一起勾上。如下圖:
上圖中,紅線框中即為RTT作業系統的元件,分別為裝置驅動,系統核心以及shell。藍線框中為Keil的RTX作業系統。我們現在要用的是RTT,所以勾選RTT的元件即可,其中Kernel為必選項,device drivers依賴kernel,shell又依賴device drivers。
shell也提一下,shell強翻成中文就是命令列外殼,如同linux作業系統一樣,RTT也提供了一套共使用者在命令列操作的操作介面。RTT提供的這套介面叫做finsh,主要用於除錯、檢視系統資訊。finsh支援兩種模式:1. C語言直譯器模式, 為行文方便稱之為c-style;2. 傳統命令列模式,此模式又稱為msh(module shell)。
在大部分嵌入式系統中,一般開發除錯都使用硬體偵錯程式和printf日誌列印,在有些情況下,這兩種方式並不是那麼好用。比如對於RT-Thread這個多執行緒系統,我們想知道某個時刻系統中的執行緒執行狀態、手動控制系統狀態。如果有一個shell,就可以輸入命令,直接相應的函式執行獲得需要的資訊,或者控制程式的行為。這無疑會非常方便。finsh就是基於此而設計,它運行於開發板,可以使用串列埠/乙太網/USB等與PC機進行通訊。
建立工程後,相對上一篇部落格建立的工程,專案中會多出了RTT,如下圖。至於各個檔案及其作用,後續使用的時候再逐步理解。我們當前最需要關注的是board.c和rtthread.h兩個檔案。從圖中可以看出,只有這兩個檔案上沒有標註鑰匙,有鑰匙標註的是不允許更改,也就是我們能更改就是這兩個檔案。後面再分析這兩個檔案。且走下一步。
編寫點燈程式
建立好工程後,開始編寫點燈程式了,與上篇部落格一樣,直接貼上程式碼:
#include "rtthread.h"
#include "stm32f10x.h"
#include "stm32f10x_gpio.h"
int main(){
GPIO_InitTypeDef gpioInit;
//開啟GPIOB的時鐘
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
//LED上拉連線GPIOB 12引腳,所以設定如下,推輓輸出,Pin12,2MHz輸出速度
gpioInit.GPIO_Mode=GPIO_Mode_Out_PP;
gpioInit.GPIO_Pin=GPIO_Pin_12;
gpioInit.GPIO_Speed=GPIO_Speed_2MHz;
GPIO_Init(GPIOB,&gpioInit);
while(1){
//點亮LED
GPIO_ResetBits(GPIOB,GPIO_Pin_12);
//延時0.5s
rt_thread_delay(RT_TICK_PER_SECOND/2);
//關閉LED
GPIO_SetBits(GPIOB,GPIO_Pin_12);
//延時0.5s
rt_thread_delay(RT_TICK_PER_SECOND/2);
}
}
這樣編寫程式後,編譯通過,燒寫後卻發現LED根本無法按照預期進行工作,這是因為我們還缺少工作沒有做。
開啟board.c,可以看到它上面有幾句註釋,根據註釋,修改如下:
#include <rthw.h>
#include <rtthread.h>
#include "stm32f10x_rcc.h"
// rtthread tick configuration
// 1. include header files
// 2. configure rtos tick and interrupt
// 3. add tick interrupt handler
// rtthread tick configuration
// 1. include some header file as need
//#include <your_header_file.h>
#ifdef __CC_ARM
extern int Image$$RW_IRAM1$$ZI$$Limit;
#define HEAP_BEGIN (&Image$$RW_IRAM1$$ZI$$Limit)
#elif __ICCARM__
#pragma section="HEAP"
#define HEAP_BEGIN (__segment_end("HEAP"))
#else
extern int __bss_end;
#define HEAP_BEGIN (&__bss_end)
#endif
#define SRAM_SIZE 8
#define SRAM_END (0x20000000 + SRAM_SIZE * 1024)
/**
* This function will initial STM32 board.
*/
void rt_hw_board_init()
{
// rtthread tick configuration
// 2. Configure rtos tick and interrupt
//SysTick_Config(SystemCoreClock / RT_TICK_PER_SECOND);
SysTick_Config(SystemCoreClock / RT_TICK_PER_SECOND);
/* Call components board initial (use INIT_BOARD_EXPORT()) */
#ifdef RT_USING_COMPONENTS_INIT
rt_components_board_init();
#endif
#if defined(RT_USING_CONSOLE) && defined(RT_USING_DEVICE)
rt_console_set_device(RT_CONSOLE_DEVICE_NAME);
#endif
#if defined(RT_USING_USER_MAIN) && defined(RT_USING_HEAP)
rt_system_heap_init((void*)HEAP_BEGIN, (void*)SRAM_END);
#endif
}
// rtthread tick configuration
// 3. add tick interrupt handler
// tickvoid SysTick_Handler(void)
// {
// /* enter interrupt */
// rt_interrupt_enter();
//
// rt_tick_increase();
//
// /* leave interrupt */
// rt_interrupt_leave();
// }
void SysTick_Handler(void)
{
// /* enter interrupt */
rt_interrupt_enter();
//
rt_tick_increase();
//
// /* leave interrupt */
rt_interrupt_leave();
}
再次編譯,燒寫程式,LED開始閃爍。
RTT第一次分析
board.c修改後,程式就正常工作了。可是為什麼呢?根據經驗來說,C程式不是從main開始的麼,board中的程式又是何時執行的呢?在main中我們有死迴圈,如果是從main開始執行的,那麼board.c的函式就絕對不可能執行了。
為什麼不是從main開始執行的
Ctrl+F搜尋rt_hw_board_init
函式。發現他在components.c中的rtthread_startup
呼叫,再搜尋rtthread_startup
結構發現其呼叫如下:
#if defined (__CC_ARM)
extern int $Super$$main(void);
/* re-define main function */
int $Sub$$main(void)
{
rt_hw_interrupt_disable();
rtthread_startup();
return 0;
}
#elif defined(__ICCARM__)
extern int main(void);
/* __low_level_init will auto called by IAR cstartup */
extern void __iar_data_init3( void );
int __low_level_init(void)
{
// call IAR table copy function.
__iar_data_init3();
rt_hw_interrupt_disable();
rtthread_startup();
return 0;
}
#elif defined(__GNUC__)
extern int main(void);
/* Add -eentry to arm-none-eabi-gcc argument */
int entry(void)
{
rt_hw_interrupt_disable();
rtthread_startup();
return 0;
}
#endif
在上面預處理指令有三段,分別判斷三個巨集是否被定義——__CC_ARM
、__ICCARM__
、__GNUC__
,這三個是什麼呢?如果全域性搜尋,會發現在core_cm3.h中它們出現很多次了。
ARM 系列目前支援三大主流的工具鏈,即ARM RealView (armcc), IAR EWARM (iccarm), and GNU Compler Collection (gcc),這三個就是用來指示當前使用的是哪個工具鏈。因為我們使用的就是RealView(Keil)了。
可以看到$Super$$main和$Sub$$main,這又是什麼呢?
在某些情況下,無法修改現有符號,例如,由於符號位於外部庫或 ROM 程式碼中。為了解決這個問題,Keil提供了使用 $Super$$ 和 $Sub$$ 模式來修補現有符號的方法。 $Super$$標識的是原函式, $Sub$$標識的是新函式。上面的程式碼就是它們用法的最好示例了。
extern int $Super$$main(void);
/* re-define main function */
int $Sub$$main(void)
{
rt_hw_interrupt_disable();
rtthread_startup();
return 0;
}
這樣,程式的執行就不是從使用者寫的main方法開始了。而是從這個$Sub$$main(void)開始的了。
main是怎麼執行的
已經知道了程式不是從main開始執行的是RTT系統作的怪,那麼使用者寫的main方法是何時執行的呢?接著搜尋$Super$$main,得到其呼叫如下:
/* the system main thread */
void main_thread_entry(void *parameter)
{
extern int main(void);
extern int $Super$$main(void);
/* RT-Thread components initialization */
rt_components_init();
/* invoke system main function */
#if defined (__CC_ARM)
$Super$$main(); /* for ARMCC. */
#elif defined(__ICCARM__) || defined(__GNUC__)
main();
#endif
}
接著搜尋main_thread_entry
得帶程式碼如下:
void rt_application_init(void)
{
rt_thread_t tid;
#ifdef RT_USING_HEAP
tid = rt_thread_create("main", main_thread_entry, RT_NULL,
RT_MAIN_THREAD_STACK_SIZE, RT_THREAD_PRIORITY_MAX / 3, 20);
RT_ASSERT(tid != RT_NULL);
#else
rt_err_t result;
tid = &main_thread;
result = rt_thread_init(tid, "main", main_thread_entry, RT_NULL,
main_stack, sizeof(main_stack), RT_THREAD_PRIORITY_MAX / 3, 20);
RT_ASSERT(result == RT_EOK);
#endif
rt_thread_startup(tid);
}
從名字就可以看得出來,這是在造執行緒啊,查閱下rtthread的官方文件果然如此。rt_application_init
被rtthread_startup
呼叫,然後它建立了一個執行緒,並在執行緒中呼叫了使用者定義的main函式。至此就真相大白了。RTT利用工具鏈提供的方式,替換掉了使用者的main,來啟動作業系統,並建立了一條執行緒,線上程中呼叫了使用者的main方法。
至此,RTT作業系統就已經在STM32C8T6核心板上跑起來了。後續使用RTT作業系統得先看下官方文件,然後在使用中實踐,在實踐中深入理解,以便更快更好的掌握RTT。