HC-SR04 × Raspberry Pi Pico
- はじめに
- 超音波距離センサー(HC-SR04)とは|非接触で距離を測れる仕組みと原理
- 今回使うセンサー(Winova 2cm-500cm)と HC-SR04 の互換性
- この記事で使う部品(Raspberry Pi Pico・HC-SR04・OLED ほか)
- Arduino ではなく Raspberry Pi Pico + MicroPython で動かす(配線の注意点)
- 配線図と MicroPython の最小コード(使い方の基本)
- 測定値がばらつく・ちらつく問題への対策
- 実際に測って精度を検証してみた
- 距離を OLED(SSD1306)にリアルタイム表示する
- 応用アイデア(駐車アシスト・障害物検知・液面レベル計)
- よくある質問(FAQ)
- まとめ
- シリーズ記事一覧
はじめに
こんにちは。ニンジンです🥕
「超音波距離センサー」を1つ買ってみました。Winova というメーカーの、検出範囲 2cm〜500cm・5V駆動のモジュールです。形は定番の「HC-SR04」とまったく同じで、目玉のような送受信部が2つ並んだ、あのセンサーです。
超音波距離センサーは、対象に触れずに距離を測れる部品です。身近なところだと、車のバックソナー(障害物に近づくとピピピと鳴るあれ)、自動ドアやトイレの人感、タンクの液面レベル計などに使われています。工作でも「壁までの距離を測って止まるロボット」「箱に手を近づけると光る」といった仕掛けが手軽に作れます。1個あたり数百円と安いのも魅力です。
この記事では、この HC-SR04(互換品)を Raspberry Pi Pico + MicroPython で動かします。ネットで「HC-SR04 使い方」を調べると Arduino の作例ばかりが出てきますが、Pico(MicroPython)での実装解説は意外と少なく、しかも Pico で使うときは Arduino にはなかった「配線の落とし穴」があります。そこを実機で確かめながら、次のことを一通りやります。
- 超音波で距離が測れる仕組み・原理
- Pico で安全に使うための配線(Echo の分圧が必須)
- 値がばらつくときのフィルタと気温補正
- メジャーと突き合わせた精度検証(どこまで信じていい数字か)
- OLED表示、障害物アラートへの応用
途中、2個セットの片方が初期不良だった話や、起動時にブザーが勝手に鳴った話など、実際にハマった箇所も挟んでいきます。
まずは完成形です。距離を OLED に数値と横バーで表示するプログラム(main.py)を動かしたところです。
なお、Raspberry Pi Pico への MicroPython の書き込みや Thonny のセットアップ、OLED(SSD1306)のライブラリ導入は過去記事で解説しているので、未経験の方はそちらを先にご覧ください。この記事では再掲しません。
超音波距離センサー(HC-SR04)とは|非接触で距離を測れる仕組みと原理
まず「なぜ音で距離が測れるのか」からです。原理はコウモリやイルカ、船のソナーとまったく同じで、音を出して、跳ね返って戻ってくるまでの時間を測るだけです。
音を出して跳ね返りが戻る時間を測る(ソナーの原理)
音は空気中を1秒間におよそ 343m 進みます(20℃のとき)。センサーから対象に向けて音を「ポン」と出し、跳ね返って戻ってくるまでに 0.001 秒(1ミリ秒)かかったとすると、音は往復で 0.343m 進んだことになります。知りたいのは片道の距離なので、これを半分にして約 17cm、と分かります。
式にするとこうです。
距離 [m] = 音速 [m/s] × Echo が HIGH だった時間 [s] ÷ 2人間の耳では聞こえない 40kHz(キロヘルツ)の高い音=超音波を使うのは、指向性(まっすぐ飛ぶ性質)を持たせやすく、周囲の生活音と混ざらないからです。
Trig(送信の合図)と Echo(反射の受信)の2本の信号線
HC-SR04 には信号線が2本あります。役割は完全に分かれています。
| ピン | 向き | 役割 |
|---|---|---|
| Trig | マイコン → センサー | ここを 10マイクロ秒だけ HIGH にすると、センサーが超音波を8発送信する |
| Echo | センサー → マイコン | 送信した瞬間から反射音を受け取るまでの時間だけ HIGH になる |
つまりプログラムがやることは「Trig を一瞬 HIGH にする」→「Echo が HIGH だった時間をストップウォッチで測る」の2つだけです。MicroPython には Echo のようなパルスの幅を測る専用関数 time_pulse_us() があるので、実装はとても短くなります(後述)。
距離の計算式と音速の温度補正(v = 331.5 + 0.6 × 気温)
音速は温度で変わります。気温が高いほど空気の分子が活発に動くので、音は速く伝わります。実用上は次の1次式で十分です。
音速 v [m/s] = 331.5 + 0.6 × 気温 [℃]
0℃ → 331.5 m/s
15℃ → 340.5 m/s
25℃ → 346.5 m/s0℃と25℃では音速が約 4.5% 違います。距離 3m を測るなら 13cm の差です。「20℃固定」で計算しているコードが多いですが、後半の精度検証で見るように、実際の室温を入れるだけで誤差がはっきり減ります。
検出範囲 2〜500cm・指向角 約15°という仕様の意味
今回の Winova のスペックは「2cm〜500cm」「指向角 15°」となっています。ここは額面通りには受け取らないほうがいいです。
- 下限 2cm: 近すぎると、送信した音がまだ鳴り終わらないうちに反射が戻ってきてしまい、受信回路が切り替わりません。実測でも 3cm は誤差が大きく出ました。
- 上限 500cm: 理想的な条件(大きく平らで固い壁に真正面)での値です。実用は 2〜3m と考えたほうが無難でした。
- 指向角 15°: センサーの正面 ±15° くらいの範囲の反射しか拾いません。裏を返すと、対象の面がその角度以上に傾いていると、音は明後日の方向へ反射して戻ってきません。
HC-SR04 は電源(VCC)5V、GND、Trig、Echo の4ピン構成です。この形と仕様が事実上の業界標準になっていて、各社から互換品が出ています。
今回使うセンサー(Winova 2cm-500cm)と HC-SR04 の互換性
ピン配列も使い方も HC-SR04 と同じ
購入した Winova のモジュールは、基板のシルク印字・ピンの並び(VCC / Trig / Echo / GND)・動作(Trig 10µs → Echo パルス)すべてオリジナルの HC-SR04 と同じでした。ネット上の HC-SR04 の情報や作例は、配線もコードもそのまま流用できます。
HC-SR04 / HC-SR04+ / 互換品の違い
売られているものには何種類かあります。
| 種類 | 電源 | Pico で使うとき |
|---|---|---|
| HC-SR04(無印・オリジナル/互換品) | 5V | Echo が 5V で出るので分圧が必要 |
| HC-SR04+ / HC-SR04P | 3.3〜5V | Echo も電源電圧に追従。3.3V 給電なら分圧不要のものがある |
| RCWL-1601 など | 3.3〜5V | 同上。Pico と相性が良い |
今回の Winova は無印相当(5V専用)なので、後述の通り Echo を分圧して使いました。「Pico や ESP32 で分圧なしに使いたい」なら、購入時に「+」や「3.3V対応」と明記されたものを選ぶと配線が1つ楽になります。
2個セットの片方が初期不良だった話 — 予備があると安心
今回のセンサーは2個セットでした。「1個は実験用、1個は完成品用に」くらいの気持ちだったのですが、結果的にこのセット構成に助けられました。
最初に配線した個体が、給電も配線もトリガも問題ないのに、反射音をただの一度も受信できませんでした。Echo は毎回「反射なし」を示す約 58ミリ秒(≒1000cm相当)を返すだけ。テスターで VCC に 5V が来ていることも、Trig のパルスが出ていることも確認しました。切り分けの結果、送信側のトランスデューサ(音を出す円筒)の不良と判断し、もう片方に差し替えたところ一発で動きました。
安い互換品にはこうした個体差・初期不良がそれなりの確率であります。2個セット以上を買っておくと、いざというとき原因の切り分け(センサーを替えて再現するか)ができて安心です。 次のセクションで、実際にやった切り分け手順も紹介します。
この記事で使う部品(Raspberry Pi Pico・HC-SR04・OLED ほか)
今回の工作で使った部品です。センサー本体以外は、これまでの Pico 記事で使ってきたものと同じで、電子工作の定番パーツです。







※リンクはアフィリエイトを含みます。価格・在庫・同梱内容は変わることがあるので、購入前に各商品ページをご確認ください。
Arduino ではなく Raspberry Pi Pico + MicroPython で動かす(配線の注意点)
ここがこの記事のいちばん大事なところです。
Arduino の作例は多いが、配線の考え方は同じ
HC-SR04 の作例は Arduino UNO のものが大量にあります。UNO は 5V マイコンなので、Echo の 5V 信号をそのまま digital ピンに入れて pulseIn() で測る、というのが定番です。
Raspberry Pi Pico は 3.3V マイコンです(今回の実機は Pico 2 W ですが、初代 Pico / Pico W でも配線・コードは同じです)。中身のチップ(初代=RP2040、Pico 2=RP2350)の GPIO は 5V 入力に対応しておらず、絶対最大定格は 3.6V 前後です。ここに HC-SR04 の Echo(5V)を直結すると、チップにダメージを与える可能性があります。
Trig と Echo で扱いが違うので、分けて考えます。
【重要】Echo の 5V を Pico の GPIO に直結しない — 抵抗2本で分圧する
Echo はセンサーから出てくる信号で、HIGH のとき電源電圧(5V)近くまで上がります。これを Pico が安全に読める電圧まで下げます。いちばん簡単なのが抵抗2本の分圧です。
今回は手元にあった 1.0kΩ と 2.2kΩ を使いました。
HC-SR04 Echo ──[ 1.0kΩ ]──┬── GP2(Pico)
│
[ 2.2kΩ ]
│
GND
分圧ノードの電圧 ≒ 5V × 2.2 /(1.0 + 2.2)≒ 3.4V3.4V は RP2040 / RP2350 の絶対最大定格 3.6V の範囲内で、かつ Pico が HIGH と判定するには十分な電圧です。実測でも問題なく測定できました。
- もっと 3.3V ぴったりに寄せたいなら 1.8kΩ + 3.3kΩ などでも構いません(ノード ≒ 3.24V)。
- 抵抗を触りたくなければ、市販の「ロジックレベル変換モジュール」を Echo の間に挟む方法もあります。
Trig は 3.3V 直結で OK・VCC は VBUS(5V)から取る
- Trig: Pico から出す信号です。Pico の 3.3V を HC-SR04 に入れることになりますが、HC-SR04 側は 3.3V でも「HIGH」と認識してくれたので、今回は直結で動きました(動かない個体に当たったら、Trig にもレベル変換かトランジスタ1石が必要です)。
- VCC: センサーの動作電源は 5V が必要です。Pico の「3V3(OUT)」から取ると、送信する超音波が弱く、近くの物しか測れない・まったく測れないといった症状が出ます。Pico を USB 給電しているなら、USB の 5V がそのまま出ている VBUS(物理40番) から取ります。
配線一覧
| 接続元 | 接続先(Pico) | 物理ピン | 備考 |
|---|---|---|---|
| HC-SR04 VCC | VBUS(5V) | 40 | 3V3 だと送信が弱くて測れない |
| HC-SR04 Trig | GP3 | 5 | 3.3V 直結で動作 |
| HC-SR04 Echo | 1.0kΩ 直列 → 分圧ノード → GP2 | 4 | Echo は 5V で出る |
| 分圧ノード | 2.2kΩ → GND | — | ノード ≒ 3.4V |
| HC-SR04 GND | GND | 38 |
配線図と MicroPython の最小コード(使い方の基本)
配線ができたら、まず「1回測って距離を print する」だけの最小コードで動作確認します。
MicroPython の書き込みや Thonny の使い方が初めての方は、先にこちらをご覧ください。
この記事で使うコード(test001〜test004・main.py・切り分け用スクリプト)は、まとめてダウンロードできます。
📦 この記事のコード一式をダウンロード(ZIP・test001〜test004 / main.py ほか)
time_pulse_us() で Echo のパルス幅を測る
machine.time_pulse_us(pin, level, timeout_us) は、「指定ピンが level(今回は 1=HIGH)である時間をマイクロ秒で返す」関数です。HC-SR04 の Echo を測るためにあるような関数で、これを使うと測定部分が数行で書けます。
パルス幅から距離(cm)へ変換する
test001 の全文です。
"""
ultrasonic-distance-sensor001 test001: HC-SR04 で距離を1回測る最小コード
"""
from machine import Pin, time_pulse_us
import time
trig = Pin(3, Pin.OUT)
echo = Pin(2, Pin.IN)
# 20℃ のときの音速 [m/s]。気温で変わる(test002 で補正する)
SOUND_SPEED = 343.2
# この距離を超えたら「測れなかった」とみなす(センサーの実用上限)
MAX_RANGE_CM = 500
def measure_cm(timeout_us=60000):
"""1回だけ測って距離[cm]を返す。範囲外 / 反射なしなら None"""
# Trig を最低 2us LOW にしてからパルスを出す
trig.low()
time.sleep_us(2)
trig.high()
time.sleep_us(10)
trig.low()
# Echo が HIGH になっている時間(us)を測る
dur = time_pulse_us(echo, 1, timeout_us)
if dur < 0:
return None
# dur[us] -> [s] にして音速[m/s]を掛け、cm に直して往復ぶんを半分にする
distance_cm = (dur / 1_000_000) * SOUND_SPEED * 100 / 2
if distance_cm > MAX_RANGE_CM:
return None
return distance_cm
while True:
d = measure_cm()
if d is None:
print("測定できませんでした(範囲外 / 反射なし)")
else:
print("距離: {:.1f} cm".format(d))
time.sleep(0.5)対応ファイル: ultrasonic-distance-sensor001-test001.py
反射が返らないときの落とし穴 — 「約58ms」というダミー値
HC-SR04 でいちばんハマりやすいのがここです。
time_pulse_us() は、指定した timeout_us の間ずっと Echo が HIGH にならなければ、負の値(-2 など)を返します。それなら if dur < 0 で弾けそうに見えます。
ところが このモジュールは、反射音を受け取れなかったとき Echo を約 58ミリ秒(距離に直すと約1000cm)HIGH にしてから LOW に戻します。 これは正の値なので dur < 0 はすり抜けます。何も対策しないと「1000cm」というありえない距離がしれっと混ざります。
対策は、MAX_RANGE_CM(実用上限=500cm)を超えた値を None にすることです。上のコードの if distance_cm > MAX_RANGE_CM: return None がそれです。ここを入れておかないと、次の章のフィルタも意味をなしません。
測れないときの切り分け手順
「配線したのに測れない」ときは、time_pulse_us を使わず自分で Echo を監視するデバッグ用スクリプトで、どこで詰まっているか切り分けます。今回、前述の初期不良を特定したときにやった順番です。
- Echo のアイドル電位: 測定していないとき Echo は LOW のはず。
echo.value()を1000回読んで全部 0 か確認する。0 でない回が多ければ、Echo の配線か分圧抵抗の向きを疑う - 自前でパルス幅を測る: Trig を出した後、
echo.value()が 1 になるのを待ち、0 に戻るまでの時間をtime.ticks_us()で測る。ここで一度も HIGH にならなければ、Trig が弱い(3.3V)か分圧しすぎ - time_pulse_us の生の戻り値:
-2ばかり=一度も HIGH にならない、-1=HIGH のまま戻らない、正の値=そのパルス幅 - テスターで VCC を実測: センサーの VCC に本当に 5V 来ているか。VBUS の電圧も見る
今回の初期不良品は「1・2・3 すべて正常なのに、パルス幅が毎回 約58800µs(反射なしのタイムアウト値)で一定」でした。センサーの正面で手や本を前後に動かしても数値がまったく変わらない=送信または受信ができていない、という結論です。別個体に替えて解決しました。
ModuleNotFoundError: No module named 'machine' が出たら、Thonny の右下のインタプリタが「Local Python 3」になっています。「MicroPython (Raspberry Pi Pico)」を選び直してください。測定値がばらつく・ちらつく問題への対策
test001 をしばらく動かすと、対象を固定していても値がチラチラ変わり、ときどき大きく外れた数字が出ることに気づきます。実用に使うにはひと手間必要です。
数十回に数回混ざる「飛び値」を中央値で無視する
面にきちんと正対させていても、数十回に数回は「反射なし(≒1000cm、コード上は None)」や、実際より大きく外れた値が混ざります。超音波が一度あらぬ方向に反射して、遠回りして戻ってきたケースなどです。
ここで平均を取ると、1回の大外れに全体が引きずられます。中央値(ソートして真ん中の値) なら、飛び値はきれいに無視できます。5回測って3番目の値を採用する、という単純な処理です。
測定と測定の間隔を 60ms 以上あける理由
立て続けに測ると、前回出した超音波の反射(残響)がまだ空気中や周囲に残っていて、それを拾って誤測定します。HC-SR04 のデータシートでも測定周期は 60ms 以上が推奨されています。連続測定するときは各測定の間に time.sleep_ms(60) を入れます。
気温を使った音速補正で誤差を減らす
test001 では音速を 343.2 m/s(20℃相当)で固定していました。ここを実際の室温で計算し直します。
今回の環境は室温 25.6℃でした。v = 331.5 + 0.6 × 25.6 ≒ 346.9 m/s です。この補正を入れたら、後述の 50cm 測定で 誤差がほぼゼロになりました。気温センサー(DHT11 など)を積んでいれば自動で、なければ手元の温度計の値を定数で渡します。
test002 の全文です。
"""
ultrasonic-distance-sensor001 test002: 測定値のばらつき対策 + 気温補正
"""
from machine import Pin, time_pulse_us
import time
trig = Pin(3, Pin.OUT)
echo = Pin(2, Pin.IN)
MEASURE_GAP_MS = 60 # 測定と測定の最小間隔
DEFAULT_TEMP_C = 25.6 # 気温センサーが無いときの仮の室温
MAX_RANGE_CM = 500 # これを超えたら「測れなかった」とみなす
def sound_speed(temp_c):
"""気温[℃]から音速[m/s]を返す"""
return 331.5 + 0.6 * temp_c
def measure_once(temp_c, timeout_us=60000):
"""1回測って距離[cm]。範囲外 / 反射なしなら None"""
trig.low()
time.sleep_us(2)
trig.high()
time.sleep_us(10)
trig.low()
dur = time_pulse_us(echo, 1, timeout_us)
if dur < 0:
return None
dist = (dur / 1_000_000) * sound_speed(temp_c) * 100 / 2
if dist > MAX_RANGE_CM:
return None
return dist
def measure_cm(samples=5, temp_c=DEFAULT_TEMP_C):
"""samples 回測って中央値[cm]を返す。全部失敗したら None"""
values = []
for _ in range(samples):
d = measure_once(temp_c)
if d is not None:
values.append(d)
time.sleep_ms(MEASURE_GAP_MS)
if not values:
return None
values.sort()
return values[len(values) // 2]
while True:
d = measure_cm(samples=5, temp_c=DEFAULT_TEMP_C)
if d is None:
print("測定できませんでした")
else:
print("距離(中央値): {:.1f} cm".format(d))
time.sleep(0.3)対応ファイル: ultrasonic-distance-sensor001-test002.py
左が test002 実行中のシェルです。中央値フィルタが効いて、test001 で見られた値の暴れ(たまに混ざる 1000cm など)が収まります。
右の写真は 50cm の位置に対象を置いた測定の様子です。
実際に測って精度を検証してみた
カタログの「2〜500cm」を鵜呑みにせず、メジャーで実距離を測って対象を置き、センサーの値と突き合わせました。「この数字はどこまで信じていいのか」を知っておくと、応用のときに判断できます。
精度検証用のロガー(test003)
指定回数ぶん連続測定して、平均・中央値・最小・最大・ばらつき(標準偏差)・実距離との誤差を出すだけのスクリプトです。TRUE_DISTANCE_CM にメジャーの値、ROOM_TEMP_C に室温を入れて実行し、対象を 10 / 20 / 50 / 100 / 200 / 300cm と動かして毎回実行しました。
# test003 の集計部分(全文は配布ファイル参照)
def stats(values):
n = len(values)
mean = sum(values) / n
var = sum((x - mean) ** 2 for x in values) / n
s = sorted(values)
return mean, s[n // 2], s[0], s[-1], var ** 0.5 # 平均, 中央値, 最小, 最大, 標準偏差対応ファイル: ultrasonic-distance-sensor001-test003.py
メジャー実測 vs センサー値(10cm〜300cm)
室温 25.6〜26.3℃、平らで固い面(本・板)に正対、各条件 30 回連続測定した結果です。
| 実距離 | 平均 | 中央値 | 最小〜最大 | ばらつき(±) | 平均の誤差 | 反射なし |
|---|---|---|---|---|---|---|
| 3cm(至近) | 4.2cm | 4.5cm | 3.7〜4.6 | 0.35 | +1.2cm(+41.5%) | 0/30 |
| 10cm | 10.8cm | 10.7cm | 10.7〜11.5 | 0.29 | +0.8cm(+7.9%) | 0/30 |
| 20cm | 22.6cm | 22.6cm | 22.6〜22.6 | 0.00 | +2.6cm(+13.1%) | 0/30 |
| 50cm | 50.0cm | 49.9cm | 49.9〜50.8 | 0.28 | +0.0cm(0.0%) | 0/30 |
| 200cm | 201.4cm | 200.1cm | 199.2〜209.0 | 2.83 | +1.4cm(+0.7%) | 0/30 |
| 250cm | 250.8cm | 250.7cm | 249.4〜254.1 | 0.87 | +0.8cm(+0.3%) | 0/30 |
| 300cm | 301.1cm | 300.6cm | 299.6〜304.3 | 1.23 | +1.1cm(+0.4%) | 0/30 |
誤差が大きくなる条件(斜めの面・柔らかい面・至近距離)
すべて実距離 50cm で、条件だけ変えた結果です。
| 条件 | 平均 | 誤差 | 反射なし | 解釈 |
|---|---|---|---|---|
| 面を45°傾ける | 59.7cm | +9.7cm(+19.3%) | 0/30 | 斜め面は正面に返らず、遠回りした反射を拾う |
| 対象をタオル/布 | 97.8cm | +47.8cm(+95.6%) | 9/30 | 繊維が超音波を吸収・散乱。実質測定不能 |
| 至近 3cm | 4.2cm | +1.2cm(+41.5%) | 0/30 | 送受信の切替が間に合わず近距離は苦手 |
左は 50cm の位置に対象を 45° 傾けて置いたときの様子です。平均で +10cm 近くずれました。指向角から外れた面は、音がまっすぐ返ってこないためです。
下は同じ 50cm の位置にタオルを置いたとき。値が倍近くになり、3割は「反射なし」でした。布はほぼ測れないと考えたほうがよいです。
カタログの 500cm は実用でどこまで届くか
- 300cm は、対象の面をセンサーときっちり平行にしないと検出できませんでした。何度も位置を調整してようやく安定します。指向性がシビアです。
- 人が横切ったのを拾えるのは体感 250cm くらいまで。
- 500cm は今回の環境(6畳間)では条件を作れず未検証。カタログ値は理想条件の数字なので、実用は 2〜3m と見ておくのが安全です。
この個体で分かったこと(記事の結論)
精度検証のまとめ
- 30cm〜300cm・正対・固い面なら優秀。絶対誤差は数cm、相対誤差 1% 以下(50cm で誤差ゼロ)。
- 20cm 以下は相対誤差が大きい(+8〜13%)。距離が近いほど「約 +1cm の下駄」が効いてくる。
- 全体に +1cm 前後の系統誤差(オフセット) がある個体。気になるなら
main.pyのOFFSET_CM = -1.0で詰められる。 - 中央値フィルタが効いている。200cm で最大 209cm(+9cm)の外れ値が出たが、中央値 200.1cm は実距離どおり。
- 斜めの面・柔らかい面は苦手。布は測定不能。距離を測りたい対象は「固くて平ら」が前提。
距離を OLED(SSD1306)にリアルタイム表示する
PC につながず単体で距離が見えるように、0.96インチ OLED(SSD1306、I2C接続)に数値と横バーグラフを出します。
OLED の配線(SDA / SCL / VDD / GND)と micropython-ssd1306 ライブラリの導入は過去記事で解説しているので、そちらを参照してください。
追加の配線はこれだけです。
| OLED | 接続先(Pico) | 物理ピン |
|---|---|---|
| SDA | GP0 | 1 |
| SCL(基板シルクは SCK) | GP1 | 2 |
| VDD | 3V3(OUT) | 36 |
| GND | GND | 38 |
i2c.scan() で 0x3C を確認する
まず配線が正しいか、I2C バス上に OLED が見えているかを確認します。
from machine import Pin, I2C
i2c = I2C(0, scl=Pin(1), sda=Pin(0), freq=400000)
print("I2Cデバイス検出:", i2c.scan()) # [60] (0x3C) が出れば正常I2Cデバイス検出: [60] と出れば OK です(60 は 0x3C、SSD1306 の標準アドレス)。[] なら SDA/SCL の挿し間違い、ジャンパの半挿し、VDD/GND の未接続を疑います。
標準フォントを拡大して大きく表示する
ssd1306 ライブラリの oled.text() は 8×8 ピクセル固定で、距離の数字を出しても離れると読めません。小さな FrameBuffer に一度描いてから、点いているピクセルを scale×scale の四角として拡大コピーする text_scaled() を使います。この仕組みは前回の記事で詳しく書いたので、ここでは関数だけ載せます。
import framebuf
def text_scaled(fb, s, x, y, scale, color=1):
"""標準8x8フォントの文字列を scale 倍に拡大して描画する"""
w = len(s) * 8
tmp = framebuf.FrameBuffer(bytearray(8 * ((w + 7) // 8)), w, 8, framebuf.MONO_HLSB)
tmp.fill(0)
tmp.text(s, 0, 0, 1)
for ty in range(8):
for tx in range(w):
if tmp.pixel(tx, ty):
fb.fill_rect(x + tx * scale, y + ty * scale, scale, scale, color)ちらつき対策 — 変化したときだけ描き直す
oled.show() は 128×64=1024バイトのバッファを I2C で丸ごと送るため、1回 20ms 以上かかります。毎ループ呼ぶと画面がちらつき、測定周期も伸びます。前回表示した距離と 0.5cm 以上変わったときだけ draw() を呼ぶようにして、静止時は再描画しないようにしました。
main.py 全文
test002(中央値フィルタ + 気温補正)に OLED 表示を統合した本番プログラムです。Pico 本体に main.py として保存すれば、PC も Thonny もつながず、モバイルバッテリー給電などで単体動作します。
"""
ultrasonic-distance-sensor001 main.py: HC-SR04 の距離を OLED(SSD1306)に
数値+横バーグラフでリアルタイム表示する
配線 (結線図と同じ):
- HC-SR04 VCC -> VBUS(5V) / 物理40番
- HC-SR04 Trig -> GP3 / 物理5番
- HC-SR04 Echo -> 1.0kΩ -> 分圧ノード -> GP2 / 物理4番
分圧ノード -> 2.2kΩ -> GND
- HC-SR04 GND -> GND / 物理38番
- OLED SDA -> GP0 / SCL -> GP1 / VCC -> 3V3(OUT) / GND -> GND
事前準備: Thonny の「パッケージを管理...」で micropython-ssd1306 を入れておく
"""
from machine import Pin, I2C, time_pulse_us
import framebuf
import ssd1306
import time
# ===== HC-SR04 =====
trig = Pin(3, Pin.OUT)
echo = Pin(2, Pin.IN)
MEASURE_GAP_MS = 60
DEFAULT_TEMP_C = 26.7 # 気温センサーが無いときの仮の室温
BAR_MAX_CM = 300 # 横バーが振り切れる距離
MAX_RANGE_CM = 500 # これを超えたら「測れなかった」とみなす
OFFSET_CM = 0.0 # 実測すると全体に +1cm ほど出る個体があった。
# メジャーと比べて詰めたいときはここに補正値(例 -1.0)を入れる
REDRAW_THRESHOLD_CM = 0.5 # この差より小さい変化では描き直さない(ちらつき対策)
def sound_speed(temp_c):
return 331.5 + 0.6 * temp_c
def measure_once(temp_c, timeout_us=60000):
trig.low()
time.sleep_us(2)
trig.high()
time.sleep_us(10)
trig.low()
dur = time_pulse_us(echo, 1, timeout_us)
if dur < 0:
return None
dist = (dur / 1_000_000) * sound_speed(temp_c) * 100 / 2 + OFFSET_CM
if dist > MAX_RANGE_CM:
return None
return dist
def measure_cm(samples=5, temp_c=DEFAULT_TEMP_C):
values = []
for _ in range(samples):
d = measure_once(temp_c)
if d is not None:
values.append(d)
time.sleep_ms(MEASURE_GAP_MS)
if not values:
return None
values.sort()
return values[len(values) // 2]
# ===== OLED =====
i2c = I2C(0, scl=Pin(1), sda=Pin(0), freq=400000)
print("I2Cデバイス検出:", i2c.scan()) # [60] (0x3C) が出れば正常
oled = ssd1306.SSD1306_I2C(128, 64, i2c)
def text_scaled(fb, s, x, y, scale, color=1):
"""標準8x8フォントの文字列を scale 倍に拡大して描画する"""
w = len(s) * 8
tmp = framebuf.FrameBuffer(bytearray(8 * ((w + 7) // 8)), w, 8, framebuf.MONO_HLSB)
tmp.fill(0)
tmp.text(s, 0, 0, 1)
for ty in range(8):
for tx in range(w):
if tmp.pixel(tx, ty):
fb.fill_rect(x + tx * scale, y + ty * scale, scale, scale, color)
def draw(distance_cm):
oled.fill(0)
if distance_cm is None:
text_scaled(oled, "-- cm", 8, 10, 2)
oled.text("out of range", 16, 44, 1)
oled.show()
return
# 上段: 距離の数値(2倍角)
text_scaled(oled, "{:>3d} cm".format(int(round(distance_cm))), 4, 6, 2)
# 下段: 0-BAR_MAX_CM の横バーグラフ
bx, by, bw, bh = 6, 42, 116, 16
oled.rect(bx, by, bw, bh, 1)
ratio = min(1.0, distance_cm / BAR_MAX_CM)
fill_w = int(ratio * (bw - 4))
oled.fill_rect(bx + 2, by + 2, fill_w, bh - 4, 1)
oled.show()
# ===== メイン =====
last_shown = None
draw(None)
print("準備完了")
while True:
d = measure_cm(samples=5, temp_c=DEFAULT_TEMP_C)
if d is None:
if last_shown is not None:
draw(None)
last_shown = None
print("距離: 範囲外")
else:
if last_shown is None or abs(d - last_shown) >= REDRAW_THRESHOLD_CM:
draw(d)
last_shown = d
print("距離: {:.1f} cm".format(d))
time.sleep_ms(120)対応ファイル: main.py
動いている様子は冒頭の完成動画のとおりです。対象を近づける/遠ざけると、OLED の数字とバーがリアルタイムに追従します。
✓ 動作確認方法
- Thonny でコードを開き F5 で実行する
- シェルに
I2Cデバイス検出: [60]が出る - OLED に大きな数字で距離(cm)、下に横バーが出る
- 対象を近づける/遠ざけると、数字とバーが追従する
- 500cm を超える/反射がない状態で
-- cm out of rangeになる
応用アイデア(駐車アシスト・障害物検知・液面レベル計)
距離が測れて表示できると、あとは「ある距離より近づいたら何かする」だけでいろいろ作れます。
- 駐車アシスト / バックソナー: 壁との距離に応じてブザーの間隔を変える。近いほど速く鳴らす
- 障害物検知ロボット: 前方の距離が一定以下になったら停止・方向転換
- 液面レベル計: タンクの上からセンサーを下向きに付け、水面までの距離で残量を推定(水面は平らなので相性が良い)
- 自動点灯 / 人感: 一定距離内に人が入ったら LED や照明を点ける
ここでは1つめの「バックソナー風の障害物アラート」を作りました(test004)。距離に応じて LED・ブザー・OLED の警告レベルを切り替えます。
| 距離 | レベル | ブザー / LED | OLED 表示 |
|---|---|---|---|
| 50cm 以上・範囲外 | SAFE | 消灯 | SAFE |
| 30〜50cm | CAUTION | 遅い断続(600ms) | CAUTION |
| 20〜30cm | WARNING | 速い断続(200ms) | WARNING(白黒反転) |
| 20cm 未満 | DANGER | 鳴りっぱなし | DANGER(白黒反転) |
使ったブザーは「アクティブ・アクティブハイ」だった
手元のブザー(基板に「PCB120」の表記)が、電圧をかけるだけで鳴る「アクティブブザー」なのか、PWM で周波数を与えないと鳴らない「パッシブブザー」なのか分からなかったので、1状態ずつ確認しました。
# buzzer-check.py の要点
buz = Pin(15, Pin.OUT)
buz.low() # step0: 静かなはず
buz.high() # step1: 鳴れば「アクティブ・アクティブハイ」
time.sleep(3)
buz.low() # step2: 止まるはず
# step3: PWM 2kHz -> アクティブなら不要、パッシブならここで初めて鳴る- DC HIGH で鳴りっぱなし → アクティブブザー(自励式)。今回のはこれ
- 一瞬「プッ」と鳴るだけ → パッシブブザー(PWM が必要)
- 見た目の目安: 裏が黒く密閉されている=アクティブ、緑の基板が見える=パッシブ。極性線(赤黒)があるものはアクティブが多い
今回の品は GP15 を HIGH にすると鳴る(アクティブハイ) で、PWM は不要でした。PWM 2kHz でも鳴りましたが、内蔵発振の共振周波数から外れるため音は弱く低くなります。
起動時に勝手に「ピー」と鳴る問題
電源を入れてからプログラムが動き出すまでの一瞬、GP15 の状態が不定になり、アクティブブザーが「ピー」と鳴ることがあります。対策はスクリプトの冒頭で必ず出力を LOW にすることです。
buzzer = Pin(15, Pin.OUT)
buzzer.low() # 起動直後のノイズ鳴りを止める。他の初期化より先にハードで確実に消したい場合は、GP15 と GND の間に 10kΩ 程度の抵抗(プルダウン)を入れておくと、マイコンが制御する前は必ず LOW に落ちます。
test004 のレベル判定と OLED 反転表示の要点です。
def level_of(distance_cm):
if distance_cm is None or distance_cm >= 50:
return "SAFE"
if distance_cm >= 30:
return "CAUTION"
if distance_cm >= 20:
return "WARNING"
return "DANGER"
# WARNING / DANGER は OLED を白背景に反転させて目立たせる
LEVELS = {
"SAFE": (None, False), # (ブザー周期ms / None=消灯 / 0=鳴りっぱなし, 反転表示)
"CAUTION": (600, False),
"WARNING": (200, True),
"DANGER": (0, True),
}対応ファイル: ultrasonic-distance-sensor001-test004.py
この記事のコードをまとめてダウンロード
ここまでで登場した MicroPython コードを ZIP にまとめました。配線メモを書いた README.txt と、うまく測れないときの切り分け用スクリプト(debug.py / debug2.py)、ブザー判別用の buzzer-check.py も同梱しています。
よくある質問(FAQ)
Qこのセンサーの精度はどれくらいですか?
A今回の個体では、30cm〜3m の範囲で、固くて平らな面に正面から向ければ、絶対誤差は数cm・相対誤差は 1% 以下でした(50cm では誤差ゼロ)。ただし 20cm 以下は +8〜13% と誤差が大きく、対象の面が傾いていたり布などの柔らかい素材だと大きくずれる・まったく測れないこともあります。「固い平面までのざっくりした距離」を測る用途なら十分実用的です。
QRaspberry Pi Pico の 3.3V で動かせますか? 5V は必須ですか?
Aセンサーの電源(VCC)は 5V が必要です。Pico の 3V3(OUT)から取ると、送信する超音波が弱く、まともに測れません。Pico を USB 給電しているなら VBUS(5V)から取ります。一方、Echo の信号は 5V で出てくるので、そのまま Pico の GPIO に入れず、抵抗2本で 3.3V 前後に分圧します(記事本文参照)。
Q分圧せずに Echo を直結したら壊れますか?
APico の GPIO の絶対最大定格は 3.6V 前後で、そこに 5V が入る状態になります。すぐ壊れないこともありますが、メーカーの想定外の使い方で、じわじわ劣化する・ある日突然そのピンが死ぬ、というリスクがあります。抵抗2本(数円)かレベル変換モジュールを挟むだけなので、必ず対策することをおすすめします。
Q超音波距離センサーを複数個いっぺんに使えますか?
A使えます。ただし Trig / Echo のピンをセンサーごとに分け、測定のタイミングをずらします。同時に鳴らすと、隣のセンサーが出した超音波を自分の反射だと勘違いして誤測定する(混信)ためです。1個測る→少し待つ→次を測る、と順番に回します。
Q屋外や車で使えますか?
A使えますが、風が強いと反射がぶれます。また気温変化で音速が変わるので、温度補正(v = 331.5 + 0.6 × 気温)を入れておくと安定します。直射日光でセンサー本体が高温になると値が甘くなることもあります。
QArduino 用の HC-SR04 コードをそのまま Pico に使えますか?
A考え方は同じですが、そのままは動きません。Arduino の pulseIn() は MicroPython では machine.time_pulse_us() に置き換えます。また Arduino UNO は 5V マイコンなので Echo を直結できましたが、Pico は 3.3V なので Echo の分圧(またはレベル変換)が必須です。この2点を直せば、あとはほぼ同じロジックで動きます。
QHC-SR04+ / HC-SR04P との違いは?
A「+」が付くものは電源電圧が 3.3〜5V に対応していて、3.3V で給電すれば Echo も 3.3V で出るため、Pico で使うときの分圧が不要になります。今回の Winova は無印相当(5V専用)なので分圧しました。これから買うなら「+」表記のものを選ぶと配線が1つ減ります。
まとめ
HC-SR04(互換品)を Raspberry Pi Pico + MicroPython で、距離測定から OLED 表示・障害物アラートまで一通り動かしました。
- 仕組みは「超音波を出して跳ね返る時間を測る」だけ。距離 = 音速 × Echo パルス幅 ÷ 2、音速は
331.5 + 0.6 × 気温 - Pico で使うときの肝は Echo の分圧。5V の Echo を抵抗2本(1.0kΩ + 2.2kΩ)で 3.4V に落として GP2 へ。Trig は 3.3V 直結、VCC は VBUS(5V)から
- 反射がないとき約1000cm のダミー値が返るので上限で弾く。値のばらつきは中央値フィルタと 60ms 間隔で抑える
- 精度は、30cm〜3m・正対・固い面なら誤差1%以下で優秀。20cm 以下と、斜め・柔らかい面は苦手
- 2個セットの初期不良、起動時のブザー鳴りなど、実機ならではの小さいハマりもあった
安くて扱いやすいセンサーですが、「どういう条件で正確に測れて、どういう条件でダメか」を先に知っておくと、応用のときに悩まずに済みます。
Follow me on X
本ブログの中の人「ニンジン🥕」はXやってます。新着記事はいち早くポストでお知らせしているので、見逃したくない方は @ninjin_py_vba をフォローしてみてください🥕
@ninjin_py_vba をフォローシリーズ記事一覧
電子工作・自動化ツールの製作記録をシリーズごとにまとめています。気になるテーマからお読みください。
-
FILE.01 — IoT
Raspberry Pi Pico W × GASラズパイPico W×GASで自作!「LINEで動く温湿度&スマートリモコン」完全ロードマップ
「外出中のペットの室温が心配…」そんな思いからスタートしたIoT自作プロジェクトの総集編。温湿度監視からエアコン遠隔操作まで、仕組みをゼロから作りたい方のための全5回の開発記録です。
シリーズを読む -
FILE.02 — CNC
Arduino × レーザー刻印CNCCNC自作シリーズ
高精度な加工を目指し、本格的な自作CNC製作に挑戦中。現在は基幹パーツであるオリエンタルモーターの納品を待つ「設計・準備編」を公開。ハードとソフトの両面から、理想のマシンを形にする過程をリアルタイムにお届けします。
シリーズを読む -
FILE.03 — XY軸制御
Raspberry Pi × ステッピングモーターXYテーブルシリーズ
Raspberry Piとステッパモーターを使い、ゼロから2軸制御に挑む記録。OS設定から回路設計、多軸制御のPythonコードまで、躓きやすいポイントを徹底図解。電子工作初心者が「動く感動」を味わうための実戦ガイドです。
シリーズを読む -
FILE.04 — 業務自動化
Python × Excel × CustomTkinter【Python開発記】NINJIN Mail制作記
PythonとExcelを連携させ、実務で即戦力となるメール送信ツールを開発。CustomTkinterによるUI構築や、ミスを防ぐテンプレート活用術など、現場の「痒い所に手が届く」自動化ノウハウを細部まで丁寧に解説します。
シリーズを読む -
FILE.05 — 業務自動化
Python × Excel × GUIアプリ化Invoice Maker(請求書・見積書自動作成)
手作業の請求書作成から卒業!PythonでExcelデータを読み込み一括PDF化する「Invoice_Maker」の作り方を全3回で解説。基本ロジックからGUIアプリ化まで、実務で役立つ自動化ノウハウが満載です。
シリーズを読む





コメント