勝手な電子工作・・

勝手なオリジナル電子工作に関する記事を書きます

手軽なWiFi信号スキャナー(今時役立つかも)

f:id:a-tomi:20210504204502j:plain

WiFi信号スキャナーをM5スタックで作ったのでご紹介します。

昨年来、住宅地の多くで近隣のWiFiネットワークが混みあってきているのではないかなと勝手に思います。この測定器はすぐできるうえ表示も見やすいのではないかと・・(自画自賛^^)

作った経緯:居間のテレビにつないである小型PCのWiFi通信が、最近ずいぶん遅くなった事に気づきました。ひょっとすると・・と、接続を802.11g(2.4GHz)から 802.11a(5GHz)に変更しました。ルーターから結構遠いので子機(アンテナ)もロッド4本のものに交換しました。これで通信が見違えるほど高速で快適になりました。遅くなっていた原因は、このところのテレワーク急増で付近の2.4GHz帯が混み出したためではないかな・・と考えついたわけでした。

比較的よく飛ぶ2.4GHz帯ですがBluetoothにも邪魔されやすいため、我が家では周辺装置も有線接続にしていますから、きっと周囲からの影響かなと考えたわけです。そこで、すぐでき操作も簡単、ポケットにいれて持ち歩ける測定器を作ったわけでした。

これを試してビックリ。近隣に意外に多くのWiFiルーターが林立しています! 1年前の何倍も多い感じです。PCで行う普段の作業はイーサネット接続なので、気づかなかっただけと思いますが。これでみると日付や時間帯によって登場するルーターの顔ぶれが少し違うようなのも不思議。スマホからのテザリングは見当たりませんので、ひょっとすると不使用時にルーター電源をオフにする方がおられるのでしょうかね?

当地は住宅の特別な密集地でもないのに、冒頭の写真のように20個のWiFiネットワークがひしめき合っています。表示は信号の強さ(dBm=デシベルミリワット)とそのSSIDで、右側にe-cと表示してあるのは各WiFiの暗号化タイプ(Encryption Type)と使用中のチャネル(Channel)番号です。続きの2ページ目を表示すると次です。

f:id:a-tomi:20210504205512j:plain

チャネル11に多く集まっているのは少し不思議なのですが、機種による特徴もありそうですね。

使っているM5スタックは、Espressif社のWiFi&Bluetooth付きマイコンESP32に、LCDやボタン3つ等をつけて大きさ約5cmの正方形に纏め、専用ライブラリーが使えるラクチンなマイコンです。数か月前に秋月電子で3500円で購入したM5 Stack Basicなのですが、今オンラインで覗いて見ると4500円ほどに値上がりしたようです。

さて、プログラムも他のマイコンよりは簡単です。Arduino-IDEで次のように作成しました。

/****************************************************
 * WiFi signal scanner for M5 Stack Basic           *
 *  Initial Version 00 5/4,2021 (c)Akira Tominaga   *
 *  Function:                                       *
 *    Scan 802.11 b/g/n WiFi networks and           *
 *    Show dBm,SSID, Encrypt.type, & channel number.*
 *  Input/Output:                                   *
 *    No external devices.                          *
 *  Revisions:                                      *
 *    (No new versions yet)                         *
 ****************************************************/
#include "M5Stack.h"
#include "WiFi.h"
uint8_t nNW;    		// number of networks
uint8_t nN;     		// NW sequence number

void setup() { // ***** Arduino setup() *****
  M5.begin();
  M5.Lcd.setTextSize(2);
  WiFi.mode(WIFI_STA);  	// station mode
  WiFi.disconnect();    	// clear connections
  delay(100);
}

void loop() { // ***** Arduino loop() *****
#define numL 13        		// # of lines per page
  M5.Lcd.fillScreen(BLACK);    	// clear screen
  M5.Lcd.setTextColor(GREEN);
  M5.Lcd.setCursor(50, 100);
  M5.Lcd.println("Scanning..");
  nNW = WiFi.scanNetworks();
  if (nNW == 0) { 		// if no network
    M5.Lcd.setCursor(50, 100);
    M5.Lcd.println("No WiFi found");
  } else {      		// if WiFi found
    uint8_t nPages = nNW / numL + 1; // # of pages
    uint8_t lastPlines = nNW % numL; // last page lines
    if (lastPlines == 0) {	// if 0 in last page
      nPages--;			// adjust nPages
    }
    for (uint8_t page = 0; page < nPages; page++) {
      // *** draw page header
      M5.Lcd.fillScreen(BLACK);
      M5.Lcd.setTextColor(GREEN);
      M5.Lcd.setCursor(0, 0);
      M5.Lcd.print("dBm  ");   // dB mW
      M5.Lcd.print(nNW);       // number of networks
      M5.Lcd.print(" SSIDs page");
      M5.Lcd.print(page + 1);  // page number
      M5.Lcd.print("/");
      M5.Lcd.print(nPages);    // total number of pages
      M5.Lcd.println(" e-c");  // encrypt.type & channel
      // *** draw page body
      if (page == nPages - 1) { // if last page
        for ( nN = page * numL; nN < nNW; nN++) {
          listNW();
        }
      } else {
        for ( nN = page * numL; nN < (page + 1)*numL; nN++) {
          listNW();
        }
      }
      delay(10);
      if (page < nPages - 1) {
        M5.Lcd.setTextColor(GREEN);
        M5.Lcd.println("Button A for next page");
      }
      if (page >= nPages - 1) break;
      while (M5.BtnA.read() == 0) {} // wait A button
    }
  }
  M5.Lcd.setTextColor(GREEN);
  M5.Lcd.setCursor(0, 224);
  M5.Lcd.println("     Button B to re-scan");
  while (M5.BtnB.read() == 0) {} // wait B button
}

/***********************************************
  User defined function(s)
***********************************************/
// *** listNW() *** list-up detected SSIDs ***
void listNW(void) {
  int8_t dBm = WiFi.RSSI(nN);
  M5.Lcd.setTextColor(WHITE);    // default White
  if (dBm > -50) {	   	 // if strong,
    M5.Lcd.setTextColor(ORANGE); // then Orange
  } else if (dBm > -85) {      // if medium,
    M5.Lcd.setTextColor(YELLOW); // then Yellow
  }
  M5.Lcd.print(dBm); 		// dB milliWatt
  M5.Lcd.print(" ");
  String ss = WiFi.SSID(nN);	// SSID name
  uint8_t sspadlen = 18 - ss.length();
  String space = "             ";
  ss.concat(space.substring(0, sspadlen));
  M5.Lcd.print(ss);		// show SSID
  M5.Lcd.print(WiFi.encryptionType(nN));
  M5.Lcd.print("-");
  M5.Lcd.println(WiFi.channel(nN));
}
// *** end of program

 

M5 Stackは二次電池も内蔵して小さく、ポケットに収まるので持ち歩き使用にも便利ですね。

f:id:a-tomi:20210504205859j:plain

なお、信号強度のdBm(デシベルミリワット)の説明は次の記事の中に書きましたので、もし必要な方はご参照ください。

かんたんにできるWiFi信号レベル表示器(テレワークに便利) - 勝手な電子工作・・

 

今回はごく簡単なご紹介でしたが、皆様のお役に立てば幸いです。

 

©2021 Akira Tominaga, All rights reserved.

24時間の動きを表示するといいかな・・温湿度気圧計

f:id:a-tomi:20210425152822j:plain

特別な工夫はないのですが(^^;、小さな画面にグラフ表示して過去1日の変化を示す気象計です。今14時ちょうどですが、昨日の今に比べてずいぶん気圧が下がったのに気づきます。これだと今晩雨が降るかもしれませんね。

昨日から2階の和室に1日置いたのですが、窓や入口の開閉に応じて温度や湿度が少し上下します。

こういう気象計が売られているのは見かけませんが、この表示方法がほかの工作にも応用できそうなので記事にするとします。前に書いたCO2濃度計(右のカテゴリーのCO2測定から参照してください)やガンマ線表示計(同じくガイガーカウンターから)などで、この表示ならかなり便利そう。それらは時間ができたらやってみるとして。

この表示を試したのは単純なきっかけからです。マイコンの容量と速度が増し、Teensy4.0などもArduino-IDEに対応して手軽に使える有難い時代になりました、これまでのESPもですが。Raspberry Pi Picoは少し試した限りではArduino-IDEとの相性が今一つですが。

ともあれ、そういうマイコンの表示はGraphicsディスプレイにしたいものです。そこで見易いOLEDを5種類ほど試してみましたが、簡単な用途向けではサイズがまだまだ小さくて拡大鏡が必要ですね・・^^; 下の写真はTeensy4.0にSSD1306OLEDをつないでみたもの。

f:id:a-tomi:20210425153947j:plain

表示装置付きのM5-stackを使うのも便利ですが、テストでなく組み込むには無理があるし。。。

もう少し大きい表示装置が何かないかなーと手持ちの部品を漁り、このTFTカラー液晶シールドに気づきました。mcufriendとシルク印刷された2.4インチの表示器で、かつてArduino-Unoで使っていました。なぜかUnoのピンをほぼ全部塞ぐだけでなくUNOの容量が次第に不足気味となりもはや用済み、部品箱で長らく眠っていたのでした^^

f:id:a-tomi:20210425154217j:plain

裏は次です。

f:id:a-tomi:20210425154243j:plain

480x320ピクセルのカラー2.4インチ。それならいいかも!・・・。

ただし、これはUNOに合わせたシールドなのでピンが扱いにくい。そこで思いついて、まずはArduino-Megaに挿してみました。内蔵のマイクロSDを使うには少し問題があり、該当ピンがUNOのSPI接続にとられるため、その4本のピンをカットしてMEGAのSPI各ピンに配線でつなぐのがよいかと思われます。とりあえずはこれを無視して放置し、表示だけをテストすることにしました。このシールドをMEGAに次のように取り付けます。

f:id:a-tomi:20210425154633j:plain

そしてセンサーとRTC(リアルタイムクロック)と配線した部分は次です。

f:id:a-tomi:20210425162235j:plain

 

このようにするとArduinoからの3.3V出力もふさがってしまうので、裏面から配線で取り出しています。 

f:id:a-tomi:20210425162321j:plain

そしてプログラムを作りますが、まずはArduinoの普通のTFT-LCDライブラリーではうんともすんとも^^; いったいどんな接続だったっけな(・・? 

この製品に書いてあるmcufriendをググってみるとMCUFRIEND_kbv libraryライブラリーにたどり着きます。Arduinoのモデルを選ばないオールマイティとか。それを使ったらなんと即、問題なく動きました。

f:id:a-tomi:20210425163311j:plain

1日放置したのが上の写真ですが、大きさも鮮明さも良い感じです。測ると電流を200mAほど消耗しているのにはびっくりですが。プログラムは色使いや位置等を少し調整して冒頭に掲げた画面を出しているのが次のスケッチです。作るのと改訂に半日以上使ってしまいましたが。

/****************************************************************
  Weather station with TFT-LCD 320x240 & BME280 -  V.02
    Initial version V.01 on Apr.18,2021   (c) Akira Tominaga
  Function:
    Display temperature-C, relative humidity-% & pressure-hPa.
    24-hour-plot wraps round every 24 hours.
  Pin connections:
    TFTLCD shield(MCUFRIEND_kbv) attached to Arduino-MEGA.
    Arduino-Mega 3.3V,G,SDA&SCL to sensor&RTC's VCC,G,SDA&SCL.
  Revisions:
    V01: Changed design of grid and position-marker.  4/19/2021
    V02: Minor changes to characters and some colors. 4/23/2021
  Remarks:
   Librarie's licensing rules & (c)s are attached in the bottom.
  **************************************************************/
#include "UTFTGLUE.h"     // MCUFRIEND_kbv library
#include "Wire.h"         // I2C for RTC-DS3231 and BME280
#include "Adafruit_Sensor.h"
#include "Adafruit_BME280.h"
// *** for LCD ***
UTFTGLUE lcd(0, A2, A1, A3, A4, A0);
#define wW 320            // TFT width 
#define hH 240            // TFT height
#define bX 43             // x @ begin-graph
#define eX wW-36          // x @ end-graph(284 in this case)
int lX = (eX - 1) - (bX + 1) + 1; // x-axis len (240 this case)
#define bY 18             // y @ begin graph
#define eY hH-15          // y @ end graph
// *** for colors ***
#define White (192,192,192)
#define Black (0,0,0)
#define Red (255,48,48)
#define Green (0,255,0)
#define Blue (0,100,255)
#define Yellow (255,255,0)
#define Purple (255,0,255)
#define rGray (127,127,127)
#define dGray (32,32,64)
// *** for application use ***
char Temp[] = "xx.xC";    // 5+1 bytes for temperature
char rH[] = "xx.x%rH";    // 7+1 bytes for relative humidity
char hPa[] = "xxxx.xhPa"; // 9+1 bytes for pressure
#define iniX bX+1         // graph initial x-position
#define lastX eX-1        // graph last x-position
uint16_t curX = iniX;     // set curX at initial position
uint16_t posY;            // position Y
// *** for measurement ***
Adafruit_BME280 bme;      // name as bme
// to keep measurement for 24hours in lX positions;
uint16_t intervalM = round(24 * 60.0 / lX); // interval minutes
uint8_t moV;              // current month
uint8_t daV;              // current day
uint8_t hrV;              // current hour
uint8_t mnV;              // current minute
uint8_t mnVsv = 99;       // former minute save area
char MDhm[] = "MM/DD hh:mm"; // editing area for clock

void setup() { // ***** Arduino setup() *****
  Serial.begin(9600);
  Wire.begin();
  lcd.InitLCD();
  lcd.clrScr();
  //////////////////////////////////////
  // *** Draw frame ***               //
  //////////////////////////////////////
  lcd.setColor Black;     // make a blackboard
  lcd.fillRect(0, 0, wW - 1, hH - 1);
  lcd.setColor Yellow;
  lcd.drawRect(0, 0, wW - 1, bY - 1); // top bar
  // *** fill texts in top bar ***
  lcd.setFont(SmallFont);
  lcd.setBackColor Black; // text's back-color
  dspVal();               // initial text in top bar
  //////////////////////////////////////
  //*** draw axes, grid and scales ***//
  //////////////////////////////////////
  // *** vertical lines and scales***
  lcd.setColor rGray;
  lcd.drawLine(eX, bY, eX, eY); // right edge (graph limit+1)
  for (uint8_t tU = 0; tU < 25; tU += 2) {
    uint16_t xL = bX + 1 + round(lX * (tU / 24.0));
    lcd.setColor rGray;
    lcd.drawLine(xL, bY, xL, eY);
    lcd.setColor White;    //
    char hhC[3];
    sprintf(hhC, "%2d", tU);
    lcd.print(hhC, xL - 8, hH - 12);
  }
  lcd.print("Hour", 8, hH - 12);
  // *** vertical scales and horizontal lines ***
#define tsMax 50          // temperature max scale
#define tsMin 0           // temperature minimum scale
#define tsStep (tsMax-tsMin)/10
#define hsMax 100         // rel. humidity max scale
#define hsMin 0           // relative hum. min scale
#define hsStep (hsMax-hsMin)/10
#define psMax 1040        // pressure max scale 1040
#define psMin 940         // pressure min scae 940
#define psStep (psMax-psMin)/10
  char tsC[4];            // chars for temperature
  char hsC[4];            // chars for rel.humidity
  char psC[5];            // chars for pressure
  int tS = tsMin;         // init. temperature scale
  int hS = hsMin;         // init. humidity scale
  int pS = psMin;         // init. pressure scale
  // draw horizontal lines and scales in a loop
  for (int g = 0; g < 10; g++) {
    int iy = round(eY - (eY - bY) * g / 10.0);
    lcd.setColor rGray;
    lcd.drawLine(bX + 1, iy, eX, iy); // horizontal line
    sprintf(tsC, "%2d", tS); // temp val. text
    sprintf(hsC, "%2d", hS); // rHum val. text
    sprintf(psC, "%4d", pS); // pres val. text
    lcd.setColor Green;   // for temperature
    lcd.print(tsC, 5, iy - 7);
    lcd.setColor Blue;    //  for humidity
    lcd.print(hsC, 26, iy - 7);
    lcd.setColor Red;     // for pressure
    lcd.print(psC, eX + 5, iy - 7);
    tS = tS + tsStep;     // increase pos. for temp.
    hS = hS + hsStep;     // increase pos. for humi.
    pS = pS + psStep;     // increase pos. for pres.
  }
  //////////////////////////////////////
  //*** prepare BME280 sensor      ***//
  //////////////////////////////////////  
  if (!bme.begin()) {     // check validity of BME280
    Serial.println(F("Invalid BME280."));
    Serial.print("SensorID=0x");
    Serial.println(bme.sensorID(), HEX);
    Serial.println(F(" ID 0x56-0x58 for BMP280."));
    Serial.println(F(" ID 0x61 for BME680."));
    while (true) {}       // if error, stop here
  }
  delay(1000);            // wait sensor stabilized
}
void loop() { // ***** Arduino loop() *****
  // *** display clock and check timing to measure   
#define aMomnt 1000       // mS to wait for a moment
waitTiming:
  geTime();               // get time and calendar
  if (mnV == mnVsv) {     // if no change in minute
    delay(aMomnt);        // then wait for a moment
    goto  waitTiming;     // repeat checking
  }
  // *** display clock in top bar each second
  sprintf(MDhm, "%02d/%02d %02d:%02d", moV, daV, hrV, mnV);
  lcd.setColor White;    // for Clock
  lcd.print(MDhm, 15, 4); // display date and clock
  // *** check graph-drawing timing or not
  if ((mnV % 6) > 0) {    // if not multiple of 6
    delay(aMomnt);        // then wait for a momentr
    goto  waitTiming;     // repeat checking
  }
  // *** now measurement start
  mnVsv = mnV;            //save time (min) processed this time
  uint16_t vMin = hrV * 60 + mnV; // cur. time in minute
  uint16_t xPos = round(lX * (float)(vMin / (60.0 * 24))); 
  curX = bX + 1 + xPos;   // get current hilizontal position
  // *** get measured data and edit them into top bar
  uint16_t Tempx10 = round(10.0 * bme.readTemperature());
  uint8_t temp = Tempx10 / 10;  // get integer part
  uint8_t tempD = Tempx10 % 10; // remainder after dot
  sprintf(Temp, "%2d", temp);   // edit integer 2chars
  Temp[2] = '.';
  sprintf(Temp + 3, "%01d", tempD); // edit after dot
  uint16_t Humix10 = round(10.0 * bme.readHumidity());
  uint8_t humi = Humix10 / 10;  // get integer
  uint8_t humiD = Humix10 % 10; // remainder after dot
  sprintf(rH, "%2d", humi);     // edit integer 2chars
  rH[2] = '.';
  sprintf(rH + 3, "%01d", humiD); // edit after dot
  uint16_t Presx10 = round(bme.readPressure() / 10.0);
  uint16_t pres = Presx10 / 10; // get integer part
  uint16_t presD = Presx10 % 10; // remainder after dot
  sprintf(hPa, "%4d", pres);    // edit integer 4chars
  hPa[4] = '.';
  sprintf(hPa + 5, "%01d", presD); // edit after dot
  dspVal();               // show the values in top bar
  // *** draw position marker line ***
  // erase the past marked line
  lcd.setColor Black;     // with blackboard color
  lcd.drawLine(curX, bY, curX, eY - 1); // erase
  // re-mark grid positions
  lcd.setColor rGray;     // with grid color
  for (int g = 0; g < 10; g++) {
    int iy = round(eY - (eY - bY) * g / 10.0);
    lcd.drawPixel(curX, iy); // re-draw grids
  }
  // ** if on the hour line, then recover it
  if ((curX - (bX + 1)) % (round(lX / 12.0)) == 0) { // if 2hours x N
    lcd.drawLine(curX, bY, curX, eY); // recover the hour line
  }
  // *** draw measurement graphs ***
  lcd.setColor Green;     // temperature
  posY = (eY - 1) - round((Tempx10 / 10.0) * (eY - bY) / 50.0);
  lcd.drawPixel(curX, posY);
  lcd.setColor Blue;      // relative humidity
  posY = (eY - 1) - round((Humix10 / 10.0) * (eY - bY) / 100.0);
  lcd.drawPixel(curX, posY);
  lcd.setColor Red;       // pressure
  posY = (eY - 1) - round(((Presx10 / 10.0) - 940) * (eY - bY) / 100.0);
  lcd.drawPixel(curX, posY);
  // increase x-position for next measurement
  curX++;                 // get next position
  if (curX > lastX) {
    curX = iniX;
  }
  /// *** draw vertical marker line ***
  lcd.setColor Yellow;    // with Yellow color
  lcd.drawLine(curX, bY, curX, eY - 1);
} // *** end of Arduino loop() ***
/**************************************************
    User defined functions
 **************************************************/
// ***** dspVal() *** display values measured *****
void dspVal(void) {
  lcd.setColor Green;     // for temperature C
  lcd.print(Temp, 120, 4);
  lcd.setColor Blue;      // for relative humidity %
  lcd.print(rH, 171, 4);
  lcd.setColor Red;       // for pressure hPa
  lcd.print(hPa, 238, 4);
}
// *** geTime() *** get month,day,hr & min from DS3231
void geTime(void) {
#define aRTC 0x68       // RTC I2C addr
#define mdm 2           // minute position in RTC data
#define mdh 3           // hour position in RTC data
#define mdD 5           // day position in RTC data
#define mdM 6           // month position in RTC data
  byte  vR[8];          // values in RTC format
  Wire.beginTransmission(aRTC);
  Wire.write(0x00);
  Wire.endTransmission();
  Wire.requestFrom(aRTC, 7);
  while (Wire.available() < 7) {}  // wait for data
  for (int i = 1; i < 8; i++) {
    vR[i] = Wire.read();
  }
  Wire.endTransmission();
  moV = ((vR[mdM] & B00010000) / 16) * 10 + (vR[mdM] & B00001111);
  daV = ((vR[mdD] & B01110000) / 16) * 10 + (vR[mdD] & B00001111);
  hrV = ((vR[mdh] & B00100000) / 32) * 20 + ((vR[mdh] & B00010000) / 16) * 10 + (vR[mdh] & B00001111);
  mnV = ((vR[mdm] & B01110000) / 16) * 10 + (vR[mdm] & B00001111);
}
// Library providers' copyrights and lisences are as follows:
/************* UTFTGLUE library *********************************
   TFT-LCD 320x240 pixels.
    MCUFRIEND_kbv (C)2015 Rinky-Dink Electronics, All rights
    reserved.   web: http://www.RinkyDinkElectronics.com/
****************************************************************/
/**** for Adafruit-GFX-Library used by UTFTGLUE *****************
  Software License Agreement (BSD License)  (c) 2012 Adafruit
  Industries, All rights reserved. Redistributions in binary form
  must reproduce copyright notice. See BSD lisence conditions.
***************************************************************/
/***********ふFor Adafruit_BME280 ******************************
  Designed specifically to work with the Adafruit BME280
   http://www.adafruit.com/products/2650
  Use I2C or SPI interface. I2C adrs is 0x76 or 0x77. Adafruit
  invests time and resources providing this open source code,
  please support Adafruit and open hardware by purchasing
  products from Adafruit!
  By Limor Fried & Kevin Townsend for Adafruit Industries.
  BSD license: all above must be included in any redistribution.
  See LICENSE file for details in the library.
*****************************************************************/
// end of program

ピンをよく確認して、これを他のマイコンで動かすのにチャレンジしてみたいと思います。電流が多いので電源の与え方に注意しないといけないかも。。。

と考えたのですが、現在は似た製品が海外ネットで色々出回っていますね。タッチパネルでなければ価格も1000円未満で手ごろそうです。省エネなども少しは改善されたことでしょう。いっそ、そちらを入手して試してみる方がいいかな・・早速今注文してしまいました^^

 

2021.4.26追記 ================================

今回使用したTFT-LCDのピンの記述を調べた結果、次のものと完全に同じです。

f:id:a-tomi:20210426112141j:plain

同型の一般的な製品ではMCUにILI9341が使われているようですが、MCUFRIENDのこのTFT-LCDシールドではILI9320が使われています。それがMCUFRIENDのライブラリーを必要としている理由かなと推察。

ついでに、動作中の接続状態を調べたらSD用の4本(上の表で下4つ)以外はすべて使用しており、Arduinoとのデータのやりとりは8ビットパラレルです。表示速度は速いですが、マイコンの限られたピンを15本消耗するのは厳しい^^;。節約してもせいぜい-4本(電源を外部供給、RSTをHIGHにしCSをLOWにして固定)、それでも11本は表示用にとられます。そのうちに、目的であるTeensyで試そうと思ってはいます。。。

そういうわけで、今回注文したものはSPI接続だけででき、画面サイズがもう少し大きな3.5インチにしました。例によって到着までに数週はかかるでしょうが、ボチボチやるのでちょうどいいかもしれません。

================================== 追記end

 

以上、簡単に書かせていただきましたが、何らかのお役に立てば幸いです。

 

©2021 Akira Tominaga, All rights reserved.

楽々フーリエ変換!(快速マイコンをArduino-IDEで使う)

f:id:a-tomi:20210405120325j:plain

トライアングルの音色は澄んでいるようで意外に複雑です。今回はマイコンでできるフーリエ変換をご紹介したいと思います。

上のグラフはトライアングルの音をマイクで採りつつフーリエ変換した結果で、周波数の成分を表しています。なんどやっても同じグラフになります。たぶんトライアングルには振動の腹が主に3つあるからだと思われます。とはいえ百均で入手したおもちゃなので、プロが使う本物とはそれなりに違うかもしれませんが^^;

f:id:a-tomi:20210405121057j:plain

このトライアングルの音の波形は次です。

f:id:a-tomi:20210405121555j:plain

よい音が長く続きますが、意外に複雑な波形です。

これをマイコンでどうやってフーリエ解析して、周波数成分の分布を知るかを説明します。また、これでいかに正確な分析ができるかについても、ついでにテストしてみたのでご紹介します。

今回の回路は次です。

f:id:a-tomi:20210405171049j:plain



快速マイコンTeensy4.0をArduinoIDEでプログラミングします。ここでの勝手なスケッチは次です。

/*****************************************************************
 *  Fast Foourier Transform & Data Recording, using Teensy 4.0
 *    Initial version V00:  April 3, 2021 (c) Akira Tominaga
 *    Function:
 *      1) Quick sampling and FFT, applying Hamming-window
 *      2) Show FFT graph, with Serial Plotter
 *      3) Record measured data to micro SD with name "FFTnnn.txt"
 *      4)Perform next measurement if tact-switch pushed
 *    Input and output: 
 *      1) Pin A2(16) for signal input (keep within 0 to 3.3V)
 *      2) Pin 10,11,12 and 13 for SPI interface to micro SD
 *      3) Pin 2 for tact-switch to continue to next FFT
 *    Remarks: Arduino-FFT library is used under GNU-GPLicense.
 *    Revisions: (not yet)
 *****************************************************************/
#include "SD.h"       // use SPI micro-SD-card-reader/writer 
#include "SPI.h"      // SPI MOSI=11, MISO=12,CLK=13
const int cS = 10;    // SPI Chip-Select = 10
File sdF;
#include "arduinoFFT.h" // use Fast Fourier Transform
// for 0~20kHz FFT, 40kHz sampling & 1024 samples=512 bins
#define sFreq 40000   // sampling Hz <55000 (ADC 17.5 micro sec)
#define nSamp 1024    // number of samplings 
#define serB 115200   // serial Baud 115.2,57.6,38.4,19.2,14.4,9.6 k
#define aPin  A2      // signal input pin
#define sW 2          // tact switch at D2
arduinoFFT FFT = arduinoFFT();
unsigned int sDur;    // duration micro sec. between samplings
unsigned long micS;   // elapsed time in microsec (max about 70min)
double vR[nSamp];     // Real part of Vector
double vI[nSamp];     // Imaginal part of Vector

void setup() { // ***** Teensy setup() *****
  Serial.begin(serB);
  while (!Serial) {}  // wait for Serial-ready
  pinMode(sW, INPUT_PULLUP); // pull-up tact switch
  sDur = round(1000000 * (1.0 / sFreq)); // calc. sampling dur.μS
  if (!SD.begin(cS)) { // start SD
    Serial.println(F("Init-SD err."));  return;
  }
}

void loop() { // ***** Teensy loop() *****
  // *** perform Fast Fourier Transform
  for (int i = 0; i < nSamp; i++) { // do a cycle of samplings
    micS = micros();  // save sampling-start-time
    vR[i] = analogRead(aPin);
    vI[i] = 0;        // set zero to imaginal part
    while (micros() < (micS + sDur)) {} // wait until cycle-end
  }
  // apply Hamming windowing
  FFT.Windowing(vR, nSamp, FFT_WIN_TYP_HAMMING, FFT_FORWARD);
  FFT.Compute(vR, vI, nSamp, FFT_FORWARD); // calculate FT
  FFT.ComplexToMagnitude(vR, vI, nSamp); // mag. to vR 1st half
  double peak = FFT.MajorPeak(vR, nSamp, sFreq); // get peak freq
  // get peak element number
  int ip = round((nSamp / 2) * peak * 1.0 / (sFreq / 2));
  
  // *** prepare SD to create file=FFTnnn.txt
  char fN[11] =  "FFTnnn.txt"; // file name
  uint16_t fnum = 0;  // file number for character nnn
  char fnumc[4];      // file number in character
  while (true) { // if the name existsts, then set nnn+1
    sprintf(fnumc, "%03d", fnum);
    for (int i = 0; i < 3; i++) {
      fN[i + 3] = fnumc[i];
    }
    if (!SD.exists(fN)) break; // if not exists, go out
    fnum ++; // if it exists, increase nnn
  }
  sdF = SD.open(fN, FILE_WRITE); // create it as output file
  if (!sdF) {		// if error, then
    Serial.println(F("Open-SD err."));
    while (true) {} // if open-err, stop here
  }
  // convert results in vR[] into simple integer hh to record to SD
  for (int i = 0; i < (nSamp / 2); i++) {
    uint16_t ff = round(i * 20000.0 / (nSamp / 2.0)); // get Hz value
    uint16_t hh = round(10000.0 * (vR[i] * 1.0 / vR[ip])); // relative Mag.
    Serial.println(hh); // plot to Serial-plotter
    // *** record( Hz, Mag.) to SD in CSV format
    sdF.print(ff);
    sdF.print(",");
    sdF.println(hh);
  }
  sdF.close();		// close the file when all data recorded
  while (digitalRead(sW) == HIGH) {} // wait for sW pushed
  // *** Peform next measurement and recording when sW pushed
}
// end of program

 

Arduino-FFT高速フーリエ変換)ライブラリーは、もともとGoogle社が開発し、今はArduinoIDEのほかにGCCなどでも使える素晴らしいライブラリーです。ArduinoIDEにこれを追加する方法は普通の入れ方と同じで、「ツール」→「ライブラリーを管理」→「ライブラリーマネジャー画面」で検索バーを使ってFFTを検索し「ArduinoFFT」を選び→該当の「インストール」をクリックします。

今回使っているマイコンTeensy4.xは、フローティング処理も含めて非常に速いので、こういう用途にはぴったりです。もちろん、このFFTはUNOなどでも使えるには使えるわけですが、非常に低い周波数領域しかカバーできません。

サンプリング周波数を40kHzにしている(44kHzもよいかな)理由は、可聴音域の20kHzまでを分析したいからです(FFTではサンプリング周波数の半分までしか分析できません)。

また、Arduino-IDEのシリアルプロッターを使う場合は、横軸を512点しか表示しませんので横軸をそれに合わせるために、サンプリング個数を1024にしています。FFT関数は周波数分解能であるその半分の512個(単位はビン)にわけて答を返してきます。上のプログラムでおわかりかもしれませんが、関数に引き渡すパラメータベクトルのリアル部分用であるvR[ ]の前半分に収めて返してくれます。

 信号測定にはADCを使います。Teensyの迅速AD変換方法はあるようですが、普通のAD変換で実測した結果17.5μS以下でできるので、この用途(40kHzでのサンプリングと処理)用には十分に速いと分かります。

最初に示したトライアングルの測定等では、コンデンサーマイクを用いています。ここでは次のものを使っています。

f:id:a-tomi:20210405135000j:plain

これを信号入力部分につぎのように接続します。

f:id:a-tomi:20210405135214j:plain

マイクは適当な保持具にもたせます。

f:id:a-tomi:20210405135301j:plain

そして動かしシリアルプロッターに表示すると、トライアングルの音の周波数は次のように表示されます。

f:id:a-tomi:20210405135444j:plain

 X軸は20kHzまでの周波数として、等間隔で512点をプロットしますが、横軸の目盛り表示はおしきせで思うに任せません。

また、左端の12点(つまり0~469Hz)は縦軸がかぶさり表示されません(ただし、左端には商用電源のノイズなどが載りやすいため、表示が要らないといえば要らないわけなのですが^^;)余談ですが、上のプログラムでは、peak = FFT.MajorPeak(vR, nSamp, sFreq);により支配的な周波数を取り出しています。試しに50Hzをまぜてみた限り、この(左端に近い)あたりのBinの値がたとえ一番大きくなっても、FFT.MajorPeak関数ではそこはピークとみなされない仕様になっている感じです。

じっくり分析するには、シリアルプロッターではなくシリアルから信号を表示するプログラムを作るか、測定結果をSDにでも記録してExcelで分析するのがよいかということで、とりあえずは各回の測定記録をマイクロSDに書くようにしたわけです。

さて、このフーリエ変換の精度はどうかを調べたくなりますね。

そこで、ファンクションジェネレータの正確な波形を信号として入れてみることにします。まずは2.000kHzの正確な正弦波(sin波)です。

f:id:a-tomi:20210405142152j:plain

オシロで表示すると次。

f:id:a-tomi:20210405142231j:plain

上の写真の右下をごらんになると、正確に2.0000kHzであるのがわかりますね。最後の0.02Hzの半端はおそらくこのオシロ側での有効数字6桁末尾の許容誤差だと思います。

これを測定した結果は次です。

f:id:a-tomi:20210405142604j:plain

高調波の全くない、実に正確なサイン波であることが納得できますし、逆に今回作ってみたFFT装置の精度も高いものであることが理解できます。 

では、同じ周波数のブロック波(square波)をいれてみます。

f:id:a-tomi:20210405142807j:plain

オシロの波形は次です。

f:id:a-tomi:20210405142842j:plain

これを測定すると次の結果になりました。

f:id:a-tomi:20210405142928j:plain

理屈通りの3倍、5倍、7倍、9倍の周波数成分が正確に出ていることがわかります!

(2021年4月8日追記)上のグラフの横軸の分解能はサンプリング数÷2の512ビンで、この場合はビン幅約39Hzへの確率分布で、いわば少しならされた値になります。論理的には幅のない成分の高さは、そのため若干下に表示されるわけです。

例えば、3倍の6kHzなら論理的には3.33、5倍の10kHzなら2.00ですが、そういうわけで表示は少しだけ低くなります。ビン幅を狭めるために、ためしにサンプリング数を16倍の16384回にしてみます。上のプログラムなら#define中の1行を、#define nSamp 16384 に変えるだけのことですのでやってみましたら次のグラフになりました。

f:id:a-tomi:20210408151501j:plain

横軸の分解能は2.44Hzになり波形は前より尖り、高さも理論値に少し近づくのがわかりますね。(以上2021.4.8追記)

 どっちにしてもファンクションジェネレータが正確な事と、今回のFFTの精度が高い点の両方がよくわかりました^^ 

それでは・・・、ということで気をよくして、今度はトイピアノを持ち出してきて測ってみます。

f:id:a-tomi:20210405143342j:plain

一番上の「ド」を鳴らします。

オシロでみると次です。

f:id:a-tomi:20210405143500j:plain

なんだか頼りない音ですね。でも測定結果は次です。

f:id:a-tomi:20210405143544j:plain

おお、あるべき基本周波数1046.5Hzを実に正確に出しています!驚き。高調波も1オクターブ上の2093Hz、そして2オクターブ上の4186Hzに!おもちゃピアノといっても馬鹿にできませんね。

今や玩具でもクリスタル付きマイコンで電子音を出してるわけですから、むしろあたりまえですかね^^

 

これに気をよくして、次はファンクションジェネレータからこれぞホワイトノイズのAWGNを出してこれにいれてみます。

f:id:a-tomi:20210405144507j:plain

f:id:a-tomi:20210405144524j:plain

そして測ってみると;

f:id:a-tomi:20210405144553j:plain

ありゃ、平たんに近いのではなくおおいに癖があるのですね。まあ、人間が数式でこしらえている波である限りは、一部を取り出せばこうなるのがむしろ当たり前かもしれませんね。しかしこれはちょっと拍子抜け・・^^;

 

では今回の記事はこのへんにしたいと思います。皆様のお役に少しでも立てば幸いです。

 

©2021 Akira Tominaga, All rights reserved.

 

快速マイコンの Teensy4.xを使ってみる

f:id:a-tomi:20210319203954j:plain

米国のPJRC最新マイコン開発ボードである、Teensy4.0と4.1を試しています。高速・大容量でとてもいいと思います。

パフォーマンスはCoreMarkのベンチマークで圧倒的に高速。

f:id:a-tomi:20210319204558j:plain

中国生まれのESPを圧倒ですね。折柄、米中対立で?・・^^;

この小さなPCBによくもCortex-M7を搭載したものだと思います。しかも安いし。ただし、PJRCで直接買えば正価で安いのですが、EU向け以外は送料がかなりかさみます。日本ではRobotShop.comで買えば、日本法人があるのでそこそこの価格で入手できます(その他ではとんでもなく高価なのにびっくり)。とはいえ、いまだに品切れになりやすい状態。主な仕様は次のとおりですが、とにかく凄い。

f:id:a-tomi:20210319204908j:plain

通常は600MHzで動かしますが、オーバークロックで動かせばなんと1GHzを超える速度が可能です(その場合は冷却が必須ですが)。

使っていたTeensy3.5と比べても長足の進歩。ここ数年間はこの世界はESPに席巻されていたようでもあるし、これは大歓迎ですね。高速信号をとらえたりする用途もこれのみで楽にできそうです。

f:id:a-tomi:20210319205341j:plain

ハードウェアの作りの品質の良さは袋にまで及び "Assembled in USA"と誇らしげ(?)に書かれているのもいい感じですね。

次のPJRCのHP

https://www.pjrc.com/

から製品の情報をたどれば、使い方のチュートリアル等もしっかりあり、英語ではありますがとてもわかりやすく書かれています。

そして、ある時期のモデルからはArduino-IDEでの開発となりましたので楽ちんです。対応ライブラリーがまだ少ないのが少し残念ではありますが、勝手な電子工作としてはひるまずに使いたいマイコンです。

この記事ではTeensy4.0について簡単に紹介したいと思います。

f:id:a-tomi:20210319210820j:plain

これにIDEでプログラミングする際に、実行速度が指定できます。

f:id:a-tomi:20210319210913j:plain

0.9GHzからはクーリングが必須と書いてありますね。

600MHzだってハンパない速さです。これでどういう発熱をするのかを測ってみました。実行開始から10分後の発熱状況は次です。

f:id:a-tomi:20210319211146j:plain

室温25℃の机上で紙の上に置いて測りましたが、最高温度は46.7℃にちゃんと収まっています。ヘダーピンをつけてない状態ですが、つけてそれなりのソケットに刺せばさらに下がると思われ、安心しました。

このPCBは裏にもオプションがつけられるようになっています。

f:id:a-tomi:20210319211828j:plain

そのためヘダーピンをさっさととり付けるのがはばかられ、最初のうちはヘダーピンをつけずに接続してテストしていました(簡単・確実にブレッドボードにつける方法を考えついて^^。そのうちに別記事でご紹介しようと思います)。

まずは20x4のキャラクターLCD表示をしようとしましたが、Liquide Crystal_I2Cでコンパイルエラーがでます。その解決に回り道するのは時間が勿体ない^^;

I2Cデバイスの多くは、データシートをみながらライブラリーなしでコーディングしても簡単です。この場合は、前に次の記事に書いた通りで、確実に動くプロトコルがわかっています。

https://a-tomi.hatenablog.com/entry/2020/12/21/211852

そこに書いたとおり、20x4のI2C-4bit-LCDは、最小限で次のプロトコルを使えば簡単に動きます。

f:id:a-tomi:20210319213117j:plain

ついでにRTC(リアルタイムクロック)もつけますが、そもそもTeensyのこのモデルにはRTCが最初からついています。しかしそのクロックの維持のためにPCB自体にバッテリーをつけておく必要があるので、当面はそれを使わず単体のRTCをつなぎます。今回はRTCを読み込むだけですが、時間を更新する際の簡単な方法は、前に次の記事に書きました。

回路不要のかんたんなRTCライター:Arduinoで - 勝手な電子工作・・

今回のスケッチは次です(2021.3.20追記:LCDへのEdit方法を少し改善したV02に差し替えておきます)。

/************************************************************
       Teensy 4.0 RTC and LCD Test via I2C - V02
         using Wire library (i2c_t3 is no more compatible)
         (c) 2021 Akira Tominaga, All rights reserved.
        Revisions:
          Initial version V00; 3/6, 2021
          V01; LCD interfaces to Roll-your-own      3/7,2021
          V02: writeString instead of writeLine    3/20,2021
 ************************************************************/
#include "Wire.h"       // Teensy Wire library
#define sclP 19         // SCL pin 19
#define sdaP 18         // SDA pin 18
#define aLCD 0x27       // LCD I2C addr
#define aRTC 0x68       // RTC I2C addr
// for LCD I2C 20x4
#define wT 12           // microsec between bytes to LCD
#define fL 20           // full-length for one line
// for Real Time Clock DS3231
byte  vR[8];            // values in RTC format
int vI[8] = { 0, 0, 0, 0, 0, 0, 0, 0};  // RTC data area
#define mdI 0           // Initial mode
#define mds 1           // ss mode
#define mdm 2           // mm mode
#define mdh 3           // hh mode
#define mdW 4           // WW=Day-of-Week mode
#define mdD 5           // DD mode
#define mdM 6           // MM mode
#define mdY 7           // YY mode
char  strYMDhms[20];    // editting area for RTC values

void setup() { // *** Teensy 4.x setup() ***
  Wire.setSCL(sclP);
  Wire.setSDA(sdaP);
  Wire.begin();
  Serial.begin(9600);
  while (!Serial) {}
  Serial.println(F("\nStarting"));
  iL();                 // initialize LCD
  Serial.println(F("LCD initialized"));
  // write strings to LCD               // *V02
  wS(0, 0, F("* Teensy 4.x Test *"));   // *V02
  wS(0, 1, F("with Real Time Clock"));  // *V02
  wS(3, 3, F("by Akira Tominaga"));     // *V02
}

void loop() { // *** Teensy 4.x loop() ***
  getTime();            // get time
  sC(1, 2);             // set cursor (1,2)
  for (uint8_t m = 0; m < 19; m++) {
    wC(strYMDhms[m]) ; // write characters
  }
  delay(100);
}

/********************************************************
     User defined fumctions
 ********************************************************/
// LCD - application program interfaces
// *** iL() *** initialize LCD ***
void iL(void) {
  wA(0x00);
  delay(100);
  wA(0x34);
  wA(0x30);
  delay(5);
  wA(0x34);
  wA(0x30);
  delayMicroseconds(200);
  wA(0x34);
  wA(0x30);
  delayMicroseconds(200);
  wA(0x24);
  wA(0x20);
  delayMicroseconds(200);
  wA(0x24);
  wA(0x20);
  wA(0x84);
  wA(0x80);
  delayMicroseconds(200);
  wA(0x04);
  wA(0x00);
  wA(0xC4);
  wA(0xC0);
  delayMicroseconds(150);
  wA(0x04);
  wA(0x00);
  wA(0x14);
  wA(0x10);
  delay(2);
  wA(0x04);
  wA(0x00);
  wA(0x64);
  wA(0x60);
  delayMicroseconds(150);
  wA(0x08);
  delay(1);
}
// *** wS(column,row,string) *** write String (x,y,string) ***
void wS(uint8_t col, uint8_t row, String strC) { // *V02
  sC(col, row);         // set cursor to (x,y)
  uint8_t sLen = strC.length();
  Serial.print("("); Serial.print(col); Serial.print(",");
  Serial.print(row); Serial.print(") "); Serial.println(strC);
  if (col + sLen > fL) {
    Serial.println(F("*** Too long & truncated."));
    sLen = fL - col;    // shorten sLen to avoid line-over 
  }
  for  (uint8_t nChr = 0; nChr < sLen; nChr++) {
    wC(strC.charAt(nChr));
  }
}
// *** sC(column,line) *** set cursor to (x,y) ***
void sC(uint8_t Col, uint8_t Line) {
  uint8_t Pos;
  switch (Line) {
    case 0: Pos = 128 + Col; break;
    case 1: Pos = 192 + Col; break;
    case 2: Pos = 148 + Col; break;
    case 3: Pos = 212 + Col; break;
    default: Serial.println("Err for LCD line");
      while (true) {}
  }
  wA((Pos & 0xF0) | 0x0C);
  wA((Pos & 0xF0) | 0x08);
  wA(((Pos << 4) & 0xF0) | 0x0C);
  wA(((Pos << 4) & 0xF0) | 0x08);
  delayMicroseconds(wT);
}
// *** wC(char) *** write a character to LCD ***
void wC(byte Chr) {
  wA((Chr & 0xF0) | 0x0D);
  wA((Chr & 0xF0) | 0x09);
  wA(((Chr << 4) & 0xF0) | 0x0D);
  wA(((Chr << 4) & 0xF0) | 0x09);
  delayMicroseconds(wT);
}
// *** cS() *** clear LCD screen ***
void cS() {
  wA(0x0C);
  wA(0x08);
  wA(0x1C);
  wA(0x18);
  delay(1);
}
// for LCD control
// *** wA(byte) *** write an absolute byte to LCD ***
void wA(byte Abs) {
  Wire.beginTransmission(aLCD);
  Wire.write(Abs);
  Wire.endTransmission();
}

// Real Time Clock -  application program interfaces
// *** getTime() *** get RTC MMDD & hhmm into strMDhm
void getTime(void) {
  rRTC();             // read RTC device
  cR2I();             // convert RTC-format to Integers
  sprintf(strYMDhms, "20%02d-%02d-%02d %02d:%02d:%02d", vI[mdY], vI[mdM], vI[mdD], vI[mdh], vI[mdm], vI[mds]);
}
// for RTC control
// *** rRTC() *** read Real-Time-Clock
void rRTC(void) {
  Wire.beginTransmission(aRTC);
  Wire.write(0x00);
  Wire.endTransmission();
  Wire.requestFrom(aRTC, 7);
  while (Wire.available() < 7) {}  // wait for data
  for (int i = 1; i < 8; i++) {
    vR[i] = Wire.read();
  }
  Wire.endTransmission();
}
// *** cR2I() *** convert RTC-format to Integers
void cR2I(void) {
  vI[mds] = ((vR[mds] & B01110000) / 16) * 10 + (vR[mds] & B00001111);
  vI[mdm] = ((vR[mdm] & B01110000) / 16) * 10 + (vR[mdm] & B00001111);
  vI[mdh] = ((vR[mdh] & B00100000) / 32) * 20 + ((vR[mdh] & B00010000) / 16) * 10 + (vR[mdh] & B00001111);
  vI[mdW] = vR[mdW];
  vI[mdD] = ((vR[mdD] & B01110000) / 16) * 10 + (vR[mdD] & B00001111);
  vI[mdM] = ((vR[mdM] & B00010000) / 16) * 10 + (vR[mdM] & B00001111);
  vI[mdY] = ((vR[mdY] & B11110000) / 16) * 10 + (vR[mdY] & B00001111);
}
// *** End of program ***

 ブレッドボードの配線は主にI2Cだけですが、3.3V専用のTeensy4.0は5Vトレランシーがありません。よってI2C 信号レベルコンバーターをつけます。また、SCL, SDAを外部の4.7kオームでそれぞれプルアップします。次の写真のとおりです。

f:id:a-tomi:20210319222336j:plain

 I2Cプロトコルは相手デバイス(I2Cスレーブ)の都合上、Defaultのボーレート100kHzで動かしていますが、その他の実行部分の速さ(Loop内実行所用時間の短さ)は、当然とはいえびっくりするほど速いです。念のためにI2Cの様子を覗くと、次のように時間的な乱れ全くなしでキッチリと動いています。

f:id:a-tomi:20210319223213j:plain

 

今後、速度や容量が要る用途には、これをどんどん使っていこうと思います。

以上、ごく簡単な紹介でしたが、なんらかのお役に立てば幸いです。

 

©2021 Akira Tominaga, All rights reserved.

続・入眠用MP3プレーヤーの製作(#4)

f:id:a-tomi:20210306154203j:plain

外出自粛でとかく運動不足。夜寝つきにくいことがあります。自作のスリープタイマー付きのMP3プレーヤーを使い始めてからは、30分のタイマーでつけておけば早く快適に眠れます。.夜中に目覚めても、また演奏を始めればOKです^^

つまり、思いのほか役にたっています!そして、家族のリクエストで追加を作りました。こちらは場所をとらないように、小サイズにカッコよく(?)まとめました(自画自賛)。自分用のは大きくて不格好のままでいいのですが^^;。3月はなにかと時間がとりにくい月ですが、週末が来たのでやっと記事に書きます。

「眠るための音楽」とか「快適に眠りにつく曲」とか「睡眠を誘う音楽」とか、適当な言葉でYouTubeを検索してみれば、そのための曲は今やてんこ盛りです^^; 今、世界中で需要が高いのでしょうかね。その中から自分に合いそうで、ダウンロード可能なものを選んでMP3にします。

ところで、市販のMP3プレーヤーにはスリープタイマー付きが未だに見当たらないよう(また、スピーカー付きのMP3プレーヤーはどれも充電タイプで、不精者には面倒)ですがどうなんでしょう?よって自作したわけで、これまでの記事に書いたDFPlayer miniという廉価なマイクロSDのMP3デコーダモジュールを、Arduino(ATmega328p)で動かしています。ところが、この小さなモジュールはシリアル通信でコントロールする場合ノイズが載りやすいため、それを避けて勝手な方法で動かしています。経緯は前の記事に書いたとおりです(右のカテゴリー欄のDFPlayer miniで前の3記事がご覧になれます)。

今回は最近の超小型スピーカーで、今年製の30㎜径3Wフルレンジを使いました。といってもAliExpressで2個で百円ほどでした^^; これに合わせてケースは次の小さくてカッコよい透明ケース。

f:id:a-tomi:20210306162604j:plain

f:id:a-tomi:20210306162705j:plain

かつて百均で入手したものですがなかなか丈夫で精密に、しっかりできています。さすがに本物の日本製。今でも同じものがあるかどうかわかりませんが(ネットで同等品を探したところ、形は少し違いますが、よさそうなのはちなみに1個200円ほどで見当たります)。

今回はどうせ作ならと、使っていなかったピンを曲のNext(Forward)とPrevious(Bakward)ボタンとして使える(使わなくてもよい)ようにしました。この記事の最後に掲げるプログラムもそのように改善しておきました。この製作ではそのプログラムを使っていますが、それらのボタンはつけていません。そのうちに別のに生かせるかと。そもそも長押し・短押しを使い分けるのは面倒ですから、必要とする場合は専用ボタンにするほうがいいですからね。

f:id:a-tomi:20210306163200j:plain

これに合わせて基板も、少しだけ模様替えしました。左下のところ。

f:id:a-tomi:20210306163645j:plain

今後の追加作成のために、まとめて3枚をCNCルーターで削りました。

f:id:a-tomi:20210306163819j:plain

30分ほどで3枚が削れました。銅箔面は次。

f:id:a-tomi:20210306163942j:plain

今回の生基板は、ベークライト(=紙フェノール樹脂)の材料がいかにも「私の原料は紙でした」という感じなのですが、こういう用途には特に問題ないですね^^;

f:id:a-tomi:20210306164119j:plain

ケースを次のように加工しました。小さなスピーカーを透明ケースに直接接着すると糊(ホットボンド)が見えて恰好わるそうですから、別に厚めの樹脂でバッフル板を作ってそれにとりつけることにしました。そうすると穴あけの手間はケースに加えてほぼ二重にかかりますがね^^;

f:id:a-tomi:20210306164431j:plain

そして組みあがりの基板をケースの底にとりつけたのが次。

f:id:a-tomi:20210306164623j:plain

そして組んでみたところが次です。まだゴミだらけですが。

f:id:a-tomi:20210306164714j:plain

ゴミやバリなどをとって下の写真が出来上がり。

f:id:a-tomi:20210306164914j:plain

今回も操作ボタンは電源と音声ボリュームの-と+だけで単純です。タイマーの設定やSDカードの交換は必要がごく少ないので蓋を開けて行いますが、それで十分ですね。我ながら小さく思い通りにできてよかったと思うわけです^^;

プログラムは次のように拡張と改善をしました。とはいってもライブラリーを特に使いませんので、ArduinoーUNOでの使用量はデータもプログラムも容量の1割以下です。

/********************************************************
   DFPlayer with Sleep-timer and Volume-control,
     without using any libraries for DFPlayer-mini.
       Initial version-01 (c)Jan.30,2021 Akira Tominaga
   How to operate:
     Insert micro SD with mp3 or wav files, in playing
     sequences. Maximum 255 files x 99 folders.
     Power off & on to re-play after the timer expired.
     15,30,45,60,75,90,& 105 minutes timer available.
     Buttons are volume-up, volume-down, and optional
     skip-forward & backward.
   Revisions:
     V02: Changed all pin assignments.     Feb.1,2021
     V02b: Added 15min timer switch        Feb.7,2021
     V03: Added skip-forward & backward    Mar.1,2021
 *****************************************************:**/
#define pBusy 15        // DFP BUSY pin, H when busy
#define Tmr15 12        // +15 min for sleep-timer (V02a)
#define Tmr30 11        // +30 min for sleep-timer 
#define Tmr60 10        // +60 min for sleep-timer
uint8_t vTsv = 0;       // save-a to detect timer changes
#define vol_D 5         // volume-down button
#define vol_U 8         // volume-up button
#define Fwd 3           // Forward button   *V03 added
#define Bkw 2           // Backward button  *V03 added
#define ledG  9         // led green for sleep-timer
#define IO1 17          // DFPIO1 short-on=<<,long-on=Vol-
#define IO2 16          // DFPIO2 short-on=>>,long-on=Vol+
#define sOn 100         // time (mS) for short-on
#define lOn 850         // time (mS) for long-on
#define chatrT 100      // chattering time to avoid
#define init_vol_D 15   // init. or ending # of vol.-downs 
unsigned long slpT = 0; // elapsed timer mS (large value)
#define stopOff 0       // stop signal is off
#define stopOn 1        // stop signal is on
uint8_t stopSig = stopOff; // initialize stop signal

void setup() {          // *** Arduino setup() ***
  pinMode (IO1, OUTPUT);
  digitalWrite(IO1, LOW);
  pinMode (IO2, OUTPUT);
  digitalWrite(IO2, LOW);
  pinMode (pBusy, INPUT);
  pinMode (Tmr15, INPUT_PULLUP);
  pinMode (Tmr30, INPUT_PULLUP);
  pinMode (Tmr60, INPUT_PULLUP);
  pinMode (vol_D, INPUT_PULLUP);
  pinMode (vol_U, INPUT_PULLUP);
  pinMode (Fwd, INPUT_PULLUP);    //  *V03 added
  pinMode (Bkw, INPUT_PULLUP);    //  *V03 added
  pinMode(ledG, OUTPUT);
  digitalWrite(ledG, HIGH); // show "I am awake"
  Serial.begin(9600);
  for (uint8_t i = 0; i < init_vol_D; i++) { // vol.down
    volDown();          // lower in the beginning
  }
  digitalWrite(ledG, LOW); // LED off
  Serial.println(F("DFP start"));
  playNext();           // start 1st music
  delay(300);           // delay before loop
}

void loop() {           // *** Arduino loop ***
  if (digitalRead(pBusy) == HIGH) { // if idle
    playNext();
  }
  if (digitalRead(vol_D) == LOW) { // vol.d button?
    while (digitalRead(vol_D) == LOW) {} // -released?
    volDown();          //  then decrease volume
  }
  if (digitalRead(vol_U) == LOW) { // vol.up button?
    while (digitalRead(vol_U) == LOW) {} // released?
    volUp();             // then increase volume
  }
  if (digitalRead(Fwd) == LOW) {  // Forward button?
    while (digitalRead(Fwd) == LOW) {} // released?
    skpFwd();            // then skip to forward file
  }
  if (digitalRead(Bkw) == LOW) {  // Backward button?
    while (digitalRead(Bkw) == LOW) {} // released?
    skpFwd();            // then skip to backward file
  } 
  setTmr();   // check and set sleep-timer
  delay(10);
}

/******************************
     User defined functions
 ******************************/
// *** Volume down as fnction volDown() ***
void volDown(void) {
  digitalWrite(IO1, HIGH);
  delay(lOn);           // long IO-on
  digitalWrite(IO1, LOW);
  Serial.println(F("Vol -" ));
  delay(chatrT);        // avoid double detect.
}
// *** Volume up as function volUp() ***
void volUp(void) {
  digitalWrite(IO2, HIGH);
  delay(lOn);           // long IO2-on
  digitalWrite(IO2, LOW);
  Serial.println(F("Vol +"));
  delay(chatrT);        // avoid double detect.
}
// *** Skip forward as function skpFwd() ***
void skpFwd(void) {     // *V03 added as optional
  digitalWrite(IO2, HIGH); // to skip to forward file
  delay(sOn);           // short IO2-on
  digitalWrite(IO2, LOW); // and off
  Serial.println(F("Fwd"));
  delay(chatrT);        // avoid double detect.
}
// *** Skip backward as function skpBkw() ***
void skpBkw(void) {     // *V03 added as optional
  digitalWrite(IO1, HIGH); // to skip to backward file
  delay(sOn);           // short IO2-on
  digitalWrite(IO1, LOW); // and off
  Serial.println(F("Bkw"));
  delay(chatrT);        // avoid double detect.
}
// *** Play the next as function playNext() ***
void playNext(void) {
  if (stopSig != stopOn) { // if not stop timing,
    digitalWrite(IO2, HIGH); // then play next, by
    delay(sOn);         // setting IO2-on
    digitalWrite(IO2, LOW); // for a short time
    Serial.println(F("Next file"));
    // wait for DFP action done
    while(digitalRead(pBusy) == HIGH){} 
  } else {
    Serial.println(F("Stopped"));
    while (true) {}   // and stop here
  }
}
// *** Set sleep-timer as function setTmr() ***
void setTmr(void) {
#define noSleep 2000000000 // 2 Billion mS (23days)
  static uint8_t vTmr;  // sleep-timer value (min)
  vTmr = 0;             // initialize static vTmr
  if (digitalRead(Tmr15) == LOW) { // if Tmr15 is set
    vTmr = vTmr + 15;   // then add 15 minutes
  }
  if (digitalRead(Tmr30) == LOW) { // if Tmr30 is set
    vTmr = vTmr + 30;   // then add 30 minutes
  }
  if (digitalRead(Tmr60) == LOW) { // if Tmr60 is set
    vTmr = vTmr + 60;   // then add 60 minutes
  }
  if (vTmr != vTsv) {   // if sleep-timer changed
    vTsv = vTmr;        // save it
    Serial.print(F("Timer(min)=")); // and show it
    Serial.println(vTmr);
  }
  static unsigned long lTmr = vTmr; // convert type
  slpT = lTmr * 60 * 1000 + 10000;  // mS val.+10sec
  if (vTmr == 0) {      // if no timer set,
    slpT = noSleep;     // then set a huge value
    digitalWrite(ledG, LOW); // and timer-LED off
  }
  if (vTmr > 0) {       // if sleep-timer set
    digitalWrite(ledG, HIGH); // timer-LED on
  }
  static unsigned long pTime;
  pTime = millis();     // get passed time
  if (slpT < pTime) {   // if timer expired,
    Serial.println(F("Timer expired"));
    digitalWrite(ledG, LOW); // timer-LED off
    for (uint8_t i = 0; i < init_vol_D; i++) {
      volDown();        // minimize volume
    }                   // power-off req'd afterward
    stopSig = stopOn;   // and set stop-sign on
  }
}
// *** End of program

 

以上、とりあえずご紹介まで。眠りにくい夜の入眠のお役に立てば幸いです。

 

©2021 Akira Tominaga, All rights reserved.

 

 

入眠タイマー付きMP3プレーヤーの製作(その3)

f:id:a-tomi:20210220180127j:plain

まさかこの単純な記事を3回にわたって書くとは思いませんでした。

前々回、前回からの続きですが、写真のような本番機をチョコッと作り試していましたら、なかなかいい感じなので書くことにしました。SN比も音質もとても良く、目的通りで眠りつくのも早い^^; ので、これが役立つ人がおられるかも。

操作部分は電源スイッチと、音量ダウンボタン、音量アップボタンのたった3つです。枕元近くに置くので、電源スイッチはトグルスイッチよりはスライドスイッチのほうが安全でよいでしょうね。

最初のうちは試しにマイクロSDに英語のセンテンスを沢山収容し、きっとこれならすぐに眠くなるだろうとやってみました。ところが30分たってもしっかり聞いてしまって眠くなりませんでした^^;。そこでタイマースイッチの15分を追加、設定が15分、30分、45分、60分、75分、90分、105分の7通りできるようにしました。

手直しした最終的なプログラムはこの記事の最後につけておきます。

この回路は「その2」と殆ど同じですが次です。

f:id:a-tomi:20210221092339j:plain



スリープタイマーがセットされていることを知らせるLED(緑)は、なるべく暗く点灯するようにRを大きめに設定してあります。

数日後から内容を入れ替えて、英語ではなく眠くなる音楽を選んで適当に入れてみました。ファイル名の順番に自動演奏されます。タイマーが来たら曲の途中でも音量がぐっと下がり、その曲が終わったら停止します。停止すると所用電流は小さいので、とくにパワーオフはしません。朝起きたらオフればよいわけで。

タイマーを45分にセットしていましたが、眠くなる音楽の場合はそんなに長い必要はありませんでした。30分で十分になりました^^;

単純な工作なのでどう作っても大丈夫でしょうが、以下、この場合の製作についてダラダラと書かせていただきます(といっても短いですが)。

基板は、50mmx50mmで次のようにデザイン(Top view=部品面から)。例によってはんだ用のランドなどは面倒なので省略し、早く作るのが優先でして^^

f:id:a-tomi:20210220181333j:plain

ソフトで変換した銅箔面の切削図(ツールパス)は次です。

f:id:a-tomi:20210220181453j:plain

毎度ですが、長年大事に使っているCNCルータでさっさと削ります。

f:id:a-tomi:20210220181608j:plain

次ができあがりの銅箔面。今回は比較的大きいので、アースはループを作らないようにしました。

f:id:a-tomi:20210220181741j:plain

表(部品面)は次です。

f:id:a-tomi:20210221164457j:plain


単純ですから、はんだ付けはすぐに終わりました。

f:id:a-tomi:20210220181916j:plain

きたないはんだ付けではありますが、一応電源を試験してとくに何も問題はない感じ。

f:id:a-tomi:20210220182016j:plain

ということで、使うスピーカーに見合うケースに組みこみました。秋月の次のケースです。

f:id:a-tomi:20210220182153j:plain

このケースは重く頑丈で箱鳴りの心配がない感じ。蓋もきっちりしまるのを選びました。容積は287mlあるので、ここで使っている小型フルレンジスピーカーなら、このほぼ密閉ボックスで問題が全くない感じです。

思いのほか大音量となるので、プログラムの最初で音量を実に15段階も下げてからスタートするようにしました。その分だけスタートが遅いですが、まあ入眠用ですからね^^

f:id:a-tomi:20210220182434j:plain

上の写真では、タイマーは30分にセットしてあります。

ついでですが、こういう基板をネジ穴なく取り付けるのに、次の「貼り付けボス」を使うとたいへん簡単で便利です。

f:id:a-tomi:20210220201910j:plain

この部品はケースにしっかり固定されますし、大きすぎる場合はニッパーで好きなように切れますので自由自在。

今回のマイコンの分担機能は単純なので、8ピンPICを使ってごく小さい基板でもできそうですね。回路のバリエーションも色々考えられますし。そして、近頃の超小型フルレンジスピーカーを使えばごく小さな箱でもできるわけです。そのうちにやってみます。

最後に、今回の製作に合わせた更新済プログラムをつけておきます。

 

/********************************************************
   DFPlayer with Sleep-timer and Volume-control,
     without using any libraries for DFPlayer-mini.
       Initial version-01 (c)Jan.30,2021 Akira Tominaga
   How to operate:
     Insert micro SD with mp3 or wav files, in playing
     sequences. Maximum 255 files x 99 folders.
     Power off & on to re-play after the timer expired.
     15,30,45,60,75,90,& 105 minutes timer available.
   Revisions:
     V02: Changed all pin assignments.     Feb.1,2021
     V02b: Added 15min timer switch        Feb.7,2021
 *******************************************************/
#define pBusy 15        // DFP BUSY pin, H when busy
#define Tmr15 12        // +15 min for sleep-timer (V02a)
#define Tmr30 11        // +30 min for sleep-timer 
#define Tmr60 10        // +60 min for sleep-timer
uint8_t vTsv = 0;       // save-a to detect timer changes
#define vol_D 5         // volume-down button
#define vol_U 8         // volume-up button
#define ledG  9         // led green for sleep-timer
#define IO1 17          // DFPIO1 short-on=<<,long-on=Vol-
#define IO2 16          // DFPIO2 short-on=>>,long-on=Vol+
#define sOn 100         // time (mS) for short-on
#define lOn 850         // time (mS) for long-on
#define chatrT 100      // chattering time to avoid
#define init_vol_D 15   // init. or ending # of vol.-downs 
unsigned long slpT = 0; // elapsed timer mS (large value)
#define stopOff 0       // stop signal is off
#define stopOn 1        // stop signal is on
uint8_t stopSig = stopOff; // initialize stop signal

void setup() {          // *** Arduino setup() ***
  pinMode (IO1, OUTPUT);
  digitalWrite(IO1, LOW);
  pinMode (IO2, OUTPUT);
  digitalWrite(IO2, LOW);
  pinMode (pBusy, INPUT);
  pinMode (Tmr15, INPUT_PULLUP);
  pinMode (Tmr30, INPUT_PULLUP);
  pinMode (Tmr60, INPUT_PULLUP);
  pinMode (vol_D, INPUT_PULLUP);
  pinMode (vol_U, INPUT_PULLUP);
  pinMode(ledG, OUTPUT);
  digitalWrite(ledG, HIGH); // show that I waked-up
  Serial.begin(9600);
  for (uint8_t i = 0; i < init_vol_D; i++) { // vol.down
    volDown();          // lower in the beginning
  }
  digitalWrite(ledG, LOW); // LED off
  Serial.println(F("DFP start"));
  playNext();           // start 1st music
  delay(300);           // delay before loop
}

void loop() {           // *** Arduino loop ***
  if (digitalRead(pBusy) == HIGH) { // if idle
    playNext();
  }
  if (digitalRead(vol_D) == LOW) { // vol.d button?
    while (digitalRead(vol_D) == LOW) {} // released?
    volDown();          //  then decrease volume
  }
  if (digitalRead(vol_U) == LOW) { // vol.up button?
    while (digitalRead(vol_U) == LOW) {} // released?
    volUp();             // then increase volue
  }
  setTmr();   // check and set sleep-timer
  delay(50);
}

/******************************
     User defined functions
 ******************************/
// *** Volume down as fnction volDown() ***
void volDown(void) {
  digitalWrite(IO1, HIGH);
  delay(lOn);           // long IO-on
  digitalWrite(IO1, LOW);
  Serial.println(F("Vol -" ));
  delay(chatrT);        // avoid double detect.
}
// *** Volume up as function volUp() ***
void volUp(void) {
  digitalWrite(IO2, HIGH);
  delay(lOn);           // long IO2-on
  digitalWrite(IO2, LOW);
  Serial.println(F("Vol +"));
  delay(chatrT);        // avoid double detect.
}

// *** Play the next as function playNext() ***
void playNext(void) {
  if (stopSig != stopOn) { // if not stop timing,
    digitalWrite(IO2, HIGH); // then play next, by
    delay(sOn);         // setting IO2-on
    digitalWrite(IO2, LOW); // for a short time
    Serial.println(F("Next file"));
    // wait for DFP action done
    while(digitalRead(pBusy) == HIGH){}
  } else {
    Serial.println(F("Stopped"));
    while (true) {}   // and stop here
  }
}

// *** Set sleep-timer as function setTmr() ***
void setTmr(void) {
#define noSleep 2000000000 // 2 Billion mS (23days)
  static uint8_t vTmr;  // sleep-timer value (min)
  vTmr = 0;             // initialize static vTmr
  if (digitalRead(Tmr15) == LOW) { // if Tmr15 is set
    vTmr = vTmr + 15;   // then add 15 minutes
  }
  if (digitalRead(Tmr30) == LOW) { // if Tmr30 is set
    vTmr = vTmr + 30;   // then add 30 minutes
  }
  if (digitalRead(Tmr60) == LOW) { // if Tmr60 is set
    vTmr = vTmr + 60;   // then add 60 minutes
  }
  if (vTmr != vTsv) {   // if sleep-timer changed
    vTsv = vTmr;        // save it
    Serial.print(F("Timer(min)=")); // and show it
    Serial.println(vTmr);
  }
  static unsigned long lTmr = vTmr; // convert type
  slpT = lTmr * 60 * 1000 + 10000;  // mS val.+10sec
  if (vTmr == 0) {      // if no timer set,
    slpT = noSleep;     // then set a huge value
    digitalWrite(ledG, LOW); // and timer-LED off
  }
  if (vTmr > 0) {       // if sleep-timer set
    digitalWrite(ledG, HIGH); // timer-LED on
  }
  static unsigned long pTime;
  pTime = millis();     // get passed time
  if (slpT < pTime) {   // if timer expired,
    Serial.println(F("Timer expired"));
    digitalWrite(ledG, LOW); // timer-LED off
    for (uint8_t i = 0; i < init_vol_D; i++) {
      volDown();        // minimize volume
    }                   // power-off req'd afterward
    stopSig = stopOn;   // and set stop-sign on
  }
}
// *** End of program

 

 簡単な紹介でしたが、どなたかのお役に立てば幸いです。

 

©2021 Akira Tominaga, All rights reserved.

 

 

これでOK!睡眠タイマー付きMP3プレイヤーをArduinoで(その2)

f:id:a-tomi:20210202121028j:plain

前回の記事の続きです。

工夫をした結果、ついにノイズの心配なくコントロールできるようになりました!

DFPlayerのノイズにつながり易いシリアル通信でのコントロールをやめて、IOボタン機能をArduinoでコントロールするようにしたらバッチリです。上の写真のとおり、大きなキャパシターは不要ですし、電源5VをATmega328Pと問題なく共用できます。そして良い音が出ます。

新たな回路は次のとおりです。毎度ながら汚い手書きですみませんが m(_ _)m

f:id:a-tomi:20210202133753j:plain
今回はDFPlayerのIO_1、IO_2ボタンにオープンコレクター出力をつないでコントロールします。このために2SC1815トランジスタを経由しますが、小信号用のNPNトランジスタなら何でもよいです。小さなトランジスタアレイ部品でもよいですね。2つだけなのでここではディスクリート(単体)部品を使っています。このやり方ならDFPlayerの発振の恐れをかんたんに回避できます。

f:id:a-tomi:20210202131610j:plain

上の写真で右下にあるIO_1、IO_2ボタンが、短押し(short push)だとそれぞれ前の曲、次の曲で、長押し(long push)だとそれぞれボリュームを下げる、上げるとなっています。また、右上にあるBUSY端子は曲の演奏中はLOWに、曲が終わり停止中はHIGHの信号を出します。

この3つの端子を使えばそのままプレイヤーとして機能します。「勝手な電子工作」としてはこれをマイコンでコントロールしたい。そして自動演奏(最後の次は最初にLoop)と、スリープ時間での停止がしたいわけです。スリープは曲の終了を待たず先にボリュームをゼロにしておけばさらによいかと。

DFPlayerのデータシートには、短押し、長押しの時間の記述箇所がなぜか見当たりません^^;  ここでは短押しは100mS、長押しは850mSにしますがこれで問題なし。(短くしたいですが、実験の結果、長押しを700mSにすると前後の曲選択ととられることがある)。

また、こういうスイッチ入力端子の場合、デバイス側では押下の検知後にチャタリングの影響回避として30~50mSのホールド時間がとられていると推定します。よって短押しも確実さのため上記の長さとしました。

Arduinoのプログラムは次のように作りました。ライブラリーなどを何も使わずに簡単にできますので、UNOの容量は10%以下に収まります。

/********************************************************
 * DFPlayer with Sleep-timer and Volume-control,        *
 *   without using any libraries for DFPlayer-mini.     *
 *     Initial version-01 (c)Jan.30,2021 Akira Tominaga *
 * How to operate:                                      *
 *   Insert micro SD with mp3 or wav files, in playing  *
 *   sequences. Maximum 255 files x 99 folders.         *
 *   Power off & on to re-play after the timer expired. *
 * Revisions:                                           *
 *   V02: Changed all pin assignments.     Feb.1,2021   *
 *****************************************************:**/
 #define pBusy 15       // DFPlayer BUSY pin, H when busy
#define Tmr30 11        // 30 min switch for sleep-timer 
#define Tmr60 10        // 60 min switch for sleep-timer
uint8_t vTsv = 0;       // save-a to detect timer changes
#define vol_D 5         // volume-down button
#define vol_U 8         // volume-up button
#define ledG  9         // led green for sleep-timer
#define IO1 17          // DFPIO1 short-on=<<,long-on=Vol-
#define IO2 16          // DFPIO2 short-on=>>,long-on=Vol+
#define sOn 100         // time (mS) for short-on
#define lOn 850         // time (mS) for long-on
unsigned long slpT = 0; // elapsed timer mS (large value)
#define stopOff 0       // stop signal is off
#define stopOn 1        // stop signal is on
uint8_t stopSig = stopOff; // initialize stop signal

void setup() {          // *** Arduino setup() ***
  pinMode (IO1, OUTPUT);
  digitalWrite(IO1, LOW);
  pinMode (IO2, OUTPUT);
  digitalWrite(IO2, LOW);
  pinMode (pBusy, INPUT);
  pinMode (Tmr30,INPUT_PULLUP);
  pinMode (Tmr60,INPUT_PULLUP);
  pinMode (vol_D, INPUT_PULLUP);
  pinMode (vol_U, INPUT_PULLUP);
  pinMode(ledG, OUTPUT);
  digitalWrite(ledG, HIGH); // show that I waked-up
  Serial.begin(9600);
   for (uint8_t i = 0; i < 3; i++) { // decrease volume
    volDown();          // lower in the beginning 
  }
  digitalWrite(ledG, LOW); // LED off 
  Serial.println(F("DFP start"));
  playNext();           // start 1st music
  delay(300);           // delay before loop
}

void loop() {           // *** Arduino loop ***
  if (digitalRead(pBusy) == HIGH) { // if idle
    playNext();
  }
  if (digitalRead(vol_D) == LOW) { // vol.d button?
    while (digitalRead(vol_D) == LOW) {} // released?
    volDown();          //  then decrease volume
  }
  if (digitalRead(vol_U) == LOW) { // vol.up button?
    while (digitalRead(vol_U) == LOW) {} // released?
    volUp();             // then increase volue
  }
  setTmr();   // check and set sleep-timer
  delay(300);
}

/******************************
 *   User defined functions   *
 ******************************/
// *** Volume down as fnction volDown() ***
void volDown(void) {
  digitalWrite(IO1, HIGH);
  delay(lOn);           // long IO-on
  digitalWrite(IO1, LOW);
  Serial.println(F("Vol -" ));
  delay(10);
}
// *** Volume up as function volUp() ***
void volUp(void) {
  digitalWrite(IO2, HIGH);
  delay(lOn);           // long IO2-on
  digitalWrite(IO2, LOW);
  Serial.println(F("Vol +"));
  delay(10);
}

// *** Play the next as function playNext() ***
void playNext(void) {
  if (stopSig != stopOn) { // if not stop timing,
    digitalWrite(IO2, HIGH); // then play next, by
    delay(sOn);         // setting IO2-on
    digitalWrite(IO2, LOW); // for a short time
    Serial.println(F("Next file"));
  } else {
    Serial.println(F("Stopped"));
    while (true) {}   // and stop here
  }
}

// *** Set sleep-timer as function setTmr() ***
void setTmr(void) {
#define noSleep 2000000000 // 2 Billion mS (23days)
  static uint8_t vTmr;  // sleep-timer value (min)
  vTmr = 0;             // initialize static vTmr
  if (digitalRead(Tmr30) == LOW) { // if Tmr30 is set
    vTmr = vTmr + 30;   // then add 30 minutes
  }
  if (digitalRead(Tmr60) == LOW) { // if Tmr60 is set
    vTmr = vTmr + 60;   // then add 630minutes
  }
  if (vTmr != vTsv) {   // if sleep-timer changed
    vTsv = vTmr;        // save it
    Serial.print(F("Timer(min)=")); // and show it
    Serial.println(vTmr); 
  }
  static unsigned long lTmr = vTmr; // convert type
  slpT = lTmr * 60 * 1000 + 10000;  // mS val.+10sec
  if (vTmr == 0) {      // if no timer set,
    slpT = noSleep;     // then set a huge value
    digitalWrite(ledG, LOW); // and timer-LED off
  }
  if (vTmr > 0) {       // if sleep-timer set
    digitalWrite(ledG, HIGH); // timer-LED on
  }
  static unsigned long pTime;
  pTime = millis();     // get passed time
  if (slpT < pTime) {   // if timer expired,
    Serial.println(F("Timer expired"));
    digitalWrite(ledG, LOW); // timer-LED off
    for (uint8_t i=0;i<10;i++){
      volDown();        // minimize volume
    }                   // power-off req'd afterward 
    stopSig = stopOn;   // and set stop-sign on
  }
}
// *** End of program

前回の記事でも書きましたが、Arduino-IDEを使ってArduinoーUNOへ普通にスケッチを書き込み、そしてATmega328p単体を抜き取って使うわけです。そうすれば本番機を小さく作るのが簡単なわけです。もちろん、ArduinoーUNOで結線しても、今回の回路は問題なくきれいに動きノイズは全くのりません。

f:id:a-tomi:20210202132718j:plain

これを実行するとシリアルモニターへ次のアウトプットが出ます。

f:id:a-tomi:20210202151142j:plain

出だしの音が少し大きすぎるので、プログラムでは最初にまずボリュームを3回下げています。

タイマー指定はパワーオンからの経過時間(+10秒としました)にしています。設定値は最初にでますが、演奏の途中で設定を変更すれば再設定され、追加で出力されるようにしてあります。曲を次々と演奏しますが、途中でボリュームボタンをいじるとそのように表示されます。

ATmega328p単体で実行する場合でも、Tx端子にシリアルモニター(作り方は次の記事の最後のほうに載せてあります。)をつなげば同じようにトレースができますね。

ソフトウェアシリアルをコンパクトに作るーArduino編-そしてトレースに使う - 勝手な電子工作・・

 

さて、本番機を作る前にこの記事を書こうとして試しました。そこで一苦労し、次のことに気づきました。

以下は余談ですから、読み飛ばされてもかまいませんが、DFPlayerを使ってもしノイズに悩まされる場合はご覧いただくと役立つかと思います。

==========================

UNOとの接続でいかにノイズが載らないとしても、下手なブレッドボードを使って下手な配置をすると発振がのる場合があります。次のような適当な配置だと音は出ますが、シュルシュルというかすかなノイズが混じり、びっくりしました。この場合、DFPlayerの電源に大きなCを入れても完全には解決しません。DFPlayerの発振だからです。

f:id:a-tomi:20210202152129j:plain

 

 接続線が長いのはまずいですが、ブレッドボードそのものにも問題の原因があることがわかりました。このブレッドボードは内部の接続部品が大きいため、浮遊容量(ストレ、Stray、寄生容量)が他のボードより大きいのです。裏をはがしてみれば一目瞭然ですが、次の写真で下のものです。

f:id:a-tomi:20210202152456j:plain

DFPlayerは小さなボードにデコーダー、DAC、それにアンプを積んでいるため、ストレが大きいと、出力が入力に回り込みやすいわけ。こういうブレッドボードでは配置をよく考えないと少し厳しいわけです。

 

テストであってもマイコン側の使用ピンを吟味して、DFlayerの入力ピンからは短く接続したいものです。そういうわけでブレッドボード自体をとりかえて、かつArduinoのピン割り当ても見直して試作したのが最初の写真です。

これであればたいへん安定しており、DFPlayerの電源に大きなCなどは不要です。ようやく完全に問題が出なくなったわけです^^; ここは普通のパスコン0.1μFでも大丈夫ですが、デジタルアンプを使うので一応1μFを入れてあります(D級アンプなので音量次第で電流はごく少ないし無音時はほぼ流れないわけです)。

ブレッドボードではなくいきなり基板を作っての本番機なら、そういう発振は簡単に防げそうでしょうが、なにせ事前にテストしたいものですから^^;

===========================

 

どっちにしても、余計な心配をしなくて済む今回の安定した回路で、近々本番機を基板から作ろうと思っています。そうすれば今後はきっとよく眠れますね・・・^^

シリアル接続でコントロールするのと比べた場合の機能上の違いは、イコライザの指定ができないことです。とはいえ、目的は睡眠用に小さな音で演奏するものなのでDefaultのLoudnessイコライザで十分。もし本格的なステレオを作る場合は、DFPlayerにStereo DAC出力ピンがついてます(大したものです)から、そこから他のアンプモジュールに入れる形にするのがよいですね。

 

ところで、あいにく決算期限と確定申告が迫ってます。ひょっとすると本番機の製作は少し後になるかもしれませんが、とにかく急いで追加記事を先に書かせていただきました。この記事がお役に立てばたいへん幸いです。

 

2021.2.22追記:チョコっと本番機を作ってしばらく試していました。音も良く入眠用に具合が良いので、ソフトも更新し次のリンクへ追加しておきました^^;

入眠タイマー付きMP3プレーヤーの製作(その3) - 勝手な電子工作・・

 

©2021 Akira Tominaga, All rights reserved.