/*
* Copyright (c) 2006-2019, RT-Thread Development Team
*
* SPDX-License-Identifier: Apache-2.0
*
* Change Logs:
* Date Author Notes
* 2017-07-24 Tanek the first version
* 2018-11-12 Ernest Chen modify copyright
*/
#include <stdint.h>
#include <rthw.h>
#include <rtthread.h>
#include "config.h"
#if 0
#define _SCB_BASE (0xE000E010UL)
#define _SYSTICK_CTRL (*(rt_uint32_t *)(_SCB_BASE + 0x0))
#define _SYSTICK_LOAD (*(rt_uint32_t *)(_SCB_BASE + 0x4))
#define _SYSTICK_VAL (*(rt_uint32_t *)(_SCB_BASE + 0x8))
#define _SYSTICK_CALIB (*(rt_uint32_t *)(_SCB_BASE + 0xC))
#define _SYSTICK_PRI (*(rt_uint8_t *)(0xE000ED23UL))
// Updates the variable SystemCoreClock and must be called
// whenever the core clock is changed during program execution.
extern void SystemCoreClockUpdate(void);
// Holds the system core clock, which is the system clock
// frequency supplied to the SysTick timer and the processor
// core clock.
extern uint32_t SystemCoreClock;
static uint32_t _SysTick_Config(rt_uint32_t ticks)
{
if ((ticks - 1) > 0xFFFFFF)
{
return 1;
}
_SYSTICK_LOAD = ticks - 1;
_SYSTICK_PRI = 0xFF;
_SYSTICK_VAL = 0;
_SYSTICK_CTRL = 0x07;
return 0;
}
#endif
#if defined(RT_USING_USER_MAIN) && defined(RT_USING_HEAP)
#define RT_HEAP_SIZE 2048
static uint32_t rt_heap[RT_HEAP_SIZE]; // heap default size: 4K(1024 * 4)
RT_WEAK void *rt_heap_begin_get(void)
{
return rt_heap;
}
RT_WEAK void *rt_heap_end_get(void)
{
return rt_heap + RT_HEAP_SIZE;
}
#endif
/**
* This function will initial your board.
*/
void rt_hw_board_init()
{
#if 0
/* System Clock Update */
SystemCoreClockUpdate();
/* System Tick Configuration */
_SysTick_Config(SystemCoreClock / RT_TICK_PER_SECOND);
#endif
SysInit();//添加硬件相关的初始化
/* Call components board initial (use INIT_BOARD_EXPORT()) */
#ifdef RT_USING_COMPONENTS_INIT
rt_components_board_init();
#endif
#if defined(RT_USING_USER_MAIN) && defined(RT_USING_HEAP)
rt_system_heap_init(rt_heap_begin_get(), rt_heap_end_get());
#endif
}
void SysTick_Handler(void)
{
/* enter interrupt */
rt_interrupt_enter();
rt_tick_increase();
/* leave interrupt */
rt_interrupt_leave();
}
/*
* File : application.c
* This file is part of RT-Thread RTOS
* COPYRIGHT (C) 2006, RT-Thread Development Team
*
* The license and distribution terms for this file may be
* found in the file LICENSE in this distribution or at
* http://www.rt-thread.org/license/LICENSE
*
* Change Logs:
* Date Author Notes
* 2017-07-24 Tanek the first version
*/
#include <rthw.h>
#include <rtthread.h>
#include "board.h"
#include "usart.h"
#include "delay.h"
#include "led.h"
#include "timer.h"
#include <rtdef.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 <stm32f10x.h>
///***
// * **$Super$$main和$Sub
// * 分别判断三个宏是否被定义——__CC_ARM、__ICCARM__、__GNUC__,
// * 这三个是什么呢?如果全局搜索,会发现在core_cm3.h中它们出现很多次了。
//ARM 系列目前支持三大主流的工具链,
//即ARM RealView (armcc),
//IAR EWARM (iccarm), and GNU Compler Collection (gcc),
//这三个就是用来指示当前使用的是哪个工具链。
//因为我们使用的就是RealView(Keil)了。
//可以看到**$Super$$main和$Sub
// * rt_application_init被rtthread_startup调用,
// * 然后它创建了一个线程,并在线程中调用了用户定义的main函数。至此就真相大白了。RTT利用工具链提供的
// *
// * 程序的执行就不是从用户写的main方法开始了。而是从这个$Sub$$main(void)开始的了。
// *
// * /
#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 64
#define SRAM_END (0x20000000 + SRAM_SIZE * 1024)
extern uint8_t OSRunning;
#define RT_USING_FINSH //FISH 组件
#if 0
#define _SCB_BASE (0xE000E010UL)
#define _SYSTICK_CTRL (*(rt_uint32_t *)(_SCB_BASE + 0x0))
#define _SYSTICK_LOAD (*(rt_uint32_t *)(_SCB_BASE + 0x4))
#define _SYSTICK_VAL (*(rt_uint32_t *)(_SCB_BASE + 0x8))
#define _SYSTICK_CALIB (*(rt_uint32_t *)(_SCB_BASE + 0xC))
#define _SYSTICK_PRI (*(rt_uint8_t *)(0xE000ED23UL))
Updates the variable SystemCoreClock and must be called
whenever the core clock is changed during program execution.
//extern void SystemCoreClockUpdate(void);
Holds the system core clock, which is the system clock
frequency supplied to the SysTick timer and the processor
core clock.
//extern uint32_t SystemCoreClock;
static uint32_t _SysTick_Config(rt_uint32_t ticks)
{
if ((ticks - 1) > 0xFFFFFF)
{
return 1;
}
_SYSTICK_LOAD = ticks - 1;
_SYSTICK_PRI = 0xFF;
_SYSTICK_VAL = 0;
_SYSTICK_CTRL = 0x07;
return 0;
}
#endif
/*以下定义为 任务堆栈的大小*/
//#if defined(RT_USING_USER_MAIN) && defined(RT_USING_HEAP)
//#define RT_HEAP_SIZE 2048
//static uint32_t rt_heap[RT_HEAP_SIZE]; // heap default size: 4K(1024 * 4)
//RT_WEAK void *rt_heap_begin_get(void)
//{
// return rt_heap;
//}
//RT_WEAK void *rt_heap_end_get(void)
//{
// return rt_heap + RT_HEAP_SIZE;
//}
//#endif
/**
* 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);
//串口初始化
uart_init(115200);
USART2_Configuration(115200);
TIM1_Int_Init(1099,7200); //5hz 的计数频率,计数到 999 为 100ms
delay_init(72);
// EXTIX_Init();//外部中断
LED_Init(); //初始化LED
//tips:把硬件初始化放上面
OSRunning=1;
/* 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) /* 定义该宏开启设置应用入口为 main 函数 */
rt_system_heap_init((void*)HEAP_BEGIN, (void*)SRAM_END);
#endif
}
// rtthread tick configuration
// 3. add tick interrupt handler
void SysTick_Handler(void)
{
/* enter interrupt */
rt_interrupt_enter();
rt_tick_increase();
/* leave interrupt */
rt_interrupt_leave();
}
/* RT-Thread config file */
#ifndef __RTTHREAD_CFG_H__
#define __RTTHREAD_CFG_H__
#if defined(__CC_ARM) || defined(__CLANG_ARM)
//#include "RTE_Components.h"
#define RT_USING_FINSH
#if defined(RTE_USING_FINSH)
#define RT_USING_FINSH
#endif //RTE_USING_FINSH
#endif //(__CC_ARM) || (__CLANG_ARM)
// <<< Use Configuration Wizard in Context Menu >>>
// <h>Basic Configuration
// <o>Maximal level of thread priority <8-256>
// <i>Default: 32
#define RT_THREAD_PRIORITY_MAX 32
// <o>OS tick per second
// <i>Default: 1000 (1ms)
#define RT_TICK_PER_SECOND 1000
// <o>Alignment size for CPU architecture data access
// <i>Default: 4
#define RT_ALIGN_SIZE 4
// <o>the max length of object name<2-16>
// <i>Default: 8
#define RT_NAME_MAX 8
// <c1>Using RT-Thread components initialization
// <i>Using RT-Thread components initialization
#define RT_USING_COMPONENTS_INIT
// </c>
#define RT_USING_USER_MAIN
// <o>the stack size of main thread<1-4086>
// <i>Default: 512
#define RT_MAIN_THREAD_STACK_SIZE 512
// </h>
// <h>Debug Configuration
// <c1>enable kernel debug configuration
// <i>Default: enable kernel debug configuration
//#define RT_DEBUG
// </c>
// <o>enable components initialization debug configuration<0-1>
// <i>Default: 0
#define RT_DEBUG_INIT 0 // 内核调试
// <c1>thread stack over flow detect
// <i> Diable Thread stack over flow detect
//#define RT_USING_OVERFLOW_CHECK
// </c>
// </h>
// <h>Hook Configuration
// <c1>using hook
// <i>using hook
//#define RT_USING_HOOK
// </c>
// <c1>using idle hook
// <i>using idle hook
//#define RT_USING_IDLE_HOOK
// </c>
// </h>
// <e>Software timers Configuration
// <i> Enables user timers
#define RT_USING_TIMER_SOFT 0
#if RT_USING_TIMER_SOFT == 0
#undef RT_USING_TIMER_SOFT
#endif
// <o>The priority level of timer thread <0-31>
// <i>Default: 4
#define RT_TIMER_THREAD_PRIO 4
// <o>The stack size of timer thread <0-8192>
// <i>Default: 512
#define RT_TIMER_THREAD_STACK_SIZE 512
// </e>
// <h>IPC(Inter-process communication) Configuration
// <c1>Using Semaphore
// <i>Using Semaphore
#define RT_USING_SEMAPHORE // 信号量
// </c>
// <c1>Using Mutex
// <i>Using Mutex
#define RT_USING_MUTEX
// </c>
// <c1>Using Event
// <i>Using Event
#define RT_USING_EVENT
// </c>
// <c1>Using MailBox
// <i>Using MailBox
#define RT_USING_MAILBOX
// </c>
// <c1>Using Message Queue
// <i>Using Message Queue
#define RT_USING_MESSAGEQUEUE
// </c>
// </h>
// <h>Memory Management Configuration
// <c1>Dynamic Heap Management
// <i>Dynamic Heap Management
#define RT_USING_HEAP //使用动态堆栈
// </c>
// <c1>using small memory
// <i>using small memory
#define RT_USING_SMALL_MEM //使用静态态堆栈
// </c>
// <c1>using tiny size of memory
// <i>using tiny size of memory
#define RT_USING_TINY_SIZE
// </c>
// </h>
// <h>Console Configuration
// <c1>Using console
// <i>Using console
#define RT_USING_CONSOLE
// </c>
// <o>the buffer size of console <1-1024>
// <i>the buffer size of console
// <i>Default: 128 (128Byte)
#define RT_CONSOLEBUF_SIZE 128
// </h>
#if defined(RT_USING_FINSH)
#define FINSH_USING_MSH
#define FINSH_USING_MSH_ONLY
// <h>Finsh Configuration
// <o>the priority of finsh thread <1-7>
// <i>the priority of finsh thread
// <i>Default: 6
#define __FINSH_THREAD_PRIORITY 5
#define FINSH_THREAD_PRIORITY (RT_THREAD_PRIORITY_MAX / 8 * __FINSH_THREAD_PRIORITY + 1)
// <o>the stack of finsh thread <1-4096>
// <i>the stack of finsh thread
// <i>Default: 4096 (4096Byte)
#define FINSH_THREAD_STACK_SIZE 512
// <o>the history lines of finsh thread <1-32>
// <i>the history lines of finsh thread
// <i>Default: 5
#define FINSH_HISTORY_LINES 1
#define FINSH_USING_SYMTAB
// </h>
#endif
// <<< end of configuration section >>>
#endif
本篇内容比较简单,但却很繁琐,篇幅也很长,毕竟是囊括了整个操作系统的生命周期。这篇文章的目的是作为后续设计多任务开发的铺垫,后续会单独再抽出一篇分析任务的相关知识。另外本篇文章以单核MCU为背景,并且以最新的3.1.xLTS版本源码进行分析。主要内容目录如下:
-
基于bsp/stm32/stm32f103-mini-system为背景
-
Cortex-M3的堆栈基础概念
-
C语言main函数和rt-thread的main
-
rt-thread操作系统的传统初始化与自动初始化组件
-
任务是怎样运行起来的
-
Idle任务与新的构想
基于bsp/stm32/stm32f103-mini-system的开机介绍
关于体系结构的知识这里不做过多的介绍,因为这些知识要讲清楚的话足以写出一本大部头的书出来。不过会简单介绍一些必要的东西。
Stm32f103单片机是cortex-m3内核,在cortex-m3内核中使用双堆栈psp和msp,模式分为线程模式和handler模式,权限级别分为非特权级别和特权级别(现在只需要知道这么多就行了),handler模式就是当处理发生中断的时候自动进入的模式,其handler模式永远为特权级。
上电开机最开始运行的是MCU内部的ROM部分,这部分代码我们通常看不到,其通常是对芯片进行必要的初始化,比如FLASH和RAM的时钟初始化等,然后跳转到用户flash区域运行用户代码。在STM32中用户flash地址从0x08000000开始。我们写的代码都是从这里开始运行的。其次由于cortexM规定其用户FLASH区域的最前面必须是一张中断向量表。所以也就是说STM32的0x08000000开始是一张中断向量表,这是必须的也是默认的,当然在之后还可以重映射其它地方的向量表。这张向量表中的第一项是一个栈地址,第二项复位向量地址。下面贴一段向量表部分代码(摘录自startup_stm32f103xb.s):
__Vectors DCD __initial_sp ; Topof Stack
DCD Reset_Handler ; Reset Handler
DCD NMI_Handler ; NMIHandler
DCD HardFault_Handler ; Hard Fault Handler
DCD MemManage_Handler ; MPU Fault Handler
DCD BusFault_Handler ; Bus Fault Handler
DCD UsageFault_Handler ; Usage Fault Handler
DCD 0 ; Reserved
DCD 0 ; Reserved
DCD 0 ; Reserved
DCD 0 ; Reserved
DCD SVC_Handler ; SVCall Handler
DCD DebugMon_Handler ; Debug Monitor Handler
DCD 0 ; Reserved
DCD PendSV_Handler ; PendSV Handler
DCD SysTick_Handler ; SysTick Handler
另外需要注意的是开机后会自动进入复位异常,通常我们叫上电复位过程,不过意外的是上电复位处理的模式是特权级线程模式。在特权模式下堆栈指针将使用MSP,非特权模式下可以被切换到PSP。RT-Thread操作系统就是这么做的。所以回过头来看,中断向量表第一项指定了MSP的栈起始地址,并被自动加载到MSP,第二项指定了复位向量地址,也被自动加载到PC并运行。这样一来开机后我们能通过debug看到PC指针最先指向复位向量的第一条指令上。我们看一下stm32f103在armcc编译器上的复位向量代码:
; Reset handler
Reset_Handler PROC
EXPORT Reset_Handler [WEAK]
IMPORT __main
IMPORT SystemInit
LDR R0, =SystemInit
BLX R0
LDR R0, =__main
BX R0
ENDP
这是一段汇编代码,其完成两件事,第一件事调用systemInit函数完成一些初始化,第二件事跳转到__main函数。其中systemInit函数我们是可以找到并可以修改的一个C语言实现的函数(暂时不讨论,有兴趣的可以看system_stm32f1xx.c)。而这个__main就牛逼了,这既不是我们自己写的C语言的main也看不到它在哪里实现的。但是现在进入__main后它就是会跑到你最终用C语言写的main。这个__main的来龙去脉稍后会在第三部分分析。
Cortex-M3的堆栈基础概念
在Cortex-M3的处理器内核上堆栈指针分为PSP和MSP。handler模式下总是使用MSP,线程模式可以通过CONTROL寄存器来配置(修改的时候必须处于特权模式才可以)。
之所以需要这样设计就是为了将普通软件和系统软件通过权限隔离开,避免普通用户权限操作系统关键资源带来安全风险。当我们使用带有操作系统的环境进行开发时,操作系统就会将关键操作例如任务切换、中断处理等在特权模式操作。而其它的操作都会运行在非特权模式下完成。
操作系统一般都会将必要的操作封装出API接口,以提供给普通软件调用。而这背后的设计思想就是通过触发异常,然后进入特权模式运行异常向量处理程序。而这段异常处理程序早就让操作系统实现了,进而这部分特权操作是操作系统接管处理的。这也就避免用户普通软件去进行不必要的特权操作。例如用户任务想主动放弃CPU从而调用yield,yield将进行任务切换,其中过程大概是“选出另一个任务”->”触发SVC或者Pendsv异常”->进入SVC/Pendsv的handler异常处理程序,此时是特权模式,完成操作后返回到新任务运行。在RT-Thread中进入任务切换是通过触发Pendsv异常。
C语言main函数和RT-Thread的main
前面提到过开机启动最后进入复位向量处运行,最终调用__main就跑到我们外面写的C语言的main函数了。但这并非这么简单,在从__main到我们的main中间还有一系列操作比如初始化堆栈、初始化全局变量区域、初始化C运行时库等,然后再在最后调用用户的main函数。
不过在不同的编译器上这个__main并非是固定的,这里也就armcc是如此,如果是GCC和IAR的话其就不太一样,不过不影响我们分析核心主题。这里仅以借用armcc为例来分析主题中心思想。另外在说明RT-Thread中开启RT_USING_USER_MAIN的时候在ARMCC编译器上还有一个支持挂钩的操作,这种操作一般见于补丁修复的时候。其实现方式是在原有函数的名字前加上$Sub$$
前缀就可以将原有函数劫持下来,并通过加上$Super$$
前缀再调用原始函数。具体如下:The followingexample shows how to use $Super$$and $Sub$$ to insert a callto the function ExtraFunc() before the call to the legacy functionfoo().
extern void ExtraFunc(void);
extern void $Super$$foo(void);
/* this functionis called instead of the original foo() */
void $Sub$$foo(void)
{
ExtraFunc(); /* does some extra setup work */
$Super$$foo(); /* calls the original foo() function */
/* To avoid calling the original foo() function
* omit the $Super$$foo(); function call.
*/
}
上例中原本有一个原始函数叫做foo,但是现在通过$Sub$$foo来劫持所有调用foo的地方,自动会调用$Sub$$foo,然后新的$Sub$$foo里面先调用自己的扩展实现ExtraFunc后,再接着调用原始版本的foo函数,不过调用原始的foo是加了前缀$Super$$的$Super$$foo.
当使用RT-Thread操作系统开启RT_USING_USER_MAIN后就是利用这种骚操作来完成RT-Thread操作系统的初始化过程的。(代码摘录自components.c)
extern int $Super$$main(void);
/* re-definemain function */
int $Sub$$main(void)
{
rtthread_startup();
return 0;
}
关于rtthread_startup函数稍后再讲解,不过先接着看下面这个函数:
/* the systemmain 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) || defined(__CLANG_ARM)
$Super$$main(); /* for ARMCC. */
#elif defined(__ICCARM__) || defined(__GNUC__)
main();
#endif
}
上面这个函数其实是个小任务,就是完成组件初始化后再跳转到用户main函数的。这个小任务在rtthread_startup中调用rt_application_init时创建的,所以此时rt-thread系统早就以经跑起来了。也就是说当调用rtthread_startup后正常情况就不再会返回到原来的调用地方,接下来会交给系统的调度器去接管,切换运行任务去了。看下面的代码了解rt_application_init:
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_MAIN_THREAD_PRIORITY, 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_MAIN_THREAD_PRIORITY, 20);
RT_ASSERT(result == RT_EOK);
/* if not define RT_USING_HEAP, using toeliminate the warning */
(void)result;
#endif
rt_thread_startup(tid);
}
至此,关于各种main的子子孙孙以经差不多了解清楚了,其流程大概如下:
ResetHandle->__main->$Sub$$main->(rtthread_startup->rt_application_init->main_thread_entry)->$Super$$main。
其中$Super$$main就是我们的用户main函数。如果没有启用RT_USING_USER_MAIN那就简单了,其流程如下:
ResetHandle->__main->main
接下来再接着分析$Sub$$main中调用的rtthread_startup函数。
RT-Thread操作系统的传统初始化与自动初始化组件
这里着重讨论rtthread_startup函数,因为这就是RT-Thread操作系统的入口和初始化流程。不过既然说到rtthread_startup函数了,就不得不一起介绍一下RT-Thread操作系统的自动初始化组件了。
rtthread_startup函数是一个函数调用链,依次调用各个阶段的初始化函数,并在最后启动调度器不再返回。代码摘录自components.c
int rtthread_startup(void)
{
rt_hw_interrupt_disable();
/* board level initialization
* NOTE: please initialize heap insideboard initialization.
*/
rt_hw_board_init();
/* show RT-Thread version */
rt_show_version();
/* timer system initialization */
rt_system_timer_init();
/* scheduler system initialization */
rt_system_scheduler_init();
#ifdef RT_USING_SIGNALS
/* signal system initialization */
rt_system_signal_init();
#endif
/* create init_thread */
rt_application_init();
/* timer thread initialization */
rt_system_timer_thread_init();
/* idle thread initialization */
rt_thread_idle_init();
/* start scheduler */
rt_system_scheduler_start();
/* never reach here */
return 0;
}
以上代码我们主要脉络是这样的:先关闭全局中断->初始化硬件板上的资源->打印RT-Thread的LOGO->系统定时器功能初始化->调度器初始化->signal功能初始化->应用程序初始化(这个通常是用来创建用户任务的)->系统软timer任务初始化->系统idle任务初始化->启动调度器,永远不再返回。
这里我们先来说一下为什么要先关闭全局中断,因为在初始化过程中,有可能MCU就有其它的中断和异常触发了,这个时候系统还没有初始化完成,这就势必导致系统出现故障,所以先关闭全局中断,并在启动调度器后再打开。
rt_hw_board_init非常关键,在这个函数里面必须完成一些必须的初始化过程:堆内存系统的初始化和硬件资源模块以及如果开启了自动初始化组件时还需要调用rt_components_board_init完成必要的初始化,这个函数是自动初始化组件的一个接口。(代码摘录自bsp\stm32\libraries\HAL_Drivers\drv_common.c)
RT_WEAK void rt_hw_board_init()
{
#ifdef SCB_EnableICache
/* EnableI-Cache---------------------------------------------------------*/
SCB_EnableICache();
#endif
#ifdef SCB_EnableDCache
/* Enable D-Cache---------------------------------------------------------*/
SCB_EnableDCache();
#endif
/* HAL_Init() function is called at thebeginning of the program */
HAL_Init();
/* System clock initialization */
SystemClock_Config();
rt_hw_systick_init();
/* Heap initialization */
#if defined(RT_USING_HEAP)
rt_system_heap_init((void*)HEAP_BEGIN, (void*)HEAP_END);
#endif
/* Pin driver initialization is open bydefault */
#ifdef RT_USING_PIN
rt_hw_pin_init();
#endif
/* USART driver initialization is openby default */
#ifdef RT_USING_SERIAL
rt_hw_usart_init();
#endif
/* Set the shell console output device*/
#ifdef RT_USING_CONSOLE
rt_console_set_device(RT_CONSOLE_DEVICE_NAME);
#endif
/* Board underlying hardwareinitialization */
#ifdef RT_USING_COMPONENTS_INIT
rt_components_board_init();
#endif
}
然后回到rtthread_startup函数中再看rt_application_init函数,由于我们是用的stm32的BSP,这个bsp系列是使用自动初始化组件和RT_USING_USER_MAIN功能的,所以过程稍微隐蔽一些,先是在rt_application_init中创建了一个小任务,然后再在小任务中调用了rt_components_init,这也是自动初始化组件的接口。如果没有开启自动初始化组件的话,通常我们的用户任务可以在rt_application_init中创建了。也可以像这里的实现一样,先创建一个小任务,然后再在小任务里完成一些初始化和创建用户任务。
然后再回到rthtread_startup中看到有初始化软timer和idle任务的,其中软件timer功能是可以通过裁剪配置选择的,如果打开后就可以在后续创建softtimer。否则所有的timer都会在OS TICK的中断上下文中计时。另外这个idle任务也是系统中必不可少和优先级最低的任务。即使我们启动调度器后没有创建任何用户任务,系统中也有一个idle任务在运行。Idle任务的优先级最低,在此我建议开发人员最好不要将自己的用户任务优先级配置成最低以免和idle竞争时间片,这会给你今后的开发带来不必要的麻烦。关于这个问题,我最后会提出一些新的设计构想。不过这里先要介绍一下idle任务的功能。Idle任务会在系统空闲时被调度运行,所以我们通常在idle任务里做低功耗设计。其次idle任务里还会完成系统资源的回收。例如被删除的任务,被删除的module等。
最后rthtread_startup启动调度器rt_system_scheduler_start开始调度系统的任务,从此就开始运行任务,不再返回。这里又要记住一个概念,在上文提到的PSP和MSP,到目前为止MCU还是使用一开始中断向量表中指定的MSP栈。但是当调度任务后,任务会有自己的栈,且rt-thread系统会将任务的栈切换到PSP栈指针。值得注意的是,这个MSP是全局共享的,所有的中断程序都会使用这个栈空间,所以我们需要根据自己的情况来配置这个MSP栈的空间大小。
接下来我们再来介绍自动初始化组件。RT-Thread中的自动初始化组件思路来自于Linux内核。其实现手段是将需要初始化的函数接口通过链接器指令放在特殊的section中。这个section的概念是当我们程序最终链接成一个image后会形成一个标准格式的文件,其中armcc中叫做ARM ELF。详细的介绍可以查阅官方资料。其中ELF文件就有将代码分成称为section的区域,可以称作段。并且可以指定自己的代码放在指定名称的段中,且可以指定这个section段的ROM地址。这样当我们设计玩初始化接口后,通过链接器的指令以及链接脚本文件将我们的初始化代码放在特定的地方,并且利用命名规则来做到顺序排序。等需要调用初始化的时候可以利用这些section的地址转换成函数指针直接批量循环调用。通常你会在MDK的工程文件链接器参数中看到这样的指令:--keep *.o(.rti_fn.*),这是为了在链接阶段保证这些自定义段不被删除。同时也可以看出rti_fn就是自动初始化组件的section名字。类似的将函数放置在这些段中的链接器指令如下:(摘录自rtdef.h)
/*initialization export */
#ifdef RT_USING_COMPONENTS_INIT
typedef int (*init_fn_t)(void);
#ifdef _MSC_VER/* we do notsupport 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
#else
#define INIT_EXPORT(fn, level)
#endif
/* board initroutines will be called in board_init() function */
#define INIT_BOARD_EXPORT(fn) INIT_EXPORT(fn,"1")
/*pre/device/component/env/app init routines will be called in init_thread */
/* componentspre-initialization (pure software initilization) */
#define INIT_PREV_EXPORT(fn) INIT_EXPORT(fn,"2")
/* deviceinitialization */
#define INIT_DEVICE_EXPORT(fn) INIT_EXPORT(fn,"3")
/* componentsinitialization (dfs, lwip, ...) */
#define INIT_COMPONENT_EXPORT(fn) INIT_EXPORT(fn,"4")
/* environmentinitialization (mount disk, ...) */
#define INIT_ENV_EXPORT(fn) INIT_EXPORT(fn,"5")
/* appliationinitialization (rtgui application etc ...) */
#define INIT_APP_EXPORT(fn) INIT_EXPORT(fn,"6")
其中不同的数字代表不同的初始化顺序,可以根据需要来选择。接着如上文提到的两个函数rt_components_board_init和rt_components_init是如何实现的:摘录自components.c
#ifdef RT_USING_COMPONENTS_INIT
/*
* Components Initialization will initializesome 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, thedriver 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");
/**
* RT-Thread Components Initialization forboard
*/
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
}
之所以要分开这两个函数就是因为board阶段的初始化比其它普通的组件初始化早,board阶段的初始化通常没什么系统资源依赖。而其它情况下则通常在操作系统已经完成必要的初始化后才能做的初始化才会放在rt_components_init里。
任务是怎样运行起来的
要说明任务是怎么运行起来的,就得知道任务是怎么创建的,其次结合之前写的文章<源码解读·RT-Thread多任务调度算法>就差不多了。那么这里就介绍一下任务的创建。照样用上面的rt_application_init里创建任务的代码来举例:
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_MAIN_THREAD_PRIORITY, 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_MAIN_THREAD_PRIORITY, 20);
RT_ASSERT(result == RT_EOK);
/* if not define RT_USING_HEAP, using toeliminate the warning */
(void)result;
#endif
rt_thread_startup(tid);
}
首先要说明的是RT-Thread任务创建有两种,一种是动态的,一种是静态的。所谓的动态就是其任务栈自动在堆内存中分配;静态是用户自己指定栈空间,当然通常这个栈来自于用户定义的数组。如上例中当RT_USING_HEAP宏被打开,也就是有堆内存的时候会采用rt_thread_create接口来创建动态资源的任务。当然可以利用rt_thread_init来创建一个静态资源的任务。先来了解一下这两个函数在创建任务时的一些参数:”main”这是任务的名称,任务名称用一个字符串来指定,不是很重要,不过最好能起到一定的说明性,有利于今后调试用。main_thread_entry这是任务的入口函数,所谓的任务就是一个C语言中的函数而已。RT_NULL,这是传给任务入口函数的参数,如果没有就为NULL.因为RT_Thread中的任务原型为:void (*entry)(void*parameter);RT_MAIN_THREAD_STACK_SIZE为任务的栈大小,以字节为单位。RT_MAIN_THREAD_PRIORITY为任务的优先级号。20为任务的时间片大小。其中静态任务中还有tid代表任务的TCB数据结构句柄。main_stack为栈空间起始地址。当用动态创建的方法创建成功后会返回一个任务的TCB任务句柄出来。之后我们利用rt_thread_startup(任务句柄)的形式启动任务即可。例如上例中rt_thread_startup(tid);不过rt_thread_startup函数真正的功能是将任务放置于调度队列中,并置任务状态为ready,由此交给调度器去调度,能不能立马运行取决与调度器的调度。一般情况下,要想任务获得运行必须满足的条件:调度器已经运行,任务已经ready,没有更高优先级任务,没有中断发生。只要条件满足调度器就会调度此任务,做好必要的栈初始化和状态置位,就会切换到任务开始运行。只要任务获得运行就会使用创建任务时指定的栈空间。
不过一般的任务通常是一直运行,持续的服务。形式如下:
void task(void *parameter)
{
while (1)
{
// do_work();
}
}
idle任务与新的构想
上面解释过idle任务在rt-thread操作系统中的功能:释放资源、低功耗设计。
关于资源释放通常是任务的析构过程,这就是任务的结束。例如上例中的main_thread_entry任务之所以称为小任务的原因就是它做完事情就结束了。那么可能就会想,既然任务都结束了那么它的资源如何释放呢?比如栈空间,TCB等。这就是idle该干的事情。即使所有的用户任务都结束,最后也会剩下idle任务在运行。如果有必要的话,可以在idle任务中可以通过调用低功耗组件进入低功耗或者干脆调用电源开关控制来关机。
其次idle任务占用了最低优先级。虽然用户任务也可以使用和idle任务相同的优先级,但是并不建议这样做,比如在低功耗设计时就会出问题。另外我个人在思考一个问题,idel任务既然以经在设计之初就明确了其获得运行的条件,那么何不做成无需优先级的任务,唯一的调度决策就是:当调度器没有任务处于ready状态时就切换到idel任务运行。这就无需关注最低优先级被idle霸占的问题了。
启动流程
1.上电;
2.运行复位函数Reset_Hhandler();
3.c库函数_mian(),初始化系统堆和栈;
4.执行s u b subsubmain函数(main扩展函数,可在执行main之前执行submain,做一些硬件初始化等预操作,是keil自带的指令);
/* re-define main function */
int $Sub$$main(void)
{
rtthread_startup();
return 0;
}
5.关中断rt_hw_interrupt_disable();
6.启动rtthread_startup():
[1 ] 关中断rt_hw_interrupt_disable();
[2 ] 硬件初始化rt_hw_board_init();
[3 ] 显示版本rt_show_version();
[4 ] 系统定时器初始化rt_system_timer_init();
[5 ] 调度器初始化rt_system_scheduler_init();
[6 ] 创建初始化线程rt_application_init();
[7 ] 创建定时器线程rt_system_timer_thread_init();
[8 ] 创建空闲线程rt_thread_idle_init();
[9 ] 开启调度器rt_system_scheduler_start();
int rtthread_startup(void)
{
rt_hw_interrupt_disable();
/* board level initialization
* NOTE: please initialize heap inside board initialization.
*/
rt_hw_board_init();
/* show RT-Thread version */
rt_show_version();
/* timer system initialization */
rt_system_timer_init();
/* scheduler system initialization */
rt_system_scheduler_init();
#ifdef RT_USING_SIGNALS
/* signal system initialization */
rt_system_signal_init();
#endif
/* create init_thread */
rt_application_init();
/* timer thread initialization */
rt_system_timer_thread_init();
/* idle thread initialization */
rt_thread_idle_init();
/* start scheduler */
rt_system_scheduler_start();
/* never reach here */
return 0;
}
在上述 [6 ] 创建初始化线程rt_application_init()中,实际创建了一个main线程,如图:
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_MAIN_THREAD_PRIORITY, 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_MAIN_THREAD_PRIORITY, 20);
RT_ASSERT(result == RT_EOK);
/* if not define RT_USING_HEAP, using to eliminate the warning */
(void)result;
#endif
rt_thread_startup(tid);
}
点击线程处理函数“main_thread_entry”,跳转至该线程处理函数,发现执行此线程会进行组件的初始化(INIT_BOARD_EXPORT(fn)、INIT_DEVICE_EXPORT(fn)、。。。、INIT_APP_EXPORT(fn))。然后调用supermain()函数(与submain()对应),去到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) || defined(__CLANG_ARM)
$Super$$main(); /* for ARMCC. */
#elif defined(__ICCARM__) || defined(__GNUC__)
main();
#endif
}
当然,由于在 [6 ] 创建初始化线程rt_application_init()时,系统调度器并没有开启,所以该线程在上述 [9 ] 开启调度器rt_system_scheduler_start()之后才会执行。
main线程执行到最后(完成所有应用线程的创建),通过LR寄存器指定的链接地址退出(如下图),rt_thread_exit()会把main线程占用的内存空间释放。
线程
1.一系列独立线程的集合,每个线程在自己的环境中运行,具有独立的栈;
2.在任何时刻,只有一个线程得到运行;
3.线程执行环境(寄存器值、堆栈内容等)保存在线程堆栈中;
#ifdef RT_USING_HEAP
/**
* This function will create a thread object and allocate thread object memory
* and stack.
*
* @param name the name of thread, which shall be unique
* @param entry the entry function of thread
* @param parameter the parameter of thread enter function
* @param stack_size the size of thread stack
* @param priority the priority of thread
* @param tick the time slice if there are same priority thread
*
* @return the created thread object
*/
rt_thread_t rt_thread_create(const char *name,
void (*entry)(void *parameter),
void *parameter,
rt_uint32_t stack_size,
rt_uint8_t priority,
rt_uint32_t tick)
{
struct rt_thread *thread;
void *stack_start;
thread = (struct rt_thread *)rt_object_allocate(RT_Object_Class_Thread,
name);
if (thread == RT_NULL)
return RT_NULL;
stack_start = (void *)RT_KERNEL_MALLOC(stack_size);
if (stack_start == RT_NULL)
{
/* allocate stack failure */
rt_object_delete((rt_object_t)thread);
return RT_NULL;
}
_rt_thread_init(thread,
name,
entry,
parameter,
stack_start,
stack_size,
priority,
tick);
return thread;
}
RTM_EXPORT(rt_thread_create);