Perl で 実数 2進数 変換

Perl の基礎を学習する上で、実数から2進数に変換するプログラムを扱うことにしました。

ハードウェアを設計していて、算術演算回路の符号付きで小数点以下の数値を扱う場合、デジタルなので当然 2進数で演算するのですが、整数から2進数の変換をしてくれる電卓はあるものの、実数から2進数に変換してくれる電卓がなく、いちいち手計算で検証するのは大変です。

そこで、 Perl でプログラムを書いてみました。あとで解説しますが、まずは全体を載せておきます。

なお、このプログラムは以下のように実行します。

  % real2bin.pl -12.1 5 5

 第1引数:実数
 第2引数:整数部のbit幅
 第3引数:小数部のbit幅

すると、結果がこのように表示されます。

  110011.11101


  1 #!/usr/bin/perl
  2
  3 sub print_input_error {
  4   print "\n";
  5   print " ##ERROR## Not enough argument.\n";
  6   print " Usage:   % real2bin.pl {real value} {integral bits} {decimal bits} \n";
  7   print " Example: % real2bin.pl -12.004 10 10\n";
  8   print "\n";
  9 }
 10
 11 if (@ARGV == 3) {
 12   $input_real = $ARGV[0];
 13   $integral_bits = $ARGV[1];
 14   $decimal_bits = $ARGV[2];
 15 } else {
 16   &print_input_error;
 17   exit;
 18 }
 19
 20 $minus = 0;
 21 if ($input_real < 0) {
 22   $minus = 1;
 23 }
 24
 25 $max = 2 ** $integral_bits;
 26
 27 if ($minus) {
 28   $p_num = $max + $input_real;
 29 } else {
 30   $p_num = $input_real;
 31 }
 32
 33 # round
 34 $round = 1;
 35 for ($i = 0; $i <= $decimal_bits; $i++) {
 36   $round = $round / 2;
 37 }
 38
 39 $p_integral = int $p_num;
 40
 41 $p_decimal = $p_num - $p_integral + $round;
 42
 43 @integral = ();
 44 $tmp_integral = $p_integral;
 45 for ($i = 0; $i < $integral_bits; $i++) {
 46   $integral[$i] = $tmp_integral % 2;
 47   $tmp_integral = int $tmp_integral / 2;
 48 }
 49
 50 @decimal = ();
 51 $tmp_decimal = $p_decimal;
 52 for ($i = 0; $i < $decimal_bits; $i++) {
 53   $tmp_decimal = $tmp_decimal * 2;
 54   if ($tmp_decimal > 1) {
 55     $decimal[$i] = 1;
 56     $tmp_decimal = $tmp_decimal - 1;
 57   } else {
 58     $decimal[$i] = 0;
 59   }
 60 }
 61
 62 # print result
 63 print "$minus";
 64 for ($i = $integral_bits - 1; $i >= 0; $i--) {
 65   print "$integral[$i]";
 66 }
 67 print ".";
 68 for ($i = 0; $i < $decimal_bits; $i++) {
 69   print "$decimal[$i]";
 70 }
 71 print "\n";
 72
 73 exit 0;

それでは1つずつ解説していきます。


実行属性の付け方

  1 #!/usr/bin/perl

1行目のこれ↑は 実行コマンドの perl がインストールされている path を指定しています。実行コマンドの perl がどの path にあるかは、以下のように which コマンドで確認します。

  % which perl
  /usr/bin/perl

#!/usr/bin/perl のように1行目に path を指定しておき、perl の script ファイル (ここでは real2bin.pl) のファイル属性に実行属性を付けておけば、script ファイル名を入力するだけで perl プログラムが実行できるようになります。

  % chmod +x real2bin.pl
  % ls -l real2bin.pl
  -rwxr-xr-x real2bin.pl

これで real2bin.pl という script は実行属性が付きましたので、以下のように実行させることができます。

  % real2bin.pl -12.1 5 5
  110011.11101


サブルーチン

いきなりですが、3〜9行目にはサブルーチンが定義されています。これは引数も戻り値も持たない簡単なサブルーチンです。

  3 sub print_input_error {
  4   print "\n";
  5   print " ##ERROR## Not enough argument.\n";
  6   print " Usage:   % real2bin.pl {real value} {integral bits} {decimal bits} \n";
  7   print " Example: % real2bin.pl -12.004 10 10\n";
  8   print "\n";
  9 }

このサブルーチンの目的は、外部引数が必要な real2bin.pl を実行させるとき、誤った引数が与えられたときに表示する syntax error メッセージであり、引数の与え方を忘れてしまったときに、あえて表示させる help メッセージの代わりとなっています。

サブルーチンの書き方として、引数の受け取りの仕方や戻り値の書き方の説明は、長くなるので別のところで説明したいと思います。


外部引数の受け取り

11〜18行目に書かれているのは、real2bin.pl に必要な引数の代入を行っています。また、引数として3つが必要なため、3つ以外の引数が与えられた場合はプログラムが終了する仕掛けになっています。

 11 if (@ARGV == 3) {
 12   $input_real = $ARGV[0];
 13   $integral_bits = $ARGV[1];
 14   $decimal_bits = $ARGV[2];
 15 } else {
 16   &print_input_error;
 17   exit;
 18 }

1つ目の引数は、実数です。整数でなく、実数ですから正負の小数点以下の数値を含みます。
2つ目の引数は、2進数に変換する際の整数部のbit幅を指定してもらいます。
3つ目の引数は、2進数に変換する際の小数点以下のbit幅を指定してもらいます。

なお、2つ目の引数は、2進数で表現するのに十分なbit幅が指定されなければなりません。


負の数の扱い

20〜31行目に書かれているのは、コンピューターが負の数を扱うルールに合わせて、引数で与えられた実数を前処理している部分です。

 20 $minus = 0;
 21 if ($input_real < 0) {
 22   $minus = 1;
 23 }
 24
 25 $max = 2 ** $integral_bits;
 26
 27 if ($minus) {
 28   $p_num = $max + $input_real;
 29 } else {
 30   $p_num = $input_real;
 31 }

コンピューターの中では、0 と 1 の値しかありませんから、負 (-) を表現するために一般的に使われているルールがあります。それが 2の補数を 負の数として扱うもので、分かりやすく説明するために整数を使って8bitで表現すると以下の数列のようになります。

   5 00000101
   4 00000100
   3 00000011
   2 00000010
   1 00000001
   0 00000000
  -1 11111111
  -2 11111110
  -3 11111101
  -4 11111100

2の補数というのは、もとの2進数を反転させて+1した数値のことです。
なぜ、2の補数が負の数として扱われるかと言うと、2の補数を使って足し算をすると、引き算をした結果を得ることができるからです。
以下に 5 - 3 の計算を例として示します。

3の2の補数は、3:00000011 を反転:11111100 して +1 した 11111101 です。
5:00000101 にこれを足し算すると以下のようになります。

    00000101
  + 11111101
  = 00000010

桁上がりで8bitの外に出てしまった1を無視すると、2:00000010 が得られました。
足して引き算の結果が得られるのだから、2の補数を負の数として扱おう、そうすれば加算器だけで引き算もできるので楽だ。と言うことになるのです。

さて、3の2の補数:11111101は、そのまま10進数の正の整数で表すと 253 です。これは、2の8乗である 256 を -3 した値です。2の補数がそういうものですから当然です。

ここで、perl プログラムに話をもどして、
perl プログラムの 28行目で、2のn乗(n:引数として与えられた整数部のbit幅)を $max として、もし、引数で与えられた実数が負の場合は、$max から引き算をしています。
実はこれで コンピューターの中で扱われる負の数、つまり、2の補数 $p_num を得ています。

なお、引数で与えられた実数が 負の数 でない場合は、与えられた実数をそのまま $p_num に代入しています。


小数部の処理 - 丸め処理

33〜41行目に書かれているのは、real2bin.pl に与えられた小数部のbit幅でおさめるために、丸め処理(四捨五入)を行っています。これも2進数に変換する前に処理しておきます。なぜなら、2進数に変換後に四捨五入すると桁上げするためのプログラミングが大変だからです。

 33 # round
 34 $round = 1;
 35 for ($i = 0; $i <= $decimal_bits; $i++) {
 36   $round = $round / 2;
 37 }
 38
 39 $p_integral = int $p_num;
 40
 41 $p_decimal = $p_num - $p_integral + $round;

例えば 実数の 0.1 を2進数で表すと 0.0001100110011... のように永遠に続く数列になります。
また、0.000110011 を実数になおすと 0.099609375 で
0.0001100110011 までbit幅を広げて実数になおすと0.0999755859375 になります。

2進数の小数側の数字というのは、重みが桁が下がるごとに、2の-1乗,-2乗,-3乗... となるのですが、実際に数字に直すと以下のようにそれほど小さい数字にならないからでしょう。

  0.1     2**-1 = 0.5
  0.01    2**-2 = 0.25
  0.001   2**-3 = 0.125
  0.0001  2**-4 = 0.0625
  0.00001 2**-5 = 0.03125

このように2進数で小数点以下の数字を表すとbit幅によって精度が変わります。そこで、限りあるbit幅で出来る限り良い精度を求めようとすると 丸め処理が行われるのが一般的です。

丸めは、求める2進数の最下位bitの1つ下位に2進数の1を足すことを行います。例えば、上記の 0.000110011 を 8bit に丸めるとすると、以下のよう 9bit目に1を加えて丸めます。

 bit#---123456789
      0.000110011 : 0.099609375
     +0.000000001 : 0.001953125
     =0.00011010  : 0.101562500 : 誤差 +0.0015625

ちなみに、もし、丸め処理せずに切り捨てになったら、以下のように 0.1 との差が広くなってしまいます。

 bit#---123456789
      0.000110011 : 0.099609375
      0.00011001  : 0.097656250 : 誤差 -0.00234375

ここで、perl プログラムに話をもどして、
perl プログラムの35〜38行目で丸めのときに足す最下位のもう1つ下位の数値 $round を求めています。
40行目で整数部のみ $p_integral を得て、42行目で丸め処理をした小数部を得ています。なお、int は与えられた引数の整数を返す関数で、perl の標準関数です。


整数部の2進数変換

さて、ここまでで、2進数へ変換する前処理が出来ました。いよいよ2進数に変換します。まずは整数部を変換します。

前処理で 負の数、丸め処理による桁上げ を済ませた数字 $p_integral なので、シンプルな2進数変換が出来ます。

 44 @integral = ();
 45 $tmp_integral = $p_integral;
 46 for ($i = 0; $i < $integral_bits; $i++) {
 47   $integral[$i] = $tmp_integral % 2;
 48   $tmp_integral = int $tmp_integral / 2;
 49 }

44行目に2進数に変換した後に 0 1 を蓄える配列を初期化しています。
あとは for文で外部引数で与えられた整数部のbit数分 $integral_bits 回 2の割り算をして、余りが出れば 1 、割り切れれば 0 を配列に蓄えて行きます。


小数部の2進数変換

 50 @decimal = ();
 51 $tmp_decimal = $p_decimal;
 52 for ($i = 0; $i < $decimal_bits; $i++) {
 53   $tmp_decimal = $tmp_decimal * 2;
 54   if ($tmp_decimal > 1) {
 55     $decimal[$i] = 1;
 56     $tmp_decimal = $tmp_decimal - 1;
 57   } else {
 58     $decimal[$i] = 0;
 59   }
 60 }

小数部の2進数変換も整数部と同様に for文で回しながら配列に蓄えて行きます。

演算の仕方は、2倍して1より大きければ 1 、小さければ 0 を配列に蓄えて行きます。このとき、1より大きかった場合は、1を減算します。


最後に演算結果を表示してプログラムは終了です。

 62 # print result
 63 print "$minus";
 64 for ($i = $integral_bits - 1; $i >= 0; $i--) {
 65   print "$integral[$i]";
 66 }
 67 print ".";
 68 for ($i = 0; $i < $decimal_bits; $i++) {
 69   print "$decimal[$i]";
 70 }
 71 print "\n";


TITLE

自己紹介

50才になる半導体エンジニアです。
大学で電子電気工学を学び、1990年にその分野のまま就職。ASICやマイコンの設計を長く続けてきましたが20年も同じ分野にいると業態も衰退したり変化するもので退職し、今は外資のIT系会社に再就職して設計請負業をやっております。
お問い合わせは
nakata.xianzhi@outlook.com







Linux と 小ネタ

デジタル回路設計

海外駐在後記