概念:时钟系统是由振荡器(信号源)、定时唤醒器、分频器等组成的电路。常用的信号源有晶体振荡器和RC振荡器
意义:时钟是嵌入式系统的脉搏,处理器内核在时钟驱动下完成指令执行,状态变换等动作,外设部件在时钟的驱动下完成各种工作,比如串口数据的发送、A/D转换、定时器计数等等。因此时钟对于计算机系统是至关重要的,通常时钟系统出现问题也是致命的,比如振荡器不起振、振荡不稳、停振等。
首先,通过晶体振荡器产生一个时钟信号,然后跟着一个开关,不开这个时钟信号就没有办法传递出去。通常来说晶体振荡器的频率是比较低的,而CPU可以承受的频率是比较大的。比如振荡器只能有几兆或几十兆的时钟信号产生,而CPU需要的时钟信号是上百兆。因此,我们需要倍频器,使输出信号频率等于输入信号频率整数倍的电路,供给CPU使用。然而我们有很多内外设,他们的需要的频率没有CPU那么高,因此我们又需要分频器,将高速频率降下来,适合各个部件的工作频率。
概念:振荡器是用来产生重复电子讯号的电子元件。其构成的电路叫振荡电路,能将直流电转换为具有一定频率交流信号输出的电子电路或装置。
分类:振荡器主要分为RC,LC振荡器和晶体振荡器。RC振荡器是采用RC网络作为选频移相网络的振荡器。LC振荡器是采用LC振荡回路作为移相和选频网络的正反馈振荡器。晶体振荡器的振荡频率受石英晶体控制。
RC振荡器:RC振荡器是由电阻电容构成的振荡电路,能将直流电转换为具有一定频率交流信号输出的电子电路或装置
晶体振荡器:石英晶体振荡器是高精度和高稳定度的振荡器,被广泛应用于彩电、计算机、遥控器等各类振荡电路中,以及通信系统中用于频率发生器、为数据处理设备产生时钟信号和为特定系统提供基准信号
STM32 中主要有四个时钟源: 可以分为高速和低速两种,高速一般几十MHz,低速一般几十kHz;还可以分为内部和外部两种,内部指在STM32内已经集成了,外部指可以在管脚外部接一个晶振。当我们开机刚上电的时候,我们需要给CPU提供时钟信号,但是外部管脚还没有被初始化,这时候我们就需要使用高速内部时钟来给CPU提供时钟信号。如果出现外部时钟出现故障,这个时候也可以调用内部时钟继续工作
STM32中所有的设备都是需要时钟的,那么怎么时钟源给每一个设备分配适合它的时钟信号都会体现在时钟树中。从上往下看,低速内部时钟主要控制看门狗;低速外部时钟控制RTC;高速内部时钟、高速外部时钟都可以直接选择作为系统时钟,也可以让他们通过倍频器将频率提高到最高168MHz作为系统时钟。当确定好选择哪一个时钟后可以通过AHB PRESC给Cortex系统等高速内核使用,也可以在通过APB PRESC给低速外设使用。
配置STM32F407的时钟,并对比STM32时钟配置前(16 MHz)后(168 MHz)LED外设闪烁的快慢。需要注意的是闪烁的时候需要用到delay
函数,然而HAL_Delay
不管设置的时钟是多少都是按照毫秒来计算的,因此我们需要自己写一个delay
函数,比较简单代码如下。
void delay(uint32_t time)
{
uint32_t i, j;
for (i = 0; i < time; i++)
{
for (j = 0; j < 5000; j++);
}
}
通过电路原理图,我的板子上的LED用的是PE2、PF8、PF9 和 PF10,因此要设置他们为输出模式。
然后我们看一下时钟配置,默认情况下用的是HSI,也就是高速内部时钟(16 MHz),导出工程后,在main
函数的while
里写如下代码。然后下载到板子里,可以观察到板子上的LED每次闪烁在一秒左右。
while (1)
{
HAL_GPIO_WritePin(GPIOE, GPIO_PIN_2, GPIO_PIN_RESET);
HAL_GPIO_WritePin(GPIOF, GPIO_PIN_8|GPIO_PIN_9|GPIO_PIN_10, GPIO_PIN_RESET);
delay(1000);
HAL_GPIO_WritePin(GPIOE, GPIO_PIN_2, GPIO_PIN_SET);
HAL_GPIO_WritePin(GPIOF, GPIO_PIN_8|GPIO_PIN_9|GPIO_PIN_10, GPIO_PIN_SET);
delay(1000);
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
然后,我们按照下图重新设置一下时钟(先设置能使用外部晶振,在配置倍频器),这样能把时钟拉满。算是STM32F407能接受的最快速度。这个时候,我们重新导出工程,下载到板子里,可以观察到闪烁速度快了将近十倍。
在上图中,我们假设AHB是 168 MHz,经过/8的分频器,那么SysClock的时钟就是 21 MHz。然后这个时钟信号作为输入,给到定时器,定时器读取重载数值寄存器,假设其值为val
,定时器每次val--
,直到等于0的时候重新加载数值寄存器。这个重载数值寄存器的值是可以设置的,假设我们要设置它到0的时候,时间经过1ms,那么我们要设置的值是多少?
1
21
M
H
z
(
s
)
∗
v
a
l
=
1
(
m
s
)
⇒
v
a
l
=
21000
<
2
24
\frac1{21 MHz}(s) * val = 1 (ms) \Rightarrow val = 21000 < 2^{24}
21MHz1(s)∗val=1(ms)⇒val=21000<224
上面两张图很好的解释了和SysTick有关的寄存器,都比较直白,没啥好解释的
我用的环境是CubeMx和Keil 5,这次要利用SysTick定时器实现1s打印一个字符串。新建一个CubeMx项目,设置USART1串口,然后导出项目。
首先,我们先了解下程序是如何初始化Systick的,打开main.c
可以看到main
函数里的第一行HAL_Init
在这里初始化了Systick
int main(void)
{
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* Configure the system clock */
SystemClock_Config();
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_USART1_UART_Init();
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}
打开HAL_Init
的定义,HAL_InitTick(TICK_INT_PRIORITY)
初始化了Systick,输入的参数是Systick的中断优先级
HAL_StatusTypeDef HAL_Init(void)
{
/* Configure Flash prefetch, Instruction cache, Data cache */
#if (INSTRUCTION_CACHE_ENABLE != 0U)
__HAL_FLASH_INSTRUCTION_CACHE_ENABLE();
#endif /* INSTRUCTION_CACHE_ENABLE */
#if (DATA_CACHE_ENABLE != 0U)
__HAL_FLASH_DATA_CACHE_ENABLE();
#endif /* DATA_CACHE_ENABLE */
#if (PREFETCH_ENABLE != 0U)
__HAL_FLASH_PREFETCH_BUFFER_ENABLE();
#endif /* PREFETCH_ENABLE */
/* Set Interrupt Group Priority */
HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4);
/* Use systick as time base source and configure 1ms tick (default clock after Reset is HSI) */
HAL_InitTick(TICK_INT_PRIORITY);
/* Init the low level hardware */
HAL_MspInit();
/* Return function status */
return HAL_OK;
}
继续追查HAL_InitTick(TICK_INT_PRIORITY)
的定义,在这里定义了HAL_SYSTICK_Config
,设置了systick需要每1ms中断一次
/**
* @brief This function configures the source of the time base.
* The time source is configured to have 1ms time base with a dedicated
* Tick interrupt priority.
* @note This function is called automatically at the beginning of program after
* reset by HAL_Init() or at any time when clock is reconfigured by HAL_RCC_ClockConfig().
* @note In the default implementation, SysTick timer is the source of time base.
* It is used to generate interrupts at regular time intervals.
* Care must be taken if HAL_Delay() is called from a peripheral ISR process,
* The SysTick interrupt must have higher priority (numerically lower)
* than the peripheral interrupt. Otherwise the caller ISR process will be blocked.
* The function is declared as __weak to be overwritten in case of other
* implementation in user file.
* @param TickPriority Tick interrupt priority.
* @retval HAL status
*/
__weak HAL_StatusTypeDef HAL_InitTick(uint32_t TickPriority)
{
/* Configure the SysTick to have interrupt in 1ms time basis*/
if (HAL_SYSTICK_Config(SystemCoreClock / (1000U / uwTickFreq)) > 0U)
{
return HAL_ERROR;
}
/* Configure the SysTick IRQ priority */
if (TickPriority < (1UL << __NVIC_PRIO_BITS))
{
HAL_NVIC_SetPriority(SysTick_IRQn, TickPriority, 0U);
uwTickPrio = TickPriority;
}
else
{
return HAL_ERROR;
}
/* Return function status */
return HAL_OK;
}
追查HAL_SYSTICK_Config(SystemCoreClock / (1000U / uwTickFreq)) > 0U)
的定义,发现还需要继续看SysTick_Config
的定义
/**
* @brief Initializes the System Timer and its interrupt, and starts the System Tick Timer.
* Counter is in free running mode to generate periodic interrupts.
* @param TicksNumb Specifies the ticks Number of ticks between two interrupts.
* @retval status: - 0 Function succeeded.
* - 1 Function failed.
*/
uint32_t HAL_SYSTICK_Config(uint32_t TicksNumb)
{
return SysTick_Config(TicksNumb);
}
定义如下,时间要设置成ticks-1,防止溢出,之后就是操作底层的寄存器。同样的道理我们也可以追查Systick优先级的定义,这里就不继续看了。
/**
\brief System Tick Configuration
\details Initializes the System Timer and its interrupt, and starts the System Tick Timer.
Counter is in free running mode to generate periodic interrupts.
\param [in] ticks Number of ticks between two interrupts.
\return 0 Function succeeded.
\return 1 Function failed.
\note When the variable __Vendor_SysTickConfig is set to 1, then the
function SysTick_Config is not included. In this case, the file device.h
must contain a vendor-specific implementation of this function.
*/
__STATIC_INLINE uint32_t SysTick_Config(uint32_t ticks)
{
if ((ticks - 1UL) > SysTick_LOAD_RELOAD_Msk)
{
return (1UL); /* Reload value impossible */
}
SysTick->LOAD = (uint32_t)(ticks - 1UL); /* set reload register */
NVIC_SetPriority (SysTick_IRQn, (1UL << __NVIC_PRIO_BITS) - 1UL); /* set Priority for Systick Interrupt */
SysTick->VAL = 0UL; /* Load the SysTick Counter Value */
SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk |
SysTick_CTRL_TICKINT_Msk |
SysTick_CTRL_ENABLE_Msk; /* Enable SysTick IRQ and SysTick Timer */
return (0UL); /* Function successful */
}
如果我们要设置1s打印一个字符串,那么我们要设置每1ms后重装载值寄存器清零后的中断回调函数。要找到这个中断回调函数,我们需要查中断向量表
__Vectors DCD __initial_sp ; Top of Stack
DCD Reset_Handler ; Reset Handler
DCD NMI_Handler ; NMI Handler
DCD HardFault_Handler ; Hard Fault Handler
DCD MemManage_Handler ; MPU Fault Handler
DCD BusFault_Handler ; Bus Fault Handler
DCD UsageFault_Handler ; Usage Fault Handler
DCD 0 ; Reserved
DCD 0 ; Reserved
DCD 0 ; Reserved
DCD 0 ; Reserved
DCD SVC_Handler ; SVCall Handler
DCD DebugMon_Handler ; Debug Monitor Handler
DCD 0 ; Reserved
DCD PendSV_Handler ; PendSV Handler
DCD SysTick_Handler ; SysTick Handler
我们需要查看SysTick_Handler
的定义
void SysTick_Handler(void)
{
/* USER CODE BEGIN SysTick_IRQn 0 */
/* USER CODE END SysTick_IRQn 0 */
HAL_IncTick();
HAL_SYSTICK_IRQHandler();
/* USER CODE BEGIN SysTick_IRQn 1 */
/* USER CODE END SysTick_IRQn 1 */
}
查看HAL_SYSTICK_IRQHandler
的定义。
/**
* @brief This function handles SYSTICK interrupt request.
* @retval None
*/
void HAL_SYSTICK_IRQHandler(void)
{
HAL_SYSTICK_Callback();
}
HAL_SYSTICK_Callback
就是我们需要写的函数,写的代码如下,因为每1ms中断一次,那么我们1s就需要1000次才能打印一句话。printf
的实现可以通过覆盖fputc
来实现。
/* USER CODE BEGIN 1 */
void HAL_SYSTICK_Callback(void)
{
static uint32_t i = 0;
i++;
if (i == 1000)
{
printf("1s \n");
i = 0;
}
}
/* USER CODE BEGIN PFP */
int fputc(int ch, FILE *p)
{
while (!(USART1->SR & (1<<7)));
USART1->DR = ch;
return ch;
}
至此上传到板子,就可以实现目标功能。
HAL_Delay是依赖SysTick来实现的,虽然常用,但是如果不正确使用会导致死机。首先我们先看下他的源码
/**
* @brief This function provides minimum delay (in milliseconds) based
* on variable incremented.
* @note In the default implementation , SysTick timer is the source of time base.
* It is used to generate interrupts at regular time intervals where uwTick
* is incremented.
* @note This function is declared as __weak to be overwritten in case of other
* implementations in user file.
* @param Delay specifies the delay time length, in milliseconds.
* @retval None
*/
__weak void HAL_Delay(uint32_t Delay)
{
uint32_t tickstart = HAL_GetTick();
uint32_t wait = Delay;
/* Add a freq to guarantee minimum wait */
if (wait < HAL_MAX_DELAY) //防止我们写delay的时间是0,加一个最小值
{
wait += (uint32_t)(uwTickFreq);
}
while((HAL_GetTick() - tickstart) < wait) // 主要看这里,延时多久主要看while的条件
// 也就是当前时间-开始时间>wait退出
{
}
}
我们可以继续看HAL_GetTick
是怎么写的。它返回了uwTick
/**
* @brief Provides a tick value in millisecond.
* @note This function is declared as __weak to be overwritten in case of other
* implementations in user file.
* @retval tick value
*/
__weak uint32_t HAL_GetTick(void)
{
return uwTick;
}
那uwTick
是怎么变化的呢,经过查找我们发现它首先是一个uint32_t
,以及一个对uwTick
的加法操作,加了什么,我们经过几次查找找到了它是1u,简单说就是每次+1
__IO uint32_t uwTick;
uwTickFreq = 1U,
/**
* @brief This function is called to increment a global variable "uwTick"
* used as application time base.
* @note In the default implementation, this variable is incremented each 1ms
* in SysTick ISR.
* @note This function is declared as __weak to be overwritten in case of other
* implementations in user file.
* @retval None
*/
__weak void HAL_IncTick(void)
{
uwTick += uwTickFreq;
}
那这个HAL_IncTick()
是在哪里被调用的呢?
其实我们之前已经见过了,这也就是说我们每毫秒会调动这个变量一次,每一次都会+1。
void SysTick_Handler(void)
{
/* USER CODE BEGIN SysTick_IRQn 0 */
/* USER CODE END SysTick_IRQn 0 */
HAL_IncTick();
HAL_SYSTICK_IRQHandler();
/* USER CODE BEGIN SysTick_IRQn 1 */
/* USER CODE END SysTick_IRQn 1 */
}
最后我们返回HAL_Delay
的定义
__weak void HAL_Delay(uint32_t Delay)
{
uint32_t tickstart = HAL_GetTick(); // HAL_GetTick每毫秒会+1
uint32_t wait = Delay;
/* Add a freq to guarantee minimum wait */
if (wait < HAL_MAX_DELAY) //防止我们写delay的时间是0,加一个最小值
{
wait += (uint32_t)(uwTickFreq);
}
while((HAL_GetTick() - tickstart) < wait) // 主要看这里,延时多久主要看while的条件
// 也就是当前时间-开始时间>wait退出
{
}
}
总的来说,HAL_Delay
是通过SysTick每毫秒调用的一个回调函数而实现的。如果这个回调函数没有办法被调用,我们的程序就会卡在while
里不更新,也退不出去,因此造成了死机。那么要保证SysTICK_IRQHandler
要顺利执行,那就需要有较高的抢占优先级。如果一个被调用的中断的优先级是 0 0,那么这个时候再调用HAL_Delay
,由于它的优先级也是0 0,这就导致了SysTICK_IRQHandler
无法抢占之前的中断,无法抢断,那么uwTick
就无法+1,最后导致卡在死循环,死机。这也是为什么我们平时设置GPIO或者其他中断的抢占优先级调低一点的原因。