//========================================================================================
//  PPI.java
//    en:8255 PPI -- It emulates joystick ports.
//    ja:8255 PPI -- ジョイスティックポートをエミュレートします。
//  Copyright (C) 2003-2025 Makoto Kamada
//
//  This file is part of the XEiJ (X68000 Emulator in Java).
//  You can use, modify and redistribute the XEiJ if the conditions are met.
//  Read the XEiJ License for more details.
//  https://stdkmd.net/xeij/
//========================================================================================

package xeij;

import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.util.*;
import javax.swing.*;
import javax.swing.event.*;

import com.fazecast.jSerialComm.*;

public class PPI {

  //ポート
  //  0x00e9a000-0x00e9bfffは下位3ビットだけデコードされる
  //  偶数アドレスをバイトサイズでリード/ライトするとバスエラーになる
  //  偶数アドレスをワードサイズでリードすると上位バイトは0xffになる
  //
  //  0x00e9a001  PPIポートA  ジョイスティック1
  //         7        6        5        4        3        2        1        0
  //    +--------+--------+--------+--------+--------+--------+--------+--------+
  //    |   PA7  |   PA6  |   PA5  |   PA4  |   PA3  |   PA2  |   PA1  |   PA0  |
  //    |    1   |   JS7  |   JS6  |    1   |   JS4  |   JS3  |   JS2  |   JS1  |
  //    |        |    B   |    A   |        |   →   |   ←   |   ↓   |   ↑   |
  //    +--------+--------+--------+--------+--------+--------+--------+--------+
  //    bit7    PA7  1
  //    bit6    PA6  JS7入力。0=Bが押されている
  //    bit5    PA5  JS6入力。0=Aが押されている
  //    bit4    PA4  1
  //    bit3    PA3  JS4入力。0=→が押されている
  //    bit2    PA2  JS3入力。0=←が押されている
  //    bit1    PA1  JS2入力。0=↓が押されている
  //    bit0    PA0  JS1入力。0=↑が押されている
  //
  //  0x00e9a003  PPIポートB  ジョイスティック2
  //         7        6        5        4        3        2        1        0
  //    +--------+--------+--------+--------+--------+--------+--------+--------+
  //    |   PB7  |   PB6  |   PB5  |   PB4  |   PB3  |   PB2  |   PB1  |   PB0  |
  //    |    1   |   JT7  |   JT6  |    1   |   JT4  |   JT3  |   JT2  |   JT1  |
  //    |        |    B   |    A   |        |   →   |   ←   |   ↓   |   ↑   |
  //    +--------+--------+--------+--------+--------+--------+--------+--------+
  //    bit7    PB7  1
  //    bit6    PB6  JT7入力。0=Bが押されている
  //    bit5    PB5  JT6入力。0=Aが押されている
  //    bit4    PB4  1
  //    bit3    PB3  JT4入力。0=→が押されている
  //    bit2    PB2  JT3入力。0=←が押されている
  //    bit1    PB1  JT2入力。0=↓が押されている
  //    bit0    PB0  JT1入力。0=↑が押されている
  //
  //  0x00e9a005  PPIポートC  ADPCMコントロール。初期値は0x0b
  //         7        6        5        4        3        2        1        0
  //    +--------+--------+--------+--------+--------+--------+--------+--------+
  //    |   PC7  |   PC6  |   PC5  |   PC4  |   PC3  |   PC2  |   PC1  |   PC0  |
  //    |   JS7  |   JS6  |   JT8  |   JS8  |      RATIO      |  LEFT  |  RIGHT |
  //    +--------+--------+--------+--------+--------+--------+--------+--------+
  //    bit7    PC7    JS7出力(負論理)
  //    bit6    PC6    JS6出力(負論理)
  //    bit5    PC5    JT8出力
  //    bit4    PC4    JS8出力
  //    bit3-2  PC3-2  ADPCM分周比。00=1/1024,01=1/768,10=1/512,11=inhibited
  //    bit1    PC1    ADPCM出力LEFT。0=出力する,1=出力しない
  //    bit0    PC0    ADPCM出力RIGHT。0=出力する,1=出力しない
  //
  //  0x00e9a007  PPIコントロール
  //    bit7=0  ポートCで出力に設定されているビットの操作
  //      bit3-1  ビット番号
  //      bit0    設定値
  //    bit7=1  モードの設定。0x92に固定
  //      bit6-5  グループA(ポートAとポートCの上位)のモード(0=モード0,1=モード1,2/3=モード2)。モード0に固定
  //      bit4    ポートAの方向(0=出力,1=入力)。入力に固定
  //      bit3    ポートCの上位の方向(0=出力,1=入力)。出力に固定
  //      bit2    グループB(ポートBとポートCの下位)のモード(0=モード0,1=モード1)。モード0に固定
  //      bit1    ポートBの方向(0=出力,1=入力)。入力に固定
  //      bit0    ポートCの下位の方向(0=出力,1=入力)。出力に固定
  //
  //  ボタンのマスク
  //    上と下または左と右のキーが同時に押された場合は両方キャンセルする
  //    SELECTボタンは上下同時押し、RUNボタンは左右同時押しに割り当てられる
  //
  //  bit4とbit7は入力できない
  //    8255の足がプルアップされているので実機ではどうすることもできない(外付けの回路でどうにかなるものではない)
  //    エミュレータで入力できるようにするのは簡単だか対応しているソフトは存在しないだろう
  //
  //  参考
  //    電脳倶楽部67号  B/MDPAD/M6PAD_AN.DOC
  //    電脳倶楽部77号  B/6B/T_EXPAD.DOC
  //
  public static final int PPI_PORT_A  = 0x00e9a001;   //PPIポートA
  public static final int PPI_PORT_B  = 0x00e9a003;   //PPIポートB
  public static final int PPI_PORT_C  = 0x00e9a005;   //PPIポートC
  public static final int PPI_CONTROL = 0x00e9a007;   //PPIコントロール

  //ジョイスティック
  public static Joystick[] ppiJoysticks;
  public static Joystick ppiJoystick1;
  public static Joystick ppiJoystick2;

  //モード
  public static boolean ppiJoyKey;  //true=キーボードの一部をジョイスティックとみなす
  public static boolean ppiJoyAuto;  //true=ポートが繰り返し読み出されている間だけ有効
  public static boolean ppiJoyBlock;  //true=ジョイスティック操作として処理されたキー入力データを取り除く

  //じょいぽーとU君
  public static final boolean PPI_UKUN_ON = true;  //じょいぽーとU君。false=使わない,true=使う
  public static final boolean PPI_UKUN_DEBUG = false;  //デバッグメッセージ。false=出力しない,true=出力する
  public static final boolean PPI_UKUN_DEBUG_IO = false;  //デバッグメッセージ。false=出力しない,true=出力する
  //  接続
  public static final int PPI_UKUN_VID = 0x04d8;  //ベンダーID
  public static final int PPI_UKUN_PID = 0xe6b3;  //プロダクトID
  public static boolean ppiUkunRequestedConnection;  //要求された接続。false=接続しない,true=接続する
  public static boolean ppiUkunCurrentConnection = false;  //現在の接続。false=接続していない,true=接続している
  //  シリアルポート
  public static String ppiUkunPortName;  //ポート名。"COM4"など
  //  モード
  public static final int PPI_UKUN_NOTIFY      = 0b0000_0000_0011_0001;  //Notifyモード
  public static final int PPI_UKUN_COMMAND     = 0b0000_0000_0011_0111;  //Commandモード
  public static final int PPI_UKUN_COMMAND_OLD = 0b0000_0000_0011_0010;  //Command(OLD)モード
  public static final int PPI_UKUN_TRANSITION  = 0b0000_0000_0000_0000;  //モード遷移中。I/Oポートのリードは前回の値が返り、ライトは無視される
  public static volatile int ppiUkunRequestedMode;//要求されたモード。PPI_UKUN_NOTIFYまたはPPI_UKUN_COMMAND
  public static volatile int ppiUkunCurrentMode;  //現在のモード。PPI_UKUN_NOTIFYまたはPPI_UKUN_COMMANDまたはPPI_UKUN_TRANSITION
  private static boolean ppiIsCommandOldMode = false; // COMMAND（OLD)モードか？
  //  データ
  public static volatile int ppiUkunPortA;  //ポートAのデータ。0b1..1_....
  public static volatile int ppiUkunPortB;  //ポートBのデータ。0b1..1_....
  public static volatile int ppiUkunPortC;  //ポートCのデータ。0b...._0000
  //  受信スレッド
  public static volatile boolean ppiUkunSending;  //false=送信中ではない,true=送信中
  //  COMMANDモード関係の値。
  private static volatile byte[] ppiCacheData = null;
  private static int ppiCacheDataPosition = 0;
  private static byte ppiLastSendAddr = -1;
  //  NOTIFYモード関係の値。
  private static int ppiControlWord = 0;
  private static int ppiControlWordNext = -1;
  //  ナノタイマー
  public static final boolean PPI_UKUN_NANO = false;  //I/Oポートのアクセス時間を計測する
  public static boolean ppiUkunNanoMeasuring;  //true=計測中
  public static long[] ppiUkunNanoBestArray;  //最短アクセス時間(ns)
  public static long[] ppiUkunNanoWorstArray;  //最長アクセス時間(ns)
  public static long[] ppiUkunNanoTotalArray;  //合計アクセス時間(ns)
  public static long[] ppiUkunNanoCountArray;  //アクセス回数
  //  ウェイト
  public static final long PPI_UKUN_WAIT = XEiJ.TMR_FREQ / (1000000 / 100);  //I/Oポートのアクセスに追加するウェイト(XEiJ.TMR_FREQ単位)。100us
  //  出力間隔調整
  public static final boolean PPI_UKUN_INTERVAL = true;
  public static final long PPI_UKUN_INTERVAL_LIMIT = XEiJ.TMR_FREQ / (1000000L / 500L);  //調整する間隔のずれの上限。500us
  public static boolean ppiUkunIntervalOn;  //true=出力間隔調整を行う
  public static long ppiUkunLastVirtualTime;  //前回のXEiJ.mpuClockTime
  public static long ppiUkunLastRealTime;  //前回のSystem.nanoTime()
  //  メニュー
  public static JCheckBox ppiUkunConnectCheckbox;  //接続チェックボックス
  public static JRadioButton ppiUkunNotifyModeRadioButton;  //Notifyモードラジオボタン
  public static JRadioButton ppiUkunCommandModeRadioButton;  //Commandモードラジオボタン
  //  jSerialComm
  public static final String PPI_UKUN_DESCRIPTION = "8255 emulator";  //プロダクト概要
  public static SerialPort ppiUkunSerialPort;  //シリアルポート
  public static UkunDataListener ppiUkunDataListener;  //シリアルポートデータリスナー

  //ポートの値
  public static int ppiPortAData;
  public static int ppiPortBData;
  public static int ppiPortCData;

  //自動切り替え
  //  PPIのポートが参照されてから一定時間だけJOYKEYを有効にする
  public static final long PPI_CONTINUOUS_ACCESS_SPAN = XEiJ.TMR_FREQ / 10;  //自動切り替えの有効期間(TMR_FREQ単位)。1/10秒
  public static long ppiLastAccessTime;  //最後にPPIのポートがアクセスされた時刻

  //XInput
  public static final boolean PPI_XINPUT_ON = true;  //false=使わない,true=使う
  public static boolean ppiXInputOn;  //false=使わない,true=使う
  public static XInput ppiXInput;  //XInput。nullでなければ動作中
  public static int ppiXInputLastButtons;

  //ウインドウ
  public static JFrame ppiFrame;

  public static JScrollPane ppiConfigurationScrollPane;


  //ppiInit ()
  //  PPIを初期化する
  public static void ppiInit () {
    ppiJoyKey = Settings.sgsGetOnOff ("joykey");
    ppiJoyAuto = Settings.sgsGetOnOff ("joyauto");
    ppiJoyBlock = Settings.sgsGetOnOff ("joyblock");

    ppiJoysticks = new Joystick[] {
      new DummyPad (),
      new Normal2ButtonPad (1),
      new Normal2ButtonPad (2),
      new MegaDrive3ButtonPad (1),
      new MegaDrive3ButtonPad (2),
      new MegaDrive6ButtonPad (1),
      new MegaDrive6ButtonPad (2),
      new CyberStickAnalog (1),
      new CyberStickAnalog (2),
      new CyberStickDigital (1),
      new CyberStickDigital (2),
      new Shiromadokun (1),
      new Shiromadokun (2),
    };

    String id1 = Settings.sgsGetString ("joystick1");  //ジョイスティックポート1に接続するデバイス
    ppiJoystick1 = ppiJoysticks[0];  //DummyPad
    for (Joystick joystick : ppiJoysticks) {
      if (joystick.getId ().equalsIgnoreCase (id1)) {
        ppiJoystick1 = joystick;
      }
    }

    String id2 = Settings.sgsGetString ("joystick2");  //ジョイスティックポート2に接続するデバイス
    ppiJoystick2 = ppiJoysticks[0];  //DummyPad
    for (Joystick joystick : ppiJoysticks) {
      if (joystick.getId ().equalsIgnoreCase (id2)) {
        ppiJoystick2 = joystick;
      }
    }

    if (PPI_UKUN_ON) {
      ppiUkunRequestedConnection = Settings.sgsGetOnOff ("joyportukun");
      ppiUkunCurrentConnection = false;
      ppiUkunAdjustMenu ();
      ppiUkunPortName = null;
      String paramUkunmode = Settings.sgsGetString ("ukunmode");
      ppiUkunRequestedMode = (paramUkunmode.equalsIgnoreCase ("notify") ? PPI_UKUN_NOTIFY :
                              paramUkunmode.equalsIgnoreCase ("command") ? PPI_UKUN_COMMAND :
                              PPI_UKUN_NOTIFY);
      ppiUkunCurrentMode = ppiUkunRequestedMode;
      if (PPI_UKUN_DEBUG) {
        System.out.printf ("UkunCurrentMode 0x%04x\n", ppiUkunCurrentMode);
      }
      ppiUkunPortA = 0b1111_1111;  //0b1..1_....
      ppiUkunPortB = 0b1111_1111;  //0b1..1_....
      ppiUkunPortC = 0b0000_0000;  //0b...._0000
      ppiUkunSending = false;
      //ppiUkunThread = null;
      if (PPI_UKUN_NANO) {
        ppiUkunNanoMeasuring = false;
        ppiUkunNanoBestArray = new long[8];
        ppiUkunNanoWorstArray = new long[8];
        ppiUkunNanoTotalArray = new long[8];
        ppiUkunNanoCountArray = new long[8];
      }
      if (PPI_UKUN_INTERVAL) {
        ppiUkunIntervalOn = Settings.sgsGetOnOff ("ukuninterval");
        ppiUkunLastVirtualTime = XEiJ.mpuClockTime;
        ppiUkunLastRealTime = System.nanoTime ();
      }
      ppiUkunSerialPort = null;
      ppiUkunDataListener = null;
    }  //if PPI_UKUN_ON

    if (PPI_XINPUT_ON) {
      ppiXInputOn = XEiJ.prgIsWindows && Settings.sgsGetOnOff ("xinput");
      if (ppiXInputOn) {
        ppiXInputStart ();
      }
      ppiXInputLastButtons = 0;
    }

    ppiReset ();
  }  //ppiInit


  //ppiTini ()
  //  後始末
  public static void ppiTini () {
    Settings.sgsPutOnOff ("joykey", ppiJoyKey);
    Settings.sgsPutOnOff ("joyauto", ppiJoyAuto);
    Settings.sgsPutOnOff ("joyblock", ppiJoyBlock);
    for (Joystick joystick : ppiJoysticks) {
      joystick.tini ();
    }
    Settings.sgsPutString ("joystick1", ppiJoystick1.getId ());
    Settings.sgsPutString ("joystick2", ppiJoystick2.getId ());
    if (PPI_UKUN_ON) {
      Settings.sgsPutOnOff ("joyportukun", ppiUkunRequestedConnection);
      Settings.sgsPutString ("ukunmode",
                             ppiUkunRequestedMode == PPI_UKUN_NOTIFY ? "notify" :
                             ppiUkunRequestedMode == PPI_UKUN_COMMAND ? "command" :
                             "notify");
      if (PPI_UKUN_INTERVAL) {
        Settings.sgsPutOnOff ("ukuninterval", ppiUkunIntervalOn);
      }
      ppiUkunDisconnect ();
    }
    //XInput
    if (PPI_XINPUT_ON) {
      Settings.sgsPutOnOff ("xinput", ppiXInputOn);
      if (ppiXInputOn) {
        ppiXInputOn = false;
        ppiXInputEnd ();
      }
    }
  }  //ppiTini


  //ppiReset ()
  //  リセット
  public static void ppiReset () {
    ppiPortAData = 0;
    ppiPortBData = 0;
    ppiPortCData = 0;
    ppiLastAccessTime = 0L;
  }


  public static void ppiXInputStart () {
    if (ppiXInput == null) {
      System.out.println (Multilingual.mlnJapanese ?
                          "XInput のポーリングを開始します" :
                          "Starts polling XInput");
      ppiXInput = new XInput ();
    }
  }


  public static void ppiXInputEnd () {
    if (ppiXInput != null) {
      System.out.println (Multilingual.mlnJapanese ?
                          "XInput のポーリングを終了します" :
                          "Ends polling XInput");
      ppiXInput.end ();
      ppiXInput = null;
    }
  }


  //ppiUkunConnect ()
  //  接続する
  //  MPU動作中はI/Oポートのアクセスと衝突させないためコアのスレッドで呼び出すこと
  //  接続はここで完結するので遷移中の状態は考慮しない
  public static void ppiUkunConnect () {
    if (PPI_UKUN_ON) {
      if (PPI_UKUN_DEBUG) {
        System.out.println ("ppiUkunConnect start");
      }
      ppiUkunRequestedConnection = true;
      if (ppiUkunCurrentConnection) {  //接続している
        ppiUkunAdjustMenu ();
        return;
      }
      //ジョイポートU君を探して開く
      if (ppiUkunSerialPort == null) {
        for (SerialPort serialPort : SerialPort.getCommPorts ()) {
          serialPort.clearRTS ();
          serialPort.clearDTR ();
          if (serialPort.getPortDescription ().startsWith (PPI_UKUN_DESCRIPTION) &&  //見つかった
              serialPort.openPort ()) {  //開けた
            ppiUkunSerialPort = serialPort;
            break;
          }
        }
      }
      if (!XEiJ.prgIsWindows && ppiUkunSerialPort == null) {  //開けなかった
        String path = "/dev/serial/by-id/usb-TNB_Factory._8255_emulator-if00";
        try {
          ppiUkunSerialPort = SerialPort.getCommPort (path);
          if (ppiUkunSerialPort.openPort ()) {
            ppiUkunSerialPort = null;
          }
        } catch (SerialPortInvalidPortException e) {
          ppiUkunSerialPort = null;
        }
      }
      if (ppiUkunSerialPort == null) {  //開けなかった
        System.out.printf (Multilingual.mlnJapanese ?
                           "じょいぽーと U 君 (%s) のシリアルポートが見つかりません\n" :
                           "Serial port for JoyPortUkun (%s) not found\n",
                           PPI_UKUN_DESCRIPTION);
        ppiUkunRequestedConnection = false;
        ppiUkunCurrentConnection = false;
        ppiUkunAdjustMenu ();
        return;
      }
      ppiUkunSerialPort.setRTS ();
      ppiUkunSerialPort.setDTR ();
      ppiUkunPortName = ppiUkunSerialPort.getSystemPortName ();
      ppiUkunSerialPort.setComPortParameters (480000000, 8, SerialPort.ONE_STOP_BIT, SerialPort.NO_PARITY);
      ppiUkunSerialPort.setComPortTimeouts (SerialPort.TIMEOUT_WRITE_BLOCKING | SerialPort.TIMEOUT_READ_BLOCKING, 0, 0);
      //接続完了
      //
      ppiUkunSerialPort.writeBytes (new byte[] { (byte)('v') }, 1);
      byte[] b = new byte[7];
      ppiUkunSerialPort.readBytes (b, 7);
      ppiIsCommandOldMode = (b[1] < '2');
      //
      ppiUkunCurrentMode = PPI_UKUN_TRANSITION;
      if (PPI_UKUN_DEBUG) {
        System.out.printf ("UkunCurrentMode 0x%04x\n", ppiUkunCurrentMode);
      }
      ppiUkunCurrentConnection = true;  //接続している
      ppiUkunAdjustMenu ();
      System.out.printf (Multilingual.mlnJapanese ?
                         "じょいぽーと U 君 (%s, %s) に接続しました\n" :
                         "Connected to JoyPortUkun (%s, %s)\n",
                         ppiUkunPortName, ppiUkunSerialPort.getPortDescription ());
      //シリアルポートデータリスナーを登録する
      if (ppiUkunDataListener == null) {
        ppiUkunSending = false;
        ppiUkunDataListener = new UkunDataListener ();
        ppiUkunSerialPort.addDataListener (ppiUkunDataListener);
      }
      //要求されたモードを送信する
      if (ppiUkunRequestedMode == PPI_UKUN_COMMAND && ppiIsCommandOldMode) {
        ppiUkunSerialPort.writeBytes (new byte[] { (byte) PPI_UKUN_COMMAND_OLD }, 1);
      } else {
        ppiUkunSerialPort.writeBytes (new byte[] { (byte) ppiUkunRequestedMode }, 1);
      }
      while (ppiUkunCurrentMode == PPI_UKUN_TRANSITION) {
      }
      ppiSetMode();
      if (PPI_UKUN_DEBUG) {
        System.out.println ("ppiUkunConnect end");
      }
    }
  }  //ppiUkunConnect


  //ppiUkunDisconnect ()
  //  切断する
  //  MPU動作中はI/Oポートのアクセスと衝突させないためコアのスレッドで呼び出すこと
  //  切断はここで完結するので遷移中の状態は考慮しない
  public static void ppiUkunDisconnect () {
    if (PPI_UKUN_ON) {
      if (PPI_UKUN_DEBUG) {
        System.out.println ("ppiUkunDisconnect start");
      }
      ppiUkunRequestedConnection = false;
      if (!ppiUkunCurrentConnection) {  //接続していない
        ppiUkunAdjustMenu ();
        return;
      }
      //切断開始
      ppiUkunCurrentMode = PPI_UKUN_TRANSITION;
      if (PPI_UKUN_DEBUG) {
        System.out.printf ("UkunCurrentMode 0x%04x\n", ppiUkunCurrentMode);
      }
      ppiUkunCurrentConnection = false;  //接続していない
      ppiUkunAdjustMenu ();
      if (ppiUkunSerialPort != null) {
        //シリアルポートデータリスナーを解除する
        ppiUkunSerialPort.removeDataListener ();
      }
      ppiUkunDataListener = null;
      //ポートを閉じる
      ppiUkunSerialPort.clearRTS ();
      ppiUkunSerialPort.clearDTR ();
      ppiUkunSerialPort.closePort ();
      ppiUkunSerialPort = null;
      ppiUkunSending = false;
      //切断完了
      ppiUkunCurrentMode = ppiUkunRequestedMode;
      if (PPI_UKUN_DEBUG) {
        System.out.printf ("UkunCurrentMode 0x%04x\n", ppiUkunCurrentMode);
      }
      ppiUkunAdjustMenu ();
      System.out.printf (Multilingual.mlnJapanese ?
                         "じょいぽーと U 君 (%s) を切り離しました\n" :
                         "Disconnected JoyPortUkun (%s)\n",
                         ppiUkunPortName);
      ppiUkunPortName = null;
      if (PPI_UKUN_DEBUG) {
        System.out.println ("ppiUkunDisconnect end");
      }
    }
  }  //ppiUkunDisconnect


  //ppiUkunNotifyMode ()
  //  Notifyモード
  public static void ppiUkunNotifyMode () {
    if (PPI_UKUN_ON) {
      if (PPI_UKUN_DEBUG) {
        System.out.println ("ppiUkunNotifyMode start");
      }
      ppiUkunRequestedMode = PPI_UKUN_NOTIFY;
      ppiUkunCurrentMode = PPI_UKUN_NOTIFY;
      if (PPI_UKUN_DEBUG) {
        System.out.printf ("UkunCurrentMode 0x%04x\n", ppiUkunCurrentMode);
      }
      ppiUkunAdjustMenu ();
      if (PPI_UKUN_DEBUG) {
        System.out.println ("ppiUkunNotifyMode end");
      }
    }
  }  //ppiUkunNotifyMode


  //ppiUkunCommandMode ()
  //  Commandモード
  public static void ppiUkunCommandMode () {
    if (PPI_UKUN_ON) {
      if (PPI_UKUN_DEBUG) {
        System.out.println ("ppiUkunCommandMode start");
      }
      ppiUkunRequestedMode = PPI_UKUN_COMMAND;
      ppiUkunCurrentMode = PPI_UKUN_COMMAND;
      if (PPI_UKUN_DEBUG) {
        System.out.printf ("UkunCurrentMode 0x%04x\n", ppiUkunCurrentMode);
      }
       ppiUkunAdjustMenu ();
      if (PPI_UKUN_DEBUG) {
        System.out.println ("ppiUkunCommandMode end");
      }
    }
  }  //ppiUkunCommandMode


  //ppiUkunMenuConnect ()
  //  メニューからの接続
  public static void ppiUkunMenuConnect () {
    if (PPI_UKUN_ON) {
      XEiJ.tmrTimer.schedule (new TimerTask () {
        @Override public void run () {
          ppiUkunConnect ();
        }
      }, 0L);
    }
  }  //ppiUkunMenuConnect


  //ppiUkunMenuDisconnect ()
  //  メニューからの切断
  public static void ppiUkunMenuDisconnect () {
    if (PPI_UKUN_ON) {
      XEiJ.tmrTimer.schedule (new TimerTask () {
        @Override public void run () {
          ppiUkunDisconnect ();
        }
      }, 0L);
    }
  }  //ppiUkunMenuDisconnect


  //ppiUkunMenuNotifyMode ()
  //  メニューからのNotifyモード
  public static void ppiUkunMenuNotifyMode () {
    if (PPI_UKUN_ON) {
      XEiJ.tmrTimer.schedule (new TimerTask () {
        @Override public void run () {
          ppiUkunNotifyMode ();
        }
      }, 0L);
    }
  }  //ppiUkunMenuNotifyMode


  //ppiUkunMenuCommandMode ()
  //  メニューからのCommandモード
  public static void ppiUkunMenuCommandMode () {
    if (PPI_UKUN_ON) {
      XEiJ.tmrTimer.schedule (new TimerTask () {
        @Override public void run () {
          ppiUkunCommandMode ();
        }
      }, 0L);
    }
  }  //ppiUkunMenuCommandMode


  //ppiUkunAdjustMenu ()
  //  メニューを調整する
  public static void ppiUkunAdjustMenu () {
    if (PPI_UKUN_ON) {
      if (ppiUkunConnectCheckbox != null) {
        //接続チェックボックスと要求された接続を合わせる
        if (ppiUkunConnectCheckbox.isSelected () != ppiUkunRequestedConnection) {
          ppiUkunConnectCheckbox.setSelected (ppiUkunRequestedConnection);
        }
        //  遷移中は操作禁止
        if (ppiUkunConnectCheckbox.isEnabled () != (ppiUkunRequestedConnection == ppiUkunCurrentConnection)) {
          ppiUkunConnectCheckbox.setEnabled (ppiUkunRequestedConnection == ppiUkunCurrentConnection);
        }
      }
      boolean x = !(ppiUkunCurrentConnection); 
      if (ppiUkunNotifyModeRadioButton != null) {
        //Notifyモードラジオボタンと要求されたモードを合わせる
        if (ppiUkunNotifyModeRadioButton.isSelected () != (ppiUkunRequestedMode == PPI_UKUN_NOTIFY)) {
          ppiUkunNotifyModeRadioButton.setSelected (ppiUkunRequestedMode == PPI_UKUN_NOTIFY);
        }
        //  接続中かつ遷移中は操作禁止
        if (ppiUkunNotifyModeRadioButton.isEnabled () != x) {
          ppiUkunNotifyModeRadioButton.setEnabled (x);
        }
      }
      if (ppiUkunCommandModeRadioButton != null) {
        //Commandモードラジオボタンと要求されたモードを合わせる
        if (ppiUkunCommandModeRadioButton.isSelected () != (ppiUkunRequestedMode == PPI_UKUN_COMMAND)) {
          ppiUkunCommandModeRadioButton.setSelected (ppiUkunRequestedMode == PPI_UKUN_COMMAND);
        }
        //  接続中かつ遷移中は操作禁止
        if (ppiUkunCommandModeRadioButton.isEnabled () != x) {
          ppiUkunCommandModeRadioButton.setEnabled (x);
        }
      }
    }
  }  //ppiUkunAdjustMenu


  private static void ppiSetMode () {
   if (ppiUkunCurrentMode == PPI_UKUN_NOTIFY) {
      ppiUkunSending = true;
      ppiUkunSerialPort.writeBytes (new byte[] { (byte) (0b0100_0000 |
                                                        (ppiControlWord & 0b0111_1000) >> 1 |
                                                        (ppiControlWord & 0b0000_0011)) }, 1);  //ライトコントロールワード
      while (ppiUkunSending) {
      }
      ppiUkunSending = true;
      ppiUkunSerialPort.writeBytes (new byte[] { (byte) (ppiPortCData >> 4) }, 1);  //ライトポートC
      while (ppiUkunSending) {
      }
    } else {
      ppiUkunSerialPort.writeBytes (new byte[] { (byte) (0b0100_0000 |
                                                        (ppiControlWord & 0b0111_1000) >> 1 |
                                                        (ppiControlWord & 0b0000_0011)) }, 1);  //ライトコントロールワード
      ppiUkunSerialPort.writeBytes (new byte[] { (byte) (ppiPortCData >> 4) }, 1);  //ライトポートC
    }
  }


  //class UkunDataListener
  //  シリアルポートデータリスナー
  public static class UkunDataListener implements SerialPortDataListener {
    @Override public int getListeningEvents () {
      return SerialPort.LISTENING_EVENT_DATA_AVAILABLE;
    }
    @Override public void serialEvent (SerialPortEvent spe) {
      if (spe.getEventType () != SerialPort.LISTENING_EVENT_DATA_AVAILABLE) {
        return;
      }
      byte[] bytes = new byte[1];
      while (ppiUkunSerialPort.bytesAvailable () != 0 &&
             ppiUkunSerialPort.readBytes (bytes, 1) != 0) {  //1バイトずつ受信する
        int data = bytes[0] & 0xff;
        if (PPI_UKUN_DEBUG) {
          System.out.printf ("UkunDataListener received 0x%02x\n", data);
        }
        //変化通知
        //  0b1..0_....  portA  0b1..1_....
        //  0b1..1_....  portB  0b1..1_....
        //  0b0000_....  portC  0b...._0000
        //完了通知
        //  0b0011_1111  completed
        //モード通知
        //  0b0011_0001  Notify mode
        //  0b0011_0111  Command mode
        //  0b0011_0010  Command(OLD) mode
        if ((data & 0b1001_0000) == 0b1000_0000) {  //0b1..0_.... portA
          ppiUkunPortA = 0b1001_0000 | data;  //0b1..1_....
        } else if ((data & 0b1001_0000) == 0b1001_0000) {  //0b1..1_.... portB
          ppiUkunPortB = data;  //0b1..1_....
        } else if (data <= 0b0000_1111) {  //0b0000_.... portC
          ppiUkunPortC = data << 4;  //0b...._0000
        } else if (data == 0b0011_1111) {  //completed
          ppiUkunSending = false;
        } else if (data == 0b0011_0001) {  //Notify mode
          System.out.printf (Multilingual.mlnJapanese ?
                               "じょいぽーと U 君 (%s) は Notify モードで初期化されました\n" :
                               "JoyPortUkun (%s) has been initialized in Notify mode\n",
                               ppiUkunPortName);
          ppiUkunRequestedMode = PPI_UKUN_NOTIFY;
          ppiUkunCurrentMode = PPI_UKUN_NOTIFY;
          if (PPI_UKUN_DEBUG) {
            System.out.printf ("UkunCurrentMode 0x%04x\n", ppiUkunCurrentMode);
          }
          ppiUkunAdjustMenu ();
        } else if (data == 0b0011_0111) {  //Command mode
          ppiUkunRequestedMode = PPI_UKUN_COMMAND;
          ppiUkunCurrentMode = PPI_UKUN_COMMAND;
          if (PPI_UKUN_DEBUG) {
            System.out.printf ("UkunCurrentMode 0x%04x\n", ppiUkunCurrentMode);
          }
          ppiUkunAdjustMenu ();
          System.out.printf (Multilingual.mlnJapanese ?
                             "じょいぽーと U 君 (%s) は Command モードで初期化されました\n" :
                             "JoyPortUkun (%s) has been initialized in Command mode\n",
                             ppiUkunPortName);
          //シリアルポートデータリスナーを解除する
          ppiUkunSerialPort.removeDataListener ();
          ppiUkunDataListener = null;
          break;
        } else if (data == 0b0011_0010) {  //Command(OLD) mode
          ppiUkunRequestedMode = PPI_UKUN_COMMAND;
          ppiUkunCurrentMode = PPI_UKUN_COMMAND;
          if (PPI_UKUN_DEBUG) {
            System.out.printf ("UkunCurrentMode 0x%04x\n", ppiUkunCurrentMode);
          }
          ppiUkunAdjustMenu ();
          System.out.printf (Multilingual.mlnJapanese ?
                             "じょいぽーと U 君 (%s) は Command(OLD) モードで初期化されました\n" :
                             "JoyPortUkun (%s) has been initialized in Command(OLD) mode\n",
                             ppiUkunPortName);
          //シリアルポートデータリスナーを解除する
          ppiUkunSerialPort.removeDataListener ();
          ppiUkunDataListener = null;
          break;
        } else {
          System.out.printf ("UkunDataListener received unknown data 0x%02x\n", data);
          //シリアルポートデータリスナーを解除する
          ppiUkunSerialPort.removeDataListener ();
          ppiUkunDataListener = null;
          break;
        }
      }  //while
    }  //serialEvent
  }  //class UkunDataListener


  //ppiUkunNanoStart ()
  //  計測開始
  public static void ppiUkunNanoStart () {
    if (PPI_UKUN_ON && PPI_UKUN_NANO) {
      XEiJ.tmrTimer.schedule (new TimerTask () {
        @Override public void run () {
          if (!ppiUkunNanoMeasuring) {
            ppiUkunNanoMeasuring = true;
            Arrays.fill (ppiUkunNanoBestArray, Long.MAX_VALUE);
            Arrays.fill (ppiUkunNanoWorstArray, 0L);
            Arrays.fill (ppiUkunNanoTotalArray, 0L);
            Arrays.fill (ppiUkunNanoCountArray, 0L);
          }
        }
      }, 0L);
    }
  }


  //ppiUkunNanoEnd ()
  //  計測終了
  public static void ppiUkunNanoEnd () {
    if (PPI_UKUN_ON && PPI_UKUN_NANO) {
      XEiJ.tmrTimer.schedule (new TimerTask () {
        @Override public void run () {
          if (ppiUkunNanoMeasuring) {
            ppiUkunNanoMeasuring = false;
            System.out.println ("Nano timer");
            for (int i = 0; i < 8; i++) {
              long best = ppiUkunNanoBestArray[i];
              long worst = ppiUkunNanoWorstArray[i];
              long total = ppiUkunNanoTotalArray[i];
              long count = ppiUkunNanoCountArray[i];
              if (count != 0L) {
                System.out.printf ("  %s %s best=%d(ns) average=%d(ns) worst=%d(ns) total=%d(ns) count=%d\n",
                                   i < 2 ? "PORT A" : i < 4 ? "PORT B" : i < 6 ? "PORT C" : "CONTROL WORD",
                                   (i & 1) == 0 ? "READ" : "WRITE",
                                   best,
                                   total / count,
                                   worst,
                                   total,
                                   count);
              }
            }
          }
        }
      }, 0L);
    }
  }


  //ppiStart ()
  public static void ppiStart () {
    if (RestorableFrame.rfmGetOpened (Settings.SGS_PPI_FRAME_KEY)) {
      ppiOpen ();
    }
    if (PPI_UKUN_ON) {
      if (ppiUkunRequestedConnection) {
        ppiUkunConnect ();
      }
    }
  }


  //ppiOpen ()
  //  ジョイスティックの設定ウインドウを開く
  public static void ppiOpen () {
    if (ppiFrame == null) {
      ppiMakeFrame ();
    }
    XEiJ.pnlExitFullScreen (false);
    ppiFrame.setVisible (true);
  }


  //ppiMakeFrame ()
  //  ジョイスティックポートの設定ウインドウを作る
  //  ここでは開かない
  public static void ppiMakeFrame () {

    //アクションリスナー
    ActionListener actionListener = new ActionListener () {
      @Override public void actionPerformed (ActionEvent ae) {
        Object source = ae.getSource ();
        String command = ae.getActionCommand ();
        switch (command) {
        case "Consider part of keyboard as joystick":
          //キーボードの一部をジョイスティックとみなす
          ppiJoyKey = ((JCheckBox) ae.getSource ()).isSelected ();
          break;
        case "Enabled only while the port is read repeatedly":
          //ポートが繰り返し読み出されている間だけ有効
          ppiJoyAuto = ((JCheckBox) ae.getSource ()).isSelected ();
          break;
        case "Remove key input data processed as a joystick operation":
          //ジョイスティック操作として処理されたキー入力データを取り除く
          ppiJoyBlock = ((JCheckBox) ae.getSource ()).isSelected ();
          break;
          //
        case "JoyPortUkun":  //じょいぽーと U 君
          if (PPI_UKUN_ON) {
            if (((JCheckBox) ae.getSource ()).isSelected ()) {  //接続
              ppiUkunMenuConnect ();
            } else {  //切断
              ppiUkunMenuDisconnect ();
            }
          }
          break;
        case "Notify mode":  //Notify モード
          if (PPI_UKUN_ON) {
            ppiUkunMenuNotifyMode ();
          }
          break;
        case "Command mode":  //Command モード
          if (PPI_UKUN_ON) {
            ppiUkunMenuCommandMode ();
          }
          break;
        case "jSerialComm":
        if (PPI_UKUN_ON && !ppiUkunCurrentConnection) {  //接続中は操作禁止
            ((JCheckBox) ae.getSource ()).isSelected ();  //Windows以外は常にtrue
            ppiUkunAdjustMenu ();
          }
          break;
        case "Output interval adjustment":  //出力間隔調整
          if (PPI_UKUN_ON && PPI_UKUN_INTERVAL) {
            boolean on = ((JCheckBox) ae.getSource ()).isSelected ();
            if (ppiUkunIntervalOn != on) {
              if (on) {
                XEiJ.tmrTimer.schedule (new TimerTask () {
                  @Override public void run () {
                    ppiUkunLastVirtualTime = XEiJ.mpuClockTime;
                    ppiUkunLastRealTime = System.nanoTime ();
                    ppiUkunIntervalOn = true;
                  }
                }, 0L);                
              } else {
                XEiJ.tmrTimer.schedule (new TimerTask () {
                  @Override public void run () {
                    ppiUkunIntervalOn = false;
                  }
                }, 0L);                
              }
            }
          }
          break;
        case "Nano timer":  //ナノタイマー
          if (PPI_UKUN_ON && PPI_UKUN_NANO) {
            if (((JCheckBox) ae.getSource ()).isSelected ()) {  //計測開始
              ppiUkunNanoStart ();
            } else {  //計測終了
              ppiUkunNanoEnd ();
            }
          }
          break;
        case "XInput":
          if (PPI_XINPUT_ON) {
            if (((JCheckBox) ae.getSource ()).isSelected ()) {  //on
              if (!ppiXInputOn) {  //off→on
                ppiXInputOn = true;
                ppiXInputStart ();
              }
            } else {  //off
              if (ppiXInputOn) {  //on→off
                ppiXInputOn = false;
                ppiXInputEnd ();
              }
            }
          }
        }
      }
    };

    //ジョイスティックポートのメニュー
    //       0   1  2
    //   0  ポート  接続
    //   1   1   2
    //   2  -------------------------
    //   3  RB  RB  なし
    //   4  RB  RB  ノーマル2ボタンパッド#1
    //   5  RB  RB  ノーマル2ボタンパッド#2
    //   6  RB  RB  メガドラ3ボタンパッド#1
    //   7  RB  RB  メガドラ3ボタンパッド#2
    //   8  RB  RB  メガドラ6ボタンパッド#1
    //   9  RB  RB  メガドラ6ボタンパッド#2
    //  10  RB  RB  サイバースティック(デジタルモード)#1
    //  12  RB  RB  サイバースティック(デジタルモード)#2
    //  13  RB  RB  サイバースティック(アナログモード)#1
    //  14  RB  RB  サイバースティック(アナログモード)#2
    //  15  RB  RB  白窓君#1
    //  16  RB  RB  白窓君#2
    ButtonGroup port1Group = new ButtonGroup ();
    ButtonGroup port2Group = new ButtonGroup ();
    ActionListener port1Listener = new ActionListener () {
      @Override public void actionPerformed (ActionEvent ae) {
        Joystick joyStick = ppiJoysticks[Integer.parseInt (ae.getActionCommand ())];
        if (ppiJoystick1 != joyStick) {
          ppiJoystick1.reset ();
          ppiJoystick1 = joyStick;
        }
        ppiConfigurationScrollPane.setViewportView (joyStick.getConfigurationPanel ());
      }
    };
    ActionListener port2Listener = new ActionListener () {
      @Override public void actionPerformed (ActionEvent ae) {
        Joystick joyStick = ppiJoysticks[Integer.parseInt (ae.getActionCommand ())];
        if (ppiJoystick2 != joyStick) {
          ppiJoystick2.reset ();
          ppiJoystick2 = joyStick;
        }
        ppiConfigurationScrollPane.setViewportView (joyStick.getConfigurationPanel ());
      }
    };
    ArrayList<Object> cellList = new ArrayList<Object> ();
    cellList.add (Multilingual.mlnText (ComponentFactory.createLabel ("Port"), "ja", "ポート"));  //(0,0)-(1,0)
    cellList.add (Multilingual.mlnText (ComponentFactory.createLabel ("Connect to"), "ja", "接続"));  //(2,0)-(2,1)
    cellList.add ("1");  //(0,1)
    cellList.add ("2");  //(1,1)
    cellList.add (ComponentFactory.createHorizontalSeparator ());  //(0,2)-(2,2)
    for (int i = 0; i < ppiJoysticks.length; i++) {
      Joystick joyStick = ppiJoysticks[i];
      cellList.add (ComponentFactory.setText (ComponentFactory.createRadioButton (
        port1Group, joyStick == ppiJoystick1, String.valueOf (i), port1Listener), ""));  //(0,3+i)
      cellList.add (ComponentFactory.setText (ComponentFactory.createRadioButton (
        port2Group, joyStick == ppiJoystick2, String.valueOf (i), port2Listener), ""));  //(1,3+i)
      cellList.add (Multilingual.mlnText (ComponentFactory.createLabel (joyStick.getNameEn ()), "ja", joyStick.getNameJa ()));  //(3,3)
    }
    JScrollPane portMenuPanel = new JScrollPane (
      ComponentFactory.createGridPanel (
        3, 3 + ppiJoysticks.length,
        "paddingLeft=3,paddingRight=3,center",   //gridStyles
        "",  //colStyles
        "italic;italic;colSpan=3,widen",  //rowStyles
        "colSpan=2;rowSpan=2",  //cellStyles
        cellList.toArray (new Object[0])));

    //個々のジョイスティックの設定パネル
    ppiConfigurationScrollPane = new JScrollPane ((((ppiJoystick1 instanceof DummyPad) &&
                                                    !(ppiJoystick2 instanceof DummyPad)) ||  //2だけ接続されている
                                                   (!(ppiJoystick1 instanceof Shiromadokun) &&
                                                    (ppiJoystick2 instanceof Shiromadokun))  //2だけ白窓君
                                                   ? ppiJoystick2 : ppiJoystick1).getConfigurationPanel ());

    //ウインドウ
    ButtonGroup ukunGroup = new ButtonGroup ();
    ppiFrame = Multilingual.mlnTitle (
      ComponentFactory.createRestorableSubFrame (
        Settings.SGS_PPI_FRAME_KEY,
        "Joystick port settings",
        null,
        ComponentFactory.setEmptyBorder (
          ComponentFactory.createVerticalBox (
            //
            ComponentFactory.createFlowPanel (
              ppiUkunConnectCheckbox =
              !(PPI_UKUN_ON) ? null :
              Multilingual.mlnText (
                ComponentFactory.setEnabled (
                  ComponentFactory.createCheckBox (ppiUkunRequestedConnection, "JoyPortUkun", actionListener),
                  ppiUkunRequestedConnection == ppiUkunCurrentConnection),
                "ja", "じょいぽーと U 君")
              ),
            !(PPI_UKUN_ON) ? null :
            ComponentFactory.createFlowPanel (
              20, 0,
              ppiUkunNotifyModeRadioButton =
              Multilingual.mlnText (
                ComponentFactory.setEnabled (
                  ComponentFactory.createRadioButton (ukunGroup, ppiUkunRequestedMode == PPI_UKUN_NOTIFY, "Notify mode", actionListener),
                  !(ppiUkunCurrentConnection && ppiUkunRequestedMode != ppiUkunCurrentMode)),
                "ja", "Notify モード"),
              ppiUkunCommandModeRadioButton =
              Multilingual.mlnText (
                ComponentFactory.setEnabled (
                  ComponentFactory.createRadioButton (ukunGroup, ppiUkunRequestedMode == PPI_UKUN_COMMAND, "Command mode", actionListener),
                  !(ppiUkunCurrentConnection && ppiUkunRequestedMode != ppiUkunCurrentMode)),
                "ja", "Command モード")
              ),
            !(PPI_UKUN_ON) ? null :
            ComponentFactory.createFlowPanel (
              20, 0,
              Multilingual.mlnText (
                ComponentFactory.createCheckBox (ppiUkunIntervalOn, "Output interval adjustment", actionListener),
                "ja", "出力間隔調整"),
              !PPI_UKUN_NANO ? null :
              Multilingual.mlnText (
                ComponentFactory.createCheckBox (ppiUkunNanoMeasuring, "Nano timer", actionListener),
                "ja", "ナノタイマー")
              ),
            !(PPI_UKUN_ON) ? null : ComponentFactory.createHorizontalSeparator (),
            //
            !(PPI_XINPUT_ON && XEiJ.prgIsWindows) ? null :
            ComponentFactory.createFlowPanel (
              ComponentFactory.createCheckBox (ppiXInputOn, "XInput", actionListener)
              ),
            !(PPI_XINPUT_ON && XEiJ.prgIsWindows) ? null : ComponentFactory.createHorizontalSeparator (),
            //
            ComponentFactory.createFlowPanel (
              Multilingual.mlnText (
                ComponentFactory.createCheckBox (
                  ppiJoyKey,
                  "Consider part of keyboard as joystick",
                  actionListener),
                "ja", "キーボードの一部をジョイスティックとみなす")
              ),
            ComponentFactory.createFlowPanel (
              Multilingual.mlnText (
                ComponentFactory.createCheckBox (
                  ppiJoyAuto,
                  "Enabled only while the port is read repeatedly",
                  actionListener),
                "ja", "ポートが繰り返し読み出されている間だけ有効")
              ),
            ComponentFactory.createFlowPanel (
              Multilingual.mlnText (
                ComponentFactory.createCheckBox (
                  ppiJoyBlock,
                  "Remove key input data processed as a joystick operation",
                  actionListener),
                "ja", "ジョイスティック操作として処理されたキー入力データを取り除く")
              ),
            Box.createVerticalStrut (5),
            ComponentFactory.createHorizontalBox (
              ComponentFactory.createHorizontalSplitPane (
                portMenuPanel,
                ppiConfigurationScrollPane)
              )
            ),
          5, 5, 5, 5)),
      "ja", "ジョイスティックポート設定");
  }  //ppiMakeFrame()


  //consume = ppiInput (ke, pressed)
  //  JOYKEYの処理
  //  consume  true=入力をキーボードに渡さない
  public static boolean ppiInput (KeyEvent ke, boolean pressed) {
    boolean consume = false;
    if (ppiJoyKey && (!ppiJoyAuto || XEiJ.mpuClockTime < ppiLastAccessTime + PPI_CONTINUOUS_ACCESS_SPAN)) {
      if (ppiJoystick1.input (ke, pressed) ||
          ppiJoystick2.input (ke, pressed)) {
        //押されたときだけキーボード入力を取り除く
        //  特に自動有効化のときは、押されている間に有効になって離されたデータだけ取り除かれると、
        //  キーボード側は押されたままになっていると判断してリピートが止まらなくなる
        consume = pressed && ppiJoyBlock;
      }
    }
    return consume;
  }


  //ppiUkunAdjustInterval ()
  //  出力間隔調整
  //  PPIへの書き込みの間隔を仮想時間と実時間でそれぞれ計測し、
  //  500usを上限として、
  //  仮想時間の間隔の方が短ければウェイトを、
  //  実時間の間隔の方が短ければ空ループを追加することで、
  //  仮想時間の間隔と実時間の間隔のずれを軽減する。
  //  空ループを追加するとき負荷率が高くなる。
  public static void ppiUkunAdjustInterval () {
    long virtualTime = XEiJ.mpuClockTime;  //仮想時間。ピコ秒
    long realTime = System.nanoTime ();  //実時間。ナノ秒
    long virtualInterval = virtualTime - ppiUkunLastVirtualTime;  //仮想時間の間隔
    long realInterval = realTime - ppiUkunLastRealTime;  //実時間の間隔
    ppiUkunLastVirtualTime = virtualTime;
    ppiUkunLastRealTime = realTime;
    realInterval *= XEiJ.TMR_FREQ / 1000000000L;  //ナノ秒→ピコ秒
    if (virtualInterval <= realInterval) {  //仮想時間の間隔の方が短い。ウェイトを追加する
      long adjustmentTime = realInterval - virtualInterval;
      adjustmentTime = Math.min (PPI_UKUN_INTERVAL_LIMIT, adjustmentTime);
      XEiJ.mpuClockTime += adjustmentTime;
    } else {  //実時間の間隔の方が短い。空ループを追加する
      long adjustmentTime = virtualInterval - realInterval;
      adjustmentTime = Math.min (PPI_UKUN_INTERVAL_LIMIT, adjustmentTime);
      adjustmentTime /= XEiJ.TMR_FREQ / 1000000000L;  //ピコ秒→ナノ秒
      if (adjustmentTime != 0L) {
        if (true) {  //空ループを追加する
          while (System.nanoTime () < realTime + adjustmentTime) {
          }
        } else {  //スリープを追加する
          try {
            Thread.sleep (adjustmentTime / 1000000L, (int) (adjustmentTime % 1000000L));
          } catch (InterruptedException ie) {
          }
        }
      }
    }
  }  //ppiUkunAdjustInterval


  //  リード
  //  FM音源レジスタのアクセスウェイトのためのPPIの空読みは、ジョイスティックのデータを得ることが目的ではないので、
  //  ジョイスティックポートが連続的に読み出されているとみなさない
  public static int ppiReadByte (int a) {
    if (PPI_UKUN_DEBUG_IO) {
      System.out.printf("Read(0x%02X)", a & 0xFF);
    }
    int d;
    //
    long t;
    if (PPI_UKUN_NANO) {
      t = System.nanoTime ();
    }
    //
    switch (a & 7) {
      //
    case PPI_PORT_A & 7:
      if (PPI_UKUN_ON && ppiUkunCurrentConnection) {  //接続している
        if (ppiUkunCurrentMode == PPI_UKUN_NOTIFY) {  //Notifyモード
          checkCtrlword();
          if ((ppiControlWord & 0x10) == 0) {
            d = ppiPortAData & 0xFF;
            break;
          }
        } else if (ppiUkunCurrentMode == PPI_UKUN_COMMAND) {  //Commandモード
          XEiJ.mpuClockTime += PPI_UKUN_WAIT;
          if ((ppiControlWord & 0x10) == 0) {
            d = ppiPortAData & 0xFF;
            break;
          }
          if (ppiCacheData != null) {
            if (ppiCacheData.length == ppiCacheDataPosition) {
              ppiCacheData = null;
              ppiCacheDataPosition = 0;
            } else {
              ppiUkunPortA = ppiCacheData[ppiCacheDataPosition++] & 0xFF;
              d = ppiUkunPortA;
              if (PPI_UKUN_DEBUG_IO) {
                System.out.printf(" ** ");
              }
              break;
            }
          }
          byte[] b = new byte[] { (byte) 0b0011_1010 };  //リードポートA
          ppiUkunSerialPort.writeBytes (b, 1);
          ppiUkunSerialPort.readBytes (b, 1);
          ppiUkunPortA = b[0] & 0xFF;
          if (!ppiIsCommandOldMode) {  //NEW COMMANDの場合
            if ((b[0] & 0x80) == 0 && b[0] > 0) {
              ppiCacheData = new byte[b[0]];
              ppiUkunSerialPort.readBytes (ppiCacheData, b[0]);
              ppiUkunPortA = ppiCacheData[0] & 0xFF;
              ppiCacheDataPosition = 1;
              if (PPI_UKUN_DEBUG_IO) {
                System.out.printf(" *0 ");
              }
            }
          }
        }
        d = ppiUkunPortA & 0xFF;
      } else {  //接続していない
        if (XEiJ.regOC >> 6 != 0b0100_101_000) {  //TST.B以外
          ppiLastAccessTime = XEiJ.mpuClockTime;
        }
        d = ppiJoystick1.readByte () & 0xff;
      }
      break;
      //
    case PPI_PORT_B & 7:
      if (PPI_UKUN_ON && ppiUkunCurrentConnection) {  //接続して
        if (ppiUkunCurrentMode == PPI_UKUN_NOTIFY) {  //Notifyモード
          checkCtrlword();
          if ((ppiControlWord & 0x02) == 0) {
            d = ppiPortBData;
            break;
          }
        } else if (ppiUkunCurrentMode == PPI_UKUN_COMMAND) {  //Commandモード
          XEiJ.mpuClockTime += PPI_UKUN_WAIT;
          if ((ppiControlWord & 0x02) == 0) {
            d = ppiPortBData;
            break;
          }
          byte[] b = new byte[] { (byte) 0b0011_1011 };  //リードポートB
          ppiUkunSerialPort.writeBytes (b, 1);
          ppiUkunSerialPort.readBytes (b, 1);
          ppiUkunPortB = b[0] & 0xff;
        }
        d = ppiUkunPortB;
      } else {  //接続していない
        if (XEiJ.regOC >> 6 != 0b0100_101_000) {  //TST.B以外
          ppiLastAccessTime = XEiJ.mpuClockTime;
        }
        d = ppiJoystick2.readByte () & 0xff;
      }
      break;
      //
    case PPI_PORT_C & 7:
      if (PPI_UKUN_ON && ppiUkunCurrentConnection) {  //接続している
        XEiJ.mpuClockTime += PPI_UKUN_WAIT;
        if (ppiUkunCurrentMode == PPI_UKUN_NOTIFY) {  //Notifyモード
          checkCtrlword();
          if ((ppiControlWord & 0x08) == 0) {
            d = ppiPortCData;
            break;
          }
        } else if (ppiUkunCurrentMode == PPI_UKUN_COMMAND) {  //Commandモード
          XEiJ.mpuClockTime += PPI_UKUN_WAIT;
          if ((ppiControlWord & 0x08) == 0) {
            d = ppiPortCData;
            break;
          }
          byte[] b = new byte[] { (byte) 0b0011_1100 };  //リードポートC
          ppiUkunSerialPort.writeBytes (b, 1);
          ppiUkunSerialPort.readBytes (b, 1);
          ppiUkunPortC = b[0] & 0xf0;
        }
        d = ((ppiUkunPortC & 0xf0) |  //上位4ビット
             (ppiPortCData & 0x0f));  //下位4ビット
      } else {  //接続していない
        d = ppiPortCData;
      }
      break;
    default:
      d = 0xff;
    }  //switch a&7
    //
    if (PPI_UKUN_NANO) {
      t = System.nanoTime () - t;
      int i = a & 6;  //0,2,4,6
      if (t < ppiUkunNanoBestArray[i]) {
        ppiUkunNanoBestArray[i] = t;
      }
      if (ppiUkunNanoWorstArray[i] < t) {
        ppiUkunNanoWorstArray[i] = t;
      }
      ppiUkunNanoTotalArray[i] += t;
      ppiUkunNanoCountArray[i]++;
    }
    //
    if (PPI_UKUN_DEBUG_IO) {
      System.out.printf(" = 0x%02X\n", d & 0xFF);
    }
    ppiLastSendAddr = (byte)(a & 0x07);
    return d;
  }  //ppiReadByte
  
  
  // 必要であれば、コントロールワードの送信を行う
  private static void checkCtrlword () {
    if (ppiControlWordNext >= 0) {
      ppiControlWord = ppiControlWordNext;
      ppiControlWordNext = -1;
      ppiUkunSending = true;
      ppiUkunSerialPort.writeBytes (new byte[] { (byte) (0b0100_0000 |
                                                        (ppiControlWord & 0b0111_1000) >> 1 |
                                                        (ppiControlWord & 0b0000_0011)) }, 1);  //ライトコントロールワード
      while (ppiUkunSending) {
      }
    }
  }  //checkCtrlword

  private static int ppi4dData = -1;


  //  ライト
  public static void ppiWriteByte (int a, int d) {
    if (PPI_UKUN_DEBUG_IO) {
      System.out.printf("Write(0x%02X, 0x%02X)\n", a & 0xFF, d & 0xFF);
    }
    d &= 0xff;
    //
    long t;
    if (PPI_UKUN_NANO) {
      t = System.nanoTime ();
    }
    //
    switch (a & 7) {
    case PPI_PORT_A & 7:
      ppiPortAData = d;
      if (PPI_UKUN_ON && ppiUkunCurrentConnection) {  //接続している
        if (PPI_UKUN_INTERVAL && ppiUkunIntervalOn) {
          ppiUkunAdjustInterval ();
        } else {
          XEiJ.mpuClockTime += PPI_UKUN_WAIT;
        }
        if (ppiUkunCurrentMode == PPI_UKUN_NOTIFY) {  //Notifyモード 
          checkCtrlword();
          ppiUkunSending = true;
          ppiUkunSerialPort.writeBytes (new byte[] { (byte) (0b1000_0000 | (d & 0b0110_1111)) }, 1);  //ライトポートA
          while (ppiUkunSending) {
          }
        } else if (ppiUkunCurrentMode == PPI_UKUN_COMMAND) {  //Commandモード
          ppiUkunSerialPort.writeBytes (new byte[] { (byte) (0b1000_0000 | (d & 0b0110_1111)) }, 1);  //ライトポートA
        }
      } else {  //接続していない
        ppiJoystick1.writeByte (d);
      }
      break;
      //
    case PPI_PORT_B & 7:
      ppiPortBData = d;
      if (PPI_UKUN_ON && ppiUkunCurrentConnection) {  //接続している
        if (PPI_UKUN_INTERVAL && ppiUkunIntervalOn) {
          ppiUkunAdjustInterval ();
        } else {
          XEiJ.mpuClockTime += PPI_UKUN_WAIT;
        }
        if (ppiUkunCurrentMode == PPI_UKUN_NOTIFY) {  //Notifyモード
          checkCtrlword();
          ppiUkunSending = true;
          ppiUkunSerialPort.writeBytes (new byte[] { (byte) (0b1001_0000 | (d & 0b0110_1111)) }, 1);  //ライトポートB
          while (ppiUkunSending) {
          }
        } else if (ppiUkunCurrentMode == PPI_UKUN_COMMAND) {  //Commandモード
          ppiUkunSerialPort.writeBytes (new byte[] { (byte) (0b1001_0000 | (d & 0b0110_1111)) }, 1);  //ライトポートB
        }
      } else {  //接続していない
        ppiJoystick2.writeByte (d);
      }
      break;
      //
    case PPI_PORT_C & 7:
      ppiPortCData = d;
      //下位4ビット
      ADPCM.pcmSetPan (d);  //パン
      ADPCM.pcmDivider = d >> 2 & 3;  //分周比。0=1/1024,1=1/768,2=1/512,3=inhibited
      ADPCM.pcmUpdateRepeatInterval ();
      //上位4ビット
      if (PPI_UKUN_ON && ppiUkunCurrentConnection) {  //接続している
        if (PPI_UKUN_INTERVAL && ppiUkunIntervalOn) {
          ppiUkunAdjustInterval ();
        } else {
          XEiJ.mpuClockTime += PPI_UKUN_WAIT;
        }
        if (ppiUkunCurrentMode == PPI_UKUN_NOTIFY) {  //Notifyモード
          checkCtrlword();
          ppiUkunSending = true;
          ppiUkunSerialPort.writeBytes (new byte[] { (byte) (d >> 4) }, 1);  //ライトポートC
          while (ppiUkunSending) {
          }
        } else if (ppiUkunCurrentMode == PPI_UKUN_COMMAND) {  //Commandモード
          ppiUkunSerialPort.writeBytes (new byte[] { (byte) (d >> 4) }, 1);  //ライトポートC
        }
      } else {  //接続していない
        ppiJoystick1.setPin8 (d >> 4 & 1);
        ppiJoystick2.setPin8 (d >> 5 & 1);
        ppiJoystick1.setPin6 ((d >> 6 & 1) ^ 1);
        ppiJoystick1.setPin7 ((d >> 7 & 1) ^ 1);
      }
      break;
      //
    case PPI_CONTROL & 7:
      if ((d & 0b1000_0000) == 0b0000_0000) {  //0b0..._nnnx
        int n = (d >> 1) & 7;  //ビット番号
        int x = d & 1;  //データ
        ppiPortCData = ppiPortCData & ~(1 << n) | x << n;
        if (n < 4) {  //下位4ビット
          switch (n) {
          case 0:
          case 1:
            ADPCM.pcmSetPan (ppiPortCData & 3);  //パン
            break;
          case 2:
          case 3:
            ADPCM.pcmDivider = ppiPortCData >> 2 & 3;  //分周比。0=1/1024,1=1/768,2=1/512,3=inhibited
            ADPCM.pcmUpdateRepeatInterval ();
            break;
          }
        } else {  //上位4ビット
          if (PPI_UKUN_ON &&  ppiUkunCurrentConnection) {  //接続している
            if (PPI_UKUN_INTERVAL && ppiUkunIntervalOn) {
              ppiUkunAdjustInterval ();
            } else {
              XEiJ.mpuClockTime += PPI_UKUN_WAIT;
            }
            if (ppi4dData != (d & 0xFF)) {
              ppi4dData = d & 0xFF;
              if (ppiUkunCurrentMode == PPI_UKUN_NOTIFY) {  //Notifyモード
                ppiUkunSending = true;
                ppiUkunSerialPort.writeBytes (new byte[] { (byte) (0b0001_0000 | (d & 0b0000_1111)) }, 1);  //ライトコントロールワード
                while (ppiUkunSending) {
                }
              } else if (ppiUkunCurrentMode == PPI_UKUN_COMMAND) {  //Commandモード
                ppiUkunSerialPort.writeBytes (new byte[] { (byte) (0b0001_0000 | (d & 0b0000_1111)) }, 1);  //ライトコントロールワード
                if (!ppiIsCommandOldMode && n == 4) {
                  byte[] b = new byte[1];
                  ppiUkunSerialPort.readBytes (b, 1);
                  if ((b[0] & 0xFF) == 0xFF && ppiCacheData != null) {
                    if (ppiLastSendAddr == 0x07) {
                      ppiCacheData = null;
                      if (PPI_UKUN_DEBUG_IO) {
                        System.out.printf(" -- cache kill  %d\n", ppiCacheDataPosition);
                      }
                    } else if (ppiCacheDataPosition >= 35) {  // STAT LUSTER 対策
                      ppiCacheData = null;
                      if (PPI_UKUN_DEBUG_IO) {
                        System.out.printf(" -- cache kill  %d\n", ppiCacheDataPosition);
                      }
                    }
                  }
                }
              }
            }
          } else {  //接続していない
            switch (n) {
            case 4:
              ppiJoystick1.setPin8 (x);
              break;
            case 5:
              ppiJoystick2.setPin8 (x);
              break;
            case 6:
              ppiJoystick1.setPin6 (x ^ 1);
              break;
            case 7:
              ppiJoystick1.setPin7 (x ^ 1);
              break;
            }
          }
        }  //if 下位/上位
      } else if ((d & 0b1000_0100) == 0b1000_0000){  //0b1..._.0..
        if (PPI_UKUN_ON && ppiUkunCurrentConnection) {  //接続している
          if (PPI_UKUN_INTERVAL && ppiUkunIntervalOn) {
            ppiUkunAdjustInterval ();
          } else {
            XEiJ.mpuClockTime += PPI_UKUN_WAIT;
          }
          if (ppiUkunCurrentMode == PPI_UKUN_NOTIFY) {  //Notifyモード
            if (ppiControlWordNext >= 0) {
              ppiControlWordNext = d;
              if ( ppiControlWord == d) {
                ppiControlWordNext = -1;
              }
            } else {
              ppiControlWordNext = d;
            }
          } else if (ppiUkunCurrentMode == PPI_UKUN_COMMAND) {  //Commandモード
            ppiUkunSerialPort.writeBytes (new byte[] { (byte) (0b0100_0000 |
                                                              (d & 0b0111_1000) >> 1 |
                                                              (d & 0b0000_0011)) }, 1);  //ライトコントロールワード
            ppiControlWord = d;
          }
        } else {  //接続していない
          ppiControlWord = d;
        }
      } else {  //0b1..._.1..
        //!!! 非対応
      }
    }  //switch a&7
    //
    if (PPI_UKUN_NANO) {
      t = System.nanoTime () - t;
      int i = (a & 6) | 1;  //1,3,5,7
      if (t < ppiUkunNanoBestArray[i]) {
        ppiUkunNanoBestArray[i] = t;
      }
      if (ppiUkunNanoWorstArray[i] < t) {
        ppiUkunNanoWorstArray[i] = t;
      }
      ppiUkunNanoTotalArray[i] += t;
      ppiUkunNanoCountArray[i]++;
    }
    //
    ppiLastSendAddr = (byte)(a & 0x07);
    //
  }  //ppiWriteByte

}  //class PPI
