我们在嵌入式环境下写的代码通常要经以下四个步骤才能转换成可烧写文件
(1)预处理,展开头文件/宏替换/去掉注释/条件编译
(2)编译,检查语法,生成汇编 。具体来说是以标准的文本格式确切描述一条机器语言指令,即将高级语言翻译为低级语言。
(3)汇编,汇编代码转换机器码,即生成二进制格式的机器语言指令
(4)链接,按照一定的顺序打包合成烧录文件(hex, bin, s19)
烧录文件中最终包含的有CPU指令,中断向量表,cont常量,有初始值的全局变量,以及RAM相关的地址长度信息等。
程序下载到Flash后上电,CPU在执行Main函数之前会对RAM会有三个操作,(1)将没有初始值的内存区域清0,(2)分配堆和栈(3)将带初始值的全局变量搬运到内存镜像中。
ps,关于堆栈
堆是需要用软件手动分配和释放的一段RAM空间,传统ECU没见用过,一般不给分配。
栈用来保护中断现场和存储函数调用期间的局部变量,过小会出现变量值乱跳的诡异现象。这里有个经验之谈,不要定义过大的局部变量,你可能觉得局部变量用的时候才分配,函数运行完就释放出来了,实际上这起不到节省内存的作用,并且函数如果有多级调用,随着调用层级越深,需要的栈也越大,很容易造成栈溢出。如果必须用到大数组,就定义成全局变量在MCU初始化阶段分配固定内存
编译器编译程序的同时生成map表,为所有参与编译的元素分配地址,这个地址就是RAM和Flash中的实际地址,其中Flash中的元素信息都会存储到最终烧录文件中。一般来说编译器给的地址都是顺序排列的,所以最后生成的烧录文件也是紧密排列的一块,可能只有文件开头一块是有用的,其他地方全是默认值x000或0xFF,这为烧录文件的裁剪创造了便利条件。
MCU的Flash中允许存在多个程序,独立程序在Flash之间不需要特定的间隔,只要保证程序踩踏、不跑飞,相互之间就没有任何干扰,最典型的应用就是BOOT程序和APP程序。为实现FOTA自刷写也可能需要两个APP程序,由BOOT判定哪个最新进而在上电以后进入到对应APP程序中执行控制器功能。
函数指针是嵌入式编程比较高阶的玩法,简单理解函数名本身就是一个指针,指向Flash中的某个地址,而从这个地址开始的一块连续的指令就是你编写的代码了,带上参数调用就是在执行这段代码了。
(关于函数指针可以学习这篇文章,函数名与函数指针-CSDN)
这样程序在RAM中运行就比较好理解了。汽车行业一般禁止烧录文件中包含Flash驱动,以避免程序跑飞导致固件异常,比如程序自己把自己从Flash上擦掉。由于单片机哈弗架构的特点,Flash放code,RAM放data,所以RAM里是没有任何指令(或者说函数)的,但是我们可以将程序从Flash复制到RAM中,或者在上电程序运行后将包含完整函数信息的数据下载到RAM,当然这段数据可以是单独的代码经编译生成,或者从现有代码中裁剪的。之后找到函数的地址,强制转换成函数指针,在主程序调用即可实现函数功能,由于函数调用后程序又会返回调用位置,所以Flash中的主函数会继续向下执行,而不用担心程序跑飞。
以上所指的Flash一般都是单片机内部的NOR Flash,一些外部存储设备比如EEPROM和NAND Flash,由于只能通过SPI或者I2C总线访问,而不提供完整的寻址与数据总线,CPU无法直接访问,所以程序在其上只能存储而不能运行。
(为什么程序不能再nandflash上执行 为什么程序不能直接在nandflash上执行?-jeffasdasd-ChinaUnix博客)
----------------------------------
汽车行业有些公司会有一级boot和二级boot的叫法,
一级boot(Primary BootLoader)其实就是BootLoader,二级boot(Secondary BootLoader)主要为flash驱动程序,外加一些负责刷写的诊断服务程序,需要通过一级boot下载到RAM。
这样分工的好处:
1,防止应用程序被意外改写
2,提高网络安全性,每次从诊断仪刷写二级boot时都可以通过安全机制确保诊断仪的合法性
3,SBL可以反向刷PBL,提高整体灵活性