海外でお化け探しといえばEMF (電磁場、electromagnetic fields、実際には静電気や磁気) を使って検知するのがメジャーなようですが、化けたん(ばけたん)は全く違う原理でお化けを探しているようです。化けたんの動作原理を理解し、自分でも同じようなお化け探知機を作りたいと思い、記事を書くことにしました。まだ「化けたん」の再発明には至っていませんが、最終的には回路図やプログラムを公開して、オープンソースなお化け探知機ができたらいいと思っています。
「良い反応」と「悪い反応」の定義について
前回の記事では「良い反応」の定義ができていませんでした。 今回作ったコードでは「良い反応の時には0101... や 000111... などのそろった目が多く出ている」というのを平均するとゼロに近い値になると読み替えて、32ビットのバイナリデータを1バイトと8ビットずつ4つに分けて0と1の出た目の差をだし、差の合計がゼロに近い時には「良い反応」としました。差の合計が大い時には出る目が偏っているので「悪い反応」としました。
こちらの記事で乱数の偏りや周期性を見つけるアルゴリズムについて考察してみました。周期性のあるデータを「良い反応」とみなしてもいいように思います。
配線
心霊スポットなどを探すのに持ち歩き中も反応をモニタ出来るようにLCDをつないでみました。使用したのは80 x 160の小型LCD(当時1個500円程度)です。
ドライバに ST7735 を使用しているとあったので Arduino のライブラリ管理画面に ST7735 と入力してトップに見つかった TFT というライブラリを使用しました。
配線についてはスマイル歯科blogのサイトを参考にしてArduino NanoとLCDの端子を以下のようにつなぎます。
D13 → SCK
D11 → SDA
D10 → CS
D9 → DC
D8 → RST
LCD → 1KΩを通じてGNDへ
GND → GND
不思議ですが3V3はどこにもつながなくても動きました。
ソースコード
汚いコードですがソースコードを以下にお示しします。
#include <avr/sleep.h>
#include <avr/wdt.h>
#include <SPI.h>
#include <TFT.h>
#define CS 10
#define DC 9
#define RESET 8
#define BYTES 4
#define BUZZ A0 // 圧電スピーカーをつなげる端子
#define ANT A5 // 浮動電圧を測る端子
#define VERYGOOD 0 // とても良い反応 (0以上の整数で設定)
#define GOOD 1 // 良い反応
#define BAD 10 // 悪い反応
#define VERYBAD 12 // とても悪い反応(16以下の整数で設定)
#define SIZE 5 // 画面サイズに合わせた倍率
#define VIEWSIZE 80
#define OFFSETY 25
#define HEIGHT 80+OFFSETY
#define WIDTH 160
TFT TFTscreen = TFT(CS, DC, RESET);
int xPos;
const unsigned int one=1;
unsigned long Xn = 'b';
union {
unsigned long int sum=0;
unsigned char div[BYTES];
} data;
void setup() {
unsigned int i;
unsigned char ypos[4];
Xn = analogRead(BUZZ) ^ analogRead(ANT); // M系列を電圧値で初期化
// バイナリデータ保存用変数の初期化
for(i=0; i<BYTES*8; i++){
data.sum <<=1;
data.sum |=i % 2;
}
// LCD初期化
TFTscreen.begin();
TFTscreen.background(128,128,128);
ypos[1]=VERYBAD*SIZE;
ypos[2]=BAD*SIZE;
ypos[3]=GOOD*SIZE;
ypos[3]=VERYGOOD*SIZE;
TFTscreen.stroke(255,255,0);
TFTscreen.line(0, OFFSETY+ypos[1], WIDTH, OFFSETY+ypos[1]);
TFTscreen.line(0, OFFSETY+ypos[2], WIDTH, OFFSETY+ypos[2]);
TFTscreen.line(0, OFFSETY+ypos[3], WIDTH, OFFSETY+ypos[3]);
Serial.begin( 9600 ); //シリアル通信開始
// スリープモードの設定
set_sleep_mode(SLEEP_MODE_PWR_DOWN);
wdt_enable_test(WDTO_15MS);
}
void loop() {
char count;
for(xPos=WIDTH; xPos>0; xPos--){
count=baketan();
// 悪い反応の時にブザーを鳴らす
if(count > BAD) buzz(400, 300);
draw2LCD(count); // LCDにグラフ表示
}
}
char baketan() {
unsigned char i;
unsigned int sensorValue=0;
char popcount;
do{
randomDelay();// ランダムな時間待つ
// ブザーと浮動電圧の電圧を読み取ってXORで足し合わせる
sensorValue = __builtin_popcount(analogRead(BUZZ) ^ analogRead(ANT));
}while(sensorValue==0); // sensorValue が 0 の場合静電気の影響を受けていると判断して再度電圧を取得する
data.sum <<=1; data.div[0] |=sensorValue & 1; // 偶奇性をメモリに登録
for(i=0, popcount=0; i<BYTES; i++){
// 1が立っている数を数えて期待値との差をとる
popcount+=abs(__builtin_popcount(data.div[i])-BYTES);
}
// シリアルモニタに送信
sendSerial(popcount, __builtin_popcount(data.div[0])-4);
return popcount;
}
void randomDelay(){
unsigned char i;
unsigned char wait;
for(i=0, wait=0; i<6; i++){
wait*=2;
wait+=mseq();
}
sleep_enable();
//delay(wait*3);
Serial.flush();
ADCSRA &= ~(1 << ADEN); // ADC off
for(i=0; i<wait; i++) {asm("sleep");};
sleep_disable();
ADCSRA |= (1 << ADEN); // ADC on
}
void sendSerial(char data1, char data2){// シリアルモニタに送信
Serial.print(data1*1);
Serial.print(", ");
Serial.print(data2*1);
Serial.print("\n");
}
void buzz(int freq, int jikan){
pinMode(BUZZ, OUTPUT);
tone(BUZZ,freq, jikan) ;
delay (jikan+50);
digitalWrite(BUZZ, LOW);
delay(50);
pinMode(BUZZ, INPUT);
delay(50);
}
void draw2LCD(char popcount){ // LCDにグラフ表示
int ypos[4];
static int avg=0;
ypos[0]=popcount*SIZE;
ypos[1]=BAD*SIZE;
ypos[2]=VERYBAD*SIZE;
ypos[3]=GOOD*SIZE;
TFTscreen.stroke(255,255,255);
TFTscreen.fill(64,64,255);
TFTscreen.rect(xPos-10, 0, 10, HEIGHT);
TFTscreen.stroke(255,255,0);
TFTscreen.line(xPos, OFFSETY+ypos[1]-1, xPos, OFFSETY+ypos[1]+1);
TFTscreen.line(xPos, OFFSETY+ypos[2]-1, xPos, OFFSETY+ypos[2]+1);
TFTscreen.line(xPos, OFFSETY+ypos[3]-1, xPos, OFFSETY+ypos[3]+1);
// 検出時のマーカー描画
if(popcount > VERYBAD){
TFTscreen.stroke(0,255,255);
TFTscreen.line(xPos, HEIGHT, xPos, HEIGHT-20);
}else if(popcount >BAD){
TFTscreen.stroke(0,0,255);
TFTscreen.line(xPos, HEIGHT, xPos, HEIGHT-10);
}else if(popcount == GOOD){
TFTscreen.stroke(255,0, 255);
TFTscreen.line(xPos, HEIGHT, xPos, HEIGHT-10);
}else if(popcount == VERYGOOD){
TFTscreen.stroke(255,0,0);
TFTscreen.line(xPos, HEIGHT, xPos, HEIGHT-20);
}else{
TFTscreen.stroke(255,255,255);
TFTscreen.line(xPos, HEIGHT, xPos, HEIGHT-10);
}
avg=avg * 7/8 + ypos[0];
TFTscreen.stroke(255,64,255);
TFTscreen.line(xPos, OFFSETY+avg/8-2, xPos, OFFSETY+avg/8+2);
TFTscreen.stroke(0,0,0);
TFTscreen.line(xPos, OFFSETY+ypos[0]-1, xPos, OFFSETY+ypos[0]+1);
}
// M系列の変換
char mseq(){
// シフトレジスタ
//static unsigned long Xn = 'b';
unsigned ret;
ret = ((Xn & 16384) >> 14) ^ (Xn & 1);
Xn = (Xn << 1) + ret;
return ret;
}
//引数にはWDTO_500MS WDTO_1S WDTO_2S WDTO_4Sなどと設定
void wdt_enable_test(byte b){
//ウォッチドッグ・タイマー分周比設定ビットの4ビット目がWDTCSRレジスタで2ビットずれているための処理
b = ((0b00001000 & b) << 2) | (0b00000111 & b);
//WDCE ウォッチドッグ変更許可ビットを立てる
b |= 0b00010000;
//WDRF ウォッチドッグ・システム・リセット・フラグにゼロを書き込んでリセットの準備をする
MCUSR &= 0b11110111;
//WDE ウォッチドッグ・システム・リセット許可のビットを立ててリセット WDCE ウォッチドッグ変更許可により4サイクル以内は設定を変更可能
WDTCSR |= 0b00011000;
//事前に作成しておいた設定を流し込む
WDTCSR = b;
//WDIE ウォッチドッグ・割込み許可ビットを立てる
WDTCSR |= 0b01000000;
}
//ウォッチドッグの割り込み処理ルーチン(空だけど必要)
ISR(WDT_vect) {
}
Githubにもソース公開しました。