目录
1.芯片启动过程
2.启动代码
2.1 堆栈定义
2.2 向量表
2.3 复位程序
2.4 中断服务程序
3. 程序执行的过程
4.数据的存取
1.芯片启动过程
芯片启动是上电后先运行芯片内部的固有程序(也就是启动代码)。启动代码程序建立完运行环境之后,就会去读串口状态,就是用户下载程序用到的各个端口,判断用户是否正在使用端口准备下载程序。
如果用户正在下载程序,就按照用户要求,把用户程序下载到指定地址上。如果没有下载程序,芯片默认跳转到已有的用户程序入口,从而把芯片控制权交给用户程序。如果是没有用户程序的新芯片,则停留在读取串口状态的循环中。
启动代码通常都写在flash中,是系统一上电就执行的一段程序。它运行在任何用户C代码之前。上电之后,arm处理器处于arm态,运行于特权模式,同时系统所有中断被禁止,PC到地址0处取指令执行。
我们以Flash启动模式为例。上电后,地址0x0800 0000开始的程序被映射到地址0x0000 0000,然后程序从0x0000 0000开始执行。芯片会从地址0x0000 0000读取32位长度的值,并赋值给栈指针SP;然后从地址0x0000 0004读取32位长度的值复制给程序计数器指针PC,程序将会从PC指针内容处开始执行。
启动代码主要完成两方面的工作,一是初始化执行环境,例如中断向量表、堆栈、I/O等;二是初始化c库和用户应用程序。
2.启动代码
以STM32为例,我们来看一下从芯片上电到main函数的过程。主要步骤如下:
1. 初始化堆栈指针SP=_initial_sp,初始化PC=Reset_Handler
2. 初始化中断向量表
3. 配置系统时钟
4. 调用C库函数__main初始化用户堆栈,然后进入main函数
因为启动过程主要是由汇编完成的,因此STM32的启动的大部分内容都是在启动文件里。现在以startup_stm32f103xe.s为例(其他类型的启动文件原理是类似的)。
2.1 堆栈定义
1. 栈Stack
栈的作用是用于局部变量、函数调用、函数形参等保存,栈的大小不能超过内部SRAM的大小。当程序较大时,需要修改栈的大小,不然可能会出现HardFault的错误。
第33行:表示栈的大小为0x00000400(1KB),EQU是伪指令,相当于C中的define。
第35行:开辟一段可读可写数据空间,ARER伪指令表示下面将开始定义一个代码段或数据段。此处是定义数据段。ARER后面的关键字表示这个段的属性。段名为STACK,可以任意命名;NOINIT表示不初始化;READWRITE表示可读可写,ALIGN=3,表示按照8字节对齐。
第36行:SPACE用于分配带大小等于Stack_Size连续内存空间,单位为字节。
第37行:__initial_sp表示栈顶地址。栈是由高向低生长的。
2. Heap堆
堆主要用来动态内存的分配,像malloc()函数申请的内存就在堆中。
开辟堆的大小为0x00000200(512字节),名字为HEAP,NOINIT即不初始化,可读可写,8字节对齐。__heap_base表示对的起始地址,__heap_limit表示堆的结束地址。
2.2 向量表
向量表是一个WORD(32位整数)数组,每个下标对应一种异常,该下标元素的值则是该ESR的入口地址。向量表在地址空间中的位置是可以设置的,通过NVIC中的一个重定位寄存器来指出向量表的地址。在复位后,该寄存器的值为0。因此,在地址0(即FLASH地址0)处必须包含一张向量表,用于初始化时的异常分配。值得注意的是这里有个另类:0号类型并不是什么入口地址,而是给出了复位后MSP的初值,后面会具体讲解。
第55行:定义一块代码段,段名字是RESET,READONLY表示只读。
第56-58行:使用EXPORT将3个标识符声明为可被外部引用,声明__Vectors、__Vectors_End和__Vectors_Size具有全局属性。
第60行:__Vectors表示向量起始地址,DCD表示分配1个4字节的空间。每行DCD都会生成一个4字节的二进制代码,中断向量表存放的实际上是中断服务程序的入口地址。当异常(也就是中断事件)发生时,CPU的中断系统会将相应的入口地址赋值给PC程序计数器,之后就开始执行中断服务程序。在60行之后,依次定义了中断服务程序的入口地址。
第138行:__Vectors_End为向量表结束地址。
第139行:__Vectors_Size是向量表的大小,向量表的size由__Vectors_End减去__Vectors得到。
2.3 复位程序
复位程序是系统上电后执行的第一个程序,复位程序也是中断程序,只是比较特殊,所以特意单讲一下。
第 145行:定义了一个服务程序,PROC表示程序的开始。
第146行:使用EXPORT将Reset_Handler声明为可被外部引用,后面[WEAK]表示弱定义。弱定义的函数可以在外部文件重新定义,此时则使用外部文件定义的函数,如果外部文件没有定义,则引用此处定义的[WEAK]函数。这里也就是表示复位程序可以被用户在其他文件重新实现。这种写法在HAL库中是很常见的。
第147-148行:表示引用两个来自外部文件的标号,其中__main是一个标准的C库函数,主要用于初始化用户堆栈。这个由编译器完成,该函数最终会调用我们写的main函数,从而进入到我们的系统中。SystemInit()是一个库函数,主要用于 系统时钟配置和中断相关配置。
第149行:将SystemInit的地址从存储器中加载到寄存器R0中
第150行:跳转到R0中的地址(也就是SystemInit函数),并根据寄存器的LSE确定处理器的状态,还要把跳转前的下一条指令地址保存到LR。
第151-152行:同上面两行,从存储器中取出__main的地址加载到R0里,并跳转到R0中的地址去。152和150不同的是152行跳转到指定寄存器的地址后,不会返回。
第153行:和PROC对应,表示程序结束。
2.4 中断服务程序
平时要使用哪个中断,只需要编写相应的中断服务程序,只是启动文件把这些函数留出来了,但是内容是空的,真正的中断服务程序需要我们在外部的C文件里面重新实现,这里只是提前占了一个位置而已。
这部分没有需要讲的,和服务程序类似的,只需要注意'B.'语句,B表示跳转,这里跳转到一个'.',即表示无线循环。
3. 程序执行的过程
单片机的程序运行过程分为取指令、分析指令和执行指令几个步骤。
(1)取指令:根据程序计数器PC中的值从程序存储器中读出指令,送到指令寄存器。
(2)分析指令:将指令寄存器中的指令操作码取出后进行译码,分析其指令性质。如指令要求操作数,则寻找操作数指令。
(3)执行指令:无非是把一条二进制代码,转换成数字信号(高低电平),操作逻辑门电路,就像我们的加法器一样输入输出。把经过逻辑门运算的结果输出,把单片机的相关引脚电平输出高或低。
单片机执行程序的过程实际上就是逐条指令地重复上述操作过程,直到遇到停机指令可循环等待指令。
例如:
开机时,程序计算器PC变为0000H
。然后单片机在时序电路作用下自动进入执行程序过程。执行过程实际上就是取出指令(取出存储器中事先存放的指令阶段)和执行指令(分析和执行指令)的循环过程。
例如执行指令:MOV A,#0E0H
,其机器码为74H E0H
,该指令的功能是把操作数E0H
送入累加器,0000H
单元中已存放74H
,0001H
单元中已存放E0H
。当单片机开始运行时,首先是进入取指阶段,其次序是:
-
程序计数器的内容(这时是
0000H
)送到地址寄存器; -
程序计数器的内容自动加1(变为
0001H
); -
地址寄存器的内容(
0000H
)通过内部地址总线送到存储器,以存储器中地址译码电跟,使地址为0000H
的单元被选中; -
CPU使读控制线有效;
-
在读命令控制下被选中存储器单元的内容(此时应为74H)送到内部数据总线上,因为是取指阶段,所以该内容通过数据总线被送到指令寄存器。
4.数据的存取
对于单片机的程序执行时指令和数据的存放与读取,理解如下:
程序的代码段、.data段、.bss段、rodata段等都存放在Flash中。单片机上电后,初始化汇编代码将.data段、.bss段,复制到RAM中,并建立好堆栈,开始调用程序的main函数。
之后,便有了程序存储器,和数据存储器之分,运行时从Flash(即指令存储器,代码存储器)中读取指令 ,从RAM中读取与写入数据。RAM存在的意义就在于速度更快。
无论是单片机也好,PC也罢,存在的存储器金字塔都是一致的,速度的因素,成本的限制导致了一级级更快的存储器的更快速度与更高的成本。应该说,对于它们的理解,就是存储器金字塔的理解。