第3.1章 SYSTEM文件夹介绍(一)

第三章 SYSTEM文件夹介绍(一)

我们介绍了如何在 MDK5 下建立 STM32F4 工程。 在这个新建的工程之中,我们用到了一个 SYSTEM 文件夹里面的代码,此文件夹里面的代码由 ALIENTEK 提供,是STM32F4xx 系列的底层核心驱动函数,可以用在 STM32F4xx 系列的各个型号上面,方便大家快速构建自己的工程。

SYSTEM 文件夹下包含了 delay、 sys、 usart 等三个文件夹。分别包含了 delay.c、 sys.c、 usart.c及其头文件。通过这 3 个 c 文件,可以快速的给任何一款 STM32F4 构建最基本的框架。使用起来是很方便的。

本章, 我们将向大家介绍这些代码,通过这章的学习,大家将了解到这些代码的由来,也希望大家可以灵活使用 SYSTEM 文件夹提供的函数,来快速构建工程,并实际应用到自己的项目中去。

1. delay文件夹代码介绍

delay 文件夹内包含了 delay.c 和 delay.h 两个文件,这两个文件用来实现系统的延时功能,其中包含 7 个函数:

  • void delay_osschedlock(void); --- os

  • void delay_osschedunlock(void); --- os

  • void delay_ostimedly(u32 ticks); --- os

  • void SysTick_Handler(void); --- os

  • void delay_init(u8 SYSCLK);

  • void delay_ms(u16 nms);

  • void delay_us(u32 nus);

前面 4 个函数,仅在支持操作系统(OS)的时候,需要用到,而后面 3 个函数,则不论是否支持 OS 都需要用到。

在介绍这些函数之前,我们先了解一下编程思想: CM4 内核的处理和 CM3 一样,内部都包含了一个 SysTick 定时器, SysTick 是一个 24 位的倒计数定时器,当计到 0 时,将从 RELOAD寄存器中自动重装载定时初值。只要不把它在 SysTick 控制及状态寄存器中的使能位清除,就永不停息。

这里我们将介绍的是 ALIENTEK 提供的最新版本的延时函数,该版本的延时函数支持在任意操作系统(OS)下面使用,它可以和操作系统共用 SysTick 定时器。

这里,我们以 UCOSII 为例,介绍如何实现操作系统和我们的 delay 函数共用 SysTick 定时器。首先,我们简单介绍下 UCOSII 的时钟: ucos 运行需要一个系统时钟节拍(类似 “心跳”),而这个节拍是固定的(由 OS_TICKS_PER_SEC 宏定义设置),比如要求 5ms 一次(即可设置: OS_TICKS_PER_SEC = 200),在 STM32 上面,一般是由 SysTick 来提供这个节拍,也就是 SysTick要设置为 5ms 中断一次,为 ucos 提供时钟节拍,而且这个时钟一般是不能被打断的(否则就不准了)。

因为在 ucos 下 systick 不能再被随意更改,如果我们还想利用 systick 来做 delay_us 或者delay_ms 的延时,就必须想点办法了,这里我们利用的是时钟摘取法。以 delay_us 为例,比如delay_us(50),在刚进入 delay_us 的时候先计算好这段延时需要等待的 systick 计数次数,这里为 5021(假设系统时钟为 168Mhz,因为 systick 的频率为系统时钟频率的 1/8,那么 systick每增加 1,就是 1/21us),然后我们就一直统计 systick 的计数变化,直到这个值变化了 50x21,一旦检测到变化达到或者超过这个值,就说明延时 50us 时间到了。这样,我们只是抓取 SysTick计数器的变化,并不需要修改 SysTick 的任何状态,完全不影响 SysTick 作为 UCOS 时钟节拍的功能,这就是实现 delay 和操作系统共用 SysTick 定时器的原理。

下面我们开始介绍这几个函数:

1.1 操作系统支持宏定义及相关函数

当需要 delay_ms 和 delay_us 支持操作系统(OS)的时候,我们需要用到 3 个宏定义和 4个函数,宏定义及函数代码如下:

//本例程仅作 UCOSII 和 UCOSIII 的支持,其他 OS,请自行参考着移植

// 支持 UCOSII
#ifdef OS_CRITICAL_METHOD // OS_CRITICAL_METHOD 定义了,说明要支持 UCOSII
#define delay_osrunning OSRunning // OS 是否运行标记,0,不运行;1,在运行
#define delay_ostickspersec OS_TICKS_PER_SEC // OS 时钟节拍,即每秒调度次数
#define delay_osintnesting OSIntNesting // 中断嵌套级别,即中断嵌套次数
#endif

// 支持 UCOSIII
#ifdef CPU_CFG_CRITICAL_METHOD // CPU_CFG_CRITICAL_METHOD 定义了,说明要支持 UCOSIII
#define delay_osrunning OSRunning // OS 是否运行标记,0,不运行;1,在运行
#define delay_ostickspersec OSCfg_TickRate_Hz // OS 时钟节拍,即每秒调度次数
#define delay_osintnesting OSIntNestingCtr // 中断嵌套级别,即中断嵌套次数
#endif

// us级延时时,关闭任务调度(防止打断 us 级延迟)
void delay_osschedlock(void)
{
#ifdef CPU_CFG_CRITICAL_METHOD // 使用 UCOSIII
    OS_ERR err;
    OSSchedLock(&err); // UCOSIII 的方式,禁止调度,防止打断 us 延时
#else //否则 UCOSII
    OSSchedLock(); //UCOSII 的方式,禁止调度,防止打断 us 延时
#endif
}

// us级延时,恢复任务调度
void delay_osschedunlock(void)
{
#ifdef CPU_CFG_CRITICAL_METHOD // 使用 UCOSIII
    OS_ERR err;
    OSSchedUnlock(&err); // UCOSIII 的方式,恢复调度
#else // 否则 UCOSII
    OSSchedUnlock(); // UCOSII 的方式,恢复调度
#endif
}

// 调用 OS 自带的延时函数延时
// ticks:延时的节拍数
void delay_ostimedly(u32 ticks)
{
#ifdef CPU_CFG_CRITICAL_METHOD
    OS_ERR err;
    OSTimeDly(ticks,OS_OPT_TIME_PERIODIC,&err); // UCOSIII 延时采用周期模式
#else
    OSTimeDly(ticks); // UCOSII 延时
#endif
}
// systick 中断服务函数,使用 OS 时用到
void SysTick_Handler(void)
{
    HAL_IncTick();
    if(delay_osrunning == 1) // OS 开始跑了,才执行正常的调度处理
    {
        OSIntEnter(); // 进入中断
        OSTimeTick(); // 调用 ucos 的时钟服务程序
        OSIntExit();  // 触发任务切换软中断
    }
}
#endif

以上代码,仅支持 UCOSII 和 UCOSIII,不过,对于其他 OS 的支持,也只需要对以上代码进行简单修改即可实现。

1.1.1 支持OS需要用到的三个宏定义(以UCOSII为例)

#define delay_osrunning OSRunning // OS 是否运行标记,0,不运行;1,在运行
#define delay_ostickspersec OS_TICKS_PER_SEC // OS 时钟节拍,即每秒调度次数
#define delay_osintnesting OSIntNesting // 中断嵌套级别,即中断嵌套次数

宏定义: delay_osrunning,用于标记 OS 是否正在运行,当 OS 已经开始运行时,该宏定义值为 1,当 OS 还未运行时,该宏定义值为 0。

宏定义: delay_ ostickspersec,用于表示 OS 的时钟节拍,即 OS 每秒钟任务调度次数。宏定义: delay_ osintnesting,用于表示 OS 中断嵌套级别,即中断嵌套次数,每进入一个中断,该值加 1,每退出一个中断,该值减 1。

1.1.2 支持OS需要用到的4个函数

函数: delay_osschedlock,用于 delay_us 延时,作用是禁止 OS 进行调度,以防打断 us 级延时,导致延时时间不准。

函数: delay_osschedunlock,同样用于 delay_us 延时,作用是在延时结束后恢复 OS 的调度,继续正常的 OS 任务调度。

函数: delay_ostimedly,则是调用 OS 自带的延时函数,实现延时。该函数的参数为时钟节拍数。

函数: SysTick_Handler,则是 systick 的中断服务函数,该函数为 OS 提供时钟节拍,同时可以引起任务调度。

以上就是 delay_ms 和 delay_us 支持操作系统时,需要实现的 3 个宏定义和 4 个函数。

1.2 delay_init函数

该函数用来初始化 2 个重要参数: fac_us 以及 fac_ms;同时把 SysTick 的时钟源选择为外部时钟,如果需要支持操作系统(OS),只需要在 sys.h 里面,设置 SYSTEM_SUPPORT_OS 宏的值为 1 即可,然后,该函数会根据 delay_ostickspersec 宏的设置,来配置 SysTick 的中断时间,并开启 SysTick 中断。具体代码如下:

// 初始化延迟函数
// 当使用 OS 的时候,此函数会初始化 OS 的时钟节拍
// SYSTICK 的时钟固定为 HCLK 时钟的 1/8
void delay_init()
{
#if SYSTEM_SUPPORT_OS //如果需要支持 OS.
    u32 reload;
#endif
    HAL_SYSTICK_CLKSourceConfig(SYSTICK_CLKSOURCE_HCLK); // SysTick 频率为 HCLK
    fac_us = SYSCLK; //不论是否使用 OS,fac_us 都需要使用
#if SYSTEM_SUPPORT_OS // 如果需要支持 OS.
    reload = SYSCLK; //每秒钟的计数次数 单位为 K
    reload*=1000000/delay_ostickspersec; //根据 delay_ostickspersec 设定溢出时间
    //reload 为 24 位寄存器,最大值:16777216,在 180M 下,约合 0.745s 左右
    fac_ms = 1000/delay_ostickspersec; // 代表 OS 可以延时的最少单位
    SysTick->CTRL |= SysTick_CTRL_TICKINT_Msk;//开启 SYSTICK 中断
    SysTick->LOAD = reload; //每 1/OS_TICKS_PER_SEC 秒中断一次
    SysTick->CTRL |= SysTick_CTRL_ENABLE_Msk; //开启 SYSTICK
#else
#endif
}

可以看到, delay_init 函数使用了条件编译,来选择不同的初始化过程,如果不使用 OS 的时候,只是设置一下 SysTick 的时钟源以及确定 fac_us 和 fac_ms 的值。而如果使用 OS 的时候,则会进行一些不同的配置,这里的条件编译是根据 SYSTEM_SUPPORT_OS 这个宏来确定的,该宏在 sys.h 里面定义。

SysTick 是 MDK 定义了的一个结构体(在 core_m4.h 里面),里面包含 CTRL、LOAD、VAL、CALIB 等 4 个寄存器

SysTick->CTRL 的各位定义如图:

屏幕截图 2024 09 10 112358

SysTick-> LOAD 的定义如图:

屏幕截图 2024 09 10 112609

SysTick-> VAL 的定义如图:

屏幕截图 2024 09 10 112625

SysTick-> CALIB 不常用,在这里我们也用不到,故不介绍了。

SysTick_CLKSourceConfig(SysTick_CLKSource_HCLK);这句代码把 SysTick 的时钟选择为内核时钟,这里需要注意的是: SysTick 的时钟源自 HCLK,假设我们外部晶振为 8M,然后倍频到 168MHZ,那么 SysTick 的时钟即为 168Mhz,也就是 SysTick 的计数器 VAL 每减 1,就代表时间过了 1/168us。所以 fac_us=SYSCLK;这句话就是计算在 SYSCLK 时钟频率下延时 1us 需要多少个 SysTick 时钟周期。

在不使用 OS 的时候: fac_us,为 us 延时的基数,也就是延时 1us, Systick 定时器需要走过的时钟周期数。 当使用 OS 的时候, fac_us,还是 us 延时的基数,不过这个值不会被写到SysTick->LOAD 寄存器来实现延时,而是通过时钟摘取的办法实现的(前面已经介绍了)。而fac_ms 则代表 ucos 自带的延时函数所能实现的最小延时时间(如 delay_ostickspersec=200,那么 fac_ms 就是 5ms)

1.3 delay_us函数

该函数用来延时指定的 us, 其参数 nus 为要延时的微秒数。 该函数有使用 OS 和不使用 OS两个版本,这里我们分别介绍,首先是不使用 OS 的时候,实现函数如下:

// 延时 nus
// nus:要延时的 us 数.
// nus:0~190887435(最大值即 2^32/fac_us@fac_us=22.5)
void delay_us(u32 nus)
{
    u32 ticks;
    u32 told,tnow,tcnt = 0;
    u32 reload = SysTick->LOAD; // LOAD 的值
    ticks = nus*fac_us;  // 需要的节拍数
    delay_osschedlock(); // 阻止 OS 调度,防止打断 us 延时
    told = SysTick->VAL; // 刚进入时的计数器值
    while(1)
    {
        tnow = SysTick->VAL;
        if(tnow!=told)
        {
            if(tnow<told)tcnt+=told-tnow;
            //这里注意一下 SYSTICK 是一个递减的计数器就可以了.
            else tcnt+=reload-tnow+told;
            told=tnow;
            if(tcnt>=ticks)break; // 时间超过/等于要延迟的时间,则退出.
        }
    };
    delay_osschedunlock(); //恢复 OS 调度
}

了上面对 SysTick 寄存器的描述,这段代码不难理解。其实就是先把要延时的 us 数换算成 SysTick 的时钟数,然后写入 LOAD 寄存器。然后清空当前寄存器 VAL 的内容,再开启倒数功能。等到倒数结束,即延时了 nus。最后关闭 SysTick,清空 VAL 的值。实现一次延时 nus的操作,但是这里要注意 nus 的值,不能太大,必须保证 nus<=(2^24) /fac_us,否则将导致延时时间不准确。 这里特别说明一下: temp&0x01,这一句是用来判断 systick 定时器是否还处于开启状态,可以防止 systick 被意外关闭导致的死循环。

再来看看使用 OS 的时候, delay_us 的实现函数如下:

// 延时 nus
// nus 为要延时的 us 数.
// nus:0~190887435(最大值即 2^32/fac_us@fac_us=22.5)
void delay_us(u32 nus)
{
    u32 ticks;
    u32 told,tnow,tcnt=0;
    u32 reload=SysTick->LOAD; //LOAD 的值
    ticks=nus*fac_us; //需要的节拍数
    told=SysTick->VAL; //刚进入时的计数器值
    while(1)
    {
        tnow=SysTick->VAL;
        if(tnow!=told)
        {
            if(tnow<told)tcnt+=told-tnow;
            //这里注意一下 SYSTICK 是一个递减的计数器就可以了.
            else tcnt+=reload-tnow+told;
            told=tnow;
            if(tcnt>=ticks)break; //时间超过/等于要延迟的时间,则退出.
        }
     };
}

这里就正是利用了我们前面提到的时钟摘取法, ticks 是延时 nus 需要等待的 SysTick 计数次数(也就是延时时间), told 用于记录最近一次的 SysTick->VAL 值,然后 tnow 则是当前的SysTick->VAL 值,通过他们的对比累加,实现 SysTick 计数次数的统计,统计值存放在 tcnt 里面,然后通过对比 tcnt 和 ticks,来判断延时是否到达,从而达到不修改 SysTick 实现 nus 的延时,从而可以和 OS 共用一个 SysTick。

上面的 delay_osschedlock 和 delay_osschedunlock 是 OS 提供的两个函数,用于调度上锁和解锁,这里为了防止 OS 在 delay_us 的时候打断延时,可能导致的延时不准,所以我们利用这两个函数来实现免打断,从而保证延时精度!同时,此时的 delay_us,,可以实现最长 2^32/fac_us,在 168M 主频下,最大延时,大概是 204 秒。

1.4 delay_ms函数

该函数是用来延时指定的 ms 的,其参数 nms 为要延时的毫秒数。 该函数有使用 OS 和不使用 OS 两个版本,这里我们分别介绍,首先是不使用 OS 的时候,实现函数如下:

// 延时 nms
// nms:要延时的 ms 数
void delay_ms(u16 nms)
{
    u32 i;
    for(i=0;i< nms;i++) delay_us(1000);
}

该函数其实就是多次调用前面所讲的 delay_us 函数,来实现毫秒级延时的。再来看看使用 OS 的时候, delay_ms 的实现函数如下:

// 延时 nms
// nms:要延时的 ms 数 nms:0~65535
void delay_ms(u16 nms)
{
    if(delay_osrunning&&delay_osintnesting==0)
    // 如果 OS 已经在跑了,并且不是在中断里面(中断里面不能任务调度)
    {
        if(nms>=fac_ms) //延时的时间大于 OS 的最少时间周期
        {
            delay_ostimedly(nms/fac_ms); //OS 延时
        }
    nms%=fac_ms; //OS 已经无法提供这么小的延时了,采用普通方式延时
    }
    delay_us((u32)(nms*1000)); //普通方式延时
}

该函数中, delay_osrunning 是 OS 正在运行的标志, delay_osintnesting 则是 OS 中断嵌套次数,必须 delay_osrunning 为真,且 delay_osintnesting 为 0 的时候,才可以调用 OS 自带的延时函数进行延时(可以进行任务调度), delay_ostimedly 函数就是利用 OS 自带的延时函数,实现任 务级 延时 的 , 其参数 代表 延时 的时 钟节拍 数 ( 假设 delay_ostickspersec=200 ,那 么delay_ostimedly (1),就代表延时 5ms)。

当 OS 还未运行的时候,我们的 delay_ms 就是直接由 delay_us 实现的, OS 下的 delay_us可以实现很长的延时(达到 204 秒)而不溢出!,所以放心的使用 delay_us 来实现 delay_ms,不过由于 delay_us 的时候,任务调度被上锁了,所以还是建议不要用 delay_us 来延时很长的时间,否则影响整个系统的性能。

当 OS 运行的时候,我们的 delay_ms 函数将先判断延时时长是否大于等于 1 个 OS 时钟节拍(fac_ms) ,当大于这个值的时候,我们就通过调用 OS 的延时函数来实现(此时任务可以调度),不足 1 个时钟节拍的时候,直接调用 delay_us 函数实现(此时任务无法调度)。


2024.10.5 第一次修订,后期不再维护

posted @ 2024-10-05 10:16  hazy1k  阅读(6)  评论(0编辑  收藏  举报