在嵌入式开发里面,有一些变量的存放位置是不能完全交给链接器去自动管理的,比如用来做Bootloader共享的区域、存放标定数据的区域、核间通信用的内存、掉电之后数据还要保留的区域,以及跟外设寄存器映射有关的那些地址段。那么在使用TASKING编译器的时候,怎么把变量放到一个指定的地址上去呢?
一、TASKING变量怎么放到指定地址
要想让变量落在固定的地址上,头一件事情是要把芯片自身的内存划分搞清楚,RAM区、Flash区、保留的地址段、堆栈要用的空间,还有共享出来的区域,这些块之间可不能有重叠的部分,要不然很可能编译阶段没报错,真正跑起来的时候却出些莫名其妙的故障。
1、先核对目标地址范围
在动手定地址之前,应该把芯片的数据手册和工程里现有的LSL文件都翻出来看一看,先确认一下想用的那块地址,到底是在可用的RAM范围里,还是落在Flash里面,如果是多个内核都要碰的数据,还得搞清楚这片区域是不是允许多核一起访问。别图省事直接去拷贝别的项目里用过的地址,毕竟芯片的型号、硬件的版本,还有当前链接布局,跟以前很可能已经不一样了。
2、少量变量使用绝对地址限定
当需要固定位置的变量就只有一两个状态量的时候,完全可以用TASKING自带的那种绝对地址限定的办法,就是在定义变量的时候,把它的数据类型、volatile属性、所属的内存空间限定词,还有那个具体的地址,一块儿都给写出来。之所以要经常加上volatile,是因为它很适合用来修饰硬件寄存器、多核之间共享的状态,以及有可能被中断服务程序修改的数据,这样编译器就不会把对这些变量的必要访问给优化掉了。
3、多组变量使用自定义段
假如需要固定的数据是比较大的共享结构体、标定用的表格,或者一块连续的数据,这个时候就不建议再一个一个去单独指定地址了,更合适的办法是把它们归拢起来,放进一个自定义的段里面,然后在LSL文件里面对应地建一个group,规定好这个段在运行时的起始地址和长度,之后如果要往里面添加新的字段,链接器就会按照段内的顺序自动往下排,维护起来也清晰很多。
4、修改后执行完整构建
只要动过了LSL文件、段的名称或者变量的属性,就一定要先做一次清理,再完整地重新构建一遍,千万别只做一个增量编译。因为增量编译的时候,那些旧的目标文件很可能还保留着原先的段信息,这样等到Map文件生成出来,里面的地址就会跟现在源码的意图对不上号了。
二、TASKING变量定位后地址不对该怎么修
当发现给变量指定的地址跑偏了以后,先别急着去修改那个地址数值本身,因为更多的时候,毛病是出在段的名字没对应上、地址空间选得不对、Flash里面还存着一份初始化的副本,或者是变量的对齐要求没有满足这几类原因上。
1、先在Map文件里搜索变量名
构建一结束,就可以去打开Map文件,在里面搜索那个变量的名字,看看它最终被分配的实际地址是哪里,再顺着这个地址,找出它归属于哪个section、哪个group,以及处在哪一个memory区里。Map文件里面的地址才是最终生效的结果,而源码里写的那些定位描述,反映的仅仅是我们的设计意图。
2、检查section名称是否匹配
要注意检查LSL文件里面设定的选择规则,是不是跟编译器实际生成的section名称对应得起来。有时候我们对变量重命名、移动了文件所在的目录,或者把模块重新拆分过之后,之前那条老规则就没法再命中新的段了;匹配一失败,链接器就会把这个变量扔回默认的数据区里去,于是地址自然就不再是我们以为的那个地方。
3、检查地址空间是否选错
在多核项目里面,往往会同时存在好几种地址空间,比如线性地址空间、单个内核私有的地址空间,还有跨核共享的地址空间。如果把一个本该共享的变量,给放到了单核自己本地的空间里去,那别的核再去读它的时候就会出问题;反过来,把仅限于单核用的变量误放进了共享区,也会白白占掉那块本就紧张的资源。对于核间通信要用到的区域,还得额外关注一下它的访问范围和缓存是怎么配的。
4、区分运行地址和初始化副本
另外,带有初始值的RAM变量,一般都会在Flash里面存放一份它的初始化数据,等到系统启动的时候,再由代码把这份数据搬进RAM里去。这样一来,在Map文件里就可能会同时看到两个地址,但我们需要明白,程序真正跑起来以后访问的是那个RAM地址,不要误以为Flash里的那个副本就表示定位失败了。
三、TASKING变量定位异常还要检查哪些内容
即便变量最后落到了我们预期的地址范围里,也还不能算完,接着还要去看一下访问它的方式对不对,以及实际运行起来的表现如何。很多情况下,问题并不是地址本身搞错了,而是声明的地方、对齐的要求,或者启动阶段的初始化动作没有同步好。
1、检查声明是否一致
变量的定义处、头文件里用extern声明的位置,还有实际调用它的地方,这三处不但数据类型要统一,连带着内存空间的限定符也必须一致。比方说,定义的时候指明了是要用远地址的方式去访问,可声明的时候却漏掉了那个限定符,就很有可能导致编译器生成出错误的访存指令,最终表现出来的,就是读写出去的数据全乱了。
2、检查对齐要求
如果是结构体、数组,或者原子变量,它们对对齐往往有比普通变量更高的要求。如果我们设定的目标地址没法满足这种对齐,链接器要不就自己往后挪一挪,要不就直接报一个冲突出来。对于那些多核共享的结构体,也还得确认一下里面字段的排列顺序,以防不同的模块用的是不同版本的定义,最后解得乱七八糟。
3、检查启动初始化
掉电之后还要保留数据的区域、不需要初始化的区域,以及普通的RAM区,这三者在启动阶段的处理手法是完全不一样的。凡是我们希望上电后仍然保持原值的区域,就绝对不能在启动过程中被清成零。假如变量的地址看着是正确的,可每次复位以后,里面的值都会变回初始的默认值,那就得去查一查启动代码,还有copy table的那些配置了。
4、保留地址分配记录
比较推荐的做法是,把每一个定位过的变量名称、所放的那个section叫什么、目标地址和长度、它归哪个内核管、需不需要初始化,以及最后一次修改的时间,都随手记下来。等到后面需要更换芯片、重新调整LSL文件,或者扩展共享区域的时候,拿着这份记录一对照,排查起来就能快很多,不至于在同一个坑里摔倒两次。
总的说来,在TASKING里面要给变量分配固定地址,如果数量很少,直接用绝对地址限定的办法就行,而变量一多,更适合的途径还是把它们集中放进自定义段,再交给LSL文件去统一安排。万一发现变量的最终地址跟设想的不一样,最优先的动作是去翻Map文件,紧接着再依次排查section的匹配情况、地址空间有没有选错、Flash里是不是还存在初始化副本、对齐要求有没有满足,以及extern声明和定义是不是配套。给变量定位这件事,光靠编译通过是远远不够的,最后一定要把Map文件和实际的运行结果放在一起对照着看,才算真的确认好了。