2024年2月17日土曜日

80287XL Hacking ソフトウェア編

287XLへの入出力操作は次のようなコードで行えます。特に変わったことはしておらず、データシート通りに信号をH やLにするだけです。

#define FWAIT    while(!(PINC & 0b00000001))
#define WAIT_PRQ while(!(PINC & 0b00000010))
#define ACK_PRQ  PORTA = PORTA & 0b11110111   
#define FREE_PRQ PORTA = PORTA | 0b00001000 

nsigned din() {
  DDRF = 0x00;
  DDRK = 0x00;
  return (PINF << 8) | PINK;
}

void dout(unsigned data) {
  DDRF = 0xff;
  DDRK = 0xff;
  PORTF = data >> 8;
  PORTK = data & 0xff;
}

void wrOp(unsigned opc) {
  unsigned swapped;
  swapped = (opc << 8) | (opc >> 8);
  PORTA = 0b11101000; /* NPS2=H CMD0=L CMD1=L NPS#=L */ 
  dout(swapped);      /* opc */
  PORTA = 0b10101000; /* NPS2=H CMD0=L CMD1=L NPS#=L NPWR#=L */  
  PORTA = 0b01101000; /* NPS2=L CMD0=L CMD1=L NPS#=L NPWR#=H */
}

void wrData(unsigned data) {
  PORTA = 0b11101001; /* NPS2=H CMD0=H CMD1=L NPS#=L */
  dout(data);          /* opc */
  PORTA = 0b10101001; /* NPS2=H CMD0=H CMD1=L NPS#=L NPWR#=L */  
  PORTA = 0b01101001; /* NPS2=L CMD0=H CMD1=L NPS#=L NPWR#=H */
  din(); 
}

unsigned rdData() {
  unsigned data;
  PORTA = 0b11101001; /* NPS2=H CMD0=H CMD1=L NPS#=L */ 
  PORTA = 0b11001001; /* NPS2=H CMD0=H CMD1=L NPS#=L NPRD#=L */  
  data = din();       /* read data */
  PORTA = 0b01101001; /* NPS2=L CMD0=H CMD1=L NPS#=L NPRD#=H */
  return data;
}
   

287のコマンドごとに入出力関数を呼び出すコードを書いてくのはかなり面倒なので、書きやすいように287のコマンドを下のようにマクロで定義しています。 

#define FISUB16(d)  wrOpWr1(0xda20, (d))
#define FSUB(i)     wrOpc(0xd8e0 | ((i) & 0x03)) /* FSUB ST, ST(i) */
#define FSUBd(i)    wrOpc(0xdce8 | ((i) & 0x03)) /* FSUB ST(i), ST */

void wrOpc(unsigned opc) {
  wrOp(opc);
  FWAIT;
}

void wrOpWr1(unsigned opc, unsigned d) {
  wrOp(opc);
  WAIT_PRQ;
  ACK_PRQ;
  wrData(d);
  FREE_PRQ;
  FWAIT;
}

試しに、287XLを使ってマンデルブロ集合のASCIIARTを動かすコードを書いてみたところ、 実行時間は2.6秒でした。



287XLを使わずソフトウェアで実行すると、Arduino Megaは結構速く、2.1秒程度で実行できてしまいます。Arduinoに接続した場合、I/Oポートを介して操作するオーバーヘッドが大きかったり、1コマンドを実行するためにステップ数が多くなるので、あまり速くできないようです。 

GitHubにコードを公開しておきます。https://github.com/4sun5bu/287Hack

80287XL Hacking ハードウェア編

 最近のCPUは内蔵しているのでなじみがないですが、数値演算プロセッサ(FPU)は、昔は外付けで、なかなかなお値段の憧れの石でした。Z8000もFPUを内蔵しておらず、ZilogはZ8070のリリースをアナウンスしていましたが、結局出なかったようです。CP/M-8000には、Z8070をエミュレートするライブラリが含まれているのですが、試してみたところ実用的なスピードではありません。

昔、Intelの80286用の数値演算プロセッサ80287がI/Oデバイスのように扱えると、どこかで読んだ気がしていたので、Z8001につなごうと80287XLを入手してありました。本当につなげらるのか事前確認するため、これをArduino Megaに接続して検証してみました。

接続は下図になります。


クロックには10MHzを供給し、CKMをLにして内部で2分周しています。残りの信号やデータバスはArduinoのI/Oに接続します。基本的に、データシートの通りに信号を動かしながら、データバスからデータを与えたり読んだりすれば動作します。

ちゃんと調べずに高性能というだけで287XLを入手していたのですが、ただの287だと接続信号数が多くむずかしいようです。287XLではNCになっていますが、287だとこれらのピンに役割があり、CPUの対応する信号に接続することになっていてます。287はCPUの動作状態を監視しているようですが、287XLではこれらは要らないと判断され廃止されたようです。

データシートを読んでみると、確かに287/287XLは286からはI/Oデバイスと扱われており、アドレスが割り当てられています。NPS#1とNPS2がチップセレクトにあたり、CMD0とCMD1がアドレスに当たり、下のような組み合わせがあります。

 CMD0=L, CMD1=L :コマンド書き込み、CWとSWの読み込み
 CMD0=H, CMD1=L:データ読み書き
 CMD0=L, CMD1=H:例外ポインタ書き込み

例外ポインタ書き込みは、まだ何なのかわかっていません。これ以外の組み合わせは予約されています。NPRD#とNPWR#は、I/Oリードとライト信号となっています。

データバスは16bitなのですが、x86系はリトルエンディアンなので、コマンドやデータをやり取りするには少し注意が必要です。データの場合は、D15-D8が上位バイトでD7-D0が下位バイトで問題ありませんが、コマンドは上下バイトを入れ替える必要があります。

x86のアセンブリ言語をみると、287のコマンドに続いてアドレッシングモードを含んだオペランドが来るのですが、287からはメモリアクセスができないので、必要な場合はCPUがメモリアクセスして287にデータを渡しています。その際、287はPEREQをHにしてデータ転送を求めていること示し、CPUはデータ転送をする前にPEACK#をLにし、データ転送要求を受け付けたことを287に伝えます。データは複数回数行う場合もあり、1転送ごとにPEACK#をHに戻す必要があるのか、すべての転送が終わるまでLに保持しておいても良いのかは不明です。試してみた感じでは、PEACK#は一度もLにせずHのままにしておいても動作するようです。

BUSY#は287がコマンドを実行中かを示しているので、この信号をみて処理の完了を確認でき、ERROR#でコマンドの実行が正常に終わったかを判断できます。

これだけ知っていれば、287XLは動かせます。


2023年11月22日水曜日

UNIX V7 カーネル 

 ブートはどうにかなってきたので、そろそろカーネル本体に手を出していきたいのですが、どこから手を付けていいのか、正直途方に暮れています。

カーネルのコードは /usr/sys/ 以下にあり、/usr/sys/conf/ で make すれば、カーネルがビルドできるようです。その中の makefile を見る限るでは、l.s, mch.s, c.c あたりから見ていけば良さそうです。


この辺りは、カーネルのか解説本を見てもあまり参考になりません。解説本には、メモリ管理やプロセス管理のコードの説明がされていますが、移植を前提とした解説ではないので。この辺りは、PDP-11のハードに依存したコードが多いのも難しいところです。

暫くこの辺りのコードを眺めながら、突破口を探すことにします。

2023年9月2日土曜日

NEC V60 入手

 Z80,000やZ80320と同じように入手を諦めていたのですが、たまたまネットで見つけ、ちょっと高価でしたが購入してしまいました。


同じ所でV70も販売していたのですが、V70はほとんど資料がネット上になく、入手しても走らすのは無理です。私はレトロCPUコレクターではないので、手に入れたら動かしたい派です。とは言え、入手したまま引き出しにしまったままのCPUが結構たまってきていますが。

このCPUはかなり高機能で、以下のような特徴があります。

  • 32本の32bit汎用レジスタ 
  • 浮動小数点演算命令
  • V20/V30エミュレーション
  • デマンドページングをサポートできるMMU

    詳しくは、Wikipedia https://en.wikipedia.org/wiki/NEC_V60 を参照して下さい。
    データシートがあるサイトも上げておきます。http://mess.redump.net/datasheets/nec

    V60は32bitCPUなのですが、モトローラのMC68000と同じように、データバスは 16bitになっています。アドレスバスも24bitで、MC68020のような完全な32bit CPUとはなっていません。そのため、いままで弄ってきたZ8001と同じくらいの配線量で動かせるはずです。この点が自分には結構重要で、Z8000のボードを何枚か作ってきていますが、はんだ付けやラッピングで配線して作るのは、16bitが限界だと感じています。

    趣味に使える時間は限られるので、Z8000をいじりつつ気分転換がてら少しずつ進めたていきたいと思います。


    2023年8月3日木曜日

    UNIX V7のブート 5

     ブートローダの2段目は、C言語とアセンブリ言語で書かれたbootで、そのソースコードは、/usr/src/cmd/standaloneにあります。M.sにあるstartから実行が開始され、MMUの設定とコードの移動が行われたのち、C言語でかかれたmain()に実行が移ります。

    / set kernel I+D to physical 0 and IO page
            clr     r1              ! start from 0x00000
            mov     $77406,r2       ! 127 blocks R/W
            mov     $KISA0,r3
            mov     $KISD0,r4
            jsr     pc,setseg
            mov     $IO,-(r3)
            clr     r1
            mov     $KDSA0,r3
            mov     $KDSD0,r4
            jsr     pc,setseg
            mov     $IO,-(r3)
    
    / set user I+D to physical 64K (words) and IO page
            mov     $4000,r1        ! start from 0x20000
            mov     $UISA0,r3
            mov     $UISD0,r4
            jsr     pc,setseg
            mov     $IO,-(r3)
            mov     $4000,r1
            mov     $UDSA0,r3
            mov     $UDSD0,r4
            jsr     pc,setseg
            mov     $IO,-(r3)
    
    / enable map
            mov     $65,SSR3        / 22-bit map ! Split I+D for Kernel and User
            bit     $20,SSR3
            beq     1f              ! Check if 22-bit actived
            mov     $3,MSCR
    1:
            mov     $30340,PS
            inc     SSR0            ! Enable MMU

    ブートローダは、1-24行でカーネルとユーザのIとD空間用にMMUを設定したのち、MMUを有効にします。この段階ではブートローダは カーネルモードで実行されているので、PSWを現在カーネル、以前をユーザに32行で設定しています。

    このままでは、ローダ自身がじゃまでUNIXカーネルを読み込めないので、ユーザ空間に移動させます。

    / copy program to user I space
            mov     $_end,r0
            asr     r0
            clr     r1
    1:
            mov     (r1),-(sp)
            mtpi    (r1)+
            sob     r0,1b
    
    
    / continue execution in user space copy
            mov     $140004,sp      ! 0xc004
            clr     *$KDSA6
            mov     $140340,-(sp)   ! PSW:0b1100000011100000
            mov     $user,-(sp)     ! Return address
            rtt                     ! Return from trap

    7行目のmtpi命令は、以前いたモードのI空間にデータをスタック経由でデータを書き込みます。繰り返し命令を使って、カーネルモードから以前のユーザモードのI空間にローダのコードがコピーされます。

    14-16行目は、スタックにカーネルからユーザモードに戻るようなPSWと、userのアドレスを積み、rtt命令でトラップからの復帰を装ってモード切替とジャンプを実行しています。

    この後は、C言語で書かれた main()に実行が移ります。

    2023年7月29日土曜日

    Z8001MBex その1

    新しくZ8001CPUボードを作製しました。今までは、ラッピングワイヤを直接はんだ付けして配線していましたが、今回初めて、ユニバーサル基板にピンヘッダをたてワイヤラッピングで配線してみました。この方法は、ピンヘッダとICソケットとの間を配線するのが単調で根気がいります。とくにPLCCソケットは苦痛でした。ラッピングでの配線はやっていて楽しいのですが、間違いに気づいたときは修正が大変です。ほどきたい配線の上にすでに配線されていると、それもほどかないといけません。一度ほどくとワイヤが傷んでしまっているので、2回目巻きなおすと折れたりするので気を使います。年で眼も弱ってきているので、そろそろPCBが作れるようにならないとだめですね。


    今回のボードは、MMUを搭載しCP/M-8000以外のOSも移植することを考えています。初代のボードでは、Z8010 MMUを使ってメモリー管理ができるように考えていたのですが、何故かうまく動作せず。結局、CPLDでセグメントアドレスを変換するロジックを組んで、CP/M-8000が動作するようにしました。しかし、この方法ではアドレス変換が固定で融通が利かず、CP/M-8000以外のOSを動かすのは難しくなります。

    新しいボードでは、Z8010を使うことは諦めて、MMUを自前で組むことにしました。これに先立って、Z8002 CPUを使ったボードで、MMUを自作できそうか簡単な回路を組んで試してあったのですが、実際に作ってみると配線量の多さからかなり苦戦しました。MMU以外は、最初のボードとほぼ同じです。

    自作MMUの仕様は下のようになっています。

    • 高速SRAMを使ってページテーブルを構成する
    • ページテーブルをメモリ空間に置く
    • ページサイズ 2Kバイト
    • 論理アドレス空間 1Mバイト(16セグメント)
    • 物理アドレス空間 1Mバイト
    • 命令とデータ空間を分離可能
    • システムとノーマルで空間を分離可能
    手持ちで256kビットの高速SRAMを持っていたので、これを使ってページテーブルを構成しました。試験的に作ったZ8002ボードでは、I/O空間にページテーブルを置いたのですが、Z8001ではメモリ空間に置くことにしました。ページテーブルはSRAMの一部しか使わないので、未使用の部分が通常のメモリとして使えます。
    また、メモリ空間だとブロック転送命令が使えるので、ページテーブルのコピーや移動が短いコードで行なえます。

    ページサイズとアドレス空間のサイズは適当に決めたもので、これが最適なのかはわかりません。欲張ると配線量が増えるので、自作するには、これ位がちょうど良いかなと思っています。

    命令とデータ空間の分離は、CP/M-8000を動かすためには必要な機能です。また、Z8001には8086のように8ビット命令がないため、コードが大きくなりがちです。64kバイトのセグメントサイズに縛られるZ8000では、データを別空間に置ける機能はあったほうが良さそうです。

    システムとノーマルで空間を分離する機能は、UNIXのようなOSを移植するために付けておきました。一応、CP/M-8000でも使われているようなのですが。

    今回は次のような機能は見送っています。
    • ページを無効に設定できる
    • ページを書き込み禁止に設定できる
    • ページにアクセスがあったか記録する
    最初の2つは、あとからでも実装できると思います。しかし、ページアクセスの記録は、別のメモリを積むか、高速SRAMにアドレス変換後に書き込む回路を追加する必要があり、実現が難しいので諦めています。

    すでに、CP/M-8000の移植は完了しています。初代のボードとほとんど同じなので、MMUの初期化を追加し、初代のボードと同じセグメントアドレス変換になるよう合わせれば動きました。

    そして今、UNIX V7を移植しようと挑んでます。

    2023年7月9日日曜日

    UNIX V7のブート 4

    PDP-11が起動し最初にディスクから読み出されるコードは、ディスクの先頭に書かれているブートプログラムです。そのソースコードは、/usr/mdec/rpuboot.s です。UNIX V6だと、このプログラムがUNIXカーネルをディスクから読み出して実行するのですが、V7では、/bootを読み込んで実行します。そしてbootがUNIXカーネルを読み込んで実行します。bootのソースコードは、/usr/src/cmd/standaloneにあります。

    ブートプログラムはアセンブリ言語で書かれています。コードのメインの部分を見ていきます。

    / now start reading the inodes
    / starting at the root and
    / going through directories
    1:
    	mov	$names,r1
    	mov	$2,r0			/ ルートディレクトリのinode番号
    1:
    	clr	bno
    	jsr	pc,iget			
    	tst	(r1)			/ namesの終端か
    	beq	1f
    2:
    	jsr	pc,rmblk		/ 
    		br start		/ rmblkが正常の終了の場合はスキップ
    	mov	$buf,r2			/ r2 = buf[]
    3:
    	mov	r1,r3			/ r3 = names[]
    	mov	r2,r4			/ r4 = buf[]
    	add	$16.,r2			/ r2 = 次のディレクトリエントリ
    	tst	(r4)+			/ inodeは空か
    	beq	5f
    4:
    	cmpb	(r3)+,(r4)+	/ ファイル名の比較
    	bne	5f
    	cmp	r4,r2			/ 
    	blo	4b
    	mov	-16.(r2),r0		/ r0 = inode番号
    	add	$14.,r1			/ 
    	br	1b
    5:
    	cmp	r2,$buf+512.	/ 
    	blo	3b				/ buf[]の最後でない
    	br	2b				/ buf[]の最後
    
    / read file into core until
    / a mapping error, (no disk address)
    1:
    	clr	r1				/ r1 = 0
    1:
    	jsr	pc,rmblk
    		br 1f
    	mov	$buf,r2
    2:
    	mov	(r2)+,(r1)+
    	cmp	r2,$buf+512.
    	blo	2b
    	br	1b
    1:
    	clr	r1

    6−9行で、ルートディレクトリのinodeを読み込んでいます。igetは、r0で指定した番号のinodeをinode[]にコピーします。

    13行は、rmblkでルートディレクトリをbuf[]に読み込みます。15行目からは、1エントリごとにファイル名とnames[]を比較します。

    一致した場合は、27行でinode番号をr0に読み込みます。r1に14を足して8行目に飛び、igetでファイル名が一致したファイルのinodeを読み込みます。

    11行でファイル名が終わっていることを確認し、38行目からinodeのdi_addr[]に沿ってファイルをメモリーに読み込んでいきます。


    次のリストは igetです。

    / get the inode specified in r0
    iget:
    	add	$15.,r0
    	mov	r0,r5
    	ash	$-3.,r0			/ r0 = (r0 + 15) / 8
    	bic	$!17777,r0		/ r0 &= 0x01fff
    	mov	r0,dno
    	clr	r0
    	jsr	pc,rblk
    	bic	$!7,r5			/ r5 &= 0x0007
    	ash	$6,r5			/ r5 *= 64
    	add	$buf,r5			/ r5 += buf
    	mov	$inod,r4
    1:
    	mov	(r5)+,(r4)+
    	cmp	r4,$inod+64.
    	blo	1b
    	rts	pc

    3-6行でinode番号をinodeが存在しているブロック番号に変換し、9行でそのブロックをbuf[]に読み込んでいます。

    10-12行で目的のinodeのbuf[]内でのオフセットを求め、13ー17行でinode[]にコピーしています。


    次のrmblkは、bnoで指定されたブロックを、inode内のdi_addr[]に従ってbuff[]に読み込みます。

    / read a mapped block
    / offset in file is in bno.
    / skip if success, no skip if fail
    / the algorithm only handles a single
    / indirect block. that means that
    / files longer than 10+128 blocks cannot
    / be loaded.
    rmblk:
    	add	$2,(sp)			/ リターンアドレスを2増やす
    	mov	bno,r0
    	cmp	r0,$10.
    	blt	1f
    	mov	$10.,r0			/ 間接参照
    1:
    	mov	r0,-(sp)		/ 
    	asl	r0				/ 
    	add	(sp)+,r0		/ r0 = bno * 3 
    	add	$addr+1,r0		/ r0 += di_addr + 1
    	movb	(r0)+,dno	
    	movb	(r0)+,dno+1	/ dno = ブロック番号の下位2バイト
    	movb	-3(r0),r0	/ ブロック番号の上位1バイト
    	bne	1f
    	tst	dno
    	beq	2f
    1:
    	jsr	pc,rblk			/ ブロック読み込み
    	mov	bno,r0
    	inc	bno
    	sub	$10.,r0
    	blt	1f				/ 直接参照ならリターン
    	ash	$2,r0			/ r0 *= 4
    	mov	buf+2(r0),dno	/ dno = ディスク番号の下位2バイト
    	mov	buf(r0),r0		/ r0 = ディスク番号の上位2バイト
    	bne	rblk
    	tst	dno
    	bne	rblk
    2:
    	sub	$2,(sp)
    1:
    	rts	pc

    ブロック番号が0から9までは直接参照で10は間接参照になりますが、コメントにあるとおり、2重間接参照には対応していません。

    13行でブロック番号10以上は、間接参照に対応するため10に固定しています。

    15-18行で、ブロック番号からdi_addr[]内でのディスク番号下位2バイトのオフセットを求めています。

    22-24行は、ブロック番号が0だった場合、rmblkを呼び出した直後のアドレスにリターンします。

    26-30行で、ブロックを読み込んだ後、直接参照の場合はリターンします。

    31行目からは間接参照の処理で、29行で10引かれたディスク番号を4倍しオフセットを求め、先に読み込んだブロックからブロック番号を取り出します。

    34-36行で、番号が0でなければブロックを読み出します。

    ブロックが正常に読み込まれた場合は、9行でリターンアドレスが+2されているので、rmblkを呼び出した次の命令をスキップします。


    これで、UNIXが起動する第一段階を追いかけられました。次は、2段階目の/bootになります。