SSブログ
English Version

リアルタイムモニタ(ZealMon)の製作 [Z80]

 Twitter(X)のタイムラインで Z80 のスタックを使ったタスク管理の話題(スタック上にタスクアドレスを並べて順次呼び出すもの)を見てリアルタイムモニタ(リアルタイムOS)を作ってみたくなりました。

 リアルタイムOSに関してはネット上にも多くの情報があり、例えばここや "リアルタイム処理の基礎知識" 等に簡潔にまとめられています。

 とは言ってもあまり本格的なものではなく、ライトウェイトである程度高速なものを作りたいと思います。今回作成した Z80 用のリアルタイムモニタ(以降、ZealMon と記す)の特徴を列挙すると
  1. 実装機能は必要最小限
    • 実行中のタスクの中断(プリエンプト)可能
    • タイマーウェイト機能
    • プライオリティは4レベル(拡張は容易)
    • ラウンドロビンタスク切替え要求機能

  2. ライトウェイト
     モニタ本体のコードは約 300 バイト

  3. タスクの状態遷移はキューで管理
     TCB(タスクコントロールブロック)内にタスク状態情報を持たず状態書換え処理を省略し軽量化

  4. アイドルタスクの実装省略
     最低プライオリティでウェイト状態にならないタスクを持ついことで、レディ状態のタスクが無い時に実行するアイドルタスクの実装を省きました。番兵的な発想ですね。

 イベント管理、セマフォ、タスク間のメッセージ管理などの機能はまだありませんが、タイマー管理と同様にすることで追加実装は割合楽だと思います。

 タスクコントロールブロック(TCB)の構造は下記のようにしました。

nameoffsetbytesmearn
TcList02address for TCB link
TcTskNo21high nibble:priority(0:highest) low nibble:task No.
TcTim31timer count
TcEvCnt41how many waiting event except timer
TcSP52stack pointer save area
TcStat71task status !! not used !!


 最近動かしている手持ちの Z80 ボードではタイマー割込みを自由に設定できるものが無かったのでネット上で公開されている MSX のエミュレータである MSXPen で試すことにしました。
 周期割込みとして MSX の垂直帰線割り込み(1/60秒周期)を使用します。割込みモードは mode1(RST 38H 実行) です。
 リアルタイムモニタが動作中の時は割込みでタスク切替が発生しているのでディバッガでブレークしてもディバッガ自身がブレークしたタスクと同じタスクの扱いになり、他のタスクが文字表示等のシステムコールをしている場合、リエントラントにコールすることになるので直ぐにハングしてしまいます。

 結局ディバッガはあまり役に立たずに机上ディバッグ(若しくは割込み機能を停止した上のでブレーク確認)がメインになります。仕事であればICEが使えるのですが、個人としてはそんな高価なものは持っていません。
  MSXPen 上では動作が中々安定せず悩みましたが、割込み禁止(DI)中でも割込みが入るらしく、DI と共に 38H をリターンコードに書き換えるようにしたら安定して動作するようになりました。※1
※1)原因判明(2023/10/05 の追記部分を参照)

 下図は MSXPen でデモプログラムを実行した際の画面です。
 表示が競合し易くなるように1文字表示毎にタイマーウェイトを入れています。

MSXPen でデモプログラム実行時の様子


 下図はディスパッチが最初に実行される直前とタスク3が実行開始した時点でのワークエリアのダンプ表示です。割込みの影響を無くすため、フックした新たな割込み処理の先頭をリターンコードに書き換えています。
 画面中央のダンプ情報からディスパッチ前はタスク1~3がプライオリティ 0 のレディキューにつながっているのが判ります。タスク0は前述のアイドルタスクでプライオリティ3のレディキューにつながっています。下側のダンプはタスク3実行開始時のメモリで実行タスク番号が3でタイマーウェイトキューにタスク1とタスク2が繋がっています。

ZealMon 実行時のメモリ状態


 まだ最適化はしていませんが、ソースも貼っておきます。無いとは思いますが今後、Z80 でリアルタイム制御したい場合に役に立つかもしれませんw

リアルタイムモニタ(ZealMon)とデモのソース(Z80アセンブラ)
;++++++++++++++++++++++++++++++++++++++++ ; ZealMon : Z80 simple real time monitor ; Ver 0.01 2023/10/03 by skyriver ;+++++++++++++++++++++++++++++++++++++++ ; ; TCB ; name ofst byte mearn ; TcList 0 : 2 address for TCB link ; TcTskNo 2 : 1 high nibble:priority(0:highest) low nibble:task No. ; TcTim 3 : 1 timer count ; TcEvCnt 4 : 1 how many waiting event except timer ; TcSP 5 : 2 stack pointer save area ; TcStat 7 : 1 task status !! not used !! ; 0 : not regist ; 1 : running ; 2 : ready ; 3 : wait 0008 TCBSIZ EQU 8 ; TCB size 0000 TcTop EQU 0 ; define offset 0000 TcList EQU TcTop 0002 TcTskNo EQU 2 0003 TcTim EQU 3 0004 TcEvCnt EQU 4 0005 TcSP EQU 5 0007 TcStat EQU 7 ; TCBTBL 4 byte 0:top priority(not used) ; 0000 tcSt_NO EQU 0 ; not regist 0001 tcSt_RUN EQU 1 ; running 0002 tcSt_RDY EQU 2 ; ready 0003 tcSt_WAT EQU 3 ; wait 0004 MAX_PRI EQU 4 ; max priority 0000 PRI0 EQU 00H ; highest 0010 PRI1 EQU 10H 0020 PRI2 EQU 20H 0030 PRI3 EQU 30H ; lowest 000F MSKTNO EQU 00FH ; task No. bit mask 00F0 MSKPRI EQU 0F0H ; priority bit mask 0000' ASEG ORG 0100H 0100 21 2000 INIRTM: LD HL,WORKST 0103 11 2001 LD DE,WORKST + 1 0106 01 002A LD BC,WORKSIZ - 1 0109 36 00 LD (HL),0 010B ED B0 LDIR 010D 21 0232 LD HL,INITCB 0110 11 2009 LD DE,TCB0 0113 01 0020 LD BC,TCBSIZ * TASKS 0116 ED B0 LDIR 0118 11 7FFF LD DE,STACK - 1 011B 21 0261 LD HL,TENTRY + 2 * ( TASKS - 1 ) + 1 011E 3E 04 LD A,TASKS 0120 01 0002 INIR10: LD BC,2 ; set return addres(=task entry) 0123 ED B8 LDDR 0125 E5 PUSH HL 0126 21 FFC2 LD HL,-(TSKSPSIZ - 2) 0129 19 ADD HL,DE 012A EB EX DE,HL 012B E1 POP HL 012C 3D DEC A 012D 20 F1 JR NZ,INIR10 012F 21 2009 LD HL,TCB0 0132 06 04 LD B,TASKS 0134 C5 INIR20: PUSH BC 0135 CD 018C CALL ADDRDY 0138 C1 POP BC 0139 11 0008 LD DE,TCBSIZ 013C 19 ADD HL,DE 013D 10 F5 DJNZ INIR20 013F F3 DI 0140 ED 73 1000 LD (SavSP),SP 0144 CD 0276 CALL IniAp ; dispatch task ; select ready task and run it ; 0147 DISPATCH: 0147 21 2001 LD HL,RDYLNK 014A 06 04 LD B,MAX_PRI 014C 7E DISP10: LD A,(HL) 014D 23 INC HL 014E B6 OR (HL) 014F 23 INC HL 0150 20 04 JR NZ,DISPFND ; find ready 0152 10 F8 DJNZ DISP10 0154 F3 DI ; TASK0(=idle task) is always ready 0155 76 HALT ; not exec here 0156 DISPFND: 0156 2B DEC HL 0157 2B DEC HL ; HL = RDYLINK 0158 CD 016F CALL REMOVE REPT TcTskNo - TcTop INC HL ENDM 015B 23 + INC HL 015C 23 + INC HL 015D 7E LD A,(HL) ; get task No. 015E 32 2000 LD (RUNTSK),A REPT TcSP - TcTskNo INC HL ENDM 0161 23 + INC HL 0162 23 + INC HL 0163 23 + INC HL 0164 5E LD E,(HL) 0165 23 INC HL 0166 56 LD D,(HL) 0167 EB EX DE,HL 0168 F9 LD SP,HL 0169 F1 POP AF 016A C1 POP BC 016B D1 POP DE 016C E1 POP HL 016D FB EI 016E C9 RET ; remove from link ; HL <- link addr(not empty link) ; HL -> removed tail TCB addr ; 016F 4E REMOVE: LD C,(HL) 0170 23 INC HL 0171 46 LD B,(HL) 0172 2B DEC HL 0173 78 LD A,B 0174 B1 OR C 0175 20 04 JR NZ,REMO10 0177 F3 DI 0178 C3 0178 JP $ 017B EB REMO10: EX DE,HL 017C 69 LD L,C 017D 60 LD H,B 017E 4E LD C,(HL) 017F 23 INC HL 0180 46 LD B,(HL) 0181 2B DEC HL ; BC = next 0182 79 LD A,C 0183 B0 OR B 0184 20 F5 JR NZ,REMO10 ; HL = tail TCB, DE = prev 0186 EB EX DE,HL 0187 77 LD (HL),A ; A = 0 0188 23 INC HL 0189 77 LD (HL),A 018A EB EX DE,HL 018B C9 RET ; add TCB to ready que ; HL <- TCB ; HL -> TCB 018C ADDRDY: REPT TcTskNo - TcTop INC HL ENDM 018C 23 + INC HL 018D 23 + INC HL 018E 7E LD A,(HL) ; get task No. & priority REPT TcTskNo - TcTop DEC HL ENDM 018F 2B + DEC HL 0190 2B + DEC HL 0191 E6 F0 AND MSKPRI 0193 0F RRCA 0194 0F RRCA 0195 0F RRCA 0196 5F LD E,A 0197 16 00 LD D,0 ; DE = pri * 2 0199 E5 PUSH HL ; save TCB 019A 21 2001 LD HL,RDYLNK 019D 19 ADD HL,DE 019E D1 POP DE 019F CD 0205 CALL AddLnk ; HL = TCB 01A2 C9 RET ; timer interrupt ; if exist timer wait task, then dec timer and if time up, add rady task link ; 01A3 Timer: 01A4 SavINT EQU $ + 1 ; original int addr 01A3 CD 0000 CALL 0 01A6 F3 Tim00: DI 01A7 E5 PUSH HL 01A8 D5 PUSH DE 01A9 C5 PUSH BC 01AA F5 PUSH AF 01AB CD 021F CALL SaveSP ; save SP into TCB 01AE CD 018C CALL ADDRDY ; add running TCB to ready link 01B1 2A 2029 LD HL,(TIMLNK) ; timer wait task link 01B4 11 2029 LD DE,TIMLNK 01B7 18 1B JR Tim30 01B9 E5 Tim10: PUSH HL REPT TcTim - TcTop INC HL ENDM 01BA 23 + INC HL 01BB 23 + INC HL 01BC 23 + INC HL 01BD 35 DEC (HL) ; decrement timer value 01BE E1 POP HL 01BF 4E LD C,(HL) 01C0 23 INC HL 01C1 46 LD B,(HL) ; BC = next link 01C2 2B DEC HL 01C3 20 0C JR NZ,Tim20 01C5 C5 PUSH BC 01C6 D5 PUSH DE 01C7 CD 018C CALL ADDRDY 01CA D1 POP DE 01CB C1 POP BC ; DE -> (HL:unlink) -> BC 01CC EB EX DE,HL 01CD 71 LD (HL),C 01CE 23 INC HL 01CF 70 LD (HL),B ; unlink from link 01D0 2B DEC HL 01D1 59 Tim20: LD E,C 01D2 50 LD D,B ; DE = next timer waiting task's TCB 01D3 EB EX DE,HL 01D4 7C Tim30: LD A,H 01D5 B5 OR L 01D6 20 E1 JR NZ,Tim10 01D8 C3 0147 JP DISPATCH ; timer wait ; A <- timer value ; 01DB F3 TWait: DI 01DC E5 PUSH HL 01DD D5 PUSH DE 01DE C5 PUSH BC 01DF F5 PUSH AF 01E0 4F LD C,A 01E1 CD 021F CALL SaveSP ; save running task's SP into TCB, HL = TCB 01E4 E5 PUSH HL REPT TcTim - TcTop INC HL ENDM 01E5 23 + INC HL 01E6 23 + INC HL 01E7 23 + INC HL 01E8 71 LD (HL),C ; set timer 01E9 D1 POP DE ; DE = TCB 01EA 21 2029 LD HL,TIMLNK ; ADD TCB to TIMLNK 01ED CD 0205 CALL AddLnk 01F0 C3 0147 JP DISPATCH ; get running TCB addr ; HL -> TCB ; 01F3 GetRTcb: 01F3 3A 2000 LD A,(RUNTSK) 01F6 E6 0F AND MSKTNO 01F8 87 ADD A,A 01F9 5F LD E,A 01FA 16 00 LD D,0 01FC 21 0252 LD HL,TCBTBL 01FF 19 ADD HL,DE 0200 5E LD E,(HL) 0201 23 INC HL 0202 56 LD D,(HL) 0203 EB EX DE,HL ; HL = TCB 0204 C9 RET ; add TCB to link ; HL <- LINK addr ; DE <- TCB addr ; HL -> TCB ; 0205 4E AddLnk: LD C,(HL) 0206 23 INC HL 0207 46 LD B,(HL) 0208 72 LD (HL),D 0209 2B DEC HL 020A 73 LD (HL),E 020B EB EX DE,HL 020C 71 LD (HL),C 020D 23 INC HL 020E 70 LD (HL),B 020F 2B DEC HL 0210 C9 RET ; run next ready task(round robin) ; 0211 F3 ROBIN: DI 0212 E5 PUSH HL 0213 D5 PUSH DE 0214 C5 PUSH BC 0215 F5 PUSH AF 0216 CD 021F CALL SaveSP ; save SP into TCB 0219 CD 018C CALL ADDRDY 021C C3 0147 JP DISPATCH ; set SP into TCB ; HL -> TCB ; 021F SaveSP: 021F CD 01F3 CALL GetRTcb ; get running task's TCB 0222 E5 PUSH HL ; save TCB 0223 11 0005 LD DE,TcSP - TcTop 0226 19 ADD HL,DE 0227 EB EX DE,HL ; DE = TCB's SP save addr 0228 21 0004 LD HL,4 ; CALL THIS + PUSH HL 022B 39 ADD HL,SP 022C EB EX DE,HL 022D 73 LD (HL),E 022E 23 INC HL 022F 72 LD (HL),D ; save SP 0230 E1 POP HL ; HL = TCB 0231 C9 RET ; ++++++++++ test apprication ++++++++ 0004 TASKS EQU 4 ; how many task 0040 TSKSPSIZ EQU 40H ; tasks stack size 8000 STACK EQU 8000H ; stack pointer 000A STREGSIZ EQU 10 ; PC & reg area size on stack ; *** TCB initial data *** 0000 TSK0NoO EQU 0 0001 TSK0No1 EQU 1 0002 TSK0No2 EQU 2 0003 TSK0No3 EQU 3 7FF6 STACK3 EQU STACK - STREGSIZ 7FB6 STACK2 EQU STACK3 - TSKSPSIZ 7F76 STACK1 EQU STACK2 - TSKSPSIZ 7F36 STACK0 EQU STACK1 - TSKSPSIZ 0232 INITCB: ; task0 : idle 0232 0000 DW 0 ; link 0234 30 DB TSK0NoO OR PRI3 ; task No. 0235 00 DB 0 ; timer 0236 00 DB 0 ; event 0237 7F36 DW STACK0 ; stack 0239 02 DB tcSt_RDY ; status(ready) ; task1 023A 0000 DW 0 ; link 023C 01 DB TSK0No1 OR PRI0 ; task No. 023D 00 DB 0 ; timer 023E 00 DB 0 ; event 023F 7F76 DW STACK1 ; stack 0241 02 DB tcSt_RDY ; status(ready) ; task2 0242 0000 DW 0 ; link 0244 02 DB TSK0No2 OR PRI0 ; task No. 0245 00 DB 0 ; timer 0246 00 DB 0 ; event 0247 7FB6 DW STACK2 ; stack 0249 02 DB tcSt_RDY ; status(ready) ; task3 024A 0000 DW 0 ; link 024C 03 DB TSK0No3 OR PRI0 ; task No. 024D 00 DB 0 ; timer 024E 00 DB 0 ; event 024F 7FF6 DW STACK3 ; stack 0251 02 DB tcSt_RDY ; status(ready) 0252 TCBTBL: ; TCB table IRPC X,0123 DW TCB&X ENDM 0252 2009 + DW TCB&0 0254 2011 + DW TCB&1 0256 2019 + DW TCB&2 0258 2021 + DW TCB&3 025A TENTRY: ; TASK entry addr table IRPC X,0123 DW TASK&X ENDM 025A 02A5 + DW TASK&0 025C 02A9 + DW TASK&1 025E 02D7 + DW TASK&2 0260 02EE + DW TASK&3 ; *** CP/M *** 0005 BDOS EQU 0005H 0002 dfPUTC EQU 02H ; D <- data ; *** MSX *** 0039 INTADR EQU 038H + 1 ; interrupt jump addr 0010 INTTIM EQU ( 1000 / 60 ) ; interrupt interval time [ms] 000D CR EQU 13 000A LF EQU 10 ; fuck Blank int for timer ; MSX : mode 1, RST 38H 0262 HookInt: 0262 2A 0039 LD HL,(INTADR) 0265 22 01A4 LD (SavINT),HL 0268 21 01A3 LD HL,Timer 026B 22 0039 LD (INTADR),HL 026E C9 RET ; unfuck blank int ; 026F 2A 01A4 UnHook: LD HL,(SavINT) 0272 22 0039 LD (INTADR),HL 0275 C9 RET ; apprication initialize proc ; 0276 CD 0262 IniAp: CALL HookInt 0279 2A 0001 LD HL,(1) ; get WBOOT addr 027C 11 0009 LD DE,3 * 3 027F 19 ADD HL,DE ; HL = conout 0280 22 028C LD (COUTADR),HL 0283 C9 RET ; putchar ; A <- data ; 0284 4F Putch: LD C,A 0285 F3 DI 0286 3E C9 LD A,0C9H ; But executed DI, interrupt occurs at MSXPen 0288 32 0038 LD (INTADR - 1),A 028C COUTADR EQU $ + 1 028B CD 0000 CALL 0 028E 3E C3 LD A,0C3H 0290 32 0038 LD (INTADR - 1),A 0293 FB EI 0294 C9 RET ; put string ; HL <- string addr ; 0295 7E Puts: LD A,(HL) 0296 B7 OR A 0297 C8 RET Z 0298 E5 PUSH HL 0299 CD 0284 CALL Putch 029C E1 POP HL 029D 23 INC HL 029E 3E 01 LD A,1 02A0 CD 01DB CALL Twait 02A3 18 F0 JR Puts 02A5 00 TASK0: NOP 02A6 C3 02A5 JP TASK0 02A9 21 0305 TASK1: LD HL,MsgStart 02AC CD 0295 CALL Puts 02AF 3E BB LD A,3000 / INTTIM 02B1 CD 01DB CALL TWait 02B4 06 10 LD B,16 02B6 TASK1_1: 02B6 C5 PUSH BC 02B7 3E 32 LD A,800 / INTTIM 02B9 CD 01DB CALL TWait 02BC 21 033C LD HL,MsgTk1 02BF CD 0295 CALL Puts 02C2 C1 POP BC 02C3 10 F1 DJNZ TASK1_1 02C5 21 0362 LD HL,MsgBye 02C8 CD 0295 CALL Puts 02CB F3 DI 02CC CD 026F CALL UnHook 02CF ED 7B 1000 LD SP,(SavSP) 02D3 FB EI 02D4 C3 0000 JP 0 02D7 TASK2: 02D7 3E A7 LD A,2680 / INTTIM 02D9 CD 01DB CALL TWait 02DC 3E 7D TSK2LP: LD A,2000 / INTTIM 02DE CD 01DB CALL TWait 02E1 3E 3E LD A,1000 / INTTIM 02E3 CD 01DB CALL TWait 02E6 21 0358 LD HL,MsgTk2 02E9 CD 0295 CALL Puts 02EC 18 EE JR TSK2LP 02EE TASK3: 02EE 3E B5 LD A,2900 / INTTIM 02F0 CD 01DB CALL TWait 02F3 TSK3LP: 02F3 3E 7A LD A,1960 / INTTIM 02F5 CD 01DB CALL TWait 02F8 3E 7D LD A,2000 / INTTIM 02FA CD 01DB CALL TWait 02FD 21 035D LD HL,MsgTk3 0300 CD 0295 CALL Puts 0303 18 EE JR TSK3LP 0305 MsgStart: 0305 0D 0A 54 61 DB CR, LF, 'Task1 says "hello", Task2 & Task3 say each number.', CR, LF, 0 0309 73 6B 31 20 030D 73 61 79 73 0311 20 22 68 65 0315 6C 6C 6F 22 0319 2C 20 54 61 031D 73 6B 32 20 0321 26 20 54 61 0325 73 6B 33 20 0329 73 61 79 20 032D 65 61 63 68 0331 20 6E 75 6D 0335 62 65 72 2E 0339 0D 0A 00 033C 0D 0A 68 65 MsgTk1: DB CR, LF, "hello, world. I am task1.", 0 0340 6C 6C 6F 2C 0344 20 77 6F 72 0348 6C 64 2E 20 034C 49 20 61 6D 0350 20 74 61 73 0354 6B 31 2E 00 0358 32 32 32 32 MsgTk2: DB "2222", 0 035C 00 035D 33 33 33 33 MsgTk3: DB "3333", 0 0361 00 0362 0D 0A 53 65 MsgBye: DB CR, LF, "See you again.", 0 0366 65 20 79 6F 036A 75 20 61 67 036E 61 69 6E 2E 0372 00 ORG 1000H 1000 SavSP: DS 2 ; original SP save area ORG 2000H 2000 WORKST EQU $ 2000 RUNTSK: DS 1 ; running task No. 2001 RDYLNK: DS 2 * MAX_PRI ; ready task que if 1 IRPC X,0123 TCB&X: DS TCBSIZ ENDM 2009 + TCB&0: DS TCBSIZ 2011 + TCB&1: DS TCBSIZ 2019 + TCB&2: DS TCBSIZ 2021 + TCB&3: DS TCBSIZ else X SET 0 REPT TASKS TCB&X: DS TCBSIZ X SET X+1 ENDM endif 2029 TIMLNK: DS 2 ; timer wait task que 202B WORKEN EQU $ 002B WORKSIZ EQU WORKEN - WORKST END Macros: Symbols: 0205 ADDLNK 018C ADDRDY 0005 BDOS 028C COUTADR 000D CR 0002 DFPUTC 014C DISP10 0147 DISPATCH 0156 DISPFND 01F3 GETRTCB 0262 HOOKINT 0276 INIAP 0120 INIR10 0134 INIR20 0100 INIRTM 0232 INITCB 0039 INTADR 0010 INTTIM 000A LF 0004 MAX_PRI 0362 MSGBYE 0305 MSGSTART 033C MSGTK1 0358 MSGTK2 035D MSGTK3 00F0 MSKPRI 000F MSKTNO 0000 PRI0 0010 PRI1 0020 PRI2 0030 PRI3 0284 PUTCH 0295 PUTS 2001 RDYLNK 017B REMO10 016F REMOVE 0211 ROBIN 2000 RUNTSK 021F SAVESP 01A4 SAVINT 1000 SAVSP 8000 STACK 7F36 STACK0 7F76 STACK1 7FB6 STACK2 7FF6 STACK3 000A STREGSIZ 02A5 TASK0 02A9 TASK1 02B6 TASK1_1 02D7 TASK2 02EE TASK3 0004 TASKS 2009 TCB0 2011 TCB1 2019 TCB2 2021 TCB3 0008 TCBSIZ 0252 TCBTBL 0004 TCEVCNT 0000 TCLIST 0005 TCSP 0007 TCSTAT 0000 TCST_NO 0002 TCST_RDY 0001 TCST_RUN 0003 TCST_WAT 0003 TCTIM 0000 TCTOP 0002 TCTSKNO 025A TENTRY 01A6 TIM00 01B9 TIM10 01D1 TIM20 01D4 TIM30 01A3 TIMER 2029 TIMLNK 0001 TSK0NO1 0002 TSK0NO2 0003 TSK0NO3 0000 TSK0NOO 02DC TSK2LP 02F3 TSK3LP 0040 TSKSPSIZ 01DB TWAIT 026F UNHOOK 202B WORKEN 002B WORKSIZ 2000 WORKST No Fatal error(s)



 Twitter(X)に投稿した動画付きメッセージを貼っておきます。




★追記 2023/10/05
 上記の DI 中に割込みが入っている現象の原因が判りました。
 下記の割込みテストプログラムで確認した結果が下の画面です。繰り返し行っても同様の結果でした。BIOS の1文字表示処理の ROM 内のコードで EI を実施しているため、割込みが入っているようです。

割込み試験実施結果


 本確認で実施した割込み試験プログラムのリストを下記に示します。割込みが入った場合、リターンアドレスとコール元のコードを保存するようにしました。

割込み試験プログラムのソース(Z80アセンブラ)
;+++++++++++++++++++++++++++++++++++ ; IntTest : MSXPen interrupt test ; Ver 0.01 2023/10/05 by skyriver ;++++++++++++++++++++++++++++++++++++ ; *** MSX *** 0039 INTADDR EQU 038H + 1 ; vsync interrupt 0000' ASEG ORG 0100H 0100 2A 0001 Start: LD HL,(1) ; get WBOOT addr 0103 11 0009 LD DE,3 * 3 0106 19 ADD HL,DE ; HL = conout 0107 22 0134 LD (COUTADR),HL 010A 21 0000 LD HL,0 010D 22 0200 LD (SavRET),HL 0110 F3 DI 0111 2A 0039 LD HL,(INTADDR) ; get interrupt addr 0114 22 015C LD (SavInt),HL 0117 21 0137 LD HL,DmyInt 011A 22 0039 LD (INTADDR),HL 011D 06 80 LD B,128 011F C5 Loop: PUSH BC 0120 3E 41 LD A,'A' 0122 CD 0132 CALL Putch 0125 C1 POP BC 0126 10 F7 DJNZ Loop 0128 2A 015C LD HL,(SavInt) 012B 22 0039 LD (INTADDR),HL 012E FB EI 012F C3 0000 JP 0 ; putchar ; A <- data ; 0132 4F Putch: LD C,A 0133 CD 0000 CALL 0 0134 COUTADR EQU $ - 2 0136 C9 RET 0137 22 0202 DmyInt: LD (SavHL),HL 013A ED 53 0204 LD (SavDE),DE 013E ED 43 0206 LD (SavBC),BC 0142 E1 POP HL ; get return addr 0143 E5 PUSH HL 0144 22 0200 LD (SavRET),HL 0147 2B DEC HL 0148 11 0210 LD DE,SavCode 014B 01 0020 LD BC,CSAVLEN 014E ED B0 LDIR 0150 2A 0202 LD HL,(SavHL) 0153 ED 5B 0204 LD DE,(SavDE) 0157 ED 4B 0206 LD BC,(SavBC) 015B C3 0000 JP 0 015C SavInt EQU $ - 2 ORG 0200H 0200 SavRET: DS 2 0202 SavHL: DS 2 0204 SavDE: DS 2 0206 SavBC: DS 2 ORG 0210H 0210 SavCode: 0210 DS 32 ; code save area 0020 CSAVLEN EQU $ - SavCode END Macros: Symbols: 0134 COUTADR 0020 CSAVLEN 0137 DMYINT 0039 INTADDR 011F LOOP 0132 PUTCH 0206 SAVBC 0210 SAVCODE 0204 SAVDE 0202 SAVHL 015C SAVINT 0200 SAVRET 0100 START No Fatal error(s)



★追記 2023/10/06
 上記の様にシステムコール内で割込み許可していることが判ったので、システムコール時は割込みは許可状態のままにしてタスクをディスパッチしない様にしてみました(システムコール毎のタイマーウェイトを無くし、ラウンドロビンコールを追加)。
 その結果、下図のように表示が競合する場合には互いに1文字ずつ表示するようになりました。割込み処理の空振りが無いのでタイマー管理も正確になったはずです。

割込み対処後のデモ画面例


 MSX-DOS 上で GAME 言語を使って BIOS 内の EI(0FBH)を検索した結果を貼っておきます。38H の割込みのエントリーが 0DDA9H なので割込み処理も含まれているのでしょうが結構あるものですね。
 ざっと見た感じ、最後の二つは相対ジャンプのオフセット値ですが、他は EI 命令のコードのようですね。

MSX-DOS BIOS 内の 0FBH コードのサーチ結果



[TOP] [ 前へ ] 連載記事一覧 [ 次へ ]
nice!(0)  コメント(0) 
共通テーマ:趣味・カルチャー

FizzBuzz問題(独自解法) [Z80]

 今回は Twitter(X) のタイムラインで見かけた Fizz Buzz 問題の解法について書いてみたいと思います。ウィキペディアにも書いてあるように Fizz Buzz 問題とは1から100までを言い合う遊びで3の倍数の場合は数字の代わりに Fizz と言い、5の倍数の場合は Buzz、両者の倍数の場合は Fizz Buzz と言うゲームです。

 FizzBuzz 問題を解くプログラムを作ること自体は容易なのですが、面白くするために高速化という観点から剰余算を使わないという制約を設けます。また、FizzBuzz の間にはスペースを入れず、それぞれの数字や 単語の後にはスペースを1個入れることとします。

 素数を求める時によく使われる「エラトステネスの篩(ふるい)」でのフラグ設定のように3周期のカウンタと5周期のカウンタを持つことでそれぞれの倍数であることを判断できます。
 最初に考えたC言語のソースは次のようなものでした。

FizzBuzz 問題の解答プログラム(C言語)
/* * FizzBuzz Solver Ver 0.01 2023/09/21 by skyriver */ #include <stdio.h> int main( void ) { for( int i=1, f=3, b=5; i <= 100; i++ ) { if( --f == 0 ) { f = 3; printf( "Fizz" ); } if( --b == 0 ) { b = 5; printf( "Buzz" ); } else if( f != 3 ) { printf( "%d", i ); } putchar( ' ' ); } return 0; }


 ネット上のコンパイラ環境サービスで実行結果を確かめることもできます(便利な世の中になったものですね)。

 上述のように3と5のカウンタを持っているので全体は15周期の繰り返しになり、4ビットでカウントできます。ならば15周期のカウンタだけを使って処理できないものでしょうか?

 ここで15以内の3と5の倍数について考えてみます。両者の倍数である15を除外して列挙すると
  • 3の倍数
     0011,0110,1100,1001
  • 5の倍数
     0101,1010
となり、いずれも1が2個で巡回ローテートしたものを同一視すると2つのパターン(0011,0101)に縮退されます。かつ、1が2個のパターンを網羅しています。
 更に面白いことに相互を XOR してみると他のパターンに推移するので、これらの3の倍数と5の倍数は2進数の観点で見ると密接な関係があるかのように思えてきます。
 この特徴を利用して Z80 のアセンブリ言語で FizzBuzz 問題を解くプログラムを作ってみることにしました。上記は独自の解析なので恐らくこの手法による FizzBuzz プログラムは世界初だと思いますw

 1のビットの数が2個だった場合、上述のように3か5の倍数ということになるのですが、どちらに属するかの判断方法は簡単そうに見えて中々思いつきませんでした。
 最も単純なのは

   CP 5
   JR Z,Buzz
   CP 10
   JR Z,Buzz

のような8バイトものコードになってしまいます。漸く思い付いたのが次の方法です。

   SUB 5
   OR A
   JP PE,Buzz

如何でしょう。引き算後のビット1の総数(パリティ)で判断できました(ほかに良い方法があればコメント下さい)。

★追記 2023/09/23 {
 もっと判り易く、簡潔な判定方法を思い付きました^^
   AND 5
   JP PE,Buzz
}

★追記 2023/09/23 {
 ビット数の判定をループ処理方式

  L1: LD BC,4 * 256 + 0
    XOR A ; clear bit counter
    LD L,D
  L2: RRC L
    ADC A,C
    DJNZ L2
    CP 2
    JR NZ,DspNum

 から下記のループ処理無しの方式に改善してみました。

    LD A,D
    OR A
    JP PO,DspNum
    CP 0FH
    JR Z,Both
}

 できたソースを下記に示します。CP/M用に作成していますがDOSコールしているのは1文字出力サービスのみですので他の環境にも移植は容易だと思います。メインループ処理の簡潔さを堪能してください。

FizzBuzz 問題の解答プログラム(Z80アセンブリ)
; FizzBuzz solver by skyriver ; Ver 0.02 2023/09/22 ; Ver 0.03 2023/09/23 ; Ver 0.04 2023/09/23 ; Ver 0.05 2023/09/23 ; Ver 0.05 2023/09/23 ; Ver 0.06 2023/09/24(95 bytes) ; Ver 0.07 2023/09/27 improve DspNum(93 bytes) 0005 DosSrv EQU 5 ; DOS Service 0002 FnCout EQU 2 ; console out(E:data) 0064 MaxNum EQU 100 ; repeat until this number 0000' ASEG ORG 0100H 0100 01 64F1 Start: LD BC,256 * MaxNum + 0F1H ; B:loop counter, C:15 counter 0103 79 Loop: LD A,C 0104 21 0153 LD HL,MsgFiz 0107 0C INC C 0108 28 17 JR Z,Both ; if 15 010A B7 OR A 010B E2 0128 JP PO,DspNum ; if 1,2,4,7,8,11,13,14 010E E6 05 AND 5 0110 E2 0116 JP PO,Dsp ; if x3 0113 21 0158 DspBuz: LD HL,MsgBuz 0116 CD 013D Dsp: CALL Puts 0119 3E 20 Next: LD A,' ' 011B CD 0148 CALL Putch 011E 10 E3 DJNZ Loop 0120 C9 RET 0121 0E F1 Both: LD C,0F1H ; restart 15 counter 0123 CD 013D CALL Puts 0126 18 EB JR DspBuz 0128 3E 65 DspNum: LD A,MaxNum + 1 012A 90 SUB B 012B 11 FF0A LD DE,256 * (-1) + 10 012E 6F DspN10: LD L,A 012F 93 SUB E 0130 14 INC D ; not affect to Cy 0131 30 FB JR NC,DspN10 0133 7A LD A,D ; get 10s digit 0134 C4 0146 CALL NZ,PutNum 0137 7D LD A,L 0138 CD 0146 CALL PutNum 013B 18 DC JR Next ; put string ; HL <- string addr ; 013D 7E Puts: LD A,(HL) 013E B7 OR A 013F C8 RET Z 0140 CD 0148 CALL Putch 0143 23 INC HL 0144 18 F7 JR Puts ; put number with ascii ; A <- data 0146 C6 30 PutNum: ADD A,'0' ; put char ; A <- data 0148 C5 Putch: PUSH BC 0149 E5 PUSH HL 014A 5F LD E,A 014B 0E 02 LD C,FnCout 014D CD 0005 CALL DosSrv 0150 E1 POP HL 0151 C1 POP BC 0152 C9 RET 0153 46 69 7A 7A MsgFiz: DB "Fizz", 0 0157 00 0158 42 75 7A 7A MsgBuz: DB "Buzz", 0 015C 00 END Start Macros: Symbols: 0121 BOTH 0005 DOSSRV 0116 DSP 0113 DSPBUZ 012E DSPN10 0128 DSPNUM 0002 FNCOUT 0103 LOOP 0064 MAXNUM 0158 MSGBUZ 0153 MSGFIZ 0119 NEXT 0148 PUTCH 0146 PUTNUM 013D PUTS 0100 START No Fatal error(s)


 上のプログラムの出力結果は次のようになります。

1 2 Fizz 4 Buzz Fizz 7 8 Fizz Buzz 11 Fizz 13 14 FizzBuzz 16 17 Fizz 19 Buzz Fizz 22 23 Fizz Buzz 26 Fizz 28 29 FizzBuzz 31 32 Fizz 34 Buzz Fizz 37 38 Fizz Buzz 41 Fizz 43 44 FizzBuzz 46 47 Fizz 49 Buzz Fizz 52 53 Fizz Buzz 56 Fizz 58 59 FizzBuzz 61 62 Fizz 64 Buzz Fizz 67 68 Fizz Buzz 71 Fizz 73 74 FizzBuzz 76 77 Fizz 79 Buzz Fizz 82 83 Fizz Buzz 86 Fizz 88 89 FizzBuzz 91 92 Fizz 94 Buzz Fizz 97 98 Fizz Buzz

 上記のソースよりは少し前のバージョンですが、ネット上のサービスとして公開されている MSX のエミュ環境である MSXPenでも実行できました(重ねて便利な世の中に・・)。


nice!(0)  コメント(0) 
共通テーマ:趣味・カルチャー

シリアルEEPROMでCP/M [Z80]

 SDカードは使用制限があるので自作基板で使用した場合、公開し辛い面がありました。SDカードの中でも使用されているであろうEEPROMを使って CP/M を起動できないか実験しました。

 秋月さんで販売している1MbitのシリアルEEPROM(S-24CM01CIーJ8T1U4)を購入していましたが、今回はより容量が大きな海外の通販で見つけた 32MBit EEPROM(W25Q32JVSIQ) を使って実験してみました。後者の方が書込み速度が遅く、許容している書込み回数も少ないのですが容量の大きさを優先しました。
 最近、秋月さんで 4Mbit のフラッシュメモリ(IS25LP040E) を販売開始したようです。

 今回使用した 32MBit EEPROM(W25Q32JV) のブロック図が下図になります。消去時の単位は 4KBytes/32KByte/64KByte/chiperase と柔軟性があり、最小単位は 4KByte です。

EEPROM(W25Q32JV)のブロック図


 EEPROM とのインターフェースは基本的には SPI で今回は GAL を使って Z80 側のソフトウェアで対応することにしました。この EEPROM はデータ線が 2bit や 4bit の拡張インターフェースの機能もあるのでソフト対応しておくことで後日高速化できる余地を残すという意図もあります。

 まずはデータ線が 1bit の SPI インターフェースでの接続実験を行いました。
 下図は今回の EEPROM 評価時の回路です。上述のように GAL(GAL22V10D)を使用して EEPROM を接続しています。

EEPROM 評価用回路図


 実験環境は下図のように Z80GalCompact の CPU ソケットを拡張して EEPROM を実装しています。

Z80GalCompact に接続した EEPROM 基板


EEPROM 部のZOOM


 下図はデバイスの ID を読込んだ際のロジアナ波形です。ソフト対応での転送ビットレートは送信/受信でそれぞれ 770KB/590KB 程です。Z80GalCompact は Z80 が 20MHz で動作していますが、シリアル通信用のタイマー割込みのため実質速度は 16MHz 程度です。

デバイスID読込み時のロジアナ波形例


 下図はステータス1~3の書込み後に読み出した際のロジアナ波形です。今回使用した EEPROM は型名の最後の Special Optoins が'Q'で Status2 の QE bit が 1 に固定になっています。

ステータスリード/ライト時のロジアナ波形例


 下図はデータリード時の波形で先頭の 0x03 がリードコマンドで次の 3 バイトがリード対象のアドレスでその後にリードデータが続きます。複数個所でクロックに隙間が発生しているのはシリアル通信のためのタイマー割込みのためです(この波形はCP/M起動時のもので割込み許可状態で動作している)。
 リードデータ長に関しては自由にリードできるのですが、ライトの場合は、事前にデータを消去する必要があり、消去の最小単位が前述したように4Kバイトなので CP/M で使う場合のブロック長は最小でも4Kバイトということになります。

シリアルEEPROMリード時のロジアナ波形例


 CP/M で使用する場合のブロック長は前述のように消去の最小単位である4Kバイトにし、ディスクパラメータを新規に作成し、ブロッキング/デブロッキング処理を実装しました。
 下図は CP/M を立ち上げて STAT コマンドで DSK: のパラメータを表示した際の画面キャプチャーです。モニタで CP/M の HEX ファイルをロード後、GO コマンドで起動していますが、 シリアル送受信の割込みルーチンをBIOS 内の処理へ切替えている関係でスタートメッセージの先頭が文字化けしていますね。4KB のバッファを BIOS 内に持っているため 59K CP/M です。
 アクセス速度に関しては SD カード時との差異を殆ど感じずに違和感の無い速度でした(消去に時間が掛かるので大量書込み時は遅く感じるかもしれません)。

DSK:パラメータ表示画面例


 冒頭で書いたように SD カードを利用して開発した CP/M 基板を公開し辛かったのですが、今回シリアル EEPROM で CP/M を起動できたので色々応用できそうです。

★追記 2023/06/06 {
 HITECH-C で hello.c をコンパイルするのに要する時間を比較してみました。下の画面キャプチャで前半がSDカードでブートした従来の CP/M で後半が EEPROM でブートした CP/M です。コンパイル時間を TeraTerm のマクロで計測した結果が "Start=" と書かれた行になります。特にリンク時間(=書込み量が多い)が遅い感じで EEPROM の場合、1.75倍程度の時間が掛かっています。

HITECH-Cでのhello.cコンパイル時間比較
}

★追記 2023/06/13 {
 今回使用している EEPROM は読出しに関しては任意のアドレスから任意長のリードができるので EEPROM バッファにヒットしない場合には EEPROM からデータを読込んで DMA 領域に直に書き込むようにしました。このようにブロッキング/デブロッキング処理をハイブリッド化することで読込み時のバッファリング処理を極力省き高速化しました。
 この変更後の HITECH-C で hello.c をコンパイルするのに要した時間を測定した結果が下図になります。前半が SD ブートで後半が EEPROM ブートになります。なんと SD カード上でのコンパイルより高速になりました^^

EEPROM上でのhello.cコンパイル時間(改善版)
}

 Twitterに投稿した動画付きメッセージも貼っておきます。




タグ:Z80GAL W25Q32JV
nice!(0)  コメント(0) 
共通テーマ:趣味・カルチャー

Z80でのDAA命令によるHEX変換テクニック [Z80]

 Z80 等で BCD 演算のための補正命令である DAA を使ってバイナリ(00H..0FH)をヘキサの文字コード('0'..'9','A'..'F')に巧妙に変換する処理が知られています。

 今回はこのバイナリ to HEX 変換処理でヘキサ文字を小文字にしたい場合の処理を考えたのでブログに記録しておきたいと思います。

 最初に結論から書いてしまうと今回考えた下記のコードで 00H..0FH のバイナリを '0'..'9','a'..'f' に変換できます。


  D6  0A    SUB    10  
  27DAA
  27DAA
  27DAA
  DE  9FSBC    A,9FH



 それでは上記のコードに辿り着いた経緯について以下に書きます。

  1. DAA 使用の一般的なヘキサ変換
     例えば居酒屋ガレージ日記の「プログラムテクニック「DAA」」の記事に書かれているように


      ADD    A,90H  
    DAA
      ADC    A,40H  
    DAA


    の6バイトのコードで 00H..0FH を '0'..'9','A'..'F' に変換できます。
     入力が 0AH 以上の場合は 最初の DAA で上位ニブルに桁上りし、更に上位ニブルが桁上りして 0 になり、キャリーが立つので2回目の DAA で上位ニブルは 4 となり、下位ニブルには キャリーも加算されるので最初の値から 9 が引かれた値になります。
     入力が 09H 以下の場合は最初の DAA では上位ニブルが 9 となることで 2回目の DAA で上位ニブルが 4-1=3 になります。
     BCD の桁上りとキャリーの効果を巧妙に組み合わせた処理ですね。

    ★追記 2023/05/24 {
     このような6バイトの変換コードは他にも存在します。例えば先頭の"ADD A,90H"は"OR 90H"でもいいし、"ADC A,40H"が"ADC A,3AH"でも結果は同じです。
     下記は今回考えたコードで引き算方式にしたもので、ここ以外では多分見かけないのではないかと思います。

    my idea 1

      SUB    0AH  
    DAA
      SBC    A,59H  
    DAA


    my idea 2

      SUB    0AH  
    SBC    A,25H
      CCF      
    DAA

    ※追記 2023/05/27 CCFではハーフキャリーが不定なので上記は環境依存性があります
    }
    ★追記 2023/05/26 {
     最初に OR を実行してハーフキャリーをクリアすれば普通に変換できます。

    my idea 3

      OR     A  
    DAA
    CP    10  
    SBC   0CFH


    my idea 4

      OR     A  
    DAA
    ADD    A,0F0H  
    ADC    A,40H

    }


  2. 更に短いコード
     上記の様なヘキサ変換で必要な処理の要素を列挙すると

    1. 下位ニブルの桁上り
       入力が 0AH 以上の場合は下位ニブルから上位ニブルに桁上りが発生するようにして上位ニブルを +1 する。
    2. 下位ニブルへの+1
       入力が 0AH 以上の場合は下位ニブルに +1 されるようにする('A'のアスキーコードは 41H なので xAH を x1H にする)

    となることが判ります。
     例えば Twitterのタイムラインで見つけた


      CP    10  
    SBCA,69H  
    DAA


    の5バイトのコードでもヘキサ変換が可能です。SBC を絶妙に組み合わせることで上記の2つの必要な要素が実現されています。

     キャリーを加算することで同じようなことが出来ないか考えてみました。入力が 0AH 以上の場合にキャリーを発生させる必要があるので


      ADD    A,0F6H  
    ADCA,3AH
    DAA


    としてみましたが、ADC 後も入力が 0AH以上の場合と 09H 以下の場合でキャリーの値が異なるので DAA の作用が不統一になり破城しましたw
     CP 命令は減算処理はするが A-reg は変更しないという命令ですが加算でも同様にフラグは更新するが A-reg を変更しない命令があればいいのですが・・・
     1バイト増えてしまいますが下記の様にキャリーの操作を加えると上手く行きました(処理内容も判り易いですね)。


      CP    10  
    CCF
    ADCA,30H  
    DAA



  3. 小文字のヘキサ変換
     上記の様に ADD や SUB でキャリーを操作した場合、次の加算または減算で 入力が 0AH 以上の場合と 09H 以下の場合でどちらかがバイトのバウンダリーを超えるので、キャリーが不一致な状態となり、DAA で不要に +6 や -6 されるので、5バイトでのヘキサ変換処理は上記の方法以外は無いのではないかと推測します。

     と言うわけで趣向を変えて小文字のヘキサ変換について挑戦してみました。DAA 命令の複雑な挙動に関しては「Zilog Z80 DAA実行結果」にまとめられているのを見つけたので、これを参考にして辿り着いたのが冒頭に書いたコードになります。

     下記のような検証用のプログラムで全てのケースについて確認を行いました。

    1バイトの小文字ヘキサ変換試験プログラム(Z80アセンブラ)
    ;+++++++++++++++++++++++++++++++++++++++++++++++ ; translate 0x00-0x0f to hex ascii code '0'-'f' ; Ver 0.01 2023/05/22 by skyriver ;+++++++++++++++++++++++++++++++++++++++++++++++ 000D CR EQU 13 000A LF EQU 10 0001 SMALL EQU 1 ; lower case ToHex macro if SMALL EQ 0 CP 10 SBC A,069H DAA else SUB 10 DAA DAA DAA SBC A,09FH endif endm 0000' ASEG ORG 0100H 0100 AF XOR A 0101 F5 LOOP: PUSH AF 0102 CD 0116 CALL TRAN 0105 3E 20 LD A,' ' 0107 CD 0135 CALL PUTC 010A F1 POP AF 010B 3C INC A 010C F5 PUSH AF 010D E6 0F AND 0FH 010F CC 013C CALL Z,NEWLN 0112 F1 POP AF 0113 20 EC JR NZ,LOOP 0115 C9 RET ; translate to hex ascii code ; A <- data(00-0f) 0116 F5 TRAN: PUSH AF 0117 0F RRCA 0118 0F RRCA 0119 0F RRCA 011A 0F RRCA 011B E6 0F AND 0FH ToHex 011D D6 0A + SUB 10 011F 27 + DAA 0120 27 + DAA 0121 27 + DAA 0122 DE 9F + SBC A,09FH 0124 CD 0135 CALL PUTC 0127 F1 POP AF 0128 E6 0F AND 0FH ToHex 012A D6 0A + SUB 10 012C 27 + DAA 012D 27 + DAA 012E 27 + DAA 012F DE 9F + SBC A,09FH 0131 CD 0135 CALL PUTC 0134 C9 RET ; put chara on screen ; A <- data 0135 PUTC: 0135 5F LD E,A 0136 0E 02 LD C,2 0138 CD 0005 CALL 0005H 013B C9 RET ; new line 013C 3E 0D NEWLN: LD A,CR 013E CD 0135 CALL PUTC 0141 3E 0A LD A,LF 0143 CD 0135 CALL PUTC 0146 C9 RET END Macros: TOHEX Symbols: 000D CR 000A LF 0101 LOOP 013C NEWLN 0135 PUTC 0001 SMALL 0116 TRAN No Fatal error(s)

     実機の CP/M-80 環境での確認結果が下図になります。

    CP/M-80 環境での確認結果(OK)

     ところが Windows の DOS 窓での CPM エミュ環境では下図のように NG でした。尚、プロンプトで表示されているパス情報はわざとぼかしています。

    CP/M エミュ環境での確認結果(NG)

     CP/M エミュでは DAA を実行した後のフラグの挙動が Z80 と違うことが原因の様です。下図は Z80 の実機(Z84C0020PEC)を使って CP/M 環境で ZSID でステップ動作した結果です。

    Z80 実機でのステップ動作

     下図が CP/M エミュでのステップ動作になります。DAA 命令での 'I'フラグの挙動が上記の実機と違いますね。尚、'I'フラグはハーフキャリーです。

    CP/M エミュでのステップ動作(NG)

     GAME 言語を使って 00H..0FH のどの範囲が NG なのか確認してみた結果が下図になります。上記の結果からも判っていましたが、00H..09H の範囲がNGの様です。

    CP/M エミュ環境での確認結果(NG)

     エミュということで試しに MSXPen 環境で確認した結果が下図になります。MSXPen 環境では流石に問題無いようです。

    MSXPen 環境での確認結果(OK)


★追記 2023/05/26
 その後も少し気になったので下記のパターンで自動スキャンしてみました。xxとyyは 00H..FFH の範囲でフルスキャンし、ニーモニックの部分はそれぞれのパタンの全組合せを自動チェックしています。

CP/OR/ADD/SUB xx; NOP/DAA; ADC/SBC yy; NOP/DAA

 結果は下図の通りで上記で既に記述したもの以外はありませんでした。「*** Find!!」以降の部分が見つけた解を表示している部分でそれ以外の表示はスキャンの進捗が判るように表示しているものです。
 尚、各ニーモニックのオペコードは

CP:FE,OR:F6,ADD:C6,SUB:D6,NOP:00,DAA:27,ADC:CE,SBC:DE

です。

変換コードパターンのスキャン結果


 2バイト分のフルスキャンとオペコード切替えなので全チェックパターンは 210 万(=4*256*2*2*256*2)程ですが、上記の画面で最後に表示されているようにほぼ4分でスキャンが完了しました。環境は Z80 20MHz で GAME コンパイラを使用しています。ソースはこれです。

★追記 2023/05/26
 ヘキサ文字への変換は最小単位としては1バイトを2桁のヘキサ文字に変換するケースが殆どなのではないかと思います。下位のニブルを取り出す時に AND 0FH を使用するとハーフキャリーが立ってしまうので厄介なのですが、OR 0F0H であればハーフキャリーが立たない(当然キャリーもゼロ)ので最初から DAA が使えます。入力データが F0H、FAH の場合、DAA 後は期待通りにそれぞれが 50H、60H になります。後は下記の様にキャリーを立てて加算/減算で調整すれば実質5バイトのコードで変換できます。
 以上より、現時点ではヘキサ文字に変換する5バイトコードは3通りあるということが判りました。

OR 0F0H method 1
  DAA      
CP60H
  SBC    A,1FH  

OR 0F0H method 2
  DAA      
ADDA,0A0H
  ADC    A,40H  



★追記 2023/08/18
 Twitter(X?)で関連するメッセージを見かけたのでこのページを紹介して、自分でも再度見てみました。最初に OR する方式で AND 方式の様に CP から始めるパターンもできることに気が付きました。

OR 0F0H method 3
  CP    0FAH  
SBCA,0B9H
  DAA      


nice!(0)  コメント(0) 
共通テーマ:趣味・カルチャー

CP/M-80でのBIOSエミュレートによるMBASICの起動 [Z80]

 Twitter のタイムラインで CP/M-80 版 MBASIC は BIOS のコンソール入出力機能のみでも基本動作はするという情報を見かけたので確認して見ました。

 最近使っている 20MHz 動作の Z80PicCompactの開発機は CP/M が自動起動する設定にしているので、「CP/M用hexローダーの製作」の記事で紹介した HexLoader を使ってヘキサファイルをロードして実験しました。

 MBASICを起動するためにはBIOS のコンソール関連の実装以外に下記の項目への対応が必要でした。
  1. BDOS コールアドレスの設定
     MBASIC は BDOS コール(0005H)部分の BDOS アドレスを参照して、BDOSCCP 領域も使用するので BDOS コール用のジャンプ命令を設定する必要がある。
    ★修正 2023/05/17
     MBASIC はディスクアクセス機能が必要なので BDOS 領域は壊さないことを確認しました。


  2. コマンドパラメータ無しの設定
     MBASIC は起動時に 0080H 部分にあるコマンドパラメータを参照してパラメータがある場合、ソースをロードしようとします。0080H を 00H に設定することでパラメータが無い状態にします(この部分は上記の HexLoader が使用するメモリなのでこのために HexLoader をアップデートしました)

上記の項目を対応することで無事 MBASIC が起動するようになりました。

 殆ど処理が無いですが、ソースは下記になります。

CP/M BIOS エミュレータのソース
; CP/M-80 for Z80PicCompact ; Ver 0.01 2023/01/14 by skyriver ; based on CP/M-80 bios for Z80GAL ; Ver 0.01 2020/11/07 by skyriver ; Ver 0.02 2021/10/02 by skyriver ; save BC-reg at CONOUT ; test for Emurate CP/M BIOS environment 2023/05/15 ;++++ CP/M parameter ++++ 0006 MAXDSK EQU 6 ;number of drives. 0040 MAX_SEC EQU 64 ;sectors/track 0006 MAX_SIF EQU 6 ; MAX_SEC = 1 << MAX_SIF 002E MSIZE EQU 46 ;size of available RAM in KB 6800 BIAS EQU (MSIZE-20) * 1024 9C00 CCP EQU 3400H+BIAS ;base of cpm ccp A406 BDOS EQU CCP+806H ;base of bdos B200 BIOS EQU CCP+1600H ;base of bios 0004 CDISK EQU 0004H ;current disk number (0 ... 15) 0003 IOBYTE EQU 0003H ;intel iobyte 0080 BUFF EQU 0080H ;default buffer address 000D CR EQU 13 000A LF EQU 10 0080 SECSIZE EQU 128 ; sector size 0800 CPMBKSZ EQU 2048 ; CP/M block size 0200 SDBLKSZ EQU 512 ; SD card block size 0038 RSTAD EQU 038H ; interrupt entry adr 0100 CPM_AP EQU 0100H ; CP/M app entry ;++++ Z80PicCompact definition +++++++++++++++++++++++++++++++++++++++ 0000 DATPORT EQU 0 ; data port 0001 CMDPORT EQU 1 ; command port ; +++ status bit allign +++ 0001 RXRDY_B EQU 01H 0080 TXRDY_B EQU 80H 0002 SDERR_B EQU 02H ; +++ mode command +++ 0001 SIO_CHAR EQU 1 ; char mode 0002 SIO_STR EQU 2 ; put string 0003 SD_SECT EQU 3 ; set sector 0004 SD_RD EQU 4 ; read block 0005 SD_WRBUF EQU 5 ; write data to buffer 0006 SD_WR EQU 6 ; write block .Z80 0000' ASEG ORG BIOS B200 C3 B256 JP BOOT B203 WBOOTE: B203 C3 B299 JP WBOOT B206 C3 B280 JP CONST B209 C3 B288 JP CONIN B20C C3 B28B JP CONOUT B20F C3 B29A JP LIST B212 C3 B29A JP PUNCH B215 C3 B29A JP READER B218 C3 B29A JP HOME B21B C3 B29A JP SELDSK B21E C3 B29A JP SETTRK B221 C3 B29A JP SETSEC B224 C3 B29A JP SETDMA B227 C3 B29A JP READ B22A C3 B29A JP WRITE B22D C3 B29A JP LISTST B230 C3 B29A JP SECTRAN B233 StMsg: B233 0D 0A 34 DB CR,LF,MSIZE/10+'0' B236 36 DB (MSIZE mod 10) + '0' B237 6B 20 43 50 DB 'k CP/M Ver 2.2 BIOS Emulator' B23B 2F 4D 20 56 B23F 65 72 20 32 B243 2E 32 20 42 B247 49 4F 53 20 B24B 45 6D 75 6C B24F 61 74 6F 72 B253 0D 0A 00 DB CR,LF,0 B256 F3 BOOT: DI B257 ED 56 IM 1 B259 31 0080 LD SP,BUFF ;; XOR A ;; LD (IOBYTE),A ;; LD (CDISK),A ;; LD (SelDk),A B25C 3E C9 LD A,0C9H B25E 32 0038 LD (RSTAD),A B261 21 B233 LD HL,StMsg B264 CD B28F CALL Puts ;; JR GOCPM B267 GOCPM: B267 3E C3 LD A,0C3H B269 32 0000 LD (0),A B26C 21 B203 LD HL,WBOOTE B26F 22 0001 LD (1),HL B272 32 0005 LD (5),A B275 21 A406 LD HL,BDOS B278 22 0006 LD (6),HL B27B 36 C9 LD (HL),0C9H ; return code ;; LD BC,BUFF ;; CALL SETDMA ;; LD A,(CDISK) ;; LD C,A ;; JP CCP B27D C3 0100 JP CPM_AP ; ; check console key data ; A -> 0ffh:data exist 00h:none B280 CONST: B280 DB 01 IN A,(CMDPORT) ; get status B282 E6 01 AND RXRDY_B B284 C8 RET Z ; no data B285 3E FF LD A,0FFH B287 C9 RET ; ; input console ; A -> key data B288 DB 00 CONIN: IN A,(DATPORT) B28A C9 RET ; out console ; C <- data B28B 79 CONOUT: LD A,C B28C D3 00 OUT (DATPORT),A B28E C9 RET ; print string ; HL <- string addr B28F Puts: B28F 7E LD A,(HL) B290 B7 OR A B291 C8 RET Z B292 4F LD C,A B293 CD B28B CALL CONOUT B296 23 INC HL B297 18 F6 JR Puts B299 76 WBOOT: HALT B29A LIST: B29A PUNCH: B29A READER: B29A HOME: B29A SELDSK: B29A SETTRK: B29A SETSEC: B29A SETDMA: B29A READ: B29A WRITE: B29A LISTST: B29A SECTRAN: B29A C9 RETURN: RET END Macros: Symbols: A406 BDOS 6800 BIAS B200 BIOS B256 BOOT 0080 BUFF 9C00 CCP 0004 CDISK 0001 CMDPORT B288 CONIN B28B CONOUT B280 CONST 0800 CPMBKSZ 0100 CPM_AP 000D CR 0000 DATPORT B267 GOCPM B29A HOME 0003 IOBYTE 000A LF B29A LIST B29A LISTST 0006 MAXDSK 0040 MAX_SEC 0006 MAX_SIF 002E MSIZE B29A PUNCH B28F PUTS B29A READ B29A READER B29A RETURN 0038 RSTAD 0001 RXRDY_B 0200 SDBLKSZ 0002 SDERR_B 0004 SD_RD 0003 SD_SECT 0006 SD_WR 0005 SD_WRBUF 0080 SECSIZE B29A SECTRAN B29A SELDSK B29A SETDMA B29A SETSEC B29A SETTRK 0001 SIO_CHAR 0002 SIO_STR B233 STMSG 0080 TXRDY_B B299 WBOOT B203 WBOOTE B29A WRITE No Fatal error(s)


 Twitter に投稿した動画付きメッセージを貼っておきます。上記の BIOS エミュレータ+ MBASIC 本体のHEXファイルをロードすることで MBASIC を起動しています。
 BIOS エミュレータの WBOOT は HALT するようにしてあるので CP/M に戻るために元々の WBOOT(0xfa03)をコールしています。



nice!(0)  コメント(0) 
共通テーマ:趣味・カルチャー