【典藏】深度剖析单片机程序的运行(C程序版)
1、日常聊一聊(听说文章与音乐更配)
文章开头为大家挑选了一首《句号》,句号并不是代表着一切的结束,仅仅是一个新的开始。就像一篇文章充满着句号,句号后却会有更多精彩的内容。哈哈,又唠唠叨叨讲大道理了!今天为大家带来一篇对于单片机学习的小伙伴非常重量级的一篇文章《深度剖析单片机程序的运行(C语言版本)》,该文章会比较全面的为大家解析我们的用C语言编译出来的程序是如何在单片机中运行,包括程序的结构、变量的存储模型、内核运行程序流程等等内容,帮助大家能够更好的理解单片机运行程序的机制,便于我们在平时的开发过程中定位并解决相关bug,从而写出更加优质且符合单片机运行的代码。
2、两大体系结构
计算机界的两大体系结构分别是:冯洛伊曼体系结构和哈佛体系结构,对于一款芯片到底属于其中哪一种体系结构大家也是争论不休,根据我的了解其实并不是这两种体系结构有多难区分,而是因为为了提高运行程序的高效等衍生出了很多变体的结构导致难以评判,不过对于一般应用小伙伴并不会说一定要对这两种体系结构进行区分,这里作者就个人认识来说上几句:冯氏结构的观点认为程序也是一种数据,所以两者可以混合存储,并且数据总线和地址总线都是共用的,而哈佛结构则是把程序和数据分开存储,并且分别有独立的数据总线进行访问,从而大大提高了数据的吞吐量。
目前大部分intel的计算机处理器大多是冯氏结构,而单片机、ARM等微型计算机大多采用的是哈佛结构。
3、单片机内部结构简介
为了便于后面内容更好的理解,这里简单补充一下单片机内部的一些单元,下图是我为大家简单画的一个示意图:
这里我们简单解析一下该图:该图描述了大部分单片机的结构简图,左边主要是用于计算和数据处理,右边主要是用于数据存储和对外输入输出结构。CPU的工作原理就几个字:取指令->译码-->执行指令;为了加快数据处理等会与数据处理单元合作进行相关运算和处理,如果把CPU看做"皇帝",那么寄存器就像是一些"小太监"配合CPU完成相关工作,而我们的CPU所取的指令来自于哪里呢?当然是我们的存储设备了,在上电前程序位于我们的ROM中,上电以后对于部分单片机等等会把ROM中的程序指令加载到RAM中从而提高程序运行速度,这样我们的程序就固化在了ROM中,而我们的RAM掉电便会丢失数据,但是在单片机正常运行过程中用于访问数据会提高运行效率,所以访问比较频繁数据都会放在RAM里面来进行访问,而我们外设也就是我们平时使用的GPIO、UART、ADC等等与外部进行交互的接口了。
从单片机的整体内部结构看来,单片机所有的运作都依赖于我们编写的程序,换一种说法,我们所编写的程序指导着单片机的运作,所以我们不仅仅要了解如何制作该程序,还需要了解程序是如何指导我们单片机运作的。在前面的《嵌入式编程“进阶有道”之C程序(1)》中我为大家详细描述了一个C程序如何生成机器码,在《Jlink调试的那些事》中也告诉大家大部分JTAG接口的芯片是如何把固件烧录到我们的芯片内部FLASH中,大家如果不理解可以回头看一下相对应的文章。
4、C程序数据存储
我们通过C程序编译生成了对应的机器码,一般单片机的程序都分为不同的段,下面我画了一张简图便于大家理解:
详细分析一下每个段:
1).bss段:该段主要是用于存储C语言中未初始化或者初始化为0的全局变量、静态变量等;
2).data段:该段主要保存C语言中对应的已经被初始化的全局变量或者静态变变量等;
3).rodata段:只读数据段,该段主要用于存储对应的常量等;
4).text段:也叫代码段,我们的程序运行逻辑基本上就是存储在该区域,该区域一般都只读不写;
5)其他段:该部分对于一些单片机存储着对应的boot代码、相关算法等等。
看了上面的每个段的分析,有些小伙伴们可能就问了?局部变量属于哪个区域呀?动态内存分配的区域又在哪里?答案是他们分别在栈上和堆上。
“堆”和“栈”
堆:属于动态内存分配区域,我们平时在C语言中调用的malloc所获得的内存就是来自于这块区域,同时我们使用完后需要进行释放,如调用对应的free函数。
栈:该概念类似于一种先进后出的数据结构,在调用函数的时候函数的参数及函数内部的局部变量都会存储在区域,函数结束以后该区域也就自动释放了。
注意:堆栈都属于程序的RAM区域。对于堆的实现,我们大部分都是通过模拟的方式进行管理,同时大家还要注意单片机的系统栈和RTOS的栈的区别。(后续的文章我都会为大家讲解这块的内容)
5、在单片机如何运行程序
好了,有了前面的准备知识,这里我就可以放开讲解单片机的运行了,如果简单说单片机的运行,那么可以说就是CPU的运行,取指令-->译码-->执行指令;那么这里我就以提问的方式为大家更加详细的说明:
1)CPU在哪里取指令?
我们都知道对于PC机,都是从硬盘中加载对应的应用程序到内存中进行执行,那么CPU所取的指令都来自于内存,那么我们的单片机是不是这样的呢?我们大部分玩单片机的小伙伴都知道,单片机的RAM都是在K级别,而我们生成的Bin文件可能比这大得多,那这样看来单片机的取指令还不是从RAM中来的,那就只有对应的flash了,是的,单片机CPU取指令是通过总线直接访问Flash,不过该Flash并不是我们平时使用的串行flash,一般为NorFlash,CPU可以直接访问,且读的速度对于单片机的速度相比RAM相差不大。
不过Flash存在擦写次数的问题,并且向Flash写入需要特殊的命令,所以单片机的程序对于代码段和只读数据段等只读段会继续存放在Flash中(如果你需要加快部分算法,可以加载到RAM中进行);而对于.data段和.bss段都会最终启动加载放到RAM中进行访问。
2)单片机程序从什么地方开始执行?
在之前的连载的《用"库"来学习嵌入式驱动编程》文章中,我详细的解析了stm32的启动文件,在文章中分析了该单片机的启动过程,所以我们要明确一点的是我们的单片机程序并不是从main函数开始执行的,在单片机执行main函数执行进行了一系列的初始化工作,我们可以大体认为一方面是初始化单片机的基础硬件配置,另一方面是为调用我们的C函数构造环境。如下图所示:
3)单片机如何调用函数?
我们都知道函数是我们程序的重要组成部分,那我们的C函数在单片机里面是如何执行的呢?首先我们应该知道PC(程序计数器),该寄存器中存储着下一条CPU需要执行的指令,当顺序执行到函数调用入口处会进行压栈push进行保存现场,比如当前调用函数的返回地址,当前栈指针以及相关寄存器等等。那么当函数即将返回,就会从对应的堆栈中pop出相关数据来进行现场的恢复。在这过程中函数中的局部变量都是在栈中分配的,函数运行完对应的局部变量内存也就自动回收。下面为大家简单的画了个过程示意图方便大家理解:
6、最后小结一下
上面为大家基本上把C语言程序与大多数单片机的运行进行了一一对应,大家如果遇到了一款自己不太熟悉的单片机,可以按照上面的思路进行理解和查阅对应的数据手册文档,只有你充分了解了一款单片机的运行机制才能编写出符合目标单片机上的优秀代码,同时也便于一些可移植框架的移植,比如RTOS,GUI等等。同时对于一些比较复杂的程序bug也能够更加详细的分析和解决。
好了,这里是公众号:"最后一个bug",以上是作者花了两个晚上为大家总结的经验与知识,大家阅读以后有什么不懂的可以加下面微信和我交流,同时大家也可以多网上查找资料来获得更多的知识,感谢大家的关注,我们下期见~!