7839

雑草魂エンジニアブログ

ビット演算

マイコンレジスタ操作などをC/C++で行おうと思った場合には、ビット演算が必須となる。ビット演算は論理演算であることを理解していたものの、実際にどのように使うのか理解できていなかったので、今回備忘録として残しておく。

ビットとは

マイコンをはじめとするコンピュータの世界はデジタル、二進数、すなわち「0」か「1」で表現される。コンピュータの世界では1つの「0」か「1」のデータを1ビットと呼ぶ。そして、1ビットを8つ組み合わせたものを1バイトと呼ぶ。

1 Byte = 8 bit

C言語では、二進数を表現する場合、先頭に0bをつける。十進数である「23」を1バイトの二進数で表現すると以下のようになる。

23 = 0b00010111

ビット演算

論理演算は以下のようになっている。 f:id:serip39:20220208013017j:plain

AND

C言語では、AND演算を&で表現する。

#include <iostream>
#include <bitset>
using namespace std;
int main()
{
  int a = 0b10101010;
  int b = 0b11110000;
  cout << bitset<8>(a & b) << endl;
  // 0b10100000
}
  • 両方が「1」の場合のみ「1」
  • それ以外は「0」

OR

C言語では、OR演算を|で表現する。

#include <iostream>
#include <bitset>
using namespace std;
int main()
{
  int a = 0b10101010;
  int b = 0b11110000;
  cout << bitset<8>(a | b) << endl;
  // 0b11111010
}
  • 両方が「0」の場合のみ「0」
  • それ以外は「1」(いずれかが「1」であれば「1」)

NOT

C言語では、NOT演算を~で表現する。

#include <iostream>
#include <bitset>
using namespace std;
int main()
{
  int a = 0b10101010;
  cout << bitset<8>(~a) << endl;
  // 0b01010101
}
  • 「0」と「1」を反転させる

XOR

C言語では、XOR演算を^で表現する。

#include <iostream>
#include <bitset>
using namespace std;
int main()
{
  int a = 0b10101010;
  int b = 0b11110000;
  cout << bitset<8>(a ^ b) << endl;
  // 0b01011010
}
  • 「0」と「1」の場合に「1」(両方が一致していなければ「1」)
  • 両方が「0」または「1」の場合に「0」(両方が一致していれば「0」)

レジスタ操作

レジスタ操作時に、8bitのレジスタ全てを上書きする場合は直接8bitを指定するだけでいい。しかしながら、現在のレジスタの内1bitのみを書き換えたりする場合、ビット演算を行う必要がある。

実際に具体的な操作対象がある方がわかりやすいので、Arduino UNOのレジスタ操作を実際にやってみる。 デフォルトで入っているExampleのBlinkのコードは、以下のようになっている。

// LED_BUILTIN is D13 for Arduino UNO
// the setup function runs once when you press reset or power the board
void setup() {
  // initialize digital pin LED_BUILTIN as an output.
  pinMode(LED_BUILTIN, OUTPUT);
}

// the loop function runs over and over again forever
void loop() {
  digitalWrite(LED_BUILTIN, HIGH);   // turn the LED on (HIGH is the voltage level)
  delay(1000);                       // wait for a second
  digitalWrite(LED_BUILTIN, LOW);    // turn the LED off by making the voltage LOW
  delay(1000);                       // wait for a second
}

上記のコードをレジスタ操作のみに書き換えてみる。

以上のことから、

  • PORTB5をOUTPUTに設定したい場合は、DDRBの5bit目を「1」にする。
  • PORTB5をHIGHに設定したい場合は、PORTBの5bit目を「1」にする。
  • PORTB5をLOWに設定したい場合は、PORTBの5bit目を「0」にする。
void setup() {
  DDRB = 0b00100000;
}
void loop() {
  PORTB = 0b00100000;
  delay(1000);                       // wait for a second
  PORTB = 0b00000000;
  delay(1000);                       // wait for a second
}

上記の実装の場合、現在のレジスタの状況が何であっても強制的に上書きしてしまっている。特定のビットのみを「0→1」、「1→0」に変更する場合はビット演算を行う。

特定のビットのみをHIGHにしたい場合

特定のビットのみを1にした値と、現在の値でOR | をとる

#include <iostream>
#include <bitset>
using namespace std;
int main()
{
  int PORTB = 0b00010101;
  cout << "現在の値:" << bitset<8>(PORTB) << endl;
  int BIT_FLAG = 1<<5;
  cout << "操作対象:" << bitset<8>(BIT_FLAG) << endl;
  PORTB |= BIT_FLAG;
  cout << "計算結果:" << bitset<8>(PORTB) << endl;
}
// 現在の値:00010101
// 操作対象:00100000
// 計算結果:00110101

a 番目のビットのみを1にした値は (1<<a) で表せる。特定のビットが「1」になっていることをフラグが立っている状態と呼ぶ。よって、

1を二進数表示すると、1 = 0b00000001である。この1ビット目の「1」を任意のビットの位置まで左にシフトすることで特定のビットのフラグが立っている状態を作ることができる。

  • 1<<0 = 0b00000001
  • 1<<1 = 0b00000010
  • 1<<7 = 0b10000000

さらに、a 番目と b 番目と c 番目のフラグが立っている状態は (1<<a) | (1<<b) | (1<<c) と表せる。

特定のビットのみをLOWにしたい場合

特定のビットのみを1にした値にNOTをかけて反転した値と、現在の値でAND & をとる

#include <iostream>
#include <bitset>
using namespace std;
int main()
{
  int PORTB = 0b00110101;
  cout << "現在の値:" << bitset<8>(PORTB) << endl;
  int BIT_FLAG = 1<<5;
  cout << "操作対象:" << bitset<8>(BIT_FLAG) << endl;
  PORTB &= ~BIT_FLAG;
  cout << "計算結果:" << bitset<8>(PORTB) << endl;
}
// 現在の値:00110101
// 操作対象:00100000
// 計算結果:00010101

特定のビットのみを反転させたい場合

特定のビットのみを1にした値と、現在の値でXOR ^ をとる

#include <iostream>
#include <bitset>
using namespace std;
int main()
{
  int PORTB = 0b00010101;
  cout << "現在の値A:" << bitset<8>(PORTB) << endl;
  int BIT_FLAG = 1<<5;
  cout << "操作対象A:" << bitset<8>(BIT_FLAG) << endl;
  PORTB ^= BIT_FLAG;
  cout << "計算結果A:" << bitset<8>(PORTB) << endl;
  PORTB = 0b00110101;
  cout << "現在の値B:" << bitset<8>(PORTB) << endl;
  cout << "操作対象B:" << bitset<8>(BIT_FLAG) << endl;
  PORTB ^= BIT_FLAG;
  cout << "計算結果B:" << bitset<8>(PORTB) << endl;
}
// 現在の値A:00010101
// 操作対象A:00100000
// 計算結果A:00110101
// 現在の値B:00110101
// 操作対象B:00100000
// 計算結果B:00010101

特定のビットがHIGHか確認する場合

特定のビットのみを1にした値と、現在の値でAND & をとり、0以上であればHIGHである

  • ビット A の i 番目がHIGHかどうか:if (A & (1<<i))
  • ビット A の i 番目がLOWかどうか:if (!(A & (1<<i)))
#include <iostream>
#include <bitset>
using namespace std;
int main()
{
  int PORTB = 0b00110101;
  cout << "現在の値:" << bitset<8>(PORTB) << endl;
  int BIT_FLAG = 1<<5;
  cout << "操作対象:" << bitset<8>(BIT_FLAG) << endl;
  if (PORTB & BIT_FLAG) {
    cout << "操作対象はHIGHです。" << endl;
  } else {
    cout << "操作対象はLOWです。" << endl;
  }
}
// 現在の値:00110101
// 操作対象:00100000
// 操作対象はHIGHです。

まとめ

ビット演算に関して、整理することができた。

操作 ビット演算
ビット A の i 番目をHIGHにする A |= (1<<i)
ビット A の i 番目をLOWにする A &= ~(1<<i)
ビット A の i 番目を反転にする A ^= (1<<i)
ビット A の i 番目がHIGHかどうか if (A & (1<<i))
ビット A の i 番目がLOWかどうか if (!(A & (1<<i)))