組み込み開発を勉強していると、&、|、^、~、<<といった記号を使ったコードを目にすることがあります。
これらはビット演算と呼ばれ、数値を0と1のビット単位で操作するために使います。
特に組み込み開発では、GPIOや周辺回路を制御するレジスタの特定ビットだけを変更したり、複数の設定値を1つの数値へまとめたりするときに頻繁に登場します。
この記事では、2進数とビットの基本から、AND・OR・XOR・NOT、シフト演算、そして組み込み開発でよく使う「特定のビットだけを操作する方法」まで順番に解説します。
ビットとは?
ビット(bit)は、コンピュータが扱うデータの最小単位で、0または1のどちらかの値を持ちます。
たとえば8ビットのデータは、次のように8個の0と1で表現できます。
01001101
右端を最下位ビット(LSB:Least Significant Bit)、左端を最上位ビット(MSB:Most Significant Bit)と呼びます。
8ビットの場合、それぞれの位置は次のように表せます。
ビット位置 7 6 5 4 3 2 1 0
↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓
0 1 0 0 1 1 0 1
この例の01001101は、10進数では77、16進数では0x4Dです。
上図では、ビット演算で使用するAND・OR・XOR・NOTとシフト演算、さらに組み込み開発で重要になる特定ビットの操作をまとめています。ここから、それぞれの仕組みを詳しく見ていきましょう。
ビット演算とは?
ビット演算とは、整数を構成しているそれぞれのビットに対して行う演算です。
C/C++では、主に次の演算子を使用します。
| 演算 | 演算子 | 意味 |
|---|---|---|
| AND | & | 両方が1なら1 |
| OR | | | どちらかが1なら1 |
| XOR | ^ | 異なっていれば1 |
| NOT | ~ | ビットを反転 |
| 左シフト | << | ビットを左へ移動 |
| 右シフト | >> | ビットを右へ移動 |
AND演算とは?
AND演算は、対応する2つのビットが両方とも1の場合だけ1になります。
| A | B | A & B |
|---|---|---|
| 0 | 0 | 0 |
| 0 | 1 | 0 |
| 1 | 0 | 0 |
| 1 | 1 | 1 |
たとえば次の2つの値をAND演算してみます。
11001100 & 10101010 ------------ 10001000
AND演算は、特定のビットが1かどうかを調べたり、不要なビットを0にしたりするときによく使います。
OR演算とは?
OR演算は、対応するビットのどちらか一方でも1なら1になります。
| A | B | A | B |
|---|---|---|
| 0 | 0 | 0 |
| 0 | 1 | 1 |
| 1 | 0 | 1 |
| 1 | 1 | 1 |
たとえば次のようになります。
11000000 | 00001000 ------------ 11001000
組み込み開発では、特定のビットだけを1にするときによく使用します。
XOR演算とは?
XOR(排他的論理和)は、2つのビットが異なる場合に1になります。
| A | B | A ^ B |
|---|---|---|
| 0 | 0 | 0 |
| 0 | 1 | 1 |
| 1 | 0 | 1 |
| 1 | 1 | 0 |
XORには、1とのXORを取ると元のビットが反転するという特徴があります。
0 ^ 1 = 1 1 ^ 1 = 0
この性質を利用すると、特定のビットだけを反転できます。
NOT演算とは?
NOT演算は、各ビットの0と1を反転します。
0 → 1 1 → 0
C/C++では~を使います。
uint8_t value = 0b00001111;
value = ~value;
8ビットのvalueとして見ると、結果は次のようになります。
00001111 ↓ NOT 11110000
実際のC/C++では整数型への変換なども関係するため、ビット幅を意識して扱うことが重要です。
シフト演算とは?
シフト演算は、ビット列を左または右へ移動させる演算です。
左シフトには<<、右シフトには>>を使用します。
左シフト
00000001 << 1
↓
00000010
1 << 1は2、1 << 2は4、1 << 3は8になります。
1 << 0 = 00000001 = 1 1 << 1 = 00000010 = 2 1 << 2 = 00000100 = 4 1 << 3 = 00001000 = 8
この「1を指定した位置まで移動する」という操作が、後ほど説明する特定ビットの操作で非常に重要になります。
右シフト
00001000 >> 1
↓
00000100
符号なし整数では、右へ1ビットシフトすると値は2で割った結果に対応します。ただし、シフトによる乗除算として考える場合は型の幅、符号、あふれなどに注意が必要です。
1 << 3 は何を意味している?
組み込みのコードでは、次のような記述をよく見かけます。
1 << 3
これは「1を左へ3ビット移動する」という意味です。
00000001 ↓ 3ビット左へ 00001000
つまり、ビット3だけが1になった値を作っています。
このような値を、特定ビットを操作するためのビットマスクとして利用できます。
特定のビットを1にする
特定のビットだけを1にしたい場合は、OR演算とシフト演算を組み合わせます。
value |= (1 << 3);
たとえば元の値が次の状態だったとします。
value 00000000 1 << 3 00001000 OR -------- 00001000
これで、ほかのビットを維持したままビット3だけを1にできます。
特定のビットを0にする
特定のビットだけを0にするときは、ANDとNOTを組み合わせます。
value &= ~(1 << 3);
まず1 << 3でビット3だけが1の値を作ります。
00001000
NOTで反転すると、ビット3だけが0になります。
11110111
これと元の値をAND演算することで、ほかのビットを維持したままビット3だけを0にできます。
特定のビットを反転する
特定のビットを0→1、または1→0へ反転したい場合はXORを使えます。
value ^= (1 << 3);
ビット3が0なら1へ、1なら0へ切り替わります。
特定のビットが1か確認する
特定のビットの状態を確認するときにはAND演算を利用できます。
if (value & (1 << 3)) {
// ビット3が1
}
valueとビット3だけが1のマスクをAND演算し、結果が0以外ならビット3が1だと判断できます。
ビットマスクとは?
必要なビットだけを取り出したり変更したりするために使う値を、ビットマスク(bit mask)と呼びます。
たとえばビット3を操作するマスクは次のように作れます。
const uint8_t BIT3_MASK = (1U << 3);
これを使えば、コードの意味も分かりやすくなります。
value |= BIT3_MASK; // ビット3を1
value &= ~BIT3_MASK; // ビット3を0
value ^= BIT3_MASK; // ビット3を反転
なぜ組み込み開発でビット演算を使うの?
組み込み開発でビット演算が重要なのは、マイコンの設定情報が1つのレジスタの各ビットに割り当てられていることが多いためです。
たとえば架空の8ビットレジスタを考えてみます。
bit 7 6 5 4 3 2 1 0
┌──┬──┬──┬──┬──┬──┬──┬──┐
│ │ │ │ │EN│ │ │ │
└──┴──┴──┴──┴──┴──┴──┴──┘
もしビット3が「機能を有効にするENビット」だった場合、その機能だけを有効にするには次のような操作ができます。
REGISTER |= (1U << 3);
ほかの設定ビットを変更せず、必要なビットだけを1にできるのがポイントです。
GPIOや周辺回路の設定にも使われる
ビット演算は、次のようなマイコン内部の設定でよく登場します。
- GPIOの設定
- 割り込みの有効・無効
- タイマーの設定
- UART・SPI・I2Cなどの周辺回路設定
- ステータスフラグの確認
- エラーフラグの管理
- 複数の設定値を1つの変数へ格納する処理
ArduinoのdigitalWrite()などを使っているとビット演算を直接書かない場合もありますが、その下ではレジスタやビット単位の制御が行われています。
KUMITATE-C3でビット演算を試してみる
KUMITATE-C3でも、Arduino環境を使ってビット演算そのものを簡単に確認できます。
次のプログラムでは、8ビット変数の特定ビットを操作し、結果をシリアルモニターへ2進数で表示します。
uint8_t value = 0;
void setup() {
Serial.begin(115200);
value |= (1U << 3);
Serial.print("bit3 ON : ");
Serial.println(value, BIN);
value |= (1U << 0);
Serial.print("bit0 ON : ");
Serial.println(value, BIN);
value &= ~(1U << 3);
Serial.print("bit3 OFF: ");
Serial.println(value, BIN);
value ^= (1U << 0);
Serial.print("bit0 XOR: ");
Serial.println(value, BIN);
}
void loop() {
}
変化を8ビット表記で考えると、次のようになります。
初期値 00000000 bit3 ON 00001000 bit0 ON 00001001 bit3 OFF 00000001 bit0 XOR 00000000
このように1ビットずつ変化を追ってみると、ビット演算の動作を理解しやすくなります。
論理演算子とビット演算子を混同しない
C/C++では、ビット演算子と似た記号の論理演算子があります。
| 種類 | AND | OR | NOT |
|---|---|---|---|
| ビット演算 | & | | | ~ |
| 論理演算 | && | || | ! |
&&や||は条件式などの真偽値を扱うために使い、&や|は整数の各ビットに対して演算します。
見た目が似ていますが役割は異なるので注意しましょう。
16進数も一緒に覚えると便利
組み込み開発では、2進数だけでなく16進数も頻繁に使用します。
16進数1桁は4ビットに対応するため、長いビット列を短く表現できます。
2進数 1111 0000 16進数 0xF0
データシートやレジスタの説明では16進数が頻繁に登場するため、ビット演算と一緒に2進数・16進数の対応を覚えておくと便利です。
まとめ
ビット演算は、整数を構成する0と1をビット単位で操作する方法です。
- ANDは両方のビットが1の場合に1になる
- ORはどちらかのビットが1なら1になる
- XORは2つのビットが異なる場合に1になる
- NOTは0と1を反転する
- <<と>>でビットを左右へシフトできる
1U << nで特定位置だけが1のマスクを作れる- ORで特定ビットを1にできる
- ANDとNOTで特定ビットを0にできる
- XORで特定ビットを反転できる
- ANDで特定ビットの状態を確認できる
ビット演算を理解すると、マイコンのデータシートに登場するレジスタ設定が一気に読みやすくなります。
次は、今回学んだビット演算が実際に使われる「レジスタ」について学んでいきましょう。

