はじめに
マイコンのレジスタ制御では、レジスタ全体を一度に書き換えるのではなく、特定のビットだけを操作したい場面が頻繁にあります。たとえばポートの出力ピンを1本だけONにしたいとき、他のピンの状態はそのまま維持しなければなりません。
そのために使うのがビット単位の演算です。組み込みCの現場では決まったパターンが繰り返し登場するので、イディオムとして身につけておくと読み書きがぐっと楽になります。
基本のビット演算 ― OR・AND・NOTの使い方
Cで使えるビット演算子は主に以下の3つです。
| 演算子 | 名称 | 用途 |
|---|---|---|
| |
ビット毎のOR | 特定ビットを1にする |
& |
ビット毎のAND | 特定ビットを0にする |
~ |
ビット毎のNOT | ビットを反転する |
これらを組み合わせることで、他のビットに影響を与えずに特定のビットだけを操作できます。
定番イディオム ― ビットのセット・クリア・トグル・確認
実際のコードでは次の4パターンがほぼすべてをカバーします。
ビットをセットする(1にする)
#define H8P47DDR 0x80 /* 7ビット目が1(16進表記) */
p4 |= H8P47DDR;
ビット毎のORをとることで、他のビットを変えずに対象ビットだけを1にできます。
ビットをクリアする(0にする)
p4 &= ~H8P47DDR;
マスク値を反転(NOT)してからANDをとることで、他のビットを変えずに対象ビットだけを0にできます。~H8P47DDR は 0x80(0b10000000)を反転した 0x7F(0b01111111)になります。
ビットをトグルする(反転する)
p4 ^= H8P47DDR;
XOR(^)を使うと、対象ビットだけを反転できます。LEDの点滅制御などでよく使います。
ビットの状態を確認する
if (p4 & H8P47DDR) {
/* 7ビット目が1のとき */
}
ANDをとって結果が0でなければ、対象ビットが1であることがわかります。
マクロで抽象化する ― 可読性と再利用性を上げる
上記のイディオムを毎回書くのは冗長なので、マクロでラップするとコードが読みやすくなります。
#define BIT_SET(reg, mask) ((reg) |= (mask))
#define BIT_CLR(reg, mask) ((reg) &= ~(mask))
#define BIT_TGL(reg, mask) ((reg) ^= (mask))
#define BIT_CHK(reg, mask) ((reg) & (mask))
使う側はこう書けます。
BIT_SET(p4, H8P47DDR); /* 7ビット目をセット */
BIT_CLR(p4, H8P47DDR); /* 7ビット目をクリア */
BIT_TGL(p4, H8P47DDR); /* 7ビット目をトグル */
if (BIT_CHK(p4, H8P47DDR)) {
/* 7ビット目が1のとき */
}
操作の意図が名前から読み取れるので、コードレビューでも見落としが減ります。
レジスタアクセスを伴う場合は、前回までに扱った volatile キャストと組み合わせます。
UB p4 = sil_reb_mem(H8P4DR); /* レジスタ読み込み(sil_reb_memはvolatileアクセスのラップ関数。前回記事参照) */
BIT_SET(p4, H8P47DDR); /* ビット操作 */
sil_wrb_mem(H8P4DR, p4); /* レジスタ書き込み */
読み込み・操作・書き込みの3ステップがはっきり分かれているので、処理の流れが追いやすくなります。
実務での注意点・よくある落とし穴
読み・変更・書き込みの間に割り込みが入る
レジスタの読み込みから書き込みまでの間に割り込みが発生し、別の処理が同じレジスタを書き換えてしまうと、意図しない値が書き込まれることがあります(Read-Modify-Writeハザード)。クリティカルな箇所では割り込み禁止区間を設けるなどの対策が必要です。
符号付き整数でビット演算をするとシフトが未定義動作になる
ビット操作は符号なし整数(UB=unsigned char・UH=unsigned short・UW=unsigned long など)で行うのが原則です。符号付き整数に対する右シフトはCの仕様上、処理系依存の動作になります。
マスク値のビット幅に注意する
UB(8ビット)のレジスタに対して16ビットや32ビットのマスク値を使うと、意図しないビットに触れる可能性があります。マスク値はアクセス対象のレジスタ幅に合わせて定義してください。
ビット番号とマスク値を混同しない
「7ビット目」を指定したいとき、マスク値は 7 ではなく 0x80(1 << 7)です。ビット番号をそのまま使うミスは意外と多いので注意してください。
/* 悪い例 */
#define BIT7 7 /* これはビット番号であってマスク値ではない */
/* 良い例 */
#define BIT7 0x80 /* または (1 << 7) */
まとめ
ビット操作は組み込みCの中でも特に頻出のテクニックです。
- ビットセットはOR(
|=)、クリアはNOTとAND(&= ~)、トグルはXOR(^=)、確認はAND(&) - 定番イディオムをマクロでラップすると可読性が上がる
- レジスタアクセスは読み込み・操作・書き込みの3ステップで書く
- ビット操作は必ず符号なし整数で行う
次回は関数ポインタと関数ベクタを取り上げます。RTOSのタスク登録まで踏み込んで、実務でどう使われているかを見ていきます。