FRDM-K64Fを例にしてNXPマイコンでセンサとかを使いたいとき、アナログ信号を取り込む必要なことがあります。これをまとめていきたいと思います。超基本ですね。
そして、今回扱うセンサはこちら。人が放った赤外線を焦電効果で検知する人感センサです。
どこで買ったか記憶にないんですが、部屋に転がっていたので、こいつを使います。このセンサは人を検知するとHigh(3.3V), 検知しない時は(0V)を指示します。
この信号をADCで拾い、しきい値を適当に決めて、HighになったらLEDを赤く点灯するシステムを作りたいとおもいます。丁度、よく駐車場にある防犯ライトみたいな。
LEDの点灯方法は前回の記事で取り扱ったので、特に説明しません。
今回はあくまでもセンサ信号をどうやって、マイコンでひろうか。これがテーマです。
こんな感じになりました。
そもそもADCとは?
そもそもすぎますが、一応、説明しときます。ADCとはAnalog Digital Converterの略です。アナログ信号をデジタル信号に変換するデバイスになります。
センサで取得した滑らかな曲線をマイコン内で扱えるようにカクカクしたデジタル信号に変換します。
ADCを扱うとき一番重要なのが分解能とサンプリング速度の二つです。分解能はアナログ信号をどんぐらい細かいカクカクで信号を変換させるかというパラメータで、8bitとか10bitとかいうような表現をされます。
例えば、センサ出力0-5Vで8bitの分解能というと、8bit=256で、フルスケール5V を256分割します。すると一分割あたり 約 0.0195 Vとなり、これが信号を解像できる最小の幅になります。
つまり、数mVとかの微妙な信号の変化を測定したいよって場合は、8bitでは足りず、もっと高分解能なADを使用しないとダメよってなります。
FRDM-K64FのADCは分解能は設定でかえられますが、使用できるレンジは8~16bitです。
次にサンプリング速度ですが、これは、一秒間に何回データをGetできるかで、ksps(キロサンプリング毎秒)の単位で表現されます。実際の値は、ADCのクロックやバスクロック、設定した分解能などの影響うけます。
FRDM-K64Fだと、最大で818kspsぐらいで測定できるようですね。
ADCのスペックを確認するのは、下のリンクの40ページ以降に記述されてます。
基本、分解能とサンプリング速度はトレードオフなので、測定したい信号に合わせて、最適値を探る必要があります。
信号のサンプリングの仕方は色々深い~話になってくるので、ボロがでそうでやめときます。
ADには、変換方式に色々ありますが、FRDM-K64FはSAR(逐次比較型)を採用してます。マイコンでは最も一般的なコンバータです。
これは、マルチプレクサ方式といって、複数のアナログ信号を1つのADCで使い回すことで、効率的に変換しますよ、みたいな便利機能が使えるのが特徴です。
サンプルコードをインポートする
以前、SDKのサンプルコードをインポートする方法を説明したので、参考にしてください。
ADCのサンプルはExamplesのツリーのなかのdriver_examples->adc16にあります。今回はadc16_pollingを使います。
pollingというフレーズがよくマイコンのプログラムでは出てきますが、これはinterruptの反対で、割込み処理をかけず、周期的に処理を行いまっせという意味合いになります。
インポート後にピンコンフィグレーションツールを起動して、pinsタブ内のリストで#55をみてください。するとこんなになっているはず。
#55はADC0_SE12として、アサインされていることがわかります。
FRDM-K64Fでは、ADCが二つあり、ADC0とADC1です。#55ピンは、AD入力する場合は、ADC0を使うというのがハード的にきまってます。
では、SE12ってなんでしょうか?Reference ManualのADC0 Channel Assignmentという表をみると、こんなリストが書いてあります。(118ページ)
赤いマーカの部分が設定しているチャンネルSE12になってます。SARのADCは複数の信号の面倒をみるので、この信号をAD変換しろ!みたいにチャンネルの指示が必要です。
ADC0_SE12の例だと、実際には、ADCのレジスタに’0110’という命令を送ることで、ADCはADC0_SE12つまりMCUの#55ピンにつながってるセンサ値を読み込むことができるって訳です。
で、#55が実際にどのコネクタにつながってるかを回路図から追っかけます
最終的には、サンプルコードで使用しているADC0_SE12は、上図のようにPTB2(J4コネクタの2番)にでていて、ここにセンサをつなげると信号がとれるっていう寸法です。
サンプルコードの場合は、Systickタイマーを使って、周期的に信号を取得して、コンソール画面に結果を表示させる仕様のようですね。
Systickタイマーについては、以前記事で扱いましたので、どーぞ。
サンプルコードを実行すると、下のようにキーボード入力するたびにコンソール画面にADCで取り込んだ信号が表示されます。
信号はちゃんと取れてそうです。
これだけだとふ~んで終わるので、ちょっと掘り下げたいと思います。
アナログ入力する4つのステップ
ADCを使う手順としては、およそ4つのステップに分けられます。
1.AD入力するピンをアサインする
2.ADCの初期設定をする
3.変換開始のトリガーをかける
4.変換後の値を取得する
実際にフルスクラッチでADCのコードを記述する際は、こんな流れになります。順に説明していきます。
AD入力するピンをアサインする
まずは、アナログ信号を取り込みたいピンを設定します。
MCUExpressoのpinコンフィグツールを使うのがいいですね。今回は、サンプルのピンから変更して、ADC0のSE1を使います。下図のように有効化します。
()内にDPとSEというのがありますが、DPは、differential、SEはSingle-endedの信号を取り込みます。
Single-endedはグランドに対して、センサ電圧を測定するもので、differentialは、グランドではなくDPとDMの二つの差動電圧をセンサの信号として、測定します。
は?そんなの何の役に立つのか?とおもうかもしれませんが、めっちゃ役に立ちます。例えば、自分のバイクとか車のセンサをマイコンに取り込もうとするとき、車に搭載されたコントローラでセンサは駆動されているので、あくまでもセンサの値はコントローラのグランドに対しての値になっています。
マイコンのグランドと車のコントローラのグランドレベルが確実に一致していれば、問題ないですが、大抵の場合は、一致していないので、Single-endedとして信号を取り込むと、おかしな値しか測定できません。
そういう場合は、differentialとして、DNをコントローラのグランド、DMをセンサの信号として、その差を取り込むことで、異なるシステムでも確実に信号を取り込むことができるっていう訳です。
メモリマップ上でADCを探す
ピンをアサインしたら、あとはGPIOの時と同じようにレジスタをたたいていくわけですが、方法を少しおさらいします。
メモリーマップは下のような感じです。
ADC0はアドレス0x4003_B000にいます。以前説明したメモリマップドIO(MMIO)のシステムでは、このアドレスを先頭にして、ADC0関連のレジスタがずらずらと定義されていて、CPUはこのアドレスにアクセスすることで、周辺機器を制御することができるんでした。
ADCのメモリマップの詳細は下です。0x4003_B000を先頭として..
つらつら、こんな感じに続いていきます。最後までは多いので貼り付けていません。
これもGPIOの説明でありましたが、もとのADC0という変数の定義を遡ると、上の先頭アドレスを指すポインタになっていて、
#define ADC0_BASE (0x4003B000u) //これがADC0の先頭アドレス
#define ADC0 ((ADC_Type *)ADC0_BASE) //ADC_Type型のポインタ?
それが下のような構造体型で宣言されています。
typedef struct {
__IO uint32_t SC1[2];
__IO uint32_t CFG1;
__IO uint32_t CFG2;
__I uint32_t R[2];
//////// 途中略 ///////
__IO uint32_t CLM0;
} ADC_Type;
構造体の変数は上のメモリマップのレジスタの並びと比べると、まったく同じ順番になっています。こうすることで、ADC0-> CFGとかADC0->CV1とかでアドレスを直接打ち込まなくても、各レジスタにミスなくアクセスできるような仕組みになっています。
これは、どの周辺機器を使うときも基本的には同じやり方です。
まずADC0の初期化する
なんでもそうですが、初期設定は、必要です。ADコンバータ本体をどう動かすかの設定します。
初期化の基本的なサンプルは下のとおりです。※印は補足説明をみてください。
レジスターは他にもありますが、初期化するうえで、基本的にはここで宣言されている内容で十分です。
//ADCの初期化をする
//ADC0_CFG1レジスタの設定
//ADICLK:クロックの設定
tmp32 = (0b11); //クロックソースはAsynchronousにした *1
//MODE:分解能の設定
tmp32 |=(0b01)<<2; //分解能は12bitに設定した
//ADLSMP:サンプルモード
tmp32 |= (0b0)<<4; //shortサンプルモード
//ADIV:分周比の設定
tmp32 |= (0b01)<<5; //分周比を2に設定
//ADLPC:省電力モード
tmp32 |= (0b0)<<7; //省電力機能はつかわない
//書き込み
ADC0->CFG1=tmp32;
//ADC0_CFG2レジスタ設定
//デフォルト設定を読む
tmp32 = ADC0->CFG2;
//ADHSC:高速変換モードの設定
tmp32 |= (0b0)<<2; //ノーマルモードを使用
//ADACKEN:Asynchronousクロック有効化
tmp32 |= (0b1)<<3; //Asynchronousクロックを使用許可
//書き込み
ADC0->CFG2=tmp32;
// ADC0_SC2レジスタ設定
//デフォルト設定を読む
tmp32= ADC0->SC2;
//REFSEL:参照電圧の設定
tmp32 |=(0b00); //VREFH,Lを使用
//ADTRG:変換実行のトリガ設定
tmp32 |=(0b0)<<6; //softwareトリガ *2
//書き込み
ADC0->SC2=tmp32;
// ADC0_SC3レジスタ設定
//デフォルト設定を読む
tmp32= ADC0->SC3;
//ADCO:連続変換モードの設定
tmp32=(0b1)<<3; //連続変換モードON *3
//書き込み
ADC0->SC3=tmp32;
コメントで書いてあるとおりですが、補足します。
*1 ADCのクロックソースの設定ですが、AsynchronousとはMCUのクロックとは別にADCが内部的に持っているクロックを使う設定のことを指します。MCU側から制御するクロックではないので、非同期という表現をつかいます。
*2 ADTRGでは、変換実行のトリガーの設定をします。Softwareトリガとは、ソフト側から変換のトリガをかけることで、今回はそれを使ってます。他にHardwareトリガを設定できますが、外部のピンからの信号たとえばSWでHIGH、LOWの信号を与えるなどして、アナログ信号を変換トリガをかけることができます
*3 連続変換を有効にすると、Softwareトリガを一回かけると、連続で信号を変換します。連続変換を無効にすると、下のサンプルコードのハイライト部分みたいに、測定するタイミング毎にトリガ(ソフトウェア)をかけていく必要があります。
while (1)
{
GETCHAR();
ADC16_SetChannelConfig(DEMO_ADC16_BASE, DEMO_ADC16_CHANNEL_GROUP, &adc16ChannelConfigStruct);
while (0U == (kADC16_ChannelConversionDoneFlag &
ADC16_GetChannelStatusFlags(DEMO_ADC16_BASE, DEMO_ADC16_CHANNEL_GROUP)))
{
}
PRINTF("ADC Value: %d\r\n", ADC16_GetChannelConversionValue(DEMO_ADC16_BASE,
DEMO_ADC16_CHANNEL_GROUP));
}
変換開始のトリガーをかける
初期化したADC0に対して、このピンに入ってくる信号を変換してくれ!という指示をSC1というレジスタに書き込みます。指示の内容ですが、3つあります。
- differential か? single-ended か?
- 変換する入力チャンネルを指定
- 変換後割り込み処理をかけるかどうか
設定方法は下のとおりです。
//3- チャンネル設定&トリガ
uint32_t sc1;
//ADCH:入力チャンネルの設定
sc1 = (0b00001); //SE1を入力
//DIFF:differentialかsingle-endか
sc1 |= (0b0)<<5;
//AIEN: 割り込み設定
sc1 |= (0b0)<<6; //割り込みしない
//書き込み
ADC0->SC1[0]=sc1;
上のADCHの設定の仕方ですが、上の方で説明したとおり、下の表を参考にして、変換したい信号の入力ピンに対して、決められたコマンドを設定します。
この表ですね。
連続変換する場合は、サンプル開始のタイミングで一回だけ、単発なら、信号を取得する毎にこの処理を行います。
変換後の値を取得する
トリガをかけたら、やることは二つです。
- 変換できたかどうか確認する
- Rレジスターで変換後の値をよみにいく
変換が完了すると、 SC1レジスタのCOCO(bit7)が1になります。変換後の値はRレジスタに格納されます。ちなみにdifferentialとsingle-endedで格納される値がsigned だったりunsignedだったりするので、データ型に要注意です。
下がサンプルです。フラグが1になるまでWhileでループして、変換完了後 Rレジスタの値を取得してます。
//SC1レジスタCOCO:変換完了フラグ(bit7) 1なら変換完了
while (0U == ((0b1<<7) &
(ADC0->SC1[0] & (0b1<<7)) ))
{
}
//Rレジスタ 変換後の値がはいる
sensor_value = ADC0->R[0];
サンプルコード
以上のコードの全文は下のようになります。あまり難しい所はありません。
/*
ADCサンプルコード 人感センサでLチカ
*/
#include <stdio.h>
#include "board.h"
#include "pin_mux.h"
#include "clock_config.h"
#include "fsl_debug_console.h"
/*******************************************************************************
* Definitions
******************************************************************************/
#define BOARD_LED_GPIO BOARD_LED_RED_GPIO
#define BOARD_LED_GPIO_PIN BOARD_LED_RED_PIN
#define ADC16_BASE ADC0 //ADC0の先頭アドレスを格納した
#define ADC16_CHANNEL_GROUP 0U //ADC0
#define ADC16_USER_CHANNEL 1U //ADC0-SE1
/*******************************************************************************
* Prototypes
******************************************************************************/
/*******************************************************************************
* Variables
******************************************************************************/
volatile uint32_t g_systickCounter;
volatile uint32_t g_Adc16InterruptCounter = 0;
//ADC 12bitを利用する
uint32_t sensor_value;
/*******************************************************************************
* Code
******************************************************************************/
void SysTick_Handler(void)
{
if (g_systickCounter != 0U)
{
g_systickCounter--;
}
}
void SysTick_DelayTicks(uint32_t n)
{
g_systickCounter = n;
while (g_systickCounter != 0U)
{
}
}
/*!
* @brief Main function
*/
int main(void)
{
/* Board pin init */
BOARD_InitPins();
BOARD_InitBootClocks();
uint32_t tmp32;
if (SysTick_Config(SystemCoreClock / 1000U))
{
while (1)
{
}
}
//1- クロック設定(説明はまた今度)
static const clock_ip_name_t s_adc16Clocks[] = ADC16_CLOCKS;
CLOCK_EnableClock(s_adc16Clocks[0]);
//2- ADCの初期化
//2-1 ADC0_CFG1レジスタの設定
//ADICLK:クロックの設定
tmp32 = (0b11); //クロックソースはAsynchronousにした
//MODE:分解能の設定
tmp32 |=(0b01)<<2; //分解能は12bitに設定した
//ADLSMP:サンプルモード
tmp32 |= (0b0)<<4; //shortサンプルモード
//ADIV:分周比の設定
tmp32 |= (0b01)<<5; //分周比を2に設定
//ADLPC:省電力モード
tmp32 |= (0b0)<<7; //省電力機能はつかわない
//書き込み
ADC0->CFG1=tmp32;
//2-2 ADC0_CFG2レジスタ設定
//デフォルト設定を読む
tmp32 = ADC0->CFG2;
//ADHSC:高速変換モードの設定
tmp32 |= (0b0)<<2; //ノーマルモードを使用
//ADACKEN:Asynchronousクロック有効化
tmp32 |= (0b1)<<3; //Asynchronousクロックを使用許可
//書き込み
ADC0->CFG2=tmp32;
//2-3 ADC0_SC2レジスタ設定
//デフォルト設定を読む
tmp32= ADC0->SC2;
//REFSEL:参照電圧の設定
tmp32 |=(0b00); //VREFH,Lを使用
//ADTRG:変換実行のトリガ設定
tmp32 |=(0b0)<<6; //softwareトリガ
//書き込み
ADC0->SC2=tmp32;
//2-4 ADC0_SC3レジスタ設定
//デフォルト設定を読む
tmp32= ADC0->SC3;
//ADCO:連続変換モードの設定
tmp32=(0b1)<<3; //連続変換モードON
//書き込み
ADC0->SC3=tmp32;
//3- チャンネル設定&トリガ
uint32_t sc1;
//ADCH:入力チャンネルの設定
sc1 = (0b00001); //SE1を入力
//DIFF:差動電圧かsingle-endか
sc1 |= (0b0)<<5;
//AIEN: 割り込み設定
sc1 |= (0b0)<<6; //割り込みしない
//書き込み
ADC0->SC1[0]=sc1;
while (1)
{
//SC1レジスタCOCO:変換完了フラグ(bit7) 1なら変換完了
while (0U == ((0b1<<7) &
(ADC0->SC1[0] & (0b1<<7)) ))
{
}
//Rレジスタ 変換後の値がはいる
sensor_value = ADC0->R[0];
if (sensor_value>4000)
{
GPIO_PortClear(BOARD_LED_GPIO, 1u << BOARD_LED_GPIO_PIN);
SysTick_DelayTicks(1000U);
}
else
GPIO_PortSet(BOARD_LED_GPIO, 1u << BOARD_LED_GPIO_PIN);
}
}
API使うとこうなる
上の例はレジスタを直打ちするコードですが、サンプルプログラムのように用意されたAPIを利用する方法も考えます。
まず、APIを使うため、下のヘッダファイルをインクルードします。
#include "fsl_adc16.h" // ADCのライブラリ
重要な構造体二つを下のように宣言します。
adc16_config_t adc16ConfigStruct;
これはADCの初期化につかう構造体です。すでに説明したADCのレジスター設定情報を保持 します。F3キーでadc16_config_tを遡ると見おぼえのあるレジスターが定義されているとおもいます。
typedef struct _adc16_config
{
/* reference voltage source. */
adc16_reference_voltage_source_t referenceVoltageSource;
/* Select the input clock source to converter. */
adc16_clock_source_t clockSource;
/* Enable the asynchronous clock output. */
bool enableAsynchronousClock;
/* Select the divider of input clock source. */
adc16_clock_divider_t clockDivider;
/* Select the sample resolution mode. */
adc16_resolution_t resolution;
/* Select the long sample mode. */
adc16_long_sample_mode_t longSampleMode;
/* Enable the high-speed mode. */
bool enableHighSpeed;
/* Enable low power. */
bool enableLowPower;
/* Enable continuous conversion mode. */
bool enableContinuousConversion;
} adc16_config_t;
adc16_config_t adc16ConfigStruct;
これは、AD変換するチャンネル毎の設定です。チャンネル番号や割り込みの設定などの情報 を保持します。
typedef struct _adc16_channel_config
{
/* the conversion channel number. The available range is 0-31.*/
uint32_t channelNumber;
/* Generate an interrupt request once the
conversion is completed. */
bool enableInterruptOnConversionCompleted;
/* Using Differential sample mode. */
bool enableDifferentialConversion;
} adc16_channel_config_t;
使ってる関数
ADC16_GetDefaultConfig(&adc16ConfigStruct);
ADCの設定情報のDefault値を取得する関数です。いちいち定義するのが面倒なので、一般的 に使われる値が設定されます。まず、これを呼んで、値を設定してから、変更したい設定だ け、上書きしていく感じで使います。上で宣言した構造体をポインタで渡します。
ADC16_Init(ADC16_BASE, &adc16ConfigStruct);
初期化関数です、設定情報をそのまま、レズスターに書き込む関数です。第一引数はADC0つ まりADC0の先頭アドレスです。
ADC16_GetChannelStatusFlags(ADC16_BASE,ADC16_CHANNEL_GROUP)
変換完了のフラグをレジスタ(COCO)からひろってきます。1がかえると完了です。
ADC16_GetChannelConversionValue(ADC16_BASE, ADC16_CHANNEL_GROUP)
Rレジスタの読み値です。変換おわったら、これで値がかえってきます。
そんなところでしょうか。最後にAPI使用バージョンのプログラムを貼り付けておきます。
/*
ADCサンプルコード 人感センサでLチカ
*/
#include <stdio.h>
#include "board.h"
#include "pin_mux.h"
#include "clock_config.h"
#include "fsl_debug_console.h"
#include "fsl_adc16.h" // ADCのライブラリ
/*******************************************************************************
* Definitions
******************************************************************************/
#define BOARD_LED_GPIO BOARD_LED_RED_GPIO
#define BOARD_LED_GPIO_PIN BOARD_LED_RED_PIN
#define ADC16_BASE ADC0 //ADC0の先頭アドレスを格納した
#define ADC16_CHANNEL_GROUP 0U //ADC0
#define ADC16_USER_CHANNEL 1U //ADC0-SE1
/*******************************************************************************
* Prototypes
******************************************************************************/
/*******************************************************************************
* Variables
******************************************************************************/
volatile uint32_t g_systickCounter;
//ADC 12bitを利用する
uint32_t sensor_value;
/*******************************************************************************
* Code
******************************************************************************/
void SysTick_Handler(void)
{
if (g_systickCounter != 0U)
{
g_systickCounter--;
}
}
void SysTick_DelayTicks(uint32_t n)
{
g_systickCounter = n;
while (g_systickCounter != 0U)
{
}
}
/*!
* @brief Main function
*/
int main(void)
{
/* Board pin init */
BOARD_InitPins();
BOARD_InitBootClocks();
adc16_config_t adc16ConfigStruct;
ADC16_GetDefaultConfig(&adc16ConfigStruct);
adc16ConfigStruct.clockSource = kADC16_ClockSourceAsynchronousClock;
adc16ConfigStruct.clockDivider=kADC16_ClockDivider2;
adc16ConfigStruct.enableContinuousConversion = true;
adc16ConfigStruct.enableAsynchronousClock = true;
ADC16_Init(ADC16_BASE, &adc16ConfigStruct);
ADC16_EnableHardwareTrigger(ADC16_BASE, false); /* Make sure the software trigger is used. */
adc16_channel_config_t adc16ChannelConfigStruct;
adc16ChannelConfigStruct.channelNumber = ADC16_USER_CHANNEL;
adc16ChannelConfigStruct.enableInterruptOnConversionCompleted = false;
adc16ChannelConfigStruct.enableDifferentialConversion = false;
/* Set systick reload value to generate 1ms interrupt */
if (SysTick_Config(SystemCoreClock / 1000U))
{
while (1)
{
}
}
ADC16_SetChannelConfig(ADC16_BASE, ADC16_CHANNEL_GROUP, &adc16ChannelConfigStruct);
while (1)
{
while (0U == (kADC16_ChannelConversionDoneFlag &
ADC16_GetChannelStatusFlags(ADC16_BASE,ADC16_CHANNEL_GROUP)))
{
}
sensor_value = ADC16_GetChannelConversionValue(ADC16_BASE, ADC16_CHANNEL_GROUP);
if (sensor_value>4000)
{
GPIO_PortClear(BOARD_LED_GPIO, 1u << BOARD_LED_GPIO_PIN);
SysTick_DelayTicks(1000U);
}
else
GPIO_PortSet(BOARD_LED_GPIO, 1u << BOARD_LED_GPIO_PIN);
}
}
以上でおわりです。意外にながくなってしまいましたが、やってることはそんな難しいことはありませんでした。レジスタの役割さえわかってしまえば。
ADCばんざい。