在LPC824内部有一个特殊的定时器——系统定时器(SysTick),它位于Cortex-M0+内核里面,是ARM内核的一部分,主要用来给操作系统提供时间片轮转的定时,一般固定为10ms的定时,所以中文也称它为“嘀嗒”定时器(也称“心跳”定时器)。在不跑操作系统时,可以把它当作普通定时器来用,一般用来进行程序延时。在前面的第一个演示示例中就用到过,下面就来讨论一下如何运用SysTick来提供延时。
系统定时器也位于“私有外设总线”(Private peripheral bus)内,其地址为0xE000E010~0xE000E0FF。下面先来看一下SysTick的内部结构,如下图所示。
从上图中可以看出,SysTick定时器的位长度是24位,即最长的计数次数为16777216次,且计数为倒数计数形式,递减到0时产生中断请求。计数的脉冲可直接取系统时钟,也可取半系统时钟。下表给出了和SysTick相关的寄存器。
上表中,SYST_CSR是系统定时器的控制寄存器,负责SysTick的启动、中断使能、输入时钟选择、溢出标志读取等操作;SYST_RVR是系统定时器的初始值重载寄存器,负责SysTick的24位初值载入;SYST_CVR是系统定时器的当前值寄存器,负责获取SysTick的24位当前计数值,当对该寄存器进行写操作时,该寄存器的数值将会被清零;SYST_CALIB是系统定时器的校准值寄存器,负责SysTick的校准。
下面给出的是上表中控制寄存器SYST_CSR的全部位结构,其字节地址为0xE000E010。(1)第0位(ENABLE)是使能SysTick位,置1启动计数,置0关闭计数。
(2)第1位(TICKINT)是SysTick的中断使能位,置1使能中断,置0禁止中断。
(3)第2位(CLKSOURCE)是输入时钟的选择位,置1时选择系统时钟做为计数脉冲,置0时选择半系统时钟做为计数脉冲的参考时钟。
(4)第3到15位为保留位,不能对它们写1。
(5)第16位(COUNTFLAG)是溢出标志位,当计数的值递减到0时,该位被置1,在读取该值后自动清零。
(6)第17到31位为保留位,不能对它们写1。SYST_RVR是系统定时器的初始值重载寄存器,负责SysTick的24位初值载入。下表给出了它的全部位结构,其字节地址为0xE000E014。
(1)第0到23位(RELOAD)为重载的初值,即当计数器计数到0后重载到计数器中的值,一共24位。
(2)第24到31位为保留位,不能对它们写1。
SYST_CVR是系统定时器的当前值寄存器,负责获取SysTick的24位当前计数值,当对该寄存器进行写操作时,该寄存器的数值将会被清零。下表给出了它的全部位结构,其字节地址为0xE000E018。
(1)第0到23位(CURRENT)为SysTick的当前值,一共24位。
(2)第24到31位为保留位,不能对它们写1。
SYST_CALIB是系统定时器的校准值寄存器,负责SysTick的校准。下表给出了它的全部位结构,其字节地址为0xE000E01C。
(1)第31位(NOREF)指示处理器是否有基准时钟,由芯片厂家出厂时决定。该位读出值为1,指明无独立的基准时钟。
(2)第30位(SKEW)指示TENMS值是否精确。由于TENMS不可知,因此10ms不精确计时的校准值不能确定,这会影响SysTick作为软件实时时钟的适用性。该位读出值为1,指明未提供TENMS值。
(3)第24到29位为保留位。
(4)第0到23位(TENMS)在发生系统时钟扭曲错误时,在10ms时序下的校正值。该位读出值为0,指明校准值未知。
下面给出系统时钟SysTick的结构体定义。
typedef struct
{
__IOM uint32_t CTRL;
__IOM uint32_t LOAD;
__IOM uint32_t VAL;
__IM uint32_t CALIB;
} SysTick_Type;
注意,在上述定义中,结构体成员CTRL对应SYST_CSR寄存器,LOAD对应SYST_RVR寄存器,VAL对应SYST_CVR寄存器,CALIB对应SYST_CALIB寄存器。结构体中并没有全部采用SysTick中的寄存器名称。
SysTick定时器组的基址为0xE000E000,所以要将基址指针强制转换为上述结构体,还要加上下面的定义。 //************************端口初始化*********************************** 在上述程序中,由于8个LED并没有接到LPC824相邻的引脚,因此使用了针对引脚的位操作方式来实现流水灯效果。把程序编译后下载到LPC824中,给系统上电,可看到接到端口上的8个LED在闪烁流动。
#define SCS_BASE (0xE000E000UL)
#define SysTick_BASE (SCS_BASE + 0x0010UL)
#define SysTick ((SysTick_Type *) SysTick_BASE )
对于系统定时器SysTick产生的中断,也有特定的入口函数,形式如下所示。
void SysTick_Handler(void)
{
系统定时中断服务程序部分
}由上述可见,其实系统定时器本身就是一个普通的定时器,在实际应用时可按以下顺序进行操作。
(1)给SYST_RVR寄存器写入定时器的初始值。
(2)写SYST_CVR寄存器对计数值进行清零。
(3)设置SYST_CSR寄存器选择相应的时钟源,启动定时并使能中断。
SysTick定时的时长由系统时钟频率、系统时钟选择和载入的初始值共同决定。假设系统时钟为24MHz,默认情况下SysTick的CLKSOURCE值为0,即选择半系统时钟频率。这样,输入给定时器计数的时钟就是24÷2=12MHz,计数周期为1/(12MHz)=1/12us,则计12次就是1us的定时,但实际上SysTick采用的是倒数计数方式,即从最大值依次递减计数直到0产生溢出信号。所以12次计数实际上是0~11次,即最大值要减1,即12-1=11。同时要注意,由于计数位宽是24位,所以最大计数值不能超过2的24次方(即16777216),由此,可得出微秒级的初始值计算公式,如下:
LOAD=((12*us)-1),其中us取值范围1~1398000
同理可得出毫秒级的初始值计算公式:
LOAD=((12000*ms)-1),其中ms取值范围1~1398
上升到一般情况,定时初始值可用下面的公式来计算:上式中,系统时钟SysClk的单位是MHz,CLKSOURCE的值是0或1,得到的是微秒级别的定时,要注意LOAD的值不能大于16777216。下面来看一个使用SysClk实现的毫秒级延时函数,代码如下:
void delay_ms(uint32_t ms)
{
SysTick->LOAD = (((12000)*ms)-1); //载入初始值
SysTick->VAL = 0; //写当前值寄存器使其清零
SysTick->CTRL |= (1<<0); //启动定时器,选择半系统时钟
while(!(SysTick->CTRL & 0x10000)); //循环查询,等待定时时间到
SysTick->CTRL &= ~(1<<0); //关闭定时器
}
在程序中,通过“while(!(SysTick ->CTRL & 0x10000));”这句来等待定时器溢出,其实就是循环查询控制寄存器SYST_CSR中的COUNTFLAG位(第16位)是否被置1,若为0则循环等待直到1为止。注意,本函数延时的最大时长为1398ms,即传递给它的参数不能超过1398,否则不能实现延时功能。下面再来看一个流水灯的例子,要求实现一个8位的流水灯,时间间隔为100ms。电路延用第一个演示示例中的电路原理图,参考代码如下:#include
void Port_init(void)
{
LPC_GPIO_PORT->DIR0 = 0x1FFFFFFF; //设置端口为输出方向
LPC_GPIO_PORT->PIN0 = 0x1FFFFFFF; //关闭所有LED
LPC_GPIO_PORT->CLR0 = (1<<7); //点亮第1个LED
}
//************************定时器初始化*********************************
void SysTick_init(void)
{
SysTick->LOAD = (((12000)*100)-1);
SysTick->VAL = 0;
SysTick->CTRL |= ((1<<1)|(1<<0));
}
//***************************主函数************************************
int main(void)
{
Port_init(); //调用端口初始化
SysTick_init(); //调用定时器初始化
while(1)
{
;
}
}
//************************定时器中断***********************************
void SysTick_Handler(void)
{
static uint8_t i=0;
switch(i++)
{
case 0:
LPC_GPIO_PORT->SET0 = (1<<7); //熄灭第1个LED
LPC_GPIO_PORT->CLR0 = (1<<13); //点亮第2个LED
break;
case 1:
LPC_GPIO_PORT->SET0 = (1<<13); //熄灭第2个LED
LPC_GPIO_PORT->CLR0 = (1<<16); //点亮第3个LED
break;
case 2:
LPC_GPIO_PORT->SET0 = (1<<16); //熄灭第3个LED
LPC_GPIO_PORT->CLR0 = (1<<17); //点亮第4个LED
break;
case 3:
LPC_GPIO_PORT->SET0 = (1<<17); //熄灭第4个LED
LPC_GPIO_PORT->CLR0 = (1<<19); //点亮第5个LED
break;
case 4:
LPC_GPIO_PORT->SET0 = (1<<19); //熄灭第5个LED
LPC_GPIO_PORT->CLR0 = (1<<27); //点亮第6个LED
break;
case 5:
LPC_GPIO_PORT->SET0 = (1<<27); //熄灭第6个LED
LPC_GPIO_PORT->CLR0 = (1<<28); //点亮第7个LED
break;
case 6:
LPC_GPIO_PORT->SET0 = (1<<28); //熄灭第7个LED
LPC_GPIO_PORT->CLR0 = (1<<18); //点亮第8个LED
break;
case 7:
LPC_GPIO_PORT->SET0 = (1<<18); //熄灭第8个LED
LPC_GPIO_PORT->CLR0 = (1<<7); //点亮第1个LED
i = 0; //计数清零
break;
}
}