ESP32でリレーやMOSFETなどを制御していると、
「電源を入れた瞬間だけリレーがONする」
「ResetするとLEDが一瞬光る」
「プログラムではLOWにしているのに、起動時だけ負荷が動く」
といった現象が発生することがあります。
プログラムを見ると、最初にGPIOをLOWへ設定している。
それなのに、なぜ負荷が動くのでしょうか。
原因を調べるときに重要なのが、
ESP32は電源投入直後から自分のプログラムを実行しているわけではない
という点です。
今回はESP32-C5とESP-IDFを例に、起動時のGPIO状態と、外部回路で誤動作を防ぐ考え方を整理します。
app_main()より前にも時間がある
ESP-IDFでプログラムを作成すると、アプリケーションではapp_main()から処理を書き始めます。
そのため、
「app_main()の最初でGPIOをLOWにすれば、起動時からLOWになる」
と思いたくなります。
しかし実際には、
電源投入
↓
Reset
↓
ROM Boot処理
↓
Bootloader
↓
Application起動
↓
app_main()
という流れがあります。
つまり、
電源投入からapp_main()がGPIOを初期化するまでには時間があります。
その期間のGPIO状態を考えずに外部回路を設計すると、起動時だけ負荷が意図しない状態になる可能性があります。
GPIOを設定するまでは出力とは限らない
アプリケーションでGPIOをOutputに設定するまでは、
「必ずLOW」
「必ずHIGH」
とは限りません。
Pinによっては内部Pull-up/Pull-down、Boot時の機能、Strapping用途なども関係します。
そのため、
起動直後の状態をアプリケーションだけで保証しようとしない
ことが重要です。
特に、
- MOSFET Gate
- Transistor Base
- Relay Driver
- Motor Driver Enable
- Heater Enable
- その他、起動時にONしてほしくない信号
では外部回路側でもDefault状態を決めておく方が安全です。
MOSFET Gateが浮くとどうなる?
例えばN-channel MOSFETをLow Side Switchとして使用するとします。
ESP32 GPIO
↓
Gate抵抗
↓
MOSFET Gate
という接続だけにした場合、ESP32がまだGPIOを制御していない期間にGate電圧が不定になる可能性があります。
MOSFETのGateは容量性の入力なので、周囲のノイズや残留電荷などによって電位が決まってしまうことがあります。
その結果、
電源投入時にMOSFETが一瞬ONする
可能性があります。
Pull-down抵抗でOFF状態を作る
N-channel MOSFETを、
GPIO HIGH → ON
として使用するなら、GateとGNDの間にPull-down抵抗を入れる方法があります。
ESP32がまだGPIOを制御していなくても、
Gate
↓
Pull-down抵抗
↓
GND
によってGateをLOW側へ保持できます。
重要なのは、
Softwareが動く前からHardwareで安全状態を作る
という考え方です。
サンプルコード
今回はGPIO10を出力として使用し、起動直後にLOWへ設定したあと、1秒周期でHIGH/LOWを切り替えるサンプルを使用します。
ここで重要なのは、
このコードだけでは電源投入直後からGPIO10がLOWであることを保証できない
という点です。
以下が今回使用するサンプルコード全文です。
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/gpio.h"
#include "esp_log.h"
#define OUTPUT_GPIO GPIO_NUM_10
static const char *TAG = "BOOT_GPIO";
void app_main(void)
{
gpio_config_t io_conf = {
.pin_bit_mask = (1ULL << OUTPUT_GPIO),
.mode = GPIO_MODE_OUTPUT,
.pull_up_en = GPIO_PULLUP_DISABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE
};
ESP_ERROR_CHECK(gpio_config(&io_conf));
ESP_ERROR_CHECK(
gpio_set_level(OUTPUT_GPIO, 0));
ESP_LOGI(
TAG,
"GPIO initialized LOW");
while (1)
{
ESP_ERROR_CHECK(
gpio_set_level(OUTPUT_GPIO, 1));
ESP_LOGI(
TAG,
"GPIO HIGH");
vTaskDelay(
pdMS_TO_TICKS(1000));
ESP_ERROR_CHECK(
gpio_set_level(OUTPUT_GPIO, 0));
ESP_LOGI(
TAG,
"GPIO LOW");
vTaskDelay(
pdMS_TO_TICKS(1000));
}
}
このサンプルでは、GPIO10をOutputへ設定した直後にLOWを出力しています。
しかし、ここで確認したいのは、
LOWへ設定したタイミング
です。
LOWにしているのはapp_main()実行後
サンプルコードでは次の部分でGPIOを初期化しています。
gpio_config_t io_conf = {
.pin_bit_mask = (1ULL << OUTPUT_GPIO),
.mode = GPIO_MODE_OUTPUT,
.pull_up_en = GPIO_PULLUP_DISABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE
};
ESP_ERROR_CHECK(gpio_config(&io_conf));
ESP_ERROR_CHECK(
gpio_set_level(OUTPUT_GPIO, 0));
確かに、この処理が実行されたあとはGPIO10をLOWへ制御できます。
しかし、
電源投入
↓
ESP32起動処理
↓
このコードを実行
という順番です。
つまり、このコードが実行されるより前の状態については、このgpio_set_level()では制御できません。
ここが起動時誤動作を考えるうえで重要なポイントです。
内部Pull-downを使えば解決する?
ESP32にはGPIOの内部Pull-up/Pull-down機能があります。
例えば今回の設定を、
GPIO_PULLDOWN_ENABLE
へ変更すればよいのではないか、と考えることもできます。
ただし、それだけに依存するのは避けたい場合があります。
内部Pull-up/Pull-downもESP32内部の機能です。
製品として、
Reset中や起動途中を含めて確実に負荷をOFFへしたい
のであれば、外部抵抗でDefault状態を決める方が回路として分かりやすくなります。
特にリレー、モーター、ヒーターなど、安全性や機械動作に関係する出力では重要です。
Softwareより先にHardwareで状態を決める
例えばMOSFET Gateなら、
ESP32 GPIO
↓
Gate抵抗
↓
MOSFET Gate
↓
10kΩ~100kΩ程度
↓
GND
という構成を検討できます。
このPull-down抵抗があれば、ESP32 GPIOがHi-ZでもGateをLOW側へ保持できます。
そしてESP32が起動してGPIOをHIGHにすれば、Pull-down抵抗に逆らってGate電圧が上がり、MOSFETをONできます。
つまり、
HardwareでDefault OFF
Softwareで必要なときだけON
という役割分担です。
Pull-down抵抗は何Ωにする?
よく使用される値としては、10kΩ~100kΩ程度があります。
ただし、
「MOSFET Gateには必ず10kΩ」
という決まりではありません。
抵抗値を小さくするとGateをより強くLOWへ保持できますが、GPIOがHIGHの間はPull-down抵抗へ電流が流れ続けます。
逆に抵抗値を大きくすると消費電流は減りますが、外来ノイズなどに対する保持は弱くなります。
回路条件に合わせて決めます。
Active Lowなら考え方が逆になる
すべての回路が、
HIGH → ON
とは限りません。
例えば、
LOW → Enable
となる外部ICもあります。
この場合、起動時にOFFへしたければ、Pull-downではなくPull-upが必要になる可能性があります。
つまり重要なのは、
Pull-upかPull-downかを先に決めることではありません。
まず、
負荷を安全な状態にする論理レベルはHIGHなのかLOWなのか
を決めます。
その後、その状態へ保持する抵抗を選びます。
Strapping Pinにも注意する
ESP32シリーズには、起動時に特定の状態を参照してBoot動作などを決めるPinがあります。
こうしたPinへ外部Pull-up/Pull-downや負荷を接続すると、意図しないBoot Modeになる可能性があります。
そのためGPIOを割り当てるときは、
「空いているGPIOだから使う」
だけで決めず、そのPinが起動時にどのような役割を持つか確認します。
特に自作基板では、この確認を回路設計段階で行うことが重要です。
起動時だけでなくReset時も考える
もう一つ忘れやすいのがResetです。
製品は電源投入時だけResetするとは限りません。
例えば、
- Watchdog Reset
- Software Reset
- Brownout
- ENによるReset
- Firmware Update後のReset
などがあります。
Resetが発生すれば、再びApplicationがGPIOを初期化するまでの期間が生まれます。
したがって、
「電源投入時に一瞬だけだから問題ない」
ではなく、
運転中にResetしても外部回路が安全状態を維持できるか
まで確認します。
Strapping Pinは回路設計の最初に確認する
ESP32シリーズでは、起動時に特定のGPIO状態を参照してBoot Modeなどを決めるPinがあります。
こうしたPinへ外部回路を接続する場合、
- 強いPull-up
- 強いPull-down
- 外部ICの出力
- MOSFET Gate回路
- LED
- Driver Enable
などが起動条件へ影響する可能性があります。
そのため、
空いているGPIOを見つけてから用途を割り当てる
のではなく、
そのGPIOが起動時に何をするPinなのかを確認してから割り当てる
方が安全です。
特に製品基板では、Boot Modeに関係するPinへ不用意に外部回路を接続すると、
「通常は動くのに、特定条件だけ起動しない」
という分かりにくいトラブルにつながります。
起動時の波形はオシロスコープで確認する
起動時だけ一瞬負荷が動く場合は、GPIO波形を実際に測定すると状況が分かりやすくなります。
確認したいのは、
電源投入直後からApplicationがGPIOを初期化するまで
です。
例えばGPIOをLowにしたい場合でも、波形が、
電源ON
↓
Hi-Z
↓
一瞬HIGH
↓
LOW
のようになっているかもしれません。
目視では分からない短いパルスでも、MOSFETやDriver Enableには十分な場合があります。
そのため、起動時誤動作が疑われる場合は、
- GPIO
- Enable信号
- MOSFET Gate
- 負荷側電圧
をオシロスコープで確認します。
MOSFET GateはHardwareでOFFを作る
N-channel MOSFETをLow Side Switchとして使用し、
HIGHでON
とする場合は、GateへPull-down抵抗を入れるのが基本的な考え方です。
ESP32 GPIO
│
Gate抵抗
│
├──── MOSFET Gate
│
Pull-down
│
GND
ESP32がまだGPIOを出力していない状態でも、GateはGND側へ引かれます。
これにより、
Default OFF
をHardware側で作れます。
一方で、Pull-down抵抗値を小さくしすぎるとGPIO HIGH時に無駄な電流が増えます。
逆に大きすぎるとノイズに弱くなる可能性があります。
そのため回路条件に合わせて選びます。
Relay Driverでも同じ考え方
リレーをトランジスタやMOSFETで駆動する場合も、起動時にDriverが勝手にONしない状態を作る必要があります。
例えばNPNトランジスタを使う場合、
Baseが浮かないようにする
ことが重要です。
MOSFETならGate Pull-down。
NPNならBase-Emitter間の抵抗。
このように、CPUが動いていなくてもDriverがOFFになるようにします。
特にリレーは一瞬ONするだけでも、
- 接点が切り替わる
- 機械が動く
- 電源が投入される
可能性があります。
そのためLEDよりも起動時状態の重要度は高くなります。
Enable信号は安全側へ倒す
Motor Driver、Power IC、外部ModuleなどにはEnable端子があることがあります。
このEnable信号は、
CPU Reset中にどちらの論理へ倒すべきか
を先に決めます。
例えば、
HIGH = Enable
ならPull-down。
LOW = Enable
ならPull-up。
というように、
安全状態をHardwareで固定
します。
ここで重要なのは、
「CPUが起動したら正しく制御できる」
ではなく、
CPUが起動していないときでも安全である
ことです。
Software初期化もできるだけ早く行う
Hardware側でDefault状態を決めたうえで、Software側でもApplication開始後にできるだけ早くGPIOを初期化します。
サンプルコードでは、app_main()の最初でGPIO設定を行っています。
gpio_config_t io_conf = {
.pin_bit_mask = (1ULL << OUTPUT_GPIO),
.mode = GPIO_MODE_OUTPUT,
.pull_up_en = GPIO_PULLUP_DISABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE
};
ESP_ERROR_CHECK(gpio_config(&io_conf));
ESP_ERROR_CHECK(
gpio_set_level(OUTPUT_GPIO, 0));
この順番なら、Applicationが開始したあと早い段階でGPIOをLOWへ確定できます。
ただし、繰り返しになりますが、
このコードが動く前の状態はHardware側で担保する
という役割分担が重要です。
GPIO初期値と出力切替順序にも注意する
回路によっては、GPIOをOutputに切り替えた瞬間の状態も気になる場合があります。
例えば、
- GPIOをOutputへ設定
- その後LOWへ設定
という順序で、ごく短時間だけ意図しない状態になる可能性がないか確認します。
必要に応じて、使用するESP-IDF APIやGPIOの初期値設定方法を確認し、
Outputとして有効になる瞬間から安全な論理になるようにする
ことも重要です。
特に高速なDriver EnableやPower Controlでは、この短い時間も無視できない場合があります。
Reset中にも同じことが起きる
起動時対策は、電源投入時だけの話ではありません。
ESP32がResetすると、再びApplicationがGPIOを制御するまでの期間が発生します。
例えば、
- Watchdog Reset
- Software Reset
- Brownout
- EN Reset
- Firmware更新後のReset
です。
そのため、
Reset = GPIO制御が一時的に解除される可能性がある
と考えます。
製品が運転中にResetしても、外部回路が勝手にONしないようにしておく必要があります。
Brownout時も安全状態を維持する
Brownoutも注意したいケースです。
電源電圧が低下すると、ESP32が正常に制御できなくなる前後で外部回路の状態が変化する可能性があります。
そのため重要なEnable信号では、
電源が不安定になっても外部回路が安全側へ倒れる
ようにします。
ここでもPull-up/Pull-downやDriver回路のDefault状態が効いてきます。
外部ICの内部Pull-up/Pull-downも確認する
ESP32側だけを見ていると見落としやすいのが、接続先ICの内部抵抗です。
例えばDriver ICのEnable端子に、
内部Pull-up
が入っている場合があります。
ESP32側で何も接続しなければ、自動的にEnableになる可能性があります。
逆に内部Pull-downがある場合もあります。
そのため外部ICのデータシートで、
- Internal Pull-up
- Internal Pull-down
- Input Leakage
- Default State
を確認します。
外付け抵抗と内部抵抗が並列になることも考慮します。
Active High / Active Lowを回路図で明確にする
製品設計では、信号名にもDefault状態を反映すると分かりやすくなります。
例えば、
MOTOR_EN
だけではなく、
MOTOR_EN
がHIGH Activeなのか、
/MOTOR_EN
のようなLOW Activeなのかを明確にします。
回路図上でActive Levelが分かると、
「起動時にPull-upなのかPull-downなのか」
も判断しやすくなります。
安全が必要な負荷はSoftwareだけに任せない
特に、
- ヒーター
- モーター
- ソレノイド
- 電磁弁
- リレー
- 高出力LED
- 電源制御
のような負荷では、
Applicationが正常に起動してからOFFにする
という設計だけでは不十分な場合があります。
Hardware側でDefault OFFを作り、
Softwareはその状態を必要なときだけ変更する。
この構成なら、
CPUがResetしても、
Firmwareが起動しなくても、
Debug中でも、
Default状態を維持しやすくなります。
起動時誤動作の切り分け方法
起動時だけ負荷が動く場合は、次の順序で確認すると整理しやすくなります。
1. GPIOの起動時波形を見る
電源投入からApplication起動までを確認します。
2. 外部回路のDefault状態を見る
Pull-up/Pull-downがあるか確認します。
3. Strapping Pinか確認する
Boot動作に関係するGPIOではないか確認します。
4. 外部ICの内部Pullを確認する
DriverやEnable端子側の仕様を確認します。
5. Reset時にも同じ現象が起きるか確認する
電源投入だけでなくSoftware Resetなどでも確認します。
6. Hardwareで安全状態を作る
必要に応じて外付けPull-up/Pull-downを追加します。
SoftwareとHardwareの役割を分ける
起動時GPIO対策では、SoftwareとHardwareのどちらか一方だけで解決しようとしない方が分かりやすくなります。
Hardware:
CPUが制御していない状態でも安全なDefaultを作る
Software:
起動後に必要な状態へ正しく制御する
という役割分担です。
例えばMotor Enableなら、
電源投入
↓
Hardware Pull-down
↓
Motor OFF
↓
ESP32起動
↓
GPIO初期化
↓
必要な条件が成立
↓
SoftwareでMotor ON
という流れにできます。
これなら起動時にMotorが勝手に回る可能性を減らせます。
設計時チェックリスト
ESP32のGPIOで外部回路を制御するときは、次の項目を確認しておくと安心です。
- そのGPIOはStrapping Pinではないか
- Reset中の状態はどうなるか
- Application起動前の状態を考えたか
- 外部回路の安全状態はHIGHかLOWか
- 外付けPull-up/Pull-downは必要か
- 外部ICに内部Pullがないか
- Reset時にも負荷が安全か
- Brownout時にも安全か
- GPIO波形を実測したか
- Software初期化をできるだけ早く行っているか
- 安全に関係する負荷をSoftwareだけに依存していないか
まとめ
ESP32で、
「起動時だけGPIOが勝手に動く」
ように見える現象は、Applicationだけを見ていると分かりにくいことがあります。
重要なのは、
ESP32がGPIOを制御できない時間も存在する
ということです。
電源投入直後。
Reset中。
Boot処理中。
Application初期化前。
こうした期間でも外部回路を安全な状態へ保つには、Hardware側のDefault設計が重要になります。
MOSFET GateならPull-down。
Active Low EnableならPull-up。
Driver回路ならDefault OFF。
そしてApplicationが起動したあと、Softwareで必要な状態へ切り替えます。
Hardwareで安全状態を作り、Softwareで制御する。
この役割分担を決めておけば、起動時やReset時の意図しない動作を防ぎやすくなります。
技術は、経験から価値になる。
DLROW Design | 現場エンジニア