作品タイトル:GR-KURUMIの機能を使う アナログ入力(ADC)編
表示名:@chobichan
コンセプト・作品説明 |
---|
GR-KURUMIのアナログ入力(ADC)機能を使います |
マイコンでアナログ信号を扱う
現実の世界の現象は(仮想現実の中ではなく)アナログ信号で満ち溢れています。しかしマイコンはデジタルの世界に生きています。マイコンでアナログ信号を扱うと言う事は、何らかの方法でアナログ信号をデジタル信号に変換する必要があります。
幸い多くのマイコンにはADC(Analog to Digital Converter)と言うアナログ信号をデジタル信号に置き換える機能が備わっていますので、その機能を使うことでマイコンでアナログ信号を扱う事ができます。
または、I2C編で使用したHDC1000と言う温湿度センサーは自身がADCを持っていますので、マイコンはデジタル化された信号をそのまま利用できます。
アナログ信号は不安定
小学生の頃、物差しの使い方を習いませんでしたでしょうか?
物の長さを計るのに一回で計らず、何度も計りなおして平均を取ってみたり、複数の物差しを用意して、その平均を取ってみたり。
特に小学生が使う様な竹製やプラスチック製の物差しの目盛のバラつきは大きく、その物差しで計った長さが絶対とはできないのです。
アナログ量には揺らぎ(誤差)が必ず存在します。計測対象の長さの揺らぎも有りますし、竹製の物差しの目盛のバラつきや目視の確度の揺らぎはアナログ量の揺らぎとも言えるでしょう。
金属製の物差しでさえ、周囲温度で膨張、収縮が起きる事は知られていますので、これも絶対とは言い難いのです。
ですが、あれも信用できない、これも信用できないでは何も計れないので、何処かに落し処が必要です。
それが精度と言われる物です。小数点以下第何位?のあれです。
アナログ量を計る時は精度を決めておきます。無限の精度を求める事はできません。
デジタル信号は大雑把
明確に0は0、1は1とするデジタル信号は、数字の確度は無限大です。唯一無二ですから。
ですが、、、
アナログ信号は連続した信号であり、何処を切り取ってみても前後の連続性が有ります。
マイコン等に用意されているADCの解像度(分解能)を考えてみます。例えばGR-KURUMIのADCは10bitとなっています。10bitで表現できる数字は0~1023の1024通りの数字です。
ADCの最大の入力電圧を3.3Vとした時、 それを1024で割った値は約3.2mVです。実際は3.22265625…mVこれを0~1023の数字に割り当てると、0が0mVだとしたら1は約3.2mV、2は約6.4mV、、、1023は約3296.8mVです。なら1.6mVは何になるのでしょう?ADCの出力は0(0mV)か1(3.2mV)のどちらかの数字にしかなりません。実際の値に対して1.6mVの誤差です。いい加減ですね。ならADCの分解能を11bitにすれば1.6mVはピッタリになり気持ち良いです。しかし0.8mVはどうでしょうか?もう1bit増やして、、、
アナログ信号をデジタル信号に変換する時のこのもやっとした領域に白黒付ける為に、分解能を増やす事はしばしば行われますが、RL78/G13単体ではできないですね。
10bitのADCで11bitの精度を得る別の方法として、アナログ信号を複数回変換、それも元の信号に小さなホワイトノイズを載せて統計的に11bit目を決める方法も有ります。
これはあれ、物差しで複数回計るあのやり方です。いずれにせよ最終的に得たい答えの精度によって、多くの手間が掛かったり、お金が沢山必要だったりするので、その辺をよく考える必要があります。
まずは手始めにマイコン内蔵の温度計で温度を測る
GR-KURUMIで使用しているRL78/G13は内部に温度計を持っていて、それを使って簡易的に温度を計測できます。
それを使ってみましょう。
なんて事無いです。WEBコンパイラで新しくプロジェクトを生成すると、自動的にソースコード(gr_sketch.cpp)にその機能が実現されています。
void setup() { //setPowerManagementMode(PM_STOP_MODE, 0, 1023); //Set CPU STOP_MODE in delay() //setOperationClockMode(CLK_LOW_SPEED_MODE); //Set CPU clock from 32MHz to 32.768kHz // initialize the digital pin as an output. Serial.begin(9600); Serial.print("Temperature: "); Serial.println(getTemperature(TEMP_MODE_CELSIUS)); //temperature from the sensor in MCU
getTemperature(TEMP_MODE_CELSIUS);
が内蔵の温度計から摂氏で温度データ取得する関数です。
このプログラムを入れたGR-KURUMIをCOMポートで開くと一回だけ温度を出力します。
アナログ温度センサーで温度を測る
アナログ温度センサーと言っても様々なタイプの物が有ります。サーミスタや白金、熱電対等がよく使われていますが、ここではICタイプの温度センサー(TMP36GT9Z)を使用します。この温度センサーは電源電圧が2.7Vから5.5Vで使えるので、3.3Vを電源としているマイコンでも使用できます。
出力は温度を電圧に換算して出力します。その傾斜は10mV/ ℃、つまり1℃増えると電圧が10mV増えます。
通常温度センサーは常温とされる25℃を基準にします。
TMP36GT9Zは25℃の時750mVとなるように調整されていますので、ADCで取った電圧を温度に換算する時は25℃を基準に考えます。今回はこのセンサーを使って50℃くらいまでの温度範囲を計測するとします。
50℃の時はセンサーの出力は1.25Vとなります。
TMP36GT9Zはマルツで購入できます。
http://www.marutsu.co.jp/pc/i/171271/
精度を決める
そもそもTMP36GT9Zの精度を知る必要があります。 データシートを読むと25℃で±1℃の精度、-40℃から125℃の範囲内では±2℃の精度と書かれていますので、小数点以下の数字を出したところで正確であるとは言えないのですが、あまり大雑把な温度表示だと変化の傾向が解り難いので、小数点以下第1位とします。
温度センサーとの接続
GR-KURUMIのA0に温度センサーの出力を接続します。データシートの通りバイパスコンデンサも用意しておきます。
センサーのVoutをGR-KURUMIのA0に接続します。
動かしてみる
以下にサンプルコードを記載しておきます。
定期的にシリアルに温度を出力するプログラムです。
/*GR-KURUMI Sketch Template Version: V1.12*/ #include#define TMP36_PIN A0 #define VREF 3.3f //基準電圧。ここでは電源電圧(3.3v)を使用 void setup() { analogReference( DEFAULT ); //基準電圧の設定 Serial.begin(9600); Serial.print("Internal temperature:"); Serial.println(getTemperature(TEMP_MODE_CELSIUS)); //temperature from the sensor in MCU while( 1 ) { unsigned short tmp36US = analogRead(TMP36_PIN); float tmp36F = ((float)tmp36US * VREF) / 1024.0f; //一旦電圧に換算している tmp36F = ((tmp36F - 0.75f) / 0.01f) + 25.0f; //25℃を基準に計算している tmp36F = tmp36F + 0.05f; //小数点以下第2位で四捨五入 char buf[32]; sprintf( buf, "%2.1f", tmp36F ); String s = "External temperature:"; s += buf; s += "c"; Serial.println( s ); //temperature from the external ic sensor delay( 1 * 1000UL ); } } void loop() { }
精度を向上する
下の図は結果です。
ところで1℃で10mVなのですから、0.1℃は1mVになります。
先に書いた様にADCの分解能から約3.2mVが最低の解像度になります。
これでは本当の0.1℃の精度にはなりませんので、何か工夫をしてみましょう。
ADCはある基準の電圧を元に、それを分解能で割った値が解像度になります。
GR-KURUMIで使用しているRL78/G13は基準に電源電圧(VCC)以外に内部で生成する1.45Vの基準電圧、外部からの基準電圧の3つから選ぶ事ができます。
analogReference と言う関数がそれです。
引数に
DEFAULT:VCC
INTERNAL:内蔵電圧1.45V
EXTERNAL:本来は外部電圧ですが基板上でVCCに固定を選択します。この関数を呼ばなければDEFAULTの設定になっています。
基準電圧が1.45Vであれば解像度は約1.4mVとなりますので、倍以上は改善しています。
しかしこの内部基準電圧は、データシートを読むと最低が1.38V、最大が1.5Vとなっていて本当に精度が高くなるのか微妙に思えてきました。
VCCは電源電圧変動の可能性があるのでVCCに比べれば安定性は高くなるのですが。
そんな時は外部基準電圧を使います。
困った事にGR-KURUMIでは基板外部に引き出されていなくてできませんが。
基準電圧を発生するICはよりどりみどりです。要求に合う物を選んでください。
繰り返しますがGR-KURUMIはできませんが。ハードウエアを使った精度向上以外に、複数回アナログ信号を取り込んで平均化したり、移動平均を取ったり、最小二乗法を使ったりしてアナログの揺らぎを慣らして必要なデータを抽出します。
温度計測について考えてみる
GR-KURUMIで温度が計測できるようになったとして、その出力結果は正しいのでしょうか?それには基準となる測定器(温度計)が必要になってきます。
しかしその辺で売っている温度計の精度を調べてみると25℃で±1℃から2℃くらいの誤差は普通です。
他のICタイプの温度センサーも似た様な誤差を持ちます。
高精度の温度測定についてネットで検索していたらこの様な資料がありましたので、リンクを貼って置きます。
http://www.el.gunma-u.ac.jp/~kobaweb/lecture/hiaccusensor.pdf
さて、室温の計測用途であれば±1℃もあれば十分な気がします。※この測定でエアコンを操作して1円でも電気代を安くする目的ならば、もっと高精度の物を選ぶ必要があるのかもしれません。
我が家の温度測定で基準に使えそうなのは温度測定機能を持つテスターと付属のK型熱電対でした。K型熱電対は2種類の異なる金属同士を接着したとても小さなセンサーです。写真の様にはだかの状態でケーブルの先に付いています。
IC温度センサーも小さいとは言え、K型熱電対に比べれば何十倍も大きいです。この大きさは熱容量の違いにもなります。
つまり周囲温度が50℃の環境に持ち込んだ時、K型熱電対は早く反応し、IC温度センサーはそれに随分遅れて反応します。
僅かな空気の流動が起きてもK型熱電対は敏感です。
二つの出力を比較するにしても、条件を考慮しないと永久に合わない事態になりかねません。
一つの方法として基準となる温度センサーと評価したい温度センサーを一緒にして綿で包み、更にそれをアルミフォイルで包んで風の影響を受けない様にし、ホットプレートや冷蔵庫等に入れてゆっくりした温度変化を加えて時間を掛けて比較します。>
各温度でIC温度センサーの出力を記録して、グラフ等にプロットします。先の資料にも有りますが、日常的な温度範囲ではIC温度センサーの出力はある程度の直線性が有るので、ADCで取得した結果にプロットから求めた傾斜を補正する係数を掛けて実際の値に近づく様にした方が良いでしょう。
実装上の注意
温度センサーの精度が幾ら良くても、いくら実験で補正を掛けたとしても、例えばIC温度センサーが消費電力の大きなデバイス、例えばマイコンや無線モジュールや電源の近くに配置されてしまうと、確実にその影響を受けてしまって正常に計測ができません。
これら消費電力の大きなデバイスから避けて、更に熱のこもらない位置に実装する必要があります。
ケース等に密着してしまうと、ケースの熱容量にも影響されてしまうので、ケースの温度を測りたい用途以外は離しておいた方が良いでしょう。
交流信号を扱ってみる
世の中には交流電流を測る為のトランスが有りまして、ちょっと前の省エネブームの時に電子工作でもよく使われていました。
http://www.u-rd.com/
http://www.multimic.com/
電気製品の消費電流や配電盤の電流を計測してみるあれですね。
GR-KURUMIのADCが扱う信号の範囲は0V~3.3Vまたは0V~1.45Vで直流です。
しかし計測するのは交流なのでプラスとマイナスの方向に信号が振れます。絶縁も含めて写真の様なトランスを使い、二次側に出力した電流を電圧に変換し、電圧に直流バイアス(重畳)を掛けてマイコンで扱える様にします。中点と振幅を調整してADCの入力範囲(ダイナミックレンジ)を有効に利用します。基準電圧が3.3Vであれば1.65Vを中心に最大電流の時に±1.5V程度の振幅とします。
交流の電流の大きさの計算はRMS(Root Mean Square)、日本語にすれば二乗平均平方根で行います。
グラフは電流の振幅を±1Apeakとした計算のプロットです。
実効値で言えばだいたい0.707Armsです。
実際の電流の波形はこんな綺麗なサイン波である事はまれです。電気機器でスイッチング電源を使用している物はもっと複雑な波形となります。
エクセルで描いたこのグラフは、1波を32個のデータで構成しています。東日本なら商用電源の周波数は50Hzで1周期は20msです。それを32等分した時間(0.625ms)毎にプロットして描いたものです。
エクセルで描いた時は、はじめに0.707Armsの電流の大きさから振幅を決めて32個のデータを作成し、そのデータをプロットしてサイン波を描いたこの過程の逆を実際には行います。
ADCを使って交流波形を0.625ms毎にデジタルデータに変換します。それぞれのデータは先程直流分を重畳している為にまずその直流分を除去します。
交流波形の定義は、1周期分の振幅の合計が0になる波形の事なので、デジタルデータを1波分足し算すれば直流分のみ残る事になり、それの平均が重畳した直流の値です。式にすれば左の式になります。
次に元のデータからこの算出した直流分を引き算すれば、今度は交流成分のみ残ります。交流分のそれぞれの値の二乗の積分を1波分で平均化、更にその値の平方根が実効値です。
数式で書けば左の数式となります。取り敢えず1波の1/32の周期で交流波形をデジタルデータとして取り込みましたが、実際の波形が複雑になるほどもっと短い周期と高い分解能で沢山のデータを変換する必要が有るでしょう。
アナログ信号の取り込みを自動化してみる
交流電流の取り込みは正確な周期で高い頻度で行う必要があります。
analogReadを使った取り込みは、デジタルデータへの変換はソフトウエアをトリガーにしているので、一定周期で行うのは難しいし、CPUの負担も大きくなります。しかしRL78/G13はADCと連動するタイマーや、DMACが有るので自動的に変換、RAMへ取り込みができます。
この機能を使用してみます。
※ライブラリでサポートされていない機能です。自分で周辺機能のレジスタを設定する必要があります。
RL78/G13のレジスタに直接アクセスする(ADC、DMA、割り込みなど)
Arduino準拠のライブラリの良いところは、あまりRL78のハードウエアに詳しくなくてもそれなりに動いてしまう事ですが、今回は完全にライブラリのサポートから離れてマイコンを動かします。
その為、レジスタをいじる局面では以下のファイルをインクルードします。
#include
#include
・ADCトリガーの選択
RL78/G13のADCのブロック図を示します。赤丸で囲ったところがADC変換トリガー入力になります。
INTRTCはRTCで発生した割込みをトリガーにします。INTTM01はタイマー0・チャネル1の割込みをトリガーにします。
INTITは12bitインターバルタイマーの割込みをトリガーにします。INTRTCはRTCを時計として使用する予定があるので使用できません。INTTM01はタイマーの原振は高速オンチップ・オシレータが生成するシステムクロック(32MHz)です。
発振精度は常温域で動作電圧を3.3Vなら±1%以内の精度となります。
INTITはmillis()等で利用するミリ秒単位の管理をしているので使用できません。
INTTM01は原振を分周して使います。問題は綺麗に割り切れる分周比が設定できるか?です。
32MHzを50Hz×32で割ると丁度20000になります。
トリガーにはINTTM01を選ぶ事とします。
次にタイマーの初期化を記載しておきます。
void TMR01Init( unsigned short tdr01 ) //タイマー0、チャネル1初期化 { PER0.BIT.tau0en = 1; //timer unit 0 enable TDR01.tdr01 = tdr01; /*0x50:CKm3=125khz,CKm2=16mhz,CKm1=1mhz,CKm0=32mhz*/ TMR01.BIT.bit15 = 0; //CKm0 select TMR01.BIT.bit14 = 0; //CKm0 select TMR01.BIT.bit12 = 0; //CKm0 select TMR01.BIT.bit11 = 0; //SPLIT 16bit timer TMR01.BIT.bit10 = 0; //STS012 only software trigger TMR01.BIT.bit9 = 0; //STS011 only software trigger TMR01.BIT.bit8 = 0; //STS010 only software trigger TMR01.BIT.bit7 = 0; //CIS011 rise TMR01.BIT.bit6 = 0; //CIS010 rise TMR01.BIT.bit3 = 0; //MD013 interval timer TMR01.BIT.bit2 = 0; //MD012 interval timer TMR01.BIT.bit1 = 0; //MD011 interval timer TMR01.BIT.bit0 = 0; //MD010 no interrupt TOE0.BIT.bit1 = 0; //disable TO01 output //TIM01 interrupt enable 0x2e IF1L.BIT.tmif01 = 0; // interrupt request flag clear // MK1L.BIT.tmmk01 = 0; // interrupt mask flag -> enable MK1L.BIT.tmmk01 = 1; // interrupt mask flag -> disable PR11.BIT.tmpr101 = 1; // interrupt priority bit1 -> min PR01.BIT.tmpr001 = 1; // interrupt priority bit0 -> min TS0.BIT.bit1 = 1; //channel1 start }
・スキャンモードを使用する
RL78/G13のADCは複数のアナログ入力をいっぺんに変換してしまうスキャンモードを持っています。
複数の入力を取り込む必要が有る時便利です。
ただし、ちょっとこの機能はクセがあります。
スキャンモードでは必ず4チャネルを選択する事になりますし、チャネルの組み合わせも制限があります。
※表を参照
また、変換チャネルにデジタルに設定した端子を含める事はできません。
今回はANI0~ANI3をスキャンする事とします。
※GR-KURUMIはANI0は3.3V、ANI1はGNDに接続されてます。アナログ入力できるのはANI2とANI3です。INTTM01を50Hz×32としてトリガーを発生させると0.625ms毎に8byteの変換データが生成されます。次にADCの初期化を記載しておきます。
void adInit( void ) //ADCの初期化 { PER0.BIT.adcen = 1; //A/Dコンバータで使用するSFRへのリード/ライト可 ADPC.BIT.bit0 = 1; //AIN0(P20) to AIN3(P23) for analog input ADPC.BIT.bit1 = 0; // ADPC.BIT.bit2 = 1; // ADPC.BIT.bit3 = 0; // PM2.BIT.bit0 = 1; // AIN0(P20) to AIN3(P23) for analog input PM2.BIT.bit1 = 1; // PM2.BIT.bit2 = 1; // PM2.BIT.bit3 = 1; // ADM0.BIT.adce = 0; //A/D電圧コンパレータの動作停止 ADM0.BIT.adcs = 0; //変換動作停止 ADM0.adm0 = 0x40; //scan,fclk/64 ADM1.BIT.bit7 = 1; //ハードウエア・トリガ・ノーウエイト・モード ADM1.BIT.bit6 = 0; //ハードウエア・トリガ・ノーウエイト・モード ADM1.BIT.bit5 = 1; //ワンショット変換モード ADM1.BIT.bit1 = 0; //INTTM01 ADM1.BIT.bit0 = 0; //INTTM01 ADM2.adm2 = 0x00; //A/Dコンバータの+側の基準電圧源の選択 //VDDから供給 //A/Dコンバータの-側の基準電圧源の選択:VSS ADM2.BIT.adrck = 0; // ADLL.adll = 0x00; ADUL.adul = 0xff; delayMicroseconds( 2 ); //wait for adc stability ADM2.BIT.adtyp = 0; //10ビット分解能 ADS.ads = 0x00; //scan ANI0->ANI1->ANI2->ANI3 //ADC interrupt enable 0x34 IF1H.BIT.adif = 0; // interrupt request flag clear // MK1H.BIT.admk = 0; // interrupt mask flag -> enable MK1H.BIT.admk = 1; // interrupt mask flag -> disable PR01H.BIT.adpr0 = 1; // interrupt priority bit1 -> min PR11H.BIT.adpr1 = 1; // interrupt priority bit0 -> min ADM0.BIT.adce = 1; //A/D電圧コンパレータの動作start delayMicroseconds( 2 ); //wait for adc stability ADM0.BIT.adcs = 1; //wait trigger }
・DMAを使用する
スキャンモードで変換したデータはメモリに転送してから信号処理を行います。
交流波形を信号処理する為には、1波分のデータが必要であり、今回の想定では1波分のデータは32データとなります。
要するにスキャンモードの関係上、32×4×2byteをADCからメモリに転送します。
こう言った処理に便利なのがDMAC(Direct Memory Access Controller)です。CPUに代わって予め設定された条件で自動的に周辺IOからメモリへの転送、またはメモリから周辺IOへ転送します。
DMACを起動するトリガーは、今回の例ではADCからの割込みになります。また、設定された回数分転送が完了するとCPUへ割込みを発生させる事ができますので、CPUは全ての転送が自動的に転送完了するのを別の作業をしながら待ち、その後ゆっくりとそのデータの処理ができます。DMACの設定は、データの元はADCのデータレジスタ、データの送り先は大域変数上に確保したSRAM領域。データの転送サイズは16bit、転送回数は32×4回です。
データを保存するバッファは2重にし、CPUで信号処理をしている間にDMACによってバッファに上書きされないようにします。
void dma1Init( void ) //DMAC初期化 { DRC1.BIT.den1 = 1; // dma enable unsigned long temp = (unsigned long)&ADCR.adcr; DSA1.dsa1 = (unsigned char)temp; //sfr address set temp = (unsigned long)currentBuffer; DRA1.dra1 = (unsigned short)temp; //sram address set DBC1.dbc1 = AD_SAMPLE_SIZE; //number of conversions DMC1.dmc1 = 0x21; // trigger is ad conversion //DMA1 interrupt enable IF0H.BIT.dmaif1 = 0; // interrupt request flag clear //__asm__("clr1 IF0H.3\n\t"); // interrupt request flag clear MK0H.BIT.dmamk1 = 0; // interrupt mask flag -> enable //MK0H.BIT.dmamk0 = 1; // interrupt mask flag -> disable PR10H.BIT.dmapr11 = 1; // interrupt priority bit1 -> min PR00H.BIT.dmapr01 = 1; // interrupt priority bit0 -> min DRC1.BIT.dst1 = 1; // dma start and wait trigger } void dma1Int( void ) //割り込み処理 { currentBufferUpdate++; //バッファが更新された事を示す変数 unsigned long temp = (unsigned long)¤tBuffer[currentBufferUpdate & 1]; DRA1.dra1 = (unsigned short)temp; //sram address set DBC1.dbc1 = AD_SAMPLE_SIZE; //number of conversions DRC1.BIT.dst1 = 1; // dma start and wait trigger }
gr_common/RLduino78/portable/e2studio/RL78以下のinterrupt_handlers.cに割り込み処理が書かれています。
DMA1の割り込みはINT_DMA1となります。
ここに先ほどのDMA割り込み処理を呼び出す記述を行います。
//0x1C extern void dma1Int( void ); void INT_DMA1 (void) { dma1Int(); }
【ADC起動処理例】
ADCを起動するまでを記載しておきます。
実際の信号処理は記載されていません。
#define AD_OVER_SAMPLE_RATE 32 #define AD_OVER_SAMPLE_MULTI 4 #define AD_SAMPLE_SIZE (AD_OVER_SAMPLE_RATE * AD_OVER_SAMPLE_MULTI) unsigned short currentBuffer[2][AD_SAMPLE_SIZE]; /*AD変換後の電流チャネルのDMAの転送先。2ウインドウ構成とする*/ volatile unsigned short currentBufferUpdate; /*DMA転送完了割り込みの中で更新されるフラグ。*/ volatile unsigned short oldUpdate = currentBufferUpdate; /*DMA転送完了フラグと比較する変数*/ adInit(); /*ADC初期化*/ dma1Init(); /*DMA初期化*/ TMR01Init( 32000000UL / 50 / 32 - 1 ); /*タイマー初期化。32MHzのシステムクロックから50Hzの32倍オーバーサンプリングタイミングを発生。*/
極一般のハードウエアエンジニア。たまに雑誌記事を書いています。
twitterアカウント:https://twitter.com/chobichan