MAXQ8913及其它MAXQ®微控制器采用的Harvard存储器映射架构为用户提供了极大的灵活性,可根据需要将不同的物理内存(例如数据SRAM)映射为程序或数据内存空间。在特定环境下,从数据SRAM执行一个程序的部分代码能够提升性能并降低功耗。这些优势都是以应用程序的更加复杂为代价。
概述
MAXQ8913和其它许多MAXQ微控制器一样,也包含了一个基于SRAM的内置数据存储区域,该存储区域可被映射为数据内存空间,或者选择映射为程序内存空间。内置SRAM通常用作数据存储器,而在程序闪存或掩膜ROM中执行大部分程序代码。然而,在特定环境下,从内部SRAM执行有限的部分代码非常有用。
本应用笔记介绍如何配置、装载汇编程序,以便从内部SRAM正确运行,本文还讨论了这种方法的优势和缺点。本应用笔记给出的例程针对MAXQ8913编写,使用基于汇编的MAX-IDE环境。用户可下载本文所涉及的应用程序代码和项目文件。
本文讨论的代码都特别针对MAXQ8913微控制器编写,所介绍的原理和方法也同样适合其它含有可映射为程序空间的内部SRAM的MAXQ微控制器。能够以这种方式执行代码的其它MAXQ微控制器包括:MAXQ2000、MAXQ2010和MAXQ3210/MAXQ3212。
该代码能很好地运行在任何基于MAXQ8913并为MAXQ8913的串口0提供一路串行接口(RS-232或USB至串口转换)的硬件。将一个终端模拟器连接到该串口,并设置为9600波特率、8个数据位、1个停止位、无奇偶校验,即可查看例程的代码输出。
用户可免费下载MAX-IDE环境的最新安装程序包和文档资料。
MAX-IDE安装程序
MAXQ核编译指南
开发工具指南
在RAM中执行代码的优势
通常情况下,MAXQ微控制器的绝大多数应用代码都被设计为在主程序空间执行,主程序空间通常是利用一片大的内部闪存或(对于掩膜ROM器件)用户指定的应用ROM来实现。主程序空间为非易失存储器,所以大多数情况下可用来保存应用程序代码。内部SRAM被用来存储变量、软件栈,以及器件被关闭时不需要保存的类似数据。
然而,对于特定应用,在数据SRAM中执行某些代码具有一定优势。
降低功耗
在大多数MAXQ微控制器中,当在内部SRAM (或固定用途ROM)中执行代码时,相对于程序闪存而言,电源电流会减小。因为闪存在不被存取时可被动态断电,所以这种情况下就能节省功率。如果某个应用程序通常在大部分活动时间内执行非常小的代码量,在SRAM中执行就能大大降低总体功耗。
直接访问主程序空间存储器
通常,从主程序闪存执行的代码不能直接读取保存在主程序闪存中的数据。这种类型的数据可以包括随应用程序数据一起的常量字符串和数据表。若要读取该数据,应用程序必须调用固定用途ROM中的专用数据传递函数。在RAM中执行代码则避开了这一限制,允许利用标准的数据指针直接读取闪存中包含的数据。这就加快了存取操作。若一个小的算法花费大量的时间遍历闪存中存储的查找表或其它常量数据,那么在RAM中执行该算法则能够在非常短的时间内完成运算。
可重写整个闪存
和大多数基于闪存的MAXQ微控制器一样,MAXQ8913中的固定用途ROM含有在应用程序控制下擦除和重写程序闪存的标准函数。该过程能够使用户装载器通过用户指定接口(例如串口、SPI或I²C)重新装载部分或全部应用程序。然而,若用户装载程序位于闪存内,则不能擦除或重写自身所占用的闪存。在RAM中执行用户装载器,可以擦除整个闪存程序空间并重新写入新的代码,包括用户装载器本身。
在RAM中执行代码的缺点
在RAM中执行应用程序代码也存在缺点和限制。有些缺点与具体工作相关,而有些缺点则是MAXQ架构所固有的。
有限的代码空间
RAM一般比程序闪存小得多,这意味着在任何给定时间只能执行少量代码。但有可能在RAM中运行一个例程,然后将其擦除并装载第二个例程,随后再运行第二个例程,依此类推。
代码映射
在RAM中执行代码之前,必须将其复制到RAM。这一过程需要时间和代码空间。此外,代码必须从某个位置复制,所以代码实际上被存储两次:一次在闪存或程序ROM,一次在RAM。即使该代码不是为了在闪存中执行,也必须被存储于其中,从而消耗了额外的空间。
不可直接存取RAM
当在RAM中执行代码时,RAM就不再是可见的数据存储空间。这意味着不能利用数据指针直接从RAM存储单元读取或写入数据。按照在闪存中运行应用代码相同的方式,有可能避开这种限制。利用固定用途ROM数据传递函数(UROM_moveDP0和类似的函数)可对RAM进行读取,以及通过在闪存中写入类似的函数,可直接对RAM进行写操作。然而,这种迂回方法也占用额外的时间和应用程序空间。
编译在RAM中执行的代码
在编写将要在数据RAM中执行的应用代码时,必须要考虑一项主要因素。代码的每个字都将被编译至一个地址并被装载至该地址的闪存中,但是将在不同地址的RAM中执行。例如,如果一段代码被装载至以程序字地址0100h开始的闪存中,并被复制到以数据字地址0100h开始的RAM中,在RAM中就不可能跳至地址0100h来执行代码。在闪存中,地址0100h仍然是代码的地址。在程序空间中,RAM中代码的地址是其数据存储地址加上偏移量A000h,如图1所示。
图1. MAXQ8913在RAM中执行代码时的内存映射
为了执行复制到RAM中数据内存地址为0100h的应用程序,必须跳至程序地址A100h。
在RAM中执行代码会为MAX-IDE编译器造成困难。MAX-IDE并不知道将在与编译地址不同的地址执行代码。例如,假设一个例程调用了闪存地址为0080h的subOne,而另一个位于0300h的例程调用了第一个例程。其代码如下所示。
org 0080h
subOne:
....perform various calculations...
ret
...
org 0300h
subTw
call subOne
...and so on...
如果两个例程均被复制到RAM并在此执行,将会发生什么? 假设例程均被复制到RAM中与其在闪存中占用的程序地址相同的数据内存地址,那么subOne将位于程序地址A080h,subTwo将位于A300h。
因为“call subOne”所在行与目标端标签subOne之间的距离超过了相对跳转距离(+127/-128个字),所以指令就必须被重新编译为绝对LCALL。然而,编译器所持有的subOne的唯一地址是0080h,所以指令将被编译为“LCALL 0080h”。当subTwo执行时,它将不调用位于RAM中的subOne副本,而是调用位于闪存中的版本。
有两种迂回方法可能解决这种困境。第一种方法也是最简单的方法,即强制编译器始终使用相对跳转和调用,并使例程在RAM中离得足够近,使其能够按照这一方式调用其它例程。总是使用SJUMP和SCALL,而不是JUMP和CALL机器码(使编译器可选择短或长跳转)。这将强制使用指令的相对跳转版本。
然而,这种方法也存在限制。如果在RAM中运行的代码量长于128个字,相对跳转就有可能不足以长到使RAM中的一个例程调用另一个例程。这种情况下的解决方法是通过ORG声明为不同的例程使用固定的地址,然后定义包含其在RAM中的正确地址的等价变量。这些等价变量可被用于LCALL和LJUMP声明中,如下所示。
subOne equ 0A080h
org 0080h
; subOne
....perform various calculations...
ret
...
org 0300h
subTw
lcall #subOne
...and so on...
这一过程强制编译器为LCALL使用正确的地址。
将代码复制至RAM
在RAM中执行代码之前,必须首先将其复制到RAM。将大量代码从闪存复制到RAM的最简单方式是使用应用ROM copyBuffer函数。该函数的输入参数为两个数据指针(DP[0]和BP[Offs])和一个长度值(LC[0])。它将指定数量的字节/字从源地址DP[0]复制到目标地址BP[Offs];一次可复制最多256个字节/字。
我们的示例应用程序将其开始的512个字从闪存复制到RAM,然后跳转至RAM中的副本开始执行代码。源指针(DP[0])指向程序闪存在应用ROM的内存映射中的地址,从8000h开始。请主意,为了避免无限循环,在复制RAM的代码之后,我们跳转至RAM中副本的部分。
org 0020h
copyToRAM:
move DPC, #1Ch ; Ensure all pointers are operating in word mode.
move DP[0], #8000h ; Start of program flash from UROM's perspective.
move BP, #0 ; Start of data memory.
move Offs, #0
move LC[0], #256 ; The Offs register limits us to a 256-word copy.
lcall UROM_copyBuffer
move DP[0], #8100h ; Copy second half.
move BP, #0100h
move Offs, #0
move LC[0], #256
lcall UROM_copyBuffer
ljump #0A040h ; Begin execution of code from RAM.
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;
;; Executing from RAM
;;
org 0040h
move LC[0], #1000
delayLoop:
move LC[1], #8000
sdjnz LC[1], $
sdjnz LC[0], delayLoop
;; Initialize serial port.
move SCON.6, #1 ; Set to mode 1 (10-bit asynchronous).
move SMD.1, #1 ; Baud rate = 16 x baud clock
move PR, #009D4h ; P = 2^21 * 9600/8.000MHz
move SCON.1, #0 ; Clear transmit character flag.