標準TTLだけ(!)でCPUをつくろう!(組立てキットです!)
(ホントは74HC、CMOSなんだけど…)
[第13回]

●DAAのお話の続きです

前回と前々回は別のテーマを飛び入りでしてしまいましたので、DAAの説明が中断してしまいました。
今回はその説明の続きです。

これまでの説明で16進数と見かけ上はよく似たBCD数というものがあって、ともにその(16進数、またはBCD数の)1桁は2進数を4ビット毎に区切ったものに、16進数は0〜Fをあてはめて示し、BCD数は0〜9を当てはめて示す、ということを説明しました。
そして、コンピュータは純2進数で計算するが、16進数は2進数とまったく同値なので、計算結果を2進数として示しても、16進数として示してもなんら問題は生じない。
しかし、BCD数は純粋な2進数とは似て非なるものなので、BCD数の2数をコンピュータが2進数だとして加算すると、その結果はBCD数同士の加算結果として期待する値とは異なってしまう、というお話をしました。
その期待と異なって出てくる値を、BCD数の加算結果と同じになるように補正する命令がDAA(decimal adjust)である、というところまでが、前回のお話でした。

●BCD数の計算でも、2進数の計算と結果が一致する場合もある

今回はもう少しつっこんで、2進数の加算とBCD数の加算の相違点をさぐることからスタートしたいと思います。
前回は、2進数の加算結果とBCD数の加算結果は異なる、と説明しましたが、正確に言うと、一致する場合と異なる場合がある、という方が正しいのです。
2進数とBCD数で結果が一致する例として、BCD数の23+45=68という計算を考えてみます。

  10 0011    …16進数の23(十進では35になる) 
 100 0101(+  …16進数の45(十進では69になる)
 110 1000    …16進数の68(十進では104になる)

前回の例とは違い、今回の計算では、2進数としての加算結果であるにもかかわらず、「見かけ上」はBCD数として加算した結果とも一致しています。
もっともそれはあくまで「見かけ上」のことだけで、計算の対象となる「数値」としては、2進数とBCD数では、まったく異なる値を扱っていることになります。
つまり上の計算結果は、コンピュータ内部ではあくまで十進数の104に相当する2進数(16進数)の68(10進数と同じ表記になって区別がつかないので、68Hとか&H68などと表記したりします)として認識します。
しかし、68を16進数として考えるのか、BCD数として考えるのかは、コンピュータ(機械)ではなくて、プログラマ(人間)なのですから、プログラマが、ここは2進数値としての68ではなくて、いわば文字記号として考えたBCD数の68なのだ、とはっきり区別して考えていれば、それで何も問題はないことになります。

●一致する計算と不一致になる計算を区別して整理してみよう

さて、そこで、それではいったいどのような場合にBCDの計算と2進数の計算結果が一致し、またどのような場合に不一致となるのでしょうか?また、不一致の場合の差は一定になるのでしょうか?

そのことを表を使って確かめてみることにします。
DAAは1バイト(8ビット)の数を対象にしています。1バイトの数はBCD数では2桁、00〜99の数になります。
しかしいきなり2桁の数について、BCD数の計算と2進数の計算を比較しようとすると計算が複雑になりますから、1桁の数同士の計算で考えてみます。
桁上がりの処理を別にすれば、1位の数の計算(加算)も、10位の数の計算(加算)も、同じ計算(加算)方法ですから、まず1桁の計算について考えてみることができれば、あとは桁上がりについて、考慮すればよいことになります。

具体的な説明に入る前に、計算の前提条件についてまず説明をします。
DAA命令はあくまで直前に行われた計算(加算。たとえばADD命令)がBCD数同士の計算であることを前提にしています。
BCD数としてはありえない数の計算、たとえば1B+2D=48などの計算のあとでDAA命令を実行したとしても、もともとそれは意味の無い結果を期待していることになりますから、DAA命令の結果も、何らかの結果は出てきますが、まったく意味のない結果であると言えます。
したがって、今回のBCD数と2進数の計算結果の相違に関する整理についても、BCD数同士の計算のみが対象になります。それ以外の数の組み合わせは除外して考えます。

そのような前提のもとで考えてみると、BCD数同士の1桁の数の加算は、0〜9+0〜9であるといえます。つまり0+0からはじまって9+9の計算までです。これを表にまとめてみます。
九九の表を足し算に直したのと同じような表になります。

 
10進 加算
 9
A 10
A B 11
A B C 12
A B C D 13
A B C D E 14
A B C D E F 15
A B C D E F 16 16?
A B C D E F 17 16?
A B C D E F 18 16?
A B C D E F 19 16?

足す数の0〜9と足される数の0〜9は、表の上の1行と左の1列で、黄色で表示されています。
計算結果はその行と列ではさまれた部分に16進数で示されています。
このうち色がついていない部分は10進数(BCD数)として計算しても、16進数として計算しても、結果は同じになります。
色がついている部分は10進数(BCD数)として計算したときと、16進数として計算したときとで、結果が異なってきます。じつは、この部分は9の列(↑で示す列)に代表的に示されていますから、その右の行に、10進数(BCD数)としての計算結果を示して、比較できるようにしてみました。

左列(足す数)の一番下がAになっているのは、足す数が9でさらに下位桁からの桁上がり(キャリー)がある場合の計算を示しています。
左列の一番上の0は、足す数が0で下位桁からのキャリーもない場合の計算を示しています。
左列の0とA以外の数は、下位桁からの桁上がりがある場合と無い場合の計算の両方を示しています。たとえば、1は足す数が1でキャリーが無い場合と、足す数が0でキャリーがある場合の計算の両方を示しています。いずれの場合でも結果は同じになるからです。

DAA命令は↑で示した9列に代表、表示されている16進数を、その右側の列に表示されている「16進数」(注意。10進数ではありません!)に変換する作業なのです。
9列の数に対してどのような計算を行うと、その右の列の数(16進数)にすることができるのでしょうか?
話が複雑になりますから、とりあえず、水色で着色された範囲についてのみ考えてみてください。

●DAAの補正の方法が見えてきた!

すると、おおお!、と思わず声をあげてしまいそうになります。
9の列とその右の列をともに16進数として考えたとすれば、いずれも9の列の数(つまり補正前の数)に6を加算すれば、見かけ上は期待したBCD表現の結果になることがわかります!
水色で着色された数は、A〜Fですから、補正前の数がA〜Fならば、それに6を加算してやればよい、ということになります。
そのことを表の右端の列に加算する数として表示してみました。
補正のために6を加算すればよい、ということは、BCD加算と2進加算のしくみをよく考えれば理解できることなのですが、このように表にして考えてみることで、難しい計算理論などのお世話にならなくても、正解にたどりつくことができます。

●しかし、それだけでは問題はまだ解決しない!

問題はピンクで着色した部分です。
この範囲にあるのは0〜3です。
これに6を足してやっても、6〜9にしかなりません。それに0〜3は着色されていない範囲にもあります。
ピンクで着色された部分は補正しなくてもよいのでしょうか?
そうはいかないはずです。0〜3を16〜19に直さなければいけません(じつはこの表現は正しくありません。でもここではこういう表現にしておきましょう)。
うーん。これは困りました。
着色された0〜3の場合には、単純に考えると16を足してやる必要がありそうです。
でも着色されていない0〜3は、何も足す必要はありません。
この一見同じにしか見えない、0〜3の2つのグループはどのような理由で区別され、またどのような補正を行えばよいのでしょうか。

じつは、着色された0〜3と着色されていない0〜3はその1桁だけをみれば同じ0〜3なのですが、その結果が出てきた、もとの計算は性質の異なるものなのです。
どのように性質が異なるのでしょうか?

着色されていない0〜3は、1桁+1桁=1桁の計算結果なのです。上の表では左上角の0〜3+0〜3の範囲にのみ、その計算が行われています。
他方で、着色されている方の0〜3は1桁+1桁=2桁の計算結果の下位桁になっているのです。
表では右下角の6〜9+7〜Aの範囲でその計算が行われています。言いかえると、ピンクに着色された範囲の計算は、下位桁同士を加算した結果、上位桁に桁上がりが生じる計算である、と言えます。

DAA命令では、結果として示された同じ0〜3に対して、着色されていない0〜3に対しては、何の補正も行わず、ピンクで着色された0〜3に対してだけ、補正をしなければならないのです。

では、同じように見える、0〜3をどのようにしたら区別することができるのでしょうか?
もう、おわかりですよね(ここまでの説明のなかに、ちゃんとその答えは書いてありますよ)。

念のために言っておきますが、コンピュータの計算結果には、色はついていませんよ!

●しかし、計算結果に「色」がついていた!

じつは、計算結果にも、「色」がつけられていたのです。
8080を考えた人たちは、まあ、なんと賢い人たちだったのでしょう!

その「色」とはHフラグ(half carry flag)のことなのです。
ADD命令などで、8ビットの数の加算を行うときに、その計算の過程でビット3からビット4への繰り上がり(キャリー)が発生すると、Hフラグがセットされます。
2進数を4ビットごとに区切ったとき、ビット3は、その2進数を16進数表現で考えたときの下位桁の最上位ビットになります。
つまりビット3からビット4へのキャリーとは、言い方を変えれば、16進数2桁同士の加算において、下位桁から上位桁へ繰り上がりが発生したことを示している、「色」であると言えるのです。

ピンクで着色された0〜3を、着色されていない0〜3と区別するには、Hフラグが立っているかどうかを見ればよい、というのが正解なのです。

●では、どのように補正するのか?

これでやっと補正すべき0〜3と補正が不要な0〜3を区別する方法がみつかりました。補正前の数が0〜3で、かつHフラグが立っているときだけ、補正を行えばよいのです。
16を加算すればよい?
そうではありません。この場合にも6を加算すればよいのです。

なぜでしょうか?
そのことを理解するために、BCD数2桁同士の加算なのですけれど、実は、09+07という実質的に1桁同士であるような、計算を考えてみます。この2数の間で2進数の加算を行うと、結果は10になります。上位桁に桁上がりが発生するために、上位桁が1になります。この10を16に補正するのですから、16を加算するのではなくて、6を加算すればよいのです。(@)

ところで、あれっ?と気になりませんか?

なんだか、だまされてしまったようだけれど、すると、水色の部分の+6の計算は本当にそれで大丈夫?
今の説明とは逆に、なんだか補正によって、新たな桁上がりが発生してしまうように思えたのだけれど…。

念のために確認してみましょう。
今と同じように、実質1桁同士の計算で確かめてみることにします。
水色の部分の計算の例として、09+06を考えてみます。この2数の間で2進数の加算を行うと、結果は0Fになります。まだ上位桁は0のままです。この0Fを15に補正するために6を加算すると、結果的に上位桁に繰り上がりが生じて、ちゃんと期待通りの15になって、この場合には補正のための計算の過程で発生する上位桁への桁上がりも、補正の一部として必要であったことがわかります。(A)

いやあ、ずいぶん長い説明だったけれど、やっと終わりました。めでたし、めでたし。

●まだ、説明は終わりません!

じつは、まだ終わりにはできないのです。まだ2つ問題があります。
そのうち1つは単純なものです。まずそちらの方から片付けてしまいましょう。

●キャリーフラグが問題!

今までは、BCD数2桁同士の加算に対して、その下位桁にのみ注目してきました。
下位桁と上位桁は本当にまったく同じに扱ってよいのでしょうか?

さきほどの補正方法の検証を行った部分(@とA)が、上位桁だった場合について考えてみます。
ともに+6をすればよい、ということでした。
注目したいのは、この補正のための加算を行う前と後のフラグについてです。
問題はピンクに着色された範囲の計算で発生します。
上の例で@の場合です。
@の場合、09+07=10が補正前に行われた計算でした。同じことを今度は上位桁に移し変えて考えてみることにします。
すると、70+90という計算について考えることになります。2進数の計算では70+90=100になります(話を簡単にするために、この場合の下位桁は足す数も足される数も、ともに0であるとします)。
この計算結果の最上位桁の1は、何なのでしょうか?
BCD数は1バイト8ビットで2桁ですから、3桁目はありません。
そうなのです。このような計算が補正前に行われたとすると、この上位桁の1は、実は上位桁への桁上がりが発生したことを示す、Cフラグ(carry flag)なのです。
ところが、この100(じつはcarry flagがついた00)を60に補正するために、上位桁に6を加算すると、それは上位桁での0+6の計算をすることなので、その場合には加算後に上位桁への桁上がりは発生しません。
ということは、補正のための加算によって、Cフラグ(carry flag)がクリアされてしまうのです!
@の場合、つまり下位桁の場合には、この1はCフラグではなく、Hフラグであると同時に、補正前の計算の過程で、すでに上位桁に加算されてしまっていますから、補正のための加算によってHフラグがクリアされてしまっても、支障はありません。
もともとHフラグはこのDAA命令のためにあるフラグですから、DAA補正が済んでしまえば、クリアされようがどうしようがまったくかまわないのです。
しかし、Cフラグは上位桁への桁上がりの情報を保持していなければならないのですから、DAA補正によってCフラグがクリアされてしまっては困ります(まだ上位桁の計算はこれからあとで行われるはずです。そのときまで、桁上がりの情報は維持していなければなりません)。
そこで表のピンクで着色した範囲の数が上位桁である場合(つまり上位桁が0〜3でかつCフラグが立っている場合)には、補正前の上位桁に6を加算するだけではなくて、補正によってクリアされてしまうCフラグをセットし直しておく必要があるのです。

Aの場合はどうなのでしょうか?
これも上位桁の計算に置き換えて考えてみます。
90+60=F0という計算が行われたあとの補正で上位桁に6を加算します。補正前の計算ではCフラグはまだ立ってはいません。補正によって上位桁のFに6を加算すると、上位桁は5になって、さらにその上の桁への桁上がりを示すCフラグが立ちます。このCフラグは結果を150にするために上位桁(上位バイト)への桁上がりに必要なものですから、この場合のCフラグの動作は補正によってかえって正しくなった、ということが言えます。したがって上の例とは異なり、補正計算のあとでCフラグの内容を変える必要はないことがわかります。

さてもうひとつの問題は少し面倒です。
この問題は補正前の数が9A〜9Fのときに発生します。
下位桁に注目すると、A〜Fですから、下位桁に6を加算する必要があります。上位桁は9です。表では色はついていませんから、補正のための加算は必要ありません。
ところが!

百聞は一見にしかず、です。
実際に補正のために、例として9Aに6を加算してみましょう。補正前の数が9Aになる計算の例としては、幾通りもありますが、なるべくわかりやすい計算例として、46+54=9Aという計算が行われたことにします。するとこの場合DAA命令に求められるのは、9Aを100に換算すること、であるということがわかります。
その換算結果を求めるために、今まで考えてきたルールにしたがって、9Aに6を加算してみます。

 1001 1010   9A
 0000 0110(+ 06
 1010 0000   A0

あれえ?おかしいですね。100、つまり結果が00になって、上位バイトへのCフラグが立たなければいけないはずなのに、A0などという予期しない結果になってしまいました。
なぜこのようなことになってしまうのでしょうか?
補正前の9は本来ならば、補正の必要の無い数なのですが、この9が上位桁にある場合に、下位桁がA〜Fの時はその下位桁に対する補正計算によって、上位桁への桁上がりが発生するために、9だったものがAになってしまうのです。
したがってこの場合には、Aになってしまった上位桁に対して、さらに6を加算する必要があるのです。
つまり、補正する前の数が、9A〜9Fの場合には、結果的に上位桁にも下位桁にも補正が必要となるため、66を加算しなければならない、ということになります。
そのことを2進数の加算で確認してみます。

  1001 1010   9A
  0110 0110(+ 66
1 0000 0000

正しく補正が行われることがわかりました。

●以上のまとめ

以上をまとめてみると、次のようになります。
補正前の数の、
1)下位桁がA〜Fの場合は、下位桁に6を加算する
2)下位桁が0〜3で、かつHフラグが立っているときも、下位桁に6を加算する
3)上位桁がA〜Fの場合は、上位桁に6を加算する
4)上位桁が0〜3で、かつCフラグが立っているときは、上位桁に6を加算後、Cフラグをセットする
5)上位桁が9で下位桁がA〜Fの場合には、66を加算する

やっと、終わりました!以上で宿題は終わり!

●まだ、宿題は全然終わりではありません!

わー、もう、いいかげんにしてほしいよー!

ですが、まだ、肝心のNフラグの話が全然できていません。
じつは、これからが、やっとお話の本論なのです!

いままでしてきたDAA(十進補正)の説明は全部、補正前の計算が「加算」であった、と仮定して説明してきました。
では、補正前に行われた計算が、減算だったとしたら、どうなのでしょうか?
その場合にも、6を加算すればよいのでしょうか?

説明がずいぶん長くなってしまいましたので、減算のあとのDAA(十進補正)については、次回にすることにいたします。

えー。正直なところ、いまどきDAAなど使う人は多分いないと思います(まだ、いらっしゃいますか?)
いまどきは、プログラムをアセンブラで書く人自体が減ってしまって、PICのプログラムだってCで書く人が増えたとかと聞きます。
Cではないにしても、最終的に計算結果をBCDで表示したいようなプログラムだった場合に、途中の計算の過程も全部BCDで通すっていうのは、ちょっとどうか…と思います。
まずはBCD数を全部2進数に変換しておいて、計算は純粋の2進数として行って、最後にBCDで表示することが必要になった時点で、はじめてBCDに変換する、というのが常道だと思います。
その場合でもDAA命令を使うのではなく、2進数から10進数への変換ルーチンを作って、それによって変換を行うようにします(多分、普通は、です)。

すると、いまどき、DAA命令など使わないので、そんなかびの生えたような命令にこれだけ長々と説明するのは、まったくムダ、ということになります。

かって、私が高校生の時、大学を出たての現代国語の教師が新任としてやってきました。彼は非常にアクが強い先生で、新任であるにもかかわらず、なかなかにクセの強い授業をすることで、一躍有名になってしまいました。
その彼の授業でいまだに印象に残っていることがあります。
高校の教科書ともなると、いろいろな現代文の中で、原文に当用漢字以外の漢字が使われている場合でも、それをひらがなに直すのではなくて、読みを括弧でつけたりルビをつけて、そのまま載せていることがあります。
彼は、授業中に、教科書の中に、そのような漢字を見つけるたびに
「これは、当用漢字ではない。だから、覚えなくてもよい!」
と口癖のように言うのが常でした。

うう。それは、そうなのかもしれないけれど、それって、ちょっと…。

いや、別に、DAA命令とは、まったく何の関係もないお話です。ただ、なんとなく、思い出してしまっただけです。
(いやー、とにかく、疲れた!)
2008.7.20upload
2008.7.22加筆訂正

前へ
次へ
ホームページトップへ戻る