一、STM32单片机的延时
STM32单片机
的延时,是指在程序中暂停一段时间,等待一定的时间后再继续执行下一条指令。常见的延时方式有循环延时
和定时器延时
。
毫秒延时的使用场景:
- 等待外设完成某项操作:在使用外设时,有时需要等待外设完成某项操作才能进行下一步操作。例如,在使用
SPI通信
、OneWire通信(DS18B20 DHT11等)
时,需要等待数据传输完成才能读取接收到的数据。 - 控制任务执行时间:在多任务系统中,任务的执行时间需要控制在一定范围内,以避免出现任务响应时间不稳定、任务饥饿等问题。
- 实现延时操作:有时需要在任务中实现一些延时操作,例如等待一定时间后再执行某些操作。
- 实现周期性任务:在一些周期性任务中,需要在每个周期内执行一定的操作。此时,可以使用毫秒延时来实现周期性的触发。
需要注意的是,使用毫秒延时时应注意精度和稳定性。
在需要更高精度的应用中,可以考虑使用定时器来实现延时。
毫秒延时的实现方法:
-
使用
SysTick
定时器 -
使用
TIM
定时器 -
使用
for循环
进行延时 -
使用
DWT
寄存器
二、测试准备
- 基于
STM32L431RCT6
的小熊派开发板 - 安装
windows
系统并安装Cubemx
和Keil MDK
的电脑
三、初始化片上外设
3.1 设置板载的LED-PC13和PA11为推挽输出模式
同样设置PA11引脚为推挽输出模式,为的是在后面方便测试。
3.2 使用定时器进行毫秒延时
使用定时器进行精确延时的原理:
定时器在设置好初始值的时候,便会自增,在自增的过程中便会产生一个时间等待,使用定时器的精确计数便可以精确设置要等待的时间。
- 打开定时器
TIM2
设置定时器2的Clock Source
为 Internal Clock
- 时钟源设置为外部高速时钟(使用内部产生的时钟源也可)
【重要】
查看开发板的板载晶振的频率(根据自己的开发板的晶振频率设置),因此设置输入的时钟的频率为8Hz,经过分频后最后设置频率为最大80MHz,查看经过设置后定时器所在的外设桥时钟频率亦为80MHz
- 针对
TIM2
的一些参数进行配置
设置分频系数为84 - 1
,则定时器的计数时钟频率为1MHz
,即为计数一次需要消耗1s/1M = 1us
的时间。
设置的计数周期是65535
,本次实验不涉及中断,因此不需要开启中断。
- 设置生成Keil-MDK代码文件
3.3 使用SysTick
定时器进行毫秒延时
STM32的滴答定时器(SysTick)是一种基于硬件的定时器,可以提供系统级别的延时和定时功能。它使用一个24位计数器和一些相关寄存器来控制其行为。
滴答定时器的相关寄存器介绍:
[来自core_cm3.h]
/** @addtogroup CMSIS_CM3_SysTick CMSIS CM3 SysTick
memory mapped structure for SysTick
@{
*/
typedef struct
{
__IO uint32_t CTRL; /*!< Offset: 0x00 SysTick Control and Status Register */
__IO uint32_t LOAD; /*!< Offset: 0x04 SysTick Reload Value Register */
__IO uint32_t VAL; /*!< Offset: 0x08 SysTick Current Value Register */
__I uint32_t CALIB; /*!< Offset: 0x0C SysTick Calibration Register */
} SysTick_Type;
CTRL | SysTick控制和状态寄存器,用于控制SysTick计数器的启动、中断、时钟源以及清零等操作 |
---|---|
LOAD | SysTick重装载值寄存器,用于设置SysTick计数器的重装载值 |
VAL | SysTick当前值寄存器,用于读取SysTick计数器当前的计数值 |
CALIB | SysTick校准寄存器,用于获取SysTick计数器的时钟周期数和是否支持64位读取操作的信息 |
四、测试
4.1 编写定时器的延时代码
[在tim.c的代码添加处添加]
/* USER CODE BEGIN 1 */
/*设置的milliseconds需要小于65535阈值*/
void us_Delay(uint32_t milliseconds) // 延时函数,参数为需要延时的毫秒数
{
HAL_TIM_Base_Start(&htim2); // 启动定时器 2
__HAL_TIM_SET_COUNTER(&htim2, 0); // 将定时器 2 的计数器清零
while(__HAL_TIM_GET_COUNTER(&htim2) < milliseconds); // 等待定时器 2 的计数器达到指定的毫秒数
HAL_TIM_Base_Stop(&htim2); // 停止定时器 2
}
/* USER CODE END 1 */
/*
设置延时时间为1ms 但是由于就是代码在执行的过程中是也有执行时间的,因此就是需要手动调节延时参数。
*/
void 1ms_Delay(void)
{
us_Delay(951);
}
/*
设置延时时间为1s
*/
void one_s_Delay(void)
{
for(uint16_t i = 0;i<1000;i++)
{
one_ms_Delay();
}
}
[在主函数while循环中添加]
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
HAL_GPIO_WritePin(USER_GPIO_Port,USER_Pin,GPIO_PIN_SET); //设置PA11为高电平
//HAL_GPIO_WritePin(LED_GPIO_Port,LED_Pin,GPIO_PIN_SET); //设置PC13为高电平
one_ms_Delay(); //延时1ms
//one_s_Delay(); //延时1s
HAL_GPIO_WritePin(USER_GPIO_Port,USER_Pin,GPIO_PIN_RESET);//设置PA11为低电平
//HAL_GPIO_WritePin(LED_GPIO_Port,LED_Pin,GPIO_PIN_SET); //设置PC13为高电平
one_ms_Delay(); //延时1ms
//one_s_Delay(); //延时1s
}
使用逻辑分析仪器进行电平的分析
设置延时时间为1ms
设置延时时间为1s 但是不是严丝合缝的1s
4.2 编写SysTick定时器的延时代码
使用滴答定时器配置相关寄存器,实现us、ms 和s级的延时
下面是适用于固件库中的一个us级延时代码
[用于固件库中使用,节选自一个项目中]
/**
*@brief 初始化延迟函数
*@param SYSCLK:系统时钟
*@return 无
*/
void systick_init (u8 sysclk)
{
SysTick->CTRL&=0xfffffffb; /*bit2清空,选择外部时钟 HCLK/8*/
fac_us=sysclk/8;
fac_ms=(u16)fac_us*1000;
}
/**
*@brief 微秒延时函数
*@param time_ms:要延时微秒时间数
*@return 无
*/
void delay_us(uint32 time_us)
{
u32 temp;
SysTick->LOAD=time_us*fac_us; /* 将时间加载进SysTick的重载值寄存器 */
SysTick->VAL=0x00; /*清空计数器*/
SysTick->CTRL=0x01 ; /* 开始倒数,使用内部时钟,开启SysTick计时器 */
do
{
temp=SysTick->CTRL; /* 获取SysTick的CTRL寄存器值 */
}
while(temp&0x01&&!(temp&(1<<16))); /*等待时间到达*/
SysTick->CTRL=0x00; /*关闭SysTick计数器*/
SysTick->VAL =0X00; /*清空计数器*/
}
/**
*@brief 毫秒延时函数
*@param time_ms:要延时毫秒时间数
*@return 无
*/
void delay_ms(uint32 time_ms)
{
u32 temp;
SysTick->LOAD=(u32)time_ms*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; /*清空计数器*/
}
/**
*@brief 秒延时函数
*@param time_s:要延时秒时间数
*@return 无
*/
void delay_s(uint32 time_s)
{
for(;time_s>0;time_s--)
delay_ms(1000);
}
[在主函数中添加]
systick_init(72); /*初始化Systick工作时钟*/
while(1)
{
delay_us(1000);
printf("一毫秒延时打印测试");
}
下面是适用于HAL库中的一个us级延时代码
[用于HAL库中,代码节选自正点原子]
//此段代码需要屏蔽,因为和Cubemx生成的代码冲突
//static uint32_t g_fac_us = 0; /* us延时倍乘数 */
///**
// * @brief 初始化延迟函数
// * @param sysclk: 系统时钟频率, 即CPU频率(rcc_c_ck), 168MHz
// * @retval
// */
//void delay_init(uint16_t sysclk)
//{
// g_fac_us = sysclk;
//}
/**
* @brief 延时nus
* @param nus: 要延时的us数.
* @note nus取值范围 : 0~190887435(最大值即 2^32 / fac_us @fac_us = 21)
* @retval 无
*/
void delay_us(uint32_t nus)
{
uint32_t ticks;
uint32_t told, tnow, tcnt = 0;
uint32_t reload = SysTick->LOAD; /* LOAD的值 */
ticks = nus * g_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; /* 时间超过/等于要延迟的时间,则退出 */
}
}
}
}
/**
* @brief 延时nms
* @param nms: 要延时的ms数 (0< nms <= 65535)
* @retval 无
*/
void delay_ms(uint16_t nms)
{
uint32_t repeat = nms / 540; /* 这里用540,是考虑到可能有超频应用, 比如248M的时候,delay_us最大只能延时541ms左右了 */
uint32_t remain = nms % 540;
while (repeat)
{
delay_us(540 * 1000); /* 利用delay_us 实现 540ms 延时 */
repeat--;
}
if (remain)
{
delay_us(remain * 1000); /* 利用delay_us, 把尾数延时(remain ms)给做了 */
}
}
/**
* @brief HAL库内部函数用到的延时
* @note HAL库的延时默认用Systick,如果我们没有开Systick的中断会导致调用这个延时后无法退出
* @param Delay : 要延时的毫秒数
* @retval None
*/
void HAL_Delay(uint32_t Delay)
{
delay_ms(Delay);
}
[在主函数while中添加]
/* USER CODE BEGIN 2 */
#define g_fac_us 84 //需要添加此宏定义,然后就可以用正点原子的代码托管后续的延时函数
/* USER CODE END 2 */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
HAL_GPIO_WritePin(USER_GPIO_Port,USER_Pin,GPIO_PIN_SET); //设置PA11为高电平
delay_us(952); //延时1ms
HAL_GPIO_WritePin(USER_GPIO_Port,USER_Pin,GPIO_PIN_RESET);//设置PA11为低电平
delay_us(952); //延时1ms
}
/* USER CODE END 3 */
在我的代码中设置为延时952us可以呈现1ms的电平变化效果,因此在使用的时候需要根据自己的代码自行调试。
后面的逻辑分析仪分析不再展示
4.3 编写for循环实现的延时代码
/* USER CODE BEGIN 4 */
void delay_us(uint32_t us)
{
for(uint32_t i = 0; i < us * 16; i++)
{
__NOP();
}
}
/* USER CODE END 4 */
[在while循环中添加]
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
HAL_GPIO_WritePin(USER_GPIO_Port,USER_Pin,GPIO_PIN_SET); //设置PA11为高电平
delay_us(1000); //延时1ms
HAL_GPIO_WritePin(USER_GPIO_Port,USER_Pin,GPIO_PIN_RESET);//设置PA11为低电平
delay_us(1000); //延时1ms
}
/* USER CODE END 3 */
这个延时数量不需要修改,非常准确的延时