目录标题
前言
在使用通讯协议来驱动外设的时候需要遵循严格的时序逻辑,往往用到微秒(us)级别的延时,在裸机编程中可以使用SysTick定时器、软件堵塞等方法来实现;但是在FreeRTOS中,SysTick定时器则是用来作为FreeRTOS系统时钟的,并且它提供的API仅能实现毫秒级别的延时,所以在网上查找资料总结了几个在FreeRTOS上运行的,可以实现us级别延时的几个方案。
1、利用SysTick(滴答)定时器
1.1、滴答定时器简介
SysTick定时器被捆绑在NVIC中,用于产生SysTick异常(异常号15)。滴答中断对操作系统尤其重要。例如,操作系统可以为多个任务许以不同数目的时间片,确保没有一个任务能霸占系统;或者把每个定时器周期的某个时间范围赐予特定的任务等,还有操作系统提供的各种定时功能,都与这个滴答定时器有关。FreeRTOS的节拍器就是使用的SysTick(系统滴答)定时器。
SysTick-系统定时器是CM3内核中的一个外设,内嵌在NVIC中,是一个24位向下递减的计数器,所有基于CM3内核的单片机都具有这个系统定时器,当重装载数值寄存器的值递减到0的时候,系统定时器就产生一次中断,以此循环往复!
有四个寄存器来控制SysTick定时器,如下图所示:
1.2、裸机中使用SysTick实现延时
操作步骤:
a.根据延时时间和定时器所选时钟频率,计算出定时器要计数的时间数值;
b.将该数值加载到重装载寄存器中;
c.将当前值寄存器清零,打开定时器开始计数;
d.等待控制及状态寄存器的位16变为1;
e.关闭定时器,退出。
程序代码:
/**
* @brief 微秒级延时
* @param xus 延时时长,范围:0~233015
* @retval 无
*/
void Delay_us(uint32_t xus)
{
SysTick->LOAD = 72 * xus; //设置定时器重装值 (系统滴答定时器SysTick:当加入操作系统时可利用其提供定时以完成任务的切换)
SysTick->VAL = 0x00; //清空当前计数值
SysTick->CTRL = 0x00000005; //设置时钟源为HCLK,启动定时器
while(!(SysTick->CTRL & 0x00010000)); //等待计数到0
SysTick->CTRL = 0x00000004; //关闭定时器
}
1.3、在FreeRTOS中使用SysTick实现延时
在FreeRTOS中,系统滴答定时器作为操作系统的时基是一直开启的,并且重装载值是固定的,所以向上面裸机的方法操作就不可行了,我们可以这样做:
操作步骤:
a.根据延时时间和定时器所选时钟频率,计算出定时器要计数的时间数值;
b.获取当前数值寄存器的数值;
c.以当前数值为基准开始计数;
d.当所计数值等于(大于)需要延时的时间数值时退出。
注:计数时间值的计算,我们以延时10us,时钟频率为72MHZ的STM32F103C8T6来计算,
计数值 = 延时时间/1S × 时钟频率 = 0.000 01/1 *72 000 000 = 720
void delay_us(u32 nus)
{
u32 ticks;
u32 told,tnow,reload,tcnt=0;
if((0x0001&(SysTick->CTRL)) ==0) //定时器未工作
vPortSetupTimerInterrupt(); //初始化定时器
reload = SysTick->LOAD; //获取重装载寄存器值
ticks = nus * (SystemCoreClock / 1000000); //计数时间值
vTaskSuspendAll();//阻止OS调度,防止打断us延时
told=SysTick->VAL; //获取当前数值寄存器值(开始时数值)
while(1)
{
tnow=SysTick->VAL; //获取当前数值寄存器值
if(tnow!=told) //当前值不等于开始值说明已在计数
{
if(tnow<told) //当前值小于开始数值,说明未计到0
tcnt+=told-tnow; //计数值=开始值-当前值
else //当前值大于开始数值,说明已计到0并重新计数
tcnt+=reload-tnow+told; //计数值=重装载值-当前值+开始值 (
//已从开始值计到0)
told=tnow; //更新开始值
if(tcnt>=ticks)break; //时间超过/等于要延迟的时间,则退出.
}
}
xTaskResumeAll(); //恢复OS调度
}
//SystemCoreClock为系统时钟(system_stmf4xx.c中),通常选择该时钟作为
//systick定时器时钟,根据具体情况更改
2、利用硬件定时器实现延时函数
暂时不写了,比较浪费开发板资源。和裸机情况下使用系统滴答定时器来延时类似。
3、利用DWT实现延时函数
3.1、DWT简介
Corex-M中的DWT外设本身是用于系统调试和追踪的。
跟踪组件:数据观察点与跟踪(DWT)
- 它包含了 4 个比较器,可以配置成在发生比较匹配时,执行如下动作:
- 硬件观察点(产生一个观察点调试事件,并且用它来调用调试模式,包括停机模式和调试监视器模式
- ETM 触发,可以触发 ETM 发出一个数据包,并汇入指令跟踪数据流中
- 程序计数器(PC)采样器事件触发
- 数据地址采样器触发
- 第一个比较器还能用于比较时钟周期计数器(CYCCNT),用于取代对数据地址的比较
- 作为计数器,DWT 可以对下列项目进行计数:
- 时钟周期(CYCCNT)
- 被折叠(Folded)的指令
- 对加载/存储单元(LSU)的操作
- 睡眠的时钟周期
- 每指令周期数(CPI)
- 中断的额外开销(overhead)
- 以固定的周期采样 PC 的值
- 中断事件跟踪
我们在使用其完成延时功能时用到的正是DWT对时钟周期计数的功能。
3.2、延时功能使用到的DWT寄存器
要实现延时的功能,总共涉及到三个寄存器:DEMCR 、DWT_CTRL、DWT_CYCCNT,它们的地址分别为:0xE000EDFC、0xE0001000、0xE0001004,分别用于开启DWT功能、开启CYCCNT及获得系统时钟计数值。
3.2、具体实现方法
我们需要按以下步骤进行操作:
a.对DEMCR寄存器的位24控制,写1使能DWT外设。
b.对于DWT的CYCCNT寄存器清0。
c.对DWT控制寄存器的位0控制,写1使能CYCCNT寄存器。
d.通过编写函数获取DWT_CYCCNT的值,并通过判断是否到达新的DWT_CYCCNT值来实现延时。
由此我们可以得到一个32位向上累加的计数器,溢出会自动清零并累加,频率是系统主频。
具体配置函数:
#define DEM_CR *(volatile u32 *)0xE000EDFC
#define DWT_CR *(volatile u32 *)0xE0001000
#define DWT_CYCCNT *(volatile u32 *)0xE0001004
#define DEM_CR_TRCENA (1 << 24)
#define DWT_CR_CYCCNTENA (1 << 0)
void DWT_Init()
{
DEM_CR |= DEM_CR_TRCENA; /*对DEMCR寄存器的位24控制,写1使能DWT外设。*/
DWT_CYCCNT = 0;/*对于DWT的CYCCNT计数寄存器清0。*/
DWT_CR |= DWT_CR_CYCCNTENA;/*对DWT控制寄存器的位0控制,写1使能CYCCNT寄存器。*/
}
void DWT_DelayUS(uint32_t _ulDelayTime)
{
uint32_t tCnt, tDelayCnt;
uint32_t tStart;
tStart = DWT_CYCCNT; /* 刚进入时的计数器值 */
tCnt = 0;
tDelayCnt = _ulDelayTime * (SystemCoreClock / 1000000);
/* 需要的节拍数 */ /*SystemCoreClock :系统时钟频率*/
while(tCnt < tDelayCnt)
{
tCnt = DWT_CYCCNT - tStart;
/* 求减过程中,如果发生第一次32位计数器重新计数,依然可以正确计算 */
}
}
void DWT_DelayMS(uint32_t _ulDelayTime)
{
bsp_DelayUS(1000*_ulDelayTime);
}
3.3、DWT延时优缺点
优点:方便移植,经过测试在M3、M4、M7内核的MCU上都可以使用。
缺点:和定时器一样,都有一个延时的最大时间,测量代码运行时间的最大值。