翻译:Predictable Programming on a Precision Timed Architecture

1.简介

在现代处理器上开发硬实时软件变得非常困难,因为它们的复杂性使得预测执行速度几乎不可能。多级管道之间危险,绕过之间复杂的交互相关的指令,超标量体系结构无序的指令取出单元,几乎重新编译程序动态,三级存储器层次结构复杂的缓存替换策略,它是很难准确预测到底有多少周期还需要执行一个简单的指令序列[10],更别说代码与条件分支。现代处理器是真正混乱的[6]。

不幸的是,最坏情况下的执行时间限制是构建所有实时软件工程的基础。当然,人们总是可以保守地估计过高,但是这已经变得不现实了,因为达到一级缓存和主存之间的差异可能是1000个周期。

我们相信实时嵌入式软件的解决方案是对处理器架构的重新思考。正如在其他[9]中所讨论的,现在是时候考虑那些提供与功能一样可预测的定时的架构了。在本文中,我们提出了这样一个精确定时(PRET)架构的具体示例:一个基于SPARC指令集架构(ISA)的多线程处理器,它提供可预测的定时以及可预测的功能和性能。下面,我们使用SystemC[23]和一个在其上运行的应用程序来演示软件如何利用PRET体系结构,从而给出一个周期精确级的PRET体系结构模型。在未来,我们也计划一个FPGA实现。

2.PRET的哲学

PRET[9]背后的哲学是,现代处理器体系结构已经陷入了一个不可预测的极端:它只关注平均情况的性能。它需要重新思考,以有效的实时嵌入式系统。帕特森和迪泽尔的类似观察引发了RISC革命。同样,我们必须重新思考实时嵌入式处理器架构。

现代处理器[13]的复杂性使得计算甚至限定一系列操作的执行时间的任务变得非常困难。虽然这对于优化计算来说并不重要,但对于硬实时系统来说却是个灾难。

PRET的哲学是,时间特征应该像功能一样可以预测。就像处理器上的算术始终是一致的、可预测的和文档化的一样,我们希望它的速度也同样是一致的、可预测的和文档化的。尽管将时钟拨回8位微处理器时代是实现这一目标的一个明显方法,但PRET的目标是:如何使许多高集成级别的现代体系结构具有可预测性。

因此,PRET支持软件管理的暂存存储器[3],线程交错的管道,没有旁路[20,11],ISA级别的显式定时控制[15],具有全局时间同步[16]的时间触发通信[17],以及具有显式定时[14]的高级语言。在本文中,我们提出了一个包含许多租户的体系结构,并演示了如何对其进行编程。特别是,我们将重点放在将计时指令集成到线程交错的管道和可预测的内存系统上。然后,我们将展示如何编写这样一个可预测的体系结构。

3.相关工作

Agarwal等人的原始处理器[29]分享了PRET哲学的某些元素。它也使用了一个软件管理的暂存器,而不是指令缓存[22],并且肯定考虑了通信延迟(“raw”这个名称提醒人们,它的ISA暴露于线路延迟)。然而,它支持带有旁路的多个单线程管道,一个相当传统的数据缓存,并且几乎完全专注于性能,与通常一样,这是以牺牲可预测性为代价的。

原始架构被设计成一个由单线程处理器组成的网格,通过一个新的互连网络连接。虽然我们为高端PRET处理器设想了类似的配置,但我们在这里提供的实现不考虑内核间通信。未来我们可能会采用类似raw的通信网络。

Java优化的处理器[27]支持精确的最坏情况执行时间界限,但不支持控制执行时间。SPEAR[8]处理器禁止条件分支,我们发现这过于严格。REMIC[26]和KIEL[21]在PRET意义上是可预测的,但它们只允许Esterel[5]作为入口语言。再一次,我们发现这个限制太多了;我们工作的中心目标是提供一个C开发环境。

Ip和Edwards[15]首先在一个非常简单的非流水线处理器中实现了deadline指令,该处理器不支持C编译器。这个最后期限指令允许可编程方法指定程序代码段的下限执行时间。我们的工作通过借用deadline指令语义并将其集成到一个线程交错的管道中,从而将他们的工作扩展到一个新的架构。我们引入了一种重放机制来在不阻塞整个管道的情况下阻塞特定的线程。在最后期限指令中再次使用这种重放机制。

Giotto语言[14]是一种在软件级别指定系统定时的新方法。但是,它依赖于通常的RTOS基础结构,该基础结构假定已知最坏情况下的执行时间来建立可调度性[7]。我们的PRET处理器将是Giotto编程环境的理想目标。

线程交错的管道至少可以追溯到1987年的[20],甚至更早。线程交错减少了处理器的面积、功耗和复杂性[11,18],但更重要的是,它促进了管道中指令的可预测执行。在线程交错的管道中对主内存的访问通常是管道的[11,18],但是现代的大型内存通常不是管道的,因此我们的方法为每个线程提供一个窗口,在这个窗口中线程可以访问主内存。这提供了对内存的可预测访问和线程之间的互斥。我们称之为记忆轮。

Mueller等人的[2]虚拟简单体系结构的目标是实现不可预测的处理器的硬实时操作。它们在一个快速、不可预测的处理器上运行实时任务,同时在一个较慢、更可预测的处理器上运行实时任务,如果速度慢的处理器超过了速度快的处理器,它们就会切换。优点是更快的处理器将有时间运行额外的、非时间关键的任务。相比之下,我们的PRET方法保证了详细的时间安排,而不仅仅是任务完成时间,而是允许使用时间安排进行同步

暂存式存储器长期以来一直被提议用于嵌入式系统,因为它比缓存[4]消耗更少的能量,但在这里,我们采用暂存式存储器纯粹是因为它们能够更好地预测。由于暂存式存储器是软件管理的,因此内存分配方案的问题变得很重要。我们未来的工作是在当前PRET架构的基础上开发内存分配方案。

翻译:Predictable Programming on a Precision Timed Architecture

                                                                           Figure 1:PRET架构框图

4.我们的架构

在本节中,我们将介绍PRET处理器、其内存系统和ISA扩展的设计,以支持截止日期计数器。我们使用循环精确的SystemC[23]模型对PRET体系结构(如图1所示的框图)进行了原型化,该模型执行用C编写的程序,并使用GNU C编译器进行编译。我们的模拟器实现了一个扩展的SPARC v8 ISA[28]。

图1中的PRET处理器组件实现了一个六阶段的线程交错管道,其中每个阶段执行一个单独的硬件线程,以避免旁路的需要[20,11]。每个硬件线程都有自己的寄存器文件、本地片内存储器和指定的片外存储器区域。线程控制器组件是一个简单的循环线程调度器——在任何时候,每个线程只占用一个管道阶段。为了可预见地处理管道的停顿,我们引入了一种重放机制,它简单地重复相同的指令,直到操作完成。因此,一个线程的停顿不会影响其他线程。线程的循环执行避免了内存一致性问题

内存层次遵循哈佛架构[13],它由用于指令和数据的独立的快速片上暂存存储器(SPM)和一个大的片外主存组成。它们连接到一个直接内存访问(DMA)控制器,该控制器负责在主存和SPMs之间移动数据。目前,我们假设程序代码完全适合SPMs,因为我们还没有开发一个自动程序内存管理方案[3]。DMA组件目前仅在打开电源时将每个线程的程序代码和数据从主内存传输到各自的spm。如前所述,内存读写使用重放机制。每个线程都有自己的访问窗口,由内存轮管理。如果一个线程错过了它的窗口,它就会阻塞,直到它到达窗口的开始。

我们将一个deadline指令[15]合并到基于sparc的ISA中。这样的指令阻塞,直到软件可编程死线计数器达到零。每个计数器由一个线程本地锁相环控制,该锁相环可以通过编程以系统时钟频率的合理倍数计数。下面,我们将详细描述我们的实现。介绍了内存系统、内存轮、内存映射、线程交错流水线、时序指令扩展和工具链流程。

4.1.内存系统

众所周知,缓存是造成[30]定时不确定性的主要原因,但是简单地删除它们是不可接受的。相反,我们使用暂存器来弥补处理器内存的缺口。暂存存储器是硬实时嵌入式处理器中常用的软件管理的片上存储器。spm由软件通过DMA传输进行管理,从而避免了不可预测性(通常是微妙的)硬件替换策略。每个线程本地SPM为64 KB,周期延迟为1。我们使用一个16 MB的主内存,我们假设它的实际延迟是50纳秒。这意味着在大约250 MHz的PRET处理器上运行12.5个周期,我们将其四舍五入为13个周期。

如果所有线程都能够在任意时间访问片外主存,则一个线程的片外内存访问时间可能取决于另一个线程的内存访问模式。这种类型的行为引入了时间的不可预测性,是不受欢迎的。为了确保可预测的时间,所有对主存的读写操作,例如共享数据,都必须通过我们的内存轮来完成。与视差螺旋桨芯片[24]中的“中心”一样,这个轮子也有一个固定的轮询调度来决定哪个线程可以访问内存。根据固定的调度和线程请求访问主内存的时间,访问可能需要13到90个周期。需要注意的是,确切的循环数只取决于发出请求的周期,而不取决于其他线程或内存访问模式的行为。

我们使用管道部分中描述的重播机制,而不是在多周期内存访问期间阻塞整个管道。一个简单的内存管理单元在spm、主内存和基于地址的内存映射I/O之间进行选择。

4.1.1.内存轮

存储器轮以确定性和可预测的方式控制对芯片外主存储器的访问。每个线程被分配一个13周期的窗口,在这个窗口中它必须完成它的内存访问;轮调度每78个周期重复一次。如果一个线程在它的窗口的第一个周期开始访问,那么访问需要13个周期。否则,线程阻塞,直到它的窗口重新出现,这可能需要77个周期。错过窗口的第一个周期后成功访问将导致77 + 13 = 90个周期。虽然这种机制会导致线程阻塞,但是没有线程间的交互,窗口的行为是可预测的。

4.1.2.内存映射

翻译:Predictable Programming on a Precision Timed Architecture

                                                                                   Figure 2:内存图

  • Boot code:这个地址空间包含每个线程在启动时使用的引导代码,用于初始化所有寄存器并跳转到线程程序的开始。
  • Shared data (8 MB):这个地址范围在我们的方案中是为多线程共享的数据保留的。
  • Thread-local instructions and data (1 MB per thread):第一个线程从地址0x40000000开始。512KB用于指令,512KB用于数据。每个后续的线程都从其前一个线程的偏移量0x100000字节开始。这意味着第一个线程包含地址从0x40000000- 0x400FFFFF,第二个0x40100000- 0x401FFFFF,以此类推。这种方法允许任何内存位置中的每个数据字节都与一个地址唯一关联。
  • Memory-mapped I/O:我们保留0x80000000以上的地址用于特殊目的,比如内存映射。特别是,我们按照映射地址0x80000200到达UART,这与我们的工具链的GCC为printf或put生成的内容一致。

图2显示了系统内存映射(地址是32位)。系统中的每一块内存都有一个惟一的全局地址(主内存和SPMs),但是每个线程只能访问整个内存映射的一部分。地址0x3F800000 - 0x405FFFFF (14 MB)是主内存,每个线程都可以看到它。这种布局考虑到了未来的扩展:共享数据空间可以向下扩展;线程本地内存可以向上扩展。我们当前的实现有512字节的引导代码、8 MB的共享数据和1 MB的SPMs总量。外围设备从0x80000000开始;我们在0x80000200处放置了一个UART。

4.2.管道

我们的架构实现了一个支持六个独立硬件线程的六阶段线程交错管道。每个线程都有自己的一组寄存器。通过设计,我们为每个管道阶段实现了一个线程,以消除同一线程中指令之间的依赖性。因此,它不需要任何危险数据检测逻辑或旁路。

线程控制器根据一个roundrobin策略调度线程,并将线程标识符传递给管道以索引特定于线程的寄存器和SPMs。在获取阶段,从SPM获取当前的程序计数器(PC)。由于线程交错,我们不需要为当前线程更新下一个PC,所以直到当前线程离开管道前,它不会被获取。因为当PC被获取时,我们确信它总是正确的,这消除了任何投机执行的需要;译码阶段对指令进行译码并设置相应的流水线控制。regacc阶段从寄存器文件中读取源操作数,并在操作数的立即数据和注册数据之间进行选择。execute阶段执行ALU操作。mem阶段访问SPM或主内存。except阶段捕获异常,如果没有异常,则将任何结果写入寄存器文件。在根据stalls和分支,确定下一个值后,我们也在这个阶段更新PC。

即使我们不需要数据危害检测,我们也必须考虑结构危害。我们使用重放机制来处理它们,我们将在下面详细说明。

4.2.1.Stalls和重播机制

我们的管道只更新except阶段中的寄存器。此阶段仅在未发生异常的情况下将数据写入寄存器文件。这为每个线程提供了一个提交点。我们在这个提交点决定是否需要重放一个指令。

通过重放指令,我们可以确保每个线程在前进之前每个循环只停留在一个管道阶段,甚至对于多循环操作(如内存访问)也是如此。例如,在内存停止时,停止的指令每六个周期重复播放一次,直到从内存中取出数据,并且线程可以继续。因此,重播提供了一种可预测的方法来独立地阻塞线程,而不是阻塞整个管道。异常阶段检查一条指令的重放位,只有当重放位为空时才提交。否则,获取阶段检查并确定下一个要获取的PC。因此,该线程的下一个迭代将重新运行相同的指令,直到多周期操作完成。

4.3.时间说明

为了给软件提供精确的定时控制,我们添加了一个“deadline”指令,允许程序员设置和访问周期精确的计时器[15]。这条指令为一段代码的执行时间设置了一个下限期限。我们提供了两种类型的截止时间计时器,可以通过这个指令访问:一组根据主时钟计数,另一组根据可编程锁相环(PLL)产生的时钟计数。这些计时器显示为只能通过deadline指令访问的附加寄存器(图1)。

4.3.1.语法

我们采用了来自Ip和Edwards[15]的deadline指令的语法。有一个直接形式:翻译:Predictable Programming on a Precision Timed Architecture和一个注册形式:翻译:Predictable Programming on a Precision Timed Architecture。每个线程有12个deadline寄存器翻译:Predictable Programming on a Precision Timed Architecture,其中8个寄存器计算指令周期,其他4个寄存器由锁相环驱动;和32个全局寄存器翻译:Predictable Programming on a Precision Timed Architecture。v是一个13位的即时值。

4.3.2.语义

最后期限指令只能对代码段的执行时间施加一个下界;使用重播,当写的最后期限寄存器不是0时,最后期限指令阻塞线程。

与Ip和Edwards[15]不同的是,我们的处理器是流水线的,所以我们每六个时钟周期减少一次注册截止时间。而锁相环则以锁相环所设定的速率注册。当一个deadline指令试图设置一个deadline寄存器时,它会阻塞,直到deadline寄存器为零,这时它会重新加载寄存器并将控制权交给下一条指令。因此,较早的deadline指令可以设置下一个deadline指令终止之前的最小时间间隔。

目前,如果最后期限已过(即,寄存器在deadline指令到达之前为零),我们什么也不做:deadline指令只是立即加载新值并继续。稍后,我们计划允许架构在错过最后期限时抛出异常。

4.3.3.实现

为了实现deadline指令,我们选择了一个未使用的操作码,并遵循了通常的SPARC指令编码格式,该格式允许我们同时包含指令的寄存器和直接形式。图5显示了两个具体的编码。

支持deadline指令需要一些额外的管道控制逻辑和deadline寄存器。

翻译:Predictable Programming on a Precision Timed Architecture

 

在我们的管道中,我们在寄存器访问阶段检查deadline寄存器,并使用重播机制阻塞deadline指令,直到它的deadline寄存器为零。

4.4.编译流

我们采用了开源LEON3实现[12]使用的SPARC工具链。图3显示了我们的编译流程。

翻译:Predictable Programming on a Precision Timed Architecture

我们要求用户在单独的文件(例如thread0.c)中为每个硬件线程提供main()函数。通过将-Ttext和-Tdata选项传递给链接器,我们在内存映射指定的位置编译它们。例如,thread0.c从地址0x40000000开始,而thread1.c从0x40010000开始。我们将生成的目标文件与设置代码合并,并将其转换为Motorola S-record (SREC)格式。然后,我们的模拟器使用SREC文件的内容初始化内存。我们计划使用相同的SREC文件作为PRET处理器的FPGA实现的输入。

5.基本PRET编程

为了说明如何将PRET计时精度用于同步,我们提供了一个简单的生产者/消费者示例,其中有一个显示传输数据的观察者。这是一个典型的互斥问题,因为我们必须处理共享资源的问题。但是,与传统方法不同,在我们的方法中,线程必须等待锁的时间是确定的,因为它不依赖于其他线程访问锁的行为。

我们的方法使用截止日期计数器和指令定时的精确知识来同步对用于通信的共享变量的访问。我们采用最坏情况执行时间(WCET)分析工具来分析从C程序生成的指令,并计算最后期限计数器的确切值,以确保正确的同步。

5.1.互斥

跨不同线程管理共享数据的一般方法是使用互斥的临界区,这些临界区一次只能由单个线程访问。我们的内存轮已经确保对共享单线程的任何访问都是原子性的,因此我们只需确保这些访问按正确的顺序进行。

翻译:Predictable Programming on a Precision Timed Architecture

图4显示了生产者、消费者和观察者访问共享变量buf的C代码(下划线)。生产者迭代并向共享数据写入一个整数值。消费者从这个共享数据中读取这个值并将其存储在一个数组中。为简单起见,我们的使用者不会对所使用的数据执行任何其他操作。它只是将数据存储在数组中。观察者还读取共享数据并将其写入到内存映射外围设备。

图4中的deadline指令用粗体标记。我们在每个线程的开头使用交错的最后期限来抵消线程,并强制生产者在消费者和观察者之前开始。在每个线程内部,最后期限强制每个循环彼此同步运行,每个线程以相同的速度前进。每个循环迭代首先执行生产者的临界部分,然后并行执行观察者和消费者。

达到这个目的的补偿是由项目开始时的最后期限给出的。相对于使用者和观察者来说,41∗6 = 246周期的最后期限是相同的,这迫使这两个线程同时进入循环。这个值是从汇编语言指令计算出来的,它是使用者线程所能完成的最小截止日期。生产者循环的偏移量是28∗6 = 168个周期,这比使用者和观察者的246个周期的偏移量少78个周期。由于此差异与轮转调度重复的频率相同,因此可以保证生产者线程在轮转的较早阶段访问共享数据。

一旦进入循环,最后期限迫使每个线程以相同的速度运行,从而维护内存访问计划。重要的是,这个速度是轮速度的倍数,以保持时间表。在本例中,我们希望每个循环迭代对轮子进行两次旋转。这允许我们在第一次轮询时写入缓冲区,并在第二次轮询时为使用者和观察者执行读操作。这也意味着,无论哪个C线程被划分到哪个硬件线程,我们的程序都将正确运行。为了实现这一点,我们将每个循环的截止日期设置为26,因为26*6 = 156循环正好对应轮子的两次旋转。

总结

PRET架构只是实现了相对时间,没有实现绝对时间。