前言
笔者使用的硬件开发平台为stm32f407ZGT6,配合CubeMX生成基本代码,使用Keil5/VScode开发编译,库函数为LL/HAL库
承接前文【stm32CubeMX裸机开发HAL库/LL库 (一)】——使用CubeMX新建工程
一、Systick系统滴答定时器与HAL时基
CM4 内核的处理和 CM3 一样,内部都包含了一个 SysTick 定时器,SysTick 是一个 24 位的倒计数定时器,当计到 0 时,将从 RELOAD寄存器中自动重装载定时初值。只要不把它在 SysTick 控制及状态寄存器中的使能位清除,就永不停息。对具体硬件如何实现有兴趣的可以翻手册,本文不细讲了。
相信有了解过原子哥的stm32教程的同学都会发现,原子的时基文件都会使用这个定时器来编写软件延时函数,由于SysTick又属于芯片内部资源,所以这样的延时方案既不占用中断,也不占用系统定时器,在对于定时器外设资源紧张的开发板上是一个不错的选择。
而HAL库为了维护自身的时间基准,默认会选择SysTick定时器作为“基准时钟”,用于实现HAL_delay和各种time_out的时间基准
打开CubeMX系统配置界面,可以看到时基的选择可以多种多样,在不使用OS的情况下一般默认SysTick
接下来看看中断管理
进入NVIC(嵌套向量中断控制器)的配置界面,可以看到默认中断分组为4(即优先级寄存器的高4位全部用于抢占优先级分配,那么共有2^4=16个级别的抢占优先级0~15,0个级别的子优先级)。而SysTick的中断优先级默认设置为最低15,这也是为什么很多新手在一些特定场合使用HAL_Delay导致程序卡死的缘故——中断优先级太低了,导致一直被抢占,延时卡死。
在代码初始化中,贴心的HAL库也自动封装好了中断服务函数(中断服务函数集中存放在stm32f4xx_it.c里)和中断回调函数(对于老玩家作用鸡肋,效率不高,不如自己写),而且开发者可以根据自身习惯选择是否需要初始化。
进入程序,可以看到
HAL库初始化时优先对时基进行了初始化,对SysTick定时器的频率进行了设置,之后才进行时钟配置。
API调用关系:
HAL_Init()
→
\rightarrow
→ HAL_InitTick
→
\rightarrow
→ HAL_SYSTICK_Config
→
\rightarrow
→ SysTick_Config
文件跳转关系:
main
→
\rightarrow
→ stm32f4xx_hal
→
\rightarrow
→ stm32f4xx_hal_cortex
→
\rightarrow
→ core_cm4
(可以看到,由于使用了SysTick,文件跳转也是从芯片外设文件到内核文件)
进入中断文件看看HAL_Delay是如何实现的
可以看到,HAL库定义了一个_IO类型的变量uwTick(_IO即volatile 类型,防止变量被优化,使其可以稳定地读取写入,此处是为了保证uwTick稳定地递增,保证时间准确),然后让其在中断服务函数中以1KHz的频率递增,即HAL_Delay最小延时时间为1ms。
同时,我们还注意到这些函数都是弱定义,即前面修饰了__weak,代表这些函数可以被重写,有想法的开发者可以自己写HAL_Delay,可以参考原子的方法实现us级延时,下面给出例子
static uint32_t fac_us=0; //us延时倍乘数
/**
* @brief 系统滴答定时器 SysTick 初始化
* @param ticks
* SystemFrequency / 1000 1ms中断一次
* SystemFrequency / 100000 10us中断一次
* SystemFrequency / 1000000 1us中断一次
* @retval 无
*/
void bsp_systick_Init(uint8_t SYSCLK)
{
HAL_SYSTICK_CLKSourceConfig(SYSTICK_CLKSOURCE_HCLK);//SysTick频率为HCLK
fac_us=SYSCLK;
}
/**
* @brief ms延时程序,1ms为一个单位
* @param
* @retval 无
*/
void delay_ms(__IO uint16_t nms)
{
uint32_t i;
for(i=0;i<nms;i++) delay_us(1000);
}
/**
* @brief us延时程序,1us为一个单位
* @param
* @retval 无
*/
void delay_us(__IO uint32_t nus)
{
uint32_t ticks;
uint32_t told,tnow,tcnt=0;
uint32_t reload=SysTick->LOAD; //LOAD的值
ticks=nus*fac_us; //需要的节拍数
told=SysTick->VAL; //刚进入时的计数器值
while(1)
{
tnow=SysTick->VAL;
if(tnow!=told)
{
if(tnow<told)tcnt+=told-tnow; //这里注意一下SYSTICK是一个递减的计数器就可以了.
else tcnt+=reload-tnow+told;
told=tnow;
if(tcnt>=ticks)break; //时间超过/等于要延迟的时间,则退出.
}
};
}
二、使用定时器编写软件延时函数
在了解了HAL库使用内核定时器SysTick编写延时函数后,我们也可以使用ST芯片的外设定时器仿写。好在手上的ZGT6外设量大管饱,不过为了节约资源还是选用基本定时器TIM7来完成。接下来进入配置环境:
TIM7 包含一个 16 位自动重载计数器,因此其重装载值最大计数为2^16=65536,我们在配置时输入65536-1=65535
查阅时钟树和手册可知:TIM7挂载在APB1总线桥上,在主频拉满168MHz的情况下,它的最大频率为84MHz,我们设置84-1=83分频数,将其降为1MHz
按图所示配置参数,使能中断
为了更清除地明白外设如何初始化的,这里选择LL库演示,HAL库的方法会在最后给出
完成以上配置初始化工程,接下来写代码
为了与CubeMX生成的代码做区分,在根目录下新建目录Src作为开发者自己的源码
在Src目录内新建文件bsp_timer.c/h
(注意:文件前缀命名有利于区分文件功能,bsp是板级支持包,(board support package)是介于主板硬件和操作系统之间的一层,主要目的是为了支持操作系统,使之能够更好的运行于硬件主板,建议养成良好的命名习惯)
编写以下代码:
bsp_timer.c
#include "bsp_timer.h"
#define TIMx TIM7 //宏定义方便替换定时器
volatile uint32_t SystemTimerCnt; //定义计数变量,使用volatile修饰,类似uwTick
/**
* @brief 初始化定时器
* @retval None
*/
void bsp_Timer_Init(void)
{
LL_TIM_EnableIT_UPDATE(TIMx); //使能定时器向上计数
LL_TIM_ClearFlag_UPDATE(TIMx); //清除向上计数溢出标志位
LL_TIM_EnableCounter(TIMx); //使能定时器开始计数
}
/**
* @brief 获取定时器时间
* @param None
* @retval 实时时间
*/
uint32_t Get_SystemTimer(void)
{
return TIMx->CNT + SystemTimerCnt * 0xffff;
}
/**
* @brief 更新定时器时间
* @not 将此函数加入定时器中断服务函数中
* @param None
* @retval None
*/
void Update_SystemTick(void)
{
if(LL_TIM_IsActiveFlag_UPDATE(TIMx) == SET) //判断定时器是否溢出
{
LL_TIM_ClearFlag_UPDATE(TIMx); //清除向上计数溢出标志位
SystemTimerCnt++; //计数
}
}
/**
* @brief 微秒级软件堵塞延时
* @param cnt : 延时时数
* @retval None
*/
void delay_us_nos(uint32_t cnt)
{
uint32_t temp = cnt + microsecond();
while(temp >= microsecond());
}
/**
* @brief 毫秒级软件堵塞延时
* @param cnt : 延时时数
* @retval None
*/
void delay_ms_nos(uint32_t cnt)
{
uint32_t temp = cnt * 1000 + microsecond();
while(temp >= microsecond());
}
bsp_timer.h
#ifndef __BSP_TIMER_H
#define __BSP_TIMER_H
#ifdef __cplusplus
extern "C"{
#endif
#include "main.h"
#define microsecond() Get_SystemTimer()
void bsp_Timer_Init(void);
void Update_SystemTick(void);
uint32_t Get_SystemTimer(void);
void delay_ms_nos(uint32_t cnt);
void delay_us_nos(uint32_t cnt);
#ifdef __cplusplus
}
#endif
#endif
完成之后按如下步骤添加头文件路径和C文件
将定时器更新函数加入中断服务函数
可以看到CubeMX生成的stm32f4xx_it.c文件将内核部分的中断和外设中断分隔开来
将头文件引入后,初始化定时器,闪个灯
Debug中也可以看到变量在递增
最后回填个坑,HAL库怎么用?
初始化定时器
HAL_TIM_Base_Start_IT(&htim7);
在中断回调函数里执行相关操作
回调函数的好处就是在任何地方都可以写,不用全局变量满天飞,但HAL库封装的回调效率太低,建议不使用,强迫症的朋友可以在CubeMX设置关掉
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if(htim->Instance == TIM7)
{
//此处省略操作部分...........
}
}
其它逻辑参考上面代码操作,HAL库封装的较为高级,很多操作都代劳了,对新手比较友好。但玩到后面要往细了写,还得用寄存器操作语句,因为HAL不会把所有操作都能封装,人是活的,得动脑[Doge]
本人水平有限,如有错误,欢迎批评指正