1)实验平台:正点原子STM32MP157开发板 2)购买链接:https://item.taobao.com/item.htm?&id=629270721801 3)全套实验源码+手册+视频下载地址:http://www.openedv.com/thread-318813-1-1.html 4)正点原子官方B站:https://space.bilibili.com/394620890 5)正点原子STM32MP157技术交流群:691905614
前面章节的实验我们使用的是HAL库里自带的API函数HAL_Delay来实现毫秒级别延时的,如果使用到更高精度的延时,例如us级别的延时,我们可以使用定时器来实现,也可以使用SysTick的时钟摘取法来实现。本节,我们介绍如何使用SysTick来实现us级别的延时。 本章分为如下几个小节: 23.1 、SysTick简介; 23.2 、HAL_Delay函数; 23.3 、SysTick高精度延时实验; 23.4 、编译和测试;
23.1 SysTick简介 23.1.1 SysTick初识
- 什么是SysTick SysTick即系统滴答定时器(system tick timer),它被捆绑在NVIC中,属于Cortex-M内核的一部分。SysTick是一个24位的递减定时器,它是可编程的,软件上可通过对其对应的LOAD寄存器中写入一个数值(最大为224-1)来配置定时器的定时初值。当SysTick以一定的频率工作的时,每来一个脉冲,SysTick从定时初值逐1递减,当递减到0时,SysTick产生一次中断,同时从RELOAD 寄存器(值等于LOAD)中自动重装载定时初值,并重新开始新一轮的递减计数,如此反复。此过程就和我们前面学习的通用定时器以及高级定时器的递减计数功能类似,通过递减计数产生周期性的中断、延时,从而达到计时的效果。
图23.1.1. 1 SysTick是递减计数 SysTick属于Cortex-M内核的一个部分,并不是MCU片上外设,所以找遍了参考手册以及数据手册的犄角旮旯,你也看不到更多有关SysTick的详细介绍,如果想了解SysTick,应该查阅内核有关的文档,例如《ARM Cortex-M3与Cortex-M4处理器权威指南》、《Cortex-M3权威指南(中文)》以及《STM32F3与F4系列Cortex M4内核编程手册》等内核相关文件,这些资料存放在 STM32MP157开发板\开发板光盘A-基础资料\4、参考资料 下。 2. 为什么要有SysTick SysTick设计的目的就是给操作系统(OS)提供时基,用于周期性地产生中断来定时触发OS内核,如用于任务管理和上下文切换,处理器也可以在不同时间片内处理不同的任务。若应用中存在嵌入式OS,例如TinyOS、UCOS、RTOS和FreeRTOS等操作系统,SysTick会被OS使。如果不使用OS ,SysTick可当做简单的定时器来用。 对于简单的应用程序,例如我们编写的裸机程序,都是在一个while循环里依次调用各单任务来实现的,如果要处理更多任务或者更复杂的应用,再使用裸机程序就力不从心了,而且会出现更多的问题,例如,在while循环的单任务引用中,如果某一个任务出现问题,后续的任务就被牵连,从而导致系统崩溃。为解决这个问题,操作系统就可以登场了,我们以实时操作系统(RTOS)为例子来说明。当RTOS以并行的架构处理多个任务时,SysTick的任务就是给系统的任务调度提供时钟节拍。根据这个节拍,系统的整个时间段被分成很多很多很小的时间片,而这个时间片则是RTOS实现多任务切换的最小时间单位,每个任务每次只能运行一个时间片的时长就得退出给别的任务运行,这样就可以确保任何一个任务都不会霸占整个系统。如果其中某个任务挂掉了,挂掉的任务并不一定会牵连到整个系统,系统依然可以运行,其它任务依然可以正常调度。 只要是Cortex-M内核都有SysTick,由于和MCU外设无关,这样就方便了程序在不同厂家的Coetex-M内核的MCU之间的移植,例如将RTOS移植到别的硬件平台上,由于SysTick的存在,这样就大大降低了移植的难度。 3. SysTick的时钟 SysTick是MCU内核的一个设备,其时钟来自MCU系统时钟,然后经过分频后得到其工作的时钟,分频值可以是1或者8,所以SysTick的时钟频率最大值为209MHz,可以说其时钟精度还是比较高的,我们从时钟树中就可以看出来:
图23.1.1. 2 SysTick的时钟频率最大为209MHz 4. SysTick和其它定时器的差别 SysTick属于Cortex-M内核外设,定时器和RTC属于片上外设。 SysTick一般由ARM设计,每个Cortex-M内核里的SysTick都一样,而定时器和RTC属于片上外设,每个芯片厂家设计的芯片可能不一样,所以定时器以及RTC的资源也会不一样。 SysTick的存在主要是用于操作系统中的,如果应用中不使用操作系统,那么SysTick就当做简单的递减定时器来用;RTC可以分配给MPU使用,不能给MCU使用;定时器既可以给MPU使用,也可以给MCU使用,不过在同一个时刻只能单选。 SysTick的时钟源来自Cortex-M内核时钟,RTC时钟源可以是HSE、LSE和LSI,定时器时钟源来自APB1和APB2。默认情况下,STM32CubeMX使用Systick作为时基给其它程序提供计时,例如HAL_Delay延时函数,以及串口程序中的Timeout 超时机制等等,当然也可以选择其它定时器作为时基:
图23.1.1. 3 Systick作为时基 23.1.2 SysTick 寄存器 固件包的STM32Cube_FW_MP1_V1.2.0\Drivers\CMSIS\Core\Include文件夹下是符合CMSIS标准的内核头文件和CMSIS编译器相关的文件,core开头的是和 Cortex-M 内核相关的文件, MPU开头的是和MPU相关的文件。其中Cortex-M4内核使用core_cm4.h头文件,该文件中有很多关于NVIC中断相关的函数定义和类型定义以及内核的外设相关定义,SysTick相关的寄存器就定义在该文件中:
表23.1.2. 1 SysTick寄存器列表 下面我们来介绍以上4个寄存器:
- STK_CTRL(SysTick 控制及状态寄存器) STK_CTRL各位如下:
图23.1.2. 1 STK_CTRL寄存器 ENABLE ENABLE是计数器使能位,用于启用计数器(也就是启用SysTick定时器)。改为置1则使能计数器,清0则关闭计数器。 当ENABLE设置为1时,SysTick定时器被使能,计数器从LOAD寄存器加载RELOAD值,然后递减计数,当递减到0时,COUNTFLAG位变为1,并根据TICKINT的值选择置位SysTick, 然后它将再次加载RELOAD值,并开始计数。 TICKINT TICKINT是SysTick异常请求使能位,该位为0时,当计数器递减到0的时候,SysTick不产生异常请求;该位为1时,当计数器递减到0时产生异常请求。 注:软件可以使用COUNTFLAG来确定SysTick是否曾经计数为零。 CLKSOURCE CLKSOURCE用于配置SysTick的时钟源选择,我们前面说了,SysTick的时钟来自MCU系统时钟,分频值可以是1或者8,当该位为0时为1分频,为1时为8分频。 COUNTFLAG COUNTFLAG是标志计数器是否已经为0,当计数器递减为0时,该位为1。如果读取寄存器或者清除计数器当前值时,该位会被清0. 2. STK_LOAD(SysTick 重装载数值寄存器) STK_LOAD的各位如下图所示:
图23.1.2. 2 STK_LOAD寄存器 RELOAD[23:0]位是计数器为0时的重装载值,值可以是0x00000001-0x00FFFFFF范围内的任何值。 3. STK_VAL(SysTick当前值寄存器)
图23.1.2. 3 STK_VAL寄存器 CURRENT[23:0]位是当前计数器的值,读取此位即可获得当前计数器的当前值。写入该位任何值均会清空此位为0,同时STK_CTRL寄存器中的COUNTFLAG位也会被清0。 4. STK_CALIB(SysTick校准寄存器)
图23.1.2. 4 STK_CALIB寄存器 TENMS[23:0] TENMS[23:0]位是10ms校准值,该值应由芯片设计者提供,若读取该值为0,表示校准值不可用。 SKEW SKEW位用于指示校准值(TENMS值)是否精确,读取此位: 为0,表示校准值正确; 为1,表示校准值不精确,或者校准值未知,这可能会影响SysTick作为软件实时时钟的适用性。 NOREF NOREF用于指示是否有参考时钟提供给处理器。读取此位: 为0,表示有外部参考时钟可供使用; 为1,表示没有外部参考时钟。 23.1.3 SysTick的HAL库驱动 SysTick的HAL库驱动我们在第七章7.4.2小节 滴答定时器相关函数 部分就已经详细分析过了,大家可以参考前面章节的分析,这里就列举出一些HAL库的API函数。
- HAL_InitTick函数 HAL_InitTick用于配置SysTick的重装载数值寄存器的值,其通过层层调用HAL_SYSTICK_Config函数和SysTick_Config函数完成SysTick的配置,此函数声明如下: __weak HAL_StatusTypeDef HAL_InitTick(uint32_t TickPriority) 函数描述: 用于初始化SysTick,配置SysTick的重装载数值寄存器的值。 函数形参: 形参TickPriority是SysTick的中断优先级。 函数返回值: 无。 注意事项: 1、此函数是弱(weak)定义函数,如果用户在别的地方重新定义了同名函数,该函数将被覆盖,编译器会编译用户定义的那个函数; 2、系统复位后,HAL_Init最先被执行时,或者程序由HAL_RCC_ClockConfig重新配置时钟的时候,该函数都会被调用来初始化SysTick; 3、默认情况下,SysTick是计时的时基,SysTick通过周期性的中断来计时的,如果在别的中断中调用HAL_Delay就要小心了,SysTick中断的优先级必须调用它的中断具有更高的优先级(中断优先级数字上更低),否则程序会卡死。
- HAL_GetTickPrio HAL_GetTickPrio用于获取SysTick的中断优先级,返回值uwTickPrio是SysTick的中断优先级,函数声明如下:
uint32_t HAL_GetTickPrio(void)
3. HAL_SetTickFreq和HAL_GetTickFreq函数
HAL_SetTickFreq 函数用于重新配置SysTick的中断频率,HAL_GetTickFreq函数用于获取SysTick的中断频率。函数声明如下:
HAL_StatusTypeDef HAL_SetTickFreq(HAL_TickFreqTypeDef Freq)
HAL_TickFreqTypeDef HAL_GetTickFreq(void)
23.2 HAL_Delay函数
23.2.1 SysTick的分频值为1
- 未执行SystemClock_Config函数之前 在前面我们有简单分析过HAL_Delay怎么实现的毫秒延时的,这里我们再次分析实现的过程。 (1)HAL_Init函数 程序进入main.c文件以后,最先执行的是HAL_Init函数,该函数用于初始化HAL库,并配置SysTick每隔1ms生成一次中断。注意的是,此时还没有配置系统时钟(未执行SystemClock_Config函数),系统还是默认使用内部高速时钟HSI,HAL库里SysTick默认采用1分频,所以SysTick时钟频率也为64MHz。
stm32mp1xx_hal.c文件代码
/**
* @brief 初始化HAL库,设置中断优先级分组为4,设置SysTick每隔1ms产生一次中断
* @note 1、此函数是在主程序中在调用任何其他HAL库的IPA函数前执行的第一个指令,
* 在这个阶段,还未配置用户指定的时钟,系统还默认使用内部HSI的64MHz运行。
* 2、调用HAL_NVIC_SetPriorityGrouping完成中断优先级分组,调用
* HAL_InitTick完成SysTick的初始化
* 3、同时,调用用户文件中(STM32CubeMX中生成的stm32mp1xx_hal_msp.c文件)的
* 回调函数HAL_MspInit进行全局底层硬件初始化 。
* @param 无
* @retval 无
*/
HAL_StatusTypeDef HAL_Init(void)
{
/* 设置中断优先级分组为4 */
#if defined (CORE_CM4)
HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4);
#endif
/* 更新SystemCoreClock全局变量 */
SystemCoreClock = HAL_RCC_GetSystemCoreClockFreq();
/* 使用滴答定时器作为时钟基准,配置1ms滴答(重置后默认的时钟源为HSI)*/
if(HAL_InitTick(TICK_INT_PRIORITY) != HAL_OK)
{
return HAL_ERROR;
}
HAL_MspInit();
return HAL_OK;
}
(2)HAL_InitTick函数 HAL_Init函数调用了HAL_InitTick函数完成SysTick的初始化,我们来看看此函数:
stm32mp1xx_hal.c文件代码
/**
* @brief 初始化SysTick
* @param TickPriority :SysTick的中断优先级
* @note 1、该函数是弱定义函数,用户可以在别的地方重新定义一个同名函数,
* 编译以后,编译器只编译用户定义的函数。
* 2、该函数主要是初始化SysTick的中断优先级、配置SysTick的时钟频率为1kHz,
* 则周期为1/1000Hz=1ms
*
* @retval 无
*/
__weak HAL_StatusTypeDef HAL_InitTick(uint32_t TickPriority)
{
/* 配置SysTick在1ms的时间产生一次中断 */
#if defined (CORE_CA7)
/****** 此处省略A7部分的代码 ******/
#endif /* CORE_CA7 */
#if defined (CORE_CM4)
/* uwTickFreq是个枚举类型,如果检测到uwTickFreq为零,则返回1(HAL_ERROR) */
if ((uint32_t)uwTickFreq == 0U)
{
return HAL_ERROR;
}
/* 将SysTick配置为在1ms的时间产生一次中断 ,uwTickFreq的值默认为1*/
if (HAL_SYSTICK_Config(SystemCoreClock /(1000U / uwTickFreq)) > 0U)
{
return HAL_ERROR;
}
/* 配置 SysTick的中断优先级 */
if (TickPriority HSICFGR & RCC_HSICFGR_HSIDIV));
12 break;
13
14 case 0x01: /* HSE used as system clock source */
15 SystemCoreClock = HSE_VALUE;
16 break;
17
18 case 0x02: /* CSI used as system clock source */
19 SystemCoreClock = CSI_VALUE;
20 break;
21
22 case 0x03: /* PLL3_P used as system clock source */
23 /*******省略部分代码*******/
24 break;
25 }
26
27 /* Compute mcu_ck */
28 SystemCoreClock = SystemCoreClock >> (RCC->MCUDIVR & RCC_MCUDIVR_MCUDIV);
29 }
所以当我们配置好系统的时钟为209MHz时,SystemCoreClock的值也是209M,当SysTick还是1分频时,SysTick的频率也为209MHz。结合前面的分析,SysTick的一个节拍用的时间为,SysTick的总节拍数为HAL_SYSTICK_Config(SystemCoreClock /(1000U / uwTickFreq))=,那么SysTick产生中断的周期是:=1ms。 通过这里可以验证,不管配置系统时钟(MCU的时钟)为多少,只要SysTick为1分频,SysTick的中断周期都是1ms,使用HAL_Delay可以实现以1ms为单位来计时。 23.2.1 SysTick的分频值为8 当SysTick的分频值为8时,SysTick的中断周期是8ms,我们以系统时钟为209MHz为例子来计算一下。SystemCoreClock的值为209MHz,SysTick分频值为8,SysTick的频率为,SysTick的一个节拍用的时间为,SysTick的总节拍数还是不变为,那么SysTick产生中断的周期是:8ms 。 所以,当修改了SysTick的分频值为8时,执行HAL_Delay(10)函数以后不再是延时10ms,而是80ms。 23.3 SysTick高精度延时实验 本实验配置好的实验工程已经放到了开发板光盘中,路径为:开发板光盘A-基础资料\1、程序源码\11、M4 CubeIDE裸机驱动例程\CubeIDE_project\ 16-1 SysTick。 经过前面的分析,使用HAL库自带的HAL_Delay延时函数可以实现的是毫秒级别的延时,如果需要更精确的延时,例如微妙级别的延时,使用HAL_Delay就不能够实现了。下面,我们使用SysTick来实现微妙级别的延时。 我们将编写delay_us函数实现微妙延时,基于delay_us的基础上编写delay_ms函数实现毫秒延时。编程的思路采用时钟摘取法,以delay_us为例,在进入函数的时候先计算需要延时一段时间所需要的等待的SysTick的计数节拍数,然后我们就一直统计SysTick的计数变化,当计数器的变化节拍数达到我们计算出的节拍数时,说明延时时间到了。比如delay_us(50)表示需要延时50us,假设系统时钟为209MHz,所以要延时50us的话则需要SysTick计数50*209,所以我们的程序就是通过判断是否达到了所需的节拍数来判断所需的延时时间是否已经达到了。 通过时钟摘取法只是抓取SysTick计数器的变化,不需要去修改SysTick的任何配置状态,所以完全不影响SysTick作为UCOS时钟节拍的功能,这就是实现delay和操作系统共用SysTick定时器的原理。下面我们开始介绍这几个函数。 23.3.1 无操作系统 本实验自行编写程序实现SysTick以us计时,然后使用LED0闪烁观察效果,实验中,我们配置系统时钟为209MHz,SysTick为1分频,所以SysTick的时钟频率也为209MHz。
- 新建和配置工程 本节实验可以在前面使用LED的工程中直接添加代码,为了方便,这里新建了一个工程SysTick,并按照前面实验的配置步骤使用HSE作为PLL3的时钟源,同时配置MCU的时钟频率为209MHz,SysTick为1分频:
图23.3.1. 1 SysTick时钟配置 生成工程后,因为本节实验用到LED0相关的驱动,在Src下新建BSP文件夹,然后把LED0的驱动文件拷贝过来,或者直接拷贝前面实验的BSP文件夹。在BSP文件夹下新建delay.c文件,在BSB/Include文件夹下新建delay.h文件,最后生成的工程如下:
图23.3.1. 2生成工程 2. 添加用户驱动 (1)添加delay.c文件代码 delay.c文件内容如下:
#include ./Include/delay.h
1 static uint32_t g_fac_us = 0; /* us延时倍乘数 */
2 /**
3 * @brief 初始化延迟函数
4 * @param sysclk: 系统时钟频率, 即MCU的频率,最大为209MHz
5 * @retval 无
6 */
7 void delay_init(uint16_t sysclk)
8 {
9 g_fac_us = sysclk; /* SYSTICK使用内核时钟源,同MCU同频率 */
10 }
11 /**
12 * @brief 要延时n个us
13 * @param nus: 要延时的us数
14 * @note 注意: nus的值,不要大于80274us(最大值即(2^24)/g_fac_us)
15 * g_fac_us最大为209MHz
16 * told是刚进入函数时的计数器的值(我们称为旧的节拍值)
17 * tnow是进入while循环后实时监测到的计数器的值(我们称为新的节拍值)
18 * tcnt是由旧的节拍值到新的节拍值经过了多少个节拍
19 * @retval 无
20 */
21 void delay_us(uint32_t nus)
22 {
23 uint32_t ticks;
24 uint32_t told, tnow, tcnt = 0;
25 uint32_t reload = SysTick->LOAD; /* LOAD的值 */
26 ticks = nus * g_fac_us; /* 计时nus需要的节拍数 */
27 told = SysTick->VAL; /* 刚进入函数时的计数器的值 */
28 while (1)
29 {
30 tnow = SysTick->VAL;/* 进入while循环后实时监测到的新的节拍值 */
31 if (tnow != told)
32 {
33 if (tnow = ticks)
43 {
44 break; /* 时间超过或者等于要延迟的时间,则退出 */
45 }
46 }
47 }
48 }
49 /**
50 * @brief 要延时n个ms
51 * @param nms: 要延时的ms数 (0< nms LOAD = reload; /* 每1/delay_ostickspersec秒中断一次 */
SysTick->CTRL |= SysTick_CTRL_ENABLE_Msk; /* 开启SYSTICK */
#endif
}
可以看到,delay_init函数使用了条件编译,来选择不同的初始化过程,如果不使用OS的时候,只是设置一下SysTick的时钟源以及确定fac_us值。而如果使用OS的时候,则会进行一些不同的配置,这里的条件编译是根据SYS_SUPPORT_OS这个宏来确定的,该宏在delay.h里面定义。
在不使用OS的时候:g_fac_us,为us延时的基数,也就是延时1us,SysTick定时器需要走过的时钟周期数。当使用OS的时候,g_fac_us,还是us延时的基数,不过这个值不会被写到SysTick->LOAD寄存器来实现延时,而是通过时钟摘取的办法实现的(前面已经介绍了)。而g_fac_ms则代表ucos自带的延时函数所能实现的最小延时时间。 3. OS相关的宏定义和函数 当需要delay_ms和delay_us支持操作系统(OS)的时候,我们需要用到3个宏定义和4个函数,宏定义及函数代码如下: /*
- 当delay_us/delay_ms需要支持OS的时候需要三个与OS相关的宏定义和函数来支持
- 首先是3个宏定义:
* delay_osrunning:用于表示OS当前是否正在运行,以决定是否可以使用相关函数
* delay_ostickspersec:用于表示OS设定的时钟节拍,delay_init
* 将根据这个参数来初始化systick
* delay_osintnesting :用于表示OS中断嵌套级别,因为中断里面不可以调度,
* delay_ms使用该参数来决定如何运行
* 然后是3个函数:
* delay_osschedlock :用于锁定OS任务调度,禁止调度
* delay_osschedunlock:用于解锁OS任务调度,重新开启调度
* delay_ostimedly :用于OS延时,可以引起任务调度.
*
* 本例程仅作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 */
/* CPU_CFG_CRITICAL_METHOD定义了,说明要支持UCOSIII */
#ifdef CPU_CFG_CRITICAL_METHOD
#define delay_osrunning OSRunning /* OS是否运行标记,0,不运行;1,在运行 */
#define delay_ostickspersec OSCfg_TickRate_Hz /* OS时钟节拍,即每秒调度次数 */
#define delay_osintnesting OSIntNestingCtr/* 中断嵌套级别,即中断嵌套次数 */
#endif
/**
* @brief us级延时时,关闭任务调度(防止打断us级延迟)
* @param 无
* @retval 无
*/
void delay_osschedlock(void)
{
#ifdef CPU_CFG_CRITICAL_METHOD /* 使用UCOSIII */
OS_ERR err;
OSSchedLock(&err); /* UCOSIII的方式,禁止调度,防止打断us延时 */
#else /* 否则UCOSII */
OSSchedLock(); /* UCOSII的方式,禁止调度,防止打断us延时 */
#endif
}
/**
* @brief us级延时时,恢复任务调度
* @param 无
* @retval 无
*/
void delay_osschedunlock(void)
{
#ifdef CPU_CFG_CRITICAL_METHOD /* 使用UCOSIII */
OS_ERR err;
OSSchedUnlock(&err); /* UCOSIII的方式,恢复调度 */
#else /* 否则UCOSII */
OSSchedUnlock(); /* UCOSII的方式,恢复调度 */
#endif
}
/**
* @brief us级延时时,恢复任务调度
* @param ticks: 延时的节拍数
* @retval 无
*/
void delay_ostimedly(uint32_t ticks)
{
#ifdef CPU_CFG_CRITICAL_METHOD
OS_ERR err;
OSTimeDly(ticks, OS_OPT_TIME_PERIODIC, &err); /* UCOSIII延时采用周期模式 */
#else
OSTimeDly(ticks); /* UCOSII延时 */
#endif
}
/**
* @brief systick中断服务函数,使用OS时用到
* @param ticks: 延时的节拍数
* @retval 无
*/
void SysTick_Handler(void)
{
HAL_IncTick();
if (delay_osrunning == 1) /* OS开始跑了,才执行正常的调度处理 */
{
OSIntEnter(); /* 进入中断 */
OSTimeTick(); /* 调用ucos的时钟服务程序 */
OSIntExit(); /* 触发任务切换软中断 */
}
}
#endif
以上代码,仅支持UCOSII和UCOSIII,不过,对于其他OS的支持,也只需要对以上代码进行简单修改即可实现。 支持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。 支持OS需要用到的4个函数,即: 1)函数:delay_osschedlock,用于delay_us延时,作用是禁止OS进行调度,以防打断us级延时,导致延时时间不准。 2)函数:delay_osschedunlock,同样用于delay_us延时,作用是在延时结束后恢复OS的调度,继续正常的OS任务调度。 3)函数:delay_ostimedly,则是调用OS自带的延时函数,实现延时。该函数的参数为时钟节拍数。 4)函数:SysTick_Handler,则是systick的中断服务函数,该函数为OS提供时钟节拍,同时可以引起任务调度。 以上就是delay_ms和delay_us支持操作系统时,需要实现的3个宏定义和4个函数。下面 我们来看看支持操作系统时delay_us函数编写:。 4. delay_us函数 使用OS的时候,delay_us函数和前面我们不使用操作系统的类似,不同的是多加了delay_osschedlock和delay_osschedunlock是OS提供的两个函数,用于调度上锁和解锁,这里为了防止OS在delay_us的时候打断延时,可能导致的延时不准,所以我们利用这两个函数来实现免打断,从而保证延时精度!
/**
* @brief 延时nus
* @param nus: 要延时的us数
* @note nus取值范围: 0~ 80274us (最大值即2^24 / g_fac_us @g_fac_us = 209)
* @retval 无
*/
void delay_us(uint32_t nus)
{
uint32_t ticks;
uint32_t told, tnow, tcnt = 0;
uint32_t reload = SysTick->LOAD; /* LOAD的值 */
ticks = nus * g_fac_us; /* 需要的节拍数 */
delay_osschedlock(); /* 阻止OS调度,防止打断us延时 */
told = SysTick->VAL; /* 刚进入时的计数器值 */
while (1)
{
tnow = SysTick->VAL;
if (tnow != told)
{
if (tnow = ticks)
{
break; /* 时间超过/等于要延迟的时间,则退出 */
}
}
}
delay_osschedunlock(); /* 恢复OS调度 */
}
不同的是多加了delay_osschedlock和delay_osschedunlock是OS提供的两个函数,用于调度上锁和解锁,这里为了防止OS在delay_us的时候打断延时,可能导致的延时不准,所以我们利用这两个函数来实现免打断,从而保证延时精度 5. delay_ms函数 使用OS的时候,delay_ms的实现函数如下:
/**
* @brief 延时nms
* @param nms: 要延时的ms数 (0< nms = g_fac_ms) /* 延时的时间大于OS的最少时间周期 */
{
delay_ostimedly(nms / g_fac_ms);/* OS延时 */
}
nms %= g_fac_ms; /* OS已经无法提供这么小的延时了,采用普通方式延时 */
}
delay_us((uint32_t)(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实现的,不过由于delay_us的时候,任务调度被上锁了,所以还是建议不要用delay_us来延时很长的时间,否则影响整个系统的性能。当OS运行的时候,我们的delay_ms函数将先判断延时时长是否大于等于1个OS时钟节拍(fac_ms),当大于这个值的时候,我们就通过调用OS的延时函数来实现(此时任务可以调度),不足1个时钟节拍的时候,直接调用delay_us函数实现(此时任务无法调度)。 23.3.3 编译和测试 本实验我们的工程是无操作系统的,我们以无操作系统情况下的工程代码为例进行编译测试,编译后,测试PI0引脚的波形。PI0引脚在JP1排针处有引出:
图23.3.3. 1开发板的PI0引脚 所测试的波形如下,高电平脉冲为500ms,和我们实验的结果一致:
图23.3.3. 2测试结果