水耕栽培のEC管理に疲れていませんか?
- 現在の濃度が不明
液肥の蒸発もあるのでいったい実際の液肥の濃度はどうなんだろう。。 - 本当にその補給大丈夫?
とりあえず水が減っているからハイポニカと一緒に水をたそう。。 - 毎日の測定は大変
ECメーターでするか。。でも毎日はめんどくさくて続きそうにない。。
自分に最適なECメーターを作ってみるのはどうでしょうか?
自作しちゃえば自分の好きなように設計できます。もちろん定期的に計測したかったら自分で設計しちゃえば可能です。
とはいっても、意外と詳細に作り方を解説したサイトはありません。
このページで実際の回路とラズパイを使ったプログラムをご紹介します。
自作のECメーターを作製して一歩進んだ水耕栽培をはじめましょう(๑•̀ㅂ•́)و✧
ECメーターはなんで必要?それを踏まえた設計は?
Electronic Conductivityの略がECです。電気伝導度という意味になるので電気の通りやすさを表します。
ECメーターで簡単に測ることができますが、測るしかできないことが残念です(×_×;)
測定結果によって処理を変えたり、日々の記録を自動的に取れないというのは今の時代にあっていないですね。
これから自作しようと思いますが、どんな原理で測定できるのかをまず確認しましょう٩( ‘ω’ )و
電気の通りやすさで液肥の状態がわかる仕組み
水耕栽培で使う液体肥料には植物の育成に必要な成分はイオンという形で溶けています。
窒素だとNO3-、カリウムだとK+ といった風に、+/-など電荷を帯びた状態で溶けています。
このイオンの状態の成分を吸収することで植物は栄養を吸収しているんですね。
ということで溶けているイオンを測定できれば液肥の状態を推測できます。
肥料の濃度と抵抗値の関係は?
イオンは電荷を帯びているので、液肥に含まれれるイオンが多いほど電気が流れやすくなります。
イオンが少なければ電気が流れにくくなります。
- 肥料が十分ある: 電気をよく通す:抵抗値が低い
- 肥料が少ない:電気があまり通らない:抵抗値が高い
この仕組みを利用してEC濃度を測定します(๑•̀ㅂ•́)و✧
EC濃度を測る仕組み
電気伝導度は電気の流れやすさそのものなので、溶液の抵抗値を測ればいいことになります。
こう考えると結構単純です。
抵抗を測るのであれば電流測定をしてその時の電圧を測ればよさそうですね(◍•ᴗ•◍)♡ ✧*。
計測波形は交流波形にする必要があります。
イオンが含まれる溶液に電圧をかけるときに注意しなければならないことは、プラスマイナスの電圧をかけることです。
プラスの電圧だけだとマイナスのイオンがプラスの方の電極に引き寄せられて結晶化してしまいます。結晶化してしまうともちろん電極が溶液に露出していませんから正確な水溶液の抵抗を測ることができなくなってしまいます。
プラスマイナスの電圧でスイングしてあげることで電極の結晶化を防ぐことが重要です。
自作のECメーターの回路の構成を詳細に解説します。
ECメーターの仕組み自体は単純そうです。交流波形で測定できるECメーターの回路を作りましょう。
交流波はウィーンブリッジ回路で実現します。
ウィーンブリッジ回路は回路だけで交流波形を作ることができます。
なんでラズパイからDAコンバーターを使ってソフトウェアから作らなかったのかはこちらの記事を参考にしてください。
今回作る回路はメインのウィーンブリッジ回路とその部品であるオペアンプに負電源を供給する回路、交流波形の出力インピーダンスを下げるエミッタフォロワで構成しようと思います٩( ‘ω’ )و
正弦波の発振源・ウィーンブリッジ回路の構成は?
まずは正弦波を発振するウィーンブリッジ回路の部品です。
- オペアンプ
今回は汎用品のUA741を使おうと思います。 - ダイオード
1N4148を使います。 - 抵抗
電流はそんなに流れないので1/6W程度で十分でしょう。 - キャパシタ
最大5Vの振幅の交流を扱うので6.3Vあれば十分じゃないでしょうか。
周波数は?
ウィーンブリッジ回路は抵抗とキャパシタの容量で周波数が決まります。
という周波数になります。
EC値と周波数は関係するのかはまだ分かりませんが、ADコンバータの処理を考えると早すぎるのは良くない気がします。
オペアンプのためのマイナス電源
こちらは前回の記事で作ったタイマーIC555を使ったものを使おうと思います。
出力インピーダンスを下げるためのエミッタフォロワ
LTSpiceでシミュレーションしているとウィーンブリッジ回路の正弦波をプラス側へオフセットすると、つなげる負荷によって大きな振幅の増減があることがわかりました。
ちょっとまずいのでエミッタフォロワを使って出力インピーダンスを下げて意図した通りの正弦波になるようにしています。
- NPNトランジスタ。エミッタフォロワの基本です。
- 抵抗。流れる電流を制限します。
EC測定部分(R9の抵抗部分が測定部分になります。)はプラスマイナスになるようにして、ADコンバーターはプラス側しか扱えないのでその入力段はプラス側にオフセットするようにしています。
今回は交流波形がプラスマイナスに振れるのでエミッタフォロワはプッシュプル型にしています。
部品はけっこう使います( ಠωಠ)
LT Spiceで事前にシミユーレション!ちゃんと動くようです。
組み上げて動かなかったらいやなのでLT Spiceでシミュレーションで事前に実験します。ちゃんとEC測定部分はプラスマイナスの交流になっていてADコンバーターの入力部分はプラス側のみになっています。
これでばっちりなはずです٩( ‘ω’ )و
回路をブレッドボード上に組んで実験してみました。
シミュレーションだけだと実験できないのでブレッドボード上で回路を組んでみたいと思います。
ちゃんとオシロスコープで測定します。
ADコンバーターを選びましょう。MCP3004を使います。
MCP3004は秋月電子で簡単に手に入れることができる4CHのADCです。
10bitの分解能があり、EC計の用途には十分な分解能があります。MCP3002が2ch用で8chのMCP3008もあります。
スペックシートも簡単に手に入ります。これを見るとMCP3004/MCP3008は同じデータシートのようです。
ざっとみてみましたがMCP3008もほぼほぼ同じプログラムでいいようです。
ラズパイではどうやって動かす?
ラズパイからMCP3004と通信するにはSPIを使います。SPI通信には便利なWiringPiを使いましょう。
SPI通信に必要なプロトコルと使用するWiringPiのAPIについては以前の記事を参照してください。
MCP3004の仕様書を確認してラズパイの設定を決めます。
ラズパイSPI通信プロトコルを確認します。
ラズパイのSPI通信は8bit単位で行われます。ここを理解していないと正しいデータを送れなくて結果として正しく通信できません。
実際にSPI通信をするときの波形を取得してみました。
黄色がSCLKでクロックです。8bit単位になっているのがわかると思います。
この8bit単位というのは意外と重要なんです。というのもシーケンシャルにデータをやりとりする方法と8bitセグメント単位で送る方法ではSPI通信を行うときのデータ送受信のプロトコルが違うからです。
実は最初の実装でなんでかうまく通信できなくて波形を取得してみたらそうだった。。。ということがありました。みなさんは気をつけてくださいね。
スペックシートからプロトコルを確認します。
MCP3004のスペックシートをみてみましょう。
こちらが連続したクロックで通信する場合です。
microchipのサイトから引用
一方、8bitセグメントの方法だとまったく送るデータは同じでもクロックのタイミングが違うことがわかりますね。
microchipのサイトから引用
連続したクロックでは送受信のために16bitのバッファーがあればいいのですが、8bitセグメント単位の場合には24bitのバッファーが必要になります。
さっき見たようにラズパイは8bitセグメントなので24bitのバッファーを使うようにします。
仕様が分かったので次はプログラムを実装します(◍•ᴗ•◍)♡ ✧*。
WiringPiを使ってC++でMCP3004を制御します。
メインの関数からEC測定用のクラスと温度補正用の温度データを取得します。
このプログラムを実行するとこんな感じで溶液の抵抗と温度が取得されます。
自作のECメーターのメイン関数
SetRefResistanceという関数で回路で設定した参照用の抵抗値を設定しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <time.h> #include "ecMeasure.hpp" #include "thermistor.hpp" int main(int argc, char *argv[]) { // current time time_t now = time(NULL); struct tm *pnow = localtime(&now); char strDateNow[16]; char strTimeNow[16]; sprintf(strDateNow, "%04d-%02d-%02d", pnow->tm_year+1900, pnow->tm_mon+1, pnow->tm_mday); sprintf(strTimeNow, "%02d:%02d:%02d", pnow->tm_hour, pnow->tm_min, pnow->tm_sec); // initialization of classes and operation EcMeasure ec = EcMeasure(); Thermistor thermistor = Thermistor(); // initial setup before SPI communication start ec.SPISetUp(); ec.SetRefResistance(1000.0); // skip SPI setup because it has already done by EcMeasure class. thermistor.SetRefResistance(10000.0); printf("Resistance and temperature : %s %s %4.2f %4.2f\n", strDateNow, strTimeNow, ec.GetEC(), thermistor.GetTemperature()); return 0; } |
EC測定用のクラスです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
#ifndef _ECMEASURE_H_ #define _ECMEASURE_H_ /* Sample program of ec measurement w/o thermal calibration from jitaku-yasai.com */ #include "mcp3004.hpp" #include "type.h" #define NUMSHOTS 10 class EcMeasure : public MCP3004 { public: void SetRefResistance(float refResistance); // Registance which devides voltage float GetRefResistance(); float GetEC(int numshots = NUMSHOTS); // GetEc ~EcMeasure(); // Destructor private: float ec; float resistance; float refResistance; float FindPeak(int ch = MCP3004_CHANNEL0, int numshots = NUMSHOTS); }; inline void EcMeasure::SetRefResistance(float theRefResistance) { refResistance = theRefResistance; } inline float EcMeasure::GetRefResistance() { return refResistance; } #endif |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 |
/* Sample program of ec measurement w/o thermal calibration from jitaku-yasai.com */ #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <math.h> #include "ecMeasure.hpp" float EcMeasure::FindPeak(int ch, int numshots) { float peakAdcVal = 0.0; // input ch check. if (ch + 1 > MCP3004_CH_NUM) { printf("EcMeasure::FindPeak input ch is incorrect %d\n", ch); exit(-1); } for(int i=0; i<numshots; i++) { float measuredADVal = (float)WriteRead(ch) - MCP3004_RESOLUTION / 2; //printf("FindPeak : shot %d value = %4.2f\n", i, measuredADVal); if (measuredADVal > peakAdcVal) peakAdcVal = measuredADVal; } printf("Found Peak : value = %4.2f\n", peakAdcVal); return peakAdcVal; } float EcMeasure::GetEC(int numshots) { // Local variables float measuredADValCh0 = 0.0, measuredADValCh1 = 0.0; float peak; // Find peak to measure resitance in stable wave // Assume 90% of peak is enough to catch the top. peak = FindPeak(MCP3004_CHANNEL1, 100) * 0.9; for(int i=0; i<numshots; i++) { float ADValCh0 = 0.0, ADValCh1 = 0.0; while(peak > ADValCh1) { // CH0. Need to subtract offset for ADC input. // The offset point is set to the half resoluton of 10bit ADC ADValCh0 = (float)WriteRead(MCP3004_CHANNEL0) - MCP3004_RESOLUTION / 2; //printf("ADC CH0 value = %4.2f\n", measuredADValCh0); // CH1. Need to subtract offset for ADC input. ADValCh1 = (float)WriteRead(MCP3004_CHANNEL1) - MCP3004_RESOLUTION / 2; //printf("ADC CH1 value = %4.2f\n", measuredADValCh1); } measuredADValCh0 += ADValCh0 / (float)numshots; measuredADValCh1 += ADValCh1 / (float)numshots; } // Calculate current. // CH0 shows original sin voltage, // CH1 shows dropped voltage by resitance for current measument // printf("EcMeasure::GetEC CH1 = %4.2f CH2 = %4.2f\n", measuredADValCh0, measuredADValCh1); float curret = (measuredADValCh0 - measuredADValCh1) / GetRefResistance(); // Registance for the measument point return measuredADValCh1 / curret; } EcMeasure::~EcMeasure() { } |
温度補正用の温度データを取得します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
#ifndef _THERMISTOR_H_ #define _THERMISTOR_H_ /* Sample program of Thermistor from jitaku-yasai.com Thermistor specification : http://akizukidenshi.com/catalog/g/gP-07257/ */ #include "mcp3004.hpp" #define CONST_B 3435 #define CONST_T 25 #define CONST_R 10000 #define ABSTEMP 273 #define NUMSHOTS 10 class Thermistor : public MCP3004 { public: void SetRefResistance(float refResistance); // Registance which devides voltage float GetTemperature(int numshots = NUMSHOTS); // GetTemperature ~Thermistor(); // Destructor private: float temperature; float resistance; float refResistance; }; inline void Thermistor::SetRefResistance(float theRefResistance) { refResistance = theRefResistance; } #endif |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
/* Sample program of Thermistor from jitaku-yasai.com Thermistor specification : http://akizukidenshi.com/catalog/g/gP-07257/ */ #include <stdio.h> #include <unistd.h> #include <math.h> #include <wiringPi.h> #include <wiringPiSPI.h> #include "thermistor.hpp" float Thermistor::GetTemperature(int numshots) { // From ADC value, culculate thermistar resistance. float measuredADVal = 0.0; for (int i = 0; i < numshots; i++) { measuredADVal += (float)WriteRead(MCP3004_CHANNEL2); } measuredADVal /= (float)numshots; // printf("ADC ave value = %4.2f\n", measuredADVal); resistance = (MCP3004_RESOLUTION - measuredADVal) / measuredADVal * CONST_R; // printf("resistance = %4.2f\n", resistance); // Intermediate value derived from resistance float calcFromResistance = log(resistance / refResistance) / (CONST_B); // Intermediate value from reference temperature of using thermistor float calcFromConstTemp = 1.0 / (CONST_T + ABSTEMP); // Calculate temperature from Intermediate values temperature = 1.0 / (calcFromResistance + calcFromConstTemp) - ABSTEMP; return temperature; } Thermistor::~Thermistor() { } |
そもそものMCP3004の基本クラスは?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
#ifndef _MCP3004_H_ #define _MCP3004_H_ /* Sample program of MCP3004 from jitaku-yasai.com MCP3004 specification : http://ww1.microchip.com/downloads/en/DeviceDoc/21294E.pdf */ #include "type.h" // 01234567 #define MCP3004_START_BIT 0b00000001 #define MCP3004_SGL_BIT 0b10000000 #define MCP3004_DONTCARE 0b01000000 //Don't care for MCP3004 #define MCP3004_CH0_BIT 0b00000000 #define MCP3004_CH1_BIT 0b00010000 #define MCP3004_CH2_BIT 0b00100000 #define MCP3004_CH3_BIT 0b00110000 #define SPI_SPEED 500000 // SPI clock range : 500,000 through 32,000,000 #define MCP3004_CHANNEL0 0 #define MCP3004_CHANNEL1 1 #define MCP3004_CHANNEL2 2 #define MCP3004_CHANNEL3 3 #define MCP3004_CH_NUM 4 #define RP_SPI_CHANNEL0 0 // Raspberry Pi has two SPI channel. #define DATALEN 3 #define MCP3004_RESOLUTION 1024 class MCP3004 { public: MCP3004( // Constructor with default value uint theChannelOfRP = RP_SPI_CHANNEL0, uint theSpiSpeed = SPI_SPEED); virtual ~MCP3004(); // Destructor int SPISetUp(); // SPI setup int WriteRead(uint adcCH); // Send setting and recieve return value. private: unsigned int spiSpeed; // SPI communication speed unsigned char data[DATALEN]; // buffer to send/recieve for SPI communication unsigned int channelOfRP; // channel for RP communication }; #endif |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
/* Sample program of MCP3004 from jitaku-yasai.com MCP3004 specification : */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <wiringPi.h> #include <wiringPiSPI.h> #include "mcp3004.hpp" MCP3004::MCP3004(uint theChannelOfRP, uint theSpiSpeed) { // Parameters channelOfRP = theChannelOfRP; spiSpeed = theSpiSpeed; } int MCP3004::SPISetUp() { return wiringPiSPISetup(channelOfRP, spiSpeed); } int MCP3004::WriteRead(uint adcCH) { // initialize buffer memset(data, 0, DATALEN); // set buffer for inquiry data[0] = MCP3004_START_BIT; data[1] = MCP3004_SGL_BIT | MCP3004_DONTCARE; // Channel setting if (MCP3004_CHANNEL0 == adcCH) data[1] |= MCP3004_CH0_BIT; if (MCP3004_CHANNEL1 == adcCH) data[1] |= MCP3004_CH1_BIT; if (MCP3004_CHANNEL2 == adcCH) data[1] |= MCP3004_CH2_BIT; if (MCP3004_CHANNEL3 == adcCH) data[1] |= MCP3004_CH3_BIT; wiringPiSPIDataRW(channelOfRP, data, DATALEN); // Write and receive from same buffer return (int)((data[1] << 8 | data[2]) & 0x3FF); } MCP3004::~MCP3004() { } |
Makefileはこちら
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
OBJS = ecMeasure.o thermistor.o mcp3004.o main.o TARGET = MeasureEc CC = g++ CFLAGS = -Wall -lwiringPi .SUFFIXES: .cpp .c .o $(TARGET): $(OBJS) $(CC) $(OBJS) -o $(TARGET) $(CFLAGS) .c.o: $< $(CC) -c $(CFLAGS) $< .cpp.o: $< $(CC) -c $(CFLAGS) $< clean: rm -f $(OBJS) $(TARGET) |
それでは実験してみましょう
プログラムも回路もできたので実際にECメーターとして使えるかを実験して見ます。
今回はシャーペンの芯を電極にして実験してみました。コップに水を入れてコップの淵にシャーペンの芯を固定しています。
実験方法は?
液体肥料のハイポニカを少しづつ加えて行って抵抗値が変わるかを見たいと思います。
スポイトで少しづつ加えていきます。
この状態で、電極間の電圧を測ることでEC値を求めていきます。
水の場合には?
水の場合にはあまり抵抗値が高く、電解質が溶けていないことがわかります。
液体肥料を少し加えて見ます。出力電圧が変わっていることがわかります。
スポイト10回分加えてみると電圧が下がっていることがわかります。抵抗値が下がっていることがわかりますね。
まとめ
自作のECメーターの電気回路の構成とプログラムの紹介をしました。実験で動作確認もできたので、回路を基板化したり、実際の運用に取り入れていきたいと思います。
記事を読んでいただいてありがとうございます。この記事がいいなと思ったら下記のSNSボタンのクリックをお願いします。励みになります😁