CH32V003的系统定时器SysTick

发布时间 2023-08-20 11:53:40作者: fxzq

在CH32V003内部有一个特殊的定时器——系统定时器(SysTick),它位于青稞V2微处理内核里面,是RISC-V内核的一个组成部分,主要用来给操作系统提供时间片轮转的定时,一般固定为10ms的定时,所以中文也称它为“嘀嗒”定时器(也称“心跳”定时器)。在不跑操作系统时,可以把它当作普通定时器来使用,一般用来进行程序延时。下面就来讨论一下如何使用SysTick系统定时器。
系统定时器位于“内核私有外设”(Core Private peripheral)内,其地址为0xE0000000~0xE0100000。SysTick定时器的位长度是32位,即最长的计数次数为4,294,967,296次。计数为正向计数形式,递增到与比较值一致时产生中断请求。计数的脉冲可直接取系统时钟,也可取8分频的系统时钟。下表给出了和SysTick相关的寄存器。

从上表中可以看到,SysTick定时器并不复杂,只用了4个寄存器。下面就分别来进行讨论。
下面给出的是上表中控制寄存器STK_CTLR的全部位结构。

(1)第31位(SWIE)为软件中断触发使能位。置1时触发软件中断被激活,置0时关闭触发。在进入软件中断后,需软件清0,否则会连续触发。该位复位值为0。
(2)第30~4位均为保留位。
(3)第3位(STRE)为自动重装载计数使能位。置1时正向计数到比较值后重新从0开始计数,置0时正向计数到比较值后继续正向计数。该位复位值为0。
(4)第2位(STCLK)为计数器时钟源选择位。置1时选取系统时钟作为计数时钟,置0时选取系统时钟的8分频作为计数时钟。该位复位值为0。
(5)第1位(STIE)为计数器中断使能控制位。置1时使能计数器中断,置0时关闭计数器中断。该位复位值为0。
(6)第0位(STE)为系统计数器使能控制位。置1时启动系统计数器STK,置0时关闭系统计数器STK,计数器停止计数。该位复位值为0。

STK_SR是系统定时器的计数状态寄存器,负责SysTick的状态标志。下表给出了它的全部位结构。

(1)第31~1位均为保留位。
(2)第0位(CNTIF)为计数值比较标志位,写0 清除,写1 无效。值为1时表示向上计数已达到比较值,值为0时表示未达到比较值。该位复位值为0。

STK_CNTR是系统定时器的计数器值寄存器,负责SysTick的计数。下表给出了它的全部位结构。

(1)第31~0位(CNTR)全部用于计数,其值为当前计数器的计数值。这些位的复位值为0。

STK_CMPR是系统定时器的计数比较值寄存器,负责SysTick的计数比较。下表给出了它的全部位结构。

(1)第31~0位(CMPR)为当前设置的计数器比较值。这些位的复位值为0。

以上介绍完了SysTick 所用到的寄存器,现在来讨论一下如何使用它。下面给出系统时钟SysTick的结构体定义(该定义位于开发环境MRS自带的core_riscv.h文件中)。

typedef struct
{
    __IO uint32_t CTLR;
    __IO uint32_t SR;
    __IO uint32_t CNT;
    uint32_t RESERVED0;
    __IO uint32_t CMP;
    uint32_t RESERVED1;
}SysTick_Type;

SysTick定时器的基址为0xE000F000,所以要将基址指针强制转换为上述结构体,还要加上下面的定义。

#define SysTick         ((SysTick_Type *) 0xE000F000)

对于系统定时器SysTick产生的的具体使用,可分为两种形式,即中断形式和软件查询形式。
先来看中断形式,它有特定的入口函数,形式如下所示。

void SysTick_Handler(void)
{
    //系统定时中断服务程序部分
}

在使用中断时要特别注意一点,根据官方手册描述,由于CH32V003具有硬件压栈模式,所以在使用中断入口函数前,必须先声明一下是启用硬件压栈模式还是软件压栈模式,具体如下。

void SysTick_Handler(void) __attribute__((interrupt("WCH-Interrupt-fast"))); //硬件压栈模式
void SysTick_Handler(void) __attribute__((interrupt())); //软件压栈模式

声明可放在main函数前面,但只能选择其中一种模式。至于要选择哪种模式,可参看“PFIC中断控制”部分的内容。

在使用中断前可通过下面的初始化函数配置一下SysTick。

void SysTick_init(void)
{
    NVIC_EnableIRQ(SysTicK_IRQn); //使能系统的SysTick快速中断
    SysTick->SR &= ~(1 << 0); //计数器状态清零
    SysTick->CMP = 3000000; //设置比较值
    SysTick->CNT = 0; //计数器清零
    SysTick->CTLR = 0xb; //设置为系统时钟8分频、自动重载、启用中断、开启定时器
}

在中断服务函数中,需要先执行“SysTick->SR &= ~(1 << 0);”语句,向系统计数状态寄存器SR的最低位写0,以清除中断标志,然后再执行其他任务。

再来看软件查询的形式。不使用中断的话,也可以通过软件查询STK_SR寄存器的第0位CNTIF是否被置1,如果置1了表示定时时间到了。下面就来看一个使用软件查询方式实现的毫秒级延时函数(设系统时钟为48MHz),代码如下。 

void delay_ms(uint32_t time)
{
    SysTick->SR &= ~(1 << 0);
    SysTick->CMP = time * 6000;
    SysTick->CNT = 0;
    SysTick->CTLR |=(1 << 0);
    while((SysTick->SR & (1 << 0)) != (1 << 0));
    SysTick->CTLR &= ~(1 << 0);
}

在程序中,通过“while((SysTick->SR & (1 << 0)) != (1 << 0));”这句来等待定时器比较成功,其实就是循环查询STK_SR寄存器的第0位CNTIF是否被置1,若为0则循环等待直到1为止。

由上述可见,其实SysTick本身是一个简易的定时器,操作较为简单方便。在实际应用时可按以下顺序进行操作。
(1)写STK_SR寄存器清零标志位。
(2)给STK_CMPR寄存器写入定时器的比较值。
(3)给STK_CNTR寄存器值清零。
(4)设置STK_CTLR寄存器配置时钟源分频,中断使能等,然后启动定时器。
接下来看一个LED灯闪烁的例子,要求通过CH32V003的PD6引脚实现一个LED的闪烁功能,周期为1秒。使用程序查询方式的代码如下(实际运行时还需要加上时钟配置)。

#include <ch32v00x.h>
void delay_ms(uint32_t time)
{
    SysTick->SR &= ~(1 << 0);   //清除标志位
    SysTick->CMP = time * 6000;   //48MHz的8分频是6MHz,故1ms需要扩大6000倍
    SysTick->CNT = 0;   //计数值清零
    SysTick->CTLR |=(1 << 0);   //仅启动计数器
    while((SysTick->SR & (1 << 0)) != (1 << 0));   //等待计数值到
    SysTick->CTLR &= ~(1 << 0);   //停止计数器
}
int main(void)
{
    RCC->APB2PCENR |= 1 << 5;   //使能PD端口的时钟
    GPIOD->CFGLR &= ~0x4000000; 
    GPIOD->CFGLR |= 0x1000000;   //通过以上两句配置PD6为通用推挽输出模式,速度为10MHz
    while(1)
    {
        GPIOD->OUTDR ^= (uint16_t)0x40;   //PD6取反(通过异或方式)
        delay_ms(500);   //延时0.5秒
    }
}

使用中断方式的代码如下:

#include <ch32v00x.h>
void SysTick_Handler(void) __attribute__(()));   //中断使用软件压栈的方式
void SysTick_init(void)
{
    NVIC_EnableIRQ(SysTicK_IRQn);   //使能系统的SysTick快速中断
    SysTick->SR &= ~(1 << 0);   //计数器状态清零
    SysTick->CMP = 3000000;   //48MHz的8分频是6MHz,定时0.5秒的值是一半
    SysTick->CNT = 0;   //计数器清零
    SysTick->CTLR = 0xb;   //设置为系统时钟8分频、自动重载、启用中断、开启定时器
}
int main(void)
{
    RCC->APB2PCENR |= 1 << 5;   //使能PD端口的时钟
    GPIOD->CFGLR &= ~0x4000000;
    GPIOD->CFGLR |= 0x1000000;   //通过以上两句配置PD6为通用推挽输出模式,速度为10MHz
    SysTick_init();   //调用定时器初始化函数
    while(1)
    {
        ;
    }
}
void SysTick_Handler(void)   //定时器中断服务函数
{
    SysTick->SR &= ~(1 << 0);   //清除标志位
    GPIOD->OUTDR ^= (uint16_t)0x40;   //PD6取反(通过异或方式)
}

把程序编译后下载到CH32V003中,给系统上电,可看到接到PD6引脚上的LED在闪烁,闪烁周期为1秒。