バーサライタ(POV)を作ってみたい　その1

# 目次
バーサライタ(POV)を作ってみたいという気持ちのメモ。
今回の結論：電子オルゴールができた（謎）

[1. はじめに](#1-はじめに)
[2. 構成検討](#2-構成検討)
[3. 設計](#3-設計)
[4. 作製](#4-作製)
[5. おわりに](#5-おわりに)

# 1. はじめに
バーサライタ、英語でいうところのPersistence of Vision（PoV）とは何か。
AIさん的に言うと、
「光の残像効果を利用して画像や映像を表示する技術」
です。
バンダイさんがとても素敵なバーサライタを作っているもんで、
触発されて作ってみようという気持ちになりました。

https://toy.bandai.co.jp/series/precure/item/detail/13006/


# 2. 構成検討
プリキュアのやつを調査されている方がいたので参考にさせていただきました。

https://toyhospital-nara.sakura.ne.jp/Toy/SnowMan/ME239_TK29_Skymirage.pdf

で、構成を考えてみました。
のちにいろいろ間違っていたり、諦めたりしてだいぶ変わりますが、
初期はこんな気持ちで動き出しました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1112433/6d2ccdb9-fbc3-74d9-0f14-13195eed4b37.png)

- 回転機構は3Dプリンタでベアリング作って、All printingで筐体と機構を作ってみよう！
- 無線給電はモジュールを買っちゃおう。
- 磁石くっつけてホールセンサで回転検知や
- ロータ側に点灯パターン持つマイコン持たせるので、非接触の光通信で入力を伝えよう！
- 回転数が分かれば、極座標計算して点灯位置きまるので、それでパターンを作ろう。

↓
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1112433/4e1d6b4b-8fae-3c38-6580-3227a28682bd.png)

- モーターは圧入で回るから楽だしそっちで
- M5なら電源/制御できるから楽そう
- IRセンサのほうがよく見る気がする
- ロータ側マイコンはちっこいの＋テープLEDで楽々点灯にしよう
- ステータ側からの入力（光通信）はまた別途にしよう


# 3. 設計
- 回路図
左側がM5からのステータ側回路、コイルでロータ側に送電する。
右側がロータ側回路、ステータ側から受電して、SeeeduinoでテープLEDを制御するだけ。
一応IRセンサの検討も残している。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1112433/0c7dfbc6-6e9d-f096-f43d-3f7af959cf2a.png)

筐体は3Dプリンタで作成。
パーツはいろいろあるが割愛。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1112433/3d962e62-fd5b-9f12-66f7-16ae236ae926.png)

モータードライバは使わず、M5からpwmでnmosを動かして、モーター回転数を制御しました。
動かしてみると、結構な音がします。
モータのコイルの励磁音だそうですね。
高周波にすると聞こえなくなるかなと思いましたが、一定以上ではﾓｰﾀが回転しませんでした。
nmosの追従的には問題ない周波数のはずですが、ﾓｰﾀのインダクタンスとか関係するんですかね。
よくわかんないので、まぁそういうもんかと納得します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1112433/3441708d-35fa-cd64-2c16-b9e6250f9079.png)

ただ、若干不快な音を鳴らすだけだと芸がないので、
ドレミファインバータよろしく、音を奏でてみようと思いました。

vscode上のcopilotちゃんにお手伝いしてもらいながらコード作成しました。
メロディラインは結局出してもらえず、気合で手打ちしました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1112433/8ab76c81-0ad5-c836-c633-7ef214907368.png)

できあがったコードがこちらです。

<details><summary>長いので折り畳み</summary><div>

```php
// #include "FS.h"
// #include "SPIFFS.h"
// #include "M5StickC.h"
#include <M5StickCPlus.h>
#include "M5Display.h"
// #include <WiFi.h>
// #include <WiFiClientSecure.h>
// #include <HTTPClient.h>
// #include <Adafruit_MLX90614.h>
// #include "esp_deep_sleep.h"
// #include "esp_sleep.h"
// #include "esp_system.h"
// #include "time.h" //時刻取得用
// #include <Wire.h>
#include "AXP192.h"//m5の電源管理ic

//ピン設定
int buttun_A = 37;
int buttun_B = 39;
int led_R = 10;
int IR_pin = 36;
int PWM_pin = 26;

//変数宣言
int pwm = 48;
volatile int pulseCount = 0;
int IR_state = 0;
bool pre_IR = 0;
unsigned long previousMillis = 0;
unsigned long currentMillis = 0;
unsigned long previousMillis2 = 0;
const long interval = 1000;  // 1秒ごとに回転数を計算する
double rpm = 0.0;
int PWMCH = 0; //PWMのチャンネル
int PWM_F = 523; //PWMの周波数

unsigned long sleep_timer = 30000;
double V_bat=0;
double I_bat=0;

//ディスプレイ
int width = 80;
int height = 160;
int size_state_x=80;
int size_state_y=16;
int size_val_x=80;
int size_val_y=16;
int pos_state_x=0;
int pos_state_y=0;
int pos_value_x=0;
int pos_value_y=30;

//モーター音
int len0=1000;//1ぱく分の長さ
int len1=500;//0.5はく分の長さ

//musicのリスト数
int music_num;
//musicのトラック番号
int track;

//音程のリスト
String notes[] = {
    "C4", "C#4", "D4", "D#4", "E4", 
    "F4", "F#4", "G4", "G#4", "A4", 
    "A#4", "B4", "C5", "C#5", "D5", 
    "D#5", "E5", "F5", "F#5", "G5", 
    "G#5", "A5", "A#5", "B5"
    };
int frequencies[] = {
    262, 277, 294, 311, 330, 
    349, 370, 392, 415, 440, 
    466, 494, 523, 554, 587, 
    622, 659, 698, 740, 784, 
    831, 880, 932, 988
    };


String Melody_list[3][96]={
    {
        //senbonzakura
    "D4", "F4", 
    "A4", "A4", "A4", "A4", 
    "A4", "A4", "A4", "A4",
    "C5", "D5", "G4", "F4", 
    "A4", "A4", "D4", "F4", 
    "A4", "A4", "A4", "A4", 
    "A4", "A4", "A4", "A4", 
    "A#4","A4", "G4", "F4",
    "F4", "F4", "D4", "F4",
    "A4", "A4", "A4", "A4", 
    "A4", "A4", "A4", "A4", 
    "C5", "D5", "G4", "F4", 
    "A4", "A4", "D4", "F4",  
    "A#4","A#4","A4", "A4", 
    "G4", "G4", "F4", "F4",
    "G4", "F4", "A4", "C5", 
    "D5", "D5", "D5", "D5",
    "", "", "", "",
    "", "", "", "",
    "", "", "", "",
    "", "", "", "",
    "", "", "", "",
    "", "", "", "",
    "", "", "", "",
    "", "", 
    },
    {
        //idol
    "E4", "G4",
    "A4", "A4", "E5", "E5",
    "A4", "A4", "G4", "A4",
    "A4", "E5", "A4", "G4",
    "A4", "A4", "E4", "G4",
    "A4", "A4", "C5", "C5", 
    "B4", "B4", "G4", "A4",
    "A4", "B4", "C5", "D5",
    "F5", "F5", "E5", "E5",
    "A4", "A4", "E5", "E5",
    "A4", "A4", "G4", "A4",    
    "A4", "E5", "A4", "G4",
    "A4", "A4", "E4", "G4",
    "A4", "A4", "C5", "C5", 
    "B4", "B4", "G4", "A4",
    "A4", "A4", "A4", "G4",
    "A4", "A4", "C5", "C5",
    "A4", "A4", "A4", "G4",
    "A4", "C5", "D5", "E5",
    "A4", "A4", "A4", "G4",
    "A4", "C5", "D5", "E5",
    "D5", "E5", "A4", "C5",
    "G4", "A4", "E4", "A4",
    "A4", "A4", "A4", "G4",
    "A4", "A4"  
    },
    {
        //sangensyoku
    "A4", "B4",
    "C5", "C5", "E5", "F5",
    "E5", "D5", "D5", "E5",
    "D5", "C5", "C5", "B4",    
    "C5", "C5", "A4", "B4",
    "C5", "C5", "E5", "F5",
    "E5", "D5", "D5", "E5",
    "D5", "C5", "C5", "G5",
    "E5", "E4", "A4", "B4",
    "C5", "A4", "C5", "C5",
    "D5", "C5", "D5", "D5",
    "E5", "D5", "E5", "G5",
    "A5", "G5", "E5", "A4",
    "C5", "C5", "G5", "C5",
    "C5", "G4", "D5", "C4",
    "", "", "", "",
    "", "", "", "", 
    "", "", "", "",
    "", "", "", "",
    "", "", "", "",
    "", "", "", "",
    "", "", "", "",
    "", "", "", "",
    "", "", "", "",
    "", ""        
    }
};


String senbonzakura[96] = {
    "E4", "G4",
    "A4", "A4", "E5", "E5",
    "A4", "A4", "G4", "A4",
    "A4", "E5", "A4", "G4",
    "A4", "A4", "E4", "G4",
    "A4", "A4", "C5", "C5", 
    "B4", "B4", "G4", "A4",
    "A4", "B4", "C5", "D5",
    "F5", "F5", "E5", "E5",
    "A4", "A4", "E5", "E5",
    "A4", "A4", "G4", "A4",    
    "A4", "E5", "A4", "G4",
    "A4", "A4", "E4", "G4",
    "A4", "A4", "C5", "C5", 
    "B4", "B4", "G4", "A4",
    "A4", "A4", "A4", "G4",
    "A4", "A4", "C5", "C5",
    "A4", "A4", "A4", "G4",
    "A4", "C5", "D5", "E5",
    "A4", "A4", "A4", "G4",
    "A4", "C5", "D5", "E5",
    "D5", "E5", "A4", "C5",
    "G4", "A4", "E4", "A4",
    "A4", "A4", "A4", "G4",
    "A4", "A4"  
    };

String idol[96] ={
    "E4", "G4",
    "A4", "A4", "E5", "E5",
    "A4", "A4", "G4", "A4",
    "A4", "E5", "A4", "G4",
    "A4", "A4", "E4", "G4",
    "A4", "A4", "C5", "C5", 
    "B4", "B4", "G4", "A4",
    "A4", "B4", "C5", "D5",
    "F5", "F5", "E5", "E5",
    "A4", "A4", "E5", "E5",
    "A4", "A4", "G4", "A4",    
    "A4", "E5", "A4", "G4",
    "A4", "A4", "E4", "G4",
    "A4", "A4", "C5", "C5", 
    "B4", "B4", "G4", "A4",
    "A4", "A4", "A4", "G4",
    "A4", "A4", "C5", "C5",
    "A4", "A4", "A4", "G4",
    "A4", "C5", "D5", "E5",
    "A4", "A4", "A4", "G4",
    "A4", "C5", "D5", "E5",
    "D5", "E5", "A4", "C5",
    "G4", "A4", "E4", "A4",
    "A4", "A4", "A4", "G4",
    "A4", "A4"    
};

String sangensyoku[96] ={
    "A4", "B4",
    "C5", "C5", "E5", "F5",
    "E5", "D5", "D5", "E5",
    "D5", "C5", "C5", "B4",    
    "C5", "C5", "A4", "B4",
    "C5", "C5", "E5", "F5",
    "E5", "D5", "D5", "E5",
    "D5", "C5", "C5", "G5",
    "E5", "E4", "A4", "B4",
    "C5", "A4", "C5", "C5",
    "D5", "C5", "D5", "D5",
    "E5", "D5", "E5", "G5",
    "A5", "G5", "E5", "G5",
    "C5", "C5", "G5", "C5",
    "C5", "G4", "D5", "C5",
    "", "", "", "",
    "", "", "", "", 
    "", "", "", "",
    "", "", "", "",
    "", "", "", "",
    "", "", "", "",
    "", ""
};

// 音程から周波数を取得する関数。mapっぽい関数。
int getFrequency(String note) {
    int arraySize = sizeof(frequencies) / sizeof(frequencies[0]);
    for(int i = 0; i < arraySize; i++) {
        if(notes[i] == note) {
        return frequencies[i];
        }
    }
  return -1; // 音程が見つからなかった場合
}

TFT_eSprite sprite = TFT_eSprite(&M5.Lcd);

//バッテリ電圧を取得
void get_bat(){
    V_bat=M5.Axp.GetBatVoltage();
    I_bat=M5.Axp.GetBatCurrent();   
}


void IRAM_ATTR countPulse() {
    pulseCount++;
    // Serial.println("interrupt on");
}


void rot_count(){
    currentMillis = millis();
    if (currentMillis - previousMillis >= interval) {
        // Serial.println("count_culc");
        int t=currentMillis - previousMillis;
        previousMillis = currentMillis;
        noInterrupts();
        rpm = double(pulseCount) * (double(t)/1000);  // 1パルス1回転、
        interrupts(); 
        disp_val(String(int(rpm))+"rpm"+", "+String(pulseCount)+"cnt");
        pulseCount = 0;
    }
}

void led(){
    ledcSetup(PWMCH, PWM_F, 8);
    ledcAttachPin(PWM_pin, PWMCH);
    Serial.println("led start");
    delay(100);
    disp_state("LED mode");
   
    ledcWrite(PWMCH, 32*4);
    delay(800);

    ledcWrite(PWMCH, pwm);
    delay(50);
    Serial.println("melody start");
    // int melody_size=sizeof(melody)/sizeof(melody[0]);
    int melody_size=sizeof(Melody_list[track])/sizeof(Melody_list[track][0]);
    Serial.println("melody_size:"+String(melody_size));
    for(int i=0;i<melody_size;i++){
        M5.update();
        rot_count();
        if(getFrequency(Melody_list[track][i])==-1){
            Serial.println("melody end");
            ledcWrite(PWMCH, 0);
            delay(250);
            break;
        }
        //ボタンAが押されてたら、melodyを中断する
        if(M5.BtnA.wasPressed()){
            Serial.println("btn A pressed");
            ledcWrite(PWMCH, 0);
            delay(1000);
            break;
        }
        ledcSetup(PWMCH, getFrequency(Melody_list[track][i]), 8);
        delay(175);
    }
    track=track+1;
    if(track>=music_num){
        track=0;
    }
    ledcWrite(PWMCH, 0);
    previousMillis2 = millis();
    Serial.println("led finish");
}

void disp_state(String text){
  sprite.createSprite(size_state_x, size_state_y);
  sprite.setCursor(0,0);
  sprite.setTextFont(2);
  sprite.setTextColor(BLUE,WHITE);//文字色,背景色
  sprite.print(text.c_str());
  sprite.pushSprite(pos_state_x,pos_state_y);
  sprite.deleteSprite();
}

void disp_val(String text){
    sprite.createSprite(size_val_x, size_val_y);
    sprite.setCursor(0,0);
    sprite.setTextFont(2);
    sprite.setTextColor(BLACK,WHITE);//文字色,背景色
    sprite.print(text.c_str());
    sprite.pushSprite(pos_value_x,pos_value_y);
    sprite.deleteSprite();    
}

void setup() {
    //pwm初期設定
    ledcSetup(PWMCH, PWM_F, 8);//
    ledcAttachPin(PWM_pin, PWMCH);

    //M5初期設定
    M5.begin();
    M5.Lcd.setRotation(0);
    M5.Lcd.fillScreen(BLACK);

    //pin設定
    pinMode(buttun_A, INPUT_PULLUP);
    pinMode(buttun_B, INPUT_PULLUP);
    pinMode(led_R, OUTPUT);
    pinMode(PWM_pin, OUTPUT);
    pinMode(IR_pin, INPUT_PULLUP);    
    esp_sleep_enable_ext0_wakeup(GPIO_NUM_37, 0); 

    //https://coconala.com/blogs/261186/186780/
    // pinMode(36, INPUT);
    gpio_pulldown_dis(GPIO_NUM_25);
    gpio_pullup_dis(GPIO_NUM_25);

    // IRカウント用にRISINGエッジで割り込み設定
    attachInterrupt(digitalPinToInterrupt(IR_pin), countPulse, RISING); 

    //music用の設定
    music_num=sizeof(Melody_list)/sizeof(Melody_list[0]);
    track=random(0,music_num);

    disp_state("START");

    //led_Rを0.5秒刻みで3回点滅
    for(int i=0;i<3;i++){
        digitalWrite(led_R, HIGH);
        delay(100);
        digitalWrite(led_R, LOW);
        delay(100);
    }
}


void loop() {
    M5.update();
    
    rot_count();
    disp_state("waiting…");

    //ボタンAが長押しされたら、Deep sleepする
    if(M5.BtnA.pressedFor(3000)){
        M5.update();
        //画面をクリア
        M5.Lcd.fillScreen(BLACK);
        Serial.println("A pressed Deep sleep");
        disp_state("SLEEP");
        delay(2000);

        M5.Axp.SetSleep(); 
        ESP.deepSleep(24*3600*1000000);//24時間待機
        delay(1000);
    }

    //ボタンAが押されたらled関数を開始
    if(M5.BtnA.wasPressed()){
        M5.update();
        //画面をクリア
        M5.Lcd.fillScreen(BLACK);
        led();
        currentMillis = millis();
        disp_state("waiting…");
        delay(100);
    }    
    //sleep_timer経過でDeep sleepする
    if(currentMillis - previousMillis2 >= sleep_timer){
        //画面をクリア
        M5.Lcd.fillScreen(BLACK);
        Serial.println("sleep_timer Deep sleep");
        disp_state("SLEEP");
        delay(2000);

        M5.Axp.SetSleep(); 
        ESP.deepSleep(24*3600*1000000);//24時間待機
        delay(1000);
    }
}
```
</div></details>

# 4. 作製
できあがったハードはこちら。
回転体はあぶないので、100金のプラ容器買ってきて適当にボルト締結でカバーにしました。
ロータのマイコンがDigisparkになってますが、動かしているうちに死亡(?)したので、
先の回路のとおり、最終的にはSeeeduino xiaoになってます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1112433/b6d1ff98-f7fc-196d-df37-32d30142a651.png)


組み立てて動かしてみた動画はこちらです。

<iframe width="348" height="619" src="https://www.youtube.com/embed/IfiXgGeyDjI" title="povで千本桜" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>

曲を増やしてみました。
カバー外れてるけど気にしない。
<iframe width="348" height="619" src="https://www.youtube.com/embed/gDZwxpLelvg" title="pov" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>

と、いい感じに動いているように見えますが、
実はM5stickにはusbで給電されてます。
内臓電池単体では電流消費が大きいため、すぐ枯渇します。
Cheero canvas使うといい感じに外部電源供給チックで安定的に動かせるのは確認済み。

M5stick必要なかったな…


# 5. おわりに
あれ？バーサライタ作ってたはずなのに、音が出て満足してしまった。。。
ということで回転して光る電子オルゴールができました。
これはこれで良しとして、次回に続く。

なお、回転数でのpid制御で回転速度をきっちりやろうと思いましたが、
開発過程でミスってM5から大電流が繰り返し流して電源ICの遮断を繰り返した結果、
1台M5stickをオシャカにしてしまいました。
モータ電流大きすぎるので、電流制限の抵抗をもっと大きめにしたほうがよさげ。（やらない）