应用笔记 3770

MAXQ架构的表操作

By: Ben Smith

摘要 : MAXQ微控制器采用Harvard结构。然而,不应该认为MAXQ微控制器也沿袭了Harvard结构通常所具有的局限性—不能访问存储于代码空间的常量。相反,嵌入在每个MAXQ器件中的工具使得这样的查找表更加容易实现。该应用笔记详细阐述了如何在MAXQ微控制器内有效地执行表操作。

简介

MAXQ架构是一种基于标准Harvard结构、功能强大的单周期RISC微控制器。Harvard结构与常见的Von Neumann结构相比,其不同之处在于重要的设计结构方面:Harvard结构的指令与数据在不同的总线上传输。由于不存在单条数据总线的冲突问题,MAXQ指令的执行时间仅需要单个周期。而传统的Von Neumann架构完成相同的操作则需要多个周期。

然而,Harvard结构中数据与代码的严格分离也带来了一系列的挑战。Von Neumann结构的一项通用技术就是可以在代码空间存储数据表,这对于标准Harvard结构来说是很难实现的。在给定总线上,单个指令周期内只能进行一个操作,因此在同一周期内,CPU核不可能既从代码存储器总线上取指令,又从代码空间的数据表中取出存储器操作数。

有人可能会认为采用Harvard结构的MAXQ微控制器也不能在代码空间内存储数据。实际上,每一款MAXQ器件都内嵌了ROM工具,因此很容易实现表项查找操作。

代码空间的表查找

从代码空间的MAXQ表中读取一个数值看似简单,然而对于不熟悉MAXQ架构的编程人员来说,第一次尝试该操作时通常会失败。
IncorrectTableLookup:
	move	dp[0], #w:StartOfTable
	move	acc, @dp[0]
	.
	.
	.
	ret
.
.
.
StartOfTable:
	dc16	01234h
	dc16	05678h
	dc16	098abh
	dc16	0cdefh
上述代码能很顺利地完成汇编,但是执行完第二条指令之后,累加器的值几乎可能是0x1234。原因很简单,Von Neumann结构只有一个单独的存储空间,一条指令根据操作的需要可能花费多个指令周期,而MAXQ与此不同,其move <reg>, @dp[0]指令在单个周期内隐含地访问数据空间并完成指令操作,即从代码空间取指令,并从数据空间读取数据。装载到累加器中的数值是存储于数据空间的数据,只不过该数据的偏移量和代码空间的StartOfTable相同

这个问题刚开始似乎很难解决。毕竟,访问代码空间需要一定的时间;CPU核不能将两次存储器访问压缩在一个时钟周期内完成,即使架构允许这样。然而,如果我们了解了MAXQ架构的微控制器如何将物理存储模块映射到不同存储空间的一些细节信息,并借助于固定用途ROM中的一些程序,就可以解决这一问题。

首先,在MAXQ架构中,将物理存储模块映射至代码空间和数据空间的方式是固定的,而是取决于正在访问的物理存储模块。编程人员为大多数MAXQ微控制器所编写的代码都运行于闪存空间内,通常他们将其软件连接到代码空间的地址0处。编程人员会认为RAM也是从数据空间的地址0开始的,事实也的确如此。

但是,MAXQ微控制器还有另一块物理存储器,即固定用途ROM。所有MAXQ微控制器的固定用途ROM都位于代码空间的地址0x8000。用户代码可以调用固定用途ROM中0x8000页面的程序,执行特定的函数。并且,只要执行固定用途ROM中的程序,用户代码存储器即被重新映射到数据空间的一个新地址上

开始执行固定用途ROM的程序后,可以继续访问数据空间以地址0x0000开始的数据RAM,而代码存储器却被重新映射到数据空间以地址0x8000开始的位置。因为代码闪存映射到了数据空间,运行的固定用途ROM代码可以像访问数据一样访问存储于用户代码中的数据信息。通过固定用途ROM函数,可简单地通过指针寄存器间接读取数据并返回结果。

因此,将上面给出的程序稍作改动后得到:
BetterTableLookup:
	move	dp[0], #w:StartOfTable + 08000h
	call	UtilityROMGetDP0
	.
	.
	.
	ret
.
.
.
StartOfTable:
	dc16	01234h
	dc16	05678h
	dc16	098abh
	dc16	0cdefh
在本例程中,调整待读取的地址,以反映执行固定用途ROM程序时闪存的映射地址,然后将其装入DP[0]。这里采用了调用固定用途ROM程序的方法,而不是直接读取数据。当然,直接读取数据只占用一个指令周期,而这一操作则占用了四个指令周期:2个周期用于长调用,1个周期用于读取数据,1个周期用于返回操作。

这个代码例程存在的更大问题是不能进行汇编操作! 标记UtilityROMGetDP0没有定义,造成这一结果的原因很简单:各款MAXQ微控制器的实用程序地址是不同的。事实上,甚至不能保证这些程序在特定MAXQ器件的不同版本中位于相同的位置!

为解决这一问题,每一款MAXQ微控制器的固定用途ROM都包含一个固定用途函数的地址表,以及一个指向该表的指针,该指针的地址固定为0x800D。需明确指出的是,固定用途ROM包含以下代码:
	org	0800Dh
	dw	UtilityFunctionTable
.
.
.
UtilityFunctionTable:
.
.
.
	dw	GetDP0
	dw	GetDP0Inc
	dw	GetDP0Dec
	dw	GetDP1
	dw	GetDP1Inc
	dw	GetDP1Dec
	dw	GetBP
	dw	GetBPInc
	dw	GetBPDec
注意:第一,固定用途函数表由地址组成,而不是指令。因此,编程人员必须提取地址并call它,而不能简单地跳转至该表。第二,第一个存储器函数也许不是该表的入口。由于每款MAXQ微控制器包含不同类型和容量的存储器以及不同的外设,每款器件很有可能包含不同的函数列表,函数在表中具有不同的相对偏移量。

3个指针寄存器各自都有3个相关的函数,总计有9个表查找函数。每个指针寄存器的第一个函数只是提取位于给定地址的数据,而后两个函数分别采用后递增和后递减形式的间接装载。在每一种情况下,都将提取到的数据装载到GR寄存器中。

现在,代码可改为如下形式:
CorrectTableLookup:
	move	dp[0], #0800Dh	; Point to pointer to function table
	move	acc, @dp[0]		; acc now has pointer to ftable
	add	#3			; For 2000, GetDP0
	move	dp[0], acc		; Load ptr + offset to dp0
	move	a[1], @dp[0]	; Get address of GetDP0 into A1
	move	dp[0], #StartOfTable + 08000h
	call	a[1]			; This will call GetDP0, finally!
	.
	.
	.
	ret
.
.
.
StartOfTable:
	dc16	01234h
	dc16	05678h
	dc16	098abh
	dc16	0cdefh
需注意,一旦找到GetDP0程序的地址,即可将该地址存放起来并重复使用。上述前5条指令只需要执行一次;然后,每次访问表数据操作只需要三个指令周期:调用,读取(运行固定用途ROM内的程序),返回(也运行固定用途ROM内的程序)。

将数据表从闪存拷贝到RAM

将整个表从闪存拷贝到RAM的方法之一是利用表的读函数实现。例如,如果在BP中给出了目标地址,那么拷贝方法如下:
SlowTableMove:
	move	dp[0], #0800Dh	; Point to pointer to function table
	move	acc, @dp[0]		; acc now has pointer to ftable
	add	#4			; For 2000, GetDP0Inc
	move	dp[0], acc		; Load ptr + offset to dp0
	move	a[1], @dp[0]	; Get address of GetDP0 into A1
	move	dp[0], #StartOfTable + 08000h
	move	bp, #RAMDest	; Set this label to desired dest
	move	offs, #0ffh		; Pre-decremented offset
	move	lc[0], #4		; Move four words
TableMoveLoop:
	move	dp[0], dp[0]	; Set source pointer
	call	a[1]			; This will call GetDP0inc
	move	@bp[++offs], gr	; Store retrieved word to dest
	djnz	lc[0], TableMoveLoop
	.
	.
	.
	ret
.
.
.
StartOfTable:
	dc16	01234h
	dc16	05678h
	dc16	098abh
	dc16	0cdefh
如上文所述,前5条指令只需要执行一次,此后可根据需要多次执行表操作,GetDP0inc子程序地址始终保存在A1中。每次执行表操作需要6个指令周期外加建立开销。

加入move dp[0], dp[0]指令是MAXQ架构的特殊性要求的。由于数据空间只有一条地址总线,因此必须在读数据空间操作的前1个周期先将地址建立起来。在表操作循环中,对DP[0]给出的地址进行读操作,然后在总线上放置写地址。如果没有move dp[0], dp[0]指令,当读取表中下一个地址的数据时,写地址会仍然占据总线。通过插入这条明显的空指令,可以为预期的下一个读操作刷新源操作数地址总线。

然而,还有一个更好的方法完成该拷贝任务。固定用途ROM中包括一个能实现上述相同功能的copyBuffer程序,而且所占用的指令周期更少。copyBuffer程序在固定用途ROM中位于表查找程序的后面。
FasterTableMove:
	move	dp[0], #0800Dh	; Point to pointer to function table
	move	acc, @dp[0]		; acc now has pointer to ftable
	add	#12			; For 2000, copyBuffer
	move	dp[0], acc		; Load ptr + offset to dp0
	move	a[1], @dp[0]	; Get address of GetDP0 into A1
	move	dp[0], #StartOfTable + 08000h
	move	bp, #RAMDest	; Set this label to desired dest
	move	offs, #0		; No need to pre-decrement offset
	move	lc[0], #4		; Move four words
	call	a[1]			; This will call copyBuffer
	.
	.
	.
	ret
.
.
.
StartOfTable:
	dc16	01234h
	dc16	05678h
	dc16	098abh
	dc16	0cdefh
copyBuffer程序将每次表操作的周期数减至3个,比之前提到的方法节省了约一半时间。当从copyBuffer程序返回时,LC[0]清零,OFFS寄存器指向最近一次写目标地址的下一个位置。因为OFFS是一个8位寄存器,因此用这种方法可以拷贝多达256字的表。

实例:字符串输出

在许多基于微控制器的应用中,通常都要将预存的消息输出到控制台。每条消息都指定了一个编号,必须由一个通用程序将该编号转换成消息文本。

完成该任务通常采用每个消息字符串以0结尾的技术,同时提供一个表,以便将各消息编号转换成消息字符串的首地址。这项技术非常可靠和快速,但必须建立两个数据结构:地址表及字符串本身。另一项技术是简单地将以0结尾的各字符串存入一个大的、毗邻的存储器空间,并采用线性查找。虽然该方法比较简单,但却是以花费大量执行时间为代价的,因为在输出之前,必须找到目标字符串里的每一个字符

还有一种较好的折衷办法,即字符串采用按长度划界的方法取代以0划界的方法。采用这种技术,首先给出每个字符串的长度,然后紧接着是该消息的实际字节信息。这样一来,可以快速跳过不用的信息,并且该表的长度没有以0划定界限的长。这种折衷技术的局限性仅在于表中的每个字符串长度不能超过255个字符。
;
; Output String
;
; Enter with ACC=an index value (one based) indicating which
; string to output.
;
; On exit, LC0=0, DPC=0, ACC, A1, A2, DP0 used.
;
output_string:
	move	lc[0], acc		;Set LC0 to index of string
move	dpc, #4		;Set DP0 to word mode
move	dp[0], #800dh	;Point to table of pointers
move	acc, @dp[0]		;Get address of table
add	#3			;Offset to GETDP0 routine
move	dp[0], acc		;Load pointer to table
move	a[1], @dp[0]++	;Get GETDP0
move	a[2], @dp[0]	;Get GETDP0INC
move	dpc, #0		;Set DP0 to byte mode
move	dp[0], #string_table + 8000h

str_search_loop:
call	a[1]			;Get a string length
djnz	lc[0], next_str	;If not this string, go to next
move	lc[0], gr		;Otherwise, put len in LC0
move	acc, @dp[0]++	;...and point past length

out_loop:
call	a[2]			;Get a char and bump pointer
call	char_out		;Output the character
djnz	lc[0], out_loop	;If more characters, loop
ret				;Otherwise, we're done.

next_str:
move	acc, gr		;GR contains len of this string
add	dp[0]			;Add current ptr to current len...
move	dp[0], acc		;...to create a new pointer
jump	str_search_loop	;Jump back and test index again

;
; Each entry in the string table begins with the string length
; followed by the string characters.
;
string_table:
dc8	string1 - string_table
dc8	"This is the first string."
string1:
dc8	string2 - string1
dc8	"This is a second example of a string"
string2:
dc8	string3 - string2
dc8	"A third string."
string3:
dc8	string4 - string3
dc8	"Finally, a fourth string in the array!!!"
string4: