Skip to content

热重载(Hot Reloading) ​

Auto语言可以实现C语言的热重载,尤其是在MCU里,可以实现增量烧录+热重载。

这样做的好处是大大提高了开发周转速度。

冷重载:修改函数->重新编译整个工程->链接整个ELF->烧录整个ELF->重启MCU->MCU加载ELF->代码从main重新执行 热重载:修改函数->编译单个函数->MCU代码暂停->RAM直接写入新函数写入->代码继续运行

整个周转时间可以从几十秒到分钟级别缩减为一两秒。

热重载基本流程 ​

  1. 把整个工程划分为多个模块。
  2. 模块中每个Symbol(如函数或全局变量)分别管理。
  3. 修改编译链接流程,让每个Symbol最后加载到RAM中时,不再紧密排列,而是各个Symbol之间留下一定的空间。
  4. 某个Symbol修改之后,直接把它的新数据加载到RAM的对应地址中。由于预留了空间,新数据即使占用更多空间,也不会覆盖其他Symbol
  5. 其他代码访问该Symbol时,通过RAM地址访问,此时读到的内容是新的。

基本预设 ​

要实现这个级别的热重载,我们需要几个基本预设:

  1. 数据和代码在RAM中可以不连续排列。
  2. 代码段可以用某种方式修改
  3. 所有的函数和变量(即Symbol)的访问都是通过固定的地址进行
  4. MCU支持直接按地址重新写入某段RAM内存。
  5. 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中。如果这一点可以做到,那么修改其中一段也就是可行的了。

具体哪种方案可行,还需要进一步调研。总的来说,这个思路应该是可行的。

  1. 所有的函数和变量(即Symbol)的访问都是通过固定的地址进行

我们需要确定MCU中代码实际运行时,访问函数、变量时,都是通过链接期间得到的固定地址进行的。 即每个Symbol对应一个固定地址。

如果确实是这样的话,要实现热重载,我们可以采取以下策略:

a. 整个工程划分为多个模块(例如引入Auto语言的module),需要热重载的模块进行特殊处理,为每个Symbol提供额外的预留空间。 b. 开发过程中,修改了某个Symbol后,直接替换它的数据,由于预先预留了空间,它的访问地址并没有变。 c. 此时其他模块再读取该Symbol时,就会得到新的结果。

  1. MCU支持直接按地址重新写入某段RAM内存。

传统的开发流程,修改代码后需要先编译整个工程,再链接成ELF,再加载到Flash,再重启MCU,加载到RAM,再重新运行。

劳德巴赫简化了这个流程,省略了加载到Flash和从Flash加载到RAM的过程,而是直接把新的ELF加载到RAM中。

既然这个思路是可行的,那么我们可以进一步简化,直接把修改的Symbol单独重新加载到RAM中对应的地址即可。

  1. MCU支持暂停CPU指令,等待RAM重写,再恢复执行。

如果不支持暂停运行,那么在写入RAM的过程中,可能会发生数据不一致的情况。

所以最好是类似于Debug的断点流程,程序先暂停在某个断点,CPU暂时不执行下一个代码,然后等Symbol重写到RAM之后再继续执行。

更多思路 ​

如果直接预留空间仍然有问题,我们还可以考虑间接访问的方式。即,

  1. 编译之前,给所有的Symbol都配一个指针,排列在模块的头部。
  2. 所有访问这些Symbol的代码,都替换成一个”间接访问宏“,即先读取模块头部的指针,再跳转到模块内的真正地址。
  3. 开发中某个Symbol修改之后,只需要替换它的RAM数据,此时甚至可以重新分配到其他的地址,然后根据需要更新模块头部的指针内容
  4. 其他代码访问这些Symbol时,通过头部指针找到新的地址,再去读取,就能够得到新的数据或函数了。

这个方案需要修改编译器,或者放在编译器之前,例如Auto语言生成C语言的过程中。 总体来说这个方案是一个更高级更复杂的方案,如果前面两节说的基本方案不好用的时候,再考虑这个方案。

Released under the MIT License.