看到这篇文章的应该都是做嵌入式的,都不是新手,可能大家都上手过一些片子,也开发过项目。
用下来感觉如何?
MCU的门槛是很低的,现在的网上资料一大堆,课程满天飞,很多人都可以快速上手,厂家给的SDK也相对完善,可以说这部分很简单。
在这种情况下,只要你懂C语言和一些简单的外设原理,对着demo你就能开发。
在这个基础上,怎么样更深一步,真正的从开发中学到东西?而不是单纯的会抄demo而已?
从我站的不高的角度来分析,我觉得要深入思考一下现象下的本质,一些原理性的东西,底层的东西。
第一篇,先从stm32 系统的启动开始。
最开始学的时候以为main就是所有程序的入口,是大家约好的,后来接触了6410/2440这些,就会有疑惑,为啥stm32不用启动代码,再后来知道这些都是keil和st帮我们做了,很多东西其实我们就一眼带过了,便捷是便捷了,导致的原因可能就是很久都不清楚真正的原因。
那这一篇文章的目标就是:
能想象出一个芯片上电之后的样子,是怎么样跑的,PC怎么动的,堆栈怎么分的,内存是怎么样子
下面我们用stm32f103c8t6,来真实的跑一遍。
刚上电,cotex-m3的内核默认会去0x0地址取出栈指针地址,然后偏移4个字节取出跳转地址。
在Cotex m3 权威指南中有介绍。
这里有个地方要注意:
如图,可以看出0x8000004的数据是0x80001ED,把0x80001ED填入PC,实际运行的是0x80001EC的代码,为什么不相等?
PC 中的数据最低两位并不代表真实的取址地址。ARM中使用最低一位来判断这条指令是 ARM 指令还是 Thumb 指令,若最低位为 0,代表 ARM 指令;若最低位为 1,代表 Thumb 指令。在 Cortex-M/R 内核中,并不支持 ARM 模式,若强行切换到 ARM 模式会引发一个 Hard Fault。
看到这里可能有朋友不理解,我们的代码明明是load到0x8000000地址的,为什么这里是从0x0开始运行的。
这里,其实是ST做了一个地址的映射,根据boot0,boot1的组合,把0x8000000(flash)映射到了0x0。这其实也就是我们熟悉的启动mode的选择。
STM32把从0x00000000到0x0005FFFF的区域作为启动空间(boot space)的别名区。
这里可以看出,ST可以把0x8000000或者片内固化启动代码的地方映射为0。
关于RAM启动,即0x20000000,我看到两种说法,
一种是,0x2000 0000 无法映射到0x0,所以要重设中断向量表。
另一种说法是,0x2000 0000 映射到了0x0,但是在启动后断开了,所以无法通过0x0的地址去访问RAM。
这部分我有点疑惑,后面又专门做了这方面的验证:stm32深入思考(2) 之 RAM启动
引用一段在别的帖子里看到的:
所有的处理器PC指针复位值都会是0,所以一定会在0地址开始执行代码。
这个0地址是个很讲究的东西。可以出现在0地址的东西通常有下面几种:
- 片内的SRAM
- 外部总线上的NOR FLASH
- 片内固化的ROM CODE。每个厂家都会给ROM CODE起个名字,比如三星管这个叫IROM
那么怎么知道map哪个呢,这种情况下CPU还没开始跑,所以只能依赖于硬件逻辑。一般的做法是通过一些外部的管脚来配置,三星平台管这个叫OM。通过这部分的配置,芯片内部逻辑确定将什么东东map过去。
这部分是ST做的,这样,系统上电就会直接到0x8000000,也就是我们下载了bin的地方。
bin是什么这里就不细说了,就是镜像文件,还有和hex的区别关系什么的,大家应该都清楚,简而言之,就是bin是啥样,flash就是啥样,一模一样的。所以做IAP升级的时候只能升级bin格式进去。
3.2我们说到,系统会从0x800 0000取MSP,从0x8000004取PC的初始值。
而我们的bin就是直接load到0x8000000的,也就是说,编译的东西,放在最前面的是什么?
查生成文件的排列顺序,一般就是直接看链接文件的,但是我在keil里面有找到.ld/.lds文件这些,有点晕,后来在输出文件夹找到一个sct文件,打开是这样的。
; *************************************************************
; *** Scatter-Loading Description File generated by uVision ***
; *************************************************************
LR_IROM1 0x08000000 0x00010000 { ; load region size_region
ER_IROM1 0x08000000 0x00010000 { ; load address = execution address
*.o (RESET, +First)
*(InRoot$$Sections)
.ANY (+RO)
}
RW_IRAM1 0x20000000 0x00004000 { ; RW data
.ANY (+RW +ZI)
}
}
这里rom和ram的配置,和我在option里设置的是一样的。
但是这里我们要注意一句话,就是:
** *.o (RESET, +First) **
把RESET段放在第一个。
OK,那找到了,RESET段在哪?
搜索一下代码,在start.s (startup_stm32f10x_hd.s)里。
这一段就是DCD了一些空间,放在最上面的第一二句:
__Vectors DCD __initial_sp ;
DCD Reset_Handler ;
就是0x8000000和0x8000004位置的东西。
可以看出,正是一个是MSP(__initial_sp),一个是系统入口( Reset_Handler装入PC)
这个文件是启动文件,是st公司提供的,根据不同的容量,有startup_stm32f10x_cl.s、startup_stm32f10x_hd.s、startup_stm32f10x_hd_vl.s。。。等等。
内部具体源码的分析,在CSDN有太多太多了,一抓一大把,这里就不细细描述了,直接拿我们要的东西。
AREA RESET, DATA, READONLY ;复位段,只包含数据,只读
...
; Reset handler
Reset_Handler PROC
EXPORT Reset_Handler [WEAK]
IMPORT __main
IMPORT SystemInit
LDR R0, =SystemInit ; 装载寄存器指令
BLX R0 ; 带链接的跳转,切换指令集
LDR R0, =__main
BX R0 ; 切换指令集,main函数不返回
ENDP
_user_initial_stackheap
; 此处是初始化两区的堆栈空间,堆是从由低到高的增长,栈是由高向低生长的,两个是互相独立的数据段,并不能交叉使用。
LDR R0, = Heap_Mem
LDR R1, = (Stack_Mem + Stack_Size)
LDR R2, = (Heap_Mem + Heap_Size)
LDR R3, = Stack_Mem
BX LR
start.s里的Reset_Handler 是和流程有关的,系统上电,PC取到Reset_Handler入口,跳转过来,然后运行,看代码。
; Reset handler
Reset_Handler PROC
EXPORT Reset_Handler [WEAK]
IMPORT __main
IMPORT SystemInit
LDR R0, =SystemInit
BLX R0
LDR R0, =__main
BX R0
ENDP
很明显看出,系统是调用了SystemInit 和 __main两个函数。
SystemInit 是在system_stm32f10x.c中,
看代码
/**
* @brief Setup the microcontroller system
* Initialize the Embedded Flash Interface, the PLL and update the
* SystemCoreClock variable.
* @note This function should be used only after reset.
* @param None
* @retval None
*/
void SystemInit (void)
{
/* Reset the RCC clock configuration to the default reset state(for debug purpose) */
/* Set HSION bit */
RCC->CR |= (uint32_t)0x00000001;
/* Reset SW, HPRE, PPRE1, PPRE2, ADCPRE and MCO bits */
#ifndef STM32F10X_CL
RCC->CFGR &= (uint32_t)0xF8FF0000;
#else
RCC->CFGR &= (uint32_t)0xF0FF0000;
#endif /* STM32F10X_CL */
/* Reset HSEON, CSSON and PLLON bits */
RCC->CR &= (uint32_t)0xFEF6FFFF;
/* Reset HSEBYP bit */
RCC->CR &= (uint32_t)0xFFFBFFFF;
/* Reset PLLSRC, PLLXTPRE, PLLMUL and USBPRE/OTGFSPRE bits */
RCC->CFGR &= (uint32_t)0xFF80FFFF;
#ifdef STM32F10X_CL
/* Reset PLL2ON and PLL3ON bits */
RCC->CR &= (uint32_t)0xEBFFFFFF;
/* Disable all interrupts and clear pending bits */
RCC->CIR = 0x00FF0000;
/* Reset CFGR2 register */
RCC->CFGR2 = 0x00000000;
#elif defined (STM32F10X_LD_VL) || defined (STM32F10X_MD_VL) || (defined STM32F10X_HD_VL)
/* Disable all interrupts and clear pending bits */
RCC->CIR = 0x009F0000;
/* Reset CFGR2 register */
RCC->CFGR2 = 0x00000000;
#else
/* Disable all interrupts and clear pending bits */
RCC->CIR = 0x009F0000;
#endif /* STM32F10X_CL */
#if defined (STM32F10X_HD) || (defined STM32F10X_XL) || (defined STM32F10X_HD_VL)
#ifdef DATA_IN_ExtSRAM
SystemInit_ExtMemCtl();
#endif /* DATA_IN_ExtSRAM */
#endif
/* Configure the System clock frequency, HCLK, PCLK2 and PCLK1 prescalers */
/* Configure the Flash Latency cycles and enable prefetch buffer */
SetSysClock();
#ifdef VECT_TAB_SRAM
SCB->VTOR = SRAM_BASE | VECT_TAB_OFFSET; /* Vector Table Relocation in Internal SRAM. */
#else
SCB->VTOR = FLASH_BASE | VECT_TAB_OFFSET; /* Vector Table Relocation in Internal FLASH. */
#endif
}
主要做了两个事,
__main这个函数我单步跟踪没有跟进去,暂时看不到是怎么实现的,只能看下汇编:
观察R0寄存器,说明__main就是在0x8000131这里。
找到对应的位置,可以看到跳转的是__scatterload函数。
为什么不是131而是130,和之前用0x80001ED跳转 0x80001EC的道理一样。
这个函数看介绍,作用是:
负责把RW/RO输出段从装载域地址复制到运行域地址,并完成了ZI运行域的初始化工作。
这个函数我没找到源码,但是综合流程,应该就是把flash中bin文件的RW/ZI段等一些运行中需要修改的内容拷贝到RAM,同时把ZI段(我理解就是BSS + heap + stack)清零。
负责初始化堆栈,完成库函数的初始化,最后自动跳转向main()函数
OK,到达目的地,成功抵达main函数。