【沁恒微CH32V307评估板试用体验】定时器使用基础:使用系统时基定时器和基本定时器闪烁LED - RISC-V MCU技术社区 - 电子技术william hill官网 - 广受欢迎的专业电子william hill官网 - 威廉希尔官方网站
分享 收藏 返回

[文章]

【沁恒微CH32V307评估板试用体验】定时器使用基础:使用系统时基定时器和基本定时器闪烁LED

CH32V307系统提供了多个类型的定时器,具体可查看手册了解:
1.定时器分类.png

这次的分享,我们使用到的定时器为系统时基定时器和基本定时器TIM6。
闪烁使用的LED,为LED1,使用连接线将LED1和PA0连接即可;实际运行时,间隔1秒闪烁一次。
7.接线.jpg

有的同学可能会说,使用Delay_MsDelay_Us做演示,也能实现LED闪烁呀!
但在Delay的时候,你的程序,需要在这个地方,等待Delay时间后,才会继续运行。
而使用中断,我们的程序,能够继续运行做其他的事情,等到定时中断到来的时候,才处理LED的闪烁。这样程序的处理效率将会更高。

一、系统时基定时器SysTick

现在,我们了解一下系统时基定时器SysTick。

系统时基定时器
这是内核控制器自带的一个 64 位可选递增或递减的计数器,用于产生 SYSTICK 异常(异常号:15),
可专用于实时操作系统,为系统提供“心跳”节律,也可当成一个标准的 64 位计数器。具有自动重加 载功能及可编程的时钟源。

通过查看提供的实例和资料,了解到如下的关键信息:

  1. 系统的运行频率可以工作在72MHz,也可以工作在144MHz,通过system_ch32v30x.h/system_ch32v30x.c来修改:
// #define SYSCLK_FREQ_72MHz  72000000
#define SYSCLK_FREQ_144MHz  144000000

在我的实例中,设置工作在144MHz。

  1. 在设置SysTick的时候,通过SysTick->CMP来设置定时中断周期:

SysTick->CMP = SystemCoreClock / 1000 * 1000; //后面的1000代表1000HZ(那就是1ms进一次中断),*1000 表示 1000ms进入一次

  1. 有人会遇到,中断只进入一次,经过请教沁恒的陶工,了解到如下处理方法:
// 中断只进入一次的问题解决方法:
// 1. 如果使用沁恒提供的工具链,则使用如下的声明:
// void SysTick_Handler(void) __attribute__((interrupt("WCH-Interrupt-fast")));
// 2. 如果使用通用的risc-v工具链,如我在macOS下使用赛昉提供的工具链,则使用如下的声明:
void SysTick_Handler(void) __attribute__((interrupt()));

最终,具体的代码如下:

/*
  使用VTF IRQ中断控制LED闪烁
*/

#include "debug.h"
#include "board.h"

// 中断只进入一次的问题解决方法:
// 1. 如果使用沁恒提供的工具链,则使用如下的声明:
// void SysTick_Handler(void) __attribute__((interrupt("WCH-Interrupt-fast")));
// 2. 如果使用通用的risc-v工具链,如我在macOS下使用赛昉提供的工具链,则使用如下的声明:
void SysTick_Handler(void) __attribute__((interrupt()));

// LED状态
volatile uint16_t LED_Status = 0; // 中断里使用的变量加 volatile 可当成全局变量

// 初始化 GPIO
void GPIO_INIT(void)
{
    GPIO_InitTypeDef GPIO_InitStructure = {0};

    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &GPIO_InitStructure);
}

// 初始化 SysTick 定时器
void SysTick_init(void)
{
    /*配置中断优先级*/
    NVIC_InitTypeDef NVIC_InitStructure = {0};
    NVIC_InitStructure.NVIC_IRQChannel = SysTicK_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;//抢占式优先级
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;//响应式优先级
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;//使能
    NVIC_Init(&NVIC_InitStructure);

    /*配置定时器*/
    SysTick->CTLR= 0;
    SysTick->SR  = 0;
    SysTick->CNT = 0;
    SysTick->CMP = SystemCoreClock / 1000 * 1000; //后面的1000代表1000HZ(那就是1ms进一次中断),*1000 表示 1000ms进入一次
    SysTick->CTLR= 0xf;
}

int main(void)
{
    /* Initialize board components. */
    BOARD_SystemClock_Config();
    BOARD_IOMUX_Init();
    BOARD_Peripheral_Init();

    GPIO_INIT();                                    // 初始化 GPIO
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //设置中断控制器的优先级分组为 占优先级 2位 ,优先级2位。

    USART_Printf_Init(115200);
    printf("SystemClk:%drn", SystemCoreClock);
    printf("Interrupt SysTick Testrn");

    SysTick_init();
    while (1)
    {
    }
}

// 中断服务处理
void SysTick_Handler(void)
{
    SysTick->SR = 0;
    LED_Status = !LED_Status;                     // 将 LED 状态值取反
    GPIO_WriteBit(GPIOA, GPIO_Pin_0, LED_Status); // 配置 PA0 (即 LED1) 状态

    printf("Toggle LED by SysTick: %drn", LED_Status);
}

在上述代码中,关键部分如下:

  • GPIO_INIT:初始化GPIO,使用PA0;记得预先连接LED1和PA0
  • SysTick_init:SysTick初始化,主要设置中断周期
  • SysTick_Handler:中断服务处理,其内部的操作需要狠准快

除了板载的LED1(前提是连好了线)会间隔1秒闪烁一次,通过串口工具,我们也可以看到输出的调试信息:
systick.png

二、基本定时器TIM6

然后,我们再来了解一下基本定时器。

基本定时器
基本定时器是一个 16 位自动装载计数器,支持 16 位可编程预分频器。可以位数模转换(DAC)提
供时钟,触发 DAC 的同步威廉希尔官方网站 。基本定时器之间是互相独立的,互不共享任何资源。

CH32V307提供了两个基本定时器TIM6和TIM7,用法是一样的,下面的实例中,使用TIM6。

通过官方提供的实例和资料了解到,基本定时器使用的过程中,有两个设置是最重要的:

  • TIM_Prescaler:定时器预分频器设置,时钟源经该预分频器才是定时器时钟。
  • TIM_Period:定时器周期,实际就是设定自动重载寄存器的值。

这两个参数结合就能实现实际所需要的定时。
根据定时器时钟的频率,比如时钟的频率是144MHz,可以理解为1秒钟MCU会自己数144M次,预分频系数就是将频率分割,比如分频系数是144,则该时钟的频率会变成144MHZ/144=1MHz,但是在设置的时候要注意,数值应该是144-1。

为了让 LED1间隔 1 秒闪烁一次,我们需要让定时器 1 秒溢出,要计数 144M * 1 = 144M 个时钟周期,而定时器只有16位,最大65535,所以这是不够的。因此,需要用到预分频器,设分频系数为 14400,可以得到 10KHz 的定时器时钟,这样设置计数值 10000 就可以做到 1s 定时。

最终具体的代码如下:

/*
  使用基本定时器TIM6控制LED闪烁
*/

#include "debug.h" // 包含 CH32V307 的头文件,C 标准单元库和delay()函数
#include "board.h"

// 中断只进入一次的问题解决方法:
// 1. 如果使用沁恒提供的工具链,则使用如下的声明:
// void TIM6_IRQHandler(void) __attribute__((interrupt("WCH-Interrupt-fast")));
// 2. 如果使用通用的risc-v工具链,如我在macOS下使用赛昉提供的工具链,则使用如下的声明:
void TIM6_IRQHandler(void) __attribute__((interrupt()));

// LED状态
volatile uint16_t LED_Status = 0; // 中断里使用的变量加 volatile 可当成全局变量

// 初始化 GPIO
void GPIO_INIT(void)
{
    GPIO_InitTypeDef GPIO_InitStructure = {0};

    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &GPIO_InitStructure);
}

// 初始化定时器 TIM6
void TIM6_Init(u16 arr, u16 psc)
{
    TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;

    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM6, ENABLE);

    TIM_TimeBaseInitStructure.TIM_Period = arr;
    TIM_TimeBaseInitStructure.TIM_Prescaler = psc;
    TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
    TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Down;
    TIM_TimeBaseInit(TIM6, &TIM_TimeBaseInitStructure);

    TIM_ITConfig(TIM6, TIM_IT_Update, ENABLE);
    TIM_ARRPreloadConfig(TIM6, ENABLE);
    TIM_Cmd(TIM6, ENABLE);
}

// 初始化定时器中断
void Interrupt_Init(void)
{
    NVIC_InitTypeDef NVIC_InitStructure = {0};
    NVIC_InitStructure.NVIC_IRQChannel = TIM6_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; //抢占优先级
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 2;        //子优先级
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStructure);
}

int main(void)
{
    /* Initialize board components. */
    BOARD_SystemClock_Config();
    BOARD_IOMUX_Init();
    BOARD_Peripheral_Init();

    GPIO_INIT();                                    // 初始化 GPIO
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //设置中断控制器的优先级分组为 占优先级 2位 ,优先级2位。

    USART_Printf_Init(115200);

    TIM6_Init(10000 - 1, 14400 - 1); // 初始化定时器,让 LED 1 秒闪烁一次,我们需要让定时器 1 秒溢出,要计数 `144M * 1 = 144M` 个时钟周期,而定时器只有16位,这是不够的。需要用到预分频器,设分频系数为 14400,可以得到 10KHz 的定时器时钟,这样设置计数值 10000 就可以做到 1s 定时。
    Interrupt_Init();               //初始化定时器中断

    printf("SystemClk:%drn", SystemCoreClock);
    printf("Interrupt TIM6 Testrn");

    while (1)
    {
    }
}

// 中断服务处理
void TIM6_IRQHandler(void)
{
    TIM_ClearFlag(TIM6, TIM_FLAG_Update);         //清除标志位
    LED_Status = !LED_Status;                     // 将 LED 状态值取反
    GPIO_WriteBit(GPIOA, GPIO_Pin_0, LED_Status); // 配置 PE11 (即 LED1) 状态

    printf("Toggle LED by TIM6: %drn", LED_Status);
}

在上述代码中,关键部分如下:

  • GPIO_INIT:初始化GPIO,使用PA0;记得预先连接LED1和PA0
  • TIM6_init:TIM6初始化,主要设置分频和时间周期
  • Interrupt_Init:终端初始化
  • TIM6_IRQHandler中断服务处理,其内部的操作同样需要狠准快

除了板载的LED1(前提是连好了线)会间隔1秒闪烁一次,通过串口工具,我们也可以看到输出的调试信息:
tim6.png

回帖(4)

华仔stm32

2022-6-1 23:16:42
科谱很强,学习了,谢谢分享哈,您总是最优秀的那个男孩!

h1654155285.8644

2022-7-6 15:56:14
很不错的处理器,写的也很好,只能眼馋了。

久醉不醒

2023-3-10 11:11:35
感谢科普,写得太好了

更多回帖

×
发帖