製作モジュール一覧

2018年8月19日日曜日

Nucleo DCOの構想 ~ いわゆるプロダクト・アウト

需要ははあまりなさそうなテーマですが、Arduino LFOと同じような仕組みでハイスペックなNucleoF767でDCOを試作したいと思います。

OTAを使ったVCAのDual OTA VCA、トランジスターラダーLPFのTLF01とアナログ回路で組んできたので、VCOもアナログ回路で組みたいところですが、一度、音源として汎用的に使えそうなデジタル・オシレーターを作ってみようと思います。

Arduino + MCP4922ではいいとこLFO程度が上限でしたが、Nucleo F767なら12bit DACを内蔵、動作クロック216MHz、double型のFPU内蔵、メモリも潤沢(Flash 2MB/RAM 512kB)、mbedでプログラミング可ということでNucleo F767をコアとして試作することにしました。

仕様案


1) 内蔵12bit DAC 1ch出力

Nucleo F767は2ch分のDACが内蔵されていますが、1つはオンボードのLEDと接続されていて出力波形が汚いので1chのみとします。

参考「Nucleo F446RE 内蔵DACのテスト

2) サンプリング・レート 48kHz以上

最低でも48kHzとします。12bit/48kHzなので20年ぐらい前のサンプラー相当です。

I2Sならオーディオ品質のDACを使えますが、とりあえず楽をする方向で考えます。I2SはPSoC 5LPで使ったことがありますが、NucleoやESP32で使ったことがないので後々。

参考「オーディオ用DACを使ったファンクションジェネレータ まとめ

3) 出力波形

SQR、SAWUP、SAWDOWN、TRI、SIN、NOISE。Arduino LFOでは計算時間が足りなくてNoiseは出力できませんでしたがNucleoならできそうです。

4) 3オシレーター

1オシレーターだと音が薄いので、3オシレーターをプログラム上でMIXして出力します。サンプリングレートを下げていけばできそうな気がしますが、問題は操作子が増えることです。1オシレーターで妥協するかもしれません。1~2オクターブ下の周波数を出力するサブオシレーターにするかも?出音を比較しつつ。

5) 出力周波数

ピアノの音階の27.5Hz~4,186Hzより広くするつもりです。POTの解像度が10bit(1024)程度しかないので、レンジ切り替えで対応すると思います。

6) 出力電圧

最低、-∞ ~ ±2.5V程度で考えています。DC成分のない両電源波形です。

1オシレーターの場合のブロック図

ほとんどArduino LFOと同じ構成ですが、両電源波形にするためDAC出力をACカップリングしています。プッシュスイッチを使ってレンジ、波形を切り替えると、フィードバックが必要になるので表示器としてLCDかOLEDを使うつもりです。

ロータリースイッチを使えば表示器は不要ですが、線数が増えまくります。

Phase POTは位相を可変するもので、1オシレーターだとあまり意味はがないんですが、複数あればオシレーターごとに位相をずらしてうねりをつくれます。

外観案


1オシレーターならこれぐらいのサイズで収まりそうです。3オシレーターにしようと思うとPOTx16 PizzaBoxぐらいの規模になりそうです。

内蔵DACからの出力波形


1kHzのサイン波を出力するプログラムを書いて出力波形を見てみました。サンプリングレートはとりあえず100kHzにしました。

DDSにつかうWaveテーブルは32,768要素にしました。簡単に出力テストしてみると、これ以上要素数を増やしてもあんまり効果はなさそうでしたが、メモリーが潤沢なので要素数は増やせます。

mbed Repository
https://os.mbed.com/users/ryood/code/Nucleo-F767_DCO_Test01/

処理時間の計測

ch1:D4 ch2:D5

ch1:D4はメインループ内で1マイクロ秒のWaitをかけてH/Lしていて、ch2:D5はサンプリング周期毎の割り込み処理の最初と最後でH/Lしています。

サンプリングレートが100kHzなのでch2の周波数は100kHzになっています。Hになっている時間(PW)は720.0nsでまずまず計算する時間の余裕はありそうです。

オシロで見ると、ch2の波形が多少ブルブル震えています。←ジッターあり。

ch1は、ch2の割り込みのタイミングで1マイクロ秒より引き伸ばされる場合があります。

以下、電源ノイズを避けるために、NucleoのUSB電源を電池使用の安定化電源にして測定しました。


出力波形

普通に内蔵DACで出力すると波形の上下でクリップするので、ファームウェアで小細工して3Vp-p弱の振幅にしました。ファームウェアの以下の部分です。

    //uint16_t v = val << 4;
    // avoid distortion of the built-in DAC
    uint16_t v = (val + 256) * 14;

出力波形(拡大)

ガタガタの幅は10usで、逆数は100kHzなのでサンプリングレートと合致します。

FFT

横軸は25kHz/DIVで100kHzと200kHz付近にエイリアスが現れています。

WaveSpectra

Audio Interface: TASCAM US-144II、MIC/LINE入力、24bit/96kHz
窓関数: Flat Top

THD: 0.03635%
THD+N: 0.19748%

出力にLPFをかけるにしても可聴帯域の20kHzあたり以下の帯域は通過域です。

メモ:


Nucleo F767のZIOヘッダは、Arduinoのシールドを使うと内側の穴が隠れてしまって使いづらい。

サンプリングレートを決めてからLPFを設計する。

MIDI対応できるように考えておく。

SyncIn必要?

2018年8月17日金曜日

KIK01 サイドパネルの塗装

サイドパネルをニス(透明クリア)で塗装しました。

板材は杉です。ホムセンで100円ぐらいだったと思います。

手順です

とのこ塗り込み後、養生テープで裏面をマスキング


マスキングしましたが、塗装ごとにきっちり養生テープを貼るのはつらいので最初の一回のみにしました。


ニスがたれてしまったりうまく行かなかったのですが、ようすを見ながらニスを刷毛で2回ぐらい塗りました。



フレーム(黒色塗装)は大失敗しましたが、サイドパネルをつけるとそこそこ高級な機材のように見えると思います。

水性塗料の塗装


水性塗料を水で薄めてMDFの端材を塗装テストしました。

下地はとのこ(アサペン)→プラサフ(Soft99)です。


左はAlescoの水性塗料を水で薄めて塗りました。右はさらにダイソーの「補修用アクリルスプレー つや消しクリア」を塗布しました。

つや消しスプレーを近づけすぎて濃いめに塗布すると塗面がバキバキになりました。


メモ:

これぐらいの小さい作品だと、多少コストはかかりますがニスでも塗料はスプレータイプの方がいいんでは?と思いました。

はけを洗ったりニスを薄めたりするのに使うペイントうすめ液のコスト(お金と手間暇)がバカになりません。

2018年8月16日木曜日

ESP WROOM 02(ESP8266)でBME280とSSD1306 OLED(I2C)を使う。

ESP WROOM 02(ESP8266)で、気温、湿度、気圧センサーのBME280とSSD1306 OLEDを同時に使用しました。

SSD1306はI2C接続のものとSPI接続のものがありますが、ESP8266のSPIは使ったことがないので、とりあえずI2C接続のものを使用しました。

I2Cは信号線をプルアップして使うので、I2CとSPIで信号線を共有するのは難しそうです。ESP8266はIOで使えるピンが少ないので、SPIとI2Cを同時使用するにはちゃんと検証する必要がありそうです。

ESP8266のI2Cの使い方もよくわかっていないので、ライブラリ等ありものを使わせていただいてとりあえず動作させるのを優先しました。

ブレッドボード配線図


運用時(USB-UART変換モジュールなし)

I2Cの信号線はBME280モジュールでもSSD1306モジュールでもオンボードでプルアップされているので、理屈としてはそれぞれの抵抗を並列した値になります。
10kΩ // 4.7kΩ ≒ 3.2kΩ
Lの時に (3.3V / 3.2kΩ) ≒ 1mA 程度流れます。SCLとSDAがあるので無視できない消費量ですね。

Arduinoのスケッチ


BME280のライブラリは「SparkFun BME280 Arduino Library」を使用していましたが、問題がありました。

I2CのピンがデフォルトでハードウェアI2Cのものを使うようになっていて、自由にピンを割り当てられるソフトウェアI2Cを使うのが少しめんどうです。

スケッチ例に「Example9_SoftwareI2C.ino」があって、Arduino IDEのライブラリマネージャから「SoftwareWire by Testato」をインストールして使うようになってます。

他にI2CのピンをアサインできるBME280のライブラリを探してみると「Arduino-ESP8266-BME280」がありました。(Ambientさんでした)

exampleもAmbientに送るようになっているのでそのまま使わせていただくことにしました。

SSD1306のライブラリはu8g2を使いました。

参考
HiLetogo 0.9" I2C OLED 128X32をu8g2で使ってみる。
u8g2のFull BufferとPage Buffer

BME280、SSD1306のどちらのライブラリもArduinoのI2C制御クラスのWireオブジェクトをクラス内で初期化(begin())していて同時に使用するのは怖いんですが、ArduinoのI2C制御クラスのWireはSingletonになっているとおもうので(←確証なしです)、SCLとSDAに使うPINを合わせておけばだいじょうぶかな~と半分やけくそで使いました。

結果としては動作しているように見えます。

WiFiでAmbientにデータを送信するようにして、DeepSleepをかけました。

<Ambient_ESP8266_BME280_u8g2.ino>

#include <ESP8266WiFi.h>
#include <Wire.h>
#include "BME280.h"
#include "Ambient.h"
#include "U8g2lib.h"
#include "etc/config.h"

extern "C" {
#include "user_interface.h"
}

#define UART_TRACE (1)
#define PIN_CHECK  (1)
#define USE_OLED   (1)

#define TITLE_STR1  ("ESP8266 BME280 Thermo")
#define TITLE_STR2  ("2018.08.13")

#define LED 4
#define SDA 14
#define SCL 13

#define PERIOD 60

/*
  const char* ssid = "ssid";
  const char* password = "password";
  unsigned int channelId = 100;
  const char* writeKey = "...writeKey...";
*/

WiFiClient client;
Ambient ambient;
BME280 bme280;
#if (USE_OLED)
U8G2_SSD1306_128X32_UNIVISION_F_SW_I2C u8g2(U8G2_R0, /* clock=*/ SCL, /* data=*/ SDA, /* reset=*/ U8X8_PIN_NONE);
#endif

float calcDiscomfort(float temperature, float humidity) {
  return 0.81 * temperature + 0.01 * humidity * (0.99 * temperature - 14.3) + 46.3;
}

void setup()
{
  int t = millis();
  //wifi_set_sleep_type(LIGHT_SLEEP_T);

  struct rst_info* p_reset_info = ESP.getResetInfoPtr();

#if (UART_TRACE)
  Serial.begin(115200);
  delay(10);
  Serial.println("");
  Serial.println("");
  Serial.println(TITLE_STR1);
  Serial.println(TITLE_STR2);
  Serial.print(ESP.getResetReason());
  Serial.print("\t");
  Serial.println(p_reset_info->reason);
  Serial.print("Start");
#endif
#if (PIN_CHECK)
  pinMode(LED, OUTPUT);
  digitalWrite(LED, HIGH);
#endif

  // BME280
  bme280.begin(SDA, SCL);
  // 電源投入直後の値は不安定なので、読み捨てる
  bme280.readTemperature();
  bme280.readHumidity();
  bme280.readPressure();

  double temperature = 0.0;
  double humidity = 0.0;
  double pressure = 0.0;
  float discomfort = 0.0;

  temperature = bme280.readTemperature();
  humidity = bme280.readHumidity();
  pressure = bme280.readPressure();
  discomfort = calcDiscomfort(temperature, humidity);

#if (UART_TRACE)
  Serial.print("temp: ");
  Serial.print(temperature);
  Serial.print(" DegC,  humid: ");
  Serial.print(humidity);
  Serial.print(" %, pressure: ");
  Serial.print(pressure);
  Serial.print(" hPa ");
  Serial.print(discomfort);
  Serial.println("");
#endif

#if (USE_OLED)
  u8g2.begin();
  if (p_reset_info->reason != REASON_DEEP_SLEEP_AWAKE) {
    u8g2.clearBuffer();
    u8g2.setFont(u8g2_font_7x14_tf);
    u8g2.drawStr(0, 16, TITLE_STR1);
    u8g2.drawStr(0, 32, TITLE_STR2);
    u8g2.sendBuffer();
    delay(3000);
  }

  char strBuffer1[16];
  char strBuffer2[16];
  
  u8g2.clearBuffer();
  u8g2.setFont(u8g2_font_logisoso20_tf);
  sprintf(strBuffer2, "%s%cC", dtostrf(temperature, 2, 1, strBuffer1), 0xb0);
  u8g2.drawStr(0, 32, strBuffer2);
  u8g2.setFont( u8g2_font_helvR14_tr);
  sprintf(strBuffer2, "%s%%", dtostrf(humidity, 2, 1, strBuffer1));
  u8g2.drawStr(78, 16, strBuffer2);
  u8g2.drawStr(78, 32, dtostrf(discomfort, 2, 1, strBuffer1));
  u8g2.sendBuffer();
#endif

  // WiFi
  WiFi.begin(ssid, password);

  int i = 0;
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);

#if (PIN_CHECK)
    digitalWrite(LED, i++ % 2);
#endif
#if (UART_TRACE)
    Serial.print(".");
#endif
  }
#if (PIN_CHECK)
  digitalWrite(LED, LOW);
#endif

#if (UART_TRACE)
  Serial.println("WiFi connected");
  Serial.print("IP address: ");
  Serial.print(WiFi.localIP());
  Serial.println("");
#endif

  ambient.begin(channelId, writeKey, &client);
  ambient.set(1, temperature);
  ambient.set(2, humidity);
  ambient.set(3, pressure);
  ambient.set(4, discomfort);
  ambient.send();

  t = millis() - t;
  t = (t < PERIOD * 1000) ? (PERIOD * 1000 - t) : 1;

  ESP.deepSleep(t * 1000, RF_DEFAULT);
  delay(1000);
}

void loop()
{
}

スケッチ冒頭の

struct rst_info* p_reset_info = ESP.getResetInfoPtr();

でResetの理由を保持するreset_infoを取得して、起動時にタイトル画面をOLEDに表示する処理を、Sleep復帰時には

if (p_reset_info->reason != REASON_DEEP_SLEEP_AWAKE) {
...
}

としてバイパスするようにしています。

ESP.getResetReason()

で、整数値ではなくString型でResetの理由も取得できます。

Sleep中もOLEDに電源を供給しておけば表示され続けますが、Sleepからの復帰時にOLEDが初期化されるので一瞬OLEDの表示が消えます。

電池ももったいないし、OLEDは動作確認用として、長期運用する場合は外しておくつもりです。

もっと細かい周期で環境を測定するためには、例えばArduino Nanoを使ってUSB経由でロギングするものを作ればいい?

Ambientのグラフ


電源電流


電源に0.1ΩのRを直列に入れOWON B35(DC/mV)で測定(1mV→10mA換算)。1秒周期

OLED、LEDあり

Avg 1.2414 mV
Min 0.59 mV
Max 12.83 mV

WakeUp時に80mA~130mA程度、Sleep時に6mA程度

OLED、LEDをブレッドボードから抜いた

Avg 0.614133333 mV
Min 0 mV
Max 12.35 mV

WakeUp時に70mA~125mA程度、Sleep時に0mA程度

OLED、LEDがあると常時6mA程度消費します。

I2C信号


ch1:SDA ch2:SCL

IC2クロックは多少揺れがあり、100kHzより少し遅いぐらいです。←Standard Modeぐらい。

と、なんとか不快指数計をWiFi対応させたところで、猛暑のピークは過ぎたようです(@@;