STM32中滴答定时器的使用并进行ms和us级延时
STM32中滴答定时器的使用并进行ms和us级延时
滴答定时器(Systick)
滴答定时器Systick是 stm32 内核中的一个系统定时器,是属于内核的外设。
滴答定时器是一个24位的倒计数定时器,当计数到0时,会从LOAD寄存器中自动重装载定时初值,开始新一轮计数。
在core_cm3.h和core_cm4.h头文件中都会有这样一段代码:
/**
\ingroup CMSIS_core_register
\defgroup CMSIS_SysTick System Tick Timer (SysTick)
\brief Type definitions for the System Timer Registers.
@{
*/
/**
\brief Structure type to access the System Timer (SysTick).
*/
typedef struct
{
__IOM uint32_t CTRL; /*!< Offset: 0x000 (R/W) SysTick Control and Status Register */
__IOM uint32_t LOAD; /*!< Offset: 0x004 (R/W) SysTick Reload Value Register */
__IOM uint32_t VAL; /*!< Offset: 0x008 (R/W) SysTick Current Value Register */
__IM uint32_t CALIB; /*!< Offset: 0x00C (R/ ) SysTick Calibration Register */
} SysTick_Type;
/* SysTick Control / Status Register Definitions */
#define SysTick_CTRL_COUNTFLAG_Pos 16U /*!< SysTick CTRL: COUNTFLAG Position */
#define SysTick_CTRL_COUNTFLAG_Msk (1UL << SysTick_CTRL_COUNTFLAG_Pos) /*!< SysTick CTRL: COUNTFLAG Mask */
#define SysTick_CTRL_CLKSOURCE_Pos 2U /*!< SysTick CTRL: CLKSOURCE Position */
#define SysTick_CTRL_CLKSOURCE_Msk (1UL << SysTick_CTRL_CLKSOURCE_Pos) /*!< SysTick CTRL: CLKSOURCE Mask */
#define SysTick_CTRL_TICKINT_Pos 1U /*!< SysTick CTRL: TICKINT Position */
#define SysTick_CTRL_TICKINT_Msk (1UL << SysTick_CTRL_TICKINT_Pos) /*!< SysTick CTRL: TICKINT Mask */
#define SysTick_CTRL_ENABLE_Pos 0U /*!< SysTick CTRL: ENABLE Position */
#define SysTick_CTRL_ENABLE_Msk (1UL /*<< SysTick_CTRL_ENABLE_Pos*/) /*!< SysTick CTRL: ENABLE Mask */
/* SysTick Reload Register Definitions */
#define SysTick_LOAD_RELOAD_Pos 0U /*!< SysTick LOAD: RELOAD Position */
#define SysTick_LOAD_RELOAD_Msk (0xFFFFFFUL /*<< SysTick_LOAD_RELOAD_Pos*/) /*!< SysTick LOAD: RELOAD Mask */
/* SysTick Current Register Definitions */
#define SysTick_VAL_CURRENT_Pos 0U /*!< SysTick VAL: CURRENT Position */
#define SysTick_VAL_CURRENT_Msk (0xFFFFFFUL /*<< SysTick_VAL_CURRENT_Pos*/) /*!< SysTick VAL: CURRENT Mask */
/* SysTick Calibration Register Definitions */
#define SysTick_CALIB_NOREF_Pos 31U /*!< SysTick CALIB: NOREF Position */
#define SysTick_CALIB_NOREF_Msk (1UL << SysTick_CALIB_NOREF_Pos) /*!< SysTick CALIB: NOREF Mask */
#define SysTick_CALIB_SKEW_Pos 30U /*!< SysTick CALIB: SKEW Position */
#define SysTick_CALIB_SKEW_Msk (1UL << SysTick_CALIB_SKEW_Pos) /*!< SysTick CALIB: SKEW Mask */
#define SysTick_CALIB_TENMS_Pos 0U /*!< SysTick CALIB: TENMS Position */
#define SysTick_CALIB_TENMS_Msk (0xFFFFFFUL /*<< SysTick_CALIB_TENMS_Pos*/) /*!< SysTick CALIB: TENMS Mask */
/*@} end of group CMSIS_SysTick */
可以看出来,该系统定时器一共有4个寄存器,分别为控制/状态寄存器(CTRL)、自动重装载寄存器(LOAD)、当前值寄存器(VAL)、校准寄存器(CALIB)。
详细介绍一下前三个寄存器的功能:
控制/状态寄存器(CTRL):
位端 | 名称 | 类型 | 复位值 | 描述 |
---|---|---|---|---|
16 | COUNTFLAG | R | 0 | 当SysTick计数到了0,该位会自动置1。如果读取该位,该位自动清零。 |
2 | CLKSOURCE | R/W | 0 | 时钟源选择位。1=处理器时钟AHB,0=AHB/8。 |
1 | TICKINT | R/W | 0 | 中断使能位。置1,SysTick倒数计数到 0时产生,SysTick异常请求,置0,数到0时无动作。 |
0 | ENABLE | R/W | 0 | SysTick定时器的使能位。 |
自动重装载寄存器(LOAD):
位端 | 名称 | 类型 | 复位值 | 描述 |
---|---|---|---|---|
23:0 | RELOAD | R/W | 0 | 当定时器计数至0时,将被重装载的值。 |
当前值寄存器(VAL):
位端 | 名称 | 类型 | 复位值 | 描述 |
---|---|---|---|---|
23:0 | CURRENT | R/Wc | 0 | 读取时返回当前倒计数的值,写它则使之清零,同时还会清除在CTRL寄存器的COUNTFLAG位。 |
hal库中滴答定时器的配置
在通过cubemax自动生成的HAL代码中,是如何对滴答定时器进行配置的呢?
首先,main
函数中第一个函数HAL_Init()
,使用如下代码进行了配置:
if (HAL_InitTick(TICK_INT_PRIORITY) != HAL_OK)
{
status = HAL_ERROR;
}
而TICK_INT_PRIORITY
是一个宏,定义为15UL
,这个参数将作为配置滴答定时器中断的优先级。(设置成了最低优先级)
HAL_InitTick()
函数具体代码如下:
__weak HAL_StatusTypeDef HAL_InitTick(uint32_t TickPriority)
{
HAL_StatusTypeDef status = HAL_OK;
if (uwTickFreq != 0U)
{
/* Configure the SysTick to have interrupt in 1ms time basis*/
if (HAL_SYSTICK_Config(SystemCoreClock / (1000U / uwTickFreq)) == 0U)
{
/* Configure the SysTick IRQ priority */
if (TickPriority < (1UL << __NVIC_PRIO_BITS))
{
HAL_NVIC_SetPriority(SysTick_IRQn, TickPriority, 0U);
uwTickPrio = TickPriority;
}
else
{
status = HAL_ERROR;
}
}
else
{
status = HAL_ERROR;
}
}
else
{
status = HAL_ERROR;
}
/* Return function status */
return status;
}
首先通过HAL_SYSTICK_Config()
函数设置了滴答定时器的定时周期,然后配置其中断优先级为最低优先级。
HAL_SYSTICK_Config()
函数传入的参数SystemCoreClock / (1000U / uwTickFreq)
,其中SystemCoreClock
为系统频率SYSCLK,表示单片机的主频,uwTickFreq
定义为了1,代表1kHz。
HAL_SYSTICK_Config()
函数最终进入SysTick_Config()
函数,如下所示:
__STATIC_INLINE uint32_t SysTick_Config(uint32_t ticks)
{
if ((ticks - 1UL) > SysTick_LOAD_RELOAD_Msk)
{
return (1UL); /* Reload value impossible */
}
SysTick->LOAD = (uint32_t)(ticks - 1UL); /* set reload register */
NVIC_SetPriority (SysTick_IRQn, (1UL << __NVIC_PRIO_BITS) - 1UL); /* set Priority for Systick Interrupt */
SysTick->VAL = 0UL; /* Load the SysTick Counter Value */
SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk |
SysTick_CTRL_TICKINT_Msk |
SysTick_CTRL_ENABLE_Msk; /* Enable SysTick IRQ and SysTick Timer */
return (0UL); /* Function successful */
}
简单来讲,就是设置了定时器的重装载寄存器LOAD,设置了中断优先级,清空了当前值VALUE,最后设置了滴答定时器的时钟源(为AHB时钟),开启定时器中断,使能定时器。
通过上面的配置,滴答定时器就可以以1ms的周期进行中断触发,HAL_Delay()
函数就是通过滴答定时器中断进行ms延时的。
由于
HAL_Delay()
函数就是通过滴答定时器中断进行ms延时,而滴答定时器中断优先级被设置成了最低,所以在其他中断服务函数中不能使用HAL_Delay()
函数,调用的话会导致程序卡死。(因为低优先级的中断不能打断高优先级的中断,而其他的中断服务函数的中断优先级肯定大于等于滴答定时器中断优先级)
使用滴答定时器进行ms和us级延时
如何在不影响HAL_Delay()
函数使用的前提下使用滴答定时器进行ms和us级延时?
大致思路是:在延时之前将滴答定时器的中断关闭,设置滴答定时器需要延时的时间并赋值,开启定时器,然后通过轮询的方式判断滴答定时器是否计数到零;延时完毕之后,再恢复滴答定时器原来的状态即可。
us级延时:
void delay_us(uint32_t us)
{
uint32_t load_before = SysTick->LOAD;
uint32_t tmp;
if(us <= 0)
return;
SysTick->CTRL &= ~SysTick_CTRL_TICKINT_Msk; //关闭滴答定时器中断
SysTick->LOAD = us*(load_before+1)/1000 - 1; //设置重装载值
SysTick->VAL = 0x00; //将定时器归零
//这里通过循环判断定时器的状态位值来确认定时器是否已归零
do{
tmp = SysTick->CTRL; //获取定时器的状态值
}while(tmp & 0x01 && !(tmp & (1 << 16)));
SysTick->CTRL &= ~SysTick_CTRL_ENABLE_Msk; //关闭定时器
//恢复
SysTick->LOAD = load_before; //设置重装载值
SysTick->VAL = 0x00; //将定时器归零
SysTick->CTRL |= SysTick_CTRL_TICKINT_Msk; //开启滴答定时器中断
SysTick->CTRL |= SysTick_CTRL_ENABLE_Msk; //开启定时器
}
1us电平翻转间隔如下图所示:
2us电平翻转间隔如下图所示:
可以看到,当进行很小的us级延时时,还是会存在0.5us内的延时误差。
如果很介意这个误差们可以修改函数内的SysTick->LOAD = us*(load_before+1)/1000 - 1;
语句,将减的1改为更大的数(比如减61),从而进行误差补偿。减的数根据单片机主频而定,需要调试进行确定。
ms级延时:
void delay_ms(uint32_t ms)
{
uint32_t load_before = SysTick->LOAD;
uint32_t tmp;
if(ms <= 0)
return;
SysTick->CTRL &= ~SysTick_CTRL_TICKINT_Msk; //关闭滴答定时器中断
SysTick->LOAD = load_before; //设置重装载值
SysTick->VAL = 0x00; //将定时器归零
//这里通过循环判断定时器的状态位值来确认定时器是否已归零
do{
do{
tmp = SysTick->CTRL; //获取定时器的状态值
}while(tmp & 0x01 && !(tmp & (1 << 16)));
ms--;
}while(ms);
SysTick->CTRL &= ~SysTick_CTRL_ENABLE_Msk; //关闭定时器
//恢复
SysTick->LOAD = load_before; //设置重装载值
SysTick->VAL = 0x00; //将定时器归零
SysTick->CTRL |= SysTick_CTRL_TICKINT_Msk; //开启滴答定时器中断
SysTick->CTRL |= SysTick_CTRL_ENABLE_Msk; //开启定时器
}
1ms电平翻转间隔如下图所示: