在【STM32F103筆記】2、單片機中的HelloWorld——流水燈中我們曾寫過一個簡單的延時函數,利用空操作函數__nop()並大致計算延時時間,但這個函數並不精確,有興趣的朋友可以再把那一篇中的程序運行結果和標準時鍾比較一下。
這一篇中將使用Cortex-M3內核自帶的系統時鐘 (System Time)設計精確的延時函數。
SysTick
Cortex-M3內核自帶一個24位的降序計數器,也就是SysTick,通常Systick是用於給實時操作系統提供準確的滴答時鐘。
在這裏將SysTick用於精確的計時,先介紹一下2個SysTick相關的寄存器:
- SysTick control and status register (STK_CTRL):控制和狀態寄存器,下圖是這個寄存器每一位的說明:
- Bit 16 COUNTFLAG:每次計數器向下自減到0的時候會自動置1,表示計數完成;
- Bit 2 CLKSOURCE:時鐘來源選擇,可以看出SysTick的時鐘源是AHB,在第二篇中可知,系統初始化後AHB時鐘爲72MHz,通過這一位可以選擇AHB時鐘不分頻或者8分頻作爲Systick的時鐘;
- Bit 1 TICKINT:是否觸發中斷,觸發中斷則會進入SysTick_Handler函數(stm32f10x_it.c文件中);
- Bit 0 ENABLE:SysTick使能位,置1則使能SysTick,開始自減計數,清0則禁用SysTick。
- SysTick reload value register (STK_LOAD):計數器加載值寄存器,每次SysTick計數器重新開始計數時,將讀取這個寄存器中的值,也就是說,計多少個數由這個寄存器控制。
由於SysTick的計數器自減操作不受其它干擾和影響,完全由其時鐘決定,若時鐘源選擇AHB 72MHz,那麼當計數器從72計數到0,相當於時間1us,因此可以使用SysTick來精確計算時間。
程序設計
程序設計思路是,首先初始化SysTick,設置固定的計數(比如720),並且每次計數完成後觸發中斷,這樣每次觸發中斷就是固定的事件(10us)。
初始化SysTick——SysTick_Config庫函數
同樣,官方庫中提供了用於SysTick配置初始化的函數:
/**
* @brief Initialize and start the SysTick counter and its interrupt.
*
* @param ticks number of ticks between two interrupts
* @return 1 = failed, 0 = successful
*
* Initialise the system tick timer and its interrupt and start the
* system tick timer / counter in free running mode to generate
* periodical interrupts.
*/
static __INLINE uint32_t SysTick_Config(uint32_t ticks)
{
if (ticks > SysTick_LOAD_RELOAD_Msk) return (1); /* Reload value impossible */
SysTick->LOAD = (ticks & SysTick_LOAD_RELOAD_Msk) - 1; /* set reload register */
NVIC_SetPriority (SysTick_IRQn, (1<<__NVIC_PRIO_BITS) - 1); /* set Priority for Cortex-M0 System Interrupts */
SysTick->VAL = 0; /* 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 (0); /* Function successful */
}
#endif
- SysTick_Config函數用於配置SysTick進行固定的計數,並在每次計數完成後觸發中斷;
- 函數參數ticks就是計數值,將寫入SysTick的LOAD寄存器;
- 首先判斷ticks的值是否超過LOAD寄存器的限制,然後將值寫入LOAD寄存器;
- 配置SysTick的中斷優先級;
- 將VAL寄存器(當前計數值寄存器)清0;
- 然後設置CTRL寄存器:
- 時鐘源配置爲AHB不分頻,即72MHz;
- 允許觸發中斷;
- 使能SysTick。
- 若設置成功函數將返回0。
延時函數
在工程的USER文件夾下新建delay.c、delay.h兩個文件用於存放延時函數,並且方便以後使用。
delay.h
這裏直接先給出delay.h文件的內容:
/**
******************************************************************************
* @file delay.h
* @author TTZZWWW
* @version
* @date
* @brief Delay functions.
******************************************************************************
* @attention
* Delay functions should be consistent with initialized SysTick clock.
******************************************************************************
*/
#ifndef __DELAY_H
#define __DELAY_H
/* Includes ------------------------------------------------------------------*/
#include <stdint.h>
/* Defines -------------------------------------------------------------------*/
#define DELAYCLOCK_1us SystemCoreClock/1000000
#define DELAYCLOCK_10us SystemCoreClock/100000
#define DELAYCLOCK_100us SystemCoreClock/10000
#define DELAYCLOCK_1ms SystemCoreClock/1000
/* External variables --------------------------------------------------------*/
extern uint32_t DelayTimeCount;
/**
* @brief Initialize SysTick for delay functions
* @param Delay clock, already defined in delay.h
* @return None
*/
extern void DelayInitSysTick(uint32_t delayclock);
/**
* @brief delay us
* @param us
* @return None
*/
extern void delay_us(uint32_t us);
/**
* @brief delay ms
* @param ms
* @return None
*/
extern void delay_ms(uint32_t ms);
#endif /* __DELAY_H */
- 作爲頭文件,需要考慮在同一個工程的不同文件中都包含時應不引起重複定義的錯誤,即include了一次就不用再include了,因此在文件頭尾加入:
#ifndef __DELAY_H
#define __DELAY_H
...
#endif /* __DELAY_H */
- 爲了方便延時程序初始化SysTick的ticks計算,設置如下計數值宏定義,其中SystemCoreClock是官方庫中定義的系統內核時鐘頻率,這裏是72MHz,具體計算方法上面已經提到了,這裏提供1us、10us、100us、1ms計數值,也就是每次進入SysTick_Handler中斷的固定時間:
#define DELAYCLOCK_1us SystemCoreClock/1000000
#define DELAYCLOCK_10us SystemCoreClock/100000
#define DELAYCLOCK_100us SystemCoreClock/10000
#define DELAYCLOCK_1ms SystemCoreClock/1000
- DelayTimeCount在delay.c文件中定義,用於計數進入中斷的次數,也就是延時了多少個固定時間;這裏使用extern關鍵字,因爲DelayTimeCount在delay.c文件中定義,而需要在其它文件中使用;
- 最後是3個函數聲明,同樣爲了能在其它文件中使用,需要加上extern關鍵字:
extern void DelayInitSysTick(uint32_t delayclock);
extern void delay_us(uint32_t us);
extern void delay_ms(uint32_t ms);
初始化函數 DelayInitSysTick
DelayInitSysTick()函數用於初始化SysTick:
/**
* @brief Initialize SysTick for delay functions
* @param None
* @retval None
*/
void DelayInitSysTick(uint32_t delayclock)
{
if (SysTick_Config(delayclock))
while(1);
DelayClock = delayclock;
SysTick->CTRL &= ~SysTick_CTRL_ENABLE_Msk;
}
- 調用庫函數SysTick_Config(delayclock)對SysTick進行配置,並將LOAD寄存器的值保存在uint3_t DelayClock變量中,用於後續計算延時,最後禁用SysTick,因爲我們現在僅需要在進行延時的時候使用SysTick。
微秒級延時函數 delay_us
delay_us()函數用於進行微秒級的延時:
/**
* @brief delay us
* @param us
* @return None
*/
void delay_us(uint32_t us)
{
DelayTimeCount = us * 72 / DelayClock;
SysTick->CTRL |= SysTick_CTRL_ENABLE_Msk;
while(DelayTimeCount);
SysTick->CTRL &= ~SysTick_CTRL_ENABLE_Msk;
}
- 首先根據SysTick的LOAD寄存器中的值(假設取DELAYCLOCK_10us=720)計算中斷的次數,假設延時1000us,則DelayTimeCount計算結果爲100,即SysTick計數720進入中斷,每次進入中斷合時間爲10us,並在中斷中將DelayTimeCount的值減1,因此進入100次中斷就可以延時1000us;
- 然後使能SysTick,開始延時;
- 通過DelayTimeCount的值來判斷延時是否完成;
- 最後關閉SysTick。
毫秒級延時函數 delay_ms
和delay_us相似:
/**
* @brief delay ms
* @param ms
* @return None
*/
void delay_ms(uint32_t ms)
{
DelayTimeCount = ms * 72000 / DelayClock;
SysTick->CTRL |= SysTick_CTRL_ENABLE_Msk;
while(DelayTimeCount);
SysTick->CTRL &= ~SysTick_CTRL_ENABLE_Msk;
}
中斷處理函數 SysTick_Handler
在中斷處理函數SysTick_Handler中先判斷DelayTimeCount是否已經爲0,若否則將其減1(注意stm32f10x_it.c文件中要include “delay.h”):
#include "delay.h"
...
/**
* @brief This function handles SysTick Handler.
* @param None
* @retval None
*/
void SysTick_Handler(void)
{
if(DelayTimeCount != 0)
DelayTimeCount--;
}
這樣就完成了延時函數的設計,在main函數中我們通過LED亮滅來驗證。
驗證程序
/* Includes ------------------------------------------------------------------*/
#include "stm32f10x.h"
#include "delay.h"
/* Private functions ---------------------------------------------------------*/
void LEDPB8Config(void);
int main(void)
{
LEDPB8Config();
DelayInitSysTick(DELAYCLOCK_1ms);
while(1)
{
GPIO_WriteBit(GPIOB, GPIO_Pin_8, (BitAction)(1-GPIO_ReadOutputDataBit(GPIOB, GPIO_Pin_8)));
delay_ms(1000);
}
}
void LEDPB8Config(void)
{
GPIO_InitTypeDef GPIOInitStruct;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
GPIOInitStruct.GPIO_Pin = GPIO_Pin_8;
GPIOInitStruct.GPIO_Mode = GPIO_Mode_Out_PP;
GPIOInitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIOInitStruct);
}
運行結果
可以看到LED以1s的延時進行閃爍,將其與標準時鍾比較,可以發現延時還是很準的: