定时器精准延时
一、为什么需要精准延时
时基对于单片机来说就是心脏,它决定着单片机能跑多快,什么时间在干什么,执行代码时是不是符合时序要求等等……在使用一些传感器或者外设模块时总要用到延时,因为传感器的启动和某些操作(模拟IIC)需要延时使功能是正常的,否则可能因为没有延时导致传感器启动失败,时序不符合要求导致数据传输失败。所以,延时对于单片机来讲是一件非常重要的事情,并且我们要让延时尽可能的精准,不然想要延时100ms结果单片机延时了1s那么带来的结果对于整个工程来说就是灾难性的!!!
延时根据方式分类可以分为阻塞式延时和非阻塞式延时,阻塞式延时又可以分为非精准延时和我们本文的主角定时器精准延时,下文我将简单的介绍一下。
二、延时方式
2.1阻塞式延时
阻塞延时顾名思义就是单片机在没有执行完延时指令时不会进行其他操作,可以理解为此时的单片机是“休眠”状态,整个程序被堵在了延时这里,只有当延时时间结束后,单片机才会继续执行其他指令,有点像等红灯,绿灯没亮可是不许走的喔,遵守交通规则(咳咳,跑题了)。
2.1.1非精准延时
所谓非精准延时,就是通过while()或者for()循环体,让单片机循环空操作,来达到延时的目的,执行空操作的次数是根据单片机系统的主频计算得来的,根据系统主频计算出执行一次空操作所需要的时间,通过计算延时时间需要多少个空操作,在循环中累加空操作次数来达到延时的目的,但是由于计算的时间不一定是正好的整数等因素,这种延时方式不精准,只有在粗略的延时过程中才会使用,例如:每隔100ms执行一次打印操作。如果是在需要精准延时的场景下,例如:模拟SPI过程中GPIO引脚翻转的延时操作,则需要下文中的定时器精准延时。
void delay_ms(uint16_t time)
{uint32_t local_time = 0;while(time --){local_time = ??; //??是根据系统主频和所需要延时的时基(ms、us)计算出来的while(local_time --){_NOP(); //空操作}}
}
普通延时参数计算公式:
T 时钟周期 t 空操作时间 T = 1/系统主频 因为执行一次空操作的时间是一个时钟周期,故:t = T
这种延时方式的缺点显而易见,不够精确,优点就是不需要中断,也不用占用任何外设资源,在外设资源紧张且不需要很精准的延时的时候用它就很有效且方便啦。
2.1.2定时器精准延时
芜湖主角闪亮登场,个人还是最爱用这种延时方式啦,精准好用,不需要中断,相较于上文非精准延时,它的优势就是非常精准啦(废话,嘻嘻)。不过也是有缺点的,那就是要占用一个定时器资源啦。
2.1.2.1系统嘀嗒定时器延时(SysTick)
SysTick又名系统嘀嗒定时器,是一个向下计数的24位定时器。
| 寄存器 | 位段 | 名称 | 功能 |
|---|---|---|---|
| 控制寄存器(CTRL) | [16] | COUNTFLAG | 计数至零,置1,读取标志清零 |
| [2] | CLKSOURCE | 时钟源,0 = 外部时钟源,1 = 内部时钟源 | |
| [1] | TICKINT | 1 = 当定时器计数到0时触发异常请求 , 0 = 无动作 | |
| [0] | ENABLE | 定时器使能 | |
| 重装载数值寄存器(LOAD) | [23:0] | RELOAD | 计数器清零时重装载的值 |
| 当前数值寄存器(VAL) | [23:0] | CURRENT | 读取时返回当前计数器的值,读取可以使控制寄存器中的COUNTFLAG位清零 |
使用该延时有两种方式,第一种就是在使用HAL、LL库时直接配置系统主频,CUBEMX在生成工程时会自动生成一个函数即:
HAL/LL_mDelay(uint16_t Time);
该函数为1ms时基的延时,可以填入参数来达到所需的延时时间。
第二种方法则是通过配置SysTick寄存器的方式:
void dealy_us(uint16_t Time)
{uint16_t local_statu = 0;SysTick -> LOAD = Time * 系统主频(单位M); //假设系统主频72M则为Time * 72SysTick -> VAL = 0; //清空计数器SysTick -> CTRL = 0x01; //使能嘀嗒定时器do{local_statu = SysTick -> CTRL;}while((local_statu & 0x01) && (!(local_statu & 0x8000)));SysTick -> CTRL = 0x00;SysTick -> VAL = 0x00;
}void delay_ms(uint16_t Time)
{uint16_t local_statu = 0;SysTick -> LOAD = Time * 系统主频(单位M); //假设系统主频72M则为Time * 72SysTick -> VAL = 0; //清空计数器SysTick -> CTRL = 0x01; //使能嘀嗒定时器for(int i = 0 ; i < 1000 ; i ++){do{local_statu = SysTick -> CTRL;}while((local_statu & 0x01) && (!(local_statu & 0x8000))); }SysTick -> CTRL = 0x00;SysTick -> VAL = 0x00;
}
系统嘀嗒定时器延时精确,但也有缺点,比如在使用FreeRTOS时SysTick被RTOS占用,更改SysTick的值可能会导致RTOS错误。
2.1.2.2TIM定时器精准延时
使用TIM定时器外设做系统精准延时,这是笔者本人最常用的方式,也是最喜欢用的方式,下面是我的示例代码。
<delay.c>
#include "delay.h"
#include "tim.h"uint8_t LL_delay_us(uint16_t tim_us)
{if(tim_us > DELAY_US_MAX){goto errorSet;}CLEAR_TIM_COUNT;while(GET_TIME_NOW < tim_us);return __DELAY_OK;errorSet:printf("The tim_us is over by DELAY_US_MAX\n Please check you tim_us set\n");
}uint8_t LL_delay_ms(uint16_t tim_ms)
{if(tim_ms > DELAY_MS_MAX){goto errorSet;}CLEAR_TIM_COUNT;for(int i = 0 ; i < 1000 ; i ++){while(GET_TIME_NOW < tim_ms);CLEAR_TIM_COUNT;}return __DELAY_OK;errorSet:printf("The tim_us is over by DELAY_MS_MAX\n Please check you tim_ms set\n");return __DELAY_FAIL;
}uint8_t LL_delay_s(uint16_t tim_s)
{if(tim_s > DELAY_S_MAX){goto errorSet;}CLEAR_TIM_COUNT;for(int i = 0 ; i < 1000000 ; i ++){while(GET_TIME_NOW < tim_s);CLEAR_TIM_COUNT;}return __DELAY_OK;errorSet:printf("The tim_us is over by DELAY_S_MAX\n Please check you tim_s set\n");return __DELAY_FAIL;
}
/***************************************************************************
**DELAY.H**
File:延时函数头文件
Author:李家琦
Time:2025/8/7Description:- 使用时应在"delay.c"文件中包含"tim.h"文件- 宏中的TIM1可替换为任意设定定时器 时基单元应按照系统主频设置为1us- 文件设置了执行状态返回值__DELAY_OK & __DELAY_FAIL,可根据需求进行错误检查- errorSet为配置溢出错误提示,可根据需求修改打印信息
***************************************************************************/
#ifndef __DELAY_H
#define __DELAY_H#include "main.h"#define DELAY_US_MAX 65535 //us最大延时时间
#define DELAY_MS_MAX 65535 //ms最大延时时间
#define DELAY_S_MAX 65535 //s最大延时时间
#define __DELAY_OK 0 //成功返回0
#define __DELAY_FAIL 1 //失败返回1
#define DELAY_US_MAX LL_TIM_GetAutoReload(TIM1) //根据自动重装载值计算最大计时单元
#define DELAY_MS_MAX LL_TIM_GetAutoReload(TIM1) //根据自动重装载值计算最大计时单元
#define DELAY_S_MAX LL_TIM_GetAutoReload(TIM1) //根据自动重装载值计算最大计时单元
#define GET_TIME_NOW LL_TIM_GetCounter(TIM1) //获取计数器值
#define CLEAR_TIM_COUNT LL_TIM_SetCounter(TIM1 , 0) //清空计数器值uint8_t LL_delay_us(uint16_t tim_us); //微秒级延时
uint8_t LL_delay_ms(uint16_t tim_ms); //毫秒级延时
uint8_t LL_delay_s(uint16_t tim_s); //秒级延时#endif
本代码是基于LL库的代码,笔者本人喜欢使用LL库,使用HAL库可以看一看思路就好,无非是库函数不同罢了,“delay.h”文件中的宏定义TIM1可以修改为所使用的定时器。这种方式的优点就是延时准确,可配置性高,可以和FreeRTOS一起使用,缺点就是需要占用一个定时器外设。
2.2非阻塞延时
非阻塞延时顾名思义就是延时过程不会阻塞程序的正常运行,只有等到延时时间到达的时候才会执行所需要的操作,这一延时方式需要使用中断。下面是一个简易的例子,只提供思路(因为笔者不爱用),并且该方式不能进行较短的延时,如以1us的时基中断延时,这样会导致程序1us就进一次中断非常不好。
//首先设定一个定时器时基为1ms,使能定时器更新中断,即1ms进入一次中断,配置定时器代码省略
//主函数:uint8_t tim_delay = 0;
int main
{while(1){操作1;if(tim_delay = 1){操作2;tim_delay = 0;}操作3;}
}uint32_t tim = 0;
void TIM1_UP_IRQHandler(void)
{if(LL_TIM_IsActiveFlag_UPDATE(TIMx) != RESET){LL_TIM_ClearFlag_UPDATE(TIMx);tim ++;if(tim == 设定的延时时间){tim_delay = 1;tim = 0;}}
}
程序运行过程中,在主循环里一开始只执行操作1和操作3,只有当系统延时到指定时间后标志位被置1,主循环才会执行操作2。这种方式的优点是充分利用单片机性能,不让单片机将性能浪费在阻塞延时中,缺点是不能进行太短的延时,频繁进出中断影响单片机的执行。
Author:李家琦
Date:2025/8/20
