N-BASIC(PC-8001)の内部ルーチンをSDCCで呼ぶ

PC-8001

前回はPC-8001用マシン語プログラムを、パソコン上でクロスコンパイルする環境整備の話題を書きました。今度はSDCCのCの関数からN-BASICの便利な内部ルーチンを呼ぶ方法をまとめてみます。

N-BASICの内部ルーチンを呼びたい

前回コンパイルしたSDCCのC言語サンプルを見るとわかりますが、VRAMに直接アクセスして文字列を書き込んでいます。もうちょっと楽したい、ということでみんな大好きN-BASICの内部ルーチンを呼び出して文字表示、カーソル移動などなどができると素敵なはず。

N-BASICの内部ルーチンの呼び出し方

N-BASICの内部ルーチンは、過去いろいろな解析が実施されてまして、秀和システムトレーディング刊の「PC-8001 – マシン語活用ハンドブック 初級編」などでも色々な内部エントリーポイントが紹介されています。

ネットの情報だとEnriさんとこの以下の「PC-8001コーナ」の「N-BASIC」のページが一番まとまってるかもです。

NBAS

このなかで、「カーソル位置X,Yを指定して文字を出力」という以下のルーチンがあります。これをSDCCのC言語の関数にしてみたいと思いました。PC-8001の画面の好きな位置に文字が出せて楽しそう。

002D7H CRT1文字表示 メイン
 入力: H=カーソルX座標(左端が01)
     L=カーソルY座標(上端が01)
     A=アスキーコード
 出力: B=アスキーコード
 使用:AF、BC、HL
    0EA5BH アトリビュートコード
    0EA61H カラー/白黒モード 0FFH=カラー、000H=白黒
    0EA65H 白黒モードのときの1行の文字数 050H=80文字
                           048H=72文字
                           028H=40文字
                           024H=36文字

SDCCでC関数の呼び出し方

N-BASICの内部ルーチンを呼び出すプログラムは関数化していろいろなところから使いたいものです。まずは、SDCCが関数を呼び出す方法について、調べました。

前述の「CRT1文字表示」を行う以下のようなC言語ルーチンに適宜プログラムを前後に追加してコンパイルしてみました。

    uint8_t x = 10;  // 表示する画面の横座標
    uint8_t y = 5;   // 表示する画面の縦座標
    char c = 'A';    // 表示するキャラクタ

    moveCursorAndPrintChar(x, y, c);  // 関数を呼び出し

SDCCはコンパイルの過程でZ80のアセンブラソースも吐き出しまして、呼び出し部分はこんな感じでした。関数呼び出しパラメーターは3つ。今回は全部8bit幅のデータを指定してます。

                                     55 ;../sample2.c:11: moveCursorAndPrintChar(x, y, c);  // 関数を呼び出し
    00000003 3E 41            [ 7]   56 	ld	a, #0x41
    00000005 F5               [11]   57 	push	af
    00000006 33               [ 6]   58 	inc	sp
    00000007 2E 05            [ 7]   59 	ld	l, #0x05
    00000009 3E 0A            [ 7]   60 	ld	a, #0x0a
    0000000B CDr00r00         [17]   61 	call	_moveCursorAndPrintChar
  • 第三パラメーター c : 表示文字 = ‘A’
    • Z80 Aレジスタにセットして、スタックにpushしてスタックポインタ(SP)を+1して戻し。
  • 第二パニメーター y : 表示位置 画面行数 = 5
    • Z80 Lレジスタにセット
  • 第一パラメーター x : 表示位置 画面桁数 = 10
    • Z80 Aレジスタにセット

関数呼び出しパラメーターが2つまでならAレジスタとLレジスタを使い、それより多い場合はスタックに積む仕様みたいです。

マニュアルによると関数呼び出し規約は?

上記仕様はSDCCのマニュアルに出てまして、以下のユーザーガイドの「4.3.3.1 Z80 SDCC calling convention, version 1」に出てまして、以下の矢印がたくさん書いた図が分かりやすいのです。

https://sdcc.sourceforge.net/doc/sdccman.pdf

呼び出し規約 Ver.1 だと、まさに 8bitの場合は Aレジスタ → Lレジスタ → スタック という順番で使われる模様です。16bitのデータの場合はHLレジスタ → DEレジスタ → スタックという感じ。

ちなみに、呼び出し規約 Ver.1は今回使用しているSDCC v4.5.0のデフォルトのようで、以前のバージョンのコンパイラだと規約 Ver.0というものらしく 変数は 全部スタック領域に積んでいた模様です。C言語だけでコードしている分には意識しないところですが、今回のように呼び出し先関数でインラインアセンブラでデータを受ける場合は呼び出し規約が変わっちゃうと困るところであります。

C関数側の実装方法

C言語の関数実装

C関数側ではPC-8001の内部ルーチン「CRT1文字表示」を呼び出すために、以下のパラメーターをセットしたいところです。

レジスタ設定内容
HレジスタカーソルX座標(左端が01)
LレジスタカーソルY座標(上端が01)
Aレジスタアスキーコード

前述のSDCCの関数呼び出し規約と見比べると、第一パラメーター「カーソルX座標」のデータがAレジスタに入ってきていて、第三パラメーター「表示文字」のアスキーコードがスタックに入っているところをいい感じでセットしてあげると良さそう…ということで、こんな感じのC言語の関数(といっても、ほぼインラインアセンブラ)を書いてみました。一応念のため関数定義の横に __sdcccall(1) と書いて「呼び出し規約 v1です」というところを明示してみました。

// カーソル移動と1文字表示を行う関数
void moveCursorAndPrintChar(uint8_t x, uint8_t y, char c) __sdcccall(1){
   __asm
    ld h,a
    push hl
    ld  hl,#4
    add hl,sp
    ld a,(hl)
    pop hl
    call CURSOR_MOVE_AND_PRINT_ROUTINE
   __endasm;  
}

ちなみにインラインアセンブラの処理は以下の通りです。

  • Aレジスタを、N-BASICの内部ルーチンの呼び出し仕様に合わせてHレジスタへセット
  • HLレジスタをスタックへpush
  • HLレジスタにスタックの変位分 4バイトをセット
  • HLレジスタにスタックポインタアドレスを加算して、スタック上のデータを特定
  • HLレジスタが指している位置のデータをAレジスタへ
  • N-BASIC内部ルーチンを呼び出し

コンパイルしてみた

上記関数をコンパイルしてみると以下のようなアセンブラコードがでてきました。

                                     75 ;../n_basic.c:33: void moveCursorAndPrintChar(uint8_t x, uint8_t y, char c) __sdcccall(1){
                                     76 ; ---------------------------------
                                     77 ; Function moveCursorAndPrintChar
                                     78 ; ---------------------------------
    0000000F                         79 _moveCursorAndPrintChar::
                                     80 ;../n_basic.c:42: __endasm;  
    0000000F 67               [ 4]   81 	ld	h,a
    00000010 E5               [11]   82 	push	hl
    00000011 21 04 00         [10]   83 	ld	hl,#4
    00000014 39               [11]   84 	add	hl,sp
    00000015 7E               [ 7]   85 	ld	a,(hl)
    00000016 E1               [10]   86 	pop	hl
    00000017 CD D7 02         [17]   87 	call	0x02D7
                                     88 ;../n_basic.c:43: }
    0000001A E1               [10]   89 	pop	hl
    0000001B 33               [ 6]   90 	inc	sp
    0000001C E9               [ 4]   91 	jp	(hl)

上記の89行目〜91行目はSDCCが自動生成した戻りのコードで、HLレジスタに呼び出し下アドレスをpopして取得し、スタックポインタに残った1バイト分のデータをスキップするため+1して、HLレジスタのアドレス(戻りアドレス)にジャンプするというコードです。ちゃんと自動生成コードでスタックの後始末もしてくれていて、いい感じです。

サンプルプログラムを作って動かしてみた

サンプルC言語ソースとN-BASIC内部ルーチン呼び出しのソース

N-BASIC内部ルーチンについて、「CRT1文字表示」のほかに「カーソルセット」、「画面クリア」、「1文字出力」の関数も実装して、以下のようなサンプルのCプログラムを作ってみました。

#include "n_basic.h"
#include <string.h>

int main() {
    screen_clear(); 

    uint8_t x = 10;  // 表示する画面の横座標
    uint8_t y = 5;   // 表示する画面の縦座標
    char c = 'A';    // 表示するキャラクタ

    moveCursorAndPrintChar(x, y, c);  // 関数を呼び出し
    
    locate(12,5);
    c = 'B';
    put_c(c);

    locate(20,6);
    put_c('C');

    locate(1,8);
    char  *msg = "** Hello SDCC! for PC-8001 **";
    int i;
    for (i=0;i<strlen(msg);i++){
      put_c(msg[i]);
    }

    while(1);  // 無限ループ
    return 0;
}

いろいろと試行錯誤したので、ちょっとサンプルソースは汚いですが…
ちょっと意味のあるメッセージを指した文字列変数も定義して、forループで表示もしてみました。

あと、N-BASICの内部ルーチンを呼び出す関数は、以下のように別のC言語ソースに切り出しています。
読み込んでいる “n_basic.h”は こちらの関数たちの extern命令を切ってありまして、使用する側のサンプルソースのコンパイル時に外部参照定義というかたちにしてリンク時に解決ってことにする定義が入ってます。
※N-BASIC内部ルーチンでZ80レジスタを壊される場合もあるので、ちょっと手抜き感がある実装ではあります…^^)>

#include "n_basic.h"

#define CURSOR_MOVE_AND_PRINT_ROUTINE 0x02D7
#define PUT_C_ROUTINE 0x0257
#define LOCATE_ROUTINE 0x03A9

//カーソルセット
void locate(uint8_t x, uint8_t y) __sdcccall(1) {
  __asm
   ld h,a
   call LOCATE_ROUTINE
   __endasm;  

}

//画面クリア
void screen_clear(void) __sdcccall(1){
  __asm
   ld a,#12
   call PUT_C_ROUTINE
 __endasm;  

}

// 1文字出力関数
void put_c(char c) __sdcccall(1){
  __asm
   call PUT_C_ROUTINE
 __endasm;  
}

// カーソル移動と1文字表示を行う関数
void moveCursorAndPrintChar(uint8_t x, uint8_t y, char c) __sdcccall(1){
   __asm
    ld h,a
    push hl
    ld  hl,#4
    add hl,sp
    ld a,(hl)
    pop hl
    call CURSOR_MOVE_AND_PRINT_ROUTINE
   __endasm;  
}

動かしてみた!

上記をコンパイルしてできたsample2.cmtをPC-8001実機へSDカードに仕込んで持ち込んで動かしてみました!

無事稼働! ちょっと嬉しいです!!
この調子で、いろいろなN-BASIC内部ルーチンをSDCC C言語の関数化を実施していくと、N-BASICでできることは一通りできちゃうのでは?という気がしています。

コメント