圧電サウンダを鳴らす機能をRaspberry Piに追加したい！

![Raspberry PiとNFCリーダー](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/30370/c833305c-a8b8-d7c8-199c-a4bc6aedb389.jpeg)















**🥳11月からJSL（日本システム技研）にJOINした七瀬です！🥳**

出退社時にIDカードをかざして出社・退社状態を書き換えるシステムが Raspberry Pi を使って既に実現・運用されています。便利！

https://qiita.com/chinoppy/items/f2b4a406cd763e4c78e3


カードリーダーにカードをかざすとドットマトリクスディスプレイが光ります。でもたまに見えづらい… それなら音が鳴らせるようにしてみたい！

今回は Raspberry Pi を拡張する形でどのような回路と基板を作ればいいのか、ハードウェアとソフトウェアから検討してみました。







# 圧電サウンダ

鳴らす音は「ピッ」という電子音です。
ダイナミックスピーカー（通常のスピーカー）で鳴らすこともできますが、今回は **圧電サウンダ（圧電スピーカー）** を使った鳴動を考えます。

| | ダイナミックスピーカー | 圧電サウンダ |
|---|---|---|
| 動作原理 | **電磁誘導**<br>電流によって電磁力を<br>発生させて振動板を動かす | **逆圧電効果**<br>圧電体に電圧を加えると<br>変形し振動する |
| 音響特性 | 広い周波数帯域をカバー | 特定の周波数に対し高効率で発音 |
| 消費電力 | 比較的高消費電力でアンプが必要 | 非常に低消費電力 |
| 最適用途 | 音楽や音声再生 | アラーム、電子機器の通知音 |


今回はカードリーダーの認識音としての用途のため、圧電サウンダを使うのが用途として最適のようです。秋月電子では主に 2kHz～4kHz の発音に適した圧電サウンダが [販売されています](https://akizukidenshi.com/catalog/r/rspeaker2/)。

![矩形波出力](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/30370/f4f13700-ec1a-d5b9-864e-1c34e9e09b4a.gif)

発音信号はいわゆる [矩形波](https://ja.wikipedia.org/wiki/%E7%9F%A9%E5%BD%A2%E6%B3%A2) です。つまりフィルタを通さず、GPIOからの出力を直接圧電サウンダに入力することができます。

デジタル機器と相性の良い圧電サウンダですが、いくつか注意点があります。

- GPIOからそのまま出力すると音量が小さい
- 直流電圧を印加してはいけない
- 衝撃によって予期せぬサージ電圧が発生する

ひとつずつ見ていきます。





## GPIOからそのまま出力すると音量が小さい

圧電サウンダは電圧印加によって圧電体が変形し発音するため、発音音量は印加する電圧の大きさによって変わります。GPIOの電圧は 3.3V または 5V ですが、この電圧は圧電サウンダにとってはやや小さな値です。聞こえないことはありませんが、騒々しい環境では十分な音量として聞こえない可能性があります。

![CircuitJS12024-12-2000-24-25-ezgif.com-optimize.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/30370/a1088894-2af5-409e-6d4a-7db5444b77a7.gif)


そこで、位相が180°反転した信号を圧電サウンダの両端に印加することで、GPIOの電圧の2倍で駆動できるようになります。つまり電源電圧 Vcc が 3.3V だとすると、圧電サウンダにかかる電圧は 6.6Vpp[^Vpp] ということになります。

[^Vpp]: ボルトピークピーク。つまり電圧の最大値と最小値の差を表します。

位相反転信号を作る方法は様々ですが、今回はインバータを使った方法を採用しました。





## 直流電圧を印加してはいけない

金属導線に直流電流を流すと[イオンの移動によって導線が欠損していく現象](https://ja.wikipedia.org/wiki/%E3%82%A8%E3%83%AC%E3%82%AF%E3%83%88%E3%83%AD%E3%83%9E%E3%82%A4%E3%82%B0%E3%83%AC%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3)が知られています。圧電サウンダではシルバーマイグレーション（Agマイグレーション）として知られており、データシート上で直流を避けるよう明示されているものがあります。

![CircuitJS12024-12-2000-29-39-ezgif.com-optimize.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/30370/6b1d3159-2777-26fa-8919-410332aa8646.gif)

要するに、長い目で見て電流の合計値がゼロになればよい、ということです。具体的には直流成分をカットし交流成分のみにするカップリングコンデンサを圧電サウンダと直列に接続します。



## 衝撃によって予期せぬサージ電圧が発生する

圧電サウンダは逆圧電効果を用いています。つまり、[圧電効果](https://ja.wikipedia.org/wiki/%E5%9C%A7%E9%9B%BB%E5%8A%B9%E6%9E%9C) も働きます。

- **逆圧電効果**: 電圧をかけると圧電体が変形する
- **圧電効果**: 圧電体を変形させると電圧が発生する

これらはまさに表裏一体で、圧電サウンダに衝撃を与えると 3.3V や 5V よりも遥かに高い電圧が発生します。無対策の場合、その高電圧はGPIOが受けることになりICを損傷させます。

![CircuitJS12024-12-2000-38-22-ezgif.com-optimize.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/30370/8f52c383-17bc-ed61-dafb-7de96cb4ebba.gif)

このような過電圧の防止のために双方向対応の [TVSダイオード](https://www.rohm.co.jp/electronics-basics/diodes/di_what8) を使います。TVSダイオードはツェナーダイオードを向かい合わせに組み合わせたように振る舞います。どのTVSダイオードでもよいわけではなく、GPIOの 5V に対応するものを選定する必要があります。



# どんな基板を作ろう？

圧電サウンダの扱いを復習したところで、どのような基板を作るか考えます。

- Raspberry Pi を拡張する（HAT基板にしたい）
- カードリーダーのためだけの基板にしたくない
- カードリーダーの動作を壊したくない
- 好きな圧電サウンダを使えるようにしたい
  - 圧電サウンダによって効率の良い音の高さが異なるから

そこでこのような構想を練りました。

- [I2C](https://ja.wikipedia.org/wiki/I2C) を使って制御ICと通信
  - 圧電サウンダの鳴動の開始、終了
  - 周波数の変更、リズムパターンの変更
  - 鳴動中の制御をRaspberry Piでする必要がない
  - I2Cスレーブデバイスを自分で作る
- 万が一 I2C が使えない場合にも備え、1ピンだけで鳴らせるようにする 

今回は圧電サウンダの出力制御のみを行いたいので以下の要件に合うマイコンICを選定します。

- I2C のスレーブデバイスが作れる
- 駆動電圧は 3.3V または 5V
- 自由に使えるGPIOピンが少なくとも 5 個
  - I2Cのピン 2個: SCL, SDA
  - I2Cのアドレス選択 1個: /SELECT
  - 信号出力 1個: OUTPUT
  - 1ピンだけで鳴らせるように 1個: TRIGGER

今回は格安で買える [CH32V003J4M6](https://akizukidenshi.com/catalog/g/g118062/) を使うことにしました。 


# 圧電サウンダ制御用ICのプログラム

## I2Cデバイスの仕様

ササッと決めてしまいます。

```xml
ビットオーダーは MSBFirst (bit7 -> bit0)
バイトオーダーは リトルエンディアン

機能:
  マスターからスレーブへの書き込み:
    1バイト目 値: 0x00～0x06、内容: レジスタのアドレス
    2バイト目 値: 0x00～0xff、内容: レジスタに書き込まれる値
    スレーブからの応答: ステータスデータのみ
  スレーブからマスターへの読み出し:
    1バイト目 値: 0x80～0x86、内容: レジスタのアドレス（書き込み時の 0x00～0x06 と対応）
    スレーブからの応答: ステータスデータと、1バイトのレジスタの値

ステータスデータ:
  [7 ビット] ステータス: 0 = NG, 1 = OK
  [6..4 ビット] 予約済み
  [3..0 ビット] ステータスデータのあとに続くバイト数 (理論上 0～15バイト)

レジスタマップ:
  0x00 R/W [7 ビット] 出力フラグ: 0 = 無効（停止）、1 = 有効（鳴動）
           [6 ビット] 鳴動モード: 0 = 出力が有効の間は常に鳴動、1 = 自動で鳴動停止
           [5..0 ビット] 予約済み
  0x01 R/W [7..0 ビット] 音の周波数 (16bit データの LSB)
  0x02 R/W [7..0 ビット] 音の周波数 (16bit データの MSB)
  0x03 R/W [7..0 ビット] リズムパターン (16bit データの LSB)
  0x04 R/W [7..0 ビット] リズムパターン (16bit データの MSB) 
  0x05 R/W [7..0 ビット] リズム間隔 (16bit データの LSB)
  0x06 R/W [7..0 ビット] リズム間隔 (16bit データの MSB)

挙動:
- アドレスが範囲外ならステータスデータのステータスをNGにする
- ブザーのパルス信号が出力されるのはレジスタのアドレス 0x00 の 7 ビット目が 1 のとき、
  なおかつリズムパターンの現在のビット位置が 1 のとき
```

ピンのアサインは以下のようにします。（[ピンの一覧図](https://raw.githubusercontent.com/Tengo10/pinout-overview/main/pinouts/CH32v003/ch32v003j4m6.svg)）

| ピン番号 | ピン名 | 機能 |
|---|---|---|
| PA1/PD6 | OUTPUT | 圧電サウンダへの信号出力 |
| VSS | GND | GND |
| PA2 | TRIGGER | 鳴動トリガ用ピン |
| VDD | 3.3V | 3.3V 電源入力 |
| PC1 | SDA | I2C の SDA ピン |
| PC2 | SCL | I2C の SCL ピン |
| PC4 | /SELECT | I2C のアドレス選択ピン |
| PD1/PD4/PD5 | SWIO | プログラム書き込み用ピン |











## ソースコード

VSCode + [PlatformIO](https://platformio.org/) を使い、CH32V003J4M6 のプログラムを作ります。プラットフォームは [ch32v](https://github.com/Community-PIO-CH32V/platform-ch32v)、フレームワークは [Arduino](https://github.com/openwch/arduino_core_ch32/) です。

ボード設定は [CH32V003F4P6](https://akizukidenshi.com/catalog/g/g118061/) 用ですが、実は CH32V003J4M6 でもこのまま使えます。

プログラム書き込みは [WCH-LinkEエミュレーター](https://akizukidenshi.com/catalog/g/g118065) を使います。

```ini:platform.ini
[env]
platform = ch32v
framework = arduino

[env:genericCH32V003J4M6]
board = ch32v003f4p6_evt_r0
```

```c++:main.cpp
#include <Arduino.h>
#include <Wire.h>

#define I2C_ADDRESS 0x34
#define I2C_ADDRESS_SUB 0x35

#define PIN_SELECT PIN_A2
#define PIN_TRIGGER PIN_A0
#define PIN_OUTPUT PIN_A6

#define REG_STATUS_UNSET 0x00
#define REG_STATUS_OK 0x80
#define REG_OUTPUT 0x80
#define REG_MODE 0x40

uint8_t registers[7]    = { 0x40, 0xdb, 0x05, 0x1d, 0x00, 0x99, 0x02 };
uint8_t currentRegister = 0xff;  // 読み出しレジスタ位置

#define registerOutputEnabled() (registers[0] & 0x80)
#define registerToneFrequency() ((uint32_t)((registers[1] | registers[2] << 8) + 1))
#define registerRhythmPattern() ((uint16_t)(registers[3] | registers[4] << 8))
#define registerRhythmInterval() ((uint32_t)((registers[5] | registers[6] << 8) + 1))
void receiveEvent(int byteNumber);
void requestEvent();
void beep(bool playing);

void setup() {
  pinMode(PIN_OUTPUT, OUTPUT);
  pinMode(PIN_SELECT, INPUT_PULLUP);
  pinMode(PIN_TRIGGER, INPUT_PULLUP);

  Wire.begin(digitalRead(PIN_SELECT) ? I2C_ADDRESS : I2C_ADDRESS_SUB);
  Wire.onReceive(receiveEvent);
  Wire.onRequest(requestEvent);
}

void loop() {
  static bool buzzerStarted      = false;
  static uint8_t currentTick     = 0;
  static uint32_t rhythmInterval = 0;
  bool buzzerEnabled             = registerOutputEnabled();

  // 鳴動開始
  if (buzzerStarted != buzzerEnabled) {
    if (buzzerEnabled) {
      currentTick    = 0;
      rhythmInterval = registerRhythmInterval();
    }

    buzzerStarted = buzzerEnabled;
  }

  // 信号出力制御
  beep(buzzerEnabled && registerRhythmPattern() & (1 << currentTick));

  // 次のリズムパターン位置
  if (rhythmInterval == 0) {
    // リズムパターン最終位置到達
    if (++currentTick >= 16) {
      // 自動停止モードならば出力フラグをクリア
      if (registers[0] & REG_MODE) {
        registers[0] &= ~REG_OUTPUT;
      }
      currentTick = 0;
    }

    rhythmInterval = registerRhythmInterval();
  } else {
    rhythmInterval--;
  }

  delayMicroseconds(100);
}

void beep(bool playing) {
  static bool toneState = false;

  // NOTE: 2回以上連続して tone および noTone 関数を呼ばない
  if (playing) {
    if (!toneState) {
      toneState = true;
      tone(PIN_OUTPUT, registerToneFrequency());
    }
  } else {
    if (toneState) {
      toneState = false;
      noTone(PIN_OUTPUT);
      digitalWrite(PIN_OUTPUT, LOW);  // 鳴動終了後は LOW に戻す
    }
  }
}

// I2Cマスターからデータが受信されたときに実行される関数
void receiveEvent(int byteNumber) {
  if (byteNumber == 1) {
    // 受信バイトが 1 => 読み出し命令
    currentRegister = Wire.read();
  } else if (byteNumber == 2) {
    // 受信バイトが 2 => 書き込み命令
    uint8_t address = Wire.read();
    uint8_t value   = Wire.read();

    if (address >= 0x00 && address <= 0x06) {
      registers[address] = value;
    }
  }
}

// I2Cマスターが読み出しを要求したときに実行される関数
void requestEvent() {
  if (currentRegister >= 0x80 && currentRegister <= 0x86) {
    uint8_t registerIndex = currentRegister & 0x7f;

    // アドレスが正しいならデータつきで応答
    if (registerIndex < sizeof(registers)) {
      Wire.write(REG_STATUS_OK | 0x01);
      Wire.write(registers[registerIndex]);
      return;
    }
  }

  // アドレス不正ならば ステータス:NG として応答
  Wire.write(REG_STATUS_UNSET);
}
```


# 回路図

[KiCad](https://ja.wikipedia.org/wiki/KiCad) で回路図を書きました。

:::note warn
回路図の部品名は仮です。参考程度にどうぞ。
:::


![回路図](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/30370/6e39e1ca-be74-ed45-3c09-81f2e4349b93.png)









インバータには [555 タイマIC](https://ja.wikipedia.org/wiki/555_%E3%82%BF%E3%82%A4%E3%83%9E%E3%83%BC) を使っています。本来は専用のインバータ（NOTゲート）ICを使うところですが、可聴域の信号しか出力しないこと、低消費電力は重視していないため、555を用いたシュミットトリガ回路でインバータを実現しています。

C4 は直流成分除去のためのカップリングコンデンサ、R3 は圧電サウンダの異常発振防止用の抵抗器、D3 は圧電サウンダで発生する高電圧を制限するためのTVSダイオードです。R4 は C4 のデカップリングコンデンサに蓄えられた電荷を逃がすための抵抗です。この電荷は OUTPUT が GND 電位になると放電されていきます。

回路図にはありませんが、最終的には C4 と R3 の間に可変抵抗を入れて音量調整ができるようにするつもりです。

前述のとおり位相反転を行っているので電子サウンダ BZ1 の両端にかかる電圧は 6.6Vpp になります。

![DSC05620.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/30370/1f32f1ca-aa95-42fb-5007-4575139f748d.jpeg)

ブレッドボードで回路検証を行った様子です。右に見える Raspberry Pi Pico がI2Cマスターデバイスとなっており、白いタクトスイッチを押すことで中央上にある CH32V003J4M6 へ I2C で圧電サウンダ鳴動開始命令を出します。波形は CH32V003J4M6 の隣にある 555 タイマICで位相が反転し、左にある圧電サウンダには 6.6Vpp が印加されて鳴動します。












# 動作風景

今回使った圧電サウンダは 4kHz 用で、3kHz の信号を出力しているため若干音量が小さめに出力されています。

<iframe width="560" height="400" src="https://www.youtube-nocookie.com/embed/tLtCAr1CA34?si=L4AJaBUzVuDmTFET" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>














# これからやること

回路はできたので、いよいよ基板設計に入っていきます。
今回の記事ではここまでですが、次は実際に Raspberry Pi から呼び出す動作をお見せしたいと思います。

**良い出退勤ライフを！🥳**




# 参考文献

https://cba.sakura.ne.jp/sub04/test12.htm

バイポーラ駆動、シルバーマイグレーション、衝撃時の波形など、圧電サウンダを扱う上で大変有益な測定結果と回路図が記載されています。