Arduinoでの自作ガイガーカウンター解説-#5プログラム
構想が固まったらプログラムをデザインします。
ガイガーカウンターにまつわるところはプログラミングする前によく考えます。GMTの仕様の指定方法ですが、1回目の記事に書いたキャリブレーションのポイントP,Qを次のように設定します。
まずQは、最終的にはできるだけ正確な他のガンマ線測定機器で測った環境中で本器が数えるCPMの平均値を入れるのですが、最初はわかりませんからPとの比例などを使い適当な値を入れます。
PはGM管の仕様から設定します。ここでは3回目の記事に書いた通り、製品仕様のCo60からCs137に設定しなおしています。そのような高い放射線源の準備は困難のため、製品の仕様を信じて使うわけです。CPMのカウントは正確である必要がありますが、μSv/hでの表示自体がもともと精度の高いものではなく、有効数字1桁程度と思います。
とはいっても、正確な機器で実際にキャリブレーションするQの値に近い領域では正確といえるわけですし、変化や差を見る場合には分解能を保つことが重要だと思います。本器では0.001μS/hの単位まで表示します。また、秒単位の計測ですからカウント指定値は1桁の数値は避けて2桁以上の値にします。とはいえ続けてカウントするわけなので、前後の値に細かい差異は含まれ、結果としては正しくとらえられることになります。
// GMT specifications
#define Xp 11280 // CPMx10: point-P for SBM-20
#define Yp 8770 // nano-Sv/h at point-P
#define Xq 156 // CPMx10 measured point-Q
#define Yq 49 // nano-SV/h at point-Q
#define sF 50 // smoothing factor (%)
単なる#defineなのでメモリーなどを消耗しませんが、小数点を含むものは定義できない(と思います)ので、上記のように小数点1位まで欲しいCPMの値は10倍、μSv/hの値はnSv/h(ナノシーベルト、μSvの千倍)の単位にして定義することにします。
また平滑化指数は上記のように%で定義しました。
このようにすればGM管が変ってもキャリブレーションポイントの値(Q点)が変っても、上だけを直せば済むわけですから。また、平滑化指数の変更も自在です。
そしてsetupの中で必要な係数を次のように計算することにします。
// Set up CPM-to-nSv/h factors
// 1) between point P and Q, Y=A*X-B
A = (float)(Yp - Yq) / (Xp - Xq);
B = (float)(Xp * A - Yp);
// 2) between point Q and O(0,0), Y=Cq*X
Cq = (float)Yq / Xq;
ピンアサインメントなども同じことですね。intなどの定数で定義することもできますが、メモリーを無駄にするので#defineがよいですね。
// Digital Pins assignment
#define Det 2 // receive detected signal(L=on) from slave
#define Mdb 4 // Mode button (L=on)
#define CLK 5 // TM1637 CLK signal
#define DIO 6 // TM1637 DIO signal
#define Tm 7 // Time button (L=on)
#define St 8 // Set button (L=on)
単純なプログラムはそのままコーディングしてしまいますが、少し込み入っている場合はそうはいきません。フローチャートなどを紙に書くのもよいですが、筆者はテキストエディターを使って次のような構造を書きつけておくのが普通です。
プログラムRadMonitor-SBM20-BB-Cnt30の仕様
(c)2018 Akira Tominaga, All rights reserved.
include TM1637Display.h
include Wire.h
include SPI.h
include SD.h
GMT定数の定義
カウント回数の指定
他の変数の定義
ピンのアサインメント
Display表示固定値のセット
RTC関連の設定
Setup
Pinの設定
Display設定
ボタンを押しながら電源オンの場合
RTCの設定をする rtcSet()
SDカード設定(filename=GMnnn)
SDへデータヘッダーの書き込み
CPM to μS/h 変換のための係数(A、B、とC)を計算
スタート時刻の取得
Loop
1)GMT信号が検出された場合
カウントを1増やす
カウントが指定数に達した場合
時刻を取得して経過時間を計算
CPMを計算
CPM(X)からナノシーベルト/h(Y)へ変換
P点とQ点の間またはそれより大きい場合
Y=AX-B
Q点と原点の間の場合
Y=CX
SDへデータを書き込む
カウントが指定数に達していない場合
何もせず80μSを消耗する(ダブルカウントを防ぐ)
2)ボタンが押された場合
SDカードをCloseする
DisplayにOffと表示する
処理を停止する
ユーザー定義関数
read Real-Time-Clock rRTC()
convert RTC-format to Integers cR2I()
convert Integers to characters ItoC()
set RTC time rtcSet()
(電源オン時にボタンが押されていると呼ばれる)
ボタンが離されるのを待つ
RTCを読む rRTC()
値をIntegerに変換するcR2I()
最初のモードを年(Y)に設定する
Setボタンが押されるまでは次を繰り返す
モードボタンが押されるまで次を行う
ディスプレイにモード文字=と読んだ値を表示する
Timeボタンが押されている間は
その値を最小最大値間で1ずつ変化させる
変化させた値を文字にする ItoC()
その文字をディスプレイ3-4桁に表示
モードボタンが押されたら
Modeボタンが離されるのを待つ
モードを次の値にする
Setボタンが押されたら繰り返しを抜ける
Setボタンが離されるのを待つ
Integerの数値をRTCフォーマットに変換する
RTCに書き込む
以上
ExcelでもTeraPadなどでもよいのですが、構造を示す大まかなシュードコード(pseudo-code)です。正式にはwhileやif、switchなどはそのまま記述しますが、その記述がなくてもわかりますから省略しています(不安な方はきちんと記述されることをお勧めします)。
正しく動くかどうか、考慮漏れがないかどうかなどを、この段階で何度か確認してからコーディングをすれば、大きなプログラムでもかなり早く完成できます。むしろプログラムが小さくできますし、無駄な時間が防げます。いきなり書き始めてぐちゃぐちゃになると、一見早そうですが完成はかえって大幅に遅れますね。皆様ご経験のとおりです。
設計書を見ながらコーディングをすると、大きな間違いが防げます。こうするとまわりくどいようですが、結果に至るのはとても早いです。小さいプログラムは別として。次がコーディング内容です。#include の<>の中身がすっぽりぬけて表示されますが、そこは次のように読んでください。
#include <TM1637Display.h>
#include <Wire.h>
#include <SPI.h>
#include <SD.h>
/* Radiation Monitor
for SBM-20 on Bread board
(c) 2018 Akira Tominaga, All rights reserved.
version 01-03s
Function
1) Count Detected signals
2) At threshold,calculate CPM & s-CPM(smoothed CPM)
3) Convert s-CPM to micro Severt/h and display it
4) Record date, time, CPM, s-CPM and μSv/h to SD
5) Provide Real Time Clock setting operations
Remarks
A) Avoid double detection of Det signal for 80μS
*/
#include
#include
#include
#include
// GMT specifications
#define Xp 11280 // CPMx10: point-P for SBM-20
#define Yp 8770 // nano-Sv/h at point-P
#define Xq 156 // CPMx10 measured point-Q
#define Yq 49 // nano-SV/h at point-Q
#define sF 50 // smoothing factor (%)
// for radiation counting
#define CntV 30 // Signal count for CPM calculation
int Cpm = 0; // Counted number until threshold CntV
int CPMx10; // CPM x 10 value
int sCPMx10 = Xq; // smoothed CPMx10 initial value
int Nsv = 0; // Nano Severt value
char strCPMx10[7]; // editing area for CPM characters
char strsCPMx10[7]; // editing area for smoothed CPM characters
char strNsv[6]; // editing area for Nsv and uSv characters
float A; // calculation factor to convert CPM to nSv
float B; // calculation constat to convert CPM to nSv
float Cq; // calculation factor in weak Nsv zone
// for micro SD card
File gmLog;
#define Cs 10 // Chip-select for D10 pin
char fname[10] = "gm000.txt"; // file name to increase number
int fnum = 0; // initial file name number
char fnumc[4]; // editing area for file name
// Digital Pins assignment
#define Det 2 // receive detected signal(L=on) from slave
#define Mdb 4 // Mode button (L=on)
#define CLK 5 // TM1637 CLK signal
#define DIO 6 // TM1637 DIO signal
#define Tm 7 // Time button (L=on)
#define St 8 // Set button (L=on)
TM1637Display display(CLK, DIO);
uint8_t Data[] = { 0x3f, 0x3f, 0x3f, 0x3f };
int k;
const uint8_t SEG_OFF[] = {
0b00111111, // O
0b01110001, // F
0b01110001, // F
0b00000000
};
const uint8_t SEG_SdEr[] = {
0b01101101, // S
0b01011110, // d
0b01111001, // E
0b01010000 // r
};
// for Real Time Clock
char strYMDHMS[17]; // editing area for calendar and clock
byte vR[8]; // values in RTC registers
int vI[8] = { 0, 0, 0, 0, 0, 0, 0, 0}; // integers for RTC data
#define RTC_addr 0x68
#define mdI 0 // Initial mode
#define mds 1 // ss
#define mdm 2 // mm
#define mdh 3 // hh
#define mdW 4 // WW=Day of Week
#define mdD 5 // DD
#define mdM 6 // MM
#define mdY 7 // YY
#define sS 0
#define mM 1
int svTime[2] = {0, 0};
void setup()
{
Serial.begin(9600);
Wire.begin();
pinMode(Det, INPUT_PULLUP);
pinMode(Mdb, INPUT_PULLUP);
pinMode(Tm, INPUT_PULLUP);
pinMode(St, INPUT_PULLUP);
display.setBrightness(8); // values are valid from 8 to 11
for (k = 0; k < 4; k++) {
Data[k] = 0x3f;
}
display.setSegments(Data);
// when mode button holded at power-on
if (digitalRead(Mdb) == LOW) {
rtcSet();
}
// check SD card
if (!SD.begin(Cs)) {
for (k = 0; k < 4; k++) {
Data[k] = SEG_SdEr[k]; // if error, show SdEr
}
display.setSegments(Data);
return;
}
// set file namde GMxxx, where xxx is number
while (1) {
sprintf(fnumc, "%03d", fnum);
fname[2] = fnumc[0];
fname[3] = fnumc[1];
fname[4] = fnumc[2];
if (!SD.exists(fname)) break;
fnum++;
}
Serial.println(fname);
gmLog = SD.open(fname, FILE_WRITE);
gmLog.println("YYMMDD,hh:mm:ss, CPM , s-CPM ,uSv/h");
Serial.println("YYMMDD,hh:mm:ss, CPM , s-CPM ,uSv/h");
// Set up CPM-to-nSv/h factors
// 1) between point P and Q, Y=A*X-B
A = (float)(Yp - Yq) / (Xp - Xq);
B = (float)(Xp * A - Yp);
// 2) between point Q and O(0,0), Y=Cq*X
Cq = (float)Yq / Xq;
//Serial.print("A=");
//Serial.print(A);
//Serial.print(" B=");
//Serial.print(B);
//Serial.print(" Cq=");
//Serial.println(Cq);
// Set svTime at start of loop
rRTC(); // read RTC device
cR2I(); // convert RTC-format to Integers
svTime[sS] = vI[mds];
svTime[mM] = vI[mdm];
}
void loop()
{
// 1) when GMT signal was detected
if (digitalRead(Det) == LOW) {
Cpm = Cpm + 1;
if (Cpm >= CntV) { // when reached count-value threshold
// get duration
rRTC(); // read RTC device
cR2I(); // convert RTC-format to Integers
int durS = vI[mds] - svTime[sS];
int durM = vI[mdm] - svTime[mM];
svTime[sS] = vI[mds]; // save new time
svTime[mM] = vI[mdm];
if (durS < 0) {
durS = durS + 60;
durM--;
}
if (durM < 0) {
durM = durM + 60;
}
durS = durS + 60 * durM;
float Cpmfx10 = (float)CntV * 600.0 / durS;
// Cpm smoothing to sCPMx10
sCPMx10 = (float)(sCPMx10 * sF/100.0 + Cpmfx10 * (100-sF)/100.0);
if (sCPMx10 >= Xq) {
Nsv = (float)(A * sCPMx10 - B );
if (Nsv < 0) {
Nsv = 0;
}
if (Nsv > 9999) {
Nsv = 9999;
}
} else {
Nsv = (float)(sCPMx10 * Cq);
}
CPMx10 = Cpmfx10; // save CPM
Cpm = 0; // clear current Cpm
// integers to characters
ItoC(); // Integers to Characters
// Set LED digits
int Su0 = Nsv / 1000;
Data[0] = display.encodeDigit(Su0);
int Su1 = (Nsv - Su0 * 1000) / 100;
Data[1] = display.encodeDigit(Su1);
int Su2 = (Nsv - Su0 * 1000 - Su1 * 100) / 10;
Data[2] = display.encodeDigit(Su2);
int Su3 = (Nsv - Su0 * 1000 - Su1 * 100 - Su2 * 10) % 10;
Data[3] = display.encodeDigit(Su3);
Data[0] = Data[0] + 0x80; // insert dot
display.setSegments(Data);
// Write SD data
String s = strYMDHMS;
s.concat(",");
strCPMx10[7] = strCPMx10[6]; // copy stopper
strCPMx10[6] = strCPMx10[5];
strCPMx10[5] = 0x2E; // insert period
s.concat(strCPMx10);
s.concat(",");
strsCPMx10[7] = strsCPMx10[6]; // copy stopper
strsCPMx10[6] = strsCPMx10[5];
strsCPMx10[5] = 0x2E; // insert period
s.concat(strsCPMx10);
s.concat(",");
for (k = 5; k >= 2; k--) {
strNsv[k] = strNsv[k - 1];
}
strNsv[1] = 0x2E; // insert period
s.concat(strNsv);
gmLog.println(s);
Serial.println(s);
}
// elese consume time only to avoid double detection
delayMicroseconds(80);
}
// 2) when Mode pushed to close SD card
if (digitalRead(Mdb) == LOW) {
gmLog.close(); // close the file
for (int k = 0; k < 4; k++) {
Data[k] = SEG_OFF[k];
}
display.setSegments(Data);
while (1) { // stop process
}
}
}
/* **********************************
User defined functions *
*********************************** */
// read Real-Time-Clock
void rRTC(void) {
Wire.beginTransmission(RTC_addr);
Wire.write(0x00);
Wire.endTransmission();
Wire.requestFrom(RTC_addr, 7);
while (Wire.available() < 7) {} // wait for data ready
for (int i = 1; i < 8; i++) {
vR[i] = Wire.read();
}
Wire.endTransmission();
}
// 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);
}
// convert Integer contents to characters
void ItoC(void) {
sprintf(strYMDHMS, "%02d%02d%02d,%02d:%02d:%02d", vI[mdY], vI[mdM], vI[mdD], vI[mdh], vI[mdm], vI[mds]);
sprintf(strCPMx10, "%06d", CPMx10);
sprintf(strsCPMx10, "%06d", sCPMx10);
sprintf(strNsv, "%04d", Nsv);
}
// set RTC when mode button pushed at power-on timing
void rtcSet(void) {
byte mC[8] = {0x00, 0x6d, 0x54, 0x76, 0x3e, 0x5e, 0x37, 0x6e};
// 7 seg modeChr 0, 's', 'm', 'h', 'W', 'D', 'M', 'Y'
char Su[3];
int maxvI[8] = { 0, 59, 59, 23, 7, 31, 12, 99};
int minvI[8] = {0, 0, 0, 0, 0, 1, 1, 0};
while (digitalRead(Mdb) == LOW) {}; // wait until Mdb released
int mD = mdY ; // initial mode=mdY
rRTC(); // read RTC device
cR2I(); // convert RTC-format to Integers
// loop waiting for Set button
while (digitalRead(St) == HIGH) {
Data[0] = mC[mD];
Data[1] = 0x48; // "="
sprintf(Su, "%02d", vI[mD]);
Data[2] = display.encodeDigit(Su[0]);
Data[3] = display.encodeDigit(Su[1]);
display.setSegments(Data);
// Mode button loop within Set button loop
while (digitalRead(Mdb) == HIGH) { // do until Mdb pushed
if (digitalRead(Tm) == LOW) {
vI[mD] = vI[mD] + 1; // increase value
if (vI[mD] == maxvI[mD] + 1) { // if exceeded max
vI[mD] = minvI[mD]; // then set minimum
}
sprintf(Su, "%02d", vI[mD]);
Data[2] = display.encodeDigit(Su[0]);
Data[3] = display.encodeDigit(Su[1]);
display.setSegments(Data);
delay(250);
// if Set on, go out of Time loop
if (digitalRead(St) == LOW) break;
} // repeat TM loop
// if Set on,go out of Mode loop
if (digitalRead(St) == LOW) break;
// repeat Mode button loop
}
// out of Mode loop within Set loop
while (digitalRead(Mdb) == LOW) {} // until end of pushing
if (digitalRead(St) == LOW) break; // if Set pushed
mD = mD - 1;
if (mD == mdI) {
mD = mdY;
}
// if (digitalRead(St) == LOW) break; // if Set pushed
} // Repeat Set loop
// out of Set loop
while (digitalRead(St) == LOW) {} // until end of pushing
// write RTC
vR[mds] = (vI[mds] / 10) * 16 + vI[mds] % 10;
vR[mdm] = (vI[mdm] / 10) * 16 + vI[mdm] % 10;
vR[mdh] = (vI[mdh] / 20) * 32 + ((vI[mdh] % 20) / 10) * 16 + vI[mdh] % 10;
vR[mdW] = vI[mdW];
vR[mdD] = (vI[mdD] / 10) * 16 + vI[mdD] % 10;
vR[mdM] = (vI[mdM] / 10) * 16 + vI[mdM] % 10;
vR[mdY] = (vI[mdY] / 10) * 16 + vI[mdY] % 10;
Wire.beginTransmission(RTC_addr);
Wire.write(0x00);
for (int i = 1; i < 9; i++) {
Wire.write(vR[i]);
}
Wire.endTransmission();
}
全部で 300行ほどに収まっていますが、RTC-Writerなどの機能も含んでいますから、かなり小さいほうかと思います。
SDカード、TM1637ディスプレイ、I2CなどのライブラリーをIncludeしていますが、RTCは簡単なので自分でコーディング。これによりスケッチはArduino-Unoの容量の60%ですみ、グローバル変数は66%(ただし実装用はSerial.printをはずしますから少し小さい)となり余裕があります。
テスト後は、ATmega328pに書き込んで使いやすいブレッドボードにします。とはいってもDIP形式のATmega328が搭載された普通のUnoに書き込んで、そのICを使えばよいだけのことで、わざわざ書き込み器などを備えるよりも簡単です。28pin DIP の無圧(zero pressure)ソケットをUnoに予めセットしておけばAT-mega328の取付・取外しがとても楽です。例えば次のようにします。
手に入る28ピン無圧ソケットは半田付け用ですから、そのままでは足が短かったりします。そういう場合はその下に他のソケットで下駄をはかせます。下の例はまず平ピンソケットをはかせ、さらにその下に足がしっかりした丸ピンソケットをはかせた例です。無圧ソケットの足は直接丸ピンへは入りません。丸ピンソケットは14pin用を2つ使う方がセットしやすいです。
このようにしてプログラムを入れたATmega328pを使えば、ブレッドボード機は 下の写真のようにすっきりします。ATmega328pに外付けが必要なのは16MHzクリスタルオシレータと22pFキャパシターx2、それに適当な電源用キャパシターです。
この形ですと周囲の部品へ設定通りの電圧が供給できるので、消費電流等の測定にはこちらが適切です。また、他の測定もこの形が楽ですね。何を作るにも同じですが、ブレッドボードで動かす間は、熱を出さない周辺機器はビニールチャック袋に収めておくのが、思わぬトラブル防止に役立ちます。むき出しでは他と接触しますので。
PCB作成から実装までを、次回説明したいと思います。
前の記事:
次回の記事:
(c)2018 Akira Tominaga, All rights reserved.