本文针对stm32w108库函数STM32W108xx_SimpleMAC_V2.0.1分析,toolchain为MDK-ARM V4.7,C Compiler版本V5.03.0.24。以下分析有不对的地方欢迎指出。我的QQ:12430300。
以前ST为stm32w108写的zigbee库函数EmberZNet-4.0.2,EmberZNet-4.3.0等,是由Ember公司写的,好多语句都写在了宏里面,查看代码的时候跳转起来相当不方便。而且这几个库把好多函数都以库文件的方式提供,使用起来也不是很方便。这次看到st官方网站提供的库都是以源代码形式提供(除了射频部分),并且格式和stm32f103的库基本保持了一致,感觉相当好。更值得高兴的是keil现在可以支持stm32w的编译,这对我这样的习惯使用keil的人来说绝对是一大福音。
我在使用EmberZNet的库的时候,mcu休眠之后唤醒是可以从休眠前的位置继续执行下去的,但这次的库在使用过程中发现唤醒后出现异常,没有继续之前的代码执行,样子像是代码跑飞。为了解决这个问题,本文先从休眠的原理开始分析。
先了解大体的休眠过程:
1.调用halInternalSleep()->2.保存寄存器值到内存,并触发pendSV软中断->3.LR等寄存器自动压栈->4.手动保存当前SP值到内存->5.休眠->6.唤醒并重启->7.触发pendSV软中断->8.恢复sp值->9.出中断回到休眠时的位置下面继续执行。
解释:
1.任何位置调用halInternalSleep进行休眠
2.保存寄存器值到内存,因为休眠重启后寄存器的值会被还原,而内存在休眠时候是不掉电的。
4.真正休眠之前还需要保存当前上下文的状态,也就是栈内容需要保存。栈也是内存,休眠后不丢失,所以只需要保存SP的值就可以。保存SP是到软中断内做的,这样做的好处是LR等寄存器的值会由硬件保证自动压栈(3),返回的时候是自动出栈的,可以直接调用BX LR返回,这样就不怕中断返回的时候没有LR地址了。因为函数嵌套调用的时候返回地址LR是不正确的,返回也不能使用BX LR这样的指令,而是当调用函数时先把LR入栈(PUSH {LR}),函数返回时直接把LR出栈到PC(POP PC)。
5.软中断中调用WFI休眠
7.唤醒重启后,先判断是否要恢复上下文(__low_level_init的工作),如果需要则触发一次软中断,恢复SP的值
由此可以了解到,只要休眠前的栈状态和LR值能够在休眠后完全复原,程序就能顺利返回到休眠前的状态继续执行下去。但是不要忘记,SP值在系统复位后是会被初始化的,这个时候只要有任何栈操作,就可能会导致休眠前的栈内的值被修改掉。
以下是代码分析
Sleep.c休眠代码
void halInternalSleep(SleepModes sleepMode) //真正进行休眠的函数
{
//之前的代码省略
//作用大体上是保存寄存器到内存,并判断是否要真正休眠
//以下是核心代码
if(!skipSleep)
{
CLK->HSECR2 &= ~CLK_HSECR2_SW1;
SCB->SCR |= SCB_SCR_SLEEPDEEP_Msk; /* enable deep sleep */
extern __IO boolean halPendSvSaveContext;
halPendSvSaveContext = 1; /*标记1,说明进行了休眠时的上下文保存*/
SCB->ICSR |= SCB_ICSR_PENDSVSET_Msk; /* 触发pendSV中断*/
while(halPendSvSaveContext) {}// 还原时halPendSvSaveContext已被清零
wakeupSleepTmrCnt = SLPTMR->CNTH<<16;
wakeupSleepTmrCnt |= SLPTMR->CNTL;
}
else
{
/* Record the fact that we skipped sleep */
halInternalWakeEvent |= BIT32(SLEEPSKIPPED_INTERNAL_WAKE_EVENT_BIT);
/* If this was a true deep sleep, we would have executed cstartup and
PRIMASK would be set right now. If we skipped sleep, PRIMASK is not
set so we explicitely set it to guarantee the powerup sequence
works cleanly and consistently with respect to interrupt
dispatching and enabling.*/
__disable_irq();
}
//以下省略,作用为还原内存中的之前的寄存器值到寄存器
}
SCB->ICSR |= SCB_ICSR_PENDSVSET_Msk这一句触发软中断,在软中断中保存上下文并真正休眠
pendSV中断代码
halPendSvIsr
LDR R0, =halPendSvSaveContext ;//load the variable's address
LDRB R0, [R0] ;//get the value in the variable
CBZ R0, contextRestore ;// 比较halPendSvSaveContext决定是保存还是还原上下文
contextSave
MRS R0, MSP ;//load the main stack pointer into R0
SUB R0, R0, #0x20 ;//make room on the stack for 8 words (32 bytes)
MSR MSP, R0 ;//load new MSP from adjusted stack pointer
STM R0, {R4-R11} ;//store R4-R11 (8 words) onto the stack
LDR R1, =savedMSP ;//load address of savedMSP into R1
STR R0, [R1] ;//保存栈指针MSP 到变量savedMSP中
WFI ;//all saved, trigger deep sleep
;;// Even if we fall through the WFI instruction, we will immediately
;;// execute a context restore and end up where we left off with no
;;// ill effects. Normally at this point the core will either be
;;// powered off or reset (depending on the deep sleep level).
contextRestore
LDR R0, =savedMSP ;//load address of savedMSP into R0
LDR R0, [R0] ;//load the MSP from savedMSP
LDM R0, {R4-R11} ;//load R4-R11 (8 words) from the stack
ADD R0, R0, #0x20 ;//eliminate the 8 words (32 bytes) from the stack
MSR MSP, R0 ;//还原栈指针 MSP from R0
BX LR ;//返回到休眠之前的状态
接着看启动代码。
Startup_stm32w108xx.s 启动代码
Stack_Size EQU 0x00001000
AREA STACK, NOINIT, READWRITE, ALIGN=3
Stack_Mem SPACE Stack_Size ;空余这么多空间,作为栈空间
__initial_sp ;指向栈顶,sp向下增长
。。。
。。。
; Reset handler重启后开始执行的位置
Reset_Handler PROC
EXPORT Reset_Handler [WEAK]
IMPORT SystemInit
IMPORT __low_level_init
IMPORT __main
1 LDR R0, =SystemInit ;
2 BLX R0 ;
3 LDR R0, =__low_level_init
4 BLX R0
5 LDR R0, =__main
6 BX R0
7 ENDP
1 SystemInit入口地址放到R0
2将下一语句(LDR R0, =__low_level_init)地址存入LR,然后跳入SystemInit
3__low_level_init入口地址放到R0
4将下一语句(LDR R0, =__main)地址存入LR,然后跳入__low_level_init
SystemInit源代码如下
void SystemInit (void)
{
// extern int cnt;
// cnt+=7;
/* reset the CLK_HSECR2 register */
CLK->HSECR2 &= (uint32_t)0x00000000;
/* reset the CLK_CPUCR register */
CLK->CPUCR &= (uint32_t)0x00000000;
/* Configure the System clock frequency */
SetSysClock();
}
SystemInit对应汇编代码
0x0800084E B510 PUSH {r4,lr}
0x08000850 F04F2040 MOV r0,#0x40004000
0x08000854 69C0 LDR r0,[r0,#0x1C]
0x08000856 2000 MOVS r0,#0x00
0x08000858 F04F2140 MOV r1,#0x40004000
0x0800085C 61C8 STR r0,[r1,#0x1C]
0x0800085E 4608 MOV r0,r1
0x08000860 6A00 LDR r0,[r0,#0x20]
0x08000862 2000 MOVS r0,#0x00
0x08000864 6208 STR r0,[r1,#0x20]
0x08000866 F7FFFFD2 BL.W SetSysClock (0x0800080E)
0x0800086A BD10 POP {r4,pc}
0x0800084E这句将r4和rl入栈,2个寄存器一共8字节,在此之前sp指向初始化后默认值,并非休眠之前的sp值,所以休眠前的堆栈被破坏了8字节。
SystemInit这个函数中可能还有其他的函数嵌套调用,嵌套的越深,栈内容被破坏的就越严重。
SystemInit执行结束后执行__low_level_init(void)判断启动原因,是否是唤醒,如果是唤醒则调用halTriggerContextRestore恢复上下文。
halTriggerContextRestore代码如下
THUMB
__EXPORT__ halTriggerContextRestore
halTriggerContextRestore
LDR R0, =savedMSP ;//load address of savedMSP into R0
LDR R0, [R0] ;//load the MSP from savedMSP
MSR MSP, R0 ;//还原休眠之前的SP
CPSIE i ;//触发 PendSV 中断
BX LR ;//这里永远不会被执行到
halTriggerContextRestore先还原了SP,然后触发软中断进行上下文恢复,这里还原SP的作用是保证进入软中断的过程中,原先的栈内容不会被冲毁。问题,为什么halTriggerContextRestore不直接还原上下文并跳转过去而要触发中断在返回呢?原因是触发中断能够触发硬件的自动入栈和出栈,这样一方面和休眠前保持了一致,另一方面函数调用的出入栈有编译器的因素在内,LR在栈内位置未知。
看到这里应该可以了解到休眠恢复后程序跑飞的真正原因了吧。原因就是休眠之前的栈内容被破坏掉了。最简单的解决方法就是在main函数内定义一个不会使用大数组,这样的话这个数组会在调用main的时候分配到栈的底部,重启的时候这部分数据被冲毁将不会影响程序正常运行。我想有些童鞋之所以使用原来的例子代码,可能是因为在while(1)之前已经有好多入栈操作,唤醒后冲毁的也只是这部分数据,所以没有导致跑飞。