Skip to content
Published on

プルアップ、プルダウン、そしてフローティングピン — ボタンひとつがこんなに難しい理由

シェア
Authors

はじめに — 何もつないでいないピンがずっと値を変え続けます

Arduinoでd2を入力に設定し、何もつながずに1秒間に10回値を読んでシリアルに出力してみます。

1 1 1 0 1 1 0 0 0 1 0 1 1 1 0 1 0 0 1 1

値がずっと変わります。手をピンに近づけるとパターンが変わり、蛍光灯を点けるとまた変わります。コードには何の問題もありません。

ソフトウェアの視点からするとこの状況は理解しにくいです。読まれていない変数は初期値を持つか、少なくとも安定したゴミ値を持ちます。ところがこのピンは読むたびに違う値を返します。

答えは簡単です。そのピンの電圧が定まっていないからです。デジタル入力は電圧を読んで閾値と比較する回路であり、比較すべき電圧そのものがなければ結果もありません。そしてボタンをつなぐ瞬間、この問題は回路設計の中心に浮かび上がってきます。

フローティング入力の正体 — ピンは小さなアンテナです

デジタル入力ピンの内部は非常にインピーダンスの高いコンパレータです。どれほど高いかというと、データシートに書かれている入力漏れ電流は最大1マイクロアンペア程度です。3.3Vでこの値を抵抗に換算すると、

3.3V / 0.000001A = 3300000オーム = 3.3メガオーム

3メガオームを超えます。つまりこのピンは事実上、ほとんど電流を流さない傍観者です。

問題はここから始まります。電流がほとんど流れないノードの電圧は、ごく小さな電荷でも大きく揺れます。ピンや配線、ブレッドボードが作る静電容量はおよそ20から30ピコファラドです。電圧と電荷の関係は、

電圧変化 = 電荷 / 静電容量

25ピコファラドに電荷25ピココロンが乗っただけで電圧が1V変わります。25ピココロンは指を近づけるだけで生じる静電結合だけで十分に作られる量です。

だから、どこにもつながっていないピンはアンテナになります。 60Hz電源線の誘導、近くの配線の信号遷移、蛍光灯の安定器、スイッチングアダプターのノイズ、すべてがこのピンの電圧を揺らします。そしてその電圧が閾値付近を行き来すると、読むたびに違う値が出ます。

閾値が正確にどこにあるかも押さえておく必要があります。CMOS入力には2つの境界があります。

ボードロジック電圧HIGHと認められる最小電圧LOWと認められる最大電圧不確定区間
Arduino Uno(ATmega328P)5V3.0V1.5V1.5〜3.0V
ラズベリーパイ(BCM)3.3V2.31V0.99V0.99〜2.31V
ESP323.3V2.475V0.825V0.825〜2.475V

真ん中の区間が問題です。ピン電圧が1.5Vから3.0Vの間にあると、Arduinoはその値をHIGHと見るかLOWと見るか決めません。データシートが保証しない領域です。フローティングピンはほとんどの時間をこの区間で過ごします。

そしてこの状態にはもうひとつ目に見えないコストがあります。入力ピン内部の最初の段は相補型トランジスタの対で、入力が中間電圧にとどまると上下のトランジスタが同時に少しずつオンになります。電源からGNDへ貫通電流が流れます。ピン1本あたり数百マイクロアンペア程度なので目立ちませんが、バッテリー駆動の機器で使わないピンを全部プルアップにまとめておく理由がこれです。

プルアップとプルダウン抵抗がしていること

解決策は明確です。何も起きていないときにピンが何ボルトであるべきかを決めてあげればよいのです。その仕事をする部品がプルアップまたはプルダウン抵抗です。

プルアップはピンと電源の間に抵抗を入れることです。何もピンに触れていなければ、電源から抵抗を通ってピンへ電流が流れ、ピンが電源電圧まで上がります。正確には、ピンの漏れ電流がほぼ0なので抵抗両端の電圧もほぼ0になり、ピン電圧は事実上電源と同じになります。10キロオームのプルアップに1マイクロアンペアの漏れがあるとすると、抵抗で落ちる電圧は、

0.000001A × 10000オーム = 0.00001V = 0.01mV

無視できる値です。だからピンは安定してHIGHを読みます。

プルダウンは逆に、ピンとGNDの間に抵抗を入れます。デフォルト値がLOWになります。

ここでなぜ抵抗を使うのか、なぜただの電線で電源に直接つながないのかが重要な問いになります。電線で直接つなぐと、そのピンは永遠にHIGHで、ボタンは何の影響も与えられません。いや、もっと悪いです。ボタンを押してそのピンをGNDにつないだ瞬間、電源とGNDが抵抗なしで直結されます。ショートです。

抵抗がしていることは「弱く引っ張る」ことです。誰も逆らわなければ勝ち、誰かが強く逆らえば負けます。10キロオームのプルアップが付いたピンをボタンがGNDにつなぐと、ボタンの抵抗はほぼ0なのでピンはGND側に引かれます。このとき流れる電流は、

3.3V / 10000オーム = 0.00033A = 0.33mA

0.33mAだけ電源からGNDへ流れます。この電流がプルアップのコストであり、抵抗値を決めるときに計算するのがまさにこの値です。

プルアップとプルダウンは論理的には対称ですが、実務ではプルアップが圧倒的によく使われます。理由は次の2節にわたって出てきます。

内部プルアップはなぜあり、値はいくつか

ほとんどすべてのマイコンはチップの中にプルアップ抵抗を内蔵していて、ソフトウェアでオンオフできます。部品をひとつ節約できるので便利です。

値を知っておくことが重要です。内部プルアップは精密抵抗ではなく半導体プロセスで作られた抵抗なので、ばらつきが大きいです。

ボード/チップ内部プルアップ抵抗内部プルダウン有効化方法
Arduino Uno(ATmega328P)20〜50キロオームなしpinMode(pin, INPUT_PULLUP)
ラズベリーパイ(BCM2835系)約50〜65キロオーム約50〜65キロオームgpiozeroのpull_up引数
ESP32約45キロオーム約45キロオームpinMode(pin, INPUT_PULLUP)
ラズベリーパイGPIO2/GPIO31.8キロオーム(基板に固定実装)使用不可除去不可

3つ確認しておきます。

第一に、ArduinoのATmega328Pには内部プルダウンがありません。プルダウンが必要なら必ず外部抵抗を付ける必要があります。この非対称性がアクティブロー慣例の技術的なルーツのひとつです。

第二に、値のばらつきが大きいです。20から50キロオームなら2.5倍の差です。正確な電流や時定数が必要な回路では内部プルアップに頼れません。

第三に、最後の行が実務でよく事故を起こします。ラズベリーパイのGPIO2とGPIO3には基板上に1.8キロオームのプルアップが物理的に付いています。I2Cバスのためのもので、除去できません。このピンを普通の入力として使うと常に強くHIGHに引かれているので、プルダウンを付けても勝てません。たとえば10キロオームのプルダウンを付けると分圧になって、

ピン電圧 = 3.3V × 10000 / (1800 + 10000) = 3.3V × 0.847 = 2.80V

2.80Vです。LOWと認められる上限0.99Vとはほど遠いです。それでもHIGHとして読まれます。GPIO2とGPIO3はI2C専用として残しておくのが正しいです。

アクティブローが慣例になった理由

ボタンの配線には2つの方式があります。

アクティブハイはプルダウン抵抗を使い、ボタンがピンと電源の間にあります。押すとHIGHになります。コードではdigitalRead(pin) == HIGHが「押された」になるので直感的です。

アクティブローはプルアップ抵抗を使い、ボタンがピンとGNDの間にあります。押すとLOWになります。コードではdigitalRead(pin) == LOWが「押された」になるので最初は逆に感じます。

ところが実際の回路図やモジュールはほとんど全部アクティブローです。理由は何層にも重なっています。

第一に、先ほど見たように内部プルアップは一般的で、内部プルダウンはないか稀です。アクティブローを使えば外部抵抗がまったく必要ありません。

第二に、配線が安全です。ボタンの片方の脚はGNDです。ブレッドボード上でGNDレールは長く伸びていてどこからでもつかみやすく、誤って他の場所に触れてもGNDなので事故になりません。逆に電源線があちこちに伸びていると、誤ってGNDに触れた瞬間にショートです。

第三に、これは歴史的に決定的だったのですが、トランジスタは電流を吸い込む能力のほうが押し出す能力より強かったのです。初期のTTLロジックではLOWへ引き下げる能力が16mAだったのに対し、HIGHへ押し上げる能力は0.4mAでした。40倍の差です。だから意味のある信号をLOWに割り当てるのが自然でした。最近のCMOSは両方向がほぼ対称ですが、慣例は残りました。

第四に、故障モードが安全です。配線が切れたらどうなるか考えてみてください。アクティブローで線が抜けると、プルアップが勝ってHIGH、つまり「押されていない」になります。アクティブハイで線が抜けるとフローティングになり、無作為に押されたように読まれることがあります。切れたときに安全な側に落ちる設計が良い設計です。

だからArduinoでボタンひとつを付ける正解はこれだけ短くなります。

const int BUTTON_PIN = 2;

void setup() {
  Serial.begin(115200);
  // 内部プルアップをオンにします。外部抵抗は不要です。
  // ボタンはD2とGNDの間につなぐだけでよいです。
  pinMode(BUTTON_PIN, INPUT_PULLUP);
}

void loop() {
  // プルアップなので普段はHIGH、押されるとLOWです。
  bool pressed = (digitalRead(BUTTON_PIN) == LOW);
  Serial.println(pressed ? "pressed" : "released");
  delay(100);
}

ラズベリーパイでgpiozeroを使うと、この慣例がそもそもデフォルト値として入っています。

from gpiozero import Button
from signal import pause

# pull_up=Trueがデフォルト値です。ボタンはGPIO17とGNDの間につなぎます。
# ライブラリが内部プルアップをオンにし、is_pressedはLOWのときTrueになります。
# つまりアクティブローの反転をライブラリが代わりに処理してくれます。
button = Button(17, pull_up=True, bounce_time=0.05)

button.when_pressed = lambda: print("pressed")
button.when_released = lambda: print("released")

pause()

bounce_timeは後で扱うデバウンスをライブラリのレベルで処理するオプションです。

外部抵抗値を選ぶ — ノイズ耐性と消費電流のトレードオフ

内部プルアップでは足りない場合があります。配線が長い、ノイズが多い環境、値が正確でなければならない、複数の機器がひとつの線を共有する場合です。このときは外部抵抗値を自分で選ぶ必要があります。

この選択は2つの要求が正面から衝突します。

抵抗が小さいほど良い理由は、ノイズに強くなるからです。ノイズがピン電圧を揺らすには、ピンの静電容量を充電しなければならず、プルアップ抵抗が小さいとその充電を素早く元に戻します。時定数で計算するとこうなります。ピンと配線の静電容量を25ピコファラドとすると、

1キロオーム  → 1000 × 0.000000000025 = 25ナノ秒
10キロオーム → 250ナノ秒
100キロオーム → 2.5マイクロ秒

100キロオームのプルアップが付いたピンは、ノイズで揺れた電圧を元に戻すのに2.5マイクロ秒かかります。その間に値が誤って読まれることがあります。

抵抗が大きいほど良い理由は、電流を少なく使うからです。ボタンを押している間、プルアップ抵抗を通して電源からGNDへ電流が流れ続けます。

1キロオーム   → 3.3V / 1000 = 3.3mA
10キロオーム  → 0.33mA
100キロオーム → 0.033mA = 33マイクロアンペア

ボタンひとつなら3.3mAも大きな問題ではありません。しかしスイッチが16個並んでいて半分が押されていれば26mAになり、バッテリー機器ではこの値が寿命を左右します。

まとめるとこのような基準になります。

抵抗値押したときの消費電流(3.3V)ノイズ復元時間適した状況
1キロオーム3.3mA25ナノ秒長い配線、モーター付近、産業環境
4.7キロオーム0.70mA118ナノ秒I2Cバスの標準値
10キロオーム0.33mA250ナノ秒一般的な基本選択
47キロオーム0.07mA1.2マイクロ秒低電力機器、短い配線
100キロオーム以上0.03mA2.5マイクロ秒推奨しない

特別な理由がなければ10キロオームが良い出発点です。そして「ボタンがときどき勝手に押される」という症状が出たら、値を下げるのが最初の対応です。配線が30センチメートルを超えたら4.7キロオームか1キロオームに下げるほうが安全です。

ボタンやスイッチを回路に入れてプル抵抗を忘れていないか確認したければ、配線検証ツールに部品を載せてみてください。プッシュボタンのようにそのままだとフローティングになる部品にはプルアップかプルダウンが必要だという案内が別に表示され、ラズベリーパイのGPIO2やGPIO3のように基板に固定プルアップが実装されたピンを選んだときも警告が出ます。

オープンドレイン出力

プルアップを理解すると、オープンドレインという出力方式が自然に続いてきます。実務でI2Cを扱ったり割り込み線を共有したりするとき必ず出会います。

普通のデジタル出力はプッシュプルです。HIGHを書くと上側のトランジスタがオンになりピンを電源につなぎ、LOWを書くと下側のトランジスタがオンになりGNDにつなぎます。両方向とも能動的に押します。

ここには制約があります。2つの機器が同じ線にプッシュプルでつながっていて、一方がHIGHを、もう一方がLOWを書くと、電源とGNDが2つのトランジスタを通して直結されます。5Vでトランジスタ2個のオン抵抗がそれぞれ25オームだとすると、

5V / (25 + 25) = 0.1A = 100mA

100mAが2つのチップを貫通します。どちらか、または両方が損傷します。

オープンドレインは上側のトランジスタを完全になくした方式です。出力はLOWに引き下げるか、何もせず手を離すかのどちらかです。HIGHを作る仕事は外部プルアップ抵抗が担当します。

こうすると複数の機器が一本の線を安全に共有できます。誰も引き下げなければプルアップが勝ってHIGH、ひとつでも引き下げればLOWです。論理的にはAND回路が配線だけで作られます。この性質のおかげでオープンドレインバスでは「ひとりでもLOWを主張すればLOW」という規則が成り立ち、I2Cのスレーブが準備できるまでクロックをつかんでおくクロックストレッチングのような技法が可能になります。

代償は速度です。LOWへ行くのはトランジスタが素早く引き下げますが、HIGHへ行くのはプルアップ抵抗が静電容量を充電する速さに左右されます。4.7キロオームに100ピコファラドなら、

時定数 = 4700 × 0.0000000001 = 0.00000047秒 = 470ナノ秒

立ち上がり時間は時定数の2倍あまりなので約1マイクロ秒です。I2C標準モードの最大立ち上がり時間規格は1マイクロ秒で、ちょうどその境界にあります。機器を増やして静電容量が増えると規格を超えます。これがI2Cバスに機器を無制限に付けられない物理的な理由です。

Arduinoでオープンドレイン出力をまねる方法も知っておくとよいです。ピンモードを切り替えることです。

const int SHARED_LINE = 4;

void setup() {
  // 手を離した状態です。外部プルアップがHIGHに上げてくれます。
  pinMode(SHARED_LINE, INPUT);
}

void assertLow() {
  // 能動的に引き下げます。
  pinMode(SHARED_LINE, OUTPUT);
  digitalWrite(SHARED_LINE, LOW);
}

void release() {
  // 再び高インピーダンスに戻ります。HIGHを使わないことが要点です。
  pinMode(SHARED_LINE, INPUT);
}

digitalWrite(pin, HIGH)を絶対に使わないことが要点です。HIGHを作る仕事はプルアップに任せ、こちら側は引き下げるか手を離すかの2状態だけを持ちます。

スイッチバウンスとデバウンス

配線を正しくしてプルアップまでオンにしたのに、ボタンを一度押しただけでカウンターが3増えます。これが最後の問題です。

スコープに実際に映る波形

機械式スイッチの接点は金属片です。押すと2つの金属がぶつかりますが、金属には弾性があるので跳ねます。くっついてはごく短く離れ、また付くことを数回繰り返してから安定します。オシロスコープでタクトスイッチを見ると、おおよそこんな波形が出ます。

                                         ┌──────────────────
      ─────┐   ┌──┐  ┌─┐ ┌┐              │
           │   │  │  │ │ ││              │
           └───┘  └──┘ └─┘└──────────────┘
           |<------ 約1〜5ms ------>|
           押した瞬間              安定した時点

接点の種類ごとにおおよそこの程度です。

スイッチの種類典型的なバウンス時間最悪の場合
小型タクトスイッチ1〜5ms10ms
トグルスイッチ5〜10ms20ms
マイクロスイッチ(リミット)1〜3ms8ms
リレー接点5〜20ms50ms
ロータリーエンコーダ1〜5ms(両チャンネル)10ms

これでカウンターが3増える理由が明確になりました。Arduinoのloopは単純なコードならマイクロ秒単位で回るので、5ミリ秒間跳ねる接点を数百回観察します。その間のすべての遷移が本物の入力に見えます。

ここで重要なのは、これが欠陥ではなく機械式接点の正常動作だということです。もっと良いスイッチを買っても消えません。回路かコードで処理する必要があります。

RC回路で波形を潰す

もっとも直感的なハードウェアの解決策は、抵抗とコンデンサで低域通過フィルタを作ることです。ピンとGNDの間にコンデンサを付けると、接点が跳ねる短い時間の間、コンデンサが電圧変化を吸収します。

10キロオームのプルアップに1マイクロファラドのコンデンサを付けたとします。

時定数 = 10000オーム × 0.000001F = 0.01秒 = 10ms

ボタンを離した後、ピン電圧がHIGH閾値の2.31Vまで上がるのにかかる時間は時定数の約1.2倍です。

10ms × 1.2 = 12ms

バウンスが5ミリ秒以内に終わるので、その間の跳ねはすべてコンデンサに吸収されてピン電圧が閾値を超えません。バウンスが消えます。

ここに落とし穴がひとつあります。RCフィルタは信号を遅くします。つまりピン電圧は先ほど見た不確定区間を12ミリ秒かけてゆっくり通過します。その区間を通っている間、ごくわずかなノイズだけでも入力コンパレータが何度も反転することがあります。バウンスをなくそうとして、別の種類の多重トリガーを作ってしまうわけです。

だからRCデバウンスはシュミットトリガー入力と組み合わせて初めてきちんと動作します。シュミットトリガーは上がるときと下がるときの閾値が異なる入力回路で、中間で震えません。74HC14が代表的な部品です。幸いESP32のGPIO入力はシュミットトリガーで、ATmega328Pの入力もある程度のヒステリシスを持ちます。それでも確実にしたいなら専用部品を使うほうがよいです。

もうひとつ実務のコツがあります。コンデンサがピンとGNDの間にあり、ボタンもピンとGNDの間にあるので、ボタンを押した瞬間、充電されていたコンデンサがボタンの接点を通して直接放電します。1マイクロファラドが瞬間的に放電すると接点に大きな電流が流れて接点が摩耗します。だからボタンとコンデンサの間に100オームから220オーム程度の抵抗をもうひとつ入れるのが定石です。

ソフトウェアデバウンス — そしてdelayが間違っている理由

部品を追加せずコードで処理するほうがはるかに一般的です。原理は単純です。値が変わった後、一定時間安定して維持されたときだけ本物の変化と認めます。

まず広く出回っていますが間違ったコードを見てみます。

// このように書いてはいけません。
void loop() {
  if (digitalRead(BUTTON_PIN) == LOW) {
    counter++;
    delay(50);   // バウンスが終わるまで待つという意図
  }
}

このコードが問題である理由は3つあります。

第一に、delayはCPUを完全に止めます。50ミリ秒の間、他のボタンも、センサー読み取りも、シリアル受信も、モーター制御もすべて止まります。ボタンが2個になった瞬間、この方式は崩れます。

第二に、離すときのバウンスを処理できません。上のコードは押すときだけ50ミリ秒待ちます。ボタンを離す瞬間も接点は同じように跳ねますが、そのときは何の保護もありません。

第三に、ボタンを押し続けていると50ミリ秒ごとにカウンターが上がり続けます。状態の変化ではなく状態そのものを見ているからです。

正しい方式は非ブロッキング構造です。最後に値が変わった時刻を記録しておき、それから十分な時間が経ったときだけ確定します。

const int BUTTON_PIN = 2;
const unsigned long DEBOUNCE_MS = 50;

// ピンから最後に読んだ生の値です。跳ねる値がそのまま入ってきます。
int lastRawReading = HIGH;
// デバウンスを通過して確定した値です。
int stableState = HIGH;
// 生の値が最後に変わった時刻です。
unsigned long lastChangeMs = 0;

unsigned long pressCount = 0;

void setup() {
  Serial.begin(115200);
  pinMode(BUTTON_PIN, INPUT_PULLUP);
}

void loop() {
  int raw = digitalRead(BUTTON_PIN);

  // 生の値が揺れるたびにタイマーを最初からやり直します。
  // バウンスが続いている間は下の確定ブロックに絶対に到達しません。
  if (raw != lastRawReading) {
    lastChangeMs = millis();
    lastRawReading = raw;
  }

  // 最後の変化からDEBOUNCE_MSの間静かだったなら本物の値です。
  if (millis() - lastChangeMs >= DEBOUNCE_MS) {
    if (raw != stableState) {
      stableState = raw;

      // ここでは状態そのものではなく遷移を見ます。
      // プルアップなのでHIGHからLOWに行くのが押されたことです。
      if (stableState == LOW) {
        pressCount++;
        Serial.print("press #");
        Serial.println(pressCount);
      }
    }
  }

  // loopは自由に回り続けます。他の処理をここに入れても構いません。
}

このコードが先ほどの間違ったコードとどう違うか見ていきます。

millis() - lastChangeMsという引き算はCPUを止めません。loopは回り続けながら他の仕事もでき、ボタンを10個付けてもそれぞれ別のタイマーを持たせれば同時に処理できます。

そして引き算で比較する点が重要です。millis()は約49.7日ごとに0に戻りますが、符号なし整数の引き算は桁あふれが起きても差分が正しく出ます。millis() > lastChangeMs + DEBOUNCE_MSのような形で書くと、この瞬間にバグが生まれます。

stableStaterawを比較する部分が状態遷移を捉えます。ボタンを押し続けてもstableStateがすでにLOWなのでカウンターは増えません。

50ミリ秒という値は、先ほどの表の中で最も長いバウンスより十分大きく、かつ人が知覚できないほど短い地点です。ロータリーエンコーダのような速い入力を扱うときはこの値がかえって入力を取りこぼす原因になるので5ミリ秒程度に減らし、他の技法と併用します。逆にリレー接点を読むなら100ミリ秒まで伸ばす必要があるかもしれません。この数字は覚える定数ではなく、接点の特性から出てくる値です。

締めくくり — 入力ピンのデフォルト値を決めることが回路の半分です

この記事の内容を一行に縮めるとこうなります。デジタル入力ピンは、何も起きていないときにどの値であるべきかを回路が決めてあげる必要があります。

プログラマーにとってこの発想はなじみ深いものです。初期化していない変数を読まないのと同じ規則だからです。ただしハードウェアでは初期化が代入文ではなく抵抗ひとつです。そして初期化していないときの結果が未定義の値ではなく、周囲の環境によって毎回変わる値になる点が違います。

ここにひとつ付け加えると完成します。そのデフォルト値が決まっていても、機械式接点は値が変わる瞬間から数ミリ秒の間、正直ではありません。その時間の間、判断を先延ばしにするコードがデバウンスであり、先延ばしにする方式がdelayではなく時刻の記録であって初めて残りのプログラムが動き続けます。

次回は入力ピンに入ってくる電圧そのものが危険な場合を扱います。5Vセンサーを3.3Vのラズベリーパイにつないだとき、ピンの内側で何が起きるかを見ていきます。