MAXQアーキテクチャのテーブル処理

要約

MAXQマイクロコントローラはハーバードマシンです。ただし、MAXQマイクロコントローラは、コード空間に格納された定数へのアクセスを禁止する多くのハーバードマシンで共通した制約を受けると想定する必要はありません。むしろ、あらゆるMAXQデバイスに内蔵されたツールを使用すると、このようなテーブルルックアップが簡単になります。このアプリケーションノートでは、MAXQマイクロコントローラで効率的なテーブル処理を実行する方法について説明します。

はじめに

MAXQアーキテクチャでは、典型的なハーバードマシンに基づいた、強力なシングルサイクルRISCマイクロコントローラについて説明しています。ハーバードマシンは、重要な設計要素において、一般的なフォンノイマンマシンとは異なります。すなわち、ハーバードマシンの命令とデータは別々のバスで処理されます。単一データバスのための競合が全くないので、MAXQの命令はシングルサイクルのみで実行することができます。従来のノイマン型アーキテクチャで同じ演算を実施すると、複数サイクルを必要とします。

ただし、ハーバードマシンでデータとコードを厳格に分離すると、一連の独自の問題が生じます。コード空間にデータテーブルを格納することは、ノイマンマシンでは一般的な手法ですが、典型的なハーバードマシンでは問題になります。所定のバス上で1つのサイクルで1つの処理しか行えないので、CPUコアは、同じサイクルで、コードメモリバス上の命令をフェッチし、さらにコード空間のデータテーブルからメモリオペランドをフェッチすることはできません。

ハーバードマシンであるため、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

上記のこのコードは問題なくアセンブルされますが、2番目の命令の後、アキュムレータにはほぼ間違いなく0x1234がありません。理由は単純です。単一のメモリ空間しかなく、命令が完了するのに必要なサイクル数だけ使えるノイマンマシンとは異なり、MAXQのmove <reg>, @dp[0]命令は暗黙のうちにデータ空間にアクセスし、コード空間からの命令のフェッチとデータ空間からの読取りを1つのサイクルで完了することになるからです。アキュムレータにロードされる値は、コード空間のStartOfTableと同じオフセットでデータ空間にある値になります。

最初、この問題は解決困難と思われます。結局のところ、コード空間にアクセスするには、一定の時間が必要になります。アーキテクチャが許可したとしても、CPUコアは1つのクロックサイクルに2回のメモリアクセスを割り込ませることができないからです。ただし、MAXQアーキテクチャでは、マイクロコントローラがさまざまなメモリ空間に物理メモリブロックをマップする方法、およびユーティリティROMの少数のルーチンについて細部を記述すれば、この問題は解決されます。

第一に、MAXQのアーキテクチャでは、コード空間とデータ空間への物理メモリブロックのマッピングは固定されているのではなく、どの物理メモリブロックがアクセスされるかによって変化します。一般に、ほとんどのMAXQマイクロコントローラのフラッシュメモリ空間で動作するコードを書くプログラマは、ソフトウェアをコード空間の0の位置で開始するようリンクします。プログラマは、RAMがデータ空間の0位置から始まると想定し、それはその通り機能します。

ただし、MAXQマイクロコントローラは別の物理メモリ、すなわちユーティリティROMも備えています。ユーティリティROMは、すべてのMAXQマイクロコントローラのコード空間の0x8000の位置にあります。ユーザコードは、ユーティリティROMの0x8000ページのルーチンを呼び出し、特定の機能を実行することができます。さらに、ユーティリティROMで実行される限り、ユーザコードメモリはデータ空間の新しい位置に再マップされます。

ユーティリティROM以外で実行すると、データRAMはデータ空間の位置0x8000に引き続きアクセス可能ですが、コードメモリはデータ空間の位置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ルーチンへのコールが行われます。データを直接読み取るには1サイクル必要としますが、この処理は当然、1サイクルではなく、ロングコールに2サイクル、読取りの実行に1サイクル、復帰処理に1サイクル、計4サイクルを要します。

このコード例に伴うさらに大きな問題は、アセンブルされないということです! ラベル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

第1に、ユーティリティ関数テーブルは、命令ではなくアドレスで構成されていることがわかります。したがって、アプリケーションプログラマは、単純にテーブルにジャンプするのではなく、アドレスを検索し、それをコールする必要があります。第2に、最初のメモリ参照関数がテーブルの最初のエントリである必要はないということに注意して下さい。それぞれのMAXQマイクロコントローラには、さまざまなタイプと量のメモリ、およびさまざまな周辺回路を搭載することができるため、それぞれのデバイスではテーブル内の異なる相対オフセットで異なる関数リストを含むことが可能です。

3つの各ポインタレジスタは、それに関連する3つの関数を持っており、合計で9つのテーブルルックアップ関数があります。各ポインタレジスタの最初の関数は所定のアドレスにある値を検索するだけですが、後の2つはそれぞれ、間接ロードのポストインクリメント形式とポストデクリメント形式を使用します。いずれの場合にも、検索されたデータは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つの命令は1回しか実行する必要がありません。また、その後の各テーブルのフェッチには、コール、リード(ユーティリティROMから実行)、およびリターン(これもユーティリティROMから実行)の3サイクルしかかかりません。

フラッシュからRAMへのテーブルのコピー

テーブル全体をフラッシュからRAMに移動する方法をテーブル読取り関数から構築することができるようになります。たとえば、宛先アドレスがBPで与えられている場合、1つの方法を以下に示します。

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つの命令は1回しか実行する必要がありません。その後、テーブルの移動を必要な回数だけ実施することが可能で、GetDP0incサブルーチンのアドレスはA1に留まります。テーブルの移動には、1回実行するたびに6サイクルを要し、さらにセットアップのためのオーバヘッドが必要です。

move dp[0], dp[0]命令は、MAXQアーキテクチャの特異な特性のために必要となります。データ空間に関連するアドレスバスは1つしかないため、データ空間に対して実行されるいずれの読取りよりも少なくとも1サイクル先にセットアップする必要があります。テーブル移動ループでは、DP[0]によって与えられたアドレスで読取りが実行された後、書込みアドレスがバス上に設定されます。しかし、move dp[0], dp[0]命令がなければ、テーブル内の次の位置を読み取るときに、書込みアドレスはまだバス上にあります。この明らかにnullの命令を挿入することによって、次の読取りイベントを見越してソースオペランドのアドレスバスがリフレッシュされます。

ただし、このタスクを実現するさらに優れた方法があります。ユーティリティ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ルーチンでは、1回の実行当りのサイクル数が3に減少しているため、前述の方法に比べて時間が約1/2に短縮されます。copyBufferルーチンが復帰するとき、LC[0]は0になり、OFFSレジスタは、最後に書き込まれた宛先位置の次の位置を示します。OFFSは8ビットレジスタであるため、最大256ワードのテーブルをこの方法でコピーすることができます。

例:文字列の出力

多くのマイクロコントローラベースのプロジェクトに共通するタスクとして、いくつかのあらかじめ用意されたメッセージの中から1つをコンソールに出力するというタスクがあります。多くの場合、各メッセージには番号が与えられており、共通ルーチンによってその番号をメッセージ文に変換する必要があります。

このタスクのために一般に使用される手法は、各メッセージ文字列を0で終了し、メッセージ番号を各文字列が存在するアドレスに変換するテーブルを提供するというものです。この手法は信頼性が高く、迅速ですが、2つのデータ構造、すなわちアドレステーブルと文字列自体を構築することが必要となります。2つ目の手法は0で終了する文字列を1つの大きな連続メモリ空間に配置し、順次検索するだけです。この方法は経済的ですが、出力が始まる前に、対象文字列に至るまでのすべての文字を検索しなければならないため、実行時間が膨大なものになります。

実用的な妥協案は、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: