- #include "debug.h"
- /*******************************************************************************
- * Function Name : TIM1Init
- * Description :
- * Input : None
- * Return : None
- *******************************************************************************/
- void TIM1Init(void)
- {
- TIM_TimeBaseInitTypeDef TimeBase_InitStructure;
- RCC_APB2PeriphClockCmd( RCC_APB2Periph_TIM1, ENABLE );
- TimeBase_InitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
- TimeBase_InitStructure.TIM_CounterMode = TIM_CounterMode_Down;
- TimeBase_InitStructure.TIM_Period = 1000;
- TimeBase_InitStructure.TIM_Prescaler = 0;
- TIM1->CNT = 1000;
- TIM_TimeBaseInit( TIM1, &TimeBase_InitStructure );
- TIM_ClearITPendingBit( TIM1, 0X0FF);
- TIM_ITConfig( TIM1, TIM_IT_Update, ENABLE );
- NVIC_EnableIRQ( TIM1_UP_IRQn );
- TIM_Cmd( TIM1, ENABLE );
- }
- /*******************************************************************************
- * Function Name : main
- * Description : Main program.
- * Input : None
- * Return : None
- *******************************************************************************/
- unsigned short cnt;
- int main(void)
- {
- RCC_ClocksTypeDef Clocks_InitStructure;
-
- Delay_Init();
- USART_Printf_Init(115200);
- RCC_GetClocksFreq( &Clocks_InitStructure );
- printf("HCLK:%d\r\n",Clocks_InitStructure.SYSCLK_Frequency);
- printf("APB2Clk:%d\r\n",Clocks_InitStructure.PCLK2_Frequency);
- printf("APB1Clk:%d\r\n",Clocks_InitStructure.PCLK1_Frequency);
- Delay_Ms( 200 );
-
- TIM1Init();
-
- while(1)
- {
- ;
- }
- }
- void TIM1_UP_IRQHandler(void)
- {
- cnt = TIM1->CNT;
- TIM_Cmd( TIM1, DISABLE );
- printf("#%d\n",cnt);
-
- if(TIM_GetFlagStatus( TIM1, TIM_FLAG_Update )!=RESET)
- {
- TIM_ClearITPendingBit( TIM1, TIM_IT_Update ); /* Clear Flag */
- }
- }
中断部分进行反汇编,如下:- TIM1_UP_IRQHandler PROC
- ;;;63 void TIM1_UP_IRQHandler(void)
- 00008e b570 PUSH {r4-r6,lr}
- ;;;64 {
- ;;;65 cnt = TIM1->CNT;
- 000090 480d LDR r0,|L1.200|
- 000092 8800 LDRH r0,[r0,#0]
- 000094 4c19 LDR r4,|L1.252|
- ;;;66 TIM_Cmd( TIM1, DISABLE );
- 000096 4d0c LDR r5,|L1.200|
- 000098 2100 MOVS r1,#0
- 00009a 8020 STRH r0,[r4,#0] ;65
- 00009c 3d24 SUBS r5,r5,#0x24
- 00009e 4628 MOV r0,r5
- 0000a0 f7fffffe BL TIM_Cmd
- ;;;67 printf("#%d\n",cnt);
- 0000a4 8821 LDRH r1,[r4,#0] ; cnt
- 0000a6 a016 ADR r0,|L1.256|
- 0000a8 f7fffffe BL __2printf
- ;;;68
- ;;;69 if(TIM_GetFlagStatus( TIM1, TIM_FLAG_Update )!=RESET)
- 0000ac 2101 MOVS r1,#1
- 0000ae 4628 MOV r0,r5
- 0000b0 f7fffffe BL TIM_GetFlagStatus
- 0000b4 2800 CMP r0,#0
- 0000b6 d005 BEQ |L1.196|
- ;;;70 {
- ;;;71 TIM_ClearITPendingBit( TIM1, TIM_IT_Update ); /* Clear Flag */
- 0000b8 4628 MOV r0,r5
- 0000ba e8bd4070 POP {r4-r6,lr}
- 0000be 2101 MOVS r1,#1
- 0000c0 f7ffbffe B.W TIM_ClearITPendingBit
- |L1.196|
- ;;;72 }
- ;;;73 }
- 0000c4 bd70 POP {r4-r6,pc}
- ;;;74
- ENDP
代码中大部分使用的驱动库编写方式,但在定时中断服务函数中获取当前定时器值采用寄存器访问方式,为了减少代码指令,快速访问到。还需注意,使用“TIM_TimeBaseInit()”函数体,完成后会立刻产生中断标志,所以在开启中断使能前要清除中断标志,否则测试的值不对。
运行结果
从串口打印结果上可以看到,输出当前计数值为980,那么可以预估在中断服务中获取到计数器的差值时间为20个系统周期。看下汇编代码(下图),已提取了有效信息并做解释。
从图上的解释可以看到,在‘5’时,将“TIM1->CNT”寄存器的值保存到全局变量的“cnt”中,但是在步骤‘3’时,已经拿到了“TIM1->CNT”寄存器的值。所以最终差值结果的20个周期时间是中断延迟时间+汇编“1-3”指令执行时间。
我没有在相关手册内部找到关于M3指令执行时间的具体说明,按一般设计,这种汇编指令执行需要2-3个周期,所以我推断上述中断服务的汇编指令大约占用了8个周期,那么实际的中断延迟大约占用了12个周期。
硬件平台CH32V103开发板,下载最新EVT包,进行代码修改。此颗MCU是RISC-V架构,所以选择了MounRiver Studio IDE环境进行编译,源代码部分和CH32F103一样可以保持不变。沁恒为代码兼容做了处理,大部分都可以使用F103上的函数名称和写法。
因为更换了硬件和软件平台,中断的写法有稍微区别,书写一个中断服务函数时,需要为其声明中断属性,即在函数名前添加__attribute__((interrupt()))(原生工具链),这样IDE在进行编译时,会将此函数体识别为中断服务函数,主动添加“压栈出栈”处理及中断返回指令。
C代码,如下:
- #include "debug.h"
- /*******************************************************************************
- * Function Name : TIM1Init
- * Description :
- * Input : None
- * Return : None
- *******************************************************************************/
- void TIM1Init(void)
- {
- TIM_TimeBaseInitTypeDef TimeBase_InitStructure;
- RCC_APB2PeriphClockCmd( RCC_APB2Periph_TIM1, ENABLE );
- TimeBase_InitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
- TimeBase_InitStructure.TIM_CounterMode = TIM_CounterMode_Down;
- TimeBase_InitStructure.TIM_Period = 1000;
- TimeBase_InitStructure.TIM_Prescaler = 0;
- TIM1->CNT = 1000;
- TIM_TimeBaseInit( TIM1, &TimeBase_InitStructure );
- TIM_ClearITPendingBit( TIM1, 0X0FF);
- TIM_ITConfig( TIM1, TIM_IT_Update, ENABLE );
- NVIC_EnableIRQ( TIM1_UP_IRQn );
- TIM_Cmd( TIM1, ENABLE );
- }
- /*******************************************************************************
- * Function Name : main
- * Description : Main program.
- * Input : None
- * Return : None
- *******************************************************************************/
- unsigned short cnt;
- int main(void)
- {
- RCC_ClocksTypeDef Clocks_InitStructure;
- Delay_Init();
- USART_Printf_Init(115200);
- RCC_GetClocksFreq( &Clocks_InitStructure );
- printf("HCLK:%d\r\n",Clocks_InitStructure.SYSCLK_Frequency);
- printf("APB2Clk:%d\r\n",Clocks_InitStructure.PCLK2_Frequency);
- printf("APB1Clk:%d\r\n",Clocks_InitStructure.PCLK1_Frequency);
- Delay_Ms( 200 );
- TIM1Init();
- // NVIC_SetFastIRQ( TIM1_UP_IRQHandler, TIM1_UP_IRQn, 0);
- while(1)
- {
- ;
- }
- }
- __attribute__((interrupt()))
- void TIM1_UP_IRQHandler(void)
- {
- cnt = TIM1->CNT;
- TIM_Cmd( TIM1, DISABLE );
- printf("#%d\n",cnt);
- if(TIM_GetFlagStatus( TIM1, TIM_FLAG_Update )!=RESET)
- {
- TIM_ClearITPendingBit( TIM1, TIM_IT_Update ); /* Clear Flag */
- }
- }
中断部分进行反汇编,如下:
- 00000286 <TIM1_UP_IRQHandler>:
- 286: 715d addi sp,sp,-80
- 288: de22 sw s0,60(sp)
- 28a: 40013437 lui s0,0x40013
- 28e: d03e sw a5,32(sp)
- 290: c2445783 lhu a5,-988(s0) # 40012c24 <_eusrstack+0x2000dc24>
- 294: c0040413 addi s0,s0,-1024
- 298: dc26 sw s1,56(sp)
- 29a: da2a sw a0,52(sp)
- 29c: d82e sw a1,48(sp)
- 29e: 4581 li a1,0
- 2a0: 8522 mv a0,s0
- 2a2: c686 sw ra,76(sp)
- 2a4: c496 sw t0,72(sp)
- 2a6: c29a sw t1,68(sp)
- 2a8: c09e sw t2,64(sp)
- 2aa: d632 sw a2,44(sp)
- 2ac: d436 sw a3,40(sp)
- 2ae: d23a sw a4,36(sp)
- 2b0: ce42 sw a6,28(sp)
- 2b2: cc46 sw a7,24(sp)
- 2b4: ca72 sw t3,20(sp)
- 2b6: c876 sw t4,16(sp)
- 2b8: c67a sw t5,12(sp)
- 2ba: c47e sw t6,8(sp)
- 2bc: 82f19023 sh a5,-2016(gp) # 20000090 <cnt>
- 2c0: 2e79 jal 65e <TIM_Cmd>
- 2c2: 8201d583 lhu a1,-2016(gp) # 20000090 <cnt>
- 2c6: 00002537 lui a0,0x2
- 2ca: a0850513 addi a0,a0,-1528 # 1a08 <_sbrk+0x20>
- 2ce: 23cd jal 8b0 <iprintf>
- 2d0: 4585 li a1,1
- 2d2: 8522 mv a0,s0
- 2d4: 2e75 jal 690 <TIM_GetFlagStatus>
- 2d6: c501 beqz a0,2de <TIM1_UP_IRQHandler+0x58>
- 2d8: 4585 li a1,1
- 2da: 8522 mv a0,s0
- 2dc: 26c1 jal 69c <TIM_ClearITPendingBit>
- 2de: 5472 lw s0,60(sp)
- 2e0: 40b6 lw ra,76(sp)
- 2e2: 42a6 lw t0,72(sp)
- 2e4: 4316 lw t1,68(sp)
- 2e6: 4386 lw t2,64(sp)
- 2e8: 54e2 lw s1,56(sp)
- 2ea: 5552 lw a0,52(sp)
- 2ec: 55c2 lw a1,48(sp)
- 2ee: 5632 lw a2,44(sp)
- 2f0: 56a2 lw a3,40(sp)
- 2f2: 5712 lw a4,36(sp)
- 2f4: 5782 lw a5,32(sp)
- 2f6: 4872 lw a6,28(sp)
- 2f8: 48e2 lw a7,24(sp)
- 2fa: 4e52 lw t3,20(sp)
- 2fc: 4ec2 lw t4,16(sp)
- 2fe: 4f32 lw t5,12(sp)
- 300: 4fa2 lw t6,8(sp)
- 302: 6161 addi sp,sp,80
- 304: 30200073 mret
根据沁恒微电子对其芯片的描述,提供了“中断硬件级的压栈和出栈处理”,所以上段描述的编译软件自动添加“压栈出栈”指令就显得多余了。所以该公司修改了原生工具链(GCC提供的RISC-V工具链),通过在中断服务函数前添加__attribute__((interrupt("WCH-Interrupt-fast"))),取消软件添加压栈出栈指令。
- //__attribute__((interrupt()))
- __attribute__((interrupt("WCH-Interrupt-fast")))
- void TIM1_UP_IRQHandler(void)
- {
- cnt = TIM1->CNT;
- TIM_Cmd( TIM1, DISABLE );
- printf("#%d\n",cnt);
- if(TIM_GetFlagStatus( TIM1, TIM_FLAG_Update )!=RESET)
- {
- TIM_ClearITPendingBit( TIM1, TIM_IT_Update ); /* Clear Flag */
- }
- }
- 00000286 <TIM1_UP_IRQHandler>:
- 286: 1141 addi sp,sp,-16
- 288: c622 sw s0,12(sp)
- 28a: 40013437 lui s0,0x40013
- 28e: c2445783 lhu a5,-988(s0) # 40012c24 <_eusrstack+0x2000dc24>
- 292: c0040413 addi s0,s0,-1024
- 296: c426 sw s1,8(sp)
- 298: 4581 li a1,0
- 29a: 8522 mv a0,s0
- 29c: 82f19023 sh a5,-2016(gp) # 20000090 <cnt>
- 2a0: 2ebd jal 61e <TIM_Cmd>
- 2a2: 8201d583 lhu a1,-2016(gp) # 20000090 <cnt>
- 2a6: 00002537 lui a0,0x2
- 2aa: 9c850513 addi a0,a0,-1592 # 19c8 <_sbrk+0x20>
- 2ae: 23c9 jal 870 <iprintf>
- 2b0: 4585 li a1,1
- 2b2: 8522 mv a0,s0
- 2b4: 2e71 jal 650 <TIM_GetFlagStatus>
- 2b6: c501 beqz a0,2be <TIM1_UP_IRQHandler+0x38>
- 2b8: 4585 li a1,1
- 2ba: 8522 mv a0,s0
- 2bc: 2645 jal 65c <TIM_ClearITPendingBit>
- 2be: 4432 lw s0,12(sp)
- 2c0: 44a2 lw s1,8(sp)
- 2c2: 0141 addi sp,sp,16
- 2c4: 30200073 mret
运行结果,以__attribute__((interrupt("WCH-Interrupt-fast")))结果如下:
从串口打印结果上可以看到,输出当前计数值为986,预估中断服务中获取到计数器的差值时间为14个系统周期。看下汇编代码(下图),已提取了有效信息并做解释。
从上图的反汇编过程,我们可以看到屏蔽了软件压栈后RISC-V结构的中断处理过程。首次通过2条指令(1-2)处理sp堆栈保存。当前函数使用的Callee属性寄存器越多,调用“sw”保存的越多。然后获取外设寄存器基地址到内部s0寄存器中(3),然后通过“lhu”Load指令将定时器当前计数值寄存器的值保存到a5寄存器中,直到图上5处才写入到全局变量cnt中。所以最终结果的14个周期差值时间应该是中断延迟时间+汇编“1-4”指令执行时间。按照沁恒给出的设计指标,2和4执行各占用2周期,其他占用1个周期,那么汇编执行占用约6个周期,中断延迟时间约为8个周期。
CH32V103还提供了4路快速中断通道,即中断触发不从向量表取指令,而是直接到中断服务入口,少一次指令跳转。软件代码上在初始化配置上,增加“NVIC_SetFastIRQ(TIM1_UP_IRQHandler,TIM1_UP_IRQn,0)”,将定时器1的中断服务入口配置到快速通道0上。(注意,这种方法没有限制中断服务的位置,可以在SRAM区也可以在Code区,如果为了提供更快的指令执行速度,可以将中断服务放入SRAM区,再配置给快速通道,而不用修改中断向量表基地址),如下运行结果:
从串口打印结果上可以看到,输出当前计数值为988,快速通道方法比前一种情况速度又快了2个周期,仅需12个系统周期。
总结通过Cortex-M3指南描述,当CM3开始响应一个中断时,会同步进行3项工作。1.入栈,将8个内部寄存器通过硬件自动保存到SRAM中;2.取向量,从向量表中找到对应的服务程序入口地址;3.选择堆栈指针,更新堆栈指针sp、LR寄存器、PC计数器。其中应该属于第一项工作花费时间最长,需要多个周期才能完成数据保存,但是由于采用不同的总线,所以几项工作可以同步执行。所以文档中描述“中断延迟”为:若存储器系统够快,且总线系统允许入栈与取指同时进行,同时该中断可以立即响应,则中断延迟是雷打不动的12周期(满足硬实时所要求的确定性)
而沁恒的RISC-V架构中,一般的中断方式,我们实验测得“中断延迟”约为8个周期,据其设计描述,入栈采用的硬件存储方式,仅需要1个周期就可以完成,剩下的时间包括内核中断挂起、取向量表指令、跳转等时间需要5-6个周期。
2种内核架构使用的是不同的中断处理结构,在时间上沁恒RISC-V中断延迟处理更优,没有入栈存储器的动作也意味着没有存储器出栈的动作,可以完成中断服务内容后更快的返回。以上是单独针对“内核中断延迟”性能方面的比较,但是实际应用中,需要从存储器中取指,如果存储器太慢而引入等待周期或者还有其他因素,则会引入额外的延时,导致整体效果变慢。此外,测试单位是以系统时钟计算,如果单位时间足够的快,貌似几个周期的差值时间似乎没有那么明显,但是高的系统频率会带来存储器的等待延时周期加大或者其他因素,反而降低代码效率,所以不能以单个方面的好评估,具体还是要看应用场景和需求选择合适的MCU产品。