打印
[STM32F4]

【安富莱STM32F407之uCOS-III教程】第9章 μCOS-III系统移植文件

[复制链接]
3778|8
手机看帖
扫描二维码
随时随地手机跟帖
跳转到指定楼层
楼主
Eric2013|  楼主 | 2014-12-22 16:13 | 只看该作者 |只看大图 回帖奖励 |倒序浏览 |阅读模式
特别说明:
1.  本教程是安富莱电子原创。
2.  安富莱STM32F407开发板资料已经全部开源,开源地址:地址链接

3.  当前共配套300多个实例,4套用户手册。

第9章  μCOS-III系统移植文件详解

    本期教程主要主要跟大家讲解μCOS-III系统的官方移植文件,理解这几个移植文件很重要,如果这几个文件理解的比较深入的话,对于后面学习源码大有裨益。这几个文件主要就是实现任务切换的功能,关于任务切换咱们在前面几期已经详细讲解了工作原理(没有看的同学一定要看第4章和第5章,要不理解学习本章教程依然吃力),本期教程就是结合前面讲解,进一步讲解μCOS-III中任务切换实现的原理。
    9.1 移植文件
    9.2 os_cpu.h文件讲解
    9.3 os_cpu_c.c文件讲解
    9.4 os_cpu_a.asm文件讲解
    9.5 总结
9.1  移植文件
    官方提供的uCOS-III,uC-LIB,uC-CPU文件中都有Ports文件夹,咱们主要关心的是uCOS-III中的Ports文件夹。Ports文件夹中含有如下三个版本的移植文档:                               
    RealView就是用MDK编译器的移植文件,其余两个文件夹中的移植文件是用IAR和GUN的。咱们就以RealView中的移植文件为例跟大家详细的讲解下。RealView中的文件如下:


    os_cpu.h,os_cpu_c.c,os_cpu_a.asm这三个文件在移植过程中最重要,下面主要的就是把这三个文件中的内容详细讲解一下。
9.2  os_cpu.h文件讲解
    此头文件的内容比较少,下面就将里面的内容说明一下
9.2.1      宏定义
#ifdef   OS_CPU_GLOBALS
#define  OS_CPU_EXT
#else
#define  OS_CPU_EXT  extern   
#endif

/*
*********************************************************************************************************
*                                               MACROS
*********************************************************************************************************
*/

#define  OS_TASK_SW()               OSCtxSw()  
l  由于工程中没有声明OS_CPU_GLOBALS,所以使用的都是#define  OS_CPU_EXT  extern源码的头文件中使用OS_CPU_EXT的地方很多,大家要记住这个的含义。
l  #define OS_TASK_SW()     OSCtxSw()
这个是任务级的任务切换函数,后面还有个中断级的任务切换。函数实体在os_cpu_a.asm文件中。此函数的主要功能就是实现任务的切换。
/*
*********************************************************************************************************
*                              OS TICK INTERRUPT PRIORITY CONFIGURATION
*
* Note(s) : (1) For systems that don't need any high, real-time priority interrupts; the tick interrupt
*               should be configured as the highest priority interrupt but won't adversely affect system
*               operations.
*
*           (2) For systems that need one or more high, real-time interrupts; these should be configured
*               higher than the tick interrupt which MAY delay execution of the tick interrupt.
*
*               (a) If the higher priority interrupts do NOT continually consume CPU cycles but only
*                   occasionally delay tick interrupts, then the real-time interrupts can successfully
*                   handle their intermittent/periodic events with the system not losing tick interrupts
*                   but only increasing the jitter.
*
*               (b) If the higher priority interrupts consume enough CPU cycles to continually delay the
*                   tick interrupt, then the CPU/system is most likely over-burdened & can't be expected
*                   to handle all its interrupts/tasks. The system time reference gets compromised as a
*                   result of losing tick interrupts.
*********************************************************************************************************
*/

#define  OS_CPU_CFG_SYSTICK_PRIO           0u
l OS_CPU_CFG_SYSTICK_PRIO 用于配制嘀嗒定时器的优先级。关于嘀嗒定时器优先级的配置还是很讲究的,也就是上面注释所写的。
  Ø 对于那些不需要高优先级中断的系统,嘀嗒定时器中断要配置成最高优先级的中断,但是不能影响系统操作。
  Ø 如果系统中多个高优先级的中断,而且优先级比嘀嗒定时器的优先级高,那么就可能会延迟嘀嗒定时器中断。
    u 如果高优先级的中断不会持续的占有CPU,只是偶尔的延迟嘀嗒定时器中断,那么实时中断可以间歇性或者周期性的处理系统事件而不丢失嘀嗒定时器中断,只是增加抖动(因为高优先级中断的执行会抢占嘀嗒定时器中断的执行或者高优先级中断执行的时候嘀嗒定时器中断会一直得不到执行从而造成嘀嗒定时器中断在执行时间上的抖动)。
    u 如果高优先级的任务长时间的占有CPU时间会造成系统超负荷运行,而不能执行嘀嗒定时器中断,任务也不能得到及时的执行。这种情况可以认为系统丢失了几次嘀嗒定时器的执行。


沙发
Eric2013|  楼主 | 2014-12-22 16:16 | 只看该作者
本帖最后由 Eric2013 于 2014-12-22 16:18 编辑

9.2.2      时间戳配置
/*
*********************************************************************************************************
*                                       TIMESTAMP CONFIGURATION
*
* Note(s) : (1) OS_TS_GET() is generally defined as CPU_TS_Get32() to allow CPU timestamp timer to be of
*               any data type size.
*
*           (2) For architectures that provide 32-bit or higher precision free running counters
*               (i.e. cycle count registers):
*
*               (a) OS_TS_GET() may be defined as CPU_TS_TmrRd() to improve performance when retrieving
*                   the timestamp.
*
*               (b) CPU_TS_TmrRd() MUST be configured to be greater or equal to 32-bits to avoid
*                   truncation of TS.
*********************************************************************************************************
*/

#if      OS_CFG_TS_EN == 1u
#define  OS_TS_GET()               (CPU_TS)CPU_TS_TmrRd() /* See Note #2a.                     */
#else
#define  OS_TS_GET()               (CPU_TS)0u
#endif

#if (CPU_CFG_TS_32_EN    == DEF_ENABLED) && \
    (CPU_CFG_TS_TMR_SIZE  < CPU_WORD_SIZE_32)
                                                   /* CPU_CFG_TS_TMR_SIZE MUST be >= 32-bit (see Note #2b).  */
#error  "cpu_cfg.h, CPU_CFG_TS_TMR_SIZE MUST be >= CPU_WORD_SIZE_32"
#endif
l OS_CFG_TS_EN在函数os_cfg.h里面进行了宏定义:
#defineOS_CFG_TS_EN                    1u   //用于使能或者禁止时间戳,这里1表示使能        
//时间戳,0表示禁止时间戳
    下面重点说一下函数CPU_TS_TmrRd(),相对来说此函数比较的重要。这个函数在μCOS-III中主要用于任务利用率的测量,和信号量、邮箱、事件标志组等执行时间的测量。这个函数不属于μCOS-III源码部分,确切的说应该是属于μC/CPU。此函数的内容需要用户根据自己使用的处理器进行实现,以提供更高精度的时间基准,至少要高于系统的时钟节拍精度,这样才能获得更准确的函数执行时间和任务利用率的测量。使用这个函数前需要先做初始化(在文件bsp.c里面):
/*
*********************************************************************************************************
*                                          CPU_TS_TmrInit()
*
* Description : Initialize & start CPU timestamp timer.
*
* Argument(s) : none.
*
* Return(s)   : none.
*
* Caller(s)   : CPU_TS_Init().
*
*               This function is an INTERNAL CPU module function & MUST be implemented by application/
*               BSP function(s) [see Note #1] but MUST NOT be called by application function(s).
*
* Note(s)     : (1) CPU_TS_TmrInit() is an application/BSP function that MUST be defined by the developer
*                   if either of the following CPU features is enabled :
*
*                   (a) CPU timestamps
*                   (b) CPU interrupts disabled time measurements
*
*                   See 'cpu_cfg.h  CPU TIMESTAMP CONFIGURATION  Note #1'
*                     & 'cpu_cfg.h  CPU INTERRUPTS DISABLED TIME MEASUREMENT CONFIGURATION  Note #1a'.
*
*               (2) (a) Timer count values MUST be returned via word-size-configurable 'CPU_TS_TMR'
*                       data type.
*
*                       (1) If timer has more bits, truncate timer values' higher-order bits greater
*                           than the configured 'CPU_TS_TMR' timestamp timer data type word size.
*
*                       (2) Since the timer MUST NOT have less bits than the configured 'CPU_TS_TMR'
*                           timestamp timer data type word size; 'CPU_CFG_TS_TMR_SIZE' MUST be
*                           configured so that ALL bits in 'CPU_TS_TMR' data type are significant.
*
*                           In other words, if timer size is not a binary-multiple of 8-bit octets
*                           (e.g. 20-bits or even 24-bits), then the next lower, binary-multiple
*                           octet word size SHOULD be configured (e.g. to 16-bits).  However, the
*                           minimum supported word size for CPU timestamp timers is 8-bits.
*
*                       See also 'cpu_cfg.h   CPU TIMESTAMP CONFIGURATION  Note #2'
*                              & 'cpu_core.h  CPU TIMESTAMP DATA TYPES     Note #1'.
*
*                   (b) Timer SHOULD be an 'up'  counter whose values increase with each time count.
*
*                   (c) When applicable, timer period SHOULD be less than the typical measured time
*                       but MUST be less than the maximum measured time; otherwise, timer resolution
*                       inadequate to measure desired times.
*
*                   See also 'CPU_TS_TmrRd()  Note #2'.
*********************************************************************************************************
*/

#if (CPU_CFG_TS_TMR_EN == DEF_ENABLED)
void  CPU_TS_TmrInit (void)
{
    CPU_INT32U  fclk_freq;


    fclk_freq = BSP_CPU_ClkFreq();

    BSP_REG_DEM_CR     |= (CPU_INT32U)BSP_BIT_DEM_CR_TRCENA;   /* Enable Cortex-M4's DWT CYCCNT reg.  */
    BSP_REG_DWT_CYCCNT  = (CPU_INT32U)0u;
    BSP_REG_DWT_CR     |= (CPU_INT32U)BSP_BIT_DWT_CR_CYCCNTENA;

    CPU_TS_TmrFreqSet((CPU_TS_TMR_FREQ)fclk_freq);
}
#endif
  Ø 这个函数不需要用户调用,用户可以通过调用函数CPU_Init();(在文件cpu_core.c里面)来调用这个函数。
  Ø 这里时基的实现是使用Cortex-M3/M4中自带的时钟周期计数器,关于时钟周期计数器的知识,大家需要查阅相关的技术手册进行了解,这里只需要知道每计数一次就是一个时钟周期,比如主频是168MHz的话,那么时钟周期就是1/168000000秒。除了μCOS-III是使用的时钟周期计数器作为时基以外,embOS也是使用的这个计数器作为时基。FreeRTOS是用的定时器做的时基。
  Ø 关于此函数的其它信息,需要大家看一下上面的英文说明,看看了解一下即可。
    说完了时间戳的初始化,下面说一下时钟周期计数器时间的读取,相关的函数还是位于bsp.c文件里面。
/*
*********************************************************************************************************
*                                           CPU_TS_TmrRd()
*
* Description : Get current CPU timestamp timer count value.
*
* Argument(s) : none.
*
* Return(s)   : Timestamp timer count (see Notes #2a & #2b).
*
* Caller(s)   : CPU_TS_Init(),
*               CPU_TS_Get32(),
*               CPU_TS_Get64(),
*               CPU_IntDisMeasStart(),
*               CPU_IntDisMeasStop().
*
*               This function is an INTERNAL CPU module function & MUST be implemented by application/
*               BSP function(s) [see Note #1] but SHOULD NOT be called by application function(s).
*
* Note(s)     : (1) CPU_TS_TmrRd() is an application/BSP function that MUST be defined by the developer
*                   if either of the following CPU features is enabled :
*
*                   (a) CPU timestamps
*                   (b) CPU interrupts disabled time measurements
*
*                   See 'cpu_cfg.h  CPU TIMESTAMP CONFIGURATION  Note #1'
*                     & 'cpu_cfg.h  CPU INTERRUPTS DISABLED TIME MEASUREMENT CONFIGURATION  Note #1a'.
*
*               (2) (a) Timer count values MUST be returned via word-size-configurable 'CPU_TS_TMR'
*                       data type.
*
*                       (1) If timer has more bits, truncate timer values' higher-order bits greater
*                           than the configured 'CPU_TS_TMR' timestamp timer data type word size.
*
*                       (2) Since the timer MUST NOT have less bits than the configured 'CPU_TS_TMR'
*                           timestamp timer data type word size; 'CPU_CFG_TS_TMR_SIZE' MUST be
*                           configured so that ALL bits in 'CPU_TS_TMR' data type are significant.
*
*                           In other words, if timer size is not a binary-multiple of 8-bit octets
*                           (e.g. 20-bits or even 24-bits), then the next lower, binary-multiple
*                           octet word size SHOULD be configured (e.g. to 16-bits).  However, the
*                           minimum supported word size for CPU timestamp timers is 8-bits.
*
*                       See also 'cpu_cfg.h   CPU TIMESTAMP CONFIGURATION  Note #2'
*                              & 'cpu_core.h  CPU TIMESTAMP DATA TYPES     Note #1'.
*
*                   (b) Timer SHOULD be an 'up'  counter whose values increase with each time count.
*
*                       (1) If timer is a 'down' counter whose values decrease with each time count,
*                           then the returned timer value MUST be ones-complemented.
*
*                   (c) (1) When applicable, the amount of time measured by CPU timestamps is
*                           calculated by either of the following equations :
*
*                           (A) Time measured  =  Number timer counts  *  Timer period
*
*                                   where
*
*                                       Number timer counts     Number of timer counts measured
*                                       Timer period            Timer's period in some units of
*                                                                   (fractional) seconds
*                                       Time measured           Amount of time measured, in same
*                                                                   units of (fractional) seconds
*                                                                   as the Timer period
*
*                                                  Number timer counts
*                           (B) Time measured  =  ---------------------
*                                                    Timer frequency
*
*                                   where
*
*                                       Number timer counts     Number of timer counts measured
*                                       Timer frequency         Timer's frequency in some units
*                                                                   of counts per second
*                                       Time measured           Amount of time measured, in seconds
*
*                       (2) Timer period SHOULD be less than the typical measured time but MUST be less
*                           than the maximum measured time; otherwise, timer resolution inadequate to
*                           measure desired times.
*********************************************************************************************************
*/

#if (CPU_CFG_TS_TMR_EN == DEF_ENABLED)
CPU_TS_TMR  CPU_TS_TmrRd (void)
{
    CPU_TS_TMR  ts_tmr_cnts;

                                                               
    ts_tmr_cnts = (CPU_TS_TMR)BSP_REG_DWT_CYCCNT;

    return (ts_tmr_cnts);
}
#endif

/*
*********************************************************************************************************
*                                         CPU_TSxx_to_uSec()
*
* Description : Convert a 32-/64-bit CPU timestamp from timer counts to microseconds.
*
* Argument(s) : ts_cnts   CPU timestamp (in timestamp timer counts [see Note #2aA]).
*
* Return(s)   : Converted CPU timestamp (in microseconds           [see Note #2aD]).
*
* Caller(s)   : Application.
*
*               This function is an (optional) CPU module application programming interface (API)
*               function which MAY be implemented by application/BSP function(s) [see Note #1] &
*               MAY be called by application function(s).
*
* Note(s)     : (1) CPU_TS32_to_uSec()/CPU_TS64_to_uSec() are application/BSP functions that MAY be
*                   optionally defined by the developer when either of the following CPU features is
*                   enabled :
*
*                   (a) CPU timestamps
*                   (b) CPU interrupts disabled time measurements
*
*                   See 'cpu_cfg.h  CPU TIMESTAMP CONFIGURATION  Note #1'
*                     & 'cpu_cfg.h  CPU INTERRUPTS DISABLED TIME MEASUREMENT CONFIGURATION  Note #1a'.
*
*               (2) (a) The amount of time measured by CPU timestamps is calculated by either of
*                       the following equations :
*
*                                                                        10^6 microseconds
*                       (1) Time measured  =   Number timer counts   *  -------------------  *  Timer period
*                                                                            1 second
*
*                                              Number timer counts       10^6 microseconds
*                       (2) Time measured  =  ---------------------  *  -------------------
*                                                Timer frequency             1 second
*
*                               where
*
*                                   (A) Number timer counts     Number of timer counts measured
*                                   (B) Timer frequency         Timer's frequency in some units
*                                                                   of counts per second
*                                   (C) Timer period            Timer's period in some units of
*                                                                   (fractional)  seconds
*                                   (D) Time measured           Amount of time measured,
*                                                                   in microseconds
*
*                   (b) Timer period SHOULD be less than the typical measured time but MUST be less
*                       than the maximum measured time; otherwise, timer resolution inadequate to
*                       measure desired times.
*
*                   (c) Specific implementations may convert any number of CPU_TS32 or CPU_TS64 bits
*                       -- up to 32 or 64, respectively -- into microseconds.
*********************************************************************************************************
*/

#if (CPU_CFG_TS_32_EN == DEF_ENABLED)
CPU_INT64U  CPU_TS32_to_uSec (CPU_TS32  ts_cnts)
{
    CPU_INT64U  ts_us;
    CPU_INT64U  fclk_freq;

   
    fclk_freq = BSP_CPU_ClkFreq();   
    ts_us     = ts_cnts / (fclk_freq / DEF_TIME_NBR_uS_PER_SEC);

    return (ts_us);
}
#endif


#if (CPU_CFG_TS_64_EN == DEF_ENABLED)
CPU_INT64U  CPU_TS64_to_uSec (CPU_TS64  ts_cnts)
{
    CPU_INT64U  ts_us;
    CPU_INT64U  fclk_freq;
   

    fclk_freq = BSP_CPU_ClkFreq();   
    ts_us     = ts_cnts / (fclk_freq / DEF_TIME_NBR_uS_PER_SEC);

    return (ts_us);
}
#endif
    从上面的函数可以看出时钟周期计数器时间的获取还是很容易的,直接读取相应寄存器就能获得当前的计数。下面的两个函数CPU_TS32_to_uSec和CPU_TS32_to_uSec用于将时钟周期的数值转换成us为单位。函数还是很容易看懂,上面的英文说明反倒搞得挺复杂,大家读读了解些即可。
9.2.3      函数声明
/*
*********************************************************************************************************
*                                          GLOBAL VARIABLES
*********************************************************************************************************
*/

OS_CPU_EXT  CPU_STK  *OS_CPU_ExceptStkBase;

/*
*********************************************************************************************************
*                                         FUNCTION PROTOTYPES
*********************************************************************************************************
*/

void  OSCtxSw              (void);
void  OSIntCtxSw           (void);
void  OSStartHighRdy       (void);

void  OS_CPU_PendSVHandler (void);


void  OS_CPU_SysTickHandler(void);
void  OS_CPU_SysTickInit   (CPU_INT32U  cnts);
l *OS_CPU_ExceptStkBase  异常堆栈指针,后面讲源码的时候在跟大家讲。
l  void  OSCtxSw              (void);
     void OSIntCtxSw           (void);
     void OSStartHighRdy       (void);
     void OS_CPU_PendSVHandler (void);
    这四个函数在文件os_cpu_a.asm里面,主要用于实现任务切换。
l  void  OS_CPU_SysTickHandler(void);
     void  OS_CPU_SysTickInit   (CPU_INT32U cnts);
    这两个函数的实体在文件os_cpu_c.c文件里面。

使用特权

评论回复
板凳
Eric2013|  楼主 | 2014-12-22 16:33 | 只看该作者
本帖最后由 Eric2013 于 2014-12-22 16:34 编辑

9.3  os_cpu_c.c文件讲解
    这文件里面主要是一些钩子函数,任务堆栈初始化和嘀嗒定时器的初始化。下面就详细的把这三个部分说明一下。
9.3.1      钩子函数
    钩子函数的主要功能就是扩展函数的功能,用户可以根据自己的需要往里面添加相关的测试函数,这个文件里面的钩子函数一共有个,下面就分别的讲解下。
/*
*********************************************************************************************************
*                                           IDLE TASK HOOK
*
* Description: This function is called by the idle task.  This hook has been added to allow you to do
*              such things as STOP the CPU to conserve power.
*
* Arguments  : None.
*
* Note(s)    : None.
*********************************************************************************************************
*/

void  OSIdleTaskHook (void)
{
#if OS_CFG_APP_HOOKS_EN > 0u
    if (OS_AppIdleTaskHookPtr != (OS_APP_HOOK_VOID)0) {
        (*OS_AppIdleTaskHookPtr)();
    }
#endif
}
l OSIdleTaskHook是空闲任务的钩子函数
    此函数被空闲任务调用,用户可以通过函数指针变量*OS_AppIdleTaskHookPtr指向用户设计的函数来实现用户所想实现的任务。这个函数主要用于实现低功耗。
/*
*********************************************************************************************************
*                                       OS INITIALIZATION HOOK
*
* Description: This function is called by OSInit() at the beginning of OSInit().
*
* Arguments  : None.
*
* Note(s)    : None.
*********************************************************************************************************
*/

void  OSInitHook (void)
{
                                                /* 8-byte align the ISR stack.     */   
    OS_CPU_ExceptStkBase = (CPU_STK *)(OSCfg_ISRStkBasePtr + OSCfg_ISRStkSize);
    OS_CPU_ExceptStkBase = (CPU_STK *)((CPU_STK)(OS_CPU_ExceptStkBase) & 0xFFFFFFF8);
}
l OSInitHook是OS初始化的钩子函数
    这个函数主要用于获取异常堆栈的基地址。这里这特别注意:这个异常堆栈是用于MSP(主堆栈空间),这样可以很方便的查询主堆栈的使用情况。针对Cortex-M4/M3内核的MCU,在使用RTOS的情况下,主堆栈主要用在中断服务程序中。
/*
*********************************************************************************************************
*                                         STATISTIC TASK HOOK
*
* Description: This function is called every second by uC/OS-III's statistics task.  This allows your
*              application to add functionality to the statistics task.
*
* Arguments  : None.
*
* Note(s)    : None.
*********************************************************************************************************
*/

void  OSStatTaskHook (void)
{
#if OS_CFG_APP_HOOKS_EN > 0u
    if (OS_AppStatTaskHookPtr != (OS_APP_HOOK_VOID)0) {
        (*OS_AppStatTaskHookPtr)();
    }
#endif
}
l OSStatTaskHook是统计任务的钩子函数
     使用方法和前面的空闲任务钩子函数一样。
/*
*********************************************************************************************************
*                                          TASK CREATION HOOK
*
* Description: This function is called when a task is created.
*
* Arguments  : p_tcb        Pointer to the task control block of the task being created.
*
* Note(s)    : None.
*********************************************************************************************************
*/

void  OSTaskCreateHook (OS_TCB  *p_tcb)
{
#if OS_CFG_APP_HOOKS_EN > 0u
    if (OS_AppTaskCreateHookPtr != (OS_APP_HOOK_TCB)0) {
        (*OS_AppTaskCreateHookPtr)(p_tcb);
    }
#else
    (void)p_tcb;                                            /* Prevent compiler warning                               */
#endif
}
l OSTaskCreateHook是创建任务时的钩子函数
    使用方法和前面的空闲任务钩子函数基本一样,只是多了个函数参数。
/*
*********************************************************************************************************
*                                           TASK DELETION HOOK
*
* Description: This function is called when a task is deleted.
*
* Arguments  : p_tcb        Pointer to the task control block of the task being deleted.
*
* Note(s)    : None.
*********************************************************************************************************
*/

void  OSTaskDelHook (OS_TCB  *p_tcb)
{
#if OS_CFG_APP_HOOKS_EN > 0u
    if (OS_AppTaskDelHookPtr != (OS_APP_HOOK_TCB)0) {
        (*OS_AppTaskDelHookPtr)(p_tcb);
    }
#else
    (void)p_tcb;                                            /* Prevent compiler warning                               */
#endif
}
l OSTaskDelHook 是任务删除的钩子函数
    使用方法和前面的创建任务钩子函数一样。
/*
*********************************************************************************************************
*                                            TASK RETURN HOOK
*
* Description: This function is called if a task accidentally returns.  In other words, a task should
*              either be an infinite loop or delete itself when done.
*
* Arguments  : p_tcb        Pointer to the task control block of the task that is returning.
*
* Note(s)    : None.
*********************************************************************************************************
*/

void  OSTaskReturnHook (OS_TCB  *p_tcb)
{
#if OS_CFG_APP_HOOKS_EN > 0u
    if (OS_AppTaskReturnHookPtr != (OS_APP_HOOK_TCB)0) {
        (*OS_AppTaskReturnHookPtr)(p_tcb);
    }
#else
    (void)p_tcb;                                            /* Prevent compiler warning                               */
#endif
}
l  OSTaskReturnHook是任务返回的钩子函数
    这个函数的使用方面和前面的创建任务钩子函数是一样的。在RTOS中,任务必须得是超级循环的形式,如果任务只执行一次或者任务中有返回函数,系统就会调用异常返回函数将这个任务挂起或者删除(用户可以自行配置)。在异常返回函数中调用钩子函数。
/*
*********************************************************************************************************
*                                           TASK SWITCH HOOK
*
* Description: This function is called when a task switch is performed.  This allows you to perform other
*              operations during a context switch.
*
* Arguments  : None.
*
* Note(s)    : 1) Interrupts are disabled during this call.
*              2) It is assumed that the global pointer 'OSTCBHighRdyPtr' points to the TCB of the task
*                 that will be 'switched in' (i.e. the highest priority task) and, 'OSTCBCurPtr' points
*                 to the task being switched out (i.e. the preempted task).
*********************************************************************************************************
*/

void  OSTaskSwHook (void)
{
#if OS_CFG_TASK_PROFILE_EN > 0u
    CPU_TS  ts;
#endif
#ifdef  CPU_CFG_INT_DIS_MEAS_EN
    CPU_TS  int_dis_time;
#endif



#if OS_CFG_APP_HOOKS_EN > 0u
    if (OS_AppTaskSwHookPtr != (OS_APP_HOOK_VOID)0) {
        (*OS_AppTaskSwHookPtr)();
    }
#endif

#if OS_CFG_TASK_PROFILE_EN > 0u
    ts = OS_TS_GET();
    if (OSTCBCurPtr != OSTCBHighRdyPtr) {
        OSTCBCurPtr->CyclesDelta  = ts - OSTCBCurPtr->CyclesStart;
        OSTCBCurPtr->CyclesTotal += (OS_CYCLES)OSTCBCurPtr->CyclesDelta;
    }

    OSTCBHighRdyPtr->CyclesStart = ts;
#endif

#ifdef  CPU_CFG_INT_DIS_MEAS_EN
    int_dis_time = CPU_IntDisMeasMaxCurReset();  /* Keep track of per-task interrupt disable time  */
    if (OSTCBCurPtr->IntDisTimeMax < int_dis_time) {
        OSTCBCurPtr->IntDisTimeMax = int_dis_time;
    }
#endif

#if OS_CFG_SCHED_LOCK_TIME_MEAS_EN > 0u
                                               /* Keep track of per-task scheduler lock time    */
    if (OSTCBCurPtr->SchedLockTimeMax < OSSchedLockTimeMaxCur) {
        OSTCBCurPtr->SchedLockTimeMax = OSSchedLockTimeMaxCur;
    }
    OSSchedLockTimeMaxCur = (CPU_TS)0;         /* Reset the per-task value                     */
#endif
}
l  OSTaskSwHook 是任务切换的钩子函数
    这个函数里面也有用到函数指针,使用方法和前面的函数是一样的。函数里面还有一些执行时间的测量,这些内容会在后面源码讲解的时候再跟大家分析。
/*
*********************************************************************************************************
*                                              TICK HOOK
*
* Description: This function is called every tick.
*
* Arguments  : None.
*
* Note(s)    : 1) This function is assumed to be called from the Tick ISR.
*********************************************************************************************************
*/

void  OSTimeTickHook (void)
{
#if OS_CFG_APP_HOOKS_EN > 0u
    if (OS_AppTimeTickHookPtr != (OS_APP_HOOK_VOID)0) {
        (*OS_AppTimeTickHookPtr)();
    }
#endif
}
l OSTimeTickHook 是嘀嗒定时器钩子函数
    这个函数的用法和前面的函数一样,被嘀嗒定时器中断服务程序所调用。

9.3.2      嘀嗒定时器配置
    关于嘀嗒定时器在V5开发板用户手册第10章:SysTick实验里面有详细的讲解。这里就不再详细的赘述了,这里特别注意咱们已经在前面的os_cpu.h文件里面配置了嘀嗒定时器的优先级为0,也就是最高优先级。
/*
*********************************************************************************************************
*                                          SYS TICK HANDLER
*
* Description: Handle the system tick (SysTick) interrupt, which is used to generate the uC/OS-II tick
*              interrupt.
*
* Arguments  : None.
*
* Note(s)    : 1) This function MUST be placed on entry 15 of the Cortex-M3 vector table.
*********************************************************************************************************
*/

void  OS_CPU_SysTickHandler (void)
{
    CPU_SR_ALLOC();


    CPU_CRITICAL_ENTER();
    OSIntNestingCtr++;                                      /* Tell uC/OS-III that we are starting an ISR             */
    CPU_CRITICAL_EXIT();

    OSTimeTick();                                           /* Call uC/OS-III's OSTimeTick()                          */

    OSIntExit();                                            /* Tell uC/OS-III that we are leaving the ISR             */
}

/*
*********************************************************************************************************
*                                         INITIALIZE SYS TICK
*
* Description: Initialize the SysTick.
*
* Arguments  : cnts         Number of SysTick counts between two OS tick interrupts.
*
* Note(s)    : 1) This function MUST be called after OSStart() & after processor initialization.
*********************************************************************************************************
*/

void  OS_CPU_SysTickInit (CPU_INT32U  cnts)
{
    CPU_INT32U  prio;


    CPU_REG_NVIC_ST_RELOAD = cnts - 1u;

                                                            /* Set SysTick handler prio.                              */
    prio                   = CPU_REG_NVIC_SHPRI3;
    prio                  &= DEF_BIT_FIELD(24, 0);
    prio                  |= DEF_BIT_MASK(OS_CPU_CFG_SYSTICK_PRIO, 24);

    CPU_REG_NVIC_SHPRI3    = prio;

                                                            /* Enable timer.                                          */
    CPU_REG_NVIC_ST_CTRL  |= CPU_REG_NVIC_ST_CTRL_CLKSOURCE |
                             CPU_REG_NVIC_ST_CTRL_ENABLE;
                                                            /* Enable timer interrupt.                                */
    CPU_REG_NVIC_ST_CTRL  |= CPU_REG_NVIC_ST_CTRL_TICKINT;
}

9.3.3      任务堆栈初始化
    每个任务创建时都会调用这个函数,主要用于初始化任务的堆栈,以便于第一次切换到各个任务时能正确的切换(特别注意这句话,用心体会)。
/*
**********************************************************************************************************
*                                       INITIALIZE A TASK'S STACK
*
* Description: This function is called by OS_Task_Create() or OSTaskCreateExt() to initialize the stack
*              frame of the task being created. This function is highly processor specific.
*
* Arguments  : p_task       Pointer to the task entry point address.
*
*              p_arg        Pointer to a user supplied data area that will be passed to the task
*                               when the task first executes.
*
*              p_stk_base   Pointer to the base address of the stack.
*
*              stk_size     Size of the stack, in number of CPU_STK elements.
*
*              opt          Options used to alter the behavior of OS_Task_StkInit().
*                            (see OS.H for OS_TASK_OPT_xxx).
*
* Returns    : Always returns the location of the new top-of-stack' once the processor registers have
*              been placed on the stack in the proper order.
*
* Note(s)    : 1) Interrupts are enabled when task starts executing.
*
*              2) All tasks run in Thread mode, using process stack.
**********************************************************************************************************
*/

CPU_STK  *OSTaskStkInit (OS_TASK_PTR    p_task,
                         void          *p_arg,
                         CPU_STK       *p_stk_base,
                         CPU_STK       *p_stk_limit,
                         CPU_STK_SIZE   stk_size,
                         OS_OPT         opt)
{
    CPU_STK  *p_stk;


    (void)opt;                             /* Prevent compiler warning                               */

    p_stk = &p_stk_base[stk_size];         /* Load stack pointer                                     */
                                           /* Align the stack to 8-bytes.                            */
    p_stk = (CPU_STK *)((CPU_STK)(p_stk) & 0xFFFFFFF8);
                                           /* Registers stacked as if auto-saved on exception        */
    *--p_stk = (CPU_STK)0x01000000u;       /* xPSR                                                   */
    *--p_stk = (CPU_STK)p_task;            /* Entry Point                                            */
    *--p_stk = (CPU_STK)OS_TaskReturn;     /* R14 (LR)                                               */
    *--p_stk = (CPU_STK)0x12121212u;       /* R12                                                    */
    *--p_stk = (CPU_STK)0x03030303u;       /* R3                                                     */
    *--p_stk = (CPU_STK)0x02020202u;       /* R2                                                     */
    *--p_stk = (CPU_STK)p_stk_limit;       /* R1                                                     */
    *--p_stk = (CPU_STK)p_arg;               /* R0 : argument                                          */
                                             /* Remaining registers saved on process stack             */
    *--p_stk = (CPU_STK)0x11111111u;         /* R11                                                    */
    *--p_stk = (CPU_STK)0x10101010u;         /* R10                                                    */
    *--p_stk = (CPU_STK)0x09090909u;         /* R9                                                     */
    *--p_stk = (CPU_STK)0x08080808u;         /* R8                                                     */
    *--p_stk = (CPU_STK)0x07070707u;         /* R7                                                     */
    *--p_stk = (CPU_STK)0x06060606u;         /* R6                                                     */
    *--p_stk = (CPU_STK)0x05050505u;         /* R5                                                     */
    *--p_stk = (CPU_STK)0x04040404u;         /* R4                                                     */

    return (p_stk);
}
    看似简单的初始化,信息量还是很大的。
  l 任务堆栈空间的8字节对齐。
  l 任务堆栈的初始化顺序,先是自动入栈的8个寄存器xPSR、PC、LR、R14、R12、R3、R2、R1、R0,然后是手动入栈的8个寄存器R4-R11。
  l 由于M4内核的堆栈生长方向是向下生长的满栈,所以初始化的时候先获得任务堆栈的末地址,然后依次递减存储这些寄存器。
  l 特别注意这句初始化:*--p_stk = (CPU_STK)OS_TaskReturn; 它的作用就是防止用户写的任务不是超级循环,而出现任务返回的情况,如果任务返回的话就会返回到OS_TaskReturn这个函数里面。
    假如任务1的堆栈控件是uint32_t  stack[1024],那么堆栈初始化后寄存器在堆栈中的排列如下:


使用特权

评论回复
地板
Eric2013|  楼主 | 2014-12-22 16:39 | 只看该作者
本帖最后由 Eric2013 于 2014-12-22 16:46 编辑

9.4  os_cpu_a.asm文件讲解
    这个文件中函数的主要作用是实现任务切换,如果大家第5章的教程学懂了,这里面的函数要简单很多,μCOS-III中没有使用到SVC,只是使用了PendSV。所以大家只需懂得PendSV的使用即可。
9.4.1      开启多任务
NVIC_INT_CTRL   EQU     0xE000ED04                              ; Interruptcontrol state register.
NVIC_SYSPRI14   EQU     0xE000ED22                              ; System priorityregister (priority 14).
NVIC_PENDSV_PRI EQU          0xFF                              ; PendSV priority value (lowest).
NVIC_PENDSVSET  EQU     0x10000000                              ; Value totrigger PendSV exception.

;********************************************************************************************************
;                                        START MULTITASKING
;                                     void OSStartHighRdy(void)
;
; Note(s) : 1) This function triggers a PendSV exception (essentially,causes a context switch) to cause
;              the first task tostart.
;
;           2) OSStartHighRdy()MUST:
;              a) Setup PendSVexception priority to lowest;
;              b) Set initial PSP to0, to tell context switcher this is first run;
;              c) Set the main stackto OS_CPU_ExceptStkBase
;              d) Trigger PendSVexception;
;              e) Enable interrupts(tasks will run with interrupts enabled).
;********************************************************************************************************

OSStartHighRdy
    LDR     R0, =NVIC_SYSPRI14                          ; Set the PendSVexception priority
    LDR     R1, =NVIC_PENDSV_PRI
    STRB    R1, [R0]

    MOVS    R0, #0                                      ; Set the PSP to 0 forinitial context switch call
    MSR     PSP, R0

    LDR     R0, =OS_CPU_ExceptStkBase                   ; Initialize the MSP to theOS_CPU_ExceptStkBase
    LDR     R1, [R0]
    MSR     MSP, R1   

    LDR     R0, =NVIC_INT_CTRL                          ; Trigger the PendSVexception (causes context switch)
    LDR     R1, =NVIC_PENDSVSET
    STR     R1, [R0]

    CPSIE   I                                           ;Enable interrupts at processor level

OSStartHang
    B       OSStartHang                                 ; Should never get here
    下面说一下上面的汇编代码都实现了什么操作。
l LDR     R0, =NVIC_SYSPRI14                             
  LDR     R1, =NVIC_PENDSV_PRI
  STRB    R1, [R0]
这三段代码的主要功能设置PendSV,这里将其设置位最低优先级,为什么要设置位最低优先级?在前面的教程中已经说得很清楚了,这里就不再赘述了。
l MOVS    R0, #0                                       
  MSR     PSP, R0
设置PSP = 0,是告诉具体的任务切换程序(OS_CPU_PendSVHandler()),这是第一次任务切换。做过切换后PSP就不会为0了,后面讲OS_CPU_PendSVHandler()时会看到。
l LDR     R0, =OS_CPU_ExceptStkBase                 
  LDR     R1, [R0]
  MSR     MSP,R1   
设置MSP = OS_CPU_ExceptStkBase,开启多任务后MSP主要用于中断服务程序。
l LDR     R0, =NVIC_INT_CTRL                                
  LDR     R1,=NVIC_PENDSVSET
  STR     R1, [R0]
这里是使能PendSV中断。
l CPSIE   I                                                
  OSStartHang
  B      OSStartHang  
这里主要是使能全局中断,使能后如果没有其它高优先级的中断需要执行,就会立即响应PendSV中断。所以说如果程序运行到了B    OSStartHang,说明多任务启动失败了。

9.4.2      任务切换使能
    主要有两个任务切换的函数,一个是任务级的任务切换,另一个是中断级的任务切换。这两个函数都只是使能PendSV中断,任务切换的实际工作是在PendSV中完成的。
;********************************************************************************************************
;                       PERFORM A CONTEXT SWITCH (From task level) - OSCtxSw()
;
; Note(s) : 1) OSCtxSw() is called when OS wants to perform a task context switch.  This function
;              triggers the PendSV exception which is where the real work is done.
;********************************************************************************************************

OSCtxSw
    LDR     R0, =NVIC_INT_CTRL                           ; Trigger the PendSV exception (causes context switch)
    LDR     R1, =NVIC_PENDSVSET
    STR     R1, [R0]
    BX      LR

;********************************************************************************************************
;                   PERFORM A CONTEXT SWITCH (From interrupt level) - OSIntCtxSw()
;
; Note(s) : 1) OSIntCtxSw() is called by OSIntExit() when it determines a context switch is needed as
;              the result of an interrupt.  This function simply triggers a PendSV exception which will
;              be handled when there are no more interrupts active and interrupts are enabled.
;********************************************************************************************************

OSIntCtxSw
    LDR     R0, =NVIC_INT_CTRL                          ; Trigger the PendSV exception (causes context switch)
    LDR     R1, =NVIC_PENDSVSET
    STR     R1, [R0]
    BX      LR

9.4.3      PendSV中断
    关于PendSV中断,我们在第4章和第5章已经非常详细的讲解了,这里主要是给大家分析一下任务切换的处理过程,这个中断函数的前面有英文的注释说的非常好,大家有必要看一下。
;********************************************************************************************************
;                                       HANDLE PendSV EXCEPTION
;                                   void OS_CPU_PendSVHandler(void)
;
; Note(s) : 1) PendSV is used to cause a context switch.  This is a recommended method for performing
;              context switches with Cortex-M3.  This is because the Cortex-M3 auto-saves half of the
;              processor context on any exception, and restores same on return from exception.  So only
;              saving of R4-R11 is required and fixing up the stack pointers.  Using the PendSV exception
;              this way means that context saving and restoring is identical whether it is initiated from
;              a thread or occurs due to an interrupt or exception.
;
;           2) Pseudo-code is:
;              a) Get the process SP, if 0 then skip (goto d) the saving part (first context switch);
;              b) Save remaining regs r4-r11 on process stack;
;              c) Save the process SP in its TCB, OSTCBCurPtr->OSTCBStkPtr = SP;
;              d) Call OSTaskSwHook();
;              e) Get current high priority, OSPrioCur = OSPrioHighRdy;
;              f) Get current ready thread TCB, OSTCBCurPtr = OSTCBHighRdyPtr;
;              g) Get new process SP from TCB, SP = OSTCBHighRdyPtr->OSTCBStkPtr;
;              h) Restore R4-R11 from new process stack;
;              i) Perform exception return which will restore remaining context.
;
;           3) On entry into PendSV handler:
;              a) The following have been saved on the process stack (by processor):
;                 xPSR, PC, LR, R12, R0-R3
;              b) Processor mode is switched to Handler mode (from Thread mode)
;              c) Stack is Main stack (switched from Process stack)
;              d) OSTCBCurPtr      points to the OS_TCB of the task to suspend
;                 OSTCBHighRdyPtr  points to the OS_TCB of the task to resume
;
;           4) Since PendSV is set to lowest priority in the system (by OSStartHighRdy() above), we
;              know that it will only be run when no other exception or interrupt is active, and
;              therefore safe to assume that context being switched out was using the process stack (PSP).
;********************************************************************************************************

OS_CPU_PendSVHandler
    CPSID   I                                           ; Prevent interruption during context switch
    MRS     R0, PSP                                     ; PSP is process stack pointer
    CBZ     R0, OS_CPU_PendSVHandler_nosave             ; Skip register save the first time

    SUBS    R0, R0, #0x20                               ; Save remaining regs r4-11 on process stack
    STM     R0, {R4-R11}

    LDR     R1, =OSTCBCurPtr                            ; OSTCBCurPtr->OSTCBStkPtr = SP;
    LDR     R1, [R1]
    STR     R0, [R1]                                     ; R0 is SP of process being switched out

                                                     ; At this point, entire context of process has been saved
OS_CPU_PendSVHandler_nosave
    PUSH    {R14}                                               ; Save LR exc_return value
    LDR     R0, =OSTaskSwHook                                   ; OSTaskSwHook();
    BLX     R0
    POP     {R14}

    LDR     R0, =OSPrioCur                                      ; OSPrioCur   = OSPrioHighRdy;
    LDR     R1, =OSPrioHighRdy
    LDRB    R2, [R1]
    STRB    R2, [R0]

    LDR     R0, =OSTCBCurPtr                                    ; OSTCBCurPtr = OSTCBHighRdyPtr;
    LDR     R1, =OSTCBHighRdyPtr
    LDR     R2, [R1]
    STR     R2, [R0]

    LDR     R0, [R2]                                            ; R0 is new process SP; SP = OSTCBHighRdyPtr->StkPtr;
    LDM     R0, {R4-R11}                                        ; Restore r4-11 from new process stack
    ADDS    R0, R0, #0x20
    MSR     PSP, R0                                             ; Load PSP with new process SP
    ORR     LR, LR, #0x04                                       ; Ensure exception return uses process stack
    CPSIE   I
    BX      LR                                                  ; Exception return will restore remaining context

    END
    这里PendSV中断的主要作用就是保存当前任务的现场,恢复下一个任务的寄存器,从而切换到下一个任务中执行程序,并保证退出PendSV后使用的是PSP (任务堆栈指针)。下面先把这些汇编代码解释一下:
OS_CPU_PendSVHandler                       ;xPSR, PC, LR, R12, R0-R3已自动保存入栈
CPSID   I                                  ;任务切换期间需要关中断
MRS     R0, PSP                            ;R0 = PSP
CBZ     R0, OS_CPU_PendSVHandler_nosave    ;如果PSP==0跳转到OS_CPU_PendSVHandler_nosave去执行 在多任务
;的初始化时PSP被初始化为0,表示任务是第一次运行,不需要压栈

/********************************将当前的任务寄存器入栈*********************************************/
SUBS    R0, R0, #0x20        ;R0 -= 0x20 保存R4-R11到任务堆栈 共32个字节            
STM     R0, {R4-R11}         ;压栈R4-R11
LDR     R1, =OSTCBCur        ;获取OSTCBCur->OSTCBStkPtr,
;OSTCBStkPtr是OSTCBCur 中的第一个变量                           
LDR     R1, [R1]             ;R1 = *R1 (R1 = OSTCBCur)
STR     R0, [R1]             ;*R1 = R0 (*OSTCBCur = SP)
                             ;将当前任务的堆栈保存到自己的任务控制块
;OSTCBCur->OSTCBStkPtr = PSP
;程序运行此位置,已经保存了当前任务的寄存器。
                           
OS_CPU_PendSVHandler_nosave
PUSH    {R14}                ;R14 入栈
LDR     R0, =OSTaskSwHook    ;OSTaskSwHook(); R0 = &OSTaskSwHook
BLX     R0                   ;调用OSTaskSwHook()
POP     {R14}                ;恢复R14

/********************************加载要执行任务的寄存器*********************************************/
LDR     R0, =OSPrioCur      ;R0 = &OSPrioCur
LDR     R1, =OSPrioHighRdy  ;R1 = &OSPrioHighRdy
LDRB    R2, [R1]            ;R2 = *R1
STRB    R2, [R0]            ;*R0 = R2
                            ;上面这四句函数的功能就是OSPrioCur = OSPrioHighRdy

LDR     R0, =OSTCBCur       ;R0 = &OSTCBCur
LDR     R1, =OSTCBHighRdy   ;R1 = &OSTCBHighRdy
LDR     R2, [R1]            ;R2 = *R1
STR     R2, [R0]            ;*R0 = R2
;上面这四句函数的功能就是OSTCBCur = OSTCBHighRdy

LDR     R0, [R2]            ;R0 = *R2 也就是R0 = OSTCBHighRdy又因为SP = OSTCBHighRdy->OSTCBStkPtr
                            ;这条指令的含义就是R0=PSP。
LDM     R0, {R4-R11}        ;从任务堆栈恢复R4-R11
ADDS    R0, R0, #0x20       ;调整R0 += 0x20
MSR     PSP, R0             ;PSP = R0
ORR     LR, LR, #0x04     ; 确保LR位2为1,返回后使用进程堆栈PSP
CPSIE   I                 ; 开中断
BX      LR                ; 中断返回,剩下的8个寄存器会自动的出栈。
END
    为了帮助大家更好的理解这个过程,这里举一个简单的例子:
l   第一步:开始多任务后,系统就会从咱们上面说的OSStartHighRdy进入到PendSV中断,进入中断前使用的是MSP,也就是说自动入栈的8个寄存器被压入到MSP所指向的堆栈空间。
l   第二步:进入中断后开始MSP(因为中断程序中只能使用MSP,而不能使用PSP)。由于是第一次进入执行PendSV,剩下的8个需要手动入栈的寄存器不需要执行入栈操作(为什么不需要入栈操作? 因为第一次执行PendSV前的函数只是μCOS-III的初始化,而不是需要执行的任务,所以现场不需要保存)。
l   第三步:获得当前需要执行的最高优先级任务A后,程序就会从任务A的堆栈空间恢复需要手动入栈的8个寄存器R4-R11,在退出PendSV后,剩下的8个寄存器内容会自动得到恢复。由于前面初始化任务堆栈的时候,已经指定了任务的地址,所以这时就能正确的切换到任务A中。
l   第四步:当任务A被挂起或者其它的原因,系统再次进入到PendSV中断,进入中断前自动入栈的8个寄存器已经被保存到任务A的堆栈空间,进入中断后再将剩余8个需要手动入栈的8个寄存器推入任务A的堆栈空间,并保存PSP到任务A的TCB控制块,剩下执行的操作就和第三步相同了,获取另一个需要执行的高优先级任务B。初学的同学一定要不这个过程分析透,并牢记。这个过程很重要,要不任务怎么切换的都不知道。
9.5  总结
    希望初学的同学将**所讲的三个文件自行分析一下,特别是PendSV中断函数的内容。本期教程的内容理解透了,对于后面学习源码大有裨益。




参考资料:
1.       www.micrium.com

使用特权

评论回复
5
mmuuss586| | 2014-12-22 22:15 | 只看该作者

这么多,支持下楼主;

使用特权

评论回复
6
Eric2013|  楼主 | 2014-12-23 11:46 | 只看该作者
mmuuss586 发表于 2014-12-22 22:15
这么多,支持下楼主;

谢谢:handshake

使用特权

评论回复
7
monkeypony| | 2014-12-30 21:59 | 只看该作者
多谢分享

使用特权

评论回复
8
redgrain| | 2017-5-17 09:49 | 只看该作者
值得学习,谢谢分享!

使用特权

评论回复
9
yqdz2005| | 2017-5-18 11:45 | 只看该作者
谢谢分享!

使用特权

评论回复
发新帖 我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

个人签名:RTX->μCOS-II->FreeRTOS->embOS->μCOS-III μCGUI->emWin->FatFs->DSP 淘宝:armfly.taobao.com

115

主题

639

帖子

34

粉丝