STM32でWS2812B V5 を光らせてみた

Miano第2弾の開発を進めている中で、MIDI音源を鳴らしたり鍵盤を光らせたりなど、やりたい事がどんどん増えてきました。 このままタスクが増え続けるとESP32が苦しくなる予感がしてきました。

そこで、WS2812B関連の処理をSTM32にバトンタッチする事にします。 440個のWS2812Bを1ポートで制御すると、更新周期が13.2ms(=440*24bit/800kHz)になります。 現状で特に問題はないのですが LED の数を増やした場合に表示がチラつく懸念もあったので、WS2812Bの制御をSTM32に任せます。
STM32C011でneopixelを130個点灯させることができた。基板は半年ぐらい放置してたので、機嫌を損ねてないか心配だったけど大丈夫みたい。 pic.twitter.com/WFxKaCVXvM
— robotoGAsensei (@robotoGAsensei) 2025年7月11日
STM32マイコン

秋月さんで160円で手に入るSTM32C011F4P6を使うことにしました。とにかく安い。 DIP変換基板(60円)、ピンヘッダー(45円)、ピンソケット(80円)などの周辺部品と肩を並べるぐらい安い。 ソフトを焼くのに必要なSTLINKも秋月さんで2200円で買えました。
STM32C011F4P6への雑感
- 開発環境のCubeIDEが無償提供されている
- PWM/SPI/UARTなどのペリフェラルを備えている
- フラッシュメモリやクロック回路を内蔵していて電源をつなぐだけでOK
- 5Vと3.3Vのどちらでも動く。今回はESP32に合わせて3.3Vを供給。
- FPUを搭載していないので浮動小数点を扱うことはできない
- SWO接続によるprintf デバッグができない
WS2812B-V5のパルス仕様


WS2812B-V5のパルス仕様を満足させるPWM波形として考えたのがこちら。 WS2812Bの制御信号を作る方法は調べるとたくさん出てきますが、今回はPWM+DMAでいきたいと思います。 RGBデータをPWMのレジスタにロードする部分をDMAが担ってくれるのでSTM32 CPUの負荷を抑える事ができそうです。
ペリフェラル設定
それでは早速 CubeIDEのGUIを設定して周辺ペリフェラルの設定を進めていきましょう。




周辺ペリフェラルを下記サイト様を参考に設定していきました。 ソースコードも丸ごと活用させていただいた結果、一日もかからずに点灯するところまで辿り着けました。本当に感謝しかありません。
変更箇所
参考にさせていただいたソースコードをベースに、自分の環境に合わせて変更した部分のメモ
neopixel.cpp
- ひとまずc言語で開発したかったので、ファイル拡張子を .cpp から .c に変更
- ヘッダ名の読み替えのため、
#include "tim.h"を#include "stm32c0xx_hal.h"に変更 - reset時間 280us 確保のため、
if (wait)以下でのwhile (busy) {};待ち処理を合計9回実施 - 設計したカウント値にするために
? 8 : 4を? 30 : 15に変更
neopixel.h
- ヘッダ名の読み替えのため、
#include "tim.h"を#include "stm32c0xx_hal.h"に変更 - bool を使うため
#include <stdbool.h>を追加 - cppからcに変更したので
NeoPixStart (uint8_t* data, int len, bool wait = true);をNeoPixStart (uint8_t* data, int len, bool wait);に変更
main.c
- 使用ポートの都合により、
NeoPixInit (&htim3, TIM_CHANNEL_1);をNeoPixInit (&htim3, TIM_CHANNEL_3);に変更 NeoPixStart (rgb, sizeof (rgb), true);をコールする際に第3引数 true を指定
動作波形
狙った通りの波形が出ていて、WS2812B-V5の仕様も満足しています。




ダンピング抵抗
STM32の出力ポートをWS2812BのDINに直結すると、リンギングしたのでダンピング抵抗を付けました。 いくつか試した中では220Ωが良かったです。




WS2812B V5への雑感
- 電源コンデンサが不要 (メリット)
- 3.3Vで動作可能 (メリット)
- JLCPCBの基板仕様がstandard指定なので基板が高い (デメリット)
- 本体の値段は旧バージョンよりも高い (デメリット)
- ベーキング処理が必要なのでPCB Assyで追加コスト発生 (デメリット)
旧バージョンではESP32などの3.3V系マイコンで信号を直接駆動する事ができませんが、 V5では仕様通り3.3Vでの動作を確認する事ができました。大変便利です。 ただ、部品代以外にもコスト差を生む要因がいくつかあるので、個人的には、 どうしても小型化したい場合はV5を選び、スペースに余裕があるなら旧バージョンを選ぼうと思っています。
Standard vs Economic (JLCPCB)
JLCPCBでは Economic と Standard の2種類のカテゴリーがあります。 いくつかの制約を守る事で Economic として発注する事ができます。
名前の通りお財布に優しいのは Economic ですが、V cut指定 で面付けすると自動的に Standard に繰り上がります。 部品によっても Standard に繰り上がってしまう場合があって、例えば、同じWS2812Bでも左側は Standard, 右側は Economic となります。 発注段階でEconomic非対応である事に気付くと、回路設計に手戻りが発生する(した※)ので要注意です。
※左側の WS2812B-V5 はパスコンが不要なのに対して、右側の WS2812B-S はパスコンが必要


PCB製造と部品実装をJLCPCBにお願いした場合、Standard と Economic でどのぐらいの金額差があるでしょうか? 試しに設計してみたのがこちらの基板。 Neopixelとして知られている WS2812B ×5 とFFC用コネクタが乗っただけのシンプルな基板です。

このシンプルな基板を V cut 指定で面付けして Standard(不可避) にした場合と、 mouse biteで面付して Economic にした場合で、どのぐらいの金額差があるでしょうか? せっかく Standard にするのであれば、JLCPCBの面付サービスを使って楽をする事にしました。 20×2に面付けした基板を5枚発注するので、上記画像の基板を合計200枚作る事になります。
結果

| 面付け | PCB費 | 実装費 | 合計金額 | 実装リードタイム | |
|---|---|---|---|---|---|
| Standard | V cut | ¥3,881 | ¥11,605 | ¥15,486 | 2~3日 |
| Economic | mouse bite | ¥1871 | ¥8329 | ¥10,200 | 1~2日 |
5000円以上の金額差になりました。 Standard では JLCPCB の面付けサービスを利用しているので、サービス料分だけPCB費が高くなっています。 mouse biteは折った後に断面がガタガタするので、V cutの方が仕上がりは綺麗です。 綺麗な断面が必要な場合はV cut指定で Standard, そうじゃない場合は mouse biteで自分で面付して Economic を目指すのが良さそうです。
ESP32でI2S ~信号出力の調査~
PWMで音を鳴らしたのが前回。 PWM変調された出力をRCローパスフィルタで復調しOPAMPで増幅するという方法だった。 PWM方式は期待以上に機能していたが、10以上の和音だと音が潰れて綺麗に聞こえないという問題があった。 そこで今回はI2Sインターフェースを備えるD級アンプ(MAX98357A)を使ってみようと思う。 前回はピアノの外にスピーカーを置いていたが、D級アンプで直接スピーカーを駆動できるので、スピーカーを本体に内蔵する狙いもある。

MAX98357A データシートおよび製品情報 | アナログ・デバイセズ | Analog Devices
まず最初にI2Sのフォーマットについて簡単に理解しておきたいので先人の知恵を拝借。 こちらの説明が分かりやすい。感謝。
http://www.easyaudiokit.com/bekkan/PCMTrans/PCMTrans.html
MAX98357Aのデータシートを確認するとこのように書かれていた。 ESP32の設定としては STAND_I2S を設定すれば良さそうだ。
The digital audio interface is highly flexible with the MAX98357A supporting I2S data and the MAX98357B supporting left-justified data.

ではコンフィグ設定(i2s_config)を変えながら信号波形を観測してみよう。
条件1 : I2S_COMM_FORMAT_STAND_I2S
- ticks_to_wait = portMAX_DELAY (i2s_writeの第4引数)
- i2s_writeのコール周期 = 50μs (main関数内のタイマ処理)
- コンフィグ設定 (i2s_config)
.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX), .sample_rate = 44100, .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT, .channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT, .communication_format = I2S_COMM_FORMAT_STAND_I2S, .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1, .dma_buf_count = 2, .dma_buf_len = BUFF_NUM, .use_apll = false, .tx_desc_auto_clear = true, .fixed_mclk = 0,
LRCLOCKについては設定値44.1kHzに対して実測値44.09kHzだった。 また、BCLKについては設定値1.411MHz (= 44.1kHz × 16bit × 2ch)に対して実測値1.411MHzだったので、 設定通りに動作している事が確認できた。 データフォーマットとしてSTAND_I2Sを設定しているので、 LRCLOCKのエッジから1クロック遅れたタイミングから0bit目が始まっている。
プローブ
BCLK:青
LRCLK:赤
DOUT:緑

DMAバッファは1byte(8bit)の配列なのに対して、I2Sの分解能は2byte(16bit)になる。 バッファ配列とI2Sで送信されるデータの関係はどうなるのだろうか? オシロの波形は配列の0番目から順に0,1,2,3・・・を設定したものなので、 順序と位置を照らし合わせると次のような関係になっている事が分かった。

ちなみに、STAND_MSBに設定してみるとこのように、左詰めフォーマットになっている事が確認できた。 余談になるが MAX98357B を使う場合STAND_MSBに設定する事になる。
プローブ
BCLK:青
LRCLK:赤
DOUT:緑

条件2 : I2S_CHANNEL_FMT_ALL_RIGHT
- ticks_to_wait = portMAX_DELAY (i2s_writeの第4引数)
- i2s_writeのコール周期 = 50μs (main関数内のタイマ処理)
- コンフィグ設定 (i2s_config)
.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX), .sample_rate = 44100, .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT, .channel_format = I2S_CHANNEL_FMT_ALL_RIGHT, <--ここが条件1と違う .communication_format = I2S_COMM_FORMAT_STAND_I2S, .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1, .dma_buf_count = 2, .dma_buf_len = BUFF_NUM, .use_apll = false, .tx_desc_auto_clear = true, .fixed_mclk = 0,
DMA BuffのL上位とL下位が、R上位とR下位に置き換えられるようだ。 左右のスピーカーから一時的にRのみ出力したい場合に使う事ができそう。
プローブ
BCLK:青
LRCLK:赤
DOUT:緑


条件3 : I2S_CHANNEL_FMT_ONLY_RIGHT
- ticks_to_wait = portMAX_DELAY (i2s_writeの第4引数)
- i2s_writeのコール周期 = 50μs (main関数内のタイマ処理)
- コンフィグ設定 (i2s_config)
.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX), .sample_rate = 44100, .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT, .channel_format = I2S_CHANNEL_FMT_ONLY_RIGHT, <--ここが条件1と違う .communication_format = I2S_COMM_FORMAT_STAND_I2S, .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1, .dma_buf_count = 2, .dma_buf_len = BUFF_NUM, .use_apll = false, .tx_desc_auto_clear = true, .fixed_mclk = 0,
どのように解釈していいか分からないので参考情報として載せておくだけにしよう。
プローブ
BCLK:青
LRCLK:赤
DOUT:緑

ESP32でSPIマスター通信
ESP32のSPI masterで色々試したので結果を残しておこうと思う。 SPIを送受信している間に他の処理が滞らないようにしたいので以下の点に気を付けた。
- 処理負荷を軽くしたいのでDMAを使う
- 処理が終わるまで返ってこないようなAPI設定は使わない
- 送信完了割り込みを使って完了判定をする
ループバック
MOSIとMISOをジャンパでつないでループバック。これで送信した内容が受信される。

オシロ波形
まずはオシロを使って波形を実測してみる。 上から順に、青:CS、赤:SCK、緑:MOSI(MISOはMOSIと結線しているので同じ)
32バイト送信 59μs

CSの両エッジ間隔は約59μsだった。32byteを4.5Mbpsで転送するには57us必要なので概ね計算通り。 また、ソフトウェアでCSのHigh/Low切り替えを行っているわけではないので、 ハードウェアの機能としてCSの切り替えがサポートされていることも分かる。
1024バイト送信 1870μs

CSの両エッジ間隔は約1870μsだった。1024byteを4.5Mbpsで転送するには1820us必要なので概ね計算通り。 同じく、ソフトウェアでCSのHigh/Low切り替えを行っているわけではないので、 ハードウェアの機能としてCSの切り替えがサポートされていることが分かる。
拡大

CLKとMOSIが潰れて見えていないので拡大してみよう。 送信バッファには0, 1, 2, 3, 4,・・・と並ぶようにダミーデータを格納しているので、 MOSIもそれに従って、00000000 00000001 00000010 00000011・・・の順に出力されている。
API処理時間の測定(準備)
ESP32の perfmon.h を利用して実行時間を1000回計測するようにした。 func_start()とfunc_end()に挟まれた部分の処理時間を計測する仕組みになっている。 まずは、計測対象が何も存在しない場合にどうなるかを事前に調べておこう。
perfmonAPP_CPU.func_start(); // 処理時間計測開始ポイント //ここの処理時間を計測する。まずは、何もない状態で計測してみる。 perfmonAPP_CPU.func_end(); // 処理時間計測終了ポイント

計測区間に何も存在しない場合はこちらの結果となったので、最大で10us程度の誤差がある事が分かった。
spi_device_queue_transの測定
では実際に、spi_device_queue_trans(送信開始API) の実行時間を計測してみよう。 待ち時間の設定として、portMAX_DELAY(処理が終わるまで関数から戻らない設定)とゼロ (待ち時間ゼロ)の2つのパターンを比べてみる。
まずは32byteで計測。
待ち設定:portMAX_DELAY, DMA:32byte → 平均27[μs]
perfmonAPP_CPU.func_start(); // 処理時間計測開始ポイント // マスターの送信開始 ESP_ERROR_CHECK(spi_device_queue_trans(spi_master_handle, &spi_master_trans, portMAX_DELAY)); perfmonAPP_CPU.func_end(); // 処理時間計測終了ポイント

待ち設定:0, DMA:32byte → 平均27[μs]
perfmonAPP_CPU.func_start(); // 処理時間計測開始ポイント // マスターの送信開始 ESP_ERROR_CHECK(spi_device_queue_trans(spi_master_handle, &spi_master_trans, 0)); perfmonAPP_CPU.func_end(); // 処理時間計測終了ポイント

2パターンの待ち時間設定を試したが、どちらも27μsだった。 DMAサイズが32byteだと差が出ないのかと思い、試しに1024byteでも試したが結果は変わらず27μsだった。
待ち設定:portMAX_DELAY, DMA:1024byte → 平均27[μs]

待ち設定:0, DMA:1024byte → 平均27[μs]

spi_device_queue_transの処理時間はデータサイズに依存しなかった。 キューに積んで送信開始トリガーを引くぐらいの処理なので、処理時間は27μs程度なのかもしれない。 では次に送信結果を取得するAPI spi_device_get_trans_result の処理時間を計測してみようと思う。
spi_device_get_trans_result の測定
まずは32byteで計測。
待ち設定:portMAX_DELAY, DMA:32byte → 平均77[μs]
// マスターの送信開始 ESP_ERROR_CHECK(spi_device_queue_trans(spi_master_handle, &spi_master_trans, 0)); perfmonAPP_CPU.func_start(); // 通信完了待ち spi_device_get_trans_result(spi_master_handle, &spi_trans, portMAX_DELAY); perfmonAPP_CPU.func_end();

待ち設定:0, DMA:32byte → 平均2[μs]
// マスターの送信開始 ESP_ERROR_CHECK(spi_device_queue_trans(spi_master_handle, &spi_master_trans, 0)); perfmonAPP_CPU.func_start(); // 通信完了待ち spi_device_get_trans_result(spi_master_handle, &spi_trans, 0); perfmonAPP_CPU.func_end();

32byteを4.5Mbpsで転送するには57us必要なのに対して計測結果が平均77usであった。 また、待ち設定を0(ゼロ)にすると平均2μs程度で関数から戻ってくることが分かった。
次に1024byteで計測。
待ち設定:portMAX_DELAY, DMA:1024byte → 平均1862[μs]

待ち設定:0, DMA:1024byte → 平均2[μs]

では、送信バイト数を1024に増やすとどうなるか試してみよう。 1024byteを4.5Mbpsで転送するには1820us必要なので、計測結果が平均1862usであった事と整合している。 また、待ち設定を0(ゼロ)にすると平均2μs程度で関数から戻ってくる事が確認できた。
コード
受信完了時にコールバック関数 postTransferCB が呼ばれる。 コールバック関数の中で複雑な時間のかかる処理は行わない方が良いとの事なので、単に spiTransmitdone をインクリメントするだけの構成とした。 普通のTaskループの中で spiTransmitdone をポーリングする事で、受信完了時に必要な処理を実装する。
header
class PianoKeySPI {
public:
void init();
void run();
private:
// callback function
static void IRAM_ATTR postTransferCB(spi_transaction_t* t);
// counts of transfer
volatile static uint32_t spiTransmitdone;
// SPI frequency
static const uint32_t SPI_CLK_HZ = 4500000;
// Transfer size in byte
static const uint32_t TRANS_SIZE = 64;
// buffer
uint8_t* spiMasterTXBuf;
uint8_t* spiMasterRXBuf;
// SPI マスタの設定
spi_transaction_t spiMasterTrans;
spi_device_interface_config_t spiMasterCFG;
spi_device_handle_t spi_master_handle;
spi_bus_config_t spiMasterBus;
uint32_t spiTransmitdone_pre=0xffffffff;
void spiBufInit();
void spiMasterInit();
};
source code
// Mutex for callback function portMUX_TYPE postTransferMux = portMUX_INITIALIZER_UNLOCKED; // counts of spi tranmit volatile uint32_t PianoKeySPI::spiTransmitdone = 0; void PianoKeySPI::init() { spiBufInit(); spiMasterInit(); } void PianoKeySPI::run() { spi_transaction_t *spi_trans; if (spiTransmitdone != spiTransmitdone_pre) { // // write code here for anything that should be done after receiving SPI // // start transfer for next sampling ESP_ERROR_CHECK(spi_device_queue_trans(spi_master_handle, &spiMasterTrans, 0)); spiTransmitdone_pre = spiTransmitdone; } } void PianoKeySPI::spiBufInit() { spiMasterTXBuf = (uint8_t *)heap_caps_malloc(TRANS_SIZE, MALLOC_CAP_DMA); spiMasterRXBuf = (uint8_t *)heap_caps_malloc(TRANS_SIZE, MALLOC_CAP_DMA); for (uint32_t i = 0; i < TRANS_SIZE; i++) { spiMasterTXBuf[i] = i & 0xFF; } memset(spiMasterRXBuf, 0, TRANS_SIZE); } void IRAM_ATTR PianoKeySPI::postTransferCB(spi_transaction_t *t) { portENTER_CRITICAL_ISR(&postTransferMux); spiTransmitdone++; portEXIT_CRITICAL_ISR(&postTransferMux); } void PianoKeySPI::spiMasterInit() { spiMasterTrans.flags = 0; spiMasterTrans.length = 8 * TRANS_SIZE; spiMasterTrans.rx_buffer = spiMasterRXBuf; spiMasterTrans.tx_buffer = spiMasterTXBuf; spiMasterCFG.mode = SPI_MODE3; spiMasterCFG.clock_speed_hz = SPI_CLK_HZ; spiMasterCFG.spics_io_num = SPI_MASTER_CS; spiMasterCFG.queue_size = 1; // キューサイズ spiMasterCFG.flags = SPI_DEVICE_NO_DUMMY; spiMasterCFG.queue_size = 1; spiMasterCFG.pre_cb = NULL; spiMasterCFG.post_cb = postTransferCB; spiMasterBus.sclk_io_num = SPI_MASTER_CLK; spiMasterBus.mosi_io_num = SPI_MASTER_MOSI; spiMasterBus.miso_io_num = SPI_MASTER_MISO; spiMasterBus.max_transfer_sz = 8192; ESP_ERROR_CHECK(spi_bus_initialize(VSPI_HOST, &spiMasterBus, 1)); // DMA 1ch ESP_ERROR_CHECK(spi_bus_add_device(VSPI_HOST, &spiMasterCFG, &spi_master_handle)); }
参考サイト
いつものように先人たちの知恵を借りまくった。本当に感謝。
ピアノを共に弾こう!みんなで楽しむ新感覚ピアノ(改善)🎹
Multi players piano

前回まで一通りの動作を見てきましたが、いくつかの課題が見つかっています。 今回はこれらの課題を解決していきたいと思います。
前回:ピアノを共に弾こう!みんなで楽しむ新感覚ピアノ(ファミコン音源)🎹 - robotoGAsensei’s diary
オペアンプの出力がクリップする
両電源用のオペアンプを5V単電源で使った事がそもそもの間違いなので、シンプルに rail to rail 出力のオペアンプと交換します。 秋月電子さんで入手可能だったMCP602を利用しました。

アイドル状態


まずは何もキーボードを押していないアイドル状態です。 左が交換前(NJM4580DD)右が交換後(MCP602)です。 オペアンプの交換と同時にPWMも312.5kHzに変更しているので、オペアンプ交換の影響が見にくくなってしまいました。 クリップの有無に着目すると交換後はオペアンプの出力がクリップする事なく期待通りの動作となっています。
高DUTY側


左が交換前(NJM4580DD)右が交換後(MCP602)です。 高電圧側はクリップすることなく期待通り動作しています。
低DUTY側


左が交換前(NJM4580DD)右が交換後(MCP602)です。 交換前は盛大にクリップし加えて電圧反転していましたが、交換後はクリップすることなく期待通り動作しています。 十分に満足な結果となりました。
キャリア波のリプルが乗る
それでは次にキャリア波のリプルが出力波形に乗っていたので、キャリア波の周波数を 78kHzから 312.5kHzに変更してみましょう。 LPFは4.7kと1500pFのまま変更はありませんのでカットオフ周波数(22.6kHz)は同一です。
左がキャリア周波数 78kHz、右が312.5kHzです。
sin波


三角波


疑似三角波


矩形波


PWM duty 25%


PMW duty 12.5%


ノコギリ波


リプル成分が減った事でラインがが細くなりました。こちらも十分に満足のいく結果となりました。 音をご紹介できないのが残念ですが私の貧しい聴覚でも十分に違いを感じ取れるぐらいの差分があります。 以上、キャリア周波数の変更とオペアンプの変更による改善でした。
ピアノを共に弾こう!みんなで楽しむ新感覚ピアノ(ファミコン音源)🎹
Multi players piano

前回のPWMに引き続き今回はファミコン音源について紹介したいと思います。
前回:ピアノを共に弾こう!みんなで楽しむ新感覚ピアノ(PWM)🎹 - robotoGAsensei’s diary
- 電気(キースイッチ、マルチプレクサ、ESP32周辺)
- メカ(バネ・ヒンジ構造)
- ソフト(PWM、 ファミコン音源 ) ← 今回
基板構成

マイコン基板


7種類の波形選択
ファミコン音源として知られているいくつかの波形を含む7種類の波形を選べるようにしました。

各波形情報は浮動小数点1024個の配列に格納しています。

こちらの黒鍵(本来は白鍵だけど目印のため黒にした鍵盤)を押す事で順次切り替わるようにしています。
期待通りの波形になっているでしょうか?

sin波


三角波


疑似三角波


矩形波


PWM duty 25%


PMW duty 12.5%


ノコギリ波


概ね予想通りでした。
LPF出力にリプルが乗って太い線になっています。もっとシャープな線を実現するにはどうすればよいでしょうか? LPFは4.7kと1500pFのシンプルな一次のフィルタで構成しています。 カットオフ周波数(22.6kHz)に対してキャリア波の周波数(78kHz)が低い事がリプルの乗る要因だと考えられます。
- キャリア波の周波数をカットオフの10倍程度(226kHz)にする
- LPFを1次ではなく2次にする
カットオフ周波数を下げるのはどうでしょうか? 波形のエッジが立たなくなるので疑似三角波の階段部分や矩形波の立ち上がり・下がりに悪い影響が出そうです。 極端に下げるのはやめた方が良さそうなので、まずは上の2つを試してみたいと思います。
ピアノを共に弾こう!みんなで楽しむ新感覚ピアノ(PWM)🎹
Multi players piano

前回はPWMの波形1つにフォーカスしました。今回はLPFで復調された波形を見ていきたいと思います。
前回:ピアノを共に弾こう!みんなで楽しむ新感覚ピアノ(マイコン基板)🎹 - robotoGAsensei’s diary
- 電気(キースイッチ、マルチプレクサ、ESP32周辺)
- メカ(バネ・ヒンジ構造)
- ソフト( PWM 、ファミコン音源 ) ← 今回
基板構成

マイコン基板


PWM復調回路
マイコンのPWM機能を使って各種波形を変調して出力しています。 最終的にオーディオ信号として出力するためにPWM変調された波形をLPFで復調しています。 今回は復調された後のsin波を見ていきたいと思います。 88個全て確認するのは大変なので各オクターブの「ド」の音を確認しました。 オペアンプの出力はクリップして見にくいので、LPF出力を見ながら周波数を確認していきます。
期待通りの周波数のsin波になっているでしょうか?

ド:C1

ド:C2

ド:C3

ド:C4

ド:C5

ド:C6

ド:C7

ド:C8

概ね期待通りでした。
とは言え、どの音程も周波数が低い方向にズレるのは何かが潜んでいる気がします。 追って原因追究していきたいと思います。
| 音程 | 期待値 | 実測値 | 誤差 |
|---|---|---|---|
| ド:C1 | 32.70Hz | 31.12Hz | 4.8% |
| ド:C2 | 65.40Hz | 62.22Hz | 4.9% |
| ド:C3 | 130.8Hz | 126.5Hz | 3.3% |
| ド:C4 | 261.6Hz | 253.5Hz | 3.1% |
| ド:C5 | 523.2Hz | 494.3Hz | 5.5% |
| ド:C6 | 1046Hz | 1020Hz | 2.5% |
| ド:C7 | 2093Hz | 2070Hz | 1.1% |
| ド:C8 | 4186Hz | 4080Hz | 2.5% |