STM32 SysTick4个寄存器

本文介绍了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的普通计数模式对延迟进行管理

//********************************************************************************

//V1.2修改说明

//修正了中断中调用出现死循环的错误

//防止延时不准确,采用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

//********************************************************************************

//V1.4修改说明

//把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初始化

//********************************************************************************

//V1.3修改说明

//支持适应不同频率下的串口波特率设置.

//加入了对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"

//////////////////////////////////////////////////////////////////////////////

////

//////////////////////////////////////////////////////////////////////////////

////

//初始化PA8和PD2为输出口.并使能这两个口的时钟

//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了,就停止循环。因为只是做延时,也不需要跳到任何中断服务那边去处理什么,只要这个循环的耗时过程完成就可以了。