热重载(Hot Reloading)
Auto语言可以实现C语言的热重载,尤其是在MCU里,可以实现增量烧录+热重载。
这样做的好处是大大提高了开发周转速度。
冷重载:修改函数->重新编译整个工程->链接整个ELF->烧录整个ELF->重启MCU->MCU加载ELF->代码从main重新执行 热重载:修改函数->编译单个函数->MCU代码暂停->RAM直接写入新函数写入->代码继续运行
整个周转时间可以从几十秒到分钟级别缩减为一两秒。
热重载基本流程
- 把整个工程划分为多个模块。
- 模块中每个Symbol(如函数或全局变量)分别管理。
- 修改编译链接流程,让每个Symbol最后加载到RAM中时,不再紧密排列,而是各个Symbol之间留下一定的空间。
- 某个Symbol修改之后,直接把它的新数据加载到RAM的对应地址中。由于预留了空间,新数据即使占用更多空间,也不会覆盖其他Symbol
- 其他代码访问该Symbol时,通过RAM地址访问,此时读到的内容是新的。
基本预设
要实现这个级别的热重载,我们需要几个基本预设:
- 数据和代码在RAM中可以不连续排列。
- 代码段可以用某种方式修改
- 所有的函数和变量(即Symbol)的访问都是通过固定的地址进行
- MCU支持直接按地址重新写入某段RAM内存。
- MCU支持暂停CPU指令,等待RAM重写,再恢复执行。
这几个预设是否所有MCU都支持,还需要进一步调研。
为什么要这几条预设?
1. 数据和代码在RAM中可以分段排列。
正常情况下,程序通过链接器编译成ELF后,代码段和数据段都是紧邻的。
这样的好处是节省了内存空间,但如果要用直接写入的方式更新某个Symbol的数据时,一旦新数据需要更多空间,就不够用了。 所以我们要实现热重载,首先需要给每个Symbol提供足够大的预留空间。
正常的ELF有两个例外情况,此时数据是不连续的:
a. 数据对齐(alignment)会导致不同的Symbol的数据之间有空格数据。 b. 在链接脚本中指定内存段(section)时,实际上是可以指定成不连续的排布的。
既然系统支持这样的不连续空间排布, 我认为理论上完全可以把所有Symbol的数据各自做成一个小段, 分开排列,每个小段都预留足够的空间。
这种”空间换时间“的策略可能导致整个程序占据的RAM地址范围大很多(比如大一倍)。 但考虑到以下几点,我认为是划得来的:
a. 我们只需要在开发过程中使用热重载,开发结束后,发布时仍然使用传统链接烧录模式,不会浪费RAM空间。 b. 实际编程时,往往只需要修改几个模块,大部分都是第三方依赖库。这类静态代码可以按照原有的方式烧录。这样占据空间的代码其实很少。
和数据不同,代码段要添加空格,就需要其他的手段了。
a. 一种方式是在编译期间,在每个函数后添加足够数量的NOP指令,并且确保它们不会被优化掉。这样最后生成的代码段中,每个函数就有足够的预留空间了。当我们修改了函数之后,只要想办法把这个函数对应的汇编代码写入到对应的代码段中,替换掉旧的代码段,就可以实现函数热重载 b. 另一种方式是通过修改链接脚本,给每个函数配置特殊的section,并且给section指定gapfill参数。但这种方法依赖于链接脚本支持,需要具体问题具体分析。
2. 代码段可以重写
正常情况下,代码是烧录在flash里的,除非重新烧录整个代码段,一般不会去动它。 但如我们要实现函数的热重载,就需要能够单独对代码段的一部分(即一个函数的整个代码段)进行重写更新。
有两种实现的可能:
a. 直接修改flash。既然可以把整个flash重新刷写,那么理论上也应当可以按地址刷写一部分。这样的话,我们只要刷写热重载的函数所在的代码段即可。 b. 劳德巴赫可以把整个ELF都写入到RAM里执行,而不需要刷写flash。这说明有机质可以实现把代码段全部放到RAM中。如果这一点可以做到,那么修改其中一段也就是可行的了。
具体哪种方案可行,还需要进一步调研。总的来说,这个思路应该是可行的。
- 所有的函数和变量(即Symbol)的访问都是通过固定的地址进行
我们需要确定MCU中代码实际运行时,访问函数、变量时,都是通过链接期间得到的固定地址进行的。 即每个Symbol对应一个固定地址。
如果确实是这样的话,要实现热重载,我们可以采取以下策略:
a. 整个工程划分为多个模块(例如引入Auto语言的module),需要热重载的模块进行特殊处理,为每个Symbol提供额外的预留空间。 b. 开发过程中,修改了某个Symbol后,直接替换它的数据,由于预先预留了空间,它的访问地址并没有变。 c. 此时其他模块再读取该Symbol时,就会得到新的结果。
- MCU支持直接按地址重新写入某段RAM内存。
传统的开发流程,修改代码后需要先编译整个工程,再链接成ELF,再加载到Flash,再重启MCU,加载到RAM,再重新运行。
劳德巴赫简化了这个流程,省略了加载到Flash和从Flash加载到RAM的过程,而是直接把新的ELF加载到RAM中。
既然这个思路是可行的,那么我们可以进一步简化,直接把修改的Symbol单独重新加载到RAM中对应的地址即可。
- MCU支持暂停CPU指令,等待RAM重写,再恢复执行。
如果不支持暂停运行,那么在写入RAM的过程中,可能会发生数据不一致的情况。
所以最好是类似于Debug的断点流程,程序先暂停在某个断点,CPU暂时不执行下一个代码,然后等Symbol重写到RAM之后再继续执行。
更多思路
如果直接预留空间仍然有问题,我们还可以考虑间接访问的方式。即,
- 编译之前,给所有的Symbol都配一个指针,排列在模块的头部。
- 所有访问这些Symbol的代码,都替换成一个”间接访问宏“,即先读取模块头部的指针,再跳转到模块内的真正地址。
- 开发中某个Symbol修改之后,只需要替换它的RAM数据,此时甚至可以重新分配到其他的地址,然后根据需要更新模块头部的指针内容
- 其他代码访问这些Symbol时,通过头部指针找到新的地址,再去读取,就能够得到新的数据或函数了。
这个方案需要修改编译器,或者放在编译器之前,例如Auto语言生成C语言的过程中。 总体来说这个方案是一个更高级更复杂的方案,如果前面两节说的基本方案不好用的时候,再考虑这个方案。