Recently in Z80 Category

Contikiのメンテナンス

| No Comments | No TrackBacks

ここんところ、SourceForgeへのCVS接続がうまく動かない障害にずっと悩まされていて放置プレイだったのですが、久しぶりにContikiのメンテナンスをはじめてみました。 一応私は、Z80全般およびPC-6001部分のソースが担当になっています。

ていうか、えらいコードが変わっていて、ついていけない...。 まぁ、メーリングリストもちゃんと読んでいるわけではないんですが。

しかし、そんなことより問題は、SDCCがバージョンアップ(2.9.0)したことにより、今までなら通っていたと思われるコードが通らなくなってしまったことでしょう。 なんか、開発時間の半分以上はSDCCのおかしな挙動の解析とその回避に費やしているような。

最初に動くようになるまでは楽しいのですが、こういうメンテナンス作業って苦痛なんですよね...。


Operating Systemの開発?

| No Comments | No TrackBacks

P6絡みののりさんが、ROM乗せかえ(実際にはエミュレータ上のBIOSイメージ入れ替え)によるフリーのPC-6001実行環境を開発しています。

私も以前、実機のROMを外して別のROMやRAMに変えられるのではないかと妄想していましたが、それをエミュレータ上でやってしまおうということですね。

現在のエミュレータでも、ROMイメージファイルの入れ替えはできますが、RAMにできたらいいなぁ。 そうしたら、SDカードから似非ROM(別に本物のROMでもよいが)で起動して、CP/MやらUN*Xもどきやらを作れそう。 もちろんContikiも動かしやすくなります。

おそらく実機に実装されているROMを外してしまえば、拡張カードに積んだROMやSRAMが動くと思うので、あながち夢物語でもないような気がしています。 PC-6001mkII以降の場合は最初から64kB RAMが実装されているので、そちらでやったほうが早いかな? でも中身しらないんだよな~。

などと思っていたら無性にOSのソースが読みたくなって、ELKSやらMINIXやらのブートアップソースを眺めちゃったりしてます。 久々にx86のアセンブリソースを見ました。 これでも昔はPC-9801(初代)内蔵のワンラインアセンブラでゲーム作ったりしてたんですョ。

Z80ベースのOSなら「まずCP/Mを読め!」と言われそうですが、Intelニーモニックのオールアセンブリなので、読むのが面倒なこと、もともと商用ライセンスのものであること、設計が古いということで、ちょっとスキップです。:-p

x86(というかIBM PCおよび互換機)ではBIOSがソフトウェア割込み(INT 13hなど)によって基本機能を提供していますが、Z80系だとRSTを使うのがいいのでしょうね。 CP/Mのように常に0番地をRAMにしろというのも微妙ですが、64kB空間を確保してRSTベクタを活用すれば、他機種への移植性も高まるでしょう。 さすがに割り込みモード0を使った機種はないと思いたい...。

BIOSといえば、LinuxなどではROM BIOSに依存するのは最初のブートアップのときだけです。 最初の一部分はどうしてもBIOSに依存しなければならないのですが、そのせいで逆にブートアップシーケンスがとてつもなく面倒なことになっています。 Linuxカーネルのbootsect.Sやsetup.Sを見ると苦労の跡がしのばれます。

SDカードアダプタへのI/Oがコールドスタートのできるだけ直後に開始できれば、機種依存のROM BIOSを可能な限り小さくできます。 そうすれば、読み込むカーネルが機種依存になるとしても、ただのFAT上のファイルなので移植やメンテナンスが多少は楽になるかもしれません。

今のSDカードアダプタを動かしたら、試してみたいものです。


PSGPCM再生時のクロックの話、再び

| No Comments | No TrackBacks

性懲りもなくPSGPCMと格闘しています。

PSGによるPCM再生は、以前11kHz/9ビット相当の比較的高音質の再生を成功させましたが、マシンパワーおよびメモリを非常に消費する方法でした。

そのため、来るべき(?)画像と音声の同時再生に向け、いかに「軽く」発音させるかについても少し検討してみることにしました。 まずはテストとして、PSG 1チャネルで8kHz/4ビットの再生を試しました。

すると、またしてもクロック数計算が合わない事象に出くわしてしまいました。 以前はI/Oウェイトの考慮漏れが原因でしたが、今回はそうでもない感じです。

データはpacked 4bit(1バイトの上位4ビット、下位4ビットにそれぞれ音量データを格納する方式)で作成しました。 再生ルーチンの中心部は次のようになっています。

	; 割り込みは禁止、DMA OFF
	; あらかじめ音量や周波数は設定しておく
	; ポート0xa0に8を出力しておく(出力チャネルのラッチ)
	; HL=データ開始アドレス、BC=データ長
_loop:
	ld      a, (hl)		;8
	ld	d, a		;5
	and	a, #0xf0	;8
	rra			;5
	rra			;5
	rra			;5
	rra			;5
	; output high nibble
	out     (#0xa1), a	;13 -> 54 (a)
	call	_wait	 	;18 + _wait
	inc	hl		;7
	dec     bc		;7
	ld	a, d		;5
	and	a, #0x0f	;8
	; dummy
	inc	hl		;7
	dec	hl		;7
	ld	d, #1		;8
	ld	d, #1		;8
	nop			;5
	; output low nibble
	out     (#0xa1), a	;13 -> 75 (b)
	call	_wait	 	;18 + _wait
	ld      a, b		;5
	or      c		;5
	jp      nz, _loop	;11 -> 21 (c)
	; 終了処理(省略)

_wait:
   	ld      e, #24		;8
_wait_loop:			; 16 per loop
	dec	e		;5
	jp      nz, _wait_loop	;11
	ret			;11
  

ニーモニックのあとに表記しているのは実行クロックです。 M1 waitおよびI/O waitを考慮した数値です。 CB/DD/ED/FD修飾のある命令は存在せず、すべてRAM上で実行させるので、これ以外のウェイトは入らないと思います。

元データを壊してもいいならrldを使えばもっと高速になったりしますが、今回は単に実験なのであまり深く考えず、上位/下位のそれぞれが同じ時間で再生されるようにします。 同じwaitルーチンを使うために、処理の速いlow nibbleはダミーを入れています。 これで

  high nibble = (a) + (c) + wait(w/ call) = 75 + wait(w/ call)
   low nibble = (b) +       wait(w/ call) = 75 + wait(w/ call)
  

と、同じ時間で処理できます。

これだと高速すぎるので、waitルーチン内においてeレジスタの設定値で時間調整をします。 クロック周波数3993600Hzで8000Hzの再生をするためには、 1サンプルあたり、

  3993600 / 8000 = 499.2
  

となり、ひとつのサンプルがおよそ499クロックになるようにウェイト時間を調整すればいいことがわかります。 callに18クロック、ウェイトのループ以外の処理に19クロック、ループ1回に16クロックなので、逆算すると、

  (499.2 - (75 + 18 + 19)) / 16 = 24.2
  

となり、eレジスタの値を24にしています。

これを再生させると、7.25秒分のサンプルの再生に実測でおよそ11秒かかってしまいます。 当然音程も低くなります。

ひとつのサンプルにかける時間を350クロック程度にすると、おおよそ原音に近い時間と音程になります。 なぜでしょうね...??

実験はエミュレータ(PC-6001VW ver 205c)で行いました。 いま分解ですごいことになっているので、実機では試験できていません。


AVR-SD拡張回路(改良後)

| No Comments | No TrackBacks

ラッチを使ってZ80との入出力ができることがわかったので、修正した回路図です。 Webページに収まるようになるべく小さく描いたので見にくいのはご勘弁。


20081225-AVR_SD-OR3.PNG

う~ん、4075(3入力OR)は持ってないぞ。 32(2入力OR)ならあるんですが、効率が悪いんですよね。 4078(8入力OR)を使ってもいいですね。 ゲートの段数も少なくなってすっきりします。 こんな感じ。

20081225-AVR_SD-OR8.PNG

その場合、グランドになっている部分を変更することでA1までデコードできます。 AVRにA0だけしか入れられなくなるけど。 まだPCからのインタフェースは設計途上なのですが、一応下位のアドレスラインはデコードして使うかと思い、残してあります。

ゲートはすべてHCシリーズを想定しています。 Z80へデータを出力するときのみ、レベルあわせのために5Vを供給しています。 ちゃんとバッファICを使ってないけど、大丈夫かなこんなんで?

事実上、NORが1ゲートあまっている(4-5-6ピン)ので、必要ならA6~A2のどこかにNOT代わりに入れればI/Oアドレスを調整できますね。

今のところ最下位アドレス(0x00~0x03)を使う想定になっています。 AVRのI/Oが3本ほど余っていますが、外部クロックを使うことになったときに使うもよし、他の機器をコントロールするもよし。

...とりあえずデバッグLED用かな?


とりあえず動作

| No Comments | No TrackBacks

とりあえず、前回のテストは期待通りの動作をさせることができました。 原因はあちらこちらの断線でした。

UEWを使っていて、しょっちゅう回路を変更するものだから、そのたびに線をいじったりしているうちに断線してしまっているのでした。 直しては他の箇所が断線するいたちごっこ状態。 こんなんじゃ先が思いやられるなぁ。

ちょっと面白かったのは、LS373の電源(Vcc)が断線していたときのことです。 入力端子の電圧が3.3V程度あり、そのときの出力端子の電圧が2.3Vくらいあるんですね。 もちろんVccがないので出力コントロール(^OE)が利かず、バス直結のラインにその電圧を出力していたわけですが、なぜかPC側本体側は暴走しませんでした。 本体側でデータバスがぶつかってるのに。

どうも、拡張スロットへのデータバス入出力にはLS245または他のバッファが接続されているようです。 拡張メモリを使っていない状態だったため、拡張スロットへの入出力がこの実験用回路に向けたもののみだったのが幸いしたのでしょう。

今回はPC側から見て入力のみを実験しましたが、同じ手法で出力もできます。 というか、出力のほうが楽(あらかじめデータを用意しなくていいから)ですね。 また回路変更で断線しまくりそうで怖いですが。

さて、そうなると次はSDカードのアクセスか...。

ラッチを入れてみる

| No Comments | No TrackBacks

I/O回路の要求には高速応答性が必要で、到底AVRの割り込み反応時間では間に合わないことがわかったので、バッファを入れる実験をしました。

例により、簡略化した回路です。 手持ちのラッチがLS373しかなかったので、電圧が混在しています。

20081221-Test.PNG

プログラムのほうは、なんかoutしたら、そのタイミングでデータを出力し、inで読めるようにしてみました。単純にデータをインクリメントしているだけです。

.org	0
	rjmp	RESET
	rjmp	INT_IN
	rjmp	INT_OUT
INT_IN:
	inc		Data
	; buffer input close
	cbi		PORTB, 1
	reti
INT_OUT:
	; output data
	mov		Temp, Data
	ori		Temp, 0x40
	out		PORTC, Temp
	mov		Temp, Data
	ori		Temp, 0x3f
	out		PORTD, Temp
	; load data to the buffer
	sbi		PORTB, 1
	reti
RESET:
	ldi		Temp, low(RAMEND)
	out		SPL, Temp
	ldi		Temp, high(RAMEND)
	out		SPH, Temp
	ldi		Data, 0x00
	ldi		Temp, 0x12
	out		DDRB, Temp
	ldi		Temp, 0x3f
	out		DDRC, Temp
	ldi		Temp, 0xc0
	out		DDRD, Temp
	ldi		Temp, 0x0a
	out		MCUCR, Temp
	ldi		Temp, 0xc0
	out		GICR, Temp
	sei
LOOP:
	rjmp	LOOP

とりあえずの「データを読ませる」という結果としては良好です。 AVRからのデータらしきものがPC側から読めています。

ただ、なんかD0がふらふらする変な挙動です。 outしないでinし続けると0が入力され続けるはずなのに、ときどき1が混じります。 out/inを交互に繰り返すと、大部分が2回ずつ同じ値を読んで、次は2インクリメントします。

データラインがショートしたりているわけではなさそうなのですが、いまのところ原因はわかりません。接触が悪い可能性はありますが、直しても同じなのが謎。そこら辺から剥ぎ取ったラッチが変なのかも。

とりあえずラッチ意味があったということで勘弁しといてあげよう(弱気)。


AVRの割り込み反応時間

| No Comments | No TrackBacks

Z80からのI/O信号に対し、どのくらいでAVRによる割り込み反応が得られるのかを調査してみました。 回路は前回作ったものを使います。

PC-6001側からI/O命令を発行し、AVRの外部割込み入力をトリガにして反応を調べます。 まずは、主な信号線の様子。

20081214-INT0.PNG

ここで、INT0が活性化されている時間(A-B)は、630ns。

実行されているプログラムは以下のような感じです。

.org	1
	rjmp	INT_IN
INT_IN:
	sbic	PIND, 2	; 割り込み終了だったらInputへ
	rjmp	Input
	; omit...
Input:
	out	DDRC, r18
	out	PORTC, r19
	sbi	PORTB, 4
	reti

INT0は両トリガでの割り込みにしているのですが、 AVRの割り込みルーチンが反応するときには、INT0はHレベルになっているのか、常にInputルーチンへ飛びます。

上図にあるLEDという信号線は実際にはAVRのポートB4で、これがLからHになる(sbi PORTB, 4)まで2,040nsかかっています。 8MHzだと約16クロック分です。

割り込みへの反応速度を見るために、プログラムを少し変えてみました。

.org	1
	rjmp	INT_IN
INT_IN:
	sbi	PORTB, 4
	; omit...

割り込みがアクティブになってから、rjmpおよびsbiのみの実行です。 タイミングは以下のようになっています。

20081214-INT1.PNG

これで、LED(PORTB4)の変化までの反応時間は1,200nsでした。8MHzで9.6クロック分。 rjmpとsbiの実行時間4クロックを差し引くと、割り込みの反応に700ns(5.6クロック)ほどかかることになります。

ちなみに、割り込みをダウンエッジのみにして追試してみましたが、反応時間は1,160nsで、それほど大きな差はありませんでした。

これを、最初の図と照らし合わせると、INT0の活性化時間が700nsに満たないため、割り込みルーチン開始時にはどう足掻いてもI/Oには間に合わない、ということになります。

やはりどうしてもZ80とAVRの間にはラッチが必要ということになりますね。


I/OによるZ80-AVRの通信テスト

| No Comments | No TrackBacks

レトロパソコンのインテリジェントな周辺機器を作るのに 大きな間違いに気づいてショックな今日この頃。 デコーダ・バッファは8255が入手困難な現在、手持ちのCPLD(Xilinx XC9536XL)があるのでそれでやってみようと思うのですが、困ったことにライタがありません。

ライタを作ろうにも、そのための部品が手持ちでは苦しいところです。 またしても鶏卵問題に陥りそうなのですが、悩んでいても埒が明かないので、とりあえずやれることからやってみることに。


まずは、テスト用に以前の回路を手直しして、以下のような簡単な回路を作ってみました。

20081213-Test.PNG

入出力に対応させてありますが、とりあえずそれは気にしないでin命令に応じてLEDを点滅させてみることにしました。

AVRのプログラムは以下のとおり。

.include	"m8def.inc"
.org	0
	rjmp	RESET
INTERRUPT:
	sbic	PORTB, 4	; 前回は点灯ならスキップ
	rjmp	LEDON
	sbi	PORTB, 4	; LEDを消灯
	reti
LEDON:
	cbi	PORTB, 4	; LEDを点灯
	reti
RESET:
	ldi	r24, low(RAMEND)
	out	SPL, r24
	ldi	r24, high(RAMEND)
	out	SPH, r24
	ldi	r24, 0x10
	out	DDRB, r24
	cbi	PORTB, 4	; 初期状態は点灯
	ldi	r24, 0x02
	out	MCUCR, r24	; 割り込みモード(INT0ダウンエッジ)
	ldi	r24, 0x40
	out	GICR, r24	; INT0割り込みイネーブル
	sei	      		; 割り込み許可
LOOP:
	rjmp	LOOP

LOOPでの無限ループ中に、割り込みがかかるとINTERRUPTルーチンが走ります(INT0のベクタアドレスに直接書いています)。 割り込み条件はINT0のダウンエッジです。

INT0は回路にあるように、IORQ | RD | A7 | A6なので、これらがすべて0のとき、すなわちBASICなどで

a=inp(0)
  

を実行すると点滅します。

ところが、不思議なことに放っておいても(BASICのテキストモードで何もしなくても)不規則に点滅します。

割り込みの条件から考えると、0x00~0x3fのI/Oリード要求により点滅するはずなのですが...。 何かおかしいんでしょうかねぇ。

I/O拡張回路の難しさ

| No Comments | No TrackBacks

以前設計した回路には、重大な盲点がありました。 2点あります。

  1. 割り込み要求線に^RD/^WRを入れておかないと、おかしなタイミングで割り込み要求がかかる
  2. Z80からのI/O要求に対する応答が間に合わない

割り込み要求線

以前の設計では、割り込み要求は次のようにしていました。

20081213-AVR1.png

IORQの確定とRD/WRの確定には時間差があるようで、どうもIORQの確定のほうが早いようです。 そのため、割り込み要求後にRD/WR線を調べないと本当にI/Oアクセスがかかっているのか分からないです。

そのため、INT線に入れる要求はRDまたはWRとORを取っておく必要があります。

20081213-AVR2.png

なお、デコードに常にA7を入れているのは、PC-6001の場合、I/Oポート0x80~はシステムによって予約されているためです。


I/O要求のタイミング

以前設計していたときは、Z80(約4MHz)のin/out命令のスピードが最低でも13クロックかかるため、8MHzのAVRなら26命令分の余裕があると思っていました。

ところが、これが大間違いだったのです。 I/Oの実際の信号が出るのは、命令読み込みではなく命令実行のタイミングのため、IORQが確定するのはかなり遅くなります。

もっとも高速なIN A, (n)やOUT (n), Aで考えても、M1ステートで命令を読み込み、M2ステートでI/Oアドレスを読み込むので、実際のI/O処理が行われるのはM3ステートのみです。

データシートを元に考えると、おおよそ以下の図のようになります。

IN A, (n)のM3ステート時の動作

20081213-Z80_RD.png

OUT (n), AのM3ステート時の動作

20081213-Z80_WR.png

すると、I/Oリクエストによる割り込みからデータ入出力まではZ80クロック基準で2.5クロック分くらいしか余裕がないのです。

これに対処するには、8MHzのAVRの割り込み処理ではとても追いつきません。 専用のデコーダと入出力用のデータバッファが必要になります。

ていうか、一般的にはこれは常識っぽいですよね。 8255がある理由がそうですから。 なんと間抜けなことをしたことか...。


Hexameterリリース

| No Comments | No TrackBacks

Hexameter 2.1.2をリリースしました.

Sourcceforgeからダウンロードできます.

変わったのは、SDOS 1.1用のテンプレートを用意したことだけですのでたいして大きな変化ではないですが、もしSDカードアダプタをお持ちの方でSDカードをアクセスしたいという奇特な(?)方がいらっしゃればぜひご利用ください. また、SDOS開発時に作成したいろいろ有用なルーチンも用意してますので、良かったら使ってみてください. 戦士カートリッジ版、1M ROMアダプタ版双方で動作するはずです.

Z80のC言語クロスコンパイル(SDCC)(5)

| No Comments | No TrackBacks
演算ライブラリについて
sdccには基本的な標準ライブラリが入っています。 その中には

stdio.h
stddef.h
stdlib.h
string.h  
など、C言語として当たり前のものもあるのですが、その他に、基本的な演算や浮動小数点なども含まれます。
たとえば、整数の掛け算や割り算をする場合も、関連するライブラリがリンク時に埋め込まれます。z80は標準で掛け算や割り算の命令を持たないので、これは仕方のないところです。

ただ、演算ライブラリは関数単位ではなくオブジェクト単位で埋め込まれるので、不要なものまで入ってしまうことがあります。

演算ライブラリのリンク例

たとえば、次のような簡単なプログラムをコンパイルしてみましょう。
-- test.c
long mul(long a, long b) {
  return a * b;
}
void main () {
  mul(654321, 123456);
}
-- 
今回のプログラムは試しやすいようにアーカイブを用意しました。

sdcc5.zip (sdccとMakeのできる環境(UNIX系のOSやCygwinなど)が必要です)

Makefileが入っていますのでmakeのみでコンパイル可能です。 まずは、アーカイブのsample1ディレクトリでmakeしてみてください。

Makefileにはいろいろ書いてありますが、要は次のようにしているのと同じです。

% sdcc -mz80 --code-loc 0 --no-std-crt0 test.c  
今回は話を簡単にするため、crt0は使っていません。

さて、これでオブジェクトのてtest.ihxのほか、マッピングファイルのtest.mapが出力されていると思います。
mapファイルは、グローバルシンボルがどこに配置されているか、各エリアのサイズがどのくらいかが示されています。

そのmapファイルを見てみましょう。

-- test.map(抜粋)
(中略)
Area                               Addr   Size   Decimal Bytes (Attributes)
--------------------------------   ----   ----   ------- ----- ------------
_CODE                              0000   090D =   2317. bytes (REL,CON)

      Value  Global
   --------  --------------------------------
     0000    _mul
     0000    _mul_start
     0032    _main
     0032    _main_start
     0032    _mul_end
     004A    __mullong_rrf_s
     004A    __mullong_rrx_s
     004A    _main_end
     004D    __modslong_rrf_s
     004D    __modslong_rrx_s
     0050    __modulong_rrf_s
     0050    __modulong_rrx_s
     0053    __divslong_rrf_s
     0053    __divslong_rrx_s
     0056    __divulong_rrf_s
     0056    __divulong_rrx_s
     0059    __mulint_rrf_s
     005F    __divsint_rrf_s
     0065    __divuint_rrf_s
     006B    __mulschar_rrf_s
     0071    __divschar_rrf_s
     0077    __muluchar_rrf_s
     007D    __divuchar_rrf_s
     0083    __modschar_rrf_s
     0089    __moduchar_rrf_s
     008F    __modsint_rrf_s
     0095    __moduint_rrf_s
     009B    __rrulong_rrf_s
     00A1    __rrslong_rrf_s
     00A7    __rlulong_rrf_s
     00AD    __rlslong_rrf_s
     00B3    __divschar_rrx_s
     00BA    __divschar_rrx_hds
     00C1    __modschar_rrx_s
     00C8    __modschar_rrx_hds
     00CF    __divsint_rrx_s
     00DB    __divsint_rrx_hds
     00E3    __modsint_rrx_s
     00EF    __modsint_rrx_hds
     00F7    __divuchar_rrx_s
     00FE    __divuchar_rrx_hds
     0105    __moduchar_rrx_s
     010C    __moduchar_rrx_hds
     0113    __divuint_rrx_s
     011F    __divuint_rrx_hds
     0127    __moduint_rrx_s
     0133    __moduint_rrx_hds
     013B    .div8
     013B    .mod8
     0143    .div16
     0143    .mod16
     0180    .divu8
     0180    .modu8
     0183    .divu16
     0183    .modu16
     01B8    __rrulong_rrx_s
     01D5    __rrslong_rrx_s
     01F2    __rlslong_rrx_s
     01F2    __rlulong_rrx_s
     020F    __modslong
     020F    __modslong_start
     02E8    __modslong_end
     02E8    __modulong
     02E8    __modulong_start
     03FF    __divslong
     03FF    __divslong_start
     03FF    __modulong_end
     04E1    __divslong_end
     04E1    __muluchar_rrx_s
     04F4    __mulschar_rrx_s
     04FB    __mulschar_rrx_hds
     0507    __mulint_rrx_s
     0513    __mulint_rrx_hds
     0513    __muluchar_rrx_hds
     052C    __divulong
     052C    __divulong_start
     0610    __divulong_end
     0610    __mullong
     0610    __mullong_start
     090D    __mullong_end
-- 
いかがでしょうか。test.c本体は0x0000~0x0049に配置されています。staticスコープではない関数はstartとendのシンボルが付加されます。_main_endなどの終了位置は終了アドレス+1を示します。

そして、0x004a~0x090cはすべて演算用モジュールです。見ると、divだのmodだの、関係ないものまでたくさん入っていて、そのサイズは実に2,243バイトに達します。

では、もし標準ライブラリをまったくリンクしないとどうなるでしょう。

% sdcc -mz80 --code-loc 0 --no-std-crt0 --nostdlib test.c
?ASlink-Warning-Undefined Global '__mullong_rrx_s' referenced by module 'test'
こうなると思います。つまり、掛け算で'__mullong_rrx_s'という関数を呼び出しているわけです。

この関数が定義されているのは、 SDCC/lib/z80/stubs.o というオブジェクトなのですが、実はこのオブジェクトで演算の本体が実装されているわけではありません。名前のとおりこのオブジェクトはただのスタブで、演算に関するありとあらゆるオブジェクトを参照するようになっています。

リンカはオブジェクト単位ですべての参照を解決しないと出力を生成しないため、スタブが参照しているオブジェクトをすべて埋め込む結果となってしまいます。

オブジェクトを追いかけてみる

ライブラリがリンクするオブジェクトを追いかけるためには、オブジェクトファイルの構造を知ることが必要ですが、ここでは最低限知っておけばいいことだけ挙げます。
先ほどのstubs.oを見ると、次のようになっています。

-- stubs.o (抜粋)
(中略)
S __muluchar_rrx_s Ref0000
S __mullong Ref0000
(中略)
S __rrulong_rrf_s Def0051
S __mullong_rrx_s Def0000
(中略)
-- 
最初の方にこのような"S"で始まる行がたくさんあります。これはシンボル定義で、シンボル名と参照・定義の種別を表しています。Refとあるのは他のオブジェクトで定義されているシンボルの参照、Defとあるのがこのオブジェクトで定義されているシンボルと定義されているアドレスです。

リンカはリンクしようとしたオブジェクトにRefで示されるシンボルがあると、それに対応するDefのシンボルが定義されているオブジェクトをリンクします。stubs.oは大量のRefがあるため、それに対応するDefのあるオブジェクトをすべてリンクするまではリンクが終了しません。

ソースを追いかけてみる

この問題を解決するためには、オブジェクトファイルを直接いじる手もあるのですが、幸いsdccにはソースがついているので、ソースを直接インポートしてしまうのが手っ取り早いでしょう。

ソースは、 SDCC/lib/src/z80/ に入っています。stubs.s を見ると、以下のようになっています。

-- stubs.s (抜粋)
__mullong_rrx_s::
__mullong_rrf_s::
        jp      __mullong
--   
何のことはない、__mullongに飛んでいるだけです。__mullongは

SDCC/lib/src/__mullong.c
に入っています。
そこで、stubs.sをコピーして、必要なmullong_rrx_s以外の定義を削除してしまいます。次に__mullong.cを個別にコンパイルしてリンクします。すると次は、

?ASlink-Warning-Undefined Global '__muluchar_rrx_s' referenced by module '_mullong'
というリンクエラーが出ます。この定義は

SDCC/lib/src/z80/mul.s
に入っているもっとも基本のライブラリなので、これもアセンブルするようにします。

これらを済ませたものがアーカイブのsample2ディレクトリに入っています。
このディレクトリでmakeしてみてください。リンクエラーは出なくなったと思います。

結果生成されたmapファイルを見てみると、

-- test.map(抜粋)
Area                               Addr   Size   Decimal Bytes (Attributes)
--------------------------------   ----   ----   ------- ----- ------------
_CODE                              0000   0395 =    917. bytes (REL,CON)

      Value  Global
   --------  --------------------------------
     0000    _mul
     0000    _mul_start
     0032    _main
     0032    _main_start
     0032    _mul_end
     004A    __mullong
     004A    __mullong_start
     004A    _main_end
     0347    __mullong_end
     0347    __mullong_rrf_s
     0347    __mullong_rrx_s
     034A    __muluchar_rrx_s
     035D    __mulschar_rrx_s
     0364    __mulschar_rrx_hds
     0370    __mulint_rrx_s
     037C    __mulint_rrx_hds
     037C    __muluchar_rrx_hds
-- 
今度はリンクされる関数がかなり少なくなりました。リンクされたライブラリの大きさは818バイトです。

__mullong.cを最適化したり、mul.sで使われていないルーチンを削ったりすることでもっとサイズを削減することも可能です。
まぁ、そのような場合は計算用ライブラリを個別に用意して、それを呼び出した方がいいかもしれません。

ちなみに、SDOSでは、以下については標準ライブラリは使わず、アセンブリ言語で独自の実装を行っています。

  • 乗算(16ビット x 16ビット = 32ビット)
  • 除算(32ビット / 8ビット = 32ビット)
どちらが絶対的によいと言うわけではないのですが、16ビットまでの大部分の演算はライブラリを呼び出さずにインライン展開されるようなので、必要な演算の種類やメモリ・速度の制約などのトレードオフを勘案して使い分ければいいと思います。

いま、PC-6001のPSGでPCM再生を試みています。
全体的にかなりいい感じで進んではいるのですが、クロック計算がなかなか合わないというのが困りものです。

カタログ上のPC-6001のクロック周波数は3,993,600Hzです。半端に見えますが、因数分解すると
3993600 = 2^12 * 5^2 * 3 * 13
となります。カセット入出力やRS-232Cなどで作られている1,200Hzの2の累乗倍で考えると、
3993600 = 1200 * 256 * 13
となり、4MHzのZ80のスペックに従いつつかなり切りのいい数字なんですね。

さて、PCM再生には正確なタイミングが不可欠です。これまでの実験で、とりあえずCD音質の44,1KHzは不可能ではないのですが、S/N比が悪くバランスが悪いことが判りました。なにせPSGの音量コントロールだと、4bitでの再生になりますからね。

その1/4の11.025KHzだと、音量に12bitを割くことができ、音質的にもかなりいい感じです。
ああ、この辺は説明してませんね。PSGを3チャンネル使うことにより可能になるんですが、私も理解が追いついていないのでこれはまた後ほどということで。

とりあえずこれで実験を続けてみました。計算だと、

3993600/11025 ~= 362.23

となります。まぁこの際小数点以下は無視するとして、3チャンネルのPSG音量をセットアップするのに362T(-state)かけられる、ということです。もちろん、外部影響を無視するため、VDGからのDMAはoffにし、割り込みも止めています。

ところが、これで再生すると、遅いんです。当然音程も低いんです。そりゃもう、一青窈が平井堅になるほどに(嘘)。

処理には余裕があるので、16Tほど削減してみました。すなわち346Tです。すると、まぁいい感じです。あ、今回はM1サイクルのT-stateをちゃんとカウントしています。
でも、完璧じゃないと言うか、やっぱり少しはずれるんです。大体、削減した16Tというのも適当に削減するウェイト用の命令を選んだだけで、なんら根拠はありません。
.......これは何かがあるに違いない、とは思うのですが、確実な原因はわからないのです。

いくつか気になることは以下のとおりです。
  • CPUの周波数が3993600Hzなのに対し、PSG(AY-3-8910)の周波数は3578545Hzです。そしてこれはMSXのCPU周波数と同じです。もっともPSGにクロックいっぱいいっぱいで出力を与えられるわけはないのですが。
  • 仮に周波数をPSGの持つ3578545Hzとして計算すると、362Tで出る周波数は約9885Hz、修正後の346Tで出る周波数は10343Hzです。
  • MSX版でPSG再生するプログラムを参考にしたのですが、そこでは
    out (c), r
    にかかるステートがなぜか15となっていました(M1 wait込み)。Z80の仕様上は12です。M1ウェイトを入れても13です。
    そして、そのMSXのCPU周波数はPC-6001のPSGと同じ、3578545Hzなのです。
どなたか教えて、Z80 Geekな人!

#注記
上記の記事を書いているときは、実は再生時にdiを忘れると言う大ポカをやってました。なので、そのときのタイミングはかなり当てになりませんでした。ただし、割り込みを禁止して実験してみたところ、やはり計算どおりには行かず、平井堅です。どうしたものでしょうね??

SDカードドライバの設計(5)

| No Comments | No TrackBacks
プリンタポートの場合

カードリーダをプリンタポートに接続する場合のコードを考えてみました。昔のパソコンの大部分は入力がBUSY信号のみですが、幸いなことにSPIで必要とする入力信号は1本なので、これで間に合います。

PC-6001のプリンタ関連のポートは以下のとおりです。
	0x91	data output
	0xc0	busy(bit1)
  
なお、回路では出力の際、インバータを通しているので、ビットは反転出力する必要があります。

では、早速コードを見てみましょう。例により、レジスタEを出力としています。
-- 
	ld	d, #0xff
_main:
	ld	a, d
	out	(#0x91), a
	dec	a
	out	(#0x91), a
	in	a, (#0xc0)
	add	a, d
	rl	e
	; _mainからを8回繰り返す
	ret
-- 
  
ここではハードウェアの設定として、以下を仮定しています。
  • CLKはビット0とします。
  • DIはビット1~7のどこでもかまいません。
ここでのトリックは、

	add	a, d
です。ポート0xc0からの入力は、ビット1以外は常に0なので、これで入力がある(a=0x02)場合は0xffを足すとキャリーが立ちます。しかも、レジスタDを使いまわせるので、利用するレジスタがA, F, D, Eの4つのみです。

結構これは重要で、このルーチンの外側でループを使ってバッファに書き込みを行うので、

	ld	(hl), e
  
のようにバッファへのポインタ(この場合HL)とループカウンタを利用します。ジョイスティックの場合はバイト入力でAFDEHLを使ってしまい、残りのレジスタは2つ。ループカウンタ用のレジスタをBにしてdjnzでまわしても、ぎりぎりレジスタが足りないので、実は裏レジスタを利用しています。惜しい...。

プリンタポートだとその必要がないのでexxと裏表のレジスタ転送の分が浮きます。バイトごとにこの処理が必要なので、1セクタあたり12T*512回分。合計6,144Tで、意外に馬鹿になりません。

もし、SDカードからの入力信号が1のときにポート0xc0の値が0になるようであれば、上記の代わりに

	ld	c, #0x02
を準備しておいて、

	sub	c
とするとよいでしょう。

T-stateは1bitあたり53、1バイトで441となります。裏レジスタを利用しなくて済む分も考慮して、1セクタあたりの時間は約260,000T、期待できる転送レートは3kB/s(VDG on)または6kB/s(VDG off)となります。前回よりさらに1.5倍のスピードになりますね。

なお、これは現在のSDカードアダプタを、ケーブル配線のみ変更すれば適用可能です。ただ、テストはしていませんのでご注意ください。

SDカードドライバの設計(4)

| 5 Comments | No TrackBacks
高速読み出しの工夫
第1回で説明したように、ジョイスティックポートからのデータ入力はかなり手間がかかります。

高速化は最もたくさん実行する部分から行うのが定石なので、心臓部であるバイト入力をする部分を最適化するのが最も効果的です。
(ポート0xa0)<-0x0f	レジスタ0x0fをラッチ
(ポート0xa1)<-0x10	DIを書き込み
(ポート0xa1)<-0x11	クロックを反転
(ポート0xa0)<-0x0e	レジスタ0x0eをラッチ
A<-(ポート0xa2)	データ読みこみ、ビット0にデータが入る
    
これで1ビット。1バイト入力するにも8倍かかります。これを限界まで高速にする挑戦をします。

最初のコード

まず、Z80の入出力は

in	a, (n)
out	(n), a
    
がそれぞれ11 T-state(以下11Tと表記します)で最速ですので、これを使います。
同じポートから連続してバイト列を読み取りたい場合などは
ini
inir
outi
otir
でブロック転送するのがいいのですが、ここでは残念ながら適しません。
入力結果はレジスタeに入れると仮定します。
-- 
	ld	e, #0x00	; 結果
	ld	b, #0x08	; ループカウンタ
_loop:
	sla	e
	ld	a, #0x0f
	out	(#0xa0), a
	ld	a, #0x10
	out	(#0xa1), a
	ld	a, #0x11
	out	(#0xa1), a
	ld	a, #0x0e
	out	(#0xa0), a
	in	a, (#0xa2)
	and	a, #0x01
	jr	nz, _zero
	set	0, e
_zero:
	djnz	_loop
	ret
-- 
  
最初は上記のような感じでした。実際には入力ポートの判別を動的にやっていたので、もっと複雑だったのですが。
上記のコードのT-stateを計算すると、1bitあたり123T~126Tで、1バイトあたり1,003T~1,027Tとなります。

PC-6001のM1サイクルにはウェイトがあるらしいです。
海外のMSX関連の資料(たとえばこことか)を見たところ、やはりM1に1サイクル追加されるようです。よって、すべてのオペレーションのT-stateはZ80の仕様のドキュメントにあるT-stateから1増加させた数値になります。おそらくPC-6001も同じだと思われます。
ただし、この記事の中の数値はM1ウェイトを入れていない数値です。
これにセクタリード用のコマンド送信やらループ管理やらを入れて、1セクタ(512バイト)のリードにかかる時間はおよそ580,000T、実効速度2MHzとすると290msecということになります。実際にはさらにFATの管理コードの実行時間があるので、もっとかかります。

最適化

ここで、時間が短縮できるものがあります。それが以下の3種類です。
  1. Aレジスタのセットアップ
    上記のソースでは、いちいちAレジスタに即値代入をしていましたが、これは7Tかかります。あらかじめ別のレジスタに値を準備しておけばレジスタ間転送で4Tで済みます。
  2. Eレジスタの値の代入
    最初はandを使ってゼロフラグ経由で入力を判断していました。しかしうまいことに、入力がアキュームレータの第0ビットに入ってくるので、4Tのrrca一発でキャリーフラグに結果を追い出せます。また、8回シフトするのでEレジスタへ最初の0代入は実は不要です。
  3. ループ
    ループを展開すればサイズは大きくなりますが、djnzの分の時間は浮きますね。
というわけで、次のようなコードになりました。
-- 
	ld	hl, #0x0e0f
	ld	d, #0x10
_main:
	ld	a, l
	out	(#0xa0), a
	ld	a, d
	out	(#0xa1), a
	inc	a
 	out	(#0xa1), a
	ld	a, h
	out	(#0xa0), a
	in	a, (#0xa2)
	rrca
	rl	e
	; _mainからを8回繰り返す
	ret
-- 
  
inでデータを読み取ったあと、rrcaでビット0をキャリーに追い出して、それをeのビット0から入れています。
これでT-stateは1bitあたり83、1バイトあたり691と、最初のコードの70%ほどになります。さらに外部でも工夫して、1セクタあたり約390,000T、2MHzで200msec以下となりました。

計算上ではなくて実測してみると、およそ2kB/s出ているので、そちらから考えれば1セクタ250msです。FAT管理などのオーバヘッドを考えるとおよそ妥当なところでしょう。

なお、よく知られているようにVDGからのBUSREQを止めると、画面は乱れますが速度は約2倍、約4kB/sとなります。

ちなみに、バイト出力はもっと高速です。inする必要がなく、レジスタの再ラッチが不要なためです。

もしプリンタポートで入出力したとすれば、やはり同じ理由で入出力が少し高速になるでしょう。ただし、プリンタポートには電源が来ていないので、ジョイスティックポートよりも物理的な接続が面倒になります。

これがバイト入力に関して私が考え付いた限界です。もし上記でもっと最適化、高速化できる方法があったら教えてください。


Z80のC言語クロスコンパイル(SDCC)(4)

| No Comments | No TrackBacks
インラインアセンブルコード
sdccのCコンパイラの出力コードの効率があまりりよくないことはお話しました。 そのため、インラインアセンブルを使うことも多いでしょう。 インラインアセンブルの利用は、他のコンパイラ同様、以下のような利点があります。
  • 特定の部分のみ高速化できます。
  • Cプリプロセッサが働くので、#defineの定義が利用できます。
インラインアセンブルコードは、関数内に__asm~__endasm;で囲んで書きます。例えば、次のようになります。
-- 
static char *message;

void func() {
  // 他のコード...

__asm
	ld	hl, #_message
	call	_show_message
_endasm;

  // 他のコード...
}
-- 
  
この例では、アセンブルコードからCコードのmessageを参照しています。 ローカル変数の参照も出来ないことはないのですが、第2回で説明したように、スタック上の位置が宣言の順番に依存するので、避けた方がいいと思います。

警告の抑制

インラインアセンブラを使ってコードを書くと、

myprog.c:10: warning 85: in function myfunc unreferenced function argument : 'arg'

という警告が出てしまうことがあります。これは、関数の引数をアセンブルコード内のみで利用し、Cコード内で参照しないためです。sdccは__asm~__endasm;で囲まれた部分はそのままアセンブラに渡し、一切検査しません。

この警告が気になるなら、#pragmaで抑制することができます。
-- 
#pragma save
#pragma disable_warning 85
void func(char* arg) {
__asm
  // アセンブルコード...
__endasm;
}
#pragma restore
-- 
  
#pragma save
でこれまでのpragma情報をいったん保存し、
#pragma disable_warning 85
でwarning 85の出力を抑制します。そのままだとこれ以降ずっとwarning 85が出なくなってしまうので、関数終了時に
#pragma restore
でpragma情報を元に戻します。

naked関数

アセンブルコードで関数本体を書いても、第2回で説明したようなixのバックアップ~リストアは出力されます。場合によってはこれが不都合なこともあります。そのため、__nakedという予約語があります。この予約語がついた関数は、一切のプリアンブルコードが出力されません。retすらしません。そのため、全て自分で書く必要があります。
-- 
void func(char* arg) __naked {
__asm
  // アセンブルコード...
  // retも自分ですること
__endasm;
}
-- 
  
インラインアセンブラTips: 関数ポインタの定数化
sdccでは、コードセグメントにおける関数ポインタの定数化ができません。 たとえば、以下のようなコードがあったとします。
-- 
char func1(int x) { ... }
char func2(int x) { ... }
char func3(int x) { ... }

const char (*functions[])(int) = {
  func1, func2, func3
};
-- 
  
ここでfunctionsは定数ですので、コードセグメント内に配置して欲しいところなのですが、sdccはfunctionsをデータエリアに置き、メンバの初期化をGSINIT内に出力します。そのため、非常に効率の悪いコードになるだけでなく、本来の意味での定数ではなくなっています。

これを、naked関数を使って解決する手段があります。 次のように書きます。
-- 
char func1(int x) { ... }
char func2(int x) { ... }
char func3(int x) { ... }

extern char (*functions[])(int);
static unsigned char* __functions() __naked {
__asm
_functions::
	.dw	#_func1
	.dw	#_func2
	.dw	#_func3
__endasm;
}
-- 
  
お分かりでしょうか。C言語からはfunctions外部変数を参照するようにしておいて、実はnaked関数内で定義しています。 なお、スコープをstaticにすることも可能です。その場合は、
_functions:
と、コロンをひとつにします。

上記の場合、メンバとなるfunc1, func2, func3の定義がプロトタイプ宣言と異なっていてもコンパイルエラーは出ません。これは良し悪しで、わざと違う定義にすることもあるかもしれません。たとえば、呼び出し側は常に引数を渡すようにしているが、呼び出された側はその引数を必要としないので、シグネチャに入れない、などということも考えられます。

これを柔軟性と見るか、悪い設計だと見るかは、アプリケーションに依存するでしょうね。

Z80のC言語クロスコンパイル(SDCC)(3)

| 2 Comments | No TrackBacks
リンクについて

sdccでは他のCコンパイラと同様、ライブラリを作って、それをリンクすることができます。ここでは、通常のスタティックリンクと、外部プログラムとリンクするテクニックについて説明します。

ライブラリの作成とリンク

ライブラリはsdcclibというコマンドで作ります。

% sdcc a mylib.lib prog1.o prog2.o

このようにして作ったライブラリをリンクするときに、

% sdcc -mz80 --out-fmt-ihx --no-std-crt0 -o myobj.ihx crt0.o -lmylib.lib

のようにします。ここでcrt0.oは初回に説明したcrt0モジュールです。 sdcclibコマンドはオブジェクトファイル(上記ならprog1.oとprog2.o)の中身をライブラリファイル内にインポートするので、作成後はオブジェクトファイルを消しても大丈夫です。

オブジェクトファイルを消さないなら、sdcclibを使わなくてもライブラリファイルは作れます。そのときのライブラリファイルの中身は、単にファイル名を並べるだけです。
-- mylib.lib prog1.o prog2.o --
このファイルを用意して、同じように

% sdcc -mz80 --out-fmt-ihx --no-std-crt0 -o myobj.ihx crt0.o -lmylib.lib

でリンクできます。mylib.libファイルと同じディレクトリにprog1.oとprog2.oが必要です。

外部プログラムとのリンク

既に作成されたプログラムがメモリ上に存在して、それを利用したい場合は、わざわざ同じルーチンをリンクするのはメモリのムダですね。ここでは、そのような外部プログラムをライブラリとしてリンクするという小技を紹介します。出力にすべてのライブラリコードを入れなくて済むので、コード量が小さくなります。

拙作のSDOSのアプリケーションを作る際は、この方法をとることができるようにしてあります。また、BIOSルーチン(BASICインタプリタなど)などがある場合にも活用できます(ただし、前回説明したsdccの関数呼び出し規約に注意)。

準備

外部プログラムをライブラリとしてリンクできるようにするため、リンク対象となるプログラムはヘッダファイルをきちんと準備しておきます。 ここではSDOSのヘッダ(の一部)を例として挙げます。
-- conio.h void libputc(unsigned char ch); void libputs(char* str); --
もちろん、conio.hの実装も作成します(ここでは省略)。

symファイルからアセンブリファイルの作成

上記の実装プログラムがコンパイル、リンクされると、symファイルというものが生成されます。このファイルは、リンクによって生成されたシンボルがどのアドレスに配置されたかの情報が入っています。
-- sdos.sym (一例) 01:5000 _libputc 01:5010 _libputs --
この情報をもとに、アセンブリファイル(.Sファイル)を作ります。
-- sdos.S .area _SDOSLIB (ABS) .org 0x5000 _libputc:: .org 0x5010 _libputs:: --
アプリケーションの作成 先ほどのconio.hを利用したアプリケーションを作ります。
-- main.c #include "conio.h" void main() { libputc("Hello World"); libputc('\n'); } --
main.cとsdos.S(とcrt0.S)をコンパイル、アセンブルすると、関数libputc、libputsの実装は存在しなくてもリンクすることができます。

ここでのポイントは、sdos.Sでのエリア定義(_SDOSLIB)が絶対アドレス指定(ABS)になっていることです。これにより、main.cをリンクするときに定義されているアドレスが利用されます。 当たり前ですが、リンク対象のプログラムを変更するとアドレスが変わってしまい、アプリケーション側もsdos.Sの再アセンブルと再リンクが必要となるので注意してください。

symファイルからアセンブリファイル作成の自動化

symファイルからアセンブリファイルを作るのはperlで簡単にできます。以下に実例を示します。
-- mklib.pl #!/usr/bin/perl print ".area _CODE2 (ABS)\n"; while (<>) { if (/^\d\d:([0-9A-F]{4})\s(\w+)/) { if ($2 =~ /^s__/ || $2 eq "_main" || $2 eq "gsinit" || $2 eq "_datastart" || $2 eq "_dataend") { } else { print ".org 0x$1\n"; print "$2::\n"; } } } --
上記を使い、

% perl mklib.pl sdos.sym > sdos.S

とすることにより、sdos.Sを生成することができます。なお、このperlスクリプトでは、リンクされるプログラムにあるシンボルのうち、以下を除外しています。
  • _main
  • gsinit
  • _datastart
  • _dataend
  • s__で始まるシンボル(各エリアのスタート位置を示す)
これらは、すべてのプログラムに存在する可能性があるため、sdos.Sに存在すると二重定義になってしまいます。また、アプリケーション作成時にリンクする意味もないでしょう。

Z80のC言語クロスコンパイル(SDCC)(2)

| 1 Comment | No TrackBacks
sdccを使うことによりC言語でプログラムが可能になりますが、実際にはアセンブリ言語と一緒に使うことが多いと思います。理由は、
  • sdccの出力するコードは速度的にも容量的にも効率が悪い
  • 直接BIOSやI/Oポートにアクセスすることがある
 などです。そのため、ここではsdccが出力する関数の構造についてみてみます。

関数の構造

簡単な関数の実装から、例を見てみましょう。

char func(char *arg) {
  char *str;
  unsigned char i;

  // 関数本体

  return i;
}
  
という関数を例にとって見ましょう。sdccでは、この関数を次のような構造で出力します。

_func_start::
_func:
	; ixのバックアップ
	push	ix
	; ix = sp
	ld	ix, #0
	add	ix, sp
	; sp = sp - 3
	ld	hl, #-3
	add	hl, sp
	ld	sp, hl

	; 関数本体

	; sp = ix
	ld	sp, ix
	; ixを戻す
	pop	ix
	ret
_sdos_openFile_end::
  
まず、最初にixをバックアップします。このレジスタはスタックポインタをバックアップするために使います。そのため、ixにspを代入します。

次に、spを3減じています。これは、関数内で利用するローカル変数のためのヒープを確保するためです。この関数では、str(2バイト)、i(1バイト)をローカル変数として使っているので、その分の領域をスタックに確保しています。

 この関数が呼び出されたときにSPが0xA200だとすると、この時点でのメモリは次のようになっています。

sdcc1.PNG 関数の最後では、spを保存されていたixに戻してretします。

関数呼び出し

次に、関数呼び出しです。先ほどのfunc内に、次のようなfunc2の呼び出しがあるとしましょう。

unsigned char func2(char *, char);
  
呼び出すためのCコードは次のようなものだとします。

i = func2(str, 2);
  
この部分の出力の様子を見てみます。

	; 引数2をスタックに入れる
	ld	a,#0x02
	push	af
	inc	sp
	; 引数strをスタックに入れる
	ld	l, -2(ix)
	ld	h, -1(ix)
	push	hl
	; 呼び出し
	call	_func2
	; 結果の退避(この部分は呼び出しによって異なる)
	ld	c, l
	; スタックを戻す
	pop	af
	inc	sp
  
まず、Aに2を代入してスタックにpushします。この引数は1バイトなので、inc spして不要なスタックを削除します。次に、HLにstrを代入してpushします。

callする直前のメモリマップは次のようになります。

sdcc2.PNG
これを見て判るように、関数呼び出しの際に与える引数は、後ろから積んでいきます。これにより、呼び出し先では最初の引数がより低いアドレスに格納されています。

呼び出し結果ですが、この関数呼び出しは返り値が1バイトで返ってきます。その場合、結果はLレジスタに入っています。いったんそれをCレジスタに退避したあと、不要となった引数スタックを捨てています。pop afで引数str(2バイト)を捨て、次のinc spで引数2(1バイト)を捨てています。これでスタックの状態が元に戻ります。

アセンブリ言語による関数作成

アセンブリ言語でC言語から呼び出される関数を作成する場合、以下に注意します。
引数の格納位置

引数は前述のように最初の引数から順番に格納されています。実際にはcallされているので、スタックの先頭はリターンアドレスになっています。そのため、一般的なアクセス方法は次のようになります。
	ld	hl, #2
	add	hl, sp
	; (HL)に最初の引数が入っている
  ; 上記のfuncの呼び出しの例で言うとHL=0xa1fa
壊してはいけないレジスタ

上述のように、sdccの関数ではixレジスタをspの保存に使っています。そのため、呼び出し先でixを破壊してしまうと戻ってからの処理がおかしくなってしまいます。必要な場合は、作成する関数の最初でpush ixし、戻る直前でpop ixしましょう。

sdccは命令ごとにレジスタを初期化しなおすので、その他のレジスタは基本的に使っても問題ありません。これが、sdccのコンパイルコードの効率が悪い理由でもあります。

結果の格納

関数呼び出しの結果ですが、結果の受け渡しにはスタックポインタは使わず、レジスタを使います。結果の大きさに応じて次のようにレジスタが決まっています。

1バイト(char)
Lレジスタ
2バイト(int, short, ポインタ(char*など))
HLレジスタ
4バイト(long)
DEHLレジスタ、DEに上位16ビット、HLに下位16ビットを入れる

型に応じて上記レジスタに結果を格納してretします。

Z80のC言語クロスコンパイル(SDCC)(1)

| No Comments | No TrackBacks
UNIXやWindows環境で利用できるフリーのZ80対応クロスC言語コンパイラとして、現在利用可能なのはSDCCくらいしかありません。

このコンパイラに関する日本語のドキュメントはまだほとんどないと思われますので、私が知っている限りで説明しておこうと思います。

コンパイルとリンク

sdccの使い方は、gccなどのコンパイラとそれほど変わりませんが、典型的な使い方を挙げます。

(各プログラムをコンパイル、アセンブル)
% sdcc -mz80 -c myprog1.c -o myprog1.o
% sdcc -mz80 -c myprog2.c -o myprog2.o
% as-z80 -o mycrt0.o mycrt0.S
(リンク)
% sdcc -mz80 --out-fmt-ihx --code-loc 0x840f --data-loc 0 --no-std-crt0 -o myprog.ihx mycrt0.o myprog1.o myprog2.o

mycrt0.Sはこの後説明する独自のスタートアップコードです。リンク時はこのオブジェクトを最初に指定しないとリンクの順番がおかしくなるので注意してください。

なお、このリンク方法でうまくihxが生成されないことがしばしばあります。おそらくバグだと思いますが、その場合、lnkファイルが生成されているので、続けてlink-z80を実行するとうまくいきます。

(リンクはされていなくても、myprog.lnkは作成されている)
% sdcc -mz80 --out-fmt-ihx --code-loc 0x840f --data-loc 0 --no-std-crt0 -o myprog.ihx mycrt0.o myprog1.o myprog2.o
(リンクしてihx作成)
% link-z80 -nf myprog

link-z80にはコマンドモードからの入力オプション(-c)がありますが、これは標準入力からの行単位入力を要求するので注意してください。コマンドライン引数では指定できません。

なお、アセンブラ(as-z80)では、オプションをソースファイルの後ろに指定できませんので注意してください。

sdccのオプション

sdccの基本的なオプションは他のコンパイラと同じですが、特殊なオプションがいろいろあるので、ここではその中でよく使うもののみ挙げます。

コンパイルオプション

-c
コンパイルのみでリンクしない。
-o
出力ファイル指定
-I
インクルードディレクトリ
-D
define定義の指定
コンパイルオプション(sdcc固有)

-mz80
コンパイル対象をz80に指定します。他にも-mhc08などいろいろあるので、Z80プログラムのコンパイルには必須です。
--std-c99
C99標準に準拠します。sdcc 2.8.0では完全ではありません。関連するオプションとして他に、--std-c89, --std-sdcc89, --std-sdcc99があります。sdccがつくオプションは、独自拡張を使うことができます。
リンク時オプション

-l
ライブラリファイルの指定です。ライブラリファイルはsdcclibで作成することもできますし、対象ファイル名を並べただけでもokです。あとで実際の使い方を説明します。
-L
ライブラリファイルのディレクトリ指定です。-lにはディレクトリを指定できませんので、カレントディレクトリ以外にライブラリファイルがある場合は必要です。
リンク時オプション(sdcc固有)

--code-loc
コードセグメント(.area _CODEおよび_GSINIT)の開始番地を指定します。_GSINITは_CODEに続いて配置されます。
--data-log
データセグメント(.area _DATA)の開始番地を指定します。0を指定すると、コードセグメントの直後に配置されます。
--out-fmt-ihx
リンク後のオブジェクトの出力フォーマットをIntel hex形式にします。これを付属のmakebinや拙作のHexameterに与えることにより、バイナリファイルを作成します。
--no-std-crt0
標準のスタートアップコード(crt0.o)を使わない指定です。以降で詳しく説明します。

crt0のリンク

sdccをインストールしてデフォルトでコンパイルすると、付属しているcrt0.oをリンクします。これが0番地に必ずinitにジャンプするスタートアップを入れたり、PC-6001や他のプラットフォームで利用するにはそのままでは使いにくいので、変更する必要があります。
しかし、ソースコードのcrt0.sはいろいろ示唆的なので、構造を知っておくと役立ちます。

デフォルトのcrt0.s

デフォルトのcrt0.oのソースコード(crt0.s)を説明します。
	; モジュール名を定義します。
	; スタートアップコードでも、任意の名前が使えます。
	.module crt0
	; C言語のmain関数への参照です。
	; アセンブラでは、C言語の名前の頭に"_"をつけます。
	.globl	_main

	; エリア名を定義します。
	; エリア名は任意ですが、いくつか典型で使われるものがあります。
	; (ABS)は、絶対アドレス指定になります。
	.area	_HEADER (ABS)

	; .orgは次からの命令の格納アドレス指定です。
	; ここからRST 0~RST 0x38までのベクタを定義しています。
	; デフォルトではすべてretiになっています。

	.org	0
	jp	init
	.org	0x08
	reti
	.org	0x10
	reti
	.org	0x18
	reti
	.org	0x20
	reti
	.org	0x28
	reti
	.org	0x30
	reti
	.org	0x38
	reti

	; 初期化ルーチンが0x100からになります。0番地からここに飛んできます。
	.org	0x100
init:
	; スタックポインタの初期化。
	ld	sp,#0xffff
	; グローバル変数初期化ルーチンを呼び出します。
	call	gsinit
	; メインルーチンを実行します。
	call	_main
	; 終了処理へ飛びます。
	jp	_exit

	; 以下はリンカに対してエリアの順序を指示しています。
	; sdccではエリアごとに別のメモリ領域を指定することができます。
	; メモリアドレスは互いに重なってもいいので、64kBを越えるプログラムを扱うことも可能です。
	; ただし、バンク切り替えなどが必要な場合は自分で処理する必要があります。
	.area	_HOME
	.area	_CODE
	.area	_GSINIT
	.area	_GSFINAL
	.area	_DATA
	.area	_BSS
	.area	_HEAP

	; ここからはコード領域です。
	; リンカに対して--code-locで指定したアドレスから配置されます
	.area	_CODE
__clock::
	ld	a,#2
	rst	0x08
	ret
	; 終了処理。エミュレータ用のコードのようです。
_exit::
	;; Exit - special code to the emulator
	ld	a,#0
	rst	0x08
1$:
	halt
	jr	1$
	; ここからはグローバル変数の初期化です。
	; コード内に初期化が必要なグローバル変数があると、初期化コードが自動的にここに配置されます。
	.area	_GSINIT
gsinit::
	; 初期化コードは各ソースから出力されて連続した領域となります。
	; 自動的にリターンしないので、ここで別エリアを定義してretします。
	.area	_GSFINAL
	ret

なお、ラベルはコロンがひとつの場合は.globl定義がない限りローカル、コロンが二つの場合は常にグローバルになります。たとえば上記では、initはローカル、gsinitはグローバルです。ただし、アセンブラのas-z80で-aオプションをつけると、すべてのラベルがグローバルになります。

crt0の入れ替え

さて、標準のcrt0.oは使えないので、入れ替えます。
sdccには、「グローバル変数がデフォルトで0に初期化されない」というC言語の仕様に沿っていない部分があるので、そこも修正します。
	; モジュール名とグローバル宣言は一緒です。
	.module	crt0
	.globl	_main

	; リンカへのエリア指定ですが、使うものだけでも構いません。
	; コード(_CODE)、初期化ルーチン(_GSINIT)、データ(_DATA)はsdccで出力するので必須です。
	; _GSFINALは初期化ルーチンから戻るために必須です。_DATAFINALは別に必須ではないのですが、
; 大域変数の初期化時にデータ領域の終了アドレスが必要なので入れています。 .area _CODE .area _GSINIT .area _GSFINAL .area _DATA .area _DATAFINAL ; コードセグメントの最初の部分です。 ; 0番地から飛んできませんが、リンカの--code-loc指定でここから開始するので問題ありません。 .area _CODE init:: ; グローバル変数が入るデータエリア(_datastartから_dataend-1まで)を0で初期化します。 ld hl, #_datastart ld bc, #_dataend _clear_loop: ld a, h sub b jr nz, _clear_next ld a, l sub c jr z, _clear_exit _clear_next: ld (hl), #0 inc hl jr _clear_loop _clear_exit: ; グローバル変数初期化ルーチンを呼び出します。 call gsinit ; メインに飛びます。 ; メインがretするとプログラム自体の呼び出し元に返ります。 jp _main .area _GSINIT gsinit:: .area _GSFINAL ret .area _DATA _datastart:: .area _DATAFINAL _dataend::
上記のcrt0.Sでは、絶対アドレス指定を一切使っていないので、アドレスはすべてリンカで指定することになり、ihxファイルを作るまではリロケータブルです。

プログラムによっては、グローバル変数の初期化が不要な場合もあるでしょう。そのような時は、init~_clear_exitの間のコードとcall gsinit、そして_GSFINALのretを削ることができ、メモリ削減になります。C言語から出力された_GSINITはけっこう効率の悪いコード なので、グローバル変数の宣言時に値を代入するより、個別にプログラムしたほうが効率がいいこともよくあります。

このあたりのテクニックはいろいろとあるのですが、少しづつ説明していきたいと思います。

上記のcrt0.sをas-z80でアセンブルしておくことにより、複数のCプログラムをリンクして、PC-6001や他のZ80ベースのパソコンでも使えるようなバイナリを作成することが可能となります。