この記事は、C言語ドリルの8本目(最終回)としてビット演算を練習する記事です。マイコンのピンを1本だけ操作する、レジスタの特定のフラグだけを立てる。そんな「1ビット単位」の操作を身につけます。WebやアプリのC学習ではあまり出てこない、組込みらしさが一番濃いテーマです。

ビット演算の6つの演算子をビットの図解と7問の練習問題(基礎5問+発展2問)で手を動かして固めるドリルです。解答と解説は各問題の「+」を押すと開きます。まず自分で書いてみて、それから答え合わせをしてください。

<自己紹介>
筆者は現役の組込みエンジニアです。未経験向けのC言語練習問題を通じて最低限の言語知識を身につけ、最終的にはArduinoでマイコンプログラムを自作できるようになるのをゴールとして、このシリーズを作成しています。ビット演算は実務のレジスタ操作で毎日のように使います。

このテーマの要点

ビット演算でやることはこれだけ

  • マスク(&で「あるビットが立っているか」を調べる
  • |= で立てる・&= ~ で落とす・^= で反転する
  • シフト(<<でビットの位置を合わせる

マイコンでは、8本や16本のピン・設定を1つの数値のビット一つひとつで表します。だから「3番ピンだけ操作する」には、その数値の3番目のビットだけをいじる技術が必要です。それがビット演算です。

前提知識:ビットと6つの演算子

コンピューターの中で、数値は 0と1の並び(2進数)で表されています。たとえば 0b00000100 は「右から3番目(bit2)だけが1」という意味です(0b は2進数の目印)。ビット演算は、この0と1を直接操作します。演算子は6つです。

演算子 名前 働き 主な用途
& AND(論理積) 両方1なら1 マスク(特定ビットの取り出し)
| OR(論理和) どちらか1なら1 ビットを立てる
^ XOR(排他的論理和) 違えば1、同じなら0 ビットの反転(トグル)
~ NOT(ビット反転) 全ビットを反転 「落とす」マスク作り
<< 左シフト ビットを左にずらす 位置合わせ(1回で値2倍)
>> 右シフト ビットを右にずらす 位置戻し(1回で値半分)

ビットの動きを表で見る(AND・OR)

言葉より、ビットの並びで見るのが一番早いです。8ビットの値どうしを縦に並べて、同じ位置のビットを比較します。

AND(&):両方が1の位置だけ1

b7 b6 b5 b4 b3 b2 b1 b0
左辺 1 1 0 1 1 0 1 0
右辺 1 0 0 0 0 1 1 0
結果 1 0 0 0 0 0 1 0

OR(|):どちらかが1の位置は1

b7 b6 b5 b4 b3 b2 b1 b0
左辺 1 1 0 1 1 0 1 0
右辺 1 0 0 0 0 1 1 0
結果 1 1 0 1 1 1 1 0

XOR(^)は「2つのビットが違う位置だけ1」、NOT(~)は「全ビットをひっくり返す」です。シフト(<< >>)は並び全体を左右にずらし、ずらして空いた場所には0が入ります(これはC言語の仕様です)。

メモ:「1 << n」で n番目のビットを作る
1 << 3 は「1を左に3つずらす」=0b00001000、つまりbit3だけが1の値になります。この (1 << n) が、狙ったビットを操作するときの基本パーツです。マスクも、立てる・落とすも、すべてこれを使います。
ビット3をON、ビット0をOFFしたときのビット列の変化
図:ビット3をON、ビット0をOFFしたときの変化。色付きのビットだけが変わる
ターミナルに元の値00000101、ビット3をONで00001101、ビット0をOFFで00001100と表示された実行結果
筆者の環境で上の操作をそのまま実行した結果。図のとおりのビット列が表示される

より正確な仕様は、cppreference(C言語リファレンス)で確認できます。

例題(まずは一緒に解いてみる)

練習問題に入る前に、簡単な例題を1つ、答えを見ながら流れを確認しましょう。

例題:reg(最初は0)の0番目のビットを立てて(1にして)、結果を表示してみましょう。

example.c

#include <stdio.h>

int main(void) {
    unsigned char reg = 0;
    reg |= (1 << 0);   // bit0 を1にする
    printf("%d\n", reg);
    return 0;
}

実行結果:1

解説:(1 << 0) は「bit0だけが1」の値。それを reg |= ... で重ねると、bit0が立って 0b00000001 = 1になります。|= で狙ったビットを立てる、が基本の形です。

流れはつかめましたか? ここからは、自分の手で解いてみましょう。

練習問題(基礎5+発展2)

基礎5問は必ず解いてください。発展2問は余力があればで大丈夫です。まず自分で書いてから、各問題の「+ 解答・解説を見る」を開いて答え合わせをしましょう。

基礎 Q1

reg = 0b00000100 について、2番目のビット(bit2)が立っているかをマスク(&)で調べ、立っていれば bit2 ON と表示してください。

解答・解説を見る
q1.c

#include <stdio.h>

int main(void) {
    unsigned char reg = 0b00000100;   // bit2 だけ1
    if (reg & (1 << 2)) {           // bit2 を取り出す
        printf("bit2 ON\n");
    } else {
        printf("bit2 OFF\n");
    }
    return 0;
}

実行結果:bit2 ON

解説:(1 << 2) は「bit2だけが1」の値(マスク)です。これと reg& すると、bit2以外は0で消され、bit2の状態だけが残ります。結果が0でなければ「立っている」という判定です。マイコンで「あるフラグが立っているか」を調べる、最も基本の形です。

基礎 Q2

reg = 0 の状態から、1番目のビット(bit1)を立てて(1にして)、結果の数値を表示してください。|= を使います。

解答・解説を見る
q2.c

#include <stdio.h>

int main(void) {
    unsigned char reg = 0b00000000;
    reg |= (1 << 1);       // bit1 を1にする
    printf("%d\n", reg);
    return 0;
}

実行結果:2

解説:reg |= (1 << 1) は「bit1を立てる」という意味です。|(OR)は「どちらかが1なら1」なので、他のビットは変えずに、狙ったビットだけを1にできます。結果は 0b00000010 = 10進数で2。マイコンで「このピンをHIGHにする」ときにそのまま使う形です。

基礎 Q3

reg = 0b00000110(bit1とbit2が1)から、1番目のビット(bit1)だけを落として(0にして)、結果を表示してください。&= ~ を使います。

解答・解説を見る
q3.c

#include <stdio.h>

int main(void) {
    unsigned char reg = 0b00000110;   // bit1,bit2 が1(=6)
    reg &= ~(1 << 1);              // bit1 を0にする
    printf("%d\n", reg);
    return 0;
}

実行結果:4

解説:ビットを落とす定番が reg &= ~(1 << n) です。~(1 << 1) は「bit1だけが0で、他は全部1」というマスク。これと & すると、bit1だけが0にされ、他はそのまま残ります。結果は 0b00000100 = 4(bit2だけ残った)。「このピンをLOWにする」操作です。

基礎 Q4

1左に3ビットシフトした値、②5左に2ビットシフトした値(=5の何倍になる?)をそれぞれ表示してください。

解答・解説を見る
q4.c

#include <stdio.h>

int main(void) {
    printf("%u\n", 1u << 3);   // 8:bit3を作る
    printf("%u\n", 5u << 2);   // 20:5×2×2=4倍
    return 0;
}

実行結果:8 / 20

解説:1 << 30b00000001 を左に3つずらして 0b00001000 = 8。左シフトには「n番目のビットを作る」役割と、1回ずらすごとに値が2倍になる(2回で4倍)という計算の役割があります。逆に右シフト >> は1回で半分です。掛け算より高速なため、組込みでは「×4」を << 2 と書くコードによく出会います。

基礎 Q5

reg = 0b00000001(bit0が1)に対して、XOR(^=)でbit0を2回反転させ、1回目と2回目それぞれの値を表示してください(1→0→1と戻ることを確かめます)。

解答・解説を見る
q5.c

#include <stdio.h>

int main(void) {
    unsigned char reg = 0b00000001;
    reg ^= (1 << 0);            // bit0を反転:1→0
    printf("%d\n", reg);
    reg ^= (1 << 0);            // もう一度反転:0→1
    printf("%d\n", reg);
    return 0;
}

実行結果:0 / 1

解説:XOR(^)は「違えば1、同じなら0」なので、1とXORすると必ずビットが反転します(トグル)。同じ操作を2回すると元に戻るのが特徴です。「呼ばれるたびにLEDの点灯と消灯を切り替える」ような処理は、この reg ^= (1 << n) 1行で書けます。if文で今の状態を調べる必要がないのがミソです。

発展 Q6

1バイトの値 0xA50b10100101)から、上位4ビット(0xA)と下位4ビット(0x5)をそれぞれ取り出して表示してください。ヒント:右シフトとマスク 0x0F を組み合わせます。

解答・解説を見る
q6.c

#include <stdio.h>

int main(void) {
    unsigned char value = 0xA5;            // 0b10100101
    unsigned char high = (value >> 4) & 0x0F;  // 上位4ビット
    unsigned char low  = value & 0x0F;         // 下位4ビット
    printf("high=0x%X low=0x%X\n", high, low);
    return 0;
}

実行結果:high=0xA low=0x5

解説:シフトで位置を合わせて、マスクで欲しい部分だけ残す」。この組み合わせがビット演算の実戦の型です。上位4ビットは、右に4つずらして下位に持ってきてから 0x0F0b00001111)でマスクします。マイコンのレジスタや通信データは「この4ビットが設定A、この2ビットが設定B」のように詰め込まれていることが多く、この取り出しは実務で何度も書くことになります。

発展 Q7

ここまでの操作を関数マクロにまとめ、ポート(複数ピン)を直接操作してみましょう。BIT_SETBIT_CLEARBIT_READ を定義し、ポートに見立てた PORTB の3番と5番を立ててから、3番だけ落とし、結果と5番の状態を表示してください。

解答・解説を見る
q7.c

#include <stdio.h>

#define BIT_SET(x, n)   ((x) |=  (1u << (n)))
#define BIT_CLEAR(x, n) ((x) &= ~(1u << (n)))
#define BIT_READ(x, n)  (((x) >> (n)) & 1u)

int main(void) {
    unsigned char PORTB = 0;   // 出力ポートに見立てる
    BIT_SET(PORTB, 3);         // 3番ピンをHIGH
    BIT_SET(PORTB, 5);         // 5番ピンをHIGH
    BIT_CLEAR(PORTB, 3);       // 3番ピンをLOWに戻す
    printf("PORTB=%d\n", PORTB);
    printf("pin5=%u\n", BIT_READ(PORTB, 5));
    return 0;
}

実行結果:PORTB=32 / pin5=1

解説:よく使うビット操作は、関数マクロ(#defineにしておくと、毎回 |= (1 << n) と書かずに BIT_SET(PORTB, 3) と読みやすく書けます(#5 関数で触れた「短い処理はマクロ」がこれです)。結果は、5番だけ残って 0b00100000 = 32。実際のマイコンでは、この PORTB にあたるレジスタが本物の出力ポートにつながっていて、複数ピンを一気に操作できます。

この発展問題は余力がある人向けです。基礎5問ができていれば、8テーマは完走です。おつかれさまでした。

例:実務で実際に書くコード

マイコンの設定は「レジスタ」と呼ばれる数値のかたまりに詰まっていて、ビット1つずつに意味が割り当てられています。だからレジスタ操作のコードは、練習問題でやった形がそのまま並びます。

例:レジスタ操作の典型パターン

REG_CTRL |=  (1 << 3);        // bit3のフラグを立てる(機能ON)
REG_CTRL &= ~(1 << 3);        // bit3を落とす(機能OFF)

if (REG_STATUS & (1 << 5)) {  // bit5が立っていたら
    // 受信完了などのイベント処理
}
実務ではビット演算を毎日使う
私が車載の現場でやっているレジスタ操作は、まさにこのビット演算の集まりです。マスク(&)で「あるフラグが立っているか」を調べ、|= で特定ビットを立て、&= ~ で落とし、<< で値をビット位置に合わせる。この4つを組み合わせて、マイコンのピンやレジスタを1ビット単位で制御します。ArduinoでもポートB(PORTB)を直接叩けば、複数ピンを一気に動かせます。そして発展問題のように、これらは関数マクロにしておくと、毎回シフトを書かずに済んで読みやすくなります。ここが「他言語」と「組込みソフト」が一番はっきり分かれる場所です。
▶ ここに実機でのポート/レジスタ操作の写真と、シリアルモニタに出したレジスタ値を入れる予定です。

最後につまずきポイントをおさらい

「&」と比較を混ぜるときはカッコを付ける
if (reg & (1 << 2)) のように、ビット演算はカッコでくくるのが安全です。C言語では & より == の方が先に計算されるため、reg & 1 == 0 のように書くと、意図とは違う順序で計算されてバグります。ビット演算はカッコで囲むと覚えておくと安全です。

もう一つは 符号(signed)の問題です。ビットを扱う変数は、unsignedunsigned char / unsigned int など)にするのが基本です。符号付きのままシフトや反転をすると、思わぬ値になることがあります。#1でやった「型選び」がここにつながります。

よくある質問(FAQ)

なぜビット演算が組込みで重要なの?

マイコンの設定は、レジスタという数値のビット一つひとつに意味が割り当てられているからです。「このビットが1なら割り込み許可」のように決まっているので、狙ったビットだけを操作するビット演算が欠かせません。

論理演算子(&&)とビット演算子(&)は何が違う?

&& は「式全体が真か偽か」を判定する論理演算子、& は「ビット1つずつ」を計算するビット演算子です。見た目が似ているぶん書き間違いやすく、if (a & b)if (a && b) は結果が変わることがあります。条件判定は &&、ビット操作は & と使い分けてください。

2進数(0b)と16進数(0x)はどっちで書く?

ビットの並びを見せたいときは 0b(2進)、桁が多いレジスタ値は 0x(16進)が読みやすいです。どちらも同じ数値の別の書き方なので、場面で使い分けます。
ただし16進数のほうが圧倒的に見やすいので基本は16進数で書いておけばいいかと思いますね

次に読む

これで必須8テーマは完走です。おつかれさまでした。仕上げに、8テーマ全部を1本のプログラムで組み合わせる総合実力テストに挑戦してみてください。

総仕上げ(9本目)C言語 総合実力テスト|要件定義書から作るエアコン模擬プログラムで8テーマを一本化する
目次へ戻る(8テーマ完走)C言語 基礎ドリル|組込みに必要な8テーマの練習問題まとめ
前のドリル(#7)C言語 構造体の練習問題7問|メンバ・typedefまで固める
あわせて読みたい組込みでC言語が使われる理由