單獨拿出來講的一個內核外設(所以不要期望在STM32中文參考手冊找到它!即使找到也只會叫你看cm3內核編程手冊),說明它真的很重要。
一、系統定時器Systick
1. SysTick簡介
SysTick是一個24位的系統節拍定時器,具有自動重載和溢出中斷功能,所有基於Cortex M3或Cortex M4處理器的微控制器都有這個定時器。
Systick定時器常用來做延時,或者用來做實時系統的心跳時鍾。這樣可以節省MCU資源,不用浪費一個定時器。比如UCOS中,分時復用,需要一個最小的時間戳,一般在STM32+UCOS系統中,都采用Systick做UCOS心跳時鍾。
它一個24位的倒計數定時器(意味着有2^24個時間間隔),計到0時,將從RELOAD寄存器中自動重裝載定時初值。只要不把它在SysTick控制及狀態寄存器中的使能位清除,就永不停息,即使在睡眠模式下也能工作。
學過51單片機的同學應該對計數器比較熟悉了。
SysTick在NVIC的中斷向量號為6(可對照STM32中文參考手冊9.1.2節中斷和異常向量中表格的灰色部分)。
2. SysTick相關寄存器
SysTick有4個寄存器:CTRL(控制和狀態寄存器)、LOAD(自動重裝載除值寄存器)、VAL(當前值寄存器)、CALIB(校准值寄存器)(如圖所示)。
關於這4個寄存器的參考資料為 《 STM32F10xxx Cortex-M3編程手冊》(STM32F10xxx/20xxx/21xxx/L1xxxx
Cortex ® -M3 programming manual)(沒找到中文版的,自己生啃一下英文吧),4.5節SysTick timer (STK)。4.5.6節還有SysTick寄存器地圖。
由於篇幅夠多,下面我們來一一介紹這4個寄存器(其實引用了上述手冊的介紹):
(1)SysTick control and status register (STK_CTRL)
16位(COUNTFLAG): SysTick數到0后,該位為1;其他情況為0。如果讀取該位,也會置0。說白了就是用來標志是不是數完了。
2位(CLKSOURCE): 0為外部時鍾源(STCLK)(HCLK的1/8),1為內部時鍾源(FCLK)(HCLK)。用來選擇時鍾源的。順便提一句,HCLK用的是AHB時鍾。
1位(TICKINT): 1為計數器數到零時產生SysTick中斷請求,0為不產生請求。
0位(ENABLE): 用來使能的。
其余位保留。
(2)SysTick reload value register (STK_LOAD)
23位-0位(RELOAD): 存儲的是一個值,當計數到0時,將重新裝載該值,開始新一輪倒數。
其余位保留。
(3)SysTick current value register (STK_VAL)
23位-0位(CURRENT): 跟上面的有點像,也是存儲一個值。但是,當你讀取它時,寄存器會返回這個值(就是讀取了這個值);當你寫它時會自動清零,同時CTRL寄存器的第16位(COUNTFLAG)置0。
其余位保留。
(4)SysTick calibration value register (STK_CALIB)
這個寄存器似乎比較少關注到,對於用戶來講應該不常用?
31位(NOREF): 1為沒有外部參考時鍾(外部時鍾源STCLK不可用),0為外部參考時鍾可用。
30位(SKEW): 1為校准值不是准確的10ms,0為校准值是准確的10ms。
23位-0位(TENMS): 存儲的是10ms的間隔倒計時的格數。芯片設計者應該提供這數值,若該值為0,則無法使用校准功能。
其余位保留。
在頭文件core_cm3.h中有結構體定義:
typedef struct
{
__IO uint32_t CTRL; /*!< Offset: 0x00 SysTick Control and Status Register */
__IO uint32_t LOAD; /*!< Offset: 0x04 SysTick Reload Value Register */
__IO uint32_t VAL; /*!< Offset: 0x08 SysTick Current Value Register */
__I uint32_t CALIB; /*!< Offset: 0x0C SysTick Calibration Register */
} SysTick_Type;
3. 詳解SysTick_Config()函數
在使用SysTick之前,用戶必須調用SysTick_Config()函數對其進行配置。這個函數是怎樣進行配置的呢?我們可以看看頭文件core_cm3.h 的1694行:
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 */
}
uint32_t SysTick_Config(uint32_t ticks)
函數形參ticks
為兩個中斷之間的脈沖數(number of ticks between two interrupts),即相隔ticks個時鍾周期會引起一次中斷。
if (ticks > SysTick_LOAD_RELOAD_Msk) return (1);
判斷ticks
是否符合規則,這個是什么規則呢?就是判斷ticks
是否大於SysTick_LOAD_RELOAD_Msk
,如果大於,則返回1(failed),表示不符合規則。
SysTick_LOAD_RELOAD_Msk
是一個宏,在文件388行可以找到:
#define SysTick_LOAD_RELOAD_Pos 0
#define SysTick_LOAD_RELOAD_Msk (0xFFFFFFul << SysTick_LOAD_RELOAD_Pos)
0xFFFFFF在十進制中是2^24(=16777216),其中,ul的意思是無符號long型整數(
unsigned long int)。然后我們就知道,ticks
最大值不能超過2^24(因為是24位系統定時器),否則就配置失敗。
SysTick->LOAD = (ticks & SysTick_LOAD_RELOAD_Msk) - 1; /* set reload register */
這里就是將ticks寫入load寄存器,ticks是計數最開始的值。減去一的原因是計數從0結束。
NVIC_SetPriority (SysTick_IRQn, (1<<__NVIC_PRIO_BITS) - 1);
NVIC中斷配置,SysTick_IRQn
在頭文件stm32f10x.h可以找到:
SysTick_IRQn = -1, /*!< 15 Cortex-M3 System Tick Interrupt*/
宏定義__NVIC_PRIO_BITS
可以在本文件core_cm3.h找到:
#define __NVIC_PRIO_BITS 4 /*!< STM32 uses 4 Bits for the Priority Levels*/
因此(1<<__NVIC_PRIO_BITS) - 1
的意思是1左移4位(即16),表達式的值為15(注意二進制表示為1111),這里面的4是因為(寄存器NVIC->IPRx)使用4個位來配置中斷優先級。
所以SysTick的優先級是最低的嗎?由於沒有指定優先級分組,所以這就要看怎么分了。
假如分組為1,則將1111分為兩組,前面組為搶占優先級,只有一個1(說明搶占優先級為1),而后面組為響應優先級,為111(說明響應優先級為7),需要注意到這里1和0的個數表示了二進制的數,進而指明了優先級別的高低。
假如分組為2,則將1111分為兩組,前面組為搶占優先級,有11(說明搶占優先級為3),而后面組為響應優先級,為11(說明響應優先級為3)。
假如分組為3,則將1111分為兩組,前面組為搶占優先級,有111(說明搶占優先級為7),而后面組為響應優先級,為1(說明響應優先級為1)。以此類推,所以SysTick的優先級是不一定的,只要你去配置,你想怎樣都行。
SysTick->VAL = 0;
將0載入到val寄存器中。
SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk | SysTick_CTRL_TICKINT_Msk | SysTick_CTRL_ENABLE_Msk;
配置時鍾源、中斷請求和使能。
4. SysTick相關的函數
SysTick_CLKSourceConfig() //Systick時鍾源選擇,在misc.c中
SysTick_Config(uint32_t ticks) //初始化systick,時鍾為HCLK,並開啟中斷,在core_m3.h中
void SysTick_Handler(void);
//Systick中斷服務函數,需要用戶自行編寫
二、延時函數
使用SysTick寫出來的延時函數,其精度要比軟件延時要高。不過這個函數要自己寫。
下面是一個寫好的微妙級別的延時函數,我們來分析一下:
void Systick_Delay_us(uint32_t us)
{
uint32_t i;
SysTick_Config(72);
for( i = 0; i < us; i++)
{
while( !((SysTick -> CTRL) & (1 << 16)) );
}
SysTick -> CTRL &= ~SysTick_CTRL_ENABLE_Msk;
}
SysTick_Config(72);
配置SysTick時鍾。這里為什么是72呢?有個公式:t = reload * ( 1/clk )
,clk為內核時鍾頻率,reload為LOAD寄存器的值。在這個程序中,Clk = 72MHz,reload = 72,因此 t = (72) *(1 / 72 MHz)= 1us。
while( !((SysTick -> CTRL) & (1 << 16)) );
這里有必要說明一下(SysTick -> CTRL) & (1 << 16)
是什么意思。SysTick -> CTRL
是CTRL寄存器的首地址,1<<16為左移16位,兩者進行與運算,正好得到CTRL寄存器第16位地址,即COUNTFLAG,因此這條語句是用來判斷計數是否到0。如果沒到0,while繼續循環。每跳出一次while循環,說明已經過去了1us的時間,而for循環則循環了指定次數的1us。
SysTick -> CTRL &= ~SysTick_CTRL_ENABLE_Msk;
每一次使用完SysTick后要記得關閉。
三、簡單例程
1. 不使用中斷服務函數
//在systick.c中
void Systick_Delay_us(uint32_t us)
{
uint32_t i;
SysTick_Config(72);
for( i = 0; i < us; i++)
{
while( !((SysTick -> CTRL) & (1 << 16)) );
}
SysTick -> CTRL &= ~SysTick_CTRL_ENABLE_Msk;
}
void Systick_Delay_ms(uint32_t ms)
{
uint32_t i;
SysTick_Config(72000);
for( i = 0; i < ms; i++)
{
while( !((SysTick -> CTRL) & (1 << 16)) );
}
SysTick -> CTRL &= ~SysTick_CTRL_ENABLE_Msk;
}
//在main.c中
//實現功能:每隔500ms,LED0和LED1交錯亮滅一次
while(1)
{
GPIO_ResetBits(GPIOB,GPIO_Pin_5);
GPIO_SetBits(GPIOE,GPIO_Pin_5);
Systick_Delay_ms(500);
GPIO_SetBits(GPIOB,GPIO_Pin_5);
GPIO_ResetBits(GPIOE,GPIO_Pin_5);
Systick_Delay_ms(500);
}
2. 使用中斷服務函數
//systick.h
#ifndef __SYSTICK_H
#define __SYSTICK_H
#include "stm32f10x.h"
#include "core_cm3.h"
void Systick_Delay_ms(uint32_t ms);
void Systick_Delay_us(uint32_t us);
void SysTick_Init(void);
void Delay_ms(uint32_t ms);
#endif
//systick.c
#include "systick.h"
/* use interrupt */
__IO uint32_t TimingDelay;
void SysTick_Init(void)
{
if(SysTick_Config(72000) == 1)
{
while(1);
}
SysTick->CTRL &= ~SysTick_CTRL_ENABLE_Msk;
}
void Delay_ms(uint32_t ms)
{
TimingDelay = ms;
SysTick->CTRL |= SysTick_CTRL_ENABLE_Msk;
while(TimingDelay != 0);
}
//stm32f10x_it.h
#include "stm32f10x_it.h"
extern uint32_t TimingDelay; //看見這玩意我就惡心
void SysTick_Handler(void)
{
if(TimingDelay != 0x00)
{
TimingDelay--;
}
}
//main.c
SysTick_Init();
while(1)
{
GPIO_ResetBits(GPIOB,GPIO_Pin_5);
GPIO_SetBits(GPIOE,GPIO_Pin_5);
Delay_ms(500);
GPIO_SetBits(GPIOB,GPIO_Pin_5);
GPIO_ResetBits(GPIOE,GPIO_Pin_5);
Delay_ms(500);
}
上面這種寫法用了extern全局變量,個人感覺很別扭,但是中斷服務函數是沒有形參的,所以還有一種折中的寫法(自己寫一個函數,然后中斷服務函數去調用):
//systick.h
#ifndef __SYSTICK_H
#define __SYSTICK_H
#include "stm32f10x.h"
#include "core_cm3.h"
void Systick_Delay_ms(uint32_t ms);
void Systick_Delay_us(uint32_t us);
void SysTick_Init(void);
void Delay_ms(uint32_t ms);
void timeDec(void); //自己寫的函數,待會服務函數去調用
static __IO uint32_t TimingDelay;
#endif
//systick.c
#include "systick.h"
/* use interrupt */
void SysTick_Init(void)
{
if(SysTick_Config(72000) == 1)
{
while(1);
}
SysTick->CTRL &= ~SysTick_CTRL_ENABLE_Msk;
}
void Delay_ms(uint32_t ms)
{
TimingDelay = ms;
SysTick->CTRL |= SysTick_CTRL_ENABLE_Msk;
while(TimingDelay != 0);
}
void timeDec(void)
{
if(TimingDelay != 0x00)
{
TimingDelay--;
}
}
//stm32f10x_it.h
#include "stm32f10x_it.h"
void SysTick_Handler(void)
{
timeDec(); //調用自己寫的函數
}
再次,我個人習慣是,extern只有到萬不得已才使用,因為這玩意太破壞文件之間的耦合度了,而且被多個文件使用,不太好追蹤源頭。