2018年3月30日金曜日

ダンボールでないAIYケース


私は不器用なので、ダンボール製のAIYは見事に基板が斜めになってあちこちスキマがorz

これではちょっと家に置いておきにくい、ということで、またCAD頼みで工作しました。


makercase.comで設計してAdobe Illustratorで穴あけ指示を行い、Elecrowでカットしてもらいました。接着剤なしでぴったりと組み上がり、基板ももちろん水平です(笑)。

難点は分解がちょっと大変w

音出してみたらダンボールと違って結構良い音がでます。

【追記】取説

わりと好評だったのでスイッチサイエンスさんから売ってもらおうかと思っていたのですが、MakerCase作者の方とコンタクトが取れません。権利関係がわからないものを売るわけにはいかないので、販売予定はありません。ご了承ください。

以下は商品化のために書いていたマニュアルです。せっかく書いたので貼っておきます。

レーザーカットしたときのアクリルの匂いが凄いので荷物が届いたらまず窓を開けます。

中にはプチプチにくるまったアクリル板と2つのビニール袋とゴム足シートと品質管理/組立ライン用手袋が入ってます。

ビニール袋は白い柱みたいなのがM2.6のラズパイ固定用で底面から順番にネジ|アクリル板|10mmスペーサー|ラズパイ基板|12mmスペーサー|ワッシャー|ナットの順。もう一つ白いのはスピーカー用で外からネジ|ワッシャー|アクリル板|スピーカーフレーム|ワッシャー|ナットの順。

アクリル板から保護シート剥がす前に、底面のどのへんにゴム足を付けるかポンチかキリで印つけておくとずれないです。ラズパイ固定ネジ穴と干渉しないように気をつけること。

あとアクリル板同士をがっちり噛ませてしまうとほぼ外すのは無理(特に6面のうちの最後の2面)になるので、まずどの組み合わせになるのかをざっと並べてみておくこと。ラズパイのコネクタ穴は似通っているので注意。側面パネルは4種類とも違うけど、手前にスピーカーが来た場合、右がSD、左がEther+USB、奥がHDMIその他になります。地板の向きを側板に合わせます。天板はスピーカーの反対側にマイク。いずれも対称軸に注意。

このへんで手袋します。保護シートを剥がすには強めの荷造りテープを貼って剥がすか、目立たない隅っこを尖ったピンセットなどで剥がします。

まず底面の片面の保護シート剥がします(内側)。外側になるシートは全部剥がさずにラズパイ部分だけはがしておいて、ラズパイ取り付けます。向きに注意。ホコリに注意。キムワイプなどで拭いても静電気が強くなるだけなので、エアーブロワーなどで吹き飛ばします。

スピーカー、これは両面剥がして取り付けるほうが良いっすね。スピーカーがずれないように注意。ネジは対角線の位置から締めること。くれぐれもホコリに注意。

スイッチとマイク基板取り付け。マイク基板は3MのW-18という透明両面テープで貼るのがおすすめです。この手袋だとテープの接着面に触っても糸くず等があまり残らないので、素手で扱うよりも楽です。素手だと確実に指紋が付きますからね。

残りのアクリル板、内側の保護シートを剥がしつつ組み立てて行きます。はめるときには水平垂直を維持しながらまっすぐ押し込む方が入りやすいです。というか板が傾いていると入らないです。組み立てながら内側のホコリを取る。

最後の一枚を入れる前にちゃんとブートすること機能することなどを確かめましょう。ピンセット使えば外からSDカード出し入れできますが、配線ミスなどを直そうとしても閉めちゃってからまた開けるのはかなり難しいので。

最後の一枚をはめたら残る保護シートをはがします。ゴム足を貼ります。ゴム足は一度貼ると簡単には剥がれないので一発で確実に貼ります。

完成です。手袋はずして鑑賞して下さい。

ホコリはとにかくものすごく目立つのでご注意を。

2018年3月23日金曜日

Pmod MTDSに一言

MTDS Library Programmers's Reference Manualさん。

word形式なのは許す。

でも目次がないってどゆこと?

もしかしてwordで開くと目次出てきたりするんだろうか。

いずれにしても、もう一生Pmodは買わない。

2018年3月19日月曜日

超低価格大容量SPI Flash(ただし遅い)

【写真】予備を作ると壊れもせず一発で動く…私はそういう星の下に生まれた男だ。

W25Q32FVSSIGっていうチップを入手しました。SPI接続のフラッシュメモリです。

結論として、消去がやたら遅い(6秒程度)ので別タスクで消す等の処理は必要ですが、4MBのストレージが約50円しかも1.27mmピッチのSOICパッケージというのは扱いやすくて良いんじゃないでしょうか。

digi-keyなどでは生産中止品扱いなのですが、中国でセカンドかサードソースかコピー品が生産されているのでしょう。

■準備■

とりあえず使ってみる場合、「Arduino チップ名」か「ESP32 チップ名」でググってみると何かライブラリが出てきます。安心して配線しましょう。

SOIC、手元に変換基板がなかったので秋月のSOP8用で代用します。パターンが足の下に隠れてしまうので手ハンダではなくリフローしました。最近0.2mm間隔のチップを相手にしていたので、1.27mmなんて鼻歌です(表面張力のおかげですが)。

何度でも書きますが、自宅リフローにつかうクリームハンダはサンハヤトに限ります(笑)。これはハンダの粒子もきめ細かいですしパターン乗りもよく本当に扱いやすいです。フラックス活性が高いのかなんだか知りませんがボールなどが発生しにくいです。価格はわりと高いんですが、どうせ有効期限内に使い切れないので丁度いいのです(笑)。

Arduino IDEでESP32を選び、Library Managerで「SPIFlash」と入力するといくつかヒットしますが、とりあえずPrajwal Bhattaram氏作のSPIFlashを選びます。接続する前にESP32にFlashDiagnosticsを書き込んで起きましょう。ESP32への電源を切ってから以下の配線をします。

接続はESP32としては標準的なSPIの接続です。SOとSIはプルアップしていません。

ESP32W25Q32
IO051 CS
IO192 SO
3V33 WP
GND4 GND
IO235 SI
IO186 SCK
3V37 HLD
3V38 Vdd

■動かしてみて■

消去がものすごく遅いです。4MB消すのに6秒かかります。
Erase Chip: Data I/O test PASS Time: 5.967 s
Erase 64KB Block: Data I/O test PASS Time: 158.449 ms
Erase 32KB Block: Data I/O test PASS Time: 120.735 ms
Erase 4KB Sector: Data I/O test PASS Time: 91.357 ms

このチップ、こんなに安いんですが、100,000サイクルの消去が可能って書いてありますので、Wear Levelingは行われているみたいですね。小さい廉価品なのに大したもんだ。

【追記2018年9月5日】セクター(256bytes)で同一アドレスにひたすら消去・書き込み・読み出し…を繰り返す実験をしたら100万回越えてもエラー出ませんでした。約150万回で初めてエラーが出ました。テスト前にも相当使っていましたのでもうちょっと長いかもしれません。個体差もあるのでこの結果が保証されるというわけではありませんが、少なくとも仕様の10万回は問題ないと言えるでしょう【追記終わり】

読み書きはuSの単位なので普通に使えそうですが…Flashは消去してからでないと書き込みができない宿命なので、別タスクでメモリ消すとかリングバッファ的に管理して使う必要があります。あと、64KBずつ消すと10秒近くかかってしまいますが、一括消去だと6秒で終わるのも注目点ですね。まぁ「たかが二倍」とも言えますが。

アルゴリズムとしては1ブロック64KBとして状態を示すフラグを64個の配列にしておき、それぞれが使用中か消去済か消去要求か消去中か…を示すようにして、消去用タスクの中でループで状態を監視し、消去要求のブロックがあれば消去中に変更して消去を実行し、完了後消去済みに書き換えてから次のループ…という形でうまく動きました。タスクを使う場合は競合などに配慮する必要がありますが、この場合はタスクごとに変更可能な状態を制限しておくことで回避することができます。

使用したいブロックが消去済より少ない場合にはランダムにスキップしてやればある程度消耗度を分散させることもできます。使用中フラグは正の整数ということにして次のブロック番号を指すようにすれば簡単なシーケンシャルファイルを実装できます。

なお、Prajwal Bhattaram氏のライブラリはGNUなので、実際の装置に組み込む場合には注意が必要です。

私は例によって書き終わってからこれに気づいたので、自分で書き直しました。SPI Flashを扱うことのできるライブラリはいくつかあるんですが、みんなGNUなんですよね。幸い処理は比較的簡単です。

私が書いたやつは、そのうちお掃除したらApacheかなにかで公開します。

ESP32でI2Sから音を出す・その2

その1の方法だと再生が終わるまで帰ってきてくれないので、音が出ている最中に止めたり他の音に変更することができません。そのヘン改良したのがこちらです。

なおこちらはKickstarter製品で使ったコードの一部です。

setupではI2S関連の初期化と音を出力するためのタスクsound_loopを準備しています。

loopでは割り込み要素として加速度計を使っていて、3軸合成加速度が平均値(ほぼ重力)を一定以上越えたら音を出力するフラグsoundOnを立てます。

sound_loopではループをぐるぐる回って、大きな加速度を検知してsoundOnがonになったら音源番号iSoundを変更して再生位置iを初期化し、内側のループでi2s_push_sampleを使って1音ずつバッファにプッシュし、音を出力します。再生が終わるか、音源番号が変わったらループを抜けます。

内側のループにはウエイトを入れていないですが、DMAバッファがいっぱいになるとi2s_push_sampleで止まるので、大丈夫っぽいです(ポイかよ)。

まぁこんな感じで…。


//
// Sound
//
#include "driver/i2s.h"
#include "driver/gpio.h"
#include "ching.h"
#include "clap.h"
#include "cymbal.h"
#include "voice.h"
const i2s_port_t I2S_NUM = (i2s_port_t)0;
const i2s_bits_per_sample_t BITS_PER_SAMPLE = (i2s_bits_per_sample_t)16;
#define ADC_MODE 1
// I2S
void set_16k_16() {
i2s_config_t i2s_config;
i2s_config.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX); // Only TX
i2s_config.sample_rate = 16000;
i2s_config.bits_per_sample = BITS_PER_SAMPLE;
i2s_config.channel_format = I2S_CHANNEL_FMT_ONLY_RIGHT; //right channels
i2s_config.communication_format = I2S_COMM_FORMAT_PCM;
i2s_config.dma_buf_count = 4;
i2s_config.dma_buf_len = 1024;
i2s_config.intr_alloc_flags = ESP_INTR_FLAG_LEVEL1; //Interrupt level 1
i2s_driver_install(I2S_NUM, &i2s_config, 0, NULL);
}
//
// 加速度
//
#include <Wire.h> // Must include Wire library for I2C
#include <SparkFun_MMA8452Q.h> // Includes the SFE_MMA8452Q library
MMA8452Q accel(0x1C);
volatile int soundOn = false;
volatile int iSound = 0;
volatile int prevSound = -1;
void sound_loop(void *param) {
char *buf = NULL;
int length;
while (1) {
if (soundOn == true) {
switch (iSound) {
case 0: buf = (char*)ching; length = sizeof(ching); break;
case 1: buf = (char*)clap; length = sizeof(clap); break;
case 2: buf = (char*)cymbal; length = sizeof(cymbal); break;
case 3: buf = (char*)voice; length = sizeof(voice); break;
default: buf = NULL; iSound = 0; break;
}
soundOn = false;
prevSound = iSound;
int i = 0;
while (i < length) {
int ret = i2s_push_sample(I2S_NUM, &buf[i], portMAX_DELAY);
if (ret <= 0) {
Serial.print("return = ");
Serial.print(ret);
delay(1);
iSound = -1;
break;
} else {
i += ret;
}
if (prevSound != iSound || soundOn == true) {
break;
}
}
i2s_zero_dma_buffer(I2S_NUM);
} else {
delay(1);
}
}
}
TaskHandle_t task;
void setup()
{
Serial.begin(115200);
Serial.println("MMA8452Q Test Code!");
pinMode(13, OUTPUT);
accel.init();
static i2s_pin_config_t pin_config;
pin_config.bck_io_num = 26;
pin_config.ws_io_num = 25;
pin_config.data_out_num = 27;
pin_config.data_in_num = -1; //Not used
set_16k_16();
i2s_set_pin(I2S_NUM, &pin_config);
xTaskCreatePinnedToCore(
sound_loop, // Handler
"sound_loop", // Task name
4096, // Stack size
NULL, // Parameter
1, // Priority : 25 is the most high
&task, // task handler
1); // Application CPU
Serial.println("setup end");
}
float ave = -1;
void loop()
{
if (accel.available())
{
// First, use accel.read() to read the new variables:
accel.read();
float gravity = sqrt(accel.x * accel.x + accel.y * accel.y + accel.z * accel.z);
if (ave == -1) {
ave = gravity;
} else {
if (abs(gravity - ave) > 1000) {
digitalWrite(13, HIGH);
soundOn = true;
delay(25);
digitalWrite(13, LOW);
} else {
digitalWrite(13, LOW);
}
ave = (ave * 9.0 + gravity) / 10.0;
}
Serial.print("gravity=");
Serial.print(gravity, 3);
Serial.print(", ave=");
Serial.print(ave, 3);
Serial.print(", abs=");
Serial.print(abs(gravity - ave));
Serial.print(" ");
printCalculatedAccels();
Serial.println(); // Print new line every time.
delay(1);
} else {
Serial.println("not available");
delay(1000);
}
}
void printCalculatedAccels()
{
Serial.print(accel.cx, 3);
Serial.print("\t");
Serial.print(accel.cy, 3);
Serial.print("\t");
Serial.print(accel.cz, 3);
Serial.print("\t");
}