STM32学习笔记(六) SysTick系统时钟滴答实验(stm32中断入门)
系统时钟滴答实验很不难,我就在面简单说下,但其中涉及到了STM32最复杂也是以后用途最广的外设-NVIC,如果说RCC是实时性所必须考虑的部分,那么NVIC就是stm32功能性实现的基础,NVIC的难度并不高,但是理解起来还是比较复杂的,我会在本文中从实际应用出发去说明,当然最好去仔细研读宋岩翻译的<Cortex-M3权威指南>第八章,注意这不是一本教你如何编写STM32代码的工具书,而是阐述Cortex-M3内核原理的参考书,十分值得阅读。
SysTick系统时钟的核心有两个,外设初始化和Systick_Handle()中断处理函数。
Systick配置:
static void SysTick_UserConfig(void)
{
SysTick->CTRL &= 0xfffffffb; //采用内核外部时钟,即SYSTICK
SysTick->LOAD = 0x8000; //重装值寄存器,VAL内数值为0时重装
SysTick->VAL = 0x00; //SysTick当前值寄存器 清零
SysTick->CTRL = 0x03; //SysTick定时器使能,中断使能
}
NVIC配置:
void NVIC_UserConfig(void)
{
NVIC_SetVectorTable(NVIC_VectTab_FLASH,0x0); //将指针指向flash中的中断向量表
}
中断函数:
void SysTick_Handler(void)
{
static uint32_t LED_Flag = 0;
if(LED_Flag < 50)
{
LED_1_ON();
}
if(LED_Flag >= 50)
{
LED_1_OFF();
}
LED_Flag++;
if(LED_Flag == 100)
{
LED_Flag = 0;
}
}
如此,就完成了简单的SysTick滴答实验,代码请参考:https://files.cnblogs.com/files/zc110747/4.SysTick.7z
看到这是不是就结束了,不过记得当时我写完这个程序,疑问有以下几点:
1.向量表是如何定义的,重定位的操作有什么作用
2.为什么中断函数名一定要是void SysTick_Handler(void),怎么确定的
3.中断打乱了正常的程序流程,cpu怎么知道回到之前运行的位置
4.中断优先级如何配置和理解
如何解决这些问题,这都需要从原理方面来解决了,了解过IAP和uC/os-ii的对这些问题应该有一定认知,下面我就系统的讲解下我的想法:
1~2问题. 当 CM3 内核响应了一个发生的异常后,对应的异常服务例程(ESR)就会执行。为了决定 ESR 的入口地址,这就是所谓的“向量表查表机制”。向量表其实是一个 WORD( 32 位整数)数组,每个下标对应一种异常,该下标元素的值则是该 ESR 的入口地址。如果细心查看startup_stm32f10x_cl.s,就会发现下面语句:
; Vector Table Mapped to Address 0 at Reset
AREA RESET, DATA, READONLY
EXPORT __Vectors
EXPORT __Vectors_End
EXPORT __Vectors_Size
__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
; External Interrupts
DCD WWDG_IRQHandler ; Window Watchdog
......
DCD OTG_FS_IRQHandler ; USB OTG FS
__Vectors_End
__Vectors_Size EQU __Vectors_End - __Vectors
AREA |.text|, CODE, READONLY
其中_Vectors既是所提到的WORD数组,这就是定义的向量表,如果了解过指向函数的指针,那么就可以知道,DCD的每一项就是定义的中断服务例程,这样我们就知道为什么Systick中断的中断服务例程是SysTick_Handler,当然根据实际情况这个向量表只要和主函数代码保持一致就可以实现中断的查询,例如uC/os-ii移植中修改PendSV_Handler。
看到这解决了第二个问题,那么第一个问题,从上面可以看出来,向量表一直位于代码的最顶端,也就是偏移量0x0的位置,为什么有时还需要重定位呢?如果看过IAP那篇,了解了启动机制后,应该明白上电后系统会默认跳转0x00000000(flash地址0x08000000映射),然后读取向量表偏移寄存器,查询向量表,因为此时向量表的偏移量就是0x0,向量表就不需要重定义。而在IAP模式下,应用代码的起始地址并不是flash首地址,而是由偏移量0x8000(假定值),从上面也可以简单推出应用代码的向量表偏移量也是0x8000,此时向量表偏移寄存器就需要重定义了。
下图来自于list下生成的.map文件
再参考生成的bin文件和启动文件:
可以很清晰证实上面的观点。
3.了解M3芯片基础的应该知道,M3拥有通用寄存器组R0~R15,这些寄存器在程序运行中保存着代码流程的所有信息,包括当前地址,正在修改的变量参数。因此在中断触发时,只要将R0~R15依次压栈,中断结束后出栈,代码就会回到运行之前的位置(uC/OS-ii正是模拟该过程实现任务切换的),当然这只是最简单的一种情况,因为m3芯片本身支持中断优先级和中断嵌套,实际复杂度远高于此.其实可以简化为如下流程:
主程序暂停 -〉相关位置和状态参数入栈-〉中断服务例程执行-〉相关位置和状态参数出栈-〉主程序恢复
4. Cortex-M3支持最多240个可配置中断,中断优先级的数目3~8位,也就是支持8~256个优先级,而事实上一般并没有支持那么多,如8,16,32级,其中最多支持128个抢占级。arm中断及复位控制寄存器中的3~8位设定优先级的部分,通过配置可将其分割为两部分,前面为抢占级,后面为亚优先级,并且亚优先级至少为1位,分组是可以从保留的优先级组开始的。一个中断的优先级可通过以下顺序判断,优先级依此降低。
优先级组:抢占级 > 亚优先级
优先级: 数值小 > 数值大
中断号: 中断号小 > 中断号大
抢占级和亚优先级的区别:
抢占级是在发生在中断嵌套的基础,上面提到了中断打断了线程的流程,但是如果有抢占级的加入,中断本身也会被优先级更高的中断打断,具体流程如下:
主程序暂停 -〉低优先级中断 -〉高优先级中断-〉低优先级中断-〉主程序恢复
亚优先级表示没有发生中断的嵌套,一个中断只有在它结束后另一个亚优先级的中断才会响应,即不会发生中断嵌套。
此外为了加快中断执行的流程,Cortex-M3提供了基于优先级的四种动作:
1.占先: 主要发生在抢占级中,即高优先级中断低优先级执行,发生中断嵌套
2.末尾连锁: 若占先发生上一个中断的末尾出栈之前,则打断出栈动作,直接执行高优先级中断,结束后如果没有发生抢占,才执行出栈
3.迟来 若占先发生在中断的开始入栈阶段,则继续入栈,低优先级中断挂起。
4.返回 出栈过程,如果收到高优先级中断则停止并产生末尾连锁
从上面可以看出,中断优先级和中断嵌套在加上ARM基于优先级机制的优化,可以让中断中高优先级任务更快执行,正是优先级设定的意义。