2016年1月29日金曜日

ESP + 2軸サーボ


いやー、やっぱり金曜日の夜はサーボに限りますな(意味不明)。

某Elecrowは基板だけでなく、いろんな電子部品も扱っています。ある日、2軸のサーボモータを使ったカメラ台?を見つけたので、すぐに注文する私であった。

Elecrowのサイトより

で、買ってそのまま忘れるのが私のいつものパターン。

で、思い出したのですぐ作る。

基板は、今や我が家の電子工作標準品となったESP-WROOM-02のバグあり試作品ボード(笑)。Tx-Rx-Vin-GndあたりはFTDIとつないだ方がラクなので足長ソケット、その他はブレッドボード用のヘッダピンを取り付けました。

机の塗装塗り直そう(と毎回思う

回路は、サーボモータのVCCとUSBシリアルのUSB 5V(VIN)、GNDとGND、上のサーボの信号線(黄色)とIO12、下のサーボの信号線(黄色)とIO13をつなぎます。その他、VINとGNDの間に1000μFの電解アルミコンデンサをブチ込んでやります。サーボの中身はモータなのでかなりノイズ乗ったり電源に悪さをしますので。今回、サーボモータ用のコネクタ(JRタイプ?)を買い忘れたので、テフロン線をひんむいてヒシチューブ加工し、IO12/IO13とつなぎました。

まだ何に使うか決めていないので、とりあえず試運転。レーザーポインターでも買ってくるかな。



そういえば高校時代(物理部)で、物理室にあったレーザーチューブを使ってレザリアムとかやったなー。当時ものすごく効果だったHe-Neレーザなので、先生に壊すな壊すなくれぐれも壊すなと顔を見るたびに言われたっけ。レーザーの値段は当時の1/1000ぐらいになったんじゃないかなぁ。あの頃1mWのレーザで10万円してたもんね。i8255の先にお手製のラダー型DACをつないでステレオのアンプでスピーカーを駆動し、スピーカーにミラーを取り付けてレーザーを当てる式。表面反射ミラーじゃないと像が二重になるんだけど、高いので銀鏡反応でプレパラートにメッキして試したもんだった。制御に使ったのはTRS-80。1.78MhzのZ80では当然アセンブラではないと間に合わず、カセットテープからアセンブラを読み込んではテストラン、テストランしてはアセンブラをロードしなおし、の大変つらい開発じゃった。



…昔話の方が長くなってすみません。

/*
ESP tilt/pan servo motor demo
by k.kurahashi 2016-01-29
based on 'Sweep' by BARRAGAN <http://barraganstudio.com>
thanks!
this code is in the public domain
*/
#include <Servo.h>
Servo lower;
Servo upper;
// upper = io12, 30 - 180
// lower = io13, 0 - 150
void setup()
{
upper.attach(12); // attaches the upper servo(up/down) on GIO12 to the servo object
lower.attach(13); // attaches the lower servo(right/left) on GIO13 to the servo object
}
void loop()
{
int pos;
for (float r = 0.0; r < 360.0; r += 0.005) {
float x = sin(r*2);
float y = cos(r);
upper.write(x * 60 + 60 + 40);
lower.write(y * 60 + 60 +10);
delay(2);
}
}

Elecrow基板、発注から10日で到着w


Elecrowへの発注は1月18日の夜、1月28日夜到着しました。

最初は何度も確認して、それでも足りないところがあったのですが、今ではググることもなくさくさくとCAMプロセッサアイコンをクリック→ファイルを開く→Elecrow Garber Generatorファイルを選び→Process Jobボタンを押すまで進み、あとは必要なファイルを選んでzipかけて送るだけ。慣れたものです。

で、EMSで26日発送。いつまで経っても中国EMSの配送ステータスが変わらないのでヤキモキしていたのですが、丸2日でした。EMSは約11ドルです。

一方、今まで使っていたDHLは$16。発送したその日に成田に着いていることがあって驚くのですが、それから先は最悪。日本に着いてから3日ぐらい止まったままなんてことがよくあります。高いし、再配達依頼のタイムリミットが前日昼までだったりする殿様商売のDHLなんてもう使わない!

それにしても、今回も仕上がりはバッチリ。今回4種類10枚ずつ頼んで例によって12枚ずつ到着、送料込みで1枚120円。ありがたやありがたや…。

初めて黄色を選んで見たけど透けたところが懐かしい色だなや

さて、今週末は充実した休みになりそうです。いや、2月は春節で中国とのやりとりはほぼ何もできないので、今週末ではなく2月一杯というべきかもしれませんが…。

2016年1月27日水曜日

お風呂時計(第三回・小改良)

■少しだけ改造■

はい、続きです。


電池が弱くなると実際より明るく判断してしまう傾向があるようで、風呂場の照明を切っていても脱衣場を点灯すると時計のスイッチも入ってしまいます。

また、ちょっと向きを変えたりした時に手で影ができると計時が止まってしまい、入浴時間ゼロに戻ってしまいます。

ということで、時計が点灯中は、暗くなったかどうかの基準値をより小さくします。つまり、一度点灯したと判断したらかなり暗くしないと止まらないよ、という改造。今までは明暗の判断を1つの数値(45)で行っていたのですが、今回、明るくなったという判断には60、暗くなったという判断は50にしてみました。これで使用中にちょっと影になっても止まらなくなりました。

もう1つ、一度止まっても、30秒間以内に明るくすれば計時をリセットしないようにしました。これでちょっと影になっても、入浴時間がゼロに戻ることがなくなりました。

というわけで、フル充電した電池での開始時間は1/27 22:10です。

追記:終了は3/4 06:00でした。37日と8時間。「暗くなった」という判断値が大きすぎたようで、入浴後に脱衣所からふと風呂場を見ると点灯している、ということがしばしばありました。うーん、しかし、こうやってパラメータをいじって正解にたどり着いたとしても、それは「うちの風呂場で、今のLED電球使っていて」という限定条件ですからね。ここはやっぱり適応制御か何かしないといけない気が。それはそれとして、良いアルゴリズムを思いつくまで、とりあえず70 / 60で使おう←直さない予感 開始:3/4 21:00。

追記の追記:70 / 60だと横にてぬぐいを置いただけで消えてしまい、それはそれで面白いのですが、実用性にかけるので70 / 45にしました。再充電して3/5 08:25再開。

それにしても、こうやってバージョンごとにソースを張っているのはあまりにも頭が悪いので、github使います。

追記^3:あ、記録してなかった。もうちょっとLEDを暗くしたいのですががっちり配線してしまったユニバーサル基板に抵抗入れるのは面倒くさい。ので、デューティー比を変えてみたのですが1:500でもまだ十分明るいです。瞬間何mA流れてるんだろ。5月27日 5時再開。

追記@4:7月6日ぐらいから物凄く暗くなりましたが、7月9日の朝、まだ生きてます。でも、表示読めないしエネループ破損が怖いので止めます。7月9日 06:00。43日で思ったほど伸びなかったのですが、夏になって朝シャワーだけでなく夕方浴槽に浸かるようになったので1日あたりの点灯時間はほぼ倍になっているはず。さすがにデューティー比1:500は暗すぎたので、次は1:200ぐらいにしてみます。7月9日 17:00スタート。明るいw

追記@5:8月21日ぐらいから暗くなり、8月24日ほとんど表示は見えませんが、ここでおしまいにします。1:200だとチラツキが目立つので1:100にしました。デューティー比あんまり電池寿命と関係ないんじゃないか?という気がしてきました(笑)。ということで8月24日5時スタート。

追記@6:いきなり10月2日に消えました。明るいと電池の余力ないですね。でも、明るさと電池寿命はほとんど関係ないですね。精密な微小消費電流計測のできる機材が欲しい。ともあれ、1:100でほぼ支障ないので、そのまま継続。10月4日、電池セット。
#include <sRTC.h>
#include <legacymsp430.h>
#include <APDS9930.h>
// LEDのポート
const int segA = P1_4;
const int segB = P1_5;
const int segC = P2_0;
const int segD = P2_5;
const int segE = P2_2;
const int segF = P2_3;
const int segG = P2_4;
//int segCol = P2_6;
int digits[] = {
P1_0, P1_1, P1_2, P1_3}; // for serial
#define ALL_OFF 10
const uint8_t patA[] = {
HIGH, LOW, HIGH, HIGH, LOW, HIGH, HIGH, HIGH, HIGH, HIGH, LOW};
const uint8_t patB[] = {
HIGH, HIGH, HIGH, HIGH, HIGH, LOW, LOW, HIGH, HIGH, HIGH, LOW};
const uint8_t patC[] = {
HIGH, HIGH, LOW, HIGH, HIGH, HIGH, HIGH, HIGH, HIGH, HIGH, LOW};
const uint8_t patD[] = {
HIGH, LOW, HIGH, HIGH, LOW, HIGH, HIGH, LOW, HIGH, HIGH, LOW};
const uint8_t patE[] = {
HIGH, LOW, HIGH, LOW, LOW, LOW, HIGH, LOW, HIGH, LOW, LOW};
const uint8_t patF[] = {
HIGH, LOW, LOW, LOW, HIGH, HIGH, HIGH, LOW, HIGH, HIGH, LOW};
const uint8_t patG[] = {
LOW, LOW, HIGH, HIGH, HIGH, HIGH, HIGH, LOW, HIGH, HIGH, LOW};
const uint8_t sevenPort[] = {
segA, segB, segC, segD, segE, segF, segG};
const uint8_t *sevenSeg[] = {
patA, patB, patC, patD, patE, patF, patG};
RealTimeClock RTC;
// APDS9930
APDS9930 apds = APDS9930();
uint16_t ch0 = 0;
uint16_t ch1 = 1;
// 最後に表示を更新した時刻(秒)
unsigned long prevSec = 99;
// sprintf用バッファ
char spfBuf[20], buf[20];
unsigned long started;
void setup()
{
// led
for (int i = 0; i < 7; i++) {
pinMode(sevenPort[i], OUTPUT);
digitalWrite(sevenPort[i], LOW);
}
for (int i = 0; i < 4; i++) {
pinMode(digits[i], OUTPUT);
digitalWrite(digits[i], HIGH);
}
// Initialize APDS-9930 (configure I2C and initial values)
if ( apds.init() ) {
// Serial.println(F("APDS-9930 initialization complete"));
}
else {
// Serial.println(F("Something went wrong during APDS-9930 init!"));
}
// Start running the APDS-9930 light sensor (no interrupts)
if ( apds.enableLightSensor(false) ) {
// Serial.println(F("Light sensor is now running"));
}
else {
// Serial.println(F("Something went wrong during light sensor init!"));
}
for (int i = 0; i < 5; i++) {
digitalWrite(P1_0, HIGH);
delay(100);
digitalWrite(P1_0, LOW);
delay(100);
}
RTC.begin();
RTC.RTC_hr = 19;
RTC.RTC_min = 30;
RTC.RTC_sec = 0;
setDigit(0);
}
void setDigit(int d) {
for (int i = 0; i < 4; i++) {
if (d == i)
digitalWrite(digits[i], LOW);
else
digitalWrite(digits[i], HIGH);
}
}
int digit = 0;
int loopCount = 999;
#define MODE_DARK 1
#define MODE_BRIGHT 2
int mode = MODE_DARK;
int isTime = 0;
// #define THRESHOLD 45
#define IS_BRIGHT_LIMIT 60
#define IS_DARK_LIMIT 48
static int duration = 0; // 入浴時間
static int durationDark = 0; // 暗くなってからの時間
static int light = 100; // 直近の明るさ。だいたい100-1000、明かりを消した風呂場で50以下
int lightToTonTime() {
int t = light / 10;
if (t > 90) t = 90;
if (t < 10) t = 10;
return t;
}
void updateLight() {
float f = 0;
apds.readAmbientLightLux(f);
int current = f;
light = (light + current) / 2;
}
void loop()
{
// turn off
setDigit(-1);
delayMicroseconds(100-lightToTonTime());
// チラつきを軽減するために点灯中にセンサーを読みに行く
int sec = RTC.RTC_sec;
if (sec != prevSec) {
if (mode == MODE_DARK) { // mode is Dark
durationDark++;
updateLight();
if (light > IS_BRIGHT_LIMIT) {
mode = MODE_BRIGHT;
BCSCTL1 = CALBC1_8MHZ;
DCOCTL = CALDCO_8MHZ;
// 入浴時間計測開始
if (durationDark > 30)
duration = 0;
isTime = 0;
}
}
else { // mode is Bright
duration++;
if ((duration % 2) == 0) {
isTime = (isTime == 0);
if (isTime) {
updateLight();
if (light <= IS_DARK_LIMIT) {
mode = MODE_DARK;
BCSCTL1 = CALBC1_1MHZ;
DCOCTL = CALDCO_1MHZ;
durationDark = 0;
}
}
}
if (isTime) { // 時刻表示
itoa(10000 + RTC.RTC_hr*100+RTC.RTC_min, buf, 10);
strcpy(spfBuf, buf+1);
}
else { // 入浴時間
int m = duration / 60;
int s = duration % 60;
itoa(10000+m*100+s, buf,10);
if (buf[1] == '0') buf[1] = ' ';
strcpy(spfBuf, buf+1);
}
}
prevSec = sec;
}
// 表示処理
// 対象となる桁の値を0-9か空白に変換しセグメントビットに出力
int num = spfBuf[digit];
if (mode == MODE_DARK) {
out(ALL_OFF, LOW);
setDigit(-1);
}
else {
num = (num >= '0' && num <= '9') ? num - '0' : ALL_OFF;
out(num, LOW);
// 桁ビットを出力(点灯=high)
setDigit(digit);
}
delayMicroseconds(lightToTonTime()); // lightはだいたい1000-100
//次の桁へ
digit++;
if (digit >= 4) digit = 0;
}
void out(int num, int DP) {
for (int i = 0; i < 7; i++) {
digitalWrite(sevenPort[i], sevenSeg[i][num]);
}
}
interrupt(TIMER1_A0_VECTOR) Tic_Tac(void) {
RTC++; // Update secondes
};

2016年1月24日日曜日

BME280とOLEDをESP-WROOM-02で使ってみた


■構成■

BME280はボッシュ製の気温湿度気圧センサー。ワンチップでこの3つが計測できてなかなか便利、しかも秋月で600円1080円。SPIかI2Cで接続できますが、今回はI2Cとします。しかし秋月さん、どのぐらいの単位で仕入れているんでしょうね。Mouser.jpとかでこのチップ取り寄せても700円以上するし基板は特殊形状なので普通ならかなり高価になるはずです。考えるだけで恐ろしい。

OLEDは今や電子工作標準品ともいうべき0.96インチサイズのアレ。作例もライブラリも豊富、I2Cと電源/GNDの合計4本だけで動いてしまいフレームバッファ(画像メモリ)も持っているので何かとラク。

できあがったあとで、WiFiついてるのにセンサーと表示器を同居させてどうすんだ?と自分自身に突っ込みました。

■製作■

何かケースに入れて外に設置したいので、今回もユニバーサル基板で組みました。以前、「いつかきっと使う」という想いで買い集めたaitendoのFRISK大の変換基板を適当に折って使います。基板上にモジュールを挿したヘッダソケットを挿して見て、レイアウトに見当を付けます。ハンダ付けが終わったあとでおとなりと干渉して実装できない、なんてのは悲しすぎますので。

それから秋月の資料とOLEDの実物をみながら、簡単に配線メモを書きます。前回のLEDで懲りました。複数の資料をあたりながら裏側での配線を考えるのは私の脳ミソでは無理だ、と。ということで、裏側、配線面で各要素のピンがどうなっているかをメモにしておきます。



上の段からOLED、ESP基板、BME280基板I2Cでの接続(ユニバーサル基板上での位置、I2Cでの接続、BME280基板での名前)と並んでます。下の2つは作業過程のメモで使うのは上の3つです。上から4ピンヘッダソケット、9ピン足長ソケット(10ピンのを切断して使用)、6ピンヘッダソケットを使います。BME280のCSBは3.3v、SDOはGNDに接続しました。SDOはHIGHかLOWかでI2Cのアドレスを切り替えることができますが、GNDに接続するとデフォルトの0x76になり、今回使ったライブラリもそのままで動きます。



VDD、GND、SDA、SCLをそれぞれ接続します。ああ言葉で言うと簡単。朝っぱらから2時間かかったですよ、これ作るのにorz ヘッダソケットで3階建てになっているので、ヤケに高さがあります。

一番上で緑の背中を見せているのはBME280


■ソフトウェア■

BME280はこちらのライブラリを使い、サンプルを参考にさせていただきました(参考というかほぼ手直しが必要ないのですけども)。

OLEDはこちらです。今回、単純な文字表示にしか使わなかったのですが、uiクラスがあってそれを使うと複数画面を指定したアニメーションで入れ替えつつ任意の位置にミリ秒単位で別の画面をオーバーレイできる、という素晴らしい完成度です。また文字表示も素晴らしく、きれいなフォントはサイズが3種類あって右寄せ左寄せなども使えます。残念ながらクラスにラインを引っ張る命令がないので、そのうちDDAか何かで作って貢献したいですが…。

これら素晴らしいライブラリの作者に経緯を表し、感謝申し上げます。

さて、書いたプログラムはごく単純です。setupで各要素を初期化、loopでは1秒ごとに計測して表示を更新し、1分ごとにThingSpeak.comに送っています。

// ESP_BME280_SSD1306 by k.kurahashi 2016-01-24
//
// thanks:
// BME280 Library
// https://github.com/embeddedadventures/BME280
// OLED Library
// https://github.com/squix78/esp8266-oled-ssd1306
//
// ThingSpeak.com
// https://thingspeak.com/channels/81094
//
#include <Wire.h>
#include <Time.h>
#include <BME280_MOD-1022.h>
#include "SSD1306.h"
#include "SSD1306Ui.h"
#include <ESP8266WiFi.h>
#include <WiFiClient.h>
extern "C" {
#include "user_interface.h"
}
//
// BME280
//
float averageHumidity; // 湿度の計測値はわりとフラフラするので簡単な移動平均を取る
//
// OLED / SSD1306
//
SSD1306 display(0x3c, 4, 5);
//
// Wifi
//
const char *ssid = "***********";
const char *password = "***********";
WiFiClient client;
//
// Setup BME280 ほぼサンプル, thanks for Embedded Adventures
//
void setup_BME280() {
uint8_t chipID;
Serial.println("Welcome to the BME280 MOD-1022 weather multi-sensor test sketch!");
Serial.println("Embedded Adventures (www.embeddedadventures.com)");
chipID = BME280.readChipId();
// find the chip ID out just for fun
Serial.print("ChipID = 0x");
Serial.print(chipID, HEX);
// need to read the NVM compensation parameters
BME280.readCompensationParams();
BME280.writeStandbyTime(tsb_0p5ms); // tsb = 0.5ms
BME280.writeFilterCoefficient(fc_16); // IIR Filter coefficient 16
BME280.writeOversamplingTemperature(os2x); // temperature x2
BME280.writeOversamplingHumidity(os1x); // humidity x1
BME280.writeOversamplingPressure(os16x); // pressure x16
BME280.writeMode(smNormal);
}
//
// setup
//
void setup() {
Serial.begin(115200);
Serial.println();
Serial.println();
//
// Wifi
//
WiFi.begin ( ssid, password );
Serial.println("Started");
// Wait for connection
while ( WiFi.status() != WL_CONNECTED ) {
delay ( 500 );
Serial.print ( "." );
}
Serial.println("Wifi Connected");
// BME and OLED
Wire.begin();
setup_BME280();
display.init();
display.flipScreenVertically();
display.displayOn();
display.clear();
}
//
// loop
//
void loop() {
draw();
delay(1000);
}
int prevMinute = minute(); // 分ごとに送信するためのフラグ
//
// OLEDへの描画
//
void draw() {
float temp, humidity, pressure, pressureMoreAccurate;
double tempMostAccurate, humidityMostAccurate, pressureMostAccurate;
char buffer[80];
// BME280起動待ち
BME280.writeMode(smForced);
while (BME280.isMeasuring()) {
delay(50);
}
// BME280計測実行
BME280.readMeasurements();
// BME280各データ取り出し
temp = BME280.getTemperature();
humidity = BME280.getHumidity();
pressure = BME280.getPressure();
pressureMoreAccurate = BME280.getPressureMoreAccurate(); // t_fine already calculated from getTemperaure() above
tempMostAccurate = BME280.getTemperatureMostAccurate();
humidityMostAccurate = BME280.getHumidityMostAccurate();
pressureMostAccurate = BME280.getPressureMostAccurate();
// 湿度の値を平準化
averageHumidity = (averageHumidity * 9.0 + humidityMostAccurate) / 10.0;
// OLEDへ表示
display.clear();
display.setFont(ArialMT_Plain_10);
display.setTextAlignment(TEXT_ALIGN_LEFT);
display.drawString( 0, 16, "Temperature");
display.drawString( 0, 32, "Humidity");
display.drawString( 0, 48, "Pressure");
display.setFont(ArialMT_Plain_16);
display.setTextAlignment(TEXT_ALIGN_RIGHT);
dtostrf(tempMostAccurate, 7, 2, buffer);
display.drawString(127, 12, buffer);
dtostrf(averageHumidity, 7, 2, buffer);
display.drawString(127, 28, buffer);
dtostrf(pressureMostAccurate, 7, 2, buffer);
display.drawString(127, 44, buffer);
display.display();
// ThingSpeak.comへ送信
if (minute() != prevMinute) {
sendTHP(tempMostAccurate, averageHumidity, pressureMostAccurate);
prevMinute = minute();
}
}
//
// ThingSpeakへのパラメータを作って送信
//
void sendTHP(float inTemperature, float inHumidity, float inPressure) {
char tBuf[10], hBuf[10], pBuf[10];
dtostrf(inTemperature, 7, 2, tBuf);
dtostrf(inHumidity, 7, 2, hBuf);
dtostrf(inPressure, 7, 2, pBuf);
String postStr = "&field1=" + String(tBuf) + "&field2=" + String(hBuf) + "&field3=" + String(pBuf);
send(postStr);
Serial.println(postStr);
}
// ThingSpeakへ送信
//
void send(String inPostStr) {
String apiKey = "********************";
Serial.print("Connecting...");
if (client.connect("184.106.153.149", 80)) { // api.thingspeak.com
Serial.print("Connected....");
String postStr = apiKey + inPostStr + "\r\n\r\n";
client.print("POST /update HTTP/1.1\n");
client.print("Host: api.thingspeak.com\n");
client.print("Connection: close\n");
client.print("X-THINGSPEAKAPIKEY: " + apiKey + "\n");
client.print("Content-Type: application/x-www-form-urlencoded\n");
client.print("Content-Length: ");
client.print(postStr.length());
client.print("\n\n");
client.print(postStr);
Serial.println("posted.");
}
client.stop();
}
ご質問などはコメント欄にご遠慮なく。

2016年1月21日木曜日

ESP-WROOM-02で温度データ収集

放熱グリスでアルミ蓋に貼り付けてカプトンテープで固定。カプトンの意味ないですが。

ESP-WROOM-02にCMOSの温度センサーをつなぎ、単純なwait loopで1分ごとにデータ収集してみました。センサーはアナログ出力で8.2mV/Cなのですが、手を抜いて1/2に分圧してESP-WROOM-02のTOUTに入れているだけなので分解能低いです。ADC入力0-1023が2vに対応するので、約2mVが最小単位。約0.25度単位でしかデータを収集できません。ちゃんとオペアンプなどでレベルシフトし、ここ東京の気温域-5 〜 40度ぐらいがフルスケールになるようにすれば、0.05度ぐらいは計れるのですが(ノイズが)。

#include <Time.h>
#include <ESP8266WiFi.h>
#include <WiFiClient.h>
extern "C" {
#include "user_interface.h"
}
//
// Wifi
//
const char *ssid = "*************";
const char *password = "*************";
WiFiClient client;
//int prevMinute;
void setup() {
Serial.begin(115200);
//
// Wifi
//
WiFi.begin ( ssid, password );
Serial.println("Started");
// Wait for connection
while ( WiFi.status() != WL_CONNECTED ) {
delay ( 500 );
Serial.print ( "." );
}
Serial.println("Wifi Connected");
prevMinute = minute();
}
void loop() {
if (prevMinute != minute()) {
sendTemperature();
prevMinute = minute();
}
delay(1000);
}
void sendTemperature() {
char buffer[20];
float t = readTemperature();
dtostrf(t, 2, 1, buffer);
Serial.println(buffer);
String postStr = "&field1=" + String(buffer);
send(postStr);
Serial.println(postStr);
}
void send(String inPostStr) {
String apiKey = "**********************";
Serial.print("Connecting...");
if (client.connect("184.106.153.149", 80)) { // api.thingspeak.com
Serial.print("Connected....");
String postStr = apiKey + inPostStr + "\r\n\r\n";
client.print("POST /update HTTP/1.1\n");
client.print("Host: api.thingspeak.com\n");
client.print("Connection: close\n");
client.print("X-THINGSPEAKAPIKEY: " + apiKey + "\n");
client.print("Content-Type: application/x-www-form-urlencoded\n");
client.print("Content-Length: ");
client.print(postStr.length());
client.print("\n\n");
client.print(postStr);
Serial.println("posted.");
}
client.stop();
}
float readTemperature() {
float v = system_adc_read() / 1.062000 * 2.00000; // mV
return 30.00000 - (v - 1474.000) / 8.2000000;
}
動作時間は53時間で使用した電源はエネループ単4☓3本。単4エネループは750mAhなので消費電流は平均14mAです。何にも工夫していないのに案外長持ちしたなぁ。

ということで、今度は「Deep Sleepで55秒休んでconnect〜送信したらすぐまた休む」方法を試そう! …と思ったら充電済のエネループがありませんでした。現在送っているデータは私の部屋の温度です。面白くもなんともないな。

追記:113時間持ちました。
強化版単3エネループに換算すると350時間。1ヶ月の道は遠いです。

とりあえずソースはこちら。

#include <Time.h>
#include <ESP8266WiFi.h>
#include <WiFiClient.h>
extern "C" {
#include "user_interface.h"
}
//
// Wifi
//
const char *ssid = "***********";
const char *password = "***********";
WiFiClient client;
void setup() {
Serial.begin(115200);
//
// Wifi
//
WiFi.begin ( ssid, password );
Serial.println("Started");
// Wait for connection
while ( WiFi.status() != WL_CONNECTED ) {
delay ( 500 );
Serial.print ( "." );
}
Serial.println("Wifi Connected");
sendTemperature();
ESP.deepSleep(55*1000*1000, WAKE_RF_DISABLED);
delay(1000);
}
void loop() {
}
void sendTemperature() {
char buffer[20];
float t = readTemperature();
dtostrf(t, 2,1, buffer);
Serial.println(buffer);
String postStr = "&field1=" + String(buffer);
send(postStr);
Serial.println(postStr);
}
void send(String inPostStr) {
String apiKey = "**********************";
Serial.print("Connecting...");
if (client.connect("184.106.153.149", 80)) { // api.thingspeak.com
Serial.print("Connected....");
String postStr = apiKey + inPostStr + "\r\n\r\n";
client.print("POST /update HTTP/1.1\n");
client.print("Host: api.thingspeak.com\n");
client.print("Connection: close\n");
client.print("X-THINGSPEAKAPIKEY: " + apiKey + "\n");
client.print("Content-Type: application/x-www-form-urlencoded\n");
client.print("Content-Length: ");
client.print(postStr.length());
client.print("\n\n");
client.print(postStr);
Serial.println("posted.");
}
client.stop();
}
float readTemperature() {
float v = system_adc_read() / 1.062000 * 2.00000; // mV
return 30.00000 - (v - 1474.000) / 8.2000000;
}
deep throatじゃなかったdeep sleepのモードはWAKE_RF_DISABLEDにしてみました。各モードを試してみましたが、55秒休んで再接続という使い方ではこのモードが一番安定して早く再接続できました。たぶんWifiルータのログが大変なことになっていると思いますけどもw 最初に試したWAKE_RF_DEFAULTは起動してから再接続するまでの時間がヤケにバラついていました。

なお、某TareObjects社のBoard1基板ではRESETとIO16をハンダジャンパだけで簡単に接続することができるのでお薦めです(謎)。

今回100均で買ったジャム瓶に入れて風呂の湯温を計ったりしてみたんですが、やっぱり水漏れしますね。プラスチック+アルミ蓋なので、お湯で温まった時に空気が出て、冷えるとその分水を吸い込んでくる感じ。低電圧なので少し濡れたぐらいでは壊れたりしないのですが、やっぱ防水って難しいです。

詳細な製作記事はいずれまた。

2016年1月14日木曜日

熱収縮チューブにはヒーティングガンを

美しい収縮っぷりに刮目せよ(大げさな

去年、ヒーティングガンを買いました。ホットガンとかヒートガンとも言われる、ドライヤーの親玉みたいなやつ。髪の毛が乾くどころか瞬殺状態になるような高温の熱風を吹き出してくれます(試さないように。火傷します)。

これを使ったリフローの方法があるというので試してみようと思ったのですが、買ったころホームリフロー環境が劇的に改善したので結局基板/ハンダペーストを炙る機会は今のところありません。リフローに失敗したQFPを外す場合なんかに使う日が来るとは思いますが。

いつその日が来るかはわかりませんので、とりあえず本来の用途の1つである「熱収縮チューブの収縮」に使ってます。これが、今までのチャッカマンやハンダゴテの上昇気流は何だったんだ!というぐらいきれいにできます。

もう、芯線が少しでも見えたら、配線2本以上が遊んでいたら即!という勢いで熱収縮ライフを送っております(ちょっとウソ

国産で5000円以下、一生使えます :-}


と、アフィリ貼ってみました。BGMは、THE BEATLESの "Paperback Writer"です。

2016年1月11日月曜日

ESP-WROOM-02でSPI接続のADCから取得したデータをUDPで送出


緑のマイク基板と赤いESP基板の間にあるのがMCP3002。赤い基板上で右側のLEDがうすぼんやりと点灯しているのは、SPIのDIN信号。

50kspsでサンプリングしたデータをUDPに激しく送信中。

■SPI接続のADC■

ADCもいろいろありますが、数十Ksps以上のシリアルはSPI接続のものが多いです。

今回、MCP3002という精度10bit、入力2chのADCを使います。これは5v電源なら200kspsまで動き(2.7vだと75ksps)、そのわりに秋月で180円という大変おトクなチップです。マイクをつなげばお手頃な音声入力などが可能です。

SPIはあまり使ったことがないので、こちらの記事(ESP8266 (ESP-WROOM-02) でセンサーを扱う)を参考にさせていただきました。ありがとうございます。

SPIドライバはこちらからダウンロードします(MetalPhreak/ESP8266_SPI_Driver)。通常のArduinoライブラリとは形式が異なるので、以下の作業でArduino IDEが認識できるようにします(もっといい方法をご存知の方、ご教示いただければ幸いです)。

  • ダウンロードしたらzipを解凍する
  • フォルダの名前をESP8266_SPI_Driverに変更する
  • フォルダの中のspi_register.h, spi.h, spi.cをESP_SPI_Driver直下に移動
  • Arduinoのlibrariesフォルダ直下へ移動
  • Arduino IDEを再起動

動作させてみたところ、1回のデータ取得に要する時間は11-12μ秒程度でした(関数を100回呼び出して計測)。

■ESP-WROOM-02からUDP■

1回のデータ送信に要する時間は60-70m秒程度でした(関数呼び出し)。

■ESP用のソース■

なるべく一定の速度でサンプリングしつつ、UDPで送信しなければならないのですが、両者のスピードが違いすぎるので何らかのマルチタスクっぽい仕組みが必要です。Arduinoではこういう場合Tickerを使うのですが、Tickerは最小単位がミリ秒なので、1秒間に1000データしか取れません。それではせっかく高速のSPIを使う意味がありません。

幸いなことにESPにはos_timer_arm_usというμ秒単位のインターバルが用意されていますので、これを使います。なお、これを使う場合には、setupの始めの方でsystem_timer_reinit()を使って初期化し直す必要がありますので、ご注意を。これを使わないとミリ秒でしか動いてくれません。

ということで、おおまかな構成としては

  • 初期化ルーチンでWifi接続してから、20μ秒ごとのインターバルを開始
  • インターバルで呼ばれたらSPIからADCのデータを取り込みバッファにしまう
  • バッファが一杯になったらフラグを立てて別のバッファに切り替える
  • loop()ではバッファ一杯になったかどうかのフラグをチェックし、いっぱいになっていたらUDPでデータを送信します。

#define USE_US_TIMER 1
extern "C" {
#include <spi.h>
#include <spi_register.h>
#include "user_interface.h"
}
#include <ESP8266WiFi.h>
#include <WiFiUDP.h>
static WiFiUDP wifiUdp;
static const char *kRemoteIpadr = "192.168.0.70";
static const int kRmoteUdpPort = 5431;
#define MaxBuffer 256
#define ShouldSend0 0
#define ShouldSend1 1
#define ShouldNotSend 2
int shouldSend = ShouldNotSend;
int selector = 0;
unsigned short *buf[2];
static unsigned short buf1[MaxBuffer];
static unsigned short buf2[MaxBuffer];
int counter[2];
ETSTimer Timer;
int seq = 0;
int temp = 0;
void fetchFunction(void *temp) {
buf[selector][counter[selector]++] = check(0);
if (counter[selector] >= MaxBuffer) {
buf[selector][0] = counter[selector];
shouldSend = selector;
buf[selector][1] = seq++;
if (seq > 500) seq = 0;
selector = 1 - selector;
counter[selector] = 1; // start from 1. [0] is counter
}
}
void setup() {
Serial.begin(115200);
Serial.print("\n");
system_timer_reinit(); // to use os_timer_arm_us
//
// wifi setup
//
static const int kLocalPort = 7000;
WiFi.begin("ssid", "password");
while( WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print('.');
}
Serial.println("connected");
wifiUdp.begin(kLocalPort);
//
// buffer clean up
//
buf[0] = buf1;
buf[1] = buf2;
counter[0] = counter[1] = 1;
selector = 0;
//
// adc setup
//
spi_init(HSPI);
Serial.println(" end.");
// fetch every 20uSec
os_timer_setfn(&Timer, fetchFunction, NULL);
os_timer_arm_us(&Timer, 20, true);
}
void loop() {
if (shouldSend != ShouldNotSend) {
wifiUdp.beginPacket(kRemoteIpadr, kRmoteUdpPort);
char *conv = (char *)buf[shouldSend];
wifiUdp.write(conv, counter[shouldSend]*2);
wifiUdp.endPacket();
Serial.print("should send = ");
Serial.print(shouldSend);
Serial.print(", counter = ");
Serial.print(counter[shouldSend]);
Serial.print(", size = ");
Serial.println(sizeof(uint16));
shouldSend = ShouldNotSend;
}
}
uint32 check(int channel) {
// start bit (1 bit) : 1
// SGL/DIFF bit (1 bit) : SGL:1
// select the input channel (3 bit) : CH0:0, CH1:1
uint8 cmd = channel == 0 ? 0b1101 : 0b1111;
const uint32 COMMAND_LENGTH = 4;
const uint32 RESPONSE_LENGTH = 12;
uint32 retval = spi_transaction(HSPI, 0, 0, 0, 0, COMMAND_LENGTH, cmd, RESPONSE_LENGTH, 0);
retval = retval & 0x3FF; // mask to 10-bit value
return retval;
}


■検証用のJavaコード■

UDPでちゃんとデータを送り出せているのかを検証するために受信側のコードをJavaで作りました。UDPを受信して、連番と最小値/最大値などを表示するだけです。グラフ化したり音声を出力する根性がなかったので値だけです。

こちらの記事を参考にさせていただきました。ありがとうございます。
http://k-hiura.cocolog-nifty.com/blog/2011/07/java-6e01.html

// UDP受信プログラムより
// http://yuu7777.fc2web.com/javanet6/12.htm
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketAddress;
public class UDPRecTest{
//UDP受信用ソケット
DatagramSocket recSocket = new DatagramSocket(5431);
//相手先アドレス
SocketAddress sockAddress;
//
// main
//
public static void main(String [] args) throws Exception{
UDPRecTest test = new UDPRecTest();
// 受信ループ
while(true) test.receive();
}
//
// 受信クラス
//
public UDPRecTest() throws Exception {
}
//
// byteを符号なしの整数値に変換
//
public int btoi(byte inValue) {
return inValue & 0xFF;
}
//
// パケット受信
//
public void receive() throws Exception {
byte buf[] = new byte[2048];
DatagramPacket packet= new DatagramPacket(buf,buf.length);
recSocket.receive(packet);
sockAddress = packet.getSocketAddress();
int len = packet.getLength(); // 受信バイト数取得
// 先頭に入っているデータサイズを取得
int size = (btoi(buf[1]) << 8) + btoi(buf[0]);
// 2番目に入っているseq番号(0-511)を取得
int seq = (btoi(buf[3]) << 8) + btoi(buf[2]);
// データの最小値と最大値を検出
int low = 1024;
int high = 0;
for (int i = 2; i < size; i++) {
int value = (btoi(buf[i*2+1]) << 8) + btoi(buf[i*2]);
if (low > value) low = value;
if (high < value) high = value;
}
// 取得したデータのサマリを表示
System.out.println("seq="+String.valueOf(seq)
+ ",low="+String.valueOf(low)+",high="+String.valueOf(high)
+ ",size=" + String.valueOf(size) + ",len=" + String.valueOf(len) + ",from:"+ sockAddress.toString());
}
}
view raw UDPRecTest.java hosted with ❤ by GitHub

■ハマりどころ■

  • 自家製ESPモジュールからIO15が出てない。IO15のプルダウン抵抗近くのviaホールにハンダ付けして引っ張り出す。
  • SPIが動かない。これは当初参考にしていた記事の配線図が間違っていたため。私の2時間を返せ。ということで著者さんには間違ってますよ、とメールしておいた。まだ直ってないなぁ。
  • 接続直後にESPが暴走する。あれさっきまで動いてたのに…?? と思ったら、初歩的なC言語的間違い。30年ぶりぐらいにやらかした。
      char **buf; buf[0] = ptr1; buf[1] = ptr2; 
  • ESPはos_timer_arm_usってAPIでマイクロ秒単位のTickerを使えるはずなのに、ミリ秒単位でしか動いてくれない。改めてSDKマニュアルを読んだら、system_timer_reinit()で初期化しないとダメよと書いてあった。
  • Javaの符号なし整数にまたハマってしまった。いい加減覚えろよ>俺

2016年1月10日日曜日

Energiaでお風呂時計(第二回)


■追記(2016/01/27)■

ついに電池切れました。前回充電したのは12/22だったので、35日ほど動いたことになります。プログラムを改善したのはこの1/10のことだったので、かなり改造が効いている様子です。ついでに、明暗の制御を少し改良しました。

■続き■

前回の「Energiaでお風呂時計」、動くことは動くんですが・・・「無印の化粧品ボックス+ジップロック+ブレッドボード」という見た目があまりにも悲惨なので「100均の瓶+ユニバーサルボード」ですっきり仕上げてみました。

最初、透明な蓋の弁当箱を探したんですが、吉祥寺の100均では見つからず。とりあえず「防水」「透明」「適当なサイズ」というところで食品保存用と思われるプラスチック+金属蓋の容器にしました。

蓋が金属だと影が出来てしまって動作が安定しないのではないか?という懸念はありましたが、いまいち透明度の低いジップロックのロスが想像以上に大きかったようで、「明暗判別」の値を2倍近くにしないと消灯してくれない状態でした。

ユニバーサル基板は、部品沼から出てきた20年物のSUNHAYATO製、150円の値札が貼ってありました。さすがにフラックスが劣化しているのか、ハンダの乗りが悪い悪い。

ボードのレイアウトは下からLED、センサー、CPUです。LEDが割と重いので、安定させるために&蓋の影にならず見やすい位置、センサーは蓋の影にならない場所に上下逆さまに、残ったところがCPU。

明暗センサーはブレイクアウトボードにヘッダピンをがっつりとハンダ付けしてしまったのでIC用の背の低いソケットには使えず、かといってヘッダソケットを使うとLEDよりも出っ張ってしまうのでアウト。ということでユニバーサルボードに直接ハンダ付けしました。100円を越える部品を直接ハンダ付けするのは心臓がドキドキしますw

セグメントの配線には電流制限用の抵抗100Ω☓7個入れました。1kΩでも1.5mAぐらい流れるのでスタティックなら十分明るいのですが、ダイナミック点灯だとさすがに暗くてダメ。試行錯誤の結果と手持ち抵抗により100Ωになりました。

■で、起動しない■

で、組み上がったところでGND/VCCがショートしていないか確認してから電池つないでも起動してくれません。

電源電圧もI2Cのプルアップも正常。何せI2C以外はみんなDigital Outputという単純な回路なので、疑うところは「CPUがちゃんと動いていない」ということぐらい。ごく簡単なLチカプログラムを動かしてみても信号が出てきません。

となるとリセットが原因です。

ブレッドボード上では何も配線しなくても動いていたのですが・・・2553で前にもあったなー。というわけで、47kΩでプルアップしたところ、動作するようになりました。推奨回路ではコンデンサでGNDとつなぐようにも書いてあるのですが(電源を入れた時にしばらくGND近くを保って、リセットさせるため)、まぁ動いているから省略w

■起動したけど■



…なんだこの表示はorz

よくみると、

  • 中央が点灯すべきところで下が点灯している
  • 下が点灯すべきところでドット点灯している

ということで、3本の配線が間違ってました。せっかくワイヤの長さを揃えてきれいに配線したのに台無しですorz


なお、ユニバーサル基板と電池ボックス(単4☓3本)の固定には輪ゴムを利用しました。

■プログラム■

前のバージョンとほとんど変わっていません。変えたのは、前記の通り明暗を判定する値を大きくしたことと、明暗によってLEDの明るさを決める関数の演算式、それに秒表示の時のゼロサプラスをやめたこと、の3点です。

■ユニバーサル基板■

昔はブレッドボードがなかったので、抵抗の値を試行錯誤で決めるのも面倒でした。

今回、アホな配線ミスはありましたが、ブレッドボードで決まった通りに配線するだけなので、昔と比べれば大分ラクになりました。

でも、私は基本的にソフト屋なので、何の痛みも伴わないundoに慣れきっています。ユニバーサル基板で配線しながら考えていたのは「基板設計する方がラクだな」ということでした。配線ミスの修正を含めて、ユニバーサル基板の作成にかかったのは3時間ほどでした。このぐらいの基板設計なら2時間ぐらいで終わりますし、届いた基板へのハンダ付けも1時間かかりません。

昔は自分で1日かけてエッチングするか、数万円だして業者に頼むしかなかったんですが、今はカード決済で送料込み2400円ですからねぇ・・・。

基板を起こせば2枚目以降はあっという間に作れますし。まぁ、大抵の場合は需要ないのでそのまま未使用基板が増えていくだけなんですけどもw

2016年1月8日金曜日

赤外線リモコンとLチカがあれば聴覚障害でもOK

私みたいに耳が悪いと何が不便って後ろで何が起こっているかわからないんですよね。「声をかける」というのが他人の注意を喚起する上で如何に有効なものであることか(詠嘆)。10mぐらい離れた人同士が会話しているのって、ほんと魔法みたいに見えます。

で、以前、赤外線リモコンのボタンを押すとLEDが点滅して誰かが呼んでいることを知らせてくれるデバイスを作りました。結構便利でした。

でもね、所詮はLED、仕事に夢中になっていると気づかないことも。

ということで、作りました。少し大きなリモコン付きのLチカを。


ビデオでは最大1wぐらいにおさえていますが、フル点灯30wの状態を直接見ると目が痛いです。

■材料■

プラットフォームはmbed、LPC1114FN28とLEDドライバと電源、それに赤外線受光器です。

以前、「普通の基板に表面実装パワーLEDを並べて何ワットぐらい点灯できるか?」を調べようとElecrowで起こした基板を引っ張りだしてきました。うーん、まだホームリフロー試行錯誤中だったので表面実装のハンダが汚いorz せっかく基板を作るのだからとファン駆動回路やPIR接続ピンなどインタフェースがてんこ盛りになっており、その1つとして「赤外線受光器インタフェース」も付いてます(実は基板見るまですっかり忘れてた)。よくある5v電源3ピンの赤外線受光器を挿すだけで使えます。なお、私が買ったものは5v電源を与えてもVoutが3vまでしか上がらないものだったので直結していますが製品や電源電圧によって異なります。買ってきたら出力電圧を確かめてもし3.3vを越えるようならば電源を3.3vにするか出力を抵抗で分圧してください。



手前の黒い直方体はDC/DCです。24vから3.3vまで落とすとさすがに通常の三端子レギュレータはかなり熱くなります。

LEDドライバはCL6807を3セット、電流検出用のシャント抵抗は0.1ΩなのでLED 7個ずつの各ストリングには1Aまで流せます。電源24vではVfの7倍にはちょっと足りないのですが、そこはマージン見越して動かしてます。なお、CL6807は秋月の「1000mA 可変定電流パワーLEDドライバーキット」にも使われています。このキット、我が家の洗面台の自動点灯LEDにも使っていますが、安定していて使いやすいです。ただし、マイコンから制御するためには多少の改造が必要です。CL6807はバラでも売ってますし、プリント基板を起こさなくてもSOT89-5用基板とSBD、シャント抵抗(0.1 - 1.0Ω)、4.7μFの積層セラミックコンデンサ、ユニバーサル基板があれば自作も可能です。なおメーカーの応用回路例には書いてないのですが、LEDへの出力の両端にパスコン(1μF)をつけると点灯が安定します。

なお基板の裏には放熱器を高品質グリースで貼り付けてありますが、これぞ焼け石に水。フルパワーにすると室温20度でも基板温度は50度を越えていきます。我ながら何をしたかったのかわからない基板です(笑)。

■ソフト■

AQ0802、IR Receiverなどのライブラリを使って、組み立てました。ループは1秒間に10回まわっていて、Lチカの基本周期となっています。リモコンの信号を受けると、「あと何回点滅させるか」という変数に値を代入します。その変数が偶数なら点灯、奇数なら消灯、という決まりにすることで、0.1秒ごとにペカペカ点滅します。これだと複数のLEDを逆相や追いかけるように点滅させるのがラクです。
// Bright board
// ver 0.0 ボードのテスト用。ボリューム輝度調整、温度による出力制限など
//
// ver 0.1 2016-01-08 IR Receiverを追加してリモコン通知機能として実装
//
#include "mbed.h"
#include "AQM0802.h"
#include "ReceiverIR.h"
// PWM Outputs
PwmOut white(dp18);
PwmOut warm(dp1);
PwmOut yobi(dp24);
PwmOut fan(dp2);
// LCD
I2C i2c(dp5, dp27);
AQM0802 lcd(i2c);
// IR Receiver
ReceiverIR irReceiver(dp4);
// Analog input
AnalogIn vol(dp9);
AnalogIn sensor(dp13);
//
// temperature calc
//
const float ReferenceVoltage = 3.3;
float readTmpSensor() {
return (sensor*ReferenceVoltage - 1.951) * -121.95121951219512 - 30.0;
}
//
// main
//
int main() {
uint8_t buf[32]; // string convesion buffer
uint8_t hex[20]; // hex convertion buffer
white.period_ms(1.0); // set all pwm freq. 1khz
warm.period_ms(1.0);
yobi.period_ms(1.0);
fan.period_ms(1.0);
white = 1.0; // turn off all pwm ports
warm = 1.0;
yobi = 1.0;
fan = 1.0;
RemoteIR::Format format; // received format identity
int bitcount; // number of bits
lcd.cls();
int countWhite = 0; // led blinking flag / counter
int countWarm = 0;
while(1) {
if (irReceiver.getState() == ReceiverIR::Received) { // check ir receiver
// got signal
bitcount = irReceiver.getData(&format, buf, sizeof(buf) * 8);
// convert to hex string
int len = bitcount > 8 ? 8 : bitcount;
int p = 0;
hex[0] = 0;
for (int i = 0; i < len; i++) {
char temp[4];
sprintf(temp, "%02x", buf[i]);
hex[p++] = temp[0];
hex[p++] = temp[1];
}
hex[p] = 0; // end of c string
hex[8] = 0; // 念のため
lcd.locate(0,1);
// turn on if defined signal
hex[8] = 0;
if (strcmp((char *)hex, "10eff807") == 0) {
countWarm = 8; // button 'A'
} else if (strcmp((char *)hex, "10ef7887") == 0) {
countWhite = 16; // button 'B'
} else if (strcmp((char *)hex, "10ef58a7") == 0) {
countWhite = 19; // button 'C'
countWarm = 20;
} else {
lcd.print((char *)hex);
}
}
//
// volume
//
float a = vol;
int i = a * 1000;
char buf[32];
// display volume value
// lcd.locate(0, 0);
// sprintf(buf, "vol%5d", i);
// lcd.print(buf);
//
// temperature sensor
//
float tmp = readTmpSensor();
i = tmp;
lcd.locate(0, 1);
sprintf(buf, "tmp%5d", i);
lcd.print(buf);
//
// fan's control : turn of if temperature over 30.0 degree
//
fan = (tmp >= 30.0) ? 1.0 : 0;
//
// if temperature is over 35.0, limit the output power
//
if (tmp >= 35.0) {
float hosei = (40.0 - tmp) / 5.0;
if (hosei > 1.0) hosei = 1.0;
if (hosei < 0) hosei = 0.0;
a = a * hosei;
}
// reverse pwm duty
a = 1.0 - a;
//
// check led
// if counter is even, turn on. and decrement the counter for next loop
if (countWhite > 0) {
white = (countWhite % 2) == 0 ? a : 1.0;
countWhite --;
}
if (countWarm > 0) {
warm = (countWarm % 2) == 0 ? a : 1.0;
countWarm --;
}
// loop period is 0.1 sec
wait_ms(100);
}
}

2016年1月1日金曜日

Energiaでお風呂時計(第一回)

完成後、CPUもブレッドボード上へ移動

まぁ、またデジタル時計なんですけど。

使ったCPUはMSP430G2553ですが、開発環境はArduino互換なので、RTCを用意すれば簡単に移植することができます。ただ、nanoを使ったとしても消費電流を押さえるのはシンドいと思います。

■仕様■

  • 表示は4桁7セグメント赤色LED
  • お風呂なので電源は電池
  • 電池は1ヶ月ぐらいもたせたい
  • 時刻と入浴時間を交互に表示

■実装■

省電力で4桁の7セグを動かせる・・・ということで、CPUにはMSP430G2553を選びます。RTCは使わず、G2553に32768hzのXtalを追加して1秒ごとに割り込みを発生させて時計代わりにします。

さすがにLEDを常時点灯していては電池がもたないので、入浴時のみ表示するようにします。入浴中であることを検出するには、
  • 倒立スイッチ、風呂に入ったら時計を立てると点灯→一番簡単
  • PIRセンサーで人の動きを検出→簡単だけどお風呂のお湯で誤動作するかも?
  • 照度センサで明かりを付けたら動作開始
  • ドップラーレーダーで(ry
などいろいろ考えたのですが、ここはAPDS9930という照度・近接センサーを使います。ebayで「gesture sensor」って書いてあったのでAPDS9960みたいなことができるのかと思って買ったら単なる照度・近接センサーだった、というオチ。9960より劇的に安かったんですが、典型的な安物買いの銭失いでした。でも、寝かせて置くのももったいないので、使います。それに携帯電話用のデバイスなので、CDSなどで明るさを検知するよりも電力を消費しません(アナログの達人ならできるのかもしれないけど、動作時の消費電流250μA/待機時90μA,Sleep時2.2μAなんて回路を作るのは私にゃ無理)。

配線にはブレッドボードを使い、7セグLED電池ボックスは秋月です。

大きな声ではいえませんが、電流制限抵抗などは一切入れてません。Vf=1.8vの赤色LEDを使う場合には3.3v - 1.8v = 1.5v、5mAも流せば十分なので1.5 / 0.005 = 300Ωぐらいの抵抗をG2553とLEDのセグメント側の間に入れておくのが正しい使い方です。正しくない使い方をしているので「1」を表示している時と「8」を表示している時で明るさが異なってしまう(各桁を接続している端子に7セグメント分の電流が集中するので「8」のときは容量が足りなくなってしまう)のですが、まぁ自分用なので壊れなければOK。そのうちボード起こしたら抵抗入れるから、それまでがんばれG2553。

G2553への書き込みにはTIのLauchPadを使います。

ソフトはArduino互換のEnergia IDEを使って書きます。くれぐれもバージョン0101E0017と0101E0016は使わないようにお気をつけてください。

以下配線表。各セグメント7本と桁4本、電源、I2Cだけです。時計合わせはそのうち考えます。hourとminuteの間にコロンが点灯する表示器が欲しい(こないだaitendoで買ったら全品不良で点灯しなかった)。


MSP430G2553相手ピン備考
P1_47セグLED 11Seg A
P1_57Seg B
P2_04Seg C
P2_52Seg D
P2_21Seg E
P2_310Seg F
P2_45Seg G
P0_012Dig 1
P0_19Dig 2
P1_28Dig 3
P1_36Dig 4
P1_7APDS9930SDA
P1_6SCL
P2_6XTAL
P2_7XTAL


この他、電池の赤をブレッドボードの赤、黒をブレッドボードの青、G2553のVccとAPDS9930のVCCをブレッドボードの赤、G2553のGndとAPDS9930のGNDをブレッドボードの青、にそれぞれ接続します。


最初のバージョンは19日間動作しました(浴室使用時間は1日平均30分ぐらい)。LED駆動のデューティー比をかなり絞り込んだ現行バージョンは現在12日目ですが、さて。


■ハマり■

他のところに書いたネタもありますが:
  • 別のところ(Energia最新版でI2Cが動かない)に書いたけど最新のEnergiaを使ったらI2Cが動かなくなってしまった。2つ前のビルドをダウンロードしたら何事もなかったかのように動いた。うがあ。
  • 作業中にミスで電源逆接とG2553逆差しで2個トバしてしまった。
  • シリアルポートをGPIOでも使うとSerial.printなどを使った瞬間にリセットがかかる。まぁ当たり前なんですが……ライブラリの中のSerial.printで発症したために「ライブラリを呼ぶとフリーズする」という症状になり、ライブラリの問題か?と誤認してしまったために発見が遅れました。いやはや。
  • 明暗のスレッショルド設定にちょっと苦労しました。最終的にはセンサーからの値をLEDに表示させるプログラムを書いていろんんな向きで値を測定して閾値を決めました。やっぱ現場に行かんとダメやで。
  • 風呂場の明かりをつけているのに時計が消灯してしまうことがあって当初は上記閾値の問題かと思ったのですが、どうやらセンサーがLED電球の高速点滅の「滅」を拾ってしまうようで照明の明るさ/閾値設定とは関係なく発生します。なので、平均値を取るようにしました。ただ、センサーの値は1秒に1回サンプリングしており点灯時の明るさで500-700、消灯時の閾値が60という設定なので、LED電球を消灯してもすぐには時計が消えません。
  • ダイナミック点灯の合間にI2Cでセンサーを取りに行くと一瞬時計表示がチラついてしまいます。なので、時刻と入力時間を切り替えるときに読みに行くように変更しました。RTOS欲しい。
  • 照度に合わせてLEDの明るさを変えていますが、違いがわかりません。手で影を作ると少し暗くなるのでソレと判断できるのですが、あまり暗くすると消灯モードになっちゃいますし。

■ソース■

基本的には以前書いたデジタル時計の使い回しです。ArduinoでLEDのダイナミック点灯させるとだいたいこんな処理になっちゃいます。あと、APDS9930のライブラリはArduino用を使いました(APDS9930 Ambient Light and Proximity sensor)。

なんということもないダイナミック点灯プログラムなのですが、CPUのクロックを点灯中には8Mhz、消灯時には1Mhzに切り替えています。切り替えることでI2Cなどに影響がでないか心配だったのですが、問題ありません。1MhzだとG2553の消費電流は230μAで9930は220μA(スリープはさせていないが通信はごく低頻度なので消費電流はもっと低いはず)なので、まったくLEDを点灯させなければ2000時間程度は動くはずですが、点灯しない時計は意味がないので困ったものです。
#include <sRTC.h>
#include <legacymsp430.h>
#include <APDS9930.h>
// LEDのポート
const int segA = P1_4;
const int segB = P1_5;
const int segC = P2_0;
const int segD = P2_5;
const int segE = P2_2;
const int segF = P2_3;
const int segG = P2_4;
//int segCol = P2_6;
int digits[] = {
P1_0, P1_1, P1_2, P1_3}; // for serial
#define ALL_OFF 10
const uint8_t patA[] = {
HIGH, LOW, HIGH, HIGH, LOW, HIGH, HIGH, HIGH, HIGH, HIGH, LOW};
const uint8_t patB[] = {
HIGH, HIGH, HIGH, HIGH, HIGH, LOW, LOW, HIGH, HIGH, HIGH, LOW};
const uint8_t patC[] = {
HIGH, HIGH, LOW, HIGH, HIGH, HIGH, HIGH, HIGH, HIGH, HIGH, LOW};
const uint8_t patD[] = {
HIGH, LOW, HIGH, HIGH, LOW, HIGH, HIGH, LOW, HIGH, HIGH, LOW};
const uint8_t patE[] = {
HIGH, LOW, HIGH, LOW, LOW, LOW, HIGH, LOW, HIGH, LOW, LOW};
const uint8_t patF[] = {
HIGH, LOW, LOW, LOW, HIGH, HIGH, HIGH, LOW, HIGH, HIGH, LOW};
const uint8_t patG[] = {
LOW, LOW, HIGH, HIGH, HIGH, HIGH, HIGH, LOW, HIGH, HIGH, LOW};
const uint8_t sevenPort[] = {
segA, segB, segC, segD, segE, segF, segG};
const uint8_t *sevenSeg[] = {
patA, patB, patC, patD, patE, patF, patG};
RealTimeClock RTC;
// APDS9930
APDS9930 apds = APDS9930();
uint16_t ch0 = 0;
uint16_t ch1 = 1;
// 最後に表示を更新した時刻(秒)
unsigned long prevSec = 99;
// sprintf用バッファ
char spfBuf[20], buf[20];
unsigned long started;
void setup()
{
// led
for (int i = 0; i < 7; i++) {
pinMode(sevenPort[i], OUTPUT);
digitalWrite(sevenPort[i], LOW);
}
for (int i = 0; i < 4; i++) {
pinMode(digits[i], OUTPUT);
digitalWrite(digits[i], HIGH);
}
// Initialize APDS-9930 (configure I2C and initial values)
if ( apds.init() ) {
// Serial.println(F("APDS-9930 initialization complete"));
}
else {
// Serial.println(F("Something went wrong during APDS-9930 init!"));
}
// Start running the APDS-9930 light sensor (no interrupts)
if ( apds.enableLightSensor(false) ) {
// Serial.println(F("Light sensor is now running"));
}
else {
// Serial.println(F("Something went wrong during light sensor init!"));
}
for (int i = 0; i < 5; i++) {
digitalWrite(P1_0, HIGH);
delay(100);
digitalWrite(P1_0, LOW);
delay(100);
}
RTC.begin();
RTC.RTC_hr = 10;
RTC.RTC_min = 7;
RTC.RTC_sec = 0;
setDigit(0);
}
void setDigit(int d) {
for (int i = 0; i < 4; i++) {
if (d == i)
digitalWrite(digits[i], LOW);
else
digitalWrite(digits[i], HIGH);
}
}
int digit = 0;
int loopCount = 999;
#define MODE_DARK 1
#define MODE_BRIGHT 2
int mode = MODE_DARK;
int isTime = 0;
#define THRESHOLD 30
static int duration = 0; // 入浴時間
static int light = 100; // 直近の明るさ。だいたい100-1000、明かりを消した風呂場で60以下
int lightToTonTime() {
int t = light / 100+5;
if (t > 10) t = 10;
if (t < 5) t = 5;
return t;
}
void updateLight() {
float f = 0;
apds.readAmbientLightLux(f);
int current = f;
light = (light + current) / 2;
}
void loop()
{
// turn off
setDigit(-1);
delayMicroseconds(100-lightToTonTime());
// 点灯中にセンサーを読みに行くとちらつくので頻度を下げている
int sec = RTC.RTC_sec;
if (sec != prevSec) {
if (mode == MODE_DARK) { // mode is Dark
updateLight();
if (light > THRESHOLD) {
mode = MODE_BRIGHT;
BCSCTL1 = CALBC1_8MHZ;
DCOCTL = CALDCO_8MHZ;
// 入浴時間計測開始
duration = 0;
isTime = 0;
}
}
else { // mode is Bright
duration++;
if ((duration % 2) == 0) {
isTime = (isTime == 0);
if (isTime) {
updateLight();
if (light <= THRESHOLD) {
mode = MODE_DARK;
BCSCTL1 = CALBC1_1MHZ;
DCOCTL = CALDCO_1MHZ;
duration = 0;
}
}
}
if (isTime) { // 時刻表示
itoa(10000 + RTC.RTC_hr*100+RTC.RTC_min, buf, 10);
strcpy(spfBuf, buf+1);
}
else { // 入浴時間
int m = duration / 60;
int s = duration % 60;
itoa(10000+m*100+s, buf,10);
if (buf[1] == '0') buf[1] = ' ';
if (buf[3] == '0') buf[3] = ' ';
strcpy(spfBuf, buf+1);
}
}
prevSec = sec;
}
// 表示処理
// 対象となる桁の値を0-9か空白に変換しセグメントビットに出力
int num = spfBuf[digit];
if (mode == MODE_DARK) {
out(ALL_OFF, LOW);
setDigit(-1);
}
else {
num = (num >= '0' && num <= '9') ? num - '0' : ALL_OFF;
out(num, LOW);
// 桁ビットを出力(点灯=high)
setDigit(digit);
}
// 少し維持
delayMicroseconds(lightToTonTime()); // lightはだいたい1000-100
//次の桁へ
digit++;
if (digit >= 4) digit = 0;
}
void out(int num, int DP) {
for (int i = 0; i < 7; i++) {
digitalWrite(sevenPort[i], sevenSeg[i][num]);
}
}
interrupt(TIMER1_A0_VECTOR) Tic_Tac(void) {
RTC++; // Update secondes
};