最近一直想用系统滴答定时器来做一个us级别的延时,用正点原子和其他的一些函数库都试了一下,最终的结果都不尽人意,要不然就是延时进不来,要不然就是频率错误,自己在示波器的帮助下,搞定了一部分内容,目前能够正常使用,产生1us的延时。
至于直接在函数库里修改系统配置文件,有以下担心,所以一直没有操作。
HAL_SYSTICK_Config(HAL_RCC_GetHCLKFreq()/1000);//此配置为HAL库自带配置,延时为1MS;
HAL_SYSTICK_Config(HAL_RCC_GetHCLKFreq()/1000000);//此配置为硬石HAL教程推荐的方法,延时为1US。
这样虽然调用HAL_DELAY() 函数能够正常产生1US的延时,但是毕竟修改了系统内置的配置,系统在初始化和调用一些外设的时候还是会自己用到这个延时函数,导致延时函数变化,可能会产生不可排查的BUG(虽然目前还没发现那个地方调用了此延时函数会发生BUG,总归还是小心点的好)。因此排除此方法。
自己在查看正点原子的教程的时候发现,正点原子的教程里面介绍SYSTICK的时钟为调用外部高速时钟(HSE)的时候时钟为HCLK的1/8,或者调用内置高速时钟(HSI)的时候为FCLK(这里面自己没理解好正点原子的意思,具体原因下面有讲解)
代码如下:
//初始化延迟函数
//当使用OS的时候,此函数会初始化OS的时钟节拍
//SYSTICK的时钟固定为AHB时钟的1/8
//SYSCLK:系统时钟频率
void delay_init(u8 SYSCLK)
{
SysTick_CLKSourceConfig(SysTick_CLKSource_HCLK_Div8); //配置systick的时钟源
fac_us=SYSCLK/8; //不论是否使用OS,fac_us都需要使用
fac_ms=(u16)fac_us*1000; //非OS下,代表每个ms需要的systick时钟数
}
以上代码删除了支持OS的部分,这样看起来比较简洁。
再来看配置时钟源的函数
/**
* @brief Configures the SysTick clock source.
* @param SysTick_CLKSource: specifies the SysTick clock source.
* This parameter can be one of the following values:
* @arg SysTick_CLKSource_HCLK_Div8: AHB clock divided by 8 selected as SysTick clock source.//当函数选择SysTick_CLKSource_HCLK_Div8的时候,systick时钟=AHPCLOCK(也就是HCLK)/8
* @arg SysTick_CLKSource_HCLK: AHB clock selected as SysTick clock source.
//当函数选择SysTick_CLKSource_HCLK的时候,systick时钟=AHBclock(也就是FCLK)
* @retval None
*/
void SysTick_CLKSourceConfig(uint32_t SysTick_CLKSource)
{
/* Check the parameters */
assert_param(IS_SYSTICK_CLK_SOURCE(SysTick_CLKSource));
if (SysTick_CLKSource == SysTick_CLKSource_HCLK)
{
SysTick->CTRL |= SysTick_CLKSource_HCLK;
}
else
{
SysTick->CTRL &= SysTick_CLKSource_HCLK_Div8;
}
}
再来看 SysTick_CLKSource_HCLK_Div8的定义
#define SysTick_CLKSource_HCLK_Div8 ((uint32_t)0xFFFFFFFB)
也就是说8分频的时候,将0xFFFFFFFB赋值(进行与运算)到SysTick->CTRL
再来看SysTick_CLKSource_HCLK 的定义
#define SysTick_CLKSource_HCLK ((uint32_t)0x00000004)
也就是将0x00000004赋值(或运算)到)SysTick->CTRL
8分频的结果就是将CTRL的2位段置0,从而选择称为外部时钟源。
不分频 的结果就是将CTRL的2位段置1,从而选择称为内部时钟源。
注:这个内部和外部时钟源不是HSE 和HSI,而是针对系统时钟HCLK而言的。
此图来源于STM32F4XX中文参考手册第107页,正确的标识了两种时钟的区别
下面解释一下这个所谓的内部和外部时钟
系统时钟SYSCLK最大频率为168MHz,它是供STM32中绝大部分部件工作的时钟源。系统时钟可由PLL、HSI或者HSE提供输出,并且它通过AHB分频器分频后送给各模块使用,AHB分频器可选择1、2、4、8、16、64、128、256、512分频。其中AHB分频器输出的时钟送给5大模块使用:
①、送给AHB总线、内核、内存和DMA使用的HCLK时钟。
②、分频后送给STM32芯片的系统定时器时钟(Systick=Sysclk/8=9Mhz)
③、直接送给Cortex的自由运行时钟(free running clock)FCLK。【ARMJISHU注:FCLK 为处理器的自由振荡的处理器时钟,用来采样中断和为调试模块计时。在处理器休眠时,通过FCLK 保证可以采样到中断和跟踪休眠事件。 Cortex-M3内核的“自由运行时钟(free running clock)”FCLK。“自由”表现在它不来自系统时钟HCLK,因此在系统时钟停止时FCLK 也继续运行。FCLK和HCLK 互相同步。FCLK 是一个自由振荡的HCLK。FCLK 和HCLK 应该互相平衡,保证进入Cortex-M3 时的延迟相同。】
④、送给APB1分频器。APB1分频器可选择1、2、4、8、16分频,其输出一路供APB1外设使用(PCLK1,最大频率36MHz),另一路送给定时器(Timer)2、3、4倍频器使用。该倍频器可选择1或者2倍频,时钟输出供定时器2、3、4使用。
⑤、送给APB2分频器。APB2分频器可选择1、2、4、8、16分频,其输出一路供APB2外设使用(PCLK2,最大频率72MHz),另一路送给定时器(Timer)1倍频器使用。该倍频器可选择1或者2倍频,时钟输出供定时器1使用。另外,APB2分频器还有一路输出供ADC分频器使用,分频后送给ADC模块使用。ADC分频器可选择为2、4、6、8分频。
注意:STMCUBEMX软件中的时钟图有一个地方容易误导大众,
因此这时候终于搞明白这两个所谓的内部和外部时钟,
当使用HAL库默认的情况时, HAL_SYSTICK_CLKSourceConfig(SYSTICK_CLKSOURCE_HCLK);
此时systick采用的时钟为FCLK,频率为168MHZ,因此配置us定时器的LOAD值应该为168*nus,
//延时nus
//nus为要延时的us数.
//注意:nus的值,不要大于2^24/168=99864us
void delay_us(u32 nus)
{
u32 temp;
SysTick->LOAD=nus*168; //时间加载
SysTick->VAL=0x00; //清空计数器
SysTick->CTRL|=SysTick_CTRL_ENABLE_Msk ; //开始倒数
do
{
temp=SysTick->CTRL;
}
while((temp&0x01)&&!(temp&(1<<16)));//等待时间到达
SysTick->CTRL&=~SysTick_CTRL_ENABLE_Msk; //关闭计数器
SysTick->VAL =0X00; //清空计数器
}
当需要修改systick的时钟源为HCLK的1/8时,HAL_SYSTICK_CLKSourceConfig(SYSTICK_CLKSOURCE_HCLK_DIV8);
此时systick的时钟源为1/8的HCLK,频率为168/8=21MHZ,因此定时器LOAD值应为21*nus
//延时nus
//nus为要延时的us数.
//注意:nus的值,不要大于2^24/21=798915us
void delay_us(u32 nus)
{
u32 temp;
SysTick->LOAD=nus*21; //时间加载
SysTick->VAL=0x00; //清空计数器
SysTick->CTRL|=SysTick_CTRL_ENABLE_Msk ; //开始倒数
do
{
temp=SysTick->CTRL;
}
while((temp&0x01)&&!(temp&(1<<16)));//等待时间到达
SysTick->CTRL&=~SysTick_CTRL_ENABLE_Msk; //关闭计数器
SysTick->VAL =0X00; //清空计数器
}
下面贴一下,在正常HAL库操作生成的代码中,如何编写延时函数,达到延时1us的目的
主函数如下
#include "main.h"
#include "stm32f4xx_hal.h"
#include "adc.h"
#include "i2c.h"
#include "rtc.h"
#include "usart.h"
#include "gpio.h"
/* USER CODE BEGIN Includes */
#include "delay.h"
/* USER CODE END Includes */
/* Private variables ---------------------------------------------------------*/
/* USER CODE BEGIN PV */
/* Private variables ---------------------------------------------------------*/
//重定义fputc函数
int fputc(int ch, FILE *f)
{
while((USART1->SR&0X40)==0);//循环发送,直到发送完毕
USART1->DR = (uint8_t) ch;
return ch;
}
/* USER CODE END PV */
/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
/* USER CODE BEGIN PFP */
/* Private function prototypes -----------------------------------------------*/
/* USER CODE END PFP */
/* USER CODE BEGIN 0 */
/* USER CODE END 0 */
/**
* @brief The application entry point.
*
* @retval None
*/
int main(void)
{
/* USER CODE BEGIN 1 */
/* USER CODE END 1 */
/* MCU Configuration----------------------------------------------------------*/
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* USER CODE BEGIN Init */
/* USER CODE END Init */
/* Configure the system clock */
SystemClock_Config();
/* USER CODE BEGIN SysInit */
delay_init(); //在系统设置玩时钟之后,初始化一下自己写的延时函数
/* USER CODE END SysInit */
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_I2C1_Init();
MX_USART1_UART_Init();
MX_RTC_Init();
MX_ADC1_Init();
/* USER CODE BEGIN 2 */
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
delay_us(50000); //延时50ms,也就是一个周期为100ms,z指示灯翻转1次
HAL_GPIO_TogglePin(GPIOA, LED2_Pin);
//HAL_Delay(1000);
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}
/**
* @brief System Clock Configuration
* @retval None
*/
void SystemClock_Config(void)
{
RCC_OscInitTypeDef RCC_OscInitStruct;
RCC_ClkInitTypeDef RCC_ClkInitStruct;
RCC_PeriphCLKInitTypeDef PeriphClkInitStruct;
/**Configure the main internal regulator output voltage
*/
__HAL_RCC_PWR_CLK_ENABLE();
__HAL_PWR_VOLTAGESCALING_CONFIG(PWR_REGULATOR_VOLTAGE_SCALE1);
/**Initializes the CPU, AHB and APB busses clocks
*/
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE|RCC_OSCILLATORTYPE_LSE;
RCC_OscInitStruct.HSEState = RCC_HSE_ON;
RCC_OscInitStruct.LSEState = RCC_LSE_ON;
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
RCC_OscInitStruct.PLL.PLLM = 4;
RCC_OscInitStruct.PLL.PLLN = 168;
RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV2;
RCC_OscInitStruct.PLL.PLLQ = 4;
if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
{
_Error_Handler(__FILE__, __LINE__);
}
/**Initializes the CPU, AHB and APB busses clocks
*/
RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
|RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV4;
RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV2;
if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_5) != HAL_OK)
{
_Error_Handler(__FILE__, __LINE__);
}
PeriphClkInitStruct.PeriphClockSelection = RCC_PERIPHCLK_RTC;
PeriphClkInitStruct.RTCClockSelection = RCC_RTCCLKSOURCE_LSE;
if (HAL_RCCEx_PeriphCLKConfig(&PeriphClkInitStruct) != HAL_OK)
{
_Error_Handler(__FILE__, __LINE__);
}
/**Enables the Clock Security System
*/
HAL_RCC_EnableCSS();
/**Configure the Systick interrupt time
*/
HAL_SYSTICK_Config(HAL_RCC_GetHCLKFreq()/1000);
/**Configure the Systick
*/
HAL_SYSTICK_CLKSourceConfig(SYSTICK_CLKSOURCE_HCLK);
/* SysTick_IRQn interrupt configuration */
HAL_NVIC_SetPriority(SysTick_IRQn, 0, 0);
}
/* USER CODE BEGIN 4 */
/* USER CODE END 4 */
/**
* @brief This function is executed in case of error occurrence.
* @param file: The file name as string.
* @param line: The line in file as a number.
* @retval None
*/
void _Error_Handler(char *file, int line)
{
/* USER CODE BEGIN Error_Handler_Debug */
/* User can add his own implementation to report the HAL error return state */
while(1)
{
}
/* USER CODE END Error_Handler_Debug */
}
#ifdef USE_FULL_ASSERT
/**
* @brief Reports the name of the source file and the source line number
* where the assert_param error has occurred.
* @param file: pointer to the source file name
* @param line: assert_param error line source number
* @retval None
*/
void assert_failed(uint8_t* file, uint32_t line)
{
/* USER CODE BEGIN 6 */
/* User can add his own implementation to report the file name and line number,
tex: printf("Wrong parameters value: file %s on line %d\r\n", file, line) */
/* USER CODE END 6 */
}
#endif /* USE_FULL_ASSERT */
/**
* @}
*/
/**
* @}
*/
延时函数如下:
#include "delay.h"
#include "sys.h"
//////////////////////////////////////////////////////////////////////////////////
static u8 fac_us=0;//us延时倍乘数
static u16 fac_ms=0;//ms延时倍乘数,
//初始化延迟函数
//SYSTICK的时钟等于FCLK时钟=168MHZ
//SYSCLK:系统时钟
void delay_init(void)
{
fac_us=168; //因为HAL库函数定义的默认值为 168MHZ
fac_ms=(u16)fac_us*1000;//
}
//延时nus
//nus为要延时的us数.
//注意:nus的值,不要大于99864us
void delay_us(u32 nus)
{
u32 temp;
SysTick->LOAD=nus*fac_us; //时间加载
SysTick->VAL=0x00; //清空计数器
SysTick->CTRL|=SysTick_CTRL_ENABLE_Msk ; //开始倒数
do
{
temp=SysTick->CTRL;
}
while((temp&0x01)&&!(temp&(1<<16)));//等待时间到达
SysTick->CTRL&=~SysTick_CTRL_ENABLE_Msk; //关闭计数器
SysTick->VAL =0X00; //清空计数器
}
//延时nms
//注意nms的范围
//SysTick->LOAD为24位寄存器,所以,最大延时为:
//nms<=2^24/168/1000=99ms
void delay_xms(u16 nms)
{
u32 temp;
SysTick->LOAD=(u32)nms*fac_ms;//时间加载(SysTick->LOAD为24bit)
SysTick->VAL =0x00; //清空计数器
SysTick->CTRL|=SysTick_CTRL_ENABLE_Msk ; //开始倒数
do
{
temp=SysTick->CTRL;
}
while((temp&0x01)&&!(temp&(1<<16)));//等待时间到达
SysTick->CTRL&=~SysTick_CTRL_ENABLE_Msk; //关闭计数器
SysTick->VAL =0X00; //清空计数器
}
//延时nms
//nms:0~65535
void delay_ms(u16 nms)
{
u8 repeat=nms/99; //这里用99,是考虑到大部分人不会超频使用,如果超频使用,请自己自行修改
//比如超频到248M的时候,delay_xms最大只能延时67ms左右了
u16 remain=nms%99;
while(repeat)
{
delay_xms(99);
repeat--;
}
if(remain)delay_xms(remain);
}
以上配置是CUBEMX默认的配置,大家在使用的过程中注意us的使用限制范围就行了0-99864us
假如想使用8分频的HCLK,作为信号源,一下是主程序和延时程序的代码,进攻参考
主程序只需要修改
HAL_SYSTICK_CLKSourceConfig(SYSTICK_CLKSOURCE_HCLK_DIV8);
// HAL_SYSTICK_CLKSourceConfig(SYSTICK_CLKSOURCE_HCLK);
延时字程序如下
#include "delay.h"
#include "sys.h"
//////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////
static u8 fac_us=0;//us延时倍乘数
static u16 fac_ms=0;//ms延时倍乘数
//初始化延迟函数
//SYSTICK的时钟等于HCLK的1/8时钟=168/8=21MHZ
//SYSCLK:系统时钟
void delay_init(void)
{
fac_us=21;
fac_ms=(u16)fac_us*1000;
}
//延时nus
//nus为要延时的us数.
//注意:nus的值,不要大于798915us
void delay_us(u32 nus)
{
u32 temp;
SysTick->LOAD=nus*fac_us; //时间加载
SysTick->VAL=0x00; //清空计数器
SysTick->CTRL|=SysTick_CTRL_ENABLE_Msk ; //开始倒数
do
{
temp=SysTick->CTRL;
}
while((temp&0x01)&&!(temp&(1<<16)));//等待时间到达
SysTick->CTRL&=~SysTick_CTRL_ENABLE_Msk; //关闭计数器
SysTick->VAL =0X00; //清空计数器
}
//延时nms
//注意nms的范围
//SysTick->LOAD为24位寄存器,所以,最大延时为:
//nms<=0xffffff*8*1000/SYSCLK
//SYSCLK单位为Hz,nms单位为ms
//对168M条件下,nms<=798ms
void delay_xms(u16 nms)
{
u32 temp;
SysTick->LOAD=(u32)nms*fac_ms;//时间加载(SysTick->LOAD为24bit)
SysTick->VAL =0x00; //清空计数器
SysTick->CTRL|=SysTick_CTRL_ENABLE_Msk ; //开始倒数
do
{
temp=SysTick->CTRL;
}
while((temp&0x01)&&!(temp&(1<<16)));//等待时间到达
SysTick->CTRL&=~SysTick_CTRL_ENABLE_Msk; //关闭计数器
SysTick->VAL =0X00; //清空计数器
}
//延时nms
//nms:0~65535
void delay_ms(u16 nms)
{
u8 repeat=nms/540; //这里用540,是考虑到某些客户可能超频使用,
//比如超频到248M的时候,delay_xms最大只能延时541ms左右了
u16 remain=nms%540;
while(repeat)
{
delay_xms(540);
repeat--;
}
if(remain)delay_xms(remain);
}
现在对比完毕了,两种方式各有优缺点,
1.使用HCLK的1/8作为时钟源的话,在us延时函数里面,延时范围是798915us,远大于用FCLK的99864us。
2.使用FCLK作为时钟源的话,好处是在系统时钟停止时FCLK 也继续运行,此时systick时钟依然运行着。