STM32F303K8でDAC/DMAを使って正弦波を出力させる

![NUCLEO-F303K8](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/30370/86b2bd82-a551-bccc-11e3-153e0780bc7c.jpeg)

Cortex-M4 の DAC の性能を測るため、[**STM32F303K8**](http://akizukidenshi.com/catalog/g/gI-10790/) を搭載した [**NUCLEO-F303K8**](http://akizukidenshi.com/catalog/g/gM-10172/) を購入しました。
足がかりとして、まずは DMA を使って DAC を使用し、正弦波を出力させます。

# やりたいこと

- **DMA を介して DAC から正弦波を出力**
- **PLL を使用してシステムクロックを 64MHz へ逓倍し、DAC と DMA の動作を高速化**

DMA を使用せずとも、直接 DAC へデータを転送して出力できます。ただし 1 サンプルごとにデータ転送が必要になり、連続した信号を出力する場合は CPU リソースを消費します。DMA を用いると、DMA コントローラに転送元のメモリアドレスなどの種々の設定をして転送開始を命令するだけで、転送中は CPU リソースを消費しません。

今回は DMA のソースアドレスを固定し、サーキュラバッファとして使用します。そのため、バッファの長さによる CPU の処理負荷の増加もありません。

# DAC with DMA

[**こちらの記事に掲載されているプログラム**](https://qiita.com/amutou/items/3d055f264e109abdcb2d) を使いました。DAC と DMA については同じ処理を用いています。

今回の検証に用いた回路図と配線例を示します。
コンデンサは直流成分の除去のためのもので、今回は [**無極性電解コンデンサ**](http://akizukidenshi.com/catalog/g/gP-04624/) を使いました。A-GND 間が直流成分除去後の電圧、B-GND 間がDAC出力の電圧を表します。

| ![schematic](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/30370/b76daa93-0f98-81f2-97f5-bc0b098aa564.png) | ![wiring](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/30370/0e838e18-0e0d-e639-dca1-2c99ebcefa05.jpeg) |
|:---:|:---:|
| 回路図 | 配線例 |

## 出力周波数の算出

今回はバッファに正弦波の 1 周期を格納し DAC へ転送するため、出力される正弦波の周波数 $f_O$ は、システムクロック $f_{\rm HCLK}$、タイマ周期 $P$、バッファ長 $N$ とすると、

$f_O = \frac{f_{\rm HCLK}}{P}\frac{1}{N}$ [Hz]

と表せます。

つまり、DAC 出力の 1 サンプルあたりの時間が $\frac{P}{f_{\rm HCLK}}$ (s) であり、それを正弦波の 1 周期分のバッファを転送しきると $\frac{P}{f_{\rm HCLK}}N$ (s) かかることになります。

サンプリング周波数は 1 サンプルあたりの時間の逆数 $f_s = \frac{f_{\rm HCLK}}{P}$ [Hz] です。

式から、タイマ周期とバッファ長を小さな値にすれば出力周波数を高くできるとわかります。ただし、タイマ周期やバッファ長は自由に設定できるわけではありません。タイマ周期を DAC の処理能力を超えて短く設定はできませんし、バッファ長を小さくすると波形の量子化歪みが目立って精度が低下します。

また、システムクロック $f_{\rm HCLK}$ を高くしても出力周波数を高くできます。これを高くするために、PLL を用いてシステムクロックを変更します（後述）。

# DAC 出力の歪み

DAC 出力値の理想特性は Vpp = 3.3 V となります。実際の特性は出力波形の歪みが発生し、決して 3.3 V にはなりません。

以下に出力値波形の歪みの変化を示します。黄色の波形が直流成分除去後の電圧（A-GND 間）、青色の波形が DAC の出力電圧（B-GND 間）です。
最大振幅の 90% まではほぼ理想的な正弦波ですが、それを越えると歪み始め、出力値が 3.24 Vpp で頭打ちになります。

| **出力波形** | ![p50](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/30370/6c1191ef-39b9-edd6-6ae3-6bc2157f1a8a.png) | ![p75](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/30370/050028af-8955-d452-9fa8-6a952e94258d.png) | ![p90](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/30370/84a0b48d-e67b-479c-df38-855a2f662513.png) | ![p95](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/30370/27add0d3-315e-a406-2ceb-b8bb7efaddf4.png) | ![p100](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/30370/1de91ee4-e85c-125e-a203-63ce7ff7c3ba.png) |
|---:|:---:|:---:|:---:|:---:|:---:|
| **振幅** | 50% | 75% | 90% | 95% | 100% |
| **Vpp** | 1.76 V | 2.64 V | 3.08 V | 3.24 V | 3.24 V |
| **DACデータの値域** | 1024 - 3072 | 512 - 3584 | 205 - 3891 | 102 - 3994 | 0 - 4095 |

# HSI/PLL で 64MHz 化

| <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/30370/5a2d5f05-4e3d-eafe-6c79-29a1d5e6f10f.png"> |
|:---:|
| STM32CubeMX Clock Configuration |

F303K8 には水晶振動子が実装されていません。そのため HSI RC の **8 MHz** が使用されます。
しかし F303K8 は PLL が内蔵されており、最大 16 倍に逓倍して **64 MHz** をシステムクロックとして使用できます。実際は HSI RC の場合は逓倍前に 2 分周されます。

設定ポイントは以下のとおり:

1. PLLSRC として HSI の 2 分周を指定 (= 8 MHz / 2 = **4 MHz**)
1. PLLMUL として 16 倍 を指定 (= 4 MHz * 16 = **64 MHz**)
1. APB1 のクロック (PCLK1) が最大値の 36 MHz を越えてしまうため、2 分周を指定 (= 64 MHz / 2 = **32 MHz**)
1. フラッシュアクセス制御レジスタにてレイテンシを **2** に設定
1. PLL を有効化 
1. 最後に SYSCLK のソースとして PLLCLK を指定

上記の設定を行うと、メモリ、DMA も HCLK を使用するために 64 MHz で駆動します。

今回は内容理解のためブロック図を STM32CubeMX で確認し、プログラムは生成コードを使わず、[**こちらに掲載のプログラム**](http://blueeyes.sakura.ne.jp/2017/08/17/303/) を使いました。記事中では各設定項目についてさらに詳しい解説があります。

# ソース全文と結果

以下に今回使ったソースを示します。

```cpp
#include "fastmath.h"
#include "stm32f3xx_hal.h"

#define TIM3_PERIOD 16

const uint16_t N   = 128;
uint16_t buffer[N] = {};

void clock_init() {
  RCC->CFGR |= RCC_CFGR_PLLSRC_HSI_DIV2 |  // PLL Source <= HSI / 2 = 4MHz
               RCC_CFGR_PLLMUL16 |         // PLLMul = x16
               RCC_CFGR_PPRE1_DIV2;        // APB1 <= HCLK / 2 = 32MHz (HCLK = PLLCLK / 1)

  FLASH->ACR |= FLASH_ACR_LATENCY_2;  // flash access latency for 48 < HCLK <= 72.
                                      // This statement must be placed immediately after PLL multiplication.

  RCC->CR |= RCC_CR_PLLON;
  while (!(RCC->CR & RCC_CR_PLLRDY)) ;  // wait until PLL is ready

  RCC->CFGR |= RCC_CFGR_SW_PLL;                             // PLL as system clock
  while ((RCC->CFGR & RCC_CFGR_SWS) != RCC_CFGR_SWS_PLL) ;  // wait until PLL clock supply starts
}

int main(void) {
  clock_init();

  for (uint16_t i = 0; i < N; i++) {
    buffer[i] = static_cast<uint16_t>(sinf(i * 2 * 3.14159f / N) * 1024 + 2048);
  }

  __HAL_RCC_GPIOA_CLK_ENABLE();
  __HAL_RCC_DAC1_CLK_ENABLE();
  __HAL_RCC_DMA1_CLK_ENABLE();
  __HAL_RCC_TIM3_CLK_ENABLE();

  GPIO_InitTypeDef gpio_dac1_1 = {
    .Pin  = GPIO_PIN_4,
    .Mode = GPIO_MODE_ANALOG,
  };
  HAL_GPIO_Init(GPIOA, &gpio_dac1_1);

  DAC_HandleTypeDef hdac = {
    .Instance = DAC1,
  };
  HAL_DAC_Init(&hdac);

  DAC_ChannelConfTypeDef dac_conf = {
    .DAC_Trigger      = DAC_TRIGGER_T3_TRGO,
    .DAC_OutputBuffer = DAC_OUTPUTBUFFER_ENABLE,
  };
  HAL_DAC_ConfigChannel(&hdac, &dac_conf, DAC_CHANNEL_1);

  TIM_HandleTypeDef htim = {
    .Instance = TIM3,
    .Init =
      TIM_Base_InitTypeDef {
        .Prescaler   = 0,
        .CounterMode = TIM_COUNTERMODE_UP,
        .Period      = TIM3_PERIOD - 1,
      },
  };
  HAL_TIM_Base_Init(&htim);

  TIM_MasterConfigTypeDef tim_master = {
    .MasterOutputTrigger = TIM_TRGO_UPDATE,
  };
  HAL_TIMEx_MasterConfigSynchronization(&htim, &tim_master);

  DMA_HandleTypeDef hdma_dac1_1 = {
    .Instance = DMA1_Channel3,
    .Init =
      DMA_InitTypeDef {
        .Direction           = DMA_MEMORY_TO_PERIPH,
        .PeriphInc           = DMA_PINC_DISABLE,
        .MemInc              = DMA_MINC_ENABLE,
        .PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD,
        .MemDataAlignment    = DMA_MDATAALIGN_HALFWORD,
        .Mode                = DMA_CIRCULAR,
        .Priority            = DMA_PRIORITY_MEDIUM,
      },
  };
  HAL_DMA_Init(&hdma_dac1_1);
  hdac.DMA_Handle1 = &hdma_dac1_1;

  __HAL_RCC_SYSCFG_CLK_ENABLE();
  SYSCFG->CFGR1 |= SYSCFG_CFGR1_DAC1_TRIG1_RMP | SYSCFG_CFGR1_TIM6DAC1Ch1_DMA_RMP;

  HAL_DAC_Start_DMA(&hdac, DAC_CHANNEL_1, reinterpret_cast<uint32_t *>(buffer), N, DAC_ALIGN_12B_R);
  HAL_TIM_Base_Start(&htim);

  while (1) ;
}
```

プログラムではタイマ周期 $P = 16$、バッファ長 $N = 128$ としました。サンプリング周波数は 4 MHz、期待される出力周波数は $f_O = 31250$ [Hz] となります。計算通りの周波数で正弦波が出力されました。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/30370/6c1191ef-39b9-edd6-6ae3-6bc2157f1a8a.png" width="600">

# 参考資料

- **STM32いじってみた(4) PLLクロック編**<br />
  http://blueeyes.sakura.ne.jp/2017/08/17/303/
- **STM32F3のDACで正弦波を出す**<br />
  https://qiita.com/amutou/items/3d055f264e109abdcb2d
- **NUCLEO-F303K8で1Mbps RS232通信を行う。**<br />
  https://qiita.com/john256/items/706ad1c6ff57e07877bf
- **miniDDSとテーブル補間**<br />
  http://elm-chan.org/junk/mdds_ipol/report_j.html
- **32 DACをDMAで動かしてsine/cos波を発生させる (DAC,DMA,TIM2,TIM3,UART,LEDチカチカ)**<br />
  https://sites.google.com/site/stm32datasheet/home/060-STM32-program-smpls/32-dac-dma