ビット演算とArduinoのスケッチで個々のビット操作を行う方法を学びます。
Author: Don Cross、Last revision: 2024/01/26
この記事は、2022年9月28日にHannes Siebeneicherにより改訂されました。
Arduino環境(もしくは他のコンピューター)でプログラミングするときに、個々のビットを操作することが有用もしくは必要となる場合がよくあります。ここでは、ビット演算が役立つ場面を、いくつか紹介します。
- 8個までの真偽データ値を1バイトにまとめてメモリを節約します。
- 制御レジスタやハードウェアのポートレジスタの個別のビットをオン・オフします。
- 2の乗数のかけ算・割り算を含む演算を実行します。
この記事では、C++言語で利用できる基本的なビット演算子を、最初に紹介します。その後、一般的に有用な演算を実行するために、ビット演算子を組み合わせる方法を学びます。この記事は、CosineKittyによるビット演算のチュートリアルを基にしています。
2進法
ビット演算子をよりよく説明するために、このチュートリアルでは、ほとんどの数値を2進法で表現します。この体系では、全ての整数値は0と1の値だけを使います。これは、今日のコンピューターが内部でデータを格納する実質的な方法です。0と1はビット(bit)と呼びます。bitは、binary digitの略です。
なじみのある10進法では、例えば、572は、5*10^2 + 7*10^1 + 2*10^0を意味します。同様に、2進数の11010は、1*2^4 + 1*2^3 + 0*2^2 + 1*2^1 + 0*2^0 = 16 + 8 + 2 = 26を意味します。
このチュートリアルの残りの部分を読み進めるには、2進法がどのように計算されるのかを理解することが重要です。この領域で支援が必要な場合は、2進法に関するWikipediaの記事(二進法/Binary number)から始めるのがいいと思います。
Arduinoでは、二進数は0bという接頭語をつけて表現できます。例えば、0b11 == 3
です。過去の経緯から、B0
からB11111111
までの定数も定義しています。これも同様に利用できます。
ビット単位のAND演算子
C++でのビット単位のAND演算子は、一つのアンパサンド(&
)で、2つの整数表現間で利用されます。ビット単位のAND演算子は、両辺の整数表現の個々のビットごとに独立して演算します。この決まりによると、両方の入力が1であれば1、そうでなければ0です。これを別の表現で表すと、以下のようになります。
|
|
Arduinoでは、int
型は16ビットなので、2つのint
表現に&
を適用すると、16個のAND演算が同時に起こります。以下のようなコード片では、
|
|
a
とb
の16ビットのそれぞれが、ビット単位のAND演算子により処理され、16個全てのビットがc
に格納されます。その結果は、2進法で01000100
で、10進法では68です。
ビット単位のAND演算子の最も一般的な使用方法のひとつは、整数値から特定のビットを選ぶというもので、マスキングとも呼ばれます。例えば、変数x
の最下位ビットを取得し、y
という別の変数に格納するには、以下のコードが利用できます。
|
|
ビット単位のOR演算子
C++でのビット単位のOR演算子は、一つのバー(|
)です。&
演算子のように、2つの整数表現間で利用されますが、(当然ですが)結果は異なります。ビット単位のOR演算子は、両辺の整数表現の個々のビットごとに独立して演算します。2つのビットに対するビット単位のOR演算子は、一方もしくは双方が1のときは1で、そうでなければ0です。
これを別の表現で表すと、以下のようになります。
|
|
ビット単位のOR演算子のC++コードでの使用例は以下の通りです。
|
|
ビット単位のOR演算子は、与えられた式で指定したビットをオンにする(1に設定する)のによく使われます。例えば、a
からb
に、最下位ビットを1にしてコピーするには、以下のコードを使います。
|
|
ビット単位のXOR演算子
ビット単位のXOR演算子という、少し毛色の変わった演算子がC++にあります。英語でXORは通常、“eks-or"と発音します。ビット単位のXOR演算子は、キャレット記号^
で表します。この演算子はビット単位のOR演算子|
に似ていますが、どちらかの入力ビットが1のときに1と評価されるところが異なる点です。双方とも0もしくは1の場合、ビット単位のXOR演算子は0と評価されます。
|
|
ビット単位のXOR演算子を他の表現で行えば、入力が異なる場合は1で、同じ場合は0ということになります。
以下に簡単なコード例を示します。
|
|
^
演算子は、数値表現の他の値を変更せず、一部のビットをトグル(0を1に、1を0にする)するのによく使われます。例えば、
|
|
ビット単位のNOT演算子
C++でのビット単位のNOTは、チルダ記号(~
)です。&
や|
とは違い、演算子右側の一つのオペランドに適用されます。ビット単位のNOT演算子は、ビットを反対の値(0は1に、1は0)に変更します。例えば、
|
|
この演算の結果が-104のような負数になることに驚くかもしれません。これは、int
の最上位ビットが、いわゆる符号ビットであるためです。最上位ビットが1のとき数値は負であると解釈されます。正負をこのように表現することは、2の補数表現と呼ばれます。詳細は、Wikipedia article on two’s complement/2の補数を参照してください。
余談ですが、任意の整数x
について、~x
は、-x-1
となることは興味深いです。
後述するように、符号付整数の符号ビットは、予期しないことを引き起こすこともあります。
ビットシフト演算子
C++には、左シフト演算子<<
と右シフト演算子>>
の、2つのビットシフト演算子があります。これらの演算子は、演算子の左側のオペランドを右側の数値分だけ、左あるいは右にシフトします。例えば、
|
|
x << y
で、x
をy
ビット分左にシフトすると、x
の左のy
ビットは失われ、文字通り失われます。
|
|
どのビットも左シフト演算子によって失われないとき、左シフト演算子は、左オペランドを2の右オペランド乗すると考えることができます。例えば、2のべき乗は以下のようにあらわすことができます。
|
|
x
の最上位ビットが1のとき、x >> y
で、x
をy
ビット分右にシフトすると、その結果はx
のデータ型に依存します。x
がint
型の場合、先述したように、最上位ビットは符号ビットで、x
が負かそうでないかを決めます。この場合、歴史的な理由により、符号ビットは下位ビットにコピーされます。
|
|
符号拡張と呼ばれるこの動作は、望まない動作の場合もあります。そうではなく、左から0を埋めたい場合もあると思います。unsigned int
表現の場合は、右シフトの規則は異なるので、キャスト演算子を使い、最上位ビットをコピーするのを防ぐことができます。
|
|
符号拡張に注意すれば、右シフト演算子>>
を2のべき乗での除算に使うことができます。例えば、
|
|
代入演算子
プログラミングでは、変数x
の値に対して演算を行い、変更された値をx
に書き戻すことがよくあります。多くのプログラミング言語では、例えば、x
の値を7増加させるには、以下のようなコードを使います。
|
|
プログラミングではこのようなことはよく起こるので、C++では短縮した特別な代入演算子を用意しています。上記のコード片は、以下のように簡潔に記述することができます。
|
|
ビット単位のANDやビット単位のOR、左シフト、右シフトにも全て、省略した代入演算子があります。以下に例を示します。
|
|
ビット単位のNOT演算子~
には、省略形の代入演算子はありません。x
の全てのビットを反転させるには、以下のようにする必要があります。
|
|
注意事項: ビット単位の演算子と論理(ブール)演算子
C++でのビット演算と論理演算はとても混乱を起こしやすいです。例えば、ビット単位のAND演算子&
と、論理AND演算子&&
は、以下の2つの理由により全く異なります。
- 双方は数値を同じようには計算しません。ビット単位のAND演算子
&
はオペランドの個々のビットを独立して演算します。一方、論理&&
は、双方のオペランドを、一つの論理値(true
==1かfalse
==0)に変換し、true
かfalse
の一つの値を返却します。例えば、4 && 2 == 0
です。4は2進数では100、2は2進数では010で、それぞれのビットで同じ位置が1になっていないからです。しかし、4 && 2 == true
で、true
は数値としては1です。4は0ではなく、2も0ではないので、双方ともtrue
だからです。 - ビット単位の演算子は常に双方のオペランドを評価します。一方、論理演算子はいわゆる短絡評価(short-cut evaluation/short-circuit evaluation)を行います。これは、例えば、出力がメモリ上に何かの値を出力したり修正したりするなど、オペランドが副作用を伴うときにだけ影響がでます。よく似た2つの行が全く異なる振る舞いをする例を示します。
|
|
|
|
|
|
このプログラムをコンパイル・アップロードし、Arduino GUIのシリアル出力を見ると、1秒ごとに以下の行が出力されています。
|
|
これは、fred(0)
とfred(1)
の両方が呼ばれ、出力が生成され、戻り値の0と1がビット単位でAND演算され、x
に0が格納されるからです。以下の行を編集し、
|
|
ビット単位の&
を論理演算の&&
に変更します。
|
|
そして、コンパイル・アップロードし、プログラムを再度実行すると、1秒ごとに1行だけが出力されることに驚くかもしれません。
|
|
なぜこのようなことがおこるのでしょうか? これは、論理&&
が短絡評価を使っているからです。左側のオペランドがゼロ(false
)であれば、式の結果がfalse
になることは明らかなので、右側のオペランドを評価する必要がないからです。言い換えれば、int x = fred(0) && fred(1);
は、以下と同じです。
|
|
明らかに、論理&&
は、この複雑なロジックを簡潔に表す方法です。
ビット単位のANDと論理ANDは、ビット単位のORと論理ORとは異なる点があります。ビット単位のOR演算子|
は常に双方のオペランドを評価します。一方、論理OR演算子||
は、左側のオペランドがfalse
(ゼロ)のときだけ、右側のオペランドを評価します。また、ビット単位の|
演算子は双方のオペランドの個々のビットを独立して演算します。一方、論理||
は、双方のオペランドを、true(非ゼロ)かfalse(ゼロ)として扱い、評価した結果、true(どちらかのオペランドが非ゼロ)か、false(双方のオペランドがゼロ)を返します。
まとめ: よくある問題を解決する
Arduino環境でC++の文法を使い、有用なタスクを実行するために、さまざまなビット単位の演算子を組み合わせる方法を見ていきましょう。
Atmega8マイクロコントローラーのポートレジスタについて
Atmega8のデジタルピンに読み書きするときは通常、Arduino環境が提供する組込み関数のdigitalRead()やdigitalWrite()を使います。setup()
関数で、デジタルピンの2番から13番を出力に設定し、11と12、13番ピンをHIGHにし、残りの全てのピンをLOWにすることを考えます。通常は以下のようにします。
|
|
Atmega8のハードウェアポートに直接アクセスと、ビット単位の演算子を使う方法もあります。
|
|
|
|
|
|
|
|
制御レジスタのDDRD
とDDRB
の8ビットそれぞれが、デジタルピンを出力(1)にするか入力(0)にするかを決める、という事実をこのコードは利用しています。Atmega8に14番ピンや15番ピンはないので、DDRBの上位2ビットは使いません。同様に、ポートレジスタのPORTB
とPORTD
は、最後にデジタルピンに書き込まれた値である、HIGH(1)かLOW(0)を保持しています。
一般に、この種の方法はいいアイデアではありません。なぜでしょうか? いくつか理由があります。
- コードのデバッグやメンテナンスが難しくなり、他の人が理解するのも難しくなります。コードを実行するのは数マイクロ秒ですが、動かない原因を見つけ、修正するのに何時間もかかります。時間は貴重ですよね? しかし、コンピューターに供給する電力のコストで測ると、コンピューター時間は安いものです。通常は、一番わかりやすい方法でコードを書くのが最もいい方法です。
- コードに移植性が損なわれます。digitalRead()とdigitalWrite()を使えば、全てのAtmeマイクロコントローラーで動作するコードを書くのは容易です。一方、制御レジスタやポートレジスタはマイクロコントローラーによっては異なる可能性があります。
- ポートへの直接アクセスは、意図しない障害を引き起こす可能性があります。
DDRD = B11111110;
という行に注目してください。これは、0番ピンを入力のままにしておくことを意図しています。0番ピンを出力にすることによって、誤ってシリアルピンが動作しなくなる可能性があります。突然シリアルデータを受信できなくなったら、混乱します。
この機能を本当に使う必要があるか、よく考えてください。直接ポートにアクセスすることの利点もあります。
- プログラムメモリが少ない状況で動作させているとき、コード量を削減するためにこれらのトリックを使うことができます。ハードウェアピンのいくつかに同時に書き込むときには、
for
ループを使ってピンを個別に設定するよりも、ポートレジスタを利用した方がコード量が圧倒的に少なくなります。場合によっては、これが、フラッシュメモリに入るか入らないかを決めることになるときもあります。 - 複数の出力ピンを同時に設定する必要があるかもしれません。
digitalWrite(10,HIGH);
の後に、digitalWrite(11,HIGH);
とすると、10番ピンは11番ピンよりも、数マイクロ秒先にHIGHになり、タイミングに制約のある外部に接続したデジタル回路を混乱させる可能性があります。一方、PORTB |= B1100;
とすることで、両方のピンを全く同時にHIGHにすることができます。 - ピンを、マイクロ秒以下という高速にオン・オフすることができます。
lib/targets/arduino/wiring.
にあるソースコードを見ると、digitalRead()やdigitalWrite()は、何十行もあることがわかります。これは、多量の機械語命令にコンパイルされます。それぞれの機械語命令は16MHzで1サイクル必要なので、時間に制約のあるアプリケーションでは、時間がかかりすぎる場合もあります。直接ポートにアクセスすると、同じ操作を、より少ないクロックサイクルで実行することができます。
より高度な例: 割り込みを禁止する
学習したことをもとに、上級プログラマが時折コード中で行っている不思議なことを理解しましょう。以下のことは何を意味しているのでしょうか?
|
|
これは、Arduino 0007の実行時ライブラリにある実際のコードで、lib\targets\arduino\winterrupts.c
というファイルに記述されています。まず、GICRとINT0が何を意味しているかを知る必要があります。GICRは、あるCPU割り込みが有効(1)か禁止(0)かを定義する制御レジスタです。Arduinoの標準ヘッダファイルでINT0を調べると、様々な定義が見つかります。利用しているマイクロコントローラーにより、いかのどちらかです。
|
|
もしくは
|
|
あるプロセッサでは、以下のように変換され、
|
|
他のプロセッサでは、以下のように変換されます。
|
|
後者の方が理解しやすいので、後者について見ていきましょう。まず、(1 << 6)
という値は、1を6ビット左シフトすることを意味し、64です。この文脈では、この数値を2進数で表現した01000000で見る方が有用です。次に、ビット単位のNOT演算子~
がこの値に適用され、全てのビットが反転し、10111111になります。そして、ビット単位のAND代入演算子が使われるので、コードは以下と同じ作用を持っています。
|
|
これは、最上位から2番目のビットをオフにし、他のビットはそのままにしておくという効果があります。
INT0が0と定義されているコンピューターでは、コードは以下のように解釈されます。
|
|
これは、最下位ビットをオフにし、他のビットはそのままにしておきます。これは、Arduino環境の実行時ライブラリの1行のソースコードが、多くのマイクロコントローラーをサポートする方法の例です。
複数のデータを1バイトに詰め込んでメモリを節約する
個々のデータの値はtrueかfalseで、多くのデータがあるという状況がよくあります。一例は、LEDグリッドを構築し、個々のLEDをオン・オフし、記号を表示したりする場合です。例えば、5x7のビットマップでXという文字は以下のようなものです。
このようなイメージを格納する単純な方法、整数の配列を用いることです。この方法でのコードは以下のようになります。
|
|
|
|
これが、プログラムで使う唯一のビットマップであれば、これはこの問題に対する簡単で効率的な解です。1ピクセルに1バイトのプログラムメモリ(Atmega8では約7kバイト利用可能です)を使うので、合計35バイトです。これはそんなに悪くありません。しかし、ASCII文字セットの96個の印字可能な文字が必要な場合はどうでしょうか? このとき、96*35=3360バイト消費し、プログラムコードに使えるフラッシュメモリはとても少なくなってしまいます。
ビットマップを格納するより効率的な方法があります。上記の2次元配列を、1次元のバイト配列に置き換えます。各バイトは8ビットなので、下位7ビットで、5x7のビットマップの行を表します。
|
|
(ここでは、Arduino 0007からリ湯可能な、事前定義された2進定数を使っています)。これにより、各ビットマップを35バイトではなく5バイト使用するだけになります。しかし、この小容量のデータ形式をどのように使えばいいのでしょうか? DisplayBitMap()をBitMapの個々のバイト内のビットにアクセスするように書き換えてみます。
|
|
理解するべき重要な行は、以下の行です。
|
|
(1 << y)
という式は、data
内でアクセスしたいビットを選択します。そして、ビット単位のAND、data & (1<<y)
がアクセスしたいビットを検査します。ビットが設定されていれば、非ゼロ値となり、if
がtrueになります。一方、ビットがゼロであれば、falseと扱われ、else
が実行されます。
クイックリファレンス
このクイックリファレンスでは、いかに図に示すように、16ビット整数のビットを、最下位ビットを第0ビット、最上位ビット(整数が符号有の場合は符号ビット)を第15ビットとして表します。
変数n
は、常に0から15までと仮定しています。
|
|
|
|
|
|
|
|
|
|
|
|
ビット単位の&
と論理&&
の両方を使う興味深い関数があります。これは、与えられた32ビットの正数x
が、2の累乗(1、2、4、8、16、32、64、…)のときだけtrueを返します。例えば、IsPowerOfTwo(64)
はtrue
を返しますが、IsPowerOfTwo(65)
はfalse
を返します。この関数がどのように動作するのかを見るために、2の累乗の例として64を使ってみます。64は2進数で1000000
です。1000000
から1を引くと、0111111
になります。びっとたんい&
を適用すると、結果は0000000
です。一方、同様のことを65(2進数で1000001)に適用すると、1000001 & 1000000 == 1000000
となり、非ゼロです。
|
|
以下は、16ビット整数x
を2進数で表したとき、何個のビットが1かを数えて返却する関数です。
|
|
別の方法も示します。
|
|
ビット操作に関連する様々な方法はこちらを参照してください。
オリジナルのページ
https://docs.arduino.cc/learn/programming/bit-math/
最終更新日
February 4, 2024