目录
1. STM32F4 低功耗模式
1.1 睡眠(Sleep)模式
1.2 停止(Stop)模式
1.3 待机(Standby)模式
2. Tickless 模式详解
2.1 如何降低功耗
2.2 Tickless 具体实现
2.2.1 宏 configUSE_TICKLESS_IDLE
2.2.2 宏 portSUPPRESS_TICKS_AND_SLEEP()
2.2.3 宏 configPRE_SLEEP_PROCESSING() 和 configPOST_SLEEP_PROCESSING()
2.2.4 宏 configEXPECTED_IDLE_TIME_BEFORE_SLEEP
3. 低功耗 Tickless 模式实验
3.1 main.c
3.2 usart.c
很多应用场合对于功耗的要求很严格,比如长期无人照看的数据采集仪器,可穿戴设备等。很多 MCU 都有相应的低功耗模式,以此来降低设备运行时的功耗,进行裸机开发的时候就可以使用这些低功耗模式。操作系统对于低功耗的支持也非常重要,通过硬件与软件相结合,可以进一步降低系统的功耗。这样开发也非常方便,系统原生支持低功耗,我们只需要按照系统的要求来编写相应的应用层代码即可。FreeRTOS 提供了一个叫做 Tickless 的低功耗模式。
1. STM32F4 低功耗模式
STM32 本身就支持低功耗模式,我们在学习 STM32 的时候知道 STM32F4 共有三种低功耗模式:睡眠模式(Sleep)、停止模式(Stop) 和 待机模式(Standby)!
睡眠模式:
进入睡眠模式:WFI WFE
唤醒:任意中断 唤醒事件
对 1.2V 域时钟的影响:CPU CLK 关闭对其他时钟或模拟时钟源无影响
对 VDD 域时钟的影响:无
调压器:开启
停止模式:
进入停止模式:PDDS 和 LPDS 位 + SLEEPDEEP 位 + WFI 或 WFE
唤醒:任意 EXTI 线(在 EXTI 寄存器中配置,内部线和外部线)
对 1.2V 域时钟的影响:所有 1.2V 域时钟都关闭
对 VDD 域时钟的影响:HSI 和 HSE 振荡器关闭
调压器:开启或处于低功耗模式
待机模式:
进入待机模式:PDDS 位 + SLEEPDEEP 位 + WFI 或 WFE
唤醒:WKUP 引脚上升沿、RTC 闹钟(闹钟 A 或 闹钟 B)RTC 唤醒事件、RTC 入侵事件、RTC 时间戳事件、NRST 引脚外部复位、IWDG 复位
对 1.2V 域时钟的影响:所有 1.2V 域时钟都关闭
对 VDD 域时钟的影响:HSI 和 HSE 振荡器关闭
调压器:关闭
这三种低功耗模式对应三种不同的功耗水平,根据实际的应用环境选择相对应的低功耗模式。
1.1 睡眠(Sleep)模式
进入睡眠模式:
进入睡眠模式有两种指令:WFI(等待中断)和 WFE(等待事件)。根据 cortex-M 内核的 SCR(系统控制寄存器)可以选择使用立即休眠还是退出时休眠,当 SCR 寄存器的 SLEEPONEXIT(bit1) 位为 0 的时候使用立即休眠,当为 1 的时候使用退出时休眠。
CMSIS(Cortex 微控制器软件接口标准)提供了两个函数来操作指令 WFI 和 WFE,我们可以直接使用这两个函数 :_WFI 和 _WFE。FreeRTOS 系统会使用 WFI 指令进入休眠模式。
退出休眠模式:
如果使用 WFI 指令进入休眠模式的话那么任意一个中断都会将 MCU 从休眠模式中唤醒,如果使用 WFE 指令进入休眠模式的话那么当有事件发送的话就会退出休眠模式,比如配置一个 EXIT 线作为事件。
当 STM32F429 处于休眠模式的时候 Cortex-M4F 内核停止运行,但是其他外设运行正常,比如 NVID 、SRAM 等。休眠模式的功耗比其他两个高,但是休眠模式没有唤醒延迟,应用程序可以立即执行。
1.2 停止(Stop)模式
停止模式基于 Cortex-M4F 的深度休眠模式与外设时钟门控,在此模式下 1.2V 域的所有时钟都会停止,PLL、HSI 和 HSE RC 振荡器会被禁止,但是内部 SRAM 的数据会被保留。调压器可以工作在正常模式,也可以配置为低功耗模式。如果有必要的话可以通过将 PWR_CR 寄存器的 FPDS 位置 1 来使 Flash 在停止模式的时候进入掉电状态,当 Flash 处于掉电状态的时候 MCU 从停止模式唤醒以后需要更多的启动延时。
进入停止模式:
WFI(等待中断)或 WFE(等待事件),且
- 1. 将 Cortex-M4F 系统控制寄存器中 SLEEPDEEP 位置 1
- 2. 将电源控制寄存器(PWR_CR)中的 PDDS 位清零
- 3. 通过配置 PWR_CR 中的 LPDS 位选择调压器模式
注意:要进入停止模式,所有 EXTI 线挂起位、RTC 闹钟(闹钟 A 和 闹钟 B)、RTC 唤醒、RTC 入侵和 RTC 时间戳标志必须复位。否则将忽略进入停止模式这一过程,继续执行程序。
退出停止模式:
如果使用 WFI 进入:所有配置为中断模式的 EXTI 线(必须在 NVIC 中使能对应的 EXTI 中断向量)。
如果使用 WFE 进入:所有配置为事件模式的 EXTI 线。
1.3 待机(Standby)模式
相比于前面两种低功耗模式,待机模式的功耗最低。待机模式是基于 Cortex-M4F 的深度睡眠模式的,其中调压器被禁止。1.2V 域断电,PLL、HSI 振荡器和 HSE 振荡器也被关闭。除了备份区域和待机电路相关的寄存器外,SRAM 和其他寄存器的内容都将丢失。
进入待机模式:
WFI(等待中断)或WFE(等待事件),且:
- 1. 将 Cortex-M4F 系统控制寄存器中的 SLEEPDEEP 位置 1
- 2. 将电源控制寄存器 (PWR_CR) 中的 PDDS 位置 1
- 3. 将电源控制/状态寄存器(PWR_CSR)中的 WUF 位清零
- 4. 将与所选唤醒源(RTC 闹钟 A、RTC 闹钟 B、RTC 唤醒、RTC 入侵或 RTC 时间戳标志)对应的 RTC 标志清零
退出待机模式:
WKUP 引脚上升沿、RTC 闹钟(闹钟 A 和闹钟 B)、RTC 唤醒事件、RTC 入侵事件、RTC 时间戳事件、NRST 引脚外部复位和 IWDG 复位
退出待机模式的话会导致 STM32F4 重启,所以待机模式的唤醒延时也是最大的。
2. Tickless 模式详解
2.1 如何降低功耗
一般的简单应用中处理器大量的时间都在处理空闲任务,所以我们就可以考虑当处理器处理空闲任务的时候就进入低功耗模式,当需要处理应用层代码的时候就将处理器从低功耗模式中唤醒。FreeRTOS 就是通过在处理器处理空闲任务的时候将处理器设置为低功耗模式来降低功耗。一般会在空闲任务的钩子函数中执行低功耗相关处理,比如设置处理器进入低功耗模式、关闭其他外设时钟、降低系统主频等等。
FreeRTOS 的系统时钟是由滴答定时器中断来提供的,系统时钟频率越高,那么滴答定时器中断频率也就越高。中断是可以将 STM32F407 从睡眠模式中唤醒的,周期性的滴答定时器中断就会导致 STM32F407 周期性的进入和退出睡眠模式。因此,如果滴答定时器中断频率太高的话会导致大量的能量和时间消耗在进出睡眠模式中,这样导致的结果就是低功耗模式的作用被大大的削弱。
为此,FreeRTOS 特地提供一个解决方法-----Tickless 模式,当处理器进入空闲任务周期以后就关闭系统节拍中断(滴答定时器中断),只有当其他中断发生或者其他任务需要处理的时候处理器才会被从低功耗模式中唤醒。但此时会面临着两个问题:
问题一:关闭系统节拍中断会导致系统节拍计数器停止,系统时钟就会停止。
FreeRTOS 的系统时钟是依赖于系统节拍中断(滴答定时器中断)的,如果关闭了系统节拍中断的话就会导致系统时钟停止运行,这是绝对不允许的!对此,我们可以记录下系统节拍中断的关闭时间,当系统节拍中断再次开启运行的时候补上这段时间就行了。这时候我们需要另外一个定时器来记录这段该补上的时间,如果使用专用的低功耗处理器的话基本上都会有一个低功耗定时器,比如 STM32L4 系列(L 系列是 ST 的低功耗处理器)就有一个叫做 LPTIM (低功耗定时器)的定时器。STM32F407 没有这种定时器那么就接着使用滴答定时器来完成这个功能。
问题二:如何能保证下一个要运行的任务能被准确的唤醒?
即使处理器进入了低功耗模式,但是我的中断和应用层任务也要保证及时的响应和处理。中断自然不用说,本身就可以将处理器从低功耗模式中唤醒。但是应用层任务就不行了,他无法将处理器从低功耗模式中唤醒,无法唤醒也就无法运行!这个问题看起来很棘手,既然应用层任务无法将处理器从低功耗模式唤醒,那么我们就借助其他的力量来完成这个功能。如果处理器在进入低功耗模式之前能够获取到还有多长时间运行下一个任务那么问题就迎刃而解了,我们只需要设置一个定时器,定时器的定时周期设置为这个时间值就可以了,定时时间到了以后产生定时器中断,处理器就从低功耗模式中唤醒了。但是这里似乎又引出了一个新的问题,如何知道还有多长时间执行下一个任务?这个时间也就是低功耗模式的执行时间,值得庆幸的是 FreeRTOS 已经帮我们完成了这个工作。
2.2 Tickless 具体实现
2.2.1 宏 configUSE_TICKLESS_IDLE
要想使用 Tickless 模式,首先必须将 FreeRTOSConfig.h 中的宏 configUSE_TICKLESS_IDLE 设置为 1,代码如下:
#define configUSE_TICKLESS_IDLE 1 //启动低功耗 tickless 模式
2.2.2 宏 portSUPPRESS_TICKS_AND_SLEEP()
使能 Tickless 模式以后,当下面两种情况都出现的时候 FreeRTOS 内核就会调用宏 portSUPPORT_TICKS_AND_SLEEP() 来处理低功耗相关的工作。
- 空闲任务是唯一可以运行的任务,因为其他所有的任务都处于阻塞态或者挂起态。
- 系统处于低功耗模式的时间至少大于 configEXPECTED_IDLE_TIME_BEFORE_SLEEP 个时钟节拍,宏 configEXPECTED_IDLE_TIME_BEFORE_SLEEP 默认在文件 FreeRTOS.h 中定义为 2,我们可以在 FreeRTOSConfig.h 中重新定义,此宏必须大于 2!
portSUPPRESS_TICKS_AND_SLEEP() 有个参数,此参数用来指定还有多长时间将有任务进入就绪态,其实就是处理器进入低功耗模式的时长(单位为时钟节拍数),因为一旦有其他任务进入就绪态处理器就必须退出低功耗模式去处理这个任务。portSUPPRESS_TICKS_AND_SLEEP() 是由用户根据自己所选择的平台来编写的,此宏会被空闲任务调用来完成具体的低功耗工作。FreeRTOS 已经完成了该宏!
#ifndef portSUPPRESS_TICKS_AND_SLEEP extern void vPortSuppressTicksAndSleep( TickType_t xExpectedIdleTime ); #define portSUPPRESS_TICKS_AND_SLEEP( xExpectedIdleTime ) vPortSuppressTicksAndSleep( xExpectedIdleTime )
#endif
从上面的代码可以看出 portSUPPORT_TICKS_AND_SLEEP() 的本质就是函数 vPortSuppressTicksAndSleep();
__weak void vPortSuppressTicksAndSleep( TickType_t xExpectedIdleTime )
{ uint32_t ulReloadValue, ulCompleteTickPeriods, ulCompletedSysTickDecrements,ulSysTickCTRL; TickType_t xModifiableIdleTime; //确保滴答定时器的 Reload(重装载)值不会溢出,也就是不能超过滴答定时器最大计数值。 if( xExpectedIdleTime > xMaximumPossibleSuppressedTicks ) (1) 参数 xExpectedIdleTime 表示处理器将要在低功耗模式运行的时长(单位为时钟节拍数),这个时间会使用滴答定时器来计时,但是滴答定时器的计数寄存器是 24 位的,因此这个时间值不能超过滴答定时器的最大计数值。xMaximumPossibleSuppressedTicks 是个静态全局变量,在文件 port.c 中有定义,此函数会在函数 vPortSetupTimerInterrupt() 中被重新赋值,代码如下:
ulTimerCountsForOneTick=(configSYSTICK_CLOCK_HZ/configTICK_RATE_HZ);
xMaximumPossibleSuppressedTicks=portMAX_24_BIT_NUMBER/ulTimerCountsForOneTick;经过计算 xMaximumPossibleSuppressedTicks=0xffffff/(168000000/1000)=99,因此进入低功耗模式的最大时长是99个时钟节拍。{ xExpectedIdleTime = xMaximumPossibleSuppressedTicks; } //停止滴答定时器。 portNVIC_SYSTICK_CTRL_REG &= ~portNVIC_SYSTICK_ENABLE_BIT; //根据参数 xExpectedIdleTime 来计算滴答定时器的重装载值。 ulReloadValue = portNVIC_SYSTICK_CURRENT_VALUE_REG + (2) 根据参数 xExpectedIdleTime 来计算滴答定时器的重装载值,因为处理器进入低功耗模式以后的计时是由滴答定时器来完成的。( ulTimerCountsForOneTick * ( xExpectedIdleTime - 1UL ) ); if( ulReloadValue > ulStoppedTimerCompensation ) (3) 从滴答定时器停止运行到把统计得到的低功耗模式运行的这段时间补偿给 FreeRTOS 系统时钟也是需要时间的,这期间也是有程序在运行的。这段程序运行的时间我们要留出来,具体的时间没法去统计。这里只能大概的留出一个时间值,这个时间值由变量 ulStoppedTimerCompensation 来确定,这是一个全局变量。
#define portMISSED_COUNTS_FACTOR(45UL)ulStoppedTimerCompensation=portMISSED_COUNTS_FACTOR/(configCPU_CLOCK_HZ/configSYSTICK_CLOCK_HZ)
通过上面的公式可以得出:ulStoppedTimerCompensation=45/(168000000/168000000)=45。{ ulReloadValue -= ulStoppedTimerCompensation; } __disable_irq(); (4) 在执行WFI 前设置寄存器 PRIMASK 的话处理器可以由中断唤醒但是不会处理这些中断,退出低功耗模式以后通过清除寄存器 PRIMASK 来使 ISR 得到执行,其实就是利用 PRIMASK 来延迟 ISR 的执行。函数 __disable_irq(); 用来设置寄存器 PRIMASK,清除寄存器 PRIMASK 使用函数 __enable_irq();__dsb( portSY_FULL_READ_WRITE ); __isb( portSY_FULL_READ_WRITE ); //确认是否可以进入低功耗模式 if( eTaskConfirmSleepModeStatus() == eAbortSleep ) (5) 调用函数 eTaskConfirmSleepModeStatus() 来判断是否可以进入低功耗模式,此函数在文件 tasks.c 中有定义。此函数通过检查是否还有就绪任务来决定处理器能不能进入低功耗模式,如果返回 eAbortSleep 的话就表示不能进入低功耗模式,既然不能进入低功耗模式那么就需要重新恢复滴答定时器的运行。{ //不能进入低功耗模式,重新启动滴答定时器 portNVIC_SYSTICK_LOAD_REG = portNVIC_SYSTICK_CURRENT_VALUE_REG; portNVIC_SYSTICK_CTRL_REG |= portNVIC_SYSTICK_ENABLE_BIT; portNVIC_SYSTICK_LOAD_REG = ulTimerCountsForOneTick - 1UL; __enable_irq(); (6) 调用函数__enable_irq();重新打开中断} else (7) 可以进入低功耗模式,完成低功耗相关设置{ //可以进入低功耗模式,设置滴答定时器 portNVIC_SYSTICK_LOAD_REG = ulReloadValue; (8) 进入低功耗模式的时间已经计算出来了,这里将这个值写入到滴答定时器的重装载寄存器中portNVIC_SYSTICK_CURRENT_VALUE_REG = 0UL; portNVIC_SYSTICK_CTRL_REG |= portNVIC_SYSTICK_ENABLE_BIT; xModifiableIdleTime = xExpectedIdleTime; configPRE_SLEEP_PROCESSING( xModifiableIdleTime ); (9) configPRE_SLEEP_PROCESSING 是个宏,在进入低功耗模式之前可能有一些其他的事情要处理,比如降低系统时钟、关闭外设时钟、关闭板子某些硬件的电源等等,这些操作就可以在这个宏中完成。if( xModifiableIdleTime > 0 ) { __dsb( portSY_FULL_READ_WRITE ); __wfi(); (10) 使用 WFI 指令使 STM32F407 进入睡眠模式。__isb( portSY_FULL_READ_WRITE ); } //当代码执行到这里的时候说明已经退出了低功耗模式! configPOST_SLEEP_PROCESSING( xExpectedIdleTime ); (11) 代码执行到这个说明处理器已经退出了低功耗模式,退出低功耗模式以后也可能需要处理一些事情。比如恢复系统时钟,使能外设时钟,打开板子某些硬件的电源等等,这些操作在宏 configPOST_SLEEP_PROCESSING() 中完成。//停止滴答定时器 ulSysTickCTRL = portNVIC_SYSTICK_CTRL_REG; (12) 读取滴答定时器 CTRL(控制和状态)寄存器。portNVIC_SYSTICK_CTRL_REG = ( ulSysTickCTRL &~portNVIC_SYSTICK_ENABLE_BIT ); __enable_irq(); (13) 调用函数 __enable_irq();打开中断//判断导致退出低功耗的是由外部中断引起的还是滴答定时器计时时间到引起的 if( ( ulSysTickCTRL & portNVIC_SYSTICK_COUNT_FLAG_BIT ) != 0 ) (14) 判断退出低功耗模式是由滴答定时器中断引起的还是由其他中断引起的,因为这两种原因所对应的系统时钟赔偿值的计算方法不同,这个系统时钟补偿值的单位是时钟节拍。{ uint32_t ulCalculatedLoadValue; ulCalculatedLoadValue = ( ulTimerCountsForOneTick - 1UL ) - ( ulReloadValue - portNVIC_SYSTICK_CURRENT_VALUE_REG ); if( ( ulCalculatedLoadValue < ulStoppedTimerCompensation ) ||( ulCalculatedLoadValue > ulTimerCountsForOneTick ) ) { ulCalculatedLoadValue = ( ulTimerCountsForOneTick - 1UL ); } portNVIC_SYSTICK_LOAD_REG = ulCalculatedLoadValue; ulCompleteTickPeriods = xExpectedIdleTime - 1UL; } else //外部中断唤醒的,需要进行时间补偿 { ulCompletedSysTickDecrements = ( xExpectedIdleTime * ulTimerCountsForOneTick ) - portNVIC_SYSTICK_CURRENT_VALUE_REG; ulCompleteTickPeriods = ulCompletedSysTickDecrements ulTimerCountsForOneTick; portNVIC_SYSTICK_LOAD_REG = ( ( ulCompleteTickPeriods + 1UL ) * ulTimerCountsForOneTick ) - ulCompletedSysTickDecrements; } //重新启动滴答定时器,滴答定时器的重装载值设置为正常值。 portNVIC_SYSTICK_CURRENT_VALUE_REG = 0UL; portENTER_CRITICAL(); { portNVIC_SYSTICK_CTRL_REG |= portNVIC_SYSTICK_ENABLE_BIT; vTaskStepTick( ulCompleteTickPeriods ); (15) 调用函数 vTaskStepTick() 补偿系统时钟,函数参数是要补偿的值,此函数在文件 tasks.c 中有如下定义
void vTaskStepTick(const TickType_t xTicksToJump)
{configASSERT((xTickCount+xTickToJump)<=xNextTaskUnblockTime);xTickCount+=xTicksToJump;traceINCREASE_TICK_COUNT(xTicksToJump);
}portNVIC_SYSTICK_LOAD_REG = ulTimerCountsForOneTick - 1UL; } portEXIT_CRITICAL(); }
}
2.2.3 宏 configPRE_SLEEP_PROCESSING() 和 configPOST_SLEEP_PROCESSING()
在真正的低功耗设计中不仅仅是将处理器设置到低功耗模式就行了,还需要做一些其他的处理:
- 将处理器降低到合适的频率,因为频率越低功耗越小,甚至可以在进入低功耗模式以后关闭系统时钟。
- 修改时钟源,晶振的功耗肯定比处理器内部的时钟源高,进入低功耗模式以后可以切换到内部时钟源,比如 STM32 的内部 RC 振荡器。
- 关闭其他外设时钟,比如 IO 口的时钟。
- 关闭板子上其他功能模块电源,这个需要在产品硬件设计的时候处理好,比如可以通过 MOS 管来控制某个模块电源的开关,在处理器进入低功耗模式之前关闭这些模块的电源。
上面列举出的这几点在处理器进入低功耗模式之前就要完成处理。FreeRTOS 为我们提供了一个宏来完成这些操作,它就是 configPRE_SLEEP_PROCESSING(),这个宏的具体实现内容需要用户去编写。如果在进入低功耗之前我们降低了处理器频率、关闭了某些外设时钟的话,那么在退出低功耗模式以后就需要恢复处理器频率、重新打开外设时钟等,这些操作在宏 configPOST_SLEEP_PROCESSING() 中完成,同样的这个宏的具体内容也需要用户去编写。这两个宏会被函数 vPortSuppressTicksAndSleep() 调用。
/*****************************************************************************************//* FreeRTOS 与低功耗管理相关的配置 */
/*****************************************************************************************/extern void PreSleepProcessing(uint32_t ulExpectedIdleTime);
extern void PostSleepProcessing(uint32_t ulExpectedIdleTime);//进入低功耗模式处理前要做的处理
#define configPRE_SLEEP_PROCESSING PreSleepProcessing
//退出低功耗模式后要做的处理
#define configPOST_SLEEP_PROCESSING PostSleepProcessing
2.2.4 宏 configEXPECTED_IDLE_TIME_BEFORE_SLEEP
处理器工作在低功耗模式的时间虽说没有任何限制,1 个时钟节拍也行,滴答定时器所能计时的最大值也行。但是时间太短的话意义不大,就 1 个时钟节拍,也就是说刚进入低功耗模式就要退出来!所以我们必须对工作在低功耗模式的时间做一个限制,不能太短了,宏 configEXPECTED_IDLE_TIME_BEFORE_SLEEP 就是用来完成这个功能的。
#ifndef configEXPECTED_IDLE_TIME_BEFORE_SLEEP#define configEXPECTED_IDLE_TIME_BEFORE_SLEEP 2
#endif#if configEXPECTED_IDLE_TIME_BEFORE_SLEEP < 2#error configEXPECTED_IDLE_TIME_BEFORE_SLEEP must not be less than 2
#endif
默认情况下, configEXPECTED_IDLE_TIME_BEFORE_SLEEP 为 2 个时钟节拍,并且最小不能小于 2 个时钟节拍。如果要修改这个值的话可以在文件 FreeRTOSConfig.h 中对其重新定义。此宏会在空闲任务函数 prvIdleTask() 中使用!
3. 低功耗 Tickless 模式实验
对于功耗要求严格的场合一般不要求有太大的数据处理量,因为功耗与性能很难兼得。一般的低功耗场合都是简单的数据采集设备或者小型的终端控制设备。它们的功能都很简单,周期性的采集数据并且发送给上层,比如服务器,或者接收服务器发送来的指令执行相应的控制操作,比如开灯关灯、开关电机等。
本实验我们就设计一个通过串口发送指令来控制开发板的 LED1 和 BEEP 开关的实验。
使用低功耗 Tickless 模式需要将宏 configUSE_TICKLESS_IDLE 设置为 1;
3.1 main.c
#include "stm32f4xx.h"
#include "FreeRTOS.h" //这里注意必须先引用FreeRTOS的头文件,然后再引用task.h
#include "task.h" //存在一个先后的关系
#include "LED.h"
#include "LCD.h"
#include "Key.h"
#include "usart.h"
#include "delay.h"
#include "string.h"
#include "beep.h"
#include "malloc.h"
#include "timer.h"
#include "queue.h"
#include "semphr.h"//任务优先级
#define START_TASK_PRIO 1 //用于创建其他两个任务
//任务堆栈大小
#define START_STK_SIZE 256
//任务句柄
TaskHandle_t StartTask_Handler;
//任务函数
void start_task(void *pvParameters);//任务优先级
#define TASK1_TASK_PRIO 2 //控制 LED0 闪烁,提示系统正在运行
//任务堆栈大小
#define TASK1_STK_SIZE 256
//任务句柄
TaskHandle_t Task1Task_Handler;
//任务函数
void task1_task(void *pvParameters);//任务优先级
#define DATAPROCESS_TASK_PRIO 3 //指令处理函数
//任务堆栈大小
#define DATAPROCESS_STK_SIZE 256
//任务句柄
TaskHandle_t DataProcess_Handler;
//任务函数
void DataProcess_task(void *pvParameters);//二值信号量句柄
SemaphoreHandle_t BinarySemaphore; //二值信号量句柄//用于命令解析用的命令值
#define LED1ON 1
#define LED1OFF 2
#define BEEPON 3
#define BEEPOFF 4
#define COMMANDERR 0xFF//进入低功耗模式前需要处理的事件
//ulExpectedIdleTime:低功耗模式运行时间
void PreSleepProcessing(uint32_t ulExpectedIdleTime) //因为二值信号量实验用到了串口,所以对GPIOB~H时钟不使能!
{//关闭某些低功耗模式下不使用的外设时钟RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB,DISABLE);RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOC,DISABLE);RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOD,DISABLE);RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOE,DISABLE);RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOF,DISABLE);RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOG,DISABLE);RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOH,DISABLE);
}//退出低功耗模式以后需要处理的事情
//ulExpectedIdleTime:低功耗模式运行时间
void PostSleepProcessing(uint32_t ulExpectedIdleTime)
{//退出低功耗模式以后打开那些被关闭的外设时钟RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB,ENABLE);RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOC,ENABLE);RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOD,ENABLE);RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOE,ENABLE);RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOF,ENABLE);RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOG,ENABLE);RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOH,ENABLE);
}//函数 LowerToCap 用于将串口发送过来的命令中的小写字母统一转换成大写字母,
//这样就可以在发送命令的时候不用区分大小写,因为开发板会统一转换成大写。
//将字符串中的小写字母转换为大写
//str:要转换的字符串
//len:字符串长度
void LowerToCap(u8 *str,u8 len)
{u8 i;for(i=0;i<len;i++){//判断字符串的ASCII码是否位于96到123之间if((96<str[i])&&(str[i]<123)) //小写字母{//ASCII码是一种用于表示字符的编码系统。在ASCII码中,每个字符都被赋予一个唯一的整数值。//大写字母的ASCII码值是65到90//小写字母的ASCII码值是97到122 所以一旦确定ASCII码值位于小写字母的范畴内,只需要将ASCII码值减去32即可转换为大写str[i] = str[i] - 32; //转换为大写}}
}//函数 CommandProcess 用于将接收到的命令字符串转换成命令值,比如说命令“LED1ON”转换成命令值就是 0(宏LED1ON为 0)
//命令处理函数,将字符串命令转换成命令值
//str:命令
//返回值:0xFF,命令错误;其他值,命令值
u8 CommandProcess(u8 *str)
{u8 CommandValue = COMMANDERR;if(strcmp((char*)str,"LED1ON")==0) //strcmp 字符串比较函数//这个函数会比较两个参数;比较时,会以字符的ASCII值进行比较//如果str1的ASCII码值小于str2,返回一个负数;反之,返回一个正数;//如果str1的ASCII码值等于str2,返回 0,此时,if判断语句成立CommandValue = LED1ON; //设置的LED1ON的宏为1,也就是在串口输入1,if判断语句成立else if(strcmp((char*)str,"LED1OFF")==0)CommandValue = LED1OFF; //在串口输入2,if判断语句成立else if(strcmp((char*)str,"BEEPON")==0)CommandValue = BEEPON; //在串口输入3,if判断语句成立else if(strcmp((char*)str,"BEEPOFF")==0)CommandValue = BEEPOFF; //在串口输入4,if判断语句成立return CommandValue;
}int main(void)
{NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4); //设置系统中断优先级delay_init(168);uart_init(115200);LED_Init();KEY_Init();BEEP_Init();LCD_Init();my_mem_init(SRAMIN); //初始化内部内存池POINT_COLOR=RED;LCD_ShowString(10,10,200,16,16,"ATK STM32F407");LCD_ShowString(10,30,200,16,16,"FreeRTOS Example");LCD_ShowString(10,50,200,16,16,"Binary Semaphore");LCD_ShowString(10,70,200,16,16,"Command Data:");//创建开始任务xTaskCreate((TaskFunction_t )start_task, //任务函数(const char* )"start_task", //任务名称(uint16_t )START_STK_SIZE, //任务堆栈大小(void* )NULL, //传递给任务函数的参数(UBaseType_t )START_TASK_PRIO, //任务优先级(TaskHandle_t* )&StartTask_Handler); //任务句柄 vTaskStartScheduler(); //开启任务调度
}//开始任务任务函数
void start_task(void *pvParameters)
{taskENTER_CRITICAL(); //进入临界区//创建二值信号量,也就是创建一个长度为1的队列BinarySemaphore = xSemaphoreCreateBinary(); //xSemaphoreCreateBinary函数为动态创建二值信号量函数//返回 NULL,二值信号量创建失败;返回其他值,表示创建成功的二值信号量的句柄;//所以BinarySemaphore表示创建成功的二值信号量的句柄;//创建Task1任务xTaskCreate((TaskFunction_t )task1_task, //任务函数(const char* )"task1_task", //任务名称(uint16_t )TASK1_STK_SIZE, //任务堆栈大小(void* )NULL, //传递给任务函数的参数(UBaseType_t )TASK1_TASK_PRIO, //任务优先级(TaskHandle_t* )&Task1Task_Handler); //任务句柄 //创建Task2任务xTaskCreate((TaskFunction_t )DataProcess_task, //任务函数(const char* )"DataProcess_task", //任务名称(uint16_t )DATAPROCESS_STK_SIZE, //任务堆栈大小(void* )NULL, //传递给任务函数的参数(UBaseType_t )DATAPROCESS_TASK_PRIO, //任务优先级(TaskHandle_t* )&DataProcess_Handler); //任务句柄 vTaskDelete(StartTask_Handler); //删除开始任务taskEXIT_CRITICAL(); //退出临界区
}//Task1任务
//控制 LED0 闪烁,提示系统正在运行
void task1_task(void *pvParameters)
{while(1){LED0=!LED0;vTaskDelay(500); //延时500ms,也就是500个时钟节拍}
}//DataProcess_task函数
//指令处理任务,根据接收到的指令来控制不同的外设
void DataProcess_task(void *pvParameters)
{u8 len=0;u8 CommandValue=COMMANDERR;BaseType_t err=pdFALSE;u8 *CommandStr;while(1){err=xSemaphoreTake(BinarySemaphore,portMAX_DELAY); //获取信号量函数;返回值pdTURE,获取信号量成功;pdFALSE,获取信号量失败;//第一个参数,要获取的信号量句柄//第二个参数,阻塞时间,这里设置为portMAX_DEALY,译为无限等待,直至获得信号量if(err==pdTRUE) //获取信号量成功{len=USART_RX_STA&0x3fff; //得到此次接收到的数据长度//接收状态//bit15, 接收完成标志//bit14, 接收到0x0d//bit13~0, 接收到的有效字节数目CommandStr=mymalloc(SRAMIN,len+1); //申请内存 指针指向申请内存的首地址sprintf((char*)CommandStr,"%s",USART_RX_BUF); //打印接收缓存区,把接收缓存区的数据保存到CommandStr中CommandStr[len]='\0'; //加上字符串结尾符号//CommandStr 是个指针,长度为len,数组是从下角标 0 开始的,所以len就表示数组的最后一个LowerToCap(CommandStr,len); //将字符串转换成大写CommandValue=CommandProcess(CommandStr); //命令解析,也就是获取上面定义的宏 1 2 3 4if(CommandValue!=COMMANDERR)//if判断语句成立,表示CommandValue不等于0xFF,那也就是 LED1ON、LED1OFF、BEEPON、BEEPOFF 其中一个指令{printf("命令为:%s\r\n",CommandStr); switch(CommandValue){case LED1ON:LED1=0;break;case LED1OFF:LED1=1;break;case BEEPON:BEEP=1;break;case BEEPOFF:BEEP=0;break;}}else{//当命令错误的时候开发板会向串口调试助手发送命令错误的提示信息//比如我们发送 LED1_off 这个命令,串口助手会显示:无效的命令,请重新输入!!printf("无效的命令,请重新输入!!\r\n");}USART_RX_STA = 0;memset(USART_RX_BUF,0,USART_REC_LEN); //串口接收缓冲区清零myfree(SRAMIN,CommandStr); //释放内存}}
}
3.2 usart.c
extern SemaphoreHandle_t BinarySemaphore; //信息队列句柄
//QueueHandle_t queue.h 中定义void USART1_IRQHandler(void) //串口1中断服务程序
{u8 Res;//xHigherPriorityTaskWoken:用来标记退出此函数以后是否进行任务切换,这个变量的值由三个函数来设置,用户不再进行设置//用户只需要提供一个变量来保存这个值就可以了。//但是切记要注意:当此值为 pdTURE 的时候在退出中断服务函数之前一定要进行一次任务切换。BaseType_t xHigherPriorityTaskWoken; //BaseType_t 也在 queue.h 中定义if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) //接收中断(接收到的数据必须是0x0d 0x0a结尾){Res =USART_ReceiveData(USART1);//(USART1->DR); //读取接收到的数据if((USART_RX_STA&0x8000)==0)//接收未完成{if(USART_RX_STA&0x4000)//接收到了0x0d{if(Res!=0x0a)USART_RX_STA=0;//接收错误,重新开始else USART_RX_STA|=0x8000; //接收完成了 }else //还没收到0X0D{ if(Res==0x0d)USART_RX_STA|=0x4000;else{USART_RX_BUF[USART_RX_STA&0X3FFF]=Res;USART_RX_STA++;if(USART_RX_STA>(USART_REC_LEN-1))USART_RX_STA=0;//接收数据错误,重新开始接收 } }} }//释放二值信号量//指令通过串口发送给开发板,串口中断用来释放二值信号量,任务用来不断获取信号量//任务一旦获取到信号量,就会从串口接收缓冲区中提取这些指令,然后根据这些指令控制相应的外设if((USART_RX_STA&0x8000)&&(BinarySemaphore!=NULL)) //串口接收到数据,并且二值信号量不为空,也就表示二值信号量是有效的{xSemaphoreGiveFromISR(BinarySemaphore,&xHigherPriorityTaskWoken); //调用在中断中释放二值信号量函数//函数第一个参数:释放二值信号量句柄//函数第二个参数:标记是否需要进行任务切换portYIELD_FROM_ISR(xHigherPriorityTaskWoken); //如果需要的话进行一次任务切换} //二值信号量用来实现同步的意思就是说:保证中断先释放信号量,然后任务在获取信号量;
}