VitaでZAVASやりたくて(3)

必要なものが一通り揃ったのでいよいよ少し技術的なお話に突入。

新たな愛機はヤフオクでゲットした電源ケーブルが根っこから切れてるPC88 FEです。まあジャンクだからね。そんなこともあるよね。
そのへんに転がってた電気ポットの電源ケーブルを引きちぎってちょちょいと修理したら(詳細は省略)、いざRCAケーブルとテレビをつなぎます。
颯爽と電源を投入!

映らない!

まさか…故障か??と不安に駆られながらも「PC88 FE 電源」とググってみると、テレビ出力にするにはF・10を押して立ち上げないとダメらしい。
(下記のページはFE2の話だけどFEでも同じ)

そうきたか。まずキーボードがいるのね。
というわけで、一度もPC88の画面を見ることのないままキーボードのお勉強に突入します。
まずはおさらいから。

スキャンコードは後回しにするとして、通信そのものは20800bpsかつ12ビットのデータを調歩同期式で送信すればよいようです。
調歩同期式というのはよくあるシリアル通信の方式でArduinoだと普通にシリアル通信すれば勝手にそれで送信されます。
問題は12bitのデータ長です。
Arduinoのシリアル通信にはHardwareSerialとSoftwareSerialがありますが、調べてみた感じだとHardwareSerialの方では12bitは決定的に無理。
HardwareSerialでは内部的にUBRRやらUCSR[ABC]やらのレジスタを設定してハードレベルで通信することになりますが、ハードの上限がどうやら9bitらしい。さすがにこれは超えられない壁。

残るはSoftwareSerialです。
こちらはソフト的にGPIOでピコピコと信号を送る仕組みらしいので何とか出来るに違いない。
標準では8bitまでしかサポートしていませんが、これを伸ばせないか検討してみます。
送信に関する部分の概要を追ってみるとざっくりこんな流れです。

  • SoftwareSerial::begin()でボーレート周りの初期化とかデータ送受信のタイミング調整時間を計算する。送信時の調整時間_tx_delayもここで計算している。

  • SoftwareSerial::write()で1ビット送信してはタイミング調整、1ビット送信してはタイミング調整…を繰り返して、調歩同期式のパケット全体を送信する。

まずSoftwareSerial::begin()の先頭付近を見るとこうなっています。

void SoftwareSerial::begin(long speed)
{
  _rx_delay_centering = _rx_delay_intrabit = _rx_delay_stopbit = _tx_delay = 0;

  // Precalculate the various delays, in number of 4-cycle delays
  uint16_t bit_delay = (F_CPU / speed) / 4;

  // 12 (gcc 4.8.2) or 13 (gcc 4.3.2) cycles from start bit to first bit,
  // 15 (gcc 4.8.2) or 16 (gcc 4.3.2) cycles between bits,
  // 12 (gcc 4.8.2) or 14 (gcc 4.3.2) cycles from last bit to stop bit
  // These are all close enough to just use 15 cycles, since the inter-bit
  // timings are the most critical (deviations stack 8 times)
  _tx_delay = subtract_cap(bit_delay, 15 / 4);

_tx_delayというのがデータ送信する際の1ビットごとの調整時間なのですが、さてどうやって求めているのでしょうか。
まずbit_delayを計算しています。コードからは何をやっているのか分かりにくいですが、これは1ビットを送信するのに必要な時間を表す基準値で、この値をベースに各種タイミングを算出します。
ただし、(後で出てきますが)タイミング調整に使用する関数_delay_loop_2()で待機できる最小単位が4クロックのため、4クロック単位に換算した値になっています。
言い換えると、bit_delayは1ビット送信するのに必要な時間が_delay_loop_2()何個分か?という値です。

「4クロック単位(4/F_CPU)が 何個(bit_delay)あれば ボーレート(speed)になるか」
(4/F_CPU) * bit_delay = (1/speed)
bit_delay = F_CPU/speed/4

_tx_delayはbit_delayに補正をかけた値で、具体的にはbit_delayから3単位分(12クロック分)引いています。
補正に使用する関数は以下の通りで基本的にただ引き算するだけ。

uint16_t SoftwareSerial::subtract_cap(uint16_t num, uint16_t sub) {
  if (num > sub)
    return num - sub;
  else
    return 1;
}

送信時には1ビット送信ごとにこの補正した値だけ待機してタイミング調整します。
なぜ補正が必要かというと、各ビットの送信はbit_delayのタイミングに合わせて行う必要があるわけですが、送信処理自体にも実行時間があるのでbit_delayまるまる待機すると長く待機しすぎてしまいます。そのためその実行時間分を差し引いた時間だけ待機しなければならないという理由です。
ソース上のコメントによると送信処理の実行時間は大体どこでも15クロック付近になるらしくて、ざっくりそれくらい引いてやったぜというワイルドな事をしている模様。(少しのズレだったらなんとか許容されるらしい)

(ちょっと分かりにくい気がするので後で図でも入れようかな)

実際に送信しているのが次のSoftwareSerial::write()です。
1ビット書くごとにtunedDelay(delay)でタイミング調整しています。(delay=_tx_delay)

size_t SoftwareSerial::write(uint8_t b)
{
  if (_tx_delay == 0) {
    setWriteError();
    return 0;
  }

  // By declaring these as local variables, the compiler will put them
  // in registers _before_ disabling interrupts and entering the
  // critical timing sections below, which makes it a lot easier to
  // verify the cycle timings
  volatile uint8_t *reg = _transmitPortRegister;
  uint8_t reg_mask = _transmitBitMask;
  uint8_t inv_mask = ~_transmitBitMask;
  uint8_t oldSREG = SREG;
  bool inv = _inverse_logic;
  uint16_t delay = _tx_delay;

  if (inv)
    b = ~b;

  cli();  // turn off interrupts for a clean txmit

  // Write the start bit
  if (inv)
    *reg |= reg_mask;
  else
    *reg &= inv_mask;

  tunedDelay(delay);

  // Write each of the 8 bits
  for (uint8_t i = 8; i > 0; --i)
  {
    if (b & 1) // choose bit
      *reg |= reg_mask; // send 1
    else
      *reg &= inv_mask; // send 0

    tunedDelay(delay);
    b >>= 1;
  }

  // restore pin to natural state
  if (inv)
    *reg &= inv_mask;
  else
    *reg |= reg_mask;

  SREG = oldSREG; // turn interrupts back on
  tunedDelay(_tx_delay);
  
  return 1;
}

待機に使用しているtunedDelay()は次のように実装されていて実質的には「4クロックの整数倍待機する」という意味になります。(_delay_loop_2()の機械語実行に4クロックかかるそうな)

inline void SoftwareSerial::tunedDelay(uint16_t delay) { 
  _delay_loop_2(delay);
}

さて、もう一度先ほどのSoftwareSerial::write()を眺めてみると、結局やってるのは、送信する1バイトを受け取って、パケットの先頭から1ビット送信してはtunedDelay()…を繰り返しているだけなのが分かります。データ部分については単に8回ループを回しているだけです。

  // Write each of the 8 bits
  for (uint8_t i = 8; i > 0; --i)
  {
    if (b & 1) // choose bit
      *reg |= reg_mask; // send 1
    else
      *reg &= inv_mask; // send 0

    tunedDelay(delay);
    b >>= 1;
  }

…12回回せばよくね?

あまりに単純で逆に心配になるけど理屈的にはそれでいいはずなんだよなあ。
というわけで8ビットより長く送れるバージョンをやっつけで追加。
単に引数を追加してループを回す回数変えただけ。
(変数名がbのままなのは、もちろんめんどくさかったからさ!)

size_t SoftwareSerial::writeEx(uint32_t b, int length)
{
  if (_tx_delay == 0) {
    setWriteError();
    return 0;
  }

  // By declaring these as local variables, the compiler will put them
  // in registers _before_ disabling interrupts and entering the
  // critical timing sections below, which makes it a lot easier to
  // verify the cycle timings
  volatile uint8_t *reg = _transmitPortRegister;
  uint8_t reg_mask = _transmitBitMask;
  uint8_t inv_mask = ~_transmitBitMask;
  uint8_t oldSREG = SREG;
  bool inv = _inverse_logic;
  uint16_t delay = _tx_delay;

  if (inv)
    b = ~b;

  cli();  // turn off interrupts for a clean txmit

  // Write the start bit
  if (inv)
    *reg |= reg_mask;
  else
    *reg &= inv_mask;

  tunedDelay(delay);

  // Write each of the length bits
  for (uint8_t i = length; i > 0; --i)
  {
    if (b & 1) // choose bit
      *reg |= reg_mask; // send 1
    else
      *reg &= inv_mask; // send 0

    tunedDelay(delay);
    b >>= 1;
  }

  // restore pin to natural state
  if (inv)
    *reg &= inv_mask;
  else
    *reg |= reg_mask;

  SREG = oldSREG; // turn interrupts back on
  tunedDelay(_tx_delay);
  
  return 1;
}

あまりお行儀良くないですけど、私は標準ライブラリのコードを直接変更しました。(ヘッダ含めてクラス定義も変更要)
出力されるコードが多少変わっているはずなのでタイミングが少しずれる可能性はあるのと、8回分の誤差が積み重なるから注意しろよな!という_tx_delay計算時のコメントが気にはなりますけど(12回に増える予定)、動かしてみたところまあ何とかなるみたいですね。

次回はこれを使ってPC88のスキャンコードを送信してみる冒険の旅。

<追記>
一つ訂正。
データ長12bitと連呼してましたがSoftwareSerialだとパリティービットを自前で付加する必要があるため合計13bitが正解。

この記事が気に入ったらサポートをしてみませんか?