本文介绍了STM32中SysTick定时器的寄存器配置及使用方式。SysTick包含4个寄存器:CTRL(控制状态)、LOAD(重装载值)、VAL(当前值)和CALIB(校准值)。通过结构体指针和地址映射实现对寄存器的操作,其中CTRL寄存器通过位操作选择外部时钟源(HCLK/8)。文章还展示了延时函数的实现,包括毫秒级delay_ms()和微秒级delay_us(),通过配置LOAD寄存器设置定时值,并轮询CTRL状态位实现精确延时。此外,提供了系统时钟初始化、中断配置等底层驱动代码,并详细说明了寄存器映射原理及操作方式。这些内容为STM32定时器编程提供了实践参考。
SysTick->CTRL&=0xfffffffb;// <==== SysTick->CTRL 结构体指针指向其成员的符号.
王胖子杂货店baidu
SysTick有4个寄存器 :
寄存器 描述
CTRL SysTick 控制和状态寄存器
LOAD SysTick 重装载值寄存器
VAL SysTick 当前值寄存器
CALIB SysTick 校准值寄存器
对应地在固件函数库中定义了这个东西
typedef struct
{
vu32 CTRL;
vu32 LOAD;
vu32 VAL;
vuc32 CALIB;
} SysTick_TypeDef;
在这背后,已经对定义的寄存器进行了一个地址映射。当我们操控我们定义的
寄存器时实际上已是通过那种映射关系操控了芯片内部的值。其实在STM32中
对寄存器的操作都是通过这种方式进行的。
具体的映射过程如下,我们可以看一下:
#define SCS_BASE ((u32)0xE000E000)
#define SysTick_BASE (SCS_BASE + 0x0010)
#ifndef DEBUG
...
#ifdef _SysTick
#define SysTick ((SysTick_TypeDef *) SysTick_BASE)
#endif /*_SysTick */
...
#else /* DEBUG */
...
#ifdef _SysTick
EXT SysTick_TypeDef *SysTick;
#endif /*_SysTick */
...
#endif
#ifdef _SysTick
SysTick = (SysTick_TypeDef *) SysTick_BASE;
#endif /*_SysTick */
为了访问SysTick寄存器,, _SysTick必须在文件"stm32f10x_conf.h"中定义如下:
#define _SysTick
映射过程就不作讨论了。总这这样映射的结果是我们能直接使用SysTick。
那就来看一下有关寄存器的设置。
#include <stm32f10x_lib.h>
#include "sys.h"
#include "usart.h"
#include "delay.h"
#include "led.h"
//Mini STM32开发板范例代码1
//跑马灯实验
//正点原子@ALIENTEK
//技术论坛:www.openedv.com
int main(void)
{
Stm32_Clock_Init(9); //系统时钟设置
delay_init(72); //延时初始化
LED_Init(); //初始化与LED连接的硬件接口
while(1)
{
LED0=0;
LED1=1;
delay_ms(3000);
LED0=1;
LED1=0;
delay_ms(3000);
}
}
#include <stm32f10x_lib.h>
#include "delay.h"
//////////////////////////////////////////////////////////////////////////////
////
//Mini STM32开发板
//使用SysTick的普通计数模式对延迟进行管理
//********************************************************************************
//修正了中断中调用出现死循环的错误
//防止延时不准确,采用do while结构!
//////////////////////////////////////////////////////////////////////////////
////
static u8 fac_us=0;//us延时倍乘数
static u16 fac_ms=0;//ms延时倍乘数
//初始化延迟函数
//SYSTICK的时钟固定为HCLK时钟的1/8
//SYSCLK:系统时钟
void delay_init(u8 SYSCLK)
{
SysTick->CTRL&=0xfffffffb;//bit2清空,选择外部时钟 HCLK/8
fac_us=SYSCLK/8;
fac_ms=(u16)fac_us*1000;
}
//延时nms
//注意nms的范围
//SysTick->LOAD为24位寄存器,所以,最大延时为:
//nms<=0xffffff*8*1000/SYSCLK
//SYSCLK单位为Hz,nms单位为ms
//对72M条件下,nms<=1864
/***SysTick定时器 ARM Cortex-M3权威指南 P142
SysTick定时器被捆绑在NVIC中,用于产生SYSTICK异常(异常号:15)。
在以前,大多操作系统需要一个硬件定时器来产生操作系统需要的滴答中断,
作为整个系统的时基。因此,需要一个定时器来产生周期性的中断,而且最好
还让用户程序不能随意访问它的寄存器,以维持操作系统"心跳"的节律。
SysTick的最大使命,就是定期地产生异常请求,作为系统的时基。
OS都需要这种"滴答"来推动任务和时间的管理。*********/
void delay_ms(u16 nms)
{
u32 temp;
SysTick->LOAD=(u32)nms*fac_ms;//时间加载(SysTick->LOAD为24bit)
SysTick->VAL =0x00; //清空计数器
SysTick->CTRL=0x01 ; //开始倒数
do
{
temp=SysTick->CTRL;
}
while(temp&0x01&&!(temp&(1<<16)));//等待时间到达
SysTick->CTRL=0x00; //关闭计数器
SysTick->VAL =0X00; //清空计数器
}
//延时nus
//nus为要延时的us数.
void delay_us(u32 nus)
{
u32 temp;
SysTick->LOAD=nus*fac_us; //时间加载
SysTick->VAL=0x00; //清空计数器
SysTick->CTRL=0x01 ; //开始倒数
do
{
temp=SysTick->CTRL;
}
while(temp&0x01&&!(temp&(1<<16)));//等待时间到达
SysTick->CTRL=0x00; //关闭计数器
SysTick->VAL =0X00; //清空计数器
}
#include <stm32f10x_lib.h>
#include "sys.h"
//////////////////////////////////////////////////////////////////////////////
////
//本程序只供学习使用,未经作者许可,不得用于其它任何用途
//Mini STM32开发板
//系统时钟初始化
//正点原子@ALIENTEK
//********************************************************************************
//把NVIC KO了,没有使用任何库文件!
//加入了JTAG_Set函数
//////////////////////////////////////////////////////////////////////////////
////
//设置向量表偏移地址
//NVIC_VectTab:基址
//Offset:偏移量
//CHECK OK
//091207
void MY_NVIC_SetVectorTable(u32 NVIC_VectTab, u32 Offset)
{
//检查参数合法性
assert_param(IS_NVIC_VECTTAB(NVIC_VectTab));
assert_param(IS_NVIC_OFFSET(Offset));
SCB->VTOR = NVIC_VectTab|(Offset & (u32)0x1FFFFF80);//设置NVIC的向量表偏
移寄存器
//用于标识向量表是在CODE区还是在RAM区
}
//设置NVIC分组
//NVIC_Group:NVIC分组 0~4 总共5组
//CHECK OK
//091209
void MY_NVIC_PriorityGroupConfig(u8 NVIC_Group)
{
u32 temp,temp1;
temp1=(~NVIC_Group)&0x07;//取后三位
temp1<<=8;
temp=SCB->AIRCR; //读取先前的设置
temp&=0X0000F8FF; //清空先前分组
temp|=0X05FA0000; //写入钥匙
temp|=temp1;
SCB->AIRCR=temp; //设置分组
}
//设置NVIC
//NVIC_PreemptionPriority:抢占优先级
//NVIC_SubPriority :响应优先级
//NVIC_Channel :中断编号
//NVIC_Group :中断分组 0~4
//注意优先级不能超过设定的组的范围!否则会有意想不到的错误
//组划分:
//组0:0位抢占优先级,4位响应优先级
//组1:1位抢占优先级,3位响应优先级
//组2:2位抢占优先级,2位响应优先级
//组3:3位抢占优先级,1位响应优先级
//组4:4位抢占优先级,0位响应优先级
//NVIC_SubPriority和NVIC_PreemptionPriority的原则是,数值越小,越优先
//CHECK OK
//100329
void MY_NVIC_Init(u8 NVIC_PreemptionPriority,u8 NVIC_SubPriority,u8 NVIC_Channel,u8
NVIC_Group)
{
u32 temp;
u8 IPRADDR=NVIC_Channel/4; //每组只能存4个,得到组地址
u8 IPROFFSET=NVIC_Channel%4;//在组内的偏移
IPROFFSET=IPROFFSET*8+4; //得到偏移的确切位置
MY_NVIC_PriorityGroupConfig(NVIC_Group);//设置分组
temp=NVIC_PreemptionPriority<<(4-NVIC_Group);
temp|=NVIC_SubPriority&(0x0f>>NVIC_Group);
temp&=0xf;//取低四位
if(NVIC_Channel<32)NVIC->ISER0|=1<<NVIC_Channel;//使能中断位(要清除的话,
相反操作就OK)
else NVIC->ISER1|=1<<(NVIC_Channel-32);
NVIC->IPRIPRADDR|=temp<<IPROFFSET;//设置响应优先级和抢断优先级
}
//外部中断配置函数
//只针对GPIOA~G;不包括PVD,RTC和USB唤醒这三个
//参数:GPIOx:0~6,代表GPIOA~G;BITx:需要使能的位;TRIM:触发模式,1,下升沿;2,上降沿;3,
任意电平触发
//该函数一次只能配置1个IO口,多个IO口,需多次调用
//该函数会自动开启对应中断,以及屏蔽线
//待测试...
void Ex_NVIC_Config(u8 GPIOx,u8 BITx,u8 TRIM)
{
u8 EXTADDR;
u8 EXTOFFSET;
EXTADDR=BITx/4;//得到中断寄存器组的编号
EXTOFFSET=(BITx%4)*4;
RCC->APB2ENR|=0x01;//使能io复用时钟
AFIO->EXTICREXTADDR&=~(0x000F<<EXTOFFSET);//清除原来设置!!!
AFIO->EXTICREXTADDR|=GPIOx<<EXTOFFSET;//EXTI.BITx映射到
GPIOx.BITx
//自动设置
EXTI->IMR|=1<<BITx;// 开启line BITx上的中断
//EXTI->EMR|=1<<BITx;//不屏蔽line BITx上的事件 (如果不屏蔽这句,在硬件上
是可以的,但是在软件仿真的时候无法进入中断!)
if(TRIM&0x01)EXTI->FTSR|=1<<BITx;//line BITx上事件下降沿触发
if(TRIM&0x02)EXTI->RTSR|=1<<BITx;//line BITx上事件上升降沿触发
}
//不能在这里执行所有外设复位!否则至少引起串口不工作.
//把所有时钟寄存器复位
//CHECK OK
//091209
void MYRCC_DeInit(void)
{
RCC->APB1RSTR = 0x00000000;//复位结束
RCC->APB2RSTR = 0x00000000;
RCC->AHBENR = 0x00000014; //睡眠模式闪存和SRAM时钟使能.其他关闭.
RCC->APB2ENR = 0x00000000; //外设时钟关闭.
RCC->APB1ENR = 0x00000000;
RCC->CR |= 0x00000001; //使能内部高速时钟HSION
RCC->CFGR &= 0xF8FF0000; //复位SW1:0,HPRE3:0,PPRE12:0,PPRE2
2:0\],ADCPRE\[1:0\],MCO\[2:0
RCC->CR &= 0xFEF6FFFF; //复位HSEON,CSSON,PLLON
RCC->CR &= 0xFFFBFFFF; //复位HSEBYP
RCC->CFGR &= 0xFF80FFFF; //复位PLLSRC, PLLXTPRE, PLLMUL3:0 and
USBPRE
RCC->CIR = 0x00000000; //关闭所有中断
//配置向量表
#ifdef VECT_TAB_RAM
MY_NVIC_SetVectorTable(NVIC_VectTab_RAM, 0x0);
#else
MY_NVIC_SetVectorTable(NVIC_VectTab_FLASH, 0x0);
#endif
}
//THUMB指令不支持汇编内联
//采用如下方法实现执行汇编指令WFI
//CHECK OK
//091209
__asm void WFI_SET(void)
{
WFI;
}
//进入待机模式
//check ok
//091202
void Sys_Standby(void)
{
SCB->SCR|=1<<2;//使能SLEEPDEEP位 (SYS->CTRL)
RCC->APB1ENR|=1<<28; //使能电源时钟
PWR->CSR|=1<<8; //设置WKUP用于唤醒
PWR->CR|=1<<2; //清除Wake-up 标志
PWR->CR|=1<<1; //PDDS置位
WFI_SET(); //执行WFI指令
}
//后备寄存器写入操作
//reg:寄存器编号
//reg:要写入的数值
////check ok
////091202
//void BKP_Write(u8 reg,u16 dat)
//{
// RCC->APB1ENR|=1<<28; //使能电源时钟
// RCC->APB1ENR|=1<<27; //使能备份时钟
// PWR->CR|=1<<8; //取消备份区写保护
// switch(reg)
// {
// case 1:
// BKP->DR1=dat;
// break;
// case 2:
// BKP->DR2=dat;
// break;
// case 3:
// BKP->DR3=dat;
// break;
// case 4:
// BKP->DR4=dat;
// break;
// case 5:
// BKP->DR5=dat;
// break;
// case 6:
// BKP->DR6=dat;
// break;
// case 7:
// BKP->DR7=dat;
// break;
// case 8:
// BKP->DR8=dat;
// break;
// case 9:
// BKP->DR9=dat;
// break;
// case 10:
// BKP->DR10=dat;
// break;
// }
//}
//系统软复位
//CHECK OK
//091209
void Sys_Soft_Reset(void)
{
SCB->AIRCR =0X05FA0000|(u32)0x04;
}
//JTAG模式设置,用于设置JTAG的模式
//mode:jtag,swd模式设置;00,全使能;01,使能SWD;10,全关闭;
//CHECK OK
//100818
void JTAG_Set(u8 mode)
{
u32 temp;
temp=mode;
temp<<=25;
RCC->APB2ENR|=1<<0; //开启辅助时钟
AFIO->MAPR&=0XF8FFFFFF; //清除MAPR的26:24
AFIO->MAPR|=temp; //设置jtag模式
}
//系统时钟初始化函数
//pll:选择的倍频数,从2开始,最大值为16
//CHECK OK
//091209
void Stm32_Clock_Init(u8 PLL)
{
unsigned char temp=0;
MYRCC_DeInit(); //复位并配置向量表
RCC->CR|=0x00010000; //外部高速时钟使能HSEON
while(!(RCC->CR>>17));//等待外部时钟就绪
RCC->CFGR=0X00000400; //APB1=DIV2;APB2=DIV1;AHB=DIV1;
PLL-=2;//抵消2个单位
RCC->CFGR|=PLL<<18; //设置PLL值 2~16
RCC->CFGR|=1<<16; //PLLSRC ON
FLASH->ACR|=0x32; //FLASH 2个延时周期
RCC->CR|=0x01000000; //PLLON
while(!(RCC->CR>>25));//等待PLL锁定
RCC->CFGR|=0x00000002;//PLL作为系统时钟
while(temp!=0x02) //等待PLL作为系统时钟设置成功
{
temp=RCC->CFGR>>2;
temp&=0x03;
}
}
#include "sys.h"
#include "usart.h"
//////////////////////////////////////////////////////////////////////////////
////
//本程序只供学习使用,未经作者许可,不得用于其它任何用途
//Mini STM32开发板
//串口1初始化
//********************************************************************************
//支持适应不同频率下的串口波特率设置.
//加入了对printf的支持
//增加了串口接收命令功能.
//修正了printf第一个字符丢失的bug
//////////////////////////////////////////////////////////////////////////////
////
//////////////////////////////////////////////////////////////////
//加入以下代码,支持printf函数,而不需要选择use MicroLIB
#if 1
#pragma import(__use_no_semihosting)
//标准库需要的支持函数
struct __FILE
{
int handle;
/* Whatever you require here. If the only file you are using is */
/* standard output using printf() for debugging, no file handling */
/* is required. */
};
/* FILE is typedef' d in stdio.h. */
FILE __stdout;
//定义_sys_exit()以避免使用半主机模式
_sys_exit(int x)
{
x = x;
}
//重定义fputc函数
int fputc(int ch, FILE *f)
{
while((USART1->SR&0X40)==0);//循环发送,直到发送完毕
USART1->DR = (u8) ch;
return ch;
}
#endif
//end
//////////////////////////////////////////////////////////////////
#ifdef EN_USART1_RX //如果使能了接收
//串口1中断服务程序
//注意,读取USARTx->SR能避免莫名其妙的错误
u8 USART_RX_BUF64; //接收缓冲,最大64个字节.
//接收状态
//bit7,接收完成标志
//bit6,接收到0x0d
//bit5~0,接收到的有效字节数目
u8 USART_RX_STA=0; //接收状态标记
void USART1_IRQHandler(void)
{
u8 res;
if(USART1->SR&(1<<5))//接收到数据
{
res=USART1->DR;
if((USART_RX_STA&0x80)==0)//接收未完成
{
if(USART_RX_STA&0x40)//接收到了0x0d
{
if(res!=0x0a)USART_RX_STA=0;//接收错误,重新开始
else USART_RX_STA|=0x80; //接收完成了
}else //还没收到0X0D
{
if(res==0x0d)USART_RX_STA|=0x40;
else
{
USART_RX_BUFUSART_RX_STA\&0X3F=res;
USART_RX_STA++;
if(USART_RX_STA>63)USART_RX_STA=0;//
接收数据错误,重新开始接收
}
}
}
}
}
#endif
//初始化IO 串口1
//pclk2:PCLK2时钟频率(Mhz)
//bound:波特率
//CHECK OK
//091209
void uart_init(u32 pclk2,u32 bound)
{
float temp;
u16 mantissa;
u16 fraction;
temp=(float)(pclk2*1000000)/(bound*16);//得到USARTDIV
mantissa=temp; //得到整数部分
fraction=(temp-mantissa)*16; //得到小数部分
mantissa<<=4;
mantissa+=fraction;
RCC->APB2ENR|=1<<2; //使能PORTA口时钟
RCC->APB2ENR|=1<<14; //使能串口时钟
GPIOA->CRH&=0XFFFFF00F;
GPIOA->CRH|=0X000008B0;//IO状态设置
RCC->APB2RSTR|=1<<14; //复位串口1
RCC->APB2RSTR&=~(1<<14);//停止复位
//波特率设置
USART1->BRR=mantissa; // 波特率设置
USART1->CR1|=0X200C; //1位停止,无校验位.
#ifdef EN_USART1_RX //如果使能了接收
//使能接收中断
USART1->CR1|=1<<8; //PE中断使能
USART1->CR1|=1<<5; //接收缓冲区非空中断使能
MY_NVIC_Init(3,3,USART1_IRQChannel,2);//组2,最低优先级
#endif
}
#include <stm32f10x_lib.h>
#include "led.h"
//////////////////////////////////////////////////////////////////////////////
////
//////////////////////////////////////////////////////////////////////////////
////
//LED IO初始化
void LED_Init(void)
{
RCC->APB2ENR|=1<<2; //使能PORTA时钟
RCC->APB2ENR|=1<<5; //使能PORTD时钟
GPIOA->CRH&=0XFFFFFFF0;
GPIOA->CRH|=0X00000003;//PA8 推挽输出
GPIOA->ODR|=1<<8; //PA8 输出高
GPIOD->CRL&=0XFFFFF0FF;
GPIOD->CRL|=0X00000300;//PD.2推挽输出
GPIOD->ODR|=1<<2; //PD.2输出高
}
《STM32不完全手册》的51到56页就是要这次笔记学习的内容,请自己参考,我不再粘帖,主要讨论的内容其实就几行:
void delay_init(u8 SYSCLK)
{
SysTick->CTRL&=0xfffffffb;
fac_us=SYSCLK/8;
fac_ms=(u16)fac_us*1000;
}
Systick主要的作用就是拿来计时,其原理和应用简述一下就是这样的:通过配置寄存器 SysTick->CTRL来设定Systick的计时频率并Enable使Systick开始计数,这里的SysTick->CTRL&=0xfffffffb应该很好理解,把第2位设定为0,查找应用手册可以知道这是把Systick的计时频率设定为CPU主频(SYSCLK)的1/8。
假定我们板子默认的晶振频率是8Mhz,默认CPU工作频率(SYSCLK)是9倍频,即72M,那Systick的频率就是72/8=9Mhz。
知道了Systick的频率,下一步就是确定倒时计数器的数值,即SysTick-> LOAD这个寄存器的配置。上面已经知道了,Systick的工作频率F=9Mhz=SYSCLK/8,即每秒钟计数器自减900万次,也就是说,SYSCLK/8次的自减耗时1秒,那么(8/SYSCLK)/1000,000次自减就耗时1微秒了,这也就是fac_us的值了。那么上面函数中的fac_us为什么是SYSCLK/8呢?这里先搞清楚一点,函数中SYSCLK的单位是Mhz,所以SYSCLK的值是72(这个以Mhz为单位应该是STM32基础库里面做过宏定义的),否则也不可能用一个8位整形去表示一个7200万的数值;而我们这里计算的SYSCLK是以Hz为单位的,即72Mhz/1000,000=72,所以这个SYSCLK/8是对的。
你可能还没搞清楚fac_us到底是干嘛的。很简单,fac_us就是要写入SysTick-> LOAD寄存器的值,Systick的工作原理是这个寄存器的值在Systick被Enable之后就开始以设定的工作频率自减,减到0的时候就发出中断,实现定时。所以,写入fac_us到SysTick-> LOAD寄存器,就是要Systick在自减了fac_us次以后发出中断,自减fac_us所耗的时间已经说了,1微秒。
下面的fac_ms应该很好理解了,就是1毫秒的计数次数,刚好是1微妙的1000倍,注意9×1000超出了8位整形的表示范围,所以要用(u16)先把fac_us转成16位变量,以保证计算的正确。
具体的应用函数是用来做延时,如下:
void delay_us(u32 nus)
{
u32 temp;
SysTick->LOAD=nus*fac_us; //时间加载
SysTick->VAL=0x00; //清空计数器
SysTick->CTRL=0x01 ; //Enable Systick,开始倒数
do
{
temp=SysTick->CTRL;
}
while(temp&0x01&&!(temp&(1<<16)));//等待时间到达
SysTick->CTRL=0x00; //Disable Systick
SysTick->VAL =0X00; //清空计数器
}
具体的寄存器配置只要看手册就知道了,这里只需要理解一句:
do
{
temp=SysTick->CTRL;
}
while(temp&0x01&&!(temp&(1<<16)));//等待时间到达
核心就是while(temp&0x01&&!(temp&(1<<16)));//等待时间到达
temp已经是Systick控制寄存器的值了,temp&0x01就是把该寄存器的值读出并且把除第一位之外位都清零(当然要通过temp变量来传递值而不是直接修改寄存器),第一位就是Systick的Enable配置信息,写入1就是Enable,写入0就是Disable,读这一位是判断Systick是否仍然处于Enable状态(可能被其他中断禁用掉),temp&(1<<16)就是读取第16位的值,这一位如果为0就表示计数器的值不是0(即还在计数),如果是1就表示计数器已经自减到0了。
那么这段就很好理解了,即判断,如果Systick还在Enable的状态,并且计数器还没数到0,就不停的循环把当前的SysTick->CTRL寄存器值写入变量temp,继续下一次判断。当Systick被Disable或者计数器数到0了,就停止循环。因为只是做延时,也不需要跳到任何中断服务那边去处理什么,只要这个循环的耗时过程完成就可以了。