BLEモジュールnRF52のPPIによるAD変換

# 記事の概要
Nordic社のBLEモジュールnRF52シリーズ（nRF52840やnRF52832など）において、PPIを用いてAD変換を実装します。


# AD変換とPPI

AD変換は、アナログ電圧をデジタルな値に変換する機能のことです。
PPIとはProgrammable Peripheral Interconnectの略で、CPUからの命令ではなく、周辺モジュールを連携して実行させます。CPUを介さないことで消費電力を低くすることができます。

今回の例では、PPIによりAD変換とタイマーをつないでいます。
低消費電力のSLEEPモード中にCPUを起こすことなく、タイマーの割り込みでAD変換を実行します。

本記事では以下のサンプルプログラムを用いて、この機能の設定方法を確認したく思います。

https://github.com/NordicPlayground/nRF52-ADC-examples/blob/master/ble_app_uart__saadc_timer_driven__scan_mode/main.c


## static変数
タイマーとPPIの実体とAD変換のデータを格納するバッファを作成しています。

```C
static const nrf_drv_timer_t   m_timer = NRF_DRV_TIMER_INSTANCE(3);
static nrf_saadc_value_t       m_buffer_pool[2][SAADC_SAMPLES_IN_BUFFER];
static nrf_ppi_channel_t       m_ppi_channel;
```

## 関数：saadc_init

AD変換モジュールの初期設定を行う関数です。

### 全ADチャンネル共通の設定
まずは全ADチャンネル共通の設定を行っています。

```c:
    nrf_drv_saadc_config_t saadc_config = NRF_DRV_SAADC_DEFAULT_CONFIG;
    saadc_config.resolution = NRF_SAADC_RESOLUTION_12BIT;
```

```c:
    err_code = nrf_drv_saadc_init(&saadc_config, saadc_callback);
    APP_ERROR_CHECK(err_code);
```

第2引数では、コールバック関数saadc_callbackを指定しています。
AD変換のイベント発生時に呼び出される関数です。
この関数の設定については後述します・

第1引数のnrf_drv_saadc_config_t のデータ構造は以下になります。

- resolution（AD変換の分解能）
    - NRF_SAADC_RESOLUTION_8BIT
    - NRF_SAADC_RESOLUTION_10BIT
    - NRF_SAADC_RESOLUTION_12BIT
    - NRF_SAADC_RESOLUTION_14BIT
- oversample（オーバーサンプリング）
    - NRF_SAADC_OVERSAMPLE_DISABLED
    -   NRF_SAADC_OVERSAMPLE_2X
    -   NRF_SAADC_OVERSAMPLE_4X
    -   NRF_SAADC_OVERSAMPLE_8X
    -   NRF_SAADC_OVERSAMPLE_16X
    -   NRF_SAADC_OVERSAMPLE_32X
    -   NRF_SAADC_OVERSAMPLE_64X
    -   NRF_SAADC_OVERSAMPLE_128X
    -   NRF_SAADC_OVERSAMPLE_256X
- interrupt_priority（割り込み優先度）
    - 優先度の数値を設定
- low_power_mode（低消費電力モード）
    - true：低消費電力モード有効
    - false：低消費電力モード無効
 	

ここで何も設定しない場合は、自動的にsdk_configファイルの以下の記述で設定した値になります。

```
// <e> SAADC_ENABLED - nrf_drv_saadc - SAADC peripheral driver - legacy layer
//==========================================================
#ifndef SAADC_ENABLED
#define SAADC_ENABLED 1
#endif
// <o> SAADC_CONFIG_RESOLUTION  - Resolution
 
// <0=> 8 bit 
// <1=> 10 bit 
// <2=> 12 bit 
// <3=> 14 bit 

#ifndef SAADC_CONFIG_RESOLUTION
#define SAADC_CONFIG_RESOLUTION 1
#endif

// <o> SAADC_CONFIG_OVERSAMPLE  - Sample period
 
// <0=> Disabled 
// <1=> 2x 
// <2=> 4x 
// <3=> 8x 
// <4=> 16x 
// <5=> 32x 
// <6=> 64x 
// <7=> 128x 
// <8=> 256x 

#ifndef SAADC_CONFIG_OVERSAMPLE
#define SAADC_CONFIG_OVERSAMPLE 0
#endif

// <q> SAADC_CONFIG_LP_MODE  - Enabling low power mode
 

#ifndef SAADC_CONFIG_LP_MODE
#define SAADC_CONFIG_LP_MODE 0
#endif

// <o> SAADC_CONFIG_IRQ_PRIORITY  - Interrupt priority
 

// <i> Priorities 0,2 (nRF51) and 0,1,4,5 (nRF52) are reserved for SoftDevice
// <0=> 0 (highest) 
// <1=> 1 
// <2=> 2 
// <3=> 3 
// <4=> 4 
// <5=> 5 
// <6=> 6 
// <7=> 7 

#ifndef SAADC_CONFIG_IRQ_PRIORITY
#define SAADC_CONFIG_IRQ_PRIORITY 6
#endif

// </e>
```

### 各ADチャンネルの設定
次いで各チャンネル個別の設定を行っています。

```c:
    nrf_saadc_channel_config_t channel_0_config =
        NRF_DRV_SAADC_DEFAULT_CHANNEL_CONFIG_SE(NRF_SAADC_INPUT_AIN4);
    channel_0_config.gain = NRF_SAADC_GAIN1_4;
    channel_0_config.reference = NRF_SAADC_REFERENCE_VDD4;
```

```
    err_code = nrf_drv_saadc_channel_init(0, &channel_0_config);
    APP_ERROR_CHECK(err_code);
```

nrf_saadc_channel_config_t のデータ構造は以下になります。
レジスタの各設定の意味は、nRF52のハードウェアマニュアルをご参照ください。
何も設定しないと初期値が適用されます。

- resistor_p
- resistor_n
    - NRF_SAADC_RESISTOR_DISABLED（初期値）
    - NRF_SAADC_RESISTOR_PULLDOWN
    - NRF_SAADC_RESISTOR_PULLUP
    - NRF_SAADC_RESISTOR_VDD1_2
- gain
    - NRF_SAADC_GAIN1_6（初期値）
    - NRF_SAADC_GAIN1_5
    - NRF_SAADC_GAIN1_4
    - NRF_SAADC_GAIN1_3
    - NRF_SAADC_GAIN1_2
    - NRF_SAADC_GAIN1
    - NRF_SAADC_GAIN2
    - NRF_SAADC_GAIN4
- reference
    - NRF_SAADC_REFERENCE_INTERNAL（初期値）
    - NRF_SAADC_REFERENCE_VDD4
- acq_time
    - NRF_SAADC_ACQTIME_3US
    - NRF_SAADC_ACQTIME_5US
    - NRF_SAADC_ACQTIME_10US（初期値）
    - NRF_SAADC_ACQTIME_15US
    - NRF_SAADC_ACQTIME_20US
    - NRF_SAADC_ACQTIME_40US
- mode
    - NRF_SAADC_MODE_SINGLE_ENDED（初期値）
    - NRF_SAADC_MODE_DIFFERENTIAL
- burst
    - NRF_SAADC_BURST_DISABLED（初期値）
    - NRF_SAADC_BURST_ENABLED

### ダブルバッファリングの設定
saadはダブルバッファリングをサポートしています。
以下の説明にあるように、AD変換したデータを１つのバッファに保存すると、次のデータは自動的にもう１つのバッファに保存されるようになります。

>The driver supports double buffering, which means that nrf_drv_saadc_buffer_convert can be called twice and the buffer that is provided in the second call will be used immediately after the first one is filled.

この方式の目的は、データの取りこぼしがないようにすることです。
AD変換したデータは関数nrf_drv_saadc_sampleを実行することでサンプリング（データをバッファに保存）されます。
このバッファの古いデータを処理している最中は、次のサンプリングができずに新たに取得したデータが消えてしまうかもしれません。
ですが次のバッファが用意されていれば、そちらにサンプリングできます。

ダブルバッファリングは関数nrf_drv_saadc_buffer_convertを2回繰り返すことで設定できます。

```C
    err_code = nrf_drv_saadc_buffer_convert(m_buffer_pool[0],SAADC_SAMPLES_IN_BUFFER);
    APP_ERROR_CHECK(err_code);   
    err_code = nrf_drv_saadc_buffer_convert(m_buffer_pool[1],SAADC_SAMPLES_IN_BUFFER);
    APP_ERROR_CHECK(err_code);
```

ダブルバッファリングを使用したくない場合は、nrf_drv_saadc_buffer_convertを1回だけ行えばいいはずです。
ですが、わざわざダブルバッファリングを使用しない理由が思いつきません。

## 関数：saadc_sampling_event_init
PPIとタイマーの初期設定を行い、タイマーイベントとAD変換のタスクを連携させます。

### PPIの初期化

PPIの初期化関数を呼び出します。

```C
    ret_code_t err_code;
    err_code = nrf_drv_ppi_init();
    APP_ERROR_CHECK(err_code);
```

### タイマーの初期化
タイマーの周期を設定してから初期化関数を呼び出します。

```C    
    nrf_drv_timer_config_t timer_config = NRF_DRV_TIMER_DEFAULT_CONFIG;
    timer_config.frequency = NRF_TIMER_FREQ_31250Hz;
    err_code = nrf_drv_timer_init(&m_timer, &timer_config, timer_handler);
    APP_ERROR_CHECK(err_code);
```

### タイマーのイベント設定
タイマーのイベントを発生させる周期を設定しています。
設定した時間経過時にイベントを発生させるコンペアマッチ割り込みを使っています。

```
    uint32_t ticks = nrf_drv_timer_ms_to_ticks(&m_timer,SAADC_SAMPLE_RATE);
    nrf_drv_timer_extended_compare(&m_timer, NRF_TIMER_CC_CHANNEL0, ticks, NRF_TIMER_SHORT_COMPARE0_CLEAR_MASK, false);
    nrf_drv_timer_enable(&m_timer);
```

### イベントのアドレス取得
後でタイマーとAD変換のイベントを連携させるために、イベントのアドレスを取得しておきます。

```
    uint32_t timer_compare_event_addr = nrf_drv_timer_compare_event_address_get(&m_timer, NRF_TIMER_CC_CHANNEL0);
    uint32_t saadc_sample_event_addr = nrf_drv_saadc_sample_task_get();
```

### タスクの割り当て
PPIチャンネルを有効化してから、タイマーのイベントにAD変換のタスクを関連付けます。
先ほどに取得したアドレスを関数nrf_drv_ppi_channel_assignに代入しています。

```
    err_code = nrf_drv_ppi_channel_alloc(&m_ppi_channel);
    APP_ERROR_CHECK(err_code);
    
    err_code = nrf_drv_ppi_channel_assign(m_ppi_channel, timer_compare_event_addr, saadc_sample_event_addr);
    APP_ERROR_CHECK(err_code);
```

## 関数：saadc_sampling_event_enable
PPIを有効化します。
この関数を実行するまでは、タイマーのイベントが発生しても、AD変換のタスクは呼び出されません。

```C
void saadc_sampling_event_enable(void)
{
    ret_code_t err_code = nrf_drv_ppi_channel_enable(m_ppi_channel);
    APP_ERROR_CHECK(err_code);
}
```

## 関数：saadc_callback

### イベント発生
関数nrf_drv_saadc_sampleを実行してサンプリングを行うとNRF_DRV_SAADC_EVT_DONEが発生します。

```C
if (p_event->type == NRF_DRV_SAADC_EVT_DONE)
```

### バッファの再設定
バッファにデータをサンプリングした後は、バッファの再設定を行わないといけません。
これはダブルバッファリングを使用する場合も、使用していない場合も同じです。

```C
        err_code = nrf_drv_saadc_buffer_convert(p_event->data.done.p_buffer, SAADC_SAMPLES_IN_BUFFER);
        APP_ERROR_CHECK(err_code);
```


ダブリングバッファの場合は以下の動作になります。

1. m_buffer_pool[0],m_buffer_pool[1]がサンプリングデータ待機
2. サンプリング実行し、m_buffer_pool[0]にデータ格納
3. nrf_drv_saadc_buffer_convertを実行してm_buffer_pool[0]再設定
4. m_buffer_pool[1],m_buffer_pool[0]がサンプリングデータ待機
5. サンプリング実行し、m_buffer_pool[1]にデータ格納
6. nrf_drv_saadc_buffer_convertを実行してm_buffer_pool[1]再設定
7. m_buffer_pool[0],m_buffer_pool[1]がサンプリングデータ待機


ダブリングバッファを使用しない場合は以下の動作になります。

1. m_buffer_pool[0]がサンプリングデータ待機
2. サンプリング実行し、m_buffer_pool[0]にデータ格納
3. nrf_drv_saadc_buffer_convertを実行してm_buffer_pool[0]再設定
4. m_buffer_pool[0]がサンプリングデータ待機

### AD変換データ取得

AD変換のデータは ` p_event->data.done.p_buffer[i]` で取得できます。
1回のサンプリングでSAADC_SAMPLES_IN_BUFFERで設定した数のデータが取得できています。
今回のプログラムでは4チャンネル分のAD変換データがあります。
AD変換の初期化時に関数nrf_drv_saadc_channel_initの第1引数で設定した順番にデータが格納されています。

```
        for (int i = 0; i < SAADC_SAMPLES_IN_BUFFER; i++)
        {
            printf("%d\r\n", p_event->data.done.p_buffer[i]);

            adc_value = p_event->data.done.p_buffer[i];
```

# サンプルプログラムの実行
サンプルプログラムを実行すると、SLEEPモードに遷移後も、周期的にAD変換を実行して、取得データをBLE通信します。

# PPIを使用しない場合



周期的に繰り返しAD変換を行う場合は、設定が少し複雑になりますが、PPIは低消費電力に有効な仕組みなので、利用できるならば使用した方がいいと考えます。

ですが不定期に、数回だけAD変換すればいいだけの場合は、周期的にAD変換を実行し続ける必要はありません。

その場合、PPIやタイマーを使用せずに、直接に関数nrf_drv_saadc_sampleを実行することでAD変換のデータをサンプリングできます。
ただし、その際も、コールバック関数saadc_callbackにおいてnrf_drv_saadc_buffer_convertによるバッファの再設定を忘れないようにご注意ください。

PPIとタイマーの設定を削除するか、関数saadc_sampling_event_enableを実行しなければタイマーとAD変換は関連付けられません。

# 備考
低消費電力のための機能は、PPI以外にはEasy DMAもあります。

例えばnRF52シリーズのI2C（NordicではTWIと呼称）はEasy DMAを使用できます。Easy DMAではPPIのTimerに相当する役割をRTCが果たしています。



# 参考資料
- [nRF52832のADC](https://qiita.com/yosukek/items/d87ef2edc1f696698b5a)
    - Noridicのsaadサンプルプログラムを用いてPPIによるAD変換について解説しています。本記事の作成にあたり、とても参考になりました。
- [nRF52でPPIを使ってGPIOTE EventでTIMER Taskを起動する](https://qiita.com/yosukek/items/ca0c202bf52c220e48fc)
    - PPIによるGPIOEとTimerの連携について解説しています。
- [BLE通信と共存するAD変換のGitHub](https://github.com/NordicPlayground/nRF52-ADC-examples/tree/master/ble_app_uart__saadc_timer_driven__scan_mode)
    - BLE通信を使用している場合のPPIによるAD変換についてのサンプルプログラムです。本記事はこのプログラムに基づいて解説しています。