技巧和窍门–从Bootloader干净地跳转到应用程序代码

引导加载程序几乎包含在每个嵌入式系统中,并提供了一种无需访问编程端口即可现场更新应用程序代码的好方法。与引导加载程序一样重要,开发人员经常会试图从引导加载程序跳入其应用程序代码。跳转需要保持整洁,但是有几个因素会引起问题,例如:

  • 写一次寄存器(例如看门狗寄存器)
  • 时钟设定
  • 堆栈和程序指针
  • 周边设定

开发人员可以采用两种不同的方法从引导加载程序完全过渡到应用程序代码。第一种方法涉及开发人员仔细匹配其应用程序代码和引导加载程序设置。例如,开发人员将匹配看门狗寄存器,时钟设置,甚至可能匹配诸如UART之类的外设。引导加载程序会将所有这些组件初始化为已知的系统状态,并简单地将程序执行交给应用程序代码。问题在于,应用程序代码中的任何更改也需要在引导加载程序中进行更改。

处理引导加载程序和应用程序代码之间跳转的第二种更简洁的方法是遵循一个简单的过程,该过程将引导加载程序触及的所有设置恢复为原始复位状态。这将包括诸如GPIO,时钟之类的外设,甚至包括对堆栈和程序计数器的修改。当开发人员执行此操作时,应用程序代码完全不会意识到内存中还有另一个应用程序正在执行。唯一需要匹配的设置是只能写入一次的寄存器!

准备从引导加载程序跳转到应用程序代码的过程非常简单,可以在下面找到该过程:

  • 验证是否已对应用程序复位向量进行了编程
  • 验证应用程序校验和和安全凭证
  • 取消初始化外设并将它们置于复位状态
  • 将向量表寄存器设置为应用程序重置向量(ARM)
  • 将堆栈指针寄存器设置为应用程序的起始地址
  • 将程序计数器设置为复位向量(跳转至应用程序)
  • 交叉手指!

让我们简要地讨论每个步骤。在引导加载程序执行任何操作之前,它需要验证应用程序的完整性。第一步是验证复位向量是否已编程,并且不是0xFFFFFFFF或0x00000000。通常,擦除的闪存将设置其所有位,因此,如果我们看到该状态,则引导加载程序知道出了点问题,应保留在引导加载程序中,这是已知且安全的状态。请记住,我们假设两者之间的任何编程值都是正确的,这可能是一个错误的假设,但对于今天来说已经足够了。

接下来,引导加载程序应在应用程序空间上执行校验和计算,以确保应用程序有效。再一次,如果在更新过程中出了点问题,那么可能会有一个局部程序带有复位向量,并且如果没有校验和就无法知道。最重要的是,引导加载程序还应检查所有数字签名或安全措施,以确保不仅该应用程序完整无缺,而且其来源也正确,而且不是恶意软件或经过修改的程序。

引导加载程序确认应用程序完整且来源正确后,就该回到原始状态了。触摸过的任何一次不写的外设都应恢复到复位状态。最好的方法是查看所使用的驱动程序,它们初始化和访问的寄存器,然后在数据表中查找这些寄存器。每个数据表均显示上电复位寄存器的值。对于每个已修改的寄存器,都可以将它们重置为这些状态。通常,我将避免更改时钟寄存器,看门狗和一次写入寄存器。我只是在应用程序和引导程序之间匹配这些状态。

此时,微控制器返回到复位状态,并准备运行应用程序代码。在这样做之前,必须更新向量表寄存器以指向应用程序而不是引导加载程序的向量表位置。中断向量表可以位于任何地方,因此引导加载程序和应用程序链接器文件之间确实需要协调。例如,开发人员将编写如下一行代码,其中PROGRAM_FLASH_BASE是应用程序的第一个向量表位置:

SCB_VTOR =(uint32_t)PROGRAM_FLASH_BASE;

完成此操作后,就该跳到该应用程序了。开发人员可以通过几种不同的方法从引导加载程序跳转到应用程序。一种方法是简单地取消引用应用程序的重置向量位置。这样做的问题是堆栈指针可能不在正确的位置,并且可能导致奇怪的行为。理想情况下,开发人员将设置堆栈指针,然后设置程序计数器。如何实现将因一个微控制器而异。几乎总是需要使用内联汇编代码来完成此操作(我主张有一次编写汇编代码是可以的)。对于ARM微控制器,可以在下面看到示例代码片段:

void Flash_StartApplication(uint32_t startAddress)

{

asm(” ldr SP,[r0,#0]”);

asm(“ ldr PC,[r0,#4]”);

}

确切的代码将根据使用的编译器而略有不同。内联汇编不是C标准,因此每个编译器供应商都以不同的方式实现了它,或者在某些情况下根本没有实现!让我们检查一下这是做什么的。

为了使汇编语言代码最少,将汇编语言代码包装在C函数中至关重要。原因是当将startAddress传递到Flash_StartApplication函数时,它将自动存储在寄存器r0中。有了这些知识,就没有理由添加额外的汇编语言指令来将所需的起始地址加载到寄存器中了。 (是的,它为我们节省了一条汇编指令,但是以这种方式执行也更加可维护和灵活)。然后,第一个汇编指令是将偏移量为0的寄存器r0中存储的值(#0)复制到堆栈指针(SP)寄存器中。然后,第二条指令告诉处理器以额外的偏移量4(#4)来获取r0中存储的值,并将其复制到程序计数器(PC)寄存器中。偏移量4实际上是将r0中存储的值加4。然后执行的下一条指令将是应用程序代码的复位向量。我们刚刚成功进入了应用程序!

这里的所有都是它的!完成此过程后,您现在可以轻松地从引导加载程序跳转到应用程序代码,并确保其行为符合您的期望。

 

如果您有兴趣了解有关引导加载程序的更多信息,请阅读Jacob’s 引导加载程序设计技术白皮书。如果您对动手启动引导程序感兴趣,请联系 jacob@beningo.com 了解有关其Bootloader设计技术课程的更多信息。

4 thoughts 上 “技巧和窍门–从Bootloader干净地跳转到应用程序代码”

  1. 也许更新的处理器的文档会更好,但是很多年前,我对处理器寄存器版本之间进行更改的处理器寄存器的重置状态有些不满。我们没有预料到的东西,实际上我们不得不致电制造商进行验证。我想我们可以重写代码以保存寄存器的重置值,但是’RAM中有足够的可用空间,我们没有很好的方法来更新我们现有的所有现有客户及其系统–这是在基于闪存的应用程序和引导加载程序之前的;一切都在一个EPROM中。

    I’ve还与一个系统配合使用,在该系统上,验证Flash应用程序的校验和花费的时间比默认的看门狗超时时间长。那不是’在特定情况下是一个问题(看门狗可能是可取的,但根据系统设计文档不是必需的),但这是一次写入的寄存器,引导加载程序无法将其留给应用程序进行编程。

    1. 即使在今天,它仍然仍然可能非常棘手!在这篇文章中,我提到了恢复到复位状态,但是根据我的大多数经验,引导加载程序和应用程序代码必须协调设置。即,引导加载程序会设置所有时钟,gpio和一些基本外设,并且应用程序代码会在启动时假定引导加载程序已运行!肯定有不止一种方法可以使跳跃成功。

      感谢您分享经验!

    1. 引导加载程序通常就是这样工作的。 MCU复位向量指向引导加载程序,然后引导加载程序决定是要完全加载到内存中还是应该跳转到应用程序代码。

发表评论

您的电子邮件地址不会被公开。 必需的地方已做标记 *

该网站使用Akismet减少垃圾邮件。 了解如何处理您的评论数据.