上一篇博客簡單說了下如何使用Keil創建STM32F103的工程,並且完成了LED點亮,及讓LED等閃爍的功能,那是諸多同學學習單片機的起手式。本篇博客繼續上一篇博客的內容,依舊是點亮LED,不同的是,這次點亮LED等,是在RT-Thread操作系統中進行的。
創建工程
創建一個Keil工程,芯片依舊選擇STM32F103C8T6,然後在Manage Run-Time Environment對話框中選擇需要用的的軟件組件,與上文不同的是,我們需要把RTT一起勾上,這裏的RTOS入口,可以通過RTT的官網搜索pack進行安裝。如下圖:
上圖中,紅線框中即爲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。