/* ================================================================================ 多機能インターフェース実験キット for UIAPduino Pro Micro CH32V003 V1.4 © 2026 JH4VAJ ================================================================================ ■概要 スライドスイッチ(D4, D3)の2bit値に応じて、LED点滅、エンコーダ値の2進数表示、 メロディ再生などの各種モードを切り替えるノンブロッキング処理のプログラム。 ■ピンアサイン (デジタル/アナログ重複ピン明記) D0(A1) : ロータリエンコーダ A相 D1(A0) : ロータリエンコーダ B相 D11, D9, D8, D7, D5 : LED 5本(D11=MSB, D5=LSB)※アナログ機能なし D4, D3 : スライドスイッチ 2bit(D4=MSB, D3=LSB)※アナログ機能なし D2(build in LED) : ビルトインLED(生存確認用ハートビート) D12(A3) : プッシュスイッチ D10 : 圧電サウンダ ※アナログ機能なし A2(D6) : 可変抵抗 (ボリューム) ■モード一覧(スライドスイッチ2bit: D4, D3) 0b00 : 【手動パターンモード】プッシュSW押下時のみ配列パターンでLED点滅 0b01 : 【可変周期点滅モード】可変抵抗で調整した間隔で全LEDが点滅 0b10 : 【メロディ再生モード】圧電サウンダでThe Entertainerを発音(LED消灯) 0b11 : 【エンコーダ表示モード】エンコーダ値をLEDに2進数表示(切替時に0クリア) ■機能解説:ハートビート(生存確認)LEDについて ビルトインLED(D2)を用いて、プログラムがフリーズ(ハングアップ)せずに メインループが正常に回り続けていることを視覚的に確認するための機能です。 1サイクルの間に「1回、2回、3回、4回」と規則的なパルス点滅を常時繰り返します。 ■コンパイル Arduino IDE 2.3.8で動作を確認しています。 コンパイル環境の準備に関しては、UIAPduino Pro Micro CH32V003 V1.4 の公式サイトをご覧ください。 https://www.uiap.jp/uiapduino/pro-micro/ch32v003/v1dot4 なお、ボードへの書き込みは少々時間がかかります(数十秒かな?) ================================================================================ ■【重要】UIAPduino (CH32V003) 開発における注意事項と対策ノウハウ 1. tone()関数の連続呼び出しバグ(タイマー更新不良) - [事象] CH32コア環境で音が鳴っている最中に新しい周波数でtone()を上書きすると、 タイマーが正常にリセットされず波形が崩れ、甲高いノイズや倍音が発生する。 - [対策] 音程を切り替える直前、および同じ音を連続して鳴らす直前には、 必ず noTone() を呼び出してハードウェアタイマーを明示的に初期化する。 2. 浮動小数点演算(float)によるFlash容量の枯渇 - [事象] FPU(浮動小数点演算器)を持たないCH32V003でfloatや数学関数(log等)を 使用すると、エミュレーションライブラリがリンクされFlash(16KB)を溢れる。 - [対策] 疑似的な対数カーブなどは、符号なし32bit整数(unsigned long)の 二次関数などを用いた整数演算に置き換えて実装する。 3. D11ピン(PD1)のGPIO利用 - [事象] D11ピンはデフォルトでデバッグピン(SWDIO)として予約されており出力できない。 - [対策] setup()の先頭で pinV32_DisconnectDebug(PD_1); を実行し解放する。 4. シリアル通信とUSB機能の競合 - [事象] Serial.begin() を有効にすると、ハードウェアTX(PD5)が有効になり、 ソフトウェアUSB認識用のプルアップ抵抗制御と競合してUSB接続が切断される。 - [対策] デバッグ完了後は Serial 関連のコードをすべて無効化(コメントアウト)する。 ================================================================================ */ // =========================================================================== // ==== ハードウェア ピン定義 ==== // =========================================================================== // ロータリエンコーダ const int ENC_A = 0; // D0(A1) const int ENC_B = 1; // D1(A0) // 外部LED配列(MSB→LSBの順に定義。配列インデックス0が最上位ビット) const int LED_PINS[] = {11, 9, 8, 7, 5}; const int NUM_LEDS = sizeof(LED_PINS) / sizeof(LED_PINS[0]); // スライドスイッチ(MSB→LSB) const int SWITCH_PINS[] = {4, 3}; const int NUM_SWITCHES = sizeof(SWITCH_PINS) / sizeof(SWITCH_PINS[0]); // その他の入出力デバイス const int HEARTBEAT_LED = 2; // D2(build in LED) const int PUSH_SW = 12; // D12(A3) const int BUZZER_PIN = 10; // D10 const int POT_PIN = A2; // A2(D6) // =========================================================================== // ==== メロディ定義 (The Entertainer) ==== // =========================================================================== // 周波数(Hz)の配列。0は休符を表す。圧電サウンダの共振特性を考慮し、 // 低すぎず高すぎない標準的なオクターブ(C5〜B7)を使用。 const unsigned int entertainer_melody[] = { 0, 0, 587, 622, // 休 休 D5 D#5 659, 1046, 659, 1046, 659, 1046, // E5 C6 E5 C6 E5 C6 1046, 1046, 1046, 1175, 1244, // C6 C6 C6 D6 D#6 1318, 1046, 1175, 1318, 1318, 988, 1175,// E6 C6 D6 E6 E6 B5 D6 523 // C5 }; // 各音符の発音時間(ミリ秒)。テンポ=80BPM (4分音符=750ms) 基準。 const unsigned int entertainer_durations[] = { 0, 0, 188, 188, // (休4) (休8) 16 16 188, 375, 188, 375, 188, 188, // 16 8 16 8 16 16 750, 188, 188, 188, 188, // 4 16 16 16 16 188, 188, 188, 188, 188, 375, // 16 16 16 16 16 8 1125 // 付点4分 }; // メロディの総音符数 const unsigned int entertainer_length = sizeof(entertainer_melody) / sizeof(entertainer_melody[0]); // メロディ再生制御用の状態変数 unsigned int entertainerMelodyIdx = 0; // 現在再生中の音符インデックス unsigned long entertainerMelodyMillis = 0; // 現在の音符の再生開始時刻 bool entertainerMelodyPlaying = false; // 再生中フラグ uint8_t entertainerMelodyPrevSwBits = 0; // 前回のスイッチ状態(モード切替検知用) // =========================================================================== // ==== システム制御パラメータ & 状態変数 ==== // =========================================================================== // --- チャタリング防止(デバウンス)時間設定 --- const unsigned long ENCODER_DEBOUNCE_MS = 5; // エンコーダは高速なため5ms const unsigned long SWITCH_DEBOUNCE_MS = 10; // 機械式スイッチは10ms // --- 点滅パターン制御用 --- const unsigned long BLINK_PERIOD = 400; // 単純点滅の基本半周期(ms) const unsigned long CYCLE_PERIOD = BLINK_PERIOD * 2;// 1サイクルの時間(800ms) const int PATTERN[] = {1,0,1,0,0,0,0,0}; // 8ステップの点灯パターン const int PATTERN_LEN = sizeof(PATTERN) / sizeof(PATTERN[0]); const unsigned long PATTERN_STEP_MS = CYCLE_PERIOD / PATTERN_LEN; // 1ステップ100ms // --- 可変抵抗による点滅周期の制限値 --- const int MIN_INTERVAL = 50; // 最短点滅周期(ms) - これ以上速いと点灯に見える const int MAX_INTERVAL = 1000; // 最長点滅周期(ms) // --- ハートビート(生存確認LED)の点滅パターン --- // 16ビットの2進数でパルス波形を定義 (1=点灯, 0=消灯)。 // 1サイクル 1120ms (70ms * 16bit)。 const uint16_t HEARTBEAT_PATTERNS[] = { 0b1000000000000000, // 1回点滅 0b1010000000000000, // 2回点滅 0b1010100000000000, // 3回点滅 0b1010101000000000 // 4回点滅 }; const int NUM_HEARTBEAT_PATTERNS = sizeof(HEARTBEAT_PATTERNS) / sizeof(HEARTBEAT_PATTERNS[0]); const int HEARTBEAT_PATTERN_BITS = 16; const unsigned long HEARTBEAT_PATTERN_STEP_MS = 70; // 1ビットあたりの待機時間 // --- 各種ハードウェアの現在状態を保持する変数 --- volatile int encoderValue = 0; // エンコーダのカウント値 uint8_t prevEncA = HIGH; // A相の直前の論理レベル unsigned long lastEncChange = 0; // A相が最後に変化した時刻 unsigned long lastSwChange[NUM_SWITCHES] = {}; // スライドSWデバウンス時刻 uint8_t stableSw[NUM_SWITCHES] = {1, 1}; // デバウンス後の確定したSW状態 unsigned long lastPushChange = 0; // プッシュSWデバウンス時刻 uint8_t stablePush = 1; // プッシュSWの確定状態 unsigned long prevBlink = 0; // トグル点滅の最終切り替え時刻 unsigned long patternTimer = 0; // パターン点滅のステップ管理用タイマ uint8_t patternStep = 0; // 現在実行中のパターンインデックス bool stateBlink = false; // 現在のLED点灯状態(全ON/全OFF用) uint8_t heartbeatPatternIndex = 0; // 現在のハートビートパターンの種類(0〜3) uint8_t heartbeatBitIndex = 0; // 現在の16bitパターン内の読み出し位置 unsigned long heartbeatPatternTimer = 0; // ハートビート更新用タイマ int prevEncoderDisplay = 0; // シリアル出力制御用:前回のエンコーダ値 uint8_t prevSwDisplay = 0xFF;// シリアル出力制御用:前回のスイッチ値 // =========================================================================== // 初期化関数 setup() // 電源投入時、またはリセット時に1度だけ実行される。 // =========================================================================== void setup() { // 【重要】D11ピンをGPIOとして使用するためのデバッグ機能解除 pinV32_DisconnectDebug(PD_1); // 入力ピンの設定 (内部プルアップ有効化により外部抵抗を省略) pinMode(ENC_A, INPUT_PULLUP); pinMode(ENC_B, INPUT_PULLUP); pinMode(PUSH_SW, INPUT_PULLUP); for (int i = 0; i < NUM_SWITCHES; i++) { pinMode(SWITCH_PINS[i], INPUT_PULLUP); } // 出力ピンの設定と初期化(消灯状態) for (int i = 0; i < NUM_LEDS; i++) { pinMode(LED_PINS[i], OUTPUT); digitalWrite(LED_PINS[i], LOW); } pinMode(BUZZER_PIN, OUTPUT); digitalWrite(BUZZER_PIN, LOW); pinMode(HEARTBEAT_LED, OUTPUT); digitalWrite(HEARTBEAT_LED, LOW); // アナログ入力ピン設定 pinMode(POT_PIN, INPUT); // 内部変数の初期化 encoderValue = 0; uint8_t swBits = getSwitchBits(); prevSwDisplay = swBits; prevEncoderDisplay = encoderValue; } // =========================================================================== // メロディ制御関数群 // =========================================================================== /** * The Entertainerの再生状態を完全に初期化(停止)する。 * モードが切り替わった際に呼び出し、安全に音を止める。 */ void resetEntertainerMelody() { entertainerMelodyIdx = 0; entertainerMelodyMillis = 0; entertainerMelodyPlaying = false; entertainerMelodyPrevSwBits = 0; noTone(BUZZER_PIN); } /** * メロディの再生処理を行う(ノンブロッキング)。 * メインループ内で毎回呼び出され、経過時間(millis)に基づいて次の音符へ進む。 */ void playEntertainerMelody() { uint8_t swBits = getSwitchBits(); // 0b10 モード以外なら即座に再生を停止して抜ける if (swBits != 0b10) { entertainerMelodyPlaying = false; noTone(BUZZER_PIN); return; } // --- 再生開始時の処理 --- // 他のモードから切り替わった直後は、曲の先頭(Idx=0)から再生を開始する if (!entertainerMelodyPlaying || entertainerMelodyPrevSwBits != 0b10) { entertainerMelodyIdx = 0; entertainerMelodyMillis = millis(); entertainerMelodyPlaying = true; // 【バグ対策】発音前に必ずnoToneを呼び、ハードウェアタイマーをリセットする noTone(BUZZER_PIN); if (entertainer_melody[entertainerMelodyIdx] > 0) { tone(BUZZER_PIN, entertainer_melody[entertainerMelodyIdx]); } entertainerMelodyPrevSwBits = swBits; return; } // --- 継続再生中の処理 --- // 現在の音符に設定された発音時間(duration)が経過したか判定 if (entertainerMelodyPlaying && swBits == 0b10) { if (millis() - entertainerMelodyMillis >= entertainer_durations[entertainerMelodyIdx]) { // 次の音符へインデックスを進める entertainerMelodyIdx++; entertainerMelodyMillis = millis(); // 配列の終端に達したら最初に戻る(ループ再生) if (entertainerMelodyIdx >= entertainer_length) { entertainerMelodyIdx = 0; } // 【バグ対策】連続でtone()を呼ぶと波形が崩れるため、必ず一度止める noTone(BUZZER_PIN); // 休符(0)でなければ発音 if (entertainer_melody[entertainerMelodyIdx] > 0) { tone(BUZZER_PIN, entertainer_melody[entertainerMelodyIdx]); } } } entertainerMelodyPrevSwBits = swBits; } // =========================================================================== // センサ入力・デバイス読み取り関数群 // =========================================================================== /** * ロータリエンコーダの回転方向を判定し、カウントを増減する。 * ※割り込み(Interrupt)を使わず、高速ポーリングで処理している。 */ void processEncoder() { int currA = digitalRead(ENC_A); unsigned long now = millis(); // A相のピン状態が前回から変化したエッジ(立ち上がり/立ち下がり)を検出 if (currA != prevEncA) { // デバウンス(チャタリング防止)期間が過ぎているか確認 if (now - lastEncChange >= ENCODER_DEBOUNCE_MS) { lastEncChange = now; // A相が「立ち下がり(LOW)」になった瞬間に、B相の状態を見て方向を判定 if (currA == LOW) { if (digitalRead(ENC_B) == HIGH) { encoderValue++; // 時計回り } else { encoderValue--; // 反時計回り } } } prevEncA = currA; // 状態を更新 } } /** * スライドスイッチの物理ピン状態を読み取り、ソフトウェアデバウンスを経て * 2ビット(0〜3)の整数値として返す。 */ uint8_t getSwitchBits() { uint8_t bits = 0; unsigned long now = millis(); // 定義されたスイッチの数(2個)だけループを回す for (int i = 0; i < NUM_SWITCHES; i++) { uint8_t raw = digitalRead(SWITCH_PINS[i]); // 状態が変化していればデバウンスタイマーをチェック if (raw != stableSw[i]) { if (now - lastSwChange[i] > SWITCH_DEBOUNCE_MS) { lastSwChange[i] = now; stableSw[i] = raw; // 安定したとみなして値を確定 } } else { lastSwChange[i] = now; // 変化がなければタイマーをリセットし続ける } // ビットシフトを用いて、確定した値を上位ビットから順に詰め込む bits = (bits << 1) | (stableSw[i] ? 1 : 0); } return bits; } /** * プッシュスイッチが押されているか(LOWか)を判定する。 * @return 押下時に true、離上時に false */ bool getPushState() { uint8_t raw = digitalRead(PUSH_SW); unsigned long now = millis(); if (raw != stablePush) { if (now - lastPushChange > SWITCH_DEBOUNCE_MS) { lastPushChange = now; stablePush = raw; } } else { lastPushChange = now; } // INPUT_PULLUPのため、押下時にGNDに落ちてLOWになる return stablePush == LOW; } // =========================================================================== // 出力制御・表示関数群 // =========================================================================== /** * ビルトインLEDを用いて、プログラムがフリーズせずに動作していることを * 示す「ハートビート(心拍)」パターンを点滅させる。 */ void beatHeartbeat() { unsigned long now = millis(); // ステップ時間(70ms)が経過したら次のビットを読み出す if (now - heartbeatPatternTimer >= HEARTBEAT_PATTERN_STEP_MS) { heartbeatPatternTimer = now; // 現在の16bitパターンを取得 uint16_t pattern = HEARTBEAT_PATTERNS[heartbeatPatternIndex]; // ビットシフトで指定位置の1bitを抽出し、ピンに出力 int bit = (pattern >> (HEARTBEAT_PATTERN_BITS - 1 - heartbeatBitIndex)) & 0x01; digitalWrite(HEARTBEAT_LED, bit ? HIGH : LOW); // インデックスを進める heartbeatBitIndex++; if (heartbeatBitIndex >= HEARTBEAT_PATTERN_BITS) { heartbeatBitIndex = 0; // 16ビット読み切ったら次へ heartbeatPatternIndex++; if (heartbeatPatternIndex >= NUM_HEARTBEAT_PATTERNS) { heartbeatPatternIndex = 0; // 最後のパターンまで行ったら最初に戻る } } } } /** * エンコーダのカウント値を5つのLEDを用いて表示する。 * 最上位(MSB)は符号ビット(負の数で点灯)、残り4つで絶対値の下位4ビットを表示。 */ void showEncoderLeds() { int val = encoderValue; int absVal = abs(val); // 絶対値を取得 // MSB (LED配列の0番目 = ピンD11) を符号として扱う digitalWrite(LED_PINS[0], val < 0 ? HIGH : LOW); // 下位4つのLEDに対して、2進数の各桁をビットシフトで展開して出力 for (int i = 1; i < NUM_LEDS; i++) { digitalWrite(LED_PINS[i], (absVal >> (NUM_LEDS - 1 - i)) & 0x01 ? HIGH : LOW); } } /** * 事前定義された配列 (PATTERN) に従って、全LEDをパターン点滅させる。 */ void patternBlinkMode() { unsigned long now = millis(); // 指定時間が経過するたびに配列のインデックスを進める if (now - patternTimer >= PATTERN_STEP_MS) { patternTimer = now; patternStep = (patternStep + 1) % PATTERN_LEN; // 配列長を超えたら0に戻す } // 配列の値(1/0)に応じてLEDを制御 allLeds(PATTERN[patternStep]); } /** * 可変抵抗(ボリューム)の電圧を読み取り、その値に応じてLEDの点滅周期を制御する。 */ void potBlinkMode() { unsigned long pot = analogRead(POT_PIN); // 0〜1023の値を取得 // 【バグ対策】Flash容量節約のため float(浮動小数点)を排除 // 値を反転(1023〜0)させ、それを二乗することで、人間の感覚に近い // 「疑似的な対数カーブ(二次曲線)」を整数演算のみで作り出す。 unsigned long invPot = 1023 - pot; // (1023 * 1023 = 1046529)。オーバーフローを防ぐため 1046529UL とする。 unsigned long interval = MIN_INTERVAL + ((MAX_INTERVAL - MIN_INTERVAL) * invPot * invPot) / 1046529UL; unsigned long now = millis(); // 算出された周期(interval)ごとに点灯状態を反転(トグル)させる if (now - prevBlink >= interval) { stateBlink = !stateBlink; prevBlink = now; allLeds(stateBlink); } // 他の点滅モードへ移行した際に備え、タイマとステップをリセットしておく patternTimer = now; patternStep = 0; } /** * 待機状態用:LEDをすべて消灯し、点滅用の状態変数を初期化する。 */ void allOffMode() { allLeds(false); stateBlink = false; prevBlink = millis(); patternTimer = millis(); patternStep = 0; } /** * LED配列に登録されているすべてのピンを、一括でONまたはOFFにする。 * @param on trueで全点灯、falseで全消灯 */ void allLeds(bool on) { for (int i = 0; i < NUM_LEDS; i++) { digitalWrite(LED_PINS[i], on ? HIGH : LOW); } } // =========================================================================== // メインループ関数 loop() // 電源が入っている間、ブロッキング(delay等)なしで超高速で繰り返される。 // C/C++の構造に従い、呼び出し先となる各関数群の定義後に配置。 // =========================================================================== void loop() { // 1. バックグラウンド処理(常時稼働) beatHeartbeat(); // 動作確認用LEDパターンの更新 processEncoder(); // エンコーダの回転監視とカウント更新 // 2. モード切り替え監視 static uint8_t prevSwBitsLoop = 0xFF; uint8_t swBits = getSwitchBits(); // 0b11(エンコーダ表示モード)に入った瞬間だけ、カウンタを0にリセットする if (prevSwBitsLoop != 0b11 && swBits == 0b11) { encoderValue = 0; showEncoderLeds(); // リセット直後の状態をLEDに即時反映 } prevSwBitsLoop = swBits; // 3. スイッチ状態に基づく機能の分岐 switch (swBits) { case 0b00: // 手動パターンモード resetEntertainerMelody(); if (getPushState()) { patternBlinkMode(); // 押下時:配列に基づくパターン点滅 } else { allOffMode(); // 離上時:全消灯 } break; case 0b01: // 可変周期点滅モード resetEntertainerMelody(); potBlinkMode(); // ボリューム値に基づく周期で全点滅 break; case 0b10: // メロディ再生モード playEntertainerMelody();// 音楽再生処理 allLeds(false); // 音楽再生中はLEDを消灯して処理を軽くする break; case 0b11: // エンコーダ表示モード resetEntertainerMelody(); showEncoderLeds(); // 2進数表示の実行 break; default: // フェールセーフ(万が一の未定義状態) resetEntertainerMelody(); allOffMode(); break; } }