Arduinoの心拍センサー:Pulse SensorとKY-039でBPMを測るガイド
Arduinoの心拍センサーガイド。Pulse SensorとKY-039の配線、PulseSensorPlaygroundでのBPM計測、シリアルプロッタでの脈波表示、OLED、ESP32とPicoのコードを解説します。
Arduinoの脈拍センサー(心拍センサーとも呼ばれます)は、指先や耳たぶの血流を光で追いかけ、心臓が1分間に何回拍動しているか、つまりBPM(beats per minute)を測る小さなモジュールです。電子工作でよく使われる2つのモデルは、丸い基板のPulse Sensorと、赤外線LEDを使ったKY-039です。どちらも3本の線でつなぎ、1本のアナログピンで読み取りますが、信号の質と、安定して動くコードの書きやすさには大きな差があります。
このガイドでは、脈拍センサーがどんな物理原理で動くのか、シリアルプロッタで本物の脈波を表示する方法、PulseSensorPlaygroundライブラリでBPMを計算する方法、KY-039の弱い信号をライブラリなしのアルゴリズムで処理する方法、そして結果をハートのアイコンと一緒にOLEDディスプレイに表示する方法をステップごとに学びます。ESP32とRaspberry Pi Picoのコード、測定を乱す要因、MAX30102との違いも取り上げます。最初に大事なことをお伝えしておきます。これらのセンサーは医療機器ではありません。健康に関わる判断には一切使わないでください。

脈拍センサーの仕組みは?光電容積脈波(PPG)
脈拍センサーはすべて、光電容積脈波法(PPG、photoplethysmography)と呼ばれる方法で動きます。心臓は収縮するたびに動脈へ血液の波を送り出します。この波が指先の細い血管に届くと、組織の中の血液量が一瞬だけ増えます。血液中のヘモグロビンは光を吸収するので、血液が多いほど、組織から反射して戻る光や組織を通り抜ける光は少なくなります。センサーはLEDで組織を照らし、光センサーが戻ってきた光を測ります。この光の量の小さく規則的な揺れこそが脈拍です。
難しいのは、測った光のほとんどが一定だという点です。皮膚、筋肉、骨、静脈の血液はいつも同じ量の光を吸収します。これを信号のDC成分と呼びます。一方、心拍に合わせて変化するAC成分は、たいてい全体の光のわずか1〜2%ほどです。つまりセンサーの本当の仕事は、大きく安定したレベルの上に乗った小さな波を取り出して増幅することです。PPGの波形は、ピーク(収縮期ピーク)まで速く立ち上がり、その後ゆっくり下がります。下り坂の途中にある小さなくぼみと、その直後の小さなこぶ(重複切痕、ダイクロティックノッチ)は、大動脈弁が閉じることで生まれます。良いアルゴリズムは心拍を1回ずつ数え、このくぼみを2回目の拍動と取り違えません。
光を組織にどう当てるかも重要です。Pulse Sensorは反射型を使います。LEDと光センサーが同じ面に並び、皮膚から散乱して戻ってくる光を測ります。使っている緑色の光はヘモグロビンに強く吸収され、皮膚のすぐ下の血管からはっきりした信号が得られます。スマートウォッチの裏側に緑色LEDが見えるのもこのためです。KY-039は透過型で動きます。赤外線LEDが指の片側を照らし、反対側のフォトトランジスタが指を通り抜けた光を測ります。赤外線は組織の奥まで届きますが、KY-039には信号を増幅する回路がないため、得られる波はとても小さくなります。
| 項目 | Pulse Sensorの値 |
|---|---|
| 測定方式 | 反射型PPG |
| 光源 | 緑色LED |
| 光センサー | APDS-9008、ピーク感度565 nm |
| 電源 | 3〜5 V |
| 電流 | 約4 mA(5 V時) |
| 出力 | アナログ。待機時は電源電圧の半分(5 Vで約2.5 V、Unoで約512) |
| 基板の直径 | 約16 mm(0.625インチ) |
| 配線 | 純正品は赤が+、黒がGND、紫が信号。互換品では色が異なることがあります |
Pulse Sensor・KY-039・MAX30102の比較

| 項目 | Pulse Sensor | KY-039 | MAX30102 |
|---|---|---|---|
| 方式 | 反射型PPG | 透過型PPG(指を挟む) | 反射型PPG |
| 光 | 緑色LED | 赤外線LED | 赤(660 nm)+赤外線(880 nm) |
| アンプとフィルタ | あり(オペアンプ) | なし | 18ビットADC内蔵、環境光キャンセル |
| 出力 | アナログ | アナログ(非常に弱い) | デジタル、I2C(アドレス0x57) |
| 電源 | 3〜5 V | 3.3〜5 V | モジュールにより3.3〜5 V |
| 信号の質 | 良い | 弱く、ノイズを拾いやすい | 非常に良い |
| SpO2(酸素) | なし | なし | あり(アルゴリズムを使用) |
| ライブラリ | PulseSensor Playground | 既製のライブラリなし | SparkFun MAX3010x |
初心者で、脈拍を測るだけでよいなら、Pulse Sensorがいちばん簡単です。信号が基板上で増幅・フィルタ処理されるので、最初の1回からシリアルプロッタにはっきりした波が表示されます。裏側のオペアンプが、この小さな変化をArduinoが読める大きさまで持ち上げます。こうしたゲインの計算方法はオペアンプ計算ツールで試せます。KY-039は安く、たいていのセンサーキットに入っていますが、出力はADC値で数カウントしか動かないため、固定しきい値の単純なコードでは安定した結果が出ません。血中酸素飽和度(SpO2)も測りたいなら、MAX30102が正解です。これは後ほど専用のセクションで取り上げます。
脈拍センサーの配線:Arduino Uno、ESP32、Pico
| センサーのピン | Arduino Uno | ESP32 | Raspberry Pi Pico |
|---|---|---|---|
| +(VCC、赤) | 5V | 3V3 | 3V3(OUT)、36番ピン |
| −(GND、黒) | GND | GND | GND、38番ピン |
| S(信号、紫) | A0 | GPIO 34(ADC1) | GP26(ADC0)、31番ピン |
| 拍動LED(任意) | D13(内蔵LED) | GPIO 2(内蔵LED) | 基板上のLED |
KY-039にも同じ表が当てはまります。Sピンはアナログ入力へ、+は電源へ、−はGNDへつなぎます。赤外線LEDがフォトトランジスタの上にかぶさるように曲がっていて、その間に指が入ることを確認してください。Pulse Sensorの裏側の部品はむき出しなので、指や汗ばんだ面が裏側に触れるとノイズの原因になります。そのため、キットに付属する透明シールを表面に貼り、裏側をホットボンドで覆っておくとよいでしょう。外付けのLEDを使う場合は、LED抵抗計算ツールで抵抗値を求めてください。ボードのピン配置はピン配置ツールでも確認できます。
最初のテスト:シリアルプロッタで生の脈波を見る
BPMに進む前に、センサーが本当に脈拍をとらえているか確認しましょう。下のコードはライブラリを使いません。1秒間に100回信号を読み取り、シリアルプロッタに送ります。コードを書き込んだら、Arduino IDEで「ツール」>「シリアルプロッタ」を開き、速度を115200 baudに設定してください。シリアルプロッタを使ったことがなければ、Arduinoのシリアルモニタの記事でウィンドウの使い方を説明しています。
1// Nabız sensörü ham sinyal testi (Seri Çizici) / Pulse sensor raw signal test (Serial Plotter)
2const int PULSE_PIN = A0; // sensörün S (mor) kablosu / sensor S (purple) wire
3const unsigned long PERIOD_MS = 10; // 10 ms = saniyede 100 örnek / 10 ms = 100 samples per second
4
5unsigned long lastSample = 0;
6
7void setup() {
8 Serial.begin(115200); // Seri Çizici'de de 115200 seç / pick 115200 in the Serial Plotter too
9 lastSample = millis();
10}
11
12void loop() {
13 if (millis() - lastSample >= PERIOD_MS) {
14 lastSample += PERIOD_MS; // sabit aralıkla örnekle / sample at a fixed interval
15 int raw = analogRead(PULSE_PIN); // 0-1023 arası değer / value between 0 and 1023
16 Serial.print("Sinyal:");
17 Serial.print(raw);
18 Serial.println(",Orta:512"); // boştaki orta seviye çizgisi / idle midpoint line
19 }
20}指を置いていないとき、Pulse Sensorの線は512付近で少し揺れながら平らに続きます。これは電源電圧の半分で、センサーが正しく配線されている証拠です。指先をセンサーに軽くのせると、数秒以内に、心拍ごとに速く立ち上がってゆっくり下がる波が現れます。振幅はたいてい100〜300カウントです。波が見えないときはBPMのコードに進まず、まず配線、電源、指の押さえ方を直してください。ピークが0や1023に張り付いて平らな線になる場合は、信号が飽和(クリップ)しています。たいていは指が動いているか、センサーを強く押しすぎています。アナログ値の0〜1023という範囲がどこから来るのか気になる場合は、ADCとは?の記事を参照してください。
PulseSensorPlaygroundでBPMを測る
- Arduino IDEでライブラリマネージャを開き、PulseSensor Playgroundを検索してインストールします。ライブラリのインストールについて詳しくは、Arduinoにライブラリを追加する方法の記事を参照してください。
- 表のとおりにセンサーを配線します。+を5Vへ、−をGNDへ、SをA0へつなぎます。
- 下のコードを書き込み、115200 baudでシリアルプロッタを開きます。1本目の線が生の信号、2本目の線がBPMの値です。
- センサーに指をのせ、手を机に置いて10秒間じっとしていてください。最初の数拍のあいだにライブラリが平均値を安定させます。
- シリアルモニタで数値を1行ずつ見たい場合は、コードのPLOTTER定数をfalseにしてください。拍動のたびにBPMと拍動間隔(IBI)が表示されます。
1// PulseSensor Playground ile BPM ölçme / Measuring BPM with PulseSensor Playground
2#include <PulseSensorPlayground.h>
3
4const int PULSE_PIN = A0; // sensörün S (mor) kablosu / sensor S (purple) wire
5const int LED_PIN = LED_BUILTIN; // her atışta yanan LED (D13) / LED lit on each beat (D13)
6const int THRESHOLD = 550; // başlangıç eşiği, boşta ~512 / starting threshold, idle ~512
7const bool PLOTTER = true; // true: Seri Çizici, false: Seri Monitör / true: Serial Plotter, false: Serial Monitor
8
9PulseSensorPlayground pulseSensor; // kütüphane nesnesi / library object
10unsigned long lastPrint = 0;
11
12void setup() {
13 Serial.begin(115200);
14 pulseSensor.analogInput(PULSE_PIN);
15 pulseSensor.blinkOnPulse(LED_PIN);
16 pulseSensor.setThreshold(THRESHOLD);
17 if (!pulseSensor.begin()) { // 2 ms'lik zamanlayıcıyı başlatır / starts the 2 ms timer
18 Serial.println(F("PulseSensor baslatilamadi / could not start"));
19 while (true) {}
20 }
21}
22
23void loop() {
24 // Atış anı: BPM ve atışlar arası süre / moment of a beat: BPM and inter-beat interval
25 if (pulseSensor.sawStartOfBeat() && !PLOTTER) {
26 Serial.print(F("BPM: "));
27 Serial.print(pulseSensor.getBeatsPerMinute());
28 Serial.print(F(" IBI: "));
29 Serial.print(pulseSensor.getInterBeatIntervalMs());
30 Serial.println(F(" ms"));
31 }
32
33 // 20 ms'de bir çizim satırı / one plotter line every 20 ms
34 if (PLOTTER && millis() - lastPrint >= 20) {
35 lastPrint = millis();
36 Serial.print(F("Sinyal:"));
37 Serial.print(pulseSensor.getLatestSample());
38 Serial.print(F(",BPM:"));
39 Serial.println(pulseSensor.getBeatsPerMinute());
40 }
41}| 関数 | 動作 |
|---|---|
| analogInput(ピン) | センサーをつないだアナログピンを選びます(初期値はA0) |
| blinkOnPulse(ピン) | 信号がしきい値を超えている間、つまり拍動のたびにこのピンのLEDを点灯します |
| fadeOnPulse(ピン) | 拍動のたびにPWMピンのLEDを点灯させ、ゆっくり消していきます |
| setThreshold(値) | しきい値の初期値。拍動が見つからないと、しきい値はこの値に戻ります |
| begin() | サンプリングを開始します。成功するとtrueを返します |
| sawStartOfBeat() | 前回の呼び出し以降に新しい拍動が始まっていればtrueを返します |
| getBeatsPerMinute() | 直近10回の拍動間隔の平均から計算したBPM |
| getInterBeatIntervalMs() | 直近2回の拍動の間隔(IBI、ミリ秒) |
| getLatestSample() | 最新の生の読み取り値(0〜1023) |
| isInsideBeat() | 信号がしきい値を超えている間trueを返します |
ライブラリはタイマー割り込みを使い、バックグラウンドで2 msごと(1秒間に500回)にセンサーを読み取ります。そのため、loop()で時間のかかる処理をしてもサンプリングは安定します。拍動のたびに波の山と谷を記録し、しきい値をちょうどその中間に移します。つまりsetThreshold()に渡す値は出発点にすぎません。前回の拍動から250 ms以内、または直前の拍動間隔の5分の3が過ぎる前に来た立ち上がりは無視するので、重複切痕が2回目の拍動として数えられることはありません。BPMは直近10回の拍動間隔の平均から計算されます。2.5秒間拍動が見つからないとすべてがリセットされ、getBeatsPerMinute()は0を返します。
しきい値の初期値は、生の信号を見て決めます。指を置いていないときに線が達する最大値より30〜50カウント上が良い出発点です(ほとんどのセンサーで530〜600)。しきい値が低すぎると指がなくてもノイズが拍動として数えられ、高すぎると弱い脈拍を見逃します。もう一つ注意点があります。Arduino UnoではライブラリがサンプリングにTimer1を使うため、9番と10番ピンではanalogWrite()が動きません。プロジェクトにServoライブラリを追加するとライブラリはTimer2に切り替わり、今度は3番と11番ピンのPWMとtone()関数が影響を受けます。
正確に測るには:指の押さえ方、環境光、動き
脈拍の測定を乱す原因のほとんどは、コードではなくセンサーの当て方にあります。シリアルプロッタを開いたまま、下のポイントを一つずつ試してみてください。それぞれで波がどう変わるかを見るのが、センサーを理解するいちばんの近道です。
- 指の押さえ方:軽く、でも一定の力で押さえてください。触れ方が軽すぎると指のわずかな動きが波より大きなノイズになり、強く押しすぎると毛細血管がつぶれて血流が減り、波が平らになったり消えたりします。いちばん良い結果が出るのは、面ファスナーのバンドでセンサーを指に巻き付ける方法です。
- 環境光:日光や照明の光は横からセンサーに入り込みます。50 Hzの商用電源では多くの照明が1秒間に100回ちらつき(60 Hzでは120回)、このちらつきが信号に混ざります(日本では東日本が50 Hz、西日本が60 Hzです)。センサーのまわりを指で完全に覆うか、上から暗い色のバンドを巻いてください。
- 動き:ケーブルの揺れ、話すこと、指を動かすこと、歩くことは、脈拍よりずっと大きな信号を生みます。測定中は手を机に置いてじっとしていてください。
- 冷たい手:指が冷えると血管が細くなり、信号が弱くなります。手を温めるか、センサーを耳たぶに挟んでください。耳たぶは動きの影響も受けにくい場所です。
- 汗と皮脂:湿った皮膚は光の散乱のしかたが変わり、センサーの表面も汚れます。表面に透明な保護シールを貼り、ときどき乾いた布でセンサーを拭いてください。
- 安定するまでの時間:指を置いてから最初の5〜10秒は、信号が落ち着き、平均値がたまっていく途中です。この間に表示される値は無視してください。
OLEDに心拍数を表示する
机の上ならシリアルプロッタで十分ですが、持ち運べる心拍計には小さな画面が必要です。下のコードは、0.96インチのSSD1306 OLEDの上部に、大きな数字のBPMと拍動のたびに大きくなるハートのアイコンを表示し、下部には左から右へ流れるリアルタイムの脈波を描きます。OLEDはUnoのA4(SDA)とA5(SCL)ピンに、電源は5VとGNDにつなぎます。ディスプレイのライブラリのインストールとアドレスの設定は、OLEDディスプレイ(SSD1306)の使い方の記事で詳しく説明しています。
1// Pulse Sensor + 0,96" OLED: BPM, kalp simgesi ve nabız dalgası / BPM, heart icon and pulse wave
2#include <Wire.h>
3#include <Adafruit_GFX.h>
4#include <Adafruit_SSD1306.h>
5#include <PulseSensorPlayground.h>
6
7Adafruit_SSD1306 display(128, 64, &Wire, -1); // 128x64, reset pini yok / no reset pin
8PulseSensorPlayground pulseSensor;
9
10const int PULSE_PIN = A0; // sensörün S pini / sensor S pin
11const int THRESHOLD = 550; // başlangıç eşiği / starting threshold
12const int GRAPH_TOP = 24; // dalga alanının üst kenarı / top edge of the wave area
13const int GRAPH_H = 40; // dalga alanının yüksekliği / height of the wave area
14
15int x = 0; // tarama sütunu (0-127) / sweep column (0-127)
16int lastY = GRAPH_TOP + GRAPH_H / 2; // önceki noktanın y'si / y of the previous point
17int lo = 1023, hi = 0; // bu taramadaki en küçük/büyük değer / min/max in this sweep
18int scaleLo = 400, scaleHi = 624; // çizimde kullanılan aralık / range used for drawing
19unsigned long lastDraw = 0, beatTime = 0;
20
21// İki daire ve bir üçgenle kalp çiz / draw a heart from two circles and a triangle
22void drawHeart(int cx, int cy, bool big) {
23 int r = big ? 6 : 4; // atış anında büyük kalp / bigger heart on a beat
24 display.fillCircle(cx - r, cy - r / 2, r, SSD1306_WHITE);
25 display.fillCircle(cx + r, cy - r / 2, r, SSD1306_WHITE);
26 display.fillTriangle(cx - 2 * r, cy, cx + 2 * r, cy, cx, cy + 2 * r, SSD1306_WHITE);
27}
28
29void setup() {
30 Serial.begin(115200);
31 if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { // I2C adresi 0x3C / I2C address 0x3C
32 Serial.println(F("SSD1306 allocation failed"));
33 while (true) {}
34 }
35 display.clearDisplay();
36 pulseSensor.analogInput(PULSE_PIN);
37 pulseSensor.setThreshold(THRESHOLD);
38 pulseSensor.begin(); // örnekleme kesmeyle sürer / sampling runs in an interrupt
39}
40
41void loop() {
42 if (pulseSensor.sawStartOfBeat()) beatTime = millis(); // atış anını sakla / store the beat time
43
44 if (millis() - lastDraw < 40) return; // saniyede 25 kare / 25 frames per second
45 lastDraw = millis();
46
47 // Alt bölüm: soldan sağa taranan nabız dalgası / bottom: pulse wave swept left to right
48 int s = pulseSensor.getLatestSample();
49 if (s < lo) lo = s;
50 if (s > hi) hi = s;
51 int y = map(constrain(s, scaleLo, scaleHi), scaleLo, scaleHi, GRAPH_TOP + GRAPH_H - 1, GRAPH_TOP);
52 display.drawFastVLine(x, GRAPH_TOP, GRAPH_H, SSD1306_BLACK); // eski sütunu sil / erase the old column
53 display.drawFastVLine((x + 1) % 128, GRAPH_TOP, GRAPH_H, SSD1306_BLACK); // önündeki boşluk / gap ahead
54 if (x > 0) display.drawLine(x - 1, lastY, x, y, SSD1306_WHITE);
55 lastY = y;
56 if (++x >= 128) { // tarama bitti: ölçeği yenile / sweep done: refresh the scale
57 x = 0;
58 if (hi - lo > 20) { scaleLo = lo; scaleHi = hi; }
59 lo = 1023;
60 hi = 0;
61 }
62
63 // Üst bölüm: kalp ve BPM / top: heart and BPM
64 display.fillRect(0, 0, 128, GRAPH_TOP, SSD1306_BLACK);
65 drawHeart(14, 10, millis() - beatTime < 150);
66 int bpm = pulseSensor.getBeatsPerMinute();
67 display.setTextColor(SSD1306_WHITE);
68 display.setTextSize(3);
69 display.setCursor(40, 0);
70 if (bpm > 0) display.print(bpm);
71 else display.print(F("--")); // parmak yok / no finger
72 display.setTextSize(1);
73 display.setCursor(104, 14);
74 display.print(F("BPM"));
75 display.display(); // tamponu ekrana gönder / push the buffer to the screen
76}このコードは画面を1秒間に25回更新し、1フレームごとに波に1点を加えるので、画面の幅で約5秒分の脈拍が表示されます。波が画面の端まで来るたびに、その間の最小値と最大値に合わせて波のスケールを調整し直すので、信号が弱くても強くても画面に収まります。I2Cでディスプレイに1フレームを送るには25〜30 msほどかかりますが、ライブラリは割り込みでサンプリングしているので、その間に拍動を取りこぼすことはありません。Unoではディスプレイのバッファが1 KBのRAMを使います。コードは波を配列に保存せずに直接画面に描くのでメモリは足りますが、空きは450バイトほどしか残りません。
KY-039で心拍数を測る
KY-039では、フォトトランジスタが抵抗と直列につながっていて、受け取る光の量によって出力電圧が変わります。光が多いと電圧は低く、少ないと高くなります。間にアンプもフィルタもありません。指をのせた状態でも、1回の心拍でこの電圧が動くのはたいていADC値で2〜10カウント、約10〜50 mVにすぎず、環境光のわずかな変化のほうがそれより大きくなります。そのため、Pulse Sensor用に書いたコードや550のような固定しきい値はKY-039では動きません。まず信号をきれいにし、それから波そのものの大きさに合わせて変わるしきい値を使う必要があります。
下のコードはこれを4つのステップで行います。まず20 msのあいだアナログピンを休みなく読み続け(約175回)、その平均をとります。これでランダムなノイズが減り、20 msはちょうど50 Hzの商用電源の1周期なので、照明のちらつきもほぼ完全に打ち消されます。次に軽いローパスフィルタで信号をなめらかにし、とてもゆっくりした移動平均でDCレベルを取り除いて、脈波だけを残します。3つ目のステップでは、波の山と谷を追いかける2本のエンベロープ(包絡線)を計算し、しきい値をその中間に置きます。最後に、信号がしきい値を下から上へ越えたときに拍動として数え、直近8回の拍動間隔の平均からBPMを求めます。
1// KY-039 ile nabız ölçme (kütüphanesiz) / Heart rate with the KY-039 (no library)
2const int SENSOR_PIN = A0; // KY-039 S pini / KY-039 S pin
3const int LED_PIN = LED_BUILTIN; // atışta yanan LED / LED lit on each beat
4const bool PLOTTER = true; // true: Seri Çizici, false: Seri Monitör / true: Serial Plotter, false: Serial Monitor
5const unsigned long WINDOW_US = 20000; // 20 ms = 50 Hz şebekenin bir periyodu / one period of 50 Hz mains
6const float MIN_AMP = 1.2; // tepe-tepe gürültü sınırı (ADC birimi) / peak-to-peak noise limit (ADC units)
7const int AVG_BEATS = 8; // BPM için ortalanan aralık sayısı / intervals averaged for BPM
8
9float smooth, baseline; // süzülmüş sinyal ve DC seviye / filtered signal and DC level
10float posEnv = 0, negEnv = 0; // tepe ve çukur zarfları / peak and trough envelopes
11bool armed = true; // yeni atışa hazır mı / ready for a new beat
12unsigned long lastBeat = 0; // son atışın zamanı / time of the last beat
13unsigned int ibi[AVG_BEATS]; // atışlar arası süreler (ms) / inter-beat intervals (ms)
14int ibiCount = 0, ibiIndex = 0, bpm = 0;
15
16// 20 ms boyunca okuyup ortalama al / read for 20 ms and average
17float readWindow() {
18 unsigned long start = micros();
19 long sum = 0;
20 int n = 0;
21 while (micros() - start < WINDOW_US) {
22 sum += analogRead(SENSOR_PIN);
23 n++;
24 }
25 return (float)sum / n;
26}
27
28float averageIbi() {
29 if (ibiCount == 0) return 500.0; // geçmiş yoksa 120 BPM varsay (ilk bekleme 350 ms) / assume 120 BPM without history (first wait 350 ms)
30 unsigned long sum = 0;
31 for (int i = 0; i < ibiCount; i++) sum += ibi[i];
32 return (float)sum / ibiCount;
33}
34
35void addInterval(unsigned int ms) {
36 ibi[ibiIndex] = ms; // halka tampon / ring buffer
37 ibiIndex = (ibiIndex + 1) % AVG_BEATS;
38 if (ibiCount < AVG_BEATS) ibiCount++;
39 bpm = (int)(60000.0 / averageIbi() + 0.5);
40}
41
42void setup() {
43 Serial.begin(115200);
44 pinMode(LED_PIN, OUTPUT);
45 smooth = baseline = readWindow(); // başlangıç seviyesi / starting level
46}
47
48void loop() {
49 float x = readWindow(); // saniyede 50 örnek / 50 samples per second
50 unsigned long now = millis();
51
52 smooth += (x - smooth) * 0.5; // hafif alçak geçiren süzgeç / light low-pass filter
53 baseline += (smooth - baseline) * 0.05; // yavaş DC takibi (~0,4 s) / slow DC tracking (~0.4 s)
54 float ac = smooth - baseline; // yalnız nabız dalgası / pulse wave only
55
56 // Zarflar: uçları hemen yakala, yavaşça bırak / envelopes: catch extremes fast, release slowly
57 posEnv = (ac > posEnv) ? ac : posEnv * 0.99;
58 negEnv = (ac < negEnv) ? ac : negEnv * 0.99;
59 float amp = posEnv - negEnv; // tepe-tepe genlik / peak-to-peak amplitude
60 float mid = (posEnv + negEnv) / 2; // eşik: dalganın ortası / threshold: middle of the wave
61
62 unsigned long sinceBeat = now - lastBeat;
63 float refractory = constrain(averageIbi() * 0.7, 250.0, 600.0); // dikrotik çentiği atla / skip the dicrotic notch
64
65 if (armed && amp > MIN_AMP && ac > mid && sinceBeat > refractory) {
66 armed = false; // atış bulundu / beat found
67 digitalWrite(LED_PIN, HIGH);
68 if (sinceBeat < 2000) addInterval(sinceBeat); // 30 BPM'den yavaşı alma / ignore gaps slower than 30 BPM
69 lastBeat = now;
70 if (!PLOTTER && bpm > 0) {
71 Serial.print(F("BPM: "));
72 Serial.println(bpm);
73 }
74 }
75 if (!armed && ac < mid - amp * 0.25) { // dalga yeterince indi: yeniden hazır / wave fell enough: re-arm
76 armed = true;
77 digitalWrite(LED_PIN, LOW);
78 }
79 if (sinceBeat > 2500 && ibiCount > 0) { // 2,5 s atış yok: sıfırla / no beat for 2.5 s: reset
80 ibiCount = 0;
81 bpm = 0;
82 }
83
84 if (PLOTTER) {
85 Serial.print(F("Nabiz:"));
86 Serial.print(ac);
87 Serial.print(F(",Esik:"));
88 Serial.println(mid);
89 }
90}| 定数・値 | 役割 |
|---|---|
| WINDOW_US = 20000(平均化の時間幅) | 平均化の時間幅。50 Hzの照明のちらつきを打ち消します。60 Hzの地域では16667を使います(日本では西日本が該当) |
| 0.5(smooth、平滑化) | 軽いローパスフィルタ。急な跳びをなめらかにします |
| 0.05(baseline、ベースライン) | 約0.4秒でDCレベルに追従し、指の押さえ方のゆっくりした変化を取り除きます |
| 0.99(エンベロープ) | 山と谷のエンベロープが戻る速さ(約2秒) |
| MIN_AMP = 1.2(最小振幅) | ピークツーピークの振幅がこれより小さい波はノイズとみなします |
| refractory(不応期) | 直近の平均間隔の70%(250〜600 ms。まだ履歴がないあいだは、速い脈拍を見逃さないよう350 ms)。重複切痕が2回目の拍動として数えられるのを防ぎます |
| mid - amp * 0.25(再検出レベル) | 再び拍動を探し始めるレベル。波が十分に下がるまで次の拍動は探しません |
コードを書き込んでシリアルプロッタを開くと、2本の線が表示されます。Nabiz(トルコ語で「脈拍」)はきれいにした波、Esik(「しきい値」)はその中央を通る適応しきい値です。指を置いていないときにNabizの線がどのくらい動くかを確認し、MIN_AMPをそのピークツーピーク値より少し上に設定してください。信号がとても弱い場合は下げます。信号をなめらかにする方法について詳しくは、センサーデータのフィルタリングの記事を参照してください。同じコードはPulse Sensorでも動きます。その場合はMIN_AMPを20程度まで上げるだけです。
ESP32で脈拍センサーを使う
ESP32では、センサーの電源を3V3ピンから取り、信号をGPIO 34につなぎます。このピンはADC1グループに属します。Wi-FiがオンのあいだはADC2のピンを読めないので、脈拍のデータをインターネットで送るなら、ADC1のピン(GPIO 32〜39)から選ぶ必要があります。ESP32のADCは12ビット(0〜4095)で、3.3 V電源ではPulse Sensorの待機時の値は2000前後になります。
PulseSensorPlaygroundの最近のバージョンはESP32にも対応していますが、ライブラリはESP32ボードパッケージの3.xのタイマー関数を使っているので、2.xではコンパイルできません。サンプルではanalogReadResolution(10)でADCを10ビットに下げているので、しきい値はUnoと同じ0〜1023のスケールで指定します(ESP32のサンプルでは685)。バージョンの違いに悩まされないよう、下にKY-039のセクションと同じアルゴリズムを使ったライブラリなしのスケッチを載せます。ボードパッケージのどちらのバージョンでも動きます。
1// ESP32 + Pulse Sensor: kütüphanesiz BPM / BPM without a library
2const int SENSOR_PIN = 34; // ADC1 pini, Wi-Fi açıkken de çalışır / ADC1 pin, works with Wi-Fi on
3const int LED_PIN = 2; // çoğu DevKit'te dahili LED / built-in LED on most DevKits
4const unsigned long PERIOD_MS = 10; // saniyede 100 örnek / 100 samples per second
5const float MIN_AMP = 80; // 12 bitte tepe-tepe gürültü sınırı / peak-to-peak noise limit in 12-bit units
6const int AVG_BEATS = 8; // BPM için ortalanan aralık sayısı / intervals averaged for BPM
7
8float smooth, baseline, posEnv = 0, negEnv = 0;
9bool armed = true;
10unsigned long lastSample = 0, lastBeat = 0;
11unsigned long ibi[AVG_BEATS];
12int ibiCount = 0, ibiIndex = 0, bpm = 0;
13
14// 8 okumanın ortalaması: ESP32 ADC'si gürültülüdür / average of 8 reads: the ESP32 ADC is noisy
15float readSensor() {
16 long sum = 0;
17 for (int i = 0; i < 8; i++) sum += analogRead(SENSOR_PIN);
18 return sum / 8.0;
19}
20
21float averageIbi() {
22 if (ibiCount == 0) return 500.0; // geçmiş yoksa 120 BPM varsay (ilk bekleme 350 ms) / assume 120 BPM without history (first wait 350 ms)
23 unsigned long sum = 0;
24 for (int i = 0; i < ibiCount; i++) sum += ibi[i];
25 return (float)sum / ibiCount;
26}
27
28void setup() {
29 Serial.begin(115200);
30 pinMode(LED_PIN, OUTPUT);
31 analogReadResolution(12); // 0-4095 arası değer / values from 0 to 4095
32 smooth = baseline = readSensor();
33 lastSample = millis();
34}
35
36void loop() {
37 if (millis() - lastSample < PERIOD_MS) return;
38 lastSample += PERIOD_MS; // sabit aralıkla örnekle / sample at a fixed interval
39 unsigned long now = millis();
40
41 float x = readSensor();
42 smooth += (x - smooth) * 0.3; // alçak geçiren süzgeç (~6 Hz) / low-pass filter (~6 Hz)
43 baseline += (smooth - baseline) * 0.02; // DC takibi (~0,5 s) / DC tracking (~0.5 s)
44 float ac = smooth - baseline;
45
46 posEnv = (ac > posEnv) ? ac : posEnv * 0.995; // tepe zarfı / peak envelope
47 negEnv = (ac < negEnv) ? ac : negEnv * 0.995; // çukur zarfı / trough envelope
48 float amp = posEnv - negEnv;
49 float mid = (posEnv + negEnv) / 2;
50
51 unsigned long sinceBeat = now - lastBeat;
52 float refractory = constrain(averageIbi() * 0.7, 250.0, 600.0);
53
54 if (armed && amp > MIN_AMP && ac > mid && sinceBeat > refractory) {
55 armed = false;
56 digitalWrite(LED_PIN, HIGH);
57 if (sinceBeat < 2000) { // 30-240 BPM aralığı / 30-240 BPM range
58 ibi[ibiIndex] = sinceBeat;
59 ibiIndex = (ibiIndex + 1) % AVG_BEATS;
60 if (ibiCount < AVG_BEATS) ibiCount++;
61 bpm = (int)(60000.0 / averageIbi() + 0.5);
62 Serial.printf("BPM: %d IBI: %lu ms\n", bpm, sinceBeat);
63 }
64 lastBeat = now;
65 }
66 if (!armed && ac < mid - amp * 0.25) {
67 armed = true;
68 digitalWrite(LED_PIN, LOW);
69 }
70 if (sinceBeat > 2500 && ibiCount > 0) { // parmak çekildi / finger removed
71 ibiCount = 0;
72 bpm = 0;
73 Serial.println("Parmak yok / no finger");
74 }
75}このコードでは信号を1秒間に100回読み取り、ESP32のADCはノイズが多いので、各サンプルを8回の読み取りの平均にしています。しきい値とエンベロープの値は12ビットのスケールに合わせてあるので、MIN_AMPは80(約65 mV)です。ESP32の本当の強みはWi-Fiです。WebSocketでBPMをWebページにリアルタイム配信したり、脈拍が上限を超えたらTelegramでメッセージを送ったり、測定値をGoogleスプレッドシートに記録したりできます。
Raspberry Pi PicoとMicroPythonで心拍数を測る
Raspberry Pi Picoでは、センサーの+ピンを3V3(OUT)(36番ピン)に、−をGND(38番ピン)に、SをGP26(ADC0、31番ピン)につなぎます。MicroPythonのread_u16()は0〜65535の値を返します。コードはこれを4ビット右にシフトしてESP32と同じ0〜4095のスケールに落とすので、どちらのボードでもしきい値の値は同じままです。Thonnyでコードをmain.pyとしてPicoに保存すると、ボードの電源を入れるたびに自動で実行されます。
1# Raspberry Pi Pico + Pulse Sensor: MicroPython ile BPM / BPM with MicroPython
2from machine import ADC, Pin
3import time
4
5sensor = ADC(26) # GP26 = ADC0, sensörün S pini / sensor S pin
6led = Pin("LED", Pin.OUT) # kartın üzerindeki LED / on-board LED
7
8PERIOD_MS = 10 # saniyede 100 örnek / 100 samples per second
9MIN_AMP = 80 # 12 bitte tepe-tepe gürültü sınırı / peak-to-peak noise limit (12-bit)
10AVG_BEATS = 8 # BPM için ortalanan aralık / intervals averaged for BPM
11
12def read12():
13 # 4 okumanın ortalaması, 0-4095 / average of 4 reads, 0-4095
14 s = 0
15 for _ in range(4):
16 s += sensor.read_u16() >> 4
17 return s / 4
18
19smooth = baseline = read12()
20pos_env = neg_env = 0.0
21armed = True
22ibis = []
23bpm = 0
24last_beat = time.ticks_ms()
25next_t = time.ticks_ms()
26
27while True:
28 while time.ticks_diff(time.ticks_ms(), next_t) < 0:
29 pass # bir sonraki örnek zamanını bekle / wait for the next sample time
30 next_t = time.ticks_add(next_t, PERIOD_MS)
31 now = time.ticks_ms()
32
33 x = read12()
34 smooth += (x - smooth) * 0.3 # alçak geçiren süzgeç / low-pass filter
35 baseline += (smooth - baseline) * 0.02 # DC takibi / DC tracking
36 ac = smooth - baseline
37
38 pos_env = ac if ac > pos_env else pos_env * 0.995
39 neg_env = ac if ac < neg_env else neg_env * 0.995
40 amp = pos_env - neg_env
41 mid = (pos_env + neg_env) / 2
42
43 since = time.ticks_diff(now, last_beat)
44 avg_ibi = sum(ibis) / len(ibis) if ibis else 500 # geçmiş yoksa 120 BPM varsay / assume 120 BPM without history
45 refractory = min(max(avg_ibi * 0.7, 250), 600) # dikrotik çentiği atla / skip the dicrotic notch
46
47 if armed and amp > MIN_AMP and ac > mid and since > refractory:
48 armed = False
49 led.on()
50 if since < 2000: # 30 BPM'den yavaşı alma / ignore gaps slower than 30 BPM
51 ibis.append(since)
52 if len(ibis) > AVG_BEATS:
53 ibis.pop(0)
54 bpm = round(60000 / (sum(ibis) / len(ibis)))
55 print("BPM:", bpm, "IBI:", since)
56 last_beat = now
57 if not armed and ac < mid - amp * 0.25:
58 armed = True
59 led.off()
60 if since > 2500 and ibis: # parmak çekildi / finger removed
61 ibis = []
62 bpm = 0
63 print("Parmak yok / no finger")このコードはESP32版と同じ考え方で動きます。10 msごとにサンプルを取り、DCレベルを取り除き、適応しきい値で拍動を見つけ、直近8回の間隔の平均からBPMを計算します。拍動のたびに基板上のLEDが点灯し、BPMと拍動間隔がThonnyのShellウィンドウに表示されます。Pythonの文法が初めてなら、MicroPythonの基本の記事から始めるとよいでしょう。
MAX30102との違い:血中酸素(SpO2)の測定
MAX30102は、Analog Devices(旧Maxim Integrated)が心拍数と血中酸素飽和度の測定用に設計した1チップのセンサーです。内部には赤(660 nm)と赤外線(880 nm)の2つのLED、フォトダイオード、環境光キャンセル回路、18ビットADCが入っています。測定はセンサー自身が行い、サンプルを32サンプル分のメモリ(FIFO)にため、I2Cバス(アドレス0x57)で数値として送ります。アナログの配線が拾うノイズやArduinoの10ビットADCの制限が関係なくなるので、信号はPulse Sensorよりずっときれいです。
SpO2測定の秘密は、2色の光を使うことにあります。酸素を運んでいるヘモグロビンは赤外線を多く、赤色光を少なく吸収し、酸素を手放したヘモグロビンはその逆になります。センサーは両方の光について脈波のAC成分とDC成分を測り、R = (AC_red / DC_red) / (AC_infrared / DC_infrared)という比を計算します。SpO2はこの比から経験的な校正曲線を使って求めます。Pulse SensorとKY-039は1色の光しか使わないのでこの比を計算できず、つまり酸素は測れません。MAX30102でいちばん人気のArduinoライブラリはSparkFun MAX3010xです。そのサンプルのうち、Example5_HeartRateは心拍数を、Example8_SPO2は酸素飽和度を計算します。
脈拍センサーは医療機器ではありません
これには具体的な理由があります。医療用の心拍計やパルスオキシメーターは決められた精度の範囲に校正され、さまざまな肌の色、血流が少ない状態、体を動かしている状態で試験されています。趣味用のモジュールや私たちが書くアルゴリズムは、こうした試験を受けていません。また、脈拍を測ることは心臓のリズムを評価することと同じではありません。リズムの異常を知るには、心臓の電気信号を記録する心電図(ECG)が必要です。一般的な情報として、成人の安静時の心拍数はふつう1分間に60〜100回で、定期的に運動している人では40〜60まで下がることがあります。自分の測定値がこの範囲から外れたら、まずセンサーとコードを確認してください。それでも心配なら医師に相談してください。
脈拍センサーのプロジェクトアイデア
- 心臓と一緒に脈打つライト:拍動のたびにWS2812B LEDリングを赤く光らせ、ゆっくり消します。BPMが上がるにつれて色を青から赤へ変えていきます。
- 音の出る心拍計:sawStartOfBeat()がtrueを返すたびにブザーで短いビープ音を鳴らします。病院のモニターの音をまねた楽しいプロジェクトです。
- 持ち運べる心拍計:OLEDのコードを電池と充電回路と一緒にケースに収めます。測定が終わったら画面を消すと電池が長持ちします。
- 運動後の回復テスト:運動を終えた直後に測り、最初の1分間で脈拍が何拍下がるかを計算します。毎日記録して進歩を追いかけましょう。
- 心拍データロガー:BPMの値をタイムスタンプ付きでSDカードに書き込むか、Excelに送ってグラフを描きます。
- 呼吸トレーニングゲーム:ゆっくり深く呼吸すると脈拍がどう変わるかを画面に表示します。脈拍を目標値より下げられたら勝ちです。
よくある失敗と直し方
| 症状 | 考えられる原因 | 対処 |
|---|---|---|
| BPMがいつも0または「--」 | 拍動を検出できていない:しきい値が高すぎる、指がない、またはSピンが違う入力につながっている | 生の信号のスケッチで波を確認し、しきい値を波の中央に移す |
| BPMが150〜220のように非常に高い | ノイズや重複切痕が2回目の拍動として数えられている | しきい値を上げ、指を安定させ、環境光を遮る |
| 指がなくてもBPMが表示される | しきい値が待機時のノイズより低い、照明のちらつきが信号に混ざっている | しきい値を待機時の最大値より30〜50カウント上にし、センサーを覆う |
| 信号が0または1023に張り付いて平らになる | +と−が逆、断線、Sピンの位置違い、またはセンサーの故障 | 表と照らし合わせて配線を確認する。ESP32とPicoでは3V3を使う |
| 波は出るが途切れてばかりいる | 指が動いている、または押さえる力が変わっている | 面ファスナーのバンドでセンサーを巻き、手を机に置いてじっとする |
| KY-039でまったく波が出ない | LEDがフォトトランジスタの上に曲げられていない、または強い環境光でフォトトランジスタが飽和している | LEDを指の上に来るように曲げ、日陰でセンサーを使い、MIN_AMPを下げる |
| ESP32で「too few arguments to function 'hw_timer_t* timerBegin(uint8_t, uint16_t, bool)'」(関数の引数が少なすぎる) | PulseSensorPlaygroundをESP32ボードパッケージの2.xでコンパイルしている | ボードパッケージを3.xに更新するか、この記事のライブラリなしのESP32コードを使う |
| 9番または10番ピンでanalogWrite()が動かない | UnoではライブラリがTimer1を使っている | PWMには3、5、6、11番ピンを使う |
脈拍センサーは、アナログ読み取り、信号のフィルタリング、タイマー割り込み、リアルタイムのグラフ表示を、1本のアナログピンだけで一度に学べる数少ないプロジェクトの一つです。しかも結果を自分の体で確かめられます。光も測る初心者向けプロジェクトに進みたいなら、LDR光センサーが次の一歩にぴったりです。BlueGraysアプリには、LDRで光の強さを読み取るプロジェクト、ブザー、OLEDディスプレイを使ったESP32のプロジェクト、PicoでのPWMによる呼吸するLEDなど、この記事の部品を使ったプロジェクトが、ステップごとの回路図とコード付きでそろっています。
よくある質問
Arduinoの脈拍センサーはどのくらい正確ですか?
しっかり当てて、じっと座っていれば、Pulse Sensorはたいてい安静時の心拍数を数BPMの誤差で測れます。動き、環境光、冷たい手、指の押さえ方によって誤差はすぐに大きくなります。KY-039は信号がずっと弱いので、値がもっと大きく揺れます。どのセンサーも医療用の測定には向いていません。
Pulse SensorとKY-039の違いは何ですか?
Pulse Sensorは緑色LEDと反射型の方式を使い、基板上のオペアンプで信号を増幅・フィルタ処理し、待機時は電源電圧の半分の値を出します。KY-039は指を通り抜けた赤外線を測り、基板にアンプがなく、出力はADC値で数カウントしか動きません。そのため、Pulse Sensorは既製のライブラリで簡単に動きますが、KY-039にはフィルタ処理と適応しきい値が必要です。
脈拍センサーで血中酸素(SpO2)は測れますか?
Pulse SensorやKY-039では測れません。SpO2の計算は、赤と赤外線という2つの異なる波長の光を比べることに基づいているからです。そのためにはMAX30102のように2つのLEDを持つセンサーが必要です。MAX30102の測定値でさえ校正されていない推定値であり、医療用のパルスオキシメーターの代わりにはなりません。
脈拍センサーの値が非常に高くなったり跳んだりするのはなぜですか?
いちばん多い原因は、ノイズや脈波の重複切痕が2回目の拍動として数えられていることです。この場合、BPMは実際の値のおよそ2倍になります。しきい値を上げ、面ファスナーのバンドで指を固定し、センサーを光から遮ってください。シリアルプロッタで生の波を見れば、問題の原因がすぐにわかります。
Pulse SensorはESP32やRaspberry Pi Picoでも使えますか?
はい。ただし、どちらのボードもアナログ入力が3.3 Vを超える電圧を受けられないので、センサーの電源は3V3ピンから取ってください。ESP32ではWi-Fiとぶつからないように、GPIO 34のようなADC1のピンを使い、PicoではGP26、GP27、GP28を使います。PulseSensorPlaygroundにはESP32ボードパッケージの3.xが必要で、MicroPythonではこの記事のように自分でアルゴリズムを書きます。
正常な心拍数はどのくらいですか?
成人の安静時の心拍数はふつう1分間に60〜100回で、定期的に運動している人では40〜60のこともあり、子どもではもっと高くなります。脈拍は運動、ストレス、カフェイン、発熱で自然に上がります。これは一般的な情報です。自分の測定値や症状については医師に相談してください。
この記事を読んだ後に作れるプロジェクト
全プロジェクトを見る
LDRで光の強さを測定
部屋の明るさを数値で見てみませんか?光に反応する小さな部品、LDRで周囲の光を測定し、Arduinoが0.5秒ごとにその値をコンピューターの画面に表示します。手で覆うと数値が変わる様子を確認できます。自動で点灯する常夜灯などのプロジェクトへの第一歩です。

ESP32とLDRモジュールで光の強さを読み取る
LDRモジュールの生のADC値と、暗い状態・明るい状態の基準値から計算した相対的な明るさの割合を、シリアルモニタでリアルタイムに確認します。この割合はルクスの測定値ではありません。モジュールの校正の仕組みを学べます。しきい値の決定に使うため、プロジェクト10の前に取り組むのが適しています。

ブザーの使い方
呼び鈴やアラームがどうやって音を出すのか、気になったことはありませんか?Arduinoが小さなブザーを短い間隔で鳴らし、音を出す、止める、を繰り返します。電気で音が生まれる仕組みを学び、メロディーを演奏するプロジェクトへの第一歩を踏み出せます。

DHT11で温度と湿度を測定
部屋の温度は何度で、空気は湿っているでしょうか、それとも乾燥しているでしょうか?DHT11センサーが温度と湿度を測定し、Arduinoが2行のLCDディスプレイに値を表示して、数秒ごとに更新します。センサーに異常があればエラーメッセージが表示されます。自分だけの小さな気象観測装置を作る第一歩です。

LCDディスプレイ付きデジタル電圧計
自作の測定器で、電池や電源アダプターが出力する電圧を調べます。2つの抵抗で作る分圧回路が、高い電圧をボードで読み取れるレベルまで下げます。Arduinoが元の電圧を計算し、2行のLCDディスプレイにボルト単位で表示します。この回路では約55ボルトまでの直流電圧を測定できます。コンセントの商用電源には絶対に接続しないでください。

ESP32 HTTPClient+天気API(SSD1306 OLED付き)
このプロジェクトでは、ESP32でHTTPClient.hを使い、インターネット経由で天気API(wttr.in)をREST APIとして呼び出します。前のI2C LCD版とは異なり、SSD1306 OLED(128×64)ディスプレイを使うため、大きな文字やベクターアイコンで、1つの画面に豊かな表現ができます。天気を表す7種類のベクターアイコン(晴れ、晴れ時々曇り、曇り、雨、雪、嵐、霧)を実行時に描画するので、ビットマップファイルは不要です。気温は中央に3倍の大きさの文字で表示します。

ESP32のディープスリープで節電(OLED + RTC RAMカウンター)
ESP32のディープスリープモードは、CPUとほとんどの周辺機器を停止し、消費電流を約10 µAまで抑えます。電池で動くIoT機器に欠かせない機能です。このプロジェクトでは、タイマー(15秒)、外部ピン(EXT0、ボタンはGPIO 33)、タッチ(T0/GPIO 4)の3種類の復帰方法を使います。RTC_DATA_ATTRを付けた変数はディープスリープ中も保持され、起動回数、タイマーによる復帰回数、ボタンによる復帰回数を継続して記録できます。復帰するたびにSSD1306 OLEDディスプレイに復帰理由と回数を表示します。

Raspberry Pi PicoのPWMでLEDをフェード(呼吸のような明滅)
LEDを単に点滅させるだけでなく、呼吸するように明滅させます!Raspberry Pi PicoのPWM(パルス幅変調)を使い、スリープ中のMacBookのランプのように、明るさをゆっくり上げてからゆっくり下げます。machine.PWMクラス、duty_u16()(0–65535の16ビットの明るさ)、PWM周波数(freq(1000)で1秒間に1000回オン・オフし、ちらつきを感じさせない)、range()による段階的なループ(増加と減少)、デジタルピンからのアナログのような出力を学びます。PWMはRGBの混色やモーターなどに使われる、組み込み開発の基本技術です。
ほかの記事
BlueGraysアプリ
この回路をスマホでステップごとに作る
回路図、コード、部品リストを1つの画面で確認できます。Arduino、ESP32、Picoのプロジェクトを200件以上用意しています。

